Merged PR 79: 画面実装(ユーザー追加ダイアログ)
## 概要 [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) ## 動作確認状況 - ローカルで確認
This commit is contained in:
parent
16b7416de0
commit
476c810cc3
@ -23,4 +23,5 @@ export const errorCodes = [
|
||||
"E010201", // 未認証ユーザエラー
|
||||
"E010202", // 認証済ユーザエラー
|
||||
"E010301", // メールアドレス登録済みエラー
|
||||
"E010302", // authorId重複エラー
|
||||
] as const;
|
||||
|
||||
7
dictation_client/src/features/user/constants.ts
Normal file
7
dictation_client/src/features/user/constants.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export const ROLE = {
|
||||
AUTHOR: "Author",
|
||||
TYPIST: "Transcriptionist",
|
||||
NONE: "None",
|
||||
} as const;
|
||||
|
||||
export type RoleType = typeof ROLE[keyof typeof ROLE];
|
||||
@ -2,3 +2,4 @@ export * from "./state";
|
||||
export * from "./operations";
|
||||
export * from "./selectors";
|
||||
export * from "./userSlice";
|
||||
export * from "./constants";
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
});
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 (
|
||||
<div className={styles.wrap}>
|
||||
{/* XXX デザイン上はヘッダに「Account」「User」「License」等の項目が設定されているが、そのままでは使用できない。PBI1128ではユーザ一覧画面は作りこまないので、ユーザ一覧のPBIでヘッダをデザイン通りにする必要がある */}
|
||||
<Header />
|
||||
<main className={styles.main}>
|
||||
<div className="">
|
||||
<div className={styles.pageHeader}>
|
||||
<h1 className={styles.pageTitle}>
|
||||
{t(getTranslationID("userListPage.label.title"))}
|
||||
</h1>
|
||||
<p className="pageTxt" />
|
||||
</div>
|
||||
<section className={styles.user}>
|
||||
<div>
|
||||
<ul className={styles.menuAction}>
|
||||
<li>
|
||||
{/* XXX ユーザ追加のポップアップ対応が必要 */}
|
||||
<a
|
||||
href="adminUserAdd.html"
|
||||
className={`${styles.menuLink} ${styles.isActive}`}
|
||||
>
|
||||
<img src={personAdd} alt="" className={styles.menuIcon} />
|
||||
{t(getTranslationID("userListPage.label.addUser"))}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="" className={styles.menuLink}>
|
||||
<img src={editImg} alt="" className={styles.menuIcon} />
|
||||
{t(getTranslationID("userListPage.label.edit"))}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="" className={styles.menuLink}>
|
||||
<img src={deleteImg} alt="" className={styles.menuIcon} />
|
||||
{t(getTranslationID("userListPage.label.delete"))}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="" className={styles.menuLink}>
|
||||
<img src={badgeImg} alt="" className={styles.menuIcon} />
|
||||
{t(
|
||||
getTranslationID("userListPage.label.licenseAllocation")
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<table className={styles.table}>
|
||||
<tbody>
|
||||
<tr className={styles.tableHeader}>
|
||||
<th>
|
||||
<a className={styles.hasSort}>
|
||||
{t(getTranslationID("userListPage.label.name"))}
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a className={styles.hasSort}>
|
||||
{t(getTranslationID("userListPage.label.role"))}
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a className={styles.hasSort}>
|
||||
{t(getTranslationID("userListPage.label.authorID"))}
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a className={styles.hasSort}>
|
||||
{t(getTranslationID("userListPage.label.typistGroup"))}
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a className={styles.hasSort}>
|
||||
{t(getTranslationID("userListPage.label.email"))}
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a className={styles.hasSort}>
|
||||
{t(getTranslationID("userListPage.label.status"))}
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a className={styles.hasSort}>
|
||||
{t(getTranslationID("userListPage.label.expiration"))}
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a className={styles.hasSort}>
|
||||
{t(getTranslationID("userListPage.label.remaining"))}
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
{t(getTranslationID("userListPage.label.autoRenew"))}
|
||||
</th>
|
||||
<th>
|
||||
{t(getTranslationID("userListPage.label.licenseAlert"))}
|
||||
</th>
|
||||
<th>
|
||||
{t(getTranslationID("userListPage.label.notification"))}
|
||||
</th>
|
||||
</tr>
|
||||
{/* XXX 「固定」の項目と、isSelected、isAlertの対応が必要 */}
|
||||
{domain.users.map((user) => (
|
||||
<tr key={user.email}>
|
||||
<td>{user.name}</td>
|
||||
<td>{user.role}</td>
|
||||
<td>{user.authorId}</td>
|
||||
<td>{user.typistGroupName}</td>
|
||||
<td>{user.email}</td>
|
||||
<td>固定:Uploaded</td>
|
||||
<td>固定:2023/8/3</td>
|
||||
<td>固定:114</td>
|
||||
<td>
|
||||
{user.autoRenew ? (
|
||||
<img
|
||||
src={checkFill}
|
||||
alt=""
|
||||
className={styles.icCheckCircle}
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={circle}
|
||||
alt=""
|
||||
className={styles.icCheckCircle}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{user.licenseAlert ? (
|
||||
<img
|
||||
src={checkFill}
|
||||
alt=""
|
||||
className={styles.icCheckCircle}
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={circle}
|
||||
alt=""
|
||||
className={styles.icCheckCircle}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{user.notification ? (
|
||||
<img
|
||||
src={checkFill}
|
||||
alt=""
|
||||
className={styles.icCheckCircle}
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={circle}
|
||||
alt=""
|
||||
className={styles.icCheckCircle}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className={styles.pagenation}>
|
||||
<nav className={styles.pagenationNav}>
|
||||
<span className={styles.pagenationTotal}>
|
||||
{domain.users.length}{" "}
|
||||
{t(getTranslationID("userListPage.label.users"))}
|
||||
</span>
|
||||
{/* XXX 複数ページの挙動、対応が必要 */}
|
||||
<a href="" className="pagenationNavFirst">
|
||||
«
|
||||
</a>
|
||||
<a href="" className="pagenationNavPrev">
|
||||
‹
|
||||
</a>
|
||||
1 {t(getTranslationID("userListPage.label.of"))} 1
|
||||
<a href="" className="pagenationNavNext isActive">
|
||||
›
|
||||
</a>
|
||||
<a href="" className="pagenationNavLast isActive">
|
||||
»
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
<>
|
||||
<UserAddPopup
|
||||
isOpen={isPopupOpen}
|
||||
onClose={() => {
|
||||
setIsPopupOpen(false);
|
||||
}}
|
||||
/>
|
||||
<div className={styles.wrap}>
|
||||
{/* XXX デザイン上はヘッダに「Account」「User」「License」等の項目が設定されているが、そのままでは使用できない。PBI1128ではユーザ一覧画面は作りこまないので、ユーザ一覧のPBIでヘッダをデザイン通りにする必要がある */}
|
||||
<Header />
|
||||
<main className={styles.main}>
|
||||
<div className="">
|
||||
<div className={styles.pageHeader}>
|
||||
<h1 className={styles.pageTitle}>
|
||||
{t(getTranslationID("userListPage.label.title"))}
|
||||
</h1>
|
||||
<p className="pageTxt" />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
<section className={styles.user}>
|
||||
<div>
|
||||
<ul className={styles.menuAction}>
|
||||
<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={onOpen}
|
||||
>
|
||||
<img src={personAdd} alt="" className={styles.menuIcon} />
|
||||
{t(getTranslationID("userListPage.label.addUser"))}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="" className={styles.menuLink}>
|
||||
<img src={editImg} alt="" className={styles.menuIcon} />
|
||||
{t(getTranslationID("userListPage.label.edit"))}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="" className={styles.menuLink}>
|
||||
<img src={deleteImg} alt="" className={styles.menuIcon} />
|
||||
{t(getTranslationID("userListPage.label.delete"))}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="" className={styles.menuLink}>
|
||||
<img src={badgeImg} alt="" className={styles.menuIcon} />
|
||||
{t(
|
||||
getTranslationID("userListPage.label.licenseAllocation")
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<table className={styles.table}>
|
||||
<tbody>
|
||||
<tr className={styles.tableHeader}>
|
||||
<th>
|
||||
<a className={styles.hasSort}>
|
||||
{t(getTranslationID("userListPage.label.name"))}
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a className={styles.hasSort}>
|
||||
{t(getTranslationID("userListPage.label.role"))}
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a className={styles.hasSort}>
|
||||
{t(getTranslationID("userListPage.label.authorID"))}
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a className={styles.hasSort}>
|
||||
{t(
|
||||
getTranslationID("userListPage.label.typistGroup")
|
||||
)}
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a className={styles.hasSort}>
|
||||
{t(getTranslationID("userListPage.label.email"))}
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a className={styles.hasSort}>
|
||||
{t(getTranslationID("userListPage.label.status"))}
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a className={styles.hasSort}>
|
||||
{t(getTranslationID("userListPage.label.expiration"))}
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a className={styles.hasSort}>
|
||||
{t(getTranslationID("userListPage.label.remaining"))}
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
{t(getTranslationID("userListPage.label.autoRenew"))}
|
||||
</th>
|
||||
<th>
|
||||
{t(getTranslationID("userListPage.label.licenseAlert"))}
|
||||
</th>
|
||||
<th>
|
||||
{t(getTranslationID("userListPage.label.notification"))}
|
||||
</th>
|
||||
</tr>
|
||||
{/* XXX 「固定」の項目と、isSelected、isAlertの対応が必要 */}
|
||||
{domain.users.map((user) => (
|
||||
<tr key={user.email}>
|
||||
<td>{user.name}</td>
|
||||
<td>{user.role}</td>
|
||||
<td>{user.authorId}</td>
|
||||
<td>{user.typistGroupName}</td>
|
||||
<td>{user.email}</td>
|
||||
<td>固定:Uploaded</td>
|
||||
<td>固定:2023/8/3</td>
|
||||
<td>固定:114</td>
|
||||
<td>
|
||||
{user.autoRenew ? (
|
||||
<img
|
||||
src={checkFill}
|
||||
alt=""
|
||||
className={styles.icCheckCircle}
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={circle}
|
||||
alt=""
|
||||
className={styles.icCheckCircle}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{user.licenseAlert ? (
|
||||
<img
|
||||
src={checkFill}
|
||||
alt=""
|
||||
className={styles.icCheckCircle}
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={circle}
|
||||
alt=""
|
||||
className={styles.icCheckCircle}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{user.notification ? (
|
||||
<img
|
||||
src={checkFill}
|
||||
alt=""
|
||||
className={styles.icCheckCircle}
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={circle}
|
||||
alt=""
|
||||
className={styles.icCheckCircle}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className={styles.pagenation}>
|
||||
<nav className={styles.pagenationNav}>
|
||||
<span className={styles.pagenationTotal}>
|
||||
{domain.users.length}{" "}
|
||||
{t(getTranslationID("userListPage.label.users"))}
|
||||
</span>
|
||||
{/* XXX 複数ページの挙動、対応が必要 */}
|
||||
<a href="" className="pagenationNavFirst">
|
||||
«
|
||||
</a>
|
||||
<a href="" className="pagenationNavPrev">
|
||||
‹
|
||||
</a>
|
||||
1 {t(getTranslationID("userListPage.label.of"))} 1
|
||||
<a href="" className="pagenationNavNext isActive">
|
||||
›
|
||||
</a>
|
||||
<a href="" className="pagenationNavLast isActive">
|
||||
»
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
341
dictation_client/src/pages/UserListPage/popup.tsx
Normal file
341
dictation_client/src/pages/UserListPage/popup.tsx
Normal file
@ -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<UserAddPopupProps> = (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<boolean>(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 (
|
||||
<div className={`${styles.modal} ${isOpen ? styles.isShow : ""}`}>
|
||||
<div className={styles.modalBox}>
|
||||
<p className={styles.modalTitle}>
|
||||
{t(getTranslationID("userListPage.label.addUser"))}
|
||||
<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}>
|
||||
{t(getTranslationID("userListPage.label.personal"))}
|
||||
</dt>
|
||||
<dt>{t(getTranslationID("userListPage.label.name"))}</dt>
|
||||
<dd>
|
||||
<input
|
||||
type="text"
|
||||
size={40}
|
||||
maxLength={255}
|
||||
className={styles.formInput}
|
||||
value={name}
|
||||
onChange={(e) => {
|
||||
dispatch(changeName({ name: e.target.value }));
|
||||
}}
|
||||
/>
|
||||
{isPushCreateButton && hasErrorEmptyName && (
|
||||
<span className={styles.formError}>
|
||||
{t(getTranslationID("signupPage.message.inputEmptyError"))}
|
||||
</span>
|
||||
)}
|
||||
</dd>
|
||||
<dt>{t(getTranslationID("userListPage.label.email"))}</dt>
|
||||
<dd>
|
||||
<input
|
||||
type="email"
|
||||
size={40}
|
||||
maxLength={255}
|
||||
className={styles.formInput}
|
||||
value={email}
|
||||
onChange={(e) => {
|
||||
dispatch(changeEmail({ email: e.target.value }));
|
||||
}}
|
||||
/>
|
||||
{isPushCreateButton && hasErrorEmptyEmail && (
|
||||
<span className={styles.formError}>
|
||||
{t(getTranslationID("signupPage.message.inputEmptyError"))}
|
||||
</span>
|
||||
)}
|
||||
{isPushCreateButton && hasErrorIncorrectEmail && (
|
||||
<span className={styles.formError}>
|
||||
{t(
|
||||
getTranslationID("signupPage.message.emailIncorrectError")
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</dd>
|
||||
<dt>{t(getTranslationID("userListPage.label.role"))}</dt>
|
||||
<dd>
|
||||
<label htmlFor={ROLE.AUTHOR}>
|
||||
<input
|
||||
type="radio"
|
||||
name="role"
|
||||
className={styles.formRadio}
|
||||
checked={role === ROLE.AUTHOR}
|
||||
onChange={() => {
|
||||
dispatch(changeRole({ role: ROLE.AUTHOR }));
|
||||
}}
|
||||
/>
|
||||
{t(getTranslationID("userListPage.label.author"))}
|
||||
</label>
|
||||
<label htmlFor={ROLE.TYPIST}>
|
||||
<input
|
||||
type="radio"
|
||||
name="role"
|
||||
className={styles.formRadio}
|
||||
checked={role === ROLE.TYPIST}
|
||||
onChange={() => {
|
||||
dispatch(changeRole({ role: ROLE.TYPIST }));
|
||||
}}
|
||||
/>
|
||||
{t(getTranslationID("userListPage.label.transcriptionist"))}
|
||||
</label>
|
||||
<label htmlFor="None">
|
||||
<input
|
||||
type="radio"
|
||||
name="role"
|
||||
className={styles.formRadio}
|
||||
checked={role === "None"}
|
||||
onChange={() => {
|
||||
dispatch(changeRole({ role: "None" }));
|
||||
}}
|
||||
/>
|
||||
{t(getTranslationID("userListPage.label.none"))}
|
||||
</label>
|
||||
</dd>
|
||||
{/** Author 選択時に表示 */}
|
||||
{role === ROLE.AUTHOR && (
|
||||
<div className={styles.slideSet} id="">
|
||||
<dt>{t(getTranslationID("userListPage.label.authorID"))}</dt>
|
||||
<dd>
|
||||
<input
|
||||
type="text"
|
||||
size={40}
|
||||
maxLength={16}
|
||||
className={styles.formInput}
|
||||
value={authorId ?? undefined}
|
||||
onChange={(e) => {
|
||||
dispatch(changeAuthorId({ authorId: e.target.value }));
|
||||
}}
|
||||
/>
|
||||
{isPushCreateButton && hasErrorEmptyAuthorId && (
|
||||
<span className={styles.formError}>
|
||||
{t(
|
||||
getTranslationID("signupPage.message.inputEmptyError")
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{isPushCreateButton && hasErrorIncorrectAuthorId && (
|
||||
<span className={styles.formError}>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"userListPage.message.authorIdIncorrectError"
|
||||
)
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/** Transcriptionist 選択時に表示 */}
|
||||
{role === ROLE.TYPIST && (
|
||||
<div className={styles.slideSet} id="">
|
||||
<dt className={`${styles.overLine} ${styles.marginBtm0}`}>
|
||||
{t(getTranslationID("userListPage.label.addToGroup"))}
|
||||
</dt>
|
||||
<dd>
|
||||
<select
|
||||
className={styles.formInput}
|
||||
onChange={(e) => {
|
||||
dispatch(
|
||||
changeTypistGroupId({
|
||||
typistGroupId: Number(e.target.value),
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
{/** XXX: タイピストグループは後ほど差し替える */}
|
||||
<option value={0}>
|
||||
{t(getTranslationID("userListPage.label.selectGroup"))}
|
||||
</option>
|
||||
<option value={1}>Tgroup A</option>
|
||||
<option value={2}>Tgroup B</option>
|
||||
<option value={3}>Tgroup C</option>
|
||||
</select>
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<dt>{t(getTranslationID("userListPage.label.setting"))}</dt>
|
||||
<dd className={styles.last}>
|
||||
<p>
|
||||
<label htmlFor="Auto renew">
|
||||
<input
|
||||
type="checkbox"
|
||||
className={styles.formCheck}
|
||||
checked={autoRenew}
|
||||
onChange={(e) => {
|
||||
dispatch(
|
||||
changeAutoRenew({ autoRenew: e.target.checked })
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{t(getTranslationID("userListPage.label.autoRenew"))}
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label htmlFor="License Alert">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={licenseAlert}
|
||||
className={styles.formCheck}
|
||||
onChange={(e) => {
|
||||
dispatch(
|
||||
changeLicenseAlert({ licenseAlert: e.target.checked })
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{t(getTranslationID("userListPage.label.licenseAlert"))}
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label htmlFor="Notification">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={notification}
|
||||
className={styles.formCheck}
|
||||
onChange={(e) => {
|
||||
dispatch(
|
||||
changeNotification({ notification: e.target.checked })
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{t(getTranslationID("userListPage.label.notification"))}
|
||||
</label>
|
||||
</p>
|
||||
</dd>
|
||||
<dd className={`${styles.full} ${styles.alignCenter}`}>
|
||||
<input
|
||||
type="button"
|
||||
name="submit"
|
||||
value="Add user"
|
||||
className={`${styles.formSubmit} ${styles.marginBtm1} ${styles.isActive}`}
|
||||
onClick={onAddUser}
|
||||
/>
|
||||
</dd>
|
||||
</dl>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user