domma-cms 1.6.59 → 1.7.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/CHANGELOG.md +4344 -0
- package/README.md +1 -1
- package/admin/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +3 -3
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +2 -2
- package/admin/js/lib/draft-owner.js +1 -0
- package/admin/js/lib/effect-defs.js +1 -1
- package/admin/js/lib/effects-builder.js +2 -2
- package/admin/js/lib/page-editor-reference.js +5 -5
- package/admin/js/lib/shortcode-tags.js +1 -1
- package/admin/js/templates/docs/usage-shortcodes.html +39 -0
- package/admin/js/views/user-editor.js +1 -1
- package/docs/DommaFeatures.md +3005 -0
- package/docs/MultiTenant.md +176 -0
- package/docs/api-reference.md +1667 -0
- package/docs/cache.md +96 -0
- package/docs/configuration.md +1811 -0
- package/docs/editions-and-licences.md +190 -0
- package/docs/getting-started.md +201 -0
- package/docs/guides/form-triggers.md +260 -0
- package/docs/guides/mongodb-storage-adapter.md +282 -0
- package/docs/guides/multi-page-forms.md +121 -0
- package/docs/images/scribe-composer.png +0 -0
- package/docs/markdown-shortcodes.md +1738 -0
- package/docs/pentest-report-2026-04-03.md +181 -0
- package/docs/plans/2026-02-28-admin-topbar-login-forms.md +349 -0
- package/docs/plans/2026-02-28-domma-ui-polish.md +286 -0
- package/docs/plans/2026-02-28-reset-seed-fresh-commands.md +520 -0
- package/docs/plans/2026-03-02-cookie-consent-design.md +86 -0
- package/docs/plans/2026-03-02-cookie-consent-plugin.md +453 -0
- package/docs/plans/2026-03-02-tooltip-sweep-and-back-to-top-plugin-design.md +126 -0
- package/docs/plans/2026-03-02-tooltip-sweep-back-to-top.md +427 -0
- package/docs/plans/2026-03-05-cookie-consent-btt-core.md +115 -0
- package/docs/plans/2026-03-05-custom-css-migrate-to-core-design.md +35 -0
- package/docs/plans/2026-03-05-custom-css-migrate-to-core.md +352 -0
- package/docs/plans/2026-03-05-scribe-script-shortcode.md +373 -0
- package/docs/plans/2026-03-05-user-types-design.md +139 -0
- package/docs/plans/2026-03-05-user-types-implementation.md +1035 -0
- package/docs/plans/2026-03-09-page-editor-view-cta-inserts-design.md +78 -0
- package/docs/plans/2026-03-09-page-editor-view-cta-inserts.md +388 -0
- package/docs/plans/2026-03-09-pro-documentation-design.md +80 -0
- package/docs/plans/2026-03-09-pro-documentation.md +1017 -0
- package/docs/plans/2026-06-10-core-project-design.md +240 -0
- package/docs/plugin-development.md +827 -0
- package/docs/recruitment-recipe.md +501 -0
- package/docs/scaffolding.md +312 -0
- package/docs/screenshots/calculator-floating.png +0 -0
- package/docs/screenshots/calculator-standard.png +0 -0
- package/docs/screenshots/email-settings.png +0 -0
- package/docs/screenshots/messages-count.png +0 -0
- package/docs/structure/user-types.md +107 -0
- package/docs/superpowers/plans/2026-05-25-admin-sidebar-via-menus.md +1112 -0
- package/docs/superpowers/plans/2026-06-08-navbar-mobile-drawer-and-menu-enrichment.md +1161 -0
- package/docs/superpowers/plans/2026-06-25-domma-docs-project.md +695 -0
- package/docs/superpowers/plans/2026-06-26-surveys-HANDOFF.md +68 -0
- package/docs/superpowers/plans/2026-06-26-surveys-pro-plugin.md +1902 -0
- package/docs/superpowers/plans/2026-08-19-collection-context-sort-export.md +1577 -0
- package/docs/superpowers/plans/2026-10-09-content-api.md +1192 -0
- package/docs/superpowers/specs/2026-03-12-form-followup-actions-design.md +152 -0
- package/docs/superpowers/specs/2026-03-12-page-editor-form-insert-and-per-page-theme-design.md +115 -0
- package/docs/superpowers/specs/2026-03-17-nav-hidden-items-design.md +85 -0
- package/docs/superpowers/specs/2026-03-19-accordion-insert-design.md +175 -0
- package/docs/superpowers/specs/2026-03-25-static-block-shortcode-design.md +153 -0
- package/docs/superpowers/specs/2026-03-25-text-shortcode-design.md +168 -0
- package/docs/superpowers/specs/2026-04-05-card-builder-design.md +315 -0
- package/docs/superpowers/specs/2026-04-06-insert-builders-design.md +278 -0
- package/docs/superpowers/specs/2026-04-06-layouts-design.md +226 -0
- package/docs/superpowers/specs/2026-04-07-blog-plugin-design.md +300 -0
- package/docs/superpowers/specs/2026-04-07-calendar-plugin-design.md +538 -0
- package/docs/superpowers/specs/2026-04-17-components-area-design.md +368 -0
- package/docs/superpowers/specs/2026-05-02-effects-slideover-overhaul-design.md +193 -0
- package/docs/superpowers/specs/2026-05-06-dashboard-rebuild-design.md +257 -0
- package/docs/superpowers/specs/2026-05-24-menus-design.md +466 -0
- package/docs/superpowers/specs/2026-05-25-admin-sidebar-via-menus-design.md +358 -0
- package/docs/superpowers/specs/2026-05-25-projects-design.md +380 -0
- package/docs/superpowers/specs/2026-06-08-navbar-mobile-drawer-and-menu-enrichment-design.md +292 -0
- package/docs/superpowers/specs/2026-06-08-sidebar-tools-plugins-folders-design.md +59 -0
- package/docs/superpowers/specs/2026-06-25-domma-docs-project-design.md +192 -0
- package/docs/superpowers/specs/2026-06-26-domma-charts-upstream-proposal.md +44 -0
- package/docs/superpowers/specs/2026-06-26-manager-scheduler-upstream-proposal.md +46 -0
- package/docs/superpowers/specs/2026-06-26-surveys-pro-plugin-design.md +206 -0
- package/docs/superpowers/specs/2026-07-02-shortcode-context-menu-design.md +96 -0
- package/docs/superpowers/specs/2026-08-19-collection-context-sort-export-design.md +330 -0
- package/docs/superpowers/specs/2026-09-22-dcms-marketplace-design.md +1150 -0
- package/docs/superpowers/specs/2026-10-09-content-api-design.md +325 -0
- package/docs/superpowers/specs/plans/2026-03-12-form-followup-actions.md +624 -0
- package/docs/superpowers/specs/plans/2026-03-12-page-editor-form-insert-and-per-page-theme.md +125 -0
- package/docs/superpowers/specs/plans/2026-03-17-nav-hidden-items.md +578 -0
- package/docs/superpowers/specs/plans/2026-03-19-accordion-insert.md +635 -0
- package/docs/superpowers/specs/plans/2026-03-25-text-shortcode.md +545 -0
- package/docs/superpowers/specs/plans/2026-04-05-card-builder.md +1970 -0
- package/docs/superpowers/specs/plans/2026-04-06-insert-builders.md +1049 -0
- package/docs/superpowers/specs/plans/2026-04-06-layouts.md +1015 -0
- package/docs/superpowers/specs/plans/2026-04-07-calendar-plugin.md +1526 -0
- package/docs/superpowers/specs/plans/2026-04-08-plugin-monetisation-split.md +242 -0
- package/docs/superpowers/specs/plans/2026-04-17-components-area.md +2038 -0
- package/docs/superpowers/specs/plans/2026-05-02-effects-slideover-overhaul.md +2335 -0
- package/docs/superpowers/specs/plans/2026-05-06-dashboard-rebuild.md +1999 -0
- package/docs/superpowers/specs/plans/2026-05-17-cache-system.md +1436 -0
- package/docs/superpowers/specs/plans/2026-05-24-menus.md +3096 -0
- package/docs/superpowers/specs/plans/2026-05-25-projects.md +2869 -0
- package/docs/superpowers/specs/plans/2026-09-22-marketplace-step2-consolidation.md +505 -0
- package/docs/superpowers/specs/plans/2026-09-22-marketplace-step3-catalogue-and-key.md +154 -0
- package/docs/superpowers/specs/plans/2026-09-22-tools-promotion.md +260 -0
- package/docs/theming.md +320 -0
- package/package.json +2 -2
- package/plugins/_lib/admin/mail/keyboard.js +49 -0
- package/plugins/_lib/admin/mail/reader-view.js +4 -2
- package/public/js/celebrations/core/canvas.js +1 -1
- package/public/js/celebrations/index.js +1 -1
- package/public/js/celebrations/licenses/ABeeZee-Regular-OFL.txt +93 -0
- package/public/js/celebrations/licenses/Allura-Regular-OFL.txt +93 -0
- package/public/js/celebrations/message-attrs.mjs +1 -0
- package/public/js/celebrations/themes/anniversary.js +1 -0
- package/public/js/celebrations/themes/autumn.js +1 -0
- package/public/js/celebrations/themes/birthday.js +1 -0
- package/public/js/celebrations/themes/butterfly-flight.js +1 -0
- package/public/js/celebrations/themes/calendar.js +1 -0
- package/public/js/celebrations/themes/chinese-new-year-dates.js +1 -0
- package/public/js/celebrations/themes/chinese-new-year.js +1 -0
- package/public/js/celebrations/themes/christmas.js +1 -1
- package/public/js/celebrations/themes/easter.js +1 -0
- package/public/js/celebrations/themes/garden-drawing.js +1 -0
- package/public/js/celebrations/themes/garden.js +1 -0
- package/public/js/celebrations/themes/graduation.js +1 -0
- package/public/js/celebrations/themes/greeting.js +1 -0
- package/public/js/celebrations/themes/make-it-rain.js +1 -0
- package/public/js/celebrations/themes/money-rain.js +1 -0
- package/public/js/celebrations/themes/new-year.js +1 -0
- package/public/js/celebrations/themes/occasion-drawing.js +1 -0
- package/public/js/celebrations/themes/occasions.js +1 -0
- package/public/js/celebrations/themes/rain-control.js +1 -0
- package/public/js/celebrations/themes/sparkler-font-data.js +1 -0
- package/public/js/celebrations/themes/sparkler-font.js +1 -0
- package/public/js/celebrations/themes/sparkler-writing.js +1 -0
- package/public/js/celebrations/themes/summer-garden.js +1 -0
- package/public/js/celebrations/themes/winter.js +1 -0
- package/public/js/celebrations/trait-data.mjs +1 -1
- package/public/js/effects.js +1 -1
- package/scripts/build.js +4 -0
- package/server/services/markdown.js +5 -2
- package/server/templates/page.html +1 -1
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
# Recipe - Building a Job Board with Stock Primitives
|
|
2
|
+
|
|
3
|
+
Domma CMS doesn't ship a recruitment plugin, but every primitive you need for a working job board is in the base
|
|
4
|
+
distribution. This recipe builds an end-to-end candidate/employer flow from scratch using only:
|
|
5
|
+
|
|
6
|
+
- **Collections** with structured filtering
|
|
7
|
+
- **Forms** that capture applications
|
|
8
|
+
- **Actions** that link applications to jobs
|
|
9
|
+
- **Pages** with role-based visibility and per-user scoping
|
|
10
|
+
|
|
11
|
+
The same pattern works for any relational app: bookings, classified ads, marketplace listings, bug trackers, RSVPs.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Architecture overview
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
content/collections/jobs/ ← postings authored by employers
|
|
19
|
+
content/collections/applications/ ← per-candidate applications linking back to jobs
|
|
20
|
+
content/pages/jobs/index.md ← public job listing with filters
|
|
21
|
+
content/pages/dashboard.md ← signed-in dashboard with "My applications"
|
|
22
|
+
content/forms/apply.json ← form embedded on each job page
|
|
23
|
+
Actions:
|
|
24
|
+
- "submit-application" creates an application linked to the job + candidate
|
|
25
|
+
- "withdraw-application" deletes an application owned by the candidate
|
|
26
|
+
Roles (added to content/collections/roles/):
|
|
27
|
+
- candidate (level 5)
|
|
28
|
+
- employer (level 4)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 1. Add the roles
|
|
34
|
+
|
|
35
|
+
In the admin panel **System > Roles** (or by editing `content/collections/roles/data.json`):
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"name": "candidate",
|
|
40
|
+
"label": "Candidate",
|
|
41
|
+
"level": 5,
|
|
42
|
+
"permissions": ["notifications"],
|
|
43
|
+
"badgeClass": "badge-info"
|
|
44
|
+
}
|
|
45
|
+
{
|
|
46
|
+
"name": "employer",
|
|
47
|
+
"label": "Employer",
|
|
48
|
+
"level": 4,
|
|
49
|
+
"permissions": ["notifications"],
|
|
50
|
+
"badgeClass": "badge-secondary"
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Roles are dynamic - once saved, every visibility check and `where_role=` filter recognises them immediately.
|
|
55
|
+
|
|
56
|
+
The base roles are `super-admin` (0), `admin` (1) and `user` (2), so levels 4 and 5 sit below `user`: a plain
|
|
57
|
+
`user` counts as "more senior" than a candidate. Use the exact form (`visibility: "=candidate"`) where a page or
|
|
58
|
+
menu item must be for candidates only.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 2. Create the `jobs` collection
|
|
63
|
+
|
|
64
|
+
Schema fields (admin → Collections → New):
|
|
65
|
+
|
|
66
|
+
| Field | Type | Notes |
|
|
67
|
+
|------------|------------|------------------------------------|
|
|
68
|
+
| `title` | text | Job title |
|
|
69
|
+
| `company` | text | Hiring organisation |
|
|
70
|
+
| `location` | text | City or "Remote" |
|
|
71
|
+
| `type` | select | `full-time`, `contract`, `part-time` |
|
|
72
|
+
| `salary` | number | Annual GBP |
|
|
73
|
+
| `tags` | multiselect| `remote`, `hybrid`, `on-site`, etc. |
|
|
74
|
+
| `summary` | textarea | Short pitch |
|
|
75
|
+
| `body` | rich-text | Full description |
|
|
76
|
+
| `postedAt` | date | Defaults to `createdAt` |
|
|
77
|
+
|
|
78
|
+
**Permissions:**
|
|
79
|
+
|
|
80
|
+
- `api.read` = enabled, access = `public` - anyone can list jobs
|
|
81
|
+
- `api.create`, `update`, `delete` = enabled, access = `employer` - employers post their own roles
|
|
82
|
+
- Add a **row-level access** rule (`content/collections/jobs/schema.json → rowAccess`):
|
|
83
|
+
```json
|
|
84
|
+
"rowAccess": {
|
|
85
|
+
"update": { "ownerField": "createdBy", "fallbackRoles": ["admin", "super-admin"] },
|
|
86
|
+
"delete": { "ownerField": "createdBy", "fallbackRoles": ["admin", "super-admin"] }
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
Employers can only edit/delete jobs they created; admins can manage all.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 3. Create the `applications` collection
|
|
94
|
+
|
|
95
|
+
Fields:
|
|
96
|
+
|
|
97
|
+
| Field | Type | Notes |
|
|
98
|
+
|---------------|-------------|--------------------------------------------------------------------------|
|
|
99
|
+
| `jobId` | **reference** | `reference: { collection: "jobs", displayField: "title", linkTemplate: "/jobs/{{id}}" }` |
|
|
100
|
+
| `candidateId` | text | Set automatically to `meta.createdBy` |
|
|
101
|
+
| `coverLetter` | textarea | What the candidate wrote |
|
|
102
|
+
| `resume` | **file** | `file: { accept: "application/pdf", maxSize: 5242880 }` - PDF up to 5 MB |
|
|
103
|
+
| `status` | select | `submitted`, `reviewing`, `interview`, `offered`, `rejected`, `withdrawn`|
|
|
104
|
+
| `submittedAt` | date | Defaults to `createdAt` |
|
|
105
|
+
|
|
106
|
+
The `jobId` is a **reference** to the `jobs` collection. This single change
|
|
107
|
+
means:
|
|
108
|
+
|
|
109
|
+
- The apply form auto-renders a populated dropdown of job titles (no manual
|
|
110
|
+
options or denormalised `jobTitle` field needed)
|
|
111
|
+
- The candidate's "My applications" view shows the job title - not the id -
|
|
112
|
+
because resolution happens automatically when listing
|
|
113
|
+
- Each row links to `/jobs/<id>` for the original posting (via `linkTemplate`)
|
|
114
|
+
- Validation prevents stale apps pointing at deleted jobs (you'll get
|
|
115
|
+
`"Job" → no entry "<id>" in "jobs"` on create)
|
|
116
|
+
|
|
117
|
+
**Permissions:**
|
|
118
|
+
|
|
119
|
+
- `api.read` = **disabled** (we'll use `scope="mine"` instead - see step 7)
|
|
120
|
+
- `api.create` = **disabled** (created via Action, not directly)
|
|
121
|
+
- No row-level rules needed - the scope=mine endpoint already restricts to `createdBy = user.id`
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## 4. Build the public job listings page
|
|
126
|
+
|
|
127
|
+
Create `content/pages/jobs/index.md`:
|
|
128
|
+
|
|
129
|
+
```markdown
|
|
130
|
+
---
|
|
131
|
+
title: Jobs
|
|
132
|
+
visibility: public
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Find your next role
|
|
136
|
+
|
|
137
|
+
[collection slug="jobs"
|
|
138
|
+
display="cards" columns="3"
|
|
139
|
+
searchable
|
|
140
|
+
filterable="location,type,salary,tags"
|
|
141
|
+
sortable
|
|
142
|
+
page-size="9"
|
|
143
|
+
title-field="title"
|
|
144
|
+
fields="company,location,salary,type,tags"
|
|
145
|
+
sort="postedAt" order="desc"
|
|
146
|
+
cta="apply-flow"
|
|
147
|
+
cta-label="View & Apply"
|
|
148
|
+
cta-icon="briefcase"
|
|
149
|
+
empty="No matching roles right now - try widening your filters." /]
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
That single shortcode renders:
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
┌────────────────────────────────────────────────────────────┐
|
|
156
|
+
│ [🔍 Search…] [Sort: Most recent ↓] │
|
|
157
|
+
├──────────────┬─────────────────────────────────────────────┤
|
|
158
|
+
│ Filters │ ┌────────┐ ┌────────┐ ┌────────┐ │
|
|
159
|
+
│ ────────── │ │ Job #1 │ │ Job #2 │ │ Job #3 │ │
|
|
160
|
+
│ Location │ └────────┘ └────────┘ └────────┘ │
|
|
161
|
+
│ [London ▾] │ ┌────────┐ ┌────────┐ ┌────────┐ │
|
|
162
|
+
│ Type │ │ Job #4 │ │ Job #5 │ │ Job #6 │ │
|
|
163
|
+
│ ☐ Full-time │ └────────┘ └────────┘ └────────┘ │
|
|
164
|
+
│ ☐ Contract │ ┌────────┐ ┌────────┐ ┌────────┐ │
|
|
165
|
+
│ Salary │ │ Job #7 │ │ Job #8 │ │ Job #9 │ │
|
|
166
|
+
│ [50k]──●─── │ └────────┘ └────────┘ └────────┘ │
|
|
167
|
+
│ Tags ├─────────────────────────────────────────────┤
|
|
168
|
+
│ ☐ remote │ ‹ Prev Page 1 of 4 Next › │
|
|
169
|
+
│ ☐ hybrid │ │
|
|
170
|
+
└──────────────┴─────────────────────────────────────────────┘
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
- The filter rail is **derived from the schema**: `location` (text) becomes a dropdown of distinct values, `type`
|
|
174
|
+
(select) becomes a single-pick dropdown, `salary` (number) becomes a min/max range, `tags` (multiselect) becomes
|
|
175
|
+
a checkbox group. No JS authoring required.
|
|
176
|
+
- Every change updates the URL: visiting `/jobs?cb_jobs-N_filter[location]=London&cb_jobs-N_filter[salary_gte]=50000`
|
|
177
|
+
deep-links to that exact filtered view - shareable, bookmarkable, back-button-friendly.
|
|
178
|
+
- All client-side by default (mode=client) - instant response.
|
|
179
|
+
|
|
180
|
+
**For a pre-narrowed landing page** like `/jobs/well-paid`, bake in a constraint that users can refine but not
|
|
181
|
+
escape:
|
|
182
|
+
|
|
183
|
+
```markdown
|
|
184
|
+
---
|
|
185
|
+
title: Well-paid Roles
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
[collection slug="jobs"
|
|
189
|
+
display="cards"
|
|
190
|
+
searchable filterable="location,type" sortable
|
|
191
|
+
where_salary_gte="50000"
|
|
192
|
+
where_type_in="full-time,contract" /]
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
The `where_*` attributes are locked in - the filter rail lets visitors narrow further (e.g. only London full-time)
|
|
196
|
+
but can never widen back to part-time or sub-£50k roles. Server-rendered initial view + interactive refinement,
|
|
197
|
+
in one shortcode.
|
|
198
|
+
|
|
199
|
+
**Mongo-backed at scale.** Once `jobs` outgrows the in-memory cap (default 1000 entries), add `mode="server"`:
|
|
200
|
+
|
|
201
|
+
```markdown
|
|
202
|
+
[collection slug="jobs" searchable filterable="location,type,salary,tags"
|
|
203
|
+
sortable mode="server" page-size="12" /]
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Every filter change becomes a `GET /api/collections/jobs/public?filter[…]&page=…` round-trip; MongoAdapter
|
|
207
|
+
translates the filter to a native `$gte`/`$in`/regex query and hits a B-tree index. Same UX, no payload bloat.
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## 5. Build the apply form
|
|
212
|
+
|
|
213
|
+
Create `content/forms/apply.json` (or via the admin Forms editor):
|
|
214
|
+
|
|
215
|
+
```json
|
|
216
|
+
{
|
|
217
|
+
"slug": "apply",
|
|
218
|
+
"title": "Apply for this role",
|
|
219
|
+
"fields": [
|
|
220
|
+
{
|
|
221
|
+
"name": "jobId", "type": "reference", "required": true, "label": "Position",
|
|
222
|
+
"reference": { "collection": "jobs", "displayField": "title" }
|
|
223
|
+
},
|
|
224
|
+
{ "name": "coverLetter", "type": "textarea", "required": true, "label": "Cover letter" },
|
|
225
|
+
{
|
|
226
|
+
"name": "resume", "type": "file", "required": true, "label": "Resume (PDF)",
|
|
227
|
+
"file": { "accept": "application/pdf", "maxSize": 5242880 }
|
|
228
|
+
}
|
|
229
|
+
],
|
|
230
|
+
"settings": {
|
|
231
|
+
"successMessage": "Application submitted!",
|
|
232
|
+
"successRedirect": "/dashboard",
|
|
233
|
+
"actionSlug": "submit-application",
|
|
234
|
+
"rateLimitPerMinute": 3,
|
|
235
|
+
"honeypot": true
|
|
236
|
+
},
|
|
237
|
+
"actions": {
|
|
238
|
+
"collection": { "enabled": false }
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Embed it on a generic apply page:
|
|
244
|
+
|
|
245
|
+
```markdown
|
|
246
|
+
[form slug="apply" /]
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
That's the whole form. The reference field auto-renders as a populated
|
|
250
|
+
dropdown of all open jobs - server-side at render time, no extra round-trips,
|
|
251
|
+
no JS-side fetching. No hidden ids, no denormalised job titles, no manual
|
|
252
|
+
glue. **The form file declares the relationship and the platform handles the
|
|
253
|
+
rest.**
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## 6. Wire the apply action
|
|
258
|
+
|
|
259
|
+
The form's `settings.actionSlug` references `submit-application`. Create this action (admin → Actions → New):
|
|
260
|
+
|
|
261
|
+
```json
|
|
262
|
+
{
|
|
263
|
+
"slug": "submit-application",
|
|
264
|
+
"title": "Submit Application",
|
|
265
|
+
"collection": "form-submissions",
|
|
266
|
+
"access": { "roles": ["candidate", "admin", "super-admin"] },
|
|
267
|
+
"steps": [
|
|
268
|
+
{
|
|
269
|
+
"type": "createInCollection",
|
|
270
|
+
"config": {
|
|
271
|
+
"targetCollection": "applications",
|
|
272
|
+
"createdBy": "{{user.id}}",
|
|
273
|
+
"data": {
|
|
274
|
+
"jobId": "{{entry.data.jobId}}",
|
|
275
|
+
"candidateId": "{{user.id}}",
|
|
276
|
+
"coverLetter": "{{entry.data.coverLetter}}",
|
|
277
|
+
"status": "submitted",
|
|
278
|
+
"submittedAt": "{{now}}"
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
"type": "email",
|
|
284
|
+
"config": {
|
|
285
|
+
"to": "{{user.email}}",
|
|
286
|
+
"subject": "We received your application for {{entry.data.jobTitle}}",
|
|
287
|
+
"template": "Hi {{user.name}},\n\nThanks for applying - we'll be in touch.\n"
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
]
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
**Key trick:** the public form submission endpoint (`POST /api/forms/submit/:slug`) does an *optional* JWT verification.
|
|
295
|
+
Signed-in candidates get their identity captured automatically; `{{user.id}}` and `{{user.email}}` resolve correctly
|
|
296
|
+
inside the action's templates. Anonymous applicants would get blank values - for the recruitment use case, gate the
|
|
297
|
+
job-detail page with `visibility: candidate` to force sign-in.
|
|
298
|
+
|
|
299
|
+
The `createInCollection` step is new (introduced alongside this recipe). Unlike `moveToCollection`, it does **not**
|
|
300
|
+
delete the source entry - so the original `form-submissions` record remains as an audit trail and the new
|
|
301
|
+
`applications` entry is what the dashboard reads.
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
## 7. Candidate dashboard with "My applications"
|
|
306
|
+
|
|
307
|
+
Create `content/pages/dashboard.md`:
|
|
308
|
+
|
|
309
|
+
```markdown
|
|
310
|
+
---
|
|
311
|
+
title: My Dashboard
|
|
312
|
+
visibility: [candidate, employer]
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## My Applications
|
|
316
|
+
|
|
317
|
+
[collection slug="applications"
|
|
318
|
+
scope="mine"
|
|
319
|
+
display="cards"
|
|
320
|
+
title-field="jobId"
|
|
321
|
+
fields="jobId,status,submittedAt"
|
|
322
|
+
sort="submittedAt"
|
|
323
|
+
order="desc"
|
|
324
|
+
cta="withdraw-application"
|
|
325
|
+
cta-label="Withdraw"
|
|
326
|
+
cta-icon="x-circle"
|
|
327
|
+
cta-style="danger"
|
|
328
|
+
cta-confirm="Withdraw this application?"
|
|
329
|
+
empty="You haven't applied for any roles yet." /]
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
`title-field="jobId"` doesn't show the raw id - because `jobId` is a
|
|
333
|
+
**reference**, the platform resolves it to the job's title automatically. The
|
|
334
|
+
card title reads "Senior Engineer", clickable to `/jobs/<id>` thanks to the
|
|
335
|
+
`linkTemplate` on the schema. No denormalisation needed, no broken links when
|
|
336
|
+
a job is updated.
|
|
337
|
+
|
|
338
|
+
`visibility: [candidate, employer]` - both roles see the page; super-admin and admin inherit access automatically.
|
|
339
|
+
`scope="mine"` makes the block render client-side via `POST /api/collections/render-scope`, which auto-applies
|
|
340
|
+
`createdBy = <current user id>`. The page itself stays cached per role; only the per-user data is fetched per visit.
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
344
|
+
## 8. Wire state-machine transitions
|
|
345
|
+
|
|
346
|
+
Define **one action per legal state transition** and let the platform render
|
|
347
|
+
the right buttons for the right row's current state. Add `transition` to each:
|
|
348
|
+
|
|
349
|
+
```json
|
|
350
|
+
{
|
|
351
|
+
"slug": "withdraw-application",
|
|
352
|
+
"title": "Withdraw",
|
|
353
|
+
"collection": "applications",
|
|
354
|
+
"transition": { "field": "status", "from": ["submitted", "reviewing"], "to": "withdrawn" },
|
|
355
|
+
"access": { "roles": ["candidate"], "rowLevel": { "mode": "owner" } },
|
|
356
|
+
"steps": [
|
|
357
|
+
{ "type": "updateField", "config": { "field": "status", "value": "withdrawn" } }
|
|
358
|
+
]
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
```json
|
|
363
|
+
{
|
|
364
|
+
"slug": "start-review",
|
|
365
|
+
"title": "Move to reviewing",
|
|
366
|
+
"collection": "applications",
|
|
367
|
+
"transition": { "field": "status", "from": ["submitted"], "to": "reviewing" },
|
|
368
|
+
"access": { "roles": ["admin", "hr"] },
|
|
369
|
+
"steps": [
|
|
370
|
+
{ "type": "updateField", "config": { "field": "status", "value": "reviewing" } },
|
|
371
|
+
{ "type": "email", "config": {
|
|
372
|
+
"to": "{{user.email}}",
|
|
373
|
+
"subject": "{{entry.data.jobId}} - moved to review",
|
|
374
|
+
"template": "Application for {{entry.data.jobId}} is now in review."
|
|
375
|
+
}}
|
|
376
|
+
]
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
```json
|
|
381
|
+
{
|
|
382
|
+
"slug": "schedule-interview",
|
|
383
|
+
"title": "Invite to interview",
|
|
384
|
+
"collection": "applications",
|
|
385
|
+
"transition": { "field": "status", "from": ["reviewing"], "to": "interview" },
|
|
386
|
+
"access": { "roles": ["admin", "hr"] },
|
|
387
|
+
"steps": [{ "type": "updateField", "config": { "field": "status", "value": "interview" }}]
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
The candidate's dashboard from step 7 just needs `paginate` and `transitions`
|
|
392
|
+
added. Transition buttons are drawn by the interactive Collection Browser, and
|
|
393
|
+
`paginate` (or `searchable`, `sortable`, `filterable`) is what turns it on - a
|
|
394
|
+
`scope="mine"` block then hydrates as the Browser over the candidate's own rows:
|
|
395
|
+
|
|
396
|
+
```markdown
|
|
397
|
+
[collection slug="applications" scope="mine" display="cards"
|
|
398
|
+
title-field="jobId"
|
|
399
|
+
fields="jobId,status,submittedAt,resume"
|
|
400
|
+
paginate
|
|
401
|
+
transitions
|
|
402
|
+
empty="You haven't applied for any roles yet." /]
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
Now each application card automatically shows the right buttons:
|
|
406
|
+
|
|
407
|
+
- `submitted` → "Withdraw"
|
|
408
|
+
- `reviewing` → "Withdraw"
|
|
409
|
+
- `interview` → (none - candidate can't withdraw from an active interview process)
|
|
410
|
+
- `offered` / `rejected` → (none - terminal)
|
|
411
|
+
|
|
412
|
+
The `resume` field renders as a clickable link to the candidate's uploaded
|
|
413
|
+
PDF; the recruiter's parallel dashboard (an interactive `display="table"`
|
|
414
|
+
block with no `scope`) sees the same files and can run their own transitions
|
|
415
|
+
(`start-review`, `schedule-interview`, etc.) thanks to the role-based
|
|
416
|
+
filter in `listTransitionsForEntry`.
|
|
417
|
+
|
|
418
|
+
`access.roles: ["admin", "hr"]` means either role is enough (and anyone more
|
|
419
|
+
senior): an `hr` user runs `start-review` although `admin` ranks above them.
|
|
420
|
+
|
|
421
|
+
Everything the candidate's block does stays inside their own rows on the
|
|
422
|
+
server: the button list and every run carry `scope: "mine"`, and a row they did
|
|
423
|
+
not create is refused with 403 - as is a direct call to `withdraw-application`
|
|
424
|
+
on someone else's row, through its `rowLevel` owner rule.
|
|
425
|
+
|
|
426
|
+
Try to skip ahead - copy an admin URL and POST it as a candidate, or POST
|
|
427
|
+
`withdraw` against an already-rejected row - and you get a clean HTTP 409
|
|
428
|
+
`Cannot apply "Withdraw" - current status is "rejected", allowed: submitted, reviewing`.
|
|
429
|
+
**The state machine is enforced server-side; the UI just renders what's legal.**
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
## 9. Employer view (bonus)
|
|
434
|
+
|
|
435
|
+
Add an employer dashboard at `content/pages/employer.md`:
|
|
436
|
+
|
|
437
|
+
```markdown
|
|
438
|
+
---
|
|
439
|
+
title: Employer Dashboard
|
|
440
|
+
visibility: employer
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
## My Jobs
|
|
444
|
+
|
|
445
|
+
[collection slug="jobs"
|
|
446
|
+
scope="mine"
|
|
447
|
+
display="table"
|
|
448
|
+
fields="title,location,type,salary,postedAt"
|
|
449
|
+
sort="postedAt"
|
|
450
|
+
order="desc"
|
|
451
|
+
cta="archive-job"
|
|
452
|
+
cta-label="Archive"
|
|
453
|
+
cta-confirm="Archive this listing?" /]
|
|
454
|
+
|
|
455
|
+
## Applications to my jobs
|
|
456
|
+
|
|
457
|
+
[collection slug="applications"
|
|
458
|
+
display="table"
|
|
459
|
+
fields="jobTitle,candidateId,status,submittedAt"
|
|
460
|
+
where_jobId_in="{{ employerJobIds }}"
|
|
461
|
+
sort="submittedAt"
|
|
462
|
+
order="desc" /]
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
The second block needs cross-collection filtering (applications matching the employer's job ids). This currently
|
|
466
|
+
needs a saved view - define one at `content/views/employer-applications.json` that does the join, then use
|
|
467
|
+
`[view slug="employer-applications" /]`. Full cross-collection joins in `[collection]` is a future enhancement.
|
|
468
|
+
|
|
469
|
+
---
|
|
470
|
+
|
|
471
|
+
## Mental model - why this works
|
|
472
|
+
|
|
473
|
+
| Need | Primitive used |
|
|
474
|
+
|-----------------------------|------------------------------------------------------------------|
|
|
475
|
+
| Per-role landing pages | `visibility` (string, or array for sibling roles) |
|
|
476
|
+
| Public job listings | `[collection]` + `api.read: public` |
|
|
477
|
+
| Filtered listings | `where_<field>_<op>` shortcode attributes |
|
|
478
|
+
| Faceted browsing UI | `[collection searchable filterable=... sortable]` (Browser) |
|
|
479
|
+
| Apply button | `[form]` + `settings.actionSlug` |
|
|
480
|
+
| Apply = create link record | Action step `createInCollection` |
|
|
481
|
+
| Job ↔ Application link | `type: "reference"` field with `displayField` + `linkTemplate` |
|
|
482
|
+
| Resume upload | `type: "file"` field with mime/size validation |
|
|
483
|
+
| "My applications" view | `[collection scope="mine"]` + hydration endpoint |
|
|
484
|
+
| Lifecycle transitions | `transition: { field, from }` on actions + `transitions` on shortcode |
|
|
485
|
+
| Row-level security | `access.rowLevel` (`mode: "owner"`) on actions |
|
|
486
|
+
| Audit trail | Form submissions persist in their original collection |
|
|
487
|
+
|
|
488
|
+
No plugin code. Everything is configuration plus Markdown. The same five building blocks (collections, forms,
|
|
489
|
+
actions, shortcodes, roles) compose into bookings, RSVPs, marketplaces, classifieds, internal task trackers, or
|
|
490
|
+
anything else where a public page needs to read structured data, accept input, and create relational records on
|
|
491
|
+
behalf of an identified user.
|
|
492
|
+
|
|
493
|
+
---
|
|
494
|
+
|
|
495
|
+
## Reference
|
|
496
|
+
|
|
497
|
+
- `docs/markdown-shortcodes.md` - full `[collection]` and `[form]` syntax
|
|
498
|
+
- `docs/api-reference.md` - filter query params, render-scope endpoint
|
|
499
|
+
- `docs/configuration.md` - page frontmatter (`visibility`, `status`, ...)
|
|
500
|
+
- `server/services/actions.js` - full list of action step types and template variables
|
|
501
|
+
- `server/services/filterEngine.js` - full operator semantics for `where_*` attributes
|