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 accordion to metadata form. #5760

Merged
merged 9 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion docs/source/recipes/widget.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ directives.widget(
)
```

It is recommended to define the vocabulary as a named `StaticCatalogVocabulary` with the field/relation name as its name.
It is recommended to define the vocabulary as a named `StaticCatalogVocabulary` with the field/relation name as its name.
This allows the {guilabel}`relations` control panel to respect the defined restrictions to potential relation targets.

{file}`vocabularies.py`
Expand Down Expand Up @@ -254,3 +254,36 @@ See [Storybook](https://6.docs.plone.org/storybook) with available widgets.
```{note}
Please contribute to this section!
```

## Sidebar

In the edit form, a sidebar is used when the form contains block data.
You can use the following helper action methods to change the form state.

### Setting tab

You can use the `setSidebarTab` action to set the current active tab, either via metadata or a block.

### Setting focus

You can use the `setMetadataFocus` action to set the current field by specifying the fieldset and the field name.

```jsx
import { useDispatch } from 'react-redux';
import { setSidebarTab, setMetadataFocus } from '@plone/volto/actions';

const dispatch = useDispatch()

return (
// ...
<button
onClick={() => {
dispatch(setSidebarTab(0));
dispatch(setMetadataFocus('ownership', 'allow_discussion'));
}}
>
This button will change the sidebar to the content form and focus ownership fieldset and the allow_discussion field
</button>
// ...
)
```
1 change: 1 addition & 0 deletions packages/volto/news/5760.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add accordion to metadata form. @robgietema
7 changes: 6 additions & 1 deletion packages/volto/src/actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,12 @@ export {
} from '@plone/volto/actions/workflow/workflow';
export { getQuerystring } from '@plone/volto/actions/querystring/querystring';
export { getQueryStringResults } from '@plone/volto/actions/querystringsearch/querystringsearch';
export { setSidebarTab } from '@plone/volto/actions/sidebar/sidebar';
export {
setMetadataFieldsets,
setMetadataFocus,
resetMetadataFocus,
setSidebarTab,
} from '@plone/volto/actions/sidebar/sidebar';
export { setFormData } from '@plone/volto/actions/form/form';
export {
deleteLinkTranslation,
Expand Down
46 changes: 45 additions & 1 deletion packages/volto/src/actions/sidebar/sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,51 @@
* @module actions/sidebar/sidebar
*/

import { SET_SIDEBAR_TAB } from '@plone/volto/constants/ActionTypes';
import {
SET_METADATA_FIELDSETS,
SET_METADATA_FOCUS,
RESET_METADATA_FOCUS,
SET_SIDEBAR_TAB,
} from '@plone/volto/constants/ActionTypes';

/**
* Set metadata fieldsets function.
* @function setMetadataFieldsets
* @param {Array} fieldsets New fieldsets.
* @returns {Object} Set metadata fieldsets action.
*/
export function setMetadataFieldsets(fieldsets) {
return {
type: SET_METADATA_FIELDSETS,
fieldsets,
};
}

/**
* Set metadata focus function.
* @function setMetadataFocus
* @param {String} fieldset Fieldset of the field.
* @param {String} field Field to set focus too.
* @returns {Object} Set metadata focus action.
*/
export function setMetadataFocus(fieldset, field) {
return {
type: SET_METADATA_FOCUS,
fieldset,
field,
};
}

/**
* Resets metadata focus function.
* @function resetMetadataFocus
* @returns {Object} Set metadata focus action.
*/
export function resetMetadataFocus() {
return {
type: RESET_METADATA_FOCUS,
};
}

/**
* Set sidebar tab function.
Expand Down
44 changes: 42 additions & 2 deletions packages/volto/src/actions/sidebar/sidebar.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,47 @@
import { setSidebarTab } from './sidebar';
import { SET_SIDEBAR_TAB } from '@plone/volto/constants/ActionTypes';
import {
setMetadataFieldsets,
setMetadataFocus,
resetMetadataFocus,
setSidebarTab,
} from './sidebar';
import {
SET_METADATA_FIELDSETS,
SET_METADATA_FOCUS,
RESET_METADATA_FOCUS,
SET_SIDEBAR_TAB,
} from '@plone/volto/constants/ActionTypes';

describe('Sidebar action', () => {
describe('setMetadataFieldsets', () => {
it('should create an action to set the metadata fieldsets', () => {
const fieldsets = ['default'];
const action = setMetadataFieldsets(fieldsets);

expect(action.type).toEqual(SET_METADATA_FIELDSETS);
expect(action.fieldsets).toEqual(fieldsets);
});
});

describe('setMetadataFocus', () => {
it('should create an action to set the metadata focus', () => {
const fieldset = ['default'];
const field = ['title'];
const action = setMetadataFocus(fieldset, field);

expect(action.type).toEqual(SET_METADATA_FOCUS);
expect(action.fieldset).toEqual(fieldset);
expect(action.field).toEqual(field);
});
});

describe('resetMetadataFocus', () => {
it('should create an action to reset the metadata focus', () => {
const action = resetMetadataFocus();

expect(action.type).toEqual(RESET_METADATA_FOCUS);
});
});

describe('setSidebarTab', () => {
it('should create an action to set the sidebar', () => {
const index = 1;
Expand Down
144 changes: 114 additions & 30 deletions packages/volto/src/components/manage/Form/Form.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
} from '@plone/volto/helpers';
import aheadSVG from '@plone/volto/icons/ahead.svg';
import clearSVG from '@plone/volto/icons/clear.svg';
import upSVG from '@plone/volto/icons/up-key.svg';
import downSVG from '@plone/volto/icons/down-key.svg';
import {
findIndex,
isEmpty,
Expand All @@ -23,6 +25,7 @@ import {
pickBy,
without,
cloneDeep,
xor,
} from 'lodash';
import isBoolean from 'lodash/isBoolean';
import PropTypes from 'prop-types';
Expand All @@ -31,6 +34,7 @@ import { injectIntl } from 'react-intl';
import { Portal } from 'react-portal';
import { connect } from 'react-redux';
import {
Accordion,
Button,
Container as SemanticContainer,
Form as UiForm,
Expand All @@ -41,7 +45,12 @@ import {
import { v4 as uuid } from 'uuid';
import { toast } from 'react-toastify';
import { BlocksToolbar, UndoToolbar } from '@plone/volto/components';
import { setSidebarTab, setFormData } from '@plone/volto/actions';
import {
setMetadataFieldsets,
resetMetadataFocus,
setSidebarTab,
setFormData,
} from '@plone/volto/actions';
import { compose } from 'redux';
import config from '@plone/volto/registry';

Expand Down Expand Up @@ -71,6 +80,8 @@ class Form extends Component {
}),
formData: PropTypes.objectOf(PropTypes.any),
globalData: PropTypes.objectOf(PropTypes.any),
metadataFieldsets: PropTypes.arrayOf(PropTypes.string),
metadataFieldFocus: PropTypes.string,
pathname: PropTypes.string,
onSubmit: PropTypes.func,
onCancel: PropTypes.func,
Expand Down Expand Up @@ -141,10 +152,16 @@ class Form extends Component {
title: uuid(),
text: uuid(),
};
let { formData } = props;
let { formData, schema: originalSchema } = props;
const blocksFieldname = getBlocksFieldname(formData);
const blocksLayoutFieldname = getBlocksLayoutFieldname(formData);

const schema = this.removeBlocksLayoutFields(originalSchema);

this.props.setMetadataFieldsets(
schema?.fieldsets ? schema.fieldsets.map((fieldset) => fieldset.id) : [],
);

if (!props.isEditForm) {
// It's a normal (add form), get defaults from schema
formData = {
Expand Down Expand Up @@ -228,6 +245,7 @@ class Form extends Component {
this.onTabChange = this.onTabChange.bind(this);
this.onBlurField = this.onBlurField.bind(this);
this.onClickInput = this.onClickInput.bind(this);
this.onToggleMetadataFieldset = this.onToggleMetadataFieldset.bind(this);
}

/**
Expand Down Expand Up @@ -268,6 +286,32 @@ class Form extends Component {
formData: this.props.globalData,
});
}

if (!isEqual(prevProps.schema, this.props.schema)) {
this.props.setMetadataFieldsets(
this.removeBlocksLayoutFields(this.props.schema).fieldsets.map(
(fieldset) => fieldset.id,
),
);
}

if (
this.props.metadataFieldFocus !== '' &&
!isEqual(prevProps.metadataFieldFocus, this.props.metadataFieldFocus)
) {
// Scroll into view
document
.querySelector(`.field-wrapper-${this.props.metadataFieldFocus}`)
.scrollIntoView();

// Set focus to first input if available
document
.querySelector(`.field-wrapper-${this.props.metadataFieldFocus} input`)
.focus();

// Reset focus field
this.props.resetMetadataFocus();
}
}

/**
Expand Down Expand Up @@ -561,6 +605,18 @@ class Form extends Component {
return newSchema;
};

/**
* Toggle metadata fieldset handler
* @method onToggleMetadataFieldset
* @param {Object} event Event object.
* @param {Object} blockProps Block properties.
* @returns {undefined}
*/
onToggleMetadataFieldset(event, blockProps) {
const { index } = blockProps;
this.props.setMetadataFieldsets(xor(this.props.metadataFieldsets, [index]));
}

/**
* Render method.
* @method render
Expand All @@ -574,6 +630,7 @@ class Form extends Component {
onSubmit,
navRoot,
type,
metadataFieldsets,
} = this.props;
const formData = this.state.formData;
const schema = this.removeBlocksLayoutFields(originalSchema);
Expand Down Expand Up @@ -657,34 +714,54 @@ class Form extends Component {
error={keys(this.state.errors).length > 0}
>
{schema &&
map(schema.fieldsets, (item) => [
<Segment
secondary
attached
className={`fieldset-${item.id}`}
key={item.title}
map(schema.fieldsets, (fieldset) => (
<Accordion
fluid
styled
className="form"
key={fieldset.title}
>
{item.title}
</Segment>,
<Segment attached key={`fieldset-contents-${item.title}`}>
{map(item.fields, (field, index) => (
<Field
{...schema.properties[field]}
id={field}
fieldSet={item.title.toLowerCase()}
formData={formData}
focus={this.state.inFocus[field]}
value={formData?.[field]}
required={schema.required.indexOf(field) !== -1}
onChange={this.onChangeField}
onBlur={this.onBlurField}
onClick={this.onClickInput}
key={field}
error={this.state.errors[field]}
/>
))}
</Segment>,
])}
<div
key={fieldset.id}
id={`metadataform-fieldset-${fieldset.id}`}
>
<Accordion.Title
active={metadataFieldsets.includes(fieldset.id)}
index={fieldset.id}
onClick={this.onToggleMetadataFieldset}
>
{fieldset.title}
{metadataFieldsets.includes(fieldset.id) ? (
<Icon name={upSVG} size="20px" />
) : (
<Icon name={downSVG} size="20px" />
)}
</Accordion.Title>
<Accordion.Content
active={metadataFieldsets.includes(fieldset.id)}
>
<Segment className="attached">
{map(fieldset.fields, (field, index) => (
<Field
{...schema.properties[field]}
id={field}
fieldSet={fieldset.title.toLowerCase()}
formData={formData}
focus={this.state.inFocus[field]}
value={formData?.[field]}
required={schema.required.indexOf(field) !== -1}
onChange={this.onChangeField}
onBlur={this.onBlurField}
onClick={this.onClickInput}
key={field}
error={this.state.errors[field]}
/>
))}
</Segment>
</Accordion.Content>
</div>
</Accordion>
))}
</UiForm>
</Portal>
)}
Expand Down Expand Up @@ -855,8 +932,15 @@ export default compose(
connect(
(state, props) => ({
globalData: state.form?.global,
metadataFieldsets: state.sidebar?.metadataFieldsets,
metadataFieldFocus: state.sidebar?.metadataFieldFocus,
}),
{ setSidebarTab, setFormData },
{
setMetadataFieldsets,
setSidebarTab,
setFormData,
resetMetadataFocus,
},
null,
{ forwardRef: true },
),
Expand Down
Loading
Loading