From 3a7bf60f3ee2c07a0a7ddf3d6a7ba88200b0a7e3 Mon Sep 17 00:00:00 2001 From: "saito.k" Date: Tue, 4 Jul 2023 06:06:37 +0000 Subject: [PATCH] =?UTF-8?q?Merged=20PR=20202:=20=E7=94=BB=E9=9D=A2?= =?UTF-8?q?=E5=AE=9F=E8=A3=85=EF=BC=88PlayBack=E3=83=9C=E3=82=BF=E3=83=B3?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task1997: 画面実装(PlayBackボタン)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/1997) - Playbackボタン押下時の挙動を実装 - typist - 自身が割り当て候補となっているタスクをPlayBackする - 成功時、カスタムURLスキームでデスクトップアプリを起動する - author - 自身のAuthorIDと一致するタスクをPlayBackする - 成功時、カスタムURLスキームでデスクトップアプリを起動する - ログイン時の、カスタムURLスキームを実際のデスクトップアプリのスキーム名に修正 ## レビューポイント - playbackAsyncのなかでソート条件更新APIを一緒に呼び出しているが問題ないか - ソート条件を更新するタイミングはここで問題ないか - ユーザーがTypistの時のみ更新するようにしたが問題ないか ## 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/Task1997?csf=1&web=1&e=9kLaxo ## 動作確認状況 - ローカルで確認 ## 補足 - Authorの挙動はAPI側の実装が完了していないので、未確認 --- dictation_client/.env | 1 + dictation_client/.env.local.example | 1 + dictation_client/.env.staging | 1 + dictation_client/src/api/api.ts | 38 ++--- dictation_client/src/common/errors/code.ts | 3 +- dictation_client/src/features/auth/utils.ts | 13 ++ .../src/features/dictation/dictationSlice.ts | 10 ++ .../src/features/dictation/operations.ts | 139 ++++++++++++------ .../src/pages/DictationPage/index.tsx | 74 ++++++++-- .../src/pages/LoginPage/index.tsx | 9 +- dictation_client/src/react-app-env.d.ts | 1 + dictation_client/src/translation/de.json | 2 + dictation_client/src/translation/en.json | 2 + dictation_client/src/translation/es.json | 2 + dictation_client/src/translation/fr.json | 2 + dictation_server/src/api/odms/openapi.json | 4 +- .../src/features/users/users.controller.ts | 4 +- .../tasks/tasks.repository.service.ts | 26 ++-- 18 files changed, 233 insertions(+), 99 deletions(-) diff --git a/dictation_client/.env b/dictation_client/.env index 0ee1b75..663f4e6 100644 --- a/dictation_client/.env +++ b/dictation_client/.env @@ -2,3 +2,4 @@ VITE_STAGE=develop VITE_B2C_CLIENTID=5eb34cba-84b6-46f9-a0ea-bc5c41157d63 VITE_B2C_AUTHORITY=https://adb2codmsdev.b2clogin.com/adb2codmsdev.onmicrosoft.com/b2c_1_signin_dev VITE_B2C_KNOWNAUTHORITIES=adb2codmsdev.b2clogin.com +VITE_DESK_TOP_APP_SCHEME=odms-desktopapp \ No newline at end of file diff --git a/dictation_client/.env.local.example b/dictation_client/.env.local.example index 48a7afa..64d94d4 100644 --- a/dictation_client/.env.local.example +++ b/dictation_client/.env.local.example @@ -2,3 +2,4 @@ VITE_STAGE=local VITE_B2C_CLIENTID=XXXX-XXXX-XXXXX-XXXX VITE_B2C_AUTHORITY=https://adb2XXXX.XXXX.com/adb2XXXX.onmicrosoft.com/XXXX VITE_B2C_KNOWNAUTHORITIES=adb2cXXXX.XXXx.com +VITE_DESK_TOP_APP_SCHEME=odms-desktopapp \ No newline at end of file diff --git a/dictation_client/.env.staging b/dictation_client/.env.staging index b667f79..b4beb04 100644 --- a/dictation_client/.env.staging +++ b/dictation_client/.env.staging @@ -2,3 +2,4 @@ VITE_STAGE=staging VITE_B2C_CLIENTID=5d8f0db9-4506-41d6-a5bb-5ec39f6eba8d VITE_B2C_AUTHORITY=https://adb2codmsstg.b2clogin.com/adb2codmsstg.onmicrosoft.com/b2c_1_signin_stg VITE_B2C_KNOWNAUTHORITIES=adb2codmsstg.b2clogin.com +VITE_DESK_TOP_APP_SCHEME=odms-desktopapp \ No newline at end of file diff --git a/dictation_client/src/api/api.ts b/dictation_client/src/api/api.ts index 6198ac1..64cc0dc 100644 --- a/dictation_client/src/api/api.ts +++ b/dictation_client/src/api/api.ts @@ -2408,7 +2408,7 @@ export const TasksApiAxiosParamCreator = function (configuration?: Configuration /** * 指定した文字起こしタスクのチェックアウト候補を変更します。 * @summary - * @param {number} audioFileId + * @param {number} audioFileId ODMS Cloud上の音声ファイルID * @param {PostCheckoutPermissionRequest} postCheckoutPermissionRequest * @param {*} [options] Override http request option. * @throws {RequiredError} @@ -2736,7 +2736,7 @@ export const TasksApiFp = function(configuration?: Configuration) { /** * 指定した文字起こしタスクのチェックアウト候補を変更します。 * @summary - * @param {number} audioFileId + * @param {number} audioFileId ODMS Cloud上の音声ファイルID * @param {PostCheckoutPermissionRequest} postCheckoutPermissionRequest * @param {*} [options] Override http request option. * @throws {RequiredError} @@ -2848,7 +2848,7 @@ export const TasksApiFactory = function (configuration?: Configuration, basePath /** * 指定した文字起こしタスクのチェックアウト候補を変更します。 * @summary - * @param {number} audioFileId + * @param {number} audioFileId ODMS Cloud上の音声ファイルID * @param {PostCheckoutPermissionRequest} postCheckoutPermissionRequest * @param {*} [options] Override http request option. * @throws {RequiredError} @@ -2957,7 +2957,7 @@ export class TasksApi extends BaseAPI { /** * 指定した文字起こしタスクのチェックアウト候補を変更します。 * @summary - * @param {number} audioFileId + * @param {number} audioFileId ODMS Cloud上の音声ファイルID * @param {PostCheckoutPermissionRequest} postCheckoutPermissionRequest * @param {*} [options] Override http request option. * @throws {RequiredError} @@ -3163,7 +3163,7 @@ export const UsersApiAxiosParamCreator = function (configuration?: Configuration * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getSortCcriteria: async (options: AxiosRequestConfig = {}): Promise => { + getSortCriteria: async (options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/users/sort-criteria`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -3272,9 +3272,9 @@ export const UsersApiAxiosParamCreator = function (configuration?: Configuration * @param {*} [options] Override http request option. * @throws {RequiredError} */ - updateSortCcriteria: async (postSortCriteriaRequest: PostSortCriteriaRequest, options: AxiosRequestConfig = {}): Promise => { + updateSortCriteria: async (postSortCriteriaRequest: PostSortCriteriaRequest, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'postSortCriteriaRequest' is not null or undefined - assertParamExists('updateSortCcriteria', 'postSortCriteriaRequest', postSortCriteriaRequest) + assertParamExists('updateSortCriteria', 'postSortCriteriaRequest', postSortCriteriaRequest) const localVarPath = `/users/sort-criteria`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -3353,8 +3353,8 @@ export const UsersApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getSortCcriteria(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getSortCcriteria(options); + async getSortCriteria(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getSortCriteria(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -3385,8 +3385,8 @@ export const UsersApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async updateSortCcriteria(postSortCriteriaRequest: PostSortCriteriaRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.updateSortCcriteria(postSortCriteriaRequest, options); + async updateSortCriteria(postSortCriteriaRequest: PostSortCriteriaRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updateSortCriteria(postSortCriteriaRequest, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, } @@ -3434,8 +3434,8 @@ export const UsersApiFactory = function (configuration?: Configuration, basePath * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getSortCcriteria(options?: any): AxiosPromise { - return localVarFp.getSortCcriteria(options).then((request) => request(axios, basePath)); + getSortCriteria(options?: any): AxiosPromise { + return localVarFp.getSortCriteria(options).then((request) => request(axios, basePath)); }, /** * @@ -3463,8 +3463,8 @@ export const UsersApiFactory = function (configuration?: Configuration, basePath * @param {*} [options] Override http request option. * @throws {RequiredError} */ - updateSortCcriteria(postSortCriteriaRequest: PostSortCriteriaRequest, options?: any): AxiosPromise { - return localVarFp.updateSortCcriteria(postSortCriteriaRequest, options).then((request) => request(axios, basePath)); + updateSortCriteria(postSortCriteriaRequest: PostSortCriteriaRequest, options?: any): AxiosPromise { + return localVarFp.updateSortCriteria(postSortCriteriaRequest, options).then((request) => request(axios, basePath)); }, }; }; @@ -3518,8 +3518,8 @@ export class UsersApi extends BaseAPI { * @throws {RequiredError} * @memberof UsersApi */ - public getSortCcriteria(options?: AxiosRequestConfig) { - return UsersApiFp(this.configuration).getSortCcriteria(options).then((request) => request(this.axios, this.basePath)); + public getSortCriteria(options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).getSortCriteria(options).then((request) => request(this.axios, this.basePath)); } /** @@ -3553,8 +3553,8 @@ export class UsersApi extends BaseAPI { * @throws {RequiredError} * @memberof UsersApi */ - public updateSortCcriteria(postSortCriteriaRequest: PostSortCriteriaRequest, options?: AxiosRequestConfig) { - return UsersApiFp(this.configuration).updateSortCcriteria(postSortCriteriaRequest, options).then((request) => request(this.axios, this.basePath)); + public updateSortCriteria(postSortCriteriaRequest: PostSortCriteriaRequest, options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).updateSortCriteria(postSortCriteriaRequest, options).then((request) => request(this.axios, this.basePath)); } } diff --git a/dictation_client/src/common/errors/code.ts b/dictation_client/src/common/errors/code.ts index da8a766..54b116c 100644 --- a/dictation_client/src/common/errors/code.ts +++ b/dictation_client/src/common/errors/code.ts @@ -25,5 +25,6 @@ export const errorCodes = [ "E010301", // メールアドレス登録済みエラー "E010302", // authorId重複エラー "E010401", // PONumber重複エラー - "E010601", // タスク変更不可エラー + "E010601", // タスク変更不可エラー(タスクが変更できる状態でない、またはタスクが存在しない) + "E010602", // タスク変更権限不足エラー ] as const; diff --git a/dictation_client/src/features/auth/utils.ts b/dictation_client/src/features/auth/utils.ts index 9b28f49..9ca5538 100644 --- a/dictation_client/src/features/auth/utils.ts +++ b/dictation_client/src/features/auth/utils.ts @@ -79,3 +79,16 @@ export const isAuthorUser = (): boolean => { } return token.role.includes(USER_ROLES.AUTHOR); }; + +/** + * is author user ログインしているユーザがAuthorかどうかを返す + * @returns bool + */ +export const isTypistUser = (): boolean => { + const jwt = loadAccessToken(); + const token = jwt ? decodeToken(jwt) : null; + if (!token) { + return false; + } + return token.role.includes(USER_ROLES.TYPIST); +}; diff --git a/dictation_client/src/features/dictation/dictationSlice.ts b/dictation_client/src/features/dictation/dictationSlice.ts index 3c467ec..f90c5ed 100644 --- a/dictation_client/src/features/dictation/dictationSlice.ts +++ b/dictation_client/src/features/dictation/dictationSlice.ts @@ -6,6 +6,7 @@ import { listTasksAsync, listTypistGroupsAsync, listTypistsAsync, + playbackAsync, updateAssigneeAsync, } from "./operations"; import { @@ -131,6 +132,15 @@ export const dictationSlice = createSlice({ builder.addCase(updateAssigneeAsync.rejected, (state) => { state.apps.isLoading = false; }); + builder.addCase(playbackAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(playbackAsync.fulfilled, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(playbackAsync.rejected, (state) => { + state.apps.isLoading = false; + }); }, }); diff --git a/dictation_client/src/features/dictation/operations.ts b/dictation_client/src/features/dictation/operations.ts index 18963cc..f3c69dd 100644 --- a/dictation_client/src/features/dictation/operations.ts +++ b/dictation_client/src/features/dictation/operations.ts @@ -94,7 +94,7 @@ export const getSortColumnAsync = createAsyncThunk< const usersApi = new UsersApi(config); try { - const sort = await usersApi.getSortCcriteria({ + const sort = await usersApi.getSortCriteria({ headers: { authorization: `Bearer ${accessToken}` }, }); @@ -125,52 +125,6 @@ export const getSortColumnAsync = createAsyncThunk< } }); -export const updateSortColumnAsync = createAsyncThunk< - { - /** empty */ - }, - { - // パラメータ - direction: DirectionType; - paramName: SortableColumnType; - }, - { - // rejectした時の返却値の型 - rejectValue: { - error: ErrorObject; - }; - } ->("dictations/updateSortColumnAsync", async (args, thunkApi) => { - const { direction, paramName } = args; - - // apiのConfigurationを取得する - const { getState } = thunkApi; - const state = getState() as RootState; - const { configuration, accessToken } = state.auth; - const config = new Configuration(configuration); - const usersApi = new UsersApi(config); - - try { - await usersApi.updateSortCcriteria( - { direction, paramName }, - { - headers: { authorization: `Bearer ${accessToken}` }, - } - ); - return {}; - } catch (e) { - // e ⇒ errorObjectに変換" - const error = createErrorObject(e); - thunkApi.dispatch( - openSnackbar({ - level: "error", - message: getTranslationID("common.message.internalServerError"), - }) - ); - return thunkApi.rejectWithValue({ error }); - } -}); - export const listTypistsAsync = createAsyncThunk< GetTypistsResponse, void, @@ -275,6 +229,12 @@ export const updateAssigneeAsync = createAsyncThunk< headers: { authorization: `Bearer ${accessToken}` }, } ); + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); return {}; } catch (e) { // e ⇒ errorObjectに変換" @@ -300,3 +260,88 @@ export const updateAssigneeAsync = createAsyncThunk< return thunkApi.rejectWithValue({ error }); } }); + +export const playbackAsync = createAsyncThunk< + { + /** empty */ + }, + { + direction: DirectionType; + paramName: SortableColumnType; + audioFileId: number; + isTypist: boolean; + }, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("dictations/playbackAsync", async (args, thunkApi) => { + const { audioFileId, direction, paramName, isTypist } = args; + + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration, accessToken } = state.auth; + const config = new Configuration(configuration); + const tasksApi = new TasksApi(config); + const usersApi = new UsersApi(config); + try { + // ユーザーがタイピストである場合に、ソート条件を保存する + if (isTypist) { + await usersApi.updateSortCriteria( + { direction, paramName }, + { + headers: { authorization: `Bearer ${accessToken}` }, + } + ); + } + await tasksApi.checkout(audioFileId, { + headers: { authorization: `Bearer ${accessToken}` }, + }); + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); + return {}; + } catch (e) { + // e ⇒ errorObjectに変換" + const error = createErrorObject(e); + + // ステータスが[Uploaded,Inprogress,Pending]以外、またはタスクが存在しない場合 + if (error.code === "E010601") { + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: getTranslationID( + "dictationPage.message.taskToPlaybackNoExists" + ), + }) + ); + return thunkApi.rejectWithValue({ error }); + } + // タスクをチェックアウトする権限がない + if (error.code === "E010602") { + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: getTranslationID( + "dictationPage.message.noPlaybackAuthorization" + ), + }) + ); + return thunkApi.rejectWithValue({ error }); + } + + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: getTranslationID("common.message.internalServerError"), + }) + ); + return thunkApi.rejectWithValue({ error }); + } +}); diff --git a/dictation_client/src/pages/DictationPage/index.tsx b/dictation_client/src/pages/DictationPage/index.tsx index da6e520..021d9fb 100644 --- a/dictation_client/src/pages/DictationPage/index.tsx +++ b/dictation_client/src/pages/DictationPage/index.tsx @@ -23,17 +23,17 @@ import { changeParamName, changeDirection, changeSelectedTask, - updateSortColumnAsync, SortableColumnType, changeAssignee, listTypistsAsync, listTypistGroupsAsync, DirectionType, selectIsLoading, + playbackAsync, } from "features/dictation"; import { getTranslationID } from "translation"; import { Task } from "api/api"; -import { isAdminUser, isAuthorUser } from "features/auth/utils"; +import { isAdminUser, isAuthorUser, isTypistUser } from "features/auth/utils"; import { STATUS, LIMIT_TASK_NUM } from "../../features/dictation"; import uploaded from "../../assets/images/uploaded.svg"; import pending from "../../assets/images/pending.svg"; @@ -51,6 +51,7 @@ const DictationPage: React.FC = (): JSX.Element => { const isAdmin = isAdminUser(); const isAuthor = isAuthorUser(); + const isTypist = isTypistUser(); // popup制御関係 const [ isChangeTranscriptionistPopupOpen, @@ -316,14 +317,65 @@ const DictationPage: React.FC = (): JSX.Element => { [dispatch, sortDirection, sortableParamName] ); - const onPlayBack = useCallback(() => { - dispatch( - updateSortColumnAsync({ - direction: sortDirection, - paramName: sortableParamName, - }) - ); - }, [dispatch, sortDirection, sortableParamName]); + const onPlayBack = useCallback( + async (audioFileId: number) => { + if ( + /* eslint-disable-next-line no-alert */ + !window.confirm(t(getTranslationID("common.message.dialogConfirm"))) + ) { + return; + } + const { meta } = await dispatch( + playbackAsync({ + audioFileId, + direction: sortDirection, + paramName: sortableParamName, + isTypist, + }) + ); + if (meta.requestStatus === "fulfilled") { + const filter = getFilter( + filterUploaded, + filterInProgress, + filterPending, + filterFinished, + filterBackup + ); + dispatch( + listTasksAsync({ + limit: LIMIT_TASK_NUM, + offset: 0, + filter, + direction: sortDirection, + paramName: sortableParamName, + }) + ); + dispatch(listTypistsAsync()); + dispatch(listTypistGroupsAsync()); + + const url = `${ + import.meta.env.VITE_DESK_TOP_APP_SCHEME + }:playback?audioId=${audioFileId}`; + const a = document.createElement("a"); + a.href = url; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + } + }, + [ + dispatch, + filterBackup, + filterFinished, + filterInProgress, + filterPending, + filterUploaded, + isTypist, + sortDirection, + sortableParamName, + t, + ] + ); const onClosePopup = useCallback( (isChanged: boolean) => { @@ -949,7 +1001,7 @@ const DictationPage: React.FC = (): JSX.Element => {