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"))}
-
-
-
-
-
-
-
-
-
-
+ <>
+
{
+ setIsPopupOpen(false);
+ }}
+ />
+
+ {/* XXX デザイン上はヘッダに「Account」「User」「License」等の項目が設定されているが、そのままでは使用できない。PBI1128ではユーザ一覧画面は作りこまないので、ユーザ一覧のPBIでヘッダをデザイン通りにする必要がある */}
+
+
+
+
+
+ {t(getTranslationID("userListPage.label.title"))}
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
);
};
diff --git a/dictation_client/src/pages/UserListPage/popup.tsx b/dictation_client/src/pages/UserListPage/popup.tsx
new file mode 100644
index 0000000..45fd300
--- /dev/null
+++ b/dictation_client/src/pages/UserListPage/popup.tsx
@@ -0,0 +1,341 @@
+import { AppDispatch } from "app/store";
+import React, { useState, useCallback } from "react";
+import styles from "styles/app.module.scss";
+import { useDispatch, useSelector } from "react-redux";
+import { getTranslationID } from "translation";
+import { useTranslation } from "react-i18next";
+import {
+ changeEmail,
+ changeName,
+ changeRole,
+ changeAuthorId,
+ changeTypistGroupId,
+ changeAutoRenew,
+ changeLicenseAlert,
+ changeNotification,
+ cleanupAddUser,
+ selectName,
+ selectEmail,
+ selectRole,
+ selectAuthorId,
+ selectTypistGroupId,
+ selectInputValidationErrors,
+ selectAutoRenew,
+ selectLicenseAlert,
+ selectNtotification,
+ addUserAsync,
+ ROLE,
+} from "features/user";
+import close from "../../assets/images/close.svg";
+
+interface UserAddPopupProps {
+ isOpen: boolean;
+ onClose: () => void;
+}
+
+export const UserAddPopup: React.FC = (props) => {
+ const { isOpen, onClose } = props;
+ const dispatch: AppDispatch = useDispatch();
+ const { t } = useTranslation();
+
+ const closePopup = useCallback(() => {
+ setIsPushCreateButton(false);
+ dispatch(cleanupAddUser());
+ onClose();
+ }, [onClose, dispatch]);
+
+ const {
+ hasErrorEmptyName,
+ hasErrorEmptyEmail,
+ hasErrorEmptyAuthorId,
+ hasErrorIncorrectEmail,
+ hasErrorIncorrectAuthorId,
+ } = useSelector(selectInputValidationErrors);
+
+ const name = useSelector(selectName);
+ const email = useSelector(selectEmail);
+ const role = useSelector(selectRole);
+ const authorId = useSelector(selectAuthorId);
+ const typistGroupId = useSelector(selectTypistGroupId);
+ const autoRenew = useSelector(selectAutoRenew);
+ const licenseAlert = useSelector(selectLicenseAlert);
+ const notification = useSelector(selectNtotification);
+
+ const [isPushCreateButton, setIsPushCreateButton] = useState(false);
+
+ const onAddUser = useCallback(async () => {
+ setIsPushCreateButton(true);
+ if (
+ hasErrorEmptyName ||
+ hasErrorEmptyEmail ||
+ hasErrorEmptyAuthorId ||
+ hasErrorIncorrectEmail ||
+ hasErrorIncorrectAuthorId
+ ) {
+ return;
+ }
+ if (role !== ROLE.AUTHOR) {
+ changeAuthorId({ authorId: undefined });
+ }
+ if (role !== ROLE.TYPIST) {
+ changeTypistGroupId({ typistGroupId: undefined });
+ }
+
+ const { meta } = await dispatch(
+ addUserAsync({
+ name,
+ email,
+ role,
+ authorId: role === ROLE.AUTHOR ? authorId ?? undefined : undefined,
+ typistGroupId: role === ROLE.TYPIST ? typistGroupId : undefined,
+ autoRenew,
+ licenseAlert,
+ notification,
+ })
+ );
+ setIsPushCreateButton(false);
+
+ if (meta.requestStatus === "fulfilled") {
+ closePopup();
+ }
+ }, [
+ dispatch,
+ closePopup,
+ hasErrorEmptyName,
+ hasErrorEmptyEmail,
+ hasErrorEmptyAuthorId,
+ hasErrorIncorrectEmail,
+ hasErrorIncorrectAuthorId,
+ name,
+ email,
+ role,
+ authorId,
+ typistGroupId,
+ autoRenew,
+ licenseAlert,
+ notification,
+ ]);
+
+ return (
+
+
+
+ {t(getTranslationID("userListPage.label.addUser"))}
+
+
+
+
+
+ );
+};
diff --git a/dictation_client/src/translation/de.json b/dictation_client/src/translation/de.json
index bd246d5..6c27146 100644
--- a/dictation_client/src/translation/de.json
+++ b/dictation_client/src/translation/de.json
@@ -95,6 +95,11 @@
}
},
"userListPage": {
+ "message": {
+ "addUserSuccess": "(de)メールアドレス宛に認証用メールを送信しました。",
+ "authorIdConflictError": "(de)このAuthor IDは既に登録されています。他のAuthor IDで登録してください。",
+ "authorIdIncorrectError": "(de)Author IDの形式が不正です。Author IDは半角英数字(大文字)と\"_\"のみ入力可能です。"
+ },
"label": {
"title": "(de)User",
"addUser": "(de)Add User",
@@ -113,7 +118,14 @@
"licenseAlert": "(de)License alert",
"notification": "(de)Notification",
"users": "(de)users",
- "of": "(de)of"
+ "of": "(de)of",
+ "personal": "(de)Personal information",
+ "setting": "(de)Setting",
+ "selectGroup": "(de)Select group",
+ "addToGroup": "(de)Add to group (Optional)",
+ "author": "(de)Author",
+ "transcriptionist": "(de)Transcriptionist",
+ "none": "(de)None"
}
}
-}
\ No newline at end of file
+}
diff --git a/dictation_client/src/translation/en.json b/dictation_client/src/translation/en.json
index 8564e76..cf937a8 100644
--- a/dictation_client/src/translation/en.json
+++ b/dictation_client/src/translation/en.json
@@ -95,6 +95,11 @@
}
},
"userListPage": {
+ "message": {
+ "addUserSuccess": "メールアドレス宛に認証用メールを送信しました。",
+ "authorIdConflictError": "このAuthor IDは既に登録されています。他のAuthor IDで登録してください。",
+ "authorIdIncorrectError": "Author IDの形式が不正です。Author IDは半角英数字(大文字)と\"_\"のみ入力可能です。"
+ },
"label": {
"title": "User",
"addUser": "Add User",
@@ -113,7 +118,14 @@
"licenseAlert": "License alert",
"notification": "Notification",
"users": "users",
- "of": "of"
+ "of": "of",
+ "personal": "Personal information",
+ "setting": "Setting",
+ "selectGroup": "Select group",
+ "addToGroup": "Add to group (Optional)",
+ "author": "Author",
+ "transcriptionist": "Transcriptionist",
+ "none": "None"
}
}
-}
\ No newline at end of file
+}
diff --git a/dictation_client/src/translation/es.json b/dictation_client/src/translation/es.json
index bab4cec..ece7341 100644
--- a/dictation_client/src/translation/es.json
+++ b/dictation_client/src/translation/es.json
@@ -95,6 +95,11 @@
}
},
"userListPage": {
+ "message": {
+ "addUserSuccess": "(es)メールアドレス宛に認証用メールを送信しました。",
+ "authorIdConflictError": "(es)このAuthor IDは既に登録されています。他のAuthor IDで登録してください。",
+ "authorIdIncorrectError": "(es)Author IDの形式が不正です。Author IDは半角英数字(大文字)と\"_\"のみ入力可能です。"
+ },
"label": {
"title": "(es)User",
"addUser": "(es)Add User",
@@ -113,7 +118,14 @@
"licenseAlert": "(es)License alert",
"notification": "(es)Notification",
"users": "(es)users",
- "of": "(es)of"
+ "of": "(es)of",
+ "personal": "(es)Personal information",
+ "setting": "(es)Setting",
+ "selectGroup": "(es)Select group",
+ "addToGroup": "(es)Add to group (Optional)",
+ "author": "(es)Author",
+ "transcriptionist": "(es)Transcriptionist",
+ "none": "(es)None"
}
}
-}
\ No newline at end of file
+}
diff --git a/dictation_client/src/translation/fr.json b/dictation_client/src/translation/fr.json
index fdedb4b..5199abf 100644
--- a/dictation_client/src/translation/fr.json
+++ b/dictation_client/src/translation/fr.json
@@ -95,6 +95,11 @@
}
},
"userListPage": {
+ "message": {
+ "addUserSuccess": "(fr)メールアドレス宛に認証用メールを送信しました。",
+ "authorIdConflictError": "(fr)このAuthor IDは既に登録されています。他のAuthor IDで登録してください。",
+ "authorIdIncorrectError": "(fr)Author IDの形式が不正です。Author IDは半角英数字(大文字)と\"_\"のみ入力可能です。"
+ },
"label": {
"title": "(fr)User",
"addUser": "(fr)Add User",
@@ -113,7 +118,14 @@
"licenseAlert": "(fr)License alert",
"notification": "(fr)Notification",
"users": "(fr)users",
- "of": "(fr)of"
+ "of": "(fr)of",
+ "personal": "(fr)Personal information",
+ "setting": "(fr)Setting",
+ "selectGroup": "(fr)Select group",
+ "addToGroup": "(fr)Add to group (Optional)",
+ "author": "(fr)Author",
+ "transcriptionist": "(fr)Transcriptionist",
+ "none": "(fr)None"
}
}
-}
\ No newline at end of file
+}