From 4be13e002da4b204dbdc528139e5538c6c5100fb Mon Sep 17 00:00:00 2001 From: "saito.k" Date: Mon, 5 Feb 2024 00:39:53 +0000 Subject: [PATCH] =?UTF-8?q?Merged=20PR=20710:=20=E7=94=BB=E9=9D=A2?= =?UTF-8?q?=E5=AE=9F=E8=A3=85=EF=BC=88=E5=89=8A=E9=99=A4=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3488: 画面実装(削除操作)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3488) - ユーザー削除の画面実装 - 確認ダイアログ - 削除API呼び出し - エラーハンドリング - 成功時のメッセージ - 成功時のユーザー一覧更新 ## レビューポイント - 特にレビューしてほしい箇所 - 軽微なものや自明なものは記載不要 - 修正範囲が大きい場合などに記載 - 全体的にや仕様を満たしているか等は本当に必要な時のみ記載 ## UIの変更 - Before/Afterのスクショなど - スクショ置き場 ## 動作確認状況 - ローカルで確認 ## 補足 - API呼び出しのエラーハンドリング部分はエラーコードが採番されたら追従します --- dictation_client/src/api/api.ts | 75 ++++++++++++ dictation_client/src/common/errors/code.ts | 8 ++ .../src/features/dictation/operations.ts | 2 +- .../src/features/user/operations.ts | 115 ++++++++++++++++++ .../src/features/user/userSlice.ts | 10 ++ .../src/pages/UserListPage/index.tsx | 44 +++++-- dictation_server/src/api/odms/openapi.json | 2 +- .../src/features/users/users.controller.ts | 2 +- 8 files changed, 244 insertions(+), 14 deletions(-) diff --git a/dictation_client/src/api/api.ts b/dictation_client/src/api/api.ts index 70d8d8f..6ed3907 100644 --- a/dictation_client/src/api/api.ts +++ b/dictation_client/src/api/api.ts @@ -6982,6 +6982,46 @@ export const UsersApiAxiosParamCreator = function (configuration?: Configuration options: localVarRequestOptions, }; }, + /** + * ユーザーを削除します + * @summary + * @param {PostDeleteUserRequest} postDeleteUserRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteUser: async (postDeleteUserRequest: PostDeleteUserRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'postDeleteUserRequest' is not null or undefined + assertParamExists('deleteUser', 'postDeleteUserRequest', postDeleteUserRequest) + const localVarPath = `/users/delete`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(postDeleteUserRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * ログインしているユーザーの情報を取得します * @summary @@ -7376,6 +7416,19 @@ export const UsersApiFp = function(configuration?: Configuration) { const operationBasePath = operationServerMap['UsersApi.deallocateLicense']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, + /** + * ユーザーを削除します + * @summary + * @param {PostDeleteUserRequest} postDeleteUserRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async deleteUser(postDeleteUserRequest: PostDeleteUserRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.deleteUser(postDeleteUserRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['UsersApi.deleteUser']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, /** * ログインしているユーザーの情報を取得します * @summary @@ -7539,6 +7592,16 @@ export const UsersApiFactory = function (configuration?: Configuration, basePath deallocateLicense(deallocateLicenseRequest: DeallocateLicenseRequest, options?: any): AxiosPromise { return localVarFp.deallocateLicense(deallocateLicenseRequest, options).then((request) => request(axios, basePath)); }, + /** + * ユーザーを削除します + * @summary + * @param {PostDeleteUserRequest} postDeleteUserRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteUser(postDeleteUserRequest: PostDeleteUserRequest, options?: any): AxiosPromise { + return localVarFp.deleteUser(postDeleteUserRequest, options).then((request) => request(axios, basePath)); + }, /** * ログインしているユーザーの情報を取得します * @summary @@ -7683,6 +7746,18 @@ export class UsersApi extends BaseAPI { return UsersApiFp(this.configuration).deallocateLicense(deallocateLicenseRequest, options).then((request) => request(this.axios, this.basePath)); } + /** + * ユーザーを削除します + * @summary + * @param {PostDeleteUserRequest} postDeleteUserRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public deleteUser(postDeleteUserRequest: PostDeleteUserRequest, options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).deleteUser(postDeleteUserRequest, options).then((request) => request(this.axios, this.basePath)); + } + /** * ログインしているユーザーの情報を取得します * @summary diff --git a/dictation_client/src/common/errors/code.ts b/dictation_client/src/common/errors/code.ts index 5d0ffd8..336b41d 100644 --- a/dictation_client/src/common/errors/code.ts +++ b/dictation_client/src/common/errors/code.ts @@ -60,6 +60,14 @@ export const errorCodes = [ "E011004", // ワークタイプ使用中エラー "E013001", // ワークフローのAuthorIDとWorktypeIDのペア重複エラー "E013002", // ワークフロー不在エラー + "E014001", // ユーザー削除エラー(削除しようとしたユーザーがすでに削除済みだった) + "E014002", // ユーザー削除エラー(削除しようとしたユーザーが管理者だった) + "E014003", // ユーザー削除エラー(削除しようとしたAuthorのAuthorIDがWorkflowに指定されていた) + "E014004", // ユーザー削除エラー(削除しようとしたTypistがWorkflowのTypist候補として指定されていた) + "E014005", // ユーザー削除エラー(削除しようとしたTypistがUserGroupに所属していた) + "E014006", // ユーザー削除エラー(削除しようとしたユーザが所有者の未完了のタスクが残っている) + "E014007", // ユーザー削除エラー(削除しようとしたユーザーが有効なライセンスを持っていた) + "E014009", // ユーザー削除エラー(削除しようとしたTypistが未完了のタスクのルーティングに設定されている) "E015001", // タイピストグループ削除済みエラー "E015002", // タイピストグループがワークフローに紐づいているエラー "E015003", // タイピストグループがルーティングされているエラー diff --git a/dictation_client/src/features/dictation/operations.ts b/dictation_client/src/features/dictation/operations.ts index 51579e7..4a80927 100644 --- a/dictation_client/src/features/dictation/operations.ts +++ b/dictation_client/src/features/dictation/operations.ts @@ -610,7 +610,7 @@ export const deleteTaskAsync = createAsyncThunk< // e ⇒ errorObjectに変換" const error = createErrorObject(e); - let message = getTranslationID("dictationPage.message.backupFailedError"); + let message = getTranslationID("common.message.internalServerError"); if (error.statusCode === 400) { if (error.code === "E010603") { diff --git a/dictation_client/src/features/user/operations.ts b/dictation_client/src/features/user/operations.ts index b7ca2b0..f9d2578 100644 --- a/dictation_client/src/features/user/operations.ts +++ b/dictation_client/src/features/user/operations.ts @@ -383,3 +383,118 @@ export const deallocateLicenseAsync = createAsyncThunk< return thunkApi.rejectWithValue({ error }); } }); + +export const deleteUserAsync = createAsyncThunk< + // 正常時の戻り値の型 + { + /* Empty Object */ + }, + // 引数 + { + userId: number; + }, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("users/deleteUserAsync", async (args, thunkApi) => { + const { userId } = args; + + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration } = state.auth; + const accessToken = getAccessToken(state.auth); + const config = new Configuration(configuration); + const usersApi = new UsersApi(config); + + try { + await usersApi.deleteUser( + { + userId, + }, + { + 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"); + + if (error.statusCode === 400) { + if (error.code === "E014001") { + // ユーザーが削除済みのため成功 + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); + return {}; + } + } + + // ユーザーに有効なライセンスが割り当たっているため削除不可 + if (error.code === "E014007") { + errorMessage = getTranslationID( + "userListPage.message.UserDeletionLicenseActiveError" + ); + } + // 管理者ユーザーため削除不可 + if (error.code === "E014002") { + errorMessage = getTranslationID( + "userListPage.message.AdminUserDeletionError" + ); + } + // タイピストユーザーで担当タスクがあるため削除不可 + if (error.code === "E014009") { + errorMessage = getTranslationID( + "userListPage.message.TypistUserDeletionTranscriptionTaskError" + ); + } + // タイピストユーザーでルーティングルールに設定されているため削除不可 + if (error.code === "E014004") { + errorMessage = getTranslationID( + "userListPage.message.TypistDeletionRoutingRuleError" + ); + } + // タイピストユーザーでTranscriptionistGroupに所属しているため削除不可 + if (error.code === "E014005") { + errorMessage = getTranslationID( + "userListPage.message.TypistUserDeletionTranscriptionistGroupError" + ); + } + // Authorユーザーで同一AuthorIDのタスクがあるため削除不可 + if (error.code === "E014006") { + errorMessage = getTranslationID( + "userListPage.message.AuthorUserDeletionTranscriptionTaskError" + ); + } + // Authorユーザーで同一AuthorIDがルーティングルールに設定されているため削除不可 + if (error.code === "E014003") { + errorMessage = getTranslationID( + "userListPage.message.AuthorDeletionRoutingRuleError" + ); + } + + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: errorMessage, + }) + ); + + return thunkApi.rejectWithValue({ error }); + } +}); diff --git a/dictation_client/src/features/user/userSlice.ts b/dictation_client/src/features/user/userSlice.ts index 881dba9..42baced 100644 --- a/dictation_client/src/features/user/userSlice.ts +++ b/dictation_client/src/features/user/userSlice.ts @@ -7,6 +7,7 @@ import { updateUserAsync, getAllocatableLicensesAsync, deallocateLicenseAsync, + deleteUserAsync, } from "./operations"; import { RoleType, UserView } from "./types"; @@ -290,6 +291,15 @@ export const userSlice = createSlice({ builder.addCase(deallocateLicenseAsync.rejected, (state) => { state.apps.isLoading = false; }); + builder.addCase(deleteUserAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(deleteUserAsync.fulfilled, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(deleteUserAsync.rejected, (state) => { + state.apps.isLoading = false; + }); }, }); diff --git a/dictation_client/src/pages/UserListPage/index.tsx b/dictation_client/src/pages/UserListPage/index.tsx index b6b0072..4dc2cee 100644 --- a/dictation_client/src/pages/UserListPage/index.tsx +++ b/dictation_client/src/pages/UserListPage/index.tsx @@ -10,6 +10,7 @@ import { selectUserViews, selectIsLoading, deallocateLicenseAsync, + deleteUserAsync, } from "features/user"; import { useTranslation } from "react-i18next"; import { getTranslationID } from "translation"; @@ -84,6 +85,24 @@ const UserListPage: React.FC = (): JSX.Element => { [dispatch, t] ); + const onDeleteUser = useCallback( + async (userId: number) => { + // ダイアログ確認 + if ( + /* eslint-disable-next-line no-alert */ + !window.confirm(t(getTranslationID("common.message.dialogConfirm"))) + ) { + return; + } + + const { meta } = await dispatch(deleteUserAsync({ userId })); + if (meta.requestStatus === "fulfilled") { + dispatch(listUsersAsync()); + } + }, + [dispatch, t] + ); + useEffect(() => { // ユーザ一覧取得処理を呼び出す dispatch(listUsersAsync()); @@ -244,17 +263,20 @@ const UserListPage: React.FC = (): JSX.Element => { )} - {/* ユーザー削除 CCB後回し分なので今は非表示 -
  • - - {t( - getTranslationID( - "userListPage.label.deleteUser" - ) - )} - -
  • - */} +
  • + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} + { + onDeleteUser(user.id); + }} + > + {t( + getTranslationID( + "userListPage.label.deleteUser" + ) + )} + +
  • {user.name} diff --git a/dictation_server/src/api/odms/openapi.json b/dictation_server/src/api/odms/openapi.json index 653ce1d..0c49633 100644 --- a/dictation_server/src/api/odms/openapi.json +++ b/dictation_server/src/api/odms/openapi.json @@ -2160,7 +2160,7 @@ }, "/users/delete": { "post": { - "operationId": "updeateUser", + "operationId": "deleteUser", "summary": "", "description": "ユーザーを削除します", "parameters": [], diff --git a/dictation_server/src/features/users/users.controller.ts b/dictation_server/src/features/users/users.controller.ts index 6a1e74e..6b19b15 100644 --- a/dictation_server/src/features/users/users.controller.ts +++ b/dictation_server/src/features/users/users.controller.ts @@ -940,7 +940,7 @@ export class UsersController { type: ErrorResponse, }) @ApiOperation({ - operationId: 'updeateUser', + operationId: 'deleteUser', description: 'ユーザーを削除します', }) @ApiBearerAuth()