UNPKG

alinea

Version:

[![npm](https://img.shields.io/npm/v/alinea.svg)](https://npmjs.org/package/alinea) [![install size](https://packagephobia.com/badge?p=alinea)](https://packagephobia.com/result?p=alinea)

551 lines (549 loc) 17.9 kB
import { atomFamily, unwrap } from "../../chunks/chunk-ZHH24SIG.js"; import { atom } from "../../chunks/chunk-OBOPLPUQ.js"; import "../../chunks/chunk-U5RRZUYZ.js"; // src/dashboard/atoms/EntryEditorAtoms.ts import { Media } from "alinea/backend"; import { EntryPhase, ROOT_KEY, Type, createId, createYDoc, parseYDoc } from "alinea/core"; import { Entry } from "alinea/core/Entry"; import { entryFileName, entryFilepath, entryInfo, entryUrl } from "alinea/core/EntryFilenames"; import { MutationType } from "alinea/core/Mutation"; import { base64 } from "alinea/core/util/Encoding"; import { createEntryRow } from "alinea/core/util/EntryRows"; import { entries, fromEntries, values } from "alinea/core/util/Objects"; import * as paths from "alinea/core/util/Paths"; import { FormAtoms } from "alinea/dashboard/atoms/FormAtoms"; import { keepPreviousData } from "alinea/dashboard/util/KeepPreviousData"; import { debounceAtom } from "../util/DebounceAtom.js"; import { clientAtom, configAtom } from "./DashboardAtoms.js"; import { entryRevisionAtoms, graphAtom, mutateAtom } from "./DbAtoms.js"; import { entryEditsAtoms } from "./Edits.js"; import { errorAtom } from "./ErrorAtoms.js"; import { locationAtom } from "./LocationAtoms.js"; import { yAtom } from "./YAtom.js"; var EditMode = /* @__PURE__ */ ((EditMode2) => { EditMode2["Editing"] = "editing"; EditMode2["Diff"] = "diff"; return EditMode2; })(EditMode || {}); var previewTokenAtom = atom(async (get) => { const client = get(clientAtom); return client.previewToken(); }); var EntryTransition = /* @__PURE__ */ ((EntryTransition2) => { EntryTransition2[EntryTransition2["SaveDraft"] = 0] = "SaveDraft"; EntryTransition2[EntryTransition2["SaveTranslation"] = 1] = "SaveTranslation"; EntryTransition2[EntryTransition2["PublishEdits"] = 2] = "PublishEdits"; EntryTransition2[EntryTransition2["RestoreRevision"] = 3] = "RestoreRevision"; EntryTransition2[EntryTransition2["PublishDraft"] = 4] = "PublishDraft"; EntryTransition2[EntryTransition2["DiscardDraft"] = 5] = "DiscardDraft"; EntryTransition2[EntryTransition2["ArchivePublished"] = 6] = "ArchivePublished"; EntryTransition2[EntryTransition2["PublishArchived"] = 7] = "PublishArchived"; EntryTransition2[EntryTransition2["DeleteFile"] = 8] = "DeleteFile"; EntryTransition2[EntryTransition2["DeleteArchived"] = 9] = "DeleteArchived"; return EntryTransition2; })(EntryTransition || {}); var entryTransitionAtoms = atomFamily((entryId) => { return atom( void 0 ); }); var entryEditorAtoms = atomFamily( ({ locale, i18nId }) => { return atom(async (get) => { if (!i18nId) return void 0; const config = get(configAtom); const client = get(clientAtom); const graph = await get(graphAtom); const search = locale ? { i18nId, locale } : { i18nId }; let entry = await graph.preferDraft.maybeGet( Entry(search) ); if (!entry) { const { searchParams } = get(locationAtom); const preferredLanguage = searchParams.get("from"); entry = await graph.preferDraft.maybeGet( Entry({ i18nId }).where( preferredLanguage ? Entry.locale.is(preferredLanguage) : true ) ); } if (!entry) return void 0; const entryId = entry.entryId; get(entryRevisionAtoms(entry.i18nId)); const type = config.schema[entry.type]; const edits = get(entryEditsAtoms(entryId)); const loadDraft = client.getDraft(entryId).then((draft) => { if (draft) { edits.applyRemoteUpdate(draft.draft); const matches = draft.fileHash === entry.fileHash; const isEmpty = !edits.hasData(); if (!matches || isEmpty) { edits.applyEntryData(type, entry.data); } } else { edits.applyEntryData(type, entry.data); } }).catch(() => { edits.applyEntryData(type, entry.data); }); if (!edits.hasData()) await loadDraft; const versions = await graph.all.find( Entry({ entryId }).select({ ...Entry, parents({ parents: parents2 }) { return parents2().select(Entry.i18nId); } }) ); const { parents } = await graph.preferDraft.get( Entry({ entryId }).select({ parents({ parents: parents2 }) { return parents2().select({ entryId: Entry.entryId, path: Entry.path }); } }) ); const translations = await graph.preferDraft.find( Entry({ i18nId }).where(Entry.locale.isNotNull(), Entry.entryId.isNot(entryId)).select({ locale: Entry.locale, entryId: Entry.entryId }) ); const parentLink = entry.parent && await graph.preferDraft.get( Entry({ entryId: entry.parent }).select(Entry.i18nId) ); const parentNeedsTranslation = parentLink ? !await graph.preferDraft.maybeGet( Entry({ i18nId: parentLink, locale }) ) : false; if (versions.length === 0) return void 0; const phases = fromEntries( versions.map((version) => [version.phase, version]) ); const availablePhases = values(EntryPhase).filter( (phase) => phases[phase] !== void 0 ); const previewToken = await get(previewTokenAtom); return createEntryEditor({ parents, translations, parentNeedsTranslation, previewToken, client, config, entryId, versions, phases, availablePhases, edits }); }); }, (a, b) => a.locale === b.locale && a.i18nId === b.i18nId ); var showHistoryAtom = atom(false); function createEntryEditor(entryData) { const { config, availablePhases, edits } = entryData; const activePhase = availablePhases[0]; const activeVersion = entryData.phases[activePhase]; const type = config.schema[activeVersion.type]; const docs = fromEntries( entries(entryData.phases).map(([phase, version]) => [ phase, createYDoc(type, version) ]) ); const yDoc = edits.doc; const hasChanges = edits.hasChanges; const draftEntry = keepPreviousData( yAtom(edits.doc.getMap(ROOT_KEY), getDraftEntry) ); const editMode = atom("editing" /* Editing */); const view = Type.meta(type).view; const previewRevision = atom( void 0 ); const showHistory = atom( (get) => get(showHistoryAtom), (get, set, value) => { set(showHistoryAtom, value); if (!value) set(previewRevision, void 0); } ); const transition = entryTransitionAtoms(activeVersion.entryId); const phaseInUrl = atom((get) => { const { search } = get(locationAtom); const phaseInSearch = search.slice(1); if (availablePhases.includes(phaseInSearch)) return phaseInSearch; return void 0; }); const selectedPhase = atom((get) => { return get(phaseInUrl) ?? activePhase; }); function entryFile(entry, parentPaths) { return entryFileName( config, entry, parentPaths ?? entryData.parents.map((p) => p.path) ); } const transact = atom( null, async (get, set, options) => { const currentTransition = get(transition); if (currentTransition) await currentTransition.done; const currentChanges = get(hasChanges); if (options.clearChanges) set(hasChanges, false); const done = options.action().catch((error) => { if (options.clearChanges) set(hasChanges, currentChanges); set(errorAtom, options.errorMessage, error); }).finally(() => { set(transition, void 0); }); set(transition, { transition: options.transition, done }); return done; } ); const saveDraft = atom(null, async (get, set) => { const update = base64.stringify(edits.getLocalUpdate()); const entry = await getDraftEntry({ phase: EntryPhase.Published, path: activeVersion.path }); const mutation = { type: MutationType.Edit, previousFile: entryFile(activeVersion), file: entryFile(entry), entryId: activeVersion.entryId, entry, update }; return set(transact, { clearChanges: true, transition: 0 /* SaveDraft */, action: () => set(mutateAtom, mutation), errorMessage: "Could not complete save action, please try again later" }); }); const saveTranslation = atom(null, async (get, set, locale) => { const { preferDraft: active } = await get(graphAtom); const parentLink = activeVersion.parent && await active.get( Entry({ entryId: activeVersion.parent }).select(Entry.i18nId) ); if (activeVersion.parent && !parentLink) throw new Error("Parent not found"); const parentData = parentLink ? await active.locale(locale).get( Entry({ i18nId: parentLink }).select({ entryId: Entry.entryId, path: Entry.path, paths({ parents }) { return parents().select(Entry.path); } }) ) : void 0; if (activeVersion.parent && !parentData) throw new Error("Parent not translated"); const parentPaths = parentData?.paths ? parentData.paths.concat(parentData.path) : []; const entryId = createId(); const entry = await getDraftEntry({ entryId, phase: EntryPhase.Published, parent: parentData?.entryId, parentPaths, locale }); const mutation = { type: MutationType.Create, file: entryFile(entry, parentPaths), entryId, entry }; return set(transact, { clearChanges: true, transition: 1 /* SaveTranslation */, action: () => set(mutateAtom, mutation), errorMessage: "Could not complete translate action, please try again later" }); }); const publishEdits = atom(null, async (get, set) => { const currentFile = entryFile(activeVersion); const update = base64.stringify(edits.getLocalUpdate()); const entry = await getDraftEntry({ phase: EntryPhase.Published }); const mutations = []; const editedFile = entryFile(entry); mutations.push({ type: MutationType.Edit, previousFile: currentFile, file: editedFile, entryId: activeVersion.entryId, entry, update }); return set(transact, { clearChanges: true, transition: 2 /* PublishEdits */, action: () => set(mutateAtom, ...mutations), errorMessage: "Could not complete publish action, please try again later" }); }); const restoreRevision = atom(null, async (get, set) => { const revision = get(previewRevision); if (!revision) return; const data = await get(revisionData(revision)); const { edits: edits2 } = entryData; edits2.applyEntryData(type, data); const update = base64.stringify(edits2.getLocalUpdate()); const entry = await getDraftEntry({ phase: EntryPhase.Published, path: activeVersion.path }); const editedFile = entryFile(entry); const mutation = { type: MutationType.Edit, previousFile: editedFile, file: editedFile, entryId: activeVersion.entryId, entry, update }; return set(transact, { clearChanges: true, transition: 3 /* RestoreRevision */, action: () => set(mutateAtom, mutation), errorMessage: "Could not complete publish action, please try again later" }); }); const publishDraft = atom(null, (get, set) => { const mutation = { type: MutationType.Publish, phase: EntryPhase.Draft, entryId: activeVersion.entryId, file: entryFile(activeVersion) }; return set(transact, { transition: 4 /* PublishDraft */, action: () => set(mutateAtom, mutation), errorMessage: "Could not complete publish action, please try again later" }); }); const discardDraft = atom(null, (get, set) => { const mutation = { type: MutationType.Discard, entryId: activeVersion.entryId, file: entryFile(activeVersion) }; return set(transact, { transition: 5 /* DiscardDraft */, action: () => set(mutateAtom, mutation), errorMessage: "Could not complete discard action, please try again later" }); }); const archivePublished = atom(null, (get, set) => { const published = entryData.phases[EntryPhase.Published]; const mutation = { type: MutationType.Archive, entryId: published.entryId, file: entryFile(published) }; return set(transact, { transition: 6 /* ArchivePublished */, action: () => set(mutateAtom, mutation), errorMessage: "Could not complete archive action, please try again later" }); }); const publishArchived = atom(null, (get, set) => { const archived = entryData.phases[EntryPhase.Archived]; const mutation = { type: MutationType.Publish, phase: EntryPhase.Archived, entryId: archived.entryId, file: entryFile(archived) }; return set(transact, { transition: 7 /* PublishArchived */, action: () => set(mutateAtom, mutation), errorMessage: "Could not complete publish action, please try again later" }); }); const deleteFile = atom(null, (get, set) => { const result = confirm("Are you sure you want to delete this file?"); if (!result) return; const published = entryData.phases[EntryPhase.Published]; const file = published.data; const mutation = { type: MutationType.FileRemove, entryId: published.entryId, workspace: published.workspace, location: Media.ORIGINAL_LOCATION in file ? file[Media.ORIGINAL_LOCATION] : file.location, file: entryFile(published), replace: false }; return set(transact, { transition: 8 /* DeleteFile */, action: () => set(mutateAtom, mutation), errorMessage: "Could not complete delete action, please try again later" }); }); const deleteArchived = atom(null, (get, set) => { const archived = entryData.phases[EntryPhase.Archived]; const mutation = { type: MutationType.Remove, entryId: archived.entryId, file: entryFile(archived) }; return set(transact, { transition: 9 /* DeleteArchived */, action: () => set(mutateAtom, mutation), errorMessage: "Could not complete delete action, please try again later" }); }); async function getDraftEntry(options = {}) { const data = parseYDoc(type, yDoc); const phase = options.phase ?? activeVersion.phase; const locale = options.locale ?? activeVersion.locale; const path = options.path ?? data.path ?? activeVersion.path; const entryId = options.entryId ?? activeVersion.entryId; const parent = options.parent ?? activeVersion.parent; const parentPaths = options.parentPaths ?? entryData.parents.map((p) => p.path); const draftEntry2 = { ...activeVersion, ...data, entryId, parent, locale, path, phase }; const filePath = entryFilepath(config, draftEntry2, parentPaths); const parentDir = paths.dirname(filePath); const extension = paths.extname(filePath); const fileName = paths.basename(filePath, extension); const [entryPath] = entryInfo(fileName); const childrenDir = paths.join(parentDir, entryPath); const urlMeta = { locale, path, phase, parentPaths }; const url = entryUrl(type, urlMeta); return createEntryRow(config, { ...draftEntry2, parentDir, childrenDir, filePath, url }); } const revisionsAtom = atom(async (get) => { const client = get(clientAtom); const file = entryFile(activeVersion); const revisions = await client.revisions(file); revisions.sort((a, b) => b.createdAt - a.createdAt); return revisions; }); const revisionData = atomFamily( (params) => { return atom((get) => { const client = get(clientAtom); return client.revisionData(params.file, params.ref); }); }, (a, b) => a.file === b.file && a.ref === b.ref ); const revisionDocState = atomFamily( (params) => { return atom(async (get) => { const data = await get(revisionData(params)); return createYDoc(type, { ...activeVersion, data }); }); }, (a, b) => a.file === b.file && a.ref === b.ref ); const selectedState = atom((get) => { const selected = get(selectedPhase); if (selected === activePhase) return edits.doc; return docs[selected]; }); const activeTitle = yAtom(edits.root, () => edits.root.get("title")); const revisionState = atom((get) => { const revision = get(previewRevision); return revision ? get(revisionDocState(revision)) : void 0; }); const identity = (prev) => prev; const currentDoc = atom((get) => { return get(unwrap(revisionState, identity)) ?? get(selectedState); }); const form = atom((get) => { const doc = get(currentDoc); return new FormAtoms(type, doc.getMap(ROOT_KEY), { readOnly: doc !== edits.doc }); }); const yUpdate = debounceAtom(edits.yUpdate, 250); const discardEdits = edits.resetChanges; const isLoading = edits.isLoading; return { ...entryData, transition, revisionId: createId(), activePhase, phaseInUrl, selectedPhase, entryData, editMode, activeVersion, type, yUpdate, activeTitle, hasChanges, draftEntry, saveDraft, publishEdits, restoreRevision, publishDraft, discardDraft, archivePublished, publishArchived, deleteFile, deleteArchived, saveTranslation, discardEdits, isLoading, showHistory, revisionsAtom, previewRevision, form, view }; } export { EditMode, EntryTransition, createEntryEditor, entryEditorAtoms };