diff --git a/packages/strapi-plugin-versioning/src/extensions/content-manager/admin/src/components/FieldComponent/index.js b/packages/strapi-plugin-versioning/src/extensions/content-manager/admin/src/components/FieldComponent/index.js
index 6ce3383..97ce8c5 100644
--- a/packages/strapi-plugin-versioning/src/extensions/content-manager/admin/src/components/FieldComponent/index.js
+++ b/packages/strapi-plugin-versioning/src/extensions/content-manager/admin/src/components/FieldComponent/index.js
@@ -1,22 +1,21 @@
/* eslint-disable import/no-cycle */
-import React, { memo } from "react";
-import PropTypes from "prop-types";
-import { get, size } from "lodash";
-import { FormattedMessage } from "react-intl";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import isEqual from "react-fast-compare";
-import pluginId from "../../pluginId";
-import useEditView from "../../hooks/useEditView";
-import ComponentInitializer from "../ComponentInitializer";
-import NonRepeatableComponent from "../NonRepeatableComponent";
-import NotAllowedInput from "../NotAllowedInput";
-import RepeatableComponent from "../RepeatableComponent";
-import connect from "./utils/connect";
-import select from "./utils/select";
-import ComponentIcon from "./ComponentIcon";
-import Label from "./Label";
-import Reset from "./ResetComponent";
-import Wrapper from "./Wrapper";
+import React, { memo } from 'react';
+import PropTypes from 'prop-types';
+import { size } from 'lodash';
+import { FormattedMessage, useIntl } from 'react-intl';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import isEqual from 'react-fast-compare';
+import { NotAllowedInput, LabelIconWrapper } from 'strapi-helper-plugin';
+import pluginId from '../../pluginId';
+import ComponentInitializer from '../ComponentInitializer';
+import NonRepeatableComponent from '../NonRepeatableComponent';
+import RepeatableComponent from '../RepeatableComponent';
+import connect from './utils/connect';
+import select from './utils/select';
+import ComponentIcon from './ComponentIcon';
+import Label from './Label';
+import Reset from './ResetComponent';
+import Wrapper from './Wrapper';
const FieldComponent = ({
componentFriendlyName,
@@ -27,6 +26,7 @@ const FieldComponent = ({
isRepeatable,
isNested,
label,
+ labelIcon,
max,
min,
name,
@@ -39,27 +39,23 @@ const FieldComponent = ({
dataForCurrentVersion,
isVersionCurrent,
}) => {
- const { allLayoutData } = useEditView();
-
+ const { formatMessage } = useIntl();
const componentValueLength = size(componentValue);
- const isInitialized = componentValue || isFromDynamicZone;
+ const isInitialized = componentValue !== null || isFromDynamicZone;
const showResetComponent =
!isRepeatable &&
isInitialized &&
!isFromDynamicZone &&
hasChildrenAllowedFields;
- const currentComponentSchema = get(
- allLayoutData,
- ["components", componentUid],
- {},
- );
- const displayedFields = get(currentComponentSchema, ["layouts", "edit"], []);
+ const formattedLabelIcon = labelIcon
+ ? { icon: labelIcon.icon, title: formatMessage(labelIcon.title) }
+ : null;
if (!hasChildrenAllowedFields && isCreatingEntry) {
return (
-
+
);
}
@@ -71,7 +67,7 @@ const FieldComponent = ({
) {
return (
-
+
);
}
@@ -89,8 +85,15 @@ const FieldComponent = ({
)}
{showResetComponent && (
@@ -128,14 +129,11 @@ const FieldComponent = ({
componentValue={componentValue}
componentValueLength={componentValueLength}
componentUid={componentUid}
- fields={displayedFields}
- isFromDynamicZone={isFromDynamicZone}
isNested={isNested}
isReadOnly={isReadOnly}
max={max}
min={min}
name={name}
- schema={currentComponentSchema}
dataForCurrentVersion={dataForCurrentVersion}
isVersionCurrent={isVersionCurrent}
/>
@@ -149,11 +147,12 @@ FieldComponent.defaultProps = {
componentFriendlyName: null,
hasChildrenAllowedFields: false,
hasChildrenReadableFields: false,
- icon: "smile",
+ icon: 'smile',
isFromDynamicZone: false,
isReadOnly: false,
isRepeatable: false,
isNested: false,
+ labelIcon: null,
max: Infinity,
min: -Infinity,
dataForCurrentVersion: undefined,
@@ -173,6 +172,13 @@ FieldComponent.propTypes = {
isRepeatable: PropTypes.bool,
isNested: PropTypes.bool,
label: PropTypes.string.isRequired,
+ labelIcon: PropTypes.shape({
+ icon: PropTypes.node.isRequired,
+ title: PropTypes.shape({
+ id: PropTypes.string.isRequired,
+ defaultMessage: PropTypes.string.isRequired,
+ }),
+ }),
max: PropTypes.number,
min: PropTypes.number,
name: PropTypes.string.isRequired,
diff --git a/packages/strapi-plugin-versioning/src/extensions/content-manager/admin/src/components/Inputs/index.js b/packages/strapi-plugin-versioning/src/extensions/content-manager/admin/src/components/Inputs/index.js
index fe088d3..01ae643 100644
--- a/packages/strapi-plugin-versioning/src/extensions/content-manager/admin/src/components/Inputs/index.js
+++ b/packages/strapi-plugin-versioning/src/extensions/content-manager/admin/src/components/Inputs/index.js
@@ -1,134 +1,208 @@
-import React, { memo, useMemo, useEffect } from "react";
-import PropTypes from "prop-types";
-import { get, isEmpty, omit, toLower } from "lodash";
-import { FormattedMessage } from "react-intl";
-import { Inputs as InputsIndex } from "@buffetjs/custom";
-import { useStrapi } from "strapi-helper-plugin";
-
-import useDataManager from "../../hooks/useDataManager";
-import InputJSONWithErrors from "../InputJSONWithErrors";
-import SelectWrapper from "../SelectWrapper";
-import WysiwygWithErrors from "../WysiwygWithErrors";
-import InputUID from "../InputUID";
-
-const getInputType = (type = "") => {
- switch (toLower(type)) {
- case "boolean":
- return "bool";
- case "biginteger":
- return "text";
- case "decimal":
- case "float":
- case "integer":
- return "number";
- case "date":
- case "datetime":
- case "time":
- return type;
- case "email":
- return "email";
- case "enumeration":
- return "select";
- case "password":
- return "password";
- case "string":
- return "text";
- case "text":
- return "textarea";
- case "media":
- case "file":
- case "files":
- return "media";
- case "json":
- return "json";
- case "wysiwyg":
- case "WYSIWYG":
- case "richtext":
- return "wysiwyg";
- case "uid":
- return "uid";
- default:
- return type || "text";
- }
-};
+import React, { memo, useEffect, useMemo } from 'react';
+import PropTypes from 'prop-types';
+import { get, omit, take } from 'lodash';
+import isEqual from 'react-fast-compare';
+import { useIntl } from 'react-intl';
+import { Inputs as InputsIndex } from '@buffetjs/custom';
+import { NotAllowedInput, useStrapi } from 'strapi-helper-plugin';
+import { useContentTypeLayout } from '../../hooks';
+import { getFieldName } from '../../utils';
+import InputJSONWithErrors from '../InputJSONWithErrors';
+import SelectWrapper from '../SelectWrapper';
+import WysiwygWithErrors from '../WysiwygWithErrors';
+import InputUID from '../InputUID';
+import {
+ connect,
+ generateOptions,
+ getInputType,
+ getStep,
+ select,
+ VALIDATIONS_TO_OMIT,
+} from './utils';
function Inputs({
+ allowedFields,
autoFocus,
+ fieldSchema,
+ formErrors,
+ isCreatingEntry,
keys,
- layout,
- name,
+ labelIcon,
+ metadatas,
onBlur,
+ onChange,
+ readableFields,
+ shouldNotRunValidations,
+ queryInfos,
+ value,
dataForCurrentVersion,
isVersionCurrent,
}) {
const {
strapi: { fieldApi },
} = useStrapi();
+ const { contentType: currentContentTypeLayout } = useContentTypeLayout();
+ const { formatMessage } = useIntl();
- const {
- didCheckErrors,
- formErrors,
- modifiedData,
- onChange,
- initialData,
- } = useDataManager();
- const attribute = useMemo(
- () => get(layout, ["schema", "attributes", name], {}),
- [layout, name],
- );
- const metadatas = useMemo(
- () => get(layout, ["metadatas", name, "edit"], {}),
- [layout, name],
- );
- const disabled = useMemo(() => !get(metadatas, "editable", true), [
+ const labelIconformatted = labelIcon
+ ? { icon: labelIcon.icon, title: formatMessage(labelIcon.title) }
+ : labelIcon;
+
+ const disabled = useMemo(() => !get(metadatas, 'editable', true), [
metadatas,
]);
- const type = useMemo(() => get(attribute, "type", null), [attribute]);
- const regexpString = useMemo(() => get(attribute, "regex", null), [
- attribute,
+ const type = fieldSchema.type;
+
+ const errorId = useMemo(() => {
+ return get(formErrors, [keys, 'id'], null);
+ }, [formErrors, keys]);
+
+ const errorMessage = errorId
+ ? formatMessage({ id: errorId, defaultMessage: errorId })
+ : null;
+
+ const fieldName = useMemo(() => {
+ return getFieldName(keys);
+ }, [keys]);
+
+ const validations = useMemo(() => {
+ const inputValidations = omit(
+ fieldSchema,
+ shouldNotRunValidations
+ ? [...VALIDATIONS_TO_OMIT, 'required', 'minLength']
+ : VALIDATIONS_TO_OMIT,
+ );
+
+ const regexpString = fieldSchema.regex || null;
+
+ if (regexpString) {
+ const regexp = new RegExp(regexpString);
+
+ if (regexp) {
+ inputValidations.regex = regexp;
+ }
+ }
+
+ return inputValidations;
+ }, [fieldSchema, shouldNotRunValidations]);
+
+ const isRequired = useMemo(() => get(validations, ['required'], false), [
+ validations,
]);
- const value = !isVersionCurrent
- ? get(dataForCurrentVersion, keys, null)
- : get(modifiedData, keys, null);
- const initialValue = get(initialData, keys, null);
- const temporaryErrorIdUntilBuffetjsSupportsFormattedMessage =
- "app.utils.defaultMessage";
- const errorId = get(
- formErrors,
- [keys, "id"],
- temporaryErrorIdUntilBuffetjsSupportsFormattedMessage,
- );
- let validationsToOmit = [
- "type",
- "model",
- "via",
- "collection",
- "default",
- "plugin",
- "enum",
- "regex",
- ];
+ const isChildOfDynamicZone = useMemo(() => {
+ const attributes = get(currentContentTypeLayout, ['attributes'], {});
+ const foundAttributeType = get(attributes, [fieldName[0], 'type'], null);
+
+ return foundAttributeType === 'dynamiczone';
+ }, [currentContentTypeLayout, fieldName]);
+
+ const inputType = useMemo(() => {
+ return getInputType(type);
+ }, [type]);
+ const getInputValue = () => {
+ if (!isVersionCurrent) {
+ return get(dataForCurrentVersion, keys);
+ }
+ // Fix for input file multipe
+ if (type === 'media' && !value) {
+ return [];
+ }
+
+ return value;
+ };
+ const inputValue = getInputValue();
useEffect(() => {
if (isVersionCurrent) {
onChange({
- target: { name: keys, value: initialValue, type: getInputType(type) },
+ target: { name: keys, value: inputValue, type: getInputType(type) },
});
} else {
onChange({ target: { name: keys, value, type: getInputType(type) } });
}
}, [isVersionCurrent]);
- const validations = omit(attribute, validationsToOmit);
+ const step = useMemo(() => {
+ return getStep(type);
+ }, [type]);
- if (regexpString) {
- const regexp = new RegExp(regexpString);
+ const isUserAllowedToEditField = useMemo(() => {
+ const joinedName = fieldName.join('.');
- if (regexp) {
- validations.regex = regexp;
+ if (allowedFields.includes(joinedName)) {
+ return true;
}
- }
+
+ if (isChildOfDynamicZone) {
+ return allowedFields.includes(fieldName[0]);
+ }
+
+ const isChildOfComponent = fieldName.length > 1;
+
+ if (isChildOfComponent) {
+ const parentFieldName = take(fieldName, fieldName.length - 1).join('.');
+
+ return allowedFields.includes(parentFieldName);
+ }
+
+ return false;
+ }, [allowedFields, fieldName, isChildOfDynamicZone]);
+
+ const isUserAllowedToReadField = useMemo(() => {
+ const joinedName = fieldName.join('.');
+
+ if (readableFields.includes(joinedName)) {
+ return true;
+ }
+
+ if (isChildOfDynamicZone) {
+ return readableFields.includes(fieldName[0]);
+ }
+
+ const isChildOfComponent = fieldName.length > 1;
+
+ if (isChildOfComponent) {
+ const parentFieldName = take(fieldName, fieldName.length - 1).join('.');
+
+ return readableFields.includes(parentFieldName);
+ }
+
+ return false;
+ }, [readableFields, fieldName, isChildOfDynamicZone]);
+
+ const shouldDisplayNotAllowedInput = useMemo(() => {
+ return isUserAllowedToReadField || isUserAllowedToEditField;
+ }, [isUserAllowedToEditField, isUserAllowedToReadField]);
+
+ const shouldDisableField = useMemo(() => {
+ if (!isCreatingEntry) {
+ const doesNotHaveRight =
+ isUserAllowedToReadField && !isUserAllowedToEditField;
+
+ if (doesNotHaveRight) {
+ return true;
+ }
+
+ return disabled;
+ }
+
+ return disabled;
+ }, [
+ disabled,
+ isCreatingEntry,
+ isUserAllowedToEditField,
+ isUserAllowedToReadField,
+ ]);
+
+ const options = useMemo(
+ () => generateOptions(fieldSchema.enum || [], isRequired),
+ [fieldSchema, isRequired],
+ );
+
+ const otherFields = useMemo(() => {
+ return fieldApi.getFields();
+ }, [fieldApi]);
const { description, visible } = metadatas;
@@ -136,121 +210,105 @@ function Inputs({
return null;
}
- const isRequired = get(validations, ["required"], false);
+ if (!shouldDisplayNotAllowedInput) {
+ return (
+
+ );
+ }
- if (type === "relation") {
+ if (type === 'relation') {
return (
);
}
- let inputValue = value;
-
- // Fix for input file multipe
- if (type === "media" && !value) {
- inputValue = [];
- }
-
- let step;
-
- if (type === "float" || type === "decimal") {
- step = "any";
- } else if (type === "time" || type === "datetime") {
- step = 30;
- } else {
- step = "1";
- }
-
- const options = get(attribute, "enum", []).map((v) => {
- return (
-
- );
- });
-
- const enumOptions = [
-
- {(msg) => (
-
- )}
- ,
- ...options,
- ];
-
return (
-
- {(error) => {
- return (
-
- );
+
+ multiple={fieldSchema.multiple || false}
+ attribute={fieldSchema}
+ name={keys}
+ onBlur={onBlur}
+ onChange={onChange}
+ options={options}
+ step={step}
+ type={inputType}
+ validations={validations}
+ value={inputValue}
+ withDefaultValue={false}
+ />
);
}
Inputs.defaultProps = {
autoFocus: false,
+ formErrors: {},
+ labelIcon: null,
onBlur: null,
+ queryInfos: {},
+ value: null,
dataForCurrentVersion: null,
isVersionCurrent: true,
};
Inputs.propTypes = {
+ allowedFields: PropTypes.array.isRequired,
autoFocus: PropTypes.bool,
+ fieldSchema: PropTypes.object.isRequired,
+ formErrors: PropTypes.object,
keys: PropTypes.string.isRequired,
- layout: PropTypes.object.isRequired,
- name: PropTypes.string.isRequired,
+ isCreatingEntry: PropTypes.bool.isRequired,
+ labelIcon: PropTypes.shape({
+ icon: PropTypes.node.isRequired,
+ title: PropTypes.shape({
+ id: PropTypes.string.isRequired,
+ defaultMessage: PropTypes.string.isRequired,
+ }).isRequired,
+ }),
+ metadatas: PropTypes.object.isRequired,
onBlur: PropTypes.func,
onChange: PropTypes.func.isRequired,
+ readableFields: PropTypes.array.isRequired,
+ shouldNotRunValidations: PropTypes.bool.isRequired,
+ queryInfos: PropTypes.shape({
+ containsKey: PropTypes.string,
+ defaultParams: PropTypes.object,
+ endPoint: PropTypes.string,
+ }),
+ value: PropTypes.any,
dataForCurrentVersion: PropTypes.object,
isVersionCurrent: PropTypes.bool,
};
-export default memo(Inputs);
+const Memoized = memo(Inputs, isEqual);
+
+export default connect(Memoized, select);
diff --git a/packages/strapi-plugin-versioning/src/extensions/content-manager/admin/src/components/NonRepeatableComponent/index.js b/packages/strapi-plugin-versioning/src/extensions/content-manager/admin/src/components/NonRepeatableComponent/index.js
index d132276..35a2baa 100644
--- a/packages/strapi-plugin-versioning/src/extensions/content-manager/admin/src/components/NonRepeatableComponent/index.js
+++ b/packages/strapi-plugin-versioning/src/extensions/content-manager/admin/src/components/NonRepeatableComponent/index.js
@@ -1,67 +1,74 @@
/* eslint-disable react/no-array-index-key */
/* eslint-disable import/no-cycle */
-import React from "react";
-import PropTypes from "prop-types";
-import { get } from "lodash";
-import NonRepeatableWrapper from "../NonRepeatableWrapper";
-import Inputs from "../Inputs";
-import FieldComponent from "../FieldComponent";
+import React, { useMemo } from 'react';
+import PropTypes from 'prop-types';
+import { useContentTypeLayout } from '../../hooks';
+import NonRepeatableWrapper from '../NonRepeatableWrapper';
+import Inputs from '../Inputs';
+import FieldComponent from '../FieldComponent';
const NonRepeatableComponent = ({
componentUid,
- fields,
isFromDynamicZone,
name,
- schema,
isVersionCurrent,
dataForCurrentVersion,
}) => {
- const getField = (fieldName) =>
- get(schema, ["schema", "attributes", fieldName], {});
- const getMeta = (fieldName) =>
- get(schema, ["metadatas", fieldName, "edit"], {});
+ const { getComponentLayout } = useContentTypeLayout();
+ const componentLayoutData = useMemo(() => getComponentLayout(componentUid), [
+ componentUid,
+ getComponentLayout,
+ ]);
+ const fields = componentLayoutData.layouts.edit;
return (
{fields.map((fieldRow, key) => {
return (
- {fieldRow.map((field) => {
- const currentField = getField(field.name);
- const isComponent = get(currentField, "type", "") === "component";
- const keys = `${name}.${field.name}`;
+ {fieldRow.map(
+ ({
+ name: fieldName,
+ size,
+ metadatas,
+ fieldSchema,
+ queryInfos,
+ }) => {
+ const isComponent = fieldSchema.type === 'component';
+ const keys = `${name}.${fieldName}`;
- if (isComponent) {
- const compoUid = currentField.component;
- const metas = getMeta(field.name);
+ if (isComponent) {
+ const compoUid = fieldSchema.component;
+
+ return (
+
+ );
+ }
return (
-
+
+
+
);
- }
-
- return (
-
-
-
- );
- })}
+ },
+ )}
);
})}
@@ -70,20 +77,13 @@ const NonRepeatableComponent = ({
};
NonRepeatableComponent.defaultProps = {
- fields: [],
isFromDynamicZone: false,
- dataForCurrentVersion: undefined,
- isVersionCurrent: true,
};
NonRepeatableComponent.propTypes = {
componentUid: PropTypes.string.isRequired,
- fields: PropTypes.array,
isFromDynamicZone: PropTypes.bool,
name: PropTypes.string.isRequired,
- schema: PropTypes.object.isRequired,
- dataForCurrentVersion: PropTypes.object,
- isVersionCurrent: PropTypes.bool,
};
export default NonRepeatableComponent;
diff --git a/packages/strapi-plugin-versioning/src/extensions/content-manager/admin/src/components/RepeatableComponent/DraggedItem/index.js b/packages/strapi-plugin-versioning/src/extensions/content-manager/admin/src/components/RepeatableComponent/DraggedItem/index.js
index 133e39c..0045a00 100644
--- a/packages/strapi-plugin-versioning/src/extensions/content-manager/admin/src/components/RepeatableComponent/DraggedItem/index.js
+++ b/packages/strapi-plugin-versioning/src/extensions/content-manager/admin/src/components/RepeatableComponent/DraggedItem/index.js
@@ -1,17 +1,15 @@
/* eslint-disable import/no-cycle */
-import React, { memo, useEffect, useRef, useState } from "react";
-import PropTypes from "prop-types";
-import { get } from "lodash";
-import { Collapse } from "reactstrap";
-import { useDrag, useDrop } from "react-dnd";
-import { getEmptyImage } from "react-dnd-html5-backend";
-import useEditView from "../../../hooks/useEditView";
-import ItemTypes from "../../../utils/ItemTypes";
-import Inputs from "../../Inputs";
-import FieldComponent from "../../FieldComponent";
-import Banner from "../Banner";
-import FormWrapper from "../FormWrapper";
-import { connect, select } from "./utils";
+import React, { memo, useEffect, useRef, useState } from 'react';
+import PropTypes from 'prop-types';
+import { Collapse } from 'reactstrap';
+import { useDrag, useDrop } from 'react-dnd';
+import { getEmptyImage } from 'react-dnd-html5-backend';
+import ItemTypes from '../../../utils/ItemTypes';
+import Inputs from '../../Inputs';
+import FieldComponent from '../../FieldComponent';
+import Banner from '../Banner';
+import FormWrapper from '../FormWrapper';
+import { connect, select } from './utils';
/* eslint-disable react/no-array-index-key */
@@ -21,22 +19,17 @@ import { connect, select } from "./utils";
const DraggedItem = ({
componentFieldName,
- componentUid,
doesPreviousFieldContainErrorsAndIsOpen,
- fields,
hasErrors,
hasMinError,
isFirst,
isReadOnly,
isOpen,
- moveCollapse,
onClickToggle,
- removeCollapse,
schema,
toggleCollapses,
dataForCurrentVersion,
isVersionCurrent,
-
// Retrieved from the select function
moveComponentField,
removeRepeatableField,
@@ -44,16 +37,17 @@ const DraggedItem = ({
checkFormErrors,
displayedValue,
}) => {
- const { setIsDraggingComponent, unsetIsDraggingComponent } = useEditView();
const dragRef = useRef(null);
const dropRef = useRef(null);
const [showForm, setShowForm] = useState(false);
+ const fields = schema.layouts.edit;
+
useEffect(() => {
if (isOpen || !isVersionCurrent) {
setShowForm(true);
}
- }, [isOpen]);
+ }, [isOpen, isVersionCurrent]);
useEffect(() => {
if (!isVersionCurrent) {
@@ -73,12 +67,12 @@ const DraggedItem = ({
const dragPath = item.originalPath;
const hoverPath = componentFieldName;
- const fullPathToComponentArray = dragPath.split(".");
+ const fullPathToComponentArray = dragPath.split('.');
const dragIndexString = fullPathToComponentArray
.slice()
.splice(-1)
- .join("");
- const hoverIndexString = hoverPath.split(".").splice(-1).join("");
+ .join('');
+ const hoverIndexString = hoverPath.split('.').splice(-1).join('');
const pathToComponentArray = fullPathToComponentArray.slice(
0,
fullPathToComponentArray.length - 1,
@@ -114,12 +108,7 @@ const DraggedItem = ({
}
// Time to actually perform the action in the data
moveComponentField(pathToComponentArray, dragIndex, hoverIndex);
- // Time to actually perform the action in the synchronized collapses
- moveCollapse(dragIndex, hoverIndex);
- // Note: we're mutating the monitor item here!
- // Generally it's better to avoid mutations,
- // but it's good here for the sake of performance
- // to avoid expensive index searches.
+
item.originalPath = hoverPath;
},
});
@@ -132,12 +121,8 @@ const DraggedItem = ({
begin: () => {
// Close all collapses
toggleCollapses(-1);
- // Prevent the relations select from firing requests
- setIsDraggingComponent();
},
end: () => {
- // Enable the relations select to fire requests
- unsetIsDraggingComponent();
// Update the errors
triggerFormValidation();
},
@@ -150,11 +135,6 @@ const DraggedItem = ({
preview(getEmptyImage(), { captureDraggingState: false });
}, [preview]);
- const getField = (fieldName) =>
- get(schema, ["schema", "attributes", fieldName], {});
- const getMeta = (fieldName) =>
- get(schema, ["metadatas", fieldName, "edit"], {});
-
// Create the refs
// We need 1 for the drop target
// 1 for the drag target
@@ -180,64 +160,63 @@ const DraggedItem = ({
onClickToggle={onClickToggle}
onClickRemove={() => {
removeRepeatableField(componentFieldName);
- removeCollapse();
+ toggleCollapses();
}}
ref={refs}
/>
setShowForm(false)}
>
{!isDragging && (
{showForm &&
fields.map((fieldRow, key) => {
return (
- {fieldRow.map((field) => {
- const currentField = getField(field.name);
- const isComponent =
- get(currentField, "type", "") === "component";
- const keys = `${componentFieldName}.${field.name}`;
+ {fieldRow.map(
+ ({ name, fieldSchema, metadatas, queryInfos, size }) => {
+ const isComponent = fieldSchema.type === 'component';
+ const keys = `${componentFieldName}.${name}`;
+
+ if (isComponent) {
+ const componentUid = fieldSchema.component;
- if (isComponent) {
- const componentUid = currentField.component;
- const metas = getMeta(field.name);
+ return (
+
+ );
+ }
return (
-
+
+
+
);
- }
-
- return (
-
-
-
- );
- })}
+ },
+ )}
);
})}
@@ -250,30 +229,22 @@ const DraggedItem = ({
DraggedItem.defaultProps = {
doesPreviousFieldContainErrorsAndIsOpen: false,
- fields: [],
hasErrors: false,
hasMinError: false,
isFirst: false,
isOpen: false,
- moveCollapse: () => {},
toggleCollapses: () => {},
- dataForCurrentVersion: undefined,
- isVersionCurrent: true,
};
DraggedItem.propTypes = {
componentFieldName: PropTypes.string.isRequired,
- componentUid: PropTypes.string.isRequired,
doesPreviousFieldContainErrorsAndIsOpen: PropTypes.bool,
- fields: PropTypes.array,
hasErrors: PropTypes.bool,
hasMinError: PropTypes.bool,
isFirst: PropTypes.bool,
isOpen: PropTypes.bool,
isReadOnly: PropTypes.bool.isRequired,
- moveCollapse: PropTypes.func,
onClickToggle: PropTypes.func.isRequired,
- removeCollapse: PropTypes.func.isRequired,
schema: PropTypes.object.isRequired,
toggleCollapses: PropTypes.func,
moveComponentField: PropTypes.func.isRequired,
@@ -281,8 +252,6 @@ DraggedItem.propTypes = {
triggerFormValidation: PropTypes.func.isRequired,
checkFormErrors: PropTypes.func.isRequired,
displayedValue: PropTypes.string.isRequired,
- dataForCurrentVersion: PropTypes.object,
- isVersionCurrent: PropTypes.bool,
};
const Memoized = memo(DraggedItem);
diff --git a/packages/strapi-plugin-versioning/src/extensions/content-manager/admin/src/components/RepeatableComponent/index.js b/packages/strapi-plugin-versioning/src/extensions/content-manager/admin/src/components/RepeatableComponent/index.js
index 2b2a169..e2fe9be 100644
--- a/packages/strapi-plugin-versioning/src/extensions/content-manager/admin/src/components/RepeatableComponent/index.js
+++ b/packages/strapi-plugin-versioning/src/extensions/content-manager/admin/src/components/RepeatableComponent/index.js
@@ -1,19 +1,19 @@
+import React, { memo, useCallback, useMemo, useState } from 'react';
/* eslint-disable import/no-cycle */
-import React, { memo, useReducer } from "react";
-import { useDrop } from "react-dnd";
-import PropTypes from "prop-types";
-import { get, take } from "lodash";
-import { FormattedMessage } from "react-intl";
-import { ErrorMessage } from "@buffetjs/styles";
-import pluginId from "../../pluginId";
-import ItemTypes from "../../utils/ItemTypes";
-import connect from "./utils/connect";
-import select from "./utils/select";
-import Button from "./AddFieldButton";
-import DraggedItem from "./DraggedItem";
-import EmptyComponent from "./EmptyComponent";
-import init from "./init";
-import reducer, { initialState } from "./reducer";
+import { useDrop } from 'react-dnd';
+import PropTypes from 'prop-types';
+import { get, take } from 'lodash';
+import { FormattedMessage } from 'react-intl';
+import { ErrorMessage } from '@buffetjs/styles';
+import pluginId from '../../pluginId';
+import { getMaxTempKey } from '../../utils';
+import { useContentTypeLayout } from '../../hooks';
+import ItemTypes from '../../utils/ItemTypes';
+import connect from './utils/connect';
+import select from './utils/select';
+import Button from './AddFieldButton';
+import DraggedItem from './DraggedItem';
+import EmptyComponent from './EmptyComponent';
const RepeatableComponent = ({
addRepeatableComponentToField,
@@ -21,50 +21,71 @@ const RepeatableComponent = ({
componentUid,
componentValue,
componentValueLength,
- fields,
isNested,
isReadOnly,
max,
min,
name,
- schema,
dataForCurrentVersion,
isVersionCurrent,
}) => {
+ const [collapseToOpen, setCollapseToOpen] = useState('');
const [, drop] = useDrop({ accept: ItemTypes.COMPONENT });
+ const { getComponentLayout } = useContentTypeLayout();
+ const componentLayoutData = useMemo(() => getComponentLayout(componentUid), [
+ componentUid,
+ getComponentLayout,
+ ]);
+
+ const nextTempKey = useMemo(() => {
+ return getMaxTempKey(componentValue || []) + 1;
+ }, [componentValue]);
const componentErrorKeys = Object.keys(formErrors)
.filter((errorKey) => {
- return take(errorKey.split("."), isNested ? 3 : 1).join(".") === name;
+ return take(errorKey.split('.'), isNested ? 3 : 1).join('.') === name;
})
.map((errorKey) => {
return errorKey
- .split(".")
- .slice(0, name.split(".").length + 1)
- .join(".");
+ .split('.')
+ .slice(0, name.split('.').length + 1)
+ .join('.');
});
- // We need to synchronize the collapses array with the data
- // The key needed for react in the list will be the one from the collapses data
- // This way we don't have to mutate the data when it is received and we can use a unique key
- const [state, dispatch] = useReducer(reducer, initialState, () =>
- init(initialState, componentValue),
- );
- const { collapses } = state.toJS();
- const toggleCollapses = (index) => {
- dispatch({
- type: "TOGGLE_COLLAPSE",
- index,
- });
+ const toggleCollapses = () => {
+ setCollapseToOpen('');
};
const missingComponentsValue = min - componentValueLength;
const errorsArray = componentErrorKeys.map((key) =>
- get(formErrors, [key, "id"], ""),
+ get(formErrors, [key, 'id'], ''),
);
- const hasMinError =
- get(errorsArray, [0], "").includes("min") &&
- !collapses.some((obj) => obj.isOpen === true);
+ const hasMinError = get(errorsArray, [0], '').includes('min');
+
+ const handleClick = useCallback(() => {
+ if (!isReadOnly) {
+ if (componentValueLength < max) {
+ const shouldCheckErrors = hasMinError;
+
+ addRepeatableComponentToField(name, componentUid, shouldCheckErrors);
+
+ setCollapseToOpen(nextTempKey);
+ } else if (componentValueLength >= max) {
+ strapi.notification.info(
+ `${pluginId}.components.notification.info.maximum-requirement`,
+ );
+ }
+ }
+ }, [
+ addRepeatableComponentToField,
+ componentUid,
+ componentValueLength,
+ hasMinError,
+ isReadOnly,
+ max,
+ name,
+ nextTempKey,
+ ]);
return (
@@ -78,16 +99,22 @@ const RepeatableComponent = ({
{componentValueLength > 0 &&
componentValue.map((data, index) => {
+ const key = data.__temp_key__;
+ const isOpen = collapseToOpen === key;
const componentFieldName = `${name}.${index}`;
+ const previousComponentTempKey = get(componentValue, [
+ index - 1,
+ '__temp_key__',
+ ]);
const doesPreviousFieldContainErrorsAndIsOpen =
componentErrorKeys.includes(`${name}.${index - 1}`) &&
index !== 0 &&
- get(collapses, [index - 1, "isOpen"], false) === false;
+ collapseToOpen === previousComponentTempKey;
+
const hasErrors = componentErrorKeys.includes(componentFieldName);
return (
{
- // Close all other collapses and open the selected one
- toggleCollapses(index);
- }}
- removeCollapse={() => {
- dispatch({
- type: "REMOVE_COLLAPSE",
- index,
- });
- }}
- moveCollapse={(dragIndex, hoverIndex) => {
- dispatch({
- type: "MOVE_COLLAPSE",
- dragIndex,
- hoverIndex,
- });
+ if (isOpen) {
+ setCollapseToOpen('');
+ } else {
+ setCollapseToOpen(key);
+ }
}}
parentName={name}
- schema={schema}
+ schema={componentLayoutData}
toggleCollapses={toggleCollapses}
dataForCurrentVersion={dataForCurrentVersion}
isVersionCurrent={isVersionCurrent}
@@ -132,29 +149,11 @@ const RepeatableComponent = ({
doesPreviousFieldContainErrorsAndIsClosed={
componentValueLength > 0 &&
componentErrorKeys.includes(`${name}.${componentValueLength - 1}`) &&
- collapses[componentValueLength - 1].isOpen === false
+ componentValue[componentValueLength - 1].__temp_key__ !==
+ collapseToOpen
}
type="button"
- onClick={() => {
- if (!isReadOnly) {
- if (componentValueLength < max) {
- const shouldCheckErrors = hasMinError;
-
- addRepeatableComponentToField(
- name,
- componentUid,
- shouldCheckErrors,
- );
- dispatch({
- type: "ADD_NEW_FIELD",
- });
- } else if (componentValueLength >= max) {
- strapi.notification.info(
- `${pluginId}.components.notification.info.maximum-requirement`,
- );
- }
- }
- }}
+ onClick={handleClick}
>
@@ -163,7 +162,7 @@ const RepeatableComponent = ({
1 ? ".plural" : ".singular"
+ missingComponentsValue > 1 ? '.plural' : '.singular'
}`}
values={{ count: missingComponentsValue }}
/>
@@ -176,7 +175,6 @@ const RepeatableComponent = ({
RepeatableComponent.defaultProps = {
componentValue: null,
componentValueLength: 0,
- fields: [],
formErrors: {},
isNested: false,
max: Infinity,
@@ -190,14 +188,12 @@ RepeatableComponent.propTypes = {
componentUid: PropTypes.string.isRequired,
componentValue: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
componentValueLength: PropTypes.number,
- fields: PropTypes.array,
formErrors: PropTypes.object,
isNested: PropTypes.bool,
isReadOnly: PropTypes.bool.isRequired,
max: PropTypes.number,
min: PropTypes.number,
name: PropTypes.string.isRequired,
- schema: PropTypes.object.isRequired,
dataForCurrentVersion: PropTypes.object,
isVersionCurrent: PropTypes.bool,
};
diff --git a/packages/strapi-plugin-versioning/src/extensions/content-manager/admin/src/components/SelectWrapper/index.js b/packages/strapi-plugin-versioning/src/extensions/content-manager/admin/src/components/SelectWrapper/index.js
index 25c697b..00ccd86 100644
--- a/packages/strapi-plugin-versioning/src/extensions/content-manager/admin/src/components/SelectWrapper/index.js
+++ b/packages/strapi-plugin-versioning/src/extensions/content-manager/admin/src/components/SelectWrapper/index.js
@@ -1,73 +1,88 @@
-/* eslint-disable react-hooks/exhaustive-deps */
-import React, { useState, useEffect, useMemo, useRef, memo } from "react";
-import PropTypes from "prop-types";
-import { FormattedMessage } from "react-intl";
-import { Link, useLocation } from "react-router-dom";
-import { cloneDeep, findIndex, get, isArray, isEmpty, set } from "lodash";
-import { request } from "strapi-helper-plugin";
-import pluginId from "../../pluginId";
-import useDataManager from "../../hooks/useDataManager";
-import useEditView from "../../hooks/useEditView";
-import { getFieldName } from "../../utils";
-import NotAllowedInput from "../NotAllowedInput";
-import SelectOne from "../SelectOne";
-import SelectMany from "../SelectMany";
-import { Nav, Wrapper } from "./components";
-import { connect, select } from "./utils";
+import React, { useCallback, useState, useEffect, useMemo, memo } from 'react';
+import PropTypes from 'prop-types';
+import { FormattedMessage, useIntl } from 'react-intl';
+import { Link, useLocation } from 'react-router-dom';
+import { findIndex, get, isArray, isEmpty, set } from 'lodash';
+import {
+ DropdownIndicator,
+ LabelIconWrapper,
+ NotAllowedInput,
+ request,
+ useContentManagerEditViewDataManager,
+ useQueryParams,
+} from 'strapi-helper-plugin';
+import { Flex, Text, Padded } from '@buffetjs/core';
+import { stringify } from 'qs';
+import pluginId from '../../pluginId';
+import SelectOne from '../SelectOne';
+import SelectMany from '../SelectMany';
+import ClearIndicator from './ClearIndicator';
+import IndicatorSeparator from './IndicatorSeparator';
+import Option from './Option';
+import { A, BaselineAlignment } from './components';
+import { connect, select, styles } from './utils';
+
+const initialPaginationState = {
+ _contains: '',
+ _limit: 20,
+ _start: 0,
+};
+
+const buildParams = (query, paramsToKeep) => {
+ if (!paramsToKeep) {
+ return {};
+ }
+
+ return paramsToKeep.reduce((acc, current) => {
+ const value = get(query, current, null);
+
+ if (value) {
+ set(acc, current, value);
+ }
+ return acc;
+ }, {});
+};
function SelectWrapper({
- componentUid,
description,
editable,
label,
+ labelIcon,
isCreatingEntry,
isFieldAllowed,
isFieldReadable,
mainField,
name,
relationType,
- slug,
targetModel,
placeholder,
+ queryInfos,
valueToSet,
}) {
+ const { formatMessage } = useIntl();
+ const [{ query }] = useQueryParams();
// Disable the input in case of a polymorphic relation
- const isMorph = relationType.toLowerCase().includes("morph");
+ const isMorph = useMemo(() => relationType.toLowerCase().includes('morph'), [relationType]);
const {
addRelation,
modifiedData,
moveRelation,
onChange,
onRemoveRelation,
- initialData,
- } = useDataManager();
- const { isDraggingComponent } = useEditView();
+ initialData
+ } = useContentManagerEditViewDataManager();
+ const { pathname } = useLocation();
+ // const value = get(modifiedData, name, null);
const value =
- valueToSet && valueToSet !== "current"
+ valueToSet && valueToSet !== 'current'
? valueToSet
: get(modifiedData, name, null);
const initialValue = get(initialData, name, null);
-
- // This is needed for making requests when used in a component
- const fieldName = useMemo(() => {
- const fieldNameArray = getFieldName(name);
- return fieldNameArray[fieldNameArray.length - 1];
- }, [name]);
-
- const { pathname } = useLocation();
-
- const [state, setState] = useState({
- _contains: "",
- _limit: 20,
- _start: 0,
- });
+ const [state, setState] = useState(initialPaginationState);
const [options, setOptions] = useState([]);
- const [isLoading, setIsLoading] = useState(true);
- const abortController = new AbortController();
- const { signal } = abortController;
- const ref = useRef();
- const startRef = useRef();
+ const [isLoading, setIsLoading] = useState(false);
+ const [isOpen, setIsOpen] = useState(false);
const filteredOptions = useMemo(() => {
return options.filter((option) => {
@@ -78,44 +93,86 @@ function SelectWrapper({
}
// SelectOne
- return get(value, "id", "") !== option.value.id;
+ return get(value, 'id', '') !== option.value.id;
}
return true;
});
}, [options, value]);
- startRef.current = state._start;
+ const {
+ endPoint,
+ containsKey,
+ defaultParams,
+ shouldDisplayRelationLink,
+ paramsToKeep,
+ } = queryInfos;
- ref.current = async () => {
- if (isMorph) {
- setIsLoading(false);
+ const isSingle = [
+ 'oneWay',
+ 'oneToOne',
+ 'manyToOne',
+ 'oneToManyMorph',
+ 'oneToOneMorph',
+ ].includes(relationType);
+
+ const changeRelationValueForCurrentVersion = () => {
+ if (valueToSet) {
+ valueToSet !== 'current'
+ ? onChange({ target: { name, value: valueToSet } })
+ : onChange({ target: { name, value: initialValue } });
+ }
+ };
+
+ useEffect(() => {
+ changeRelationValueForCurrentVersion();
+ }, [valueToSet]);
- return;
+ const idsToOmit = useMemo(() => {
+ if (!value) {
+ return [];
}
- if (!isDraggingComponent) {
- try {
- const requestUrl = `/${pluginId}/explorer/${slug}/relation-list/${fieldName}`;
+ if (isSingle) {
+ return [value.id];
+ }
- const containsKey = `${mainField}_contains`;
- const { _contains, ...restState } = cloneDeep(state);
- const params = isEmpty(state._contains)
- ? restState
- : { [containsKey]: _contains, ...restState };
+ return value.map((val) => val.id);
+ }, [isSingle, value]);
- if (componentUid) {
- set(params, "_component", componentUid);
- }
+ const getData = useCallback(
+ async (signal) => {
+ // Currently polymorphic relations are not handled
+ if (isMorph) {
+ setIsLoading(false);
+
+ return;
+ }
+
+ if (!isFieldAllowed) {
+ setIsLoading(false);
+
+ return;
+ }
+
+ setIsLoading(true);
+
+ const params = { _limit: state._limit, ...defaultParams };
+
+ if (state._contains) {
+ params[containsKey] = state._contains;
+ }
- const data = await request(requestUrl, {
- method: "GET",
+ try {
+ const data = await request(endPoint, {
+ method: 'POST',
params,
signal,
+ body: { idsToOmit },
});
const formattedData = data.map((obj) => {
- return { value: obj, label: obj[mainField] };
+ return { value: obj, label: obj[mainField.name] };
});
setOptions((prevState) =>
@@ -128,51 +185,40 @@ function SelectWrapper({
return true;
}
- return (
- prevState.findIndex((el) => el.value.id === obj.value.id) ===
- index
- );
- }),
+ return prevState.findIndex(el => el.value.id === obj.value.id) === index;
+ })
);
setIsLoading(false);
} catch (err) {
- if (err.code !== 20) {
- strapi.notification.error("notification.error");
- }
+ // Silent
}
- }
- };
+ },
+ [
+ isMorph,
+ isFieldAllowed,
+ state._limit,
+ state._contains,
+ defaultParams,
+ containsKey,
+ endPoint,
+ idsToOmit,
+ mainField.name,
+ ],
+ );
useEffect(() => {
- if (state._contains !== "") {
- let timer = setTimeout(() => {
- ref.current();
- }, 300);
-
- return () => clearTimeout(timer);
- }
+ const abortController = new AbortController();
+ const { signal } = abortController;
- if (isFieldAllowed) {
- ref.current();
+ if (isOpen) {
+ getData(signal);
}
- return () => {
- abortController.abort();
- };
- }, [state._contains, isFieldAllowed]);
+ return () => abortController.abort();
+ }, [getData, isOpen]);
- useEffect(() => {
- if (state._start !== 0) {
- ref.current();
- }
-
- return () => {
- abortController.abort();
- };
- }, [state._start]);
-
- const onInputChange = (inputValue, { action }) => {
- if (action === "input-change") {
+ const handleInputChange = (inputValue, { action }) => {
+ if (action === 'input-change') {
setState((prevState) => {
if (prevState._contains === inputValue) {
return prevState;
@@ -185,156 +231,187 @@ function SelectWrapper({
return inputValue;
};
- const onMenuScrollToBottom = () => {
- setState((prevState) => ({ ...prevState, _start: prevState._start + 20 }));
+ const handleMenuScrollToBottom = () => {
+ setState((prevState) => ({ ...prevState, _limit: prevState._limit + 20 }));
};
- const isSingle = [
- "oneWay",
- "oneToOne",
- "manyToOne",
- "oneToManyMorph",
- "oneToOneMorph",
- ].includes(relationType);
+ const handleMenuClose = () => {
+ setState(initialPaginationState);
+ setIsOpen(false);
+ };
- const changeRelationValueForCurrentVersion = () => {
- if (valueToSet && startRef.current != 0) {
- valueToSet !== "current"
- ? onChange({ target: { name, value: valueToSet } })
- : onChange({ target: { name, value: initialValue } });
+ const handleChange = (value) => {
+ onChange({ target: { name, value: value ? value.value : value } });
+ };
+
+ const handleAddRelation = (value) => {
+ if (!isEmpty(value)) {
+ addRelation({ target: { name, value } });
}
};
- useEffect(() => {
- changeRelationValueForCurrentVersion();
- }, [valueToSet]);
+ const handleMenuOpen = () => {
+ setIsOpen(true);
+ };
- const to = `/plugins/${pluginId}/collectionType/${targetModel}/${
- value ? value.id : null
- }`;
- const link =
- value === null ||
- value === undefined ||
- [
- "plugins::users-permissions.role",
- "plugins::users-permissions.permission",
- ].includes(targetModel) ? null : (
-
-
+ const to = `/plugins/${pluginId}/collectionType/${targetModel}/${value ? value.id : null}`;
+
+ const searchToPersist = stringify(buildParams(query, paramsToKeep), { encode: false });
+
+ const link = useMemo(() => {
+ if (!value) {
+ return null;
+ }
+
+ if (!shouldDisplayRelationLink) {
+ return null;
+ }
+
+ return (
+
+
+ {(msg) => {msg}}
+
);
+ }, [shouldDisplayRelationLink, pathname, to, value, searchToPersist]);
+
const Component = isSingle ? SelectOne : SelectMany;
const associationsLength = isArray(value) ? value.length : 0;
- const customStyles = {
- option: (provided) => {
- return {
- ...provided,
- maxWidth: "100% !important",
- overflow: "hidden",
- textOverflow: "ellipsis",
- whiteSpace: "nowrap",
- };
- },
- };
-
const isDisabled = useMemo(() => {
if (isMorph) {
return true;
}
if (!isCreatingEntry) {
- return !isFieldAllowed && isFieldReadable;
+ return (!isFieldAllowed && isFieldReadable) || !editable;
}
return !editable;
- });
+ }, [isMorph, isCreatingEntry, editable, isFieldAllowed, isFieldReadable]);
+
+ const labelIconformatted = labelIcon
+ ? { icon: labelIcon.icon, title: formatMessage(labelIcon.title) }
+ : labelIcon;
if (!isFieldAllowed && isCreatingEntry) {
- return ;
+ return ;
}
if (!isCreatingEntry && !isFieldAllowed && !isFieldReadable) {
- return ;
+ return ;
}
return (
-
-
- {
- addRelation({ target: { name, value } });
- }}
- id={name}
- isDisabled={isDisabled}
- isLoading={isLoading}
- isClearable
- mainField={mainField}
- move={moveRelation}
- name={name}
- options={filteredOptions}
- onChange={(value) => {
- onChange({ target: { name, value: value ? value.value : value } });
- }}
- onInputChange={onInputChange}
- onMenuClose={() => {
- setState((prevState) => ({ ...prevState, _contains: "" }));
- }}
- onMenuScrollToBottom={onMenuScrollToBottom}
- onRemove={onRemoveRelation}
- placeholder={
- isEmpty(placeholder) ? (
-
- ) : (
- placeholder
- )
- }
- styles={customStyles}
- targetModel={targetModel}
- value={value}
- />
-
-
+
+
+
+
+
+
+ {label}
+ {!isSingle && ` (${associationsLength})`}
+
+
+ {labelIconformatted && (
+
+
+ {labelIconformatted.icon}
+
+
+ )}
+
+ {isSingle && link}
+
+ {!isEmpty(description) && (
+
+
+
+ {description}
+
+
+ )}
+
+
+
+
+ ) : (
+ placeholder
+ )
+ }
+ searchToPersist={searchToPersist}
+ styles={styles}
+ targetModel={targetModel}
+ value={value}
+ />
+
+
+
);
}
SelectWrapper.defaultProps = {
- componentUid: null,
editable: true,
- description: "",
- label: "",
+ description: '',
+ label: '',
+ labelIcon: null,
isFieldAllowed: true,
- placeholder: "",
+ placeholder: '',
valueToSet: null,
};
SelectWrapper.propTypes = {
- componentUid: PropTypes.string,
editable: PropTypes.bool,
description: PropTypes.string,
label: PropTypes.string,
+ labelIcon: PropTypes.shape({
+ icon: PropTypes.node.isRequired,
+ title: PropTypes.shape({
+ id: PropTypes.string.isRequired,
+ defaultMessage: PropTypes.string,
+ }),
+ }),
isCreatingEntry: PropTypes.bool.isRequired,
isFieldAllowed: PropTypes.bool,
isFieldReadable: PropTypes.bool.isRequired,
- mainField: PropTypes.string.isRequired,
+ mainField: PropTypes.shape({
+ name: PropTypes.string.isRequired,
+ schema: PropTypes.shape({
+ type: PropTypes.string.isRequired,
+ }).isRequired,
+ }).isRequired,
name: PropTypes.string.isRequired,
placeholder: PropTypes.string,
relationType: PropTypes.string.isRequired,
- slug: PropTypes.string.isRequired,
targetModel: PropTypes.string.isRequired,
+ queryInfos: PropTypes.shape({
+ containsKey: PropTypes.string.isRequired,
+ defaultParams: PropTypes.object,
+ endPoint: PropTypes.string.isRequired,
+ shouldDisplayRelationLink: PropTypes.bool.isRequired,
+ paramsToKeep: PropTypes.array,
+ }).isRequired,
valueToSet: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
};
diff --git a/packages/strapi-plugin-versioning/src/extensions/content-manager/admin/src/containers/EditView/index.js b/packages/strapi-plugin-versioning/src/extensions/content-manager/admin/src/containers/EditView/index.js
index 143edcd..66dba85 100644
--- a/packages/strapi-plugin-versioning/src/extensions/content-manager/admin/src/containers/EditView/index.js
+++ b/packages/strapi-plugin-versioning/src/extensions/content-manager/admin/src/containers/EditView/index.js
@@ -1,69 +1,57 @@
-import React, {
- memo,
- useCallback,
- useMemo,
- useEffect,
- useReducer,
- useRef,
- useState,
-} from "react";
-import PropTypes from "prop-types";
-import { get, isEqual, sortBy } from "lodash";
-import { FormattedMessage } from "react-intl";
-import { Select, Button } from "@buffetjs/core";
-import { useHistory, useLocation, useRouteMatch } from "react-router-dom";
+import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
+import PropTypes from 'prop-types';
+import { get, isEqual, sortBy } from 'lodash';
+import { FormattedMessage } from 'react-intl';
import {
BackHeader,
- LiLink,
+ BaselineAlignment,
CheckPermissions,
- useUserPermissions,
+ LiLink,
request,
-} from "strapi-helper-plugin";
-import pluginId from "../../pluginId";
-import pluginPermissions from "../../permissions";
-import { generatePermissionsObject } from "../../utils";
-import Container from "../../components/Container";
-import DynamicZone from "../../components/DynamicZone";
-import FormWrapper from "../../components/FormWrapper";
-import FieldComponent from "../../components/FieldComponent";
-import Inputs from "../../components/Inputs";
-import SelectWrapper from "../../components/SelectWrapper";
-import getInjectedComponents from "../../utils/getComponents";
-import EditViewDataManagerProvider from "../EditViewDataManagerProvider";
-import EditViewProvider from "../EditViewProvider";
-import Header from "./Header";
-import createAttributesLayout from "./utils/createAttributesLayout";
-import { LinkWrapper, SubWrapper } from "./components";
-import init from "./init";
-import reducer, { initialState } from "./reducer";
-import getRequestUrl from "../../utils/getRequestUrl";
+ useGlobalContext,
+} from 'strapi-helper-plugin';
+import { Button, Padded, Select } from '@buffetjs/core';
+import pluginId from '../../pluginId';
+import pluginPermissions from '../../permissions';
+import Container from '../../components/Container';
+import DynamicZone from '../../components/DynamicZone';
+import FormWrapper from '../../components/FormWrapper';
+import FieldComponent from '../../components/FieldComponent';
+import Inputs from '../../components/Inputs';
+import SelectWrapper from '../../components/SelectWrapper';
+import { getInjectedComponents } from '../../utils';
+import CollectionTypeFormWrapper from '../CollectionTypeFormWrapper';
+import EditViewDataManagerProvider from '../EditViewDataManagerProvider';
+import SingleTypeFormWrapper from '../SingleTypeFormWrapper';
+import Header from './Header';
+import {
+ createAttributesLayout,
+ getFieldsActionMatchingPermissions,
+} from './utils';
+import { LinkWrapper, SubWrapper } from './components';
+import DeleteLink from './DeleteLink';
+import InformationCard from './InformationCard';
+import getRequestUrl from '../../utils/getRequestUrl';
+import { useLocation } from 'react-router-dom';
/* eslint-disable react/no-array-index-key */
-
const EditView = ({
- components,
- currentEnvironment,
- deleteLayout,
- layouts,
- plugins,
+ allowedActions,
+ isSingleType,
+ goBack,
+ layout,
slug,
+ state,
+ id,
+ origin,
+ userPermissions,
}) => {
- const formatLayoutRef = useRef();
- formatLayoutRef.current = createAttributesLayout;
- const { goBack } = useHistory();
- // Retrieve the search and the pathname
+ const { currentEnvironment, plugins } = useGlobalContext();
const { pathname } = useLocation();
- const {
- params: { contentType },
- } = useRouteMatch("/plugins/content-manager/:contentType");
- const viewPermissions = useMemo(() => generatePermissionsObject(slug), [
- slug,
- ]);
- const { allowedActions } = useUserPermissions(viewPermissions);
- const entityId = pathname.split("/").pop();
- const [versions, setVersions] = useState([{ date: "current" }]);
- const [selectedVersion, setSelectedVersion] = useState("current");
+ const entityId = pathname.split('/').pop();
+ const [versions, setVersions] = useState([{ date: 'current' }]);
+ const [selectedVersion, setSelectedVersion] = useState('current');
const changeLatestDateToCurrent = (versions) => {
if (versions.length) {
@@ -71,145 +59,115 @@ const EditView = ({
(previous, next) =>
new Date(previous.date).getTime() - new Date(next.date).getTime(),
);
-
return sortedVersions.map((sortedVersion, i, arr) => {
if (i === arr.length - 1) {
sortedVersion = {
...sortedVersion,
- date: "current",
+ date: 'current',
};
}
return sortedVersion;
});
}
- return [{ date: "current" }];
+ return [{ date: 'current' }];
};
-
useEffect(() => {
const getVersions = async () => {
try {
const versions = await request(
getRequestUrl(`explorer/versions/${slug}/${entityId}`),
{
- method: "GET",
+ method: 'GET',
},
);
+ console.log('versions', versions);
setVersions(changeLatestDateToCurrent(versions));
} catch (err) {
- strapi.notification.error("content-manager.error.relation.fetch");
+ strapi.notification.error('content-manager.error.relation.fetch');
}
};
- if (entityId != "create") {
+ if (entityId != 'create') {
getVersions();
}
}, [slug, entityId]);
-
const generateDataForSelectedOption = () =>
versions.length <= 1
? {}
: versions.find((version) => version.date === selectedVersion).content;
- const isSingleType = useMemo(() => contentType === "singleType", [
- contentType,
- ]);
- const [
- { formattedContentTypeLayout, isDraggingComponent },
- dispatch,
- ] = useReducer(reducer, initialState, () => init(initialState));
- const allLayoutData = useMemo(() => get(layouts, [slug], {}), [
- layouts,
- slug,
- ]);
+ // Here in case of a 403 response when fetching data we will either redirect to the previous page
+ // Or to the homepage if there's no state in the history stack
+ const from = get(state, 'from', '/');
+
+ const {
+ createActionAllowedFields,
+ readActionAllowedFields,
+ updateActionAllowedFields,
+ } = useMemo(() => {
+ return getFieldsActionMatchingPermissions(userPermissions, slug);
+ }, [userPermissions, slug]);
+ const configurationPermissions = useMemo(() => {
+ return isSingleType
+ ? pluginPermissions.singleTypesConfigurations
+ : pluginPermissions.collectionTypesConfigurations;
+ }, [isSingleType]);
+
+ const configurationsURL = `/plugins/${pluginId}/${
+ isSingleType ? 'singleType' : 'collectionType'
+ }/${slug}/configurations/edit`;
const currentContentTypeLayoutData = useMemo(
- () => get(allLayoutData, ["contentType"], {}),
- [allLayoutData],
- );
- const currentContentTypeLayout = useMemo(
- () => get(currentContentTypeLayoutData, ["layouts", "edit"], []),
- [currentContentTypeLayoutData],
+ () => get(layout, ['contentType'], {}),
+ [layout],
);
+
const currentContentTypeLayoutRelations = useMemo(
- () => get(currentContentTypeLayoutData, ["layouts", "editRelations"], []),
- [currentContentTypeLayoutData],
- );
- const currentContentTypeSchema = useMemo(
- () => get(currentContentTypeLayoutData, ["schema"], {}),
+ () => get(currentContentTypeLayoutData, ['layouts', 'editRelations'], []),
[currentContentTypeLayoutData],
);
- const getFieldMetas = useCallback(
- (fieldName) => {
- return get(
- currentContentTypeLayoutData,
- ["metadatas", fieldName, "edit"],
- {},
- );
- },
- [currentContentTypeLayoutData],
- );
- const getField = useCallback(
- (fieldName) => {
- return get(currentContentTypeSchema, ["attributes", fieldName], {});
- },
- [currentContentTypeSchema],
- );
- const getFieldType = useCallback(
- (fieldName) => {
- return get(getField(fieldName), ["type"], "");
- },
- [getField],
- );
- const getFieldComponentUid = useCallback(
- (fieldName) => {
- return get(getField(fieldName), ["component"], "");
- },
- [getField],
+ const DataManagementWrapper = useMemo(
+ () => (isSingleType ? SingleTypeFormWrapper : CollectionTypeFormWrapper),
+ [isSingleType],
);
// Check if a block is a dynamic zone
- const isDynamicZone = useCallback(
- (block) => {
- return block.every((subBlock) => {
- return subBlock.every(
- (obj) => getFieldType(obj.name) === "dynamiczone",
- );
- });
- },
- [getFieldType],
- );
-
- useEffect(() => {
- // Force state to be cleared when navigation from one entry to another
- dispatch({ type: "RESET_PROPS" });
- dispatch({
- type: "SET_LAYOUT_DATA",
- formattedContentTypeLayout: formatLayoutRef.current(
- currentContentTypeLayout,
- currentContentTypeSchema.attributes,
- ),
+ const isDynamicZone = useCallback((block) => {
+ return block.every((subBlock) => {
+ return subBlock.every((obj) => obj.fieldSchema.type === 'dynamiczone');
});
+ }, []);
- return () => deleteLayout(slug);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [currentContentTypeLayout, currentContentTypeSchema.attributes]);
+ const formattedContentTypeLayout = useMemo(() => {
+ if (!currentContentTypeLayoutData.layouts) {
+ return [];
+ }
- const isVersionCurrent = () => selectedVersion === "current";
- const currentFieldNames = Object.keys(currentContentTypeSchema.attributes);
+ return createAttributesLayout(
+ currentContentTypeLayoutData.layouts.edit,
+ currentContentTypeLayoutData.attributes
+ );
+ }, [currentContentTypeLayoutData]);
+ const isVersionCurrent = () => selectedVersion === 'current';
+ const currentFieldNames = Object.keys(
+ currentContentTypeLayoutData.attributes,
+ );
const currentVersionFieldNames = () => {
- if (selectedVersion === "current") {
+ if (selectedVersion === 'current') {
return [];
}
-
return Object.keys(generateDataForSelectedOption());
};
const isSelectVersionContainsAllCurrentRelations = () => {
+ console.log(
+ 'generateDataForSelectedOption()',
+ generateDataForSelectedOption(),
+ );
if (generateDataForSelectedOption()) {
const selectedVersionAttributeNames = Object.keys(
generateDataForSelectedOption(),
);
-
return currentContentTypeLayoutRelations.every((el) =>
selectedVersionAttributeNames.includes(el),
);
@@ -228,10 +186,10 @@ const EditView = ({
const isRevertButtonDisabled = () => {
const unnecessaryAttributes = [
- "created_at",
- "created_by",
- "updated_at",
- "updated_by",
+ 'created_at',
+ 'created_by',
+ 'updated_at',
+ 'updated_by',
];
const sortedCurrentFieldNames = sortBy(
removeUnnecessaryAttributes(currentFieldNames, unnecessaryAttributes),
@@ -253,246 +211,261 @@ const EditView = ({
!isSelectVersionContainsAllCurrentRelations()
);
};
-
const findSelectedVersionRelationValue = (name) => {
const dataForSelectedOption = generateDataForSelectedOption();
return dataForSelectedOption[name];
};
+ // TODO: create a hook to handle/provide the permissions this should be done for the i18n feature
return (
- {
- dispatch({
- type: "SET_IS_DRAGGING_COMPONENT",
- });
- }}
- unsetIsDraggingComponent={() => {
- dispatch({
- type: "UNSET_IS_DRAGGING_COMPONENT",
- });
- }}
+
-
-
-
-
-
-
- {formattedContentTypeLayout.map((block, blockIndex) => {
- if (isDynamicZone(block)) {
- const {
- 0: {
- 0: { name },
- },
- } = block;
- const { max, min } = getField(name);
-
- return (
-
- );
- }
-
- return (
-
- {block.map((fieldsBlock, fieldsBlockIndex) => {
- return (
-
- {fieldsBlock.map(({ name, size }, fieldIndex) => {
- const isComponent =
- getFieldType(name) === "component";
-
- if (isComponent) {
- const componentUid = getFieldComponentUid(name);
- const isRepeatable = get(
- getField(name),
- "repeatable",
- false,
- );
- const { max, min } = getField(name);
-
- const label = get(
- getFieldMetas(name),
- "label",
- componentUid,
- );
-
+ {({
+ componentsDataStructure,
+ contentTypeDataStructure,
+ data,
+ isCreatingEntry,
+ isLoadingForData,
+ onDelete,
+ onDeleteSucceeded,
+ onPost,
+ onPublish,
+ onPut,
+ onUnpublish,
+ redirectionLink,
+ status,
+ }) => {
+ return (
+
+ {({ onChangeVersion }) => (
+ <>
+
+
+
+
+
+ {formattedContentTypeLayout.map((block, blockIndex) => {
+ if (isDynamicZone(block)) {
+ const {
+ 0: {
+ 0: { name, fieldSchema, metadatas, labelIcon },
+ },
+ } = block;
+ const baselineAlignementSize = blockIndex === 0 ? '3px' : '0';
+
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ {block.map((fieldsBlock, fieldsBlockIndex) => {
return (
-
+
+ {fieldsBlock.map(
+ ({ name, size, fieldSchema, labelIcon, metadatas }, fieldIndex) => {
+ const isComponent = fieldSchema.type === 'component';
+
+ if (isComponent) {
+ const { component, max, min, repeatable = false } = fieldSchema;
+ const componentUid = fieldSchema.component;
+
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+ );
+ },
+ )}
+
);
- }
-
- return (
-
-
-
- );
- })}
+ })}
+
+ );
+ })}
+
+
+
+
+ {currentContentTypeLayoutData.layouts.editRelations.length > 0 && (
+
+
+ {currentContentTypeLayoutData.layouts.editRelations.map(
+ ({ name, fieldSchema, labelIcon, metadatas, queryInfos }) => {
+ return (
+
+ );
+ },
+ )}
+
+
+ )}
+
+
+
+ {
+ // emitEvent('willEditContentTypeLayoutFromEditView');
+ }}
+ />
+
+ {getInjectedComponents(
+ 'editView',
+ 'right.links',
+ plugins,
+ currentEnvironment,
+ slug,
+ )}
+ {allowedActions.canDelete && (
+
+ )}
+
+
+ {entityId !== 'create' && (
+
+
+
+
+
+
- );
- })}
-
- );
- })}
-
-
-
- {currentContentTypeLayoutRelations.length > 0 && (
-
-
- {currentContentTypeLayoutRelations.map((relationName) => {
- const relation = get(
- currentContentTypeLayoutData,
- ["schema", "attributes", relationName],
- {},
- );
- const relationMetas = get(
- currentContentTypeLayoutData,
- ["metadatas", relationName, "edit"],
- {},
- );
-
- return (
-
- );
- })}
-
-
- )}
-
-
-
- {
- // emitEvent('willEditContentTypeLayoutFromEditView');
- }}
- />
-
- {getInjectedComponents(
- "editView",
- "right.links",
- plugins,
- currentEnvironment,
- slug,
- )}
-
-
- {entityId != "create" && (
-
-
-
-
- )}
-
-
-
-
-
+
+ >
+ )}
+
+ );
+ }}
+
);
};
EditView.defaultProps = {
- currentEnvironment: "production",
- emitEvent: () => {},
- plugins: {},
+ id: null,
+ isSingleType: false,
+ origin: null,
+ state: {},
};
EditView.propTypes = {
- components: PropTypes.array.isRequired,
- currentEnvironment: PropTypes.string,
- deleteLayout: PropTypes.func.isRequired,
- emitEvent: PropTypes.func,
- layouts: PropTypes.object.isRequired,
- plugins: PropTypes.object,
+ layout: PropTypes.shape({
+ components: PropTypes.object.isRequired,
+ contentType: PropTypes.shape({
+ uid: PropTypes.string.isRequired,
+ settings: PropTypes.object.isRequired,
+ metadatas: PropTypes.object.isRequired,
+ options: PropTypes.object.isRequired,
+ attributes: PropTypes.object.isRequired,
+ }).isRequired,
+ }).isRequired,
+ id: PropTypes.string,
+ isSingleType: PropTypes.bool,
+ goBack: PropTypes.func.isRequired,
+ origin: PropTypes.string,
+ state: PropTypes.object,
slug: PropTypes.string.isRequired,
};
diff --git a/packages/strapi-plugin-versioning/src/extensions/content-manager/admin/src/containers/EditViewDataManagerProvider/index.js b/packages/strapi-plugin-versioning/src/extensions/content-manager/admin/src/containers/EditViewDataManagerProvider/index.js
new file mode 100644
index 0000000..864500f
--- /dev/null
+++ b/packages/strapi-plugin-versioning/src/extensions/content-manager/admin/src/containers/EditViewDataManagerProvider/index.js
@@ -0,0 +1,558 @@
+import React, {
+ useCallback,
+ useEffect,
+ useMemo,
+ useReducer,
+ useRef,
+} from 'react';
+import { cloneDeep, get, isEmpty, isEqual, set } from 'lodash';
+import PropTypes from 'prop-types';
+import { Prompt, Redirect } from 'react-router-dom';
+import {
+ LoadingIndicatorPage,
+ useGlobalContext,
+ OverlayBlocker,
+ ContentManagerEditViewDataManagerContext,
+} from 'strapi-helper-plugin';
+import { getTrad, removeKeyInObject } from '../../utils';
+import reducer, { initialState } from './reducer';
+import { cleanData, createYupSchema, getYupInnerErrors } from './utils';
+
+const EditViewDataManagerProvider = ({
+ allLayoutData,
+ allowedActions: { canRead, canUpdate },
+ children,
+ componentsDataStructure,
+ contentTypeDataStructure,
+ createActionAllowedFields,
+ from,
+ initialValues,
+ isCreatingEntry,
+ isLoadingForData,
+ isSingleType,
+ onPost,
+ onPublish,
+ onPut,
+ onUnpublish,
+ readActionAllowedFields,
+ // Not sure this is needed anymore
+ redirectToPreviousPage,
+ slug,
+ status,
+ updateActionAllowedFields,
+}) => {
+ const [reducerState, dispatch] = useReducer(reducer, initialState);
+ const {
+ formErrors,
+ initialData,
+ modifiedData,
+ modifiedDZName,
+ shouldCheckErrors,
+ } = reducerState.toJS();
+
+ const currentContentTypeLayout = get(allLayoutData, ['contentType'], {});
+
+ const hasDraftAndPublish = useMemo(() => {
+ return get(currentContentTypeLayout, ['options', 'draftAndPublish'], false);
+ }, [currentContentTypeLayout]);
+
+ const shouldNotRunValidations = useMemo(() => {
+ return hasDraftAndPublish && !initialData.published_at;
+ }, [hasDraftAndPublish, initialData.published_at]);
+
+ const { emitEvent, formatMessage } = useGlobalContext();
+ const emitEventRef = useRef(emitEvent);
+
+ const shouldRedirectToHomepageWhenEditingEntry = useMemo(() => {
+ if (isLoadingForData) {
+ return false;
+ }
+
+ if (isCreatingEntry) {
+ return false;
+ }
+
+ if (canRead === false && canUpdate === false) {
+ return true;
+ }
+
+ return false;
+ }, [isLoadingForData, isCreatingEntry, canRead, canUpdate]);
+
+ // TODO check this effect if it is really needed (not prio)
+ useEffect(() => {
+ if (!isLoadingForData) {
+ checkFormErrors();
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [shouldCheckErrors]);
+
+ useEffect(() => {
+ if (shouldRedirectToHomepageWhenEditingEntry) {
+ strapi.notification.info(getTrad('permissions.not-allowed.update'));
+ }
+ }, [shouldRedirectToHomepageWhenEditingEntry]);
+
+ useEffect(() => {
+ dispatch({
+ type: 'SET_DEFAULT_DATA_STRUCTURES',
+ componentsDataStructure,
+ contentTypeDataStructure,
+ });
+ }, [componentsDataStructure, contentTypeDataStructure]);
+
+ useEffect(() => {
+ dispatch({
+ type: 'INIT_FORM',
+ initialValues,
+ });
+ }, [initialValues]);
+
+ const addComponentToDynamicZone = useCallback(
+ (keys, componentUid, shouldCheckErrors = false) => {
+ emitEventRef.current('didAddComponentToDynamicZone');
+
+ dispatch({
+ type: 'ADD_COMPONENT_TO_DYNAMIC_ZONE',
+ keys: keys.split('.'),
+ componentUid,
+ shouldCheckErrors,
+ });
+ },
+ [],
+ );
+
+ const addNonRepeatableComponentToField = useCallback((keys, componentUid) => {
+ dispatch({
+ type: 'ADD_NON_REPEATABLE_COMPONENT_TO_FIELD',
+ keys: keys.split('.'),
+ componentUid,
+ });
+ }, []);
+
+ const addRelation = useCallback(({ target: { name, value } }) => {
+ dispatch({
+ type: 'ADD_RELATION',
+ keys: name.split('.'),
+ value,
+ });
+ }, []);
+
+ const addRepeatableComponentToField = useCallback(
+ (keys, componentUid, shouldCheckErrors = false) => {
+ dispatch({
+ type: 'ADD_REPEATABLE_COMPONENT_TO_FIELD',
+ keys: keys.split('.'),
+ componentUid,
+ shouldCheckErrors,
+ });
+ },
+ [],
+ );
+
+ const yupSchema = useMemo(() => {
+ const options = {
+ isCreatingEntry,
+ isDraft: shouldNotRunValidations,
+ isFromComponent: false,
+ };
+
+ return createYupSchema(
+ currentContentTypeLayout,
+ {
+ components: allLayoutData.components || {},
+ },
+ options,
+ );
+ }, [
+ allLayoutData.components,
+ currentContentTypeLayout,
+ isCreatingEntry,
+ shouldNotRunValidations,
+ ]);
+
+ const checkFormErrors = useCallback(
+ async (dataToSet = {}) => {
+ let errors = {};
+ const updatedData = cloneDeep(modifiedData);
+
+ if (!isEmpty(updatedData)) {
+ set(updatedData, dataToSet.path, dataToSet.value);
+ }
+
+ try {
+ // Validate the form using yup
+ await yupSchema.validate(updatedData, { abortEarly: false });
+ } catch (err) {
+ errors = getYupInnerErrors(err);
+
+ if (modifiedDZName) {
+ errors = Object.keys(errors).reduce((acc, current) => {
+ const dzName = current.split('.')[0];
+
+ if (dzName !== modifiedDZName) {
+ acc[current] = errors[current];
+ }
+
+ return acc;
+ }, {});
+ }
+ }
+
+ dispatch({
+ type: 'SET_FORM_ERRORS',
+ errors,
+ });
+ },
+ [modifiedDZName, modifiedData, yupSchema],
+ );
+
+ const handleChange = useCallback(
+ ({ target: { name, value, type } }, shouldSetInitialValue = false) => {
+ let inputValue = value;
+
+ // Empty string is not a valid date,
+ // Set the date to null when it's empty
+ if (type === 'date' && value === '') {
+ inputValue = null;
+ }
+
+ if (type === 'password' && !value) {
+ dispatch({
+ type: 'REMOVE_PASSWORD_FIELD',
+ keys: name.split('.'),
+ });
+
+ return;
+ }
+
+ // Allow to reset enum
+ if (type === 'select-one' && value === '') {
+ inputValue = null;
+ }
+
+ // Allow to reset number input
+ if (type === 'number' && value === '') {
+ inputValue = null;
+ }
+
+ dispatch({
+ type: 'ON_CHANGE',
+ keys: name.split('.'),
+ value: inputValue,
+ shouldSetInitialValue,
+ });
+ },
+ [],
+ );
+
+ const createFormData = useCallback(
+ (data) => {
+ // First we need to remove the added keys needed for the dnd
+ const preparedData = removeKeyInObject(cloneDeep(data), '__temp_key__');
+ // Then we need to apply our helper
+ const cleanedData = cleanData(
+ preparedData,
+ currentContentTypeLayout,
+ allLayoutData.components,
+ );
+
+ return cleanedData;
+ },
+ [allLayoutData.components, currentContentTypeLayout],
+ );
+
+ const trackerProperty = useMemo(() => {
+ if (!hasDraftAndPublish) {
+ return {};
+ }
+
+ return shouldNotRunValidations ? { status: 'draft' } : {};
+ }, [hasDraftAndPublish, shouldNotRunValidations]);
+
+ const handleSubmit = useCallback(
+ async (e) => {
+ e.preventDefault();
+ let errors = {};
+
+ // First validate the form
+ try {
+ await yupSchema.validate(modifiedData, { abortEarly: false });
+
+ const formData = createFormData(modifiedData);
+
+ if (isCreatingEntry) {
+ onPost(formData, trackerProperty);
+ } else {
+ onPut(formData, trackerProperty);
+ }
+ } catch (err) {
+ console.error('ValidationError');
+ console.error(err);
+
+ errors = getYupInnerErrors(err);
+ }
+
+ dispatch({
+ type: 'SET_FORM_ERRORS',
+ errors,
+ });
+ },
+ [
+ createFormData,
+ isCreatingEntry,
+ modifiedData,
+ onPost,
+ onPut,
+ trackerProperty,
+ yupSchema,
+ ],
+ );
+
+ const handlePublish = useCallback(async () => {
+ // Create yup schema here's we need to apply all the validations
+ const schema = createYupSchema(
+ currentContentTypeLayout,
+ {
+ components: get(allLayoutData, 'components', {}),
+ },
+ { isCreatingEntry, isDraft: false, isFromComponent: false },
+ );
+ let errors = {};
+
+ try {
+ // Validate the form using yup
+ await schema.validate(modifiedData, { abortEarly: false });
+
+ onPublish();
+ } catch (err) {
+ console.error('ValidationError');
+ console.error(err);
+
+ errors = getYupInnerErrors(err);
+ }
+
+ dispatch({
+ type: 'SET_FORM_ERRORS',
+ errors,
+ });
+ }, [
+ allLayoutData,
+ currentContentTypeLayout,
+ isCreatingEntry,
+ modifiedData,
+ onPublish,
+ ]);
+
+ const shouldCheckDZErrors = useCallback(
+ (dzName) => {
+ const doesDZHaveError = Object.keys(formErrors).some(
+ (key) => key.split('.')[0] === dzName,
+ );
+ const shouldCheckErrors = !isEmpty(formErrors) && doesDZHaveError;
+
+ return shouldCheckErrors;
+ },
+ [formErrors],
+ );
+
+ const moveComponentDown = useCallback(
+ (dynamicZoneName, currentIndex) => {
+ emitEventRef.current('changeComponentsOrder');
+
+ dispatch({
+ type: 'MOVE_COMPONENT_DOWN',
+ dynamicZoneName,
+ currentIndex,
+ shouldCheckErrors: shouldCheckDZErrors(dynamicZoneName),
+ });
+ },
+ [shouldCheckDZErrors],
+ );
+
+ const moveComponentUp = useCallback(
+ (dynamicZoneName, currentIndex) => {
+ emitEventRef.current('changeComponentsOrder');
+
+ dispatch({
+ type: 'MOVE_COMPONENT_UP',
+ dynamicZoneName,
+ currentIndex,
+ shouldCheckErrors: shouldCheckDZErrors(dynamicZoneName),
+ });
+ },
+ [shouldCheckDZErrors],
+ );
+
+ const moveComponentField = useCallback(
+ (pathToComponent, dragIndex, hoverIndex) => {
+ dispatch({
+ type: 'MOVE_COMPONENT_FIELD',
+ pathToComponent,
+ dragIndex,
+ hoverIndex,
+ });
+ },
+ [],
+ );
+
+ const moveRelation = useCallback((dragIndex, overIndex, name) => {
+ dispatch({
+ type: 'MOVE_FIELD',
+ dragIndex,
+ overIndex,
+ keys: name.split('.'),
+ });
+ }, []);
+
+ const onRemoveRelation = useCallback((keys) => {
+ dispatch({
+ type: 'REMOVE_RELATION',
+ keys,
+ });
+ }, []);
+
+ const removeComponentFromDynamicZone = useCallback(
+ (dynamicZoneName, index) => {
+ emitEventRef.current('removeComponentFromDynamicZone');
+
+ dispatch({
+ type: 'REMOVE_COMPONENT_FROM_DYNAMIC_ZONE',
+ dynamicZoneName,
+ index,
+ shouldCheckErrors: shouldCheckDZErrors(dynamicZoneName),
+ });
+ },
+ [shouldCheckDZErrors],
+ );
+
+ const removeComponentFromField = useCallback((keys, componentUid) => {
+ dispatch({
+ type: 'REMOVE_COMPONENT_FROM_FIELD',
+ keys: keys.split('.'),
+ componentUid,
+ });
+ }, []);
+
+ const removeRepeatableField = useCallback((keys, componentUid) => {
+ dispatch({
+ type: 'REMOVE_REPEATABLE_FIELD',
+ keys: keys.split('.'),
+ componentUid,
+ });
+ }, []);
+
+ const triggerFormValidation = useCallback(() => {
+ dispatch({
+ type: 'TRIGGER_FORM_VALIDATION',
+ });
+ }, []);
+
+ const onChangeVersion = useCallback((version) => {
+ dispatch({
+ type: 'CHANGE_VERSION',
+ payload: version,
+ });
+ }, []);
+
+ const overlayBlockerParams = useMemo(
+ () => ({
+ children: ,
+ noGradient: true,
+ }),
+ []
+ );
+
+ // Redirect the user to the previous page if he is not allowed to read/update a document
+ if (shouldRedirectToHomepageWhenEditingEntry) {
+ return ;
+ }
+
+ return (
+
+ <>
+
+ {isLoadingForData ? (
+
+ ) : (
+ <>
+
+
+ >
+ )}
+ >
+
+ );
+};
+
+EditViewDataManagerProvider.defaultProps = {
+ from: '/',
+ redirectToPreviousPage: () => {},
+};
+
+EditViewDataManagerProvider.propTypes = {
+ allLayoutData: PropTypes.object.isRequired,
+ allowedActions: PropTypes.object.isRequired,
+ children: PropTypes.arrayOf(PropTypes.element).isRequired,
+ componentsDataStructure: PropTypes.object.isRequired,
+ contentTypeDataStructure: PropTypes.object.isRequired,
+ createActionAllowedFields: PropTypes.array.isRequired,
+ from: PropTypes.string,
+ initialValues: PropTypes.object.isRequired,
+ isCreatingEntry: PropTypes.bool.isRequired,
+ isLoadingForData: PropTypes.bool.isRequired,
+ isSingleType: PropTypes.bool.isRequired,
+ onPost: PropTypes.func.isRequired,
+ onPublish: PropTypes.func.isRequired,
+ onPut: PropTypes.func.isRequired,
+ onUnpublish: PropTypes.func.isRequired,
+ readActionAllowedFields: PropTypes.array.isRequired,
+ redirectToPreviousPage: PropTypes.func,
+ slug: PropTypes.string.isRequired,
+ status: PropTypes.string.isRequired,
+ updateActionAllowedFields: PropTypes.array.isRequired,
+};
+
+export default EditViewDataManagerProvider;
diff --git a/packages/strapi-plugin-versioning/src/extensions/content-manager/admin/src/containers/EditViewDataManagerProvider/reducer.js b/packages/strapi-plugin-versioning/src/extensions/content-manager/admin/src/containers/EditViewDataManagerProvider/reducer.js
new file mode 100644
index 0000000..3a13d8d
--- /dev/null
+++ b/packages/strapi-plugin-versioning/src/extensions/content-manager/admin/src/containers/EditViewDataManagerProvider/reducer.js
@@ -0,0 +1,248 @@
+import { fromJS } from 'immutable';
+import { getMaxTempKey } from '../../utils';
+
+const initialState = fromJS({
+ componentsDataStructure: {},
+ contentTypeDataStructure: {},
+ formErrors: {},
+ initialData: {},
+ modifiedData: {},
+ shouldCheckErrors: false,
+ modifiedDZName: null,
+});
+
+const reducer = (state, action) => {
+ switch (action.type) {
+ case 'ADD_NON_REPEATABLE_COMPONENT_TO_FIELD':
+ return state.updateIn(['modifiedData', ...action.keys], () => {
+ const defaultDataStructure = state.getIn([
+ 'componentsDataStructure',
+ action.componentUid,
+ ]);
+
+ return fromJS(defaultDataStructure);
+ });
+ case 'ADD_REPEATABLE_COMPONENT_TO_FIELD': {
+ return state
+ .updateIn(['modifiedData', ...action.keys], (list) => {
+ const defaultDataStructure = state
+ .getIn(['componentsDataStructure', action.componentUid])
+ .set('__temp_key__', getMaxTempKey(list ? list.toJS() : []) + 1);
+
+ if (list) {
+ return list.push(defaultDataStructure);
+ }
+
+ return fromJS([defaultDataStructure]);
+ })
+ .update('shouldCheckErrors', (v) => {
+ if (action.shouldCheckErrors === true) {
+ return !v;
+ }
+
+ return v;
+ });
+ }
+ case 'ADD_COMPONENT_TO_DYNAMIC_ZONE':
+ return state
+ .updateIn(['modifiedData', ...action.keys], (list) => {
+ const defaultDataStructure = state
+ .getIn(['componentsDataStructure', action.componentUid])
+ .set('__component', action.componentUid);
+
+ if (list) {
+ return list.push(defaultDataStructure);
+ }
+
+ return fromJS([defaultDataStructure]);
+ })
+ .update('modifiedDZName', () => action.keys[0])
+ .update('shouldCheckErrors', (v) => {
+ if (action.shouldCheckErrors === true) {
+ return !v;
+ }
+
+ return v;
+ });
+ case 'ADD_RELATION':
+ return state.updateIn(['modifiedData', ...action.keys], (list) => {
+ if (!Array.isArray(action.value) || !action.value.length) {
+ return list;
+ }
+
+ const el = action.value[0].value;
+
+ if (list) {
+ return list.push(fromJS(el));
+ }
+
+ return fromJS([el]);
+ });
+ case 'INIT_FORM': {
+ return state
+ .update('formErrors', () => fromJS({}))
+ .update('initialData', () => fromJS(action.initialValues))
+ .update('modifiedData', () => fromJS(action.initialValues))
+ .update('modifiedDZName', () => null)
+ .update('shouldCheckErrors', () => false);
+ }
+ case 'MOVE_COMPONENT_FIELD':
+ return state.updateIn(
+ ['modifiedData', ...action.pathToComponent],
+ (list) => {
+ return list
+ .delete(action.dragIndex)
+ .insert(
+ action.hoverIndex,
+ state.getIn([
+ 'modifiedData',
+ ...action.pathToComponent,
+ action.dragIndex,
+ ]),
+ );
+ },
+ );
+ case 'MOVE_COMPONENT_UP':
+ return state
+ .update('shouldCheckErrors', (v) => {
+ if (action.shouldCheckErrors) {
+ return !v;
+ }
+
+ return v;
+ })
+ .updateIn(['modifiedData', action.dynamicZoneName], (list) => {
+ return list
+ .delete(action.currentIndex)
+ .insert(
+ action.currentIndex - 1,
+ state.getIn([
+ 'modifiedData',
+ action.dynamicZoneName,
+ action.currentIndex,
+ ]),
+ );
+ });
+ case 'MOVE_COMPONENT_DOWN':
+ return state
+ .update('shouldCheckErrors', (v) => {
+ if (action.shouldCheckErrors) {
+ return !v;
+ }
+
+ return v;
+ })
+ .updateIn(['modifiedData', action.dynamicZoneName], (list) => {
+ return list
+ .delete(action.currentIndex)
+ .insert(
+ action.currentIndex + 1,
+ state.getIn([
+ 'modifiedData',
+ action.dynamicZoneName,
+ action.currentIndex,
+ ]),
+ );
+ });
+ case 'MOVE_FIELD':
+ return state.updateIn(['modifiedData', ...action.keys], (list) => {
+ return list
+ .delete(action.dragIndex)
+ .insert(action.overIndex, list.get(action.dragIndex));
+ });
+ case 'ON_CHANGE': {
+ let newState = state;
+ const [nonRepeatableComponentKey] = action.keys;
+
+ // This is used to set the initialData for inputs
+ // that needs an asynchronous initial value like the UID field
+ // This is just a temporary patch.
+ // TODO : Refactor the default form creation (workflow) to accept async default values.
+ if (action.shouldSetInitialValue) {
+ newState = state.updateIn(['initialData', ...action.keys], () => {
+ return action.value;
+ });
+ }
+
+ if (
+ action.keys.length === 2 &&
+ state.getIn(['modifiedData', nonRepeatableComponentKey]) === null
+ ) {
+ newState = newState.updateIn(
+ ['modifiedData', nonRepeatableComponentKey],
+ () => fromJS({}),
+ );
+ }
+
+ return newState.updateIn(['modifiedData', ...action.keys], () => {
+ return action.value;
+ });
+ }
+ case 'REMOVE_COMPONENT_FROM_DYNAMIC_ZONE':
+ return state
+ .update('shouldCheckErrors', (v) => {
+ if (action.shouldCheckErrors) {
+ return !v;
+ }
+
+ return v;
+ })
+ .deleteIn(['modifiedData', action.dynamicZoneName, action.index]);
+ case 'REMOVE_COMPONENT_FROM_FIELD': {
+ const componentPathToRemove = ['modifiedData', ...action.keys];
+
+ return state.updateIn(componentPathToRemove, () => null);
+ }
+ case 'REMOVE_PASSWORD_FIELD': {
+ return state.removeIn(['modifiedData', ...action.keys]);
+ }
+ case 'REMOVE_REPEATABLE_FIELD': {
+ const componentPathToRemove = ['modifiedData', ...action.keys];
+
+ return state
+ .update('shouldCheckErrors', (v) => {
+ const hasErrors = state.get('formErrors').keySeq().size > 0;
+
+ if (hasErrors) {
+ return !v;
+ }
+
+ return v;
+ })
+ .deleteIn(componentPathToRemove);
+ }
+ case 'REMOVE_RELATION':
+ return state.removeIn(['modifiedData', ...action.keys.split('.')]);
+ case 'SET_DEFAULT_DATA_STRUCTURES':
+ return state
+ .update('componentsDataStructure', () =>
+ fromJS(action.componentsDataStructure),
+ )
+ .update('contentTypeDataStructure', () =>
+ fromJS(action.contentTypeDataStructure),
+ );
+ case 'SET_FORM_ERRORS': {
+ return state
+ .update('modifiedDZName', () => null)
+ .update('formErrors', () => fromJS(action.errors));
+ }
+ case 'TRIGGER_FORM_VALIDATION':
+ return state.update('shouldCheckErrors', (v) => {
+ const hasErrors = state.get('formErrors').keySeq().size > 0;
+
+ if (hasErrors) {
+ return !v;
+ }
+
+ return v;
+ });
+ case 'CHANGE_VERSION': {
+ return state.update('modifiedData', () => fromJS(action.payload));
+ }
+ default:
+ return state;
+ }
+};
+
+export default reducer;
+export { initialState };
diff --git a/packages/strapi-plugin-versioning/src/extensions/content-manager/admin/src/translations/en.json b/packages/strapi-plugin-versioning/src/extensions/content-manager/admin/src/translations/en.json
index afc7ff8..e7a259b 100644
--- a/packages/strapi-plugin-versioning/src/extensions/content-manager/admin/src/translations/en.json
+++ b/packages/strapi-plugin-versioning/src/extensions/content-manager/admin/src/translations/en.json
@@ -1,17 +1,14 @@
{
- "api.id": "API ID",
- "models": "Collection Types",
- "models.numbered": "Collection Types ({number})",
- "groups": "Groups",
- "groups.numbered": "Groups ({number})",
"EditRelations.title": "Relational data",
+ "api.id": "API ID",
"components.AddFilterCTA.add": "Filters",
"components.AddFilterCTA.hide": "Filters",
"components.DraggableAttr.edit": "Click to edit",
"components.DynamicZone.add-compo": "Add to {componentName}",
- "components.DynamicZone.pick-compo": "Pick one component",
- "components.DynamicZone.missing.singular": "There is {count} missing component",
"components.DynamicZone.missing.plural": "There is {count} missing components",
+ "components.DynamicZone.missing.singular": "There is {count} missing component",
+ "components.DynamicZone.pick-compo": "Pick one component",
+ "components.DynamicZone.required": "Component is required",
"components.EmptyAttributesBlock.button": "Go to settings page",
"components.EmptyAttributesBlock.description": "You can change your settings",
"components.FieldItem.linkToComponentLayout": "Set the component's layout",
@@ -21,86 +18,95 @@
"components.FiltersPickWrapper.PluginHeader.description": "Set the conditions to apply to filter the entries",
"components.FiltersPickWrapper.PluginHeader.title.filter": "Filters",
"components.FiltersPickWrapper.hide": "Hide",
- "components.notification.info.minimum-requirement": "A field has been added to match the minimum requirement",
- "components.notification.info.maximum-requirement": "You have already reached the maximum number of fields",
- "components.empty-repeatable": "No entry yet. Click on the button below to add one.",
- "components.reset-entry": "Reset entry",
"components.LimitSelect.itemsPerPage": "Items per page",
+ "components.NotAllowedInput.text": "No permissions to see this field",
"components.Search.placeholder": "Search for an entry...",
-
- "components.SettingsViewWrapper.pluginHeader.title": "Configure the view - {name}",
+ "components.Select.draft-info-title": "State: Draft",
+ "components.Select.publish-info-title": "State: Published",
"components.SettingsViewWrapper.pluginHeader.description.edit-settings": "Customize how the edit view will look like.",
"components.SettingsViewWrapper.pluginHeader.description.list-settings": "Define the settings of the list view.",
-
+ "components.SettingsViewWrapper.pluginHeader.title": "Configure the view - {name}",
"components.TableDelete.delete": "Delete all",
"components.TableDelete.deleteSelected": "Delete selected",
"components.TableDelete.entries.plural": "{number} entries selected",
"components.TableDelete.entries.singular": "{number} entry selected",
- "components.TableEmpty.withFilters": "There is no {contentType} with the applied filters...",
- "components.TableEmpty.withSearch": "There is no {contentType} corresponding to the search ({search})...",
- "components.TableEmpty.withoutFilter": "There is no {contentType}...",
-
- "components.uid.available": "available",
+ "components.TableEmpty.withFilters": "There are no {contentType} with the applied filters...",
+ "components.TableEmpty.withSearch": "There are no {contentType} corresponding to the search ({search})...",
+ "components.TableEmpty.withoutFilter": "There are no {contentType}...",
+ "components.empty-repeatable": "No entry yet. Click on the button below to add one.",
+ "components.repeatable.reorder.error": "An error occurred while reordering your component's field, please try again",
+ "components.notification.info.maximum-requirement": "You have already reached the maximum number of fields",
+ "components.notification.info.minimum-requirement": "A field has been added to match the minimum requirement",
+ "components.reset-entry": "Reset entry",
"components.uid.apply": "apply",
+ "components.uid.available": "available",
"components.uid.regenerate": "regenerate",
"components.uid.suggested": "suggested",
"components.uid.unavailable": "unavailable",
-
+ "containers.Edit.Link.Fields": "Edit the fields",
+ "containers.Edit.Link.Layout": "Configure the layout",
+ "containers.Edit.Link.Model": "Edit the collection-type",
"containers.Edit.addAnItem": "Add an item...",
- "containers.Edit.pluginHeader.title.new": "Create an entry",
"containers.Edit.clickToJump": "Click to jump to the entry",
"containers.Edit.delete": "Delete",
+ "containers.Edit.delete-entry": "Delete this entry",
"containers.Edit.editing": "Editing...",
+ "containers.Edit.information": "Information",
+ "containers.Edit.information.by": "By",
+ "containers.Edit.information.draftVersion": "draft version",
+ "containers.Edit.information.editing": "Editing",
+ "containers.Edit.information.lastUpdate": "Last update",
+ "containers.Edit.information.publishedVersion": "published version",
+ "containers.Edit.pluginHeader.title.new": "Create an entry",
"containers.Edit.reset": "Reset",
"containers.Edit.returnList": "Return to list",
"containers.Edit.seeDetails": "Details",
"containers.Edit.submit": "Save",
- "containers.Edit.Link.Layout": "Configure the layout",
- "containers.Edit.Link.Fields": "Edit the fields",
- "containers.Edit.Link.Model": "Edit the collection-type",
+ "containers.EditSettingsView.modal-form.edit-field": "Edit the field",
+ "containers.EditView.add.new": "ADD NEW ENTRY",
+ "containers.EditView.components.missing.plural": "There is {count} missing components",
+ "containers.EditView.components.missing.singular": "There is {count} missing component",
"containers.EditView.notification.errors": "The form contains some errors",
- "containers.EditView.revert": "Revert",
- "containers.EditView.versions": "Versions",
"containers.Home.introduction": "To edit your entries go to the specific link in the left menu. This plugin doesn't have a proper way to edit settings and it's still under active development.",
"containers.Home.pluginHeaderDescription": "Manage your entries through a powerful and beautiful interface.",
"containers.Home.pluginHeaderTitle": "Content Manager",
-
"containers.List.addAnEntry": "Add New {entity}",
+ "containers.List.draft": "Draft",
"containers.List.errorFetchRecords": "Error",
"containers.List.pluginHeaderDescription": "{label} entries found",
"containers.List.pluginHeaderDescription.singular": "{label} entry found",
+ "containers.List.published": "Published",
"containers.ListPage.displayedFields": "Displayed Fields",
-
+ "containers.ListPage.items.plural": "items",
+ "containers.ListPage.items.singular": "item",
+ "containers.ListPage.table-headers.published_at": "State",
"containers.ListSettingsView.modal-form.edit-label": "Edit the label",
- "containers.EditSettingsView.modal-form.edit-field": "Edit the field",
-
"containers.SettingPage.add.field": "Insert another field",
"containers.SettingPage.add.relational-field": "Insert another relational field",
"containers.SettingPage.attributes": "Attributes fields",
"containers.SettingPage.attributes.description": "Define the order of the attributes",
"containers.SettingPage.editSettings.description": "Drag & drop the fields to build the layout",
- "containers.SettingPage.editSettings.title": "Edit view (settings)",
"containers.SettingPage.editSettings.entry.title": "Entry title",
"containers.SettingPage.editSettings.entry.title.description": "Set the displayed field of your entry",
+ "containers.SettingPage.editSettings.relation-field.description": "Set the displayed field in both the edit and list views",
+ "containers.SettingPage.editSettings.title": "Edit view (settings)",
+ "containers.SettingPage.layout": "Layout",
"containers.SettingPage.listSettings.description": "Configure the options for this collection type",
"containers.SettingPage.listSettings.title": "List view (settings)",
"containers.SettingPage.pluginHeaderDescription": "Configure the specific settings for this Collection Type",
"containers.SettingPage.relations": "Relational fields",
"containers.SettingPage.settings": "Settings",
- "containers.SettingPage.layout": "Layout",
+ "containers.EditView.revert": "Revert",
"containers.SettingPage.view": "View",
- "containers.EditView.add.new": "ADD NEW ENTRY",
- "containers.EditView.components.missing.singular": "There is {count} missing component",
- "containers.EditView.components.missing.plural": "There is {count} missing components",
"containers.SettingViewModel.pluginHeader.title": "Content Manager - {name}",
"containers.SettingsPage.Block.contentType.description": "Configure the specific settings",
"containers.SettingsPage.Block.contentType.title": "Collection Types",
"containers.SettingsPage.Block.generalSettings.description": "Configure the default options for your Collection Types",
"containers.SettingsPage.Block.generalSettings.title": "General",
"containers.SettingsPage.pluginHeaderDescription": "Configure the settings for all your Collection types and Groups",
- "containers.SettingsView.list.title": "Display configurations",
"containers.SettingsView.list.subtitle": "Configure the layout and display of your Collection types and groups",
-
+ "containers.SettingsView.list.title": "Display configurations",
+ "containers.EditView.versions": "Versions",
"emptyAttributes.button": "Go to collection type builder",
"emptyAttributes.description": "Add your first field to your Collection Type",
"emptyAttributes.title": "There are no fields yet",
@@ -122,10 +128,9 @@
"error.validation.min": "The value is too low.",
"error.validation.minLength": "The value is too short.",
"error.validation.minSupMax": "Can't be superior",
- "error.validation.regex": "The value not match the regex.",
+ "error.validation.regex": "The value does not match the regex.",
"error.validation.required": "This value input is required.",
"error.relation.fetch": "Unable to get versions data",
-
"form.Input.bulkActions": "Enable bulk actions",
"form.Input.defaultSort": "Default sort attribute",
"form.Input.description": "Description",
@@ -142,25 +147,34 @@
"form.Input.search.field": "Enable search on this field",
"form.Input.sort.field": "Enable sort on this field",
"form.Input.wysiwyg": "Display as WYSIWYG",
-
"global.displayedFields": "Displayed Fields",
-
+ "groups": "Groups",
+ "groups.numbered": "Groups ({number})",
+ "models": "Collection Types",
+ "models.numbered": "Collection Types ({number})",
"notification.error.displayedFields": "You need at least one displayed field",
"notification.error.relationship.fetch": "An error occurred during relationship fetch.",
"notification.info.SettingPage.disableSort": "You need to have one attribute with the sorting allowed",
+ "notification.info.minimumFields": "You need to have at least one field displayed",
+ "notification.upload.error": "An error has occurred while uploading your files",
"pageNotFound": "Page not found",
+ "permissions.not-allowed.create": "You are not allowed to create a document",
+ "permissions.not-allowed.update": "You are not allowed to see this document",
"plugin.description.long": "Quick way to see, edit and delete the data in your database.",
"plugin.description.short": "Quick way to see, edit and delete the data in your database.",
"popUpWarning.bodyMessage.contentType.delete": "Are you sure you want to delete this entry?",
"popUpWarning.bodyMessage.contentType.delete.all": "Are you sure you want to delete theses entries?",
- "popUpWarning.button.cancel": "Cancel",
- "popUpWarning.button.confirm": "Confirm",
- "popUpWarning.title": "Please confirm",
"popUpWarning.warning.cancelAllSettings": "Are you sure you want to cancel your modifications?",
+ "popUpWarning.warning.publish-question": "Do you still want to publish it?",
+ "popUpWarning.warning.unpublish": "Unpublish this content will
automatically change it to a draft.",
+ "popUpWarning.warning.unpublish-question": "Are you sure you want to unpublish it?",
"popUpWarning.warning.updateAllSettings": "This will modify all your settings",
+ "popUpwarning.warning.has-draft-relations.button-confirm": "Yes, publish",
+ "popUpwarning.warning.has-draft-relations.message.plural": "{count} of your content relations are not published yet.
It might engender broken links and errors on your project.",
+ "popUpwarning.warning.has-draft-relations.message.singular": "{count} of your content relations is not published yet.
It might engender broken links and errors on your project.",
+ "popUpwarning.warning.has-draft-relations.second-message": "It might engender broken links and errors on your project.",
"success.record.delete": "Deleted",
+ "success.record.publish": "Published",
"success.record.save": "Saved",
- "notification.info.minimumFields": "You need to have at least one field displayed",
- "notification.upload.error": "An error has occured while uploading your files"
+ "success.record.unpublish": "Unpublished"
}
-
diff --git a/packages/strapi-plugin-versioning/src/extensions/content-manager/config/routes.json b/packages/strapi-plugin-versioning/src/extensions/content-manager/config/routes.json
index a8cf56e..29cdfab 100644
--- a/packages/strapi-plugin-versioning/src/extensions/content-manager/config/routes.json
+++ b/packages/strapi-plugin-versioning/src/extensions/content-manager/config/routes.json
@@ -11,123 +11,334 @@
{
"method": "GET",
"path": "/content-types",
- "handler": "ContentTypes.listContentTypes",
+ "handler": "content-types.findContentTypes",
"config": {
"policies": []
}
},
{
"method": "GET",
- "path": "/content-types/:uid",
- "handler": "ContentTypes.findContentType",
+ "path": "/content-types/:uid/configuration",
+ "handler": "content-types.findContentTypeConfiguration",
"config": {
"policies": []
}
},
{
"method": "PUT",
- "path": "/content-types/:uid",
- "handler": "ContentTypes.updateContentType",
+ "path": "/content-types/:uid/configuration",
+ "handler": "content-types.updateContentTypeConfiguration",
"config": {
- "policies": []
+ "policies": [
+ "admin::isAuthenticatedAdmin"
+ ]
}
},
{
"method": "GET",
"path": "/components",
- "handler": "Components.listComponents",
+ "handler": "components.findComponents",
"config": {
"policies": []
}
},
{
"method": "GET",
- "path": "/components/:uid",
- "handler": "Components.findComponent",
+ "path": "/components/:uid/configuration",
+ "handler": "components.findComponentConfiguration",
"config": {
"policies": []
}
},
{
"method": "PUT",
- "path": "/components/:uid",
- "handler": "Components.updateComponent",
+ "path": "/components/:uid/configuration",
+ "handler": "components.updateComponentConfiguration",
"config": {
"policies": []
}
},
{
"method": "POST",
- "path": "/explorer/uid/generate",
- "handler": "ContentManager.generateUID",
+ "path": "/uid/generate",
+ "handler": "uid.generateUID",
"config": {
"policies": []
}
},
{
"method": "POST",
- "path": "/explorer/uid/check-availability",
- "handler": "ContentManager.checkUIDAvailability",
+ "path": "/uid/check-availability",
+ "handler": "uid.checkUIDAvailability",
"config": {
"policies": []
}
},
+ {
+ "method": "POST",
+ "path": "/relations/:model/:targetField",
+ "handler": "relations.find",
+ "config": {
+ "policies": [
+ "admin::isAuthenticatedAdmin",
+ [
+ "plugins::content-manager.hasPermissions",
+ [
+ "plugins::content-manager.explorer.create",
+ "plugins::content-manager.explorer.update"
+ ],
+ {
+ "hasAtLeastOne": true
+ }
+ ]
+ ]
+ }
+ },
{
"method": "GET",
- "path": "/explorer/:model",
- "handler": "ContentManager.find",
+ "path": "/single-types/:model",
+ "handler": "single-types.find",
+ "config": {
+ "policies": [
+ "routing",
+ "admin::isAuthenticatedAdmin",
+ [
+ "plugins::content-manager.hasPermissions",
+ [
+ "plugins::content-manager.explorer.read"
+ ]
+ ]
+ ]
+ }
+ },
+ {
+ "method": "PUT",
+ "path": "/single-types/:model",
+ "handler": "single-types.createOrUpdate",
"config": {
- "policies": ["routing"]
+ "policies": [
+ "routing",
+ "admin::isAuthenticatedAdmin",
+ [
+ "plugins::content-manager.hasPermissions",
+ [
+ "plugins::content-manager.explorer.create",
+ "plugins::content-manager.explorer.update"
+ ],
+ {
+ "hasAtLeastOne": true
+ }
+ ]
+ ]
+ }
+ },
+ {
+ "method": "DELETE",
+ "path": "/single-types/:model",
+ "handler": "single-types.delete",
+ "config": {
+ "policies": [
+ "routing",
+ "admin::isAuthenticatedAdmin",
+ [
+ "plugins::content-manager.hasPermissions",
+ [
+ "plugins::content-manager.explorer.delete"
+ ]
+ ]
+ ]
+ }
+ },
+ {
+ "method": "POST",
+ "path": "/single-types/:model/actions/publish",
+ "handler": "single-types.publish",
+ "config": {
+ "policies": [
+ "routing",
+ "plugins::content-manager.has-draft-and-publish",
+ "admin::isAuthenticatedAdmin",
+ [
+ "plugins::content-manager.hasPermissions",
+ [
+ "plugins::content-manager.explorer.publish"
+ ]
+ ]
+ ]
+ }
+ },
+ {
+ "method": "POST",
+ "path": "/single-types/:model/actions/unpublish",
+ "handler": "single-types.unpublish",
+ "config": {
+ "policies": [
+ "routing",
+ "plugins::content-manager.has-draft-and-publish",
+ "admin::isAuthenticatedAdmin",
+ [
+ "plugins::content-manager.hasPermissions",
+ [
+ "plugins::content-manager.explorer.publish"
+ ]
+ ]
+ ]
}
},
{
"method": "GET",
- "path": "/explorer/:model/count",
- "handler": "ContentManager.count",
+ "path": "/collection-types/:model/:id/:targetField",
+ "handler": "collection-types.previewManyRelations",
"config": {
- "policies": ["routing"]
+ "policies": [
+ "routing",
+ "admin::isAuthenticatedAdmin",
+ [
+ "plugins::content-manager.hasPermissions",
+ [
+ "plugins::content-manager.explorer.read"
+ ]
+ ]
+ ]
}
},
{
"method": "GET",
- "path": "/explorer/:model/:id",
- "handler": "ContentManager.findOne",
+ "path": "/collection-types/:model",
+ "handler": "collection-types.find",
"config": {
- "policies": ["routing"]
+ "policies": [
+ "routing",
+ "admin::isAuthenticatedAdmin",
+ [
+ "plugins::content-manager.hasPermissions",
+ [
+ "plugins::content-manager.explorer.read"
+ ]
+ ]
+ ]
}
},
{
"method": "POST",
- "path": "/explorer/:model",
- "handler": "ContentManager.create",
+ "path": "/collection-types/:model",
+ "handler": "collection-types.create",
"config": {
- "policies": ["routing"]
+ "policies": [
+ "routing",
+ "admin::isAuthenticatedAdmin",
+ [
+ "plugins::content-manager.hasPermissions",
+ [
+ "plugins::content-manager.explorer.create"
+ ]
+ ]
+ ]
+ }
+ },
+ {
+ "method": "GET",
+ "path": "/collection-types/:model/:id",
+ "handler": "collection-types.findOne",
+ "config": {
+ "policies": [
+ "routing",
+ "admin::isAuthenticatedAdmin",
+ [
+ "plugins::content-manager.hasPermissions",
+ [
+ "plugins::content-manager.explorer.read"
+ ]
+ ]
+ ]
}
},
{
"method": "PUT",
- "path": "/explorer/:model/:id",
- "handler": "ContentManager.update",
+ "path": "/collection-types/:model/:id",
+ "handler": "collection-types.update",
"config": {
- "policies": ["routing"]
+ "policies": [
+ "routing",
+ "admin::isAuthenticatedAdmin",
+ [
+ "plugins::content-manager.hasPermissions",
+ [
+ "plugins::content-manager.explorer.update"
+ ]
+ ]
+ ]
}
},
{
"method": "DELETE",
- "path": "/explorer/deleteAll/:model",
- "handler": "ContentManager.deleteMany",
+ "path": "/collection-types/:model/:id",
+ "handler": "collection-types.delete",
"config": {
- "policies": ["routing"]
+ "policies": [
+ "routing",
+ "admin::isAuthenticatedAdmin",
+ [
+ "plugins::content-manager.hasPermissions",
+ [
+ "plugins::content-manager.explorer.delete"
+ ]
+ ]
+ ]
}
},
{
- "method": "DELETE",
- "path": "/explorer/:model/:id",
- "handler": "ContentManager.delete",
+ "method": "POST",
+ "path": "/collection-types/:model/:id/actions/publish",
+ "handler": "collection-types.publish",
+ "config": {
+ "policies": [
+ "routing",
+ "plugins::content-manager.has-draft-and-publish",
+ "admin::isAuthenticatedAdmin",
+ [
+ "plugins::content-manager.hasPermissions",
+ [
+ "plugins::content-manager.explorer.publish"
+ ]
+ ]
+ ]
+ }
+ },
+ {
+ "method": "POST",
+ "path": "/collection-types/:model/:id/actions/unpublish",
+ "handler": "collection-types.unpublish",
+ "config": {
+ "policies": [
+ "routing",
+ "plugins::content-manager.has-draft-and-publish",
+ "admin::isAuthenticatedAdmin",
+ [
+ "plugins::content-manager.hasPermissions",
+ [
+ "plugins::content-manager.explorer.publish"
+ ]
+ ]
+ ]
+ }
+ },
+ {
+ "method": "POST",
+ "path": "/collection-types/:model/actions/bulkDelete",
+ "handler": "collection-types.bulkDelete",
"config": {
- "policies": ["routing"]
+ "policies": [
+ "routing",
+ "admin::isAuthenticatedAdmin",
+ [
+ "plugins::content-manager.hasPermissions",
+ [
+ "plugins::content-manager.explorer.delete"
+ ]
+ ]
+ ]
}
}
]
}
-
diff --git a/packages/strapi-plugin-versioning/src/extensions/content-manager/controllers/Versions.ts b/packages/strapi-plugin-versioning/src/extensions/content-manager/controllers/Versions.ts
index 777d955..6cf0ada 100644
--- a/packages/strapi-plugin-versioning/src/extensions/content-manager/controllers/Versions.ts
+++ b/packages/strapi-plugin-versioning/src/extensions/content-manager/controllers/Versions.ts
@@ -1,4 +1,4 @@
-import { Context } from "koa";
+import { Context } from 'koa';
type Content = {
created_at: string;
@@ -12,23 +12,24 @@ type Version = {
id: number;
};
+const saveParse = (entity: string) => {
+ try {
+ return JSON.parse(entity);
+ } catch (e) {
+ return entity;
+ }
+};
+
module.exports = {
async listEntityVersions(ctx: Context): Promise {
const { model, id } = ctx.params;
- const versionsForAllContentTypes: Content[] = await global.strapi.plugins[
- "versioning"
- ].services.versioning.getVersionsForAllConentTypes();
-
- const versionsForCurrentContentType = versionsForAllContentTypes.filter(
- (version) => version.content_type == model,
+ const service = global.strapi.plugins['versioning'].services.versioning;
+ const versionsForCurrentContentType: Content[] = await service.getVersionsForEntity(
+ model,
+ id,
);
-
- const versionsForCurrentId = versionsForCurrentContentType.filter(
- (version) => version.entity_id === parseInt(id),
- );
-
- return versionsForCurrentId.map((el) => ({
- content: el.entity,
+ return versionsForCurrentContentType.map((el) => ({
+ content: saveParse(el.entity),
date: el.date,
id: el.id,
}));
diff --git a/packages/strapi-plugin-versioning/src/middlewares/versions-middleware/index.ts b/packages/strapi-plugin-versioning/src/middlewares/versions-middleware/index.ts
index 1062d19..d24733f 100644
--- a/packages/strapi-plugin-versioning/src/middlewares/versions-middleware/index.ts
+++ b/packages/strapi-plugin-versioning/src/middlewares/versions-middleware/index.ts
@@ -1,21 +1,27 @@
-const versioningService =
- global.strapi.plugins["versioning"].services.versioning;
+const getVersioningService = () =>
+ global.strapi.plugins['versioning'].services.versioning;
module.exports = () => {
return {
initialize() {
+ const versioningService = getVersioningService();
global.strapi.app.use(async (ctx, next) => {
await next();
- const properMethods = ["PUT", "POST"];
+ const properMethods = ['PUT', 'POST'];
const model = versioningService.getModelFromCtx(ctx);
if (
properMethods.includes(ctx.response.request.method) &&
versioningService.isModelExists(model) &&
- ctx.response.message == "OK"
+ ctx.response.message == 'OK'
) {
- const uid = versioningService.findUid(ctx);
- const updatedData = await global.strapi.db.query(uid).find();
- versioningService.saveDataInDB(updatedData, uid, ctx.response.body);
+ const entity = ctx.response.body;
+ if (entity.id) {
+ const uid = versioningService.findUid(ctx);
+ const updatedData = await global.strapi.db
+ .query(uid)
+ .findOne({ id: entity.id });
+ versioningService.saveDataInDB(updatedData, uid, entity);
+ }
}
});
},
diff --git a/packages/strapi-plugin-versioning/src/services/Versioning.ts b/packages/strapi-plugin-versioning/src/services/Versioning.ts
index b30bdf6..4db43da 100644
--- a/packages/strapi-plugin-versioning/src/services/Versioning.ts
+++ b/packages/strapi-plugin-versioning/src/services/Versioning.ts
@@ -1,4 +1,9 @@
-import { Context } from "koa";
+import { Context } from 'koa';
+import {
+ isObject,
+ noop,
+} from 'lodash';
+import { GlobalQueryResult } from 'strapi-types';
type Entity = any;
type Data = Array;
@@ -7,7 +12,7 @@ const getModelFromCtx = (ctx: Context): string | undefined => {
if (ctx.params?.model) {
return ctx.params.model;
}
- return ctx.url.split("/")[1];
+ return ctx.url.split('/')[1];
};
const isModelExists = (model: string | undefined): boolean => {
@@ -23,33 +28,67 @@ const findUid = (ctx: Context): string =>
ctx.params.model
? global.strapi.db.getModel(ctx.params.model).uid
: Object.values(global.strapi.models).find(
- (el) => el.collectionName == ctx.url.split("/")[1],
- )!.uid;
+ (el) => el.collectionName == ctx.url.split('/')[1],
+ )!.uid;
const saveDataInDB = async (
data: Data,
model: string,
entity: Entity,
): Promise => {
- const knexQueryBuilder = global.strapi.connections.default("versions");
+ const knexQueryBuilder = global.strapi.connections.default('versions');
knexQueryBuilder
.insert({
date: new Date().toISOString(),
content_type: model,
- content: JSON.stringify(data),
+ content: isObject(data) ? JSON.stringify(data) : data,
entity_id: entity.id,
- entity: JSON.stringify(entity),
+ entity: isObject(data) ? JSON.stringify(data) : data,
})
- .then();
+ .then(noop)
+ .catch(noop);
};
const getVersionsForAllConentTypes = async () => {
- const knexQueryBuilder = global.strapi.connections.default("versions");
- knexQueryBuilder.select().returning("*").toString();
- return await knexQueryBuilder;
+ const knexQueryBuilder = global.strapi.connections.default('versions');
+ knexQueryBuilder.select().returning('*').toString();
+ return knexQueryBuilder;
+};
+
+const isSingleType = (kind: 'singleType' | 'collectionType') => kind === 'singleType';
+
+const knexBuildQuery = (contentType: string, id: string): Promise => {
+ const builder = global.strapi.connections.default('versions');
+ builder
+ .select()
+ .where({ content_type: contentType, entity_id: id })
+ .returning('*')
+ .toString();
+ return builder;
+};
+
+const getVersionsForSingleType = async (model: GlobalQueryResult, contentType: string) => {
+ const entity = await model.findOne({});
+ if (entity) {
+ return knexBuildQuery(contentType, entity.id)
+ }
+ return Promise.resolve([]);
+};
+
+const getVersionsForEntity = async (contentType: string, id: string) => {
+ const model = global.strapi.query(contentType);
+ if (model) {
+ const { model: { kind } } = model;
+ if (isSingleType(kind)) {
+ return getVersionsForSingleType(model, contentType);
+ }
+ return knexBuildQuery(contentType, id);
+ }
+ return (global.strapi).errors.badRequest('Content not exists');
};
module.exports = {
+ getVersionsForEntity,
saveDataInDB,
getVersionsForAllConentTypes,
isModelExists,
diff --git a/packages/strapi-types/schemas/model.ts b/packages/strapi-types/schemas/model.ts
index 2c461ae..0057c91 100644
--- a/packages/strapi-types/schemas/model.ts
+++ b/packages/strapi-types/schemas/model.ts
@@ -44,6 +44,7 @@ export type Attribute =
export type Model = BookshelfModel & {
collectionName: string;
+ kind: 'singleType' | 'collectionType'
modelName: string;
databaseName: string;
primaryKey: string;
diff --git a/packages/strapi-types/strapi-global/query.ts b/packages/strapi-types/strapi-global/query.ts
index 8c128b2..db784e0 100644
--- a/packages/strapi-types/strapi-global/query.ts
+++ b/packages/strapi-types/strapi-global/query.ts
@@ -1,4 +1,4 @@
-import { Model } from "../schemas";
+import { Model } from '../schemas';
type Plugin = any;
type Filter = any;
@@ -8,31 +8,32 @@ type Association = {
type: string;
model: string;
via: string | undefined;
- nature: "oneWay" | string;
+ nature: 'oneWay' | string;
autoPopulate: boolean;
dominant: boolean;
plugin: Plugin | undefined;
filter: Filter | undefined;
};
-type AsyncFunction = (p: any) => Promise;
+type AsyncFunction = (p: any) => Promise;
+type AsyncFunctionMultiple = (p: any) => Promise;
export type StrapiGlobalQuery = (s: string) => GlobalQueryResult | undefined;
export type GlobalQueryResult = {
model: Model;
- orm: "bookshelf" | "mongoose";
- primaryKey: "id" | string;
+ orm: 'bookshelf' | 'mongoose';
+ primaryKey: 'id' | string;
associations: Association[];
custom: unknown;
create: AsyncFunction;
createMany: any;
update: AsyncFunction;
delete: AsyncFunction;
- find: AsyncFunction;
+ find: AsyncFunctionMultiple;
findOne: AsyncFunction;
count: AsyncFunction;
- search: AsyncFunction;
+ search: AsyncFunctionMultiple;
countSearch: AsyncFunction;
- findPage: AsyncFunction;
- searchPage: AsyncFunction;
+ findPage: AsyncFunctionMultiple;
+ searchPage: AsyncFunctionMultiple;
};
diff --git a/tsconfig.json b/tsconfig.json
index 085ac31..b15440a 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -5,7 +5,7 @@
"esnext",
"dom"
],
- "target": "es5",
+ "target": "es2018",
"module": "commonjs",
"jsx": "react",
"moduleResolution": "node",