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
22 changes: 22 additions & 0 deletions .changeset/10948-flow-inspector-required-keys.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
'@object-ui/app-shell': minor
---

feat(app-shell): the flow node inspector marks the config keys the installed spec refuses the node
without (objectui#10948).

`@objectstack/spec` 17.5.0 refuses, at authoring, a flow node whose executor could not run it: a key
its config contract requires left out (a CRUD node's `objectName`, an `http` node's `url`, a
`notify` node's `title` while it has no `template`, a `script` node's `function`, a `subflow` or
`map` node's `flowName`, …), a decision branch with no `label` or `expression`, a screen field with
no `name`, and a `connector_action` whose `connectorConfig` names no connector or action. The
designer's live flow check already reports each one at the node's config path; the inspector now
marks the same keys before the author reaches that error, with the metadata form's own required
marker — the `*` in the field label, the row label of a branch or screen-field row, and
`aria-required` on the control where the inspector owns it.

The inspector keeps no list of required keys. Each marker is the installed spec's own verdict, asked
of the node as it stands: the key is removed from a copy of the node and handed to the judges the
flow parse runs (`flowNodeConfigRefusals`, the predicate-slot walk, and `FlowNodeSchema`), so a
requirement that depends on the configuration — `notify`'s `title` without a `template`, a `loop`'s
`collection` once it has a body, a refused `end`'s `message` — is marked only while it applies.
15 changes: 10 additions & 5 deletions content/docs/guide/flow-designer.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,16 @@ built-in defaults, so authoring still works offline.

## The node inspector

Selecting a node opens its inspector: **ID**, **Label**, **Node Type**, an
optional **Description**, and a **Configuration** section. New nodes start with
spec-valid defaults (a *Wait* node already carries a timer config, an *HTTP*
node defaults to `GET`) so a freshly dropped block is never in a broken
intermediate state.
Selecting a node opens its inspector: **ID**, **Label**, **Node Type**, and a
**Configuration** section. New nodes start with defaults where the spec allows
one: a *Wait* node already carries a timer config, and an *HTTP* node defaults to
`GET`. What the author must still supply is marked with a red `*`, the same
required marker the other metadata forms use. That covers an *HTTP* node's URL, a
record node's object, a decision branch's label and expression, and a screen
field's name. The marker follows the installed spec, so a key required only in
some configurations (a *Notify* node's title while it has no template) is marked
only while it applies. Until the value is supplied, the spec's error is shown on
that node and listed in the **Problems** panel.

For node types whose engine executor publishes a `configSchema` (ADR-0018), the
inspector renders a **server-driven property form** from that schema — so a
Expand Down
10 changes: 10 additions & 0 deletions packages/app-shell/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,16 @@ variables* shape and an *email/SMS* notification shape (*Template* / *Recipients
*Wait for* mode. A conditional field is never hidden while it still holds a
value, so existing config is always reachable.

A config key the installed `@objectstack/spec` refuses the node without — an
`http` node's *URL*, a record node's *Object*, a decision branch's *Label* and
*Expression*, a screen field's *Name* — carries the same required marker (`*`)
`SchemaForm` draws, and a control the inspector renders itself also carries
`aria-required`. No list of required keys is kept here: `flow-required-keys.ts`
removes the key from a copy of the node and asks the spec's own judges
(`flowNodeConfigRefusals`, the predicate-slot walk, `FlowNodeSchema`). So a
rule-dependent key such as a `notify` node's *Title*, which is required only
while the node has no `template`, is marked only while the rule applies.

Config keys come in three editable shapes so authors never hand-write JSON:

- **Flat object maps** — a `create_record` node's **Field values**, a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
import * as React from 'react';
import { Code2, Plus, X } from 'lucide-react';
import { Button, Input, Label, cn } from '@object-ui/components';
import { uniqueId } from './_shared.js';
import { RequiredMarker, uniqueId } from './_shared.js';
import { VariableTextInput } from './VariableTextInput.js';
import type { ScopeGroup } from './useFlowScope.js';
import { FlowExprIssue } from './FlowExprIssue.js';
Expand Down Expand Up @@ -211,6 +211,8 @@ export interface FlowKeyValueFieldProps {
/** Placeholder of an expression row's source input. */
expressionPlaceholder: string;
};
/** The spec requires this map (objectui#10948): the label carries {@link RequiredMarker}. */
required?: boolean;
}

/**
Expand Down Expand Up @@ -242,6 +244,7 @@ export function FlowKeyValueField({
emptyLabel,
scopeGroups,
valueEnvelope,
required,
}: FlowKeyValueFieldProps) {
// Preserve whichever shape the value was authored in (object map vs the
// assignment-node array form) across edits.
Expand Down Expand Up @@ -301,7 +304,10 @@ export function FlowKeyValueField({

return (
<div className="space-y-1.5">
<Label className="text-xs text-muted-foreground">{label}</Label>
<Label className="text-xs text-muted-foreground">
{label}
{required && <RequiredMarker />}
</Label>
{envelopeSlot && arrayShape && (
<p className="text-[11px] leading-snug text-amber-700 dark:text-amber-400" role="note">
{ASSIGNMENT_ARRAY_FORM_PRESCRIPTION}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
InspectorNumberField,
InspectorSelectField,
InspectorCheckboxField,
RequiredMarker,
flagUnknownValue,
} from './_shared.js';
import { Button, Label } from '@object-ui/components';
Expand Down Expand Up @@ -150,9 +151,22 @@ export interface FlowNodeConfigFieldProps {
* DELIBERATELY. Omit to render the notice without the button.
*/
onClearInactive?: () => void;
/**
* objectui#10948 — the installed spec refuses this node without a value at
* `field.path`. Supplied by the host inspector, which owns the node, from
* `specRequiresField` (`flow-required-keys.ts`) — the spec's own verdict,
* never a list kept here. The label carries {@link RequiredMarker}; a control
* this component owns carries `aria-required`.
*/
required?: boolean;
/**
* objectui#10948 — for an `objectList` field, the column keys the spec
* requires on every row (`specRequiredColumns`), handed to the list editor.
*/
requiredColumns?: ReadonlySet<string>;
}

export function FlowNodeConfigField({ field, value, onCommit, disabled, locale, context, scopeGroups, approvalScopeGroups, triggerScope, inactiveRetained, onClearInactive }: FlowNodeConfigFieldProps) {
export function FlowNodeConfigField({ field, value, onCommit, disabled, locale, context, scopeGroups, approvalScopeGroups, triggerScope, inactiveRetained, onClearInactive, required, requiredColumns }: FlowNodeConfigFieldProps) {
const refMode: 'expression' | 'template' =
field.refMode ?? (field.kind === 'expression' ? 'expression' : 'template');
// objectui#6226 — the row-based condition builder, on the fields that opted in
Expand Down Expand Up @@ -190,6 +204,7 @@ export function FlowNodeConfigField({ field, value, onCommit, disabled, locale,
onCommit={(v) => onCommit(v)}
disabled={disabled}
context={context}
required={required}
/>
);
case 'keyValue':
Expand All @@ -205,6 +220,7 @@ export function FlowNodeConfigField({ field, value, onCommit, disabled, locale,
removeLabel={t('engine.inspector.flowNode.kv.remove', locale)}
emptyLabel={t('engine.inspector.flowNode.kv.empty', locale)}
scopeGroups={scopeGroups}
required={required}
// objectui#7588 — the per-value text / expression toggle, offered
// only on a map the spec's expression ledger declares `value`-role
// for this node type (today the assignment node's `assignments`).
Expand All @@ -225,6 +241,7 @@ export function FlowNodeConfigField({ field, value, onCommit, disabled, locale,
value={value}
onCommit={(v) => onCommit(v)}
disabled={disabled}
required={required}
addLabel={t('engine.inspector.flowNode.list.add', locale)}
itemLabel={t('engine.inspector.flowNode.list.item', locale)}
removeLabel={t('engine.inspector.flowNode.list.remove', locale)}
Expand All @@ -246,6 +263,7 @@ export function FlowNodeConfigField({ field, value, onCommit, disabled, locale,
onCommit(nums.length ? nums : undefined);
}}
disabled={disabled}
required={required}
addLabel={t('engine.inspector.flowNode.list.add', locale)}
itemLabel={t('engine.inspector.flowNode.list.item', locale)}
removeLabel={t('engine.inspector.flowNode.list.remove', locale)}
Expand All @@ -270,6 +288,8 @@ export function FlowNodeConfigField({ field, value, onCommit, disabled, locale,
// objectui#10772 — with `context.node`, names a screen's `fields`
// list, whose `visibleWhen` column binds the screen's own fields.
fieldId={field.id}
required={required}
requiredColumns={requiredColumns}
/>
);
case 'number':
Expand All @@ -280,6 +300,7 @@ export function FlowNodeConfigField({ field, value, onCommit, disabled, locale,
placeholder={field.placeholder}
onCommit={(v) => onCommit(v)}
disabled={disabled}
required={required}
/>
);
case 'boolean': {
Expand Down Expand Up @@ -391,6 +412,7 @@ export function FlowNodeConfigField({ field, value, onCommit, disabled, locale,
}
onCommit={(v) => onCommit(v)}
disabled={disabled}
required={required}
/>
);
})();
Expand All @@ -402,6 +424,7 @@ export function FlowNodeConfigField({ field, value, onCommit, disabled, locale,
<div className="space-y-1">
<Label htmlFor={secretId} className="text-xs text-muted-foreground">
{field.label}
{required && <RequiredMarker />}
</Label>
<FlowSecretControl key={nodeId} id={secretId} value={value} onCommit={onCommit} disabled={disabled} />
</div>
Expand All @@ -410,7 +433,10 @@ export function FlowNodeConfigField({ field, value, onCommit, disabled, locale,
case 'textarea':
return (
<div className="space-y-1">
<Label className="text-xs text-muted-foreground">{field.label}</Label>
<Label className="text-xs text-muted-foreground">
{field.label}
{required && <RequiredMarker />}
</Label>
<VariableTextInput
multiline
rows={4}
Expand All @@ -420,6 +446,7 @@ export function FlowNodeConfigField({ field, value, onCommit, disabled, locale,
groups={scopeGroups ?? []}
placeholder={field.placeholder}
disabled={disabled}
ariaRequired={required}
/>
</div>
);
Expand All @@ -428,7 +455,10 @@ export function FlowNodeConfigField({ field, value, onCommit, disabled, locale,
default:
return (
<div className="space-y-1">
<Label className="text-xs text-muted-foreground">{field.label}</Label>
<Label className="text-xs text-muted-foreground">
{field.label}
{required && <RequiredMarker />}
</Label>
<VariableTextInput
mode={refMode}
mono={field.kind === 'expression'}
Expand All @@ -437,6 +467,7 @@ export function FlowNodeConfigField({ field, value, onCommit, disabled, locale,
groups={scopeGroups ?? []}
placeholder={field.placeholder}
disabled={disabled}
ariaRequired={required}
/>
</div>
);
Expand Down
Loading
Loading