Merged PR 78: 画面実装(ユーザー一覧)

## 概要
[Task1595: 画面実装(ユーザー一覧)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/1595)

- デザイン部門のHTMLをもとに画面レイアウトを作成
- ルーティング処理を実装
- ユーザー一覧取得APIを呼び出してユーザ情報の一覧を取得
- 取得したユーザ情報を画面の一覧に表示
- 多言語対応

## レビュー対象外
- ユーザ追加ボタン押下時の挙動については[Task 1596: 画面実装(ユーザー追加ダイアログ)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/OMDSDictation/_sprints/taskboard/OMDSDictation%20%E3%83%81%E3%83%BC%E3%83%A0/OMDSDictation/%E3%82%B9%E3%83%97%E3%83%AA%E3%83%B3%E3%83%88%207-1?workitem=1596)にて実装のため本タスク対象外

## レビューポイント
- ユーザ一覧の実装についてはPBI対象外ですが、ユーザ登録の動作確認をするうえで問題ないかどうか確認おねがいします

## UIの変更
[スクリーンショット](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/Task1595?csf=1&web=1&e=236oE5)

## 動作確認状況
- ローカル環境で確認済

## 補足
- 初実装なので、各コンポーネント(state,operation,selectors...)の使い方が誤っていないかも見ていただきたいです
This commit is contained in:
masaaki 2023-04-25 00:29:41 +00:00
parent 8a0815821e
commit 469eb6542c
14 changed files with 448 additions and 51 deletions

View File

@ -12,6 +12,7 @@ import VerifySuccessPage from "pages/VerifySuccessPage";
import VerifyFailedPage from "pages/VerifyFailedPage";
import VerifyAlreadyExistPage from "pages/VerifyAlreadyExistPage";
import SignupCompletePage from "pages/SignupCompletePage";
import UserListPage from "pages/UserListPage";
const AppRouter: React.FC = () => (
<Routes>
@ -31,6 +32,7 @@ const AppRouter: React.FC = () => (
path="/mail-confirm/alreadyExist"
element={<VerifyAlreadyExistPage />}
/>
<Route path="/userlist" element={<UserListPage />} />
<Route
path="/xxx"
element={<RouteAuthGuard component={<SamplePage />} />}

View File

@ -4,6 +4,7 @@ import auth from "features/auth/authSlice";
import signup from "features/signup/signupSlice";
import verify from "features/verify/verifySlice";
import ui from "features/ui/uiSlice";
import user from "features/user/userSlice";
export const store = configureStore({
reducer: {
@ -12,6 +13,7 @@ export const store = configureStore({
signup,
verify,
ui,
user,
},
});

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.3.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="レイヤー_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
y="0px" viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve">
<style type="text/css">
.st0{fill:#282828;}
</style>
<path class="st0" d="M39.6,4.4H8.4C6,4.4,4,6.4,4,8.8v30.4c0,2.4,2,4.3,4.4,4.3h31.1c2.5,0,4.4-2,4.4-4.3V8.8
C44,6.4,42,4.4,39.6,4.4z M19.6,34.9L8.4,24l3.1-3.1l8,7.8l16.9-16.5l3.1,3.1L19.6,34.9z"/>
</svg>

After

Width:  |  Height:  |  Size: 610 B

View File

@ -0,0 +1,4 @@
export * from "./state";
export * from "./operations";
export * from "./selectors";
export * from "./userSlice";

View File

@ -0,0 +1,38 @@
import { createAsyncThunk } from "@reduxjs/toolkit";
import type { RootState } from "app/store";
import { UsersApi, GetUsersResponse } from "../../api/api";
import { Configuration } from "../../api/configuration";
import { ErrorObject, createErrorObject } from "../../common/errors";
export const listUsersAsync = createAsyncThunk<
// 正常時の戻り値の型
GetUsersResponse,
// 引数
void,
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("users/listUsersAsync", async (args, thunkApi) => {
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration, accessToken } = state.auth;
const config = new Configuration(configuration);
const usersApi = new UsersApi(config);
try {
const res = await usersApi.getUsers({
headers: { authorization: `Bearer ${accessToken}` },
});
return { users: res.data.users };
} catch (e) {
// e ⇒ errorObjectに変換
const error = createErrorObject(e);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -0,0 +1,3 @@
import { RootState } from "app/store";
export const selectDomain = (state: RootState) => state.user.domain;

View File

@ -0,0 +1,9 @@
import { User } from "../../api/api";
export interface UsersState {
domain: Domain;
}
export interface Domain {
users: User[];
}

View File

@ -0,0 +1,20 @@
import { createSlice } from "@reduxjs/toolkit";
import { UsersState } from "./state";
import { listUsersAsync } from "./operations";
const initialState: UsersState = {
domain: { users: [] },
};
export const userSlice = createSlice({
name: "user",
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(listUsersAsync.fulfilled, (state, action) => {
state.domain.users = action.payload.users;
});
},
});
export default userSlice.reducer;

View File

@ -0,0 +1,221 @@
import { AppDispatch } from "app/store";
import React, { useEffect } from "react";
import Header from "components/header";
import Footer from "components/footer";
import styles from "styles/app.module.scss";
import { useDispatch, useSelector } from "react-redux";
import { listUsersAsync, selectDomain } from "features/user";
import { useTranslation } from "react-i18next";
import { getTranslationID } from "translation";
import personAdd from "../../assets/images/person_add.svg";
import editImg from "../../assets/images/edit.svg";
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";
// eslintの検査エラー無視設定
/* eslint-disable jsx-a11y/anchor-is-valid */
const UserListPage: React.FC = (): JSX.Element => {
const dispatch: AppDispatch = useDispatch();
const [t] = useTranslation();
useEffect(() => {
// ユーザ一覧取得処理を呼び出す
dispatch(listUsersAsync());
}, [dispatch]);
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>
</div>
</section>
</div>
</main>
<Footer />
</div>
);
};
export default UserListPage;

View File

@ -43,7 +43,7 @@
"label": {
"company": "(de)Company Name",
"country": "(de)Country",
"dealer": "(de)Dealer (Optional)",
"dealer": "(de)Dealer",
"adminName": "(de)Admin Name",
"email": "(de)Email",
"password": "(de)Password",
@ -66,7 +66,7 @@
"label": {
"company": "(de)Company Name",
"country": "(de)Country",
"dealer": "(de)Dealer",
"dealer": "(de)Dealer (Optional)",
"adminName": "(de)Admin Name",
"email": "(de)Email",
"password": "(de)Password",
@ -93,5 +93,27 @@
"alreadySuccess": "(de)Already Verified!",
"returnToSignIn": "(de)Return to Sign in"
}
},
"userListPage": {
"label": {
"title": "(de)User",
"addUser": "(de)Add User",
"edit": "(de)Edit",
"delete": "(de)Delete",
"licenseAllocation": "(de)License Allocation",
"name": "(de)Name",
"role": "(de)Role",
"authorID": "(de)Author ID",
"typistGroup": "(de)Typist Group",
"email": "(de)Email",
"status": "(de)Status",
"expiration": "(de)Expiration",
"remaining": "(de)Remaining",
"autoRenew": "(de)Auto renew",
"licenseAlert": "(de)License alert",
"notification": "(de)Notification",
"users": "(de)users",
"of": "(de)of"
}
}
}

View File

@ -43,7 +43,7 @@
"label": {
"company": "Company Name",
"country": "Country",
"dealer": "Dealer (Optional)",
"dealer": "Dealer",
"adminName": "Admin Name",
"email": "Email",
"password": "Password",
@ -66,7 +66,7 @@
"label": {
"company": "Company Name",
"country": "Country",
"dealer": "Dealer",
"dealer": "Dealer (Optional)",
"adminName": "Admin Name",
"email": "Email",
"password": "Password",
@ -93,5 +93,27 @@
"alreadySuccess": "Already Verified!",
"returnToSignIn": "Return to Sign in"
}
},
"userListPage": {
"label": {
"title": "User",
"addUser": "Add User",
"edit": "Edit",
"delete": "Delete",
"licenseAllocation": "License Allocation",
"name": "Name",
"role": "Role",
"authorID": "Author ID",
"typistGroup": "Typist Group",
"email": "Email",
"status": "Status",
"expiration": "Expiration",
"remaining": "Remaining",
"autoRenew": "Auto renew",
"licenseAlert": "License alert",
"notification": "Notification",
"users": "users",
"of": "of"
}
}
}

View File

@ -43,7 +43,7 @@
"label": {
"company": "(es)Company Name",
"country": "(es)Country",
"dealer": "(es)Dealer (Optional)",
"dealer": "(es)Dealer",
"adminName": "(es)Admin Name",
"email": "(es)Email",
"password": "(es)Password",
@ -66,7 +66,7 @@
"label": {
"company": "(es)Company Name",
"country": "(es)Country",
"dealer": "(es)Dealer",
"dealer": "(es)Dealer (Optional)",
"adminName": "(es)Admin Name",
"email": "(es)Email",
"password": "(es)Password",
@ -93,5 +93,27 @@
"alreadySuccess": "(es)Already Verified!",
"returnToSignIn": "(es)Return to Sign in"
}
},
"userListPage": {
"label": {
"title": "(es)User",
"addUser": "(es)Add User",
"edit": "(es)Edit",
"delete": "(es)Delete",
"licenseAllocation": "(es)License Allocation",
"name": "(es)Name",
"role": "(es)Role",
"authorID": "(es)Author ID",
"typistGroup": "(es)Typist Group",
"email": "(es)Email",
"status": "(es)Status",
"expiration": "(es)Expiration",
"remaining": "(es)Remaining",
"autoRenew": "(es)Auto renew",
"licenseAlert": "(es)License alert",
"notification": "(es)Notification",
"users": "(es)users",
"of": "(es)of"
}
}
}

View File

@ -43,7 +43,7 @@
"label": {
"company": "(fr)Company Name",
"country": "(fr)Country",
"dealer": "(fr)Dealer (Optional)",
"dealer": "(fr)Dealer",
"adminName": "(fr)Admin Name",
"email": "(fr)Email",
"password": "(fr)Password",
@ -66,7 +66,7 @@
"label": {
"company": "(fr)Company Name",
"country": "(fr)Country",
"dealer": "(fr)Dealer",
"dealer": "(fr)Dealer (Optional)",
"adminName": "(fr)Admin Name",
"email": "(fr)Email",
"password": "(fr)Password",
@ -93,5 +93,27 @@
"alreadySuccess": "(fr)Already Verified!",
"returnToSignIn": "(fr)Return to Sign in"
}
},
"userListPage": {
"label": {
"title": "(fr)User",
"addUser": "(fr)Add User",
"edit": "(fr)Edit",
"delete": "(fr)Delete",
"licenseAllocation": "(fr)License Allocation",
"name": "(fr)Name",
"role": "(fr)Role",
"authorID": "(fr)Author ID",
"typistGroup": "(fr)Typist Group",
"email": "(fr)Email",
"status": "(fr)Status",
"expiration": "(fr)Expiration",
"remaining": "(fr)Remaining",
"autoRenew": "(fr)Auto renew",
"licenseAlert": "(fr)License alert",
"notification": "(fr)Notification",
"users": "(fr)users",
"of": "(fr)of"
}
}
}