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
14 changes: 14 additions & 0 deletions .changeset/kanban-conditional-formatting-cel.md
Original file line number Diff line number Diff line change
@@ -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.
11 changes: 4 additions & 7 deletions packages/plugin-kanban/src/KanbanEnhanced.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(' ')
Expand All @@ -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[]
Expand Down
11 changes: 4 additions & 7 deletions packages/plugin-kanban/src/KanbanImpl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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[]
Expand Down
9 changes: 2 additions & 7 deletions packages/plugin-kanban/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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[];
};
}

Expand Down
14 changes: 5 additions & 9 deletions packages/plugin-kanban/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
71 changes: 71 additions & 0 deletions packages/types/src/__tests__/kanban-conditional-formatting.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
1 change: 1 addition & 0 deletions packages/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ export type {
ObjectCalendarSchema,
ObjectKanbanSchema,
KanbanConditionalFormattingRule,
KanbanNativeConditionalFormattingRule,
ObjectChartSchema,
ListViewSchema,
ObjectGridSchema,
Expand Down
16 changes: 14 additions & 2 deletions packages/types/src/objectql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -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
*/
Expand Down
24 changes: 17 additions & 7 deletions packages/types/src/zod/objectql.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
Loading