alinea
Version:
[](https://npmjs.org/package/alinea) [](https://packagephobia.com/result?p=alinea)
551 lines (549 loc) • 17.9 kB
JavaScript
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
};