Files
krow_talent_app/src/components/ui-tree/UiEditingProvider.jsx
Aravind 6249e00a3a
Some checks failed
CI / check (push) Failing after 4m58s
candidates and board ui agent issue
2026-09-05 10:46:06 +05:30

172 lines
6.3 KiB
JavaScript

import React from 'react';
import { useUiLayouts } from '@/lib/krowHooks';
import { useSkillSections } from '@/components/skills/SkillSurface';
import { skillNodesByPlacement } from '@/lib/ui/skillNodes';
import { composePage } from '@/lib/ui/composition';
import { applyOperation } from '@/lib/ui/operations';
import { clearOps, emptyPatch, popOp, pushOp } from '@/lib/ui/patch';
import { nodeRegistry } from '@/lib/ui/registry';
/**
* Preview, Apply, and the line between them.
*
* Three states of a UI change live here, and keeping them apart is the whole
* job of this module:
*
* - **Preview** is in React state. It is what the person is trying. It never
* reaches the network, and a reload discards it.
* - **Saved** is the person's own stored operation list, read from and
* written to their account preferences. It survives a reload and a new
* session.
* - **Source** — the page's registered composition and the Markdown skill
* files — is never written by anything here. Not on preview, not on apply.
*
* A proposed operation is validated *before* it becomes a preview, so a change
* that could not be saved is never shown as though it could. Preview and apply
* then run the identical merge through `composePage` and the identical
* renderer, which is what makes a preview honest: there is no second code path
* for the applied state that could disagree with it.
*/
const UiEditingContext = React.createContext(null);
/** The editing session for the page around it. Null outside a provider. */
export const useUiEditing = () => React.useContext(UiEditingContext);
export function UiEditingProvider({ page, role = null, registry = nodeRegistry, children }) {
const { layouts, save, saving } = useUiLayouts();
/**
* What the person is trying, not yet theirs.
*
* Held per page so that navigating away and back does not carry an
* unfinished experiment onto a different surface.
*/
const [preview, setPreview] = React.useState(() => emptyPatch(page));
const [problems, setProblems] = React.useState([]);
/* A page change is a new editing session. Anything unsaved was about the page
that is no longer on screen. */
React.useEffect(() => {
setPreview(emptyPatch(page));
setProblems([]);
}, [page]);
const saved = layouts[page] || emptyPatch(page);
/**
* What this page's definitions contribute, as nodes.
*
* Resolved here because it needs the account's custom skills and disabled
* list, and handed to `composePage` so that module stays a pure function.
* The result is that a Board skill's card is a node in the same tree as the
* page's own sections — addressable by the same operations, and hidden or
* moved by a patch rather than by editing the definition.
*/
const sections = useSkillSections(page);
const skillNodes = React.useMemo(() => skillNodesByPlacement(sections), [sections]);
/**
* The tree on screen: what the application ships, with what the person saved,
* with what they are trying, in that order.
*/
const composed = React.useMemo(
() => composePage(page, { patch: saved, preview, registry, role, skillNodes }),
[page, saved, preview, registry, role, skillNodes]
);
/**
* Try an operation.
*
* Validated against the tree as it currently stands — saved changes included
* — so an operation is judged against what the person is actually looking at.
* A refusal returns the reasons and changes nothing; there is no partially
* applied preview.
*/
const propose = React.useCallback((op) => {
const result = applyOperation(composed.tree, op, { registry, role });
if (!result.ok) {
setProblems(result.problems);
return result;
}
setProblems([]);
setPreview((current) => pushOp(current, op));
return result;
}, [composed.tree, registry, role]);
/** Throw the experiment away. Nothing was stored, so nothing is undone. */
const discard = React.useCallback(() => {
setPreview(emptyPatch(page));
setProblems([]);
}, [page]);
/**
* Keep it.
*
* The preview's operations are appended to what was already saved and written
* as one list. The preview is only cleared once the write resolves, so a
* failed save leaves the person looking at the change they asked for rather
* than watching it disappear with an error beside it.
*/
const apply = React.useCallback(async () => {
if (!preview.ops.length) return { ok: true, saved };
const next = { ...saved, page, ops: [...saved.ops, ...preview.ops], updatedAt: new Date().toISOString() };
const result = await save(page, next);
if (result?.persisted === false) {
setProblems([{ at: null, message: result.error || 'That change could not be saved.' }]);
return { ok: false, saved };
}
setPreview(emptyPatch(page));
return { ok: true, saved: next };
}, [preview, saved, page, save]);
/**
* Undo one step.
*
* The most recent thing first: an unsaved operation if there is one, and only
* then a saved one. Undoing a saved change is a write, because the saved list
* is the record of what the person chose.
*/
const undo = React.useCallback(async () => {
if (preview.ops.length) {
setPreview((current) => popOp(current));
return { ok: true };
}
if (!saved.ops.length) return { ok: true };
await save(page, popOp(saved));
return { ok: true };
}, [preview, saved, page, save]);
/** Back to the page as the application ships it. Clears both tiers. */
const reset = React.useCallback(async () => {
setPreview(emptyPatch(page));
setProblems([]);
if (saved.ops.length) await save(page, clearOps(saved));
return { ok: true };
}, [page, saved, save]);
const value = React.useMemo(() => ({
page,
tree: composed.tree,
/* Operations that no longer apply — a saved change naming a section a
release has since removed. Surfaced so a page can say so quietly rather
than leaving the person wondering why nothing happened. */
skipped: composed.skipped,
saved,
preview,
problems,
previewing: preview.ops.length > 0,
customised: saved.ops.length > 0,
saving,
propose,
discard,
apply,
undo,
reset,
}), [
page, composed, saved, preview, problems, saving, propose, discard, apply, undo, reset,
]);
return <UiEditingContext.Provider value={value}>{children}</UiEditingContext.Provider>;
}