Skip to content

Commit 6dd695e

Browse files
authored
Merge pull request #7 from objectql/copilot/add-page-metadata-support
2 parents 8d0ef8a + 5b0f2eb commit 6dd695e

File tree

6 files changed

+280
-1
lines changed

6 files changed

+280
-1
lines changed

docs/spec/metadata-format.md

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,3 +374,155 @@ sort:
374374
- - budget
375375
- desc
376376
```
377+
378+
## 8. Page Definition
379+
380+
Page files define user interface pages or dashboards that display data and visualizations. They use the naming convention `*.page.yml` or `*.page.yaml`.
381+
382+
Similar to Airtable's interface builder, pages allow you to compose various components (charts, tables, forms) into cohesive user experiences.
383+
384+
### 8.1 Root Properties
385+
386+
| Property | Type | Description |
387+
| :--- | :--- | :--- |
388+
| `name` | `string` | **Required.** Unique API name of the page. |
389+
| `label` | `string` | Human-readable label for the page. |
390+
| `description` | `string` | Description of the page's purpose. |
391+
| `icon` | `string` | Icon identifier for the page (e.g., `dashboard`, `table`, `chart`). |
392+
| `layout` | `string` | Layout type: `grid`, `flex`, `stack`, or `tabs`. Default: `grid`. |
393+
| `components` | `array` | Array of page components to display. |
394+
| `settings` | `object` | Layout-specific settings (e.g., grid columns, gaps, responsive behavior). |
395+
396+
### 8.2 Page Components
397+
398+
Components are defined as objects with a `type` and optional `props`:
399+
400+
```yaml
401+
components:
402+
- type: chart
403+
props:
404+
chartName: projects_by_status
405+
- type: table
406+
props:
407+
object: projects
408+
fields:
409+
- name
410+
- status
411+
- priority
412+
```
413+
414+
**Common Component Types:**
415+
- `chart`: Display a chart visualization (requires `chartName` prop)
416+
- `table`: Display a data table (requires `object` prop)
417+
- `form`: Display a data entry form
418+
- `text`: Display static text or markdown content
419+
- `custom`: Custom component implementation
420+
421+
### 8.3 Layout Types
422+
423+
**Grid Layout**: Responsive grid with configurable columns
424+
425+
**Flex Layout**: Flexible box layout for responsive designs
426+
427+
**Stack Layout**: Vertical or horizontal stack of components
428+
429+
**Tabs Layout**: Tabbed interface for organizing multiple views
430+
431+
### 8.4 Example Page Definitions
432+
433+
#### Dashboard with Charts
434+
435+
```yaml
436+
name: projects_dashboard
437+
label: Projects Dashboard
438+
description: Overview of all projects with charts and task tracking
439+
icon: dashboard
440+
layout: grid
441+
components:
442+
- type: chart
443+
props:
444+
chartName: projects_by_status
445+
- type: chart
446+
props:
447+
chartName: projects_by_priority
448+
- type: chart
449+
props:
450+
chartName: project_budget
451+
- type: chart
452+
props:
453+
chartName: tasks_completion
454+
- type: table
455+
props:
456+
object: projects
457+
fields:
458+
- name
459+
- status
460+
- priority
461+
- start_date
462+
- budget
463+
settings:
464+
gridColumns: 2
465+
gap: 20
466+
responsive: true
467+
```
468+
469+
#### Simple Detail Page
470+
471+
```yaml
472+
name: project_detail
473+
label: Project Details
474+
description: Detailed view of a single project
475+
icon: file
476+
layout: stack
477+
components:
478+
- type: form
479+
props:
480+
object: projects
481+
mode: view
482+
- type: table
483+
props:
484+
object: tasks
485+
filters:
486+
- - project
487+
- =
488+
- $current.id
489+
settings:
490+
direction: vertical
491+
gap: 16
492+
```
493+
494+
#### Tabbed Interface
495+
496+
```yaml
497+
name: project_tabs
498+
label: Project Workspace
499+
description: Tabbed interface for project management
500+
icon: layers
501+
layout: tabs
502+
components:
503+
- type: tab
504+
props:
505+
label: Overview
506+
children:
507+
- type: chart
508+
props:
509+
chartName: projects_by_status
510+
- type: tab
511+
props:
512+
label: Tasks
513+
children:
514+
- type: table
515+
props:
516+
object: tasks
517+
- type: tab
518+
props:
519+
label: Budget
520+
children:
521+
- type: chart
522+
props:
523+
chartName: project_budget
524+
settings:
525+
defaultTab: 0
526+
tabPosition: top
527+
```
528+
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: projects_dashboard
2+
label: Projects Dashboard
3+
description: Overview of all projects with charts and task tracking
4+
icon: dashboard
5+
layout: grid
6+
components:
7+
- type: chart
8+
props:
9+
chartName: projects_by_status
10+
- type: chart
11+
props:
12+
chartName: projects_by_priority
13+
- type: chart
14+
props:
15+
chartName: project_budget
16+
- type: chart
17+
props:
18+
chartName: tasks_completion
19+
- type: table
20+
props:
21+
object: projects
22+
fields:
23+
- name
24+
- status
25+
- priority
26+
- start_date
27+
- budget
28+
settings:
29+
gridColumns: 2
30+
gap: 20
31+
responsive: true

packages/metadata/src/plugins/objectql.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as yaml from 'js-yaml';
22
import * as path from 'path';
33
import { MetadataLoader } from '../loader';
4-
import { ObjectConfig, ChartConfig } from '../types';
4+
import { ObjectConfig, ChartConfig, PageConfig } from '../types';
55

66
export function registerObjectQLPlugins(loader: MetadataLoader) {
77
// Objects
@@ -192,6 +192,32 @@ export function registerObjectQLPlugins(loader: MetadataLoader) {
192192
}
193193
}
194194
});
195+
196+
// Pages
197+
loader.use({
198+
name: 'page',
199+
glob: ['**/*.page.yml', '**/*.page.yaml'],
200+
handler: (ctx) => {
201+
try {
202+
const doc = yaml.load(ctx.content) as any;
203+
if (!doc) return;
204+
205+
if (doc.name) {
206+
registerPage(ctx.registry, doc, ctx.file, ctx.packageName);
207+
} else {
208+
for (const [key, value] of Object.entries(doc)) {
209+
if (value && !Array.isArray(value) && typeof value === 'object') {
210+
const page = value as any;
211+
if (!page.name) page.name = key;
212+
registerPage(ctx.registry, page, ctx.file, ctx.packageName);
213+
}
214+
}
215+
}
216+
} catch (e) {
217+
console.error(`Error loading page from ${ctx.file}:`, e);
218+
}
219+
}
220+
});
195221
}
196222

197223
function registerObject(registry: any, obj: any, file: string, packageName?: string) {
@@ -224,3 +250,13 @@ function registerChart(registry: any, chart: any, file: string, packageName?: st
224250
});
225251
}
226252

253+
function registerPage(registry: any, page: any, file: string, packageName?: string) {
254+
registry.register('page', {
255+
type: 'page',
256+
id: page.name,
257+
path: file,
258+
package: packageName,
259+
content: page
260+
});
261+
}
262+

packages/metadata/src/types.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,21 @@ export interface ChartConfig {
114114
filters?: any[];
115115
sort?: [string, 'asc' | 'desc'][];
116116
}
117+
118+
export type PageLayoutType = 'grid' | 'flex' | 'stack' | 'tabs';
119+
120+
export interface PageComponent {
121+
type: string;
122+
props?: Record<string, any>;
123+
children?: PageComponent[];
124+
}
125+
126+
export interface PageConfig {
127+
name: string;
128+
label?: string;
129+
description?: string;
130+
icon?: string;
131+
layout?: PageLayoutType;
132+
components?: PageComponent[];
133+
settings?: Record<string, any>;
134+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: dashboard
2+
label: Dashboard
3+
description: Main dashboard page with charts and metrics
4+
icon: dashboard
5+
layout: grid
6+
components:
7+
- type: chart
8+
props:
9+
chartName: projects_by_status
10+
- type: chart
11+
props:
12+
chartName: projects_by_priority
13+
- type: table
14+
props:
15+
object: projects
16+
settings:
17+
gridColumns: 2
18+
gap: 16

packages/metadata/test/index.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,27 @@ describe('Chart Metadata Loader', () => {
102102
expect(chart.yAxisKeys).toEqual(['value']);
103103
});
104104
});
105+
106+
describe('Page Metadata Loader', () => {
107+
it('should load page metadata from .page.yml files', () => {
108+
const registry = new MetadataRegistry();
109+
const loader = new MetadataLoader(registry);
110+
111+
registerObjectQLPlugins(loader);
112+
113+
const fixturesDir = path.join(__dirname, 'fixtures');
114+
loader.load(fixturesDir);
115+
116+
const page = registry.get('page', 'dashboard');
117+
expect(page).toBeDefined();
118+
expect(page.name).toBe('dashboard');
119+
expect(page.label).toBe('Dashboard');
120+
expect(page.description).toBe('Main dashboard page with charts and metrics');
121+
expect(page.icon).toBe('dashboard');
122+
expect(page.layout).toBe('grid');
123+
expect(page.components).toBeDefined();
124+
expect(page.components).toHaveLength(3);
125+
expect(page.settings).toBeDefined();
126+
expect(page.settings.gridColumns).toBe(2);
127+
});
128+
});

0 commit comments

Comments
 (0)