|
@@ -1,3 +1,4 @@
|
|
|
|
|
+import { Node } from 'reactflow'
|
|
|
import { useEffect, useState } from 'react'
|
|
import { useEffect, useState } from 'react'
|
|
|
import { X, Database, Maximize2, Minimize2, ExternalLink, Download, Loader2 } from 'lucide-react'
|
|
import { X, Database, Maximize2, Minimize2, ExternalLink, Download, Loader2 } from 'lucide-react'
|
|
|
import { useQuery } from '@tanstack/react-query'
|
|
import { useQuery } from '@tanstack/react-query'
|
|
@@ -8,6 +9,7 @@ import { NodeOptionsSelect } from './NodeOptionsSelect'
|
|
|
import { groupFields, isCredentialKey } from './fieldGroups'
|
|
import { groupFields, isCredentialKey } from './fieldGroups'
|
|
|
import { CredentialSelect } from './CredentialSelect'
|
|
import { CredentialSelect } from './CredentialSelect'
|
|
|
import { CollectionSelect } from './CollectionSelect'
|
|
import { CollectionSelect } from './CollectionSelect'
|
|
|
|
|
+import { nodeNameProblem } from '../../utils/nodeNames'
|
|
|
import { EnumSelect } from './EnumSelect'
|
|
import { EnumSelect } from './EnumSelect'
|
|
|
import { KeyValueEditor } from './KeyValueEditor'
|
|
import { KeyValueEditor } from './KeyValueEditor'
|
|
|
import { ArrayFieldEditor } from './ArrayFieldEditor'
|
|
import { ArrayFieldEditor } from './ArrayFieldEditor'
|
|
@@ -44,6 +46,8 @@ interface NodeConfigModalProps {
|
|
|
// question may use.
|
|
// question may use.
|
|
|
workflowId: string
|
|
workflowId: string
|
|
|
editingConfig: Record<string, any>
|
|
editingConfig: Record<string, any>
|
|
|
|
|
+ // Every node on the canvas, so a rename can tell whether the name is taken.
|
|
|
|
|
+ allNodes: Node[]
|
|
|
nodeDefs: NodeDefinition[]
|
|
nodeDefs: NodeDefinition[]
|
|
|
storageCollections: StorageCollection[]
|
|
storageCollections: StorageCollection[]
|
|
|
// Give this workflow access to a collection without leaving the node. The
|
|
// Give this workflow access to a collection without leaving the node. The
|
|
@@ -64,6 +68,7 @@ export function NodeConfigModal({
|
|
|
suppliedConfig,
|
|
suppliedConfig,
|
|
|
workflowId,
|
|
workflowId,
|
|
|
editingConfig,
|
|
editingConfig,
|
|
|
|
|
+ allNodes,
|
|
|
nodeDefs,
|
|
nodeDefs,
|
|
|
storageCollections,
|
|
storageCollections,
|
|
|
onGrantCollection,
|
|
onGrantCollection,
|
|
@@ -83,6 +88,16 @@ export function NodeConfigModal({
|
|
|
|
|
|
|
|
const credentials: CredentialInfo[] = credentialsData?.credentials || []
|
|
const credentials: CredentialInfo[] = credentialsData?.credentials || []
|
|
|
|
|
|
|
|
|
|
+ // A rename is deliberate, so a clash is refused and explained rather than
|
|
|
|
|
+ // quietly numbered - silently changing what somebody typed is worse than
|
|
|
|
|
+ // telling them the name is taken. Adding and pasting are the opposite case
|
|
|
|
|
+ // and get numbered automatically, because nobody chose those names.
|
|
|
|
|
+ const nameProblem = nodeNameProblem(
|
|
|
|
|
+ editingConfig._customLabel || '',
|
|
|
|
|
+ allNodes,
|
|
|
|
|
+ selectedNodeData.id
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
// Names for the credential types nodes registered, so a credential shows as
|
|
// Names for the credential types nodes registered, so a credential shows as
|
|
|
// "SD.cpp Server" rather than the "basic" it is stored as.
|
|
// "SD.cpp Server" rather than the "basic" it is stored as.
|
|
|
const credentialTypeLabels: Record<string, string> = {}
|
|
const credentialTypeLabels: Record<string, string> = {}
|
|
@@ -298,14 +313,22 @@ export function NodeConfigModal({
|
|
|
value={editingConfig._customLabel || ''}
|
|
value={editingConfig._customLabel || ''}
|
|
|
onChange={(e) => onConfigChange({ ...editingConfig, _customLabel: e.target.value })}
|
|
onChange={(e) => onConfigChange({ ...editingConfig, _customLabel: e.target.value })}
|
|
|
placeholder={selectedNodeData.name}
|
|
placeholder={selectedNodeData.name}
|
|
|
- className="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg text-sm
|
|
|
|
|
|
|
+ className={`w-full px-3 py-2 border rounded-lg text-sm
|
|
|
bg-white dark:bg-slate-900 text-gray-900 dark:text-gray-100
|
|
bg-white dark:bg-slate-900 text-gray-900 dark:text-gray-100
|
|
|
placeholder-gray-400 dark:placeholder-gray-500
|
|
placeholder-gray-400 dark:placeholder-gray-500
|
|
|
- focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
|
|
|
|
|
|
+ focus:outline-none focus:ring-2 focus:border-transparent ${
|
|
|
|
|
+ nameProblem
|
|
|
|
|
+ ? 'border-red-400 dark:border-red-500 focus:ring-red-500'
|
|
|
|
|
+ : 'border-gray-300 dark:border-slate-600 focus:ring-primary-500'
|
|
|
|
|
+ }`}
|
|
|
/>
|
|
/>
|
|
|
- <p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
|
|
|
- Custom name for this node. Leave empty to use default: {selectedNodeData.name}
|
|
|
|
|
- </p>
|
|
|
|
|
|
|
+ {nameProblem ? (
|
|
|
|
|
+ <p className="mt-1 text-xs text-red-600 dark:text-red-400">{nameProblem}</p>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
|
|
|
+ Custom name for this node. Leave empty to use default: {selectedNodeData.name}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ )}
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div>
|
|
<div>
|
|
@@ -978,7 +1001,9 @@ export function NodeConfigModal({
|
|
|
</button>
|
|
</button>
|
|
|
<button
|
|
<button
|
|
|
onClick={onSave}
|
|
onClick={onSave}
|
|
|
- className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700"
|
|
|
|
|
|
|
+ disabled={!!nameProblem}
|
|
|
|
|
+ title={nameProblem || undefined}
|
|
|
|
|
+ className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
|
>
|
|
>
|
|
|
Save Configuration
|
|
Save Configuration
|
|
|
</button>
|
|
</button>
|