@cavuno/board 4.26.0 → 4.28.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 +134 -0
- package/dist/{_spec-EgN7VmAI.d.mts → _spec-DQvmsp5o.d.mts} +589 -67
- package/dist/{_spec-EgN7VmAI.d.ts → _spec-DQvmsp5o.d.ts} +589 -67
- package/dist/bin.mjs +7989 -10
- package/dist/{blog-BIjFcNcm.d.ts → blog-CUkvhn3c.d.ts} +1 -1
- package/dist/{blog-T7gQ0ciz.d.mts → blog-CvuObn9O.d.mts} +1 -1
- package/dist/{board-CsEAgsKh.d.ts → board-CCG1pO6r.d.ts} +25 -4
- package/dist/{board-q0svBKW3.d.mts → board-D6xOidUX.d.mts} +25 -4
- package/dist/{breadcrumbs-CFcCjuG7.d.mts → breadcrumbs-BBvJ5gq-.d.mts} +2 -2
- package/dist/{breadcrumbs-xuDid8BS.d.ts → breadcrumbs-D2Cwrju4.d.ts} +2 -2
- package/dist/browser/cavuno-board.global.min.js +1 -1
- package/dist/{chunk-DP2ZZZEO.mjs → chunk-EHSB2VAJ.mjs} +5 -0
- package/dist/doctor.js +7989 -10
- package/dist/doctor.mjs +7989 -10
- package/dist/filters.d.mts +19 -3
- package/dist/filters.d.ts +19 -3
- package/dist/filters.js +10 -1
- package/dist/filters.mjs +10 -1
- package/dist/format.d.mts +4 -4
- package/dist/format.d.ts +4 -4
- package/dist/index.d.mts +251 -14
- package/dist/index.d.ts +251 -14
- package/dist/index.js +218 -26
- package/dist/index.mjs +214 -27
- package/dist/{jobs-ub3gywNQ.d.ts → jobs-CEIuahbz.d.ts} +26 -2
- package/dist/{jobs-CfrxlDMU.d.mts → jobs-DjMcghv6.d.mts} +26 -2
- package/dist/{search-DKS7OIn2.d.mts → search-C0LiPXVb.d.mts} +1 -1
- package/dist/{search-Dx1TiAZk.d.ts → search-D-EjtZH3.d.ts} +1 -1
- package/dist/{seo-BbG2Ujyo.d.ts → seo-CjNGqZZH.d.ts} +1 -1
- package/dist/{seo-CI3wLKUW.d.mts → seo-DCQp8V1c.d.mts} +1 -1
- package/dist/seo-files.d.mts +2 -2
- package/dist/seo-files.d.ts +2 -2
- package/dist/seo.d.mts +6 -6
- package/dist/seo.d.ts +6 -6
- package/dist/server.d.mts +6 -6
- package/dist/server.d.ts +6 -6
- package/dist/server.mjs +1 -1
- package/dist/sitemap.d.mts +6 -6
- package/dist/sitemap.d.ts +6 -6
- package/dist/sitemap.mjs +1 -1
- package/dist/suggest.d.mts +2 -2
- package/dist/suggest.d.ts +2 -2
- package/package.json +2 -1
- package/skills/cavuno-board-account/SKILL.md +122 -1
- package/skills/cavuno-board-collections/SKILL.md +148 -0
- package/skills/cavuno-board-companies/SKILL.md +43 -1
- package/skills/cavuno-board-errors/SKILL.md +1 -1
- package/skills/cavuno-board-filters/SKILL.md +160 -1
- package/skills/cavuno-board-format/SKILL.md +52 -0
- package/skills/cavuno-board-paywall/SKILL.md +5 -2
- package/skills/cavuno-board-post-a-job/SKILL.md +88 -0
- package/skills/cavuno-board-salaries/SKILL.md +7 -0
- package/skills/manifest.json +11 -4
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cavuno-board-filters
|
|
3
|
-
description: Listing-filter contracts with @cavuno/board. Use for job filter controls, sort controls, listing URL validation, or taxonomy-backed filter options.
|
|
3
|
+
description: Listing-filter contracts with @cavuno/board. Use for job filter controls, job and company custom-field filters, profile-field filters, collection choices, sort controls, listing URL validation, or taxonomy-backed filter options.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Listing filters
|
|
@@ -41,6 +41,33 @@ const page = await board.jobs.list({
|
|
|
41
41
|
Company slugs are the URL identity. Map `filters.company` directly to
|
|
42
42
|
`companySlug` in `jobs.list` queries or `jobs.search` filters.
|
|
43
43
|
|
|
44
|
+
## Filter by category in search
|
|
45
|
+
|
|
46
|
+
An "All filters" panel can carry a category facet next to seniority, company,
|
|
47
|
+
workplace and employment type. `parseListingFilters` reads `category` the same
|
|
48
|
+
way as company (repeated or comma-separated slugs, first 10 kept). Send it as
|
|
49
|
+
`filters.categories` in a `jobs.search` body; a job matches when it has any of
|
|
50
|
+
the given categories, and the other filters still apply.
|
|
51
|
+
|
|
52
|
+
```ts snippet
|
|
53
|
+
const results = await board.jobs.search({
|
|
54
|
+
query: filters.q,
|
|
55
|
+
sort: filters.sort,
|
|
56
|
+
filters: {
|
|
57
|
+
categories: filters.category,
|
|
58
|
+
seniority: filters.seniority,
|
|
59
|
+
companySlug: filters.company,
|
|
60
|
+
remoteOption: filters.remoteOption ? [filters.remoteOption] : undefined,
|
|
61
|
+
},
|
|
62
|
+
limit: 20,
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`categories` accepts a term's `sourceSlug` or its board-language
|
|
67
|
+
`canonicalSlug`; an unknown slug matches no jobs. `jobs.list` takes a single
|
|
68
|
+
`category` instead, which seeds a category landing page rather than filtering
|
|
69
|
+
a search. Use `jobs.search` whenever the category is one facet among several.
|
|
70
|
+
|
|
44
71
|
## Render controls from the vocabulary
|
|
45
72
|
|
|
46
73
|
```ts snippet
|
|
@@ -123,6 +150,129 @@ const { items } = await board.search.suggest({
|
|
|
123
150
|
The host router owns URL serialization and saved-filter persistence.
|
|
124
151
|
Locations come from `board.taxonomy.places` rather than a static filter export.
|
|
125
152
|
|
|
153
|
+
## Filter jobs by a job custom field
|
|
154
|
+
|
|
155
|
+
The operator's job custom fields arrive as definitions on
|
|
156
|
+
`board.context().customFields.job`. Expose a field as a filter only when the
|
|
157
|
+
operator intends it as one: keep an explicit list of filter keys in your app
|
|
158
|
+
config. Do not render a dropdown for every select field automatically; most
|
|
159
|
+
custom fields are details for the job page, not listing facets.
|
|
160
|
+
|
|
161
|
+
```ts snippet
|
|
162
|
+
const context = await board.context();
|
|
163
|
+
|
|
164
|
+
// The operator picked these; everything else stays a job-page detail.
|
|
165
|
+
const FILTER_FIELD_KEYS = ['work_type'];
|
|
166
|
+
const filterFields = context.customFields.job.filter(
|
|
167
|
+
(field) =>
|
|
168
|
+
FILTER_FIELD_KEYS.includes(field.key) &&
|
|
169
|
+
(field.type === 'single_select' || field.type === 'multi_select'));
|
|
170
|
+
|
|
171
|
+
// Controls: option KEYS are the values, option labels are the text.
|
|
172
|
+
const controls = filterFields.map((field) => ({
|
|
173
|
+
key: field.key,
|
|
174
|
+
label: field.label,
|
|
175
|
+
options: (field.options ?? []).map((option) => ({
|
|
176
|
+
value: option.key,
|
|
177
|
+
label: option.label,
|
|
178
|
+
})),
|
|
179
|
+
}));
|
|
180
|
+
|
|
181
|
+
// URL state holds option keys (`/jobs?work_type=contract`), never labels.
|
|
182
|
+
// Drop anything that is not a current option before it reaches the API.
|
|
183
|
+
const customFields = filterFields.flatMap((field) => {
|
|
184
|
+
const values = searchParams
|
|
185
|
+
.getAll(field.key)
|
|
186
|
+
.filter((value) => field.options?.some((option) => option.key === value));
|
|
187
|
+
return values.length > 0 ? [{ key: field.key, values }] : [];
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const results = await board.jobs.search({
|
|
191
|
+
query: filters.q,
|
|
192
|
+
filters: {
|
|
193
|
+
seniority: filters.seniority,
|
|
194
|
+
customFields: customFields.length > 0 ? customFields : undefined,
|
|
195
|
+
},
|
|
196
|
+
limit: 20,
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Clauses are AND-matched; values inside one clause are alternatives. Each
|
|
201
|
+
request takes at most 10 clauses of 10 values. An unknown key or an option key
|
|
202
|
+
the field no longer offers returns `invalid_filter`, so validate URL input
|
|
203
|
+
against the definitions as above. Boolean and number fields filter the same way
|
|
204
|
+
with `values: [true]` or `values: [3]`. Labels change when the operator renames
|
|
205
|
+
an option; keys do not, so bookmarked URLs keep working.
|
|
206
|
+
|
|
207
|
+
## Build filters from public profile fields
|
|
208
|
+
|
|
209
|
+
`board.profileFields.retrieve('company')` and `retrieve('candidate')` return only public scalar and collection-reference definitions. Use each stored field key for filtering. For a collection reference, page or search its active public choices and submit the returned record `id`; the optional `logoUrl` is display metadata.
|
|
210
|
+
|
|
211
|
+
```ts snippet
|
|
212
|
+
const companyFields = await board.profileFields.retrieve('company');
|
|
213
|
+
const technologyChoices = await board.profileFields.choices(
|
|
214
|
+
'company',
|
|
215
|
+
'technologies',
|
|
216
|
+
{ search: 'typescript', limit: 20 });
|
|
217
|
+
|
|
218
|
+
const companies = await board.companies.search({
|
|
219
|
+
query: 'engineering',
|
|
220
|
+
customFields: [{ key: 'member_tier', values: ['gold'] }],
|
|
221
|
+
objectReferences: [
|
|
222
|
+
{ key: 'technologies', recordIds: [technologyChoices.data[0]!.id] },
|
|
223
|
+
],
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const candidateFields = await board.profileFields.retrieve('candidate');
|
|
227
|
+
const certifications = await board.profileFields.choices(
|
|
228
|
+
'candidate',
|
|
229
|
+
'certifications',
|
|
230
|
+
{ limit: 20 });
|
|
231
|
+
const talent = await board.talent.list({
|
|
232
|
+
customFields: [{ key: 'available_for_mentoring', values: [true] }],
|
|
233
|
+
objectReferences: [
|
|
234
|
+
{ key: 'certifications', recordIds: [certifications.data[0]!.id] },
|
|
235
|
+
],
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
void companyFields.definitions;
|
|
239
|
+
void candidateFields.referenceDefinitions;
|
|
240
|
+
void companies.data;
|
|
241
|
+
void talent.data;
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Clauses are AND-matched. Values or record IDs inside one clause are alternatives. Unknown, private, invalid, or archived choices return `invalid_filter`; each array accepts at most 10 clauses and each clause at most 10 choices. Job search supports scalar `customFields` under `filters`, but job collection references are outside this surface.
|
|
245
|
+
|
|
246
|
+
## Filter jobs by job and company custom fields
|
|
247
|
+
|
|
248
|
+
`jobs.search` takes two custom-field filters. `filters.customFields` matches the job's own fields, defined in `board.context().customFields.job`. `filters.companyCustomFields` matches the public profile fields of the job's company, defined in `board.profileFields.retrieve('company')`. Both use the clause shape above; the two lists never share keys, so a job field and a company field may both be called `type`.
|
|
249
|
+
|
|
250
|
+
```ts snippet
|
|
251
|
+
const [context, companyFields] = await Promise.all([
|
|
252
|
+
board.context(),
|
|
253
|
+
board.profileFields.retrieve('company'),
|
|
254
|
+
]);
|
|
255
|
+
const opportunityType = context.customFields.job.find(
|
|
256
|
+
(field) => field.key === 'opportunity_type');
|
|
257
|
+
const employerType = companyFields.definitions.find(
|
|
258
|
+
(field) => field.key === 'employer_type');
|
|
259
|
+
|
|
260
|
+
const results = await board.jobs.search({
|
|
261
|
+
query: filters.q,
|
|
262
|
+
filters: {
|
|
263
|
+
customFields: [{ key: 'opportunity_type', values: ['identified'] }],
|
|
264
|
+
companyCustomFields: [{ key: 'employer_type', values: ['acco'] }],
|
|
265
|
+
},
|
|
266
|
+
limit: 20,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
void opportunityType;
|
|
270
|
+
void employerType;
|
|
271
|
+
void results.data;
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Render each dropdown from the definition's `options` in order: show `label`, submit `key`, and keep the key in the URL so shared links survive a label change. Accept only keys the current definition offers; drop anything else from a hand-typed URL. Only `jobs.search` takes these filters. A company's jobs are re-indexed automatically when its values change. Worked example: [Custom field filters and badges](https://cavuno.com/docs/sdk/cookbook/custom-field-filters-and-badges).
|
|
275
|
+
|
|
126
276
|
## Completion gate
|
|
127
277
|
|
|
128
278
|
Finish only after every applicable check passes:
|
|
@@ -135,7 +285,16 @@ Finish only after every applicable check passes:
|
|
|
135
285
|
relevance, newest, and salary high.
|
|
136
286
|
- Category and skill filtering sends `sourceSlug`, while links use
|
|
137
287
|
`canonicalSlug`.
|
|
288
|
+
- `/jobs?category=engineering,design` sends
|
|
289
|
+
`filters.categories: ['engineering', 'design']` to `jobs.search`.
|
|
138
290
|
- Every paged taxonomy request forwards the previous opaque `nextCursor`.
|
|
291
|
+
- Job custom-field controls exist only for keys the operator chose as
|
|
292
|
+
filters, read their options from `board.context().customFields.job`, and send
|
|
293
|
+
option keys in `filters.customFields`; an unknown option in the URL never
|
|
294
|
+
reaches the API.
|
|
295
|
+
- Company and talent custom controls come from `board.profileFields`; private definitions and archived choices never become filter options.
|
|
296
|
+
- Profile-filter requests use stored scalar values and returned collection entry IDs, not display labels.
|
|
297
|
+
- Job-field dropdowns send `filters.customFields` and company-field dropdowns send `filters.companyCustomFields` to `jobs.search`, both built from the current definitions; a stale option key in the URL is dropped, not sent.
|
|
139
298
|
|
|
140
299
|
## Cavuno SDK reference
|
|
141
300
|
|
|
@@ -96,6 +96,55 @@ the same card view-model as listings — do not convert a full job client-side.
|
|
|
96
96
|
const card = toJobCardVM(saved.job, { language, /* … */ });
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
+
## Badges on job cards
|
|
100
|
+
|
|
101
|
+
Every job card (listings, search, company jobs, similar, recommended, and
|
|
102
|
+
saved jobs) carries `customFieldValues` for the job and
|
|
103
|
+
`company.customFieldValues` for its company. Both hold only select, boolean,
|
|
104
|
+
and number values keyed by field key; select values are option keys. Text
|
|
105
|
+
fields stay on the full job and company, and private company fields never
|
|
106
|
+
appear. Both are `{}` when empty, so a card needs no extra request to show a
|
|
107
|
+
badge.
|
|
108
|
+
|
|
109
|
+
```ts snippet
|
|
110
|
+
import { resolveCustomFieldDisplay } from '@cavuno/board/format';
|
|
111
|
+
|
|
112
|
+
const context = await board.context();
|
|
113
|
+
const companyFields = await board.profileFields.retrieve('company');
|
|
114
|
+
|
|
115
|
+
// Badge only the fields the operator wants on cards (an app-config list).
|
|
116
|
+
const JOB_BADGE_KEYS = ['work_type'];
|
|
117
|
+
const jobBadges = resolveCustomFieldDisplay(
|
|
118
|
+
context.language,
|
|
119
|
+
context.customFields.job.filter((field) => JOB_BADGE_KEYS.includes(field.key)),
|
|
120
|
+
card.customFieldValues).flatMap((entry) => {
|
|
121
|
+
switch (entry.kind) {
|
|
122
|
+
case 'text': // a single_select resolves to its option label
|
|
123
|
+
return [entry.value];
|
|
124
|
+
case 'multi_select':
|
|
125
|
+
return entry.values;
|
|
126
|
+
case 'boolean':
|
|
127
|
+
return entry.value ? [entry.label] : [];
|
|
128
|
+
case 'number':
|
|
129
|
+
return [`${entry.label}: ${entry.value}`];
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Company badge: resolve the stored option key to its public label.
|
|
134
|
+
const employerField = companyFields.definitions.find(
|
|
135
|
+
(field) => field.key === 'employer_category');
|
|
136
|
+
const employerKey = card.company?.customFieldValues.employer_category;
|
|
137
|
+
const employerBadge =
|
|
138
|
+
typeof employerKey === 'string'
|
|
139
|
+
? (employerField?.options?.find((option) => option.key === employerKey)
|
|
140
|
+
?.label ?? null)
|
|
141
|
+
: null;
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Load the definitions once per page (both are cacheable board metadata), not
|
|
145
|
+
once per card. A key with no current definition or option resolves to nothing:
|
|
146
|
+
drop the badge rather than printing the raw key.
|
|
147
|
+
|
|
99
148
|
## Completion gate
|
|
100
149
|
|
|
101
150
|
Finish only after every applicable check passes:
|
|
@@ -107,6 +156,9 @@ Finish only after every applicable check passes:
|
|
|
107
156
|
- Open ranges return `bound: 'from' | 'upTo'`; the application adds open-range
|
|
108
157
|
chrome and composes order itself.
|
|
109
158
|
- Saved jobs arrive as cards; no `fullJobToCard` conversion.
|
|
159
|
+
- Card badges read `card.customFieldValues` / `card.company.customFieldValues`
|
|
160
|
+
and resolve option keys to labels through the definitions; no raw key is
|
|
161
|
+
shown and no card triggers a per-job detail fetch.
|
|
110
162
|
- Salary-stat formatters receive the detail's `currency` (never invent USD).
|
|
111
163
|
- Formatting changes neither salary amounts nor invents chrome words.
|
|
112
164
|
|
|
@@ -26,7 +26,7 @@ for (const offer of offers) {
|
|
|
26
26
|
}
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
`offerType` is `'recurring' | 'lifetime'`; post the selected `offerKey` to checkout.
|
|
29
|
+
`offerType` is `'recurring' | 'lifetime'`; post the selected `offerKey` to checkout unchanged. Treat it as opaque: newer offers use plan IDs, while older offers may use tier keys. Never derive it from the billing interval.
|
|
30
30
|
|
|
31
31
|
Gated catalog reads expose withheld inventory as `gatedCount`. The same jobs endpoint returns the entitled view when called with the candidate bearer token.
|
|
32
32
|
|
|
@@ -44,8 +44,11 @@ if ((page.gatedCount ?? 0) > 0) {
|
|
|
44
44
|
```ts snippet
|
|
45
45
|
import { loadStripe } from '@stripe/stripe-js';
|
|
46
46
|
|
|
47
|
+
const { data: checkoutOffers } = await board.paywall.offers();
|
|
48
|
+
const selectedOffer = checkoutOffers[0]; // Or the offer selected in your UI.
|
|
49
|
+
if (!selectedOffer) throw new Error('No access offers available');
|
|
47
50
|
const kit = await board.me.access.checkout({
|
|
48
|
-
offerKey:
|
|
51
|
+
offerKey: selectedOffer.offerKey,
|
|
49
52
|
returnPath: '/account/access',
|
|
50
53
|
colorMode: 'light',
|
|
51
54
|
});
|
|
@@ -24,6 +24,93 @@ for (const plan of plans) {
|
|
|
24
24
|
|
|
25
25
|
Pass the chosen `plan.id` as `submission.selectedPlan`. Invoice-only plans collect `invoiceBilling` rather than immediate payment.
|
|
26
26
|
|
|
27
|
+
## Render the form from the operator's layout
|
|
28
|
+
|
|
29
|
+
The operator orders the job form and chooses which fields show and which are required. `board.context().forms.job` is that form as one ordered list; render it top to bottom and never hard-code the order. Skip entries with `visible: false`: a hidden field is not collected and is never required.
|
|
30
|
+
|
|
31
|
+
```ts snippet
|
|
32
|
+
const { forms, jobForm } = await board.context();
|
|
33
|
+
|
|
34
|
+
for (const field of forms.job) {
|
|
35
|
+
if (!field.visible) continue;
|
|
36
|
+
switch (field.kind) {
|
|
37
|
+
case 'builtin':
|
|
38
|
+
renderBuiltin(field.key, { required: field.required, locked: field.locked });
|
|
39
|
+
break;
|
|
40
|
+
case 'custom':
|
|
41
|
+
renderCustomField(field.definition, { required: field.required });
|
|
42
|
+
break;
|
|
43
|
+
case 'collection':
|
|
44
|
+
renderCollectionPicker(field.definition, { required: field.required });
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
A `builtin` entry is drawn by your own control for its `key`. A `locked` built-in is always shown and required; `lockReason` says why (for example `google_required` for the title and description Google for Jobs needs). Skip a built-in key you do not recognise. The keys map to submission fields:
|
|
51
|
+
|
|
52
|
+
| `key` | Submission fields |
|
|
53
|
+
| --- | --- |
|
|
54
|
+
| `employmentType` | `employmentType`, from `jobForm.employmentType.allowedOptions` |
|
|
55
|
+
| `seniority` | `seniority`, from `jobForm.seniority.allowedOptions` |
|
|
56
|
+
| `title` | `title` |
|
|
57
|
+
| `workArrangement` | `remoteOption`, from `jobForm.workArrangement.allowedOptions` |
|
|
58
|
+
| `location` | `officeLocations`, `inOfficePeriod`, `inOfficeFrequency` |
|
|
59
|
+
| `remoteEligibility` | `remoteWorkingPermits`, `remoteTimezones`, and `remoteSponsorship` when `jobForm.sponsorship.visible` |
|
|
60
|
+
| `description` | `description` |
|
|
61
|
+
| `salary` | `salaryRangeEnabled`, `salaryMin`, `salaryMax`, `salaryTimeframe`, `salaryCurrency` |
|
|
62
|
+
| `applyMethod` | `applicationUrl` |
|
|
63
|
+
| `company` | `companyName`, `companyWebsite`, and the resolved logo |
|
|
64
|
+
|
|
65
|
+
`contactName`, `contactEmail`, and the plan are not layout fields; always collect them. `jobForm` still carries each built-in's allowed options and salary bounds.
|
|
66
|
+
|
|
67
|
+
A `custom` entry renders from `definition` (`label`, `type`, `options`, `helpText`) and submits under `submission.customFieldValues[field.key]`; store option keys, never labels. A `collection` entry renders a picker capped by `definition.maxSelections` (one when `multiple` is false) with choices from `board.jobs.collectionChoices(field.key)`. The anonymous funnel takes no collection values, so collection fields belong on the signed-in employer job form (`board.me.companies.jobs.create`, `collectionValues`). Custom and collection rendering rules live in `cavuno-board-collections`.
|
|
68
|
+
|
|
69
|
+
When a collection definition has `allowOverrides`, the employer job form can give each selected entry its own wording for this job — "Unlimited PTO" in place of the shared "Annual leave", for example. Offer an edit action per selected entry with a title (up to 160 characters) and a plain-text description (up to 2,000 characters), and send them as `collectionOverrides`, one item per entry that has wording:
|
|
70
|
+
|
|
71
|
+
```ts snippet
|
|
72
|
+
const created = await board.me.companies.jobs.create('acme', {
|
|
73
|
+
title: 'Head chef',
|
|
74
|
+
description: 'Run the kitchen.',
|
|
75
|
+
collectionValues: { benefits: [annualLeaveId] },
|
|
76
|
+
collectionOverrides: [
|
|
77
|
+
{ fieldKey: 'benefits', recordId: annualLeaveId, title: 'Unlimited PTO' },
|
|
78
|
+
],
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await board.me.companies.jobs.update('acme', created.id, {
|
|
82
|
+
collectionOverrides: [],
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The list replaces the job's wording: omit it on `update` to keep what is stored, and send `[]` to restore every default. Wording for an entry the job does not select, or on a field without `allowOverrides`, is rejected with `jobs_constraint_violation`; `details.violations[].params.fieldKey` names the field, so show the message under it. Deselecting an entry drops its wording. The job's collection entries then return the resolved `title` and `description` to render.
|
|
87
|
+
|
|
88
|
+
Validate `required` client-side from these entries before submitting, so the poster sees a missing field before the request.
|
|
89
|
+
|
|
90
|
+
## Pick office locations
|
|
91
|
+
|
|
92
|
+
Back the `location` field with `board.locations.search`, a worldwide search over countries, regions, cities, and localities. `session` is required: generate one per location-field interaction, reuse it across keystrokes and the pick-time `board.locations.resolve` call, then generate a new one for the next interaction. Renew it before 50 search requests or after 180 seconds. Pass `jobForm.location.allowedCountries` as `country` to narrow suggestions to the board's allowed countries.
|
|
93
|
+
|
|
94
|
+
```ts snippet
|
|
95
|
+
const session = crypto.randomUUID();
|
|
96
|
+
const { data: locations } = await board.locations.search({
|
|
97
|
+
q: 'berl',
|
|
98
|
+
session,
|
|
99
|
+
country: jobForm.location.allowedCountries?.join(','),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const picked = locations[0];
|
|
103
|
+
if (picked) {
|
|
104
|
+
const location = await board.locations.resolve({
|
|
105
|
+
locationId: picked.id,
|
|
106
|
+
session,
|
|
107
|
+
});
|
|
108
|
+
submission.officeLocations.push({ locationId: location.id });
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Show `fullName` in the input once picked and `contextLabel` as the secondary line in the result list. Resolve the picked `id` with the same session before committing the selection, then send the resolved `id` as `locationId` in the later job submission. `board.taxonomy.places.list({ q })` ids are accepted as `locationId` too. An id the server does not recognise throws `locations_invalid_id`, and `locations_unavailable` means the search is temporarily down, so let the poster retry.
|
|
113
|
+
|
|
27
114
|
## Resolve a logo
|
|
28
115
|
|
|
29
116
|
Both logo paths return a stored `publicUrl` for `create`. The SDK owns multipart encoding.
|
|
@@ -111,6 +198,7 @@ Bare-email billing lookup is intentionally absent because it would expose whethe
|
|
|
111
198
|
|
|
112
199
|
## Completion gate
|
|
113
200
|
|
|
201
|
+
- The form renders `forms.job` in order: hidden entries are absent, required and locked entries block submit while empty, and an unknown built-in key is skipped.
|
|
114
202
|
- Every `create` call handles all four statuses.
|
|
115
203
|
- Paid checkout redirects through `checkoutUrl`; the job appears only after webhook completion.
|
|
116
204
|
- Free unmoderated submission returns a resolving `jobSlug`.
|
|
@@ -37,6 +37,13 @@ const companies = await board.salaries.companies.list();
|
|
|
37
37
|
region, and city nesting from each `SalaryLocation.parentSlug`; top-level rows
|
|
38
38
|
have `parentSlug: null`.
|
|
39
39
|
|
|
40
|
+
Every place row in a salary response (location index nodes, `topLocations`,
|
|
41
|
+
child, sibling, and other locations) carries `placeName` and `placeLabel`.
|
|
42
|
+
Render `placeLabel` wherever places are listed: it tells a city from the
|
|
43
|
+
region it shares a name with ("New York, NY" under "New York", "Washington,
|
|
44
|
+
DC" beside "Washington"). Keep `placeName` for data you pass on, such as
|
|
45
|
+
JSON-LD.
|
|
46
|
+
|
|
40
47
|
## Retrieve detail and canonicalize slugs
|
|
41
48
|
|
|
42
49
|
Every axis `retrieve(slug)` accepts an inbound English or board-language slug.
|
package/skills/manifest.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "4.
|
|
2
|
+
"version": "4.28.0",
|
|
3
3
|
"skills": [
|
|
4
4
|
{
|
|
5
5
|
"name": "cavuno-board-account",
|
|
6
|
-
"description": "Candidate self-service boundary with @cavuno/board. Use for account, profile, resume onboarding, recommended jobs, avatar, experience, education, skills, languages, or notification preferences.",
|
|
6
|
+
"description": "Candidate self-service boundary with @cavuno/board. Use for account, profile, custom profile fields, collection selections, resume onboarding, recommended jobs, avatar, experience, education, skills, languages, or notification preferences.",
|
|
7
7
|
"path": "skills/cavuno-board-account/SKILL.md",
|
|
8
8
|
"framework": null,
|
|
9
9
|
"category": "core"
|
|
@@ -36,9 +36,16 @@
|
|
|
36
36
|
"framework": null,
|
|
37
37
|
"category": "core"
|
|
38
38
|
},
|
|
39
|
+
{
|
|
40
|
+
"name": "cavuno-board-collections",
|
|
41
|
+
"description": "Custom profile fields and collection references with @cavuno/board. Use for rendering company, talent or job custom fields, collection entries, benefits/technologies/certification sections, or collection-backed filters.",
|
|
42
|
+
"path": "skills/cavuno-board-collections/SKILL.md",
|
|
43
|
+
"framework": null,
|
|
44
|
+
"category": "core"
|
|
45
|
+
},
|
|
39
46
|
{
|
|
40
47
|
"name": "cavuno-board-companies",
|
|
41
|
-
"description": "Company catalog reads with @cavuno/board. Use for company indexes, market archives, profiles, company job rails, similar companies,
|
|
48
|
+
"description": "Company catalog reads with @cavuno/board. Use for company indexes, market archives, profiles, company job rails, similar companies, company salary pages, or employer job drafts.",
|
|
42
49
|
"path": "skills/cavuno-board-companies/SKILL.md",
|
|
43
50
|
"framework": null,
|
|
44
51
|
"category": "core"
|
|
@@ -52,7 +59,7 @@
|
|
|
52
59
|
},
|
|
53
60
|
{
|
|
54
61
|
"name": "cavuno-board-filters",
|
|
55
|
-
"description": "Listing-filter contracts with @cavuno/board. Use for job filter controls, sort controls, listing URL validation, or taxonomy-backed filter options.",
|
|
62
|
+
"description": "Listing-filter contracts with @cavuno/board. Use for job filter controls, job and company custom-field filters, profile-field filters, collection choices, sort controls, listing URL validation, or taxonomy-backed filter options.",
|
|
56
63
|
"path": "skills/cavuno-board-filters/SKILL.md",
|
|
57
64
|
"framework": null,
|
|
58
65
|
"category": "core"
|