@cavuno/board 4.26.0 → 4.27.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 +89 -0
- package/dist/{_spec-EgN7VmAI.d.mts → _spec-B_pBNVUd.d.mts} +487 -47
- package/dist/{_spec-EgN7VmAI.d.ts → _spec-B_pBNVUd.d.ts} +487 -47
- package/dist/bin.mjs +7989 -10
- package/dist/{blog-BIjFcNcm.d.ts → blog-DzGRUsj5.d.ts} +1 -1
- package/dist/{blog-T7gQ0ciz.d.mts → blog-fhXRik6k.d.mts} +1 -1
- package/dist/{board-q0svBKW3.d.mts → board-DEiEHcpe.d.mts} +25 -4
- package/dist/{board-CsEAgsKh.d.ts → board-fVAXZ8T5.d.ts} +25 -4
- package/dist/{breadcrumbs-xuDid8BS.d.ts → breadcrumbs-DNaaP3xa.d.ts} +2 -2
- package/dist/{breadcrumbs-CFcCjuG7.d.mts → breadcrumbs-qE3CD--N.d.mts} +2 -2
- package/dist/browser/cavuno-board.global.min.js +1 -1
- package/dist/{chunk-DP2ZZZEO.mjs → chunk-DUTQOLOF.mjs} +1 -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 +173 -13
- package/dist/index.d.ts +173 -13
- package/dist/index.js +175 -23
- package/dist/index.mjs +175 -24
- package/dist/{jobs-ub3gywNQ.d.ts → jobs-BRysn2m0.d.ts} +25 -3
- package/dist/{jobs-CfrxlDMU.d.mts → jobs-R-h3A3kl.d.mts} +25 -3
- package/dist/{search-Dx1TiAZk.d.ts → search-BK_zHHNQ.d.ts} +1 -1
- package/dist/{search-DKS7OIn2.d.mts → search-BlB690ma.d.mts} +1 -1
- package/dist/{seo-CI3wLKUW.d.mts → seo-0mbZ7M_E.d.mts} +1 -1
- package/dist/{seo-BbG2Ujyo.d.ts → seo-C7gMl6Y7.d.ts} +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 +95 -1
- package/skills/cavuno-board-collections/SKILL.md +148 -0
- package/skills/cavuno-board-companies/SKILL.md +43 -1
- package/skills/cavuno-board-filters/SKILL.md +71 -1
- package/skills/cavuno-board-paywall/SKILL.md +5 -2
- package/skills/cavuno-board-post-a-job/SKILL.md +64 -0
- package/skills/manifest.json +11 -4
package/dist/sitemap.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { BoardSdk } from './index.js';
|
|
2
|
-
import './_spec-
|
|
3
|
-
import './search-
|
|
4
|
-
import './board-
|
|
5
|
-
import './jobs-
|
|
6
|
-
import './seo-
|
|
7
|
-
import './blog-
|
|
2
|
+
import './_spec-B_pBNVUd.js';
|
|
3
|
+
import './search-BK_zHHNQ.js';
|
|
4
|
+
import './board-fVAXZ8T5.js';
|
|
5
|
+
import './jobs-BRysn2m0.js';
|
|
6
|
+
import './seo-C7gMl6Y7.js';
|
|
7
|
+
import './blog-DzGRUsj5.js';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Sitemap primitives — the pure XML + bucket-filename logic behind a board
|
package/dist/sitemap.mjs
CHANGED
package/dist/suggest.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { h as SuggestionItem, b as SearchSuggestQuery, F as FetchOptions, g as SuggestResult } from './search-
|
|
2
|
-
import './_spec-
|
|
1
|
+
import { h as SuggestionItem, b as SearchSuggestQuery, F as FetchOptions, g as SuggestResult } from './search-BlB690ma.mjs';
|
|
2
|
+
import './_spec-B_pBNVUd.mjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* `@cavuno/board/suggest` — framework-agnostic headless search-suggest
|
package/dist/suggest.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { h as SuggestionItem, b as SearchSuggestQuery, F as FetchOptions, g as SuggestResult } from './search-
|
|
2
|
-
import './_spec-
|
|
1
|
+
import { h as SuggestionItem, b as SearchSuggestQuery, F as FetchOptions, g as SuggestResult } from './search-BK_zHHNQ.js';
|
|
2
|
+
import './_spec-B_pBNVUd.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* `@cavuno/board/suggest` — framework-agnostic headless search-suggest
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cavuno/board",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.27.0",
|
|
4
4
|
"description": "Build custom job boards and careers pages with Cavuno’s TypeScript SDK.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"api-client",
|
|
@@ -236,6 +236,7 @@
|
|
|
236
236
|
"@playwright/test": "^1.59.1",
|
|
237
237
|
"@types/node": "24.9.1",
|
|
238
238
|
"esbuild": "^0.27.7",
|
|
239
|
+
"parse5": "7.3.0",
|
|
239
240
|
"publint": "^0.3.12",
|
|
240
241
|
"tsup": "^8.4.0",
|
|
241
242
|
"tsx": "^4.19.2",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cavuno-board-account
|
|
3
|
-
description: Candidate self-service boundary with @cavuno/board. Use for account, profile, resume onboarding, recommended jobs, avatar, experience, education, skills, languages, or notification preferences.
|
|
3
|
+
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.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Candidate self-service boundary
|
|
@@ -41,6 +41,97 @@ const { available } = await board.me.profile.handleAvailable('jane');
|
|
|
41
41
|
|
|
42
42
|
Profile updates are merge-patches. Handle availability is advisory—your current handle counts as available—and the write re-checks uniqueness.
|
|
43
43
|
|
|
44
|
+
## Render the profile form from the operator's layout
|
|
45
|
+
|
|
46
|
+
`board.context().forms.talent` is the candidate profile form as one ordered list, set by the operator. Render it in order and skip entries with `visible: false`. Each entry has `required`; validate it in the form before saving.
|
|
47
|
+
|
|
48
|
+
```ts snippet
|
|
49
|
+
const { forms } = await board.context();
|
|
50
|
+
|
|
51
|
+
for (const field of forms.talent) {
|
|
52
|
+
if (!field.visible) continue;
|
|
53
|
+
if (field.kind === 'builtin') {
|
|
54
|
+
renderProfileBuiltin(field.key, { required: field.required });
|
|
55
|
+
} else if (field.definition.editableByOwner) {
|
|
56
|
+
renderProfileField(field, { required: field.required });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Built-in keys map to the methods below: `name` → `displayName` and `headline`, `location`, `jobSearchStatus`, `bio` → `profile.update`; `email` → `requestEmailChange`; `avatar` → `uploadAvatar`; `experience` and `education` → their CRUD methods; `skills` and `languages` → `updateSkills` / `updateLanguages`. `name` and `email` are locked (always shown and required). Skip a built-in key you do not recognise.
|
|
62
|
+
|
|
63
|
+
`custom` entries are scalar profile fields (`updateCustomFields`) and `collection` entries are collection fields (`updateObjectReferences`); both carry their public `definition` inline. Render only definitions with `editableByOwner: true` as inputs. The layout lists public fields only; private owner-editable fields come from `retrieveCustomFields` and `retrieveObjectReferences` below. A collection definition with `required: true` needs at least one selection while shown.
|
|
64
|
+
|
|
65
|
+
## Custom fields and collection selections
|
|
66
|
+
|
|
67
|
+
Scalar custom-field updates are additive. Render controls from the returned definitions, preserve their value types, and send only owner-editable keys. Omitted keys remain unchanged.
|
|
68
|
+
|
|
69
|
+
Collection-selection updates replace the complete editable set. Read before writing when the user means to retain existing selections. Each resolved selection includes its display title, optional description and logo, shared attributes, and profile-specific details. Use `listObjectReferenceChoices` to search the active choices for one editable field; do not guess record IDs from names.
|
|
70
|
+
|
|
71
|
+
```ts snippet
|
|
72
|
+
const customFields = await board.me.profile.retrieveCustomFields();
|
|
73
|
+
await board.me.profile.updateCustomFields({
|
|
74
|
+
values: { membership_number: 'A-1234' },
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const references = await board.me.profile.retrieveObjectReferences();
|
|
78
|
+
const choices = await board.me.profile.listObjectReferenceChoices(
|
|
79
|
+
'certifications',
|
|
80
|
+
{ search: 'professional', limit: 20 });
|
|
81
|
+
await board.me.profile.updateObjectReferences({
|
|
82
|
+
selections: [
|
|
83
|
+
...references.selections
|
|
84
|
+
.filter((selection) => selection.fieldKey !== 'certifications')
|
|
85
|
+
.map(({ fieldKey, recordId, values, entries }) => ({
|
|
86
|
+
fieldKey,
|
|
87
|
+
recordId,
|
|
88
|
+
values,
|
|
89
|
+
entries,
|
|
90
|
+
})),
|
|
91
|
+
{
|
|
92
|
+
fieldKey: 'certifications',
|
|
93
|
+
recordId: choices.data[0]!.id,
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Approved company members use the parallel company methods. The company slug comes before the body or field key.
|
|
100
|
+
|
|
101
|
+
```ts snippet
|
|
102
|
+
const companyFields = await board.me.companies.retrieveCustomFields('acme');
|
|
103
|
+
await board.me.companies.updateCustomFields('acme', {
|
|
104
|
+
values: { founded_year: 2018 },
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const companyReferences =
|
|
108
|
+
await board.me.companies.retrieveObjectReferences('acme');
|
|
109
|
+
const technologies =
|
|
110
|
+
await board.me.companies.listObjectReferenceChoices(
|
|
111
|
+
'acme',
|
|
112
|
+
'technologies',
|
|
113
|
+
{ search: 'typescript', limit: 20 });
|
|
114
|
+
await board.me.companies.updateObjectReferences('acme', {
|
|
115
|
+
selections: [
|
|
116
|
+
...companyReferences.selections
|
|
117
|
+
.filter((selection) => selection.fieldKey !== 'technologies')
|
|
118
|
+
.map(({ fieldKey, recordId, values, entries }) => ({
|
|
119
|
+
fieldKey,
|
|
120
|
+
recordId,
|
|
121
|
+
values,
|
|
122
|
+
entries,
|
|
123
|
+
})),
|
|
124
|
+
{
|
|
125
|
+
fieldKey: 'technologies',
|
|
126
|
+
recordId: technologies.data[0]!.id,
|
|
127
|
+
values: { years: 4 },
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Collection creation, collection entry mutation, and bulk imports are operator API operations. Keep that API key on the server; the Board SDK intentionally exposes only public discovery and signed-in owner reads and writes.
|
|
134
|
+
|
|
44
135
|
## Experience and education
|
|
45
136
|
|
|
46
137
|
Both collections are id-keyed CRUD with merge-patch updates. Experience creation requires `title`, `companyName`, and `startDate`; education creation requires `institutionName`.
|
|
@@ -161,7 +252,10 @@ await board.me.notificationPreferences.unsubscribeWithToken({
|
|
|
161
252
|
|
|
162
253
|
## Completion gate
|
|
163
254
|
|
|
255
|
+
- The profile form follows `forms.talent`: order matches, hidden fields are absent, and required fields block save while empty.
|
|
164
256
|
- Profile update is visible after retrieval.
|
|
257
|
+
- Scalar field updates preserve omitted values; collection selection updates preserve every selection the user meant to keep.
|
|
258
|
+
- Choice searches use the returned record `id`, and an optional returned `logoUrl` renders without a separate logo map.
|
|
165
259
|
- Adding a skill preserves every existing skill.
|
|
166
260
|
- Uploaded `avatarUrl` renders.
|
|
167
261
|
- Resume parsing reaches `parsed`, reports `parseFailureReason`, or reaches the explicit delayed state without blocking editing.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cavuno-board-collections
|
|
3
|
+
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.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Custom fields and collections
|
|
7
|
+
|
|
8
|
+
An operator defines two kinds of extra data. A **custom field** stores one typed value directly on a company, talent profile, or job. A **collection reference** points that record at reusable entries — benefits, technologies, certifications, learning resources — that the operator maintains once and every record shares.
|
|
9
|
+
|
|
10
|
+
Hosted Cavuno boards do not render either one. The API resolves them and your frontend decides the layout, so this surface is read-heavy: definitions tell you what to draw, resolved selections tell you what to draw it with.
|
|
11
|
+
|
|
12
|
+
## The read model
|
|
13
|
+
|
|
14
|
+
Scalar values arrive opaque and keyed by field key; the labels, types, and select options live in a separate definition list. Never hard-code a label.
|
|
15
|
+
|
|
16
|
+
- Job definitions: `board.context()` → `customFields.job` (scalar) and `customFields.jobCollections` (collection fields).
|
|
17
|
+
- Company and talent definitions: `board.profileFields.retrieve('company' | 'candidate')` → `definitions` (scalar) and `referenceDefinitions` (collection fields).
|
|
18
|
+
|
|
19
|
+
To build an input form rather than a display, read `board.context().forms` (`job`, `company`, `talent`): the operator's field order with each custom and collection definition inlined, plus which fields are shown and required. `cavuno-board-post-a-job`, `cavuno-board-account`, and `cavuno-board-companies` show how to render each form.
|
|
20
|
+
|
|
21
|
+
A collection field definition carries `multiple`, and a multiple one may carry `maxSelections` — an operator-defined maximum, up to 100. Cap a picker at that number when it is present, at 100 when it is absent, and at one when `multiple` is false.
|
|
22
|
+
|
|
23
|
+
A job collection definition may also carry `allowOverrides` — each job can replace a selected entry's title and description — and `descriptionFieldKey`, the entry field whose value is the default description. Both are absent when the operator has not set them.
|
|
24
|
+
|
|
25
|
+
Resolved data rides on the detail responses:
|
|
26
|
+
|
|
27
|
+
```ts snippet
|
|
28
|
+
const context = await board.context();
|
|
29
|
+
const definitions = context.customFields.job;
|
|
30
|
+
|
|
31
|
+
const job = await board.jobs.retrieve('senior-chef');
|
|
32
|
+
job.customFieldValues.clearance;
|
|
33
|
+
job.resolvedCollectionFields;
|
|
34
|
+
|
|
35
|
+
const company = await board.companies.retrieve('acme');
|
|
36
|
+
company.customFieldValues.member_tier;
|
|
37
|
+
company.objectReferences;
|
|
38
|
+
|
|
39
|
+
const profile = await board.talent.retrieve('jane-doe');
|
|
40
|
+
profile.customFieldValues.available_for_mentoring;
|
|
41
|
+
profile.objectReferences;
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
A company or talent selection (`objectReferences[]`) carries `fieldKey`, `fieldLabel`, `recordId`, the entry's `slug` when it has one, a resolved `title` and optional `description`, optional `logoUrl`, the entry's own `fields` and `attributes`, and the record-specific `valueDefinitions` / `values` plus `entryDefinitions` / `entries`. `titleOverride` and `descriptionOverride` are present only when the profile replaced the shared wording — `title` and `description` are already resolved, so render those.
|
|
45
|
+
|
|
46
|
+
```ts snippet
|
|
47
|
+
const company = await board.companies.retrieve('acme');
|
|
48
|
+
|
|
49
|
+
for (const selection of company.objectReferences) {
|
|
50
|
+
if (selection.fieldKey !== 'benefits') continue;
|
|
51
|
+
renderBenefit({
|
|
52
|
+
heading: selection.title,
|
|
53
|
+
body: selection.description,
|
|
54
|
+
logoUrl: selection.logoUrl,
|
|
55
|
+
daysPerYear: selection.values.days_per_year,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`values` holds one set of typed details for the selection. `entries` holds repeatable rows, each `{ key, values }` — several awards of one certification, for example. Both are `{}` / `[]` when the field defines no details.
|
|
61
|
+
|
|
62
|
+
Jobs use a different shape: `resolvedCollectionFields` is an array of `{ key, label, entries }`, and each entry is `{ id, name, slug?, logoUrl?, title, description, titleOverride, descriptionOverride, fields, values, references? }`. `title` and `description` are the entry's wording on this job, already resolved: the job's own wording when it set one, otherwise the entry `name` and the value of the definition's `descriptionFieldKey` field (`description` is `null` when neither exists). `titleOverride` and `descriptionOverride` hold the job's own wording, or `null` when the job uses the defaults. A job's wording is plain text; when the default description field is rich text, `description` is sanitised HTML with that wording escaped into paragraphs, so insert `description` as HTML only in that case. Jobs have no per-selection details or repeatable rows.
|
|
63
|
+
|
|
64
|
+
```ts snippet
|
|
65
|
+
const job = await board.jobs.retrieve('senior-chef');
|
|
66
|
+
|
|
67
|
+
for (const field of job.resolvedCollectionFields) {
|
|
68
|
+
for (const entry of field.entries) {
|
|
69
|
+
renderChip({
|
|
70
|
+
label: entry.title,
|
|
71
|
+
description: entry.description,
|
|
72
|
+
logoUrl: entry.logoUrl,
|
|
73
|
+
group: field.label,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Visibility is enforced server-side. Public reads return only public scalar fields, public collection fields, and active entries; private definitions and archived entries are omitted entirely, not nulled. Do not render a placeholder for a key that is absent — the operator meant it to be invisible. An owner read returns the private owner-editable fields too, so the same component can receive either payload; branch on what is present.
|
|
80
|
+
|
|
81
|
+
## Owner writes
|
|
82
|
+
|
|
83
|
+
Collection creation, entry mutation, and imports are operator API operations and are not on this SDK. A signed-in candidate or approved company member edits their own selections through `board.me`, where a selection update replaces the whole editable set and a scalar update is additive. Those read-modify-write rules, the choice search, and the repeatable-entry `key` contract live in `cavuno-board-account` — follow that skill for any write.
|
|
84
|
+
|
|
85
|
+
An approved company member sets a job's own wording with `collectionOverrides` on `board.me.companies.jobs.create` / `update`: one `{ fieldKey, recordId, title?, description? }` per selected entry, only on a field whose definition has `allowOverrides`. `cavuno-board-post-a-job` shows the form.
|
|
86
|
+
|
|
87
|
+
## Rendering
|
|
88
|
+
|
|
89
|
+
- Take every label from the definition list (`referenceDefinitions[].label`, `definitions[].label`, `fieldLabel`, `field.label`), never from the key.
|
|
90
|
+
- Render in the order the API returns. Definitions are in operator display order and selections follow the record's order; do not re-sort.
|
|
91
|
+
- Resolve a select value through the definition's `options[].key` → `label`. A stored value is an option key, or an array of them for multi-select.
|
|
92
|
+
- Media attributes are already metadata objects: `{ id, name, contentType, sizeBytes, url }`. An `image` or `file` attribute is one object, an `image_gallery` attribute is an array. Load by the returned `url`; never build a URL yourself.
|
|
93
|
+
- `logoUrl` is optional on a selection, a job collection entry, and a choice row. The operator gives a collection a logo by adding an `image` field and marking it as the logo; `logoUrl` is that image's URL for the entry (older entries may instead carry a logo URL saved before logos were image fields). When present it is the entry's own logo — render it directly, loading it like any other media `url`, rather than keeping a second name-to-logo map, and fall back to initials or nothing when it is absent. The logo image may also appear among the entry's `attributes`; do not render it twice.
|
|
94
|
+
- A `rich_text` value is sanitised HTML from the server. Insert it as HTML; do not re-escape it into visible tags and do not run a second markdown pass over it.
|
|
95
|
+
- `references` on an entry is keyed by the reference field's key and always holds an array, even for a single reference.
|
|
96
|
+
- Build an entry URL from its `slug`, never from `name` or `id`. A slug is URL-safe and unique within one collection only, so keep the collection or field in the path (`/benefits/annual-leave`). Renaming an entry keeps its slug; when the operator changes a slug, the previous one stays reserved for that entry. `slug` is on selections, job collection entries, nested `references`, and choice rows, and can be absent on entries saved before slugs existed — fall back to a non-linked render then.
|
|
97
|
+
- Number, boolean, and date values keep their JSON types. `0` and `false` are real values — test for `undefined`, not falsiness.
|
|
98
|
+
|
|
99
|
+
## Filters
|
|
100
|
+
|
|
101
|
+
Public collection entries are filter values. Company and talent search accept `objectReferences: [{ key, recordIds }]`; job, company, and talent search accept `customFields: [{ key, values }]`. Load the options first and submit the returned record `id`.
|
|
102
|
+
|
|
103
|
+
```ts snippet
|
|
104
|
+
const choices = await board.profileFields.choices('company', 'technologies', {
|
|
105
|
+
search: 'typescript',
|
|
106
|
+
limit: 20,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const companies = await board.companies.search({
|
|
110
|
+
objectReferences: [
|
|
111
|
+
{ key: 'technologies', recordIds: [choices.data[0]!.id] },
|
|
112
|
+
],
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const talent = await board.talent.list({
|
|
116
|
+
objectReferences: [
|
|
117
|
+
{ key: 'certifications', recordIds: [choices.data[0]!.id] },
|
|
118
|
+
],
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const jobChoices = await board.jobs.collectionChoices('tech_stack', {
|
|
122
|
+
limit: 20,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
void companies.data;
|
|
126
|
+
void talent.data;
|
|
127
|
+
void jobChoices.data;
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Clause and cursor rules, the `invalid_filter` cases, and the listing-URL contract belong to `cavuno-board-filters`.
|
|
131
|
+
|
|
132
|
+
## Completion gate
|
|
133
|
+
|
|
134
|
+
Finish only after every applicable check passes:
|
|
135
|
+
|
|
136
|
+
- Every rendered label comes from a definition or a resolved `fieldLabel`, and no key is displayed raw.
|
|
137
|
+
- Select values render as their option label, and `0` / `false` survive to the screen.
|
|
138
|
+
- A selection or job collection entry renders its resolved `title` and `description`, so an overridden entry shows the record's wording and a non-overridden one shows the collection default.
|
|
139
|
+
- Media renders from the returned `url`, an `image_gallery` renders every image, and a missing `logoUrl` degrades without a broken image.
|
|
140
|
+
- Rich text renders as formatted HTML, not escaped tags.
|
|
141
|
+
- A private field and an archived entry are absent from the public render, with no empty placeholder left behind.
|
|
142
|
+
- Job pages read `resolvedCollectionFields`, and company or talent pages read `objectReferences` — neither shape is assumed for the other.
|
|
143
|
+
- Filter requests carry record IDs from a choices call, and the filter control's options come from the public definition list.
|
|
144
|
+
- A multi-select picker stops at the definition's `maxSelections`, or at 100 when the definition omits it.
|
|
145
|
+
|
|
146
|
+
## Cavuno SDK reference
|
|
147
|
+
|
|
148
|
+
For setup and API details beyond this workflow, use the [Cavuno Board SDK documentation](https://cavuno.com/docs/sdk).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cavuno-board-companies
|
|
3
|
-
description: Company catalog reads with @cavuno/board. Use for company indexes, market archives, profiles, company job rails, similar companies,
|
|
3
|
+
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.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Companies
|
|
@@ -39,6 +39,47 @@ await board.me.companies.revokeInvite('acme', invites[0].id);
|
|
|
39
39
|
const { companySlug } = await board.me.acceptInvite({ token });
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
+
An approved member drafts jobs with `board.me.companies.jobs.create`. The body
|
|
43
|
+
must carry `remoteOption` (`on_site`, `hybrid`, or `remote`): `on_site` and
|
|
44
|
+
`hybrid` also need `officeLocations`, and `remote` needs `remotePermits`. A
|
|
45
|
+
salary is optional, but a `salaryMin` or `salaryMax` needs both
|
|
46
|
+
`salaryCurrency` and `salaryTimeframe`. `update` cannot clear `remoteOption`,
|
|
47
|
+
and an update that sends salary fields is checked against the stored salary.
|
|
48
|
+
Each violation is a 400 `validation_bad_request` naming the field.
|
|
49
|
+
|
|
50
|
+
```ts snippet
|
|
51
|
+
const draft = await board.me.companies.jobs.create('acme', {
|
|
52
|
+
title: 'Staff Engineer',
|
|
53
|
+
description: 'Build the platform.',
|
|
54
|
+
remoteOption: 'remote',
|
|
55
|
+
remotePermits: [{ type: 'worldwide', value: 'worldwide' }],
|
|
56
|
+
salaryMin: 150000,
|
|
57
|
+
salaryMax: 190000,
|
|
58
|
+
salaryCurrency: 'USD',
|
|
59
|
+
salaryTimeframe: 'per_year',
|
|
60
|
+
});
|
|
61
|
+
await board.me.companies.jobs.update('acme', draft.id, { salaryMax: 200000 });
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Render the company profile form
|
|
65
|
+
|
|
66
|
+
`board.context().forms.company` is the company profile form as one ordered list, set by the operator. Use it for `board.me.companies.create` and `update`: render in order, skip entries with `visible: false`, and validate `required` in the form before saving.
|
|
67
|
+
|
|
68
|
+
```ts snippet
|
|
69
|
+
const { forms } = await board.context();
|
|
70
|
+
|
|
71
|
+
for (const field of forms.company) {
|
|
72
|
+
if (!field.visible) continue;
|
|
73
|
+
if (field.kind === 'builtin') {
|
|
74
|
+
renderCompanyBuiltin(field.key, { required: field.required });
|
|
75
|
+
} else if (field.definition.editableByOwner) {
|
|
76
|
+
renderCompanyField(field, { required: field.required });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Built-in keys `name`, `website`, `summary`, `xUrl`, `linkedinUrl`, `facebookUrl`, and `description` are the same-named body fields of `create` / `update`; `logo` is `board.me.companies.uploadLogo`. `name` is locked (always shown and required). Skip a built-in key you do not recognise. `custom` entries write through `updateCustomFields` and `collection` entries through `updateObjectReferences`; both carry their public `definition` inline, and only `editableByOwner` definitions are inputs. See `cavuno-board-account` for those write rules.
|
|
82
|
+
|
|
42
83
|
## List and search
|
|
43
84
|
|
|
44
85
|
`companies.list` returns `CompanyListEnvelope`: `ListEnvelope<PublicCompany>`
|
|
@@ -146,6 +187,7 @@ one. Gate a Salaries tab with `company.salarySampleCount > 0` from
|
|
|
146
187
|
|
|
147
188
|
Finish only after every applicable check passes:
|
|
148
189
|
|
|
190
|
+
- The company form follows `forms.company`: order matches, hidden fields are absent, and required fields block save while empty.
|
|
149
191
|
- A known company is a `public_company`; only its retrieve response has a
|
|
150
192
|
`markets` array.
|
|
151
193
|
- A listed market slug works as `marketSlug`, while an invented one produces a
|
|
@@ -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, 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,45 @@ 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
|
+
## Build filters from public profile fields
|
|
154
|
+
|
|
155
|
+
`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.
|
|
156
|
+
|
|
157
|
+
```ts snippet
|
|
158
|
+
const companyFields = await board.profileFields.retrieve('company');
|
|
159
|
+
const technologyChoices = await board.profileFields.choices(
|
|
160
|
+
'company',
|
|
161
|
+
'technologies',
|
|
162
|
+
{ search: 'typescript', limit: 20 });
|
|
163
|
+
|
|
164
|
+
const companies = await board.companies.search({
|
|
165
|
+
query: 'engineering',
|
|
166
|
+
customFields: [{ key: 'member_tier', values: ['gold'] }],
|
|
167
|
+
objectReferences: [
|
|
168
|
+
{ key: 'technologies', recordIds: [technologyChoices.data[0]!.id] },
|
|
169
|
+
],
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const candidateFields = await board.profileFields.retrieve('candidate');
|
|
173
|
+
const certifications = await board.profileFields.choices(
|
|
174
|
+
'candidate',
|
|
175
|
+
'certifications',
|
|
176
|
+
{ limit: 20 });
|
|
177
|
+
const talent = await board.talent.list({
|
|
178
|
+
customFields: [{ key: 'available_for_mentoring', values: [true] }],
|
|
179
|
+
objectReferences: [
|
|
180
|
+
{ key: 'certifications', recordIds: [certifications.data[0]!.id] },
|
|
181
|
+
],
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
void companyFields.definitions;
|
|
185
|
+
void candidateFields.referenceDefinitions;
|
|
186
|
+
void companies.data;
|
|
187
|
+
void talent.data;
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
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.
|
|
191
|
+
|
|
126
192
|
## Completion gate
|
|
127
193
|
|
|
128
194
|
Finish only after every applicable check passes:
|
|
@@ -135,7 +201,11 @@ Finish only after every applicable check passes:
|
|
|
135
201
|
relevance, newest, and salary high.
|
|
136
202
|
- Category and skill filtering sends `sourceSlug`, while links use
|
|
137
203
|
`canonicalSlug`.
|
|
204
|
+
- `/jobs?category=engineering,design` sends
|
|
205
|
+
`filters.categories: ['engineering', 'design']` to `jobs.search`.
|
|
138
206
|
- Every paged taxonomy request forwards the previous opaque `nextCursor`.
|
|
207
|
+
- Company and talent custom controls come from `board.profileFields`; private definitions and archived choices never become filter options.
|
|
208
|
+
- Profile-filter requests use stored scalar values and returned collection entry IDs, not display labels.
|
|
139
209
|
|
|
140
210
|
## Cavuno SDK reference
|
|
141
211
|
|
|
@@ -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,69 @@ 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
|
+
|
|
27
90
|
## Resolve a logo
|
|
28
91
|
|
|
29
92
|
Both logo paths return a stored `publicUrl` for `create`. The SDK owns multipart encoding.
|
|
@@ -111,6 +174,7 @@ Bare-email billing lookup is intentionally absent because it would expose whethe
|
|
|
111
174
|
|
|
112
175
|
## Completion gate
|
|
113
176
|
|
|
177
|
+
- 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
178
|
- Every `create` call handles all four statuses.
|
|
115
179
|
- Paid checkout redirects through `checkoutUrl`; the job appears only after webhook completion.
|
|
116
180
|
- Free unmoderated submission returns a resolving `jobSlug`.
|
package/skills/manifest.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "4.
|
|
2
|
+
"version": "4.27.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, 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"
|