Merged PR 315: アカウント登録画面修正

## 概要
[Task2350: アカウント登録画面修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2350)

- アカウント登録画面のディーラー周りの実装を追加しました。
  - クエリパラメータからアカウントIDを拾って入力画面に反映するようにしています。
- 言語情報をクエリパラメータから取得できるようにしました。

## レビューポイント
- 画面の挙動として不自然な点はないか
- ディーラー情報の取り扱いは適切か

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

## 動作確認状況
- ローカルで確認
This commit is contained in:
makabe.t 2023-08-14 09:29:16 +00:00
parent 415a76b6bf
commit 1145debabf
7 changed files with 266 additions and 15 deletions

View File

@ -373,6 +373,31 @@ export interface CreatePartnerAccountRequest {
*/ */
'email': string; 'email': string;
} }
/**
*
* @export
* @interface Dealer
*/
export interface Dealer {
/**
* ID
* @type {number}
* @memberof Dealer
*/
'id': number;
/**
*
* @type {string}
* @memberof Dealer
*/
'name': string;
/**
* (ISO 3166-1 alpha-2)
* @type {string}
* @memberof Dealer
*/
'country': string;
}
/** /**
* *
* @export * @export
@ -392,6 +417,19 @@ export interface ErrorResponse {
*/ */
'code': string; 'code': string;
} }
/**
*
* @export
* @interface GetDealersResponse
*/
export interface GetDealersResponse {
/**
*
* @type {Array<Dealer>}
* @memberof GetDealersResponse
*/
'dealers': Array<Dealer>;
}
/** /**
* *
* @export * @export
@ -1504,6 +1542,36 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat
options: localVarRequestOptions, options: localVarRequestOptions,
}; };
}, },
/**
*
* @summary
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getDealers: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/accounts/dealers`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/** /**
* *
* @summary * @summary
@ -1798,6 +1866,16 @@ export const AccountsApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.createPartnerAccount(createPartnerAccountRequest, options); const localVarAxiosArgs = await localVarAxiosParamCreator.createPartnerAccount(createPartnerAccountRequest, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
/**
*
* @summary
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getDealers(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<GetDealersResponse>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getDealers(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/** /**
* *
* @summary * @summary
@ -1902,6 +1980,15 @@ export const AccountsApiFactory = function (configuration?: Configuration, baseP
createPartnerAccount(createPartnerAccountRequest: CreatePartnerAccountRequest, options?: any): AxiosPromise<object> { createPartnerAccount(createPartnerAccountRequest: CreatePartnerAccountRequest, options?: any): AxiosPromise<object> {
return localVarFp.createPartnerAccount(createPartnerAccountRequest, options).then((request) => request(axios, basePath)); return localVarFp.createPartnerAccount(createPartnerAccountRequest, options).then((request) => request(axios, basePath));
}, },
/**
*
* @summary
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getDealers(options?: any): AxiosPromise<GetDealersResponse> {
return localVarFp.getDealers(options).then((request) => request(axios, basePath));
},
/** /**
* *
* @summary * @summary
@ -2003,6 +2090,17 @@ export class AccountsApi extends BaseAPI {
return AccountsApiFp(this.configuration).createPartnerAccount(createPartnerAccountRequest, options).then((request) => request(this.axios, this.basePath)); return AccountsApiFp(this.configuration).createPartnerAccount(createPartnerAccountRequest, options).then((request) => request(this.axios, this.basePath));
} }
/**
*
* @summary
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof AccountsApi
*/
public getDealers(options?: AxiosRequestConfig) {
return AccountsApiFp(this.configuration).getDealers(options).then((request) => request(this.axios, this.basePath));
}
/** /**
* *
* @summary * @summary

View File

@ -3,7 +3,11 @@ import type { RootState } from "app/store";
import { ErrorObject, createErrorObject } from "common/errors"; import { ErrorObject, createErrorObject } from "common/errors";
import { getTranslationID } from "translation"; import { getTranslationID } from "translation";
import { closeSnackbar, openSnackbar } from "features/ui/uiSlice"; import { closeSnackbar, openSnackbar } from "features/ui/uiSlice";
import { AccountsApi, CreateAccountRequest } from "../../api/api"; import {
AccountsApi,
CreateAccountRequest,
GetDealersResponse,
} from "../../api/api";
import { Configuration } from "../../api/configuration"; import { Configuration } from "../../api/configuration";
export const signupAsync = createAsyncThunk< export const signupAsync = createAsyncThunk<
@ -56,3 +60,36 @@ export const signupAsync = createAsyncThunk<
return thunkApi.rejectWithValue({ error }); return thunkApi.rejectWithValue({ error });
} }
}); });
export const getDealersAsync = createAsyncThunk<
GetDealersResponse,
void,
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("login/getDealersAsync", async (args, thunkApi) => {
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const config = new Configuration(configuration);
const accountApi = new AccountsApi(config);
try {
const res = await accountApi.getDealers();
return res.data;
} catch (e) {
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -1,3 +1,4 @@
import { Dealer } from "api/api";
import { RootState } from "app/store"; import { RootState } from "app/store";
export const selectInputValidationErrors = (state: RootState) => { export const selectInputValidationErrors = (state: RootState) => {
@ -56,3 +57,18 @@ export const selectPassword = (state: RootState) => state.signup.apps.password;
export const selectPageState = (state: RootState) => export const selectPageState = (state: RootState) =>
state.signup.apps.pageState; state.signup.apps.pageState;
export const selectAllDealers = (state: RootState) =>
state.signup.domain.dealers;
export const selectSameCountryDealers = (state: RootState) => {
const { dealers } = state.signup.domain;
const { country } = state.signup.apps;
return dealers.filter((x: Dealer) => x.country === country);
};
export const selectSelectedDealer = (state: RootState) => {
const { dealers } = state.signup.domain;
const { dealer } = state.signup.apps;
return dealers.find((x: Dealer) => x.id === dealer);
};

View File

@ -1,16 +1,20 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { SignupState } from "./state"; import { SignupState } from "./state";
import { signupAsync } from "./operations"; import { getDealersAsync, signupAsync } from "./operations";
const initialState: SignupState = { const initialState: SignupState = {
apps: { apps: {
pageState: "input", pageState: "input",
company: "", company: "",
country: "", country: "",
dealer: "", dealer: undefined,
adminName: "", adminName: "",
email: "", email: "",
password: "", password: "",
dealers: [],
},
domain: {
dealers: [],
}, },
}; };
@ -32,10 +36,11 @@ export const signupSlice = createSlice({
changeCountry: (state, action: PayloadAction<{ country: string }>) => { changeCountry: (state, action: PayloadAction<{ country: string }>) => {
const { country } = action.payload; const { country } = action.payload;
state.apps.country = country; state.apps.country = country;
state.apps.dealer = undefined;
}, },
changeDealer: (state, action: PayloadAction<{ dealer: string }>) => { changeDealer: (state, action: PayloadAction<{ dealer: number }>) => {
const { dealer } = action.payload; const { dealer } = action.payload;
state.apps.dealer = dealer; state.apps.dealer = Number.isNaN(dealer) ? undefined : dealer;
}, },
changeAdminName: (state, action: PayloadAction<{ adminName: string }>) => { changeAdminName: (state, action: PayloadAction<{ adminName: string }>) => {
const { adminName } = action.payload; const { adminName } = action.payload;
@ -60,6 +65,15 @@ export const signupSlice = createSlice({
builder.addCase(signupAsync.rejected, () => { builder.addCase(signupAsync.rejected, () => {
// //
}); });
builder.addCase(getDealersAsync.pending, () => {
//
});
builder.addCase(getDealersAsync.fulfilled, (state, action) => {
state.domain.dealers = action.payload.dealers;
});
builder.addCase(getDealersAsync.rejected, () => {
//
});
}, },
}); });
export const { export const {

View File

@ -1,13 +1,21 @@
import { Dealer } from "api/api";
export interface SignupState { export interface SignupState {
apps: Apps; apps: Apps;
domain: Domain;
} }
export interface Apps { export interface Apps {
pageState: "input" | "confirm" | "complete"; pageState: "input" | "confirm" | "complete";
company: string; company: string;
country: string; country: string;
dealer: string; dealer?: number | undefined;
adminName: string; adminName: string;
email: string; email: string;
password: string; password: string;
dealers: Dealer[];
}
export interface Domain {
dealers: Dealer[];
} }

View File

@ -13,6 +13,7 @@ import {
selectAdminName, selectAdminName,
selectEmail, selectEmail,
selectPassword, selectPassword,
selectSelectedDealer,
} from "../../features/signup/selectors"; } from "../../features/signup/selectors";
import { signupAsync } from "../../features/signup/operations"; import { signupAsync } from "../../features/signup/operations";
@ -25,13 +26,14 @@ const SignupConfirm: React.FC = (): JSX.Element => {
const adminName = useSelector(selectAdminName); const adminName = useSelector(selectAdminName);
const adminMail = useSelector(selectEmail); const adminMail = useSelector(selectEmail);
const adminPassword = useSelector(selectPassword); const adminPassword = useSelector(selectPassword);
const dealer = useSelector(selectSelectedDealer);
const onSubmit = useCallback(() => { const onSubmit = useCallback(() => {
dispatch( dispatch(
signupAsync({ signupAsync({
companyName, companyName,
country, country,
dealerAccountId: 0, dealerAccountId: dealer?.id ?? 0,
adminName, adminName,
adminMail, adminMail,
adminPassword, adminPassword,
@ -39,7 +41,15 @@ const SignupConfirm: React.FC = (): JSX.Element => {
token: "", token: "",
}) })
); );
}, [dispatch, companyName, country, adminName, adminMail, adminPassword]); }, [
dispatch,
companyName,
country,
dealer,
adminName,
adminMail,
adminPassword,
]);
return ( return (
<main className={styles.main}> <main className={styles.main}>
@ -71,7 +81,9 @@ const SignupConfirm: React.FC = (): JSX.Element => {
<dt className={styles.marginBtm1}> <dt className={styles.marginBtm1}>
{t(getTranslationID("signupConfirmPage.label.dealer"))} {t(getTranslationID("signupConfirmPage.label.dealer"))}
</dt> </dt>
<dd /> <dd>
<p className={styles.formConfirm}>{dealer?.name}&nbsp;</p>
</dd>
<dt className={styles.formTitle}> <dt className={styles.formTitle}>
{t(getTranslationID("signupConfirmPage.text.adminInfoTitle"))} {t(getTranslationID("signupConfirmPage.text.adminInfoTitle"))}

View File

@ -1,30 +1,37 @@
import { AppDispatch } from "app/store"; import { AppDispatch } from "app/store";
import { import {
selectAdminName, selectAdminName,
selectAllDealers,
selectCompany, selectCompany,
selectCountry, selectCountry,
selectDealer,
selectEmail, selectEmail,
selectInputValidationErrors, selectInputValidationErrors,
selectSameCountryDealers,
} from "features/signup/selectors"; } from "features/signup/selectors";
import { import {
changeAdminName, changeAdminName,
changeCompany, changeCompany,
changeCountry, changeCountry,
changeDealer,
changeEmail, changeEmail,
changePageState, changePageState,
changePassword, changePassword,
} from "features/signup/signupSlice"; } from "features/signup/signupSlice";
import React, { useCallback, useState } from "react"; import React, { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { useNavigate } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import { getTranslationID } from "translation"; import { getTranslationID } from "translation";
import styles from "styles/app.module.scss"; import styles from "styles/app.module.scss";
import { getDealersAsync } from "features/signup/operations";
import { LANGUAGE_LIST } from "features/top/constants";
import { COUNTRY_LIST } from "./constants"; import { COUNTRY_LIST } from "./constants";
const SignupInput: React.FC = (): JSX.Element => { const SignupInput: React.FC = (): JSX.Element => {
const dispatch: AppDispatch = useDispatch(); const dispatch: AppDispatch = useDispatch();
const [t] = useTranslation(); const [t, i18n] = useTranslation();
const { search } = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const [isPasswordHide, setIsPasswordHide] = useState<boolean>(true); const [isPasswordHide, setIsPasswordHide] = useState<boolean>(true);
const [isOpenPolicy, setIsOpenPolicy] = useState<boolean>(false); const [isOpenPolicy, setIsOpenPolicy] = useState<boolean>(false);
@ -39,6 +46,7 @@ const SignupInput: React.FC = (): JSX.Element => {
hasErrorIncorrectEmail, hasErrorIncorrectEmail,
hasErrorIncorrectPassword, hasErrorIncorrectPassword,
} = useSelector(selectInputValidationErrors); } = useSelector(selectInputValidationErrors);
const onSubmit = useCallback(() => { const onSubmit = useCallback(() => {
if ( if (
hasErrorEmptyAdminName || hasErrorEmptyAdminName ||
@ -63,11 +71,49 @@ const SignupInput: React.FC = (): JSX.Element => {
hasErrorIncorrectPassword, hasErrorIncorrectPassword,
]); ]);
const allDealers = useSelector(selectAllDealers);
const dealers = useSelector(selectSameCountryDealers);
const company = useSelector(selectCompany); const company = useSelector(selectCompany);
const country = useSelector(selectCountry); const country = useSelector(selectCountry);
const dealer = useSelector(selectDealer);
const adminName = useSelector(selectAdminName); const adminName = useSelector(selectAdminName);
const email = useSelector(selectEmail); const email = useSelector(selectEmail);
// 入力画面の初期化時の処理
useEffect(() => {
dispatch(getDealersAsync());
}, [dispatch]);
useEffect(() => {
// 外部のWebサイトからの遷移時にURLのパラメータを取得
// 以下のようなURLで遷移してきた場合に、Dealerと言語を変更する
// https://xxx/signup?dealer=1&language=en
// dealer={account_id第四階層のアカウントID} でDealerを指定
// language={language(en/de/fr/es)} で言語を指定
const query = new URLSearchParams(search);
const dealerId = parseInt(query.get("dealer") ?? "", 10);
const language = query.get("language");
// URLで言語が指定されていたら言語を変更
if (language && LANGUAGE_LIST.map((x) => x.value).includes(language)) {
i18n.changeLanguage(language);
// 既にcookieに選択言語があれば削除
document.cookie = "language=; max-age=0";
// cookieの期限は1年
document.cookie = `language=${language}; max-age=31536000`;
}
// URLでDealerが指定されていたら、そのdealerを選択国も選択したDealerの国に変更
if (!Number.isNaN(dealerId)) {
const urlDealer = allDealers.find((x) => x.id === dealerId);
if (urlDealer) {
dispatch(changeCountry({ country: urlDealer.country }));
dispatch(changeDealer({ dealer: urlDealer.id }));
}
}
}, [i18n, dispatch, search, allDealers]);
return ( return (
<div className={styles.wrap}> <div className={styles.wrap}>
<header /> <header />
@ -145,9 +191,29 @@ const SignupInput: React.FC = (): JSX.Element => {
</dd> </dd>
<dt> {t(getTranslationID("signupPage.label.dealer"))}</dt> <dt> {t(getTranslationID("signupPage.label.dealer"))}</dt>
<dd> <dd>
<select className={styles.formInput}> <select
<option>Select dealer</option> className={styles.formInput}
<option value="Tokyo">Tokyo</option> onChange={(event) => {
dispatch(
changeDealer({
dealer: parseInt(event.target.value, 10),
})
);
}}
value={dealers.find((x) => x.id === dealer)?.id ?? NaN}
>
{[
{
id: NaN,
country: "",
name: "Select dealer",
},
...dealers,
].map((x) => (
<option key={x.id} value={x.id}>
{x.name}
</option>
))}
</select> </select>
<span className={styles.formComment}> <span className={styles.formComment}>
{t(getTranslationID("signupPage.text.dealerExplanation"))} {t(getTranslationID("signupPage.text.dealerExplanation"))}