## 概要 [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 ## 動作確認状況 - ローカルで確認 ## 補足 - 相談、参考資料などがあれば
492 lines
16 KiB
TypeScript
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>
|
|
);
|
|
};
|