From d5987dd882ae152df8f01226413db7d1d209cc9f Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 26 Feb 2025 10:53:24 +0530 Subject: [PATCH] rework rename dialog --- .../base/components/SingleInputDialog.tsx | 4 +- .../base/components/SingleInputForm.tsx | 9 +- web/packages/gallery/components/FileInfo.tsx | 128 +++++++++--------- 3 files changed, 72 insertions(+), 69 deletions(-) diff --git a/web/packages/base/components/SingleInputDialog.tsx b/web/packages/base/components/SingleInputDialog.tsx index aa45ab65ad..6e9d42724c 100644 --- a/web/packages/base/components/SingleInputDialog.tsx +++ b/web/packages/base/components/SingleInputDialog.tsx @@ -13,8 +13,8 @@ type SingleInputDialogProps = ModalVisibilityProps & * A dialog that can be used to ask for a single text input using a * {@link SingleInputForm}. * - * If the submission handler provided to this component resolves successfully, - * then the dialog is closed. + * The dialog closes when the promise returned by the {@link onSubmit} callback + * fulfills. * * See also: {@link CollectionNamer}, its older sibling. */ diff --git a/web/packages/base/components/SingleInputForm.tsx b/web/packages/base/components/SingleInputForm.tsx index 5b97c50109..8ab4adbb60 100644 --- a/web/packages/base/components/SingleInputForm.tsx +++ b/web/packages/base/components/SingleInputForm.tsx @@ -8,7 +8,7 @@ import React from "react"; export type SingleInputFormProps = Pick< TextFieldProps, - "label" | "placeholder" | "autoComplete" | "autoFocus" + "label" | "placeholder" | "autoComplete" | "autoFocus" | "slotProps" > & { /** * The initial value, if any, to prefill in the input. @@ -43,9 +43,8 @@ export type SingleInputFormProps = Pick< * A TextField and two buttons. * * A common requirement is taking a single textual input from the user. This is - * a form suitable for that purpose - it is form containing a single MUI - * {@link TextField}, with two accompanying buttons; one to submit, and one to - * cancel. + * a form suitable for that purpose. It contains a single MUI {@link TextField} + * and two accompanying buttons; one to submit, and one to cancel. * * Submission is handled as an async function, during which the input is * disabled and a loading indicator is shown. Errors during submission are shown @@ -107,7 +106,7 @@ export const SingleInputForm: React.FC = ({ diff --git a/web/packages/gallery/components/FileInfo.tsx b/web/packages/gallery/components/FileInfo.tsx index 2aa845713a..44064bee0b 100644 --- a/web/packages/gallery/components/FileInfo.tsx +++ b/web/packages/gallery/components/FileInfo.tsx @@ -12,6 +12,7 @@ import { TitledMiniDialog } from "@/base/components/MiniDialog"; import { type ButtonishProps } from "@/base/components/mui"; import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator"; import { SidebarDrawer } from "@/base/components/mui/SidebarDrawer"; +import { SingleInputForm } from "@/base/components/SingleInputForm"; import { Titlebar } from "@/base/components/Titlebar"; import { EllipsizedTypography } from "@/base/components/Typography"; import { @@ -22,7 +23,6 @@ import { useBaseContext } from "@/base/context"; import { haveWindow } from "@/base/env"; import { nameAndExtension } from "@/base/file-name"; import { formattedDate, formattedTime } from "@/base/i18n-date"; -import log from "@/base/log"; import type { Location } from "@/base/types"; import { CopyButton } from "@/gallery/components/FileInfoComponents"; import { tagNumericValue, type RawExifTags } from "@/gallery/services/exif"; @@ -60,9 +60,6 @@ import { } from "@/new/photos/services/ml"; import { updateMapEnabled } from "@/new/photos/services/settings"; import { FlexWrapper } from "@ente/shared/components/Container"; -import SingleInputForm, { - type SingleInputFormProps, -} from "@ente/shared/components/SingleInputForm"; import CalendarTodayIcon from "@mui/icons-material/CalendarToday"; import CameraOutlinedIcon from "@mui/icons-material/CameraOutlined"; import CloseIcon from "@mui/icons-material/Close"; @@ -79,6 +76,7 @@ import { Button, CircularProgress, IconButton, + InputAdornment, Link, Stack, styled, @@ -739,26 +737,13 @@ const FileName: React.FC = ({ allowEdits, scheduleUpdate, }) => { - const [isInEditMode, setIsInEditMode] = useState(false); - const openEditMode = () => setIsInEditMode(true); - const closeEditMode = () => setIsInEditMode(false); - const [fileName, setFileName] = useState(); - const [extension, setExtension] = useState(); + const { show: showRename, props: renameVisibilityProps } = + useModalVisibility(); - useEffect(() => { - const [filename, extension] = nameAndExtension(file.metadata.title); - setFileName(filename); - setExtension(extension); - }, [file]); + const fileName = file.metadata.title; - const saveEdits = async (newFilename: string) => { - if (fileName === newFilename) { - closeEditMode(); - return; - } - setFileName(newFilename); - const newTitle = [newFilename, extension].join("."); - const updatedFile = await changeFileName(file, newTitle); + const handleRename = async (newFileName: string) => { + const updatedFile = await changeFileName(file, newFileName); updateExistingFilePubMetadata(file, updatedFile); scheduleUpdate(); }; @@ -781,18 +766,16 @@ const FileName: React.FC = ({ <> + allowEdits && } /> - ); @@ -810,49 +793,70 @@ const createMultipartCaption = ( ); -interface FileNameEditDialogProps { - isInEditMode: boolean; - closeEditMode: () => void; - filename: string; - extension: string | undefined; - saveEdits: (name: string) => Promise; -} +type RenameFileDialogProps = ModalVisibilityProps & { + /** + * The current name of the file. + */ + fileName: string; + /** + * Called when the user makes a change to the existing name and activates the + * rename button on the dialog. + * + * @param newFileName The changed name. The extension currently cannot be + * modified, but it is guaranteed the name component of {@link newFileName} + * will be different from that of the {@link fileName} prop of the dialog. + * + * Until the promise settles, the dialog will show an activity indicator. If + * the promise rejects, it will also show an error. If the promise is + * fulfilled, then the dialog will also be closed. + * + * The dialog will also be closed if the user activates the rename button + * without changing the name. + */ + onRename: (newFileName: string) => Promise; +}; -const FileNameEditDialog: React.FC = ({ - isInEditMode, - closeEditMode, - filename, - extension, - saveEdits, +const RenameFileDialog: React.FC = ({ + open, + onClose, + fileName, + onRename, }) => { - const onSubmit: SingleInputFormProps["callback"] = async ( - filename, - setFieldError, - ) => { - try { - await saveEdits(filename); - closeEditMode(); - } catch (e) { - log.error(e); - setFieldError(t("generic_error_retry")); + const [name, extension] = nameAndExtension(fileName); + + const handleSubmit = async (newName: string) => { + const newFileName = [newName, extension].filter((x) => !!x).join("."); + if (newFileName != fileName) { + await onRename(newFileName); } + onClose(); }; + return ( + {`.${extension}`} + + ), + }, + }} /> );