mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
tidy(ui): dnd stuff
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import { Box, Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useCanvasEntityListDnd } from 'features/controlLayers/components/CanvasEntityList/useCanvasEntityListDnd';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { useEntityIsSelected } from 'features/controlLayers/hooks/useEntityIsSelected';
|
||||
import { useEntitySelectionColor } from 'features/controlLayers/hooks/useEntitySelectionColor';
|
||||
import { entitySelected } from 'features/controlLayers/store/canvasSlice';
|
||||
import { DndListDropIndicator } from 'features/dnd/DndListDropIndicator';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
|
||||
export const CanvasEntityContainer = memo((props: PropsWithChildren) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const entityIdentifier = useEntityIdentifierContext();
|
||||
const isSelected = useEntityIsSelected(entityIdentifier);
|
||||
const selectionColor = useEntitySelectionColor(entityIdentifier);
|
||||
const onClick = useCallback(() => {
|
||||
if (isSelected) {
|
||||
return;
|
||||
}
|
||||
dispatch(entitySelected({ entityIdentifier }));
|
||||
}, [dispatch, entityIdentifier, isSelected]);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const dndState = useCanvasEntityListDnd(ref, entityIdentifier);
|
||||
|
||||
return (
|
||||
<Box position="relative">
|
||||
<Flex
|
||||
// This is used to trigger the post-move flash animation
|
||||
data-entity-id={entityIdentifier.id}
|
||||
ref={ref}
|
||||
position="relative"
|
||||
flexDir="column"
|
||||
w="full"
|
||||
bg={isSelected ? 'base.800' : 'base.850'}
|
||||
onClick={onClick}
|
||||
borderInlineStartWidth={5}
|
||||
borderColor={isSelected ? selectionColor : 'base.800'}
|
||||
borderRadius="base"
|
||||
>
|
||||
{props.children}
|
||||
</Flex>
|
||||
<DndListDropIndicator dndState={dndState} />
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasEntityContainer.displayName = 'CanvasEntityContainer';
|
||||
@@ -1,13 +1,13 @@
|
||||
import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import { extractClosestEdge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
|
||||
import { reorderWithEdge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/util/reorder-with-edge';
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Button, Collapse, Flex, Icon, Spacer, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||
import { useBoolean } from 'common/hooks/useBoolean';
|
||||
import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar';
|
||||
import { fixTooltipCloseOnScrollStyles } from 'common/util/fixTooltipCloseOnScrollStyles';
|
||||
import { singleCanvasEntity } from 'features/controlLayers/components/CanvasEntityList/useCanvasEntityListDnd';
|
||||
import { CanvasEntityAddOfTypeButton } from 'features/controlLayers/components/common/CanvasEntityAddOfTypeButton';
|
||||
import { CanvasEntityMergeVisibleButton } from 'features/controlLayers/components/common/CanvasEntityMergeVisibleButton';
|
||||
import { CanvasEntityTypeIsHiddenToggle } from 'features/controlLayers/components/common/CanvasEntityTypeIsHiddenToggle';
|
||||
@@ -16,7 +16,7 @@ import { useEntityTypeTitle } from 'features/controlLayers/hooks/useEntityTypeTi
|
||||
import { entitiesReordered } from 'features/controlLayers/store/canvasSlice';
|
||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { isRenderableEntityType } from 'features/controlLayers/store/types';
|
||||
import { Dnd, triggerPostMoveFlash } from 'features/dnd/dnd';
|
||||
import { triggerPostMoveFlash } from 'features/dnd/dnd';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { memo, useEffect } from 'react';
|
||||
import { flushSync } from 'react-dom';
|
||||
@@ -28,10 +28,6 @@ type Props = PropsWithChildren<{
|
||||
entityIdentifiers: CanvasEntityIdentifier[];
|
||||
}>;
|
||||
|
||||
const _hover: SystemStyleObject = {
|
||||
opacity: 1,
|
||||
};
|
||||
|
||||
export const CanvasEntityGroupList = memo(({ isSelected, type, children, entityIdentifiers }: Props) => {
|
||||
const title = useEntityTypeTitle(type);
|
||||
const informationalPopoverFeature = useEntityTypeInformationalPopover(type);
|
||||
@@ -41,7 +37,7 @@ export const CanvasEntityGroupList = memo(({ isSelected, type, children, entityI
|
||||
useEffect(() => {
|
||||
return monitorForElements({
|
||||
canMonitor({ source }) {
|
||||
if (!Dnd.Source.singleCanvasEntity.typeGuard(source.data)) {
|
||||
if (!singleCanvasEntity.typeGuard(source.data)) {
|
||||
return false;
|
||||
}
|
||||
if (source.data.payload.entityIdentifier.type !== type) {
|
||||
@@ -58,10 +54,7 @@ export const CanvasEntityGroupList = memo(({ isSelected, type, children, entityI
|
||||
const sourceData = source.data;
|
||||
const targetData = target.data;
|
||||
|
||||
if (
|
||||
!Dnd.Source.singleCanvasEntity.typeGuard(sourceData) ||
|
||||
!Dnd.Source.singleCanvasEntity.typeGuard(targetData)
|
||||
) {
|
||||
if (!singleCanvasEntity.typeGuard(sourceData) || !singleCanvasEntity.typeGuard(targetData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
|
||||
import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import { attachClosestEdge, extractClosestEdge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
|
||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import type { DndListState } from 'features/dnd/dnd';
|
||||
import { buildDndSourceApi, idle } from 'features/dnd/dnd';
|
||||
import type { RefObject } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
/**
|
||||
* Dnd source API for a single canvas entity.
|
||||
*/
|
||||
export const singleCanvasEntity = buildDndSourceApi<{ entityIdentifier: CanvasEntityIdentifier }>('SingleCanvasEntity');
|
||||
|
||||
export const useCanvasEntityListDnd = (ref: RefObject<HTMLElement>, entityIdentifier: CanvasEntityIdentifier) => {
|
||||
const [dndState, setDndState] = useState<DndListState>(idle);
|
||||
|
||||
useEffect(() => {
|
||||
const element = ref.current;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
return combine(
|
||||
draggable({
|
||||
element,
|
||||
getInitialData() {
|
||||
return singleCanvasEntity.getData({ entityIdentifier });
|
||||
},
|
||||
onDragStart() {
|
||||
setDndState({ type: 'is-dragging' });
|
||||
},
|
||||
onDrop() {
|
||||
setDndState(idle);
|
||||
},
|
||||
}),
|
||||
dropTargetForElements({
|
||||
element,
|
||||
canDrop({ source }) {
|
||||
if (!singleCanvasEntity.typeGuard(source.data)) {
|
||||
return false;
|
||||
}
|
||||
if (source.data.payload.entityIdentifier.type !== entityIdentifier.type) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
getData({ input }) {
|
||||
const data = singleCanvasEntity.getData({ entityIdentifier });
|
||||
return attachClosestEdge(data, {
|
||||
element,
|
||||
input,
|
||||
allowedEdges: ['top', 'bottom'],
|
||||
});
|
||||
},
|
||||
getIsSticky() {
|
||||
return true;
|
||||
},
|
||||
onDragEnter({ self }) {
|
||||
const closestEdge = extractClosestEdge(self.data);
|
||||
setDndState({ type: 'is-dragging-over', closestEdge });
|
||||
},
|
||||
onDrag({ self }) {
|
||||
const closestEdge = extractClosestEdge(self.data);
|
||||
|
||||
// Only need to update react state if nothing has changed.
|
||||
// Prevents re-rendering.
|
||||
setDndState((current) => {
|
||||
if (current.type === 'is-dragging-over' && current.closestEdge === closestEdge) {
|
||||
return current;
|
||||
}
|
||||
return { type: 'is-dragging-over', closestEdge };
|
||||
});
|
||||
},
|
||||
onDragLeave() {
|
||||
setDndState(idle);
|
||||
},
|
||||
onDrop() {
|
||||
setDndState(idle);
|
||||
},
|
||||
})
|
||||
);
|
||||
}, [entityIdentifier, ref]);
|
||||
|
||||
return dndState;
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Spacer } from '@invoke-ai/ui-library';
|
||||
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
|
||||
import { CanvasEntityContainer } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityContainer';
|
||||
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
|
||||
import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions';
|
||||
import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { CanvasEntityGroupList } from 'features/controlLayers/components/common/CanvasEntityGroupList';
|
||||
import { CanvasEntityGroupList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityGroupList';
|
||||
import { ControlLayer } from 'features/controlLayers/components/ControlLayer/ControlLayer';
|
||||
import { selectCanvasSlice, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
|
||||
import { getEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Spacer } from '@invoke-ai/ui-library';
|
||||
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
|
||||
import { CanvasEntityContainer } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityContainer';
|
||||
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
|
||||
import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions';
|
||||
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { CanvasEntityGroupList } from 'features/controlLayers/components/common/CanvasEntityGroupList';
|
||||
import { CanvasEntityGroupList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityGroupList';
|
||||
import { IPAdapter } from 'features/controlLayers/components/IPAdapter/IPAdapter';
|
||||
import { mapId } from 'features/controlLayers/konva/util';
|
||||
import { selectCanvasSlice, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Spacer } from '@invoke-ai/ui-library';
|
||||
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
|
||||
import { CanvasEntityContainer } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityContainer';
|
||||
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
|
||||
import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions';
|
||||
import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { CanvasEntityGroupList } from 'features/controlLayers/components/common/CanvasEntityGroupList';
|
||||
import { CanvasEntityGroupList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityGroupList';
|
||||
import { InpaintMask } from 'features/controlLayers/components/InpaintMask/InpaintMask';
|
||||
import { selectCanvasSlice, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
|
||||
import { getEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Spacer } from '@invoke-ai/ui-library';
|
||||
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
|
||||
import { CanvasEntityContainer } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityContainer';
|
||||
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
|
||||
import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions';
|
||||
import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { CanvasEntityGroupList } from 'features/controlLayers/components/common/CanvasEntityGroupList';
|
||||
import { CanvasEntityGroupList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityGroupList';
|
||||
import { RasterLayer } from 'features/controlLayers/components/RasterLayer/RasterLayer';
|
||||
import { selectCanvasSlice, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
|
||||
import { getEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Spacer } from '@invoke-ai/ui-library';
|
||||
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
|
||||
import { CanvasEntityContainer } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityContainer';
|
||||
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
|
||||
import { CanvasEntityHeaderCommonActions } from 'features/controlLayers/components/common/CanvasEntityHeaderCommonActions';
|
||||
import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { CanvasEntityGroupList } from 'features/controlLayers/components/common/CanvasEntityGroupList';
|
||||
import { CanvasEntityGroupList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityGroupList';
|
||||
import { RegionalGuidance } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidance';
|
||||
import { selectCanvasSlice, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
|
||||
import { getEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
|
||||
import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import { attachClosestEdge, extractClosestEdge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
|
||||
import { Box, Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { useEntityIsSelected } from 'features/controlLayers/hooks/useEntityIsSelected';
|
||||
import { useEntitySelectionColor } from 'features/controlLayers/hooks/useEntitySelectionColor';
|
||||
import { entitySelected } from 'features/controlLayers/store/canvasSlice';
|
||||
import type { DndState } from 'features/dnd/dnd';
|
||||
import { Dnd, idle } from 'features/dnd/dnd';
|
||||
import { DndDropIndicator } from 'features/dnd/DndDropIndicator';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
export const CanvasEntityContainer = memo((props: PropsWithChildren) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const entityIdentifier = useEntityIdentifierContext();
|
||||
const isSelected = useEntityIsSelected(entityIdentifier);
|
||||
const selectionColor = useEntitySelectionColor(entityIdentifier);
|
||||
const onClick = useCallback(() => {
|
||||
if (isSelected) {
|
||||
return;
|
||||
}
|
||||
dispatch(entitySelected({ entityIdentifier }));
|
||||
}, [dispatch, entityIdentifier, isSelected]);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [dndState, setDndState] = useState<DndState>(idle);
|
||||
|
||||
useEffect(() => {
|
||||
const element = ref.current;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
return combine(
|
||||
draggable({
|
||||
element,
|
||||
getInitialData() {
|
||||
return Dnd.Source.singleCanvasEntity.getData({ entityIdentifier });
|
||||
},
|
||||
onDragStart() {
|
||||
setDndState({ type: 'is-dragging' });
|
||||
},
|
||||
onDrop() {
|
||||
setDndState(idle);
|
||||
},
|
||||
}),
|
||||
dropTargetForElements({
|
||||
element,
|
||||
canDrop({ source }) {
|
||||
if (!Dnd.Source.singleCanvasEntity.typeGuard(source.data)) {
|
||||
return false;
|
||||
}
|
||||
if (source.data.payload.entityIdentifier.type !== entityIdentifier.type) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
getData({ input }) {
|
||||
const data = Dnd.Source.singleCanvasEntity.getData({ entityIdentifier });
|
||||
return attachClosestEdge(data, {
|
||||
element,
|
||||
input,
|
||||
allowedEdges: ['top', 'bottom'],
|
||||
});
|
||||
},
|
||||
getIsSticky() {
|
||||
return true;
|
||||
},
|
||||
onDragEnter({ self }) {
|
||||
const closestEdge = extractClosestEdge(self.data);
|
||||
setDndState({ type: 'is-dragging-over', closestEdge });
|
||||
},
|
||||
onDrag({ self }) {
|
||||
const closestEdge = extractClosestEdge(self.data);
|
||||
|
||||
// Only need to update react state if nothing has changed.
|
||||
// Prevents re-rendering.
|
||||
setDndState((current) => {
|
||||
if (current.type === 'is-dragging-over' && current.closestEdge === closestEdge) {
|
||||
return current;
|
||||
}
|
||||
return { type: 'is-dragging-over', closestEdge };
|
||||
});
|
||||
},
|
||||
onDragLeave() {
|
||||
setDndState(idle);
|
||||
},
|
||||
onDrop() {
|
||||
setDndState(idle);
|
||||
},
|
||||
})
|
||||
);
|
||||
}, [entityIdentifier]);
|
||||
|
||||
return (
|
||||
<Box position="relative">
|
||||
<Flex
|
||||
// This is used to trigger the post-move flash animation
|
||||
data-entity-id={entityIdentifier.id}
|
||||
ref={ref}
|
||||
position="relative"
|
||||
flexDir="column"
|
||||
w="full"
|
||||
bg={isSelected ? 'base.800' : 'base.850'}
|
||||
onClick={onClick}
|
||||
borderInlineStartWidth={5}
|
||||
borderColor={isSelected ? selectionColor : 'base.800'}
|
||||
borderRadius="base"
|
||||
>
|
||||
{props.children}
|
||||
</Flex>
|
||||
{dndState.type === 'is-dragging-over' && dndState.closestEdge ? (
|
||||
<DndDropIndicator
|
||||
edge={dndState.closestEdge}
|
||||
// This is the gap between items in the list
|
||||
gap="var(--invoke-space-2)"
|
||||
/>
|
||||
) : null}
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasEntityContainer.displayName = 'CanvasEntityContainer';
|
||||
@@ -1213,7 +1213,7 @@ export const canvasSlice = createSlice({
|
||||
}
|
||||
},
|
||||
entityRasterized: (state, action: PayloadAction<EntityRasterizedPayload>) => {
|
||||
const { entityIdentifier, imageObject, position, replaceObjects } = action.payload;
|
||||
const { entityIdentifier, imageObject, position, replaceObjects, isSelected } = action.payload;
|
||||
const entity = selectEntity(state, entityIdentifier);
|
||||
if (!entity) {
|
||||
return;
|
||||
@@ -1225,6 +1225,10 @@ export const canvasSlice = createSlice({
|
||||
entity.position = position;
|
||||
}
|
||||
}
|
||||
|
||||
if (isSelected) {
|
||||
state.selectedEntityIdentifier = entityIdentifier;
|
||||
}
|
||||
},
|
||||
entityBrushLineAdded: (state, action: PayloadAction<EntityBrushLineAddedPayload>) => {
|
||||
const { entityIdentifier, brushLine } = action.payload;
|
||||
|
||||
@@ -451,6 +451,7 @@ export type EntityRasterizedPayload = EntityIdentifierPayload<{
|
||||
imageObject: CanvasImageState;
|
||||
position: Coordinate;
|
||||
replaceObjects: boolean;
|
||||
isSelected?: boolean;
|
||||
}>;
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,6 @@ import { containsFiles, getFiles } from '@atlaskit/pragmatic-drag-and-drop/exter
|
||||
import { preventUnhandled } from '@atlaskit/pragmatic-drag-and-drop/prevent-unhandled';
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Box } from '@invoke-ai/ui-library';
|
||||
import { dndDropped } from 'app/store/middleware/listenerMiddleware/listeners/dnd';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { Dnd } from 'features/dnd/dnd';
|
||||
import { DndDropOverlay } from 'features/dnd/DndDropOverlay';
|
||||
@@ -103,13 +102,13 @@ export const DndDropTarget = memo((props: Props) => {
|
||||
setDndState('potential');
|
||||
},
|
||||
getData: () => targetData,
|
||||
onDrop: (args) => {
|
||||
const sourceData = args.source.data;
|
||||
if (!Dnd.Util.isDndSourceData(sourceData)) {
|
||||
return;
|
||||
}
|
||||
dispatch(dndDropped({ sourceData, targetData }));
|
||||
},
|
||||
// onDrop: (args) => {
|
||||
// const sourceData = args.source.data;
|
||||
// if (!Dnd.Util.isDndSourceData(sourceData)) {
|
||||
// return;
|
||||
// }
|
||||
// dispatch(dndDropped({ sourceData, targetData }));
|
||||
// },
|
||||
}),
|
||||
monitorForElements({
|
||||
canMonitor: (args) => {
|
||||
@@ -174,12 +173,7 @@ export const DndDropTarget = memo((props: Props) => {
|
||||
image_category: 'user',
|
||||
is_intermediate: false,
|
||||
});
|
||||
dispatch(
|
||||
dndDropped({
|
||||
sourceData: Dnd.Source.singleImage.getData({ imageDTO }),
|
||||
targetData,
|
||||
})
|
||||
);
|
||||
Dnd.Util.handleDrop(Dnd.Source.singleImage.getData({ imageDTO }), targetData);
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import type { Edge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/types';
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Box } from '@invoke-ai/ui-library';
|
||||
import type { DndListState } from 'features/dnd/dnd';
|
||||
import type { CSSProperties } from 'react';
|
||||
|
||||
/**
|
||||
@@ -88,7 +89,7 @@ const edgeStyles: Record<Edge, SystemStyleObject> = {
|
||||
*
|
||||
* A drop indicator is used to communicate the intended resting place of the draggable item. The orientation of the drop indicator should always match the direction of the content flow.
|
||||
*/
|
||||
export function DndDropIndicator({ edge, gap = '0px' }: DropIndicatorProps) {
|
||||
function DndDropIndicatorInternal({ edge, gap = '0px' }: DropIndicatorProps) {
|
||||
/**
|
||||
* To clearly communicate the resting place of a draggable item during a drag operation,
|
||||
* the drop indicator should be positioned half way between draggable items.
|
||||
@@ -104,3 +105,21 @@ export function DndDropIndicator({ edge, gap = '0px' }: DropIndicatorProps) {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const DndListDropIndicator = ({ dndState }: { dndState: DndListState }) => {
|
||||
if (dndState.type !== 'is-dragging-over') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!dndState.closestEdge) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DndDropIndicatorInternal
|
||||
edge={dndState.closestEdge}
|
||||
// This is the gap between items in the list, used to calculate the position of the drop indicator
|
||||
gap="var(--invoke-space-2)"
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -4,11 +4,35 @@ import type { Input } from '@atlaskit/pragmatic-drag-and-drop/dist/types/entry-p
|
||||
import type { GetOffsetFn } from '@atlaskit/pragmatic-drag-and-drop/dist/types/public-utils/element/custom-native-drag-preview/types';
|
||||
import type { Edge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/dist/types/closest-edge';
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { getStore } from 'app/store/nanostores/store';
|
||||
import { selectDefaultControlAdapter, selectDefaultIPAdapter } from 'features/controlLayers/hooks/addLayerHooks';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import {
|
||||
controlLayerAdded,
|
||||
entityRasterized,
|
||||
inpaintMaskAdded,
|
||||
rasterLayerAdded,
|
||||
referenceImageAdded,
|
||||
referenceImageIPAdapterImageChanged,
|
||||
rgAdded,
|
||||
rgIPAdapterImageChanged,
|
||||
} from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectBboxRect } from 'features/controlLayers/store/selectors';
|
||||
import type {
|
||||
CanvasControlLayerState,
|
||||
CanvasEntityIdentifier,
|
||||
CanvasInpaintMaskState,
|
||||
CanvasRasterLayerState,
|
||||
CanvasReferenceImageState,
|
||||
CanvasRegionalGuidanceState,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { imageDTOToImageObject, imageDTOToImageWithDims } from 'features/controlLayers/store/util';
|
||||
import { imageToCompareChanged, selectionChanged } from 'features/gallery/store/gallerySlice';
|
||||
import type { BoardId } from 'features/gallery/store/types';
|
||||
import type { FieldIdentifier } from 'features/nodes/types/field';
|
||||
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
import type { ValueOf } from 'type-fest';
|
||||
import type { Jsonifiable } from 'type-fest/source/jsonifiable';
|
||||
@@ -151,7 +175,7 @@ type DndSourceAPI<T extends DndData> = {
|
||||
* @template P The optional payload of the Dnd source.
|
||||
* @param type The type of the Dnd source.
|
||||
*/
|
||||
const buildDndSourceApi = <P extends Jsonifiable | undefined = undefined>(type: string) => {
|
||||
export const buildDndSourceApi = <P extends Jsonifiable | undefined = undefined>(type: string) => {
|
||||
return {
|
||||
type,
|
||||
kind: 'source',
|
||||
@@ -169,20 +193,10 @@ const singleImage = buildDndSourceApi<{ imageDTO: ImageDTO }>('SingleImage');
|
||||
* Dnd source API for multiple image source.
|
||||
*/
|
||||
const multipleImage = buildDndSourceApi<{ imageDTOs: ImageDTO[]; boardId: BoardId }>('MultipleImage');
|
||||
/**
|
||||
* Dnd source API for a single canvas entity.
|
||||
*/
|
||||
const singleCanvasEntity = buildDndSourceApi<{ entityIdentifier: CanvasEntityIdentifier }>('SingleCanvasEntity');
|
||||
/**
|
||||
* Dnd source API for a single workflow field.
|
||||
*/
|
||||
const singleWorkflowField = buildDndSourceApi<{ fieldIdentifier: FieldIdentifier }>('SingleWorkflowField');
|
||||
|
||||
const DndSource = {
|
||||
singleImage,
|
||||
multipleImage,
|
||||
singleCanvasEntity,
|
||||
singleWorkflowField,
|
||||
} as const;
|
||||
|
||||
type SourceDataTypeMap = {
|
||||
@@ -207,6 +221,7 @@ type DndTargetApi<T extends DndData> = DndSourceAPI<T> & {
|
||||
* @returns Whether the drop is valid.
|
||||
*/
|
||||
validateDrop: (sourceData: DndData<string, 'source', Jsonifiable>, targetData: T) => boolean;
|
||||
handleDrop: (sourceData: DndData<string, 'source', Jsonifiable>, targetData: T) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -220,7 +235,11 @@ const buildDndTargetApi = <P extends Jsonifiable | undefined = undefined>(
|
||||
validateDrop: (
|
||||
sourceData: DndData<string, 'source', Jsonifiable>,
|
||||
targetData: DndData<typeof type, 'target', P>
|
||||
) => boolean
|
||||
) => boolean,
|
||||
handleDrop: (
|
||||
sourceData: DndData<string, 'source', Jsonifiable>,
|
||||
targetData: DndData<typeof type, 'target', P>
|
||||
) => void
|
||||
) => {
|
||||
return {
|
||||
type,
|
||||
@@ -228,56 +247,172 @@ const buildDndTargetApi = <P extends Jsonifiable | undefined = undefined>(
|
||||
typeGuard: _buildDataTypeGuard<DndData<typeof type, 'target', P>>(type, 'target'),
|
||||
getData: _buildDataGetter<DndData<typeof type, 'target', P>>(type, 'target'),
|
||||
validateDrop,
|
||||
handleDrop,
|
||||
} satisfies DndTargetApi<DndData<typeof type, 'target', P>>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Dnd target API for setting the image on an existing Global Reference Image layer.
|
||||
*/
|
||||
const setGlobalReferenceImage = buildDndTargetApi<{ globalReferenceImageId: string }>(
|
||||
const setGlobalReferenceImage = buildDndTargetApi<{ entityIdentifier: CanvasEntityIdentifier<'reference_image'> }>(
|
||||
'SetGlobalReferenceImage',
|
||||
singleImage.typeGuard
|
||||
singleImage.typeGuard,
|
||||
(sourceData, targetData) => {
|
||||
if (!singleImage.typeGuard(sourceData)) {
|
||||
return false;
|
||||
}
|
||||
const { dispatch } = getStore();
|
||||
const { imageDTO } = sourceData.payload;
|
||||
const { entityIdentifier } = targetData.payload;
|
||||
dispatch(referenceImageIPAdapterImageChanged({ entityIdentifier, imageDTO }));
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Dnd target API for setting the image on an existing Regional Guidance layer's Reference Image.
|
||||
*/
|
||||
const setRegionalGuidanceReferenceImage = buildDndTargetApi<{
|
||||
regionalGuidanceId: string;
|
||||
entityIdentifier: CanvasEntityIdentifier<'regional_guidance'>;
|
||||
referenceImageId: string;
|
||||
}>('SetRegionalGuidanceReferenceImage', singleImage.typeGuard);
|
||||
}>('SetRegionalGuidanceReferenceImage', singleImage.typeGuard, (sourceData, targetData) => {
|
||||
if (!singleImage.typeGuard(sourceData)) {
|
||||
return false;
|
||||
}
|
||||
const { dispatch } = getStore();
|
||||
const { imageDTO } = sourceData.payload;
|
||||
const { entityIdentifier, referenceImageId } = targetData.payload;
|
||||
dispatch(rgIPAdapterImageChanged({ entityIdentifier, referenceImageId, imageDTO }));
|
||||
});
|
||||
|
||||
/**
|
||||
* Dnd target API for creating a new a Raster Layer from an image.
|
||||
*/
|
||||
const newRasterLayerFromImage = buildDndTargetApi('NewRasterLayerFromImage', singleImage.typeGuard);
|
||||
const newRasterLayerFromImage = buildDndTargetApi(
|
||||
'NewRasterLayerFromImage',
|
||||
singleImage.typeGuard,
|
||||
(sourceData, _targetData) => {
|
||||
if (!singleImage.typeGuard(sourceData)) {
|
||||
return false;
|
||||
}
|
||||
const { dispatch, getState } = getStore();
|
||||
const { imageDTO } = sourceData.payload;
|
||||
const imageObject = imageDTOToImageObject(imageDTO);
|
||||
const { x, y } = selectBboxRect(getState());
|
||||
const overrides: Partial<CanvasRasterLayerState> = {
|
||||
objects: [imageObject],
|
||||
position: { x, y },
|
||||
};
|
||||
dispatch(rasterLayerAdded({ overrides, isSelected: true }));
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Dnd target API for creating a new a Control Layer from an image.
|
||||
*/
|
||||
const newControlLayerFromImage = buildDndTargetApi('NewControlLayerFromImage', singleImage.typeGuard);
|
||||
const newControlLayerFromImage = buildDndTargetApi(
|
||||
'NewControlLayerFromImage',
|
||||
singleImage.typeGuard,
|
||||
(sourceData, _targetData) => {
|
||||
if (!singleImage.typeGuard(sourceData)) {
|
||||
return false;
|
||||
}
|
||||
const { imageDTO } = sourceData.payload;
|
||||
const { dispatch, getState } = getStore();
|
||||
const state = getState();
|
||||
const imageObject = imageDTOToImageObject(imageDTO);
|
||||
const { x, y } = selectBboxRect(state);
|
||||
const controlAdapter = selectDefaultControlAdapter(state);
|
||||
const overrides: Partial<CanvasControlLayerState> = {
|
||||
objects: [imageObject],
|
||||
position: { x, y },
|
||||
controlAdapter,
|
||||
};
|
||||
dispatch(controlLayerAdded({ overrides, isSelected: true }));
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Dnd target API for adding an Inpaint Mask from an image.
|
||||
*/
|
||||
const newInpaintMaskFromImage = buildDndTargetApi('NewInpaintMaskFromImage', singleImage.typeGuard);
|
||||
const newInpaintMaskFromImage = buildDndTargetApi(
|
||||
'NewInpaintMaskFromImage',
|
||||
singleImage.typeGuard,
|
||||
(sourceData, _targetData) => {
|
||||
if (!singleImage.typeGuard(sourceData)) {
|
||||
return false;
|
||||
}
|
||||
const { dispatch, getState } = getStore();
|
||||
const { imageDTO } = sourceData.payload;
|
||||
const imageObject = imageDTOToImageObject(imageDTO);
|
||||
const { x, y } = selectBboxRect(getState());
|
||||
const overrides: Partial<CanvasInpaintMaskState> = {
|
||||
objects: [imageObject],
|
||||
position: { x, y },
|
||||
};
|
||||
dispatch(inpaintMaskAdded({ overrides, isSelected: true }));
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Dnd target API for adding a new Global Reference Image layer with a pre-set Reference Image from an image.
|
||||
*/
|
||||
const newGlobalReferenceImageFromImage = buildDndTargetApi('NewGlobalReferenceImageFromImage', singleImage.typeGuard);
|
||||
const newGlobalReferenceImageFromImage = buildDndTargetApi(
|
||||
'NewGlobalReferenceImageFromImage',
|
||||
singleImage.typeGuard,
|
||||
(sourceData, _targetData) => {
|
||||
if (!singleImage.typeGuard(sourceData)) {
|
||||
return false;
|
||||
}
|
||||
const { dispatch, getState } = getStore();
|
||||
const { imageDTO } = sourceData.payload;
|
||||
const ipAdapter = selectDefaultIPAdapter(getState());
|
||||
ipAdapter.image = imageDTOToImageWithDims(imageDTO);
|
||||
const overrides: Partial<CanvasReferenceImageState> = { ipAdapter };
|
||||
dispatch(referenceImageAdded({ overrides, isSelected: true }));
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Dnd target API for adding a new Regional Guidance layer from an image.
|
||||
*/
|
||||
const newRegionalGuidanceFromImage = buildDndTargetApi('NewRegionalGuidanceFromImage', singleImage.typeGuard);
|
||||
const newRegionalGuidanceFromImage = buildDndTargetApi(
|
||||
'NewRegionalGuidanceFromImage',
|
||||
singleImage.typeGuard,
|
||||
(sourceData, _targetData) => {
|
||||
if (!singleImage.typeGuard(sourceData)) {
|
||||
return false;
|
||||
}
|
||||
const { dispatch, getState } = getStore();
|
||||
const { imageDTO } = sourceData.payload;
|
||||
const imageObject = imageDTOToImageObject(imageDTO);
|
||||
const { x, y } = selectBboxRect(getState());
|
||||
const overrides: Partial<CanvasRegionalGuidanceState> = {
|
||||
objects: [imageObject],
|
||||
position: { x, y },
|
||||
};
|
||||
dispatch(rgAdded({ overrides, isSelected: true }));
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Dnd target API for adding a new Regional Guidance layer with a pre-set Reference Image from an image.
|
||||
*/
|
||||
const newRegionalGuidanceReferenceImageFromImage = buildDndTargetApi(
|
||||
'NewRegionalGuidanceReferenceImageFromImage',
|
||||
singleImage.typeGuard
|
||||
singleImage.typeGuard,
|
||||
(sourceData, _targetData) => {
|
||||
if (!singleImage.typeGuard(sourceData)) {
|
||||
return false;
|
||||
}
|
||||
const { dispatch, getState } = getStore();
|
||||
const { imageDTO } = sourceData.payload;
|
||||
const ipAdapter = selectDefaultIPAdapter(getState());
|
||||
ipAdapter.image = imageDTOToImageWithDims(imageDTO);
|
||||
const overrides: Partial<CanvasRegionalGuidanceState> = {
|
||||
referenceImages: [{ id: getPrefixedId('regional_guidance_reference_image'), ipAdapter }],
|
||||
};
|
||||
dispatch(rgAdded({ overrides, isSelected: true }));
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -286,19 +421,57 @@ const newRegionalGuidanceReferenceImageFromImage = buildDndTargetApi(
|
||||
*/
|
||||
const replaceLayerWithImage = buildDndTargetApi<{
|
||||
entityIdentifier: CanvasEntityIdentifier<'control_layer' | 'raster_layer' | 'inpaint_mask' | 'regional_guidance'>;
|
||||
}>('ReplaceLayerWithImage', singleImage.typeGuard);
|
||||
}>('ReplaceLayerWithImage', singleImage.typeGuard, (sourceData, targetData) => {
|
||||
if (!singleImage.typeGuard(sourceData)) {
|
||||
return false;
|
||||
}
|
||||
const { dispatch, getState } = getStore();
|
||||
const { imageDTO } = sourceData.payload;
|
||||
const { entityIdentifier } = targetData.payload;
|
||||
const imageObject = imageDTOToImageObject(imageDTO);
|
||||
const { x, y } = selectBboxRect(getState());
|
||||
dispatch(
|
||||
entityRasterized({
|
||||
entityIdentifier,
|
||||
imageObject,
|
||||
position: { x, y },
|
||||
replaceObjects: true,
|
||||
isSelected: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Dnd target API for setting the initial image on the upscaling tab.
|
||||
*/
|
||||
const setUpscaleInitialImageFromImage = buildDndTargetApi('SetUpscaleInitialImageFromImage', singleImage.typeGuard);
|
||||
const setUpscaleInitialImageFromImage = buildDndTargetApi(
|
||||
'SetUpscaleInitialImageFromImage',
|
||||
singleImage.typeGuard,
|
||||
(sourceData) => {
|
||||
if (!singleImage.typeGuard(sourceData)) {
|
||||
return false;
|
||||
}
|
||||
const { dispatch } = getStore();
|
||||
const { imageDTO } = sourceData.payload;
|
||||
dispatch(upscaleInitialImageChanged(imageDTO));
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Dnd target API for setting an image field on a node.
|
||||
*/
|
||||
const setNodeImageField = buildDndTargetApi<{ nodeId: string; fieldName: string }>(
|
||||
'SetNodeImageField',
|
||||
singleImage.typeGuard
|
||||
singleImage.typeGuard,
|
||||
(sourceData, targetData) => {
|
||||
if (!singleImage.typeGuard(sourceData)) {
|
||||
return false;
|
||||
}
|
||||
const { dispatch } = getStore();
|
||||
const { imageDTO } = sourceData.payload;
|
||||
const { fieldName, nodeId } = targetData.payload;
|
||||
dispatch(fieldImageValueChanged({ nodeId, fieldName, value: imageDTO }));
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -307,55 +480,104 @@ const setNodeImageField = buildDndTargetApi<{ nodeId: string; fieldName: string
|
||||
const selectForCompare = buildDndTargetApi<{
|
||||
firstImageName?: string | null;
|
||||
secondImageName?: string | null;
|
||||
}>('SelectForCompare', (sourceData, targetData) => {
|
||||
if (!singleImage.typeGuard(sourceData)) {
|
||||
return false;
|
||||
}>(
|
||||
'SelectForCompare',
|
||||
(sourceData, targetData) => {
|
||||
if (!singleImage.typeGuard(sourceData)) {
|
||||
return false;
|
||||
}
|
||||
// Do not allow the same images to be selected for comparison
|
||||
if (sourceData.payload.imageDTO.image_name === targetData.payload.firstImageName) {
|
||||
return false;
|
||||
}
|
||||
if (sourceData.payload.imageDTO.image_name === targetData.payload.secondImageName) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
(sourceData) => {
|
||||
if (!singleImage.typeGuard(sourceData)) {
|
||||
return false;
|
||||
}
|
||||
const { dispatch } = getStore();
|
||||
const { imageDTO } = sourceData.payload;
|
||||
dispatch(imageToCompareChanged(imageDTO));
|
||||
}
|
||||
// Do not allow the same images to be selected for comparison
|
||||
if (sourceData.payload.imageDTO.image_name === targetData.payload.firstImageName) {
|
||||
return false;
|
||||
}
|
||||
if (sourceData.payload.imageDTO.image_name === targetData.payload.secondImageName) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
);
|
||||
|
||||
/**
|
||||
* Dnd target API for adding an image to a board.
|
||||
*/
|
||||
const addToBoard = buildDndTargetApi<{ boardId: string }>('AddToBoard', (sourceData, targetData) => {
|
||||
if (singleImage.typeGuard(sourceData)) {
|
||||
const currentBoard = sourceData.payload.imageDTO.board_id ?? 'none';
|
||||
const destinationBoard = targetData.payload.boardId;
|
||||
return currentBoard !== destinationBoard;
|
||||
}
|
||||
const addToBoard = buildDndTargetApi<{ boardId: string }>(
|
||||
'AddToBoard',
|
||||
(sourceData, targetData) => {
|
||||
if (singleImage.typeGuard(sourceData)) {
|
||||
const currentBoard = sourceData.payload.imageDTO.board_id ?? 'none';
|
||||
const destinationBoard = targetData.payload.boardId;
|
||||
return currentBoard !== destinationBoard;
|
||||
}
|
||||
|
||||
if (multipleImage.typeGuard(sourceData)) {
|
||||
const currentBoard = sourceData.payload.boardId;
|
||||
const destinationBoard = targetData.payload.boardId;
|
||||
return currentBoard !== destinationBoard;
|
||||
}
|
||||
if (multipleImage.typeGuard(sourceData)) {
|
||||
const currentBoard = sourceData.payload.boardId;
|
||||
const destinationBoard = targetData.payload.boardId;
|
||||
return currentBoard !== destinationBoard;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
return false;
|
||||
},
|
||||
(sourceData, targetData) => {
|
||||
if (singleImage.typeGuard(sourceData)) {
|
||||
const { dispatch } = getStore();
|
||||
const { imageDTO } = sourceData.payload;
|
||||
const { boardId } = targetData.payload;
|
||||
dispatch(imagesApi.endpoints.addImageToBoard.initiate({ imageDTO, board_id: boardId }, { track: false }));
|
||||
dispatch(selectionChanged([]));
|
||||
}
|
||||
|
||||
if (multipleImage.typeGuard(sourceData)) {
|
||||
const { dispatch } = getStore();
|
||||
const { imageDTOs } = sourceData.payload;
|
||||
const { boardId } = targetData.payload;
|
||||
dispatch(imagesApi.endpoints.addImagesToBoard.initiate({ imageDTOs, board_id: boardId }, { track: false }));
|
||||
dispatch(selectionChanged([]));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Dnd target API for removing an image from a board.
|
||||
*/
|
||||
const removeFromBoard = buildDndTargetApi('RemoveFromBoard', (sourceData) => {
|
||||
if (singleImage.typeGuard(sourceData)) {
|
||||
const currentBoard = sourceData.payload.imageDTO.board_id ?? 'none';
|
||||
return currentBoard !== 'none';
|
||||
}
|
||||
const removeFromBoard = buildDndTargetApi(
|
||||
'RemoveFromBoard',
|
||||
(sourceData) => {
|
||||
if (singleImage.typeGuard(sourceData)) {
|
||||
const currentBoard = sourceData.payload.imageDTO.board_id ?? 'none';
|
||||
return currentBoard !== 'none';
|
||||
}
|
||||
|
||||
if (multipleImage.typeGuard(sourceData)) {
|
||||
const currentBoard = sourceData.payload.boardId;
|
||||
return currentBoard !== 'none';
|
||||
}
|
||||
if (multipleImage.typeGuard(sourceData)) {
|
||||
const currentBoard = sourceData.payload.boardId;
|
||||
return currentBoard !== 'none';
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
return false;
|
||||
},
|
||||
(sourceData) => {
|
||||
if (singleImage.typeGuard(sourceData)) {
|
||||
const { dispatch } = getStore();
|
||||
const { imageDTO } = sourceData.payload;
|
||||
dispatch(imagesApi.endpoints.removeImageFromBoard.initiate({ imageDTO }, { track: false }));
|
||||
dispatch(selectionChanged([]));
|
||||
}
|
||||
|
||||
if (multipleImage.typeGuard(sourceData)) {
|
||||
const { dispatch } = getStore();
|
||||
const { imageDTOs } = sourceData.payload;
|
||||
dispatch(imagesApi.endpoints.removeImagesFromBoard.initiate({ imageDTOs }, { track: false }));
|
||||
dispatch(selectionChanged([]));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const DndTarget = {
|
||||
/**
|
||||
@@ -491,6 +713,25 @@ export const Dnd = {
|
||||
}
|
||||
return false;
|
||||
},
|
||||
/**
|
||||
* Validates whether a drop is valid.
|
||||
* @param sourceData The data being dragged.
|
||||
* @param targetData The data of the target being dragged onto.
|
||||
* @returns Whether the drop is valid.
|
||||
*/
|
||||
handleDrop: (sourceData: SourceDataUnion, targetData: TargetDataUnion): void => {
|
||||
for (const targetApi of targetApisArray) {
|
||||
if (targetApi.typeGuard(targetData)) {
|
||||
/**
|
||||
* TS cannot narrow the type of the targetApi and will error in the handleDrop call.
|
||||
* We've just checked that targetData is of the right type, though, so this cast to `any` is safe.
|
||||
*/
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
targetApi.handleDrop(sourceData, targetData as any);
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -536,7 +777,7 @@ export function triggerPostMoveFlash(element: HTMLElement, backgroundColor: CSSP
|
||||
});
|
||||
}
|
||||
|
||||
export type DndState =
|
||||
export type DndListState =
|
||||
| {
|
||||
type: 'idle';
|
||||
}
|
||||
@@ -551,4 +792,4 @@ export type DndState =
|
||||
type: 'is-dragging-over';
|
||||
closestEdge: Edge | null;
|
||||
};
|
||||
export const idle: DndState = { type: 'idle' };
|
||||
export const idle: DndListState = { type: 'idle' };
|
||||
|
||||
69
invokeai/frontend/web/src/features/dnd/useDndMonitor.ts
Normal file
69
invokeai/frontend/web/src/features/dnd/useDndMonitor.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
|
||||
import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import { monitorForExternal } from '@atlaskit/pragmatic-drag-and-drop/external/adapter';
|
||||
import { containsFiles } from '@atlaskit/pragmatic-drag-and-drop/external/file';
|
||||
import { preventUnhandled } from '@atlaskit/pragmatic-drag-and-drop/prevent-unhandled';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||
import { Dnd } from 'features/dnd/dnd';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const log = logger('dnd');
|
||||
|
||||
export const useDndMonitor = () => {
|
||||
useAssertSingleton('useDropMonitor');
|
||||
|
||||
useEffect(() => {
|
||||
return combine(
|
||||
monitorForElements({
|
||||
canMonitor: ({ source }) => {
|
||||
const sourceData = source.data;
|
||||
|
||||
// Check for allowed sources
|
||||
if (!Dnd.Source.singleImage.typeGuard(sourceData) && !Dnd.Source.multipleImage.typeGuard(sourceData)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
onDrop: ({ source, location }) => {
|
||||
const target = location.current.dropTargets[0];
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceData = source.data;
|
||||
const targetData = target.data;
|
||||
|
||||
// Check for allowed sources
|
||||
if (!Dnd.Source.singleImage.typeGuard(sourceData) && !Dnd.Source.multipleImage.typeGuard(sourceData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for allowed targets
|
||||
if (!Dnd.Util.isDndTargetData(targetData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug({ sourceData, targetData }, 'Dropped image');
|
||||
|
||||
Dnd.Util.handleDrop(sourceData, targetData);
|
||||
},
|
||||
}),
|
||||
monitorForExternal({
|
||||
canMonitor: (args) => {
|
||||
if (!containsFiles(args)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
onDragStart: () => {
|
||||
preventUnhandled.start();
|
||||
},
|
||||
onDrop: () => {
|
||||
preventUnhandled.stop();
|
||||
},
|
||||
})
|
||||
);
|
||||
}, []);
|
||||
};
|
||||
@@ -1,17 +1,14 @@
|
||||
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
|
||||
import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import { attachClosestEdge, extractClosestEdge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
|
||||
import { Box, Circle, Flex, Icon, IconButton, Spacer, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import type { DndState } from 'features/dnd/dnd';
|
||||
import { Dnd, idle } from 'features/dnd/dnd';
|
||||
import { DndDropIndicator } from 'features/dnd/DndDropIndicator';
|
||||
import { DndListDropIndicator } from 'features/dnd/DndListDropIndicator';
|
||||
import { InvocationInputFieldCheck } from 'features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck';
|
||||
import { useLinearViewFieldDnd } from 'features/nodes/components/sidePanel/workflow/useLinearViewFieldDnd';
|
||||
import { useFieldOriginalValue } from 'features/nodes/hooks/useFieldOriginalValue';
|
||||
import { useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
|
||||
import { workflowExposedFieldRemoved } from 'features/nodes/store/workflowSlice';
|
||||
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import type { FieldIdentifier } from 'features/nodes/types/field';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowCounterClockwiseBold, PiInfoBold, PiTrashSimpleBold } from 'react-icons/pi';
|
||||
|
||||
@@ -20,92 +17,28 @@ import FieldTooltipContent from './FieldTooltipContent';
|
||||
import InputFieldRenderer from './InputFieldRenderer';
|
||||
|
||||
type Props = {
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
fieldIdentifier: FieldIdentifier;
|
||||
};
|
||||
|
||||
const LinearViewFieldInternal = ({ nodeId, fieldName }: Props) => {
|
||||
const LinearViewFieldInternal = ({ fieldIdentifier }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { isValueChanged, onReset } = useFieldOriginalValue(nodeId, fieldName);
|
||||
const { isMouseOverNode, handleMouseOut, handleMouseOver } = useMouseOverNode(nodeId);
|
||||
const { isValueChanged, onReset } = useFieldOriginalValue(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
|
||||
const { isMouseOverNode, handleMouseOut, handleMouseOver } = useMouseOverNode(fieldIdentifier.nodeId);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleRemoveField = useCallback(() => {
|
||||
dispatch(workflowExposedFieldRemoved({ nodeId, fieldName }));
|
||||
}, [dispatch, fieldName, nodeId]);
|
||||
dispatch(workflowExposedFieldRemoved(fieldIdentifier));
|
||||
}, [dispatch, fieldIdentifier]);
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [dndState, setDndState] = useState<DndState>(idle);
|
||||
|
||||
useEffect(() => {
|
||||
const element = ref.current;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
return combine(
|
||||
draggable({
|
||||
element,
|
||||
getInitialData() {
|
||||
return Dnd.Source.singleWorkflowField.getData({ fieldIdentifier: { nodeId, fieldName } });
|
||||
},
|
||||
onDragStart() {
|
||||
setDndState({ type: 'is-dragging' });
|
||||
},
|
||||
onDrop() {
|
||||
setDndState(idle);
|
||||
},
|
||||
}),
|
||||
dropTargetForElements({
|
||||
element,
|
||||
canDrop({ source }) {
|
||||
if (!Dnd.Source.singleWorkflowField.typeGuard(source.data)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
getData({ input }) {
|
||||
const data = Dnd.Source.singleWorkflowField.getData({ fieldIdentifier: { nodeId, fieldName } });
|
||||
return attachClosestEdge(data, {
|
||||
element,
|
||||
input,
|
||||
allowedEdges: ['top', 'bottom'],
|
||||
});
|
||||
},
|
||||
getIsSticky() {
|
||||
return true;
|
||||
},
|
||||
onDragEnter({ self }) {
|
||||
const closestEdge = extractClosestEdge(self.data);
|
||||
setDndState({ type: 'is-dragging-over', closestEdge });
|
||||
},
|
||||
onDrag({ self }) {
|
||||
const closestEdge = extractClosestEdge(self.data);
|
||||
|
||||
// Only need to update react state if nothing has changed.
|
||||
// Prevents re-rendering.
|
||||
setDndState((current) => {
|
||||
if (current.type === 'is-dragging-over' && current.closestEdge === closestEdge) {
|
||||
return current;
|
||||
}
|
||||
return { type: 'is-dragging-over', closestEdge };
|
||||
});
|
||||
},
|
||||
onDragLeave() {
|
||||
setDndState(idle);
|
||||
},
|
||||
onDrop() {
|
||||
setDndState(idle);
|
||||
},
|
||||
})
|
||||
);
|
||||
}, [fieldName, nodeId]);
|
||||
const dndState = useLinearViewFieldDnd(ref, fieldIdentifier);
|
||||
|
||||
return (
|
||||
<Box position="relative" w="full">
|
||||
<Flex
|
||||
ref={ref}
|
||||
// This is used to trigger the post-move flash animation
|
||||
data-field-name={fieldName}
|
||||
data-field-name={fieldIdentifier.fieldName}
|
||||
onMouseEnter={handleMouseOver}
|
||||
onMouseLeave={handleMouseOut}
|
||||
layerStyle="second"
|
||||
@@ -117,7 +50,7 @@ const LinearViewFieldInternal = ({ nodeId, fieldName }: Props) => {
|
||||
>
|
||||
<Flex flexDir="column" w="full">
|
||||
<Flex alignItems="center" gap={2}>
|
||||
<EditableFieldTitle nodeId={nodeId} fieldName={fieldName} kind="inputs" />
|
||||
<EditableFieldTitle nodeId={fieldIdentifier.nodeId} fieldName={fieldIdentifier.fieldName} kind="inputs" />
|
||||
<Spacer />
|
||||
{isMouseOverNode && <Circle size={2} borderRadius="full" bg="invokeBlue.500" />}
|
||||
{isValueChanged && (
|
||||
@@ -131,7 +64,13 @@ const LinearViewFieldInternal = ({ nodeId, fieldName }: Props) => {
|
||||
/>
|
||||
)}
|
||||
<Tooltip
|
||||
label={<FieldTooltipContent nodeId={nodeId} fieldName={fieldName} kind="inputs" />}
|
||||
label={
|
||||
<FieldTooltipContent
|
||||
nodeId={fieldIdentifier.nodeId}
|
||||
fieldName={fieldIdentifier.fieldName}
|
||||
kind="inputs"
|
||||
/>
|
||||
}
|
||||
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
|
||||
placement="top"
|
||||
>
|
||||
@@ -148,24 +87,18 @@ const LinearViewFieldInternal = ({ nodeId, fieldName }: Props) => {
|
||||
icon={<PiTrashSimpleBold />}
|
||||
/>
|
||||
</Flex>
|
||||
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />
|
||||
<InputFieldRenderer nodeId={fieldIdentifier.nodeId} fieldName={fieldIdentifier.fieldName} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
{dndState.type === 'is-dragging-over' && dndState.closestEdge ? (
|
||||
<DndDropIndicator
|
||||
edge={dndState.closestEdge}
|
||||
// This is the gap between items in the list
|
||||
gap="var(--invoke-space-2)"
|
||||
/>
|
||||
) : null}
|
||||
<DndListDropIndicator dndState={dndState} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const LinearViewField = ({ nodeId, fieldName }: Props) => {
|
||||
const LinearViewField = ({ fieldIdentifier }: Props) => {
|
||||
return (
|
||||
<InvocationInputFieldCheck nodeId={nodeId} fieldName={fieldName}>
|
||||
<LinearViewFieldInternal nodeId={nodeId} fieldName={fieldName} />
|
||||
<InvocationInputFieldCheck nodeId={fieldIdentifier.nodeId} fieldName={fieldIdentifier.fieldName}>
|
||||
<LinearViewFieldInternal fieldIdentifier={fieldIdentifier} />
|
||||
</InvocationInputFieldCheck>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,8 +7,9 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
||||
import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar';
|
||||
import { Dnd, triggerPostMoveFlash } from 'features/dnd/dnd';
|
||||
import { triggerPostMoveFlash } from 'features/dnd/dnd';
|
||||
import LinearViewFieldInternal from 'features/nodes/components/flow/nodes/Invocation/fields/LinearViewField';
|
||||
import { singleWorkflowField } from 'features/nodes/components/sidePanel/workflow/useLinearViewFieldDnd';
|
||||
import { selectWorkflowSlice, workflowExposedFieldsReordered } from 'features/nodes/store/workflowSlice';
|
||||
import type { FieldIdentifier } from 'features/nodes/types/field';
|
||||
import { memo, useEffect } from 'react';
|
||||
@@ -56,7 +57,7 @@ const FieldListInnerContent = memo(({ fields }: { fields: FieldIdentifier[] }) =
|
||||
useEffect(() => {
|
||||
return monitorForElements({
|
||||
canMonitor({ source }) {
|
||||
if (!Dnd.Source.singleWorkflowField.typeGuard(source.data)) {
|
||||
if (!singleWorkflowField.typeGuard(source.data)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -70,10 +71,7 @@ const FieldListInnerContent = memo(({ fields }: { fields: FieldIdentifier[] }) =
|
||||
const sourceData = source.data;
|
||||
const targetData = target.data;
|
||||
|
||||
if (
|
||||
!Dnd.Source.singleWorkflowField.typeGuard(sourceData) ||
|
||||
!Dnd.Source.singleWorkflowField.typeGuard(targetData)
|
||||
) {
|
||||
if (!singleWorkflowField.typeGuard(sourceData) || !singleWorkflowField.typeGuard(targetData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -135,8 +133,11 @@ const FieldListInnerContent = memo(({ fields }: { fields: FieldIdentifier[] }) =
|
||||
|
||||
return (
|
||||
<>
|
||||
{fields.map(({ nodeId, fieldName }) => (
|
||||
<LinearViewFieldInternal key={`${nodeId}.${fieldName}`} nodeId={nodeId} fieldName={fieldName} />
|
||||
{fields.map((fieldIdentifier) => (
|
||||
<LinearViewFieldInternal
|
||||
key={`${fieldIdentifier.nodeId}.${fieldIdentifier.fieldName}`}
|
||||
fieldIdentifier={fieldIdentifier}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
|
||||
import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import { attachClosestEdge, extractClosestEdge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
|
||||
import type { DndListState } from 'features/dnd/dnd';
|
||||
import { buildDndSourceApi, idle } from 'features/dnd/dnd';
|
||||
import type { FieldIdentifier } from 'features/nodes/types/field';
|
||||
import type { RefObject } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
/**
|
||||
* Dnd source API for a single workflow field.
|
||||
*/
|
||||
export const singleWorkflowField = buildDndSourceApi<{ fieldIdentifier: FieldIdentifier }>('SingleWorkflowField');
|
||||
|
||||
export const useLinearViewFieldDnd = (ref: RefObject<HTMLElement>, fieldIdentifier: FieldIdentifier) => {
|
||||
const [dndState, setDndState] = useState<DndListState>(idle);
|
||||
|
||||
useEffect(() => {
|
||||
const element = ref.current;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
return combine(
|
||||
draggable({
|
||||
element,
|
||||
getInitialData() {
|
||||
return singleWorkflowField.getData({ fieldIdentifier });
|
||||
},
|
||||
onDragStart() {
|
||||
setDndState({ type: 'is-dragging' });
|
||||
},
|
||||
onDrop() {
|
||||
setDndState(idle);
|
||||
},
|
||||
}),
|
||||
dropTargetForElements({
|
||||
element,
|
||||
canDrop({ source }) {
|
||||
if (!singleWorkflowField.typeGuard(source.data)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
getData({ input }) {
|
||||
const data = singleWorkflowField.getData({ fieldIdentifier });
|
||||
return attachClosestEdge(data, {
|
||||
element,
|
||||
input,
|
||||
allowedEdges: ['top', 'bottom'],
|
||||
});
|
||||
},
|
||||
getIsSticky() {
|
||||
return true;
|
||||
},
|
||||
onDragEnter({ self }) {
|
||||
const closestEdge = extractClosestEdge(self.data);
|
||||
setDndState({ type: 'is-dragging-over', closestEdge });
|
||||
},
|
||||
onDrag({ self }) {
|
||||
const closestEdge = extractClosestEdge(self.data);
|
||||
|
||||
// Only need to update react state if nothing has changed.
|
||||
// Prevents re-rendering.
|
||||
setDndState((current) => {
|
||||
if (current.type === 'is-dragging-over' && current.closestEdge === closestEdge) {
|
||||
return current;
|
||||
}
|
||||
return { type: 'is-dragging-over', closestEdge };
|
||||
});
|
||||
},
|
||||
onDragLeave() {
|
||||
setDndState(idle);
|
||||
},
|
||||
onDrop() {
|
||||
setDndState(idle);
|
||||
},
|
||||
})
|
||||
);
|
||||
}, [fieldIdentifier, ref]);
|
||||
|
||||
return dndState;
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import { Box, Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { CanvasMainPanelContent } from 'features/controlLayers/components/CanvasMainPanelContent';
|
||||
import { CanvasRightPanel } from 'features/controlLayers/components/CanvasRightPanel';
|
||||
import { useDndMonitor } from 'features/dnd/useDndMonitor';
|
||||
import GalleryPanelContent from 'features/gallery/components/GalleryPanelContent';
|
||||
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
|
||||
import NodeEditorPanelGroup from 'features/nodes/components/sidePanel/NodeEditorPanelGroup';
|
||||
@@ -40,6 +41,7 @@ const onRightPanelCollapse = (isCollapsed: boolean) => $isRightPanelOpen.set(!is
|
||||
|
||||
export const AppContent = memo(() => {
|
||||
const imperativePanelGroupRef = useRef<ImperativePanelGroupHandle>(null);
|
||||
useDndMonitor();
|
||||
|
||||
const withLeftPanel = useAppSelector(selectWithLeftPanel);
|
||||
const leftPanelUsePanelOptions = useMemo<UsePanelOptions>(
|
||||
|
||||
Reference in New Issue
Block a user