OMDSCloud/dictation_client/src/pages/LicensePage/cardLicenseActivatePopup.tsx
saito.k c7d34e1ccb Merged PR 281: ユーザー一覧画面修正
## 概要
[Task2232: ユーザー一覧画面修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2232)

- ユーザー一覧の画面修正
  - 新しい項目を追加
    - Encryption
    - Prompt
    - EmailVerified
  - ユーザーのRoleに応じて表示内容を切り替える
    - author
      - Typistgroupをハイフン表示
    - typist
      - AuthorIDをハイフン表示
      - Encryptionをハイフン表示
      - Promptをハイフン表示
  - Statusに応じて表示を変更
    - Alert
      - Status, Expiration , Remainingを赤文字表示
    - NoLicense
      - Statusを赤文字表示
  - ログインユーザーのTierによる表示変更
    - 行をマウスオーバーすると出てくるボタンの表示非表示

## レビューポイント
- Selectorで画面表示する内容に変換して取得しているが問題ないか
- 表示する内容、条件に漏れはないか

## UIの変更
- 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/Task2232?csf=1&web=1&e=nX7ayK
## 動作確認状況
- ローカルで確認

## 補足
- 相談、参考資料などがあれば
2023-08-01 01:54:38 +00:00

492 lines
16 KiB
TypeScript

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 styles from "../../styles/app.module.scss";
import { getTranslationID } from "../../translation";
import close from "../../assets/images/close.svg";
import {
activateCardLicenseAsync,
cleanupApps,
selectIsLoading,
} from "../../features/license/licenseCardActivate/index";
import progress_activit from "../../assets/images/progress_activit.svg";
interface CardLicenseActivatePopupProps {
onClose: () => void;
}
export const CardLicenseActivatePopup: React.FC<
CardLicenseActivatePopupProps
> = (props) => {
const { onClose } = props;
const { t } = useTranslation();
const dispatch: AppDispatch = useDispatch();
const isLoading = useSelector(selectIsLoading);
// 各テキストエリアの値
const [keyNumber1, setKeyNumber1] = useState<string>("");
const [keyNumber2, setKeyNumber2] = useState<string>("");
const [keyNumber3, setKeyNumber3] = useState<string>("");
const [keyNumber4, setKeyNumber4] = useState<string>("");
const [keyNumber5, setKeyNumber5] = useState<string>("");
// 1つのテキストエリアに入る最大文字数
const TEXTAREASIZE = 4;
// フォーカス操作時に使用
const ref1 = useRef<HTMLTextAreaElement>(null);
const ref2 = useRef<HTMLTextAreaElement>(null);
const ref3 = useRef<HTMLTextAreaElement>(null);
const ref4 = useRef<HTMLTextAreaElement>(null);
const ref5 = useRef<HTMLTextAreaElement>(null);
// ポップアップを閉じる処理
const closePopup = useCallback(() => {
if (isLoading) {
return;
}
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 inputValueOnTextarea = (input: string, startArea: number) => {
let roopCount = startArea;
while (input.length !== 0 || roopCount === startArea) {
switch (roopCount) {
case 1:
setKeyNumber1(input.slice(0, TEXTAREASIZE));
input = input.substring(TEXTAREASIZE);
break;
case 2:
setKeyNumber2(input.slice(0, TEXTAREASIZE));
input = input.substring(TEXTAREASIZE);
break;
case 3:
setKeyNumber3(input.slice(0, TEXTAREASIZE));
input = input.substring(TEXTAREASIZE);
break;
case 4:
setKeyNumber4(input.slice(0, TEXTAREASIZE));
input = input.substring(TEXTAREASIZE);
break;
case 5:
setKeyNumber5(input.slice(0, TEXTAREASIZE));
input = input.substring(TEXTAREASIZE);
break;
default:
// テキストエリア5つから溢れる文字は切り捨てる
input = "";
}
roopCount += 1;
}
};
// テキストエリア入力時
const changeTextarea = (
e: React.ChangeEvent<HTMLTextAreaElement>,
areaNum: number
) => {
const input = e.target.value
.toUpperCase()
.replace(/[^A-Z0-9]/g, "")
.substring(0, 20);
inputValueOnTextarea(input, areaNum);
if (e.target.value.includes("\n")) {
setTimeout(() => {
onKeyDownEnter();
}, 0);
}
moveTextarea(input.length, areaNum);
};
// テキストエリア入力時(paste仕様)
const changeTextareaByPaste = (
e: React.ClipboardEvent<HTMLTextAreaElement>,
areaNum: number
) => {
const input = e.clipboardData
.getData("text")
.toUpperCase()
.replace(/[^A-Z0-9]/g, "")
.substring(0, 20);
inputValueOnTextarea(input, areaNum);
moveFocusByPaste(input.length, areaNum);
if (e.clipboardData.getData("text").includes("\n")) {
setTimeout(() => {
onKeyDownEnter();
}, 0);
}
e.preventDefault();
};
// フォーカスを移動する
const moveFocus = (target: string) => {
// stateの反映を同期してから実施
setTimeout(() => {
const obj = document.getElementById(target);
if (obj) {
obj.focus();
}
}, 0);
};
// フォーカス移動判定
const moveTextarea = (length: number, areaNum: number) => {
if (length === TEXTAREASIZE && areaNum !== 5) {
moveFocus(`textarea${(areaNum + 1).toString()}`);
}
};
// カーソルを現在入力されている中での最後尾に移動する
// eslint-disable-next-line react-hooks/exhaustive-deps
const moveCursorToLast = () => {
if (keyNumber5) {
moveFocus("textarea5");
} else if (keyNumber4) {
moveFocus("textarea4");
} else if (keyNumber3) {
moveFocus("textarea3");
} else if (keyNumber2) {
moveFocus("textarea2");
} else {
moveFocus("textarea1");
}
};
// ペースト時のフォーカスを設定する
const moveFocusByPaste = (length: number, areaNum: number) => {
let targetNum = areaNum + Math.floor(length / 4);
if (targetNum > 5) {
targetNum = 5;
}
const target = `textarea${targetNum.toString()}`;
moveFocus(target);
};
// キー入力時判定
const keyDown = (
e: React.KeyboardEvent<HTMLTextAreaElement>,
areaNum: number
) => {
if (e.key === "Enter") {
e.preventDefault();
onKeyDownEnter();
} else if (e.key === "ArrowRight") {
if (e.target.selectionStart === e.target.value.length) {
moveFocus(`textarea${(areaNum + 1).toString()}`);
}
} else if (e.key === "ArrowLeft") {
if (e.target.selectionStart === 0) {
moveFocus(`textarea${(areaNum - 1).toString()}`);
}
} else if (/^[a-zA-Z0-9]$/.test(e.key)) {
if (e.target.value.length === TEXTAREASIZE && !e.ctrlKey) {
e.preventDefault();
}
} else if (/^[!-/:-@[-`{-~]$/.test(e.key)) {
e.preventDefault();
}
};
// Enterキー押下時の処理
const onKeyDownEnter = () => {
const button = document.getElementById("button");
if (button) {
button.focus();
button.click();
}
};
// activateボタン押下時
const onActivateLicense = useCallback(async () => {
setIsPushActivateButton(true);
const keyNumber = `${keyNumber1}${keyNumber2}${keyNumber3}${keyNumber4}${keyNumber5}`;
if (keyNumber.length !== 20) {
moveCursorToLast();
return;
}
// activateAPIの呼び出し
const { meta } = await dispatch(
activateCardLicenseAsync({ cardLicenseKey: keyNumber })
);
setIsPushActivateButton(false);
if (meta.requestStatus === "fulfilled") {
// カーソルを左端に戻す
const inputBox = document.getElementById("textarea1");
if (inputBox) {
inputBox.focus();
}
dispatch(cleanupApps());
setKeyNumber1("");
setKeyNumber2("");
setKeyNumber3("");
setKeyNumber4("");
setKeyNumber5("");
} else {
moveCursorToLast();
}
}, [
keyNumber1,
keyNumber2,
keyNumber3,
keyNumber4,
keyNumber5,
dispatch,
moveCursorToLast,
]);
// HTML
return (
<div className={`${styles.modal} ${styles.isShow}`}>
<div className={styles.modalBox}>
<p className={styles.modalTitle}>
{t(getTranslationID("cardLicenseActivatePopupPage.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} />
<dt>
<label htmlFor="inputBox">
{t(
getTranslationID(
"cardLicenseActivatePopupPage.label.keyNumber"
)
)}
</label>
</dt>
<dd className={styles.last}>
<textarea
id="textarea1"
name="key1"
value={keyNumber1}
className={styles.formInputKey}
ref={ref1}
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus
onChange={(e) => {
// カーソル位置保存
const cursorPos = e.target.selectionStart;
changeTextarea(e, 1);
setTimeout(() => {
if (ref1.current) {
// 保存したカーソル位置に設定
ref1.current.selectionStart = cursorPos;
ref1.current.selectionEnd = cursorPos;
}
}, 0);
}}
onKeyDown={(e) => {
keyDown(e, 1);
}}
onFocus={(e) => {
if (ref1.current) {
ref1.current.selectionStart = e.target.value.length;
ref1.current.selectionEnd = e.target.value.length;
}
}}
onPaste={(e) => {
changeTextareaByPaste(e, 1);
}}
/>
<textarea
id="textarea2"
name="key2"
value={keyNumber2}
className={styles.formInputKey}
ref={ref2}
onChange={(e) => {
// カーソル位置保存
const cursorPos = e.target.selectionStart;
changeTextarea(e, 2);
setTimeout(() => {
if (ref2.current) {
// 保存したカーソル位置に設定
ref2.current.selectionStart = cursorPos;
ref2.current.selectionEnd = cursorPos;
}
}, 0);
}}
onKeyDown={(e) => {
keyDown(e, 2);
}}
onFocus={(e) => {
if (ref2.current) {
ref2.current.selectionStart = e.target.value.length;
ref2.current.selectionEnd = e.target.value.length;
}
}}
onPaste={(e) => {
changeTextareaByPaste(e, 2);
}}
/>
<textarea
id="textarea3"
name="key3"
value={keyNumber3}
className={styles.formInputKey}
ref={ref3}
onChange={(e) => {
// カーソル位置保存
const cursorPos = e.target.selectionStart;
changeTextarea(e, 3);
setTimeout(() => {
if (ref3.current) {
// 保存したカーソル位置に設定
ref3.current.selectionStart = cursorPos;
ref3.current.selectionEnd = cursorPos;
}
}, 0);
}}
onKeyDown={(e) => {
keyDown(e, 3);
}}
onFocus={(e) => {
if (ref3.current) {
ref3.current.selectionStart = e.target.value.length;
ref3.current.selectionEnd = e.target.value.length;
}
}}
onPaste={(e) => {
changeTextareaByPaste(e, 3);
}}
/>
<textarea
id="textarea4"
name="key4"
value={keyNumber4}
className={styles.formInputKey}
ref={ref4}
onChange={(e) => {
// カーソル位置保存
const cursorPos = e.target.selectionStart;
changeTextarea(e, 4);
setTimeout(() => {
if (ref4.current) {
// 保存したカーソル位置に設定
ref4.current.selectionStart = cursorPos;
ref4.current.selectionEnd = cursorPos;
}
}, 0);
}}
onKeyDown={(e) => {
keyDown(e, 4);
}}
onFocus={(e) => {
if (ref4.current) {
ref4.current.selectionStart = e.target.value.length;
ref4.current.selectionEnd = e.target.value.length;
}
}}
onPaste={(e) => {
changeTextareaByPaste(e, 4);
}}
/>
<textarea
id="textarea5"
name="key5"
value={keyNumber5}
className={styles.formInputKey}
ref={ref5}
onChange={(e) => {
// カーソル位置保存
const cursorPos = e.target.selectionStart;
changeTextarea(e, 5);
setTimeout(() => {
if (ref5.current) {
// 保存したカーソル位置に設定
ref5.current.selectionStart = cursorPos;
ref5.current.selectionEnd = cursorPos;
}
}, 0);
}}
onKeyDown={(e) => {
keyDown(e, 5);
}}
onFocus={(e) => {
if (ref5.current) {
ref5.current.selectionStart = e.target.value.length;
ref5.current.selectionEnd = e.target.value.length;
}
}}
onPaste={(e) => {
changeTextareaByPaste(e, 5);
}}
/>
{isPushActivateButton &&
keyNumber1.length +
keyNumber2.length +
keyNumber3.length +
keyNumber4.length +
keyNumber5.length !==
20 && (
<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={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>
</form>
</div>
</div>
);
};