Merged PR 69: 画面実装(スナックバー)

## 概要
[Task1506: 画面実装(スナックバー)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/1506)

- スナックバーを実装し、アカウント登録のエラー時にエラースナックバーを表示するようにしました。

## レビューポイント
- スナックバーの構成に問題はないか
- 別タスクのコードが混ざっているので、スナックバー実装周りのご確認をお願いします。
  - component/snackbar
  - App.tsx
  - features/ui
  - features/signup/operations

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

## 動作確認状況
- ローカルで確認
This commit is contained in:
makabe.t 2023-04-10 10:34:59 +00:00
parent 50f4cf5070
commit 33509fb228
18 changed files with 283 additions and 45 deletions

View File

@ -4,11 +4,14 @@ import { PublicClientApplication } from "@azure/msal-browser";
import { MsalProvider, useMsal } from "@azure/msal-react";
import { msalConfig } from "common/msalConfig";
import { useEffect, useLayoutEffect } from "react";
import { useDispatch } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
import globalAxios, { AxiosError, AxiosResponse } from "axios";
import { clearToken } from "features/auth";
import { useTranslation } from "react-i18next";
import "./styles/GlobalStyle.css";
import Snackbar from "components/snackbar";
import { selectSnackber } from "features/ui/selectors";
import { closeSnackbar } from "features/ui/uiSlice";
const App = (): JSX.Element => {
const dispatch = useDispatch();
@ -48,12 +51,25 @@ const App = (): JSX.Element => {
}
}, [i18n]);
const snackbarInfo = useSelector(selectSnackber);
return (
<>
<Snackbar
isOpen={snackbarInfo.isOpen}
level={snackbarInfo.level}
message={t(snackbarInfo.message)}
duration={snackbarInfo.duration}
onClose={() => {
dispatch(closeSnackbar());
}}
/>
<MsalProvider instance={pca}>
<BrowserRouter>
<AppRouter />
</BrowserRouter>
</MsalProvider>
</>
);
};

View File

@ -3,6 +3,7 @@ import login from "features/login/loginSlice";
import auth from "features/auth/authSlice";
import signup from "features/signup/signupSlice";
import verify from "features/verify/verifySlice";
import ui from "features/ui/uiSlice";
export const store = configureStore({
reducer: {
@ -10,6 +11,7 @@ export const store = configureStore({
auth,
signup,
verify,
ui,
},
});

View File

@ -21,5 +21,6 @@ export const errorCodes = [
"E000105", // トークン発行元エラー
"E000106", // トークンアルゴリズムエラー
"E010201", // 未認証ユーザエラー
"E010202", // 認証済ユーザエラー
"E010301", // メールアドレス登録済みエラー
] as const;

View File

@ -0,0 +1,71 @@
import React, { useCallback, useEffect, useRef } from "react";
import reportWhite from "../../assets/images/report_white.svg";
import closeWhite from "../../assets/images/close_white.svg";
import checkCircleWhite from "../../assets/images/check_circle_white.svg";
import styles from "../../styles/app.module.scss";
export type SnackbarLevel = "info" | "error";
interface SnackbarProps {
isOpen: boolean;
level: SnackbarLevel;
message: string;
duration?: number;
onClose: () => void;
}
const Snackbar: React.FC<SnackbarProps> = (props) => {
const { isOpen, level, message, duration, onClose } = props;
const timer = useRef(0);
const onTimeout = useCallback(() => {
timer.current = window.setTimeout(() => onClose(), duration);
}, [onClose, duration]);
const onCloseSnackbar = useCallback(() => {
clearTimeout(timer.current);
onClose();
}, [onClose]);
useEffect(() => {
if (duration !== 0) {
onTimeout();
}
return () => {
clearTimeout(timer.current);
};
}, [duration, onTimeout]);
const isAlert = level === "error" ? styles.isAlert : "";
const isShow = isOpen ? styles.isShow : "";
return (
<div className={`${styles.snackbar} ${isAlert} ${isShow}`}>
{level === "error" ? (
<img
src={checkCircleWhite}
className={styles.snackbarIcon}
alt="check"
/>
) : (
<img src={reportWhite} className={styles.snackbarIcon} alt="report" />
)}
<p className={styles.txNormal}>{message}</p>
{level === "error" && (
<button type="button" onClick={onCloseSnackbar}>
<img
src={closeWhite}
className={styles.snackbarIconClose}
alt="close"
/>
</button>
)}
</div>
);
};
Snackbar.defaultProps = {
duration: 0,
};
export default Snackbar;

View File

@ -1,5 +1,8 @@
import { createAsyncThunk } from "@reduxjs/toolkit";
import type { RootState } from "app/store";
import { ErrorObject, createErrorObject } from "common/errors";
import { getTranslationID } from "translation";
import { closeSnackbar, openSnackbar } from "features/ui/uiSlice";
import { AccountsApi, CreateAccountRequest } from "../../api/api";
import { Configuration } from "../../api/configuration";
@ -11,7 +14,7 @@ export const signupAsync = createAsyncThunk<
{
// rejectした時の返却値の型
rejectValue: {
/* Empty Object */
error: ErrorObject;
};
}
>("login/signupAsync", async (args, thunkApi) => {
@ -24,10 +27,32 @@ export const signupAsync = createAsyncThunk<
const accountApi = new AccountsApi(config);
try {
const { data } = await accountApi.createAccount(createAccountRequest);
thunkApi.dispatch(closeSnackbar());
await accountApi.createAccount(createAccountRequest);
thunkApi.dispatch(closeSnackbar());
return {};
} catch (e) {
return thunkApi.rejectWithValue({});
const error = createErrorObject(e);
if (error.code === "E010301") {
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID(
"signupConfirmPage.message.emailConflictError"
),
})
);
} else {
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
}
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -51,13 +51,13 @@ export const signupSlice = createSlice({
},
},
extraReducers: (builder) => {
builder.addCase(signupAsync.pending, (state) => {
builder.addCase(signupAsync.pending, () => {
//
});
builder.addCase(signupAsync.fulfilled, (state) => {
state.apps.pageState = "complete";
});
builder.addCase(signupAsync.rejected, (state) => {
builder.addCase(signupAsync.rejected, () => {
//
});
},

View File

@ -0,0 +1,2 @@
// 標準のスナックバー表示時間(ミリ秒)
export const DEFAULT_SNACKBAR_DURATION = 3000;

View File

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

View File

@ -0,0 +1,15 @@
import { RootState } from "app/store";
import { SnackbarLevel } from "components/snackbar";
export const selectSnackber = (
state: RootState
): {
isOpen: boolean;
level: SnackbarLevel;
message: string;
duration?: number;
} => {
const { isOpen, level, message, duration } = state.ui;
return { isOpen, level, message, duration };
};

View File

@ -0,0 +1,8 @@
import { SnackbarLevel } from "components/snackbar";
export interface UIState {
isOpen: boolean;
level: SnackbarLevel;
message: string;
duration?: number;
}

View File

@ -0,0 +1,38 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { SnackbarLevel } from "components/snackbar";
import { UIState } from "./state";
import { DEFAULT_SNACKBAR_DURATION } from "./constants";
const initialState: UIState = {
isOpen: false,
level: "error",
message: "",
};
export const uiSlice = createSlice({
name: "ui",
initialState,
reducers: {
openSnackbar: (
state,
action: PayloadAction<{
level: SnackbarLevel;
message: string;
duration?: number;
}>
) => {
const { level, message, duration } = action.payload;
state.isOpen = true;
state.level = level;
state.message = message;
state.duration =
level === "error" ? undefined : duration ?? DEFAULT_SNACKBAR_DURATION;
},
closeSnackbar: (state) => {
state.isOpen = false;
},
},
});
export const { openSnackbar, closeSnackbar } = uiSlice.actions;
export default uiSlice.reducer;

View File

@ -23,7 +23,7 @@ export const verifySlice = createSlice({
const { payload } = action;
// メール認証済みかをエラーコードから判定
if (payload?.error.code === "E010301") {
if (payload?.error.code === "E010202") {
state.apps.VerifyState = "alreadySuccess";
} else {
state.apps.VerifyState = "failed";

View File

@ -1,4 +1,3 @@
import { AppDispatch } from "app/store";
import React from "react";
import { useTranslation } from "react-i18next";
import { getTranslationID } from "translation";

View File

@ -105,8 +105,9 @@ const SignupInput: React.FC = (): JSX.Element => {
/>
{isPushCreateButton && hasErrorEmptyCompany && (
<span className={styles.formError}>
{" "}
{t(getTranslationID("common.message.inputEmptyError"))}
{t(
getTranslationID("signupPage.message.inputEmptyError")
)}
</span>
)}
</dd>
@ -132,7 +133,11 @@ const SignupInput: React.FC = (): JSX.Element => {
))}
</select>
{isPushCreateButton && hasErrorEmptyCountry && (
<span className={styles.formError}>Error message</span>
<span className={styles.formError}>
{t(
getTranslationID("signupPage.message.inputEmptyError")
)}
</span>
)}
<span className={styles.formComment}>
{t(getTranslationID("signupPage.text.countryExplanation"))}
@ -170,7 +175,9 @@ const SignupInput: React.FC = (): JSX.Element => {
{isPushCreateButton && hasErrorEmptyAdminName && (
<span className={styles.formError}>
{" "}
{t(getTranslationID("common.message.inputEmptyError"))}
{t(
getTranslationID("signupPage.message.inputEmptyError")
)}
</span>
)}
</dd>
@ -193,13 +200,17 @@ const SignupInput: React.FC = (): JSX.Element => {
/>
{isPushCreateButton && hasErrorEmptyEmail && (
<span className={styles.formError}>
{t(getTranslationID("common.message.inputEmptyError"))}
{t(
getTranslationID("signupPage.message.inputEmptyError")
)}
</span>
)}
{isPushCreateButton && hasErrorIncorrectEmail && (
<span className={styles.formError}>
{t(
getTranslationID("common.message.emailIncorrectError")
getTranslationID(
"signupPage.message.emailIncorrectError"
)
)}
</span>
)}
@ -234,14 +245,16 @@ const SignupInput: React.FC = (): JSX.Element => {
/>
{isPushCreateButton && hasErrorEmptyPassword && (
<span className={styles.formError}>
{t(getTranslationID("common.message.inputEmptyError"))}
{t(
getTranslationID("signupPage.message.inputEmptyError")
)}
</span>
)}
{isPushCreateButton && hasErrorIncorrectPassword && (
<span className={styles.formError}>
{t(
getTranslationID(
"common.message.passwordIncorrectError"
"signupPage.message.passwordIncorrectError"
)
)}
</span>

View File

@ -3,7 +3,8 @@
"message": {
"inputEmptyError": "(de)Error Message",
"passwordIncorrectError": "(de)Error Message",
"emailIncorrectError": "(de)Error Message"
"emailIncorrectError": "(de)Error Message",
"internalServerError": "(de)処理に失敗しました。時間をおいて再実行しても解決しない場合はシステム管理者にお問い合わせください。"
},
"label": {
"cancel": "(de)Cancel",
@ -25,6 +26,11 @@
}
},
"signupPage": {
"message": {
"inputEmptyError": "(de)この項目の入力は必須です。入力してください。",
"passwordIncorrectError": "(de)入力されたパスワードがルールを満たしていません。下記のルールを満たすパスワードを入力してください。",
"emailIncorrectError": "(de)メールアドレスの形式が不正です。正しいメールアドレスの形式で入力してください。"
},
"text": {
"title": "(de)Create your account",
"pageExplanation": "(de)Explanation...",
@ -48,12 +54,14 @@
}
},
"signupConfirmPage": {
"message": {
"emailConflictError": "(de)このメールアドレスは既に登録されています。他のメールアドレスで登録してください。"
},
"text": {
"title": "(de)Confirmation",
"pageExplanation": "(de)Explanation ......",
"accountInfoTitle": "(de)Your account information",
"adminInfoTitle": "(de)Primary administrator's information",
"createdInfo": "(de)Your account has been created and a verification email has been set to your registered email address. Please click on the verification link included in the email to activate your account."
"adminInfoTitle": "(de)Primary administrator's information"
},
"label": {
"company": "(de)Company Name",
@ -62,12 +70,15 @@
"adminName": "(de)Admin Name",
"email": "(de)Email",
"password": "(de)Password",
"title": "(de)Account created"
"signupButton": "(de)Sign up"
}
},
"signupCompletePage": {
"text": {
"createdInfo": "(de)Your account has been created and a verification email has been set to your registered email address. Please click on the verification link included in the email to activate your account."
},
"label": {
"signupButton": "(de)Sign up"
"title": "(de)Account created"
}
},
"signupVerifyPage": {

View File

@ -3,7 +3,8 @@
"message": {
"inputEmptyError": "Error Message",
"passwordIncorrectError": "Error Message",
"emailIncorrectError": "Error Message"
"emailIncorrectError": "Error Message",
"internalServerError": "処理に失敗しました。時間をおいて再実行しても解決しない場合はシステム管理者にお問い合わせください。"
},
"label": {
"cancel": "Cancel",
@ -25,6 +26,11 @@
}
},
"signupPage": {
"message": {
"inputEmptyError": "この項目の入力は必須です。入力してください。",
"passwordIncorrectError": "入力されたパスワードがルールを満たしていません。下記のルールを満たすパスワードを入力してください。",
"emailIncorrectError": "メールアドレスの形式が不正です。正しいメールアドレスの形式で入力してください。"
},
"text": {
"title": "Create your account",
"pageExplanation": "Explanation...",
@ -48,12 +54,14 @@
}
},
"signupConfirmPage": {
"message": {
"emailConflictError": "このメールアドレスは既に登録されています。他のメールアドレスで登録してください。"
},
"text": {
"title": "Confirmation",
"pageExplanation": "Explanation ......",
"accountInfoTitle": "Your account information",
"adminInfoTitle": "Primary administrator's information",
"createdInfo": "Your account has been created and a verification email has been set to your registered email address. Please click on the verification link included in the email to activate your account."
"adminInfoTitle": "Primary administrator's information"
},
"label": {
"company": "Company Name",
@ -62,12 +70,15 @@
"adminName": "Admin Name",
"email": "Email",
"password": "Password",
"title": "Account created"
"signupButton": "Sign up"
}
},
"signupCompletePage": {
"text": {
"createdInfo": "Your account has been created and a verification email has been set to your registered email address. Please click on the verification link included in the email to activate your account."
},
"label": {
"signupButton": "Sign up"
"title": "Account created"
}
},
"signupVerifyPage": {

View File

@ -3,7 +3,8 @@
"message": {
"inputEmptyError": "(es)Error Message",
"passwordIncorrectError": "(es)Error Message",
"emailIncorrectError": "(es)Error Message"
"emailIncorrectError": "(es)Error Message",
"internalServerError": "(es)処理に失敗しました。時間をおいて再実行しても解決しない場合はシステム管理者にお問い合わせください。"
},
"label": {
"cancel": "(es)Cancel",
@ -25,6 +26,11 @@
}
},
"signupPage": {
"message": {
"inputEmptyError": "(es)この項目の入力は必須です。入力してください。",
"passwordIncorrectError": "(es)入力されたパスワードがルールを満たしていません。下記のルールを満たすパスワードを入力してください。",
"emailIncorrectError": "(es)メールアドレスの形式が不正です。正しいメールアドレスの形式で入力してください。"
},
"text": {
"title": "(es)Create your account",
"pageExplanation": "(es)Explanation...",
@ -48,12 +54,14 @@
}
},
"signupConfirmPage": {
"message": {
"emailConflictError": "(es)このメールアドレスは既に登録されています。他のメールアドレスで登録してください。"
},
"text": {
"title": "(es)Confirmation",
"pageExplanation": "(es)Explanation ......",
"accountInfoTitle": "(es)Your account information",
"adminInfoTitle": "(es)Primary administrator's information",
"createdInfo": "(es)Your account has been created and a verification email has been set to your registered email address. Please click on the verification link included in the email to activate your account."
"adminInfoTitle": "(es)Primary administrator's information"
},
"label": {
"company": "(es)Company Name",
@ -62,12 +70,15 @@
"adminName": "(es)Admin Name",
"email": "(es)Email",
"password": "(es)Password",
"title": "(es)Account created"
"signupButton": "(es)Sign up"
}
},
"signupCompletePage": {
"text": {
"createdInfo": "(es)Your account has been created and a verification email has been set to your registered email address. Please click on the verification link included in the email to activate your account."
},
"label": {
"signupButton": "(es)Sign up"
"title": "(es)Account created"
}
},
"signupVerifyPage": {

View File

@ -3,7 +3,8 @@
"message": {
"inputEmptyError": "(fr)Error Message",
"passwordIncorrectError": "(fr)Error Message",
"emailIncorrectError": "(fr)Error Message"
"emailIncorrectError": "(fr)Error Message",
"internalServerError": "(fr)処理に失敗しました。時間をおいて再実行しても解決しない場合はシステム管理者にお問い合わせください。"
},
"label": {
"cancel": "(fr)Cancel",
@ -25,6 +26,11 @@
}
},
"signupPage": {
"message": {
"inputEmptyError": "(fr)この項目の入力は必須です。入力してください。",
"passwordIncorrectError": "(fr)入力されたパスワードがルールを満たしていません。下記のルールを満たすパスワードを入力してください。",
"emailIncorrectError": "(fr)メールアドレスの形式が不正です。正しいメールアドレスの形式で入力してください。"
},
"text": {
"title": "(fr)Create your account",
"pageExplanation": "(fr)Explanation...",
@ -48,12 +54,14 @@
}
},
"signupConfirmPage": {
"message": {
"emailConflictError": "(fr)このメールアドレスは既に登録されています。他のメールアドレスで登録してください。"
},
"text": {
"title": "(fr)Confirmation",
"pageExplanation": "(fr)Explanation ......",
"accountInfoTitle": "(fr)Your account information",
"adminInfoTitle": "(fr)Primary administrator's information",
"createdInfo": "(fr)Your account has been created and a verification email has been set to your registered email address. Please click on the verification link included in the email to activate your account."
"adminInfoTitle": "(fr)Primary administrator's information"
},
"label": {
"company": "(fr)Company Name",
@ -62,12 +70,15 @@
"adminName": "(fr)Admin Name",
"email": "(fr)Email",
"password": "(fr)Password",
"title": "(fr)Account created"
"signupButton": "(fr)Sign up"
}
},
"signupCompletePage": {
"text": {
"createdInfo": "(fr)Your account has been created and a verification email has been set to your registered email address. Please click on the verification link included in the email to activate your account."
},
"label": {
"signupButton": "(fr)Sign up"
"title": "(fr)Account created"
}
},
"signupVerifyPage": {