Merge branch 'develop' into ccb

# Conflicts:
#	dictation_client/src/pages/DictationPage/index.tsx
This commit is contained in:
SAITO-PC-3\saito.k 2024-02-02 12:01:46 +09:00
commit fbcafd2014
9 changed files with 158 additions and 45 deletions

View File

@ -43,7 +43,12 @@ export const UNAUTHORIZED_TO_CONTINUE_ERROR_CODES = [
* *
* @const {string[]} * @const {string[]}
*/ */
export const KEYS_TO_PRESERVE = ["accessToken", "refreshToken", "displayInfo"]; export const KEYS_TO_PRESERVE = [
"accessToken",
"refreshToken",
"displayInfo",
"sortCriteria",
];
/** /**
* *

View File

@ -28,6 +28,13 @@ export const SORTABLE_COLUMN = {
export type SortableColumnType = export type SortableColumnType =
typeof SORTABLE_COLUMN[keyof typeof SORTABLE_COLUMN]; 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 = export type SortableColumnList =
typeof SORTABLE_COLUMN[keyof typeof SORTABLE_COLUMN]; typeof SORTABLE_COLUMN[keyof typeof SORTABLE_COLUMN];
@ -38,6 +45,10 @@ export const DIRECTION = {
export type DirectionType = typeof DIRECTION[keyof typeof DIRECTION]; export type DirectionType = typeof DIRECTION[keyof typeof DIRECTION];
// DirectionTypeの型チェック関数
export const isDirectionType = (arg: string): arg is DirectionType =>
arg in DIRECTION;
export interface DisplayInfoType { export interface DisplayInfoType {
JobNumber: boolean; JobNumber: boolean;
Status: boolean; Status: boolean;

View File

@ -280,7 +280,6 @@ export const playbackAsync = createAsyncThunk<
direction: DirectionType; direction: DirectionType;
paramName: SortableColumnType; paramName: SortableColumnType;
audioFileId: number; audioFileId: number;
isTypist: boolean;
}, },
{ {
// rejectした時の返却値の型 // rejectした時の返却値の型
@ -289,7 +288,7 @@ export const playbackAsync = createAsyncThunk<
}; };
} }
>("dictations/playbackAsync", async (args, thunkApi) => { >("dictations/playbackAsync", async (args, thunkApi) => {
const { audioFileId, direction, paramName, isTypist } = args; const { audioFileId, direction, paramName } = args;
// apiのConfigurationを取得する // apiのConfigurationを取得する
const { getState } = thunkApi; const { getState } = thunkApi;
@ -300,15 +299,12 @@ export const playbackAsync = createAsyncThunk<
const tasksApi = new TasksApi(config); const tasksApi = new TasksApi(config);
const usersApi = new UsersApi(config); const usersApi = new UsersApi(config);
try { try {
// ユーザーがタイピストである場合に、ソート条件を保存する await usersApi.updateSortCriteria(
if (isTypist) { { direction, paramName },
await usersApi.updateSortCriteria( {
{ direction, paramName }, headers: { authorization: `Bearer ${accessToken}` },
{ }
headers: { authorization: `Bearer ${accessToken}` }, );
}
);
}
await tasksApi.checkout(audioFileId, { await tasksApi.checkout(audioFileId, {
headers: { authorization: `Bearer ${accessToken}` }, headers: { authorization: `Bearer ${accessToken}` },
}); });

View File

@ -34,6 +34,8 @@ import {
cancelAsync, cancelAsync,
PRIORITY, PRIORITY,
deleteTaskAsync, deleteTaskAsync,
isSortableColumnType,
isDirectionType,
} from "features/dictation"; } from "features/dictation";
import { getTranslationID } from "translation"; import { getTranslationID } from "translation";
import { Task } from "api/api"; import { Task } from "api/api";
@ -245,6 +247,12 @@ const DictationPage: React.FC = (): JSX.Element => {
dispatch(changeDirection({ direction: currentDirection })); dispatch(changeDirection({ direction: currentDirection }));
dispatch(changeParamName({ paramName })); dispatch(changeParamName({ paramName }));
// ローカルストレージにソート情報を保存する
localStorage.setItem(
"sortCriteria",
`direction:${currentDirection},paramName:${paramName}`
);
const filter = getFilter( const filter = getFilter(
filterUploaded, filterUploaded,
filterInProgress, filterInProgress,
@ -351,10 +359,11 @@ const DictationPage: React.FC = (): JSX.Element => {
audioFileId, audioFileId,
direction: sortDirection, direction: sortDirection,
paramName: sortableParamName, paramName: sortableParamName,
isTypist,
}) })
); );
if (meta.requestStatus === "fulfilled") { if (meta.requestStatus === "fulfilled") {
// ローカルストレージにソート情報を削除する
localStorage.removeItem("sortCriteria");
const filter = getFilter( const filter = getFilter(
filterUploaded, filterUploaded,
filterInProgress, filterInProgress,
@ -391,7 +400,6 @@ const DictationPage: React.FC = (): JSX.Element => {
filterInProgress, filterInProgress,
filterPending, filterPending,
filterUploaded, filterUploaded,
isTypist,
sortDirection, sortDirection,
sortableParamName, sortableParamName,
t, t,
@ -572,13 +580,39 @@ const DictationPage: React.FC = (): JSX.Element => {
dispatch(changeDisplayInfo({ column: displayInfo })); dispatch(changeDisplayInfo({ column: displayInfo }));
const filter = getFilter(true, true, true, true, false); const filter = getFilter(true, true, true, true, false);
const { meta, payload } = await dispatch(getSortColumnAsync()); const { meta, payload } = await dispatch(getSortColumnAsync());
if ( if (
meta.requestStatus === "fulfilled" && meta.requestStatus === "fulfilled" &&
payload && payload &&
!("error" in 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( dispatch(
listTasksAsync({ listTasksAsync({
limit: LIMIT_TASK_NUM, limit: LIMIT_TASK_NUM,

View File

@ -0,0 +1,5 @@
-- +migrate Up
ALTER TABLE `users` ADD INDEX `idx_role` (role);
-- +migrate Down
ALTER TABLE `users` DROP INDEX `idx_role`;

View File

@ -39,6 +39,8 @@ import {
updateEntity, updateEntity,
} from '../../common/repository'; } from '../../common/repository';
import { Context } from '../../common/log'; import { Context } from '../../common/log';
import { User } from '../users/entity/user.entity';
import { UserNotFoundError } from '../users/errors/types';
@Injectable() @Injectable()
export class LicensesRepositoryService { export class LicensesRepositoryService {
@ -559,6 +561,19 @@ export class LicensesRepositoryService {
accountId: number, accountId: number,
): Promise<void> { ): Promise<void> {
await this.dataSource.transaction(async (entityManager) => { 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 licenseRepo = entityManager.getRepository(License);
const licenseAllocationHistoryRepo = entityManager.getRepository( const licenseAllocationHistoryRepo = entityManager.getRepository(
LicenseAllocationHistory, LicenseAllocationHistory,

View File

@ -53,6 +53,7 @@ import {
deleteEntity, deleteEntity,
} from '../../common/repository'; } from '../../common/repository';
import { Context } from '../../common/log'; import { Context } from '../../common/log';
import { UserNotFoundError } from '../users/errors/types';
@Injectable() @Injectable()
export class TasksRepositoryService { export class TasksRepositoryService {
@ -172,6 +173,20 @@ export class TasksRepositoryService {
permittedSourceStatus: TaskStatus[], permittedSourceStatus: TaskStatus[],
): Promise<void> { ): Promise<void> {
await this.dataSource.transaction(async (entityManager) => { 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); const taskRepo = entityManager.getRepository(Task);
// 指定した音声ファイルIDに紐づくTaskの中でStatusが[Uploaded,Inprogress,Pending]であるものを取得 // 指定した音声ファイルIDに紐づくTaskの中でStatusが[Uploaded,Inprogress,Pending]であるものを取得
const task = await taskRepo.findOne({ const task = await taskRepo.findOne({
@ -851,6 +866,22 @@ export class TasksRepositoryService {
const createdEntity = await this.dataSource.transaction( const createdEntity = await this.dataSource.transaction(
async (entityManager) => { 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 audioFileRepo = entityManager.getRepository(AudioFile);
const newAudioFile = audioFileRepo.create(audioFile); const newAudioFile = audioFileRepo.create(audioFile);
const savedAudioFile = await insertEntity( const savedAudioFile = await insertEntity(
@ -972,6 +1003,7 @@ export class TasksRepositoryService {
deleted_at: IsNull(), deleted_at: IsNull(),
}, },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
// idはユニークであるため取得件数の一致でユーザーの存在を確認 // idはユニークであるため取得件数の一致でユーザーの存在を確認
if (typistUserIds.length !== userRecords.length) { if (typistUserIds.length !== userRecords.length) {

View File

@ -122,6 +122,7 @@ export class UserGroupsRepositoryService {
role: USER_ROLES.TYPIST, role: USER_ROLES.TYPIST,
email_verified: true, email_verified: true,
}, },
lock: { mode: 'pessimistic_write' },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
}); });
if (userRecords.length !== typistIds.length) { if (userRecords.length !== typistIds.length) {
@ -188,6 +189,7 @@ export class UserGroupsRepositoryService {
role: USER_ROLES.TYPIST, role: USER_ROLES.TYPIST,
email_verified: true, email_verified: true,
}, },
lock: { mode: 'pessimistic_write' },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
}); });
if (userRecords.length !== typistIds.length) { if (userRecords.length !== typistIds.length) {
@ -204,6 +206,7 @@ export class UserGroupsRepositoryService {
id: typistGroupId, id: typistGroupId,
account_id: accountId, account_id: accountId,
}, },
lock: { mode: 'pessimistic_write' },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
}); });
if (!typistGroup) { if (!typistGroup) {

View File

@ -87,6 +87,7 @@ export class WorkflowsRepositoryService {
const author = await userRepo.findOne({ const author = await userRepo.findOne({
where: { account_id: accountId, id: authorId, email_verified: true }, where: { account_id: accountId, id: authorId, email_verified: true },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
if (!author) { if (!author) {
throw new UserNotFoundError( throw new UserNotFoundError(
@ -100,6 +101,7 @@ export class WorkflowsRepositoryService {
const worktypes = await worktypeRepo.find({ const worktypes = await worktypeRepo.find({
where: { account_id: accountId, id: worktypeId }, where: { account_id: accountId, id: worktypeId },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
if (worktypes.length === 0) { if (worktypes.length === 0) {
throw new WorktypeIdNotFoundError( throw new WorktypeIdNotFoundError(
@ -114,6 +116,7 @@ export class WorkflowsRepositoryService {
const template = await templateRepo.findOne({ const template = await templateRepo.findOne({
where: { account_id: accountId, id: templateId }, where: { account_id: accountId, id: templateId },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
if (!template) { if (!template) {
throw new TemplateFileNotExistError('template not found.'); throw new TemplateFileNotExistError('template not found.');
@ -131,6 +134,7 @@ export class WorkflowsRepositoryService {
email_verified: true, email_verified: true,
}, },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
if (typistUsers.length !== typistIds.length) { if (typistUsers.length !== typistIds.length) {
throw new UserNotFoundError( throw new UserNotFoundError(
@ -146,6 +150,7 @@ export class WorkflowsRepositoryService {
const typistGroups = await userGroupRepo.find({ const typistGroups = await userGroupRepo.find({
where: { account_id: accountId, id: In(groupIds) }, where: { account_id: accountId, id: In(groupIds) },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
if (typistGroups.length !== groupIds.length) { if (typistGroups.length !== groupIds.length) {
throw new TypistGroupNotExistError( throw new TypistGroupNotExistError(
@ -163,6 +168,7 @@ export class WorkflowsRepositoryService {
worktype_id: worktypeId !== undefined ? worktypeId : IsNull(), worktype_id: worktypeId !== undefined ? worktypeId : IsNull(),
}, },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
if (workflow.length !== 0) { if (workflow.length !== 0) {
throw new AuthorIdAndWorktypeIdPairAlreadyExistsError( throw new AuthorIdAndWorktypeIdPairAlreadyExistsError(
@ -227,23 +233,12 @@ export class WorkflowsRepositoryService {
): Promise<void> { ): Promise<void> {
return await this.dataSource.transaction(async (entityManager) => { return await this.dataSource.transaction(async (entityManager) => {
const workflowRepo = entityManager.getRepository(Workflow); 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の存在確認 // authorの存在確認
const userRepo = entityManager.getRepository(User); const userRepo = entityManager.getRepository(User);
const author = await userRepo.findOne({ const author = await userRepo.findOne({
where: { account_id: accountId, id: authorId, email_verified: true }, where: { account_id: accountId, id: authorId, email_verified: true },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
if (!author) { if (!author) {
throw new UserNotFoundError( 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の存在確認 // worktypeの存在確認
if (worktypeId !== undefined) { if (worktypeId !== undefined) {
const worktypeRepo = entityManager.getRepository(Worktype); const worktypeRepo = entityManager.getRepository(Worktype);
const worktypes = await worktypeRepo.find({ const worktypes = await worktypeRepo.find({
where: { account_id: accountId, id: worktypeId }, where: { account_id: accountId, id: worktypeId },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
if (worktypes.length === 0) { if (worktypes.length === 0) {
throw new WorktypeIdNotFoundError( throw new WorktypeIdNotFoundError(
@ -271,6 +298,7 @@ export class WorkflowsRepositoryService {
const template = await templateRepo.findOne({ const template = await templateRepo.findOne({
where: { account_id: accountId, id: templateId }, where: { account_id: accountId, id: templateId },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
if (!template) { if (!template) {
throw new TemplateFileNotExistError( 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) => { const groupIds = typists.flatMap((typist) => {
return typist.typistGroupId ? [typist.typistGroupId] : []; return typist.typistGroupId ? [typist.typistGroupId] : [];
@ -305,6 +315,7 @@ export class WorkflowsRepositoryService {
const typistGroups = await userGroupRepo.find({ const typistGroups = await userGroupRepo.find({
where: { account_id: accountId, id: In(groupIds) }, where: { account_id: accountId, id: In(groupIds) },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
if (typistGroups.length !== groupIds.length) { if (typistGroups.length !== groupIds.length) {
throw new TypistGroupNotExistError( throw new TypistGroupNotExistError(
@ -399,6 +410,7 @@ export class WorkflowsRepositoryService {
const workflow = await workflowRepo.findOne({ const workflow = await workflowRepo.findOne({
where: { account_id: accountId, id: workflowId }, where: { account_id: accountId, id: workflowId },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
if (!workflow) { if (!workflow) {
throw new WorkflowNotFoundError( throw new WorkflowNotFoundError(