Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ReferenceManyInput #484

Closed
wants to merge 24 commits into from
Closed

Add ReferenceManyInput #484

wants to merge 24 commits into from

Conversation

kimkha
Copy link
Contributor

@kimkha kimkha commented Mar 20, 2017

I finished issue #45 . Please review.

@fzaninotto
Copy link
Member

Can you please PR against the next branch?

@kimkha
Copy link
Contributor Author

kimkha commented Mar 21, 2017

OK.

@kimkha kimkha changed the base branch from master to next March 21, 2017 00:51
@kimkha
Copy link
Contributor Author

kimkha commented Mar 21, 2017

DONE. But look like no new travis build against next branch, right?

@kimkha
Copy link
Contributor Author

kimkha commented Mar 22, 2017

@fzaninotto Is it OK?

Copy link
Member

@fzaninotto fzaninotto left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super encouraging, looking forward to merging this one! Your PR needs a few changes before that.

@@ -0,0 +1,222 @@
import React, {Component, PropTypes} from 'react';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CS: please add spaces inside brackets. {Component, PropTypes} => { Component, PropTypes }

This happens in many places in this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:D The IntelliJ auto formatting...

const noFilter = () => true;

/**
* An Input component for choosing many reference records. Useful for 'hasMany' relationship.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to show the expected value in the record (an array of ids)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean, I need to provide detailed comment?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

*
* Pass possible options as an array of objects in the 'choices' attribute.
* @example
* const choices = [ 'Male', 'Female' ];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't agree with the addition of this type prop. It complicates things IMO. Keep to the same API as SelectInput and remove the type prop.

const {pagination, sort, filter} = this.params;
const ids = input.value;
if (ids && Array.isArray(ids)) {
this.props.crudGetMany(reference, ids);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should probably throw an explicit error if ids is not an array.

* <SelectManyInput type="object" optionText="title" />
* </ReferenceManyInput>
*/
export class ReferenceManyInput extends Component {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my 2c: as there is a lot of common code with ReferenceInput, why don't you extend ReferenceInput instead of Component?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will try to do so... :)

export class SelectManyInput extends Component {
// Comment input onBlur because SelectManyInput don't pass correct values on this event
handleBlur = (eventOrValue) => {
// this.props.onBlur(eventOrValue);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

commented out code

* @see https://github.com/TeamWertarbyte/material-ui-chip-input
*/
export class SelectManyInput extends Component {
// Comment input onBlur because SelectManyInput don't pass correct values on this event
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand, can you elaborate? Not catching onBlur or onFocus from the child component will probably break redux-form validation

};
// Comment input onFocus because SelectManyInput don't pass correct values on this event
handleFocus = (event) => {
// this.props.onFocus(event);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same, please don't commit commented out code


handleChange = (eventOrValue) => {
if (this.props.type == 'object') {
eventOrValue = this.extractIds(eventOrValue);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't modify a parameter of the function. Use a new const instead

type,
} = this.props;

// Always use uncontrolled mode
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand. On the contrary, we should use controlled mode, shouldn't we?

@fzaninotto
Copy link
Member

Any updates on this one?

@kimkha
Copy link
Contributor Author

kimkha commented Mar 27, 2017

Sorry, I'm on vacation now, I will back in 2 more days... :)

@DjLeChuck
Copy link
Contributor

DjLeChuck commented Mar 31, 2017

I'm very interested in this component so I try to implement it with your code.

Some points I see (I'm new with redux-form/admin-on-rest so maybe I'm wrong!):

FYI, my onRequestAdd and onRequestDelete:

    handleAdd = (newValue) => {
      const { input: { value, onChange } } = this.props;

      onChange([...value, ...[newValue.id]]);
    };

    handleDelete = (newValue) => {
      const { input: { value, onChange } } = this.props;

      onChange(value.filter(v => (v != newValue)));
    };

EDIT:
I don't know why but if I don't pass onBlur, all my chips disappear when I loose focus. There is a default behaviour about onBlur and redux-form?

EDIT2:
Calling onBlur and onFocus from props works:

    <ChipInput
        onBlur={() => this.props.onBlur(defaultValue)}
        onFocus={() => this.props.onFocus(defaultValue)}
    />

@DjLeChuck
Copy link
Contributor

In ReferenceManyInput::mapStateToProps, you should call getPossibleReferences even if there is no referenceIds otherwise the autocomplete list will not show up when the field is empty.

function mapStateToProps(state, props) {
    const referenceIds = props.input.value;
    const referenceRecords = [];
    let matchingReferences = [];
    const data = state.admin[props.reference].data;

    if (!referenceIds.length) {
        matchingReferences = getPossibleReferences(state, referenceSource(props.resource, props.source), props.reference);
    } else {
        for (let i = 0; i < referenceIds.length; i++) {
            if (data[referenceIds[i]]) {
                referenceRecords.push(data[referenceIds[i]]);
                const possibleReferences = getPossibleReferences(state, referenceSource(props.resource, props.source), props.reference, referenceIds[i]);
                matchingReferences = Object.assign(matchingReferences, possibleReferences);
            }
        }
    }

    return {
        referenceRecords: referenceRecords,
        matchingReferences: matchingReferences,
    }
}

In SelectManyInput, I have add onUpdateInput={this.props.setFilter} on <ChipInput /> so the data are fetch when the user's input change.

However, the setFilter function is not good for me. The received value is a string (the search text) and it's compared to this.params.filter which is an object so the condition is always false and data are always fetch.

Further, when the function is called, the default filters are erased by filterToQuery. For example my first call:

includeDisabled:false
fields:title
sort:title
limit:25
skip:0

My second call:

q:apprends

I know you use the same code as ReferenceInput and I can override filterToQuery but it's a strange behaviour IMO. Why not let the default filters and only add the search one? Maybe @fzaninotto you can explain me your thinking behind this usage?

@fzaninotto
Copy link
Member

any ETA on this one? We'll be releasing 1.0 within a week, and I'd love to see it merged.

@kimkha
Copy link
Contributor Author

kimkha commented Apr 6, 2017

ReferenceManyInput look same with ReferenceInput, but from recommendation of FB, we shouldn't use inheritance way (see https://facebook.github.io/react/docs/composition-vs-inheritance.html#so-what-about-inheritance)

@kimkha
Copy link
Contributor Author

kimkha commented Apr 6, 2017

@fzaninotto Please check again! 😄

Copy link
Member

@fzaninotto fzaninotto left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still some work to do. Also, could you document the two fields in docs/Inputs.md?

*
* @example
* {
* postIds: [ "1", "23", "4" ]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The following example (and the ones below) uses post_id and this one postIds. Please make them consistent. I think it should be post_ids everywhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

choices: matchingReferences,
basePath,
onChange,
filter: noFilter, // for AutocompleteInput
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bump

filter: {},
filterToQuery: searchText => ({ q: searchText }),
matchingReferences: [],
meta: {},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed this one from other components because it has side effects, please remove it here, too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you mean the filter and filterToQuery?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, I mean meta. Use other components as models.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK

if (!referenceIds.length) {
matchingReferences = getPossibleReferences(state, referenceSource(props.resource, props.source), props.reference);
} else {
for (let i = 0; i < referenceIds.length; i++) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be placed in a selector, otherwise it's executed for every global state change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see... But what should I do?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

create a selector in the reducer - there are others like it (see https://github.com/marmelab/admin-on-rest/blob/master/src/reducer/references/oneToMany.js#L19-L29)

import FieldTitle from '../../util/FieldTitle';

/**
* An Input component for a array
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/for a array/for an array/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😄

* An Input component for a array
*
* @example
* <SelectManyInput source="first_name" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bad example. How can first_name be an array? Same for the rest of the comments for that component.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK

};

extractIds = (eventOrValue) => {
let value = eventOrValue;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make it a const by using a ternary expression.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK

return this.props.choices.filter((o) => ids.indexOf(o[this.props.optionValue]) >= 0);
} else {
return ids.map((id) => {
let o = {};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better return an object literal directly:

return ids.map((id) => ({
   [this.props.optionValue]: id,
   [this.props.optionText]: id,
}))

Also, how come you use the id both for value and text?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will change the variable name... 👍


handleChange = (eventOrValue) => {
var extracted = this.extractIds(eventOrValue);
this.props.onChange(extracted);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's this onChange for? Same for the other custom handlers (onBlur, etc): they don't exist in other components, so it doesn't make sense to add them just for that component

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure... I copied from TextInput, what's it for?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the doc:

Some other props are progressively implemented. The <TextInput /> and <NumberInput /> inputs also accept following props:
onBlur: [...]
onChange: [...]
onFocus: [...]

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my bad, you're right

} = this.props;

// Convert the name of fields in choices
options['dataSourceConfig'] = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please use dot notation. same for dataSource

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

Copy link
Member

@fzaninotto fzaninotto left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're getting very close. To do before merge:

  • Move the logic from mapStateToProps to a selector
  • Add documentation for the 2 new inputs


if (this.props.choices && this.props.choices.length > 0) {
return this.props.choices.filter((o) => values.indexOf(o[this.props.optionValue]) >= 0);
} else {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for an else after a return

@DjLeChuck
Copy link
Contributor

Please, what about the onUpdateInput={this.props.setFilter} property on <ChipInput />?

Actualy, only one request is made to load possible values. If the user type text it only filter loaded values and not make aditionnal requests. It's the good behaviour @fzaninotto ?

@fzaninotto
Copy link
Member

I don't know how <ChipInput> is supposed to work. But if there is a problem in your tests, please advise @kimkha about it.

@DjLeChuck
Copy link
Contributor

Sorry, my question is not about <ChipInput /> but how the component should work. My question can be the same with the component ReferenceInput. When the user type text in the field, this should only filter loaded documents or it should do additional request with the searches term (using filterToQuery)?

@fzaninotto
Copy link
Member

The ChipInput should pass the new search text up to ReferenceManyInput, which should issue a new request.

@kimkha
Copy link
Contributor Author

kimkha commented Apr 7, 2017

So, I think that we should remove filter on SelectManyInput, right?

@DjLeChuck
Copy link
Contributor

Actually it's on <ReferenceManyInput />, not <SelectManyInput /> 😄

@kimkha
Copy link
Contributor Author

kimkha commented Apr 7, 2017

Ah, I want to make ReferenceManyInput to handle all filter request, not ChipInput. Because ChipInput is managed by SelectManyInput, but SelectManyInput can use like other inputs (put directly on <Edit /> or <Create />). So, handling filter in SelectManyInput (I mean passing filter from ReferenceManyInput) will make something messed.

And, later, I think we also support CheckboxGroupInput as a child of ReferenceManyInput...

@DjLeChuck
Copy link
Contributor

DjLeChuck commented Apr 7, 2017

Yes, ReferenceManyInput should handle filtering that's what @fzaninotto said! 😄

But SelectManyInput should tell to ReferenceManyInput to launch a new request when the user type in the input. That's the aim of onUpdateInput property (the doc of ChipInput says Callback function that is called when the input changes (useful for auto complete).)

@kimkha
Copy link
Contributor Author

kimkha commented Apr 7, 2017

Ah, sorry because I didn't understand what you mean...

I will implement it soon. :D

@kimkha
Copy link
Contributor Author

kimkha commented Apr 7, 2017

@DjLeChuck Done for onUpdateInput

@kimkha
Copy link
Contributor Author

kimkha commented Apr 7, 2017

@fzaninotto About map state, let me explain why I need 2 params:

  • referenceRecords is used to display current selection (especially on Edit page)
  • matchingReferences is used to filter the input results (auto complete)

But to make it display correctly, I must join them to a single param (matchingReferences in my case), so that I can send matchingReferences to ChipInput to display both (the current selection and the chosen list).

If we want to move to a reducer selector, we must find out better solution. What should I do for that?

@DjLeChuck
Copy link
Contributor

DjLeChuck commented Apr 13, 2017

I have a strange behaviour and I don't know if it's my fault or not.
Using this code in Edit, the object has 2 themes:

<ReferenceManyInput label="Thèmes" source="themes" reference="themes" allowEmpty>
    <SelectManyInput optionText="label" options={themesSelectStyle} />
</ReferenceManyInput>
  • If I come from the listing: No Chips
  • If I hit directly the URL (or reload the page): Chips but with ID, not the label
  • If I remove allowEmpty and come from the listing: Chips with label
  • If I remove allowEmpty hit directly the URL (or reload the page): Only the first Chip with label

@fzaninotto fzaninotto mentioned this pull request Apr 25, 2017
7 tasks
@fzaninotto
Copy link
Member

With all the rebases and merges it was hard to keep track of the code, so I've squashed your commits (keeping you as the author) and started a new PR (#597) which I'll finish.

@fzaninotto fzaninotto closed this Apr 25, 2017
@kimkha
Copy link
Contributor Author

kimkha commented Apr 25, 2017

Many thanks... :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants