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) {
|
||||
super(props);
|
||||
const { field, value } = props;
|
||||
const allItemsCollapsed = field.get('collapsed', true);
|
||||
const itemsCollapsed = value && Array(value.size).fill(allItemsCollapsed);
|
||||
const keys = value && Array.from({ length: value.size }, () => uuid());
|
||||
const listCollapsed = field.get('collapsed', true);
|
||||
const itemsCollapsed = (value && Array(value.size).fill(listCollapsed)) || [];
|
||||
const keys = (value && Array.from({ length: value.size }, () => uuid())) || [];
|
||||
|
||||
this.state = {
|
||||
itemsCollapsed: List(itemsCollapsed),
|
||||
listCollapsed,
|
||||
itemsCollapsed,
|
||||
value: valueToString(value),
|
||||
keys: List(keys),
|
||||
keys,
|
||||
};
|
||||
}
|
||||
|
||||
@ -184,8 +185,8 @@ export default class ListControl extends React.Component {
|
||||
? this.singleDefault()
|
||||
: fromJS(this.multipleDefault(field.get('fields')));
|
||||
this.setState({
|
||||
itemsCollapsed: this.state.itemsCollapsed.push(false),
|
||||
keys: this.state.keys.push(uuid()),
|
||||
itemsCollapsed: [...this.state.itemsCollapsed, false],
|
||||
keys: [...this.state.keys, uuid()],
|
||||
});
|
||||
onChange((value || List()).push(parsedValue));
|
||||
};
|
||||
@ -202,8 +203,8 @@ export default class ListControl extends React.Component {
|
||||
const { value, onChange } = this.props;
|
||||
const parsedValue = fromJS(this.mixedDefault(typeKey, type));
|
||||
this.setState({
|
||||
itemsCollapsed: this.state.itemsCollapsed.push(false),
|
||||
keys: this.state.keys.push(uuid()),
|
||||
itemsCollapsed: [...this.state.itemsCollapsed, false],
|
||||
keys: [...this.state.keys, uuid()],
|
||||
});
|
||||
onChange((value || List()).push(parsedValue));
|
||||
};
|
||||
@ -300,7 +301,10 @@ export default class ListControl extends React.Component {
|
||||
? { [collectionName]: metadata.removeIn(metadataRemovePath) }
|
||||
: 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);
|
||||
clearFieldErrors();
|
||||
@ -314,16 +318,35 @@ export default class ListControl extends React.Component {
|
||||
handleItemCollapseToggle = (index, event) => {
|
||||
event.preventDefault();
|
||||
const { itemsCollapsed } = this.state;
|
||||
const collapsed = itemsCollapsed.get(index);
|
||||
this.setState({ itemsCollapsed: itemsCollapsed.set(index, !collapsed) });
|
||||
const newItemsCollapsed = itemsCollapsed.map((collapsed, itemIndex) => {
|
||||
if (index === itemIndex) {
|
||||
return !collapsed;
|
||||
}
|
||||
return collapsed;
|
||||
});
|
||||
this.setState({
|
||||
itemsCollapsed: newItemsCollapsed,
|
||||
});
|
||||
};
|
||||
|
||||
handleCollapseAllToggle = e => {
|
||||
e.preventDefault();
|
||||
const { value } = this.props;
|
||||
const { itemsCollapsed } = this.state;
|
||||
const { value, field } = this.props;
|
||||
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);
|
||||
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) {
|
||||
@ -368,11 +391,18 @@ export default class ListControl extends React.Component {
|
||||
this.props.onChange(newValue);
|
||||
|
||||
// Update collapsing
|
||||
const collapsed = itemsCollapsed.get(oldIndex);
|
||||
const updatedItemsCollapsed = itemsCollapsed.delete(oldIndex).insert(newIndex, collapsed);
|
||||
const collapsed = itemsCollapsed[oldIndex];
|
||||
itemsCollapsed.splice(oldIndex, 1);
|
||||
const updatedItemsCollapsed = [...itemsCollapsed];
|
||||
updatedItemsCollapsed.splice(newIndex, 0, collapsed);
|
||||
|
||||
// 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 });
|
||||
|
||||
//clear error fields and remove old validations
|
||||
@ -394,8 +424,8 @@ export default class ListControl extends React.Component {
|
||||
} = this.props;
|
||||
|
||||
const { itemsCollapsed, keys } = this.state;
|
||||
const collapsed = itemsCollapsed.get(index);
|
||||
const key = keys.get(index);
|
||||
const collapsed = itemsCollapsed[index];
|
||||
const key = keys[index];
|
||||
let field = this.props.field;
|
||||
|
||||
if (this.getValueType() === valueTypes.MIXED) {
|
||||
@ -473,11 +503,14 @@ export default class ListControl extends React.Component {
|
||||
|
||||
renderListControl() {
|
||||
const { value, forID, field, classNameWrapper } = this.props;
|
||||
const { itemsCollapsed } = this.state;
|
||||
const { itemsCollapsed, listCollapsed } = this.state;
|
||||
const items = value || List();
|
||||
const label = 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 minimizeCollapsedItems = field.get('minimize_collapsed', false);
|
||||
const allItemsCollapsed = itemsCollapsed.every(val => val === true);
|
||||
const selfCollapsed = allItemsCollapsed && (listCollapsed || !minimizeCollapsedItems);
|
||||
|
||||
return (
|
||||
<ClassNames>
|
||||
@ -499,15 +532,17 @@ export default class ListControl extends React.Component {
|
||||
heading={`${items.size} ${listLabel}`}
|
||||
label={labelSingular.toLowerCase()}
|
||||
onCollapseToggle={this.handleCollapseAllToggle}
|
||||
collapsed={itemsCollapsed.every(val => val === true)}
|
||||
/>
|
||||
<SortableList
|
||||
items={items}
|
||||
renderItem={this.renderItem}
|
||||
onSortEnd={this.onSortEnd}
|
||||
useDragHandle
|
||||
lockAxis="y"
|
||||
collapsed={selfCollapsed}
|
||||
/>
|
||||
{(!selfCollapsed || !minimizeCollapsedItems) && (
|
||||
<SortableList
|
||||
items={items}
|
||||
renderItem={this.renderItem}
|
||||
onSortEnd={this.onSortEnd}
|
||||
useDragHandle
|
||||
lockAxis="y"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</ClassNames>
|
||||
|
@ -20,6 +20,7 @@ jest.mock('netlify-cms-ui-default', () => {
|
||||
const actual = jest.requireActual('netlify-cms-ui-default');
|
||||
const ListItemTopBar = props => (
|
||||
<mock-list-item-top-bar {...props} onClick={props.onCollapseToggle}>
|
||||
<button onClick={props.onRemove}>Remove</button>
|
||||
{props.children}
|
||||
</mock-list-item-top-bar>
|
||||
);
|
||||
@ -454,4 +455,177 @@ describe('ListControl', () => {
|
||||
);
|
||||
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
|
||||
- `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)
|
||||
- `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
|
||||
- `fields`: a nested list of multiple widget fields to be included in each repeatable iteration
|
||||
- **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: 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