@routevn/creator-model 1.0.3 → 1.0.4
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 +934 -136
package/src/model.js
CHANGED
|
@@ -30,8 +30,27 @@ 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];
|
|
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
|
+
};
|
|
53
|
+
};
|
|
35
54
|
const isString = (value) => typeof value === "string";
|
|
36
55
|
const FILE_ITEM_TYPES = [
|
|
37
56
|
"image",
|
|
@@ -115,7 +134,7 @@ const ANIMATION_EASING_KEYS = [
|
|
|
115
134
|
];
|
|
116
135
|
const VARIABLE_SCOPE_KEYS = ["context", "global-device", "global-account"];
|
|
117
136
|
const VARIABLE_TYPE_KEYS = ["string", "number", "boolean"];
|
|
118
|
-
const LAYOUT_TYPE_KEYS = ["normal", "dialogue", "nvl", "choice"
|
|
137
|
+
const LAYOUT_TYPE_KEYS = ["normal", "dialogue", "nvl", "choice"];
|
|
119
138
|
const LAYOUT_ELEMENT_TEXT_STYLE_ALIGN_KEYS = ["left", "center", "right"];
|
|
120
139
|
const LAYOUT_ELEMENT_BASE_TYPES = [
|
|
121
140
|
"folder",
|
|
@@ -2588,7 +2607,7 @@ const validateLayoutItems = ({ items, path, errorFactory }) => {
|
|
|
2588
2607
|
allowedKeys:
|
|
2589
2608
|
item.type === "folder"
|
|
2590
2609
|
? ["id", "type", "name"]
|
|
2591
|
-
: ["id", "type", "name", "layoutType", "elements"
|
|
2610
|
+
: ["id", "type", "name", "layoutType", "elements"],
|
|
2592
2611
|
path: itemPath,
|
|
2593
2612
|
errorFactory,
|
|
2594
2613
|
});
|
|
@@ -2622,7 +2641,7 @@ const validateLayoutItems = ({ items, path, errorFactory }) => {
|
|
|
2622
2641
|
if (!LAYOUT_TYPE_KEYS.includes(item.layoutType)) {
|
|
2623
2642
|
return invalidFromErrorFactory(
|
|
2624
2643
|
errorFactory,
|
|
2625
|
-
`${itemPath}.layoutType must be 'normal', 'dialogue', 'nvl',
|
|
2644
|
+
`${itemPath}.layoutType must be 'normal', 'dialogue', 'nvl', or 'choice'`,
|
|
2626
2645
|
);
|
|
2627
2646
|
}
|
|
2628
2647
|
|
|
@@ -2639,6 +2658,71 @@ const validateLayoutItems = ({ items, path, errorFactory }) => {
|
|
|
2639
2658
|
}
|
|
2640
2659
|
}
|
|
2641
2660
|
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
};
|
|
2664
|
+
|
|
2665
|
+
const validateControlItems = ({ items, path, errorFactory }) => {
|
|
2666
|
+
for (const [itemId, item] of Object.entries(items)) {
|
|
2667
|
+
const itemPath = `${path}.${itemId}`;
|
|
2668
|
+
|
|
2669
|
+
if (item?.type !== "folder" && item?.type !== "control") {
|
|
2670
|
+
return invalidFromErrorFactory(
|
|
2671
|
+
errorFactory,
|
|
2672
|
+
`${itemPath}.type must be 'folder' or 'control'`,
|
|
2673
|
+
);
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
{
|
|
2677
|
+
const result = validateAllowedKeys({
|
|
2678
|
+
value: item,
|
|
2679
|
+
allowedKeys:
|
|
2680
|
+
item.type === "folder"
|
|
2681
|
+
? ["id", "type", "name"]
|
|
2682
|
+
: ["id", "type", "name", "elements", "keyboard"],
|
|
2683
|
+
path: itemPath,
|
|
2684
|
+
errorFactory,
|
|
2685
|
+
});
|
|
2686
|
+
if (result?.valid === false) {
|
|
2687
|
+
return result;
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
if (!isNonEmptyString(item.id)) {
|
|
2692
|
+
return invalidFromErrorFactory(
|
|
2693
|
+
errorFactory,
|
|
2694
|
+
`${itemPath}.id must be a non-empty string`,
|
|
2695
|
+
);
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
if (item.id !== itemId) {
|
|
2699
|
+
return invalidFromErrorFactory(
|
|
2700
|
+
errorFactory,
|
|
2701
|
+
`${itemPath}.id must match item key '${itemId}'`,
|
|
2702
|
+
);
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
if (!isNonEmptyString(item.name)) {
|
|
2706
|
+
return invalidFromErrorFactory(
|
|
2707
|
+
errorFactory,
|
|
2708
|
+
`${itemPath}.name must be a non-empty string`,
|
|
2709
|
+
);
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
if (item.type === "control") {
|
|
2713
|
+
{
|
|
2714
|
+
const result = validateNestedCollection({
|
|
2715
|
+
collection: item.elements,
|
|
2716
|
+
path: `${itemPath}.elements`,
|
|
2717
|
+
itemValidator: validateLayoutElementItems,
|
|
2718
|
+
treeValidator: validateLayoutElementTreeOwnership,
|
|
2719
|
+
treeNodeLabel: "control element",
|
|
2720
|
+
});
|
|
2721
|
+
if (result?.valid === false) {
|
|
2722
|
+
return result;
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2642
2726
|
{
|
|
2643
2727
|
const result = validateKeyboardMap({
|
|
2644
2728
|
value: item.keyboard,
|
|
@@ -3264,6 +3348,17 @@ const validateCollection = ({ collection, path }) => {
|
|
|
3264
3348
|
return result;
|
|
3265
3349
|
}
|
|
3266
3350
|
}
|
|
3351
|
+
} else if (path === "state.controls") {
|
|
3352
|
+
{
|
|
3353
|
+
const result = validateControlItems({
|
|
3354
|
+
items: collection.items,
|
|
3355
|
+
path: `${path}.items`,
|
|
3356
|
+
errorFactory: createStateValidationError,
|
|
3357
|
+
});
|
|
3358
|
+
if (result?.valid === false) {
|
|
3359
|
+
return result;
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3267
3362
|
}
|
|
3268
3363
|
|
|
3269
3364
|
if (!Array.isArray(collection.tree)) {
|
|
@@ -3366,7 +3461,8 @@ const validateCollection = ({ collection, path }) => {
|
|
|
3366
3461
|
path === "state.variables" ||
|
|
3367
3462
|
path === "state.textStyles" ||
|
|
3368
3463
|
path === "state.characters" ||
|
|
3369
|
-
path === "state.layouts"
|
|
3464
|
+
path === "state.layouts" ||
|
|
3465
|
+
path === "state.controls"
|
|
3370
3466
|
) {
|
|
3371
3467
|
{
|
|
3372
3468
|
const result = validateGenericFolderOwnership({
|
|
@@ -3374,7 +3470,11 @@ const validateCollection = ({ collection, path }) => {
|
|
|
3374
3470
|
items: collection.items,
|
|
3375
3471
|
path: `${path}.tree`,
|
|
3376
3472
|
folderLabel:
|
|
3377
|
-
path === "state.layouts"
|
|
3473
|
+
path === "state.layouts"
|
|
3474
|
+
? "folder layout item"
|
|
3475
|
+
: path === "state.controls"
|
|
3476
|
+
? "folder control item"
|
|
3477
|
+
: "folder item",
|
|
3378
3478
|
});
|
|
3379
3479
|
if (result?.valid === false) {
|
|
3380
3480
|
return result;
|
|
@@ -3714,13 +3814,20 @@ export const assertInvariants = ({ state }) => {
|
|
|
3714
3814
|
}
|
|
3715
3815
|
}
|
|
3716
3816
|
|
|
3717
|
-
const assertImageReference = ({
|
|
3817
|
+
const assertImageReference = ({
|
|
3818
|
+
ownerIdField,
|
|
3819
|
+
ownerId,
|
|
3820
|
+
ownerLabel,
|
|
3821
|
+
elementId,
|
|
3822
|
+
field,
|
|
3823
|
+
targetId,
|
|
3824
|
+
}) => {
|
|
3718
3825
|
const image = state.images.items[targetId];
|
|
3719
3826
|
if (!isPlainObject(image) || image.type === "folder") {
|
|
3720
3827
|
return invalidInvariant(
|
|
3721
|
-
|
|
3828
|
+
`${ownerLabel} element ${field} must reference an existing non-folder image`,
|
|
3722
3829
|
{
|
|
3723
|
-
|
|
3830
|
+
[ownerIdField]: ownerId,
|
|
3724
3831
|
elementId,
|
|
3725
3832
|
field,
|
|
3726
3833
|
targetId,
|
|
@@ -3732,7 +3839,9 @@ export const assertInvariants = ({ state }) => {
|
|
|
3732
3839
|
};
|
|
3733
3840
|
|
|
3734
3841
|
const assertTextStyleReference = ({
|
|
3735
|
-
|
|
3842
|
+
ownerIdField,
|
|
3843
|
+
ownerId,
|
|
3844
|
+
ownerLabel,
|
|
3736
3845
|
elementId,
|
|
3737
3846
|
field,
|
|
3738
3847
|
targetId,
|
|
@@ -3740,9 +3849,9 @@ export const assertInvariants = ({ state }) => {
|
|
|
3740
3849
|
const textStyle = state.textStyles.items[targetId];
|
|
3741
3850
|
if (!isPlainObject(textStyle) || textStyle.type === "folder") {
|
|
3742
3851
|
return invalidInvariant(
|
|
3743
|
-
|
|
3852
|
+
`${ownerLabel} element ${field} must reference an existing non-folder text style`,
|
|
3744
3853
|
{
|
|
3745
|
-
|
|
3854
|
+
[ownerIdField]: ownerId,
|
|
3746
3855
|
elementId,
|
|
3747
3856
|
field,
|
|
3748
3857
|
targetId,
|
|
@@ -3753,13 +3862,19 @@ export const assertInvariants = ({ state }) => {
|
|
|
3753
3862
|
return VALID_RESULT;
|
|
3754
3863
|
};
|
|
3755
3864
|
|
|
3756
|
-
const assertVariableReference = ({
|
|
3865
|
+
const assertVariableReference = ({
|
|
3866
|
+
ownerIdField,
|
|
3867
|
+
ownerId,
|
|
3868
|
+
ownerLabel,
|
|
3869
|
+
elementId,
|
|
3870
|
+
targetId,
|
|
3871
|
+
}) => {
|
|
3757
3872
|
const variable = state.variables.items[targetId];
|
|
3758
3873
|
if (!isPlainObject(variable) || variable.type === "folder") {
|
|
3759
3874
|
return invalidInvariant(
|
|
3760
|
-
|
|
3875
|
+
`${ownerLabel} element variableId must reference an existing non-folder variable`,
|
|
3761
3876
|
{
|
|
3762
|
-
|
|
3877
|
+
[ownerIdField]: ownerId,
|
|
3763
3878
|
elementId,
|
|
3764
3879
|
variableId: targetId,
|
|
3765
3880
|
},
|
|
@@ -3769,62 +3884,101 @@ export const assertInvariants = ({ state }) => {
|
|
|
3769
3884
|
return VALID_RESULT;
|
|
3770
3885
|
};
|
|
3771
3886
|
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3887
|
+
const assertElementReferencesForCollection = ({
|
|
3888
|
+
items,
|
|
3889
|
+
ownerIdField,
|
|
3890
|
+
ownerLabel,
|
|
3891
|
+
ownerType,
|
|
3892
|
+
}) => {
|
|
3893
|
+
for (const [ownerId, owner] of Object.entries(items)) {
|
|
3894
|
+
if (owner.type !== ownerType) {
|
|
3895
|
+
continue;
|
|
3896
|
+
}
|
|
3776
3897
|
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3898
|
+
for (const [elementId, element] of Object.entries(owner.elements.items)) {
|
|
3899
|
+
for (const field of [
|
|
3900
|
+
"imageId",
|
|
3901
|
+
"hoverImageId",
|
|
3902
|
+
"clickImageId",
|
|
3903
|
+
"thumbImageId",
|
|
3904
|
+
"barImageId",
|
|
3905
|
+
"hoverThumbImageId",
|
|
3906
|
+
"hoverBarImageId",
|
|
3907
|
+
]) {
|
|
3908
|
+
if (element[field] !== undefined) {
|
|
3909
|
+
const result = assertImageReference({
|
|
3910
|
+
ownerIdField,
|
|
3911
|
+
ownerId,
|
|
3912
|
+
ownerLabel,
|
|
3913
|
+
elementId,
|
|
3914
|
+
field,
|
|
3915
|
+
targetId: element[field],
|
|
3916
|
+
});
|
|
3917
|
+
if (!result.valid) {
|
|
3918
|
+
return result;
|
|
3919
|
+
}
|
|
3796
3920
|
}
|
|
3797
3921
|
}
|
|
3798
|
-
}
|
|
3799
3922
|
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3923
|
+
for (const field of [
|
|
3924
|
+
"textStyleId",
|
|
3925
|
+
"hoverTextStyleId",
|
|
3926
|
+
"clickTextStyleId",
|
|
3927
|
+
]) {
|
|
3928
|
+
if (element[field] !== undefined) {
|
|
3929
|
+
const result = assertTextStyleReference({
|
|
3930
|
+
ownerIdField,
|
|
3931
|
+
ownerId,
|
|
3932
|
+
ownerLabel,
|
|
3933
|
+
elementId,
|
|
3934
|
+
field,
|
|
3935
|
+
targetId: element[field],
|
|
3936
|
+
});
|
|
3937
|
+
if (!result.valid) {
|
|
3938
|
+
return result;
|
|
3939
|
+
}
|
|
3940
|
+
}
|
|
3941
|
+
}
|
|
3942
|
+
|
|
3943
|
+
if (element.variableId !== undefined) {
|
|
3944
|
+
const result = assertVariableReference({
|
|
3945
|
+
ownerIdField,
|
|
3946
|
+
ownerId,
|
|
3947
|
+
ownerLabel,
|
|
3808
3948
|
elementId,
|
|
3809
|
-
|
|
3810
|
-
targetId: element[field],
|
|
3949
|
+
targetId: element.variableId,
|
|
3811
3950
|
});
|
|
3812
3951
|
if (!result.valid) {
|
|
3813
3952
|
return result;
|
|
3814
3953
|
}
|
|
3815
3954
|
}
|
|
3816
3955
|
}
|
|
3956
|
+
}
|
|
3817
3957
|
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3958
|
+
return VALID_RESULT;
|
|
3959
|
+
};
|
|
3960
|
+
|
|
3961
|
+
{
|
|
3962
|
+
const result = assertElementReferencesForCollection({
|
|
3963
|
+
items: state.layouts.items,
|
|
3964
|
+
ownerIdField: "layoutId",
|
|
3965
|
+
ownerLabel: "layout",
|
|
3966
|
+
ownerType: "layout",
|
|
3967
|
+
});
|
|
3968
|
+
if (!result.valid) {
|
|
3969
|
+
return result;
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
|
|
3973
|
+
{
|
|
3974
|
+
const result = assertElementReferencesForCollection({
|
|
3975
|
+
items: state.controls.items,
|
|
3976
|
+
ownerIdField: "controlId",
|
|
3977
|
+
ownerLabel: "control",
|
|
3978
|
+
ownerType: "control",
|
|
3979
|
+
});
|
|
3980
|
+
if (!result.valid) {
|
|
3981
|
+
return result;
|
|
3828
3982
|
}
|
|
3829
3983
|
}
|
|
3830
3984
|
|
|
@@ -3833,9 +3987,11 @@ export const assertInvariants = ({ state }) => {
|
|
|
3833
3987
|
|
|
3834
3988
|
const runValidateState = ({ state }) => {
|
|
3835
3989
|
return captureValidation(() => {
|
|
3990
|
+
const normalizedState = normalizeStateCollections(state);
|
|
3991
|
+
|
|
3836
3992
|
{
|
|
3837
3993
|
const result = validateExactKeys({
|
|
3838
|
-
value:
|
|
3994
|
+
value: normalizedState,
|
|
3839
3995
|
expectedKeys: ROOT_KEYS,
|
|
3840
3996
|
path: "state",
|
|
3841
3997
|
errorFactory: createStateValidationError,
|
|
@@ -3847,7 +4003,7 @@ const runValidateState = ({ state }) => {
|
|
|
3847
4003
|
|
|
3848
4004
|
{
|
|
3849
4005
|
const result = validateAllowedKeys({
|
|
3850
|
-
value:
|
|
4006
|
+
value: normalizedState.project,
|
|
3851
4007
|
allowedKeys: ["resolution"],
|
|
3852
4008
|
path: "state.project",
|
|
3853
4009
|
errorFactory: createStateValidationError,
|
|
@@ -3857,10 +4013,10 @@ const runValidateState = ({ state }) => {
|
|
|
3857
4013
|
}
|
|
3858
4014
|
}
|
|
3859
4015
|
|
|
3860
|
-
if (
|
|
4016
|
+
if (normalizedState.project.resolution !== undefined) {
|
|
3861
4017
|
{
|
|
3862
4018
|
const result = validateExactKeys({
|
|
3863
|
-
value:
|
|
4019
|
+
value: normalizedState.project.resolution,
|
|
3864
4020
|
expectedKeys: ["width", "height"],
|
|
3865
4021
|
path: "state.project.resolution",
|
|
3866
4022
|
errorFactory: createStateValidationError,
|
|
@@ -3870,13 +4026,13 @@ const runValidateState = ({ state }) => {
|
|
|
3870
4026
|
}
|
|
3871
4027
|
}
|
|
3872
4028
|
|
|
3873
|
-
if (!isFiniteNumber(
|
|
4029
|
+
if (!isFiniteNumber(normalizedState.project.resolution.width)) {
|
|
3874
4030
|
return invalidState(
|
|
3875
4031
|
"state.project.resolution.width must be a finite number",
|
|
3876
4032
|
);
|
|
3877
4033
|
}
|
|
3878
4034
|
|
|
3879
|
-
if (!isFiniteNumber(
|
|
4035
|
+
if (!isFiniteNumber(normalizedState.project.resolution.height)) {
|
|
3880
4036
|
return invalidState(
|
|
3881
4037
|
"state.project.resolution.height must be a finite number",
|
|
3882
4038
|
);
|
|
@@ -3885,7 +4041,7 @@ const runValidateState = ({ state }) => {
|
|
|
3885
4041
|
|
|
3886
4042
|
{
|
|
3887
4043
|
const result = validateExactKeys({
|
|
3888
|
-
value:
|
|
4044
|
+
value: normalizedState.story,
|
|
3889
4045
|
expectedKeys: ["initialSceneId"],
|
|
3890
4046
|
path: "state.story",
|
|
3891
4047
|
errorFactory: createStateValidationError,
|
|
@@ -3896,8 +4052,8 @@ const runValidateState = ({ state }) => {
|
|
|
3896
4052
|
}
|
|
3897
4053
|
|
|
3898
4054
|
if (
|
|
3899
|
-
|
|
3900
|
-
!isNonEmptyString(
|
|
4055
|
+
normalizedState.story.initialSceneId !== null &&
|
|
4056
|
+
!isNonEmptyString(normalizedState.story.initialSceneId)
|
|
3901
4057
|
) {
|
|
3902
4058
|
return invalidState(
|
|
3903
4059
|
"state.story.initialSceneId must be a non-empty string or null",
|
|
@@ -3907,7 +4063,7 @@ const runValidateState = ({ state }) => {
|
|
|
3907
4063
|
for (const collectionKey of COLLECTION_KEYS) {
|
|
3908
4064
|
{
|
|
3909
4065
|
const result = validateCollection({
|
|
3910
|
-
collection:
|
|
4066
|
+
collection: normalizedState[collectionKey],
|
|
3911
4067
|
path: `state.${collectionKey}`,
|
|
3912
4068
|
});
|
|
3913
4069
|
if (result?.valid === false) {
|
|
@@ -3916,7 +4072,7 @@ const runValidateState = ({ state }) => {
|
|
|
3916
4072
|
}
|
|
3917
4073
|
}
|
|
3918
4074
|
|
|
3919
|
-
const invariantResult = assertInvariants({ state });
|
|
4075
|
+
const invariantResult = assertInvariants({ state: normalizedState });
|
|
3920
4076
|
if (!invariantResult.valid) {
|
|
3921
4077
|
return invariantResult;
|
|
3922
4078
|
}
|
|
@@ -5928,7 +6084,7 @@ const validateLayoutCreateData = ({ data, errorFactory }) => {
|
|
|
5928
6084
|
allowedKeys:
|
|
5929
6085
|
data.type === "folder"
|
|
5930
6086
|
? ["type", "name"]
|
|
5931
|
-
: ["type", "name", "layoutType", "elements"
|
|
6087
|
+
: ["type", "name", "layoutType", "elements"],
|
|
5932
6088
|
path: "payload.data",
|
|
5933
6089
|
errorFactory,
|
|
5934
6090
|
});
|
|
@@ -5948,7 +6104,7 @@ const validateLayoutCreateData = ({ data, errorFactory }) => {
|
|
|
5948
6104
|
if (!LAYOUT_TYPE_KEYS.includes(data.layoutType)) {
|
|
5949
6105
|
return invalidFromErrorFactory(
|
|
5950
6106
|
errorFactory,
|
|
5951
|
-
"payload.data.layoutType must be 'normal', 'dialogue', 'nvl',
|
|
6107
|
+
"payload.data.layoutType must be 'normal', 'dialogue', 'nvl', or 'choice'",
|
|
5952
6108
|
);
|
|
5953
6109
|
}
|
|
5954
6110
|
|
|
@@ -5965,16 +6121,6 @@ const validateLayoutCreateData = ({ data, errorFactory }) => {
|
|
|
5965
6121
|
}
|
|
5966
6122
|
}
|
|
5967
6123
|
|
|
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
6124
|
}
|
|
5979
6125
|
};
|
|
5980
6126
|
|
|
@@ -5982,7 +6128,7 @@ const validateLayoutUpdateData = ({ data, errorFactory }) => {
|
|
|
5982
6128
|
{
|
|
5983
6129
|
const result = validateAllowedKeys({
|
|
5984
6130
|
value: data,
|
|
5985
|
-
allowedKeys: ["name", "layoutType"
|
|
6131
|
+
allowedKeys: ["name", "layoutType"],
|
|
5986
6132
|
path: "payload.data",
|
|
5987
6133
|
errorFactory,
|
|
5988
6134
|
});
|
|
@@ -6011,7 +6157,100 @@ const validateLayoutUpdateData = ({ data, errorFactory }) => {
|
|
|
6011
6157
|
) {
|
|
6012
6158
|
return invalidFromErrorFactory(
|
|
6013
6159
|
errorFactory,
|
|
6014
|
-
"payload.data.layoutType must be 'normal', 'dialogue', 'nvl',
|
|
6160
|
+
"payload.data.layoutType must be 'normal', 'dialogue', 'nvl', or 'choice' when provided",
|
|
6161
|
+
);
|
|
6162
|
+
}
|
|
6163
|
+
|
|
6164
|
+
};
|
|
6165
|
+
|
|
6166
|
+
const validateControlCreateData = ({ data, errorFactory }) => {
|
|
6167
|
+
if (!isPlainObject(data)) {
|
|
6168
|
+
return invalidFromErrorFactory(
|
|
6169
|
+
errorFactory,
|
|
6170
|
+
"payload.data must be an object",
|
|
6171
|
+
);
|
|
6172
|
+
}
|
|
6173
|
+
|
|
6174
|
+
if (data.type !== "folder" && data.type !== "control") {
|
|
6175
|
+
return invalidFromErrorFactory(
|
|
6176
|
+
errorFactory,
|
|
6177
|
+
"payload.data.type must be 'folder' or 'control'",
|
|
6178
|
+
);
|
|
6179
|
+
}
|
|
6180
|
+
|
|
6181
|
+
{
|
|
6182
|
+
const result = validateAllowedKeys({
|
|
6183
|
+
value: data,
|
|
6184
|
+
allowedKeys:
|
|
6185
|
+
data.type === "folder"
|
|
6186
|
+
? ["type", "name"]
|
|
6187
|
+
: ["type", "name", "elements", "keyboard"],
|
|
6188
|
+
path: "payload.data",
|
|
6189
|
+
errorFactory,
|
|
6190
|
+
});
|
|
6191
|
+
if (result?.valid === false) {
|
|
6192
|
+
return result;
|
|
6193
|
+
}
|
|
6194
|
+
}
|
|
6195
|
+
|
|
6196
|
+
if (!isNonEmptyString(data.name)) {
|
|
6197
|
+
return invalidFromErrorFactory(
|
|
6198
|
+
errorFactory,
|
|
6199
|
+
"payload.data.name must be a non-empty string",
|
|
6200
|
+
);
|
|
6201
|
+
}
|
|
6202
|
+
|
|
6203
|
+
if (data.type === "control") {
|
|
6204
|
+
{
|
|
6205
|
+
const result = validateNestedCollection({
|
|
6206
|
+
collection: data.elements,
|
|
6207
|
+
path: "payload.data.elements",
|
|
6208
|
+
itemValidator: validateLayoutElementItems,
|
|
6209
|
+
treeValidator: validateLayoutElementTreeOwnership,
|
|
6210
|
+
errorFactory,
|
|
6211
|
+
});
|
|
6212
|
+
if (result?.valid === false) {
|
|
6213
|
+
return result;
|
|
6214
|
+
}
|
|
6215
|
+
}
|
|
6216
|
+
|
|
6217
|
+
{
|
|
6218
|
+
const result = validateKeyboardMap({
|
|
6219
|
+
value: data.keyboard,
|
|
6220
|
+
path: "payload.data.keyboard",
|
|
6221
|
+
errorFactory,
|
|
6222
|
+
});
|
|
6223
|
+
if (result?.valid === false) {
|
|
6224
|
+
return result;
|
|
6225
|
+
}
|
|
6226
|
+
}
|
|
6227
|
+
}
|
|
6228
|
+
};
|
|
6229
|
+
|
|
6230
|
+
const validateControlUpdateData = ({ data, errorFactory }) => {
|
|
6231
|
+
{
|
|
6232
|
+
const result = validateAllowedKeys({
|
|
6233
|
+
value: data,
|
|
6234
|
+
allowedKeys: ["name", "keyboard"],
|
|
6235
|
+
path: "payload.data",
|
|
6236
|
+
errorFactory,
|
|
6237
|
+
});
|
|
6238
|
+
if (result?.valid === false) {
|
|
6239
|
+
return result;
|
|
6240
|
+
}
|
|
6241
|
+
}
|
|
6242
|
+
|
|
6243
|
+
if (Object.keys(data).length === 0) {
|
|
6244
|
+
return invalidFromErrorFactory(
|
|
6245
|
+
errorFactory,
|
|
6246
|
+
"payload.data must include at least one updatable field",
|
|
6247
|
+
);
|
|
6248
|
+
}
|
|
6249
|
+
|
|
6250
|
+
if (data.name !== undefined && !isNonEmptyString(data.name)) {
|
|
6251
|
+
return invalidFromErrorFactory(
|
|
6252
|
+
errorFactory,
|
|
6253
|
+
"payload.data.name must be a non-empty string when provided",
|
|
6015
6254
|
);
|
|
6016
6255
|
}
|
|
6017
6256
|
|
|
@@ -6061,8 +6300,10 @@ const validateLayoutElementUpdateData = ({ data, errorFactory, replace }) => {
|
|
|
6061
6300
|
}
|
|
6062
6301
|
};
|
|
6063
6302
|
|
|
6064
|
-
const
|
|
6065
|
-
|
|
6303
|
+
const validateVisualElementReferenceTargets = ({
|
|
6304
|
+
ownerIdField,
|
|
6305
|
+
ownerId,
|
|
6306
|
+
ownerLabel,
|
|
6066
6307
|
elementId,
|
|
6067
6308
|
data,
|
|
6068
6309
|
state,
|
|
@@ -6073,9 +6314,9 @@ const validateLayoutElementReferenceTargets = ({
|
|
|
6073
6314
|
if (!isPlainObject(image) || image.type === "folder") {
|
|
6074
6315
|
return invalidFromErrorFactory(
|
|
6075
6316
|
errorFactory,
|
|
6076
|
-
|
|
6317
|
+
`${ownerLabel} element imageId must reference an existing non-folder image`,
|
|
6077
6318
|
{
|
|
6078
|
-
|
|
6319
|
+
[ownerIdField]: ownerId,
|
|
6079
6320
|
elementId,
|
|
6080
6321
|
field: "imageId",
|
|
6081
6322
|
targetId: data.imageId,
|
|
@@ -6089,9 +6330,9 @@ const validateLayoutElementReferenceTargets = ({
|
|
|
6089
6330
|
if (!isPlainObject(image) || image.type === "folder") {
|
|
6090
6331
|
return invalidFromErrorFactory(
|
|
6091
6332
|
errorFactory,
|
|
6092
|
-
|
|
6333
|
+
`${ownerLabel} element hoverImageId must reference an existing non-folder image`,
|
|
6093
6334
|
{
|
|
6094
|
-
|
|
6335
|
+
[ownerIdField]: ownerId,
|
|
6095
6336
|
elementId,
|
|
6096
6337
|
field: "hoverImageId",
|
|
6097
6338
|
targetId: data.hoverImageId,
|
|
@@ -6105,9 +6346,9 @@ const validateLayoutElementReferenceTargets = ({
|
|
|
6105
6346
|
if (!isPlainObject(image) || image.type === "folder") {
|
|
6106
6347
|
return invalidFromErrorFactory(
|
|
6107
6348
|
errorFactory,
|
|
6108
|
-
|
|
6349
|
+
`${ownerLabel} element clickImageId must reference an existing non-folder image`,
|
|
6109
6350
|
{
|
|
6110
|
-
|
|
6351
|
+
[ownerIdField]: ownerId,
|
|
6111
6352
|
elementId,
|
|
6112
6353
|
field: "clickImageId",
|
|
6113
6354
|
targetId: data.clickImageId,
|
|
@@ -6121,9 +6362,9 @@ const validateLayoutElementReferenceTargets = ({
|
|
|
6121
6362
|
if (!isPlainObject(image) || image.type === "folder") {
|
|
6122
6363
|
return invalidFromErrorFactory(
|
|
6123
6364
|
errorFactory,
|
|
6124
|
-
|
|
6365
|
+
`${ownerLabel} element thumbImageId must reference an existing non-folder image`,
|
|
6125
6366
|
{
|
|
6126
|
-
|
|
6367
|
+
[ownerIdField]: ownerId,
|
|
6127
6368
|
elementId,
|
|
6128
6369
|
field: "thumbImageId",
|
|
6129
6370
|
targetId: data.thumbImageId,
|
|
@@ -6137,9 +6378,9 @@ const validateLayoutElementReferenceTargets = ({
|
|
|
6137
6378
|
if (!isPlainObject(image) || image.type === "folder") {
|
|
6138
6379
|
return invalidFromErrorFactory(
|
|
6139
6380
|
errorFactory,
|
|
6140
|
-
|
|
6381
|
+
`${ownerLabel} element hoverThumbImageId must reference an existing non-folder image`,
|
|
6141
6382
|
{
|
|
6142
|
-
|
|
6383
|
+
[ownerIdField]: ownerId,
|
|
6143
6384
|
elementId,
|
|
6144
6385
|
field: "hoverThumbImageId",
|
|
6145
6386
|
targetId: data.hoverThumbImageId,
|
|
@@ -6153,9 +6394,9 @@ const validateLayoutElementReferenceTargets = ({
|
|
|
6153
6394
|
if (!isPlainObject(image) || image.type === "folder") {
|
|
6154
6395
|
return invalidFromErrorFactory(
|
|
6155
6396
|
errorFactory,
|
|
6156
|
-
|
|
6397
|
+
`${ownerLabel} element barImageId must reference an existing non-folder image`,
|
|
6157
6398
|
{
|
|
6158
|
-
|
|
6399
|
+
[ownerIdField]: ownerId,
|
|
6159
6400
|
elementId,
|
|
6160
6401
|
field: "barImageId",
|
|
6161
6402
|
targetId: data.barImageId,
|
|
@@ -6169,9 +6410,9 @@ const validateLayoutElementReferenceTargets = ({
|
|
|
6169
6410
|
if (!isPlainObject(image) || image.type === "folder") {
|
|
6170
6411
|
return invalidFromErrorFactory(
|
|
6171
6412
|
errorFactory,
|
|
6172
|
-
|
|
6413
|
+
`${ownerLabel} element hoverBarImageId must reference an existing non-folder image`,
|
|
6173
6414
|
{
|
|
6174
|
-
|
|
6415
|
+
[ownerIdField]: ownerId,
|
|
6175
6416
|
elementId,
|
|
6176
6417
|
field: "hoverBarImageId",
|
|
6177
6418
|
targetId: data.hoverBarImageId,
|
|
@@ -6185,9 +6426,9 @@ const validateLayoutElementReferenceTargets = ({
|
|
|
6185
6426
|
if (!isPlainObject(textStyle) || textStyle.type === "folder") {
|
|
6186
6427
|
return invalidFromErrorFactory(
|
|
6187
6428
|
errorFactory,
|
|
6188
|
-
|
|
6429
|
+
`${ownerLabel} element textStyleId must reference an existing non-folder text style`,
|
|
6189
6430
|
{
|
|
6190
|
-
|
|
6431
|
+
[ownerIdField]: ownerId,
|
|
6191
6432
|
elementId,
|
|
6192
6433
|
field: "textStyleId",
|
|
6193
6434
|
targetId: data.textStyleId,
|
|
@@ -6201,9 +6442,9 @@ const validateLayoutElementReferenceTargets = ({
|
|
|
6201
6442
|
if (!isPlainObject(textStyle) || textStyle.type === "folder") {
|
|
6202
6443
|
return invalidFromErrorFactory(
|
|
6203
6444
|
errorFactory,
|
|
6204
|
-
|
|
6445
|
+
`${ownerLabel} element hoverTextStyleId must reference an existing non-folder text style`,
|
|
6205
6446
|
{
|
|
6206
|
-
|
|
6447
|
+
[ownerIdField]: ownerId,
|
|
6207
6448
|
elementId,
|
|
6208
6449
|
field: "hoverTextStyleId",
|
|
6209
6450
|
targetId: data.hoverTextStyleId,
|
|
@@ -6217,9 +6458,9 @@ const validateLayoutElementReferenceTargets = ({
|
|
|
6217
6458
|
if (!isPlainObject(textStyle) || textStyle.type === "folder") {
|
|
6218
6459
|
return invalidFromErrorFactory(
|
|
6219
6460
|
errorFactory,
|
|
6220
|
-
|
|
6461
|
+
`${ownerLabel} element clickTextStyleId must reference an existing non-folder text style`,
|
|
6221
6462
|
{
|
|
6222
|
-
|
|
6463
|
+
[ownerIdField]: ownerId,
|
|
6223
6464
|
elementId,
|
|
6224
6465
|
field: "clickTextStyleId",
|
|
6225
6466
|
targetId: data.clickTextStyleId,
|
|
@@ -6233,9 +6474,9 @@ const validateLayoutElementReferenceTargets = ({
|
|
|
6233
6474
|
if (!isPlainObject(variable) || variable.type === "folder") {
|
|
6234
6475
|
return invalidFromErrorFactory(
|
|
6235
6476
|
errorFactory,
|
|
6236
|
-
|
|
6477
|
+
`${ownerLabel} element variableId must reference an existing non-folder variable`,
|
|
6237
6478
|
{
|
|
6238
|
-
|
|
6479
|
+
[ownerIdField]: ownerId,
|
|
6239
6480
|
elementId,
|
|
6240
6481
|
variableId: data.variableId,
|
|
6241
6482
|
},
|
|
@@ -6774,6 +7015,9 @@ const getCharacterSpriteCollection = ({ state, characterId }) =>
|
|
|
6774
7015
|
const getLayoutElementCollection = ({ state, layoutId }) =>
|
|
6775
7016
|
state.layouts.items[layoutId]?.elements;
|
|
6776
7017
|
|
|
7018
|
+
const getControlElementCollection = ({ state, controlId }) =>
|
|
7019
|
+
state.controls.items[controlId]?.elements;
|
|
7020
|
+
|
|
6777
7021
|
const findReferencedFileUsage = ({ state, fileId }) => {
|
|
6778
7022
|
for (const [imageId, image] of Object.entries(state.images.items)) {
|
|
6779
7023
|
if (image.type !== "image") {
|
|
@@ -10911,11 +11155,6 @@ const COMMAND_DEFINITIONS = [
|
|
|
10911
11155
|
? {
|
|
10912
11156
|
layoutType: payload.data.layoutType,
|
|
10913
11157
|
elements: structuredClone(payload.data.elements),
|
|
10914
|
-
...(payload.data.keyboard !== undefined
|
|
10915
|
-
? {
|
|
10916
|
-
keyboard: structuredClone(payload.data.keyboard),
|
|
10917
|
-
}
|
|
10918
|
-
: {}),
|
|
10919
11158
|
}
|
|
10920
11159
|
: {}),
|
|
10921
11160
|
}),
|
|
@@ -10930,21 +11169,54 @@ const COMMAND_DEFINITIONS = [
|
|
|
10930
11169
|
}
|
|
10931
11170
|
},
|
|
10932
11171
|
}),
|
|
10933
|
-
{
|
|
10934
|
-
|
|
10935
|
-
|
|
10936
|
-
|
|
10937
|
-
|
|
10938
|
-
|
|
10939
|
-
|
|
10940
|
-
|
|
10941
|
-
|
|
10942
|
-
|
|
10943
|
-
|
|
10944
|
-
|
|
10945
|
-
|
|
10946
|
-
|
|
10947
|
-
|
|
11172
|
+
...createFolderedCollectionCommandDefinitions({
|
|
11173
|
+
familyName: "control",
|
|
11174
|
+
collectionKey: "controls",
|
|
11175
|
+
idField: "controlId",
|
|
11176
|
+
itemLabel: "control item",
|
|
11177
|
+
createDataValidator: validateControlCreateData,
|
|
11178
|
+
updateDataValidator: validateControlUpdateData,
|
|
11179
|
+
createItem: ({ payload }) => ({
|
|
11180
|
+
id: payload.controlId,
|
|
11181
|
+
type: payload.data.type,
|
|
11182
|
+
name: payload.data.name,
|
|
11183
|
+
...(payload.data.type === "control"
|
|
11184
|
+
? {
|
|
11185
|
+
elements: structuredClone(payload.data.elements),
|
|
11186
|
+
...(payload.data.keyboard !== undefined
|
|
11187
|
+
? {
|
|
11188
|
+
keyboard: structuredClone(payload.data.keyboard),
|
|
11189
|
+
}
|
|
11190
|
+
: {}),
|
|
11191
|
+
}
|
|
11192
|
+
: {}),
|
|
11193
|
+
}),
|
|
11194
|
+
validateUpdateState: ({ payload, currentItem }) => {
|
|
11195
|
+
if (
|
|
11196
|
+
currentItem.type === "folder" &&
|
|
11197
|
+
Object.keys(payload.data).some((key) => key !== "name")
|
|
11198
|
+
) {
|
|
11199
|
+
return invalidPrecondition(
|
|
11200
|
+
"folder control items cannot update control fields",
|
|
11201
|
+
);
|
|
11202
|
+
}
|
|
11203
|
+
},
|
|
11204
|
+
}),
|
|
11205
|
+
{
|
|
11206
|
+
type: "character.sprite.create",
|
|
11207
|
+
validatePayload: ({ payload }) => {
|
|
11208
|
+
{
|
|
11209
|
+
const result = validateAllowedKeys({
|
|
11210
|
+
value: payload,
|
|
11211
|
+
allowedKeys: [
|
|
11212
|
+
"characterId",
|
|
11213
|
+
"spriteId",
|
|
11214
|
+
"parentId",
|
|
11215
|
+
"data",
|
|
11216
|
+
"index",
|
|
11217
|
+
"position",
|
|
11218
|
+
"positionTargetId",
|
|
11219
|
+
],
|
|
10948
11220
|
path: "payload",
|
|
10949
11221
|
errorFactory: createPayloadValidationError,
|
|
10950
11222
|
});
|
|
@@ -11510,8 +11782,10 @@ const COMMAND_DEFINITIONS = [
|
|
|
11510
11782
|
}
|
|
11511
11783
|
|
|
11512
11784
|
{
|
|
11513
|
-
const result =
|
|
11514
|
-
|
|
11785
|
+
const result = validateVisualElementReferenceTargets({
|
|
11786
|
+
ownerIdField: "layoutId",
|
|
11787
|
+
ownerId: payload.layoutId,
|
|
11788
|
+
ownerLabel: "layout",
|
|
11515
11789
|
elementId: payload.elementId,
|
|
11516
11790
|
data: payload.data,
|
|
11517
11791
|
state,
|
|
@@ -11628,8 +11902,10 @@ const COMMAND_DEFINITIONS = [
|
|
|
11628
11902
|
};
|
|
11629
11903
|
|
|
11630
11904
|
{
|
|
11631
|
-
const result =
|
|
11632
|
-
|
|
11905
|
+
const result = validateVisualElementReferenceTargets({
|
|
11906
|
+
ownerIdField: "layoutId",
|
|
11907
|
+
ownerId: payload.layoutId,
|
|
11908
|
+
ownerLabel: "layout",
|
|
11633
11909
|
elementId: payload.elementId,
|
|
11634
11910
|
data: mergedData,
|
|
11635
11911
|
state,
|
|
@@ -11754,6 +12030,515 @@ const COMMAND_DEFINITIONS = [
|
|
|
11754
12030
|
return state;
|
|
11755
12031
|
},
|
|
11756
12032
|
},
|
|
12033
|
+
{
|
|
12034
|
+
type: "control.element.create",
|
|
12035
|
+
validatePayload: ({ payload }) => {
|
|
12036
|
+
{
|
|
12037
|
+
const result = validateAllowedKeys({
|
|
12038
|
+
value: payload,
|
|
12039
|
+
allowedKeys: [
|
|
12040
|
+
"controlId",
|
|
12041
|
+
"elementId",
|
|
12042
|
+
"parentId",
|
|
12043
|
+
"data",
|
|
12044
|
+
"index",
|
|
12045
|
+
"position",
|
|
12046
|
+
"positionTargetId",
|
|
12047
|
+
],
|
|
12048
|
+
path: "payload",
|
|
12049
|
+
errorFactory: createPayloadValidationError,
|
|
12050
|
+
});
|
|
12051
|
+
if (result?.valid === false) {
|
|
12052
|
+
return result;
|
|
12053
|
+
}
|
|
12054
|
+
}
|
|
12055
|
+
|
|
12056
|
+
if (!isNonEmptyString(payload.controlId)) {
|
|
12057
|
+
return invalidPayload("payload.controlId must be a non-empty string");
|
|
12058
|
+
}
|
|
12059
|
+
|
|
12060
|
+
if (!isNonEmptyString(payload.elementId)) {
|
|
12061
|
+
return invalidPayload("payload.elementId must be a non-empty string");
|
|
12062
|
+
}
|
|
12063
|
+
|
|
12064
|
+
if (
|
|
12065
|
+
payload.parentId !== undefined &&
|
|
12066
|
+
payload.parentId !== null &&
|
|
12067
|
+
!isNonEmptyString(payload.parentId)
|
|
12068
|
+
) {
|
|
12069
|
+
return invalidPayload(
|
|
12070
|
+
"payload.parentId must be a non-empty string when provided",
|
|
12071
|
+
);
|
|
12072
|
+
}
|
|
12073
|
+
|
|
12074
|
+
{
|
|
12075
|
+
const result = validateLayoutElementCreateData({
|
|
12076
|
+
data: payload.data,
|
|
12077
|
+
errorFactory: createPayloadValidationError,
|
|
12078
|
+
});
|
|
12079
|
+
if (result?.valid === false) {
|
|
12080
|
+
return result;
|
|
12081
|
+
}
|
|
12082
|
+
}
|
|
12083
|
+
|
|
12084
|
+
{
|
|
12085
|
+
const result = validatePlacementFields({
|
|
12086
|
+
payload,
|
|
12087
|
+
errorFactory: createPayloadValidationError,
|
|
12088
|
+
});
|
|
12089
|
+
if (result?.valid === false) {
|
|
12090
|
+
return result;
|
|
12091
|
+
}
|
|
12092
|
+
}
|
|
12093
|
+
},
|
|
12094
|
+
validateAgainstState: ({ state, payload }) => {
|
|
12095
|
+
const control = state.controls.items[payload.controlId];
|
|
12096
|
+
if (!isPlainObject(control) || control.type !== "control") {
|
|
12097
|
+
return invalidPrecondition(
|
|
12098
|
+
"payload.controlId must reference an existing control",
|
|
12099
|
+
);
|
|
12100
|
+
}
|
|
12101
|
+
|
|
12102
|
+
const collection = getControlElementCollection({
|
|
12103
|
+
state,
|
|
12104
|
+
controlId: payload.controlId,
|
|
12105
|
+
});
|
|
12106
|
+
|
|
12107
|
+
if (isPlainObject(collection.items[payload.elementId])) {
|
|
12108
|
+
return invalidPrecondition("payload.elementId must not already exist");
|
|
12109
|
+
}
|
|
12110
|
+
|
|
12111
|
+
const parentId = payload.parentId ?? null;
|
|
12112
|
+
if (parentId !== null) {
|
|
12113
|
+
const parentItem = collection.items[parentId];
|
|
12114
|
+
if (
|
|
12115
|
+
!isPlainObject(parentItem) ||
|
|
12116
|
+
!LAYOUT_CONTAINER_ELEMENT_TYPES.includes(parentItem.type)
|
|
12117
|
+
) {
|
|
12118
|
+
return invalidPrecondition(
|
|
12119
|
+
"payload.parentId must reference a folder or container control element",
|
|
12120
|
+
);
|
|
12121
|
+
}
|
|
12122
|
+
}
|
|
12123
|
+
|
|
12124
|
+
if (payload.positionTargetId !== undefined) {
|
|
12125
|
+
if (!isPlainObject(collection.items[payload.positionTargetId])) {
|
|
12126
|
+
return invalidPrecondition(
|
|
12127
|
+
"payload.positionTargetId must reference an existing control element",
|
|
12128
|
+
);
|
|
12129
|
+
}
|
|
12130
|
+
|
|
12131
|
+
const targetParentId = getNodeParentId({
|
|
12132
|
+
tree: collection.tree,
|
|
12133
|
+
nodeId: payload.positionTargetId,
|
|
12134
|
+
});
|
|
12135
|
+
|
|
12136
|
+
if (targetParentId !== parentId) {
|
|
12137
|
+
return invalidPrecondition(
|
|
12138
|
+
"payload.positionTargetId must reference a sibling under payload.parentId",
|
|
12139
|
+
);
|
|
12140
|
+
}
|
|
12141
|
+
}
|
|
12142
|
+
|
|
12143
|
+
{
|
|
12144
|
+
const result = validateVisualElementReferenceTargets({
|
|
12145
|
+
ownerIdField: "controlId",
|
|
12146
|
+
ownerId: payload.controlId,
|
|
12147
|
+
ownerLabel: "control",
|
|
12148
|
+
elementId: payload.elementId,
|
|
12149
|
+
data: payload.data,
|
|
12150
|
+
state,
|
|
12151
|
+
errorFactory: createPreconditionValidationError,
|
|
12152
|
+
});
|
|
12153
|
+
if (result?.valid === false) {
|
|
12154
|
+
return result;
|
|
12155
|
+
}
|
|
12156
|
+
}
|
|
12157
|
+
},
|
|
12158
|
+
reduce: ({ state, payload }) => {
|
|
12159
|
+
const collection = getControlElementCollection({
|
|
12160
|
+
state,
|
|
12161
|
+
controlId: payload.controlId,
|
|
12162
|
+
});
|
|
12163
|
+
|
|
12164
|
+
collection.items[payload.elementId] = {
|
|
12165
|
+
id: payload.elementId,
|
|
12166
|
+
...structuredClone(payload.data),
|
|
12167
|
+
};
|
|
12168
|
+
|
|
12169
|
+
insertTreeNode({
|
|
12170
|
+
tree: collection.tree,
|
|
12171
|
+
node: {
|
|
12172
|
+
id: payload.elementId,
|
|
12173
|
+
children: [],
|
|
12174
|
+
},
|
|
12175
|
+
parentId: payload.parentId ?? null,
|
|
12176
|
+
index: payload.index,
|
|
12177
|
+
position: payload.position,
|
|
12178
|
+
positionTargetId: payload.positionTargetId,
|
|
12179
|
+
});
|
|
12180
|
+
|
|
12181
|
+
return state;
|
|
12182
|
+
},
|
|
12183
|
+
},
|
|
12184
|
+
{
|
|
12185
|
+
type: "control.element.update",
|
|
12186
|
+
validatePayload: ({ payload }) => {
|
|
12187
|
+
let result = captureValidation(() =>
|
|
12188
|
+
validateAllowedKeys({
|
|
12189
|
+
value: payload,
|
|
12190
|
+
allowedKeys: ["controlId", "elementId", "data", "replace"],
|
|
12191
|
+
path: "payload",
|
|
12192
|
+
errorFactory: createPayloadValidationError,
|
|
12193
|
+
}),
|
|
12194
|
+
);
|
|
12195
|
+
if (!result.valid) {
|
|
12196
|
+
return result;
|
|
12197
|
+
}
|
|
12198
|
+
|
|
12199
|
+
if (!isNonEmptyString(payload.controlId)) {
|
|
12200
|
+
return invalidPayload("payload.controlId must be a non-empty string");
|
|
12201
|
+
}
|
|
12202
|
+
|
|
12203
|
+
if (!isNonEmptyString(payload.elementId)) {
|
|
12204
|
+
return invalidPayload("payload.elementId must be a non-empty string");
|
|
12205
|
+
}
|
|
12206
|
+
|
|
12207
|
+
if (
|
|
12208
|
+
payload.replace !== undefined &&
|
|
12209
|
+
typeof payload.replace !== "boolean"
|
|
12210
|
+
) {
|
|
12211
|
+
return invalidPayload(
|
|
12212
|
+
"payload.replace must be a boolean when provided",
|
|
12213
|
+
);
|
|
12214
|
+
}
|
|
12215
|
+
|
|
12216
|
+
result = captureValidation(() =>
|
|
12217
|
+
validateLayoutElementUpdateData({
|
|
12218
|
+
data: payload.data,
|
|
12219
|
+
replace: payload.replace,
|
|
12220
|
+
errorFactory: createPayloadValidationError,
|
|
12221
|
+
}),
|
|
12222
|
+
);
|
|
12223
|
+
if (!result.valid) {
|
|
12224
|
+
return result;
|
|
12225
|
+
}
|
|
12226
|
+
|
|
12227
|
+
return VALID_RESULT;
|
|
12228
|
+
},
|
|
12229
|
+
validateAgainstState: ({ state, payload }) => {
|
|
12230
|
+
const control = state.controls.items[payload.controlId];
|
|
12231
|
+
if (!isPlainObject(control) || control.type !== "control") {
|
|
12232
|
+
return invalidPrecondition(
|
|
12233
|
+
"payload.controlId must reference an existing control",
|
|
12234
|
+
);
|
|
12235
|
+
}
|
|
12236
|
+
|
|
12237
|
+
const collection = getControlElementCollection({
|
|
12238
|
+
state,
|
|
12239
|
+
controlId: payload.controlId,
|
|
12240
|
+
});
|
|
12241
|
+
const currentItem = collection.items[payload.elementId];
|
|
12242
|
+
if (!isPlainObject(currentItem)) {
|
|
12243
|
+
return invalidPrecondition(
|
|
12244
|
+
"payload.elementId must reference an existing control element",
|
|
12245
|
+
);
|
|
12246
|
+
}
|
|
12247
|
+
|
|
12248
|
+
if (
|
|
12249
|
+
payload.data.type !== undefined &&
|
|
12250
|
+
payload.data.type !== currentItem.type
|
|
12251
|
+
) {
|
|
12252
|
+
return invalidPrecondition("control element type cannot be changed");
|
|
12253
|
+
}
|
|
12254
|
+
|
|
12255
|
+
if (currentItem.type !== "folder") {
|
|
12256
|
+
const mergedData = payload.replace
|
|
12257
|
+
? { ...structuredClone(payload.data) }
|
|
12258
|
+
: {
|
|
12259
|
+
...structuredClone(currentItem),
|
|
12260
|
+
...structuredClone(payload.data),
|
|
12261
|
+
};
|
|
12262
|
+
|
|
12263
|
+
{
|
|
12264
|
+
const result = validateVisualElementReferenceTargets({
|
|
12265
|
+
ownerIdField: "controlId",
|
|
12266
|
+
ownerId: payload.controlId,
|
|
12267
|
+
ownerLabel: "control",
|
|
12268
|
+
elementId: payload.elementId,
|
|
12269
|
+
data: mergedData,
|
|
12270
|
+
state,
|
|
12271
|
+
errorFactory: createPreconditionValidationError,
|
|
12272
|
+
});
|
|
12273
|
+
if (result?.valid === false) {
|
|
12274
|
+
return result;
|
|
12275
|
+
}
|
|
12276
|
+
}
|
|
12277
|
+
}
|
|
12278
|
+
|
|
12279
|
+
if (
|
|
12280
|
+
currentItem.type === "folder" &&
|
|
12281
|
+
Object.keys(payload.data).some((key) => key !== "name")
|
|
12282
|
+
) {
|
|
12283
|
+
return invalidPrecondition(
|
|
12284
|
+
"folder control elements cannot update non-name fields",
|
|
12285
|
+
);
|
|
12286
|
+
}
|
|
12287
|
+
|
|
12288
|
+
return VALID_RESULT;
|
|
12289
|
+
},
|
|
12290
|
+
reduce: ({ state, payload }) => {
|
|
12291
|
+
const collection = getControlElementCollection({
|
|
12292
|
+
state,
|
|
12293
|
+
controlId: payload.controlId,
|
|
12294
|
+
});
|
|
12295
|
+
const currentItem = collection.items[payload.elementId];
|
|
12296
|
+
|
|
12297
|
+
collection.items[payload.elementId] =
|
|
12298
|
+
payload.replace === true
|
|
12299
|
+
? {
|
|
12300
|
+
id: payload.elementId,
|
|
12301
|
+
...structuredClone(payload.data),
|
|
12302
|
+
}
|
|
12303
|
+
: {
|
|
12304
|
+
...structuredClone(currentItem),
|
|
12305
|
+
...structuredClone(payload.data),
|
|
12306
|
+
};
|
|
12307
|
+
|
|
12308
|
+
return state;
|
|
12309
|
+
},
|
|
12310
|
+
},
|
|
12311
|
+
{
|
|
12312
|
+
type: "control.element.delete",
|
|
12313
|
+
validatePayload: ({ payload }) => {
|
|
12314
|
+
{
|
|
12315
|
+
const result = validateExactKeys({
|
|
12316
|
+
value: payload,
|
|
12317
|
+
expectedKeys: ["controlId", "elementIds"],
|
|
12318
|
+
path: "payload",
|
|
12319
|
+
errorFactory: createPayloadValidationError,
|
|
12320
|
+
});
|
|
12321
|
+
if (result?.valid === false) {
|
|
12322
|
+
return result;
|
|
12323
|
+
}
|
|
12324
|
+
}
|
|
12325
|
+
|
|
12326
|
+
if (!isNonEmptyString(payload.controlId)) {
|
|
12327
|
+
return invalidPayload("payload.controlId must be a non-empty string");
|
|
12328
|
+
}
|
|
12329
|
+
|
|
12330
|
+
{
|
|
12331
|
+
const result = validateRequiredUniqueIdArray({
|
|
12332
|
+
value: payload.elementIds,
|
|
12333
|
+
path: "payload.elementIds",
|
|
12334
|
+
errorFactory: createPayloadValidationError,
|
|
12335
|
+
});
|
|
12336
|
+
if (result?.valid === false) {
|
|
12337
|
+
return result;
|
|
12338
|
+
}
|
|
12339
|
+
}
|
|
12340
|
+
},
|
|
12341
|
+
validateAgainstState: ({ state, payload }) => {
|
|
12342
|
+
const control = state.controls.items[payload.controlId];
|
|
12343
|
+
if (!isPlainObject(control) || control.type !== "control") {
|
|
12344
|
+
return invalidPrecondition(
|
|
12345
|
+
"payload.controlId must reference an existing control",
|
|
12346
|
+
);
|
|
12347
|
+
}
|
|
12348
|
+
|
|
12349
|
+
const collection = getControlElementCollection({
|
|
12350
|
+
state,
|
|
12351
|
+
controlId: payload.controlId,
|
|
12352
|
+
});
|
|
12353
|
+
|
|
12354
|
+
for (const elementId of payload.elementIds) {
|
|
12355
|
+
if (!isPlainObject(collection.items[elementId])) {
|
|
12356
|
+
return invalidPrecondition(
|
|
12357
|
+
"payload.elementIds must reference existing control elements",
|
|
12358
|
+
{ elementId },
|
|
12359
|
+
);
|
|
12360
|
+
}
|
|
12361
|
+
}
|
|
12362
|
+
},
|
|
12363
|
+
reduce: ({ state, payload }) => {
|
|
12364
|
+
const collection = getControlElementCollection({
|
|
12365
|
+
state,
|
|
12366
|
+
controlId: payload.controlId,
|
|
12367
|
+
});
|
|
12368
|
+
const deletedIds = new Set();
|
|
12369
|
+
|
|
12370
|
+
for (const elementId of payload.elementIds) {
|
|
12371
|
+
const removedNode = removeTreeNode({
|
|
12372
|
+
nodes: collection.tree,
|
|
12373
|
+
nodeId: elementId,
|
|
12374
|
+
});
|
|
12375
|
+
|
|
12376
|
+
if (!removedNode) {
|
|
12377
|
+
continue;
|
|
12378
|
+
}
|
|
12379
|
+
|
|
12380
|
+
for (const id of collectTreeDescendantIds({ node: removedNode })) {
|
|
12381
|
+
deletedIds.add(id);
|
|
12382
|
+
}
|
|
12383
|
+
}
|
|
12384
|
+
|
|
12385
|
+
for (const elementId of deletedIds) {
|
|
12386
|
+
delete collection.items[elementId];
|
|
12387
|
+
}
|
|
12388
|
+
|
|
12389
|
+
return state;
|
|
12390
|
+
},
|
|
12391
|
+
},
|
|
12392
|
+
{
|
|
12393
|
+
type: "control.element.move",
|
|
12394
|
+
validatePayload: ({ payload }) => {
|
|
12395
|
+
{
|
|
12396
|
+
const result = validateAllowedKeys({
|
|
12397
|
+
value: payload,
|
|
12398
|
+
allowedKeys: [
|
|
12399
|
+
"controlId",
|
|
12400
|
+
"elementId",
|
|
12401
|
+
"parentId",
|
|
12402
|
+
"index",
|
|
12403
|
+
"position",
|
|
12404
|
+
"positionTargetId",
|
|
12405
|
+
],
|
|
12406
|
+
path: "payload",
|
|
12407
|
+
errorFactory: createPayloadValidationError,
|
|
12408
|
+
});
|
|
12409
|
+
if (result?.valid === false) {
|
|
12410
|
+
return result;
|
|
12411
|
+
}
|
|
12412
|
+
}
|
|
12413
|
+
|
|
12414
|
+
if (!isNonEmptyString(payload.controlId)) {
|
|
12415
|
+
return invalidPayload("payload.controlId must be a non-empty string");
|
|
12416
|
+
}
|
|
12417
|
+
|
|
12418
|
+
if (!isNonEmptyString(payload.elementId)) {
|
|
12419
|
+
return invalidPayload("payload.elementId must be a non-empty string");
|
|
12420
|
+
}
|
|
12421
|
+
|
|
12422
|
+
if (
|
|
12423
|
+
payload.parentId !== undefined &&
|
|
12424
|
+
payload.parentId !== null &&
|
|
12425
|
+
!isNonEmptyString(payload.parentId)
|
|
12426
|
+
) {
|
|
12427
|
+
return invalidPayload(
|
|
12428
|
+
"payload.parentId must be a non-empty string when provided",
|
|
12429
|
+
);
|
|
12430
|
+
}
|
|
12431
|
+
|
|
12432
|
+
{
|
|
12433
|
+
const result = validatePlacementFields({
|
|
12434
|
+
payload,
|
|
12435
|
+
errorFactory: createPayloadValidationError,
|
|
12436
|
+
});
|
|
12437
|
+
if (result?.valid === false) {
|
|
12438
|
+
return result;
|
|
12439
|
+
}
|
|
12440
|
+
}
|
|
12441
|
+
},
|
|
12442
|
+
validateAgainstState: ({ state, payload }) => {
|
|
12443
|
+
const control = state.controls.items[payload.controlId];
|
|
12444
|
+
if (!isPlainObject(control) || control.type !== "control") {
|
|
12445
|
+
return invalidPrecondition(
|
|
12446
|
+
"payload.controlId must reference an existing control",
|
|
12447
|
+
);
|
|
12448
|
+
}
|
|
12449
|
+
|
|
12450
|
+
const collection = getControlElementCollection({
|
|
12451
|
+
state,
|
|
12452
|
+
controlId: payload.controlId,
|
|
12453
|
+
});
|
|
12454
|
+
const currentItem = collection.items[payload.elementId];
|
|
12455
|
+
|
|
12456
|
+
if (!isPlainObject(currentItem)) {
|
|
12457
|
+
return invalidPrecondition(
|
|
12458
|
+
"payload.elementId must reference an existing control element",
|
|
12459
|
+
);
|
|
12460
|
+
}
|
|
12461
|
+
|
|
12462
|
+
const currentNode = findTreeNode({
|
|
12463
|
+
nodes: collection.tree,
|
|
12464
|
+
nodeId: payload.elementId,
|
|
12465
|
+
});
|
|
12466
|
+
|
|
12467
|
+
if (payload.parentId !== undefined && payload.parentId !== null) {
|
|
12468
|
+
const parentItem = collection.items[payload.parentId];
|
|
12469
|
+
if (
|
|
12470
|
+
!isPlainObject(parentItem) ||
|
|
12471
|
+
!LAYOUT_CONTAINER_ELEMENT_TYPES.includes(parentItem.type)
|
|
12472
|
+
) {
|
|
12473
|
+
return invalidPrecondition(
|
|
12474
|
+
"payload.parentId must reference a folder or container control element",
|
|
12475
|
+
);
|
|
12476
|
+
}
|
|
12477
|
+
|
|
12478
|
+
const descendantIds = new Set(
|
|
12479
|
+
collectTreeDescendantIds({
|
|
12480
|
+
node: currentNode,
|
|
12481
|
+
}),
|
|
12482
|
+
);
|
|
12483
|
+
|
|
12484
|
+
if (descendantIds.has(payload.parentId)) {
|
|
12485
|
+
return invalidPrecondition(
|
|
12486
|
+
"payload.parentId must not target the moved control element or its descendants",
|
|
12487
|
+
);
|
|
12488
|
+
}
|
|
12489
|
+
}
|
|
12490
|
+
|
|
12491
|
+
if (payload.positionTargetId !== undefined) {
|
|
12492
|
+
if (payload.positionTargetId === payload.elementId) {
|
|
12493
|
+
return invalidPrecondition(
|
|
12494
|
+
"payload.positionTargetId must not reference the moved control element",
|
|
12495
|
+
);
|
|
12496
|
+
}
|
|
12497
|
+
|
|
12498
|
+
if (!isPlainObject(collection.items[payload.positionTargetId])) {
|
|
12499
|
+
return invalidPrecondition(
|
|
12500
|
+
"payload.positionTargetId must reference an existing control element",
|
|
12501
|
+
);
|
|
12502
|
+
}
|
|
12503
|
+
|
|
12504
|
+
const targetParentId = getNodeParentId({
|
|
12505
|
+
tree: collection.tree,
|
|
12506
|
+
nodeId: payload.positionTargetId,
|
|
12507
|
+
});
|
|
12508
|
+
|
|
12509
|
+
if (targetParentId !== (payload.parentId ?? null)) {
|
|
12510
|
+
return invalidPrecondition(
|
|
12511
|
+
"payload.positionTargetId must reference a sibling under payload.parentId",
|
|
12512
|
+
);
|
|
12513
|
+
}
|
|
12514
|
+
}
|
|
12515
|
+
},
|
|
12516
|
+
reduce: ({ state, payload }) => {
|
|
12517
|
+
const collection = getControlElementCollection({
|
|
12518
|
+
state,
|
|
12519
|
+
controlId: payload.controlId,
|
|
12520
|
+
});
|
|
12521
|
+
const nodeResult = removeNodeOrResult({
|
|
12522
|
+
tree: collection.tree,
|
|
12523
|
+
nodeId: payload.elementId,
|
|
12524
|
+
errorMessage: "control element move target missing from tree",
|
|
12525
|
+
});
|
|
12526
|
+
if (!nodeResult.valid) {
|
|
12527
|
+
return nodeResult;
|
|
12528
|
+
}
|
|
12529
|
+
|
|
12530
|
+
insertTreeNode({
|
|
12531
|
+
tree: collection.tree,
|
|
12532
|
+
node: nodeResult.node,
|
|
12533
|
+
parentId: payload.parentId ?? null,
|
|
12534
|
+
index: payload.index,
|
|
12535
|
+
position: payload.position,
|
|
12536
|
+
positionTargetId: payload.positionTargetId,
|
|
12537
|
+
});
|
|
12538
|
+
|
|
12539
|
+
return state;
|
|
12540
|
+
},
|
|
12541
|
+
},
|
|
11757
12542
|
{
|
|
11758
12543
|
type: "layout.element.move",
|
|
11759
12544
|
validatePayload: ({ payload }) => {
|
|
@@ -11942,6 +12727,8 @@ export const validatePayload = ({ type, payload }) => {
|
|
|
11942
12727
|
|
|
11943
12728
|
export const validateAgainstState = ({ state, command }) => {
|
|
11944
12729
|
return captureValidation(() => {
|
|
12730
|
+
const normalizedState = normalizeStateCollections(state);
|
|
12731
|
+
|
|
11945
12732
|
if (!isPlainObject(command)) {
|
|
11946
12733
|
return invalidPrecondition("command must be an object");
|
|
11947
12734
|
}
|
|
@@ -11954,7 +12741,7 @@ export const validateAgainstState = ({ state, command }) => {
|
|
|
11954
12741
|
);
|
|
11955
12742
|
}
|
|
11956
12743
|
|
|
11957
|
-
const stateResult = validateState({ state });
|
|
12744
|
+
const stateResult = validateState({ state: normalizedState });
|
|
11958
12745
|
if (!stateResult.valid) {
|
|
11959
12746
|
if (stateResult.error.kind === "invariant") {
|
|
11960
12747
|
return invalidInvariant(
|
|
@@ -11976,7 +12763,7 @@ export const validateAgainstState = ({ state, command }) => {
|
|
|
11976
12763
|
|
|
11977
12764
|
const validationResult = captureValidation(() =>
|
|
11978
12765
|
definition.validateAgainstState({
|
|
11979
|
-
state,
|
|
12766
|
+
state: normalizedState,
|
|
11980
12767
|
payload: command.payload,
|
|
11981
12768
|
}),
|
|
11982
12769
|
);
|
|
@@ -11987,12 +12774,16 @@ export const validateAgainstState = ({ state, command }) => {
|
|
|
11987
12774
|
|
|
11988
12775
|
export const processCommand = ({ state, command }) => {
|
|
11989
12776
|
return captureValidation(() => {
|
|
12777
|
+
const normalizedState = normalizeStateCollections(state);
|
|
12778
|
+
const shouldMaterializeControls =
|
|
12779
|
+
typeof command?.type === "string" && command.type.startsWith("control.");
|
|
12780
|
+
|
|
11990
12781
|
if (!isPlainObject(command)) {
|
|
11991
12782
|
return invalidPrecondition("command must be an object");
|
|
11992
12783
|
}
|
|
11993
12784
|
|
|
11994
12785
|
const preconditionResult = validateAgainstState({
|
|
11995
|
-
state,
|
|
12786
|
+
state: normalizedState,
|
|
11996
12787
|
command,
|
|
11997
12788
|
});
|
|
11998
12789
|
if (!preconditionResult.valid) {
|
|
@@ -12005,14 +12796,21 @@ export const processCommand = ({ state, command }) => {
|
|
|
12005
12796
|
}
|
|
12006
12797
|
|
|
12007
12798
|
const nextState = definition.reduce({
|
|
12008
|
-
state: structuredClone(
|
|
12799
|
+
state: structuredClone(
|
|
12800
|
+
shouldMaterializeControls ? normalizedState : state,
|
|
12801
|
+
),
|
|
12009
12802
|
payload: command.payload,
|
|
12010
12803
|
});
|
|
12011
12804
|
if (nextState?.valid === false) {
|
|
12012
12805
|
return nextState;
|
|
12013
12806
|
}
|
|
12014
12807
|
|
|
12015
|
-
const finalState =
|
|
12808
|
+
const finalState =
|
|
12809
|
+
nextState === undefined
|
|
12810
|
+
? shouldMaterializeControls
|
|
12811
|
+
? normalizedState
|
|
12812
|
+
: state
|
|
12813
|
+
: nextState;
|
|
12016
12814
|
const stateResult = validateState({
|
|
12017
12815
|
state: finalState,
|
|
12018
12816
|
});
|