Merged PR 365: 画面実装(TypistGroup編集ポップアップ&TypistGroup設定画面)

## 概要
[Task2458: 画面実装(TypistGroup編集ポップアップ&TypistGroup設定画面)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2458)

- タイピストグループ編集Popup作成
- Popupを呼び出す処理を追加
- 実行ボタンを「Save」に統一
  - タスク一覧にも同様のボタンがあったので一緒に修正

## レビューポイント
- Popupを呼び出すときに選択したTypistGroupのIdを渡すようにしているが問題ないか
  - 引数として渡した方がわかりやすいと思ったのでこの実装にしました
- Popupの項目をクリーンアップするタイミングをPopupが閉じたときに統一したが問題ないか

## UIの変更
- 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/Task2458?csf=1&web=1&e=urD3Fa

## 動作確認状況
- ローカルで確認

## 補足
- 相談、参考資料などがあれば
This commit is contained in:
saito.k 2023-08-30 02:37:01 +00:00
parent bba69651fe
commit e82f66e32a
14 changed files with 4797 additions and 5733 deletions

View File

@ -2,6 +2,6 @@
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2,
"generator-cli": {
"version": "6.6.0"
"version": "7.0.0"
}
}

View File

@ -1 +1 @@
6.6.0
7.0.0

File diff suppressed because it is too large Load Diff

View File

@ -144,7 +144,7 @@ export const toPathString = function (url: URL) {
*/
export const createRequestFunction = function (axiosArgs: RequestArgs, globalAxios: AxiosInstance, BASE_PATH: string, configuration?: Configuration) {
return <T = unknown, R = AxiosResponse<T>>(axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
const axiosRequestArgs = {...axiosArgs.options, url: (configuration?.basePath || basePath) + axiosArgs.url};
const axiosRequestArgs = {...axiosArgs.options, url: (configuration?.basePath || axios.defaults.baseURL || basePath) + axiosArgs.url};
return axios.request<T, R>(axiosRequestArgs);
};
}

View File

@ -7,6 +7,8 @@ import {
GetTypistGroupsResponse,
GetTypistsResponse,
CreateTypistGroupRequest,
Typist,
TypistGroup,
} from "../../../api/api";
import { Configuration } from "../../../api/configuration";
import { ErrorObject, createErrorObject } from "../../../common/errors";
@ -119,7 +121,124 @@ export const createTypistGroupAsync = createAsyncThunk<
const message =
error.statusCode === 400
? getTranslationID("typistGroupSetting.message.groupAddFailedError")
? getTranslationID("typistGroupSetting.message.groupSaveFailedError")
: getTranslationID("common.message.internalServerError");
thunkApi.dispatch(
openSnackbar({
level: "error",
message,
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const getTypistGroupAsync = createAsyncThunk<
{
typists: Typist[];
typistGroup: TypistGroup;
selectedTypistIds: number[];
},
{ typistGroupId: number },
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("workflow/getTypistGroupAsync", async (args, thunkApi) => {
const { typistGroupId } = args;
// 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);
try {
// タイピスト取得処理が別にあるが、storeの状態を意識せずに処理を行うためにここで取得する
const { typists } = (
await accountsApi.getTypists({
headers: { authorization: `Bearer ${accessToken}` },
})
).data;
const { typistGroupName, typistIds } = (
await accountsApi.getTypistGroup(typistGroupId, {
headers: { authorization: `Bearer ${accessToken}` },
})
).data;
return {
typists,
typistGroup: { id: typistGroupId, name: typistGroupName },
selectedTypistIds: typistIds,
};
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
const message = getTranslationID("common.message.internalServerError");
thunkApi.dispatch(
openSnackbar({
level: "error",
message,
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const updateTypistGroupAsync = createAsyncThunk<
{
/* Empty Object */
},
void,
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("workflow/updateTypistGroupAsync", async (args, thunkApi) => {
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration, accessToken } = state.auth;
const { updateTypistGroupId, selectedTypists, groupName } =
state.typistGroup.apps;
const config = new Configuration(configuration);
const accountsApi = new AccountsApi(config);
if (!updateTypistGroupId) {
throw new Error("updateTypistGroupId is undefined.");
}
try {
await accountsApi.updateTypistGroup(
updateTypistGroupId,
{
typistGroupName: groupName,
typistIds: selectedTypists.map((x) => x.id),
},
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
const message =
error.statusCode === 400
? getTranslationID("typistGroupSetting.message.groupSaveFailedError")
: getTranslationID("common.message.internalServerError");
thunkApi.dispatch(

View File

@ -9,6 +9,7 @@ export interface Apps {
isLoading: boolean;
selectedTypists: Typist[];
groupName: string;
updateTypistGroupId?: number;
}
export interface Domain {

View File

@ -1,7 +1,12 @@
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
import { Typist } from "api";
import { TypistGroupState } from "./state";
import { listTypistGroupsAsync, listTypistsAsync } from "./operations";
import {
getTypistGroupAsync,
listTypistGroupsAsync,
listTypistsAsync,
updateTypistGroupAsync,
} from "./operations";
const initialState: TypistGroupState = {
apps: {
@ -19,9 +24,11 @@ export const typistGroupSlice = createSlice({
name: "typistGroup",
initialState,
reducers: {
cleanupAddCroup: (state) => {
cleanupTypistGroup: (state) => {
state.apps.groupName = "";
state.apps.selectedTypists = [];
state.apps.updateTypistGroupId = undefined;
state.domain.typists = [];
},
addSelectedTypist: (state, action: PayloadAction<{ typist: Typist }>) => {
const { typist } = action.payload;
@ -70,11 +77,40 @@ export const typistGroupSlice = createSlice({
builder.addCase(listTypistsAsync.rejected, (state) => {
state.apps.isLoading = false;
});
builder.addCase(getTypistGroupAsync.rejected, (state) => {
state.apps.isLoading = false;
});
builder.addCase(getTypistGroupAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(getTypistGroupAsync.fulfilled, (state, action) => {
const { typistGroup, selectedTypistIds, typists } = action.payload;
// 対象タイピストグループのID・名前を設定
state.apps.updateTypistGroupId = typistGroup.id;
state.apps.groupName = typistGroup.name;
// 選択済みのタイピストを設定
state.apps.selectedTypists = typists
.filter((x) => selectedTypistIds.includes(x.id))
.sort((a, b) => a.id - b.id);
// すべてのタイピストを設定
state.domain.typists = typists;
state.apps.isLoading = false;
});
builder.addCase(updateTypistGroupAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(updateTypistGroupAsync.fulfilled, (state) => {
state.apps.isLoading = false;
});
builder.addCase(updateTypistGroupAsync.rejected, (state) => {
state.apps.isLoading = false;
});
},
});
export const {
cleanupAddCroup,
cleanupTypistGroup,
addSelectedTypist,
removeSelectedTypist,
changeGroupName,

View File

@ -8,13 +8,13 @@ import { useTranslation } from "react-i18next";
import {
addSelectedTypist,
listTypistsAsync,
cleanupAddCroup,
removeSelectedTypist,
selectPoolTypists,
selectSelectedTypists,
selectGroupName,
changeGroupName,
selectAddGroupErrors,
cleanupTypistGroup,
} from "features/workflow/typistGroup";
import {
createTypistGroupAsync,
@ -47,7 +47,6 @@ export const AddTypistGroupPopup: React.FC<AddTypistGroupPopupProps> = (
// 開閉時のみ実行
useEffect(() => {
if (isOpen) {
dispatch(cleanupAddCroup());
dispatch(listTypistsAsync());
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -57,7 +56,8 @@ export const AddTypistGroupPopup: React.FC<AddTypistGroupPopupProps> = (
const closePopup = useCallback(() => {
setIsPushAddButton(false);
onClose(false);
}, [onClose]);
dispatch(cleanupTypistGroup());
}, [dispatch, onClose]);
// グループ追加を実行
const addTypistGroup = useCallback(async () => {
@ -190,7 +190,7 @@ export const AddTypistGroupPopup: React.FC<AddTypistGroupPopupProps> = (
<input
type="button"
name="submit"
value={t(getTranslationID("typistGroupSetting.label.addGroup"))}
value={t(getTranslationID("typistGroupSetting.label.save"))}
className={`${styles.formSubmit} ${styles.marginBtm1} ${styles.isActive}`}
onClick={addTypistGroup}
/>

View File

@ -0,0 +1,197 @@
import React, { useCallback, useEffect, useState } from "react";
import styles from "styles/app.module.scss";
import { useDispatch, useSelector } from "react-redux";
import { Typist } from "api";
import { AppDispatch } from "app/store";
import { getTranslationID } from "translation";
import { useTranslation } from "react-i18next";
import {
addSelectedTypist,
cleanupTypistGroup,
removeSelectedTypist,
selectPoolTypists,
selectSelectedTypists,
selectGroupName,
changeGroupName,
selectAddGroupErrors,
} from "features/workflow/typistGroup";
import {
getTypistGroupAsync,
listTypistGroupsAsync,
updateTypistGroupAsync,
} from "features/workflow/typistGroup/operations";
import { openSnackbar } from "features/ui";
import close from "../../assets/images/close.svg";
interface EditTypistGroupPopupProps {
onClose: (isChanged: boolean) => void;
isOpen: boolean;
typistGroupId: number;
}
export const EditTypistGroupPopup: React.FC<EditTypistGroupPopupProps> = (
props
) => {
const { onClose, isOpen, typistGroupId } = props;
const dispatch: AppDispatch = useDispatch();
const [t] = useTranslation();
const [isPushEditButton, setIsPushEditButton] = useState<boolean>(false);
const poolTypists = useSelector(selectPoolTypists);
const selectedTypists = useSelector(selectSelectedTypists);
const groupName = useSelector(selectGroupName);
const { hasErrorEmptyGroupName, hasErrorSelectedTypistsEmpty } =
useSelector(selectAddGroupErrors);
// 表示時のみ実行
useEffect(() => {
if (isOpen) {
dispatch(getTypistGroupAsync({ typistGroupId }));
}
}, [dispatch, isOpen, typistGroupId]);
// ポップアップを閉じる処理
const closePopup = useCallback(() => {
setIsPushEditButton(false);
onClose(false);
dispatch(cleanupTypistGroup());
}, [dispatch, onClose]);
// グループ更新を実行
const editTypistGroup = useCallback(async () => {
setIsPushEditButton(true);
// 入力チェック
if (hasErrorEmptyGroupName || hasErrorSelectedTypistsEmpty) {
if (hasErrorSelectedTypistsEmpty) {
dispatch(
openSnackbar({
level: "error",
message: t(
getTranslationID(
"typistGroupSetting.message.selectedTypistEmptyError"
)
),
})
);
}
return;
}
// グループ追加APIを実行
const { meta } = await dispatch(updateTypistGroupAsync());
setIsPushEditButton(false);
if (meta.requestStatus === "fulfilled") {
closePopup();
dispatch(listTypistGroupsAsync());
dispatch(cleanupTypistGroup());
}
}, [
t,
closePopup,
dispatch,
hasErrorEmptyGroupName,
hasErrorSelectedTypistsEmpty,
]);
return (
<div className={`${styles.modal} ${isOpen ? styles.isShow : ""}`}>
<div className={styles.modalBox}>
<p className={styles.modalTitle}>
{t(getTranslationID("typistGroupSetting.label.editTypistGroup"))}
<button type="button" onClick={closePopup}>
<img src={close} className={styles.modalTitleIcon} alt="close" />
</button>
</p>
<form className={styles.form}>
<dl className={`${styles.formList} ${styles.hasbg}`}>
<dt className={styles.formTitle} />
<dt>{t(getTranslationID("typistGroupSetting.label.groupName"))}</dt>
<dd>
<input
type="text"
size={40}
maxLength={50}
className={styles.formInput}
value={groupName}
onChange={(e) =>
dispatch(changeGroupName({ groupName: e.target.value }))
}
/>
{isPushEditButton && hasErrorEmptyGroupName && (
<span className={styles.formError}>
{t(getTranslationID("common.message.inputEmptyError"))}
</span>
)}
</dd>
<dt className={styles.formTitle}>
{t(getTranslationID("typistGroupSetting.label.transcriptionist"))}
</dt>
<dd className={`${styles.formChange} ${styles.last}`}>
<ul className={styles.chooseMember}>
<li className={styles.changeTitle}>
{t(getTranslationID("typistGroupSetting.label.selected"))}
</li>
{selectedTypists.map((typist: Typist) => (
<li key={typist.id}>
<input
type="checkbox"
className={styles.formCheck}
value={typist.name}
id={`${typist.id}`}
checked
onClick={() => dispatch(removeSelectedTypist({ typist }))}
/>
<label
htmlFor={`${typist.id}`}
title={t(
getTranslationID("typistGroupSetting.label.remove")
)}
>
{typist.name}
</label>
</li>
))}
</ul>
<p />
<ul className={styles.holdMember}>
<li className={styles.changeTitle}>
{t(getTranslationID("typistGroupSetting.label.pool"))}
</li>
{poolTypists.map((typist: Typist) => (
<li key={typist.id}>
<input
type="checkbox"
className={styles.formCheck}
value={typist.name}
id={`${typist.id}`}
onClick={() => dispatch(addSelectedTypist({ typist }))}
/>
<label
htmlFor={`${typist.id}`}
title={t(
getTranslationID("typistGroupSetting.label.add")
)}
>
{typist.name}
</label>
</li>
))}
</ul>
</dd>
<dd className={`${styles.full} ${styles.alignCenter}`}>
<input
type="button"
name="submit"
value={t(getTranslationID("typistGroupSetting.label.save"))}
className={`${styles.formSubmit} ${styles.marginBtm1} ${styles.isActive}`}
onClick={editTypistGroup}
/>
</dd>
</dl>
</form>
</div>
</div>
);
};

View File

@ -16,6 +16,7 @@ import { AppDispatch } from "app/store";
import { useTranslation } from "react-i18next";
import { getTranslationID } from "translation";
import { AddTypistGroupPopup } from "./addTypistGroupPopup";
import { EditTypistGroupPopup } from "./editTypistGroupPopup";
const TypistGroupSettingPage: React.FC = (): JSX.Element => {
const dispatch: AppDispatch = useDispatch();
@ -25,11 +26,21 @@ const TypistGroupSettingPage: React.FC = (): JSX.Element => {
const typistGroup = useSelector(selectTypistGroups);
const [isAddPopupOpen, setIsAddPopupOpen] = useState(false);
const [isEditPopupOpen, setIsEditPopupOpen] = useState(false);
const [editTypistGroupId, setEditTypistGroupId] = useState<number>(NaN);
const onAddPopupOpen = useCallback(() => {
// typist一覧を取得
setIsAddPopupOpen(true);
}, [setIsAddPopupOpen]);
const onEditPopupOpen = useCallback(
(typistGroupId: number) => {
setEditTypistGroupId(typistGroupId);
setIsEditPopupOpen(true);
},
[setIsEditPopupOpen]
);
useEffect(() => {
dispatch(listTypistGroupsAsync());
}, [dispatch]);
@ -42,6 +53,13 @@ const TypistGroupSettingPage: React.FC = (): JSX.Element => {
}}
isOpen={isAddPopupOpen}
/>
<EditTypistGroupPopup
onClose={() => {
setIsEditPopupOpen(false);
}}
isOpen={isEditPopupOpen}
typistGroupId={editTypistGroupId}
/>
<div className={styles.wrap}>
<Header userName="XXXXXX" />
<UpdateTokenTimer />
@ -102,8 +120,12 @@ const TypistGroupSettingPage: React.FC = (): JSX.Element => {
className={`${styles.menuAction} ${styles.inTable}`}
>
<li>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
<a
className={`${styles.menuLink} ${styles.isActive}`}
onClick={() => {
onEditPopupOpen(group.id);
}}
>
{t(
getTranslationID(

View File

@ -236,7 +236,7 @@
"deleteDictation": "(de)Delete Dictation",
"selectedTranscriptionist": "(de)Selected",
"poolTranscriptionist": "(de)Pool",
"saveChanges": "(de)Save changes"
"saveChanges": "(de)Save"
}
},
"cardLicenseIssuePopupPage": {
@ -372,11 +372,13 @@
"selected": "(de)Selected",
"pool": "(de)Pool",
"add": "(de)Add",
"remove": "(de)Remove"
"remove": "(de)Remove",
"editTypistGroup": "(de)Edit Transcriptionist Group",
"save": "(de)Save"
},
"message": {
"selectedTypistEmptyError": "(de)TranscriptionistGroupには最低1名のメンバーが必要です。",
"groupAddFailedError": "(de)TypistGroupの追加に失敗しました。画面を更新し、再度実行してください"
"selectedTypistEmptyError": "(de)TranscriptionistがいないTranscriptionistGroupは保存できません。1名以上をTranscriptionistとして選択してください。",
"groupSaveFailedError": "(de)TypistGroupの保存に失敗しました。画面を更新し、再度実行してください"
}
}
}

View File

@ -236,7 +236,7 @@
"deleteDictation": "Delete Dictation",
"selectedTranscriptionist": "Selected",
"poolTranscriptionist": "Pool",
"saveChanges": "Save changes"
"saveChanges": "Save"
}
},
"cardLicenseIssuePopupPage": {
@ -372,11 +372,13 @@
"selected": "Selected",
"pool": "Pool",
"add": "Add",
"remove": "Remove"
"remove": "Remove",
"editTypistGroup": "Edit Transcriptionist Group",
"save": "Save"
},
"message": {
"selectedTypistEmptyError": "TranscriptionistGroupには最低1名のメンバーが必要です。",
"groupAddFailedError": "TypistGroupの追加に失敗しました。画面を更新し、再度実行してください"
"selectedTypistEmptyError": "TranscriptionistがいないTranscriptionistGroupは保存できません。1名以上をTranscriptionistとして選択してください。",
"groupSaveFailedError": "TypistGroupの保存に失敗しました。画面を更新し、再度実行してください"
}
}
}

View File

@ -236,7 +236,7 @@
"deleteDictation": "(es)Delete Dictation",
"selectedTranscriptionist": "(es)Selected",
"poolTranscriptionist": "(es)Pool",
"saveChanges": "(es)Save changes"
"saveChanges": "(es)Save"
}
},
"cardLicenseIssuePopupPage": {
@ -372,11 +372,13 @@
"selected": "(es)Selected",
"pool": "(es)Pool",
"add": "(es)Add",
"remove": "(es)Remove"
"remove": "(es)Remove",
"editTypistGroup": "(es)Edit Transcriptionist Group",
"save": "(es)Save"
},
"message": {
"selectedTypistEmptyError": "(es)TranscriptionistGroupには最低1名のメンバーが必要です。",
"groupAddFailedError": "(es)TypistGroupの追加に失敗しました。画面を更新し、再度実行してください"
"selectedTypistEmptyError": "(es)TranscriptionistがいないTranscriptionistGroupは保存できません。1名以上をTranscriptionistとして選択してください。",
"groupSaveFailedError": "(es)TypistGroupの保存に失敗しました。画面を更新し、再度実行してください"
}
}
}

View File

@ -236,7 +236,7 @@
"deleteDictation": "(fr)Delete Dictation",
"selectedTranscriptionist": "(fr)Selected",
"poolTranscriptionist": "(fr)Pool",
"saveChanges": "(fr)Save changes"
"saveChanges": "(fr)Save"
}
},
"cardLicenseIssuePopupPage": {
@ -372,11 +372,13 @@
"selected": "(fr)Selected",
"pool": "(fr)Pool",
"add": "(fr)Add",
"remove": "(fr)Remove"
"remove": "(fr)Remove",
"editTypistGroup": "(fr)Edit Transcriptionist Group",
"save": "(fr)Save"
},
"message": {
"selectedTypistEmptyError": "(fr)TranscriptionistGroupには最低1名のメンバーが必要です。",
"groupAddFailedError": "(fr)TypistGroupの追加に失敗しました。画面を更新し、再度実行してください"
"selectedTypistEmptyError": "(fr)TranscriptionistがいないTranscriptionistGroupは保存できません。1名以上をTranscriptionistとして選択してください。",
"groupSaveFailedError": "(fr)TypistGroupの保存に失敗しました。画面を更新し、再度実行してください"
}
}
}