@dmitryvim/form-builder 0.1.37 → 0.1.39

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/README.md CHANGED
@@ -105,9 +105,9 @@ npm install @dmitryvim/form-builder
105
105
  "extensions": ["jpg", "png", "webp"]
106
106
  },
107
107
  "actions": [
108
- { "value": "enhance", "label": "Enhance Quality" },
109
- { "value": "crop", "label": "Auto Crop" },
110
- { "value": "retry", "label": "Try Again" }
108
+ { "key": "enhance", "label": "Enhance Quality" },
109
+ { "key": "crop", "label": "Auto Crop" },
110
+ { "key": "retry", "label": "Try Again" }
111
111
  ]
112
112
  },
113
113
  {
@@ -152,9 +152,14 @@ FormBuilder.setUploadHandler(async (file) => {
152
152
  // Your upload logic - return resource ID
153
153
  return "resource-123";
154
154
  });
155
- FormBuilder.setActionHandler((value) => {
155
+ FormBuilder.setActionHandler((value, key, relatedField) => {
156
156
  // Handle action button clicks
157
- console.log("Action clicked:", value);
157
+ console.log("Action clicked:", { value, key, relatedField });
158
+ if (relatedField) {
159
+ // Field-level action
160
+ } else {
161
+ // Form-level action
162
+ }
158
163
  });
159
164
  FormBuilder.renderForm(schema, prefillData);
160
165
  ```
package/dist/demo.js CHANGED
@@ -5,6 +5,16 @@
5
5
  const EXAMPLE_SCHEMA = {
6
6
  version: "0.3",
7
7
  title: "Asset Uploader with Slides",
8
+ actions: [
9
+ {
10
+ key: "test-missing",
11
+ label: "Другая потеря"
12
+ },
13
+ {
14
+ key: "save-draft",
15
+ label: "Сохранить черновик"
16
+ }
17
+ ],
8
18
  elements: [
9
19
  {
10
20
  type: "file",
@@ -19,9 +29,9 @@ const EXAMPLE_SCHEMA = {
19
29
  },
20
30
  maxSizeMB: 10,
21
31
  actions: [
22
- { value: "cover1.retry", label: "А давай ещё разок" },
23
- { value: "cover1.enhance", label: "Улучшить качество" },
24
- { value: "cover1.crop", label: "Обрезать изображение" },
32
+ { key: "retry", label: "А давай ещё разок" },
33
+ { key: "enhance", label: "Улучшить качество" },
34
+ { key: "crop", label: "Обрезать изображение" },
25
35
  ],
26
36
  },
27
37
  {
@@ -323,38 +333,6 @@ class InMemoryFileStorage {
323
333
  // Initialize file storage
324
334
  const fileStorage = new InMemoryFileStorage();
325
335
 
326
- // Cache for action value -> label mapping for efficient lookup
327
- let actionLabelMap = new Map();
328
-
329
- // Build action value -> label mapping from schema for efficient lookup
330
- function buildActionLabelMap(schema) {
331
- const map = new Map();
332
-
333
- function processElements(elements) {
334
- if (!Array.isArray(elements)) return;
335
-
336
- for (const element of elements) {
337
- if (element.actions && Array.isArray(element.actions)) {
338
- for (const action of element.actions) {
339
- if (action.value && action.label) {
340
- map.set(action.value, action.label);
341
- }
342
- }
343
- }
344
-
345
- // Process nested group elements
346
- if (element.elements && Array.isArray(element.elements)) {
347
- processElements(element.elements);
348
- }
349
- }
350
- }
351
-
352
- if (schema && schema.elements) {
353
- processElements(schema.elements);
354
- }
355
-
356
- return map;
357
- }
358
336
 
359
337
  // DOM element references
360
338
  const el = {
@@ -429,16 +407,21 @@ function parseActions(actionsText) {
429
407
  if (!action || typeof action !== "object") {
430
408
  throw new Error(`Action at index ${i} must be an object`);
431
409
  }
432
- if (!action.related_field || typeof action.related_field !== "string") {
410
+ if (!action.key || typeof action.key !== "string") {
433
411
  throw new Error(
434
- `Action at index ${i} missing valid 'related_field' property`,
412
+ `Action at index ${i} missing valid 'key' property`,
435
413
  );
436
414
  }
437
415
  if (!action.value || typeof action.value !== "string") {
438
416
  throw new Error(`Action at index ${i} missing valid 'value' property`);
439
417
  }
440
- if (!action.label || typeof action.label !== "string") {
441
- throw new Error(`Action at index ${i} missing valid 'label' property`);
418
+ // related_field is optional - if not provided, action is form-level
419
+ if (action.related_field && typeof action.related_field !== "string") {
420
+ throw new Error(`Action at index ${i} has invalid 'related_field' property type`);
421
+ }
422
+ // Label is optional - will be resolved from schema or key
423
+ if (action.label && typeof action.label !== "string") {
424
+ throw new Error(`Action at index ${i} has invalid 'label' property type`);
442
425
  }
443
426
  }
444
427
 
@@ -480,43 +463,28 @@ function setupFormBuilder() {
480
463
  });
481
464
 
482
465
  // Action handler - display message when action button is clicked
483
- // Updated to support both old system (1 param) and new system (2 params)
484
- FormBuilder.setActionHandler((relatedFieldOrValue, value) => {
485
- let actionLabel, actionValue, relatedField;
486
-
487
- // Determine if this is the old system (1 param) or new system (2 params)
488
- if (arguments.length === 1) {
489
- // Old system: only value parameter
490
- actionValue = relatedFieldOrValue;
491
- actionLabel = actionLabelMap.get(actionValue) || actionValue;
492
- relatedField = null;
493
- } else {
494
- // New system: related_field and value parameters
495
- relatedField = relatedFieldOrValue;
496
- actionValue = value;
497
- actionLabel = `Action for ${relatedField}`;
498
- }
499
-
466
+ // New system: value, key, related_field parameters
467
+ FormBuilder.setActionHandler((value, key, relatedField) => {
500
468
  console.log("Action clicked:", {
501
- label: actionLabel,
502
- value: actionValue,
469
+ value,
470
+ key,
503
471
  relatedField,
504
- system: arguments.length === 1 ? "schema-based" : "external",
472
+ type: relatedField ? "field-level" : "form-level",
505
473
  });
506
474
 
507
475
  // Show message to user (compatible with all environments)
508
476
  if (typeof window !== "undefined" && window.alert) {
509
477
  if (relatedField) {
510
478
  window.alert(
511
- `External Action: "${actionLabel}" clicked for field "${relatedField}" with value: ${actionValue}`,
479
+ `Field Action: "${key}" clicked for field "${relatedField}" with value: ${value}`,
512
480
  );
513
481
  } else {
514
- window.alert(`Schema Action: "${actionLabel}" clicked: ${actionValue}`);
482
+ window.alert(`Form Action: "${key}" clicked with value: ${value}`);
515
483
  }
516
484
  } else {
517
485
  console.log(
518
- `Demo action: ${actionLabel} clicked: ${actionValue}`,
519
- relatedField ? ` for field: ${relatedField}` : "",
486
+ `Demo action: ${key} clicked with value: ${value}`,
487
+ relatedField ? ` for field: ${relatedField}` : " (form-level)",
520
488
  );
521
489
  }
522
490
  });
@@ -553,8 +521,6 @@ function applyCurrentSchema() {
553
521
  return false;
554
522
  }
555
523
 
556
- // Build action value -> label map for efficient lookup
557
- actionLabelMap = buildActionLabelMap(schema);
558
524
 
559
525
  // Set mode based on toggle
560
526
  const isReadOnly = el.readOnlyToggle.checked;
@@ -801,25 +767,62 @@ el.clearActionsBtn.addEventListener("click", () => {
801
767
 
802
768
  // Example external actions for demonstration
803
769
  const EXAMPLE_ACTIONS = [
770
+ // Field-level actions using predefined labels from schema
771
+ {
772
+ related_field: "cover",
773
+ key: "retry",
774
+ value: "regenerate_cover_image", // Specific action value for handler
775
+ },
776
+ {
777
+ related_field: "cover",
778
+ key: "enhance",
779
+ value: "enhance_cover_quality", // Specific action value for handler
780
+ },
781
+ {
782
+ related_field: "cover",
783
+ key: "crop",
784
+ value: "auto_crop_cover", // Specific action value for handler
785
+ },
786
+ // Field-level actions with custom labels
804
787
  {
805
788
  related_field: "title[0]",
806
- value: "generate-title",
807
- label: "🤖 Generate Title",
789
+ key: "generate",
790
+ value: "ai_generate_title",
791
+ label: "🤖 Generate Title", // Custom label overrides schema
808
792
  },
809
793
  {
810
794
  related_field: "description",
811
- value: "improve-description",
812
- label: " Improve Description",
795
+ key: "improve",
796
+ value: "ai_improve_description", // Key will be used as fallback label
813
797
  },
814
798
  {
815
799
  related_field: "slides[0].title",
816
- value: "optimize-slide-title",
817
- label: "🎯 Optimize Slide Title",
800
+ key: "optimize",
801
+ value: "ai_optimize_slide_title",
802
+ label: "🎯 Optimize Slide Title", // Custom label
818
803
  },
804
+ // Form-level actions (no related_field)
819
805
  {
820
- related_field: "cover",
821
- value: "analyze-image",
822
- label: "🔍 Analyze Image",
806
+ key: "save-draft",
807
+ value: "save_form_draft",
808
+ label: "💾 Save Draft",
809
+ },
810
+ // Action with missing related field (should appear at bottom of form)
811
+ {
812
+ related_field: "non_existent_field",
813
+ key: "test-missing",
814
+ value: "test_missing_field_action",
815
+ label: "🔍 Test Missing Field Action",
816
+ },
817
+ {
818
+ key: "preview",
819
+ value: "preview_infographic",
820
+ label: "👁️ Preview",
821
+ },
822
+ {
823
+ key: "export",
824
+ value: "export_json_data",
825
+ label: "📄 Export JSON",
823
826
  },
824
827
  ];
825
828
 
@@ -290,50 +290,8 @@ function renderElement(element, ctx) {
290
290
  }
291
291
  }
292
292
 
293
- // Add action buttons in readonly mode
294
- if (
295
- state.config.readonly &&
296
- element.actions &&
297
- Array.isArray(element.actions) &&
298
- element.actions.length > 0
299
- ) {
300
- const actionsContainer = document.createElement("div");
301
- actionsContainer.className = "mt-3 flex flex-wrap gap-2";
302
-
303
- element.actions.forEach((action) => {
304
- if (action.value && action.label) {
305
- const actionBtn = document.createElement("button");
306
- actionBtn.type = "button";
307
- actionBtn.className =
308
- "px-3 py-2 text-sm border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors";
309
- actionBtn.textContent = action.label;
310
-
311
- actionBtn.addEventListener("click", (e) => {
312
- e.preventDefault();
313
- e.stopPropagation();
314
-
315
- if (
316
- state.config.actionHandler &&
317
- typeof state.config.actionHandler === "function"
318
- ) {
319
- // For schema-based actions (old system), call with just the value for backward compatibility
320
- // Check if the handler expects 2 parameters (new system) or 1 parameter (old system)
321
- if (state.config.actionHandler.length > 1) {
322
- // New system: pass related_field (element key) and value
323
- state.config.actionHandler(element.key, action.value);
324
- } else {
325
- // Old system: pass only value for backward compatibility
326
- state.config.actionHandler(action.value);
327
- }
328
- }
329
- });
330
-
331
- actionsContainer.appendChild(actionBtn);
332
- }
333
- });
334
-
335
- wrapper.appendChild(actionsContainer);
336
- }
293
+ // Actions are now only rendered via external actions system in renderExternalActions()
294
+ // element.actions are only used for label lookup, not direct rendering
337
295
 
338
296
  return wrapper;
339
297
  }
@@ -597,6 +555,23 @@ async function renderFilePreview(container, resourceId, options = {}) {
597
555
  }
598
556
  }
599
557
 
558
+ function renderThumbnailForResource(slot, rid, meta) {
559
+ const url = state.config.getThumbnail(rid);
560
+ if (url) {
561
+ const img = document.createElement("img");
562
+ img.className = "w-full h-full object-contain";
563
+ img.alt = meta.name;
564
+ img.src = url;
565
+ slot.appendChild(img);
566
+ } else {
567
+ slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
568
+ <svg class="w-12 h-12" fill="currentColor" viewBox="0 0 24 24">
569
+ <path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
570
+ </svg>
571
+ </div>`;
572
+ }
573
+ }
574
+
600
575
  function renderResourcePills(container, rids, onRemove) {
601
576
  clear(container);
602
577
 
@@ -721,21 +696,7 @@ function renderResourcePills(container, rids, onRemove) {
721
696
  slot.appendChild(img);
722
697
  } else if (state.config.getThumbnail) {
723
698
  // Use getThumbnail for uploaded files
724
- const img = document.createElement("img");
725
- img.className = "w-full h-full object-contain";
726
- img.alt = meta.name;
727
-
728
- const url = state.config.getThumbnail(rid);
729
- if (url) {
730
- img.src = url;
731
- slot.appendChild(img);
732
- } else {
733
- slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
734
- <svg class="w-12 h-12" fill="currentColor" viewBox="0 0 24 24">
735
- <path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
736
- </svg>
737
- </div>`;
738
- }
699
+ renderThumbnailForResource(slot, rid, meta);
739
700
  } else {
740
701
  slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
741
702
  <svg class="w-12 h-12" fill="currentColor" viewBox="0 0 24 24">
@@ -985,21 +946,48 @@ function findFormElementByFieldPath(fieldPath) {
985
946
 
986
947
  if (!state.formRoot) return null;
987
948
 
988
- // Try exact match first
989
- let element = state.formRoot.querySelector(`[name="${fieldPath}"]`);
990
- if (element) return element;
949
+ // In edit mode, try to find input elements with name attributes
950
+ if (!state.config.readonly) {
951
+ // Try exact match first
952
+ let element = state.formRoot.querySelector(`[name="${fieldPath}"]`);
953
+ if (element) return element;
991
954
 
992
- // Try with array notation variations
993
- const variations = [
994
- fieldPath,
995
- fieldPath.replace(/\[(\d+)\]/g, "[$1]"), // normalize array notation
996
- fieldPath.replace(/\./g, "[") +
997
- "]".repeat((fieldPath.match(/\./g) || []).length), // convert dots to brackets
998
- ];
955
+ // Try with array notation variations
956
+ const variations = [
957
+ fieldPath,
958
+ fieldPath.replace(/\[(\d+)\]/g, "[$1]"), // normalize array notation
959
+ fieldPath.replace(/\./g, "[") +
960
+ "]".repeat((fieldPath.match(/\./g) || []).length), // convert dots to brackets
961
+ ];
962
+
963
+ for (const variation of variations) {
964
+ element = state.formRoot.querySelector(`[name="${variation}"]`);
965
+ if (element) return element;
966
+ }
967
+ }
999
968
 
1000
- for (const variation of variations) {
1001
- element = state.formRoot.querySelector(`[name="${variation}"]`);
1002
- if (element) return element;
969
+ // In readonly mode, or if no input found, look for field wrappers using data attributes
970
+ // Find the schema element for this field path to match against rendered fields
971
+ const schemaElement = findSchemaElement(fieldPath);
972
+ if (!schemaElement) return null;
973
+
974
+ // Look for field wrappers that contain the field key
975
+ const fieldWrappers = state.formRoot.querySelectorAll('.fb-field-wrapper');
976
+ for (const wrapper of fieldWrappers) {
977
+ // Try to find a label or element that matches this field
978
+ const labelText = schemaElement.label || schemaElement.key;
979
+ const labelElement = wrapper.querySelector('label');
980
+ if (labelElement && (labelElement.textContent === labelText || labelElement.textContent === `${labelText}*`)) {
981
+ // Create a dummy element for the field so actions can attach
982
+ let fieldElement = wrapper.querySelector('.field-placeholder');
983
+ if (!fieldElement) {
984
+ fieldElement = document.createElement('div');
985
+ fieldElement.className = 'field-placeholder';
986
+ fieldElement.style.display = 'none';
987
+ wrapper.appendChild(fieldElement);
988
+ }
989
+ return fieldElement;
990
+ }
1003
991
  }
1004
992
 
1005
993
  return null;
@@ -1008,15 +996,36 @@ function findFormElementByFieldPath(fieldPath) {
1008
996
  function renderExternalActions() {
1009
997
  if (!state.externalActions || !Array.isArray(state.externalActions)) return;
1010
998
 
999
+ // Group actions by related_field (null for form-level actions)
1000
+ const actionsByField = new Map();
1001
+ const trueFormLevelActions = [];
1002
+ const movedFormLevelActions = [];
1003
+
1011
1004
  state.externalActions.forEach((action) => {
1012
- if (!action.related_field || !action.value || !action.label) return;
1005
+ if (!action.key || !action.value) return;
1013
1006
 
1007
+ if (!action.related_field) {
1008
+ // True form-level action
1009
+ trueFormLevelActions.push(action);
1010
+ } else {
1011
+ // Field-level action
1012
+ if (!actionsByField.has(action.related_field)) {
1013
+ actionsByField.set(action.related_field, []);
1014
+ }
1015
+ actionsByField.get(action.related_field).push(action);
1016
+ }
1017
+ });
1018
+
1019
+ // Render field-level actions
1020
+ actionsByField.forEach((actions, fieldPath) => {
1014
1021
  // Find the form element for this related field
1015
- const fieldElement = findFormElementByFieldPath(action.related_field);
1022
+ const fieldElement = findFormElementByFieldPath(fieldPath);
1016
1023
  if (!fieldElement) {
1017
1024
  console.warn(
1018
- `External action: Could not find form element for field "${action.related_field}"`,
1025
+ `External action: Could not find form element for field "${fieldPath}", treating as form-level actions`,
1019
1026
  );
1027
+ // If field is not found, treat these actions as moved form-level actions
1028
+ movedFormLevelActions.push(...actions);
1020
1029
  return;
1021
1030
  }
1022
1031
 
@@ -1028,25 +1037,95 @@ function renderExternalActions() {
1028
1037
 
1029
1038
  if (!wrapper) {
1030
1039
  console.warn(
1031
- `External action: Could not find wrapper for field "${action.related_field}"`,
1040
+ `External action: Could not find wrapper for field "${fieldPath}"`,
1032
1041
  );
1033
1042
  return;
1034
1043
  }
1035
1044
 
1036
- // Check if we already added external actions to this wrapper
1037
- if (wrapper.querySelector(".external-actions-container")) return;
1045
+ // Remove any existing actions container
1046
+ const existingContainer = wrapper.querySelector(".external-actions-container");
1047
+ if (existingContainer) {
1048
+ existingContainer.remove();
1049
+ }
1038
1050
 
1039
1051
  // Create actions container
1040
1052
  const actionsContainer = document.createElement("div");
1041
1053
  actionsContainer.className =
1042
- "external-actions-container mt-4 flex flex-wrap gap-2";
1054
+ "external-actions-container mt-3 flex flex-wrap gap-2";
1055
+
1056
+ // Find the corresponding schema element for label lookup
1057
+ const schemaElement = findSchemaElement(fieldPath);
1058
+
1059
+ // Create action buttons
1060
+ actions.forEach((action) => {
1061
+ const actionBtn = document.createElement("button");
1062
+ actionBtn.type = "button";
1063
+ actionBtn.className =
1064
+ "bg-white text-gray-700 border border-gray-200 px-3 py-2 text-sm rounded-lg hover:bg-gray-50 hover:border-gray-300 transition-all duration-200 shadow-sm";
1065
+
1066
+ // Resolve action label with priority:
1067
+ // 1. Use explicit label from action if provided
1068
+ // 2. Try to find label from schema element labels using key
1069
+ // 3. Fall back to using key as label
1070
+ const resolvedLabel = resolveActionLabel(action.key, action.label, schemaElement);
1071
+ actionBtn.textContent = resolvedLabel;
1072
+
1073
+ actionBtn.addEventListener("click", (e) => {
1074
+ e.preventDefault();
1075
+ e.stopPropagation();
1076
+
1077
+ if (
1078
+ state.config.actionHandler &&
1079
+ typeof state.config.actionHandler === "function"
1080
+ ) {
1081
+ // Call with value, key, and related_field for the new actions system
1082
+ state.config.actionHandler(action.value, action.key, action.related_field);
1083
+ }
1084
+ });
1085
+
1086
+ actionsContainer.appendChild(actionBtn);
1087
+ });
1088
+
1089
+ wrapper.appendChild(actionsContainer);
1090
+ });
1091
+
1092
+ // Render form-level actions at the bottom of the form
1093
+ const allFormLevelActions = [...trueFormLevelActions, ...movedFormLevelActions];
1094
+ if (allFormLevelActions.length > 0) {
1095
+ renderFormLevelActions(allFormLevelActions, trueFormLevelActions);
1096
+ }
1097
+ }
1098
+
1099
+ function renderFormLevelActions(actions, trueFormLevelActions = []) {
1100
+ if (!state.formRoot) return;
1101
+
1102
+ // Remove any existing form-level actions container
1103
+ const existingContainer = state.formRoot.querySelector(".form-level-actions-container");
1104
+ if (existingContainer) {
1105
+ existingContainer.remove();
1106
+ }
1107
+
1108
+ // Create form-level actions container
1109
+ const actionsContainer = document.createElement("div");
1110
+ actionsContainer.className =
1111
+ "form-level-actions-container mt-6 pt-4 border-t border-gray-200 flex flex-wrap gap-3 justify-center";
1043
1112
 
1044
- // Create action button
1113
+ // Create action buttons
1114
+ actions.forEach((action) => {
1045
1115
  const actionBtn = document.createElement("button");
1046
1116
  actionBtn.type = "button";
1047
1117
  actionBtn.className =
1048
- "px-3 py-2 text-sm border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors";
1049
- actionBtn.textContent = action.label;
1118
+ "bg-white text-gray-700 border border-gray-200 px-4 py-2 text-sm font-medium rounded-lg hover:bg-gray-50 hover:border-gray-300 transition-all duration-200 shadow-sm";
1119
+
1120
+ // Check if this is a true form-level action (no related_field originally)
1121
+ const isTrueFormLevelAction = trueFormLevelActions.includes(action);
1122
+
1123
+ // Resolve action label with priority:
1124
+ // 1. Use explicit label from action if provided
1125
+ // 2. Try to find label from schema element labels using key (only for true form-level actions)
1126
+ // 3. Fall back to using key as label
1127
+ const resolvedLabel = resolveActionLabel(action.key, action.label, null, isTrueFormLevelAction);
1128
+ actionBtn.textContent = resolvedLabel;
1050
1129
 
1051
1130
  actionBtn.addEventListener("click", (e) => {
1052
1131
  e.preventDefault();
@@ -1056,14 +1135,66 @@ function renderExternalActions() {
1056
1135
  state.config.actionHandler &&
1057
1136
  typeof state.config.actionHandler === "function"
1058
1137
  ) {
1059
- // Call with both related_field and value for the new actions system
1060
- state.config.actionHandler(action.related_field, action.value);
1138
+ // Call with value, key, and null related_field for form-level actions
1139
+ state.config.actionHandler(action.value, action.key, null);
1061
1140
  }
1062
1141
  });
1063
1142
 
1064
1143
  actionsContainer.appendChild(actionBtn);
1065
- wrapper.appendChild(actionsContainer);
1066
1144
  });
1145
+
1146
+ // Append to form root
1147
+ state.formRoot.appendChild(actionsContainer);
1148
+ }
1149
+
1150
+ // Helper function to resolve action label
1151
+ function resolveActionLabel(actionKey, externalLabel, schemaElement, isTrueFormLevelAction = false) {
1152
+ // 1. Try to find label from predefined actions in schema element using key (highest priority)
1153
+ if (schemaElement && schemaElement.actions && Array.isArray(schemaElement.actions)) {
1154
+ const predefinedAction = schemaElement.actions.find(a => a.key === actionKey);
1155
+ if (predefinedAction && predefinedAction.label) {
1156
+ return predefinedAction.label;
1157
+ }
1158
+ }
1159
+
1160
+ // 2. Try to find label from root-level schema actions (only for true form-level actions)
1161
+ if (isTrueFormLevelAction && state.schema && state.schema.actions && Array.isArray(state.schema.actions)) {
1162
+ const rootAction = state.schema.actions.find(a => a.key === actionKey);
1163
+ if (rootAction && rootAction.label) {
1164
+ return rootAction.label;
1165
+ }
1166
+ }
1167
+
1168
+ // 3. Use explicit label from external action if provided
1169
+ if (externalLabel) {
1170
+ return externalLabel;
1171
+ }
1172
+
1173
+ // 4. Fall back to using key as label
1174
+ return actionKey;
1175
+ }
1176
+
1177
+ // Helper function to find schema element by field path
1178
+ function findSchemaElement(fieldPath) {
1179
+ if (!state.schema || !state.schema.elements) return null;
1180
+
1181
+ let currentElements = state.schema.elements;
1182
+ let foundElement = null;
1183
+
1184
+ // Handle paths like 'a.b' or 'a[0].b' by looking for keys in sequence
1185
+ const keys = fieldPath.replace(/\[\d+\]/g, '').split('.').filter(Boolean);
1186
+
1187
+ for (const key of keys) {
1188
+ foundElement = currentElements.find(el => el.key === key);
1189
+ if (!foundElement) {
1190
+ return null; // Key not found at this level
1191
+ }
1192
+ if (foundElement.elements) {
1193
+ currentElements = foundElement.elements;
1194
+ }
1195
+ }
1196
+
1197
+ return foundElement;
1067
1198
  }
1068
1199
 
1069
1200
  function showTooltip(tooltipId, button) {
package/dist/index.html CHANGED
@@ -296,7 +296,7 @@
296
296
  id="actionsTextarea"
297
297
  class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 font-mono text-sm resize-none flex-1"
298
298
  spellcheck="false"
299
- placeholder='[{"related_field": "fieldName", "value": "action-key", "label": "Button Label"}]'
299
+ placeholder='[{"key": "action-key", "value": "specific-value", "related_field": "fieldName", "label": "Button Label"}]'
300
300
  ></textarea>
301
301
  <div
302
302
  id="actionsErrors"
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.1.37",
6
+ "version": "0.1.39",
7
7
  "description": "A reusable JSON schema form builder library",
8
8
  "main": "dist/form-builder.js",
9
9
  "module": "dist/form-builder.js",