alinea
Version:
Headless git-based CMS
623 lines (621 loc) • 19.5 kB
JavaScript
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
};