金村 勇祐 ef70deee14 Merged PR 1048: 英語以外の言語選択時、一部が英語のままとなっている_実装・単体テスト
## 概要
商用環境にて、一部リテラルが別言語にした場合も英語表記ののままというご指摘がありました。
指摘対象の修正と他にも漏れがないか調査して合わせて修正しております。

### チケット
[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)

画面のみの修正のためユニットテストは実施しておりません
2025-04-23 02:05:10 +00:00

551 lines
21 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 { 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;