From 2d569aee6d26b91bef4e97ee9fecc256c6c4b04e Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Thu, 12 Oct 2023 07:42:53 +0000 Subject: [PATCH] =?UTF-8?q?Merged=20PR=20479:=20=E3=83=AF=E3=83=BC?= =?UTF-8?q?=E3=82=AF=E3=83=95=E3=83=AD=E3=83=BC=E6=9B=B4=E6=96=B0=E3=83=9D?= =?UTF-8?q?=E3=83=83=E3=83=97=E3=82=A2=E3=83=83=E3=83=97=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task2777: ワークフロー更新ポップアップ実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2777) - ワークフロー編集ポップアップを実装しました。 ## レビューポイント - 表示内容は適切か - 選択ワークフローの値取得処理は適切か ## UIの変更 - [Task2777](https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task2777?csf=1&web=1&e=RfM1Dv) ## 動作確認状況 - ローカルで確認 --- .../src/features/workflow/operations.ts | 99 ++++++ .../src/features/workflow/selectors.ts | 9 + .../src/features/workflow/state.ts | 1 + .../src/features/workflow/workflowSlice.ts | 40 ++- .../pages/WorkflowPage/editworkflowPopup.tsx | 313 ++++++++++++++++++ .../src/pages/WorkflowPage/index.tsx | 29 +- dictation_client/src/translation/de.json | 12 +- dictation_client/src/translation/en.json | 12 +- dictation_client/src/translation/es.json | 12 +- dictation_client/src/translation/fr.json | 12 +- 10 files changed, 529 insertions(+), 10 deletions(-) create mode 100644 dictation_client/src/pages/WorkflowPage/editworkflowPopup.tsx diff --git a/dictation_client/src/features/workflow/operations.ts b/dictation_client/src/features/workflow/operations.ts index 7810d20..7b11e04 100644 --- a/dictation_client/src/features/workflow/operations.ts +++ b/dictation_client/src/features/workflow/operations.ts @@ -140,6 +140,105 @@ export const createWorkflowAsync = createAsyncThunk< } }); +export const updateWorkflowAsync = createAsyncThunk< + { + /* Empty Object */ + }, + void, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("workflow/updateWorkflowAsync", async (args, thunkApi) => { + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration, accessToken } = state.auth; + const config = new Configuration(configuration); + const workflowsApi = new WorkflowsApi(config); + const { + selectedWorkflow, + selectedAssignees, + authorId, + templateId, + worktypeId, + } = state.workflow.apps; + + try { + if (selectedWorkflow === undefined) { + throw new Error("selectedWorkflow is not found"); + } + + if (authorId === undefined) { + throw new Error("authorId is not found"); + } + // 選択されたタイピストを取得し、リクエスト用の型に変換する + const typists = selectedAssignees.map( + (item): WorkflowTypist => ({ + typistId: item.typistUserId, + typistGroupId: item.typistGroupId, + }) + ); + await workflowsApi.updateWorkflow( + selectedWorkflow.id, + { + authorId, + typists, + templateId, + worktypeId, + }, + { + headers: { authorization: `Bearer ${accessToken}` }, + } + ); + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); + return {}; + } catch (e) { + // e ⇒ errorObjectに変換" + const error = createErrorObject(e); + const { code, statusCode } = error; + + if (statusCode === 400) { + // AuthorIDとWorktypeIDが一致するものが既に存在する場合 + if (code === "E013001") { + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: getTranslationID( + "workflowPage.message.workflowConflictError" + ), + }) + ); + return thunkApi.rejectWithValue({ error }); + } + + // パラメータが存在しない場合 + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: getTranslationID("workflowPage.message.saveFailedError"), + }) + ); + return thunkApi.rejectWithValue({ error }); + } + // その他のエラー + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: getTranslationID("common.message.internalServerError"), + }) + ); + return thunkApi.rejectWithValue({ error }); + } +}); + export const getworkflowRelationsAsync = createAsyncThunk< { authors: Author[]; diff --git a/dictation_client/src/features/workflow/selectors.ts b/dictation_client/src/features/workflow/selectors.ts index a71ba4a..685d81e 100644 --- a/dictation_client/src/features/workflow/selectors.ts +++ b/dictation_client/src/features/workflow/selectors.ts @@ -36,6 +36,15 @@ export const selectWorkflowAssinee = (state: RootState) => { ), }; }; +export const selectSelectedWorkflow = (state: RootState) => + state.workflow.apps.selectedWorkflow; +export const selectAuthorId = (state: RootState) => + state.workflow.apps.authorId; +export const selectWorktypeId = (state: RootState) => + state.workflow.apps.worktypeId; +export const selectTemplateId = (state: RootState) => + state.workflow.apps.templateId; + export const selectIsAddLoading = (state: RootState) => state.workflow.apps.isAddLoading; diff --git a/dictation_client/src/features/workflow/state.ts b/dictation_client/src/features/workflow/state.ts index e99a186..6bb892a 100644 --- a/dictation_client/src/features/workflow/state.ts +++ b/dictation_client/src/features/workflow/state.ts @@ -12,6 +12,7 @@ export interface Apps { authorId?: number; worktypeId?: number; templateId?: number; + selectedWorkflow?: Workflow; } export interface Domain { diff --git a/dictation_client/src/features/workflow/workflowSlice.ts b/dictation_client/src/features/workflow/workflowSlice.ts index 4011d19..737f5cf 100644 --- a/dictation_client/src/features/workflow/workflowSlice.ts +++ b/dictation_client/src/features/workflow/workflowSlice.ts @@ -5,6 +5,7 @@ import { getworkflowRelationsAsync, deleteWorkflowAsync, listWorkflowAsync, + updateWorkflowAsync, } from "./operations"; import { WorkflowState } from "./state"; @@ -22,12 +23,17 @@ export const workflowSlice = createSlice({ initialState, reducers: { clearWorkflow: (state) => { + state.apps.selectedWorkflow = undefined; state.apps.selectedAssignees = []; state.apps.authorId = undefined; state.apps.worktypeId = undefined; state.apps.templateId = undefined; state.domain.workflowRelations = undefined; }, + setAssignees: (state, action: PayloadAction<{ assignees: Assignee[] }>) => { + const { assignees } = action.payload; + state.apps.selectedAssignees = assignees; + }, addAssignee: (state, action: PayloadAction<{ assignee: Assignee }>) => { const { assignee } = action.payload; const { selectedAssignees } = state.apps; @@ -56,18 +62,35 @@ export const workflowSlice = createSlice({ x.typistGroupId !== assignee.typistGroupId ); }, - changeAuthor: (state, action: PayloadAction<{ authorId: number }>) => { + changeAuthor: ( + state, + action: PayloadAction<{ authorId?: number | undefined }> + ) => { const { authorId } = action.payload; state.apps.authorId = authorId; }, - changeWorktype: (state, action: PayloadAction<{ worktypeId?: number }>) => { + changeWorktype: ( + state, + action: PayloadAction<{ worktypeId?: number | undefined }> + ) => { const { worktypeId } = action.payload; state.apps.worktypeId = worktypeId; }, - changeTemplate: (state, action: PayloadAction<{ templateId?: number }>) => { + changeTemplate: ( + state, + action: PayloadAction<{ templateId?: number | undefined }> + ) => { const { templateId } = action.payload; state.apps.templateId = templateId; }, + changeSelectedWorkflow: ( + state, + action: PayloadAction<{ workflowId: number }> + ) => { + const { workflowId } = action.payload; + const workflow = state.domain.workflows?.find((x) => x.id === workflowId); + state.apps.selectedWorkflow = workflow; + }, }, extraReducers: (builder) => { builder.addCase(listWorkflowAsync.pending, (state) => { @@ -128,6 +151,15 @@ export const workflowSlice = createSlice({ builder.addCase(createWorkflowAsync.rejected, (state) => { state.apps.isAddLoading = false; }); + builder.addCase(updateWorkflowAsync.pending, (state) => { + state.apps.isAddLoading = true; + }); + builder.addCase(updateWorkflowAsync.fulfilled, (state) => { + state.apps.isAddLoading = false; + }); + builder.addCase(updateWorkflowAsync.rejected, (state) => { + state.apps.isAddLoading = false; + }); builder.addCase(deleteWorkflowAsync.pending, (state) => { state.apps.isLoading = true; }); @@ -141,11 +173,13 @@ export const workflowSlice = createSlice({ }); export const { + setAssignees, addAssignee, removeAssignee, changeAuthor, changeWorktype, changeTemplate, + changeSelectedWorkflow, clearWorkflow, } = workflowSlice.actions; diff --git a/dictation_client/src/pages/WorkflowPage/editworkflowPopup.tsx b/dictation_client/src/pages/WorkflowPage/editworkflowPopup.tsx new file mode 100644 index 0000000..47f7054 --- /dev/null +++ b/dictation_client/src/pages/WorkflowPage/editworkflowPopup.tsx @@ -0,0 +1,313 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { AppDispatch } from "app/store"; +import progress_activit from "assets/images/progress_activit.svg"; +import { + addAssignee, + removeAssignee, + changeAuthor, + changeTemplate, + changeWorktype, + clearWorkflow, + selectIsAddLoading, + selectWorkflowAssinee, + selectWorkflowError, + selectWorkflowRelations, + selectSelectedWorkflow, + selectAuthorId, + selectWorktypeId, + setAssignees, + selectTemplateId, +} from "features/workflow"; +import { + getworkflowRelationsAsync, + listWorkflowAsync, + updateWorkflowAsync, +} from "features/workflow/operations"; +import { useTranslation } from "react-i18next"; +import { useDispatch, useSelector } from "react-redux"; +import styles from "styles/app.module.scss"; +import { getTranslationID } from "translation"; +import close from "../../assets/images/close.svg"; + +interface EditWorkflowPopupProps { + onClose: () => void; +} + +export const EditWorkflowPopup: React.FC = ( + props +): JSX.Element => { + const { onClose } = props; + const dispatch: AppDispatch = useDispatch(); + const [t] = useTranslation(); + // 保存ボタンを押したかどうか + const [isPushEditButton, setIsPushEditButton] = useState(false); + + const workflow = useSelector(selectSelectedWorkflow); + const authorId = useSelector(selectAuthorId); + const worktypeId = useSelector(selectWorktypeId); + const templateId = useSelector(selectTemplateId); + + const workflowRelations = useSelector(selectWorkflowRelations); + const { poolAssignees, selectedAssignees } = useSelector( + selectWorkflowAssinee + ); + const isLoading = useSelector(selectIsAddLoading); + const { hasAuthorIdEmptyError, hasSelectedWorkflowAssineeEmptyError } = + useSelector(selectWorkflowError); + useEffect(() => { + dispatch(getworkflowRelationsAsync()); + // ポップアップのアンマウント時に初期化を行う + return () => { + dispatch(clearWorkflow()); + setIsPushEditButton(false); + }; + }, [dispatch]); + + useEffect(() => { + dispatch(changeAuthor({ authorId: workflow?.author?.id })); + dispatch(changeWorktype({ worktypeId: workflow?.worktype?.id })); + dispatch(changeTemplate({ templateId: workflow?.template?.id })); + dispatch(setAssignees({ assignees: workflow?.typists ?? [] })); + }, [dispatch, workflow]); + + const changeWorktypeId = useCallback( + (target: string) => { + // 空文字の場合はundefinedをdispatchする + if (target === "") { + dispatch(changeWorktype({ worktypeId: undefined })); + } else if (!Number.isNaN(Number(target))) { + dispatch(changeWorktype({ worktypeId: Number(target) })); + } + }, + [dispatch] + ); + + const changeTemplateId = useCallback( + (target: string) => { + // 空文字の場合はundefinedをdispatchする + if (target === "") { + dispatch(changeTemplate({ templateId: undefined })); + } else if (!Number.isNaN(Number(target))) { + dispatch(changeTemplate({ templateId: Number(target) })); + } + }, + [dispatch] + ); + + const changeAuthorId = useCallback( + (target: string) => { + if (!Number.isNaN(target)) { + dispatch(changeAuthor({ authorId: Number(target) })); + } + }, + [dispatch] + ); + + // 保存ボタン押下時の処理 + const handleSave = useCallback(async () => { + setIsPushEditButton(true); + // エラーチェック + if (hasAuthorIdEmptyError || hasSelectedWorkflowAssineeEmptyError) { + return; + } + const { meta } = await dispatch(updateWorkflowAsync()); + if (meta.requestStatus === "fulfilled") { + onClose(); + dispatch(listWorkflowAsync()); + } + }, [ + dispatch, + hasAuthorIdEmptyError, + hasSelectedWorkflowAssineeEmptyError, + onClose, + ]); + + return ( +
+
+

+ {t(getTranslationID("workflowPage.label.editRoutingRule"))} + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions */} + close +

+
+
+
+
{t(getTranslationID("workflowPage.label.authorID"))}
+
+ + {isPushEditButton && hasAuthorIdEmptyError && ( + + {t(getTranslationID("workflowPage.message.inputEmptyError"))} + + )} +
+
+ {t(getTranslationID("workflowPage.label.worktypeOptional"))} +
+
+ +
+
+ {t(getTranslationID("typistGroupSetting.label.transcriptionist"))} +
+
+
    +
  • + {t(getTranslationID("workflowPage.label.selected"))} +
  • + {selectedAssignees?.map((x) => { + const key = `${x.typistName}_${ + x.typistUserId ?? x.typistGroupId + }`; + return ( +
  • + { + dispatch(removeAssignee({ assignee: x })); + }} + /> + +
  • + ); + })} +
+

+

    +
  • + {t(getTranslationID("workflowPage.label.pool"))} +
  • + {poolAssignees?.map((x) => { + const key = `${x.typistName}_${ + x.typistUserId ?? x.typistGroupId + }`; + return ( +
  • + dispatch(addAssignee({ assignee: x }))} + /> + +
  • + ); + })} +
+ {isPushEditButton && hasSelectedWorkflowAssineeEmptyError && ( + + {t( + getTranslationID( + "workflowPage.message.selectedTypistEmptyError" + ) + )} + + )} +
+
+ {t(getTranslationID("workflowPage.label.templateOptional"))} +
+
+ +
+
+ + {isLoading && ( + Loading + )} +
+
+
+
+
+ ); +}; diff --git a/dictation_client/src/pages/WorkflowPage/index.tsx b/dictation_client/src/pages/WorkflowPage/index.tsx index 1831085..16f5c49 100644 --- a/dictation_client/src/pages/WorkflowPage/index.tsx +++ b/dictation_client/src/pages/WorkflowPage/index.tsx @@ -13,17 +13,23 @@ import { useDispatch, useSelector } from "react-redux"; import { deleteWorkflowAsync, listWorkflowAsync, -} from "features/workflow/operations"; -import { selectIsLoading, selectWorkflows } from "features/workflow"; + changeSelectedWorkflow, + selectIsLoading, + selectWorkflows, +} from "features/workflow"; import progress_activit from "assets/images/progress_activit.svg"; import { getTranslationID } from "translation"; import { AddWorkflowPopup } from "./addworkflowPopup"; +import { EditWorkflowPopup } from "./editworkflowPopup"; const WorkflowPage: React.FC = (): JSX.Element => { const dispatch: AppDispatch = useDispatch(); const [t] = useTranslation(); // 追加Popupの表示制御 const [isShowAddPopup, setIsShowAddPopup] = useState(false); + // 編集Popupの表示制御 + const [isShowEditPopup, setIsShowEditPopup] = useState(false); + const workflows = useSelector(selectWorkflows); const isLoading = useSelector(selectIsLoading); @@ -57,6 +63,13 @@ const WorkflowPage: React.FC = (): JSX.Element => { }} /> )} + {isShowEditPopup && ( + { + setIsShowEditPopup(false); + }} + /> + )}
@@ -157,7 +170,17 @@ const WorkflowPage: React.FC = (): JSX.Element => {