From aef32027a15e9534f93f27126dd660347b0b194e Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 26 Sep 2024 15:49:25 +0530 Subject: [PATCH] Name input --- .../new/photos/components/Gallery/index.tsx | 11 +-- .../new/photos/components/NameInputDialog.tsx | 68 +++++++++++++++++++ .../new/photos/components/mui-custom.tsx | 10 +++ .../new/photos/components/use-wrap.tsx | 46 ++++++++++--- web/packages/new/photos/services/ml/people.ts | 21 +++++- 5 files changed, 140 insertions(+), 16 deletions(-) create mode 100644 web/packages/new/photos/components/NameInputDialog.tsx diff --git a/web/packages/new/photos/components/Gallery/index.tsx b/web/packages/new/photos/components/Gallery/index.tsx index 73edf70f08..202fbef019 100644 --- a/web/packages/new/photos/components/Gallery/index.tsx +++ b/web/packages/new/photos/components/Gallery/index.tsx @@ -21,7 +21,7 @@ import { t } from "i18next"; import React from "react"; import type { NewAppContextPhotos } from "../../types/context"; import { SpaceBetweenFlex } from "../mui-custom"; -import { useWrapAsyncOperation } from "../use-wrap"; +import { useWrapLoad, useWrapLoadError } from "../use-wrap"; import { GalleryItemsHeaderAdapter, GalleryItemsSummary } from "./ListHeader"; /** @@ -64,15 +64,18 @@ export const PersonListHeader: React.FC = ({ // TODO-Cluster const hasOptions = process.env.NEXT_PUBLIC_ENTE_WIP_CL; - const wrap = useWrapAsyncOperation(appContext); + const wrapLoadError = useWrapLoadError(appContext); - const addPerson = wrap(async () => { + const wrapLoad = useWrapLoad(appContext); + + const addPerson = wrapLoadError(async () => { console.log("add person"); + await wait(2000); throw new Error("test"); }); - const rename = wrap(async () => { + const rename = wrapLoad(async () => { console.log("add person"); await wait(2000); throw new Error("test"); diff --git a/web/packages/new/photos/components/NameInputDialog.tsx b/web/packages/new/photos/components/NameInputDialog.tsx new file mode 100644 index 0000000000..de7c5b99d3 --- /dev/null +++ b/web/packages/new/photos/components/NameInputDialog.tsx @@ -0,0 +1,68 @@ +import log from "@/base/log"; +import DialogBoxV2 from "@ente/shared/components/DialogBoxV2"; +import SingleInputForm, { + type SingleInputFormProps, +} from "@ente/shared/components/SingleInputForm"; +import { t } from "i18next"; +import React from "react"; +import type { DialogVisiblityProps } from "./mui-custom"; + +type NameInputDialogProps = DialogVisiblityProps & { + /** Title of the dialog. */ + title: string; + /** Placeholder string to show in the text input when it is empty. */ + placeholder: string; + /** The existing value, if any, of the text input. */ + initialValue: string | undefined; + /** Title of the submit button */ + submitButtonTitle: string; + /** + * Callback invoked when the submit button is pressed. + * + * @param name The current value of the text input. + * */ + onSubmit: (name: string) => Promise; +}; + +/** + * A dialog that can be used to ask for a name or some other such singular text + * input. + * + * See also: {@link CollectionNamer}, its older sibling. + */ +export const NameInputDialog: React.FC = ({ + open, + onClose, + title, + placeholder, + initialValue, + submitButtonTitle, + onSubmit, +}) => { + const handleSubmit: SingleInputFormProps["callback"] = async ( + inputValue, + setFieldError, + ) => { + try { + await onSubmit(inputValue); + onClose(); + } catch (e) { + log.error(`Error when submitting value ${inputValue}`, e); + setFieldError(t("UNKNOWN_ERROR")); + } + }; + + return ( + + + + ); +}; diff --git a/web/packages/new/photos/components/mui-custom.tsx b/web/packages/new/photos/components/mui-custom.tsx index ceed61ea91..10ab3fdba6 100644 --- a/web/packages/new/photos/components/mui-custom.tsx +++ b/web/packages/new/photos/components/mui-custom.tsx @@ -1,5 +1,15 @@ import { Box, IconButton, styled } from "@mui/material"; +/** + * Common props to control the display of a dialog-like component. + */ +export interface DialogVisiblityProps { + /** If `true`, the dialog is shown. */ + open: boolean; + /** Callback fired when the dialog wants to be closed. */ + onClose: () => void; +} + /** * A MUI {@link IconButton} filled in with at faint background. */ diff --git a/web/packages/new/photos/components/use-wrap.tsx b/web/packages/new/photos/components/use-wrap.tsx index 035a2b46b3..0667afaff6 100644 --- a/web/packages/new/photos/components/use-wrap.tsx +++ b/web/packages/new/photos/components/use-wrap.tsx @@ -4,17 +4,21 @@ import type { NewAppContextPhotos } from "../types/context"; /** * Return a wrap function. * - * This wrap function itself takes an async function, and returns new - * function by wrapping an async function in an error handler, showing the - * global loading bar when the function runs. + * This returned wrap function itself takes an async function, and will return a + * new function that wraps the provided async function (a) in an error handler, + * and (b) shows the global loading bar when the function runs. + * + * This legend of the three functions that are involved might help: + * + * - useWrap: () => wrap + * - wrap: (f) => void + * - f: async () => Promise */ -export const useWrapAsyncOperation = ( +export const useWrapLoadError = ( /** See: [Note: Migrating components that need the app context]. */ - appContext: NewAppContextPhotos, -) => { - const { startLoading, finishLoading, onGenericError } = appContext; - - const wrap = React.useCallback( + { startLoading, finishLoading, onGenericError }: NewAppContextPhotos, +) => + React.useCallback( (f: () => Promise) => { const wrapped = async () => { startLoading(); @@ -31,5 +35,25 @@ export const useWrapAsyncOperation = ( [onGenericError, startLoading, finishLoading], ); - return wrap; -}; +/** + * A variant of {@link useWrapLoadError} that does not handle the error, only + * does the loading indicator. + */ +export const useWrapLoad = ( + /** See: [Note: Migrating components that need the app context]. */ + { startLoading, finishLoading }: NewAppContextPhotos, +) => + React.useCallback( + (f: () => Promise) => { + const wrapped = async () => { + startLoading(); + try { + await f(); + } finally { + finishLoading(); + } + }; + return (): void => void wrapped(); + }, + [startLoading, finishLoading], + ); diff --git a/web/packages/new/photos/services/ml/people.ts b/web/packages/new/photos/services/ml/people.ts index 4ab8549609..ef05554713 100644 --- a/web/packages/new/photos/services/ml/people.ts +++ b/web/packages/new/photos/services/ml/people.ts @@ -1,7 +1,8 @@ +import { masterKeyFromSession } from "@/base/session-store"; import { wipClusterEnable } from "."; import type { EnteFile } from "../../types/file"; import { getLocalFiles } from "../files"; -import { savedCGroupUserEntities } from "../user-entity"; +import { addUserEntity, savedCGroupUserEntities } from "../user-entity"; import type { FaceCluster } from "./cluster"; import { getFaceIndexes, savedFaceClusters } from "./db"; import { fileIDFromFaceID } from "./face"; @@ -252,3 +253,21 @@ export const reconstructPeople = async (): Promise => { .filter((c) => !!c) .sort((a, b) => b.fileIDs.length - a.fileIDs.length); }; + +/** + * Convert a cluster into a named cgroup, updating both remote and local state. + * + * @param name Name of the new cgroup user entity. + * + * @param cluster The underlying cluster to use to populate the cgroup. + */ +export const addPerson = async (name: string, cluster: FaceCluster) => + addUserEntity( + "cgroup", + { + name, + assigned: [cluster], + isHidden: false, + }, + await masterKeyFromSession(), + );