From bb1abb3cc90ac5d8bbc40a165c9c94e044f65bc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Ch=C3=A1vez?= Date: Wed, 17 Jan 2024 15:13:55 +0100 Subject: [PATCH] feat(content-releases): Using useDocument for validations (#19222) * feat(content-releases): introducing useDocument * improve useDocument * change useDocument to return a validate fn * apply feedback * apply josh feedback * populate entries and sanitize them * set strapi/admin version to 4.19.0 --- .../core/admin/admin/src/hooks/useDocument.ts | 59 +++++++++++++ packages/core/admin/admin/src/index.ts | 2 + .../src/components/ReleaseActionMenu.tsx | 6 +- .../admin/src/pages/ReleaseDetailsPage.tsx | 85 ++++++++++++++++--- .../pages/tests/ReleaseDetailsPage.test.tsx | 15 +++- .../pages/tests/mockReleaseDetailsPageData.ts | 82 +++++++++--------- .../content-releases/admin/src/store/hooks.ts | 11 +++ packages/core/content-releases/package.json | 2 + .../src/controllers/__tests__/release.test.ts | 18 ++++ .../server/src/controllers/release-action.ts | 39 ++++++++- .../src/services/__tests__/release.test.ts | 44 +++++----- .../server/src/services/release.ts | 66 +++++++++++--- .../shared/contracts/release-actions.ts | 37 ++++---- yarn.lock | 2 + 14 files changed, 354 insertions(+), 114 deletions(-) create mode 100644 packages/core/admin/admin/src/hooks/useDocument.ts create mode 100644 packages/core/content-releases/admin/src/store/hooks.ts diff --git a/packages/core/admin/admin/src/hooks/useDocument.ts b/packages/core/admin/admin/src/hooks/useDocument.ts new file mode 100644 index 0000000000..8fb078c049 --- /dev/null +++ b/packages/core/admin/admin/src/hooks/useDocument.ts @@ -0,0 +1,59 @@ +import { getYupInnerErrors } from '@strapi/helper-plugin'; +import { Schema, Entity as StrapiEntity, Attribute } from '@strapi/types'; +import { ValidationError } from 'yup'; + +import { createYupSchema } from '../content-manager/utils/validation'; + +export interface Entity { + id: StrapiEntity.ID; + createdAt: string; + updatedAt: string; +} + +interface ValidateOptions { + contentType: Schema.ContentType; + components: { + [key: Schema.Component['uid']]: Schema.Component; + }; + isCreatingEntry?: boolean; +} + +/** + * @alpha - This hook is not stable and likely to change. Use at your own risk. + */ +export function useDocument() { + /** + * @TODO: Ideally, we should get the contentType and components schemas from the redux store + * But at the moment the store is populated only inside the content-manager by useContentManagerInitData + * So, we need to receive the content type schema and the components to use the function + */ + const validate = ( + entry: Entity & { [key: string]: Attribute.Any }, + { contentType, components, isCreatingEntry = false }: ValidateOptions + ) => { + const schema = createYupSchema( + // @ts-expect-error - @TODO: createYupSchema types need to be revisited + contentType, + { components }, + { isCreatingEntry, isDraft: false, isJSONTestDisabled: true } + ); + + try { + schema.validateSync(entry, { abortEarly: false }); + + return { + errors: {}, + }; + } catch (error) { + if (error instanceof ValidationError) { + return { + errors: getYupInnerErrors(error), + }; + } + + throw error; + } + }; + + return { validate }; +} diff --git a/packages/core/admin/admin/src/index.ts b/packages/core/admin/admin/src/index.ts index af236bfad2..a662976485 100644 --- a/packages/core/admin/admin/src/index.ts +++ b/packages/core/admin/admin/src/index.ts @@ -3,3 +3,5 @@ export * from './render'; export type { Store } from './core/store/configure'; export type { SanitizedAdminUser } from '../../shared/contracts/shared'; + +export { useDocument as unstable_useDocument } from './hooks/useDocument'; diff --git a/packages/core/content-releases/admin/src/components/ReleaseActionMenu.tsx b/packages/core/content-releases/admin/src/components/ReleaseActionMenu.tsx index 3243bb1804..810056abf1 100644 --- a/packages/core/content-releases/admin/src/components/ReleaseActionMenu.tsx +++ b/packages/core/content-releases/admin/src/components/ReleaseActionMenu.tsx @@ -6,20 +6,16 @@ import { CheckPermissions, useAPIErrorHandler, useNotification } from '@strapi/h import { Cross, More, Pencil } from '@strapi/icons'; import { isAxiosError } from 'axios'; import { useIntl } from 'react-intl'; -import { useSelector, TypedUseSelectorHook } from 'react-redux'; import { NavLink } from 'react-router-dom'; import styled from 'styled-components'; import { DeleteReleaseAction, ReleaseAction } from '../../../shared/contracts/release-actions'; import { PERMISSIONS } from '../constants'; import { useDeleteReleaseActionMutation } from '../services/release'; +import { useTypedSelector } from '../store/hooks'; -import type { Store } from '@strapi/admin/strapi-admin'; import type { Permission } from '@strapi/helper-plugin'; -type RootState = ReturnType; -const useTypedSelector: TypedUseSelectorHook = useSelector; - const StyledMenuItem = styled(Menu.Item)<{ variant?: 'neutral' | 'danger' }>` &:hover { background: ${({ theme, variant = 'neutral' }) => theme.colors[`${variant}100`]}; diff --git a/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx b/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx index 64a73efc8e..50967a80ea 100644 --- a/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx +++ b/packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; +import { unstable_useDocument } from '@strapi/admin/strapi-admin'; import { Button, ContentLayout, @@ -16,6 +17,7 @@ import { SingleSelect, SingleSelectOption, Icon, + Tooltip, } from '@strapi/design-system'; import { LinkButton } from '@strapi/design-system/v2'; import { @@ -33,7 +35,7 @@ import { useRBAC, AnErrorOccurred, } from '@strapi/helper-plugin'; -import { ArrowLeft, CheckCircle, More, Pencil, Trash } from '@strapi/icons'; +import { ArrowLeft, CheckCircle, More, Pencil, Trash, CrossCircle } from '@strapi/icons'; import { useIntl } from 'react-intl'; import { useParams, useHistory, Link as ReactRouterLink, Redirect } from 'react-router-dom'; import styled from 'styled-components'; @@ -51,12 +53,16 @@ import { useUpdateReleaseActionMutation, usePublishReleaseMutation, useDeleteReleaseMutation, + releaseApi, } from '../services/release'; +import { useTypedDispatch } from '../store/hooks'; import type { ReleaseAction, ReleaseActionGroupBy, + ReleaseActionEntry, } from '../../../shared/contracts/release-actions'; +import type { Schema } from '@strapi/types'; /* ------------------------------------------------------------------------------------------------- * ReleaseDetailsLayout @@ -97,6 +103,10 @@ const TrashIcon = styled(Trash)` } `; +const TypographyMaxWidth = styled(Typography)` + max-width: 300px; +`; + interface PopoverButtonProps { onClick?: (event: React.MouseEvent) => void; disabled?: boolean; @@ -123,18 +133,49 @@ const PopoverButton = ({ onClick, disabled, children }: PopoverButtonProps) => { }; interface EntryValidationTextProps { - status: ReleaseAction['entry']['status']; action: ReleaseAction['type']; + schema: Schema.ContentType; + components: { [key: Schema.Component['uid']]: Schema.Component }; + entry: ReleaseActionEntry; } -const EntryValidationText = ({ status, action }: EntryValidationTextProps) => { +const EntryValidationText = ({ action, schema, components, entry }: EntryValidationTextProps) => { const { formatMessage } = useIntl(); + const { validate } = unstable_useDocument(); + + const { errors } = validate(entry, { + contentType: schema, + components, + isCreatingEntry: false, + }); + + if (Object.keys(errors).length > 0) { + const validationErrorsMessages = Object.entries(errors) + .map(([key, value]) => + formatMessage( + { id: `${value.id}.withField`, defaultMessage: value.defaultMessage }, + { field: key } + ) + ) + .join(' '); + + return ( + + + + + {validationErrorsMessages} + + + + ); + } if (action == 'publish') { return ( - {status === 'published' ? ( + {entry.publishedAt ? ( {formatMessage({ id: 'content-releases.pages.ReleaseDetails.entry-validation.already-published', @@ -156,7 +197,7 @@ const EntryValidationText = ({ status, action }: EntryValidationTextProps) => { return ( - {status === 'draft' ? ( + {!entry.publishedAt ? ( {formatMessage({ id: 'content-releases.pages.ReleaseDetails.entry-validation.already-unpublished', @@ -201,6 +242,7 @@ export const ReleaseDetailsLayout = ({ const { allowedActions: { canUpdate, canDelete }, } = useRBAC(PERMISSIONS); + const dispatch = useTypedDispatch(); const release = data?.data; @@ -245,6 +287,10 @@ export const ReleaseDetailsLayout = ({ handleTogglePopover(); }; + const handleRefresh = () => { + dispatch(releaseApi.util.invalidateTags([{ type: 'ReleaseAction', id: 'LIST' }])); + }; + if (isLoadingDetails) { return (
@@ -361,6 +407,12 @@ export const ReleaseDetailsLayout = ({ )} +