Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/document-methods/src/index.ts
Original file line number Diff line number Diff line change
@@ -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";
18 changes: 18 additions & 0 deletions packages/document-methods/src/instance.ts
Original file line number Diff line number Diff line change
@@ -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<InstanceJudgment>(),
version: currentVersion(),
});
3 changes: 3 additions & 0 deletions packages/document-types/src/v0/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ pub enum LinkType {
#[serde(rename = "diagram-in")]
DiagramIn,

#[serde(rename = "instance-in")]
InstanceIn,

#[serde(rename = "instantiation")]
Instantiation,
}
Expand Down
12 changes: 12 additions & 0 deletions packages/document-types/src/v2/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<super::instance_judgment::InstanceJudgment>,
pub version: String,
}

#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Tsify)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct AnalysisDocumentContent {
Expand All @@ -58,6 +68,8 @@ pub enum Document {
Diagram(DiagramDocumentContent),
#[serde(rename = "analysis")]
Analysis(AnalysisDocumentContent),
#[serde(rename = "instance")]
Instance(InstanceDocumentContent),
}

impl Document {
Expand Down
35 changes: 35 additions & 0 deletions packages/document-types/src/v2/instance.rs
Original file line number Diff line number Diff line change
@@ -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<Uuid, CellValue>,
}

#[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<TableRow>,
}
11 changes: 11 additions & 0 deletions packages/document-types/src/v2/instance_judgment.rs
Original file line number Diff line number Diff line change
@@ -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,
}
2 changes: 2 additions & 0 deletions packages/document-types/src/v2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand Down
6 changes: 6 additions & 0 deletions packages/frontend/src/instance/context.ts
Original file line number Diff line number Diff line change
@@ -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<Accessor<LiveInstanceDoc>>();
83 changes: 83 additions & 0 deletions packages/frontend/src/instance/document.ts
Original file line number Diff line number Diff line change
@@ -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<InstanceDocument>;

/** Live model that the instance is in. */
liveModel: LiveModelDoc;

/** A memo of the formal content of the model. */
formalJudgments: Accessor<Array<InstanceJudgment>>;
};

export function enlivenInstanceDocument(
liveDoc: LiveDoc<InstanceDocument>,
liveModel: LiveModelDoc,
): LiveInstanceDoc {
const { doc } = liveDoc;

const formalJudgments = createMemo<Array<InstanceJudgment>>(() =>
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<string> {
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<Uuid>,
): Promise<LiveInstanceDocWithRef> {
const { liveDoc, docRef } = await api.getLiveDoc<InstanceDocument>(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<AnyDocumentId>,
): Promise<LiveInstanceDoc> {
const docHandle = await findAndMigrate(repo, docId);
const liveDoc = makeLiveDoc<InstanceDocument>(docHandle, "instance");
const modelDocId = liveDoc.doc.instanceIn._id as AnyDocumentId;

const liveModel = await models.getLiveModel(modelDocId);
return enlivenInstanceDocument(liveDoc, liveModel);
}
2 changes: 2 additions & 0 deletions packages/frontend/src/instance/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./context";
export * from "./document";
51 changes: 51 additions & 0 deletions packages/frontend/src/instance/instance_editor.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<MultiProvider
values={[
[LiveModelContext, liveModel],
[LiveInstanceContext, () => props.liveInstance],
]}
>
<NotebookEditor
handle={liveDoc().docHandle}
path={["notebook"]}
notebook={liveDoc().doc.notebook}
changeNotebook={(f) => {
liveDoc().changeDoc((doc) => f(doc.notebook));
}}
formalCellEditor={InstanceCellEditor}
cellConstructors={undefined}
cellLabel={undefined}
focus={props.focus}
/>
</MultiProvider>
);
}

/** Editor for a notebook cell in a instance notebook.
*/
function InstanceCellEditor(_props: FormalCellEditorProps<InstanceJudgment>) {
const liveInstance = useContext(LiveInstanceContext);
invariant(liveInstance, "Live instance should be provided as context");

return <></>;
}
20 changes: 20 additions & 0 deletions packages/frontend/src/instance/instance_info.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<div class="name">{liveModel().theory()?.instanceOfName}</div>
<div class="model">
<A href={`/model/${modelRefId()}`}>{liveModelDoc().doc.name || "Untitled"}</A>
</div>
</>
);
}
2 changes: 1 addition & 1 deletion packages/frontend/src/model/model_library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
2 changes: 2 additions & 0 deletions packages/frontend/src/page/document_breadcrumbs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
30 changes: 29 additions & 1 deletion packages/frontend/src/page/document_menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -106,6 +125,10 @@ export function DocumentMenu(props: {
<DocumentTypeIcon documentType="diagram" />
<MenuItemLabel>{"New diagram in this model"}</MenuItemLabel>
</MenuItem>
<MenuItem onSelect={() => onNewInstance()}>
<DocumentTypeIcon documentType="instance" />
<MenuItemLabel>{"New instance in this model"}</MenuItemLabel>
</MenuItem>
</Match>
<Match when={docType() === "diagram"}>
<MenuItem onSelect={() => onNewDiagram()}>
Expand All @@ -114,7 +137,12 @@ export function DocumentMenu(props: {
</MenuItem>
</Match>
</Switch>
<Show when={props.liveDoc.doc.type !== "analysis"}>
<Show
when={
props.liveDoc.doc.type !== "analysis" &&
props.liveDoc.doc.type !== "instance"
}
>
<MenuItem onSelect={() => onNewAnalysis()}>
<DocumentTypeIcon documentType="analysis" />
<MenuItemLabel>{`New analysis of this ${docType()}`}</MenuItemLabel>
Expand Down
Loading
Loading