@svgrid/enterprise 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -81
- package/dist/cdn/svgrid-enterprise.svelte-external.js +22479 -10466
- package/dist/node/studio.js +11590 -2907
- package/package.json +31 -10
- package/src/SvAlertRuleEditor.svelte +294 -0
- package/src/SvAlertsManager.svelte +210 -0
- package/src/SvAlertsPanel.svelte +129 -0
- package/src/SvBoard.svelte +3 -1
- package/src/SvExpressionEditor.svelte +341 -0
- package/src/SvGridAlerts.dom.test.ts +113 -0
- package/src/SvGridAlerts.svelte +265 -0
- package/src/SvGridBoard.svelte +2355 -0
- package/src/SvGridEditPanel.svelte +849 -794
- package/src/SvGridScheduler.svelte +5334 -4410
- package/src/SvPivotDesigner.svelte +3 -3
- package/src/SvRecordDetail.svelte +6 -2
- package/src/SvSchedule.svelte +3 -1
- package/src/{ai-export-pdf.test.ts → ai-export-pdf.dom.test.ts} +4 -1
- package/src/{ai-export-xlsx.test.ts → ai-export-xlsx.dom.test.ts} +4 -1
- package/src/{ai-export.test.ts → ai-export.dom.test.ts} +5 -1
- package/src/alerts/alert-engine-attach.ts +165 -0
- package/src/alerts/alert-engine.test.ts +135 -0
- package/src/alerts/alert-engine.ts +260 -0
- package/src/alerts/alert-formats.test.ts +87 -0
- package/src/alerts/alert-formats.ts +77 -0
- package/src/alerts/alert-observer.test.ts +189 -0
- package/src/alerts/alert-observer.ts +208 -0
- package/src/alerts/alert-scheduler.ts +96 -0
- package/src/alerts/alert-storage.test.ts +54 -0
- package/src/alerts/alert-storage.ts +116 -0
- package/src/alerts/alert-store.svelte.ts +80 -0
- package/src/alerts/alert-types.ts +94 -0
- package/src/alerts.ts +28 -0
- package/src/board.dom.test.ts +941 -0
- package/src/board.ts +36 -0
- package/src/export.ts +13 -1
- package/src/expressions/evaluate.test.ts +111 -0
- package/src/expressions/evaluate.ts +228 -0
- package/src/expressions/expression-columns.ts +133 -0
- package/src/expressions/expression-types.ts +84 -0
- package/src/expressions/parse.test.ts +106 -0
- package/src/expressions/parse.ts +610 -0
- package/src/import.ts +2 -0
- package/src/index.ts +180 -46
- package/src/install.ts +12 -2
- package/src/pivot-enable.ts +44 -0
- package/src/scheduler-assignments.test.ts +97 -0
- package/src/scheduler-assignments.ts +134 -0
- package/src/scheduler-axis.test.ts +108 -0
- package/src/scheduler-axis.ts +238 -0
- package/src/scheduler-booking.test.ts +57 -0
- package/src/scheduler-booking.ts +63 -0
- package/src/scheduler-config.ts +179 -0
- package/src/scheduler-dependencies.test.ts +155 -0
- package/src/scheduler-dependencies.ts +223 -0
- package/src/scheduler-freebusy.test.ts +41 -0
- package/src/scheduler-freebusy.ts +36 -0
- package/src/scheduler-heatmap.test.ts +39 -0
- package/src/scheduler-heatmap.ts +55 -0
- package/src/scheduler-resource-tree.test.ts +84 -0
- package/src/scheduler-resource-tree.ts +107 -0
- package/src/scheduler-slots.test.ts +53 -0
- package/src/scheduler-slots.ts +94 -0
- package/src/scheduler-summary.test.ts +47 -0
- package/src/scheduler-summary.ts +81 -0
- package/src/sources/index.ts +1 -1
- package/src/sources/introspect-supabase.test.ts +13 -1
- package/src/sources/introspect-supabase.ts +20 -0
- package/src/studio/copilot-core.test.ts +45 -0
- package/src/studio/copilot-core.ts +65 -0
- package/src/studio/deploy-cli.test.ts +56 -0
- package/src/studio/deploy-cli.ts +100 -0
- package/src/studio/emit-project.test.ts +503 -18
- package/src/studio/emit-project.ts +829 -111
- package/src/studio/emit-schema.test.ts +17 -0
- package/src/studio/emit-schema.ts +166 -35
- package/src/studio/index.ts +25 -1
- package/src/studio/introspect-openapi.test.ts +84 -0
- package/src/studio/introspect-openapi.ts +252 -0
- package/src/studio/project.test.ts +57 -0
- package/src/studio/project.ts +193 -6
- package/src/studio/samples/crm.ts +282 -258
- package/src/studio/samples/fleet.ts +213 -186
- package/src/studio/samples/insurance.ts +223 -195
- package/src/studio/samples/inventory.ts +213 -182
- package/src/studio/samples/live-data.test.ts +99 -98
- package/src/studio/samples/live-data.ts +8 -10
- package/src/studio/samples/projects.ts +212 -190
- package/src/studio/samples/samples.test.ts +52 -0
- package/src/studio/samples/shared.ts +26 -6
- package/src/studio/samples/starter.ts +251 -0
- package/src/studio/samples/support.ts +209 -184
- package/src/studio/ui-components-surface.test.ts +185 -0
- package/src/studio/ui-components.generated.ts +5617 -0
- package/src/studio/ui-components.ts +641 -472
- package/src/sveltekit/sql-source.test.ts +13 -0
- package/src/sveltekit/sql-source.ts +11 -6
- package/src/upgrade-prompt.ts +2 -2
- package/src/watermark.ts +2 -2
- package/src/ai.test.ts +0 -522
- package/src/ai.ts +0 -1388
|
@@ -1,190 +1,212 @@
|
|
|
1
|
-
import type { EntitySchema } from '../../schema.js'
|
|
2
|
-
import type { FormatRule } from '../project.js'
|
|
3
|
-
import { screen, formScreen, boardScreen, schedulerScreen, detailScreen, project, dashScreen, statusPills, pad, ids, type SampleApp } from './shared.js'
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
{ field: '
|
|
20
|
-
{ field: '
|
|
21
|
-
{ field: '
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
{ field: '
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
{ field: '
|
|
43
|
-
{ field: '
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
{ field: '
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
{ field: '
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
{ value: '
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
{ field: '
|
|
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
|
-
{ id: '
|
|
101
|
-
{ id: '
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
{ id: '
|
|
107
|
-
{ id: '
|
|
108
|
-
{ id: '
|
|
109
|
-
{ id: '
|
|
110
|
-
{ id: '
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
{
|
|
153
|
-
]
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
{
|
|
168
|
-
{
|
|
169
|
-
{
|
|
170
|
-
{
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
{
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
]),
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
1
|
+
import type { EntitySchema } from '../../schema.js'
|
|
2
|
+
import type { FormatRule } from '../project.js'
|
|
3
|
+
import { screen, formScreen, boardScreen, schedulerScreen, detailScreen, project, dashScreen, statusPills, pad, ids, type SampleApp } from './shared.js'
|
|
4
|
+
|
|
5
|
+
// Jira-style chrome: a dark navy issue-tracker sidebar with muted labels and a
|
|
6
|
+
// blue active row (accent bar + tinted fill), the board front and centre.
|
|
7
|
+
const JIRA_CSS = `.sv-app.theme-jira .sv-app__side { background: #1d2125; border-right: 1px solid #2c333a; }
|
|
8
|
+
.sv-app.theme-jira .sv-app__brandtext { color: #fff; font-weight: 700; }
|
|
9
|
+
.sv-app.theme-jira .sv-app__link { color: #c7d1db; border-radius: 4px; padding: 7px 12px; }
|
|
10
|
+
.sv-app.theme-jira .sv-app__link:hover { background: rgba(255, 255, 255, 0.08); color: #fff; }
|
|
11
|
+
.sv-app.theme-jira .sv-app__link.is-active { background: color-mix(in srgb, #0c66e4 30%, #1d2125); color: #fff; box-shadow: inset 3px 0 0 #4c9aff; }
|
|
12
|
+
.sv-app.theme-jira .sv-app__collapse, .sv-app.theme-jira .sv-app__foot { color: #8c9bab; }`
|
|
13
|
+
|
|
14
|
+
const projectsEntity: EntitySchema = {
|
|
15
|
+
name: 'projects',
|
|
16
|
+
label: 'Project',
|
|
17
|
+
idField: 'id',
|
|
18
|
+
fields: [
|
|
19
|
+
{ field: 'id', type: 'text', primaryKey: true, readonly: true },
|
|
20
|
+
{ field: 'name', type: 'text', required: true, minLength: 2 },
|
|
21
|
+
{ field: 'status', type: 'enum', required: true, defaultValue: 'planning', options: [
|
|
22
|
+
{ value: 'planning', label: 'Planning', color: '#94a3b8' },
|
|
23
|
+
{ value: 'active', label: 'Active', color: '#6366f1' },
|
|
24
|
+
{ value: 'on_hold', label: 'On hold', color: '#f59e0b' },
|
|
25
|
+
{ value: 'done', label: 'Done', color: '#10b981' },
|
|
26
|
+
] },
|
|
27
|
+
{ field: 'owner', type: 'text', label: 'Owner' },
|
|
28
|
+
{ field: 'budget', type: 'number', label: 'Budget ($)', min: 0, input: { prefix: '$', help: 'Approved budget' } },
|
|
29
|
+
{ field: 'progress', type: 'number', label: 'Progress (%)', min: 0, max: 100, defaultValue: 0, input: { editorType: 'slider', help: 'Overall completion' } },
|
|
30
|
+
{ field: 'color', type: 'text', label: 'Label color', defaultValue: '#6366f1', input: { editorType: 'color', help: 'Board / calendar color' } },
|
|
31
|
+
{ field: 'tags', type: 'text', label: 'Tags', input: { editorType: 'chips', help: 'Themes / labels' } },
|
|
32
|
+
{ field: 'dueDate', type: 'date', label: 'Due date', required: true },
|
|
33
|
+
],
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const members: EntitySchema = {
|
|
37
|
+
name: 'members',
|
|
38
|
+
label: 'Member',
|
|
39
|
+
idField: 'id',
|
|
40
|
+
fields: [
|
|
41
|
+
{ field: 'id', type: 'text', primaryKey: true, readonly: true },
|
|
42
|
+
{ field: 'name', type: 'text', required: true, minLength: 2 },
|
|
43
|
+
{ field: 'email', type: 'text', format: 'email', required: true },
|
|
44
|
+
{ field: 'phone', type: 'text', label: 'Phone', input: { editorType: 'phone', help: 'Direct line' } },
|
|
45
|
+
{ field: 'role', type: 'enum', required: true, defaultValue: 'dev', options: [
|
|
46
|
+
{ value: 'dev', label: 'Engineer', color: '#6366f1' },
|
|
47
|
+
{ value: 'design', label: 'Designer', color: '#ec4899' },
|
|
48
|
+
{ value: 'pm', label: 'PM', color: '#f59e0b' },
|
|
49
|
+
{ value: 'qa', label: 'QA', color: '#10b981' },
|
|
50
|
+
] },
|
|
51
|
+
{ field: 'skills', type: 'text', label: 'Skills', input: { editorType: 'chips', help: 'Tech / expertise' } },
|
|
52
|
+
{ field: 'rating', type: 'number', label: 'Performance', min: 1, max: 5, defaultValue: 3, input: { editorType: 'rating', help: '1-5 review score' } },
|
|
53
|
+
],
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const tasks: EntitySchema = {
|
|
57
|
+
name: 'tasks',
|
|
58
|
+
label: 'Task',
|
|
59
|
+
idField: 'id',
|
|
60
|
+
fields: [
|
|
61
|
+
{ field: 'id', type: 'text', primaryKey: true, readonly: true },
|
|
62
|
+
{ field: 'title', type: 'text', required: true, minLength: 2 },
|
|
63
|
+
{ field: 'projectId', type: 'relation', label: 'Project', relation: { entity: 'projects', foreignKey: 'projectId', labelField: 'name' } },
|
|
64
|
+
{ field: 'assigneeId', type: 'relation', label: 'Assignee', relation: { entity: 'members', foreignKey: 'assigneeId', labelField: 'name' } },
|
|
65
|
+
{ field: 'priority', type: 'enum', required: true, defaultValue: 'medium', options: [
|
|
66
|
+
{ value: 'low', label: 'Low', color: '#94a3b8' },
|
|
67
|
+
{ value: 'medium', label: 'Medium', color: '#6366f1' },
|
|
68
|
+
{ value: 'high', label: 'High', color: '#f59e0b' },
|
|
69
|
+
{ value: 'urgent', label: 'Urgent', color: '#ef4444' },
|
|
70
|
+
] },
|
|
71
|
+
{ field: 'status', type: 'enum', required: true, defaultValue: 'todo', options: [
|
|
72
|
+
{ value: 'todo', label: 'To do', color: '#94a3b8' },
|
|
73
|
+
{ value: 'doing', label: 'In progress', color: '#6366f1' },
|
|
74
|
+
{ value: 'review', label: 'In review', color: '#f59e0b' },
|
|
75
|
+
{ value: 'done', label: 'Done', color: '#10b981' },
|
|
76
|
+
] },
|
|
77
|
+
{ field: 'estimate', type: 'number', label: 'Estimate (h)', min: 0, defaultValue: 0, input: { suffix: 'h', help: 'Estimated hours' } },
|
|
78
|
+
{ field: 'spent', type: 'number', label: 'Spent (h)', min: 0, defaultValue: 0, input: { suffix: 'h', help: 'Hours logged so far' } },
|
|
79
|
+
// Derived: hours left on the task (display-only, never stored).
|
|
80
|
+
{ field: 'remaining', type: 'number', label: 'Remaining (h)', readonly: true, formula: 'estimate - spent' },
|
|
81
|
+
{ field: 'progress', type: 'number', label: 'Progress (%)', min: 0, max: 100, defaultValue: 0, input: { editorType: 'slider', help: 'Task completion' } },
|
|
82
|
+
{ field: 'startedAt', type: 'datetime', label: 'Started', input: { editorType: 'datetime', help: 'When work began' } },
|
|
83
|
+
{ field: 'dueDate', type: 'date', label: 'Due' },
|
|
84
|
+
],
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Task grid formatting: status + priority pills, plus numeric thresholds -
|
|
88
|
+
// over-budget tasks (spent past estimate) red + bold, nearly-done tasks green,
|
|
89
|
+
// barely-started tasks red - data-driven color, not just status pills.
|
|
90
|
+
const taskFormats: FormatRule[] = [
|
|
91
|
+
...statusPills(tasks, 'status'),
|
|
92
|
+
...statusPills(tasks, 'priority'),
|
|
93
|
+
{ field: 'spent', op: 'gt', value: 16, color: '#dc2626', bold: true },
|
|
94
|
+
{ field: 'progress', op: 'gte', value: 80, color: '#16a34a', bold: true },
|
|
95
|
+
{ field: 'progress', op: 'lt', value: 25, color: '#dc2626' },
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
const seed = {
|
|
99
|
+
projects: [
|
|
100
|
+
{ id: 'pr1', name: 'Website redesign', status: 'active', owner: 'Sam Rivera', budget: 48000, progress: 65, color: '#6366f1', tags: ['Marketing', 'Design'], dueDate: '2026-09-01' },
|
|
101
|
+
{ id: 'pr2', name: 'Mobile app v2', status: 'planning', owner: 'Jamie Chen', budget: 120000, progress: 15, color: '#10b981', tags: ['Mobile', 'Growth'], dueDate: '2026-11-15' },
|
|
102
|
+
{ id: 'pr3', name: 'Billing migration', status: 'on_hold', owner: 'Priya Patel', budget: 32000, progress: 40, color: '#f59e0b', tags: ['Infra', 'Finance'], dueDate: '2026-08-20' },
|
|
103
|
+
{ id: 'pr4', name: 'Onboarding revamp', status: 'done', owner: 'Sam Rivera', budget: 18000, progress: 100, color: '#ef4444', tags: ['Product'], dueDate: '2026-06-10' },
|
|
104
|
+
],
|
|
105
|
+
members: [
|
|
106
|
+
{ id: 'm1', name: 'Ada Lovelace', email: 'ada@team.dev', phone: '+1 (415) 555-0101', role: 'dev', skills: ['TypeScript', 'Svelte'], rating: 5 },
|
|
107
|
+
{ id: 'm2', name: 'Grace Hopper', email: 'grace@team.dev', phone: '+1 (415) 555-0102', role: 'dev', skills: ['Go', 'SQL', 'Docker'], rating: 4 },
|
|
108
|
+
{ id: 'm3', name: 'Margaret Hamilton', email: 'mh@team.dev', phone: '+1 (415) 555-0103', role: 'pm', skills: ['Product', 'Leadership'], rating: 5 },
|
|
109
|
+
{ id: 'm4', name: 'Radia Perlman', email: 'radia@team.dev', phone: '+1 (415) 555-0104', role: 'design', skills: ['Figma', 'UX'], rating: 4 },
|
|
110
|
+
{ id: 'm5', name: 'Katherine Johnson', email: 'kj@team.dev', phone: '+1 (415) 555-0105', role: 'qa', skills: ['Python', 'GraphQL'], rating: 3 },
|
|
111
|
+
],
|
|
112
|
+
tasks: [
|
|
113
|
+
{ id: 't1', title: 'Design new homepage', projectId: 'pr1', assigneeId: 'm4', priority: 'high', status: 'doing', estimate: 16, spent: 6, progress: 40, startedAt: '2026-07-02T09:15:00Z', dueDate: '2026-07-20' },
|
|
114
|
+
{ id: 't2', title: 'Set up CI pipeline', projectId: 'pr1', assigneeId: 'm1', priority: 'medium', status: 'done', estimate: 8, spent: 8, progress: 100, startedAt: '2026-06-24T10:00:00Z', dueDate: '2026-07-05' },
|
|
115
|
+
{ id: 't3', title: 'API rate limiting', projectId: 'pr2', assigneeId: 'm2', priority: 'urgent', status: 'todo', estimate: 12, spent: 0, progress: 0, startedAt: '2026-07-18T08:30:00Z', dueDate: '2026-07-25' },
|
|
116
|
+
{ id: 't4', title: 'Migrate invoices', projectId: 'pr3', assigneeId: 'm1', priority: 'high', status: 'review', estimate: 24, spent: 18, progress: 80, startedAt: '2026-07-06T13:45:00Z', dueDate: '2026-08-01' },
|
|
117
|
+
{ id: 't5', title: 'QA onboarding flow', projectId: 'pr4', assigneeId: 'm5', priority: 'medium', status: 'done', estimate: 6, spent: 6, progress: 100, startedAt: '2026-06-02T11:20:00Z', dueDate: '2026-06-08' },
|
|
118
|
+
{ id: 't6', title: 'Push notifications', projectId: 'pr2', assigneeId: 'm2', priority: 'low', status: 'todo', estimate: 10, spent: 0, progress: 0, startedAt: '2026-07-15T09:00:00Z', dueDate: '2026-08-15' },
|
|
119
|
+
{ id: 't7', title: 'Accessibility audit', projectId: 'pr1', assigneeId: 'm5', priority: 'high', status: 'doing', estimate: 14, spent: 5, progress: 35, startedAt: '2026-07-10T14:00:00Z', dueDate: '2026-07-30' },
|
|
120
|
+
{ id: 't8', title: 'Pricing page copy', projectId: 'pr1', assigneeId: 'm3', priority: 'low', status: 'review', estimate: 4, spent: 3, progress: 75, startedAt: '2026-07-11T15:30:00Z', dueDate: '2026-07-18' },
|
|
121
|
+
],
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const projectTracker: SampleApp = {
|
|
125
|
+
id: 'projects',
|
|
126
|
+
name: 'Project tracker',
|
|
127
|
+
description: 'Projects, tasks and a team roster - a Jira-style issue tracker behind a sign-in, with portfolio + task-board dashboards (budget KPI sparkline + target, progress gauge, status/priority breakdowns with pie + area trend, status x priority pivot, tabbed views), filtered status-pill grids with row actions, master/detail, and rich edit forms (progress slider, color labels, tag chips, phone, rating, datetime).',
|
|
128
|
+
emoji: '\u{1F4CB}',
|
|
129
|
+
accent: '#0c66e4',
|
|
130
|
+
build: () => {
|
|
131
|
+
const projectRows = pad(projectsEntity, seed.projects, 10)
|
|
132
|
+
const projectPool = ids(projectRows)
|
|
133
|
+
const memberRows = pad(members, seed.members, 12)
|
|
134
|
+
const memberPool = ids(memberRows)
|
|
135
|
+
const taskRows = pad(tasks, seed.tasks, 60, { projectId: projectPool, assigneeId: memberPool })
|
|
136
|
+
return project({
|
|
137
|
+
title: 'Project Tracker',
|
|
138
|
+
brand: 'Tracker',
|
|
139
|
+
accent: '#0c66e4',
|
|
140
|
+
preset: 'linear',
|
|
141
|
+
footer: '',
|
|
142
|
+
// Jira-style issue tracker: dark navy rail, board-forward.
|
|
143
|
+
appClass: 'theme-jira',
|
|
144
|
+
customCss: JIRA_CSS,
|
|
145
|
+
// Sign-in with two roles: developers move issues but cannot delete them.
|
|
146
|
+
auth: { enabled: true, protect: true },
|
|
147
|
+
access: {
|
|
148
|
+
enabled: true,
|
|
149
|
+
defaultRole: 'developer',
|
|
150
|
+
roles: [
|
|
151
|
+
{ role: 'admin', screens: '*', actions: '*' },
|
|
152
|
+
{ role: 'developer', screens: '*', actions: ['create', 'update'] },
|
|
153
|
+
],
|
|
154
|
+
},
|
|
155
|
+
entities: [projectsEntity, members, tasks],
|
|
156
|
+
seed: { projects: projectRows, members: memberRows, tasks: taskRows },
|
|
157
|
+
screens: [
|
|
158
|
+
// Portfolio dashboard: budget KPIs (a total-budget card with a sparkline over
|
|
159
|
+
// due dates + a $400k target), an average-progress gauge (0-100), status
|
|
160
|
+
// breakdowns (bar + pie), a budget-by-owner x status pivot, a tabbed view, and
|
|
161
|
+
// a status-pill project grid with inline edit actions.
|
|
162
|
+
dashScreen(projectsEntity, { id: 'portfolio', title: 'Portfolio', order: 0 }, [
|
|
163
|
+
{ kpi: 'Total budget', measure: 'budget', reduce: 'sum', format: 'currency', trendField: 'dueDate', trendReduce: 'sum', target: 400000, span: 1 },
|
|
164
|
+
{ kpi: 'Projects', reduce: 'count', span: 1 },
|
|
165
|
+
{ kpi: 'Largest budget', measure: 'budget', reduce: 'max', format: 'currency', span: 1 },
|
|
166
|
+
{ gauge: 'Avg progress', measure: 'progress', reduce: 'avg', min: 0, max: 100, unit: '%', span: 1 },
|
|
167
|
+
{ chart: 'status', measure: 'budget', reduce: 'sum', type: 'bar', span: 2 },
|
|
168
|
+
{ chart: 'status', reduce: 'count', type: 'pie', span: 1 },
|
|
169
|
+
{ pivot: { rows: ['owner'], cols: ['status'], measure: 'budget', aggregate: 'sum' }, span: 3 },
|
|
170
|
+
{ tabs: [
|
|
171
|
+
{ label: 'Budget by status', tiles: [{ chart: 'status', measure: 'budget', reduce: 'sum', type: 'bar' }] },
|
|
172
|
+
{ label: 'Projects by status', tiles: [{ chart: 'status', reduce: 'count', type: 'pie' }] },
|
|
173
|
+
], span: 3 },
|
|
174
|
+
{ grid: true, format: [...statusPills(projectsEntity, 'status'), { field: 'progress', op: 'gte', value: 100, color: '#16a34a', bold: true }], summaries: true, rowActions: [{ kind: 'edit' }], span: 3 },
|
|
175
|
+
]),
|
|
176
|
+
// Task board: task KPIs (an estimate card trending over due dates), a completion
|
|
177
|
+
// gauge, status + priority breakdowns (bar + pie), a spent-over-time area trend,
|
|
178
|
+
// an estimate pivot by status (rows) x priority (columns), a tabbed view, a
|
|
179
|
+
// filter, and a task grid with status + priority pills and inline edit actions.
|
|
180
|
+
// Kanban board: tasks as cards in status columns (drag to change status).
|
|
181
|
+
boardScreen(tasks, { id: 'board', title: 'Kanban', order: 1 }, { groupBy: 'status', titleField: 'title', badgeField: 'estimate', subtitleField: 'priority', filter: ['priority', 'assigneeId'] }),
|
|
182
|
+
// Project detail: header (status pill) + budget / progress tiles with a
|
|
183
|
+
// timeline of the project's tasks (by due date, colored by status).
|
|
184
|
+
detailScreen(projectsEntity, { id: 'project-detail', title: 'Project detail', order: 2 }, {
|
|
185
|
+
titleField: 'name', subtitleField: 'owner', statusField: 'status', metricFields: ['budget', 'progress'],
|
|
186
|
+
related: [{ entity: 'tasks', foreignKey: 'projectId', label: 'Tasks', titleField: 'title', subtitleField: 'priority', dateField: 'dueDate', statusField: 'status' }],
|
|
187
|
+
}),
|
|
188
|
+
dashScreen(tasks, { id: 'tasks-board', title: 'Task board', order: 3 }, [
|
|
189
|
+
{ kpi: 'Tasks', reduce: 'count', span: 1 },
|
|
190
|
+
{ kpi: 'Logged hours', measure: 'spent', reduce: 'sum', format: 'compact', span: 1 },
|
|
191
|
+
{ kpi: 'Estimated hours', measure: 'estimate', reduce: 'sum', format: 'compact', trendField: 'dueDate', trendReduce: 'sum', span: 1 },
|
|
192
|
+
{ gauge: 'Avg completion', measure: 'progress', reduce: 'avg', min: 0, max: 100, unit: '%', span: 1 },
|
|
193
|
+
{ chart: 'status', reduce: 'count', type: 'bar', span: 2 },
|
|
194
|
+
{ chart: 'priority', reduce: 'count', type: 'pie', span: 1 },
|
|
195
|
+
{ chart: 'dueDate', measure: 'spent', reduce: 'sum', type: 'area', span: 3 },
|
|
196
|
+
{ pivot: { rows: ['status'], cols: ['priority'], measure: 'estimate', aggregate: 'sum' }, span: 3 },
|
|
197
|
+
{ tabs: [
|
|
198
|
+
{ label: 'Hours by status', tiles: [{ chart: 'status', measure: 'spent', reduce: 'sum', type: 'bar' }] },
|
|
199
|
+
{ label: 'Tasks by priority', tiles: [{ chart: 'priority', reduce: 'count', type: 'bar' }] },
|
|
200
|
+
], span: 3 },
|
|
201
|
+
{ filter: ['status', 'priority', 'assigneeId'], span: 3 },
|
|
202
|
+
{ grid: true, format: taskFormats, summaries: true, rowActions: [{ kind: 'edit' }], span: 3 },
|
|
203
|
+
]),
|
|
204
|
+
screen(projectsEntity, 'master-detail', { id: 'projects', title: 'Projects', order: 4, child: tasks, foreignKey: 'projectId', linkScreen: 'project-detail' }),
|
|
205
|
+
formScreen(tasks, { id: 'tasks', title: 'Tasks', order: 5 }, undefined, { format: taskFormats, summaries: true, rowActions: [{ kind: 'edit' }] }, ['status', 'priority', 'assigneeId']),
|
|
206
|
+
formScreen(members, { id: 'members', title: 'Members', order: 6 }, undefined, { format: statusPills(members, 'role'), summaries: true }, ['role']),
|
|
207
|
+
// Schedule: tasks placed on a month calendar by due date.
|
|
208
|
+
schedulerScreen(tasks, { id: 'calendar', title: 'Schedule', order: 7 }, { startField: 'startedAt', endField: 'dueDate', titleField: 'title', colorField: 'priority', resourceField: 'assigneeId', initialView: 'timelineWeek' }),
|
|
209
|
+
],
|
|
210
|
+
})
|
|
211
|
+
},
|
|
212
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
2
|
import { compile } from 'svelte/compiler'
|
|
3
3
|
import { sampleApps, getSampleApp } from './index.js'
|
|
4
|
+
import { starterProject } from './starter.js'
|
|
4
5
|
import { validateProject, parseProject, serializeProject } from '../project.js'
|
|
5
6
|
import { emitStudioProject } from '../emit-project.js'
|
|
6
7
|
|
|
@@ -214,3 +215,54 @@ describe('sample apps', () => {
|
|
|
214
215
|
})
|
|
215
216
|
})
|
|
216
217
|
})
|
|
218
|
+
|
|
219
|
+
describe('starter project (fresh `svgrid-studio designer` session)', () => {
|
|
220
|
+
const project = starterProject()
|
|
221
|
+
|
|
222
|
+
it('is a valid, multi-page CRM (dashboard + CRUD + board + calendar + forecast) with seeded rows', () => {
|
|
223
|
+
expect(validateProject(project).filter((i) => i.level === 'error')).toHaveLength(0)
|
|
224
|
+
expect(project.entities.map((e) => e.name)).toEqual(['customers', 'deals'])
|
|
225
|
+
// Several pages, so the first-run app is a real app, not a lone table.
|
|
226
|
+
const ids = project.screens.map((s) => s.id)
|
|
227
|
+
expect(ids).toEqual(['overview', 'customers', 'pipeline', 'deals', 'calendar', 'forecast'])
|
|
228
|
+
// A range of block kinds across the app (the "wow" surface).
|
|
229
|
+
const kinds = new Set(project.screens.flatMap((s) => s.blocks.map((b) => b.config.kind)))
|
|
230
|
+
for (const k of ['kpi', 'chart', 'pivot', 'grid', 'board', 'calendar']) expect(kinds.has(k as never), `missing ${k}`).toBe(true)
|
|
231
|
+
for (const e of ['customers', 'deals']) {
|
|
232
|
+
const seed = (project.dataSources?.[e] as { seed?: unknown[] } | undefined)?.seed
|
|
233
|
+
expect(seed?.length ?? 0, `${e} seed`).toBeGreaterThanOrEqual(10)
|
|
234
|
+
}
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('the Customers page turns on the enterprise-grade grid surface (filter + export + pills + row actions)', () => {
|
|
238
|
+
const screen = project.screens.find((s) => s.id === 'customers')!
|
|
239
|
+
const grid = screen.blocks.find((b) => b.config.kind === 'grid')!.config as Record<string, unknown>
|
|
240
|
+
expect(screen.blocks.some((b) => b.config.kind === 'filter')).toBe(true)
|
|
241
|
+
expect(grid.filterable).toBe(true)
|
|
242
|
+
expect(grid.export).toEqual({ csv: true, json: true, copy: true })
|
|
243
|
+
expect(grid.rowSummaries).toBe(true)
|
|
244
|
+
expect((grid.rowActions as unknown[])?.length).toBeGreaterThan(0)
|
|
245
|
+
// status / industry / plan render as colored chips (editorType 'list' on the enum fields).
|
|
246
|
+
const cust = project.entities.find((e) => e.name === 'customers')!
|
|
247
|
+
for (const f of ['status', 'industry', 'plan']) {
|
|
248
|
+
expect(cust.fields.find((x) => x.field === f)?.input?.editorType, `${f} chip`).toBe('list')
|
|
249
|
+
}
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it('generates enterprise-wired, compiling code with a visible export toolbar', () => {
|
|
253
|
+
const files = emitStudioProject(project)
|
|
254
|
+
const customersPage = files.find((f) => f.path.endsWith('customers/+page.svelte'))!.contents
|
|
255
|
+
expect(customersPage).toContain('@svgrid/enterprise')
|
|
256
|
+
expect(customersPage).toContain('exportCsv') // export toolbar wired to the grid api
|
|
257
|
+
// Every generated screen compiles (dashboard, board, calendar, forecast, ...).
|
|
258
|
+
for (const f of files.filter((f) => f.path.endsWith('.svelte'))) {
|
|
259
|
+
expect(() => compile(f.contents, { filename: f.path, generate: 'client' }), f.path).not.toThrow()
|
|
260
|
+
}
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('round-trips through parse/serialize', () => {
|
|
264
|
+
const again = parseProject(serializeProject(project))
|
|
265
|
+
expect(again.entities.map((e) => e.name)).toEqual(project.entities.map((e) => e.name))
|
|
266
|
+
expect(again.dataSources).toEqual(project.dataSources)
|
|
267
|
+
})
|
|
268
|
+
})
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import type { EntitySchema } from '../../schema.js'
|
|
11
11
|
import type { ChartType } from '@svgrid/grid'
|
|
12
12
|
import { generateValue } from '../sample-data.js'
|
|
13
|
-
import { defaultBlockConfig, sanitizeProject, screenFromTemplate, buildDockLayout, type Block, type BlockConfig, type DetailRelated, type FormatRule, type GridConfig, type GridDensity, type KpiFormat, type Reduce, type RowAction, type RowLink, type Screen, type ScreenTemplate, type StudioProject, type EntityDataSource, type ShellStyle, type SchedulerViewConfig, type SchedulerViewMode } from '../project.js'
|
|
13
|
+
import { defaultBlockConfig, sanitizeProject, screenFromTemplate, buildDockLayout, type Block, type BlockConfig, type DetailRelated, type FormatRule, type GridConfig, type GridDensity, type KpiFormat, type Reduce, type RowAction, type RowLink, type Screen, type ScreenTemplate, type StudioProject, type EntityDataSource, type ShellStyle, type SchedulerViewConfig, type SchedulerViewMode, type AuthConfig, type AccessControl } from '../project.js'
|
|
14
14
|
|
|
15
15
|
type Row = Record<string, unknown>
|
|
16
16
|
|
|
@@ -111,9 +111,12 @@ function gridConfig(entity: EntitySchema, opts: GridOpts): BlockConfig {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
/** One dashboard tile -> a Block. Recurses for `tabs` (whose children must be
|
|
114
|
-
* display blocks: chart / kpi / gauge / pivot / tree).
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
* display blocks: chart / kpi / gauge / pivot / tree). `nextId` yields ids unique
|
|
115
|
+
* across the WHOLE screen (top-level tiles AND every nested tab child) - a per-level
|
|
116
|
+
* index would collide (top-level `blk-1` vs each tab's first child `blk-1`), which
|
|
117
|
+
* makes a keyed each throw `each_key_duplicate`. */
|
|
118
|
+
function tileBlock(entity: EntitySchema, t: Tile, nextId: () => string): Block {
|
|
119
|
+
const id = nextId()
|
|
117
120
|
if ('kpi' in t) {
|
|
118
121
|
const config: BlockConfig = {
|
|
119
122
|
kind: 'kpi', label: t.kpi, ...(t.measure ? { measure: t.measure } : {}), reduce: t.reduce,
|
|
@@ -138,7 +141,7 @@ function tileBlock(entity: EntitySchema, t: Tile, i: number): Block {
|
|
|
138
141
|
return { id, span: t.span ?? 3, config: { kind: 'filter', fields: t.filter } }
|
|
139
142
|
}
|
|
140
143
|
if ('tabs' in t) {
|
|
141
|
-
return { id, span: t.span ?? 3, config: { kind: 'tabs', tabs: t.tabs.map((tab) => ({ label: tab.label, blocks: tab.tiles.map((tt
|
|
144
|
+
return { id, span: t.span ?? 3, config: { kind: 'tabs', tabs: t.tabs.map((tab) => ({ label: tab.label, blocks: tab.tiles.map((tt) => tileBlock(entity, tt, nextId)) })) } }
|
|
142
145
|
}
|
|
143
146
|
return { id, span: t.span ?? 3, config: gridConfig(entity, t) }
|
|
144
147
|
}
|
|
@@ -150,7 +153,9 @@ export function dashScreen(
|
|
|
150
153
|
meta: { id: string; title: string; order: number },
|
|
151
154
|
tiles: Tile[],
|
|
152
155
|
): Screen {
|
|
153
|
-
|
|
156
|
+
let n = 0
|
|
157
|
+
const nextId = () => `blk-${++n}`
|
|
158
|
+
const blocks = tiles.map((t) => tileBlock(entity, t, nextId))
|
|
154
159
|
return { id: meta.id, entity: entity.name, title: meta.title, route: meta.id, blocks, nav: { show: true, label: meta.title, order: meta.order } }
|
|
155
160
|
}
|
|
156
161
|
|
|
@@ -324,6 +329,17 @@ export function project(opts: {
|
|
|
324
329
|
* what makes each app read as a distinct product (Salesforce vs Excel vs Linear). */
|
|
325
330
|
preset?: string
|
|
326
331
|
mode?: 'light' | 'dark'
|
|
332
|
+
/** Bespoke app chrome: raw CSS written to the generated app's `custom.css` (imported
|
|
333
|
+
* after `app.css`, so it overrides). Scope rules under `.sv-app.<appClass>` so an
|
|
334
|
+
* app can look like its real-world archetype, not a recolored sibling. */
|
|
335
|
+
customCss?: string
|
|
336
|
+
/** Extra class on the shell root (the hook `customCss` targets). */
|
|
337
|
+
appClass?: string
|
|
338
|
+
/** A sign-in starter: `{ enabled: true }` gives the generated app a `/login` page,
|
|
339
|
+
* a signed-cookie session, and route guards (dependency-free, no DB). */
|
|
340
|
+
auth?: AuthConfig
|
|
341
|
+
/** Role-based access. With `auth`, seeds one demo login per role. */
|
|
342
|
+
access?: AccessControl
|
|
327
343
|
entities: EntitySchema[]
|
|
328
344
|
screens: Screen[]
|
|
329
345
|
seed: Record<string, Record<string, unknown>[]>
|
|
@@ -340,7 +356,11 @@ export function project(opts: {
|
|
|
340
356
|
accent: opts.accent,
|
|
341
357
|
...(opts.preset ? { preset: opts.preset } : {}),
|
|
342
358
|
...(opts.mode ? { mode: opts.mode } : {}),
|
|
359
|
+
...(opts.customCss ? { customCss: opts.customCss } : {}),
|
|
360
|
+
...(opts.appClass ? { appClass: opts.appClass } : {}),
|
|
343
361
|
shell: { style: opts.navStyle ?? 'sidebar', brand: opts.brand, footer: opts.footer ?? '', navPosition: opts.navPosition ?? 'left' },
|
|
344
362
|
},
|
|
363
|
+
...(opts.auth?.enabled ? { auth: opts.auth } : {}),
|
|
364
|
+
...(opts.access?.enabled ? { access: opts.access } : {}),
|
|
345
365
|
})
|
|
346
366
|
}
|