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 { 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
|
||||||
|
@ -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>
|
||||||
|
@ -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));
|
||||||
|
@ -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 },
|
||||||
|
@ -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}>
|
||||||
|
@ -112,7 +112,6 @@ export function ReviewWorkflowsEditView() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
validationSchema: getWorkflowValidationSchema({ formatMessage }),
|
validationSchema: getWorkflowValidationSchema({ formatMessage }),
|
||||||
validateOnChange: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
useInjectReducer(REDUX_NAMESPACE, reducer);
|
useInjectReducer(REDUX_NAMESPACE, reducer);
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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,
|
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
@ -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),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user