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

custom_fields: compose fields from given list #229

Merged
merged 4 commits into from
Jan 30, 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
4 changes: 4 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changes

Version 3.0.0 (released 2024-01-30)

- add discoverable custom fields components

Version 2.8.4 (released 2023-12-12)

- Replace CKEditor with TinyMCE
Expand Down
5 changes: 3 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-invenio-forms",
"version": "2.8.4",
"version": "3.0.0",
"description": "React components to build forms in Invenio",
"main": "dist/cjs/index.js",
"browser": "dist/cjs/index.js",
Expand Down
122 changes: 122 additions & 0 deletions src/lib/forms/widgets/custom_fields/ComposeFields.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import _isEmpty from "lodash/isEmpty";
import React, { Component } from "react";
import PropTypes from "prop-types";
import { Divider } from "semantic-ui-react";
import { AccordionField } from "../../AccordionField";
import { FieldLabel } from "../../FieldLabel";
import { Extensions } from "./Extensions";

export class ComposeFields extends Component {
constructor(props) {
super(props);
const { composeSections, record } = props;
const filled = Object.keys(record.custom_fields).map(
Copy link
Member

Choose a reason for hiding this comment

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

hmm here are not only the "compose" fields but all record save fields right?

(key) => `custom_fields.${key}`
);
this.state = { sections: composeSections, tempFields: [], recordFields: filled };
this.fieldsCfg = this.getFieldsConfig(composeSections);
this.sectionsList = composeSections.map((section) => section.section);
}

getFieldsConfig = (sectionCfg) => {
const cfg = {};
for (const section of sectionCfg) {
for (const fieldCfg of section.fieldsConfig) {
const { field, props, ui_widget, ...otherCfg } = fieldCfg;
cfg[field] = { ui_widget: ui_widget, section: section, ...props, ...otherCfg };
}
}

return cfg;
};

getFieldsWithValues = (sectionFields) => {
Copy link
Member

Choose a reason for hiding this comment

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

maybe here a better name would be availableSectionFields? To differentiate from the "active" ones you compute in this function...

const { record } = this.props;
const { tempFields, recordFields } = this.state;
const filledFields = [];
if (!record.custom_fields) {
return [];
}
for (const field of sectionFields) {
if (recordFields.includes(field.key) || tempFields.includes(field)) {
filledFields.push(field);
}
}
return filledFields;
};

getSectionOfField = (field) => {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
getSectionOfField = (field) => {
getFieldSection = (field) => {

const { sections } = this.state;
for (const section of sections) {
if (section.fields.map((field) => field.key).includes(field.key)) {
return section.section;
}
}
};

addFieldCallback = (fields) => {
const { sections: prevSections, tempFields: prevTempFields } = this.state;

const sections = [...prevSections];
for (const field of fields) {
const sectionToUpdate = this.getSectionOfField(field);
for (const section of sections) {
if (section.section === sectionToUpdate) {
section["fields"] = [...section.fields, field].sort((a, b) =>
a.key.localeCompare(b.key)
);
}
}
}
this.setState({
sections: [...sections],
tempFields: [...prevTempFields, ...fields],
});
};

render() {
const { templateLoaders, record } = this.props;
const { sections, tempFields, recordFields } = this.state;
const existingFields = [
...Object.entries(tempFields).map(([key, value]) => value.key),
Copy link
Member

Choose a reason for hiding this comment

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

I think tempFields it might be a bit confusing depends on what they are referring to...maybe we need a better name on that. Aren't these addedFields? maybe we discuss the naming....

...recordFields,
];

return (
<AccordionField key="compose fields" label="Domain specific fields" active>
{sections.map(({ fields, paths, ...sectionConfig }) => {
const recordCustomFields = this.getFieldsWithValues(fields);
if (_isEmpty(recordCustomFields)) {
return undefined;
}
return (
<div key={sectionConfig.section} className="rel-mb-2">
<FieldLabel
htmlFor={sectionConfig.section}
icon={sectionConfig.icon}
label={sectionConfig.section}
/>
<Divider fitted className="rel-mb-1" />
<div className="rel-ml-1">{recordCustomFields}</div>
</div>
);
})}
<Extensions
fieldPath="custom_fields"
{...this.fieldsCfg}
templateLoaders={templateLoaders}
addFieldCallback={this.addFieldCallback}
sections={this.sectionsList}
record={record}
existingFields={existingFields}
/>
</AccordionField>
);
}
}

ComposeFields.propTypes = {
templateLoaders: PropTypes.array.isRequired,
composeSections: PropTypes.array.isRequired,
record: PropTypes.object.isRequired,
};
83 changes: 60 additions & 23 deletions src/lib/forms/widgets/custom_fields/CustomFields.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,54 +7,90 @@

import React, { Component } from "react";
import PropTypes from "prop-types";
import { ComposeFields } from "./ComposeFields";
import { AccordionField } from "../../AccordionField";
import { loadWidgetsFromConfig } from "../loader";

export class CustomFields extends Component {
state = { sections: [] };
constructor(props) {
super(props);
this.state = { sections: undefined, composeSections: undefined };
Copy link
Member

Choose a reason for hiding this comment

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

I think we need to explain e.g docstring what is the difference between sections and composeSections.

}

componentDidMount() {
this.populateConfig();
}

populateConfig = async () => {
const { includesPaths, fieldPathPrefix } = this.props;
// use of `Promise.then()` as eslint is giving an error when calling setState() directly
// in the componentDidMount() method
this.loadCustomFieldsWidgets()
.then((sections) => {
sections = sections.map((sectionCfg) => {
const paths = includesPaths(sectionCfg.fields, fieldPathPrefix);
return { ...sectionCfg, paths };
});
this.setState({ sections });
})
.catch((error) => {
console.error("Couldn't load custom fields widgets.", error);
try {
const { sectionsConfig, composeSectionConfig } =
await this.loadCustomFieldsWidgets();
const sections = sectionsConfig.map((sectionCfg) => {
const paths = includesPaths(sectionCfg.fields, fieldPathPrefix);
return { ...sectionCfg, paths };
});
}

const composeSections = composeSectionConfig.map((sectionCfg) => {
const paths = includesPaths(sectionCfg.fields, fieldPathPrefix);
return { ...sectionCfg, paths };
});

this.setState({ sections: sections, composeSections: composeSections });
} catch (error) {
console.error("Couldn't load custom fields widgets.", error);
}
};

async loadCustomFieldsWidgets() {
const { config, fieldPathPrefix, templateLoaders } = this.props;
const { config, fieldPathPrefix, templateLoaders, record } = this.props;

const sections = [];
const composeFieldSections = [];
for (const sectionCfg of config) {
// Path to end user's folder defining custom fields ui widgets
const fields = await loadWidgetsFromConfig({
templateLoaders: templateLoaders,
fieldPathPrefix: fieldPathPrefix,
fields: sectionCfg.fields,
record: record,
});
sections.push({ ...sectionCfg, fields });
if (sectionCfg.compose_fields) {
Copy link
Member

Choose a reason for hiding this comment

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

We should align the name with the comment here

composeFieldSections.push({
...sectionCfg,
fields: fields,
fieldsConfig: sectionCfg.fields,
});
} else {
sections.push({ ...sectionCfg, fields });
}
}
return sections;
return { sectionsConfig: sections, composeSectionConfig: composeFieldSections };
}

render() {
const { sections } = this.state;
const { sections, composeSections } = this.state;
const { templateLoaders, record } = this.props;
return (
<>
{sections.map(({ section, fields, paths }) => (
<AccordionField key={section} includesPaths={paths} label={section} active>
{fields}
</AccordionField>
))}
{sections &&
sections.map(({ fields, paths, ...sectionConfig }) => (
<AccordionField
key={sectionConfig.section}
includesPaths={paths}
label={sectionConfig.section}
active
>
{fields}
</AccordionField>
))}
{composeSections && composeSections && (
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
{composeSections && composeSections && (
{composeSections && (

<ComposeFields
Copy link
Member

Choose a reason for hiding this comment

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

isn't it more of ComposedSections? I think here, if I understand correctly, it is a matter of having different section components. Some will have the default presentation and some the "composed" one.

templateLoaders={templateLoaders}
composeSections={composeSections}
record={record}
/>
)}
</>
);
}
Expand All @@ -76,6 +112,7 @@ CustomFields.propTypes = {
templateLoaders: PropTypes.array.isRequired,
fieldPathPrefix: PropTypes.string.isRequired,
includesPaths: PropTypes.func,
record: PropTypes.object.isRequired,
};

CustomFields.defaultProps = {
Expand Down
Loading
Loading