From d843affe8845297f462701237879de8962332ca4 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Fri, 15 Sep 2023 06:26:37 +0000 Subject: [PATCH 01/13] =?UTF-8?q?Merged=20PR=20413:=20API=E5=AE=9F?= =?UTF-8?q?=E8=A3=85=EF=BC=88ActiveWorktypeID=E6=9B=B4=E6=96=B0API?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task2622: API実装(ActiveWorktypeID更新API)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2622) - ActiveWorkTypeID更新APIとテストを実装しました。 ## レビューポイント - リポジトリの更新処理は適切か - テストケースは適切か ## UIの変更 - なし ## 動作確認状況 - ローカルで確認 --- .../features/accounts/accounts.controller.ts | 4 +- .../accounts/accounts.service.spec.ts | 185 ++++++++++++++++++ .../src/features/accounts/accounts.service.ts | 52 +++++ .../accounts/accounts.repository.service.ts | 28 +++ 4 files changed, 266 insertions(+), 3 deletions(-) diff --git a/dictation_server/src/features/accounts/accounts.controller.ts b/dictation_server/src/features/accounts/accounts.controller.ts index 27a3980..7d6bde3 100644 --- a/dictation_server/src/features/accounts/accounts.controller.ts +++ b/dictation_server/src/features/accounts/accounts.controller.ts @@ -909,9 +909,7 @@ export class AccountsController { const context = makeContext(userId); - console.log('id', id); - console.log(context.trackingId); - + await this.accountService.updateActiveWorktype(context, userId, id); return {}; } diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index 2e30743..1ac8181 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -65,6 +65,7 @@ import { import { WorktypesRepositoryService } from '../../repositories/worktypes/worktypes.repository.service'; import { AdB2cUser } from '../../gateways/adb2c/types/types'; import { Worktype } from '../../repositories/worktypes/entity/worktype.entity'; +import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service'; describe('createAccount', () => { let source: DataSource = null; @@ -4322,6 +4323,190 @@ describe('updateOptionItems', () => { }); }); +describe('updateActiveWorktype', () => { + 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('アカウントのActiveWorktypeIDを指定WorktypeIDに更新できる(NULL⇒ID設定)', async () => { + const module = await makeTestingModule(source); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + + const service = module.get(AccountsService); + const context = makeContext(admin.external_id); + + const worktype = await createWorktype(source, account.id, 'worktype1'); + + //作成したデータを確認 + { + const beforeAccount = await getAccount(source, account.id); + expect(beforeAccount.active_worktype_id).toBe(null); + } + + await service.updateActiveWorktype(context, admin.external_id, worktype.id); + + //実行結果を確認 + { + const { active_worktype_id } = await getAccount(source, account.id); + expect(active_worktype_id).toBe(worktype.id); + } + }); + + it('アカウントのActiveWorktypeIDを指定WorktypeIDに更新できる(別のWorkTypeIDを設定)', async () => { + const module = await makeTestingModule(source); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + + const service = module.get(AccountsService); + const context = makeContext(admin.external_id); + + const worktype1 = await createWorktype( + source, + account.id, + 'worktype1', + 'description1', + true, + ); + const worktype2 = await createWorktype(source, account.id, 'worktype2'); + + //作成したデータを確認 + { + const beforeAccount = await getAccount(source, account.id); + expect(beforeAccount.active_worktype_id).toBe(worktype1.id); + } + + await service.updateActiveWorktype( + context, + admin.external_id, + worktype2.id, + ); + + //実行結果を確認 + { + const { active_worktype_id } = await getAccount(source, account.id); + expect(active_worktype_id).toBe(worktype2.id); + } + }); + + it('自アカウント内に指定されたIDのWorktypeIDが存在しない場合、400エラーとなること(WorkTypeIDが存在しない場合)', async () => { + const module = await makeTestingModule(source); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + + const service = module.get(AccountsService); + const context = makeContext(admin.external_id); + + await createWorktype(source, account.id, 'worktype1'); + + //作成したデータを確認 + { + const beforeAccount = await getAccount(source, account.id); + expect(beforeAccount.active_worktype_id).toBe(null); + } + + try { + await service.updateActiveWorktype(context, admin.external_id, 999); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E011003')); + } else { + fail(); + } + } + }); + + it('自アカウント内に指定されたIDのWorktypeIDが存在しない場合、400エラーとなること(WorkTypeIDが別アカウントの場合)', async () => { + const module = await makeTestingModule(source); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + const { account: otherAccount } = await makeTestAccount(source, { + tier: 5, + }); + + const service = module.get(AccountsService); + const context = makeContext(admin.external_id); + + await createWorktype(source, account.id, 'worktype1'); + await createWorktype(source, otherAccount.id, 'worktype2'); + + //作成したデータを確認 + { + const beforeAccount = await getAccount(source, account.id); + const worktype1 = await getWorktypes(source, account.id); + const worktype2 = await getWorktypes(source, otherAccount.id); + expect(beforeAccount.active_worktype_id).toBe(null); + + expect(worktype1.length).toBe(1); + expect(worktype1[0].custom_worktype_id).toBe('worktype1'); + + expect(worktype2.length).toBe(1); + expect(worktype2[0].custom_worktype_id).toBe('worktype2'); + } + + try { + await service.updateActiveWorktype(context, admin.external_id, 999); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E011003')); + } else { + fail(); + } + } + }); + + it('DBアクセスに失敗した場合、500エラーを返却する', async () => { + const module = await makeTestingModule(source); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + + const service = module.get(AccountsService); + const context = makeContext(admin.external_id); + + await createWorktype(source, account.id, 'worktype1'); + + //作成したデータを確認 + { + const beforeAccount = await getAccount(source, account.id); + expect(beforeAccount.active_worktype_id).toBe(null); + } + + //DBアクセスに失敗するようにする + const accountsRepositoryService = module.get( + AccountsRepositoryService, + ); + accountsRepositoryService.updateActiveWorktypeId = jest + .fn() + .mockRejectedValue('DB failed'); + + try { + await service.updateActiveWorktype(context, admin.external_id, 999); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + }); +}); + describe('ライセンス発行キャンセル', () => { let source: DataSource = null; beforeEach(async () => { diff --git a/dictation_server/src/features/accounts/accounts.service.ts b/dictation_server/src/features/accounts/accounts.service.ts index 024089f..d2465c2 100644 --- a/dictation_server/src/features/accounts/accounts.service.ts +++ b/dictation_server/src/features/accounts/accounts.service.ts @@ -1473,6 +1473,58 @@ export class AccountsService { } } + /** + * ActiveWorktypeの更新 + * @param context + * @param externalId + * @param id ActiveWorktypeの内部ID + * @returns active worktype + */ + async updateActiveWorktype( + context: Context, + externalId: string, + id: number, + ): Promise { + this.logger.log( + `[IN] [${context.trackingId}] ${this.updateActiveWorktype.name} | params: { ` + + `externalId: ${externalId}, ` + + `id: ${id} };`, + ); + try { + // 外部IDをもとにユーザー情報を取得する + const { account_id: accountId } = + await this.usersRepository.findUserByExternalId(externalId); + + // ActiveWorktypeを更新する + await this.accountRepository.updateActiveWorktypeId(accountId, id); + } catch (e) { + this.logger.error(e); + if (e instanceof Error) { + switch (e.constructor) { + // 内部IDで指定されたWorktypeが存在しない場合は400エラーを返す + case WorktypeIdNotFoundError: + throw new HttpException( + makeErrorResponse('E011003'), + HttpStatus.BAD_REQUEST, + ); + default: + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } finally { + this.logger.log( + `[OUT] [${context.trackingId}] ${this.updateActiveWorktype.name}`, + ); + } + } + /** * パートナー一覧を取得します * @param context diff --git a/dictation_server/src/repositories/accounts/accounts.repository.service.ts b/dictation_server/src/repositories/accounts/accounts.repository.service.ts index eb6523a..8ce21f2 100644 --- a/dictation_server/src/repositories/accounts/accounts.repository.service.ts +++ b/dictation_server/src/repositories/accounts/accounts.repository.service.ts @@ -36,6 +36,8 @@ import { CancellationPeriodExpiredError, } from '../licenses/errors/types'; import { DateWithZeroTime } from '../../features/licenses/types/types'; +import { Worktype } from '../worktypes/entity/worktype.entity'; +import { WorktypeIdNotFoundError } from '../worktypes/errors/types'; @Injectable() export class AccountsRepositoryService { @@ -765,4 +767,30 @@ export class AccountsRepositoryService { }; }); } + + /** + * ActiveWorktypeIdを更新する + * @param accountId + * @param id ActiveWorktypeIdの内部ID + * @returns active worktype id + */ + async updateActiveWorktypeId(accountId: number, id: number): Promise { + return await this.dataSource.transaction(async (entityManager) => { + const worktypeRepo = entityManager.getRepository(Worktype); + const accountRepo = entityManager.getRepository(Account); + + // 自アカウント内に指定IDのワークタイプが存在するか確認 + const worktype = await worktypeRepo.findOne({ + where: { account_id: accountId, id: id }, + }); + + // ワークタイプが存在しない場合はエラー + if (!worktype) { + throw new WorktypeIdNotFoundError('Worktype is not found. id: ${id}'); + } + + // アカウントのActiveWorktypeIDを更新 + await accountRepo.update({ id: accountId }, { active_worktype_id: id }); + }); + } } From b5ecd6de151871740a1385e77ef66d1e45377924 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Fri, 15 Sep 2023 08:28:18 +0000 Subject: [PATCH 02/13] =?UTF-8?q?Merged=20PR=20410:=20=E7=94=BB=E9=9D=A2?= =?UTF-8?q?=E5=AE=9F=E8=A3=85=EF=BC=88ActiveWorktypeID=E8=A8=AD=E5=AE=9A?= =?UTF-8?q?=E3=82=BB=E3=83=AC=E3=82=AF=E3=83=88=E3=83=9C=E3=83=83=E3=82=AF?= =?UTF-8?q?=E3=82=B9=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task2623: 画面実装(ActiveWorktypeID設定セレクトボックス)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2623) - WorktypeID設定画面でのActiveWorktypeID選択処理を実装しました。 ## レビューポイント - WorkTypeIDの変更時の処理に問題はないか - 画面の表示に問題はないか ## UIの変更 - [Task2623](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/Task2623?csf=1&web=1&e=sUPTbC) ## 動作確認状況 - ローカルで確認 --- dictation_client/src/api/api.ts | 16 ++---- .../features/workflow/worktype/operations.ts | 52 +++++++++++++++++++ .../features/workflow/worktype/selectors.ts | 3 ++ .../src/features/workflow/worktype/state.ts | 1 + .../workflow/worktype/worktypeSlice.ts | 16 +++++- .../src/pages/PartnerPage/index.tsx | 1 - .../src/pages/WorkTypeIdSettingPage/index.tsx | 39 +++++++++++++- dictation_client/src/translation/de.json | 29 ++++++++++- dictation_client/src/translation/en.json | 29 ++++++++++- dictation_client/src/translation/es.json | 29 ++++++++++- dictation_client/src/translation/fr.json | 29 ++++++++++- 11 files changed, 220 insertions(+), 24 deletions(-) diff --git a/dictation_client/src/api/api.ts b/dictation_client/src/api/api.ts index f5d0b72..aed73de 100644 --- a/dictation_client/src/api/api.ts +++ b/dictation_client/src/api/api.ts @@ -824,7 +824,7 @@ export interface GetRelationsResponse { * @type {string} * @memberof GetRelationsResponse */ - 'encryptionPassword': string | null; + 'encryptionPassword'?: string; /** * アカウントがデフォルトで利用するWorkTypeID(アカウントに紐づくWorkTypeIDから一つ指定。activeWorktypeがなければ空文字を返却する) * @type {string} @@ -969,7 +969,7 @@ export interface GetWorktypesResponse { * @type {number} * @memberof GetWorktypesResponse */ - 'acrive'?: number; + 'active'?: number; } /** * @@ -1307,12 +1307,6 @@ export interface PostUpdateUserRequest { * @interface PostWorktypeOptionItem */ export interface PostWorktypeOptionItem { - /** - * - * @type {number} - * @memberof PostWorktypeOptionItem - */ - 'id': number; /** * * @type {string} @@ -1682,7 +1676,7 @@ export interface UpdateAccountInfoRequest { * @type {number} * @memberof UpdateAccountInfoRequest */ - 'parentAccountId': number; + 'parentAccountId'?: number; /** * 代行操作許可 * @type {boolean} @@ -1694,13 +1688,13 @@ export interface UpdateAccountInfoRequest { * @type {number} * @memberof UpdateAccountInfoRequest */ - 'primaryAdminUserId': number; + 'primaryAdminUserId'?: number; /** * セカンダリ管理者ID * @type {number} * @memberof UpdateAccountInfoRequest */ - 'secondryAdminUserId': number; + 'secondryAdminUserId'?: number; } /** * diff --git a/dictation_client/src/features/workflow/worktype/operations.ts b/dictation_client/src/features/workflow/worktype/operations.ts index 1875d48..6cbd133 100644 --- a/dictation_client/src/features/workflow/worktype/operations.ts +++ b/dictation_client/src/features/workflow/worktype/operations.ts @@ -284,3 +284,55 @@ export const editOptionItemsAsync = createAsyncThunk< return thunkApi.rejectWithValue({ error }); } }); + +export const updateActiveWorktypeAsync = createAsyncThunk< + { + // return empty + }, + { id?: number | undefined }, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("workflow/updateActiveWorktypeAsync", 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); + const { id } = args; + + try { + await accountsApi.activeWorktype( + { id }, + { + headers: { authorization: `Bearer ${accessToken}` }, + } + ); + + return {}; + } catch (e) { + // e ⇒ errorObjectに変換" + const error = createErrorObject(e); + + let errorMessage = getTranslationID("common.message.internalServerError"); + + // ActiveWorktypeの保存に失敗した場合 + if (error.code === "E011003") { + errorMessage = getTranslationID( + "worktypeIdSetting.message.updateActiveWorktypeFailedError" + ); + } + + 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 1f4aabc..bdd6bc1 100644 --- a/dictation_client/src/features/workflow/worktype/selectors.ts +++ b/dictation_client/src/features/workflow/worktype/selectors.ts @@ -70,3 +70,6 @@ export const selectHasErrorOptionItems = (state: RootState) => { // isOptionItemsLoadingを取得する export const selectIsOptionItemsLoading = (state: RootState) => state.worktype.apps.isOptionItemsLoading; + +export const selectActiveWorktypeId = (state: RootState) => + state.worktype.apps.activeWorktypeId; diff --git a/dictation_client/src/features/workflow/worktype/state.ts b/dictation_client/src/features/workflow/worktype/state.ts index 0475ab5..3acbae2 100644 --- a/dictation_client/src/features/workflow/worktype/state.ts +++ b/dictation_client/src/features/workflow/worktype/state.ts @@ -15,6 +15,7 @@ export interface Apps { worktypeId: string; description?: string; optionItems?: OptionItem[]; + activeWorktypeId?: number | undefined; } export interface Domain { diff --git a/dictation_client/src/features/workflow/worktype/worktypeSlice.ts b/dictation_client/src/features/workflow/worktype/worktypeSlice.ts index 833aa27..224e7e3 100644 --- a/dictation_client/src/features/workflow/worktype/worktypeSlice.ts +++ b/dictation_client/src/features/workflow/worktype/worktypeSlice.ts @@ -6,6 +6,7 @@ import { editWorktypeAsync, getOptionItemsAsync, listWorktypesAsync, + updateActiveWorktypeAsync, } from "./operations"; import { OptionItem, isOptionItemDefaultValueType } from "./types"; import { OPTION_ITEMS_DEFAULT_VALUE_TYPE } from "./constants"; @@ -20,6 +21,7 @@ const initialState: WorktypeState = { worktypeId: "", description: undefined, optionItems: undefined, + activeWorktypeId: undefined, }, domain: {}, }; @@ -82,9 +84,10 @@ export const worktypeSlice = createSlice({ state.apps.isLoading = true; }); builder.addCase(listWorktypesAsync.fulfilled, (state, action) => { - // TODO:Active WorktypeIDも取得する - const { worktypes } = action.payload; + const { worktypes, active } = action.payload; + state.domain.worktypes = worktypes; + state.apps.activeWorktypeId = active; state.apps.isLoading = false; }); builder.addCase(listWorktypesAsync.rejected, (state) => { @@ -137,6 +140,15 @@ export const worktypeSlice = createSlice({ builder.addCase(editOptionItemsAsync.rejected, (state) => { state.apps.isOptionItemsLoading = false; }); + builder.addCase(updateActiveWorktypeAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(updateActiveWorktypeAsync.fulfilled, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(updateActiveWorktypeAsync.rejected, (state) => { + state.apps.isLoading = false; + }); }, }); export const { diff --git a/dictation_client/src/pages/PartnerPage/index.tsx b/dictation_client/src/pages/PartnerPage/index.tsx index 617015b..38adaca 100644 --- a/dictation_client/src/pages/PartnerPage/index.tsx +++ b/dictation_client/src/pages/PartnerPage/index.tsx @@ -24,7 +24,6 @@ import personAdd from "../../assets/images/person_add.svg"; import { TIERS } from "../../components/auth/constants"; import { AddPartnerAccountPopup } from "./addPartnerAccountPopup"; import checkFill from "../../assets/images/check_fill.svg"; -import checkOutline from "../../assets/images/check_outline.svg"; const PartnerPage: React.FC = (): JSX.Element => { const dispatch: AppDispatch = useDispatch(); diff --git a/dictation_client/src/pages/WorkTypeIdSettingPage/index.tsx b/dictation_client/src/pages/WorkTypeIdSettingPage/index.tsx index d33fa1b..4fa599c 100644 --- a/dictation_client/src/pages/WorkTypeIdSettingPage/index.tsx +++ b/dictation_client/src/pages/WorkTypeIdSettingPage/index.tsx @@ -1,7 +1,7 @@ import { UpdateTokenTimer } from "components/auth/updateTokenTimer"; import Footer from "components/footer"; import Header from "components/header"; -import React, { useEffect, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { getTranslationID } from "translation"; import styles from "styles/app.module.scss"; import undo from "assets/images/undo.svg"; @@ -14,8 +14,10 @@ import { changeWorktypeId, changeDescription, listWorktypesAsync, + updateActiveWorktypeAsync, selectIsLoading, selectWorktypes, + selectActiveWorktypeId, } from "features/workflow/worktype"; import { AppDispatch } from "app/store"; import { AddWorktypeIdPopup } from "./addWorktypeIdPopup"; @@ -27,6 +29,8 @@ const WorktypeIdSettingPage: React.FC = (): JSX.Element => { const [t] = useTranslation(); const isLoading = useSelector(selectIsLoading); const worktypes = useSelector(selectWorktypes); + const activeWorktypeId = useSelector(selectActiveWorktypeId); + const [selectedRow, setSelectedRow] = useState(NaN); // 追加Popupの表示制御 const [isShowAddPopup, setIsShowAddPopup] = useState(false); @@ -34,10 +38,34 @@ const WorktypeIdSettingPage: React.FC = (): JSX.Element => { const [isShowEditPopup, setIsShowEditPopup] = useState(false); const [isShowEditOptionItemPopup, setIsShowEditOptionItemPopup] = useState(false); + useEffect(() => { dispatch(listWorktypesAsync()); }, [dispatch]); + const onChangeActiveWorktype = useCallback( + async (e: React.ChangeEvent) => { + // ダイアログ確認 + if ( + // eslint-disable-next-line no-alert + !window.confirm(t(getTranslationID("common.message.dialogConfirm"))) + ) { + return; + } + const { value } = e.target; + const active = value === "" ? undefined : Number(value); + + const { meta } = await dispatch( + updateActiveWorktypeAsync({ id: active }) + ); + + if (meta.requestStatus === "fulfilled") { + dispatch(listWorktypesAsync()); + } + }, + [dispatch, t] + ); + return ( <> { "worktypeIdSetting.label.activeWorktypeId" ) )}:`} - + {/* eslint-disable-next-line jsx-a11y/control-has-associated-label */} +