rework rename dialog

This commit is contained in:
Manav Rathi
2025-02-26 10:53:24 +05:30
parent 1bd44351d9
commit d5987dd882
3 changed files with 72 additions and 69 deletions

View File

@@ -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.
*/

View File

@@ -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<SingleInputFormProps> = ({
</FocusVisibleButton>
<LoadingButton
fullWidth
color="accent"
color="primary"
type="submit"
loading={formik.isSubmitting}
>

View File

@@ -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<FileNameProps> = ({
allowEdits,
scheduleUpdate,
}) => {
const [isInEditMode, setIsInEditMode] = useState(false);
const openEditMode = () => setIsInEditMode(true);
const closeEditMode = () => setIsInEditMode(false);
const [fileName, setFileName] = useState<string>();
const [extension, setExtension] = useState<string>();
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<FileNameProps> = ({
<>
<InfoItem
icon={icon}
title={[fileName, extension].join(".")}
title={fileName}
caption={caption}
trailingButton={
allowEdits && <EditButton onClick={openEditMode} />
allowEdits && <EditButton onClick={showRename} />
}
/>
<FileNameEditDialog
isInEditMode={isInEditMode}
closeEditMode={closeEditMode}
filename={fileName!}
extension={extension}
saveEdits={saveEdits}
<RenameFileDialog
{...renameVisibilityProps}
fileName={fileName}
onRename={handleRename}
/>
</>
);
@@ -810,49 +793,70 @@ const createMultipartCaption = (
</Stack>
);
interface FileNameEditDialogProps {
isInEditMode: boolean;
closeEditMode: () => void;
filename: string;
extension: string | undefined;
saveEdits: (name: string) => Promise<void>;
}
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<void>;
};
const FileNameEditDialog: React.FC<FileNameEditDialogProps> = ({
isInEditMode,
closeEditMode,
filename,
extension,
saveEdits,
const RenameFileDialog: React.FC<RenameFileDialogProps> = ({
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 (
<TitledMiniDialog
{...{ open, onClose }}
sx={{ zIndex: aboveFileViewerContentZ }}
open={isInEditMode}
onClose={closeEditMode}
title={t("rename_file")}
>
<SingleInputForm
initialValue={filename}
callback={onSubmit}
label={t("file_name")}
placeholder={t("enter_file_name")}
buttonText={t("rename")}
fieldType="text"
caption={extension}
secondaryButtonAction={closeEditMode}
submitButtonProps={{ sx: { mt: 1, mb: 2 } }}
autoFocus
initialValue={name}
submitButtonTitle={t("rename")}
onSubmit={handleSubmit}
onCancel={onClose}
slotProps={{
input: {
// Align the adornment text to the input text.
sx: { alignItems: "baseline" },
endAdornment: extension && (
<InputAdornment position="end">
{`.${extension}`}
</InputAdornment>
),
},
}}
/>
</TitledMiniDialog>
);