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
5 changes: 3 additions & 2 deletions apps/storybook/src/patterns/FieldTypes.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1082,8 +1082,9 @@ function FieldTypesPage({ globalTheme }: { globalTheme: string }) {
</GuidanceItem>
<GuidanceItem>
<span className="font-medium text-foreground">Lock.</span>{' '}
<InlineCode>locked</InlineCode> and <InlineCode>onLockedChange</InlineCode> make the
value read-only for the person filling in the form, shown as plain text;{' '}
<InlineCode>locked</InlineCode> and <InlineCode>onLockedChange</InlineCode> mark the
value read-only for the person filling in the form. The author can still edit it;{' '}
<InlineCode>disabled</InlineCode> disables the controls, and{' '}
<InlineCode>showLock</InlineCode> hides the toggle.
</GuidanceItem>
<GuidanceItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2440,7 +2440,6 @@ export function QuickFormPanel({
...c,
fieldType,
value: '',
mode: FIELD_TYPE_META[fieldType].supportsExpression ? c.mode : 'literal',
}
: c
)
Expand Down Expand Up @@ -3305,7 +3304,6 @@ function PanelUIInventoryStory() {
const updateCompositionFieldType = (fieldType: QuickFieldType) => {
setCompositionFieldType(fieldType);
setCompositionValue('');
if (!FIELD_TYPE_META[fieldType].supportsExpression) setCompositionMode('literal');
};

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export function FieldHeader({
onFieldTypeChange,
onRequiredChange,
compact,
disabled,
isExpression,
showFieldActions,
onValueChange,
variables,
Expand All @@ -51,6 +53,9 @@ export function FieldHeader({
onFieldTypeChange?: (fieldType: QuickFieldType) => void;
onRequiredChange?: (required: boolean) => void;
compact?: boolean;
disabled: boolean;
/** Whether the AI output is described as an expression rather than a value. */
isExpression: boolean;
showFieldActions: boolean;
onValueChange?: (value: string) => void;
variables: QuickFormFieldOption[];
Expand Down Expand Up @@ -111,8 +116,9 @@ export function FieldHeader({
<button
type="button"
aria-label={strings.fieldTypeAriaLabel}
disabled={disabled}
className={cn(
'flex h-7 items-center gap-1 rounded-lg px-2 text-[11px] text-foreground-subtle transition hover:bg-surface-overlay hover:text-foreground',
'flex h-7 items-center gap-1 rounded-lg px-2 text-[11px] text-foreground-subtle transition hover:bg-surface-overlay hover:text-foreground disabled:pointer-events-none disabled:opacity-50',
collapsedPaddingClass
)}
>
Expand Down Expand Up @@ -155,6 +161,7 @@ export function FieldHeader({
size="sm"
checked={!!required}
onCheckedChange={onRequiredChange}
disabled={disabled}
className="data-[state=checked]:bg-brand data-[state=unchecked]:bg-foreground-subtle"
aria-label={
required ? strings.requiredAriaLabel : strings.optionalAriaLabel
Expand All @@ -175,7 +182,8 @@ export function FieldHeader({
aria-label={
required ? strings.requiredAriaLabel : strings.optionalAriaLabel
}
className="grid size-7 place-items-center rounded-lg text-foreground-subtle transition hover:bg-surface-overlay hover:text-foreground"
disabled={disabled}
className="grid size-7 place-items-center rounded-lg text-foreground-subtle transition hover:bg-surface-overlay hover:text-foreground disabled:pointer-events-none disabled:opacity-50"
>
<Asterisk size={12} />
</button>
Expand All @@ -192,6 +200,7 @@ export function FieldHeader({
size="sm"
checked={!!required}
onCheckedChange={onRequiredChange}
disabled={disabled}
className="data-[state=checked]:bg-brand data-[state=unchecked]:bg-foreground-subtle"
aria-label={
required ? strings.requiredAriaLabel : strings.optionalAriaLabel
Expand All @@ -208,9 +217,8 @@ export function FieldHeader({
{onGenerateWithAi && (
<AiAssistAction
onGenerate={onGenerateWithAi}
hint={strings.aiOutputHint(
strings.aiOutputLabel(typeLabel, typeMeta.supportsExpression)
)}
disabled={disabled}
hint={strings.aiOutputHint(strings.aiOutputLabel(typeLabel, isExpression))}
strings={{
trigger: strings.aiAssistAriaLabel,
tooltip: strings.aiAssistTooltip,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,23 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/comp
import type { QuickFormFieldStrings } from '../types';

/**
* The lock/unlock toggle shared by both the InputGroup and plain-Input
* layouts. Disabled (and its label adjusted) when onLockedChange isn't
* provided, since clicking it wouldn't do anything otherwise.
* The lock/unlock toggle in the field's leading addon. Disabled (and its label
* adjusted) with the field, or when onLockedChange isn't provided, since
* clicking it wouldn't do anything otherwise.
*/
export function LockToggleButton({
locked,
onLockedChange,
disabled,
strings,
}: {
locked: boolean;
onLockedChange?: (locked: boolean) => void;
disabled?: boolean;
/** See QuickFormFieldStrings. */
strings: QuickFormFieldStrings;
}) {
const interactive = !!onLockedChange;
const interactive = !!onLockedChange && !disabled;
return (
<TooltipProvider delayDuration={300}>
<Tooltip>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export { QuickFormField } from './quick-form-field';
export type {
QuickFieldType,
QuickFormFieldControlProps,
QuickFormFieldMode,
QuickFormFieldMoreActions,
QuickFormFieldOption,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import userEvent from '@testing-library/user-event';
import { describe, expect, it, vi } from 'vitest';
import { QuickFormField } from './quick-form-field';
import { FIELD_TYPE_META, FIELD_TYPE_ORDER } from './types';
import { getLockedDisplayValue } from './utils';

/**
* Covers the widenings added on top of the original component: the extra types
Expand Down Expand Up @@ -108,24 +107,19 @@ describe('QuickFormField — divergences from apollo-wind', () => {
).toBeInTheDocument();
});

// Locked fields show text in place of the control, so every added type
// needs a display form or it renders blank.
it('shows the time of day when a locked datetime is displayed', () => {
const shown = getLockedDisplayValue('datetime', '2024-01-15T13:45:00.000Z', []);
expect(shown).not.toBe('');
expect(shown).not.toBe('2024-01-15T13:45:00.000Z');
// A date-only render would drop the clock time entirely.
const dateOnly = getLockedDisplayValue('date', '2024-01-15', []);
expect(shown).not.toBe(dateOnly);
});

it('falls back to the raw value for an unparseable datetime rather than throwing', () => {
expect(getLockedDisplayValue('datetime', 'not-a-date', [])).toBe('not-a-date');
expect(getLockedDisplayValue('datetime', '', [])).toBe('');
});

it('treats an array as an expression-capable value, like object', () => {
expect(FIELD_TYPE_META.array.supportsExpression).toBe(true);
it('renders the expression input for every type in expression mode', () => {
for (const fieldType of FIELD_TYPE_ORDER) {
const { unmount } = render(
<QuickFormField
fieldType={fieldType}
mode="expression"
value="$vars.x"
onValueChange={vi.fn()}
/>
);
expect(screen.getByDisplayValue('$vars.x')).toHaveClass('font-mono');
unmount();
}
});
});

Expand All @@ -140,23 +134,8 @@ describe('QuickFormField — divergences from apollo-wind', () => {
}
});

// Only `expression` is coerced for a type that cannot hold one.
it('still pins a non-expression type out of expression mode', () => {
render(
<QuickFormField
locked
fieldType="single-select"
mode="expression"
value="option-1"
options={[{ label: 'Option 1', value: 'option-1' }]}
/>
);
expect(screen.getByDisplayValue('Option 1')).toBeInTheDocument();
});

// The consumer is offered the raw mode, even one the built-in rendering
// would coerce away, since it may have its own editor.
it('offers a non-expression type its expression mode to the consumer', () => {
// A consumer's own editor replaces the built-in expression input for any type.
it('offers a select its expression mode to the consumer', () => {
render(
<QuickFormField
locked={false}
Expand All @@ -169,8 +148,7 @@ describe('QuickFormField — divergences from apollo-wind', () => {
expect(screen.getByText('consumer expression')).toBeInTheDocument();
});

// ...but a select CAN be bound to a variable, so that mode must survive.
it('lets a non-expression type use a variable binding', () => {
it('lets a select use a variable binding', () => {
render(
<QuickFormField
locked={false}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,7 @@ import { InputGroupButton, InputGroupInput } from '../input-group';
import { Label, RequiredIndicator } from '../label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../select';
import { QuickFormField } from './quick-form-field';
import {
FIELD_TYPE_META,
type QuickFieldType,
type QuickFormFieldMode,
type QuickFormFieldStrings,
} from './types';
import type { QuickFieldType, QuickFormFieldMode, QuickFormFieldStrings } from './types';

const meta = {
title: 'Components/UiPath/Quick Form Field',
Expand All @@ -33,11 +28,11 @@ describe the fields to \`MetadataForm\`; for a single field outside a form, comp
the form-field anatomy (\`FormField\`, \`FormFieldLabel\`, \`InputGroup\`). See
**Forms/Guidance Field Anatomy**.

- Left lock icon toggles Editable / Read-only. Read-only fields show plain
text, not a disabled control.
- Left lock icon toggles Editable / Read-only for the person filling in the
form. The author can still edit a read-only value; \`disabled\` is what
disables the controls.
- Right value-mode icon switches between Fixed value and Expression,
updating the value styling. Only shown for types an expression can
produce. \`renderModeControl\` supplies the control for any mode, and a
updating the value styling. \`renderModeControl\` supplies the control for any mode, and a
consumer's \`trailingAddon\` can offer all four. See **Variable and prompt modes**.
- Field type dropdown swaps the control itself: String, Integer, Decimal
number, Date, Date and time, Boolean, Single select, Multi select, Array,
Expand Down Expand Up @@ -103,9 +98,6 @@ function DefaultDemo() {
const handleFieldTypeChange = (type: QuickFieldType) => {
setFieldType(type);
setValue('');
if (!FIELD_TYPE_META[type].supportsExpression) {
setMode('literal');
}
};

if (deleted) return null;
Expand Down Expand Up @@ -415,6 +407,33 @@ export const InlineValidation: Story = {
},
};

export const Disabled: Story = {
render: () => (
<div className="w-80">
<QuickFormField
value="option-2"
fieldType="single-select"
locked
onLockedChange={() => {}}
onValueChange={() => {}}
onModeChange={() => {}}
onFieldTypeChange={() => {}}
onRequiredChange={() => {}}
variables={[{ label: 'Item ID', value: 'item.id' }]}
disabled
/>
</div>
),
parameters: {
docs: {
description: {
story:
'Use `disabled` to stop the author editing the field. It disables the value control, the lock, the value-mode menu, and the header controls. A locked field stays editable: the lock only marks the value read-only for the person filling in the form.',
},
},
},
};

const MODE_ITEMS: { mode: QuickFormFieldMode; label: string; icon: typeof Type }[] = [
{ mode: 'literal', label: 'Fixed value', icon: Type },
{ mode: 'expression', label: 'Expression', icon: Code2 },
Expand Down Expand Up @@ -690,9 +709,6 @@ function ResponsiveDemo() {
const handleFieldTypeChange = (type: QuickFieldType) => {
setFieldType(type);
setValue('');
if (!FIELD_TYPE_META[type].supportsExpression) {
setMode('literal');
}
};

const label = (fieldId: string) => (
Expand Down
Loading
Loading