Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ export function NodePropertyPanel({
contentInset = '1.5rem',
children,
headerExtra,
sectionVariant,
activeStepId,
onActiveStepChange,
}: NodePropertyPanelProps) {
const hasNodeHeader = !!(nodeLabel || nodeCategory || nodeIcon || action);

Expand Down Expand Up @@ -129,14 +132,39 @@ export function NodePropertyPanel({
<div className="py-4 text-xs text-foreground-subtle [padding-inline:var(--mf-content-inset,1.5rem)]">
No form schema defined for this node.
</div>
) : formSchema.steps ? (
// Tabbed schema: MetadataForm owns the scroll so its tab bar pins under
// the header. This wrapper is a height-filling flex column, NOT the
// scroll container — the form's inset/padding move inside TabbedStepForm.
<div className="flex min-h-0 flex-1 flex-col">
<div
style={SURFACE_REMAP}
className="flex min-h-0 flex-1 flex-col [&_label]:text-foreground-muted"
>
<MetadataForm
key={resetKey}
schema={formSchema}
plugins={plugins}
stepVariant="tabs"
sectionVariant={sectionVariant}
activeStepId={activeStepId}
onActiveStepChange={onActiveStepChange}
onSubmit={onSubmit}
disabled={disabled}
className="flex min-h-0 flex-1 flex-col"
/>
</div>
</div>
) : (
// Single-page schema: classic behavior — this wrapper scrolls the whole
// form. No stepVariant: it only applies to step-based schemas.
<div className="min-h-0 flex-1 overflow-auto">
<div style={SURFACE_REMAP} className="[&_label]:text-foreground-muted">
<MetadataForm
key={resetKey}
schema={formSchema}
plugins={plugins}
stepVariant="tabs"
sectionVariant={sectionVariant}
Comment thread
CalinaCristian marked this conversation as resolved.
onSubmit={onSubmit}
disabled={disabled}
className="flex flex-col gap-4 pb-6 pt-3 [padding-inline:var(--mf-content-inset)]"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,24 @@ export interface NodePropertyPanelProps {
* verbatim to the underlying single form instance.
*/
plugins?: FormPlugin[];
/**
* Visual treatment for each form section, forwarded to `MetadataForm`.
* `'card'` (default) frames every section in a bordered box; `'plain'` drops
* the border/rounding/horizontal padding so sections read as flush headers —
* for hosts that already frame the panel and want a borderless list.
*/
sectionVariant?: 'card' | 'plain';
/**
* Controlled active tab id for a tabbed (multi-step) schema. Persist it
* across node switches to keep the user on the same tab; an id absent from
* the current node's tabs falls back to the first tab. Omit for uncontrolled.
*/
activeStepId?: string;
/**
* Fires with the tab id whenever the user selects a tab, in both controlled
* and uncontrolled mode. Pair it with `activeStepId` to persist the selection.
*/
onActiveStepChange?: (stepId: string) => void;
/** Called when the form is submitted (only when the schema defines a submit action). */
onSubmit?: (data: unknown) => void | Promise<void>;
/** Disables all fields (e.g. read-only nodes). */
Expand Down
8 changes: 8 additions & 0 deletions packages/apollo-wind/src/components/forms/form-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,14 @@ export interface FormStep {
validation?: 'onChange' | 'onBlur' | 'onSubmit';
canSkip?: boolean;
conditions?: FieldCondition[]; // Show/hide entire step
/**
* Message shown (in the `tabs` step variant) when this step has no visible
* sections. A step that sets this stays in the tab bar even while empty and
* renders the message in place of its content — e.g. a "Parameters" tab that
* always shows, reading "No available parameters" for a node with no inputs.
* Steps without both sections and `emptyState` are hidden from the tab bar.
*/
emptyState?: string;
}

export interface FormAction {
Expand Down
129 changes: 129 additions & 0 deletions packages/apollo-wind/src/components/forms/metadata-form.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { useState } from 'react';
import {
Controller,
type FieldValues,
Expand Down Expand Up @@ -877,6 +878,134 @@ export const TabbedSteps: Story = {
},
};

// A tabbed node schema exercising both section shapes the properties panel
// produces: Parameters carries two titled, collapsible sections (where the two
// sectionVariants differ visibly), while Error handling and Advanced each hold a
// single untitled, non-collapsible section. That mirrors the consumer rule that a
// tab's lone section drops the collapse affordance and title because the tab
// already labels it. `card` frames each section in its own bordered box; `plain`
// renders flush headers separated by hairline dividers (the host frames the
// panel).
const sectionVariantSchema: FormSchema = {
id: 'section-variant-demo',
title: 'Node properties',
actions: [],
initialData: {
'inputs.method': 'GET',
'inputs.url': 'https://api.example.com/v1/orders',
'inputs.timeout': 30,
'inputs.errorHandlingEnabled': false,
nodeId: 'httpRequest1',
'display.label': 'HTTP request',
},
steps: [
{
id: 'parameters',
title: 'Parameters',
sections: [
{
id: 'connection',
title: 'Connection',
collapsible: true,
defaultExpanded: true,
fields: [
{
name: 'inputs.method',
type: 'select',
label: 'Method',
options: [
{ label: 'GET', value: 'GET' },
{ label: 'POST', value: 'POST' },
{ label: 'PUT', value: 'PUT' },
],
},
{ name: 'inputs.url', type: 'text', label: 'URL' },
],
},
{
id: 'options',
title: 'Options',
collapsible: true,
defaultExpanded: true,
fields: [{ name: 'inputs.timeout', type: 'number', label: 'Timeout (s)' }],
},
],
},
{
id: 'error-handling',
title: 'Error handling',
sections: [
// Sole section in its tab: no title, no collapse. The tab labels it.
{
id: 'error',
fields: [
{
name: 'inputs.errorHandlingEnabled',
type: 'switch',
label: 'Enable error handling',
description: 'Add an error output handle on the node to catch and handle failures.',
},
],
},
],
},
{
id: 'advanced',
title: 'Advanced',
sections: [
// Sole section in its tab: no title, no collapse. The tab labels it.
{
id: 'general',
fields: [
{ name: 'nodeId', type: 'text', label: 'ID', disabled: true },
{ name: 'display.label', type: 'text', label: 'Label' },
{ name: 'display.description', type: 'textarea', label: 'Description' },
],
},
],
},
],
};

/**
* Section variants (card vs plain)
*
* Flip the toggle to compare the two `sectionVariant` treatments live. `card`
* (default) wraps each section in its own bordered, rounded box. `plain` drops
* the border, rounding, and horizontal padding so sections read as flush headers
* over their content. `plain` assumes the host already frames the panel, so the
* form is rendered inside a narrow panel-like container here to match that use
* case (the canvas node properties panel).
*/
export const SectionVariants: Story = {
render: (args) => {
const [plain, setPlain] = useState(true);
return (
// Canvas backdrop + panel frame matching the NodePropertyPanel stories:
// the canvas sits on `--surface` and the docked panel is a raised,
// subtly-bordered card (bg-surface-raised) floating over it. The variant
// toggle floats on the canvas, centered above the panel it controls.
<div className="flex flex-col items-center gap-6 rounded-lg bg-surface p-10">
<div className="flex items-center gap-2">
<Switch id="section-variant-toggle" checked={plain} onCheckedChange={setPlain} />
<Label htmlFor="section-variant-toggle">
Plain sections (host-framed) {'·'} currently{' '}
<code>sectionVariant="{plain ? 'plain' : 'card'}"</code>
</Label>
</div>
<div className="w-[380px] overflow-hidden rounded-2xl border border-border-subtle bg-surface-raised px-4 py-3 shadow-lg">
<MetadataForm
{...args}
schema={sectionVariantSchema}
stepVariant="tabs"
sectionVariant={plain ? 'plain' : 'card'}
/>
</div>
</div>
);
},
};

// ============================================================================
// FILE UPLOAD
// ============================================================================
Expand Down
Loading
Loading