Something works

This commit is contained in:
Zef Hemel
2022-03-09 12:25:42 +01:00
parent 5e34395407
commit 7ae3496749
19 changed files with 587 additions and 387 deletions
+2
View File
@@ -18,6 +18,7 @@
"@parcel/transformer-webmanifest": "2.3.2",
"@parcel/validator-typescript": "^2.3.2",
"@types/events": "^3.0.0",
"@types/lodash": "^4.14.179",
"@types/node": "^17.0.21",
"@types/react": "^17.0.39",
"@types/react-dom": "^17.0.11",
@@ -52,6 +53,7 @@
"@parcel/service-worker": "^2.3.2",
"dexie": "^3.2.1",
"idb": "^7.0.0",
"lodash": "^4.17.21",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"socket.io-client": "^4.4.1"
+149 -14
View File
@@ -1,35 +1,129 @@
import { EditorView, ViewPlugin, ViewUpdate } from "@codemirror/view";
import { HttpRemoteSpace, Space } from "./space";
// TODO:
// Send state to client
// Shape of editor.editorView.state.toJSON({"cursors": cursorField})
// From there import it
// EditorState.fromJSON(js, {extensions: cursorField}, {cursors: cursorField})
import {
Update,
receiveUpdates,
sendableUpdates,
collab,
getSyncedVersion,
receiveUpdates,
sendableUpdates,
} from "@codemirror/collab";
import { PageMeta } from "./types";
import { Text } from "@codemirror/state";
import { EditorState, StateEffect, StateField, Text } from "@codemirror/state";
import {
Decoration,
DecorationSet,
EditorView,
ViewPlugin,
ViewUpdate,
WidgetType,
} from "@codemirror/view";
import { cursorEffect } from "./cursorEffect";
import { HttpRemoteSpace } from "./space";
export class Document {
text: Text;
meta: PageMeta;
version: number;
constructor(text: Text, meta: PageMeta) {
constructor(text: Text, version: number) {
this.text = text;
this.meta = meta;
this.version = version;
}
}
let meId = "";
const cursorField = StateField.define<DecorationSet>({
create() {
return Decoration.none;
},
update(cursors, tr) {
cursors = cursors.map(tr.changes);
for (let e of tr.effects) {
if (e.is(cursorEffect)) {
const newCursorDecoration = Decoration.widget({
widget: new CursorWidget(e.value.userId, e.value.color, e.value.pos),
side: 1,
});
cursors = cursors.update({
filter: (from, to, d) => !d.eq(newCursorDecoration),
// add: [newCursorDecoration.range(e.value.pos)],
sort: true,
});
}
}
// console.log("New cursors", cursors.size);
return cursors;
},
provide: (f) => EditorView.decorations.from(f),
fromJSON(cursorJSONs) {
let cursors = [];
for (let cursorJSON of cursorJSONs) {
cursors.push(
Decoration.widget({
widget: new CursorWidget(
cursorJSON.userId,
cursorJSON.color,
cursorJSON.pos
),
side: 1,
}).range(cursorJSON.pos)
);
}
return Decoration.set(cursors);
},
toJSON(cursors) {
let cursor = cursors.iter();
let results = [];
while (cursor.value) {
results.push({ ...cursor.value.spec.widget });
cursor.next();
}
return results;
},
});
class CursorWidget extends WidgetType {
userId: string;
color: string;
pos: number;
constructor(userId: string, color: string, pos: number) {
super();
this.userId = userId;
this.color = color;
this.pos = pos;
}
eq(other: CursorWidget) {
return other.userId == this.userId;
}
toDOM() {
let el = document.createElement("span");
el.className = "other-cursor";
el.style.backgroundColor = this.color;
if (this.userId == meId) {
el.style.display = "none";
}
return el;
}
}
export function collabExtension(
pageName: string,
clientID: string,
startVersion: number,
space: HttpRemoteSpace,
reloadCallback: () => void
) {
meId = clientID;
let plugin = ViewPlugin.fromClass(
class {
private pushing = false;
private done = false;
private failedPushes = 0;
constructor(private view: EditorView) {
if (pageName) {
@@ -38,19 +132,47 @@ export function collabExtension(
}
update(update: ViewUpdate) {
if (update.docChanged) this.push();
if (update.selectionSet) {
let pos = update.state.selection.main.head;
console.log("New pos", pos);
// return;
setTimeout(() => {
update.view.dispatch({
effects: [
cursorEffect.of({ pos: pos, userId: clientID, color: "red" }),
],
});
});
}
let foundEffect = false;
for (let tx of update.transactions) {
if (tx.effects.some((e) => e.is(cursorEffect))) {
foundEffect = true;
}
}
if (update.docChanged || foundEffect) this.push();
}
async push() {
let updates = sendableUpdates(this.view.state);
if (this.pushing || !updates.length) return;
console.log("Updates", updates);
this.pushing = true;
let version = getSyncedVersion(this.view.state);
let success = await space.pushUpdates(pageName, version, updates);
this.pushing = false;
if (!success) {
reloadCallback();
this.failedPushes++;
if (this.failedPushes > 10) {
// Not sure if 10 is a good number, but YOLO
console.log("10 pushes failed, reloading");
reloadCallback();
return this.destroy();
}
console.log("Push failed temporarily, but will try again");
} else {
this.failedPushes = 0;
}
// Regardless of whether the push failed or new updates came in
@@ -64,7 +186,9 @@ export function collabExtension(
while (!this.done) {
let version = getSyncedVersion(this.view.state);
let updates = await space.pullUpdates(pageName, version);
this.view.dispatch(receiveUpdates(this.view.state, updates));
let d = receiveUpdates(this.view.state, updates);
console.log("Received", d);
this.view.dispatch(d);
}
}
@@ -73,5 +197,16 @@ export function collabExtension(
}
}
);
return [collab({ startVersion }), plugin];
return [
collab({
startVersion,
clientID,
sharedEffects: (tr) => {
return tr.effects.filter((e) => e.is(cursorEffect));
},
}),
cursorField,
plugin,
];
}
+2 -2
View File
@@ -9,11 +9,11 @@ export function PageNavigator({
}: {
allPages: PageMeta[];
onNavigate: (page: string | undefined) => void;
currentPage?: PageMeta;
currentPage?: string;
}) {
let options: Option[] = [];
for (let pageMeta of allPages) {
if (currentPage && currentPage.name == pageMeta.name) {
if (currentPage && currentPage === pageMeta.name) {
continue;
}
// Order by last modified date in descending order
+2 -8
View File
@@ -1,13 +1,7 @@
import { EditorView } from "@codemirror/view";
import * as util from "../util";
export function StatusBar({
isSaved,
editorView,
}: {
isSaved: boolean;
editorView?: EditorView;
}) {
export function StatusBar({ editorView }: { editorView?: EditorView }) {
let wordCount = 0,
readingTime = 0;
if (editorView) {
@@ -17,7 +11,7 @@ export function StatusBar({
}
return (
<div id="bottom">
{wordCount} words | {readingTime} min | {isSaved ? "Saved" : "Edited"}
{wordCount} words | {readingTime} min
</div>
);
}
+2 -2
View File
@@ -13,7 +13,7 @@ export function TopBar({
currentPage,
onClick,
}: {
currentPage?: PageMeta;
currentPage?: string;
onClick: () => void;
}) {
return (
@@ -22,7 +22,7 @@ export function TopBar({
<span className="icon">
<FontAwesomeIcon icon={faFileLines} />
</span>
<span className="current-page">{prettyName(currentPage?.name)}</span>
<span className="current-page">{prettyName(currentPage)}</span>
</div>
</div>
);
+11
View File
@@ -0,0 +1,11 @@
import { StateEffect } from "@codemirror/state";
export const cursorEffect = StateEffect.define<{
pos: number;
userId: string;
color: string;
}>({
map({ pos, userId, color }, changes) {
return { pos: changes.mapPos(pos), userId, color };
},
});
+80 -141
View File
@@ -20,6 +20,8 @@ import {
keymap,
} from "@codemirror/view";
import { debounce } from "lodash";
import React, { useEffect, useReducer } from "react";
import ReactDOM from "react-dom";
import coreManifest from "./generated/core.plug.json";
@@ -62,16 +64,15 @@ import { safeRun } from "./util";
import { collabExtension } from "./collab";
import { Document } from "./collab";
import { EditorSelection } from "@codemirror/state";
class PageState {
editorState: EditorState;
scrollTop: number;
meta: PageMeta;
selection: EditorSelection;
constructor(editorState: EditorState, scrollTop: number, meta: PageMeta) {
this.editorState = editorState;
constructor(scrollTop: number, selection: EditorSelection) {
this.scrollTop = scrollTop;
this.meta = meta;
this.selection = selection;
}
}
@@ -88,6 +89,7 @@ export class Editor implements AppEventDispatcher {
indexer: Indexer;
navigationResolve?: (val: undefined) => void;
pageNavigator: IPageNavigator;
indexCurrentPageDebounced: () => any;
constructor(space: HttpRemoteSpace, parent: Element) {
this.editorCommands = new Map();
@@ -98,18 +100,13 @@ export class Editor implements AppEventDispatcher {
this.viewDispatch = () => {};
this.render(parent);
this.editorView = new EditorView({
state: this.createEditorState(
new Document(Text.of([""]), {
name: "",
lastModified: new Date(),
version: 0,
})
),
state: this.createEditorState("", new Document(Text.of([""]), 0)),
parent: document.getElementById("editor")!,
});
this.pageNavigator = new PathPageNavigator();
this.indexer = new Indexer("page-index", space);
// this.watch();
this.indexCurrentPageDebounced = debounce(this.indexCurrentPage, 2000);
}
async init() {
@@ -118,16 +115,38 @@ export class Editor implements AppEventDispatcher {
this.focus();
this.pageNavigator.subscribe(async (pageName) => {
await this.save();
console.log("Now navigating to", pageName);
if (!this.editorView) {
return;
}
if (this.currentPage) {
let pageState = this.openPages.get(this.currentPage)!;
pageState.selection = this.editorView!.state.selection;
pageState.scrollTop = this.editorView!.scrollDOM.scrollTop;
this.space.closePage(this.currentPage);
}
await this.loadPage(pageName);
});
this.space.addEventListener("connect", () => {
if (this.currentPage) {
console.log("Connected to socket, fetch fresh?");
this.reloadPage();
}
});
this.space.addEventListener("reload", (e) => {
let pageName = (e as CustomEvent).detail;
if (this.currentPage === pageName) {
console.log("Was told to reload the page");
this.reloadPage();
}
});
if (this.pageNavigator.getCurrentPage() === "") {
this.pageNavigator.navigate("start");
}
@@ -182,11 +201,11 @@ export class Editor implements AppEventDispatcher {
return results;
}
get currentPage(): PageMeta | undefined {
get currentPage(): string | undefined {
return this.viewState.currentPage;
}
createEditorState(doc: Document): EditorState {
createEditorState(pageName: string, doc: Document): EditorState {
const editor = this;
let commandKeyBindings: KeyBinding[] = [];
for (let def of this.editorCommands.values()) {
@@ -217,8 +236,9 @@ export class Editor implements AppEventDispatcher {
bracketMatching(),
closeBrackets(),
collabExtension(
doc.meta.name,
doc.meta.version!,
pageName,
this.space.socket.id,
doc.version,
this.space,
this.reloadPage.bind(this)
),
@@ -277,14 +297,6 @@ export class Editor implements AppEventDispatcher {
return true;
},
},
{
key: "Ctrl-s",
mac: "Cmd-s",
run: (target): boolean => {
this.save();
return true;
},
},
{
key: "Ctrl-.",
mac: "Cmd-.",
@@ -310,17 +322,6 @@ export class Editor implements AppEventDispatcher {
await this.dispatchAppEvent("page:click", clickEvent);
});
},
// focus: (event: FocusEvent, view: EditorView) => {
// console.log("Got focus");
// document.body.classList.add("keyboard");
// },
// blur: (event: FocusEvent, view: EditorView) => {
// console.log("Lost focus");
// document.body.classList.remove("keyboard");
// },
// focusout: (event: FocusEvent, view: EditorView) => {
// window.scrollTo(0, 0);
// },
}),
markdown({
base: customMarkDown,
@@ -333,7 +334,10 @@ export class Editor implements AppEventDispatcher {
});
}
reloadPage() {}
reloadPage() {
console.log("Reloading page");
this.loadPage(this.currentPage!);
}
async plugCompleter(
ctx: CompletionContext
@@ -385,51 +389,22 @@ export class Editor implements AppEventDispatcher {
update(value: null, transaction: Transaction): null {
if (transaction.docChanged) {
this.viewDispatch({
type: "page-updated",
});
this.indexCurrentPageDebounced();
}
return null;
}
async save() {
const editorState = this.editorView!.state;
if (!this.currentPage) {
return;
private async indexCurrentPage() {
if (this.currentPage) {
console.log("Indexing page", this.currentPage);
await this.indexer.indexPage(
this,
this.currentPage,
this.editorView!.state.sliceDoc(),
true
);
}
if (this.viewState.isSaved) {
console.log("Page not modified, skipping saving");
return;
}
// Write to the space
const pageName = this.currentPage.name;
const text = editorState.sliceDoc();
let pageMeta = await this.space.writePage(pageName, text);
// Update in open page cache
this.openPages.set(
pageName,
new PageState(editorState, this.editorView!.scrollDOM.scrollTop, pageMeta)
);
// Dispatch update to view
this.viewDispatch({ type: "page-saved", meta: pageMeta });
// If a new page was created, let's refresh the page list
if (pageMeta.created) {
await this.loadPageList();
}
// Reindex page
await this.indexPage(text, pageMeta);
}
private async indexPage(text: string, pageMeta: PageMeta) {
console.log("Indexing page", pageMeta.name);
this.indexer.indexPage(this, pageMeta, text, true);
}
async loadPageList() {
@@ -440,32 +415,6 @@ export class Editor implements AppEventDispatcher {
});
}
watch() {
setInterval(() => {
safeRun(async () => {
if (this.currentPage && this.viewState.isSaved) {
await this.checkForNewVersion(this.currentPage);
}
});
}, watchInterval);
}
async checkForNewVersion(cachedMeta: PageMeta) {
const currentPageName = cachedMeta.name;
let newPageMeta = await this.space.getPageMeta(currentPageName);
if (
cachedMeta.lastModified.getTime() !== newPageMeta.lastModified.getTime()
) {
console.log("File changed on disk, reloading");
let doc = await this.space.openPage(currentPageName);
this.openPages.set(
currentPageName,
new PageState(this.createEditorState(doc), 0, doc.meta)
);
await this.loadPage(currentPageName, false);
}
}
focus() {
this.editorView!.focus();
}
@@ -474,39 +423,41 @@ export class Editor implements AppEventDispatcher {
this.pageNavigator.navigate(name);
}
async loadPage(pageName: string, checkNewVersion: boolean = true) {
async loadPage(pageName: string) {
let doc = await this.space.openPage(pageName);
let editorState = this.createEditorState(pageName, doc);
let pageState = this.openPages.get(pageName);
const editorView = this.editorView;
if (!editorView) {
return;
}
editorView.setState(editorState);
if (!pageState) {
let doc = await this.space.openPage(pageName);
pageState = new PageState(this.createEditorState(doc), 0, doc.meta);
pageState = new PageState(0, editorState.selection);
this.openPages.set(pageName, pageState!);
// Freshly loaded, no need to check for a new version either way
checkNewVersion = false;
} else {
// Restore state
console.log("Restoring selection state");
editorView.dispatch({
selection: pageState.selection,
});
editorView.scrollDOM.scrollTop = pageState!.scrollTop;
}
this.editorView!.setState(pageState!.editorState);
this.editorView!.scrollDOM.scrollTop = pageState!.scrollTop;
this.viewDispatch({
type: "page-loaded",
meta: pageState.meta,
name: pageName,
});
let indexerPageMeta = await this.indexer.getPageIndexPageMeta(pageName);
if (
(indexerPageMeta &&
pageState.meta.lastModified.getTime() !==
indexerPageMeta.lastModified.getTime()) ||
!indexerPageMeta
) {
await this.indexPage(pageState.editorState.sliceDoc(), pageState.meta);
}
if (checkNewVersion) {
// Loaded page from in-memory cache, let's async see if this page hasn't been updated
this.checkForNewVersion(pageState.meta).catch((e) => {
console.error("Failed to check for new version");
});
}
// let indexerPageMeta = await this.indexer.getPageIndexPageMeta(pageName);
// if (
// (indexerPageMeta &&
// doc.meta.lastModified.getTime() !==
// indexerPageMeta.lastModified.getTime()) ||
// !indexerPageMeta
// ) {
await this.indexCurrentPage();
// }
}
ViewComponent(): React.ReactElement {
@@ -514,23 +465,11 @@ export class Editor implements AppEventDispatcher {
this.viewState = viewState;
this.viewDispatch = dispatch;
// Auto save
useEffect(() => {
const id = setTimeout(() => {
if (!viewState.isSaved) {
this.save();
}
}, 2000);
return () => {
clearTimeout(id);
};
}, [viewState.isSaved]);
let editor = this;
useEffect(() => {
if (viewState.currentPage) {
document.title = viewState.currentPage.name;
document.title = viewState.currentPage;
}
}, [viewState.currentPage]);
@@ -573,7 +512,7 @@ export class Editor implements AppEventDispatcher {
}}
/>
<div id="editor"></div>
<StatusBar isSaved={viewState.isSaved} editorView={this.editorView} />
<StatusBar editorView={this.editorView} />
</>
);
}
+21 -26
View File
@@ -38,39 +38,39 @@ export class Indexer {
await this.pageIndex.clear();
}
async setPageIndexPageMeta(pageName: string, meta: PageMeta) {
await this.set(pageName, "$meta", {
lastModified: meta.lastModified.getTime(),
});
}
// async setPageIndexPageMeta(pageName: string, meta: PageMeta) {
// await this.set(pageName, "$meta", {
// lastModified: meta.lastModified.getTime(),
// });
// }
async getPageIndexPageMeta(pageName: string): Promise<PageMeta | null> {
let meta = await this.get(pageName, "$meta");
if (meta) {
return {
name: pageName,
lastModified: new Date(meta.lastModified),
};
} else {
return null;
}
}
// async getPageIndexPageMeta(pageName: string): Promise<PageMeta | null> {
// let meta = await this.get(pageName, "$meta");
// if (meta) {
// return {
// name: pageName,
// lastModified: new Date(meta.lastModified),
// };
// } else {
// return null;
// }
// }
async indexPage(
appEventDispatcher: AppEventDispatcher,
pageMeta: PageMeta,
pageName: string,
text: string,
withFlush: boolean
) {
if (withFlush) {
await this.clearPageIndexForPage(pageMeta.name);
await this.clearPageIndexForPage(pageName);
}
let indexEvent: IndexEvent = {
name: pageMeta.name,
name: pageName,
text,
};
await appEventDispatcher.dispatchAppEvent("page:index", indexEvent);
await this.setPageIndexPageMeta(pageMeta.name, pageMeta);
// await this.setPageIndexPageMeta(pageMeta.name, pageMeta);
}
async reindexSpace(space: Space, appEventDispatcher: AppEventDispatcher) {
@@ -79,12 +79,7 @@ export class Indexer {
// TODO: Parallelize?
for (let page of allPages) {
let pageData = await space.readPage(page.name);
await this.indexPage(
appEventDispatcher,
pageData.meta,
pageData.text,
false
);
await this.indexPage(appEventDispatcher, page.name, pageData.text, false);
}
}
+2 -18
View File
@@ -10,27 +10,11 @@ export default function reducer(
return {
...state,
allPages: state.allPages.map((pageMeta) =>
pageMeta.name === action.meta.name
pageMeta.name === action.name
? { ...pageMeta, lastOpened: new Date() }
: pageMeta
),
currentPage: action.meta,
isSaved: true,
};
case "page-saved":
return {
...state,
currentPage: action.meta,
isSaved: true,
};
case "page-updated":
// Minor rerender optimization, this is triggered a lot
if (!state.isSaved) {
return state;
}
return {
...state,
isSaved: false,
currentPage: action.name,
};
case "start-navigate":
return {
+33 -30
View File
@@ -4,6 +4,7 @@ import { Update } from "@codemirror/collab";
import { Transaction, Text, ChangeSet } from "@codemirror/state";
import { Document } from "./collab";
import { cursorEffect } from "./cursorEffect";
export interface Space {
listPages(): Promise<PageMeta[]>;
@@ -13,38 +14,48 @@ export interface Space {
getPageMeta(name: string): Promise<PageMeta>;
}
export class HttpRemoteSpace implements Space {
export class HttpRemoteSpace extends EventTarget implements Space {
url: string;
socket: Socket;
reqId = 0;
constructor(url: string, socket: Socket) {
super();
this.url = url;
this.socket = socket;
socket.on("connect", () => {
console.log("connected via SocketIO");
console.log("connected to socket");
this.dispatchEvent(new Event("connect"));
});
socket.on("reload", (pageName: string) => {
this.dispatchEvent(new CustomEvent("reload", { detail: pageName }));
});
}
pushUpdates(
private wsCall(eventName: string, ...args: any[]): Promise<any> {
return new Promise((resolve) => {
this.reqId++;
this.socket!.once(`${eventName}Resp${this.reqId}`, resolve);
this.socket!.emit(eventName, this.reqId, ...args);
});
}
async pushUpdates(
pageName: string,
version: number,
fullUpdates: readonly (Update & { origin: Transaction })[]
): Promise<boolean> {
return new Promise((resolve) => {
if (this.socket) {
let updates = fullUpdates.map((u) => ({
clientID: u.clientID,
changes: u.changes.toJSON(),
}));
this.reqId++;
this.socket.emit("pushUpdates", this.reqId, pageName, version, updates);
this.socket.once("pushUpdatesResp" + this.reqId, (result) => {
resolve(result);
});
}
});
if (this.socket) {
let updates = fullUpdates.map((u) => ({
clientID: u.clientID,
changes: u.changes.toJSON(),
cursors: u.effects?.map((e) => e.value),
}));
return this.wsCall("pushUpdates", pageName, version, updates);
}
return false;
}
async pullUpdates(
@@ -52,11 +63,13 @@ export class HttpRemoteSpace implements Space {
version: number
): Promise<readonly Update[]> {
let updates: Update[] = await this.wsCall("pullUpdates", pageName, version);
console.log("Got updates", updates);
return updates.map((u) => ({
let ups = updates.map((u) => ({
changes: ChangeSet.fromJSON(u.changes),
effects: u.effects?.map((e) => cursorEffect.of(e.value)),
clientID: u.clientID,
}));
console.log("Got updates", ups);
return ups;
}
async listPages(): Promise<PageMeta[]> {
@@ -70,20 +83,10 @@ export class HttpRemoteSpace implements Space {
}));
}
wsCall(eventName: string, ...args: any[]): Promise<any> {
return new Promise((resolve) => {
this.reqId++;
this.socket!.once(`${eventName}Resp${this.reqId}`, resolve);
this.socket!.emit(eventName, this.reqId, ...args);
});
}
async openPage(name: string): Promise<Document> {
this.reqId++;
let [meta, text] = await this.wsCall("openPage", name);
console.log("Got this", meta, text);
meta.lastModified = new Date(meta.lastModified);
return new Document(Text.of(text), meta);
let [version, text] = await this.wsCall("openPage", name);
return new Document(Text.of(text), version);
}
async closePage(name: string): Promise<void> {
+7
View File
@@ -9,6 +9,13 @@
max-width: 800px;
}
.other-cursor {
display: inline-block;
width: 1px;
margin-right: -1px;
height: 1em;
}
.cm-selectionBackground {
background-color: #d7e1f6 !important;
}
+1 -1
View File
@@ -27,7 +27,7 @@ function ensureAnchor(expr: any, start: boolean) {
}
export default (editor: Editor) => ({
"editor.getCurrentPage": (): PageMeta => {
"editor.getCurrentPage": (): string => {
return editor.currentPage!;
},
"editor.getText": () => {
+1 -1
View File
@@ -23,7 +23,7 @@ export default (editor: Editor) => ({
console.log("Clearing page index", name);
await editor.indexer.clearPageIndexForPage(name);
// If we're deleting the current page, navigate to the start page
if (editor.currentPage?.name === name) {
if (editor.currentPage === name) {
await editor.navigate("start");
}
// Remove page from open pages in editor
+2 -6
View File
@@ -37,8 +37,7 @@ export interface CommandDef {
}
export type AppViewState = {
currentPage?: PageMeta;
isSaved: boolean;
currentPage?: string;
showPageNavigator: boolean;
showCommandPalette: boolean;
allPages: PageMeta[];
@@ -46,7 +45,6 @@ export type AppViewState = {
};
export const initialViewState: AppViewState = {
isSaved: false,
showPageNavigator: false,
showCommandPalette: false,
allPages: [],
@@ -54,9 +52,7 @@ export const initialViewState: AppViewState = {
};
export type Action =
| { type: "page-loaded"; meta: PageMeta }
| { type: "page-saved"; meta: PageMeta }
| { type: "page-updated" }
| { type: "page-loaded"; name: string }
| { type: "pages-listed"; pages: PageMeta[] }
| { type: "start-navigate" }
| { type: "stop-navigate" }
+10
View File
@@ -979,6 +979,11 @@
resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==
"@types/lodash@^4.14.179":
version "4.14.179"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.179.tgz#490ec3288088c91295780237d2497a3aa9dfb5c5"
integrity sha512-uwc1x90yCKqGcIOAT6DwOSuxnrAbpkdPsUOZtwrXb4D/6wZs+6qG7QnIawDuZWg0sWpxl+ltIKCaLoMlna678w==
"@types/node@^17.0.21":
version "17.0.21"
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.21.tgz#864b987c0c68d07b4345845c3e63b75edd143644"
@@ -2051,6 +2056,11 @@ lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
loose-envify@^1.1.0, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"