Merged PR 343: 画面実装(TypistGroup設定画面)

## 概要
[Task2411: 画面実装(TypistGroup設定画面)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2411)

- タイピストグループの設定画面を実装しました。
  - タイピストグループの一覧を取得できるようにしています。
  - return ボタンでワークフロー画面に遷移します。
  - Edit、Add Groupは対象外です。
- ヘッダタブ表示のため、ヘッダを修正しています。

## レビューポイント
- パスの構成は問題ないか
- ヘッダの対応に問題はないか

## UIの変更
- [Task2411](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/Task2411?csf=1&web=1&e=cxxeCf)

## 動作確認状況
- ローカルで確認
This commit is contained in:
makabe.t 2023-08-23 08:44:01 +00:00
parent c85ecbfc90
commit 84e7deb52a
14 changed files with 304 additions and 16 deletions

View File

@ -13,6 +13,7 @@ import partnerLicense from "features/license/partnerLicense/partnerLicenseSlice"
import dictation from "features/dictation/dictationSlice";
import partner from "features/partner/partnerSlice";
import licenseOrderHistory from "features/license/licenseOrderHistory/licenseOrderHistorySlice";
import typistGroup from "features/workflow/typistGroup/typistGroupSlice";
export const store = configureStore({
reducer: {
@ -30,6 +31,7 @@ export const store = configureStore({
partnerLicense,
dictation,
partner,
typistGroup,
},
});

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.7.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="レイヤー_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
y="0px" viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve">
<style type="text/css">
.st0{fill:#282828;}
</style>
<g>
<path class="st0" d="M25.7,23.7c2.5,0.6,4.8,0.2,6.7-1.2s2.9-3.4,2.9-6.1s-1-4.8-2.9-6.1c-1.9-1.3-4.1-1.7-6.7-1.1
c0.9,1.1,1.5,2.2,1.9,3.3s0.6,2.5,0.6,4s-0.2,2.8-0.6,3.9S26.6,22.6,25.7,23.7z"/>
<path class="st0" d="M17.8,24c2.2,0,4-0.7,5.4-2.1s2.1-3.2,2.1-5.4s-0.7-4-2.1-5.4C21.8,9.6,20,9,17.8,9s-4,0.7-5.4,2.1
s-2.1,3.2-2.1,5.4s0.7,4,2.1,5.4C13.8,23.2,15.5,24,17.8,24z M14.5,13.2c0.8-0.8,1.9-1.3,3.2-1.3s2.4,0.4,3.2,1.3
c0.9,0.9,1.3,1.9,1.3,3.2s-0.4,2.4-1.3,3.2C20.1,20.5,19,21,17.8,21s-2.4-0.4-3.2-1.3c-0.9-0.8-1.3-1.9-1.3-3.2
C13.2,15.1,13.7,14.1,14.5,13.2z"/>
<path class="st0" d="M5,37v-1.7c0-0.5,0.1-1,0.4-1.5s0.7-0.8,1.2-1.1c2.4-1.1,4.3-1.8,5.9-2.2c1.6-0.4,3.3-0.5,5.2-0.5
s3.7,0.2,5.2,0.5c0.4,0.1,0.9,0.2,1.4,0.4c0.7-0.8,1.5-1.6,2.4-2.3c-1.1-0.4-2.2-0.7-3.1-1c-1.9-0.5-3.8-0.7-5.9-0.7
s-4,0.2-5.9,0.7s-4,1.2-6.4,2.3c-1,0.5-1.9,1.2-2.5,2.1C2.3,33,2,34,2,35.2V40h19.1c0-1,0.1-2,0.3-3H5z"/>
<polygon class="st0" points="33.4,43.9 33.4,37.4 26.9,37.4 26.9,33.5 33.4,33.5 33.4,27 37.3,27 37.3,33.5 43.8,33.5 43.8,37.4
37.3,37.4 37.3,43.9 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -28,6 +28,7 @@ export const ADMIN_ONLY_TABS = [
HEADER_MENUS_USER,
HEADER_MENUS_WORKFLOW,
HEADER_MENUS_PARTNER,
HEADER_MENUS_WORKFLOW,
];
/**

View File

@ -11,7 +11,15 @@ interface HeaderProps {
const Header: React.FC<HeaderProps> = (props) => {
const { userName } = props;
const location = useLocation();
return getHeader(location.pathname, userName);
const splitPaths = location.pathname.split("/");
let path = location.pathname;
if (splitPaths.length >= 2) {
path = `/${splitPaths[1]}`;
}
return getHeader(path, userName);
};
export default Header;

View File

@ -0,0 +1,4 @@
export * from "./typistGroupSlice";
export * from "./state";
export * from "./selectors";
export * from "./operations";

View File

@ -0,0 +1,43 @@
import { createAsyncThunk } from "@reduxjs/toolkit";
import type { RootState } from "app/store";
import { openSnackbar } from "features/ui/uiSlice";
import { getTranslationID } from "translation";
import { AccountsApi, GetTypistGroupsResponse } from "../../../api/api";
import { Configuration } from "../../../api/configuration";
import { ErrorObject, createErrorObject } from "../../../common/errors";
export const listTypistGroupsAsync = createAsyncThunk<
GetTypistGroupsResponse,
void,
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("dictations/listTypistGroupsAsync", 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);
try {
const typistGroup = await accountsApi.getTypistGroups({
headers: { authorization: `Bearer ${accessToken}` },
});
return typistGroup.data;
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -0,0 +1,7 @@
import { RootState } from "app/store";
export const selectTypistGroups = (state: RootState) =>
state.typistGroup.domain.typistGroups;
export const selectIsLoading = (state: RootState) =>
state.typistGroup.apps.isLoading;

View File

@ -0,0 +1,14 @@
import { TypistGroup } from "../../../api/api";
export interface TypistGroupState {
apps: Apps;
domain: Domain;
}
export interface Apps {
isLoading: boolean;
}
export interface Domain {
typistGroups: TypistGroup[];
}

View File

@ -0,0 +1,32 @@
import { createSlice } from "@reduxjs/toolkit";
import { TypistGroupState } from "./state";
import { listTypistGroupsAsync } from "./operations";
const initialState: TypistGroupState = {
apps: {
isLoading: false,
},
domain: {
typistGroups: [],
},
};
export const typistGroupSlice = createSlice({
name: "typistGroup",
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(listTypistGroupsAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(listTypistGroupsAsync.fulfilled, (state, action) => {
state.domain.typistGroups = action.payload.typistGroups;
state.apps.isLoading = false;
});
builder.addCase(listTypistGroupsAsync.rejected, (state) => {
state.apps.isLoading = false;
});
},
});
export default typistGroupSlice.reducer;

View File

@ -1,18 +1,118 @@
import React from "react";
import React, { useEffect } from "react";
import Header from "components/header";
import Footer from "components/footer";
import styles from "styles/app.module.scss";
import { UpdateTokenTimer } from "components/auth/updateTokenTimer";
import progress_activit from "assets/images/progress_activit.svg";
import undo from "assets/images/undo.svg";
import group_add from "assets/images/group_add.svg";
import { useDispatch, useSelector } from "react-redux";
import {
selectTypistGroups,
selectIsLoading,
listTypistGroupsAsync,
} from "features/workflow/typistGroup";
import { AppDispatch } from "app/store";
import { useTranslation } from "react-i18next";
import { getTranslationID } from "translation";
const TypistGroupSettingPage: React.FC = (): JSX.Element => (
<div className={styles.wrap}>
<Header userName="XXXXXX" />
<UpdateTokenTimer />
<main className={styles.main}>
<div className="">Transcriptionist Group Setting</div>
</main>
<Footer />
</div>
);
const TypistGroupSettingPage: React.FC = (): JSX.Element => {
const dispatch: AppDispatch = useDispatch();
const [t] = useTranslation();
const isLoading = useSelector(selectIsLoading);
const typistGroup = useSelector(selectTypistGroups);
useEffect(() => {
dispatch(listTypistGroupsAsync());
}, [dispatch]);
return (
<div className={styles.wrap}>
<Header userName="XXXXXX" />
<UpdateTokenTimer />
<main className={styles.main}>
<div>
<div className={styles.pageHeader}>
<h1 className={styles.pageTitle}>
{t(getTranslationID("workflowPage.label.title"))}
</h1>
<p className={styles.pageTx}>{` ${t(
getTranslationID("typistGroupSetting.label.title")
)}`}</p>
</div>
<section className={styles.workflow}>
<div>
<ul className={styles.menuAction}>
<li>
<a
href="/workflow"
className={`${styles.menuLink} ${styles.isActive}`}
>
<img src={undo} alt="" className={styles.menuIcon} />
{t(getTranslationID("typistGroupSetting.label.return"))}
</a>
</li>
<li>
<a className={`${styles.menuLink} ${styles.isActive}`}>
<img src={group_add} alt="" className={styles.menuIcon} />
{t(getTranslationID("typistGroupSetting.label.addGroup"))}
</a>
</li>
</ul>
<table className={`${styles.table} ${styles.group}`}>
<tr className={styles.tableHeader}>
<th className={styles.noLine}>
{t(getTranslationID("typistGroupSetting.label.groupName"))}
</th>
<th>{/** empty th */}</th>
</tr>
{!isLoading && typistGroup.length === 0 ? (
<p style={{ margin: "10px", textAlign: "center" }}>
{t(getTranslationID("common.message.listEmpty"))}
</p>
) : (
typistGroup.map((group) => (
<tr key={group.id}>
<td>{group.name}</td>
<td>
<ul
className={`${styles.menuAction} ${styles.inTable}`}
>
<li>
<a
className={`${styles.menuLink} ${styles.isActive}`}
>
{t(
getTranslationID(
"typistGroupSetting.label.edit"
)
)}
</a>
</li>
</ul>
</td>
</tr>
))
)}
</table>
{isLoading && (
<img
src={progress_activit}
className={styles.icLoading}
alt="Loading"
/>
)}
</div>
</section>
</div>
</main>
<Footer />
</div>
);
};
export default TypistGroupSettingPage;

View File

@ -352,5 +352,19 @@
"inputEmptyError": "(de)この項目の入力は必須です。入力してください。",
"licenseAllocationFailure": "(de)ライセンスの割り当てに失敗しました。他のライセンスを選択して再度割り当てをしてください。"
}
},
"workflowPage": {
"label": {
"title": "(de)Workflow"
}
},
"typistGroupSetting": {
"label": {
"title": "(de)Transctiprionist Group",
"return": "(de)Return",
"addGroup": "(de)Add Group",
"groupName": "(de)Group Name",
"edit": "(de)Edit"
}
}
}
}

View File

@ -352,5 +352,19 @@
"inputEmptyError": "この項目の入力は必須です。入力してください。",
"licenseAllocationFailure": "ライセンスの割り当てに失敗しました。他のライセンスを選択して再度割り当てをしてください。"
}
},
"workflowPage": {
"label": {
"title": "Workflow"
}
},
"typistGroupSetting": {
"label": {
"title": "Transctiprionist Group",
"return": "Return",
"addGroup": "Add Group",
"groupName": "Group Name",
"edit": "Edit"
}
}
}
}

View File

@ -352,5 +352,19 @@
"inputEmptyError": "(es)この項目の入力は必須です。入力してください。",
"licenseAllocationFailure": "(es)ライセンスの割り当てに失敗しました。他のライセンスを選択して再度割り当てをしてください。"
}
},
"workflowPage": {
"label": {
"title": "(es)Workflow"
}
},
"typistGroupSetting": {
"label": {
"title": "(es)Transctiprionist Group",
"return": "(es)Return",
"addGroup": "(es)Add Group",
"groupName": "(es)Group Name",
"edit": "(es)Edit"
}
}
}
}

View File

@ -352,5 +352,19 @@
"inputEmptyError": "(fr)この項目の入力は必須です。入力してください。",
"licenseAllocationFailure": "(fr)ライセンスの割り当てに失敗しました。他のライセンスを選択して再度割り当てをしてください。"
}
},
"workflowPage": {
"label": {
"title": "(fr)Workflow"
}
},
"typistGroupSetting": {
"label": {
"title": "(fr)Transctiprionist Group",
"return": "(fr)Return",
"addGroup": "(fr)Add Group",
"groupName": "(fr)Group Name",
"edit": "(fr)Edit"
}
}
}
}