@pixel-point/toolcraft 0.0.7 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/templates/runtime/contracts/component-contracts.test.ts +84 -12
- package/templates/runtime/contracts/component-contracts.ts +40 -17
- package/templates/runtime/contracts/decision-contracts.ts +1 -1
- package/templates/runtime/export/export.test.ts +31 -0
- package/templates/runtime/export/export.ts +41 -0
- package/templates/runtime/react/canvas-shell.test.tsx +77 -1
- package/templates/runtime/react/canvas-shell.tsx +178 -23
- package/templates/runtime/react/control-conditions.ts +166 -0
- package/templates/runtime/react/controls-panel-filedrop-reorder.test.tsx +176 -0
- package/templates/runtime/react/controls-panel.test.tsx +404 -8
- package/templates/runtime/react/controls-panel.tsx +90 -4
- package/templates/runtime/react/media-file.ts +19 -0
- package/templates/runtime/schema/define-toolcraft.test.ts +1 -0
- package/templates/runtime/schema/define-toolcraft.ts +4 -2
- package/templates/runtime/schema/types.ts +4 -0
- package/templates/runtime/state/reducer.test.ts +304 -0
- package/templates/runtime/state/reducer.ts +148 -9
- package/templates/runtime/state/types.ts +10 -2
- package/templates/runtime/testing/performance.test.ts +1282 -48
- package/templates/runtime/testing/performance.ts +676 -39
- package/templates/starter/AGENTS.md +6 -5
- package/templates/starter/docs/toolcraft/acceptance-testing.md +11 -5
- package/templates/starter/docs/toolcraft/assembly-workflow.md +4 -2
- package/templates/starter/docs/toolcraft/component-rules.md +16 -7
- package/templates/starter/docs/toolcraft/custom-controls.md +8 -4
- package/templates/starter/docs/toolcraft/performance.md +47 -5
- package/templates/starter/docs/toolcraft/renderer-technique.md +4 -0
- package/templates/starter/docs/toolcraft/schema-reference.md +6 -6
- package/templates/starter/docs/toolcraft/workflow.md +2 -2
- package/templates/starter/e2e/app-performance.spec.ts +136 -3
- package/templates/starter/e2e/performance-helpers.ts +197 -0
- package/templates/starter/package.json +3 -0
- package/templates/starter/src/app/starter-acceptance.test.ts +529 -19
- package/templates/starter/src/app/starter-acceptance.ts +269 -12
- package/templates/starter/src/app/starter-performance.test.ts +66 -5
- package/templates/ui/components/controls/actions/actions-control.tsx +3 -5
- package/templates/ui/components/controls/file-drop/file-drop-control.tsx +340 -44
- package/templates/ui/components/controls/file-drop/index.ts +1 -1
- package/templates/ui/components/controls/index.ts +1 -0
- package/templates/ui/components/panel/panel-section.tsx +53 -1
|
@@ -3,9 +3,22 @@
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
|
|
5
5
|
import { clampToolcraftCanvasZoom } from "../state/canvas-zoom";
|
|
6
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
ToolcraftMediaAsset,
|
|
8
|
+
ToolcraftPoint,
|
|
9
|
+
ToolcraftState,
|
|
10
|
+
} from "../state/types";
|
|
11
|
+
import type { ToolcraftControlSchema } from "../schema/types";
|
|
12
|
+
import {
|
|
13
|
+
isToolcraftControlVisible,
|
|
14
|
+
isToolcraftSectionVisible,
|
|
15
|
+
} from "./control-conditions";
|
|
7
16
|
import { isToolcraftLayerVisibleInTree } from "./layer-tree";
|
|
8
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
isToolcraftImageFile,
|
|
19
|
+
readImportedFile,
|
|
20
|
+
readImportedImageFile,
|
|
21
|
+
} from "./media-file";
|
|
9
22
|
import { useToolcraft } from "./use-toolcraft";
|
|
10
23
|
|
|
11
24
|
export type CanvasShellProps = {
|
|
@@ -20,6 +33,9 @@ type CanvasDragState = {
|
|
|
20
33
|
startX: number;
|
|
21
34
|
startY: number;
|
|
22
35
|
};
|
|
36
|
+
type ToolcraftCanvasImageAsset = ToolcraftMediaAsset & {
|
|
37
|
+
size: NonNullable<ToolcraftMediaAsset["size"]>;
|
|
38
|
+
};
|
|
23
39
|
|
|
24
40
|
const wheelZoomSensitivity = 0.12;
|
|
25
41
|
const wheelPinchZoomSensitivity = 0.5;
|
|
@@ -28,13 +44,6 @@ function cn(...classNames: Array<string | false | null | undefined>): string {
|
|
|
28
44
|
return classNames.filter(Boolean).join(" ");
|
|
29
45
|
}
|
|
30
46
|
|
|
31
|
-
function isImageFile(file: File): boolean {
|
|
32
|
-
return (
|
|
33
|
-
file.type.startsWith("image/") ||
|
|
34
|
-
/\.(avif|gif|jpe?g|png|svg|webp)$/i.test(file.name)
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
47
|
function isDragLeavingCurrentTarget(
|
|
39
48
|
event: React.DragEvent<HTMLElement>,
|
|
40
49
|
): boolean {
|
|
@@ -79,6 +88,94 @@ function getNextWheelZoom(
|
|
|
79
88
|
return clampToolcraftCanvasZoom(currentZoom + zoomDelta);
|
|
80
89
|
}
|
|
81
90
|
|
|
91
|
+
function getFileExtension(file: File): string {
|
|
92
|
+
const match = /\.([a-z0-9]+)$/iu.exec(file.name);
|
|
93
|
+
|
|
94
|
+
return match?.[1]?.toLowerCase() ?? "";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function controlAcceptsFile(control: ToolcraftControlSchema, file: File): boolean {
|
|
98
|
+
const accept = control.accept?.trim();
|
|
99
|
+
|
|
100
|
+
if (!accept) {
|
|
101
|
+
return control.assetKind === "file" || isToolcraftImageFile(file);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const fileExtension = getFileExtension(file);
|
|
105
|
+
const mimeType = file.type.toLowerCase();
|
|
106
|
+
|
|
107
|
+
return accept.split(",").some((rawToken) => {
|
|
108
|
+
const token = rawToken.trim().toLowerCase();
|
|
109
|
+
|
|
110
|
+
if (!token) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (token.startsWith(".")) {
|
|
115
|
+
return token.slice(1) === fileExtension;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (token.endsWith("/*")) {
|
|
119
|
+
const prefix = token.slice(0, -1);
|
|
120
|
+
|
|
121
|
+
return mimeType.startsWith(prefix);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (token.includes("/")) {
|
|
125
|
+
return mimeType === token;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return token === fileExtension;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function getVisibleFileDropControls(state: ToolcraftState): ToolcraftControlSchema[] {
|
|
133
|
+
return (state.schema.panels.controls?.sections ?? []).flatMap((section) => {
|
|
134
|
+
if (!isToolcraftSectionVisible(state, section)) {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return Object.values(section.controls).filter(
|
|
139
|
+
(control) =>
|
|
140
|
+
control.type === "fileDrop" && isToolcraftControlVisible(state, control),
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function getCanvasDropTarget(
|
|
146
|
+
state: ToolcraftState,
|
|
147
|
+
file: File,
|
|
148
|
+
): ToolcraftControlSchema | null {
|
|
149
|
+
const isImage = isToolcraftImageFile(file);
|
|
150
|
+
const controls = getVisibleFileDropControls(state).filter((control) =>
|
|
151
|
+
controlAcceptsFile(control, file),
|
|
152
|
+
);
|
|
153
|
+
const preferredAssetKind = isImage ? "image" : "file";
|
|
154
|
+
const exactMatch = controls.find(
|
|
155
|
+
(control) =>
|
|
156
|
+
(control.assetKind === "file" ? "file" : "image") === preferredAssetKind,
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
if (exactMatch) {
|
|
160
|
+
return exactMatch;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return isImage
|
|
164
|
+
? (controls.find((control) => control.assetKind === "file") ?? null)
|
|
165
|
+
: null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function isDefaultCanvasImageAsset(
|
|
169
|
+
state: ToolcraftState,
|
|
170
|
+
mediaAsset: ToolcraftMediaAsset,
|
|
171
|
+
): mediaAsset is ToolcraftCanvasImageAsset {
|
|
172
|
+
return (
|
|
173
|
+
(mediaAsset.assetKind ?? "image") === "image" &&
|
|
174
|
+
mediaAsset.size !== undefined &&
|
|
175
|
+
isToolcraftLayerVisibleInTree(state.layers, mediaAsset.layerId)
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
82
179
|
function getZoomedCanvasOffset({
|
|
83
180
|
clientX,
|
|
84
181
|
clientY,
|
|
@@ -122,7 +219,7 @@ export function CanvasShell({
|
|
|
122
219
|
const visibleMediaAssets = React.useMemo(
|
|
123
220
|
() =>
|
|
124
221
|
state.mediaAssets.filter((mediaAsset) =>
|
|
125
|
-
|
|
222
|
+
isDefaultCanvasImageAsset(state, mediaAsset),
|
|
126
223
|
),
|
|
127
224
|
[state.layers, state.mediaAssets],
|
|
128
225
|
);
|
|
@@ -279,28 +376,86 @@ export function CanvasShell({
|
|
|
279
376
|
event.preventDefault();
|
|
280
377
|
setDragOver(false);
|
|
281
378
|
|
|
282
|
-
const
|
|
379
|
+
const uploadedFiles = Array.from(event.dataTransfer?.files ?? []);
|
|
283
380
|
|
|
284
|
-
if (
|
|
381
|
+
if (uploadedFiles.length === 0) {
|
|
285
382
|
return;
|
|
286
383
|
}
|
|
287
384
|
|
|
288
385
|
const position = getCanvasPositionFromEvent(event, offset, zoom);
|
|
386
|
+
const hasFileDropControls = getVisibleFileDropControls(state).length > 0;
|
|
387
|
+
|
|
388
|
+
uploadedFiles.forEach((uploadedFile) => {
|
|
389
|
+
const targetControl = getCanvasDropTarget(state, uploadedFile);
|
|
289
390
|
|
|
290
|
-
|
|
291
|
-
|
|
391
|
+
if (!targetControl) {
|
|
392
|
+
if (hasFileDropControls || !isToolcraftImageFile(uploadedFile)) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
void readImportedImageFile(uploadedFile, size).then((importedImage) => {
|
|
397
|
+
if (!importedImage) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
dispatch({
|
|
402
|
+
asset: {
|
|
403
|
+
assetKind: "image",
|
|
404
|
+
dataUrl: importedImage.dataUrl,
|
|
405
|
+
fileName: uploadedFile.name,
|
|
406
|
+
mimeType: uploadedFile.type || "image/*",
|
|
407
|
+
position,
|
|
408
|
+
size: importedImage.size,
|
|
409
|
+
},
|
|
410
|
+
type: "media.import",
|
|
411
|
+
});
|
|
412
|
+
});
|
|
292
413
|
return;
|
|
293
414
|
}
|
|
294
415
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
416
|
+
const assetKind = targetControl.assetKind === "file" ? "file" : "image";
|
|
417
|
+
const replaceExisting = targetControl.multiple !== true;
|
|
418
|
+
|
|
419
|
+
if (assetKind === "file") {
|
|
420
|
+
void readImportedFile(uploadedFile).then((importedFile) => {
|
|
421
|
+
if (!importedFile) {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
dispatch({
|
|
426
|
+
asset: {
|
|
427
|
+
assetKind: "file",
|
|
428
|
+
dataUrl: importedFile.dataUrl,
|
|
429
|
+
fileName: uploadedFile.name,
|
|
430
|
+
mimeType: uploadedFile.type || "application/octet-stream",
|
|
431
|
+
position: { x: 0, y: 0 },
|
|
432
|
+
sourceTarget: targetControl.target,
|
|
433
|
+
},
|
|
434
|
+
replaceExisting,
|
|
435
|
+
type: "media.import",
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
void readImportedImageFile(uploadedFile, size).then((importedImage) => {
|
|
442
|
+
if (!importedImage) {
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
dispatch({
|
|
447
|
+
asset: {
|
|
448
|
+
assetKind: "image",
|
|
449
|
+
dataUrl: importedImage.dataUrl,
|
|
450
|
+
fileName: uploadedFile.name,
|
|
451
|
+
mimeType: uploadedFile.type || "image/*",
|
|
452
|
+
position,
|
|
453
|
+
size: importedImage.size,
|
|
454
|
+
sourceTarget: targetControl.target,
|
|
455
|
+
},
|
|
456
|
+
replaceExisting,
|
|
457
|
+
type: "media.import",
|
|
458
|
+
});
|
|
304
459
|
});
|
|
305
460
|
});
|
|
306
461
|
};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getToolcraftCanvasSizeTargetDimension,
|
|
3
|
+
} from "../schema/runtime-targets";
|
|
4
|
+
import type {
|
|
5
|
+
ToolcraftControlConditionSchema,
|
|
6
|
+
ToolcraftControlSchema,
|
|
7
|
+
ToolcraftControlSectionSchema,
|
|
8
|
+
} from "../schema/types";
|
|
9
|
+
import type { ToolcraftState } from "../state/types";
|
|
10
|
+
|
|
11
|
+
function valuesEqual(first: unknown, second: unknown): boolean {
|
|
12
|
+
if (Object.is(first, second)) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (
|
|
17
|
+
(typeof first !== "object" || first === null) &&
|
|
18
|
+
(typeof second !== "object" || second === null)
|
|
19
|
+
) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
return JSON.stringify(first) === JSON.stringify(second);
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function valuesInclude(value: unknown, options: readonly unknown[]): boolean {
|
|
31
|
+
return options.some((option) => valuesEqual(value, option));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readComparableNumber(value: unknown): number | null {
|
|
35
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (typeof value === "string") {
|
|
40
|
+
const numberValue = Number(value.trim());
|
|
41
|
+
|
|
42
|
+
return Number.isFinite(numberValue) ? numberValue : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function compareConditionNumbers(
|
|
49
|
+
value: unknown,
|
|
50
|
+
expected: number | undefined,
|
|
51
|
+
comparator: (value: number, expected: number) => boolean,
|
|
52
|
+
): boolean | null {
|
|
53
|
+
if (typeof expected !== "number" || !Number.isFinite(expected)) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const numberValue = readComparableNumber(value);
|
|
58
|
+
|
|
59
|
+
return numberValue === null ? false : comparator(numberValue, expected);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getControlDefaultValueByTarget(
|
|
63
|
+
sections: readonly ToolcraftControlSectionSchema[],
|
|
64
|
+
target: string,
|
|
65
|
+
): unknown {
|
|
66
|
+
for (const section of sections) {
|
|
67
|
+
for (const control of Object.values(section.controls)) {
|
|
68
|
+
if (control.target === target) {
|
|
69
|
+
return control.defaultValue;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function getToolcraftTargetValue(
|
|
78
|
+
state: ToolcraftState,
|
|
79
|
+
target: string,
|
|
80
|
+
): unknown {
|
|
81
|
+
const canvasSizeDimension = getToolcraftCanvasSizeTargetDimension(target);
|
|
82
|
+
|
|
83
|
+
return canvasSizeDimension
|
|
84
|
+
? state.canvas.size[canvasSizeDimension]
|
|
85
|
+
: (state.values[target] ??
|
|
86
|
+
getControlDefaultValueByTarget(
|
|
87
|
+
state.schema.panels.controls?.sections ?? [],
|
|
88
|
+
target,
|
|
89
|
+
));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function toolcraftConditionMatches(
|
|
93
|
+
state: ToolcraftState,
|
|
94
|
+
condition: ToolcraftControlConditionSchema,
|
|
95
|
+
): boolean {
|
|
96
|
+
const value = getToolcraftTargetValue(state, condition.target);
|
|
97
|
+
const matches: boolean[] = [];
|
|
98
|
+
|
|
99
|
+
if ("equals" in condition) {
|
|
100
|
+
matches.push(valuesEqual(value, condition.equals));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if ("notEquals" in condition) {
|
|
104
|
+
matches.push(!valuesEqual(value, condition.notEquals));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (Array.isArray(condition.oneOf)) {
|
|
108
|
+
matches.push(valuesInclude(value, condition.oneOf));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (Array.isArray(condition.notOneOf)) {
|
|
112
|
+
matches.push(!valuesInclude(value, condition.notOneOf));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const greaterThan = compareConditionNumbers(
|
|
116
|
+
value,
|
|
117
|
+
condition.greaterThan,
|
|
118
|
+
(numberValue, expected) => numberValue > expected,
|
|
119
|
+
);
|
|
120
|
+
const greaterThanOrEqual = compareConditionNumbers(
|
|
121
|
+
value,
|
|
122
|
+
condition.greaterThanOrEqual,
|
|
123
|
+
(numberValue, expected) => numberValue >= expected,
|
|
124
|
+
);
|
|
125
|
+
const lessThan = compareConditionNumbers(
|
|
126
|
+
value,
|
|
127
|
+
condition.lessThan,
|
|
128
|
+
(numberValue, expected) => numberValue < expected,
|
|
129
|
+
);
|
|
130
|
+
const lessThanOrEqual = compareConditionNumbers(
|
|
131
|
+
value,
|
|
132
|
+
condition.lessThanOrEqual,
|
|
133
|
+
(numberValue, expected) => numberValue <= expected,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
for (const match of [
|
|
137
|
+
greaterThan,
|
|
138
|
+
greaterThanOrEqual,
|
|
139
|
+
lessThan,
|
|
140
|
+
lessThanOrEqual,
|
|
141
|
+
]) {
|
|
142
|
+
if (match !== null) {
|
|
143
|
+
matches.push(match);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return matches.length > 0 && matches.every(Boolean);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function isToolcraftSectionVisible(
|
|
151
|
+
state: ToolcraftState,
|
|
152
|
+
section: ToolcraftControlSectionSchema,
|
|
153
|
+
): boolean {
|
|
154
|
+
return section.visibleWhen
|
|
155
|
+
? toolcraftConditionMatches(state, section.visibleWhen)
|
|
156
|
+
: true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function isToolcraftControlVisible(
|
|
160
|
+
state: ToolcraftState,
|
|
161
|
+
control: ToolcraftControlSchema,
|
|
162
|
+
): boolean {
|
|
163
|
+
return control.visibleWhen
|
|
164
|
+
? toolcraftConditionMatches(state, control.visibleWhen)
|
|
165
|
+
: true;
|
|
166
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { defineToolcraft } from "../schema/define-toolcraft";
|
|
6
|
+
import type { FileDropPreview } from "@repo/ui";
|
|
7
|
+
import { ControlsPanel } from "./controls-panel";
|
|
8
|
+
import { ToolcraftRoot } from "./toolcraft-root";
|
|
9
|
+
import { useToolcraft } from "./use-toolcraft";
|
|
10
|
+
|
|
11
|
+
vi.mock("@repo/ui", async (importOriginal) => {
|
|
12
|
+
const actual = await importOriginal<typeof import("@repo/ui")>();
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
...actual,
|
|
16
|
+
FileDrop: ({
|
|
17
|
+
assetKind = "image",
|
|
18
|
+
onPreviewReorder,
|
|
19
|
+
previews = [],
|
|
20
|
+
}: {
|
|
21
|
+
assetKind?: "file" | "image";
|
|
22
|
+
onPreviewReorder?: (previews: FileDropPreview[]) => void;
|
|
23
|
+
previews?: readonly FileDropPreview[];
|
|
24
|
+
}) => (
|
|
25
|
+
<button
|
|
26
|
+
onClick={() => {
|
|
27
|
+
onPreviewReorder?.([...previews].reverse());
|
|
28
|
+
}}
|
|
29
|
+
type="button"
|
|
30
|
+
>
|
|
31
|
+
Reorder {assetKind}
|
|
32
|
+
</button>
|
|
33
|
+
),
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
beforeAll(() => {
|
|
38
|
+
Object.defineProperty(window, "matchMedia", {
|
|
39
|
+
value: (query: string) => ({
|
|
40
|
+
addEventListener: () => undefined,
|
|
41
|
+
addListener: () => undefined,
|
|
42
|
+
dispatchEvent: () => false,
|
|
43
|
+
matches: query === "(prefers-reduced-motion: reduce)",
|
|
44
|
+
media: query,
|
|
45
|
+
onchange: null,
|
|
46
|
+
removeEventListener: () => undefined,
|
|
47
|
+
removeListener: () => undefined,
|
|
48
|
+
}),
|
|
49
|
+
writable: true,
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
cleanup();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
function StateProbe() {
|
|
58
|
+
const { state } = useToolcraft();
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<span data-testid="media-ids">
|
|
62
|
+
{state.mediaAssets.map((asset) => asset.id).join(",")}
|
|
63
|
+
</span>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describe("ControlsPanel fileDrop reorder integration", () => {
|
|
68
|
+
it("commits image and file preview sorting back to runtime media order separately", async () => {
|
|
69
|
+
const schema = defineToolcraft({
|
|
70
|
+
canvas: { enabled: true, upload: true },
|
|
71
|
+
panels: {
|
|
72
|
+
controls: {
|
|
73
|
+
sections: [
|
|
74
|
+
{
|
|
75
|
+
controls: {
|
|
76
|
+
images: {
|
|
77
|
+
label: "Images",
|
|
78
|
+
multiple: true,
|
|
79
|
+
target: "input.images",
|
|
80
|
+
type: "fileDrop",
|
|
81
|
+
},
|
|
82
|
+
files: {
|
|
83
|
+
assetKind: "file",
|
|
84
|
+
label: "Files",
|
|
85
|
+
multiple: true,
|
|
86
|
+
target: "input.files",
|
|
87
|
+
type: "fileDrop",
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
title: "Sources",
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
title: "Controls",
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
render(
|
|
99
|
+
<ToolcraftRoot
|
|
100
|
+
initialState={{
|
|
101
|
+
layers: [
|
|
102
|
+
{ id: "layer-img-1", kind: "layer", name: "Image 1", visible: true },
|
|
103
|
+
{ id: "layer-file-1", kind: "layer", name: "File 1", visible: true },
|
|
104
|
+
{ id: "layer-img-2", kind: "layer", name: "Image 2", visible: true },
|
|
105
|
+
{ id: "layer-file-2", kind: "layer", name: "File 2", visible: true },
|
|
106
|
+
],
|
|
107
|
+
mediaAssets: [
|
|
108
|
+
{
|
|
109
|
+
assetKind: "image",
|
|
110
|
+
dataUrl: "data:image/png;base64,aW1nMQ==",
|
|
111
|
+
fileName: "one.png",
|
|
112
|
+
id: "img-1",
|
|
113
|
+
layerId: "layer-img-1",
|
|
114
|
+
mimeType: "image/png",
|
|
115
|
+
position: { x: 0, y: 0 },
|
|
116
|
+
size: { height: 64, unit: "px", width: 64 },
|
|
117
|
+
sourceTarget: "input.images",
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
assetKind: "file",
|
|
121
|
+
dataUrl: "data:text/plain;base64,ZmlsZTE=",
|
|
122
|
+
fileName: "one.txt",
|
|
123
|
+
id: "file-1",
|
|
124
|
+
layerId: "layer-file-1",
|
|
125
|
+
mimeType: "text/plain",
|
|
126
|
+
position: { x: 0, y: 0 },
|
|
127
|
+
sourceTarget: "input.files",
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
assetKind: "image",
|
|
131
|
+
dataUrl: "data:image/png;base64,aW1nMg==",
|
|
132
|
+
fileName: "two.png",
|
|
133
|
+
id: "img-2",
|
|
134
|
+
layerId: "layer-img-2",
|
|
135
|
+
mimeType: "image/png",
|
|
136
|
+
position: { x: 0, y: 0 },
|
|
137
|
+
size: { height: 64, unit: "px", width: 64 },
|
|
138
|
+
sourceTarget: "input.images",
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
assetKind: "file",
|
|
142
|
+
dataUrl: "data:application/json;base64,e30=",
|
|
143
|
+
fileName: "two.json",
|
|
144
|
+
id: "file-2",
|
|
145
|
+
layerId: "layer-file-2",
|
|
146
|
+
mimeType: "application/json",
|
|
147
|
+
position: { x: 0, y: 0 },
|
|
148
|
+
sourceTarget: "input.files",
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
selectedLayerId: "layer-file-2",
|
|
152
|
+
}}
|
|
153
|
+
schema={schema}
|
|
154
|
+
>
|
|
155
|
+
<ControlsPanel framed={false} />
|
|
156
|
+
<StateProbe />
|
|
157
|
+
</ToolcraftRoot>,
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
expect(screen.getByTestId("media-ids").textContent).toBe(
|
|
161
|
+
"img-1,file-1,img-2,file-2",
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
fireEvent.click(screen.getByRole("button", { name: "Reorder image" }));
|
|
165
|
+
|
|
166
|
+
expect(screen.getByTestId("media-ids").textContent).toBe(
|
|
167
|
+
"img-2,file-1,img-1,file-2",
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
fireEvent.click(screen.getByRole("button", { name: "Reorder file" }));
|
|
171
|
+
|
|
172
|
+
expect(screen.getByTestId("media-ids").textContent).toBe(
|
|
173
|
+
"img-2,file-2,img-1,file-1",
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
});
|