mirror of
https://github.com/strapi/strapi.git
synced 2025-11-10 07:10:11 +00:00
feat(content-releases): add create release action to cm edit view (#18905)
This commit is contained in:
parent
8145b7becc
commit
4f6722c6d4
@ -11,15 +11,11 @@ import { useAPIErrorHandler, useNotification } from '@strapi/helper-plugin';
|
|||||||
import { Formik, Form } from 'formik';
|
import { Formik, Form } from 'formik';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import * as yup from 'yup';
|
|
||||||
|
|
||||||
|
import { RELEASE_SCHEMA } from '../../../shared/validation-schemas';
|
||||||
import { isAxiosError } from '../services/axios';
|
import { isAxiosError } from '../services/axios';
|
||||||
import { useCreateReleaseMutation } from '../services/release';
|
import { useCreateReleaseMutation } from '../services/release';
|
||||||
|
|
||||||
const RELEASE_SCHEMA = yup.object({
|
|
||||||
name: yup.string().required(),
|
|
||||||
});
|
|
||||||
|
|
||||||
interface FormValues {
|
interface FormValues {
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
@ -35,8 +31,8 @@ interface AddReleaseDialogProps {
|
|||||||
export const AddReleaseDialog = ({ handleClose }: AddReleaseDialogProps) => {
|
export const AddReleaseDialog = ({ handleClose }: AddReleaseDialogProps) => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const toggleNotification = useNotification();
|
const toggleNotification = useNotification();
|
||||||
const { push } = useHistory();
|
|
||||||
const { formatAPIError } = useAPIErrorHandler();
|
const { formatAPIError } = useAPIErrorHandler();
|
||||||
|
const { push } = useHistory();
|
||||||
|
|
||||||
const [createRelease, { isLoading }] = useCreateReleaseMutation();
|
const [createRelease, { isLoading }] = useCreateReleaseMutation();
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,272 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
FieldLabel,
|
||||||
|
Flex,
|
||||||
|
ModalBody,
|
||||||
|
ModalHeader,
|
||||||
|
ModalLayout,
|
||||||
|
SingleSelect,
|
||||||
|
SingleSelectOption,
|
||||||
|
Typography,
|
||||||
|
ModalFooter,
|
||||||
|
} from '@strapi/design-system';
|
||||||
|
import {
|
||||||
|
CheckPermissions,
|
||||||
|
useAPIErrorHandler,
|
||||||
|
useCMEditViewDataManager,
|
||||||
|
useNotification,
|
||||||
|
} from '@strapi/helper-plugin';
|
||||||
|
import { Plus } from '@strapi/icons';
|
||||||
|
import { isAxiosError } from 'axios';
|
||||||
|
import { Formik, Form } from 'formik';
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import * as yup from 'yup';
|
||||||
|
|
||||||
|
import { CreateReleaseAction } from '../../../shared/contracts/release-actions';
|
||||||
|
import { PERMISSIONS } from '../constants';
|
||||||
|
import { useCreateReleaseActionMutation, useGetReleasesForEntryQuery } from '../services/release';
|
||||||
|
|
||||||
|
import { ReleaseActionOptions } from './ReleaseActionOptions';
|
||||||
|
|
||||||
|
const RELEASE_ACTION_FORM_SCHEMA = yup.object().shape({
|
||||||
|
type: yup.string().oneOf(['publish', 'unpublish']).required(),
|
||||||
|
releaseId: yup.string().required(),
|
||||||
|
});
|
||||||
|
|
||||||
|
interface FormValues {
|
||||||
|
type: CreateReleaseAction.Request['body']['type'];
|
||||||
|
releaseId: CreateReleaseAction.Request['params']['releaseId'];
|
||||||
|
}
|
||||||
|
|
||||||
|
const INITIAL_VALUES = {
|
||||||
|
type: 'publish',
|
||||||
|
releaseId: '',
|
||||||
|
} satisfies FormValues;
|
||||||
|
|
||||||
|
interface AddActionToReleaseModalProps {
|
||||||
|
handleClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AddActionToReleaseModal = ({ handleClose }: AddActionToReleaseModalProps) => {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
const toggleNotification = useNotification();
|
||||||
|
const { formatAPIError } = useAPIErrorHandler();
|
||||||
|
const params = useParams<{ id?: string }>();
|
||||||
|
const {
|
||||||
|
allLayoutData: { contentType },
|
||||||
|
} = useCMEditViewDataManager();
|
||||||
|
// Get all 'pending' releases
|
||||||
|
const response = useGetReleasesForEntryQuery();
|
||||||
|
|
||||||
|
const releases = response.data?.data;
|
||||||
|
const [createReleaseAction, { isLoading }] = useCreateReleaseActionMutation();
|
||||||
|
|
||||||
|
const handleSubmit = async (values: FormValues) => {
|
||||||
|
/**
|
||||||
|
* contentType uid and entry id are not provided by the form but required to create a Release Action.
|
||||||
|
* Optimistically we expect them to always be provided via params and CMEditViewDataManager.
|
||||||
|
* In the event they are not, we should throw an error.
|
||||||
|
*/
|
||||||
|
if (!contentType?.uid || !params.id) {
|
||||||
|
toggleNotification({
|
||||||
|
type: 'warning',
|
||||||
|
message: formatMessage({
|
||||||
|
id: 'content-releases.content-manager.notification.entry-error',
|
||||||
|
defaultMessage: 'Failed to get entry',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const releaseActionEntry = {
|
||||||
|
contentType: contentType.uid,
|
||||||
|
id: params.id,
|
||||||
|
};
|
||||||
|
const response = await createReleaseAction({
|
||||||
|
body: { type: values.type, entry: releaseActionEntry },
|
||||||
|
params: { releaseId: values.releaseId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if ('data' in response) {
|
||||||
|
// Handle success
|
||||||
|
toggleNotification({
|
||||||
|
type: 'success',
|
||||||
|
message: formatMessage({
|
||||||
|
id: 'content-releases.content-manager-edit-view.add-to-release.notification.success',
|
||||||
|
defaultMessage: 'Entry added to release',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
handleClose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('error' in response) {
|
||||||
|
if (isAxiosError(response.error)) {
|
||||||
|
// Handle axios error
|
||||||
|
toggleNotification({
|
||||||
|
type: 'warning',
|
||||||
|
message: formatAPIError(response.error),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Handle generic error
|
||||||
|
toggleNotification({
|
||||||
|
type: 'warning',
|
||||||
|
message: formatMessage({ id: 'notification.error', defaultMessage: 'An error occurred' }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalLayout onClose={handleClose} labelledBy="title">
|
||||||
|
<ModalHeader>
|
||||||
|
<Typography id="title" fontWeight="bold" textColor="neutral800">
|
||||||
|
{formatMessage({
|
||||||
|
id: 'content-releases.content-manager-edit-view.add-to-release',
|
||||||
|
defaultMessage: 'Add to release',
|
||||||
|
})}
|
||||||
|
</Typography>
|
||||||
|
</ModalHeader>
|
||||||
|
<Formik
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
validationSchema={RELEASE_ACTION_FORM_SCHEMA}
|
||||||
|
initialValues={INITIAL_VALUES}
|
||||||
|
>
|
||||||
|
{({ values, setFieldValue }) => {
|
||||||
|
return (
|
||||||
|
<Form>
|
||||||
|
<ModalBody>
|
||||||
|
<Flex direction="column" alignItems="stretch" gap={2}>
|
||||||
|
<Box paddingBottom={6}>
|
||||||
|
<SingleSelect
|
||||||
|
required
|
||||||
|
label={formatMessage({
|
||||||
|
id: 'content-releases.content-manager-edit-view.add-to-release.select-label',
|
||||||
|
defaultMessage: 'Select a release',
|
||||||
|
})}
|
||||||
|
placeholder={formatMessage({
|
||||||
|
id: 'content-releases.content-manager-edit-view.add-to-release.select-placeholder',
|
||||||
|
defaultMessage: 'Select',
|
||||||
|
})}
|
||||||
|
onChange={(value) => setFieldValue('releaseId', value)}
|
||||||
|
value={values.releaseId}
|
||||||
|
>
|
||||||
|
{releases?.map((release) => (
|
||||||
|
<SingleSelectOption key={release.id} value={release.id}>
|
||||||
|
{release.name}
|
||||||
|
</SingleSelectOption>
|
||||||
|
))}
|
||||||
|
</SingleSelect>
|
||||||
|
</Box>
|
||||||
|
<FieldLabel>
|
||||||
|
{formatMessage({
|
||||||
|
id: 'content-releases.content-manager-edit-view.add-to-release.action-type-label',
|
||||||
|
defaultMessage: 'What do you want to do with this entry?',
|
||||||
|
})}
|
||||||
|
</FieldLabel>
|
||||||
|
<Flex>
|
||||||
|
<ReleaseActionOptions
|
||||||
|
selected={values.type}
|
||||||
|
handleChange={(e) => setFieldValue('type', e.target.value)}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter
|
||||||
|
startActions={
|
||||||
|
<Button onClick={handleClose} variant="tertiary" name="cancel">
|
||||||
|
{formatMessage({
|
||||||
|
id: 'content-releases.content-manager-edit-view.add-to-release.cancel-button',
|
||||||
|
defaultMessage: 'Cancel',
|
||||||
|
})}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
endActions={
|
||||||
|
/**
|
||||||
|
* TODO: Ideally we would use isValid from Formik to disable the button, however currently it always returns true
|
||||||
|
* for yup.string().required(), even when the value is falsy (including empty string)
|
||||||
|
*/
|
||||||
|
<Button type="submit" disabled={!values.releaseId} loading={isLoading}>
|
||||||
|
{formatMessage({
|
||||||
|
id: 'content-releases.content-manager-edit-view.add-to-release.continue-button',
|
||||||
|
defaultMessage: 'Continue',
|
||||||
|
})}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Formik>
|
||||||
|
</ModalLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CMReleasesContainer = () => {
|
||||||
|
const [showModal, setShowModal] = React.useState(false);
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
const {
|
||||||
|
isCreatingEntry,
|
||||||
|
allLayoutData: { contentType },
|
||||||
|
} = useCMEditViewDataManager();
|
||||||
|
|
||||||
|
const toggleAddActionToReleaseModal = () => setShowModal((prev) => !prev);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* - Impossible to add entry to release before it exists
|
||||||
|
* - Content types without draft and publish cannot add entries to release
|
||||||
|
* TODO v5: All contentTypes will have draft and publish enabled
|
||||||
|
*/
|
||||||
|
if (isCreatingEntry || !contentType?.options?.draftAndPublish) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CheckPermissions permissions={PERMISSIONS.main}>
|
||||||
|
<Box
|
||||||
|
as="aside"
|
||||||
|
aria-label={formatMessage({
|
||||||
|
id: 'content-releases.plugin.name',
|
||||||
|
defaultMessage: 'Releases',
|
||||||
|
})}
|
||||||
|
background="neutral0"
|
||||||
|
borderColor="neutral150"
|
||||||
|
hasRadius
|
||||||
|
padding={4}
|
||||||
|
shadow="tableShadow"
|
||||||
|
>
|
||||||
|
<Flex direction="column" alignItems="stretch" gap={4}>
|
||||||
|
<Typography variant="sigma" textColor="neutral600" textTransform="uppercase">
|
||||||
|
{formatMessage({
|
||||||
|
id: 'content-releases.plugin.name',
|
||||||
|
defaultMessage: 'RELEASES',
|
||||||
|
})}
|
||||||
|
</Typography>
|
||||||
|
<CheckPermissions permissions={PERMISSIONS.createAction}>
|
||||||
|
<Button
|
||||||
|
justifyContent="center"
|
||||||
|
paddingLeft={4}
|
||||||
|
paddingRight={4}
|
||||||
|
color="neutral700"
|
||||||
|
variant="tertiary"
|
||||||
|
startIcon={<Plus />}
|
||||||
|
onClick={toggleAddActionToReleaseModal}
|
||||||
|
>
|
||||||
|
{formatMessage({
|
||||||
|
id: 'content-releases.content-manager-edit-view.add-to-release',
|
||||||
|
defaultMessage: 'Add to release',
|
||||||
|
})}
|
||||||
|
</Button>
|
||||||
|
</CheckPermissions>
|
||||||
|
</Flex>
|
||||||
|
{showModal && <AddActionToReleaseModal handleClose={toggleAddActionToReleaseModal} />}
|
||||||
|
</Box>
|
||||||
|
</CheckPermissions>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,94 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
FieldInput,
|
||||||
|
FieldLabel,
|
||||||
|
VisuallyHidden,
|
||||||
|
Field,
|
||||||
|
type FieldProps,
|
||||||
|
} from '@strapi/design-system';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
interface FieldWrapperProps extends FieldProps {
|
||||||
|
actionType: 'publish' | 'unpublish';
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBorderLeftRadiusValue = (actionType: FieldWrapperProps['actionType']) => {
|
||||||
|
return actionType === 'publish' ? 1 : 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBorderRightRadiusValue = (actionType: FieldWrapperProps['actionType']) => {
|
||||||
|
return actionType === 'publish' ? 0 : 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FieldWrapper = styled(Field)<FieldWrapperProps>`
|
||||||
|
border-top-left-radius: ${({ actionType, theme }) =>
|
||||||
|
theme.spaces[getBorderLeftRadiusValue(actionType)]};
|
||||||
|
border-bottom-left-radius: ${({ actionType, theme }) =>
|
||||||
|
theme.spaces[getBorderLeftRadiusValue(actionType)]};
|
||||||
|
border-top-right-radius: ${({ actionType, theme }) =>
|
||||||
|
theme.spaces[getBorderRightRadiusValue(actionType)]};
|
||||||
|
border-bottom-right-radius: ${({ actionType, theme }) =>
|
||||||
|
theme.spaces[getBorderRightRadiusValue(actionType)]};
|
||||||
|
|
||||||
|
> label {
|
||||||
|
color: inherit;
|
||||||
|
padding: ${({ theme }) => `${theme.spaces[2]} ${theme.spaces[3]}`};
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&[data-checked='true'] {
|
||||||
|
color: ${({ theme }) => theme.colors.primary700};
|
||||||
|
background-color: ${({ theme }) => theme.colors.primary100};
|
||||||
|
border-color: ${({ theme }) => theme.colors.primary700};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface ActionOptionProps {
|
||||||
|
selected: 'publish' | 'unpublish';
|
||||||
|
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OptionProps extends ActionOptionProps {
|
||||||
|
actionType: 'publish' | 'unpublish';
|
||||||
|
}
|
||||||
|
|
||||||
|
const ActionOption = ({ selected, actionType, handleChange }: OptionProps) => {
|
||||||
|
return (
|
||||||
|
<FieldWrapper
|
||||||
|
actionType={actionType}
|
||||||
|
background="primary0"
|
||||||
|
borderColor="neutral200"
|
||||||
|
color={selected === actionType ? 'primary600' : 'neutral600'}
|
||||||
|
position="relative"
|
||||||
|
cursor="pointer"
|
||||||
|
data-checked={selected === actionType}
|
||||||
|
>
|
||||||
|
<FieldLabel htmlFor={`release-action-${actionType}`}>
|
||||||
|
<VisuallyHidden>
|
||||||
|
<FieldInput
|
||||||
|
type="radio"
|
||||||
|
id={`release-action-${actionType}`}
|
||||||
|
name="type"
|
||||||
|
checked={selected === actionType}
|
||||||
|
onChange={handleChange}
|
||||||
|
value={actionType}
|
||||||
|
/>
|
||||||
|
</VisuallyHidden>
|
||||||
|
{actionType}
|
||||||
|
</FieldLabel>
|
||||||
|
</FieldWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ReleaseActionOptions = ({ selected, handleChange }: ActionOptionProps) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ActionOption actionType="publish" selected={selected} handleChange={handleChange} />
|
||||||
|
<ActionOption actionType="unpublish" selected={selected} handleChange={handleChange} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,111 @@
|
|||||||
|
import { useCMEditViewDataManager } from '@strapi/helper-plugin';
|
||||||
|
import { screen } from '@testing-library/react';
|
||||||
|
import { render, server } from '@tests/utils';
|
||||||
|
import { rest } from 'msw';
|
||||||
|
|
||||||
|
import { CMReleasesContainer } from '../CMReleasesContainer';
|
||||||
|
|
||||||
|
jest.mock('@strapi/helper-plugin', () => ({
|
||||||
|
...jest.requireActual('@strapi/helper-plugin'),
|
||||||
|
// eslint-disable-next-line
|
||||||
|
CheckPermissions: ({ children }: { children: JSX.Element }) => <div>{children}</div>,
|
||||||
|
useCMEditViewDataManager: jest.fn().mockReturnValue({
|
||||||
|
isCreatingEntry: false,
|
||||||
|
allLayoutData: {
|
||||||
|
contentType: {
|
||||||
|
uid: 'api::article.article',
|
||||||
|
options: {
|
||||||
|
draftAndPublish: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('CMReleasesContainer', () => {
|
||||||
|
it('should not render the injection zone when creating an entry', () => {
|
||||||
|
// @ts-expect-error - Ignore error
|
||||||
|
useCMEditViewDataManager.mockReturnValueOnce({
|
||||||
|
isCreatingEntry: true,
|
||||||
|
allLayoutData: {
|
||||||
|
contentType: {
|
||||||
|
options: {
|
||||||
|
draftAndPublish: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<CMReleasesContainer />);
|
||||||
|
|
||||||
|
const informationBox = screen.queryByRole('complementary', { name: 'Releases' });
|
||||||
|
expect(informationBox).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render the injection zone without draft and publish enabled', () => {
|
||||||
|
// @ts-expect-error - Ignore error
|
||||||
|
useCMEditViewDataManager.mockReturnValueOnce({
|
||||||
|
isCreatingEntry: false,
|
||||||
|
allLayoutData: {
|
||||||
|
contentType: {
|
||||||
|
options: {
|
||||||
|
draftAndPublish: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<CMReleasesContainer />);
|
||||||
|
|
||||||
|
const informationBox = screen.queryByRole('complementary', { name: 'Releases' });
|
||||||
|
expect(informationBox).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the injection zone', () => {
|
||||||
|
render(<CMReleasesContainer />);
|
||||||
|
|
||||||
|
const addToReleaseButton = screen.getByRole('button', { name: 'Add to release' });
|
||||||
|
const informationBox = screen.getByRole('complementary', { name: 'Releases' });
|
||||||
|
expect(informationBox).toBeInTheDocument();
|
||||||
|
expect(addToReleaseButton).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open and close the add to release modal', async () => {
|
||||||
|
const { user } = render(<CMReleasesContainer />);
|
||||||
|
|
||||||
|
const addToReleaseButton = screen.getByRole('button', { name: 'Add to release' });
|
||||||
|
await user.click(addToReleaseButton);
|
||||||
|
const modalDialog = screen.getByRole('dialog', { name: 'Add to release' });
|
||||||
|
expect(modalDialog).toBeVisible();
|
||||||
|
|
||||||
|
const closeButton = screen.getByRole('button', { name: 'Close the modal' });
|
||||||
|
await user.click(closeButton);
|
||||||
|
expect(modalDialog).not.toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should enable the modal's submit button", async () => {
|
||||||
|
// Mock the response from the server
|
||||||
|
server.use(
|
||||||
|
rest.get('/content-releases', (req, res, ctx) => {
|
||||||
|
return res(
|
||||||
|
ctx.json({
|
||||||
|
data: [{ name: 'release1', id: '1' }],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const { user } = render(<CMReleasesContainer />);
|
||||||
|
|
||||||
|
const addToReleaseButton = screen.getByRole('button', { name: 'Add to release' });
|
||||||
|
await user.click(addToReleaseButton);
|
||||||
|
|
||||||
|
// Select a value received from the server
|
||||||
|
const select = screen.getByRole('combobox', { name: 'Select a release' });
|
||||||
|
await user.click(select);
|
||||||
|
await user.click(screen.getByRole('option', { name: 'release1' }));
|
||||||
|
|
||||||
|
const submitButtom = screen.getByRole('button', { name: 'Continue' });
|
||||||
|
expect(submitButtom).toBeEnabled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
import { fireEvent, screen } from '@testing-library/react';
|
||||||
|
import { render } from '@tests/utils';
|
||||||
|
|
||||||
|
import { ReleaseActionOptions } from '../ReleaseActionOptions';
|
||||||
|
|
||||||
|
describe('ReleaseActionOptions', () => {
|
||||||
|
it('should render the component', () => {
|
||||||
|
const handleChange = jest.fn();
|
||||||
|
render(<ReleaseActionOptions selected="publish" handleChange={handleChange} />);
|
||||||
|
|
||||||
|
const publishOption = screen.getByRole('radio', { name: 'publish' });
|
||||||
|
const unpublishOption = screen.getByRole('radio', { name: 'unpublish' });
|
||||||
|
|
||||||
|
expect(publishOption).toBeInTheDocument();
|
||||||
|
expect(publishOption).toBeChecked();
|
||||||
|
expect(unpublishOption).toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.click(unpublishOption);
|
||||||
|
expect(handleChange).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -11,4 +11,10 @@ export const PERMISSIONS = {
|
|||||||
subject: null,
|
subject: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
createAction: [
|
||||||
|
{
|
||||||
|
action: 'plugin::content-releases.create-action',
|
||||||
|
subject: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { prefixPluginTranslations } from '@strapi/helper-plugin';
|
import { prefixPluginTranslations } from '@strapi/helper-plugin';
|
||||||
import { PaperPlane } from '@strapi/icons';
|
import { PaperPlane } from '@strapi/icons';
|
||||||
|
|
||||||
|
import { CMReleasesContainer } from './components/CMReleasesContainer';
|
||||||
import { PERMISSIONS } from './constants';
|
import { PERMISSIONS } from './constants';
|
||||||
import { pluginId } from './pluginId';
|
import { pluginId } from './pluginId';
|
||||||
import { releaseApi } from './services/release';
|
import { releaseApi } from './services/release';
|
||||||
@ -35,6 +36,12 @@ const admin: Plugin.Config.AdminInput = {
|
|||||||
app.addReducers({
|
app.addReducers({
|
||||||
[releaseApi.reducerPath]: releaseApi.reducer,
|
[releaseApi.reducerPath]: releaseApi.reducer,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Insert the Releases container in the 'right-links' zone of the Content Manager's edit view
|
||||||
|
app.injectContentManagerComponent('editView', 'right-links', {
|
||||||
|
name: `${pluginId}-link`,
|
||||||
|
Component: CMReleasesContainer,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async registerTrads({ locales }: { locales: string[] }) {
|
async registerTrads({ locales }: { locales: string[] }) {
|
||||||
|
|||||||
@ -33,7 +33,7 @@ import styled from 'styled-components';
|
|||||||
import { GetReleases } from '../../../shared/contracts/releases';
|
import { GetReleases } from '../../../shared/contracts/releases';
|
||||||
import { AddReleaseDialog } from '../components/AddReleaseDialog';
|
import { AddReleaseDialog } from '../components/AddReleaseDialog';
|
||||||
import { PERMISSIONS } from '../constants';
|
import { PERMISSIONS } from '../constants';
|
||||||
import { useGetReleasesQuery, GetAllReleasesQueryParams } from '../services/release';
|
import { useGetReleasesQuery, GetReleasesQueryParams } from '../services/release';
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------------------------------
|
/* -------------------------------------------------------------------------------------------------
|
||||||
* ReleasesLayout
|
* ReleasesLayout
|
||||||
@ -167,7 +167,7 @@ const ReleasesGrid = ({ sectionTitle, releases = [], isError = false }: Releases
|
|||||||
const ReleasesPage = () => {
|
const ReleasesPage = () => {
|
||||||
const [addReleaseDialogIsShown, setAddReleaseDialogIsShown] = React.useState(false);
|
const [addReleaseDialogIsShown, setAddReleaseDialogIsShown] = React.useState(false);
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const [{ query }, setQuery] = useQueryParams<GetAllReleasesQueryParams>();
|
const [{ query }, setQuery] = useQueryParams<GetReleasesQueryParams>();
|
||||||
const response = useGetReleasesQuery(query);
|
const response = useGetReleasesQuery(query);
|
||||||
|
|
||||||
const { isLoading, isSuccess, isError } = response;
|
const { isLoading, isSuccess, isError } = response;
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
import { createApi } from '@reduxjs/toolkit/query/react';
|
import { createApi } from '@reduxjs/toolkit/query/react';
|
||||||
|
|
||||||
|
import { CreateReleaseAction } from '../../../shared/contracts/release-actions';
|
||||||
import { pluginId } from '../pluginId';
|
import { pluginId } from '../pluginId';
|
||||||
|
|
||||||
import { axiosBaseQuery } from './axios';
|
import { axiosBaseQuery } from './axios';
|
||||||
|
|
||||||
import type { CreateRelease, GetReleases } from '../../../shared/contracts/releases';
|
import type { CreateRelease, GetReleases } from '../../../shared/contracts/releases';
|
||||||
|
|
||||||
export interface GetAllReleasesQueryParams {
|
interface GetReleasesQueryParams {
|
||||||
page?: number;
|
page?: number;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
filters?: {
|
filters?: {
|
||||||
@ -17,13 +18,44 @@ export interface GetAllReleasesQueryParams {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GetReleasesTabResponse = GetReleases.Response & {
|
||||||
|
meta: {
|
||||||
|
activeTab: 'pending' | 'done';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const releaseApi = createApi({
|
const releaseApi = createApi({
|
||||||
reducerPath: pluginId,
|
reducerPath: pluginId,
|
||||||
baseQuery: axiosBaseQuery,
|
baseQuery: axiosBaseQuery,
|
||||||
tagTypes: ['Releases'],
|
tagTypes: ['Releases'],
|
||||||
endpoints: (build) => {
|
endpoints: (build) => {
|
||||||
return {
|
return {
|
||||||
getReleases: build.query<GetReleases.Response, GetAllReleasesQueryParams | void>({
|
/**
|
||||||
|
* TODO: This will need to evolve to handle queries for:
|
||||||
|
* - Get all releases where the entry is attached
|
||||||
|
* - Get all releases where the entry is not attached
|
||||||
|
*
|
||||||
|
* We need to explore the best way to filter on polymorphic relations in another PR
|
||||||
|
*/
|
||||||
|
getReleasesForEntry: build.query<GetReleases.Response, GetReleasesQueryParams | void>({
|
||||||
|
query() {
|
||||||
|
return {
|
||||||
|
url: '/content-releases',
|
||||||
|
method: 'GET',
|
||||||
|
config: {
|
||||||
|
params: {
|
||||||
|
filters: {
|
||||||
|
releasedAt: {
|
||||||
|
$notNull: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
providesTags: ['Releases'],
|
||||||
|
}),
|
||||||
|
getReleases: build.query<GetReleasesTabResponse, GetReleasesQueryParams | void>({
|
||||||
query(
|
query(
|
||||||
{ page, pageSize, filters } = {
|
{ page, pageSize, filters } = {
|
||||||
page: 1,
|
page: 1,
|
||||||
@ -47,10 +79,10 @@ const releaseApi = createApi({
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
transformResponse(response: GetReleases.Response, meta, arg) {
|
transformResponse(response: GetReleasesTabResponse, meta, arg) {
|
||||||
const releasedAtValue = arg?.filters?.releasedAt?.$notNull;
|
const releasedAtValue = arg?.filters?.releasedAt?.$notNull;
|
||||||
const isActiveDoneTab = releasedAtValue === 'true';
|
const isActiveDoneTab = releasedAtValue === 'true';
|
||||||
const newResponse = {
|
const newResponse: GetReleasesTabResponse = {
|
||||||
...response,
|
...response,
|
||||||
meta: {
|
meta: {
|
||||||
...response.meta,
|
...response.meta,
|
||||||
@ -72,10 +104,36 @@ const releaseApi = createApi({
|
|||||||
},
|
},
|
||||||
invalidatesTags: ['Releases'],
|
invalidatesTags: ['Releases'],
|
||||||
}),
|
}),
|
||||||
|
createReleaseAction: build.mutation<
|
||||||
|
CreateReleaseAction.Response,
|
||||||
|
CreateReleaseAction.Request
|
||||||
|
>({
|
||||||
|
query({ body, params }) {
|
||||||
|
return {
|
||||||
|
url: `/content-releases/${params.releaseId}/actions`,
|
||||||
|
method: 'POST',
|
||||||
|
data: body,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
invalidatesTags: ['Releases'],
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { useGetReleasesQuery, useCreateReleaseMutation } = releaseApi;
|
const {
|
||||||
|
useGetReleasesQuery,
|
||||||
|
useGetReleasesForEntryQuery,
|
||||||
|
useCreateReleaseMutation,
|
||||||
|
useCreateReleaseActionMutation,
|
||||||
|
} = releaseApi;
|
||||||
|
|
||||||
export { useGetReleasesQuery, useCreateReleaseMutation, releaseApi };
|
export {
|
||||||
|
useGetReleasesQuery,
|
||||||
|
useGetReleasesForEntryQuery,
|
||||||
|
useCreateReleaseMutation,
|
||||||
|
useCreateReleaseActionMutation,
|
||||||
|
releaseApi,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type { GetReleasesQueryParams };
|
||||||
|
|||||||
@ -1,4 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"content-manager-edit-view.add-to-release.select-label": "Select a release",
|
||||||
|
"content-manager-edit-view.add-to-release.select-placeholder": "Select",
|
||||||
|
"content-manager-edit-view.add-to-release.action-type-label": "What do you want to do with this entry?",
|
||||||
|
"content-manager-edit-view.add-to-release.cancel-button": "Cancel",
|
||||||
|
"content-manager-edit-view.add-to-release.continue-button": "Continue",
|
||||||
|
"content-manager-edit-view.add-to-release": "Add to release",
|
||||||
|
"content-manager-edit-view.add-to-release.notification.success": "Entry added to release",
|
||||||
|
"content-manager.notification.entry-error": "Failed to get entry data",
|
||||||
"plugin.name": "Releases",
|
"plugin.name": "Releases",
|
||||||
"pages.Releases.title": "Releases",
|
"pages.Releases.title": "Releases",
|
||||||
"pages.Releases.header-subtitle": "{number, plural, =0 {No releases} one {# release} other {# releases}}",
|
"pages.Releases.header-subtitle": "{number, plural, =0 {No releases} one {# release} other {# releases}}",
|
||||||
|
|||||||
@ -1,6 +1,98 @@
|
|||||||
import releaseController from '../release';
|
import releaseController from '../release';
|
||||||
|
|
||||||
describe('Release controller', () => {
|
describe('Release controller', () => {
|
||||||
|
describe('findMany', () => {
|
||||||
|
it('should call findPage', async () => {
|
||||||
|
const findPage = jest.fn().mockResolvedValue({ results: [], pagination: {} });
|
||||||
|
const findMany = jest.fn().mockResolvedValue([]);
|
||||||
|
const userAbility = {
|
||||||
|
can: jest.fn(),
|
||||||
|
};
|
||||||
|
const ctx = {
|
||||||
|
state: {
|
||||||
|
userAbility: {},
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
global.strapi = {
|
||||||
|
// @ts-expect-error Ignore missing properties
|
||||||
|
admin: {
|
||||||
|
services: {
|
||||||
|
permission: {
|
||||||
|
createPermissionsManager: jest.fn(() => ({
|
||||||
|
ability: userAbility,
|
||||||
|
validateQuery: jest.fn(),
|
||||||
|
sanitizeQuery: jest.fn(() => ctx.query),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
// @ts-expect-error Ignore missing properties
|
||||||
|
'content-releases': {
|
||||||
|
services: {
|
||||||
|
release: {
|
||||||
|
findPage,
|
||||||
|
findMany,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// @ts-expect-error partial context
|
||||||
|
await releaseController.findMany(ctx);
|
||||||
|
|
||||||
|
expect(findPage).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call findMany', async () => {
|
||||||
|
const findPage = jest.fn().mockResolvedValue({ results: [], pagination: {} });
|
||||||
|
const findMany = jest.fn().mockResolvedValue([]);
|
||||||
|
const userAbility = {
|
||||||
|
can: jest.fn(),
|
||||||
|
};
|
||||||
|
const ctx = {
|
||||||
|
state: {
|
||||||
|
userAbility: {},
|
||||||
|
},
|
||||||
|
query: {},
|
||||||
|
};
|
||||||
|
global.strapi = {
|
||||||
|
// @ts-expect-error Ignore missing properties
|
||||||
|
admin: {
|
||||||
|
services: {
|
||||||
|
permission: {
|
||||||
|
createPermissionsManager: jest.fn(() => ({
|
||||||
|
ability: userAbility,
|
||||||
|
validateQuery: jest.fn(),
|
||||||
|
sanitizeQuery: jest.fn(() => ctx.query),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
// @ts-expect-error Ignore missing properties
|
||||||
|
'content-releases': {
|
||||||
|
services: {
|
||||||
|
release: {
|
||||||
|
findPage,
|
||||||
|
findMany,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// @ts-expect-error partial context
|
||||||
|
await releaseController.findMany(ctx);
|
||||||
|
|
||||||
|
expect(findMany).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
describe('create', () => {
|
describe('create', () => {
|
||||||
it('throws an error given bad request arguments', () => {
|
it('throws an error given bad request arguments', () => {
|
||||||
const ctx = {
|
const ctx = {
|
||||||
@ -17,7 +109,7 @@ describe('Release controller', () => {
|
|||||||
expect(() => releaseController.create(ctx)).rejects.toThrow('name is a required field');
|
expect(() => releaseController.create(ctx)).rejects.toThrow('name is a required field');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('update', () => {
|
describe('update', () => {
|
||||||
it('throws an error given bad request arguments', () => {
|
it('throws an error given bad request arguments', () => {
|
||||||
const ctx = {
|
const ctx = {
|
||||||
@ -27,18 +119,18 @@ describe('Release controller', () => {
|
|||||||
// Mock missing name on request
|
// Mock missing name on request
|
||||||
request: {
|
request: {
|
||||||
body: {
|
body: {
|
||||||
name: ''
|
name: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
params: {
|
params: {
|
||||||
id: 1
|
id: 1,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// @ts-expect-error partial context
|
// @ts-expect-error partial context
|
||||||
expect(() => releaseController.update(ctx)).rejects.toThrow('name is a required field');
|
expect(() => releaseController.update(ctx)).rejects.toThrow('name is a required field');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws an error given unknown request arguments', () => {
|
it('throws an error given unknown request arguments', () => {
|
||||||
const ctx = {
|
const ctx = {
|
||||||
state: {
|
state: {
|
||||||
@ -48,16 +140,18 @@ describe('Release controller', () => {
|
|||||||
request: {
|
request: {
|
||||||
body: {
|
body: {
|
||||||
name: 'Test',
|
name: 'Test',
|
||||||
unknown: ''
|
unknown: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
params: {
|
params: {
|
||||||
id: 1
|
id: 1,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// @ts-expect-error partial context
|
// @ts-expect-error partial context
|
||||||
expect(() => releaseController.update(ctx)).rejects.toThrow('this field has unspecified keys: unknown');
|
expect(() => releaseController.update(ctx)).rejects.toThrow(
|
||||||
|
'this field has unspecified keys: unknown'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import type Koa from 'koa';
|
import type Koa from 'koa';
|
||||||
import { validateReleaseActionCreateSchema } from './validation/release-action';
|
import { validateReleaseAction } from './validation/release-action';
|
||||||
import type { CreateReleaseAction } from '../../../shared/contracts/release-actions';
|
import type { CreateReleaseAction } from '../../../shared/contracts/release-actions';
|
||||||
import { getService } from '../utils';
|
import { getService } from '../utils';
|
||||||
|
|
||||||
@ -8,7 +8,7 @@ const releaseActionController = {
|
|||||||
const releaseId: CreateReleaseAction.Request['params']['releaseId'] = ctx.params.releaseId;
|
const releaseId: CreateReleaseAction.Request['params']['releaseId'] = ctx.params.releaseId;
|
||||||
const releaseActionArgs: CreateReleaseAction.Request['body'] = ctx.request.body;
|
const releaseActionArgs: CreateReleaseAction.Request['body'] = ctx.request.body;
|
||||||
|
|
||||||
await validateReleaseActionCreateSchema(releaseActionArgs);
|
await validateReleaseAction(releaseActionArgs);
|
||||||
|
|
||||||
const releaseService = getService('release', { strapi });
|
const releaseService = getService('release', { strapi });
|
||||||
const releaseAction = await releaseService.createAction(releaseId, releaseActionArgs);
|
const releaseAction = await releaseService.createAction(releaseId, releaseActionArgs);
|
||||||
|
|||||||
@ -13,6 +13,21 @@ import { getService } from '../utils';
|
|||||||
|
|
||||||
type ReleaseWithPopulatedActions = Release & { actions: { count: number } };
|
type ReleaseWithPopulatedActions = Release & { actions: { count: number } };
|
||||||
|
|
||||||
|
const formatDataObject = (releases: ReleaseWithPopulatedActions[]) => {
|
||||||
|
return releases.map((release) => {
|
||||||
|
const { actions, ...releaseData } = release;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...releaseData,
|
||||||
|
actions: {
|
||||||
|
meta: {
|
||||||
|
count: actions.count,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const releaseController = {
|
const releaseController = {
|
||||||
async findMany(ctx: Koa.Context) {
|
async findMany(ctx: Koa.Context) {
|
||||||
const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
|
const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
|
||||||
@ -23,23 +38,19 @@ const releaseController = {
|
|||||||
await permissionsManager.validateQuery(ctx.query);
|
await permissionsManager.validateQuery(ctx.query);
|
||||||
const query = await permissionsManager.sanitizeQuery(ctx.query);
|
const query = await permissionsManager.sanitizeQuery(ctx.query);
|
||||||
|
|
||||||
const { results, pagination } = await getService('release', { strapi }).findMany(query);
|
const isPaginatedRequest =
|
||||||
|
query && Object.keys(query).some((key) => ['page', 'pageSize'].includes(key));
|
||||||
|
|
||||||
// Format the data object
|
if (isPaginatedRequest) {
|
||||||
const data = results.map((release: ReleaseWithPopulatedActions) => {
|
const { results, pagination } = await getService('release', { strapi }).findPage(query);
|
||||||
const { actions, ...releaseData } = release;
|
// Format the data object
|
||||||
|
const data = formatDataObject(results);
|
||||||
|
|
||||||
return {
|
ctx.body = { data, meta: { pagination } };
|
||||||
...releaseData,
|
} else {
|
||||||
actions: {
|
const results = await getService('release', { strapi }).findMany(query);
|
||||||
meta: {
|
ctx.body = { data: formatDataObject(results) };
|
||||||
count: actions.count,
|
}
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
ctx.body = { data, meta: { pagination } };
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async findOne(ctx: Koa.Context) {
|
async findOne(ctx: Koa.Context) {
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
import { yup, validateYupSchema } from '@strapi/utils';
|
import { yup, validateYupSchema } from '@strapi/utils';
|
||||||
|
|
||||||
const releaseActionCreateSchema = yup.object().shape({
|
const RELEASE_ACTION_SCHEMA = yup.object().shape({
|
||||||
entry: yup
|
entry: yup
|
||||||
.object()
|
.object()
|
||||||
.shape({
|
.shape({
|
||||||
id: yup.number().required(),
|
id: yup.strapiID().required(),
|
||||||
contentType: yup.string().required(),
|
contentType: yup.string().required(),
|
||||||
})
|
})
|
||||||
.required(),
|
.required(),
|
||||||
type: yup.string().oneOf(['publish', 'unpublish']).required(),
|
type: yup.string().oneOf(['publish', 'unpublish']).required(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const validateReleaseActionCreateSchema = validateYupSchema(releaseActionCreateSchema);
|
export const validateReleaseAction = validateYupSchema(RELEASE_ACTION_SCHEMA);
|
||||||
|
|||||||
@ -1,11 +1,4 @@
|
|||||||
import { yup, validateYupSchema } from '@strapi/utils';
|
import { validateYupSchema } from '@strapi/utils';
|
||||||
|
import { RELEASE_SCHEMA } from '../../../../shared/validation-schemas';
|
||||||
|
|
||||||
const validateReleaseSchema = yup
|
export const validateRelease = validateYupSchema(RELEASE_SCHEMA);
|
||||||
.object()
|
|
||||||
.shape({
|
|
||||||
name: yup.string().trim().required(),
|
|
||||||
})
|
|
||||||
.required()
|
|
||||||
.noUnknown();
|
|
||||||
|
|
||||||
export const validateRelease = validateYupSchema(validateReleaseSchema);
|
|
||||||
|
|||||||
@ -1,7 +1,12 @@
|
|||||||
import { setCreatorFields, errors } from '@strapi/utils';
|
import { setCreatorFields, errors } from '@strapi/utils';
|
||||||
import type { LoadedStrapi } from '@strapi/types';
|
import type { LoadedStrapi } from '@strapi/types';
|
||||||
import { RELEASE_ACTION_MODEL_UID, RELEASE_MODEL_UID } from '../constants';
|
import { RELEASE_ACTION_MODEL_UID, RELEASE_MODEL_UID } from '../constants';
|
||||||
import type { GetReleases, CreateRelease, UpdateRelease, GetRelease } from '../../../shared/contracts/releases';
|
import type {
|
||||||
|
GetReleases,
|
||||||
|
CreateRelease,
|
||||||
|
UpdateRelease,
|
||||||
|
GetRelease,
|
||||||
|
} from '../../../shared/contracts/releases';
|
||||||
import type { CreateReleaseAction } from '../../../shared/contracts/release-actions';
|
import type { CreateReleaseAction } from '../../../shared/contracts/release-actions';
|
||||||
import type { UserInfo } from '../../../shared/types';
|
import type { UserInfo } from '../../../shared/types';
|
||||||
import { getService } from '../utils';
|
import { getService } from '../utils';
|
||||||
@ -14,7 +19,7 @@ const createReleaseService = ({ strapi }: { strapi: LoadedStrapi }) => ({
|
|||||||
data: releaseWithCreatorFields,
|
data: releaseWithCreatorFields,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
findMany(query?: GetReleases.Request['query']) {
|
findPage(query?: GetReleases.Request['query']) {
|
||||||
return strapi.entityService.findPage(RELEASE_MODEL_UID, {
|
return strapi.entityService.findPage(RELEASE_MODEL_UID, {
|
||||||
...query,
|
...query,
|
||||||
populate: {
|
populate: {
|
||||||
@ -25,6 +30,17 @@ const createReleaseService = ({ strapi }: { strapi: LoadedStrapi }) => ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
findMany(query?: GetReleases.Request['query']) {
|
||||||
|
return strapi.entityService.findMany(RELEASE_MODEL_UID, {
|
||||||
|
...query,
|
||||||
|
populate: {
|
||||||
|
actions: {
|
||||||
|
// @ts-expect-error TS error on populate, is not considering count
|
||||||
|
count: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
findOne(id: GetRelease.Request['params']['id']) {
|
findOne(id: GetRelease.Request['params']['id']) {
|
||||||
return strapi.entityService.findOne(RELEASE_MODEL_UID, id, {
|
return strapi.entityService.findOne(RELEASE_MODEL_UID, id, {
|
||||||
populate: {
|
populate: {
|
||||||
@ -35,11 +51,21 @@ const createReleaseService = ({ strapi }: { strapi: LoadedStrapi }) => ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
async update(id: number, releaseData: UpdateRelease.Request['body'], { user }: { user: UserInfo }) {
|
async update(
|
||||||
|
id: number,
|
||||||
|
releaseData: UpdateRelease.Request['body'],
|
||||||
|
{ user }: { user: UserInfo }
|
||||||
|
) {
|
||||||
const updatedRelease = await setCreatorFields({ user, isEdition: true })(releaseData);
|
const updatedRelease = await setCreatorFields({ user, isEdition: true })(releaseData);
|
||||||
|
|
||||||
// @ts-expect-error Type 'ReleaseUpdateArgs' has no properties in common with type 'Partial<Input<"plugin::content-releases.release">>'
|
const release = await strapi.entityService.update(RELEASE_MODEL_UID, id, {
|
||||||
const release = await strapi.entityService.update(RELEASE_MODEL_UID, id, { data: updatedRelease });
|
/*
|
||||||
|
* The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
|
||||||
|
* is not compatible with the type we are passing here: UpdateRelease.Request['body']
|
||||||
|
*/
|
||||||
|
// @ts-expect-error see above
|
||||||
|
data: updatedRelease,
|
||||||
|
});
|
||||||
|
|
||||||
if (!release) {
|
if (!release) {
|
||||||
throw new errors.NotFoundError(`No release found for id ${id}`);
|
throw new errors.NotFoundError(`No release found for id ${id}`);
|
||||||
|
|||||||
@ -23,7 +23,7 @@ const createReleaseValidationService = ({ strapi }: { strapi: LoadedStrapi }) =>
|
|||||||
|
|
||||||
const isEntryInRelease = release.actions.some(
|
const isEntryInRelease = release.actions.some(
|
||||||
(action) =>
|
(action) =>
|
||||||
action.entry.id === releaseActionArgs.entry.id &&
|
Number(action.entry.id) === Number(releaseActionArgs.entry.id) &&
|
||||||
action.contentType === releaseActionArgs.entry.contentType
|
action.contentType === releaseActionArgs.entry.contentType
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -34,8 +34,7 @@ export declare namespace GetReleases {
|
|||||||
export interface Response {
|
export interface Response {
|
||||||
data: ReleaseDataResponse[];
|
data: ReleaseDataResponse[];
|
||||||
meta: {
|
meta: {
|
||||||
pagination: Pagination;
|
pagination?: Pagination;
|
||||||
activeTab?: string;
|
|
||||||
};
|
};
|
||||||
error?: errors.ApplicationError;
|
error?: errors.ApplicationError;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
import * as yup from 'yup';
|
||||||
|
|
||||||
|
export const RELEASE_SCHEMA = yup
|
||||||
|
.object()
|
||||||
|
.shape({
|
||||||
|
name: yup.string().trim().required(),
|
||||||
|
})
|
||||||
|
.required()
|
||||||
|
.noUnknown();
|
||||||
@ -14,6 +14,7 @@ export interface CheckPermissionsProps {
|
|||||||
|
|
||||||
const CheckPermissions = ({ permissions = [], children }: CheckPermissionsProps) => {
|
const CheckPermissions = ({ permissions = [], children }: CheckPermissionsProps) => {
|
||||||
const { allPermissions } = useRBACProvider();
|
const { allPermissions } = useRBACProvider();
|
||||||
|
|
||||||
const toggleNotification = useNotification();
|
const toggleNotification = useNotification();
|
||||||
const [state, setState] = React.useState({ isLoading: true, canAccess: false });
|
const [state, setState] = React.useState({ isLoading: true, canAccess: false });
|
||||||
const isMounted = React.useRef(true);
|
const isMounted = React.useRef(true);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user