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 ? ( + + ) : ( + + )} +
+
+ +
+
+
+ + +