@routevn/creator-model 1.2.10 → 1.3.1

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/model.js +2959 -342
package/src/model.js CHANGED
@@ -35,7 +35,7 @@ const COLLECTION_KEYS = [
35
35
  "layouts",
36
36
  "controls",
37
37
  ];
38
- const ROOT_KEYS = ["project", "story", ...COLLECTION_KEYS];
38
+ const ROOT_KEYS = ["project", "story", "tags", ...COLLECTION_KEYS];
39
39
  const LINE_UPDATE_ACTIONS_PRESERVE_PATHS = ["dialogue.content"];
40
40
  const LINE_UPDATE_ACTIONS_PRESERVE_PATHS_SET = new Set(
41
41
  LINE_UPDATE_ACTIONS_PRESERVE_PATHS,
@@ -44,6 +44,73 @@ const createEmptyCollectionState = () => ({
44
44
  items: {},
45
45
  tree: [],
46
46
  });
47
+ const TAG_SCOPE_BASE_KEYS = [
48
+ "images",
49
+ "sounds",
50
+ "videos",
51
+ "characters",
52
+ "fonts",
53
+ "transforms",
54
+ "colors",
55
+ "textStyles",
56
+ "variables",
57
+ "layouts",
58
+ "controls",
59
+ "animations",
60
+ "particles",
61
+ "spritesheets",
62
+ ];
63
+ const CHARACTER_SPRITE_TAG_SCOPE_PREFIX = "characterSprites:";
64
+ const createEmptyTagsState = () => ({
65
+ images: createEmptyCollectionState(),
66
+ sounds: createEmptyCollectionState(),
67
+ videos: createEmptyCollectionState(),
68
+ characters: createEmptyCollectionState(),
69
+ fonts: createEmptyCollectionState(),
70
+ transforms: createEmptyCollectionState(),
71
+ colors: createEmptyCollectionState(),
72
+ textStyles: createEmptyCollectionState(),
73
+ variables: createEmptyCollectionState(),
74
+ layouts: createEmptyCollectionState(),
75
+ controls: createEmptyCollectionState(),
76
+ animations: createEmptyCollectionState(),
77
+ particles: createEmptyCollectionState(),
78
+ spritesheets: createEmptyCollectionState(),
79
+ });
80
+ const isCharacterSpriteTagScopeKey = (value) =>
81
+ isNonEmptyString(value) &&
82
+ value.startsWith(CHARACTER_SPRITE_TAG_SCOPE_PREFIX) &&
83
+ value.length > CHARACTER_SPRITE_TAG_SCOPE_PREFIX.length;
84
+ const getCharacterSpriteTagScopeCharacterId = (scopeKey) =>
85
+ isCharacterSpriteTagScopeKey(scopeKey)
86
+ ? scopeKey.slice(CHARACTER_SPRITE_TAG_SCOPE_PREFIX.length)
87
+ : undefined;
88
+ const isBaseTagScopeKey = (scopeKey) => TAG_SCOPE_BASE_KEYS.includes(scopeKey);
89
+ const normalizeTagsState = (tags) => {
90
+ if (tags === undefined) {
91
+ return createEmptyTagsState();
92
+ }
93
+
94
+ if (!isPlainObject(tags)) {
95
+ return tags;
96
+ }
97
+
98
+ const missingBaseScopeKeys = TAG_SCOPE_BASE_KEYS.filter(
99
+ (scopeKey) => tags[scopeKey] === undefined,
100
+ );
101
+ if (missingBaseScopeKeys.length === 0) {
102
+ return tags;
103
+ }
104
+
105
+ const nextTags = {
106
+ ...tags,
107
+ };
108
+ for (const scopeKey of missingBaseScopeKeys) {
109
+ nextTags[scopeKey] = createEmptyCollectionState();
110
+ }
111
+
112
+ return nextTags;
113
+ };
47
114
 
48
115
  const normalizeStateCollections = (state) => {
49
116
  if (!isPlainObject(state)) {
@@ -55,8 +122,10 @@ const normalizeStateCollections = (state) => {
55
122
  "particles",
56
123
  "controls",
57
124
  ].filter((key) => state[key] === undefined);
125
+ const normalizedTags = normalizeTagsState(state.tags);
126
+ const hasNormalizedTags = normalizedTags !== state.tags;
58
127
 
59
- if (missingCollectionKeys.length === 0) {
128
+ if (missingCollectionKeys.length === 0 && !hasNormalizedTags) {
60
129
  return state;
61
130
  }
62
131
 
@@ -68,6 +137,10 @@ const normalizeStateCollections = (state) => {
68
137
  nextState[key] = createEmptyCollectionState();
69
138
  });
70
139
 
140
+ if (hasNormalizedTags) {
141
+ nextState.tags = normalizedTags;
142
+ }
143
+
71
144
  return nextState;
72
145
  };
73
146
  const isString = (value) => typeof value === "string";
@@ -164,7 +237,7 @@ const LAYOUT_ELEMENT_BASE_TYPES = [
164
237
  "container-ref-confirm-dialog-ok",
165
238
  "container-ref-confirm-dialog-cancel",
166
239
  ];
167
- export const SCHEMA_VERSION = 2;
240
+ export const SCHEMA_VERSION = 3;
168
241
  const LAYOUT_CONTAINER_ELEMENT_TYPES = [
169
242
  "folder",
170
243
  "container",
@@ -268,6 +341,7 @@ const isVariableReferenceTarget = (state, variableId) => {
268
341
  };
269
342
 
270
343
  const LAYOUT_ITEM_TARGET_SET = new Set(["item.savedAt"]);
344
+ const LAYOUT_DIALOGUE_TARGET_SET = new Set(["dialogue.characterId"]);
271
345
  const RUNTIME_TARGET_DOT_PATTERN = /^runtime\.([A-Za-z_$][A-Za-z0-9_$]*)$/;
272
346
  const VARIABLE_TARGET_DOT_PATTERN = /^variables\.([A-Za-z_$][A-Za-z0-9_$]*)$/;
273
347
  const VARIABLE_TARGET_BRACKET_PATTERN = /^variables\[(.+)\]$/;
@@ -284,6 +358,13 @@ const parseLayoutConditionTarget = (target) => {
284
358
  };
285
359
  }
286
360
 
361
+ if (LAYOUT_DIALOGUE_TARGET_SET.has(target)) {
362
+ return {
363
+ kind: "dialogue",
364
+ target,
365
+ };
366
+ }
367
+
287
368
  const runtimeMatch = target.match(RUNTIME_TARGET_DOT_PATTERN);
288
369
  if (runtimeMatch && isRuntimeFieldId(runtimeMatch[1])) {
289
370
  return {
@@ -340,7 +421,7 @@ const isLayoutConditionTarget = (state, target) => {
340
421
  return false;
341
422
  }
342
423
 
343
- if (parsedTarget.kind === "runtime") {
424
+ if (parsedTarget.kind !== "variable") {
344
425
  return true;
345
426
  }
346
427
 
@@ -781,6 +862,7 @@ const validateImageItems = ({ items, path, errorFactory }) => {
781
862
  "type",
782
863
  "name",
783
864
  "description",
865
+ "tagIds",
784
866
  "thumbnailFileId",
785
867
  "fileId",
786
868
  "width",
@@ -823,6 +905,18 @@ const validateImageItems = ({ items, path, errorFactory }) => {
823
905
  }
824
906
 
825
907
  if (item.type === "image") {
908
+ {
909
+ const result = validateOptionalUniqueIdArray({
910
+ value: item.tagIds,
911
+ path: `${itemPath}.tagIds`,
912
+ errorFactory,
913
+ allowEmpty: false,
914
+ });
915
+ if (result?.valid === false) {
916
+ return result;
917
+ }
918
+ }
919
+
826
920
  if (
827
921
  item.thumbnailFileId !== undefined &&
828
922
  !isNonEmptyString(item.thumbnailFileId)
@@ -948,6 +1042,7 @@ const validateSpritesheetItems = ({ items, path, errorFactory }) => {
948
1042
  "type",
949
1043
  "name",
950
1044
  "description",
1045
+ "tagIds",
951
1046
  "thumbnailFileId",
952
1047
  "fileId",
953
1048
  "sheetWidth",
@@ -995,6 +1090,18 @@ const validateSpritesheetItems = ({ items, path, errorFactory }) => {
995
1090
  }
996
1091
 
997
1092
  if (item.type === "spritesheet") {
1093
+ {
1094
+ const result = validateOptionalUniqueIdArray({
1095
+ value: item.tagIds,
1096
+ path: `${itemPath}.tagIds`,
1097
+ errorFactory,
1098
+ allowEmpty: false,
1099
+ });
1100
+ if (result?.valid === false) {
1101
+ return result;
1102
+ }
1103
+ }
1104
+
998
1105
  if (
999
1106
  item.thumbnailFileId !== undefined &&
1000
1107
  !isNonEmptyString(item.thumbnailFileId)
@@ -1070,6 +1177,7 @@ const validateSoundItems = ({ items, path, errorFactory }) => {
1070
1177
  "type",
1071
1178
  "name",
1072
1179
  "description",
1180
+ "tagIds",
1073
1181
  "fileId",
1074
1182
  "waveformDataFileId",
1075
1183
  "duration",
@@ -1111,6 +1219,18 @@ const validateSoundItems = ({ items, path, errorFactory }) => {
1111
1219
  }
1112
1220
 
1113
1221
  if (item.type === "sound") {
1222
+ {
1223
+ const result = validateOptionalUniqueIdArray({
1224
+ value: item.tagIds,
1225
+ path: `${itemPath}.tagIds`,
1226
+ errorFactory,
1227
+ allowEmpty: false,
1228
+ });
1229
+ if (result?.valid === false) {
1230
+ return result;
1231
+ }
1232
+ }
1233
+
1114
1234
  if (!isNonEmptyString(item.fileId)) {
1115
1235
  return invalidFromErrorFactory(
1116
1236
  errorFactory,
@@ -1161,6 +1281,7 @@ const validateVideoItems = ({ items, path, errorFactory }) => {
1161
1281
  "type",
1162
1282
  "name",
1163
1283
  "description",
1284
+ "tagIds",
1164
1285
  "fileId",
1165
1286
  "thumbnailFileId",
1166
1287
  "duration",
@@ -1204,6 +1325,18 @@ const validateVideoItems = ({ items, path, errorFactory }) => {
1204
1325
  }
1205
1326
 
1206
1327
  if (item.type === "video") {
1328
+ {
1329
+ const result = validateOptionalUniqueIdArray({
1330
+ value: item.tagIds,
1331
+ path: `${itemPath}.tagIds`,
1332
+ errorFactory,
1333
+ allowEmpty: false,
1334
+ });
1335
+ if (result?.valid === false) {
1336
+ return result;
1337
+ }
1338
+ }
1339
+
1207
1340
  if (!isNonEmptyString(item.fileId)) {
1208
1341
  return invalidFromErrorFactory(
1209
1342
  errorFactory,
@@ -1868,7 +2001,7 @@ const validateAnimationItems = ({ items, path, errorFactory }) => {
1868
2001
  allowedKeys:
1869
2002
  item.type === "folder"
1870
2003
  ? ["id", "type", "name", "description"]
1871
- : ["id", "type", "name", "description", "animation"],
2004
+ : ["id", "type", "name", "description", "tagIds", "animation"],
1872
2005
  path: itemPath,
1873
2006
  errorFactory,
1874
2007
  });
@@ -1906,6 +2039,18 @@ const validateAnimationItems = ({ items, path, errorFactory }) => {
1906
2039
  }
1907
2040
 
1908
2041
  if (item.type === "animation") {
2042
+ {
2043
+ const result = validateOptionalUniqueIdArray({
2044
+ value: item.tagIds,
2045
+ path: `${itemPath}.tagIds`,
2046
+ errorFactory,
2047
+ allowEmpty: false,
2048
+ });
2049
+ if (result?.valid === false) {
2050
+ return result;
2051
+ }
2052
+ }
2053
+
1909
2054
  {
1910
2055
  const result = validateAnimationDefinition({
1911
2056
  animation: item.animation,
@@ -1942,6 +2087,7 @@ const validateFontItems = ({ items, path, errorFactory }) => {
1942
2087
  "type",
1943
2088
  "name",
1944
2089
  "description",
2090
+ "tagIds",
1945
2091
  "fileId",
1946
2092
  "fontFamily",
1947
2093
  ],
@@ -1982,6 +2128,18 @@ const validateFontItems = ({ items, path, errorFactory }) => {
1982
2128
  }
1983
2129
 
1984
2130
  if (item.type === "font") {
2131
+ {
2132
+ const result = validateOptionalUniqueIdArray({
2133
+ value: item.tagIds,
2134
+ path: `${itemPath}.tagIds`,
2135
+ errorFactory,
2136
+ allowEmpty: false,
2137
+ });
2138
+ if (result?.valid === false) {
2139
+ return result;
2140
+ }
2141
+ }
2142
+
1985
2143
  if (!isNonEmptyString(item.fileId)) {
1986
2144
  return invalidFromErrorFactory(
1987
2145
  errorFactory,
@@ -1995,7 +2153,6 @@ const validateFontItems = ({ items, path, errorFactory }) => {
1995
2153
  `${itemPath}.fontFamily must be a non-empty string`,
1996
2154
  );
1997
2155
  }
1998
-
1999
2156
  }
2000
2157
  }
2001
2158
  };
@@ -2017,7 +2174,7 @@ const validateColorItems = ({ items, path, errorFactory }) => {
2017
2174
  allowedKeys:
2018
2175
  item.type === "folder"
2019
2176
  ? ["id", "type", "name", "description"]
2020
- : ["id", "type", "name", "description", "hex"],
2177
+ : ["id", "type", "name", "description", "tagIds", "hex"],
2021
2178
  path: itemPath,
2022
2179
  errorFactory,
2023
2180
  });
@@ -2054,11 +2211,25 @@ const validateColorItems = ({ items, path, errorFactory }) => {
2054
2211
  );
2055
2212
  }
2056
2213
 
2057
- if (item.type === "color" && !isHexColor(item.hex)) {
2058
- return invalidFromErrorFactory(
2059
- errorFactory,
2060
- `${itemPath}.hex must be a #RRGGBB string`,
2061
- );
2214
+ if (item.type === "color") {
2215
+ {
2216
+ const result = validateOptionalUniqueIdArray({
2217
+ value: item.tagIds,
2218
+ path: `${itemPath}.tagIds`,
2219
+ errorFactory,
2220
+ allowEmpty: false,
2221
+ });
2222
+ if (result?.valid === false) {
2223
+ return result;
2224
+ }
2225
+ }
2226
+
2227
+ if (!isHexColor(item.hex)) {
2228
+ return invalidFromErrorFactory(
2229
+ errorFactory,
2230
+ `${itemPath}.hex must be a #RRGGBB string`,
2231
+ );
2232
+ }
2062
2233
  }
2063
2234
  }
2064
2235
  };
@@ -2085,6 +2256,7 @@ const validateTransformItems = ({ items, path, errorFactory }) => {
2085
2256
  "type",
2086
2257
  "name",
2087
2258
  "description",
2259
+ "tagIds",
2088
2260
  "x",
2089
2261
  "y",
2090
2262
  "scaleX",
@@ -2130,6 +2302,18 @@ const validateTransformItems = ({ items, path, errorFactory }) => {
2130
2302
  }
2131
2303
 
2132
2304
  if (item.type === "transform") {
2305
+ {
2306
+ const result = validateOptionalUniqueIdArray({
2307
+ value: item.tagIds,
2308
+ path: `${itemPath}.tagIds`,
2309
+ errorFactory,
2310
+ allowEmpty: false,
2311
+ });
2312
+ if (result?.valid === false) {
2313
+ return result;
2314
+ }
2315
+ }
2316
+
2133
2317
  for (const key of [
2134
2318
  "x",
2135
2319
  "y",
@@ -2206,6 +2390,7 @@ const validateParticleItems = ({ items, path, errorFactory }) => {
2206
2390
  "type",
2207
2391
  "name",
2208
2392
  "description",
2393
+ "tagIds",
2209
2394
  "width",
2210
2395
  "height",
2211
2396
  "seed",
@@ -2251,6 +2436,18 @@ const validateParticleItems = ({ items, path, errorFactory }) => {
2251
2436
  continue;
2252
2437
  }
2253
2438
 
2439
+ {
2440
+ const result = validateOptionalUniqueIdArray({
2441
+ value: item.tagIds,
2442
+ path: `${itemPath}.tagIds`,
2443
+ errorFactory,
2444
+ allowEmpty: false,
2445
+ });
2446
+ if (result?.valid === false) {
2447
+ return result;
2448
+ }
2449
+ }
2450
+
2254
2451
  if (!isFiniteNumber(item.width) || item.width <= 0) {
2255
2452
  return invalidFromErrorFactory(
2256
2453
  errorFactory,
@@ -2333,6 +2530,7 @@ const validateVariableItems = ({ items, path, errorFactory }) => {
2333
2530
  "type",
2334
2531
  "name",
2335
2532
  "description",
2533
+ "tagIds",
2336
2534
  "scope",
2337
2535
  "default",
2338
2536
  "value",
@@ -2374,6 +2572,18 @@ const validateVariableItems = ({ items, path, errorFactory }) => {
2374
2572
  }
2375
2573
 
2376
2574
  if (variableType !== "folder") {
2575
+ {
2576
+ const result = validateOptionalUniqueIdArray({
2577
+ value: item.tagIds,
2578
+ path: `${itemPath}.tagIds`,
2579
+ errorFactory,
2580
+ allowEmpty: false,
2581
+ });
2582
+ if (result?.valid === false) {
2583
+ return result;
2584
+ }
2585
+ }
2586
+
2377
2587
  if (!VARIABLE_SCOPE_KEYS.includes(item.scope)) {
2378
2588
  return invalidFromErrorFactory(
2379
2589
  errorFactory,
@@ -2429,6 +2639,7 @@ const validateTextStyleItems = ({ items, path, errorFactory }) => {
2429
2639
  "type",
2430
2640
  "name",
2431
2641
  "description",
2642
+ "tagIds",
2432
2643
  "fontId",
2433
2644
  "colorId",
2434
2645
  "fontSize",
@@ -2481,6 +2692,18 @@ const validateTextStyleItems = ({ items, path, errorFactory }) => {
2481
2692
  }
2482
2693
 
2483
2694
  if (item.type === "textStyle") {
2695
+ {
2696
+ const result = validateOptionalUniqueIdArray({
2697
+ value: item.tagIds,
2698
+ path: `${itemPath}.tagIds`,
2699
+ errorFactory,
2700
+ allowEmpty: false,
2701
+ });
2702
+ if (result?.valid === false) {
2703
+ return result;
2704
+ }
2705
+ }
2706
+
2484
2707
  if (!isNonEmptyString(item.fontId)) {
2485
2708
  return invalidFromErrorFactory(
2486
2709
  errorFactory,
@@ -2594,7 +2817,12 @@ const validateTextStyleItems = ({ items, path, errorFactory }) => {
2594
2817
  }
2595
2818
  };
2596
2819
 
2597
- const validateCharacterSpriteItems = ({ items, path, errorFactory }) => {
2820
+ const validateCharacterSpriteItems = ({
2821
+ items,
2822
+ path,
2823
+ errorFactory,
2824
+ allowTagIds = true,
2825
+ }) => {
2598
2826
  for (const [itemId, item] of Object.entries(items)) {
2599
2827
  const itemPath = `${path}.${itemId}`;
2600
2828
 
@@ -2616,6 +2844,7 @@ const validateCharacterSpriteItems = ({ items, path, errorFactory }) => {
2616
2844
  "type",
2617
2845
  "name",
2618
2846
  "description",
2847
+ ...(allowTagIds ? ["tagIds"] : []),
2619
2848
  "thumbnailFileId",
2620
2849
  "fileId",
2621
2850
  "width",
@@ -2658,6 +2887,20 @@ const validateCharacterSpriteItems = ({ items, path, errorFactory }) => {
2658
2887
  }
2659
2888
 
2660
2889
  if (item.type === "image") {
2890
+ if (allowTagIds) {
2891
+ {
2892
+ const result = validateOptionalUniqueIdArray({
2893
+ value: item.tagIds,
2894
+ path: `${itemPath}.tagIds`,
2895
+ errorFactory,
2896
+ allowEmpty: false,
2897
+ });
2898
+ if (result?.valid === false) {
2899
+ return result;
2900
+ }
2901
+ }
2902
+ }
2903
+
2661
2904
  if (
2662
2905
  item.thumbnailFileId !== undefined &&
2663
2906
  !isNonEmptyString(item.thumbnailFileId)
@@ -3453,6 +3696,8 @@ const validateCharacterItems = ({ items, path, errorFactory }) => {
3453
3696
  "type",
3454
3697
  "name",
3455
3698
  "description",
3699
+ "tagIds",
3700
+ "spriteGroups",
3456
3701
  "shortcut",
3457
3702
  "fileId",
3458
3703
  "sprites",
@@ -3494,6 +3739,31 @@ const validateCharacterItems = ({ items, path, errorFactory }) => {
3494
3739
  }
3495
3740
 
3496
3741
  if (item.type === "character") {
3742
+ {
3743
+ const result = validateOptionalUniqueIdArray({
3744
+ value: item.tagIds,
3745
+ path: `${itemPath}.tagIds`,
3746
+ errorFactory,
3747
+ allowEmpty: false,
3748
+ });
3749
+ if (result?.valid === false) {
3750
+ return result;
3751
+ }
3752
+ }
3753
+
3754
+ {
3755
+ const result = validateCharacterSpriteGroups({
3756
+ value: item.spriteGroups,
3757
+ path: `${itemPath}.spriteGroups`,
3758
+ errorFactory,
3759
+ allowEmpty: false,
3760
+ allowMissingId: true,
3761
+ });
3762
+ if (result?.valid === false) {
3763
+ return result;
3764
+ }
3765
+ }
3766
+
3497
3767
  if (item.shortcut !== undefined && !isString(item.shortcut)) {
3498
3768
  return invalidFromErrorFactory(
3499
3769
  errorFactory,
@@ -3525,80 +3795,23 @@ const validateCharacterItems = ({ items, path, errorFactory }) => {
3525
3795
  }
3526
3796
  };
3527
3797
 
3528
- const validateKeyboardMap = ({ value, path, errorFactory }) => {
3529
- if (value === undefined) {
3530
- return VALID_RESULT;
3531
- }
3532
-
3533
- if (!isPlainObject(value)) {
3534
- return invalidFromErrorFactory(
3535
- errorFactory,
3536
- `${path} must be an object when provided`,
3537
- );
3538
- }
3539
-
3540
- for (const [key, interaction] of Object.entries(value)) {
3541
- if (!isNonEmptyString(key)) {
3542
- return invalidFromErrorFactory(
3543
- errorFactory,
3544
- `${path} keys must be non-empty strings`,
3545
- );
3546
- }
3547
-
3548
- if (!isPlainObject(interaction)) {
3549
- return invalidFromErrorFactory(
3550
- errorFactory,
3551
- `${path}.${key} must be an object`,
3552
- );
3553
- }
3554
- }
3555
-
3556
- return VALID_RESULT;
3557
- };
3558
-
3559
- const validatePreviewObject = ({ value, path, errorFactory }) => {
3560
- if (value === undefined) {
3561
- return VALID_RESULT;
3562
- }
3563
-
3564
- if (!isPlainObject(value)) {
3565
- return invalidFromErrorFactory(
3566
- errorFactory,
3567
- `${path} must be an object when provided`,
3568
- );
3569
- }
3570
-
3571
- return VALID_RESULT;
3572
- };
3798
+ const validateTagItems = ({ items, path, errorFactory }) => {
3799
+ const seenNames = new Set();
3573
3800
 
3574
- const validateLayoutItems = ({ items, path, errorFactory }) => {
3575
3801
  for (const [itemId, item] of Object.entries(items)) {
3576
3802
  const itemPath = `${path}.${itemId}`;
3577
3803
 
3578
- if (item?.type !== "folder" && item?.type !== "layout") {
3804
+ if (item?.type !== "tag") {
3579
3805
  return invalidFromErrorFactory(
3580
3806
  errorFactory,
3581
- `${itemPath}.type must be 'folder' or 'layout'`,
3807
+ `${itemPath}.type must be 'tag'`,
3582
3808
  );
3583
3809
  }
3584
3810
 
3585
3811
  {
3586
3812
  const result = validateAllowedKeys({
3587
3813
  value: item,
3588
- allowedKeys:
3589
- item.type === "folder"
3590
- ? ["id", "type", "name", "description"]
3591
- : [
3592
- "id",
3593
- "type",
3594
- "name",
3595
- "description",
3596
- "layoutType",
3597
- "isFragment",
3598
- "thumbnailFileId",
3599
- "preview",
3600
- "elements",
3601
- ],
3814
+ allowedKeys: ["id", "type", "name", "color"],
3602
3815
  path: itemPath,
3603
3816
  errorFactory,
3604
3817
  });
@@ -3628,55 +3841,790 @@ const validateLayoutItems = ({ items, path, errorFactory }) => {
3628
3841
  );
3629
3842
  }
3630
3843
 
3631
- if (item.description !== undefined && !isString(item.description)) {
3844
+ if (item.color !== undefined && !isHexColor(item.color)) {
3632
3845
  return invalidFromErrorFactory(
3633
3846
  errorFactory,
3634
- `${itemPath}.description must be a string when provided`,
3847
+ `${itemPath}.color must be a hex color when provided`,
3635
3848
  );
3636
3849
  }
3637
3850
 
3638
- if (
3639
- item.thumbnailFileId !== undefined &&
3640
- !isNonEmptyString(item.thumbnailFileId)
3641
- ) {
3851
+ const normalizedName = item.name.trim().toLowerCase();
3852
+ if (seenNames.has(normalizedName)) {
3642
3853
  return invalidFromErrorFactory(
3643
3854
  errorFactory,
3644
- `${itemPath}.thumbnailFileId must be a non-empty string when provided`,
3855
+ `${itemPath}.name must be unique within its tag scope`,
3645
3856
  );
3646
3857
  }
3647
3858
 
3648
- {
3649
- const result = validatePreviewObject({
3650
- value: item.preview,
3651
- path: `${itemPath}.preview`,
3652
- errorFactory,
3653
- });
3654
- if (result?.valid === false) {
3655
- return result;
3656
- }
3859
+ seenNames.add(normalizedName);
3860
+ }
3861
+ };
3862
+
3863
+ const validateFlatTagTree = ({ nodes, path, errorFactory }) => {
3864
+ const visitNodes = (entries, entryPath) => {
3865
+ if (!Array.isArray(entries)) {
3866
+ return VALID_RESULT;
3657
3867
  }
3658
3868
 
3659
- if (item.type === "layout") {
3660
- if (!LAYOUT_TYPE_KEYS.includes(item.layoutType)) {
3869
+ for (const [index, node] of entries.entries()) {
3870
+ if (Object.hasOwn(node, "children")) {
3661
3871
  return invalidFromErrorFactory(
3662
3872
  errorFactory,
3663
- `${itemPath}.layoutType must be 'general', 'save-load', 'confirmDialog', 'dialogue-adv', 'dialogue-nvl', 'choice', or 'history'`,
3873
+ `${entryPath}[${index}].children is not allowed`,
3664
3874
  );
3665
3875
  }
3876
+ }
3666
3877
 
3667
- if (
3668
- item.isFragment !== undefined &&
3669
- typeof item.isFragment !== "boolean"
3670
- ) {
3671
- return invalidFromErrorFactory(
3672
- errorFactory,
3673
- `${itemPath}.isFragment must be a boolean when provided`,
3674
- );
3675
- }
3878
+ return VALID_RESULT;
3879
+ };
3676
3880
 
3677
- {
3678
- const result = validateNestedCollection({
3679
- collection: item.elements,
3881
+ return visitNodes(nodes, path);
3882
+ };
3883
+
3884
+ const validateTagCreateData = ({
3885
+ data,
3886
+ path = "payload.data",
3887
+ errorFactory = createPayloadValidationError,
3888
+ }) => {
3889
+ {
3890
+ const result = validateAllowedKeys({
3891
+ value: data,
3892
+ allowedKeys: ["type", "name", "color"],
3893
+ path,
3894
+ errorFactory,
3895
+ });
3896
+ if (result?.valid === false) {
3897
+ return result;
3898
+ }
3899
+ }
3900
+
3901
+ if (data?.type !== "tag") {
3902
+ return invalidFromErrorFactory(errorFactory, `${path}.type must be 'tag'`);
3903
+ }
3904
+
3905
+ if (!isNonEmptyString(data.name)) {
3906
+ return invalidFromErrorFactory(
3907
+ errorFactory,
3908
+ `${path}.name must be a non-empty string`,
3909
+ );
3910
+ }
3911
+
3912
+ if (data.color !== undefined && !isHexColor(data.color)) {
3913
+ return invalidFromErrorFactory(
3914
+ errorFactory,
3915
+ `${path}.color must be a hex color when provided`,
3916
+ );
3917
+ }
3918
+ };
3919
+
3920
+ const validateTagUpdateData = ({
3921
+ data,
3922
+ path = "payload.data",
3923
+ errorFactory = createPayloadValidationError,
3924
+ }) => {
3925
+ {
3926
+ const result = validateAllowedKeys({
3927
+ value: data,
3928
+ allowedKeys: ["name", "color"],
3929
+ path,
3930
+ errorFactory,
3931
+ });
3932
+ if (result?.valid === false) {
3933
+ return result;
3934
+ }
3935
+ }
3936
+
3937
+ if (Object.keys(data).length === 0) {
3938
+ return invalidFromErrorFactory(
3939
+ errorFactory,
3940
+ `${path} must include at least one field`,
3941
+ );
3942
+ }
3943
+
3944
+ if (data.name !== undefined && !isNonEmptyString(data.name)) {
3945
+ return invalidFromErrorFactory(
3946
+ errorFactory,
3947
+ `${path}.name must be a non-empty string when provided`,
3948
+ );
3949
+ }
3950
+
3951
+ if (
3952
+ data.color !== undefined &&
3953
+ data.color !== null &&
3954
+ !isHexColor(data.color)
3955
+ ) {
3956
+ return invalidFromErrorFactory(
3957
+ errorFactory,
3958
+ `${path}.color must be a hex color or null when provided`,
3959
+ );
3960
+ }
3961
+ };
3962
+
3963
+ const validateTagsRoot = ({ state, tags, path, errorFactory }) => {
3964
+ if (!isPlainObject(tags)) {
3965
+ return invalidFromErrorFactory(errorFactory, `${path} must be an object`);
3966
+ }
3967
+
3968
+ for (const scopeKey of Object.keys(tags)) {
3969
+ if (
3970
+ !isBaseTagScopeKey(scopeKey) &&
3971
+ !isCharacterSpriteTagScopeKey(scopeKey)
3972
+ ) {
3973
+ return invalidFromErrorFactory(
3974
+ errorFactory,
3975
+ `${path}.${scopeKey} is not allowed`,
3976
+ );
3977
+ }
3978
+ }
3979
+
3980
+ for (const scopeKey of TAG_SCOPE_BASE_KEYS) {
3981
+ if (!Object.hasOwn(tags, scopeKey)) {
3982
+ return invalidFromErrorFactory(
3983
+ errorFactory,
3984
+ `${path}.${scopeKey} is required`,
3985
+ );
3986
+ }
3987
+ }
3988
+
3989
+ for (const [scopeKey, collection] of Object.entries(tags)) {
3990
+ {
3991
+ const result = validateNestedCollection({
3992
+ collection,
3993
+ path: `${path}.${scopeKey}`,
3994
+ itemValidator: validateTagItems,
3995
+ treeValidator: validateFlatTagTree,
3996
+ errorFactory,
3997
+ });
3998
+ if (result?.valid === false) {
3999
+ return result;
4000
+ }
4001
+ }
4002
+
4003
+ if (!isCharacterSpriteTagScopeKey(scopeKey)) {
4004
+ continue;
4005
+ }
4006
+
4007
+ const characterId = getCharacterSpriteTagScopeCharacterId(scopeKey);
4008
+ const character = state.characters?.items?.[characterId];
4009
+ if (!isPlainObject(character) || character.type !== "character") {
4010
+ return invalidFromErrorFactory(
4011
+ errorFactory,
4012
+ `${path}.${scopeKey} must reference an existing character`,
4013
+ );
4014
+ }
4015
+ }
4016
+ };
4017
+
4018
+ const getTagScopeCollection = ({ state, scopeKey }) => state.tags?.[scopeKey];
4019
+
4020
+ const validateTagScopeKey = ({ scopeKey, path, errorFactory }) => {
4021
+ if (!isNonEmptyString(scopeKey)) {
4022
+ return invalidFromErrorFactory(
4023
+ errorFactory,
4024
+ `${path} must be a non-empty string`,
4025
+ );
4026
+ }
4027
+
4028
+ if (isBaseTagScopeKey(scopeKey) || isCharacterSpriteTagScopeKey(scopeKey)) {
4029
+ return VALID_RESULT;
4030
+ }
4031
+
4032
+ return invalidFromErrorFactory(
4033
+ errorFactory,
4034
+ `${path} must be a supported tag scope key`,
4035
+ );
4036
+ };
4037
+
4038
+ const validateTagScopeAgainstState = ({
4039
+ state,
4040
+ scopeKey,
4041
+ path,
4042
+ details = {},
4043
+ errorFactory = createPreconditionValidationError,
4044
+ }) => {
4045
+ const scopeKeyResult = validateTagScopeKey({
4046
+ scopeKey,
4047
+ path,
4048
+ errorFactory,
4049
+ });
4050
+ if (!scopeKeyResult.valid) {
4051
+ return scopeKeyResult;
4052
+ }
4053
+
4054
+ if (!isCharacterSpriteTagScopeKey(scopeKey)) {
4055
+ return VALID_RESULT;
4056
+ }
4057
+
4058
+ const characterId = getCharacterSpriteTagScopeCharacterId(scopeKey);
4059
+ const character = state.characters?.items?.[characterId];
4060
+ if (!isPlainObject(character) || character.type !== "character") {
4061
+ return invalidFromErrorFactory(
4062
+ errorFactory,
4063
+ `${path} must reference an existing character sprite tag scope`,
4064
+ {
4065
+ ...details,
4066
+ characterId,
4067
+ scopeKey,
4068
+ },
4069
+ );
4070
+ }
4071
+
4072
+ return VALID_RESULT;
4073
+ };
4074
+
4075
+ const validateTagIdsAgainstScope = ({
4076
+ state,
4077
+ tagIds,
4078
+ scopeKey,
4079
+ path,
4080
+ details = {},
4081
+ errorFactory = createPreconditionValidationError,
4082
+ }) => {
4083
+ if (tagIds === undefined) {
4084
+ return VALID_RESULT;
4085
+ }
4086
+
4087
+ const scopeResult = validateTagScopeAgainstState({
4088
+ state,
4089
+ scopeKey,
4090
+ path,
4091
+ details,
4092
+ errorFactory,
4093
+ });
4094
+ if (!scopeResult.valid) {
4095
+ return scopeResult;
4096
+ }
4097
+
4098
+ const collection = getTagScopeCollection({ state, scopeKey });
4099
+ for (const [index, tagId] of tagIds.entries()) {
4100
+ const tag = collection?.items?.[tagId];
4101
+ if (!isPlainObject(tag) || tag.type !== "tag") {
4102
+ return invalidFromErrorFactory(
4103
+ errorFactory,
4104
+ `${path}[${index}] must reference an existing tag in scope '${scopeKey}'`,
4105
+ {
4106
+ ...details,
4107
+ scopeKey,
4108
+ tagId,
4109
+ },
4110
+ );
4111
+ }
4112
+ }
4113
+
4114
+ return VALID_RESULT;
4115
+ };
4116
+
4117
+ const validateCharacterSpriteGroupsAgainstScope = ({
4118
+ state,
4119
+ spriteGroups,
4120
+ scopeKey,
4121
+ path,
4122
+ details = {},
4123
+ errorFactory = createPreconditionValidationError,
4124
+ }) => {
4125
+ if (spriteGroups === undefined) {
4126
+ return VALID_RESULT;
4127
+ }
4128
+
4129
+ for (const [index, spriteGroup] of spriteGroups.entries()) {
4130
+ const result = validateTagIdsAgainstScope({
4131
+ state,
4132
+ tagIds: spriteGroup.tags,
4133
+ scopeKey,
4134
+ path: `${path}[${index}].tags`,
4135
+ details: {
4136
+ ...details,
4137
+ spriteGroupIndex: index,
4138
+ spriteGroupName: spriteGroup.name,
4139
+ },
4140
+ errorFactory,
4141
+ });
4142
+ if (!result.valid) {
4143
+ return result;
4144
+ }
4145
+ }
4146
+
4147
+ return VALID_RESULT;
4148
+ };
4149
+
4150
+ const validateUniqueTagNameInScope = ({
4151
+ collection,
4152
+ name,
4153
+ path,
4154
+ excludeTagId,
4155
+ errorFactory = createPreconditionValidationError,
4156
+ }) => {
4157
+ const normalizedName = name.trim().toLowerCase();
4158
+
4159
+ for (const [tagId, tag] of Object.entries(collection?.items || {})) {
4160
+ if (tagId === excludeTagId || tag?.type !== "tag") {
4161
+ continue;
4162
+ }
4163
+
4164
+ if (tag.name?.trim?.().toLowerCase?.() === normalizedName) {
4165
+ return invalidFromErrorFactory(
4166
+ errorFactory,
4167
+ `${path} must be unique within its tag scope`,
4168
+ );
4169
+ }
4170
+ }
4171
+
4172
+ return VALID_RESULT;
4173
+ };
4174
+
4175
+ const ensureTagScopeCollection = ({ state, scopeKey }) => {
4176
+ state.tags ??= createEmptyTagsState();
4177
+ state.tags[scopeKey] ??= createEmptyCollectionState();
4178
+ return state.tags[scopeKey];
4179
+ };
4180
+
4181
+ const assignOptionalTagIds = ({ target, tagIds }) => {
4182
+ if (Array.isArray(tagIds) && tagIds.length > 0) {
4183
+ target.tagIds = structuredClone(tagIds);
4184
+ }
4185
+ };
4186
+
4187
+ const assignOptionalCharacterSpriteGroups = ({ target, spriteGroups }) => {
4188
+ if (Array.isArray(spriteGroups) && spriteGroups.length > 0) {
4189
+ target.spriteGroups = structuredClone(spriteGroups);
4190
+ }
4191
+ };
4192
+
4193
+ const applyTagIdsUpdate = ({ currentItem, data }) => {
4194
+ const nextData = structuredClone(data);
4195
+ if (nextData.tagIds === undefined) {
4196
+ delete nextData.tagIds;
4197
+ }
4198
+
4199
+ const nextItem = {
4200
+ ...structuredClone(currentItem),
4201
+ ...nextData,
4202
+ };
4203
+
4204
+ if (data.tagIds !== undefined) {
4205
+ if (Array.isArray(data.tagIds) && data.tagIds.length > 0) {
4206
+ nextItem.tagIds = structuredClone(data.tagIds);
4207
+ } else {
4208
+ delete nextItem.tagIds;
4209
+ }
4210
+ }
4211
+
4212
+ return nextItem;
4213
+ };
4214
+
4215
+ const applyCharacterUpdate = ({ currentItem, data }) => {
4216
+ const nextItem = applyTagIdsUpdate({
4217
+ currentItem,
4218
+ data,
4219
+ });
4220
+
4221
+ if (data.spriteGroups !== undefined) {
4222
+ if (Array.isArray(data.spriteGroups) && data.spriteGroups.length > 0) {
4223
+ nextItem.spriteGroups = structuredClone(data.spriteGroups);
4224
+ } else {
4225
+ delete nextItem.spriteGroups;
4226
+ }
4227
+ }
4228
+
4229
+ return nextItem;
4230
+ };
4231
+
4232
+ const stripDeletedTagIdsFromItem = ({ item, deletedTagIds }) => {
4233
+ if (!Array.isArray(item?.tagIds) || item.tagIds.length === 0) {
4234
+ return;
4235
+ }
4236
+
4237
+ const remainingTagIds = item.tagIds.filter(
4238
+ (tagId) => !deletedTagIds.has(tagId),
4239
+ );
4240
+ if (remainingTagIds.length === item.tagIds.length) {
4241
+ return;
4242
+ }
4243
+
4244
+ if (remainingTagIds.length === 0) {
4245
+ delete item.tagIds;
4246
+ return;
4247
+ }
4248
+
4249
+ item.tagIds = remainingTagIds;
4250
+ };
4251
+
4252
+ const stripDeletedTagIdsFromCharacterSpriteGroups = ({
4253
+ item,
4254
+ deletedTagIds,
4255
+ }) => {
4256
+ if (!Array.isArray(item?.spriteGroups) || item.spriteGroups.length === 0) {
4257
+ return;
4258
+ }
4259
+
4260
+ const nextSpriteGroups = [];
4261
+ let didChange = false;
4262
+
4263
+ for (const spriteGroup of item.spriteGroups) {
4264
+ const currentTags = Array.isArray(spriteGroup?.tags)
4265
+ ? spriteGroup.tags
4266
+ : [];
4267
+ const remainingTags = currentTags.filter(
4268
+ (tagId) => !deletedTagIds.has(tagId),
4269
+ );
4270
+
4271
+ if (remainingTags.length !== currentTags.length) {
4272
+ didChange = true;
4273
+ }
4274
+
4275
+ if (remainingTags.length === 0) {
4276
+ didChange = true;
4277
+ continue;
4278
+ }
4279
+
4280
+ if (remainingTags.length === currentTags.length) {
4281
+ nextSpriteGroups.push(spriteGroup);
4282
+ continue;
4283
+ }
4284
+
4285
+ nextSpriteGroups.push({
4286
+ ...spriteGroup,
4287
+ tags: remainingTags,
4288
+ });
4289
+ }
4290
+
4291
+ if (!didChange) {
4292
+ return;
4293
+ }
4294
+
4295
+ if (nextSpriteGroups.length === 0) {
4296
+ delete item.spriteGroups;
4297
+ return;
4298
+ }
4299
+
4300
+ item.spriteGroups = nextSpriteGroups;
4301
+ };
4302
+
4303
+ const stripDeletedTagIdsFromScopeItems = ({
4304
+ state,
4305
+ scopeKey,
4306
+ deletedTagIds,
4307
+ }) => {
4308
+ if (deletedTagIds.size === 0) {
4309
+ return;
4310
+ }
4311
+
4312
+ if (scopeKey === "images") {
4313
+ for (const item of Object.values(state.images.items)) {
4314
+ if (item?.type === "image") {
4315
+ stripDeletedTagIdsFromItem({ item, deletedTagIds });
4316
+ }
4317
+ }
4318
+ return;
4319
+ }
4320
+
4321
+ if (scopeKey === "sounds") {
4322
+ for (const item of Object.values(state.sounds.items)) {
4323
+ if (item?.type === "sound") {
4324
+ stripDeletedTagIdsFromItem({ item, deletedTagIds });
4325
+ }
4326
+ }
4327
+ return;
4328
+ }
4329
+
4330
+ if (scopeKey === "videos") {
4331
+ for (const item of Object.values(state.videos.items)) {
4332
+ if (item?.type === "video") {
4333
+ stripDeletedTagIdsFromItem({ item, deletedTagIds });
4334
+ }
4335
+ }
4336
+ return;
4337
+ }
4338
+
4339
+ if (scopeKey === "characters") {
4340
+ for (const item of Object.values(state.characters.items)) {
4341
+ if (item?.type === "character") {
4342
+ stripDeletedTagIdsFromItem({ item, deletedTagIds });
4343
+ }
4344
+ }
4345
+ return;
4346
+ }
4347
+
4348
+ if (scopeKey === "transforms") {
4349
+ for (const item of Object.values(state.transforms.items)) {
4350
+ if (item?.type === "transform") {
4351
+ stripDeletedTagIdsFromItem({ item, deletedTagIds });
4352
+ }
4353
+ }
4354
+ return;
4355
+ }
4356
+
4357
+ if (scopeKey === "fonts") {
4358
+ for (const item of Object.values(state.fonts.items)) {
4359
+ if (item?.type === "font") {
4360
+ stripDeletedTagIdsFromItem({ item, deletedTagIds });
4361
+ }
4362
+ }
4363
+ return;
4364
+ }
4365
+
4366
+ if (scopeKey === "colors") {
4367
+ for (const item of Object.values(state.colors.items)) {
4368
+ if (item?.type === "color") {
4369
+ stripDeletedTagIdsFromItem({ item, deletedTagIds });
4370
+ }
4371
+ }
4372
+ return;
4373
+ }
4374
+
4375
+ if (scopeKey === "textStyles") {
4376
+ for (const item of Object.values(state.textStyles.items)) {
4377
+ if (item?.type === "textStyle") {
4378
+ stripDeletedTagIdsFromItem({ item, deletedTagIds });
4379
+ }
4380
+ }
4381
+ return;
4382
+ }
4383
+
4384
+ if (scopeKey === "variables") {
4385
+ for (const item of Object.values(state.variables.items)) {
4386
+ if (item?.type !== "folder") {
4387
+ stripDeletedTagIdsFromItem({ item, deletedTagIds });
4388
+ }
4389
+ }
4390
+ return;
4391
+ }
4392
+
4393
+ if (scopeKey === "layouts") {
4394
+ for (const item of Object.values(state.layouts.items)) {
4395
+ if (item?.type === "layout") {
4396
+ stripDeletedTagIdsFromItem({ item, deletedTagIds });
4397
+ }
4398
+ }
4399
+ return;
4400
+ }
4401
+
4402
+ if (scopeKey === "controls") {
4403
+ for (const item of Object.values(state.controls.items)) {
4404
+ if (item?.type === "control") {
4405
+ stripDeletedTagIdsFromItem({ item, deletedTagIds });
4406
+ }
4407
+ }
4408
+ return;
4409
+ }
4410
+
4411
+ if (scopeKey === "animations") {
4412
+ for (const item of Object.values(state.animations.items)) {
4413
+ if (item?.type === "animation") {
4414
+ stripDeletedTagIdsFromItem({ item, deletedTagIds });
4415
+ }
4416
+ }
4417
+ return;
4418
+ }
4419
+
4420
+ if (scopeKey === "particles") {
4421
+ for (const item of Object.values(state.particles.items)) {
4422
+ if (item?.type === "particle") {
4423
+ stripDeletedTagIdsFromItem({ item, deletedTagIds });
4424
+ }
4425
+ }
4426
+ return;
4427
+ }
4428
+
4429
+ if (scopeKey === "spritesheets") {
4430
+ for (const item of Object.values(state.spritesheets.items)) {
4431
+ if (item?.type === "spritesheet") {
4432
+ stripDeletedTagIdsFromItem({ item, deletedTagIds });
4433
+ }
4434
+ }
4435
+ return;
4436
+ }
4437
+
4438
+ if (!isCharacterSpriteTagScopeKey(scopeKey)) {
4439
+ return;
4440
+ }
4441
+
4442
+ const characterId = getCharacterSpriteTagScopeCharacterId(scopeKey);
4443
+ const characterItem = state.characters?.items?.[characterId];
4444
+ const collection = getCharacterSpriteCollection({
4445
+ state,
4446
+ characterId,
4447
+ });
4448
+
4449
+ if (characterItem?.type === "character") {
4450
+ stripDeletedTagIdsFromCharacterSpriteGroups({
4451
+ item: characterItem,
4452
+ deletedTagIds,
4453
+ });
4454
+ }
4455
+
4456
+ for (const item of Object.values(collection?.items || {})) {
4457
+ if (item?.type === "image") {
4458
+ stripDeletedTagIdsFromItem({ item, deletedTagIds });
4459
+ }
4460
+ }
4461
+ };
4462
+
4463
+ const validateKeyboardMap = ({ value, path, errorFactory }) => {
4464
+ if (value === undefined) {
4465
+ return VALID_RESULT;
4466
+ }
4467
+
4468
+ if (!isPlainObject(value)) {
4469
+ return invalidFromErrorFactory(
4470
+ errorFactory,
4471
+ `${path} must be an object when provided`,
4472
+ );
4473
+ }
4474
+
4475
+ for (const [key, interaction] of Object.entries(value)) {
4476
+ if (!isNonEmptyString(key)) {
4477
+ return invalidFromErrorFactory(
4478
+ errorFactory,
4479
+ `${path} keys must be non-empty strings`,
4480
+ );
4481
+ }
4482
+
4483
+ if (!isPlainObject(interaction)) {
4484
+ return invalidFromErrorFactory(
4485
+ errorFactory,
4486
+ `${path}.${key} must be an object`,
4487
+ );
4488
+ }
4489
+ }
4490
+
4491
+ return VALID_RESULT;
4492
+ };
4493
+
4494
+ const validatePreviewObject = ({ value, path, errorFactory }) => {
4495
+ if (value === undefined) {
4496
+ return VALID_RESULT;
4497
+ }
4498
+
4499
+ if (!isPlainObject(value)) {
4500
+ return invalidFromErrorFactory(
4501
+ errorFactory,
4502
+ `${path} must be an object when provided`,
4503
+ );
4504
+ }
4505
+
4506
+ return VALID_RESULT;
4507
+ };
4508
+
4509
+ const validateLayoutItems = ({ items, path, errorFactory }) => {
4510
+ for (const [itemId, item] of Object.entries(items)) {
4511
+ const itemPath = `${path}.${itemId}`;
4512
+
4513
+ if (item?.type !== "folder" && item?.type !== "layout") {
4514
+ return invalidFromErrorFactory(
4515
+ errorFactory,
4516
+ `${itemPath}.type must be 'folder' or 'layout'`,
4517
+ );
4518
+ }
4519
+
4520
+ {
4521
+ const result = validateAllowedKeys({
4522
+ value: item,
4523
+ allowedKeys:
4524
+ item.type === "folder"
4525
+ ? ["id", "type", "name", "description"]
4526
+ : [
4527
+ "id",
4528
+ "type",
4529
+ "name",
4530
+ "description",
4531
+ "tagIds",
4532
+ "layoutType",
4533
+ "isFragment",
4534
+ "thumbnailFileId",
4535
+ "preview",
4536
+ "elements",
4537
+ ],
4538
+ path: itemPath,
4539
+ errorFactory,
4540
+ });
4541
+ if (result?.valid === false) {
4542
+ return result;
4543
+ }
4544
+ }
4545
+
4546
+ if (!isNonEmptyString(item.id)) {
4547
+ return invalidFromErrorFactory(
4548
+ errorFactory,
4549
+ `${itemPath}.id must be a non-empty string`,
4550
+ );
4551
+ }
4552
+
4553
+ if (item.id !== itemId) {
4554
+ return invalidFromErrorFactory(
4555
+ errorFactory,
4556
+ `${itemPath}.id must match item key '${itemId}'`,
4557
+ );
4558
+ }
4559
+
4560
+ if (!isNonEmptyString(item.name)) {
4561
+ return invalidFromErrorFactory(
4562
+ errorFactory,
4563
+ `${itemPath}.name must be a non-empty string`,
4564
+ );
4565
+ }
4566
+
4567
+ if (item.description !== undefined && !isString(item.description)) {
4568
+ return invalidFromErrorFactory(
4569
+ errorFactory,
4570
+ `${itemPath}.description must be a string when provided`,
4571
+ );
4572
+ }
4573
+
4574
+ if (
4575
+ item.thumbnailFileId !== undefined &&
4576
+ !isNonEmptyString(item.thumbnailFileId)
4577
+ ) {
4578
+ return invalidFromErrorFactory(
4579
+ errorFactory,
4580
+ `${itemPath}.thumbnailFileId must be a non-empty string when provided`,
4581
+ );
4582
+ }
4583
+
4584
+ {
4585
+ const result = validatePreviewObject({
4586
+ value: item.preview,
4587
+ path: `${itemPath}.preview`,
4588
+ errorFactory,
4589
+ });
4590
+ if (result?.valid === false) {
4591
+ return result;
4592
+ }
4593
+ }
4594
+
4595
+ if (item.type === "layout") {
4596
+ {
4597
+ const result = validateOptionalUniqueIdArray({
4598
+ value: item.tagIds,
4599
+ path: `${itemPath}.tagIds`,
4600
+ errorFactory,
4601
+ allowEmpty: false,
4602
+ });
4603
+ if (result?.valid === false) {
4604
+ return result;
4605
+ }
4606
+ }
4607
+
4608
+ if (!LAYOUT_TYPE_KEYS.includes(item.layoutType)) {
4609
+ return invalidFromErrorFactory(
4610
+ errorFactory,
4611
+ `${itemPath}.layoutType must be 'general', 'save-load', 'confirmDialog', 'dialogue-adv', 'dialogue-nvl', 'choice', or 'history'`,
4612
+ );
4613
+ }
4614
+
4615
+ if (
4616
+ item.isFragment !== undefined &&
4617
+ typeof item.isFragment !== "boolean"
4618
+ ) {
4619
+ return invalidFromErrorFactory(
4620
+ errorFactory,
4621
+ `${itemPath}.isFragment must be a boolean when provided`,
4622
+ );
4623
+ }
4624
+
4625
+ {
4626
+ const result = validateNestedCollection({
4627
+ collection: item.elements,
3680
4628
  path: `${itemPath}.elements`,
3681
4629
  itemValidator: validateLayoutElementItems,
3682
4630
  treeValidator: validateLayoutElementTreeOwnership,
@@ -3712,6 +4660,7 @@ const validateControlItems = ({ items, path, errorFactory }) => {
3712
4660
  "type",
3713
4661
  "name",
3714
4662
  "description",
4663
+ "tagIds",
3715
4664
  "thumbnailFileId",
3716
4665
  "preview",
3717
4666
  "elements",
@@ -3775,6 +4724,18 @@ const validateControlItems = ({ items, path, errorFactory }) => {
3775
4724
  }
3776
4725
 
3777
4726
  if (item.type === "control") {
4727
+ {
4728
+ const result = validateOptionalUniqueIdArray({
4729
+ value: item.tagIds,
4730
+ path: `${itemPath}.tagIds`,
4731
+ errorFactory,
4732
+ allowEmpty: false,
4733
+ });
4734
+ if (result?.valid === false) {
4735
+ return result;
4736
+ }
4737
+ }
4738
+
3778
4739
  {
3779
4740
  const result = validateNestedCollection({
3780
4741
  collection: item.elements,
@@ -4854,6 +5815,22 @@ export const assertInvariants = ({ state }) => {
4854
5815
  return result;
4855
5816
  }
4856
5817
  }
5818
+
5819
+ {
5820
+ const result = validateTagIdsAgainstScope({
5821
+ state,
5822
+ tagIds: image.tagIds,
5823
+ scopeKey: "images",
5824
+ path: "image.tagIds",
5825
+ details: {
5826
+ imageId,
5827
+ },
5828
+ errorFactory: createInvariantValidationError,
5829
+ });
5830
+ if (!result.valid) {
5831
+ return result;
5832
+ }
5833
+ }
4857
5834
  }
4858
5835
 
4859
5836
  for (const [spritesheetId, spritesheet] of Object.entries(
@@ -4874,6 +5851,22 @@ export const assertInvariants = ({ state }) => {
4874
5851
  return result;
4875
5852
  }
4876
5853
 
5854
+ {
5855
+ const tagResult = validateTagIdsAgainstScope({
5856
+ state,
5857
+ tagIds: spritesheet.tagIds,
5858
+ scopeKey: "spritesheets",
5859
+ path: "spritesheet.tagIds",
5860
+ details: {
5861
+ spritesheetId,
5862
+ },
5863
+ errorFactory: createInvariantValidationError,
5864
+ });
5865
+ if (!tagResult.valid) {
5866
+ return tagResult;
5867
+ }
5868
+ }
5869
+
4877
5870
  if (spritesheet.thumbnailFileId === undefined) {
4878
5871
  continue;
4879
5872
  }
@@ -4926,6 +5919,22 @@ export const assertInvariants = ({ state }) => {
4926
5919
  return result;
4927
5920
  }
4928
5921
  }
5922
+
5923
+ {
5924
+ const result = validateTagIdsAgainstScope({
5925
+ state,
5926
+ tagIds: sound.tagIds,
5927
+ scopeKey: "sounds",
5928
+ path: "sound.tagIds",
5929
+ details: {
5930
+ soundId,
5931
+ },
5932
+ errorFactory: createInvariantValidationError,
5933
+ });
5934
+ if (!result.valid) {
5935
+ return result;
5936
+ }
5937
+ }
4929
5938
  }
4930
5939
 
4931
5940
  for (const [videoId, video] of Object.entries(state.videos.items)) {
@@ -4958,6 +5967,22 @@ export const assertInvariants = ({ state }) => {
4958
5967
  return result;
4959
5968
  }
4960
5969
  }
5970
+
5971
+ {
5972
+ const result = validateTagIdsAgainstScope({
5973
+ state,
5974
+ tagIds: video.tagIds,
5975
+ scopeKey: "videos",
5976
+ path: "video.tagIds",
5977
+ details: {
5978
+ videoId,
5979
+ },
5980
+ errorFactory: createInvariantValidationError,
5981
+ });
5982
+ if (!result.valid) {
5983
+ return result;
5984
+ }
5985
+ }
4961
5986
  }
4962
5987
 
4963
5988
  for (const [fontId, font] of Object.entries(state.fonts.items)) {
@@ -4965,6 +5990,22 @@ export const assertInvariants = ({ state }) => {
4965
5990
  continue;
4966
5991
  }
4967
5992
 
5993
+ {
5994
+ const result = validateTagIdsAgainstScope({
5995
+ state,
5996
+ tagIds: font.tagIds,
5997
+ scopeKey: "fonts",
5998
+ path: "font.tagIds",
5999
+ details: {
6000
+ fontId,
6001
+ },
6002
+ errorFactory: createInvariantValidationError,
6003
+ });
6004
+ if (!result.valid) {
6005
+ return result;
6006
+ }
6007
+ }
6008
+
4968
6009
  const result = validateFileReference({
4969
6010
  state,
4970
6011
  fileId: font.fileId,
@@ -4984,6 +6025,22 @@ export const assertInvariants = ({ state }) => {
4984
6025
  continue;
4985
6026
  }
4986
6027
 
6028
+ {
6029
+ const tagResult = validateTagIdsAgainstScope({
6030
+ state,
6031
+ tagIds: animation.tagIds,
6032
+ scopeKey: "animations",
6033
+ path: "animation.tagIds",
6034
+ details: {
6035
+ animationId,
6036
+ },
6037
+ errorFactory: createInvariantValidationError,
6038
+ });
6039
+ if (!tagResult.valid) {
6040
+ return tagResult;
6041
+ }
6042
+ }
6043
+
4987
6044
  const result = validateAnimationMaskImageReferences({
4988
6045
  state,
4989
6046
  animation: animation.animation,
@@ -5016,6 +6073,38 @@ export const assertInvariants = ({ state }) => {
5016
6073
  }
5017
6074
  }
5018
6075
 
6076
+ {
6077
+ const result = validateTagIdsAgainstScope({
6078
+ state,
6079
+ tagIds: character.tagIds,
6080
+ scopeKey: "characters",
6081
+ path: "character.tagIds",
6082
+ details: {
6083
+ characterId,
6084
+ },
6085
+ errorFactory: createInvariantValidationError,
6086
+ });
6087
+ if (!result.valid) {
6088
+ return result;
6089
+ }
6090
+ }
6091
+
6092
+ {
6093
+ const result = validateCharacterSpriteGroupsAgainstScope({
6094
+ state,
6095
+ spriteGroups: character.spriteGroups,
6096
+ scopeKey: `${CHARACTER_SPRITE_TAG_SCOPE_PREFIX}${characterId}`,
6097
+ path: "character.spriteGroups",
6098
+ details: {
6099
+ characterId,
6100
+ },
6101
+ errorFactory: createInvariantValidationError,
6102
+ });
6103
+ if (!result.valid) {
6104
+ return result;
6105
+ }
6106
+ }
6107
+
5019
6108
  for (const [spriteId, sprite] of Object.entries(
5020
6109
  character.sprites?.items || {},
5021
6110
  )) {
@@ -5034,6 +6123,23 @@ export const assertInvariants = ({ state }) => {
5034
6123
  return result;
5035
6124
  }
5036
6125
 
6126
+ {
6127
+ const tagResult = validateTagIdsAgainstScope({
6128
+ state,
6129
+ tagIds: sprite.tagIds,
6130
+ scopeKey: `${CHARACTER_SPRITE_TAG_SCOPE_PREFIX}${characterId}`,
6131
+ path: "character.sprite.tagIds",
6132
+ details: {
6133
+ characterId,
6134
+ spriteId,
6135
+ },
6136
+ errorFactory: createInvariantValidationError,
6137
+ });
6138
+ if (!tagResult.valid) {
6139
+ return tagResult;
6140
+ }
6141
+ }
6142
+
5037
6143
  if (sprite.thumbnailFileId === undefined) {
5038
6144
  continue;
5039
6145
  }
@@ -5055,25 +6161,177 @@ export const assertInvariants = ({ state }) => {
5055
6161
  }
5056
6162
  }
5057
6163
 
6164
+ for (const [transformId, transform] of Object.entries(
6165
+ state.transforms.items,
6166
+ )) {
6167
+ if (transform.type !== "transform") {
6168
+ continue;
6169
+ }
6170
+
6171
+ {
6172
+ const result = validateTagIdsAgainstScope({
6173
+ state,
6174
+ tagIds: transform.tagIds,
6175
+ scopeKey: "transforms",
6176
+ path: "transform.tagIds",
6177
+ details: {
6178
+ transformId,
6179
+ },
6180
+ errorFactory: createInvariantValidationError,
6181
+ });
6182
+ if (!result.valid) {
6183
+ return result;
6184
+ }
6185
+ }
6186
+ }
6187
+
6188
+ for (const [particleId, particle] of Object.entries(state.particles.items)) {
6189
+ if (particle.type !== "particle") {
6190
+ continue;
6191
+ }
6192
+
6193
+ {
6194
+ const result = validateTagIdsAgainstScope({
6195
+ state,
6196
+ tagIds: particle.tagIds,
6197
+ scopeKey: "particles",
6198
+ path: "particle.tagIds",
6199
+ details: {
6200
+ particleId,
6201
+ },
6202
+ errorFactory: createInvariantValidationError,
6203
+ });
6204
+ if (!result.valid) {
6205
+ return result;
6206
+ }
6207
+ }
6208
+ }
6209
+
6210
+ for (const [colorId, color] of Object.entries(state.colors.items)) {
6211
+ if (color.type !== "color") {
6212
+ continue;
6213
+ }
6214
+
6215
+ {
6216
+ const result = validateTagIdsAgainstScope({
6217
+ state,
6218
+ tagIds: color.tagIds,
6219
+ scopeKey: "colors",
6220
+ path: "color.tagIds",
6221
+ details: {
6222
+ colorId,
6223
+ },
6224
+ errorFactory: createInvariantValidationError,
6225
+ });
6226
+ if (!result.valid) {
6227
+ return result;
6228
+ }
6229
+ }
6230
+ }
6231
+
6232
+ for (const [textStyleId, textStyle] of Object.entries(
6233
+ state.textStyles.items,
6234
+ )) {
6235
+ if (textStyle.type !== "textStyle") {
6236
+ continue;
6237
+ }
6238
+
6239
+ {
6240
+ const result = validateTagIdsAgainstScope({
6241
+ state,
6242
+ tagIds: textStyle.tagIds,
6243
+ scopeKey: "textStyles",
6244
+ path: "textStyle.tagIds",
6245
+ details: {
6246
+ textStyleId,
6247
+ },
6248
+ errorFactory: createInvariantValidationError,
6249
+ });
6250
+ if (!result.valid) {
6251
+ return result;
6252
+ }
6253
+ }
6254
+ }
6255
+
5058
6256
  for (const [layoutId, layout] of Object.entries(state.layouts.items)) {
5059
- if (layout.type !== "layout" || layout.thumbnailFileId === undefined) {
6257
+ if (layout.type !== "layout") {
5060
6258
  continue;
5061
6259
  }
5062
6260
 
5063
- const result = validateFileReference({
5064
- state,
5065
- fileId: layout.thumbnailFileId,
5066
- path: "layout.thumbnailFileId",
5067
- details: { layoutId, thumbnailFileId: layout.thumbnailFileId },
5068
- errorFactory: createInvariantValidationError,
5069
- });
5070
- if (!result.valid) {
5071
- return result;
6261
+ {
6262
+ const result = validateTagIdsAgainstScope({
6263
+ state,
6264
+ tagIds: layout.tagIds,
6265
+ scopeKey: "layouts",
6266
+ path: "layout.tagIds",
6267
+ details: {
6268
+ layoutId,
6269
+ },
6270
+ errorFactory: createInvariantValidationError,
6271
+ });
6272
+ if (!result.valid) {
6273
+ return result;
6274
+ }
6275
+ }
6276
+
6277
+ if (layout.thumbnailFileId !== undefined) {
6278
+ const result = validateFileReference({
6279
+ state,
6280
+ fileId: layout.thumbnailFileId,
6281
+ path: "layout.thumbnailFileId",
6282
+ details: { layoutId, thumbnailFileId: layout.thumbnailFileId },
6283
+ errorFactory: createInvariantValidationError,
6284
+ });
6285
+ if (!result.valid) {
6286
+ return result;
6287
+ }
6288
+ }
6289
+ }
6290
+
6291
+ for (const [variableId, variable] of Object.entries(state.variables.items)) {
6292
+ if (variable.type === "folder") {
6293
+ continue;
6294
+ }
6295
+
6296
+ {
6297
+ const result = validateTagIdsAgainstScope({
6298
+ state,
6299
+ tagIds: variable.tagIds,
6300
+ scopeKey: "variables",
6301
+ path: "variable.tagIds",
6302
+ details: {
6303
+ variableId,
6304
+ },
6305
+ errorFactory: createInvariantValidationError,
6306
+ });
6307
+ if (!result.valid) {
6308
+ return result;
6309
+ }
5072
6310
  }
5073
6311
  }
5074
6312
 
5075
6313
  for (const [controlId, control] of Object.entries(state.controls.items)) {
5076
- if (control.type !== "control" || control.thumbnailFileId === undefined) {
6314
+ if (control.type !== "control") {
6315
+ continue;
6316
+ }
6317
+
6318
+ {
6319
+ const result = validateTagIdsAgainstScope({
6320
+ state,
6321
+ tagIds: control.tagIds,
6322
+ scopeKey: "controls",
6323
+ path: "control.tagIds",
6324
+ details: {
6325
+ controlId,
6326
+ },
6327
+ errorFactory: createInvariantValidationError,
6328
+ });
6329
+ if (!result.valid) {
6330
+ return result;
6331
+ }
6332
+ }
6333
+
6334
+ if (control.thumbnailFileId === undefined) {
5077
6335
  continue;
5078
6336
  }
5079
6337
 
@@ -5366,7 +6624,7 @@ export const assertInvariants = ({ state }) => {
5366
6624
  !isLayoutConditionTarget(state, rule.when.target)
5367
6625
  ) {
5368
6626
  return invalidInvariant(
5369
- `${ownerLabel} element conditionalOverrides when target must reference an existing variable or supported runtime condition`,
6627
+ `${ownerLabel} element conditionalOverrides when target must reference an existing variable or supported layout condition`,
5370
6628
  {
5371
6629
  [ownerIdField]: ownerId,
5372
6630
  elementId,
@@ -5511,6 +6769,18 @@ const runValidateState = ({ state }) => {
5511
6769
  );
5512
6770
  }
5513
6771
 
6772
+ {
6773
+ const result = validateTagsRoot({
6774
+ state: normalizedState,
6775
+ tags: normalizedState.tags,
6776
+ path: "state.tags",
6777
+ errorFactory: createStateValidationError,
6778
+ });
6779
+ if (result?.valid === false) {
6780
+ return result;
6781
+ }
6782
+ }
6783
+
5514
6784
  for (const collectionKey of COLLECTION_KEYS) {
5515
6785
  {
5516
6786
  const result = validateCollection({
@@ -5705,25 +6975,156 @@ const validateRequiredUniqueIdArray = ({ value, path, errorFactory }) => {
5705
6975
  );
5706
6976
  }
5707
6977
 
5708
- const seen = new Set();
6978
+ const seen = new Set();
6979
+
6980
+ for (const [index, entry] of value.entries()) {
6981
+ if (!isNonEmptyString(entry)) {
6982
+ return invalidFromErrorFactory(
6983
+ errorFactory,
6984
+ `${path}[${index}] must be a non-empty string`,
6985
+ );
6986
+ }
6987
+
6988
+ if (seen.has(entry)) {
6989
+ return invalidFromErrorFactory(
6990
+ errorFactory,
6991
+ `${path}[${index}] must be unique`,
6992
+ );
6993
+ }
6994
+
6995
+ seen.add(entry);
6996
+ }
6997
+ };
6998
+
6999
+ const validateOptionalUniqueIdArray = ({
7000
+ value,
7001
+ path,
7002
+ errorFactory,
7003
+ allowEmpty = true,
7004
+ }) => {
7005
+ if (value === undefined) {
7006
+ return VALID_RESULT;
7007
+ }
7008
+
7009
+ if (!Array.isArray(value) || (!allowEmpty && value.length === 0)) {
7010
+ return invalidFromErrorFactory(
7011
+ errorFactory,
7012
+ allowEmpty
7013
+ ? `${path} must be an array when provided`
7014
+ : `${path} must be a non-empty array when provided`,
7015
+ );
7016
+ }
7017
+
7018
+ const seen = new Set();
7019
+
7020
+ for (const [index, entry] of value.entries()) {
7021
+ if (!isNonEmptyString(entry)) {
7022
+ return invalidFromErrorFactory(
7023
+ errorFactory,
7024
+ `${path}[${index}] must be a non-empty string`,
7025
+ );
7026
+ }
7027
+
7028
+ if (seen.has(entry)) {
7029
+ return invalidFromErrorFactory(
7030
+ errorFactory,
7031
+ `${path}[${index}] must be unique`,
7032
+ );
7033
+ }
7034
+
7035
+ seen.add(entry);
7036
+ }
7037
+ };
7038
+
7039
+ const validateCharacterSpriteGroups = ({
7040
+ value,
7041
+ path,
7042
+ errorFactory,
7043
+ allowEmpty = true,
7044
+ allowMissingId = false,
7045
+ }) => {
7046
+ if (value === undefined) {
7047
+ return VALID_RESULT;
7048
+ }
7049
+
7050
+ if (!Array.isArray(value) || (!allowEmpty && value.length === 0)) {
7051
+ return invalidFromErrorFactory(
7052
+ errorFactory,
7053
+ allowEmpty
7054
+ ? `${path} must be an array when provided`
7055
+ : `${path} must be a non-empty array when provided`,
7056
+ );
7057
+ }
7058
+
7059
+ const seenIds = new Set();
7060
+
7061
+ for (const [index, spriteGroup] of value.entries()) {
7062
+ const itemPath = `${path}[${index}]`;
7063
+
7064
+ if (!isPlainObject(spriteGroup)) {
7065
+ return invalidFromErrorFactory(
7066
+ errorFactory,
7067
+ `${itemPath} must be an object`,
7068
+ );
7069
+ }
7070
+
7071
+ {
7072
+ const result = validateAllowedKeys({
7073
+ value: spriteGroup,
7074
+ allowedKeys: ["id", "name", "tags"],
7075
+ path: itemPath,
7076
+ errorFactory,
7077
+ });
7078
+ if (result?.valid === false) {
7079
+ return result;
7080
+ }
7081
+ }
7082
+
7083
+ if (spriteGroup.id === undefined) {
7084
+ if (!allowMissingId) {
7085
+ return invalidFromErrorFactory(
7086
+ errorFactory,
7087
+ `${itemPath}.id must be a non-empty string`,
7088
+ );
7089
+ }
7090
+ } else {
7091
+ if (!isNonEmptyString(spriteGroup.id)) {
7092
+ return invalidFromErrorFactory(
7093
+ errorFactory,
7094
+ `${itemPath}.id must be a non-empty string`,
7095
+ );
7096
+ }
7097
+
7098
+ if (seenIds.has(spriteGroup.id)) {
7099
+ return invalidFromErrorFactory(
7100
+ errorFactory,
7101
+ `${itemPath}.id must be unique within spriteGroups`,
7102
+ );
7103
+ }
5709
7104
 
5710
- for (const [index, entry] of value.entries()) {
5711
- if (!isNonEmptyString(entry)) {
5712
- return invalidFromErrorFactory(
5713
- errorFactory,
5714
- `${path}[${index}] must be a non-empty string`,
5715
- );
7105
+ seenIds.add(spriteGroup.id);
5716
7106
  }
5717
7107
 
5718
- if (seen.has(entry)) {
7108
+ if (!isNonEmptyString(spriteGroup.name)) {
5719
7109
  return invalidFromErrorFactory(
5720
7110
  errorFactory,
5721
- `${path}[${index}] must be unique`,
7111
+ `${itemPath}.name must be a non-empty string`,
5722
7112
  );
5723
7113
  }
5724
7114
 
5725
- seen.add(entry);
7115
+ {
7116
+ const result = validateRequiredUniqueIdArray({
7117
+ value: spriteGroup.tags,
7118
+ path: `${itemPath}.tags`,
7119
+ errorFactory,
7120
+ });
7121
+ if (result?.valid === false) {
7122
+ return result;
7123
+ }
7124
+ }
5726
7125
  }
7126
+
7127
+ return VALID_RESULT;
5727
7128
  };
5728
7129
 
5729
7130
  const validateSectionCreateData = ({ data, errorFactory }) => {
@@ -5948,6 +7349,7 @@ const validateImageCreateData = ({ data, errorFactory }) => {
5948
7349
  "type",
5949
7350
  "name",
5950
7351
  "description",
7352
+ "tagIds",
5951
7353
  "thumbnailFileId",
5952
7354
  "fileId",
5953
7355
  "width",
@@ -5976,6 +7378,17 @@ const validateImageCreateData = ({ data, errorFactory }) => {
5976
7378
  }
5977
7379
 
5978
7380
  if (data.type === "image") {
7381
+ {
7382
+ const result = validateOptionalUniqueIdArray({
7383
+ value: data.tagIds,
7384
+ path: "payload.data.tagIds",
7385
+ errorFactory,
7386
+ });
7387
+ if (result?.valid === false) {
7388
+ return result;
7389
+ }
7390
+ }
7391
+
5979
7392
  if (
5980
7393
  data.thumbnailFileId !== undefined &&
5981
7394
  !isNonEmptyString(data.thumbnailFileId)
@@ -6016,6 +7429,7 @@ const validateImageUpdateData = ({ data, errorFactory }) => {
6016
7429
  allowedKeys: [
6017
7430
  "name",
6018
7431
  "description",
7432
+ "tagIds",
6019
7433
  "thumbnailFileId",
6020
7434
  "fileId",
6021
7435
  "width",
@@ -6050,6 +7464,17 @@ const validateImageUpdateData = ({ data, errorFactory }) => {
6050
7464
  );
6051
7465
  }
6052
7466
 
7467
+ {
7468
+ const result = validateOptionalUniqueIdArray({
7469
+ value: data.tagIds,
7470
+ path: "payload.data.tagIds",
7471
+ errorFactory,
7472
+ });
7473
+ if (result?.valid === false) {
7474
+ return result;
7475
+ }
7476
+ }
7477
+
6053
7478
  if (
6054
7479
  data.thumbnailFileId !== undefined &&
6055
7480
  !isNonEmptyString(data.thumbnailFileId)
@@ -6107,6 +7532,7 @@ const validateSpritesheetCreateData = ({ data, errorFactory }) => {
6107
7532
  "type",
6108
7533
  "name",
6109
7534
  "description",
7535
+ "tagIds",
6110
7536
  "thumbnailFileId",
6111
7537
  "fileId",
6112
7538
  "sheetWidth",
@@ -6140,6 +7566,17 @@ const validateSpritesheetCreateData = ({ data, errorFactory }) => {
6140
7566
  }
6141
7567
 
6142
7568
  if (data.type === "spritesheet") {
7569
+ {
7570
+ const result = validateOptionalUniqueIdArray({
7571
+ value: data.tagIds,
7572
+ path: "payload.data.tagIds",
7573
+ errorFactory,
7574
+ });
7575
+ if (result?.valid === false) {
7576
+ return result;
7577
+ }
7578
+ }
7579
+
6143
7580
  if (
6144
7581
  data.thumbnailFileId !== undefined &&
6145
7582
  !isNonEmptyString(data.thumbnailFileId)
@@ -6199,6 +7636,7 @@ const validateSpritesheetUpdateData = ({ data, errorFactory }) => {
6199
7636
  allowedKeys: [
6200
7637
  "name",
6201
7638
  "description",
7639
+ "tagIds",
6202
7640
  "thumbnailFileId",
6203
7641
  "fileId",
6204
7642
  "sheetWidth",
@@ -6238,6 +7676,17 @@ const validateSpritesheetUpdateData = ({ data, errorFactory }) => {
6238
7676
  );
6239
7677
  }
6240
7678
 
7679
+ {
7680
+ const result = validateOptionalUniqueIdArray({
7681
+ value: data.tagIds,
7682
+ path: "payload.data.tagIds",
7683
+ errorFactory,
7684
+ });
7685
+ if (result?.valid === false) {
7686
+ return result;
7687
+ }
7688
+ }
7689
+
6241
7690
  if (
6242
7691
  data.thumbnailFileId !== undefined &&
6243
7692
  !isNonEmptyString(data.thumbnailFileId)
@@ -6314,6 +7763,7 @@ const validateSoundCreateData = ({ data, errorFactory }) => {
6314
7763
  "type",
6315
7764
  "name",
6316
7765
  "description",
7766
+ "tagIds",
6317
7767
  "fileId",
6318
7768
  "waveformDataFileId",
6319
7769
  "duration",
@@ -6341,6 +7791,17 @@ const validateSoundCreateData = ({ data, errorFactory }) => {
6341
7791
  }
6342
7792
 
6343
7793
  if (data.type === "sound") {
7794
+ {
7795
+ const result = validateOptionalUniqueIdArray({
7796
+ value: data.tagIds,
7797
+ path: "payload.data.tagIds",
7798
+ errorFactory,
7799
+ });
7800
+ if (result?.valid === false) {
7801
+ return result;
7802
+ }
7803
+ }
7804
+
6344
7805
  if (!isNonEmptyString(data.fileId)) {
6345
7806
  return invalidFromErrorFactory(
6346
7807
  errorFactory,
@@ -6375,6 +7836,7 @@ const validateSoundUpdateData = ({ data, errorFactory }) => {
6375
7836
  allowedKeys: [
6376
7837
  "name",
6377
7838
  "description",
7839
+ "tagIds",
6378
7840
  "fileId",
6379
7841
  "waveformDataFileId",
6380
7842
  "duration",
@@ -6408,6 +7870,17 @@ const validateSoundUpdateData = ({ data, errorFactory }) => {
6408
7870
  );
6409
7871
  }
6410
7872
 
7873
+ {
7874
+ const result = validateOptionalUniqueIdArray({
7875
+ value: data.tagIds,
7876
+ path: "payload.data.tagIds",
7877
+ errorFactory,
7878
+ });
7879
+ if (result?.valid === false) {
7880
+ return result;
7881
+ }
7882
+ }
7883
+
6411
7884
  if (data.fileId !== undefined && !isNonEmptyString(data.fileId)) {
6412
7885
  return invalidFromErrorFactory(
6413
7886
  errorFactory,
@@ -6459,6 +7932,7 @@ const validateVideoCreateData = ({ data, errorFactory }) => {
6459
7932
  "type",
6460
7933
  "name",
6461
7934
  "description",
7935
+ "tagIds",
6462
7936
  "fileId",
6463
7937
  "thumbnailFileId",
6464
7938
  "duration",
@@ -6488,6 +7962,17 @@ const validateVideoCreateData = ({ data, errorFactory }) => {
6488
7962
  }
6489
7963
 
6490
7964
  if (data.type === "video") {
7965
+ {
7966
+ const result = validateOptionalUniqueIdArray({
7967
+ value: data.tagIds,
7968
+ path: "payload.data.tagIds",
7969
+ errorFactory,
7970
+ });
7971
+ if (result?.valid === false) {
7972
+ return result;
7973
+ }
7974
+ }
7975
+
6491
7976
  if (!isNonEmptyString(data.fileId)) {
6492
7977
  return invalidFromErrorFactory(
6493
7978
  errorFactory,
@@ -6532,6 +8017,7 @@ const validateVideoUpdateData = ({ data, errorFactory }) => {
6532
8017
  allowedKeys: [
6533
8018
  "name",
6534
8019
  "description",
8020
+ "tagIds",
6535
8021
  "fileId",
6536
8022
  "thumbnailFileId",
6537
8023
  "duration",
@@ -6567,6 +8053,17 @@ const validateVideoUpdateData = ({ data, errorFactory }) => {
6567
8053
  );
6568
8054
  }
6569
8055
 
8056
+ {
8057
+ const result = validateOptionalUniqueIdArray({
8058
+ value: data.tagIds,
8059
+ path: "payload.data.tagIds",
8060
+ errorFactory,
8061
+ });
8062
+ if (result?.valid === false) {
8063
+ return result;
8064
+ }
8065
+ }
8066
+
6570
8067
  if (data.fileId !== undefined && !isNonEmptyString(data.fileId)) {
6571
8068
  return invalidFromErrorFactory(
6572
8069
  errorFactory,
@@ -6627,13 +8124,7 @@ const validateFontCreateData = ({ data, errorFactory }) => {
6627
8124
  allowedKeys:
6628
8125
  data.type === "folder"
6629
8126
  ? ["type", "name", "description"]
6630
- : [
6631
- "type",
6632
- "name",
6633
- "description",
6634
- "fileId",
6635
- "fontFamily",
6636
- ],
8127
+ : ["type", "name", "description", "tagIds", "fileId", "fontFamily"],
6637
8128
  path: "payload.data",
6638
8129
  errorFactory,
6639
8130
  });
@@ -6657,6 +8148,17 @@ const validateFontCreateData = ({ data, errorFactory }) => {
6657
8148
  }
6658
8149
 
6659
8150
  if (data.type === "font") {
8151
+ {
8152
+ const result = validateOptionalUniqueIdArray({
8153
+ value: data.tagIds,
8154
+ path: "payload.data.tagIds",
8155
+ errorFactory,
8156
+ });
8157
+ if (result?.valid === false) {
8158
+ return result;
8159
+ }
8160
+ }
8161
+
6660
8162
  if (!isNonEmptyString(data.fileId)) {
6661
8163
  return invalidFromErrorFactory(
6662
8164
  errorFactory,
@@ -6670,7 +8172,6 @@ const validateFontCreateData = ({ data, errorFactory }) => {
6670
8172
  "payload.data.fontFamily must be a non-empty string",
6671
8173
  );
6672
8174
  }
6673
-
6674
8175
  }
6675
8176
  };
6676
8177
 
@@ -6678,12 +8179,7 @@ const validateFontUpdateData = ({ data, errorFactory }) => {
6678
8179
  {
6679
8180
  const result = validateAllowedKeys({
6680
8181
  value: data,
6681
- allowedKeys: [
6682
- "name",
6683
- "description",
6684
- "fileId",
6685
- "fontFamily",
6686
- ],
8182
+ allowedKeys: ["name", "description", "tagIds", "fileId", "fontFamily"],
6687
8183
  path: "payload.data",
6688
8184
  errorFactory,
6689
8185
  });
@@ -6713,6 +8209,17 @@ const validateFontUpdateData = ({ data, errorFactory }) => {
6713
8209
  );
6714
8210
  }
6715
8211
 
8212
+ {
8213
+ const result = validateOptionalUniqueIdArray({
8214
+ value: data.tagIds,
8215
+ path: "payload.data.tagIds",
8216
+ errorFactory,
8217
+ });
8218
+ if (result?.valid === false) {
8219
+ return result;
8220
+ }
8221
+ }
8222
+
6716
8223
  if (data.fileId !== undefined && !isNonEmptyString(data.fileId)) {
6717
8224
  return invalidFromErrorFactory(
6718
8225
  errorFactory,
@@ -6726,7 +8233,6 @@ const validateFontUpdateData = ({ data, errorFactory }) => {
6726
8233
  "payload.data.fontFamily must be a non-empty string when provided",
6727
8234
  );
6728
8235
  }
6729
-
6730
8236
  };
6731
8237
 
6732
8238
  const validateFileCreateData = ({ data, errorFactory }) => {
@@ -6850,7 +8356,7 @@ const validateColorCreateData = ({ data, errorFactory }) => {
6850
8356
  allowedKeys:
6851
8357
  data.type === "folder"
6852
8358
  ? ["type", "name", "description"]
6853
- : ["type", "name", "description", "hex"],
8359
+ : ["type", "name", "description", "tagIds", "hex"],
6854
8360
  path: "payload.data",
6855
8361
  errorFactory,
6856
8362
  });
@@ -6873,11 +8379,24 @@ const validateColorCreateData = ({ data, errorFactory }) => {
6873
8379
  );
6874
8380
  }
6875
8381
 
6876
- if (data.type === "color" && !isHexColor(data.hex)) {
6877
- return invalidFromErrorFactory(
6878
- errorFactory,
6879
- "payload.data.hex must be a #RRGGBB string",
6880
- );
8382
+ if (data.type === "color") {
8383
+ {
8384
+ const result = validateOptionalUniqueIdArray({
8385
+ value: data.tagIds,
8386
+ path: "payload.data.tagIds",
8387
+ errorFactory,
8388
+ });
8389
+ if (result?.valid === false) {
8390
+ return result;
8391
+ }
8392
+ }
8393
+
8394
+ if (!isHexColor(data.hex)) {
8395
+ return invalidFromErrorFactory(
8396
+ errorFactory,
8397
+ "payload.data.hex must be a #RRGGBB string",
8398
+ );
8399
+ }
6881
8400
  }
6882
8401
  };
6883
8402
 
@@ -6885,7 +8404,7 @@ const validateColorUpdateData = ({ data, errorFactory }) => {
6885
8404
  {
6886
8405
  const result = validateAllowedKeys({
6887
8406
  value: data,
6888
- allowedKeys: ["name", "description", "hex"],
8407
+ allowedKeys: ["name", "description", "tagIds", "hex"],
6889
8408
  path: "payload.data",
6890
8409
  errorFactory,
6891
8410
  });
@@ -6915,6 +8434,17 @@ const validateColorUpdateData = ({ data, errorFactory }) => {
6915
8434
  );
6916
8435
  }
6917
8436
 
8437
+ {
8438
+ const result = validateOptionalUniqueIdArray({
8439
+ value: data.tagIds,
8440
+ path: "payload.data.tagIds",
8441
+ errorFactory,
8442
+ });
8443
+ if (result?.valid === false) {
8444
+ return result;
8445
+ }
8446
+ }
8447
+
6918
8448
  if (data.hex !== undefined && !isHexColor(data.hex)) {
6919
8449
  return invalidFromErrorFactory(
6920
8450
  errorFactory,
@@ -6944,7 +8474,7 @@ const validateAnimationCreateData = ({ data, errorFactory }) => {
6944
8474
  allowedKeys:
6945
8475
  data.type === "folder"
6946
8476
  ? ["type", "name", "description"]
6947
- : ["type", "name", "description", "animation"],
8477
+ : ["type", "name", "description", "tagIds", "animation"],
6948
8478
  path: "payload.data",
6949
8479
  errorFactory,
6950
8480
  });
@@ -6968,6 +8498,17 @@ const validateAnimationCreateData = ({ data, errorFactory }) => {
6968
8498
  }
6969
8499
 
6970
8500
  if (data.type === "animation") {
8501
+ {
8502
+ const result = validateOptionalUniqueIdArray({
8503
+ value: data.tagIds,
8504
+ path: "payload.data.tagIds",
8505
+ errorFactory,
8506
+ });
8507
+ if (result?.valid === false) {
8508
+ return result;
8509
+ }
8510
+ }
8511
+
6971
8512
  {
6972
8513
  const result = validateAnimationDefinition({
6973
8514
  animation: data.animation,
@@ -6985,7 +8526,7 @@ const validateAnimationUpdateData = ({ data, errorFactory }) => {
6985
8526
  {
6986
8527
  const result = validateAllowedKeys({
6987
8528
  value: data,
6988
- allowedKeys: ["name", "description", "animation"],
8529
+ allowedKeys: ["name", "description", "tagIds", "animation"],
6989
8530
  path: "payload.data",
6990
8531
  errorFactory,
6991
8532
  });
@@ -7015,6 +8556,17 @@ const validateAnimationUpdateData = ({ data, errorFactory }) => {
7015
8556
  );
7016
8557
  }
7017
8558
 
8559
+ {
8560
+ const result = validateOptionalUniqueIdArray({
8561
+ value: data.tagIds,
8562
+ path: "payload.data.tagIds",
8563
+ errorFactory,
8564
+ });
8565
+ if (result?.valid === false) {
8566
+ return result;
8567
+ }
8568
+ }
8569
+
7018
8570
  if (data.animation !== undefined) {
7019
8571
  {
7020
8572
  const result = validateAnimationDefinition({
@@ -7054,6 +8606,7 @@ const validateTransformCreateData = ({ data, errorFactory }) => {
7054
8606
  "type",
7055
8607
  "name",
7056
8608
  "description",
8609
+ "tagIds",
7057
8610
  "x",
7058
8611
  "y",
7059
8612
  "scaleX",
@@ -7085,6 +8638,17 @@ const validateTransformCreateData = ({ data, errorFactory }) => {
7085
8638
  }
7086
8639
 
7087
8640
  if (data.type === "transform") {
8641
+ {
8642
+ const result = validateOptionalUniqueIdArray({
8643
+ value: data.tagIds,
8644
+ path: "payload.data.tagIds",
8645
+ errorFactory,
8646
+ });
8647
+ if (result?.valid === false) {
8648
+ return result;
8649
+ }
8650
+ }
8651
+
7088
8652
  for (const key of [
7089
8653
  "x",
7090
8654
  "y",
@@ -7129,6 +8693,7 @@ const validateParticleCreateData = ({ data, errorFactory }) => {
7129
8693
  "type",
7130
8694
  "name",
7131
8695
  "description",
8696
+ "tagIds",
7132
8697
  "width",
7133
8698
  "height",
7134
8699
  "seed",
@@ -7160,6 +8725,17 @@ const validateParticleCreateData = ({ data, errorFactory }) => {
7160
8725
  return;
7161
8726
  }
7162
8727
 
8728
+ {
8729
+ const result = validateOptionalUniqueIdArray({
8730
+ value: data.tagIds,
8731
+ path: "payload.data.tagIds",
8732
+ errorFactory,
8733
+ });
8734
+ if (result?.valid === false) {
8735
+ return result;
8736
+ }
8737
+ }
8738
+
7163
8739
  if (!isFiniteNumber(data.width) || data.width <= 0) {
7164
8740
  return invalidFromErrorFactory(
7165
8741
  errorFactory,
@@ -7204,6 +8780,7 @@ const validateParticleUpdateData = ({ data, errorFactory }) => {
7204
8780
  allowedKeys: [
7205
8781
  "name",
7206
8782
  "description",
8783
+ "tagIds",
7207
8784
  "width",
7208
8785
  "height",
7209
8786
  "seed",
@@ -7238,6 +8815,17 @@ const validateParticleUpdateData = ({ data, errorFactory }) => {
7238
8815
  );
7239
8816
  }
7240
8817
 
8818
+ {
8819
+ const result = validateOptionalUniqueIdArray({
8820
+ value: data.tagIds,
8821
+ path: "payload.data.tagIds",
8822
+ errorFactory,
8823
+ });
8824
+ if (result?.valid === false) {
8825
+ return result;
8826
+ }
8827
+ }
8828
+
7241
8829
  if (
7242
8830
  data.width !== undefined &&
7243
8831
  (!isFiniteNumber(data.width) || data.width <= 0)
@@ -7290,6 +8878,7 @@ const validateTransformUpdateData = ({ data, errorFactory }) => {
7290
8878
  allowedKeys: [
7291
8879
  "name",
7292
8880
  "description",
8881
+ "tagIds",
7293
8882
  "x",
7294
8883
  "y",
7295
8884
  "scaleX",
@@ -7327,6 +8916,17 @@ const validateTransformUpdateData = ({ data, errorFactory }) => {
7327
8916
  );
7328
8917
  }
7329
8918
 
8919
+ {
8920
+ const result = validateOptionalUniqueIdArray({
8921
+ value: data.tagIds,
8922
+ path: "payload.data.tagIds",
8923
+ errorFactory,
8924
+ });
8925
+ if (result?.valid === false) {
8926
+ return result;
8927
+ }
8928
+ }
8929
+
7330
8930
  for (const key of [
7331
8931
  "x",
7332
8932
  "y",
@@ -7366,7 +8966,15 @@ const validateVariableCreateData = ({ data, errorFactory }) => {
7366
8966
  allowedKeys:
7367
8967
  data.type === "folder"
7368
8968
  ? ["type", "name", "description"]
7369
- : ["type", "name", "description", "scope", "default", "value"],
8969
+ : [
8970
+ "type",
8971
+ "name",
8972
+ "description",
8973
+ "tagIds",
8974
+ "scope",
8975
+ "default",
8976
+ "value",
8977
+ ],
7370
8978
  path: "payload.data",
7371
8979
  errorFactory,
7372
8980
  });
@@ -7390,6 +8998,18 @@ const validateVariableCreateData = ({ data, errorFactory }) => {
7390
8998
  }
7391
8999
 
7392
9000
  if (data.type !== "folder") {
9001
+ {
9002
+ const result = validateOptionalUniqueIdArray({
9003
+ value: data.tagIds,
9004
+ path: "payload.data.tagIds",
9005
+ errorFactory,
9006
+ allowEmpty: false,
9007
+ });
9008
+ if (result?.valid === false) {
9009
+ return result;
9010
+ }
9011
+ }
9012
+
7393
9013
  if (!VARIABLE_SCOPE_KEYS.includes(data.scope)) {
7394
9014
  return invalidFromErrorFactory(
7395
9015
  errorFactory,
@@ -7426,7 +9046,14 @@ const validateVariableUpdateData = ({ data, errorFactory }) => {
7426
9046
  {
7427
9047
  const result = validateAllowedKeys({
7428
9048
  value: data,
7429
- allowedKeys: ["name", "description", "scope", "default", "value"],
9049
+ allowedKeys: [
9050
+ "name",
9051
+ "description",
9052
+ "tagIds",
9053
+ "scope",
9054
+ "default",
9055
+ "value",
9056
+ ],
7430
9057
  path: "payload.data",
7431
9058
  errorFactory,
7432
9059
  });
@@ -7462,6 +9089,18 @@ const validateVariableUpdateData = ({ data, errorFactory }) => {
7462
9089
  "payload.data.scope must be 'context', 'device', or 'account' when provided",
7463
9090
  );
7464
9091
  }
9092
+
9093
+ {
9094
+ const result = validateOptionalUniqueIdArray({
9095
+ value: data.tagIds,
9096
+ path: "payload.data.tagIds",
9097
+ errorFactory,
9098
+ allowEmpty: false,
9099
+ });
9100
+ if (result?.valid === false) {
9101
+ return result;
9102
+ }
9103
+ }
7465
9104
  };
7466
9105
 
7467
9106
  const validateTextStyleCreateData = ({ data, errorFactory }) => {
@@ -7489,6 +9128,7 @@ const validateTextStyleCreateData = ({ data, errorFactory }) => {
7489
9128
  "type",
7490
9129
  "name",
7491
9130
  "description",
9131
+ "tagIds",
7492
9132
  "fontId",
7493
9133
  "colorId",
7494
9134
  "fontSize",
@@ -7527,6 +9167,17 @@ const validateTextStyleCreateData = ({ data, errorFactory }) => {
7527
9167
  }
7528
9168
 
7529
9169
  if (data.type === "textStyle") {
9170
+ {
9171
+ const result = validateOptionalUniqueIdArray({
9172
+ value: data.tagIds,
9173
+ path: "payload.data.tagIds",
9174
+ errorFactory,
9175
+ });
9176
+ if (result?.valid === false) {
9177
+ return result;
9178
+ }
9179
+ }
9180
+
7530
9181
  {
7531
9182
  const result = validateTextStyleItems({
7532
9183
  items: {
@@ -7552,6 +9203,7 @@ const validateTextStyleUpdateData = ({ data, errorFactory }) => {
7552
9203
  allowedKeys: [
7553
9204
  "name",
7554
9205
  "description",
9206
+ "tagIds",
7555
9207
  "fontId",
7556
9208
  "colorId",
7557
9209
  "fontSize",
@@ -7596,6 +9248,17 @@ const validateTextStyleUpdateData = ({ data, errorFactory }) => {
7596
9248
  );
7597
9249
  }
7598
9250
 
9251
+ {
9252
+ const result = validateOptionalUniqueIdArray({
9253
+ value: data.tagIds,
9254
+ path: "payload.data.tagIds",
9255
+ errorFactory,
9256
+ });
9257
+ if (result?.valid === false) {
9258
+ return result;
9259
+ }
9260
+ }
9261
+
7599
9262
  for (const key of ["fontId", "colorId", "strokeColorId"]) {
7600
9263
  if (data[key] !== undefined && !isNonEmptyString(data[key])) {
7601
9264
  return invalidFromErrorFactory(
@@ -7680,6 +9343,7 @@ const validateCharacterSpriteCreateData = ({ data, errorFactory }) => {
7680
9343
  "type",
7681
9344
  "name",
7682
9345
  "description",
9346
+ "tagIds",
7683
9347
  "fileId",
7684
9348
  "thumbnailFileId",
7685
9349
  "width",
@@ -7708,6 +9372,17 @@ const validateCharacterSpriteCreateData = ({ data, errorFactory }) => {
7708
9372
  }
7709
9373
 
7710
9374
  if (data.type === "image") {
9375
+ {
9376
+ const result = validateOptionalUniqueIdArray({
9377
+ value: data.tagIds,
9378
+ path: "payload.data.tagIds",
9379
+ errorFactory,
9380
+ });
9381
+ if (result?.valid === false) {
9382
+ return result;
9383
+ }
9384
+ }
9385
+
7711
9386
  if (!isNonEmptyString(data.fileId)) {
7712
9387
  return invalidFromErrorFactory(
7713
9388
  errorFactory,
@@ -7748,6 +9423,7 @@ const validateCharacterSpriteUpdateData = ({ data, errorFactory }) => {
7748
9423
  allowedKeys: [
7749
9424
  "name",
7750
9425
  "description",
9426
+ "tagIds",
7751
9427
  "fileId",
7752
9428
  "thumbnailFileId",
7753
9429
  "width",
@@ -7775,6 +9451,17 @@ const validateCharacterSpriteUpdateData = ({ data, errorFactory }) => {
7775
9451
  );
7776
9452
  }
7777
9453
 
9454
+ {
9455
+ const result = validateOptionalUniqueIdArray({
9456
+ value: data.tagIds,
9457
+ path: "payload.data.tagIds",
9458
+ errorFactory,
9459
+ });
9460
+ if (result?.valid === false) {
9461
+ return result;
9462
+ }
9463
+ }
9464
+
7778
9465
  if (data.fileId !== undefined && !isNonEmptyString(data.fileId)) {
7779
9466
  return invalidFromErrorFactory(
7780
9467
  errorFactory,
@@ -7839,6 +9526,8 @@ const validateCharacterCreateData = ({ data, errorFactory }) => {
7839
9526
  "type",
7840
9527
  "name",
7841
9528
  "description",
9529
+ "tagIds",
9530
+ "spriteGroups",
7842
9531
  "shortcut",
7843
9532
  "fileId",
7844
9533
  "sprites",
@@ -7865,7 +9554,29 @@ const validateCharacterCreateData = ({ data, errorFactory }) => {
7865
9554
  );
7866
9555
  }
7867
9556
 
7868
- if (data.type === "character") {
9557
+ if (data.type === "character") {
9558
+ {
9559
+ const result = validateOptionalUniqueIdArray({
9560
+ value: data.tagIds,
9561
+ path: "payload.data.tagIds",
9562
+ errorFactory,
9563
+ });
9564
+ if (result?.valid === false) {
9565
+ return result;
9566
+ }
9567
+ }
9568
+
9569
+ {
9570
+ const result = validateCharacterSpriteGroups({
9571
+ value: data.spriteGroups,
9572
+ path: "payload.data.spriteGroups",
9573
+ errorFactory,
9574
+ });
9575
+ if (result?.valid === false) {
9576
+ return result;
9577
+ }
9578
+ }
9579
+
7869
9580
  if (data.shortcut !== undefined && !isString(data.shortcut)) {
7870
9581
  return invalidFromErrorFactory(
7871
9582
  errorFactory,
@@ -7885,7 +9596,13 @@ const validateCharacterCreateData = ({ data, errorFactory }) => {
7885
9596
  const result = validateNestedCollection({
7886
9597
  collection: data.sprites,
7887
9598
  path: "payload.data.sprites",
7888
- itemValidator: validateCharacterSpriteItems,
9599
+ itemValidator: ({ items, path, errorFactory }) =>
9600
+ validateCharacterSpriteItems({
9601
+ items,
9602
+ path,
9603
+ errorFactory,
9604
+ allowTagIds: false,
9605
+ }),
7889
9606
  treeValidator: validateGenericFolderOwnership,
7890
9607
  folderLabel: "folder sprite item",
7891
9608
  errorFactory,
@@ -7905,6 +9622,8 @@ const validateCharacterUpdateData = ({ data, errorFactory }) => {
7905
9622
  allowedKeys: [
7906
9623
  "name",
7907
9624
  "description",
9625
+ "tagIds",
9626
+ "spriteGroups",
7908
9627
  "shortcut",
7909
9628
  "fileId",
7910
9629
  ],
@@ -7944,13 +9663,34 @@ const validateCharacterUpdateData = ({ data, errorFactory }) => {
7944
9663
  );
7945
9664
  }
7946
9665
 
9666
+ {
9667
+ const result = validateOptionalUniqueIdArray({
9668
+ value: data.tagIds,
9669
+ path: "payload.data.tagIds",
9670
+ errorFactory,
9671
+ });
9672
+ if (result?.valid === false) {
9673
+ return result;
9674
+ }
9675
+ }
9676
+
9677
+ {
9678
+ const result = validateCharacterSpriteGroups({
9679
+ value: data.spriteGroups,
9680
+ path: "payload.data.spriteGroups",
9681
+ errorFactory,
9682
+ });
9683
+ if (result?.valid === false) {
9684
+ return result;
9685
+ }
9686
+ }
9687
+
7947
9688
  if (data.fileId !== undefined && !isNonEmptyString(data.fileId)) {
7948
9689
  return invalidFromErrorFactory(
7949
9690
  errorFactory,
7950
9691
  "payload.data.fileId must be a non-empty string when provided",
7951
9692
  );
7952
9693
  }
7953
-
7954
9694
  };
7955
9695
 
7956
9696
  const validateLayoutCreateData = ({ data, errorFactory }) => {
@@ -7978,6 +9718,7 @@ const validateLayoutCreateData = ({ data, errorFactory }) => {
7978
9718
  "type",
7979
9719
  "name",
7980
9720
  "description",
9721
+ "tagIds",
7981
9722
  "layoutType",
7982
9723
  "isFragment",
7983
9724
  "thumbnailFileId",
@@ -8028,6 +9769,17 @@ const validateLayoutCreateData = ({ data, errorFactory }) => {
8028
9769
  }
8029
9770
 
8030
9771
  if (data.type === "layout") {
9772
+ {
9773
+ const result = validateOptionalUniqueIdArray({
9774
+ value: data.tagIds,
9775
+ path: "payload.data.tagIds",
9776
+ errorFactory,
9777
+ });
9778
+ if (result?.valid === false) {
9779
+ return result;
9780
+ }
9781
+ }
9782
+
8031
9783
  if (!LAYOUT_TYPE_KEYS.includes(data.layoutType)) {
8032
9784
  return invalidFromErrorFactory(
8033
9785
  errorFactory,
@@ -8064,6 +9816,7 @@ const validateLayoutUpdateData = ({ data, errorFactory }) => {
8064
9816
  allowedKeys: [
8065
9817
  "name",
8066
9818
  "description",
9819
+ "tagIds",
8067
9820
  "layoutType",
8068
9821
  "isFragment",
8069
9822
  "thumbnailFileId",
@@ -8098,6 +9851,17 @@ const validateLayoutUpdateData = ({ data, errorFactory }) => {
8098
9851
  );
8099
9852
  }
8100
9853
 
9854
+ {
9855
+ const result = validateOptionalUniqueIdArray({
9856
+ value: data.tagIds,
9857
+ path: "payload.data.tagIds",
9858
+ errorFactory,
9859
+ });
9860
+ if (result?.valid === false) {
9861
+ return result;
9862
+ }
9863
+ }
9864
+
8101
9865
  if (
8102
9866
  data.thumbnailFileId !== undefined &&
8103
9867
  !isNonEmptyString(data.thumbnailFileId)
@@ -8162,6 +9926,7 @@ const validateControlCreateData = ({ data, errorFactory }) => {
8162
9926
  "type",
8163
9927
  "name",
8164
9928
  "description",
9929
+ "tagIds",
8165
9930
  "thumbnailFileId",
8166
9931
  "preview",
8167
9932
  "elements",
@@ -8211,6 +9976,18 @@ const validateControlCreateData = ({ data, errorFactory }) => {
8211
9976
  }
8212
9977
 
8213
9978
  if (data.type === "control") {
9979
+ {
9980
+ const result = validateOptionalUniqueIdArray({
9981
+ value: data.tagIds,
9982
+ path: "payload.data.tagIds",
9983
+ errorFactory,
9984
+ allowEmpty: false,
9985
+ });
9986
+ if (result?.valid === false) {
9987
+ return result;
9988
+ }
9989
+ }
9990
+
8214
9991
  {
8215
9992
  const result = validateNestedCollection({
8216
9993
  collection: data.elements,
@@ -8244,6 +10021,7 @@ const validateControlUpdateData = ({ data, errorFactory }) => {
8244
10021
  allowedKeys: [
8245
10022
  "name",
8246
10023
  "description",
10024
+ "tagIds",
8247
10025
  "keyboard",
8248
10026
  "thumbnailFileId",
8249
10027
  "preview",
@@ -8308,6 +10086,18 @@ const validateControlUpdateData = ({ data, errorFactory }) => {
8308
10086
  return result;
8309
10087
  }
8310
10088
  }
10089
+
10090
+ {
10091
+ const result = validateOptionalUniqueIdArray({
10092
+ value: data.tagIds,
10093
+ path: "payload.data.tagIds",
10094
+ errorFactory,
10095
+ allowEmpty: false,
10096
+ });
10097
+ if (result?.valid === false) {
10098
+ return result;
10099
+ }
10100
+ }
8311
10101
  };
8312
10102
 
8313
10103
  const validateLayoutElementCreateData = ({ data, errorFactory }) => {
@@ -8616,7 +10406,7 @@ const validateVisualElementReferenceTargets = ({
8616
10406
  if (!isLayoutConditionTarget(state, rule?.when?.target)) {
8617
10407
  return invalidFromErrorFactory(
8618
10408
  errorFactory,
8619
- `${ownerLabel} element conditionalOverrides.${index}.when.target must reference an existing variable or supported runtime condition`,
10409
+ `${ownerLabel} element conditionalOverrides.${index}.when.target must reference an existing variable or supported layout condition`,
8620
10410
  {
8621
10411
  [ownerIdField]: ownerId,
8622
10412
  elementId,
@@ -8790,6 +10580,7 @@ const createFolderedCollectionCommandDefinitions = ({
8790
10580
  validateCreateState = () => {},
8791
10581
  validateUpdateState = () => {},
8792
10582
  validateDeleteState = () => {},
10583
+ afterDelete = () => {},
8793
10584
  includeUpdate = true,
8794
10585
  }) => {
8795
10586
  const existingMessage = `payload.${idField} must reference an existing ${itemLabel}`;
@@ -9037,6 +10828,7 @@ const createFolderedCollectionCommandDefinitions = ({
9037
10828
  },
9038
10829
  reduce: ({ state, payload }) => {
9039
10830
  const deletedIds = new Set();
10831
+ const deletedItemsById = new Map();
9040
10832
 
9041
10833
  for (const itemId of payload[deleteArrayField]) {
9042
10834
  const removedNode = removeTreeNode({
@@ -9052,6 +10844,10 @@ const createFolderedCollectionCommandDefinitions = ({
9052
10844
  node: removedNode,
9053
10845
  })) {
9054
10846
  deletedIds.add(descendantId);
10847
+ deletedItemsById.set(
10848
+ descendantId,
10849
+ state[collectionKey].items[descendantId],
10850
+ );
9055
10851
  }
9056
10852
  }
9057
10853
 
@@ -9059,6 +10855,13 @@ const createFolderedCollectionCommandDefinitions = ({
9059
10855
  delete state[collectionKey].items[itemId];
9060
10856
  }
9061
10857
 
10858
+ afterDelete({
10859
+ state,
10860
+ payload,
10861
+ deletedIds,
10862
+ deletedItemsById,
10863
+ });
10864
+
9062
10865
  return state;
9063
10866
  },
9064
10867
  },
@@ -9389,7 +11192,8 @@ const COMMAND_DEFINITIONS = [
9389
11192
  }
9390
11193
  },
9391
11194
  validateAgainstState: () => {},
9392
- reduce: ({ payload }) => structuredClone(payload.state),
11195
+ reduce: ({ payload }) =>
11196
+ structuredClone(normalizeStateCollections(payload.state)),
9393
11197
  },
9394
11198
  ...createFolderedCollectionCommandDefinitions({
9395
11199
  familyName: "file",
@@ -9442,59 +11246,68 @@ const COMMAND_DEFINITIONS = [
9442
11246
  itemLabel: "spritesheet item",
9443
11247
  createDataValidator: validateSpritesheetCreateData,
9444
11248
  updateDataValidator: validateSpritesheetUpdateData,
9445
- createItem: ({ payload }) => ({
9446
- id: payload.spritesheetId,
9447
- type: payload.data.type,
9448
- name: payload.data.name,
9449
- ...(payload.data.description !== undefined
9450
- ? {
9451
- description: payload.data.description,
9452
- }
9453
- : {}),
9454
- ...(payload.data.type === "spritesheet"
9455
- ? {
9456
- fileId: payload.data.fileId,
9457
- ...(payload.data.thumbnailFileId !== undefined
9458
- ? {
9459
- thumbnailFileId: payload.data.thumbnailFileId,
9460
- }
9461
- : {}),
9462
- ...(payload.data.sheetWidth !== undefined
9463
- ? {
9464
- sheetWidth: payload.data.sheetWidth,
9465
- }
9466
- : {}),
9467
- ...(payload.data.sheetHeight !== undefined
9468
- ? {
9469
- sheetHeight: payload.data.sheetHeight,
9470
- }
9471
- : {}),
9472
- ...(payload.data.frameCount !== undefined
9473
- ? {
9474
- frameCount: payload.data.frameCount,
9475
- }
9476
- : {}),
9477
- ...(payload.data.width !== undefined
9478
- ? {
9479
- width: payload.data.width,
9480
- }
9481
- : {}),
9482
- ...(payload.data.height !== undefined
9483
- ? {
9484
- height: payload.data.height,
9485
- }
9486
- : {}),
9487
- jsonData: structuredClone(payload.data.jsonData),
9488
- animations: structuredClone(payload.data.animations),
9489
- }
9490
- : {}),
9491
- }),
11249
+ createItem: ({ payload }) => {
11250
+ const item = {
11251
+ id: payload.spritesheetId,
11252
+ type: payload.data.type,
11253
+ name: payload.data.name,
11254
+ };
11255
+
11256
+ if (payload.data.description !== undefined) {
11257
+ item.description = payload.data.description;
11258
+ }
11259
+
11260
+ if (payload.data.type !== "spritesheet") {
11261
+ return item;
11262
+ }
11263
+
11264
+ item.fileId = payload.data.fileId;
11265
+
11266
+ if (payload.data.thumbnailFileId !== undefined) {
11267
+ item.thumbnailFileId = payload.data.thumbnailFileId;
11268
+ }
11269
+
11270
+ if (payload.data.sheetWidth !== undefined) {
11271
+ item.sheetWidth = payload.data.sheetWidth;
11272
+ }
11273
+
11274
+ if (payload.data.sheetHeight !== undefined) {
11275
+ item.sheetHeight = payload.data.sheetHeight;
11276
+ }
11277
+
11278
+ if (payload.data.frameCount !== undefined) {
11279
+ item.frameCount = payload.data.frameCount;
11280
+ }
11281
+
11282
+ if (payload.data.width !== undefined) {
11283
+ item.width = payload.data.width;
11284
+ }
11285
+
11286
+ if (payload.data.height !== undefined) {
11287
+ item.height = payload.data.height;
11288
+ }
11289
+
11290
+ assignOptionalTagIds({
11291
+ target: item,
11292
+ tagIds: payload.data.tagIds,
11293
+ });
11294
+
11295
+ item.jsonData = structuredClone(payload.data.jsonData);
11296
+ item.animations = structuredClone(payload.data.animations);
11297
+
11298
+ return item;
11299
+ },
11300
+ updateItem: ({ currentItem, payload }) =>
11301
+ applyTagIdsUpdate({
11302
+ currentItem,
11303
+ data: payload.data,
11304
+ }),
9492
11305
  validateCreateState: ({ state, payload }) => {
9493
11306
  if (payload.data.type !== "spritesheet") {
9494
11307
  return;
9495
11308
  }
9496
11309
 
9497
- return validateReferencedFilesInData({
11310
+ const fileResult = validateReferencedFilesInData({
9498
11311
  state,
9499
11312
  data: payload.data,
9500
11313
  fields: ["fileId", "thumbnailFileId"],
@@ -9502,6 +11315,19 @@ const COMMAND_DEFINITIONS = [
9502
11315
  spritesheetId: payload.spritesheetId,
9503
11316
  },
9504
11317
  });
11318
+ if (!fileResult.valid) {
11319
+ return fileResult;
11320
+ }
11321
+
11322
+ return validateTagIdsAgainstScope({
11323
+ state,
11324
+ tagIds: payload.data.tagIds,
11325
+ scopeKey: "spritesheets",
11326
+ path: "payload.data.tagIds",
11327
+ details: {
11328
+ spritesheetId: payload.spritesheetId,
11329
+ },
11330
+ });
9505
11331
  },
9506
11332
  validateUpdateState: ({ state, payload, currentItem }) => {
9507
11333
  if (
@@ -9516,7 +11342,7 @@ const COMMAND_DEFINITIONS = [
9516
11342
  }
9517
11343
 
9518
11344
  if (currentItem.type === "spritesheet") {
9519
- return validateReferencedFilesInData({
11345
+ const fileResult = validateReferencedFilesInData({
9520
11346
  state,
9521
11347
  data: payload.data,
9522
11348
  fields: ["fileId", "thumbnailFileId"],
@@ -9524,6 +11350,19 @@ const COMMAND_DEFINITIONS = [
9524
11350
  spritesheetId: payload.spritesheetId,
9525
11351
  },
9526
11352
  });
11353
+ if (!fileResult.valid) {
11354
+ return fileResult;
11355
+ }
11356
+
11357
+ return validateTagIdsAgainstScope({
11358
+ state,
11359
+ tagIds: payload.data.tagIds,
11360
+ scopeKey: "spritesheets",
11361
+ path: "payload.data.tagIds",
11362
+ details: {
11363
+ spritesheetId: payload.spritesheetId,
11364
+ },
11365
+ });
9527
11366
  }
9528
11367
  },
9529
11368
  }),
@@ -10881,6 +12720,16 @@ const COMMAND_DEFINITIONS = [
10881
12720
  if (!result.valid) {
10882
12721
  return result;
10883
12722
  }
12723
+
12724
+ return validateTagIdsAgainstScope({
12725
+ state,
12726
+ tagIds: payload.data.tagIds,
12727
+ scopeKey: "images",
12728
+ path: "payload.data.tagIds",
12729
+ details: {
12730
+ imageId: payload.imageId,
12731
+ },
12732
+ });
10884
12733
  }
10885
12734
  },
10886
12735
  reduce: ({ state, payload }) => {
@@ -10905,6 +12754,11 @@ const COMMAND_DEFINITIONS = [
10905
12754
  if (payload.data.height !== undefined) {
10906
12755
  nextImage.height = payload.data.height;
10907
12756
  }
12757
+
12758
+ assignOptionalTagIds({
12759
+ target: nextImage,
12760
+ tagIds: payload.data.tagIds,
12761
+ });
10908
12762
  }
10909
12763
 
10910
12764
  state.images.items[payload.imageId] = nextImage;
@@ -10966,7 +12820,8 @@ const COMMAND_DEFINITIONS = [
10966
12820
  (payload.data.fileId !== undefined ||
10967
12821
  payload.data.thumbnailFileId !== undefined ||
10968
12822
  payload.data.width !== undefined ||
10969
- payload.data.height !== undefined)
12823
+ payload.data.height !== undefined ||
12824
+ payload.data.tagIds !== undefined)
10970
12825
  ) {
10971
12826
  return invalidPrecondition(
10972
12827
  "folder image items cannot update file fields",
@@ -10985,14 +12840,24 @@ const COMMAND_DEFINITIONS = [
10985
12840
  if (!result.valid) {
10986
12841
  return result;
10987
12842
  }
12843
+
12844
+ return validateTagIdsAgainstScope({
12845
+ state,
12846
+ tagIds: payload.data.tagIds,
12847
+ scopeKey: "images",
12848
+ path: "payload.data.tagIds",
12849
+ details: {
12850
+ imageId: payload.imageId,
12851
+ },
12852
+ });
10988
12853
  }
10989
12854
  },
10990
12855
  reduce: ({ state, payload }) => {
10991
12856
  const currentImage = state.images.items[payload.imageId];
10992
- state.images.items[payload.imageId] = {
10993
- ...structuredClone(currentImage),
10994
- ...structuredClone(payload.data),
10995
- };
12857
+ state.images.items[payload.imageId] = applyTagIdsUpdate({
12858
+ currentItem: currentImage,
12859
+ data: payload.data,
12860
+ });
10996
12861
  return state;
10997
12862
  },
10998
12863
  },
@@ -11298,6 +13163,16 @@ const COMMAND_DEFINITIONS = [
11298
13163
  if (!result.valid) {
11299
13164
  return result;
11300
13165
  }
13166
+
13167
+ return validateTagIdsAgainstScope({
13168
+ state,
13169
+ tagIds: payload.data.tagIds,
13170
+ scopeKey: "sounds",
13171
+ path: "payload.data.tagIds",
13172
+ details: {
13173
+ soundId: payload.soundId,
13174
+ },
13175
+ });
11301
13176
  }
11302
13177
  },
11303
13178
  reduce: ({ state, payload }) => {
@@ -11319,6 +13194,11 @@ const COMMAND_DEFINITIONS = [
11319
13194
  if (payload.data.duration !== undefined) {
11320
13195
  nextSound.duration = payload.data.duration;
11321
13196
  }
13197
+
13198
+ assignOptionalTagIds({
13199
+ target: nextSound,
13200
+ tagIds: payload.data.tagIds,
13201
+ });
11322
13202
  }
11323
13203
 
11324
13204
  state.sounds.items[payload.soundId] = nextSound;
@@ -11379,7 +13259,8 @@ const COMMAND_DEFINITIONS = [
11379
13259
  currentSound.type === "folder" &&
11380
13260
  (payload.data.fileId !== undefined ||
11381
13261
  payload.data.waveformDataFileId !== undefined ||
11382
- payload.data.duration !== undefined)
13262
+ payload.data.duration !== undefined ||
13263
+ payload.data.tagIds !== undefined)
11383
13264
  ) {
11384
13265
  return invalidPrecondition(
11385
13266
  "folder sound items cannot update file fields",
@@ -11399,14 +13280,24 @@ const COMMAND_DEFINITIONS = [
11399
13280
  if (!result.valid) {
11400
13281
  return result;
11401
13282
  }
13283
+
13284
+ return validateTagIdsAgainstScope({
13285
+ state,
13286
+ tagIds: payload.data.tagIds,
13287
+ scopeKey: "sounds",
13288
+ path: "payload.data.tagIds",
13289
+ details: {
13290
+ soundId: payload.soundId,
13291
+ },
13292
+ });
11402
13293
  }
11403
13294
  },
11404
13295
  reduce: ({ state, payload }) => {
11405
13296
  const currentSound = state.sounds.items[payload.soundId];
11406
- state.sounds.items[payload.soundId] = {
11407
- ...structuredClone(currentSound),
11408
- ...structuredClone(payload.data),
11409
- };
13297
+ state.sounds.items[payload.soundId] = applyTagIdsUpdate({
13298
+ currentItem: currentSound,
13299
+ data: payload.data,
13300
+ });
11410
13301
  return state;
11411
13302
  },
11412
13303
  },
@@ -11711,6 +13602,16 @@ const COMMAND_DEFINITIONS = [
11711
13602
  if (!result.valid) {
11712
13603
  return result;
11713
13604
  }
13605
+
13606
+ return validateTagIdsAgainstScope({
13607
+ state,
13608
+ tagIds: payload.data.tagIds,
13609
+ scopeKey: "videos",
13610
+ path: "payload.data.tagIds",
13611
+ details: {
13612
+ videoId: payload.videoId,
13613
+ },
13614
+ });
11714
13615
  }
11715
13616
  },
11716
13617
  reduce: ({ state, payload }) => {
@@ -11736,6 +13637,11 @@ const COMMAND_DEFINITIONS = [
11736
13637
  if (payload.data.height !== undefined) {
11737
13638
  nextVideo.height = payload.data.height;
11738
13639
  }
13640
+
13641
+ assignOptionalTagIds({
13642
+ target: nextVideo,
13643
+ tagIds: payload.data.tagIds,
13644
+ });
11739
13645
  }
11740
13646
 
11741
13647
  state.videos.items[payload.videoId] = nextVideo;
@@ -11798,7 +13704,8 @@ const COMMAND_DEFINITIONS = [
11798
13704
  payload.data.thumbnailFileId !== undefined ||
11799
13705
  payload.data.duration !== undefined ||
11800
13706
  payload.data.width !== undefined ||
11801
- payload.data.height !== undefined)
13707
+ payload.data.height !== undefined ||
13708
+ payload.data.tagIds !== undefined)
11802
13709
  ) {
11803
13710
  return invalidPrecondition(
11804
13711
  "folder video items cannot update file fields",
@@ -11817,14 +13724,24 @@ const COMMAND_DEFINITIONS = [
11817
13724
  if (!result.valid) {
11818
13725
  return result;
11819
13726
  }
13727
+
13728
+ return validateTagIdsAgainstScope({
13729
+ state,
13730
+ tagIds: payload.data.tagIds,
13731
+ scopeKey: "videos",
13732
+ path: "payload.data.tagIds",
13733
+ details: {
13734
+ videoId: payload.videoId,
13735
+ },
13736
+ });
11820
13737
  }
11821
13738
  },
11822
13739
  reduce: ({ state, payload }) => {
11823
13740
  const currentVideo = state.videos.items[payload.videoId];
11824
- state.videos.items[payload.videoId] = {
11825
- ...structuredClone(currentVideo),
11826
- ...structuredClone(payload.data),
11827
- };
13741
+ state.videos.items[payload.videoId] = applyTagIdsUpdate({
13742
+ currentItem: currentVideo,
13743
+ data: payload.data,
13744
+ });
11828
13745
  return state;
11829
13746
  },
11830
13747
  },
@@ -12130,6 +14047,16 @@ const COMMAND_DEFINITIONS = [
12130
14047
  if (!result.valid) {
12131
14048
  return result;
12132
14049
  }
14050
+
14051
+ return validateTagIdsAgainstScope({
14052
+ state,
14053
+ tagIds: payload.data.tagIds,
14054
+ scopeKey: "animations",
14055
+ path: "payload.data.tagIds",
14056
+ details: {
14057
+ animationId: payload.animationId,
14058
+ },
14059
+ });
12133
14060
  }
12134
14061
  },
12135
14062
  reduce: ({ state, payload }) => {
@@ -12144,6 +14071,10 @@ const COMMAND_DEFINITIONS = [
12144
14071
  }
12145
14072
 
12146
14073
  if (payload.data.type === "animation") {
14074
+ assignOptionalTagIds({
14075
+ target: nextAnimation,
14076
+ tagIds: payload.data.tagIds,
14077
+ });
12147
14078
  nextAnimation.animation = structuredClone(payload.data.animation);
12148
14079
  }
12149
14080
 
@@ -12203,32 +14134,45 @@ const COMMAND_DEFINITIONS = [
12203
14134
 
12204
14135
  if (
12205
14136
  currentAnimation.type === "folder" &&
12206
- payload.data.animation !== undefined
14137
+ Object.keys(payload.data).some(
14138
+ (key) => key !== "name" && key !== "description",
14139
+ )
12207
14140
  ) {
12208
14141
  return invalidPrecondition(
12209
14142
  "folder animation items cannot update animation fields",
12210
14143
  );
12211
14144
  }
12212
14145
 
12213
- if (payload.data.animation !== undefined) {
12214
- const result = validateAnimationMaskImageReferences({
14146
+ if (currentAnimation.type === "animation") {
14147
+ if (payload.data.animation !== undefined) {
14148
+ const result = validateAnimationMaskImageReferences({
14149
+ state,
14150
+ animation: payload.data.animation,
14151
+ path: "payload.data.animation",
14152
+ details: { animationId: payload.animationId },
14153
+ errorFactory: createPreconditionValidationError,
14154
+ });
14155
+ if (!result.valid) {
14156
+ return result;
14157
+ }
14158
+ }
14159
+
14160
+ return validateTagIdsAgainstScope({
12215
14161
  state,
12216
- animation: payload.data.animation,
12217
- path: "payload.data.animation",
14162
+ tagIds: payload.data.tagIds,
14163
+ scopeKey: "animations",
14164
+ path: "payload.data.tagIds",
12218
14165
  details: { animationId: payload.animationId },
12219
14166
  errorFactory: createPreconditionValidationError,
12220
14167
  });
12221
- if (!result.valid) {
12222
- return result;
12223
- }
12224
14168
  }
12225
14169
  },
12226
14170
  reduce: ({ state, payload }) => {
12227
14171
  const currentAnimation = state.animations.items[payload.animationId];
12228
- state.animations.items[payload.animationId] = {
12229
- ...structuredClone(currentAnimation),
12230
- ...structuredClone(payload.data),
12231
- };
14172
+ state.animations.items[payload.animationId] = applyTagIdsUpdate({
14173
+ currentItem: currentAnimation,
14174
+ data: payload.data,
14175
+ });
12232
14176
  return state;
12233
14177
  },
12234
14178
  },
@@ -12522,6 +14466,21 @@ const COMMAND_DEFINITIONS = [
12522
14466
  }
12523
14467
 
12524
14468
  if (payload.data.type === "font") {
14469
+ {
14470
+ const result = validateTagIdsAgainstScope({
14471
+ state,
14472
+ tagIds: payload.data.tagIds,
14473
+ scopeKey: "fonts",
14474
+ path: "payload.data.tagIds",
14475
+ details: {
14476
+ fontId: payload.fontId,
14477
+ },
14478
+ });
14479
+ if (!result.valid) {
14480
+ return result;
14481
+ }
14482
+ }
14483
+
12525
14484
  const result = validateReferencedFilesInData({
12526
14485
  state,
12527
14486
  data: payload.data,
@@ -12547,6 +14506,10 @@ const COMMAND_DEFINITIONS = [
12547
14506
  }
12548
14507
 
12549
14508
  if (payload.data.type === "font") {
14509
+ assignOptionalTagIds({
14510
+ target: nextFont,
14511
+ tagIds: payload.data.tagIds,
14512
+ });
12550
14513
  nextFont.fileId = payload.data.fileId;
12551
14514
  nextFont.fontFamily = payload.data.fontFamily;
12552
14515
  }
@@ -12607,7 +14570,8 @@ const COMMAND_DEFINITIONS = [
12607
14570
 
12608
14571
  if (
12609
14572
  currentFont.type === "folder" &&
12610
- (payload.data.fileId !== undefined ||
14573
+ (payload.data.tagIds !== undefined ||
14574
+ payload.data.fileId !== undefined ||
12611
14575
  payload.data.fontFamily !== undefined)
12612
14576
  ) {
12613
14577
  return invalidPrecondition(
@@ -12616,6 +14580,21 @@ const COMMAND_DEFINITIONS = [
12616
14580
  }
12617
14581
 
12618
14582
  if (currentFont.type === "font") {
14583
+ {
14584
+ const result = validateTagIdsAgainstScope({
14585
+ state,
14586
+ tagIds: payload.data.tagIds,
14587
+ scopeKey: "fonts",
14588
+ path: "payload.data.tagIds",
14589
+ details: {
14590
+ fontId: payload.fontId,
14591
+ },
14592
+ });
14593
+ if (!result.valid) {
14594
+ return result;
14595
+ }
14596
+ }
14597
+
12619
14598
  const result = validateReferencedFilesInData({
12620
14599
  state,
12621
14600
  data: payload.data,
@@ -12631,10 +14610,10 @@ const COMMAND_DEFINITIONS = [
12631
14610
  },
12632
14611
  reduce: ({ state, payload }) => {
12633
14612
  const currentFont = state.fonts.items[payload.fontId];
12634
- state.fonts.items[payload.fontId] = {
12635
- ...structuredClone(currentFont),
12636
- ...structuredClone(payload.data),
12637
- };
14613
+ state.fonts.items[payload.fontId] = applyTagIdsUpdate({
14614
+ currentItem: currentFont,
14615
+ data: payload.data,
14616
+ });
12638
14617
  return state;
12639
14618
  },
12640
14619
  },
@@ -12926,6 +14905,18 @@ const COMMAND_DEFINITIONS = [
12926
14905
  );
12927
14906
  }
12928
14907
  }
14908
+
14909
+ if (payload.data.type === "color") {
14910
+ return validateTagIdsAgainstScope({
14911
+ state,
14912
+ tagIds: payload.data.tagIds,
14913
+ scopeKey: "colors",
14914
+ path: "payload.data.tagIds",
14915
+ details: {
14916
+ colorId: payload.colorId,
14917
+ },
14918
+ });
14919
+ }
12929
14920
  },
12930
14921
  reduce: ({ state, payload }) => {
12931
14922
  const nextColor = {
@@ -12940,6 +14931,10 @@ const COMMAND_DEFINITIONS = [
12940
14931
 
12941
14932
  if (payload.data.type === "color") {
12942
14933
  nextColor.hex = payload.data.hex;
14934
+ assignOptionalTagIds({
14935
+ target: nextColor,
14936
+ tagIds: payload.data.tagIds,
14937
+ });
12943
14938
  }
12944
14939
 
12945
14940
  state.colors.items[payload.colorId] = nextColor;
@@ -12996,18 +14991,33 @@ const COMMAND_DEFINITIONS = [
12996
14991
  );
12997
14992
  }
12998
14993
 
12999
- if (currentColor.type === "folder" && payload.data.hex !== undefined) {
14994
+ if (
14995
+ currentColor.type === "folder" &&
14996
+ (payload.data.tagIds !== undefined || payload.data.hex !== undefined)
14997
+ ) {
13000
14998
  return invalidPrecondition(
13001
14999
  "folder color items cannot update color fields",
13002
15000
  );
13003
15001
  }
15002
+
15003
+ if (currentColor.type === "color") {
15004
+ return validateTagIdsAgainstScope({
15005
+ state,
15006
+ tagIds: payload.data.tagIds,
15007
+ scopeKey: "colors",
15008
+ path: "payload.data.tagIds",
15009
+ details: {
15010
+ colorId: payload.colorId,
15011
+ },
15012
+ });
15013
+ }
13004
15014
  },
13005
15015
  reduce: ({ state, payload }) => {
13006
15016
  const currentColor = state.colors.items[payload.colorId];
13007
- state.colors.items[payload.colorId] = {
13008
- ...structuredClone(currentColor),
13009
- ...structuredClone(payload.data),
13010
- };
15017
+ state.colors.items[payload.colorId] = applyTagIdsUpdate({
15018
+ currentItem: currentColor,
15019
+ data: payload.data,
15020
+ });
13011
15021
  return state;
13012
15022
  },
13013
15023
  },
@@ -13229,6 +15239,10 @@ const COMMAND_DEFINITIONS = [
13229
15239
  item.width = payload.data.width;
13230
15240
  item.height = payload.data.height;
13231
15241
  item.modules = structuredClone(payload.data.modules);
15242
+ assignOptionalTagIds({
15243
+ target: item,
15244
+ tagIds: payload.data.tagIds,
15245
+ });
13232
15246
 
13233
15247
  if (payload.data.seed !== undefined && payload.data.seed !== null) {
13234
15248
  item.seed = payload.data.seed;
@@ -13237,10 +15251,10 @@ const COMMAND_DEFINITIONS = [
13237
15251
  return item;
13238
15252
  },
13239
15253
  updateItem: ({ currentItem, payload }) => {
13240
- const nextItem = {
13241
- ...structuredClone(currentItem),
13242
- ...structuredClone(payload.data),
13243
- };
15254
+ const nextItem = applyTagIdsUpdate({
15255
+ currentItem,
15256
+ data: payload.data,
15257
+ });
13244
15258
 
13245
15259
  if (payload.data.seed === null) {
13246
15260
  delete nextItem.seed;
@@ -13248,7 +15262,7 @@ const COMMAND_DEFINITIONS = [
13248
15262
 
13249
15263
  return nextItem;
13250
15264
  },
13251
- validateUpdateState: ({ payload, currentItem }) => {
15265
+ validateUpdateState: ({ state, payload, currentItem }) => {
13252
15266
  if (
13253
15267
  currentItem.type === "folder" &&
13254
15268
  Object.keys(payload.data).some(
@@ -13259,6 +15273,33 @@ const COMMAND_DEFINITIONS = [
13259
15273
  "folder particle items cannot update particle fields",
13260
15274
  );
13261
15275
  }
15276
+
15277
+ if (currentItem.type === "particle") {
15278
+ return validateTagIdsAgainstScope({
15279
+ state,
15280
+ tagIds: payload.data.tagIds,
15281
+ scopeKey: "particles",
15282
+ path: "payload.data.tagIds",
15283
+ details: {
15284
+ particleId: payload.particleId,
15285
+ },
15286
+ });
15287
+ }
15288
+ },
15289
+ validateCreateState: ({ state, payload }) => {
15290
+ if (payload.data.type !== "particle") {
15291
+ return;
15292
+ }
15293
+
15294
+ return validateTagIdsAgainstScope({
15295
+ state,
15296
+ tagIds: payload.data.tagIds,
15297
+ scopeKey: "particles",
15298
+ path: "payload.data.tagIds",
15299
+ details: {
15300
+ particleId: payload.particleId,
15301
+ },
15302
+ });
13262
15303
  },
13263
15304
  }),
13264
15305
  ...createFolderedCollectionCommandDefinitions({
@@ -13268,28 +15309,57 @@ const COMMAND_DEFINITIONS = [
13268
15309
  itemLabel: "transform item",
13269
15310
  createDataValidator: validateTransformCreateData,
13270
15311
  updateDataValidator: validateTransformUpdateData,
13271
- createItem: ({ payload }) => ({
13272
- id: payload.transformId,
13273
- type: payload.data.type,
13274
- name: payload.data.name,
13275
- ...(payload.data.description !== undefined
13276
- ? {
13277
- description: payload.data.description,
13278
- }
13279
- : {}),
13280
- ...(payload.data.type === "transform"
13281
- ? {
13282
- x: payload.data.x,
13283
- y: payload.data.y,
13284
- scaleX: payload.data.scaleX,
13285
- scaleY: payload.data.scaleY,
13286
- anchorX: payload.data.anchorX,
13287
- anchorY: payload.data.anchorY,
13288
- rotation: payload.data.rotation,
13289
- }
13290
- : {}),
13291
- }),
13292
- validateUpdateState: ({ payload, currentItem }) => {
15312
+ createItem: ({ payload }) => {
15313
+ const item = {
15314
+ id: payload.transformId,
15315
+ type: payload.data.type,
15316
+ name: payload.data.name,
15317
+ ...(payload.data.description !== undefined
15318
+ ? {
15319
+ description: payload.data.description,
15320
+ }
15321
+ : {}),
15322
+ };
15323
+
15324
+ if (payload.data.type !== "transform") {
15325
+ return item;
15326
+ }
15327
+
15328
+ item.x = payload.data.x;
15329
+ item.y = payload.data.y;
15330
+ item.scaleX = payload.data.scaleX;
15331
+ item.scaleY = payload.data.scaleY;
15332
+ item.anchorX = payload.data.anchorX;
15333
+ item.anchorY = payload.data.anchorY;
15334
+ item.rotation = payload.data.rotation;
15335
+ assignOptionalTagIds({
15336
+ target: item,
15337
+ tagIds: payload.data.tagIds,
15338
+ });
15339
+
15340
+ return item;
15341
+ },
15342
+ updateItem: ({ currentItem, payload }) =>
15343
+ applyTagIdsUpdate({
15344
+ currentItem,
15345
+ data: payload.data,
15346
+ }),
15347
+ validateCreateState: ({ state, payload }) => {
15348
+ if (payload.data.type !== "transform") {
15349
+ return;
15350
+ }
15351
+
15352
+ return validateTagIdsAgainstScope({
15353
+ state,
15354
+ tagIds: payload.data.tagIds,
15355
+ scopeKey: "transforms",
15356
+ path: "payload.data.tagIds",
15357
+ details: {
15358
+ transformId: payload.transformId,
15359
+ },
15360
+ });
15361
+ },
15362
+ validateUpdateState: ({ state, payload, currentItem }) => {
13293
15363
  if (
13294
15364
  currentItem.type === "folder" &&
13295
15365
  Object.keys(payload.data).some(
@@ -13300,6 +15370,18 @@ const COMMAND_DEFINITIONS = [
13300
15370
  "folder transform items cannot update transform fields",
13301
15371
  );
13302
15372
  }
15373
+
15374
+ if (currentItem.type === "transform") {
15375
+ return validateTagIdsAgainstScope({
15376
+ state,
15377
+ tagIds: payload.data.tagIds,
15378
+ scopeKey: "transforms",
15379
+ path: "payload.data.tagIds",
15380
+ details: {
15381
+ transformId: payload.transformId,
15382
+ },
15383
+ });
15384
+ }
13303
15385
  },
13304
15386
  }),
13305
15387
  ...createFolderedCollectionCommandDefinitions({
@@ -13309,24 +15391,38 @@ const COMMAND_DEFINITIONS = [
13309
15391
  itemLabel: "variable item",
13310
15392
  createDataValidator: validateVariableCreateData,
13311
15393
  updateDataValidator: validateVariableUpdateData,
13312
- createItem: ({ payload }) => ({
13313
- id: payload.variableId,
13314
- type: payload.data.type,
13315
- name: payload.data.name,
13316
- ...(payload.data.description !== undefined
13317
- ? {
13318
- description: payload.data.description,
13319
- }
13320
- : {}),
13321
- ...(payload.data.type === "folder"
13322
- ? {}
13323
- : {
13324
- scope: payload.data.scope,
13325
- default: payload.data.default,
13326
- value: payload.data.value,
13327
- }),
13328
- }),
13329
- validateUpdateState: ({ payload, currentItem }) => {
15394
+ createItem: ({ payload }) => {
15395
+ const data = structuredClone(payload.data);
15396
+ if (!Array.isArray(data.tagIds) || data.tagIds.length === 0) {
15397
+ delete data.tagIds;
15398
+ }
15399
+
15400
+ return {
15401
+ id: payload.variableId,
15402
+ ...data,
15403
+ };
15404
+ },
15405
+ updateItem: ({ currentItem, payload }) =>
15406
+ applyTagIdsUpdate({
15407
+ currentItem,
15408
+ data: payload.data,
15409
+ }),
15410
+ validateCreateState: ({ state, payload }) => {
15411
+ if (payload.data.type === "folder") {
15412
+ return;
15413
+ }
15414
+
15415
+ return validateTagIdsAgainstScope({
15416
+ state,
15417
+ tagIds: payload.data.tagIds,
15418
+ scopeKey: "variables",
15419
+ path: "payload.data.tagIds",
15420
+ details: {
15421
+ variableId: payload.variableId,
15422
+ },
15423
+ });
15424
+ },
15425
+ validateUpdateState: ({ state, payload, currentItem }) => {
13330
15426
  if (
13331
15427
  currentItem.type === "folder" &&
13332
15428
  Object.keys(payload.data).some(
@@ -13339,6 +15435,21 @@ const COMMAND_DEFINITIONS = [
13339
15435
  }
13340
15436
 
13341
15437
  if (currentItem.type !== "folder") {
15438
+ {
15439
+ const result = validateTagIdsAgainstScope({
15440
+ state,
15441
+ tagIds: payload.data.tagIds,
15442
+ scopeKey: "variables",
15443
+ path: "payload.data.tagIds",
15444
+ details: {
15445
+ variableId: payload.variableId,
15446
+ },
15447
+ });
15448
+ if (!result.valid) {
15449
+ return result;
15450
+ }
15451
+ }
15452
+
13342
15453
  if (payload.data.default !== undefined) {
13343
15454
  {
13344
15455
  const result = validateVariableTypedValue({
@@ -13376,16 +15487,43 @@ const COMMAND_DEFINITIONS = [
13376
15487
  itemLabel: "text style item",
13377
15488
  createDataValidator: validateTextStyleCreateData,
13378
15489
  updateDataValidator: validateTextStyleUpdateData,
13379
- createItem: ({ payload }) => ({
13380
- id: payload.textStyleId,
13381
- ...structuredClone(payload.data),
13382
- }),
15490
+ createItem: ({ payload }) => {
15491
+ const data = structuredClone(payload.data);
15492
+ if (!Array.isArray(data.tagIds) || data.tagIds.length === 0) {
15493
+ delete data.tagIds;
15494
+ }
15495
+
15496
+ return {
15497
+ id: payload.textStyleId,
15498
+ ...data,
15499
+ };
15500
+ },
15501
+ updateItem: ({ currentItem, payload }) =>
15502
+ applyTagIdsUpdate({
15503
+ currentItem,
15504
+ data: payload.data,
15505
+ }),
13383
15506
  validateCreateState: ({ state, payload }) => {
13384
15507
  const data = payload.data;
13385
15508
  if (data.type !== "textStyle") {
13386
15509
  return;
13387
15510
  }
13388
15511
 
15512
+ {
15513
+ const result = validateTagIdsAgainstScope({
15514
+ state,
15515
+ tagIds: payload.data.tagIds,
15516
+ scopeKey: "textStyles",
15517
+ path: "payload.data.tagIds",
15518
+ details: {
15519
+ textStyleId: payload.textStyleId,
15520
+ },
15521
+ });
15522
+ if (!result.valid) {
15523
+ return result;
15524
+ }
15525
+ }
15526
+
13389
15527
  for (const field of ["fontId", "colorId", "strokeColorId"]) {
13390
15528
  if (data[field] === undefined) {
13391
15529
  continue;
@@ -13412,6 +15550,23 @@ const COMMAND_DEFINITIONS = [
13412
15550
  );
13413
15551
  }
13414
15552
 
15553
+ if (currentItem.type === "textStyle") {
15554
+ {
15555
+ const result = validateTagIdsAgainstScope({
15556
+ state,
15557
+ tagIds: payload.data.tagIds,
15558
+ scopeKey: "textStyles",
15559
+ path: "payload.data.tagIds",
15560
+ details: {
15561
+ textStyleId: payload.textStyleId,
15562
+ },
15563
+ });
15564
+ if (!result.valid) {
15565
+ return result;
15566
+ }
15567
+ }
15568
+ }
15569
+
13415
15570
  for (const field of ["fontId", "colorId", "strokeColorId"]) {
13416
15571
  if (payload.data[field] === undefined) {
13417
15572
  continue;
@@ -13457,6 +15612,15 @@ const COMMAND_DEFINITIONS = [
13457
15612
  item.fileId = payload.data.fileId;
13458
15613
  }
13459
15614
 
15615
+ assignOptionalTagIds({
15616
+ target: item,
15617
+ tagIds: payload.data.tagIds,
15618
+ });
15619
+ assignOptionalCharacterSpriteGroups({
15620
+ target: item,
15621
+ spriteGroups: payload.data.spriteGroups,
15622
+ });
15623
+
13460
15624
  item.sprites =
13461
15625
  payload.data.sprites === undefined
13462
15626
  ? { items: {}, tree: [] }
@@ -13464,6 +15628,11 @@ const COMMAND_DEFINITIONS = [
13464
15628
 
13465
15629
  return item;
13466
15630
  },
15631
+ updateItem: ({ currentItem, payload }) =>
15632
+ applyCharacterUpdate({
15633
+ currentItem,
15634
+ data: payload.data,
15635
+ }),
13467
15636
  validateCreateState: ({ state, payload }) => {
13468
15637
  if (payload.data.type !== "character") {
13469
15638
  return;
@@ -13483,6 +15652,36 @@ const COMMAND_DEFINITIONS = [
13483
15652
  }
13484
15653
  }
13485
15654
 
15655
+ {
15656
+ const result = validateTagIdsAgainstScope({
15657
+ state,
15658
+ tagIds: payload.data.tagIds,
15659
+ scopeKey: "characters",
15660
+ path: "payload.data.tagIds",
15661
+ details: {
15662
+ characterId: payload.characterId,
15663
+ },
15664
+ });
15665
+ if (!result.valid) {
15666
+ return result;
15667
+ }
15668
+ }
15669
+
15670
+ {
15671
+ const result = validateCharacterSpriteGroupsAgainstScope({
15672
+ state,
15673
+ spriteGroups: payload.data.spriteGroups,
15674
+ scopeKey: `${CHARACTER_SPRITE_TAG_SCOPE_PREFIX}${payload.characterId}`,
15675
+ path: "payload.data.spriteGroups",
15676
+ details: {
15677
+ characterId: payload.characterId,
15678
+ },
15679
+ });
15680
+ if (!result.valid) {
15681
+ return result;
15682
+ }
15683
+ }
15684
+
13486
15685
  for (const [spriteId, sprite] of Object.entries(
13487
15686
  payload.data.sprites?.items || {},
13488
15687
  )) {
@@ -13490,6 +15689,16 @@ const COMMAND_DEFINITIONS = [
13490
15689
  continue;
13491
15690
  }
13492
15691
 
15692
+ if (sprite.tagIds !== undefined) {
15693
+ return invalidPrecondition(
15694
+ "payload.data.sprites.items.*.tagIds are not supported during character.create",
15695
+ {
15696
+ characterId: payload.characterId,
15697
+ spriteId,
15698
+ },
15699
+ );
15700
+ }
15701
+
13493
15702
  const result = validateFileReference({
13494
15703
  state,
13495
15704
  fileId: sprite.fileId,
@@ -13547,6 +15756,40 @@ const COMMAND_DEFINITIONS = [
13547
15756
  if (!result.valid) {
13548
15757
  return result;
13549
15758
  }
15759
+
15760
+ {
15761
+ const tagResult = validateTagIdsAgainstScope({
15762
+ state,
15763
+ tagIds: payload.data.tagIds,
15764
+ scopeKey: "characters",
15765
+ path: "payload.data.tagIds",
15766
+ details: {
15767
+ characterId: payload.characterId,
15768
+ },
15769
+ });
15770
+ if (!tagResult.valid) {
15771
+ return tagResult;
15772
+ }
15773
+ }
15774
+
15775
+ return validateCharacterSpriteGroupsAgainstScope({
15776
+ state,
15777
+ spriteGroups: payload.data.spriteGroups,
15778
+ scopeKey: `${CHARACTER_SPRITE_TAG_SCOPE_PREFIX}${payload.characterId}`,
15779
+ path: "payload.data.spriteGroups",
15780
+ details: {
15781
+ characterId: payload.characterId,
15782
+ },
15783
+ });
15784
+ }
15785
+ },
15786
+ afterDelete: ({ state, deletedItemsById }) => {
15787
+ for (const [characterId, item] of deletedItemsById.entries()) {
15788
+ if (item?.type !== "character") {
15789
+ continue;
15790
+ }
15791
+
15792
+ delete state.tags[`${CHARACTER_SPRITE_TAG_SCOPE_PREFIX}${characterId}`];
13550
15793
  }
13551
15794
  },
13552
15795
  }),
@@ -13568,6 +15811,12 @@ const COMMAND_DEFINITIONS = [
13568
15811
  : {}),
13569
15812
  ...(payload.data.type === "layout"
13570
15813
  ? {
15814
+ ...(Array.isArray(payload.data.tagIds) &&
15815
+ payload.data.tagIds.length > 0
15816
+ ? {
15817
+ tagIds: structuredClone(payload.data.tagIds),
15818
+ }
15819
+ : {}),
13571
15820
  layoutType: payload.data.layoutType,
13572
15821
  isFragment: payload.data.isFragment,
13573
15822
  ...(payload.data.thumbnailFileId !== undefined
@@ -13584,11 +15833,31 @@ const COMMAND_DEFINITIONS = [
13584
15833
  }
13585
15834
  : {}),
13586
15835
  }),
15836
+ updateItem: ({ currentItem, payload }) =>
15837
+ applyTagIdsUpdate({
15838
+ currentItem,
15839
+ data: payload.data,
15840
+ }),
13587
15841
  validateCreateState: ({ state, payload }) => {
13588
15842
  if (payload.data.type !== "layout") {
13589
15843
  return;
13590
15844
  }
13591
15845
 
15846
+ {
15847
+ const result = validateTagIdsAgainstScope({
15848
+ state,
15849
+ tagIds: payload.data.tagIds,
15850
+ scopeKey: "layouts",
15851
+ path: "payload.data.tagIds",
15852
+ details: {
15853
+ layoutId: payload.layoutId,
15854
+ },
15855
+ });
15856
+ if (!result.valid) {
15857
+ return result;
15858
+ }
15859
+ }
15860
+
13592
15861
  return validateReferencedFilesInData({
13593
15862
  state,
13594
15863
  data: payload.data,
@@ -13611,6 +15880,21 @@ const COMMAND_DEFINITIONS = [
13611
15880
  }
13612
15881
 
13613
15882
  if (currentItem.type === "layout") {
15883
+ {
15884
+ const result = validateTagIdsAgainstScope({
15885
+ state,
15886
+ tagIds: payload.data.tagIds,
15887
+ scopeKey: "layouts",
15888
+ path: "payload.data.tagIds",
15889
+ details: {
15890
+ layoutId: payload.layoutId,
15891
+ },
15892
+ });
15893
+ if (!result.valid) {
15894
+ return result;
15895
+ }
15896
+ }
15897
+
13614
15898
  return validateReferencedFilesInData({
13615
15899
  state,
13616
15900
  data: payload.data,
@@ -13621,49 +15905,50 @@ const COMMAND_DEFINITIONS = [
13621
15905
  });
13622
15906
  }
13623
15907
  },
13624
- }),
13625
- ...createFolderedCollectionCommandDefinitions({
13626
- familyName: "control",
13627
- collectionKey: "controls",
13628
- idField: "controlId",
13629
- itemLabel: "control item",
13630
- createDataValidator: validateControlCreateData,
13631
- updateDataValidator: validateControlUpdateData,
13632
- createItem: ({ payload }) => ({
13633
- id: payload.controlId,
13634
- type: payload.data.type,
13635
- name: payload.data.name,
13636
- ...(payload.data.description !== undefined
13637
- ? {
13638
- description: payload.data.description,
13639
- }
13640
- : {}),
13641
- ...(payload.data.type === "control"
13642
- ? {
13643
- ...(payload.data.thumbnailFileId !== undefined
13644
- ? {
13645
- thumbnailFileId: payload.data.thumbnailFileId,
13646
- }
13647
- : {}),
13648
- ...(payload.data.preview !== undefined
13649
- ? {
13650
- preview: structuredClone(payload.data.preview),
13651
- }
13652
- : {}),
13653
- elements: structuredClone(payload.data.elements),
13654
- ...(payload.data.keyboard !== undefined
13655
- ? {
13656
- keyboard: structuredClone(payload.data.keyboard),
13657
- }
13658
- : {}),
13659
- }
13660
- : {}),
13661
- }),
15908
+ }),
15909
+ ...createFolderedCollectionCommandDefinitions({
15910
+ familyName: "control",
15911
+ collectionKey: "controls",
15912
+ idField: "controlId",
15913
+ itemLabel: "control item",
15914
+ createDataValidator: validateControlCreateData,
15915
+ updateDataValidator: validateControlUpdateData,
15916
+ createItem: ({ payload }) => {
15917
+ const data = structuredClone(payload.data);
15918
+ if (!Array.isArray(data.tagIds) || data.tagIds.length === 0) {
15919
+ delete data.tagIds;
15920
+ }
15921
+
15922
+ return {
15923
+ id: payload.controlId,
15924
+ ...data,
15925
+ };
15926
+ },
15927
+ updateItem: ({ currentItem, payload }) =>
15928
+ applyTagIdsUpdate({
15929
+ currentItem,
15930
+ data: payload.data,
15931
+ }),
13662
15932
  validateCreateState: ({ state, payload }) => {
13663
15933
  if (payload.data.type !== "control") {
13664
15934
  return;
13665
15935
  }
13666
15936
 
15937
+ {
15938
+ const result = validateTagIdsAgainstScope({
15939
+ state,
15940
+ tagIds: payload.data.tagIds,
15941
+ scopeKey: "controls",
15942
+ path: "payload.data.tagIds",
15943
+ details: {
15944
+ controlId: payload.controlId,
15945
+ },
15946
+ });
15947
+ if (!result.valid) {
15948
+ return result;
15949
+ }
15950
+ }
15951
+
13667
15952
  return validateReferencedFilesInData({
13668
15953
  state,
13669
15954
  data: payload.data,
@@ -13686,6 +15971,21 @@ const COMMAND_DEFINITIONS = [
13686
15971
  }
13687
15972
 
13688
15973
  if (currentItem.type === "control") {
15974
+ {
15975
+ const result = validateTagIdsAgainstScope({
15976
+ state,
15977
+ tagIds: payload.data.tagIds,
15978
+ scopeKey: "controls",
15979
+ path: "payload.data.tagIds",
15980
+ details: {
15981
+ controlId: payload.controlId,
15982
+ },
15983
+ });
15984
+ if (!result.valid) {
15985
+ return result;
15986
+ }
15987
+ }
15988
+
13689
15989
  return validateReferencedFilesInData({
13690
15990
  state,
13691
15991
  data: payload.data,
@@ -13817,6 +16117,17 @@ const COMMAND_DEFINITIONS = [
13817
16117
  if (!result.valid) {
13818
16118
  return result;
13819
16119
  }
16120
+
16121
+ return validateTagIdsAgainstScope({
16122
+ state,
16123
+ tagIds: payload.data.tagIds,
16124
+ scopeKey: `${CHARACTER_SPRITE_TAG_SCOPE_PREFIX}${payload.characterId}`,
16125
+ path: "payload.data.tagIds",
16126
+ details: {
16127
+ characterId: payload.characterId,
16128
+ spriteId: payload.spriteId,
16129
+ },
16130
+ });
13820
16131
  }
13821
16132
  },
13822
16133
  reduce: ({ state, payload }) => {
@@ -13825,10 +16136,18 @@ const COMMAND_DEFINITIONS = [
13825
16136
  characterId: payload.characterId,
13826
16137
  });
13827
16138
 
13828
- collection.items[payload.spriteId] = {
16139
+ const nextSprite = {
13829
16140
  id: payload.spriteId,
13830
16141
  ...structuredClone(payload.data),
13831
16142
  };
16143
+ if (
16144
+ payload.data.tagIds !== undefined &&
16145
+ payload.data.tagIds.length === 0
16146
+ ) {
16147
+ delete nextSprite.tagIds;
16148
+ }
16149
+
16150
+ collection.items[payload.spriteId] = nextSprite;
13832
16151
 
13833
16152
  insertTreeNode({
13834
16153
  tree: collection.tree,
@@ -13922,6 +16241,17 @@ const COMMAND_DEFINITIONS = [
13922
16241
  if (!result.valid) {
13923
16242
  return result;
13924
16243
  }
16244
+
16245
+ return validateTagIdsAgainstScope({
16246
+ state,
16247
+ tagIds: payload.data.tagIds,
16248
+ scopeKey: `${CHARACTER_SPRITE_TAG_SCOPE_PREFIX}${payload.characterId}`,
16249
+ path: "payload.data.tagIds",
16250
+ details: {
16251
+ characterId: payload.characterId,
16252
+ spriteId: payload.spriteId,
16253
+ },
16254
+ });
13925
16255
  }
13926
16256
  },
13927
16257
  reduce: ({ state, payload }) => {
@@ -13931,10 +16261,10 @@ const COMMAND_DEFINITIONS = [
13931
16261
  });
13932
16262
  const currentItem = collection.items[payload.spriteId];
13933
16263
 
13934
- collection.items[payload.spriteId] = {
13935
- ...structuredClone(currentItem),
13936
- ...structuredClone(payload.data),
13937
- };
16264
+ collection.items[payload.spriteId] = applyTagIdsUpdate({
16265
+ currentItem,
16266
+ data: payload.data,
16267
+ });
13938
16268
 
13939
16269
  return state;
13940
16270
  },
@@ -14166,6 +16496,297 @@ const COMMAND_DEFINITIONS = [
14166
16496
  return state;
14167
16497
  },
14168
16498
  },
16499
+ {
16500
+ type: "tag.create",
16501
+ validatePayload: ({ payload }) => {
16502
+ {
16503
+ const result = validateExactKeys({
16504
+ value: payload,
16505
+ expectedKeys: ["scopeKey", "tagId", "data"],
16506
+ path: "payload",
16507
+ errorFactory: createPayloadValidationError,
16508
+ });
16509
+ if (result?.valid === false) {
16510
+ return result;
16511
+ }
16512
+ }
16513
+
16514
+ {
16515
+ const result = validateTagScopeKey({
16516
+ scopeKey: payload.scopeKey,
16517
+ path: "payload.scopeKey",
16518
+ errorFactory: createPayloadValidationError,
16519
+ });
16520
+ if (result?.valid === false) {
16521
+ return result;
16522
+ }
16523
+ }
16524
+
16525
+ if (!isNonEmptyString(payload.tagId)) {
16526
+ return invalidPayload("payload.tagId must be a non-empty string");
16527
+ }
16528
+
16529
+ {
16530
+ const result = validateTagCreateData({
16531
+ data: payload.data,
16532
+ errorFactory: createPayloadValidationError,
16533
+ });
16534
+ if (result?.valid === false) {
16535
+ return result;
16536
+ }
16537
+ }
16538
+ },
16539
+ validateAgainstState: ({ state, payload }) => {
16540
+ {
16541
+ const result = validateTagScopeAgainstState({
16542
+ state,
16543
+ scopeKey: payload.scopeKey,
16544
+ path: "payload.scopeKey",
16545
+ });
16546
+ if (!result.valid) {
16547
+ return result;
16548
+ }
16549
+ }
16550
+
16551
+ const collection = getTagScopeCollection({
16552
+ state,
16553
+ scopeKey: payload.scopeKey,
16554
+ });
16555
+ if (isPlainObject(collection?.items?.[payload.tagId])) {
16556
+ return invalidPrecondition("payload.tagId must not already exist");
16557
+ }
16558
+
16559
+ return validateUniqueTagNameInScope({
16560
+ collection,
16561
+ name: payload.data.name,
16562
+ path: "payload.data.name",
16563
+ });
16564
+ },
16565
+ reduce: ({ state, payload }) => {
16566
+ const collection = ensureTagScopeCollection({
16567
+ state,
16568
+ scopeKey: payload.scopeKey,
16569
+ });
16570
+ const nextTag = {
16571
+ id: payload.tagId,
16572
+ type: "tag",
16573
+ name: payload.data.name,
16574
+ };
16575
+
16576
+ if (payload.data.color !== undefined) {
16577
+ nextTag.color = payload.data.color;
16578
+ }
16579
+
16580
+ collection.items[payload.tagId] = nextTag;
16581
+ collection.tree.push({
16582
+ id: payload.tagId,
16583
+ });
16584
+
16585
+ return state;
16586
+ },
16587
+ },
16588
+ {
16589
+ type: "tag.update",
16590
+ validatePayload: ({ payload }) => {
16591
+ {
16592
+ const result = validateExactKeys({
16593
+ value: payload,
16594
+ expectedKeys: ["scopeKey", "tagId", "data"],
16595
+ path: "payload",
16596
+ errorFactory: createPayloadValidationError,
16597
+ });
16598
+ if (result?.valid === false) {
16599
+ return result;
16600
+ }
16601
+ }
16602
+
16603
+ {
16604
+ const result = validateTagScopeKey({
16605
+ scopeKey: payload.scopeKey,
16606
+ path: "payload.scopeKey",
16607
+ errorFactory: createPayloadValidationError,
16608
+ });
16609
+ if (result?.valid === false) {
16610
+ return result;
16611
+ }
16612
+ }
16613
+
16614
+ if (!isNonEmptyString(payload.tagId)) {
16615
+ return invalidPayload("payload.tagId must be a non-empty string");
16616
+ }
16617
+
16618
+ {
16619
+ const result = validateTagUpdateData({
16620
+ data: payload.data,
16621
+ errorFactory: createPayloadValidationError,
16622
+ });
16623
+ if (result?.valid === false) {
16624
+ return result;
16625
+ }
16626
+ }
16627
+ },
16628
+ validateAgainstState: ({ state, payload }) => {
16629
+ {
16630
+ const result = validateTagScopeAgainstState({
16631
+ state,
16632
+ scopeKey: payload.scopeKey,
16633
+ path: "payload.scopeKey",
16634
+ });
16635
+ if (!result.valid) {
16636
+ return result;
16637
+ }
16638
+ }
16639
+
16640
+ const collection = getTagScopeCollection({
16641
+ state,
16642
+ scopeKey: payload.scopeKey,
16643
+ });
16644
+ const currentTag = collection?.items?.[payload.tagId];
16645
+ if (!isPlainObject(currentTag) || currentTag.type !== "tag") {
16646
+ return invalidPrecondition(
16647
+ "payload.tagId must reference an existing tag in payload.scopeKey",
16648
+ );
16649
+ }
16650
+
16651
+ if (payload.data.name === undefined) {
16652
+ return VALID_RESULT;
16653
+ }
16654
+
16655
+ return validateUniqueTagNameInScope({
16656
+ collection,
16657
+ name: payload.data.name,
16658
+ path: "payload.data.name",
16659
+ excludeTagId: payload.tagId,
16660
+ });
16661
+ },
16662
+ reduce: ({ state, payload }) => {
16663
+ const collection = ensureTagScopeCollection({
16664
+ state,
16665
+ scopeKey: payload.scopeKey,
16666
+ });
16667
+ const currentTag = collection.items[payload.tagId];
16668
+ const nextTag = {
16669
+ ...structuredClone(currentTag),
16670
+ };
16671
+
16672
+ if (payload.data.name !== undefined) {
16673
+ nextTag.name = payload.data.name;
16674
+ }
16675
+
16676
+ if (payload.data.color === null) {
16677
+ delete nextTag.color;
16678
+ } else if (payload.data.color !== undefined) {
16679
+ nextTag.color = payload.data.color;
16680
+ }
16681
+
16682
+ collection.items[payload.tagId] = nextTag;
16683
+ return state;
16684
+ },
16685
+ },
16686
+ {
16687
+ type: "tag.delete",
16688
+ validatePayload: ({ payload }) => {
16689
+ {
16690
+ const result = validateExactKeys({
16691
+ value: payload,
16692
+ expectedKeys: ["scopeKey", "tagIds"],
16693
+ path: "payload",
16694
+ errorFactory: createPayloadValidationError,
16695
+ });
16696
+ if (result?.valid === false) {
16697
+ return result;
16698
+ }
16699
+ }
16700
+
16701
+ {
16702
+ const result = validateTagScopeKey({
16703
+ scopeKey: payload.scopeKey,
16704
+ path: "payload.scopeKey",
16705
+ errorFactory: createPayloadValidationError,
16706
+ });
16707
+ if (result?.valid === false) {
16708
+ return result;
16709
+ }
16710
+ }
16711
+
16712
+ {
16713
+ const result = validateRequiredUniqueIdArray({
16714
+ value: payload.tagIds,
16715
+ path: "payload.tagIds",
16716
+ errorFactory: createPayloadValidationError,
16717
+ });
16718
+ if (result?.valid === false) {
16719
+ return result;
16720
+ }
16721
+ }
16722
+ },
16723
+ validateAgainstState: ({ state, payload }) => {
16724
+ {
16725
+ const result = validateTagScopeAgainstState({
16726
+ state,
16727
+ scopeKey: payload.scopeKey,
16728
+ path: "payload.scopeKey",
16729
+ });
16730
+ if (!result.valid) {
16731
+ return result;
16732
+ }
16733
+ }
16734
+
16735
+ const collection = getTagScopeCollection({
16736
+ state,
16737
+ scopeKey: payload.scopeKey,
16738
+ });
16739
+ for (const tagId of payload.tagIds) {
16740
+ const tag = collection?.items?.[tagId];
16741
+ if (!isPlainObject(tag) || tag.type !== "tag") {
16742
+ return invalidPrecondition(
16743
+ "payload.tagIds must reference existing tags in payload.scopeKey",
16744
+ {
16745
+ scopeKey: payload.scopeKey,
16746
+ tagId,
16747
+ },
16748
+ );
16749
+ }
16750
+ }
16751
+
16752
+ return VALID_RESULT;
16753
+ },
16754
+ reduce: ({ state, payload }) => {
16755
+ const collection = ensureTagScopeCollection({
16756
+ state,
16757
+ scopeKey: payload.scopeKey,
16758
+ });
16759
+ const deletedTagIds = new Set();
16760
+
16761
+ for (const tagId of payload.tagIds) {
16762
+ const removedNode = removeTreeNode({
16763
+ nodes: collection.tree,
16764
+ nodeId: tagId,
16765
+ });
16766
+ if (!removedNode) {
16767
+ continue;
16768
+ }
16769
+
16770
+ for (const deletedTagId of collectTreeDescendantIds({
16771
+ node: removedNode,
16772
+ })) {
16773
+ deletedTagIds.add(deletedTagId);
16774
+ }
16775
+ }
16776
+
16777
+ for (const tagId of deletedTagIds) {
16778
+ delete collection.items[tagId];
16779
+ }
16780
+
16781
+ stripDeletedTagIdsFromScopeItems({
16782
+ state,
16783
+ scopeKey: payload.scopeKey,
16784
+ deletedTagIds,
16785
+ });
16786
+
16787
+ return state;
16788
+ },
16789
+ },
14169
16790
  {
14170
16791
  type: "layout.element.create",
14171
16792
  validatePayload: ({ payload }) => {
@@ -15338,11 +17959,7 @@ export const validateAgainstState = ({ state, command }) => {
15338
17959
  export const processCommand = ({ state, command }) => {
15339
17960
  return captureValidation(() => {
15340
17961
  const normalizedState = normalizeStateCollections(state);
15341
- const shouldMaterializeNormalizedState =
15342
- typeof command?.type === "string" &&
15343
- (command.type.startsWith("control.") ||
15344
- command.type.startsWith("spritesheet.") ||
15345
- command.type.startsWith("particle."));
17962
+ const shouldMaterializeNormalizedState = normalizedState !== state;
15346
17963
 
15347
17964
  const stateResult = validateState({ state: normalizedState });
15348
17965
  if (!stateResult.valid) {