From 5305984b1a22bf8cfc90402e1139f029c64aaa64 Mon Sep 17 00:00:00 2001 From: Kentaro Fukunaga Date: Tue, 27 Feb 2024 00:01:02 +0000 Subject: [PATCH] =?UTF-8?q?Merged=20PR=20764:=20=E7=AC=AC=E4=BA=94?= =?UTF-8?q?=E9=9A=8E=E5=B1=A4=E3=83=A9=E3=82=A4=E3=82=BB=E3=83=B3=E3=82=B9?= =?UTF-8?q?=E6=83=85=E5=A0=B1=E7=94=BB=E9=9D=A2=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [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 ## 動作確認状況 - ローカルで動作確認しました。 --- dictation_client/src/api/api.ts | 94 +++++++++++++++++++ .../licenseCardActivateSlice.ts | 12 +++ .../licenseSummary/licenseSummarySlice.ts | 24 ++++- .../license/licenseSummary/operations.ts | 56 +++++++++++ .../license/licenseSummary/selectors.ts | 5 +- .../pages/LicensePage/licenseOrderPopup.tsx | 3 +- .../src/pages/LicensePage/licenseSummary.tsx | 63 ++++++++++++- dictation_client/src/styles/app.module.scss | 12 +++ .../src/styles/app.module.scss.d.ts | 1 + dictation_client/src/translation/de.json | 6 +- dictation_client/src/translation/en.json | 6 +- dictation_client/src/translation/es.json | 6 +- dictation_client/src/translation/fr.json | 6 +- 13 files changed, 284 insertions(+), 10 deletions(-) diff --git a/dictation_client/src/api/api.ts b/dictation_client/src/api/api.ts index 0b3aacf..e166e6f 100644 --- a/dictation_client/src/api/api.ts +++ b/dictation_client/src/api/api.ts @@ -2121,6 +2121,25 @@ export interface UpdateOptionItemsRequest { */ 'optionItems': Array; } +/** + * + * @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 => { + // 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> { + 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 { 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 { + 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 diff --git a/dictation_client/src/features/license/licenseCardActivate/licenseCardActivateSlice.ts b/dictation_client/src/features/license/licenseCardActivate/licenseCardActivateSlice.ts index f4530e7..06781a3 100644 --- a/dictation_client/src/features/license/licenseCardActivate/licenseCardActivateSlice.ts +++ b/dictation_client/src/features/license/licenseCardActivate/licenseCardActivateSlice.ts @@ -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; diff --git a/dictation_client/src/features/license/licenseSummary/licenseSummarySlice.ts b/dictation_client/src/features/license/licenseSummary/licenseSummarySlice.ts index e9d9816..283661a 100644 --- a/dictation_client/src/features/license/licenseSummary/licenseSummarySlice.ts +++ b/dictation_client/src/features/license/licenseSummary/licenseSummarySlice.ts @@ -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; + }); }, }); diff --git a/dictation_client/src/features/license/licenseSummary/operations.ts b/dictation_client/src/features/license/licenseSummary/operations.ts index a5f18f8..f36966f 100644 --- a/dictation_client/src/features/license/licenseSummary/operations.ts +++ b/dictation_client/src/features/license/licenseSummary/operations.ts @@ -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 }); + } +}); diff --git a/dictation_client/src/features/license/licenseSummary/selectors.ts b/dictation_client/src/features/license/licenseSummary/selectors.ts index 79ba5e9..d760a9e 100644 --- a/dictation_client/src/features/license/licenseSummary/selectors.ts +++ b/dictation_client/src/features/license/licenseSummary/selectors.ts @@ -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; diff --git a/dictation_client/src/pages/LicensePage/licenseOrderPopup.tsx b/dictation_client/src/pages/LicensePage/licenseOrderPopup.tsx index 8d46a79..19d14ed 100644 --- a/dictation_client/src/pages/LicensePage/licenseOrderPopup.tsx +++ b/dictation_client/src/pages/LicensePage/licenseOrderPopup.tsx @@ -41,9 +41,10 @@ export const LicenseOrderPopup: React.FC = (props) => { // ポップアップを閉じる処理 const closePopup = useCallback(() => { + if (isLoading) return; setIsPushOrderButton(false); onClose(); - }, [onClose]); + }, [isLoading, onClose]); // 画面からのパラメータ const poNumber = useSelector(selectPoNumber); diff --git a/dictation_client/src/pages/LicensePage/licenseSummary.tsx b/dictation_client/src/pages/LicensePage/licenseSummary.tsx index 0314a33..586dca8 100644 --- a/dictation_client/src/pages/LicensePage/licenseSummary.tsx +++ b/dictation_client/src/pages/LicensePage/licenseSummary.tsx @@ -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 = ( // 代行操作用のトークンを取得する 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 = ( }, [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 = ( } }, [onReturn]); + const onStorageAvailableChange = useCallback( + async (e: React.ChangeEvent) => { + 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 = (
+ {isTier1 && isAdmin && ( +

+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} + +

+ )}
diff --git a/dictation_client/src/styles/app.module.scss b/dictation_client/src/styles/app.module.scss index 0753e75..8e77dfa 100644 --- a/dictation_client/src/styles/app.module.scss +++ b/dictation_client/src/styles/app.module.scss @@ -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%); diff --git a/dictation_client/src/styles/app.module.scss.d.ts b/dictation_client/src/styles/app.module.scss.d.ts index d6ebd8f..656d36a 100644 --- a/dictation_client/src/styles/app.module.scss.d.ts +++ b/dictation_client/src/styles/app.module.scss.d.ts @@ -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"; diff --git a/dictation_client/src/translation/de.json b/dictation_client/src/translation/de.json index a61cd85..9730841 100644 --- a/dictation_client/src/translation/de.json +++ b/dictation_client/src/translation/de.json @@ -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": { diff --git a/dictation_client/src/translation/en.json b/dictation_client/src/translation/en.json index 263b66d..959ea1d 100644 --- a/dictation_client/src/translation/en.json +++ b/dictation_client/src/translation/en.json @@ -188,7 +188,11 @@ "usedSize": "Storage Used", "storageAvailable": "Storage Unavailable (Exceeded Amount)", "licenseLabel": "License", - "storageLabel": "Storage" + "storageLabel": "Storage", + "storageUnavailableCheckbox": "Storage Unavailable" + }, + "message": { + "storageUnavalableSwitchingConfirm": "対象アカウントのストレージ使用制限状態を変更します。よろしいですか?" } }, "licenseOrderPage": { diff --git a/dictation_client/src/translation/es.json b/dictation_client/src/translation/es.json index c63b5c3..f89d531 100644 --- a/dictation_client/src/translation/es.json +++ b/dictation_client/src/translation/es.json @@ -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": { diff --git a/dictation_client/src/translation/fr.json b/dictation_client/src/translation/fr.json index f290e40..b313ce4 100644 --- a/dictation_client/src/translation/fr.json +++ b/dictation_client/src/translation/fr.json @@ -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": {