From 476c810cc3ef87b2eaaca52ba4ddc172b2cb542c Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Wed, 26 Apr 2023 00:04:48 +0000 Subject: [PATCH] =?UTF-8?q?Merged=20PR=2079:=20=E7=94=BB=E9=9D=A2=E5=AE=9F?= =?UTF-8?q?=E8=A3=85=EF=BC=88=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0=E3=83=80=E3=82=A4=E3=82=A2=E3=83=AD=E3=82=B0=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task1596: 画面実装(ユーザー追加ダイアログ)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/1596) - ユーザ一覧画面にユーザ追加ポップアップを追加しました。 ## レビューポイント - 入力エラーチェックは適切か - Roleを切り替えた際の内容は適切か - タイピストの選択はデザインのみです。 ## UIの変更 - [Task1596](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/Task1596?csf=1&web=1&e=iiTOxd) ## 動作確認状況 - ローカルで確認 --- dictation_client/src/common/errors/code.ts | 1 + .../src/features/user/constants.ts | 7 + dictation_client/src/features/user/index.ts | 1 + .../src/features/user/operations.ts | 80 +++- .../src/features/user/selectors.ts | 52 +++ dictation_client/src/features/user/state.ts | 9 + .../src/features/user/userSlice.ts | 78 +++- .../src/pages/UserListPage/index.tsx | 389 +++++++++--------- .../src/pages/UserListPage/popup.tsx | 341 +++++++++++++++ dictation_client/src/translation/de.json | 16 +- dictation_client/src/translation/en.json | 16 +- dictation_client/src/translation/es.json | 16 +- dictation_client/src/translation/fr.json | 16 +- 13 files changed, 825 insertions(+), 197 deletions(-) create mode 100644 dictation_client/src/features/user/constants.ts create mode 100644 dictation_client/src/pages/UserListPage/popup.tsx diff --git a/dictation_client/src/common/errors/code.ts b/dictation_client/src/common/errors/code.ts index 1e05efe..305eb60 100644 --- a/dictation_client/src/common/errors/code.ts +++ b/dictation_client/src/common/errors/code.ts @@ -23,4 +23,5 @@ export const errorCodes = [ "E010201", // 未認証ユーザエラー "E010202", // 認証済ユーザエラー "E010301", // メールアドレス登録済みエラー + "E010302", // authorId重複エラー ] as const; diff --git a/dictation_client/src/features/user/constants.ts b/dictation_client/src/features/user/constants.ts new file mode 100644 index 0000000..320e99e --- /dev/null +++ b/dictation_client/src/features/user/constants.ts @@ -0,0 +1,7 @@ +export const ROLE = { + AUTHOR: "Author", + TYPIST: "Transcriptionist", + NONE: "None", +} as const; + +export type RoleType = typeof ROLE[keyof typeof ROLE]; diff --git a/dictation_client/src/features/user/index.ts b/dictation_client/src/features/user/index.ts index abfe789..3419fc2 100644 --- a/dictation_client/src/features/user/index.ts +++ b/dictation_client/src/features/user/index.ts @@ -2,3 +2,4 @@ export * from "./state"; export * from "./operations"; export * from "./selectors"; export * from "./userSlice"; +export * from "./constants"; diff --git a/dictation_client/src/features/user/operations.ts b/dictation_client/src/features/user/operations.ts index 3637c97..6ef9645 100644 --- a/dictation_client/src/features/user/operations.ts +++ b/dictation_client/src/features/user/operations.ts @@ -1,6 +1,8 @@ import { createAsyncThunk } from "@reduxjs/toolkit"; import type { RootState } from "app/store"; -import { UsersApi, GetUsersResponse } from "../../api/api"; +import { getTranslationID } from "translation"; +import { openSnackbar } from "features/ui/uiSlice"; +import { SignupRequest, UsersApi, GetUsersResponse } from "../../api/api"; import { Configuration } from "../../api/configuration"; import { ErrorObject, createErrorObject } from "../../common/errors"; @@ -36,3 +38,79 @@ export const listUsersAsync = createAsyncThunk< return thunkApi.rejectWithValue({ error }); } }); + +export const addUserAsync = createAsyncThunk< + { + /* Empty Object */ + }, + SignupRequest, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("users/addUserAsync", async (args, thunkApi) => { + const { + name, + email, + role, + authorId, + typistGroupId, + autoRenew, + licenseAlert, + notification, + } = args; + + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration } = state.auth; + const config = new Configuration(configuration); + const usersApi = new UsersApi(config); + + try { + await usersApi.signup({ + name, + email, + role, + authorId, + typistGroupId, + autoRenew, + licenseAlert, + notification, + }); + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("userListPage.message.addUserSuccess"), + }) + ); + return {}; + } catch (e) { + // e ⇒ errorObjectに変換"z + const error = createErrorObject(e); + + let errorMessage = getTranslationID("common.message.internalServerError"); + + if (error.code === "E010301") { + errorMessage = getTranslationID( + "signupConfirmPage.message.emailConflictError" + ); + } + if (error.code === "E010302") { + errorMessage = getTranslationID( + "userListPage.message.authorIdConflictError" + ); + } + + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: errorMessage, + }) + ); + + return thunkApi.rejectWithValue({ error }); + } +}); diff --git a/dictation_client/src/features/user/selectors.ts b/dictation_client/src/features/user/selectors.ts index efadaf5..ad726e4 100644 --- a/dictation_client/src/features/user/selectors.ts +++ b/dictation_client/src/features/user/selectors.ts @@ -1,3 +1,55 @@ import { RootState } from "app/store"; +import { ROLE } from "./constants"; +export const selectInputValidationErrors = (state: RootState) => { + const { name, email, role, authorId } = state.user.apps.addUser; + + // 必須項目のチェック + const hasErrorEmptyName = name === ""; + const hasErrorEmptyEmail = email === ""; + const hasErrorEmptyAuthorId = role === ROLE.AUTHOR && authorId === ""; + + const hasErrorIncorrectAuthorId = checkErrorIncorrectAuthorId( + authorId ?? undefined, + role + ); + + const hasErrorIncorrectEmail = email.match(/^[^@]+@[^@]+$/)?.length !== 1; + + return { + hasErrorEmptyName, + hasErrorEmptyEmail, + hasErrorEmptyAuthorId, + hasErrorIncorrectEmail, + hasErrorIncorrectAuthorId, + }; +}; +export const checkErrorIncorrectAuthorId = ( + authorId: string | undefined, + role: string +): boolean => { + if (!authorId || role !== ROLE.AUTHOR) { + return false; + } + + // 半角英数字と_の組み合わせで16文字まで + const charaTypePattern = /^[A-Z0-9_]{1,16}$/; + const charaType = new RegExp(charaTypePattern).test(authorId); + + return !charaType; +}; + +export const selectName = (state: RootState) => state.user.apps.addUser.name; +export const selectEmail = (state: RootState) => state.user.apps.addUser.email; +export const selectRole = (state: RootState) => state.user.apps.addUser.role; +export const selectAuthorId = (state: RootState) => + state.user.apps.addUser.authorId; +export const selectTypistGroupId = (state: RootState) => + state.user.apps.addUser.typistGroupId; +export const selectAutoRenew = (state: RootState) => + state.user.apps.addUser.autoRenew; +export const selectLicenseAlert = (state: RootState) => + state.user.apps.addUser.licenseAlert; +export const selectNtotification = (state: RootState) => + state.user.apps.addUser.notification; export const selectDomain = (state: RootState) => state.user.domain; diff --git a/dictation_client/src/features/user/state.ts b/dictation_client/src/features/user/state.ts index 4e31f2f..760c0e5 100644 --- a/dictation_client/src/features/user/state.ts +++ b/dictation_client/src/features/user/state.ts @@ -2,8 +2,17 @@ import { User } from "../../api/api"; export interface UsersState { domain: Domain; + apps: Apps; } export interface Domain { users: User[]; } + +export interface Apps { + addUser: AddUser; +} + +export interface AddUser extends User { + typistGroupId?: number | undefined; +} diff --git a/dictation_client/src/features/user/userSlice.ts b/dictation_client/src/features/user/userSlice.ts index 49124c6..a178060 100644 --- a/dictation_client/src/features/user/userSlice.ts +++ b/dictation_client/src/features/user/userSlice.ts @@ -1,15 +1,77 @@ -import { createSlice } from "@reduxjs/toolkit"; +import { PayloadAction, createSlice } from "@reduxjs/toolkit"; import { UsersState } from "./state"; import { listUsersAsync } from "./operations"; +import { ROLE, RoleType } from "./constants"; const initialState: UsersState = { domain: { users: [] }, + apps: { + addUser: { + name: "", + role: ROLE.NONE, + authorId: "", + typistGroupName: "", + email: "", + emailVerified: true, + autoRenew: true, + licenseAlert: true, + notification: true, + }, + }, }; export const userSlice = createSlice({ name: "user", initialState, - reducers: {}, + reducers: { + changeName: (state, action: PayloadAction<{ name: string }>) => { + const { name } = action.payload; + state.apps.addUser.name = name; + }, + changeEmail: (state, action: PayloadAction<{ email: string }>) => { + const { email } = action.payload; + state.apps.addUser.email = email; + }, + changeRole: (state, action: PayloadAction<{ role: RoleType }>) => { + const { role } = action.payload; + state.apps.addUser.role = role; + }, + changeAuthorId: ( + state, + action: PayloadAction<{ authorId: string | undefined }> + ) => { + const { authorId } = action.payload; + state.apps.addUser.authorId = authorId ?? null; + }, + changeTypistGroupId: ( + state, + action: PayloadAction<{ typistGroupId: number | undefined }> + ) => { + const { typistGroupId } = action.payload; + state.apps.addUser.typistGroupId = typistGroupId; + }, + changeAutoRenew: (state, action: PayloadAction<{ autoRenew: boolean }>) => { + const { autoRenew } = action.payload; + state.apps.addUser.autoRenew = autoRenew; + }, + changeLicenseAlert: ( + state, + action: PayloadAction<{ licenseAlert: boolean }> + ) => { + const { licenseAlert } = action.payload; + state.apps.addUser.licenseAlert = licenseAlert; + }, + changeNotification: ( + state, + action: PayloadAction<{ notification: boolean }> + ) => { + const { notification } = action.payload; + state.apps.addUser.notification = notification; + }, + cleanupAddUser: (state) => { + state.apps.addUser = initialState.apps.addUser; + }, + }, extraReducers: (builder) => { builder.addCase(listUsersAsync.fulfilled, (state, action) => { state.domain.users = action.payload.users; @@ -17,4 +79,16 @@ export const userSlice = createSlice({ }, }); +export const { + changeName, + changeEmail, + changeRole, + changeAuthorId, + changeTypistGroupId, + changeAutoRenew, + changeLicenseAlert, + changeNotification, + cleanupAddUser, +} = userSlice.actions; + export default userSlice.reducer; diff --git a/dictation_client/src/pages/UserListPage/index.tsx b/dictation_client/src/pages/UserListPage/index.tsx index a7d055c..0700f28 100644 --- a/dictation_client/src/pages/UserListPage/index.tsx +++ b/dictation_client/src/pages/UserListPage/index.tsx @@ -1,5 +1,5 @@ import { AppDispatch } from "app/store"; -import React, { useEffect } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import Header from "components/header"; import Footer from "components/footer"; import styles from "styles/app.module.scss"; @@ -13,6 +13,7 @@ import deleteImg from "../../assets/images/delete.svg"; import badgeImg from "../../assets/images/badge.svg"; import checkFill from "../../assets/images/check_fill.svg"; import circle from "../../assets/images/circle.svg"; +import { UserAddPopup } from "./popup"; // eslintの検査エラー無視設定 /* eslint-disable jsx-a11y/anchor-is-valid */ @@ -21,6 +22,12 @@ const UserListPage: React.FC = (): JSX.Element => { const dispatch: AppDispatch = useDispatch(); const [t] = useTranslation(); + const [isPopupOpen, setIsPopupOpen] = useState(false); + + const onOpen = useCallback(() => { + setIsPopupOpen(true); + }, [setIsPopupOpen]); + useEffect(() => { // ユーザ一覧取得処理を呼び出す dispatch(listUsersAsync()); @@ -29,192 +36,202 @@ const UserListPage: React.FC = (): JSX.Element => { const domain = useSelector(selectDomain); return ( -
- {/* XXX デザイン上はヘッダに「Account」「User」「License」等の項目が設定されているが、そのままでは使用できない。PBI1128ではユーザ一覧画面は作りこまないので、ユーザ一覧のPBIでヘッダをデザイン通りにする必要がある */} -
-
-
-
-

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

-

-

-
-
- - - - - - - - - - - - - - - - - {/* XXX 「固定」の項目と、isSelected、isAlertの対応が必要 */} - {domain.users.map((user) => ( - - - - - - - - - - - - - - ))} - -
- - {t(getTranslationID("userListPage.label.name"))} - - - - {t(getTranslationID("userListPage.label.role"))} - - - - {t(getTranslationID("userListPage.label.authorID"))} - - - - {t(getTranslationID("userListPage.label.typistGroup"))} - - - - {t(getTranslationID("userListPage.label.email"))} - - - - {t(getTranslationID("userListPage.label.status"))} - - - - {t(getTranslationID("userListPage.label.expiration"))} - - - - {t(getTranslationID("userListPage.label.remaining"))} - - - {t(getTranslationID("userListPage.label.autoRenew"))} - - {t(getTranslationID("userListPage.label.licenseAlert"))} - - {t(getTranslationID("userListPage.label.notification"))} -
{user.name}{user.role}{user.authorId}{user.typistGroupName}{user.email}固定:Uploaded固定:2023/8/3固定:114 - {user.autoRenew ? ( - - ) : ( - - )} - - {user.licenseAlert ? ( - - ) : ( - - )} - - {user.notification ? ( - - ) : ( - - )} -
-
- -
+ <> + { + setIsPopupOpen(false); + }} + /> +
+ {/* XXX デザイン上はヘッダに「Account」「User」「License」等の項目が設定されているが、そのままでは使用できない。PBI1128ではユーザ一覧画面は作りこまないので、ユーザ一覧のPBIでヘッダをデザイン通りにする必要がある */} +
+
+
+
+

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

+

-
-
-
-
-
+
+
+ + + + + + + + + + + + + + + + + {/* XXX 「固定」の項目と、isSelected、isAlertの対応が必要 */} + {domain.users.map((user) => ( + + + + + + + + + + + + + + ))} + +
+ + {t(getTranslationID("userListPage.label.name"))} + + + + {t(getTranslationID("userListPage.label.role"))} + + + + {t(getTranslationID("userListPage.label.authorID"))} + + + + {t( + getTranslationID("userListPage.label.typistGroup") + )} + + + + {t(getTranslationID("userListPage.label.email"))} + + + + {t(getTranslationID("userListPage.label.status"))} + + + + {t(getTranslationID("userListPage.label.expiration"))} + + + + {t(getTranslationID("userListPage.label.remaining"))} + + + {t(getTranslationID("userListPage.label.autoRenew"))} + + {t(getTranslationID("userListPage.label.licenseAlert"))} + + {t(getTranslationID("userListPage.label.notification"))} +
{user.name}{user.role}{user.authorId}{user.typistGroupName}{user.email}固定:Uploaded固定:2023/8/3固定:114 + {user.autoRenew ? ( + + ) : ( + + )} + + {user.licenseAlert ? ( + + ) : ( + + )} + + {user.notification ? ( + + ) : ( + + )} +
+
+ +
+
+
+ + +