Skip to content
Open
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
8 changes: 6 additions & 2 deletions apps/roam/src/components/CreateRelationDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useRelationSchemaRevision } from "~/utils/relationSchemaChanges";
import React, { useState, useMemo } from "react";
import { Dialog, Classes, Label, Button, Callout } from "@blueprintjs/core";
import renderOverlay from "roamjs-components/util/renderOverlay";
Expand All @@ -8,6 +9,7 @@ import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageU
import getDiscourseRelations, {
type DiscourseRelation,
} from "~/utils/getDiscourseRelations";
import { excludeProvisionalRelationSchemas } from "~/utils/relationSchemaAcceptance";
import { createReifiedRelation } from "~/utils/createReifiedBlock";
import { getStoredRelationsEnabled } from "~/utils/storedRelations";
import findDiscourseNode from "~/utils/findDiscourseNode";
Expand Down Expand Up @@ -291,7 +293,7 @@ const prepareRelData = (
): RelWithDirection[] => {
nodeTitle = nodeTitle || getPageTitleByPageUid(targetNodeUid).trim();
const discourseNodeSchemas = getDiscourseNodes();
const relations = getDiscourseRelations();
const relations = excludeProvisionalRelationSchemas(getDiscourseRelations());
const nodeSchema = findDiscourseNode({
uid: targetNodeUid,
title: nodeTitle,
Expand Down Expand Up @@ -387,6 +389,7 @@ export const renderCreateRelationDialog = (
export const CreateRelationButton = (
props: CreateRelationDialogProps & { fill?: boolean },
): React.JSX.Element | null => {
useRelationSchemaRevision();
const { fill = false, ...relationProps } = props;
const storedRelationsEnabled = getStoredRelationsEnabled();
if (!storedRelationsEnabled) return null;
Expand All @@ -405,7 +408,8 @@ export const CreateRelationButton = (
text="Add relation"
disabled={extProps === null}
onClick={() => {
renderCreateRelationDialog(extProps);
// A schema may have been accepted since this button last rendered.
renderCreateRelationDialog(relationProps);
Comment thread
sid597 marked this conversation as resolved.
}}
/>
);
Expand Down
10 changes: 9 additions & 1 deletion apps/roam/src/components/SuggestionsBody.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useRelationSchemaRevision } from "~/utils/relationSchemaChanges";
import React, { useMemo, useState, useEffect, useCallback } from "react";
import {
Button,
Expand All @@ -21,6 +22,7 @@ import getDiscourseContextResults from "~/utils/getDiscourseContextResults";
import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle";
import findDiscourseNode from "~/utils/findDiscourseNode";
import getDiscourseRelations from "~/utils/getDiscourseRelations";
import { excludeProvisionalRelationSchemas } from "~/utils/relationSchemaAcceptance";
import getDiscourseNodes from "~/utils/getDiscourseNodes";
import normalizePageTitle from "roamjs-components/queries/normalizePageTitle";
import { type RelationDetails } from "~/utils/hyde";
Expand Down Expand Up @@ -232,7 +234,13 @@ const SuggestionsBody = ({
() => findDiscourseNode({ uid: tagUid }),
[tagUid],
);
const allRelations = useMemo(() => getDiscourseRelations(), []);
const relationSchemaRevision = useRelationSchemaRevision();
const allRelations = useMemo(
() => excludeProvisionalRelationSchemas(getDiscourseRelations()),
// Acceptance and deletion invalidate the relation data stored outside React.
// eslint-disable-next-line react-hooks/exhaustive-deps
[relationSchemaRevision],
);
const allNodes = useMemo(() => getDiscourseNodes(), []);

const validRelations = useMemo(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import {
DiscourseRelationShape,
getRelationColor,
} from "./DiscourseRelationUtil";
import { discourseContext } from "~/components/canvas/Tldraw";
import {
discourseContext,
isAcceptedRelationSchema,
} from "~/components/canvas/Tldraw";
import { dispatchToastEvent } from "~/components/canvas/ToastListener";
import { isRelationComplete } from "~/utils/isRelationComplete";
import {
Expand Down Expand Up @@ -350,7 +353,9 @@ export const createAllRelationShapeTools = (
override onEnter = () => {
this.didTimeout = false;

const selectedRelations = discourseContext.relations[name] || [];
const selectedRelations = (
discourseContext.relations[name] || []
).filter(isAcceptedRelationSchema);
const hasIncompleteSelectedRelation = selectedRelations.some(
(relation) => !isRelationComplete(relation),
);
Expand Down Expand Up @@ -384,15 +389,15 @@ export const createAllRelationShapeTools = (
target && isDiscourseNodeShape(target)
? getDiscourseNodeTypeId({ shape: target })
: undefined;
const relation = discourseContext.relations[name].find(
const relation = selectedRelations.find(
(r) =>
r.source === targetNodeTypeId ||
r.destination === targetNodeTypeId,
);
if (relation) {
this.shapeType = relation.id;
} else {
const acceptableTypes = discourseContext.relations[name]
const acceptableTypes = selectedRelations
.flatMap((r) => [
discourseContext.nodes[r.source]?.text,
discourseContext.nodes[r.destination]?.text,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,11 @@ import {
import { createReifiedRelation } from "~/utils/createReifiedBlock";
import { getStoredRelationsEnabled } from "~/utils/storedRelations";
import type { DiscourseRelation } from "~/utils/getDiscourseRelations";
import { discourseContext, isPageUid } from "~/components/canvas/Tldraw";
import {
discourseContext,
isAcceptedRelationSchema,
isPageUid,
} from "~/components/canvas/Tldraw";
import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle";

/**
Expand Down Expand Up @@ -659,11 +663,11 @@ export const createAllRelationShapeUtils = (
isDirect,
isReverse,
matchingRelation: foundRelation,
} = this.checkConnectionTypeAcrossLabel(
relation.label,
} = this.checkConnectionTypeAcrossLabel({
label: relation.label,
sourceNodeType,
targetNodeType,
);
});
const matchingRelation = foundRelation ?? relation;

if (!isDirect && !isReverse) {
Expand Down Expand Up @@ -1036,11 +1040,11 @@ export const createAllRelationShapeUtils = (
const endNodeType = getDiscourseNodeTypeId({ shape: endNode });

const { isReverse, matchingRelation } =
this.checkConnectionTypeAcrossLabel(
relation.label,
startNodeType,
endNodeType,
);
this.checkConnectionTypeAcrossLabel({
label: relation.label,
sourceNodeType: startNodeType,
targetNodeType: endNodeType,
});

const effectiveRelation = matchingRelation ?? relation;

Expand Down Expand Up @@ -1758,16 +1762,24 @@ export class BaseDiscourseRelationUtil extends ShapeUtil<DiscourseRelationShape>
return checkConnectionType(relation, sourceNodeType, targetNodeType);
}

checkConnectionTypeAcrossLabel(
label: string,
sourceNodeType: string,
targetNodeType: string,
): {
checkConnectionTypeAcrossLabel({
label,
sourceNodeType,
targetNodeType,
includeProvisional,
}: {
label: string;
sourceNodeType: string;
targetNodeType: string;
includeProvisional?: boolean;
}): {
isDirect: boolean;
isReverse: boolean;
matchingRelation: DiscourseRelation | null;
} {
const relationsWithLabel = discourseContext.relations[label];
const relationsWithLabel = includeProvisional
? discourseContext.relations[label]
: discourseContext.relations[label]?.filter(isAcceptedRelationSchema);
if (!relationsWithLabel) {
return { isDirect: false, isReverse: false, matchingRelation: null };
}
Expand All @@ -1787,7 +1799,9 @@ export class BaseDiscourseRelationUtil extends ShapeUtil<DiscourseRelationShape>
}

getValidTargetTypes(label: string, sourceNodeType: string): string[] {
const relationsWithLabel = discourseContext.relations[label];
const relationsWithLabel = discourseContext.relations[label]?.filter(
isAcceptedRelationSchema,
);
if (!relationsWithLabel) return [];

const targets = new Set<string>();
Expand All @@ -1807,11 +1821,14 @@ export class BaseDiscourseRelationUtil extends ShapeUtil<DiscourseRelationShape>
const relation = relations.find((r) => r.id === relationId);
if (!relation) return false;

const { isDirect, isReverse } = this.checkConnectionTypeAcrossLabel(
relation.label,
// Validates handle drags of arrows that already exist, so provisional
// relations stay re-bindable; only creation paths filter them out.
const { isDirect, isReverse } = this.checkConnectionTypeAcrossLabel({
label: relation.label,
sourceNodeType,
targetNodeType,
);
includeProvisional: true,
});
return isDirect || isReverse;
}

Expand Down
47 changes: 44 additions & 3 deletions apps/roam/src/components/canvas/Tldraw.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import {
isRelationSchemaDeleted,
useRelationSchemaRevision,
} from "~/utils/relationSchemaChanges";
import React, {
useState,
useRef,
Expand Down Expand Up @@ -120,6 +124,7 @@ import posthog from "posthog-js";
import { getPersonalSetting } from "~/components/settings/utils/accessors";
import { PERSONAL_KEYS } from "~/components/settings/utils/settingKeys";
import { json, normalizeProps } from "~/utils/getBlockProps";
import { isProvisionalRelationSchema } from "~/utils/relationSchemaAcceptance";
import { onPageRefObserverChange } from "~/utils/pageRefObserverHandlers";

declare global {
Expand All @@ -133,17 +138,25 @@ export type DiscourseContextType = {
nodes: Record<string, DiscourseNode & { index: number }>;
// { [Relation.Label] => DiscourseRelation[] }
relations: Record<string, DiscourseRelation[]>;
// Imported, not-yet-accepted relation schemas; excluded from relation
// creation but kept in `relations` so existing shapes still render.
provisionalRelationIds: Set<string>;
lastAppEvent: string;
lastActions: HistoryEntry<TLRecord>[];
};

export const discourseContext: DiscourseContextType = {
nodes: {},
relations: {},
provisionalRelationIds: new Set(),
lastAppEvent: "",
lastActions: [],
};

export const isAcceptedRelationSchema = (relation: { id: string }): boolean =>
!discourseContext.provisionalRelationIds.has(relation.id) &&
!isRelationSchemaDeleted(relation.id);

let activeCanvasPageUid: string | null = null;
let activeCanvasEditor: Editor | null = null;

Expand Down Expand Up @@ -766,6 +779,7 @@ const TldrawCanvasShared = ({
},
{} as Record<string, DiscourseRelation[]>,
);

return relations;
}, []);
const allRelationsById = useMemo(() => {
Expand All @@ -777,9 +791,34 @@ const TldrawCanvasShared = ({
const allRelationIds = useMemo(() => {
return Object.keys(allRelationsById);
}, [allRelationsById]);
const relationSchemaRevision = useRelationSchemaRevision();
const registeredRelationNames = useMemo(
() => [...new Set(allRelations.map((relation) => relation.label))],
[allRelations],
);
const allRelationNames = useMemo(() => {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A label drops out of the canvas toolbar only when every relation carrying it is provisional. A label with one local and one imported relation keeps its tool, and the endpoint-matching step picks from the accepted list only.

return Object.keys(discourseContext.relations);
}, []);
discourseContext.provisionalRelationIds = new Set(
allRelations
.filter((r) => isProvisionalRelationSchema(r.id))
.map((r) => r.id),
);
return Object.entries(discourseContext.relations)
.filter(([, relations]) => relations.some(isAcceptedRelationSchema))
.map(([name]) => name);
// Acceptance and deletion invalidate the relation data stored outside React.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [allRelations, relationSchemaRevision]);
useEffect(() => {
const editor = appRef.current;
if (!editor) return;
const tool = editor.getCurrentToolId();
if (
registeredRelationNames.includes(tool) &&
!allRelationNames.includes(tool)
) {
editor.setCurrentTool("select");
}
}, [allRelationNames, registeredRelationNames]);
const allNodes = useMemo(() => {
const allNodes = getDiscourseNodes();
discourseContext.nodes = Object.fromEntries(
Expand Down Expand Up @@ -1025,7 +1064,9 @@ const TldrawCanvasShared = ({
static override isLockable = true;
};
const discourseNodeTools = createNodeShapeTools(allNodes);
const discourseRelationTools = createAllRelationShapeTools(allRelationNames);
const discourseRelationTools = createAllRelationShapeTools(
registeredRelationNames,
);
const referencedNodeTools = createAllReferencedNodeTools(
allAddReferencedNodeByAction,
);
Expand Down
13 changes: 9 additions & 4 deletions apps/roam/src/components/canvas/canvasUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import {
DiscourseNodeUtil,
DiscourseNodeShape,
} from "~/components/canvas/DiscourseNodeUtil";
import { discourseContext } from "~/components/canvas/Tldraw";
import {
discourseContext,
isAcceptedRelationSchema,
} from "~/components/canvas/Tldraw";

export const isDiscourseNodeShape = (
editor: Editor,
Expand All @@ -16,8 +19,10 @@ export const isDiscourseNodeShape = (
}
};

export const getAllRelations = () =>
Object.values(discourseContext.relations).flat();
export const getCreatableRelations = () =>
Object.values(discourseContext.relations)
.flat()
.filter(isAcceptedRelationSchema);

export const checkConnectionType = (
relation: { source: string; destination: string },
Expand All @@ -36,7 +41,7 @@ export const hasValidRelationTypes = (
sourceNodeType: string,
targetNodeType: string,
): boolean =>
getAllRelations().some(
getCreatableRelations().some(
(r) =>
(r.source === sourceNodeType && r.destination === targetNodeType) ||
(r.source === targetNodeType && r.destination === sourceNodeType),
Expand Down
8 changes: 5 additions & 3 deletions apps/roam/src/components/canvas/overlays/relationCreation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { createOrUpdateArrowBinding } from "~/components/canvas/DiscourseRelatio
import { getDiscourseNodeTypeId } from "~/components/canvas/DiscourseNodeUtil";
import {
checkConnectionType,
getAllRelations,
getCreatableRelations,
isDiscourseNodeShape,
} from "~/components/canvas/canvasUtils";
import type { DiscourseRelation } from "~/utils/getDiscourseRelations";
Expand Down Expand Up @@ -93,7 +93,7 @@ export const getValidRelationTypesBetween = (
const validTypes: RelationTypeOption[] = [];
const seenLabels = new Set<string>();

for (const relation of getAllRelations()) {
for (const relation of getCreatableRelations()) {
if (!isRelationComplete(relation)) continue;
const { isDirect, isReverse } = checkConnectionType(
relation,
Expand Down Expand Up @@ -129,7 +129,9 @@ export const createDefaultRelationBetweenNodes = async ({
sourceId: TLShapeId;
targetId: TLShapeId;
}): Promise<TLShapeId | null> => {
const selectedRelation = getAllRelations().find((r) => r.id === relationId);
const selectedRelation = getCreatableRelations().find(
(r) => r.id === relationId,
);
if (!selectedRelation) return null;

const sourceNode = editor.getShape(sourceId);
Expand Down
6 changes: 4 additions & 2 deletions apps/roam/src/components/canvas/uiOverrides.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ import {
getValidRelationTypesBetween,
persistRelationArrow,
} from "./overlays/relationCreation";
import { getAllRelations } from "./canvasUtils";
import { getCreatableRelations } from "./canvasUtils";
import { createOrUpdateArrowBinding } from "./DiscourseRelationShape/helpers";
import DiscourseGraphPanel from "./DiscourseToolPanel";
import type { CanvasNodeShortcuts } from "~/components/settings/utils/zodSchema";
Expand Down Expand Up @@ -299,7 +299,9 @@ const convertArrowToRelation = async ({
const boundNodes = getArrowBoundNodeInfo(editor, arrow);
if (!boundNodes) return null;

const selectedRelation = getAllRelations().find((r) => r.id === relationId);
const selectedRelation = getCreatableRelations().find(
(r) => r.id === relationId,
);
if (!selectedRelation) return null;

const sourceNode = editor.getShape(boundNodes.startId);
Expand Down
Loading