domma-cms 0.17.0 → 0.21.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/CLAUDE.md +39 -3
- package/admin/css/admin.css +1 -1
- package/admin/css/dashboard.css +1 -1
- package/admin/index.html +2 -2
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +4 -4
- package/admin/js/config/sidebar-config.js +1 -1
- package/admin/js/lib/card-builder.js +3 -3
- package/admin/js/lib/crud-tutorial.js +1 -0
- package/admin/js/lib/effects-builder.js +1 -1
- package/admin/js/lib/markdown-toolbar.js +6 -6
- package/admin/js/lib/project-context.js +1 -0
- package/admin/js/lib/sidebar-renderer.js +4 -0
- package/admin/js/templates/action-editor.html +7 -0
- package/admin/js/templates/block-editor.html +7 -0
- package/admin/js/templates/collection-editor.html +9 -0
- package/admin/js/templates/dashboard/cache.html +32 -0
- package/admin/js/templates/dashboard.html +4 -0
- package/admin/js/templates/form-editor.html +9 -0
- package/admin/js/templates/menu-editor.html +98 -0
- package/admin/js/templates/menu-locations.html +14 -0
- package/admin/js/templates/menus.html +14 -0
- package/admin/js/templates/page-editor.html +9 -2
- package/admin/js/templates/project-detail.html +50 -0
- package/admin/js/templates/project-editor.html +45 -0
- package/admin/js/templates/project-settings.html +60 -0
- package/admin/js/templates/projects.html +13 -0
- package/admin/js/templates/role-editor.html +11 -0
- package/admin/js/templates/settings.html +26 -0
- package/admin/js/templates/tutorials.html +335 -2
- package/admin/js/templates/view-editor.html +7 -0
- package/admin/js/views/action-editor.js +1 -1
- package/admin/js/views/actions-list.js +1 -1
- package/admin/js/views/block-editor-enhance.js +1 -1
- package/admin/js/views/block-editor.js +8 -8
- package/admin/js/views/blocks.js +2 -2
- package/admin/js/views/collection-editor.js +4 -4
- package/admin/js/views/collections.js +1 -1
- package/admin/js/views/dashboard/widgets/activity-feed.js +1 -1
- package/admin/js/views/dashboard/widgets/cache.js +1 -0
- package/admin/js/views/dashboard/widgets/journeys.js +1 -1
- package/admin/js/views/dashboard/widgets/spike-feed.js +1 -1
- package/admin/js/views/dashboard/widgets/top-pages.js +1 -1
- package/admin/js/views/dashboard.js +1 -1
- package/admin/js/views/form-editor.js +6 -6
- package/admin/js/views/forms.js +1 -1
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/menu-editor.js +19 -0
- package/admin/js/views/menu-locations.js +1 -0
- package/admin/js/views/menus.js +5 -0
- package/admin/js/views/page-editor.js +41 -36
- package/admin/js/views/pages.js +3 -3
- package/admin/js/views/project-detail.js +4 -0
- package/admin/js/views/project-editor.js +1 -0
- package/admin/js/views/project-settings.js +1 -0
- package/admin/js/views/projects.js +7 -0
- package/admin/js/views/role-editor.js +1 -1
- package/admin/js/views/roles.js +3 -3
- package/admin/js/views/settings.js +3 -3
- package/admin/js/views/tutorials.js +1 -1
- package/admin/js/views/user-editor.js +1 -1
- package/admin/js/views/users.js +3 -3
- package/admin/js/views/view-editor.js +1 -1
- package/admin/js/views/views-list.js +1 -1
- package/config/cache.json +4 -0
- package/config/cache.json.example +12 -0
- package/config/menu-locations.json +5 -0
- package/config/menus/admin-sidebar.json +185 -0
- package/config/menus/footer.json +33 -0
- package/config/menus/main.json +35 -0
- package/config/menus/sproj-1779696558011-menu.json +17 -0
- package/config/menus/sproj-1779696960337-menu.json +18 -0
- package/config/menus/sproj-1779696985353-menu.json +18 -0
- package/config/site.json +6 -22
- package/package.json +4 -3
- package/plugins/analytics/daily.json +3 -0
- package/plugins/analytics/journeys.json +8 -0
- package/plugins/analytics/lifetime.json +1 -1
- package/public/css/site.css +1 -1
- package/public/js/collection-browser.js +4 -0
- package/public/js/forms.js +1 -1
- package/public/js/site.js +1 -1
- package/server/config.js +12 -1
- package/server/middleware/auth.js +88 -22
- package/server/routes/api/actions.js +58 -5
- package/server/routes/api/auth.js +2 -2
- package/server/routes/api/blocks.js +18 -3
- package/server/routes/api/cache.js +57 -0
- package/server/routes/api/collections.js +201 -8
- package/server/routes/api/forms.js +266 -21
- package/server/routes/api/menu-locations.js +46 -0
- package/server/routes/api/menus.js +115 -0
- package/server/routes/api/navigation.js +2 -0
- package/server/routes/api/pages.js +1 -1
- package/server/routes/api/projects.js +107 -0
- package/server/routes/api/scaffold.js +86 -0
- package/server/routes/api/settings.js +3 -0
- package/server/routes/api/sidebar.js +23 -0
- package/server/routes/api/users.js +32 -7
- package/server/routes/api/views.js +10 -2
- package/server/routes/public.js +88 -7
- package/server/server.js +54 -3
- package/server/services/actions.js +137 -8
- package/server/services/adapters/FileAdapter.js +23 -8
- package/server/services/adapters/MongoAdapter.js +36 -18
- package/server/services/blocks.js +23 -8
- package/server/services/cache/drivers/MemoryDriver.js +118 -0
- package/server/services/cache/drivers/NoneDriver.js +12 -0
- package/server/services/cache/index.js +229 -0
- package/server/services/cache/lru.js +61 -0
- package/server/services/collections.js +102 -12
- package/server/services/content.js +25 -6
- package/server/services/filterEngine.js +281 -0
- package/server/services/forms.js +3 -0
- package/server/services/hooks.js +48 -0
- package/server/services/markdown.js +711 -124
- package/server/services/menus-migration.js +107 -0
- package/server/services/menus.js +422 -0
- package/server/services/permissionRegistry.js +26 -0
- package/server/services/plugins.js +9 -2
- package/server/services/presetCollections.js +22 -0
- package/server/services/projects.js +429 -0
- package/server/services/recipes/contact-list.json +78 -0
- package/server/services/recipes/onboarding.json +426 -0
- package/server/services/references.js +174 -0
- package/server/services/renderer.js +237 -40
- package/server/services/roles.js +6 -1
- package/server/services/rowAccess.js +86 -13
- package/server/services/scaffolder.js +465 -0
- package/server/services/sidebar-migration.js +117 -0
- package/server/services/sitemap.js +112 -0
- package/server/services/userRoles.js +86 -0
- package/server/services/users.js +23 -2
- package/server/services/views.js +19 -4
- package/server/templates/page.html +135 -130
- /package/config/{navigation.json → navigation.json.bak} +0 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
{
|
|
2
|
+
"slug": "onboarding",
|
|
3
|
+
"name": "Employee Onboarding",
|
|
4
|
+
"description": "Collects new-starter details and tracks them through a Pending \u2192 Reviewing \u2192 Approved/Rejected workflow. Includes a right-to-work file upload and HR review actions.",
|
|
5
|
+
"icon": "user-plus",
|
|
6
|
+
"roles": [
|
|
7
|
+
{
|
|
8
|
+
"name": "hr",
|
|
9
|
+
"label": "HR Reviewer",
|
|
10
|
+
"level": 2,
|
|
11
|
+
"permissions": [
|
|
12
|
+
"notifications"
|
|
13
|
+
],
|
|
14
|
+
"badgeClass": "badge-info"
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"options": [
|
|
18
|
+
{
|
|
19
|
+
"name": "namespace",
|
|
20
|
+
"label": "Namespace",
|
|
21
|
+
"default": "onboarding",
|
|
22
|
+
"hint": "Single name that cascades into every slug below. Change this once and the collection, form, and actions all rename in lockstep — or override the individual slugs below to break the chain."
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"name": "collectionSlug",
|
|
26
|
+
"label": "Collection slug",
|
|
27
|
+
"default": "{{namespace}}",
|
|
28
|
+
"hint": "Where submitted starter records are stored"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "formSlug",
|
|
32
|
+
"label": "Form slug",
|
|
33
|
+
"default": "{{namespace}}-form",
|
|
34
|
+
"hint": "Embedded via [form name=\"...\" /]"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "actionPrefix",
|
|
38
|
+
"label": "Action slug prefix",
|
|
39
|
+
"default": "{{namespace}}",
|
|
40
|
+
"hint": "Per-action slugs: <prefix>-review, <prefix>-approve, <prefix>-reject"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"name": "hrEmail",
|
|
44
|
+
"label": "HR email (optional)",
|
|
45
|
+
"default": "",
|
|
46
|
+
"hint": "If set, scaffolds an HR user account that can review and approve onboarding submissions. Leave blank to skip."
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"name": "hrPassword",
|
|
50
|
+
"label": "HR initial password (optional)",
|
|
51
|
+
"default": "",
|
|
52
|
+
"hint": "Required when HR email is set. The HR user can change this immediately via their profile after first login."
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
"collection": {
|
|
56
|
+
"title": "Onboarding",
|
|
57
|
+
"description": "New-starter onboarding submissions",
|
|
58
|
+
"fields": [
|
|
59
|
+
{
|
|
60
|
+
"name": "fullName",
|
|
61
|
+
"label": "Full name",
|
|
62
|
+
"type": "text",
|
|
63
|
+
"required": true
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"name": "email",
|
|
67
|
+
"label": "Work email",
|
|
68
|
+
"type": "email",
|
|
69
|
+
"required": true
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"name": "phone",
|
|
73
|
+
"label": "Phone number",
|
|
74
|
+
"type": "text"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"name": "startDate",
|
|
78
|
+
"label": "Start date",
|
|
79
|
+
"type": "date",
|
|
80
|
+
"required": true
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"name": "role",
|
|
84
|
+
"label": "Role / position",
|
|
85
|
+
"type": "text",
|
|
86
|
+
"required": true
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"name": "manager",
|
|
90
|
+
"label": "Manager",
|
|
91
|
+
"type": "text"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"name": "residencyStatus",
|
|
95
|
+
"label": "Right-to-work status",
|
|
96
|
+
"type": "multiselect",
|
|
97
|
+
"required": true,
|
|
98
|
+
"options": [
|
|
99
|
+
{
|
|
100
|
+
"value": "uk-national",
|
|
101
|
+
"label": "UK national"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"value": "settled-status",
|
|
105
|
+
"label": "Settled status"
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"value": "visa-required",
|
|
109
|
+
"label": "Visa required"
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"value": "other",
|
|
113
|
+
"label": "Other"
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"name": "rightToWorkDoc",
|
|
119
|
+
"label": "Right-to-work document",
|
|
120
|
+
"type": "file",
|
|
121
|
+
"required": false,
|
|
122
|
+
"file": {
|
|
123
|
+
"accept": "application/pdf,image/jpeg,image/png",
|
|
124
|
+
"maxSize": 5242880
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"name": "references",
|
|
129
|
+
"label": "Reference contacts",
|
|
130
|
+
"type": "textarea",
|
|
131
|
+
"required": false,
|
|
132
|
+
"placeholder": "Name, email, relationship \u2014 one per line"
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"name": "notes",
|
|
136
|
+
"label": "Notes for HR",
|
|
137
|
+
"type": "textarea"
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"name": "status",
|
|
141
|
+
"label": "Status",
|
|
142
|
+
"type": "select",
|
|
143
|
+
"required": false,
|
|
144
|
+
"options": [
|
|
145
|
+
{
|
|
146
|
+
"value": "pending",
|
|
147
|
+
"label": "Pending review"
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"value": "reviewing",
|
|
151
|
+
"label": "Reviewing"
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"value": "approved",
|
|
155
|
+
"label": "Approved"
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"value": "rejected",
|
|
159
|
+
"label": "Rejected"
|
|
160
|
+
}
|
|
161
|
+
]
|
|
162
|
+
}
|
|
163
|
+
],
|
|
164
|
+
"api": {
|
|
165
|
+
"read": {
|
|
166
|
+
"enabled": false
|
|
167
|
+
},
|
|
168
|
+
"create": {
|
|
169
|
+
"enabled": false
|
|
170
|
+
},
|
|
171
|
+
"update": {
|
|
172
|
+
"enabled": false
|
|
173
|
+
},
|
|
174
|
+
"delete": {
|
|
175
|
+
"enabled": false
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
"rowAccess": {
|
|
179
|
+
"update": {
|
|
180
|
+
"ownerField": "createdBy",
|
|
181
|
+
"fallbackRoles": [
|
|
182
|
+
"admin",
|
|
183
|
+
"super-admin"
|
|
184
|
+
]
|
|
185
|
+
},
|
|
186
|
+
"delete": {
|
|
187
|
+
"ownerField": "createdBy",
|
|
188
|
+
"fallbackRoles": [
|
|
189
|
+
"admin",
|
|
190
|
+
"super-admin"
|
|
191
|
+
]
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
"form": {
|
|
196
|
+
"title": "Employee Onboarding",
|
|
197
|
+
"description": "Welcome aboard \u2014 let's get you set up.",
|
|
198
|
+
"fields": [
|
|
199
|
+
{
|
|
200
|
+
"name": "fullName",
|
|
201
|
+
"label": "Full name",
|
|
202
|
+
"type": "text",
|
|
203
|
+
"required": true,
|
|
204
|
+
"placeholder": "Alex Morgan"
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
"name": "email",
|
|
208
|
+
"label": "Work email",
|
|
209
|
+
"type": "email",
|
|
210
|
+
"required": true,
|
|
211
|
+
"placeholder": "alex@company.com"
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
"name": "phone",
|
|
215
|
+
"label": "Phone number",
|
|
216
|
+
"type": "text",
|
|
217
|
+
"required": false
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
"name": "startDate",
|
|
221
|
+
"label": "Start date",
|
|
222
|
+
"type": "date",
|
|
223
|
+
"required": true
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
"name": "role",
|
|
227
|
+
"label": "Role you're starting",
|
|
228
|
+
"type": "text",
|
|
229
|
+
"required": true
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
"name": "manager",
|
|
233
|
+
"label": "Your manager",
|
|
234
|
+
"type": "text",
|
|
235
|
+
"required": false
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"name": "residencyStatus",
|
|
239
|
+
"label": "Right-to-work status",
|
|
240
|
+
"type": "select",
|
|
241
|
+
"required": true,
|
|
242
|
+
"options": [
|
|
243
|
+
{
|
|
244
|
+
"value": "uk-national",
|
|
245
|
+
"label": "UK national"
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
"value": "settled-status",
|
|
249
|
+
"label": "Settled status"
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
"value": "visa-required",
|
|
253
|
+
"label": "Visa required"
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
"value": "other",
|
|
257
|
+
"label": "Other"
|
|
258
|
+
}
|
|
259
|
+
]
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
"name": "rightToWorkDoc",
|
|
263
|
+
"label": "Right-to-work document",
|
|
264
|
+
"type": "file",
|
|
265
|
+
"required": true,
|
|
266
|
+
"file": {
|
|
267
|
+
"accept": "application/pdf,image/jpeg,image/png",
|
|
268
|
+
"maxSize": 5242880
|
|
269
|
+
},
|
|
270
|
+
"helper": "PDF, JPEG or PNG \u2014 up to 5 MB"
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
"name": "references",
|
|
274
|
+
"label": "Reference contacts",
|
|
275
|
+
"type": "textarea",
|
|
276
|
+
"required": false,
|
|
277
|
+
"placeholder": "Name, email, relationship \u2014 one per line"
|
|
278
|
+
}
|
|
279
|
+
],
|
|
280
|
+
"settings": {
|
|
281
|
+
"submitText": "Send to HR",
|
|
282
|
+
"successMessage": "Thanks \u2014 HR will review your details shortly.",
|
|
283
|
+
"honeypot": true,
|
|
284
|
+
"rateLimitPerMinute": 3,
|
|
285
|
+
"actionSlug": ""
|
|
286
|
+
},
|
|
287
|
+
"actions": {
|
|
288
|
+
"collection": {
|
|
289
|
+
"enabled": true,
|
|
290
|
+
"slug": ""
|
|
291
|
+
},
|
|
292
|
+
"email": {
|
|
293
|
+
"enabled": false
|
|
294
|
+
},
|
|
295
|
+
"webhook": {
|
|
296
|
+
"enabled": false
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
"users": [
|
|
301
|
+
{
|
|
302
|
+
"email": "{{hrEmail}}",
|
|
303
|
+
"name": "HR Lead",
|
|
304
|
+
"password": "{{hrPassword}}",
|
|
305
|
+
"role": "hr",
|
|
306
|
+
"additionalRoles": []
|
|
307
|
+
}
|
|
308
|
+
],
|
|
309
|
+
"actions": [
|
|
310
|
+
{
|
|
311
|
+
"slug": "{{actionPrefix}}-review",
|
|
312
|
+
"title": "Move to reviewing",
|
|
313
|
+
"description": "HR has picked this up and started checks.",
|
|
314
|
+
"collection": "{{collectionSlug}}",
|
|
315
|
+
"trigger": {
|
|
316
|
+
"label": "Mark as reviewing",
|
|
317
|
+
"icon": "eye",
|
|
318
|
+
"style": "primary"
|
|
319
|
+
},
|
|
320
|
+
"transition": {
|
|
321
|
+
"field": "status",
|
|
322
|
+
"from": [
|
|
323
|
+
"pending"
|
|
324
|
+
],
|
|
325
|
+
"to": "reviewing"
|
|
326
|
+
},
|
|
327
|
+
"access": {
|
|
328
|
+
"roles": [
|
|
329
|
+
"hr",
|
|
330
|
+
"admin",
|
|
331
|
+
"super-admin"
|
|
332
|
+
]
|
|
333
|
+
},
|
|
334
|
+
"steps": [
|
|
335
|
+
{
|
|
336
|
+
"type": "updateField",
|
|
337
|
+
"config": {
|
|
338
|
+
"field": "status",
|
|
339
|
+
"value": "reviewing"
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
]
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
"slug": "{{actionPrefix}}-approve",
|
|
346
|
+
"title": "Approve",
|
|
347
|
+
"description": "Right-to-work and references checked; cleared to start.",
|
|
348
|
+
"collection": "{{collectionSlug}}",
|
|
349
|
+
"trigger": {
|
|
350
|
+
"label": "Approve",
|
|
351
|
+
"icon": "check-circle",
|
|
352
|
+
"style": "success",
|
|
353
|
+
"confirmMessage": "Mark this starter as approved?"
|
|
354
|
+
},
|
|
355
|
+
"transition": {
|
|
356
|
+
"field": "status",
|
|
357
|
+
"from": [
|
|
358
|
+
"reviewing"
|
|
359
|
+
],
|
|
360
|
+
"to": "approved"
|
|
361
|
+
},
|
|
362
|
+
"access": {
|
|
363
|
+
"roles": [
|
|
364
|
+
"hr",
|
|
365
|
+
"admin",
|
|
366
|
+
"super-admin"
|
|
367
|
+
]
|
|
368
|
+
},
|
|
369
|
+
"steps": [
|
|
370
|
+
{
|
|
371
|
+
"type": "updateField",
|
|
372
|
+
"config": {
|
|
373
|
+
"field": "status",
|
|
374
|
+
"value": "approved"
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
"type": "email",
|
|
379
|
+
"config": {
|
|
380
|
+
"to": "{{entry.data.email}}",
|
|
381
|
+
"subject": "Welcome \u2014 you're cleared to start",
|
|
382
|
+
"template": "Hi {{entry.data.fullName}},\n\nGreat news \u2014 your onboarding paperwork has been approved. Your first day is {{entry.data.startDate}}.\n\nIf you have any questions before then, just reply to this email.\n\nThe HR team"
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
]
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
"slug": "{{actionPrefix}}-reject",
|
|
389
|
+
"title": "Reject",
|
|
390
|
+
"description": "Information missing or right-to-work check failed.",
|
|
391
|
+
"collection": "{{collectionSlug}}",
|
|
392
|
+
"trigger": {
|
|
393
|
+
"label": "Reject",
|
|
394
|
+
"icon": "x-circle",
|
|
395
|
+
"style": "danger",
|
|
396
|
+
"confirmMessage": "Reject this onboarding submission?"
|
|
397
|
+
},
|
|
398
|
+
"transition": {
|
|
399
|
+
"field": "status",
|
|
400
|
+
"from": [
|
|
401
|
+
"pending",
|
|
402
|
+
"reviewing"
|
|
403
|
+
],
|
|
404
|
+
"to": "rejected"
|
|
405
|
+
},
|
|
406
|
+
"access": {
|
|
407
|
+
"roles": [
|
|
408
|
+
"hr",
|
|
409
|
+
"admin",
|
|
410
|
+
"super-admin"
|
|
411
|
+
]
|
|
412
|
+
},
|
|
413
|
+
"steps": [
|
|
414
|
+
{
|
|
415
|
+
"type": "updateField",
|
|
416
|
+
"config": {
|
|
417
|
+
"field": "status",
|
|
418
|
+
"value": "rejected"
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
]
|
|
422
|
+
}
|
|
423
|
+
],
|
|
424
|
+
"wireForm": true,
|
|
425
|
+
"snippet": "[form name=\"{{formSlug}}\" /]\n\n## All onboarding submissions\n\n[collection slug=\"{{collectionSlug}}\" display=\"cards\" columns=\"2\" title-field=\"fullName\" searchable filterable=\"status,residencyStatus\" sortable transitions /]"
|
|
426
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reference resolver — turn raw foreign-key ids stored on entries into
|
|
3
|
+
* displayable values from the target collections.
|
|
4
|
+
*
|
|
5
|
+
* Reference field schema shape:
|
|
6
|
+
* {
|
|
7
|
+
* name: "jobId",
|
|
8
|
+
* label: "Job",
|
|
9
|
+
* type: "reference",
|
|
10
|
+
* reference: {
|
|
11
|
+
* collection: "jobs", // target collection slug (required)
|
|
12
|
+
* displayField: "title", // field on the target to show (default: "title")
|
|
13
|
+
* searchField: "title", // field to search on in picker UI (default: displayField)
|
|
14
|
+
* linkTemplate: "/jobs/{{id}}" // optional link template — `{{id}}` substituted
|
|
15
|
+
* }
|
|
16
|
+
* }
|
|
17
|
+
*
|
|
18
|
+
* Storage stays the same: `entry.data.jobId` is the target's id (or an array
|
|
19
|
+
* of ids for one-to-many references). The resolver attaches a parallel
|
|
20
|
+
* `entry._refs` map that callers can use to render labels without mutating
|
|
21
|
+
* the raw data shape.
|
|
22
|
+
*
|
|
23
|
+
* Batching: when resolving N entries that each have a `jobId`, we collect
|
|
24
|
+
* all distinct ids once, fetch the target collection in a single pass, then
|
|
25
|
+
* decorate every entry from the lookup map — one adapter call per *target
|
|
26
|
+
* collection* per resolve, not per entry.
|
|
27
|
+
*
|
|
28
|
+
* Missing-target handling: if a referenced id no longer exists (the target
|
|
29
|
+
* was deleted), the entry gets `_refs.<field> = { id, display: null, missing: true }`
|
|
30
|
+
* so callers can render a graceful "(deleted)" indicator instead of crashing.
|
|
31
|
+
*/
|
|
32
|
+
import {getAdapter} from './adapterRegistry.js';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Extract the list of reference field definitions from a schema.
|
|
36
|
+
*
|
|
37
|
+
* @param {object} schema
|
|
38
|
+
* @returns {Array<{name:string, label:string, reference:object}>}
|
|
39
|
+
*/
|
|
40
|
+
export function referenceFields(schema) {
|
|
41
|
+
return (schema?.fields || []).filter(f => f.type === 'reference' && f.reference?.collection);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Resolve all reference fields on a batch of entries.
|
|
46
|
+
*
|
|
47
|
+
* Each entry is mutated in place to add `_refs`:
|
|
48
|
+
* entry._refs = {
|
|
49
|
+
* jobId: { id: 'abc', display: 'Senior Engineer', missing: false, link: '/jobs/abc' }
|
|
50
|
+
* }
|
|
51
|
+
*
|
|
52
|
+
* For one-to-many (array) references the shape is:
|
|
53
|
+
* entry._refs = {
|
|
54
|
+
* skillIds: [{id, display, link, missing}, ...]
|
|
55
|
+
* }
|
|
56
|
+
*
|
|
57
|
+
* Returns the entries (same reference) for chainable use.
|
|
58
|
+
*
|
|
59
|
+
* @param {object} schema - Source collection schema
|
|
60
|
+
* @param {object[]} entries - Source entries; each may be mutated
|
|
61
|
+
* @param {object} [opts]
|
|
62
|
+
* @param {Function} [opts.adapterResolver] - Optional async (slug) → adapter — for tests
|
|
63
|
+
* @returns {Promise<object[]>}
|
|
64
|
+
*/
|
|
65
|
+
export async function resolveReferences(schema, entries, opts = {}) {
|
|
66
|
+
if (!Array.isArray(entries) || !entries.length) return entries;
|
|
67
|
+
|
|
68
|
+
const refs = referenceFields(schema);
|
|
69
|
+
if (!refs.length) return entries;
|
|
70
|
+
|
|
71
|
+
const resolver = opts.adapterResolver || getAdapter;
|
|
72
|
+
|
|
73
|
+
// Group distinct ids per target collection so we make one adapter call
|
|
74
|
+
// per collection regardless of how many fields reference it.
|
|
75
|
+
/** @type {Map<string, Set<string>>} */
|
|
76
|
+
const idsByCollection = new Map();
|
|
77
|
+
for (const field of refs) {
|
|
78
|
+
const target = field.reference.collection;
|
|
79
|
+
if (!idsByCollection.has(target)) idsByCollection.set(target, new Set());
|
|
80
|
+
const bucket = idsByCollection.get(target);
|
|
81
|
+
for (const e of entries) {
|
|
82
|
+
const raw = e.data?.[field.name];
|
|
83
|
+
if (raw == null || raw === '') continue;
|
|
84
|
+
if (Array.isArray(raw)) raw.forEach(v => v != null && bucket.add(String(v)));
|
|
85
|
+
else bucket.add(String(raw));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Fetch each target collection once, build a lookup map { collectionSlug → Map<id, entry> }
|
|
90
|
+
/** @type {Map<string, Map<string, object>>} */
|
|
91
|
+
const lookups = new Map();
|
|
92
|
+
for (const [collectionSlug, idSet] of idsByCollection) {
|
|
93
|
+
if (!idSet.size) { lookups.set(collectionSlug, new Map()); continue; }
|
|
94
|
+
try {
|
|
95
|
+
const adapter = await resolver(collectionSlug);
|
|
96
|
+
// FileAdapter: cheap to read all then filter; MongoAdapter: $in is the
|
|
97
|
+
// right call but we don't have a list-by-ids API yet. For now, get()
|
|
98
|
+
// each id in parallel — N requests but small N (distinct refs only).
|
|
99
|
+
const map = new Map();
|
|
100
|
+
await Promise.all([...idSet].map(async id => {
|
|
101
|
+
try {
|
|
102
|
+
const target = await adapter.get(collectionSlug, id);
|
|
103
|
+
if (target) map.set(id, target);
|
|
104
|
+
} catch { /* missing target — left out of the map */ }
|
|
105
|
+
}));
|
|
106
|
+
lookups.set(collectionSlug, map);
|
|
107
|
+
} catch {
|
|
108
|
+
lookups.set(collectionSlug, new Map()); // collection gone — all refs render as missing
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Decorate every entry with resolved _refs
|
|
113
|
+
for (const entry of entries) {
|
|
114
|
+
entry._refs = entry._refs || {};
|
|
115
|
+
for (const field of refs) {
|
|
116
|
+
const raw = entry.data?.[field.name];
|
|
117
|
+
if (raw == null || raw === '') continue;
|
|
118
|
+
const map = lookups.get(field.reference.collection) || new Map();
|
|
119
|
+
if (Array.isArray(raw)) {
|
|
120
|
+
entry._refs[field.name] = raw.filter(v => v != null).map(id => buildRef(field, String(id), map));
|
|
121
|
+
} else {
|
|
122
|
+
entry._refs[field.name] = buildRef(field, String(raw), map);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return entries;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Build a single resolved-ref record for an id.
|
|
132
|
+
*
|
|
133
|
+
* @param {object} field
|
|
134
|
+
* @param {string} id
|
|
135
|
+
* @param {Map<string, object>} lookupMap
|
|
136
|
+
* @returns {{id:string, display:string|null, missing:boolean, link:string|null}}
|
|
137
|
+
*/
|
|
138
|
+
function buildRef(field, id, lookupMap) {
|
|
139
|
+
const target = lookupMap.get(id);
|
|
140
|
+
if (!target) {
|
|
141
|
+
return { id, display: null, missing: true, link: null };
|
|
142
|
+
}
|
|
143
|
+
const displayField = field.reference.displayField || 'title';
|
|
144
|
+
const raw = target.data?.[displayField];
|
|
145
|
+
const display = (raw == null || raw === '') ? id : String(raw);
|
|
146
|
+
let link = null;
|
|
147
|
+
if (field.reference.linkTemplate) {
|
|
148
|
+
link = String(field.reference.linkTemplate).replace(/\{\{\s*id\s*\}\}/g, encodeURIComponent(id));
|
|
149
|
+
}
|
|
150
|
+
return { id, display, missing: false, link };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Convenience helper for renderers — get the display label for a reference
|
|
155
|
+
* field on a single entry, falling back to the raw value when no resolution
|
|
156
|
+
* was performed (e.g. a caller skipped `resolveRefs: true`).
|
|
157
|
+
*
|
|
158
|
+
* Handles arrays by joining with ", ".
|
|
159
|
+
*
|
|
160
|
+
* @param {object} entry
|
|
161
|
+
* @param {string} fieldName
|
|
162
|
+
* @returns {string}
|
|
163
|
+
*/
|
|
164
|
+
export function refDisplay(entry, fieldName) {
|
|
165
|
+
const r = entry?._refs?.[fieldName];
|
|
166
|
+
if (r == null) {
|
|
167
|
+
const raw = entry?.data?.[fieldName];
|
|
168
|
+
return raw == null ? '' : (Array.isArray(raw) ? raw.join(', ') : String(raw));
|
|
169
|
+
}
|
|
170
|
+
if (Array.isArray(r)) {
|
|
171
|
+
return r.map(x => x.missing ? `${x.id} (missing)` : x.display).join(', ');
|
|
172
|
+
}
|
|
173
|
+
return r.missing ? `${r.id} (missing)` : r.display;
|
|
174
|
+
}
|