|
@@ -0,0 +1,124 @@
|
|
|
|
|
+/**
|
|
|
|
|
+ * Deciding what order a node's settings appear in, and under which headings.
|
|
|
|
|
+ *
|
|
|
|
|
+ * The order they are written in the node file does not survive: a schema is
|
|
|
|
|
+ * carried as a JSON object all the way from the node file to the browser, and
|
|
|
|
|
+ * JSON objects here are stored sorted, so "width" arrives last and "height"
|
|
|
|
|
+ * near the top no matter how the node was written. Anything that wants a
|
|
|
|
|
+ * particular order has to say so in something that keeps its order - a list.
|
|
|
|
|
+ *
|
|
|
|
|
+ * A node can therefore declare uiGroups in its configSchema:
|
|
|
|
|
+ *
|
|
|
|
|
+ * uiGroups: [
|
|
|
|
|
+ * { title: 'Server', fields: ['serverUrl', 'credentialId'] },
|
|
|
|
|
+ * { title: 'Prompt', fields: ['prompt', 'negativePrompt'] },
|
|
|
|
|
+ * { title: 'Sampling', fields: ['width', 'height', 'steps'] }
|
|
|
|
|
+ * ]
|
|
|
|
|
+ *
|
|
|
|
|
+ * A node that declares nothing still gets connection settings and credentials
|
|
|
|
|
+ * lifted to the top, because that is where someone looks first and it costs
|
|
|
|
|
+ * nothing to apply everywhere.
|
|
|
|
|
+ */
|
|
|
|
|
+
|
|
|
|
|
+export interface FieldGroup {
|
|
|
|
|
+ title: string
|
|
|
|
|
+ keys: string[]
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Fields that say WHERE a node talks to, in the order they are usually filled.
|
|
|
|
|
+// Listed explicitly rather than guessed from the name, so an unrelated field
|
|
|
|
|
+// that happens to contain "url" is not dragged to the top.
|
|
|
|
|
+const CONNECTION_KEYS = [
|
|
|
|
|
+ 'serverUrl',
|
|
|
|
|
+ 'baseUrl',
|
|
|
|
|
+ 'apiUrl',
|
|
|
|
|
+ 'url',
|
|
|
|
|
+ 'host',
|
|
|
|
|
+ 'port',
|
|
|
|
|
+ 'useSsl',
|
|
|
|
|
+ 'database',
|
|
|
|
|
+ 'mailbox',
|
|
|
|
|
+]
|
|
|
|
|
+
|
|
|
|
|
+export function isCredentialKey(key: string, prop: any): boolean {
|
|
|
|
|
+ return (
|
|
|
|
|
+ prop?.dynamicOptions?.source === 'credentials' ||
|
|
|
|
|
+ key === 'credentialId' ||
|
|
|
|
|
+ key === 'credentials'
|
|
|
|
|
+ )
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Group and order a node's settings for display.
|
|
|
|
|
+ *
|
|
|
|
|
+ * Every field appears exactly once. A field a node forgot to list in its
|
|
|
|
|
+ * uiGroups still shows up, under a heading of its own - a setting that silently
|
|
|
|
|
+ * vanished from the form because of a typo in the grouping would be far worse
|
|
|
|
|
+ * than one in the wrong place.
|
|
|
|
|
+ */
|
|
|
|
|
+export function groupFields(
|
|
|
|
|
+ properties: Record<string, any>,
|
|
|
|
|
+ schema: Record<string, any>
|
|
|
|
|
+): FieldGroup[] {
|
|
|
|
|
+ const all = Object.keys(properties)
|
|
|
|
|
+ const used = new Set<string>()
|
|
|
|
|
+ const groups: FieldGroup[] = []
|
|
|
|
|
+
|
|
|
|
|
+ const declared = Array.isArray(schema?.uiGroups) ? schema.uiGroups : null
|
|
|
|
|
+
|
|
|
|
|
+ if (declared) {
|
|
|
|
|
+ for (const group of declared) {
|
|
|
|
|
+ const keys: string[] = []
|
|
|
|
|
+ for (const key of Array.isArray(group?.fields) ? group.fields : []) {
|
|
|
|
|
+ // Skip names that no longer exist, so a renamed setting leaves a gap
|
|
|
|
|
+ // rather than an empty control.
|
|
|
|
|
+ if (properties[key] && !used.has(key)) {
|
|
|
|
|
+ keys.push(key)
|
|
|
|
|
+ used.add(key)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (keys.length > 0) {
|
|
|
|
|
+ groups.push({ title: String(group?.title || ''), keys })
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const leftover = all.filter((k) => !used.has(k)).sort() // eslint-disable-line
|
|
|
|
|
+ if (leftover.length > 0) {
|
|
|
|
|
+ groups.push({ title: 'Other', keys: leftover })
|
|
|
|
|
+ }
|
|
|
|
|
+ return groups
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // No grouping declared: lift the two things everyone looks for first, and
|
|
|
|
|
+ // leave the rest as it comes.
|
|
|
|
|
+ const connection = CONNECTION_KEYS.filter((k) => properties[k])
|
|
|
|
|
+ connection.forEach((k) => used.add(k))
|
|
|
|
|
+
|
|
|
|
|
+ const credentials = all.filter((k) => !used.has(k) && isCredentialKey(k, properties[k]))
|
|
|
|
|
+ credentials.forEach((k) => used.add(k))
|
|
|
|
|
+
|
|
|
|
|
+ // Declaration order, recorded as an array when the node file was parsed
|
|
|
|
|
+ // because the schema object itself arrives with its keys sorted. Anything the
|
|
|
|
|
+ // list does not mention goes after it, alphabetically, so a field added
|
|
|
|
|
+ // without reparsing still appears.
|
|
|
|
|
+ const declaredOrder: string[] = Array.isArray(schema?.uiOrder) ? schema.uiOrder : []
|
|
|
|
|
+ const rank = new Map<string, number>()
|
|
|
|
|
+ declaredOrder.forEach((key, index) => rank.set(key, index))
|
|
|
|
|
+ const rest = all
|
|
|
|
|
+ .filter((k) => !used.has(k))
|
|
|
|
|
+ .sort((a, b) => {
|
|
|
|
|
+ const ra = rank.has(a) ? rank.get(a)! : Number.MAX_SAFE_INTEGER
|
|
|
|
|
+ const rb = rank.has(b) ? rank.get(b)! : Number.MAX_SAFE_INTEGER
|
|
|
|
|
+ return ra === rb ? a.localeCompare(b) : ra - rb
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ if (connection.length > 0) groups.push({ title: 'Connection', keys: connection })
|
|
|
|
|
+ if (credentials.length > 0) groups.push({ title: 'Credentials', keys: credentials })
|
|
|
|
|
+ if (rest.length > 0) {
|
|
|
|
|
+ // Only call it "Settings" when it is not the whole form - a single unnamed
|
|
|
|
|
+ // heading above every field is noise.
|
|
|
|
|
+ groups.push({ title: groups.length > 0 ? 'Settings' : '', keys: rest })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return groups
|
|
|
|
|
+}
|