@d34dman/flowdrop 0.0.1 → 0.0.3
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 +307 -215
- package/dist/adapters/WorkflowAdapter.d.ts +1 -1
- package/dist/adapters/WorkflowAdapter.js +30 -30
- package/dist/api/client.d.ts +24 -1
- package/dist/api/client.js +55 -38
- package/dist/api/enhanced-client.d.ts +46 -0
- package/dist/api/enhanced-client.js +211 -0
- package/dist/clients/ApiClient.d.ts +19 -23
- package/dist/clients/ApiClient.js +36 -34
- package/dist/components/App.svelte +1299 -230
- package/dist/components/App.svelte.d.ts +21 -1
- package/dist/components/CanvasBanner.svelte +50 -44
- package/dist/components/CanvasBanner.svelte.d.ts +5 -19
- package/dist/components/ConfigForm.svelte +555 -0
- package/dist/components/ConfigForm.svelte.d.ts +32 -0
- package/dist/components/ConfigModal.svelte +261 -0
- package/dist/components/ConfigModal.svelte.d.ts +31 -0
- package/dist/components/ConfigSidebar.svelte +934 -0
- package/dist/components/ConfigSidebar.svelte.d.ts +51 -0
- package/dist/components/ConnectionLine.svelte +32 -0
- package/dist/components/ConnectionLine.svelte.d.ts +3 -0
- package/dist/components/GatewayNode.svelte +471 -0
- package/dist/components/GatewayNode.svelte.d.ts +15 -0
- package/dist/components/LoadingSpinner.svelte +23 -23
- package/dist/components/LoadingSpinner.svelte.d.ts +1 -1
- package/dist/components/Logo.svelte +82 -0
- package/dist/components/Logo.svelte.d.ts +26 -0
- package/dist/components/LogsSidebar.svelte +565 -0
- package/dist/components/LogsSidebar.svelte.d.ts +34 -0
- package/dist/components/MarkdownDisplay.svelte +28 -0
- package/dist/components/MarkdownDisplay.svelte.d.ts +7 -0
- package/dist/components/Navbar.svelte +663 -0
- package/dist/components/Navbar.svelte.d.ts +21 -0
- package/dist/components/NodeSidebar.svelte +629 -488
- package/dist/components/NodeSidebar.svelte.d.ts +1 -2
- package/dist/components/NodeStatusOverlay.svelte +327 -0
- package/dist/components/NodeStatusOverlay.svelte.d.ts +11 -0
- package/dist/components/NotesNode.svelte +566 -0
- package/dist/components/NotesNode.svelte.d.ts +43 -0
- package/dist/components/PipelineStatus.svelte +331 -0
- package/dist/components/PipelineStatus.svelte.d.ts +18 -0
- package/dist/components/SimpleNode.svelte +447 -0
- package/dist/components/SimpleNode.svelte.d.ts +24 -0
- package/dist/components/SquareNode.svelte +346 -0
- package/dist/components/SquareNode.svelte.d.ts +24 -0
- package/dist/components/StatusIcon.svelte +112 -0
- package/dist/components/StatusIcon.svelte.d.ts +10 -0
- package/dist/components/StatusLabel.svelte +33 -0
- package/dist/components/StatusLabel.svelte.d.ts +7 -0
- package/dist/components/ToolNode.svelte +385 -0
- package/dist/components/ToolNode.svelte.d.ts +36 -0
- package/dist/components/UniversalNode.svelte +126 -0
- package/dist/components/UniversalNode.svelte.d.ts +15 -0
- package/dist/components/WorkflowEditor.svelte +871 -528
- package/dist/components/WorkflowEditor.svelte.d.ts +15 -5
- package/dist/components/WorkflowNode.svelte +428 -542
- package/dist/components/WorkflowNode.svelte.d.ts +7 -3
- package/dist/config/apiConfig.d.ts +33 -0
- package/dist/config/apiConfig.js +39 -0
- package/dist/config/defaultPortConfig.d.ts +6 -0
- package/dist/config/defaultPortConfig.js +192 -0
- package/dist/config/demo.d.ts +58 -0
- package/dist/config/demo.js +142 -0
- package/dist/config/endpoints.d.ts +106 -0
- package/dist/config/endpoints.js +128 -0
- package/dist/data/samples.d.ts +38 -4
- package/dist/data/samples.js +2789 -737
- package/dist/examples/adapter-usage.d.ts +4 -4
- package/dist/examples/adapter-usage.js +21 -26
- package/dist/examples/api-client-usage.d.ts +6 -6
- package/dist/examples/api-client-usage.js +55 -54
- package/dist/index.d.ts +23 -15
- package/dist/index.js +23 -15
- package/dist/mocks/app-environment.d.ts +8 -0
- package/dist/mocks/app-environment.js +16 -0
- package/dist/mocks/app-forms.d.ts +2 -0
- package/dist/mocks/app-forms.js +21 -0
- package/dist/mocks/app-navigation.d.ts +5 -0
- package/dist/mocks/app-navigation.js +34 -0
- package/dist/mocks/app-stores.d.ts +14 -0
- package/dist/mocks/app-stores.js +26 -0
- package/dist/services/api.d.ts +13 -3
- package/dist/services/api.js +91 -36
- package/dist/services/globalSave.d.ts +20 -0
- package/dist/services/globalSave.js +165 -0
- package/dist/services/nodeExecutionService.d.ts +63 -0
- package/dist/services/nodeExecutionService.js +261 -0
- package/dist/services/portConfigApi.d.ts +14 -0
- package/dist/services/portConfigApi.js +69 -0
- package/dist/services/toastService.d.ts +147 -0
- package/dist/services/toastService.js +235 -0
- package/dist/services/workflowStorage.d.ts +2 -2
- package/dist/services/workflowStorage.js +10 -10
- package/dist/stores/workflowStore.d.ts +53 -0
- package/dist/stores/workflowStore.js +264 -0
- package/dist/styles/base.css +896 -363
- package/dist/svelte-app.d.ts +52 -5
- package/dist/svelte-app.js +128 -6
- package/dist/types/config.d.ts +291 -0
- package/dist/types/config.js +4 -0
- package/dist/types/index.d.ts +231 -19
- package/dist/types/index.js +1 -1
- package/dist/utils/colors.d.ts +67 -33
- package/dist/utils/colors.js +183 -118
- package/dist/utils/config.d.ts +41 -0
- package/dist/utils/config.js +248 -0
- package/dist/utils/connections.d.ts +40 -3
- package/dist/utils/connections.js +115 -44
- package/dist/utils/icons.d.ts +1 -1
- package/dist/utils/icons.js +71 -70
- package/dist/utils/nodeStatus.d.ts +53 -0
- package/dist/utils/nodeStatus.js +183 -0
- package/dist/utils/nodeTypes.d.ts +57 -0
- package/dist/utils/nodeTypes.js +109 -0
- package/dist/utils/nodeWrapper.d.ts +39 -0
- package/dist/utils/nodeWrapper.js +62 -0
- package/package.json +132 -97
- package/dist/app.css +0 -0
- package/dist/components/Node.svelte +0 -38
- package/dist/components/Node.svelte.d.ts +0 -4
|
@@ -5,233 +5,1302 @@
|
|
|
5
5
|
-->
|
|
6
6
|
|
|
7
7
|
<script lang="ts">
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
8
|
+
import { onMount, tick } from 'svelte';
|
|
9
|
+
import { page } from '$app/stores';
|
|
10
|
+
import WorkflowEditor from './WorkflowEditor.svelte';
|
|
11
|
+
import NodeSidebar from './NodeSidebar.svelte';
|
|
12
|
+
import ConfigSidebar from './ConfigSidebar.svelte';
|
|
13
|
+
import Navbar from './Navbar.svelte';
|
|
14
|
+
import { api, setEndpointConfig } from '../services/api.js';
|
|
15
|
+
import type { NodeMetadata, Workflow, WorkflowNode, ConfigSchema } from '../types/index.js';
|
|
16
|
+
import { sampleNodes } from '../data/samples.js';
|
|
17
|
+
import { createEndpointConfig } from '../config/endpoints.js';
|
|
18
|
+
import type { EndpointConfig } from '../config/endpoints.js';
|
|
19
|
+
import { workflowStore, workflowActions, workflowName } from '../stores/workflowStore.js';
|
|
20
|
+
import { resolveComponentName } from '../utils/nodeTypes.js';
|
|
21
|
+
import { apiToasts, dismissToast } from '../services/toastService.js';
|
|
22
|
+
|
|
23
|
+
// Configuration props for runtime customization
|
|
24
|
+
interface Props {
|
|
25
|
+
workflow?: Workflow;
|
|
26
|
+
height?: string | number;
|
|
27
|
+
width?: string | number;
|
|
28
|
+
showNavbar?: boolean;
|
|
29
|
+
// New configuration options for pipeline status mode
|
|
30
|
+
disableSidebar?: boolean;
|
|
31
|
+
lockWorkflow?: boolean;
|
|
32
|
+
readOnly?: boolean;
|
|
33
|
+
nodeStatuses?: Record<string, 'pending' | 'running' | 'completed' | 'error'>;
|
|
34
|
+
// Pipeline ID for fetching node execution info from jobs
|
|
35
|
+
pipelineId?: string;
|
|
36
|
+
// Navbar customization
|
|
37
|
+
navbarTitle?: string;
|
|
38
|
+
navbarActions?: Array<{
|
|
39
|
+
label: string;
|
|
40
|
+
href: string;
|
|
41
|
+
icon?: string;
|
|
42
|
+
variant?: 'primary' | 'secondary' | 'outline';
|
|
43
|
+
onclick?: (event: Event) => void;
|
|
44
|
+
}>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let {
|
|
48
|
+
workflow: initialWorkflow,
|
|
49
|
+
height = '100vh',
|
|
50
|
+
width = '100%',
|
|
51
|
+
showNavbar = false,
|
|
52
|
+
disableSidebar = false,
|
|
53
|
+
lockWorkflow = false,
|
|
54
|
+
readOnly = false,
|
|
55
|
+
nodeStatuses = {},
|
|
56
|
+
pipelineId,
|
|
57
|
+
navbarTitle,
|
|
58
|
+
navbarActions = []
|
|
59
|
+
}: Props = $props();
|
|
60
|
+
|
|
61
|
+
// Create breadcrumb-style title - at top level to avoid store subscription issues
|
|
62
|
+
let breadcrumbTitle = $derived(() => {
|
|
63
|
+
// Use custom navbar title if provided
|
|
64
|
+
if (navbarTitle) {
|
|
65
|
+
return navbarTitle;
|
|
66
|
+
}
|
|
67
|
+
// Default workflow title logic
|
|
68
|
+
if (!$workflowName || $workflowName === 'Untitled Workflow') {
|
|
69
|
+
return 'Workflow / New Workflow';
|
|
70
|
+
}
|
|
71
|
+
return `Workflow / ${$workflowName}`;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
let nodes = $state<NodeMetadata[]>([]);
|
|
75
|
+
// Remove workflow prop - use global store directly
|
|
76
|
+
// let workflow = $derived($workflowStore || initialWorkflow);
|
|
77
|
+
let error = $state<string | null>(null);
|
|
78
|
+
let loading = $state(true);
|
|
79
|
+
let endpointConfig = $state<EndpointConfig | null>(null);
|
|
80
|
+
|
|
81
|
+
// ConfigSidebar state
|
|
82
|
+
let isConfigSidebarOpen = $state(false);
|
|
83
|
+
let selectedNodeId = $state<string | null>(null);
|
|
84
|
+
|
|
85
|
+
// Workflow settings sidebar state
|
|
86
|
+
let isWorkflowSettingsOpen = $state(false);
|
|
87
|
+
|
|
88
|
+
// Workflow configuration schema
|
|
89
|
+
const workflowConfigSchema: ConfigSchema = {
|
|
90
|
+
type: 'object',
|
|
91
|
+
properties: {
|
|
92
|
+
name: {
|
|
93
|
+
type: 'string',
|
|
94
|
+
title: 'Workflow Name',
|
|
95
|
+
description: 'The name of the workflow',
|
|
96
|
+
default: ''
|
|
97
|
+
},
|
|
98
|
+
description: {
|
|
99
|
+
type: 'string',
|
|
100
|
+
title: 'Description',
|
|
101
|
+
description: 'A description of the workflow',
|
|
102
|
+
default: ''
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
required: ['name']
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// Workflow configuration values
|
|
109
|
+
let workflowConfigValues = $derived({
|
|
110
|
+
name: $workflowName || '',
|
|
111
|
+
description: $workflowStore?.description || ''
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Get the current node from the workflow store
|
|
115
|
+
let selectedNodeForConfig = $derived(() => {
|
|
116
|
+
if (!selectedNodeId || !$workflowStore) return null;
|
|
117
|
+
return $workflowStore.nodes.find((node) => node.id === selectedNodeId) || null;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// WorkflowEditor reference for save functionality
|
|
121
|
+
let workflowEditorRef: WorkflowEditor | null = null;
|
|
122
|
+
|
|
123
|
+
// Removed currentWorkflowState - no longer needed
|
|
124
|
+
// The global store ($workflowStore) serves as the single source of truth
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Fetch node types from the server
|
|
128
|
+
*/
|
|
129
|
+
async function fetchNodeTypes(): Promise<void> {
|
|
130
|
+
// Show loading toast
|
|
131
|
+
const loadingToast = apiToasts.loading('Loading node types');
|
|
132
|
+
try {
|
|
133
|
+
loading = true;
|
|
134
|
+
error = null;
|
|
135
|
+
|
|
136
|
+
const fetchedNodes = await api.nodes.getNodes();
|
|
137
|
+
|
|
138
|
+
nodes = fetchedNodes;
|
|
139
|
+
error = null;
|
|
140
|
+
|
|
141
|
+
// Dismiss loading toast
|
|
142
|
+
dismissToast(loadingToast);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
// Dismiss loading toast and show error toast
|
|
145
|
+
dismissToast(loadingToast);
|
|
146
|
+
// Show error but don't block the UI
|
|
147
|
+
error = `API Error: ${err instanceof Error ? err.message : 'Unknown error'}. Using sample data.`;
|
|
148
|
+
apiToasts.error('Load node types', err instanceof Error ? err.message : 'Unknown error');
|
|
149
|
+
|
|
150
|
+
// Fallback to sample data
|
|
151
|
+
nodes = sampleNodes;
|
|
152
|
+
} finally {
|
|
153
|
+
loading = false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Retry loading node types
|
|
159
|
+
*/
|
|
160
|
+
function retryLoad(): void {
|
|
161
|
+
fetchNodeTypes();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Test API connection
|
|
166
|
+
*/
|
|
167
|
+
async function testApiConnection(): Promise<void> {
|
|
168
|
+
try {
|
|
169
|
+
const testUrl = '/api/flowdrop/nodes';
|
|
170
|
+
|
|
171
|
+
const response = await fetch(testUrl);
|
|
172
|
+
const data = await response.json();
|
|
173
|
+
|
|
174
|
+
if (response.ok && data.success) {
|
|
175
|
+
apiToasts.success('API connection test', 'Connection successful');
|
|
176
|
+
} else {
|
|
177
|
+
apiToasts.error('API connection test', 'Connection failed');
|
|
178
|
+
}
|
|
179
|
+
} catch (err) {
|
|
180
|
+
apiToasts.error('API connection test', err instanceof Error ? err.message : 'Unknown error');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Initialize API endpoints
|
|
186
|
+
*/
|
|
187
|
+
async function initializeApiEndpoints(): Promise<void> {
|
|
188
|
+
// Use the same environment variable priority as the global save function
|
|
189
|
+
// Prioritize VITE_API_BASE_URL since it's configured correctly
|
|
190
|
+
const apiBaseUrl =
|
|
191
|
+
import.meta.env.VITE_API_BASE_URL || import.meta.env.VITE_DRUPAL_API_URL || '/api/flowdrop';
|
|
192
|
+
|
|
193
|
+
const config = createEndpointConfig(apiBaseUrl, {
|
|
194
|
+
auth: {
|
|
195
|
+
type: 'none' // No authentication for now
|
|
196
|
+
},
|
|
197
|
+
timeout: 10000, // 10 second timeout
|
|
198
|
+
retry: {
|
|
199
|
+
enabled: true,
|
|
200
|
+
maxAttempts: 2,
|
|
201
|
+
delay: 1000,
|
|
202
|
+
backoff: 'exponential'
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
setEndpointConfig(config);
|
|
207
|
+
// Store the configuration for passing to WorkflowEditor
|
|
208
|
+
endpointConfig = config;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* ConfigSidebar functions
|
|
213
|
+
*/
|
|
214
|
+
function openConfigSidebar(node: WorkflowNode): void {
|
|
215
|
+
// Close if already open for the same node
|
|
216
|
+
if (isConfigSidebarOpen && selectedNodeId === node.id) {
|
|
217
|
+
closeConfigSidebar();
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
selectedNodeId = node.id;
|
|
221
|
+
isConfigSidebarOpen = true;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function closeConfigSidebar(): void {
|
|
225
|
+
isConfigSidebarOpen = false;
|
|
226
|
+
selectedNodeId = null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Toggle workflow settings sidebar
|
|
231
|
+
*/
|
|
232
|
+
function toggleWorkflowSettings(): void {
|
|
233
|
+
isWorkflowSettingsOpen = !isWorkflowSettingsOpen;
|
|
234
|
+
// Close config sidebar if opening workflow settings
|
|
235
|
+
if (isWorkflowSettingsOpen) {
|
|
236
|
+
closeConfigSidebar();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Handle workflow configuration save
|
|
242
|
+
*/
|
|
243
|
+
async function handleWorkflowSave(config: any): Promise<void> {
|
|
244
|
+
console.log('Workflow configuration saved:', config);
|
|
245
|
+
|
|
246
|
+
// Update the workflow store
|
|
247
|
+
if ($workflowStore) {
|
|
248
|
+
$workflowStore.name = config.name;
|
|
249
|
+
$workflowStore.description = config.description;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Close the sidebar
|
|
253
|
+
isWorkflowSettingsOpen = false;
|
|
254
|
+
|
|
255
|
+
// Also save the workflow to the backend
|
|
256
|
+
try {
|
|
257
|
+
await saveWorkflow();
|
|
258
|
+
console.log('Workflow saved to backend successfully');
|
|
259
|
+
} catch (error) {
|
|
260
|
+
console.error('Failed to save workflow to backend:', error);
|
|
261
|
+
// Note: We don't throw the error here to avoid breaking the UI flow
|
|
262
|
+
// The user can still manually save via the main Save button if needed
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Removed handleWorkflowChange function - no longer needed
|
|
267
|
+
// The global store serves as the single source of truth and is already reactive
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Save workflow - exposed API function
|
|
271
|
+
*/
|
|
272
|
+
async function saveWorkflow(): Promise<void> {
|
|
273
|
+
try {
|
|
274
|
+
// Wait for any pending DOM updates before saving
|
|
275
|
+
await tick();
|
|
276
|
+
|
|
277
|
+
// Import necessary modules
|
|
278
|
+
const { workflowApi } = await import('../services/api.js');
|
|
279
|
+
const { v4: uuidv4 } = await import('uuid');
|
|
280
|
+
|
|
281
|
+
// Use current workflow from global store
|
|
282
|
+
const workflowToSave = $workflowStore;
|
|
283
|
+
|
|
284
|
+
if (!workflowToSave) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Determine the workflow ID
|
|
289
|
+
let workflowId: string;
|
|
290
|
+
if (workflowToSave.id) {
|
|
291
|
+
workflowId = workflowToSave.id;
|
|
292
|
+
} else {
|
|
293
|
+
workflowId = uuidv4();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Create workflow object for saving
|
|
297
|
+
const finalWorkflow = {
|
|
298
|
+
id: workflowId,
|
|
299
|
+
name: workflowToSave.name || 'Untitled Workflow',
|
|
300
|
+
description: workflowToSave.description || '',
|
|
301
|
+
nodes: workflowToSave.nodes || [],
|
|
302
|
+
edges: workflowToSave.edges || [],
|
|
303
|
+
metadata: {
|
|
304
|
+
version: '1.0.0',
|
|
305
|
+
createdAt: workflowToSave.metadata?.createdAt || new Date().toISOString(),
|
|
306
|
+
updatedAt: new Date().toISOString()
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const savedWorkflow = await workflowApi.saveWorkflow(finalWorkflow);
|
|
311
|
+
|
|
312
|
+
// Update the workflow ID if it changed (new workflow)
|
|
313
|
+
// Keep our current workflow state, only update ID and metadata from Drupal
|
|
314
|
+
if (savedWorkflow.id && savedWorkflow.id !== finalWorkflow.id) {
|
|
315
|
+
workflowActions.batchUpdate({
|
|
316
|
+
nodes: finalWorkflow.nodes,
|
|
317
|
+
edges: finalWorkflow.edges,
|
|
318
|
+
name: finalWorkflow.name,
|
|
319
|
+
metadata: {
|
|
320
|
+
...finalWorkflow.metadata,
|
|
321
|
+
...savedWorkflow.metadata
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
} catch (error) {
|
|
326
|
+
throw error; // Re-throw so caller can handle
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Export workflow - exposed API function
|
|
332
|
+
*/
|
|
333
|
+
async function exportWorkflow(): Promise<void> {
|
|
334
|
+
try {
|
|
335
|
+
// Wait for any pending DOM updates before exporting
|
|
336
|
+
await tick();
|
|
337
|
+
|
|
338
|
+
// Use current workflow from global store
|
|
339
|
+
const workflowToExport = $workflowStore;
|
|
340
|
+
|
|
341
|
+
if (!workflowToExport) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Create workflow object for export
|
|
346
|
+
const finalWorkflow = {
|
|
347
|
+
id: workflowToExport.id || 'untitled-workflow',
|
|
348
|
+
name: workflowToExport.name || 'Untitled Workflow',
|
|
349
|
+
nodes: workflowToExport.nodes || [],
|
|
350
|
+
edges: workflowToExport.edges || [],
|
|
351
|
+
metadata: {
|
|
352
|
+
version: '1.0.0',
|
|
353
|
+
createdAt: workflowToExport.metadata?.createdAt || new Date().toISOString(),
|
|
354
|
+
updatedAt: new Date().toISOString()
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
// Create and download the file
|
|
359
|
+
const dataStr = JSON.stringify(finalWorkflow, null, 2);
|
|
360
|
+
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
|
361
|
+
const url = URL.createObjectURL(dataBlob);
|
|
362
|
+
const link = document.createElement('a');
|
|
363
|
+
link.href = url;
|
|
364
|
+
link.download = `${finalWorkflow.name}.json`;
|
|
365
|
+
link.click();
|
|
366
|
+
URL.revokeObjectURL(url);
|
|
367
|
+
} catch (error) {
|
|
368
|
+
// Export failed
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Expose save and export functions globally for external access
|
|
373
|
+
if (typeof window !== 'undefined') {
|
|
374
|
+
// @ts-ignore - Adding to window for external access
|
|
375
|
+
window.flowdropSave = saveWorkflow;
|
|
376
|
+
// @ts-ignore - Adding to window for external access
|
|
377
|
+
window.flowdropExport = exportWorkflow;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Function to handle clicks outside the sidebar
|
|
381
|
+
function handleCanvasClick(event: MouseEvent): void {
|
|
382
|
+
// Check if the click is outside the right sidebar
|
|
383
|
+
const rightSidebar = document.querySelector('.flowdrop-sidebar--right');
|
|
384
|
+
if (rightSidebar && !rightSidebar.contains(event.target as Node)) {
|
|
385
|
+
// Close sidebar when clicking outside of it
|
|
386
|
+
if (isConfigSidebarOpen) {
|
|
387
|
+
closeConfigSidebar();
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Load node types on mount
|
|
393
|
+
onMount(() => {
|
|
394
|
+
(async () => {
|
|
395
|
+
await initializeApiEndpoints();
|
|
396
|
+
await fetchNodeTypes();
|
|
397
|
+
|
|
398
|
+
// Initialize the workflow store if we have an initial workflow
|
|
399
|
+
if (initialWorkflow) {
|
|
400
|
+
workflowActions.initialize(initialWorkflow);
|
|
401
|
+
}
|
|
402
|
+
})();
|
|
403
|
+
|
|
404
|
+
// Listen for workflow settings toggle from main navbar
|
|
405
|
+
const handleWorkflowSettingsToggle = () => {
|
|
406
|
+
toggleWorkflowSettings();
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
window.addEventListener('workflow-settings-toggle', handleWorkflowSettingsToggle);
|
|
410
|
+
|
|
411
|
+
return () => {
|
|
412
|
+
window.removeEventListener('workflow-settings-toggle', handleWorkflowSettingsToggle);
|
|
413
|
+
};
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
// Monitor workflow store changes for testing node drag updates
|
|
417
|
+
$effect(() => {
|
|
418
|
+
const currentWorkflow = $workflowStore;
|
|
419
|
+
if (currentWorkflow) {
|
|
420
|
+
// Workflow store updated
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
</script>
|
|
424
|
+
|
|
425
|
+
<svelte:head>
|
|
426
|
+
<title>FlowDrop - Visual Workflow Manager</title>
|
|
427
|
+
<meta name="description" content="A modern drag-and-drop workflow editor for LLM applications" />
|
|
428
|
+
</svelte:head>
|
|
429
|
+
|
|
430
|
+
<div
|
|
431
|
+
class="flowdrop-app"
|
|
432
|
+
style="height: {typeof height === 'number' ? `${height}px` : height}; width: {typeof width ===
|
|
433
|
+
'number'
|
|
434
|
+
? `${width}px`
|
|
435
|
+
: width};"
|
|
436
|
+
>
|
|
437
|
+
<!-- Navbar (conditionally rendered) - hide on workflow edit pages -->
|
|
438
|
+
{#if showNavbar && !$page.url.pathname.includes('/edit')}
|
|
439
|
+
<Navbar
|
|
440
|
+
title={breadcrumbTitle()}
|
|
441
|
+
primaryActions={navbarActions.length > 0
|
|
442
|
+
? navbarActions
|
|
443
|
+
: [
|
|
444
|
+
{
|
|
445
|
+
label: 'Save',
|
|
446
|
+
href: '#save',
|
|
447
|
+
icon: 'heroicons:document-arrow-down',
|
|
448
|
+
variant: 'primary',
|
|
449
|
+
onclick: (e) => {
|
|
450
|
+
e.preventDefault();
|
|
451
|
+
saveWorkflow();
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
label: 'Export',
|
|
456
|
+
href: '#export',
|
|
457
|
+
icon: 'heroicons:arrow-down-tray',
|
|
458
|
+
variant: 'outline',
|
|
459
|
+
onclick: (e) => {
|
|
460
|
+
e.preventDefault();
|
|
461
|
+
exportWorkflow();
|
|
462
|
+
}
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
label: 'Workflow Settings',
|
|
466
|
+
href: '#settings',
|
|
467
|
+
icon: 'heroicons:cog-6-tooth',
|
|
468
|
+
variant: 'outline',
|
|
469
|
+
onclick: (e) => {
|
|
470
|
+
e.preventDefault();
|
|
471
|
+
toggleWorkflowSettings();
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
]}
|
|
475
|
+
showStatus={true}
|
|
476
|
+
/>
|
|
477
|
+
{/if}
|
|
478
|
+
|
|
479
|
+
<!-- Main Content -->
|
|
480
|
+
<main class="flowdrop-main">
|
|
481
|
+
<!-- Status Display -->
|
|
482
|
+
{#if error}
|
|
483
|
+
<div class="flowdrop-status flowdrop-status--error">
|
|
484
|
+
<div class="flowdrop-status__content">
|
|
485
|
+
<div class="flowdrop-flex flowdrop-gap--3">
|
|
486
|
+
<div class="flowdrop-status__indicator flowdrop-status__indicator--error"></div>
|
|
487
|
+
<span class="flowdrop-text--sm flowdrop-font--medium">Error: {error}</span>
|
|
488
|
+
</div>
|
|
489
|
+
<div class="flowdrop-flex flowdrop-gap--2">
|
|
490
|
+
<button
|
|
491
|
+
class="flowdrop-btn flowdrop-btn--sm flowdrop-btn--outline"
|
|
492
|
+
onclick={retryLoad}
|
|
493
|
+
type="button"
|
|
494
|
+
>
|
|
495
|
+
Retry
|
|
496
|
+
</button>
|
|
497
|
+
<button
|
|
498
|
+
class="flowdrop-btn flowdrop-btn--sm flowdrop-btn--primary"
|
|
499
|
+
onclick={() => {
|
|
500
|
+
nodes = sampleNodes;
|
|
501
|
+
error = null;
|
|
502
|
+
}}
|
|
503
|
+
type="button"
|
|
504
|
+
>
|
|
505
|
+
Use Sample Data
|
|
506
|
+
</button>
|
|
507
|
+
<button
|
|
508
|
+
class="flowdrop-btn flowdrop-btn--sm flowdrop-btn--outline"
|
|
509
|
+
onclick={() => {
|
|
510
|
+
const defaultUrl = '/api/flowdrop';
|
|
511
|
+
const newUrl = prompt('Enter Drupal API URL:', defaultUrl);
|
|
512
|
+
if (newUrl) {
|
|
513
|
+
const endpointConfig = createEndpointConfig(newUrl);
|
|
514
|
+
setEndpointConfig(endpointConfig);
|
|
515
|
+
fetchNodeTypes();
|
|
516
|
+
}
|
|
517
|
+
}}
|
|
518
|
+
type="button"
|
|
519
|
+
>
|
|
520
|
+
Set API URL
|
|
521
|
+
</button>
|
|
522
|
+
<button
|
|
523
|
+
class="flowdrop-btn flowdrop-btn--sm flowdrop-btn--outline"
|
|
524
|
+
onclick={testApiConnection}
|
|
525
|
+
type="button"
|
|
526
|
+
>
|
|
527
|
+
Test API
|
|
528
|
+
</button>
|
|
529
|
+
<button
|
|
530
|
+
class="flowdrop-btn flowdrop-btn--ghost flowdrop-btn--sm"
|
|
531
|
+
onclick={() => (error = null)}
|
|
532
|
+
type="button"
|
|
533
|
+
>
|
|
534
|
+
✕
|
|
535
|
+
</button>
|
|
536
|
+
</div>
|
|
537
|
+
</div>
|
|
538
|
+
</div>
|
|
539
|
+
{/if}
|
|
540
|
+
|
|
541
|
+
<!-- Workflow Editor with Sidebars -->
|
|
542
|
+
<div class="flowdrop-editor-container">
|
|
543
|
+
<!-- Left Sidebar - Node Components (conditionally rendered) -->
|
|
544
|
+
{#if !disableSidebar}
|
|
545
|
+
<div class="flowdrop-sidebar flowdrop-sidebar--left">
|
|
546
|
+
<NodeSidebar {nodes} />
|
|
547
|
+
</div>
|
|
548
|
+
{/if}
|
|
549
|
+
|
|
550
|
+
<!-- Main Editor Area -->
|
|
551
|
+
<div
|
|
552
|
+
class="flowdrop-editor-main"
|
|
553
|
+
class:pipeline-view={!!pipelineId}
|
|
554
|
+
onclick={handleCanvasClick}
|
|
555
|
+
onkeydown={(e) => e.key === 'Escape' && closeConfigSidebar()}
|
|
556
|
+
role="button"
|
|
557
|
+
tabindex="0"
|
|
558
|
+
aria-label="Workflow canvas - click to close sidebar"
|
|
559
|
+
>
|
|
560
|
+
<WorkflowEditor
|
|
561
|
+
bind:this={workflowEditorRef}
|
|
562
|
+
{nodes}
|
|
563
|
+
{height}
|
|
564
|
+
{width}
|
|
565
|
+
{endpointConfig}
|
|
566
|
+
{isConfigSidebarOpen}
|
|
567
|
+
selectedNodeForConfig={selectedNodeForConfig()}
|
|
568
|
+
{openConfigSidebar}
|
|
569
|
+
{closeConfigSidebar}
|
|
570
|
+
{lockWorkflow}
|
|
571
|
+
{readOnly}
|
|
572
|
+
{nodeStatuses}
|
|
573
|
+
{pipelineId}
|
|
574
|
+
/>
|
|
575
|
+
</div>
|
|
576
|
+
|
|
577
|
+
<!-- Right Sidebar - Configuration or Workflow Settings (conditionally rendered) -->
|
|
578
|
+
{#if !disableSidebar && isWorkflowSettingsOpen}
|
|
579
|
+
<ConfigSidebar
|
|
580
|
+
isOpen={isWorkflowSettingsOpen}
|
|
581
|
+
title="Workflow Settings"
|
|
582
|
+
configSchema={workflowConfigSchema}
|
|
583
|
+
configValues={workflowConfigValues}
|
|
584
|
+
onSave={handleWorkflowSave}
|
|
585
|
+
onClose={() => (isWorkflowSettingsOpen = false)}
|
|
586
|
+
/>
|
|
587
|
+
{:else if !disableSidebar && selectedNodeForConfig()}
|
|
588
|
+
<div class="flowdrop-sidebar flowdrop-sidebar--right">
|
|
589
|
+
<div class="flowdrop-config-sidebar">
|
|
590
|
+
<!-- Header -->
|
|
591
|
+
<div class="flowdrop-config-sidebar__header">
|
|
592
|
+
<h2 class="flowdrop-config-sidebar__title">{selectedNodeForConfig().data.label}</h2>
|
|
593
|
+
<button
|
|
594
|
+
class="flowdrop-config-sidebar__close"
|
|
595
|
+
onclick={closeConfigSidebar}
|
|
596
|
+
aria-label="Close configuration sidebar"
|
|
597
|
+
>
|
|
598
|
+
×
|
|
599
|
+
</button>
|
|
600
|
+
</div>
|
|
601
|
+
|
|
602
|
+
<!-- Content -->
|
|
603
|
+
<div class="flowdrop-config-sidebar__content">
|
|
604
|
+
<!-- Node Details -->
|
|
605
|
+
<div class="flowdrop-config-sidebar__section">
|
|
606
|
+
<h3 class="flowdrop-config-sidebar__section-title">Node Details</h3>
|
|
607
|
+
<div class="flowdrop-config-sidebar__details">
|
|
608
|
+
<div class="flowdrop-config-sidebar__detail">
|
|
609
|
+
<span class="flowdrop-config-sidebar__detail-label">Type:</span>
|
|
610
|
+
<span class="flowdrop-config-sidebar__detail-value"
|
|
611
|
+
>{selectedNodeForConfig().data.metadata?.type ||
|
|
612
|
+
selectedNodeForConfig().type}</span
|
|
613
|
+
>
|
|
614
|
+
</div>
|
|
615
|
+
<div class="flowdrop-config-sidebar__detail">
|
|
616
|
+
<span class="flowdrop-config-sidebar__detail-label">Category:</span>
|
|
617
|
+
<span class="flowdrop-config-sidebar__detail-value"
|
|
618
|
+
>{selectedNodeForConfig().data.metadata?.category || 'general'}</span
|
|
619
|
+
>
|
|
620
|
+
</div>
|
|
621
|
+
<div class="flowdrop-config-sidebar__detail">
|
|
622
|
+
<span class="flowdrop-config-sidebar__detail-label">Description:</span>
|
|
623
|
+
<p class="flowdrop-config-sidebar__detail-description">
|
|
624
|
+
{selectedNodeForConfig().data.metadata?.description || 'Node configuration'}
|
|
625
|
+
</p>
|
|
626
|
+
</div>
|
|
627
|
+
</div>
|
|
628
|
+
</div>
|
|
629
|
+
|
|
630
|
+
<!-- Configuration Form -->
|
|
631
|
+
<div class="flowdrop-config-sidebar__section">
|
|
632
|
+
<h3 class="flowdrop-config-sidebar__section-title">Configuration</h3>
|
|
633
|
+
<div class="flowdrop-config-sidebar__form">
|
|
634
|
+
{#if selectedNodeForConfig().data.metadata?.configSchema}
|
|
635
|
+
<!-- Debug: Log the config schema -->
|
|
636
|
+
{@const configSchema = selectedNodeForConfig().data.metadata.configSchema}
|
|
637
|
+
{@const nodeConfig = selectedNodeForConfig().data.config || {}}
|
|
638
|
+
{@const configValues = (() => {
|
|
639
|
+
// Create a config object that merges defaults with existing values
|
|
640
|
+
const mergedConfig = {};
|
|
641
|
+
if (configSchema.properties) {
|
|
642
|
+
Object.entries(configSchema.properties).forEach(([key, field]) => {
|
|
643
|
+
const fieldConfig = field as any;
|
|
644
|
+
// Use existing value if available, otherwise use default
|
|
645
|
+
mergedConfig[key] =
|
|
646
|
+
nodeConfig[key] !== undefined ? nodeConfig[key] : fieldConfig.default;
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
return mergedConfig;
|
|
650
|
+
})()}
|
|
651
|
+
|
|
652
|
+
<!-- Render configuration fields based on schema -->
|
|
653
|
+
{#if configSchema.properties}
|
|
654
|
+
{#each Object.entries(configSchema.properties) as [key, field]}
|
|
655
|
+
{@const fieldConfig = field as any}
|
|
656
|
+
{#if fieldConfig.format !== 'hidden'}
|
|
657
|
+
<div class="flowdrop-config-sidebar__field">
|
|
658
|
+
<label class="flowdrop-config-sidebar__field-label" for={key}>
|
|
659
|
+
{fieldConfig.title || fieldConfig.description || key}
|
|
660
|
+
</label>
|
|
661
|
+
{#if fieldConfig.enum && fieldConfig.multiple}
|
|
662
|
+
<!-- Checkboxes for enum with multiple selection -->
|
|
663
|
+
<div class="flowdrop-config-sidebar__checkbox-group">
|
|
664
|
+
{#each fieldConfig.enum as option}
|
|
665
|
+
<label class="flowdrop-config-sidebar__checkbox-item">
|
|
666
|
+
<input
|
|
667
|
+
type="checkbox"
|
|
668
|
+
class="flowdrop-config-sidebar__checkbox"
|
|
669
|
+
value={String(option)}
|
|
670
|
+
checked={Array.isArray(configValues[key]) &&
|
|
671
|
+
configValues[key].includes(String(option))}
|
|
672
|
+
onchange={(e) => {
|
|
673
|
+
const checked = e.currentTarget.checked;
|
|
674
|
+
const currentValues = Array.isArray(configValues[key])
|
|
675
|
+
? [...configValues[key]]
|
|
676
|
+
: [];
|
|
677
|
+
if (checked) {
|
|
678
|
+
if (!currentValues.includes(String(option))) {
|
|
679
|
+
configValues[key] = [...currentValues, String(option)];
|
|
680
|
+
}
|
|
681
|
+
} else {
|
|
682
|
+
configValues[key] = currentValues.filter(
|
|
683
|
+
(v) => v !== String(option)
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
}}
|
|
687
|
+
/>
|
|
688
|
+
<span class="flowdrop-config-sidebar__checkbox-label">
|
|
689
|
+
{String(option)}
|
|
690
|
+
</span>
|
|
691
|
+
</label>
|
|
692
|
+
{/each}
|
|
693
|
+
</div>
|
|
694
|
+
{:else if fieldConfig.enum}
|
|
695
|
+
<!-- Select for enum with single selection -->
|
|
696
|
+
<select
|
|
697
|
+
id={key}
|
|
698
|
+
class="flowdrop-config-sidebar__select"
|
|
699
|
+
bind:value={configValues[key]}
|
|
700
|
+
>
|
|
701
|
+
{#each fieldConfig.enum as option}
|
|
702
|
+
<option value={String(option)}>{String(option)}</option>
|
|
703
|
+
{/each}
|
|
704
|
+
</select>
|
|
705
|
+
{:else if fieldConfig.type === 'string' && fieldConfig.format === 'multiline'}
|
|
706
|
+
<!-- Textarea for multiline strings -->
|
|
707
|
+
<textarea
|
|
708
|
+
id={key}
|
|
709
|
+
class="flowdrop-config-sidebar__textarea"
|
|
710
|
+
bind:value={configValues[key]}
|
|
711
|
+
placeholder={String(fieldConfig.placeholder || '')}
|
|
712
|
+
rows="4"
|
|
713
|
+
></textarea>
|
|
714
|
+
{:else if fieldConfig.type === 'string'}
|
|
715
|
+
<input
|
|
716
|
+
id={key}
|
|
717
|
+
type="text"
|
|
718
|
+
class="flowdrop-config-sidebar__input"
|
|
719
|
+
bind:value={configValues[key]}
|
|
720
|
+
placeholder={String(fieldConfig.placeholder || '')}
|
|
721
|
+
/>
|
|
722
|
+
{:else if fieldConfig.type === 'number'}
|
|
723
|
+
<input
|
|
724
|
+
id={key}
|
|
725
|
+
type="number"
|
|
726
|
+
class="flowdrop-config-sidebar__input"
|
|
727
|
+
bind:value={configValues[key]}
|
|
728
|
+
placeholder={String(fieldConfig.placeholder || '')}
|
|
729
|
+
/>
|
|
730
|
+
{:else if fieldConfig.type === 'boolean'}
|
|
731
|
+
<input
|
|
732
|
+
id={key}
|
|
733
|
+
type="checkbox"
|
|
734
|
+
class="flowdrop-config-sidebar__checkbox"
|
|
735
|
+
checked={Boolean(configValues[key] || fieldConfig.default || false)}
|
|
736
|
+
onchange={(e) => {
|
|
737
|
+
configValues[key] = e.currentTarget.checked;
|
|
738
|
+
}}
|
|
739
|
+
/>
|
|
740
|
+
{:else if fieldConfig.type === 'select' || fieldConfig.options}
|
|
741
|
+
<select
|
|
742
|
+
id={key}
|
|
743
|
+
class="flowdrop-config-sidebar__select"
|
|
744
|
+
bind:value={configValues[key]}
|
|
745
|
+
>
|
|
746
|
+
{#if fieldConfig.options}
|
|
747
|
+
{#each fieldConfig.options as option}
|
|
748
|
+
{@const optionConfig = option as any}
|
|
749
|
+
<option value={String(optionConfig.value)}
|
|
750
|
+
>{String(optionConfig.label)}</option
|
|
751
|
+
>
|
|
752
|
+
{/each}
|
|
753
|
+
{/if}
|
|
754
|
+
</select>
|
|
755
|
+
{:else}
|
|
756
|
+
<!-- Fallback for unknown field types -->
|
|
757
|
+
<input
|
|
758
|
+
id={key}
|
|
759
|
+
type="text"
|
|
760
|
+
class="flowdrop-config-sidebar__input"
|
|
761
|
+
bind:value={configValues[key]}
|
|
762
|
+
placeholder={String(fieldConfig.placeholder || '')}
|
|
763
|
+
/>
|
|
764
|
+
{/if}
|
|
765
|
+
{#if fieldConfig.description}
|
|
766
|
+
<p class="flowdrop-config-sidebar__field-description">
|
|
767
|
+
{String(fieldConfig.description)}
|
|
768
|
+
</p>
|
|
769
|
+
{/if}
|
|
770
|
+
</div>
|
|
771
|
+
{/if}
|
|
772
|
+
{/each}
|
|
773
|
+
{:else}
|
|
774
|
+
<!-- If no properties, show the raw schema for debugging -->
|
|
775
|
+
<div class="flowdrop-config-sidebar__debug">
|
|
776
|
+
<p><strong>Debug - Config Schema:</strong></p>
|
|
777
|
+
<pre>{JSON.stringify(configSchema, null, 2)}</pre>
|
|
778
|
+
</div>
|
|
779
|
+
{/if}
|
|
780
|
+
{:else}
|
|
781
|
+
<p class="flowdrop-config-sidebar__no-config">
|
|
782
|
+
No configuration options available for this node.
|
|
783
|
+
</p>
|
|
784
|
+
{/if}
|
|
785
|
+
</div>
|
|
786
|
+
</div>
|
|
787
|
+
</div>
|
|
788
|
+
|
|
789
|
+
<!-- Footer -->
|
|
790
|
+
<div class="flowdrop-config-sidebar__footer">
|
|
791
|
+
<button
|
|
792
|
+
class="flowdrop-config-sidebar__button flowdrop-config-sidebar__button--secondary"
|
|
793
|
+
onclick={closeConfigSidebar}
|
|
794
|
+
>
|
|
795
|
+
Cancel
|
|
796
|
+
</button>
|
|
797
|
+
<button
|
|
798
|
+
class="flowdrop-config-sidebar__button flowdrop-config-sidebar__button--primary"
|
|
799
|
+
onclick={() => {
|
|
800
|
+
// Get the current config values from the form
|
|
801
|
+
const currentNode = selectedNodeForConfig();
|
|
802
|
+
if (selectedNodeId && currentNode) {
|
|
803
|
+
// Collect the current form values
|
|
804
|
+
const form = document.querySelector('.flowdrop-config-sidebar__form');
|
|
805
|
+
const updatedConfig: Record<string, unknown> = {};
|
|
806
|
+
|
|
807
|
+
if (form) {
|
|
808
|
+
const inputs = form.querySelectorAll('input, select, textarea');
|
|
809
|
+
inputs.forEach((input: any) => {
|
|
810
|
+
if (input.id) {
|
|
811
|
+
if (input.type === 'checkbox') {
|
|
812
|
+
updatedConfig[input.id] = input.checked;
|
|
813
|
+
} else if (input.type === 'number') {
|
|
814
|
+
updatedConfig[input.id] = input.value
|
|
815
|
+
? Number(input.value)
|
|
816
|
+
: input.value;
|
|
817
|
+
} else if (input.type === 'hidden') {
|
|
818
|
+
// Parse hidden field values that might be JSON
|
|
819
|
+
try {
|
|
820
|
+
const parsed = JSON.parse(input.value);
|
|
821
|
+
updatedConfig[input.id] = parsed;
|
|
822
|
+
} catch {
|
|
823
|
+
// If not JSON, use raw value
|
|
824
|
+
updatedConfig[input.id] = input.value;
|
|
825
|
+
}
|
|
826
|
+
} else {
|
|
827
|
+
updatedConfig[input.id] = input.value;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// Preserve hidden field values from original config if not collected from form
|
|
834
|
+
if (
|
|
835
|
+
currentNode.data.config &&
|
|
836
|
+
currentNode.data.metadata?.configSchema?.properties
|
|
837
|
+
) {
|
|
838
|
+
Object.entries(currentNode.data.metadata.configSchema.properties).forEach(
|
|
839
|
+
([key, property]: [string, any]) => {
|
|
840
|
+
if (
|
|
841
|
+
property.format === 'hidden' &&
|
|
842
|
+
!(key in updatedConfig) &&
|
|
843
|
+
key in currentNode.data.config
|
|
844
|
+
) {
|
|
845
|
+
updatedConfig[key] = currentNode.data.config[key];
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// Handle nodeType switching if nodeType is in the config
|
|
852
|
+
let nodeUpdates: Record<string, unknown> = {
|
|
853
|
+
data: {
|
|
854
|
+
...currentNode.data,
|
|
855
|
+
config: updatedConfig
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
|
|
859
|
+
// NOTE: We do NOT change the node's type field anymore
|
|
860
|
+
// All nodes use 'universalNode' and UniversalNode handles internal switching
|
|
861
|
+
workflowActions.updateNode(selectedNodeId, nodeUpdates);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
closeConfigSidebar();
|
|
865
|
+
}}
|
|
866
|
+
>
|
|
867
|
+
Save Changes
|
|
868
|
+
</button>
|
|
869
|
+
</div>
|
|
870
|
+
</div>
|
|
871
|
+
</div>
|
|
872
|
+
{/if}
|
|
873
|
+
</div>
|
|
874
|
+
</main>
|
|
875
|
+
</div>
|
|
876
|
+
|
|
877
|
+
<style>
|
|
878
|
+
.flowdrop-app {
|
|
879
|
+
background: linear-gradient(135deg, #f9fafb 0%, #e0e7ff 50%, #c7d2fe 100%);
|
|
880
|
+
display: flex;
|
|
881
|
+
flex-direction: column;
|
|
882
|
+
overflow: hidden;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
.flowdrop-main {
|
|
886
|
+
flex: 1;
|
|
887
|
+
position: relative;
|
|
888
|
+
display: flex;
|
|
889
|
+
flex-direction: column;
|
|
890
|
+
min-height: 0;
|
|
891
|
+
overflow: hidden;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
.flowdrop-status {
|
|
895
|
+
background-color: #eff6ff;
|
|
896
|
+
border-bottom: 1px solid #bfdbfe;
|
|
897
|
+
padding: 1rem;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
.flowdrop-status--error {
|
|
901
|
+
background-color: #fef2f2;
|
|
902
|
+
border-bottom: 1px solid #fecaca;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
.flowdrop-status__content {
|
|
906
|
+
max-width: 80rem;
|
|
907
|
+
margin: 0 auto;
|
|
908
|
+
display: flex;
|
|
909
|
+
align-items: center;
|
|
910
|
+
justify-content: space-between;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
.flowdrop-status__indicator {
|
|
914
|
+
width: 0.5rem;
|
|
915
|
+
height: 0.5rem;
|
|
916
|
+
border-radius: 50%;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
.flowdrop-status__indicator--error {
|
|
920
|
+
background-color: #ef4444;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
.flowdrop-btn {
|
|
924
|
+
padding: 0.375rem 0.75rem;
|
|
925
|
+
border-radius: 0.375rem;
|
|
926
|
+
font-size: 0.75rem;
|
|
927
|
+
font-weight: 500;
|
|
928
|
+
cursor: pointer;
|
|
929
|
+
border: 1px solid transparent;
|
|
930
|
+
transition: all 0.2s ease-in-out;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
.flowdrop-btn--sm {
|
|
934
|
+
padding: 0.25rem 0.5rem;
|
|
935
|
+
font-size: 0.625rem;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
.flowdrop-btn--outline {
|
|
939
|
+
background-color: transparent;
|
|
940
|
+
border-color: #d1d5db;
|
|
941
|
+
color: #374151;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
.flowdrop-btn--outline:hover {
|
|
945
|
+
background-color: #f9fafb;
|
|
946
|
+
border-color: #9ca3af;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
.flowdrop-btn--primary {
|
|
950
|
+
background-color: #3b82f6;
|
|
951
|
+
border-color: #3b82f6;
|
|
952
|
+
color: #ffffff;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
.flowdrop-btn--primary:hover {
|
|
956
|
+
background-color: #2563eb;
|
|
957
|
+
border-color: #2563eb;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
.flowdrop-btn--ghost {
|
|
961
|
+
background-color: transparent;
|
|
962
|
+
border-color: transparent;
|
|
963
|
+
color: #6b7280;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
.flowdrop-btn--ghost:hover {
|
|
967
|
+
background-color: #f3f4f6;
|
|
968
|
+
color: #374151;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
.flowdrop-flex {
|
|
972
|
+
display: flex;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
.flowdrop-gap--2 {
|
|
976
|
+
gap: 0.5rem;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
.flowdrop-gap--3 {
|
|
980
|
+
gap: 0.75rem;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
.flowdrop-text--sm {
|
|
984
|
+
font-size: 0.875rem;
|
|
985
|
+
line-height: 1.25rem;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
.flowdrop-font--medium {
|
|
989
|
+
font-weight: 500;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
@keyframes spin {
|
|
993
|
+
0% {
|
|
994
|
+
transform: rotate(0deg);
|
|
995
|
+
}
|
|
996
|
+
100% {
|
|
997
|
+
transform: rotate(360deg);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
.flowdrop-editor-container {
|
|
1002
|
+
flex: 1;
|
|
1003
|
+
position: relative;
|
|
1004
|
+
min-height: 0;
|
|
1005
|
+
overflow: hidden;
|
|
1006
|
+
display: flex;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
.flowdrop-sidebar {
|
|
1010
|
+
background-color: #ffffff;
|
|
1011
|
+
border: 1px solid #e5e7eb;
|
|
1012
|
+
overflow-y: auto;
|
|
1013
|
+
overflow-x: hidden;
|
|
1014
|
+
height: 100%;
|
|
1015
|
+
display: flex;
|
|
1016
|
+
flex-direction: column;
|
|
1017
|
+
/* Custom scrollbar styling */
|
|
1018
|
+
scrollbar-width: thin;
|
|
1019
|
+
scrollbar-color: #cbd5e1 #f1f5f9;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
.flowdrop-sidebar::-webkit-scrollbar {
|
|
1023
|
+
width: 8px;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
.flowdrop-sidebar::-webkit-scrollbar-track {
|
|
1027
|
+
background: #f1f5f9;
|
|
1028
|
+
border-radius: 4px;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
.flowdrop-sidebar::-webkit-scrollbar-thumb {
|
|
1032
|
+
background: #cbd5e1;
|
|
1033
|
+
border-radius: 4px;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
.flowdrop-sidebar::-webkit-scrollbar-thumb:hover {
|
|
1037
|
+
background: #94a3b8;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
.flowdrop-sidebar--left {
|
|
1041
|
+
width: 320px;
|
|
1042
|
+
min-width: 320px;
|
|
1043
|
+
border-right: 1px solid #e5e7eb;
|
|
1044
|
+
box-shadow: 2px 0 4px rgba(0, 0, 0, 0.1);
|
|
1045
|
+
display: flex;
|
|
1046
|
+
flex-direction: column;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
.flowdrop-sidebar--right {
|
|
1050
|
+
border-left: 1px solid #e5e7eb;
|
|
1051
|
+
box-shadow: -2px 0 4px rgba(0, 0, 0, 0.1);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
.flowdrop-editor-main {
|
|
1055
|
+
flex: 1;
|
|
1056
|
+
position: relative;
|
|
1057
|
+
min-width: 0;
|
|
1058
|
+
overflow: hidden;
|
|
1059
|
+
background-color: #1f2937;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/* Configuration Sidebar Styles */
|
|
1063
|
+
.flowdrop-config-sidebar {
|
|
1064
|
+
height: 100%;
|
|
1065
|
+
display: flex;
|
|
1066
|
+
flex-direction: column;
|
|
1067
|
+
background-color: #ffffff;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
.flowdrop-config-sidebar__header {
|
|
1071
|
+
display: flex;
|
|
1072
|
+
justify-content: space-between;
|
|
1073
|
+
align-items: center;
|
|
1074
|
+
padding: 1rem;
|
|
1075
|
+
border-bottom: 1px solid #e5e7eb;
|
|
1076
|
+
background-color: #f9fafb;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
.flowdrop-config-sidebar__title {
|
|
1080
|
+
margin: 0;
|
|
1081
|
+
font-size: 1.125rem;
|
|
1082
|
+
font-weight: 600;
|
|
1083
|
+
color: #111827;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
.flowdrop-config-sidebar__close {
|
|
1087
|
+
background: none;
|
|
1088
|
+
border: none;
|
|
1089
|
+
font-size: 1.5rem;
|
|
1090
|
+
cursor: pointer;
|
|
1091
|
+
color: #6b7280;
|
|
1092
|
+
padding: 0.25rem;
|
|
1093
|
+
border-radius: 0.25rem;
|
|
1094
|
+
transition: color 0.2s;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
.flowdrop-config-sidebar__close:hover {
|
|
1098
|
+
color: #374151;
|
|
1099
|
+
background-color: #f3f4f6;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
.flowdrop-config-sidebar__content {
|
|
1103
|
+
flex: 1;
|
|
1104
|
+
overflow-y: auto;
|
|
1105
|
+
padding: 1rem;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
.flowdrop-config-sidebar__section {
|
|
1109
|
+
margin-bottom: 1.5rem;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
.flowdrop-config-sidebar__section-title {
|
|
1113
|
+
margin: 0 0 0.75rem 0;
|
|
1114
|
+
font-size: 0.875rem;
|
|
1115
|
+
font-weight: 600;
|
|
1116
|
+
color: #374151;
|
|
1117
|
+
text-transform: uppercase;
|
|
1118
|
+
letter-spacing: 0.05em;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
.flowdrop-config-sidebar__details {
|
|
1122
|
+
display: flex;
|
|
1123
|
+
flex-direction: column;
|
|
1124
|
+
gap: 0.5rem;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
.flowdrop-config-sidebar__detail {
|
|
1128
|
+
display: flex;
|
|
1129
|
+
flex-direction: column;
|
|
1130
|
+
gap: 0.25rem;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
.flowdrop-config-sidebar__detail-label {
|
|
1134
|
+
font-size: 0.75rem;
|
|
1135
|
+
font-weight: 500;
|
|
1136
|
+
color: #6b7280;
|
|
1137
|
+
text-transform: uppercase;
|
|
1138
|
+
letter-spacing: 0.05em;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
.flowdrop-config-sidebar__detail-value {
|
|
1142
|
+
font-size: 0.875rem;
|
|
1143
|
+
color: #111827;
|
|
1144
|
+
font-weight: 500;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
.flowdrop-config-sidebar__detail-description {
|
|
1148
|
+
margin: 0;
|
|
1149
|
+
font-size: 0.875rem;
|
|
1150
|
+
color: #6b7280;
|
|
1151
|
+
line-height: 1.4;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
.flowdrop-config-sidebar__form {
|
|
1155
|
+
display: flex;
|
|
1156
|
+
flex-direction: column;
|
|
1157
|
+
gap: 1rem;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
.flowdrop-config-sidebar__field {
|
|
1161
|
+
display: flex;
|
|
1162
|
+
flex-direction: column;
|
|
1163
|
+
gap: 0.5rem;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
.flowdrop-config-sidebar__field-label {
|
|
1167
|
+
font-size: 0.875rem;
|
|
1168
|
+
font-weight: 500;
|
|
1169
|
+
color: #374151;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
.flowdrop-config-sidebar__input,
|
|
1173
|
+
.flowdrop-config-sidebar__select {
|
|
1174
|
+
padding: 0.5rem;
|
|
1175
|
+
border: 1px solid #d1d5db;
|
|
1176
|
+
border-radius: 0.375rem;
|
|
1177
|
+
font-size: 0.875rem;
|
|
1178
|
+
transition:
|
|
1179
|
+
border-color 0.2s,
|
|
1180
|
+
box-shadow 0.2s;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
.flowdrop-config-sidebar__input:focus,
|
|
1184
|
+
.flowdrop-config-sidebar__select:focus {
|
|
1185
|
+
outline: none;
|
|
1186
|
+
border-color: #3b82f6;
|
|
1187
|
+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
.flowdrop-config-sidebar__checkbox-group {
|
|
1191
|
+
display: flex;
|
|
1192
|
+
flex-direction: column;
|
|
1193
|
+
gap: 0.5rem;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
.flowdrop-config-sidebar__checkbox-item {
|
|
1197
|
+
display: flex;
|
|
1198
|
+
align-items: center;
|
|
1199
|
+
gap: 0.5rem;
|
|
1200
|
+
cursor: pointer;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
.flowdrop-config-sidebar__checkbox {
|
|
1204
|
+
width: 1rem;
|
|
1205
|
+
height: 1rem;
|
|
1206
|
+
accent-color: #3b82f6;
|
|
1207
|
+
cursor: pointer;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
.flowdrop-config-sidebar__checkbox-label {
|
|
1211
|
+
font-size: 0.875rem;
|
|
1212
|
+
color: #374151;
|
|
1213
|
+
cursor: pointer;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
.flowdrop-config-sidebar__textarea {
|
|
1217
|
+
width: 100%;
|
|
1218
|
+
padding: 0.5rem 0.75rem;
|
|
1219
|
+
border: 1px solid #d1d5db;
|
|
1220
|
+
border-radius: 0.375rem;
|
|
1221
|
+
font-size: 0.875rem;
|
|
1222
|
+
background-color: #ffffff;
|
|
1223
|
+
transition: all 0.2s ease-in-out;
|
|
1224
|
+
resize: vertical;
|
|
1225
|
+
min-height: 4rem;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
.flowdrop-config-sidebar__textarea:focus {
|
|
1229
|
+
outline: none;
|
|
1230
|
+
border-color: #3b82f6;
|
|
1231
|
+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
.flowdrop-config-sidebar__field-description {
|
|
1235
|
+
margin: 0;
|
|
1236
|
+
font-size: 0.75rem;
|
|
1237
|
+
color: #6b7280;
|
|
1238
|
+
line-height: 1.4;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
.flowdrop-config-sidebar__no-config {
|
|
1242
|
+
text-align: center;
|
|
1243
|
+
color: #6b7280;
|
|
1244
|
+
font-style: italic;
|
|
1245
|
+
padding: 2rem 1rem;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
.flowdrop-config-sidebar__footer {
|
|
1249
|
+
padding: 1rem;
|
|
1250
|
+
border-top: 1px solid #e5e7eb;
|
|
1251
|
+
background-color: #f9fafb;
|
|
1252
|
+
display: flex;
|
|
1253
|
+
gap: 0.75rem;
|
|
1254
|
+
justify-content: flex-end;
|
|
1255
|
+
height: 40px;
|
|
1256
|
+
align-items: center;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
.flowdrop-config-sidebar__button {
|
|
1260
|
+
padding: 0.5rem 1rem;
|
|
1261
|
+
border-radius: 0.375rem;
|
|
1262
|
+
font-size: 0.875rem;
|
|
1263
|
+
font-weight: 500;
|
|
1264
|
+
cursor: pointer;
|
|
1265
|
+
transition: all 0.2s;
|
|
1266
|
+
border: 1px solid transparent;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
.flowdrop-config-sidebar__button--secondary {
|
|
1270
|
+
background-color: #ffffff;
|
|
1271
|
+
border-color: #d1d5db;
|
|
1272
|
+
color: #374151;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
.flowdrop-config-sidebar__button--secondary:hover {
|
|
1276
|
+
background-color: #f9fafb;
|
|
1277
|
+
border-color: #9ca3af;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
.flowdrop-config-sidebar__button--primary {
|
|
1281
|
+
background-color: #3b82f6;
|
|
1282
|
+
color: #ffffff;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
.flowdrop-config-sidebar__button--primary:hover {
|
|
1286
|
+
background-color: #2563eb;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
.flowdrop-config-sidebar__debug {
|
|
1290
|
+
background-color: #f3f4f6;
|
|
1291
|
+
border: 1px solid #d1d5db;
|
|
1292
|
+
border-radius: 0.375rem;
|
|
1293
|
+
padding: 1rem;
|
|
1294
|
+
margin: 1rem 0;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
.flowdrop-config-sidebar__debug pre {
|
|
1298
|
+
background-color: #ffffff;
|
|
1299
|
+
border: 1px solid #e5e7eb;
|
|
1300
|
+
border-radius: 0.25rem;
|
|
1301
|
+
padding: 0.75rem;
|
|
1302
|
+
font-size: 0.75rem;
|
|
1303
|
+
overflow-x: auto;
|
|
1304
|
+
margin: 0.5rem 0 0 0;
|
|
1305
|
+
}
|
|
1306
|
+
</style>
|