From 883224c9146ada0d06c99cbaaaeb2bef37ffd4bb Mon Sep 17 00:00:00 2001 From: "oura.a" Date: Tue, 11 Jul 2023 07:02:33 +0000 Subject: [PATCH] =?UTF-8?q?Merged=20PR=20229:=20=E5=86=8D=E3=82=B3?= =?UTF-8?q?=E3=83=9F=E3=83=83=E3=83=88=5F=E7=94=BB=E9=9D=A2=E5=AE=9F?= =?UTF-8?q?=E8=A3=85=EF=BC=88=E3=82=AB=E3=83=BC=E3=83=89=E3=83=A9=E3=82=A4?= =?UTF-8?q?=E3=82=BB=E3=83=B3=E3=82=B9=E5=8F=96=E3=82=8A=E8=BE=BC=E3=81=BF?= =?UTF-8?q?PU=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [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環境で確認など ## 補足 - 相談、参考資料などがあれば --- dictation_client/src/app/store.ts | 2 + .../license/licenseCardActivate/index.ts | 4 + .../licenseCardActivateSlice.ts | 31 +++ .../license/licenseCardActivate/operations.ts | 64 +++++++ .../license/licenseCardActivate/selectors.ts | 18 ++ .../license/licenseCardActivate/state.ts | 8 + .../LicensePage/cardLicenseActivatePopup.tsx | 179 ++++++++++++++++-- dictation_client/src/translation/de.json | 12 ++ dictation_client/src/translation/en.json | 12 ++ dictation_client/src/translation/es.json | 12 ++ dictation_client/src/translation/fr.json | 12 ++ 11 files changed, 343 insertions(+), 11 deletions(-) create mode 100644 dictation_client/src/features/license/licenseCardActivate/index.ts create mode 100644 dictation_client/src/features/license/licenseCardActivate/licenseCardActivateSlice.ts create mode 100644 dictation_client/src/features/license/licenseCardActivate/operations.ts create mode 100644 dictation_client/src/features/license/licenseCardActivate/selectors.ts create mode 100644 dictation_client/src/features/license/licenseCardActivate/state.ts diff --git a/dictation_client/src/app/store.ts b/dictation_client/src/app/store.ts index fd28168..4cc9a10 100644 --- a/dictation_client/src/app/store.ts +++ b/dictation_client/src/app/store.ts @@ -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, }, diff --git a/dictation_client/src/features/license/licenseCardActivate/index.ts b/dictation_client/src/features/license/licenseCardActivate/index.ts new file mode 100644 index 0000000..8698973 --- /dev/null +++ b/dictation_client/src/features/license/licenseCardActivate/index.ts @@ -0,0 +1,4 @@ +export * from "./state"; +export * from "./operations"; +export * from "./selectors"; +export * from "./licenseCardActivateSlice"; diff --git a/dictation_client/src/features/license/licenseCardActivate/licenseCardActivateSlice.ts b/dictation_client/src/features/license/licenseCardActivate/licenseCardActivateSlice.ts new file mode 100644 index 0000000..e06838c --- /dev/null +++ b/dictation_client/src/features/license/licenseCardActivate/licenseCardActivateSlice.ts @@ -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; diff --git a/dictation_client/src/features/license/licenseCardActivate/operations.ts b/dictation_client/src/features/license/licenseCardActivate/operations.ts new file mode 100644 index 0000000..103da22 --- /dev/null +++ b/dictation_client/src/features/license/licenseCardActivate/operations.ts @@ -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 }); + } +}); diff --git a/dictation_client/src/features/license/licenseCardActivate/selectors.ts b/dictation_client/src/features/license/licenseCardActivate/selectors.ts new file mode 100644 index 0000000..8ff2f38 --- /dev/null +++ b/dictation_client/src/features/license/licenseCardActivate/selectors.ts @@ -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; diff --git a/dictation_client/src/features/license/licenseCardActivate/state.ts b/dictation_client/src/features/license/licenseCardActivate/state.ts new file mode 100644 index 0000000..4ade669 --- /dev/null +++ b/dictation_client/src/features/license/licenseCardActivate/state.ts @@ -0,0 +1,8 @@ +export interface LicenseCardActivateState { + apps: Apps; +} + +export interface Apps { + keyLicense: string; + isLoading: boolean; +} diff --git a/dictation_client/src/pages/LicensePage/cardLicenseActivatePopup.tsx b/dictation_client/src/pages/LicensePage/cardLicenseActivatePopup.tsx index 71a1d6d..5bc654b 100644 --- a/dictation_client/src/pages/LicensePage/cardLicenseActivatePopup.tsx +++ b/dictation_client/src/pages/LicensePage/cardLicenseActivatePopup.tsx @@ -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(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(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 (

- Activate License Key + {t(getTranslationID("cardLicenseActivatePopupPage.label.title"))} @@ -30,24 +118,93 @@ export const CardLicenseActivatePopup: React.FC<

-
Key number
+
+ +
{ + 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 && ( + + {t( + getTranslationID( + "cardLicenseActivatePopupPage.label.keyNumberIncorrectError" + ) + )} + + )}
+ Loading
diff --git a/dictation_client/src/translation/de.json b/dictation_client/src/translation/de.json index 42dc3b5..06e3583 100644 --- a/dictation_client/src/translation/de.json +++ b/dictation_client/src/translation/de.json @@ -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)入力されたライセンスキーは、既に有効化されています。ライセンスキーを再度お確かめください。" + } } } diff --git a/dictation_client/src/translation/en.json b/dictation_client/src/translation/en.json index a7f3f7b..b2b4e35 100644 --- a/dictation_client/src/translation/en.json +++ b/dictation_client/src/translation/en.json @@ -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": "入力されたライセンスキーは、既に有効化されています。ライセンスキーを再度お確かめください。" + } } } diff --git a/dictation_client/src/translation/es.json b/dictation_client/src/translation/es.json index 85e871c..a575daf 100644 --- a/dictation_client/src/translation/es.json +++ b/dictation_client/src/translation/es.json @@ -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)入力されたライセンスキーは、既に有効化されています。ライセンスキーを再度お確かめください。" + } } } diff --git a/dictation_client/src/translation/fr.json b/dictation_client/src/translation/fr.json index 4dfaae3..9a19b52 100644 --- a/dictation_client/src/translation/fr.json +++ b/dictation_client/src/translation/fr.json @@ -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)入力されたライセンスキーは、既に有効化されています。ライセンスキーを再度お確かめください。" + } } }