Merge branch 'develop' into ccb
# Conflicts: # dictation_client/src/pages/DictationPage/index.tsx
This commit is contained in:
commit
fbcafd2014
@ -43,7 +43,12 @@ export const UNAUTHORIZED_TO_CONTINUE_ERROR_CODES = [
|
||||
* ローカルストレージに残すキー類
|
||||
* @const {string[]}
|
||||
*/
|
||||
export const KEYS_TO_PRESERVE = ["accessToken", "refreshToken", "displayInfo"];
|
||||
export const KEYS_TO_PRESERVE = [
|
||||
"accessToken",
|
||||
"refreshToken",
|
||||
"displayInfo",
|
||||
"sortCriteria",
|
||||
];
|
||||
|
||||
/**
|
||||
* アクセストークンを更新する基準の秒数
|
||||
|
||||
@ -28,6 +28,13 @@ export const SORTABLE_COLUMN = {
|
||||
export type SortableColumnType =
|
||||
typeof SORTABLE_COLUMN[keyof typeof SORTABLE_COLUMN];
|
||||
|
||||
export const isSortableColumnType = (
|
||||
value: string
|
||||
): value is SortableColumnType => {
|
||||
const arg = value as SortableColumnType;
|
||||
return Object.values(SORTABLE_COLUMN).includes(arg);
|
||||
};
|
||||
|
||||
export type SortableColumnList =
|
||||
typeof SORTABLE_COLUMN[keyof typeof SORTABLE_COLUMN];
|
||||
|
||||
@ -38,6 +45,10 @@ export const DIRECTION = {
|
||||
|
||||
export type DirectionType = typeof DIRECTION[keyof typeof DIRECTION];
|
||||
|
||||
// DirectionTypeの型チェック関数
|
||||
export const isDirectionType = (arg: string): arg is DirectionType =>
|
||||
arg in DIRECTION;
|
||||
|
||||
export interface DisplayInfoType {
|
||||
JobNumber: boolean;
|
||||
Status: boolean;
|
||||
|
||||
@ -280,7 +280,6 @@ export const playbackAsync = createAsyncThunk<
|
||||
direction: DirectionType;
|
||||
paramName: SortableColumnType;
|
||||
audioFileId: number;
|
||||
isTypist: boolean;
|
||||
},
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
@ -289,7 +288,7 @@ export const playbackAsync = createAsyncThunk<
|
||||
};
|
||||
}
|
||||
>("dictations/playbackAsync", async (args, thunkApi) => {
|
||||
const { audioFileId, direction, paramName, isTypist } = args;
|
||||
const { audioFileId, direction, paramName } = args;
|
||||
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
@ -300,15 +299,12 @@ export const playbackAsync = createAsyncThunk<
|
||||
const tasksApi = new TasksApi(config);
|
||||
const usersApi = new UsersApi(config);
|
||||
try {
|
||||
// ユーザーがタイピストである場合に、ソート条件を保存する
|
||||
if (isTypist) {
|
||||
await usersApi.updateSortCriteria(
|
||||
{ direction, paramName },
|
||||
{
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
}
|
||||
);
|
||||
}
|
||||
await usersApi.updateSortCriteria(
|
||||
{ direction, paramName },
|
||||
{
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
}
|
||||
);
|
||||
await tasksApi.checkout(audioFileId, {
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
|
||||
@ -34,6 +34,8 @@ import {
|
||||
cancelAsync,
|
||||
PRIORITY,
|
||||
deleteTaskAsync,
|
||||
isSortableColumnType,
|
||||
isDirectionType,
|
||||
} from "features/dictation";
|
||||
import { getTranslationID } from "translation";
|
||||
import { Task } from "api/api";
|
||||
@ -245,6 +247,12 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
dispatch(changeDirection({ direction: currentDirection }));
|
||||
dispatch(changeParamName({ paramName }));
|
||||
|
||||
// ローカルストレージにソート情報を保存する
|
||||
localStorage.setItem(
|
||||
"sortCriteria",
|
||||
`direction:${currentDirection},paramName:${paramName}`
|
||||
);
|
||||
|
||||
const filter = getFilter(
|
||||
filterUploaded,
|
||||
filterInProgress,
|
||||
@ -351,10 +359,11 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
audioFileId,
|
||||
direction: sortDirection,
|
||||
paramName: sortableParamName,
|
||||
isTypist,
|
||||
})
|
||||
);
|
||||
if (meta.requestStatus === "fulfilled") {
|
||||
// ローカルストレージにソート情報を削除する
|
||||
localStorage.removeItem("sortCriteria");
|
||||
const filter = getFilter(
|
||||
filterUploaded,
|
||||
filterInProgress,
|
||||
@ -391,7 +400,6 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filterInProgress,
|
||||
filterPending,
|
||||
filterUploaded,
|
||||
isTypist,
|
||||
sortDirection,
|
||||
sortableParamName,
|
||||
t,
|
||||
@ -572,13 +580,39 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
dispatch(changeDisplayInfo({ column: displayInfo }));
|
||||
|
||||
const filter = getFilter(true, true, true, true, false);
|
||||
|
||||
const { meta, payload } = await dispatch(getSortColumnAsync());
|
||||
if (
|
||||
meta.requestStatus === "fulfilled" &&
|
||||
payload &&
|
||||
!("error" in payload)
|
||||
) {
|
||||
const { direction, paramName } = payload;
|
||||
// ソート情報をローカルストレージから取得する
|
||||
const sortColumnValue = localStorage.getItem("sortCriteria") ?? "";
|
||||
let direction: DirectionType;
|
||||
let paramName: SortableColumnType;
|
||||
if (sortColumnValue === "") {
|
||||
direction = payload.direction;
|
||||
paramName = payload.paramName;
|
||||
} else {
|
||||
// ソート情報をDirectionとParamNameに分割する
|
||||
const sortColumn = sortColumnValue?.split(",");
|
||||
const localStorageDirection = sortColumn[0].split(":")[1] ?? "";
|
||||
|
||||
const localStorageParamName = sortColumn[1]?.split(":")[1] ?? "";
|
||||
|
||||
// 正常なソート情報がローカルストレージに存在する場合はローカルストレージの情報を使用する
|
||||
direction = isDirectionType(localStorageDirection)
|
||||
? localStorageDirection
|
||||
: payload.direction;
|
||||
paramName = isSortableColumnType(localStorageParamName)
|
||||
? localStorageParamName
|
||||
: payload.paramName;
|
||||
|
||||
dispatch(changeDirection({ direction }));
|
||||
dispatch(changeParamName({ paramName }));
|
||||
}
|
||||
|
||||
dispatch(
|
||||
listTasksAsync({
|
||||
limit: LIMIT_TASK_NUM,
|
||||
|
||||
5
dictation_server/db/migrations/055-add_users_index.sql
Normal file
5
dictation_server/db/migrations/055-add_users_index.sql
Normal file
@ -0,0 +1,5 @@
|
||||
-- +migrate Up
|
||||
ALTER TABLE `users` ADD INDEX `idx_role` (role);
|
||||
|
||||
-- +migrate Down
|
||||
ALTER TABLE `users` DROP INDEX `idx_role`;
|
||||
@ -39,6 +39,8 @@ import {
|
||||
updateEntity,
|
||||
} from '../../common/repository';
|
||||
import { Context } from '../../common/log';
|
||||
import { User } from '../users/entity/user.entity';
|
||||
import { UserNotFoundError } from '../users/errors/types';
|
||||
|
||||
@Injectable()
|
||||
export class LicensesRepositoryService {
|
||||
@ -559,6 +561,19 @@ export class LicensesRepositoryService {
|
||||
accountId: number,
|
||||
): Promise<void> {
|
||||
await this.dataSource.transaction(async (entityManager) => {
|
||||
// 対象ユーザの存在チェック
|
||||
const userRepo = entityManager.getRepository(User);
|
||||
const user = await userRepo.findOne({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
lock: { mode: 'pessimistic_write' },
|
||||
});
|
||||
if (!user) {
|
||||
throw new UserNotFoundError(`User not exist. userId: ${userId}`);
|
||||
}
|
||||
|
||||
const licenseRepo = entityManager.getRepository(License);
|
||||
const licenseAllocationHistoryRepo = entityManager.getRepository(
|
||||
LicenseAllocationHistory,
|
||||
|
||||
@ -53,6 +53,7 @@ import {
|
||||
deleteEntity,
|
||||
} from '../../common/repository';
|
||||
import { Context } from '../../common/log';
|
||||
import { UserNotFoundError } from '../users/errors/types';
|
||||
|
||||
@Injectable()
|
||||
export class TasksRepositoryService {
|
||||
@ -172,6 +173,20 @@ export class TasksRepositoryService {
|
||||
permittedSourceStatus: TaskStatus[],
|
||||
): Promise<void> {
|
||||
await this.dataSource.transaction(async (entityManager) => {
|
||||
// 対象ユーザの存在確認
|
||||
const userRepo = entityManager.getRepository(User);
|
||||
const user = await userRepo.findOne({
|
||||
where: {
|
||||
id: user_id,
|
||||
},
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
lock: { mode: 'pessimistic_write' },
|
||||
});
|
||||
if (!user) {
|
||||
throw new TypistUserNotFoundError(
|
||||
`Typist user not exists. user_id:${user_id}`,
|
||||
);
|
||||
}
|
||||
const taskRepo = entityManager.getRepository(Task);
|
||||
// 指定した音声ファイルIDに紐づくTaskの中でStatusが[Uploaded,Inprogress,Pending]であるものを取得
|
||||
const task = await taskRepo.findOne({
|
||||
@ -851,6 +866,22 @@ export class TasksRepositoryService {
|
||||
|
||||
const createdEntity = await this.dataSource.transaction(
|
||||
async (entityManager) => {
|
||||
// タスクの所有者の存在確認
|
||||
const userRepo = entityManager.getRepository(User);
|
||||
const user = await userRepo.findOne({
|
||||
where: {
|
||||
id: owner_user_id,
|
||||
account_id: account_id,
|
||||
},
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
lock: { mode: 'pessimistic_write' },
|
||||
});
|
||||
if (!user) {
|
||||
throw new UserNotFoundError(
|
||||
`User not exists. owner_user_id:${owner_user_id}`,
|
||||
);
|
||||
}
|
||||
|
||||
const audioFileRepo = entityManager.getRepository(AudioFile);
|
||||
const newAudioFile = audioFileRepo.create(audioFile);
|
||||
const savedAudioFile = await insertEntity(
|
||||
@ -972,6 +1003,7 @@ export class TasksRepositoryService {
|
||||
deleted_at: IsNull(),
|
||||
},
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
lock: { mode: 'pessimistic_write' },
|
||||
});
|
||||
// idはユニークであるため取得件数の一致でユーザーの存在を確認
|
||||
if (typistUserIds.length !== userRecords.length) {
|
||||
|
||||
@ -122,6 +122,7 @@ export class UserGroupsRepositoryService {
|
||||
role: USER_ROLES.TYPIST,
|
||||
email_verified: true,
|
||||
},
|
||||
lock: { mode: 'pessimistic_write' },
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
});
|
||||
if (userRecords.length !== typistIds.length) {
|
||||
@ -188,6 +189,7 @@ export class UserGroupsRepositoryService {
|
||||
role: USER_ROLES.TYPIST,
|
||||
email_verified: true,
|
||||
},
|
||||
lock: { mode: 'pessimistic_write' },
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
});
|
||||
if (userRecords.length !== typistIds.length) {
|
||||
@ -204,6 +206,7 @@ export class UserGroupsRepositoryService {
|
||||
id: typistGroupId,
|
||||
account_id: accountId,
|
||||
},
|
||||
lock: { mode: 'pessimistic_write' },
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
});
|
||||
if (!typistGroup) {
|
||||
|
||||
@ -87,6 +87,7 @@ export class WorkflowsRepositoryService {
|
||||
const author = await userRepo.findOne({
|
||||
where: { account_id: accountId, id: authorId, email_verified: true },
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
lock: { mode: 'pessimistic_write' },
|
||||
});
|
||||
if (!author) {
|
||||
throw new UserNotFoundError(
|
||||
@ -100,6 +101,7 @@ export class WorkflowsRepositoryService {
|
||||
const worktypes = await worktypeRepo.find({
|
||||
where: { account_id: accountId, id: worktypeId },
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
lock: { mode: 'pessimistic_write' },
|
||||
});
|
||||
if (worktypes.length === 0) {
|
||||
throw new WorktypeIdNotFoundError(
|
||||
@ -114,6 +116,7 @@ export class WorkflowsRepositoryService {
|
||||
const template = await templateRepo.findOne({
|
||||
where: { account_id: accountId, id: templateId },
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
lock: { mode: 'pessimistic_write' },
|
||||
});
|
||||
if (!template) {
|
||||
throw new TemplateFileNotExistError('template not found.');
|
||||
@ -131,6 +134,7 @@ export class WorkflowsRepositoryService {
|
||||
email_verified: true,
|
||||
},
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
lock: { mode: 'pessimistic_write' },
|
||||
});
|
||||
if (typistUsers.length !== typistIds.length) {
|
||||
throw new UserNotFoundError(
|
||||
@ -146,6 +150,7 @@ export class WorkflowsRepositoryService {
|
||||
const typistGroups = await userGroupRepo.find({
|
||||
where: { account_id: accountId, id: In(groupIds) },
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
lock: { mode: 'pessimistic_write' },
|
||||
});
|
||||
if (typistGroups.length !== groupIds.length) {
|
||||
throw new TypistGroupNotExistError(
|
||||
@ -163,6 +168,7 @@ export class WorkflowsRepositoryService {
|
||||
worktype_id: worktypeId !== undefined ? worktypeId : IsNull(),
|
||||
},
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
lock: { mode: 'pessimistic_write' },
|
||||
});
|
||||
if (workflow.length !== 0) {
|
||||
throw new AuthorIdAndWorktypeIdPairAlreadyExistsError(
|
||||
@ -227,23 +233,12 @@ export class WorkflowsRepositoryService {
|
||||
): Promise<void> {
|
||||
return await this.dataSource.transaction(async (entityManager) => {
|
||||
const workflowRepo = entityManager.getRepository(Workflow);
|
||||
|
||||
// ワークフローの存在確認
|
||||
const targetWorkflow = await workflowRepo.findOne({
|
||||
where: { account_id: accountId, id: workflowId },
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
});
|
||||
if (!targetWorkflow) {
|
||||
throw new WorkflowNotFoundError(
|
||||
`workflow not found. id: ${workflowId}`,
|
||||
);
|
||||
}
|
||||
|
||||
// authorの存在確認
|
||||
const userRepo = entityManager.getRepository(User);
|
||||
const author = await userRepo.findOne({
|
||||
where: { account_id: accountId, id: authorId, email_verified: true },
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
lock: { mode: 'pessimistic_write' },
|
||||
});
|
||||
if (!author) {
|
||||
throw new UserNotFoundError(
|
||||
@ -251,12 +246,44 @@ export class WorkflowsRepositoryService {
|
||||
);
|
||||
}
|
||||
|
||||
// ルーティング候補ユーザーの存在確認
|
||||
const typistIds = typists.flatMap((typist) =>
|
||||
typist.typistId ? [typist.typistId] : [],
|
||||
);
|
||||
const typistUsers = await userRepo.find({
|
||||
where: {
|
||||
account_id: accountId,
|
||||
id: In(typistIds),
|
||||
email_verified: true,
|
||||
},
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
lock: { mode: 'pessimistic_write' },
|
||||
});
|
||||
if (typistUsers.length !== typistIds.length) {
|
||||
throw new UserNotFoundError(
|
||||
`typist not found or email not verified. ids: ${typistIds}`,
|
||||
);
|
||||
}
|
||||
|
||||
// ワークフローの存在確認
|
||||
const targetWorkflow = await workflowRepo.findOne({
|
||||
where: { account_id: accountId, id: workflowId },
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
lock: { mode: 'pessimistic_write' },
|
||||
});
|
||||
if (!targetWorkflow) {
|
||||
throw new WorkflowNotFoundError(
|
||||
`workflow not found. id: ${workflowId}`,
|
||||
);
|
||||
}
|
||||
|
||||
// worktypeの存在確認
|
||||
if (worktypeId !== undefined) {
|
||||
const worktypeRepo = entityManager.getRepository(Worktype);
|
||||
const worktypes = await worktypeRepo.find({
|
||||
where: { account_id: accountId, id: worktypeId },
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
lock: { mode: 'pessimistic_write' },
|
||||
});
|
||||
if (worktypes.length === 0) {
|
||||
throw new WorktypeIdNotFoundError(
|
||||
@ -271,6 +298,7 @@ export class WorkflowsRepositoryService {
|
||||
const template = await templateRepo.findOne({
|
||||
where: { account_id: accountId, id: templateId },
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
lock: { mode: 'pessimistic_write' },
|
||||
});
|
||||
if (!template) {
|
||||
throw new TemplateFileNotExistError(
|
||||
@ -279,24 +307,6 @@ export class WorkflowsRepositoryService {
|
||||
}
|
||||
}
|
||||
|
||||
// ルーティング候補ユーザーの存在確認
|
||||
const typistIds = typists.flatMap((typist) =>
|
||||
typist.typistId ? [typist.typistId] : [],
|
||||
);
|
||||
const typistUsers = await userRepo.find({
|
||||
where: {
|
||||
account_id: accountId,
|
||||
id: In(typistIds),
|
||||
email_verified: true,
|
||||
},
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
});
|
||||
if (typistUsers.length !== typistIds.length) {
|
||||
throw new UserNotFoundError(
|
||||
`typist not found or email not verified. ids: ${typistIds}`,
|
||||
);
|
||||
}
|
||||
|
||||
// ルーティング候補ユーザーグループの存在確認
|
||||
const groupIds = typists.flatMap((typist) => {
|
||||
return typist.typistGroupId ? [typist.typistGroupId] : [];
|
||||
@ -305,6 +315,7 @@ export class WorkflowsRepositoryService {
|
||||
const typistGroups = await userGroupRepo.find({
|
||||
where: { account_id: accountId, id: In(groupIds) },
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
lock: { mode: 'pessimistic_write' },
|
||||
});
|
||||
if (typistGroups.length !== groupIds.length) {
|
||||
throw new TypistGroupNotExistError(
|
||||
@ -399,6 +410,7 @@ export class WorkflowsRepositoryService {
|
||||
const workflow = await workflowRepo.findOne({
|
||||
where: { account_id: accountId, id: workflowId },
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
lock: { mode: 'pessimistic_write' },
|
||||
});
|
||||
if (!workflow) {
|
||||
throw new WorkflowNotFoundError(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user