Skip to content

Commit f1ed882

Browse files
authored
Merge pull request #806 from objectstack-ai/copilot/fix-ui-protocol-design-issues
2 parents a81ffb2 + 3282f5b commit f1ed882

14 files changed

Lines changed: 551 additions & 44 deletions

ROADMAP.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ObjectStack Protocol — Road Map
22

3-
> **Last Updated:** 2026-02-22
3+
> **Last Updated:** 2026-02-24
44
> **Current Version:** v3.0.8
55
> **Status:** Protocol Specification Complete · Runtime Implementation In Progress
66
@@ -696,6 +696,14 @@ Protocol enhancements and core component implementations for dashboard feature p
696696
- [x] Enhance `globalFilters` with `options`, `optionsFrom`, `defaultValue`, `scope`, `targetWidgets` ([#712](https://github.com/objectstack-ai/spec/issues/712))
697697
- [x] Add `header` configuration to `DashboardSchema` with `showTitle`, `showDescription`, `actions` ([#714](https://github.com/objectstack-ai/spec/issues/714))
698698
- [x] Add `pivotConfig` and `measures` array to `DashboardWidgetSchema` for multi-measure pivots ([#714](https://github.com/objectstack-ai/spec/issues/714))
699+
- [x] Add required `id` field (SnakeCaseIdentifier) to `DashboardWidgetSchema` for `targetWidgets` referencing
700+
- [x] Unify `WidgetActionTypeSchema` with `ActionSchema.type` — add `script` and `api` types
701+
- [x] Add `.superRefine` conditional validation to `PageSchema` (`recordReview` required for `record_review`, `blankLayout` for `blank`)
702+
- [x] Unify easing naming in `AnimationSchema` (theme.zod) to snake_case (`ease_in`, `ease_out`, `ease_in_out`)
703+
- [x] Add `themeToken` reference to `TransitionConfigSchema` for theme animation token binding
704+
- [x] Add `ResponsiveConfigSchema` and `PerformanceConfigSchema` to `ListViewSchema`
705+
- [x] Migrate `HttpMethodSchema` / `HttpRequestSchema` from `view.zod.ts` to `shared/http.zod.ts` (re-exported for backward compat)
706+
- [x] Rename `ThemeMode``ThemeModeSchema`, `DensityMode``DensityModeSchema`, `WcagContrastLevel``WcagContrastLevelSchema` (deprecated aliases kept)
699707

700708
**ObjectUI Component Implementations:**
701709
- [ ] Implement `DashboardFilterBar` component for global filters ([objectui#588](https://github.com/objectstack-ai/objectui/issues/588))

packages/spec/src/shared/http.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { describe, it, expect } from 'vitest';
22
import {
33
HttpMethod,
4+
HttpMethodSchema,
5+
HttpRequestSchema,
46
CorsConfigSchema,
57
RateLimitConfigSchema,
68
StaticMountSchema,
@@ -134,3 +136,44 @@ describe('StaticMountSchema', () => {
134136
expect(() => StaticMountSchema.parse({ path: '/static' })).toThrow();
135137
});
136138
});
139+
140+
// ============================================================================
141+
// Issue #8: HttpMethodSchema and HttpRequestSchema migrated to shared
142+
// ============================================================================
143+
describe('HttpMethodSchema (migrated from view.zod)', () => {
144+
it('should accept common HTTP methods', () => {
145+
const valid = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
146+
valid.forEach(m => {
147+
expect(HttpMethodSchema.parse(m)).toBe(m);
148+
});
149+
});
150+
151+
it('should reject HEAD and OPTIONS (subset for UI data sources)', () => {
152+
expect(() => HttpMethodSchema.parse('HEAD')).toThrow();
153+
expect(() => HttpMethodSchema.parse('OPTIONS')).toThrow();
154+
});
155+
});
156+
157+
describe('HttpRequestSchema (migrated from view.zod)', () => {
158+
it('should accept minimal request with url only', () => {
159+
const result = HttpRequestSchema.parse({ url: 'https://api.example.com/data' });
160+
expect(result.url).toBe('https://api.example.com/data');
161+
expect(result.method).toBe('GET');
162+
});
163+
164+
it('should accept full request with all fields', () => {
165+
const result = HttpRequestSchema.parse({
166+
url: 'https://api.example.com/data',
167+
method: 'POST',
168+
headers: { 'Authorization': 'Bearer token' },
169+
params: { page: 1 },
170+
body: { name: 'test' },
171+
});
172+
expect(result.method).toBe('POST');
173+
expect(result.headers?.['Authorization']).toBe('Bearer token');
174+
});
175+
176+
it('should reject request without url', () => {
177+
expect(() => HttpRequestSchema.parse({})).toThrow();
178+
});
179+
});

packages/spec/src/shared/http.zod.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,30 @@ export const HttpMethod = z.enum([
2828

2929
export type HttpMethod = z.infer<typeof HttpMethod>;
3030

31+
/**
32+
* HTTP Method Schema (subset for UI/View data sources)
33+
* Common HTTP methods used in view data source configurations.
34+
* Migrated from ui/view.zod.ts to shared for reuse across modules.
35+
*/
36+
export const HttpMethodSchema = z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']);
37+
38+
export type HttpMethodType = z.infer<typeof HttpMethodSchema>;
39+
40+
/**
41+
* HTTP Request Configuration Schema
42+
* Defines a complete HTTP request configuration used by API data providers.
43+
* Migrated from ui/view.zod.ts to shared for reuse across modules.
44+
*/
45+
export const HttpRequestSchema = z.object({
46+
url: z.string().describe('API endpoint URL'),
47+
method: HttpMethodSchema.optional().default('GET').describe('HTTP method'),
48+
headers: z.record(z.string(), z.string()).optional().describe('Custom HTTP headers'),
49+
params: z.record(z.string(), z.unknown()).optional().describe('Query parameters'),
50+
body: z.unknown().optional().describe('Request body for POST/PUT/PATCH'),
51+
});
52+
53+
export type HttpRequest = z.infer<typeof HttpRequestSchema>;
54+
3155
// ==========================================
3256
// CORS Configuration
3357
// ==========================================

packages/spec/src/stack.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,8 +1013,8 @@ describe('defineStack - Example-Level Strict Validation', () => {
10131013
name: 'task_overview',
10141014
label: 'Task Overview',
10151015
widgets: [
1016-
{ title: 'Total Tasks', type: 'metric', object: 'task', aggregate: 'count', layout: { x: 0, y: 0, w: 3, h: 2 } },
1017-
{ title: 'By Status', type: 'pie', object: 'task', categoryField: 'status', aggregate: 'count', layout: { x: 3, y: 0, w: 6, h: 4 } },
1016+
{ id: 'total_tasks', title: 'Total Tasks', type: 'metric', object: 'task', aggregate: 'count', layout: { x: 0, y: 0, w: 3, h: 2 } },
1017+
{ id: 'by_status', title: 'By Status', type: 'pie', object: 'task', categoryField: 'status', aggregate: 'count', layout: { x: 3, y: 0, w: 6, h: 4 } },
10181018
],
10191019
},
10201020
],
@@ -1094,7 +1094,7 @@ describe('defineStack - Example-Level Strict Validation', () => {
10941094
name: 'sales_overview',
10951095
label: 'Sales Overview',
10961096
widgets: [
1097-
{ title: 'Pipeline Value', type: 'metric', object: 'opportunity', valueField: 'amount', aggregate: 'sum', layout: { x: 0, y: 0, w: 4, h: 2 } },
1097+
{ id: 'pipeline_value', title: 'Pipeline Value', type: 'metric', object: 'opportunity', valueField: 'amount', aggregate: 'sum', layout: { x: 0, y: 0, w: 4, h: 2 } },
10981098
],
10991099
},
11001100
],

packages/spec/src/ui/animation.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,3 +235,31 @@ describe('I18n and ARIA integration', () => {
235235
expect(result.role).toBeUndefined();
236236
});
237237
});
238+
239+
// ============================================================================
240+
// Issue #6: TransitionConfigSchema themeToken support
241+
// ============================================================================
242+
describe('TransitionConfigSchema - themeToken', () => {
243+
it('should accept transition with themeToken reference', () => {
244+
const result = TransitionConfigSchema.parse({
245+
themeToken: 'animation.duration.fast',
246+
});
247+
expect(result.themeToken).toBe('animation.duration.fast');
248+
});
249+
250+
it('should accept transition combining themeToken with explicit values', () => {
251+
const result = TransitionConfigSchema.parse({
252+
preset: 'fade',
253+
duration: 200,
254+
easing: 'ease_in_out',
255+
themeToken: 'animation.timing.ease_in_out',
256+
});
257+
expect(result.themeToken).toBe('animation.timing.ease_in_out');
258+
expect(result.duration).toBe(200);
259+
});
260+
261+
it('should leave themeToken undefined when not provided', () => {
262+
const result = TransitionConfigSchema.parse({ duration: 300 });
263+
expect(result.themeToken).toBeUndefined();
264+
});
265+
});

packages/spec/src/ui/animation.zod.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export const TransitionConfigSchema = z.object({
4646
easing: EasingFunctionSchema.optional().describe('Easing function for the transition'),
4747
delay: z.number().optional().describe('Delay before transition starts in milliseconds'),
4848
customKeyframes: z.string().optional().describe('CSS @keyframes name for custom animations'),
49+
themeToken: z.string().optional().describe('Reference to a theme animation token (e.g. "animation.duration.fast")'),
4950
}).describe('Animation transition configuration');
5051

5152
export type TransitionConfig = z.infer<typeof TransitionConfigSchema>;

0 commit comments

Comments
 (0)