Merged PR 434: 画面実装(テンプレートファイルアップロードPopupデザイン)

## 概要
[Task2664: 画面実装(テンプレートファイルアップロードPopupデザイン)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2664)

- デザイン反映
- ファイルピッカーからファイル取得→storeに保存

## レビューポイント
- デザイン反映に不備はあるか
- 想定としてstoreに保持したfileを、Operationsでblobにアップロードする流れにしようとしているがよさそうか

## 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/Task2664?csf=1&web=1&e=optFai

## 動作確認状況
- ローカルで確認、develop環境で確認など

## 補足
- 相談、参考資料などがあれば
This commit is contained in:
saito.k 2023-09-25 07:53:45 +00:00
parent ecc44e58e0
commit 9ca4ae61f8
12 changed files with 281 additions and 102 deletions

View File

@ -0,0 +1,2 @@
// アップロード可能なファイルサイズの上限(MB)
export const UPLOAD_FILE_SIZE_LIMIT: number = 5 * 1024 * 1024;

View File

@ -1,7 +1,26 @@
import { RootState } from "app/store";
import { UPLOAD_FILE_SIZE_LIMIT } from "./constants";
export const selectTemplates = (state: RootState) =>
state.template.domain.templates;
export const selectIsLoading = (state: RootState) =>
state.template.apps.isLoading;
export const selectUploadFile = (state: RootState) =>
state.template.apps.uploadFile;
export const selectUploadFileError = (state: RootState) => {
const { uploadFile } = state.template.apps;
// 必須チェック
if (!uploadFile) {
return { hasErrorRequired: true, hasErrorFileSize: false };
}
// ファイルサイズチェック(5MB)
if (uploadFile.size > UPLOAD_FILE_SIZE_LIMIT) {
return { hasErrorRequired: false, hasErrorFileSize: true };
}
return { hasErrorRequired: false, hasErrorFileSize: false };
};

View File

@ -7,6 +7,7 @@ export interface TemplateState {
export interface Apps {
isLoading: boolean;
uploadFile?: File;
}
export interface Domain {

View File

@ -1,20 +1,27 @@
import { createSlice } from "@reduxjs/toolkit";
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
import { TemplateState } from "./state";
import { listTemplateAsync } from "./operations";
const initialState: TemplateState = {
apps: {
isLoading: false,
uploadFile: undefined,
},
domain: {
templates: undefined,
},
domain: {},
};
export const templateSlice = createSlice({
name: "template",
initialState,
reducers: {},
reducers: {
cleanupTemplate: (state) => {
state.apps.uploadFile = initialState.apps.uploadFile;
},
changeUploadFile: (state, action: PayloadAction<{ file: File }>) => {
const { file } = action.payload;
state.apps.uploadFile = file;
},
},
extraReducers: (builder) => {
builder.addCase(listTemplateAsync.pending, (state) => {
state.apps.isLoading = true;
@ -31,4 +38,6 @@ export const templateSlice = createSlice({
},
});
export const { changeUploadFile, cleanupTemplate } = templateSlice.actions;
export default templateSlice.reducer;

View File

@ -127,7 +127,8 @@ const AccountPage: React.FC = (): JSX.Element => {
{isTier5 && !viewInfo.account.parentAccountName && (
<dd className={styles.form}>
<select
className={`${styles.formInput} ${styles.required}`}
className={styles.formInput}
required
onChange={(event) => {
dispatch(
changeDealer({
@ -210,7 +211,8 @@ const AccountPage: React.FC = (): JSX.Element => {
<dd className={styles.form}>
<select
name=""
className={`${styles.formInput} ${styles.required}`}
className={styles.formInput}
required
onChange={(event) => {
dispatch(
changePrimaryAdministrator({
@ -269,7 +271,8 @@ const AccountPage: React.FC = (): JSX.Element => {
<dd className={styles.form}>
<select
name=""
className={`${styles.formInput} ${styles.required}`}
className={styles.formInput}
required
onChange={(event) => {
dispatch(
changeSecondryAdministrator({

View File

@ -0,0 +1,99 @@
import React, { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { getTranslationID } from "translation";
import styles from "styles/app.module.scss";
import { useDispatch, useSelector } from "react-redux";
import { AppDispatch } from "app/store";
import {
cleanupTemplate,
selectUploadFile,
changeUploadFile,
} from "features/workflow/template";
import close from "../../assets/images/close.svg";
interface AddTemplateFilePopupProps {
onClose: () => void;
isOpen: boolean;
}
export const AddTemplateFilePopup: React.FC<AddTemplateFilePopupProps> = (
props: AddTemplateFilePopupProps
) => {
const { onClose, isOpen } = props;
const [t] = useTranslation();
const dispatch: AppDispatch = useDispatch();
// 閉じるボタンを押したときの処理
const closePopup = useCallback(() => {
onClose();
dispatch(cleanupTemplate());
}, [onClose, dispatch]);
// アップロード対象のファイル情報
const uploadFile = useSelector(selectUploadFile);
// ファイルが選択されたときの処理
const handleFileChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
// 選択されたファイルを取得(複数選択されても先頭を取得)
const file = event.target.files?.[0];
// ファイルが選択されていれば、storeに保存
if (file) {
dispatch(changeUploadFile({ file }));
}
},
[dispatch]
);
return (
<div className={`${styles.modal} ${isOpen ? styles.isShow : ""}`}>
<div className={styles.modalBox}>
<p className={styles.modalTitle}>
{t(getTranslationID("templateFilePage.label.addTemplate"))}
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions */}
<img
src={close}
className={styles.modalTitleIcon}
alt="close"
onClick={closePopup}
/>
</p>
<form className={styles.form}>
<dl className={`${styles.formList} ${styles.hasbg}`}>
<dt className={styles.formTitle} />
<dd
className={`${styles.full} ${styles.alignCenter} ${styles.last}`}
>
<p className={styles.formFileName} style={{ marginRight: "5px" }}>
{uploadFile?.name ??
t(getTranslationID("templateFilePage.label.notFileChosen"))}
</p>
<label htmlFor="template" className={styles.formFileButton}>
{t(getTranslationID("templateFilePage.label.chooseFile"))}
<input
type="file"
id="template"
className={`${styles.formInput} ${styles.isHide}`}
onChange={handleFileChange}
/>
</label>
</dd>
<dd className={`${styles.full} ${styles.alignCenter}`}>
<input
type="button"
name="submit"
value={t(
getTranslationID("templateFilePage.label.addTemplate")
)}
className={`${styles.formSubmit} ${styles.marginBtm1} ${styles.isActive}`}
/>
</dd>
</dl>
</form>
</div>
</div>
);
};

View File

@ -1,4 +1,4 @@
import React, { useEffect } from "react";
import React, { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { AppDispatch } from "app/store";
import Header from "components/header";
@ -7,13 +7,14 @@ import { useTranslation } from "react-i18next";
import { getTranslationID } from "translation";
import undo from "assets/images/undo.svg";
import styles from "styles/app.module.scss";
import {
listTemplateAsync,
selectIsLoading,
selectTemplates,
} from "features/workflow/template";
import addTemplate from "assets/images/template_add.svg";
import progress_activit from "assets/images/progress_activit.svg";
import {
selectTemplates,
listTemplateAsync,
selectIsLoading,
} from "features/workflow/template";
import { AddTemplateFilePopup } from "./addTemplateFilePopup";
export const TemplateFilePage: React.FC = () => {
const dispatch: AppDispatch = useDispatch();
@ -21,89 +22,106 @@ export const TemplateFilePage: React.FC = () => {
const templates = useSelector(selectTemplates);
const isLoading = useSelector(selectIsLoading);
// 追加Popupの表示制御
const [isShowAddPopup, setIsShowAddPopup] = useState<boolean>(false);
useEffect(() => {
dispatch(listTemplateAsync());
}, [dispatch]);
return (
<div className={styles.wrap}>
<Header userName="XXXXXXXX" />
<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("templateFilePage.label.title"))}
</p>
</div>
</div>
<section className={styles.workflow}>
<>
<AddTemplateFilePopup
onClose={() => {
setIsShowAddPopup(false);
}}
isOpen={isShowAddPopup}
/>
<div className={styles.wrap}>
<Header userName="XXXXXXXX" />
<UpdateTokenTimer />
<main className={styles.main}>
<div>
<ul className={`${styles.menuAction} ${styles.worktype}`}>
<li>
<a
href="/workflow"
className={`${styles.menuLink} ${styles.isActive}`}
>
<img src={undo} alt="" className={styles.menuIcon} />
{t(getTranslationID("common.label.return"))}
</a>
</li>
<li>
<a className={`${styles.menuLink} ${styles.isActive}`}>
<img src={addTemplate} alt="" className={styles.menuIcon} />
{t(getTranslationID("templateFilePage.label.addTemplate"))}
</a>
</li>
</ul>
<table className={`${styles.table} ${styles.template}`}>
<tr className={styles.tableHeader}>
<th className={styles.noLine}>
{t(getTranslationID("templateFilePage.label.fileName"))}
</th>
<th>{/** empty th */}</th>
</tr>
{templates?.map((template) => (
<tr key={template.id}>
<td>{template.name}</td>
<td>
<ul className={`${styles.menuAction} ${styles.inTable}`}>
<li>
<a
href=""
className={`${styles.menuLink} ${styles.isActive}`}
>
{t(getTranslationID("common.label.delete"))}
</a>
</li>
</ul>
</td>
</tr>
))}
{!isLoading && templates?.length === 0 && (
<p
style={{
margin: "10px",
textAlign: "center",
}}
>
{t(getTranslationID("common.message.listEmpty"))}
</p>
)}
{isLoading && (
<img
src={progress_activit}
className={styles.icLoading}
alt="Loading"
/>
)}
</table>
<div className={styles.pageHeader}>
<h1 className={styles.pageTitle}>
{t(getTranslationID("workflowPage.label.title"))}
</h1>
<p className={styles.pageTx}>
{t(getTranslationID("templateFilePage.label.title"))}
</p>
</div>
</div>
</section>
</main>
</div>
<section className={styles.workflow}>
<div>
<ul className={`${styles.menuAction} ${styles.worktype}`}>
<li>
<a
href="/workflow"
className={`${styles.menuLink} ${styles.isActive}`}
>
<img src={undo} alt="" className={styles.menuIcon} />
{t(getTranslationID("common.label.return"))}
</a>
</li>
<li>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<a
className={`${styles.menuLink} ${styles.isActive}`}
onClick={() => {
setIsShowAddPopup(true);
}}
>
<img src={addTemplate} alt="" className={styles.menuIcon} />
{t(getTranslationID("templateFilePage.label.addTemplate"))}
</a>
</li>
</ul>
<table className={`${styles.table} ${styles.template}`}>
<tr className={styles.tableHeader}>
<th className={styles.noLine}>
{t(getTranslationID("templateFilePage.label.fileName"))}
</th>
<th>{/** empty th */}</th>
</tr>
{templates?.map((template) => (
<tr key={template.id}>
<td>{template.name}</td>
<td>
<ul className={`${styles.menuAction} ${styles.inTable}`}>
<li>
<a
href=""
className={`${styles.menuLink} ${styles.isActive}`}
>
{t(getTranslationID("common.label.delete"))}
</a>
</li>
</ul>
</td>
</tr>
))}
{!isLoading && templates?.length === 0 && (
<p
style={{
margin: "10px",
textAlign: "center",
}}
>
{t(getTranslationID("common.message.listEmpty"))}
</p>
)}
{isLoading && (
<img
src={progress_activit}
className={styles.icLoading}
alt="Loading"
/>
)}
</table>
</div>
</section>
</main>
</div>
</>
);
};

View File

@ -22,6 +22,7 @@ declare const classNames: {
readonly buttonText: "buttonText";
readonly formList: "formList";
readonly formTitle: "formTitle";
readonly alignCenter: "alignCenter";
readonly overLine: "overLine";
readonly full: "full";
readonly hasbg: "hasbg";
@ -38,10 +39,13 @@ declare const classNames: {
readonly formError: "formError";
readonly formConfirm: "formConfirm";
readonly formSubmit: "formSubmit";
readonly formButtonFul: "formButtonFul";
readonly formButton: "formButton";
readonly formDelete: "formDelete";
readonly formBack: "formBack";
readonly formButtonTx: "formButtonTx";
readonly formDone: "formDone";
readonly formTrash: "formTrash";
readonly listVertical: "listVertical";
readonly listHeader: "listHeader";
readonly boxFlex: "boxFlex";
@ -109,6 +113,11 @@ declare const classNames: {
readonly isDisable: "isDisable";
readonly icCheckCircle: "icCheckCircle";
readonly icInTable: "icInTable";
readonly manage: "manage";
readonly manageInfo: "manageInfo";
readonly txNormal: "txNormal";
readonly manageIcon: "manageIcon";
readonly manageIconClose: "manageIconClose";
readonly history: "history";
readonly cardHistory: "cardHistory";
readonly partner: "partner";
@ -116,6 +125,7 @@ declare const classNames: {
readonly displayOptions: "displayOptions";
readonly tableFilter: "tableFilter";
readonly tableFilter2: "tableFilter2";
readonly mnBack: "mnBack";
readonly txWsline: "txWsline";
readonly hidePri: "hidePri";
readonly opPri: "opPri";
@ -175,6 +185,7 @@ declare const classNames: {
readonly op9: "op9";
readonly hideO10: "hideO10";
readonly op10: "op10";
readonly property: "property";
readonly formChange: "formChange";
readonly chooseMember: "chooseMember";
readonly holdMember: "holdMember";
@ -184,7 +195,6 @@ declare const classNames: {
readonly template: "template";
readonly worktype: "worktype";
readonly selectMenu: "selectMenu";
readonly alignCenter: "alignCenter";
readonly alignLeft: "alignLeft";
readonly alignRight: "alignRight";
readonly floatNone: "floatNone";
@ -206,9 +216,7 @@ declare const classNames: {
readonly paddSide1: "paddSide1";
readonly paddSide2: "paddSide2";
readonly paddSide3: "paddSide3";
readonly txNormal: "txNormal";
readonly txIcon: "txIcon";
readonly txWswrap: "txWswrap";
readonly required: "required";
};
export = classNames;

View File

@ -417,7 +417,12 @@
"label": {
"title": "(de)Template List",
"addTemplate": "(de)Add Template",
"fileName": "(de)Flie Name"
"fileName": "(de)Flie Name",
"chooseFile": "(de)Choose File",
"notFileChosen": "(de)- Not file chosen -",
"fileSizeTerms": "(de)Flie Name",
"fileSizeError": "(de)選択されたファイルのサイズが大きすぎます。サイズがMB以下のファイルを選択してください。",
"fileEmptyError": "(de)ファイル選択は必須です。ファイルを選択してください。"
}
},
"partnerPage": {

View File

@ -417,7 +417,12 @@
"label": {
"title": "Template List",
"addTemplate": "Add Template",
"fileName": "Flie Name"
"fileName": "Flie Name",
"chooseFile": "Choose File",
"notFileChosen": "- Not file chosen -",
"fileSizeTerms": "Flie Name",
"fileSizeError": "選択されたファイルのサイズが大きすぎます。サイズがMB以下のファイルを選択してください。",
"fileEmptyError": "ファイル選択は必須です。ファイルを選択してください。"
}
},
"partnerPage": {

View File

@ -417,7 +417,12 @@
"label": {
"title": "(es)Template List",
"addTemplate": "(es)Add Template",
"fileName": "(es)Flie Name"
"fileName": "(es)Flie Name",
"chooseFile": "(es)Choose File",
"notFileChosen": "(es)- Not file chosen -",
"fileSizeTerms": "(es)Flie Name",
"fileSizeError": "(es)選択されたファイルのサイズが大きすぎます。サイズがMB以下のファイルを選択してください。",
"fileEmptyError": "(es)ファイル選択は必須です。ファイルを選択してください。"
}
},
"partnerPage": {

View File

@ -417,7 +417,12 @@
"label": {
"title": "(fr)Template List",
"addTemplate": "(fr)Add Template",
"fileName": "(fr)Flie Name"
"fileName": "(fr)Flie Name",
"chooseFile": "(fr)Choose File",
"notFileChosen": "(fr)- Not file chosen -",
"fileSizeTerms": "(fr)Flie Name",
"fileSizeError": "(fr)選択されたファイルのサイズが大きすぎます。サイズがMB以下のファイルを選択してください。",
"fileEmptyError": "(fr)ファイル選択は必須です。ファイルを選択してください。"
}
},
"partnerPage": {