## 概要 商用環境にて、一部リテラルが別言語にした場合も英語表記ののままというご指摘がありました。 指摘対象の修正と他にも漏れがないか調査して合わせて修正しております。 ### チケット [OMDS_IS-492 英語以外の言語選択時、一部が英語のままとなっている](https://so-net.backlog.jp/board/OMDS_IS?selectedIssueKey=OMDS_IS-492&category=1074456203) ### 修正内容 修正内容はは以下の内容となっております - ユーザー一覧画面 - ユーザーのRoleについて:リテラル表 99,100,107行 - ライセンス割り当てステータスについて : 540~543行目 - タスク一覧画面 - タスクのステータスについて:160~164行目 - タスクのPriorityとPriorityのステータスについて: 135行目、544、545行目 [単体テスト](https://ndstokyo.sharepoint.com/❌/r/sites/SNC-OMDS/Shared%20Documents/%E4%BF%9D%E5%AE%88/10_%E3%83%86%E3%82%B9%E3%83%88%E9%96%A2%E9%80%A3/%E5%8D%98%E4%BD%93%E3%83%86%E3%82%B9%E3%83%88/OMDS_IS-492%20%E8%8B%B1%E8%AA%9E%E4%BB%A5%E5%A4%96%E3%81%AE%E8%A8%80%E8%AA%9E%E9%81%B8%E6%8A%9E%E6%99%82%E3%80%81%E4%B8%80%E9%83%A8%E3%81%8C%E8%8B%B1%E8%AA%9E%E3%81%AE%E3%81%BE%E3%81%BE%E3%81%A8%E3%81%AA%E3%81%A3%E3%81%A6%E3%81%84%E3%82%8B/%E5%8D%98%E4%BD%93%E3%83%86%E3%82%B9%E3%83%88%E3%82%A8%E3%83%93%E3%83%87%E3%83%B3%E3%82%B9_OMDS_IS-492_%E8%8B%B1%E8%AA%9E%E4%BB%A5%E5%A4%96%E3%81%AE%E8%A8%80%E8%AA%9E%E9%81%B8%E6%8A%9E%E6%99%82%E3%80%81%E4%B8%80%E9%83%A8%E3%81%8C%E8%8B%B1%E8%AA%9E%E3%81%AE%E3%81%BE%E3%81%BE%E3%81%A8%E3%81%AA%E3%81%A3%E3%81%A6%E3%81%84%E3%82%8B.xlsx?d=w3018e2715f4c402f83f23581ffbcf0c2&csf=1&web=1&e=DgoO0B) [リテラル表](https://ndstokyo.sharepoint.com/❌/r/sites/SNC-OMDS/Shared%20Documents/%E4%BF%9D%E5%AE%88/10_%E3%83%86%E3%82%B9%E3%83%88%E9%96%A2%E9%80%A3/%E5%8D%98%E4%BD%93%E3%83%86%E3%82%B9%E3%83%88/OMDS_IS-492%20%E8%8B%B1%E8%AA%9E%E4%BB%A5%E5%A4%96%E3%81%AE%E8%A8%80%E8%AA%9E%E9%81%B8%E6%8A%9E%E6%99%82%E3%80%81%E4%B8%80%E9%83%A8%E3%81%8C%E8%8B%B1%E8%AA%9E%E3%81%AE%E3%81%BE%E3%81%BE%E3%81%A8%E3%81%AA%E3%81%A3%E3%81%A6%E3%81%84%E3%82%8B/%E3%83%A9%E3%83%99%E3%83%AB%E3%83%BB%E3%83%A1%E3%83%83%E3%82%BB%E3%83%BC%E3%82%B8%E7%AE%A1%E7%90%86_dictation_20250401_2.xlsx?d=w4f470fdc3aa948caae53c1c735295677&csf=1&web=1&e=AgX8ga) 画面のみの修正のためユニットテストは実施しておりません
551 lines
21 KiB
TypeScript
551 lines
21 KiB
TypeScript
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 { useDispatch, useSelector } from "react-redux";
|
||
import {
|
||
listUsersAsync,
|
||
selectUserViews,
|
||
selectIsLoading,
|
||
deallocateLicenseAsync,
|
||
deleteUserAsync,
|
||
confirmUserForceAsync,
|
||
} from "features/user";
|
||
import { useTranslation } from "react-i18next";
|
||
import { getTranslationID } from "translation";
|
||
import { UserView } from "features/user/types";
|
||
import { LICENSE_STATUS } from "features/user/constants";
|
||
import { isApproveTier } from "features/auth";
|
||
import { TIERS, USER_ROLES } 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 upload from "../../assets/images/upload.svg";
|
||
import searchIcon from "../../assets/images/search.svg";
|
||
import { UserAddPopup } from "./popup";
|
||
import { UserUpdatePopup } from "./updatePopup";
|
||
import { AllocateLicensePopup } from "./allocateLicensePopup";
|
||
import { ImportPopup } from "./importPopup";
|
||
|
||
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 [isImportPopupOpen, setIsImportPopupOpen] = useState(false);
|
||
const [searchEmail, setSearchEmail] = useState("");
|
||
const [searchUserName, setSearchUserName] = useState("");
|
||
|
||
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 onImportPopupOpen = useCallback(() => {
|
||
setIsImportPopupOpen(true);
|
||
}, [setIsImportPopupOpen]);
|
||
|
||
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") {
|
||
clearUserSearchInputs();
|
||
dispatch(listUsersAsync());
|
||
}
|
||
},
|
||
[dispatch, t]
|
||
);
|
||
|
||
const onDeleteUser = useCallback(
|
||
async (userId: number) => {
|
||
// ダイアログ確認
|
||
if (
|
||
/* eslint-disable-next-line no-alert */
|
||
!window.confirm(t(getTranslationID("common.message.dialogConfirm")))
|
||
) {
|
||
return;
|
||
}
|
||
|
||
const { meta } = await dispatch(deleteUserAsync({ userId }));
|
||
if (meta.requestStatus === "fulfilled") {
|
||
clearUserSearchInputs();
|
||
dispatch(listUsersAsync());
|
||
}
|
||
},
|
||
[dispatch, t]
|
||
);
|
||
|
||
const onForceEmailVerification = useCallback(
|
||
async (userId: number) => {
|
||
// ダイアログ確認
|
||
if (
|
||
/* eslint-disable-next-line no-alert */
|
||
!window.confirm(
|
||
t(
|
||
getTranslationID(
|
||
"userListPage.message.forceEmailVerificationConfirm"
|
||
)
|
||
)
|
||
)
|
||
) {
|
||
return;
|
||
}
|
||
|
||
const { meta } = await dispatch(confirmUserForceAsync({ userId }));
|
||
if (meta.requestStatus === "fulfilled") {
|
||
clearUserSearchInputs();
|
||
dispatch(listUsersAsync());
|
||
}
|
||
},
|
||
[dispatch, t]
|
||
);
|
||
|
||
const requestSearch = (e: React.FormEvent<HTMLFormElement>) => {
|
||
e.preventDefault();
|
||
dispatch(
|
||
listUsersAsync({
|
||
userInputUserName: searchUserName,
|
||
userInputEmail: searchEmail,
|
||
})
|
||
);
|
||
};
|
||
|
||
const clearUserSearchInputs = useCallback(() => {
|
||
setSearchEmail("");
|
||
setSearchUserName("");
|
||
}, [setSearchEmail, setSearchUserName]);
|
||
|
||
useEffect(() => {
|
||
// ユーザ一覧取得処理を呼び出す
|
||
dispatch(listUsersAsync());
|
||
}, [dispatch]);
|
||
|
||
const users = useSelector(selectUserViews);
|
||
const isLoading = useSelector(selectIsLoading);
|
||
// ユーザーが第5階層であるかどうかを判定する(代行操作中は第5階層として扱う)
|
||
const isTier5 =
|
||
isApproveTier([TIERS.TIER5]) || delegationAccessToken !== null;
|
||
|
||
const getUserRole = (userRole: string): string => {
|
||
switch (userRole) {
|
||
case USER_ROLES.AUTHOR:
|
||
return t(getTranslationID("userListPage.label.author"));
|
||
case USER_ROLES.TYPIST:
|
||
return t(getTranslationID("userListPage.label.transcriptionist"));
|
||
default:
|
||
return t(getTranslationID("userListPage.label.none"));
|
||
}
|
||
};
|
||
|
||
// ライセンスステータスに応じて、ライセンスステータスの文字列を返す
|
||
const getLicenseStatus = (licenseStatus: string): string => {
|
||
switch (licenseStatus) {
|
||
case LICENSE_STATUS.NOLICENSE:
|
||
return t(getTranslationID("userListPage.label.notAllocated"));
|
||
case LICENSE_STATUS.ALERT:
|
||
return t(getTranslationID("userListPage.label.alert"));
|
||
case LICENSE_STATUS.RENEW:
|
||
return t(getTranslationID("userListPage.label.renew"));
|
||
case LICENSE_STATUS.NORMAL:
|
||
return t(getTranslationID("userListPage.label.allocated"));
|
||
default:
|
||
return licenseStatus;
|
||
}
|
||
};
|
||
|
||
return (
|
||
<>
|
||
<UserUpdatePopup
|
||
isOpen={isUpdatePopupOpen}
|
||
onClose={() => {
|
||
setIsUpdatePopupOpen(false);
|
||
}}
|
||
clearUserSearchInputs={clearUserSearchInputs}
|
||
/>
|
||
<UserAddPopup
|
||
isOpen={isPopupOpen}
|
||
onClose={() => {
|
||
setIsPopupOpen(false);
|
||
}}
|
||
clearUserSearchInputs={clearUserSearchInputs}
|
||
/>
|
||
<AllocateLicensePopup
|
||
isOpen={isAllocateLicensePopupOpen}
|
||
onClose={() => {
|
||
setIsAllocateLicensePopupOpen(false);
|
||
}}
|
||
clearUserSearchInputs={clearUserSearchInputs}
|
||
/>
|
||
<ImportPopup
|
||
isOpen={isImportPopupOpen}
|
||
onClose={() => {
|
||
setIsImportPopupOpen(false);
|
||
}}
|
||
/>
|
||
<div
|
||
className={`${styles.wrap} ${
|
||
delegationAccessToken ? styles.manage : ""
|
||
}`}
|
||
>
|
||
{delegationAccessToken && <DelegationBar />}
|
||
<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>
|
||
{/* 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>
|
||
<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={onImportPopupOpen}
|
||
>
|
||
<img src={upload} alt="" className={styles.menuIcon} />
|
||
{t(getTranslationID("userListPage.label.bulkImport"))}
|
||
</a>
|
||
</li>
|
||
<li className={styles.floatRight}>
|
||
<form
|
||
className={styles.searchBar}
|
||
onSubmit={(e) => requestSearch(e)}
|
||
>
|
||
<input
|
||
type="text"
|
||
placeholder={t(
|
||
getTranslationID("userListPage.label.name")
|
||
)}
|
||
value={searchUserName}
|
||
onChange={(e) =>
|
||
setSearchUserName(e.target.value.trimStart())
|
||
}
|
||
className={styles.searchInput}
|
||
/>
|
||
<input
|
||
type="text"
|
||
placeholder={t(
|
||
getTranslationID("userListPage.label.email")
|
||
)}
|
||
value={searchEmail}
|
||
onChange={(e) =>
|
||
setSearchEmail(e.target.value.trimStart())
|
||
}
|
||
className={styles.searchInput}
|
||
/>
|
||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||
<button
|
||
className={`${styles.menuLink} ${
|
||
!isLoading ? styles.isActive : ""
|
||
}`}
|
||
type="submit"
|
||
disabled={isLoading}
|
||
>
|
||
<img
|
||
src={searchIcon}
|
||
alt="search"
|
||
className={styles.menuIcon}
|
||
/>
|
||
{t(getTranslationID("userListPage.label.search"))}
|
||
</button>
|
||
</form>
|
||
</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>
|
||
</>
|
||
)}
|
||
<li>
|
||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
|
||
<a
|
||
onClick={() => {
|
||
onDeleteUser(user.id);
|
||
}}
|
||
>
|
||
{t(
|
||
getTranslationID(
|
||
"userListPage.label.deleteUser"
|
||
)
|
||
)}
|
||
</a>
|
||
</li>
|
||
{/* 第五階層の管理者が、メール認証済みではないユーザーの行をマウスオーバーしている場合のみ */}
|
||
{isTier5 && !user.emailVerified && (
|
||
<li>
|
||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
|
||
<a
|
||
onClick={() => {
|
||
onForceEmailVerification(user.id);
|
||
}}
|
||
>
|
||
{t(
|
||
getTranslationID(
|
||
"userListPage.label.forceEmailVerification"
|
||
)
|
||
)}
|
||
</a>
|
||
</li>
|
||
)}
|
||
</ul>
|
||
</td>
|
||
<td> {user.name}</td>
|
||
<td>{getUserRole(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>
|
||
));
|
||
};
|
||
|
||
export default UserListPage;
|