@routevn/creator-model 1.0.3 → 1.0.5
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/src/model.js +925 -206
package/src/model.js
CHANGED
|
@@ -30,36 +30,28 @@ const COLLECTION_KEYS = [
|
|
|
30
30
|
"textStyles",
|
|
31
31
|
"variables",
|
|
32
32
|
"layouts",
|
|
33
|
+
"controls",
|
|
33
34
|
];
|
|
34
35
|
const ROOT_KEYS = ["project", "story", ...COLLECTION_KEYS];
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
};
|
|
53
|
-
const VIDEO_FILE_REFERENCE_TYPES = {
|
|
54
|
-
fileId: ["video"],
|
|
55
|
-
thumbnailFileId: ["video-thumbnail"],
|
|
56
|
-
};
|
|
57
|
-
const FONT_FILE_REFERENCE_TYPES = {
|
|
58
|
-
fileId: ["font"],
|
|
59
|
-
};
|
|
60
|
-
const CHARACTER_FILE_REFERENCE_TYPES = {
|
|
61
|
-
fileId: ["image"],
|
|
36
|
+
const createEmptyCollectionState = () => ({
|
|
37
|
+
items: {},
|
|
38
|
+
tree: [],
|
|
39
|
+
});
|
|
40
|
+
const normalizeStateCollections = (state) => {
|
|
41
|
+
if (!isPlainObject(state)) {
|
|
42
|
+
return state;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (state.controls !== undefined) {
|
|
46
|
+
return state;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
...state,
|
|
51
|
+
controls: createEmptyCollectionState(),
|
|
52
|
+
};
|
|
62
53
|
};
|
|
54
|
+
const isString = (value) => typeof value === "string";
|
|
63
55
|
const isHexColor = (value) =>
|
|
64
56
|
typeof value === "string" && /^#[0-9a-fA-F]{6}$/.test(value);
|
|
65
57
|
const LIVE_TWEEN_PROPERTY_KEYS = [
|
|
@@ -115,7 +107,7 @@ const ANIMATION_EASING_KEYS = [
|
|
|
115
107
|
];
|
|
116
108
|
const VARIABLE_SCOPE_KEYS = ["context", "global-device", "global-account"];
|
|
117
109
|
const VARIABLE_TYPE_KEYS = ["string", "number", "boolean"];
|
|
118
|
-
const LAYOUT_TYPE_KEYS = ["normal", "dialogue", "nvl", "choice"
|
|
110
|
+
const LAYOUT_TYPE_KEYS = ["normal", "dialogue", "nvl", "choice"];
|
|
119
111
|
const LAYOUT_ELEMENT_TEXT_STYLE_ALIGN_KEYS = ["left", "center", "right"];
|
|
120
112
|
const LAYOUT_ELEMENT_BASE_TYPES = [
|
|
121
113
|
"folder",
|
|
@@ -562,10 +554,10 @@ const validateFileItems = ({ items, path, errorFactory }) => {
|
|
|
562
554
|
for (const [itemId, item] of Object.entries(items)) {
|
|
563
555
|
const itemPath = `${path}.${itemId}`;
|
|
564
556
|
|
|
565
|
-
if (item?.type !==
|
|
557
|
+
if (item?.type !== undefined && !isNonEmptyString(item.type)) {
|
|
566
558
|
return invalidFromErrorFactory(
|
|
567
559
|
errorFactory,
|
|
568
|
-
`${itemPath}.type must be
|
|
560
|
+
`${itemPath}.type must be a non-empty string when provided`,
|
|
569
561
|
);
|
|
570
562
|
}
|
|
571
563
|
|
|
@@ -2588,7 +2580,7 @@ const validateLayoutItems = ({ items, path, errorFactory }) => {
|
|
|
2588
2580
|
allowedKeys:
|
|
2589
2581
|
item.type === "folder"
|
|
2590
2582
|
? ["id", "type", "name"]
|
|
2591
|
-
: ["id", "type", "name", "layoutType", "elements"
|
|
2583
|
+
: ["id", "type", "name", "layoutType", "elements"],
|
|
2592
2584
|
path: itemPath,
|
|
2593
2585
|
errorFactory,
|
|
2594
2586
|
});
|
|
@@ -2622,7 +2614,7 @@ const validateLayoutItems = ({ items, path, errorFactory }) => {
|
|
|
2622
2614
|
if (!LAYOUT_TYPE_KEYS.includes(item.layoutType)) {
|
|
2623
2615
|
return invalidFromErrorFactory(
|
|
2624
2616
|
errorFactory,
|
|
2625
|
-
`${itemPath}.layoutType must be 'normal', 'dialogue', 'nvl',
|
|
2617
|
+
`${itemPath}.layoutType must be 'normal', 'dialogue', 'nvl', or 'choice'`,
|
|
2626
2618
|
);
|
|
2627
2619
|
}
|
|
2628
2620
|
|
|
@@ -2639,6 +2631,71 @@ const validateLayoutItems = ({ items, path, errorFactory }) => {
|
|
|
2639
2631
|
}
|
|
2640
2632
|
}
|
|
2641
2633
|
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
};
|
|
2637
|
+
|
|
2638
|
+
const validateControlItems = ({ items, path, errorFactory }) => {
|
|
2639
|
+
for (const [itemId, item] of Object.entries(items)) {
|
|
2640
|
+
const itemPath = `${path}.${itemId}`;
|
|
2641
|
+
|
|
2642
|
+
if (item?.type !== "folder" && item?.type !== "control") {
|
|
2643
|
+
return invalidFromErrorFactory(
|
|
2644
|
+
errorFactory,
|
|
2645
|
+
`${itemPath}.type must be 'folder' or 'control'`,
|
|
2646
|
+
);
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
{
|
|
2650
|
+
const result = validateAllowedKeys({
|
|
2651
|
+
value: item,
|
|
2652
|
+
allowedKeys:
|
|
2653
|
+
item.type === "folder"
|
|
2654
|
+
? ["id", "type", "name"]
|
|
2655
|
+
: ["id", "type", "name", "elements", "keyboard"],
|
|
2656
|
+
path: itemPath,
|
|
2657
|
+
errorFactory,
|
|
2658
|
+
});
|
|
2659
|
+
if (result?.valid === false) {
|
|
2660
|
+
return result;
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
if (!isNonEmptyString(item.id)) {
|
|
2665
|
+
return invalidFromErrorFactory(
|
|
2666
|
+
errorFactory,
|
|
2667
|
+
`${itemPath}.id must be a non-empty string`,
|
|
2668
|
+
);
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
if (item.id !== itemId) {
|
|
2672
|
+
return invalidFromErrorFactory(
|
|
2673
|
+
errorFactory,
|
|
2674
|
+
`${itemPath}.id must match item key '${itemId}'`,
|
|
2675
|
+
);
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
if (!isNonEmptyString(item.name)) {
|
|
2679
|
+
return invalidFromErrorFactory(
|
|
2680
|
+
errorFactory,
|
|
2681
|
+
`${itemPath}.name must be a non-empty string`,
|
|
2682
|
+
);
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
if (item.type === "control") {
|
|
2686
|
+
{
|
|
2687
|
+
const result = validateNestedCollection({
|
|
2688
|
+
collection: item.elements,
|
|
2689
|
+
path: `${itemPath}.elements`,
|
|
2690
|
+
itemValidator: validateLayoutElementItems,
|
|
2691
|
+
treeValidator: validateLayoutElementTreeOwnership,
|
|
2692
|
+
treeNodeLabel: "control element",
|
|
2693
|
+
});
|
|
2694
|
+
if (result?.valid === false) {
|
|
2695
|
+
return result;
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2642
2699
|
{
|
|
2643
2700
|
const result = validateKeyboardMap({
|
|
2644
2701
|
value: item.keyboard,
|
|
@@ -3264,6 +3321,17 @@ const validateCollection = ({ collection, path }) => {
|
|
|
3264
3321
|
return result;
|
|
3265
3322
|
}
|
|
3266
3323
|
}
|
|
3324
|
+
} else if (path === "state.controls") {
|
|
3325
|
+
{
|
|
3326
|
+
const result = validateControlItems({
|
|
3327
|
+
items: collection.items,
|
|
3328
|
+
path: `${path}.items`,
|
|
3329
|
+
errorFactory: createStateValidationError,
|
|
3330
|
+
});
|
|
3331
|
+
if (result?.valid === false) {
|
|
3332
|
+
return result;
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3267
3335
|
}
|
|
3268
3336
|
|
|
3269
3337
|
if (!Array.isArray(collection.tree)) {
|
|
@@ -3366,7 +3434,8 @@ const validateCollection = ({ collection, path }) => {
|
|
|
3366
3434
|
path === "state.variables" ||
|
|
3367
3435
|
path === "state.textStyles" ||
|
|
3368
3436
|
path === "state.characters" ||
|
|
3369
|
-
path === "state.layouts"
|
|
3437
|
+
path === "state.layouts" ||
|
|
3438
|
+
path === "state.controls"
|
|
3370
3439
|
) {
|
|
3371
3440
|
{
|
|
3372
3441
|
const result = validateGenericFolderOwnership({
|
|
@@ -3374,7 +3443,11 @@ const validateCollection = ({ collection, path }) => {
|
|
|
3374
3443
|
items: collection.items,
|
|
3375
3444
|
path: `${path}.tree`,
|
|
3376
3445
|
folderLabel:
|
|
3377
|
-
path === "state.layouts"
|
|
3446
|
+
path === "state.layouts"
|
|
3447
|
+
? "folder layout item"
|
|
3448
|
+
: path === "state.controls"
|
|
3449
|
+
? "folder control item"
|
|
3450
|
+
: "folder item",
|
|
3378
3451
|
});
|
|
3379
3452
|
if (result?.valid === false) {
|
|
3380
3453
|
return result;
|
|
@@ -3393,7 +3466,6 @@ const validateFileReference = ({
|
|
|
3393
3466
|
state,
|
|
3394
3467
|
fileId,
|
|
3395
3468
|
path,
|
|
3396
|
-
allowedTypes,
|
|
3397
3469
|
details = {},
|
|
3398
3470
|
errorFactory = createPreconditionValidationError,
|
|
3399
3471
|
}) => {
|
|
@@ -3401,36 +3473,10 @@ const validateFileReference = ({
|
|
|
3401
3473
|
return VALID_RESULT;
|
|
3402
3474
|
}
|
|
3403
3475
|
|
|
3404
|
-
const expectedTypeMessage =
|
|
3405
|
-
Array.isArray(allowedTypes) && allowedTypes.length > 0
|
|
3406
|
-
? `${path} must reference an existing non-folder file with type ${allowedTypes
|
|
3407
|
-
.map((type) => `'${type}'`)
|
|
3408
|
-
.join(" or ")}`
|
|
3409
|
-
: `${path} must reference an existing non-folder file`;
|
|
3476
|
+
const expectedTypeMessage = `${path} must reference an existing non-folder file`;
|
|
3410
3477
|
const file = state.files?.items?.[fileId];
|
|
3411
3478
|
if (!isPlainObject(file) || file.type === "folder") {
|
|
3412
|
-
return invalidFromErrorFactory(
|
|
3413
|
-
errorFactory,
|
|
3414
|
-
expectedTypeMessage,
|
|
3415
|
-
Array.isArray(allowedTypes) && allowedTypes.length > 0
|
|
3416
|
-
? {
|
|
3417
|
-
...details,
|
|
3418
|
-
expectedFileTypes: [...allowedTypes],
|
|
3419
|
-
}
|
|
3420
|
-
: details,
|
|
3421
|
-
);
|
|
3422
|
-
}
|
|
3423
|
-
|
|
3424
|
-
if (
|
|
3425
|
-
Array.isArray(allowedTypes) &&
|
|
3426
|
-
allowedTypes.length > 0 &&
|
|
3427
|
-
!allowedTypes.includes(file.type)
|
|
3428
|
-
) {
|
|
3429
|
-
return invalidFromErrorFactory(errorFactory, expectedTypeMessage, {
|
|
3430
|
-
...details,
|
|
3431
|
-
expectedFileTypes: [...allowedTypes],
|
|
3432
|
-
actualFileType: file.type,
|
|
3433
|
-
});
|
|
3479
|
+
return invalidFromErrorFactory(errorFactory, expectedTypeMessage, details);
|
|
3434
3480
|
}
|
|
3435
3481
|
|
|
3436
3482
|
return VALID_RESULT;
|
|
@@ -3559,7 +3605,6 @@ export const assertInvariants = ({ state }) => {
|
|
|
3559
3605
|
state,
|
|
3560
3606
|
fileId: image.fileId,
|
|
3561
3607
|
path: "image.fileId",
|
|
3562
|
-
allowedTypes: IMAGE_FILE_REFERENCE_TYPES.fileId,
|
|
3563
3608
|
details: { imageId, fileId: image.fileId },
|
|
3564
3609
|
errorFactory: createInvariantValidationError,
|
|
3565
3610
|
});
|
|
@@ -3573,7 +3618,6 @@ export const assertInvariants = ({ state }) => {
|
|
|
3573
3618
|
state,
|
|
3574
3619
|
fileId: image.thumbnailFileId,
|
|
3575
3620
|
path: "image.thumbnailFileId",
|
|
3576
|
-
allowedTypes: IMAGE_FILE_REFERENCE_TYPES.thumbnailFileId,
|
|
3577
3621
|
details: { imageId, thumbnailFileId: image.thumbnailFileId },
|
|
3578
3622
|
errorFactory: createInvariantValidationError,
|
|
3579
3623
|
});
|
|
@@ -3593,7 +3637,6 @@ export const assertInvariants = ({ state }) => {
|
|
|
3593
3637
|
state,
|
|
3594
3638
|
fileId: sound.fileId,
|
|
3595
3639
|
path: "sound.fileId",
|
|
3596
|
-
allowedTypes: SOUND_FILE_REFERENCE_TYPES.fileId,
|
|
3597
3640
|
details: { soundId, fileId: sound.fileId },
|
|
3598
3641
|
errorFactory: createInvariantValidationError,
|
|
3599
3642
|
});
|
|
@@ -3610,7 +3653,6 @@ export const assertInvariants = ({ state }) => {
|
|
|
3610
3653
|
state,
|
|
3611
3654
|
fileId: sound.waveformDataFileId,
|
|
3612
3655
|
path: "sound.waveformDataFileId",
|
|
3613
|
-
allowedTypes: SOUND_FILE_REFERENCE_TYPES.waveformDataFileId,
|
|
3614
3656
|
details: { soundId, waveformDataFileId: sound.waveformDataFileId },
|
|
3615
3657
|
errorFactory: createInvariantValidationError,
|
|
3616
3658
|
});
|
|
@@ -3630,7 +3672,6 @@ export const assertInvariants = ({ state }) => {
|
|
|
3630
3672
|
state,
|
|
3631
3673
|
fileId: video.fileId,
|
|
3632
3674
|
path: "video.fileId",
|
|
3633
|
-
allowedTypes: VIDEO_FILE_REFERENCE_TYPES.fileId,
|
|
3634
3675
|
details: { videoId, fileId: video.fileId },
|
|
3635
3676
|
errorFactory: createInvariantValidationError,
|
|
3636
3677
|
});
|
|
@@ -3644,7 +3685,6 @@ export const assertInvariants = ({ state }) => {
|
|
|
3644
3685
|
state,
|
|
3645
3686
|
fileId: video.thumbnailFileId,
|
|
3646
3687
|
path: "video.thumbnailFileId",
|
|
3647
|
-
allowedTypes: VIDEO_FILE_REFERENCE_TYPES.thumbnailFileId,
|
|
3648
3688
|
details: { videoId, thumbnailFileId: video.thumbnailFileId },
|
|
3649
3689
|
errorFactory: createInvariantValidationError,
|
|
3650
3690
|
});
|
|
@@ -3663,7 +3703,6 @@ export const assertInvariants = ({ state }) => {
|
|
|
3663
3703
|
state,
|
|
3664
3704
|
fileId: font.fileId,
|
|
3665
3705
|
path: "font.fileId",
|
|
3666
|
-
allowedTypes: FONT_FILE_REFERENCE_TYPES.fileId,
|
|
3667
3706
|
details: { fontId, fileId: font.fileId },
|
|
3668
3707
|
errorFactory: createInvariantValidationError,
|
|
3669
3708
|
});
|
|
@@ -3684,7 +3723,6 @@ export const assertInvariants = ({ state }) => {
|
|
|
3684
3723
|
state,
|
|
3685
3724
|
fileId: character.fileId,
|
|
3686
3725
|
path: "character.fileId",
|
|
3687
|
-
allowedTypes: CHARACTER_FILE_REFERENCE_TYPES.fileId,
|
|
3688
3726
|
details: { characterId, fileId: character.fileId },
|
|
3689
3727
|
errorFactory: createInvariantValidationError,
|
|
3690
3728
|
});
|
|
@@ -3704,7 +3742,6 @@ export const assertInvariants = ({ state }) => {
|
|
|
3704
3742
|
state,
|
|
3705
3743
|
fileId: sprite.fileId,
|
|
3706
3744
|
path: "character.sprite.fileId",
|
|
3707
|
-
allowedTypes: CHARACTER_FILE_REFERENCE_TYPES.fileId,
|
|
3708
3745
|
details: { characterId, spriteId, fileId: sprite.fileId },
|
|
3709
3746
|
errorFactory: createInvariantValidationError,
|
|
3710
3747
|
});
|
|
@@ -3714,13 +3751,20 @@ export const assertInvariants = ({ state }) => {
|
|
|
3714
3751
|
}
|
|
3715
3752
|
}
|
|
3716
3753
|
|
|
3717
|
-
const assertImageReference = ({
|
|
3754
|
+
const assertImageReference = ({
|
|
3755
|
+
ownerIdField,
|
|
3756
|
+
ownerId,
|
|
3757
|
+
ownerLabel,
|
|
3758
|
+
elementId,
|
|
3759
|
+
field,
|
|
3760
|
+
targetId,
|
|
3761
|
+
}) => {
|
|
3718
3762
|
const image = state.images.items[targetId];
|
|
3719
3763
|
if (!isPlainObject(image) || image.type === "folder") {
|
|
3720
3764
|
return invalidInvariant(
|
|
3721
|
-
|
|
3765
|
+
`${ownerLabel} element ${field} must reference an existing non-folder image`,
|
|
3722
3766
|
{
|
|
3723
|
-
|
|
3767
|
+
[ownerIdField]: ownerId,
|
|
3724
3768
|
elementId,
|
|
3725
3769
|
field,
|
|
3726
3770
|
targetId,
|
|
@@ -3732,7 +3776,9 @@ export const assertInvariants = ({ state }) => {
|
|
|
3732
3776
|
};
|
|
3733
3777
|
|
|
3734
3778
|
const assertTextStyleReference = ({
|
|
3735
|
-
|
|
3779
|
+
ownerIdField,
|
|
3780
|
+
ownerId,
|
|
3781
|
+
ownerLabel,
|
|
3736
3782
|
elementId,
|
|
3737
3783
|
field,
|
|
3738
3784
|
targetId,
|
|
@@ -3740,9 +3786,9 @@ export const assertInvariants = ({ state }) => {
|
|
|
3740
3786
|
const textStyle = state.textStyles.items[targetId];
|
|
3741
3787
|
if (!isPlainObject(textStyle) || textStyle.type === "folder") {
|
|
3742
3788
|
return invalidInvariant(
|
|
3743
|
-
|
|
3789
|
+
`${ownerLabel} element ${field} must reference an existing non-folder text style`,
|
|
3744
3790
|
{
|
|
3745
|
-
|
|
3791
|
+
[ownerIdField]: ownerId,
|
|
3746
3792
|
elementId,
|
|
3747
3793
|
field,
|
|
3748
3794
|
targetId,
|
|
@@ -3753,13 +3799,19 @@ export const assertInvariants = ({ state }) => {
|
|
|
3753
3799
|
return VALID_RESULT;
|
|
3754
3800
|
};
|
|
3755
3801
|
|
|
3756
|
-
const assertVariableReference = ({
|
|
3802
|
+
const assertVariableReference = ({
|
|
3803
|
+
ownerIdField,
|
|
3804
|
+
ownerId,
|
|
3805
|
+
ownerLabel,
|
|
3806
|
+
elementId,
|
|
3807
|
+
targetId,
|
|
3808
|
+
}) => {
|
|
3757
3809
|
const variable = state.variables.items[targetId];
|
|
3758
3810
|
if (!isPlainObject(variable) || variable.type === "folder") {
|
|
3759
3811
|
return invalidInvariant(
|
|
3760
|
-
|
|
3812
|
+
`${ownerLabel} element variableId must reference an existing non-folder variable`,
|
|
3761
3813
|
{
|
|
3762
|
-
|
|
3814
|
+
[ownerIdField]: ownerId,
|
|
3763
3815
|
elementId,
|
|
3764
3816
|
variableId: targetId,
|
|
3765
3817
|
},
|
|
@@ -3769,62 +3821,101 @@ export const assertInvariants = ({ state }) => {
|
|
|
3769
3821
|
return VALID_RESULT;
|
|
3770
3822
|
};
|
|
3771
3823
|
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3824
|
+
const assertElementReferencesForCollection = ({
|
|
3825
|
+
items,
|
|
3826
|
+
ownerIdField,
|
|
3827
|
+
ownerLabel,
|
|
3828
|
+
ownerType,
|
|
3829
|
+
}) => {
|
|
3830
|
+
for (const [ownerId, owner] of Object.entries(items)) {
|
|
3831
|
+
if (owner.type !== ownerType) {
|
|
3832
|
+
continue;
|
|
3833
|
+
}
|
|
3776
3834
|
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3835
|
+
for (const [elementId, element] of Object.entries(owner.elements.items)) {
|
|
3836
|
+
for (const field of [
|
|
3837
|
+
"imageId",
|
|
3838
|
+
"hoverImageId",
|
|
3839
|
+
"clickImageId",
|
|
3840
|
+
"thumbImageId",
|
|
3841
|
+
"barImageId",
|
|
3842
|
+
"hoverThumbImageId",
|
|
3843
|
+
"hoverBarImageId",
|
|
3844
|
+
]) {
|
|
3845
|
+
if (element[field] !== undefined) {
|
|
3846
|
+
const result = assertImageReference({
|
|
3847
|
+
ownerIdField,
|
|
3848
|
+
ownerId,
|
|
3849
|
+
ownerLabel,
|
|
3850
|
+
elementId,
|
|
3851
|
+
field,
|
|
3852
|
+
targetId: element[field],
|
|
3853
|
+
});
|
|
3854
|
+
if (!result.valid) {
|
|
3855
|
+
return result;
|
|
3856
|
+
}
|
|
3796
3857
|
}
|
|
3797
3858
|
}
|
|
3798
|
-
}
|
|
3799
3859
|
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3860
|
+
for (const field of [
|
|
3861
|
+
"textStyleId",
|
|
3862
|
+
"hoverTextStyleId",
|
|
3863
|
+
"clickTextStyleId",
|
|
3864
|
+
]) {
|
|
3865
|
+
if (element[field] !== undefined) {
|
|
3866
|
+
const result = assertTextStyleReference({
|
|
3867
|
+
ownerIdField,
|
|
3868
|
+
ownerId,
|
|
3869
|
+
ownerLabel,
|
|
3870
|
+
elementId,
|
|
3871
|
+
field,
|
|
3872
|
+
targetId: element[field],
|
|
3873
|
+
});
|
|
3874
|
+
if (!result.valid) {
|
|
3875
|
+
return result;
|
|
3876
|
+
}
|
|
3877
|
+
}
|
|
3878
|
+
}
|
|
3879
|
+
|
|
3880
|
+
if (element.variableId !== undefined) {
|
|
3881
|
+
const result = assertVariableReference({
|
|
3882
|
+
ownerIdField,
|
|
3883
|
+
ownerId,
|
|
3884
|
+
ownerLabel,
|
|
3808
3885
|
elementId,
|
|
3809
|
-
|
|
3810
|
-
targetId: element[field],
|
|
3886
|
+
targetId: element.variableId,
|
|
3811
3887
|
});
|
|
3812
3888
|
if (!result.valid) {
|
|
3813
3889
|
return result;
|
|
3814
3890
|
}
|
|
3815
3891
|
}
|
|
3816
3892
|
}
|
|
3893
|
+
}
|
|
3817
3894
|
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3895
|
+
return VALID_RESULT;
|
|
3896
|
+
};
|
|
3897
|
+
|
|
3898
|
+
{
|
|
3899
|
+
const result = assertElementReferencesForCollection({
|
|
3900
|
+
items: state.layouts.items,
|
|
3901
|
+
ownerIdField: "layoutId",
|
|
3902
|
+
ownerLabel: "layout",
|
|
3903
|
+
ownerType: "layout",
|
|
3904
|
+
});
|
|
3905
|
+
if (!result.valid) {
|
|
3906
|
+
return result;
|
|
3907
|
+
}
|
|
3908
|
+
}
|
|
3909
|
+
|
|
3910
|
+
{
|
|
3911
|
+
const result = assertElementReferencesForCollection({
|
|
3912
|
+
items: state.controls.items,
|
|
3913
|
+
ownerIdField: "controlId",
|
|
3914
|
+
ownerLabel: "control",
|
|
3915
|
+
ownerType: "control",
|
|
3916
|
+
});
|
|
3917
|
+
if (!result.valid) {
|
|
3918
|
+
return result;
|
|
3828
3919
|
}
|
|
3829
3920
|
}
|
|
3830
3921
|
|
|
@@ -3833,9 +3924,11 @@ export const assertInvariants = ({ state }) => {
|
|
|
3833
3924
|
|
|
3834
3925
|
const runValidateState = ({ state }) => {
|
|
3835
3926
|
return captureValidation(() => {
|
|
3927
|
+
const normalizedState = normalizeStateCollections(state);
|
|
3928
|
+
|
|
3836
3929
|
{
|
|
3837
3930
|
const result = validateExactKeys({
|
|
3838
|
-
value:
|
|
3931
|
+
value: normalizedState,
|
|
3839
3932
|
expectedKeys: ROOT_KEYS,
|
|
3840
3933
|
path: "state",
|
|
3841
3934
|
errorFactory: createStateValidationError,
|
|
@@ -3847,7 +3940,7 @@ const runValidateState = ({ state }) => {
|
|
|
3847
3940
|
|
|
3848
3941
|
{
|
|
3849
3942
|
const result = validateAllowedKeys({
|
|
3850
|
-
value:
|
|
3943
|
+
value: normalizedState.project,
|
|
3851
3944
|
allowedKeys: ["resolution"],
|
|
3852
3945
|
path: "state.project",
|
|
3853
3946
|
errorFactory: createStateValidationError,
|
|
@@ -3857,10 +3950,10 @@ const runValidateState = ({ state }) => {
|
|
|
3857
3950
|
}
|
|
3858
3951
|
}
|
|
3859
3952
|
|
|
3860
|
-
if (
|
|
3953
|
+
if (normalizedState.project.resolution !== undefined) {
|
|
3861
3954
|
{
|
|
3862
3955
|
const result = validateExactKeys({
|
|
3863
|
-
value:
|
|
3956
|
+
value: normalizedState.project.resolution,
|
|
3864
3957
|
expectedKeys: ["width", "height"],
|
|
3865
3958
|
path: "state.project.resolution",
|
|
3866
3959
|
errorFactory: createStateValidationError,
|
|
@@ -3870,13 +3963,13 @@ const runValidateState = ({ state }) => {
|
|
|
3870
3963
|
}
|
|
3871
3964
|
}
|
|
3872
3965
|
|
|
3873
|
-
if (!isFiniteNumber(
|
|
3966
|
+
if (!isFiniteNumber(normalizedState.project.resolution.width)) {
|
|
3874
3967
|
return invalidState(
|
|
3875
3968
|
"state.project.resolution.width must be a finite number",
|
|
3876
3969
|
);
|
|
3877
3970
|
}
|
|
3878
3971
|
|
|
3879
|
-
if (!isFiniteNumber(
|
|
3972
|
+
if (!isFiniteNumber(normalizedState.project.resolution.height)) {
|
|
3880
3973
|
return invalidState(
|
|
3881
3974
|
"state.project.resolution.height must be a finite number",
|
|
3882
3975
|
);
|
|
@@ -3885,7 +3978,7 @@ const runValidateState = ({ state }) => {
|
|
|
3885
3978
|
|
|
3886
3979
|
{
|
|
3887
3980
|
const result = validateExactKeys({
|
|
3888
|
-
value:
|
|
3981
|
+
value: normalizedState.story,
|
|
3889
3982
|
expectedKeys: ["initialSceneId"],
|
|
3890
3983
|
path: "state.story",
|
|
3891
3984
|
errorFactory: createStateValidationError,
|
|
@@ -3896,8 +3989,8 @@ const runValidateState = ({ state }) => {
|
|
|
3896
3989
|
}
|
|
3897
3990
|
|
|
3898
3991
|
if (
|
|
3899
|
-
|
|
3900
|
-
!isNonEmptyString(
|
|
3992
|
+
normalizedState.story.initialSceneId !== null &&
|
|
3993
|
+
!isNonEmptyString(normalizedState.story.initialSceneId)
|
|
3901
3994
|
) {
|
|
3902
3995
|
return invalidState(
|
|
3903
3996
|
"state.story.initialSceneId must be a non-empty string or null",
|
|
@@ -3907,7 +4000,7 @@ const runValidateState = ({ state }) => {
|
|
|
3907
4000
|
for (const collectionKey of COLLECTION_KEYS) {
|
|
3908
4001
|
{
|
|
3909
4002
|
const result = validateCollection({
|
|
3910
|
-
collection:
|
|
4003
|
+
collection: normalizedState[collectionKey],
|
|
3911
4004
|
path: `state.${collectionKey}`,
|
|
3912
4005
|
});
|
|
3913
4006
|
if (result?.valid === false) {
|
|
@@ -3916,7 +4009,7 @@ const runValidateState = ({ state }) => {
|
|
|
3916
4009
|
}
|
|
3917
4010
|
}
|
|
3918
4011
|
|
|
3919
|
-
const invariantResult = assertInvariants({ state });
|
|
4012
|
+
const invariantResult = assertInvariants({ state: normalizedState });
|
|
3920
4013
|
if (!invariantResult.valid) {
|
|
3921
4014
|
return invariantResult;
|
|
3922
4015
|
}
|
|
@@ -4904,10 +4997,10 @@ const validateFileCreateData = ({ data, errorFactory }) => {
|
|
|
4904
4997
|
);
|
|
4905
4998
|
}
|
|
4906
4999
|
|
|
4907
|
-
if (data.type !==
|
|
5000
|
+
if (data.type !== undefined && !isNonEmptyString(data.type)) {
|
|
4908
5001
|
return invalidFromErrorFactory(
|
|
4909
5002
|
errorFactory,
|
|
4910
|
-
"payload.data.type must be
|
|
5003
|
+
"payload.data.type must be a non-empty string when provided",
|
|
4911
5004
|
);
|
|
4912
5005
|
}
|
|
4913
5006
|
|
|
@@ -4962,7 +5055,6 @@ const validateReferencedFilesInData = ({
|
|
|
4962
5055
|
state,
|
|
4963
5056
|
data,
|
|
4964
5057
|
fields,
|
|
4965
|
-
fieldTypes = {},
|
|
4966
5058
|
nullableFields = [],
|
|
4967
5059
|
details = {},
|
|
4968
5060
|
errorFactory = createPreconditionValidationError,
|
|
@@ -4982,7 +5074,6 @@ const validateReferencedFilesInData = ({
|
|
|
4982
5074
|
state,
|
|
4983
5075
|
fileId,
|
|
4984
5076
|
path: `payload.data.${field}`,
|
|
4985
|
-
allowedTypes: fieldTypes[field],
|
|
4986
5077
|
details: {
|
|
4987
5078
|
...details,
|
|
4988
5079
|
field,
|
|
@@ -5928,7 +6019,7 @@ const validateLayoutCreateData = ({ data, errorFactory }) => {
|
|
|
5928
6019
|
allowedKeys:
|
|
5929
6020
|
data.type === "folder"
|
|
5930
6021
|
? ["type", "name"]
|
|
5931
|
-
: ["type", "name", "layoutType", "elements"
|
|
6022
|
+
: ["type", "name", "layoutType", "elements"],
|
|
5932
6023
|
path: "payload.data",
|
|
5933
6024
|
errorFactory,
|
|
5934
6025
|
});
|
|
@@ -5948,7 +6039,7 @@ const validateLayoutCreateData = ({ data, errorFactory }) => {
|
|
|
5948
6039
|
if (!LAYOUT_TYPE_KEYS.includes(data.layoutType)) {
|
|
5949
6040
|
return invalidFromErrorFactory(
|
|
5950
6041
|
errorFactory,
|
|
5951
|
-
"payload.data.layoutType must be 'normal', 'dialogue', 'nvl',
|
|
6042
|
+
"payload.data.layoutType must be 'normal', 'dialogue', 'nvl', or 'choice'",
|
|
5952
6043
|
);
|
|
5953
6044
|
}
|
|
5954
6045
|
|
|
@@ -5965,16 +6056,6 @@ const validateLayoutCreateData = ({ data, errorFactory }) => {
|
|
|
5965
6056
|
}
|
|
5966
6057
|
}
|
|
5967
6058
|
|
|
5968
|
-
{
|
|
5969
|
-
const result = validateKeyboardMap({
|
|
5970
|
-
value: data.keyboard,
|
|
5971
|
-
path: "payload.data.keyboard",
|
|
5972
|
-
errorFactory,
|
|
5973
|
-
});
|
|
5974
|
-
if (result?.valid === false) {
|
|
5975
|
-
return result;
|
|
5976
|
-
}
|
|
5977
|
-
}
|
|
5978
6059
|
}
|
|
5979
6060
|
};
|
|
5980
6061
|
|
|
@@ -5982,7 +6063,7 @@ const validateLayoutUpdateData = ({ data, errorFactory }) => {
|
|
|
5982
6063
|
{
|
|
5983
6064
|
const result = validateAllowedKeys({
|
|
5984
6065
|
value: data,
|
|
5985
|
-
allowedKeys: ["name", "layoutType"
|
|
6066
|
+
allowedKeys: ["name", "layoutType"],
|
|
5986
6067
|
path: "payload.data",
|
|
5987
6068
|
errorFactory,
|
|
5988
6069
|
});
|
|
@@ -6011,7 +6092,100 @@ const validateLayoutUpdateData = ({ data, errorFactory }) => {
|
|
|
6011
6092
|
) {
|
|
6012
6093
|
return invalidFromErrorFactory(
|
|
6013
6094
|
errorFactory,
|
|
6014
|
-
"payload.data.layoutType must be 'normal', 'dialogue', 'nvl',
|
|
6095
|
+
"payload.data.layoutType must be 'normal', 'dialogue', 'nvl', or 'choice' when provided",
|
|
6096
|
+
);
|
|
6097
|
+
}
|
|
6098
|
+
|
|
6099
|
+
};
|
|
6100
|
+
|
|
6101
|
+
const validateControlCreateData = ({ data, errorFactory }) => {
|
|
6102
|
+
if (!isPlainObject(data)) {
|
|
6103
|
+
return invalidFromErrorFactory(
|
|
6104
|
+
errorFactory,
|
|
6105
|
+
"payload.data must be an object",
|
|
6106
|
+
);
|
|
6107
|
+
}
|
|
6108
|
+
|
|
6109
|
+
if (data.type !== "folder" && data.type !== "control") {
|
|
6110
|
+
return invalidFromErrorFactory(
|
|
6111
|
+
errorFactory,
|
|
6112
|
+
"payload.data.type must be 'folder' or 'control'",
|
|
6113
|
+
);
|
|
6114
|
+
}
|
|
6115
|
+
|
|
6116
|
+
{
|
|
6117
|
+
const result = validateAllowedKeys({
|
|
6118
|
+
value: data,
|
|
6119
|
+
allowedKeys:
|
|
6120
|
+
data.type === "folder"
|
|
6121
|
+
? ["type", "name"]
|
|
6122
|
+
: ["type", "name", "elements", "keyboard"],
|
|
6123
|
+
path: "payload.data",
|
|
6124
|
+
errorFactory,
|
|
6125
|
+
});
|
|
6126
|
+
if (result?.valid === false) {
|
|
6127
|
+
return result;
|
|
6128
|
+
}
|
|
6129
|
+
}
|
|
6130
|
+
|
|
6131
|
+
if (!isNonEmptyString(data.name)) {
|
|
6132
|
+
return invalidFromErrorFactory(
|
|
6133
|
+
errorFactory,
|
|
6134
|
+
"payload.data.name must be a non-empty string",
|
|
6135
|
+
);
|
|
6136
|
+
}
|
|
6137
|
+
|
|
6138
|
+
if (data.type === "control") {
|
|
6139
|
+
{
|
|
6140
|
+
const result = validateNestedCollection({
|
|
6141
|
+
collection: data.elements,
|
|
6142
|
+
path: "payload.data.elements",
|
|
6143
|
+
itemValidator: validateLayoutElementItems,
|
|
6144
|
+
treeValidator: validateLayoutElementTreeOwnership,
|
|
6145
|
+
errorFactory,
|
|
6146
|
+
});
|
|
6147
|
+
if (result?.valid === false) {
|
|
6148
|
+
return result;
|
|
6149
|
+
}
|
|
6150
|
+
}
|
|
6151
|
+
|
|
6152
|
+
{
|
|
6153
|
+
const result = validateKeyboardMap({
|
|
6154
|
+
value: data.keyboard,
|
|
6155
|
+
path: "payload.data.keyboard",
|
|
6156
|
+
errorFactory,
|
|
6157
|
+
});
|
|
6158
|
+
if (result?.valid === false) {
|
|
6159
|
+
return result;
|
|
6160
|
+
}
|
|
6161
|
+
}
|
|
6162
|
+
}
|
|
6163
|
+
};
|
|
6164
|
+
|
|
6165
|
+
const validateControlUpdateData = ({ data, errorFactory }) => {
|
|
6166
|
+
{
|
|
6167
|
+
const result = validateAllowedKeys({
|
|
6168
|
+
value: data,
|
|
6169
|
+
allowedKeys: ["name", "keyboard"],
|
|
6170
|
+
path: "payload.data",
|
|
6171
|
+
errorFactory,
|
|
6172
|
+
});
|
|
6173
|
+
if (result?.valid === false) {
|
|
6174
|
+
return result;
|
|
6175
|
+
}
|
|
6176
|
+
}
|
|
6177
|
+
|
|
6178
|
+
if (Object.keys(data).length === 0) {
|
|
6179
|
+
return invalidFromErrorFactory(
|
|
6180
|
+
errorFactory,
|
|
6181
|
+
"payload.data must include at least one updatable field",
|
|
6182
|
+
);
|
|
6183
|
+
}
|
|
6184
|
+
|
|
6185
|
+
if (data.name !== undefined && !isNonEmptyString(data.name)) {
|
|
6186
|
+
return invalidFromErrorFactory(
|
|
6187
|
+
errorFactory,
|
|
6188
|
+
"payload.data.name must be a non-empty string when provided",
|
|
6015
6189
|
);
|
|
6016
6190
|
}
|
|
6017
6191
|
|
|
@@ -6061,8 +6235,10 @@ const validateLayoutElementUpdateData = ({ data, errorFactory, replace }) => {
|
|
|
6061
6235
|
}
|
|
6062
6236
|
};
|
|
6063
6237
|
|
|
6064
|
-
const
|
|
6065
|
-
|
|
6238
|
+
const validateVisualElementReferenceTargets = ({
|
|
6239
|
+
ownerIdField,
|
|
6240
|
+
ownerId,
|
|
6241
|
+
ownerLabel,
|
|
6066
6242
|
elementId,
|
|
6067
6243
|
data,
|
|
6068
6244
|
state,
|
|
@@ -6073,9 +6249,9 @@ const validateLayoutElementReferenceTargets = ({
|
|
|
6073
6249
|
if (!isPlainObject(image) || image.type === "folder") {
|
|
6074
6250
|
return invalidFromErrorFactory(
|
|
6075
6251
|
errorFactory,
|
|
6076
|
-
|
|
6252
|
+
`${ownerLabel} element imageId must reference an existing non-folder image`,
|
|
6077
6253
|
{
|
|
6078
|
-
|
|
6254
|
+
[ownerIdField]: ownerId,
|
|
6079
6255
|
elementId,
|
|
6080
6256
|
field: "imageId",
|
|
6081
6257
|
targetId: data.imageId,
|
|
@@ -6089,9 +6265,9 @@ const validateLayoutElementReferenceTargets = ({
|
|
|
6089
6265
|
if (!isPlainObject(image) || image.type === "folder") {
|
|
6090
6266
|
return invalidFromErrorFactory(
|
|
6091
6267
|
errorFactory,
|
|
6092
|
-
|
|
6268
|
+
`${ownerLabel} element hoverImageId must reference an existing non-folder image`,
|
|
6093
6269
|
{
|
|
6094
|
-
|
|
6270
|
+
[ownerIdField]: ownerId,
|
|
6095
6271
|
elementId,
|
|
6096
6272
|
field: "hoverImageId",
|
|
6097
6273
|
targetId: data.hoverImageId,
|
|
@@ -6105,9 +6281,9 @@ const validateLayoutElementReferenceTargets = ({
|
|
|
6105
6281
|
if (!isPlainObject(image) || image.type === "folder") {
|
|
6106
6282
|
return invalidFromErrorFactory(
|
|
6107
6283
|
errorFactory,
|
|
6108
|
-
|
|
6284
|
+
`${ownerLabel} element clickImageId must reference an existing non-folder image`,
|
|
6109
6285
|
{
|
|
6110
|
-
|
|
6286
|
+
[ownerIdField]: ownerId,
|
|
6111
6287
|
elementId,
|
|
6112
6288
|
field: "clickImageId",
|
|
6113
6289
|
targetId: data.clickImageId,
|
|
@@ -6121,9 +6297,9 @@ const validateLayoutElementReferenceTargets = ({
|
|
|
6121
6297
|
if (!isPlainObject(image) || image.type === "folder") {
|
|
6122
6298
|
return invalidFromErrorFactory(
|
|
6123
6299
|
errorFactory,
|
|
6124
|
-
|
|
6300
|
+
`${ownerLabel} element thumbImageId must reference an existing non-folder image`,
|
|
6125
6301
|
{
|
|
6126
|
-
|
|
6302
|
+
[ownerIdField]: ownerId,
|
|
6127
6303
|
elementId,
|
|
6128
6304
|
field: "thumbImageId",
|
|
6129
6305
|
targetId: data.thumbImageId,
|
|
@@ -6137,9 +6313,9 @@ const validateLayoutElementReferenceTargets = ({
|
|
|
6137
6313
|
if (!isPlainObject(image) || image.type === "folder") {
|
|
6138
6314
|
return invalidFromErrorFactory(
|
|
6139
6315
|
errorFactory,
|
|
6140
|
-
|
|
6316
|
+
`${ownerLabel} element hoverThumbImageId must reference an existing non-folder image`,
|
|
6141
6317
|
{
|
|
6142
|
-
|
|
6318
|
+
[ownerIdField]: ownerId,
|
|
6143
6319
|
elementId,
|
|
6144
6320
|
field: "hoverThumbImageId",
|
|
6145
6321
|
targetId: data.hoverThumbImageId,
|
|
@@ -6153,9 +6329,9 @@ const validateLayoutElementReferenceTargets = ({
|
|
|
6153
6329
|
if (!isPlainObject(image) || image.type === "folder") {
|
|
6154
6330
|
return invalidFromErrorFactory(
|
|
6155
6331
|
errorFactory,
|
|
6156
|
-
|
|
6332
|
+
`${ownerLabel} element barImageId must reference an existing non-folder image`,
|
|
6157
6333
|
{
|
|
6158
|
-
|
|
6334
|
+
[ownerIdField]: ownerId,
|
|
6159
6335
|
elementId,
|
|
6160
6336
|
field: "barImageId",
|
|
6161
6337
|
targetId: data.barImageId,
|
|
@@ -6169,9 +6345,9 @@ const validateLayoutElementReferenceTargets = ({
|
|
|
6169
6345
|
if (!isPlainObject(image) || image.type === "folder") {
|
|
6170
6346
|
return invalidFromErrorFactory(
|
|
6171
6347
|
errorFactory,
|
|
6172
|
-
|
|
6348
|
+
`${ownerLabel} element hoverBarImageId must reference an existing non-folder image`,
|
|
6173
6349
|
{
|
|
6174
|
-
|
|
6350
|
+
[ownerIdField]: ownerId,
|
|
6175
6351
|
elementId,
|
|
6176
6352
|
field: "hoverBarImageId",
|
|
6177
6353
|
targetId: data.hoverBarImageId,
|
|
@@ -6185,9 +6361,9 @@ const validateLayoutElementReferenceTargets = ({
|
|
|
6185
6361
|
if (!isPlainObject(textStyle) || textStyle.type === "folder") {
|
|
6186
6362
|
return invalidFromErrorFactory(
|
|
6187
6363
|
errorFactory,
|
|
6188
|
-
|
|
6364
|
+
`${ownerLabel} element textStyleId must reference an existing non-folder text style`,
|
|
6189
6365
|
{
|
|
6190
|
-
|
|
6366
|
+
[ownerIdField]: ownerId,
|
|
6191
6367
|
elementId,
|
|
6192
6368
|
field: "textStyleId",
|
|
6193
6369
|
targetId: data.textStyleId,
|
|
@@ -6201,9 +6377,9 @@ const validateLayoutElementReferenceTargets = ({
|
|
|
6201
6377
|
if (!isPlainObject(textStyle) || textStyle.type === "folder") {
|
|
6202
6378
|
return invalidFromErrorFactory(
|
|
6203
6379
|
errorFactory,
|
|
6204
|
-
|
|
6380
|
+
`${ownerLabel} element hoverTextStyleId must reference an existing non-folder text style`,
|
|
6205
6381
|
{
|
|
6206
|
-
|
|
6382
|
+
[ownerIdField]: ownerId,
|
|
6207
6383
|
elementId,
|
|
6208
6384
|
field: "hoverTextStyleId",
|
|
6209
6385
|
targetId: data.hoverTextStyleId,
|
|
@@ -6217,9 +6393,9 @@ const validateLayoutElementReferenceTargets = ({
|
|
|
6217
6393
|
if (!isPlainObject(textStyle) || textStyle.type === "folder") {
|
|
6218
6394
|
return invalidFromErrorFactory(
|
|
6219
6395
|
errorFactory,
|
|
6220
|
-
|
|
6396
|
+
`${ownerLabel} element clickTextStyleId must reference an existing non-folder text style`,
|
|
6221
6397
|
{
|
|
6222
|
-
|
|
6398
|
+
[ownerIdField]: ownerId,
|
|
6223
6399
|
elementId,
|
|
6224
6400
|
field: "clickTextStyleId",
|
|
6225
6401
|
targetId: data.clickTextStyleId,
|
|
@@ -6233,9 +6409,9 @@ const validateLayoutElementReferenceTargets = ({
|
|
|
6233
6409
|
if (!isPlainObject(variable) || variable.type === "folder") {
|
|
6234
6410
|
return invalidFromErrorFactory(
|
|
6235
6411
|
errorFactory,
|
|
6236
|
-
|
|
6412
|
+
`${ownerLabel} element variableId must reference an existing non-folder variable`,
|
|
6237
6413
|
{
|
|
6238
|
-
|
|
6414
|
+
[ownerIdField]: ownerId,
|
|
6239
6415
|
elementId,
|
|
6240
6416
|
variableId: data.variableId,
|
|
6241
6417
|
},
|
|
@@ -6774,6 +6950,9 @@ const getCharacterSpriteCollection = ({ state, characterId }) =>
|
|
|
6774
6950
|
const getLayoutElementCollection = ({ state, layoutId }) =>
|
|
6775
6951
|
state.layouts.items[layoutId]?.elements;
|
|
6776
6952
|
|
|
6953
|
+
const getControlElementCollection = ({ state, controlId }) =>
|
|
6954
|
+
state.controls.items[controlId]?.elements;
|
|
6955
|
+
|
|
6777
6956
|
const findReferencedFileUsage = ({ state, fileId }) => {
|
|
6778
6957
|
for (const [imageId, image] of Object.entries(state.images.items)) {
|
|
6779
6958
|
if (image.type !== "image") {
|
|
@@ -6954,7 +7133,6 @@ const COMMAND_DEFINITIONS = [
|
|
|
6954
7133
|
}
|
|
6955
7134
|
: {
|
|
6956
7135
|
id: payload.fileId,
|
|
6957
|
-
type: payload.data.type,
|
|
6958
7136
|
mimeType: payload.data.mimeType,
|
|
6959
7137
|
size: payload.data.size,
|
|
6960
7138
|
sha256: payload.data.sha256,
|
|
@@ -8304,7 +8482,6 @@ const COMMAND_DEFINITIONS = [
|
|
|
8304
8482
|
state,
|
|
8305
8483
|
data: payload.data,
|
|
8306
8484
|
fields: ["fileId", "thumbnailFileId"],
|
|
8307
|
-
fieldTypes: IMAGE_FILE_REFERENCE_TYPES,
|
|
8308
8485
|
details: {
|
|
8309
8486
|
imageId: payload.imageId,
|
|
8310
8487
|
},
|
|
@@ -8417,7 +8594,6 @@ const COMMAND_DEFINITIONS = [
|
|
|
8417
8594
|
state,
|
|
8418
8595
|
data: payload.data,
|
|
8419
8596
|
fields: ["fileId", "thumbnailFileId"],
|
|
8420
|
-
fieldTypes: IMAGE_FILE_REFERENCE_TYPES,
|
|
8421
8597
|
details: {
|
|
8422
8598
|
imageId: payload.imageId,
|
|
8423
8599
|
},
|
|
@@ -8730,7 +8906,6 @@ const COMMAND_DEFINITIONS = [
|
|
|
8730
8906
|
state,
|
|
8731
8907
|
data: payload.data,
|
|
8732
8908
|
fields: ["fileId", "waveformDataFileId"],
|
|
8733
|
-
fieldTypes: SOUND_FILE_REFERENCE_TYPES,
|
|
8734
8909
|
nullableFields: ["waveformDataFileId"],
|
|
8735
8910
|
details: {
|
|
8736
8911
|
soundId: payload.soundId,
|
|
@@ -8840,7 +9015,6 @@ const COMMAND_DEFINITIONS = [
|
|
|
8840
9015
|
state,
|
|
8841
9016
|
data: payload.data,
|
|
8842
9017
|
fields: ["fileId", "waveformDataFileId"],
|
|
8843
|
-
fieldTypes: SOUND_FILE_REFERENCE_TYPES,
|
|
8844
9018
|
nullableFields: ["waveformDataFileId"],
|
|
8845
9019
|
details: {
|
|
8846
9020
|
soundId: payload.soundId,
|
|
@@ -9154,7 +9328,6 @@ const COMMAND_DEFINITIONS = [
|
|
|
9154
9328
|
state,
|
|
9155
9329
|
data: payload.data,
|
|
9156
9330
|
fields: ["fileId", "thumbnailFileId"],
|
|
9157
|
-
fieldTypes: VIDEO_FILE_REFERENCE_TYPES,
|
|
9158
9331
|
details: {
|
|
9159
9332
|
videoId: payload.videoId,
|
|
9160
9333
|
},
|
|
@@ -9265,7 +9438,6 @@ const COMMAND_DEFINITIONS = [
|
|
|
9265
9438
|
state,
|
|
9266
9439
|
data: payload.data,
|
|
9267
9440
|
fields: ["fileId", "thumbnailFileId"],
|
|
9268
|
-
fieldTypes: VIDEO_FILE_REFERENCE_TYPES,
|
|
9269
9441
|
details: {
|
|
9270
9442
|
videoId: payload.videoId,
|
|
9271
9443
|
},
|
|
@@ -9952,7 +10124,6 @@ const COMMAND_DEFINITIONS = [
|
|
|
9952
10124
|
state,
|
|
9953
10125
|
data: payload.data,
|
|
9954
10126
|
fields: ["fileId"],
|
|
9955
|
-
fieldTypes: FONT_FILE_REFERENCE_TYPES,
|
|
9956
10127
|
details: {
|
|
9957
10128
|
fontId: payload.fontId,
|
|
9958
10129
|
},
|
|
@@ -10051,7 +10222,6 @@ const COMMAND_DEFINITIONS = [
|
|
|
10051
10222
|
state,
|
|
10052
10223
|
data: payload.data,
|
|
10053
10224
|
fields: ["fileId"],
|
|
10054
|
-
fieldTypes: FONT_FILE_REFERENCE_TYPES,
|
|
10055
10225
|
details: {
|
|
10056
10226
|
fontId: payload.fontId,
|
|
10057
10227
|
},
|
|
@@ -10837,7 +11007,6 @@ const COMMAND_DEFINITIONS = [
|
|
|
10837
11007
|
state,
|
|
10838
11008
|
data: payload.data,
|
|
10839
11009
|
fields: ["fileId"],
|
|
10840
|
-
fieldTypes: CHARACTER_FILE_REFERENCE_TYPES,
|
|
10841
11010
|
details: {
|
|
10842
11011
|
characterId: payload.characterId,
|
|
10843
11012
|
},
|
|
@@ -10858,7 +11027,6 @@ const COMMAND_DEFINITIONS = [
|
|
|
10858
11027
|
state,
|
|
10859
11028
|
fileId: sprite.fileId,
|
|
10860
11029
|
path: "payload.data.sprites.items.*.fileId",
|
|
10861
|
-
allowedTypes: CHARACTER_FILE_REFERENCE_TYPES.fileId,
|
|
10862
11030
|
details: {
|
|
10863
11031
|
characterId: payload.characterId,
|
|
10864
11032
|
spriteId,
|
|
@@ -10885,7 +11053,6 @@ const COMMAND_DEFINITIONS = [
|
|
|
10885
11053
|
state,
|
|
10886
11054
|
data: payload.data,
|
|
10887
11055
|
fields: ["fileId"],
|
|
10888
|
-
fieldTypes: CHARACTER_FILE_REFERENCE_TYPES,
|
|
10889
11056
|
details: {
|
|
10890
11057
|
characterId: payload.characterId,
|
|
10891
11058
|
},
|
|
@@ -10911,11 +11078,6 @@ const COMMAND_DEFINITIONS = [
|
|
|
10911
11078
|
? {
|
|
10912
11079
|
layoutType: payload.data.layoutType,
|
|
10913
11080
|
elements: structuredClone(payload.data.elements),
|
|
10914
|
-
...(payload.data.keyboard !== undefined
|
|
10915
|
-
? {
|
|
10916
|
-
keyboard: structuredClone(payload.data.keyboard),
|
|
10917
|
-
}
|
|
10918
|
-
: {}),
|
|
10919
11081
|
}
|
|
10920
11082
|
: {}),
|
|
10921
11083
|
}),
|
|
@@ -10930,6 +11092,39 @@ const COMMAND_DEFINITIONS = [
|
|
|
10930
11092
|
}
|
|
10931
11093
|
},
|
|
10932
11094
|
}),
|
|
11095
|
+
...createFolderedCollectionCommandDefinitions({
|
|
11096
|
+
familyName: "control",
|
|
11097
|
+
collectionKey: "controls",
|
|
11098
|
+
idField: "controlId",
|
|
11099
|
+
itemLabel: "control item",
|
|
11100
|
+
createDataValidator: validateControlCreateData,
|
|
11101
|
+
updateDataValidator: validateControlUpdateData,
|
|
11102
|
+
createItem: ({ payload }) => ({
|
|
11103
|
+
id: payload.controlId,
|
|
11104
|
+
type: payload.data.type,
|
|
11105
|
+
name: payload.data.name,
|
|
11106
|
+
...(payload.data.type === "control"
|
|
11107
|
+
? {
|
|
11108
|
+
elements: structuredClone(payload.data.elements),
|
|
11109
|
+
...(payload.data.keyboard !== undefined
|
|
11110
|
+
? {
|
|
11111
|
+
keyboard: structuredClone(payload.data.keyboard),
|
|
11112
|
+
}
|
|
11113
|
+
: {}),
|
|
11114
|
+
}
|
|
11115
|
+
: {}),
|
|
11116
|
+
}),
|
|
11117
|
+
validateUpdateState: ({ payload, currentItem }) => {
|
|
11118
|
+
if (
|
|
11119
|
+
currentItem.type === "folder" &&
|
|
11120
|
+
Object.keys(payload.data).some((key) => key !== "name")
|
|
11121
|
+
) {
|
|
11122
|
+
return invalidPrecondition(
|
|
11123
|
+
"folder control items cannot update control fields",
|
|
11124
|
+
);
|
|
11125
|
+
}
|
|
11126
|
+
},
|
|
11127
|
+
}),
|
|
10933
11128
|
{
|
|
10934
11129
|
type: "character.sprite.create",
|
|
10935
11130
|
validatePayload: ({ payload }) => {
|
|
@@ -11042,7 +11237,6 @@ const COMMAND_DEFINITIONS = [
|
|
|
11042
11237
|
state,
|
|
11043
11238
|
data: payload.data,
|
|
11044
11239
|
fields: ["fileId"],
|
|
11045
|
-
fieldTypes: CHARACTER_FILE_REFERENCE_TYPES,
|
|
11046
11240
|
details: {
|
|
11047
11241
|
characterId: payload.characterId,
|
|
11048
11242
|
spriteId: payload.spriteId,
|
|
@@ -11146,7 +11340,6 @@ const COMMAND_DEFINITIONS = [
|
|
|
11146
11340
|
state,
|
|
11147
11341
|
data: payload.data,
|
|
11148
11342
|
fields: ["fileId"],
|
|
11149
|
-
fieldTypes: CHARACTER_FILE_REFERENCE_TYPES,
|
|
11150
11343
|
details: {
|
|
11151
11344
|
characterId: payload.characterId,
|
|
11152
11345
|
spriteId: payload.spriteId,
|
|
@@ -11510,8 +11703,10 @@ const COMMAND_DEFINITIONS = [
|
|
|
11510
11703
|
}
|
|
11511
11704
|
|
|
11512
11705
|
{
|
|
11513
|
-
const result =
|
|
11514
|
-
|
|
11706
|
+
const result = validateVisualElementReferenceTargets({
|
|
11707
|
+
ownerIdField: "layoutId",
|
|
11708
|
+
ownerId: payload.layoutId,
|
|
11709
|
+
ownerLabel: "layout",
|
|
11515
11710
|
elementId: payload.elementId,
|
|
11516
11711
|
data: payload.data,
|
|
11517
11712
|
state,
|
|
@@ -11628,8 +11823,10 @@ const COMMAND_DEFINITIONS = [
|
|
|
11628
11823
|
};
|
|
11629
11824
|
|
|
11630
11825
|
{
|
|
11631
|
-
const result =
|
|
11632
|
-
|
|
11826
|
+
const result = validateVisualElementReferenceTargets({
|
|
11827
|
+
ownerIdField: "layoutId",
|
|
11828
|
+
ownerId: payload.layoutId,
|
|
11829
|
+
ownerLabel: "layout",
|
|
11633
11830
|
elementId: payload.elementId,
|
|
11634
11831
|
data: mergedData,
|
|
11635
11832
|
state,
|
|
@@ -11754,6 +11951,515 @@ const COMMAND_DEFINITIONS = [
|
|
|
11754
11951
|
return state;
|
|
11755
11952
|
},
|
|
11756
11953
|
},
|
|
11954
|
+
{
|
|
11955
|
+
type: "control.element.create",
|
|
11956
|
+
validatePayload: ({ payload }) => {
|
|
11957
|
+
{
|
|
11958
|
+
const result = validateAllowedKeys({
|
|
11959
|
+
value: payload,
|
|
11960
|
+
allowedKeys: [
|
|
11961
|
+
"controlId",
|
|
11962
|
+
"elementId",
|
|
11963
|
+
"parentId",
|
|
11964
|
+
"data",
|
|
11965
|
+
"index",
|
|
11966
|
+
"position",
|
|
11967
|
+
"positionTargetId",
|
|
11968
|
+
],
|
|
11969
|
+
path: "payload",
|
|
11970
|
+
errorFactory: createPayloadValidationError,
|
|
11971
|
+
});
|
|
11972
|
+
if (result?.valid === false) {
|
|
11973
|
+
return result;
|
|
11974
|
+
}
|
|
11975
|
+
}
|
|
11976
|
+
|
|
11977
|
+
if (!isNonEmptyString(payload.controlId)) {
|
|
11978
|
+
return invalidPayload("payload.controlId must be a non-empty string");
|
|
11979
|
+
}
|
|
11980
|
+
|
|
11981
|
+
if (!isNonEmptyString(payload.elementId)) {
|
|
11982
|
+
return invalidPayload("payload.elementId must be a non-empty string");
|
|
11983
|
+
}
|
|
11984
|
+
|
|
11985
|
+
if (
|
|
11986
|
+
payload.parentId !== undefined &&
|
|
11987
|
+
payload.parentId !== null &&
|
|
11988
|
+
!isNonEmptyString(payload.parentId)
|
|
11989
|
+
) {
|
|
11990
|
+
return invalidPayload(
|
|
11991
|
+
"payload.parentId must be a non-empty string when provided",
|
|
11992
|
+
);
|
|
11993
|
+
}
|
|
11994
|
+
|
|
11995
|
+
{
|
|
11996
|
+
const result = validateLayoutElementCreateData({
|
|
11997
|
+
data: payload.data,
|
|
11998
|
+
errorFactory: createPayloadValidationError,
|
|
11999
|
+
});
|
|
12000
|
+
if (result?.valid === false) {
|
|
12001
|
+
return result;
|
|
12002
|
+
}
|
|
12003
|
+
}
|
|
12004
|
+
|
|
12005
|
+
{
|
|
12006
|
+
const result = validatePlacementFields({
|
|
12007
|
+
payload,
|
|
12008
|
+
errorFactory: createPayloadValidationError,
|
|
12009
|
+
});
|
|
12010
|
+
if (result?.valid === false) {
|
|
12011
|
+
return result;
|
|
12012
|
+
}
|
|
12013
|
+
}
|
|
12014
|
+
},
|
|
12015
|
+
validateAgainstState: ({ state, payload }) => {
|
|
12016
|
+
const control = state.controls.items[payload.controlId];
|
|
12017
|
+
if (!isPlainObject(control) || control.type !== "control") {
|
|
12018
|
+
return invalidPrecondition(
|
|
12019
|
+
"payload.controlId must reference an existing control",
|
|
12020
|
+
);
|
|
12021
|
+
}
|
|
12022
|
+
|
|
12023
|
+
const collection = getControlElementCollection({
|
|
12024
|
+
state,
|
|
12025
|
+
controlId: payload.controlId,
|
|
12026
|
+
});
|
|
12027
|
+
|
|
12028
|
+
if (isPlainObject(collection.items[payload.elementId])) {
|
|
12029
|
+
return invalidPrecondition("payload.elementId must not already exist");
|
|
12030
|
+
}
|
|
12031
|
+
|
|
12032
|
+
const parentId = payload.parentId ?? null;
|
|
12033
|
+
if (parentId !== null) {
|
|
12034
|
+
const parentItem = collection.items[parentId];
|
|
12035
|
+
if (
|
|
12036
|
+
!isPlainObject(parentItem) ||
|
|
12037
|
+
!LAYOUT_CONTAINER_ELEMENT_TYPES.includes(parentItem.type)
|
|
12038
|
+
) {
|
|
12039
|
+
return invalidPrecondition(
|
|
12040
|
+
"payload.parentId must reference a folder or container control element",
|
|
12041
|
+
);
|
|
12042
|
+
}
|
|
12043
|
+
}
|
|
12044
|
+
|
|
12045
|
+
if (payload.positionTargetId !== undefined) {
|
|
12046
|
+
if (!isPlainObject(collection.items[payload.positionTargetId])) {
|
|
12047
|
+
return invalidPrecondition(
|
|
12048
|
+
"payload.positionTargetId must reference an existing control element",
|
|
12049
|
+
);
|
|
12050
|
+
}
|
|
12051
|
+
|
|
12052
|
+
const targetParentId = getNodeParentId({
|
|
12053
|
+
tree: collection.tree,
|
|
12054
|
+
nodeId: payload.positionTargetId,
|
|
12055
|
+
});
|
|
12056
|
+
|
|
12057
|
+
if (targetParentId !== parentId) {
|
|
12058
|
+
return invalidPrecondition(
|
|
12059
|
+
"payload.positionTargetId must reference a sibling under payload.parentId",
|
|
12060
|
+
);
|
|
12061
|
+
}
|
|
12062
|
+
}
|
|
12063
|
+
|
|
12064
|
+
{
|
|
12065
|
+
const result = validateVisualElementReferenceTargets({
|
|
12066
|
+
ownerIdField: "controlId",
|
|
12067
|
+
ownerId: payload.controlId,
|
|
12068
|
+
ownerLabel: "control",
|
|
12069
|
+
elementId: payload.elementId,
|
|
12070
|
+
data: payload.data,
|
|
12071
|
+
state,
|
|
12072
|
+
errorFactory: createPreconditionValidationError,
|
|
12073
|
+
});
|
|
12074
|
+
if (result?.valid === false) {
|
|
12075
|
+
return result;
|
|
12076
|
+
}
|
|
12077
|
+
}
|
|
12078
|
+
},
|
|
12079
|
+
reduce: ({ state, payload }) => {
|
|
12080
|
+
const collection = getControlElementCollection({
|
|
12081
|
+
state,
|
|
12082
|
+
controlId: payload.controlId,
|
|
12083
|
+
});
|
|
12084
|
+
|
|
12085
|
+
collection.items[payload.elementId] = {
|
|
12086
|
+
id: payload.elementId,
|
|
12087
|
+
...structuredClone(payload.data),
|
|
12088
|
+
};
|
|
12089
|
+
|
|
12090
|
+
insertTreeNode({
|
|
12091
|
+
tree: collection.tree,
|
|
12092
|
+
node: {
|
|
12093
|
+
id: payload.elementId,
|
|
12094
|
+
children: [],
|
|
12095
|
+
},
|
|
12096
|
+
parentId: payload.parentId ?? null,
|
|
12097
|
+
index: payload.index,
|
|
12098
|
+
position: payload.position,
|
|
12099
|
+
positionTargetId: payload.positionTargetId,
|
|
12100
|
+
});
|
|
12101
|
+
|
|
12102
|
+
return state;
|
|
12103
|
+
},
|
|
12104
|
+
},
|
|
12105
|
+
{
|
|
12106
|
+
type: "control.element.update",
|
|
12107
|
+
validatePayload: ({ payload }) => {
|
|
12108
|
+
let result = captureValidation(() =>
|
|
12109
|
+
validateAllowedKeys({
|
|
12110
|
+
value: payload,
|
|
12111
|
+
allowedKeys: ["controlId", "elementId", "data", "replace"],
|
|
12112
|
+
path: "payload",
|
|
12113
|
+
errorFactory: createPayloadValidationError,
|
|
12114
|
+
}),
|
|
12115
|
+
);
|
|
12116
|
+
if (!result.valid) {
|
|
12117
|
+
return result;
|
|
12118
|
+
}
|
|
12119
|
+
|
|
12120
|
+
if (!isNonEmptyString(payload.controlId)) {
|
|
12121
|
+
return invalidPayload("payload.controlId must be a non-empty string");
|
|
12122
|
+
}
|
|
12123
|
+
|
|
12124
|
+
if (!isNonEmptyString(payload.elementId)) {
|
|
12125
|
+
return invalidPayload("payload.elementId must be a non-empty string");
|
|
12126
|
+
}
|
|
12127
|
+
|
|
12128
|
+
if (
|
|
12129
|
+
payload.replace !== undefined &&
|
|
12130
|
+
typeof payload.replace !== "boolean"
|
|
12131
|
+
) {
|
|
12132
|
+
return invalidPayload(
|
|
12133
|
+
"payload.replace must be a boolean when provided",
|
|
12134
|
+
);
|
|
12135
|
+
}
|
|
12136
|
+
|
|
12137
|
+
result = captureValidation(() =>
|
|
12138
|
+
validateLayoutElementUpdateData({
|
|
12139
|
+
data: payload.data,
|
|
12140
|
+
replace: payload.replace,
|
|
12141
|
+
errorFactory: createPayloadValidationError,
|
|
12142
|
+
}),
|
|
12143
|
+
);
|
|
12144
|
+
if (!result.valid) {
|
|
12145
|
+
return result;
|
|
12146
|
+
}
|
|
12147
|
+
|
|
12148
|
+
return VALID_RESULT;
|
|
12149
|
+
},
|
|
12150
|
+
validateAgainstState: ({ state, payload }) => {
|
|
12151
|
+
const control = state.controls.items[payload.controlId];
|
|
12152
|
+
if (!isPlainObject(control) || control.type !== "control") {
|
|
12153
|
+
return invalidPrecondition(
|
|
12154
|
+
"payload.controlId must reference an existing control",
|
|
12155
|
+
);
|
|
12156
|
+
}
|
|
12157
|
+
|
|
12158
|
+
const collection = getControlElementCollection({
|
|
12159
|
+
state,
|
|
12160
|
+
controlId: payload.controlId,
|
|
12161
|
+
});
|
|
12162
|
+
const currentItem = collection.items[payload.elementId];
|
|
12163
|
+
if (!isPlainObject(currentItem)) {
|
|
12164
|
+
return invalidPrecondition(
|
|
12165
|
+
"payload.elementId must reference an existing control element",
|
|
12166
|
+
);
|
|
12167
|
+
}
|
|
12168
|
+
|
|
12169
|
+
if (
|
|
12170
|
+
payload.data.type !== undefined &&
|
|
12171
|
+
payload.data.type !== currentItem.type
|
|
12172
|
+
) {
|
|
12173
|
+
return invalidPrecondition("control element type cannot be changed");
|
|
12174
|
+
}
|
|
12175
|
+
|
|
12176
|
+
if (currentItem.type !== "folder") {
|
|
12177
|
+
const mergedData = payload.replace
|
|
12178
|
+
? { ...structuredClone(payload.data) }
|
|
12179
|
+
: {
|
|
12180
|
+
...structuredClone(currentItem),
|
|
12181
|
+
...structuredClone(payload.data),
|
|
12182
|
+
};
|
|
12183
|
+
|
|
12184
|
+
{
|
|
12185
|
+
const result = validateVisualElementReferenceTargets({
|
|
12186
|
+
ownerIdField: "controlId",
|
|
12187
|
+
ownerId: payload.controlId,
|
|
12188
|
+
ownerLabel: "control",
|
|
12189
|
+
elementId: payload.elementId,
|
|
12190
|
+
data: mergedData,
|
|
12191
|
+
state,
|
|
12192
|
+
errorFactory: createPreconditionValidationError,
|
|
12193
|
+
});
|
|
12194
|
+
if (result?.valid === false) {
|
|
12195
|
+
return result;
|
|
12196
|
+
}
|
|
12197
|
+
}
|
|
12198
|
+
}
|
|
12199
|
+
|
|
12200
|
+
if (
|
|
12201
|
+
currentItem.type === "folder" &&
|
|
12202
|
+
Object.keys(payload.data).some((key) => key !== "name")
|
|
12203
|
+
) {
|
|
12204
|
+
return invalidPrecondition(
|
|
12205
|
+
"folder control elements cannot update non-name fields",
|
|
12206
|
+
);
|
|
12207
|
+
}
|
|
12208
|
+
|
|
12209
|
+
return VALID_RESULT;
|
|
12210
|
+
},
|
|
12211
|
+
reduce: ({ state, payload }) => {
|
|
12212
|
+
const collection = getControlElementCollection({
|
|
12213
|
+
state,
|
|
12214
|
+
controlId: payload.controlId,
|
|
12215
|
+
});
|
|
12216
|
+
const currentItem = collection.items[payload.elementId];
|
|
12217
|
+
|
|
12218
|
+
collection.items[payload.elementId] =
|
|
12219
|
+
payload.replace === true
|
|
12220
|
+
? {
|
|
12221
|
+
id: payload.elementId,
|
|
12222
|
+
...structuredClone(payload.data),
|
|
12223
|
+
}
|
|
12224
|
+
: {
|
|
12225
|
+
...structuredClone(currentItem),
|
|
12226
|
+
...structuredClone(payload.data),
|
|
12227
|
+
};
|
|
12228
|
+
|
|
12229
|
+
return state;
|
|
12230
|
+
},
|
|
12231
|
+
},
|
|
12232
|
+
{
|
|
12233
|
+
type: "control.element.delete",
|
|
12234
|
+
validatePayload: ({ payload }) => {
|
|
12235
|
+
{
|
|
12236
|
+
const result = validateExactKeys({
|
|
12237
|
+
value: payload,
|
|
12238
|
+
expectedKeys: ["controlId", "elementIds"],
|
|
12239
|
+
path: "payload",
|
|
12240
|
+
errorFactory: createPayloadValidationError,
|
|
12241
|
+
});
|
|
12242
|
+
if (result?.valid === false) {
|
|
12243
|
+
return result;
|
|
12244
|
+
}
|
|
12245
|
+
}
|
|
12246
|
+
|
|
12247
|
+
if (!isNonEmptyString(payload.controlId)) {
|
|
12248
|
+
return invalidPayload("payload.controlId must be a non-empty string");
|
|
12249
|
+
}
|
|
12250
|
+
|
|
12251
|
+
{
|
|
12252
|
+
const result = validateRequiredUniqueIdArray({
|
|
12253
|
+
value: payload.elementIds,
|
|
12254
|
+
path: "payload.elementIds",
|
|
12255
|
+
errorFactory: createPayloadValidationError,
|
|
12256
|
+
});
|
|
12257
|
+
if (result?.valid === false) {
|
|
12258
|
+
return result;
|
|
12259
|
+
}
|
|
12260
|
+
}
|
|
12261
|
+
},
|
|
12262
|
+
validateAgainstState: ({ state, payload }) => {
|
|
12263
|
+
const control = state.controls.items[payload.controlId];
|
|
12264
|
+
if (!isPlainObject(control) || control.type !== "control") {
|
|
12265
|
+
return invalidPrecondition(
|
|
12266
|
+
"payload.controlId must reference an existing control",
|
|
12267
|
+
);
|
|
12268
|
+
}
|
|
12269
|
+
|
|
12270
|
+
const collection = getControlElementCollection({
|
|
12271
|
+
state,
|
|
12272
|
+
controlId: payload.controlId,
|
|
12273
|
+
});
|
|
12274
|
+
|
|
12275
|
+
for (const elementId of payload.elementIds) {
|
|
12276
|
+
if (!isPlainObject(collection.items[elementId])) {
|
|
12277
|
+
return invalidPrecondition(
|
|
12278
|
+
"payload.elementIds must reference existing control elements",
|
|
12279
|
+
{ elementId },
|
|
12280
|
+
);
|
|
12281
|
+
}
|
|
12282
|
+
}
|
|
12283
|
+
},
|
|
12284
|
+
reduce: ({ state, payload }) => {
|
|
12285
|
+
const collection = getControlElementCollection({
|
|
12286
|
+
state,
|
|
12287
|
+
controlId: payload.controlId,
|
|
12288
|
+
});
|
|
12289
|
+
const deletedIds = new Set();
|
|
12290
|
+
|
|
12291
|
+
for (const elementId of payload.elementIds) {
|
|
12292
|
+
const removedNode = removeTreeNode({
|
|
12293
|
+
nodes: collection.tree,
|
|
12294
|
+
nodeId: elementId,
|
|
12295
|
+
});
|
|
12296
|
+
|
|
12297
|
+
if (!removedNode) {
|
|
12298
|
+
continue;
|
|
12299
|
+
}
|
|
12300
|
+
|
|
12301
|
+
for (const id of collectTreeDescendantIds({ node: removedNode })) {
|
|
12302
|
+
deletedIds.add(id);
|
|
12303
|
+
}
|
|
12304
|
+
}
|
|
12305
|
+
|
|
12306
|
+
for (const elementId of deletedIds) {
|
|
12307
|
+
delete collection.items[elementId];
|
|
12308
|
+
}
|
|
12309
|
+
|
|
12310
|
+
return state;
|
|
12311
|
+
},
|
|
12312
|
+
},
|
|
12313
|
+
{
|
|
12314
|
+
type: "control.element.move",
|
|
12315
|
+
validatePayload: ({ payload }) => {
|
|
12316
|
+
{
|
|
12317
|
+
const result = validateAllowedKeys({
|
|
12318
|
+
value: payload,
|
|
12319
|
+
allowedKeys: [
|
|
12320
|
+
"controlId",
|
|
12321
|
+
"elementId",
|
|
12322
|
+
"parentId",
|
|
12323
|
+
"index",
|
|
12324
|
+
"position",
|
|
12325
|
+
"positionTargetId",
|
|
12326
|
+
],
|
|
12327
|
+
path: "payload",
|
|
12328
|
+
errorFactory: createPayloadValidationError,
|
|
12329
|
+
});
|
|
12330
|
+
if (result?.valid === false) {
|
|
12331
|
+
return result;
|
|
12332
|
+
}
|
|
12333
|
+
}
|
|
12334
|
+
|
|
12335
|
+
if (!isNonEmptyString(payload.controlId)) {
|
|
12336
|
+
return invalidPayload("payload.controlId must be a non-empty string");
|
|
12337
|
+
}
|
|
12338
|
+
|
|
12339
|
+
if (!isNonEmptyString(payload.elementId)) {
|
|
12340
|
+
return invalidPayload("payload.elementId must be a non-empty string");
|
|
12341
|
+
}
|
|
12342
|
+
|
|
12343
|
+
if (
|
|
12344
|
+
payload.parentId !== undefined &&
|
|
12345
|
+
payload.parentId !== null &&
|
|
12346
|
+
!isNonEmptyString(payload.parentId)
|
|
12347
|
+
) {
|
|
12348
|
+
return invalidPayload(
|
|
12349
|
+
"payload.parentId must be a non-empty string when provided",
|
|
12350
|
+
);
|
|
12351
|
+
}
|
|
12352
|
+
|
|
12353
|
+
{
|
|
12354
|
+
const result = validatePlacementFields({
|
|
12355
|
+
payload,
|
|
12356
|
+
errorFactory: createPayloadValidationError,
|
|
12357
|
+
});
|
|
12358
|
+
if (result?.valid === false) {
|
|
12359
|
+
return result;
|
|
12360
|
+
}
|
|
12361
|
+
}
|
|
12362
|
+
},
|
|
12363
|
+
validateAgainstState: ({ state, payload }) => {
|
|
12364
|
+
const control = state.controls.items[payload.controlId];
|
|
12365
|
+
if (!isPlainObject(control) || control.type !== "control") {
|
|
12366
|
+
return invalidPrecondition(
|
|
12367
|
+
"payload.controlId must reference an existing control",
|
|
12368
|
+
);
|
|
12369
|
+
}
|
|
12370
|
+
|
|
12371
|
+
const collection = getControlElementCollection({
|
|
12372
|
+
state,
|
|
12373
|
+
controlId: payload.controlId,
|
|
12374
|
+
});
|
|
12375
|
+
const currentItem = collection.items[payload.elementId];
|
|
12376
|
+
|
|
12377
|
+
if (!isPlainObject(currentItem)) {
|
|
12378
|
+
return invalidPrecondition(
|
|
12379
|
+
"payload.elementId must reference an existing control element",
|
|
12380
|
+
);
|
|
12381
|
+
}
|
|
12382
|
+
|
|
12383
|
+
const currentNode = findTreeNode({
|
|
12384
|
+
nodes: collection.tree,
|
|
12385
|
+
nodeId: payload.elementId,
|
|
12386
|
+
});
|
|
12387
|
+
|
|
12388
|
+
if (payload.parentId !== undefined && payload.parentId !== null) {
|
|
12389
|
+
const parentItem = collection.items[payload.parentId];
|
|
12390
|
+
if (
|
|
12391
|
+
!isPlainObject(parentItem) ||
|
|
12392
|
+
!LAYOUT_CONTAINER_ELEMENT_TYPES.includes(parentItem.type)
|
|
12393
|
+
) {
|
|
12394
|
+
return invalidPrecondition(
|
|
12395
|
+
"payload.parentId must reference a folder or container control element",
|
|
12396
|
+
);
|
|
12397
|
+
}
|
|
12398
|
+
|
|
12399
|
+
const descendantIds = new Set(
|
|
12400
|
+
collectTreeDescendantIds({
|
|
12401
|
+
node: currentNode,
|
|
12402
|
+
}),
|
|
12403
|
+
);
|
|
12404
|
+
|
|
12405
|
+
if (descendantIds.has(payload.parentId)) {
|
|
12406
|
+
return invalidPrecondition(
|
|
12407
|
+
"payload.parentId must not target the moved control element or its descendants",
|
|
12408
|
+
);
|
|
12409
|
+
}
|
|
12410
|
+
}
|
|
12411
|
+
|
|
12412
|
+
if (payload.positionTargetId !== undefined) {
|
|
12413
|
+
if (payload.positionTargetId === payload.elementId) {
|
|
12414
|
+
return invalidPrecondition(
|
|
12415
|
+
"payload.positionTargetId must not reference the moved control element",
|
|
12416
|
+
);
|
|
12417
|
+
}
|
|
12418
|
+
|
|
12419
|
+
if (!isPlainObject(collection.items[payload.positionTargetId])) {
|
|
12420
|
+
return invalidPrecondition(
|
|
12421
|
+
"payload.positionTargetId must reference an existing control element",
|
|
12422
|
+
);
|
|
12423
|
+
}
|
|
12424
|
+
|
|
12425
|
+
const targetParentId = getNodeParentId({
|
|
12426
|
+
tree: collection.tree,
|
|
12427
|
+
nodeId: payload.positionTargetId,
|
|
12428
|
+
});
|
|
12429
|
+
|
|
12430
|
+
if (targetParentId !== (payload.parentId ?? null)) {
|
|
12431
|
+
return invalidPrecondition(
|
|
12432
|
+
"payload.positionTargetId must reference a sibling under payload.parentId",
|
|
12433
|
+
);
|
|
12434
|
+
}
|
|
12435
|
+
}
|
|
12436
|
+
},
|
|
12437
|
+
reduce: ({ state, payload }) => {
|
|
12438
|
+
const collection = getControlElementCollection({
|
|
12439
|
+
state,
|
|
12440
|
+
controlId: payload.controlId,
|
|
12441
|
+
});
|
|
12442
|
+
const nodeResult = removeNodeOrResult({
|
|
12443
|
+
tree: collection.tree,
|
|
12444
|
+
nodeId: payload.elementId,
|
|
12445
|
+
errorMessage: "control element move target missing from tree",
|
|
12446
|
+
});
|
|
12447
|
+
if (!nodeResult.valid) {
|
|
12448
|
+
return nodeResult;
|
|
12449
|
+
}
|
|
12450
|
+
|
|
12451
|
+
insertTreeNode({
|
|
12452
|
+
tree: collection.tree,
|
|
12453
|
+
node: nodeResult.node,
|
|
12454
|
+
parentId: payload.parentId ?? null,
|
|
12455
|
+
index: payload.index,
|
|
12456
|
+
position: payload.position,
|
|
12457
|
+
positionTargetId: payload.positionTargetId,
|
|
12458
|
+
});
|
|
12459
|
+
|
|
12460
|
+
return state;
|
|
12461
|
+
},
|
|
12462
|
+
},
|
|
11757
12463
|
{
|
|
11758
12464
|
type: "layout.element.move",
|
|
11759
12465
|
validatePayload: ({ payload }) => {
|
|
@@ -11942,6 +12648,8 @@ export const validatePayload = ({ type, payload }) => {
|
|
|
11942
12648
|
|
|
11943
12649
|
export const validateAgainstState = ({ state, command }) => {
|
|
11944
12650
|
return captureValidation(() => {
|
|
12651
|
+
const normalizedState = normalizeStateCollections(state);
|
|
12652
|
+
|
|
11945
12653
|
if (!isPlainObject(command)) {
|
|
11946
12654
|
return invalidPrecondition("command must be an object");
|
|
11947
12655
|
}
|
|
@@ -11954,7 +12662,7 @@ export const validateAgainstState = ({ state, command }) => {
|
|
|
11954
12662
|
);
|
|
11955
12663
|
}
|
|
11956
12664
|
|
|
11957
|
-
const stateResult = validateState({ state });
|
|
12665
|
+
const stateResult = validateState({ state: normalizedState });
|
|
11958
12666
|
if (!stateResult.valid) {
|
|
11959
12667
|
if (stateResult.error.kind === "invariant") {
|
|
11960
12668
|
return invalidInvariant(
|
|
@@ -11976,7 +12684,7 @@ export const validateAgainstState = ({ state, command }) => {
|
|
|
11976
12684
|
|
|
11977
12685
|
const validationResult = captureValidation(() =>
|
|
11978
12686
|
definition.validateAgainstState({
|
|
11979
|
-
state,
|
|
12687
|
+
state: normalizedState,
|
|
11980
12688
|
payload: command.payload,
|
|
11981
12689
|
}),
|
|
11982
12690
|
);
|
|
@@ -11987,12 +12695,16 @@ export const validateAgainstState = ({ state, command }) => {
|
|
|
11987
12695
|
|
|
11988
12696
|
export const processCommand = ({ state, command }) => {
|
|
11989
12697
|
return captureValidation(() => {
|
|
12698
|
+
const normalizedState = normalizeStateCollections(state);
|
|
12699
|
+
const shouldMaterializeControls =
|
|
12700
|
+
typeof command?.type === "string" && command.type.startsWith("control.");
|
|
12701
|
+
|
|
11990
12702
|
if (!isPlainObject(command)) {
|
|
11991
12703
|
return invalidPrecondition("command must be an object");
|
|
11992
12704
|
}
|
|
11993
12705
|
|
|
11994
12706
|
const preconditionResult = validateAgainstState({
|
|
11995
|
-
state,
|
|
12707
|
+
state: normalizedState,
|
|
11996
12708
|
command,
|
|
11997
12709
|
});
|
|
11998
12710
|
if (!preconditionResult.valid) {
|
|
@@ -12005,14 +12717,21 @@ export const processCommand = ({ state, command }) => {
|
|
|
12005
12717
|
}
|
|
12006
12718
|
|
|
12007
12719
|
const nextState = definition.reduce({
|
|
12008
|
-
state: structuredClone(
|
|
12720
|
+
state: structuredClone(
|
|
12721
|
+
shouldMaterializeControls ? normalizedState : state,
|
|
12722
|
+
),
|
|
12009
12723
|
payload: command.payload,
|
|
12010
12724
|
});
|
|
12011
12725
|
if (nextState?.valid === false) {
|
|
12012
12726
|
return nextState;
|
|
12013
12727
|
}
|
|
12014
12728
|
|
|
12015
|
-
const finalState =
|
|
12729
|
+
const finalState =
|
|
12730
|
+
nextState === undefined
|
|
12731
|
+
? shouldMaterializeControls
|
|
12732
|
+
? normalizedState
|
|
12733
|
+
: state
|
|
12734
|
+
: nextState;
|
|
12016
12735
|
const stateResult = validateState({
|
|
12017
12736
|
state: finalState,
|
|
12018
12737
|
});
|