masaaki 6f92ef9453 Merged PR 319: 画面実装(ライセンス割り当てポップアップ)
## 概要
[Task2358: 画面実装(ライセンス割り当てポップアップ)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2358)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 何をどう変更したか、追加したライブラリなど
  - ライセンス割り当てポップアップを新規に追加しました
  - ユーザー一覧画面に対して、対象のユーザ情報をstateに格納してライセンス割り当てポップアップを呼び出す処理を追加しました
- このPull Requestでの対象/対象外
  - 対象外はありません
- 影響範囲(他の機能にも影響があるか)
  - 特にありません

## レビューポイント
- 特にレビューしてほしい箇所
  - selectors.tsのselectAllocatableLicensesについて、
UTC変換を伴う時刻算出を行っています。その中で共通関数化やサブ関数化を行っていますが、外だしが妥当か確認いただきたいです。

## 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/Task2358?csf=1&web=1&e=yDndMf

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

## 補足
- 相談、参考資料などがあれば
2023-08-21 07:55:13 +00:00

330 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { RootState } from "app/store";
import { USER_ROLES } from "components/auth/constants";
import { convertLocalToUTCDate } from "common/convertLocalToUTCDate";
import {
AddUser,
RoleType,
UserView,
isLicenseStatusType,
isRoleType,
} from "./types";
import { LICENSE_STATUS, LICENSE_ALLOCATE_STATUS } from "./constants";
export const selectInputValidationErrors = (state: RootState) => {
const { name, email, role, authorId, encryption, encryptionPassword } =
state.user.apps.addUser;
// 必須項目のチェック
const hasErrorEmptyName = name === "";
const hasErrorEmptyEmail = email === "";
// Authorの場合、AuthorIDが必須(空文字,undefinedは不可)
const hasErrorEmptyAuthorId =
role === USER_ROLES.AUTHOR && (authorId === "" || !authorId);
const hasErrorIncorrectAuthorId = checkErrorIncorrectAuthorId(
authorId ?? undefined,
role
);
const hasErrorIncorrectEmail = email.match(/^[^@]+@[^@]+$/)?.length !== 1;
const hasErrorIncorrectEncryptionPassword =
checkErrorIncorrectEncryptionPassword(encryptionPassword, role, encryption);
return {
hasErrorEmptyName,
hasErrorEmptyEmail,
hasErrorEmptyAuthorId,
hasErrorIncorrectEmail,
hasErrorIncorrectAuthorId,
hasErrorIncorrectEncryptionPassword,
};
};
export const selectUpdateValidationErrors = (state: RootState) => {
const { role, authorId, encryption, encryptionPassword } =
state.user.apps.updateUser;
const { encryption: initEncryption } = state.user.apps.selectedUser;
// Authorの場合、AuthorIDが必須(空文字,undefinedは不可)
const hasErrorEmptyAuthorId =
role === USER_ROLES.AUTHOR && (authorId === "" || !authorId);
const hasErrorIncorrectAuthorId = checkErrorIncorrectAuthorId(
authorId ?? undefined,
role
);
let hasErrorIncorrectEncryptionPassword = false;
const passwordError = checkErrorIncorrectEncryptionPassword(
encryptionPassword,
role,
encryption
);
if (passwordError) {
// 最初にEncryptionがtrueで、EncryptionPassword変更されていない場合はエラーとしない
if (initEncryption && encryptionPassword === undefined) {
hasErrorIncorrectEncryptionPassword = false;
} else {
hasErrorIncorrectEncryptionPassword = true;
}
}
return {
hasErrorEmptyAuthorId,
hasErrorIncorrectAuthorId,
hasErrorIncorrectEncryptionPassword,
};
};
// encreyptionPasswordのチェック
const checkErrorIncorrectEncryptionPassword = (
encryptionPassword: string | undefined,
role: RoleType,
encryption: boolean | undefined
): boolean => {
// roleがAuthor以外の場合、チェックしない
if (role !== USER_ROLES.AUTHOR) {
return false;
}
// roleがAuthorかつencryptionがfalseの場合、チェックしない
if (!encryption) {
return false;
}
// encryptionPasswordがundefined,空文字の場合、エラー
if (!encryptionPassword || encryptionPassword === "") {
return true;
}
// encryptionPasswordがルールに則していない場合、エラー
const regex = /^[!-~]{4,16}$/;
if (!regex.test(encryptionPassword)) {
return true;
}
// チェックを通ったらエラーではない
return false;
};
export const checkErrorIncorrectAuthorId = (
authorId: string | undefined,
role: string
): boolean => {
if (!authorId || role !== USER_ROLES.AUTHOR) {
return false;
}
// 半角英数字と_の組み合わせで文字まで
const charaTypePattern = /^[A-Z0-9_]{1,16}$/;
const charaType = new RegExp(charaTypePattern).test(authorId);
return !charaType;
};
export const selectName = (state: RootState) => state.user.apps.addUser.name;
export const selectEmail = (state: RootState) => state.user.apps.addUser.email;
export const selectRole = (state: RootState) => state.user.apps.addUser.role;
export const selectAuthorId = (state: RootState) =>
state.user.apps.addUser.authorId;
export const selectAutoRenew = (state: RootState) =>
state.user.apps.addUser.autoRenew;
export const selectLicenseAlert = (state: RootState) =>
state.user.apps.addUser.licenseAlert;
export const selectNotification = (state: RootState) =>
state.user.apps.addUser.notification;
// AddUserを返却する
export const selectAddUser = (state: RootState): AddUser =>
state.user.apps.addUser;
// usersからUserViewに変換して返却する
export const selectUserViews = (state: RootState): UserView[] => {
const { users } = state.user.domain;
const userViews = users.map((user): UserView => {
const {
role,
authorId,
encryption,
prompt,
typistGroupName,
licenseStatus,
expiration,
remaining,
...rest
} = user;
// roleの型がstringなので、isRoleTypeで型ガードを行う
// roleの型がRoleTypeでなければ、何も返さない
if (!isRoleType(role) || !isLicenseStatusType(licenseStatus)) {
return {} as UserView;
}
const convertedValues = convertValueBasedOnRole(
role,
authorId,
encryption,
prompt,
typistGroupName
);
// restのid以外をUserViewに追加する
return {
typistGroupName: convertedValues.typistGroupName,
prompt: convertedValues.prompt,
encryption: convertedValues.encryption,
authorId: convertedValues.authorId,
// roleの一文字目を大文字に変換する
role: role.charAt(0).toUpperCase() + role.slice(1),
licenseStatus:
licenseStatus === LICENSE_STATUS.NORMAL ? "-" : licenseStatus,
expiration: expiration ?? "-",
remaining: remaining ?? "-",
...rest,
};
});
// 空のオブジェクトを除外する
return userViews.filter((userView) => Object.keys(userView).length !== 0);
};
export const selectIsLoading = (state: RootState) => state.user.apps.isLoading;
// roleに応じて値を変換する
const convertValueBasedOnRole = (
role: RoleType,
authorId: string | undefined,
encryption: boolean,
prompt: boolean,
typistGroupName: string[]
): {
authorId: string;
encryption: boolean | string;
prompt: boolean | string;
typistGroupName: string[] | string;
} => {
if (role === USER_ROLES.AUTHOR && authorId) {
return {
authorId,
encryption,
prompt,
typistGroupName: "-",
};
}
if (role === USER_ROLES.TYPIST) {
return {
authorId: "-",
encryption: "-",
prompt: "-",
typistGroupName,
};
}
return {
authorId: "-",
encryption: "-",
prompt: "-",
typistGroupName: "-",
};
};
export const selectUpdateUser = (state: RootState) =>
state.user.apps.updateUser;
export const selectHasPasswordMask = (state: RootState) =>
state.user.apps.hasPasswordMask;
export const selectLicenseAllocateUserId = (state: RootState) =>
state.user.apps.licenseAllocateUser.id;
export const selectLicenseAllocateUserEmail = (state: RootState) =>
state.user.apps.licenseAllocateUser.email;
export const selectLicenseAllocateUserName = (state: RootState) =>
state.user.apps.licenseAllocateUser.name;
export const selectLicenseAllocateUserAuthorId = (state: RootState) =>
state.user.apps.licenseAllocateUser.authorId;
export const selectLicenseAllocateUserStatus = (state: RootState) =>
// ライセンスが割り当てられてるかどうかのステータスを返却する。NORMAL,ALERT,RENEWはすべてライセンス割り当て扱い
state.user.apps.licenseAllocateUser.licenseStatus === LICENSE_STATUS.NOLICENSE
? LICENSE_ALLOCATE_STATUS.NOTALLOCATED
: LICENSE_ALLOCATE_STATUS.ALLOCATED;
export const selectLicenseAllocateUserExpirationDate = (state: RootState) => {
const { licenseStatus, remaining, expiration } =
state.user.apps.licenseAllocateUser;
// ライセンスが割当たっていない場合は-、割当たってる場合はremaining(expiration)の形式で返却
if (licenseStatus === LICENSE_STATUS.NOLICENSE) {
return "-";
}
return `${expiration}(${remaining})`;
};
export const selectSelectedlicenseId = (state: RootState) =>
state.user.apps.selectedlicenseId;
export const selectAllocatableLicenses = (state: RootState) => {
const { allocatableLicenses } = state.user.domain;
// licenseIdはそのまま返却、expiryDateは「nullならundifined」「null以外ならyyyy/mm/dd(現在との差分日数)」を返却
const transformedLicenses = allocatableLicenses.map((license) => ({
licenseId: license.licenseId,
expiryDate: license.expiryDate
? calculateExpiryDate(license.expiryDate)
: undefined,
}));
return transformedLicenses;
};
export const selectInputValidationErrorsForLicenseAcclocation = (
state: RootState
) => {
// 必須項目のチェック(License Acclocation画面用)
// License available選択チェック
// 初期値である0と、「Select a license」選択時のNaNの場合エラーとする
const hasErrorEmptyLicense =
state.user.apps.selectedlicenseId === 0 ||
Number.isNaN(state.user.apps.selectedlicenseId);
return {
hasErrorEmptyLicense,
};
};
// 日付の差分を計算するサブ関数
const calculateExpiryDate = (expiryDate: string) => {
const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000; // 1日のミリ秒数
const currentDate = new Date();
const expirationDate = new Date(expiryDate);
// タイムゾーンオフセットを考慮して、ローカルタイムでの日付を取得
const currentDateLocal = convertLocalToUTCDate(currentDate);
const expirationDateLocal = convertLocalToUTCDate(expirationDate);
// ライセンスは時刻を考慮しないので時分秒を意識しない日付を取得する
const currentDateWithoutTime = new Date(
currentDateLocal.getFullYear(),
currentDateLocal.getMonth(),
currentDateLocal.getDate()
);
const expirationDateWithoutTime = new Date(
expirationDateLocal.getFullYear(),
expirationDateLocal.getMonth(),
expirationDateLocal.getDate()
);
// 差分日数を取得
const timeDifference =
expirationDateWithoutTime.getTime() - currentDateWithoutTime.getTime();
const daysDifference = Math.ceil(timeDifference / MILLISECONDS_IN_A_DAY);
// yyyy/mm/dd形式の年月日を取得
const expirationYear = expirationDateWithoutTime.getFullYear();
const expirationMonth = expirationDateWithoutTime.getMonth() + 1; // getMonth() の結果は0から始まるため、1を足して実際の月に合わせる
const expirationDay = expirationDateWithoutTime.getDate();
const formattedExpirationDate = `${expirationYear}/${expirationMonth}/${expirationDay}`;
return `${formattedExpirationDate} (${daysDifference})`;
};