UNPKG

alinea

Version:
623 lines (621 loc) 19.5 kB
import { atomFamily, unwrap } from "../../chunks/chunk-WDCPVJJC.js"; import { atom } from "../../chunks/chunk-WJ67RR7S.js"; import "../../chunks/chunk-NZLE2WMY.js"; // src/dashboard/atoms/EntryEditorAtoms.ts import { createYDoc, DOC_KEY, parseYDoc } from "alinea/core/Doc"; import { Entry } from "alinea/core/Entry"; import { createRecord } from "alinea/core/EntryRecord"; import { Field } from "alinea/core/Field"; import { createId } from "alinea/core/Id"; import { getType } from "alinea/core/Internal"; import { createFilePatch } from "alinea/core/source/FilePatch"; import { Root } from "alinea/core/Root"; import { Type } from "alinea/core/Type"; import { entryFileName, entryFilepath, entryInfo, entryUrl } from "alinea/core/util/EntryFilenames"; import { createEntryRow } from "alinea/core/util/EntryRows"; import { entries, fromEntries } from "alinea/core/util/Objects"; import * as paths from "alinea/core/util/Paths"; import { Workspace } from "alinea/core/Workspace"; import { JsonLoader } from "alinea/backend/loader/JsonLoader"; import { FormAtoms } from "alinea/dashboard/atoms/FormAtoms"; import { keepPreviousData } from "alinea/dashboard/util/KeepPreviousData"; import { encodePreviewPayload } from "alinea/preview/PreviewPayload"; import { debounceAtom } from "../util/DebounceAtom.js"; import { clientAtom, configAtom } from "./DashboardAtoms.js"; import { dbAtom, dbMetaAtom, entryRevisionAtoms } from "./DbAtoms.js"; import { entryEditsAtoms } from "./Edits.js"; import { errorAtom } from "./ErrorAtoms.js"; import { locationAtom } from "./LocationAtoms.js"; import { yAtom } from "./YAtom.js"; var decoder = new TextDecoder(); var EditMode = /* @__PURE__ */ ((EditMode2) => { EditMode2["Editing"] = "editing"; EditMode2["Diff"] = "diff"; return EditMode2; })(EditMode || {}); 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["UnpublishDraft"] = 5] = "UnpublishDraft"; EntryTransition2[EntryTransition2["DiscardDraft"] = 6] = "DiscardDraft"; EntryTransition2[EntryTransition2["ArchivePublished"] = 7] = "ArchivePublished"; EntryTransition2[EntryTransition2["PublishArchived"] = 8] = "PublishArchived"; EntryTransition2[EntryTransition2["DeleteFile"] = 9] = "DeleteFile"; EntryTransition2[EntryTransition2["DeleteEntry"] = 10] = "DeleteEntry"; return EntryTransition2; })(EntryTransition || {}); var entryTransitionAtoms = atomFamily((id) => { return atom(void 0); }); var entryEditorAtoms = atomFamily( ({ id, locale: searchLocale }) => { return atom(async (get) => { if (!id) return void 0; get(entryRevisionAtoms(id)); const config = get(configAtom); const client = get(clientAtom); const graph = get(dbAtom); let entry = await graph.first({ select: Entry, id, locale: searchLocale, status: "preferDraft" }); if (!entry) { const { searchParams } = get(locationAtom); const preferredLanguage = searchParams.get("from"); entry = await graph.first({ select: Entry, locale: preferredLanguage ?? void 0, id, status: "preferDraft" }); } if (!entry) return void 0; const entryId = entry.id; const locale = entry.locale; const untranslated = Boolean( entry.locale && searchLocale !== entry.locale ); const edits = get( entryEditsAtoms( untranslated ? { ...entry, data: { ...entry.data, path: void 0 } } : entry ) ); const versions = await graph.find({ select: { ...Entry, parents: { edge: "parents", select: Entry.id } }, locale, id: entryId, status: "all" }); const withParents = await graph.first({ select: { parents: { edge: "parents", select: { id: Entry.id, path: Entry.path, status: Entry.status, main: Entry.main } } }, id: entryId, locale, status: "preferDraft" }); const translations = await graph.find({ select: { locale: Entry.locale, entryId: Entry.id }, id: entryId, filter: { _locale: { isNot: locale } }, status: "preferDraft" }); const parentLink = entry.parentId && await graph.first({ select: Entry.id, id: entry.parentId, locale: searchLocale, status: "preferDraft" }); const parentNeedsTranslation = entry.parentId ? !parentLink : false; const parents = withParents?.parents ?? []; const canPublish = parents.every((parent) => parent.status === "published"); const canDelete = !entry.seeded; if (versions.length === 0) return void 0; const statuses = fromEntries( versions.map((version) => [version.status, version]) ); const availableStatuses = Array( "draft", "published", "archived" ).filter((status) => statuses[status] !== void 0); return createEntryEditor({ parents, canDelete, canPublish, translations, untranslated, parentNeedsTranslation, client, config, entryId, versions, statuses, availableStatuses, edits }); }); }, (a, b) => a.locale === b.locale && a.id === b.id ); var showHistoryAtom = atom(false); function createEntryEditor(entryData) { const { config, availableStatuses, edits } = entryData; const activeStatus = availableStatuses[0]; const activeVersion = entryData.statuses[activeStatus]; const type = config.schema[activeVersion.type]; const docs = fromEntries( entries(entryData.statuses).map(([status, version]) => [ status, createYDoc(type, version) ]) ); const yDoc = edits.doc; const hasChanges = edits.hasChanges; const draftEntry = keepPreviousData( yAtom(edits.doc.getMap(DOC_KEY), getDraftEntry) ); const editMode = atom("editing" /* Editing */); const view = getType(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.id); const statusInUrl = atom((get) => { const { search } = get(locationAtom); const statusInSearch = search.slice(1); if (availableStatuses.includes(statusInSearch)) return statusInSearch; return void 0; }); const selectedStatus = atom((get) => { return get(statusInUrl) ?? activeStatus; }); function entryFile(entry, parentPaths) { return entryFileName( config, entry, parentPaths ?? entryData.parents.map((p) => p.path) ); } const action = atom( null, (get, set, { transition: entryTransition, result, errorMessage, clearChanges = false }) => { const now = Date.now(); set(transition, entryTransition); return result.then(async () => { if (clearChanges) set(hasChanges, false); }).catch((error) => { set(errorAtom, errorMessage, error); }).finally(() => { const duration = Date.now() - now; const delay = Math.max(0, 500 - duration); setTimeout(() => { set(transition, void 0); }, delay); }); } ); const parent = entryData.parents.at(-1); const parentArchived = parent?.status === "archived"; const parentUnpublished = parent?.status === "draft" && parent.main; const saveDraft = atom(null, async (get, set) => { if (parentArchived || parentUnpublished) return set(publishEdits); const db = get(dbAtom); const entry = await getDraftEntry({ status: "published", path: activeVersion.path }); return set(action, { transition: 0 /* SaveDraft */, result: db.create({ type, id: entry.id, locale: entry.locale, status: "draft", set: entry.data, overwrite: true }), errorMessage: "Could not complete draft action, please try again later", clearChanges: true }); }); const saveTranslation = atom(null, async (get, set, locale) => { const db = get(dbAtom); const parentId = activeVersion.parentId; const parentData = parentId ? await db.get({ select: { entryId: Entry.id, path: Entry.path, paths: { edge: "parents", select: Entry.path } }, id: parentId, locale, status: "preferDraft" }) : void 0; if (parentId && !parentData) throw new Error("Parent not translated"); const parentPaths = parentData?.paths ? parentData.paths.concat(parentData.path) : []; const entry = await getDraftEntry({ id: activeVersion.id, status: "published", parent: parentData?.entryId, parentPaths, locale }); return set(action, { transition: 1 /* SaveTranslation */, result: db.create({ type, id: entry.id, parentId, locale, status: config.enableDrafts ? "draft" : "published", set: entry.data }), errorMessage: "Could not complete translate action, please try again later", clearChanges: true }); }); const errorsAtom = atom((get) => { return get(get(form).errors); }); const confirmErrorsAtom = atom(null, (get) => { const errors = get(errorsAtom); if (errors.size > 0) { let errorMessage = ""; for (const [path, { field, error }] of errors.entries()) { const label = Field.label(field); const line = typeof error === "string" ? `${label}: ${error}` : label; errorMessage += ` \u2014 ${line}`; } const message = `These fields contains errors, are you sure you want to publish?${errorMessage}`; return confirm(message); } return true; }); const publishEdits = atom(null, async (get, set) => { if (!set(confirmErrorsAtom)) return; const db = get(dbAtom); const entry = await getDraftEntry({ status: "published" }); return set(action, { transition: 2 /* PublishEdits */, result: db.create({ type, id: entry.id, locale: entry.locale, status: "published", set: entry.data, overwrite: true }), errorMessage: "Could not complete publish action, please try again later", clearChanges: true }); }); const restoreRevision = atom(null, async (get, set) => { const revision = get(previewRevision); if (!revision) return; const data = await get(revisionData(revision)); if (!data) return; const db = get(dbAtom); const { edits: edits2 } = entryData; edits2.applyEntryData(data); const entry = await getDraftEntry({ status: "published", path: activeVersion.path }); return set(action, { transition: 3 /* RestoreRevision */, result: db.update({ type, id: entry.id, locale: entry.locale, status: "published", set: entry.data }), errorMessage: "Could not complete publish action, please try again later", clearChanges: true }); }); const publishDraft = atom(null, async (get, set) => { if (!set(confirmErrorsAtom)) return; const db = get(dbAtom); return set(action, { transition: 4 /* PublishDraft */, result: db.publish({ id: activeVersion.id, locale: activeVersion.locale, status: "draft" }), errorMessage: "Could not complete publish action, please try again later" }); }); const discardDraft = atom(null, async (get, set) => { const db = get(dbAtom); return set(action, { transition: 6 /* DiscardDraft */, result: db.discard({ id: activeVersion.id, locale: activeVersion.locale, status: "draft" }), errorMessage: "Could not complete discard action, please try again later" }); }); const unPublish = atom(null, async (get, set) => { const db = get(dbAtom); return set(action, { transition: 5 /* UnpublishDraft */, result: db.unpublish({ id: activeVersion.id, locale: activeVersion.locale }), errorMessage: "Could not complete unpublish action, please try again later" }); }); const archive = atom(null, async (get, set) => { const db = get(dbAtom); return set(action, { transition: 7 /* ArchivePublished */, result: db.archive({ id: activeVersion.id, locale: activeVersion.locale }), errorMessage: "Could not complete archive action, please try again later" }); }); const publishArchived = atom(null, async (get, set) => { const db = get(dbAtom); return set(action, { transition: 8 /* PublishArchived */, result: db.publish({ id: activeVersion.id, locale: activeVersion.locale, status: "archived" }), errorMessage: "Could not complete publish action, please try again later" }); }); const deleteMediaLibrary = atom(null, async (get, set) => { const result = confirm( "Are you sure you want to delete this folder and all its files?" ); if (!result) return; const db = get(dbAtom); return set(action, { transition: 10 /* DeleteEntry */, result: db.remove(activeVersion.id), errorMessage: "Could not complete delete action, please try again later" }); }); const deleteFile = atom(null, async (get, set) => { const result = confirm("Are you sure you want to delete this file?"); if (!result) return; const db = get(dbAtom); return set(action, { transition: 9 /* DeleteFile */, result: db.remove(activeVersion.id), errorMessage: "Could not complete delete action, please try again later" }); }); const deleteEntry = atom(null, async (get, set) => { const db = get(dbAtom); return set(action, { transition: 10 /* DeleteEntry */, result: db.remove(activeVersion.id), errorMessage: "Could not complete delete action, please try again later" }); }); async function getDraftEntry(options = {}) { const data = parseYDoc(type, yDoc); const status = options.status ?? activeVersion.status; const locale = options.locale ?? activeVersion.locale; const path = options.path ?? data.path ?? activeVersion.path; const id = options.id ?? activeVersion.id; const parent2 = options.parent ?? activeVersion.parentId; const parentPaths = options.parentPaths ?? entryData.parents.map((p) => p.path); const draftEntry2 = { ...activeVersion, ...data, id, parent: parent2, locale, path, status }; 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, status, parentPaths }; const url = entryUrl(type, urlMeta); return createEntryRow( config, { ...draftEntry2, parentDir, childrenDir, filePath, url }, status ); } 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)); const entry = data ? { ...activeVersion, data } : activeVersion; return createYDoc(type, entry); }); }, (a, b) => a.file === b.file && a.ref === b.ref ); const selectedState = atom((get) => { const selected = get(selectedStatus); if (selected === activeStatus) 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); const readOnly = doc !== edits.doc; return new FormAtoms(type, doc.getMap(DOC_KEY), "", { readOnly }); }); const yUpdate = debounceAtom(edits.yUpdate, 250); const previewPayload = atom(async (get) => { const sha = await get(dbMetaAtom); get(yUpdate); const status = get(selectedStatus); const baseText = decoder.decode( JsonLoader.format( config.schema, createRecord(activeVersion, activeVersion.status) ) ); const updated = await getDraftEntry({ status }); const patch = await createFilePatch( baseText, decoder.decode( JsonLoader.format(config.schema, createRecord(updated, status)) ) ); return encodePreviewPayload({ locale: activeVersion.locale, entryId: activeVersion.id, contentHash: sha, status, patch }); }); const discardEdits = edits.resetChanges; const preview = Type.preview(type) ?? Root.preview( config.workspaces[activeVersion.workspace][activeVersion.root] ) ?? Workspace.preview(config.workspaces[activeVersion.workspace]) ?? config.preview; const previewToken = atom(async (get) => { const client = get(clientAtom); return client.previewToken({ url: activeVersion.url }); }); return { ...entryData, transition, revisionId: createId(), activeStatus, statusInUrl, selectedStatus, entryData, editMode, activeVersion, type, previewPayload, activeTitle, hasChanges, draftEntry, saveDraft, publishEdits, restoreRevision, publishDraft, discardDraft, unPublish, archive, publishArchived, deleteFile, deleteMediaLibrary, deleteEntry, saveTranslation, discardEdits, showHistory, revisionsAtom, previewToken, previewRevision, preview, form, view }; } export { EditMode, EntryTransition, createEntryEditor, entryEditorAtoms };