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 dispatch = useDispatch();
const [isOpen, setIsOpen] = React.useState(isOpenDefault);
const [nameField, nameMeta] = useField(`stages.${index}.name`);
const [colorField, colorMeta] = useField(`stages.${index}.color`);
const [nameField, nameMeta, nameHelper] = useField(`stages.${index}.name`);
const [colorField, colorMeta, colorHelper] = useField(`stages.${index}.color`);
const [{ handlerId, isDragging, handleKeyDown }, stageRef, dropRef, dragRef, dragPreviewRef] =
useDragAndDrop(canReorder, {
index,
@ -249,7 +249,7 @@ export function Stage({
})}
error={nameMeta.error ?? false}
onChange={(event) => {
nameField.onChange(event);
nameHelper.setValue(event.target.value);
dispatch(updateStage(id, { name: event.target.value }));
}}
required
@ -278,7 +278,7 @@ export function Stage({
name={colorField.name}
options={colorOptions}
onChange={({ value }) => {
colorField.onChange({ target: { value } });
colorHelper.setValue(value);
dispatch(updateStage(id, { color: value }));
}}
// 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 } }) {
const { formatMessage } = useIntl();
const dispatch = useDispatch();
const [nameField, nameMeta] = useField('name');
const [contentTypesField, contentTypesMeta] = useField('contentTypes');
const [nameField, nameMeta, nameHelper] = useField('name');
const [contentTypesField, contentTypesMeta, contentTypesHelper] = useField('contentTypes');
return (
<Grid background="neutral0" hasRadius gap={4} padding={6} shadow="tableShadow">
@ -26,7 +26,7 @@ export function WorkflowAttributes({ contentTypes: { collectionTypes, singleType
error={nameMeta.error ?? false}
onChange={(event) => {
dispatch(updateWorkflow({ name: event.target.value }));
nameField.onChange(event);
nameHelper.setValue(event.target.value);
}}
required
/>
@ -53,7 +53,7 @@ export function WorkflowAttributes({ contentTypes: { collectionTypes, singleType
})}
onChange={(values) => {
dispatch(updateWorkflow({ contentTypes: values }));
contentTypesField.onChange({ target: { value: values } });
contentTypesHelper.setValue(values);
}}
options={[
{
@ -82,7 +82,6 @@ export function WorkflowAttributes({ contentTypes: { collectionTypes, singleType
id: 'Settings.review-workflows.workflow.contentTypes.placeholder',
defaultMessage: 'Select',
})}
required
/>
</GridItem>
</Grid>

View File

@ -65,7 +65,7 @@ describe('useReviewWorkflows', () => {
expect(result.current.workflows.isLoading).toBe(true);
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));

View File

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

View File

@ -3,6 +3,7 @@ import { useFormik, Form, FormikProvider } from 'formik';
import { useIntl } from 'react-intl';
import { useDispatch, useSelector } from 'react-redux';
import { useMutation } from 'react-query';
import { useHistory } from 'react-router-dom';
import {
CheckPagePermissions,
@ -28,6 +29,7 @@ import * as Layout from '../../components/Layout';
export function ReviewWorkflowsCreateView() {
const { formatMessage } = useIntl();
const { post } = useFetchClient();
const { push } = useHistory();
const { formatAPIError } = useAPIErrorHandler();
const dispatch = useDispatch();
const toggleNotification = useNotification();
@ -42,7 +44,7 @@ export function ReviewWorkflowsCreateView() {
async ({ workflow }) => {
const {
data: { data },
} = await post(`/admin/review-workflows/workflow`, {
} = await post(`/admin/review-workflows/workflows`, {
data: workflow,
});
@ -52,7 +54,10 @@ export function ReviewWorkflowsCreateView() {
onSuccess() {
toggleNotification({
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 () => {
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) {
toggleNotification({
type: 'warning',
@ -71,8 +78,6 @@ export function ReviewWorkflowsCreateView() {
return null;
}
// TODO: redirect to edit view
};
const formik = useFormik({
@ -82,14 +87,13 @@ export function ReviewWorkflowsCreateView() {
submitForm();
},
validationSchema: getWorkflowValidationSchema({ formatMessage }),
validateOnChange: false,
});
useInjectReducer(REDUX_NAMESPACE, reducer);
React.useEffect(() => {
dispatch(resetWorkflow());
}, [dispatch, collectionTypes, singleTypes]);
}, [dispatch]);
return (
<CheckPagePermissions permissions={adminPermissions.settings['review-workflows'].main}>

View File

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

View File

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

View File

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

View File

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

View File

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