feat(widget-list): add hiding list content with minimize_collapsed option (#3607)
This commit is contained in:
parent
088b1a8ab6
commit
4dd58c5dcb
@ -115,14 +115,15 @@ export default class ListControl extends React.Component {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
const { field, value } = props;
|
const { field, value } = props;
|
||||||
const allItemsCollapsed = field.get('collapsed', true);
|
const listCollapsed = field.get('collapsed', true);
|
||||||
const itemsCollapsed = value && Array(value.size).fill(allItemsCollapsed);
|
const itemsCollapsed = (value && Array(value.size).fill(listCollapsed)) || [];
|
||||||
const keys = value && Array.from({ length: value.size }, () => uuid());
|
const keys = (value && Array.from({ length: value.size }, () => uuid())) || [];
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
itemsCollapsed: List(itemsCollapsed),
|
listCollapsed,
|
||||||
|
itemsCollapsed,
|
||||||
value: valueToString(value),
|
value: valueToString(value),
|
||||||
keys: List(keys),
|
keys,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,8 +185,8 @@ export default class ListControl extends React.Component {
|
|||||||
? this.singleDefault()
|
? this.singleDefault()
|
||||||
: fromJS(this.multipleDefault(field.get('fields')));
|
: fromJS(this.multipleDefault(field.get('fields')));
|
||||||
this.setState({
|
this.setState({
|
||||||
itemsCollapsed: this.state.itemsCollapsed.push(false),
|
itemsCollapsed: [...this.state.itemsCollapsed, false],
|
||||||
keys: this.state.keys.push(uuid()),
|
keys: [...this.state.keys, uuid()],
|
||||||
});
|
});
|
||||||
onChange((value || List()).push(parsedValue));
|
onChange((value || List()).push(parsedValue));
|
||||||
};
|
};
|
||||||
@ -202,8 +203,8 @@ export default class ListControl extends React.Component {
|
|||||||
const { value, onChange } = this.props;
|
const { value, onChange } = this.props;
|
||||||
const parsedValue = fromJS(this.mixedDefault(typeKey, type));
|
const parsedValue = fromJS(this.mixedDefault(typeKey, type));
|
||||||
this.setState({
|
this.setState({
|
||||||
itemsCollapsed: this.state.itemsCollapsed.push(false),
|
itemsCollapsed: [...this.state.itemsCollapsed, false],
|
||||||
keys: this.state.keys.push(uuid()),
|
keys: [...this.state.keys, uuid()],
|
||||||
});
|
});
|
||||||
onChange((value || List()).push(parsedValue));
|
onChange((value || List()).push(parsedValue));
|
||||||
};
|
};
|
||||||
@ -300,7 +301,10 @@ export default class ListControl extends React.Component {
|
|||||||
? { [collectionName]: metadata.removeIn(metadataRemovePath) }
|
? { [collectionName]: metadata.removeIn(metadataRemovePath) }
|
||||||
: metadata;
|
: metadata;
|
||||||
|
|
||||||
this.setState({ itemsCollapsed: itemsCollapsed.delete(index), keys: keys.delete(index) });
|
itemsCollapsed.splice(index, 1);
|
||||||
|
keys.splice(index, 1);
|
||||||
|
|
||||||
|
this.setState({ itemsCollapsed: [...itemsCollapsed], keys: [...keys] });
|
||||||
|
|
||||||
onChange(value.remove(index), parsedMetadata);
|
onChange(value.remove(index), parsedMetadata);
|
||||||
clearFieldErrors();
|
clearFieldErrors();
|
||||||
@ -314,16 +318,35 @@ export default class ListControl extends React.Component {
|
|||||||
handleItemCollapseToggle = (index, event) => {
|
handleItemCollapseToggle = (index, event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const { itemsCollapsed } = this.state;
|
const { itemsCollapsed } = this.state;
|
||||||
const collapsed = itemsCollapsed.get(index);
|
const newItemsCollapsed = itemsCollapsed.map((collapsed, itemIndex) => {
|
||||||
this.setState({ itemsCollapsed: itemsCollapsed.set(index, !collapsed) });
|
if (index === itemIndex) {
|
||||||
|
return !collapsed;
|
||||||
|
}
|
||||||
|
return collapsed;
|
||||||
|
});
|
||||||
|
this.setState({
|
||||||
|
itemsCollapsed: newItemsCollapsed,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
handleCollapseAllToggle = e => {
|
handleCollapseAllToggle = e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const { value } = this.props;
|
const { value, field } = this.props;
|
||||||
const { itemsCollapsed } = this.state;
|
const { itemsCollapsed, listCollapsed } = this.state;
|
||||||
|
const minimizeCollapsedItems = field.get('minimize_collapsed', false);
|
||||||
|
const listCollapsedByDefault = field.get('collapsed', true);
|
||||||
const allItemsCollapsed = itemsCollapsed.every(val => val === true);
|
const allItemsCollapsed = itemsCollapsed.every(val => val === true);
|
||||||
this.setState({ itemsCollapsed: List(Array(value.size).fill(!allItemsCollapsed)) });
|
|
||||||
|
if (minimizeCollapsedItems) {
|
||||||
|
let updatedItemsCollapsed = itemsCollapsed;
|
||||||
|
// Only allow collapsing all items in this mode but not opening all at once
|
||||||
|
if (!listCollapsed || !listCollapsedByDefault) {
|
||||||
|
updatedItemsCollapsed = Array(value.size).fill(!listCollapsed);
|
||||||
|
}
|
||||||
|
this.setState({ listCollapsed: !listCollapsed, itemsCollapsed: updatedItemsCollapsed });
|
||||||
|
} else {
|
||||||
|
this.setState({ itemsCollapsed: Array(value.size).fill(!allItemsCollapsed) });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
objectLabel(item) {
|
objectLabel(item) {
|
||||||
@ -368,11 +391,18 @@ export default class ListControl extends React.Component {
|
|||||||
this.props.onChange(newValue);
|
this.props.onChange(newValue);
|
||||||
|
|
||||||
// Update collapsing
|
// Update collapsing
|
||||||
const collapsed = itemsCollapsed.get(oldIndex);
|
const collapsed = itemsCollapsed[oldIndex];
|
||||||
const updatedItemsCollapsed = itemsCollapsed.delete(oldIndex).insert(newIndex, collapsed);
|
itemsCollapsed.splice(oldIndex, 1);
|
||||||
|
const updatedItemsCollapsed = [...itemsCollapsed];
|
||||||
|
updatedItemsCollapsed.splice(newIndex, 0, collapsed);
|
||||||
|
|
||||||
// Reset item to ensure updated state
|
// Reset item to ensure updated state
|
||||||
const updatedKeys = keys.set(oldIndex, uuid()).set(newIndex, uuid());
|
const updatedKeys = keys.map((key, keyIndex) => {
|
||||||
|
if (keyIndex === oldIndex || keyIndex === newIndex) {
|
||||||
|
return uuid();
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
});
|
||||||
this.setState({ itemsCollapsed: updatedItemsCollapsed, keys: updatedKeys });
|
this.setState({ itemsCollapsed: updatedItemsCollapsed, keys: updatedKeys });
|
||||||
|
|
||||||
//clear error fields and remove old validations
|
//clear error fields and remove old validations
|
||||||
@ -394,8 +424,8 @@ export default class ListControl extends React.Component {
|
|||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const { itemsCollapsed, keys } = this.state;
|
const { itemsCollapsed, keys } = this.state;
|
||||||
const collapsed = itemsCollapsed.get(index);
|
const collapsed = itemsCollapsed[index];
|
||||||
const key = keys.get(index);
|
const key = keys[index];
|
||||||
let field = this.props.field;
|
let field = this.props.field;
|
||||||
|
|
||||||
if (this.getValueType() === valueTypes.MIXED) {
|
if (this.getValueType() === valueTypes.MIXED) {
|
||||||
@ -473,11 +503,14 @@ export default class ListControl extends React.Component {
|
|||||||
|
|
||||||
renderListControl() {
|
renderListControl() {
|
||||||
const { value, forID, field, classNameWrapper } = this.props;
|
const { value, forID, field, classNameWrapper } = this.props;
|
||||||
const { itemsCollapsed } = this.state;
|
const { itemsCollapsed, listCollapsed } = this.state;
|
||||||
const items = value || List();
|
const items = value || List();
|
||||||
const label = field.get('label', field.get('name'));
|
const label = field.get('label', field.get('name'));
|
||||||
const labelSingular = field.get('label_singular') || field.get('label', field.get('name'));
|
const labelSingular = field.get('label_singular') || field.get('label', field.get('name'));
|
||||||
const listLabel = items.size === 1 ? labelSingular.toLowerCase() : label.toLowerCase();
|
const listLabel = items.size === 1 ? labelSingular.toLowerCase() : label.toLowerCase();
|
||||||
|
const minimizeCollapsedItems = field.get('minimize_collapsed', false);
|
||||||
|
const allItemsCollapsed = itemsCollapsed.every(val => val === true);
|
||||||
|
const selfCollapsed = allItemsCollapsed && (listCollapsed || !minimizeCollapsedItems);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ClassNames>
|
<ClassNames>
|
||||||
@ -499,15 +532,17 @@ export default class ListControl extends React.Component {
|
|||||||
heading={`${items.size} ${listLabel}`}
|
heading={`${items.size} ${listLabel}`}
|
||||||
label={labelSingular.toLowerCase()}
|
label={labelSingular.toLowerCase()}
|
||||||
onCollapseToggle={this.handleCollapseAllToggle}
|
onCollapseToggle={this.handleCollapseAllToggle}
|
||||||
collapsed={itemsCollapsed.every(val => val === true)}
|
collapsed={selfCollapsed}
|
||||||
/>
|
|
||||||
<SortableList
|
|
||||||
items={items}
|
|
||||||
renderItem={this.renderItem}
|
|
||||||
onSortEnd={this.onSortEnd}
|
|
||||||
useDragHandle
|
|
||||||
lockAxis="y"
|
|
||||||
/>
|
/>
|
||||||
|
{(!selfCollapsed || !minimizeCollapsedItems) && (
|
||||||
|
<SortableList
|
||||||
|
items={items}
|
||||||
|
renderItem={this.renderItem}
|
||||||
|
onSortEnd={this.onSortEnd}
|
||||||
|
useDragHandle
|
||||||
|
lockAxis="y"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</ClassNames>
|
</ClassNames>
|
||||||
|
@ -20,6 +20,7 @@ jest.mock('netlify-cms-ui-default', () => {
|
|||||||
const actual = jest.requireActual('netlify-cms-ui-default');
|
const actual = jest.requireActual('netlify-cms-ui-default');
|
||||||
const ListItemTopBar = props => (
|
const ListItemTopBar = props => (
|
||||||
<mock-list-item-top-bar {...props} onClick={props.onCollapseToggle}>
|
<mock-list-item-top-bar {...props} onClick={props.onCollapseToggle}>
|
||||||
|
<button onClick={props.onRemove}>Remove</button>
|
||||||
{props.children}
|
{props.children}
|
||||||
</mock-list-item-top-bar>
|
</mock-list-item-top-bar>
|
||||||
);
|
);
|
||||||
@ -454,4 +455,177 @@ describe('ListControl', () => {
|
|||||||
);
|
);
|
||||||
expect(getByText('hello - world - index.md')).toBeInTheDocument();
|
expect(getByText('hello - world - index.md')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should render list with fields with default collapse ("true") and minimize_collapsed ("false")', () => {
|
||||||
|
const field = fromJS({
|
||||||
|
name: 'list',
|
||||||
|
label: 'List',
|
||||||
|
fields: [{ label: 'String', name: 'string', widget: 'string' }],
|
||||||
|
});
|
||||||
|
const { asFragment, getByTestId } = render(
|
||||||
|
<ListControl
|
||||||
|
{...props}
|
||||||
|
field={field}
|
||||||
|
value={fromJS([{ string: 'item 1' }, { string: 'item 2' }])}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getByTestId('styled-list-item-top-bar-0')).toHaveAttribute('collapsed', 'true');
|
||||||
|
expect(getByTestId('styled-list-item-top-bar-1')).toHaveAttribute('collapsed', 'true');
|
||||||
|
|
||||||
|
expect(getByTestId('object-control-0')).toHaveAttribute('collapsed', 'true');
|
||||||
|
expect(getByTestId('object-control-1')).toHaveAttribute('collapsed', 'true');
|
||||||
|
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render list with fields with collapse = "false" and default minimize_collapsed ("false")', () => {
|
||||||
|
const field = fromJS({
|
||||||
|
name: 'list',
|
||||||
|
label: 'List',
|
||||||
|
collapsed: false,
|
||||||
|
fields: [{ label: 'String', name: 'string', widget: 'string' }],
|
||||||
|
});
|
||||||
|
const { asFragment, getByTestId } = render(
|
||||||
|
<ListControl
|
||||||
|
{...props}
|
||||||
|
field={field}
|
||||||
|
value={fromJS([{ string: 'item 1' }, { string: 'item 2' }])}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getByTestId('styled-list-item-top-bar-0')).toHaveAttribute('collapsed', 'false');
|
||||||
|
expect(getByTestId('styled-list-item-top-bar-1')).toHaveAttribute('collapsed', 'false');
|
||||||
|
|
||||||
|
expect(getByTestId('object-control-0')).toHaveAttribute('collapsed', 'false');
|
||||||
|
expect(getByTestId('object-control-1')).toHaveAttribute('collapsed', 'false');
|
||||||
|
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render list with fields with default collapse ("true") and minimize_collapsed = "true"', () => {
|
||||||
|
const field = fromJS({
|
||||||
|
name: 'list',
|
||||||
|
label: 'List',
|
||||||
|
minimize_collapsed: true,
|
||||||
|
fields: [{ label: 'String', name: 'string', widget: 'string' }],
|
||||||
|
});
|
||||||
|
const { asFragment, getByTestId, queryByTestId } = render(
|
||||||
|
<ListControl
|
||||||
|
{...props}
|
||||||
|
field={field}
|
||||||
|
value={fromJS([{ string: 'item 1' }, { string: 'item 2' }])}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(queryByTestId('styled-list-item-top-bar-0')).toBeNull();
|
||||||
|
expect(queryByTestId('styled-list-item-top-bar-1')).toBeNull();
|
||||||
|
|
||||||
|
expect(queryByTestId('object-control-0')).toBeNull();
|
||||||
|
expect(queryByTestId('object-control-1')).toBeNull();
|
||||||
|
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
|
||||||
|
fireEvent.click(getByTestId('expand-button'));
|
||||||
|
|
||||||
|
expect(getByTestId('styled-list-item-top-bar-0')).toHaveAttribute('collapsed', 'true');
|
||||||
|
expect(getByTestId('styled-list-item-top-bar-1')).toHaveAttribute('collapsed', 'true');
|
||||||
|
|
||||||
|
expect(getByTestId('object-control-0')).toHaveAttribute('collapsed', 'true');
|
||||||
|
expect(getByTestId('object-control-1')).toHaveAttribute('collapsed', 'true');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render list with fields with collapse = "false" and default minimize_collapsed = "true"', () => {
|
||||||
|
const field = fromJS({
|
||||||
|
name: 'list',
|
||||||
|
label: 'List',
|
||||||
|
collapsed: false,
|
||||||
|
minimize_collapsed: true,
|
||||||
|
fields: [{ label: 'String', name: 'string', widget: 'string' }],
|
||||||
|
});
|
||||||
|
const { asFragment, getByTestId, queryByTestId } = render(
|
||||||
|
<ListControl
|
||||||
|
{...props}
|
||||||
|
field={field}
|
||||||
|
value={fromJS([{ string: 'item 1' }, { string: 'item 2' }])}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getByTestId('styled-list-item-top-bar-0')).toHaveAttribute('collapsed', 'false');
|
||||||
|
expect(getByTestId('styled-list-item-top-bar-1')).toHaveAttribute('collapsed', 'false');
|
||||||
|
|
||||||
|
expect(getByTestId('object-control-0')).toHaveAttribute('collapsed', 'false');
|
||||||
|
expect(getByTestId('object-control-1')).toHaveAttribute('collapsed', 'false');
|
||||||
|
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
|
||||||
|
fireEvent.click(getByTestId('expand-button'));
|
||||||
|
|
||||||
|
expect(queryByTestId('styled-list-item-top-bar-0')).toBeNull();
|
||||||
|
expect(queryByTestId('styled-list-item-top-bar-1')).toBeNull();
|
||||||
|
|
||||||
|
expect(queryByTestId('object-control-0')).toBeNull();
|
||||||
|
expect(queryByTestId('object-control-1')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add to list when add button is clicked', () => {
|
||||||
|
const field = fromJS({
|
||||||
|
name: 'list',
|
||||||
|
label: 'List',
|
||||||
|
fields: [{ label: 'String', name: 'string', widget: 'string' }],
|
||||||
|
});
|
||||||
|
const { asFragment, getByText, queryByTestId, rerender, getByTestId } = render(
|
||||||
|
<ListControl {...props} field={field} value={fromJS([])} />,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(queryByTestId('object-control-0')).toBeNull();
|
||||||
|
|
||||||
|
fireEvent.click(getByText('Add list'));
|
||||||
|
|
||||||
|
expect(props.onChange).toHaveBeenCalledTimes(1);
|
||||||
|
expect(props.onChange).toHaveBeenCalledWith(fromJS([{}]));
|
||||||
|
|
||||||
|
rerender(<ListControl {...props} field={field} value={fromJS([{}])} />);
|
||||||
|
|
||||||
|
expect(getByTestId('styled-list-item-top-bar-0')).toHaveAttribute('collapsed', 'false');
|
||||||
|
expect(getByTestId('object-control-0')).toHaveAttribute('collapsed', 'false');
|
||||||
|
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove from list when remove button is clicked', () => {
|
||||||
|
const field = fromJS({
|
||||||
|
name: 'list',
|
||||||
|
label: 'List',
|
||||||
|
collapsed: false,
|
||||||
|
minimize_collapsed: true,
|
||||||
|
fields: [{ label: 'String', name: 'string', widget: 'string' }],
|
||||||
|
});
|
||||||
|
const { asFragment, getAllByText, rerender } = render(
|
||||||
|
<ListControl
|
||||||
|
{...props}
|
||||||
|
field={field}
|
||||||
|
value={fromJS([{ string: 'item 1' }, { string: 'item 2' }])}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
|
||||||
|
let mock;
|
||||||
|
try {
|
||||||
|
mock = jest.spyOn(console, 'error').mockImplementation(() => undefined);
|
||||||
|
|
||||||
|
const items = getAllByText('Remove');
|
||||||
|
fireEvent.click(items[0]);
|
||||||
|
|
||||||
|
expect(props.onChange).toHaveBeenCalledTimes(1);
|
||||||
|
expect(props.onChange).toHaveBeenCalledWith(fromJS([{ string: 'item 2' }]), undefined);
|
||||||
|
|
||||||
|
rerender(<ListControl {...props} field={field} value={fromJS([{ string: 'item 2' }])} />);
|
||||||
|
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
} finally {
|
||||||
|
mock.mockRestore();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -13,6 +13,7 @@ The list widget allows you to create a repeatable item in the UI which saves as
|
|||||||
- `allow_add`: if added and labeled `false`, button to add additional widgets disappears
|
- `allow_add`: if added and labeled `false`, button to add additional widgets disappears
|
||||||
- `collapsed`: if added and labeled `false`, the list widget's content does not collapse by default
|
- `collapsed`: if added and labeled `false`, the list widget's content does not collapse by default
|
||||||
- `summary`: allows customization of a collapsed list item object in a similar way to a [collection summary](/docs/configuration-options/?#summary)
|
- `summary`: allows customization of a collapsed list item object in a similar way to a [collection summary](/docs/configuration-options/?#summary)
|
||||||
|
- `minimize_collapsed`: if added and labeled `true`, the list widget's content will be completely hidden instead of only collapsed if the list widget itself is collapsed
|
||||||
- `field`: a single widget field to be repeated
|
- `field`: a single widget field to be repeated
|
||||||
- `fields`: a nested list of multiple widget fields to be included in each repeatable iteration
|
- `fields`: a nested list of multiple widget fields to be included in each repeatable iteration
|
||||||
- **Example** (`field`/`fields` not specified):
|
- **Example** (`field`/`fields` not specified):
|
||||||
@ -63,3 +64,13 @@ The list widget allows you to create a repeatable item in the UI which saves as
|
|||||||
- {label: Quote, name: quote, widget: string, default: "Everything is awesome!"}
|
- {label: Quote, name: quote, widget: string, default: "Everything is awesome!"}
|
||||||
- {label: Author, name: author, widget: string }
|
- {label: Author, name: author, widget: string }
|
||||||
```
|
```
|
||||||
|
- **Example** (`minimize_collapsed` marked `true`):
|
||||||
|
```yaml
|
||||||
|
- label: "Testimonials"
|
||||||
|
name: "testimonials"
|
||||||
|
minimize_collapsed: true
|
||||||
|
widget: "list"
|
||||||
|
fields:
|
||||||
|
- {label: Quote, name: quote, widget: string, default: "Everything is awesome!"}
|
||||||
|
- {label: Author, name: author, widget: string }
|
||||||
|
```
|
||||||
|
Loading…
x
Reference in New Issue
Block a user