update the workspace and skills flow

This commit is contained in:
2026-08-20 11:04:13 +05:30
parent d3f7f439f6
commit 161b237695
8 changed files with 1100 additions and 204 deletions

View File

@@ -438,6 +438,7 @@ export const DATA_SOURCES = [
context: 'positionId',
summary: 'Applications to this position, counted over time.',
shapes: ['flow', 'stats', 'timeline', 'table', 'insight', 'card'],
options: ['periods'],
},
{
id: 'position.pipeline',
@@ -452,6 +453,7 @@ export const DATA_SOURCES = [
context: 'positionId',
summary: 'Candidates matched to this position, best first.',
shapes: ['list', 'table', 'stats', 'card'],
options: ['limit'],
},
{
/**
@@ -469,6 +471,7 @@ export const DATA_SOURCES = [
context: 'positionId',
summary: 'The candidate pool scored against this position, best first.',
shapes: ['list', 'table', 'stats', 'card', 'insight'],
options: ['limit'],
},
{
id: 'position.requirements',
@@ -504,6 +507,7 @@ export const DATA_SOURCES = [
context: null,
summary: 'Applications across the workspace, counted over time.',
shapes: ['flow', 'stats', 'timeline', 'table', 'card'],
options: ['periods'],
},
{
id: 'positions.demand',
@@ -511,6 +515,7 @@ export const DATA_SOURCES = [
context: null,
summary: 'Open positions and what they still need.',
shapes: ['list', 'table', 'stats', 'card'],
options: ['limit'],
},
{
id: 'workforce.training',
@@ -518,6 +523,7 @@ export const DATA_SOURCES = [
context: null,
summary: 'Training paths and progress against them.',
shapes: ['progress', 'list', 'stats', 'table', 'card'],
options: ['limit'],
},
{
/* The vetting weights a position is being specified with. `positionId`
@@ -548,6 +554,7 @@ export const DATA_SOURCES = [
context: null,
summary: 'Who was hired, for which role, and when.',
shapes: ['list', 'table', 'timeline', 'stats', 'card'],
options: ['limit'],
},
{
id: 'hires.performance',
@@ -562,6 +569,7 @@ export const DATA_SOURCES = [
context: null,
summary: 'What has happened across the workspace, most recent first.',
shapes: ['timeline', 'list', 'table', 'stats', 'card'],
options: ['limit'],
},
];
@@ -569,6 +577,60 @@ export const SUPPORTED_DATA_SOURCES = DATA_SOURCES.map((s) => s.id);
export const dataSourceFor = (id) => DATA_SOURCES.find((s) => s.id === id) || null;
/* ── Source / shape compatibility ───────────────────────────────────────────
One resolver, three consumers: `normalizeSection` refuses on it, the Board
editor's source picker filters on it, and the Owliver editor's per-capability
picker filters on it. They used to be three readings of `source.shapes` —
inline in the normalizer, absent from one editor and approximated in the
other — which is how a form could compose `list` against a source that has no
list in it and only find out at save. */
/**
* The section type a capability is drawn with, or null when it is prose.
*
* `summary` is the one capability with no component, so it is compatible with
* every source: the figures are read back as sentences rather than drawn.
*/
export const shapeForCapability = (capability) => owliverCapabilityFor(capability)?.shape || null;
/** Can this source be drawn as this shape? A null shape is prose, and always can. */
export function sourceSupportsShape(sourceId, shape) {
const source = dataSourceFor(sourceId);
if (!source) return false;
if (!shape) return true;
return Boolean(source.shapes?.includes(shape));
}
/**
* The sources that can fill this shape, optionally narrowed to what a placement
* can supply context for.
*
* `context` is the list a placement provides — `contextSuppliedBy(pages,
* placement)`. Passing it is how the Board editor offers only sources that can
* actually resolve where the section is mounted; the Owliver editor passes
* nothing, because a response with an unmet need asks which record is meant
* rather than rendering dead.
*/
export function sourcesForShape(shape, { context = null } = {}) {
return DATA_SOURCES.filter((source) => {
if (!sourceSupportsShape(source.id, shape)) return false;
if (!context) return true;
return !source.context || context.includes(source.context);
});
}
/**
* Whether a source reads an option a form can offer — `periods` or `limit`.
*
* Declared per source rather than inferred from its shapes, because the two do
* not line up: `positions.demand` is a list that honours `limit` and has no
* periods at all, while `candidates.activity` is the reverse. The editors used
* to guess from the shape list and offered period checkboxes that changed
* nothing.
*/
export const sourceSupportsOption = (sourceId, option) =>
Boolean(dataSourceFor(sourceId)?.options?.includes(option));
/**
* Can this reading be written back?
*