Merged PR 800: 画面実装(一括追加ボタン&ポップアップ画面)

## 概要
[Task3753: 画面実装(一括追加ボタン&ポップアップ画面)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3753)

- ユーザー一括登録画面を実装しました。
  - 一括登録ポップアップ
    - テンプレートCSVダウンロード
    - ファイルインポート
      - エラー行表示

## レビューポイント
- 行エラーの条件、内容は認識通りでしょうか?
- 画面の表示内容は認識通りでしょうか?
- CSV変換時にworkerを有効にしているとエラーとなりうまくいかないのでOFFにしてしまいましたが問題ないでしょうか?
  - @<湯本 開> さん

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

## 動作確認状況
- ローカルで確認
 -  ファイルチェックするところまで
This commit is contained in:
makabe.t 2024-03-06 01:58:32 +00:00
parent d6a47932e7
commit da40e8f09c
15 changed files with 1026 additions and 35 deletions

View File

@ -1344,6 +1344,92 @@ export interface LicenseOrder {
*/
'status': string;
}
/**
*
* @export
* @interface MultipleImportErrors
*/
export interface MultipleImportErrors {
/**
*
* @type {string}
* @memberof MultipleImportErrors
*/
'name': string;
/**
*
* @type {number}
* @memberof MultipleImportErrors
*/
'line': number;
/**
*
* @type {string}
* @memberof MultipleImportErrors
*/
'errorCode': string;
}
/**
*
* @export
* @interface MultipleImportUser
*/
export interface MultipleImportUser {
/**
*
* @type {string}
* @memberof MultipleImportUser
*/
'name': string;
/**
*
* @type {string}
* @memberof MultipleImportUser
*/
'email': string;
/**
* 0(none)/1(author)/2(typist)
* @type {number}
* @memberof MultipleImportUser
*/
'role': number;
/**
*
* @type {string}
* @memberof MultipleImportUser
*/
'authorId'?: string;
/**
* 0(false)/1(true)
* @type {number}
* @memberof MultipleImportUser
*/
'autoRenew': number;
/**
* 0(false)/1(true)
* @type {number}
* @memberof MultipleImportUser
*/
'notification': number;
/**
* 0(false)/1(true)
* @type {number}
* @memberof MultipleImportUser
*/
'encryption'?: number;
/**
*
* @type {string}
* @memberof MultipleImportUser
*/
'encryptionPassword'?: string;
/**
* 0(false)/1(true)
* @type {number}
* @memberof MultipleImportUser
*/
'prompt'?: number;
}
/**
*
* @export
@ -1525,6 +1611,56 @@ export interface PostDeleteUserRequest {
*/
'userId': number;
}
/**
*
* @export
* @interface PostMultipleImportsCompleteRequest
*/
export interface PostMultipleImportsCompleteRequest {
/**
* ID
* @type {number}
* @memberof PostMultipleImportsCompleteRequest
*/
'accountId': number;
/**
* CSVファイル名
* @type {string}
* @memberof PostMultipleImportsCompleteRequest
*/
'filename': string;
/**
* (UNIXTIME/)
* @type {number}
* @memberof PostMultipleImportsCompleteRequest
*/
'requestTime': number;
/**
*
* @type {Array<MultipleImportErrors>}
* @memberof PostMultipleImportsCompleteRequest
*/
'errors': Array<MultipleImportErrors>;
}
/**
*
* @export
* @interface PostMultipleImportsRequest
*/
export interface PostMultipleImportsRequest {
/**
* CSVファイル名
* @type {string}
* @memberof PostMultipleImportsRequest
*/
'filename': string;
/**
*
* @type {Array<MultipleImportUser>}
* @memberof PostMultipleImportsRequest
*/
'users': Array<MultipleImportUser>;
}
/**
*
* @export
@ -7431,6 +7567,86 @@ export const UsersApiAxiosParamCreator = function (configuration?: Configuration
options: localVarRequestOptions,
};
},
/**
*
* @summary
* @param {PostMultipleImportsRequest} postMultipleImportsRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
multipleImports: async (postMultipleImportsRequest: PostMultipleImportsRequest, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'postMultipleImportsRequest' is not null or undefined
assertParamExists('multipleImports', 'postMultipleImportsRequest', postMultipleImportsRequest)
const localVarPath = `/users/multiple-imports`;
// 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: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(postMultipleImportsRequest, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @summary
* @param {PostMultipleImportsCompleteRequest} postMultipleImportsCompleteRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
multipleImportsComplate: async (postMultipleImportsCompleteRequest: PostMultipleImportsCompleteRequest, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'postMultipleImportsCompleteRequest' is not null or undefined
assertParamExists('multipleImportsComplate', 'postMultipleImportsCompleteRequest', postMultipleImportsCompleteRequest)
const localVarPath = `/users/multiple-imports/complete`;
// 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: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(postMultipleImportsCompleteRequest, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @summary
@ -7710,6 +7926,32 @@ export const UsersApiFp = function(configuration?: Configuration) {
const operationBasePath = operationServerMap['UsersApi.getUsers']?.[index]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
},
/**
*
* @summary
* @param {PostMultipleImportsRequest} postMultipleImportsRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async multipleImports(postMultipleImportsRequest: PostMultipleImportsRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.multipleImports(postMultipleImportsRequest, options);
const index = configuration?.serverIndex ?? 0;
const operationBasePath = operationServerMap['UsersApi.multipleImports']?.[index]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
},
/**
*
* @summary
* @param {PostMultipleImportsCompleteRequest} postMultipleImportsCompleteRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async multipleImportsComplate(postMultipleImportsCompleteRequest: PostMultipleImportsCompleteRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.multipleImportsComplate(postMultipleImportsCompleteRequest, options);
const index = configuration?.serverIndex ?? 0;
const operationBasePath = operationServerMap['UsersApi.multipleImportsComplate']?.[index]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
},
/**
*
* @summary
@ -7858,6 +8100,26 @@ export const UsersApiFactory = function (configuration?: Configuration, basePath
getUsers(options?: any): AxiosPromise<GetUsersResponse> {
return localVarFp.getUsers(options).then((request) => request(axios, basePath));
},
/**
*
* @summary
* @param {PostMultipleImportsRequest} postMultipleImportsRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
multipleImports(postMultipleImportsRequest: PostMultipleImportsRequest, options?: any): AxiosPromise<object> {
return localVarFp.multipleImports(postMultipleImportsRequest, options).then((request) => request(axios, basePath));
},
/**
*
* @summary
* @param {PostMultipleImportsCompleteRequest} postMultipleImportsCompleteRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
multipleImportsComplate(postMultipleImportsCompleteRequest: PostMultipleImportsCompleteRequest, options?: any): AxiosPromise<object> {
return localVarFp.multipleImportsComplate(postMultipleImportsCompleteRequest, options).then((request) => request(axios, basePath));
},
/**
*
* @summary
@ -8012,6 +8274,30 @@ export class UsersApi extends BaseAPI {
return UsersApiFp(this.configuration).getUsers(options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary
* @param {PostMultipleImportsRequest} postMultipleImportsRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof UsersApi
*/
public multipleImports(postMultipleImportsRequest: PostMultipleImportsRequest, options?: AxiosRequestConfig) {
return UsersApiFp(this.configuration).multipleImports(postMultipleImportsRequest, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary
* @param {PostMultipleImportsCompleteRequest} postMultipleImportsCompleteRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof UsersApi
*/
public multipleImportsComplate(postMultipleImportsCompleteRequest: PostMultipleImportsCompleteRequest, options?: AxiosRequestConfig) {
return UsersApiFp(this.configuration).multipleImportsComplate(postMultipleImportsCompleteRequest, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M450-313v-371L330-564l-43-43 193-193 193 193-43 43-120-120v371h-60ZM220-160q-24 0-42-18t-18-42v-143h60v143h520v-143h60v143q0 24-18 42t-42 18H220Z"/></svg>

After

Width:  |  Height:  |  Size: 251 B

View File

@ -40,7 +40,7 @@ export const parseCSV = async (csvString: string): Promise<CSVType[]> =>
new Promise((resolve, reject) => {
Papa.parse<CSVType>(csvString, {
download: false,
worker: true,
worker: false, // XXX: workerを使うとエラーが発生するためfalseに設定
header: true,
dynamicTyping: true,
complete: (results: ParseResult<CSVType>) => {

View File

@ -9,6 +9,7 @@ import {
UsersApi,
LicensesApi,
GetAllocatableLicensesResponse,
MultipleImportUser,
} from "../../api/api";
import { Configuration } from "../../api/configuration";
import { ErrorObject, createErrorObject } from "../../common/errors";
@ -498,3 +499,74 @@ export const deleteUserAsync = createAsyncThunk<
return thunkApi.rejectWithValue({ error });
}
});
export const importUsersAsync = createAsyncThunk<
// 正常時の戻り値の型
{
/* Empty Object */
},
// 引数
void,
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("users/importUsersAsync", async (args, thunkApi) => {
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const { importFileName, importUsers } = state.user.apps;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const usersApi = new UsersApi(config);
try {
if (importFileName === undefined) {
throw new Error("importFileName is undefined");
}
// CSVデータをAPIに送信するためのデータに変換
const users: MultipleImportUser[] = importUsers.map((user) => ({
name: user.name ?? "",
email: user.email ?? "",
role: user.role ?? 0,
authorId: user.author_id ?? undefined,
autoRenew: user.auto_renew ?? 0,
notification: user.notification ?? 0,
encryption: user.encryption ?? undefined,
encryptionPassword: user.encryption_password ?? undefined,
prompt: user.prompt ?? undefined,
}));
await usersApi.multipleImports(
{
filename: importFileName,
users,
},
{ headers: { authorization: `Bearer ${accessToken}` } }
);
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("userListPage.message.importSuccess"),
})
);
return {};
} catch (e) {
// e ⇒ errorObjectに変換
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -382,3 +382,142 @@ const convertValueBasedOnLicenseStatus = (
remaining: undefined,
};
};
export const selectImportFileName = (state: RootState) =>
state.user.apps.importFileName;
export const selectImportValidationErrors = (state: RootState) => {
const csvUsers = state.user.apps.importUsers;
let rowNumber = 0;
const invalidInput: number[] = [];
const duplicatedEmailsMap = new Map<string, number>();
const duplicatedAuthorIdsMap = new Map<string, number>();
const overMaxRow = csvUsers.length > 100;
// eslint-disable-next-line no-restricted-syntax
for (const csvUser of csvUsers) {
rowNumber += 1;
// メールアドレスの重複がある場合、エラーとしてその行番号を追加する
const duplicatedEmailUser = csvUsers.filter(
(x) => x.email === csvUser.email
);
if (duplicatedEmailUser.length > 1) {
if (csvUser.email !== null && !duplicatedEmailsMap.has(csvUser.email)) {
duplicatedEmailsMap.set(csvUser.email, rowNumber);
}
}
// AuthorIDの重複がある場合、エラーとしてその行番号を追加する
const duplicatedAuthorIdUser = csvUsers.filter(
(x) => x.author_id === csvUser.author_id
);
if (duplicatedAuthorIdUser.length > 1) {
if (
csvUser.author_id !== null &&
!duplicatedAuthorIdsMap.has(csvUser.author_id)
) {
duplicatedAuthorIdsMap.set(csvUser.author_id, rowNumber);
}
}
// name
if (csvUser.name === null || csvUser.name.length > 225) {
invalidInput.push(rowNumber);
// eslint-disable-next-line no-continue
continue;
}
// email
const emailPattern =
/^[a-zA-Z0-9!#$%&'_`/=~+\-?^{|}.]+@[a-zA-Z0-9!#$%&'_`/=~+\-?^{|}.]*\.[a-zA-Z0-9!#$%&'_`/=~+\-?^{|}.]*[a-zA-Z]$/;
if (
csvUser.name === null ||
csvUser.name.length > 225 ||
!emailPattern.test(csvUser.email ?? "")
) {
invalidInput.push(rowNumber);
// eslint-disable-next-line no-continue
continue;
}
// role
if (csvUser.role === null || ![0, 1, 2].includes(csvUser.role)) {
invalidInput.push(rowNumber);
// eslint-disable-next-line no-continue
continue;
}
// role=1(Author)
if (csvUser.role === 1) {
// author_id
if (csvUser.author_id === null || csvUser.author_id.length > 16) {
invalidInput.push(rowNumber);
// eslint-disable-next-line no-continue
continue;
}
// 半角英数字と_の組み合わせで文字まで
const charaTypePattern = /^[A-Z0-9_]{1,16}$/;
const charaType = new RegExp(charaTypePattern).test(csvUser.author_id);
if (!charaType) {
invalidInput.push(rowNumber);
// eslint-disable-next-line no-continue
continue;
}
// encryption
if (csvUser.encryption === null || ![0, 1].includes(csvUser.encryption)) {
invalidInput.push(rowNumber);
// eslint-disable-next-line no-continue
continue;
}
if (csvUser.encryption === 1) {
// encryption_password
if (csvUser.encryption === 1) {
const regex = /^[!-~]{4,16}$/;
if (!regex.test(csvUser.encryption_password ?? "")) {
invalidInput.push(rowNumber);
// eslint-disable-next-line no-continue
continue;
}
}
}
// prompt
if (csvUser.prompt === null || ![0, 1].includes(csvUser.prompt)) {
invalidInput.push(rowNumber);
// eslint-disable-next-line no-continue
continue;
}
}
// auto_renew
if (csvUser.auto_renew === null || ![0, 1].includes(csvUser.auto_renew)) {
invalidInput.push(rowNumber);
// eslint-disable-next-line no-continue
continue;
}
// notification
if (
csvUser.notification === null ||
![0, 1].includes(csvUser.notification)
) {
invalidInput.push(rowNumber);
// eslint-disable-next-line no-continue
continue;
}
}
const duplicatedEmails = Array.from(duplicatedEmailsMap.values());
const duplicatedAuthorIds = Array.from(duplicatedAuthorIdsMap.values());
return {
invalidInput,
duplicatedEmails,
duplicatedAuthorIds,
overMaxRow,
};
};

View File

@ -1,4 +1,9 @@
import { User, AllocatableLicenseInfo } from "../../api/api";
import { CSVType } from "common/parser";
import {
User,
AllocatableLicenseInfo,
MultipleImportUser,
} from "../../api/api";
import { AddUser, UpdateUser, LicenseAllocateUser } from "./types";
export interface UsersState {
@ -19,4 +24,6 @@ export interface Apps {
selectedlicenseId: number;
hasPasswordMask: boolean;
isLoading: boolean;
importFileName: string | undefined;
importUsers: CSVType[];
}

View File

@ -1,5 +1,6 @@
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
import { USER_ROLES } from "components/auth/constants";
import { CSVType } from "common/parser";
import { UsersState } from "./state";
import {
addUserAsync,
@ -8,6 +9,7 @@ import {
getAllocatableLicensesAsync,
deallocateLicenseAsync,
deleteUserAsync,
importUsersAsync,
} from "./operations";
import { RoleType, UserView } from "./types";
@ -61,6 +63,8 @@ const initialState: UsersState = {
selectedlicenseId: 0,
hasPasswordMask: false,
isLoading: false,
importFileName: undefined,
importUsers: [],
},
};
@ -242,6 +246,17 @@ export const userSlice = createSlice({
state.apps.licenseAllocateUser = initialState.apps.licenseAllocateUser;
state.apps.selectedlicenseId = initialState.apps.selectedlicenseId;
},
changeImportFileName: (
state,
action: PayloadAction<{ fileName: string }>
) => {
const { fileName } = action.payload;
state.apps.importFileName = fileName;
},
changeImportCsv: (state, action: PayloadAction<{ users: CSVType[] }>) => {
const { users } = action.payload;
state.apps.importUsers = users;
},
},
extraReducers: (builder) => {
builder.addCase(listUsersAsync.pending, (state) => {
@ -300,6 +315,15 @@ export const userSlice = createSlice({
builder.addCase(deleteUserAsync.rejected, (state) => {
state.apps.isLoading = false;
});
builder.addCase(importUsersAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(importUsersAsync.fulfilled, (state) => {
state.apps.isLoading = false;
});
builder.addCase(importUsersAsync.rejected, (state) => {
state.apps.isLoading = false;
});
},
});
@ -327,6 +351,8 @@ export const {
changeLicenseAllocateUser,
changeSelectedlicenseId,
cleanupLicenseAllocateInfo,
changeImportFileName,
changeImportCsv,
} = userSlice.actions;
export default userSlice.reducer;

View File

@ -0,0 +1,254 @@
import { AppDispatch } from "app/store";
import React, { useState, useCallback } from "react";
import styles from "styles/app.module.scss";
import { useDispatch, useSelector } from "react-redux";
import { getTranslationID } from "translation";
import { useTranslation } from "react-i18next";
import {
selectIsLoading,
importUsersAsync,
changeImportFileName,
changeImportCsv,
selectImportFileName,
selectImportValidationErrors,
} from "features/user";
import { parseCSV } from "common/parser";
import close from "../../assets/images/close.svg";
import download from "../../assets/images/download.svg";
import upload from "../../assets/images/upload.svg";
import progress_activit from "../../assets/images/progress_activit.svg";
interface UserAddPopupProps {
isOpen: boolean;
onClose: () => void;
}
export const ImportPopup: React.FC<UserAddPopupProps> = (props) => {
const { isOpen, onClose } = props;
const dispatch: AppDispatch = useDispatch();
const { t } = useTranslation();
// AddUserの情報を取得
const closePopup = useCallback(() => {
setIsPushImportButton(false);
onClose();
}, [onClose]);
const [isPushImportButton, setIsPushImportButton] = useState<boolean>(false);
const isLoading = useSelector(selectIsLoading);
const importFileName = useSelector(selectImportFileName);
const { invalidInput, duplicatedEmails, duplicatedAuthorIds, overMaxRow } =
useSelector(selectImportValidationErrors);
const onDownloadCsv = useCallback(() => {
// csvファイルダウンロード処理
const filename = `import_users.csv`;
const importCsvHeader = [
"name",
"email",
"role",
"author_id",
"auto_renew",
"notification",
"encryption",
"encryption_password",
"prompt",
].toString();
const blob = new Blob([importCsvHeader], {
type: "mime",
});
const blobURL = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = blobURL;
a.download = filename;
document.body.appendChild(a);
a.click();
a.parentNode?.removeChild(a);
}, []);
// ファイルが選択されたときの処理
const handleFileChange = useCallback(
async (event: React.ChangeEvent<HTMLInputElement>) => {
// 選択されたファイルを取得(複数選択されても先頭を取得)
const file = event.target.files?.[0];
// ファイルが選択されていれば、storeに保存
if (file) {
const text = await file.text();
const users = await parseCSV(text.trimEnd());
dispatch(changeImportCsv({ users }));
dispatch(changeImportFileName({ fileName: file.name }));
}
// 同名のファイルを選択した場合、onChangeが発火しないため、valueをクリアする
event.target.value = "";
},
[dispatch]
);
const onImportUsers = useCallback(async () => {
setIsPushImportButton(true);
if (
invalidInput.length > 0 ||
duplicatedEmails.length > 0 ||
duplicatedAuthorIds.length > 0 ||
overMaxRow
) {
return;
}
await dispatch(importUsersAsync());
setIsPushImportButton(false);
}, [
dispatch,
invalidInput,
duplicatedEmails,
duplicatedAuthorIds,
overMaxRow,
]);
return (
<div className={`${styles.modal} ${isOpen ? styles.isShow : ""}`}>
<div className={styles.modalBox}>
<p className={styles.modalTitle}>
{t(getTranslationID("userListPage.label.bulkImport"))}
<button type="button" onClick={closePopup}>
<img src={close} className={styles.modalTitleIcon} alt="close" />
</button>
</p>
<form className={styles.form}>
<dl
className={`${styles.formList} ${styles.userImport} ${styles.hasbg}`}
>
<dd className={styles.full}>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
<a
style={{ marginInlineEnd: "350px", marginTop: "15px" }}
className={`${styles.menuLink} ${styles.isActive}`}
onClick={onDownloadCsv}
>
<img src={download} alt="" className={styles.menuIcon} />
{t(getTranslationID("userListPage.label.downloadCsv"))}
</a>
{t(getTranslationID("userListPage.text.downloadExplain"))}
</dd>
<dd className={styles.full}>
<label
style={{ marginInlineEnd: "350px", marginTop: "15px" }}
htmlFor="import"
className={`${styles.menuLink} ${styles.isActive}`}
>
<input
type="file"
id="import"
style={{ display: "none" }}
onChange={handleFileChange}
/>
<img src={upload} alt="" className={styles.menuIcon} />
{t(getTranslationID("userListPage.label.importCsv"))}
</label>
</dd>
<dt className={styles.formTitle}>Input rules</dt>
<dt>{t(getTranslationID("userListPage.label.nameLabel"))}</dt>
<dd>{t(getTranslationID("userListPage.text.nameRule"))}</dd>
<dt>
{t(getTranslationID("userListPage.label.emailAddressLabel"))}
</dt>
<dd>{t(getTranslationID("userListPage.text.emailAddressRule"))}</dd>
<dt>{t(getTranslationID("userListPage.label.roleLabel"))}</dt>
<dd>{t(getTranslationID("userListPage.text.roleRule"))}</dd>
<dt>{t(getTranslationID("userListPage.label.authorIdLabel"))}</dt>
<dd>{t(getTranslationID("userListPage.text.authorIdRule"))}</dd>
<dt>{t(getTranslationID("userListPage.label.autoRenewLabel"))}</dt>
<dd>{t(getTranslationID("userListPage.text.autoRenewRule"))}</dd>
<dt>
{t(getTranslationID("userListPage.label.notificationLabel"))}
</dt>
<dd>{t(getTranslationID("userListPage.text.notificationRule"))}</dd>
<dt>{t(getTranslationID("userListPage.label.encryptionLabel"))}</dt>
<dd>{t(getTranslationID("userListPage.text.encryptionRule"))}</dd>
<dt>
{t(
getTranslationID("userListPage.label.encryptionPasswordLabel")
)}
</dt>
<dd>
{t(getTranslationID("userListPage.text.encryptionPasswordRule"))}
</dd>
<dt>{t(getTranslationID("userListPage.label.promptLabel"))}</dt>
<dd>{t(getTranslationID("userListPage.text.promptRule"))}</dd>
<dd className={styles.full}>
{isPushImportButton && overMaxRow && (
<span className={styles.formError}>
{t(getTranslationID("userListPage.message.overMaxUserError"))}
</span>
)}
{isPushImportButton && invalidInput.length > 0 && (
<>
<span className={styles.formError}>
{t(
getTranslationID("userListPage.message.invalidInputError")
)}
</span>
<span className={styles.formError}>
{invalidInput.map((row) => `L${row}`).join(", ")}
</span>
</>
)}
{isPushImportButton && duplicatedEmails.length > 0 && (
<>
<span className={styles.formError}>
{t(
getTranslationID(
"userListPage.message.duplicateEmailError"
)
)}
</span>
<span className={styles.formError}>
{duplicatedEmails.map((row) => `L${row}`).join(", ")}
</span>
</>
)}
{isPushImportButton && duplicatedAuthorIds.length > 0 && (
<>
<span className={styles.formError}>
{t(
getTranslationID(
"userListPage.message.duplicateAuthorIdError"
)
)}
</span>
<span className={styles.formError}>
{duplicatedAuthorIds.map((row) => `L${row}`).join(", ")}
</span>
</>
)}
</dd>
<dd className={`${styles.full} ${styles.alignCenter}`}>
<input
type="button"
name="submit"
value={t(getTranslationID("userListPage.label.addUsers"))}
className={`${styles.formSubmit} ${styles.marginBtm1} ${
!isLoading && importFileName !== undefined
? styles.isActive
: ""
}`}
onClick={onImportUsers}
/>
<img
style={{ display: isLoading ? "inline" : "none" }}
src={progress_activit}
className={styles.icLoading}
alt="Loading"
/>
</dd>
</dl>
</form>
</div>
</div>
);
};

View File

@ -32,9 +32,11 @@ import personAdd from "../../assets/images/person_add.svg";
import checkFill from "../../assets/images/check_fill.svg";
import checkOutline from "../../assets/images/check_outline.svg";
import progress_activit from "../../assets/images/progress_activit.svg";
import upload from "../../assets/images/upload.svg";
import { UserAddPopup } from "./popup";
import { UserUpdatePopup } from "./updatePopup";
import { AllocateLicensePopup } from "./allocateLicensePopup";
import { ImportPopup } from "./importPopup";
const UserListPage: React.FC = (): JSX.Element => {
const dispatch: AppDispatch = useDispatch();
@ -46,6 +48,7 @@ const UserListPage: React.FC = (): JSX.Element => {
const [isUpdatePopupOpen, setIsUpdatePopupOpen] = useState(false);
const [isAllocateLicensePopupOpen, setIsAllocateLicensePopupOpen] =
useState(false);
const [isImportPopupOpen, setIsImportPopupOpen] = useState(false);
const onOpen = useCallback(() => {
setIsPopupOpen(true);
@ -66,6 +69,9 @@ const UserListPage: React.FC = (): JSX.Element => {
},
[setIsAllocateLicensePopupOpen, dispatch]
);
const onImportPopupOpen = useCallback(() => {
setIsImportPopupOpen(true);
}, [setIsImportPopupOpen]);
const onLicenseDeallocation = useCallback(
async (userId: number) => {
@ -134,6 +140,12 @@ const UserListPage: React.FC = (): JSX.Element => {
setIsAllocateLicensePopupOpen(false);
}}
/>
<ImportPopup
isOpen={isImportPopupOpen}
onClose={() => {
setIsImportPopupOpen(false);
}}
/>
<div
className={`${styles.wrap} ${
delegationAccessToken ? styles.manage : ""
@ -165,6 +177,16 @@ const UserListPage: React.FC = (): JSX.Element => {
{t(getTranslationID("userListPage.label.addUser"))}
</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={onImportPopupOpen}
>
<img src={upload} alt="" className={styles.menuIcon} />
{t(getTranslationID("userListPage.label.bulkImport"))}
</a>
</li>
</ul>
<div className={styles.tableWrap}>
<table className={`${styles.table} ${styles.user}`}>

View File

@ -1630,6 +1630,43 @@ _:-ms-lang(x)::-ms-backdrop,
margin-bottom: 5rem;
}
.formList.userImport .formTitle {
padding: 1rem 4% 0;
line-height: 1.2;
}
.formList.userImport dt:not(.formTitle) {
width: 30%;
padding: 0 4% 0 4%;
font-size: 0.9rem;
}
.formList.userImport dt:not(.formTitle):nth-of-type(odd) {
background: #f0f0f0;
}
.formList.userImport dt:not(.formTitle):nth-of-type(odd) + dd {
background: #f0f0f0;
}
.formList.userImport dd {
width: 58%;
padding: 0.2rem 4% 0.2rem 0;
margin-bottom: 0;
white-space: pre-line;
word-wrap: break-word;
font-size: 0.9rem;
line-height: 1.2;
}
.formList.userImport dd.full {
width: 100%;
padding: 0.2rem 4% 0.2rem 4%;
}
.formList.userImport dd.full .buttonText {
padding: 0 0 0.8rem;
}
.formList.userImport dd .menuLink {
display: inline-block;
margin-bottom: 0.6rem;
padding: 0.5rem 1.5rem 0.5rem 1.3rem;
}
.account .listVertical {
margin-bottom: 3rem;
}
@ -2284,6 +2321,9 @@ tr.isSelected .menuInTable li a.isDisable {
.formList.property dt:not(.formTitle):nth-of-type(odd) + dd {
background: #f0f0f0;
}
.formList.property dt:has(+ dd.hasInput) {
padding-top: 0.4rem;
}
.formList.property dd {
width: 58%;
padding: 0.2rem 4% 0.2rem 0;
@ -2295,6 +2335,16 @@ tr.isSelected .menuInTable li a.isDisable {
.formList.property dd img {
height: 1.1rem;
}
.formList.property dd .formInput.short {
width: 250px;
padding: 0.3rem 0.3rem 0.1rem;
}
.formList.property dd .formSubmit {
min-width: auto;
padding: 0.2rem 0.5rem;
position: absolute;
right: 0.5rem;
}
.formList.property dd.full {
width: 100%;
padding: 0.2rem 4% 0.2rem 4%;

View File

@ -108,11 +108,12 @@ declare const classNames: {
readonly clm0: "clm0";
readonly menuInTable: "menuInTable";
readonly isSelected: "isSelected";
readonly userImport: "userImport";
readonly menuLink: "menuLink";
readonly odd: "odd";
readonly alignRight: "alignRight";
readonly menuAction: "menuAction";
readonly inTable: "inTable";
readonly menuLink: "menuLink";
readonly menuIcon: "menuIcon";
readonly colorLink: "colorLink";
readonly isDisable: "isDisable";
@ -193,6 +194,7 @@ declare const classNames: {
readonly hideO10: "hideO10";
readonly op10: "op10";
readonly property: "property";
readonly hasInput: "hasInput";
readonly formChange: "formChange";
readonly chooseMember: "chooseMember";
readonly holdMember: "holdMember";

View File

@ -64,7 +64,7 @@
"countryExplanation": "Wählen Sie das Land aus, in dem Sie sich befinden. Wenn Ihr Land nicht aufgeführt ist, wählen Sie bitte das nächstgelegene Land aus.",
"dealerExplanation": "Bitte wählen Sie den Händler aus, bei dem Sie die Lizenz erwerben möchten.",
"adminInfoTitle": "Registrieren Sie die Informationen des primären Administrators",
"passwordTerms": "Bitte legen Sie ein Passwort fest. Das Passwort muss 825 Zeichen lang sein und Buchstaben, Zahlen und Symbole enthalten. (Sollte ein kompatibles Symbol auflisten und angeben, ob ein Großbuchstabe erforderlich ist.)"
"passwordTerms": "Bitte legen Sie ein Passwort fest. Das Passwort muss 864 Zeichen lang sein und Buchstaben, Zahlen und Symbole enthalten."
},
"label": {
"company": "Name der Firma",
@ -135,7 +135,12 @@
"typistUserDeletionTranscriptionTaskError": "(de)ユーザーの削除に失敗しました。Dictation画面でタスクのルーティングから対象Transcriptionistを外してください。",
"authorUserDeletionTranscriptionTaskError": "(de)ユーザーの削除に失敗しました。Dictation画面で対象AuthorのAuthorIDが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。",
"typistUserDeletionTranscriptionistGroupError": "(de)ユーザーの削除に失敗しました。Workflow画面でTranscriptionistGroupから対象Transcriptionistを外してください。",
"authorDeletionRoutingRuleError": "(de)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。"
"authorDeletionRoutingRuleError": "(de)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。",
"importSuccess": "(de)ユーザー一括追加を受け付けました。登録処理が完了次第メールが届きますのでご確認ください。",
"duplicateEmailError": "(de)以下の行のメールアドレスがCSV中で重複しています。",
"duplicateAuthorIdError": "(de)以下の行のAuthorIDがCSV中で重複しています。",
"overMaxUserError": "(de)一度に追加できるユーザーは100件までです。",
"invalidInputError": "(de)以下の行のユーザー情報が入力ルールに準拠していません。"
},
"label": {
"title": "Benutzer",
@ -168,7 +173,33 @@
"deleteUser": "Benutzer löschen",
"none": "Keiner",
"encryptionPassword": "Passwort",
"encryptionPasswordTerm": "Bitte legen Sie Ihr Passwort mit 4 bis 16 alphanumerischen Zeichen und Symbolen fest."
"encryptionPasswordTerm": "Bitte legen Sie Ihr Passwort mit 4 bis 16 alphanumerischen Zeichen und Symbolen fest.",
"bulkImport": "(de)Bulk import",
"downloadCsv": "(de)Download CSV",
"importCsv": "(de)Import CSV",
"inputRules": "(de)Input rules",
"nameLabel": "(de)Name",
"emailAddressLabel": "(de)Email Address",
"roleLabel": "(de)Role",
"authorIdLabel": "(de)Author ID",
"autoRenewLabel": "(de)Auto Renew",
"notificationLabel": "(de)Notification",
"encryptionLabel": "(de)Encryption",
"encryptionPasswordLabel": "(de)Encryption Password",
"promptLabel": "(de)Prompt",
"addUsers": "(de)Add users"
},
"text": {
"downloadExplain": "(de)Download the csv format and enter it according to the rules below.",
"nameRule": "(de)Maximum 225 characters",
"emailAddressRule": "(de)Maximum 225 characters\nCannot use an email address that is already in use.",
"roleRule": "(de)None : 0\nAuthor : 1\nTranscriptionist : 2",
"authorIdRule": "(de)Required only when Role=Author(1)\nMaximum 16 characters\nOnly uppercase alphanumeric characters and \"_\" can be entered.\nCannot use an Author ID that is already in use.",
"autoRenewRule": "(de)0 or 1",
"notificationRule": "(de)0 or 1",
"encryptionRule": "(de)Required only when Role=Author(1)\n0 or 1",
"encryptionPasswordRule": "(de)Required only when Role=Author(1) and Encryption=ON(1)\nOnly 4 to 16 letters, numbers, and symbols can be entered.",
"promptRule": "(de)Required only when Role=Author(1)\n0 or 1"
}
},
"LicenseSummaryPage": {
@ -198,16 +229,16 @@
"licenseOrderPage": {
"message": {
"inputEmptyError": "Pflichtfeld",
"poNumberIncorrectError": "Das Format der Bestellnummer ist ungültig. Für die Bestellnummer können nur alphanumerische Zeichen eingegeben werden.",
"poNumberIncorrectError": "Das Format der PO-Nummer ist ungültig. Für die PO-Nummer können nur alphanumerische Zeichen eingegeben werden.",
"newOrderIncorrectError": "Bitte geben Sie für die neue Bestellung eine Zahl größer oder gleich 1 ein.",
"confirmOrder": "Möchten Sie eine Bestellung aufgeben?",
"poNumberConflictError": "Die eingegebene Bestellnummer existiert bereits. Bitte geben Sie eine andere Bestellnummer ein.",
"dealerNotFoundError": "(de)ディーラーが設定されていないため、ライセンスを注文できません。アカウント画面でディーラーを指定してください。"
"poNumberConflictError": "Die eingegebene PO-Nummer existiert bereits. Bitte geben Sie eine andere PO-Nummer ein.",
"dealerNotFoundError": "Um eine Lizenz zu bestellen, müssen Sie den Händler angeben, bei dem Sie die Lizenz erwerben möchten. Melden Sie sich bei ODMS Cloud an und richten Sie „Händler“ auf der Registerkarte „Konto“ ein."
},
"label": {
"title": "Lizenz bestellen",
"licenses": "Lizenz-Typ",
"poNumber": "Bestellnummer",
"poNumber": "PO-Nummer",
"newOrder": "Anzahl der Lizenzen",
"orderButton": "Bestellen",
"licenseTypeText": "Ein Jahr"
@ -220,7 +251,9 @@
"taskNotEditable": "Der Transkriptionist kann nicht geändert werden, da die Transkription bereits ausgeführt wird oder die Datei nicht vorhanden ist. Bitte aktualisieren Sie den Bildschirm und prüfen Sie den aktuellen Status.",
"backupFailedError": "Der Prozess „Dateisicherung“ ist fehlgeschlagen. Bitte versuchen Sie es später noch einmal. Wenn der Fehler weiterhin besteht, wenden Sie sich an Ihren Systemadministrator.",
"cancelFailedError": "Die Diktate konnten nicht gelöscht werden. Bitte aktualisieren Sie Ihren Bildschirm und versuchen Sie es erneut.",
"deleteFailedError": "(de)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。"
"deleteFailedError": "(de)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。",
"licenseNotAssignedError": "Das Diktat kann nicht hochgeladen werden, da keine gültige Lizenz zugewiesen ist. Bitte fragen Sie Ihren Administrator.",
"licenseExpiredError": "Die Transkription ist nicht möglich, da Ihre Lizenz abgelaufen ist. Bitte bitten Sie Ihren Administrator, Ihnen eine gültige Lizenz zuzuweisen."
},
"label": {
"title": "Diktate",
@ -262,7 +295,7 @@
"changeTranscriptionist": "Transkriptionist ändern",
"deleteDictation": "Diktat löschen",
"selectedTranscriptionist": "Ausgewählter transkriptionist",
"poolTranscriptionist": "Liste der Transkriptionisten",
"poolTranscriptionist": "Transkriptionsliste",
"fileBackup": "Dateisicherung",
"downloadForBackup": "Zur Sicherung herunterladen",
"applications": "Desktopanwendung",
@ -345,7 +378,7 @@
"orderDate": "Auftragsdatum",
"issueDate": "Ausgabetag",
"numberOfOrder": "Anzahl der bestellten Lizenzen",
"poNumber": "Bestellnummer",
"poNumber": "PO-Nummer",
"status": "Status",
"issueRequesting": "Lizenzen auf Bestellung",
"issued": "Lizenz ausgestellt",
@ -429,7 +462,7 @@
"message": {
"selectedTypistEmptyError": "Um eine Transkriptionsgruppe zu speichern, müssen ein oder mehrere Transkriptionisten ausgewählt werden.",
"groupSaveFailedError": "Die Transkriptionistengruppe konnte nicht gespeichert werden. Die angezeigten Informationen sind möglicherweise veraltet. Aktualisieren Sie daher bitte den Bildschirm, um den neuesten Status anzuzeigen.",
"GroupNameAlreadyExistError": "(de)このTranscriptionistGroup名は既に登録されています。他のTranscriptionistGroup名で登録してください。",
"GroupNameAlreadyExistError": "Der Name dieser Transkriptionistengruppe ist bereits registriert. Bitte registrieren Sie sich mit einem anderen Namen der Transkriptionistengruppe.",
"deleteFailedWorkflowAssigned": "(de)TranscriptionistGroupの削除に失敗しました。Workflow画面でルーティングルールから対象TranscriptionistGroupを外してください。",
"deleteFailedCheckoutPermissionExisted": "(de)TranscriptionistGroupの削除に失敗しました。Dictation画面でタスクのルーティングから対象TranscriptionistGroupを外してください。"
}

View File

@ -64,7 +64,7 @@
"countryExplanation": "Select the country where you are located. If your country isn't listed, please select the nearest country.",
"dealerExplanation": "Please select the dealer you would like to purchase the license from.",
"adminInfoTitle": "Register primary administrator's information",
"passwordTerms": "Please set a password. The password must be 8-25 characters must contain letters, numbers, and symbols. (Should list compatible symbol and state if capital letter is needed)."
"passwordTerms": "Please set a password. The password must be 8-64 characters must contain letters, numbers, and symbols."
},
"label": {
"company": "Company Name",
@ -135,7 +135,12 @@
"typistUserDeletionTranscriptionTaskError": "ユーザーの削除に失敗しました。Dictation画面でタスクのルーティングから対象Transcriptionistを外してください。",
"authorUserDeletionTranscriptionTaskError": "ユーザーの削除に失敗しました。Dictation画面で対象AuthorのAuthorIDが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。",
"typistUserDeletionTranscriptionistGroupError": "ユーザーの削除に失敗しました。Workflow画面でTranscriptionistGroupから対象Transcriptionistを外してください。",
"authorDeletionRoutingRuleError": "ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。"
"authorDeletionRoutingRuleError": "ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。",
"importSuccess": "ユーザー一括追加を受け付けました。登録処理が完了次第メールが届きますのでご確認ください。",
"duplicateEmailError": "以下の行のメールアドレスがCSV中で重複しています。",
"duplicateAuthorIdError": "以下の行のAuthorIDがCSV中で重複しています。",
"overMaxUserError": "一度に追加できるユーザーは100件までです。",
"invalidInputError": "以下の行のユーザー情報が入力ルールに準拠していません。"
},
"label": {
"title": "User",
@ -168,7 +173,33 @@
"deleteUser": "Delete User",
"none": "None",
"encryptionPassword": "Password",
"encryptionPasswordTerm": "Please set your password using 4 to 16 alphanumeric and symbols."
"encryptionPasswordTerm": "Please set your password using 4 to 16 alphanumeric and symbols.",
"bulkImport": "Bulk import",
"downloadCsv": "Download CSV",
"importCsv": "Import CSV",
"inputRules": "Input rules",
"nameLabel": "Name",
"emailAddressLabel": "Email Address",
"roleLabel": "Role",
"authorIdLabel": "Author ID",
"autoRenewLabel": "Auto Renew",
"notificationLabel": "Notification",
"encryptionLabel": "Encryption",
"encryptionPasswordLabel": "Encryption Password",
"promptLabel": "Prompt",
"addUsers": "Add users"
},
"text": {
"downloadExplain": "Download the csv format and enter it according to the rules below.",
"nameRule": "Maximum 225 characters",
"emailAddressRule": "Maximum 225 characters\nCannot use an email address that is already in use.",
"roleRule": "None : 0\nAuthor : 1\nTranscriptionist : 2",
"authorIdRule": "Required only when Role=Author(1)\nMaximum 16 characters\nOnly uppercase alphanumeric characters and \"_\" can be entered.\nCannot use an Author ID that is already in use.",
"autoRenewRule": "0 or 1",
"notificationRule": "0 or 1",
"encryptionRule": "Required only when Role=Author(1)\n0 or 1",
"encryptionPasswordRule": "Required only when Role=Author(1) and Encryption=ON(1)\nOnly 4 to 16 letters, numbers, and symbols can be entered.",
"promptRule": "Required only when Role=Author(1)\n0 or 1"
}
},
"LicenseSummaryPage": {
@ -202,7 +233,7 @@
"newOrderIncorrectError": "Please enter a number greater than or equal to 1 for the New Order.",
"confirmOrder": "Would you like to place an order?",
"poNumberConflictError": "PO Number entered already exists. Please enter a different PO Number.",
"dealerNotFoundError": "ディーラーが設定されていないため、ライセンスを注文できません。アカウント画面でディーラーを指定してください。"
"dealerNotFoundError": "In order to order a license, you need to set up the dealer where you want to purchase it. Sign in to ODMS Cloud and set up \"Dealer\" in the \"Account\" tab."
},
"label": {
"title": "Order License",
@ -220,7 +251,9 @@
"taskNotEditable": "The transcriptionist cannot be changed because the transcription is already in progress or the file does not exist. Please refresh the screen and check the latest status.",
"backupFailedError": "The \"File Backup\" process has failed. Please try again later. If the error continues, contact your system administrator.",
"cancelFailedError": "Failed to delete the dictations. Please refresh your screen and try again.",
"deleteFailedError": "タスクの削除に失敗しました。画面を更新し、再度ご確認ください。"
"deleteFailedError": "タスクの削除に失敗しました。画面を更新し、再度ご確認ください。",
"licenseNotAssignedError": "Dictation cannot be uploaded because a vaild license is not assigned. Please ask your administrator.",
"licenseExpiredError": "Transcription is not possible because your license is expired. Please ask your administrator to assign a valid license."
},
"label": {
"title": "Dictations",
@ -262,7 +295,7 @@
"changeTranscriptionist": "Change Transcriptionist",
"deleteDictation": "Delete Dictation",
"selectedTranscriptionist": "Selected Transcriptionist",
"poolTranscriptionist": "Transcriptionist List",
"poolTranscriptionist": "Transcription List",
"fileBackup": "File Backup",
"downloadForBackup": "Download for backup",
"applications": "Desktop Application",
@ -429,7 +462,7 @@
"message": {
"selectedTypistEmptyError": "One or more transcriptonist must be selected to save a transcrption group.",
"groupSaveFailedError": "Transcriptionist Group could not be saved. The displayed information may be outdated, so please refresh the screen to see the latest status.",
"GroupNameAlreadyExistError": "このTranscriptionistGroup名は既に登録されています。他のTranscriptionistGroup名で登録してください。",
"GroupNameAlreadyExistError": "This Transcriptionist Group name is already registered. Please register with another Transcriptionist Group name.",
"deleteFailedWorkflowAssigned": "TranscriptionistGroupの削除に失敗しました。Workflow画面でルーティングルールから対象TranscriptionistGroupを外してください。",
"deleteFailedCheckoutPermissionExisted": "TranscriptionistGroupの削除に失敗しました。Dictation画面でタスクのルーティングから対象TranscriptionistGroupを外してください。"
}

View File

@ -64,7 +64,7 @@
"countryExplanation": "Seleccione el país donde se encuentra. Si su país no aparece en la lista, seleccione el país más cercano.",
"dealerExplanation": "Seleccione el distribuidor al que le gustaría comprar la licencia.",
"adminInfoTitle": "Registre la información del administrador principal",
"passwordTerms": "Establezca una contraseña. La contraseña debe tener entre 8 y 25 caracteres y debe contener letras, números y símbolos. (Debe enumerar el símbolo compatible e indicar si se necesita una letra mayúscula)."
"passwordTerms": "Establezca una contraseña. La contraseña debe tener entre 8 y 64 caracteres y debe contener letras, números y símbolos."
},
"label": {
"company": "Nombre de empresa",
@ -135,7 +135,12 @@
"typistUserDeletionTranscriptionTaskError": "(es)ユーザーの削除に失敗しました。Dictation画面でタスクのルーティングから対象Transcriptionistを外してください。",
"authorUserDeletionTranscriptionTaskError": "(es)ユーザーの削除に失敗しました。Dictation画面で対象AuthorのAuthorIDが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。",
"typistUserDeletionTranscriptionistGroupError": "(es)ユーザーの削除に失敗しました。Workflow画面でTranscriptionistGroupから対象Transcriptionistを外してください。",
"authorDeletionRoutingRuleError": "(es)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。"
"authorDeletionRoutingRuleError": "(es)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。",
"importSuccess": "(es)ユーザー一括追加を受け付けました。登録処理が完了次第メールが届きますのでご確認ください。",
"duplicateEmailError": "(es)以下の行のメールアドレスがCSV中で重複しています。",
"duplicateAuthorIdError": "(es)以下の行のAuthorIDがCSV中で重複しています。",
"overMaxUserError": "(es)一度に追加できるユーザーは100件までです。",
"invalidInputError": "(es)以下の行のユーザー情報が入力ルールに準拠していません。"
},
"label": {
"title": "Usuario",
@ -168,7 +173,33 @@
"deleteUser": "Borrar usuario",
"none": "Ninguno",
"encryptionPassword": "Contraseña",
"encryptionPasswordTerm": "Configure su contraseña utilizando de 4 a 16 símbolos alfanuméricos y."
"encryptionPasswordTerm": "Configure su contraseña utilizando de 4 a 16 símbolos alfanuméricos y.",
"bulkImport": "(es)Bulk import",
"downloadCsv": "(es)Download CSV",
"importCsv": "(es)Import CSV",
"inputRules": "(es)Input rules",
"nameLabel": "(es)Name",
"emailAddressLabel": "(es)Email Address",
"roleLabel": "(es)Role",
"authorIdLabel": "(es)Author ID",
"autoRenewLabel": "(es)Auto Renew",
"notificationLabel": "(es)Notification",
"encryptionLabel": "(es)Encryption",
"encryptionPasswordLabel": "(es)Encryption Password",
"promptLabel": "(es)Prompt",
"addUsers": "(es)Add users"
},
"text": {
"downloadExplain": "(es)Download the csv format and enter it according to the rules below.",
"nameRule": "(es)Maximum 225 characters",
"emailAddressRule": "(es)Maximum 225 characters\nCannot use an email address that is already in use.",
"roleRule": "(es)None : 0\nAuthor : 1\nTranscriptionist : 2",
"authorIdRule": "(es)Required only when Role=Author(1)\nMaximum 16 characters\nOnly uppercase alphanumeric characters and \"_\" can be entered.\nCannot use an Author ID that is already in use.",
"autoRenewRule": "(es)0 or 1",
"notificationRule": "(es)0 or 1",
"encryptionRule": "(es)Required only when Role=Author(1)\n0 or 1",
"encryptionPasswordRule": "(es)Required only when Role=Author(1) and Encryption=ON(1)\nOnly 4 to 16 letters, numbers, and symbols can be entered.",
"promptRule": "(es)Required only when Role=Author(1)\n0 or 1"
}
},
"LicenseSummaryPage": {
@ -202,7 +233,7 @@
"newOrderIncorrectError": "Ingrese un número mayor o igual a 1 para el Nuevo Pedido.",
"confirmOrder": "¿Quieres hacer un pedido?",
"poNumberConflictError": "El número de orden de compra ingresado ya existe. Ingrese un número de orden de compra diferente.",
"dealerNotFoundError": "(es)ディーラーが設定されていないため、ライセンスを注文できません。アカウント画面でディーラーを指定してください。"
"dealerNotFoundError": "Para solicitar una licencia, debe configurar el distribuidor donde desea comprarla. Inicie sesión en ODMS Cloud y configure \"Distribuidor\" en la pestaña \"Cuenta\"."
},
"label": {
"title": "Licencia de pedido",
@ -220,7 +251,9 @@
"taskNotEditable": "No se puede cambiar el transcriptor porque la transcripción ya está en curso o el archivo no existe. Actualice la pantalla y verifique el estado más reciente.",
"backupFailedError": "El proceso de \"Copia de seguridad de archivos\" ha fallado. Por favor, inténtelo de nuevo más tarde. Si el error continúa, comuníquese con el administrador del sistema.",
"cancelFailedError": "No se pudieron eliminar los dictados. Actualice su pantalla e inténtelo nuevamente.",
"deleteFailedError": "(es)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。"
"deleteFailedError": "(es)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。",
"licenseNotAssignedError": "No se puede cargar el dictado porque no se ha asignado una licencia válida. Consulte a su administrador.",
"licenseExpiredError": "La transcripción no es posible porque su licencia ha caducado. Solicite a su administrador que le asigne una licencia válida."
},
"label": {
"title": "Dictado",
@ -262,7 +295,7 @@
"changeTranscriptionist": "Cambiar transcriptor",
"deleteDictation": "Borrar dictado",
"selectedTranscriptionist": "Transcriptor seleccionado",
"poolTranscriptionist": "Lista de transcriptores",
"poolTranscriptionist": "Lista de transcriptor",
"fileBackup": "Copia de seguridad de archivos",
"downloadForBackup": "Descargar para respaldo",
"applications": "Aplicación de escritorio",
@ -429,7 +462,7 @@
"message": {
"selectedTypistEmptyError": "Se deben seleccionar uno o más transcriptores para guardar un grupo de transcripción.",
"groupSaveFailedError": "El grupo transcriptor no se pudo salvar. La información mostrada puede estar desactualizada. Así que actualice la pantalla para ver el estado más reciente.",
"GroupNameAlreadyExistError": "(es)このTranscriptionistGroup名は既に登録されています。他のTranscriptionistGroup名で登録してください。",
"GroupNameAlreadyExistError": "El nombre de este grupo transcriptor ya está registrado. Regístrese con otro nombre de grupo transcriptor.",
"deleteFailedWorkflowAssigned": "(es)TranscriptionistGroupの削除に失敗しました。Workflow画面でルーティングルールから対象TranscriptionistGroupを外してください。",
"deleteFailedCheckoutPermissionExisted": "(es)TranscriptionistGroupの削除に失敗しました。Dictation画面でタスクのルーティングから対象TranscriptionistGroupを外してください。"
}

View File

@ -64,7 +64,7 @@
"countryExplanation": "Sélectionnez le pays où vous vous trouvez. Si votre pays ne figure pas dans la liste, veuillez sélectionner le pays le plus proche.",
"dealerExplanation": "Veuillez sélectionner le revendeur auprès duquel vous souhaitez acheter la licence.",
"adminInfoTitle": "Enregistrer les informations de l'administrateur principal",
"passwordTerms": "Veuillez définir un mot de passe. Le mot de passe doit être composé de 8 à 25 caractères et doit contenir des lettres, des chiffres et des symboles. (Devrait lister les symboles compatibles et indiquer si une majuscule est nécessaire)."
"passwordTerms": "Veuillez définir un mot de passe. Le mot de passe doit être composé de 8 à 64 caractères et doit contenir des lettres, des chiffres et des symboles."
},
"label": {
"company": "Nom de l'entreprise",
@ -135,7 +135,12 @@
"typistUserDeletionTranscriptionTaskError": "(fr)ユーザーの削除に失敗しました。Dictation画面でタスクのルーティングから対象Transcriptionistを外してください。",
"authorUserDeletionTranscriptionTaskError": "(fr)ユーザーの削除に失敗しました。Dictation画面で対象AuthorのAuthorIDが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。",
"typistUserDeletionTranscriptionistGroupError": "(fr)ユーザーの削除に失敗しました。Workflow画面でTranscriptionistGroupから対象Transcriptionistを外してください。",
"authorDeletionRoutingRuleError": "(fr)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。"
"authorDeletionRoutingRuleError": "(fr)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。",
"importSuccess": "(fr)ユーザー一括追加を受け付けました。登録処理が完了次第メールが届きますのでご確認ください。",
"duplicateEmailError": "(fr)以下の行のメールアドレスがCSV中で重複しています。",
"duplicateAuthorIdError": "(fr)以下の行のAuthorIDがCSV中で重複しています。",
"overMaxUserError": "(fr)一度に追加できるユーザーは100件までです。",
"invalidInputError": "(fr)以下の行のユーザー情報が入力ルールに準拠していません。"
},
"label": {
"title": "Utilisateur",
@ -168,7 +173,33 @@
"deleteUser": "Supprimer l'utilisateur",
"none": "Aucun",
"encryptionPassword": "Mot de passe",
"encryptionPasswordTerm": "Veuillez définir votre mot de passe en utilisant 4 à 16 caractères alphanumériques et symboles."
"encryptionPasswordTerm": "Veuillez définir votre mot de passe en utilisant 4 à 16 caractères alphanumériques et symboles.",
"bulkImport": "(fr)Bulk import",
"downloadCsv": "(fr)Download CSV",
"importCsv": "(fr)Import CSV",
"inputRules": "(fr)Input rules",
"nameLabel": "(fr)Name",
"emailAddressLabel": "(fr)Email Address",
"roleLabel": "(fr)Role",
"authorIdLabel": "(fr)Author ID",
"autoRenewLabel": "(fr)Auto Renew",
"notificationLabel": "(fr)Notification",
"encryptionLabel": "(fr)Encryption",
"encryptionPasswordLabel": "(fr)Encryption Password",
"promptLabel": "(fr)Prompt",
"addUsers": "(fr)Add users"
},
"text": {
"downloadExplain": "(fr)Download the csv format and enter it according to the rules below.",
"nameRule": "(fr)Maximum 225 characters",
"emailAddressRule": "(fr)Maximum 225 characters\nCannot use an email address that is already in use.",
"roleRule": "(fr)None : 0\nAuthor : 1\nTranscriptionist : 2",
"authorIdRule": "(fr)Required only when Role=Author(1)\nMaximum 16 characters\nOnly uppercase alphanumeric characters and \"_\" can be entered.\nCannot use an Author ID that is already in use.",
"autoRenewRule": "(fr)0 or 1",
"notificationRule": "(fr)0 or 1",
"encryptionRule": "(fr)Required only when Role=Author(1)\n0 or 1",
"encryptionPasswordRule": "(fr)Required only when Role=Author(1) and Encryption=ON(1)\nOnly 4 to 16 letters, numbers, and symbols can be entered.",
"promptRule": "(fr)Required only when Role=Author(1)\n0 or 1"
}
},
"LicenseSummaryPage": {
@ -202,7 +233,7 @@
"newOrderIncorrectError": "Veuillez saisir un nombre supérieur ou égal à 1 pour la nouvelle commande.",
"confirmOrder": "Voulez-vous passer commande?",
"poNumberConflictError": "Le numéro de bon de commande saisi existe déjà. Veuillez saisir un autre numéro de bon de commande.",
"dealerNotFoundError": "(fr)ディーラーが設定されていないため、ライセンスを注文できません。アカウント画面でディーラーを指定してください。"
"dealerNotFoundError": "Pour commander une licence, vous devez identifier le revendeur où vous souhaitez l'acheter. Connectez-vous à ODMS Cloud et configurez « Revendeur » dans l'onglet « Compte »."
},
"label": {
"title": "Commander licence",
@ -220,7 +251,9 @@
"taskNotEditable": "Le transcripteur ne peut pas être changé car la transcription est déjà en cours ou le fichier n'existe pas. Veuillez actualiser l'écran et vérifier le dernier statut.",
"backupFailedError": "Le processus de « Sauvegarde de fichier » a échoué. Veuillez réessayer plus tard. Si l'erreur persiste, contactez votre administrateur système.",
"cancelFailedError": "Échec de la suppression des dictées. Veuillez actualiser votre écran et réessayer.",
"deleteFailedError": "(fr)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。"
"deleteFailedError": "(fr)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。",
"licenseNotAssignedError": "La dictée ne peut pas être téléchargée parce qu'une licence valable n'est pas attribuée. Veuillez vous adresser à votre administrateur.",
"licenseExpiredError": "La transcription n'est pas possible car votre licence est expirée. Veuillez demander à votre administrateur de vous attribuer une licence valide."
},
"label": {
"title": "Dictées",
@ -262,7 +295,7 @@
"changeTranscriptionist": "Changer de transcriptionniste ",
"deleteDictation": "Supprimer la dictée",
"selectedTranscriptionist": "Transcriptionniste sélectionné",
"poolTranscriptionist": "Liste des transcripteurs",
"poolTranscriptionist": "Liste de transcriptionniste",
"fileBackup": "Sauvegarde de fichiers",
"downloadForBackup": "Télécharger pour sauvegarde",
"applications": "Application de bureau",
@ -429,7 +462,7 @@
"message": {
"selectedTypistEmptyError": "Un ou plusieurs transcripteurs doivent être sélectionnés pour enregistrer un groupe de transcription.",
"groupSaveFailedError": "Le groupe de transcriptionniste n'a pas pu être enregistré. Les informations affichées peuvent être obsolètes, veuillez donc actualiser l'écran pour voir le dernier statut.",
"GroupNameAlreadyExistError": "(fr)このTranscriptionistGroup名は既に登録されています。他のTranscriptionistGroup名で登録してください。",
"GroupNameAlreadyExistError": "Ce nom de groupe transcripteur est déjà enregistré. Veuillez vous inscrire avec un autre nom de groupe transcripteur.",
"deleteFailedWorkflowAssigned": "(fr)TranscriptionistGroupの削除に失敗しました。Workflow画面でルーティングルールから対象TranscriptionistGroupを外してください。",
"deleteFailedCheckoutPermissionExisted": "(fr)TranscriptionistGroupの削除に失敗しました。Dictation画面でタスクのルーティングから対象TranscriptionistGroupを外してください。"
}