Merged PR 764: 第五階層ライセンス情報画面実装

## 概要
[Task3709: 第五階層ライセンス情報画面実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3709)

- ストレージ使用可否切り替えの画面実装をしました
- 動作確認中に、既存実装でライセンスオーダーするときとカードライセンスアクティベートするときの操作不能化処理に漏れがあったのを修正しました

## レビューポイント
- Redux周りの実装でお作法に違反しているところがないか。もしくは改善点ないか。
- ライセンス情報表示のAPI結果待ち部分のローディング処理で、最低限の改善にしたが現時点ではこれでよいか?(いつ修正するかも未定だけど、実害はないためひとまずこんな感じで。。。)
    - `licenseSummarySlice.ts` のコメント部分が該当箇所です

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

## 動作確認状況
- ローカルで動作確認しました。
This commit is contained in:
Kentaro Fukunaga 2024-02-27 00:01:02 +00:00
parent c95fb1e1f6
commit 5305984b1a
13 changed files with 284 additions and 10 deletions

View File

@ -2121,6 +2121,25 @@ export interface UpdateOptionItemsRequest {
*/
'optionItems': Array<PostWorktypeOptionItem>;
}
/**
*
* @export
* @interface UpdateRestrictionStatusRequest
*/
export interface UpdateRestrictionStatusRequest {
/**
* ID
* @type {number}
* @memberof UpdateRestrictionStatusRequest
*/
'accountId': number;
/**
* trur:制限をかける
* @type {boolean}
* @memberof UpdateRestrictionStatusRequest
*/
'restricted': boolean;
}
/**
*
* @export
@ -3443,6 +3462,46 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat
options: localVarRequestOptions,
};
},
/**
*
* @summary
* @param {UpdateRestrictionStatusRequest} updateRestrictionStatusRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updateRestrictionStatus: async (updateRestrictionStatusRequest: UpdateRestrictionStatusRequest, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'updateRestrictionStatusRequest' is not null or undefined
assertParamExists('updateRestrictionStatus', 'updateRestrictionStatusRequest', updateRestrictionStatusRequest)
const localVarPath = `/accounts/restriction-status`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(updateRestrictionStatusRequest, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
* IDで指定されたタイピストグループを更新します
* @summary
@ -3888,6 +3947,19 @@ export const AccountsApiFp = function(configuration?: Configuration) {
const operationBasePath = operationServerMap['AccountsApi.updateOptionItems']?.[index]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
},
/**
*
* @summary
* @param {UpdateRestrictionStatusRequest} updateRestrictionStatusRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async updateRestrictionStatus(updateRestrictionStatusRequest: UpdateRestrictionStatusRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.updateRestrictionStatus(updateRestrictionStatusRequest, options);
const index = configuration?.serverIndex ?? 0;
const operationBasePath = operationServerMap['AccountsApi.updateRestrictionStatus']?.[index]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
},
/**
* IDで指定されたタイピストグループを更新します
* @summary
@ -4192,6 +4264,16 @@ export const AccountsApiFactory = function (configuration?: Configuration, baseP
updateOptionItems(id: number, updateOptionItemsRequest: UpdateOptionItemsRequest, options?: any): AxiosPromise<object> {
return localVarFp.updateOptionItems(id, updateOptionItemsRequest, options).then((request) => request(axios, basePath));
},
/**
*
* @summary
* @param {UpdateRestrictionStatusRequest} updateRestrictionStatusRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updateRestrictionStatus(updateRestrictionStatusRequest: UpdateRestrictionStatusRequest, options?: any): AxiosPromise<object> {
return localVarFp.updateRestrictionStatus(updateRestrictionStatusRequest, options).then((request) => request(axios, basePath));
},
/**
* IDで指定されたタイピストグループを更新します
* @summary
@ -4544,6 +4626,18 @@ export class AccountsApi extends BaseAPI {
return AccountsApiFp(this.configuration).updateOptionItems(id, updateOptionItemsRequest, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary
* @param {UpdateRestrictionStatusRequest} updateRestrictionStatusRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof AccountsApi
*/
public updateRestrictionStatus(updateRestrictionStatusRequest: UpdateRestrictionStatusRequest, options?: AxiosRequestConfig) {
return AccountsApiFp(this.configuration).updateRestrictionStatus(updateRestrictionStatusRequest, options).then((request) => request(this.axios, this.basePath));
}
/**
* IDで指定されたタイピストグループを更新します
* @summary

View File

@ -1,5 +1,6 @@
import { createSlice } from "@reduxjs/toolkit";
import { LicenseCardActivateState } from "./state";
import { activateCardLicenseAsync } from "./operations";
const initialState: LicenseCardActivateState = {
apps: {
@ -14,6 +15,17 @@ export const licenseCardActivateSlice = createSlice({
state.apps = initialState.apps;
},
},
extraReducers: (builder) => {
builder.addCase(activateCardLicenseAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(activateCardLicenseAsync.fulfilled, (state) => {
state.apps.isLoading = false;
});
builder.addCase(activateCardLicenseAsync.rejected, (state) => {
state.apps.isLoading = false;
});
},
});
export const { cleanupApps } = licenseCardActivateSlice.actions;

View File

@ -1,6 +1,10 @@
import { createSlice } from "@reduxjs/toolkit";
import { LicenseSummaryState } from "./state";
import { getCompanyNameAsync, getLicenseSummaryAsync } from "./operations";
import {
getCompanyNameAsync,
getLicenseSummaryAsync,
updateRestrictionStatusAsync,
} from "./operations";
const initialState: LicenseSummaryState = {
domain: {
@ -35,12 +39,30 @@ export const licenseSummarySlice = createSlice({
},
},
extraReducers: (builder) => {
builder.addCase(getLicenseSummaryAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(getLicenseSummaryAsync.fulfilled, (state, action) => {
state.domain.licenseSummaryInfo = action.payload;
state.apps.isLoading = false;
});
builder.addCase(getLicenseSummaryAsync.rejected, (state) => {
state.apps.isLoading = false;
});
// 画面側ではgetLicenseSummaryAsyncと並行して呼び出されているため、レーシングを考慮してこちらではisLoadingを更新しない
// 本来は両方の完了を待ってからisLoadingを更新するべきだが、現時点ではスピード重視のためケアしない。
builder.addCase(getCompanyNameAsync.fulfilled, (state, action) => {
state.domain.accountInfo.companyName = action.payload.companyName;
});
builder.addCase(updateRestrictionStatusAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(updateRestrictionStatusAsync.fulfilled, (state) => {
state.apps.isLoading = false;
});
builder.addCase(updateRestrictionStatusAsync.rejected, (state) => {
state.apps.isLoading = false;
});
},
});

View File

@ -8,6 +8,7 @@ import {
GetCompanyNameResponse,
GetLicenseSummaryResponse,
PartnerLicenseInfo,
UpdateRestrictionStatusRequest,
} from "../../../api/api";
import { Configuration } from "../../../api/configuration";
import { ErrorObject, createErrorObject } from "../../../common/errors";
@ -123,3 +124,58 @@ export const getCompanyNameAsync = createAsyncThunk<
return thunkApi.rejectWithValue({ error });
}
});
export const updateRestrictionStatusAsync = createAsyncThunk<
{
/* Empty Object */
},
{
accountId: number;
restricted: boolean;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("accounts/updateRestrictionStatusAsync", async (args, thunkApi) => {
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const accountApi = new AccountsApi(config);
const requestParam: UpdateRestrictionStatusRequest = {
accountId: args.accountId,
restricted: args.restricted,
};
try {
await accountApi.updateRestrictionStatus(requestParam, {
headers: { authorization: `Bearer ${accessToken}` },
});
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
const error = createErrorObject(e);
// このAPIでは個別のエラーメッセージは不要
const errorMessage = getTranslationID("common.message.internalServerError");
thunkApi.dispatch(
openSnackbar({
level: "error",
message: errorMessage,
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -1,10 +1,11 @@
import { RootState } from "app/store";
// 各値はそのまま画面に表示するので、licenseSummaryInfoとして値を取得する
export const selecLicenseSummaryInfo = (state: RootState) =>
export const selectLicenseSummaryInfo = (state: RootState) =>
state.licenseSummary.domain.licenseSummaryInfo;
export const selectCompanyName = (state: RootState) =>
state.licenseSummary.domain.accountInfo.companyName;
export const selectIsLoading = (state: RootState) => state.license;
export const selectIsLoading = (state: RootState) =>
state.licenseSummary.apps.isLoading;

View File

@ -41,9 +41,10 @@ export const LicenseOrderPopup: React.FC<LicenseOrderPopupProps> = (props) => {
// ポップアップを閉じる処理
const closePopup = useCallback(() => {
if (isLoading) return;
setIsPushOrderButton(false);
onClose();
}, [onClose]);
}, [isLoading, onClose]);
// 画面からのパラメータ
const poNumber = useSelector(selectPoNumber);

View File

@ -10,12 +10,16 @@ import { useDispatch, useSelector } from "react-redux";
import {
getCompanyNameAsync,
getLicenseSummaryAsync,
selecLicenseSummaryInfo,
selectLicenseSummaryInfo,
selectCompanyName,
selectIsLoading,
updateRestrictionStatusAsync,
} from "features/license/licenseSummary";
import { selectSelectedRow } from "features/license/partnerLicense";
import { selectDelegationAccessToken } from "features/auth/selectors";
import { DelegationBar } from "components/delegate";
import { TIERS } from "components/auth/constants";
import { isAdminUser, isApproveTier } from "features/auth/utils";
import postAdd from "../../assets/images/post_add.svg";
import history from "../../assets/images/history.svg";
import key from "../../assets/images/key.svg";
@ -40,6 +44,8 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
// 代行操作用のトークンを取得する
const delegationAccessToken = useSelector(selectDelegationAccessToken);
const isLoading = useSelector(selectIsLoading);
// popup制御関係
const [islicenseOrderPopupOpen, setIslicenseOrderPopupOpen] = useState(false);
const [isCardLicenseActivatePopupOpen, setIsCardLicenseActivatePopupOpen] =
@ -62,9 +68,12 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
}, [setIsLicenseOrderHistoryOpen]);
// apiからの値取得関係
const licenseSummaryInfo = useSelector(selecLicenseSummaryInfo);
const licenseSummaryInfo = useSelector(selectLicenseSummaryInfo);
const companyName = useSelector(selectCompanyName);
const isTier1 = isApproveTier([TIERS.TIER1]);
const isAdmin = isAdminUser();
useEffect(() => {
dispatch(getLicenseSummaryAsync({ selectedRow }));
dispatch(getCompanyNameAsync({ selectedRow }));
@ -78,6 +87,35 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
}
}, [onReturn]);
const onStorageAvailableChange = useCallback(
async (e: React.ChangeEvent<HTMLInputElement>) => {
if (
/* eslint-disable-next-line no-alert */
!window.confirm(
t(
getTranslationID(
"LicenseSummaryPage.message.storageUnavalableSwitchingConfirm"
)
)
)
) {
return;
}
const restricted = e.target.checked;
const accountId = selectedRow?.accountId;
// 本関数が実行されるときはselectedRowが存在する前提のため、accountIdが存在しない場合の処理は不要
if (!accountId) return;
const { meta } = await dispatch(
updateRestrictionStatusAsync({ accountId, restricted })
);
if (meta.requestStatus === "fulfilled") {
dispatch(getLicenseSummaryAsync({ selectedRow }));
}
},
[dispatch, selectedRow, t]
);
return (
<>
{/* isPopupOpenがfalseの場合はポップアップのhtmlを生成しないように対応。これによりポップアップは都度生成されて初期化の考慮が減る */}
@ -272,6 +310,27 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
</dl>
</div>
<div>
{isTier1 && isAdmin && (
<p
className={`${styles.checkAvail} ${styles.alignRight}`}
>
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
<label>
<input
type="checkbox"
className={styles.formCheck}
checked={licenseSummaryInfo.isStorageAvailable}
disabled={isLoading}
onChange={onStorageAvailableChange}
/>
{t(
getTranslationID(
"LicenseSummaryPage.label.storageUnavailableCheckbox"
)
)}
</label>
</p>
)}
<dl
className={`${styles.listVertical} ${styles.marginBtm3}`}
>

View File

@ -1857,6 +1857,18 @@ tr.isSelected .menuInTable li a.isDisable {
cursor: pointer;
}
.license .checkAvail {
height: 30px;
padding: 0 0.3rem 0.3rem 0;
margin-top: -30px;
box-sizing: border-box;
}
.license .checkAvail label {
cursor: pointer;
}
.license .checkAvail label .formCheck {
vertical-align: middle;
}
.license .listVertical dd img[src*="circle"] {
filter: brightness(0) saturate(100%) invert(58%) sepia(41%) saturate(5814%)
hue-rotate(143deg) brightness(96%) contrast(101%);

View File

@ -123,6 +123,7 @@ declare const classNames: {
readonly txNormal: "txNormal";
readonly manageIcon: "manageIcon";
readonly manageIconClose: "manageIconClose";
readonly checkAvail: "checkAvail";
readonly history: "history";
readonly cardHistory: "cardHistory";
readonly partner: "partner";

View File

@ -188,7 +188,11 @@
"usedSize": "Gebrauchter Lagerung",
"storageAvailable": "Speicher nicht verfügbar (Menge überschritten)",
"licenseLabel": "Lizenz",
"storageLabel": "Lagerung"
"storageLabel": "Lagerung",
"storageUnavailableCheckbox": "(de)Storage Unavailable"
},
"message": {
"storageUnavalableSwitchingConfirm": "(de)対象アカウントのストレージ使用制限状態を変更します。よろしいですか?"
}
},
"licenseOrderPage": {

View File

@ -188,7 +188,11 @@
"usedSize": "Storage Used",
"storageAvailable": "Storage Unavailable (Exceeded Amount)",
"licenseLabel": "License",
"storageLabel": "Storage"
"storageLabel": "Storage",
"storageUnavailableCheckbox": "Storage Unavailable"
},
"message": {
"storageUnavalableSwitchingConfirm": "対象アカウントのストレージ使用制限状態を変更します。よろしいですか?"
}
},
"licenseOrderPage": {

View File

@ -188,7 +188,11 @@
"usedSize": "Almacenamiento utilizado",
"storageAvailable": "Almacenamiento no disponible (cantidad excedida)",
"licenseLabel": "Licencia",
"storageLabel": "Almacenamiento"
"storageLabel": "Almacenamiento",
"storageUnavailableCheckbox": "(es)Storage Unavailable"
},
"message": {
"storageUnavalableSwitchingConfirm": "(es)対象アカウントのストレージ使用制限状態を変更します。よろしいですか?"
}
},
"licenseOrderPage": {

View File

@ -188,7 +188,11 @@
"usedSize": "Stockage utilisé",
"storageAvailable": "Stockage indisponible (montant dépassée)",
"licenseLabel": "Licence",
"storageLabel": "Stockage"
"storageLabel": "Stockage",
"storageUnavailableCheckbox": "(fr)Storage Unavailable"
},
"message": {
"storageUnavalableSwitchingConfirm": "(fr)対象アカウントのストレージ使用制限状態を変更します。よろしいですか?"
}
},
"licenseOrderPage": {