diff --git a/.changeset/kanban-conditional-formatting-cel.md b/.changeset/kanban-conditional-formatting-cel.md new file mode 100644 index 000000000..43f73fe44 --- /dev/null +++ b/.changeset/kanban-conditional-formatting-cel.md @@ -0,0 +1,14 @@ +--- +"@object-ui/types": patch +"@object-ui/plugin-kanban": patch +--- + +Kanban `conditionalFormatting` now accepts CEL rules in its type + schema (#1584 follow-up). + +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. The +`KanbanConditionalFormattingRule` type and `ObjectKanbanSchema` zod schema are +widened to a union of both shapes, matching list/grid `conditionalFormatting` and +the runtime. Back-compat: the native shape keeps validating unchanged. diff --git a/packages/plugin-kanban/src/KanbanEnhanced.tsx b/packages/plugin-kanban/src/KanbanEnhanced.tsx index 95d0a27c9..81ec0bfa8 100644 --- a/packages/plugin-kanban/src/KanbanEnhanced.tsx +++ b/packages/plugin-kanban/src/KanbanEnhanced.tsx @@ -27,6 +27,7 @@ import { import { CSS } from "@dnd-kit/utilities" import { Badge, Card, CardHeader, CardTitle, CardDescription, CardContent, Button, Input } from "@object-ui/components" import { resolveConditionalFormatting } from "@object-ui/core" +import type { KanbanConditionalFormattingRule } from "@object-ui/types" import { ChevronDown, ChevronRight, AlertTriangle, Plus } from "lucide-react" const cn = (...classes: (string | undefined)[]) => classes.filter(Boolean).join(' ') @@ -49,13 +50,9 @@ export interface KanbanColumn { collapsed?: boolean } -export interface ConditionalFormattingRule { - field: string - operator: 'equals' | 'not_equals' | 'contains' | 'in' - value: string | string[] - backgroundColor?: string - borderColor?: string -} +// Card formatting accepts the native `{ field, operator, value }` shape and the +// spec `{ condition, style }` CEL shape (issue #1584) — see @object-ui/types. +export type ConditionalFormattingRule = KanbanConditionalFormattingRule export interface KanbanEnhancedProps { columns: KanbanColumn[] diff --git a/packages/plugin-kanban/src/KanbanImpl.tsx b/packages/plugin-kanban/src/KanbanImpl.tsx index fae132c3e..4d2c39365 100644 --- a/packages/plugin-kanban/src/KanbanImpl.tsx +++ b/packages/plugin-kanban/src/KanbanImpl.tsx @@ -28,6 +28,7 @@ import { CSS } from "@dnd-kit/utilities" import { Badge, Card, CardHeader, CardTitle, CardDescription, CardContent, ScrollArea, Button, Input, useResizeObserver, DataEmptyState } from "@object-ui/components" import { useHasDndProvider, useDnd } from "@object-ui/react" import { resolveConditionalFormatting } from "@object-ui/core" +import type { KanbanConditionalFormattingRule } from "@object-ui/types" import { createSafeTranslation } from "@object-ui/i18n" import { Plus } from "lucide-react" @@ -80,13 +81,9 @@ export interface KanbanColumn { className?: string } -export interface ConditionalFormattingRule { - field: string - operator: 'equals' | 'not_equals' | 'contains' | 'in' - value: string | string[] - backgroundColor?: string - borderColor?: string -} +// Card formatting accepts the native `{ field, operator, value }` shape and the +// spec `{ condition, style }` CEL shape (issue #1584) — see @object-ui/types. +export type ConditionalFormattingRule = KanbanConditionalFormattingRule export interface KanbanBoardProps { columns: KanbanColumn[] diff --git a/packages/plugin-kanban/src/index.tsx b/packages/plugin-kanban/src/index.tsx index 05bcfcb03..6720e9d4a 100644 --- a/packages/plugin-kanban/src/index.tsx +++ b/packages/plugin-kanban/src/index.tsx @@ -10,6 +10,7 @@ import React, { Suspense } from 'react'; import { ComponentRegistry } from '@object-ui/core'; import { useSchemaContext } from '@object-ui/react'; import { Skeleton } from '@object-ui/components'; +import type { KanbanConditionalFormattingRule } from '@object-ui/types'; import { ObjectKanban } from './ObjectKanban'; // Export types for external use @@ -47,13 +48,7 @@ export interface KanbanRendererProps { quickAdd?: boolean; onQuickAdd?: (columnId: string, title: string) => void; coverImageField?: string; - conditionalFormatting?: Array<{ - field: string; - operator: 'equals' | 'not_equals' | 'contains' | 'in'; - value: string | string[]; - backgroundColor?: string; - borderColor?: string; - }>; + conditionalFormatting?: KanbanConditionalFormattingRule[]; }; } diff --git a/packages/plugin-kanban/src/types.ts b/packages/plugin-kanban/src/types.ts index c6cc120fb..d92678bc6 100644 --- a/packages/plugin-kanban/src/types.ts +++ b/packages/plugin-kanban/src/types.ts @@ -6,7 +6,7 @@ * LICENSE file in the root directory of this source tree. */ -import type { BaseSchema, GroupingConfig } from '@object-ui/types'; +import type { BaseSchema, GroupingConfig, KanbanConditionalFormattingRule } from '@object-ui/types'; /** * Kanban card interface. @@ -118,15 +118,11 @@ export interface KanbanSchema extends BaseSchema { allowCollapse?: boolean; /** - * Conditional formatting rules for card coloring. + * Conditional formatting rules for card coloring. Accepts the native + * `{ field, operator, value }` shape and the spec `{ condition, style }` CEL + * shape (issue #1584). */ - conditionalFormatting?: Array<{ - field: string; - operator: 'equals' | 'not_equals' | 'contains' | 'in'; - value: string | string[]; - backgroundColor?: string; - borderColor?: string; - }>; + conditionalFormatting?: KanbanConditionalFormattingRule[]; /** * Predefined card templates for quick-add. diff --git a/packages/types/src/__tests__/kanban-conditional-formatting.test.ts b/packages/types/src/__tests__/kanban-conditional-formatting.test.ts new file mode 100644 index 000000000..ac28cb510 --- /dev/null +++ b/packages/types/src/__tests__/kanban-conditional-formatting.test.ts @@ -0,0 +1,71 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * Kanban conditional formatting accepts CEL (#1584). + * + * Since kanban card styling moved onto the shared CEL evaluator, the kanban + * schema's type + zod contract must match the runtime: a rule may be the native + * `{ field, operator, value }` shape OR the spec `{ condition, style }` CEL + * shape. This locks both so the two can't drift back apart. + */ +import { describe, it, expect } from 'vitest'; +import { ObjectKanbanSchema } from '../zod/index.zod'; +import type { KanbanConditionalFormattingRule } from '../objectql'; + +describe('kanban conditionalFormatting — zod contract', () => { + const base = { type: 'object-kanban', objectName: 'task', groupField: 'status' }; + + it('accepts the native { field, operator, value } rule (back-compat)', () => { + const parsed = ObjectKanbanSchema.safeParse({ + ...base, + conditionalFormatting: [ + { field: 'priority', operator: 'equals', value: 'high', backgroundColor: '#fee2e2' }, + ], + }); + expect(parsed.success).toBe(true); + }); + + it('accepts the spec { condition, style } CEL rule (new)', () => { + const parsed = ObjectKanbanSchema.safeParse({ + ...base, + conditionalFormatting: [ + { condition: "record.status == 'done'", style: { backgroundColor: '#e0ffe0' } }, + ], + }); + expect(parsed.success).toBe(true); + }); + + it('accepts a mix of both shapes in one rule list', () => { + const parsed = ObjectKanbanSchema.safeParse({ + ...base, + conditionalFormatting: [ + { condition: "record.blocked == true", style: { borderColor: 'red' } }, + { field: 'priority', operator: 'in', value: ['high', 'urgent'], backgroundColor: '#fef9c3' }, + ], + }); + expect(parsed.success).toBe(true); + }); +}); + +describe('kanban conditionalFormatting — type contract', () => { + it('KanbanConditionalFormattingRule admits both shapes at compile time', () => { + const native: KanbanConditionalFormattingRule = { + field: 'priority', + operator: 'equals', + value: 'high', + backgroundColor: '#fee2e2', + }; + const cel: KanbanConditionalFormattingRule = { + condition: "record.status == 'done'", + style: { backgroundColor: '#e0ffe0' }, + }; + expect(native).toBeTruthy(); + expect(cel).toBeTruthy(); + }); +}); diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 5c1af4592..f40db597b 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -336,6 +336,7 @@ export type { ObjectCalendarSchema, ObjectKanbanSchema, KanbanConditionalFormattingRule, + KanbanNativeConditionalFormattingRule, ObjectChartSchema, ListViewSchema, ObjectGridSchema, diff --git a/packages/types/src/objectql.ts b/packages/types/src/objectql.ts index 246bd0784..5cbf82b8a 100644 --- a/packages/types/src/objectql.ts +++ b/packages/types/src/objectql.ts @@ -2185,9 +2185,9 @@ export interface ObjectKanbanSchema extends BaseSchema { } /** - * Conditional formatting rule for Kanban cards + * Native (field/operator/value) conditional formatting rule for Kanban cards. */ -export interface KanbanConditionalFormattingRule { +export interface KanbanNativeConditionalFormattingRule { /** Field name to check */ field: string; /** Operator for comparison */ @@ -2200,6 +2200,18 @@ export interface KanbanConditionalFormattingRule { borderColor?: string; } +/** + * Conditional formatting rule for Kanban cards. + * + * Since #1584, kanban card styling runs on the shared CEL evaluator, so a rule + * accepts BOTH the native `{ field, operator, value }` shape and the spec + * `{ condition, style }` shape (a CEL predicate + style map) — the same + * `record.*` predicates authors use on list/grid rows. + */ +export type KanbanConditionalFormattingRule = + | KanbanNativeConditionalFormattingRule + | SpecConditionalFormattingRule; + /** * Object Chart Component Schema */ diff --git a/packages/types/src/zod/objectql.zod.ts b/packages/types/src/zod/objectql.zod.ts index a3919eba4..70a74d257 100644 --- a/packages/types/src/zod/objectql.zod.ts +++ b/packages/types/src/zod/objectql.zod.ts @@ -436,13 +436,23 @@ export const ObjectCalendarSchema = BaseSchema.extend({ /** * ObjectKanban Schema */ -const KanbanConditionalFormattingRuleSchema = z.object({ - field: z.string().describe('Field name to check'), - operator: z.enum(['equals', 'not_equals', 'contains', 'in']).describe('Comparison operator'), - value: z.union([z.string(), z.array(z.string())]).describe('Value to compare against'), - backgroundColor: z.string().optional().describe('Background color'), - borderColor: z.string().optional().describe('Border color'), -}); +// Since #1584, kanban card styling runs on the shared CEL evaluator, so a +// kanban rule accepts BOTH the native `{ field, operator, value }` shape and the +// spec `{ condition, style }` shape (a CEL predicate + style map) — matching +// list/grid `conditionalFormatting`. The type/schema now match the runtime. +const KanbanConditionalFormattingRuleSchema = z.union([ + z.object({ + field: z.string().describe('Field name to check'), + operator: z.enum(['equals', 'not_equals', 'contains', 'in']).describe('Comparison operator'), + value: z.union([z.string(), z.array(z.string())]).describe('Value to compare against'), + backgroundColor: z.string().optional().describe('Background color'), + borderColor: z.string().optional().describe('Border color'), + }), + z.object({ + condition: z.string().describe('CEL predicate evaluated against the card record'), + style: z.record(z.string(), z.string()).describe('CSS styles applied when the condition is true'), + }), +]); export const ObjectKanbanSchema = BaseSchema.extend({ type: z.literal('object-kanban'),