Merge pull request #16771 from strapi/feature/review-workflow-multiple-qa

Settings: Improve setting pages for multiple workflows
This commit is contained in:
Gustav Hansen 2023-05-19 17:16:02 +02:00 committed by GitHub
commit 7c44f1830a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 125 additions and 80 deletions

View File

@ -143,8 +143,8 @@ export function Stage({
const { trackUsage } = useTracking(); const { trackUsage } = useTracking();
const dispatch = useDispatch(); const dispatch = useDispatch();
const [isOpen, setIsOpen] = React.useState(isOpenDefault); const [isOpen, setIsOpen] = React.useState(isOpenDefault);
const [nameField, nameMeta] = useField(`stages.${index}.name`); const [nameField, nameMeta, nameHelper] = useField(`stages.${index}.name`);
const [colorField, colorMeta] = useField(`stages.${index}.color`); const [colorField, colorMeta, colorHelper] = useField(`stages.${index}.color`);
const [{ handlerId, isDragging, handleKeyDown }, stageRef, dropRef, dragRef, dragPreviewRef] = const [{ handlerId, isDragging, handleKeyDown }, stageRef, dropRef, dragRef, dragPreviewRef] =
useDragAndDrop(canReorder, { useDragAndDrop(canReorder, {
index, index,
@ -249,7 +249,7 @@ export function Stage({
})} })}
error={nameMeta.error ?? false} error={nameMeta.error ?? false}
onChange={(event) => { onChange={(event) => {
nameField.onChange(event); nameHelper.setValue(event.target.value);
dispatch(updateStage(id, { name: event.target.value })); dispatch(updateStage(id, { name: event.target.value }));
}} }}
required required
@ -278,7 +278,7 @@ export function Stage({
name={colorField.name} name={colorField.name}
options={colorOptions} options={colorOptions}
onChange={({ value }) => { onChange={({ value }) => {
colorField.onChange({ target: { value } }); colorHelper.setValue(value);
dispatch(updateStage(id, { color: value })); dispatch(updateStage(id, { color: value }));
}} }}
// If no color was found in all the valid theme colors it means a user // If no color was found in all the valid theme colors it means a user

View File

@ -10,8 +10,8 @@ import { updateWorkflow } from '../../actions';
export function WorkflowAttributes({ contentTypes: { collectionTypes, singleTypes } }) { export function WorkflowAttributes({ contentTypes: { collectionTypes, singleTypes } }) {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const dispatch = useDispatch(); const dispatch = useDispatch();
const [nameField, nameMeta] = useField('name'); const [nameField, nameMeta, nameHelper] = useField('name');
const [contentTypesField, contentTypesMeta] = useField('contentTypes'); const [contentTypesField, contentTypesMeta, contentTypesHelper] = useField('contentTypes');
return ( return (
<Grid background="neutral0" hasRadius gap={4} padding={6} shadow="tableShadow"> <Grid background="neutral0" hasRadius gap={4} padding={6} shadow="tableShadow">
@ -26,7 +26,7 @@ export function WorkflowAttributes({ contentTypes: { collectionTypes, singleType
error={nameMeta.error ?? false} error={nameMeta.error ?? false}
onChange={(event) => { onChange={(event) => {
dispatch(updateWorkflow({ name: event.target.value })); dispatch(updateWorkflow({ name: event.target.value }));
nameField.onChange(event); nameHelper.setValue(event.target.value);
}} }}
required required
/> />
@ -53,7 +53,7 @@ export function WorkflowAttributes({ contentTypes: { collectionTypes, singleType
})} })}
onChange={(values) => { onChange={(values) => {
dispatch(updateWorkflow({ contentTypes: values })); dispatch(updateWorkflow({ contentTypes: values }));
contentTypesField.onChange({ target: { value: values } }); contentTypesHelper.setValue(values);
}} }}
options={[ options={[
{ {
@ -82,7 +82,6 @@ export function WorkflowAttributes({ contentTypes: { collectionTypes, singleType
id: 'Settings.review-workflows.workflow.contentTypes.placeholder', id: 'Settings.review-workflows.workflow.contentTypes.placeholder',
defaultMessage: 'Select', defaultMessage: 'Select',
})} })}
required
/> />
</GridItem> </GridItem>
</Grid> </Grid>

View File

@ -65,7 +65,7 @@ describe('useReviewWorkflows', () => {
expect(result.current.workflows.isLoading).toBe(true); expect(result.current.workflows.isLoading).toBe(true);
expect(get).toBeCalledWith('/admin/review-workflows/workflows/', { expect(get).toBeCalledWith('/admin/review-workflows/workflows/', {
params: { populate: 'stages' }, params: { sort: 'name:asc', populate: 'stages' },
}); });
await waitFor(() => expect(result.current.workflows.isLoading).toBe(false)); await waitFor(() => expect(result.current.workflows.isLoading).toBe(false));

View File

@ -9,7 +9,7 @@ export function useReviewWorkflows(workflowId) {
const client = useQueryClient(); const client = useQueryClient();
const workflowQueryKey = [QUERY_BASE_KEY, workflowId ?? 'default']; const workflowQueryKey = [QUERY_BASE_KEY, workflowId ?? 'default'];
async function fetchWorkflows({ params = { populate: 'stages' } }) { async function fetchWorkflows({ params = { sort: 'name:asc', populate: 'stages' } }) {
try { try {
const { const {
data: { data }, data: { data },

View File

@ -3,6 +3,7 @@ import { useFormik, Form, FormikProvider } from 'formik';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useMutation } from 'react-query'; import { useMutation } from 'react-query';
import { useHistory } from 'react-router-dom';
import { import {
CheckPagePermissions, CheckPagePermissions,
@ -28,6 +29,7 @@ import * as Layout from '../../components/Layout';
export function ReviewWorkflowsCreateView() { export function ReviewWorkflowsCreateView() {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const { post } = useFetchClient(); const { post } = useFetchClient();
const { push } = useHistory();
const { formatAPIError } = useAPIErrorHandler(); const { formatAPIError } = useAPIErrorHandler();
const dispatch = useDispatch(); const dispatch = useDispatch();
const toggleNotification = useNotification(); const toggleNotification = useNotification();
@ -42,7 +44,7 @@ export function ReviewWorkflowsCreateView() {
async ({ workflow }) => { async ({ workflow }) => {
const { const {
data: { data }, data: { data },
} = await post(`/admin/review-workflows/workflow`, { } = await post(`/admin/review-workflows/workflows`, {
data: workflow, data: workflow,
}); });
@ -52,7 +54,10 @@ export function ReviewWorkflowsCreateView() {
onSuccess() { onSuccess() {
toggleNotification({ toggleNotification({
type: 'success', type: 'success',
message: { id: 'notification.success.saved', defaultMessage: 'Saved' }, message: {
id: 'Settings.review-workflows.create.page.notification.success',
defaultMessage: 'Workflow successfully created',
},
}); });
}, },
} }
@ -60,9 +65,11 @@ export function ReviewWorkflowsCreateView() {
const submitForm = async () => { const submitForm = async () => {
try { try {
const res = await mutateAsync({ workflow: currentWorkflow }); const workflow = await mutateAsync({ workflow: currentWorkflow });
return res; push(`/settings/review-workflows/${workflow.id}`);
return workflow;
} catch (error) { } catch (error) {
toggleNotification({ toggleNotification({
type: 'warning', type: 'warning',
@ -71,8 +78,6 @@ export function ReviewWorkflowsCreateView() {
return null; return null;
} }
// TODO: redirect to edit view
}; };
const formik = useFormik({ const formik = useFormik({
@ -82,14 +87,13 @@ export function ReviewWorkflowsCreateView() {
submitForm(); submitForm();
}, },
validationSchema: getWorkflowValidationSchema({ formatMessage }), validationSchema: getWorkflowValidationSchema({ formatMessage }),
validateOnChange: false,
}); });
useInjectReducer(REDUX_NAMESPACE, reducer); useInjectReducer(REDUX_NAMESPACE, reducer);
React.useEffect(() => { React.useEffect(() => {
dispatch(resetWorkflow()); dispatch(resetWorkflow());
}, [dispatch, collectionTypes, singleTypes]); }, [dispatch]);
return ( return (
<CheckPagePermissions permissions={adminPermissions.settings['review-workflows'].main}> <CheckPagePermissions permissions={adminPermissions.settings['review-workflows'].main}>

View File

@ -112,7 +112,6 @@ export function ReviewWorkflowsEditView() {
} }
}, },
validationSchema: getWorkflowValidationSchema({ formatMessage }), validationSchema: getWorkflowValidationSchema({ formatMessage }),
validateOnChange: true,
}); });
useInjectReducer(REDUX_NAMESPACE, reducer); useInjectReducer(REDUX_NAMESPACE, reducer);

View File

@ -8,6 +8,7 @@ import {
ConfirmDialog, ConfirmDialog,
Link, Link,
LinkButton, LinkButton,
onRowClick,
pxToRem, pxToRem,
useAPIErrorHandler, useAPIErrorHandler,
useFetchClient, useFetchClient,
@ -34,7 +35,17 @@ import adminPermissions from '../../../../../../../../admin/src/permissions';
import * as Layout from '../../components/Layout'; import * as Layout from '../../components/Layout';
const ActionLink = styled(Link)` const ActionLink = styled(Link)`
align-items: center;
height: ${pxToRem(32)};
display: flex;
justify-content: center;
padding: ${({ theme }) => `${theme.spaces[2]}}`};
width: ${pxToRem(32)};
svg { svg {
height: ${pxToRem(12)};
width: ${pxToRem(12)};
path { path {
fill: ${({ theme }) => theme.colors.neutral500}; fill: ${({ theme }) => theme.colors.neutral500};
} }
@ -154,6 +165,14 @@ export function ReviewWorkflowsListView() {
})} })}
</Typography> </Typography>
</Th> </Th>
<Th>
<Typography variant="sigma">
{formatMessage({
id: 'Settings.review-workflows.list.page.list.column.contentTypes.title',
defaultMessage: 'Content Types',
})}
</Typography>
</Th>
<Th> <Th>
<VisuallyHidden> <VisuallyHidden>
{formatMessage({ {formatMessage({
@ -168,7 +187,16 @@ export function ReviewWorkflowsListView() {
<Tbody> <Tbody>
{workflowsData.data.map((workflow) => ( {workflowsData.data.map((workflow) => (
<Tr <Tr
onRowClick={() => push(`/settings/review-workflows/${workflow.id}`)} {...onRowClick({
fn(event) {
// Abort row onClick event when the user click on the delete button
if (event.target.nodeName === 'BUTTON') {
return;
}
push(`/settings/review-workflows/${workflow.id}`);
},
})}
key={`workflow-${workflow.id}`} key={`workflow-${workflow.id}`}
> >
<Td width={pxToRem(250)}> <Td width={pxToRem(250)}>
@ -180,24 +208,12 @@ export function ReviewWorkflowsListView() {
<Typography textColor="neutral800">{workflow.stages.length}</Typography> <Typography textColor="neutral800">{workflow.stages.length}</Typography>
</Td> </Td>
<Td> <Td>
<Flex gap={2} justifyContent="end"> <Typography textColor="neutral800">
{workflowsData.data.length > 1 && ( {(workflow?.contentTypes ?? []).join(', ')}
<IconButton </Typography>
aria-label={formatMessage( </Td>
{ <Td>
id: 'Settings.review-workflows.list.page.list.column.actions.delete.label', <Flex alignItems="center" justifyContent="end">
defaultMessage: 'Delete {name}',
},
{ name: 'Default workflow' }
)}
icon={<Trash />}
noBorder
onClick={() => {
handleDeleteWorkflow(workflow.id);
}}
/>
)}
<ActionLink <ActionLink
to={`/settings/review-workflows/${workflow.id}`} to={`/settings/review-workflows/${workflow.id}`}
aria-label={formatMessage( aria-label={formatMessage(
@ -210,6 +226,22 @@ export function ReviewWorkflowsListView() {
> >
<Pencil /> <Pencil />
</ActionLink> </ActionLink>
<IconButton
aria-label={formatMessage(
{
id: 'Settings.review-workflows.list.page.list.column.actions.delete.label',
defaultMessage: 'Delete {name}',
},
{ name: 'Default workflow' }
)}
disabled={workflowsData.data.length === 1}
icon={<Trash />}
noBorder
onClick={() => {
handleDeleteWorkflow(workflow.id);
}}
/>
</Flex> </Flex>
</Td> </Td>
</Tr> </Tr>

View File

@ -20,10 +20,13 @@ export const initialState = {
clientState: { clientState: {
currentWorkflow: { currentWorkflow: {
data: { data: {
name: '',
contentTypes: [],
stages: [ stages: [
{ {
color: STAGE_COLOR_DEFAULT, color: STAGE_COLOR_DEFAULT,
name: '', name: '',
__temp_key__: 1,
}, },
], ],
}, },
@ -159,8 +162,8 @@ export function reducer(state = initialState, action) {
draft.serverState.workflow draft.serverState.workflow
); );
} else { } else {
// if there is no workflow on the server, the workflow can never be dirty // if there is no workflow on the server, the workflow is awalys considered dirty
draft.clientState.currentWorkflow.isDirty = false; draft.clientState.currentWorkflow.isDirty = true;
} }
}); });
} }

View File

@ -553,15 +553,11 @@ describe('Admin | Settings | Review Workflows | reducer', () => {
expect.objectContaining({ expect.objectContaining({
clientState: expect.objectContaining({ clientState: expect.objectContaining({
currentWorkflow: expect.objectContaining({ currentWorkflow: expect.objectContaining({
data: { data: expect.objectContaining({
stages: [ name: '',
{ stages: [expect.objectContaining({ name: '', __temp_key__: 1 })],
color: '#4945ff', }),
name: '', isDirty: true,
},
],
},
isDirty: false,
}), }),
}), }),
}) })

View File

@ -2,36 +2,48 @@ import * as yup from 'yup';
export function getWorkflowValidationSchema({ formatMessage }) { export function getWorkflowValidationSchema({ formatMessage }) {
return yup.object({ return yup.object({
contentTypes: yup.array().of(yup.string()).required(), contentTypes: yup.array().of(yup.string()),
name: yup.string().required(), name: yup
.string()
.max(
255,
formatMessage({
id: 'Settings.review-workflows.validation.name.max-length',
defaultMessage: 'Name can not be longer than 255 characters',
})
)
.required(),
stages: yup.array().of( stages: yup
yup.object().shape({ .array()
name: yup .of(
.string() yup.object().shape({
.required( name: yup
formatMessage({ .string()
id: 'Settings.review-workflows.validation.stage.name', .required(
defaultMessage: 'Name is required', formatMessage({
}) id: 'Settings.review-workflows.validation.stage.name',
) defaultMessage: 'Name is required',
.max( })
255, )
formatMessage({ .max(
id: 'Settings.review-workflows.validation.stage.max-length', 255,
defaultMessage: 'Name can not be longer than 255 characters', formatMessage({
}) id: 'Settings.review-workflows.validation.stage.max-length',
), defaultMessage: 'Name can not be longer than 255 characters',
color: yup })
.string() ),
.required( color: yup
formatMessage({ .string()
id: 'Settings.review-workflows.validation.stage.color', .required(
defaultMessage: 'Color is required', formatMessage({
}) id: 'Settings.review-workflows.validation.stage.color',
) defaultMessage: 'Color is required',
.matches(/^#(?:[0-9a-fA-F]{3}){1,2}$/i), })
}) )
), .matches(/^#(?:[0-9a-fA-F]{3}){1,2}$/i),
})
)
.min(1),
}); });
} }