From e4bc4776b04c342630124ad1934731974d2a75b3 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Fri, 30 Jun 2023 05:51:14 +0000 Subject: [PATCH] =?UTF-8?q?Merged=20PR=20184:=20=E3=83=AD=E3=83=BC?= =?UTF-8?q?=E3=83=87=E3=82=A3=E3=83=B3=E3=82=B0=E8=A1=A8=E7=8F=BE=E3=81=AB?= =?UTF-8?q?=E9=96=A2=E3=81=99=E3=82=8B=E5=AE=9F=E8=A3=85=E7=AD=89=E3=82=92?= =?UTF-8?q?=E8=A1=8C=E3=81=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task2015: [Task1895完了後][Sp11-2着手] ローディング表現に関する実装等を行う](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2015) - 現在実装中のテーブル、ポップアップについてローディング表現を実装しました。 - ローディング中にローディング中を示すぐるぐるを表示 - ローディング中はボタンを非活性にする ## レビューポイント - 対応箇所は適切か - 表示内容に問題はないか ## UIの変更 - [Task2015](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/Task2015?csf=1&web=1&e=TmrOXa) ## 動作確認状況 - ローカルで確認 --- dictation_client/.eslintignore | 2 - .../src/features/dictation/dictationSlice.ts | 19 +++ .../src/features/dictation/operations.ts | 7 - .../src/features/dictation/selectors.ts | 3 + .../src/features/dictation/state.ts | 1 + .../license/licenseOrder/licenseSlice.ts | 13 ++ .../license/licenseOrder/operations.ts | 4 +- .../license/licenseOrder/selectors.ts | 3 + .../features/license/licenseOrder/state.ts | 1 + .../licenseSummary/licenseSummarySlice.ts | 3 + .../license/licenseSummary/selectors.ts | 2 + .../features/license/licenseSummary/state.ts | 5 + .../src/features/user/selectors.ts | 1 + dictation_client/src/features/user/state.ts | 1 + .../src/features/user/userSlice.ts | 19 ++- .../changeTranscriptionistPopup.tsx | 14 +- .../src/pages/DictationPage/index.tsx | 87 ++++++++++-- .../pages/LicensePage/licenseOrderPopup.tsx | 14 +- .../src/pages/UserListPage/index.tsx | 130 ++++++++++-------- .../src/pages/UserListPage/popup.tsx | 14 +- .../src/styles/app.module.scss.d.ts | 2 +- dictation_client/src/translation/de.json | 4 +- dictation_client/src/translation/en.json | 4 +- dictation_client/src/translation/es.json | 4 +- dictation_client/src/translation/fr.json | 4 +- .../migrations/016-create_template_files.sql | 15 -- dictation_server/package.json | 1 + 27 files changed, 269 insertions(+), 108 deletions(-) delete mode 100644 dictation_server/db/migrations/016-create_template_files.sql diff --git a/dictation_client/.eslintignore b/dictation_client/.eslintignore index 2646704..1fbf4c4 100644 --- a/dictation_client/.eslintignore +++ b/dictation_client/.eslintignore @@ -5,5 +5,3 @@ jest.config.js vite.config.ts .env.local -# デザイナのcssから生成するため除外 -src/styles/app.module.scss.d.ts diff --git a/dictation_client/src/features/dictation/dictationSlice.ts b/dictation_client/src/features/dictation/dictationSlice.ts index 009d47f..3c467ec 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, + updateAssigneeAsync, } from "./operations"; import { SORTABLE_COLUMN, @@ -35,6 +36,7 @@ const initialState: DictationState = { selected: [], pool: [], }, + isLoading: true, }, }; @@ -96,11 +98,19 @@ export const dictationSlice = createSlice({ }, }, extraReducers: (builder) => { + builder.addCase(listTasksAsync.pending, (state) => { + state.apps.isLoading = true; + }); builder.addCase(listTasksAsync.fulfilled, (state, action) => { state.domain.limit = action.payload.limit; state.domain.offset = action.payload.offset; state.domain.total = action.payload.total; state.domain.tasks = action.payload.tasks; + + state.apps.isLoading = false; + }); + builder.addCase(listTasksAsync.rejected, (state) => { + state.apps.isLoading = false; }); builder.addCase(getSortColumnAsync.fulfilled, (state, action) => { state.apps.direction = action.payload.direction; @@ -112,6 +122,15 @@ export const dictationSlice = createSlice({ builder.addCase(listTypistGroupsAsync.fulfilled, (state, action) => { state.domain.typistGroups = action.payload.typistGroups; }); + builder.addCase(updateAssigneeAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(updateAssigneeAsync.fulfilled, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(updateAssigneeAsync.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 4dccf91..18963cc 100644 --- a/dictation_client/src/features/dictation/operations.ts +++ b/dictation_client/src/features/dictation/operations.ts @@ -275,13 +275,6 @@ export const updateAssigneeAsync = createAsyncThunk< headers: { authorization: `Bearer ${accessToken}` }, } ); - - thunkApi.dispatch( - openSnackbar({ - level: "info", - message: getTranslationID("common.message.success"), - }) - ); return {}; } catch (e) { // e ⇒ errorObjectに変換" diff --git a/dictation_client/src/features/dictation/selectors.ts b/dictation_client/src/features/dictation/selectors.ts index 455edd0..6461175 100644 --- a/dictation_client/src/features/dictation/selectors.ts +++ b/dictation_client/src/features/dictation/selectors.ts @@ -38,3 +38,6 @@ export const selectSelectedTranscriptionists = (state: RootState) => export const selectPoolTranscriptionists = (state: RootState) => state.dictation.apps.assignee.pool; + +export const selectIsLoading = (state: RootState) => + state.dictation.apps.isLoading; diff --git a/dictation_client/src/features/dictation/state.ts b/dictation_client/src/features/dictation/state.ts index 244b2ad..0d383f7 100644 --- a/dictation_client/src/features/dictation/state.ts +++ b/dictation_client/src/features/dictation/state.ts @@ -28,4 +28,5 @@ export interface Apps { selected: Assignee[]; pool: Assignee[]; }; + isLoading: boolean; } diff --git a/dictation_client/src/features/license/licenseOrder/licenseSlice.ts b/dictation_client/src/features/license/licenseOrder/licenseSlice.ts index 583f992..6332532 100644 --- a/dictation_client/src/features/license/licenseOrder/licenseSlice.ts +++ b/dictation_client/src/features/license/licenseOrder/licenseSlice.ts @@ -1,10 +1,12 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit"; import { LicenseOrdersState } from "./state"; +import { orderLicenseAsync } from "./operations"; const initialState: LicenseOrdersState = { apps: { poNumber: "", newOrder: 0, + isLoading: false, }, }; export const licenseSlice = createSlice({ @@ -23,6 +25,17 @@ export const licenseSlice = createSlice({ state.apps = initialState.apps; }, }, + extraReducers: (builder) => { + builder.addCase(orderLicenseAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(orderLicenseAsync.fulfilled, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(orderLicenseAsync.rejected, (state) => { + state.apps.isLoading = false; + }); + }, }); export const { changePoNumber, changeNewOrder, cleanupApps } = diff --git a/dictation_client/src/features/license/licenseOrder/operations.ts b/dictation_client/src/features/license/licenseOrder/operations.ts index 0d39ee5..77d97d9 100644 --- a/dictation_client/src/features/license/licenseOrder/operations.ts +++ b/dictation_client/src/features/license/licenseOrder/operations.ts @@ -44,7 +44,9 @@ export const orderLicenseAsync = createAsyncThunk< thunkApi.dispatch( openSnackbar({ level: "info", - message: getTranslationID("common.message.success"), + message: getTranslationID( + "licenseOrderPage.message.createOrderSuccess" + ), }) ); return {}; diff --git a/dictation_client/src/features/license/licenseOrder/selectors.ts b/dictation_client/src/features/license/licenseOrder/selectors.ts index 78d3cbb..9b7a0e6 100644 --- a/dictation_client/src/features/license/licenseOrder/selectors.ts +++ b/dictation_client/src/features/license/licenseOrder/selectors.ts @@ -34,3 +34,6 @@ export const checkErrorIncorrectNewOrder = (newOrder: number): boolean => { export const selectPoNumber = (state: RootState) => state.license.apps.poNumber; export const selectNewOrder = (state: RootState) => state.license.apps.newOrder; + +export const selectIsLoading = (state: RootState) => + state.license.apps.isLoading; diff --git a/dictation_client/src/features/license/licenseOrder/state.ts b/dictation_client/src/features/license/licenseOrder/state.ts index 2a1f7c3..b5fe233 100644 --- a/dictation_client/src/features/license/licenseOrder/state.ts +++ b/dictation_client/src/features/license/licenseOrder/state.ts @@ -5,4 +5,5 @@ export interface LicenseOrdersState { export interface Apps { poNumber: string; newOrder: number; + isLoading: boolean; } diff --git a/dictation_client/src/features/license/licenseSummary/licenseSummarySlice.ts b/dictation_client/src/features/license/licenseSummary/licenseSummarySlice.ts index 2b5b746..82cfadf 100644 --- a/dictation_client/src/features/license/licenseSummary/licenseSummarySlice.ts +++ b/dictation_client/src/features/license/licenseSummary/licenseSummarySlice.ts @@ -16,6 +16,9 @@ const initialState: LicenseSummaryState = { usedSize: 0, isAccountLock: false, }, + apps: { + isLoading: false, + }, }; export const licenseSummarySlice = createSlice({ diff --git a/dictation_client/src/features/license/licenseSummary/selectors.ts b/dictation_client/src/features/license/licenseSummary/selectors.ts index e4eceee..87df71f 100644 --- a/dictation_client/src/features/license/licenseSummary/selectors.ts +++ b/dictation_client/src/features/license/licenseSummary/selectors.ts @@ -3,3 +3,5 @@ import { RootState } from "app/store"; // 各値はそのまま画面に表示するので、licenseSummaryInfoとして値を取得する export const selecLicenseSummaryInfo = (state: RootState) => state.licenseSummary.domain; + +export const selectIsLoading = (state: RootState) => state.license; diff --git a/dictation_client/src/features/license/licenseSummary/state.ts b/dictation_client/src/features/license/licenseSummary/state.ts index d7145e6..4a0d07f 100644 --- a/dictation_client/src/features/license/licenseSummary/state.ts +++ b/dictation_client/src/features/license/licenseSummary/state.ts @@ -1,5 +1,6 @@ export interface LicenseSummaryState { domain: Domain; + apps: Apps; } export interface Domain { @@ -15,3 +16,7 @@ export interface Domain { usedSize: number; isAccountLock: boolean; } + +export interface Apps { + isLoading: boolean; +} diff --git a/dictation_client/src/features/user/selectors.ts b/dictation_client/src/features/user/selectors.ts index ad726e4..725206e 100644 --- a/dictation_client/src/features/user/selectors.ts +++ b/dictation_client/src/features/user/selectors.ts @@ -53,3 +53,4 @@ export const selectLicenseAlert = (state: RootState) => export const selectNtotification = (state: RootState) => state.user.apps.addUser.notification; export const selectDomain = (state: RootState) => state.user.domain; +export const selectIsLoading = (state: RootState) => state.user.apps.isLoading; diff --git a/dictation_client/src/features/user/state.ts b/dictation_client/src/features/user/state.ts index 760c0e5..00dac8e 100644 --- a/dictation_client/src/features/user/state.ts +++ b/dictation_client/src/features/user/state.ts @@ -11,6 +11,7 @@ export interface Domain { export interface Apps { addUser: AddUser; + isLoading: boolean; } export interface AddUser extends User { diff --git a/dictation_client/src/features/user/userSlice.ts b/dictation_client/src/features/user/userSlice.ts index a178060..25fcc89 100644 --- a/dictation_client/src/features/user/userSlice.ts +++ b/dictation_client/src/features/user/userSlice.ts @@ -1,6 +1,6 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit"; import { UsersState } from "./state"; -import { listUsersAsync } from "./operations"; +import { addUserAsync, listUsersAsync } from "./operations"; import { ROLE, RoleType } from "./constants"; const initialState: UsersState = { @@ -17,6 +17,7 @@ const initialState: UsersState = { licenseAlert: true, notification: true, }, + isLoading: false, }, }; @@ -73,8 +74,24 @@ export const userSlice = createSlice({ }, }, extraReducers: (builder) => { + builder.addCase(listUsersAsync.pending, (state) => { + state.apps.isLoading = true; + }); builder.addCase(listUsersAsync.fulfilled, (state, action) => { state.domain.users = action.payload.users; + state.apps.isLoading = false; + }); + builder.addCase(listUsersAsync.rejected, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(addUserAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(addUserAsync.fulfilled, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(addUserAsync.rejected, (state) => { + state.apps.isLoading = false; }); }, }); diff --git a/dictation_client/src/pages/DictationPage/changeTranscriptionistPopup.tsx b/dictation_client/src/pages/DictationPage/changeTranscriptionistPopup.tsx index 3ebc642..e13e8dd 100644 --- a/dictation_client/src/pages/DictationPage/changeTranscriptionistPopup.tsx +++ b/dictation_client/src/pages/DictationPage/changeTranscriptionistPopup.tsx @@ -3,6 +3,7 @@ import styles from "styles/app.module.scss"; import { useDispatch, useSelector } from "react-redux"; import { changeAssignee, + selectIsLoading, selectPoolTranscriptionists, selectSelectedTask, selectSelectedTranscriptionists, @@ -13,6 +14,7 @@ import { AppDispatch } from "app/store"; import { getTranslationID } from "translation"; import { useTranslation } from "react-i18next"; import close from "../../assets/images/close.svg"; +import progress_activit from "../../assets/images/progress_activit.svg"; interface ChangeTranscriptionistPopupProps { onClose: (isChanged: boolean) => void; @@ -26,6 +28,8 @@ export const ChangeTranscriptionistPopup: React.FC< const dispatch: AppDispatch = useDispatch(); const [t] = useTranslation(); + const isLoading = useSelector(selectIsLoading); + // ポップアップを閉じる処理 const closePopup = useCallback(() => { onClose(false); @@ -182,9 +186,17 @@ export const ChangeTranscriptionistPopup: React.FC< type="button" name="submit" value={t(getTranslationID("dictationPage.label.saveChanges"))} - className={`${styles.formSubmit} ${styles.marginBtm1} ${styles.isActive}`} + className={`${styles.formSubmit} ${styles.marginBtm1} ${ + !isLoading ? styles.isActive : "" + }`} onClick={onChangeTranscriptionist} /> + Loading diff --git a/dictation_client/src/pages/DictationPage/index.tsx b/dictation_client/src/pages/DictationPage/index.tsx index cd7a285..cb449b2 100644 --- a/dictation_client/src/pages/DictationPage/index.tsx +++ b/dictation_client/src/pages/DictationPage/index.tsx @@ -28,6 +28,7 @@ import { changeAssignee, listTypistsAsync, listTypistGroupsAsync, + selectIsLoading, } from "features/dictation"; import { getTranslationID } from "translation"; import { Task } from "api/api"; @@ -39,6 +40,7 @@ import inprogress from "../../assets/images/inprogress.svg"; import finished from "../../assets/images/finished.svg"; import backup from "../../assets/images/backup.svg"; import lock from "../../assets/images/lock.svg"; +import progress_activit from "../../assets/images/progress_activit.svg"; import { DisPlayInfo } from "./displayInfo"; import { ChangeTranscriptionistPopup } from "./changeTranscriptionistPopup"; @@ -82,6 +84,8 @@ const DictationPage: React.FC = (): JSX.Element => { const totalPage = useSelector(selectTotalPage); const currentPage = useSelector(selectCurrentPage); + const isLoading = useSelector(selectIsLoading); + // ページネーションのボタンクリック時のアクション const getFirstPage = useCallback(() => { const filter = getFilter( @@ -421,6 +425,7 @@ const DictationPage: React.FC = (): JSX.Element => { value="flUploaded" className={styles.formCheck} checked={filterUploaded} + disabled={isLoading} onChange={(e) => { setFilterUploaded(e.target.checked); updateFilter( @@ -443,6 +448,7 @@ const DictationPage: React.FC = (): JSX.Element => { value="flInProgress" className={styles.formCheck} checked={filterInProgress} + disabled={isLoading} onChange={(e) => { setFilterInProgress(e.target.checked); updateFilter( @@ -465,6 +471,7 @@ const DictationPage: React.FC = (): JSX.Element => { value="flPending" className={styles.formCheck} checked={filterPending} + disabled={isLoading} onChange={(e) => { setFilterPending(e.target.checked); updateFilter( @@ -487,6 +494,7 @@ const DictationPage: React.FC = (): JSX.Element => { value="flFinished" className={styles.formCheck} checked={filterFinished} + disabled={isLoading} onChange={(e) => { setFilterFinished(e.target.checked); updateFilter( @@ -509,6 +517,7 @@ const DictationPage: React.FC = (): JSX.Element => { value="flBackup" className={styles.formCheck} checked={filterBackup} + disabled={isLoading} onChange={(e) => { setFilterBackup(e.target.checked); updateFilter( @@ -537,6 +546,9 @@ const DictationPage: React.FC = (): JSX.Element => { onClick={() => updateSortColumn(SORTABLE_COLUMN.JobNumber) } + style={{ + pointerEvents: isLoading ? "none" : "auto", + }} > {t( getTranslationID("dictationPage.label.jobNumber") @@ -552,6 +564,9 @@ const DictationPage: React.FC = (): JSX.Element => { onClick={() => updateSortColumn(SORTABLE_COLUMN.Status) } + style={{ + pointerEvents: isLoading ? "none" : "auto", + }} > {t(getTranslationID("dictationPage.label.status"))} @@ -568,6 +583,9 @@ const DictationPage: React.FC = (): JSX.Element => { onClick={() => updateSortColumn(SORTABLE_COLUMN.Encryption) } + style={{ + pointerEvents: isLoading ? "none" : "auto", + }} > {t( getTranslationID("dictationPage.label.encryption") @@ -583,6 +601,9 @@ const DictationPage: React.FC = (): JSX.Element => { onClick={() => updateSortColumn(SORTABLE_COLUMN.AuthorId) } + style={{ + pointerEvents: isLoading ? "none" : "auto", + }} > {t( getTranslationID("dictationPage.label.authorId") @@ -598,6 +619,9 @@ const DictationPage: React.FC = (): JSX.Element => { onClick={() => updateSortColumn(SORTABLE_COLUMN.WorkType) } + style={{ + pointerEvents: isLoading ? "none" : "auto", + }} > {t( getTranslationID("dictationPage.label.workType") @@ -613,6 +637,9 @@ const DictationPage: React.FC = (): JSX.Element => { onClick={() => updateSortColumn(SORTABLE_COLUMN.FileName) } + style={{ + pointerEvents: isLoading ? "none" : "auto", + }} > {t( getTranslationID("dictationPage.label.fileName") @@ -628,6 +655,9 @@ const DictationPage: React.FC = (): JSX.Element => { onClick={() => updateSortColumn(SORTABLE_COLUMN.FileLength) } + style={{ + pointerEvents: isLoading ? "none" : "auto", + }} > {t( getTranslationID("dictationPage.label.fileLength") @@ -643,6 +673,9 @@ const DictationPage: React.FC = (): JSX.Element => { onClick={() => updateSortColumn(SORTABLE_COLUMN.FileSize) } + style={{ + pointerEvents: isLoading ? "none" : "auto", + }} > {t( getTranslationID("dictationPage.label.fileSize") @@ -660,6 +693,9 @@ const DictationPage: React.FC = (): JSX.Element => { SORTABLE_COLUMN.RecordingStartedDate ) } + style={{ + pointerEvents: isLoading ? "none" : "auto", + }} > {t( getTranslationID( @@ -679,6 +715,9 @@ const DictationPage: React.FC = (): JSX.Element => { SORTABLE_COLUMN.RecordingFinishedDate ) } + style={{ + pointerEvents: isLoading ? "none" : "auto", + }} > {t( getTranslationID( @@ -696,6 +735,9 @@ const DictationPage: React.FC = (): JSX.Element => { onClick={() => updateSortColumn(SORTABLE_COLUMN.UploadDate) } + style={{ + pointerEvents: isLoading ? "none" : "auto", + }} > {t( getTranslationID("dictationPage.label.uploadDate") @@ -713,6 +755,9 @@ const DictationPage: React.FC = (): JSX.Element => { SORTABLE_COLUMN.TranscriptionStartedDate ) } + style={{ + pointerEvents: isLoading ? "none" : "auto", + }} > {t( getTranslationID( @@ -732,6 +777,9 @@ const DictationPage: React.FC = (): JSX.Element => { SORTABLE_COLUMN.TranscriptionFinishedDate ) } + style={{ + pointerEvents: isLoading ? "none" : "auto", + }} > {t( getTranslationID( @@ -826,7 +874,8 @@ const DictationPage: React.FC = (): JSX.Element => { )} - {tasks.length !== 0 && + {(isChangeTranscriptionistPopupOpen || !isLoading) && + tasks.length !== 0 && tasks.map((x) => ( @@ -1032,13 +1081,21 @@ const DictationPage: React.FC = (): JSX.Element => { ))} - {tasks.length === 0 && ( -

- {t(getTranslationID("common.message.listEmpty"))} -

- )} + {(isChangeTranscriptionistPopupOpen || !isLoading) && + tasks.length === 0 && ( +

+ {t(getTranslationID("common.message.listEmpty"))} +

+ )} - + {isLoading && ( + Loading + )} {/** pagenation */}