hlassiege/darylpublic Fork
⑂ c821b56
Commits
⬇ Clone ▾
git clone https://git.rickub.com/hlassiege/daryl.git
git clone ssh://git@rickub.com/hlassiege/daryl.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

feat(web): switch between script versions in the Script step

A version picker in the toolbar, with buttons to fork the current version, rename it and delete it. Pending edits are saved to the current version before switching.
hlassiege committed 2026-10-12T10:10:13+02:00 Browse files
c821b56 parent: 0d901d7
modified web/src/i18n/locales/en/script.ts +11 -0
@@ -20,6 +20,16 @@ export default {
2020 chosenAngle: "Chosen angle",
2121 rewriteLink: "Rewrite from the angle",
2222 rewriteConfirm: "Rewrite the script from the chosen angle? The current version will be replaced.",
23+ drafts: {
24+ option: "{name} · {words} words",
25+ switchTitle: "Script version: the others are kept as they are, plans included",
26+ fork: "+ Version",
27+ forkTitle: "Start from a copy of this version (text, plans, setups); this one stays available",
28+ rename: "Rename",
29+ renameTitle: "Rename this version",
30+ renamePrompt: "Version name",
31+ deleteTitle: "Delete this version",
32+ },
2333 save: { saved: "saved automatically", saving: "saving…", error: "save failed" },
2434 research: {
2535 placeholder: "Paste a link or a source title",
@@ -40,6 +50,7 @@ export default {
4050 scriptLoadError: "Couldn’t load the script: {error}",
4151 plansSaveError: "Couldn’t save shots: {error}",
4252 setupsSaveError: "Couldn’t save setups: {error}",
53+ draftError: "Couldn’t update script versions: {error}",
4354 },
4455 editor: {
4556 addTitle: "A shot on this whole paragraph (for a specific passage, select it)",
@@ -20,6 +20,16 @@ export default {
20 chosenAngle: "Chosen angle",20 chosenAngle: "Chosen angle",
21 rewriteLink: "Rewrite from the angle",21 rewriteLink: "Rewrite from the angle",
22 rewriteConfirm: "Rewrite the script from the chosen angle? The current version will be replaced.",22 rewriteConfirm: "Rewrite the script from the chosen angle? The current version will be replaced.",
23+ drafts: {
24+ option: "{name} · {words} words",
25+ switchTitle: "Script version: the others are kept as they are, plans included",
26+ fork: "+ Version",
27+ forkTitle: "Start from a copy of this version (text, plans, setups); this one stays available",
28+ rename: "Rename",
29+ renameTitle: "Rename this version",
30+ renamePrompt: "Version name",
31+ deleteTitle: "Delete this version",
32+ },
23 save: { saved: "saved automatically", saving: "saving…", error: "save failed" },33 save: { saved: "saved automatically", saving: "saving…", error: "save failed" },
24 research: {34 research: {
25 placeholder: "Paste a link or a source title",35 placeholder: "Paste a link or a source title",
@@ -40,6 +50,7 @@ export default {
40 scriptLoadError: "Couldn’t load the script: {error}",50 scriptLoadError: "Couldn’t load the script: {error}",
41 plansSaveError: "Couldn’t save shots: {error}",51 plansSaveError: "Couldn’t save shots: {error}",
42 setupsSaveError: "Couldn’t save setups: {error}",52 setupsSaveError: "Couldn’t save setups: {error}",
53+ draftError: "Couldn’t update script versions: {error}",
43 },54 },
44 editor: {55 editor: {
45 addTitle: "A shot on this whole paragraph (for a specific passage, select it)",56 addTitle: "A shot on this whole paragraph (for a specific passage, select it)",
modified web/src/i18n/locales/fr/script.ts +11 -0
@@ -20,6 +20,16 @@ export default {
2020 chosenAngle: "Angle retenu",
2121 rewriteLink: "Réécrire depuis l’angle",
2222 rewriteConfirm: "Réécrire le script depuis l’angle retenu ? La version actuelle sera remplacée.",
23+ drafts: {
24+ option: "{name} · {words} mots",
25+ switchTitle: "Version du script : les autres sont gardées telles quelles, plans compris",
26+ fork: "+ Version",
27+ forkTitle: "Repartir d’une copie de cette version (texte, plans, setups) ; celle-ci reste disponible",
28+ rename: "Renommer",
29+ renameTitle: "Renommer cette version",
30+ renamePrompt: "Nom de la version",
31+ deleteTitle: "Supprimer cette version",
32+ },
2333 save: { saved: "enregistré automatiquement", saving: "enregistrement…", error: "échec de l’enregistrement" },
2434 research: {
2535 placeholder: "Colle un lien ou un titre de source",
@@ -40,6 +50,7 @@ export default {
4050 scriptLoadError: "Lecture du script impossible : {error}",
4151 plansSaveError: "Enregistrement des plans impossible : {error}",
4252 setupsSaveError: "Enregistrement des setups impossible : {error}",
53+ draftError: "Opération sur les versions impossible : {error}",
4354 },
4455 editor: {
4556 addTitle: "Un plan sur tout ce paragraphe (pour un passage précis : sélectionne-le)",
@@ -20,6 +20,16 @@ export default {
20 chosenAngle: "Angle retenu",20 chosenAngle: "Angle retenu",
21 rewriteLink: "Réécrire depuis l’angle",21 rewriteLink: "Réécrire depuis l’angle",
22 rewriteConfirm: "Réécrire le script depuis l’angle retenu ? La version actuelle sera remplacée.",22 rewriteConfirm: "Réécrire le script depuis l’angle retenu ? La version actuelle sera remplacée.",
23+ drafts: {
24+ option: "{name} · {words} mots",
25+ switchTitle: "Version du script : les autres sont gardées telles quelles, plans compris",
26+ fork: "+ Version",
27+ forkTitle: "Repartir d’une copie de cette version (texte, plans, setups) ; celle-ci reste disponible",
28+ rename: "Renommer",
29+ renameTitle: "Renommer cette version",
30+ renamePrompt: "Nom de la version",
31+ deleteTitle: "Supprimer cette version",
32+ },
23 save: { saved: "enregistré automatiquement", saving: "enregistrement…", error: "échec de l’enregistrement" },33 save: { saved: "enregistré automatiquement", saving: "enregistrement…", error: "échec de l’enregistrement" },
24 research: {34 research: {
25 placeholder: "Colle un lien ou un titre de source",35 placeholder: "Colle un lien ou un titre de source",
@@ -40,6 +50,7 @@ export default {
40 scriptLoadError: "Lecture du script impossible : {error}",50 scriptLoadError: "Lecture du script impossible : {error}",
41 plansSaveError: "Enregistrement des plans impossible : {error}",51 plansSaveError: "Enregistrement des plans impossible : {error}",
42 setupsSaveError: "Enregistrement des setups impossible : {error}",52 setupsSaveError: "Enregistrement des setups impossible : {error}",
53+ draftError: "Opération sur les versions impossible : {error}",
43 },54 },
44 editor: {55 editor: {
45 addTitle: "Un plan sur tout ce paragraphe (pour un passage précis : sélectionne-le)",56 addTitle: "Un plan sur tout ce paragraphe (pour un passage précis : sélectionne-le)",
modified web/src/project/steps/ScriptStep.vue +42 -0
@@ -202,6 +202,35 @@ function replacePassage(passage: string, text: string): void {
202202 toast(t("script.toasts.replaced"));
203203 }
204204
205+// --- versions (V1, V2…) ----------------------------------------------------------------
206+
207+const drafts = computed(() => script.drafts.value);
208+const draft = computed(() => drafts.value.find((d) => d.active) ?? null);
209+
210+/** Avant de changer de version, ce qui est en cours de frappe rejoint la version actuelle. */
211+function switchDraft(id: string): void {
212+ editor.value?.flush();
213+ active.value = null;
214+ script.activateDraft(id);
215+}
216+
217+function forkDraft(): void {
218+ editor.value?.flush();
219+ script.forkDraft();
220+}
221+
222+function renameDraft(): void {
223+ if (!draft.value) return;
224+ const name = prompt(t("script.drafts.renamePrompt"), draft.value.name)?.trim();
225+ if (name && name !== draft.value.name) script.renameDraft(draft.value.id, name);
226+}
227+
228+function deleteDraft(): void {
229+ if (!draft.value) return;
230+ active.value = null;
231+ script.deleteDraft(draft.value.id);
232+}
233+
205234 function writeScript(): void {
206235 act("/script/write");
207236 }
@@ -307,6 +336,17 @@ function setNote(i: number, n: string): void {
307336 {{ t("script.writing") }}<template v-if="project.runtime.jobs.script?.activity"> · {{ project.runtime.jobs.script.activity }}</template>
308337 </span>
309338 <span v-else class="eyebrow hint">{{ t("script.hint") }}</span>
339+ <div v-if="draft" class="drafts">
340+ <select class="select" :value="draft.id" :disabled="writing" :title="t('script.drafts.switchTitle')"
341+ @change="switchDraft(($event.target as HTMLSelectElement).value)">
342+ <option v-for="d in drafts" :key="d.id" :value="d.id">
343+ {{ t("script.drafts.option", { name: d.name, words: d.words }) }}</option>
344+ </select>
345+ <button class="btn sm" :disabled="writing" :title="t('script.drafts.forkTitle')" @click="forkDraft">
346+ {{ t("script.drafts.fork") }}</button>
347+ <button class="btn sm" :title="t('script.drafts.renameTitle')" @click="renameDraft">{{ t("script.drafts.rename") }}</button>
348+ <ConfirmX v-if="drafts.length > 1 && !writing" :title="t('script.drafts.deleteTitle')" @confirm="deleteDraft" />
349+ </div>
310350 <button class="btn sm" @click="editor?.selectAll()">{{ t("script.selectAll") }}</button>
311351 <button class="btn sm" @click="copy(md, t('script.scriptCopied'))">{{ t("script.copyMarkdown") }}</button>
312352 <button class="btn sm" @click="download">{{ t("script.exportMd") }}</button>
@@ -479,6 +519,8 @@ function setNote(i: number, n: string): void {
479519 .center { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 8px; }
480520 .toolbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
481521 .toolbar .btn.sm { height: 30px; }
522+.drafts { display: flex; align-items: center; gap: 6px; padding-right: 8px; border-right: 1px solid var(--line); }
523+.drafts .select { height: 30px; font-size: 12.5px; }
482524 .hint { flex: 1 1 200px; min-width: 0; }
483525 .writing { flex: 1 1 200px; min-width: 0; font-size: 12px; color: var(--red-ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
484526 .sheet-scroll { flex: 1; min-height: 0; overflow-y: auto; background: var(--bg-softer); border: 1px solid var(--line); border-radius: 6px; }
@@ -202,6 +202,35 @@ function replacePassage(passage: string, text: string): void {
202 toast(t("script.toasts.replaced"));202 toast(t("script.toasts.replaced"));
203 }203 }
204 204
205+// --- versions (V1, V2…) ----------------------------------------------------------------
206+
207+const drafts = computed(() => script.drafts.value);
208+const draft = computed(() => drafts.value.find((d) => d.active) ?? null);
209+
210+/** Avant de changer de version, ce qui est en cours de frappe rejoint la version actuelle. */
211+function switchDraft(id: string): void {
212+ editor.value?.flush();
213+ active.value = null;
214+ script.activateDraft(id);
215+}
216+
217+function forkDraft(): void {
218+ editor.value?.flush();
219+ script.forkDraft();
220+}
221+
222+function renameDraft(): void {
223+ if (!draft.value) return;
224+ const name = prompt(t("script.drafts.renamePrompt"), draft.value.name)?.trim();
225+ if (name && name !== draft.value.name) script.renameDraft(draft.value.id, name);
226+}
227+
228+function deleteDraft(): void {
229+ if (!draft.value) return;
230+ active.value = null;
231+ script.deleteDraft(draft.value.id);
232+}
233+
205 function writeScript(): void {234 function writeScript(): void {
206 act("/script/write");235 act("/script/write");
207 }236 }
@@ -307,6 +336,17 @@ function setNote(i: number, n: string): void {
307 {{ t("script.writing") }}<template v-if="project.runtime.jobs.script?.activity"> · {{ project.runtime.jobs.script.activity }}</template>336 {{ t("script.writing") }}<template v-if="project.runtime.jobs.script?.activity"> · {{ project.runtime.jobs.script.activity }}</template>
308 </span>337 </span>
309 <span v-else class="eyebrow hint">{{ t("script.hint") }}</span>338 <span v-else class="eyebrow hint">{{ t("script.hint") }}</span>
339+ <div v-if="draft" class="drafts">
340+ <select class="select" :value="draft.id" :disabled="writing" :title="t('script.drafts.switchTitle')"
341+ @change="switchDraft(($event.target as HTMLSelectElement).value)">
342+ <option v-for="d in drafts" :key="d.id" :value="d.id">
343+ {{ t("script.drafts.option", { name: d.name, words: d.words }) }}</option>
344+ </select>
345+ <button class="btn sm" :disabled="writing" :title="t('script.drafts.forkTitle')" @click="forkDraft">
346+ {{ t("script.drafts.fork") }}</button>
347+ <button class="btn sm" :title="t('script.drafts.renameTitle')" @click="renameDraft">{{ t("script.drafts.rename") }}</button>
348+ <ConfirmX v-if="drafts.length > 1 && !writing" :title="t('script.drafts.deleteTitle')" @confirm="deleteDraft" />
349+ </div>
310 <button class="btn sm" @click="editor?.selectAll()">{{ t("script.selectAll") }}</button>350 <button class="btn sm" @click="editor?.selectAll()">{{ t("script.selectAll") }}</button>
311 <button class="btn sm" @click="copy(md, t('script.scriptCopied'))">{{ t("script.copyMarkdown") }}</button>351 <button class="btn sm" @click="copy(md, t('script.scriptCopied'))">{{ t("script.copyMarkdown") }}</button>
312 <button class="btn sm" @click="download">{{ t("script.exportMd") }}</button>352 <button class="btn sm" @click="download">{{ t("script.exportMd") }}</button>
@@ -479,6 +519,8 @@ function setNote(i: number, n: string): void {
479 .center { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 8px; }519 .center { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 8px; }
480 .toolbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }520 .toolbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
481 .toolbar .btn.sm { height: 30px; }521 .toolbar .btn.sm { height: 30px; }
522+.drafts { display: flex; align-items: center; gap: 6px; padding-right: 8px; border-right: 1px solid var(--line); }
523+.drafts .select { height: 30px; font-size: 12.5px; }
482 .hint { flex: 1 1 200px; min-width: 0; }524 .hint { flex: 1 1 200px; min-width: 0; }
483 .writing { flex: 1 1 200px; min-width: 0; font-size: 12px; color: var(--red-ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }525 .writing { flex: 1 1 200px; min-width: 0; font-size: 12px; color: var(--red-ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
484 .sheet-scroll { flex: 1; min-height: 0; overflow-y: auto; background: var(--bg-softer); border: 1px solid var(--line); border-radius: 6px; }526 .sheet-scroll { flex: 1; min-height: 0; overflow-y: auto; background: var(--bg-softer); border: 1px solid var(--line); border-radius: 6px; }
modified web/src/project/steps/script/useScript.ts +33 -3
@@ -1,6 +1,6 @@
11 /** Le script du projet en blocs et ses plans (script.json côté serveur) : chargement et enregistrement automatique. */
22 import { onBeforeUnmount, onMounted, ref, shallowRef } from "vue";
3-import { api, putJson } from "../../../lib/api";
3+import { api, del, patchJson, post, putJson } from "../../../lib/api";
44 import { t } from "../../../i18n";
55 import { toast } from "../../../lib/toast";
66 import { refresh, store } from "../../store";
@@ -10,6 +10,9 @@ import type { SaveState } from "./doc";
1010 /** Un bloc du texte : paragraphe, titre, citation, liste… avec son Markdown. L'id ne change pas à l'édition. */
1111 export interface Block { id: string; md: string }
1212
13+/** Une version du script (V1, V2…) ; seule l'active est éditée, les autres attendent côté serveur. */
14+export interface Draft { id: string; name: string; created: number; active: boolean; words: number }
15+
1316 export function useScript(onSaved?: () => void) {
1417 const url = `/api/projects/${store.project!.id}/script`;
1518 /** null tant que le script n'est pas chargé */
@@ -18,6 +21,7 @@ export function useScript(onSaved?: () => void) {
1821 const setups = shallowRef<Setup[]>([]);
1922 /** le setup de chaque section : id du bloc titre → id du setup */
2023 const sections = shallowRef<Record<string, string>>({});
24+ const drafts = shallowRef<Draft[]>([]);
2125 /** change à chaque chargement depuis le serveur : l'éditeur repart de ces blocs */
2226 const version = ref(0);
2327 const state = ref<SaveState>("saved");
@@ -30,11 +34,14 @@ export function useScript(onSaved?: () => void) {
3034
3135 async function load(): Promise<void> {
3236 try {
33- const data = await api<{ blocks: Block[]; plans: Plan[]; setups: Setup[]; sections: Record<string, string> }>(url);
37+ const data = await api<{
38+ blocks: Block[]; plans: Plan[]; setups: Setup[]; sections: Record<string, string>; drafts: Draft[];
39+ }>(url);
3440 blocks.value = data.blocks;
3541 plans.value = data.plans;
3642 setups.value = data.setups;
3743 sections.value = data.sections;
44+ drafts.value = data.drafts;
3845 dirty = plansDirty = false;
3946 state.value = "saved";
4047 version.value++;
@@ -125,6 +132,28 @@ export function useScript(onSaved?: () => void) {
125132 }
126133 }
127134
135+ /**
136+ * Une opération sur les versions : ce qui attend d'être enregistré l'est d'abord (dans la version
137+ * en cours), puis on relit le script, qui a pu changer de version.
138+ */
139+ async function onDrafts(request: () => Promise<{ drafts: Draft[] }>): Promise<void> {
140+ await Promise.all([save(), savePlans(), saveSetups()]);
141+ try {
142+ drafts.value = (await request()).drafts;
143+ } catch (e) {
144+ toast(t("script.toasts.draftError", { error: (e as Error).message }), true);
145+ return;
146+ }
147+ await load();
148+ refresh().catch(() => {});
149+ }
150+
151+ const draftUrl = (id: string) => `${url}/drafts/${id}`;
152+ const forkDraft = () => onDrafts(() => post(`${url}/drafts`));
153+ const activateDraft = (id: string) => onDrafts(() => post(`${draftUrl(id)}/activate`));
154+ const renameDraft = (id: string, name: string) => onDrafts(() => patchJson(draftUrl(id), { name }));
155+ const deleteDraft = (id: string) => onDrafts(() => del(draftUrl(id)));
156+
128157 onMounted(load);
129158 onBeforeUnmount(() => {
130159 save();
@@ -132,7 +161,8 @@ export function useScript(onSaved?: () => void) {
132161 saveSetups();
133162 });
134163 return {
135- blocks, plans, setups, sections, version, state, load, set, save, setPlans, updatePlan, removePlan, setSetups,
164+ blocks, plans, setups, sections, drafts, version, state, load, set, save, setPlans, updatePlan, removePlan, setSetups,
165+ forkDraft, activateDraft, renameDraft, deleteDraft,
136166 };
137167 }
138168
@@ -1,6 +1,6 @@
1 /** Le script du projet en blocs et ses plans (script.json côté serveur) : chargement et enregistrement automatique. */1 /** Le script du projet en blocs et ses plans (script.json côté serveur) : chargement et enregistrement automatique. */
2 import { onBeforeUnmount, onMounted, ref, shallowRef } from "vue";2 import { onBeforeUnmount, onMounted, ref, shallowRef } from "vue";
3-import { api, putJson } from "../../../lib/api";3+import { api, del, patchJson, post, putJson } from "../../../lib/api";
4 import { t } from "../../../i18n";4 import { t } from "../../../i18n";
5 import { toast } from "../../../lib/toast";5 import { toast } from "../../../lib/toast";
6 import { refresh, store } from "../../store";6 import { refresh, store } from "../../store";
@@ -10,6 +10,9 @@ import type { SaveState } from "./doc";
10 /** Un bloc du texte : paragraphe, titre, citation, liste… avec son Markdown. L'id ne change pas à l'édition. */10 /** Un bloc du texte : paragraphe, titre, citation, liste… avec son Markdown. L'id ne change pas à l'édition. */
11 export interface Block { id: string; md: string }11 export interface Block { id: string; md: string }
12 12
13+/** Une version du script (V1, V2…) ; seule l'active est éditée, les autres attendent côté serveur. */
14+export interface Draft { id: string; name: string; created: number; active: boolean; words: number }
15+
13 export function useScript(onSaved?: () => void) {16 export function useScript(onSaved?: () => void) {
14 const url = `/api/projects/${store.project!.id}/script`;17 const url = `/api/projects/${store.project!.id}/script`;
15 /** null tant que le script n'est pas chargé */18 /** null tant que le script n'est pas chargé */
@@ -18,6 +21,7 @@ export function useScript(onSaved?: () => void) {
18 const setups = shallowRef<Setup[]>([]);21 const setups = shallowRef<Setup[]>([]);
19 /** le setup de chaque section : id du bloc titre → id du setup */22 /** le setup de chaque section : id du bloc titre → id du setup */
20 const sections = shallowRef<Record<string, string>>({});23 const sections = shallowRef<Record<string, string>>({});
24+ const drafts = shallowRef<Draft[]>([]);
21 /** change à chaque chargement depuis le serveur : l'éditeur repart de ces blocs */25 /** change à chaque chargement depuis le serveur : l'éditeur repart de ces blocs */
22 const version = ref(0);26 const version = ref(0);
23 const state = ref<SaveState>("saved");27 const state = ref<SaveState>("saved");
@@ -30,11 +34,14 @@ export function useScript(onSaved?: () => void) {
30 34
31 async function load(): Promise<void> {35 async function load(): Promise<void> {
32 try {36 try {
33- const data = await api<{ blocks: Block[]; plans: Plan[]; setups: Setup[]; sections: Record<string, string> }>(url);37+ const data = await api<{
38+ blocks: Block[]; plans: Plan[]; setups: Setup[]; sections: Record<string, string>; drafts: Draft[];
39+ }>(url);
34 blocks.value = data.blocks;40 blocks.value = data.blocks;
35 plans.value = data.plans;41 plans.value = data.plans;
36 setups.value = data.setups;42 setups.value = data.setups;
37 sections.value = data.sections;43 sections.value = data.sections;
44+ drafts.value = data.drafts;
38 dirty = plansDirty = false;45 dirty = plansDirty = false;
39 state.value = "saved";46 state.value = "saved";
40 version.value++;47 version.value++;
@@ -125,6 +132,28 @@ export function useScript(onSaved?: () => void) {
125 }132 }
126 }133 }
127 134
135+ /**
136+ * Une opération sur les versions : ce qui attend d'être enregistré l'est d'abord (dans la version
137+ * en cours), puis on relit le script, qui a pu changer de version.
138+ */
139+ async function onDrafts(request: () => Promise<{ drafts: Draft[] }>): Promise<void> {
140+ await Promise.all([save(), savePlans(), saveSetups()]);
141+ try {
142+ drafts.value = (await request()).drafts;
143+ } catch (e) {
144+ toast(t("script.toasts.draftError", { error: (e as Error).message }), true);
145+ return;
146+ }
147+ await load();
148+ refresh().catch(() => {});
149+ }
150+
151+ const draftUrl = (id: string) => `${url}/drafts/${id}`;
152+ const forkDraft = () => onDrafts(() => post(`${url}/drafts`));
153+ const activateDraft = (id: string) => onDrafts(() => post(`${draftUrl(id)}/activate`));
154+ const renameDraft = (id: string, name: string) => onDrafts(() => patchJson(draftUrl(id), { name }));
155+ const deleteDraft = (id: string) => onDrafts(() => del(draftUrl(id)));
156+
128 onMounted(load);157 onMounted(load);
129 onBeforeUnmount(() => {158 onBeforeUnmount(() => {
130 save();159 save();
@@ -132,7 +161,8 @@ export function useScript(onSaved?: () => void) {
132 saveSetups();161 saveSetups();
133 });162 });
134 return {163 return {
135- blocks, plans, setups, sections, version, state, load, set, save, setPlans, updatePlan, removePlan, setSetups,164+ blocks, plans, setups, sections, drafts, version, state, load, set, save, setPlans, updatePlan, removePlan, setSetups,
165+ forkDraft, activateDraft, renameDraft, deleteDraft,
136 };166 };
137 }167 }
138 168