From 1ae12def4d4548cbaf4b435da6ca197cad00c408 Mon Sep 17 00:00:00 2001 From: Paul Sonnentag Date: Wed, 29 May 2024 14:31:40 +0200 Subject: [PATCH 01/30] load datatypes lazily --- src/components/utils.ts | 5 - src/datatypes/bot/datatype.ts | 4 +- src/datatypes/bot/essayEditingBot.ts | 2 +- src/datatypes/{markdown => essay}/datatype.ts | 12 +- src/datatypes/{markdown => essay}/index.ts | 0 src/datatypes/essay/loader.ts | 18 +++ src/datatypes/{markdown => essay}/schema.ts | 0 src/datatypes/{markdown => essay}/utils.ts | 0 src/os/datatypes.ts | 142 +++++++++++++----- src/os/explorer/account.ts | 2 +- src/os/explorer/components/AccountPicker.tsx | 34 +++-- src/os/explorer/components/Explorer.tsx | 18 ++- src/os/explorer/components/Sidebar.tsx | 28 ++-- src/os/explorer/components/Topbar.tsx | 106 +++++++------ ...ectedDocLink.tsx => useSelectedDocLink.ts} | 22 +-- src/os/explorer/hooks/useSyncDocTitle.ts | 9 +- src/os/hooks/useForceUpdate.ts | 6 + src/os/versionControl/annotations.ts | 47 +++--- src/os/versionControl/branches.ts | 2 +- .../components/ReviewSidebar.tsx | 15 +- .../components/TimelineSidebar.tsx | 11 +- .../components/VersionControlEditor.tsx | 12 +- src/os/versionControl/utils.ts | 2 +- src/tools/bot/BotEditor.tsx | 2 +- .../annotationDecorations.ts | 2 +- .../previewMarkdownImages.ts | 2 +- .../tableOfContentsPreview.tsx | 2 +- .../essay/components/CodeMirrorEditor.tsx | 2 +- .../essay/components/EssayAnnotations.tsx | 2 +- src/tools/essay/components/EssayEditor.tsx | 2 +- src/tools/folder/FolderViewer.tsx | 5 +- 31 files changed, 312 insertions(+), 204 deletions(-) rename src/datatypes/{markdown => essay}/datatype.ts (97%) rename src/datatypes/{markdown => essay}/index.ts (100%) create mode 100644 src/datatypes/essay/loader.ts rename src/datatypes/{markdown => essay}/schema.ts (100%) rename src/datatypes/{markdown => essay}/utils.ts (100%) rename src/os/explorer/hooks/{useSelectedDocLink.tsx => useSelectedDocLink.ts} (94%) create mode 100644 src/os/hooks/useForceUpdate.ts diff --git a/src/components/utils.ts b/src/components/utils.ts index 940515c2..1489d71f 100644 --- a/src/components/utils.ts +++ b/src/components/utils.ts @@ -5,8 +5,3 @@ import { useReducer } from "react"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } - -export function useForceUpdate() { - const [, forceUpdate] = useReducer((x) => x + 1, 0); - return forceUpdate; -} diff --git a/src/datatypes/bot/datatype.ts b/src/datatypes/bot/datatype.ts index 1780fe9f..00700679 100644 --- a/src/datatypes/bot/datatype.ts +++ b/src/datatypes/bot/datatype.ts @@ -1,6 +1,6 @@ import { ContactDoc, RegisteredContactDoc } from "@/os/explorer/account"; -import { MarkdownDatatype } from "@/datatypes/markdown/datatype"; -import { MarkdownDoc } from "@/datatypes/markdown/schema"; +import { MarkdownDatatype } from "@/datatypes/essay/datatype"; +import { MarkdownDoc } from "@/datatypes/essay/schema"; import { type DataType } from "@/os/datatypes"; import { AutomergeUrl, Repo } from "@automerge/automerge-repo"; import { Bot } from "lucide-react"; diff --git a/src/datatypes/bot/essayEditingBot.ts b/src/datatypes/bot/essayEditingBot.ts index 6ba52a7a..41ac7376 100644 --- a/src/datatypes/bot/essayEditingBot.ts +++ b/src/datatypes/bot/essayEditingBot.ts @@ -1,7 +1,7 @@ import { RegisteredContactDoc } from "@/os/explorer/account"; import { DEFAULT_MODEL, openaiClient } from "@/os/lib/llm"; import { createBranch } from "@/os/versionControl/branches"; -import { MarkdownDoc } from "@/datatypes/markdown/schema"; +import { MarkdownDoc } from "@/datatypes/essay/schema"; import { AutomergeUrl, DocHandle, Repo } from "@automerge/automerge-repo"; import { splice } from "@automerge/automerge/next"; import { EssayEditingBotDoc } from "./schema"; diff --git a/src/datatypes/markdown/datatype.ts b/src/datatypes/essay/datatype.ts similarity index 97% rename from src/datatypes/markdown/datatype.ts rename to src/datatypes/essay/datatype.ts index a651187d..dd225be8 100644 --- a/src/datatypes/markdown/datatype.ts +++ b/src/datatypes/essay/datatype.ts @@ -1,4 +1,4 @@ -import { DataType } from "@/os/datatypes"; +import { DataTypeWitoutMetaData } from "@/os/datatypes"; import { FileExportMethod } from "@/os/fileExports"; import { DecodedChangeWithMetadata } from "@/os/versionControl/groupChanges"; import { @@ -12,9 +12,8 @@ import { } from "@/os/versionControl/utils"; import { next as A } from "@automerge/automerge"; import { Repo } from "@automerge/automerge-repo"; -import { Doc, splice } from "@automerge/automerge/next"; +import { splice } from "@automerge/automerge/next"; import { pick } from "lodash"; -import { Text } from "lucide-react"; import { AssetsDoc } from "../../tools/essay/assets"; import { MarkdownDoc, MarkdownDocAnchor } from "./schema"; @@ -289,14 +288,11 @@ const fileExportMethods: FileExportMethod[] = [ }, ]; -export const MarkdownDatatype: DataType< +export const MarkdownDatatype: DataTypeWitoutMetaData< MarkdownDoc, MarkdownDocAnchor, string > = { - id: "essay", - name: "Essay", - icon: Text, init, getTitle, markCopy, @@ -309,3 +305,5 @@ export const MarkdownDatatype: DataType< sortAnchorsBy, fileExportMethods, }; + +console.log("load markdown"); diff --git a/src/datatypes/markdown/index.ts b/src/datatypes/essay/index.ts similarity index 100% rename from src/datatypes/markdown/index.ts rename to src/datatypes/essay/index.ts diff --git a/src/datatypes/essay/loader.ts b/src/datatypes/essay/loader.ts new file mode 100644 index 00000000..b6f7af6b --- /dev/null +++ b/src/datatypes/essay/loader.ts @@ -0,0 +1,18 @@ +import { + DataTypeLoaderConfig, + DataTypeWitoutMetaData, + DatatypeId, +} from "@/os/datatypes"; +import { Text } from "lucide-react"; +import { MarkdownDoc, MarkdownDocAnchor } from "./schema"; + +export default { + metadata: { + id: "essay" as DatatypeId, + name: "Essay", + icon: Text, + }, + + load: () => + import("./datatype").then(({ MarkdownDatatype }) => MarkdownDatatype), +} as DataTypeLoaderConfig; diff --git a/src/datatypes/markdown/schema.ts b/src/datatypes/essay/schema.ts similarity index 100% rename from src/datatypes/markdown/schema.ts rename to src/datatypes/essay/schema.ts diff --git a/src/datatypes/markdown/utils.ts b/src/datatypes/essay/utils.ts similarity index 100% rename from src/datatypes/markdown/utils.ts rename to src/datatypes/essay/utils.ts diff --git a/src/os/datatypes.ts b/src/os/datatypes.ts index 63622ffe..127a2172 100644 --- a/src/os/datatypes.ts +++ b/src/os/datatypes.ts @@ -2,38 +2,32 @@ import { ChangeGroup, DecodedChangeWithMetadata, } from "@/os/versionControl/groupChanges"; -import { - Annotation, - HasVersionControlMetadata, -} from "@/os/versionControl/schema"; +import { Annotation } from "@/os/versionControl/schema"; import { TextPatch } from "@/os/versionControl/utils"; import { next as A, Doc } from "@automerge/automerge"; import { Repo } from "@automerge/automerge-repo"; // datatypes - -import bot from "@/datatypes/bot"; -import datagrid from "@/datatypes/datagrid"; -import folder from "@/datatypes/folder"; -import kanban from "@/datatypes/kanban"; -import markdown from "@/datatypes/markdown"; -import tldraw from "@/datatypes/tldraw"; +import { useEffect, useRef, useState } from "react"; import { FileExportMethod } from "./fileExports"; -export type CoreDataType = { - id: string; +export type DataTypeMetadata = { + id: DatatypeId; name: string; icon: any; + + /* Marking a data types as experimental hides it by default + * so the user has to enable them in their account first */ + isExperimental?: boolean; +}; + +export type CoreDataType = { init: (doc: D, repo: Repo) => void; getTitle: (doc: D, repo: Repo) => Promise; setTitle?: (doc: any, title: string) => void; markCopy: (doc: D) => void; // TODO: this shouldn't be part of the interface actions?: Record, args: object) => void>; fileExportMethods?: FileExportMethod[]; - - /* Marking a data types as experimental hides it by default - * so the user has to enable them in their account first */ - isExperimental?: boolean; }; export type VersionedDataType = { @@ -110,22 +104,98 @@ export type VersionedDataType = { sortAnchorsBy?: (doc: D, anchor: T) => any; }; -export type DataType = CoreDataType & VersionedDataType; - -// TODO: we can narrow the types below by constructing a mapping from datatype IDs -// to the corresponding typescript type. This will be more natural once we have a -// schema system for generating typescript types. - -export const DATA_TYPES: Record< - string, - DataType, unknown, unknown> -> = { - essay: markdown, // todo: migrate, we can't just rename it - tldraw, - datagrid, - bot, - kanban, - folder, -} as const; - -export type DatatypeId = keyof typeof DATA_TYPES; +export type DataTypeWitoutMetaData = CoreDataType & + VersionedDataType; + +export type DataType = DataTypeWitoutMetaData & + DataTypeMetadata; + +export type DataTypeLoaderConfig = { + metadata: DataTypeMetadata; + load: () => Promise>; +}; + +export type DataTypeLoader = { + metadata: DataTypeMetadata; + load: () => Promise>; +}; + +export const getDatatypeLoaders = () => { + const dataTypeLoaders: Record< + DatatypeId, + DataTypeLoader + > = {}; + const dataTypeLoaderModules = import.meta.glob("../datatypes/*/loader.ts", { + eager: true, + }) as Record< + string, + { + default: DataTypeLoader; + } + >; + + for (const [path, { default: loader }] of Object.entries( + dataTypeLoaderModules + )) { + const datatypeId = path.split("/")[2] as DatatypeId; + + if (datatypeId !== loader.metadata.id) { + throw new Error( + `${path} can't be loaded because the id is wrong: "${loader.metadata.id}" should match the folder name` + ); + } + + dataTypeLoaders[datatypeId] = { + async load() { + const result = { + ...(await loader.load()), + ...loader.metadata, + }; + + return result; + }, + metadata: loader.metadata, + } as DataTypeLoader; + } + return dataTypeLoaders; +}; + +const DATA_TYPE_LOADERS = getDatatypeLoaders(); + +export const useDataTypeLoaders = () => { + return DATA_TYPE_LOADERS; +}; + +export const useDataType = ( + dataTypeId: DatatypeId +): DataType | undefined => { + const dataTypeLoaders = useDataTypeLoaders(); + const [dataType, setDataType] = useState>(); + const dataTypeIdRef = useRef(); + dataTypeIdRef.current = dataTypeId; + + useEffect(() => { + const dataTypeLoader = dataTypeLoaders[dataTypeId] as DataTypeLoader< + D, + T, + V + >; + + if (!dataTypeLoader) { + setDataType(undefined); + return; + } + + dataTypeLoader.load().then((dataType) => { + // ignore if dataTypeId has changed in the meantime + if (dataType.id !== dataTypeIdRef.current) { + return; + } + setDataType(dataType); + }); + }, [dataTypeId, dataTypeLoaders]); + + return dataType; +}; + +export type DatatypeId = string & { __datatypeId: true }; diff --git a/src/os/explorer/account.ts b/src/os/explorer/account.ts index 09ba6431..12ba52ec 100644 --- a/src/os/explorer/account.ts +++ b/src/os/explorer/account.ts @@ -11,7 +11,7 @@ import { EventEmitter } from "eventemitter3"; import { useEffect, useState } from "react"; import { uploadFile } from "./utils"; import { ChangeFn } from "@automerge/automerge/next"; -import { useForceUpdate } from "@/components/utils"; +import { useForceUpdate } from "@/os/hooks/useForceUpdate"; import { FolderDoc } from "@/datatypes/folder"; import { useFolderDocWithChildren } from "../../datatypes/folder/hooks/useFolderDocWithChildren"; diff --git a/src/os/explorer/components/AccountPicker.tsx b/src/os/explorer/components/AccountPicker.tsx index 74018f8c..a8f419a3 100644 --- a/src/os/explorer/components/AccountPicker.tsx +++ b/src/os/explorer/components/AccountPicker.tsx @@ -33,7 +33,7 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { ContactAvatar } from "./ContactAvatar"; -import { DATA_TYPES } from "@/os/datatypes"; +import { useDataTypeLoaders } from "@/os/datatypes"; import { Checkbox } from "@/components/ui/checkbox"; // 1MB in bytes @@ -56,6 +56,7 @@ export const AccountPicker = ({ const currentAccount = useCurrentAccount(); const self = useSelf(); + const datatypes = useDataTypeLoaders(); const [name, setName] = useState(""); const [avatar, setAvatar] = useState(); const [activeTab, setActiveTab] = useState( @@ -145,10 +146,6 @@ export const AccountPicker = ({ const isLoggedIn = self?.type === "registered"; - const experimentalDatatypes = Object.values(DATA_TYPES).filter( - ({ isExperimental }) => isExperimental - ); - return ( @@ -322,38 +319,45 @@ export const AccountPicker = ({
- +
{datatypeSettingsDoc && - experimentalDatatypes.map((datatype) => { + Object.values(datatypes).map((datatypeLoader) => { return (
e.stopPropagation()} onCheckedChange={() => { changeDatatypeSettingsDoc((settings) => { - settings.enabledDatatypeIds[datatype.id] = - !settings.enabledDatatypeIds[datatype.id]; + settings.enabledDatatypeIds[ + datatypeLoader.metadata.id + ] = + !settings.enabledDatatypeIds[ + datatypeLoader.metadata.id + ]; }); }} />
); diff --git a/src/os/explorer/components/Explorer.tsx b/src/os/explorer/components/Explorer.tsx index 0f36c83b..1d954ed5 100644 --- a/src/os/explorer/components/Explorer.tsx +++ b/src/os/explorer/components/Explorer.tsx @@ -12,7 +12,7 @@ import { useCurrentAccountDoc, useRootFolderDocWithChildren, } from "../account"; -import { DatatypeId, DATA_TYPES } from "@/os/datatypes"; +import { DatatypeId, useDataTypeLoaders } from "@/os/datatypes"; import { Toaster } from "@/components/ui/sonner"; import { LoadingScreen } from "./LoadingScreen"; @@ -29,6 +29,7 @@ import { ErrorFallback } from "./ErrorFallback"; export const Explorer: React.FC = () => { const repo = useRepo(); + const datatypeLoaders = useDataTypeLoaders(); const currentAccount = useCurrentAccount(); const [accountDoc] = useCurrentAccountDoc(); @@ -62,14 +63,17 @@ export const Explorer: React.FC = () => { }, [selectedBranchUrl, selectedDoc]); const addNewDocument = useCallback( - ({ type }: { type: DatatypeId }) => { - if (!DATA_TYPES[type]) { + async ({ type }: { type: DatatypeId }) => { + const datatypeLoader = datatypeLoaders[type]; + + if (!datatypeLoader) { throw new Error(`Unsupported document type: ${type}`); } + const datatype = await datatypeLoader.load(); const newDocHandle = repo.create>(); - newDocHandle.change((doc) => DATA_TYPES[type].init(doc, repo)); + newDocHandle.change((doc) => datatype.init(doc, repo)); let parentFolderUrl: AutomergeUrl; let folderPath: AutomergeUrl[]; @@ -128,7 +132,7 @@ export const Explorer: React.FC = () => { // if there's no document selected and the user hits enter, make a new document if (!selectedDocUrl && event.key === "Enter") { - addNewDocument({ type: "essay" }); + addNewDocument({ type: "essay" as DatatypeId }); } }; @@ -222,7 +226,9 @@ export const Explorer: React.FC = () => { No document selected

- {Object.entries(DATA_TYPES).map(([id, datatype]) => { + {Object.values(datatypeLoaders).map((datatypeLoader) => { + const { id } = datatypeLoader.metadata; if ( - datatype.isExperimental && + datatypeLoader.metadata.isExperimental && !datatypeSettings?.enabledDatatypeIds[id] ) { return; } return ( -
+
{" "}
addNewDocument({ type: id as DatatypeId })} > - - New {datatype.name} + New {datatypeLoader.metadata.name}
); diff --git a/src/os/explorer/components/Topbar.tsx b/src/os/explorer/components/Topbar.tsx index 4a1a3d5c..e5495346 100644 --- a/src/os/explorer/components/Topbar.tsx +++ b/src/os/explorer/components/Topbar.tsx @@ -1,5 +1,5 @@ import { DocHandle, isValidAutomergeUrl, Doc } from "@automerge/automerge-repo"; -import React, { useCallback } from "react"; +import React, { useCallback, useEffect, useRef, useState } from "react"; import { Bot, Download, @@ -27,15 +27,15 @@ import { } from "@/components/ui/dropdown-menu"; import { getHeads, save } from "@automerge/automerge"; -import { MarkdownDoc } from "@/datatypes/markdown/schema"; -import { DatatypeId, DATA_TYPES } from "../../datatypes"; +import { MarkdownDoc } from "@/datatypes/essay/schema"; +import { DatatypeId, useDataTypeLoaders } from "../../datatypes"; import { runBot } from "@/datatypes/bot/essayEditingBot"; import { Button } from "@/components/ui/button"; import { HasVersionControlMetadata } from "@/os/versionControl/schema"; import { useDatatypeSettings, useRootFolderDocWithChildren } from "../account"; import botDataType from "@/datatypes/bot"; import { getUrlSafeName } from "../hooks/useSelectedDocLink"; -import { genericExportMethods } from "@/os/fileExports"; +import { FileExportMethod, genericExportMethods } from "@/os/fileExports"; type TopbarProps = { showSidebar: boolean; @@ -68,22 +68,27 @@ export const Topbar: React.FC = ({ const selectedDocUrl = selectedDocLink?.url; const selectedDocName = selectedDocLink?.name; - const selectedDocType = selectedDocLink?.type; + const selectedDataType = selectedDocLink?.type; + const selectedDataTypeRef = useRef(); + selectedDataTypeRef.current = selectedDataType; - const selectedDatatypeMetadata = DATA_TYPES[selectedDocType]; + const dataTypeLoaders = useDataTypeLoaders(); + const selectedDataTypeLoader = dataTypeLoaders[selectedDataType]; - const downloadAsAutomerge = useCallback(() => { - const file = new Blob([save(selectedDoc)], { - type: "application/octet-stream", - }); - saveFile(file, `${selectedDocUrl}.automerge`, [ - { - accept: { - "application/octet-stream": [".automerge"], - }, - }, - ]); - }, [selectedDocUrl, selectedDoc]); + const [fileExportMethods, setFileExportMethods] = useState< + FileExportMethod[] + >([]); + useEffect(() => { + if (!selectedDataTypeLoader) { + setFileExportMethods([]); + } else { + selectedDataTypeLoader.load().then((datatype) => { + if (datatype.id === selectedDataType) { + setFileExportMethods(datatype.fileExportMethods ?? []); + } + }); + } + }, [selectedDataTypeLoader]); const botDocLinks = flatDocLinks?.filter((doc) => doc.type === "bot") ?? []; @@ -98,9 +103,11 @@ export const Topbar: React.FC = ({
)}
- {selectedDatatypeMetadata && ( - - )} + {selectedDataTypeLoader && + React.createElement(selectedDataTypeLoader.metadata.icon, { + className: "inline mr-1", + size: 14, + })} {selectedDocName}
@@ -173,7 +180,10 @@ export const Topbar: React.FC = ({ size={14} className="inline-block ml-2 cursor-pointer" onClick={(e) => { - selectDocLink({ ...botDocLink, type: "essay" }); + selectDocLink({ + ...botDocLink, + type: "essay" as DatatypeId, + }); e.stopPropagation(); }} /> @@ -207,12 +217,14 @@ export const Topbar: React.FC = ({ { + const selectedDataType = await selectedDataTypeLoader.load(); + const newHandle = repo.clone>( selectedDocHandle ); newHandle.change((doc: any) => { - DATA_TYPES[selectedDocType].markCopy(doc); + selectedDataType.markCopy(doc); doc.branchMetadata.source = { url: selectedDocUrl, branchHeads: getHeads(selectedDocHandle.docSync()), @@ -221,7 +233,7 @@ export const Topbar: React.FC = ({ const newDocLink: DocLink = { url: newHandle.url, - name: await DATA_TYPES[selectedDocType].getTitle( + name: await selectedDataType.getTitle( newHandle.docSync(), repo ), @@ -256,31 +268,29 @@ export const Topbar: React.FC = ({ Make a copy - {(selectedDatatypeMetadata?.fileExportMethods ?? []) - .concat(genericExportMethods) - .map((method) => ( - { - const blob = await method.export(selectedDoc, repo); - const filename = `${getUrlSafeName(selectedDocLink.name)}.${ - method.extension - }`; - saveFile(blob, filename, [ - { - accept: { - [method.contentType]: [`.${method.extension}`], - }, + {fileExportMethods.concat(genericExportMethods).map((method) => ( + { + const blob = await method.export(selectedDoc, repo); + const filename = `${getUrlSafeName(selectedDocLink.name)}.${ + method.extension + }`; + saveFile(blob, filename, [ + { + accept: { + [method.contentType]: [`.${method.extension}`], }, - ]); - }} - > - {" "} - Export as {method.name} - - ))} + }, + ]); + }} + > + {" "} + Export as {method.name} + + ))} removeDocLink(selectedDocLink)}> { return urlSafeName; }; -const isDatatypeId = (x: string): x is DatatypeId => - Object.keys(DATA_TYPES).includes(x as DatatypeId); - // Parse older URL formats and map them into our newer formatu const parseLegacyUrl = ( url: URL @@ -80,7 +77,7 @@ const parseLegacyUrl = ( if (isValidAutomergeUrl(possibleAutomergeUrl)) { return { url: possibleAutomergeUrl, - type: "essay", + type: "essay" as DatatypeId, }; } @@ -99,11 +96,6 @@ const parseLegacyUrl = ( return null; } - if (typeof docType === "string" && !isDatatypeId(docType)) { - alert(`Invalid doc type in URL: ${docType}`); - return null; - } - if (typeof branchUrl === "string" && !isValidAutomergeUrl(branchUrl)) { alert(`Invalid branch in URL: ${branchUrl}`); return null; @@ -111,7 +103,7 @@ const parseLegacyUrl = ( return { url: docUrl, - type: docType, + type: docType as DatatypeId, branchUrl: branchUrl as AutomergeUrl, }; }; @@ -133,12 +125,8 @@ const parseUrl = (url: URL): Omit | null => { return null; } - const datatypeId = - url.searchParams.get("type") ?? url.searchParams.get("docType"); // use legacy docType as a fallback - if (!isDatatypeId(datatypeId)) { - alert(`Invalid data type in URL: ${datatypeId}`); - return null; - } + const datatypeId = (url.searchParams.get("type") ?? + url.searchParams.get("docType")) as DatatypeId; // use legacy docType as a fallback const branchUrl = url.searchParams.get("branchUrl"); if (branchUrl && !isValidAutomergeUrl(branchUrl)) { diff --git a/src/os/explorer/hooks/useSyncDocTitle.ts b/src/os/explorer/hooks/useSyncDocTitle.ts index b7cb2049..1031c9ad 100644 --- a/src/os/explorer/hooks/useSyncDocTitle.ts +++ b/src/os/explorer/hooks/useSyncDocTitle.ts @@ -1,5 +1,5 @@ import { HasVersionControlMetadata } from "@/os/versionControl/schema"; -import { DATA_TYPES } from "../../datatypes"; +import { useDataTypeLoaders } from "../../datatypes"; import { DocLinkWithFolderPath, FolderDoc } from "@/datatypes/folder"; import { AutomergeUrl, Repo } from "@automerge/automerge-repo"; import { Doc } from "@automerge/automerge/next"; @@ -26,6 +26,8 @@ export const useSyncDocTitle = ({ const counterRef = useRef(0); const selectedDocTitleRef = useRef<{ url: AutomergeUrl; title?: string }>(); + const dataTypeLoaders = useDataTypeLoaders(); + useEffect(() => { if (!selectedDocLink || !selectedDoc) { selectedDocTitleRef.current = null; @@ -40,8 +42,9 @@ export const useSyncDocTitle = ({ let counter = (counterRef.current = counterRef.current + 1); // load title - DATA_TYPES[selectedDocLink.type] - .getTitle(selectedDoc, repo) + dataTypeLoaders[selectedDocLink.type] + .load() + .then((dataType) => dataType.getTitle(selectedDoc, repo)) .then((title) => { // do nothing if selectedDocLink has changed in between // or if this promise resolved after newer update diff --git a/src/os/hooks/useForceUpdate.ts b/src/os/hooks/useForceUpdate.ts new file mode 100644 index 00000000..25a1a74a --- /dev/null +++ b/src/os/hooks/useForceUpdate.ts @@ -0,0 +1,6 @@ +import { useReducer } from "react"; + +export function useForceUpdate() { + const [, forceUpdate] = useReducer((x) => x + 1, 0); + return forceUpdate; +} diff --git a/src/os/versionControl/annotations.ts b/src/os/versionControl/annotations.ts index a7e1c399..c741af76 100644 --- a/src/os/versionControl/annotations.ts +++ b/src/os/versionControl/annotations.ts @@ -1,8 +1,8 @@ -import { useState, useMemo } from "react"; +import { useState, useMemo, useRef } from "react"; import * as A from "@automerge/automerge/next"; import { isEqual, sortBy, min } from "lodash"; import { useStaticCallback } from "@/os/hooks/useStaticCallback"; -import { DatatypeId, DATA_TYPES } from "@/os/datatypes"; +import { DataType, DatatypeId, useDataTypeLoaders } from "@/os/datatypes"; import { Annotation, HighlightAnnotation, @@ -33,12 +33,12 @@ type HoverState = HoverAnchorState | ActiveGroupState; export function useAnnotations({ doc, - datatypeId, + dataType, diff, isCommentInputFocused, }: { doc: A.Doc>; - datatypeId: DatatypeId; + dataType: DataType; diff?: DiffWithProvenance; isCommentInputFocused: boolean; }): { @@ -51,6 +51,7 @@ export function useAnnotations({ setHoveredAnnotationGroupId: (id: string) => void; setSelectedAnnotationGroupId: (id: string) => void; } { + const datatypeLoaders = useDataTypeLoaders(); const [hoveredState, setHoveredState] = useState>(); const [selectedState, setSelectedState] = useState>(); @@ -105,12 +106,12 @@ export function useAnnotations({ ); const { annotations, annotationGroups } = useMemo(() => { - if (!doc) { + if (!doc || !dataType) { return { annotations: [], annotationGroups: [] }; } - const patchesToAnnotations = DATA_TYPES[datatypeId].patchesToAnnotations; - const valueOfAnchor = DATA_TYPES[datatypeId].valueOfAnchor ?? (() => null); + const patchesToAnnotations = dataType.patchesToAnnotations; + const valueOfAnchor = dataType.valueOfAnchor ?? (() => null); const discussions = Object.values(doc?.discussions ?? []); const discussionGroups: AnnotationGroup[] = []; @@ -165,7 +166,7 @@ export function useAnnotations({ editAnnotations.forEach((editAnnotation) => { if ( discussion.anchors.some((anchor) => - doAnchorsOverlap(datatypeId, editAnnotation.anchor, anchor, doc) + doAnchorsOverlap(dataType, editAnnotation.anchor, anchor, doc) ) ) { // mark any annotation that is part of a discussion as claimed @@ -184,7 +185,7 @@ export function useAnnotations({ const computedAnnotationGroups: AnnotationGroup[] = groupAnnotations( - datatypeId, + dataType, editAnnotations.filter( (annotation) => !claimedAnnotations.has(annotation) ) @@ -206,7 +207,7 @@ export function useAnnotations({ highlightAnnotations.push(...selectionAnnotations); } - const sortAnchorsBy = DATA_TYPES[datatypeId].sortAnchorsBy; + const sortAnchorsBy = dataType.sortAnchorsBy; return { annotations: editAnnotations.concat(highlightAnnotations), @@ -220,7 +221,7 @@ export function useAnnotations({ ) : combinedAnnotationGroups, }; - }, [doc, diff, selectedState, isCommentInputFocused, datatypeId]); + }, [doc, diff, selectedState, isCommentInputFocused, dataType]); const { selectedAnchors, @@ -242,7 +243,7 @@ export function useAnnotations({ // first annotationGroup that contains all selected anchors is expanded const annotationGroup = annotationGroups.find((group) => doesAnnotationGroupContainAnchors( - datatypeId, + dataType, group, selectedState.anchors, doc @@ -286,7 +287,7 @@ export function useAnnotations({ // find first discussion that contains the hovered anchor and hover all anchors that are part of that discussion as wellp const annotationGroup = annotationGroups.find((group) => doesAnnotationGroupContainAnchors( - datatypeId, + dataType, group, [hoveredState.anchor], doc @@ -377,17 +378,17 @@ export function useAnnotations({ } export const doAnchorsOverlap = ( - type: DatatypeId, + datatype: DataType, a: unknown, b: unknown, doc: HasVersionControlMetadata ) => { - const comperator = DATA_TYPES[type].doAnchorsOverlap; + const comperator = datatype.doAnchorsOverlap; return comperator ? comperator(doc, a, b) : isEqual(a, b); }; export const areAnchorSelectionsEqual = ( - type: DatatypeId, + datatype: DataType, a: unknown[], b: unknown[], doc: HasVersionControlMetadata @@ -397,7 +398,7 @@ export const areAnchorSelectionsEqual = ( } return a.every((anchor) => - b.some((other) => doAnchorsOverlap(type, anchor, other, doc)) + b.some((other) => doAnchorsOverlap(datatype, anchor, other, doc)) ); }; @@ -413,25 +414,25 @@ export function getAnnotationGroupId( return `${firstAnnotation.type}:${JSON.stringify(firstAnnotation.anchor)}`; } -export function doesAnnotationGroupContainAnchors( - datatypeId: DatatypeId, +export function doesAnnotationGroupContainAnchors( + datatype: DataType, group: AnnotationGroup, anchors: T[], doc: HasVersionControlMetadata ) { return anchors.every((anchor) => group.annotations.some((annotation) => - doAnchorsOverlap(datatypeId, annotation.anchor, anchor, doc) + doAnchorsOverlap(datatype, annotation.anchor, anchor, doc) ) ); } -export function groupAnnotations( - datatypeId: DatatypeId, +export function groupAnnotations( + datatype: DataType, annotations: Annotation[] ): Annotation[][] { const grouper = - DATA_TYPES[datatypeId].groupAnnotations ?? + datatype.groupAnnotations ?? ((annotations: Annotation[]) => annotations.map((annotation) => [annotation])); diff --git a/src/os/versionControl/branches.ts b/src/os/versionControl/branches.ts index 514ac85f..0f98883c 100644 --- a/src/os/versionControl/branches.ts +++ b/src/os/versionControl/branches.ts @@ -2,7 +2,7 @@ import * as A from "@automerge/automerge/next"; import { AutomergeUrl, DocHandle, Repo } from "@automerge/automerge-repo"; import { Branch, Branchable } from "./schema"; import { getStringCompletion } from "@/os/lib/llm"; -import { MarkdownDoc } from "@/datatypes/markdown"; +import { MarkdownDoc } from "@/datatypes/essay"; import { Hash } from "@automerge/automerge-wasm"; export const createBranch = ({ diff --git a/src/os/versionControl/components/ReviewSidebar.tsx b/src/os/versionControl/components/ReviewSidebar.tsx index 3c626824..478e73b4 100644 --- a/src/os/versionControl/components/ReviewSidebar.tsx +++ b/src/os/versionControl/components/ReviewSidebar.tsx @@ -6,7 +6,7 @@ import { PopoverTrigger, } from "@/components/ui/popover"; import { Textarea } from "@/components/ui/textarea"; -import { DATA_TYPES, DatatypeId } from "@/os/datatypes"; +import { DataType, DatatypeId } from "@/os/datatypes"; import { useCurrentAccount } from "@/os/explorer/account"; import { ContactAvatar } from "@/os/explorer/components/ContactAvatar"; import { getRelativeTimeString } from "@/os/lib/dates"; @@ -27,7 +27,7 @@ import { getAnnotationGroupId } from "../annotations"; type ReviewSidebarProps = { doc: HasVersionControlMetadata; handle: DocHandle>; - datatypeId: DatatypeId; + dataType: DataType; annotationGroups: AnnotationGroupWithState[]; selectedAnchors: unknown[]; changeDoc: ( @@ -47,7 +47,7 @@ export const ReviewSidebar = React.memo( ({ doc, handle, - datatypeId, + dataType, annotationGroups, selectedAnchors, changeDoc, @@ -67,14 +67,13 @@ export const ReviewSidebar = React.memo( unknown >[] = useMemo(() => { if (!doc) return []; - const valueOfAnchor = - DATA_TYPES[datatypeId].valueOfAnchor ?? (() => null); + const valueOfAnchor = dataType.valueOfAnchor ?? (() => null); return selectedAnchors.map((anchor) => ({ type: "highlighted", anchor: [anchor], value: valueOfAnchor(doc, anchor), })); - }, [selectedAnchors, doc, datatypeId]); + }, [selectedAnchors, doc, dataType]); const addCommentToAnnotationGroup = ( annotationGroup: AnnotationGroup, @@ -179,7 +178,7 @@ export const ReviewSidebar = React.memo(
diff --git a/src/os/versionControl/components/TimelineSidebar.tsx b/src/os/versionControl/components/TimelineSidebar.tsx index 72b0c5d8..b043bc7c 100644 --- a/src/os/versionControl/components/TimelineSidebar.tsx +++ b/src/os/versionControl/components/TimelineSidebar.tsx @@ -11,7 +11,6 @@ import { GenericChangeGroup, groupingByEditTime, } from "../groupChanges"; -import { DATA_TYPES } from "@/os/datatypes"; import { MilestoneIcon, @@ -49,7 +48,7 @@ import { useAutoPopulateChangeGroupSummaries, } from "@/os/versionControl/changeGroupSummaries"; -import { DatatypeId } from "@/os/datatypes"; +import { DataType } from "@/os/datatypes"; import { ChangeGroupingOptions } from "../groupChanges"; import { ChangeGrouper } from "../ChangeGrouper"; @@ -110,14 +109,14 @@ export type ChangelogSelection = | undefined; export const TimelineSidebar: React.FC<{ - datatypeId: DatatypeId; + dataType: DataType; docUrl: AutomergeUrl; selectedBranch: Branch; setSelectedBranch: (branch: Branch) => void; setDocHeads: (heads: Heads) => void; setDiff: (diff: DiffWithProvenance) => void; }> = ({ - datatypeId, + dataType, docUrl, selectedBranch, setSelectedBranch, @@ -138,7 +137,7 @@ export const TimelineSidebar: React.FC<{ includePatchInChangeGroup, promptForAIChangeGroupSummary: promptForAutoChangeGroupDescription, fallbackSummaryForChangeGroup, - } = DATA_TYPES[datatypeId] ?? {}; + } = dataType ?? {}; // todo: extract this as an interface that different doc types can implement const changeGroupingOptions = useMemo< @@ -154,7 +153,7 @@ export const TimelineSidebar: React.FC<{ fallbackSummaryForChangeGroup, }; }, [ - datatypeId, + dataType.id, includeChangeInHistory, includePatchInChangeGroup, fallbackSummaryForChangeGroup, diff --git a/src/os/versionControl/components/VersionControlEditor.tsx b/src/os/versionControl/components/VersionControlEditor.tsx index 36a2aa5a..c9264f8d 100644 --- a/src/os/versionControl/components/VersionControlEditor.tsx +++ b/src/os/versionControl/components/VersionControlEditor.tsx @@ -23,8 +23,8 @@ import { import { ErrorBoundary } from "react-error-boundary"; import { ErrorFallback } from "@/os/explorer/components/ErrorFallback"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { isMarkdownDoc } from "@/datatypes/markdown"; -import { DatatypeId } from "@/os/datatypes"; +import { isMarkdownDoc } from "@/datatypes/essay"; +import { DatatypeId, useDataType } from "@/os/datatypes"; import { getRelativeTimeString } from "@/os/lib/dates"; import { isLLMActive } from "@/os/lib/llm"; import { EditorProps, TOOLS } from "@/os/tools"; @@ -303,6 +303,8 @@ export const VersionControlEditor: React.FC<{ const activeChangeDoc = selectedBranch ? changeBranchDoc : changeDoc; const activeHandle = selectedBranch ? branchHandle : handle; + const dataType = useDataType(datatypeId); + const { annotations, annotationGroups, @@ -314,7 +316,7 @@ export const VersionControlEditor: React.FC<{ setSelectedAnnotationGroupId, } = useAnnotations({ doc: activeDoc, - datatypeId, + dataType, diff: diffForEditor, isCommentInputFocused, }); @@ -600,7 +602,7 @@ export const VersionControlEditor: React.FC<{ > = ({ docUrl, docHeads, }: EditorProps) => { + const dataTypeLoaders = useDataTypeLoaders(); const [folder] = useDocument(docUrl); // used to trigger re-rendering when the doc loads const folderAtHeads = docHeads ? A.view(folder, docHeads) : folder; @@ -28,7 +29,7 @@ export const FolderViewer: React.FC> = ({
{folderAtHeads.docs.map((docLink) => { const Tool = TOOLS[docLink.type][0].editorComponent; - const Icon = DATA_TYPES[docLink.type].icon; + const Icon = dataTypeLoaders[docLink.type].metadata.icon; return (
From 4503f110172576b3c4ff43be7f2071d172ac4798 Mon Sep 17 00:00:00 2001 From: Paul Sonnentag Date: Wed, 29 May 2024 15:00:54 +0200 Subject: [PATCH 02/30] add loaders for remaining datatypes --- src/datatypes/bot/datatype.ts | 8 ++--- src/datatypes/bot/loader.ts | 17 ++++++++++ src/datatypes/datagrid/datatype.ts | 8 ++--- src/datatypes/datagrid/loader.ts | 15 +++++++++ src/datatypes/essay/datatype.ts | 2 -- src/datatypes/essay/loader.ts | 8 ++--- src/datatypes/folder/datatype.ts | 7 ++--- src/datatypes/folder/loader.ts | 13 ++++++++ src/datatypes/kanban/datatype.ts | 33 ++------------------ src/datatypes/kanban/loader.ts | 15 +++++++++ src/datatypes/kanban/schema.ts | 24 ++++++++++++++ src/datatypes/tldraw/datatype.ts | 11 ++++--- src/datatypes/tldraw/loader.ts | 14 +++++++++ src/os/explorer/components/AccountPicker.tsx | 21 +++++++------ src/os/explorer/components/Sidebar.tsx | 5 +-- 15 files changed, 130 insertions(+), 71 deletions(-) create mode 100644 src/datatypes/bot/loader.ts create mode 100644 src/datatypes/datagrid/loader.ts create mode 100644 src/datatypes/folder/loader.ts create mode 100644 src/datatypes/kanban/loader.ts create mode 100644 src/datatypes/kanban/schema.ts create mode 100644 src/datatypes/tldraw/loader.ts diff --git a/src/datatypes/bot/datatype.ts b/src/datatypes/bot/datatype.ts index 00700679..487ff50f 100644 --- a/src/datatypes/bot/datatype.ts +++ b/src/datatypes/bot/datatype.ts @@ -1,22 +1,18 @@ import { ContactDoc, RegisteredContactDoc } from "@/os/explorer/account"; import { MarkdownDatatype } from "@/datatypes/essay/datatype"; import { MarkdownDoc } from "@/datatypes/essay/schema"; -import { type DataType } from "@/os/datatypes"; +import { DataTypeWitoutMetaData, type DataType } from "@/os/datatypes"; import { AutomergeUrl, Repo } from "@automerge/automerge-repo"; import { Bot } from "lucide-react"; import { EssayEditingBotDoc } from "./schema"; const BOT_AVATAR_URL = "automerge:uL1duhieqUV4qaeHGHX1dg8FnNy" as AutomergeUrl; -export const EssayEditingBotDatatype: DataType< +export const EssayEditingBotDatatype: DataTypeWitoutMetaData< EssayEditingBotDoc, never, never > = { - id: "bot", - name: "Bot", - isExperimental: true, - icon: Bot, init: (doc: any, repo: Repo) => { const contactHandle = repo.create(); const promptHandle = repo.create(); diff --git a/src/datatypes/bot/loader.ts b/src/datatypes/bot/loader.ts new file mode 100644 index 00000000..3e841be8 --- /dev/null +++ b/src/datatypes/bot/loader.ts @@ -0,0 +1,17 @@ +import { DataTypeLoaderConfig } from "@/os/datatypes"; +import { Bot } from "lucide-react"; +import { EssayEditingBotDoc } from "./schema"; + +export default { + metadata: { + id: "bot", + name: "Bot", + icon: Bot, + isExperimental: true, + }, + + load: () => + import("./datatype").then( + ({ EssayEditingBotDatatype }) => EssayEditingBotDatatype + ), +} as DataTypeLoaderConfig; diff --git a/src/datatypes/datagrid/datatype.ts b/src/datatypes/datagrid/datatype.ts index 93e7977b..76d26c71 100644 --- a/src/datatypes/datagrid/datatype.ts +++ b/src/datatypes/datagrid/datatype.ts @@ -1,4 +1,4 @@ -import { DataType } from "@/os/datatypes"; +import { DataType, DataTypeWitoutMetaData } from "@/os/datatypes"; import { DecodedChangeWithMetadata } from "@/os/versionControl/groupChanges"; import { Annotation } from "@/os/versionControl/schema"; import { next as A } from "@automerge/automerge"; @@ -123,15 +123,11 @@ const patchesToAnnotations = ( }); }; -export const DataGridDatatype: DataType< +export const DataGridDatatype: DataTypeWitoutMetaData< DataGridDoc, DataGridDocAnchor, string > = { - id: "datagrid", - name: "Spreadsheet", - isExperimental: true, - icon: Sheet, init, getTitle, setTitle, diff --git a/src/datatypes/datagrid/loader.ts b/src/datatypes/datagrid/loader.ts new file mode 100644 index 00000000..93edef06 --- /dev/null +++ b/src/datatypes/datagrid/loader.ts @@ -0,0 +1,15 @@ +import { DataTypeLoaderConfig } from "@/os/datatypes"; +import { Sheet } from "lucide-react"; +import { DataGridDoc, DataGridDocAnchor } from "./schema"; + +export default { + metadata: { + id: "datagrid", + name: "Spreadsheet", + icon: Sheet, + isExperimental: true, + }, + + load: () => + import("./datatype").then(({ DataGridDatatype }) => DataGridDatatype), +} as DataTypeLoaderConfig; diff --git a/src/datatypes/essay/datatype.ts b/src/datatypes/essay/datatype.ts index dd225be8..cd0a7a77 100644 --- a/src/datatypes/essay/datatype.ts +++ b/src/datatypes/essay/datatype.ts @@ -305,5 +305,3 @@ export const MarkdownDatatype: DataTypeWitoutMetaData< sortAnchorsBy, fileExportMethods, }; - -console.log("load markdown"); diff --git a/src/datatypes/essay/loader.ts b/src/datatypes/essay/loader.ts index b6f7af6b..a6f42a7a 100644 --- a/src/datatypes/essay/loader.ts +++ b/src/datatypes/essay/loader.ts @@ -1,14 +1,10 @@ -import { - DataTypeLoaderConfig, - DataTypeWitoutMetaData, - DatatypeId, -} from "@/os/datatypes"; +import { DataTypeLoaderConfig } from "@/os/datatypes"; import { Text } from "lucide-react"; import { MarkdownDoc, MarkdownDocAnchor } from "./schema"; export default { metadata: { - id: "essay" as DatatypeId, + id: "essay", name: "Essay", icon: Text, }, diff --git a/src/datatypes/folder/datatype.ts b/src/datatypes/folder/datatype.ts index 1ce0350b..bac617f9 100644 --- a/src/datatypes/folder/datatype.ts +++ b/src/datatypes/folder/datatype.ts @@ -1,4 +1,4 @@ -import { DataType } from "@/os/datatypes"; +import { DataType, DataTypeWitoutMetaData } from "@/os/datatypes"; import { FolderIcon } from "lucide-react"; import { FolderDoc } from "."; @@ -22,10 +22,7 @@ export const setTitle = (doc: FolderDoc, title: string) => { doc.title = title; }; -export const FolderDatatype: DataType = { - id: "folder", - name: "Folder", - icon: FolderIcon, +export const FolderDatatype: DataTypeWitoutMetaData = { init, getTitle, setTitle, diff --git a/src/datatypes/folder/loader.ts b/src/datatypes/folder/loader.ts new file mode 100644 index 00000000..57f8ef95 --- /dev/null +++ b/src/datatypes/folder/loader.ts @@ -0,0 +1,13 @@ +import { DataTypeLoaderConfig } from "@/os/datatypes"; +import { Folder } from "lucide-react"; +import { FolderDoc } from "./schema"; + +export default { + metadata: { + id: "folder", + name: "Folder", + icon: Folder, + }, + + load: () => import("./datatype").then(({ FolderDatatype }) => FolderDatatype), +} as DataTypeLoaderConfig; diff --git a/src/datatypes/kanban/datatype.ts b/src/datatypes/kanban/datatype.ts index 690603e0..5c61658c 100644 --- a/src/datatypes/kanban/datatype.ts +++ b/src/datatypes/kanban/datatype.ts @@ -1,35 +1,12 @@ -import { DataType } from "@/os/datatypes"; +import { DataType, DataTypeWitoutMetaData } from "@/os/datatypes"; import { ChangeGroup } from "@/os/versionControl/groupChanges"; import { Annotation, - HasVersionControlMetadata, initVersionControlMetadata, } from "@/os/versionControl/schema"; import { next as A } from "@automerge/automerge"; import { KanbanSquare } from "lucide-react"; - -export type Lane = { - id: string; - title: string; - cardIds: string[]; -}; - -export type Card = { - id: string; - title: string; - description: string; - label: string; -}; - -export type KanbanBoardDocAnchor = - | { type: "card"; id: string } - | { type: "lane"; id: string }; - -export type KanbanBoardDoc = { - title: string; - lanes: Lane[]; - cards: Card[]; -} & HasVersionControlMetadata; +import { Card, KanbanBoardDoc, KanbanBoardDocAnchor, Lane } from "./schema"; // When a copy of the document has been made, // update the title so it's more clear which one is the copy vs original. @@ -240,15 +217,11 @@ const actions = { }, }; -export const KanbanBoardDatatype: DataType< +export const KanbanBoardDatatype: DataTypeWitoutMetaData< KanbanBoardDoc, KanbanBoardDocAnchor, undefined > = { - id: "kanban", - name: "Kanban Board", - isExperimental: true, - icon: KanbanSquare, init, getTitle, setTitle, diff --git a/src/datatypes/kanban/loader.ts b/src/datatypes/kanban/loader.ts new file mode 100644 index 00000000..deb1464e --- /dev/null +++ b/src/datatypes/kanban/loader.ts @@ -0,0 +1,15 @@ +import { DataTypeLoaderConfig } from "@/os/datatypes"; +import { KanbanSquare } from "lucide-react"; +import { KanbanBoardDoc } from "./schema"; + +export default { + metadata: { + id: "kanban", + name: "Kanban Board", + icon: KanbanSquare, + isExperimental: true, + }, + + load: () => + import("./datatype").then(({ KanbanBoardDatatype }) => KanbanBoardDatatype), +} as DataTypeLoaderConfig; diff --git a/src/datatypes/kanban/schema.ts b/src/datatypes/kanban/schema.ts new file mode 100644 index 00000000..267d2a65 --- /dev/null +++ b/src/datatypes/kanban/schema.ts @@ -0,0 +1,24 @@ +import { HasVersionControlMetadata } from "@/os/versionControl/schema"; + +export type Lane = { + id: string; + title: string; + cardIds: string[]; +}; + +export type Card = { + id: string; + title: string; + description: string; + label: string; +}; + +export type KanbanBoardDocAnchor = + | { type: "card"; id: string } + | { type: "lane"; id: string }; + +export type KanbanBoardDoc = { + title: string; + lanes: Lane[]; + cards: Card[]; +} & HasVersionControlMetadata; diff --git a/src/datatypes/tldraw/datatype.ts b/src/datatypes/tldraw/datatype.ts index 08b99f74..6587fa88 100644 --- a/src/datatypes/tldraw/datatype.ts +++ b/src/datatypes/tldraw/datatype.ts @@ -1,5 +1,5 @@ import { next as A } from "@automerge/automerge"; -import { DataType } from "@/os/datatypes"; +import { DataType, DataTypeWitoutMetaData } from "@/os/datatypes"; import { init as tldrawinit } from "automerge-tldraw"; import { PenLine } from "lucide-react"; import { TLDrawDoc, TLDrawDocAnchor } from "./schema"; @@ -293,10 +293,11 @@ const valueOfAnnotation = (annotation: Annotation) => { } }; -export const TLDrawDatatype: DataType = { - id: "tldraw", - name: "Drawing", - icon: PenLine, +export const TLDrawDatatype: DataTypeWitoutMetaData< + TLDrawDoc, + TLDrawDocAnchor, + TLShape +> = { init, getTitle, setTitle, diff --git a/src/datatypes/tldraw/loader.ts b/src/datatypes/tldraw/loader.ts new file mode 100644 index 00000000..85ef163c --- /dev/null +++ b/src/datatypes/tldraw/loader.ts @@ -0,0 +1,14 @@ +import { DataTypeLoaderConfig } from "@/os/datatypes"; +import { PenLine } from "lucide-react"; +import { TLDrawDoc, TLDrawDocAnchor } from "./schema"; +import { TLShape } from "@tldraw/tldraw"; + +export default { + metadata: { + id: "tldraw", + name: "Drawing", + icon: PenLine, + }, + + load: () => import("./datatype").then(({ TLDrawDatatype }) => TLDrawDatatype), +} as DataTypeLoaderConfig; diff --git a/src/os/explorer/components/AccountPicker.tsx b/src/os/explorer/components/AccountPicker.tsx index a8f419a3..7c9639df 100644 --- a/src/os/explorer/components/AccountPicker.tsx +++ b/src/os/explorer/components/AccountPicker.tsx @@ -324,6 +324,16 @@ export const AccountPicker = ({
{datatypeSettingsDoc && Object.values(datatypes).map((datatypeLoader) => { + const isEnabled = + datatypeSettingsDoc.enabledDatatypeIds[ + datatypeLoader.metadata.id + ]; + + const isChecked = + isEnabled || + (isEnabled === undefined && + !datatypeLoader.metadata.isExperimental); + return (
e.stopPropagation()} onCheckedChange={() => { changeDatatypeSettingsDoc((settings) => { settings.enabledDatatypeIds[ datatypeLoader.metadata.id - ] = - !settings.enabledDatatypeIds[ - datatypeLoader.metadata.id - ]; + ] = !isChecked; }); }} /> diff --git a/src/os/explorer/components/Sidebar.tsx b/src/os/explorer/components/Sidebar.tsx index b416f89e..240bac9c 100644 --- a/src/os/explorer/components/Sidebar.tsx +++ b/src/os/explorer/components/Sidebar.tsx @@ -336,9 +336,10 @@ export const Sidebar: React.FC = ({
{Object.values(datatypeLoaders).map((datatypeLoader) => { const { id } = datatypeLoader.metadata; + const isEnabled = datatypeSettings?.enabledDatatypeIds[id]; if ( - datatypeLoader.metadata.isExperimental && - !datatypeSettings?.enabledDatatypeIds[id] + isEnabled == false || + (isEnabled !== true && datatypeLoader.metadata.isExperimental) ) { return; } From f200ef9e17c1af33cb20de6dfcd24024df02dc2b Mon Sep 17 00:00:00 2001 From: Paul Sonnentag Date: Thu, 30 May 2024 11:44:50 +0200 Subject: [PATCH 03/30] Abstract datatypeLoaders to modules --- src/datatypes/bot/datatype.ts | 5 +- src/datatypes/bot/{loader.ts => module.ts} | 10 +- src/datatypes/datagrid/datatype.ts | 3 +- .../datagrid/{loader.ts => module.ts} | 14 ++- src/datatypes/essay/{loader.ts => module.ts} | 10 +- src/datatypes/folder/datatype.ts | 3 +- src/datatypes/folder/{loader.ts => module.ts} | 10 +- src/datatypes/kanban/datatype.ts | 3 +- src/datatypes/kanban/loader.ts | 15 --- src/datatypes/kanban/module.ts | 23 +++++ src/datatypes/tldraw/datatype.ts | 20 ++-- src/datatypes/tldraw/{loader.ts => module.ts} | 12 ++- src/os/datatypes.ts | 99 ++++++------------- src/os/explorer/components/AccountPicker.tsx | 4 +- src/os/explorer/components/Explorer.tsx | 4 +- src/os/explorer/components/Sidebar.tsx | 6 +- src/os/explorer/components/Topbar.tsx | 5 +- src/os/explorer/hooks/useSyncDocTitle.ts | 4 +- src/os/modules.ts | 41 ++++++++ src/os/versionControl/annotations.ts | 4 +- src/tools/folder/FolderViewer.tsx | 4 +- 21 files changed, 165 insertions(+), 134 deletions(-) rename src/datatypes/bot/{loader.ts => module.ts} (56%) rename src/datatypes/datagrid/{loader.ts => module.ts} (52%) rename src/datatypes/essay/{loader.ts => module.ts} (52%) rename src/datatypes/folder/{loader.ts => module.ts} (51%) delete mode 100644 src/datatypes/kanban/loader.ts create mode 100644 src/datatypes/kanban/module.ts rename src/datatypes/tldraw/{loader.ts => module.ts} (56%) create mode 100644 src/os/modules.ts diff --git a/src/datatypes/bot/datatype.ts b/src/datatypes/bot/datatype.ts index 487ff50f..7b8b7f1c 100644 --- a/src/datatypes/bot/datatype.ts +++ b/src/datatypes/bot/datatype.ts @@ -1,9 +1,8 @@ -import { ContactDoc, RegisteredContactDoc } from "@/os/explorer/account"; import { MarkdownDatatype } from "@/datatypes/essay/datatype"; import { MarkdownDoc } from "@/datatypes/essay/schema"; -import { DataTypeWitoutMetaData, type DataType } from "@/os/datatypes"; +import { DataTypeWitoutMetaData } from "@/os/datatypes"; +import { ContactDoc, RegisteredContactDoc } from "@/os/explorer/account"; import { AutomergeUrl, Repo } from "@automerge/automerge-repo"; -import { Bot } from "lucide-react"; import { EssayEditingBotDoc } from "./schema"; const BOT_AVATAR_URL = "automerge:uL1duhieqUV4qaeHGHX1dg8FnNy" as AutomergeUrl; diff --git a/src/datatypes/bot/loader.ts b/src/datatypes/bot/module.ts similarity index 56% rename from src/datatypes/bot/loader.ts rename to src/datatypes/bot/module.ts index 3e841be8..7cb64f04 100644 --- a/src/datatypes/bot/loader.ts +++ b/src/datatypes/bot/module.ts @@ -1,8 +1,12 @@ -import { DataTypeLoaderConfig } from "@/os/datatypes"; +import { DataTypeMetadata, DataTypeWitoutMetaData } from "@/os/datatypes"; +import { Module } from "@/os/modules"; import { Bot } from "lucide-react"; import { EssayEditingBotDoc } from "./schema"; -export default { +export default new Module< + DataTypeMetadata, + DataTypeWitoutMetaData +>({ metadata: { id: "bot", name: "Bot", @@ -14,4 +18,4 @@ export default { import("./datatype").then( ({ EssayEditingBotDatatype }) => EssayEditingBotDatatype ), -} as DataTypeLoaderConfig; +}); diff --git a/src/datatypes/datagrid/datatype.ts b/src/datatypes/datagrid/datatype.ts index 76d26c71..cd094cf7 100644 --- a/src/datatypes/datagrid/datatype.ts +++ b/src/datatypes/datagrid/datatype.ts @@ -1,9 +1,8 @@ -import { DataType, DataTypeWitoutMetaData } from "@/os/datatypes"; +import { DataTypeWitoutMetaData } from "@/os/datatypes"; import { DecodedChangeWithMetadata } from "@/os/versionControl/groupChanges"; import { Annotation } from "@/os/versionControl/schema"; import { next as A } from "@automerge/automerge"; import { pick } from "lodash"; -import { Sheet } from "lucide-react"; import { DataGridDoc, DataGridDocAnchor } from "./schema"; // When a copy of the document has been made, diff --git a/src/datatypes/datagrid/loader.ts b/src/datatypes/datagrid/module.ts similarity index 52% rename from src/datatypes/datagrid/loader.ts rename to src/datatypes/datagrid/module.ts index 93edef06..f7617700 100644 --- a/src/datatypes/datagrid/loader.ts +++ b/src/datatypes/datagrid/module.ts @@ -1,8 +1,16 @@ -import { DataTypeLoaderConfig } from "@/os/datatypes"; +import { + DataTypeLoaderConfig, + DataTypeMetadata, + DataTypeWitoutMetaData, +} from "@/os/datatypes"; import { Sheet } from "lucide-react"; import { DataGridDoc, DataGridDocAnchor } from "./schema"; +import { Module } from "@/os/modules"; -export default { +export default new Module< + DataTypeMetadata, + DataTypeWitoutMetaData +>({ metadata: { id: "datagrid", name: "Spreadsheet", @@ -12,4 +20,4 @@ export default { load: () => import("./datatype").then(({ DataGridDatatype }) => DataGridDatatype), -} as DataTypeLoaderConfig; +}); diff --git a/src/datatypes/essay/loader.ts b/src/datatypes/essay/module.ts similarity index 52% rename from src/datatypes/essay/loader.ts rename to src/datatypes/essay/module.ts index a6f42a7a..66fb7362 100644 --- a/src/datatypes/essay/loader.ts +++ b/src/datatypes/essay/module.ts @@ -1,8 +1,12 @@ -import { DataTypeLoaderConfig } from "@/os/datatypes"; +import { DataTypeMetadata, DataTypeWitoutMetaData } from "@/os/datatypes"; import { Text } from "lucide-react"; import { MarkdownDoc, MarkdownDocAnchor } from "./schema"; +import { Module } from "@/os/modules"; -export default { +export default new Module< + DataTypeMetadata, + DataTypeWitoutMetaData +>({ metadata: { id: "essay", name: "Essay", @@ -11,4 +15,4 @@ export default { load: () => import("./datatype").then(({ MarkdownDatatype }) => MarkdownDatatype), -} as DataTypeLoaderConfig; +}); diff --git a/src/datatypes/folder/datatype.ts b/src/datatypes/folder/datatype.ts index bac617f9..9e379f1a 100644 --- a/src/datatypes/folder/datatype.ts +++ b/src/datatypes/folder/datatype.ts @@ -1,5 +1,4 @@ -import { DataType, DataTypeWitoutMetaData } from "@/os/datatypes"; -import { FolderIcon } from "lucide-react"; +import { DataTypeWitoutMetaData } from "@/os/datatypes"; import { FolderDoc } from "."; export const init = (doc: any) => { diff --git a/src/datatypes/folder/loader.ts b/src/datatypes/folder/module.ts similarity index 51% rename from src/datatypes/folder/loader.ts rename to src/datatypes/folder/module.ts index 57f8ef95..9a619162 100644 --- a/src/datatypes/folder/loader.ts +++ b/src/datatypes/folder/module.ts @@ -1,8 +1,12 @@ -import { DataTypeLoaderConfig } from "@/os/datatypes"; +import { DataTypeMetadata, DataTypeWitoutMetaData } from "@/os/datatypes"; import { Folder } from "lucide-react"; import { FolderDoc } from "./schema"; +import { Module } from "@/os/modules"; -export default { +export default new Module< + DataTypeMetadata, + DataTypeWitoutMetaData +>({ metadata: { id: "folder", name: "Folder", @@ -10,4 +14,4 @@ export default { }, load: () => import("./datatype").then(({ FolderDatatype }) => FolderDatatype), -} as DataTypeLoaderConfig; +}); diff --git a/src/datatypes/kanban/datatype.ts b/src/datatypes/kanban/datatype.ts index 5c61658c..b19a87b1 100644 --- a/src/datatypes/kanban/datatype.ts +++ b/src/datatypes/kanban/datatype.ts @@ -1,11 +1,10 @@ -import { DataType, DataTypeWitoutMetaData } from "@/os/datatypes"; +import { DataTypeWitoutMetaData } from "@/os/datatypes"; import { ChangeGroup } from "@/os/versionControl/groupChanges"; import { Annotation, initVersionControlMetadata, } from "@/os/versionControl/schema"; import { next as A } from "@automerge/automerge"; -import { KanbanSquare } from "lucide-react"; import { Card, KanbanBoardDoc, KanbanBoardDocAnchor, Lane } from "./schema"; // When a copy of the document has been made, diff --git a/src/datatypes/kanban/loader.ts b/src/datatypes/kanban/loader.ts deleted file mode 100644 index deb1464e..00000000 --- a/src/datatypes/kanban/loader.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { DataTypeLoaderConfig } from "@/os/datatypes"; -import { KanbanSquare } from "lucide-react"; -import { KanbanBoardDoc } from "./schema"; - -export default { - metadata: { - id: "kanban", - name: "Kanban Board", - icon: KanbanSquare, - isExperimental: true, - }, - - load: () => - import("./datatype").then(({ KanbanBoardDatatype }) => KanbanBoardDatatype), -} as DataTypeLoaderConfig; diff --git a/src/datatypes/kanban/module.ts b/src/datatypes/kanban/module.ts new file mode 100644 index 00000000..7dd1db1b --- /dev/null +++ b/src/datatypes/kanban/module.ts @@ -0,0 +1,23 @@ +import { + DataTypeLoaderConfig, + DataTypeMetadata, + DataTypeWitoutMetaData, +} from "@/os/datatypes"; +import { KanbanSquare } from "lucide-react"; +import { KanbanBoardDoc, KanbanBoardDocAnchor } from "./schema"; +import { Module } from "@/os/modules"; + +export default new Module< + DataTypeMetadata, + DataTypeWitoutMetaData +>({ + metadata: { + id: "kanban", + name: "Kanban Board", + icon: KanbanSquare, + isExperimental: true, + }, + + load: () => + import("./datatype").then(({ KanbanBoardDatatype }) => KanbanBoardDatatype), +}); diff --git a/src/datatypes/tldraw/datatype.ts b/src/datatypes/tldraw/datatype.ts index 6587fa88..58bc73bf 100644 --- a/src/datatypes/tldraw/datatype.ts +++ b/src/datatypes/tldraw/datatype.ts @@ -1,16 +1,20 @@ -import { next as A } from "@automerge/automerge"; -import { DataType, DataTypeWitoutMetaData } from "@/os/datatypes"; -import { init as tldrawinit } from "automerge-tldraw"; -import { PenLine } from "lucide-react"; -import { TLDrawDoc, TLDrawDocAnchor } from "./schema"; +import { DataTypeWitoutMetaData } from "@/os/datatypes"; import { DecodedChangeWithMetadata } from "@/os/versionControl/groupChanges"; -import { pick } from "lodash"; -import { TLShape, TLShapeId, createTLStore } from "@tldraw/tldraw"; import { Annotation, initVersionControlMetadata, } from "@/os/versionControl/schema"; -import { defaultShapeUtils, Editor } from "@tldraw/tldraw"; +import { next as A } from "@automerge/automerge"; +import { + Editor, + TLShape, + TLShapeId, + createTLStore, + defaultShapeUtils, +} from "@tldraw/tldraw"; +import { init as tldrawinit } from "automerge-tldraw"; +import { pick } from "lodash"; +import { TLDrawDoc, TLDrawDocAnchor } from "./schema"; // When a copy of the document has been made, // update the title so it's more clear which one is the copy vs original. diff --git a/src/datatypes/tldraw/loader.ts b/src/datatypes/tldraw/module.ts similarity index 56% rename from src/datatypes/tldraw/loader.ts rename to src/datatypes/tldraw/module.ts index 85ef163c..6ae12d3c 100644 --- a/src/datatypes/tldraw/loader.ts +++ b/src/datatypes/tldraw/module.ts @@ -1,9 +1,13 @@ -import { DataTypeLoaderConfig } from "@/os/datatypes"; +import { DataTypeMetadata, DataTypeWitoutMetaData } from "@/os/datatypes"; +import { Module } from "@/os/modules"; +import { TLShape } from "@tldraw/tldraw"; import { PenLine } from "lucide-react"; import { TLDrawDoc, TLDrawDocAnchor } from "./schema"; -import { TLShape } from "@tldraw/tldraw"; -export default { +export default new Module< + DataTypeMetadata, + DataTypeWitoutMetaData +>({ metadata: { id: "tldraw", name: "Drawing", @@ -11,4 +15,4 @@ export default { }, load: () => import("./datatype").then(({ TLDrawDatatype }) => TLDrawDatatype), -} as DataTypeLoaderConfig; +}); diff --git a/src/os/datatypes.ts b/src/os/datatypes.ts index 127a2172..9516b387 100644 --- a/src/os/datatypes.ts +++ b/src/os/datatypes.ts @@ -8,8 +8,8 @@ import { next as A, Doc } from "@automerge/automerge"; import { Repo } from "@automerge/automerge-repo"; // datatypes -import { useEffect, useRef, useState } from "react"; import { FileExportMethod } from "./fileExports"; +import { Module, useModule } from "./modules"; export type DataTypeMetadata = { id: DatatypeId; @@ -120,82 +120,41 @@ export type DataTypeLoader = { load: () => Promise>; }; -export const getDatatypeLoaders = () => { - const dataTypeLoaders: Record< - DatatypeId, - DataTypeLoader - > = {}; - const dataTypeLoaderModules = import.meta.glob("../datatypes/*/loader.ts", { - eager: true, - }) as Record< - string, - { - default: DataTypeLoader; - } - >; - - for (const [path, { default: loader }] of Object.entries( - dataTypeLoaderModules - )) { - const datatypeId = path.split("/")[2] as DatatypeId; - - if (datatypeId !== loader.metadata.id) { - throw new Error( - `${path} can't be loaded because the id is wrong: "${loader.metadata.id}" should match the folder name` - ); - } - - dataTypeLoaders[datatypeId] = { - async load() { - const result = { - ...(await loader.load()), - ...loader.metadata, - }; - - return result; - }, - metadata: loader.metadata, - } as DataTypeLoader; +const dataTypesFolder: Record< + string, + { default: Module> } +> = import.meta.glob("../datatypes/*/module.@(ts|js|tsx|jsx)", { + eager: true, +}); + +const DATA_TYPE_MODULES: Record< + string, + Module> +> = {}; + +for (const [path, { default: module }] of Object.entries(dataTypesFolder)) { + const id = path.split("/")[2]; + + if (id !== module.metadata.id) { + throw new Error( + `Can't load datatype: id "${module.metadata.id}" does not match the folder name ${id} ` + ); } - return dataTypeLoaders; -}; -const DATA_TYPE_LOADERS = getDatatypeLoaders(); + DATA_TYPE_MODULES[id] = module; +} -export const useDataTypeLoaders = () => { - return DATA_TYPE_LOADERS; +export const useDataTypeModules = () => { + return DATA_TYPE_MODULES; }; export const useDataType = ( dataTypeId: DatatypeId ): DataType | undefined => { - const dataTypeLoaders = useDataTypeLoaders(); - const [dataType, setDataType] = useState>(); - const dataTypeIdRef = useRef(); - dataTypeIdRef.current = dataTypeId; - - useEffect(() => { - const dataTypeLoader = dataTypeLoaders[dataTypeId] as DataTypeLoader< - D, - T, - V - >; - - if (!dataTypeLoader) { - setDataType(undefined); - return; - } - - dataTypeLoader.load().then((dataType) => { - // ignore if dataTypeId has changed in the meantime - if (dataType.id !== dataTypeIdRef.current) { - return; - } - setDataType(dataType); - }); - }, [dataTypeId, dataTypeLoaders]); - - return dataType; + const dataTypeModules = useDataTypeModules(); + const dataType = useModule(dataTypeModules[dataTypeId]); + + return dataType as DataType; }; -export type DatatypeId = string & { __datatypeId: true }; +export type DatatypeId = string; diff --git a/src/os/explorer/components/AccountPicker.tsx b/src/os/explorer/components/AccountPicker.tsx index 7c9639df..60700874 100644 --- a/src/os/explorer/components/AccountPicker.tsx +++ b/src/os/explorer/components/AccountPicker.tsx @@ -33,7 +33,7 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { ContactAvatar } from "./ContactAvatar"; -import { useDataTypeLoaders } from "@/os/datatypes"; +import { useDataTypeModules } from "@/os/datatypes"; import { Checkbox } from "@/components/ui/checkbox"; // 1MB in bytes @@ -56,7 +56,7 @@ export const AccountPicker = ({ const currentAccount = useCurrentAccount(); const self = useSelf(); - const datatypes = useDataTypeLoaders(); + const datatypes = useDataTypeModules(); const [name, setName] = useState(""); const [avatar, setAvatar] = useState(); const [activeTab, setActiveTab] = useState( diff --git a/src/os/explorer/components/Explorer.tsx b/src/os/explorer/components/Explorer.tsx index 1d954ed5..9bd8802b 100644 --- a/src/os/explorer/components/Explorer.tsx +++ b/src/os/explorer/components/Explorer.tsx @@ -12,7 +12,7 @@ import { useCurrentAccountDoc, useRootFolderDocWithChildren, } from "../account"; -import { DatatypeId, useDataTypeLoaders } from "@/os/datatypes"; +import { DatatypeId, useDataTypeModules } from "@/os/datatypes"; import { Toaster } from "@/components/ui/sonner"; import { LoadingScreen } from "./LoadingScreen"; @@ -29,7 +29,7 @@ import { ErrorFallback } from "./ErrorFallback"; export const Explorer: React.FC = () => { const repo = useRepo(); - const datatypeLoaders = useDataTypeLoaders(); + const datatypeLoaders = useDataTypeModules(); const currentAccount = useCurrentAccount(); const [accountDoc] = useCurrentAccountDoc(); diff --git a/src/os/explorer/components/Sidebar.tsx b/src/os/explorer/components/Sidebar.tsx index 240bac9c..8850326f 100644 --- a/src/os/explorer/components/Sidebar.tsx +++ b/src/os/explorer/components/Sidebar.tsx @@ -20,7 +20,7 @@ import { import { DatatypeId, getDatatypeLoaders, - useDataTypeLoaders, + useDataTypeModules, } from "../../datatypes"; import { @@ -40,7 +40,7 @@ import { useDatatypeSettings } from "../account"; const Node = (props: NodeRendererProps) => { const { node, style, dragHandle } = props; - const datatypeLoaders = useDataTypeLoaders(); + const datatypeLoaders = useDataTypeModules(); let Icon; if (node.data.type === "folder") { @@ -160,7 +160,7 @@ export const Sidebar: React.FC = ({ rootFolderDoc, }) => { const repo = useRepo(); - const datatypeLoaders = useDataTypeLoaders(); + const datatypeLoaders = useDataTypeModules(); const { doc: rootFolderDocWithChildren, status, diff --git a/src/os/explorer/components/Topbar.tsx b/src/os/explorer/components/Topbar.tsx index e5495346..05b45a65 100644 --- a/src/os/explorer/components/Topbar.tsx +++ b/src/os/explorer/components/Topbar.tsx @@ -7,7 +7,6 @@ import { GitForkIcon, Menu, MoreHorizontal, - SaveIcon, ShareIcon, Trash2Icon, } from "lucide-react"; @@ -28,7 +27,7 @@ import { import { getHeads, save } from "@automerge/automerge"; import { MarkdownDoc } from "@/datatypes/essay/schema"; -import { DatatypeId, useDataTypeLoaders } from "../../datatypes"; +import { DatatypeId, useDataTypeModules } from "../../datatypes"; import { runBot } from "@/datatypes/bot/essayEditingBot"; import { Button } from "@/components/ui/button"; import { HasVersionControlMetadata } from "@/os/versionControl/schema"; @@ -72,7 +71,7 @@ export const Topbar: React.FC = ({ const selectedDataTypeRef = useRef(); selectedDataTypeRef.current = selectedDataType; - const dataTypeLoaders = useDataTypeLoaders(); + const dataTypeLoaders = useDataTypeModules(); const selectedDataTypeLoader = dataTypeLoaders[selectedDataType]; const [fileExportMethods, setFileExportMethods] = useState< diff --git a/src/os/explorer/hooks/useSyncDocTitle.ts b/src/os/explorer/hooks/useSyncDocTitle.ts index 1031c9ad..2e67ca8b 100644 --- a/src/os/explorer/hooks/useSyncDocTitle.ts +++ b/src/os/explorer/hooks/useSyncDocTitle.ts @@ -1,5 +1,5 @@ import { HasVersionControlMetadata } from "@/os/versionControl/schema"; -import { useDataTypeLoaders } from "../../datatypes"; +import { useDataTypeModules } from "../../datatypes"; import { DocLinkWithFolderPath, FolderDoc } from "@/datatypes/folder"; import { AutomergeUrl, Repo } from "@automerge/automerge-repo"; import { Doc } from "@automerge/automerge/next"; @@ -26,7 +26,7 @@ export const useSyncDocTitle = ({ const counterRef = useRef(0); const selectedDocTitleRef = useRef<{ url: AutomergeUrl; title?: string }>(); - const dataTypeLoaders = useDataTypeLoaders(); + const dataTypeLoaders = useDataTypeModules(); useEffect(() => { if (!selectedDocLink || !selectedDoc) { diff --git a/src/os/modules.ts b/src/os/modules.ts new file mode 100644 index 00000000..24194857 --- /dev/null +++ b/src/os/modules.ts @@ -0,0 +1,41 @@ +import { useEffect, useRef, useState } from "react"; + +export class Module { + readonly metadata: M; + + #load: () => Promise; + + constructor({ metadata, load }: { metadata: M; load: () => Promise }) { + this.metadata = metadata; + this.#load = load; + } + + async load(): Promise { + return { + ...(await this.#load()), + ...this.metadata, + }; + } +} + +export const useModule = (module: Module): (D & M) | undefined => { + const [loadedModule, setLoadedModule] = useState(); + const moduleRef = useRef>(); + moduleRef.current = module; + + useEffect(() => { + if (!module) { + setLoadedModule(undefined); + } + + module.load().then((loadedModule) => { + // ignore if module has changed in the meantime + if (module !== moduleRef.current) { + return; + } + setLoadedModule(loadedModule); + }); + }, [module]); + + return module ? loadedModule : undefined; +}; diff --git a/src/os/versionControl/annotations.ts b/src/os/versionControl/annotations.ts index c741af76..027a3305 100644 --- a/src/os/versionControl/annotations.ts +++ b/src/os/versionControl/annotations.ts @@ -2,7 +2,7 @@ import { useState, useMemo, useRef } from "react"; import * as A from "@automerge/automerge/next"; import { isEqual, sortBy, min } from "lodash"; import { useStaticCallback } from "@/os/hooks/useStaticCallback"; -import { DataType, DatatypeId, useDataTypeLoaders } from "@/os/datatypes"; +import { DataType, DatatypeId, useDataTypeModules } from "@/os/datatypes"; import { Annotation, HighlightAnnotation, @@ -51,7 +51,7 @@ export function useAnnotations({ setHoveredAnnotationGroupId: (id: string) => void; setSelectedAnnotationGroupId: (id: string) => void; } { - const datatypeLoaders = useDataTypeLoaders(); + const datatypeLoaders = useDataTypeModules(); const [hoveredState, setHoveredState] = useState>(); const [selectedState, setSelectedState] = useState>(); diff --git a/src/tools/folder/FolderViewer.tsx b/src/tools/folder/FolderViewer.tsx index 69e23061..663c992a 100644 --- a/src/tools/folder/FolderViewer.tsx +++ b/src/tools/folder/FolderViewer.tsx @@ -6,13 +6,13 @@ import { EditorProps } from "@/os/tools"; import { FolderDoc } from "@/datatypes/folder"; import { TOOLS } from "@/os/tools"; import { selectDocLink } from "@/os/explorer/hooks/useSelectedDocLink"; -import { useDataTypeLoaders } from "@/os/datatypes"; +import { useDataTypeModules } from "@/os/datatypes"; export const FolderViewer: React.FC> = ({ docUrl, docHeads, }: EditorProps) => { - const dataTypeLoaders = useDataTypeLoaders(); + const dataTypeLoaders = useDataTypeModules(); const [folder] = useDocument(docUrl); // used to trigger re-rendering when the doc loads const folderAtHeads = docHeads ? A.view(folder, docHeads) : folder; From af734a2be64e59aec9752f066d0319f2e066a957 Mon Sep 17 00:00:00 2001 From: Paul Sonnentag Date: Thu, 30 May 2024 13:28:45 +0200 Subject: [PATCH 04/30] load tools lazily --- src/datatypes/datagrid/module.ts | 8 +- src/datatypes/kanban/module.ts | 8 +- src/os/datatypes.ts | 12 +-- src/os/explorer/components/Sidebar.tsx | 23 +++--- src/os/explorer/components/Topbar.tsx | 26 +++--- src/os/modules.ts | 1 + src/os/tools.ts | 66 ++++++++------- .../components/ReviewSidebar.tsx | 17 +++- .../components/VersionControlEditor.tsx | 17 +++- src/tools/bot/module.ts | 12 +++ src/tools/bot/{index.ts => tool.ts} | 6 +- src/tools/datagrid/module.ts | 12 +++ src/tools/datagrid/{index.ts => tool.ts} | 6 +- src/tools/essay/module.ts | 12 +++ src/tools/essay/{index.ts => tool.ts} | 6 +- src/tools/folder/FolderViewer.tsx | 81 +++++++++++-------- src/tools/folder/module.ts | 12 +++ src/tools/folder/{index.ts => tool.ts} | 6 +- src/tools/kanban/module.ts | 12 +++ src/tools/kanban/{index.ts => tool.ts} | 6 +- src/tools/tldraw/module.ts | 12 +++ src/tools/tldraw/{index.ts => tool.ts} | 6 +- 22 files changed, 228 insertions(+), 139 deletions(-) create mode 100644 src/tools/bot/module.ts rename src/tools/bot/{index.ts => tool.ts} (65%) create mode 100644 src/tools/datagrid/module.ts rename src/tools/datagrid/{index.ts => tool.ts} (60%) create mode 100644 src/tools/essay/module.ts rename src/tools/essay/{index.ts => tool.ts} (79%) create mode 100644 src/tools/folder/module.ts rename src/tools/folder/{index.ts => tool.ts} (65%) create mode 100644 src/tools/kanban/module.ts rename src/tools/kanban/{index.ts => tool.ts} (64%) create mode 100644 src/tools/tldraw/module.ts rename src/tools/tldraw/{index.ts => tool.ts} (77%) diff --git a/src/datatypes/datagrid/module.ts b/src/datatypes/datagrid/module.ts index f7617700..1aa795cb 100644 --- a/src/datatypes/datagrid/module.ts +++ b/src/datatypes/datagrid/module.ts @@ -1,11 +1,7 @@ -import { - DataTypeLoaderConfig, - DataTypeMetadata, - DataTypeWitoutMetaData, -} from "@/os/datatypes"; +import { DataTypeMetadata, DataTypeWitoutMetaData } from "@/os/datatypes"; +import { Module } from "@/os/modules"; import { Sheet } from "lucide-react"; import { DataGridDoc, DataGridDocAnchor } from "./schema"; -import { Module } from "@/os/modules"; export default new Module< DataTypeMetadata, diff --git a/src/datatypes/kanban/module.ts b/src/datatypes/kanban/module.ts index 7dd1db1b..79dbc3c8 100644 --- a/src/datatypes/kanban/module.ts +++ b/src/datatypes/kanban/module.ts @@ -1,11 +1,7 @@ -import { - DataTypeLoaderConfig, - DataTypeMetadata, - DataTypeWitoutMetaData, -} from "@/os/datatypes"; +import { DataTypeMetadata, DataTypeWitoutMetaData } from "@/os/datatypes"; +import { Module } from "@/os/modules"; import { KanbanSquare } from "lucide-react"; import { KanbanBoardDoc, KanbanBoardDocAnchor } from "./schema"; -import { Module } from "@/os/modules"; export default new Module< DataTypeMetadata, diff --git a/src/os/datatypes.ts b/src/os/datatypes.ts index 9516b387..658be60b 100644 --- a/src/os/datatypes.ts +++ b/src/os/datatypes.ts @@ -110,16 +110,6 @@ export type DataTypeWitoutMetaData = CoreDataType & export type DataType = DataTypeWitoutMetaData & DataTypeMetadata; -export type DataTypeLoaderConfig = { - metadata: DataTypeMetadata; - load: () => Promise>; -}; - -export type DataTypeLoader = { - metadata: DataTypeMetadata; - load: () => Promise>; -}; - const dataTypesFolder: Record< string, { default: Module> } @@ -137,7 +127,7 @@ for (const [path, { default: module }] of Object.entries(dataTypesFolder)) { if (id !== module.metadata.id) { throw new Error( - `Can't load datatype: id "${module.metadata.id}" does not match the folder name ${id} ` + `Can't load datatype: id "${module.metadata.id}" does not match the folder name "${id}" ` ); } diff --git a/src/os/explorer/components/Sidebar.tsx b/src/os/explorer/components/Sidebar.tsx index 8850326f..74008246 100644 --- a/src/os/explorer/components/Sidebar.tsx +++ b/src/os/explorer/components/Sidebar.tsx @@ -1,5 +1,4 @@ import { AutomergeUrl, isValidAutomergeUrl } from "@automerge/automerge-repo"; -import React, { useEffect, useMemo, useRef, useState } from "react"; import { ChevronDown, ChevronRight, @@ -7,9 +6,10 @@ import { FileQuestionIcon, FolderInput, } from "lucide-react"; -import { Tree, NodeRendererProps } from "react-arborist"; -import { FillFlexParent } from "./FillFlexParent"; +import React, { useEffect, useMemo, useRef, useState } from "react"; +import { NodeRendererProps, Tree } from "react-arborist"; import { AccountPicker } from "./AccountPicker"; +import { FillFlexParent } from "./FillFlexParent"; import { DocLink, @@ -17,11 +17,7 @@ import { FolderDoc, FolderDocWithChildren, } from "@/datatypes/folder"; -import { - DatatypeId, - getDatatypeLoaders, - useDataTypeModules, -} from "../../datatypes"; +import { DatatypeId, useDataTypeModules } from "../../datatypes"; import { Popover, @@ -30,13 +26,16 @@ import { } from "@/components/ui/popover"; import { Input } from "@/components/ui/input"; +import { FolderDocWithMetadata } from "@/datatypes/folder/hooks/useFolderDocWithChildren"; +import { HasVersionControlMetadata } from "@/os/versionControl/schema"; import { useDocument, useRepo } from "@automerge/automerge-repo-react-hooks"; import { structuredClone } from "@tldraw/tldraw"; -import { FolderDocWithMetadata } from "@/datatypes/folder/hooks/useFolderDocWithChildren"; import { capitalize, uniqBy } from "lodash"; -import { HasVersionControlMetadata } from "@/os/versionControl/schema"; -import { UIStateDoc, useCurrentAccountDoc } from "../account"; -import { useDatatypeSettings } from "../account"; +import { + UIStateDoc, + useCurrentAccountDoc, + useDatatypeSettings, +} from "../account"; const Node = (props: NodeRendererProps) => { const { node, style, dragHandle } = props; diff --git a/src/os/explorer/components/Topbar.tsx b/src/os/explorer/components/Topbar.tsx index 05b45a65..06ff199a 100644 --- a/src/os/explorer/components/Topbar.tsx +++ b/src/os/explorer/components/Topbar.tsx @@ -1,5 +1,6 @@ -import { DocHandle, isValidAutomergeUrl, Doc } from "@automerge/automerge-repo"; -import React, { useCallback, useEffect, useRef, useState } from "react"; +import { DocLink, DocLinkWithFolderPath, FolderDoc } from "@/datatypes/folder"; +import { Doc, DocHandle, isValidAutomergeUrl } from "@automerge/automerge-repo"; +import { useRepo } from "@automerge/automerge-repo-react-hooks"; import { Bot, Download, @@ -10,12 +11,11 @@ import { ShareIcon, Trash2Icon, } from "lucide-react"; +import React, { useEffect, useRef, useState } from "react"; import { toast } from "sonner"; -import { useRepo } from "@automerge/automerge-repo-react-hooks"; -import { SyncIndicator } from "./SyncIndicator"; -import { AccountPicker } from "./AccountPicker"; import { saveFile } from "../utils"; -import { DocLink, DocLinkWithFolderPath, FolderDoc } from "@/datatypes/folder"; +import { AccountPicker } from "./AccountPicker"; +import { SyncIndicator } from "./SyncIndicator"; import { DropdownMenu, @@ -25,16 +25,15 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { getHeads, save } from "@automerge/automerge"; -import { MarkdownDoc } from "@/datatypes/essay/schema"; -import { DatatypeId, useDataTypeModules } from "../../datatypes"; -import { runBot } from "@/datatypes/bot/essayEditingBot"; import { Button } from "@/components/ui/button"; +import { runBot } from "@/datatypes/bot/essayEditingBot"; +import { MarkdownDoc } from "@/datatypes/essay/schema"; +import { FileExportMethod, genericExportMethods } from "@/os/fileExports"; import { HasVersionControlMetadata } from "@/os/versionControl/schema"; +import { getHeads } from "@automerge/automerge"; +import { DatatypeId, useDataTypeModules } from "../../datatypes"; import { useDatatypeSettings, useRootFolderDocWithChildren } from "../account"; -import botDataType from "@/datatypes/bot"; import { getUrlSafeName } from "../hooks/useSelectedDocLink"; -import { FileExportMethod, genericExportMethods } from "@/os/fileExports"; type TopbarProps = { showSidebar: boolean; @@ -62,8 +61,7 @@ export const Topbar: React.FC = ({ const { flatDocLinks } = useRootFolderDocWithChildren(); const datatypeSettings = useDatatypeSettings(); - const isBotDatatypeEnabled = - datatypeSettings?.enabledDatatypeIds[botDataType.id]; + const isBotDatatypeEnabled = datatypeSettings?.enabledDatatypeIds.bot; const selectedDocUrl = selectedDocLink?.url; const selectedDocName = selectedDocLink?.name; diff --git a/src/os/modules.ts b/src/os/modules.ts index 24194857..628ec5ca 100644 --- a/src/os/modules.ts +++ b/src/os/modules.ts @@ -26,6 +26,7 @@ export const useModule = (module: Module): (D & M) | undefined => { useEffect(() => { if (!module) { setLoadedModule(undefined); + return; } module.load().then((loadedModule) => { diff --git a/src/os/tools.ts b/src/os/tools.ts index 0ca40405..29c13fc3 100644 --- a/src/os/tools.ts +++ b/src/os/tools.ts @@ -1,11 +1,5 @@ import * as A from "@automerge/automerge/next"; -import React from "react"; -import essay from "@/tools/essay"; -import tldraw from "@/tools/tldraw"; -import folder from "@/tools/folder"; -import datagrid from "@/tools/datagrid"; -import bot from "@/tools/bot"; -import kanban from "@/tools/kanban"; +import React, { useMemo } from "react"; import { AutomergeUrl } from "@automerge/automerge-repo"; import { @@ -13,12 +7,16 @@ import { HasVersionControlMetadata, } from "@/os/versionControl/schema"; import { AnnotationWithUIState } from "@/os/versionControl/schema"; -import { DatatypeId } from "./datatypes"; import { DocHandle } from "@automerge/automerge-repo"; +import { Module } from "./modules"; -export type Tool = { - id: DatatypeId; +export type ToolMetaData = { + id: string; + supportedDatatypes: string[]; name: string; +}; + +export type Tool = { editorComponent: React.FC>; annotationViewComponent?: React.FC< AnnotationsViewProps< @@ -50,25 +48,39 @@ export type AnnotationsViewProps< annotations: Annotation[]; }; -const getToolsMap = (tools: Tool[]): Record => { - const map = {}; +const TOOLS: Module[] = []; - tools.forEach((tool) => { - if (!map[tool.id]) { - map[tool.id] = [tool]; - } else { - map[tool.id].push(tools); - } +const toolsFolder: Record }> = + import.meta.glob("../tools/*/module.@(ts|js|tsx|jsx)", { + eager: true, }); - return map; +for (const [path, { default: module }] of Object.entries(toolsFolder)) { + const id = path.split("/")[2]; + + if (id !== module.metadata.id) { + throw new Error( + `Can't load tool: id "${module.metadata.id}" does not match the folder name "${id}"` + ); + } + + TOOLS.push(module); +} + +export const useToolModules = () => { + return TOOLS; }; -export const TOOLS = getToolsMap([ - essay, - tldraw, - folder, - datagrid, - bot, - kanban, -]); +export const useToolModulesForDataType = (dataTypeId: string) => { + const tools = useToolModules(); + + return useMemo( + () => + tools.filter( + (tool) => + tool.metadata.supportedDatatypes.includes(dataTypeId) || + tool.metadata.supportedDatatypes.includes("*") + ), + [tools] + ); +}; diff --git a/src/os/versionControl/components/ReviewSidebar.tsx b/src/os/versionControl/components/ReviewSidebar.tsx index 478e73b4..7e33e760 100644 --- a/src/os/versionControl/components/ReviewSidebar.tsx +++ b/src/os/versionControl/components/ReviewSidebar.tsx @@ -10,7 +10,12 @@ import { DataType, DatatypeId } from "@/os/datatypes"; import { useCurrentAccount } from "@/os/explorer/account"; import { ContactAvatar } from "@/os/explorer/components/ContactAvatar"; import { getRelativeTimeString } from "@/os/lib/dates"; -import { AnnotationsViewProps, TOOLS } from "@/os/tools"; +import { + AnnotationsViewProps, + Tool, + ToolMetaData, + useToolModulesForDataType, +} from "@/os/tools"; import { AnnotationGroup, AnnotationGroupWithState, @@ -23,6 +28,7 @@ import { DocHandle } from "@automerge/automerge-repo"; import { Check, MessageCircleIcon } from "lucide-react"; import React, { forwardRef, useEffect, useMemo, useRef, useState } from "react"; import { getAnnotationGroupId } from "../annotations"; +import { useModule } from "@/os/modules"; type ReviewSidebarProps = { doc: HasVersionControlMetadata; @@ -564,11 +570,18 @@ const AnnotationsView = ({ unknown, unknown >) => { + const tools = useToolModulesForDataType(datatypeId); + const tool = useModule(tools[0]); + + if (!tool) { + return; + } + // For now, we just use the first annotation viewer available for this doc type. // In the future, we might want to: // - use an annotations view that's similar to the viewer being used for the main doc // - allow switching between different viewers? - const Viewer = TOOLS[datatypeId]?.[0].annotationViewComponent; + const Viewer = tool.annotationViewComponent; if (!Viewer) { return (
diff --git a/src/os/versionControl/components/VersionControlEditor.tsx b/src/os/versionControl/components/VersionControlEditor.tsx index c9264f8d..eeb3c508 100644 --- a/src/os/versionControl/components/VersionControlEditor.tsx +++ b/src/os/versionControl/components/VersionControlEditor.tsx @@ -27,7 +27,12 @@ import { isMarkdownDoc } from "@/datatypes/essay"; import { DatatypeId, useDataType } from "@/os/datatypes"; import { getRelativeTimeString } from "@/os/lib/dates"; import { isLLMActive } from "@/os/lib/llm"; -import { EditorProps, TOOLS } from "@/os/tools"; +import { + EditorProps, + Tool, + ToolMetaData, + useToolModulesForDataType, +} from "@/os/tools"; import { SideBySide as TLDrawSideBySide } from "@/tools/tldraw/components/TLDraw"; import { AutomergeUrl } from "@automerge/automerge-repo"; import { @@ -74,6 +79,7 @@ import { } from "../utils"; import { PositionMap, ReviewSidebar } from "./ReviewSidebar"; import { TimelineSidebar } from "./TimelineSidebar"; +import { useModule } from "@/os/modules"; interface MakeBranchOptions { name?: string; @@ -652,8 +658,15 @@ const DocEditor = ({ setSelectedAnchors, setHoveredAnchor, }: EditorPropsWithDatatype) => { + const toolModules = useToolModulesForDataType(datatypeId); // Currently we don't have a toolpicker so we just show the first tool for the doc type - const Component = TOOLS[datatypeId][0].editorComponent; + const tool = useModule(toolModules[0]); + + if (!tool) { + return; + } + + const Component = tool.editorComponent; return ( ({ + metadata: { + id: "bot", + name: "Bot", + supportedDatatypes: ["bot"], + }, + + load: () => import("./tool").then(({ BotTool }) => BotTool), +}); diff --git a/src/tools/bot/index.ts b/src/tools/bot/tool.ts similarity index 65% rename from src/tools/bot/index.ts rename to src/tools/bot/tool.ts index dc52028e..48ae076d 100644 --- a/src/tools/bot/index.ts +++ b/src/tools/bot/tool.ts @@ -1,8 +1,6 @@ import { Tool } from "@/os/tools"; import { BotEditor } from "./BotEditor"; -export default { - id: "bot", - name: "Bot", +export const BotTool: Tool = { editorComponent: BotEditor, -} as Tool; +}; diff --git a/src/tools/datagrid/module.ts b/src/tools/datagrid/module.ts new file mode 100644 index 00000000..56eb0451 --- /dev/null +++ b/src/tools/datagrid/module.ts @@ -0,0 +1,12 @@ +import { Module } from "@/os/modules"; +import { Tool, ToolMetaData } from "@/os/tools"; + +export default new Module({ + metadata: { + name: "Spreadsheet", + id: "datagrid", + supportedDatatypes: ["datagrid"], + }, + + load: () => import("./tool").then(({ SpreadsheetTool }) => SpreadsheetTool), +}); diff --git a/src/tools/datagrid/index.ts b/src/tools/datagrid/tool.ts similarity index 60% rename from src/tools/datagrid/index.ts rename to src/tools/datagrid/tool.ts index 81a1cd34..09ae193a 100644 --- a/src/tools/datagrid/index.ts +++ b/src/tools/datagrid/tool.ts @@ -1,8 +1,6 @@ import { Tool } from "@/os/tools"; import { DataGrid } from "./DataGrid"; -export default { - id: "datagrid", - name: "Spreadsheet", +export const SpreadsheetTool: Tool = { editorComponent: DataGrid, -} as Tool; +}; diff --git a/src/tools/essay/module.ts b/src/tools/essay/module.ts new file mode 100644 index 00000000..6ea8ca63 --- /dev/null +++ b/src/tools/essay/module.ts @@ -0,0 +1,12 @@ +import { Module } from "@/os/modules"; +import { Tool, ToolMetaData } from "@/os/tools"; + +export default new Module({ + metadata: { + id: "essay", + name: "Editor", + supportedDatatypes: ["essay"], + }, + + load: () => import("./tool").then(({ EssayEditorTool }) => EssayEditorTool), +}); diff --git a/src/tools/essay/index.ts b/src/tools/essay/tool.ts similarity index 79% rename from src/tools/essay/index.ts rename to src/tools/essay/tool.ts index b0918c2d..4e44a131 100644 --- a/src/tools/essay/index.ts +++ b/src/tools/essay/tool.ts @@ -2,9 +2,7 @@ import { Tool } from "@/os/tools"; import { EssayEditor } from "./components/EssayEditor"; import { EssayAnnotations } from "./components/EssayAnnotations"; -export default { - id: "essay", - name: "Editor", +export const EssayEditorTool: Tool = { editorComponent: EssayEditor, annotationViewComponent: EssayAnnotations, -} as Tool; +}; diff --git a/src/tools/folder/FolderViewer.tsx b/src/tools/folder/FolderViewer.tsx index 663c992a..983ec3be 100644 --- a/src/tools/folder/FolderViewer.tsx +++ b/src/tools/folder/FolderViewer.tsx @@ -2,17 +2,16 @@ import { useDocument } from "@automerge/automerge-repo-react-hooks"; import * as A from "@automerge/automerge/next"; import React from "react"; -import { EditorProps } from "@/os/tools"; -import { FolderDoc } from "@/datatypes/folder"; -import { TOOLS } from "@/os/tools"; +import { EditorProps, useToolModulesForDataType } from "@/os/tools"; +import { DocLink, FolderDoc } from "@/datatypes/folder"; import { selectDocLink } from "@/os/explorer/hooks/useSelectedDocLink"; import { useDataTypeModules } from "@/os/datatypes"; +import { useModule } from "@/os/modules"; export const FolderViewer: React.FC> = ({ docUrl, docHeads, }: EditorProps) => { - const dataTypeLoaders = useDataTypeModules(); const [folder] = useDocument(docUrl); // used to trigger re-rendering when the doc loads const folderAtHeads = docHeads ? A.view(folder, docHeads) : folder; @@ -27,38 +26,50 @@ export const FolderViewer: React.FC> = ({ {folderAtHeads.docs.length} documents
- {folderAtHeads.docs.map((docLink) => { - const Tool = TOOLS[docLink.type][0].editorComponent; - const Icon = dataTypeLoaders[docLink.type].metadata.icon; + {folderAtHeads.docs.map((docLink) => ( + + ))} +
+
+ ); +}; + +interface DocLinkPreviewProps { + docLink: DocLink; +} - return ( -
-
- -
{docLink.name}
- -
-
- {!Tool &&
No editor available
} - {Tool && docLink.type !== "folder" && ( - - )} - {docLink.type === "folder" && ( -
- Click "open" to see nested folder contents -
- )} -
-
- ); - })} +const DocLinkPreview: React.FC = ({ docLink }) => { + const dataTypeModules = useDataTypeModules(); + // we currently don't have a tool picker so we just use the first tool + const toolModules = useToolModulesForDataType(docLink.type); + const tool = useModule(toolModules[0]); + + const Tool = tool?.editorComponent; + + const Icon = dataTypeModules[docLink.type].metadata.icon; + + return ( +
+
+ +
{docLink.name}
+ +
+
+ {toolModules.length === 0 &&
No editor available
} + {Tool && docLink.type !== "folder" && } + {docLink.type === "folder" && ( +
+ Click "open" to see nested folder contents +
+ )}
); diff --git a/src/tools/folder/module.ts b/src/tools/folder/module.ts new file mode 100644 index 00000000..a756e72f --- /dev/null +++ b/src/tools/folder/module.ts @@ -0,0 +1,12 @@ +import { Module } from "@/os/modules"; +import { Tool, ToolMetaData } from "@/os/tools"; + +export default new Module({ + metadata: { + id: "folder", + name: "Folder", + supportedDatatypes: ["folder"], + }, + + load: () => import("./tool").then(({ FolderViewerTool }) => FolderViewerTool), +}); diff --git a/src/tools/folder/index.ts b/src/tools/folder/tool.ts similarity index 65% rename from src/tools/folder/index.ts rename to src/tools/folder/tool.ts index 2da8ed12..05eeefc3 100644 --- a/src/tools/folder/index.ts +++ b/src/tools/folder/tool.ts @@ -1,8 +1,6 @@ import { Tool } from "@/os/tools"; import { FolderViewer } from "./FolderViewer"; -export default { - id: "folder", - name: "Folder", +export const FolderViewerTool: Tool = { editorComponent: FolderViewer, -} as Tool; +}; diff --git a/src/tools/kanban/module.ts b/src/tools/kanban/module.ts new file mode 100644 index 00000000..721dbda5 --- /dev/null +++ b/src/tools/kanban/module.ts @@ -0,0 +1,12 @@ +import { Module } from "@/os/modules"; +import { Tool, ToolMetaData } from "@/os/tools"; + +export default new Module({ + metadata: { + id: "kanban", + name: "Kanban", + supportedDatatypes: ["kanban"], + }, + + load: () => import("./tool").then(({ KanbanTool }) => KanbanTool), +}); diff --git a/src/tools/kanban/index.ts b/src/tools/kanban/tool.ts similarity index 64% rename from src/tools/kanban/index.ts rename to src/tools/kanban/tool.ts index 310af2ad..086c14e9 100644 --- a/src/tools/kanban/index.ts +++ b/src/tools/kanban/tool.ts @@ -1,8 +1,6 @@ import { Tool } from "@/os/tools"; import { KanbanBoard } from "./KanbanBoard"; -export default { - id: "kanban", - name: "Kanban", +export const KanbanTool: Tool = { editorComponent: KanbanBoard, -} as Tool; +}; diff --git a/src/tools/tldraw/module.ts b/src/tools/tldraw/module.ts new file mode 100644 index 00000000..3dfaa757 --- /dev/null +++ b/src/tools/tldraw/module.ts @@ -0,0 +1,12 @@ +import { Module } from "@/os/modules"; +import { Tool, ToolMetaData } from "@/os/tools"; + +export default new Module({ + metadata: { + id: "tldraw", + name: "Drawing", + supportedDatatypes: ["tldraw"], + }, + + load: () => import("./tool").then(({ DrawingTool }) => DrawingTool), +}); diff --git a/src/tools/tldraw/index.ts b/src/tools/tldraw/tool.ts similarity index 77% rename from src/tools/tldraw/index.ts rename to src/tools/tldraw/tool.ts index 15b1fe39..df1ff2be 100644 --- a/src/tools/tldraw/index.ts +++ b/src/tools/tldraw/tool.ts @@ -2,9 +2,7 @@ import { Tool } from "@/os/tools"; import { TLDraw } from "./components/TLDraw"; import { TLDrawAnnotations } from "./components/TLDrawAnnotations"; -export default { - id: "tldraw", - name: "Drawing", +export const DrawingTool: Tool = { editorComponent: TLDraw, annotationViewComponent: TLDrawAnnotations, -} as Tool; +}; From 4d4f87674de6c7b225a4501738e3e126dbf68efe Mon Sep 17 00:00:00 2001 From: Paul Sonnentag Date: Thu, 30 May 2024 13:33:06 +0200 Subject: [PATCH 05/30] cleanup naming dataTypeLoader -> dataTypeModule --- src/os/explorer/components/AccountPicker.tsx | 22 +++++++------- src/os/explorer/components/Explorer.tsx | 8 ++--- src/os/explorer/components/Sidebar.tsx | 32 ++++++++++++-------- src/os/explorer/components/Topbar.tsx | 16 +++++----- src/os/explorer/hooks/useSyncDocTitle.ts | 4 +-- src/os/versionControl/annotations.ts | 1 - 6 files changed, 44 insertions(+), 39 deletions(-) diff --git a/src/os/explorer/components/AccountPicker.tsx b/src/os/explorer/components/AccountPicker.tsx index 60700874..8a7c0912 100644 --- a/src/os/explorer/components/AccountPicker.tsx +++ b/src/os/explorer/components/AccountPicker.tsx @@ -56,7 +56,7 @@ export const AccountPicker = ({ const currentAccount = useCurrentAccount(); const self = useSelf(); - const datatypes = useDataTypeModules(); + const datatypeModules = useDataTypeModules(); const [name, setName] = useState(""); const [avatar, setAvatar] = useState(); const [activeTab, setActiveTab] = useState( @@ -323,44 +323,44 @@ export const AccountPicker = ({
{datatypeSettingsDoc && - Object.values(datatypes).map((datatypeLoader) => { + Object.values(datatypeModules).map((datatypeModule) => { const isEnabled = datatypeSettingsDoc.enabledDatatypeIds[ - datatypeLoader.metadata.id + datatypeModule.metadata.id ]; const isChecked = isEnabled || (isEnabled === undefined && - !datatypeLoader.metadata.isExperimental); + !datatypeModule.metadata.isExperimental); return (
e.stopPropagation()} onCheckedChange={() => { changeDatatypeSettingsDoc((settings) => { settings.enabledDatatypeIds[ - datatypeLoader.metadata.id + datatypeModule.metadata.id ] = !isChecked; }); }} />
); diff --git a/src/os/explorer/components/Explorer.tsx b/src/os/explorer/components/Explorer.tsx index 9bd8802b..d86b291d 100644 --- a/src/os/explorer/components/Explorer.tsx +++ b/src/os/explorer/components/Explorer.tsx @@ -29,7 +29,7 @@ import { ErrorFallback } from "./ErrorFallback"; export const Explorer: React.FC = () => { const repo = useRepo(); - const datatypeLoaders = useDataTypeModules(); + const datatypeModules = useDataTypeModules(); const currentAccount = useCurrentAccount(); const [accountDoc] = useCurrentAccountDoc(); @@ -64,12 +64,12 @@ export const Explorer: React.FC = () => { const addNewDocument = useCallback( async ({ type }: { type: DatatypeId }) => { - const datatypeLoader = datatypeLoaders[type]; + const datatypeModule = datatypeModules[type]; - if (!datatypeLoader) { + if (!datatypeModule) { throw new Error(`Unsupported document type: ${type}`); } - const datatype = await datatypeLoader.load(); + const datatype = await datatypeModule.load(); const newDocHandle = repo.create>(); diff --git a/src/os/explorer/components/Sidebar.tsx b/src/os/explorer/components/Sidebar.tsx index 74008246..0bdc6741 100644 --- a/src/os/explorer/components/Sidebar.tsx +++ b/src/os/explorer/components/Sidebar.tsx @@ -6,7 +6,13 @@ import { FileQuestionIcon, FolderInput, } from "lucide-react"; -import React, { useEffect, useMemo, useRef, useState } from "react"; +import React, { + ReactElement, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import { NodeRendererProps, Tree } from "react-arborist"; import { AccountPicker } from "./AccountPicker"; import { FillFlexParent } from "./FillFlexParent"; @@ -39,7 +45,7 @@ import { const Node = (props: NodeRendererProps) => { const { node, style, dragHandle } = props; - const datatypeLoaders = useDataTypeModules(); + const datatypeModules = useDataTypeModules(); let Icon; if (node.data.type === "folder") { @@ -49,7 +55,7 @@ const Node = (props: NodeRendererProps) => { Icon = ChevronRight; } } else { - Icon = datatypeLoaders[node.data.type]?.metadata.icon ?? FileQuestionIcon; + Icon = datatypeModules[node.data.type]?.metadata.icon ?? FileQuestionIcon; } return ( @@ -79,7 +85,7 @@ const Node = (props: NodeRendererProps) => { {!node.isEditing && ( <>
- {datatypeLoaders[node.data.type] + {datatypeModules[node.data.type] ? node.data.name : `Unknown type: ${node.data.type}`}
@@ -159,7 +165,7 @@ export const Sidebar: React.FC = ({ rootFolderDoc, }) => { const repo = useRepo(); - const datatypeLoaders = useDataTypeModules(); + const datatypeModules = useDataTypeModules(); const { doc: rootFolderDocWithChildren, status, @@ -239,8 +245,8 @@ export const Sidebar: React.FC = ({ const onRename = async ({ node, name }) => { const docLink = flatDocLinks.find((doc) => doc.url === node.data.url); - const datatypeLoader = datatypeLoaders[docLink.type]; - const datatype = await datatypeLoader.load(); + const datatypeModule = datatypeModules[docLink.type]; + const datatype = await datatypeModule.load(); if (!datatype.setTitle) { alert( @@ -333,28 +339,28 @@ export const Sidebar: React.FC = ({
- {Object.values(datatypeLoaders).map((datatypeLoader) => { - const { id } = datatypeLoader.metadata; + {Object.values(datatypeModules).map((datatypeModule) => { + const { id } = datatypeModule.metadata; const isEnabled = datatypeSettings?.enabledDatatypeIds[id]; if ( isEnabled == false || - (isEnabled !== true && datatypeLoader.metadata.isExperimental) + (isEnabled !== true && datatypeModule.metadata.isExperimental) ) { return; } return ( -
+
{" "}
addNewDocument({ type: id as DatatypeId })} > - - New {datatypeLoader.metadata.name} + New {datatypeModule.metadata.name}
); diff --git a/src/os/explorer/components/Topbar.tsx b/src/os/explorer/components/Topbar.tsx index 06ff199a..c674086f 100644 --- a/src/os/explorer/components/Topbar.tsx +++ b/src/os/explorer/components/Topbar.tsx @@ -69,23 +69,23 @@ export const Topbar: React.FC = ({ const selectedDataTypeRef = useRef(); selectedDataTypeRef.current = selectedDataType; - const dataTypeLoaders = useDataTypeModules(); - const selectedDataTypeLoader = dataTypeLoaders[selectedDataType]; + const dataTypeModules = useDataTypeModules(); + const selectedDataTypeModule = dataTypeModules[selectedDataType]; const [fileExportMethods, setFileExportMethods] = useState< FileExportMethod[] >([]); useEffect(() => { - if (!selectedDataTypeLoader) { + if (!selectedDataTypeModule) { setFileExportMethods([]); } else { - selectedDataTypeLoader.load().then((datatype) => { + selectedDataTypeModule.load().then((datatype) => { if (datatype.id === selectedDataType) { setFileExportMethods(datatype.fileExportMethods ?? []); } }); } - }, [selectedDataTypeLoader]); + }, [selectedDataTypeModule]); const botDocLinks = flatDocLinks?.filter((doc) => doc.type === "bot") ?? []; @@ -100,8 +100,8 @@ export const Topbar: React.FC = ({
)}
- {selectedDataTypeLoader && - React.createElement(selectedDataTypeLoader.metadata.icon, { + {selectedDataTypeModule && + React.createElement(selectedDataTypeModule.metadata.icon, { className: "inline mr-1", size: 14, })} @@ -214,7 +214,7 @@ export const Topbar: React.FC = ({ { - const selectedDataType = await selectedDataTypeLoader.load(); + const selectedDataType = await selectedDataTypeModule.load(); const newHandle = repo.clone>( diff --git a/src/os/explorer/hooks/useSyncDocTitle.ts b/src/os/explorer/hooks/useSyncDocTitle.ts index 2e67ca8b..38b45a31 100644 --- a/src/os/explorer/hooks/useSyncDocTitle.ts +++ b/src/os/explorer/hooks/useSyncDocTitle.ts @@ -26,7 +26,7 @@ export const useSyncDocTitle = ({ const counterRef = useRef(0); const selectedDocTitleRef = useRef<{ url: AutomergeUrl; title?: string }>(); - const dataTypeLoaders = useDataTypeModules(); + const dataTypeModule = useDataTypeModules(); useEffect(() => { if (!selectedDocLink || !selectedDoc) { @@ -42,7 +42,7 @@ export const useSyncDocTitle = ({ let counter = (counterRef.current = counterRef.current + 1); // load title - dataTypeLoaders[selectedDocLink.type] + dataTypeModule[selectedDocLink.type] .load() .then((dataType) => dataType.getTitle(selectedDoc, repo)) .then((title) => { diff --git a/src/os/versionControl/annotations.ts b/src/os/versionControl/annotations.ts index 027a3305..d0f826ec 100644 --- a/src/os/versionControl/annotations.ts +++ b/src/os/versionControl/annotations.ts @@ -51,7 +51,6 @@ export function useAnnotations({ setHoveredAnnotationGroupId: (id: string) => void; setSelectedAnnotationGroupId: (id: string) => void; } { - const datatypeLoaders = useDataTypeModules(); const [hoveredState, setHoveredState] = useState>(); const [selectedState, setSelectedState] = useState>(); From 485530e727f56d626975a37792e0e19db9567f04 Mon Sep 17 00:00:00 2001 From: Paul Sonnentag Date: Thu, 30 May 2024 13:43:27 +0200 Subject: [PATCH 06/30] Fix typescript errors --- src/datatypes/kanban/datatype.ts | 6 +++--- src/datatypes/kanban/index.ts | 5 ++--- src/datatypes/kanban/useDocumentWithActions.ts | 14 ++++++++++---- src/tools/kanban/KanbanBoard.tsx | 4 ++-- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/datatypes/kanban/datatype.ts b/src/datatypes/kanban/datatype.ts index b19a87b1..bf9d777f 100644 --- a/src/datatypes/kanban/datatype.ts +++ b/src/datatypes/kanban/datatype.ts @@ -1,4 +1,4 @@ -import { DataTypeWitoutMetaData } from "@/os/datatypes"; +import { DataTypeWitoutMetaData, useDataType } from "@/os/datatypes"; import { ChangeGroup } from "@/os/versionControl/groupChanges"; import { Annotation, @@ -9,7 +9,7 @@ import { Card, KanbanBoardDoc, KanbanBoardDocAnchor, Lane } from "./schema"; // When a copy of the document has been made, // update the title so it's more clear which one is the copy vs original. -export const markCopy = () => { +const markCopy = () => { console.error("todo"); }; @@ -21,7 +21,7 @@ const setTitle = async (doc: KanbanBoardDoc, title: string) => { doc.title = title; }; -export const init = (doc: any) => { +const init = (doc: any) => { doc.title = "Untitled Board"; doc.lanes = []; doc.cards = []; diff --git a/src/datatypes/kanban/index.ts b/src/datatypes/kanban/index.ts index d0380db5..320a9929 100644 --- a/src/datatypes/kanban/index.ts +++ b/src/datatypes/kanban/index.ts @@ -1,4 +1,3 @@ -import { KanbanBoardDatatype } from "./datatype"; -export default KanbanBoardDatatype; - +export { KanbanBoardDatatype } from "./datatype"; +export * from "./schema"; export * from "./useDocumentWithActions"; diff --git a/src/datatypes/kanban/useDocumentWithActions.ts b/src/datatypes/kanban/useDocumentWithActions.ts index 109fdb80..f47f26cc 100644 --- a/src/datatypes/kanban/useDocumentWithActions.ts +++ b/src/datatypes/kanban/useDocumentWithActions.ts @@ -1,7 +1,7 @@ import { AutomergeUrl } from "@automerge/automerge-repo"; import { useDocument, useHandle } from "@automerge/automerge-repo-react-hooks"; import { useMemo } from "react"; -import { DataType } from "../../os/datatypes"; +import { DataType, useDataType } from "../../os/datatypes"; /** Returns a doc with helper actions from a datatype definition. * @@ -14,13 +14,19 @@ import { DataType } from "../../os/datatypes"; */ export const useDocumentWithActions = ( docUrl: AutomergeUrl, - datatype: DataType + datatypeId: string ) => { + const dataType = useDataType(datatypeId); const [doc, changeDoc] = useDocument(docUrl); const handle = useHandle(docUrl); const actions = useMemo(() => { const result = {}; - for (const [key, value] of Object.entries(datatype.actions)) { + + if (!dataType) { + return; + } + + for (const [key, value] of Object.entries(dataType.actions)) { result[key] = (args: object) => { handle.change( (doc: D) => { @@ -36,6 +42,6 @@ export const useDocumentWithActions = ( }; } return result as Record void>; - }, [datatype, handle]); + }, [dataType, handle]); return [doc, changeDoc, actions] as const; }; diff --git a/src/tools/kanban/KanbanBoard.tsx b/src/tools/kanban/KanbanBoard.tsx index 344498a1..aeb5f985 100644 --- a/src/tools/kanban/KanbanBoard.tsx +++ b/src/tools/kanban/KanbanBoard.tsx @@ -4,7 +4,7 @@ import { KanbanBoardDatatype, KanbanBoardDoc, KanbanBoardDocAnchor, -} from "../../datatypes/kanban/datatype"; +} from "@/datatypes/kanban"; import { EditorProps } from "@/os/tools"; import { useDocumentWithActions } from "@/datatypes/kanban/useDocumentWithActions"; @@ -18,7 +18,7 @@ export const KanbanBoard = ({ annotations = [], }: EditorProps & { readOnly?: boolean }) => { const [latestDoc, _changeDoc, actions] = - useDocumentWithActions(docUrl, KanbanBoardDatatype); // used to trigger re-rendering when the doc loads + useDocumentWithActions(docUrl, "kanban"); // used to trigger re-rendering when the doc loads const doc = useMemo( () => (docHeads ? A.view(latestDoc, docHeads) : latestDoc), From 534cc4200cfda87f05374efbe901bb895d885621 Mon Sep 17 00:00:00 2001 From: Paul Sonnentag Date: Thu, 30 May 2024 16:07:18 +0200 Subject: [PATCH 07/30] export all datatypes as searate files --- package.json | 1 + src/tools/kanban/KanbanBoard.tsx | 6 +----- vite.config.ts | 23 +++++++++++++++++++++++ yarn.lock | 29 +++++++++++++++++++++++++++-- 4 files changed, 52 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 653b2c27..1dfe58a4 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "eslint": "^8.45.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.3", + "glob": "^10.4.1", "jsdom": "^22.1.0", "pnpm": "^8.14.1", "postcss": "^8.4.31", diff --git a/src/tools/kanban/KanbanBoard.tsx b/src/tools/kanban/KanbanBoard.tsx index aeb5f985..31ee5dcb 100644 --- a/src/tools/kanban/KanbanBoard.tsx +++ b/src/tools/kanban/KanbanBoard.tsx @@ -1,10 +1,6 @@ import * as A from "@automerge/automerge/next"; -import { - KanbanBoardDatatype, - KanbanBoardDoc, - KanbanBoardDocAnchor, -} from "@/datatypes/kanban"; +import { KanbanBoardDoc, KanbanBoardDocAnchor } from "@/datatypes/kanban"; import { EditorProps } from "@/os/tools"; import { useDocumentWithActions } from "@/datatypes/kanban/useDocumentWithActions"; diff --git a/vite.config.ts b/vite.config.ts index fcb35fc6..5bb8c2d2 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,6 +4,8 @@ import path from "path"; import react from "@vitejs/plugin-react"; import wasm from "vite-plugin-wasm"; import topLevelAwait from "vite-plugin-top-level-await"; +import { globSync } from "glob"; +import { fileURLToPath } from "node:url"; export default defineConfig({ base: "./", @@ -35,6 +37,18 @@ export default defineConfig({ input: { main: path.resolve(__dirname, "index.html"), "service-worker": path.resolve(__dirname, "service-worker.js"), + ...Object.fromEntries( + globSync( + path.resolve(__dirname, "src/datatypes/*/module.@(ts|js|tsx|jsx)") + ).map((path) => { + const datatypeId = path.split("/").slice(-2)[0]; + + return [ + `dataType-${datatypeId}`, + fileURLToPath(new URL(path, import.meta.url)), + ]; + }) + ), }, output: { // We put index.css in dist instead of dist/assets so that we can link to fonts @@ -52,9 +66,18 @@ export default defineConfig({ if (chunkInfo.name === "service-worker") { return "[name].js"; // This will place service-worker.js directly under dist } + + if (chunkInfo.name.startsWith("dataType-")) { + const typeId = chunkInfo.name.split("-")[1]; + + return `dataTypes/${typeId}.js`; + } + return "assets/[name]-[hash].js"; // Default behavior for other entries }, + exports: "named", }, + preserveEntrySignatures: "allow-extension", }, }, diff --git a/yarn.lock b/yarn.lock index 0ad23e0b..7a3353f2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3898,6 +3898,17 @@ glob@^10.3.10: minipass "^7.0.4" path-scurry "^1.11.0" +glob@^10.4.1: + version "10.4.1" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.1.tgz#0cfb01ab6a6b438177bfe6a58e2576f6efe909c2" + integrity sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw== + dependencies: + foreground-child "^3.1.0" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + path-scurry "^1.11.1" + glob@^7.1.3: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" @@ -4185,6 +4196,15 @@ jackspeak@^2.3.6: optionalDependencies: "@pkgjs/parseargs" "^0.11.0" +jackspeak@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.1.2.tgz#eada67ea949c6b71de50f1b09c92a961897b90ab" + integrity sha512-kWmLKn2tRtfYMF/BakihVVRzBKOxz4gJMiL2Rj91WnAB5TPZumSH99R/Yf1qE1u4uRimvCSJfm6hnxohXeEXjQ== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + jiti@^1.21.0: version "1.21.0" resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d" @@ -4454,7 +4474,7 @@ minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimatch@^9.0.1: +minimatch@^9.0.1, minimatch@^9.0.4: version "9.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51" integrity sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw== @@ -4466,6 +4486,11 @@ minimatch@^9.0.1: resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.1.tgz#f7f85aff59aa22f110b20e27692465cf3bf89481" integrity sha512-UZ7eQ+h8ywIRAW1hIEl2AqdwzJucU/Kp59+8kkZeSvafXhZjul247BvIJjEVFVeON6d7lM46XX1HXCduKAS8VA== +minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + mlly@^1.4.0, mlly@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.7.0.tgz#587383ae40dda23cadb11c3c3cc972b277724271" @@ -4679,7 +4704,7 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-scurry@^1.11.0: +path-scurry@^1.11.0, path-scurry@^1.11.1: version "1.11.1" resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== From 0fa88c361afb4adc7705c716659f12ab425c696c Mon Sep 17 00:00:00 2001 From: Paul Sonnentag Date: Thu, 30 May 2024 16:14:34 +0200 Subject: [PATCH 08/30] export tools as separate files --- vite.config.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index 5bb8c2d2..4b3f7480 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -49,6 +49,18 @@ export default defineConfig({ ]; }) ), + ...Object.fromEntries( + globSync( + path.resolve(__dirname, "src/tools/*/module.@(ts|js|tsx|jsx)") + ).map((path) => { + const toolId = path.split("/").slice(-2)[0]; + + return [ + `tool-${toolId}`, + fileURLToPath(new URL(path, import.meta.url)), + ]; + }) + ), }, output: { // We put index.css in dist instead of dist/assets so that we can link to fonts @@ -67,9 +79,15 @@ export default defineConfig({ return "[name].js"; // This will place service-worker.js directly under dist } - if (chunkInfo.name.startsWith("dataType-")) { + // output tools under "/tools" + if (chunkInfo.name.startsWith("tool-")) { const typeId = chunkInfo.name.split("-")[1]; + return `tools/${typeId}.js`; + } + // output datatypes under "/dataTypes" + if (chunkInfo.name.startsWith("dataType-")) { + const typeId = chunkInfo.name.split("-")[1]; return `dataTypes/${typeId}.js`; } From d4f2022f555dd16f10f9e84585d1d593e78393ac Mon Sep 17 00:00:00 2001 From: Paul Sonnentag Date: Fri, 31 May 2024 09:37:46 +0200 Subject: [PATCH 09/30] Add tool picker --- package.json | 1 + src/os/explorer/components/Explorer.tsx | 25 +++ src/os/explorer/components/Topbar.tsx | 180 ++++++++++-------- .../components/VersionControlEditor.tsx | 14 +- src/tools/confetti/Confetti.tsx | 19 ++ src/tools/confetti/module.ts | 12 ++ src/tools/confetti/tool.ts | 6 + yarn.lock | 5 + 8 files changed, 174 insertions(+), 88 deletions(-) create mode 100644 src/tools/confetti/Confetti.tsx create mode 100644 src/tools/confetti/module.ts create mode 100644 src/tools/confetti/tool.ts diff --git a/package.json b/package.json index 1dfe58a4..d1292cf2 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "@uiw/react-codemirror": "^4.21.24", "@xstate/react": "^4.1.1", "automerge-tldraw": "0.1.5", + "canvas-confetti": "^1.9.3", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "cmdk": "^0.2.0", diff --git a/src/os/explorer/components/Explorer.tsx b/src/os/explorer/components/Explorer.tsx index d86b291d..e01e3af2 100644 --- a/src/os/explorer/components/Explorer.tsx +++ b/src/os/explorer/components/Explorer.tsx @@ -26,6 +26,8 @@ import { DocLinkWithFolderPath, FolderDoc } from "@/datatypes/folder"; import { useSelectedDocLink } from "../hooks/useSelectedDocLink"; import { useSyncDocTitle } from "../hooks/useSyncDocTitle"; import { ErrorFallback } from "./ErrorFallback"; +import { Module, useModule } from "@/os/modules"; +import { ToolMetaData, Tool, useToolModulesForDataType } from "@/os/tools"; export const Explorer: React.FC = () => { const repo = useRepo(); @@ -50,6 +52,7 @@ export const Explorer: React.FC = () => { useDocument>(selectedDocUrl); const selectedDocName = selectedDocLink?.name; + const selectedDataType = selectedDocLink?.type; const selectedBranchUrl = selectedDocLink?.branchUrl; const selectedBranch = useMemo(() => { @@ -62,6 +65,24 @@ export const Explorer: React.FC = () => { ); }, [selectedBranchUrl, selectedDoc]); + const [selectedToolModuleId, setSelectedToolModuleId] = useState(); + const toolModules = useToolModulesForDataType(selectedDataType); + const selectedToolModule = toolModules.find( + (module) => module.metadata.id === selectedToolModuleId + ); + + const currentToolModule = + // make sure the current tool is reset to the fallback tool + // if the selected datatype changes and the selected tool is not compatible + selectedToolModule && + selectedToolModule.metadata.supportedDatatypes.some( + (supportedDataType) => supportedDataType === selectedDataType + ) + ? selectedToolModule + : toolModules[0]; + + const currentTool = useModule(currentToolModule); + const addNewDocument = useCallback( async ({ type }: { type: DatatypeId }) => { const datatypeModule = datatypeModules[type]; @@ -217,6 +238,9 @@ export const Explorer: React.FC = () => { selectedDocHandle={selectedDocHandle} removeDocLink={removeDocLink} addNewDocument={addNewDocument} + toolModuleId={currentToolModule.metadata.id} + setToolModuleId={setSelectedToolModuleId} + toolModules={toolModules} />
{!selectedDocUrl && ( @@ -245,6 +269,7 @@ export const Explorer: React.FC = () => { datatypeId={selectedDocLink?.type} docUrl={selectedDocUrl} key={selectedDocUrl} + tool={currentTool} selectedBranch={selectedBranch} setSelectedBranch={(branch) => { selectDocLink({ diff --git a/src/os/explorer/components/Topbar.tsx b/src/os/explorer/components/Topbar.tsx index c674086f..f90bf601 100644 --- a/src/os/explorer/components/Topbar.tsx +++ b/src/os/explorer/components/Topbar.tsx @@ -3,6 +3,7 @@ import { Doc, DocHandle, isValidAutomergeUrl } from "@automerge/automerge-repo"; import { useRepo } from "@automerge/automerge-repo-react-hooks"; import { Bot, + BotIcon, Download, EditIcon, GitForkIcon, @@ -34,6 +35,9 @@ import { getHeads } from "@automerge/automerge"; import { DatatypeId, useDataTypeModules } from "../../datatypes"; import { useDatatypeSettings, useRootFolderDocWithChildren } from "../account"; import { getUrlSafeName } from "../hooks/useSelectedDocLink"; +import { Module, useModule } from "@/os/modules"; +import { Tool, ToolMetaData, useToolModules } from "@/os/tools"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; type TopbarProps = { showSidebar: boolean; @@ -46,6 +50,9 @@ type TopbarProps = { | undefined; addNewDocument: (doc: { type: DatatypeId }) => void; removeDocLink: (link: DocLinkWithFolderPath) => void; + toolModules: Module[]; + toolModuleId: string; + setToolModuleId: (id: string) => void; }; export const Topbar: React.FC = ({ @@ -55,9 +62,13 @@ export const Topbar: React.FC = ({ selectedDocLink, selectedDoc, selectedDocHandle, + toolModules, + toolModuleId, + setToolModuleId, removeDocLink, }) => { const repo = useRepo(); + const { flatDocLinks } = useRootFolderDocWithChildren(); const datatypeSettings = useDatatypeSettings(); @@ -113,85 +124,25 @@ export const Topbar: React.FC = ({ )}
- {/* todo: the logic for running bots and when to show the menu should - probably live inside the bots directory -- - how do datatypes contribute things to the global topbar? */} - {selectedDocLink?.type === "essay" && ( -
- {isBotDatatypeEnabled && ( - - - - - - {botDocLinks.length === 0 && ( -
-
- No bots in sidebar.
- Click "New Bot" or get a share link from someone. -
-
- )} - {botDocLinks.map((botDocLink) => ( - { - const resultPromise = runBot({ - botDocUrl: botDocLink.url, - targetDocHandle: - selectedDocHandle as DocHandle, - repo, - }); - toast.promise(resultPromise, { - loading: `Running ${botDocLink.name}...`, - success: (result) => ( -
-
-
- {botDocLink.name} ran successfully. -
- -
-
- ), - error: `${botDocLink.name} failed, see console`, - }); - }} - > - Run {botDocLink.name} - { - selectDocLink({ - ...botDocLink, - type: "essay" as DatatypeId, - }); - e.stopPropagation(); - }} - /> -
- ))} -
-
- )} -
- )} -
+ + + {toolModules.map((module) => ( + + {module.metadata.name} + + ))} + + + +
= ({ />{" "} Make a copy - + {fileExportMethods.concat(genericExportMethods).map((method) => ( { @@ -288,6 +239,75 @@ export const Topbar: React.FC = ({ Export as {method.name} ))} + + {selectedDocLink?.type === "essay" && isBotDatatypeEnabled && ( + <> + {/* todo: the logic for running bots and when to show the menu should + probably live inside the bots directory -- + how do datatypes contribute things to the global topbar? */} + + + {botDocLinks.map((botDocLink) => ( + { + const resultPromise = runBot({ + botDocUrl: botDocLink.url, + targetDocHandle: + selectedDocHandle as DocHandle, + repo, + }); + toast.promise(resultPromise, { + loading: `Running ${botDocLink.name}...`, + success: (result) => ( +
+
+
+ {botDocLink.name} ran successfully. +
+ +
+
+ ), + error: `${botDocLink.name} failed, see console`, + }); + }} + > +
+ {" "} + Run {botDocLink.name} +
+ { + selectDocLink({ + ...botDocLink, + type: "essay" as DatatypeId, + }); + e.stopPropagation(); + }} + /> +
+ ))} + + )} + removeDocLink(selectedDocLink)}> void; }> = ({ docUrl: mainDocUrl, datatypeId, + tool, selectedBranch, setSelectedBranch, }) => { @@ -107,9 +108,7 @@ export const VersionControlEditor: React.FC<{ useHandle>(mainDocUrl); const account = useCurrentAccount(); const [sessionStartHeads, setSessionStartHeads] = useState(); - const [isCommentInputFocused, setIsCommentInputFocused] = useState(false); - const [isHoveringYankToBranchOption, setIsHoveringYankToBranchOption] = useState(false); const [showChangesFlag, setShowChangesFlag] = useState(true); @@ -552,6 +551,7 @@ export const VersionControlEditor: React.FC<{ key={mainDocUrl} mainDocUrl={mainDocUrl} datatypeId={datatypeId} + tool={tool} docUrl={selectedBranch.url} docHeads={docHeads} annotations={annotations} @@ -563,6 +563,7 @@ export const VersionControlEditor: React.FC<{ extends EditorProps { datatypeId: DatatypeId; + tool: Tool; } /* Wrapper component that dispatches to the tool for the doc type */ const DocEditor = ({ - datatypeId, + tool, docUrl, docHeads, annotations, @@ -658,10 +660,6 @@ const DocEditor = ({ setSelectedAnchors, setHoveredAnchor, }: EditorPropsWithDatatype) => { - const toolModules = useToolModulesForDataType(datatypeId); - // Currently we don't have a toolpicker so we just show the first tool for the doc type - const tool = useModule(toolModules[0]); - if (!tool) { return; } diff --git a/src/tools/confetti/Confetti.tsx b/src/tools/confetti/Confetti.tsx new file mode 100644 index 00000000..1392eb72 --- /dev/null +++ b/src/tools/confetti/Confetti.tsx @@ -0,0 +1,19 @@ +import confetti from "canvas-confetti"; +import { EditorProps } from "@/os/tools"; +import { Button } from "@/components/ui/button"; + +export const Confetti = ({}: EditorProps) => { + const onClick = () => { + confetti({ + particleCount: 100, + spread: 70, + origin: { y: 0.6 }, + }); + }; + + return ( +
+ +
+ ); +}; diff --git a/src/tools/confetti/module.ts b/src/tools/confetti/module.ts new file mode 100644 index 00000000..30f30a70 --- /dev/null +++ b/src/tools/confetti/module.ts @@ -0,0 +1,12 @@ +import { Module } from "@/os/modules"; +import { Tool, ToolMetaData } from "@/os/tools"; + +export default new Module({ + metadata: { + name: "Confetti", + id: "confetti", + supportedDatatypes: ["*"], + }, + + load: () => import("./tool").then(({ ConfettiTool }) => ConfettiTool), +}); diff --git a/src/tools/confetti/tool.ts b/src/tools/confetti/tool.ts new file mode 100644 index 00000000..3961e144 --- /dev/null +++ b/src/tools/confetti/tool.ts @@ -0,0 +1,6 @@ +import { Tool } from "@/os/tools"; +import { Confetti } from "./Confetti"; + +export const ConfettiTool: Tool = { + editorComponent: Confetti, +}; diff --git a/yarn.lock b/yarn.lock index 7a3353f2..60cc3144 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2913,6 +2913,11 @@ caniuse-lite@^1.0.30001587, caniuse-lite@^1.0.30001599: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001618.tgz#fad74fa006aef0f01e8e5c0a5540c74d8d36ec6f" integrity sha512-p407+D1tIkDvsEAPS22lJxLQQaG8OTBEqo0KhzfABGk0TU4juBNDSfH0hyAp/HRyx+M8L17z/ltyhxh27FTfQg== +canvas-confetti@^1.9.3: + version "1.9.3" + resolved "https://registry.yarnpkg.com/canvas-confetti/-/canvas-confetti-1.9.3.tgz#ef4c857420ad8045ab4abe8547261c8cdf229845" + integrity sha512-rFfTURMvmVEX1gyXFgn5QMn81bYk70qa0HLzcIOSVEyl57n6o9ItHeBtUSWdvKAPY0xlvBHno4/v3QPrT83q9g== + canvas-size@^1.2.6: version "1.2.6" resolved "https://registry.yarnpkg.com/canvas-size/-/canvas-size-1.2.6.tgz#1eaa6b56167cf2a70fa4021680829d2073b45706" From 32f87704c3ba64968cb2bf844b0dbed0c2c2297c Mon Sep 17 00:00:00 2001 From: Paul Sonnentag Date: Fri, 31 May 2024 11:19:35 +0200 Subject: [PATCH 10/30] Add dummy button to load modules --- src/os/explorer/components/AccountPicker.tsx | 20 +++++++++++++++++++- src/os/tools.ts | 2 +- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/os/explorer/components/AccountPicker.tsx b/src/os/explorer/components/AccountPicker.tsx index 8a7c0912..012e6c69 100644 --- a/src/os/explorer/components/AccountPicker.tsx +++ b/src/os/explorer/components/AccountPicker.tsx @@ -23,7 +23,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useDocument } from "@automerge/automerge-repo-react-hooks"; -import { Copy, Eye, EyeOff } from "lucide-react"; +import { Copy, Eye, EyeOff, PlusIcon } from "lucide-react"; import { Label } from "@/components/ui/label"; import { @@ -146,6 +146,14 @@ export const AccountPicker = ({ const isLoggedIn = self?.type === "registered"; + // load custom tools + const [moduleUrl, setModuleUrl] = useState(); + + const onAddModuleUrl = () => { + console.log("add", moduleUrl); + setModuleUrl(""); + }; + return ( @@ -365,6 +373,16 @@ export const AccountPicker = ({
); })} + +
+ setModuleUrl(evt.target.value)} + value={moduleUrl} + /> + +

diff --git a/src/os/tools.ts b/src/os/tools.ts index 29c13fc3..9c751ca8 100644 --- a/src/os/tools.ts +++ b/src/os/tools.ts @@ -81,6 +81,6 @@ export const useToolModulesForDataType = (dataTypeId: string) => { tool.metadata.supportedDatatypes.includes(dataTypeId) || tool.metadata.supportedDatatypes.includes("*") ), - [tools] + [tools, dataTypeId] ); }; From 24362ef9f7758447c34abea80ed65b489a0018e5 Mon Sep 17 00:00:00 2001 From: Paul Sonnentag Date: Fri, 31 May 2024 11:30:12 +0200 Subject: [PATCH 11/30] Rename datatypeSettings -> moduleSettings --- src/os/explorer/account.ts | 20 +++++++++++--------- src/os/explorer/components/AccountPicker.tsx | 4 ++-- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/os/explorer/account.ts b/src/os/explorer/account.ts index 12ba52ec..0d88f8c8 100644 --- a/src/os/explorer/account.ts +++ b/src/os/explorer/account.ts @@ -17,15 +17,16 @@ import { FolderDoc } from "@/datatypes/folder"; import { useFolderDocWithChildren } from "../../datatypes/folder/hooks/useFolderDocWithChildren"; import { DatatypeId } from "../datatypes"; -export type DatatypeSettingsDoc = { +export type ModuleSettingsDoc = { enabledDatatypeIds: { [id: DatatypeId]: boolean }; + moduleUrls: AutomergeUrl[]; }; export interface AccountDoc { contactUrl: AutomergeUrl; rootFolderUrl: AutomergeUrl; uiStateUrl: AutomergeUrl; - datatypeSettingsUrl: AutomergeUrl; + moduleSettingsUrl: AutomergeUrl; } export type UIStateDoc = { @@ -268,13 +269,14 @@ export function useCurrentAccount(): Account | undefined { }); } - if (doc && doc.datatypeSettingsUrl === undefined) { - const datatypeSettingsHandle = repo.create(); - datatypeSettingsHandle.change((settings) => { + if (doc && doc.moduleSettingsUrl === undefined) { + const moduleSettingsHandle = repo.create(); + moduleSettingsHandle.change((settings) => { settings.enabledDatatypeIds = {}; + settings.moduleUrls = []; }); account.handle.change((account) => { - account.datatypeSettingsUrl = datatypeSettingsHandle.url; + account.moduleSettingsUrl = moduleSettingsHandle.url; }); } }, [account?.handle.docSync()]); @@ -315,10 +317,10 @@ export function useSelf(): ContactDoc { return contactDoc; } -export const useDatatypeSettings = (): DatatypeSettingsDoc => { +export const useDatatypeSettings = (): ModuleSettingsDoc => { const [accountDoc] = useCurrentAccountDoc(); - const [datatypeSettingsDoc] = useDocument( - accountDoc?.datatypeSettingsUrl + const [datatypeSettingsDoc] = useDocument( + accountDoc?.moduleSettingsUrl ); return datatypeSettingsDoc; diff --git a/src/os/explorer/components/AccountPicker.tsx b/src/os/explorer/components/AccountPicker.tsx index 012e6c69..a14c2e11 100644 --- a/src/os/explorer/components/AccountPicker.tsx +++ b/src/os/explorer/components/AccountPicker.tsx @@ -5,7 +5,7 @@ import { useSelf, automergeUrlToAccountToken, accountTokenToAutomergeUrl, - DatatypeSettingsDoc, + ModuleSettingsDoc, } from "../account"; import { ChangeEvent, useEffect, useState } from "react"; @@ -76,7 +76,7 @@ export const AccountPicker = ({ const [accountToLogin] = useDocument(accountAutomergeUrlToLogin); const [contactToLogin] = useDocument(accountToLogin?.contactUrl); const [datatypeSettingsDoc, changeDatatypeSettingsDoc] = - useDocument(currentAccountDoc?.datatypeSettingsUrl); + useDocument(currentAccountDoc?.moduleSettingsUrl); const accountTokenToLoginStatus: AccountTokenToLoginStatus = (() => { if (!accountTokenToLogin || accountTokenToLogin === "") return null; From a28026e9e0b8a09901faee60437c2cf58f5b60de Mon Sep 17 00:00:00 2001 From: Paul Sonnentag Date: Fri, 31 May 2024 11:34:42 +0200 Subject: [PATCH 12/30] hide tool picker if there is just one tool --- src/os/explorer/components/Topbar.tsx | 38 ++++++++++++++------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/os/explorer/components/Topbar.tsx b/src/os/explorer/components/Topbar.tsx index f90bf601..2bd91a88 100644 --- a/src/os/explorer/components/Topbar.tsx +++ b/src/os/explorer/components/Topbar.tsx @@ -124,25 +124,27 @@ export const Topbar: React.FC = ({ )}

- - - {toolModules.map((module) => ( - - {module.metadata.name} - - ))} - - + {toolModules.length > 1 && ( + + + {toolModules.map((module) => ( + + {module.metadata.name} + + ))} + + + )} -
+
Date: Fri, 31 May 2024 12:14:23 +0200 Subject: [PATCH 13/30] Add cors headers for netlify deploy --- _headers | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 _headers diff --git a/_headers b/_headers new file mode 100644 index 00000000..c776a479 --- /dev/null +++ b/_headers @@ -0,0 +1,2 @@ +/* + Access-Control-Allow-Origin: * From a86e13db1465625625a1090a2a0f62ae012c82bf Mon Sep 17 00:00:00 2001 From: Paul Sonnentag Date: Fri, 31 May 2024 12:19:21 +0200 Subject: [PATCH 14/30] fix _headers file --- _headers => public/_headers | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename _headers => public/_headers (100%) diff --git a/_headers b/public/_headers similarity index 100% rename from _headers rename to public/_headers From 78412b8b94d189805d6bd18af458048c143e28f2 Mon Sep 17 00:00:00 2001 From: Paul Sonnentag Date: Fri, 31 May 2024 12:35:59 +0200 Subject: [PATCH 15/30] Allow to add modules --- src/os/explorer/account.ts | 2 +- src/os/explorer/components/AccountPicker.tsx | 67 +++++++++++++++----- src/os/explorer/components/Explorer.tsx | 2 +- src/os/tools.ts | 27 ++++++-- 4 files changed, 75 insertions(+), 23 deletions(-) diff --git a/src/os/explorer/account.ts b/src/os/explorer/account.ts index 0d88f8c8..7a0d647d 100644 --- a/src/os/explorer/account.ts +++ b/src/os/explorer/account.ts @@ -19,7 +19,7 @@ import { DatatypeId } from "../datatypes"; export type ModuleSettingsDoc = { enabledDatatypeIds: { [id: DatatypeId]: boolean }; - moduleUrls: AutomergeUrl[]; + moduleUrls: string[]; }; export interface AccountDoc { diff --git a/src/os/explorer/components/AccountPicker.tsx b/src/os/explorer/components/AccountPicker.tsx index a14c2e11..ab3e9f1e 100644 --- a/src/os/explorer/components/AccountPicker.tsx +++ b/src/os/explorer/components/AccountPicker.tsx @@ -75,7 +75,7 @@ export const AccountPicker = ({ ); const [accountToLogin] = useDocument(accountAutomergeUrlToLogin); const [contactToLogin] = useDocument(accountToLogin?.contactUrl); - const [datatypeSettingsDoc, changeDatatypeSettingsDoc] = + const [moduleSettingsDoc, changeModuleSettingsDoc] = useDocument(currentAccountDoc?.moduleSettingsUrl); const accountTokenToLoginStatus: AccountTokenToLoginStatus = (() => { @@ -147,11 +147,36 @@ export const AccountPicker = ({ const isLoggedIn = self?.type === "registered"; // load custom tools - const [moduleUrl, setModuleUrl] = useState(); + const [moduleUrl, setModuleUrl] = useState(""); const onAddModuleUrl = () => { - console.log("add", moduleUrl); - setModuleUrl(""); + // validate url + try { + new URL(moduleUrl); + } catch (err) { + alert(`Invalid url "${moduleUrl}}"`); + return; + } + + import(moduleUrl) + .then((module) => { + if ( + !module.default || + !module.default.metadata || + !module.default.load + ) { + alert("Invalid module"); + return; + } + + changeModuleSettingsDoc((doc) => { + doc.moduleUrls.push(moduleUrl); + setModuleUrl(""); + }); + }) + .catch((err) => { + alert(`Failed to load module at "${moduleUrl}"`); + }); }; return ( @@ -330,10 +355,10 @@ export const AccountPicker = ({
- {datatypeSettingsDoc && + {moduleSettingsDoc && Object.values(datatypeModules).map((datatypeModule) => { const isEnabled = - datatypeSettingsDoc.enabledDatatypeIds[ + moduleSettingsDoc.enabledDatatypeIds[ datatypeModule.metadata.id ]; @@ -352,7 +377,7 @@ export const AccountPicker = ({ checked={isChecked} onClick={(e) => e.stopPropagation()} onCheckedChange={() => { - changeDatatypeSettingsDoc((settings) => { + changeModuleSettingsDoc((settings) => { settings.enabledDatatypeIds[ datatypeModule.metadata.id ] = !isChecked; @@ -373,16 +398,6 @@ export const AccountPicker = ({
); })} - -
- setModuleUrl(evt.target.value)} - value={moduleUrl} - /> - -

@@ -390,6 +405,24 @@ export const AccountPicker = ({ to break!

+ +
+ + + {moduleSettingsDoc?.moduleUrls.map((url) => { + return
{url}
; + })} + +
+ setModuleUrl(evt.target.value)} + value={moduleUrl} + /> + +
+
)} diff --git a/src/os/explorer/components/Explorer.tsx b/src/os/explorer/components/Explorer.tsx index e01e3af2..cb1ca045 100644 --- a/src/os/explorer/components/Explorer.tsx +++ b/src/os/explorer/components/Explorer.tsx @@ -238,7 +238,7 @@ export const Explorer: React.FC = () => { selectedDocHandle={selectedDocHandle} removeDocLink={removeDocLink} addNewDocument={addNewDocument} - toolModuleId={currentToolModule.metadata.id} + toolModuleId={currentToolModule?.metadata.id} setToolModuleId={setSelectedToolModuleId} toolModules={toolModules} /> diff --git a/src/os/tools.ts b/src/os/tools.ts index 9c751ca8..e8199fc2 100644 --- a/src/os/tools.ts +++ b/src/os/tools.ts @@ -1,13 +1,18 @@ import * as A from "@automerge/automerge/next"; -import React, { useMemo } from "react"; +import React, { useMemo, useState } from "react"; -import { AutomergeUrl } from "@automerge/automerge-repo"; import { Annotation, + AnnotationWithUIState, HasVersionControlMetadata, } from "@/os/versionControl/schema"; -import { AnnotationWithUIState } from "@/os/versionControl/schema"; -import { DocHandle } from "@automerge/automerge-repo"; +import { AutomergeUrl, DocHandle } from "@automerge/automerge-repo"; +import { useDocument } from "@automerge/automerge-repo-react-hooks"; +import { + AccountDoc, + ModuleSettingsDoc, + useCurrentAccount, +} from "./explorer/account"; import { Module } from "./modules"; export type ToolMetaData = { @@ -69,6 +74,20 @@ for (const [path, { default: module }] of Object.entries(toolsFolder)) { export const useToolModules = () => { return TOOLS; + + const account = useCurrentAccount(); + const [accountDoc] = useDocument(account.handle.url); + const [moduleSettingsDoc] = useDocument( + accountDoc.moduleSettingsUrl + ); + + const [dynamicallyLoadedModules, setDynamicallyLoadedModules] = useState< + Module[] + >([]); + + moduleSettingsDoc.moduleUrls; + + return TOOLS.concat(dynamicallyLoadedModules); }; export const useToolModulesForDataType = (dataTypeId: string) => { From e663f53c5b43205d404f77a6405886040e9a752c Mon Sep 17 00:00:00 2001 From: Paul Sonnentag Date: Fri, 31 May 2024 13:05:02 +0200 Subject: [PATCH 16/30] load dynamic modules --- src/os/explorer/components/Explorer.tsx | 6 ++-- src/os/tools.ts | 37 +++++++++++++------- src/tools/confetti/{module.ts => module_.ts} | 0 3 files changed, 28 insertions(+), 15 deletions(-) rename src/tools/confetti/{module.ts => module_.ts} (100%) diff --git a/src/os/explorer/components/Explorer.tsx b/src/os/explorer/components/Explorer.tsx index cb1ca045..b7243d02 100644 --- a/src/os/explorer/components/Explorer.tsx +++ b/src/os/explorer/components/Explorer.tsx @@ -66,6 +66,7 @@ export const Explorer: React.FC = () => { }, [selectedBranchUrl, selectedDoc]); const [selectedToolModuleId, setSelectedToolModuleId] = useState(); + const toolModules = useToolModulesForDataType(selectedDataType); const selectedToolModule = toolModules.find( (module) => module.metadata.id === selectedToolModuleId @@ -76,7 +77,8 @@ export const Explorer: React.FC = () => { // if the selected datatype changes and the selected tool is not compatible selectedToolModule && selectedToolModule.metadata.supportedDatatypes.some( - (supportedDataType) => supportedDataType === selectedDataType + (supportedDataType) => + supportedDataType === selectedDataType || supportedDataType === "*" ) ? selectedToolModule : toolModules[0]; @@ -264,7 +266,7 @@ export const Explorer: React.FC = () => { {/* NOTE: we set the URL as the component key, to force re-mount on URL change. If we want more continuity we could not do this. */} - {selectedDocUrl && selectedDoc && ( + {selectedDocUrl && selectedDoc && currentTool && ( { - return TOOLS; - const account = useCurrentAccount(); - const [accountDoc] = useDocument(account.handle.url); + const [accountDoc] = useDocument(account?.handle.url); const [moduleSettingsDoc] = useDocument( - accountDoc.moduleSettingsUrl + accountDoc?.moduleSettingsUrl ); - const [dynamicallyLoadedModules, setDynamicallyLoadedModules] = useState< - Module[] - >([]); + const [dynamicModules, setDynamicModules] = useState([]); + + const dynamicModuleUrls = moduleSettingsDoc?.moduleUrls ?? []; + const dynamicModuleUrlsRef = useRef(); + dynamicModuleUrlsRef.current = dynamicModuleUrls; + + useEffect(() => { + Promise.all( + dynamicModuleUrls.map(async (url) => (await import(url)).default) + ).then((modules) => { + // do nothing if dynamicModuleUrls has changed in the meantime + if (dynamicModuleUrls !== dynamicModuleUrlsRef.current) { + return; + } - moduleSettingsDoc.moduleUrls; + setDynamicModules(modules); + }); + }, [dynamicModuleUrls]); - return TOOLS.concat(dynamicallyLoadedModules); + return TOOLS.concat(dynamicModules); }; export const useToolModulesForDataType = (dataTypeId: string) => { - const tools = useToolModules(); + const toolModules = useToolModules(); return useMemo( () => - tools.filter( + toolModules.filter( (tool) => tool.metadata.supportedDatatypes.includes(dataTypeId) || tool.metadata.supportedDatatypes.includes("*") ), - [tools, dataTypeId] + [toolModules, dataTypeId] ); }; diff --git a/src/tools/confetti/module.ts b/src/tools/confetti/module_.ts similarity index 100% rename from src/tools/confetti/module.ts rename to src/tools/confetti/module_.ts From 0bee0e0689c862aa130eee6251b93d5d641275ec Mon Sep 17 00:00:00 2001 From: Paul Sonnentag Date: Fri, 31 May 2024 14:43:20 +0200 Subject: [PATCH 17/30] use automerge in data type --- src/tools/confetti/Confetti.tsx | 10 ++++++++-- src/tools/confetti/{module_.ts => module.ts} | 0 2 files changed, 8 insertions(+), 2 deletions(-) rename src/tools/confetti/{module_.ts => module.ts} (100%) diff --git a/src/tools/confetti/Confetti.tsx b/src/tools/confetti/Confetti.tsx index 1392eb72..7e163a48 100644 --- a/src/tools/confetti/Confetti.tsx +++ b/src/tools/confetti/Confetti.tsx @@ -1,8 +1,11 @@ import confetti from "canvas-confetti"; import { EditorProps } from "@/os/tools"; import { Button } from "@/components/ui/button"; +import { useDocument } from "@automerge/automerge-repo-react-hooks"; + +export const Confetti = ({ docUrl }: EditorProps) => { + const [doc] = useDocument(docUrl); -export const Confetti = ({}: EditorProps) => { const onClick = () => { confetti({ particleCount: 100, @@ -13,7 +16,10 @@ export const Confetti = ({}: EditorProps) => { return (
- +
+ +
+
{JSON.stringify(doc, null, 2)}
); }; diff --git a/src/tools/confetti/module_.ts b/src/tools/confetti/module.ts similarity index 100% rename from src/tools/confetti/module_.ts rename to src/tools/confetti/module.ts From 8222be423b688b574267a768d40132f94a68d7c3 Mon Sep 17 00:00:00 2001 From: Paul Sonnentag Date: Fri, 31 May 2024 15:39:51 +0200 Subject: [PATCH 18/30] load automerge-repo and react through import map - doens't work with service worker yet --- index.html | 26 ++++++++++++++++++++++++++ src/os/main.tsx | 20 ++++++++++---------- vite.config.ts | 5 +++++ 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/index.html b/index.html index dfa13bf2..f4dbbc70 100644 --- a/index.html +++ b/index.html @@ -20,6 +20,32 @@
+ + + + diff --git a/src/os/main.tsx b/src/os/main.tsx index 7c976d2d..78db4a44 100644 --- a/src/os/main.tsx +++ b/src/os/main.tsx @@ -18,25 +18,25 @@ import { getAccount } from "./explorer/account.js"; import { Explorer } from "./explorer/components/Explorer.js"; import "./index.css"; -const serviceWorker = await setupServiceWorker(); +//const serviceWorker = await setupServiceWorker(); // Service workers stop on their own, which breaks sync. // Here we ping the service worker while the tab is running // to keep it alive (and make it restart if it did stop.) -setInterval(() => { +/*setInterval(() => { serviceWorker.postMessage({ type: "PING" }); -}, 5000); +}, 5000);*/ // This case should never happen // if the service worker is not defined here either the initialization failed // or we found a new case that we haven't considered yet -if (!serviceWorker) { +/*if (!serviceWorker) { throw new Error("Failed to setup service worker"); -} +}*/ const repo = await setupRepo(); -establishMessageChannel(serviceWorker); +//establishMessageChannel(serviceWorker); async function setupServiceWorker(): Promise { return navigator.serviceWorker @@ -84,7 +84,7 @@ async function setupRepo() { } // Re-establish the MessageChannel if the controlling service worker changes. -navigator.serviceWorker.addEventListener("controllerchange", (event) => { +/* navigator.serviceWorker.addEventListener("controllerchange", (event) => { const newServiceWorker = (event.target as ServiceWorkerContainer).controller; // controllerchange is fired after a new service worker is installed // even if we wait above in setupServiceWorker() until the service worker state changes to activated. @@ -92,15 +92,15 @@ navigator.serviceWorker.addEventListener("controllerchange", (event) => { if (newServiceWorker !== serviceWorker) { establishMessageChannel(newServiceWorker); } -}); +}); */ // Re-establish the MessageChannel if the service worker restarts -navigator.serviceWorker.addEventListener("message", (event) => { +/* navigator.serviceWorker.addEventListener("message", (event) => { if (event.data.type === "SERVICE_WORKER_RESTARTED") { console.log("Service worker restarted, establishing message channel"); establishMessageChannel(serviceWorker); } -}); +}); */ // Connects the repo in the tab with the repo in the service worker through a message channel. // The repo in the tab takes advantage of loaded state in the SW. diff --git a/vite.config.ts b/vite.config.ts index 4b3f7480..9a2821d0 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -34,6 +34,11 @@ export default defineConfig({ }, build: { rollupOptions: { + external: [ + "@automerge/automerge", + "@automerge/automerge-repo-react-hooks", + "react", + ], input: { main: path.resolve(__dirname, "index.html"), "service-worker": path.resolve(__dirname, "service-worker.js"), From 1ae911a9ee483a1b02c0c037d5ed13e1a6db926b Mon Sep 17 00:00:00 2001 From: Paul Sonnentag Date: Fri, 31 May 2024 15:57:17 +0200 Subject: [PATCH 19/30] Delete confetti tool --- src/tools/confetti/Confetti.tsx | 25 ------------------------- src/tools/confetti/module.ts | 12 ------------ src/tools/confetti/tool.ts | 6 ------ 3 files changed, 43 deletions(-) delete mode 100644 src/tools/confetti/Confetti.tsx delete mode 100644 src/tools/confetti/module.ts delete mode 100644 src/tools/confetti/tool.ts diff --git a/src/tools/confetti/Confetti.tsx b/src/tools/confetti/Confetti.tsx deleted file mode 100644 index 7e163a48..00000000 --- a/src/tools/confetti/Confetti.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import confetti from "canvas-confetti"; -import { EditorProps } from "@/os/tools"; -import { Button } from "@/components/ui/button"; -import { useDocument } from "@automerge/automerge-repo-react-hooks"; - -export const Confetti = ({ docUrl }: EditorProps) => { - const [doc] = useDocument(docUrl); - - const onClick = () => { - confetti({ - particleCount: 100, - spread: 70, - origin: { y: 0.6 }, - }); - }; - - return ( -
-
- -
-
{JSON.stringify(doc, null, 2)}
-
- ); -}; diff --git a/src/tools/confetti/module.ts b/src/tools/confetti/module.ts deleted file mode 100644 index 30f30a70..00000000 --- a/src/tools/confetti/module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Module } from "@/os/modules"; -import { Tool, ToolMetaData } from "@/os/tools"; - -export default new Module({ - metadata: { - name: "Confetti", - id: "confetti", - supportedDatatypes: ["*"], - }, - - load: () => import("./tool").then(({ ConfettiTool }) => ConfettiTool), -}); diff --git a/src/tools/confetti/tool.ts b/src/tools/confetti/tool.ts deleted file mode 100644 index 3961e144..00000000 --- a/src/tools/confetti/tool.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Tool } from "@/os/tools"; -import { Confetti } from "./Confetti"; - -export const ConfettiTool: Tool = { - editorComponent: Confetti, -}; From be98d1d730c22835cc64dd0e47617eadb25048da Mon Sep 17 00:00:00 2001 From: Paul Sonnentag Date: Fri, 31 May 2024 15:59:09 +0200 Subject: [PATCH 20/30] hide tool picker if no document is selected --- src/os/explorer/components/Topbar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/os/explorer/components/Topbar.tsx b/src/os/explorer/components/Topbar.tsx index 2bd91a88..2a2ddd7a 100644 --- a/src/os/explorer/components/Topbar.tsx +++ b/src/os/explorer/components/Topbar.tsx @@ -124,7 +124,7 @@ export const Topbar: React.FC = ({ )}
- {toolModules.length > 1 && ( + {toolModules.length > 1 && selectedDocLink && ( Date: Mon, 3 Jun 2024 08:28:33 +0200 Subject: [PATCH 21/30] Replace url input with prompt --- index.html | 1 - src/os/explorer/components/AccountPicker.tsx | 57 +++++++++++++++----- 2 files changed, 43 insertions(+), 15 deletions(-) diff --git a/index.html b/index.html index f4dbbc70..f1bc1323 100644 --- a/index.html +++ b/index.html @@ -45,7 +45,6 @@ src="https://ga.jspm.io/npm:es-module-shims@1.10.0/dist/es-module-shims.js" crossorigin="anonymous" > - diff --git a/src/os/explorer/components/AccountPicker.tsx b/src/os/explorer/components/AccountPicker.tsx index ab3e9f1e..3ccc9804 100644 --- a/src/os/explorer/components/AccountPicker.tsx +++ b/src/os/explorer/components/AccountPicker.tsx @@ -23,7 +23,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useDocument } from "@automerge/automerge-repo-react-hooks"; -import { Copy, Eye, EyeOff, PlusIcon } from "lucide-react"; +import { Copy, Eye, EyeOff, PlusIcon, XIcon } from "lucide-react"; import { Label } from "@/components/ui/label"; import { @@ -146,10 +146,13 @@ export const AccountPicker = ({ const isLoggedIn = self?.type === "registered"; - // load custom tools - const [moduleUrl, setModuleUrl] = useState(""); - const onAddModuleUrl = () => { + const moduleUrl = prompt("Enter a module url"); + + if (!moduleUrl) { + return; + } + // validate url try { new URL(moduleUrl); @@ -171,7 +174,6 @@ export const AccountPicker = ({ changeModuleSettingsDoc((doc) => { doc.moduleUrls.push(moduleUrl); - setModuleUrl(""); }); }) .catch((err) => { @@ -179,6 +181,12 @@ export const AccountPicker = ({ }); }; + const onDeleteModuleUrlAtIndex = (index) => { + changeModuleSettingsDoc((doc) => { + delete doc.moduleUrls[index]; + }); + }; + return ( @@ -406,19 +414,40 @@ export const AccountPicker = ({

-
+
- {moduleSettingsDoc?.moduleUrls.map((url) => { - return
{url}
; + {moduleSettingsDoc?.moduleUrls.map((url, index) => { + const name = new URL(url).pathname.split("/").slice(-1)[0]; + + return ( +
+ + + +
+ ); })} -
- setModuleUrl(evt.target.value)} - value={moduleUrl} - /> -
From 7487d9ab9185435a40ef8c9ace3f6883179ea742 Mon Sep 17 00:00:00 2001 From: Paul Sonnentag Date: Thu, 6 Jun 2024 13:20:43 +0200 Subject: [PATCH 22/30] enable service worker again --- src/os/main.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/os/main.tsx b/src/os/main.tsx index 78db4a44..e3ff4c78 100644 --- a/src/os/main.tsx +++ b/src/os/main.tsx @@ -18,25 +18,25 @@ import { getAccount } from "./explorer/account.js"; import { Explorer } from "./explorer/components/Explorer.js"; import "./index.css"; -//const serviceWorker = await setupServiceWorker(); +const serviceWorker = await setupServiceWorker(); // Service workers stop on their own, which breaks sync. // Here we ping the service worker while the tab is running // to keep it alive (and make it restart if it did stop.) -/*setInterval(() => { +setInterval(() => { serviceWorker.postMessage({ type: "PING" }); -}, 5000);*/ +}, 5000); // This case should never happen // if the service worker is not defined here either the initialization failed // or we found a new case that we haven't considered yet -/*if (!serviceWorker) { +if (!serviceWorker) { throw new Error("Failed to setup service worker"); -}*/ +} const repo = await setupRepo(); -//establishMessageChannel(serviceWorker); +establishMessageChannel(serviceWorker); async function setupServiceWorker(): Promise { return navigator.serviceWorker @@ -84,7 +84,7 @@ async function setupRepo() { } // Re-establish the MessageChannel if the controlling service worker changes. -/* navigator.serviceWorker.addEventListener("controllerchange", (event) => { +navigator.serviceWorker.addEventListener("controllerchange", (event) => { const newServiceWorker = (event.target as ServiceWorkerContainer).controller; // controllerchange is fired after a new service worker is installed // even if we wait above in setupServiceWorker() until the service worker state changes to activated. @@ -92,7 +92,7 @@ async function setupRepo() { if (newServiceWorker !== serviceWorker) { establishMessageChannel(newServiceWorker); } -}); */ +}); // Re-establish the MessageChannel if the service worker restarts /* navigator.serviceWorker.addEventListener("message", (event) => { From 82b68e34f265c0f2373f411fe88c88390ec1758c Mon Sep 17 00:00:00 2001 From: Paul Sonnentag Date: Thu, 6 Jun 2024 13:27:46 +0200 Subject: [PATCH 23/30] remove unused dependencies --- package.json | 1 - yarn.lock | 5 ----- 2 files changed, 6 deletions(-) diff --git a/package.json b/package.json index d1292cf2..1dfe58a4 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,6 @@ "@uiw/react-codemirror": "^4.21.24", "@xstate/react": "^4.1.1", "automerge-tldraw": "0.1.5", - "canvas-confetti": "^1.9.3", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "cmdk": "^0.2.0", diff --git a/yarn.lock b/yarn.lock index 60cc3144..7a3353f2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2913,11 +2913,6 @@ caniuse-lite@^1.0.30001587, caniuse-lite@^1.0.30001599: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001618.tgz#fad74fa006aef0f01e8e5c0a5540c74d8d36ec6f" integrity sha512-p407+D1tIkDvsEAPS22lJxLQQaG8OTBEqo0KhzfABGk0TU4juBNDSfH0hyAp/HRyx+M8L17z/ltyhxh27FTfQg== -canvas-confetti@^1.9.3: - version "1.9.3" - resolved "https://registry.yarnpkg.com/canvas-confetti/-/canvas-confetti-1.9.3.tgz#ef4c857420ad8045ab4abe8547261c8cdf229845" - integrity sha512-rFfTURMvmVEX1gyXFgn5QMn81bYk70qa0HLzcIOSVEyl57n6o9ItHeBtUSWdvKAPY0xlvBHno4/v3QPrT83q9g== - canvas-size@^1.2.6: version "1.2.6" resolved "https://registry.yarnpkg.com/canvas-size/-/canvas-size-1.2.6.tgz#1eaa6b56167cf2a70fa4021680829d2073b45706" From 301622424e85800c58d880e6ce0246c661e995a7 Mon Sep 17 00:00:00 2001 From: Paul Sonnentag Date: Thu, 6 Jun 2024 14:09:01 +0200 Subject: [PATCH 24/30] add module data type --- src/datatypes/module/datatype.ts | 29 +++++++ src/datatypes/module/index.ts | 4 + src/datatypes/module/module.ts | 18 +++++ src/datatypes/module/schema.ts | 6 ++ src/os/explorer/account.ts | 2 - src/os/explorer/components/AccountPicker.tsx | 80 -------------------- src/os/tools.ts | 10 +-- src/tools/folder/FolderViewer.tsx | 76 ------------------- src/tools/folder/tool.ts | 6 -- src/tools/module/ModuleEditor.tsx | 35 +++++++++ src/tools/{folder => module}/module.ts | 6 +- src/tools/module/tool.ts | 6 ++ 12 files changed, 103 insertions(+), 175 deletions(-) create mode 100644 src/datatypes/module/datatype.ts create mode 100644 src/datatypes/module/index.ts create mode 100644 src/datatypes/module/module.ts create mode 100644 src/datatypes/module/schema.ts delete mode 100644 src/tools/folder/FolderViewer.tsx delete mode 100644 src/tools/folder/tool.ts create mode 100644 src/tools/module/ModuleEditor.tsx rename src/tools/{folder => module}/module.ts (76%) create mode 100644 src/tools/module/tool.ts diff --git a/src/datatypes/module/datatype.ts b/src/datatypes/module/datatype.ts new file mode 100644 index 00000000..1e41a4cc --- /dev/null +++ b/src/datatypes/module/datatype.ts @@ -0,0 +1,29 @@ +import { DataTypeWitoutMetaData } from "@/os/datatypes"; +import { ModuleDoc } from "./schema"; + +export const init = (doc: any) => { + doc.title = "Untitled Module"; + doc.docs = []; +}; + +// When a copy of the document has been made, +// update the title so it's more clear which one is the copy vs original. +// TODO: generalize this to a HasTitle schema? +export const markCopy = (doc: ModuleDoc) => { + doc.title = `Copy of ${doc.title}`; +}; + +export const getTitle = async (doc: any) => { + return doc.title; +}; + +export const setTitle = (doc: ModuleDoc, title: string) => { + doc.title = title; +}; + +export const ModuleDataType: DataTypeWitoutMetaData = { + init, + getTitle, + setTitle, + markCopy, +}; diff --git a/src/datatypes/module/index.ts b/src/datatypes/module/index.ts new file mode 100644 index 00000000..945d97c2 --- /dev/null +++ b/src/datatypes/module/index.ts @@ -0,0 +1,4 @@ +import { ModuleDataType } from "./datatype"; +export default ModuleDataType; + +export * from "./schema"; diff --git a/src/datatypes/module/module.ts b/src/datatypes/module/module.ts new file mode 100644 index 00000000..787a9979 --- /dev/null +++ b/src/datatypes/module/module.ts @@ -0,0 +1,18 @@ +import { DataTypeMetadata, DataTypeWitoutMetaData } from "@/os/datatypes"; +import { Package } from "lucide-react"; +import { ModuleDoc } from "./schema"; +import { Module } from "@/os/modules"; + +export default new Module< + DataTypeMetadata, + DataTypeWitoutMetaData +>({ + metadata: { + id: "module", + name: "Module", + icon: Package, + isExperimental: true, + }, + + load: () => import("./datatype").then(({ ModuleDataType }) => ModuleDataType), +}); diff --git a/src/datatypes/module/schema.ts b/src/datatypes/module/schema.ts new file mode 100644 index 00000000..509fc834 --- /dev/null +++ b/src/datatypes/module/schema.ts @@ -0,0 +1,6 @@ +import { AutomergeUrl } from "@automerge/automerge-repo"; + +export type ModuleDoc = { + title: string; + url: AutomergeUrl; +}; diff --git a/src/os/explorer/account.ts b/src/os/explorer/account.ts index 7a0d647d..296ba413 100644 --- a/src/os/explorer/account.ts +++ b/src/os/explorer/account.ts @@ -19,7 +19,6 @@ import { DatatypeId } from "../datatypes"; export type ModuleSettingsDoc = { enabledDatatypeIds: { [id: DatatypeId]: boolean }; - moduleUrls: string[]; }; export interface AccountDoc { @@ -273,7 +272,6 @@ export function useCurrentAccount(): Account | undefined { const moduleSettingsHandle = repo.create(); moduleSettingsHandle.change((settings) => { settings.enabledDatatypeIds = {}; - settings.moduleUrls = []; }); account.handle.change((account) => { account.moduleSettingsUrl = moduleSettingsHandle.url; diff --git a/src/os/explorer/components/AccountPicker.tsx b/src/os/explorer/components/AccountPicker.tsx index 3ccc9804..1871a8a2 100644 --- a/src/os/explorer/components/AccountPicker.tsx +++ b/src/os/explorer/components/AccountPicker.tsx @@ -146,47 +146,6 @@ export const AccountPicker = ({ const isLoggedIn = self?.type === "registered"; - const onAddModuleUrl = () => { - const moduleUrl = prompt("Enter a module url"); - - if (!moduleUrl) { - return; - } - - // validate url - try { - new URL(moduleUrl); - } catch (err) { - alert(`Invalid url "${moduleUrl}}"`); - return; - } - - import(moduleUrl) - .then((module) => { - if ( - !module.default || - !module.default.metadata || - !module.default.load - ) { - alert("Invalid module"); - return; - } - - changeModuleSettingsDoc((doc) => { - doc.moduleUrls.push(moduleUrl); - }); - }) - .catch((err) => { - alert(`Failed to load module at "${moduleUrl}"`); - }); - }; - - const onDeleteModuleUrlAtIndex = (index) => { - changeModuleSettingsDoc((doc) => { - delete doc.moduleUrls[index]; - }); - }; - return ( @@ -413,45 +372,6 @@ export const AccountPicker = ({ to break!

- -
- - - {moduleSettingsDoc?.moduleUrls.map((url, index) => { - const name = new URL(url).pathname.split("/").slice(-1)[0]; - - return ( -
- - - -
- ); - })} - -
- -
-
)} diff --git a/src/os/tools.ts b/src/os/tools.ts index 4996cdaf..e41a2bf1 100644 --- a/src/os/tools.ts +++ b/src/os/tools.ts @@ -73,15 +73,9 @@ for (const [path, { default: module }] of Object.entries(toolsFolder)) { } export const useToolModules = () => { - const account = useCurrentAccount(); - const [accountDoc] = useDocument(account?.handle.url); - const [moduleSettingsDoc] = useDocument( - accountDoc?.moduleSettingsUrl - ); - const [dynamicModules, setDynamicModules] = useState([]); - const dynamicModuleUrls = moduleSettingsDoc?.moduleUrls ?? []; + /* const dynamicModuleUrls = []; const dynamicModuleUrlsRef = useRef(); dynamicModuleUrlsRef.current = dynamicModuleUrls; @@ -96,7 +90,7 @@ export const useToolModules = () => { setDynamicModules(modules); }); - }, [dynamicModuleUrls]); + }, [dynamicModuleUrls]); */ return TOOLS.concat(dynamicModules); }; diff --git a/src/tools/folder/FolderViewer.tsx b/src/tools/folder/FolderViewer.tsx deleted file mode 100644 index 983ec3be..00000000 --- a/src/tools/folder/FolderViewer.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { useDocument } from "@automerge/automerge-repo-react-hooks"; -import * as A from "@automerge/automerge/next"; -import React from "react"; - -import { EditorProps, useToolModulesForDataType } from "@/os/tools"; -import { DocLink, FolderDoc } from "@/datatypes/folder"; -import { selectDocLink } from "@/os/explorer/hooks/useSelectedDocLink"; -import { useDataTypeModules } from "@/os/datatypes"; -import { useModule } from "@/os/modules"; - -export const FolderViewer: React.FC> = ({ - docUrl, - docHeads, -}: EditorProps) => { - const [folder] = useDocument(docUrl); // used to trigger re-rendering when the doc loads - - const folderAtHeads = docHeads ? A.view(folder, docHeads) : folder; - - if (!folder) { - return null; - } - - return ( -
-
- {folderAtHeads.docs.length} documents -
-
- {folderAtHeads.docs.map((docLink) => ( - - ))} -
-
- ); -}; - -interface DocLinkPreviewProps { - docLink: DocLink; -} - -const DocLinkPreview: React.FC = ({ docLink }) => { - const dataTypeModules = useDataTypeModules(); - // we currently don't have a tool picker so we just use the first tool - const toolModules = useToolModulesForDataType(docLink.type); - const tool = useModule(toolModules[0]); - - const Tool = tool?.editorComponent; - - const Icon = dataTypeModules[docLink.type].metadata.icon; - - return ( -
-
- -
{docLink.name}
- -
-
- {toolModules.length === 0 &&
No editor available
} - {Tool && docLink.type !== "folder" && } - {docLink.type === "folder" && ( -
- Click "open" to see nested folder contents -
- )} -
-
- ); -}; diff --git a/src/tools/folder/tool.ts b/src/tools/folder/tool.ts deleted file mode 100644 index 05eeefc3..00000000 --- a/src/tools/folder/tool.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Tool } from "@/os/tools"; -import { FolderViewer } from "./FolderViewer"; - -export const FolderViewerTool: Tool = { - editorComponent: FolderViewer, -}; diff --git a/src/tools/module/ModuleEditor.tsx b/src/tools/module/ModuleEditor.tsx new file mode 100644 index 00000000..f71bfdc8 --- /dev/null +++ b/src/tools/module/ModuleEditor.tsx @@ -0,0 +1,35 @@ +import { useDocument } from "@automerge/automerge-repo-react-hooks"; +import * as A from "@automerge/automerge/next"; +import React from "react"; + +import { ModuleDoc } from "@/datatypes/module"; +import { EditorProps } from "@/os/tools"; +import { Input } from "@/components/ui/input"; + +export const ModuleEditor: React.FC> = ({ + docUrl, + docHeads, +}: EditorProps) => { + const [moduleDoc, changeModuleDoc] = useDocument(docUrl); + + const moduleAtHeads = docHeads ? A.view(moduleDoc, docHeads) : moduleDoc; + + if (!moduleDoc) { + return null; + } + + const onChangeUrlInput = (evt) => { + changeModuleDoc((doc) => { + doc.url = evt.target.value; + }); + }; + + return ( +
+
+
URL
+ +
+
+ ); +}; diff --git a/src/tools/folder/module.ts b/src/tools/module/module.ts similarity index 76% rename from src/tools/folder/module.ts rename to src/tools/module/module.ts index a756e72f..1e465890 100644 --- a/src/tools/folder/module.ts +++ b/src/tools/module/module.ts @@ -3,9 +3,9 @@ import { Tool, ToolMetaData } from "@/os/tools"; export default new Module({ metadata: { - id: "folder", - name: "Folder", - supportedDatatypes: ["folder"], + id: "module", + name: "Module", + supportedDatatypes: ["module"], }, load: () => import("./tool").then(({ FolderViewerTool }) => FolderViewerTool), diff --git a/src/tools/module/tool.ts b/src/tools/module/tool.ts new file mode 100644 index 00000000..c094b3eb --- /dev/null +++ b/src/tools/module/tool.ts @@ -0,0 +1,6 @@ +import { Tool } from "@/os/tools"; +import { ModuleEditor } from "./ModuleEditor"; + +export const FolderViewerTool: Tool = { + editorComponent: ModuleEditor, +}; From 3cba60bcc510096feec621fc3ef469fab8920d1a Mon Sep 17 00:00:00 2001 From: Paul Sonnentag Date: Thu, 6 Jun 2024 14:46:33 +0200 Subject: [PATCH 25/30] wip: load module documents in root folder --- index.html | 2 +- src/os/modules.ts | 19 ++++++++++++------- src/os/tools.ts | 36 +++++++++++++++++++++++------------- 3 files changed, 36 insertions(+), 21 deletions(-) diff --git a/index.html b/index.html index f1bc1323..5e940afc 100644 --- a/index.html +++ b/index.html @@ -25,11 +25,11 @@ "imports": { "@automerge/automerge": "https://ga.jspm.io/npm:@automerge/automerge@2.2.2/dist/mjs/index.js", "@automerge/automerge-repo-react-hooks": "https://ga.jspm.io/npm:@automerge/automerge-repo-react-hooks@1.1.12/dist/index.js", + "@automerge/automerge-wasm": "https://ga.jspm.io/npm:@automerge/automerge-wasm@0.17.0/bundler/automerge_wasm.js", "react": "https://ga.jspm.io/npm:react@18.3.1/dev.index.js" }, "scopes": { "https://ga.jspm.io/": { - "@automerge/automerge-wasm": "https://ga.jspm.io/npm:@automerge/automerge-wasm@0.17.0/bundler/automerge_wasm.js", "uuid": "https://ga.jspm.io/npm:uuid@9.0.1/dist/esm-browser/index.js" } } diff --git a/src/os/modules.ts b/src/os/modules.ts index 628ec5ca..fdde8dc6 100644 --- a/src/os/modules.ts +++ b/src/os/modules.ts @@ -29,13 +29,18 @@ export const useModule = (module: Module): (D & M) | undefined => { return; } - module.load().then((loadedModule) => { - // ignore if module has changed in the meantime - if (module !== moduleRef.current) { - return; - } - setLoadedModule(loadedModule); - }); + module + .load() + .then((loadedModule) => { + // ignore if module has changed in the meantime + if (module !== moduleRef.current) { + return; + } + setLoadedModule(loadedModule); + }) + .catch((err) => { + console.log(err); + }); }, [module]); return module ? loadedModule : undefined; diff --git a/src/os/tools.ts b/src/os/tools.ts index e41a2bf1..029f9848 100644 --- a/src/os/tools.ts +++ b/src/os/tools.ts @@ -7,13 +7,11 @@ import { HasVersionControlMetadata, } from "@/os/versionControl/schema"; import { AutomergeUrl, DocHandle } from "@automerge/automerge-repo"; -import { useDocument } from "@automerge/automerge-repo-react-hooks"; -import { - AccountDoc, - ModuleSettingsDoc, - useCurrentAccount, -} from "./explorer/account"; +import { useRootFolderDocWithChildren } from "./explorer/account"; import { Module } from "./modules"; +import { DocLink } from "@/datatypes/folder"; +import { useRepo } from "@automerge/automerge-repo-react-hooks"; +import { ModuleDoc } from "@/datatypes/module"; export type ToolMetaData = { id: string; @@ -74,23 +72,35 @@ for (const [path, { default: module }] of Object.entries(toolsFolder)) { export const useToolModules = () => { const [dynamicModules, setDynamicModules] = useState([]); + const repo = useRepo(); + + const { flatDocLinks } = useRootFolderDocWithChildren(); + + const moduleDocLinks = useMemo( + () => + flatDocLinks ? flatDocLinks.filter((link) => link.type === "module") : [], + [flatDocLinks] + ); - /* const dynamicModuleUrls = []; - const dynamicModuleUrlsRef = useRef(); - dynamicModuleUrlsRef.current = dynamicModuleUrls; + const moduleDocLinksRef = useRef(); + moduleDocLinksRef.current = moduleDocLinks; useEffect(() => { Promise.all( - dynamicModuleUrls.map(async (url) => (await import(url)).default) + moduleDocLinks.map(async ({ url }) => { + const moduleDoc = await repo.find(url).doc(); + const module = await import(moduleDoc.url); + return module.default; + }) ).then((modules) => { - // do nothing if dynamicModuleUrls has changed in the meantime - if (dynamicModuleUrls !== dynamicModuleUrlsRef.current) { + // skip if moduleDocLinks has changed in the meantime + if (moduleDocLinks !== moduleDocLinksRef.current) { return; } setDynamicModules(modules); }); - }, [dynamicModuleUrls]); */ + }, [moduleDocLinks]); return TOOLS.concat(dynamicModules); }; From 719447f3e5523b7a23ba0ffb885dc91355bc50a7 Mon Sep 17 00:00:00 2001 From: Paul Sonnentag Date: Fri, 7 Jun 2024 09:37:17 +0200 Subject: [PATCH 26/30] add vite plugin to externalize automerge and react deps --- index.html | 22 +++++++++++++++++- package.json | 2 ++ vite.config.ts | 18 ++++++++++++++- yarn.lock | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 102 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index 5e940afc..bc23a71b 100644 --- a/index.html +++ b/index.html @@ -24,19 +24,39 @@ { "imports": { "@automerge/automerge": "https://ga.jspm.io/npm:@automerge/automerge@2.2.2/dist/mjs/index.js", + "@automerge/automerge-repo": "https://ga.jspm.io/npm:@automerge/automerge-repo@1.1.12/dist/index.js", + "@automerge/automerge-repo-network-broadcastchannel": "https://ga.jspm.io/npm:@automerge/automerge-repo-network-broadcastchannel@1.1.12/dist/index.js", + "@automerge/automerge-repo-network-messagechannel": "https://ga.jspm.io/npm:@automerge/automerge-repo-network-messagechannel@1.1.12/dist/index.js", + "@automerge/automerge-repo-network-websocket": "https://ga.jspm.io/npm:@automerge/automerge-repo-network-websocket@1.1.12/dist/index.js", "@automerge/automerge-repo-react-hooks": "https://ga.jspm.io/npm:@automerge/automerge-repo-react-hooks@1.1.12/dist/index.js", + "@automerge/automerge-repo-storage-indexeddb": "https://ga.jspm.io/npm:@automerge/automerge-repo-storage-indexeddb@1.1.12/dist/index.js", "@automerge/automerge-wasm": "https://ga.jspm.io/npm:@automerge/automerge-wasm@0.17.0/bundler/automerge_wasm.js", + "@automerge/automerge/next": "https://ga.jspm.io/npm:@automerge/automerge@2.2.2/dist/mjs/next.js", "react": "https://ga.jspm.io/npm:react@18.3.1/dev.index.js" }, "scopes": { "https://ga.jspm.io/": { - "uuid": "https://ga.jspm.io/npm:uuid@9.0.1/dist/esm-browser/index.js" + "@noble/hashes/crypto": "https://ga.jspm.io/npm:@noble/hashes@1.4.0/crypto.js", + "@noble/hashes/sha256": "https://ga.jspm.io/npm:@noble/hashes@1.4.0/sha256.js", + "base-x": "https://ga.jspm.io/npm:base-x@4.0.0/src/index.js", + "bs58": "https://ga.jspm.io/npm:bs58@5.0.0/index.js", + "bs58check": "https://ga.jspm.io/npm:bs58check@3.0.1/index.js", + "cbor-x": "https://ga.jspm.io/npm:cbor-x@1.5.9/index.js", + "debug": "https://ga.jspm.io/npm:debug@4.3.5/src/browser.js", + "eventemitter3": "https://ga.jspm.io/npm:eventemitter3@5.0.1/index.mjs", + "fast-sha256": "https://ga.jspm.io/npm:fast-sha256@1.3.0/sha256.js", + "isomorphic-ws": "https://ga.jspm.io/npm:isomorphic-ws@5.0.0/browser.js", + "ms": "https://ga.jspm.io/npm:ms@2.1.2/index.js", + "process": "https://ga.jspm.io/npm:@jspm/core@2.0.1/nodelibs/browser/process.js", + "uuid": "https://ga.jspm.io/npm:uuid@9.0.1/dist/esm-browser/index.js", + "xstate": "https://ga.jspm.io/npm:xstate@5.13.1/dist/xstate.development.esm.js" } } } diff --git a/package.json b/package.json index 1dfe58a4..6a10253c 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,8 @@ "tsc-alias": "^1.8.8", "typescript": "^5.0.2", "vite": "^5.0.12", + "vite-plugin-external": "^4.3.1", + "vite-plugin-externalize-dependencies": "^0.12.0", "vite-plugin-top-level-await": "^1.4.1", "vite-plugin-wasm": "^3.3.0", "vitest": "^0.34.6" diff --git a/vite.config.ts b/vite.config.ts index 9a2821d0..a2eeb0c3 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,10 +6,26 @@ import wasm from "vite-plugin-wasm"; import topLevelAwait from "vite-plugin-top-level-await"; import { globSync } from "glob"; import { fileURLToPath } from "node:url"; +import externalize from "vite-plugin-externalize-dependencies"; export default defineConfig({ base: "./", - plugins: [topLevelAwait(), react()], + plugins: [ + topLevelAwait(), + react(), + externalize({ + externals: [ + "react", + "@automerge/automerge", + "@automerge/automerge-repo", + "@automerge/automerge-repo-network-broadcastchannel", + "@automerge/automerge-repo-network-messagechannel", + "@automerge/automerge-repo-network-websocket", + "@automerge/automerge-repo-react-hooks", + "@automerge/automerge-repo-storage-indexeddb", + ], + }), + ], resolve: { alias: { "@": path.resolve(__dirname, "./src"), diff --git a/yarn.lock b/yarn.lock index 7a3353f2..0bba7e68 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2381,11 +2381,26 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== +"@types/fs-extra@^11.0.4": + version "11.0.4" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-11.0.4.tgz#e16a863bb8843fba8c5004362b5a73e17becca45" + integrity sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ== + dependencies: + "@types/jsonfile" "*" + "@types/node" "*" + "@types/json-schema@^7.0.12": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== +"@types/jsonfile@*": + version "6.1.4" + resolved "https://registry.yarnpkg.com/@types/jsonfile/-/jsonfile-6.1.4.tgz#614afec1a1164e7d670b4a7ad64df3e7beb7b702" + integrity sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ== + dependencies: + "@types/node" "*" + "@types/lodash@^4.14.199": version "4.17.1" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.1.tgz#0fabfcf2f2127ef73b119d98452bd317c4a17eb8" @@ -2406,6 +2421,11 @@ dependencies: undici-types "~5.26.4" +"@types/node@^14.18.63": + version "14.18.63" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.63.tgz#1788fa8da838dbb5f9ea994b834278205db6ca2b" + integrity sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ== + "@types/node@^18.11.18": version "18.19.33" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.33.tgz#98cd286a1b8a5e11aa06623210240bcc28e95c48" @@ -3843,6 +3863,15 @@ fraction.js@^4.3.7: resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== +fs-extra@^11.1.1: + version "11.2.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" + integrity sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -3945,6 +3974,11 @@ globby@^11.0.4, globby@^11.1.0: merge2 "^1.4.1" slash "^3.0.0" +graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + graphemer@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" @@ -4281,6 +4315,15 @@ json5@^2.2.3: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + jszip@^3.10.1: version "3.10.1" resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2" @@ -5657,6 +5700,11 @@ universalify@^0.2.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== +universalify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== + update-browserslist-db@^1.0.13: version "1.0.15" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.15.tgz#60ed9f8cba4a728b7ecf7356f641a31e3a691d97" @@ -5744,6 +5792,20 @@ vite-node@0.34.6: picocolors "^1.0.0" vite "^3.0.0 || ^4.0.0 || ^5.0.0-0" +vite-plugin-external@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/vite-plugin-external/-/vite-plugin-external-4.3.1.tgz#d91cf44736fc9e311ed7576d0d41acc630901b1b" + integrity sha512-aoukfac66QevFAbRF2ZD81WPSqeqhgEfbfhPYQP9RPWO1en+Lw4HyhWRdvSjYn56gcKUJCtHdFG2jEpYgleLng== + dependencies: + "@types/fs-extra" "^11.0.4" + "@types/node" "^14.18.63" + fs-extra "^11.1.1" + +vite-plugin-externalize-dependencies@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/vite-plugin-externalize-dependencies/-/vite-plugin-externalize-dependencies-0.12.0.tgz#f0a74b6796dffdfbd861eb10b473217f82a84f84" + integrity sha512-sauIb6N7I93DbRKD2SNWZhrfCdBB3+/L/O4uujCH+8NyhISqSGcGCUDD7bYz0hkVpXmUhTzIQGnzUJDGbFb0Cg== + vite-plugin-top-level-await@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/vite-plugin-top-level-await/-/vite-plugin-top-level-await-1.4.1.tgz#607dfe084157550fa33df18062b99ceea774cd9c" From 64e82f7b47f3286991c672e49ca6effbc6166c1a Mon Sep 17 00:00:00 2001 From: Paul Sonnentag Date: Fri, 7 Jun 2024 13:09:20 +0200 Subject: [PATCH 27/30] replace plugin with simple import map lookup --- index.html | 8 ++++-- vite.config.ts | 74 ++++++++++++++++++++++++++++++++++---------------- 2 files changed, 56 insertions(+), 26 deletions(-) diff --git a/index.html b/index.html index bc23a71b..f269f52e 100644 --- a/index.html +++ b/index.html @@ -32,7 +32,11 @@ "@automerge/automerge-repo-storage-indexeddb": "https://ga.jspm.io/npm:@automerge/automerge-repo-storage-indexeddb@1.1.12/dist/index.js", "@automerge/automerge-wasm": "https://ga.jspm.io/npm:@automerge/automerge-wasm@0.17.0/bundler/automerge_wasm.js", "@automerge/automerge/next": "https://ga.jspm.io/npm:@automerge/automerge@2.2.2/dist/mjs/next.js", - "react": "https://ga.jspm.io/npm:react@18.3.1/dev.index.js" + "react": "https://ga.jspm.io/npm:react@18.3.1/dev.index.js", + "react-dom": "https://ga.jspm.io/npm:react-dom@18.3.1/dev.index.js", + "react-dom/client": "https://ga.jspm.io/npm:react-dom@18.3.1/dev.client.js", + "react/jsx-dev-runtime": "https://ga.jspm.io/npm:react@18.3.1/dev.jsx-dev-runtime.js", + "react/jsx-runtime": "https://ga.jspm.io/npm:react@18.3.1/dev.jsx-runtime.js" }, "scopes": { "https://ga.jspm.io/": { @@ -48,6 +52,7 @@ "isomorphic-ws": "https://ga.jspm.io/npm:isomorphic-ws@5.0.0/browser.js", "ms": "https://ga.jspm.io/npm:ms@2.1.2/index.js", "process": "https://ga.jspm.io/npm:@jspm/core@2.0.1/nodelibs/browser/process.js", + "scheduler": "https://ga.jspm.io/npm:scheduler@0.23.2/dev.index.js", "uuid": "https://ga.jspm.io/npm:uuid@9.0.1/dist/esm-browser/index.js", "xstate": "https://ga.jspm.io/npm:xstate@5.13.1/dist/xstate.development.esm.js" } @@ -56,7 +61,6 @@ diff --git a/vite.config.ts b/vite.config.ts index a2eeb0c3..10d45917 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,30 +1,60 @@ // vite.config.ts -import { defineConfig } from "vite"; -import path from "path"; import react from "@vitejs/plugin-react"; -import wasm from "vite-plugin-wasm"; -import topLevelAwait from "vite-plugin-top-level-await"; import { globSync } from "glob"; import { fileURLToPath } from "node:url"; -import externalize from "vite-plugin-externalize-dependencies"; +import path from "path"; +import { defineConfig } from "vite"; +import topLevelAwait from "vite-plugin-top-level-await"; +import wasm from "vite-plugin-wasm"; + +const IMPORT_MAP = { + imports: { + "@automerge/automerge": + "https://ga.jspm.io/npm:@automerge/automerge@2.2.2/dist/mjs/index.js", + "@automerge/automerge-repo": + "https://ga.jspm.io/npm:@automerge/automerge-repo@1.1.12/dist/index.js", + "@automerge/automerge-repo-network-broadcastchannel": + "https://ga.jspm.io/npm:@automerge/automerge-repo-network-broadcastchannel@1.1.12/dist/index.js", + "@automerge/automerge-repo-network-messagechannel": + "https://ga.jspm.io/npm:@automerge/automerge-repo-network-messagechannel@1.1.12/dist/index.js", + "@automerge/automerge-repo-network-websocket": + "https://ga.jspm.io/npm:@automerge/automerge-repo-network-websocket@1.1.12/dist/index.js", + "@automerge/automerge-repo-react-hooks": + "https://ga.jspm.io/npm:@automerge/automerge-repo-react-hooks@1.1.12/dist/index.js", + "@automerge/automerge-repo-storage-indexeddb": + "https://ga.jspm.io/npm:@automerge/automerge-repo-storage-indexeddb@1.1.12/dist/index.js", + "@automerge/automerge-wasm": + "https://ga.jspm.io/npm:@automerge/automerge-wasm@0.17.0/bundler/automerge_wasm.js", + "@automerge/automerge/next": + "https://ga.jspm.io/npm:@automerge/automerge@2.2.2/dist/mjs/next.js", + react: "https://ga.jspm.io/npm:react@18.3.1/dev.index.js", + "react-dom": "https://ga.jspm.io/npm:react-dom@18.3.1/dev.index.js", + "react-dom/client": "https://ga.jspm.io/npm:react-dom@18.3.1/dev.client.js", + "react/jsx-dev-runtime": + "https://ga.jspm.io/npm:react@18.3.1/dev.jsx-dev-runtime.js", + "react/jsx-runtime": + "https://ga.jspm.io/npm:react@18.3.1/dev.jsx-runtime.js", + }, +}; + +const EXTERNALS = Object.keys(IMPORT_MAP); export default defineConfig({ base: "./", plugins: [ topLevelAwait(), react(), - externalize({ - externals: [ - "react", - "@automerge/automerge", - "@automerge/automerge-repo", - "@automerge/automerge-repo-network-broadcastchannel", - "@automerge/automerge-repo-network-messagechannel", - "@automerge/automerge-repo-network-websocket", - "@automerge/automerge-repo-react-hooks", - "@automerge/automerge-repo-storage-indexeddb", - ], - }), + // replace dependencies in import map with url + { + name: "resolve-external-deps", + enforce: "pre", + resolveId(source) { + if (IMPORT_MAP[source]) { + return IMPORT_MAP[source]; + } + return null; + }, + }, ], resolve: { alias: { @@ -38,8 +68,8 @@ export default defineConfig({ // wrapper has a module level variable to track JS side heap // allocations, and initializing this twice causes horrible breakage exclude: [ - "@automerge/automerge-wasm", - "@automerge/automerge-wasm/bundler/bindgen_bg.wasm", + /* "@automerge/automerge-wasm", + "@automerge/automerge-wasm/bundler/bindgen_bg.wasm", */ "@syntect/wasm", ], }, @@ -50,11 +80,7 @@ export default defineConfig({ }, build: { rollupOptions: { - external: [ - "@automerge/automerge", - "@automerge/automerge-repo-react-hooks", - "react", - ], + external: EXTERNALS, input: { main: path.resolve(__dirname, "index.html"), "service-worker": path.resolve(__dirname, "service-worker.js"), From b03192deaaa788ff92119194683ed63241e9b0d1 Mon Sep 17 00:00:00 2001 From: Paul Sonnentag Date: Fri, 7 Jun 2024 13:57:20 +0200 Subject: [PATCH 28/30] fix import map and comment out service worker --- package.json | 3 --- src/os/main.tsx | 25 +++++++++++----------- vite.config.ts | 55 +++++++++++++++++++++++-------------------------- yarn.lock | 4 +++- 4 files changed, 42 insertions(+), 45 deletions(-) diff --git a/package.json b/package.json index 6a10253c..c4b59b18 100644 --- a/package.json +++ b/package.json @@ -102,8 +102,5 @@ "vite-plugin-top-level-await": "^1.4.1", "vite-plugin-wasm": "^3.3.0", "vitest": "^0.34.6" - }, - "resolutions": { - "@automerge/automerge-wasm": "file:./src/vendor/automerge-wasm" } } diff --git a/src/os/main.tsx b/src/os/main.tsx index e3ff4c78..72b0834e 100644 --- a/src/os/main.tsx +++ b/src/os/main.tsx @@ -17,28 +17,29 @@ import { RepoContext } from "@automerge/automerge-repo-react-hooks"; import { getAccount } from "./explorer/account.js"; import { Explorer } from "./explorer/components/Explorer.js"; import "./index.css"; +import { Button } from "@/components/ui/button.js"; -const serviceWorker = await setupServiceWorker(); +// const serviceWorker = await setupServiceWorker(); // Service workers stop on their own, which breaks sync. // Here we ping the service worker while the tab is running // to keep it alive (and make it restart if it did stop.) -setInterval(() => { +/* setInterval(() => { serviceWorker.postMessage({ type: "PING" }); -}, 5000); +}, 5000); */ // This case should never happen // if the service worker is not defined here either the initialization failed // or we found a new case that we haven't considered yet -if (!serviceWorker) { +/* if (!serviceWorker) { throw new Error("Failed to setup service worker"); -} +} */ const repo = await setupRepo(); -establishMessageChannel(serviceWorker); +// establishMessageChannel(serviceWorker); -async function setupServiceWorker(): Promise { +/* async function setupServiceWorker(): Promise { return navigator.serviceWorker .register("/service-worker.js", { type: "module", @@ -59,7 +60,7 @@ async function setupServiceWorker(): Promise { // otherwise return the active service worker return registration.active; }); -} +} */ async function setupRepo() { // in our vendored version we export a promise that resolves once the wasm is loaded @@ -84,7 +85,7 @@ async function setupRepo() { } // Re-establish the MessageChannel if the controlling service worker changes. -navigator.serviceWorker.addEventListener("controllerchange", (event) => { +/* navigator.serviceWorker.addEventListener("controllerchange", (event) => { const newServiceWorker = (event.target as ServiceWorkerContainer).controller; // controllerchange is fired after a new service worker is installed // even if we wait above in setupServiceWorker() until the service worker state changes to activated. @@ -92,7 +93,7 @@ navigator.serviceWorker.addEventListener("controllerchange", (event) => { if (newServiceWorker !== serviceWorker) { establishMessageChannel(newServiceWorker); } -}); +}); */ // Re-establish the MessageChannel if the service worker restarts /* navigator.serviceWorker.addEventListener("message", (event) => { @@ -105,7 +106,7 @@ navigator.serviceWorker.addEventListener("controllerchange", (event) => { // Connects the repo in the tab with the repo in the service worker through a message channel. // The repo in the tab takes advantage of loaded state in the SW. // TODO: clean up MessageChannels to old repos -function establishMessageChannel(serviceWorker: ServiceWorker) { +/* function establishMessageChannel(serviceWorker: ServiceWorker) { // Send one side of a MessageChannel to the service worker and register the other with the repo. const messageChannel = new MessageChannel(); repo.networkSubsystem.addNetworkAdapter( @@ -113,7 +114,7 @@ function establishMessageChannel(serviceWorker: ServiceWorker) { ); serviceWorker.postMessage({ type: "INIT_PORT" }, [messageChannel.port2]); console.log("Connected to service worker"); -} +} */ // Setup account diff --git a/vite.config.ts b/vite.config.ts index 10d45917..e5914a73 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -8,33 +8,30 @@ import topLevelAwait from "vite-plugin-top-level-await"; import wasm from "vite-plugin-wasm"; const IMPORT_MAP = { - imports: { - "@automerge/automerge": - "https://ga.jspm.io/npm:@automerge/automerge@2.2.2/dist/mjs/index.js", - "@automerge/automerge-repo": - "https://ga.jspm.io/npm:@automerge/automerge-repo@1.1.12/dist/index.js", - "@automerge/automerge-repo-network-broadcastchannel": - "https://ga.jspm.io/npm:@automerge/automerge-repo-network-broadcastchannel@1.1.12/dist/index.js", - "@automerge/automerge-repo-network-messagechannel": - "https://ga.jspm.io/npm:@automerge/automerge-repo-network-messagechannel@1.1.12/dist/index.js", - "@automerge/automerge-repo-network-websocket": - "https://ga.jspm.io/npm:@automerge/automerge-repo-network-websocket@1.1.12/dist/index.js", - "@automerge/automerge-repo-react-hooks": - "https://ga.jspm.io/npm:@automerge/automerge-repo-react-hooks@1.1.12/dist/index.js", - "@automerge/automerge-repo-storage-indexeddb": - "https://ga.jspm.io/npm:@automerge/automerge-repo-storage-indexeddb@1.1.12/dist/index.js", - "@automerge/automerge-wasm": - "https://ga.jspm.io/npm:@automerge/automerge-wasm@0.17.0/bundler/automerge_wasm.js", - "@automerge/automerge/next": - "https://ga.jspm.io/npm:@automerge/automerge@2.2.2/dist/mjs/next.js", - react: "https://ga.jspm.io/npm:react@18.3.1/dev.index.js", - "react-dom": "https://ga.jspm.io/npm:react-dom@18.3.1/dev.index.js", - "react-dom/client": "https://ga.jspm.io/npm:react-dom@18.3.1/dev.client.js", - "react/jsx-dev-runtime": - "https://ga.jspm.io/npm:react@18.3.1/dev.jsx-dev-runtime.js", - "react/jsx-runtime": - "https://ga.jspm.io/npm:react@18.3.1/dev.jsx-runtime.js", - }, + "@automerge/automerge": + "https://ga.jspm.io/npm:@automerge/automerge@2.2.2/dist/mjs/index.js", + "@automerge/automerge-repo": + "https://ga.jspm.io/npm:@automerge/automerge-repo@1.1.12/dist/index.js", + "@automerge/automerge-repo-network-broadcastchannel": + "https://ga.jspm.io/npm:@automerge/automerge-repo-network-broadcastchannel@1.1.12/dist/index.js", + "@automerge/automerge-repo-network-messagechannel": + "https://ga.jspm.io/npm:@automerge/automerge-repo-network-messagechannel@1.1.12/dist/index.js", + "@automerge/automerge-repo-network-websocket": + "https://ga.jspm.io/npm:@automerge/automerge-repo-network-websocket@1.1.12/dist/index.js", + "@automerge/automerge-repo-react-hooks": + "https://ga.jspm.io/npm:@automerge/automerge-repo-react-hooks@1.1.12/dist/index.js", + "@automerge/automerge-repo-storage-indexeddb": + "https://ga.jspm.io/npm:@automerge/automerge-repo-storage-indexeddb@1.1.12/dist/index.js", + "@automerge/automerge-wasm": + "https://ga.jspm.io/npm:@automerge/automerge-wasm@0.17.0/bundler/automerge_wasm.js", + "@automerge/automerge/next": + "https://ga.jspm.io/npm:@automerge/automerge@2.2.2/dist/mjs/next.js", + react: "https://ga.jspm.io/npm:react@18.3.1/dev.index.js", + "react-dom": "https://ga.jspm.io/npm:react-dom@18.3.1/dev.index.js", + "react-dom/client": "https://ga.jspm.io/npm:react-dom@18.3.1/dev.client.js", + "react/jsx-dev-runtime": + "https://ga.jspm.io/npm:react@18.3.1/dev.jsx-dev-runtime.js", + "react/jsx-runtime": "https://ga.jspm.io/npm:react@18.3.1/dev.jsx-runtime.js", }; const EXTERNALS = Object.keys(IMPORT_MAP); @@ -68,8 +65,8 @@ export default defineConfig({ // wrapper has a module level variable to track JS side heap // allocations, and initializing this twice causes horrible breakage exclude: [ - /* "@automerge/automerge-wasm", - "@automerge/automerge-wasm/bundler/bindgen_bg.wasm", */ + "@automerge/automerge-wasm", + "@automerge/automerge-wasm/bundler/bindgen_bg.wasm", "@syntect/wasm", ], }, diff --git a/yarn.lock b/yarn.lock index 0bba7e68..bc07edc5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -88,8 +88,10 @@ uuid "^9.0.0" xstate "^5.9.1" -"@automerge/automerge-wasm@0.17.0", "@automerge/automerge-wasm@file:./src/vendor/automerge-wasm": +"@automerge/automerge-wasm@0.17.0": version "0.17.0" + resolved "https://registry.yarnpkg.com/@automerge/automerge-wasm/-/automerge-wasm-0.17.0.tgz#d6076287e87c3eb3a95c19a20f3fe09fff5381bc" + integrity sha512-EPTqadOj+wzv73rPaa80Q+tKnM+XpEtYhqZ4reB9647OZdiQjQwlXo+y/7VrGO+x48Kow0sYpZa9rgx8t785kA== "@automerge/automerge@^2.1.13", "@automerge/automerge@^2.1.9", "@automerge/automerge@^2.2.2": version "2.2.2" From 88ccdb33d69e430c3cd657ededc2d1f47cedced2 Mon Sep 17 00:00:00 2001 From: Paul Sonnentag Date: Mon, 10 Jun 2024 11:20:45 +0200 Subject: [PATCH 29/30] don't change imports in dev mode --- index.html | 3 +++ package.json | 3 +++ src/os/tools.ts | 6 ++--- vite.config.ts | 59 ++++++++++--------------------------------------- yarn.lock | 4 +--- 5 files changed, 22 insertions(+), 53 deletions(-) diff --git a/index.html b/index.html index f269f52e..0e8e9d81 100644 --- a/index.html +++ b/index.html @@ -36,10 +36,13 @@ "react-dom": "https://ga.jspm.io/npm:react-dom@18.3.1/dev.index.js", "react-dom/client": "https://ga.jspm.io/npm:react-dom@18.3.1/dev.client.js", "react/jsx-dev-runtime": "https://ga.jspm.io/npm:react@18.3.1/dev.jsx-dev-runtime.js", + "react-dom/client": "https://ga.jspm.io/npm:react-dom@18.3.1/dev.client.js", "react/jsx-runtime": "https://ga.jspm.io/npm:react@18.3.1/dev.jsx-runtime.js" }, "scopes": { "https://ga.jspm.io/": { + "@automerge/automerge-wasm": "https://ga.jspm.io/npm:@automerge/automerge-wasm@0.17.0/bundler/automerge_wasm.js", + "@automerge/automerge/next": "https://ga.jspm.io/npm:@automerge/automerge@2.2.2/dist/mjs/next.js", "@noble/hashes/crypto": "https://ga.jspm.io/npm:@noble/hashes@1.4.0/crypto.js", "@noble/hashes/sha256": "https://ga.jspm.io/npm:@noble/hashes@1.4.0/sha256.js", "base-x": "https://ga.jspm.io/npm:base-x@4.0.0/src/index.js", diff --git a/package.json b/package.json index c4b59b18..6a10253c 100644 --- a/package.json +++ b/package.json @@ -102,5 +102,8 @@ "vite-plugin-top-level-await": "^1.4.1", "vite-plugin-wasm": "^3.3.0", "vitest": "^0.34.6" + }, + "resolutions": { + "@automerge/automerge-wasm": "file:./src/vendor/automerge-wasm" } } diff --git a/src/os/tools.ts b/src/os/tools.ts index 029f9848..1a021a4e 100644 --- a/src/os/tools.ts +++ b/src/os/tools.ts @@ -71,7 +71,7 @@ for (const [path, { default: module }] of Object.entries(toolsFolder)) { } export const useToolModules = () => { - const [dynamicModules, setDynamicModules] = useState([]); + /*const [dynamicModules, setDynamicModules] = useState([]); const repo = useRepo(); const { flatDocLinks } = useRootFolderDocWithChildren(); @@ -100,9 +100,9 @@ export const useToolModules = () => { setDynamicModules(modules); }); - }, [moduleDocLinks]); + }, [moduleDocLinks]); */ - return TOOLS.concat(dynamicModules); + return TOOLS; }; export const useToolModulesForDataType = (dataTypeId: string) => { diff --git a/vite.config.ts b/vite.config.ts index e5914a73..71cb247d 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,58 +1,23 @@ // vite.config.ts +import { defineConfig } from "vite"; +import path from "path"; import react from "@vitejs/plugin-react"; +import wasm from "vite-plugin-wasm"; import { globSync } from "glob"; import { fileURLToPath } from "node:url"; -import path from "path"; -import { defineConfig } from "vite"; import topLevelAwait from "vite-plugin-top-level-await"; -import wasm from "vite-plugin-wasm"; - -const IMPORT_MAP = { - "@automerge/automerge": - "https://ga.jspm.io/npm:@automerge/automerge@2.2.2/dist/mjs/index.js", - "@automerge/automerge-repo": - "https://ga.jspm.io/npm:@automerge/automerge-repo@1.1.12/dist/index.js", - "@automerge/automerge-repo-network-broadcastchannel": - "https://ga.jspm.io/npm:@automerge/automerge-repo-network-broadcastchannel@1.1.12/dist/index.js", - "@automerge/automerge-repo-network-messagechannel": - "https://ga.jspm.io/npm:@automerge/automerge-repo-network-messagechannel@1.1.12/dist/index.js", - "@automerge/automerge-repo-network-websocket": - "https://ga.jspm.io/npm:@automerge/automerge-repo-network-websocket@1.1.12/dist/index.js", - "@automerge/automerge-repo-react-hooks": - "https://ga.jspm.io/npm:@automerge/automerge-repo-react-hooks@1.1.12/dist/index.js", - "@automerge/automerge-repo-storage-indexeddb": - "https://ga.jspm.io/npm:@automerge/automerge-repo-storage-indexeddb@1.1.12/dist/index.js", - "@automerge/automerge-wasm": - "https://ga.jspm.io/npm:@automerge/automerge-wasm@0.17.0/bundler/automerge_wasm.js", - "@automerge/automerge/next": - "https://ga.jspm.io/npm:@automerge/automerge@2.2.2/dist/mjs/next.js", - react: "https://ga.jspm.io/npm:react@18.3.1/dev.index.js", - "react-dom": "https://ga.jspm.io/npm:react-dom@18.3.1/dev.index.js", - "react-dom/client": "https://ga.jspm.io/npm:react-dom@18.3.1/dev.client.js", - "react/jsx-dev-runtime": - "https://ga.jspm.io/npm:react@18.3.1/dev.jsx-dev-runtime.js", - "react/jsx-runtime": "https://ga.jspm.io/npm:react@18.3.1/dev.jsx-runtime.js", -}; -const EXTERNALS = Object.keys(IMPORT_MAP); +const SHARED_DEPENDENCIES = [ + "@automerge/automerge-wasm", + "@automerge/automerge", + "@automerge/automerge-repo", + "@automerge/automerge-repo-react-hooks", + "react", +]; export default defineConfig({ base: "./", - plugins: [ - topLevelAwait(), - react(), - // replace dependencies in import map with url - { - name: "resolve-external-deps", - enforce: "pre", - resolveId(source) { - if (IMPORT_MAP[source]) { - return IMPORT_MAP[source]; - } - return null; - }, - }, - ], + plugins: [topLevelAwait(), react()], resolve: { alias: { "@": path.resolve(__dirname, "./src"), @@ -77,7 +42,7 @@ export default defineConfig({ }, build: { rollupOptions: { - external: EXTERNALS, + external: SHARED_DEPENDENCIES, input: { main: path.resolve(__dirname, "index.html"), "service-worker": path.resolve(__dirname, "service-worker.js"), diff --git a/yarn.lock b/yarn.lock index bc07edc5..0bba7e68 100644 --- a/yarn.lock +++ b/yarn.lock @@ -88,10 +88,8 @@ uuid "^9.0.0" xstate "^5.9.1" -"@automerge/automerge-wasm@0.17.0": +"@automerge/automerge-wasm@0.17.0", "@automerge/automerge-wasm@file:./src/vendor/automerge-wasm": version "0.17.0" - resolved "https://registry.yarnpkg.com/@automerge/automerge-wasm/-/automerge-wasm-0.17.0.tgz#d6076287e87c3eb3a95c19a20f3fe09fff5381bc" - integrity sha512-EPTqadOj+wzv73rPaa80Q+tKnM+XpEtYhqZ4reB9647OZdiQjQwlXo+y/7VrGO+x48Kow0sYpZa9rgx8t785kA== "@automerge/automerge@^2.1.13", "@automerge/automerge@^2.1.9", "@automerge/automerge@^2.2.2": version "2.2.2" From 3f1eecca4b0443aaead6a4d71a8cd9266d5c0d77 Mon Sep 17 00:00:00 2001 From: Paul Sonnentag Date: Mon, 10 Jun 2024 14:39:10 +0200 Subject: [PATCH 30/30] add source map for shared dependencies --- index.html | 42 -------------------- package.json | 1 + vite.config.ts | 62 ++++++++++++++++++++++++++--- yarn.lock | 105 ++++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 161 insertions(+), 49 deletions(-) diff --git a/index.html b/index.html index 0e8e9d81..41ef5b1b 100644 --- a/index.html +++ b/index.html @@ -20,48 +20,6 @@
- ` + ); + + return $.html(); + }; + }, + }, + ], resolve: { alias: { "@": path.resolve(__dirname, "./src"), diff --git a/yarn.lock b/yarn.lock index 0bba7e68..6457db82 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2856,6 +2856,11 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== +boolbase@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -2996,6 +3001,31 @@ check-error@^1.0.3: dependencies: get-func-name "^2.0.2" +cheerio-select@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-2.1.0.tgz#4d8673286b8126ca2a8e42740d5e3c4884ae21b4" + integrity sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g== + dependencies: + boolbase "^1.0.0" + css-select "^5.1.0" + css-what "^6.1.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + domutils "^3.0.1" + +cheerio@^1.0.0-rc.12: + version "1.0.0-rc.12" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.12.tgz#788bf7466506b1c6bf5fae51d24a2c4d62e47683" + integrity sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q== + dependencies: + cheerio-select "^2.1.0" + dom-serializer "^2.0.0" + domhandler "^5.0.3" + domutils "^3.0.1" + htmlparser2 "^8.0.1" + parse5 "^7.0.0" + parse5-htmlparser2-tree-adapter "^7.0.0" + chevrotain@^6.5.0: version "6.5.0" resolved "https://registry.yarnpkg.com/chevrotain/-/chevrotain-6.5.0.tgz#dcbef415516b0af80fd423cc0d96b28d3f11374e" @@ -3155,6 +3185,17 @@ css-color-keywords@^1.0.0: resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05" integrity sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg== +css-select@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" + integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== + dependencies: + boolbase "^1.0.0" + css-what "^6.1.0" + domhandler "^5.0.2" + domutils "^3.0.1" + nth-check "^2.0.1" + css-to-react-native@3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/css-to-react-native/-/css-to-react-native-3.2.0.tgz#cdd8099f71024e149e4f6fe17a7d46ecd55f1e32" @@ -3164,6 +3205,11 @@ css-to-react-native@3.2.0: css-color-keywords "^1.0.0" postcss-value-parser "^4.0.2" +css-what@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" + integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== + cssesc@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" @@ -3548,6 +3594,20 @@ dom-accessibility-api@^0.5.9: resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== +dom-serializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" + integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.2" + entities "^4.2.0" + +domelementtype@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + domexception@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/domexception/-/domexception-4.0.0.tgz#4ad1be56ccadc86fc76d033353999a8037d03673" @@ -3555,11 +3615,27 @@ domexception@^4.0.0: dependencies: webidl-conversions "^7.0.0" +domhandler@^5.0.2, domhandler@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" + integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== + dependencies: + domelementtype "^2.3.0" + dompurify@^2.1.1: version "2.5.3" resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.5.3.tgz#bc901a9c40a7d97176c1d0ab9a24939db54270a2" integrity sha512-09uyBM2URzOfXMUAqGRnm9R9IUeSkzO9PktXc2eVQIsBmmJUqRmfL1xW2QPBxVJEtlEVs5d8ndrsIQsyAqs81g== +domutils@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e" + integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== + dependencies: + dom-serializer "^2.0.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + eastasianwidth@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" @@ -3580,7 +3656,7 @@ emoji-regex@^9.2.2: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== -entities@^4.4.0: +entities@^4.2.0, entities@^4.4.0: version "4.5.0" resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== @@ -4044,6 +4120,16 @@ html-encoding-sniffer@^3.0.0: dependencies: whatwg-encoding "^2.0.0" +htmlparser2@^8.0.1: + version "8.0.2" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21" + integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.3" + domutils "^3.0.1" + entities "^4.4.0" + http-proxy-agent@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" @@ -4632,6 +4718,13 @@ normalize-range@^0.1.2: resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== +nth-check@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" + integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== + dependencies: + boolbase "^1.0.0" + numbro@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/numbro/-/numbro-2.1.2.tgz#2d51104f09b5d69aef7e15bb565d7795e47ecfd6" @@ -4720,7 +4813,15 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse5@^7.1.2: +parse5-htmlparser2-tree-adapter@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz#23c2cc233bcf09bb7beba8b8a69d46b08c62c2f1" + integrity sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g== + dependencies: + domhandler "^5.0.2" + parse5 "^7.0.0" + +parse5@^7.0.0, parse5@^7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==