diff --git a/dictation_client/src/common/errors/code.ts b/dictation_client/src/common/errors/code.ts index 241d0be..d8eb794 100644 --- a/dictation_client/src/common/errors/code.ts +++ b/dictation_client/src/common/errors/code.ts @@ -51,4 +51,6 @@ export const errorCodes = [ "E010809", // ライセンス発行キャンセル不可エラー(ステータスが変えられている場合) "E010810", // ライセンス発行キャンセル不可エラー(発行から一定期間経過した場合) "E010811", // ライセンス発行キャンセル不可エラー(発行したライセンスが割り当てされている場合) + "E011001", // ワークタイプ重複エラー + "E011002", // ワークタイプ登録上限超過エラー ] as const; diff --git a/dictation_client/src/features/workflow/worktype/operations.ts b/dictation_client/src/features/workflow/worktype/operations.ts index ad05f7c..5f754c9 100644 --- a/dictation_client/src/features/workflow/worktype/operations.ts +++ b/dictation_client/src/features/workflow/worktype/operations.ts @@ -43,3 +43,70 @@ export const listWorktypesAsync = createAsyncThunk< return thunkApi.rejectWithValue({ error }); } }); + +export const addWorktypeAsync = createAsyncThunk< + { + // return empty + }, + void, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("workflow/addWorktypeAsync", async (args, thunkApi) => { + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration, accessToken } = state.auth; + const config = new Configuration(configuration); + const accountsApi = new AccountsApi(config); + // stateからworktypeIdとdescriptionを取得する + const { worktypeId, description } = state.worktype.apps; + + try { + await accountsApi.createWorktype( + { + worktypeId, + description, + }, + { + headers: { authorization: `Bearer ${accessToken}` }, + } + ); + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); + + return {}; + } catch (e) { + // e ⇒ errorObjectに変換" + const error = createErrorObject(e); + + let errorMessage = getTranslationID("common.message.internalServerError"); + + // 既に同じworktypeIdが存在する場合 + if (error.code === "E011001") { + errorMessage = getTranslationID( + "worktypeIdSetting.message.alreadyWorktypeIdExistError" + ); + } + // worktypeIdが上限に達している場合 + if (error.code === "E011002") { + errorMessage = getTranslationID( + "worktypeIdSetting.message.worktypeIDLimitError" + ); + } + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: errorMessage, + }) + ); + return thunkApi.rejectWithValue({ error }); + } +}); diff --git a/dictation_client/src/features/workflow/worktype/selectors.ts b/dictation_client/src/features/workflow/worktype/selectors.ts index 343d665..e1ec412 100644 --- a/dictation_client/src/features/workflow/worktype/selectors.ts +++ b/dictation_client/src/features/workflow/worktype/selectors.ts @@ -2,6 +2,24 @@ import { RootState } from "app/store"; export const selectWorktypes = (state: RootState) => state.worktype.domain.worktypes; - export const selectIsLoading = (state: RootState) => state.worktype.apps.isLoading; + +export const selectWorktypeId = (state: RootState) => + state.worktype.apps.worktypeId; + +export const selectDescription = (state: RootState) => + state.worktype.apps.description; + +// worktypeIdの値をチェックする +export const selectHasErrorWorktypeId = (state: RootState) => { + const { worktypeId } = state.worktype.apps; + // worktypeIdが空文字の場合はエラー + const isEmptyWorktypeId = worktypeId === ""; + + // worktypeIdに\/ : * ? “< > | .が含まれている場合はエラー + const incorrectPattern = /[\\/:*?"<>|.]|[^ -~]/; + const hasIncorrectPatternWorktypeId = incorrectPattern.test(worktypeId); + + return { isEmptyWorktypeId, hasIncorrectPatternWorktypeId }; +}; diff --git a/dictation_client/src/features/workflow/worktype/state.ts b/dictation_client/src/features/workflow/worktype/state.ts index 365b7b7..2e101f7 100644 --- a/dictation_client/src/features/workflow/worktype/state.ts +++ b/dictation_client/src/features/workflow/worktype/state.ts @@ -7,6 +7,8 @@ export interface WorktypeState { export interface Apps { isLoading: boolean; + worktypeId: string; + description?: string; } export interface Domain { diff --git a/dictation_client/src/features/workflow/worktype/worktypeSlice.ts b/dictation_client/src/features/workflow/worktype/worktypeSlice.ts index 624f3bd..d0345e5 100644 --- a/dictation_client/src/features/workflow/worktype/worktypeSlice.ts +++ b/dictation_client/src/features/workflow/worktype/worktypeSlice.ts @@ -1,10 +1,11 @@ -import { createSlice } from "@reduxjs/toolkit"; +import { PayloadAction, createSlice } from "@reduxjs/toolkit"; import { WorktypeState } from "./state"; -import { listWorktypesAsync } from "./operations"; +import { addWorktypeAsync, listWorktypesAsync } from "./operations"; const initialState: WorktypeState = { apps: { isLoading: false, + worktypeId: "", }, domain: {}, }; @@ -12,7 +13,26 @@ const initialState: WorktypeState = { export const worktypeSlice = createSlice({ name: "worktype", initialState, - reducers: {}, + reducers: { + cleanupWorktype: (state) => { + state.apps.worktypeId = initialState.apps.worktypeId; + state.apps.description = undefined; + }, + changeWorktypeId: ( + state, + action: PayloadAction<{ worktypeId: string }> + ) => { + const { worktypeId } = action.payload; + state.apps.worktypeId = worktypeId; + }, + changeDescription: ( + state, + action: PayloadAction<{ description?: string }> + ) => { + const { description } = action.payload; + state.apps.description = description; + }, + }, extraReducers: (builder) => { builder.addCase(listWorktypesAsync.pending, (state) => { state.apps.isLoading = true; @@ -25,7 +45,14 @@ export const worktypeSlice = createSlice({ builder.addCase(listWorktypesAsync.rejected, (state) => { state.apps.isLoading = false; }); + builder.addCase(addWorktypeAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(addWorktypeAsync.rejected, (state) => { + state.apps.isLoading = false; + }); }, }); - +export const { changeDescription, changeWorktypeId, cleanupWorktype } = + worktypeSlice.actions; export default worktypeSlice.reducer; diff --git a/dictation_client/src/pages/WorkTypeIdSettingPage/addWorktypeIdPopup.tsx b/dictation_client/src/pages/WorkTypeIdSettingPage/addWorktypeIdPopup.tsx new file mode 100644 index 0000000..e611f9f --- /dev/null +++ b/dictation_client/src/pages/WorkTypeIdSettingPage/addWorktypeIdPopup.tsx @@ -0,0 +1,144 @@ +import React, { useCallback, useState } from "react"; +import { useTranslation } from "react-i18next"; +import styles from "styles/app.module.scss"; +import { useDispatch, useSelector } from "react-redux"; +import { + addWorktypeAsync, + changeDescription, + changeWorktypeId, + cleanupWorktype, + listWorktypesAsync, + selectDescription, + selectHasErrorWorktypeId, + selectWorktypeId, +} from "features/workflow/worktype"; +import { AppDispatch } from "app/store"; +import { getTranslationID } from "translation"; +import close from "../../assets/images/close.svg"; + +// popupのpropsの型定義 +interface AddWorktypeIdPopupProps { + onClose: () => void; + isOpen: boolean; +} + +export const AddWorktypeIdPopup: React.FC = ( + props: AddWorktypeIdPopupProps +): JSX.Element => { + const { onClose, isOpen } = props; + const [t] = useTranslation(); + const dispatch: AppDispatch = useDispatch(); + const worktypeId = useSelector(selectWorktypeId); + const description = useSelector(selectDescription); + // 追加ボタンを押したかどうか + const [isPushAddButton, setIsPushAddButton] = useState(false); + // WorktypeIdのバリデーションチェック + const { hasIncorrectPatternWorktypeId, isEmptyWorktypeId } = useSelector( + selectHasErrorWorktypeId + ); + + // ×ボタンを押した時の処理 + const closePopup = useCallback(() => { + dispatch(cleanupWorktype()); + setIsPushAddButton(false); + onClose(); + }, [onClose, dispatch]); + + // 追加ボタンを押した時の処理 + const addWorktypeId = useCallback(async () => { + setIsPushAddButton(true); + if (isEmptyWorktypeId || hasIncorrectPatternWorktypeId) { + return; + } + const { meta } = await dispatch(addWorktypeAsync()); + if (meta.requestStatus === "fulfilled") { + dispatch(listWorktypesAsync()); + closePopup(); + } + }, [closePopup, dispatch, hasIncorrectPatternWorktypeId, isEmptyWorktypeId]); + + return ( +
+
+

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

+
+
+
+
{t(getTranslationID("worktypeIdSetting.label.worktypeId"))}
+
+ { + dispatch(changeWorktypeId({ worktypeId: e.target.value })); + }} + /> + {isPushAddButton && isEmptyWorktypeId && ( + + {t(getTranslationID("common.message.inputEmptyError"))} + + )} + {isPushAddButton && hasIncorrectPatternWorktypeId && ( + + {t( + getTranslationID( + "worktypeIdSetting.message.worktypeIdIncorrectError" + ) + )} + + )} + + {t(getTranslationID("worktypeIdSetting.label.worktypeIdTerms"))} + +
+
+ {t( + getTranslationID("worktypeIdSetting.label.descriptionOptional") + )} +
+
+ { + const description = + e.target.value === "" ? undefined : e.target.value; + dispatch(changeDescription({ description })); + }} + /> +
+
+ +
+
+
+
+
+ ); +}; diff --git a/dictation_client/src/pages/WorkTypeIdSettingPage/index.tsx b/dictation_client/src/pages/WorkTypeIdSettingPage/index.tsx index 2a77caf..ef20581 100644 --- a/dictation_client/src/pages/WorkTypeIdSettingPage/index.tsx +++ b/dictation_client/src/pages/WorkTypeIdSettingPage/index.tsx @@ -15,6 +15,7 @@ import { selectWorktypes, } from "features/workflow/worktype"; import { AppDispatch } from "app/store"; +import { AddWorktypeIdPopup } from "./addWorktypeIdPopup"; const WorktypeIdSettingPage: React.FC = (): JSX.Element => { const dispatch: AppDispatch = useDispatch(); @@ -22,148 +23,171 @@ const WorktypeIdSettingPage: React.FC = (): JSX.Element => { const isLoading = useSelector(selectIsLoading); const worktypes = useSelector(selectWorktypes); const [selectedRow, setSelectedRow] = useState(NaN); + // 追加Popupの表示制御 + const [isShowAddPopup, setIsShowAddPopup] = useState(false); useEffect(() => { dispatch(listWorktypesAsync()); }, [dispatch]); return ( -
-
- -
-
-
-

- {t(getTranslationID("workflowPage.label.title"))} -

-

- {t(getTranslationID("worktypeIdSetting.label.title"))} -

-
-
-
- - - - - - - - {worktypes?.map((worktype) => ( - { - setSelectedRow(worktype.id); - }} - onMouseLeave={() => { - setSelectedRow(NaN); - }} - > - - - - - ))} -
- {t(getTranslationID("worktypeIdSetting.label.worktypeId"))} - - {t(getTranslationID("worktypeIdSetting.label.description"))} - {/** empty th */}
{worktype.worktypeId}{worktype.description} - -
- {!isLoading && worktypes?.length === 0 && ( -

- {t(getTranslationID("common.message.listEmpty"))} -

- )} - {isLoading && ( - Loading - )} + <> + { + setIsShowAddPopup(false); + }} + isOpen={isShowAddPopup} + /> +
+
+ +
+
+
+

+ {t(getTranslationID("workflowPage.label.title"))} +

+

+ {t(getTranslationID("worktypeIdSetting.label.title"))} +

-
-
-
-
-
+
+
+ + + + + + + + {worktypes?.map((worktype) => ( + { + setSelectedRow(worktype.id); + }} + onMouseLeave={() => { + setSelectedRow(NaN); + }} + > + + + + + ))} +
+ {t( + getTranslationID("worktypeIdSetting.label.worktypeId") + )} + + {t( + getTranslationID("worktypeIdSetting.label.description") + )} + {/** empty th */}
{worktype.worktypeId}{worktype.description} + +
+ {!isLoading && worktypes?.length === 0 && ( +

+ {t(getTranslationID("common.message.listEmpty"))} +

+ )} + {isLoading && ( + Loading + )} +
+
+ + +