From fb5e46230016f4ce8a2dca7e91048907c25bc520 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 28 Oct 2024 15:40:21 +1000 Subject: [PATCH] tidy(ui): document & clean up dnd --- .../frontend/web/src/features/dnd2/dnd.ts | 122 +++++++++++++----- 1 file changed, 92 insertions(+), 30 deletions(-) diff --git a/invokeai/frontend/web/src/features/dnd2/dnd.ts b/invokeai/frontend/web/src/features/dnd2/dnd.ts index cc6a5df4a1..234e6b5fb5 100644 --- a/invokeai/frontend/web/src/features/dnd2/dnd.ts +++ b/invokeai/frontend/web/src/features/dnd2/dnd.ts @@ -7,8 +7,6 @@ import type { ImageDTO } from 'services/api/types'; import type { ValueOf } from 'type-fest'; import type { Jsonifiable } from 'type-fest/source/jsonifiable'; -type UnknownDndData = Record; - /** * This file contains types, APIs, and utilities for Dnd functionality, as provided by pragmatic-drag-and-drop: * - Source and target data types @@ -21,31 +19,71 @@ type UnknownDndData = Record; * - https://atlassian.design/components/pragmatic-drag-and-drop/about */ +/** + * A type for unknown Dnd data. `pragmatic-drag-and-drop` types all data as this type. + */ +type UnknownDndData = Record; + +/** + * A Dnd kind, which can be either a source or a target. + */ type DndKind = 'source' | 'target'; -type Data< +/** + * Data for a given Dnd source or target, which contains metadata and payload. + * @template T The type string of the Dnd data. This should be unique for each type of Dnd data. + * @template K The kind of the Dnd data ('source' or 'target'). + * @template P The optional payload of the Dnd data. This can be any "Jsonifiable" data - that is, data that can be + * serialized to JSON. This ensures the data can be safely stored in Redux, logged, etc. + */ +type DndData< T extends string = string, K extends DndKind = DndKind, P extends Jsonifiable | undefined = Jsonifiable | undefined, > = { + /** + * Metadata about the DndData. + */ meta: { + /** + * An identifier for this data. This may or may not be unique. This is primarily used to prevent a source from + * dropping on itself. + * + * A consumer may be both a Dnd source and target of the same type. For example, the upscaling initial image is + * a Dnd target and may contain an image, which is itself a Dnd source. In this case, the Dnd ID is used to prevent + * the upscaling initial image (and other instances of that same image) from being dropped onto itself. + * + * This is accomplished by checking the Dnd ID of the source against the Dnd ID of the target. If they match, the + * drop is rejected. + */ id: string; + /** + * The type of the DndData. + */ type: T; + /** + * The kind of the DndData (source or target). + */ kind: K; }; + /** + * The arbitrarily-shaped payload of the DndData. + */ payload: P; }; /** * Builds a type guard for a specific DndData type. - * @param key The unique symbol key for the DndData type. - * @returns A type guard for the DndData type. + * @template T The Dnd data type. + * @param type The type of the Dnd source or target data. + * @param kind The kind of the Dnd source or target data. + * @returns A type guard for the Dnd data. */ -const _buildDataTypeGuard = (type: string, kind: DndKind) => { +const _buildDataTypeGuard = (type: T['meta']['type'], kind: T['meta']['kind']) => { // pragmatic-drag-and-drop types all data as unknown, so we need to cast it to the expected type return (data: UnknownDndData): data is T => { try { - return (data as Data).meta.type === type && (data as Data).meta.kind === kind; + return (data as DndData).meta.type === type && (data as DndData).meta.kind === kind; } catch { return false; } @@ -57,11 +95,13 @@ const _buildDataTypeGuard = (type: string, kind: DndKind) => { * * The getter accepts arbitrary data and an optional Dnd ID. If no Dnd ID is provided, a unique one is generated. * - * @param key The unique symbol key for the DndData type. + * @template T The Dnd data type. + * @param type The type of the Dnd source or target data. + * @param kind The kind of the Dnd source or target data. * @returns A getter for the DndData type. */ const _buildDataGetter = - (type: T['meta']['type'], kind: T['meta']['kind']) => + (type: T['meta']['type'], kind: T['meta']['kind']) => (payload: T['payload'] extends undefined ? void : T['payload'], dndId?: string | null): T => { return { meta: { @@ -74,10 +114,16 @@ const _buildDataGetter = }; /** - * An API for a Dnd source. It provides a type guard, a getter, and a unique symbol key for the DndData type. + * The API for a Dnd source. */ -type DndSourceAPI = { +type DndSourceAPI = { + /** + * The type of the Dnd source. + */ type: string; + /** + * The kind of the Dnd source. It is always 'source'. + */ kind: 'source'; /** * A type guard for the DndData type. @@ -86,8 +132,8 @@ type DndSourceAPI = { */ typeGuard: ReturnType>; /** - * A getter for the DndData type. - * @param data The data to get. + * Gets a typed DndData object for the parent type. + * @param payload The payload for this DndData. * @param dndId The Dnd ID to use. If not provided, a unique one is generated. * @returns The DndData. */ @@ -95,16 +141,17 @@ type DndSourceAPI = { }; /** - * Builds a DndSourceAPI object. - * @param key The unique symbol key for the DndData type. + * Builds a Dnd source API. + * @template P The optional payload of the Dnd source. + * @param type The type of the Dnd source. */ const buildDndSourceApi =

(type: string) => { return { type, kind: 'source', - typeGuard: _buildDataTypeGuard>(type, 'source'), - getData: _buildDataGetter>(type, 'source'), - } satisfies DndSourceAPI>; + typeGuard: _buildDataTypeGuard>(type, 'source'), + getData: _buildDataGetter>(type, 'source'), + } satisfies DndSourceAPI>; }; //#region DndSourceData @@ -134,34 +181,38 @@ type SourceDataUnion = ValueOf; //#region DndTargetData /** - * An API for a Dnd target. It extends the DndSourceAPI with a validateDrop function. + * The API for a Dnd target. */ -type DndTargetApi = DndSourceAPI & { +type DndTargetApi = DndSourceAPI & { /** * Validates whether a drop is valid, give the source and target data. * @param sourceData The source data (i.e. the data being dragged) * @param targetData The target data (i.e. the data being dragged onto) * @returns Whether the drop is valid. */ - validateDrop: (sourceData: Data, targetData: T) => boolean; + validateDrop: (sourceData: DndData, targetData: T) => boolean; }; /** - * Builds a DndTargetApi object. - * @param key The unique symbol key for the DndData type. + * Builds a Dnd target API. + * @template P The optional payload of the Dnd target. + * @param type The type of the Dnd target. * @param validateDrop A function that validates whether a drop is valid. */ const buildDndTargetApi =

( type: string, - validateDrop: (sourceData: Data, targetData: Data) => boolean + validateDrop: ( + sourceData: DndData, + targetData: DndData + ) => boolean ) => { return { type, kind: 'source', - typeGuard: _buildDataTypeGuard>(type, 'target'), - getData: _buildDataGetter>(type, 'target'), + typeGuard: _buildDataTypeGuard>(type, 'target'), + getData: _buildDataGetter>(type, 'target'), validateDrop, - } satisfies DndTargetApi>; + } satisfies DndTargetApi>; }; /** @@ -329,6 +380,9 @@ const targetApisArray = Object.values(DndTarget); //#endregion +/** + * The Dnd namespace, providing types and APIs for Dnd functionality. + */ export declare namespace Dnd { export type types = { /** @@ -339,6 +393,14 @@ export declare namespace Dnd { * - `over`: A drag is occurring, and the drag is valid for the current drop target, and the drag is over the drop target. */ DndState: 'idle' | 'potential' | 'over'; + /** + * A Dnd kind, which can be either a source or a target. + */ + DndKind: DndKind; + /** + * A type for unknown Dnd data. `pragmatic-drag-and-drop` types all data as this type. + */ + UnknownDndData: UnknownDndData; /** * A map of target APIs to their data types. */ @@ -367,7 +429,7 @@ export const Dnd = { * @param data The DndData object. * @returns The Dnd ID. */ - getDndId: (data: Data): string => { + getDndId: (data: DndData): string => { return data.meta.id; }, /** @@ -377,7 +439,7 @@ export const Dnd = { isDndSourceData: (data: UnknownDndData): data is SourceDataUnion => { try { /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - return (data as Data).meta.kind === 'source'; + return (data as DndData).meta.kind === 'source'; } catch { return false; } @@ -389,7 +451,7 @@ export const Dnd = { isDndTargetData: (data: UnknownDndData): data is TargetDataUnion => { try { /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - return (data as Data).meta.kind === 'target'; + return (data as DndData).meta.kind === 'target'; } catch { return false; }