mirror of
https://github.com/strapi/strapi.git
synced 2025-09-25 16:29:34 +00:00
Merge pull request #16771 from strapi/feature/review-workflow-multiple-qa
Settings: Improve setting pages for multiple workflows
This commit is contained in:
commit
7c44f1830a
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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));
|
||||
|
@ -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 },
|
||||
|
@ -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}>
|
||||
|
@ -112,7 +112,6 @@ export function ReviewWorkflowsEditView() {
|
||||
}
|
||||
},
|
||||
validationSchema: getWorkflowValidationSchema({ formatMessage }),
|
||||
validateOnChange: true,
|
||||
});
|
||||
|
||||
useInjectReducer(REDUX_NAMESPACE, reducer);
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -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,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
@ -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),
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user