Merged PR 229: 再コミット_画面実装(カードライセンス取り込みPU)

## 概要
[Task2171: 再コミット_画面実装(カードライセンス取り込みPU)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2171)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 何をどう変更したか、追加したライブラリなど
- このPull Requestでの対象/対象外
- 影響範囲(他の機能にも影響があるか)

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

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## 動作確認状況
- ローカルで確認、develop環境で確認など

## 補足
- 相談、参考資料などがあれば
This commit is contained in:
oura.a 2023-07-11 07:02:33 +00:00
parent 3584a65682
commit 883224c914
11 changed files with 343 additions and 11 deletions

View File

@ -7,6 +7,7 @@ import ui from "features/ui/uiSlice";
import user from "features/user/userSlice";
import license from "features/license/licenseOrder/licenseSlice";
import licenseCardIssue from "features/license/licenseCardIssue/licenseCardIssueSlice";
import licenseCardActivate from "features/license/licenseCardActivate/licenseCardActivateSlice";
import licenseSummary from "features/license/licenseSummary/licenseSummarySlice";
import dictation from "features/dictation/dictationSlice";
@ -20,6 +21,7 @@ export const store = configureStore({
user,
license,
licenseCardIssue,
licenseCardActivate,
licenseSummary,
dictation,
},

View File

@ -0,0 +1,4 @@
export * from "./state";
export * from "./operations";
export * from "./selectors";
export * from "./licenseCardActivateSlice";

View File

@ -0,0 +1,31 @@
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
import { LicenseCardActivateState } from "./state";
import { activateCardLicenseAsync } from "./operations";
const initialState: LicenseCardActivateState = {
apps: {
keyLicense: "",
isLoading: false,
},
};
export const licenseCardActivateSlice = createSlice({
name: "licenseCardActivate",
initialState,
reducers: {
changeKeyLicense: (
state,
action: PayloadAction<{ keyLicense: string }>
) => {
const { keyLicense } = action.payload;
state.apps.keyLicense = keyLicense.toUpperCase();
},
cleanupApps: (state) => {
state.apps = initialState.apps;
},
},
});
export const { changeKeyLicense, cleanupApps } =
licenseCardActivateSlice.actions;
export default licenseCardActivateSlice.reducer;

View File

@ -0,0 +1,64 @@
import { createAsyncThunk } from "@reduxjs/toolkit";
import { openSnackbar } from "../../ui/uiSlice";
import type { RootState } from "../../../app/store";
import { getTranslationID } from "../../../translation";
import { LicensesApi } from "../../../api/api";
import { Configuration } from "../../../api/configuration";
import { ErrorObject, createErrorObject } from "../../../common/errors";
export const activateCardLicenseAsync = createAsyncThunk<
{
/* Empty Object */
},
{
// パラメータ
cardLicenseKey: string;
},
{
// rejectした時の返却値の型s
rejectValue: {
error: ErrorObject;
};
}
>("licenses/activateCardLicenseAsync", async (args, thunkApi) => {
const { cardLicenseKey } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration, accessToken } = state.auth;
const config = new Configuration(configuration);
const licensesApi = new LicensesApi(config);
try {
await licensesApi.activateCardLicenses(
{
cardLicenseKey,
},
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
const errorMessage = getTranslationID("common.message.internalServerError");
thunkApi.dispatch(
openSnackbar({
level: "error",
message: errorMessage,
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -0,0 +1,18 @@
import { RootState } from "../../../app/store";
export const selectInputValidationErrors = (state: RootState) => {
const { keyLicense } = state.licenseCardActivate.apps;
const hasErrorIncorrectKeyNumber = checkErrorIncorrectKeyNumber(keyLicense);
return {
hasErrorIncorrectKeyNumber,
};
};
export const checkErrorIncorrectKeyNumber = (keyLicense: string): boolean =>
// // 20+4(20文字space4個以外の場合はエラー
keyLicense.length !== 24;
export const selectKeyLicense = (state: RootState) =>
state.licenseCardActivate.apps.keyLicense;
export const selectIsLoading = (state: RootState) =>
state.licenseCardActivate.apps.isLoading;

View File

@ -0,0 +1,8 @@
export interface LicenseCardActivateState {
apps: Apps;
}
export interface Apps {
keyLicense: string;
isLoading: boolean;
}

View File

@ -1,6 +1,20 @@
import React, { useCallback } from "react";
import styles from "styles/app.module.scss";
import React, { useState, useCallback, useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import { AppDispatch } from "app/store";
import { useDispatch, useSelector } from "react-redux";
import _ from "lodash";
import styles from "../../styles/app.module.scss";
import { getTranslationID } from "../../translation";
import close from "../../assets/images/close.svg";
import {
activateCardLicenseAsync,
selectKeyLicense,
cleanupApps,
selectIsLoading,
changeKeyLicense,
selectInputValidationErrors,
} from "../../features/license/licenseCardActivate/index";
import progress_activit from "../../assets/images/progress_activit.svg";
interface CardLicenseActivatePopupProps {
onClose: () => void;
@ -10,18 +24,92 @@ export const CardLicenseActivatePopup: React.FC<
CardLicenseActivatePopupProps
> = (props) => {
const { onClose } = props;
const { t } = useTranslation();
const dispatch: AppDispatch = useDispatch();
const cardLicenseKey = useSelector(selectKeyLicense);
const [keyNumber, setKeyNumber] = useState<string>(cardLicenseKey);
const isLoading = useSelector(selectIsLoading);
// ポップアップを閉じる処理
const closePopup = useCallback(() => {
if (isLoading) {
return;
}
onClose();
}, [onClose]);
}, [isLoading, onClose]);
// ブラウザのウィンドウが閉じられようとしている場合に発火するイベントハンドラ
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
// isLoadingがtrueの場合は確認ダイアログを表示する
if (isLoading) {
e.preventDefault();
// ChromeではreturnValueが必要
e.returnValue = "";
}
};
// コンポーネントがマウントされた時にイベントハンドラを登録する
useEffect(() => {
window.addEventListener("beforeunload", handleBeforeUnload);
// コンポーネントがアンマウントされるときにイベントハンドラを解除する
return () => {
window.removeEventListener("beforeunload", handleBeforeUnload);
};
});
useEffect(
() => () => {
// useEffectのreturnとしてcleanupAppsを実行することで、ポップアップのアンマウント時に初期化を行う
dispatch(cleanupApps());
},
[dispatch]
);
const [isPushActivateButton, setIsPushActivateButton] =
useState<boolean>(false);
// エラー宣言
const { hasErrorIncorrectKeyNumber } = useSelector(
selectInputValidationErrors
);
// activateボタン押下時
const onActivateLicense = useCallback(async () => {
setIsPushActivateButton(true);
if (keyNumber.length !== 24) {
const inputBox = document.getElementById("inputBox");
// カーソルをテキストボックスに戻す
if (inputBox) {
inputBox.focus();
}
return;
}
// activateAPIの呼び出し
const cardLicenseKeyWithoutSpaces = keyNumber.replace(/\s/g, "");
const { meta } = await dispatch(
activateCardLicenseAsync({ cardLicenseKey: cardLicenseKeyWithoutSpaces })
);
setIsPushActivateButton(false);
// カーソルをテキストボックスに戻す
const inputBox = document.getElementById("inputBox");
if (inputBox) {
inputBox.focus();
}
if (meta.requestStatus === "fulfilled") {
dispatch(cleanupApps());
setKeyNumber("");
}
}, [keyNumber, dispatch]);
// HTML
return (
<div className={`${styles.modal} ${styles.isShow}`}>
<div className={styles.modalBox}>
<p className={styles.modalTitle}>
Activate License Key
{t(getTranslationID("cardLicenseActivatePopupPage.label.title"))}
<button type="button" onClick={closePopup}>
<img src={close} className={styles.modalTitleIcon} alt="close" />
</button>
@ -30,24 +118,93 @@ export const CardLicenseActivatePopup: React.FC<
<form className={styles.form}>
<dl className={`${styles.formList} ${styles.hasbg}`}>
<dt className={styles.formTitle} />
<dt>Key number</dt>
<dt>
<label htmlFor="inputBox">
{t(
getTranslationID(
"cardLicenseActivatePopupPage.label.keyNumber"
)
)}
</label>
</dt>
<dd className="">
<input
id="inputBox"
type="text"
size={40}
size={48}
name=""
value=""
maxLength={20}
value={keyNumber}
maxLength={24} // 20+4(20文字space4個
className={styles.formInput}
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus
onChange={(e) => {
let input = e.target.value.toUpperCase();
input = input.replace(/[^A-Z0-9]/g, "");
// _.chunk関数で、配列の要素を一定の要素数ごとに分ける
input = _.chunk(input, 4)
.map((a) => a.join(""))
.join(" ");
setKeyNumber(input);
if (input.includes("\n")) {
dispatch(
changeKeyLicense({
keyLicense: e.target.value.toUpperCase(),
})
);
onActivateLicense();
}
}}
onBlur={(e) => {
dispatch(
changeKeyLicense({
keyLicense: e.target.value.toUpperCase(),
})
);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
const input = e.target.value.toUpperCase();
setKeyNumber(input);
const button = document.getElementById("button");
if (button) {
button.focus();
button.click();
}
}
}}
/>
{isPushActivateButton && hasErrorIncorrectKeyNumber && (
<span className={styles.formError}>
{t(
getTranslationID(
"cardLicenseActivatePopupPage.label.keyNumberIncorrectError"
)
)}
</span>
)}
</dd>
<dd className={`${styles.full} ${styles.alignCenter}`}>
<input
id="button"
type="button"
name="submit"
value="Activate"
className={`${styles.formSubmit} ${styles.marginBtm1} ${styles.isActive}`}
onClick={closePopup}
value={t(
getTranslationID(
"cardLicenseActivatePopupPage.label.activateButton"
)
)}
onClick={onActivateLicense}
className={`${styles.formSubmit} ${styles.marginBtm1} ${
!isLoading ? styles.isActive : ""
}`}
/>
<img
style={{ display: isLoading ? "inline" : "none" }}
src={progress_activit}
className={styles.icLoading}
alt="Loading"
/>
</dd>
</dl>

View File

@ -238,5 +238,17 @@
"label": {
"cardLicenseButton": "(de)License Card"
}
},
"cardLicenseActivatePopupPage": {
"label": {
"title": "(de)Activate License Key",
"keyNumber": "(de)Key number",
"activateButton": "(de)activate",
"keyNumberIncorrectError": "(de)Key Numberには20桁の半角大文字英数字を入力してください。"
},
"message": {
"LicenseKeyNotExistError": "(de)入力されたライセンスキーは存在しません。ライセンスキーを再度お確かめください。",
"LicenseKeyAlreadyActivatedError": "(de)入力されたライセンスキーは、既に有効化されています。ライセンスキーを再度お確かめください。"
}
}
}

View File

@ -238,5 +238,17 @@
"label": {
"cardLicenseButton": "License Card"
}
},
"cardLicenseActivatePopupPage": {
"label": {
"title": "Activate License Key",
"keyNumber": "Key number",
"activateButton": "activate",
"keyNumberIncorrectError": "Key Numberには20桁の半角大文字英数字を入力してください。"
},
"message": {
"LicenseKeyNotExistError": "入力されたライセンスキーは存在しません。ライセンスキーを再度お確かめください。",
"LicenseKeyAlreadyActivatedError": "入力されたライセンスキーは、既に有効化されています。ライセンスキーを再度お確かめください。"
}
}
}

View File

@ -238,5 +238,17 @@
"label": {
"cardLicenseButton": "(es)License Card"
}
},
"cardLicenseActivatePopupPage": {
"label": {
"title": "(es)Activate License Key",
"keyNumber": "(es)Key number",
"activateButton": "(es)activate",
"keyNumberIncorrectError": "(es)Key Numberには20桁の半角大文字英数字を入力してください。"
},
"message": {
"LicenseKeyNotExistError": "(es)入力されたライセンスキーは存在しません。ライセンスキーを再度お確かめください。",
"LicenseKeyAlreadyActivatedError": "(es)入力されたライセンスキーは、既に有効化されています。ライセンスキーを再度お確かめください。"
}
}
}

View File

@ -238,5 +238,17 @@
"label": {
"cardLicenseButton": "(fr)License Card"
}
},
"cardLicenseActivatePopupPage": {
"label": {
"title": "(fr)Activate License Key",
"keyNumber": "(fr)Key number",
"activateButton": "(fr)activate",
"keyNumberIncorrectError": "(fr)Key Numberには20桁の半角大文字英数字を入力してください。"
},
"message": {
"LicenseKeyNotExistError": "(fr)入力されたライセンスキーは存在しません。ライセンスキーを再度お確かめください。",
"LicenseKeyAlreadyActivatedError": "(fr)入力されたライセンスキーは、既に有効化されています。ライセンスキーを再度お確かめください。"
}
}
}