Skip to content

Commit 9e2d58f

Browse files
os-zhuangclaude
andauthored
feat(kanban): accept CEL { condition, style } conditional-formatting rules (#1584 follow-up) (#2550)
Since #1584 moved kanban card styling onto the shared CEL evaluator, the runtime already accepts the spec `{ condition, style }` rule shape — but the type and zod schema still only allowed the native `{ field, operator, value }` shape, so a CEL kanban rule failed validation for something that worked at runtime. Widen `KanbanConditionalFormattingRule` (now a union with `SpecConditionalFormattingRule`) and `ObjectKanbanSchema`'s zod to accept both shapes, matching list/grid `conditionalFormatting` and the runtime. The four plugin-kanban type sites now reference the shared `@object-ui/types` type. Back-compat: the native shape keeps validating unchanged. Claude-Session: https://claude.ai/code/session_019LwrWThBJgvcfPtqNHpstB Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1629d25 commit 9e2d58f

9 files changed

Lines changed: 132 additions & 39 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@object-ui/types": patch
3+
"@object-ui/plugin-kanban": patch
4+
---
5+
6+
Kanban `conditionalFormatting` now accepts CEL rules in its type + schema (#1584 follow-up).
7+
8+
Since #1584 moved kanban card styling onto the shared CEL evaluator, the runtime
9+
already accepts the spec `{ condition, style }` rule shape — but the type and zod
10+
schema still only allowed the native `{ field, operator, value }` shape, so a
11+
CEL kanban rule failed validation for something that worked at runtime. The
12+
`KanbanConditionalFormattingRule` type and `ObjectKanbanSchema` zod schema are
13+
widened to a union of both shapes, matching list/grid `conditionalFormatting` and
14+
the runtime. Back-compat: the native shape keeps validating unchanged.

packages/plugin-kanban/src/KanbanEnhanced.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
import { CSS } from "@dnd-kit/utilities"
2828
import { Badge, Card, CardHeader, CardTitle, CardDescription, CardContent, Button, Input } from "@object-ui/components"
2929
import { resolveConditionalFormatting } from "@object-ui/core"
30+
import type { KanbanConditionalFormattingRule } from "@object-ui/types"
3031
import { ChevronDown, ChevronRight, AlertTriangle, Plus } from "lucide-react"
3132

3233
const cn = (...classes: (string | undefined)[]) => classes.filter(Boolean).join(' ')
@@ -49,13 +50,9 @@ export interface KanbanColumn {
4950
collapsed?: boolean
5051
}
5152

52-
export interface ConditionalFormattingRule {
53-
field: string
54-
operator: 'equals' | 'not_equals' | 'contains' | 'in'
55-
value: string | string[]
56-
backgroundColor?: string
57-
borderColor?: string
58-
}
53+
// Card formatting accepts the native `{ field, operator, value }` shape and the
54+
// spec `{ condition, style }` CEL shape (issue #1584) — see @object-ui/types.
55+
export type ConditionalFormattingRule = KanbanConditionalFormattingRule
5956

6057
export interface KanbanEnhancedProps {
6158
columns: KanbanColumn[]

packages/plugin-kanban/src/KanbanImpl.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { CSS } from "@dnd-kit/utilities"
2828
import { Badge, Card, CardHeader, CardTitle, CardDescription, CardContent, ScrollArea, Button, Input, useResizeObserver, DataEmptyState } from "@object-ui/components"
2929
import { useHasDndProvider, useDnd } from "@object-ui/react"
3030
import { resolveConditionalFormatting } from "@object-ui/core"
31+
import type { KanbanConditionalFormattingRule } from "@object-ui/types"
3132
import { createSafeTranslation } from "@object-ui/i18n"
3233
import { Plus } from "lucide-react"
3334

@@ -80,13 +81,9 @@ export interface KanbanColumn {
8081
className?: string
8182
}
8283

83-
export interface ConditionalFormattingRule {
84-
field: string
85-
operator: 'equals' | 'not_equals' | 'contains' | 'in'
86-
value: string | string[]
87-
backgroundColor?: string
88-
borderColor?: string
89-
}
84+
// Card formatting accepts the native `{ field, operator, value }` shape and the
85+
// spec `{ condition, style }` CEL shape (issue #1584) — see @object-ui/types.
86+
export type ConditionalFormattingRule = KanbanConditionalFormattingRule
9087

9188
export interface KanbanBoardProps {
9289
columns: KanbanColumn[]

packages/plugin-kanban/src/index.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import React, { Suspense } from 'react';
1010
import { ComponentRegistry } from '@object-ui/core';
1111
import { useSchemaContext } from '@object-ui/react';
1212
import { Skeleton } from '@object-ui/components';
13+
import type { KanbanConditionalFormattingRule } from '@object-ui/types';
1314
import { ObjectKanban } from './ObjectKanban';
1415

1516
// Export types for external use
@@ -47,13 +48,7 @@ export interface KanbanRendererProps {
4748
quickAdd?: boolean;
4849
onQuickAdd?: (columnId: string, title: string) => void;
4950
coverImageField?: string;
50-
conditionalFormatting?: Array<{
51-
field: string;
52-
operator: 'equals' | 'not_equals' | 'contains' | 'in';
53-
value: string | string[];
54-
backgroundColor?: string;
55-
borderColor?: string;
56-
}>;
51+
conditionalFormatting?: KanbanConditionalFormattingRule[];
5752
};
5853
}
5954

packages/plugin-kanban/src/types.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* LICENSE file in the root directory of this source tree.
77
*/
88

9-
import type { BaseSchema, GroupingConfig } from '@object-ui/types';
9+
import type { BaseSchema, GroupingConfig, KanbanConditionalFormattingRule } from '@object-ui/types';
1010

1111
/**
1212
* Kanban card interface.
@@ -118,15 +118,11 @@ export interface KanbanSchema extends BaseSchema {
118118
allowCollapse?: boolean;
119119

120120
/**
121-
* Conditional formatting rules for card coloring.
121+
* Conditional formatting rules for card coloring. Accepts the native
122+
* `{ field, operator, value }` shape and the spec `{ condition, style }` CEL
123+
* shape (issue #1584).
122124
*/
123-
conditionalFormatting?: Array<{
124-
field: string;
125-
operator: 'equals' | 'not_equals' | 'contains' | 'in';
126-
value: string | string[];
127-
backgroundColor?: string;
128-
borderColor?: string;
129-
}>;
125+
conditionalFormatting?: KanbanConditionalFormattingRule[];
130126

131127
/**
132128
* Predefined card templates for quick-add.
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
/**
10+
* Kanban conditional formatting accepts CEL (#1584).
11+
*
12+
* Since kanban card styling moved onto the shared CEL evaluator, the kanban
13+
* schema's type + zod contract must match the runtime: a rule may be the native
14+
* `{ field, operator, value }` shape OR the spec `{ condition, style }` CEL
15+
* shape. This locks both so the two can't drift back apart.
16+
*/
17+
import { describe, it, expect } from 'vitest';
18+
import { ObjectKanbanSchema } from '../zod/index.zod';
19+
import type { KanbanConditionalFormattingRule } from '../objectql';
20+
21+
describe('kanban conditionalFormatting — zod contract', () => {
22+
const base = { type: 'object-kanban', objectName: 'task', groupField: 'status' };
23+
24+
it('accepts the native { field, operator, value } rule (back-compat)', () => {
25+
const parsed = ObjectKanbanSchema.safeParse({
26+
...base,
27+
conditionalFormatting: [
28+
{ field: 'priority', operator: 'equals', value: 'high', backgroundColor: '#fee2e2' },
29+
],
30+
});
31+
expect(parsed.success).toBe(true);
32+
});
33+
34+
it('accepts the spec { condition, style } CEL rule (new)', () => {
35+
const parsed = ObjectKanbanSchema.safeParse({
36+
...base,
37+
conditionalFormatting: [
38+
{ condition: "record.status == 'done'", style: { backgroundColor: '#e0ffe0' } },
39+
],
40+
});
41+
expect(parsed.success).toBe(true);
42+
});
43+
44+
it('accepts a mix of both shapes in one rule list', () => {
45+
const parsed = ObjectKanbanSchema.safeParse({
46+
...base,
47+
conditionalFormatting: [
48+
{ condition: "record.blocked == true", style: { borderColor: 'red' } },
49+
{ field: 'priority', operator: 'in', value: ['high', 'urgent'], backgroundColor: '#fef9c3' },
50+
],
51+
});
52+
expect(parsed.success).toBe(true);
53+
});
54+
});
55+
56+
describe('kanban conditionalFormatting — type contract', () => {
57+
it('KanbanConditionalFormattingRule admits both shapes at compile time', () => {
58+
const native: KanbanConditionalFormattingRule = {
59+
field: 'priority',
60+
operator: 'equals',
61+
value: 'high',
62+
backgroundColor: '#fee2e2',
63+
};
64+
const cel: KanbanConditionalFormattingRule = {
65+
condition: "record.status == 'done'",
66+
style: { backgroundColor: '#e0ffe0' },
67+
};
68+
expect(native).toBeTruthy();
69+
expect(cel).toBeTruthy();
70+
});
71+
});

packages/types/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,7 @@ export type {
336336
ObjectCalendarSchema,
337337
ObjectKanbanSchema,
338338
KanbanConditionalFormattingRule,
339+
KanbanNativeConditionalFormattingRule,
339340
ObjectChartSchema,
340341
ListViewSchema,
341342
ObjectGridSchema,

packages/types/src/objectql.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2208,9 +2208,9 @@ export interface ObjectKanbanSchema extends BaseSchema {
22082208
}
22092209

22102210
/**
2211-
* Conditional formatting rule for Kanban cards
2211+
* Native (field/operator/value) conditional formatting rule for Kanban cards.
22122212
*/
2213-
export interface KanbanConditionalFormattingRule {
2213+
export interface KanbanNativeConditionalFormattingRule {
22142214
/** Field name to check */
22152215
field: string;
22162216
/** Operator for comparison */
@@ -2223,6 +2223,18 @@ export interface KanbanConditionalFormattingRule {
22232223
borderColor?: string;
22242224
}
22252225

2226+
/**
2227+
* Conditional formatting rule for Kanban cards.
2228+
*
2229+
* Since #1584, kanban card styling runs on the shared CEL evaluator, so a rule
2230+
* accepts BOTH the native `{ field, operator, value }` shape and the spec
2231+
* `{ condition, style }` shape (a CEL predicate + style map) — the same
2232+
* `record.*` predicates authors use on list/grid rows.
2233+
*/
2234+
export type KanbanConditionalFormattingRule =
2235+
| KanbanNativeConditionalFormattingRule
2236+
| SpecConditionalFormattingRule;
2237+
22262238
/**
22272239
* Object Chart Component Schema
22282240
*/

packages/types/src/zod/objectql.zod.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -436,13 +436,23 @@ export const ObjectCalendarSchema = BaseSchema.extend({
436436
/**
437437
* ObjectKanban Schema
438438
*/
439-
const KanbanConditionalFormattingRuleSchema = z.object({
440-
field: z.string().describe('Field name to check'),
441-
operator: z.enum(['equals', 'not_equals', 'contains', 'in']).describe('Comparison operator'),
442-
value: z.union([z.string(), z.array(z.string())]).describe('Value to compare against'),
443-
backgroundColor: z.string().optional().describe('Background color'),
444-
borderColor: z.string().optional().describe('Border color'),
445-
});
439+
// Since #1584, kanban card styling runs on the shared CEL evaluator, so a
440+
// kanban rule accepts BOTH the native `{ field, operator, value }` shape and the
441+
// spec `{ condition, style }` shape (a CEL predicate + style map) — matching
442+
// list/grid `conditionalFormatting`. The type/schema now match the runtime.
443+
const KanbanConditionalFormattingRuleSchema = z.union([
444+
z.object({
445+
field: z.string().describe('Field name to check'),
446+
operator: z.enum(['equals', 'not_equals', 'contains', 'in']).describe('Comparison operator'),
447+
value: z.union([z.string(), z.array(z.string())]).describe('Value to compare against'),
448+
backgroundColor: z.string().optional().describe('Background color'),
449+
borderColor: z.string().optional().describe('Border color'),
450+
}),
451+
z.object({
452+
condition: z.string().describe('CEL predicate evaluated against the card record'),
453+
style: z.record(z.string(), z.string()).describe('CSS styles applied when the condition is true'),
454+
}),
455+
]);
446456

447457
export const ObjectKanbanSchema = BaseSchema.extend({
448458
type: z.literal('object-kanban'),

0 commit comments

Comments
 (0)