From f14c086980b3d4c4677c45b163e0e40f58a83f47 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Thu, 7 Sep 2023 08:58:36 +0000 Subject: [PATCH 1/7] =?UTF-8?q?Merged=20PR=20386:=20=E3=83=AF=E3=83=BC?= =?UTF-8?q?=E3=82=AF=E3=82=BF=E3=82=A4=E3=83=97=E7=B7=A8=E9=9B=86=E3=83=9D?= =?UTF-8?q?=E3=83=83=E3=83=97=E3=82=A2=E3=83=83=E3=83=97=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task2570: ワークタイプ編集ポップアップ実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2570) - ワークタイプ編集ポップアップを実装しました。 - ローディング中処理をタスク追加にも追加しています。 ## レビューポイント - 画面の表示は想定通りか - ローディング中処理の追加に問題はないか ## UIの変更 - [Task2570](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/Task2570?csf=1&web=1&e=bcRjsJ) ## 動作確認状況 - ローカルで確認 --- dictation_client/src/api/api.ts | 251 ++++++++++++++++++ .../features/workflow/worktype/operations.ts | 63 +++++ .../features/workflow/worktype/selectors.ts | 10 + .../src/features/workflow/worktype/state.ts | 3 + .../workflow/worktype/worktypeSlice.ts | 41 ++- .../addWorktypeIdPopup.tsx | 31 ++- .../editWorktypeIdPopup.tsx | 162 +++++++++++ .../src/pages/WorkTypeIdSettingPage/index.tsx | 28 +- dictation_client/src/translation/de.json | 8 +- dictation_client/src/translation/en.json | 8 +- dictation_client/src/translation/es.json | 8 +- dictation_client/src/translation/fr.json | 8 +- 12 files changed, 596 insertions(+), 25 deletions(-) create mode 100644 dictation_client/src/pages/WorkTypeIdSettingPage/editWorktypeIdPopup.tsx diff --git a/dictation_client/src/api/api.ts b/dictation_client/src/api/api.ts index 429eb58..0c2ff5c 100644 --- a/dictation_client/src/api/api.ts +++ b/dictation_client/src/api/api.ts @@ -757,6 +757,25 @@ export interface GetPartnerLicensesResponse { */ 'childrenPartnerLicenses': Array; } +/** + * + * @export + * @interface GetPartnersResponse + */ +export interface GetPartnersResponse { + /** + * 合計件数 + * @type {number} + * @memberof GetPartnersResponse + */ + 'total': number; + /** + * + * @type {Array} + * @memberof GetPartnersResponse + */ + 'partners': Array; +} /** * * @export @@ -1028,6 +1047,55 @@ export interface OptionItemList { */ 'optionItemList': Array; } +/** + * + * @export + * @interface Partner + */ +export interface Partner { + /** + * 会社名 + * @type {string} + * @memberof Partner + */ + 'name': string; + /** + * 階層 + * @type {number} + * @memberof Partner + */ + 'tier': number; + /** + * アカウントID + * @type {number} + * @memberof Partner + */ + 'accountId': number; + /** + * 国 + * @type {string} + * @memberof Partner + */ + 'country': string; + /** + * プライマリ管理者 + * @type {string} + * @memberof Partner + */ + 'primaryAdmin': string; + /** + * プライマリ管理者メールアドレス + * @type {string} + * @memberof Partner + */ + 'email': string; + /** + * 代行操作許可 + * @type {boolean} + * @memberof Partner + */ + 'dealerManagement': boolean; +} /** * * @export @@ -1528,6 +1596,25 @@ export interface UpdateTypistGroupRequest { */ 'typistIds': Array; } +/** + * + * @export + * @interface UpdateWorktypesRequest + */ +export interface UpdateWorktypesRequest { + /** + * WorktypeID + * @type {string} + * @memberof UpdateWorktypesRequest + */ + 'worktypeId': string; + /** + * Worktypeの説明 + * @type {string} + * @memberof UpdateWorktypesRequest + */ + 'description'?: string; +} /** * * @export @@ -2037,6 +2124,54 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat options: localVarRequestOptions, }; }, + /** + * + * @summary + * @param {number} limit 取得件数 + * @param {number} offset 開始位置 + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getPartners: async (limit: number, offset: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'limit' is not null or undefined + assertParamExists('getPartners', 'limit', limit) + // verify required parameter 'offset' is not null or undefined + assertParamExists('getPartners', 'offset', offset) + const localVarPath = `/accounts/partners`; + // 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: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + if (limit !== undefined) { + localVarQueryParameter['limit'] = limit; + } + + if (offset !== undefined) { + localVarQueryParameter['offset'] = offset; + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを取得します * @summary @@ -2256,6 +2391,50 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; localVarRequestOptions.data = serializeDataIfNeeded(updateTypistGroupRequest, localVarRequestOptions, configuration) + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {UpdateWorktypesRequest} updateWorktypesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateWorktype: async (id: number, updateWorktypesRequest: UpdateWorktypesRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('updateWorktype', 'id', id) + // verify required parameter 'updateWorktypesRequest' is not null or undefined + assertParamExists('updateWorktype', 'updateWorktypesRequest', updateWorktypesRequest) + const localVarPath = `/accounts/worktypes/{id}` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // 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(updateWorktypesRequest, localVarRequestOptions, configuration) + return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, @@ -2379,6 +2558,18 @@ export const AccountsApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.getPartnerLicenses(getPartnerLicensesRequest, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @summary + * @param {number} limit 取得件数 + * @param {number} offset 開始位置 + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getPartners(limit: number, offset: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getPartners(limit, offset, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを取得します * @summary @@ -2443,6 +2634,18 @@ export const AccountsApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.updateTypistGroup(typistGroupId, updateTypistGroupRequest, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {UpdateWorktypesRequest} updateWorktypesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updateWorktype(id: number, updateWorktypesRequest: UpdateWorktypesRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updateWorktype(id, updateWorktypesRequest, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, } }; @@ -2551,6 +2754,17 @@ export const AccountsApiFactory = function (configuration?: Configuration, baseP getPartnerLicenses(getPartnerLicensesRequest: GetPartnerLicensesRequest, options?: any): AxiosPromise { return localVarFp.getPartnerLicenses(getPartnerLicensesRequest, options).then((request) => request(axios, basePath)); }, + /** + * + * @summary + * @param {number} limit 取得件数 + * @param {number} offset 開始位置 + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getPartners(limit: number, offset: number, options?: any): AxiosPromise { + return localVarFp.getPartners(limit, offset, options).then((request) => request(axios, basePath)); + }, /** * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを取得します * @summary @@ -2609,6 +2823,17 @@ export const AccountsApiFactory = function (configuration?: Configuration, baseP updateTypistGroup(typistGroupId: number, updateTypistGroupRequest: UpdateTypistGroupRequest, options?: any): AxiosPromise { return localVarFp.updateTypistGroup(typistGroupId, updateTypistGroupRequest, options).then((request) => request(axios, basePath)); }, + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {UpdateWorktypesRequest} updateWorktypesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateWorktype(id: number, updateWorktypesRequest: UpdateWorktypesRequest, options?: any): AxiosPromise { + return localVarFp.updateWorktype(id, updateWorktypesRequest, options).then((request) => request(axios, basePath)); + }, }; }; @@ -2737,6 +2962,19 @@ export class AccountsApi extends BaseAPI { return AccountsApiFp(this.configuration).getPartnerLicenses(getPartnerLicensesRequest, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @summary + * @param {number} limit 取得件数 + * @param {number} offset 開始位置 + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public getPartners(limit: number, offset: number, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).getPartners(limit, offset, options).then((request) => request(this.axios, this.basePath)); + } + /** * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを取得します * @summary @@ -2806,6 +3044,19 @@ export class AccountsApi extends BaseAPI { public updateTypistGroup(typistGroupId: number, updateTypistGroupRequest: UpdateTypistGroupRequest, options?: AxiosRequestConfig) { return AccountsApiFp(this.configuration).updateTypistGroup(typistGroupId, updateTypistGroupRequest, options).then((request) => request(this.axios, this.basePath)); } + + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {UpdateWorktypesRequest} updateWorktypesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public updateWorktype(id: number, updateWorktypesRequest: UpdateWorktypesRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).updateWorktype(id, updateWorktypesRequest, options).then((request) => request(this.axios, this.basePath)); + } } diff --git a/dictation_client/src/features/workflow/worktype/operations.ts b/dictation_client/src/features/workflow/worktype/operations.ts index 5f754c9..4945831 100644 --- a/dictation_client/src/features/workflow/worktype/operations.ts +++ b/dictation_client/src/features/workflow/worktype/operations.ts @@ -110,3 +110,66 @@ export const addWorktypeAsync = createAsyncThunk< return thunkApi.rejectWithValue({ error }); } }); + +export const editWorktypeAsync = createAsyncThunk< + { + // return empty + }, + void, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("workflow/editWorktypeAsync", async (args, thunkApi) => { + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration, accessToken } = state.auth; + const config = new Configuration(configuration); + const accountsApi = new AccountsApi(config); + // stateからworktypeIdとdescriptionを取得する + const { selectedId, worktypeId, description } = state.worktype.apps; + + try { + await accountsApi.updateWorktype( + selectedId, + { + worktypeId, + description, + }, + { + headers: { authorization: `Bearer ${accessToken}` }, + } + ); + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); + + return {}; + } catch (e) { + // e ⇒ errorObjectに変換" + const error = createErrorObject(e); + + let errorMessage = getTranslationID("common.message.internalServerError"); + + // 既に同じworktypeIdが存在する場合 + if (error.code === "E011001") { + errorMessage = getTranslationID( + "worktypeIdSetting.message.alreadyWorktypeIdExistError" + ); + } + + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: errorMessage, + }) + ); + return thunkApi.rejectWithValue({ error }); + } +}); diff --git a/dictation_client/src/features/workflow/worktype/selectors.ts b/dictation_client/src/features/workflow/worktype/selectors.ts index e1ec412..41d3a75 100644 --- a/dictation_client/src/features/workflow/worktype/selectors.ts +++ b/dictation_client/src/features/workflow/worktype/selectors.ts @@ -2,9 +2,19 @@ import { RootState } from "app/store"; export const selectWorktypes = (state: RootState) => state.worktype.domain.worktypes; + export const selectIsLoading = (state: RootState) => state.worktype.apps.isLoading; +export const selectIsAddLoading = (state: RootState) => + state.worktype.apps.isAddLoading; + +export const selectIsEditLoading = (state: RootState) => + state.worktype.apps.isEditLoading; + +export const selectSelectedId = (state: RootState) => + state.worktype.apps.selectedId; + export const selectWorktypeId = (state: RootState) => state.worktype.apps.worktypeId; diff --git a/dictation_client/src/features/workflow/worktype/state.ts b/dictation_client/src/features/workflow/worktype/state.ts index 2e101f7..d2006e2 100644 --- a/dictation_client/src/features/workflow/worktype/state.ts +++ b/dictation_client/src/features/workflow/worktype/state.ts @@ -7,6 +7,9 @@ export interface WorktypeState { export interface Apps { isLoading: boolean; + isAddLoading: boolean; + isEditLoading: boolean; + selectedId: number; worktypeId: string; description?: string; } diff --git a/dictation_client/src/features/workflow/worktype/worktypeSlice.ts b/dictation_client/src/features/workflow/worktype/worktypeSlice.ts index d0345e5..a064ec6 100644 --- a/dictation_client/src/features/workflow/worktype/worktypeSlice.ts +++ b/dictation_client/src/features/workflow/worktype/worktypeSlice.ts @@ -1,11 +1,19 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit"; import { WorktypeState } from "./state"; -import { addWorktypeAsync, listWorktypesAsync } from "./operations"; +import { + addWorktypeAsync, + editWorktypeAsync, + listWorktypesAsync, +} from "./operations"; const initialState: WorktypeState = { apps: { isLoading: false, + isAddLoading: false, + isEditLoading: false, + selectedId: NaN, worktypeId: "", + description: undefined, }, domain: {}, }; @@ -15,8 +23,13 @@ export const worktypeSlice = createSlice({ initialState, reducers: { cleanupWorktype: (state) => { + state.apps.selectedId = initialState.apps.selectedId; state.apps.worktypeId = initialState.apps.worktypeId; - state.apps.description = undefined; + state.apps.description = initialState.apps.description; + }, + changeSelectedId: (state, action: PayloadAction<{ id: number }>) => { + const { id } = action.payload; + state.apps.selectedId = id; }, changeWorktypeId: ( state, @@ -46,13 +59,29 @@ export const worktypeSlice = createSlice({ state.apps.isLoading = false; }); builder.addCase(addWorktypeAsync.pending, (state) => { - state.apps.isLoading = true; + state.apps.isAddLoading = true; }); builder.addCase(addWorktypeAsync.rejected, (state) => { - state.apps.isLoading = false; + state.apps.isAddLoading = false; + }); + builder.addCase(addWorktypeAsync.fulfilled, (state) => { + state.apps.isAddLoading = false; + }); + builder.addCase(editWorktypeAsync.pending, (state) => { + state.apps.isEditLoading = true; + }); + builder.addCase(editWorktypeAsync.rejected, (state) => { + state.apps.isEditLoading = false; + }); + builder.addCase(editWorktypeAsync.fulfilled, (state) => { + state.apps.isEditLoading = false; }); }, }); -export const { changeDescription, changeWorktypeId, cleanupWorktype } = - worktypeSlice.actions; +export const { + changeDescription, + changeWorktypeId, + changeSelectedId, + cleanupWorktype, +} = worktypeSlice.actions; export default worktypeSlice.reducer; diff --git a/dictation_client/src/pages/WorkTypeIdSettingPage/addWorktypeIdPopup.tsx b/dictation_client/src/pages/WorkTypeIdSettingPage/addWorktypeIdPopup.tsx index e611f9f..7f4934f 100644 --- a/dictation_client/src/pages/WorkTypeIdSettingPage/addWorktypeIdPopup.tsx +++ b/dictation_client/src/pages/WorkTypeIdSettingPage/addWorktypeIdPopup.tsx @@ -10,11 +10,13 @@ import { listWorktypesAsync, selectDescription, selectHasErrorWorktypeId, + selectIsAddLoading, selectWorktypeId, } from "features/workflow/worktype"; import { AppDispatch } from "app/store"; import { getTranslationID } from "translation"; import close from "../../assets/images/close.svg"; +import progress_activit from "../../assets/images/progress_activit.svg"; // popupのpropsの型定義 interface AddWorktypeIdPopupProps { @@ -28,6 +30,8 @@ export const AddWorktypeIdPopup: React.FC = ( const { onClose, isOpen } = props; const [t] = useTranslation(); const dispatch: AppDispatch = useDispatch(); + + const isAddLoading = useSelector(selectIsAddLoading); const worktypeId = useSelector(selectWorktypeId); const description = useSelector(selectDescription); // 追加ボタンを押したかどうか @@ -39,10 +43,13 @@ export const AddWorktypeIdPopup: React.FC = ( // ×ボタンを押した時の処理 const closePopup = useCallback(() => { + if (isAddLoading) { + return; + } dispatch(cleanupWorktype()); setIsPushAddButton(false); onClose(); - }, [onClose, dispatch]); + }, [onClose, dispatch, isAddLoading]); // 追加ボタンを押した時の処理 const addWorktypeId = useCallback(async () => { @@ -70,7 +77,7 @@ export const AddWorktypeIdPopup: React.FC = ( onClick={closePopup} />

-
+
{t(getTranslationID("worktypeIdSetting.label.worktypeId"))}
@@ -119,9 +126,12 @@ export const AddWorktypeIdPopup: React.FC = ( value={description ?? ""} className={styles.formInput} onChange={(e) => { - const description = - e.target.value === "" ? undefined : e.target.value; - dispatch(changeDescription({ description })); + dispatch( + changeDescription({ + description: + e.target.value === "" ? undefined : e.target.value, + }) + ); }} /> @@ -132,9 +142,18 @@ export const AddWorktypeIdPopup: React.FC = ( value={t( getTranslationID("worktypeIdSetting.label.addWorktype") )} - className={`${styles.formSubmit} ${styles.marginBtm1} ${styles.isActive}`} + className={`${styles.formSubmit} ${styles.marginBtm1} ${ + !isAddLoading ? styles.isActive : "" + }`} onClick={addWorktypeId} /> + {isAddLoading && ( + Loading + )}
diff --git a/dictation_client/src/pages/WorkTypeIdSettingPage/editWorktypeIdPopup.tsx b/dictation_client/src/pages/WorkTypeIdSettingPage/editWorktypeIdPopup.tsx new file mode 100644 index 0000000..248005b --- /dev/null +++ b/dictation_client/src/pages/WorkTypeIdSettingPage/editWorktypeIdPopup.tsx @@ -0,0 +1,162 @@ +import React, { useCallback, useState } from "react"; +import { useTranslation } from "react-i18next"; +import styles from "styles/app.module.scss"; +import { useDispatch, useSelector } from "react-redux"; +import { + editWorktypeAsync, + changeDescription, + changeWorktypeId, + cleanupWorktype, + listWorktypesAsync, + selectDescription, + selectHasErrorWorktypeId, + selectIsEditLoading, + selectWorktypeId, +} from "features/workflow/worktype"; +import { AppDispatch } from "app/store"; +import { getTranslationID } from "translation"; +import close from "../../assets/images/close.svg"; +import progress_activit from "../../assets/images/progress_activit.svg"; + +// popupのpropsの型定義 +interface EditWorktypeIdPopupProps { + onClose: () => void; + isOpen: boolean; +} + +export const EditWorktypeIdPopup: React.FC = ( + props: EditWorktypeIdPopupProps +): JSX.Element => { + const { onClose, isOpen } = props; + const [t] = useTranslation(); + const dispatch: AppDispatch = useDispatch(); + const isEditLoading = useSelector(selectIsEditLoading); + const worktypeId = useSelector(selectWorktypeId); + const description = useSelector(selectDescription); + // 保存ボタンを押したかどうか + const [isPushSaveButton, setIsPushSaveButton] = useState(false); + // WorktypeIdのバリデーションチェック + const { hasIncorrectPatternWorktypeId, isEmptyWorktypeId } = useSelector( + selectHasErrorWorktypeId + ); + + // ×ボタンを押した時の処理 + const closePopup = useCallback(() => { + if (isEditLoading) { + return; + } + dispatch(cleanupWorktype()); + setIsPushSaveButton(false); + onClose(); + }, [onClose, dispatch, isEditLoading]); + + // 保存ボタンを押した時の処理 + const saveWorktypeId = useCallback(async () => { + setIsPushSaveButton(true); + if (isEmptyWorktypeId || hasIncorrectPatternWorktypeId) { + return; + } + const { meta } = await dispatch(editWorktypeAsync()); + if (meta.requestStatus === "fulfilled") { + dispatch(listWorktypesAsync()); + closePopup(); + } + }, [closePopup, dispatch, hasIncorrectPatternWorktypeId, isEmptyWorktypeId]); + + return ( +
+
+

+ {t(getTranslationID("worktypeIdSetting.label.editWorktypeId"))} + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions */} + close +

+
+
+
+
{t(getTranslationID("worktypeIdSetting.label.worktypeId"))}
+
+ { + dispatch(changeWorktypeId({ worktypeId: e.target.value })); + }} + /> + {isPushSaveButton && isEmptyWorktypeId && ( + + {t(getTranslationID("common.message.inputEmptyError"))} + + )} + {isPushSaveButton && hasIncorrectPatternWorktypeId && ( + + {t( + getTranslationID( + "worktypeIdSetting.message.worktypeIdIncorrectError" + ) + )} + + )} + + {t(getTranslationID("worktypeIdSetting.label.worktypeIdTerms"))} + +
+
+ {t( + getTranslationID("worktypeIdSetting.label.descriptionOptional") + )} +
+
+ { + dispatch( + changeDescription({ + description: + e.target.value === "" ? undefined : e.target.value, + }) + ); + }} + /> +
+
+ + {isEditLoading && ( + Loading + )} +
+
+
+
+
+ ); +}; diff --git a/dictation_client/src/pages/WorkTypeIdSettingPage/index.tsx b/dictation_client/src/pages/WorkTypeIdSettingPage/index.tsx index ef20581..6d3e7cf 100644 --- a/dictation_client/src/pages/WorkTypeIdSettingPage/index.tsx +++ b/dictation_client/src/pages/WorkTypeIdSettingPage/index.tsx @@ -10,12 +10,16 @@ import progress_activit from "assets/images/progress_activit.svg"; import { useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; import { + changeSelectedId, + changeWorktypeId, + changeDescription, listWorktypesAsync, selectIsLoading, selectWorktypes, } from "features/workflow/worktype"; import { AppDispatch } from "app/store"; import { AddWorktypeIdPopup } from "./addWorktypeIdPopup"; +import { EditWorktypeIdPopup } from "./editWorktypeIdPopup"; const WorktypeIdSettingPage: React.FC = (): JSX.Element => { const dispatch: AppDispatch = useDispatch(); @@ -25,6 +29,8 @@ const WorktypeIdSettingPage: React.FC = (): JSX.Element => { const [selectedRow, setSelectedRow] = useState(NaN); // 追加Popupの表示制御 const [isShowAddPopup, setIsShowAddPopup] = useState(false); + // 編集Popupの表示制御 + const [isShowEditPopup, setIsShowEditPopup] = useState(false); useEffect(() => { dispatch(listWorktypesAsync()); }, [dispatch]); @@ -37,6 +43,12 @@ const WorktypeIdSettingPage: React.FC = (): JSX.Element => { }} isOpen={isShowAddPopup} /> + { + setIsShowEditPopup(false); + }} + isOpen={isShowEditPopup} + />
@@ -131,9 +143,23 @@ const WorktypeIdSettingPage: React.FC = (): JSX.Element => { className={`${styles.menuAction} ${styles.inTable}`} >
  • + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */} { + dispatch(changeSelectedId({ id: worktype.id })); + dispatch( + changeWorktypeId({ + worktypeId: worktype.worktypeId, + }) + ); + dispatch( + changeDescription({ + description: worktype.description, + }) + ); + setIsShowEditPopup(true); + }} > {t(getTranslationID("common.label.edit"))} diff --git a/dictation_client/src/translation/de.json b/dictation_client/src/translation/de.json index db871fe..5a7bd67 100644 --- a/dictation_client/src/translation/de.json +++ b/dictation_client/src/translation/de.json @@ -390,8 +390,10 @@ "description": "(de)Description", "descriptionOptional": "(de)Description (Optional)", "optionItem": "(de)Option Item", - "worktypeIdTerms": "(de)WorktypeID should be alphanumeric and symbols,\n but not include: \\ / : * ? “ < > | .", - "addWorktype": "(de)Add Worktype" + "worktypeIdTerms": "(de)WorktypeID should be alphanumeric and symbols,\nbut not include: \\ / : * ? “ < > | .", + "addWorktype": "(de)Add Worktype", + "editWorktypeId": "(de)Edit Worktype ID", + "saveChange": "(de)Save Changes" }, "message": { "worktypeIdIncorrectError": "(de)入力されたWorktypeIDがルールを満たしていません。下記のルールを満たすWorktypeIDを入力してください", @@ -414,4 +416,4 @@ "deleteAccount": "(de)Delete Account" } } -} +} \ No newline at end of file diff --git a/dictation_client/src/translation/en.json b/dictation_client/src/translation/en.json index a041725..63ce4e3 100644 --- a/dictation_client/src/translation/en.json +++ b/dictation_client/src/translation/en.json @@ -390,8 +390,10 @@ "description": "Description", "descriptionOptional": "Description (Optional)", "optionItem": "Option Item", - "worktypeIdTerms": "WorktypeID should be alphanumeric and symbols,\n but not include: \\ / : * ? “ < > | .", - "addWorktype": "Add Worktype" + "worktypeIdTerms": "WorktypeID should be alphanumeric and symbols,\nbut not include: \\ / : * ? “ < > | .", + "addWorktype": "Add Worktype", + "editWorktypeId": "Edit Worktype ID", + "saveChange": "Save Changes" }, "message": { "worktypeIdIncorrectError": "入力されたWorktypeIDがルールを満たしていません。下記のルールを満たすWorktypeIDを入力してください", @@ -414,4 +416,4 @@ "deleteAccount": "Delete Account" } } -} +} \ No newline at end of file diff --git a/dictation_client/src/translation/es.json b/dictation_client/src/translation/es.json index 3fe365a..3edb794 100644 --- a/dictation_client/src/translation/es.json +++ b/dictation_client/src/translation/es.json @@ -390,8 +390,10 @@ "description": "(es)Description", "descriptionOptional": "(es)Description (Optional)", "optionItem": "(es)Option Item", - "worktypeIdTerms": "(es)WorktypeID should be alphanumeric and symbols,\n but not include: \\ / : * ? “ < > | .", - "addWorktype": "(es)Add Worktype" + "worktypeIdTerms": "(es)WorktypeID should be alphanumeric and symbols,\nbut not include: \\ / : * ? “ < > | .", + "addWorktype": "(es)Add Worktype", + "editWorktypeId": "(es)Edit Worktype ID", + "saveChange": "(es)Save Changes" }, "message": { "worktypeIdIncorrectError": "(es)入力されたWorktypeIDがルールを満たしていません。下記のルールを満たすWorktypeIDを入力してください", @@ -414,4 +416,4 @@ "deleteAccount": "(es)Delete Account" } } -} +} \ No newline at end of file diff --git a/dictation_client/src/translation/fr.json b/dictation_client/src/translation/fr.json index 741646d..5ccc10f 100644 --- a/dictation_client/src/translation/fr.json +++ b/dictation_client/src/translation/fr.json @@ -390,8 +390,10 @@ "description": "(fr)Description", "descriptionOptional": "(fr)Description (Optional)", "optionItem": "(fr)Option Item", - "worktypeIdTerms": "(fr)WorktypeID should be alphanumeric and symbols,\n but not include: \\ / : * ? “ < > | .", - "addWorktype": "(fr)Add Worktype" + "worktypeIdTerms": "(fr)WorktypeID should be alphanumeric and symbols,\nbut not include: \\ / : * ? “ < > | .", + "addWorktype": "(fr)Add Worktype", + "editWorktypeId": "(fr)Edit Worktype ID", + "saveChange": "(fr)Save Changes" }, "message": { "worktypeIdIncorrectError": "(fr)入力されたWorktypeIDがルールを満たしていません。下記のルールを満たすWorktypeIDを入力してください", @@ -414,4 +416,4 @@ "deleteAccount": "(fr)Delete Account" } } -} +} \ No newline at end of file From c71cab92cb0fe1e9920231c275bdfb39a31d6ba3 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Fri, 8 Sep 2023 01:38:44 +0000 Subject: [PATCH 2/7] =?UTF-8?q?Merged=20PR=20392:=20API=20IF=E5=AE=9F?= =?UTF-8?q?=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task2591: API IF実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2591) - オプションアイテムの取得/更新APIのIFを実装し、OpenAPIの定義を更新しました。 ## レビューポイント - パスは適切か - パラメータの内容、バリデータは適切か ## UIの変更 - なし ## 動作確認状況 - ローカルで確認 --- dictation_server/src/api/odms/openapi.json | 152 ++++++++++++++++++ .../features/accounts/accounts.controller.ts | 150 ++++++++++++++++- .../src/features/accounts/types/types.ts | 60 +++++++ 3 files changed, 361 insertions(+), 1 deletion(-) diff --git a/dictation_server/src/api/odms/openapi.json b/dictation_server/src/api/odms/openapi.json index da51092..7ecacde 100644 --- a/dictation_server/src/api/odms/openapi.json +++ b/dictation_server/src/api/odms/openapi.json @@ -901,6 +901,120 @@ "security": [{ "bearer": [] }] } }, + "/accounts/worktypes/{id}/option-items": { + "get": { + "operationId": "getOptionItems", + "summary": "", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "description": "Worktypeの内部ID", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOptionItemsResponse" + } + } + } + }, + "400": { + "description": "WorktypeIDが不在", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + }, + "post": { + "operationId": "updateOptionItems", + "summary": "", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "description": "Worktypeの内部ID", + "schema": { "type": "number" } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOptionItemsRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOptionItemsResponse" + } + } + } + }, + "400": { + "description": "WorktypeIDが不在", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, "/accounts/partners": { "get": { "operationId": "getPartners", @@ -2901,6 +3015,44 @@ "required": ["worktypeId"] }, "UpdateWorktypeResponse": { "type": "object", "properties": {} }, + "WorktypeOptionItem": { + "type": "object", + "properties": { + "itemLabel": { "type": "string", "maxLength": 16 }, + "defaultValueType": { + "type": "string", + "maxLength": 20, + "description": "Default / Blank / LastInput" + }, + "initialValue": { "type": "string", "maxLength": 20 } + }, + "required": ["itemLabel", "defaultValueType", "initialValue"] + }, + "GetOptionItemsResponse": { + "type": "object", + "properties": { + "optionItems": { + "maxItems": 10, + "minItems": 10, + "type": "array", + "items": { "$ref": "#/components/schemas/WorktypeOptionItem" } + } + }, + "required": ["optionItems"] + }, + "UpdateOptionItemsRequest": { + "type": "object", + "properties": { + "optionItems": { + "maxItems": 10, + "minItems": 10, + "type": "array", + "items": { "$ref": "#/components/schemas/WorktypeOptionItem" } + } + }, + "required": ["optionItems"] + }, + "UpdateOptionItemsResponse": { "type": "object", "properties": {} }, "Partner": { "type": "object", "properties": { diff --git a/dictation_server/src/features/accounts/accounts.controller.ts b/dictation_server/src/features/accounts/accounts.controller.ts index 93abc48..84c091b 100644 --- a/dictation_server/src/features/accounts/accounts.controller.ts +++ b/dictation_server/src/features/accounts/accounts.controller.ts @@ -51,8 +51,18 @@ import { UpdateWorktypeRequestParam, UpdateWorktypeResponse, UpdateWorktypesRequest, + GetOptionItemsRequestParam, + GetOptionItemsResponse, + UpdateOptionItemsResponse, + UpdateOptionItemsRequestParam, + UpdateOptionItemsRequest, } from './types/types'; -import { USER_ROLES, ADMIN_ROLES, TIERS } from '../../constants'; +import { + USER_ROLES, + ADMIN_ROLES, + TIERS, + OPTION_ITEM_VALUE_TYPE, +} from '../../constants'; import { AuthGuard } from '../../common/guards/auth/authguards'; import { RoleGuard } from '../../common/guards/role/roleguards'; import { retrieveAuthorizationToken } from '../../common/http/helper'; @@ -770,6 +780,144 @@ export class AccountsController { return {}; } + @Get('/worktypes/:id/option-items') + @ApiResponse({ + status: HttpStatus.OK, + type: GetOptionItemsResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: 'WorktypeIDが不在', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: '認証エラー', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ operationId: 'getOptionItems' }) + @ApiBearerAuth() + @UseGuards(AuthGuard) + @UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] })) + async getOptionItems( + @Req() req: Request, + @Param() param: GetOptionItemsRequestParam, + ): Promise { + const { id } = param; + const token = retrieveAuthorizationToken(req); + const { userId } = jwt.decode(token, { json: true }) as AccessToken; + + const context = makeContext(userId); + + console.log('id', id); + console.log(context.trackingId); + + return { + optionItems: [ + { + itemLabel: '', + defaultValueType: OPTION_ITEM_VALUE_TYPE.DEFAULT, + initialValue: '', + }, + { + itemLabel: '', + defaultValueType: OPTION_ITEM_VALUE_TYPE.DEFAULT, + initialValue: '', + }, + { + itemLabel: '', + defaultValueType: OPTION_ITEM_VALUE_TYPE.DEFAULT, + initialValue: '', + }, + { + itemLabel: '', + defaultValueType: OPTION_ITEM_VALUE_TYPE.DEFAULT, + initialValue: '', + }, + { + itemLabel: '', + defaultValueType: OPTION_ITEM_VALUE_TYPE.DEFAULT, + initialValue: '', + }, + { + itemLabel: '', + defaultValueType: OPTION_ITEM_VALUE_TYPE.DEFAULT, + initialValue: '', + }, + { + itemLabel: '', + defaultValueType: OPTION_ITEM_VALUE_TYPE.DEFAULT, + initialValue: '', + }, + { + itemLabel: '', + defaultValueType: OPTION_ITEM_VALUE_TYPE.DEFAULT, + initialValue: '', + }, + { + itemLabel: '', + defaultValueType: OPTION_ITEM_VALUE_TYPE.DEFAULT, + initialValue: '', + }, + { + itemLabel: '', + defaultValueType: OPTION_ITEM_VALUE_TYPE.DEFAULT, + initialValue: '', + }, + ], + }; + } + + @Post('/worktypes/:id/option-items') + @ApiResponse({ + status: HttpStatus.OK, + type: UpdateOptionItemsResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: 'WorktypeIDが不在', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: '認証エラー', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ operationId: 'updateOptionItems' }) + @ApiBearerAuth() + @UseGuards(AuthGuard) + @UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] })) + async updateOptionItems( + @Req() req: Request, + @Param() param: UpdateOptionItemsRequestParam, + @Body() body: UpdateOptionItemsRequest, + ): Promise { + const { optionItems } = body; + const { id } = param; + const token = retrieveAuthorizationToken(req); + const { userId } = jwt.decode(token, { json: true }) as AccessToken; + + const context = makeContext(userId); + + console.log('id', id); + console.log('optionItems', optionItems); + console.log(context.trackingId); + + return {}; + } + @Get('/partners') @ApiResponse({ status: HttpStatus.OK, diff --git a/dictation_server/src/features/accounts/types/types.ts b/dictation_server/src/features/accounts/types/types.ts index 9679586..d63013c 100644 --- a/dictation_server/src/features/accounts/types/types.ts +++ b/dictation_server/src/features/accounts/types/types.ts @@ -9,11 +9,15 @@ import { ArrayMinSize, MinLength, IsArray, + IsIn, + ArrayMaxSize, + ValidateNested, } from 'class-validator'; import { IsAdminPasswordvalid } from '../../../common/validators/admin.validator'; import { IsUnique } from '../../../common/validators/IsUnique.validator'; import { Type } from 'class-transformer'; import { IsWorktypeId } from '../../../common/validators/worktype.validator'; +import { OPTION_ITEM_VALUE_TYPE } from '../../../constants'; export class CreateAccountRequest { @ApiProperty() @@ -376,6 +380,62 @@ export class UpdateWorktypesRequest { export class UpdateWorktypeResponse {} +export class WorktypeOptionItem { + @ApiProperty({ maxLength: 16 }) + @MaxLength(16) + itemLabel: string; + @ApiProperty({ + maxLength: 20, + description: `${Object.values(OPTION_ITEM_VALUE_TYPE).join(' / ')}`, + }) + @MaxLength(20) + @IsIn(Object.values(OPTION_ITEM_VALUE_TYPE)) + defaultValueType: string; + @ApiProperty({ maxLength: 20 }) + @MaxLength(20) + initialValue: string; +} +export class GetOptionItemsResponse { + @ApiProperty({ + maxItems: 10, + minItems: 10, + type: [WorktypeOptionItem], + }) + optionItems: WorktypeOptionItem[]; +} + +export class GetOptionItemsRequestParam { + @ApiProperty({ description: 'Worktypeの内部ID' }) + @Type(() => Number) + @IsInt() + @Min(0) + id: number; +} + +export class UpdateOptionItemsRequest { + @ApiProperty({ + maxItems: 10, + minItems: 10, + type: [WorktypeOptionItem], + }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => WorktypeOptionItem) + @ArrayMinSize(10) + @ArrayMaxSize(10) + optionItems: WorktypeOptionItem[]; +} + +export class UpdateOptionItemsResponse {} + +export class UpdateOptionItemsRequestParam { + @ApiProperty({ description: 'Worktypeの内部ID' }) + @Type(() => Number) + @IsInt() + @Min(0) + id: number; +} + export class UpdateWorktypeRequestParam { @ApiProperty({ description: 'Worktypeの内部ID' }) @Type(() => Number) From da1ee8a9c282a1eb84577462acae86179471996d Mon Sep 17 00:00:00 2001 From: "saito.k" Date: Fri, 8 Sep 2023 05:01:24 +0000 Subject: [PATCH 3/7] =?UTF-8?q?Merged=20PR=20391:=20[Sp17=E7=9D=80?= =?UTF-8?q?=E6=89=8B]=E3=82=BB=E3=83=AC=E3=82=AF=E3=83=88=E3=83=9C?= =?UTF-8?q?=E3=83=83=E3=82=AF=E3=82=B9=E3=81=A7=E9=95=B7=E3=81=84=E6=96=87?= =?UTF-8?q?=E5=AD=97=E5=88=97=E3=81=8C=E5=85=A5=E3=81=A3=E3=81=A6=E3=82=82?= =?UTF-8?q?=E5=95=8F=E9=A1=8C=E3=81=AA=E3=81=84=E3=82=88=E3=81=86=E3=81=AB?= =?UTF-8?q?=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task2550: [Sp17着手]セレクトボックスで長い文字列が入っても問題ないようにする](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2550) - タイトルの通り ## レビューポイント - 特になし ## 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/Task2550?csf=1&web=1&e=EcQEIB ## 動作確認状況 - ローカルで確認 ## 補足 - 相談、参考資料などがあれば --- dictation_client/src/pages/SignupPage/signupInput.tsx | 2 +- dictation_client/src/styles/app.module.scss | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/dictation_client/src/pages/SignupPage/signupInput.tsx b/dictation_client/src/pages/SignupPage/signupInput.tsx index fd9bef5..296d559 100644 --- a/dictation_client/src/pages/SignupPage/signupInput.tsx +++ b/dictation_client/src/pages/SignupPage/signupInput.tsx @@ -144,7 +144,7 @@ const SignupInput: React.FC = (): JSX.Element => { {t(getTranslationID("signupPage.text.pageExplanation"))}

  • -
    +
    diff --git a/dictation_client/src/styles/app.module.scss b/dictation_client/src/styles/app.module.scss index 995e34a..e42918c 100644 --- a/dictation_client/src/styles/app.module.scss +++ b/dictation_client/src/styles/app.module.scss @@ -555,6 +555,9 @@ h3 + .brCrumb .tlIcon { border-top: 2px #282828 solid; background: #fafafa; } +.form select { + text-overflow: ellipsis; +} .form select:invalid { color: #999999; } @@ -689,6 +692,7 @@ h3 + .brCrumb .tlIcon { padding: 0.6rem 0.6rem; background: #f0f0f0; box-sizing: border-box; + word-wrap: break-word; } .formSubmit { min-width: 15rem; @@ -2288,10 +2292,11 @@ tr.isSelected .menuInTable li a { font-size: 0.9rem; } .workflow .menuAction.worktype .formInput { - width: inherit; + max-width: 350px; margin-left: 0.5rem; padding: 0.2rem 0.8rem; font-size: 0.9rem; + text-overflow: ellipsis; } .formList dd.formChange { From 82fb224d672913b54f3abe80e3a8643283d660f1 Mon Sep 17 00:00:00 2001 From: "maruyama.t" Date: Fri, 8 Sep 2023 05:27:52 +0000 Subject: [PATCH 4/7] =?UTF-8?q?Merged=20PR=20390:=20[Sp17=E7=9D=80?= =?UTF-8?q?=E6=89=8B]=E3=83=9C=E3=82=BF=E3=83=B3=E6=8A=BC=E4=B8=8B?= =?UTF-8?q?=E6=99=82=E5=87=A6=E7=90=86=E3=81=ABpreventDefault()=E3=82=92?= =?UTF-8?q?=E8=A1=8C=E3=81=86=E3=82=82=E3=81=AE=E3=81=A8=E3=81=8A=E3=81=93?= =?UTF-8?q?=E3=81=AA=E3=82=8F=E3=81=AA=E3=81=84=E3=82=82=E3=81=AE=E3=81=8C?= =?UTF-8?q?=E6=B7=B7=E5=9C=A8=E3=81=99=E3=82=8B=E3=81=AE=E3=82=92=E6=95=B4?= =?UTF-8?q?=E7=90=86=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task2547: [Sp17着手]ボタン押下時処理にpreventDefault()を行うものとおこなわないものが混在するのを整理する](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2547) - 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず) - 何をどう変更したか、追加したライブラリなど パートナー追加ポップアップで処理実行中にでブラウザを閉じようとしたときに確認ダイアログを表示していた箇所を削除(コピペ元からの削除漏れ) ライセンス注文履歴画面で、ボタン押下時に不要なpreventDefaultを行っていた箇所を削除 - このPull Requestでの対象/対象外 - 影響範囲(他の機能にも影響があるか) ## レビューポイント - 特にレビューしてほしい箇所 - 軽微なものや自明なものは記載不要 - 修正範囲が大きい場合などに記載 - 全体的にや仕様を満たしているか等は本当に必要な時のみ記載 ## UIの変更 - Before/Afterのスクショなど - スクショ置き場 ## 動作確認状況 - ローカルで確認 ## 補足 - 相談、参考資料などがあれば --- .../LicensePage/cardLicenseActivatePopup.tsx | 18 ------------------ .../LicensePage/cardLicenseIssuePopup.tsx | 2 +- .../pages/LicensePage/licenseOrderHistory.tsx | 6 ++---- .../PartnerPage/addPartnerAccountPopup.tsx | 19 +------------------ 4 files changed, 4 insertions(+), 41 deletions(-) diff --git a/dictation_client/src/pages/LicensePage/cardLicenseActivatePopup.tsx b/dictation_client/src/pages/LicensePage/cardLicenseActivatePopup.tsx index 1d2909e..6b7b79a 100644 --- a/dictation_client/src/pages/LicensePage/cardLicenseActivatePopup.tsx +++ b/dictation_client/src/pages/LicensePage/cardLicenseActivatePopup.tsx @@ -46,24 +46,6 @@ export const CardLicenseActivatePopup: React.FC< 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を実行することで、ポップアップのアンマウント時に初期化を行う diff --git a/dictation_client/src/pages/LicensePage/cardLicenseIssuePopup.tsx b/dictation_client/src/pages/LicensePage/cardLicenseIssuePopup.tsx index 84b466d..9298a50 100644 --- a/dictation_client/src/pages/LicensePage/cardLicenseIssuePopup.tsx +++ b/dictation_client/src/pages/LicensePage/cardLicenseIssuePopup.tsx @@ -31,7 +31,7 @@ export const CardLicenseIssuePopup: React.FC = ( // ブラウザのウィンドウが閉じられようとしている場合に発火するイベントハンドラ const handleBeforeUnload = (e: BeforeUnloadEvent) => { - // isLoadingがtrueの場合は確認ダイアログを表示する + // 後続の処理で、ブラウザのCSVダウンロードを行うため、ダイアログを表示させる if (isLoading) { e.preventDefault(); // ChromeではreturnValueが必要 diff --git a/dictation_client/src/pages/LicensePage/licenseOrderHistory.tsx b/dictation_client/src/pages/LicensePage/licenseOrderHistory.tsx index cc6d2ca..aee1606 100644 --- a/dictation_client/src/pages/LicensePage/licenseOrderHistory.tsx +++ b/dictation_client/src/pages/LicensePage/licenseOrderHistory.tsx @@ -291,8 +291,7 @@ export const LicenseOrderHistory: React.FC = ( ? styles.isActive : "" }`} - onClick={(event) => { - event.preventDefault(); + onClick={() => { issueLicense(x.poNumber); }} > @@ -312,8 +311,7 @@ export const LicenseOrderHistory: React.FC = ( ? styles.isActive : "" }`} - onClick={(event) => { - event.preventDefault(); + onClick={() => { onCancelIssue( selectedRow.accountId, x.poNumber diff --git a/dictation_client/src/pages/PartnerPage/addPartnerAccountPopup.tsx b/dictation_client/src/pages/PartnerPage/addPartnerAccountPopup.tsx index 565bfe7..43453b8 100644 --- a/dictation_client/src/pages/PartnerPage/addPartnerAccountPopup.tsx +++ b/dictation_client/src/pages/PartnerPage/addPartnerAccountPopup.tsx @@ -51,24 +51,7 @@ export const AddPartnerAccountPopup: React.FC = ( const email = useSelector(selectEmail); const isLoading = useSelector(selectIsLoading); - // ブラウザのウィンドウが閉じられようとしている場合に発火するイベントハンドラ - const handleBeforeUnload = (e: BeforeUnloadEvent) => { - // isLoadingがtrueの場合は確認ダイアログを表示する - if (isLoading) { - e.preventDefault(); - // ChromeではreturnValueが必要 - e.returnValue = ""; - } - }; - // コンポーネントがマウントされた時にイベントハンドラを登録する - useEffect(() => { - window.addEventListener("beforeunload", handleBeforeUnload); - // コンポーネントがアンマウントされるときにイベントハンドラを解除する - return () => { - window.removeEventListener("beforeunload", handleBeforeUnload); - }; - }); - + // ポップアップを閉じる処理 const closePopup = useCallback(() => { if (isLoading) { return; From 2812bc3d20ec187ce4267cfcb0f4e5bc41158bbe Mon Sep 17 00:00:00 2001 From: "oura.a" Date: Fri, 8 Sep 2023 09:45:10 +0000 Subject: [PATCH 5/7] =?UTF-8?q?Merged=20PR=20383:=20API=E5=AE=9F=E8=A3=85?= =?UTF-8?q?=EF=BC=88=E3=83=91=E3=83=BC=E3=83=88=E3=83=8A=E3=83=BC=E5=8F=96?= =?UTF-8?q?=E5=BE=97API=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task2540: API実装(パートナー取得API)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2540) パートナー取得APIを実装しました。 ## レビューポイント ・データ取得方法が適切かどうか。 以下の優先順位を意識して作成したが適切か?また、意識できていない実装になっていないか? ①QueryBuilderを使用せずに処理する ②RDB、adb2cへのアクセス回数を最小限にする ## UIの変更 なし ## 動作確認状況 ローカルで動作確認済み、UT実施済み ## 補足 プライマリ、セカンダリ管理者IDがない場合のテストはUTでは実装せず、ローカルでの動作確認で正常に動作することを確認しました。 (プライマリ、セカンダリ管理者IDを指定してアカウントを作成するテストユーティリティを作成する必要があるが、あまり汎用的には思えず作成する手間が惜しかったため) --- dictation_server/src/common/test/overrides.ts | 11 ++ dictation_server/src/common/test/utility.ts | 9 +- dictation_server/src/constants/index.ts | 8 + .../features/accounts/accounts.controller.ts | 46 +----- .../accounts/accounts.service.spec.ts | 143 ++++++++++++++++++ .../src/features/accounts/accounts.service.ts | 84 +++++++++- .../src/features/accounts/types/types.ts | 10 ++ .../features/users/test/users.service.mock.ts | 7 +- .../src/features/users/users.service.ts | 3 +- .../src/gateways/adb2c/adb2c.service.ts | 3 +- .../accounts/accounts.repository.service.ts | 80 ++++++++++ 11 files changed, 358 insertions(+), 46 deletions(-) diff --git a/dictation_server/src/common/test/overrides.ts b/dictation_server/src/common/test/overrides.ts index ec2b531..014c7e1 100644 --- a/dictation_server/src/common/test/overrides.ts +++ b/dictation_server/src/common/test/overrides.ts @@ -9,6 +9,7 @@ import { UsersRepositoryService } from '../../repositories/users/users.repositor import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service'; import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service'; import { Account } from '../../repositories/accounts/entity/account.entity'; +import { AdB2cUser } from '../../gateways/adb2c/types/types'; // ### ユニットテスト用コード以外では絶対に使用してはいけないダーティな手段を使用しているが、他の箇所では使用しないこと ### @@ -28,6 +29,10 @@ export const overrideAdB2cService = ( username: string, ) => Promise<{ sub: string } | ConflictError>; deleteUser?: (externalId: string, context: Context) => Promise; + getUsers?: ( + context: Context, + externalIds: string[], + ) => Promise; }, ): void => { // テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得 @@ -44,6 +49,12 @@ export const overrideAdB2cService = ( writable: true, }); } + if (overrides.getUsers) { + Object.defineProperty(obj, obj.getUsers.name, { + value: overrides.getUsers, + writable: true, + }); + } }; /** diff --git a/dictation_server/src/common/test/utility.ts b/dictation_server/src/common/test/utility.ts index a2f1005..a5c998b 100644 --- a/dictation_server/src/common/test/utility.ts +++ b/dictation_server/src/common/test/utility.ts @@ -149,6 +149,8 @@ export const makeTestAccount = async ( datasource: DataSource, defaultAccountValue?: AccountDefault, defaultAdminUserValue?: UserDefault, + isPrimaryAdminNotExist?: boolean, + isSecondaryAdminNotExist?: boolean, ): Promise<{ account: Account; admin: User }> => { let accountId: number; let userId: number; @@ -198,10 +200,15 @@ export const makeTestAccount = async ( } // Accountの管理者を設定する + let secondaryAdminUserId = null; + if (isPrimaryAdminNotExist && !isSecondaryAdminNotExist) { + secondaryAdminUserId = userId; + } await datasource.getRepository(Account).update( { id: accountId }, { - primary_admin_user_id: userId, + primary_admin_user_id: isPrimaryAdminNotExist ? null : userId, + secondary_admin_user_id: secondaryAdminUserId, }, ); diff --git a/dictation_server/src/constants/index.ts b/dictation_server/src/constants/index.ts index ca38ec5..66471b9 100644 --- a/dictation_server/src/constants/index.ts +++ b/dictation_server/src/constants/index.ts @@ -238,3 +238,11 @@ export const OPTION_ITEM_VALUE_TYPE = { BLANK: 'Blank', LAST_INPUT: 'LastInput', } as const; + +/** + * ADB2Cユーザのidentity.signInType + * @const {string[]} + */ +export const ADB2C_SIGN_IN_TYPE = { + EAMILADDRESS: 'emailAddress', +} as const; diff --git a/dictation_server/src/features/accounts/accounts.controller.ts b/dictation_server/src/features/accounts/accounts.controller.ts index 84c091b..7099620 100644 --- a/dictation_server/src/features/accounts/accounts.controller.ts +++ b/dictation_server/src/features/accounts/accounts.controller.ts @@ -957,45 +957,13 @@ export class AccountsController { const { userId } = jwt.decode(token, { json: true }) as AccessToken; const context = makeContext(userId); - // TODO: パートナー取得APIで実装 - // await this.accountService.getPartners( - // context, - // body.limit, - // body.offset, - // ); + const response = await this.accountService.getPartners( + context, + userId, + limit, + offset, + ); - // 仮のreturn - return { - total: 1, - partners: [ - { - name: 'testA', - tier: 5, - accountId: 1, - country: 'US', - primaryAdmin: 'nameA', - email: 'aaa@example.com', - dealerManagement: true, - }, - { - name: 'testB', - tier: 5, - accountId: 2, - country: 'US', - primaryAdmin: 'nameB', - email: 'bbb@example.com', - dealerManagement: false, - }, - { - name: 'testC', - tier: 5, - accountId: 1, - country: 'US', - primaryAdmin: 'nothing', - email: 'nothing', - dealerManagement: false, - }, - ], - }; + return response; } } diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index 618abf9..6932651 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -37,6 +37,7 @@ import { import { AccountsService } from './accounts.service'; import { Context, makeContext } from '../../common/log'; import { + ADB2C_SIGN_IN_TYPE, LICENSE_ALLOCATED_STATUS, LICENSE_ISSUE_STATUS, LICENSE_TYPE, @@ -61,6 +62,7 @@ import { selectOrderLicense, } from '../licenses/test/utility'; import { WorktypesRepositoryService } from '../../repositories/worktypes/worktypes.repository.service'; +import { AdB2cUser } from '../../gateways/adb2c/types/types'; import { Worktype } from '../../repositories/worktypes/entity/worktype.entity'; describe('createAccount', () => { @@ -4155,3 +4157,144 @@ describe('ライセンス発行キャンセル', () => { ); }); }); + +describe('パートナー一覧取得', () => { + let source: DataSource = null; + beforeEach(async () => { + source = new DataSource({ + type: 'sqlite', + database: ':memory:', + logging: false, + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: true, // trueにすると自動的にmigrationが行われるため注意 + }); + return source.initialize(); + }); + + afterEach(async () => { + await source.destroy(); + source = null; + }); + it('パートナー一覧を取得する', async () => { + const module = await makeTestingModule(source); + const service = module.get(AccountsService); + const { tier1Accounts: tier1Accounts, tier2Accounts: tier2Accounts } = + await makeHierarchicalAccounts(source); + const tier1Difference = await makeTestAccount(source, { + tier: 1, + }); + const tier2_3 = await makeTestAccount( + source, + { + parent_account_id: tier1Accounts[0].account.id, + tier: 2, + }, + {}, + true, + false, + ); + const tier2_4 = await makeTestAccount( + source, + { + parent_account_id: tier1Accounts[0].account.id, + tier: 2, + }, + {}, + true, + true, + ); + + await makeTestAccount(source, { + parent_account_id: tier1Difference.account.id, + tier: 2, + }); + + const adb2cReturn = [ + { + id: tier2Accounts[0].users[0].external_id, + displayName: 'partner1', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EAMILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'partner1@example.com', + }, + ], + }, + { + id: tier2Accounts[1].users[0].external_id, + displayName: 'partner2', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EAMILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'partner2@example.com', + }, + ], + }, + ] as AdB2cUser[]; + + overrideAdB2cService(service, { + getUsers: async (_context: Context, _externalIds: string[]) => { + return adb2cReturn; + }, + }); + + const partners = await service.getPartners( + makeContext('trackingId'), + tier1Accounts[0].users[0].external_id, + 15, + 0, + ); + + // 違うアカウントのパートナーは取得していないこと + expect(partners.total).toBe(4); + // 会社名の昇順に取得できていること + expect(partners.partners[0].name).toBe( + tier2Accounts[1].account.company_name, + ); + expect(partners.partners[1].name).toBe( + tier2Accounts[0].account.company_name, + ); + expect(partners.partners[2].name).toBe(tier2_3.account.company_name); + expect(partners.partners[3].name).toBe(tier2_4.account.company_name); + expect(partners.partners[0].email).toBe('partner2@example.com'); + expect(partners.partners[1].email).toBe('partner1@example.com'); + expect(partners.partners[2].email).toBeUndefined; + expect(partners.partners[3].email).toBeUndefined; + + expect(partners.partners[0].tier).toBe(tier2Accounts[1].account.tier); + expect(partners.partners[0].country).toBe(tier2Accounts[1].account.country); + expect(partners.partners[0].accountId).toBe(tier2Accounts[1].account.id); + expect(partners.partners[0].tier).toBe(tier2Accounts[1].account.tier); + expect(partners.partners[0].primaryAdmin).toBe('partner2'); + expect(partners.partners[0].dealerManagement).toBe( + tier2Accounts[1].account.delegation_permission, + ); + }); + it('パートナー一覧を取得する(パートナーが0件の場合)', async () => { + const module = await makeTestingModule(source); + const service = module.get(AccountsService); + const account = await makeTestAccount(source, { + tier: 1, + }); + + const adb2cReturn = [{}] as AdB2cUser[]; + + overrideAdB2cService(service, { + getUsers: async (_context: Context, _externalIds: string[]) => { + return adb2cReturn; + }, + }); + + const partners = await service.getPartners( + makeContext('trackingId'), + account.admin.external_id, + 15, + 0, + ); + + // 結果が0件で成功となること + expect(partners.total).toBe(0); + }); +}); diff --git a/dictation_server/src/features/accounts/accounts.service.ts b/dictation_server/src/features/accounts/accounts.service.ts index bdfb545..23a9fc2 100644 --- a/dictation_server/src/features/accounts/accounts.service.ts +++ b/dictation_server/src/features/accounts/accounts.service.ts @@ -10,7 +10,7 @@ import { } from '../../gateways/adb2c/adb2c.service'; import { Account } from '../../repositories/accounts/entity/account.entity'; import { User } from '../../repositories/users/entity/user.entity'; -import { TIERS, USER_ROLES } from '../../constants'; +import { TIERS, USER_ROLES, ADB2C_SIGN_IN_TYPE } from '../../constants'; import { makeErrorResponse } from '../../common/error/makeErrorResponse'; import { TypistGroup, @@ -23,6 +23,7 @@ import { GetMyAccountResponse, GetTypistGroupResponse, GetWorktypesResponse, + GetPartnersResponse, } from './types/types'; import { DateWithZeroTime, @@ -1312,4 +1313,85 @@ export class AccountsService { ); } } + + /** + * パートナー一覧を取得します + * @param context + * @param externalId + * @param limit + * @param offset + * @returns GetPartnersResponse + */ + async getPartners( + context: Context, + externalId: string, + limit: number, + offset: number, + ): Promise { + this.logger.log( + `[IN] [${context.trackingId}] ${this.getPartners.name} | params: { ` + + `externalId: ${externalId}, ` + + `limit: ${limit}, ` + + `offset: ${offset}, };`, + ); + + try { + const { account_id: accountId } = + await this.usersRepository.findUserByExternalId(externalId); + + const partners = await this.accountRepository.getPartners( + accountId, + limit, + offset, + ); + + // DBから取得したユーザーの外部IDをもとにADB2Cからユーザーを取得する + let externalIds = partners.partnersInfo.map( + (x) => x.primaryAccountExternalId, + ); + externalIds = externalIds.filter((item) => item !== undefined); + const adb2cUsers = await this.adB2cService.getUsers(context, externalIds); + + // DBから取得した情報とADB2Cから取得した情報をマージ + const response = partners.partnersInfo.map((db) => { + const adb2cUser = adb2cUsers.find( + (adb2c) => db.primaryAccountExternalId === adb2c.id, + ); + + let primaryAdmin = undefined; + let mail = undefined; + if (adb2cUser) { + primaryAdmin = adb2cUser.displayName; + mail = adb2cUser.identities.find( + (identity) => + identity.signInType === ADB2C_SIGN_IN_TYPE.EAMILADDRESS, + ).issuerAssignedId; + } + return { + name: db.name, + tier: db.tier, + accountId: db.accountId, + country: db.country, + primaryAdmin: primaryAdmin, + email: mail, + dealerManagement: db.dealerManagement, + }; + }); + + return { + total: partners.total, + partners: response, + }; + } catch (e) { + this.logger.error(`error=${e}`); + if (e instanceof Error) { + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } finally { + this.logger.log(`[OUT] [${context.trackingId}] ${this.getPartners.name}`); + } + } } diff --git a/dictation_server/src/features/accounts/types/types.ts b/dictation_server/src/features/accounts/types/types.ts index d63013c..216b3cb 100644 --- a/dictation_server/src/features/accounts/types/types.ts +++ b/dictation_server/src/features/accounts/types/types.ts @@ -480,3 +480,13 @@ export class GetPartnersResponse { @ApiProperty({ type: [Partner] }) partners: Partner[]; } + +// RepositoryからPartnerLicenseInfoに関する情報を取得する際の型 +export type PartnerInfoFromDb = { + name: string; + tier: number; + accountId: number; + country: string; + primaryAccountExternalId: string; + dealerManagement: boolean; +}; diff --git a/dictation_server/src/features/users/test/users.service.mock.ts b/dictation_server/src/features/users/test/users.service.mock.ts index 4882ce2..d874872 100644 --- a/dictation_server/src/features/users/test/users.service.mock.ts +++ b/dictation_server/src/features/users/test/users.service.mock.ts @@ -17,6 +17,7 @@ import { TaskListSortableAttribute, } from '../../../common/types/sort'; import { AdB2cUser } from '../../../gateways/adb2c/types/types'; +import { ADB2C_SIGN_IN_TYPE } from '../../../constants'; export type SortCriteriaRepositoryMockValue = { updateSortCriteria: SortCriteria | Error; @@ -403,7 +404,7 @@ const AdB2cMockUsers: AdB2cUser[] = [ displayName: 'test1', identities: [ { - signInType: 'emailAddress', + signInType: ADB2C_SIGN_IN_TYPE.EAMILADDRESS, issuer: 'issuer', issuerAssignedId: 'test1@mail.com', }, @@ -414,7 +415,7 @@ const AdB2cMockUsers: AdB2cUser[] = [ displayName: 'test2', identities: [ { - signInType: 'emailAddress', + signInType: ADB2C_SIGN_IN_TYPE.EAMILADDRESS, issuer: 'issuer', issuerAssignedId: 'test2@mail.com', }, @@ -425,7 +426,7 @@ const AdB2cMockUsers: AdB2cUser[] = [ displayName: 'test3', identities: [ { - signInType: 'emailAddress', + signInType: ADB2C_SIGN_IN_TYPE.EAMILADDRESS, issuer: 'issuer', issuerAssignedId: 'test3@mail.com', }, diff --git a/dictation_server/src/features/users/users.service.ts b/dictation_server/src/features/users/users.service.ts index 0fc460c..112140b 100644 --- a/dictation_server/src/features/users/users.service.ts +++ b/dictation_server/src/features/users/users.service.ts @@ -33,6 +33,7 @@ import { UserNotFoundError, } from '../../repositories/users/errors/types'; import { + ADB2C_SIGN_IN_TYPE, LICENSE_EXPIRATION_THRESHOLD_DAYS, USER_LICENSE_STATUS, USER_ROLES, @@ -470,7 +471,7 @@ export class UsersService { // メールアドレスを取得する const mail = adb2cUser.identities.find( - (identity) => identity.signInType === 'emailAddress', + (identity) => identity.signInType === ADB2C_SIGN_IN_TYPE.EAMILADDRESS, ).issuerAssignedId; let status = USER_LICENSE_STATUS.NORMAL; diff --git a/dictation_server/src/gateways/adb2c/adb2c.service.ts b/dictation_server/src/gateways/adb2c/adb2c.service.ts index 7d8c17b..d1000c5 100644 --- a/dictation_server/src/gateways/adb2c/adb2c.service.ts +++ b/dictation_server/src/gateways/adb2c/adb2c.service.ts @@ -7,6 +7,7 @@ import axios from 'axios'; import { Aadb2cUser, B2cMetadata, JwkSignKey } from '../../common/token'; import { AdB2cResponse, AdB2cUser } from './types/types'; import { Context } from '../../common/log'; +import { ADB2C_SIGN_IN_TYPE } from '../../constants'; export type ConflictError = { reason: 'email'; @@ -74,7 +75,7 @@ export class AdB2cService { }, identities: [ { - signinType: 'emailAddress', + signinType: ADB2C_SIGN_IN_TYPE.EAMILADDRESS, issuer: `${this.tenantName}.onmicrosoft.com`, issuerAssignedId: email, }, diff --git a/dictation_server/src/repositories/accounts/accounts.repository.service.ts b/dictation_server/src/repositories/accounts/accounts.repository.service.ts index cdd882c..eb6523a 100644 --- a/dictation_server/src/repositories/accounts/accounts.repository.service.ts +++ b/dictation_server/src/repositories/accounts/accounts.repository.service.ts @@ -27,6 +27,7 @@ import { import { LicenseSummaryInfo, PartnerLicenseInfoForRepository, + PartnerInfoFromDb, } from '../../features/accounts/types/types'; import { AccountNotFoundError } from './errors/types'; import { @@ -685,4 +686,83 @@ export class AccountsRepositoryService { await licenseRepo.delete({ order_id: targetOrder.id }); }); } + + /** + * アカウントIDをもとに、パートナー一覧を取得する + * @param id + * @param limit + * @param offset + * @returns total: 総件数 + * @returns partners: DBから取得できるパートナー一覧情報 + */ + async getPartners( + id: number, + limit: number, + offset: number, + ): Promise<{ + total: number; + partnersInfo: PartnerInfoFromDb[]; + }> { + return await this.dataSource.transaction(async (entityManager) => { + const accountRepo = entityManager.getRepository(Account); + + // limit/offsetによらない総件数を取得する + const total = await accountRepo.count({ + where: { + parent_account_id: id, + }, + }); + + const partnerAccounts = await accountRepo.find({ + where: { + parent_account_id: id, + }, + order: { + company_name: 'ASC', + }, + take: limit, + skip: offset, + }); + + // ADB2Cから情報を取得するための外部ユーザIDを取得する(念のためプライマリ管理者IDが存在しない場合を考慮) + const primaryUserIds = partnerAccounts.map((x) => { + if (x.primary_admin_user_id) { + return x.primary_admin_user_id; + } else if (x.secondary_admin_user_id) { + return x.secondary_admin_user_id; + } + }); + const userRepo = entityManager.getRepository(User); + const primaryUsers = await userRepo.find({ + where: { + id: In(primaryUserIds), + }, + }); + + // アカウント情報とプライマリ管理者の外部ユーザIDをマージ + const partners = partnerAccounts.map((account) => { + const primaryUser = primaryUsers.find( + (user) => + user.id === account.primary_admin_user_id || + user.id === account.secondary_admin_user_id, + ); + const primaryAccountExternalId = primaryUser + ? primaryUser.external_id + : undefined; + return { + name: account.company_name, + tier: account.tier, + accountId: account.id, + country: account.country, + primaryAccountExternalId: primaryAccountExternalId, + dealerManagement: account.delegation_permission, + }; + }); + + return { + total: total, + partnersInfo: partners, + }; + }); + } } From 28c5704b16fd161a96668e13e26a6d6c47770528 Mon Sep 17 00:00:00 2001 From: "saito.k" Date: Sat, 9 Sep 2023 10:20:32 +0900 Subject: [PATCH 6/7] =?UTF-8?q?redis=E6=8E=A5=E7=B6=9A=E7=A2=BA=E8=AA=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dictation_server/.env.local.example | 5 ++- dictation_server/src/app.module.ts | 2 ++ .../src/features/auth/auth.controller.ts | 8 ++++- .../src/features/auth/auth.module.ts | 3 +- .../src/gateways/redis/redis.module.ts | 36 +++++++++++++++++++ .../src/gateways/redis/redis.service.ts | 30 ++++++++++++++++ 6 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 dictation_server/src/gateways/redis/redis.module.ts create mode 100644 dictation_server/src/gateways/redis/redis.service.ts diff --git a/dictation_server/.env.local.example b/dictation_server/.env.local.example index 679d57e..1acdf09 100644 --- a/dictation_server/.env.local.example +++ b/dictation_server/.env.local.example @@ -24,4 +24,7 @@ STORAGE_ACCOUNT_KEY_AU=XXXXXXXXXXXXXXXXXXXXXXX STORAGE_ACCOUNT_KEY_EU=XXXXXXXXXXXXXXXXXXXXXXX STORAGE_ACCOUNT_ENDPOINT_US=https://AAAAAAAAAAAAA STORAGE_ACCOUNT_ENDPOINT_AU=https://AAAAAAAAAAAAA -STORAGE_ACCOUNT_ENDPOINT_EU=https://AAAAAAAAAAAAA \ No newline at end of file +STORAGE_ACCOUNT_ENDPOINT_EU=https://AAAAAAAAAAAAA +REDIS_HOST=redis-cache +REDIS_PORT=6379 +REDIS_PASSWORD=omdsredispass \ No newline at end of file diff --git a/dictation_server/src/app.module.ts b/dictation_server/src/app.module.ts index e8b4375..a7f613a 100644 --- a/dictation_server/src/app.module.ts +++ b/dictation_server/src/app.module.ts @@ -42,6 +42,7 @@ import { SortCriteriaRepositoryModule } from './repositories/sort_criteria/sort_ import { TemplateFilesRepositoryModule } from './repositories/template_files/template_files.repository.module'; import { WorktypesRepositoryModule } from './repositories/worktypes/worktypes.repository.module'; import { OptionItemsRepositoryModule } from './repositories/option_items/option_items.repository.module'; +import { RedisModule } from './gateways/redis/redis.module'; @Module({ imports: [ @@ -98,6 +99,7 @@ import { OptionItemsRepositoryModule } from './repositories/option_items/option_ SortCriteriaRepositoryModule, WorktypesRepositoryModule, OptionItemsRepositoryModule, + RedisModule, ], controllers: [ HealthController, diff --git a/dictation_server/src/features/auth/auth.controller.ts b/dictation_server/src/features/auth/auth.controller.ts index 83b75de..52bfe8d 100644 --- a/dictation_server/src/features/auth/auth.controller.ts +++ b/dictation_server/src/features/auth/auth.controller.ts @@ -23,13 +23,14 @@ import { import { retrieveAuthorizationToken } from '../../common/http/helper'; import { makeContext } from '../../common/log'; import { v4 as uuidv4 } from 'uuid'; +import { RedisService } from '../../gateways/redis/redis.service'; @ApiTags('auth') @Controller('auth') export class AuthController { constructor( // TODO「タスク 1828: IDトークンを一度しか使えないようにする」で使用する予定 - // private readonly redisService: RedisService, + private readonly redisService: RedisService, private readonly authService: AuthService, ) {} @@ -64,6 +65,8 @@ export class AuthController { HttpStatus.BAD_REQUEST, ); } + console.log('Storing ID tokens in redis'); + await this.redisService.set(body.idToken, 'used'); const context = makeContext(uuidv4()); @@ -77,6 +80,9 @@ export class AuthController { context, refreshToken, ); + const res = await this.redisService.get(body.idToken); + console.log('Obtaining an ID token from redis'); + console.log(res); return { accessToken, diff --git a/dictation_server/src/features/auth/auth.module.ts b/dictation_server/src/features/auth/auth.module.ts index 6db0c3f..00fae6e 100644 --- a/dictation_server/src/features/auth/auth.module.ts +++ b/dictation_server/src/features/auth/auth.module.ts @@ -4,8 +4,9 @@ import { AdB2cModule } from '../../gateways/adb2c/adb2c.module'; import { UsersRepositoryModule } from '../../repositories/users/users.repository.module'; import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; +import { RedisModule } from '../../gateways/redis/redis.module'; @Module({ - imports: [ConfigModule, AdB2cModule, UsersRepositoryModule], + imports: [ConfigModule, AdB2cModule, UsersRepositoryModule, RedisModule], controllers: [AuthController], providers: [AuthService], }) diff --git a/dictation_server/src/gateways/redis/redis.module.ts b/dictation_server/src/gateways/redis/redis.module.ts new file mode 100644 index 0000000..e222aa1 --- /dev/null +++ b/dictation_server/src/gateways/redis/redis.module.ts @@ -0,0 +1,36 @@ +import { CacheModule, Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import * as redisStore from 'cache-manager-redis-store'; +import { RedisService } from './redis.service'; + +@Module({ + imports: [ + CacheModule.registerAsync({ + imports: [ConfigModule], + useFactory: async (configService: ConfigService) => { + if (process.env.STAGE === 'local') { + return { + store: redisStore, + host: configService.get('REDIS_HOST'), + port: configService.get('REDIS_PORT'), + password: configService.get('REDIS_PASSWORD'), + ttl: configService.get('REDIS_TTL'), + }; + } + return { + store: redisStore, + url: `rediss://${configService.get('REDIS_HOST')}:${configService.get( + 'REDIS_PORT', + )}`, + password: configService.get('REDIS_PASSWORD'), + ttl: configService.get('REDIS_TTL'), + tls: {}, + }; + }, + inject: [ConfigService], + }), + ], + providers: [RedisService], + exports: [RedisService], +}) +export class RedisModule {} diff --git a/dictation_server/src/gateways/redis/redis.service.ts b/dictation_server/src/gateways/redis/redis.service.ts new file mode 100644 index 0000000..330a9bc --- /dev/null +++ b/dictation_server/src/gateways/redis/redis.service.ts @@ -0,0 +1,30 @@ +import { + CACHE_MANAGER, + Inject, + Injectable, + InternalServerErrorException, +} from '@nestjs/common'; +import { Cache } from 'cache-manager'; + +// TODO「タスク 1828: IDトークンを一度しか使えないようにする」で本実装する予定 +@Injectable() +export class RedisService { + constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {} + + async set(key: string, value: string): Promise { + try { + await this.cacheManager.set(key, value); + } catch (error) { + throw new InternalServerErrorException(); + } + } + + async get(key: string): Promise { + try { + const value = await this.cacheManager.get(key); + return value; + } catch (error) { + throw new InternalServerErrorException(); + } + } +} From d8d5789f5a7d586f39a330c24c5973f9134b33f1 Mon Sep 17 00:00:00 2001 From: "saito.k" Date: Sun, 10 Sep 2023 11:27:02 +0900 Subject: [PATCH 7/7] =?UTF-8?q?Revert=20"redis=E6=8E=A5=E7=B6=9A=E7=A2=BA?= =?UTF-8?q?=E8=AA=8D"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 28c5704b16fd161a96668e13e26a6d6c47770528. --- dictation_server/.env.local.example | 5 +-- dictation_server/src/app.module.ts | 2 -- .../src/features/auth/auth.controller.ts | 8 +---- .../src/features/auth/auth.module.ts | 3 +- .../src/gateways/redis/redis.module.ts | 36 ------------------- .../src/gateways/redis/redis.service.ts | 30 ---------------- 6 files changed, 3 insertions(+), 81 deletions(-) delete mode 100644 dictation_server/src/gateways/redis/redis.module.ts delete mode 100644 dictation_server/src/gateways/redis/redis.service.ts diff --git a/dictation_server/.env.local.example b/dictation_server/.env.local.example index 1acdf09..679d57e 100644 --- a/dictation_server/.env.local.example +++ b/dictation_server/.env.local.example @@ -24,7 +24,4 @@ STORAGE_ACCOUNT_KEY_AU=XXXXXXXXXXXXXXXXXXXXXXX STORAGE_ACCOUNT_KEY_EU=XXXXXXXXXXXXXXXXXXXXXXX STORAGE_ACCOUNT_ENDPOINT_US=https://AAAAAAAAAAAAA STORAGE_ACCOUNT_ENDPOINT_AU=https://AAAAAAAAAAAAA -STORAGE_ACCOUNT_ENDPOINT_EU=https://AAAAAAAAAAAAA -REDIS_HOST=redis-cache -REDIS_PORT=6379 -REDIS_PASSWORD=omdsredispass \ No newline at end of file +STORAGE_ACCOUNT_ENDPOINT_EU=https://AAAAAAAAAAAAA \ No newline at end of file diff --git a/dictation_server/src/app.module.ts b/dictation_server/src/app.module.ts index a7f613a..e8b4375 100644 --- a/dictation_server/src/app.module.ts +++ b/dictation_server/src/app.module.ts @@ -42,7 +42,6 @@ import { SortCriteriaRepositoryModule } from './repositories/sort_criteria/sort_ import { TemplateFilesRepositoryModule } from './repositories/template_files/template_files.repository.module'; import { WorktypesRepositoryModule } from './repositories/worktypes/worktypes.repository.module'; import { OptionItemsRepositoryModule } from './repositories/option_items/option_items.repository.module'; -import { RedisModule } from './gateways/redis/redis.module'; @Module({ imports: [ @@ -99,7 +98,6 @@ import { RedisModule } from './gateways/redis/redis.module'; SortCriteriaRepositoryModule, WorktypesRepositoryModule, OptionItemsRepositoryModule, - RedisModule, ], controllers: [ HealthController, diff --git a/dictation_server/src/features/auth/auth.controller.ts b/dictation_server/src/features/auth/auth.controller.ts index 52bfe8d..83b75de 100644 --- a/dictation_server/src/features/auth/auth.controller.ts +++ b/dictation_server/src/features/auth/auth.controller.ts @@ -23,14 +23,13 @@ import { import { retrieveAuthorizationToken } from '../../common/http/helper'; import { makeContext } from '../../common/log'; import { v4 as uuidv4 } from 'uuid'; -import { RedisService } from '../../gateways/redis/redis.service'; @ApiTags('auth') @Controller('auth') export class AuthController { constructor( // TODO「タスク 1828: IDトークンを一度しか使えないようにする」で使用する予定 - private readonly redisService: RedisService, + // private readonly redisService: RedisService, private readonly authService: AuthService, ) {} @@ -65,8 +64,6 @@ export class AuthController { HttpStatus.BAD_REQUEST, ); } - console.log('Storing ID tokens in redis'); - await this.redisService.set(body.idToken, 'used'); const context = makeContext(uuidv4()); @@ -80,9 +77,6 @@ export class AuthController { context, refreshToken, ); - const res = await this.redisService.get(body.idToken); - console.log('Obtaining an ID token from redis'); - console.log(res); return { accessToken, diff --git a/dictation_server/src/features/auth/auth.module.ts b/dictation_server/src/features/auth/auth.module.ts index 00fae6e..6db0c3f 100644 --- a/dictation_server/src/features/auth/auth.module.ts +++ b/dictation_server/src/features/auth/auth.module.ts @@ -4,9 +4,8 @@ import { AdB2cModule } from '../../gateways/adb2c/adb2c.module'; import { UsersRepositoryModule } from '../../repositories/users/users.repository.module'; import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; -import { RedisModule } from '../../gateways/redis/redis.module'; @Module({ - imports: [ConfigModule, AdB2cModule, UsersRepositoryModule, RedisModule], + imports: [ConfigModule, AdB2cModule, UsersRepositoryModule], controllers: [AuthController], providers: [AuthService], }) diff --git a/dictation_server/src/gateways/redis/redis.module.ts b/dictation_server/src/gateways/redis/redis.module.ts deleted file mode 100644 index e222aa1..0000000 --- a/dictation_server/src/gateways/redis/redis.module.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { CacheModule, Module } from '@nestjs/common'; -import { ConfigModule, ConfigService } from '@nestjs/config'; -import * as redisStore from 'cache-manager-redis-store'; -import { RedisService } from './redis.service'; - -@Module({ - imports: [ - CacheModule.registerAsync({ - imports: [ConfigModule], - useFactory: async (configService: ConfigService) => { - if (process.env.STAGE === 'local') { - return { - store: redisStore, - host: configService.get('REDIS_HOST'), - port: configService.get('REDIS_PORT'), - password: configService.get('REDIS_PASSWORD'), - ttl: configService.get('REDIS_TTL'), - }; - } - return { - store: redisStore, - url: `rediss://${configService.get('REDIS_HOST')}:${configService.get( - 'REDIS_PORT', - )}`, - password: configService.get('REDIS_PASSWORD'), - ttl: configService.get('REDIS_TTL'), - tls: {}, - }; - }, - inject: [ConfigService], - }), - ], - providers: [RedisService], - exports: [RedisService], -}) -export class RedisModule {} diff --git a/dictation_server/src/gateways/redis/redis.service.ts b/dictation_server/src/gateways/redis/redis.service.ts deleted file mode 100644 index 330a9bc..0000000 --- a/dictation_server/src/gateways/redis/redis.service.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { - CACHE_MANAGER, - Inject, - Injectable, - InternalServerErrorException, -} from '@nestjs/common'; -import { Cache } from 'cache-manager'; - -// TODO「タスク 1828: IDトークンを一度しか使えないようにする」で本実装する予定 -@Injectable() -export class RedisService { - constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {} - - async set(key: string, value: string): Promise { - try { - await this.cacheManager.set(key, value); - } catch (error) { - throw new InternalServerErrorException(); - } - } - - async get(key: string): Promise { - try { - const value = await this.cacheManager.get(key); - return value; - } catch (error) { - throw new InternalServerErrorException(); - } - } -}