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
1 change: 1 addition & 0 deletions examples/app-showcase/src/apps/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const ShowcaseApp = App.create({
{ id: 'nav_projects', type: 'object', objectName: 'showcase_project', label: 'Projects', icon: 'folder-kanban' },
{ id: 'nav_tasks', type: 'object', objectName: 'showcase_task', label: 'Tasks', icon: 'check-square' },
{ id: 'nav_accounts', type: 'object', objectName: 'showcase_account', label: 'Accounts', icon: 'building' },
{ id: 'nav_invoices', type: 'object', objectName: 'showcase_invoice', label: 'Invoices', icon: 'receipt' },
{ id: 'nav_teams', type: 'object', objectName: 'showcase_team', label: 'Teams', icon: 'users' },
{ id: 'nav_categories', type: 'object', objectName: 'showcase_category', label: 'Categories', icon: 'list-tree' },
{ id: 'nav_field_zoo', type: 'object', objectName: 'showcase_field_zoo', label: 'Field Zoo', icon: 'shapes' },
Expand Down
1 change: 1 addition & 0 deletions examples/app-showcase/src/objects/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export { Project } from './project.object.js';
export { Task } from './task.object.js';
export { Category } from './category.object.js';
export { Team, ProjectMembership } from './team.object.js';
export { Invoice, InvoiceLine } from './invoice.object.js';
export { FieldZoo } from './field-zoo.object.js';
66 changes: 66 additions & 0 deletions examples/app-showcase/src/objects/invoice.object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

import { ObjectSchema, Field } from '@objectstack/spec/data';

/**
* Invoice + Invoice Line β€” the canonical master-detail "header + line items"
* shape. Unlike project↔task (a task is added to a project over time), an
* invoice is meaningless without its lines: you enter the header AND its lines
* together, in one atomic transaction. So `invoice_line.invoice` declares
* `inlineEdit: 'grid'` β€” every standard New/Edit Invoice form renders an
* editable line-item grid, and the invoice `total` rolls the line amounts up
* server-side. This is where inline master-detail entry belongs.
*/
export const Invoice = ObjectSchema.create({
name: 'showcase_invoice',
label: 'Invoice',
pluralLabel: 'Invoices',
icon: 'receipt',
description: 'A customer invoice entered together with its line items.',

fields: {
name: Field.text({ label: 'Invoice Number', required: true, searchable: true, maxLength: 60 }),
account: Field.lookup('showcase_account', { label: 'Account', required: true }),
status: Field.select({
label: 'Status',
required: true,
options: [
{ label: 'Draft', value: 'draft', default: true, color: '#94A3B8' },
{ label: 'Sent', value: 'sent', color: '#3B82F6' },
{ label: 'Paid', value: 'paid', color: '#10B981' },
{ label: 'Void', value: 'void', color: '#EF4444' },
],
}),
issued_on: Field.date({ label: 'Issued On' }),
// Roll-up: recomputed server-side as line items are inserted/updated/deleted
// (child FK auto-detected: showcase_invoice_line.invoice).
total: Field.summary({
label: 'Total',
summaryOperations: { object: 'showcase_invoice_line', field: 'amount', function: 'sum' },
}),
},
});

/** Invoice line item β€” owned by its invoice, entered inline in the grid. */
export const InvoiceLine = ObjectSchema.create({
name: 'showcase_invoice_line',
label: 'Invoice Line',
pluralLabel: 'Invoice Lines',
icon: 'list',
description: 'A single billable line on an invoice.',

fields: {
invoice: Field.masterDetail('showcase_invoice', {
label: 'Invoice',
required: true,
deleteBehavior: 'cascade',
// Thin, high-volume line items β†’ the editable grid form factor.
inlineEdit: 'grid',
inlineTitle: 'Line Items',
}),
product: Field.text({ label: 'Product', required: true, maxLength: 200 }),
quantity: Field.number({ label: 'Qty', required: true, min: 0, defaultValue: 1 }),
unit_price: Field.currency({ label: 'Unit Price', scale: 2, min: 0 }),
amount: Field.currency({ label: 'Amount', scale: 2, min: 0 }),
},
});
18 changes: 6 additions & 12 deletions examples/app-showcase/src/objects/task.object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,15 @@ export const Task = ObjectSchema.create({

fields: {
title: Field.text({ label: 'Title', required: true, searchable: true, maxLength: 200 }),
// `inlineEdit` declares (in the data model) that tasks are entered inline
// within their project's form β€” so the standard New/Edit Project form
// auto-renders an atomic Tasks subtable, with no form view config and no
// bespoke page. `relatedList*` is the read-side mirror: the Project's
// record DETAIL page auto-renders a Tasks related list, with a focused
// column set β€” again, derived from the relationship, no page config.
// NOTE: no `inlineEdit` here. A task is added to a project over time, not
// entered together with it β€” so "New Project" must NOT force a Tasks
// subtable (that felt heavy/odd). Tasks are added later from the Project
// DETAIL page's Tasks related list (`relatedList*` below β€” the read side).
// Inline master-detail entry is reserved for true header+line shapes like
// Invoice + Invoice Line (see invoice.object.ts).
project: Field.masterDetail('showcase_project', {
label: 'Project',
required: true,
// Pin the editable-grid form factor (fast bulk line-item entry, with the
// column chooser + per-row expand). Left at `true`, the smart default
// would pick `form` for this fat child β€” the right call for many apps;
// here we keep the grid demo. Use `'form'` to force the per-row form.
inlineEdit: 'grid',
inlineTitle: 'Tasks',
relatedListTitle: 'Tasks',
relatedListColumns: ['title', 'status', 'priority', 'assignee', 'due_date'],
}),
Expand Down
5 changes: 5 additions & 0 deletions examples/app-showcase/src/pages/project-workspace.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ export const ProjectWorkspacePage: Page = {
// columns are auto-derived from the child object's metadata β€” no
// hand-authored columns block. Add `columns`/`relationshipField`
// here only to override the derived defaults.
// `showcase_task` has rich fields (notes/location/cover), so the
// relationship's smart default resolves to the per-row `form`
// factor: a read-only Tasks list with an "Add task" button that
// opens the child's full form inline. (Thin children like invoice
// lines get the editable `grid` instead β€” see invoice.object.ts.)
details: [
{ title: 'Tasks', childObject: 'showcase_task', addLabel: 'Add task' },
],
Expand Down