Merge branch 'develop'

This commit is contained in:
SAITO-PC-3\saito.k 2024-02-14 17:58:56 +09:00
commit d5534e8c6d
22 changed files with 590 additions and 363 deletions

View File

@ -1,8 +1,6 @@
import AppRouter from "AppRouter";
import { BrowserRouter } from "react-router-dom";
import { PublicClientApplication } from "@azure/msal-browser";
import { MsalProvider, useMsal } from "@azure/msal-react";
import { msalConfig } from "common/msalConfig";
import { useMsal } from "@azure/msal-react";
import { useEffect, useLayoutEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import globalAxios, { AxiosError, AxiosResponse } from "axios";
@ -19,7 +17,6 @@ const App = (): JSX.Element => {
const { instance } = useMsal();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [t, i18n] = useTranslation();
const pca = new PublicClientApplication(msalConfig);
useEffect(() => {
const id = globalAxios.interceptors.response.use(
(response: AxiosResponse) => response,
@ -70,11 +67,9 @@ const App = (): JSX.Element => {
dispatch(closeSnackbar());
}}
/>
<MsalProvider instance={pca}>
<BrowserRouter>
<AppRouter />
</BrowserRouter>
</MsalProvider>
<BrowserRouter>
<AppRouter />
</BrowserRouter>
</>
);
};

View File

@ -76,3 +76,16 @@ export const getIdTokenFromLocalStorage = (
}
return null;
};
// JWTが有効期限切れかどうかを判定する
export const isTokenExpired = (token: string | null): boolean => {
if (token == null) {
return true;
}
const tokenObject = JSON.parse(atob(token.split(".")[1]));
if (isToken(tokenObject)) {
const now = Math.floor(Date.now() / 1000);
return tokenObject.exp < now;
}
return true;
};

View File

@ -3,18 +3,25 @@ import React from "react";
import { createRoot } from "react-dom/client";
import { I18nextProvider } from "react-i18next";
import { Provider } from "react-redux";
import { PublicClientApplication } from "@azure/msal-browser";
import { msalConfig } from "common/msalConfig";
import { MsalProvider } from "@azure/msal-react";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import i18n from "./i18n";
const pca = new PublicClientApplication(msalConfig);
const container = document.getElementById("root");
if (container) {
const root = createRoot(container);
root.render(
<React.StrictMode>
<Provider store={store}>
<I18nextProvider i18n={i18n} />
<App />
<MsalProvider instance={pca}>
<I18nextProvider i18n={i18n} />
<App />
</MsalProvider>
</Provider>
</React.StrictMode>
);

View File

@ -10,14 +10,6 @@ import {
import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import {
clearToken,
isAdminUser,
isApproveTier,
isStandardUser,
loadAccessToken,
} from "features/auth";
import { TIERS } from "components/auth/constants";
const AuthPage: React.FC = (): JSX.Element => {
const { instance } = useMsal();
@ -34,38 +26,7 @@ const AuthPage: React.FC = (): JSX.Element => {
(async () => {
try {
// ログイン済みの場合、ログイン後の遷移先を決定する
if (loadAccessToken()) {
// 第一~第四階層の管理者はライセンス画面へ遷移
if (
isApproveTier([
TIERS.TIER1,
TIERS.TIER2,
TIERS.TIER3,
TIERS.TIER4,
]) &&
isAdminUser()
) {
navigate("/license");
return;
}
// 第五階層の管理者はユーザー画面へ遷移
if (isApproveTier([TIERS.TIER5]) && isAdminUser()) {
navigate("/user");
return;
}
// 一般ユーザーはdictationPageへ遷移
if (isStandardUser()) {
navigate("/dictations");
return;
}
// それ以外は認証エラー画面へ遷移
instance.logoutRedirect({
postLogoutRedirectUri: "/AuthError",
});
clearToken();
return;
}
// idTokenが有効セットされているかを確認する
const loginResult = await instance.handleRedirectPromise();
if (loginResult && loginResult.account) {
const { homeAccountId, idTokenClaims } = loginResult.account;

View File

@ -353,6 +353,7 @@ export const DisPlayInfo: React.FC = (): JSX.Element => {
<li>
<label htmlFor="comment">
<input
id="comment"
type="checkbox"
value="clm16"
className={styles.formCheck}

View File

@ -1116,13 +1116,7 @@ const DictationPage: React.FC = (): JSX.Element => {
{(isChangeTranscriptionistPopupOpen || !isLoading) &&
tasks.length !== 0 &&
tasks.map((x) => (
<tr
key={x.audioFileId}
style={{
backgroundColor:
x.priority === "01" ? "#ff00004f" : "#ffffff",
}}
>
<tr key={x.audioFileId}>
<td className={styles.clm0}>
<ul className={styles.menuInTable}>
<li>
@ -1231,7 +1225,12 @@ const DictationPage: React.FC = (): JSX.Element => {
</td>
)}
{displayColumn.Priority && (
<td className={styles.clm3}>
<td
className={styles.clm3}
style={{
color: x.priority === "01" ? "red" : undefined,
}}
>
{x.priority === "01"
? PRIORITY.HIGH
: PRIORITY.NORMAL}

View File

@ -1,8 +1,10 @@
import React from "react";
import { Link } from "react-router-dom";
export const AuthErrorPage = (): JSX.Element => (
<div>
<p></p>
<p>login failed</p>
<br />
<Link to="/">return to TopPage</Link>
</div>
);

View File

@ -171,8 +171,11 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
<div>
<div className={styles.pageHeader}>
<h1 className={styles.pageTitle}>
{t(getTranslationID("orderHistoriesPage.label.title"))}
{t(getTranslationID("LicenseSummaryPage.label.title"))}
</h1>
<p className={styles.pageTx}>
{t(getTranslationID("orderHistoriesPage.label.orderHistory"))}
</p>
</div>
<section className={styles.license}>
<div>

View File

@ -1,6 +1,6 @@
import { useMsal } from "@azure/msal-react";
import { AppDispatch } from "app/store";
import { isIdToken } from "common/token";
import { isIdToken, isTokenExpired } from "common/token";
import {
clearToken,
isAdminUser,
@ -52,10 +52,10 @@ const LoginPage: React.FC = (): JSX.Element => {
instance.logoutRedirect({
postLogoutRedirectUri: "/AuthError",
});
clearToken();
}, [instance, navigate]);
dispatch(clearToken());
}, [instance, navigate, dispatch]);
const tokenSet = useCallback(
const tokenSetAndNavigate = useCallback(
async (idToken: string) => {
// ログイン処理呼び出し
const { meta, payload } = await dispatch(loginAsync({ idToken }));
@ -96,31 +96,32 @@ const LoginPage: React.FC = (): JSX.Element => {
useEffect(() => {
// idTokenStringがあるか⇒認証中
// accessTokenがある場合⇒ログイン済み
// どちらもなければ直打ち
// accessTokenがある場合⇒ログイン済みなのにブラウザバックでログイン画面に戻ってきた場合
// どちらもなければURL直打ち
(async () => {
if (loadAccessToken()) {
navigateToLoginedPage();
return;
// ローカルストレージにidTokenがある場合は取得する
let idTokenString: string | null = null;
if (localStorageKeyforIdToken !== null) {
idTokenString = localStorage.getItem(localStorageKeyforIdToken);
}
// AADB2Cのログイン画面とLoginPageを経由していない場合はトップページに遷移する
if (!localStorageKeyforIdToken) {
navigate("/");
return;
}
const idTokenString = localStorage.getItem(localStorageKeyforIdToken);
// idTokenがない(=正常なログインプロセス中でない)場合は有効なアクセストークンを所持しているか確認し、
// 有効であればログイン画面に遷移 or 無効であればトップページに遷移
if (idTokenString === null) {
navigate("/");
const token = loadAccessToken();
// アクセストークンがない or 有効期限切れ場合はトップページに遷移
if (isTokenExpired(token)) {
navigate("/");
} else {
// 有効なアクセストークンがある場合はログイン画面に遷移
navigateToLoginedPage();
}
return;
}
if (idTokenString) {
const idTokenObject = JSON.parse(idTokenString);
if (isIdToken(idTokenObject)) {
await tokenSet(idTokenObject.secret);
}
const idTokenObject = JSON.parse(idTokenString);
if (isIdToken(idTokenObject)) {
await tokenSetAndNavigate(idTokenObject.secret);
}
})();
// 画面描画後のみ実行するため引数を設定しない

View File

@ -147,104 +147,115 @@ const UserListPage: React.FC = (): JSX.Element => {
</a>
</li>
</ul>
<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
<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(
@ -255,57 +266,58 @@ const UserListPage: React.FC = (): JSX.Element => {
</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>
</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={{

View File

@ -128,107 +128,132 @@ export const EditOptionItemsPopup: React.FC<EditOptionItemsPopupProps> = (
<dl className={`${styles.formList} ${styles.hasbg}`}>
<dd className={styles.full}>
<div className={styles.tableWrap}>
<table className={styles.table}>
<tr className={styles.tableHeader}>
<th className={styles.noLine}>
{t(getTranslationID("worktypeIdSetting.label.itemLabel"))}
</th>
<th className={styles.noLine}>
{t(
getTranslationID("worktypeIdSetting.label.defaultValue")
)}
</th>
<th>
{t(
getTranslationID("worktypeIdSetting.label.initialValue")
)}
</th>
</tr>
{optionItems?.map((item) => (
<tr key={`optionItem_${item.id}`}>
<td>
<input
type="text"
maxLength={16}
name="itemLabel"
value={item.itemLabel}
className={styles.formInput}
onChange={(e) => {
const { value } = e.target;
// optionItemsの更新
const newOptionItem = {
...item,
itemLabel: value,
};
onChangeOptionItem(newOptionItem);
}}
/>
</td>
<td>
<select
name="defaultValueType"
className={styles.formInput}
value={item.defaultValueType}
onChange={(e) => {
const { value } = e.target;
// optionItemsの更新
const newOptionItem = {
...item,
defaultValueType: value,
};
onChangeOptionItem(newOptionItem);
}}
>
<option
value={OPTION_ITEMS_DEFAULT_VALUE_TYPE.DEFAULT}
>
{t(
getTranslationID(
"worktypeIdSetting.label.default"
)
)}
</option>
<option value={OPTION_ITEMS_DEFAULT_VALUE_TYPE.BLANK}>
{t(
getTranslationID("worktypeIdSetting.label.blank")
)}
</option>
<option
value={OPTION_ITEMS_DEFAULT_VALUE_TYPE.LAST_INPUT}
>
{t(
getTranslationID(
"worktypeIdSetting.label.lastInput"
)
)}
</option>
</select>
</td>
<td>
{item.defaultValueType ===
OPTION_ITEMS_DEFAULT_VALUE_TYPE.DEFAULT ? (
<table className={`${styles.table} ${styles.optionItem}`}>
<tbody>
<tr className={styles.tableHeader}>
<th className={styles.noLine}>
{t(
getTranslationID("worktypeIdSetting.label.itemLabel")
)}
</th>
<th className={styles.noLine}>
{t(
getTranslationID(
"worktypeIdSetting.label.defaultValue"
)
)}
</th>
<th>
{t(
getTranslationID(
"worktypeIdSetting.label.initialValue"
)
)}
</th>
</tr>
{optionItems?.map((item) => (
<tr key={`optionItem_${item.id}`}>
<td>
<input
type="text"
maxLength={20}
name="initialValue"
value={item.initialValue}
maxLength={16}
name="itemLabel"
value={item.itemLabel}
className={styles.formInput}
onChange={(e) => {
const { value } = e.target;
// optionItemsの更新
const newOptionItem = {
...item,
initialValue: value,
itemLabel: value,
};
onChangeOptionItem(newOptionItem);
}}
/>
) : (
"-"
)}
</td>
<td>
<select
name="defaultValueType"
className={styles.formInput}
value={item.defaultValueType}
onChange={(e) => {
const { value } = e.target;
// optionItemsの更新
const newOptionItem = {
...item,
defaultValueType: value,
};
onChangeOptionItem(newOptionItem);
}}
>
<option
value={OPTION_ITEMS_DEFAULT_VALUE_TYPE.DEFAULT}
>
{t(
getTranslationID(
"worktypeIdSetting.label.default"
)
)}
</option>
<option
value={OPTION_ITEMS_DEFAULT_VALUE_TYPE.BLANK}
>
{t(
getTranslationID(
"worktypeIdSetting.label.blank"
)
)}
</option>
<option
value={OPTION_ITEMS_DEFAULT_VALUE_TYPE.LAST_INPUT}
>
{t(
getTranslationID(
"worktypeIdSetting.label.lastInput"
)
)}
</option>
</select>
</td>
<td>
{item.defaultValueType ===
OPTION_ITEMS_DEFAULT_VALUE_TYPE.DEFAULT ? (
<input
type="text"
maxLength={20}
name="initialValue"
value={item.initialValue}
className={styles.formInput}
onChange={(e) => {
const { value } = e.target;
// optionItemsの更新
const newOptionItem = {
...item,
initialValue: value,
};
onChangeOptionItem(newOptionItem);
}}
/>
) : (
"-"
)}
</td>
</tr>
))}
<tr>
<td colSpan={3}>
<span
className={`${styles.formComment} ${styles.alignCenter}`}
>
{t(
getTranslationID(
"worktypeIdSetting.label.optionItemTerms"
)
)}
</span>
</td>
</tr>
))}
</tbody>
</table>
{isPushSaveButton && hasInvalidOptionItems && (
<span className={`${styles.formError} ${styles.alignCenter}`}>
@ -248,14 +273,6 @@ export const EditOptionItemsPopup: React.FC<EditOptionItemsPopupProps> = (
)}
</span>
)}
<span
style={{ display: "block" }}
className={`${styles.formComment} ${styles.alignCenter}`}
>
{t(
getTranslationID("worktypeIdSetting.label.optionItemTerms")
)}
</span>
</div>
</dd>
<dd className={`${styles.full} ${styles.alignCenter}`}>

View File

@ -1130,6 +1130,9 @@ h3 + .brCrumb .tlIcon {
.modal .form .table.backup td:first-child {
padding: 0.6rem 0.2rem;
}
.modal .form .table.optionItem select {
width: 123px;
}
.modal .form .pagenation {
margin-bottom: 1.5rem;
padding-right: 2.5%;
@ -1597,15 +1600,6 @@ _:-ms-lang(x)::-ms-backdrop,
left: 0;
z-index: 2;
}
.account .table.user,
.user .table.user,
.license .table.user,
.dictation .table.user,
.partners .table.user,
.workflow .table.user,
.support .table.user {
margin-bottom: 5rem;
}
.account .table.user th::after,
.user .table.user th::after,
.license .table.user th::after,
@ -1626,6 +1620,16 @@ _:-ms-lang(x)::-ms-backdrop,
vertical-align: top;
}
.user .table {
margin-bottom: 0;
}
.user .tableWrap {
max-width: calc(100vw - 5.1rem);
max-height: 90vh;
overflow-x: scroll;
margin-bottom: 5rem;
}
.account .listVertical {
margin-bottom: 3rem;
}
@ -2488,10 +2492,13 @@ tr.isSelected .menuInTable li a.isDisable {
.formChange ul.chooseMember li input + label,
.formChange ul.holdMember li input + label {
display: block;
padding: 0.2rem 0 0.2rem 1.5rem;
padding: 0.4rem 0 0.4rem 1.5rem;
margin-right: 0;
background: url(../assets/images/circle.svg) no-repeat left center;
background-size: 1.3rem;
white-space: pre-line;
word-break: break-all;
line-height: 1.3;
}
.formChange ul.chooseMember li input + label:hover,
.formChange ul.holdMember li input + label:hover {
@ -2501,7 +2508,7 @@ tr.isSelected .menuInTable li a.isDisable {
}
.formChange ul.chooseMember li input:checked + label,
.formChange ul.holdMember li input:checked + label {
padding: 0.2rem 1rem 0.2rem 0;
padding: 0.4rem 1.5rem 0.4rem 0;
background: url(../assets/images/check_circle_fill.svg) no-repeat right center;
background-size: 1.3rem;
}

View File

@ -74,6 +74,7 @@ declare const classNames: {
readonly table: "table";
readonly tableHeader: "tableHeader";
readonly backup: "backup";
readonly optionItem: "optionItem";
readonly pagenation: "pagenation";
readonly encryptionPass: "encryptionPass";
readonly pageHeader: "pageHeader";

View File

@ -40,9 +40,6 @@ export class User {
@Column({ default: true })
auto_renew: boolean;
@Column({ default: true })
license_alert: boolean;
@Column({ default: true })
notification: boolean;

View File

@ -48,7 +48,6 @@ export const makeTestUser = async (
accepted_dpa_version: d?.accepted_dpa_version ?? "1.0",
email_verified: d?.email_verified ?? true,
auto_renew: d?.auto_renew ?? true,
license_alert: d?.license_alert ?? true,
notification: d?.notification ?? true,
encryption: d?.encryption ?? true,
encryption_password: d?.encryption_password,
@ -117,7 +116,6 @@ export const makeTestAccount = async (
accepted_dpa_version: d?.accepted_dpa_version ?? "1.0",
email_verified: d?.email_verified ?? true,
auto_renew: d?.auto_renew ?? true,
license_alert: d?.license_alert ?? true,
notification: d?.notification ?? true,
encryption: d?.encryption ?? true,
encryption_password: d?.encryption_password ?? "password",

View File

@ -11,6 +11,7 @@ import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import { TasksService } from './tasks.service';
import { DataSource } from 'typeorm';
import {
createAudioFile,
createCheckoutPermissions,
createTask,
createUserGroup,
@ -703,6 +704,19 @@ describe('TasksService', () => {
role: 'author',
author_id: 'MY_AUTHOR_ID',
});
//「バグ 3661: [FB対応]Option Itemにチェックを付けると真っ白な画面になる」の確認のため
// audio_file_idをTaskIdと異なる値にするために、AudioFileを作成
await createAudioFile(
source,
accountId,
userId,
'MY_AUTHOR_ID',
'',
'00',
);
// Taskを作成
await createTask(
source,
accountId,
@ -746,10 +760,26 @@ describe('TasksService', () => {
{
const task = tasks[0];
expect(task.jobNumber).toEqual('00000001');
// AudioOptionItem
const audioOptionItems = Array.from({ length: 10 }).map((_, i) => {
return {
optionItemLabel: `label${i}:audio_file_id${task.audioFileId}`,
optionItemValue: `value${i}:audio_file_id${task.audioFileId}`,
};
});
expect(task.optionItemList).toEqual(audioOptionItems);
}
{
const task = tasks[1];
expect(task.jobNumber).toEqual('00000002');
// AudioOptionItem
const audioOptionItems = Array.from({ length: 10 }).map((_, i) => {
return {
optionItemLabel: `label${i}:audio_file_id${task.audioFileId}`,
optionItemValue: `value${i}:audio_file_id${task.audioFileId}`,
};
});
expect(task.optionItemList).toEqual(audioOptionItems);
}
});
it('[Author] Authorは同一アカウントであっても自分以外のAuhtorのTaskは取得できない', async () => {

View File

@ -38,6 +38,7 @@ import {
NotificationhubServiceMockValue,
makeNotificationhubServiceMock,
} from './tasks.service.mock';
import { AudioOptionItem } from '../../../repositories/audio_option_items/entity/audio_option_item.entity';
export const makeTaskTestingModuleWithNotificaiton = async (
datasource: DataSource,
@ -130,6 +131,18 @@ export const createTask = async (
audio_format: 'audio_format',
is_encrypted: true,
});
// AudioOptionItemを10個作成
const audioOptionItems = Array.from({ length: 10 }).map((_, i) => {
return {
audio_file_id: audioFileIdentifiers[0].id,
label: `label${i}:audio_file_id${audioFileIdentifiers[0].id}`,
value: `value${i}:audio_file_id${audioFileIdentifiers[0].id}`,
};
}
);
await datasource.getRepository(AudioOptionItem).insert(audioOptionItems);
const audioFile = audioFileIdentifiers.pop() as AudioFile;
const { identifiers: taskIdentifiers } = await datasource
.getRepository(Task)
@ -147,6 +160,37 @@ export const createTask = async (
const task = taskIdentifiers.pop() as Task;
return { taskId: task.id, audioFileId: audioFile.id };
};
export const createAudioFile = async(
datasource: DataSource,
account_id: number,
owner_user_id: number,
author_id: string,
work_type_id: string,
priority: string,
): Promise<{ audioFileId: number }> => {
const { identifiers: audioFileIdentifiers } = await datasource
.getRepository(AudioFile)
.insert({
account_id: account_id,
owner_user_id: owner_user_id,
url: '',
file_name: 'x.zip',
author_id: author_id,
work_type_id: work_type_id,
started_at: new Date(),
duration: '100000',
finished_at: new Date(),
uploaded_at: new Date(),
file_size: 10000,
priority: priority,
audio_format: 'audio_format',
is_encrypted: true,
});
const audioFile = audioFileIdentifiers.pop() as AudioFile;
return { audioFileId: audioFile.id };
}
/**
*
* @param datasource

View File

@ -1073,13 +1073,13 @@ export class UsersService {
const { parent_account_id: dealerId } =
await this.accountsRepository.findAccountById(context, accountId);
if (dealerId == null) {
throw new Error(`dealer is null. account_id=${accountId}`);
let dealerName: string | null = null;
if (dealerId !== null) {
const { company_name } =
await this.accountsRepository.findAccountById(context, dealerId);
dealerName = company_name;
}
const { company_name: dealerName } =
await this.accountsRepository.findAccountById(context, dealerId);
const { companyName, adminEmails } = await this.getAccountInformation(
context,
accountId,

View File

@ -41,6 +41,9 @@ export class SendGridService {
private readonly templateU107Text: string;
private readonly templateU108Html: string;
private readonly templateU108Text: string;
// U-108のテンプレート差分親アカウントがない場合
private readonly templateU108NoParentHtml: string;
private readonly templateU108NoParentText: string;
private readonly templateU109Html: string;
private readonly templateU109Text: string;
private readonly templateU111Html: string;
@ -118,6 +121,17 @@ export class SendGridService {
path.resolve(__dirname, `../../templates/template_U_108.txt`),
'utf-8',
);
this.templateU108NoParentHtml = readFileSync(
path.resolve(
__dirname,
`../../templates/template_U_108_no_parent.html`,
),
'utf-8',
);
this.templateU108NoParentText = readFileSync(
path.resolve(__dirname, `../../templates/template_U_108_no_parent.txt`),
'utf-8',
);
this.templateU109Html = readFileSync(
path.resolve(__dirname, `../../templates/template_U_109.html`),
'utf-8',
@ -462,7 +476,7 @@ export class SendGridService {
userMail: string,
customerAdminMails: string[],
customerAccountName: string,
dealerAccountName: string,
dealerAccountName: string | null,
): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${this.sendMailWithU108.name}`,
@ -471,19 +485,34 @@ export class SendGridService {
const subject = 'License Assigned Notification [U-108]';
const url = new URL(this.appDomain).href;
// メールの本文を作成する
const html = this.templateU108Html
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(USER_NAME, userName)
.replaceAll(USER_EMAIL, userMail)
.replaceAll(TOP_URL, url);
const text = this.templateU108Text
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(USER_NAME, userName)
.replaceAll(USER_EMAIL, userMail)
.replaceAll(TOP_URL, url);
let html: string;
let text: string;
if (dealerAccountName === null) {
html = this.templateU108NoParentHtml
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(USER_NAME, userName)
.replaceAll(USER_EMAIL, userMail)
.replaceAll(TOP_URL, url);
text = this.templateU108NoParentText
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(USER_NAME, userName)
.replaceAll(USER_EMAIL, userMail)
.replaceAll(TOP_URL, url);
} else {
html = this.templateU108Html
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(USER_NAME, userName)
.replaceAll(USER_EMAIL, userMail)
.replaceAll(TOP_URL, url);
text = this.templateU108Text
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(USER_NAME, userName)
.replaceAll(USER_EMAIL, userMail)
.replaceAll(TOP_URL, url);
}
const ccAddress = customerAdminMails.includes(userMail) ? [] : [userMail];

View File

@ -18,6 +18,6 @@ export class AudioOptionItem {
@Column()
value: string;
@ManyToOne(() => Task, (task) => task.audio_file_id)
@JoinColumn({ name: 'audio_file_id' })
@JoinColumn({ name: 'audio_file_id', referencedColumnName: 'audio_file_id' })
task: Task | null;
}

View File

@ -0,0 +1,69 @@
<html>
<head>
<title>License Assigned Notification [U-108]</title>
</head>
<body>
<div>
<h3>&lt;English&gt;</h3>
<p>Dear $CUSTOMER_NAME$,</p>
<p>
Please be informed that a license has been assigned to the following
user.<br />
- User Name: $USER_NAME$<br />
- Email: $USER_EMAIL$
</p>
<p>
Please log in to ODMS Cloud to verify the license expiration date.<br />
URL: $TOP_URL$
</p>
<p>
If you have received this e-mail in error, please delete this e-mail
from your system.<br />
This is an automatically generated e-mail and this mailbox is not
monitored. Please do not reply.
</p>
</div>
<div>
<h3>&lt;Deutsch&gt;</h3>
<p>Sehr geehrte(r) $CUSTOMER_NAME$,</p>
<p>
Bitte beachten Sie, dass dem folgenden Benutzer eine Lizenz zugewiesen
wurde.<br />
- Nutzername: $USER_NAME$<br />
- Email: $USER_EMAIL$
</p>
<p>
Bitte melden Sie sich bei ODMS Cloud an, um das Ablaufdatum der Lizenz
zu überprüfen.<br />
URL: $TOP_URL$
</p>
<p>
Wenn Sie diese E-Mail fälschlicherweise erhalten haben, löschen Sie
diese E-Mail bitte aus Ihrem System.<br />
Dies ist eine automatisch generierte E-Mail und dieses Postfach wird
nicht überwacht. Bitte nicht antworten.
</p>
</div>
<div>
<h3>&lt;Français&gt;</h3>
<p>Chère/Cher $CUSTOMER_NAME$,</p>
<p>
Veuillez être informé qu'une licence a été attribuée à l'utilisateur
suivant.<br />
- Nom d'utilisateur: $USER_NAME$<br />
- Email: $USER_EMAIL$
</p>
<p>
Veuillez vous connecter à ODMS Cloud pour vérifier la date d'expiration
de la licence.<br />
URL: $TOP_URL$
</p>
<p>
Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail
de votre système.<br />
Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres
n'est pas surveillée. Merci de ne pas répondre.
</p>
</div>
</body>
</html>

View File

@ -0,0 +1,41 @@
<English>
Dear $CUSTOMER_NAME$,
Please be informed that a license has been assigned to the following user.
- User Name: $USER_NAME$
- Email: $USER_EMAIL$
Please log in to ODMS Cloud to verify the license expiration date.
URL: $TOP_URL$
If you have received this e-mail in error, please delete this e-mail from your system.
This is an automatically generated e-mail and this mailbox is not monitored. Please do not reply.
<Deutsch>
Sehr geehrte(r) $CUSTOMER_NAME$,
Bitte beachten Sie, dass dem folgenden Benutzer eine Lizenz zugewiesen wurde.
- Nutzername: $USER_NAME$
- Email: $USER_EMAIL$
Bitte melden Sie sich bei ODMS Cloud an, um das Ablaufdatum der Lizenz zu überprüfen.
URL: $TOP_URL$
Wenn Sie diese E-Mail fälschlicherweise erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System.
Dies ist eine automatisch generierte E-Mail und dieses Postfach wird nicht überwacht. Bitte nicht antworten.
<Français>
Chère/Cher $CUSTOMER_NAME$,
Veuillez être informé qu'une licence a été attribuée à l'utilisateur suivant.
- Nom d'utilisateur: $USER_NAME$
- Email: $USER_EMAIL$
Veuillez vous connecter à ODMS Cloud pour vérifier la date d'expiration de la licence.
URL: $TOP_URL$
Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail de votre système.
Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres n'est pas surveillée. Merci de ne pas répondre.