saito.k 25b7936bf4 Merged PR 747: デザイン反映
## 概要
[Task3670: デザイン反映](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3670)

- ユーザー一覧画面のヘッダーのスタイル修正
  - ブラウザのサイズに合わせて折り返すようにした
  - ユーザー一覧のテーブルを横スクロールできるように修正
- ライセンス履歴画面にパンくずリストを表示
- OptionItem更新ポップアップのスタイル修正
  - 端が切れてしまっていたため枠に収まるようにした
- Typistを「Selected」と「Pool」で選択するUIのスタイル修正
  - typist名やグループ名が長くてもチェックアイコンに重ならないようにした

## レビューポイント
- 特になし

## 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/Task3670?csf=1&web=1&e=zIcngJ
## 動作確認状況
- ローカルで確認

## 補足
- 相談、参考資料などがあれば
2024-02-14 01:50:38 +00:00

387 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { AppDispatch } from "app/store";
import React, { useCallback, useEffect, useState } from "react";
import Header from "components/header";
import Footer from "components/footer";
import styles from "styles/app.module.scss";
import { UpdateTokenTimer } from "components/auth/updateTokenTimer";
import { useDispatch, useSelector } from "react-redux";
import {
listUsersAsync,
selectUserViews,
selectIsLoading,
deallocateLicenseAsync,
} from "features/user";
import { useTranslation } from "react-i18next";
import { getTranslationID } from "translation";
import { LicenseStatusType, UserView } from "features/user/types";
import {
LICENSE_NORMAL,
LICENSE_STATUS,
NO_LICENSE,
} from "features/user/constants";
import { isApproveTier } from "features/auth";
import { TIERS } from "components/auth/constants";
import {
changeUpdateUser,
changeLicenseAllocateUser,
} from "features/user/userSlice";
import { DelegationBar } from "components/delegate";
import { selectDelegationAccessToken } from "features/auth/selectors";
import personAdd from "../../assets/images/person_add.svg";
import checkFill from "../../assets/images/check_fill.svg";
import checkOutline from "../../assets/images/check_outline.svg";
import progress_activit from "../../assets/images/progress_activit.svg";
import { UserAddPopup } from "./popup";
import { UserUpdatePopup } from "./updatePopup";
import { AllocateLicensePopup } from "./allocateLicensePopup";
const UserListPage: React.FC = (): JSX.Element => {
const dispatch: AppDispatch = useDispatch();
const [t] = useTranslation();
// 代行操作用のトークンを取得する
const delegationAccessToken = useSelector(selectDelegationAccessToken);
const [isPopupOpen, setIsPopupOpen] = useState(false);
const [isUpdatePopupOpen, setIsUpdatePopupOpen] = useState(false);
const [isAllocateLicensePopupOpen, setIsAllocateLicensePopupOpen] =
useState(false);
const onOpen = useCallback(() => {
setIsPopupOpen(true);
}, [setIsPopupOpen]);
const onUpdateOpen = useCallback(
(id: number) => {
setIsUpdatePopupOpen(true);
dispatch(changeUpdateUser({ id }));
},
[setIsUpdatePopupOpen, dispatch]
);
const onAllocateLicensePopupOpen = useCallback(
(selectedUser: UserView) => {
setIsAllocateLicensePopupOpen(true);
dispatch(changeLicenseAllocateUser({ selectedUser }));
},
[setIsAllocateLicensePopupOpen, dispatch]
);
const onLicenseDeallocation = useCallback(
async (userId: number) => {
// ダイアログ確認
if (
/* eslint-disable-next-line no-alert */
!window.confirm(t(getTranslationID("common.message.dialogConfirm")))
) {
return;
}
const { meta } = await dispatch(deallocateLicenseAsync({ userId }));
if (meta.requestStatus === "fulfilled") {
dispatch(listUsersAsync());
}
},
[dispatch, t]
);
useEffect(() => {
// ユーザ一覧取得処理を呼び出す
dispatch(listUsersAsync());
}, [dispatch]);
const users = useSelector(selectUserViews);
const isLoading = useSelector(selectIsLoading);
// ユーザーが第5階層であるかどうかを判定する代行操作中は第5階層として扱う
const isTier5 =
isApproveTier([TIERS.TIER5]) || delegationAccessToken !== null;
return (
<>
<UserUpdatePopup
isOpen={isUpdatePopupOpen}
onClose={() => {
setIsUpdatePopupOpen(false);
}}
/>
<UserAddPopup
isOpen={isPopupOpen}
onClose={() => {
setIsPopupOpen(false);
}}
/>
<AllocateLicensePopup
isOpen={isAllocateLicensePopupOpen}
onClose={() => {
setIsAllocateLicensePopupOpen(false);
}}
/>
<div
className={`${styles.wrap} ${
delegationAccessToken ? styles.manage : ""
}`}
>
{delegationAccessToken && <DelegationBar />}
<Header />
<UpdateTokenTimer />
<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>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
<a
className={`${styles.menuLink} ${
!isLoading ? styles.isActive : ""
}`}
onClick={onOpen}
>
<img src={personAdd} alt="" className={styles.menuIcon} />
{t(getTranslationID("userListPage.label.addUser"))}
</a>
</li>
</ul>
<div className={styles.tableWrap}>
<table className={`${styles.table} ${styles.user}`}>
<tbody>
<tr className={styles.tableHeader}>
<th className={styles.clm0}>{/** th is empty */}</th>
<th>
{t(getTranslationID("userListPage.label.name"))}
</th>
<th>
{t(getTranslationID("userListPage.label.role"))}
</th>
<th>
{t(getTranslationID("userListPage.label.authorID"))}
</th>
<th>
{t(getTranslationID("userListPage.label.encryption"))}
</th>
<th>
{t(getTranslationID("userListPage.label.prompt"))}
</th>
<th>
{t(
getTranslationID("userListPage.label.typistGroup")
)}
</th>
<th>
{t(getTranslationID("userListPage.label.email"))}
</th>
<th>
{t(getTranslationID("userListPage.label.status"))}
</th>
<th>
{t(getTranslationID("userListPage.label.expiration"))}
</th>
<th>
{t(getTranslationID("userListPage.label.remaining"))}
</th>
<th>
{t(getTranslationID("userListPage.label.autoRenew"))}
</th>
<th>
{t(
getTranslationID("userListPage.label.notification")
)}
</th>
<th>
{t(
getTranslationID("userListPage.label.emailVerified")
)}
</th>
</tr>
{!isLoading &&
users.map((user) => (
<tr key={user.email}>
<td className={styles.clm0}>
<ul className={styles.menuInTable}>
<li>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
<a
onClick={() => {
onUpdateOpen(user.id);
}}
>
{t(
getTranslationID(
"userListPage.label.editUser"
)
)}
</a>
</li>
{isTier5 && (
<>
<li>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
<a
onClick={() => {
onAllocateLicensePopupOpen(user);
}}
>
{t(
getTranslationID(
"userListPage.label.licenseAllocation"
)
)}
</a>
</li>
<li>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
<a
className={
user.licenseStatus ===
LICENSE_STATUS.NOLICENSE
? styles.isDisable
: ""
}
onClick={() => {
onLicenseDeallocation(user.id);
}}
>
{t(
getTranslationID(
"userListPage.label.licenseDeallocation"
)
)}
</a>
</li>
</>
)}
{/* ユーザー削除 CCB後回し分なので今は非表示
<li>
<a href="">
{t(
getTranslationID(
"userListPage.label.deleteUser"
)
)}
</a>
</li>
*/}
</ul>
</td>
<td> {user.name}</td>
<td>{user.role}</td>
<td>{user.authorId}</td>
<td>{boolToElement(user.encryption)}</td>
<td>{boolToElement(user.prompt)}</td>
<td>{arrayToElement(user.typistGroupName)}</td>
<td>{user.email}</td>
<td>
<span
className={
user.licenseStatus ===
LICENSE_STATUS.NOLICENSE ||
user.licenseStatus === LICENSE_STATUS.ALERT
? styles.isAlert
: ""
}
>
{getLicenseStatus(user.licenseStatus)}
</span>
</td>
<td>
<span
className={
user.licenseStatus === LICENSE_STATUS.ALERT
? styles.isAlert
: ""
}
>
{user.expiration ?? "-"}
</span>
</td>
<td>
<span
className={
user.licenseStatus === LICENSE_STATUS.ALERT
? styles.isAlert
: ""
}
>
{user.remaining ?? "-"}
</span>
</td>
<td>{boolToElement(user.autoRenew)}</td>
<td>{boolToElement(user.notification)}</td>
<td>{boolToElement(user.emailVerified)}</td>
</tr>
))}
</tbody>
</table>
</div>
{!isLoading && users.length === 0 && (
<p
style={{
margin: "10px",
textAlign: "center",
}}
>
{t(getTranslationID("common.message.listEmpty"))}
</p>
)}
{isLoading && (
<img
src={progress_activit}
className={styles.icLoading}
alt="Loading"
/>
)}
</div>
</section>
</div>
</main>
<Footer />
</div>
</>
);
};
// boolかstringを受け取って、stringの場合はそのまま返し、boolの場合はチェックマークON/OFFを返す
const boolToElement = (value: boolean | string): JSX.Element | string => {
if (typeof value === "string") {
return value;
}
return value ? (
<img src={checkFill} alt="" className={styles.menuIcon} />
) : (
<img src={checkOutline} alt="" className={styles.menuIcon} />
);
};
// 文字列の配列または文字列を受け取って、文字列の場合はそのまま返し、配列の場合は最後の要素以外に<br>をつけて返す
const arrayToElement = (
typistGroupNames: string[] | string
): JSX.Element[] | string => {
if (typeof typistGroupNames === "string") {
return typistGroupNames;
}
return typistGroupNames.map((v, i) => (
<span key={v}>
{v}
{i !== typistGroupNames.length - 1 && <br />}
</span>
));
};
// ライセンスステータスに応じて、ライセンスステータスの文字列を返す
const getLicenseStatus = (licenseStatus: LicenseStatusType): string => {
if (licenseStatus === LICENSE_STATUS.NOLICENSE) {
return NO_LICENSE;
}
if (licenseStatus === LICENSE_STATUS.NORMAL) {
return LICENSE_NORMAL;
}
return licenseStatus;
};
export default UserListPage;