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.
c821b56 parent: 0d901d7 modified
web/src/i18n/locales/en/script.ts +11 -0 | @@ -20,6 +20,16 @@ export default { | ||
| 20 | 20 | chosenAngle: "Chosen angle", |
| 21 | 21 | rewriteLink: "Rewrite from the angle", |
| 22 | 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 | 33 | save: { saved: "saved automatically", saving: "saving…", error: "save failed" }, |
| 24 | 34 | research: { |
| 25 | 35 | placeholder: "Paste a link or a source title", |
| @@ -40,6 +50,7 @@ export default { | ||
| 40 | 50 | scriptLoadError: "Couldn’t load the script: {error}", |
| 41 | 51 | plansSaveError: "Couldn’t save shots: {error}", |
| 42 | 52 | setupsSaveError: "Couldn’t save setups: {error}", |
| 53 | + draftError: "Couldn’t update script versions: {error}", | |
| 43 | 54 | }, |
| 44 | 55 | editor: { |
| 45 | 56 | 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 { | ||
| 20 | 20 | chosenAngle: "Angle retenu", |
| 21 | 21 | rewriteLink: "Réécrire depuis l’angle", |
| 22 | 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 | 33 | save: { saved: "enregistré automatiquement", saving: "enregistrement…", error: "échec de l’enregistrement" }, |
| 24 | 34 | research: { |
| 25 | 35 | placeholder: "Colle un lien ou un titre de source", |
| @@ -40,6 +50,7 @@ export default { | ||
| 40 | 50 | scriptLoadError: "Lecture du script impossible : {error}", |
| 41 | 51 | plansSaveError: "Enregistrement des plans impossible : {error}", |
| 42 | 52 | setupsSaveError: "Enregistrement des setups impossible : {error}", |
| 53 | + draftError: "Opération sur les versions impossible : {error}", | |
| 43 | 54 | }, |
| 44 | 55 | editor: { |
| 45 | 56 | 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 { | ||
| 202 | 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 | 234 | function writeScript(): void { |
| 206 | 235 | act("/script/write"); |
| 207 | 236 | } |
| @@ -307,6 +336,17 @@ function setNote(i: number, n: string): void { | ||
| 307 | 336 | {{ t("script.writing") }}<template v-if="project.runtime.jobs.script?.activity"> · {{ project.runtime.jobs.script.activity }}</template> |
| 308 | 337 | </span> |
| 309 | 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 | 350 | <button class="btn sm" @click="editor?.selectAll()">{{ t("script.selectAll") }}</button> |
| 311 | 351 | <button class="btn sm" @click="copy(md, t('script.scriptCopied'))">{{ t("script.copyMarkdown") }}</button> |
| 312 | 352 | <button class="btn sm" @click="download">{{ t("script.exportMd") }}</button> |
| @@ -479,6 +519,8 @@ function setNote(i: number, n: string): void { | ||
| 479 | 519 | .center { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 8px; } |
| 480 | 520 | .toolbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } |
| 481 | 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 | 524 | .hint { flex: 1 1 200px; min-width: 0; } |
| 483 | 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 | 526 | .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 @@ | ||
| 1 | 1 | /** Le script du projet en blocs et ses plans (script.json côté serveur) : chargement et enregistrement automatique. */ |
| 2 | 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 | 4 | import { t } from "../../../i18n"; |
| 5 | 5 | import { toast } from "../../../lib/toast"; |
| 6 | 6 | import { refresh, store } from "../../store"; |
| @@ -10,6 +10,9 @@ import type { SaveState } from "./doc"; | ||
| 10 | 10 | /** Un bloc du texte : paragraphe, titre, citation, liste… avec son Markdown. L'id ne change pas à l'édition. */ |
| 11 | 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 | 16 | export function useScript(onSaved?: () => void) { |
| 14 | 17 | const url = `/api/projects/${store.project!.id}/script`; |
| 15 | 18 | /** null tant que le script n'est pas chargé */ |
| @@ -18,6 +21,7 @@ export function useScript(onSaved?: () => void) { | ||
| 18 | 21 | const setups = shallowRef<Setup[]>([]); |
| 19 | 22 | /** le setup de chaque section : id du bloc titre → id du setup */ |
| 20 | 23 | const sections = shallowRef<Record<string, string>>({}); |
| 24 | + const drafts = shallowRef<Draft[]>([]); | |
| 21 | 25 | /** change à chaque chargement depuis le serveur : l'éditeur repart de ces blocs */ |
| 22 | 26 | const version = ref(0); |
| 23 | 27 | const state = ref<SaveState>("saved"); |
| @@ -30,11 +34,14 @@ export function useScript(onSaved?: () => void) { | ||
| 30 | 34 | |
| 31 | 35 | async function load(): Promise<void> { |
| 32 | 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 | 40 | blocks.value = data.blocks; |
| 35 | 41 | plans.value = data.plans; |
| 36 | 42 | setups.value = data.setups; |
| 37 | 43 | sections.value = data.sections; |
| 44 | + drafts.value = data.drafts; | |
| 38 | 45 | dirty = plansDirty = false; |
| 39 | 46 | state.value = "saved"; |
| 40 | 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 | 157 | onMounted(load); |
| 129 | 158 | onBeforeUnmount(() => { |
| 130 | 159 | save(); |
| @@ -132,7 +161,8 @@ export function useScript(onSaved?: () => void) { | ||
| 132 | 161 | saveSetups(); |
| 133 | 162 | }); |
| 134 | 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 | |
| @@ -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 | ||