ngx-form-designerr 0.0.10 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/ngx-form-designer.mjs +1302 -400
- package/fesm2022/ngx-form-designer.mjs.map +1 -1
- package/lib/form-core/models.d.ts +10 -1
- package/lib/form-designer/designer-state.service.d.ts +21 -0
- package/lib/form-renderer/json-form-renderer.component.d.ts +6 -3
- package/lib/form-renderer/layout-node.component.d.ts +4 -1
- package/lib/widgets/field-widgets/repeatable-group/repeatable-group-widget.component.d.ts +69 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { Injectable, InjectionToken, NgModule, signal, computed, EventEmitter,
|
|
3
|
-
import { BehaviorSubject, Subject, merge, of, filter, map, debounceTime as debounceTime$1, firstValueFrom } from 'rxjs';
|
|
2
|
+
import { Injectable, InjectionToken, NgModule, inject, signal, computed, EventEmitter, DestroyRef, Injector, ViewContainerRef, Input, ViewChild, Output, Inject, ChangeDetectionStrategy, Component, effect, ElementRef, NgZone, input, output, HostListener, ContentChildren, untracked, ChangeDetectorRef } from '@angular/core';
|
|
3
|
+
import { BehaviorSubject, Subject, merge, of, filter, map, debounceTime as debounceTime$1, skip, firstValueFrom } from 'rxjs';
|
|
4
4
|
import { v4 } from 'uuid';
|
|
5
5
|
import * as i1 from '@angular/common';
|
|
6
6
|
import { CommonModule, DOCUMENT } from '@angular/common';
|
|
@@ -241,7 +241,7 @@ class FormEngine {
|
|
|
241
241
|
return;
|
|
242
242
|
}
|
|
243
243
|
const isRequired = this.isFieldRequired(field.id);
|
|
244
|
-
const isEmpty = value === null || value === undefined || value === '';
|
|
244
|
+
const isEmpty = value === null || value === undefined || value === '' || (Array.isArray(value) && value.length === 0);
|
|
245
245
|
if (isRequired && isEmpty) {
|
|
246
246
|
fieldErrors.push('This field is required.');
|
|
247
247
|
}
|
|
@@ -402,6 +402,12 @@ function computeAccessFlags(schema) {
|
|
|
402
402
|
// hierarchyAccess: true if field participates in hierarchy (either direction)
|
|
403
403
|
field.dataConfig.hierarchyAccess = hierarchyAccess;
|
|
404
404
|
}
|
|
405
|
+
for (const field of schema.fields) {
|
|
406
|
+
const itemSchema = field.repeatable?.itemSchema;
|
|
407
|
+
if (!itemSchema)
|
|
408
|
+
continue;
|
|
409
|
+
computeAccessFlags(itemSchema);
|
|
410
|
+
}
|
|
405
411
|
}
|
|
406
412
|
|
|
407
413
|
/**
|
|
@@ -478,6 +484,10 @@ function checkSchemaForApiOnlySources(schema) {
|
|
|
478
484
|
return issues;
|
|
479
485
|
for (const field of schema.fields) {
|
|
480
486
|
checkField(field, issues);
|
|
487
|
+
const itemSchema = field.repeatable?.itemSchema;
|
|
488
|
+
if (itemSchema) {
|
|
489
|
+
issues.push(...checkSchemaForApiOnlySources(itemSchema));
|
|
490
|
+
}
|
|
481
491
|
}
|
|
482
492
|
return issues;
|
|
483
493
|
}
|
|
@@ -551,6 +561,9 @@ function stripEmbeddedSourceData(schema) {
|
|
|
551
561
|
delete field.staticOptions;
|
|
552
562
|
}
|
|
553
563
|
}
|
|
564
|
+
if (field.repeatable?.itemSchema) {
|
|
565
|
+
field.repeatable.itemSchema = stripEmbeddedSourceData(field.repeatable.itemSchema);
|
|
566
|
+
}
|
|
554
567
|
}
|
|
555
568
|
}
|
|
556
569
|
return clone;
|
|
@@ -640,25 +653,46 @@ function mergeAndNormalize(base, override) {
|
|
|
640
653
|
|
|
641
654
|
const WIDGET_EDITOR_CONTEXT = new InjectionToken('WIDGET_EDITOR_CONTEXT');
|
|
642
655
|
|
|
643
|
-
|
|
656
|
+
const SCOPED_NODE_SEPARATOR = '::scope::';
|
|
657
|
+
function composeScopedNodeId(scopePath, nodeId) {
|
|
658
|
+
if (scopePath.length === 0)
|
|
659
|
+
return nodeId;
|
|
660
|
+
return `${scopePath.join('/')}${SCOPED_NODE_SEPARATOR}${nodeId}`;
|
|
661
|
+
}
|
|
662
|
+
function buildLayoutIndex(schema) {
|
|
644
663
|
const index = {};
|
|
645
|
-
const walk = (node, parentId, path, position) => {
|
|
646
|
-
const
|
|
647
|
-
|
|
664
|
+
const walk = (scopeSchema, node, parentId, path, position, scopePath) => {
|
|
665
|
+
const scopedNodeId = composeScopedNodeId(scopePath, node.id);
|
|
666
|
+
const nextPath = [...path, scopedNodeId];
|
|
667
|
+
index[scopedNodeId] = {
|
|
648
668
|
node,
|
|
649
669
|
parentId,
|
|
650
670
|
path: nextPath,
|
|
651
|
-
index: position
|
|
671
|
+
index: position,
|
|
672
|
+
scopePath: [...scopePath],
|
|
673
|
+
rawNodeId: node.id
|
|
652
674
|
};
|
|
653
675
|
if ('children' in node) {
|
|
654
676
|
const children = node.children;
|
|
655
|
-
children.forEach((child, idx) => walk(child,
|
|
677
|
+
children.forEach((child, idx) => walk(scopeSchema, child, scopedNodeId, nextPath, idx, scopePath));
|
|
678
|
+
}
|
|
679
|
+
if (node.type === 'widget') {
|
|
680
|
+
const widgetNode = node;
|
|
681
|
+
if (!widgetNode.refId)
|
|
682
|
+
return;
|
|
683
|
+
const field = scopeSchema.fields.find(candidate => candidate.id === widgetNode.refId);
|
|
684
|
+
const itemSchema = field?.repeatable?.itemSchema;
|
|
685
|
+
if (!field || !itemSchema?.layout || !Array.isArray(itemSchema.fields))
|
|
686
|
+
return;
|
|
687
|
+
const nestedScopePath = [...scopePath, field.id];
|
|
688
|
+
walk(itemSchema, itemSchema.layout, scopedNodeId, nextPath, 0, nestedScopePath);
|
|
656
689
|
}
|
|
657
690
|
};
|
|
658
|
-
walk(
|
|
691
|
+
walk(schema, schema.layout, null, [], 0, []);
|
|
659
692
|
return index;
|
|
660
693
|
}
|
|
661
694
|
class DesignerStateService {
|
|
695
|
+
widgetDefs = (inject(WIDGET_DEFINITIONS, { optional: true }) ?? []).flat();
|
|
662
696
|
activeFlavor = signal('form');
|
|
663
697
|
schema = signal({
|
|
664
698
|
id: v4(),
|
|
@@ -711,7 +745,7 @@ class DesignerStateService {
|
|
|
711
745
|
constructor() {
|
|
712
746
|
this.recordHistory(this.schema());
|
|
713
747
|
}
|
|
714
|
-
layoutIndex = computed(() => buildLayoutIndex(this.schema()
|
|
748
|
+
layoutIndex = computed(() => buildLayoutIndex(this.schema()));
|
|
715
749
|
selectedNode = computed(() => {
|
|
716
750
|
const id = this.selectedNodeId();
|
|
717
751
|
if (!id)
|
|
@@ -720,12 +754,16 @@ class DesignerStateService {
|
|
|
720
754
|
return entry?.node ?? null;
|
|
721
755
|
});
|
|
722
756
|
selectedField = computed(() => {
|
|
723
|
-
const
|
|
724
|
-
if (
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
757
|
+
const selectedId = this.selectedNodeId();
|
|
758
|
+
if (!selectedId)
|
|
759
|
+
return null;
|
|
760
|
+
const entry = this.layoutIndex()[selectedId];
|
|
761
|
+
if (!entry || entry.node.type !== 'widget')
|
|
762
|
+
return null;
|
|
763
|
+
const refId = entry.node.refId;
|
|
764
|
+
if (!refId)
|
|
765
|
+
return null;
|
|
766
|
+
return this.findFieldByIdInScope(this.schema(), entry.scopePath, refId);
|
|
729
767
|
});
|
|
730
768
|
updateSchema(schema) {
|
|
731
769
|
this.setSchema(schema);
|
|
@@ -788,6 +826,28 @@ class DesignerStateService {
|
|
|
788
826
|
isNodeSelected(nodeId) {
|
|
789
827
|
return this.selectedNodeIds().includes(nodeId);
|
|
790
828
|
}
|
|
829
|
+
composeScopedNodeId(scopePath, nodeId) {
|
|
830
|
+
return composeScopedNodeId(scopePath, nodeId);
|
|
831
|
+
}
|
|
832
|
+
getSelectedScopeFields() {
|
|
833
|
+
const selectedId = this.selectedNodeId();
|
|
834
|
+
if (!selectedId)
|
|
835
|
+
return this.schema().fields;
|
|
836
|
+
const entry = this.layoutIndex()[selectedId];
|
|
837
|
+
if (!entry)
|
|
838
|
+
return this.schema().fields;
|
|
839
|
+
const scopeSchema = this.resolveSchemaAtScope(this.schema(), entry.scopePath);
|
|
840
|
+
return scopeSchema?.fields ?? this.schema().fields;
|
|
841
|
+
}
|
|
842
|
+
isSelectionInScope(scopePath) {
|
|
843
|
+
const selectedId = this.selectedNodeId();
|
|
844
|
+
if (!selectedId)
|
|
845
|
+
return false;
|
|
846
|
+
const entry = this.layoutIndex()[selectedId];
|
|
847
|
+
if (!entry)
|
|
848
|
+
return false;
|
|
849
|
+
return this.isScopePrefix(scopePath, entry.scopePath);
|
|
850
|
+
}
|
|
791
851
|
openContextMenu(position) {
|
|
792
852
|
this.contextMenu.set(position);
|
|
793
853
|
}
|
|
@@ -819,11 +879,14 @@ class DesignerStateService {
|
|
|
819
879
|
const nodesToCopy = [];
|
|
820
880
|
const fieldsToCopy = [];
|
|
821
881
|
const index = this.layoutIndex();
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
882
|
+
const primarySelectedId = this.selectedNodeId() ?? selectedIds[0];
|
|
883
|
+
const primaryEntry = primarySelectedId ? index[primarySelectedId] : undefined;
|
|
884
|
+
if (!primaryEntry)
|
|
885
|
+
return;
|
|
886
|
+
const scopePath = primaryEntry.scopePath;
|
|
887
|
+
const scopeSchema = this.resolveSchemaAtScope(current, scopePath);
|
|
888
|
+
if (!scopeSchema)
|
|
889
|
+
return;
|
|
827
890
|
const isDescendantOfSelected = (id) => {
|
|
828
891
|
let parentId = index[id]?.parentId;
|
|
829
892
|
while (parentId) {
|
|
@@ -833,17 +896,27 @@ class DesignerStateService {
|
|
|
833
896
|
}
|
|
834
897
|
return false;
|
|
835
898
|
};
|
|
836
|
-
const topLevelSelected = selectedIds.filter(id =>
|
|
899
|
+
const topLevelSelected = selectedIds.filter(id => {
|
|
900
|
+
const entry = index[id];
|
|
901
|
+
if (!entry)
|
|
902
|
+
return false;
|
|
903
|
+
if (!this.sameScope(entry.scopePath, scopePath))
|
|
904
|
+
return false;
|
|
905
|
+
return !isDescendantOfSelected(id);
|
|
906
|
+
});
|
|
837
907
|
for (const id of topLevelSelected) {
|
|
838
908
|
const entry = index[id];
|
|
839
909
|
if (entry && entry.node) {
|
|
840
910
|
nodesToCopy.push(entry.node);
|
|
841
|
-
|
|
842
|
-
this.collectFields(entry.node, current.fields, fieldsToCopy);
|
|
911
|
+
this.collectFields(entry.node, scopeSchema.fields, fieldsToCopy);
|
|
843
912
|
}
|
|
844
913
|
}
|
|
845
914
|
if (nodesToCopy.length > 0) {
|
|
846
|
-
this.clipboard.set({
|
|
915
|
+
this.clipboard.set({
|
|
916
|
+
nodes: JSON.parse(JSON.stringify(nodesToCopy)),
|
|
917
|
+
fields: JSON.parse(JSON.stringify(fieldsToCopy)),
|
|
918
|
+
scopePath: [...scopePath]
|
|
919
|
+
});
|
|
847
920
|
}
|
|
848
921
|
}
|
|
849
922
|
cut() {
|
|
@@ -859,83 +932,83 @@ class DesignerStateService {
|
|
|
859
932
|
if (!data || data.nodes.length === 0)
|
|
860
933
|
return;
|
|
861
934
|
const current = this.schema();
|
|
862
|
-
const
|
|
863
|
-
const
|
|
864
|
-
// Determine Drop Target
|
|
865
|
-
const selectedId = this.selectedNodeId(); // Primary selection for paste target
|
|
935
|
+
const nextSchema = this.cloneValue(current);
|
|
936
|
+
const selectedId = this.selectedNodeId();
|
|
866
937
|
const index = this.layoutIndex();
|
|
938
|
+
const selectedEntry = selectedId ? index[selectedId] : undefined;
|
|
939
|
+
const targetScopePath = selectedEntry
|
|
940
|
+
? this.resolveInsertionScopePath(selectedEntry)
|
|
941
|
+
: [...(data.scopePath ?? [])];
|
|
942
|
+
const targetSchema = this.resolveSchemaAtScope(nextSchema, targetScopePath);
|
|
943
|
+
if (!targetSchema)
|
|
944
|
+
return;
|
|
867
945
|
let targetParentId = null;
|
|
868
946
|
let targetIndex = -1;
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
insertMode = 'append';
|
|
947
|
+
if (selectedEntry && this.sameScope(selectedEntry.scopePath, targetScopePath)) {
|
|
948
|
+
if ((selectedEntry.node.type === 'row' || selectedEntry.node.type === 'col') && 'children' in selectedEntry.node) {
|
|
949
|
+
targetParentId = selectedEntry.rawNodeId;
|
|
950
|
+
targetIndex = selectedEntry.node.children.length;
|
|
951
|
+
}
|
|
952
|
+
else {
|
|
953
|
+
if (selectedEntry.parentId) {
|
|
954
|
+
const parentEntry = index[selectedEntry.parentId];
|
|
955
|
+
targetParentId = parentEntry?.rawNodeId ?? targetSchema.layout.id;
|
|
879
956
|
}
|
|
880
957
|
else {
|
|
881
|
-
|
|
882
|
-
targetParentId = entry.parentId;
|
|
883
|
-
targetIndex = entry.index + 1;
|
|
884
|
-
insertMode = 'insert';
|
|
958
|
+
targetParentId = targetSchema.layout.id;
|
|
885
959
|
}
|
|
960
|
+
targetIndex = selectedEntry.index + 1;
|
|
886
961
|
}
|
|
887
962
|
}
|
|
888
|
-
// Fallback: If no selection or invalid target, paste to
|
|
963
|
+
// Fallback: If no selection or invalid target, paste to scope root.
|
|
889
964
|
if (!targetParentId) {
|
|
890
|
-
targetParentId =
|
|
891
|
-
|
|
892
|
-
|
|
965
|
+
targetParentId = targetSchema.layout.id;
|
|
966
|
+
if ('children' in targetSchema.layout) {
|
|
967
|
+
targetIndex = targetSchema.layout.children.length;
|
|
968
|
+
}
|
|
969
|
+
else {
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
893
972
|
}
|
|
894
973
|
const newSelectedIds = [];
|
|
974
|
+
const clipboardFields = new Map(data.fields.map(field => [field.id, field]));
|
|
895
975
|
// Helper to clone node with new IDs
|
|
896
976
|
const processNode = (node) => {
|
|
897
|
-
const
|
|
898
|
-
newNode
|
|
977
|
+
const oldNode = JSON.parse(JSON.stringify(node));
|
|
978
|
+
const newNode = { ...oldNode, id: v4() };
|
|
899
979
|
newSelectedIds.push(newNode.id);
|
|
900
980
|
if (newNode.type === 'widget' && newNode.refId) {
|
|
901
|
-
|
|
902
|
-
const originalField =
|
|
903
|
-
// Or map via old refId?
|
|
904
|
-
// Wait, processNode receives 'node' which is from clipboard (already has old IDs).
|
|
905
|
-
// We need to map oldFieldId -> newFieldId.
|
|
981
|
+
const oldRefId = oldNode.refId;
|
|
982
|
+
const originalField = clipboardFields.get(oldRefId);
|
|
906
983
|
if (originalField) {
|
|
907
|
-
// Create new field
|
|
908
984
|
const newFieldId = v4();
|
|
909
985
|
const newField = {
|
|
910
986
|
...originalField,
|
|
911
987
|
id: newFieldId,
|
|
912
988
|
name: `${originalField.name}_${Date.now()}_${Math.floor(Math.random() * 1000)}`
|
|
913
989
|
};
|
|
914
|
-
|
|
990
|
+
targetSchema.fields.push(newField);
|
|
915
991
|
newNode.refId = newFieldId;
|
|
916
992
|
}
|
|
917
993
|
}
|
|
918
994
|
if ('children' in newNode) {
|
|
919
|
-
newNode.children =
|
|
995
|
+
newNode.children =
|
|
996
|
+
newNode.children.map((child) => processNode(child));
|
|
920
997
|
}
|
|
921
998
|
return newNode;
|
|
922
999
|
};
|
|
923
1000
|
const clonedNodes = data.nodes.map(n => processNode(n));
|
|
924
1001
|
// Insert into Layout
|
|
925
1002
|
if (!targetParentId)
|
|
926
|
-
return;
|
|
927
|
-
const targetNode = this.findNode(
|
|
1003
|
+
return;
|
|
1004
|
+
const targetNode = this.findNode(targetSchema.layout, targetParentId);
|
|
928
1005
|
if (targetNode && 'children' in targetNode) {
|
|
929
1006
|
targetNode.children.splice(targetIndex, 0, ...clonedNodes);
|
|
930
|
-
this.setSchema(
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
// Select pasted items
|
|
936
|
-
this.selectedNodeIds.set(newSelectedIds);
|
|
937
|
-
if (newSelectedIds.length > 0) {
|
|
938
|
-
this.selectedNodeId.set(newSelectedIds[0]);
|
|
1007
|
+
this.setSchema(nextSchema);
|
|
1008
|
+
const scopedSelectedIds = newSelectedIds.map(id => this.composeScopedNodeId(targetScopePath, id));
|
|
1009
|
+
this.selectedNodeIds.set(scopedSelectedIds);
|
|
1010
|
+
if (scopedSelectedIds.length > 0) {
|
|
1011
|
+
this.selectedNodeId.set(scopedSelectedIds[0]);
|
|
939
1012
|
}
|
|
940
1013
|
}
|
|
941
1014
|
}
|
|
@@ -945,41 +1018,65 @@ class DesignerStateService {
|
|
|
945
1018
|
const idsToDelete = this.selectedNodeIds();
|
|
946
1019
|
if (idsToDelete.length === 0)
|
|
947
1020
|
return;
|
|
948
|
-
const
|
|
949
|
-
const
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
if ('children' in node) {
|
|
956
|
-
node.children.forEach((c) => collectFieldIds(c));
|
|
1021
|
+
const index = this.layoutIndex();
|
|
1022
|
+
const isDescendantOfSelected = (id) => {
|
|
1023
|
+
let parentId = index[id]?.parentId;
|
|
1024
|
+
while (parentId) {
|
|
1025
|
+
if (idsToDelete.includes(parentId))
|
|
1026
|
+
return true;
|
|
1027
|
+
parentId = index[parentId]?.parentId;
|
|
957
1028
|
}
|
|
1029
|
+
return false;
|
|
958
1030
|
};
|
|
959
|
-
const
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1031
|
+
const topLevelEntries = idsToDelete
|
|
1032
|
+
.filter(id => !isDescendantOfSelected(id))
|
|
1033
|
+
.map(id => index[id])
|
|
1034
|
+
.filter((entry) => !!entry);
|
|
1035
|
+
if (topLevelEntries.length === 0)
|
|
1036
|
+
return;
|
|
1037
|
+
const current = this.schema();
|
|
1038
|
+
const nextSchema = this.cloneValue(current);
|
|
1039
|
+
const entriesByScope = new Map();
|
|
1040
|
+
for (const entry of topLevelEntries) {
|
|
1041
|
+
const key = entry.scopePath.join('/');
|
|
1042
|
+
const scopeEntries = entriesByScope.get(key) ?? [];
|
|
1043
|
+
scopeEntries.push(entry);
|
|
1044
|
+
entriesByScope.set(key, scopeEntries);
|
|
1045
|
+
}
|
|
1046
|
+
for (const scopeEntries of entriesByScope.values()) {
|
|
1047
|
+
const scopePath = scopeEntries[0].scopePath;
|
|
1048
|
+
const scopeSchema = this.resolveSchemaAtScope(nextSchema, scopePath);
|
|
1049
|
+
if (!scopeSchema)
|
|
1050
|
+
continue;
|
|
1051
|
+
const deleteSet = new Set(scopeEntries.map(entry => entry.rawNodeId));
|
|
1052
|
+
const fieldsToRemove = new Set();
|
|
1053
|
+
const collectFieldIds = (node) => {
|
|
1054
|
+
if (node.type === 'widget' && node.refId) {
|
|
1055
|
+
fieldsToRemove.add(node.refId);
|
|
973
1056
|
}
|
|
974
|
-
|
|
975
|
-
|
|
1057
|
+
if ('children' in node) {
|
|
1058
|
+
node.children.forEach((child) => collectFieldIds(child));
|
|
976
1059
|
}
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1060
|
+
};
|
|
1061
|
+
const removeRecursive = (parent) => {
|
|
1062
|
+
if (!('children' in parent))
|
|
1063
|
+
return;
|
|
1064
|
+
const children = parent.children;
|
|
1065
|
+
for (let i = children.length - 1; i >= 0; i -= 1) {
|
|
1066
|
+
const child = children[i];
|
|
1067
|
+
if (deleteSet.has(child.id)) {
|
|
1068
|
+
collectFieldIds(child);
|
|
1069
|
+
children.splice(i, 1);
|
|
1070
|
+
}
|
|
1071
|
+
else {
|
|
1072
|
+
removeRecursive(child);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
removeRecursive(scopeSchema.layout);
|
|
1077
|
+
scopeSchema.fields = scopeSchema.fields.filter(field => !fieldsToRemove.has(field.id));
|
|
1078
|
+
}
|
|
1079
|
+
this.setSchema(nextSchema);
|
|
983
1080
|
this.selectNode(null);
|
|
984
1081
|
}
|
|
985
1082
|
// Helper to find parent of a node (simple traversal)
|
|
@@ -1063,20 +1160,21 @@ class DesignerStateService {
|
|
|
1063
1160
|
return [];
|
|
1064
1161
|
const path = [];
|
|
1065
1162
|
for (const id of entry.path) {
|
|
1066
|
-
const
|
|
1163
|
+
const indexedEntry = index[id];
|
|
1164
|
+
const node = indexedEntry?.node;
|
|
1067
1165
|
if (!node)
|
|
1068
1166
|
continue;
|
|
1069
|
-
path.push({ id, label: this.getNodeLabel(node), type: node.type });
|
|
1167
|
+
path.push({ id, label: this.getNodeLabel(node, indexedEntry.scopePath), type: node.type });
|
|
1070
1168
|
}
|
|
1071
1169
|
return path;
|
|
1072
1170
|
}
|
|
1073
|
-
getNodeLabel(node) {
|
|
1171
|
+
getNodeLabel(node, scopePath) {
|
|
1074
1172
|
if (node.type === 'row')
|
|
1075
1173
|
return 'Row';
|
|
1076
1174
|
if (node.type === 'col')
|
|
1077
1175
|
return 'Column';
|
|
1078
1176
|
if (node.type === 'widget') {
|
|
1079
|
-
const field = this.schema()
|
|
1177
|
+
const field = this.findFieldByIdInScope(this.schema(), scopePath, node.refId ?? '');
|
|
1080
1178
|
return field?.label || field?.type || 'Widget';
|
|
1081
1179
|
}
|
|
1082
1180
|
return 'Root';
|
|
@@ -1115,24 +1213,16 @@ class DesignerStateService {
|
|
|
1115
1213
|
return false;
|
|
1116
1214
|
}
|
|
1117
1215
|
handleDrop(event, targetColId) {
|
|
1216
|
+
const targetEntry = this.layoutIndex()[targetColId];
|
|
1217
|
+
if (!targetEntry || targetEntry.node.type !== 'col')
|
|
1218
|
+
return;
|
|
1118
1219
|
const current = this.schema();
|
|
1119
|
-
const
|
|
1120
|
-
const
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
if ('children' in root) {
|
|
1126
|
-
for (const child of root.children) {
|
|
1127
|
-
const res = findCol(child, id);
|
|
1128
|
-
if (res)
|
|
1129
|
-
return res;
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
return null;
|
|
1133
|
-
};
|
|
1134
|
-
const targetCol = findCol(newLayout, targetColId);
|
|
1135
|
-
if (!targetCol)
|
|
1220
|
+
const newSchema = this.cloneValue(current);
|
|
1221
|
+
const targetSchema = this.resolveSchemaAtScope(newSchema, targetEntry.scopePath);
|
|
1222
|
+
if (!targetSchema)
|
|
1223
|
+
return;
|
|
1224
|
+
const targetCol = this.findNode(targetSchema.layout, targetEntry.rawNodeId);
|
|
1225
|
+
if (!targetCol || targetCol.type !== 'col')
|
|
1136
1226
|
return;
|
|
1137
1227
|
if (event.previousContainer === event.container) {
|
|
1138
1228
|
// Reorder
|
|
@@ -1150,44 +1240,24 @@ class DesignerStateService {
|
|
|
1150
1240
|
console.warn('Drag data missing widget info.');
|
|
1151
1241
|
return;
|
|
1152
1242
|
}
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
type: data.widgetType,
|
|
1163
|
-
label: 'New Widget',
|
|
1164
|
-
};
|
|
1165
|
-
newFields.push(field);
|
|
1166
|
-
widgetNode = {
|
|
1167
|
-
id: v4(),
|
|
1168
|
-
type: 'widget',
|
|
1169
|
-
widgetKind: data.widgetKind,
|
|
1170
|
-
refId: fieldId
|
|
1171
|
-
};
|
|
1172
|
-
}
|
|
1173
|
-
else {
|
|
1174
|
-
// other widgets
|
|
1175
|
-
widgetNode = {
|
|
1176
|
-
id: v4(),
|
|
1177
|
-
type: 'widget',
|
|
1178
|
-
widgetKind: data.widgetKind,
|
|
1179
|
-
refId: v4()
|
|
1180
|
-
};
|
|
1181
|
-
}
|
|
1243
|
+
const widgetDef = this.resolveWidgetDefinition(data);
|
|
1244
|
+
const field = this.createFieldFromWidget(widgetDef, data.widgetType);
|
|
1245
|
+
targetSchema.fields.push(field);
|
|
1246
|
+
const widgetNode = {
|
|
1247
|
+
id: v4(),
|
|
1248
|
+
type: 'widget',
|
|
1249
|
+
widgetKind: widgetDef?.kind ?? data.widgetKind,
|
|
1250
|
+
refId: field.id
|
|
1251
|
+
};
|
|
1182
1252
|
targetCol.children.splice(event.currentIndex, 0, widgetNode);
|
|
1183
1253
|
}
|
|
1184
1254
|
else if (data && data.type === 'new-section' && data.section) {
|
|
1185
1255
|
const draft = data.section.build();
|
|
1186
1256
|
targetCol.children.splice(event.currentIndex, 0, draft.layout);
|
|
1187
|
-
|
|
1257
|
+
targetSchema.fields.push(...draft.fields);
|
|
1188
1258
|
}
|
|
1189
1259
|
else if (this.isLayoutNode(data)) {
|
|
1190
|
-
const movedNode = this.detachNode(
|
|
1260
|
+
const movedNode = this.detachNode(targetSchema.layout, data.id);
|
|
1191
1261
|
if (!movedNode)
|
|
1192
1262
|
return;
|
|
1193
1263
|
targetCol.children.splice(event.currentIndex, 0, movedNode);
|
|
@@ -1196,84 +1266,76 @@ class DesignerStateService {
|
|
|
1196
1266
|
console.warn('Unsupported drag data dropped into layout.');
|
|
1197
1267
|
}
|
|
1198
1268
|
}
|
|
1199
|
-
this.setSchema(
|
|
1200
|
-
...current,
|
|
1201
|
-
fields: newFields,
|
|
1202
|
-
layout: newLayout
|
|
1203
|
-
});
|
|
1269
|
+
this.setSchema(newSchema);
|
|
1204
1270
|
}
|
|
1205
1271
|
pushField(widgetDef) {
|
|
1206
1272
|
if (this.isReadOnly())
|
|
1207
1273
|
return;
|
|
1208
|
-
// Existing logic unchanged
|
|
1209
|
-
// Simplified Logic: Always append to the last column of the last row for now,
|
|
1210
|
-
// or check if there is a selected column.
|
|
1211
1274
|
const current = this.schema();
|
|
1212
|
-
const
|
|
1213
|
-
const
|
|
1214
|
-
const
|
|
1215
|
-
const
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
};
|
|
1222
|
-
newFields.push(field);
|
|
1275
|
+
const selectedId = this.selectedNodeId();
|
|
1276
|
+
const selectedEntry = selectedId ? this.layoutIndex()[selectedId] : undefined;
|
|
1277
|
+
const insertionScopePath = this.resolveInsertionScopePath(selectedEntry);
|
|
1278
|
+
const nextSchema = this.cloneValue(current);
|
|
1279
|
+
const targetSchema = this.resolveSchemaAtScope(nextSchema, insertionScopePath);
|
|
1280
|
+
if (!targetSchema)
|
|
1281
|
+
return;
|
|
1282
|
+
const field = this.createFieldFromWidget(widgetDef, widgetDef.type);
|
|
1283
|
+
targetSchema.fields.push(field);
|
|
1223
1284
|
const widgetNode = {
|
|
1224
1285
|
id: v4(),
|
|
1225
1286
|
type: 'widget',
|
|
1226
|
-
refId:
|
|
1227
|
-
widgetKind: widgetDef.kind
|
|
1287
|
+
refId: field.id,
|
|
1288
|
+
widgetKind: widgetDef.kind
|
|
1228
1289
|
};
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
// Or we could implement findParent(root, id)
|
|
1242
|
-
}
|
|
1290
|
+
const targetCol = this.resolveTargetColumnForFieldInsert(targetSchema, selectedEntry, insertionScopePath);
|
|
1291
|
+
if (!targetCol)
|
|
1292
|
+
return;
|
|
1293
|
+
targetCol.children.push(widgetNode);
|
|
1294
|
+
this.setSchema(nextSchema);
|
|
1295
|
+
this.selectNode(this.composeScopedNodeId(insertionScopePath, widgetNode.id));
|
|
1296
|
+
}
|
|
1297
|
+
resolveWidgetDefinition(data) {
|
|
1298
|
+
if (data.widgetId) {
|
|
1299
|
+
const byId = this.widgetDefs.find(widget => widget.id === data.widgetId);
|
|
1300
|
+
if (byId)
|
|
1301
|
+
return byId;
|
|
1243
1302
|
}
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
layout: newLayout
|
|
1267
|
-
});
|
|
1268
|
-
// Auto Select new field
|
|
1269
|
-
this.selectNode(widgetNode.id);
|
|
1303
|
+
return this.widgetDefs.find(widget => widget.kind === data.widgetKind && widget.type === data.widgetType);
|
|
1304
|
+
}
|
|
1305
|
+
createFieldFromWidget(widgetDef, fallbackType) {
|
|
1306
|
+
const baseConfig = widgetDef?.createConfig ? widgetDef.createConfig() : {};
|
|
1307
|
+
const normalizedName = typeof baseConfig.name === 'string' && baseConfig.name.trim().length > 0
|
|
1308
|
+
? baseConfig.name
|
|
1309
|
+
: `field_${Date.now()}`;
|
|
1310
|
+
const normalizedType = typeof baseConfig.type === 'string' && baseConfig.type.trim().length > 0
|
|
1311
|
+
? baseConfig.type
|
|
1312
|
+
: (widgetDef?.type || fallbackType || 'text');
|
|
1313
|
+
const normalizedLabel = typeof baseConfig.label === 'string' && baseConfig.label.trim().length > 0
|
|
1314
|
+
? baseConfig.label
|
|
1315
|
+
: (widgetDef?.label || 'New Widget');
|
|
1316
|
+
const field = {
|
|
1317
|
+
...baseConfig,
|
|
1318
|
+
id: v4(),
|
|
1319
|
+
name: normalizedName,
|
|
1320
|
+
type: normalizedType,
|
|
1321
|
+
label: normalizedLabel
|
|
1322
|
+
};
|
|
1323
|
+
if (widgetDef?.id) {
|
|
1324
|
+
field.widgetId = widgetDef.id;
|
|
1270
1325
|
}
|
|
1326
|
+
return field;
|
|
1271
1327
|
}
|
|
1272
1328
|
pushStructure(type) {
|
|
1273
1329
|
if (this.isReadOnly())
|
|
1274
1330
|
return;
|
|
1275
1331
|
const current = this.schema();
|
|
1276
|
-
const
|
|
1332
|
+
const selectedId = this.selectedNodeId();
|
|
1333
|
+
const selectedEntry = selectedId ? this.layoutIndex()[selectedId] : undefined;
|
|
1334
|
+
const insertionScopePath = this.resolveInsertionScopePath(selectedEntry);
|
|
1335
|
+
const newSchema = this.cloneValue(current);
|
|
1336
|
+
const targetSchema = this.resolveSchemaAtScope(newSchema, insertionScopePath);
|
|
1337
|
+
if (!targetSchema)
|
|
1338
|
+
return;
|
|
1277
1339
|
// Create a new Row
|
|
1278
1340
|
const newRow = {
|
|
1279
1341
|
id: v4(),
|
|
@@ -1288,8 +1350,9 @@ class DesignerStateService {
|
|
|
1288
1350
|
children: []
|
|
1289
1351
|
};
|
|
1290
1352
|
newRow.children.push(col1);
|
|
1353
|
+
let col2;
|
|
1291
1354
|
if (type === '2col') {
|
|
1292
|
-
|
|
1355
|
+
col2 = {
|
|
1293
1356
|
id: v4(),
|
|
1294
1357
|
type: 'col',
|
|
1295
1358
|
responsive: { xs: 6 },
|
|
@@ -1297,30 +1360,39 @@ class DesignerStateService {
|
|
|
1297
1360
|
};
|
|
1298
1361
|
newRow.children.push(col2);
|
|
1299
1362
|
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1363
|
+
const layoutRoot = targetSchema.layout;
|
|
1364
|
+
if (layoutRoot.type === 'col') {
|
|
1365
|
+
layoutRoot.children.push(newRow);
|
|
1366
|
+
}
|
|
1367
|
+
else if (layoutRoot.type === 'row') {
|
|
1368
|
+
layoutRoot.children.push(col1);
|
|
1369
|
+
if (col2) {
|
|
1370
|
+
layoutRoot.children.push(col2);
|
|
1371
|
+
}
|
|
1304
1372
|
}
|
|
1305
1373
|
else {
|
|
1306
|
-
|
|
1307
|
-
// We can't put a row in a row effectively without wrapping.
|
|
1308
|
-
// For now assume root is fixed to col.
|
|
1309
|
-
newLayout.children.push(newRow);
|
|
1374
|
+
return;
|
|
1310
1375
|
}
|
|
1311
|
-
this.setSchema(
|
|
1376
|
+
this.setSchema(newSchema);
|
|
1377
|
+
this.selectNode(this.composeScopedNodeId(insertionScopePath, col1.id));
|
|
1312
1378
|
}
|
|
1313
1379
|
// --- Column Management Methods ---
|
|
1314
1380
|
/** Remove a column by its id */
|
|
1315
1381
|
removeColumn(columnId) {
|
|
1316
1382
|
if (this.isReadOnly())
|
|
1317
1383
|
return;
|
|
1384
|
+
const entry = this.resolveEntry(columnId);
|
|
1385
|
+
if (!entry)
|
|
1386
|
+
return;
|
|
1318
1387
|
const current = this.schema();
|
|
1319
|
-
const
|
|
1388
|
+
const newSchema = this.cloneValue(current);
|
|
1389
|
+
const scopeSchema = this.resolveSchemaAtScope(newSchema, entry.scopePath);
|
|
1390
|
+
if (!scopeSchema)
|
|
1391
|
+
return;
|
|
1320
1392
|
// Find parent row and remove column
|
|
1321
1393
|
const removeFromRow = (node) => {
|
|
1322
1394
|
if (node.type === 'row') {
|
|
1323
|
-
const idx = node.children.findIndex((c) => c.id ===
|
|
1395
|
+
const idx = node.children.findIndex((c) => c.id === entry.rawNodeId && c.type === 'col');
|
|
1324
1396
|
if (idx !== -1) {
|
|
1325
1397
|
node.children.splice(idx, 1);
|
|
1326
1398
|
return true;
|
|
@@ -1334,16 +1406,22 @@ class DesignerStateService {
|
|
|
1334
1406
|
}
|
|
1335
1407
|
return false;
|
|
1336
1408
|
};
|
|
1337
|
-
removeFromRow(
|
|
1338
|
-
this.setSchema(
|
|
1409
|
+
removeFromRow(scopeSchema.layout);
|
|
1410
|
+
this.setSchema(newSchema);
|
|
1339
1411
|
}
|
|
1340
1412
|
/** Set preset column layout (e.g., 3,4,6) on a row */
|
|
1341
1413
|
setPresetLayout(rowId, preset) {
|
|
1342
1414
|
if (this.isReadOnly())
|
|
1343
1415
|
return;
|
|
1416
|
+
const entry = this.resolveEntry(rowId);
|
|
1417
|
+
if (!entry)
|
|
1418
|
+
return;
|
|
1344
1419
|
const current = this.schema();
|
|
1345
|
-
const
|
|
1346
|
-
const
|
|
1420
|
+
const newSchema = this.cloneValue(current);
|
|
1421
|
+
const scopeSchema = this.resolveSchemaAtScope(newSchema, entry.scopePath);
|
|
1422
|
+
if (!scopeSchema)
|
|
1423
|
+
return;
|
|
1424
|
+
const rowNode = this.findNode(scopeSchema.layout, entry.rawNodeId);
|
|
1347
1425
|
if (!rowNode || rowNode.type !== 'row')
|
|
1348
1426
|
return;
|
|
1349
1427
|
rowNode.maxColumns = preset;
|
|
@@ -1368,15 +1446,21 @@ class DesignerStateService {
|
|
|
1368
1446
|
rowNode.children.forEach((c) => {
|
|
1369
1447
|
c.responsive = { xs: Math.floor(12 / preset) };
|
|
1370
1448
|
});
|
|
1371
|
-
this.setSchema(
|
|
1449
|
+
this.setSchema(newSchema);
|
|
1372
1450
|
}
|
|
1373
1451
|
/** Add one or more columns to a row */
|
|
1374
1452
|
addColumn(rowId, count = 1) {
|
|
1375
1453
|
if (this.isReadOnly())
|
|
1376
1454
|
return;
|
|
1455
|
+
const entry = this.resolveEntry(rowId);
|
|
1456
|
+
if (!entry)
|
|
1457
|
+
return;
|
|
1377
1458
|
const current = this.schema();
|
|
1378
|
-
const
|
|
1379
|
-
const
|
|
1459
|
+
const newSchema = this.cloneValue(current);
|
|
1460
|
+
const scopeSchema = this.resolveSchemaAtScope(newSchema, entry.scopePath);
|
|
1461
|
+
if (!scopeSchema)
|
|
1462
|
+
return;
|
|
1463
|
+
const rowNode = this.findNode(scopeSchema.layout, entry.rawNodeId);
|
|
1380
1464
|
if (!rowNode || rowNode.type !== 'row')
|
|
1381
1465
|
return;
|
|
1382
1466
|
for (let i = 0; i < count; i++) {
|
|
@@ -1388,13 +1472,19 @@ class DesignerStateService {
|
|
|
1388
1472
|
};
|
|
1389
1473
|
rowNode.children.push(col);
|
|
1390
1474
|
}
|
|
1391
|
-
this.setSchema(
|
|
1475
|
+
this.setSchema(newSchema);
|
|
1392
1476
|
}
|
|
1393
1477
|
// --- Node Manipulation Methods (for floating toolbar) ---
|
|
1394
1478
|
/** Move a node up or down within its parent container */
|
|
1395
1479
|
moveNode(nodeId, direction) {
|
|
1480
|
+
const entry = this.resolveEntry(nodeId);
|
|
1481
|
+
if (!entry)
|
|
1482
|
+
return;
|
|
1396
1483
|
const current = this.schema();
|
|
1397
|
-
const
|
|
1484
|
+
const newSchema = this.cloneValue(current);
|
|
1485
|
+
const scopeSchema = this.resolveSchemaAtScope(newSchema, entry.scopePath);
|
|
1486
|
+
if (!scopeSchema)
|
|
1487
|
+
return;
|
|
1398
1488
|
// Find parent that contains this node
|
|
1399
1489
|
const findParentAndIndex = (parent, id) => {
|
|
1400
1490
|
if ('children' in parent) {
|
|
@@ -1411,7 +1501,7 @@ class DesignerStateService {
|
|
|
1411
1501
|
}
|
|
1412
1502
|
return null;
|
|
1413
1503
|
};
|
|
1414
|
-
const result = findParentAndIndex(
|
|
1504
|
+
const result = findParentAndIndex(scopeSchema.layout, entry.rawNodeId);
|
|
1415
1505
|
if (!result)
|
|
1416
1506
|
return;
|
|
1417
1507
|
const { parent, index } = result;
|
|
@@ -1424,13 +1514,19 @@ class DesignerStateService {
|
|
|
1424
1514
|
// Swap with next
|
|
1425
1515
|
[children[index], children[index + 1]] = [children[index + 1], children[index]];
|
|
1426
1516
|
}
|
|
1427
|
-
this.setSchema(
|
|
1517
|
+
this.setSchema(newSchema);
|
|
1428
1518
|
}
|
|
1429
1519
|
/** Duplicate a node (row, col, or widget) */
|
|
1430
1520
|
duplicateNode(nodeId) {
|
|
1521
|
+
const entry = this.resolveEntry(nodeId);
|
|
1522
|
+
if (!entry)
|
|
1523
|
+
return;
|
|
1431
1524
|
const current = this.schema();
|
|
1432
|
-
const
|
|
1433
|
-
const
|
|
1525
|
+
const newSchema = this.cloneValue(current);
|
|
1526
|
+
const scopeSchema = this.resolveSchemaAtScope(newSchema, entry.scopePath);
|
|
1527
|
+
if (!scopeSchema)
|
|
1528
|
+
return;
|
|
1529
|
+
const scopeFields = scopeSchema.fields;
|
|
1434
1530
|
// Find node and its parent
|
|
1435
1531
|
const findParentAndNode = (parent, id) => {
|
|
1436
1532
|
if ('children' in parent) {
|
|
@@ -1447,7 +1543,7 @@ class DesignerStateService {
|
|
|
1447
1543
|
}
|
|
1448
1544
|
return null;
|
|
1449
1545
|
};
|
|
1450
|
-
const result = findParentAndNode(
|
|
1546
|
+
const result = findParentAndNode(scopeSchema.layout, entry.rawNodeId);
|
|
1451
1547
|
if (!result)
|
|
1452
1548
|
return;
|
|
1453
1549
|
const { parent, node, index } = result;
|
|
@@ -1458,7 +1554,7 @@ class DesignerStateService {
|
|
|
1458
1554
|
cloned.id = v4();
|
|
1459
1555
|
// If it's a widget, also duplicate the field
|
|
1460
1556
|
if (cloned.type === 'widget' && cloned.refId) {
|
|
1461
|
-
const originalField =
|
|
1557
|
+
const originalField = scopeFields.find(f => f.id === cloned.refId);
|
|
1462
1558
|
if (originalField) {
|
|
1463
1559
|
const newFieldId = v4();
|
|
1464
1560
|
const newField = {
|
|
@@ -1467,7 +1563,7 @@ class DesignerStateService {
|
|
|
1467
1563
|
name: `${originalField.name}_copy_${Date.now()}`,
|
|
1468
1564
|
label: `${originalField.label || 'Field'} (Copy)`
|
|
1469
1565
|
};
|
|
1470
|
-
|
|
1566
|
+
scopeFields.push(newField);
|
|
1471
1567
|
cloned.refId = newFieldId;
|
|
1472
1568
|
}
|
|
1473
1569
|
}
|
|
@@ -1478,18 +1574,24 @@ class DesignerStateService {
|
|
|
1478
1574
|
};
|
|
1479
1575
|
const duplicatedNode = cloneWithNewIds(node);
|
|
1480
1576
|
children.splice(index + 1, 0, duplicatedNode);
|
|
1481
|
-
this.setSchema(
|
|
1482
|
-
this.selectNode(duplicatedNode.id);
|
|
1577
|
+
this.setSchema(newSchema);
|
|
1578
|
+
this.selectNode(this.composeScopedNodeId(entry.scopePath, duplicatedNode.id));
|
|
1483
1579
|
}
|
|
1484
1580
|
/** Delete a node (row, col, or widget) and associated field if widget */
|
|
1485
1581
|
deleteNode(nodeId) {
|
|
1582
|
+
const entry = this.resolveEntry(nodeId);
|
|
1583
|
+
if (!entry)
|
|
1584
|
+
return;
|
|
1486
1585
|
const current = this.schema();
|
|
1487
|
-
const
|
|
1488
|
-
|
|
1586
|
+
const newSchema = this.cloneValue(current);
|
|
1587
|
+
const scopeSchema = this.resolveSchemaAtScope(newSchema, entry.scopePath);
|
|
1588
|
+
if (!scopeSchema)
|
|
1589
|
+
return;
|
|
1590
|
+
const fieldsToRemove = new Set();
|
|
1489
1591
|
// Collect field IDs to remove (for widgets)
|
|
1490
1592
|
const collectFieldIds = (node) => {
|
|
1491
1593
|
if (node.type === 'widget' && node.refId) {
|
|
1492
|
-
fieldsToRemove.
|
|
1594
|
+
fieldsToRemove.add(node.refId);
|
|
1493
1595
|
}
|
|
1494
1596
|
if ('children' in node) {
|
|
1495
1597
|
node.children.forEach(c => collectFieldIds(c));
|
|
@@ -1512,82 +1614,82 @@ class DesignerStateService {
|
|
|
1512
1614
|
}
|
|
1513
1615
|
return false;
|
|
1514
1616
|
};
|
|
1515
|
-
removeFromParent(
|
|
1516
|
-
|
|
1517
|
-
const newFields = current.fields.filter(f => !fieldsToRemove.includes(f.id));
|
|
1617
|
+
removeFromParent(scopeSchema.layout, entry.rawNodeId);
|
|
1618
|
+
scopeSchema.fields = scopeSchema.fields.filter(field => !fieldsToRemove.has(field.id));
|
|
1518
1619
|
// Clear selection
|
|
1519
|
-
if (this.selectedNodeId() === nodeId) {
|
|
1620
|
+
if (this.selectedNodeId() === nodeId || this.selectedNodeId() === this.composeScopedNodeId(entry.scopePath, entry.rawNodeId)) {
|
|
1520
1621
|
this.selectNode(null);
|
|
1521
1622
|
}
|
|
1522
|
-
this.setSchema(
|
|
1623
|
+
this.setSchema(newSchema);
|
|
1523
1624
|
}
|
|
1524
1625
|
// Phase 2: Duplicate field
|
|
1525
1626
|
duplicateField(fieldId) {
|
|
1526
1627
|
const current = this.schema();
|
|
1527
|
-
const
|
|
1528
|
-
|
|
1628
|
+
const nextSchema = this.cloneValue(current);
|
|
1629
|
+
const preferredScope = this.selectedNodeScopePath();
|
|
1630
|
+
const location = this.findFieldLocation(nextSchema, fieldId, preferredScope);
|
|
1631
|
+
if (!location)
|
|
1529
1632
|
return;
|
|
1530
|
-
// Create new field with copied properties
|
|
1531
1633
|
const newFieldId = v4();
|
|
1532
1634
|
const newField = {
|
|
1533
|
-
...JSON.parse(JSON.stringify(field)),
|
|
1635
|
+
...JSON.parse(JSON.stringify(location.field)),
|
|
1534
1636
|
id: newFieldId,
|
|
1535
|
-
name: `${field.name}_copy_${Date.now()}`,
|
|
1536
|
-
label: `${field.label || 'Field'} (Copy)`
|
|
1637
|
+
name: `${location.field.name}_copy_${Date.now()}`,
|
|
1638
|
+
label: `${location.field.label || 'Field'} (Copy)`
|
|
1537
1639
|
};
|
|
1538
|
-
|
|
1539
|
-
const
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
const parent = this.findParentCol(newLayout, widgetNode.id);
|
|
1544
|
-
if (parent) {
|
|
1545
|
-
const index = parent.children.findIndex((c) => c.id === widgetNode.id);
|
|
1546
|
-
const newWidgetNode = {
|
|
1547
|
-
id: v4(),
|
|
1548
|
-
type: 'widget',
|
|
1549
|
-
widgetKind: 'field',
|
|
1550
|
-
refId: newFieldId
|
|
1551
|
-
};
|
|
1552
|
-
parent.children.splice(index + 1, 0, newWidgetNode);
|
|
1553
|
-
}
|
|
1640
|
+
location.schema.fields.push(newField);
|
|
1641
|
+
const widgetNode = this.findWidgetByRefId(location.schema.layout, fieldId);
|
|
1642
|
+
if (!widgetNode) {
|
|
1643
|
+
this.setSchema(nextSchema);
|
|
1644
|
+
return;
|
|
1554
1645
|
}
|
|
1555
|
-
this.
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
}
|
|
1646
|
+
const parent = this.findParentCol(location.schema.layout, widgetNode.id);
|
|
1647
|
+
if (!parent) {
|
|
1648
|
+
this.setSchema(nextSchema);
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
const index = parent.children.findIndex((candidate) => candidate.id === widgetNode.id);
|
|
1652
|
+
const newWidgetNode = {
|
|
1653
|
+
id: v4(),
|
|
1654
|
+
type: 'widget',
|
|
1655
|
+
widgetKind: widgetNode.widgetKind ?? 'field',
|
|
1656
|
+
refId: newFieldId
|
|
1657
|
+
};
|
|
1658
|
+
parent.children.splice(index + 1, 0, newWidgetNode);
|
|
1659
|
+
this.setSchema(nextSchema);
|
|
1660
|
+
this.selectNode(this.composeScopedNodeId(location.scopePath, newWidgetNode.id));
|
|
1560
1661
|
}
|
|
1561
1662
|
// Phase 2: Remove field
|
|
1562
1663
|
removeField(fieldId) {
|
|
1563
1664
|
const current = this.schema();
|
|
1564
|
-
const
|
|
1565
|
-
const
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1665
|
+
const nextSchema = this.cloneValue(current);
|
|
1666
|
+
const preferredScope = this.selectedNodeScopePath();
|
|
1667
|
+
const location = this.findFieldLocation(nextSchema, fieldId, preferredScope);
|
|
1668
|
+
if (!location)
|
|
1669
|
+
return;
|
|
1670
|
+
location.schema.fields = location.schema.fields.filter(field => field.id !== fieldId);
|
|
1671
|
+
this.removeWidgetByRefId(location.schema.layout, fieldId);
|
|
1672
|
+
const selectedId = this.selectedNodeId();
|
|
1673
|
+
if (selectedId) {
|
|
1674
|
+
const selectedEntry = this.layoutIndex()[selectedId];
|
|
1675
|
+
if (selectedEntry?.node.type === 'widget' && selectedEntry.node.refId === fieldId && this.sameScope(selectedEntry.scopePath, location.scopePath)) {
|
|
1676
|
+
this.selectNode(null);
|
|
1677
|
+
}
|
|
1572
1678
|
}
|
|
1573
|
-
this.setSchema(
|
|
1574
|
-
...current,
|
|
1575
|
-
fields: newFields,
|
|
1576
|
-
layout: newLayout
|
|
1577
|
-
});
|
|
1679
|
+
this.setSchema(nextSchema);
|
|
1578
1680
|
}
|
|
1579
1681
|
updateField(fieldId, updates) {
|
|
1580
1682
|
const current = this.schema();
|
|
1581
|
-
const
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
return
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
...
|
|
1589
|
-
|
|
1590
|
-
|
|
1683
|
+
const nextSchema = this.cloneValue(current);
|
|
1684
|
+
const preferredScope = this.selectedNodeScopePath();
|
|
1685
|
+
const location = this.findFieldLocation(nextSchema, fieldId, preferredScope);
|
|
1686
|
+
if (!location)
|
|
1687
|
+
return;
|
|
1688
|
+
location.schema.fields[location.index] = {
|
|
1689
|
+
...location.field,
|
|
1690
|
+
...updates
|
|
1691
|
+
};
|
|
1692
|
+
this.setSchema(nextSchema);
|
|
1591
1693
|
}
|
|
1592
1694
|
groupSelectedFields() {
|
|
1593
1695
|
const selected = this.getSelectedWidgetFields();
|
|
@@ -1638,23 +1740,23 @@ class DesignerStateService {
|
|
|
1638
1740
|
return this.getSelectedWidgetFields().some(field => !!field.groupKey);
|
|
1639
1741
|
}
|
|
1640
1742
|
getSelectedWidgetFields() {
|
|
1641
|
-
const current = this.schema();
|
|
1642
1743
|
const index = this.layoutIndex();
|
|
1643
|
-
const fieldMap = new Map(current.fields.map(field => [field.id, field]));
|
|
1644
1744
|
const selectedFields = [];
|
|
1645
1745
|
const seen = new Set();
|
|
1646
1746
|
for (const nodeId of this.selectedNodeIds()) {
|
|
1647
|
-
const
|
|
1747
|
+
const entry = index[nodeId];
|
|
1748
|
+
const node = entry?.node;
|
|
1648
1749
|
if (!node || node.type !== 'widget')
|
|
1649
1750
|
continue;
|
|
1650
1751
|
const refId = node.refId;
|
|
1651
|
-
|
|
1752
|
+
const scopeRefKey = `${entry.scopePath.join('/')}:${refId ?? ''}`;
|
|
1753
|
+
if (!refId || seen.has(scopeRefKey))
|
|
1652
1754
|
continue;
|
|
1653
|
-
const field =
|
|
1755
|
+
const field = this.findFieldByIdInScope(this.schema(), entry.scopePath, refId);
|
|
1654
1756
|
if (!field)
|
|
1655
1757
|
continue;
|
|
1656
1758
|
selectedFields.push(field);
|
|
1657
|
-
seen.add(
|
|
1759
|
+
seen.add(scopeRefKey);
|
|
1658
1760
|
}
|
|
1659
1761
|
return selectedFields;
|
|
1660
1762
|
}
|
|
@@ -1663,20 +1765,36 @@ class DesignerStateService {
|
|
|
1663
1765
|
* Only the path from root to the target node gets new references.
|
|
1664
1766
|
*/
|
|
1665
1767
|
updateNodeStyle(nodeId, style) {
|
|
1768
|
+
const entry = this.resolveEntry(nodeId);
|
|
1769
|
+
if (!entry)
|
|
1770
|
+
return;
|
|
1666
1771
|
const current = this.schema();
|
|
1667
|
-
const
|
|
1668
|
-
|
|
1669
|
-
|
|
1772
|
+
const nextSchema = this.cloneValue(current);
|
|
1773
|
+
const scopeSchema = this.resolveSchemaAtScope(nextSchema, entry.scopePath);
|
|
1774
|
+
if (!scopeSchema)
|
|
1775
|
+
return;
|
|
1776
|
+
const newLayout = this.updateNodeInTree(scopeSchema.layout, entry.rawNodeId, { style });
|
|
1777
|
+
if (newLayout !== scopeSchema.layout) {
|
|
1778
|
+
scopeSchema.layout = newLayout;
|
|
1779
|
+
this.setSchema(nextSchema);
|
|
1670
1780
|
}
|
|
1671
1781
|
}
|
|
1672
1782
|
/**
|
|
1673
1783
|
* Update a specific node's responsive settings.
|
|
1674
1784
|
*/
|
|
1675
1785
|
updateNodeResponsive(nodeId, responsive) {
|
|
1786
|
+
const entry = this.resolveEntry(nodeId);
|
|
1787
|
+
if (!entry)
|
|
1788
|
+
return;
|
|
1676
1789
|
const current = this.schema();
|
|
1677
|
-
const
|
|
1678
|
-
|
|
1679
|
-
|
|
1790
|
+
const nextSchema = this.cloneValue(current);
|
|
1791
|
+
const scopeSchema = this.resolveSchemaAtScope(nextSchema, entry.scopePath);
|
|
1792
|
+
if (!scopeSchema)
|
|
1793
|
+
return;
|
|
1794
|
+
const newLayout = this.updateNodeInTree(scopeSchema.layout, entry.rawNodeId, { responsive });
|
|
1795
|
+
if (newLayout !== scopeSchema.layout) {
|
|
1796
|
+
scopeSchema.layout = newLayout;
|
|
1797
|
+
this.setSchema(nextSchema);
|
|
1680
1798
|
}
|
|
1681
1799
|
}
|
|
1682
1800
|
/**
|
|
@@ -1703,6 +1821,146 @@ class DesignerStateService {
|
|
|
1703
1821
|
}
|
|
1704
1822
|
return node; // Unchanged - return original reference
|
|
1705
1823
|
}
|
|
1824
|
+
resolveEntry(nodeId) {
|
|
1825
|
+
const direct = this.layoutIndex()[nodeId];
|
|
1826
|
+
if (direct)
|
|
1827
|
+
return direct;
|
|
1828
|
+
const selectedId = this.selectedNodeId();
|
|
1829
|
+
if (!selectedId)
|
|
1830
|
+
return null;
|
|
1831
|
+
const selectedEntry = this.layoutIndex()[selectedId];
|
|
1832
|
+
if (!selectedEntry)
|
|
1833
|
+
return null;
|
|
1834
|
+
if (selectedEntry.rawNodeId !== nodeId)
|
|
1835
|
+
return null;
|
|
1836
|
+
return selectedEntry;
|
|
1837
|
+
}
|
|
1838
|
+
resolveSchemaAtScope(root, scopePath) {
|
|
1839
|
+
let cursor = root;
|
|
1840
|
+
for (const repeatableFieldId of scopePath) {
|
|
1841
|
+
const field = cursor.fields.find(candidate => candidate.id === repeatableFieldId);
|
|
1842
|
+
const itemSchema = field?.repeatable?.itemSchema;
|
|
1843
|
+
if (!field || !itemSchema?.layout || !Array.isArray(itemSchema.fields)) {
|
|
1844
|
+
return null;
|
|
1845
|
+
}
|
|
1846
|
+
cursor = itemSchema;
|
|
1847
|
+
}
|
|
1848
|
+
return cursor;
|
|
1849
|
+
}
|
|
1850
|
+
findFieldByIdInScope(root, scopePath, fieldId) {
|
|
1851
|
+
const scopeSchema = this.resolveSchemaAtScope(root, scopePath);
|
|
1852
|
+
if (!scopeSchema)
|
|
1853
|
+
return null;
|
|
1854
|
+
return scopeSchema.fields.find(field => field.id === fieldId) ?? null;
|
|
1855
|
+
}
|
|
1856
|
+
resolveInsertionScopePath(selectedEntry) {
|
|
1857
|
+
if (!selectedEntry)
|
|
1858
|
+
return [];
|
|
1859
|
+
if (selectedEntry.node.type !== 'widget')
|
|
1860
|
+
return selectedEntry.scopePath;
|
|
1861
|
+
const widgetNode = selectedEntry.node;
|
|
1862
|
+
const field = this.findFieldByIdInScope(this.schema(), selectedEntry.scopePath, widgetNode.refId ?? '');
|
|
1863
|
+
if (field?.type === 'repeatable-group' && field.repeatable?.itemSchema) {
|
|
1864
|
+
return [...selectedEntry.scopePath, field.id];
|
|
1865
|
+
}
|
|
1866
|
+
return selectedEntry.scopePath;
|
|
1867
|
+
}
|
|
1868
|
+
resolveTargetColumnForFieldInsert(targetSchema, selectedEntry, insertionScopePath) {
|
|
1869
|
+
if (selectedEntry && this.sameScope(selectedEntry.scopePath, insertionScopePath)) {
|
|
1870
|
+
if (selectedEntry.node.type === 'col') {
|
|
1871
|
+
const selectedCol = this.findNode(targetSchema.layout, selectedEntry.rawNodeId);
|
|
1872
|
+
if (selectedCol && selectedCol.type === 'col') {
|
|
1873
|
+
return selectedCol;
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
if (selectedEntry.node.type === 'widget') {
|
|
1877
|
+
const parent = this.findParentCol(targetSchema.layout, selectedEntry.rawNodeId);
|
|
1878
|
+
if (parent)
|
|
1879
|
+
return parent;
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
return this.findFirstColumn(targetSchema.layout);
|
|
1883
|
+
}
|
|
1884
|
+
findFirstColumn(node) {
|
|
1885
|
+
if (node.type === 'col')
|
|
1886
|
+
return node;
|
|
1887
|
+
if (!('children' in node))
|
|
1888
|
+
return null;
|
|
1889
|
+
for (const child of node.children) {
|
|
1890
|
+
const match = this.findFirstColumn(child);
|
|
1891
|
+
if (match)
|
|
1892
|
+
return match;
|
|
1893
|
+
}
|
|
1894
|
+
return null;
|
|
1895
|
+
}
|
|
1896
|
+
selectedNodeScopePath() {
|
|
1897
|
+
const selectedId = this.selectedNodeId();
|
|
1898
|
+
if (!selectedId)
|
|
1899
|
+
return undefined;
|
|
1900
|
+
const entry = this.layoutIndex()[selectedId];
|
|
1901
|
+
if (!entry)
|
|
1902
|
+
return undefined;
|
|
1903
|
+
return entry.scopePath;
|
|
1904
|
+
}
|
|
1905
|
+
findFieldLocation(root, fieldId, preferredScopePath) {
|
|
1906
|
+
const searchInScope = (scopeSchema, scopePath) => {
|
|
1907
|
+
const index = scopeSchema.fields.findIndex(candidate => candidate.id === fieldId);
|
|
1908
|
+
if (index !== -1) {
|
|
1909
|
+
return {
|
|
1910
|
+
schema: scopeSchema,
|
|
1911
|
+
field: scopeSchema.fields[index],
|
|
1912
|
+
index,
|
|
1913
|
+
scopePath
|
|
1914
|
+
};
|
|
1915
|
+
}
|
|
1916
|
+
for (const field of scopeSchema.fields) {
|
|
1917
|
+
const itemSchema = field.repeatable?.itemSchema;
|
|
1918
|
+
if (!itemSchema?.layout || !Array.isArray(itemSchema.fields))
|
|
1919
|
+
continue;
|
|
1920
|
+
const nestedScopePath = [...scopePath, field.id];
|
|
1921
|
+
const nestedMatch = searchInScope(itemSchema, nestedScopePath);
|
|
1922
|
+
if (nestedMatch)
|
|
1923
|
+
return nestedMatch;
|
|
1924
|
+
}
|
|
1925
|
+
return null;
|
|
1926
|
+
};
|
|
1927
|
+
if (preferredScopePath) {
|
|
1928
|
+
const preferredSchema = this.resolveSchemaAtScope(root, preferredScopePath);
|
|
1929
|
+
if (preferredSchema) {
|
|
1930
|
+
const preferredIndex = preferredSchema.fields.findIndex(candidate => candidate.id === fieldId);
|
|
1931
|
+
if (preferredIndex !== -1) {
|
|
1932
|
+
return {
|
|
1933
|
+
schema: preferredSchema,
|
|
1934
|
+
field: preferredSchema.fields[preferredIndex],
|
|
1935
|
+
index: preferredIndex,
|
|
1936
|
+
scopePath: preferredScopePath
|
|
1937
|
+
};
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
return searchInScope(root, []);
|
|
1942
|
+
}
|
|
1943
|
+
sameScope(left, right) {
|
|
1944
|
+
if (left.length !== right.length)
|
|
1945
|
+
return false;
|
|
1946
|
+
for (let i = 0; i < left.length; i += 1) {
|
|
1947
|
+
if (left[i] !== right[i])
|
|
1948
|
+
return false;
|
|
1949
|
+
}
|
|
1950
|
+
return true;
|
|
1951
|
+
}
|
|
1952
|
+
isScopePrefix(prefix, value) {
|
|
1953
|
+
if (prefix.length > value.length)
|
|
1954
|
+
return false;
|
|
1955
|
+
for (let i = 0; i < prefix.length; i += 1) {
|
|
1956
|
+
if (prefix[i] !== value[i])
|
|
1957
|
+
return false;
|
|
1958
|
+
}
|
|
1959
|
+
return true;
|
|
1960
|
+
}
|
|
1961
|
+
cloneValue(value) {
|
|
1962
|
+
return JSON.parse(JSON.stringify(value));
|
|
1963
|
+
}
|
|
1706
1964
|
isLayoutNode(value) {
|
|
1707
1965
|
if (!value || typeof value !== 'object')
|
|
1708
1966
|
return false;
|
|
@@ -1767,9 +2025,15 @@ class DesignerStateService {
|
|
|
1767
2025
|
* Useful for creating nested grids.
|
|
1768
2026
|
*/
|
|
1769
2027
|
insertRowInColumn(columnId, colCount = 1) {
|
|
2028
|
+
const entry = this.resolveEntry(columnId);
|
|
2029
|
+
if (!entry)
|
|
2030
|
+
return;
|
|
1770
2031
|
const current = this.schema();
|
|
1771
|
-
const
|
|
1772
|
-
const
|
|
2032
|
+
const newSchema = this.cloneValue(current);
|
|
2033
|
+
const scopeSchema = this.resolveSchemaAtScope(newSchema, entry.scopePath);
|
|
2034
|
+
if (!scopeSchema)
|
|
2035
|
+
return;
|
|
2036
|
+
const colNode = this.findNode(scopeSchema.layout, entry.rawNodeId);
|
|
1773
2037
|
if (!colNode || colNode.type !== 'col')
|
|
1774
2038
|
return;
|
|
1775
2039
|
const newRow = {
|
|
@@ -1787,9 +2051,9 @@ class DesignerStateService {
|
|
|
1787
2051
|
});
|
|
1788
2052
|
}
|
|
1789
2053
|
colNode.children.push(newRow);
|
|
1790
|
-
this.setSchema(
|
|
2054
|
+
this.setSchema(newSchema);
|
|
1791
2055
|
// Select the first column of the new row to aid flow
|
|
1792
|
-
this.selectNode(newRow.children[0].id);
|
|
2056
|
+
this.selectNode(this.composeScopedNodeId(entry.scopePath, newRow.children[0].id));
|
|
1793
2057
|
}
|
|
1794
2058
|
/**
|
|
1795
2059
|
* Wrap an existing widget in a new Row -> Column structure.
|
|
@@ -1797,10 +2061,16 @@ class DesignerStateService {
|
|
|
1797
2061
|
* and puts the widget inside the new Row's Column.
|
|
1798
2062
|
*/
|
|
1799
2063
|
wrapWidgetInRow(widgetId) {
|
|
2064
|
+
const entry = this.resolveEntry(widgetId);
|
|
2065
|
+
if (!entry)
|
|
2066
|
+
return;
|
|
1800
2067
|
const current = this.schema();
|
|
1801
|
-
const
|
|
2068
|
+
const newSchema = this.cloneValue(current);
|
|
2069
|
+
const scopeSchema = this.resolveSchemaAtScope(newSchema, entry.scopePath);
|
|
2070
|
+
if (!scopeSchema)
|
|
2071
|
+
return;
|
|
1802
2072
|
// 1. Find the widget and its parent
|
|
1803
|
-
const findResult = this.findParentAndIndex(
|
|
2073
|
+
const findResult = this.findParentAndIndex(scopeSchema.layout, entry.rawNodeId);
|
|
1804
2074
|
if (!findResult)
|
|
1805
2075
|
return;
|
|
1806
2076
|
const { parent, index } = findResult;
|
|
@@ -1820,10 +2090,10 @@ class DesignerStateService {
|
|
|
1820
2090
|
};
|
|
1821
2091
|
// 3. Replace widget in parent with new Row
|
|
1822
2092
|
parent.children.splice(index, 1, newRow);
|
|
1823
|
-
this.setSchema(
|
|
2093
|
+
this.setSchema(newSchema);
|
|
1824
2094
|
// Keep selection on widget? Or select the new Row?
|
|
1825
2095
|
// Let's keep widget selected so context remains
|
|
1826
|
-
this.selectNode(
|
|
2096
|
+
this.selectNode(this.composeScopedNodeId(entry.scopePath, entry.rawNodeId));
|
|
1827
2097
|
}
|
|
1828
2098
|
// Internal helper which was seemingly missing in previous steps but needed now
|
|
1829
2099
|
findParentAndIndex(root, childId) {
|
|
@@ -1861,6 +2131,7 @@ class LayoutNodeComponent {
|
|
|
1861
2131
|
engine;
|
|
1862
2132
|
fields = [];
|
|
1863
2133
|
designMode = false;
|
|
2134
|
+
scopePath = [];
|
|
1864
2135
|
device = 'desktop';
|
|
1865
2136
|
breakpoint = 'xl';
|
|
1866
2137
|
nodeDrop = new EventEmitter();
|
|
@@ -1887,10 +2158,10 @@ class LayoutNodeComponent {
|
|
|
1887
2158
|
this.widgetDefs = this.allWidgetDefs;
|
|
1888
2159
|
}
|
|
1889
2160
|
get isSelected() {
|
|
1890
|
-
return this.designerState.isNodeSelected(this.node.id);
|
|
2161
|
+
return this.designerState.isNodeSelected(this.getScopedNodeId(this.node.id));
|
|
1891
2162
|
}
|
|
1892
2163
|
isNodeSelected(nodeId) {
|
|
1893
|
-
return this.designerState.isNodeSelected(nodeId);
|
|
2164
|
+
return this.designerState.isNodeSelected(this.getScopedNodeId(nodeId));
|
|
1894
2165
|
}
|
|
1895
2166
|
onWidgetClick(e) {
|
|
1896
2167
|
if (!this.designMode)
|
|
@@ -1898,10 +2169,10 @@ class LayoutNodeComponent {
|
|
|
1898
2169
|
e.stopPropagation();
|
|
1899
2170
|
this.designerState.closeContextMenu();
|
|
1900
2171
|
if (e instanceof MouseEvent && (e.ctrlKey || e.metaKey || e.shiftKey)) {
|
|
1901
|
-
this.designerState.toggleNodeSelection(this.node.id);
|
|
2172
|
+
this.designerState.toggleNodeSelection(this.getScopedNodeId(this.node.id));
|
|
1902
2173
|
}
|
|
1903
2174
|
else {
|
|
1904
|
-
this.nodeSelect.emit(this.node.id);
|
|
2175
|
+
this.nodeSelect.emit(this.getScopedNodeId(this.node.id));
|
|
1905
2176
|
}
|
|
1906
2177
|
}
|
|
1907
2178
|
onWidgetContextMenu(e) {
|
|
@@ -1909,8 +2180,8 @@ class LayoutNodeComponent {
|
|
|
1909
2180
|
return;
|
|
1910
2181
|
e.preventDefault();
|
|
1911
2182
|
e.stopPropagation();
|
|
1912
|
-
if (!this.designerState.isNodeSelected(this.node.id)) {
|
|
1913
|
-
this.nodeSelect.emit(this.node.id);
|
|
2183
|
+
if (!this.designerState.isNodeSelected(this.getScopedNodeId(this.node.id))) {
|
|
2184
|
+
this.nodeSelect.emit(this.getScopedNodeId(this.node.id));
|
|
1914
2185
|
}
|
|
1915
2186
|
this.designerState.openContextMenu({ x: e.clientX, y: e.clientY });
|
|
1916
2187
|
}
|
|
@@ -1920,10 +2191,10 @@ class LayoutNodeComponent {
|
|
|
1920
2191
|
e.stopPropagation();
|
|
1921
2192
|
this.designerState.closeContextMenu();
|
|
1922
2193
|
if (e instanceof MouseEvent && (e.ctrlKey || e.metaKey || e.shiftKey)) {
|
|
1923
|
-
this.designerState.toggleNodeSelection(this.node.id);
|
|
2194
|
+
this.designerState.toggleNodeSelection(this.getScopedNodeId(this.node.id));
|
|
1924
2195
|
}
|
|
1925
2196
|
else {
|
|
1926
|
-
this.nodeSelect.emit(this.node.id);
|
|
2197
|
+
this.nodeSelect.emit(this.getScopedNodeId(this.node.id));
|
|
1927
2198
|
}
|
|
1928
2199
|
}
|
|
1929
2200
|
onRowClick(e) {
|
|
@@ -1932,10 +2203,10 @@ class LayoutNodeComponent {
|
|
|
1932
2203
|
e.stopPropagation();
|
|
1933
2204
|
this.designerState.closeContextMenu();
|
|
1934
2205
|
if (e instanceof MouseEvent && (e.ctrlKey || e.metaKey || e.shiftKey)) {
|
|
1935
|
-
this.designerState.toggleNodeSelection(this.node.id);
|
|
2206
|
+
this.designerState.toggleNodeSelection(this.getScopedNodeId(this.node.id));
|
|
1936
2207
|
}
|
|
1937
2208
|
else {
|
|
1938
|
-
this.nodeSelect.emit(this.node.id);
|
|
2209
|
+
this.nodeSelect.emit(this.getScopedNodeId(this.node.id));
|
|
1939
2210
|
}
|
|
1940
2211
|
}
|
|
1941
2212
|
onRowContextMenu(e) {
|
|
@@ -1943,8 +2214,8 @@ class LayoutNodeComponent {
|
|
|
1943
2214
|
return;
|
|
1944
2215
|
e.preventDefault();
|
|
1945
2216
|
e.stopPropagation();
|
|
1946
|
-
if (!this.designerState.isNodeSelected(this.node.id)) {
|
|
1947
|
-
this.nodeSelect.emit(this.node.id);
|
|
2217
|
+
if (!this.designerState.isNodeSelected(this.getScopedNodeId(this.node.id))) {
|
|
2218
|
+
this.nodeSelect.emit(this.getScopedNodeId(this.node.id));
|
|
1948
2219
|
}
|
|
1949
2220
|
this.designerState.openContextMenu({ x: e.clientX, y: e.clientY });
|
|
1950
2221
|
}
|
|
@@ -1953,36 +2224,36 @@ class LayoutNodeComponent {
|
|
|
1953
2224
|
return;
|
|
1954
2225
|
e.preventDefault();
|
|
1955
2226
|
e.stopPropagation();
|
|
1956
|
-
if (!this.designerState.isNodeSelected(this.node.id)) {
|
|
1957
|
-
this.nodeSelect.emit(this.node.id);
|
|
2227
|
+
if (!this.designerState.isNodeSelected(this.getScopedNodeId(this.node.id))) {
|
|
2228
|
+
this.nodeSelect.emit(this.getScopedNodeId(this.node.id));
|
|
1958
2229
|
}
|
|
1959
2230
|
this.designerState.openContextMenu({ x: e.clientX, y: e.clientY });
|
|
1960
2231
|
}
|
|
1961
2232
|
onDrop(event) {
|
|
1962
2233
|
if (this.designMode) {
|
|
1963
|
-
this.designerState.handleDrop(event, this.node.id);
|
|
2234
|
+
this.designerState.handleDrop(event, this.getScopedNodeId(this.node.id));
|
|
1964
2235
|
}
|
|
1965
2236
|
}
|
|
1966
2237
|
// Floating Toolbar Actions
|
|
1967
2238
|
moveNodeUp() {
|
|
1968
|
-
this.designerState.moveNode(this.node.id, 'up');
|
|
2239
|
+
this.designerState.moveNode(this.getScopedNodeId(this.node.id), 'up');
|
|
1969
2240
|
}
|
|
1970
2241
|
moveNodeDown() {
|
|
1971
|
-
this.designerState.moveNode(this.node.id, 'down');
|
|
2242
|
+
this.designerState.moveNode(this.getScopedNodeId(this.node.id), 'down');
|
|
1972
2243
|
}
|
|
1973
2244
|
deleteNode() {
|
|
1974
|
-
this.designerState.deleteNode(this.node.id);
|
|
2245
|
+
this.designerState.deleteNode(this.getScopedNodeId(this.node.id));
|
|
1975
2246
|
}
|
|
1976
2247
|
duplicateNode() {
|
|
1977
|
-
this.designerState.duplicateNode(this.node.id);
|
|
2248
|
+
this.designerState.duplicateNode(this.getScopedNodeId(this.node.id));
|
|
1978
2249
|
}
|
|
1979
2250
|
// Phase 5 Actions
|
|
1980
2251
|
insertRowInColumn() {
|
|
1981
2252
|
// Default to adding a single column row, maybe prompt user in future?
|
|
1982
|
-
this.designerState.insertRowInColumn(this.node.id, 1);
|
|
2253
|
+
this.designerState.insertRowInColumn(this.getScopedNodeId(this.node.id), 1);
|
|
1983
2254
|
}
|
|
1984
2255
|
wrapWidgetInRow() {
|
|
1985
|
-
this.designerState.wrapWidgetInRow(this.node.id);
|
|
2256
|
+
this.designerState.wrapWidgetInRow(this.getScopedNodeId(this.node.id));
|
|
1986
2257
|
}
|
|
1987
2258
|
getNodeTypeLabel() {
|
|
1988
2259
|
if (this.node.type === 'widget') {
|
|
@@ -2039,7 +2310,7 @@ class LayoutNodeComponent {
|
|
|
2039
2310
|
this.resizeStartHeight = isNaN(currentHeight) ? 50 : currentHeight;
|
|
2040
2311
|
document.addEventListener('mousemove', this.resizeMoveListener);
|
|
2041
2312
|
document.addEventListener('mouseup', this.resizeUpListener);
|
|
2042
|
-
this.designerState.selectNode(node.id);
|
|
2313
|
+
this.designerState.selectNode(this.getScopedNodeId(node.id));
|
|
2043
2314
|
}
|
|
2044
2315
|
// Column Width Resize
|
|
2045
2316
|
onColResizeStart(e, node) {
|
|
@@ -2056,7 +2327,7 @@ class LayoutNodeComponent {
|
|
|
2056
2327
|
this.resizeStartSpan = activeSpan;
|
|
2057
2328
|
document.addEventListener('mousemove', this.resizeMoveListener);
|
|
2058
2329
|
document.addEventListener('mouseup', this.resizeUpListener);
|
|
2059
|
-
this.designerState.selectNode(node.id);
|
|
2330
|
+
this.designerState.selectNode(this.getScopedNodeId(node.id));
|
|
2060
2331
|
}
|
|
2061
2332
|
onResizeMove(e) {
|
|
2062
2333
|
// Handle Height Resize
|
|
@@ -2191,6 +2462,9 @@ class LayoutNodeComponent {
|
|
|
2191
2462
|
return;
|
|
2192
2463
|
this.widgetComponentRef.setInput('config', config);
|
|
2193
2464
|
this.widgetComponentRef.setInput('engine', this.getEngineForRender());
|
|
2465
|
+
if (config.type === 'repeatable-group') {
|
|
2466
|
+
this.widgetComponentRef.setInput('scopePath', this.scopePath);
|
|
2467
|
+
}
|
|
2194
2468
|
const control = this.getWidgetControl(field);
|
|
2195
2469
|
if (control) {
|
|
2196
2470
|
this.widgetComponentRef.setInput('control', control);
|
|
@@ -2213,7 +2487,7 @@ class LayoutNodeComponent {
|
|
|
2213
2487
|
createWidgetInjector(widgetNode) {
|
|
2214
2488
|
const context = {
|
|
2215
2489
|
isDesignMode: () => this.designMode,
|
|
2216
|
-
isSelected: () => this.designMode && this.designerState.selectedNodeId() === widgetNode.id,
|
|
2490
|
+
isSelected: () => this.designMode && this.designerState.selectedNodeId() === this.getScopedNodeId(widgetNode.id),
|
|
2217
2491
|
updateField: (fieldId, updates) => {
|
|
2218
2492
|
if (!this.designMode)
|
|
2219
2493
|
return;
|
|
@@ -2243,6 +2517,21 @@ class LayoutNodeComponent {
|
|
|
2243
2517
|
trackById(index, node) {
|
|
2244
2518
|
return node.id;
|
|
2245
2519
|
}
|
|
2520
|
+
getScopedNodeId(nodeId) {
|
|
2521
|
+
return this.designerState.composeScopedNodeId(this.scopePath, nodeId);
|
|
2522
|
+
}
|
|
2523
|
+
hasActiveNestedSelection() {
|
|
2524
|
+
if (this.node.type !== 'widget')
|
|
2525
|
+
return false;
|
|
2526
|
+
const widgetNode = this.node;
|
|
2527
|
+
if (!widgetNode.refId)
|
|
2528
|
+
return false;
|
|
2529
|
+
const field = this.fields.find(candidate => candidate.id === widgetNode.refId)
|
|
2530
|
+
?? this.engine?.getSchema()?.fields?.find(candidate => candidate.id === widgetNode.refId);
|
|
2531
|
+
if (!field || field.type !== 'repeatable-group')
|
|
2532
|
+
return false;
|
|
2533
|
+
return this.designerState.isSelectionInScope([...this.scopePath, field.id]);
|
|
2534
|
+
}
|
|
2246
2535
|
asRow(node) { return node; }
|
|
2247
2536
|
asCol(node) { return node; }
|
|
2248
2537
|
getColClasses(node) {
|
|
@@ -2349,7 +2638,7 @@ class LayoutNodeComponent {
|
|
|
2349
2638
|
return !this.engine.isFieldVisible(field.id);
|
|
2350
2639
|
}
|
|
2351
2640
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: LayoutNodeComponent, deps: [{ token: WIDGET_DEFINITIONS }, { token: DesignerStateService }, { token: i0.ChangeDetectorRef }, { token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
2352
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.17", type: LayoutNodeComponent, isStandalone: true, selector: "app-layout-node", inputs: { node: "node", engine: "engine", fields: "fields", designMode: "designMode", device: "device", breakpoint: "breakpoint", connectedDropLists: "connectedDropLists" }, outputs: { nodeDrop: "nodeDrop", nodeSelect: "nodeSelect" }, viewQueries: [{ propertyName: "widgetContainer", first: true, predicate: ["widgetContainer"], descendants: true, read: ViewContainerRef }], usesOnChanges: true, ngImport: i0, template: `
|
|
2641
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.17", type: LayoutNodeComponent, isStandalone: true, selector: "app-layout-node", inputs: { node: "node", engine: "engine", fields: "fields", designMode: "designMode", scopePath: "scopePath", device: "device", breakpoint: "breakpoint", connectedDropLists: "connectedDropLists" }, outputs: { nodeDrop: "nodeDrop", nodeSelect: "nodeSelect" }, viewQueries: [{ propertyName: "widgetContainer", first: true, predicate: ["widgetContainer"], descendants: true, read: ViewContainerRef }], usesOnChanges: true, ngImport: i0, template: `
|
|
2353
2642
|
<ng-container [ngSwitch]="node.type">
|
|
2354
2643
|
|
|
2355
2644
|
<!-- ROW -->
|
|
@@ -2397,6 +2686,7 @@ class LayoutNodeComponent {
|
|
|
2397
2686
|
[engine]="engine"
|
|
2398
2687
|
[fields]="fields"
|
|
2399
2688
|
[designMode]="designMode"
|
|
2689
|
+
[scopePath]="scopePath"
|
|
2400
2690
|
[device]="device"
|
|
2401
2691
|
[breakpoint]="breakpoint"
|
|
2402
2692
|
[connectedDropLists]="connectedDropLists"
|
|
@@ -2423,7 +2713,7 @@ class LayoutNodeComponent {
|
|
|
2423
2713
|
(contextmenu)="onColumnContextMenu($event)"
|
|
2424
2714
|
cdkDropList
|
|
2425
2715
|
[cdkDropListData]="node"
|
|
2426
|
-
[id]="node.id"
|
|
2716
|
+
[id]="getScopedNodeId(node.id)"
|
|
2427
2717
|
[cdkDropListConnectedTo]="connectedDropLists"
|
|
2428
2718
|
(cdkDropListDropped)="onDrop($event)"
|
|
2429
2719
|
[class.outline-dashed]="designMode"
|
|
@@ -2477,6 +2767,7 @@ class LayoutNodeComponent {
|
|
|
2477
2767
|
[engine]="engine"
|
|
2478
2768
|
[fields]="fields"
|
|
2479
2769
|
[designMode]="designMode"
|
|
2770
|
+
[scopePath]="scopePath"
|
|
2480
2771
|
[device]="device"
|
|
2481
2772
|
[breakpoint]="breakpoint"
|
|
2482
2773
|
[connectedDropLists]="connectedDropLists"
|
|
@@ -2552,7 +2843,7 @@ class LayoutNodeComponent {
|
|
|
2552
2843
|
</div>
|
|
2553
2844
|
|
|
2554
2845
|
<!-- Overlay for selection in design mode - only show on unselected widgets to allow interaction with selected ones -->
|
|
2555
|
-
<div *ngIf="designMode && !isSelected" class="absolute inset-0 cursor-pointer z-10 hover:bg-blue-500/5" (click)="onWidgetClick($event)"></div>
|
|
2846
|
+
<div *ngIf="designMode && !isSelected && !hasActiveNestedSelection()" class="absolute inset-0 cursor-pointer z-10 hover:bg-blue-500/5" (click)="onWidgetClick($event)"></div>
|
|
2556
2847
|
<div *ngIf="designMode && isSelected"
|
|
2557
2848
|
class="absolute bottom-0 left-0 right-0 h-3 cursor-row-resize bg-transparent hover:bg-blue-400/20 transition-colors z-50 group-hover:bg-blue-100/30"
|
|
2558
2849
|
(mousedown)="onResizeStart($event, node)"
|
|
@@ -2562,7 +2853,7 @@ class LayoutNodeComponent {
|
|
|
2562
2853
|
</ng-container>
|
|
2563
2854
|
|
|
2564
2855
|
</ng-container>
|
|
2565
|
-
`, isInline: true, dependencies: [{ kind: "component", type: LayoutNodeComponent, selector: "app-layout-node", inputs: ["node", "engine", "fields", "designMode", "device", "breakpoint", "connectedDropLists"], outputs: ["nodeDrop", "nodeSelect"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i1.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i3.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i3.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: i3.CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }, { kind: "directive", type: i3.CdkDragPlaceholder, selector: "ng-template[cdkDragPlaceholder]", inputs: ["data"] }, { kind: "ngmodule", type: UiIconModule }, { kind: "component", type: i2.LucideAngularComponent, selector: "lucide-angular, lucide-icon, i-lucide, span-lucide", inputs: ["class", "name", "img", "color", "absoluteStrokeWidth", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2856
|
+
`, isInline: true, dependencies: [{ kind: "component", type: LayoutNodeComponent, selector: "app-layout-node", inputs: ["node", "engine", "fields", "designMode", "scopePath", "device", "breakpoint", "connectedDropLists"], outputs: ["nodeDrop", "nodeSelect"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i1.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i3.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i3.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: i3.CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }, { kind: "directive", type: i3.CdkDragPlaceholder, selector: "ng-template[cdkDragPlaceholder]", inputs: ["data"] }, { kind: "ngmodule", type: UiIconModule }, { kind: "component", type: i2.LucideAngularComponent, selector: "lucide-angular, lucide-icon, i-lucide, span-lucide", inputs: ["class", "name", "img", "color", "absoluteStrokeWidth", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2566
2857
|
}
|
|
2567
2858
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: LayoutNodeComponent, decorators: [{
|
|
2568
2859
|
type: Component,
|
|
@@ -2619,6 +2910,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
|
|
|
2619
2910
|
[engine]="engine"
|
|
2620
2911
|
[fields]="fields"
|
|
2621
2912
|
[designMode]="designMode"
|
|
2913
|
+
[scopePath]="scopePath"
|
|
2622
2914
|
[device]="device"
|
|
2623
2915
|
[breakpoint]="breakpoint"
|
|
2624
2916
|
[connectedDropLists]="connectedDropLists"
|
|
@@ -2645,7 +2937,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
|
|
|
2645
2937
|
(contextmenu)="onColumnContextMenu($event)"
|
|
2646
2938
|
cdkDropList
|
|
2647
2939
|
[cdkDropListData]="node"
|
|
2648
|
-
[id]="node.id"
|
|
2940
|
+
[id]="getScopedNodeId(node.id)"
|
|
2649
2941
|
[cdkDropListConnectedTo]="connectedDropLists"
|
|
2650
2942
|
(cdkDropListDropped)="onDrop($event)"
|
|
2651
2943
|
[class.outline-dashed]="designMode"
|
|
@@ -2699,6 +2991,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
|
|
|
2699
2991
|
[engine]="engine"
|
|
2700
2992
|
[fields]="fields"
|
|
2701
2993
|
[designMode]="designMode"
|
|
2994
|
+
[scopePath]="scopePath"
|
|
2702
2995
|
[device]="device"
|
|
2703
2996
|
[breakpoint]="breakpoint"
|
|
2704
2997
|
[connectedDropLists]="connectedDropLists"
|
|
@@ -2774,7 +3067,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
|
|
|
2774
3067
|
</div>
|
|
2775
3068
|
|
|
2776
3069
|
<!-- Overlay for selection in design mode - only show on unselected widgets to allow interaction with selected ones -->
|
|
2777
|
-
<div *ngIf="designMode && !isSelected" class="absolute inset-0 cursor-pointer z-10 hover:bg-blue-500/5" (click)="onWidgetClick($event)"></div>
|
|
3070
|
+
<div *ngIf="designMode && !isSelected && !hasActiveNestedSelection()" class="absolute inset-0 cursor-pointer z-10 hover:bg-blue-500/5" (click)="onWidgetClick($event)"></div>
|
|
2778
3071
|
<div *ngIf="designMode && isSelected"
|
|
2779
3072
|
class="absolute bottom-0 left-0 right-0 h-3 cursor-row-resize bg-transparent hover:bg-blue-400/20 transition-colors z-50 group-hover:bg-blue-100/30"
|
|
2780
3073
|
(mousedown)="onResizeStart($event, node)"
|
|
@@ -2797,6 +3090,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
|
|
|
2797
3090
|
type: Input
|
|
2798
3091
|
}], designMode: [{
|
|
2799
3092
|
type: Input
|
|
3093
|
+
}], scopePath: [{
|
|
3094
|
+
type: Input
|
|
2800
3095
|
}], device: [{
|
|
2801
3096
|
type: Input
|
|
2802
3097
|
}], breakpoint: [{
|
|
@@ -3073,32 +3368,73 @@ class JsonFormRendererComponent {
|
|
|
3073
3368
|
return {};
|
|
3074
3369
|
const uploaded = {};
|
|
3075
3370
|
const schema = this.engine.getSchema();
|
|
3371
|
+
const values = this.engine.getValues();
|
|
3372
|
+
const changedTopLevelFields = new Set();
|
|
3373
|
+
await this.uploadPendingFilesInSchema({
|
|
3374
|
+
schema,
|
|
3375
|
+
valueScope: values,
|
|
3376
|
+
uploaded,
|
|
3377
|
+
markTopLevelChanged: (fieldName) => changedTopLevelFields.add(fieldName)
|
|
3378
|
+
});
|
|
3379
|
+
for (const fieldName of changedTopLevelFields) {
|
|
3380
|
+
this.engine.setValue(fieldName, values[fieldName]);
|
|
3381
|
+
}
|
|
3382
|
+
return uploaded;
|
|
3383
|
+
}
|
|
3384
|
+
async uploadPendingFilesInSchema(params) {
|
|
3385
|
+
const { schema, valueScope, uploaded, markTopLevelChanged, pathPrefix = '', topLevelFieldName } = params;
|
|
3076
3386
|
for (const field of schema.fields) {
|
|
3077
|
-
if (field
|
|
3387
|
+
if (!field?.name)
|
|
3078
3388
|
continue;
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3389
|
+
if (field.type === 'file') {
|
|
3390
|
+
const pendingFiles = this.getPendingFiles(valueScope[field.name]);
|
|
3391
|
+
if (pendingFiles.length === 0)
|
|
3392
|
+
continue;
|
|
3393
|
+
const endpoint = field.uploadUrl?.trim() || DEFAULT_UPLOAD_ENDPOINT;
|
|
3394
|
+
try {
|
|
3395
|
+
const refs = await this.uploadClient.upload(pendingFiles, {
|
|
3396
|
+
endpoint,
|
|
3397
|
+
fieldId: field.id,
|
|
3398
|
+
fieldName: field.name,
|
|
3399
|
+
fieldKey: field.uploadFieldName || undefined
|
|
3400
|
+
});
|
|
3401
|
+
if (refs.length > 0) {
|
|
3402
|
+
const storedValue = field.multiple ? refs : (refs[0] ?? null);
|
|
3403
|
+
valueScope[field.name] = storedValue;
|
|
3404
|
+
const uploadedKey = pathPrefix ? `${pathPrefix}.${field.id}` : field.id;
|
|
3405
|
+
uploaded[uploadedKey] = refs;
|
|
3406
|
+
markTopLevelChanged(topLevelFieldName ?? field.name);
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
catch (error) {
|
|
3410
|
+
console.warn('[ngx-form-designer] File upload failed', { fieldId: field.id, error });
|
|
3095
3411
|
}
|
|
3412
|
+
continue;
|
|
3096
3413
|
}
|
|
3097
|
-
|
|
3098
|
-
|
|
3414
|
+
if (field.type !== 'repeatable-group')
|
|
3415
|
+
continue;
|
|
3416
|
+
const itemSchema = field.repeatable?.itemSchema;
|
|
3417
|
+
if (!itemSchema || !Array.isArray(itemSchema.fields) || !itemSchema.layout)
|
|
3418
|
+
continue;
|
|
3419
|
+
const rawRows = valueScope[field.name];
|
|
3420
|
+
if (!Array.isArray(rawRows))
|
|
3421
|
+
continue;
|
|
3422
|
+
const nestedTopLevel = topLevelFieldName ?? field.name;
|
|
3423
|
+
for (let rowIndex = 0; rowIndex < rawRows.length; rowIndex += 1) {
|
|
3424
|
+
const rowValue = rawRows[rowIndex];
|
|
3425
|
+
if (!this.isObjectRecord(rowValue))
|
|
3426
|
+
continue;
|
|
3427
|
+
const rowPath = pathPrefix ? `${pathPrefix}.${field.id}[${rowIndex}]` : `${field.id}[${rowIndex}]`;
|
|
3428
|
+
await this.uploadPendingFilesInSchema({
|
|
3429
|
+
schema: itemSchema,
|
|
3430
|
+
valueScope: rowValue,
|
|
3431
|
+
uploaded,
|
|
3432
|
+
markTopLevelChanged,
|
|
3433
|
+
pathPrefix: rowPath,
|
|
3434
|
+
topLevelFieldName: nestedTopLevel
|
|
3435
|
+
});
|
|
3099
3436
|
}
|
|
3100
3437
|
}
|
|
3101
|
-
return uploaded;
|
|
3102
3438
|
}
|
|
3103
3439
|
getPendingFiles(value) {
|
|
3104
3440
|
if (this.isFile(value))
|
|
@@ -3117,16 +3453,42 @@ class JsonFormRendererComponent {
|
|
|
3117
3453
|
isFileList(value) {
|
|
3118
3454
|
return typeof FileList !== 'undefined' && value instanceof FileList;
|
|
3119
3455
|
}
|
|
3456
|
+
isObjectRecord(value) {
|
|
3457
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
3458
|
+
}
|
|
3120
3459
|
buildFieldValueMap(values) {
|
|
3121
3460
|
const schema = this.engine?.getSchema();
|
|
3122
3461
|
if (!schema)
|
|
3123
3462
|
return {};
|
|
3124
|
-
|
|
3463
|
+
return this.buildFieldValueMapForSchema(schema, values);
|
|
3464
|
+
}
|
|
3465
|
+
buildFieldValueMapForSchema(schema, valuesScope) {
|
|
3125
3466
|
const mapped = {};
|
|
3126
|
-
for (const
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3467
|
+
for (const field of schema.fields) {
|
|
3468
|
+
if (!field?.id || !field?.name)
|
|
3469
|
+
continue;
|
|
3470
|
+
const rawValue = valuesScope[field.name];
|
|
3471
|
+
if (field.type === 'repeatable-group') {
|
|
3472
|
+
const itemSchema = field.repeatable?.itemSchema;
|
|
3473
|
+
const rows = [];
|
|
3474
|
+
if (itemSchema && Array.isArray(rawValue)) {
|
|
3475
|
+
for (const row of rawValue) {
|
|
3476
|
+
if (!this.isObjectRecord(row)) {
|
|
3477
|
+
rows.push({});
|
|
3478
|
+
continue;
|
|
3479
|
+
}
|
|
3480
|
+
rows.push(this.buildFieldValueMapForSchema(itemSchema, row));
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3483
|
+
mapped[field.id] = {
|
|
3484
|
+
fieldName: field.name,
|
|
3485
|
+
value: rows
|
|
3486
|
+
};
|
|
3487
|
+
continue;
|
|
3488
|
+
}
|
|
3489
|
+
mapped[field.id] = {
|
|
3490
|
+
fieldName: field.name,
|
|
3491
|
+
value: rawValue
|
|
3130
3492
|
};
|
|
3131
3493
|
}
|
|
3132
3494
|
return mapped;
|
|
@@ -3143,9 +3505,9 @@ class JsonFormRendererComponent {
|
|
|
3143
3505
|
if (!grouped[groupKey]) {
|
|
3144
3506
|
grouped[groupKey] = {};
|
|
3145
3507
|
}
|
|
3146
|
-
const fieldValue = values[field.
|
|
3508
|
+
const fieldValue = values[field.id];
|
|
3147
3509
|
if (fieldValue !== undefined) {
|
|
3148
|
-
grouped[groupKey][field.
|
|
3510
|
+
grouped[groupKey][field.id] = fieldValue;
|
|
3149
3511
|
}
|
|
3150
3512
|
}
|
|
3151
3513
|
return grouped;
|
|
@@ -3159,14 +3521,14 @@ class JsonFormRendererComponent {
|
|
|
3159
3521
|
const groupKey = field.groupKey?.trim();
|
|
3160
3522
|
if (!groupKey)
|
|
3161
3523
|
continue;
|
|
3162
|
-
const fieldValue = values[field.
|
|
3524
|
+
const fieldValue = values[field.id];
|
|
3163
3525
|
if (!fieldValue)
|
|
3164
3526
|
continue;
|
|
3165
3527
|
const currentGroup = combined[groupKey];
|
|
3166
3528
|
const groupValues = this.isGroupValue(currentGroup) ? currentGroup : {};
|
|
3167
|
-
groupValues[field.
|
|
3529
|
+
groupValues[field.id] = fieldValue;
|
|
3168
3530
|
combined[groupKey] = groupValues;
|
|
3169
|
-
delete combined[field.
|
|
3531
|
+
delete combined[field.id];
|
|
3170
3532
|
}
|
|
3171
3533
|
return combined;
|
|
3172
3534
|
}
|
|
@@ -3174,7 +3536,8 @@ class JsonFormRendererComponent {
|
|
|
3174
3536
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
3175
3537
|
return false;
|
|
3176
3538
|
const record = value;
|
|
3177
|
-
return typeof record['
|
|
3539
|
+
return typeof record['fieldName'] === 'string'
|
|
3540
|
+
&& Object.hasOwn(record, 'value');
|
|
3178
3541
|
}
|
|
3179
3542
|
isGroupValue(value) {
|
|
3180
3543
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
@@ -3202,7 +3565,7 @@ class JsonFormRendererComponent {
|
|
|
3202
3565
|
</ng-container>
|
|
3203
3566
|
<ng-template #loading>Loading form...</ng-template>
|
|
3204
3567
|
</div>
|
|
3205
|
-
`, isInline: true, styles: [".form-renderer-container{width:100%;height:100%;overflow-y:auto}.form-renderer-container.is-design{padding-top:2.25rem;height:auto;overflow:visible}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: LayoutNodeComponent, selector: "app-layout-node", inputs: ["node", "engine", "fields", "designMode", "device", "breakpoint", "connectedDropLists"], outputs: ["nodeDrop", "nodeSelect"] }] });
|
|
3568
|
+
`, isInline: true, styles: [".form-renderer-container{width:100%;height:100%;overflow-y:auto}.form-renderer-container.is-design{padding-top:2.25rem;height:auto;overflow:visible}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: LayoutNodeComponent, selector: "app-layout-node", inputs: ["node", "engine", "fields", "designMode", "scopePath", "device", "breakpoint", "connectedDropLists"], outputs: ["nodeDrop", "nodeSelect"] }] });
|
|
3206
3569
|
}
|
|
3207
3570
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: JsonFormRendererComponent, decorators: [{
|
|
3208
3571
|
type: Component,
|
|
@@ -3975,6 +4338,7 @@ class FieldPaletteComponent {
|
|
|
3975
4338
|
getDragData(widget) {
|
|
3976
4339
|
return {
|
|
3977
4340
|
type: 'new-widget',
|
|
4341
|
+
widgetId: widget.id,
|
|
3978
4342
|
widgetType: widget.type,
|
|
3979
4343
|
widgetKind: widget.kind
|
|
3980
4344
|
};
|
|
@@ -6554,7 +6918,7 @@ class DynamicPropertiesComponent {
|
|
|
6554
6918
|
return sections;
|
|
6555
6919
|
}
|
|
6556
6920
|
getFieldReferenceCandidates(field) {
|
|
6557
|
-
const all = this.
|
|
6921
|
+
const all = this.getAllFields();
|
|
6558
6922
|
const allowed = field.allowedFieldTypes;
|
|
6559
6923
|
const includeSelf = field.includeSelf === true;
|
|
6560
6924
|
const currentId = this.config?.id;
|
|
@@ -12932,7 +13296,7 @@ class WidgetInspectorComponent {
|
|
|
12932
13296
|
<div class="flex flex-col min-h-0 bg-white">
|
|
12933
13297
|
<app-dynamic-properties
|
|
12934
13298
|
[config]="field()"
|
|
12935
|
-
[allFields]="stateService.
|
|
13299
|
+
[allFields]="stateService.getSelectedScopeFields()"
|
|
12936
13300
|
[excludeSections]="['Layout', 'Spacing', 'Size', 'Typography', 'Appearance', 'Box Model', 'Position']"
|
|
12937
13301
|
(configChange)="fieldChange.emit($event)">
|
|
12938
13302
|
</app-dynamic-properties>
|
|
@@ -12944,7 +13308,7 @@ class WidgetInspectorComponent {
|
|
|
12944
13308
|
<div class="flex flex-col min-h-0 bg-white p-1">
|
|
12945
13309
|
<app-data-panel
|
|
12946
13310
|
[config]="field()"
|
|
12947
|
-
[allFields]="stateService.
|
|
13311
|
+
[allFields]="stateService.getSelectedScopeFields()"
|
|
12948
13312
|
[dataConsumer]="dataConsumer()"
|
|
12949
13313
|
[bindingShape]="bindingShape()"
|
|
12950
13314
|
[widgetType]="field().type"
|
|
@@ -12958,7 +13322,7 @@ class WidgetInspectorComponent {
|
|
|
12958
13322
|
<div class="flex flex-col min-h-0 bg-white">
|
|
12959
13323
|
<app-rules-panel
|
|
12960
13324
|
[rules]="field().rules || []"
|
|
12961
|
-
[allFields]="stateService.
|
|
13325
|
+
[allFields]="stateService.getSelectedScopeFields()"
|
|
12962
13326
|
(rulesChange)="rulesChange.emit($event)">
|
|
12963
13327
|
</app-rules-panel>
|
|
12964
13328
|
</div>
|
|
@@ -12969,7 +13333,7 @@ class WidgetInspectorComponent {
|
|
|
12969
13333
|
<div class="flex flex-col min-h-0 bg-white">
|
|
12970
13334
|
<app-events-panel
|
|
12971
13335
|
[config]="field()"
|
|
12972
|
-
[allFields]="stateService.
|
|
13336
|
+
[allFields]="stateService.getSelectedScopeFields()"
|
|
12973
13337
|
(configChange)="fieldChange.emit($event)">
|
|
12974
13338
|
</app-events-panel>
|
|
12975
13339
|
</div>
|
|
@@ -13155,7 +13519,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
|
|
|
13155
13519
|
<div class="flex flex-col min-h-0 bg-white">
|
|
13156
13520
|
<app-dynamic-properties
|
|
13157
13521
|
[config]="field()"
|
|
13158
|
-
[allFields]="stateService.
|
|
13522
|
+
[allFields]="stateService.getSelectedScopeFields()"
|
|
13159
13523
|
[excludeSections]="['Layout', 'Spacing', 'Size', 'Typography', 'Appearance', 'Box Model', 'Position']"
|
|
13160
13524
|
(configChange)="fieldChange.emit($event)">
|
|
13161
13525
|
</app-dynamic-properties>
|
|
@@ -13167,7 +13531,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
|
|
|
13167
13531
|
<div class="flex flex-col min-h-0 bg-white p-1">
|
|
13168
13532
|
<app-data-panel
|
|
13169
13533
|
[config]="field()"
|
|
13170
|
-
[allFields]="stateService.
|
|
13534
|
+
[allFields]="stateService.getSelectedScopeFields()"
|
|
13171
13535
|
[dataConsumer]="dataConsumer()"
|
|
13172
13536
|
[bindingShape]="bindingShape()"
|
|
13173
13537
|
[widgetType]="field().type"
|
|
@@ -13181,7 +13545,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
|
|
|
13181
13545
|
<div class="flex flex-col min-h-0 bg-white">
|
|
13182
13546
|
<app-rules-panel
|
|
13183
13547
|
[rules]="field().rules || []"
|
|
13184
|
-
[allFields]="stateService.
|
|
13548
|
+
[allFields]="stateService.getSelectedScopeFields()"
|
|
13185
13549
|
(rulesChange)="rulesChange.emit($event)">
|
|
13186
13550
|
</app-rules-panel>
|
|
13187
13551
|
</div>
|
|
@@ -13192,7 +13556,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
|
|
|
13192
13556
|
<div class="flex flex-col min-h-0 bg-white">
|
|
13193
13557
|
<app-events-panel
|
|
13194
13558
|
[config]="field()"
|
|
13195
|
-
[allFields]="stateService.
|
|
13559
|
+
[allFields]="stateService.getSelectedScopeFields()"
|
|
13196
13560
|
(configChange)="fieldChange.emit($event)">
|
|
13197
13561
|
</app-events-panel>
|
|
13198
13562
|
</div>
|
|
@@ -13735,7 +14099,8 @@ class PropertiesPanelComponent {
|
|
|
13735
14099
|
return;
|
|
13736
14100
|
const currentStyle = { ...(node.style || {}) };
|
|
13737
14101
|
currentStyle[key] = value;
|
|
13738
|
-
this.state.
|
|
14102
|
+
const scopedNodeId = this.state.selectedNodeId() ?? nodeId;
|
|
14103
|
+
this.state.updateNodeStyle(scopedNodeId, currentStyle);
|
|
13739
14104
|
}
|
|
13740
14105
|
/**
|
|
13741
14106
|
* Handle style changes from the widget inspector.
|
|
@@ -13748,7 +14113,8 @@ class PropertiesPanelComponent {
|
|
|
13748
14113
|
// For now, we only support container styling (node.style)
|
|
13749
14114
|
// Widget-specific styling would require extending the FieldSchema
|
|
13750
14115
|
if (event.target === 'container') {
|
|
13751
|
-
this.state.
|
|
14116
|
+
const scopedNodeId = this.state.selectedNodeId() ?? node.id;
|
|
14117
|
+
this.state.updateNodeStyle(scopedNodeId, event.style);
|
|
13752
14118
|
}
|
|
13753
14119
|
}
|
|
13754
14120
|
/**
|
|
@@ -13767,7 +14133,8 @@ class PropertiesPanelComponent {
|
|
|
13767
14133
|
else {
|
|
13768
14134
|
currentResponsive[breakpoint] = Number(value);
|
|
13769
14135
|
}
|
|
13770
|
-
this.state.
|
|
14136
|
+
const scopedNodeId = this.state.selectedNodeId() ?? nodeId;
|
|
14137
|
+
this.state.updateNodeResponsive(scopedNodeId, currentResponsive);
|
|
13771
14138
|
}
|
|
13772
14139
|
/**
|
|
13773
14140
|
* Legacy method for backward compatibility with dynamic properties.
|
|
@@ -13832,7 +14199,7 @@ class PropertiesPanelComponent {
|
|
|
13832
14199
|
// Fallback to Data-Driven Properties
|
|
13833
14200
|
const ref = this.inspectorContainer.createComponent(DynamicPropertiesComponent);
|
|
13834
14201
|
ref.setInput('config', field);
|
|
13835
|
-
ref.setInput('allFields', this.state.
|
|
14202
|
+
ref.setInput('allFields', this.state.getSelectedScopeFields());
|
|
13836
14203
|
ref.setInput('readOnly', this.state.isReadOnly());
|
|
13837
14204
|
if (this.isBrickWidget(node)) {
|
|
13838
14205
|
ref.setInput('excludeSections', ['Brick']);
|
|
@@ -13898,13 +14265,13 @@ class PropertiesPanelComponent {
|
|
|
13898
14265
|
return def?.dataBinding || null;
|
|
13899
14266
|
}
|
|
13900
14267
|
applyPreset(rowId, preset) {
|
|
13901
|
-
this.state.setPresetLayout(rowId, preset);
|
|
14268
|
+
this.state.setPresetLayout(this.state.selectedNodeId() ?? rowId, preset);
|
|
13902
14269
|
}
|
|
13903
14270
|
addColumn(rowId) {
|
|
13904
|
-
this.state.addColumn(rowId);
|
|
14271
|
+
this.state.addColumn(this.state.selectedNodeId() ?? rowId);
|
|
13905
14272
|
}
|
|
13906
14273
|
removeColumn(colId) {
|
|
13907
|
-
this.state.removeColumn(colId);
|
|
14274
|
+
this.state.removeColumn(this.state.selectedNodeId() ?? colId);
|
|
13908
14275
|
// Deselect because it's gone
|
|
13909
14276
|
this.state.selectNode(null);
|
|
13910
14277
|
}
|
|
@@ -23415,6 +23782,484 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
|
|
|
23415
23782
|
type: Input
|
|
23416
23783
|
}] } });
|
|
23417
23784
|
|
|
23785
|
+
class RepeatableGroupWidgetComponent {
|
|
23786
|
+
config;
|
|
23787
|
+
engine;
|
|
23788
|
+
control = new FormControl();
|
|
23789
|
+
scopePath = [];
|
|
23790
|
+
rowStates = [];
|
|
23791
|
+
itemSchema = createEmptySchema('form', { title: 'Repeatable Item' });
|
|
23792
|
+
designPreviewEngine;
|
|
23793
|
+
parentValueSub;
|
|
23794
|
+
parentSubmitSub;
|
|
23795
|
+
rowIdCounter = 0;
|
|
23796
|
+
isWritingToParent = false;
|
|
23797
|
+
lastCommittedRowsRef;
|
|
23798
|
+
editorContext = inject(WIDGET_EDITOR_CONTEXT, { optional: true });
|
|
23799
|
+
designerState = inject(DesignerStateService);
|
|
23800
|
+
get groupId() {
|
|
23801
|
+
return `field-${this.config.id}`;
|
|
23802
|
+
}
|
|
23803
|
+
get isDesignMode() {
|
|
23804
|
+
return this.editorContext?.isDesignMode() ?? false;
|
|
23805
|
+
}
|
|
23806
|
+
get isSelected() {
|
|
23807
|
+
return this.editorContext?.isSelected() ?? false;
|
|
23808
|
+
}
|
|
23809
|
+
get nestedScopePath() {
|
|
23810
|
+
return [...this.scopePath, this.config.id];
|
|
23811
|
+
}
|
|
23812
|
+
get addButtonLabel() {
|
|
23813
|
+
const label = this.config.repeatable?.addButtonLabel?.trim();
|
|
23814
|
+
return label || 'Add item';
|
|
23815
|
+
}
|
|
23816
|
+
get removeButtonLabel() {
|
|
23817
|
+
const label = this.config.repeatable?.removeButtonLabel?.trim();
|
|
23818
|
+
return label || 'Remove';
|
|
23819
|
+
}
|
|
23820
|
+
get itemLabel() {
|
|
23821
|
+
const label = this.config.repeatable?.itemLabel?.trim();
|
|
23822
|
+
return label || 'Item';
|
|
23823
|
+
}
|
|
23824
|
+
get minItems() {
|
|
23825
|
+
const raw = Number(this.config.repeatable?.minItems ?? 0);
|
|
23826
|
+
if (!Number.isFinite(raw) || raw < 0)
|
|
23827
|
+
return 0;
|
|
23828
|
+
return Math.floor(raw);
|
|
23829
|
+
}
|
|
23830
|
+
get maxItems() {
|
|
23831
|
+
const raw = this.config.repeatable?.maxItems;
|
|
23832
|
+
if (raw === undefined || raw === null)
|
|
23833
|
+
return Number.POSITIVE_INFINITY;
|
|
23834
|
+
const parsed = Number(raw);
|
|
23835
|
+
if (!Number.isFinite(parsed) || parsed < 0)
|
|
23836
|
+
return Number.POSITIVE_INFINITY;
|
|
23837
|
+
return Math.max(this.minItems, Math.floor(parsed));
|
|
23838
|
+
}
|
|
23839
|
+
get canAddItem() {
|
|
23840
|
+
if (!this.enabled)
|
|
23841
|
+
return false;
|
|
23842
|
+
return this.rowStates.length < this.maxItems;
|
|
23843
|
+
}
|
|
23844
|
+
get canRemoveItem() {
|
|
23845
|
+
if (!this.enabled)
|
|
23846
|
+
return false;
|
|
23847
|
+
return this.rowStates.length > this.minItems;
|
|
23848
|
+
}
|
|
23849
|
+
ngOnInit() {
|
|
23850
|
+
this.initialize();
|
|
23851
|
+
}
|
|
23852
|
+
ngOnChanges(changes) {
|
|
23853
|
+
if (changes['config'] || changes['engine']) {
|
|
23854
|
+
this.initialize();
|
|
23855
|
+
}
|
|
23856
|
+
}
|
|
23857
|
+
ngOnDestroy() {
|
|
23858
|
+
this.disposeParentSubscriptions();
|
|
23859
|
+
this.disposeRowStates();
|
|
23860
|
+
}
|
|
23861
|
+
addItem() {
|
|
23862
|
+
if (!this.canAddItem)
|
|
23863
|
+
return;
|
|
23864
|
+
const row = this.createDefaultRow(this.itemSchema);
|
|
23865
|
+
this.rowStates.push(this.createRowState(row));
|
|
23866
|
+
this.commitRowsToParent();
|
|
23867
|
+
}
|
|
23868
|
+
removeItem(index) {
|
|
23869
|
+
if (!this.canRemoveItem)
|
|
23870
|
+
return;
|
|
23871
|
+
const row = this.rowStates[index];
|
|
23872
|
+
if (!row)
|
|
23873
|
+
return;
|
|
23874
|
+
row.valueSub.unsubscribe();
|
|
23875
|
+
this.rowStates.splice(index, 1);
|
|
23876
|
+
this.commitRowsToParent();
|
|
23877
|
+
}
|
|
23878
|
+
trackByRowId(index, row) {
|
|
23879
|
+
return row.id;
|
|
23880
|
+
}
|
|
23881
|
+
getWrapperStyles() {
|
|
23882
|
+
if (!this.config?.style)
|
|
23883
|
+
return {};
|
|
23884
|
+
return mergeAndNormalize({
|
|
23885
|
+
width: '100%'
|
|
23886
|
+
}, this.config.style);
|
|
23887
|
+
}
|
|
23888
|
+
get visible() {
|
|
23889
|
+
return this.engine ? this.engine.isFieldVisible(this.config.id) : true;
|
|
23890
|
+
}
|
|
23891
|
+
get enabled() {
|
|
23892
|
+
if (this.config?.disabled)
|
|
23893
|
+
return false;
|
|
23894
|
+
return this.engine ? this.engine.isFieldEnabled(this.config.id) : true;
|
|
23895
|
+
}
|
|
23896
|
+
get required() {
|
|
23897
|
+
return this.engine ? this.engine.isFieldRequired(this.config.id) : !!this.config?.html5?.required;
|
|
23898
|
+
}
|
|
23899
|
+
onNestedNodeSelect(nodeId) {
|
|
23900
|
+
if (!this.isDesignMode)
|
|
23901
|
+
return;
|
|
23902
|
+
this.designerState.selectNode(nodeId);
|
|
23903
|
+
}
|
|
23904
|
+
initialize() {
|
|
23905
|
+
this.itemSchema = this.resolveItemSchema();
|
|
23906
|
+
this.buildDesignPreviewEngine();
|
|
23907
|
+
if (this.isDesignMode) {
|
|
23908
|
+
this.disposeParentSubscriptions();
|
|
23909
|
+
this.disposeRowStates();
|
|
23910
|
+
return;
|
|
23911
|
+
}
|
|
23912
|
+
this.rebuildRows(this.readRowsFromParent(), true);
|
|
23913
|
+
this.bindParentSubscriptions();
|
|
23914
|
+
}
|
|
23915
|
+
bindParentSubscriptions() {
|
|
23916
|
+
this.disposeParentSubscriptions();
|
|
23917
|
+
if (!this.engine)
|
|
23918
|
+
return;
|
|
23919
|
+
this.parentValueSub = this.engine.valueChanges$.subscribe(values => {
|
|
23920
|
+
if (!this.config?.name)
|
|
23921
|
+
return;
|
|
23922
|
+
if (this.isWritingToParent)
|
|
23923
|
+
return;
|
|
23924
|
+
const incoming = values[this.config.name];
|
|
23925
|
+
if (incoming === this.lastCommittedRowsRef)
|
|
23926
|
+
return;
|
|
23927
|
+
this.rebuildRows(this.normalizeRows(incoming), true);
|
|
23928
|
+
});
|
|
23929
|
+
this.parentSubmitSub = this.engine.submitted$.subscribe(() => {
|
|
23930
|
+
for (const row of this.rowStates) {
|
|
23931
|
+
row.engine.validate();
|
|
23932
|
+
row.engine.submit();
|
|
23933
|
+
}
|
|
23934
|
+
});
|
|
23935
|
+
}
|
|
23936
|
+
disposeParentSubscriptions() {
|
|
23937
|
+
this.parentValueSub?.unsubscribe();
|
|
23938
|
+
this.parentSubmitSub?.unsubscribe();
|
|
23939
|
+
this.parentValueSub = undefined;
|
|
23940
|
+
this.parentSubmitSub = undefined;
|
|
23941
|
+
}
|
|
23942
|
+
rebuildRows(sourceRows, enforceBounds) {
|
|
23943
|
+
this.disposeRowStates();
|
|
23944
|
+
const boundedRows = enforceBounds ? this.applyBounds(sourceRows) : sourceRows;
|
|
23945
|
+
this.rowStates = boundedRows.map(row => this.createRowState(row));
|
|
23946
|
+
if (!this.engine || !this.config?.name)
|
|
23947
|
+
return;
|
|
23948
|
+
const current = this.engine.getValue(this.config.name);
|
|
23949
|
+
if (current === boundedRows) {
|
|
23950
|
+
this.lastCommittedRowsRef = current;
|
|
23951
|
+
return;
|
|
23952
|
+
}
|
|
23953
|
+
if (!this.isSameLength(current, boundedRows)) {
|
|
23954
|
+
this.commitRowsToParent();
|
|
23955
|
+
return;
|
|
23956
|
+
}
|
|
23957
|
+
this.lastCommittedRowsRef = current;
|
|
23958
|
+
}
|
|
23959
|
+
createRowState(rowValue) {
|
|
23960
|
+
const rowEngine = createFormEngine(this.itemSchema, rowValue);
|
|
23961
|
+
const valueSub = rowEngine.valueChanges$.pipe(skip(1)).subscribe(() => {
|
|
23962
|
+
this.commitRowsToParent();
|
|
23963
|
+
});
|
|
23964
|
+
this.rowIdCounter += 1;
|
|
23965
|
+
return {
|
|
23966
|
+
id: `repeatable-row-${this.config.id}-${this.rowIdCounter}`,
|
|
23967
|
+
engine: rowEngine,
|
|
23968
|
+
valueSub
|
|
23969
|
+
};
|
|
23970
|
+
}
|
|
23971
|
+
disposeRowStates() {
|
|
23972
|
+
for (const row of this.rowStates) {
|
|
23973
|
+
row.valueSub.unsubscribe();
|
|
23974
|
+
}
|
|
23975
|
+
this.rowStates = [];
|
|
23976
|
+
}
|
|
23977
|
+
commitRowsToParent() {
|
|
23978
|
+
if (!this.engine || !this.config?.name)
|
|
23979
|
+
return;
|
|
23980
|
+
const nextRows = this.rowStates.map(row => row.engine.getValues());
|
|
23981
|
+
this.lastCommittedRowsRef = nextRows;
|
|
23982
|
+
this.isWritingToParent = true;
|
|
23983
|
+
this.engine.setValue(this.config.name, nextRows);
|
|
23984
|
+
this.isWritingToParent = false;
|
|
23985
|
+
}
|
|
23986
|
+
readRowsFromParent() {
|
|
23987
|
+
if (!this.engine || !this.config?.name) {
|
|
23988
|
+
return this.applyBounds([]);
|
|
23989
|
+
}
|
|
23990
|
+
const value = this.engine.getValue(this.config.name);
|
|
23991
|
+
return this.applyBounds(this.normalizeRows(value));
|
|
23992
|
+
}
|
|
23993
|
+
normalizeRows(value) {
|
|
23994
|
+
if (!Array.isArray(value))
|
|
23995
|
+
return [];
|
|
23996
|
+
return value.map(item => this.normalizeRow(item));
|
|
23997
|
+
}
|
|
23998
|
+
normalizeRow(value) {
|
|
23999
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
24000
|
+
return {};
|
|
24001
|
+
return { ...value };
|
|
24002
|
+
}
|
|
24003
|
+
applyBounds(rows) {
|
|
24004
|
+
const bounded = [...rows];
|
|
24005
|
+
if (bounded.length > this.maxItems) {
|
|
24006
|
+
bounded.length = this.maxItems;
|
|
24007
|
+
}
|
|
24008
|
+
while (bounded.length < this.minItems) {
|
|
24009
|
+
bounded.push(this.createDefaultRow(this.itemSchema));
|
|
24010
|
+
}
|
|
24011
|
+
return bounded;
|
|
24012
|
+
}
|
|
24013
|
+
createDefaultRow(schema) {
|
|
24014
|
+
const row = {};
|
|
24015
|
+
for (const field of schema.fields) {
|
|
24016
|
+
if (!field.name)
|
|
24017
|
+
continue;
|
|
24018
|
+
if (field.defaultValue !== undefined) {
|
|
24019
|
+
row[field.name] = this.cloneValue(field.defaultValue);
|
|
24020
|
+
}
|
|
24021
|
+
else {
|
|
24022
|
+
row[field.name] = null;
|
|
24023
|
+
}
|
|
24024
|
+
}
|
|
24025
|
+
return row;
|
|
24026
|
+
}
|
|
24027
|
+
resolveItemSchema() {
|
|
24028
|
+
const schema = this.config?.repeatable?.itemSchema;
|
|
24029
|
+
if (schema && this.isSchemaLike(schema)) {
|
|
24030
|
+
return schema;
|
|
24031
|
+
}
|
|
24032
|
+
return createEmptySchema('form', { title: 'Repeatable Item' });
|
|
24033
|
+
}
|
|
24034
|
+
isSchemaLike(value) {
|
|
24035
|
+
if (!value || typeof value !== 'object')
|
|
24036
|
+
return false;
|
|
24037
|
+
const candidate = value;
|
|
24038
|
+
return Array.isArray(candidate.fields) && !!candidate.layout;
|
|
24039
|
+
}
|
|
24040
|
+
isSameLength(left, right) {
|
|
24041
|
+
return Array.isArray(left) && left.length === right.length;
|
|
24042
|
+
}
|
|
24043
|
+
cloneValue(value) {
|
|
24044
|
+
if (typeof structuredClone === 'function') {
|
|
24045
|
+
return structuredClone(value);
|
|
24046
|
+
}
|
|
24047
|
+
try {
|
|
24048
|
+
return JSON.parse(JSON.stringify(value));
|
|
24049
|
+
}
|
|
24050
|
+
catch {
|
|
24051
|
+
return value;
|
|
24052
|
+
}
|
|
24053
|
+
}
|
|
24054
|
+
buildDesignPreviewEngine() {
|
|
24055
|
+
if (!this.itemSchema?.layout) {
|
|
24056
|
+
this.designPreviewEngine = undefined;
|
|
24057
|
+
return;
|
|
24058
|
+
}
|
|
24059
|
+
const previewRow = this.createDefaultRow(this.itemSchema);
|
|
24060
|
+
this.designPreviewEngine = createFormEngine(this.itemSchema, previewRow);
|
|
24061
|
+
}
|
|
24062
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: RepeatableGroupWidgetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
24063
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.17", type: RepeatableGroupWidgetComponent, isStandalone: true, selector: "app-repeatable-group-widget", inputs: { config: "config", engine: "engine", control: "control", scopePath: "scopePath" }, usesOnChanges: true, ngImport: i0, template: `
|
|
24064
|
+
<div class="w-full pb-4 font-sans" [class.hidden]="!visible" [ngStyle]="getWrapperStyles()">
|
|
24065
|
+
<div class="flex items-start justify-between gap-3 mb-2">
|
|
24066
|
+
<div class="min-w-0">
|
|
24067
|
+
<label [attr.for]="groupId" class="block text-sm font-medium text-gray-700">
|
|
24068
|
+
{{ config.label }} <span *ngIf="required" class="text-red-500" aria-label="required">*</span>
|
|
24069
|
+
</label>
|
|
24070
|
+
<p *ngIf="config.helpText" class="mt-1 text-xs text-gray-500">{{ config.helpText }}</p>
|
|
24071
|
+
</div>
|
|
24072
|
+
<div class="flex shrink-0 items-center gap-2">
|
|
24073
|
+
<button
|
|
24074
|
+
*ngIf="!isDesignMode"
|
|
24075
|
+
type="button"
|
|
24076
|
+
class="h-9 rounded-md border border-blue-200 px-3 text-xs font-semibold text-blue-700 transition-colors hover:bg-blue-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-40"
|
|
24077
|
+
[disabled]="!canAddItem"
|
|
24078
|
+
(click)="addItem()">
|
|
24079
|
+
{{ addButtonLabel }}
|
|
24080
|
+
</button>
|
|
24081
|
+
</div>
|
|
24082
|
+
</div>
|
|
24083
|
+
|
|
24084
|
+
<ng-container *ngIf="!isDesignMode; else designModeView">
|
|
24085
|
+
<div [id]="groupId" class="flex flex-col gap-3">
|
|
24086
|
+
<div *ngFor="let row of rowStates; let rowIndex = index; trackBy: trackByRowId"
|
|
24087
|
+
class="rounded-lg border border-gray-200 bg-white p-3">
|
|
24088
|
+
<div class="mb-3 flex items-center justify-between">
|
|
24089
|
+
<span class="text-xs font-semibold uppercase tracking-wide text-gray-500">
|
|
24090
|
+
{{ itemLabel }} {{ rowIndex + 1 }}
|
|
24091
|
+
</span>
|
|
24092
|
+
<button
|
|
24093
|
+
type="button"
|
|
24094
|
+
class="h-8 rounded-md border border-red-200 px-2.5 text-xs font-medium text-red-700 transition-colors hover:bg-red-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-40"
|
|
24095
|
+
[disabled]="!canRemoveItem"
|
|
24096
|
+
(click)="removeItem(rowIndex)">
|
|
24097
|
+
{{ removeButtonLabel }}
|
|
24098
|
+
</button>
|
|
24099
|
+
</div>
|
|
24100
|
+
|
|
24101
|
+
<app-layout-node
|
|
24102
|
+
*ngIf="itemSchema.layout"
|
|
24103
|
+
[node]="itemSchema.layout"
|
|
24104
|
+
[engine]="row.engine"
|
|
24105
|
+
[fields]="itemSchema.fields"
|
|
24106
|
+
[designMode]="false"
|
|
24107
|
+
[device]="'desktop'"
|
|
24108
|
+
[breakpoint]="'xl'">
|
|
24109
|
+
</app-layout-node>
|
|
24110
|
+
</div>
|
|
24111
|
+
</div>
|
|
24112
|
+
|
|
24113
|
+
<p *ngIf="rowStates.length === 0" class="rounded-md border border-dashed border-gray-300 bg-gray-50 px-3 py-4 text-xs text-gray-500">
|
|
24114
|
+
No items yet.
|
|
24115
|
+
</p>
|
|
24116
|
+
</ng-container>
|
|
24117
|
+
|
|
24118
|
+
<ng-template #designModeView>
|
|
24119
|
+
<div [id]="groupId" class="flex flex-col gap-3">
|
|
24120
|
+
<div class="rounded-lg border border-dashed border-gray-300 bg-gray-50 p-3">
|
|
24121
|
+
<div class="mb-2 flex items-center justify-between gap-2">
|
|
24122
|
+
<p class="text-xs font-semibold uppercase tracking-wide text-gray-500">Item Template</p>
|
|
24123
|
+
<span class="rounded bg-gray-200 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-gray-600">
|
|
24124
|
+
{{ itemSchema.fields.length }} fields
|
|
24125
|
+
</span>
|
|
24126
|
+
</div>
|
|
24127
|
+
<p class="mb-2 rounded-md border border-gray-200 bg-white px-3 py-2 text-xs text-gray-600">
|
|
24128
|
+
Select this group, then add fields from the main palette. New fields are added inside this group scope.
|
|
24129
|
+
</p>
|
|
24130
|
+
<p *ngIf="itemSchema.fields.length === 0" class="rounded-md border border-dashed border-gray-300 bg-white px-3 py-3 text-xs text-gray-500">
|
|
24131
|
+
No nested fields yet. You can still add rows/columns now.
|
|
24132
|
+
</p>
|
|
24133
|
+
<div class="rounded-md border border-gray-200 bg-white p-2"
|
|
24134
|
+
(click)="$event.stopPropagation()"
|
|
24135
|
+
(mousedown)="$event.stopPropagation()"
|
|
24136
|
+
(contextmenu)="$event.stopPropagation()">
|
|
24137
|
+
<app-layout-node
|
|
24138
|
+
*ngIf="itemSchema.layout && designPreviewEngine"
|
|
24139
|
+
[node]="itemSchema.layout"
|
|
24140
|
+
[engine]="designPreviewEngine"
|
|
24141
|
+
[fields]="itemSchema.fields"
|
|
24142
|
+
[designMode]="true"
|
|
24143
|
+
[scopePath]="nestedScopePath"
|
|
24144
|
+
[device]="'desktop'"
|
|
24145
|
+
[breakpoint]="'xl'"
|
|
24146
|
+
(nodeSelect)="onNestedNodeSelect($event)">
|
|
24147
|
+
</app-layout-node>
|
|
24148
|
+
</div>
|
|
24149
|
+
</div>
|
|
24150
|
+
</div>
|
|
24151
|
+
</ng-template>
|
|
24152
|
+
</div>
|
|
24153
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: LayoutNodeComponent, selector: "app-layout-node", inputs: ["node", "engine", "fields", "designMode", "scopePath", "device", "breakpoint", "connectedDropLists"], outputs: ["nodeDrop", "nodeSelect"] }] });
|
|
24154
|
+
}
|
|
24155
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: RepeatableGroupWidgetComponent, decorators: [{
|
|
24156
|
+
type: Component,
|
|
24157
|
+
args: [{
|
|
24158
|
+
selector: 'app-repeatable-group-widget',
|
|
24159
|
+
standalone: true,
|
|
24160
|
+
imports: [CommonModule, LayoutNodeComponent],
|
|
24161
|
+
template: `
|
|
24162
|
+
<div class="w-full pb-4 font-sans" [class.hidden]="!visible" [ngStyle]="getWrapperStyles()">
|
|
24163
|
+
<div class="flex items-start justify-between gap-3 mb-2">
|
|
24164
|
+
<div class="min-w-0">
|
|
24165
|
+
<label [attr.for]="groupId" class="block text-sm font-medium text-gray-700">
|
|
24166
|
+
{{ config.label }} <span *ngIf="required" class="text-red-500" aria-label="required">*</span>
|
|
24167
|
+
</label>
|
|
24168
|
+
<p *ngIf="config.helpText" class="mt-1 text-xs text-gray-500">{{ config.helpText }}</p>
|
|
24169
|
+
</div>
|
|
24170
|
+
<div class="flex shrink-0 items-center gap-2">
|
|
24171
|
+
<button
|
|
24172
|
+
*ngIf="!isDesignMode"
|
|
24173
|
+
type="button"
|
|
24174
|
+
class="h-9 rounded-md border border-blue-200 px-3 text-xs font-semibold text-blue-700 transition-colors hover:bg-blue-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-40"
|
|
24175
|
+
[disabled]="!canAddItem"
|
|
24176
|
+
(click)="addItem()">
|
|
24177
|
+
{{ addButtonLabel }}
|
|
24178
|
+
</button>
|
|
24179
|
+
</div>
|
|
24180
|
+
</div>
|
|
24181
|
+
|
|
24182
|
+
<ng-container *ngIf="!isDesignMode; else designModeView">
|
|
24183
|
+
<div [id]="groupId" class="flex flex-col gap-3">
|
|
24184
|
+
<div *ngFor="let row of rowStates; let rowIndex = index; trackBy: trackByRowId"
|
|
24185
|
+
class="rounded-lg border border-gray-200 bg-white p-3">
|
|
24186
|
+
<div class="mb-3 flex items-center justify-between">
|
|
24187
|
+
<span class="text-xs font-semibold uppercase tracking-wide text-gray-500">
|
|
24188
|
+
{{ itemLabel }} {{ rowIndex + 1 }}
|
|
24189
|
+
</span>
|
|
24190
|
+
<button
|
|
24191
|
+
type="button"
|
|
24192
|
+
class="h-8 rounded-md border border-red-200 px-2.5 text-xs font-medium text-red-700 transition-colors hover:bg-red-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-40"
|
|
24193
|
+
[disabled]="!canRemoveItem"
|
|
24194
|
+
(click)="removeItem(rowIndex)">
|
|
24195
|
+
{{ removeButtonLabel }}
|
|
24196
|
+
</button>
|
|
24197
|
+
</div>
|
|
24198
|
+
|
|
24199
|
+
<app-layout-node
|
|
24200
|
+
*ngIf="itemSchema.layout"
|
|
24201
|
+
[node]="itemSchema.layout"
|
|
24202
|
+
[engine]="row.engine"
|
|
24203
|
+
[fields]="itemSchema.fields"
|
|
24204
|
+
[designMode]="false"
|
|
24205
|
+
[device]="'desktop'"
|
|
24206
|
+
[breakpoint]="'xl'">
|
|
24207
|
+
</app-layout-node>
|
|
24208
|
+
</div>
|
|
24209
|
+
</div>
|
|
24210
|
+
|
|
24211
|
+
<p *ngIf="rowStates.length === 0" class="rounded-md border border-dashed border-gray-300 bg-gray-50 px-3 py-4 text-xs text-gray-500">
|
|
24212
|
+
No items yet.
|
|
24213
|
+
</p>
|
|
24214
|
+
</ng-container>
|
|
24215
|
+
|
|
24216
|
+
<ng-template #designModeView>
|
|
24217
|
+
<div [id]="groupId" class="flex flex-col gap-3">
|
|
24218
|
+
<div class="rounded-lg border border-dashed border-gray-300 bg-gray-50 p-3">
|
|
24219
|
+
<div class="mb-2 flex items-center justify-between gap-2">
|
|
24220
|
+
<p class="text-xs font-semibold uppercase tracking-wide text-gray-500">Item Template</p>
|
|
24221
|
+
<span class="rounded bg-gray-200 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-gray-600">
|
|
24222
|
+
{{ itemSchema.fields.length }} fields
|
|
24223
|
+
</span>
|
|
24224
|
+
</div>
|
|
24225
|
+
<p class="mb-2 rounded-md border border-gray-200 bg-white px-3 py-2 text-xs text-gray-600">
|
|
24226
|
+
Select this group, then add fields from the main palette. New fields are added inside this group scope.
|
|
24227
|
+
</p>
|
|
24228
|
+
<p *ngIf="itemSchema.fields.length === 0" class="rounded-md border border-dashed border-gray-300 bg-white px-3 py-3 text-xs text-gray-500">
|
|
24229
|
+
No nested fields yet. You can still add rows/columns now.
|
|
24230
|
+
</p>
|
|
24231
|
+
<div class="rounded-md border border-gray-200 bg-white p-2"
|
|
24232
|
+
(click)="$event.stopPropagation()"
|
|
24233
|
+
(mousedown)="$event.stopPropagation()"
|
|
24234
|
+
(contextmenu)="$event.stopPropagation()">
|
|
24235
|
+
<app-layout-node
|
|
24236
|
+
*ngIf="itemSchema.layout && designPreviewEngine"
|
|
24237
|
+
[node]="itemSchema.layout"
|
|
24238
|
+
[engine]="designPreviewEngine"
|
|
24239
|
+
[fields]="itemSchema.fields"
|
|
24240
|
+
[designMode]="true"
|
|
24241
|
+
[scopePath]="nestedScopePath"
|
|
24242
|
+
[device]="'desktop'"
|
|
24243
|
+
[breakpoint]="'xl'"
|
|
24244
|
+
(nodeSelect)="onNestedNodeSelect($event)">
|
|
24245
|
+
</app-layout-node>
|
|
24246
|
+
</div>
|
|
24247
|
+
</div>
|
|
24248
|
+
</div>
|
|
24249
|
+
</ng-template>
|
|
24250
|
+
</div>
|
|
24251
|
+
`
|
|
24252
|
+
}]
|
|
24253
|
+
}], propDecorators: { config: [{
|
|
24254
|
+
type: Input
|
|
24255
|
+
}], engine: [{
|
|
24256
|
+
type: Input
|
|
24257
|
+
}], control: [{
|
|
24258
|
+
type: Input
|
|
24259
|
+
}], scopePath: [{
|
|
24260
|
+
type: Input
|
|
24261
|
+
}] } });
|
|
24262
|
+
|
|
23418
24263
|
class ButtonWidgetComponent {
|
|
23419
24264
|
router = inject(Router, { optional: true });
|
|
23420
24265
|
pageLinks = inject(WIDGET_PAGE_LINKS, { optional: true });
|
|
@@ -24164,6 +25009,36 @@ const FILE_WIDGET_PROPERTIES = [
|
|
|
24164
25009
|
{ label: 'Appearance', fields: COMMON_APPEARANCE_FIELDS },
|
|
24165
25010
|
...COMMON_STYLE_SECTIONS
|
|
24166
25011
|
];
|
|
25012
|
+
const REPEATABLE_WIDGET_PROPERTIES = [
|
|
25013
|
+
{
|
|
25014
|
+
label: 'Basic',
|
|
25015
|
+
fields: [
|
|
25016
|
+
{ key: 'name', type: 'text', label: 'Field Key (Name)' },
|
|
25017
|
+
{ key: 'label', type: 'text', label: 'Label' },
|
|
25018
|
+
{ key: 'helpText', type: 'text', label: 'Help Text' },
|
|
25019
|
+
{ key: 'tooltip', type: 'text', label: 'Tooltip' },
|
|
25020
|
+
...COMMON_GROUP_FIELDS
|
|
25021
|
+
]
|
|
25022
|
+
},
|
|
25023
|
+
{
|
|
25024
|
+
label: 'Repeatable',
|
|
25025
|
+
fields: [
|
|
25026
|
+
{ key: 'repeatable.minItems', type: 'number', label: 'Minimum Items', min: 0 },
|
|
25027
|
+
{ key: 'repeatable.maxItems', type: 'number', label: 'Maximum Items', min: 1 },
|
|
25028
|
+
{ key: 'repeatable.itemLabel', type: 'text', label: 'Item Label', placeholder: 'Item' },
|
|
25029
|
+
{ key: 'repeatable.addButtonLabel', type: 'text', label: 'Add Button Label', placeholder: 'Add item' },
|
|
25030
|
+
{ key: 'repeatable.removeButtonLabel', type: 'text', label: 'Remove Button Label', placeholder: 'Remove' }
|
|
25031
|
+
]
|
|
25032
|
+
},
|
|
25033
|
+
{
|
|
25034
|
+
label: 'Validation',
|
|
25035
|
+
fields: [
|
|
25036
|
+
BASE_REQUIRED_FIELD
|
|
25037
|
+
]
|
|
25038
|
+
},
|
|
25039
|
+
{ label: 'Appearance', fields: COMMON_APPEARANCE_FIELDS },
|
|
25040
|
+
...COMMON_STYLE_SECTIONS
|
|
25041
|
+
];
|
|
24167
25042
|
const pluginId$2 = 'core.form';
|
|
24168
25043
|
const FIELD_WIDGETS = [
|
|
24169
25044
|
defineWidget(pluginId$2, {
|
|
@@ -24470,6 +25345,33 @@ const FIELD_WIDGETS = [
|
|
|
24470
25345
|
dataConsumer: 'none',
|
|
24471
25346
|
properties: FILE_WIDGET_PROPERTIES
|
|
24472
25347
|
}),
|
|
25348
|
+
defineWidget(pluginId$2, {
|
|
25349
|
+
kind: 'field',
|
|
25350
|
+
flavor: 'form',
|
|
25351
|
+
type: 'repeatable-group',
|
|
25352
|
+
icon: 'list-plus',
|
|
25353
|
+
label: 'Repeatable Group',
|
|
25354
|
+
createConfig: () => ({
|
|
25355
|
+
id: generateId$2(),
|
|
25356
|
+
name: 'group_' + Date.now(),
|
|
25357
|
+
type: 'repeatable-group',
|
|
25358
|
+
label: 'Repeatable Group',
|
|
25359
|
+
helpText: '',
|
|
25360
|
+
defaultValue: [],
|
|
25361
|
+
repeatable: {
|
|
25362
|
+
minItems: 1,
|
|
25363
|
+
maxItems: 5,
|
|
25364
|
+
itemLabel: 'Item',
|
|
25365
|
+
addButtonLabel: 'Add item',
|
|
25366
|
+
removeButtonLabel: 'Remove',
|
|
25367
|
+
itemSchema: createEmptySchema('form', { title: 'Repeatable Item' })
|
|
25368
|
+
},
|
|
25369
|
+
style: { width: '100%' }
|
|
25370
|
+
}),
|
|
25371
|
+
renderer: RepeatableGroupWidgetComponent,
|
|
25372
|
+
dataConsumer: 'none',
|
|
25373
|
+
properties: REPEATABLE_WIDGET_PROPERTIES
|
|
25374
|
+
}),
|
|
24473
25375
|
defineWidget(pluginId$2, {
|
|
24474
25376
|
kind: 'field',
|
|
24475
25377
|
flavor: 'form',
|