diff --git a/packages/strapi-admin/admin/src/translations/en.json b/packages/strapi-admin/admin/src/translations/en.json index 74800cc4b7..86adefa8b2 100644 --- a/packages/strapi-admin/admin/src/translations/en.json +++ b/packages/strapi-admin/admin/src/translations/en.json @@ -251,6 +251,7 @@ "app.utils.add-filter": "Add filter", "app.utils.defaultMessage": " ", "app.utils.delete": "Delete", + "app.utils.publish": "Publish", "app.utils.filters": "Filters", "app.utils.placeholder.defaultMessage": " ", "app.utils.select-all": "Select all", diff --git a/packages/strapi-admin/admin/src/utils/getAttributesToDisplay.js b/packages/strapi-admin/admin/src/utils/getAttributesToDisplay.js index 900bda6ec0..9d5f014100 100644 --- a/packages/strapi-admin/admin/src/utils/getAttributesToDisplay.js +++ b/packages/strapi-admin/admin/src/utils/getAttributesToDisplay.js @@ -6,10 +6,11 @@ const getAttributesToDisplay = contentType => { // Sometimes timestamps is false let timestampsArray = Array.isArray(timestamps) ? timestamps : []; const idsAttributes = ['id', '_id']; // For both SQL and mongo + const unwritableAttributes = [...idsAttributes, ...timestampsArray, 'published_at']; const schemaAttributes = get(contentType, ['schema', 'attributes'], {}); return Object.keys(schemaAttributes).reduce((acc, current) => { - if (![...idsAttributes, ...timestampsArray].includes(current)) { + if (!unwritableAttributes.includes(current)) { acc.push({ ...schemaAttributes[current], attributeName: current }); } diff --git a/packages/strapi-admin/admin/src/utils/tests/getAttributesToDisplay.test.js b/packages/strapi-admin/admin/src/utils/tests/getAttributesToDisplay.test.js index 5b9ecd64a5..76f4c7974e 100644 --- a/packages/strapi-admin/admin/src/utils/tests/getAttributesToDisplay.test.js +++ b/packages/strapi-admin/admin/src/utils/tests/getAttributesToDisplay.test.js @@ -10,6 +10,7 @@ describe('ADMIN | utils | getAttributesToDisplay', () => { description: { type: 'string' }, created_at: { type: 'timestamp' }, updated_at: { type: 'timestamp' }, + published_at: { type: 'timestamp' }, }, options: { timestamps: ['created_at', 'updated_at'], diff --git a/packages/strapi-plugin-content-manager/admin/src/containers/EditView/Header.js b/packages/strapi-plugin-content-manager/admin/src/containers/EditView/Header.js index c306414a1e..5c7e975060 100644 --- a/packages/strapi-plugin-content-manager/admin/src/containers/EditView/Header.js +++ b/packages/strapi-plugin-content-manager/admin/src/containers/EditView/Header.js @@ -24,21 +24,26 @@ const Header = () => { isCreatingEntry, isSingleType, isSubmitting, + isPublishing, layout, modifiedData, + onPublish, redirectToPreviousPage, resetData, slug, clearData, } = useDataManager(); const { - allowedActions: { canDelete, canUpdate, canCreate }, + allowedActions: { canDelete, canUpdate, canCreate, canPublish }, } = useEditView(); const currentContentTypeMainField = useMemo(() => get(layout, ['settings', 'mainField'], 'id'), [ layout, ]); const currentContentTypeName = useMemo(() => get(layout, ['schema', 'info', 'name']), [layout]); + const hasDraftAndPublish = useMemo(() => get(layout, ['schema', 'options', 'draftAndPublish']), [ + layout, + ]); const didChangeData = useMemo(() => { return !isEqual(initialData, modifiedData) || (isCreatingEntry && !isEmpty(modifiedData)); }, [initialData, isCreatingEntry, modifiedData]); @@ -63,22 +68,6 @@ const Header = () => { if ((isCreatingEntry && canCreate) || (!isCreatingEntry && canUpdate)) { headerActions = [ - { - disabled: !didChangeData, - onClick: () => { - toggleWarningCancel(); - }, - color: 'cancel', - label: formatMessage({ - id: `${pluginId}.containers.Edit.reset`, - }), - type: 'button', - style: { - paddingLeft: 15, - paddingRight: 15, - fontWeight: 600, - }, - }, { disabled: !didChangeData, color: 'success', @@ -95,6 +84,23 @@ const Header = () => { ]; } + if (hasDraftAndPublish && canPublish) { + headerActions.unshift({ + disabled: didChangeData, + label: formatMessage({ + id: 'app.utils.publish', + }), + color: 'primary', + onClick: onPublish, + type: 'button', + isLoading: isPublishing, + style: { + minWidth: 150, + fontWeight: 600, + }, + }); + } + if (!isCreatingEntry && canDelete) { headerActions.unshift({ label: formatMessage({ @@ -114,14 +120,18 @@ const Header = () => { } return headerActions; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ - canCreate, - canDelete, - canUpdate, - didChangeData, isCreatingEntry, - isSubmitting, + canCreate, + canUpdate, + hasDraftAndPublish, + canPublish, + canDelete, + didChangeData, formatMessage, + isSubmitting, + isPublishing, ]); const headerProps = useMemo(() => { diff --git a/packages/strapi-plugin-content-manager/admin/src/containers/EditViewDataManagerProvider/index.js b/packages/strapi-plugin-content-manager/admin/src/containers/EditViewDataManagerProvider/index.js index 096b99ce37..f0e4f71dc0 100644 --- a/packages/strapi-plugin-content-manager/admin/src/containers/EditViewDataManagerProvider/index.js +++ b/packages/strapi-plugin-content-manager/admin/src/containers/EditViewDataManagerProvider/index.js @@ -1,7 +1,14 @@ import React, { useCallback, useEffect, useMemo, useReducer, useState } from 'react'; import { cloneDeep, get, isEmpty, isEqual, pick, set } from 'lodash'; import PropTypes from 'prop-types'; -import { Prompt, Redirect, useParams, useLocation, useHistory } from 'react-router-dom'; +import { + Prompt, + Redirect, + useParams, + useLocation, + useHistory, + useRouteMatch, +} from 'react-router-dom'; import { LoadingIndicatorPage, request, @@ -52,7 +59,11 @@ const EditViewDataManagerProvider = ({ } = reducerState.toJS(); const [isCreatingEntry, setIsCreatingEntry] = useState(id === 'create'); const [isSubmitting, setIsSubmitting] = useState(false); + const [isPublishing, setIsPublishing] = useState(false); const currentContentTypeLayout = get(allLayoutData, ['contentType'], {}); + const { + params: { contentType }, + } = useRouteMatch('/plugins/content-manager/:contentType'); // This is used for the readonly mode when updating an entry const allDynamicZoneFields = useMemo(() => { const attributes = get(currentContentTypeLayout, ['schema', 'attributes'], {}); @@ -406,7 +417,7 @@ const EditViewDataManagerProvider = ({ try { // Time to actually send the data - await request( + const res = await request( getRequestUrl(endPoint), { method, @@ -426,10 +437,10 @@ const EditViewDataManagerProvider = ({ }); strapi.notification.success(`${pluginId}.success.record.save`); - if (isSingleType) { - setIsCreatingEntry(false); - } else { - redirectToPreviousPage(); + setIsCreatingEntry(false); + + if (!isSingleType) { + push(`/plugins/${pluginId}/${contentType}/${slug}/${res.id}`); } } catch (err) { console.error({ err }); @@ -474,6 +485,85 @@ const EditViewDataManagerProvider = ({ } }; + const handlePublish = async e => { + e.preventDefault(); + + // Create yup schema + const schema = createYupSchema( + currentContentTypeLayout, + { + components: get(allLayoutData, 'components', {}), + }, + isCreatingEntry + ); + + try { + // Validate the form using yup + await schema.validate(modifiedData, { abortEarly: false }); + + // Show a loading button in the EditView/Header.js + setIsPublishing(true); + + try { + // Time to actually send the data + await request( + getRequestUrl(`${slug}/publish/${id}`), + { + method: 'POST', + signal, + }, + false, + false + ); + + setIsPublishing(false); + + dispatch({ + type: 'PUBLISH_SUCCESS', + }); + strapi.notification.success(`${pluginId}.success.record.publish`); + } catch (err) { + // ---------- @Soupette Is this error handling still mandatory? ---------- + // The api error send response.payload.message: 'The error message'. + // There isn't : response.payload.message[0].messages[0].id + console.error({ err }); + setIsPublishing(false); + + const error = get( + err, + ['response', 'payload', 'message', '0', 'messages', '0', 'id'], + 'SERVER ERROR' + ); + + if (error === 'ValidationError') { + const errors = get(err, ['response', 'payload', 'data', '0', 'errors'], {}); + const formattedErrors = Object.keys(errors).reduce((acc, current) => { + acc[current] = { id: errors[current][0] }; + + return acc; + }, {}); + + dispatch({ + type: 'PUBLISH_ERRORS', + errors: formattedErrors, + }); + } + + const errorMessage = get(err, ['response', 'payload', 'message'], 'SERVER ERROR'); + strapi.notification.error(errorMessage); + } + } catch (err) { + console.error({ err }); + const errors = getYupInnerErrors(err); + setIsSubmitting(false); + + dispatch({ + type: 'PUBLISH_ERRORS', + errors, + }); + } + }; + const shouldCheckDZErrors = useCallback( dzName => { const doesDZHaveError = Object.keys(formErrors).some(key => key.split('.')[0] === dzName); @@ -630,6 +720,7 @@ const EditViewDataManagerProvider = ({ isCreatingEntry, isSingleType, isSubmitting, + isPublishing, layout: currentContentTypeLayout, modifiedData, moveComponentDown, @@ -637,6 +728,7 @@ const EditViewDataManagerProvider = ({ moveComponentUp, moveRelation, onChange: handleChange, + onPublish: handlePublish, onRemoveRelation, readActionAllowedFields, redirectToPreviousPage, @@ -650,7 +742,11 @@ const EditViewDataManagerProvider = ({ }} > <> - + {isLoading ? ( ) : ( diff --git a/packages/strapi-plugin-content-manager/admin/src/containers/EditViewDataManagerProvider/reducer.js b/packages/strapi-plugin-content-manager/admin/src/containers/EditViewDataManagerProvider/reducer.js index b4de5eafbc..9d5c384c39 100644 --- a/packages/strapi-plugin-content-manager/admin/src/containers/EditViewDataManagerProvider/reducer.js +++ b/packages/strapi-plugin-content-manager/admin/src/containers/EditViewDataManagerProvider/reducer.js @@ -210,9 +210,11 @@ const reducer = (state, action) => { .update('modifiedDZName', () => null) .update('formErrors', () => fromJS(action.errors)); case 'SUBMIT_ERRORS': + case 'PUBLISH_ERRORS': return state .update('formErrors', () => fromJS(action.errors)) .update('shouldShowLoadingState', () => false); + case 'PUBLISH_SUCCESS': case 'SUBMIT_SUCCESS': case 'DELETE_SUCCEEDED': return state diff --git a/packages/strapi-plugin-content-manager/admin/src/translations/en.json b/packages/strapi-plugin-content-manager/admin/src/translations/en.json index 2c92c38659..b4592f063f 100644 --- a/packages/strapi-plugin-content-manager/admin/src/translations/en.json +++ b/packages/strapi-plugin-content-manager/admin/src/translations/en.json @@ -151,5 +151,6 @@ "popUpWarning.warning.cancelAllSettings": "Are you sure you want to cancel your modifications?", "popUpWarning.warning.updateAllSettings": "This will modify all your settings", "success.record.delete": "Deleted", + "success.record.publish": "Published", "success.record.save": "Saved" } diff --git a/packages/strapi-plugin-content-manager/admin/src/utils/generatePermissionsObject.js b/packages/strapi-plugin-content-manager/admin/src/utils/generatePermissionsObject.js index 02186adc88..49a7470b8c 100644 --- a/packages/strapi-plugin-content-manager/admin/src/utils/generatePermissionsObject.js +++ b/packages/strapi-plugin-content-manager/admin/src/utils/generatePermissionsObject.js @@ -4,6 +4,7 @@ const generatePermissionsObject = uid => { const permissions = { create: [{ action: 'plugins::content-manager.explorer.create', subject: null }], delete: [{ action: 'plugins::content-manager.explorer.delete', subject: null }], + publish: [{ action: 'plugins::content-manager.explorer.publish', subject: null }], read: [{ action: 'plugins::content-manager.explorer.read', subject: null }], update: [{ action: 'plugins::content-manager.explorer.update', subject: null }], };