mirror of
https://github.com/strapi/strapi.git
synced 2025-09-07 23:57:19 +00:00
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
This commit is contained in:
parent
29b5e272e5
commit
bb1abb3cc9
59
packages/core/admin/admin/src/hooks/useDocument.ts
Normal file
59
packages/core/admin/admin/src/hooks/useDocument.ts
Normal file
@ -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 };
|
||||
}
|
@ -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';
|
||||
|
@ -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<Store['getState']>;
|
||||
const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector;
|
||||
|
||||
const StyledMenuItem = styled(Menu.Item)<{ variant?: 'neutral' | 'danger' }>`
|
||||
&:hover {
|
||||
background: ${({ theme, variant = 'neutral' }) => theme.colors[`${variant}100`]};
|
||||
|
@ -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<HTMLElement>) => 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 (
|
||||
<Flex gap={2}>
|
||||
<Icon color="danger600" as={CrossCircle} />
|
||||
<Tooltip description={validationErrorsMessages}>
|
||||
<TypographyMaxWidth textColor="danger600" variant="omega" fontWeight="semiBold" ellipsis>
|
||||
{validationErrorsMessages}
|
||||
</TypographyMaxWidth>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
if (action == 'publish') {
|
||||
return (
|
||||
<Flex gap={2}>
|
||||
<Icon color="success600" as={CheckCircle} />
|
||||
{status === 'published' ? (
|
||||
{entry.publishedAt ? (
|
||||
<Typography textColor="success600" fontWeight="bold">
|
||||
{formatMessage({
|
||||
id: 'content-releases.pages.ReleaseDetails.entry-validation.already-published',
|
||||
@ -156,7 +197,7 @@ const EntryValidationText = ({ status, action }: EntryValidationTextProps) => {
|
||||
return (
|
||||
<Flex gap={2}>
|
||||
<Icon color="success600" as={CheckCircle} />
|
||||
{status === 'draft' ? (
|
||||
{!entry.publishedAt ? (
|
||||
<Typography textColor="success600" fontWeight="bold">
|
||||
{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 (
|
||||
<Main aria-busy={isLoadingDetails}>
|
||||
@ -361,6 +407,12 @@ export const ReleaseDetailsLayout = ({
|
||||
</ReleaseInfoWrapper>
|
||||
</Popover>
|
||||
)}
|
||||
<Button size="S" variant="tertiary" onClick={handleRefresh}>
|
||||
{formatMessage({
|
||||
id: 'content-releases.header.actions.refresh',
|
||||
defaultMessage: 'Refresh',
|
||||
})}
|
||||
</Button>
|
||||
<CheckPermissions permissions={PERMISSIONS.publish}>
|
||||
<Button
|
||||
size="S"
|
||||
@ -479,6 +531,8 @@ const ReleaseDetailsBody = () => {
|
||||
|
||||
const releaseActions = data?.data;
|
||||
const releaseMeta = data?.meta;
|
||||
const contentTypes = releaseMeta?.contentTypes || {};
|
||||
const components = releaseMeta?.components || {};
|
||||
|
||||
if (isReleaseError || !release) {
|
||||
const errorsArray = [];
|
||||
@ -633,20 +687,18 @@ const ReleaseDetailsBody = () => {
|
||||
</Table.Head>
|
||||
<Table.LoadingBody />
|
||||
<Table.Body>
|
||||
{releaseActions[key].map(({ id, type, entry, contentType, locale }) => (
|
||||
{releaseActions[key].map(({ id, contentType, locale, type, entry }) => (
|
||||
<Tr key={id}>
|
||||
<Td width="25%" maxWidth="200px">
|
||||
<Typography ellipsis>{`${
|
||||
entry.contentType.mainFieldValue || entry.id
|
||||
contentType.mainFieldValue || entry.id
|
||||
}`}</Typography>
|
||||
</Td>
|
||||
<Td width="10%">
|
||||
<Typography>{`${
|
||||
entry?.locale?.name ? entry.locale.name : '-'
|
||||
}`}</Typography>
|
||||
<Typography>{`${locale?.name ? locale.name : '-'}`}</Typography>
|
||||
</Td>
|
||||
<Td width="10%">
|
||||
<Typography ellipsis>{entry.contentType.displayName || ''}</Typography>
|
||||
<Typography>{contentType.displayName || ''}</Typography>
|
||||
</Td>
|
||||
<Td width="20%">
|
||||
{release.releasedAt ? (
|
||||
@ -676,15 +728,20 @@ const ReleaseDetailsBody = () => {
|
||||
{!release.releasedAt && (
|
||||
<>
|
||||
<Td width="20%" minWidth="200px">
|
||||
<EntryValidationText status={entry.status} action={type} />
|
||||
<EntryValidationText
|
||||
action={type}
|
||||
schema={contentTypes?.[contentType.uid]}
|
||||
components={components}
|
||||
entry={entry}
|
||||
/>
|
||||
</Td>
|
||||
<Td>
|
||||
<Flex justifyContent="flex-end">
|
||||
<ReleaseActionMenu.Root>
|
||||
<ReleaseActionMenu.ReleaseActionEntryLinkItem
|
||||
contentTypeUid={contentType}
|
||||
contentTypeUid={contentType.uid}
|
||||
entryId={entry.id}
|
||||
locale={locale}
|
||||
locale={locale?.code}
|
||||
/>
|
||||
<ReleaseActionMenu.DeleteReleaseActionItem
|
||||
releaseId={release.id}
|
||||
|
@ -17,6 +17,15 @@ jest.mock('@strapi/helper-plugin', () => ({
|
||||
})),
|
||||
}));
|
||||
|
||||
/**
|
||||
* Mocking the useDocument hook to avoid validation errors for testing
|
||||
*/
|
||||
jest.mock('@strapi/admin/strapi-admin', () => ({
|
||||
unstable_useDocument: jest
|
||||
.fn()
|
||||
.mockReturnValue({ validate: jest.fn().mockReturnValue({ errors: {} }) }),
|
||||
}));
|
||||
|
||||
describe('Releases details page', () => {
|
||||
it('renders the details page with no actions', async () => {
|
||||
server.use(
|
||||
@ -97,19 +106,19 @@ describe('Releases details page', () => {
|
||||
// should show the entries
|
||||
expect(
|
||||
screen.getByText(
|
||||
mockReleaseDetailsPageData.withActionsBodyData.data['Category'][0].entry.contentType
|
||||
mockReleaseDetailsPageData.withActionsBodyData.data['Category'][0].contentType
|
||||
.mainFieldValue
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('gridcell', {
|
||||
name: mockReleaseDetailsPageData.withActionsBodyData.data['Category'][0].entry.contentType
|
||||
name: mockReleaseDetailsPageData.withActionsBodyData.data['Category'][0].contentType
|
||||
.displayName,
|
||||
})
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
mockReleaseDetailsPageData.withActionsBodyData.data['Category'][0].entry.locale.name
|
||||
mockReleaseDetailsPageData.withActionsBodyData.data['Category'][0].locale.name
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
|
||||
|
@ -98,19 +98,19 @@ const RELEASE_WITH_ACTIONS_BODY_MOCK_DATA = {
|
||||
{
|
||||
id: 3,
|
||||
type: 'publish',
|
||||
contentType: 'api::category.category',
|
||||
createdAt: '2023-12-05T09:03:57.155Z',
|
||||
updatedAt: '2023-12-05T09:03:57.155Z',
|
||||
entry: {
|
||||
id: 1,
|
||||
contentType: {
|
||||
displayName: 'Category',
|
||||
mainFieldValue: 'cat1',
|
||||
uid: 'api::category.category',
|
||||
},
|
||||
locale: {
|
||||
name: 'English (en)',
|
||||
code: 'en',
|
||||
},
|
||||
entry: {
|
||||
id: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
@ -122,6 +122,8 @@ const RELEASE_WITH_ACTIONS_BODY_MOCK_DATA = {
|
||||
total: 1,
|
||||
pageCount: 1,
|
||||
},
|
||||
contentTypes: {},
|
||||
components: {},
|
||||
},
|
||||
};
|
||||
|
||||
@ -134,39 +136,39 @@ const RELEASE_WITH_MULTIPLE_ACTIONS_BODY_MOCK_DATA = {
|
||||
{
|
||||
id: 3,
|
||||
type: 'publish',
|
||||
contentType: 'api::category.category',
|
||||
createdAt: '2023-12-05T09:03:57.155Z',
|
||||
updatedAt: '2023-12-05T09:03:57.155Z',
|
||||
entry: {
|
||||
id: 1,
|
||||
contentType: {
|
||||
displayName: 'Category',
|
||||
mainFieldValue: 'cat1',
|
||||
uid: 'api::category.category',
|
||||
},
|
||||
locale: {
|
||||
name: 'English (en)',
|
||||
code: 'en',
|
||||
},
|
||||
status: 'draft',
|
||||
entry: {
|
||||
id: 1,
|
||||
publishedAt: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
type: 'unpublish',
|
||||
contentType: 'api::category.category',
|
||||
createdAt: '2023-12-05T09:03:57.155Z',
|
||||
updatedAt: '2023-12-05T09:03:57.155Z',
|
||||
entry: {
|
||||
id: 2,
|
||||
contentType: {
|
||||
displayName: 'Category',
|
||||
mainFieldValue: 'cat2',
|
||||
uid: 'api::category.category',
|
||||
},
|
||||
locale: {
|
||||
name: 'English (en)',
|
||||
code: 'en',
|
||||
},
|
||||
status: 'published',
|
||||
entry: {
|
||||
id: 2,
|
||||
publishedAt: '2023-12-05T09:03:57.155Z',
|
||||
},
|
||||
},
|
||||
],
|
||||
@ -174,20 +176,20 @@ const RELEASE_WITH_MULTIPLE_ACTIONS_BODY_MOCK_DATA = {
|
||||
{
|
||||
id: 5,
|
||||
type: 'publish',
|
||||
contentType: 'api::address.address',
|
||||
createdAt: '2023-12-05T09:03:57.155Z',
|
||||
updatedAt: '2023-12-05T09:03:57.155Z',
|
||||
entry: {
|
||||
id: 1,
|
||||
contentType: {
|
||||
displayName: 'Address',
|
||||
mainFieldValue: 'add1',
|
||||
uid: 'api::address.address',
|
||||
},
|
||||
locale: {
|
||||
name: 'English (en)',
|
||||
code: 'en',
|
||||
},
|
||||
status: 'published',
|
||||
entry: {
|
||||
id: 1,
|
||||
publishedAt: '2023-12-05T09:03:57.155Z',
|
||||
},
|
||||
},
|
||||
],
|
||||
@ -199,6 +201,8 @@ const RELEASE_WITH_MULTIPLE_ACTIONS_BODY_MOCK_DATA = {
|
||||
total: 1,
|
||||
pageCount: 1,
|
||||
},
|
||||
contentTypes: {},
|
||||
components: {},
|
||||
},
|
||||
};
|
||||
|
||||
|
11
packages/core/content-releases/admin/src/store/hooks.ts
Normal file
11
packages/core/content-releases/admin/src/store/hooks.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Dispatch } from '@reduxjs/toolkit';
|
||||
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import type { Store } from '@strapi/admin/strapi-admin';
|
||||
|
||||
type RootState = ReturnType<Store['getState']>;
|
||||
|
||||
const useTypedDispatch: () => Dispatch = useDispatch;
|
||||
const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector;
|
||||
|
||||
export { useTypedSelector, useTypedDispatch };
|
@ -68,6 +68,7 @@
|
||||
"yup": "0.32.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@strapi/admin": "4.17.1",
|
||||
"@strapi/admin-test-utils": "4.17.1",
|
||||
"@strapi/pack-up": "workspace:*",
|
||||
"@strapi/strapi": "4.17.1",
|
||||
@ -84,6 +85,7 @@
|
||||
"typescript": "5.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@strapi/admin": "^4.19.0",
|
||||
"@strapi/strapi": "^4.15.1",
|
||||
"react": "^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^17.0.0 || ^18.0.0",
|
||||
|
@ -165,6 +165,24 @@ describe('Release controller', () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
// @ts-expect-error Ignore missing properties
|
||||
'content-manager': {
|
||||
services: {
|
||||
'content-types': {
|
||||
findAllContentTypes: jest
|
||||
.fn()
|
||||
.mockReturnValue([
|
||||
{ uid: 'api::contentTypeA.contentTypeA' },
|
||||
{ uid: 'api::contentTypeB.contentTypeB' },
|
||||
]),
|
||||
},
|
||||
components: {
|
||||
findAllComponents: jest.fn().mockReturnValue([{ uid: 'component.component' }]),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import type Koa from 'koa';
|
||||
|
||||
import { mapAsync } from '@strapi/utils';
|
||||
import {
|
||||
validateReleaseAction,
|
||||
validateReleaseActionUpdateSchema,
|
||||
@ -41,12 +42,48 @@ const releaseActionController = {
|
||||
sort: query.groupBy === 'action' ? 'type' : query.groupBy,
|
||||
...query,
|
||||
});
|
||||
const groupedData = await releaseService.groupActions(results, query.groupBy);
|
||||
|
||||
/**
|
||||
* Release actions can be related to entries of different content types.
|
||||
* We need to sanitize the entry output according to that content type.
|
||||
* So, we group the sanitized output function by content type.
|
||||
*/
|
||||
const contentTypeOutputSanitizers = results.reduce((acc, action) => {
|
||||
if (acc[action.contentType]) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
const contentTypePermissionsManager =
|
||||
strapi.admin.services.permission.createPermissionsManager({
|
||||
ability: ctx.state.userAbility,
|
||||
model: action.contentType,
|
||||
});
|
||||
|
||||
acc[action.contentType] = contentTypePermissionsManager.sanitizeOutput;
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
/**
|
||||
* sanitizeOutput doesn't work if you use it directly on the Release Action model, it doesn't sanitize the entries
|
||||
* So, we need to sanitize manually each entry inside a Release Action
|
||||
*/
|
||||
const sanitizedResults = await mapAsync(results, async (action) => ({
|
||||
...action,
|
||||
entry: await contentTypeOutputSanitizers[action.contentType](action.entry),
|
||||
}));
|
||||
|
||||
const groupedData = await releaseService.groupActions(sanitizedResults, query.groupBy);
|
||||
|
||||
const contentTypes = releaseService.getContentTypeModelsFromActions(results);
|
||||
const components = await releaseService.getAllComponents();
|
||||
|
||||
ctx.body = {
|
||||
data: groupedData,
|
||||
meta: {
|
||||
pagination,
|
||||
contentTypes,
|
||||
components,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
@ -406,39 +406,39 @@ describe('release service', () => {
|
||||
expect(groupedData).toEqual({
|
||||
contentTypeA: [
|
||||
{
|
||||
id: 1,
|
||||
locale: 'en',
|
||||
contentType: 'api::contentTypeA.contentTypeA',
|
||||
entry: {
|
||||
id: 1,
|
||||
contentType: {
|
||||
displayName: 'contentTypeA',
|
||||
mainFieldValue: 'test 1',
|
||||
uid: 'api::contentTypeA.contentTypeA',
|
||||
},
|
||||
locale: {
|
||||
code: 'en',
|
||||
name: 'English (en)',
|
||||
},
|
||||
status: 'published',
|
||||
entry: {
|
||||
id: 1,
|
||||
name: 'test 1',
|
||||
publishedAt: '2021-01-01',
|
||||
},
|
||||
},
|
||||
],
|
||||
contentTypeB: [
|
||||
{
|
||||
id: 2,
|
||||
locale: 'fr',
|
||||
contentType: 'api::contentTypeB.contentTypeB',
|
||||
entry: {
|
||||
id: 2,
|
||||
contentType: {
|
||||
displayName: 'contentTypeB',
|
||||
mainFieldValue: 'test 2',
|
||||
uid: 'api::contentTypeB.contentTypeB',
|
||||
},
|
||||
locale: {
|
||||
code: 'fr',
|
||||
name: 'French (fr)',
|
||||
},
|
||||
status: 'draft',
|
||||
entry: {
|
||||
id: 2,
|
||||
name: 'test 2',
|
||||
publishedAt: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { setCreatorFields, errors } from '@strapi/utils';
|
||||
|
||||
import type { LoadedStrapi, EntityService, UID } from '@strapi/types';
|
||||
import type { LoadedStrapi, EntityService, UID, Schema } from '@strapi/types';
|
||||
|
||||
import _ from 'lodash/fp';
|
||||
|
||||
@ -38,13 +38,13 @@ type LocaleDictionary = {
|
||||
const getGroupName = (queryValue?: ReleaseActionGroupBy) => {
|
||||
switch (queryValue) {
|
||||
case 'contentType':
|
||||
return 'entry.contentType.displayName';
|
||||
return 'contentType.displayName';
|
||||
case 'action':
|
||||
return 'type';
|
||||
case 'locale':
|
||||
return _.getOr('No locale', 'entry.locale.name');
|
||||
return _.getOr('No locale', 'locale.name');
|
||||
default:
|
||||
return 'entry.contentType.displayName';
|
||||
return 'contentType.displayName';
|
||||
}
|
||||
};
|
||||
|
||||
@ -238,7 +238,9 @@ const createReleaseService = ({ strapi }: { strapi: LoadedStrapi }) => ({
|
||||
return strapi.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
|
||||
...query,
|
||||
populate: {
|
||||
entry: true,
|
||||
entry: {
|
||||
populate: '*',
|
||||
},
|
||||
},
|
||||
filters: {
|
||||
release: releaseId,
|
||||
@ -268,14 +270,11 @@ const createReleaseService = ({ strapi }: { strapi: LoadedStrapi }) => ({
|
||||
|
||||
return {
|
||||
...action,
|
||||
entry: {
|
||||
id: action.entry.id,
|
||||
locale: action.locale ? allLocalesDictionary[action.locale] : null,
|
||||
contentType: {
|
||||
displayName,
|
||||
mainFieldValue: action.entry[mainField],
|
||||
},
|
||||
locale: action.locale ? allLocalesDictionary[action.locale] : null,
|
||||
status: action.entry.publishedAt ? 'published' : 'draft',
|
||||
uid: action.contentType,
|
||||
},
|
||||
};
|
||||
});
|
||||
@ -318,6 +317,47 @@ const createReleaseService = ({ strapi }: { strapi: LoadedStrapi }) => ({
|
||||
return contentTypesData;
|
||||
},
|
||||
|
||||
getContentTypeModelsFromActions(actions: ReleaseAction[]) {
|
||||
const contentTypeUids = actions.reduce<ReleaseAction['contentType'][]>((acc, action) => {
|
||||
if (!acc.includes(action.contentType)) {
|
||||
acc.push(action.contentType);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const contentTypeModelsMap = contentTypeUids.reduce(
|
||||
(
|
||||
acc: { [key: ReleaseAction['contentType']]: Schema.ContentType },
|
||||
contentTypeUid: ReleaseAction['contentType']
|
||||
) => {
|
||||
acc[contentTypeUid] = strapi.getModel(contentTypeUid);
|
||||
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
return contentTypeModelsMap;
|
||||
},
|
||||
|
||||
async getAllComponents() {
|
||||
const contentManagerComponentsService = strapi.plugin('content-manager').service('components');
|
||||
|
||||
const components = await contentManagerComponentsService.findAllComponents();
|
||||
|
||||
const componentsMap = components.reduce(
|
||||
(acc: { [key: Schema.Component['uid']]: Schema.Component }, component: Schema.Component) => {
|
||||
acc[component.uid] = component;
|
||||
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
return componentsMap;
|
||||
},
|
||||
|
||||
async delete(releaseId: DeleteRelease.Request['params']['id']) {
|
||||
const release = (await strapi.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
|
||||
populate: {
|
||||
|
@ -1,28 +1,14 @@
|
||||
import { Attribute, Common } from '@strapi/types';
|
||||
import { Attribute, Common, Schema } from '@strapi/types';
|
||||
import type { Release, Pagination } from './releases';
|
||||
import type { Entity } from '../types';
|
||||
|
||||
import type { errors } from '@strapi/utils';
|
||||
|
||||
type ReleaseActionEntry = Entity & {
|
||||
export type ReleaseActionEntry = Entity & {
|
||||
// Entity attributes
|
||||
[key: string]: Attribute.Any;
|
||||
} & {
|
||||
locale?: string;
|
||||
status: 'published' | 'draft';
|
||||
};
|
||||
|
||||
type ReleaseActionEntryData = {
|
||||
id: ReleaseActionEntry['id'];
|
||||
locale?: {
|
||||
name: string;
|
||||
code: string;
|
||||
};
|
||||
contentType: {
|
||||
mainFieldValue?: string;
|
||||
displayName: string;
|
||||
};
|
||||
status: 'published' | 'draft';
|
||||
};
|
||||
|
||||
export interface ReleaseAction extends Entity {
|
||||
@ -33,6 +19,21 @@ export interface ReleaseAction extends Entity {
|
||||
release: Release;
|
||||
}
|
||||
|
||||
export interface FormattedReleaseAction extends Entity {
|
||||
type: 'publish' | 'unpublish';
|
||||
entry: ReleaseActionEntry;
|
||||
contentType: {
|
||||
uid: Common.UID.ContentType;
|
||||
mainFieldValue?: string;
|
||||
displayName: string;
|
||||
};
|
||||
locale?: {
|
||||
name: string;
|
||||
code: string;
|
||||
};
|
||||
release: Release;
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /content-releases/:releaseId/actions - Create a release action
|
||||
*/
|
||||
@ -74,10 +75,12 @@ export declare namespace GetReleaseActions {
|
||||
|
||||
export interface Response {
|
||||
data: {
|
||||
[key: string]: Array<ReleaseAction & { entry: ReleaseActionEntryData }>;
|
||||
[key: string]: Array<FormattedReleaseAction>;
|
||||
};
|
||||
meta: {
|
||||
pagination: Pagination;
|
||||
contentTypes: Record<Schema.ContentType['uid'], Schema.ContentType>;
|
||||
components: Record<Schema.Component['uid'], Schema.Component>;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -7906,6 +7906,7 @@ __metadata:
|
||||
resolution: "@strapi/content-releases@workspace:packages/core/content-releases"
|
||||
dependencies:
|
||||
"@reduxjs/toolkit": "npm:1.9.7"
|
||||
"@strapi/admin": "npm:4.17.1"
|
||||
"@strapi/admin-test-utils": "npm:4.17.1"
|
||||
"@strapi/design-system": "npm:1.14.1"
|
||||
"@strapi/helper-plugin": "npm:4.17.1"
|
||||
@ -7932,6 +7933,7 @@ __metadata:
|
||||
typescript: "npm:5.2.2"
|
||||
yup: "npm:0.32.9"
|
||||
peerDependencies:
|
||||
"@strapi/admin": ^4.19.0
|
||||
"@strapi/strapi": ^4.15.1
|
||||
react: ^17.0.0 || ^18.0.0
|
||||
react-dom: ^17.0.0 || ^18.0.0
|
||||
|
Loading…
x
Reference in New Issue
Block a user