diff --git a/packages/document-methods/src/index.ts b/packages/document-methods/src/index.ts index 41db31078..711619ec4 100644 --- a/packages/document-methods/src/index.ts +++ b/packages/document-methods/src/index.ts @@ -1,7 +1,9 @@ export type { DiagramDocument } from "./diagram"; +export type { InstanceDocument } from "./instance"; export type { ModelDocument } from "./model"; export type { FormalCell, RichTextCell } from "./notebook"; export * as Diagram from "./diagram"; +export * as Instance from "./instance"; export * as Model from "./model"; export * as Nb from "./notebook"; diff --git a/packages/document-methods/src/instance.ts b/packages/document-methods/src/instance.ts new file mode 100644 index 000000000..589327afe --- /dev/null +++ b/packages/document-methods/src/instance.ts @@ -0,0 +1,18 @@ +import type { InstanceJudgment, Document, StableRef } from "catcolab-document-types"; +import { currentVersion } from "catcolab-document-types"; +import { newNotebook } from "./notebook"; + +/** A document defining a instance in a model. */ +export type InstanceDocument = Document & { type: "instance" }; + +/** Create an empty instance of a model. */ +export const newInstanceDocument = (modelRef: StableRef): InstanceDocument => ({ + name: "", + type: "instance", + instanceIn: { + ...modelRef, + type: "instance-in", + }, + notebook: newNotebook(), + version: currentVersion(), +}); diff --git a/packages/document-types/src/v0/api.rs b/packages/document-types/src/v0/api.rs index 5ab7838eb..55c41123f 100644 --- a/packages/document-types/src/v0/api.rs +++ b/packages/document-types/src/v0/api.rs @@ -57,6 +57,9 @@ pub enum LinkType { #[serde(rename = "diagram-in")] DiagramIn, + #[serde(rename = "instance-in")] + InstanceIn, + #[serde(rename = "instantiation")] Instantiation, } diff --git a/packages/document-types/src/v2/document.rs b/packages/document-types/src/v2/document.rs index 6f142093e..d3e135f65 100644 --- a/packages/document-types/src/v2/document.rs +++ b/packages/document-types/src/v2/document.rs @@ -36,6 +36,16 @@ pub struct DiagramDocumentContent { pub version: String, } +#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Tsify)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct InstanceDocumentContent { + pub name: String, + #[serde(rename = "instanceIn")] + pub instance_in: Link, + pub notebook: Notebook, + pub version: String, +} + #[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Tsify)] #[tsify(into_wasm_abi, from_wasm_abi)] pub struct AnalysisDocumentContent { @@ -58,6 +68,8 @@ pub enum Document { Diagram(DiagramDocumentContent), #[serde(rename = "analysis")] Analysis(AnalysisDocumentContent), + #[serde(rename = "instance")] + Instance(InstanceDocumentContent), } impl Document { diff --git a/packages/document-types/src/v2/instance.rs b/packages/document-types/src/v2/instance.rs new file mode 100644 index 000000000..74fd5adf7 --- /dev/null +++ b/packages/document-types/src/v2/instance.rs @@ -0,0 +1,35 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use tsify::Tsify; +use uuid::Uuid; + +#[derive(PartialEq, Debug, Serialize, Deserialize, Tsify)] +pub enum CellValue { + // If the column corresponds to an attribute morphism then we provide the value of the type. + Null, + Bool(bool), + Int(i32), + Float(f32), + String(String), + // If the column corresponds to a mapping morphism then we provide the uuid of the entity. + EntityRef(Uuid), +} + +#[derive(PartialEq, Debug, Serialize, Deserialize, Tsify)] +pub struct TableRow { + // The row "number". + id: Uuid, + // The content of the row, thought of as the predicate-object parts of a semantic triple, i.e. the + // column and the value. + content: HashMap, +} + +#[derive(PartialEq, Debug, Serialize, Deserialize, Tsify)] +pub struct Table { + // The uuid of the table. + id: Uuid, + // The uuid of the entity to which this table corresponds. + entity: Uuid, + // The rows of the table. + rows: Vec, +} diff --git a/packages/document-types/src/v2/instance_judgment.rs b/packages/document-types/src/v2/instance_judgment.rs new file mode 100644 index 000000000..66d9a59e2 --- /dev/null +++ b/packages/document-types/src/v2/instance_judgment.rs @@ -0,0 +1,11 @@ +use serde::{Deserialize, Serialize}; +use tsify::Tsify; + +/// A judgment defining part of a instance in a model. +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Tsify)] +#[serde(tag = "tag")] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub enum InstanceJudgment { + #[serde(rename = "nullJudgment")] + NullJudgment, +} diff --git a/packages/document-types/src/v2/mod.rs b/packages/document-types/src/v2/mod.rs index ce20e6a8a..de2ade943 100644 --- a/packages/document-types/src/v2/mod.rs +++ b/packages/document-types/src/v2/mod.rs @@ -4,6 +4,8 @@ pub use v1::{analysis, api, diagram_judgment, model, model_judgment, path, theor pub mod cell; pub mod document; +pub mod instance; +pub mod instance_judgment; pub mod notebook; pub use analysis::*; diff --git a/packages/frontend/src/instance/context.ts b/packages/frontend/src/instance/context.ts new file mode 100644 index 000000000..429a28f7d --- /dev/null +++ b/packages/frontend/src/instance/context.ts @@ -0,0 +1,6 @@ +import { type Accessor, createContext } from "solid-js"; + +import type { LiveInstanceDoc } from "./document"; + +/** Context for a live diagram in a model. */ +export const LiveInstanceContext = createContext>(); diff --git a/packages/frontend/src/instance/document.ts b/packages/frontend/src/instance/document.ts new file mode 100644 index 000000000..7a054d44c --- /dev/null +++ b/packages/frontend/src/instance/document.ts @@ -0,0 +1,83 @@ +import type { AnyDocumentId, Repo } from "@automerge/automerge-repo"; +import { type Accessor, createMemo } from "solid-js"; + +import type { InstanceDocument } from "catcolab-document-methods"; +import { Instance, Nb } from "catcolab-document-methods"; +import type { InstanceJudgment, StableRef, Uuid } from "catcolab-document-types"; +import { type Api, type DocRef, findAndMigrate, type LiveDoc, makeLiveDoc } from "../api"; +import type { LiveModelDoc, ModelLibrary } from "../model"; + +/** A instance document "live" for editing. */ +export type LiveInstanceDoc = { + /** Tag for use in tagged unions of document types. */ + type: "instance"; + + /** Live document containing the instance data. */ + liveDoc: LiveDoc; + + /** Live model that the instance is in. */ + liveModel: LiveModelDoc; + + /** A memo of the formal content of the model. */ + formalJudgments: Accessor>; +}; + +export function enlivenInstanceDocument( + liveDoc: LiveDoc, + liveModel: LiveModelDoc, +): LiveInstanceDoc { + const { doc } = liveDoc; + + const formalJudgments = createMemo>(() => + Nb.getFormalContent(doc.notebook), + ); + + return { + type: "instance", + liveDoc, + liveModel, + formalJudgments, + }; +} + +/** Create a new, empty instance in the backend. */ +export function createInstance(api: Api, inModel: StableRef): Promise { + const init = Instance.newInstanceDocument(inModel); + return api.createDoc(init); +} + +export type LiveInstanceDocWithRef = { + liveInstance: LiveInstanceDoc; + docRef: DocRef; +}; + +/** Retrieve a instance from the backend and make it "live" for editing. */ +export async function getLiveInstance( + refId: Uuid, + api: Api, + models: ModelLibrary, +): Promise { + const { liveDoc, docRef } = await api.getLiveDoc(refId, "instance"); + const modelRefId = liveDoc.doc.instanceIn._id; + + const liveModel = await models.getLiveModel(modelRefId); + const liveInstance = enlivenInstanceDocument(liveDoc, liveModel); + return { liveInstance, docRef }; +} + +/** Get a instance from an Automerge repo and make it "live" for editing. + +Prefer [`getLiveInstance`] unless you're bypassing the official backend. + */ +export async function getLiveInstanceFromRepo( + docId: AnyDocumentId, + repo: Repo, + models: ModelLibrary, +): Promise { + const docHandle = await findAndMigrate(repo, docId); + const liveDoc = makeLiveDoc(docHandle, "instance"); + const modelDocId = liveDoc.doc.instanceIn._id as AnyDocumentId; + + const liveModel = await models.getLiveModel(modelDocId); + return enlivenInstanceDocument(liveDoc, liveModel); +} diff --git a/packages/frontend/src/instance/index.ts b/packages/frontend/src/instance/index.ts new file mode 100644 index 000000000..1e4041662 --- /dev/null +++ b/packages/frontend/src/instance/index.ts @@ -0,0 +1,2 @@ +export * from "./context"; +export * from "./document"; diff --git a/packages/frontend/src/instance/instance_editor.tsx b/packages/frontend/src/instance/instance_editor.tsx new file mode 100644 index 000000000..9586bbfbb --- /dev/null +++ b/packages/frontend/src/instance/instance_editor.tsx @@ -0,0 +1,51 @@ +import { MultiProvider } from "@solid-primitives/context"; +import { useContext } from "solid-js"; +import invariant from "tiny-invariant"; + +import type { InstanceJudgment } from "catcolab-document-types"; +import { type FocusHandle } from "catcolab-ui-components"; +import { LiveModelContext } from "../model"; +import { type FormalCellEditorProps, NotebookEditor } from "../notebook"; +import { LiveInstanceContext } from "./context"; +import type { LiveInstanceDoc } from "./document"; + +/** Notebook editor for a instance in a model. + */ +export function InstanceNotebookEditor(props: { + liveInstance: LiveInstanceDoc; + focus: FocusHandle; +}) { + const liveDoc = () => props.liveInstance.liveDoc; + const liveModel = () => props.liveInstance.liveModel; + + return ( + props.liveInstance], + ]} + > + { + liveDoc().changeDoc((doc) => f(doc.notebook)); + }} + formalCellEditor={InstanceCellEditor} + cellConstructors={undefined} + cellLabel={undefined} + focus={props.focus} + /> + + ); +} + +/** Editor for a notebook cell in a instance notebook. + */ +function InstanceCellEditor(_props: FormalCellEditorProps) { + const liveInstance = useContext(LiveInstanceContext); + invariant(liveInstance, "Live instance should be provided as context"); + + return <>; +} diff --git a/packages/frontend/src/instance/instance_info.tsx b/packages/frontend/src/instance/instance_info.tsx new file mode 100644 index 000000000..b14f32015 --- /dev/null +++ b/packages/frontend/src/instance/instance_info.tsx @@ -0,0 +1,20 @@ +import { A } from "@solidjs/router"; + +import type { LiveInstanceDoc } from "./document"; + +/** Widget in the top right corner of a instance document pane. + */ +export function InstanceInfo(props: { liveInstance: LiveInstanceDoc }) { + const liveModel = () => props.liveInstance.liveModel; + const liveModelDoc = () => props.liveInstance.liveModel.liveDoc; + const modelRefId = () => props.liveInstance.liveDoc.doc.instanceIn._id; + + return ( + <> +
{liveModel().theory()?.instanceOfName}
+ + + ); +} diff --git a/packages/frontend/src/model/model_library.ts b/packages/frontend/src/model/model_library.ts index cd3e068b1..286df4b14 100644 --- a/packages/frontend/src/model/model_library.ts +++ b/packages/frontend/src/model/model_library.ts @@ -316,7 +316,7 @@ function isPatchToFormalContent(doc: Document, patch: Patch): boolean { // Ignore changes to top-level data like document name. return false; } - if (path[0] === "notebook" && path[1] === "cellContents" && path[2]) { + if (path[0] === "notebook" && path[1] === "cellContents" && path[2] && "notebook" in doc) { // Ignores changes to cells without formal content. const cell = doc.notebook.cellContents[path[2]]; if (cell?.tag !== "formal") { diff --git a/packages/frontend/src/page/document_breadcrumbs.tsx b/packages/frontend/src/page/document_breadcrumbs.tsx index 0d47c70bb..8cf6230d4 100644 --- a/packages/frontend/src/page/document_breadcrumbs.tsx +++ b/packages/frontend/src/page/document_breadcrumbs.tsx @@ -39,6 +39,8 @@ export function getParentRefId(document: Document): string | null { return null; case "diagram": return document.diagramIn._id; + case "instance": + return document.instanceIn._id; case "analysis": return document.analysisOf._id; default: diff --git a/packages/frontend/src/page/document_menu.tsx b/packages/frontend/src/page/document_menu.tsx index 8e9e1e7ff..fc08f66e5 100644 --- a/packages/frontend/src/page/document_menu.tsx +++ b/packages/frontend/src/page/document_menu.tsx @@ -57,10 +57,29 @@ export function DocumentMenu(props: { handleDocCreated("diagram", newRef); }; + const onNewInstance = async () => { + let modelRefId: string | undefined; + switch (props.liveDoc.doc.type) { + case "instance": + modelRefId = props.liveDoc.doc.instanceIn._id; + invariant(modelRefId, "To create instance, parent model should have a ref ID"); + break; + case "model": + modelRefId = props.docRef.refId; + break; + default: + throw `Can't create instance for ${props.liveDoc.doc.type}`; + } + + const newRef = await createDiagram(api, api.makeUnversionedRef(modelRefId)); + handleDocCreated("diagram", newRef); + }; + const onNewAnalysis = async () => { const docRefId = props.docRef.refId; const docType = props.liveDoc.doc.type; invariant(docType !== "analysis", "Analysis cannot be created on other analysis"); + invariant(docType !== "instance", "Analysis cannot yet be created on instance"); const newRef = await createAnalysis(api, docType, api.makeUnversionedRef(docRefId)); handleDocCreated("analysis", newRef); @@ -106,6 +125,10 @@ export function DocumentMenu(props: { {"New diagram in this model"} + onNewInstance()}> + + {"New instance in this model"} + onNewDiagram()}> @@ -114,7 +137,12 @@ export function DocumentMenu(props: { - + onNewAnalysis()}> {`New analysis of this ${docType()}`} diff --git a/packages/frontend/src/page/document_page.tsx b/packages/frontend/src/page/document_page.tsx index 5ea18b825..7841430e9 100644 --- a/packages/frontend/src/page/document_page.tsx +++ b/packages/frontend/src/page/document_page.tsx @@ -36,6 +36,9 @@ import { type Api, type DocRef, type DocumentType, useApi } from "../api"; import { getLiveDiagram, type LiveDiagramDoc } from "../diagram"; import { DiagramNotebookEditor } from "../diagram/diagram_editor"; import { DiagramInfo } from "../diagram/diagram_info"; +import { getLiveInstance, type LiveInstanceDoc } from "../instance"; +import { InstanceNotebookEditor } from "../instance/instance_editor"; +import { InstanceInfo } from "../instance/instance_info"; import { type LiveModelDoc, type ModelLibrary, ModelLibraryContext } from "../model"; import { ModelNotebookEditor } from "../model/model_editor"; import { ModelDocumentHead } from "../model/model_info"; @@ -51,7 +54,7 @@ import { useSnapshotHistory } from "./use_snapshot_history"; import "./document_page.css"; -type AnyLiveDoc = LiveModelDoc | LiveDiagramDoc | LiveAnalysisDoc; +type AnyLiveDoc = LiveModelDoc | LiveDiagramDoc | LiveAnalysisDoc | LiveInstanceDoc; /** A Live*Document bundled with its backend DocRef. * @@ -505,6 +508,13 @@ export function DocumentPane(props: { )} + + {(liveInstance) => ( + + + + )} + {(liveAnalysis) => ( @@ -530,6 +540,14 @@ export function DocumentPane(props: { /> )} + + {(liveInstance) => ( + + )} + {(liveAnalysis) => ( + + + ); }