Skip to content

Commit 100ef3e

Browse files
authored
Merge branch 'main' into copilot/enhance-multiuser-token-warning
2 parents 6bb1880 + be015a5 commit 100ef3e

11 files changed

Lines changed: 169 additions & 28 deletions

File tree

Makefile

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@ help:
1212
@echo "mypy-all Run mypy ignoring the config in pyproject.tom but still ignoring missing imports"
1313
@echo "test Run the unit tests."
1414
@echo "update-config-docstring Update the app's config docstring so mkdocs can autogenerate it correctly."
15-
@echo "frontend-install Install the pnpm modules needed for the front end"
16-
@echo "frontend-build Build the frontend in order to run on localhost:9090"
15+
@echo "frontend-install Install the pnpm modules needed for the frontend"
16+
@echo "frontend-build Build the frontend for localhost:9090"
17+
@echo "frontend-test Run the frontend test suite once"
1718
@echo "frontend-dev Run the frontend in developer mode on localhost:5173"
1819
@echo "frontend-typegen Generate types for the frontend from the OpenAPI schema"
19-
@echo "frontend-prettier Format the frontend using lint:prettier"
20-
@echo "wheel Build the wheel for the current version"
20+
@echo "frontend-lint Run frontend checks and fixable lint/format steps"
21+
@echo "wheel Build the wheel for the current version"
2122
@echo "tag-release Tag the GitHub repository with the current version (use at release time only!)"
2223
@echo "openapi Generate the OpenAPI schema for the app, outputting to stdout"
2324
@echo "docs Serve the mkdocs site with live reload"
@@ -57,6 +58,10 @@ frontend-install:
5758
frontend-build:
5859
cd invokeai/frontend/web && pnpm build
5960

61+
# Run the frontend test suite once
62+
frontend-test:
63+
cd invokeai/frontend/web && pnpm run test:run
64+
6065
# Run the frontend in dev mode
6166
frontend-dev:
6267
cd invokeai/frontend/web && pnpm dev

invokeai/frontend/web/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"scripts": {
2222
"dev": "vite dev",
2323
"dev:host": "vite dev --host",
24-
"build": "pnpm run lint && vite build",
24+
"build": "pnpm run lint && vitest run && vite build",
2525
"typegen": "node scripts/typegen.js",
2626
"preview": "vite preview",
2727
"lint:knip": "knip --tags=-knipignore",
@@ -35,6 +35,7 @@
3535
"storybook": "storybook dev -p 6006",
3636
"build-storybook": "storybook build",
3737
"test": "vitest",
38+
"test:run": "vitest run",
3839
"test:ui": "vitest --coverage --ui",
3940
"test:no-watch": "vitest --no-watch"
4041
},
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { getFocusedRegion, setFocusedRegion } from './focus';
4+
5+
describe('focus regions', () => {
6+
it('supports the workflows region', () => {
7+
setFocusedRegion('workflows');
8+
expect(getFocusedRegion()).toBe('workflows');
9+
10+
setFocusedRegion(null);
11+
expect(getFocusedRegion()).toBe(null);
12+
});
13+
});

invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const addControlLayerFromImageDndTargetData = newCanvasEntityFromImageDndTarget.
1212
const addRegionalGuidanceReferenceImageFromImageDndTargetData = newCanvasEntityFromImageDndTarget.getData({
1313
type: 'regional_guidance_with_reference_image',
1414
});
15+
const addInpaintMaskFromImageDndTargetData = newCanvasEntityFromImageDndTarget.getData({ type: 'inpaint_mask' });
1516
const addResizedControlLayerFromImageDndTargetData = newCanvasEntityFromImageDndTarget.getData({
1617
type: 'control_layer',
1718
withResize: true,
@@ -25,39 +26,47 @@ export const CanvasDropArea = memo(() => {
2526
<>
2627
<Grid
2728
gridTemplateRows="1fr 1fr"
28-
gridTemplateColumns="1fr 1fr"
29+
gridTemplateColumns="repeat(6, 1fr)"
2930
position="absolute"
3031
top={0}
3132
right={0}
3233
bottom={0}
3334
left={0}
3435
pointerEvents="none"
3536
>
36-
<GridItem position="relative">
37+
<GridItem position="relative" colSpan={3}>
3738
<DndDropTarget
3839
dndTarget={newCanvasEntityFromImageDndTarget}
3940
dndTargetData={addRasterLayerFromImageDndTargetData}
4041
label={t('controlLayers.canvasContextMenu.newRasterLayer')}
4142
isDisabled={isBusy}
4243
/>
4344
</GridItem>
44-
<GridItem position="relative">
45+
<GridItem position="relative" colSpan={3}>
4546
<DndDropTarget
4647
dndTarget={newCanvasEntityFromImageDndTarget}
4748
dndTargetData={addControlLayerFromImageDndTargetData}
4849
label={t('controlLayers.canvasContextMenu.newControlLayer')}
4950
isDisabled={isBusy}
5051
/>
5152
</GridItem>
52-
<GridItem position="relative">
53+
<GridItem position="relative" colSpan={2}>
5354
<DndDropTarget
5455
dndTarget={newCanvasEntityFromImageDndTarget}
5556
dndTargetData={addRegionalGuidanceReferenceImageFromImageDndTargetData}
5657
label={t('controlLayers.canvasContextMenu.newRegionalReferenceImage')}
5758
isDisabled={isBusy}
5859
/>
5960
</GridItem>
60-
<GridItem position="relative">
61+
<GridItem position="relative" colSpan={2}>
62+
<DndDropTarget
63+
dndTarget={newCanvasEntityFromImageDndTarget}
64+
dndTargetData={addInpaintMaskFromImageDndTargetData}
65+
label={t('controlLayers.canvasContextMenu.newInpaintMask')}
66+
isDisabled={isBusy}
67+
/>
68+
</GridItem>
69+
<GridItem position="relative" colSpan={2}>
6170
<DndDropTarget
6271
dndTarget={newCanvasEntityFromImageDndTarget}
6372
dndTargetData={addResizedControlLayerFromImageDndTargetData}

invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterBase.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,7 @@ export abstract class CanvasEntityAdapterBase<T extends CanvasEntityState, U ext
542542
this.renderer.updateCompositingRectSize();
543543
this.renderer.updateCompositingRectPosition();
544544
this.renderer.updateCompositingRectFill();
545+
this.renderer.updateOpacity();
545546
}
546547
this.renderer.syncKonvaCache();
547548
};

invokeai/frontend/web/src/features/controlLayers/konva/CanvasEntity/CanvasEntityObjectRenderer.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -320,10 +320,6 @@ export class CanvasEntityObjectRenderer extends CanvasModuleBase {
320320
};
321321

322322
updateOpacity = throttle(() => {
323-
if (!this.parent.konva.layer.visible()) {
324-
return;
325-
}
326-
327323
this.log.trace('Updating opacity');
328324

329325
const opacity = this.parent.state.opacity;

invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ import { selectSelectionMode, selectShouldSnapToGrid } from 'features/nodes/stor
5151
import { NO_DRAG_CLASS, NO_PAN_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants';
5252
import type { AnyEdge, AnyNode } from 'features/nodes/types/invocation';
5353
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
54-
import type { CSSProperties, MouseEvent } from 'react';
54+
import type { CSSProperties, MouseEvent, RefObject } from 'react';
5555
import { memo, useCallback, useMemo, useRef } from 'react';
5656
import { useHotkeys } from 'react-hotkeys-hook';
5757

@@ -61,6 +61,7 @@ import InvocationDefaultEdge from './edges/InvocationDefaultEdge';
6161
import CurrentImageNode from './nodes/CurrentImage/CurrentImageNode';
6262
import InvocationNodeWrapper from './nodes/Invocation/InvocationNodeWrapper';
6363
import NotesNode from './nodes/Notes/NotesNode';
64+
import { isWorkflowHotkeyEnabled, shouldIgnoreWorkflowCopyHotkey } from './workflowHotkeys';
6465

6566
const edgeTypes = {
6667
collapsed: InvocationCollapsedEdge,
@@ -248,14 +249,14 @@ export const Flow = memo(() => {
248249
>
249250
<Background gap={snapGrid} offset={snapGrid} />
250251
</ReactFlow>
251-
<HotkeyIsolator />
252+
<HotkeyIsolator flowWrapper={flowWrapper} />
252253
</>
253254
);
254255
});
255256

256257
Flow.displayName = 'Flow';
257258

258-
const HotkeyIsolator = memo(() => {
259+
const HotkeyIsolator = memo(({ flowWrapper }: { flowWrapper: RefObject<HTMLDivElement> }) => {
259260
const mayUndo = useAppSelector(selectMayUndo);
260261
const mayRedo = useAppSelector(selectMayRedo);
261262

@@ -270,8 +271,12 @@ const HotkeyIsolator = memo(() => {
270271
id: 'copySelection',
271272
category: 'workflows',
272273
callback: copySelection,
273-
options: { enabled: isWorkflowsFocused, preventDefault: true },
274-
dependencies: [copySelection],
274+
options: {
275+
enabled: isWorkflowHotkeyEnabled(isWorkflowsFocused),
276+
preventDefault: true,
277+
ignoreEventWhen: () => shouldIgnoreWorkflowCopyHotkey(window.getSelection(), flowWrapper.current),
278+
},
279+
dependencies: [copySelection, isWorkflowsFocused],
275280
});
276281

277282
const selectAll = useCallback(() => {
@@ -299,23 +304,23 @@ const HotkeyIsolator = memo(() => {
299304
id: 'selectAll',
300305
category: 'workflows',
301306
callback: selectAll,
302-
options: { enabled: isWorkflowsFocused, preventDefault: true },
307+
options: { enabled: isWorkflowHotkeyEnabled(isWorkflowsFocused), preventDefault: true },
303308
dependencies: [selectAll, isWorkflowsFocused],
304309
});
305310

306311
useRegisteredHotkeys({
307312
id: 'pasteSelection',
308313
category: 'workflows',
309314
callback: pasteSelection,
310-
options: { enabled: isWorkflowsFocused, preventDefault: true },
315+
options: { enabled: isWorkflowHotkeyEnabled(isWorkflowsFocused), preventDefault: true },
311316
dependencies: [pasteSelection, isWorkflowsFocused],
312317
});
313318

314319
useRegisteredHotkeys({
315320
id: 'pasteSelectionWithEdges',
316321
category: 'workflows',
317322
callback: pasteSelectionWithEdges,
318-
options: { enabled: isWorkflowsFocused, preventDefault: true },
323+
options: { enabled: isWorkflowHotkeyEnabled(isWorkflowsFocused), preventDefault: true },
319324
dependencies: [pasteSelectionWithEdges, isWorkflowsFocused],
320325
});
321326

@@ -325,7 +330,7 @@ const HotkeyIsolator = memo(() => {
325330
callback: () => {
326331
store.dispatch(undo());
327332
},
328-
options: { enabled: isWorkflowsFocused && mayUndo, preventDefault: true },
333+
options: { enabled: isWorkflowHotkeyEnabled(isWorkflowsFocused) && mayUndo, preventDefault: true },
329334
dependencies: [store, mayUndo, isWorkflowsFocused],
330335
});
331336

@@ -335,7 +340,7 @@ const HotkeyIsolator = memo(() => {
335340
callback: () => {
336341
store.dispatch(redo());
337342
},
338-
options: { enabled: isWorkflowsFocused && mayRedo, preventDefault: true },
343+
options: { enabled: isWorkflowHotkeyEnabled(isWorkflowsFocused) && mayRedo, preventDefault: true },
339344
dependencies: [store, mayRedo, isWorkflowsFocused],
340345
});
341346

@@ -373,7 +378,7 @@ const HotkeyIsolator = memo(() => {
373378
id: 'deleteSelection',
374379
category: 'workflows',
375380
callback: deleteSelection,
376-
options: { preventDefault: true, enabled: isWorkflowsFocused },
381+
options: { preventDefault: true, enabled: isWorkflowHotkeyEnabled(isWorkflowsFocused) },
377382
dependencies: [deleteSelection, isWorkflowsFocused],
378383
});
379384

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { isEventTargetWithinElement, isWorkflowHotkeyEnabled, shouldIgnoreWorkflowCopyHotkey } from './workflowHotkeys';
4+
5+
describe('isEventTargetWithinElement', () => {
6+
it('returns true when the element contains the event target', () => {
7+
const target = new EventTarget();
8+
const element = {
9+
contains: (node: unknown) => node === target,
10+
};
11+
12+
expect(isEventTargetWithinElement(target, element as never)).toBe(true);
13+
});
14+
15+
it('returns false when the element does not contain the event target', () => {
16+
const target = new EventTarget();
17+
const element = {
18+
contains: () => false,
19+
};
20+
21+
expect(isEventTargetWithinElement(target, element as never)).toBe(false);
22+
});
23+
24+
it('returns false when the element is missing', () => {
25+
expect(isEventTargetWithinElement(new EventTarget(), null)).toBe(false);
26+
});
27+
});
28+
29+
describe('isWorkflowHotkeyEnabled', () => {
30+
it('enables workflow hotkeys whenever the workflows pane is focused', () => {
31+
expect(isWorkflowHotkeyEnabled(true)).toBe(true);
32+
});
33+
34+
it('disables workflow hotkeys when the workflows pane is not focused', () => {
35+
expect(isWorkflowHotkeyEnabled(false)).toBe(false);
36+
});
37+
});
38+
39+
describe('shouldIgnoreWorkflowCopyHotkey', () => {
40+
const insideNode = new EventTarget() as Node;
41+
const outsideNode = new EventTarget() as Node;
42+
const element = {
43+
contains: (node: Node) => node === insideNode,
44+
};
45+
46+
it('returns false when there is no selection', () => {
47+
expect(shouldIgnoreWorkflowCopyHotkey(null, element)).toBe(false);
48+
});
49+
50+
it('returns false for collapsed selections', () => {
51+
expect(
52+
shouldIgnoreWorkflowCopyHotkey(
53+
{ isCollapsed: true, toString: () => 'text', anchorNode: outsideNode, focusNode: outsideNode },
54+
element
55+
)
56+
).toBe(false);
57+
});
58+
59+
it('returns false when the selection is inside the editor element', () => {
60+
expect(
61+
shouldIgnoreWorkflowCopyHotkey(
62+
{ isCollapsed: false, toString: () => 'text', anchorNode: insideNode, focusNode: insideNode },
63+
element
64+
)
65+
).toBe(false);
66+
});
67+
68+
it('returns true when the selection is outside the editor element', () => {
69+
expect(
70+
shouldIgnoreWorkflowCopyHotkey(
71+
{ isCollapsed: false, toString: () => 'text', anchorNode: outsideNode, focusNode: outsideNode },
72+
element
73+
)
74+
).toBe(true);
75+
});
76+
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
export const isEventTargetWithinElement = (
2+
target: EventTarget | null,
3+
element: { contains: (node: Node) => boolean } | null
4+
) => {
5+
return Boolean(target && element?.contains(target as Node));
6+
};
7+
8+
export const isWorkflowHotkeyEnabled = (isWorkflowsFocused: boolean) => {
9+
return isWorkflowsFocused;
10+
};
11+
12+
type SelectionLike = {
13+
isCollapsed: boolean;
14+
toString(): string;
15+
anchorNode: Node | null;
16+
focusNode: Node | null;
17+
};
18+
19+
export const shouldIgnoreWorkflowCopyHotkey = (
20+
selection: SelectionLike | null | undefined,
21+
element: { contains: (node: Node) => boolean } | null
22+
) => {
23+
if (!selection || !element || selection.isCollapsed || selection.toString().length === 0) {
24+
return false;
25+
}
26+
27+
const nodes = [selection.anchorNode, selection.focusNode].filter((node): node is Node => node !== null);
28+
29+
if (nodes.length === 0) {
30+
return false;
31+
}
32+
33+
return nodes.some((node) => !element.contains(node));
34+
};

invokeai/frontend/web/vite.config.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export default defineConfig(({ mode }) => {
3939
host: '0.0.0.0',
4040
},
4141
test: {
42+
reporters: [['default', { summary: false }]],
4243
typecheck: {
4344
enabled: true,
4445
ignoreSourceErrors: true,

0 commit comments

Comments
 (0)