@bravostudioai/react 0.1.28 → 0.1.30

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.
@@ -6,7 +6,20 @@ import {
6
6
  SelectInputInfo,
7
7
  ActionButtonInfo,
8
8
  } from "./types";
9
+ import { qualifyPropNames } from "./propQualification";
9
10
 
11
+ /**
12
+ * Sanitizes a component name into a valid camelCase prop name
13
+ *
14
+ * Removes special characters, handles spaces, and converts to camelCase.
15
+ *
16
+ * @param name - Raw component name from Figma/design tool
17
+ * @returns Valid JavaScript property name
18
+ *
19
+ * @example
20
+ * sanitizePropName("My Component!") // "myComponent"
21
+ * sanitizePropName("user-name") // "userName"
22
+ */
10
23
  export function sanitizePropName(name: string): string {
11
24
  // Convert to camelCase and remove invalid characters
12
25
  const cleaned = name
@@ -350,219 +363,8 @@ export function findSlidersAndDataBindings(pageData: any): SliderInfo[] {
350
363
  } as ComponentInfo & { _parentPath: string[] });
351
364
  }
352
365
 
353
- // Detect duplicates and qualify them with minimal distinguishing paths
354
- // First pass: collect all base prop names and group duplicates
355
- const propNameGroups = new Map<
356
- string,
357
- Array<ComponentInfo & { _parentPath: string[] }>
358
- >();
359
- components.forEach((comp) => {
360
- const compWithPath = comp as ComponentInfo & {
361
- _parentPath: string[];
362
- };
363
- const baseName = comp.propName;
364
- if (!propNameGroups.has(baseName)) {
365
- propNameGroups.set(baseName, []);
366
- }
367
- propNameGroups.get(baseName)!.push(compWithPath);
368
- });
369
-
370
- // Second pass: for each group with duplicates, find minimal distinguishing paths
371
- // and ensure all qualified names are unique
372
- propNameGroups.forEach((group, _baseName) => {
373
- if (group.length === 1) {
374
- // No duplicates, keep the simple name
375
- return;
376
- }
377
-
378
- // First, find minimal distinguishing paths for all components
379
- group.forEach((comp) => {
380
- const otherPaths = group
381
- .filter((c) => c.id !== comp.id)
382
- .map((c) => c._parentPath || []);
383
-
384
- const minimalPath = findMinimalDistinguishingPath(
385
- comp._parentPath || [],
386
- otherPaths
387
- );
388
-
389
- // Use the minimal distinguishing path to qualify the name
390
- comp.propName = generateQualifiedPropName(
391
- comp.name || "item",
392
- minimalPath
393
- );
394
- });
395
-
396
- // Check if qualified names are still duplicates and expand paths if needed
397
- let hasDuplicates = true;
398
- let iteration = 0;
399
- const maxIterations = 10; // Safety limit
400
-
401
- while (hasDuplicates && iteration < maxIterations) {
402
- iteration++;
403
- const qualifiedNameGroups = new Map<
404
- string,
405
- Array<ComponentInfo & { _parentPath: string[] }>
406
- >();
407
- group.forEach((comp) => {
408
- if (!qualifiedNameGroups.has(comp.propName)) {
409
- qualifiedNameGroups.set(comp.propName, []);
410
- }
411
- qualifiedNameGroups.get(comp.propName)!.push(comp);
412
- });
413
-
414
- hasDuplicates = false;
415
- // For each group of still-duplicated qualified names, expand their paths
416
- qualifiedNameGroups.forEach((dupGroup, _qualifiedName) => {
417
- if (dupGroup.length > 1) {
418
- hasDuplicates = true;
419
- // Expand the distinguishing path for each duplicate
420
- dupGroup.forEach((comp) => {
421
- // Find a longer distinguishing path by comparing with others in the duplicate group
422
- const fullPath = comp._parentPath || [];
423
- const otherFullPaths = dupGroup
424
- .filter((c) => c.id !== comp.id)
425
- .map((c) => c._parentPath || []);
426
-
427
- // Find where this path diverges from others in the duplicate group
428
- let commonPrefixLength = 0;
429
- const maxCommonLength = Math.min(
430
- fullPath.length,
431
- ...otherFullPaths.map((p) => p.length)
432
- );
433
-
434
- for (let i = 0; i < maxCommonLength; i++) {
435
- const thisPart = fullPath[i];
436
- const allMatch = otherFullPaths.every((otherPath) => {
437
- return otherPath[i] === thisPart;
438
- });
439
- if (allMatch) {
440
- commonPrefixLength++;
441
- } else {
442
- break;
443
- }
444
- }
445
-
446
- // Use progressively more of the distinguishing suffix until unique
447
- const distinguishingSuffix =
448
- fullPath.slice(commonPrefixLength);
449
-
450
- // Try expanding the distinguishing suffix until we find a unique name
451
- let foundUnique = false;
452
- for (
453
- let suffixLength = 1;
454
- suffixLength <= distinguishingSuffix.length;
455
- suffixLength++
456
- ) {
457
- const expandedPath = distinguishingSuffix.slice(
458
- 0,
459
- suffixLength
460
- );
461
- const testQualifiedName = generateQualifiedPropName(
462
- comp.name || "item",
463
- expandedPath
464
- );
465
-
466
- // Check if this qualified name is unique among ALL components (not just duplicates)
467
- const isUnique = components.every((otherComp) => {
468
- if (otherComp.id === comp.id) return true;
469
- // If other component is in the same duplicate group, compare expanded paths
470
- if (dupGroup.some((c) => c.id === otherComp.id)) {
471
- const otherFullPath =
472
- (
473
- otherComp as ComponentInfo & {
474
- _parentPath: string[];
475
- }
476
- )._parentPath || [];
477
- const otherCommonPrefixLength = Math.min(
478
- commonPrefixLength,
479
- otherFullPath.length
480
- );
481
- const otherDistinguishingSuffix = otherFullPath.slice(
482
- otherCommonPrefixLength
483
- );
484
- const otherExpandedPath =
485
- otherDistinguishingSuffix.slice(0, suffixLength);
486
- const otherQualifiedName = generateQualifiedPropName(
487
- otherComp.name || "item",
488
- otherExpandedPath
489
- );
490
- return testQualifiedName !== otherQualifiedName;
491
- }
492
- // For components outside the duplicate group, just check the final prop name
493
- return testQualifiedName !== otherComp.propName;
494
- });
495
-
496
- if (isUnique) {
497
- comp.propName = testQualifiedName;
498
- foundUnique = true;
499
- break;
500
- }
501
- }
502
-
503
- // If we couldn't find a unique name with the distinguishing suffix,
504
- // use the distinguishing suffix we found (it's the minimal we can do)
505
- // We'll handle truly identical paths with numeric suffixes in the final pass
506
- if (!foundUnique) {
507
- // Use the distinguishing suffix - it's the minimal distinguishing part
508
- // Even if it's not globally unique yet, it's better than the full path
509
- comp.propName = generateQualifiedPropName(
510
- comp.name || "item",
511
- distinguishingSuffix.length > 0
512
- ? distinguishingSuffix
513
- : []
514
- );
515
- }
516
- });
517
- }
518
- });
519
- }
520
-
521
- // Final check: if there are still duplicates after using full paths,
522
- // and they have identical paths, use numeric suffixes as last resort
523
- const finalQualifiedNameGroups = new Map<
524
- string,
525
- Array<ComponentInfo & { _parentPath: string[] }>
526
- >();
527
- group.forEach((comp) => {
528
- if (!finalQualifiedNameGroups.has(comp.propName)) {
529
- finalQualifiedNameGroups.set(comp.propName, []);
530
- }
531
- finalQualifiedNameGroups.get(comp.propName)!.push(comp);
532
- });
533
-
534
- finalQualifiedNameGroups.forEach(
535
- (finalDupGroup, finalQualifiedName) => {
536
- if (finalDupGroup.length > 1) {
537
- // Check if all duplicates have identical paths
538
- const allPathsIdentical = finalDupGroup.every((comp) => {
539
- const thisPath = comp._parentPath || [];
540
- return finalDupGroup.every((otherComp) => {
541
- if (otherComp.id === comp.id) return true;
542
- const otherPath = otherComp._parentPath || [];
543
- return arraysEqual(thisPath, otherPath);
544
- });
545
- });
546
-
547
- // Only use numeric suffixes if paths are truly identical
548
- if (allPathsIdentical) {
549
- let index = 0;
550
- finalDupGroup.forEach((comp) => {
551
- if (index > 0) {
552
- comp.propName = `${finalQualifiedName}${index + 1}`;
553
- }
554
- index++;
555
- });
556
- }
557
- }
558
- }
559
- );
560
-
561
- // Remove the temporary _parentPath property
562
- group.forEach((comp) => {
563
- delete (comp as any)._parentPath;
564
- });
565
- });
366
+ // Qualify duplicate prop names using minimal distinguishing paths
367
+ qualifyPropNames(components);
566
368
 
567
369
  // If we have an image component, remove color components with similar names
568
370
  // (they're usually placeholders/backgrounds)
@@ -623,6 +425,15 @@ export function findSlidersAndDataBindings(pageData: any): SliderInfo[] {
623
425
  return sliders;
624
426
  }
625
427
 
428
+ /**
429
+ * Finds standalone data-bound components (not in sliders)
430
+ *
431
+ * Locates components tagged with encore:data (but not encore:data:array)
432
+ * for standalone data binding at the page level.
433
+ *
434
+ * @param pageData - Page definition from Encore service
435
+ * @returns Array of component metadata
436
+ */
626
437
  export function findStandaloneComponents(pageData: any): ComponentInfo[] {
627
438
  const components: ComponentInfo[] = [];
628
439
  const sliderIds = new Set<string>();
@@ -722,209 +533,21 @@ export function findStandaloneComponents(pageData: any): ComponentInfo[] {
722
533
  body.forEach((node: any) => traverse(node));
723
534
  }
724
535
 
725
- // Detect duplicates and qualify them with minimal distinguishing paths
726
- // First pass: collect all base prop names and group duplicates
727
- const propNameGroups = new Map<
728
- string,
729
- Array<ComponentInfo & { _parentPath: string[] }>
730
- >();
731
- components.forEach((comp) => {
732
- const compWithPath = comp as ComponentInfo & { _parentPath: string[] };
733
- const baseName = comp.propName;
734
- if (!propNameGroups.has(baseName)) {
735
- propNameGroups.set(baseName, []);
736
- }
737
- propNameGroups.get(baseName)!.push(compWithPath);
738
- });
739
-
740
- // Second pass: for each group with duplicates, find minimal distinguishing paths
741
- // and ensure all qualified names are unique
742
- propNameGroups.forEach((group, _baseName) => {
743
- if (group.length === 1) {
744
- // No duplicates, keep the simple name
745
- return;
746
- }
747
-
748
- // First, find minimal distinguishing paths for all components
749
- group.forEach((comp) => {
750
- const otherPaths = group
751
- .filter((c) => c.id !== comp.id)
752
- .map((c) => c._parentPath || []);
753
-
754
- const minimalPath = findMinimalDistinguishingPath(
755
- comp._parentPath || [],
756
- otherPaths
757
- );
758
-
759
- // Use the minimal distinguishing path to qualify the name
760
- comp.propName = generateQualifiedPropName(
761
- comp.name || "item",
762
- minimalPath
763
- );
764
- });
765
-
766
- // Check if qualified names are still duplicates and expand paths if needed
767
- let hasDuplicates = true;
768
- let iteration = 0;
769
- const maxIterations = 10; // Safety limit
770
-
771
- while (hasDuplicates && iteration < maxIterations) {
772
- iteration++;
773
- const qualifiedNameGroups = new Map<
774
- string,
775
- Array<ComponentInfo & { _parentPath: string[] }>
776
- >();
777
- group.forEach((comp) => {
778
- if (!qualifiedNameGroups.has(comp.propName)) {
779
- qualifiedNameGroups.set(comp.propName, []);
780
- }
781
- qualifiedNameGroups.get(comp.propName)!.push(comp);
782
- });
783
-
784
- hasDuplicates = false;
785
- // For each group of still-duplicated qualified names, expand their paths
786
- qualifiedNameGroups.forEach((dupGroup, _qualifiedName) => {
787
- if (dupGroup.length > 1) {
788
- hasDuplicates = true;
789
- // Expand the distinguishing path for each duplicate
790
- dupGroup.forEach((comp) => {
791
- // Find a longer distinguishing path by comparing with others in the duplicate group
792
- const fullPath = comp._parentPath || [];
793
- const otherFullPaths = dupGroup
794
- .filter((c) => c.id !== comp.id)
795
- .map((c) => c._parentPath || []);
796
-
797
- // Find where this path diverges from others in the duplicate group
798
- let commonPrefixLength = 0;
799
- const maxCommonLength = Math.min(
800
- fullPath.length,
801
- ...otherFullPaths.map((p) => p.length)
802
- );
803
-
804
- for (let i = 0; i < maxCommonLength; i++) {
805
- const thisPart = fullPath[i];
806
- const allMatch = otherFullPaths.every((otherPath) => {
807
- return otherPath[i] === thisPart;
808
- });
809
- if (allMatch) {
810
- commonPrefixLength++;
811
- } else {
812
- break;
813
- }
814
- }
815
-
816
- // Use progressively more of the distinguishing suffix until unique
817
- const distinguishingSuffix = fullPath.slice(commonPrefixLength);
818
-
819
- // Try expanding the distinguishing suffix until we find a unique name
820
- let foundUnique = false;
821
- for (
822
- let suffixLength = 1;
823
- suffixLength <= distinguishingSuffix.length;
824
- suffixLength++
825
- ) {
826
- const expandedPath = distinguishingSuffix.slice(0, suffixLength);
827
- const testQualifiedName = generateQualifiedPropName(
828
- comp.name || "item",
829
- expandedPath
830
- );
831
-
832
- // Check if this qualified name is unique among ALL components (not just duplicates)
833
- const isUnique = components.every((otherComp) => {
834
- if (otherComp.id === comp.id) return true;
835
- // If other component is in the same duplicate group, compare expanded paths
836
- if (dupGroup.some((c) => c.id === otherComp.id)) {
837
- const otherFullPath =
838
- (otherComp as ComponentInfo & { _parentPath: string[] })
839
- ._parentPath || [];
840
- const otherCommonPrefixLength = Math.min(
841
- commonPrefixLength,
842
- otherFullPath.length
843
- );
844
- const otherDistinguishingSuffix = otherFullPath.slice(
845
- otherCommonPrefixLength
846
- );
847
- const otherExpandedPath = otherDistinguishingSuffix.slice(
848
- 0,
849
- suffixLength
850
- );
851
- const otherQualifiedName = generateQualifiedPropName(
852
- otherComp.name || "item",
853
- otherExpandedPath
854
- );
855
- return testQualifiedName !== otherQualifiedName;
856
- }
857
- // For components outside the duplicate group, just check the final prop name
858
- return testQualifiedName !== otherComp.propName;
859
- });
860
-
861
- if (isUnique) {
862
- comp.propName = testQualifiedName;
863
- foundUnique = true;
864
- break;
865
- }
866
- }
867
-
868
- // If we couldn't find a unique name with the distinguishing suffix,
869
- // use the full path to ensure uniqueness (even if it makes names longer)
870
- if (!foundUnique) {
871
- comp.propName = generateQualifiedPropName(
872
- comp.name || "item",
873
- fullPath
874
- );
875
- }
876
- });
877
- }
878
- });
879
- }
880
-
881
- // Final check: if there are still duplicates after using full paths,
882
- // and they have identical paths, use numeric suffixes as last resort
883
- const finalQualifiedNameGroups = new Map<
884
- string,
885
- Array<ComponentInfo & { _parentPath: string[] }>
886
- >();
887
- group.forEach((comp) => {
888
- if (!finalQualifiedNameGroups.has(comp.propName)) {
889
- finalQualifiedNameGroups.set(comp.propName, []);
890
- }
891
- finalQualifiedNameGroups.get(comp.propName)!.push(comp);
892
- });
893
-
894
- finalQualifiedNameGroups.forEach((finalDupGroup, finalQualifiedName) => {
895
- if (finalDupGroup.length > 1) {
896
- // Check if all duplicates have identical paths
897
- const allPathsIdentical = finalDupGroup.every((comp) => {
898
- const thisPath = comp._parentPath || [];
899
- return finalDupGroup.every((otherComp) => {
900
- if (otherComp.id === comp.id) return true;
901
- const otherPath = otherComp._parentPath || [];
902
- return arraysEqual(thisPath, otherPath);
903
- });
904
- });
905
-
906
- // Only use numeric suffixes if paths are truly identical
907
- if (allPathsIdentical) {
908
- let index = 0;
909
- finalDupGroup.forEach((comp) => {
910
- if (index > 0) {
911
- comp.propName = `${finalQualifiedName}${index + 1}`;
912
- }
913
- index++;
914
- });
915
- }
916
- }
917
- });
918
-
919
- // Remove the temporary _parentPath property
920
- group.forEach((comp) => {
921
- delete (comp as any)._parentPath;
922
- });
923
- });
536
+ // Qualify duplicate prop names using minimal distinguishing paths
537
+ qualifyPropNames(components);
924
538
 
925
539
  return components;
926
540
  }
927
541
 
542
+ /**
543
+ * Finds input groups (radio button-like components) in the page
544
+ *
545
+ * Locates stateful sets tagged with encore:input-group for coordinated
546
+ * selection behavior.
547
+ *
548
+ * @param pageData - Page definition from Encore service
549
+ * @returns Array of input group metadata
550
+ */
928
551
  export function findInputGroups(pageData: any): InputGroupInfo[] {
929
552
  const groupsMap = new Map<string, InputGroupInfo>();
930
553
 
@@ -985,6 +608,15 @@ export function findInputGroups(pageData: any): InputGroupInfo[] {
985
608
  return Array.from(groupsMap.values());
986
609
  }
987
610
 
611
+ /**
612
+ * Finds forms and their input fields in the page
613
+ *
614
+ * Locates containers tagged with encore:form and extracts all input
615
+ * components within them for form submission handling.
616
+ *
617
+ * @param pageData - Page definition from Encore service
618
+ * @returns Array of form metadata with input fields
619
+ */
988
620
  export function findForms(pageData: any): FormInfo[] {
989
621
  const forms: FormInfo[] = [];
990
622
 
@@ -1215,48 +847,7 @@ export function findStandaloneSelectInputs(
1215
847
  * Qualifies select input prop names to ensure uniqueness.
1216
848
  */
1217
849
  function qualifySelectInputs(selectInputs: SelectInputInfo[]): void {
1218
- const propNameGroups = new Map<
1219
- string,
1220
- Array<SelectInputInfo & { _parentPath: string[] }>
1221
- >();
1222
-
1223
- selectInputs.forEach((input) => {
1224
- const inputWithPath = input as SelectInputInfo & { _parentPath: string[] };
1225
- const baseName = input.propName;
1226
- if (!propNameGroups.has(baseName)) {
1227
- propNameGroups.set(baseName, []);
1228
- }
1229
- propNameGroups.get(baseName)!.push(inputWithPath);
1230
- });
1231
-
1232
- propNameGroups.forEach((group, _baseName) => {
1233
- if (group.length === 1) {
1234
- delete (group[0] as any)._parentPath;
1235
- return;
1236
- }
1237
-
1238
- // Find minimal distinguishing paths for duplicates
1239
- group.forEach((input) => {
1240
- const otherPaths = group
1241
- .filter((i) => i.id !== input.id)
1242
- .map((i) => i._parentPath || []);
1243
-
1244
- const minimalPath = findMinimalDistinguishingPath(
1245
- input._parentPath || [],
1246
- otherPaths
1247
- );
1248
-
1249
- input.propName = generateQualifiedPropName(
1250
- input.name || "input",
1251
- minimalPath
1252
- );
1253
- });
1254
-
1255
- // Clean up
1256
- group.forEach((input) => {
1257
- delete (input as any)._parentPath;
1258
- });
1259
- });
850
+ qualifyPropNames(selectInputs);
1260
851
  }
1261
852
 
1262
853
  /**
@@ -1349,50 +940,7 @@ export function findActionButtons(pageData: any): ActionButtonInfo[] {
1349
940
  * Qualifies action button prop names to ensure uniqueness.
1350
941
  */
1351
942
  function qualifyActionButtons(buttons: ActionButtonInfo[]): void {
1352
- const propNameGroups = new Map<
1353
- string,
1354
- Array<ActionButtonInfo & { _parentPath: string[] }>
1355
- >();
1356
-
1357
- buttons.forEach((button) => {
1358
- const buttonWithPath = button as ActionButtonInfo & {
1359
- _parentPath: string[];
1360
- };
1361
- const baseName = button.propName;
1362
- if (!propNameGroups.has(baseName)) {
1363
- propNameGroups.set(baseName, []);
1364
- }
1365
- propNameGroups.get(baseName)!.push(buttonWithPath);
1366
- });
1367
-
1368
- propNameGroups.forEach((group, _baseName) => {
1369
- if (group.length === 1) {
1370
- delete (group[0] as any)._parentPath;
1371
- return;
1372
- }
1373
-
1374
- // Find minimal distinguishing paths for duplicates
1375
- group.forEach((button) => {
1376
- const otherPaths = group
1377
- .filter((b) => b.id !== button.id)
1378
- .map((b) => b._parentPath || []);
1379
-
1380
- const minimalPath = findMinimalDistinguishingPath(
1381
- button._parentPath || [],
1382
- otherPaths
1383
- );
1384
-
1385
- button.propName = generateQualifiedPropName(
1386
- button.name || "button",
1387
- minimalPath
1388
- );
1389
- });
1390
-
1391
- // Clean up
1392
- group.forEach((button) => {
1393
- delete (button as any)._parentPath;
1394
- });
1395
- });
943
+ qualifyPropNames(buttons);
1396
944
  }
1397
945
 
1398
946
  /**
@@ -1401,214 +949,6 @@ function qualifyActionButtons(buttons: ActionButtonInfo[]): void {
1401
949
  */
1402
950
  export function qualifyFormInputs(forms: FormInfo[]): void {
1403
951
  forms.forEach((form) => {
1404
- const inputs = form.inputs;
1405
-
1406
- // Group inputs by base prop name
1407
- const propNameGroups = new Map<
1408
- string,
1409
- Array<FormInfo["inputs"][0] & { _parentPath: string[] }>
1410
- >();
1411
-
1412
- inputs.forEach((input) => {
1413
- const inputWithPath = input as FormInfo["inputs"][0] & {
1414
- _parentPath: string[];
1415
- };
1416
- const baseName = input.propName;
1417
- if (!propNameGroups.has(baseName)) {
1418
- propNameGroups.set(baseName, []);
1419
- }
1420
- propNameGroups.get(baseName)!.push(inputWithPath);
1421
- });
1422
-
1423
- // For each group with duplicates, find minimal distinguishing paths
1424
- propNameGroups.forEach((group, _baseName) => {
1425
- if (group.length === 1) {
1426
- // No duplicates, keep the simple name
1427
- // Remove the temporary _parentPath property
1428
- delete (group[0] as any)._parentPath;
1429
- return;
1430
- }
1431
-
1432
- // Find minimal distinguishing paths for all inputs
1433
- group.forEach((input) => {
1434
- const otherPaths = group
1435
- .filter((i) => i.id !== input.id)
1436
- .map((i) => i._parentPath || []);
1437
-
1438
- const minimalPath = findMinimalDistinguishingPath(
1439
- input._parentPath || [],
1440
- otherPaths
1441
- );
1442
-
1443
- // Use the minimal distinguishing path to qualify the name
1444
- input.propName = generateQualifiedPropName(
1445
- input.name || "input",
1446
- minimalPath
1447
- );
1448
- });
1449
-
1450
- // Check if qualified names are still duplicates and expand paths if needed
1451
- let hasDuplicates = true;
1452
- let iteration = 0;
1453
- const maxIterations = 10; // Safety limit
1454
-
1455
- while (hasDuplicates && iteration < maxIterations) {
1456
- iteration++;
1457
- const qualifiedNameGroups = new Map<
1458
- string,
1459
- Array<FormInfo["inputs"][0] & { _parentPath: string[] }>
1460
- >();
1461
- group.forEach((input) => {
1462
- if (!qualifiedNameGroups.has(input.propName)) {
1463
- qualifiedNameGroups.set(input.propName, []);
1464
- }
1465
- qualifiedNameGroups.get(input.propName)!.push(input);
1466
- });
1467
-
1468
- hasDuplicates = false;
1469
- // For each group of still-duplicated qualified names, expand their paths
1470
- qualifiedNameGroups.forEach((dupGroup, _qualifiedName) => {
1471
- if (dupGroup.length > 1) {
1472
- hasDuplicates = true;
1473
- // Expand the distinguishing path for each duplicate
1474
- dupGroup.forEach((input) => {
1475
- const fullPath = input._parentPath || [];
1476
- const otherFullPaths = dupGroup
1477
- .filter((i) => i.id !== input.id)
1478
- .map((i) => i._parentPath || []);
1479
-
1480
- // Find where this path diverges from others in the duplicate group
1481
- let commonPrefixLength = 0;
1482
- const maxCommonLength = Math.min(
1483
- fullPath.length,
1484
- ...otherFullPaths.map((p) => p.length)
1485
- );
1486
-
1487
- for (let i = 0; i < maxCommonLength; i++) {
1488
- const thisPart = fullPath[i];
1489
- const allMatch = otherFullPaths.every((otherPath) => {
1490
- return otherPath[i] === thisPart;
1491
- });
1492
- if (allMatch) {
1493
- commonPrefixLength++;
1494
- } else {
1495
- break;
1496
- }
1497
- }
1498
-
1499
- // Use progressively more of the distinguishing suffix until unique
1500
- const distinguishingSuffix = fullPath.slice(commonPrefixLength);
1501
-
1502
- // Try expanding the distinguishing suffix until we find a unique name
1503
- let foundUnique = false;
1504
- for (
1505
- let suffixLength = 1;
1506
- suffixLength <= distinguishingSuffix.length;
1507
- suffixLength++
1508
- ) {
1509
- const expandedPath = distinguishingSuffix.slice(
1510
- 0,
1511
- suffixLength
1512
- );
1513
- const testQualifiedName = generateQualifiedPropName(
1514
- input.name || "input",
1515
- expandedPath
1516
- );
1517
-
1518
- // Check if this qualified name is unique among ALL inputs in this form
1519
- const isUnique = inputs.every((otherInput) => {
1520
- if (otherInput.id === input.id) return true;
1521
- // If other input is in the same duplicate group, compare expanded paths
1522
- if (dupGroup.some((i) => i.id === otherInput.id)) {
1523
- const otherFullPath =
1524
- (
1525
- otherInput as FormInfo["inputs"][0] & {
1526
- _parentPath: string[];
1527
- }
1528
- )._parentPath || [];
1529
- const otherCommonPrefixLength = Math.min(
1530
- commonPrefixLength,
1531
- otherFullPath.length
1532
- );
1533
- const otherDistinguishingSuffix = otherFullPath.slice(
1534
- otherCommonPrefixLength
1535
- );
1536
- const otherExpandedPath = otherDistinguishingSuffix.slice(
1537
- 0,
1538
- suffixLength
1539
- );
1540
- const otherQualifiedName = generateQualifiedPropName(
1541
- otherInput.name || "input",
1542
- otherExpandedPath
1543
- );
1544
- return testQualifiedName !== otherQualifiedName;
1545
- }
1546
- // For inputs outside the duplicate group, just check the final prop name
1547
- return testQualifiedName !== otherInput.propName;
1548
- });
1549
-
1550
- if (isUnique) {
1551
- input.propName = testQualifiedName;
1552
- foundUnique = true;
1553
- break;
1554
- }
1555
- }
1556
-
1557
- // If we couldn't find a unique name with the distinguishing suffix,
1558
- // use the distinguishing suffix we found (it's the minimal we can do)
1559
- if (!foundUnique) {
1560
- input.propName = generateQualifiedPropName(
1561
- input.name || "input",
1562
- distinguishingSuffix.length > 0 ? distinguishingSuffix : []
1563
- );
1564
- }
1565
- });
1566
- }
1567
- });
1568
- }
1569
-
1570
- // Final check: if there are still duplicates after using full paths,
1571
- // and they have identical paths, use numeric suffixes as last resort
1572
- const finalQualifiedNameGroups = new Map<
1573
- string,
1574
- Array<FormInfo["inputs"][0] & { _parentPath: string[] }>
1575
- >();
1576
- group.forEach((input) => {
1577
- if (!finalQualifiedNameGroups.has(input.propName)) {
1578
- finalQualifiedNameGroups.set(input.propName, []);
1579
- }
1580
- finalQualifiedNameGroups.get(input.propName)!.push(input);
1581
- });
1582
-
1583
- finalQualifiedNameGroups.forEach((finalDupGroup, finalQualifiedName) => {
1584
- if (finalDupGroup.length > 1) {
1585
- // Check if all duplicates have identical paths
1586
- const allPathsIdentical = finalDupGroup.every((input) => {
1587
- const thisPath = input._parentPath || [];
1588
- return finalDupGroup.every((otherInput) => {
1589
- if (otherInput.id === input.id) return true;
1590
- const otherPath = otherInput._parentPath || [];
1591
- return arraysEqual(thisPath, otherPath);
1592
- });
1593
- });
1594
-
1595
- // Only use numeric suffixes if paths are truly identical
1596
- if (allPathsIdentical) {
1597
- let index = 0;
1598
- finalDupGroup.forEach((input) => {
1599
- if (index > 0) {
1600
- input.propName = `${finalQualifiedName}${index + 1}`;
1601
- }
1602
- index++;
1603
- });
1604
- }
1605
- }
1606
- });
1607
-
1608
- // Remove the temporary _parentPath property
1609
- group.forEach((input) => {
1610
- delete (input as any)._parentPath;
1611
- });
1612
- });
952
+ qualifyPropNames(form.inputs);
1613
953
  });
1614
954
  }