diff --git a/apps/storybook/package.json b/apps/storybook/package.json index 72c469a4d..a3b824133 100644 --- a/apps/storybook/package.json +++ b/apps/storybook/package.json @@ -31,7 +31,7 @@ "@storybook/icons": "^2.0.1", "@storybook/react": "^10.4.1", "@storybook/react-vite": "^10.4.1", - "@tailwindcss/vite": "^4.2.2", + "@tailwindcss/vite": "4.3.0", "@types/react": "^19.2.6", "@types/react-dom": "^19.2.2", "@vitejs/plugin-react": "^5.2.0", diff --git a/packages/apollo-react/src/canvas/components/NodePropertyPanel/LockableValueField.tsx b/packages/apollo-react/src/canvas/components/NodePropertyPanel/LockableValueField.tsx new file mode 100644 index 000000000..08a91d035 --- /dev/null +++ b/packages/apollo-react/src/canvas/components/NodePropertyPanel/LockableValueField.tsx @@ -0,0 +1,588 @@ +import { + Button, + Calendar, + cn, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + FileUpload, + InputGroup, + InputGroupAddon, + InputGroupButton, + InputGroupInput, + Label, + MultiSelect, + Popover, + PopoverContent, + PopoverTrigger, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + Switch, + Textarea, + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@uipath/apollo-wind'; +import { + Calendar as CalendarIcon, + ChevronDown, + Code2, + Hash, + List, + ListChecks, + Lock, + LockOpen, + type LucideIcon, + Paperclip, + Sparkles, + ToggleLeft, + Type, +} from 'lucide-react'; +import type { ReactNode } from 'react'; +import { useId } from 'react'; + +export type LockableValueFieldMode = 'fixed' | 'expression'; + +export type LockableFieldType = + | 'string' + | 'integer' + | 'date' + | 'boolean' + | 'single-select' + | 'multi-select' + | 'file'; + +interface FieldTypeMeta { + label: string; + icon: LucideIcon; + supportsExpression: boolean; + fixedLabel: string; + fixedDescription: string; +} + +export const FIELD_TYPE_META: Record = { + string: { + label: 'String', + icon: Type, + supportsExpression: true, + fixedLabel: 'Fixed value', + fixedDescription: 'Use a literal string value', + }, + integer: { + label: 'Integer', + icon: Hash, + supportsExpression: true, + fixedLabel: 'Fixed value', + fixedDescription: 'Use a literal number value', + }, + date: { + label: 'Date', + icon: CalendarIcon, + supportsExpression: true, + fixedLabel: 'Fixed date', + fixedDescription: 'Use a literal date value', + }, + boolean: { + label: 'Boolean', + icon: ToggleLeft, + supportsExpression: true, + fixedLabel: 'Fixed value', + fixedDescription: 'Use a literal true or false value', + }, + 'single-select': { + label: 'Single select', + icon: List, + supportsExpression: false, + fixedLabel: 'Fixed value', + fixedDescription: 'Choose one option', + }, + 'multi-select': { + label: 'Multiselect', + icon: ListChecks, + supportsExpression: false, + fixedLabel: 'Fixed value', + fixedDescription: 'Choose one or more options', + }, + file: { + label: 'File', + icon: Paperclip, + supportsExpression: false, + fixedLabel: 'Fixed value', + fixedDescription: 'Upload a file', + }, +}; + +const FIELD_TYPE_ORDER: LockableFieldType[] = [ + 'string', + 'integer', + 'date', + 'boolean', + 'single-select', + 'multi-select', + 'file', +]; + +export const DEMO_SELECT_OPTIONS = [ + { label: 'Option 1', value: 'option-1' }, + { label: 'Option 2', value: 'option-2' }, + { label: 'Option 3', value: 'option-3' }, +]; + +export function parseListValue(value: string): string[] { + try { + const parsed = JSON.parse(value); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } +} + +export interface LockableValueFieldProps { + /** Current field value. Encoding depends on fieldType (e.g. multi-select is a JSON array string). */ + value?: string; + /** Called when the user edits the value (only fires while unlocked). */ + onValueChange?: (value: string) => void; + /** Whether the field is read-only. Defaults to true. */ + locked?: boolean; + /** Called when the user toggles the lock. */ + onLockedChange?: (locked: boolean) => void; + /** Fixed value vs. JS expression. Defaults to 'fixed'. Ignored for types that don't support expressions. */ + mode?: LockableValueFieldMode; + /** Called when the user switches modes. */ + onModeChange?: (mode: LockableValueFieldMode) => void; + /** The field's data type. Defaults to 'string'. Determines which control renders the value. */ + fieldType?: LockableFieldType; + /** Called when the user switches the field type. */ + onFieldTypeChange?: (fieldType: LockableFieldType) => void; + /** Shows a required-field asterisk next to the default label. Ignored when `label` is provided. */ + required?: boolean; + /** Overrides the default mode-based label (e.g. a field name instead of "String value"). */ + label?: ReactNode; + /** Extra content rendered after the built-in AI assist / Insert variable buttons (e.g. a delete button). */ + headerActions?: ReactNode; + /** Forces the header row into its narrow-container icon-only layout, regardless of actual width. For demos/comparisons. */ + compact?: boolean; + id?: string; + className?: string; +} + +const FIELD_LABEL: Record = { + fixed: 'String value', + expression: 'Write a string expression', +}; + +const FIELD_PLACEHOLDER: Record = { + fixed: 'Enter a value', + expression: 'Enter an expression', +}; + +/** + * LockableValueField — a field that can be locked to read-only, typed as one + * of several data types, and (for scalar types) switched between a literal + * value and a JS expression. + * + * Prototype: the expression mode is styled as code (monospace) but does not + * yet carry real syntax highlighting or evaluation. See ExpressionField for + * the direction a full code-editing surface would take. Select/multiselect + * options are demo placeholders; file uploads aren't persisted anywhere. + */ +export function LockableValueField({ + value = '', + onValueChange, + locked = true, + onLockedChange, + mode = 'fixed', + onModeChange, + fieldType = 'string', + onFieldTypeChange, + required, + label, + headerActions, + compact, + id, + className, +}: LockableValueFieldProps) { + const promptId = useId(); + const typeMeta = FIELD_TYPE_META[fieldType]; + const effectiveMode = typeMeta.supportsExpression ? mode : 'fixed'; + const collapsedTextClass = cn('@max-[259px]:hidden', compact && '!hidden'); + const collapsedPaddingClass = cn('@max-[259px]:px-1.5', compact && '!px-1.5'); + + return ( +
+
+ {label ?? ( + + )} + +
+ {onFieldTypeChange && ( + + + + + + + + Change field type + + + {FIELD_TYPE_ORDER.map((type) => { + const meta = FIELD_TYPE_META[type]; + const isActive = type === fieldType; + return ( + onFieldTypeChange(type)}> + + + {meta.label} + + + ); + })} + + + )} + + + + + + + + Generate a value with AI + + +
+ +