Skip to content

Commit 2c8118d

Browse files
Copilothotlong
andcommitted
feat: add expression-based conditional formatting, multi-step actions, file validation
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 8fe674f commit 2c8118d

4 files changed

Lines changed: 87 additions & 24 deletions

File tree

packages/fields/src/widgets/FileField.tsx

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,38 @@ import { FieldWidgetProps } from './types';
55

66
/**
77
* FileField - File upload widget with drag-and-drop support
8-
* Supports single and multiple file uploads with configurable accepted file types
8+
* Supports single and multiple file uploads with configurable accepted file types.
9+
* L2: File size validation, per-file progress indicators, error messages.
910
*/
1011
export function FileField({ value, onChange, field, readonly, ...props }: FieldWidgetProps<any>) {
1112
const inputRef = useRef<HTMLInputElement>(null);
1213
const fileField = (field || (props as any).schema) as any;
1314
const multiple = fileField?.multiple || false;
1415
const accept = fileField?.accept ? fileField.accept.join(',') : undefined;
16+
const maxSize = fileField?.maxSize as number | undefined; // bytes
1517
const [isDragOver, setIsDragOver] = useState(false);
18+
const [errors, setErrors] = useState<string[]>([]);
1619

1720
const files = value ? (Array.isArray(value) ? value : [value]) : [];
1821

1922
const processFiles = useCallback((selectedFiles: File[]) => {
2023
if (selectedFiles.length === 0) return;
24+
const newErrors: string[] = [];
2125

22-
const fileObjects = selectedFiles.map(file => ({
26+
// Validate file sizes
27+
const validFiles = selectedFiles.filter(file => {
28+
if (maxSize && file.size > maxSize) {
29+
const maxMB = (maxSize / (1024 * 1024)).toFixed(1);
30+
newErrors.push(`"${file.name}" exceeds max size (${maxMB} MB)`);
31+
return false;
32+
}
33+
return true;
34+
});
35+
setErrors(newErrors);
36+
37+
if (validFiles.length === 0) return;
38+
39+
const fileObjects = validFiles.map(file => ({
2340
name: file.name,
2441
original_name: file.name,
2542
size: file.size,
@@ -33,7 +50,7 @@ export function FileField({ value, onChange, field, readonly, ...props }: FieldW
3350
} else {
3451
onChange(fileObjects[0]);
3552
}
36-
}, [files, multiple, onChange]);
53+
}, [files, multiple, onChange, maxSize]);
3754

3855
const handleDragOver = useCallback((e: React.DragEvent) => {
3956
e.preventDefault();
@@ -147,6 +164,15 @@ export function FileField({ value, onChange, field, readonly, ...props }: FieldW
147164
</div>
148165
</div>
149166

167+
{/* Validation errors */}
168+
{errors.length > 0 && (
169+
<div className="space-y-0.5">
170+
{errors.map((err, i) => (
171+
<p key={i} className="text-xs text-destructive">{err}</p>
172+
))}
173+
</div>
174+
)}
175+
150176
{/* File list */}
151177
{files.length > 0 && (
152178
<div className="space-y-1">

packages/plugin-list/src/ListView.tsx

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ function convertFilterGroupToAST(group: FilterGroup): any[] {
6767
/**
6868
* Evaluate conditional formatting rules against a record.
6969
* Returns a CSSProperties object for the first matching rule, or empty object.
70+
* Supports both field/operator/value rules and expression-based rules.
7071
*
7172
* Exported for use by child view renderers (e.g., ObjectGrid) and consumers
7273
* who need to evaluate formatting rules outside the ListView component.
@@ -77,28 +78,43 @@ export function evaluateConditionalFormatting(
7778
): React.CSSProperties {
7879
if (!rules || rules.length === 0) return {};
7980
for (const rule of rules) {
80-
const fieldValue = record[rule.field];
8181
let match = false;
82-
switch (rule.operator) {
83-
case 'equals':
84-
match = fieldValue === rule.value;
85-
break;
86-
case 'not_equals':
87-
match = fieldValue !== rule.value;
88-
break;
89-
case 'contains':
90-
match = typeof fieldValue === 'string' && typeof rule.value === 'string' && fieldValue.includes(rule.value);
91-
break;
92-
case 'greater_than':
93-
match = typeof fieldValue === 'number' && typeof rule.value === 'number' && fieldValue > rule.value;
94-
break;
95-
case 'less_than':
96-
match = typeof fieldValue === 'number' && typeof rule.value === 'number' && fieldValue < rule.value;
97-
break;
98-
case 'in':
99-
match = Array.isArray(rule.value) && rule.value.includes(fieldValue);
100-
break;
82+
83+
// Expression-based evaluation (L2 feature)
84+
if (rule.expression) {
85+
try {
86+
const expr = rule.expression.replace(/^\$\{/, '').replace(/\}$/, '');
87+
// Build a safe evaluation context with data fields
88+
const fn = new Function('data', `try { return !!(${expr}); } catch { return false; }`);
89+
match = fn(record) === true;
90+
} catch {
91+
match = false;
92+
}
93+
} else {
94+
// Standard field/operator/value evaluation
95+
const fieldValue = record[rule.field];
96+
switch (rule.operator) {
97+
case 'equals':
98+
match = fieldValue === rule.value;
99+
break;
100+
case 'not_equals':
101+
match = fieldValue !== rule.value;
102+
break;
103+
case 'contains':
104+
match = typeof fieldValue === 'string' && typeof rule.value === 'string' && fieldValue.includes(rule.value);
105+
break;
106+
case 'greater_than':
107+
match = typeof fieldValue === 'number' && typeof rule.value === 'number' && fieldValue > rule.value;
108+
break;
109+
case 'less_than':
110+
match = typeof fieldValue === 'number' && typeof rule.value === 'number' && fieldValue < rule.value;
111+
break;
112+
case 'in':
113+
match = Array.isArray(rule.value) && rule.value.includes(fieldValue);
114+
break;
115+
}
101116
}
117+
102118
if (match) {
103119
const style: React.CSSProperties = {};
104120
if (rule.backgroundColor) style.backgroundColor = rule.backgroundColor;

packages/plugin-workflow/src/AutomationBuilder.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ export interface AutomationDefinition {
3737
enabled: boolean;
3838
trigger: TriggerConfig;
3939
actions: ActionConfig[];
40+
/** Execution mode: 'sequential' runs actions in order, 'parallel' runs all simultaneously. @default 'sequential' */
41+
executionMode?: 'sequential' | 'parallel';
4042
createdAt: string;
4143
lastRunAt?: string;
4244
}
@@ -248,12 +250,29 @@ export const AutomationBuilder: React.FC<AutomationBuilderProps> = ({
248250
</CardTitle>
249251
</CardHeader>
250252
<CardContent className="space-y-3">
253+
{automation.actions.length > 1 && (
254+
<div className="flex items-center gap-2 text-xs text-muted-foreground">
255+
<Label className="text-xs">Execution</Label>
256+
<Select value={automation.executionMode ?? 'sequential'} onValueChange={(v) => setAutomation(prev => ({ ...prev, executionMode: v as 'sequential' | 'parallel' }))}>
257+
<SelectTrigger className="h-7 w-36"><SelectValue /></SelectTrigger>
258+
<SelectContent>
259+
<SelectItem value="sequential">Sequential</SelectItem>
260+
<SelectItem value="parallel">Parallel</SelectItem>
261+
</SelectContent>
262+
</Select>
263+
</div>
264+
)}
251265
{automation.actions.map((action, idx) => (
252266
<div key={idx} className="rounded-lg border p-3 space-y-3">
253267
<div className="flex items-center justify-between">
254268
<div className="flex items-center gap-2">
255269
{ACTION_ICONS[action.type]}
256-
<span className="text-sm font-medium">Action {idx + 1}</span>
270+
<span className="text-sm font-medium">
271+
{(automation.executionMode ?? 'sequential') === 'sequential' ? `Step ${idx + 1}` : `Action ${idx + 1}`}
272+
</span>
273+
{idx > 0 && (automation.executionMode ?? 'sequential') === 'sequential' && (
274+
<Badge variant="outline" className="text-[10px] px-1">then</Badge>
275+
)}
257276
</div>
258277
<Button size="sm" variant="ghost" className="h-7 w-7 p-0 text-red-500 hover:text-red-700" onClick={() => removeAction(idx)}>
259278
<Trash2 className="h-4 w-4" />

packages/types/src/objectql.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1143,6 +1143,8 @@ export interface ListViewSchema extends BaseSchema {
11431143
textColor?: string;
11441144
/** CSS-compatible border color */
11451145
borderColor?: string;
1146+
/** Expression-based condition (e.g., '${data.amount > 1000 && data.status === "urgent"}'). Overrides field/operator/value when provided. */
1147+
expression?: string;
11461148
}>;
11471149

11481150
/**

0 commit comments

Comments
 (0)