shimoda.m b3845187f6 Merged PR 1009: Revert "Merged PR 1006: 2025/1/27 PH1エンハンス 本番リリース"
Revert "Merged PR 1006: 2025/1/27 PH1エンハンス 本番リリース"

Reverted commit `b5293888`.

デプロイミスによる切り戻し
2025-01-21 04:47:21 +00:00

832 lines
22 KiB
TypeScript

import { createAsyncThunk } from "@reduxjs/toolkit";
import type { RootState } from "app/store";
import { getTranslationID } from "translation";
import { openSnackbar } from "features/ui/uiSlice";
import { getAccessToken } from "features/auth";
import { BlobClient } from "@azure/storage-blob";
import {
TasksResponse,
TasksApi,
UsersApi,
AccountsApi,
GetTypistsResponse,
GetTypistGroupsResponse,
Assignee,
FilesApi,
} from "../../api/api";
import { Configuration } from "../../api/configuration";
import { ErrorObject, createErrorObject } from "../../common/errors";
import {
BACKUP_POPUP_LIST_SIZE,
BACKUP_POPUP_LIST_STATUS,
DIRECTION,
DirectionType,
SORTABLE_COLUMN,
SortableColumnType,
} from "./constants";
import { BackupTask } from "./types";
export const listTasksAsync = createAsyncThunk<
TasksResponse,
{
// パラメータ
limit: number;
offset: number;
filter?: string;
direction: DirectionType;
paramName: SortableColumnType;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("dictations/listTasksAsync", async (args, thunkApi) => {
const { limit, offset, filter, direction, paramName } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const tasksApi = new TasksApi(config);
try {
const res = await tasksApi.getTasks(
limit,
offset,
filter,
direction,
paramName,
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
return res.data;
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const getSortColumnAsync = createAsyncThunk<
{
direction: DirectionType;
paramName: SortableColumnType;
},
void,
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("dictations/getSortColumnAsync", async (args, thunkApi) => {
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const usersApi = new UsersApi(config);
try {
const sort = await usersApi.getSortCriteria({
headers: { authorization: `Bearer ${accessToken}` },
});
const { direction, paramName } = sort.data;
if (
Object.values<string>(DIRECTION).includes(direction) &&
Object.values<string>(SORTABLE_COLUMN).includes(paramName)
) {
return {
direction: direction as DirectionType,
paramName: paramName as SortableColumnType,
};
}
throw new Error(
`invalid param. direction=${direction}, paramName=${paramName}`
);
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const listTypistsAsync = createAsyncThunk<
GetTypistsResponse,
void,
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("dictations/listTypistsAsync", async (args, thunkApi) => {
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const accountsApi = new AccountsApi(config);
try {
const typists = await accountsApi.getTypists({
headers: { authorization: `Bearer ${accessToken}` },
});
return typists.data;
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const listTypistGroupsAsync = createAsyncThunk<
GetTypistGroupsResponse,
void,
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("dictations/listTypistGroupsAsync", async (args, thunkApi) => {
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const accountsApi = new AccountsApi(config);
try {
const typistGroup = await accountsApi.getTypistGroups({
headers: { authorization: `Bearer ${accessToken}` },
});
return typistGroup.data;
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const updateAssigneeAsync = createAsyncThunk<
{
/** empty */
},
{
audioFileId: number;
assignees: Assignee[];
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("dictations/updateAssigneeAsync", async (args, thunkApi) => {
const { audioFileId, assignees } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const tasksApi = new TasksApi(config);
try {
await tasksApi.changeCheckoutPermission(
audioFileId,
{ assignees },
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
// ステータスがUploaded以外、タスクが存在しない場合
if (error.code === "E010601") {
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("dictationPage.message.taskNotEditable"),
})
);
return thunkApi.rejectWithValue({ error });
}
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const playbackAsync = createAsyncThunk<
{
/** empty */
},
{
direction: DirectionType;
paramName: SortableColumnType;
audioFileId: number;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("dictations/playbackAsync", async (args, thunkApi) => {
const { audioFileId, direction, paramName } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const tasksApi = new TasksApi(config);
const usersApi = new UsersApi(config);
try {
await usersApi.updateSortCriteria(
{ direction, paramName },
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
await tasksApi.checkout(audioFileId, {
headers: { authorization: `Bearer ${accessToken}` },
});
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
// ステータスが[Uploaded,Inprogress,Pending]以外、またはタスクが存在しない場合
if (error.code === "E010601") {
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID(
"dictationPage.message.taskToPlaybackNoExists"
),
})
);
return thunkApi.rejectWithValue({ error });
}
// タスクをチェックアウトする権限がない
if (error.code === "E010602") {
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID(
"dictationPage.message.noPlaybackAuthorization"
),
})
);
return thunkApi.rejectWithValue({ error });
}
// ライセンスの有効期限が切れている場合
if (error.code === "E010805") {
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID(
"dictationPage.message.licenseExpiredError"
),
})
);
return thunkApi.rejectWithValue({ error });
}
// ライセンスが未割当の場合
if (error.code === "E010812") {
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID(
"dictationPage.message.licenseNotAssignedError"
),
})
);
return thunkApi.rejectWithValue({ error });
}
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const cancelAsync = createAsyncThunk<
{
/** empty */
},
{
direction: DirectionType;
paramName: SortableColumnType;
audioFileId: number;
isTypist: boolean;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("dictations/cancelAsync", async (args, thunkApi) => {
const { audioFileId, direction, paramName, isTypist } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const tasksApi = new TasksApi(config);
const usersApi = new UsersApi(config);
try {
// ユーザーがタイピストである場合に、ソート条件を保存する
if (isTypist) {
await usersApi.updateSortCriteria(
{ direction, paramName },
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
}
await tasksApi.cancel(audioFileId, {
headers: { authorization: `Bearer ${accessToken}` },
});
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
// ステータスが[Inprogress,Pending]以外、またはタスクが存在しない場合、またはtypistで自分のタスクでない場合
if (error.code === "E010601" || error.code === "E010603") {
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("dictationPage.message.cancelFailedError"),
})
);
return thunkApi.rejectWithValue({ error });
}
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const reopenAsync = createAsyncThunk<
{
/** empty */
},
{
direction: DirectionType;
paramName: SortableColumnType;
audioFileId: number;
isTypist: boolean;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("dictations/reopenAsync", async (args, thunkApi) => {
const { audioFileId, direction, paramName, isTypist } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const tasksApi = new TasksApi(config);
const usersApi = new UsersApi(config);
try {
// ユーザーがタイピストである場合に、ソート条件を保存する
if (isTypist) {
await usersApi.updateSortCriteria(
{ direction, paramName },
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
}
await tasksApi.reopen(audioFileId, {
headers: { authorization: `Bearer ${accessToken}` },
});
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
// ステータスが[Finished]以外、またはタスクが存在しない場合、またはtypistで自分のタスクでない場合
if (error.code === "E010601" || error.code === "E010603") {
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("dictationPage.message.reopenFailedError"),
})
);
return thunkApi.rejectWithValue({ error });
}
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const listBackupPopupTasksAsync = createAsyncThunk<
TasksResponse,
{
// パラメータ
offset: number;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("dictations/listBackupPopupTasksAsync", async (args, thunkApi) => {
const { offset } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const tasksApi = new TasksApi(config);
try {
const res = await tasksApi.getTasks(
BACKUP_POPUP_LIST_SIZE,
offset,
BACKUP_POPUP_LIST_STATUS.join(","), // ステータスはFinished,Backupのみ
DIRECTION.DESC,
SORTABLE_COLUMN.Status,
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
return res.data;
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const backupTasksAsync = createAsyncThunk<
{
// empty
},
{
// パラメータ
tasks: BackupTask[];
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("dictations/backupTasksAsync", async (args, thunkApi) => {
const { tasks } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const tasksApi = new TasksApi(config);
const filesApi = new FilesApi(config);
try {
// eslint-disable-next-line no-restricted-syntax
for (const task of tasks) {
if (task.checked) {
// eslint-disable-next-line no-await-in-loop
const { data } = await filesApi.downloadLocation(task.audioFileId, {
headers: { authorization: `Bearer ${accessToken}` },
});
const { url } = data;
const { pathname } = new URL(url);
const paths = pathname.split("/").filter((p) => p !== "");
if (paths.length < 2) {
throw new Error("invalid path");
}
const blobName = paths[1];
// コンテナとBlobを取得
const blobClient = new BlobClient(url);
// Blobをダウンロード
// eslint-disable-next-line no-await-in-loop
const blobDownloadResponse = await blobClient.download();
// eslint-disable-next-line no-await-in-loop
const blobBody = await blobDownloadResponse.blobBody;
if (!blobBody) {
throw new Error("invalid blobBody");
}
// ダウンロードしたBlobをローカルに保存するリンクを作成してクリックする
const blobURL = window.URL.createObjectURL(blobBody);
const a = document.createElement("a");
a.href = blobURL;
a.download = blobName;
document.body.appendChild(a);
a.click();
a.parentNode?.removeChild(a);
// バックアップ済みに更新
try {
// eslint-disable-next-line no-await-in-loop
await tasksApi.backup(task.audioFileId, {
headers: { authorization: `Bearer ${accessToken}` },
});
} catch (e) {
// e ⇒ errorObjectに変換
const error = createErrorObject(e);
if (error.code === "E010603") {
// タスクが削除済みの場合は成功扱いとする
} else {
throw e;
}
}
}
}
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
// e ⇒ errorObjectに変換
const error = createErrorObject(e);
if (error.code === "E010603") {
// 存在しない音声ファイルをダウンロードしようとした場合
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID(
"dictationPage.message.fileAlreadyDeletedError"
),
})
);
return thunkApi.rejectWithValue({ error });
}
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("dictationPage.message.backupFailedError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const deleteTaskAsync = createAsyncThunk<
{
// empty
},
{
// パラメータ
audioFileId: number;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("dictations/deleteTaskAsync", async (args, thunkApi) => {
const { audioFileId } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const tasksApi = new TasksApi(config);
try {
await tasksApi.deleteTask(audioFileId, {
headers: { authorization: `Bearer ${accessToken}` },
});
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
let message = getTranslationID("common.message.internalServerError");
if (error.statusCode === 400) {
if (error.code === "E010603") {
// タスクが削除済みの場合は成功扱いとする
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
}
if (error.code === "E010601") {
// タスクがInprogressの場合はエラー
message = getTranslationID("dictationPage.message.deleteFailedError");
}
}
thunkApi.dispatch(
openSnackbar({
level: "error",
message,
})
);
return thunkApi.rejectWithValue({ error });
}
});
export const renameFileAsync = createAsyncThunk<
{
// empty
},
{
// パラメータ
audioFileId: number;
fileName: string;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("dictations/renameFileAsync", async (args, thunkApi) => {
const { audioFileId, fileName } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const filesApi = new FilesApi(config);
try {
await filesApi.fileRename(
{ fileName, audioFileId },
{ headers: { authorization: `Bearer ${accessToken}` } }
);
thunkApi.dispatch(
openSnackbar({
level: "info",
message: getTranslationID("common.message.success"),
})
);
return {};
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
let message = getTranslationID("common.message.internalServerError");
// 変更権限がない場合はエラー
if (error.code === "E021001") {
message = getTranslationID("dictationPage.message.fileRenameFailedError");
}
// ファイル名が既に存在する場合はエラー
if (error.code === "E021002") {
message = getTranslationID(
"dictationPage.message.fileNameAleadyExistsError"
);
}
thunkApi.dispatch(
openSnackbar({
level: "error",
message,
})
);
return thunkApi.rejectWithValue({ error });
}
});