-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathproject.object.ts
More file actions
128 lines (124 loc) · 5.29 KB
/
Copy pathproject.object.ts
File metadata and controls
128 lines (124 loc) · 5.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
import { ObjectSchema, Field } from '@objectstack/spec/data';
import { cel, P } from '@objectstack/spec';
/**
* Project — the realistic backbone object. Demonstrates lookup relations,
* a formula field, a select that drives Kanban grouping, date fields that
* drive Gantt/timeline views, validations, and a status state machine.
*/
export const Project = ObjectSchema.create({
name: 'showcase_project',
label: 'Project',
pluralLabel: 'Projects',
icon: 'folder-kanban',
description: 'A delivery project for an account.',
fields: {
name: Field.text({ label: 'Project Name', required: true, searchable: true, maxLength: 200 }),
// `relatedList*` is the read-side mirror of inline editing: the Account's
// record DETAIL page auto-renders a "Projects" related list — derived from
// this lookup relationship, with no page config. Title and columns are
// declared here on the relationship (where AI authors the model), not in a
// hand-built page.
account: Field.lookup('showcase_account', {
label: 'Account',
required: true,
relatedListTitle: 'Projects',
relatedListColumns: ['name', 'status', 'health', 'budget', 'end_date'],
}),
status: Field.select({
label: 'Status',
required: true,
options: [
{ label: 'Planned', value: 'planned', default: true, color: '#94A3B8' },
{ label: 'Active', value: 'active', color: '#3B82F6' },
{ label: 'On Hold', value: 'on_hold', color: '#F59E0B' },
{ label: 'Completed', value: 'completed', color: '#10B981' },
{ label: 'Cancelled', value: 'cancelled', color: '#EF4444' },
],
}),
health: Field.select({
label: 'Health',
options: [
{ label: 'Green', value: 'green', default: true, color: '#10B981' },
{ label: 'Yellow', value: 'yellow', color: '#F59E0B' },
{ label: 'Red', value: 'red', color: '#EF4444' },
],
}),
budget: Field.currency({ label: 'Budget', scale: 2, min: 0 }),
spent: Field.currency({ label: 'Spent', scale: 2, min: 0, defaultValue: 0 }),
budget_remaining: Field.formula({
label: 'Budget Remaining',
expression: cel`(record.budget == null ? 0 : record.budget) - (record.spent == null ? 0 : record.spent)`,
}),
start_date: Field.date({ label: 'Start Date' }),
end_date: Field.date({ label: 'Target End Date' }),
owner: Field.text({ label: 'Owner', maxLength: 200 }),
// Roll-up summaries — recomputed server-side whenever a child task is
// inserted / updated / deleted (FK auto-detected: showcase_task.project).
task_count: Field.summary({
label: 'Tasks',
summaryOperations: { object: 'showcase_task', field: 'estimate_hours', function: 'count' },
}),
total_estimate: Field.summary({
label: 'Total Estimate (h)',
summaryOperations: { object: 'showcase_task', field: 'estimate_hours', function: 'sum' },
}),
},
validations: [
{
type: 'cross_field' as const,
name: 'end_after_start',
label: 'End After Start',
description: 'Target end date must be on or after the start date.',
fields: ['start_date', 'end_date'],
condition: P`has(record.start_date) && has(record.end_date) && record.end_date < record.start_date`,
message: 'Target End Date must be on or after the Start Date.',
},
{
type: 'script' as const,
name: 'spent_within_budget',
label: 'Spend Within 120% of Budget',
description: 'Flag projects spending more than 120% of budget.',
condition: P`record.budget != null && record.spent != null && record.spent > record.budget * 1.2`,
message: 'Spend exceeds 120% of budget — escalate before continuing.',
severity: 'error' as const,
},
{
type: 'state_machine' as const,
name: 'project_status_flow',
label: 'Project Status Flow',
description: 'Projects progress through valid status transitions.',
field: 'status',
// State machines validate *transitions*, so they run on update only —
// an insert sets the initial state (constrained by the select options).
events: ['update'] as const,
message: 'Invalid project status transition.',
transitions: {
planned: ['active', 'cancelled'],
active: ['on_hold', 'completed', 'cancelled'],
on_hold: ['active', 'cancelled'],
completed: [],
cancelled: ['planned'],
},
},
{
// Advisory (severity: 'warning') state machine — demonstrates a soft
// transition guard: health should escalate/de-escalate one step at a
// time (green↔yellow↔red). A jump (e.g. green→red) is *flagged* (logged
// server-side) but NOT blocked, unlike the error-severity status flow.
type: 'state_machine' as const,
name: 'project_health_progression',
label: 'Project Health Progression (advisory)',
description: 'Health should change one step at a time; skips are warned, not blocked.',
field: 'health',
events: ['update'] as const,
severity: 'warning' as const,
message: 'Health changed by more than one step — confirm this is intentional.',
transitions: {
green: ['yellow'],
yellow: ['green', 'red'],
red: ['yellow'],
},
},
],
});