diff --git a/.commitlintrc.cjs b/.commitlintrc.cjs index d517a44ff..46974db51 100644 --- a/.commitlintrc.cjs +++ b/.commitlintrc.cjs @@ -14,6 +14,7 @@ module.exports = { 'apollo-react', 'apollo-wind', 'ap-chat', + 'flow-node-schema', // Apps scopes 'apollo-vertex', diff --git a/.github/workflows/flow-node-schema-release.yml b/.github/workflows/flow-node-schema-release.yml new file mode 100644 index 000000000..596245309 --- /dev/null +++ b/.github/workflows/flow-node-schema-release.yml @@ -0,0 +1,85 @@ +name: Flow Node Schema Release + +on: + push: + branches: + - main + paths: + - 'packages/flow-node-schema/**' + - '.github/workflows/flow-node-schema-release.yml' + +env: + GH_NPM_REGISTRY_TOKEN: ${{ secrets.GH_NPM_REGISTRY_TOKEN }} + NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} + TURBO_TELEMETRY_DISABLED: 1 + DO_NOT_TRACK: 1 + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +jobs: + release: + name: Release @uipath/flow-node-schema + runs-on: ubuntu-latest + permissions: + contents: write + packages: write + + steps: + - name: Checkout code + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + fetch-depth: 0 + token: ${{ secrets.RELEASE_TOKEN }} + + - name: Setup pnpm + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4 + + - name: Setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22 + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build flow-node-schema + run: pnpm --filter @uipath/flow-node-schema build + + - name: Test flow-node-schema + run: pnpm --filter @uipath/flow-node-schema test + + - name: Setup git user + run: | + git config --global user.name "semantic-release-bot" + git config --global user.email "semantic-release-bot@users.noreply.github.com" + + - name: Release + env: + GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} + NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} + GH_NPM_REGISTRY_TOKEN: ${{ secrets.GH_NPM_REGISTRY_TOKEN }} + HUSKY: 0 + working-directory: packages/flow-node-schema + run: semantic-release + + - name: Commit version bump + env: + GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} + run: | + if [ -n "$(git status --porcelain packages/flow-node-schema)" ]; then + git add packages/flow-node-schema/ + git commit -m "chore(release): @uipath/flow-node-schema version bump [skip ci]" + + if ! git push; then + echo "::error::Failed to push version bump commit" + exit 1 + fi + echo "✓ Pushed version bump commit" + else + echo "No changes to commit" + fi diff --git a/packages/apollo-react/package.json b/packages/apollo-react/package.json index a6a25f5d3..03cca0a39 100644 --- a/packages/apollo-react/package.json +++ b/packages/apollo-react/package.json @@ -218,6 +218,7 @@ "sanitize-html": "^2.17.0", "unist-util-visit": "^5.0.0", "use-sync-external-store": "^1.2.0", + "@uipath/flow-node-schema": "workspace:*", "zod": "^4.3.5", "zustand": "^5.0.9" }, diff --git a/packages/apollo-react/src/canvas/schema/node-definition/category-manifest.ts b/packages/apollo-react/src/canvas/schema/node-definition/category-manifest.ts index 0ee412e7f..057e88997 100644 --- a/packages/apollo-react/src/canvas/schema/node-definition/category-manifest.ts +++ b/packages/apollo-react/src/canvas/schema/node-definition/category-manifest.ts @@ -1,67 +1,2 @@ -/** - * Category Manifest Schemas - * - * Zod schemas for category definitions. - * Supports arbitrary nesting using parent references. - * Nodes define their own category membership. - */ - -import { z } from 'zod'; - -/** - * Category manifest from server - * - * Categories form a hierarchy using parentId references: - * - { id: 'control-flow', parentId: null } (root) - * - { id: 'decision', parentId: 'control-flow' } (child) - * - { id: 'advanced', parentId: 'decision' } (grandchild) - * - * Nodes define which categories they belong to via category property. - * This allows: - * - Stable category IDs (reorganization just changes parentId) - * - Nodes control their own categorization - */ -export const categoryManifestSchema = z.object({ - /** - * Unique category identifier - * - * Should be stable by version. - * Examples: 'control-flow', 'decision', 'loops', 'advanced' - */ - id: z.string().min(1), - - /** Human-readable category name */ - name: z.string().min(1), - - /** - * Parent category ID for nesting - * - * - null or undefined: root category - * - string: ID of parent category - * - * Categories can be reorganized by changing this field without - * affecting node references or constraint definitions. - */ - parentId: z.string().optional(), - - /** - * Sort order for display within the same parent level - * Categories at the same level are sorted by this value - */ - sortOrder: z.number().int().nonnegative(), - - /** Light mode color/gradient */ - color: z.string().min(1), - - /** Dark mode color/gradient */ - colorDark: z.string().min(1), - - /** Icon identifier */ - icon: z.string().min(1), - - /** Tags for search and filtering */ - tags: z.array(z.string()), -}); - -// Export inferred type -export type CategoryManifest = z.infer; +export type { CategoryManifest } from '@uipath/flow-node-schema'; +export { categoryManifestSchema } from '@uipath/flow-node-schema'; diff --git a/packages/apollo-react/src/canvas/schema/node-definition/constraints.ts b/packages/apollo-react/src/canvas/schema/node-definition/constraints.ts index 2f5544add..9a5987042 100644 --- a/packages/apollo-react/src/canvas/schema/node-definition/constraints.ts +++ b/packages/apollo-react/src/canvas/schema/node-definition/constraints.ts @@ -1,140 +1,2 @@ -/** - * Connection Constraints - * - * Defines semantic rules for valid connections between nodes and handles. - * Focus on workflow composition semantics, not just data types. - */ - -import { z } from 'zod'; - -/** - * Specific node and handle targeting - * Used for precise semantic constraints like "Agent Model can only connect to Agent node's 'model' handle" - */ -export const handleTargetSchema = z.object({ - /** - * Specific node type this can connect to - * Example: "uipath.agent" (exact match, no wildcards) - */ - nodeType: z.string(), - - /** - * Specific handle ID on the target node - * Example: "model", "input", "configuration" - */ - handleId: z.string().optional(), -}); - -/** - * Connection constraint configuration for a handle - */ -export const connectionConstraintSchema = z.object({ - /** - * Maximum number of connections allowed - * - 1: Single connection only - * - undefined: Unlimited connections - */ - maxConnections: z.number().int().positive().optional(), - - /** - * Minimum number of connections required (for validation) - * Example: Trigger output must connect to at least 1 node - */ - minConnections: z.number().int().nonnegative().optional(), - - /** - * WHITELIST: Allowed target nodes/handles (for source handles) - * If specified, this handle can ONLY connect to these specific targets - * - * Example: Agent Model output can only connect to Agent node's "model" handle - * ``` - * allowedTargets: [ - * { nodeType: "uipath.agent", handleId: "model" } - * ] - * ``` - */ - allowedTargets: z.array(handleTargetSchema).optional(), - - /** - * BLACKLIST: Forbidden target nodes/handles (for source handles) - * If specified, this handle CANNOT connect to these targets - * - * Example: Control flow cannot connect to triggers - * ``` - * forbiddenTargets: [ - * { nodeType: "uipath.trigger.*" } - * ] - * ``` - */ - forbiddenTargets: z.array(handleTargetSchema).optional(), - - /** - * WHITELIST: Allowed source nodes/handles (for target handles) - * If specified, this handle can ONLY accept connections from these sources - * - * Example: Agent's "model" handle can only accept Agent Model nodes - * ``` - * allowedSources: [ - * { nodeType: "uipath.ai.agent-model" } - * ] - * ``` - */ - allowedSources: z.array(handleTargetSchema).optional(), - - /** - * BLACKLIST: Forbidden source nodes/handles (for target handles) - * If specified, this handle CANNOT accept connections from these sources - */ - forbiddenSources: z.array(handleTargetSchema).optional(), - - /** - * Required target categories - * If specified, can only connect to nodes in these categories - * Example: ["agent", "data-and-tools"] - */ - allowedTargetCategories: z.array(z.string()).optional(), - - /** - * Forbidden target categories - * If specified, cannot connect to nodes in these categories - * Example: ["control-flow", "trigger"] - */ - forbiddenTargetCategories: z.array(z.string()).optional(), - - /** - * Required source categories (for target handles) - */ - allowedSourceCategories: z.array(z.string()).optional(), - - /** - * Forbidden source categories (for target handles) - */ - forbiddenSourceCategories: z.array(z.string()).optional(), - - /** - * Custom validation expression - * Template expression that must evaluate to true for connection to be valid - * Context: sourceNode, targetNode, sourceHandle, targetHandle - * Example: "{sourceNode.model.agentType === targetNode.model.agentType}" - */ - customValidation: z.string().optional(), - - /** - * Error message to show when connection is invalid - * Supports template expressions - * Example: "Agent Model can only connect to Agent node's model handle" - */ - validationMessage: z.string().optional(), - - /** - * Severity level for constraint violations - * Controls whether violations appear as errors or warnings - * - 'error' (default): Blocks execution/publish - * - 'warning': Shows advisory badge but doesn't block - */ - severity: z.enum(['warning', 'error', 'critical', 'info']).optional(), -}); - -// Export inferred types -export type HandleTarget = z.infer; -export type ConnectionConstraint = z.infer; +export type { HandleTarget, ConnectionConstraint } from '@uipath/flow-node-schema'; +export { handleTargetSchema, connectionConstraintSchema } from '@uipath/flow-node-schema'; diff --git a/packages/apollo-react/src/canvas/schema/node-definition/handle.ts b/packages/apollo-react/src/canvas/schema/node-definition/handle.ts index 89736b63a..61f26d002 100644 --- a/packages/apollo-react/src/canvas/schema/node-definition/handle.ts +++ b/packages/apollo-react/src/canvas/schema/node-definition/handle.ts @@ -1,153 +1,19 @@ -/** - * Handle Configuration Schemas - * - * Zod schemas for node handle (input/output port) configurations. - */ - -import { z } from 'zod'; -import { connectionConstraintSchema } from './constraints'; - -/** - * Position enum for handle placement - * Uses XYFlow Position enum values - */ -export const handlePositionSchema = z.enum(['left', 'top', 'right', 'bottom']); - -/** - * Handle type (source or target) - */ -export const handleTypeSchema = z.enum(['source', 'target']); - -/** - * Handle display type (artifact, input, or output) - */ -export const handleTypeDisplaySchema = z.enum(['artifact', 'input', 'output']); - -export const handleConfigurationSpecificPositionSchema = z.object({ - /** The height of the area where the handles will be located in the node. Has no effect if no top or bottom is set. */ - height: z.number().optional(), - - /** The width of the area where the handles will be located in the node. Has no effect if no left or right is set. */ - width: z.number().optional(), - - /** The top offset of where the node handles should be placed */ - top: z.number().optional(), - - /** The bottom offset of where the node handles should be placed */ - bottom: z.number().optional(), - - /** The left offset of where the node handles should be placed */ - left: z.number().optional(), - - /** The right offset of where the node handles should be placed */ - right: z.number().optional(), -}); - -/** - * Individual handle configuration - */ -export const handleManifestSchema = z.object({ - /** Unique identifier for this handle */ - id: z.string().min(1), - - /** Type of handle */ - type: handleTypeSchema, - - /** Category of handle */ - handleType: handleTypeDisplaySchema, - - /** - * Label template (supports expressions) - * Examples: - * - Literal: "True" - * - Dynamic: "{parameters.trueLabel || 'True'}" - * - Concatenation: "Case {index + 1}: {name}" - */ - label: z.string().optional(), - - /** - * Visibility expression (supports expressions) - * Examples: - * - Literal: true - * - Dynamic: "{inputs.hasDefault}" - * Defaults to true if not specified - */ - visible: z.union([z.boolean(), z.string()]).optional(), - - /** - * Repeat expression - generates multiple handles from an array - * The expression should evaluate to an array in the node data. - * Each array item generates one handle instance. - * Examples: - * - "inputs.cases" - iterates over the cases array - * - * Within id/label templates, use: - * - {index} - current array index (0-based) - * - {item} - current array item - * - {item.propertyName} - access item properties - */ - repeat: z.string().optional(), - - /** - * Custom variable name for the current item in repeat expressions - * Defaults to "item" if not specified - */ - itemVar: z.string().optional(), - - /** - * Custom variable name for the current index in repeat expressions - * Defaults to "index" if not specified - */ - indexVar: z.string().optional(), - - /** Whether to show action button */ - showButton: z.boolean().optional(), - - /** Connection constraints for this handle */ - constraints: connectionConstraintSchema.optional(), - - /** Whether this handle is the default for its type. Helps determine how to connect when node is newly added. */ - isDefaultForType: z.boolean().optional(), -}); - -/** - * Group of handles at a specific position - */ -export const handleGroupManifestSchema = z.object({ - /** Position on the node */ - position: handlePositionSchema, - - customPositionAndOffsets: handleConfigurationSpecificPositionSchema.optional(), - - /** Array of handles at this position */ - handles: z.array(handleManifestSchema), - - /** Whether the handle group is visible */ - visible: z.boolean().optional(), -}); - -/** - * Instance-level handle group replacement - * Allows individual nodes to completely replace a handle group - */ -export const handleGroupOverrideSchema = z.object({ - /** Position identifier (matches manifest group) */ - position: handlePositionSchema, - - /** Replacement handles for this position */ - handles: z.array(handleManifestSchema), - - /** Whether the handle group is visible */ - visible: z.boolean().optional(), -}); - -// Export inferred types -export type HandlePosition = z.infer; -export type HandleType = z.infer; -export type HandleCategory = z.infer; -export type HandleManifest = z.infer; -export type HandleGroupManifest = z.infer; -export type HandleGroupOverride = z.infer; -export type HandleConfigurationSpecificPosition = z.infer< - typeof handleConfigurationSpecificPositionSchema ->; +export type { + HandlePosition, + HandleType, + HandleCategory, + HandleManifest, + HandleGroupManifest, + HandleGroupOverride, + HandleConfigurationSpecificPosition, +} from '@uipath/flow-node-schema'; + +export { + handlePositionSchema, + handleTypeSchema, + handleTypeDisplaySchema, + handleConfigurationSpecificPositionSchema, + handleManifestSchema, + handleGroupManifestSchema, + handleGroupOverrideSchema, +} from '@uipath/flow-node-schema'; diff --git a/packages/apollo-react/src/canvas/schema/node-definition/index.ts b/packages/apollo-react/src/canvas/schema/node-definition/index.ts index c2edaeeab..c6a0813cc 100644 --- a/packages/apollo-react/src/canvas/schema/node-definition/index.ts +++ b/packages/apollo-react/src/canvas/schema/node-definition/index.ts @@ -1,23 +1,26 @@ -export type { CategoryManifest } from './category-manifest'; -export { categoryManifestSchema } from './category-manifest'; -export type { ConnectionConstraint, HandleTarget } from './constraints'; -// Re-export types export type { + CategoryManifest, + ConnectionConstraint, HandleCategory, + HandleConfigurationSpecificPosition, HandleGroupManifest, HandleGroupOverride, HandleManifest, HandlePosition, + HandleTarget, HandleType, -} from './handle'; -// Re-export schemas +} from '@uipath/flow-node-schema'; + export { + categoryManifestSchema, handleGroupManifestSchema, handleGroupOverrideSchema, handleManifestSchema, handlePositionSchema, handleTypeDisplaySchema, handleTypeSchema, -} from './handle'; +} from '@uipath/flow-node-schema'; + +// Re-export from local file to preserve narrowed NodeManifest type (form?: FormSchema) export type { NodeDisplayManifest, NodeManifest, NodeShape } from './node-manifest'; export { nodeDisplayManifestSchema, nodeManifestSchema, nodeShapeSchema } from './node-manifest'; diff --git a/packages/apollo-react/src/canvas/schema/node-definition/node-manifest.ts b/packages/apollo-react/src/canvas/schema/node-definition/node-manifest.ts index 9d2281a4f..4ffd2434e 100644 --- a/packages/apollo-react/src/canvas/schema/node-definition/node-manifest.ts +++ b/packages/apollo-react/src/canvas/schema/node-definition/node-manifest.ts @@ -1,124 +1,26 @@ -/** - * Node Manifest Schemas - * - * Zod schemas for server-provided node manifests. - */ - import type { FormSchema } from '@uipath/apollo-wind'; +import { + nodeManifestSchema as baseNodeManifestSchema, + type NodeManifest as BaseNodeManifest, +} from '@uipath/flow-node-schema'; import { z } from 'zod'; -import { toolbarConfigurationSchema } from '../toolbar'; -import { handleGroupManifestSchema } from './handle'; - -/** - * Node shape for display - */ -export const nodeShapeSchema = z.enum(['circle', 'square', 'rectangle']); -/** - * Debug configuration for a node - */ -export const nodeDebugManifestSchema = z.object({ - /** Debug configuration runtime */ - runtime: z.string().min(1), -}); +export type { NodeShape, NodeDisplayManifest } from '@uipath/flow-node-schema'; +export { nodeShapeSchema, nodeDisplayManifestSchema } from '@uipath/flow-node-schema'; /** - * Display configuration for a node + * Node manifest schema with `form` narrowed to FormSchema from @uipath/apollo-wind. + * Extends the base schema from @uipath/flow-node-schema so that + * `z.infer` matches the exported `NodeManifest` type. */ -export const nodeDisplayManifestSchema = z.object({ - /** Human-readable display name */ - label: z.string().min(1), - - /** Description of what the node does */ - description: z.string().optional(), - - /** Icon identifier (e.g., "timer", "uipath.decision") */ - icon: z.string().min(1), - - /** Shape of the node */ - shape: nodeShapeSchema.optional(), - - /** Text/foreground color */ - color: z.string().optional(), - - /** Background gradient/color */ - background: z.string().optional(), - - /** Icon background color */ - iconBackground: z.string().optional(), - - /** Icon background color for dark mode (optional - if not provided, iconBackground will be used for both modes) */ - iconBackgroundDark: z.string().optional(), - - /** Icon color */ - iconColor: z.string().optional(), +export const nodeManifestSchema = baseNodeManifestSchema.extend({ + form: z.custom().optional(), }); /** - * Complete node manifest from server + * NodeManifest with the `form` field narrowed to the full FormSchema type + * from @uipath/apollo-wind, preserving backward compatibility for apollo-react consumers. */ -export const nodeManifestSchema = z.object({ - // Core identification - /** Unique node type identifier (e.g., "uipath.control-flow.decision") */ - nodeType: z.string().min(1), - - /** Version of the node definition */ - version: z.string().min(1), - - /** Human-readable description (optional) */ - description: z.string().optional(), - - // Categorization - /** Category ID this node belongs to (optional - nodes can exist at root level) */ - category: z.string().min(1).optional(), - - /** Tags for search and filtering */ - tags: z.array(z.string()), - - /** Sort order within category */ - sortOrder: z.number().int().nonnegative(), - - // Visual configuration (required) - /** Display configuration including label, icon, colors, etc. */ - display: nodeDisplayManifestSchema, - - // Node structure - /** Handle configurations (simple array of groups) */ - handleConfiguration: z.array(handleGroupManifestSchema), - - /** Toolbar extensions per mode (adds to global defaults) */ - toolbarExtensions: toolbarConfigurationSchema.optional(), - - /** Input defaults for the node */ - inputDefaults: z.record(z.string(), z.unknown()).optional(), - - /** Input definition for the node, record of string to any */ - inputDefinition: z.record(z.string(), z.any()).optional(), - - /** Output definition for the node, record of string to any */ - outputDefinition: z.record(z.string(), z.any()).optional(), - - /** Whether this node type supports drill-in (has a child canvas) */ - drillable: z.boolean().optional(), - - /** Debug configuration for the node */ - debug: nodeDebugManifestSchema.optional(), - - /** Model definition for the node, open for consumers to implement */ - model: z.any().optional(), - - /** Default property values when creating a new node */ - defaultProperties: z.record(z.string(), z.unknown()).optional(), - - /** Form schema for the properties panel (uses MetadataForm from @uipath/apollo-wind) */ - form: z.custom().optional(), - - // Optional metadata - /** Whether the node is deprecated */ - deprecated: z.boolean().optional(), -}); - -// Export inferred types -export type NodeShape = z.infer; -export type NodeDisplayManifest = z.infer; -export type NodeManifest = z.infer; +export type NodeManifest = Omit & { + form?: FormSchema; +}; diff --git a/packages/apollo-react/src/canvas/schema/node-instance/base.ts b/packages/apollo-react/src/canvas/schema/node-instance/base.ts index 895b55ea5..3e788d413 100644 --- a/packages/apollo-react/src/canvas/schema/node-instance/base.ts +++ b/packages/apollo-react/src/canvas/schema/node-instance/base.ts @@ -1,53 +1,13 @@ -import { z } from 'zod'; -import { nodeShapeSchema } from '../node-definition/node-manifest'; - -/** - * Unique identifier for nodes, types, and workflows - */ -export const idSchema = z.string().min(1); - -/** - * Semantic versioning string (e.g., "1.0.0") - */ -export const versionSchema = z - .string() - .regex(/^\d+\.\d+\.\d+$/, 'Version must be in semver format (e.g., "1.0.0")'); - -/** - * Composite key for type lookup: "type:version" - */ -export const typeVersionKeySchema = z - .string() - .regex( - /^.+:\d+\.\d+\.\d+$/, - 'TypeVersionKey must be in format "type:version" (e.g., "myType:1.0.0")' - ); - -/** - * Display configuration for UI rendering - */ -export const displayConfigSchema = z - .object({ - label: z.string().optional(), - subLabel: z.any().optional(), // Can be string or React.ReactNode - shape: nodeShapeSchema.optional(), - background: z.string().optional(), - iconBackground: z.string().optional(), - iconBackgroundDark: z.string().optional(), - iconColor: z.string().optional(), - icon: z.string().optional(), - iconElement: z.any().optional(), // React.ReactNode for custom icons - color: z.string().optional(), - labelTooltip: z.string().optional(), - labelBackgroundColor: z.string().optional(), - centerAdornmentComponent: z.any().optional(), // React.ReactNode - footerComponent: z.any().optional(), // React.ReactNode - footerVariant: z.string().optional(), // FooterVariant enum - }) - .catchall(z.unknown()); // Allow additional properties - -// Export inferred TypeScript types -export type InstanceId = z.infer; -export type InstanceVersion = z.infer; -export type InstanceTypeVersionKey = z.infer; -export type InstanceDisplayConfig = z.infer; +export type { + InstanceId, + InstanceVersion, + InstanceTypeVersionKey, + InstanceDisplayConfig, +} from '@uipath/flow-node-schema'; + +export { + idSchema, + versionSchema, + typeVersionKeySchema, + displayConfigSchema, +} from '@uipath/flow-node-schema'; diff --git a/packages/apollo-react/src/canvas/schema/node-instance/edge.ts b/packages/apollo-react/src/canvas/schema/node-instance/edge.ts index f5933bf83..0cd84cabe 100644 --- a/packages/apollo-react/src/canvas/schema/node-instance/edge.ts +++ b/packages/apollo-react/src/canvas/schema/node-instance/edge.ts @@ -1,25 +1,2 @@ -import { z } from 'zod'; -import { idSchema } from './base'; - -/** - * A connection between nodes in a workflow - */ -export const edgeSchema = z.object({ - id: idSchema, - sourceNodeId: idSchema, - /** The source port name (output port) */ - sourcePort: z.string().min(1), - targetNodeId: idSchema, - /** The target port name (input port) */ - targetPort: z.string().min(1), - data: z - .object({ - /** Optional label displayed at the edge midpoint */ - label: z.string().optional(), - }) - .passthrough() - .optional(), -}); - -// Export inferred TypeScript type -export type EdgeInstance = z.infer; +export type { EdgeInstance } from '@uipath/flow-node-schema'; +export { edgeSchema } from '@uipath/flow-node-schema'; diff --git a/packages/apollo-react/src/canvas/schema/node-instance/index.ts b/packages/apollo-react/src/canvas/schema/node-instance/index.ts index 309d1f529..5f30b7a11 100644 --- a/packages/apollo-react/src/canvas/schema/node-instance/index.ts +++ b/packages/apollo-react/src/canvas/schema/node-instance/index.ts @@ -1,13 +1,19 @@ -// Re-export types export type { InstanceDisplayConfig, InstanceId, InstanceTypeVersionKey, InstanceVersion, -} from './base'; -// Re-export schemas -export { displayConfigSchema, idSchema, typeVersionKeySchema, versionSchema } from './base'; -export type { EdgeInstance } from './edge'; -export { edgeSchema } from './edge'; -export type { InstanceUiConfig, NodeInstance } from './node'; -export { nodeSchema, uiSchema } from './node'; + EdgeInstance, + InstanceUiConfig, + NodeInstance, +} from '@uipath/flow-node-schema'; + +export { + displayConfigSchema, + idSchema, + typeVersionKeySchema, + versionSchema, + edgeSchema, + nodeSchema, + uiSchema, +} from '@uipath/flow-node-schema'; diff --git a/packages/apollo-react/src/canvas/schema/node-instance/node.ts b/packages/apollo-react/src/canvas/schema/node-instance/node.ts index 46c3fc9e9..63165750d 100644 --- a/packages/apollo-react/src/canvas/schema/node-instance/node.ts +++ b/packages/apollo-react/src/canvas/schema/node-instance/node.ts @@ -1,66 +1,2 @@ -import { z } from 'zod'; -import { handleGroupOverrideSchema } from '../node-definition/handle'; -import { displayConfigSchema, idSchema, versionSchema } from './base'; - -/** - * UI configuration for node rendering (where/how big on canvas) - */ -export const uiSchema = z - .object({ - /** Canvas position */ - position: z.object({ - x: z.number(), - y: z.number(), - }), - size: z - .object({ - width: z.number(), - height: z.number(), - }) - .optional(), - collapsed: z.boolean().optional(), - }) - .catchall(z.unknown()); // Allow additional UI properties - -/** - * A node instance in a workflow - */ -export const nodeSchema = z.object({ - id: idSchema, - type: z.string().min(1), - typeVersion: versionSchema, - - /** UI configuration (position, size, collapsed, etc.) */ - ui: uiSchema, - - /** Display overrides for the node instance */ - display: displayConfigSchema, - - /** Input port values/connections */ - inputs: z.record(z.string(), z.unknown()).optional(), - - /** - * Output port values/connections - * @deprecated Outputs are now tracked at workflow level as variables with direction='out' - * This field is kept for backward compatibility but should not be written to - */ - outputs: z.record(z.string(), z.unknown()).optional(), - - /** Handle customizations for this instance */ - handleCustomization: z - .object({ - groups: z.array(handleGroupOverrideSchema), - }) - .optional(), - - /** Model data */ - model: z.unknown().optional(), - - /** Parent node ID */ - parentId: idSchema.optional(), -}); - -// Export inferred TypeScript types -export type InstanceUiConfig = z.infer; - -export type NodeInstance = z.infer; +export type { InstanceUiConfig, NodeInstance } from '@uipath/flow-node-schema'; +export { uiSchema, nodeSchema } from '@uipath/flow-node-schema'; diff --git a/packages/apollo-react/src/canvas/schema/toolbar.ts b/packages/apollo-react/src/canvas/schema/toolbar.ts index a944a859d..a3e409054 100644 --- a/packages/apollo-react/src/canvas/schema/toolbar.ts +++ b/packages/apollo-react/src/canvas/schema/toolbar.ts @@ -1,88 +1,13 @@ -/** - * Toolbar Configuration Types - * - * Defines toolbar actions and mode-specific configurations - */ - -import { z } from 'zod'; - -/** - * Toolbar action definition - */ -export const toolbarActionSchema = z.object({ - /** Unique action identifier */ - id: z.string(), - - /** Lucide icon name */ - icon: z.string(), - - /** Display label */ - label: z.string(), - - /** Optional keyboard shortcut hint */ - shortcut: z.string().optional(), - - /** Optional condition for visibility */ - condition: z - .object({ - /** Required permissions */ - requiresPermissions: z.array(z.string()).optional(), - - /** Hide when execution state exists */ - hideOnExecution: z.boolean().optional(), - - /** Show only for specific node types */ - nodeTypes: z.array(z.string()).optional(), - - /** Show only if node has handles defined in its manifest */ - handles: z - .array( - z.object({ - handleType: z.enum(['output', 'input', 'artifact']), - type: z.enum(['source', 'target']).optional(), - }) - ) - .optional(), - }) - .optional(), -}); - -/** - * Mode-specific toolbar configuration - */ -export const modeToolbarConfigSchema = z.object({ - /** Primary visible actions */ - actions: z.array(toolbarActionSchema), - - /** Overflow menu actions */ - overflowActions: z.array(toolbarActionSchema).optional(), -}); - -/** - * Default toolbar configuration modes - */ -export const toolbarConfigurationSchema = z.record(z.string(), modeToolbarConfigSchema); - -// Export inferred types -export type ToolbarActionSchema = z.infer; -export type ModeToolbarConfig = z.infer; -export type ToolbarConfiguration = z.infer; - -/** - * Toolbar action event payload - */ -export interface ToolbarActionEvent { - /** The action identifier (e.g., 'delete', 'duplicate', 'breakpoint') */ - actionId: string; - /** The node ID the action was triggered on */ - nodeId: string; - /** Current canvas mode when action was triggered */ - mode: string; - /** Optional node data for context */ - nodeData?: Record; -} - -/** - * Callback signature for toolbar action handler - */ -export type ToolbarActionHandler = (event: ToolbarActionEvent) => void; +export type { + ToolbarActionSchema, + ModeToolbarConfig, + ToolbarConfiguration, + ToolbarActionEvent, + ToolbarActionHandler, +} from '@uipath/flow-node-schema'; + +export { + toolbarActionSchema, + modeToolbarConfigSchema, + toolbarConfigurationSchema, +} from '@uipath/flow-node-schema'; diff --git a/packages/flow-node-schema/.releaserc.json b/packages/flow-node-schema/.releaserc.json new file mode 100644 index 000000000..19455be9f --- /dev/null +++ b/packages/flow-node-schema/.releaserc.json @@ -0,0 +1,45 @@ +{ + "extends": "semantic-release-monorepo", + "branches": ["main"], + "tagFormat": "@uipath/flow-node-schema@${version}", + "plugins": [ + [ + "@semantic-release/commit-analyzer", + { + "preset": "conventionalcommits" + } + ], + [ + "@semantic-release/release-notes-generator", + { + "preset": "conventionalcommits" + } + ], + [ + "@semantic-release/changelog", + { + "changelogFile": "CHANGELOG.md" + } + ], + [ + "@semantic-release/npm", + { + "npmPublish": false + } + ], + [ + "@semantic-release/exec", + { + "publishCmd": "bash ../../scripts/publish-to-registries.sh --no-git-checks --access public" + } + ], + [ + "@semantic-release/github", + { + "successComment": false, + "failComment": false, + "releasedLabels": false + } + ] + ] +} diff --git a/packages/flow-node-schema/README.md b/packages/flow-node-schema/README.md new file mode 100644 index 000000000..be1e0e3e0 --- /dev/null +++ b/packages/flow-node-schema/README.md @@ -0,0 +1,64 @@ +# @uipath/flow-node-schema + +Lightweight [Zod](https://zod.dev) schemas for UiPath flow/canvas node definitions. Zero React or UI dependencies — only `zod` as a runtime dependency. + +## Install + +```bash +pnpm add @uipath/flow-node-schema +``` + +## Usage + +```typescript +import { + nodeManifestSchema, + nodeSchema, + edgeSchema, + categoryManifestSchema, +} from '@uipath/flow-node-schema'; + +// Validate a node manifest from the server +const result = nodeManifestSchema.safeParse(data); +if (result.success) { + console.log(result.data.nodeType); +} + +// Infer TypeScript types from schemas +import type { NodeManifest, NodeInstance, EdgeInstance } from '@uipath/flow-node-schema'; +``` + +## Exported Schemas + +### Node Definitions + +| Schema | Description | +|--------|-------------| +| `nodeManifestSchema` | Server-provided node type definition | +| `nodeDisplayManifestSchema` | Visual display configuration | +| `nodeShapeSchema` | Node shape enum (`circle`, `square`, `rectangle`) | +| `categoryManifestSchema` | Hierarchical category definition | +| `handleManifestSchema` | Individual handle (port) configuration | +| `handleGroupManifestSchema` | Group of handles at a position | +| `connectionConstraintSchema` | Connection rules between nodes | + +### Node Instances + +| Schema | Description | +|--------|-------------| +| `nodeSchema` | A node instance in a workflow | +| `edgeSchema` | A connection between nodes | +| `displayConfigSchema` | Runtime display overrides | +| `idSchema` | Non-empty string identifier | +| `versionSchema` | Semver string validation | + +### Toolbar + +| Schema | Description | +|--------|-------------| +| `toolbarActionSchema` | Toolbar action definition | +| `toolbarConfigurationSchema` | Mode-specific toolbar config | + +## License + +MIT diff --git a/packages/flow-node-schema/package.json b/packages/flow-node-schema/package.json new file mode 100644 index 000000000..96e4fb57f --- /dev/null +++ b/packages/flow-node-schema/package.json @@ -0,0 +1,58 @@ +{ + "name": "@uipath/flow-node-schema", + "version": "1.0.0", + "description": "Lightweight Zod schemas for UiPath flow/canvas definitions — no React or UI dependencies", + "repository": { + "type": "git", + "url": "https://github.com/UiPath/apollo-ui.git", + "directory": "packages/flow-node-schema" + }, + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "rslib build", + "dev": "rslib build --watch", + "lint": "biome lint src", + "lint:fix": "biome lint --write src", + "format": "biome format --write src", + "format:check": "biome format src", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", + "clean": "rm -rf dist" + }, + "keywords": [ + "zod", + "schema", + "flow", + "canvas", + "workflow", + "apollo", + "uipath" + ], + "license": "MIT", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + }, + "dependencies": { + "zod": "^4.3.5" + }, + "devDependencies": { + "@rslib/core": "^0.19.6", + "@vitest/coverage-v8": "^4.1.0", + "typescript": "^5.9.3", + "vitest": "^4.1.0" + } +} diff --git a/packages/flow-node-schema/rslib.config.ts b/packages/flow-node-schema/rslib.config.ts new file mode 100644 index 000000000..729b8775b --- /dev/null +++ b/packages/flow-node-schema/rslib.config.ts @@ -0,0 +1,39 @@ +import { defineConfig } from '@rslib/core'; + +export default defineConfig({ + lib: [ + { + format: 'esm', + output: { + distPath: { + root: './dist', + }, + filename: { + js: '[name].js', + }, + }, + dts: true, + }, + { + format: 'cjs', + output: { + distPath: { + root: './dist', + }, + filename: { + js: '[name].cjs', + }, + }, + dts: false, + }, + ], + source: { + entry: { + index: './src/index.ts', + }, + }, + output: { + target: 'web', + cleanDistPath: true, + }, +}); diff --git a/packages/flow-node-schema/src/__tests__/schemas.test.ts b/packages/flow-node-schema/src/__tests__/schemas.test.ts new file mode 100644 index 000000000..45e659741 --- /dev/null +++ b/packages/flow-node-schema/src/__tests__/schemas.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from 'vitest'; +import { categoryManifestSchema, edgeSchema, nodeManifestSchema, nodeSchema } from '../index'; + +describe('flow-node-schema', () => { + it('validates a minimal node manifest', () => { + const result = nodeManifestSchema.safeParse({ + nodeType: 'uipath.test', + version: '1.0.0', + tags: ['test'], + sortOrder: 0, + display: { + label: 'Test Node', + icon: 'test-icon', + }, + handleConfiguration: [], + }); + expect(result.success).toBe(true); + }); + + it('rejects a node manifest missing required fields', () => { + const result = nodeManifestSchema.safeParse({}); + expect(result.success).toBe(false); + }); + + it('validates a node instance', () => { + const result = nodeSchema.safeParse({ + id: 'node-1', + type: 'uipath.test', + typeVersion: '1.0.0', + ui: { position: { x: 0, y: 0 } }, + display: {}, + }); + expect(result.success).toBe(true); + }); + + it('validates an edge instance', () => { + const result = edgeSchema.safeParse({ + id: 'edge-1', + sourceNodeId: 'node-1', + sourcePort: 'output', + targetNodeId: 'node-2', + targetPort: 'input', + }); + expect(result.success).toBe(true); + }); + + it('validates a category manifest', () => { + const result = categoryManifestSchema.safeParse({ + id: 'control-flow', + name: 'Control Flow', + sortOrder: 0, + color: '#000', + colorDark: '#fff', + icon: 'flow', + tags: [], + }); + expect(result.success).toBe(true); + }); +}); diff --git a/packages/flow-node-schema/src/index.ts b/packages/flow-node-schema/src/index.ts new file mode 100644 index 000000000..6ef2989f1 --- /dev/null +++ b/packages/flow-node-schema/src/index.ts @@ -0,0 +1,3 @@ +export * from './node-definition'; +export * from './node-instance'; +export * from './toolbar'; diff --git a/packages/flow-node-schema/src/node-definition/category-manifest.ts b/packages/flow-node-schema/src/node-definition/category-manifest.ts new file mode 100644 index 000000000..457a724e8 --- /dev/null +++ b/packages/flow-node-schema/src/node-definition/category-manifest.ts @@ -0,0 +1,67 @@ +/** + * Category Manifest Schemas + * + * Zod schemas for category definitions. + * Supports arbitrary nesting using parent references. + * Nodes define their own category membership. + */ + +import { z } from 'zod'; + +/** + * Category manifest from server + * + * Categories form a hierarchy using parentId references: + * - { id: 'control-flow', parentId: null } (root) + * - { id: 'decision', parentId: 'control-flow' } (child) + * - { id: 'advanced', parentId: 'decision' } (grandchild) + * + * Nodes define which categories they belong to via category property. + * This allows: + * - Stable category IDs (reorganization just changes parentId) + * - Nodes control their own categorization + */ +export const categoryManifestSchema = z.object({ + /** + * Unique category identifier + * + * Should be stable by version. + * Examples: 'control-flow', 'decision', 'loops', 'advanced' + */ + id: z.string().min(1), + + /** Human-readable category name */ + name: z.string().min(1), + + /** + * Parent category ID for nesting + * + * - undefined (omitted): root category + * - string: ID of parent category + * + * Categories can be reorganized by changing this field without + * affecting node references or constraint definitions. + */ + parentId: z.string().optional(), + + /** + * Sort order for display within the same parent level + * Categories at the same level are sorted by this value + */ + sortOrder: z.number().int().nonnegative(), + + /** Light mode color/gradient */ + color: z.string().min(1), + + /** Dark mode color/gradient */ + colorDark: z.string().min(1), + + /** Icon identifier */ + icon: z.string().min(1), + + /** Tags for search and filtering */ + tags: z.array(z.string()), +}); + +// Export inferred type +export type CategoryManifest = z.infer; diff --git a/packages/flow-node-schema/src/node-definition/constraints.ts b/packages/flow-node-schema/src/node-definition/constraints.ts new file mode 100644 index 000000000..1351872c8 --- /dev/null +++ b/packages/flow-node-schema/src/node-definition/constraints.ts @@ -0,0 +1,141 @@ +/** + * Connection Constraints + * + * Defines semantic rules for valid connections between nodes and handles. + * Focus on workflow composition semantics, not just data types. + */ + +import { z } from 'zod'; + +/** + * Specific node and handle targeting + * Used for precise semantic constraints like "Agent Model can only connect to Agent node's 'model' handle" + */ +export const handleTargetSchema = z.object({ + /** + * Node type this can connect to + * Examples: "uipath.agent", "uipath.trigger.*" + */ + nodeType: z.string(), + + /** + * Specific handle ID on the target node + * Example: "model", "input", "configuration" + */ + handleId: z.string().optional(), +}); + +/** + * Connection constraint configuration for a handle + */ +export const connectionConstraintSchema = z.object({ + /** + * Maximum number of connections allowed + * - 1: Single connection only + * - undefined: Unlimited connections + */ + maxConnections: z.number().int().positive().optional(), + + /** + * Minimum number of connections required (for validation) + * Example: Trigger output must connect to at least 1 node + */ + minConnections: z.number().int().nonnegative().optional(), + + /** + * WHITELIST: Allowed target nodes/handles (for source handles) + * If specified, this handle can ONLY connect to these specific targets + * + * Example: Agent Model output can only connect to Agent node's "model" handle + * ``` + * allowedTargets: [ + * { nodeType: "uipath.agent", handleId: "model" } + * ] + * ``` + */ + allowedTargets: z.array(handleTargetSchema).optional(), + + /** + * BLACKLIST: Forbidden target nodes/handles (for source handles) + * If specified, this handle CANNOT connect to these targets + * + * Example: Control flow cannot connect to triggers + * ``` + * forbiddenTargets: [ + * { nodeType: "uipath.trigger.*" } + * ] + * ``` + */ + forbiddenTargets: z.array(handleTargetSchema).optional(), + + /** + * WHITELIST: Allowed source nodes/handles (for target handles) + * If specified, this handle can ONLY accept connections from these sources + * + * Example: Agent's "model" handle can only accept Agent Model nodes + * ``` + * allowedSources: [ + * { nodeType: "uipath.ai.agent-model" } + * ] + * ``` + */ + allowedSources: z.array(handleTargetSchema).optional(), + + /** + * BLACKLIST: Forbidden source nodes/handles (for target handles) + * If specified, this handle CANNOT accept connections from these sources + */ + forbiddenSources: z.array(handleTargetSchema).optional(), + + /** + * Required target categories + * If specified, can only connect to nodes in these categories + * Example: ["agent", "data-and-tools"] + */ + allowedTargetCategories: z.array(z.string()).optional(), + + /** + * Forbidden target categories + * If specified, cannot connect to nodes in these categories + * Example: ["control-flow", "trigger"] + */ + forbiddenTargetCategories: z.array(z.string()).optional(), + + /** + * Required source categories (for target handles) + */ + allowedSourceCategories: z.array(z.string()).optional(), + + /** + * Forbidden source categories (for target handles) + */ + forbiddenSourceCategories: z.array(z.string()).optional(), + + /** + * Custom validation expression + * Template expression that must evaluate to true for connection to be valid + * Context: sourceNode, targetNode, sourceHandle, targetHandle + * Example: "{sourceNode.model.agentType === targetNode.model.agentType}" + */ + customValidation: z.string().optional(), + + /** + * Error message to show when connection is invalid + * Supports template expressions + * Example: "Agent Model can only connect to Agent node's model handle" + */ + validationMessage: z.string().optional(), + + /** + * Severity level for constraint violations + * - 'error' (default): Blocks execution/publish + * - 'critical': Blocks execution/publish with elevated prominence + * - 'warning': Shows advisory badge but doesn't block + * - 'info': Informational only, no blocking + */ + severity: z.enum(['warning', 'error', 'critical', 'info']).optional(), +}); + +// Export inferred types +export type HandleTarget = z.infer; +export type ConnectionConstraint = z.infer; diff --git a/packages/flow-node-schema/src/node-definition/handle.ts b/packages/flow-node-schema/src/node-definition/handle.ts new file mode 100644 index 000000000..89736b63a --- /dev/null +++ b/packages/flow-node-schema/src/node-definition/handle.ts @@ -0,0 +1,153 @@ +/** + * Handle Configuration Schemas + * + * Zod schemas for node handle (input/output port) configurations. + */ + +import { z } from 'zod'; +import { connectionConstraintSchema } from './constraints'; + +/** + * Position enum for handle placement + * Uses XYFlow Position enum values + */ +export const handlePositionSchema = z.enum(['left', 'top', 'right', 'bottom']); + +/** + * Handle type (source or target) + */ +export const handleTypeSchema = z.enum(['source', 'target']); + +/** + * Handle display type (artifact, input, or output) + */ +export const handleTypeDisplaySchema = z.enum(['artifact', 'input', 'output']); + +export const handleConfigurationSpecificPositionSchema = z.object({ + /** The height of the area where the handles will be located in the node. Has no effect if no top or bottom is set. */ + height: z.number().optional(), + + /** The width of the area where the handles will be located in the node. Has no effect if no left or right is set. */ + width: z.number().optional(), + + /** The top offset of where the node handles should be placed */ + top: z.number().optional(), + + /** The bottom offset of where the node handles should be placed */ + bottom: z.number().optional(), + + /** The left offset of where the node handles should be placed */ + left: z.number().optional(), + + /** The right offset of where the node handles should be placed */ + right: z.number().optional(), +}); + +/** + * Individual handle configuration + */ +export const handleManifestSchema = z.object({ + /** Unique identifier for this handle */ + id: z.string().min(1), + + /** Type of handle */ + type: handleTypeSchema, + + /** Category of handle */ + handleType: handleTypeDisplaySchema, + + /** + * Label template (supports expressions) + * Examples: + * - Literal: "True" + * - Dynamic: "{parameters.trueLabel || 'True'}" + * - Concatenation: "Case {index + 1}: {name}" + */ + label: z.string().optional(), + + /** + * Visibility expression (supports expressions) + * Examples: + * - Literal: true + * - Dynamic: "{inputs.hasDefault}" + * Defaults to true if not specified + */ + visible: z.union([z.boolean(), z.string()]).optional(), + + /** + * Repeat expression - generates multiple handles from an array + * The expression should evaluate to an array in the node data. + * Each array item generates one handle instance. + * Examples: + * - "inputs.cases" - iterates over the cases array + * + * Within id/label templates, use: + * - {index} - current array index (0-based) + * - {item} - current array item + * - {item.propertyName} - access item properties + */ + repeat: z.string().optional(), + + /** + * Custom variable name for the current item in repeat expressions + * Defaults to "item" if not specified + */ + itemVar: z.string().optional(), + + /** + * Custom variable name for the current index in repeat expressions + * Defaults to "index" if not specified + */ + indexVar: z.string().optional(), + + /** Whether to show action button */ + showButton: z.boolean().optional(), + + /** Connection constraints for this handle */ + constraints: connectionConstraintSchema.optional(), + + /** Whether this handle is the default for its type. Helps determine how to connect when node is newly added. */ + isDefaultForType: z.boolean().optional(), +}); + +/** + * Group of handles at a specific position + */ +export const handleGroupManifestSchema = z.object({ + /** Position on the node */ + position: handlePositionSchema, + + customPositionAndOffsets: handleConfigurationSpecificPositionSchema.optional(), + + /** Array of handles at this position */ + handles: z.array(handleManifestSchema), + + /** Whether the handle group is visible */ + visible: z.boolean().optional(), +}); + +/** + * Instance-level handle group replacement + * Allows individual nodes to completely replace a handle group + */ +export const handleGroupOverrideSchema = z.object({ + /** Position identifier (matches manifest group) */ + position: handlePositionSchema, + + /** Replacement handles for this position */ + handles: z.array(handleManifestSchema), + + /** Whether the handle group is visible */ + visible: z.boolean().optional(), +}); + +// Export inferred types +export type HandlePosition = z.infer; +export type HandleType = z.infer; +export type HandleCategory = z.infer; +export type HandleManifest = z.infer; +export type HandleGroupManifest = z.infer; +export type HandleGroupOverride = z.infer; +export type HandleConfigurationSpecificPosition = z.infer< + typeof handleConfigurationSpecificPositionSchema +>; diff --git a/packages/flow-node-schema/src/node-definition/index.ts b/packages/flow-node-schema/src/node-definition/index.ts new file mode 100644 index 000000000..74ab82da7 --- /dev/null +++ b/packages/flow-node-schema/src/node-definition/index.ts @@ -0,0 +1,31 @@ +export type { CategoryManifest } from './category-manifest'; +export { categoryManifestSchema } from './category-manifest'; +export type { ConnectionConstraint, HandleTarget } from './constraints'; +export { connectionConstraintSchema, handleTargetSchema } from './constraints'; +// Re-export types +export type { + HandleCategory, + HandleConfigurationSpecificPosition, + HandleGroupManifest, + HandleGroupOverride, + HandleManifest, + HandlePosition, + HandleType, +} from './handle'; +// Re-export schemas +export { + handleConfigurationSpecificPositionSchema, + handleGroupManifestSchema, + handleGroupOverrideSchema, + handleManifestSchema, + handlePositionSchema, + handleTypeDisplaySchema, + handleTypeSchema, +} from './handle'; +export type { FormSchemaLike, NodeDisplayManifest, NodeManifest, NodeShape } from './node-manifest'; +export { + nodeDebugManifestSchema, + nodeDisplayManifestSchema, + nodeManifestSchema, + nodeShapeSchema, +} from './node-manifest'; diff --git a/packages/flow-node-schema/src/node-definition/node-manifest.ts b/packages/flow-node-schema/src/node-definition/node-manifest.ts new file mode 100644 index 000000000..0247bebc1 --- /dev/null +++ b/packages/flow-node-schema/src/node-definition/node-manifest.ts @@ -0,0 +1,132 @@ +/** + * Node Manifest Schemas + * + * Zod schemas for server-provided node manifests. + */ + +import { z } from 'zod'; +import { toolbarConfigurationSchema } from '../toolbar'; +import { handleGroupManifestSchema } from './handle'; + +/** + * Generic form schema type. + * + * In apollo-react this is typed as `FormSchema` from `@uipath/apollo-wind`, + * but we keep the schema package dependency-free beyond zod. + * Consumers that need the full FormSchema type can cast after parsing. + */ +export type FormSchemaLike = Record; + +/** + * Node shape for display + */ +export const nodeShapeSchema = z.enum(['circle', 'square', 'rectangle']); + +/** + * Debug configuration for a node + */ +export const nodeDebugManifestSchema = z.object({ + /** Debug configuration runtime */ + runtime: z.string().min(1), +}); + +/** + * Display configuration for a node + */ +export const nodeDisplayManifestSchema = z.object({ + /** Human-readable display name */ + label: z.string().min(1), + + /** Description of what the node does */ + description: z.string().optional(), + + /** Icon identifier (e.g., "timer", "uipath.decision") */ + icon: z.string().min(1), + + /** Shape of the node */ + shape: nodeShapeSchema.optional(), + + /** Text/foreground color */ + color: z.string().optional(), + + /** Background gradient/color */ + background: z.string().optional(), + + /** Icon background color */ + iconBackground: z.string().optional(), + + /** Icon background color for dark mode (optional - if not provided, iconBackground will be used for both modes) */ + iconBackgroundDark: z.string().optional(), + + /** Icon color */ + iconColor: z.string().optional(), +}); + +/** + * Complete node manifest from server + */ +export const nodeManifestSchema = z.object({ + // Core identification + /** Unique node type identifier (e.g., "uipath.control-flow.decision") */ + nodeType: z.string().min(1), + + /** Version of the node definition */ + version: z.string().min(1), + + /** Human-readable description (optional) */ + description: z.string().optional(), + + // Categorization + /** Category ID this node belongs to (optional - nodes can exist at root level) */ + category: z.string().min(1).optional(), + + /** Tags for search and filtering */ + tags: z.array(z.string()), + + /** Sort order within category */ + sortOrder: z.number().int().nonnegative(), + + // Visual configuration (required) + /** Display configuration including label, icon, colors, etc. */ + display: nodeDisplayManifestSchema, + + // Node structure + /** Handle configurations (simple array of groups) */ + handleConfiguration: z.array(handleGroupManifestSchema), + + /** Toolbar extensions per mode (adds to global defaults) */ + toolbarExtensions: toolbarConfigurationSchema.optional(), + + /** Input defaults for the node */ + inputDefaults: z.record(z.string(), z.unknown()).optional(), + + /** Input definition for the node, record of string to any */ + inputDefinition: z.record(z.string(), z.any()).optional(), + + /** Output definition for the node, record of string to any */ + outputDefinition: z.record(z.string(), z.any()).optional(), + + /** Whether this node type supports drill-in (has a child canvas) */ + drillable: z.boolean().optional(), + + /** Debug configuration for the node */ + debug: nodeDebugManifestSchema.optional(), + + /** Model definition for the node, open for consumers to implement */ + model: z.any().optional(), + + /** Default property values when creating a new node */ + defaultProperties: z.record(z.string(), z.unknown()).optional(), + + /** Form schema for the properties panel */ + form: z.custom().optional(), + + // Optional metadata + /** Whether the node is deprecated */ + deprecated: z.boolean().optional(), +}); + +// Export inferred types +export type NodeShape = z.infer; +export type NodeDisplayManifest = z.infer; +export type NodeManifest = z.infer; diff --git a/packages/flow-node-schema/src/node-instance/base.ts b/packages/flow-node-schema/src/node-instance/base.ts new file mode 100644 index 000000000..7a413230c --- /dev/null +++ b/packages/flow-node-schema/src/node-instance/base.ts @@ -0,0 +1,53 @@ +import { z } from 'zod'; +import { nodeShapeSchema } from '../node-definition/node-manifest'; + +/** + * Unique identifier for nodes, types, and workflows + */ +export const idSchema = z.string().min(1); + +/** + * Semantic versioning string (e.g., "1.0.0") + */ +export const versionSchema = z + .string() + .regex(/^\d+\.\d+\.\d+$/, 'Version must be in semver format (e.g., "1.0.0")'); + +/** + * Composite key for type lookup: "type:version" + */ +export const typeVersionKeySchema = z + .string() + .regex( + /^.+:\d+\.\d+\.\d+$/, + 'TypeVersionKey must be in format "type:version" (e.g., "myType:1.0.0")' + ); + +/** + * Display configuration for UI rendering + */ +export const displayConfigSchema = z + .object({ + label: z.string().optional(), + subLabel: z.any().optional(), + shape: nodeShapeSchema.optional(), + background: z.string().optional(), + iconBackground: z.string().optional(), + iconBackgroundDark: z.string().optional(), + iconColor: z.string().optional(), + icon: z.string().optional(), + iconElement: z.any().optional(), + color: z.string().optional(), + labelTooltip: z.string().optional(), + labelBackgroundColor: z.string().optional(), + centerAdornmentComponent: z.any().optional(), + footerComponent: z.any().optional(), + footerVariant: z.string().optional(), + }) + .catchall(z.unknown()); // Allow additional properties + +// Export inferred TypeScript types +export type InstanceId = z.infer; +export type InstanceVersion = z.infer; +export type InstanceTypeVersionKey = z.infer; +export type InstanceDisplayConfig = z.infer; diff --git a/packages/flow-node-schema/src/node-instance/edge.ts b/packages/flow-node-schema/src/node-instance/edge.ts new file mode 100644 index 000000000..f5933bf83 --- /dev/null +++ b/packages/flow-node-schema/src/node-instance/edge.ts @@ -0,0 +1,25 @@ +import { z } from 'zod'; +import { idSchema } from './base'; + +/** + * A connection between nodes in a workflow + */ +export const edgeSchema = z.object({ + id: idSchema, + sourceNodeId: idSchema, + /** The source port name (output port) */ + sourcePort: z.string().min(1), + targetNodeId: idSchema, + /** The target port name (input port) */ + targetPort: z.string().min(1), + data: z + .object({ + /** Optional label displayed at the edge midpoint */ + label: z.string().optional(), + }) + .passthrough() + .optional(), +}); + +// Export inferred TypeScript type +export type EdgeInstance = z.infer; diff --git a/packages/flow-node-schema/src/node-instance/index.ts b/packages/flow-node-schema/src/node-instance/index.ts new file mode 100644 index 000000000..309d1f529 --- /dev/null +++ b/packages/flow-node-schema/src/node-instance/index.ts @@ -0,0 +1,13 @@ +// Re-export types +export type { + InstanceDisplayConfig, + InstanceId, + InstanceTypeVersionKey, + InstanceVersion, +} from './base'; +// Re-export schemas +export { displayConfigSchema, idSchema, typeVersionKeySchema, versionSchema } from './base'; +export type { EdgeInstance } from './edge'; +export { edgeSchema } from './edge'; +export type { InstanceUiConfig, NodeInstance } from './node'; +export { nodeSchema, uiSchema } from './node'; diff --git a/packages/flow-node-schema/src/node-instance/node.ts b/packages/flow-node-schema/src/node-instance/node.ts new file mode 100644 index 000000000..46c3fc9e9 --- /dev/null +++ b/packages/flow-node-schema/src/node-instance/node.ts @@ -0,0 +1,66 @@ +import { z } from 'zod'; +import { handleGroupOverrideSchema } from '../node-definition/handle'; +import { displayConfigSchema, idSchema, versionSchema } from './base'; + +/** + * UI configuration for node rendering (where/how big on canvas) + */ +export const uiSchema = z + .object({ + /** Canvas position */ + position: z.object({ + x: z.number(), + y: z.number(), + }), + size: z + .object({ + width: z.number(), + height: z.number(), + }) + .optional(), + collapsed: z.boolean().optional(), + }) + .catchall(z.unknown()); // Allow additional UI properties + +/** + * A node instance in a workflow + */ +export const nodeSchema = z.object({ + id: idSchema, + type: z.string().min(1), + typeVersion: versionSchema, + + /** UI configuration (position, size, collapsed, etc.) */ + ui: uiSchema, + + /** Display overrides for the node instance */ + display: displayConfigSchema, + + /** Input port values/connections */ + inputs: z.record(z.string(), z.unknown()).optional(), + + /** + * Output port values/connections + * @deprecated Outputs are now tracked at workflow level as variables with direction='out' + * This field is kept for backward compatibility but should not be written to + */ + outputs: z.record(z.string(), z.unknown()).optional(), + + /** Handle customizations for this instance */ + handleCustomization: z + .object({ + groups: z.array(handleGroupOverrideSchema), + }) + .optional(), + + /** Model data */ + model: z.unknown().optional(), + + /** Parent node ID */ + parentId: idSchema.optional(), +}); + +// Export inferred TypeScript types +export type InstanceUiConfig = z.infer; + +export type NodeInstance = z.infer; diff --git a/packages/flow-node-schema/src/toolbar.ts b/packages/flow-node-schema/src/toolbar.ts new file mode 100644 index 000000000..a944a859d --- /dev/null +++ b/packages/flow-node-schema/src/toolbar.ts @@ -0,0 +1,88 @@ +/** + * Toolbar Configuration Types + * + * Defines toolbar actions and mode-specific configurations + */ + +import { z } from 'zod'; + +/** + * Toolbar action definition + */ +export const toolbarActionSchema = z.object({ + /** Unique action identifier */ + id: z.string(), + + /** Lucide icon name */ + icon: z.string(), + + /** Display label */ + label: z.string(), + + /** Optional keyboard shortcut hint */ + shortcut: z.string().optional(), + + /** Optional condition for visibility */ + condition: z + .object({ + /** Required permissions */ + requiresPermissions: z.array(z.string()).optional(), + + /** Hide when execution state exists */ + hideOnExecution: z.boolean().optional(), + + /** Show only for specific node types */ + nodeTypes: z.array(z.string()).optional(), + + /** Show only if node has handles defined in its manifest */ + handles: z + .array( + z.object({ + handleType: z.enum(['output', 'input', 'artifact']), + type: z.enum(['source', 'target']).optional(), + }) + ) + .optional(), + }) + .optional(), +}); + +/** + * Mode-specific toolbar configuration + */ +export const modeToolbarConfigSchema = z.object({ + /** Primary visible actions */ + actions: z.array(toolbarActionSchema), + + /** Overflow menu actions */ + overflowActions: z.array(toolbarActionSchema).optional(), +}); + +/** + * Default toolbar configuration modes + */ +export const toolbarConfigurationSchema = z.record(z.string(), modeToolbarConfigSchema); + +// Export inferred types +export type ToolbarActionSchema = z.infer; +export type ModeToolbarConfig = z.infer; +export type ToolbarConfiguration = z.infer; + +/** + * Toolbar action event payload + */ +export interface ToolbarActionEvent { + /** The action identifier (e.g., 'delete', 'duplicate', 'breakpoint') */ + actionId: string; + /** The node ID the action was triggered on */ + nodeId: string; + /** Current canvas mode when action was triggered */ + mode: string; + /** Optional node data for context */ + nodeData?: Record; +} + +/** + * Callback signature for toolbar action handler + */ +export type ToolbarActionHandler = (event: ToolbarActionEvent) => void; diff --git a/packages/flow-node-schema/tsconfig.json b/packages/flow-node-schema/tsconfig.json new file mode 100644 index 000000000..1febc0724 --- /dev/null +++ b/packages/flow-node-schema/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "composite": true, + "declaration": true, + "declarationMap": false + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"] +} diff --git a/packages/flow-node-schema/vitest.config.ts b/packages/flow-node-schema/vitest.config.ts new file mode 100644 index 000000000..de879913a --- /dev/null +++ b/packages/flow-node-schema/vitest.config.ts @@ -0,0 +1,5 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: {}, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 81c1c1497..eab7fea91 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -575,6 +575,9 @@ importers: '@uipath/apollo-wind': specifier: workspace:* version: link:../apollo-wind + '@uipath/flow-node-schema': + specifier: workspace:* + version: link:../flow-node-schema '@xyflow/react': specifier: 12.8.2 version: 12.8.2(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -1067,6 +1070,25 @@ importers: specifier: ^4.1.0 version: 4.1.0(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/ui@4.1.0)(happy-dom@20.0.11)(jsdom@27.3.0)(msw@2.12.3(@types/node@24.10.1)(typescript@5.9.3))(vite@7.3.1(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1)) + packages/flow-node-schema: + dependencies: + zod: + specifier: ^4.3.5 + version: 4.3.5 + devDependencies: + '@rslib/core': + specifier: ^0.19.6 + version: 0.19.6(@microsoft/api-extractor@7.57.6(@types/node@24.10.1))(typescript@5.9.3) + '@vitest/coverage-v8': + specifier: ^4.1.0 + version: 4.1.0(vitest@4.1.0) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vitest: + specifier: ^4.1.0 + version: 4.1.0(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/ui@4.1.0)(happy-dom@20.0.11)(jsdom@27.3.0)(msw@2.12.3(@types/node@24.10.1)(typescript@5.9.3))(vite@7.3.1(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1)) + web-packages/ap-chat: dependencies: '@emotion/cache': @@ -12801,10 +12823,10 @@ snapshots: '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) '@babel/helpers': 7.28.4 - '@babel/parser': 7.28.5 + '@babel/parser': 7.29.2 '@babel/template': 7.27.2 '@babel/traverse': 7.28.5 - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3 @@ -12824,7 +12846,7 @@ snapshots: '@babel/helper-annotate-as-pure@7.27.3': dependencies: - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 '@babel/helper-compilation-targets@7.27.2': dependencies: @@ -12870,14 +12892,14 @@ snapshots: '@babel/helper-member-expression-to-functions@7.28.5': dependencies: '@babel/traverse': 7.28.5 - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color '@babel/helper-module-imports@7.27.1': dependencies: '@babel/traverse': 7.28.5 - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color @@ -12892,7 +12914,7 @@ snapshots: '@babel/helper-optimise-call-expression@7.27.1': dependencies: - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 '@babel/helper-plugin-utils@7.27.1': {} @@ -12917,7 +12939,7 @@ snapshots: '@babel/helper-skip-transparent-expression-wrappers@7.27.1': dependencies: '@babel/traverse': 7.28.5 - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color @@ -12931,18 +12953,18 @@ snapshots: dependencies: '@babel/template': 7.27.2 '@babel/traverse': 7.28.5 - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color '@babel/helpers@7.28.4': dependencies: '@babel/template': 7.27.2 - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 '@babel/parser@7.28.5': dependencies: - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 '@babel/parser@7.29.2': dependencies: @@ -13327,7 +13349,7 @@ snapshots: '@babel/helper-module-imports': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5) - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color @@ -13495,7 +13517,7 @@ snapshots: dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 esutils: 2.0.3 '@babel/preset-react@7.28.5(@babel/core@7.28.5)': @@ -13530,17 +13552,17 @@ snapshots: '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 '@babel/traverse@7.28.5': dependencies: '@babel/code-frame': 7.27.1 '@babel/generator': 7.28.5 '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.28.5 + '@babel/parser': 7.29.2 '@babel/template': 7.27.2 - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -14624,7 +14646,7 @@ snapshots: dependencies: '@babel/core': 7.28.5 '@babel/runtime': 7.28.4 - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 '@lingui/conf': 5.6.1(typescript@5.9.3) '@lingui/core': 5.6.1(@lingui/babel-plugin-lingui-macro@5.6.1(babel-plugin-macros@3.1.0)(typescript@5.9.3))(babel-plugin-macros@3.1.0) '@lingui/message-utils': 5.6.1 @@ -17048,7 +17070,7 @@ snapshots: '@svgr/hast-util-to-babel-ast@8.0.0': dependencies: - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 entities: 4.5.0 '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@5.9.3))': @@ -17412,16 +17434,16 @@ snapshots: '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 '@types/babel__traverse@7.28.0': dependencies: - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 '@types/chai@5.2.3': dependencies: