Skip to content

Commit 3e60913

Browse files
ENG-839: Guard incomplete relation types in canvas (#1095)
* ENG-839: Guard incomplete relation types in canvas Prevent crashes and confusing warnings when relation definitions are missing required fields, and guide users to settings to fix relation configuration. Co-authored-by: Cursor <cursoragent@cursor.com> * cleanup * ENG-839: Remove dead validation from referenced node tools Referenced node tools are keyed by action names, not relation labels, so the incomplete-relation check never ran there. Co-authored-by: Cursor <cursoragent@cursor.com> * ENG-839: Treat placeholder relation fields as incomplete in dropdown Skip relations with "?" source/destination/complement values so the dropdown matches relation tool validation. Co-authored-by: Cursor <cursoragent@cursor.com> * use DRY check * address type checks --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent e21a45d commit 3e60913

4 files changed

Lines changed: 43 additions & 2 deletions

File tree

apps/roam/src/components/canvas/DiscourseRelationShape/DiscourseRelationTool.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from "./DiscourseRelationUtil";
1111
import { discourseContext } from "~/components/canvas/Tldraw";
1212
import { dispatchToastEvent } from "~/components/canvas/ToastListener";
13+
import { isRelationComplete } from "~/utils/isRelationComplete";
1314

1415
export type AddReferencedNodeType = Record<string, ReferenceFormatType[]>;
1516
type ReferenceFormatType = {
@@ -341,6 +342,17 @@ export const createAllRelationShapeTools = (
341342
override onEnter = () => {
342343
this.didTimeout = false;
343344

345+
const selectedRelations = discourseContext.relations[name] || [];
346+
const hasIncompleteSelectedRelation = selectedRelations.some(
347+
(relation) => !isRelationComplete(relation),
348+
);
349+
if (hasIncompleteSelectedRelation) {
350+
this.cancelAndWarn(
351+
"Relation type is incomplete. Set label, complement, source, and destination in settings.",
352+
);
353+
return;
354+
}
355+
344356
const target = this.editor.getShapeAtPoint(
345357
this.editor.inputs.currentPagePoint,
346358
// {

apps/roam/src/components/canvas/overlays/RelationTypeDropdown.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
getAllRelations,
77
isDiscourseNodeShape,
88
} from "~/components/canvas/canvasUtils";
9+
import { isRelationComplete } from "~/utils/isRelationComplete";
910

1011
type RelationTypeDropdownProps = {
1112
sourceId: TLShapeId;
@@ -47,6 +48,7 @@ export const RelationTypeDropdown = ({
4748
const seenLabels = new Set<string>();
4849

4950
for (const relation of allRelations) {
51+
if (!isRelationComplete(relation)) continue;
5052
const { isDirect: isForward, isReverse } = checkConnectionType(
5153
relation,
5254
startNodeType,

apps/roam/src/components/settings/DiscourseRelationConfigPanel.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageU
4444
import updateBlock from "roamjs-components/writes/updateBlock";
4545
import getTextByBlockUid from "roamjs-components/queries/getTextByBlockUid";
4646
import getDiscourseNodes from "~/utils/getDiscourseNodes";
47+
import { isRelationComplete } from "~/utils/isRelationComplete";
4748
import { getConditionLabels } from "~/utils/conditionToDatalog";
4849
import { formatHexColor } from "./DiscourseNodeCanvasSettings";
4950
import posthog from "posthog-js";
@@ -516,7 +517,14 @@ export const RelationEditPanel = ({
516517
const relationEl = document.getElementById("relation-label");
517518
relationEl?.focus();
518519
}
519-
}, []);
520+
}, [label]);
521+
522+
const isEditingRelationComplete = isRelationComplete({
523+
label,
524+
complement,
525+
source,
526+
destination,
527+
});
520528

521529
return (
522530
<>
@@ -535,7 +543,7 @@ export const RelationEditPanel = ({
535543
icon={"floppy-disk"}
536544
text={"Save"}
537545
intent={Intent.PRIMARY}
538-
disabled={loading || !hasChanges}
546+
disabled={loading || !hasChanges || !isEditingRelationComplete}
539547
className="select-none"
540548
onClick={() => {
541549
setLoading(true);
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { DiscourseRelation } from "./getDiscourseRelations";
2+
3+
const PLACEHOLDER_VALUES = new Set(["?"]);
4+
5+
const isNonEmptyNonPlaceholder = (
6+
value: string | null | undefined,
7+
): boolean => {
8+
if (!value) return false;
9+
const trimmed = value.trim();
10+
return trimmed.length > 0 && !PLACEHOLDER_VALUES.has(trimmed);
11+
};
12+
13+
export const isRelationComplete = (
14+
relation: Partial<DiscourseRelation>,
15+
): boolean =>
16+
isNonEmptyNonPlaceholder(relation.label) &&
17+
isNonEmptyNonPlaceholder(relation.complement) &&
18+
isNonEmptyNonPlaceholder(relation.source) &&
19+
isNonEmptyNonPlaceholder(relation.destination);

0 commit comments

Comments
 (0)