diff --git a/dictation_client/src/api/api.ts b/dictation_client/src/api/api.ts index 993f4c0..72982f3 100644 --- a/dictation_client/src/api/api.ts +++ b/dictation_client/src/api/api.ts @@ -725,6 +725,25 @@ export interface ErrorResponse { */ 'code': string; } +/** + * + * @export + * @interface FileRenameRequest + */ +export interface FileRenameRequest { + /** + * ファイル名変更対象の音声ファイルID + * @type {number} + * @memberof FileRenameRequest + */ + 'audioFileId': number; + /** + * 変更するファイル名 + * @type {string} + * @memberof FileRenameRequest + */ + 'fileName': string; +} /** * * @export @@ -1971,6 +1990,12 @@ export interface Task { * @memberof Task */ 'fileName': string; + /** + * 生(Blob Storage上の)音声ファイル名 + * @type {string} + * @memberof Task + */ + 'rawFileName': string; /** * 音声ファイルの録音時間(ミリ秒の整数値) * @type {string} @@ -5723,6 +5748,46 @@ export const FilesApiAxiosParamCreator = function (configuration?: Configuration options: localVarRequestOptions, }; }, + /** + * 音声ファイルの表示ファイル名を変更します + * @summary + * @param {FileRenameRequest} fileRenameRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + fileRename: async (fileRenameRequest: FileRenameRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'fileRenameRequest' is not null or undefined + assertParamExists('fileRename', 'fileRenameRequest', fileRenameRequest) + const localVarPath = `/files/rename`; + // 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(fileRenameRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * アップロードが完了した音声ファイルの情報を登録し、文字起こしタスクを生成します * @summary @@ -5907,6 +5972,19 @@ export const FilesApiFp = function(configuration?: Configuration) { const operationBasePath = operationServerMap['FilesApi.downloadTemplateLocation']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, + /** + * 音声ファイルの表示ファイル名を変更します + * @summary + * @param {FileRenameRequest} fileRenameRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async fileRename(fileRenameRequest: FileRenameRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.fileRename(fileRenameRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['FilesApi.fileRename']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, /** * アップロードが完了した音声ファイルの情報を登録し、文字起こしタスクを生成します * @summary @@ -5987,6 +6065,16 @@ export const FilesApiFactory = function (configuration?: Configuration, basePath downloadTemplateLocation(audioFileId: number, options?: any): AxiosPromise { return localVarFp.downloadTemplateLocation(audioFileId, options).then((request) => request(axios, basePath)); }, + /** + * 音声ファイルの表示ファイル名を変更します + * @summary + * @param {FileRenameRequest} fileRenameRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + fileRename(fileRenameRequest: FileRenameRequest, options?: any): AxiosPromise { + return localVarFp.fileRename(fileRenameRequest, options).then((request) => request(axios, basePath)); + }, /** * アップロードが完了した音声ファイルの情報を登録し、文字起こしタスクを生成します * @summary @@ -6059,6 +6147,18 @@ export class FilesApi extends BaseAPI { return FilesApiFp(this.configuration).downloadTemplateLocation(audioFileId, options).then((request) => request(this.axios, this.basePath)); } + /** + * 音声ファイルの表示ファイル名を変更します + * @summary + * @param {FileRenameRequest} fileRenameRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof FilesApi + */ + public fileRename(fileRenameRequest: FileRenameRequest, options?: AxiosRequestConfig) { + return FilesApiFp(this.configuration).fileRename(fileRenameRequest, options).then((request) => request(this.axios, this.basePath)); + } + /** * アップロードが完了した音声ファイルの情報を登録し、文字起こしタスクを生成します * @summary diff --git a/dictation_client/src/common/errors/code.ts b/dictation_client/src/common/errors/code.ts index 477282c..fcef6f2 100644 --- a/dictation_client/src/common/errors/code.ts +++ b/dictation_client/src/common/errors/code.ts @@ -84,4 +84,6 @@ export const errorCodes = [ "E018001", // パートナーアカウント削除エラー(削除条件を満たしていない) "E019001", // パートナーアカウント取得不可エラー(階層構造が不正) "E020001", // パートナーアカウント変更エラー(変更条件を満たしていない) + "E021001", // 音声ファイル名変更不可エラー(権限不足) + "E021002", // 音声ファイル名変更不可エラー(同名ファイルが存在) ] as const; diff --git a/dictation_client/src/features/dictation/dictationSlice.ts b/dictation_client/src/features/dictation/dictationSlice.ts index b187c88..e8361af 100644 --- a/dictation_client/src/features/dictation/dictationSlice.ts +++ b/dictation_client/src/features/dictation/dictationSlice.ts @@ -12,6 +12,7 @@ import { updateAssigneeAsync, cancelAsync, deleteTaskAsync, + renameFileAsync, } from "./operations"; import { SORTABLE_COLUMN, @@ -228,6 +229,16 @@ export const dictationSlice = createSlice({ builder.addCase(deleteTaskAsync.rejected, (state) => { state.apps.isLoading = false; }); + + builder.addCase(renameFileAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(renameFileAsync.fulfilled, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(renameFileAsync.rejected, (state) => { + state.apps.isLoading = false; + }); }, }); diff --git a/dictation_client/src/features/dictation/operations.ts b/dictation_client/src/features/dictation/operations.ts index 522b6d9..a25c5d3 100644 --- a/dictation_client/src/features/dictation/operations.ts +++ b/dictation_client/src/features/dictation/operations.ts @@ -689,3 +689,71 @@ export const deleteTaskAsync = createAsyncThunk< 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 }); + } +}); diff --git a/dictation_client/src/pages/DictationPage/filePropertyPopup.tsx b/dictation_client/src/pages/DictationPage/filePropertyPopup.tsx index 979bbf1..ab234b2 100644 --- a/dictation_client/src/pages/DictationPage/filePropertyPopup.tsx +++ b/dictation_client/src/pages/DictationPage/filePropertyPopup.tsx @@ -1,13 +1,15 @@ -import React, { useCallback } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import styles from "styles/app.module.scss"; -import { useSelector } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { selectSelectedFileTask, selectIsLoading, PRIORITY, + renameFileAsync, } from "features/dictation"; import { getTranslationID } from "translation"; import { useTranslation } from "react-i18next"; +import { AppDispatch } from "app/store"; import close from "../../assets/images/close.svg"; import lock from "../../assets/images/lock.svg"; @@ -19,14 +21,46 @@ interface FilePropertyPopupProps { export const FilePropertyPopup: React.FC = (props) => { const { onClose, isOpen } = props; const [t] = useTranslation(); + const dispatch: AppDispatch = useDispatch(); const isLoading = useSelector(selectIsLoading); + const [isPushSaveButton, setIsPushSaveButton] = useState(false); + // ポップアップを閉じる処理 const closePopup = useCallback(() => { + setIsPushSaveButton(false); onClose(false); }, [onClose]); const selectedFileTask = useSelector(selectSelectedFileTask); + const [fileName, setFileName] = useState(""); + + useEffect(() => { + if (isOpen) { + setFileName(selectedFileTask?.fileName ?? ""); + } + }, [selectedFileTask, isOpen]); + + // ファイル名の保存処理 + const saveFileName = useCallback(async () => { + setIsPushSaveButton(true); + if (fileName.length === 0) { + return; + } + const { meta } = await dispatch( + renameFileAsync({ + audioFileId: selectedFileTask?.audioFileId ?? 0, + fileName, + }) + ); + + setIsPushSaveButton(false); + + if (meta.requestStatus === "fulfilled") { + onClose(true); + } + }, [dispatch, onClose, fileName, selectedFileTask]); + return (
@@ -45,7 +79,32 @@ export const FilePropertyPopup: React.FC = (props) => { {t(getTranslationID("filePropertyPopup.label.general"))}
{t(getTranslationID("dictationPage.label.fileName"))}
-
{selectedFileTask?.fileName.replace(".zip", "") ?? ""}
+
+ setFileName(e.target.value)} + /> + + {isPushSaveButton && fileName.length === 0 && ( + + {t(getTranslationID("common.message.inputEmptyError"))} + + )} +
+
{t(getTranslationID("dictationPage.label.rawFileName"))}
+
{selectedFileTask?.rawFileName ?? ""}
{t(getTranslationID("dictationPage.label.fileSize"))}
{selectedFileTask?.fileSize ?? ""}
{t(getTranslationID("dictationPage.label.fileLength"))}
diff --git a/dictation_client/src/pages/DictationPage/index.tsx b/dictation_client/src/pages/DictationPage/index.tsx index ec0ee93..f73dd27 100644 --- a/dictation_client/src/pages/DictationPage/index.tsx +++ b/dictation_client/src/pages/DictationPage/index.tsx @@ -499,9 +499,39 @@ const DictationPage: React.FC = (): JSX.Element => { setIsBackupPopupOpen(true); }, []); - const onCloseFilePropertyPopup = useCallback(() => { - setIsFilePropertyPopupOpen(false); - }, []); + const onCloseFilePropertyPopup = useCallback( + (isChanged: boolean) => { + if (isChanged) { + const filter = getFilter( + filterUploaded, + filterInProgress, + filterPending, + filterFinished, + filterBackup + ); + dispatch( + listTasksAsync({ + limit: LIMIT_TASK_NUM, + offset: 0, + filter, + direction: sortDirection, + paramName: sortableParamName, + }) + ); + } + setIsFilePropertyPopupOpen(false); + }, + [ + dispatch, + filterUploaded, + filterInProgress, + filterPending, + filterFinished, + filterBackup, + sortDirection, + sortableParamName, + ] + ); const sortIconClass = ( currentParam: SortableColumnType, @@ -1309,9 +1339,7 @@ const DictationPage: React.FC = (): JSX.Element => { {x.workType} )} {displayColumn.FileName && ( - - {x.fileName.replace(".zip", "")} - + {x.fileName} )} {displayColumn.FileLength && ( {x.audioDuration} diff --git a/dictation_client/src/translation/de.json b/dictation_client/src/translation/de.json index e163161..f6ba44f 100644 --- a/dictation_client/src/translation/de.json +++ b/dictation_client/src/translation/de.json @@ -254,7 +254,9 @@ "deleteFailedError": "(de)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。", "licenseNotAssignedError": "Die Transkription ist nicht möglich, da keine gültige Lizenz zugewiesen ist. Bitten Sie Ihren Administrator, eine gültige Lizenz zuzuweisen.", "licenseExpiredError": "Die Transkription ist nicht möglich, da Ihre Lizenz abgelaufen ist. Bitte bitten Sie Ihren Administrator, Ihnen eine gültige Lizenz zuzuweisen.", - "fileAlreadyDeletedError": "(de)既に削除された音声ファイルが含まれています。画面を更新し、再度ご確認ください" + "fileAlreadyDeletedError": "(de)既に削除された音声ファイルが含まれています。画面を更新し、再度ご確認ください", + "fileRenameFailedError": "(de)ファイル名の変更に失敗しました。画面を更新し、再度ご確認ください。", + "fileNameAleadyExistsError": "(de)このファイル名は既に登録されています。他のファイル名で登録してください。" }, "label": { "title": "Diktate", @@ -300,7 +302,9 @@ "fileBackup": "Dateisicherung", "downloadForBackup": "Zur Sicherung herunterladen", "applications": "Desktopanwendung", - "cancelDictation": "Transkription abbrechen" + "cancelDictation": "Transkription abbrechen", + "rawFileName": "(de)Raw File Name", + "fileNameSave": "(de)Save" } }, "cardLicenseIssuePopupPage": { diff --git a/dictation_client/src/translation/en.json b/dictation_client/src/translation/en.json index df0214b..cee7426 100644 --- a/dictation_client/src/translation/en.json +++ b/dictation_client/src/translation/en.json @@ -254,7 +254,9 @@ "deleteFailedError": "タスクの削除に失敗しました。画面を更新し、再度ご確認ください。", "licenseNotAssignedError": "Transcription is not possible because a valid license is not assigned. Please ask your administrator to assign a valid license.", "licenseExpiredError": "Transcription is not possible because your license is expired. Please ask your administrator to assign a valid license.", - "fileAlreadyDeletedError": "既に削除された音声ファイルが含まれています。画面を更新し、再度ご確認ください" + "fileAlreadyDeletedError": "既に削除された音声ファイルが含まれています。画面を更新し、再度ご確認ください", + "fileRenameFailedError": "ファイル名の変更に失敗しました。画面を更新し、再度ご確認ください。", + "fileNameAleadyExistsError": "このファイル名は既に登録されています。他のファイル名で登録してください。" }, "label": { "title": "Dictations", @@ -300,7 +302,9 @@ "fileBackup": "File Backup", "downloadForBackup": "Download for backup", "applications": "Desktop Application", - "cancelDictation": "Cancel Transcription" + "cancelDictation": "Cancel Transcription", + "rawFileName": "Raw File Name", + "fileNameSave": "Save" } }, "cardLicenseIssuePopupPage": { diff --git a/dictation_client/src/translation/es.json b/dictation_client/src/translation/es.json index e526a1d..c6bfe33 100644 --- a/dictation_client/src/translation/es.json +++ b/dictation_client/src/translation/es.json @@ -254,7 +254,9 @@ "deleteFailedError": "(es)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。", "licenseNotAssignedError": "La transcripción no es posible porque no se ha asignado una licencia válida. Solicite a su administrador que le asigne una licencia válida.", "licenseExpiredError": "La transcripción no es posible porque su licencia ha caducado. Solicite a su administrador que le asigne una licencia válida.", - "fileAlreadyDeletedError": "(es)既に削除された音声ファイルが含まれています。画面を更新し、再度ご確認ください" + "fileAlreadyDeletedError": "(es)既に削除された音声ファイルが含まれています。画面を更新し、再度ご確認ください", + "fileRenameFailedError": "(es)ファイル名の変更に失敗しました。画面を更新し、再度ご確認ください。", + "fileNameAleadyExistsError": "(es)このファイル名は既に登録されています。他のファイル名で登録してください。" }, "label": { "title": "Dictado", @@ -300,7 +302,9 @@ "fileBackup": "Copia de seguridad de archivos", "downloadForBackup": "Descargar para respaldo", "applications": "Aplicación de escritorio", - "cancelDictation": "Cancelar transcripción" + "cancelDictation": "Cancelar transcripción", + "rawFileName": "(es)Raw File Name", + "fileNameSave": "(es)Save" } }, "cardLicenseIssuePopupPage": { diff --git a/dictation_client/src/translation/fr.json b/dictation_client/src/translation/fr.json index 393e5d9..071295b 100644 --- a/dictation_client/src/translation/fr.json +++ b/dictation_client/src/translation/fr.json @@ -254,7 +254,9 @@ "deleteFailedError": "(fr)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。", "licenseNotAssignedError": "La transcription n'est pas possible car aucune licence valide n'a été attribuée. Veuillez demander à votre administrateur d'attribuer une licence valide.", "licenseExpiredError": "La transcription n'est pas possible car votre licence est expirée. Veuillez demander à votre administrateur de vous attribuer une licence valide.", - "fileAlreadyDeletedError": "(fr)既に削除された音声ファイルが含まれています。画面を更新し、再度ご確認ください" + "fileAlreadyDeletedError": "(fr)既に削除された音声ファイルが含まれています。画面を更新し、再度ご確認ください", + "fileRenameFailedError": "(fr)ファイル名の変更に失敗しました。画面を更新し、再度ご確認ください。", + "fileNameAleadyExistsError": "(fr)このファイル名は既に登録されています。他のファイル名で登録してください。" }, "label": { "title": "Dictées", @@ -300,7 +302,9 @@ "fileBackup": "Sauvegarde de fichiers", "downloadForBackup": "Télécharger pour sauvegarde", "applications": "Application de bureau", - "cancelDictation": "Annuler la transcription" + "cancelDictation": "Annuler la transcription", + "rawFileName": "(fr)Raw File Name", + "fileNameSave": "(fr)Save" } }, "cardLicenseIssuePopupPage": { diff --git a/dictation_server/src/repositories/tasks/tasks.repository.service.ts b/dictation_server/src/repositories/tasks/tasks.repository.service.ts index 4a1e484..0727f5b 100644 --- a/dictation_server/src/repositories/tasks/tasks.repository.service.ts +++ b/dictation_server/src/repositories/tasks/tasks.repository.service.ts @@ -853,7 +853,7 @@ export class TasksRepositoryService { audioFile.account_id = account_id; audioFile.owner_user_id = owner_user_id; audioFile.url = url; - audioFile.file_name = file_name; + audioFile.file_name = file_name.replace('.zip', ''); audioFile.raw_file_name = file_name; audioFile.author_id = author_id; audioFile.work_type_id = work_type_id;