@d34dman/flowdrop 0.0.26 → 0.0.28

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.
@@ -669,6 +669,7 @@
669
669
  >
670
670
  <ConfigForm
671
671
  node={currentNode}
672
+ workflowId={$workflowStore?.id}
672
673
  onSave={async (updatedConfig, uiExtensions?: NodeUIExtensions) => {
673
674
  if (selectedNodeId && currentNode) {
674
675
  // Build the updated node data
@@ -694,12 +695,8 @@
694
695
  // All nodes use 'universalNode' and UniversalNode handles internal switching
695
696
  workflowActions.updateNode(selectedNodeId, nodeUpdates);
696
697
 
697
- // For gateway nodes (which have branches), refresh edge positions
698
- // This fixes the bug where reordering branches doesn't update connections visually
699
- const nodeType = currentNode.data?.metadata?.type;
700
- if (nodeType === "gateway" && workflowEditorRef && selectedNodeId) {
701
- await workflowEditorRef.refreshEdgePositions(selectedNodeId);
702
- }
698
+ // Refresh edge positions just in case. This is a safe bet.
699
+ await workflowEditorRef.refreshEdgePositions(selectedNodeId);
703
700
  }
704
701
 
705
702
  closeConfigSidebar();
@@ -8,6 +8,7 @@
8
8
  - Dynamic form generation from JSON Schema using modular form components
9
9
  - UI Extensions support for display settings (e.g., hide unconnected handles)
10
10
  - Extensible architecture for complex schema types (array, object)
11
+ - Admin/Edit support for external configuration links and dynamic schema fetching
11
12
 
12
13
  Accessibility features:
13
14
  - Proper label associations with for/id attributes
@@ -18,9 +19,21 @@
18
19
 
19
20
  <script lang="ts">
20
21
  import Icon from '@iconify/svelte';
21
- import type { ConfigSchema, WorkflowNode, NodeUIExtensions } from '../types/index.js';
22
+ import type {
23
+ ConfigSchema,
24
+ WorkflowNode,
25
+ NodeUIExtensions,
26
+ ConfigEditOptions
27
+ } from '../types/index.js';
22
28
  import { FormField, FormFieldWrapper, FormToggle } from './form/index.js';
23
29
  import type { FieldSchema } from './form/index.js';
30
+ import {
31
+ getEffectiveConfigEditOptions,
32
+ fetchDynamicSchema,
33
+ resolveExternalEditUrl,
34
+ invalidateSchemaCache,
35
+ type DynamicSchemaResult
36
+ } from '../services/dynamicSchemaService.js';
24
37
 
25
38
  interface Props {
26
39
  /** Optional workflow node (if provided, schema and values are derived from it) */
@@ -31,20 +44,81 @@
31
44
  values?: Record<string, unknown>;
32
45
  /** Whether to show UI extension settings section */
33
46
  showUIExtensions?: boolean;
47
+ /** Optional workflow ID for context in external links */
48
+ workflowId?: string;
34
49
  /** Callback when form is saved (includes both config and extensions if enabled) */
35
50
  onSave: (config: Record<string, unknown>, uiExtensions?: NodeUIExtensions) => void;
36
51
  /** Callback when form is cancelled */
37
52
  onCancel: () => void;
38
53
  }
39
54
 
40
- let { node, schema, values, showUIExtensions = true, onSave, onCancel }: Props = $props();
55
+ let {
56
+ node,
57
+ schema,
58
+ values,
59
+ showUIExtensions = true,
60
+ workflowId,
61
+ onSave,
62
+ onCancel
63
+ }: Props = $props();
41
64
 
42
65
  /**
43
- * Get the configuration schema from node metadata or direct prop
66
+ * State for dynamic schema loading
44
67
  */
45
- const configSchema = $derived(
46
- schema ?? (node?.data.metadata?.configSchema as ConfigSchema | undefined)
47
- );
68
+ let dynamicSchemaLoading = $state(false);
69
+ let dynamicSchemaError = $state<string | null>(null);
70
+ let fetchedDynamicSchema = $state<ConfigSchema | null>(null);
71
+
72
+ /**
73
+ * Get the admin edit configuration for the node
74
+ */
75
+ const configEditOptions = $derived.by<ConfigEditOptions | undefined>(() => {
76
+ if (!node) return undefined;
77
+ return getEffectiveConfigEditOptions(node);
78
+ });
79
+
80
+ /**
81
+ * Determine if we should show the external edit link
82
+ */
83
+ const showExternalEditLink = $derived.by(() => {
84
+ if (!configEditOptions?.externalEditLink) return false;
85
+ // Show if no dynamic schema, or if both exist but preferDynamicSchema is false
86
+ if (!configEditOptions.dynamicSchema) return true;
87
+ return !configEditOptions.preferDynamicSchema;
88
+ });
89
+
90
+ /**
91
+ * Determine if we should use/fetch dynamic schema
92
+ */
93
+ const useDynamicSchema = $derived.by(() => {
94
+ if (!configEditOptions?.dynamicSchema) return false;
95
+ // Use if no external link, or if both exist and preferDynamicSchema is true
96
+ if (!configEditOptions.externalEditLink) return true;
97
+ return configEditOptions.preferDynamicSchema === true;
98
+ });
99
+
100
+ /**
101
+ * Get the configuration schema from node metadata, direct prop, or fetched dynamic schema
102
+ * Priority: fetchedDynamicSchema > direct schema prop > node metadata configSchema
103
+ */
104
+ const configSchema = $derived.by<ConfigSchema | undefined>(() => {
105
+ // If we have a fetched dynamic schema, use it
106
+ if (fetchedDynamicSchema) {
107
+ return fetchedDynamicSchema;
108
+ }
109
+ // Otherwise use the direct prop or node metadata
110
+ return schema ?? (node?.data.metadata?.configSchema as ConfigSchema | undefined);
111
+ });
112
+
113
+ /**
114
+ * Check if the node has no static schema and needs dynamic loading
115
+ */
116
+ const needsDynamicSchemaLoad = $derived.by(() => {
117
+ if (!node) return false;
118
+ const staticSchema = schema ?? node.data.metadata?.configSchema;
119
+ // Need to load if: no static schema AND dynamic schema is configured
120
+ return !staticSchema && useDynamicSchema && !fetchedDynamicSchema && !dynamicSchemaLoading;
121
+ });
48
122
 
49
123
  /**
50
124
  * Get the current configuration from node or direct prop
@@ -74,6 +148,85 @@
74
148
  return { ...typeDefaults, ...instanceOverrides };
75
149
  });
76
150
 
151
+ /**
152
+ * Fetch dynamic schema when needed
153
+ */
154
+ async function loadDynamicSchema(): Promise<void> {
155
+ if (!node || !configEditOptions?.dynamicSchema) return;
156
+
157
+ dynamicSchemaLoading = true;
158
+ dynamicSchemaError = null;
159
+
160
+ try {
161
+ const result: DynamicSchemaResult = await fetchDynamicSchema(
162
+ configEditOptions.dynamicSchema,
163
+ node,
164
+ workflowId
165
+ );
166
+
167
+ if (result.success && result.schema) {
168
+ fetchedDynamicSchema = result.schema;
169
+ } else {
170
+ dynamicSchemaError =
171
+ result.error ?? configEditOptions.errorMessage ?? 'Failed to load configuration schema';
172
+ }
173
+ } catch (err) {
174
+ dynamicSchemaError =
175
+ err instanceof Error
176
+ ? err.message
177
+ : (configEditOptions.errorMessage ?? 'Failed to load configuration schema');
178
+ } finally {
179
+ dynamicSchemaLoading = false;
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Refresh the dynamic schema (invalidate cache and reload)
185
+ */
186
+ async function refreshDynamicSchema(): Promise<void> {
187
+ if (!node || !configEditOptions?.dynamicSchema) return;
188
+
189
+ // Invalidate the cache first
190
+ invalidateSchemaCache(node, configEditOptions.dynamicSchema);
191
+ fetchedDynamicSchema = null;
192
+
193
+ // Reload the schema
194
+ await loadDynamicSchema();
195
+ }
196
+
197
+ /**
198
+ * Get the resolved external edit URL
199
+ */
200
+ function getExternalEditUrl(): string {
201
+ if (!node || !configEditOptions?.externalEditLink) return '#';
202
+ return resolveExternalEditUrl(configEditOptions.externalEditLink, node, workflowId);
203
+ }
204
+
205
+ /**
206
+ * Handle opening external edit link
207
+ */
208
+ function handleExternalEditClick(): void {
209
+ if (!node || !configEditOptions?.externalEditLink) return;
210
+
211
+ const url = getExternalEditUrl();
212
+ const openInNewTab = configEditOptions.externalEditLink.openInNewTab !== false;
213
+
214
+ if (openInNewTab) {
215
+ window.open(url, '_blank', 'noopener,noreferrer');
216
+ } else {
217
+ window.location.href = url;
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Auto-load dynamic schema on mount if needed
223
+ */
224
+ $effect(() => {
225
+ if (needsDynamicSchemaLoad) {
226
+ loadDynamicSchema();
227
+ }
228
+ });
229
+
77
230
  /**
78
231
  * Initialize config values when node/schema changes
79
232
  */
@@ -179,7 +332,74 @@
179
332
  }
180
333
  </script>
181
334
 
182
- {#if configSchema}
335
+ <!-- External Edit Link Section (shown when configured and preferred) -->
336
+ {#if showExternalEditLink && configEditOptions?.externalEditLink}
337
+ <div class="config-form__admin-edit">
338
+ <div class="config-form__admin-edit-header">
339
+ <Icon icon="heroicons:arrow-top-right-on-square" />
340
+ <span>External Configuration</span>
341
+ </div>
342
+ <div class="config-form__admin-edit-content">
343
+ <p class="config-form__admin-edit-description">
344
+ {configEditOptions.externalEditLink.description ??
345
+ 'This node requires external configuration. Click the button below to open the configuration panel.'}
346
+ </p>
347
+ <button
348
+ type="button"
349
+ class="config-form__button config-form__button--external"
350
+ onclick={handleExternalEditClick}
351
+ >
352
+ <Icon
353
+ icon={configEditOptions.externalEditLink.icon ?? 'heroicons:arrow-top-right-on-square'}
354
+ />
355
+ <span>{configEditOptions.externalEditLink.label ?? 'Configure Externally'}</span>
356
+ </button>
357
+ </div>
358
+ </div>
359
+ {/if}
360
+
361
+ <!-- Dynamic Schema Loading State -->
362
+ {#if dynamicSchemaLoading}
363
+ <div class="config-form__loading">
364
+ <div class="config-form__loading-spinner"></div>
365
+ <p class="config-form__loading-text">
366
+ {configEditOptions?.loadingMessage ?? 'Loading configuration options...'}
367
+ </p>
368
+ </div>
369
+ {:else if dynamicSchemaError}
370
+ <div class="config-form__error">
371
+ <div class="config-form__error-header">
372
+ <Icon icon="heroicons:exclamation-triangle" />
373
+ <span>Configuration Error</span>
374
+ </div>
375
+ <div class="config-form__error-content">
376
+ <p class="config-form__error-message">{dynamicSchemaError}</p>
377
+ <div class="config-form__error-actions">
378
+ <button
379
+ type="button"
380
+ class="config-form__button config-form__button--secondary"
381
+ onclick={refreshDynamicSchema}
382
+ >
383
+ <Icon icon="heroicons:arrow-path" />
384
+ <span>Retry</span>
385
+ </button>
386
+ {#if configEditOptions?.externalEditLink}
387
+ <button
388
+ type="button"
389
+ class="config-form__button config-form__button--external"
390
+ onclick={handleExternalEditClick}
391
+ >
392
+ <Icon
393
+ icon={configEditOptions.externalEditLink.icon ??
394
+ 'heroicons:arrow-top-right-on-square'}
395
+ />
396
+ <span>{configEditOptions.externalEditLink.label ?? 'Use External Editor'}</span>
397
+ </button>
398
+ {/if}
399
+ </div>
400
+ </div>
401
+ </div>
402
+ {:else if configSchema}
183
403
  <form
184
404
  class="config-form"
185
405
  onsubmit={(e) => {
@@ -187,6 +407,35 @@
187
407
  handleSave();
188
408
  }}
189
409
  >
410
+ <!-- Dynamic Schema Refresh Button -->
411
+ {#if fetchedDynamicSchema && configEditOptions?.showRefreshButton !== false}
412
+ <div class="config-form__schema-actions">
413
+ <button
414
+ type="button"
415
+ class="config-form__schema-refresh"
416
+ onclick={refreshDynamicSchema}
417
+ title="Refresh configuration schema"
418
+ >
419
+ <Icon icon="heroicons:arrow-path" />
420
+ <span>Refresh Schema</span>
421
+ </button>
422
+ {#if configEditOptions?.externalEditLink}
423
+ <button
424
+ type="button"
425
+ class="config-form__schema-external"
426
+ onclick={handleExternalEditClick}
427
+ title={configEditOptions.externalEditLink.description ?? 'Open external editor'}
428
+ >
429
+ <Icon
430
+ icon={configEditOptions.externalEditLink.icon ??
431
+ 'heroicons:arrow-top-right-on-square'}
432
+ />
433
+ <span>{configEditOptions.externalEditLink.label ?? 'External Editor'}</span>
434
+ </button>
435
+ {/if}
436
+ </div>
437
+ {/if}
438
+
190
439
  {#if configSchema.properties}
191
440
  <div class="config-form__fields">
192
441
  {#each Object.entries(configSchema.properties) as [key, field], index (key)}
@@ -259,12 +508,24 @@
259
508
  </button>
260
509
  </div>
261
510
  </form>
262
- {:else}
511
+ {:else if !dynamicSchemaLoading && !showExternalEditLink}
263
512
  <div class="config-form__empty">
264
513
  <div class="config-form__empty-icon">
265
514
  <Icon icon="heroicons:cog-6-tooth" />
266
515
  </div>
267
516
  <p class="config-form__empty-text">No configuration options available for this node.</p>
517
+ {#if configEditOptions?.externalEditLink}
518
+ <button
519
+ type="button"
520
+ class="config-form__button config-form__button--external config-form__empty-button"
521
+ onclick={handleExternalEditClick}
522
+ >
523
+ <Icon
524
+ icon={configEditOptions.externalEditLink.icon ?? 'heroicons:arrow-top-right-on-square'}
525
+ />
526
+ <span>{configEditOptions.externalEditLink.label ?? 'Configure Externally'}</span>
527
+ </button>
528
+ {/if}
268
529
  </div>
269
530
  {/if}
270
531
 
@@ -482,4 +743,231 @@
482
743
  font-style: italic;
483
744
  line-height: 1.5;
484
745
  }
746
+
747
+ .config-form__empty-button {
748
+ margin-top: 1rem;
749
+ }
750
+
751
+ /* ============================================
752
+ ADMIN/EDIT SECTION - External Configuration
753
+ ============================================ */
754
+
755
+ .config-form__admin-edit {
756
+ background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
757
+ border: 1px solid var(--color-ref-blue-200, #bfdbfe);
758
+ border-radius: 0.625rem;
759
+ overflow: hidden;
760
+ margin-bottom: 1rem;
761
+ }
762
+
763
+ .config-form__admin-edit-header {
764
+ display: flex;
765
+ align-items: center;
766
+ gap: 0.5rem;
767
+ padding: 0.75rem 1rem;
768
+ background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
769
+ border-bottom: 1px solid var(--color-ref-blue-200, #bfdbfe);
770
+ font-size: 0.8125rem;
771
+ font-weight: 600;
772
+ color: var(--color-ref-blue-800, #1e40af);
773
+ }
774
+
775
+ .config-form__admin-edit-header :global(svg) {
776
+ width: 1rem;
777
+ height: 1rem;
778
+ color: var(--color-ref-blue-600, #2563eb);
779
+ }
780
+
781
+ .config-form__admin-edit-content {
782
+ padding: 1rem;
783
+ display: flex;
784
+ flex-direction: column;
785
+ gap: 0.75rem;
786
+ }
787
+
788
+ .config-form__admin-edit-description {
789
+ margin: 0;
790
+ font-size: 0.8125rem;
791
+ color: var(--color-ref-blue-700, #1d4ed8);
792
+ line-height: 1.5;
793
+ }
794
+
795
+ /* ============================================
796
+ LOADING STATE
797
+ ============================================ */
798
+
799
+ .config-form__loading {
800
+ display: flex;
801
+ flex-direction: column;
802
+ align-items: center;
803
+ justify-content: center;
804
+ padding: 3rem 1.5rem;
805
+ gap: 1rem;
806
+ }
807
+
808
+ .config-form__loading-spinner {
809
+ width: 2.5rem;
810
+ height: 2.5rem;
811
+ border: 3px solid var(--color-ref-blue-100, #dbeafe);
812
+ border-top-color: var(--color-ref-blue-500, #3b82f6);
813
+ border-radius: 50%;
814
+ animation: config-form-spin 0.8s linear infinite;
815
+ }
816
+
817
+ @keyframes config-form-spin {
818
+ to {
819
+ transform: rotate(360deg);
820
+ }
821
+ }
822
+
823
+ .config-form__loading-text {
824
+ margin: 0;
825
+ font-size: 0.875rem;
826
+ color: var(--color-ref-gray-600, #4b5563);
827
+ }
828
+
829
+ /* ============================================
830
+ ERROR STATE
831
+ ============================================ */
832
+
833
+ .config-form__error {
834
+ background-color: var(--color-ref-red-50, #fef2f2);
835
+ border: 1px solid var(--color-ref-red-200, #fecaca);
836
+ border-radius: 0.5rem;
837
+ overflow: hidden;
838
+ }
839
+
840
+ .config-form__error-header {
841
+ display: flex;
842
+ align-items: center;
843
+ gap: 0.5rem;
844
+ padding: 0.75rem 1rem;
845
+ background-color: var(--color-ref-red-100, #fee2e2);
846
+ border-bottom: 1px solid var(--color-ref-red-200, #fecaca);
847
+ font-size: 0.8125rem;
848
+ font-weight: 600;
849
+ color: var(--color-ref-red-800, #991b1b);
850
+ }
851
+
852
+ .config-form__error-header :global(svg) {
853
+ width: 1rem;
854
+ height: 1rem;
855
+ color: var(--color-ref-red-600, #dc2626);
856
+ }
857
+
858
+ .config-form__error-content {
859
+ padding: 1rem;
860
+ display: flex;
861
+ flex-direction: column;
862
+ gap: 0.75rem;
863
+ }
864
+
865
+ .config-form__error-message {
866
+ margin: 0;
867
+ font-size: 0.8125rem;
868
+ color: var(--color-ref-red-700, #b91c1c);
869
+ line-height: 1.5;
870
+ }
871
+
872
+ .config-form__error-actions {
873
+ display: flex;
874
+ gap: 0.5rem;
875
+ flex-wrap: wrap;
876
+ }
877
+
878
+ /* ============================================
879
+ SCHEMA ACTIONS (Refresh, External Editor)
880
+ ============================================ */
881
+
882
+ .config-form__schema-actions {
883
+ display: flex;
884
+ gap: 0.5rem;
885
+ margin-bottom: 1rem;
886
+ padding-bottom: 0.75rem;
887
+ border-bottom: 1px solid var(--color-ref-gray-100, #f3f4f6);
888
+ }
889
+
890
+ .config-form__schema-refresh,
891
+ .config-form__schema-external {
892
+ display: inline-flex;
893
+ align-items: center;
894
+ gap: 0.375rem;
895
+ padding: 0.375rem 0.625rem;
896
+ font-size: 0.75rem;
897
+ font-weight: 500;
898
+ font-family: inherit;
899
+ border-radius: 0.375rem;
900
+ cursor: pointer;
901
+ transition: all 0.15s ease;
902
+ border: 1px solid transparent;
903
+ }
904
+
905
+ .config-form__schema-refresh {
906
+ background-color: var(--color-ref-gray-50, #f9fafb);
907
+ border-color: var(--color-ref-gray-200, #e5e7eb);
908
+ color: var(--color-ref-gray-600, #4b5563);
909
+ }
910
+
911
+ .config-form__schema-refresh:hover {
912
+ background-color: var(--color-ref-gray-100, #f3f4f6);
913
+ border-color: var(--color-ref-gray-300, #d1d5db);
914
+ color: var(--color-ref-gray-700, #374151);
915
+ }
916
+
917
+ .config-form__schema-refresh :global(svg),
918
+ .config-form__schema-external :global(svg) {
919
+ width: 0.875rem;
920
+ height: 0.875rem;
921
+ }
922
+
923
+ .config-form__schema-external {
924
+ background-color: var(--color-ref-blue-50, #eff6ff);
925
+ border-color: var(--color-ref-blue-200, #bfdbfe);
926
+ color: var(--color-ref-blue-700, #1d4ed8);
927
+ }
928
+
929
+ .config-form__schema-external:hover {
930
+ background-color: var(--color-ref-blue-100, #dbeafe);
931
+ border-color: var(--color-ref-blue-300, #93c5fd);
932
+ color: var(--color-ref-blue-800, #1e40af);
933
+ }
934
+
935
+ /* ============================================
936
+ EXTERNAL BUTTON STYLE
937
+ ============================================ */
938
+
939
+ .config-form__button--external {
940
+ background: linear-gradient(
941
+ 135deg,
942
+ var(--color-ref-indigo-500, #6366f1) 0%,
943
+ var(--color-ref-blue-600, #2563eb) 100%
944
+ );
945
+ color: #ffffff;
946
+ box-shadow:
947
+ 0 1px 3px rgba(99, 102, 241, 0.3),
948
+ inset 0 1px 0 rgba(255, 255, 255, 0.1);
949
+ }
950
+
951
+ .config-form__button--external:hover {
952
+ background: linear-gradient(
953
+ 135deg,
954
+ var(--color-ref-indigo-600, #4f46e5) 0%,
955
+ var(--color-ref-blue-700, #1d4ed8) 100%
956
+ );
957
+ box-shadow:
958
+ 0 4px 12px rgba(99, 102, 241, 0.35),
959
+ inset 0 1px 0 rgba(255, 255, 255, 0.1);
960
+ transform: translateY(-1px);
961
+ }
962
+
963
+ .config-form__button--external:active {
964
+ transform: translateY(0);
965
+ }
966
+
967
+ .config-form__button--external:focus-visible {
968
+ outline: none;
969
+ box-shadow:
970
+ 0 0 0 3px rgba(99, 102, 241, 0.4),
971
+ 0 4px 12px rgba(99, 102, 241, 0.35);
972
+ }
485
973
  </style>
@@ -8,6 +8,8 @@ interface Props {
8
8
  values?: Record<string, unknown>;
9
9
  /** Whether to show UI extension settings section */
10
10
  showUIExtensions?: boolean;
11
+ /** Optional workflow ID for context in external links */
12
+ workflowId?: string;
11
13
  /** Callback when form is saved (includes both config and extensions if enabled) */
12
14
  onSave: (config: Record<string, unknown>, uiExtensions?: NodeUIExtensions) => void;
13
15
  /** Callback when form is cancelled */
@@ -39,4 +39,3 @@
39
39
  </script>
40
40
 
41
41
  <!-- This component renders nothing - it's just for the hook logic -->
42
-