## 概要 [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 ## 動作確認状況 - ローカルで動作確認済 ## 補足 - 相談、参考資料などがあれば
330 lines
10 KiB
TypeScript
330 lines
10 KiB
TypeScript
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;
|
||
}
|
||
|
||
// 半角英数字と_の組み合わせで16文字まで
|
||
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})`;
|
||
};
|