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
5 changes: 4 additions & 1 deletion examples/app-showcase/e2e/showcase-smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const SURFACES: { name: string; path: string; chart?: boolean }[] = [
{ name: 'Field Zoo', path: base('showcase_field_zoo') },
{ name: 'Delivery Operations', path: base('dashboard/showcase_ops_dashboard'), chart: true },
{ name: 'Chart Gallery', path: base('dashboard/showcase_chart_gallery'), chart: true },
{ name: 'Command Center', path: base('page/showcase_command_center'), chart: true },
{ name: 'Hours by Status', path: base('report/showcase_hours_by_status') },
{ name: 'Status × Priority', path: base('report/showcase_status_priority_matrix') },
{ name: 'Task Overview', path: base('report/showcase_task_overview') },
Expand Down Expand Up @@ -64,7 +65,9 @@ for (const surface of SURFACES) {
await svg.waitFor({ state: 'visible', timeout: 25_000 });
const box = await svg.boundingBox();
expect(box, `${surface.name}: no chart SVG`).not.toBeNull();
expect(box!.width, `${surface.name}: chart width`).toBeGreaterThan(0);
// >0 alone previously passed even on a collapsed ~130px-wide chart panel
// (#2616 D) — require a width a real chart panel would actually have.
expect(box!.width, `${surface.name}: chart width`).toBeGreaterThan(200);
expect(box!.height, `${surface.name}: chart height`).toBeGreaterThan(0);
}
});
Expand Down
31 changes: 21 additions & 10 deletions examples/app-showcase/src/data/seed/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@ import { FieldZoo } from '../objects/field-zoo.object.js';
* work location (map), and projects span every status and health.
*/

/**
* Local, offline-safe placeholder cover image. Task `cover` seeds used to
* point at picsum.photos, which renders as a wall of broken images in
* offline/restricted-network environments (Gallery, All Views). A data: URI
* needs no network at all and still gives each card a distinct color + number.
*/
function placeholderCover(seed: number, color: string): string {
const svg = `<svg xmlns='http://www.w3.org/2000/svg' width='480' height='300'><rect width='480' height='300' fill='${color}'/><text x='50%' y='50%' dominant-baseline='middle' text-anchor='middle' font-family='system-ui,sans-serif' font-size='96' font-weight='700' fill='#ffffff' fill-opacity='0.35'>${seed}</text></svg>`;
return `data:image/svg+xml,${encodeURIComponent(svg)}`;
}

const accounts = defineSeed(Account, {
mode: 'upsert',
externalId: 'name',
Expand Down Expand Up @@ -77,16 +88,16 @@ const tasks = defineSeed(Task, {
mode: 'upsert',
externalId: 'title',
records: [
{ cover: 'https://picsum.photos/seed/showcasetask1/480/300', title: 'Audit current IA', project: 'Website Relaunch', assignee: 'ada@example.com', status: 'done', priority: 'medium', estimate_hours: 8, progress: 100, done: true, created_at: cel`daysAgo(20)`, start_date: cel`daysAgo(20)`, end_date: cel`daysAgo(18)`, due_date: cel`daysAgo(18)`, location: { lat: 47.6062, lng: -122.3321 } },
{ cover: 'https://picsum.photos/seed/showcasetask2/480/300', title: 'Design system', project: 'Website Relaunch', assignee: 'ada@example.com', status: 'in_review', priority: 'high', estimate_hours: 24, progress: 80, done: false, created_at: cel`daysAgo(14)`, start_date: cel`daysAgo(12)`, end_date: cel`daysFromNow(2)`, due_date: cel`daysFromNow(2)`, location: { lat: 37.7749, lng: -122.4194 } },
{ cover: 'https://picsum.photos/seed/showcasetask3/480/300', title: 'Build homepage', project: 'Website Relaunch', assignee: 'sam@example.com', status: 'in_progress', priority: 'high', estimate_hours: 40, progress: 45, done: false, created_at: cel`daysAgo(8)`, start_date: cel`daysAgo(6)`, end_date: cel`daysFromNow(10)`, due_date: cel`daysFromNow(10)`, location: { lat: 40.7128, lng: -74.0060 } },
{ cover: 'https://picsum.photos/seed/showcasetask4/480/300', title: 'SEO migration plan', project: 'Website Relaunch', assignee: 'sam@example.com', status: 'todo', priority: 'medium', estimate_hours: 16, progress: 0, done: false, created_at: cel`daysAgo(3)`, start_date: cel`daysFromNow(5)`, end_date: cel`daysFromNow(15)`, due_date: cel`daysFromNow(15)`, location: { lat: 30.2672, lng: -97.7431 } },
{ cover: 'https://picsum.photos/seed/showcasetask5/480/300', title: 'Content backlog', project: 'Website Relaunch', assignee: 'grace@example.com', status: 'backlog', priority: 'low', estimate_hours: 12, progress: 0, done: false, created_at: cel`daysAgo(2)`, due_date: cel`daysFromNow(30)`, location: { lat: 41.8781, lng: -87.6298 } },
{ cover: 'https://picsum.photos/seed/showcasetask6/480/300', title: 'Ingest pipeline', project: 'Data Platform', assignee: 'linus@example.com', status: 'in_progress', priority: 'urgent', estimate_hours: 60, progress: 55, done: false, created_at: cel`daysAgo(40)`, start_date: cel`daysAgo(35)`, end_date: cel`daysFromNow(20)`, due_date: cel`daysFromNow(20)`, location: { lat: 39.7392, lng: -104.9903 } },
{ cover: 'https://picsum.photos/seed/showcasetask7/480/300', title: 'Warehouse schema', project: 'Data Platform', assignee: 'linus@example.com', status: 'in_review', priority: 'high', estimate_hours: 30, progress: 90, done: false, created_at: cel`daysAgo(25)`, start_date: cel`daysAgo(22)`, end_date: cel`daysFromNow(3)`, due_date: cel`daysFromNow(3)`, location: { lat: 42.3601, lng: -71.0589 } },
{ cover: 'https://picsum.photos/seed/showcasetask8/480/300', title: 'PII access review', project: 'Compliance Audit', assignee: 'grace@example.com', status: 'todo', priority: 'urgent', estimate_hours: 20, progress: 0, done: false, created_at: cel`daysAgo(5)`, start_date: cel`daysFromNow(2)`, end_date: cel`daysFromNow(12)`, due_date: cel`daysFromNow(12)`, location: { lat: 38.9072, lng: -77.0369 } },
{ cover: 'https://picsum.photos/seed/showcasetask9/480/300', title: 'Evidence collection', project: 'Compliance Audit', assignee: 'grace@example.com', status: 'backlog', priority: 'medium', estimate_hours: 18, progress: 0, done: false, created_at: cel`daysAgo(1)`, due_date: cel`daysFromNow(25)`, location: { lat: 34.0522, lng: -118.2437 } },
{ cover: 'https://picsum.photos/seed/showcasetask10/480/300', title: 'App wireframes', project: 'Mobile App', assignee: 'ada@example.com', status: 'done', priority: 'medium', estimate_hours: 16, progress: 100, done: true, created_at: cel`daysAgo(10)`, start_date: cel`daysAgo(10)`, end_date: cel`daysAgo(6)`, due_date: cel`daysAgo(6)`, location: { lat: 45.5152, lng: -122.6784 } },
{ cover: placeholderCover(1, '#10B981'), title: 'Audit current IA', project: 'Website Relaunch', assignee: 'ada@example.com', status: 'done', priority: 'medium', estimate_hours: 8, progress: 100, done: true, created_at: cel`daysAgo(20)`, start_date: cel`daysAgo(20)`, end_date: cel`daysAgo(18)`, due_date: cel`daysAgo(18)`, location: { lat: 47.6062, lng: -122.3321 } },
{ cover: placeholderCover(2, '#8B5CF6'), title: 'Design system', project: 'Website Relaunch', assignee: 'ada@example.com', status: 'in_review', priority: 'high', estimate_hours: 24, progress: 80, done: false, created_at: cel`daysAgo(14)`, start_date: cel`daysAgo(12)`, end_date: cel`daysFromNow(2)`, due_date: cel`daysFromNow(2)`, location: { lat: 37.7749, lng: -122.4194 } },
{ cover: placeholderCover(3, '#F59E0B'), title: 'Build homepage', project: 'Website Relaunch', assignee: 'sam@example.com', status: 'in_progress', priority: 'high', estimate_hours: 40, progress: 45, done: false, created_at: cel`daysAgo(8)`, start_date: cel`daysAgo(6)`, end_date: cel`daysFromNow(10)`, due_date: cel`daysFromNow(10)`, location: { lat: 40.7128, lng: -74.0060 } },
{ cover: placeholderCover(4, '#3B82F6'), title: 'SEO migration plan', project: 'Website Relaunch', assignee: 'sam@example.com', status: 'todo', priority: 'medium', estimate_hours: 16, progress: 0, done: false, created_at: cel`daysAgo(3)`, start_date: cel`daysFromNow(5)`, end_date: cel`daysFromNow(15)`, due_date: cel`daysFromNow(15)`, location: { lat: 30.2672, lng: -97.7431 } },
{ cover: placeholderCover(5, '#94A3B8'), title: 'Content backlog', project: 'Website Relaunch', assignee: 'grace@example.com', status: 'backlog', priority: 'low', estimate_hours: 12, progress: 0, done: false, created_at: cel`daysAgo(2)`, due_date: cel`daysFromNow(30)`, location: { lat: 41.8781, lng: -87.6298 } },
{ cover: placeholderCover(6, '#F59E0B'), title: 'Ingest pipeline', project: 'Data Platform', assignee: 'linus@example.com', status: 'in_progress', priority: 'urgent', estimate_hours: 60, progress: 55, done: false, created_at: cel`daysAgo(40)`, start_date: cel`daysAgo(35)`, end_date: cel`daysFromNow(20)`, due_date: cel`daysFromNow(20)`, location: { lat: 39.7392, lng: -104.9903 } },
{ cover: placeholderCover(7, '#8B5CF6'), title: 'Warehouse schema', project: 'Data Platform', assignee: 'linus@example.com', status: 'in_review', priority: 'high', estimate_hours: 30, progress: 90, done: false, created_at: cel`daysAgo(25)`, start_date: cel`daysAgo(22)`, end_date: cel`daysFromNow(3)`, due_date: cel`daysFromNow(3)`, location: { lat: 42.3601, lng: -71.0589 } },
{ cover: placeholderCover(8, '#3B82F6'), title: 'PII access review', project: 'Compliance Audit', assignee: 'grace@example.com', status: 'todo', priority: 'urgent', estimate_hours: 20, progress: 0, done: false, created_at: cel`daysAgo(5)`, start_date: cel`daysFromNow(2)`, end_date: cel`daysFromNow(12)`, due_date: cel`daysFromNow(12)`, location: { lat: 38.9072, lng: -77.0369 } },
{ cover: placeholderCover(9, '#94A3B8'), title: 'Evidence collection', project: 'Compliance Audit', assignee: 'grace@example.com', status: 'backlog', priority: 'medium', estimate_hours: 18, progress: 0, done: false, created_at: cel`daysAgo(1)`, due_date: cel`daysFromNow(25)`, location: { lat: 34.0522, lng: -118.2437 } },
{ cover: placeholderCover(10, '#10B981'), title: 'App wireframes', project: 'Mobile App', assignee: 'ada@example.com', status: 'done', priority: 'medium', estimate_hours: 16, progress: 100, done: true, created_at: cel`daysAgo(10)`, start_date: cel`daysAgo(10)`, end_date: cel`daysAgo(6)`, due_date: cel`daysAgo(6)`, location: { lat: 45.5152, lng: -122.6784 } },
],
});

Expand Down
19 changes: 13 additions & 6 deletions examples/app-showcase/src/ui/pages/command-center.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,16 @@ function kpi(id: string, object: string, label: string, colorVariant: string, ag
};
}

/** A dataset-bound chart with the shared brand palette. */
function chart(id: string, chartType: string, dataset: string, dimensions: string[], values: string[]): any {
return { id, type: 'object-chart', responsiveStyles: { large: { width: '100%', minWidth: '0' } }, properties: { dataset, dimensions, values, chartType, colors: PALETTE } };
/**
* A dataset-bound chart with the shared brand palette. `integerYAxis` pins the
* tick step to 1 — without it, count aggregates (small integers like 0-5)
* render fractional gridlines (0.8, 2.3, …) on the default Recharts axis.
*/
function chart(id: string, chartType: string, dataset: string, dimensions: string[], values: string[], integerYAxis?: boolean): any {
return {
id, type: 'object-chart', responsiveStyles: { large: { width: '100%', minWidth: '0' } },
properties: { dataset, dimensions, values, chartType, colors: PALETTE, ...(integerYAxis ? { yAxis: [{ field: values[0], stepSize: 1 }] } : {}) },
};
}

const CHART_H = '376px';
Expand Down Expand Up @@ -148,14 +155,14 @@ export const CommandCenterPage = definePage({

// ── Row A — three equal chart panels ─────────────────────────
band('cc_rowA', 3, [
panel({ id: 'cc_status', title: '任务状态分布', accent: A.c1, minHeight: CHART_H, child: chart('cc_status_c', 'bar', 'showcase_task_metrics', ['status'], ['task_count']) }),
panel({ id: 'cc_status', title: '任务状态分布', accent: A.c1, minHeight: CHART_H, child: chart('cc_status_c', 'bar', 'showcase_task_metrics', ['status'], ['task_count'], true) }),
panel({ id: 'cc_health', title: '项目健康度', accent: A.c4, minHeight: CHART_H, child: chart('cc_health_c', 'donut', 'showcase_project_metrics', ['health'], ['project_count']) }),
panel({ id: 'cc_priority', title: '优先级分布', accent: A.c5, minHeight: CHART_H, child: chart('cc_pri_c', 'bar', 'showcase_task_metrics', ['priority'], ['task_count']) }),
panel({ id: 'cc_priority', title: '优先级分布', accent: A.c5, minHeight: CHART_H, child: chart('cc_pri_c', 'bar', 'showcase_task_metrics', ['priority'], ['task_count'], true) }),
]),

// ── Row B — wide trend (span 2) + spend ──────────────────────
band('cc_rowB', 3, [
panel({ id: 'cc_throughput', title: '任务吞吐趋势 (月)', accent: A.c2, span: 'span 2', minHeight: CHART_H, child: chart('cc_thr_c', 'area', 'showcase_task_metrics', ['created_at'], ['task_count']) }),
panel({ id: 'cc_throughput', title: '任务吞吐趋势 (月)', accent: A.c2, span: 'span 2', minHeight: CHART_H, child: chart('cc_thr_c', 'area', 'showcase_task_metrics', ['created_at'], ['task_count'], true) }),
panel({ id: 'cc_budget', title: '预算 vs 支出 (按客户)', accent: A.c4, minHeight: CHART_H, child: chart('cc_bud_c', 'bar', 'showcase_project_metrics', ['account'], ['budget_sum', 'spent_sum']) }),
]),

Expand Down
7 changes: 5 additions & 2 deletions examples/app-showcase/src/ui/pages/crm-workbench.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,13 @@ function Page() {
const refreshStats = React.useCallback(async () => {
if (!adapter) return;
try {
const all = await adapter.find('showcase_project', { top: 200 });
// Canonical QueryOptionsV2 keys only (limit, not the legacy top alias
// removed in 11.0) — the KPI cards silently stuck at 0 whenever adapter.find
// rejected on the unrecognized param, and the empty catch hid the failure.
const all = await adapter.find('showcase_project', { limit: 200 });
const rows = Array.isArray(all) ? all : (all && all.records) || [];
setStats({ total: rows.length, active: rows.filter((r) => r.status === 'active').length });
} catch (e) { /* ignore in demo */ }
} catch (e) { console.warn('[CRM Workbench] failed to refresh stats', e); }
}, [adapter]);
React.useEffect(() => { refreshStats(); }, [refreshStats, reloadKey]);

Expand Down
5 changes: 5 additions & 0 deletions examples/app-showcase/src/ui/pages/new-project-wizard.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ export const NewProjectWizardPage = definePage({
{ label: 'Status & Health', description: 'Where does it stand today?', fields: ['status', 'health'] },
{ label: 'Budget & Schedule', description: 'Money and dates.', fields: ['budget', 'spent', 'start_date', 'end_date'] },
],
// Without this, a successful submit left the filled step-3 form in
// place with only a toast — re-clicking "Create" duplicated the
// record. `thank-you` swaps the form for a confirmation panel so
// there's nothing left to resubmit.
submitBehavior: { kind: 'thank-you', title: 'Project created', message: 'Your new project is ready — find it in Projects, or reopen this wizard to start another.' },
},
},
],
Expand Down
15 changes: 10 additions & 5 deletions examples/app-showcase/src/ui/pages/review-queue.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@ import { definePage } from '@objectstack/spec/ui';
* sees what is waiting on them; this is that surface.
*
* An interface (list) page over tasks currently `in_review` — the work
* awaiting a decision — with the source object's actions surfaced as toolbar
* buttons (Mark done = approve & complete) and a drawer to inspect each item.
* Tabs let the reviewer pivot to urgent or blocked work.
* awaiting a decision — with a drawer to inspect each item. "Mark Done" is
* deliberately NOT wired as a page-level `buttons:` toolbar entry: that
* surface has no bound record, so `MarkDoneAction`'s `visible: '!record.done'`
* expression has nothing to evaluate against and the button would render
* regardless of state. `MarkDoneAction.locations` already includes
* `list_item`, so it correctly appears per-row (with that row's record bound)
* instead. Tabs let the reviewer pivot to urgent or blocked work.
*/
export const ReviewQueuePage = definePage({
name: 'showcase_review_queue',
Expand All @@ -29,8 +33,9 @@ export const ReviewQueuePage = definePage({
sort: [{ field: 'due_date', order: 'asc' }],
appearance: { showDescription: true, allowedVisualizations: ['grid'] },
userActions: { sort: true, search: true, filter: false, rowHeight: false, addRecordForm: false },
// Object actions as toolbar buttons — "approve & complete" the reviewed item.
buttons: ['showcase_mark_done'],
// No `buttons:` entry here — see file header comment: "Mark Done" is a
// record-scoped action and belongs per-row (`list_item`), not as an
// unbound page-toolbar button.
// Click a row → drawer with the full record to review before deciding.
recordAction: 'drawer',
showRecordCount: true,
Expand Down
10 changes: 10 additions & 0 deletions packages/metadata-protocol/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2203,6 +2203,16 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
}
if (options.select !== undefined) delete options.select;

// fields: comma-separated string → array. Clients may pass `?fields=name`
// directly (not only via the `?select=` alias above) — a single-value
// querystring param arrives as a bare string, which drivers' `.map()`
// calls over `query.fields` would otherwise throw on.
if (typeof options.fields === 'string') {
options.fields = options.fields.split(',').map((s: string) => s.trim()).filter(Boolean);
} else if (options.fields !== undefined && !Array.isArray(options.fields)) {
delete options.fields;
}

// Sort/orderBy → orderBy: string → SortNode[] array
const sortValue = options.orderBy ?? options.sort;
if (typeof sortValue === 'string') {
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/driver-sql/src/sql-driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -633,7 +633,7 @@ export class SqlDriver implements IDataDriver {
const builder = buildBase();

// SELECT
if (query.fields) {
if (Array.isArray(query.fields) && query.fields.length > 0) {
builder.select((query.fields as string[]).map((f: string) => this.mapSortField(f)));
} else {
builder.select('*');
Expand Down
9 changes: 9 additions & 0 deletions packages/runtime/src/http-dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3280,6 +3280,15 @@ export class HttpDispatcher {
}

if (!aiService) {
// The console polls `GET /ai/agents` on every navigation to decide
// whether to show AI affordances. Reporting that as a 404 turns the
// normal "no AI service configured" state (the open-source default —
// service-ai is a Cloud/Enterprise package) into console error-log
// spam on every page. An empty list conveys the same information
// without looking like a fault. Every other /ai/* route still 404s.
if (method === 'GET' && subPath === '/ai/agents') {
return { handled: true, response: { status: 200, body: { agents: [] } } };
}
return {
handled: true,
response: {
Expand Down