Merged PR 415: 画面実装(アカウント削除確認ポップアップ)

## 概要
[Task2669: 画面実装(アカウント削除確認ポップアップ)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2669)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 何をどう変更したか、追加したライブラリなど
アカウント削除ボタンの挙動を実装

- このPull Requestでの対象/対象外
・削除後のページ遷移は対象外

- 影響範囲(他の機能にも影響があるか)

## レビューポイント
- 特にレビューしてほしい箇所
- 軽微なものや自明なものは記載不要
- 修正範囲が大きい場合などに記載
- 全体的にや仕様を満たしているか等は本当に必要な時のみ記載

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場
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/Task2669?csf=1&web=1&e=VbkLlR

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

## 補足
- 相談、参考資料などがあれば
This commit is contained in:
水本 祐希 2023-09-26 00:52:46 +00:00
parent 9ca4ae61f8
commit 1a0edee5c9
6 changed files with 8181 additions and 5814 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,10 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { AccountState } from "./state";
import { updateAccountInfoAsync, getAccountRelationsAsync } from "./operations";
import {
updateAccountInfoAsync,
getAccountRelationsAsync,
deleteAccountAsync,
} from "./operations";
const initialState: AccountState = {
domain: {
@ -95,6 +99,15 @@ export const accountSlice = createSlice({
builder.addCase(updateAccountInfoAsync.rejected, (state) => {
state.apps.isLoading = false;
});
builder.addCase(deleteAccountAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(deleteAccountAsync.fulfilled, (state) => {
state.apps.isLoading = false;
});
builder.addCase(deleteAccountAsync.rejected, (state) => {
state.apps.isLoading = false;
});
},
});
export const {

View File

@ -3,7 +3,12 @@ import type { RootState } from "app/store";
import { ErrorObject, createErrorObject } from "common/errors";
import { getTranslationID } from "translation";
import { openSnackbar } from "features/ui/uiSlice";
import { AccountsApi, UpdateAccountInfoRequest, UsersApi } from "../../api/api";
import {
AccountsApi,
UpdateAccountInfoRequest,
UsersApi,
DeleteAccountRequest,
} from "../../api/api";
import { Configuration } from "../../api/configuration";
import { ViewAccountRelationsInfo } from "./types";
@ -103,3 +108,43 @@ export const updateAccountInfoAsync = createAsyncThunk<
return thunkApi.rejectWithValue({ error });
}
});
export const deleteAccountAsync = createAsyncThunk<
{
/* Empty Object */
},
DeleteAccountRequest,
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("account/deleteAccountAsync", async (args, thunkApi) => {
const deleteAccounRequest = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration, accessToken } = state.auth;
const config = new Configuration(configuration);
const accountApi = new AccountsApi(config);
try {
await accountApi.deleteAccount(deleteAccounRequest, {
headers: { authorization: `Bearer ${accessToken}` },
});
return {};
} catch (e) {
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -0,0 +1,102 @@
import React, { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { AppDispatch } from "app/store";
import { useDispatch, useSelector } from "react-redux";
import styles from "../../styles/app.module.scss";
import { getTranslationID } from "../../translation";
import close from "../../assets/images/close.svg";
import deleteButton from "../../assets/images/delete.svg";
import { selectAccountInfo, selectIsLoading } from "features/account";
import { deleteAccountAsync } from "features/account/operations";
interface deleteAccountPopupProps {
onClose: () => void;
}
export const DeleteAccountPopup: React.FC<deleteAccountPopupProps> = (
props
) => {
const { onClose } = props;
const { t } = useTranslation();
const dispatch: AppDispatch = useDispatch();
const isLoading = useSelector(selectIsLoading);
const accountInfo = useSelector(selectAccountInfo);
// ポップアップを閉じる処理
const closePopup = useCallback(() => {
if (isLoading) {
return;
}
onClose();
}, [isLoading, onClose]);
const onDeleteAccount = useCallback(() => {
dispatch(
deleteAccountAsync({
accountId: accountInfo.account.accountId,
})
);
}, [dispatch]);
// HTML
return (
<div className={`${styles.modal} ${styles.isShow}`}>
<div className={styles.modalBox}>
<p className={styles.modalTitle}>
{t(getTranslationID("deleteAccountPopup.label.title"))}
<button type="button" onClick={closePopup}>
<img src={close} className={styles.modalTitleIcon} alt="close" />
</button>
</p>
<form className={styles.form}>
<dl className={`${styles.formList} ${styles.hasbg}`}>
<dt className={`${styles.formTitle} ${styles.alignCenter}`}>
<p>
<img
src={deleteButton}
alt="delete"
className={styles.formTrash}
/>
</p>
{t(getTranslationID("deleteAccountPopup.label.subTitle"))}
</dt>
<dd
className={`${styles.full} ${styles.alignCenter} ${styles.full}`}
>
<p className={styles.txWsline}>
{t(
getTranslationID(
"deleteAccountPopup.label.cautionOfDeleteingAccountData"
)
)}
</p>
</dd>
<dd className={`${styles.full} ${styles.alignCenter}`}>
<input
type="submit"
name="submit"
value={t(
getTranslationID("deleteAccountPopup.label.deleteButton")
)}
className={`${styles.formDelete} ${styles.marginBtm1} ${styles.isActive}`}
onClick={onDeleteAccount}
/>
<p>
<input
type="button"
name="cancel"
value={t(
getTranslationID("deleteAccountPopup.label.cancelButton")
)}
className={`${styles.formButtonTx} ${styles.marginBtm1}`}
onClick={closePopup}
/>
</p>
</dd>
</dl>
</form>
</div>
</div>
);
};

View File

@ -23,6 +23,7 @@ import { getTranslationID } from "translation";
import { TIERS } from "components/auth/constants";
import { isApproveTier } from "features/auth/utils";
import progress_activit from "../../assets/images/progress_activit.svg";
import { DeleteAccountPopup } from "./deleteAccountPopup";
const AccountPage: React.FC = (): JSX.Element => {
const dispatch: AppDispatch = useDispatch();
@ -36,6 +37,13 @@ const AccountPage: React.FC = (): JSX.Element => {
// ユーザーが第5階層であるかどうかを判定する
const isTier5 = isApproveTier([TIERS.TIER5]);
const [isDeleteAccountPopupOpen, setIsDeleteAccountPopupOpen] =
useState(false);
const onDeleteAccountOpen = useCallback(() => {
setIsDeleteAccountPopupOpen(true);
}, [setIsDeleteAccountPopupOpen]);
// 階層表示用
const tierNames: { [key: number]: string } = {
// eslint-disable-next-line @typescript-eslint/naming-convention
@ -73,280 +81,304 @@ const AccountPage: React.FC = (): JSX.Element => {
}, [dispatch, updateAccountInfo]);
return (
<div className={styles.wrap}>
<Header userName="XXXXXX" />
<UpdateTokenTimer />
<main className={styles.main}>
<div>
<div className={styles.pageHeader}>
<h1 className={styles.pageTitle}>
{t(getTranslationID("accountPage.label.title"))}
</h1>
</div>
<>
{isDeleteAccountPopupOpen && (
<DeleteAccountPopup
onClose={() => {
setIsDeleteAccountPopupOpen(false);
}}
/>
)}
<div className={styles.wrap}>
<Header userName="XXXXXX" />
<UpdateTokenTimer />
<main className={styles.main}>
<div>
<div className={styles.pageHeader}>
<h1 className={styles.pageTitle}>
{t(getTranslationID("accountPage.label.title"))}
</h1>
</div>
<section className={styles.account}>
<div className={styles.boxFlex}>
<ul className={`${styles.menuAction} ${styles.box100}`}>
<li>
<a
href="account_setting.html"
className={`${styles.menuLink} ${styles.isActive}`}
>
<img
src="images/file_delete.svg"
alt=""
className={styles.menuIcon}
/>
{t(getTranslationID("accountPage.label.fileDeleteSetting"))}
</a>
</li>
</ul>
<section className={styles.account}>
<div className={styles.boxFlex}>
<ul className={`${styles.menuAction} ${styles.box100}`}>
<li>
<a
href="account_setting.html"
className={`${styles.menuLink} ${styles.isActive}`}
>
<img
src="images/file_delete.svg"
alt=""
className={styles.menuIcon}
/>
{t(
getTranslationID("accountPage.label.fileDeleteSetting")
)}
</a>
</li>
</ul>
<div className={styles.marginRgt3}>
<dl className={styles.listVertical}>
<h4 className={styles.listHeader}>
{t(
getTranslationID("accountPage.label.accountInformation")
<div className={styles.marginRgt3}>
<dl className={styles.listVertical}>
<h4 className={styles.listHeader}>
{t(
getTranslationID("accountPage.label.accountInformation")
)}
</h4>
<dt>
{t(getTranslationID("accountPage.label.companyName"))}
</dt>
<dd>{viewInfo.account.companyName}</dd>
<dt>
{t(getTranslationID("accountPage.label.accountID"))}
</dt>
<dd>{viewInfo.account.accountId}</dd>
<dt>
{t(getTranslationID("accountPage.label.yourCategory"))}
</dt>
<dd>{tierNames[viewInfo.account.tier]}</dd>
<dt>
{t(getTranslationID("accountPage.label.yourCountry"))}
</dt>
<dd>{viewInfo.account.country}</dd>
<dt>
{t(getTranslationID("accountPage.label.yourDealer"))}
</dt>
{isTier5 && !viewInfo.account.parentAccountName && (
<dd className={styles.form}>
<select
className={`${styles.formInput} ${styles.required}`}
onChange={(event) => {
dispatch(
changeDealer({
parentAccountId:
dealers.find(
(x) => x.name === event.target.value
)?.id || undefined,
})
);
}}
>
<option value="">
{t(
getTranslationID("accountPage.label.selectDealer")
)}
</option>
{/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
<option value="Blank" />
{dealers.map((x) => (
<option key={x.name} value={x.name}>
{x.name}
</option>
))}
</select>
</dd>
)}
</h4>
<dt>
{t(getTranslationID("accountPage.label.companyName"))}
</dt>
<dd>{viewInfo.account.companyName}</dd>
<dt>{t(getTranslationID("accountPage.label.accountID"))}</dt>
<dd>{viewInfo.account.accountId}</dd>
<dt>
{t(getTranslationID("accountPage.label.yourCategory"))}
</dt>
<dd>{tierNames[viewInfo.account.tier]}</dd>
<dt>
{t(getTranslationID("accountPage.label.yourCountry"))}
</dt>
<dd>{viewInfo.account.country}</dd>
<dt>{t(getTranslationID("accountPage.label.yourDealer"))}</dt>
{isTier5 && !viewInfo.account.parentAccountName && (
{(!isTier5 || viewInfo.account.parentAccountName) && (
<dd>{viewInfo.account.parentAccountName ?? "-"}</dd>
)}
<dt>
{t(
getTranslationID("accountPage.label.dealerManagement")
)}
</dt>
{isTier5 && (
<dd>
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
<label>
<input
type="checkbox"
className={styles.formCheck}
checked={updateAccountInfo.delegationPermission}
onChange={(e) => {
dispatch(
changeDealerPermission({
delegationPermission: e.target.checked,
})
);
}}
/>
</label>
</dd>
)}
{!isTier5 && <dd>-</dd>}
</dl>
</div>
<div>
<dl className={styles.listVertical}>
<h4 className={styles.listHeader}>
{t(
getTranslationID(
"accountPage.label.administratorInformation"
)
)}
</h4>
<dt>
{t(
getTranslationID(
"accountPage.label.primaryAdministrator"
)
)}
</dt>
<dd>
{
users.find(
(x) => x.id === viewInfo.account.primaryAdminUserId
)?.name
}
</dd>
<dt>
{t(getTranslationID("accountPage.label.emailAddress"))}
</dt>
<dd className={styles.form}>
<select
className={styles.formInput}
required
name=""
className={`${styles.formInput} ${styles.required}`}
onChange={(event) => {
dispatch(
changeDealer({
parentAccountId:
dealers.find(
(x) => x.name === event.target.value
)?.id || undefined,
changePrimaryAdministrator({
primaryAdminUserId:
users.find(
(x) => x.email === event.target.value
)?.id || 0,
})
);
}}
>
<option value="">
{t(
getTranslationID("accountPage.label.selectDealer")
)}
<option
value={
users.find(
(x) =>
x.id === viewInfo.account.primaryAdminUserId
)?.email || undefined
}
>
{
users.find(
(x) =>
x.id === viewInfo.account.primaryAdminUserId
)?.email
}
</option>
{/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
<option value="Blank" />
{dealers.map((x) => (
<option key={x.name} value={x.name}>
{x.name}
{users.map((x) => (
<option key={x.email} value={x.email}>
{x.email}
</option>
))}
</select>
{isPushSaveChangesButton && isEmptyPrimaryAdmin && (
<span className={styles.formError}>
{" "}
{t(
getTranslationID(
"signupPage.message.inputEmptyError"
)
)}
</span>
)}
</dd>
)}
{(!isTier5 || viewInfo.account.parentAccountName) && (
<dd>{viewInfo.account.parentAccountName ?? "-"}</dd>
)}
<dt>
{t(getTranslationID("accountPage.label.dealerManagement"))}
</dt>
{isTier5 && (
<dt>
{t(
getTranslationID(
"accountPage.label.secondaryAdministrator"
)
)}
</dt>
<dd>
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
<label>
<input
type="checkbox"
className={styles.formCheck}
checked={updateAccountInfo.delegationPermission}
onChange={(e) => {
dispatch(
changeDealerPermission({
delegationPermission: e.target.checked,
})
);
}}
/>
</label>
{
users.find(
(x) => x.id === viewInfo.account.secondryAdminUserId
)?.name
}
</dd>
)}
{!isTier5 && <dd>-</dd>}
</dl>
</div>
<div>
<dl className={styles.listVertical}>
<h4 className={styles.listHeader}>
{t(
getTranslationID(
"accountPage.label.administratorInformation"
)
)}
</h4>
<dt>
{t(
getTranslationID("accountPage.label.primaryAdministrator")
)}
</dt>
<dd>
{
users.find(
(x) => x.id === viewInfo.account.primaryAdminUserId
)?.name
}
</dd>
<dt>
{t(getTranslationID("accountPage.label.emailAddress"))}
</dt>
<dd className={styles.form}>
<select
name=""
className={styles.formInput}
required
onChange={(event) => {
dispatch(
changePrimaryAdministrator({
primaryAdminUserId:
users.find((x) => x.email === event.target.value)
?.id || 0,
})
);
}}
>
<option
value={
users.find(
(x) => x.id === viewInfo.account.primaryAdminUserId
)?.email || undefined
}
<dt>
{t(getTranslationID("accountPage.label.emailAddress"))}
</dt>
<dd className={styles.form}>
<select
name=""
className={`${styles.formInput} ${styles.required}`}
onChange={(event) => {
dispatch(
changeSecondryAdministrator({
secondryAdminUserId:
users.find(
(x) => x.email === event.target.value
)?.id ?? undefined,
})
);
}}
>
{
users.find(
(x) => x.id === viewInfo.account.primaryAdminUserId
)?.email
}
</option>
{users.map((x) => (
<option key={x.email} value={x.email}>
{x.email}
</option>
))}
</select>
{isPushSaveChangesButton && isEmptyPrimaryAdmin && (
<span className={styles.formError}>
{" "}
{t(
getTranslationID("signupPage.message.inputEmptyError")
)}
</span>
)}
</dd>
<dt>
{t(
getTranslationID(
"accountPage.label.secondaryAdministrator"
)
)}
</dt>
<dd>
{
users.find(
(x) => x.id === viewInfo.account.secondryAdminUserId
)?.name
}
</dd>
<dt>
{t(getTranslationID("accountPage.label.emailAddress"))}
</dt>
<dd className={styles.form}>
<select
name=""
className={styles.formInput}
required
onChange={(event) => {
dispatch(
changeSecondryAdministrator({
secondryAdminUserId:
users.find((x) => x.email === event.target.value)
?.id ?? undefined,
})
);
}}
>
<option
value={
viewInfo.account.secondryAdminUserId
<option
value={
viewInfo.account.secondryAdminUserId
? users.find(
(x) =>
x.id ===
viewInfo.account.secondryAdminUserId
)?.email
: undefined
}
>
{viewInfo.account.secondryAdminUserId
? users.find(
(x) =>
x.id === viewInfo.account.secondryAdminUserId
)?.email
: undefined
}
>
{viewInfo.account.secondryAdminUserId
? users.find(
(x) =>
x.id === viewInfo.account.secondryAdminUserId
)?.email
: t(
getTranslationID(
"accountPage.label.selectSecondaryAdministrator"
)
)}
</option>
{/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
<option value="Blank" />
{users.map((x) => (
<option key={x.email} value={x.email}>
{x.email}
: t(
getTranslationID(
"accountPage.label.selectSecondaryAdministrator"
)
)}
</option>
))}
</select>
</dd>
</dl>
</div>
<div className={`${styles.box100} ${styles.alignRight} `}>
<input
type="submit"
name="submit"
value={t(getTranslationID("accountPage.label.saveChanges"))}
className={`${styles.formSubmit} ${
!isLoading ? styles.isActive : ""
}
{/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
<option value="Blank" />
{users.map((x) => (
<option key={x.email} value={x.email}>
{x.email}
</option>
))}
</select>
</dd>
</dl>
</div>
<div className={`${styles.box100} ${styles.alignLeft} `}>
<input
type="submit"
name="submit"
value={t(getTranslationID("accountPage.label.saveChanges"))}
className={`${styles.formSubmit} ${
!isLoading ? styles.isActive : ""
}
`}
onClick={onSaveChangesButton}
/>
<img
style={{ display: isLoading ? "inline" : "none" }}
src={progress_activit}
className={styles.icLoading}
alt="Loading"
/>
onClick={onSaveChangesButton}
/>
<img
style={{ display: isLoading ? "inline" : "none" }}
src={progress_activit}
className={styles.icLoading}
alt="Loading"
/>
</div>
</div>
</div>
<ul className={styles.linkBottom}>
<li>
<a href="" className={styles.linkTx}>
{t(getTranslationID("accountPage.label.deleteAccount"))}
</a>
</li>
</ul>
</section>
</div>
</main>
<Footer />
</div>
{isTier5 && (
<ul className={styles.linkBottom}>
<li>
<a className={styles.linkTx} onClick={onDeleteAccountOpen}>
{t(getTranslationID("accountPage.label.deleteAccount"))}
</a>
</li>
</ul>
)}
</section>
</div>
</main>
<Footer />
</div>
</>
);
};

View File

@ -45,6 +45,7 @@ declare const classNames: {
readonly formBack: "formBack";
readonly formButtonTx: "formButtonTx";
readonly formDone: "formDone";
readonly formDelete: "formDelete";
readonly formTrash: "formTrash";
readonly listVertical: "listVertical";
readonly listHeader: "listHeader";
@ -218,5 +219,6 @@ declare const classNames: {
readonly paddSide3: "paddSide3";
readonly txIcon: "txIcon";
readonly txWswrap: "txWswrap";
readonly required: "required";
};
export = classNames;