Skip to content

Commit ad6afb8

Browse files
trangdoan982claude
andauthored
ENG-1611 Add provisional status to imported relation schemas (#948)
* ENG-1611 Add provisional status to imported relation schemas Mark newly imported relation types and triplets as provisional so they don't appear in creation UIs until explicitly accepted by the user. - Add ImportStatus type and status? field to DiscourseRelationType and DiscourseRelation - Add isAcceptedSchema / isProvisionalSchema helpers (imported + no status = provisional) - Set status: "provisional" on all newly imported relation types and triplets - Fix provisional: true on imported relation instances (was incorrectly false) - Settings UI: show Provisional badge + Accept/Delete buttons for provisional imported schemas - Accepting a triplet cascades to also accept its relation type - Filter provisional schemas from all creation panels (canvas tool, RelationshipSection, RelationPanel) - Guard publishNewRelation and sync against provisional schemas - Prevent status from leaking into literal_content.source_data in conceptConversion Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * lint * Fix tentative semantics and missing provisional filter - tentative: false = unreviewed (not tentative: true); fix importRelations, publishNode guard, and syncDgNodesToSupabase filter accordingly - Add isAcceptedSchema to compatible node types useEffect in RelationshipSection Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * add guards in more relation creation flows --------- Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent 114ed85 commit ad6afb8

13 files changed

Lines changed: 176 additions & 26 deletions

apps/obsidian/src/components/ModifyNodeModal.tsx

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
import { DiscourseNode } from "~/types";
1212
import type DiscourseGraphPlugin from "~/index";
1313
import { QueryEngine } from "~/services/QueryEngine";
14+
import { isProvisionalSchema } from "~/utils/typeUtils";
1415

1516
type ModifyNodeFormProps = {
1617
nodeTypes: DiscourseNode[];
@@ -159,21 +160,25 @@ export const ModifyNodeForm = ({
159160
return [];
160161
}
161162

162-
// Find all relations that connect the current node type to the selected node type
163+
// Find all accepted relations that connect the current node type to the selected node type
163164
const relevantRelations = plugin.settings.discourseRelations.filter(
164-
(relation) =>
165-
(relation.sourceId === currentNodeTypeId &&
166-
relation.destinationId === selectedNodeType.id) ||
167-
(relation.sourceId === selectedNodeType.id &&
168-
relation.destinationId === currentNodeTypeId),
165+
(relation) => {
166+
if (isProvisionalSchema(relation)) return false;
167+
return (
168+
(relation.sourceId === currentNodeTypeId &&
169+
relation.destinationId === selectedNodeType.id) ||
170+
(relation.sourceId === selectedNodeType.id &&
171+
relation.destinationId === currentNodeTypeId)
172+
);
173+
},
169174
);
170175

171176
const relations = relevantRelations
172177
.map((relation) => {
173178
const relationType = plugin.settings.relationTypes.find(
174179
(rt) => rt.id === relation.relationshipTypeId,
175180
);
176-
if (!relationType) return null;
181+
if (!relationType || isProvisionalSchema(relationType)) return null;
177182

178183
const isCurrentFileSource = relation.sourceId === currentNodeTypeId;
179184
return {

apps/obsidian/src/components/RelationshipSection.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ import SearchBar from "./SearchBar";
1111
import { DiscourseNode } from "~/types";
1212
import DropdownSelect from "./DropdownSelect";
1313
import { usePlugin } from "./PluginContext";
14-
import { getNodeTypeById, getAndFormatImportSource } from "~/utils/typeUtils";
14+
import {
15+
getNodeTypeById,
16+
getAndFormatImportSource,
17+
isAcceptedSchema,
18+
} from "~/utils/typeUtils";
1519
import type { RelationInstance } from "~/types";
1620
import {
1721
getNodeInstanceIdForFile,
@@ -72,6 +76,7 @@ const AddRelationship = ({
7276

7377
const relations = plugin.settings.discourseRelations.filter(
7478
(relation) =>
79+
isAcceptedSchema(relation) &&
7580
relation.relationshipTypeId === selectedRelationType.id &&
7681
(selectedRelationType.isSource
7782
? relation.sourceId === activeNodeTypeId
@@ -100,8 +105,9 @@ const AddRelationship = ({
100105

101106
const relevantRelations = plugin.settings.discourseRelations.filter(
102107
(relation) =>
103-
relation.sourceId === activeNodeTypeId ||
104-
relation.destinationId === activeNodeTypeId,
108+
isAcceptedSchema(relation) &&
109+
(relation.sourceId === activeNodeTypeId ||
110+
relation.destinationId === activeNodeTypeId),
105111
);
106112

107113
relevantRelations.forEach((relation) => {

apps/obsidian/src/components/RelationshipSettings.tsx

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {
77
getNodeTypeById,
88
getImportInfo,
99
formatImportSource,
10+
isAcceptedSchema,
11+
isProvisionalSchema,
1012
} from "~/utils/typeUtils";
1113
import generateUid from "~/utils/generateUid";
1214

@@ -25,7 +27,7 @@ const RelationshipSettings = () => {
2527

2628
type EditableFieldKey = keyof Omit<
2729
DiscourseRelation,
28-
"id" | "modified" | "created" | "importedFromRid"
30+
"id" | "modified" | "created" | "importedFromRid" | "status"
2931
>;
3032

3133
const saveSettings = (relations: DiscourseRelation[]): void => {
@@ -136,6 +138,33 @@ const RelationshipSettings = () => {
136138
modal.open();
137139
};
138140

141+
const handleAcceptRelation = async (index: number): Promise<void> => {
142+
const updatedRelations = [...discourseRelations];
143+
const relation = updatedRelations[index];
144+
if (!relation) return;
145+
updatedRelations[index] = { ...relation, status: "accepted" };
146+
147+
// Cascade: also accept the relation type if it is still provisional
148+
const updatedRelationTypes = [...plugin.settings.relationTypes];
149+
const relTypeIndex = updatedRelationTypes.findIndex(
150+
(rt) => rt.id === relation.relationshipTypeId,
151+
);
152+
if (
153+
relTypeIndex >= 0 &&
154+
isProvisionalSchema(updatedRelationTypes[relTypeIndex]!)
155+
) {
156+
updatedRelationTypes[relTypeIndex] = {
157+
...updatedRelationTypes[relTypeIndex]!,
158+
status: "accepted",
159+
};
160+
plugin.settings.relationTypes = updatedRelationTypes;
161+
}
162+
163+
setDiscourseRelations(updatedRelations);
164+
plugin.settings.discourseRelations = updatedRelations;
165+
await plugin.saveSettings();
166+
};
167+
139168
const handleDeleteRelation = async (index: number): Promise<void> => {
140169
const updatedRelations = discourseRelations.filter((_, i) => i !== index);
141170
setDiscourseRelations(updatedRelations);
@@ -154,6 +183,10 @@ const RelationshipSettings = () => {
154183
const renderRelationItem = (relation: DiscourseRelation, index: number) => {
155184
const importInfo = getImportInfo(relation.importedFromRid);
156185
const isImported = importInfo.isImported;
186+
const isProvisional = isProvisionalSchema(relation);
187+
const spaceName = importInfo.spaceUri
188+
? formatImportSource(importInfo.spaceUri, plugin.settings.spaceNames)
189+
: "imported space";
157190
const error = errors[index];
158191

159192
return (
@@ -189,7 +222,10 @@ const RelationshipSettings = () => {
189222
disabled={isImported}
190223
>
191224
<option value="">Relation Type</option>
192-
{plugin.settings.relationTypes.map((relType) => (
225+
{(isImported
226+
? plugin.settings.relationTypes
227+
: plugin.settings.relationTypes.filter(isAcceptedSchema)
228+
).map((relType) => (
193229
<option key={relType.id} value={relType.id}>
194230
{relType.label} / {relType.complement}
195231
</option>
@@ -212,7 +248,25 @@ const RelationshipSettings = () => {
212248
))}
213249
</select>
214250

215-
{!isImported && (
251+
{isImported ? (
252+
<div className="flex gap-2">
253+
{isProvisional && (
254+
<button
255+
onClick={() => void handleAcceptRelation(index)}
256+
className="p-2"
257+
title={`Accept this relation triplet from ${spaceName} to create instances of this relation`}
258+
>
259+
Accept
260+
</button>
261+
)}
262+
<button
263+
onClick={() => confirmDeleteRelation(index)}
264+
className="mod-warning p-2"
265+
>
266+
Delete
267+
</button>
268+
</div>
269+
) : (
216270
<button
217271
onClick={() => confirmDeleteRelation(index)}
218272
className="mod-warning p-2"
@@ -224,6 +278,11 @@ const RelationshipSettings = () => {
224278
{error && <div className="text-error text-xs">{error}</div>}
225279
{isImported && (
226280
<div className="text-muted flex items-center gap-2 text-xs">
281+
{isProvisional && (
282+
<span className="rounded bg-yellow-100 px-1.5 py-0.5 text-xs font-medium text-yellow-800">
283+
Provisional
284+
</span>
285+
)}
227286
{importInfo.spaceUri && (
228287
<span>
229288
from{" "}

apps/obsidian/src/components/RelationshipTypeSettings.tsx

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ import {
1212
type TldrawColorName,
1313
} from "~/utils/tldrawColors";
1414
import { getContrastColor } from "~/utils/colorUtils";
15-
import { getImportInfo, formatImportSource } from "~/utils/typeUtils";
15+
import {
16+
getImportInfo,
17+
formatImportSource,
18+
isProvisionalSchema,
19+
} from "~/utils/typeUtils";
1620

1721
type ColorPickerProps = {
1822
value: string;
@@ -105,7 +109,7 @@ const RelationshipTypeSettings = () => {
105109

106110
type EditableFieldKey = keyof Omit<
107111
DiscourseRelationType,
108-
"id" | "modified" | "created" | "importedFromRid"
112+
"id" | "modified" | "created" | "importedFromRid" | "status"
109113
>;
110114

111115
const saveSettings = (updatedRelationTypes: DiscourseRelationType[]) => {
@@ -242,6 +246,16 @@ const RelationshipTypeSettings = () => {
242246
new Notice("Relation type deleted successfully");
243247
};
244248

249+
const handleAcceptRelationType = async (index: number): Promise<void> => {
250+
const updatedRelationTypes = [...relationTypes];
251+
const relType = updatedRelationTypes[index];
252+
if (!relType) return;
253+
updatedRelationTypes[index] = { ...relType, status: "accepted" };
254+
setRelationTypes(updatedRelationTypes);
255+
plugin.settings.relationTypes = updatedRelationTypes;
256+
await plugin.saveSettings();
257+
};
258+
245259
const localRelationTypes = relationTypes.filter(
246260
(relationType) => !relationType.importedFromRid,
247261
);
@@ -255,6 +269,10 @@ const RelationshipTypeSettings = () => {
255269
) => {
256270
const importInfo = getImportInfo(relationType.importedFromRid);
257271
const isImported = importInfo.isImported;
272+
const isProvisional = isProvisionalSchema(relationType);
273+
const spaceName = importInfo.spaceUri
274+
? formatImportSource(importInfo.spaceUri, plugin.settings.spaceNames)
275+
: "imported space";
258276

259277
const error = errors[index];
260278

@@ -291,7 +309,25 @@ const RelationshipTypeSettings = () => {
291309
}
292310
disabled={isImported}
293311
/>
294-
{!isImported && (
312+
{isImported ? (
313+
<div className="flex gap-2">
314+
{isProvisional && (
315+
<button
316+
onClick={() => void handleAcceptRelationType(index)}
317+
className="p-2"
318+
title={`Accept this relation type from ${spaceName} to create relations of this type`}
319+
>
320+
Accept
321+
</button>
322+
)}
323+
<button
324+
onClick={() => confirmDeleteRelationType(index)}
325+
className="mod-warning p-2"
326+
>
327+
Delete
328+
</button>
329+
</div>
330+
) : (
295331
<button
296332
onClick={() => confirmDeleteRelationType(index)}
297333
className="mod-warning p-2"
@@ -303,6 +339,11 @@ const RelationshipTypeSettings = () => {
303339
{error && <div className="text-error text-xs">{error}</div>}
304340
{isImported && (
305341
<div className="text-muted flex items-center gap-2 text-xs">
342+
{isProvisional && (
343+
<span className="rounded bg-yellow-100 px-1.5 py-0.5 text-xs font-medium text-yellow-800">
344+
Provisional
345+
</span>
346+
)}
306347
{importInfo.spaceUri && (
307348
<span>
308349
from{" "}

apps/obsidian/src/components/canvas/DiscourseRelationTool.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { StateNode, TLEventHandlers, TLStateNodeConstructor } from "tldraw";
22
import { createShapeId } from "tldraw";
33
import type { TFile } from "obsidian";
44
import DiscourseGraphPlugin from "~/index";
5-
import { getRelationTypeById } from "~/utils/typeUtils";
5+
import { getRelationTypeById, isAcceptedSchema } from "~/utils/typeUtils";
66
import { DiscourseRelationShape } from "./shapes/DiscourseRelationShape";
77
import { getNodeTypeById } from "~/utils/typeUtils";
88
import { showToast } from "./utils/toastUtils";
@@ -95,9 +95,10 @@ class Pointing extends StateNode {
9595
): string[] => {
9696
const compatibleTypes: string[] = [];
9797

98-
// Find all discourse relations that match the relation type and source
98+
// Find all accepted discourse relations that match the relation type and source
9999
const relations = plugin.settings.discourseRelations.filter(
100100
(relation) =>
101+
isAcceptedSchema(relation) &&
101102
relation.relationshipTypeId === relationTypeId &&
102103
relation.sourceId === sourceNodeTypeId,
103104
);
@@ -109,6 +110,7 @@ class Pointing extends StateNode {
109110
// Also check reverse relations (where current node is destination)
110111
const reverseRelations = plugin.settings.discourseRelations.filter(
111112
(relation) =>
113+
isAcceptedSchema(relation) &&
112114
relation.relationshipTypeId === relationTypeId &&
113115
relation.destinationId === sourceNodeTypeId,
114116
);

apps/obsidian/src/components/canvas/DiscourseToolPanel.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import * as React from "react";
1010
import { TFile } from "obsidian";
1111
import DiscourseGraphPlugin from "~/index";
1212
import { openCreateDiscourseNodeAt } from "./utils/nodeCreationFlow";
13-
import { getNodeTypeById } from "~/utils/typeUtils";
13+
import { getNodeTypeById, isAcceptedSchema } from "~/utils/typeUtils";
1414
import { useEffect } from "react";
1515
import { setDiscourseNodeToolContext } from "./DiscourseNodeTool";
1616
import {
@@ -195,7 +195,7 @@ export const DiscourseToolPanel = ({
195195
);
196196

197197
const nodeTypes = plugin.settings.nodeTypes;
198-
const relationTypes = plugin.settings.relationTypes;
198+
const relationTypes = plugin.settings.relationTypes.filter(isAcceptedSchema);
199199

200200
useEffect(() => {
201201
if (!focusedNodeTypeId) return;

apps/obsidian/src/components/canvas/overlays/RelationPanel.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
getArrowBindings,
1818
} from "~/components/canvas/utils/relationUtils";
1919
import { getFrontmatterForFile } from "~/components/canvas/shapes/discourseNodeShapeUtils";
20-
import { getRelationTypeById } from "~/utils/typeUtils";
20+
import { getRelationTypeById, isAcceptedSchema } from "~/utils/typeUtils";
2121
import { showToast } from "~/components/canvas/utils/toastUtils";
2222
import { toTldrawColor } from "~/utils/tldrawColors";
2323
import {
@@ -534,8 +534,13 @@ const computeRelations = async (
534534
const relations = await getRelationsForNodeInstanceId(plugin, nodeInstanceId);
535535
const result = new Map<string, GroupedRelation>();
536536

537-
for (const relationType of plugin.settings.relationTypes) {
538-
const typeLevelRelation = plugin.settings.discourseRelations.find(
537+
const acceptedRelationTypes =
538+
plugin.settings.relationTypes.filter(isAcceptedSchema);
539+
const acceptedDiscourseRelations =
540+
plugin.settings.discourseRelations.filter(isAcceptedSchema);
541+
542+
for (const relationType of acceptedRelationTypes) {
543+
const typeLevelRelation = acceptedDiscourseRelations.find(
539544
(rel) =>
540545
(rel.sourceId === activeNodeTypeId ||
541546
rel.destinationId === activeNodeTypeId) &&

apps/obsidian/src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export type DiscourseNode = {
1717
importedFromRid?: string;
1818
};
1919

20+
export type ImportStatus = "provisional" | "accepted";
21+
2022
export type DiscourseRelationType = {
2123
id: string;
2224
label: string;
@@ -25,6 +27,7 @@ export type DiscourseRelationType = {
2527
created: number;
2628
modified: number;
2729
importedFromRid?: string;
30+
status?: ImportStatus;
2831
};
2932

3033
export type DiscourseRelation = {
@@ -35,6 +38,7 @@ export type DiscourseRelation = {
3538
created: number;
3639
modified: number;
3740
importedFromRid?: string;
41+
status?: ImportStatus;
3842
};
3943

4044
export type RelationInstance = {

apps/obsidian/src/utils/conceptConversion.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ export const discourseRelationTypeToLocalConcept = ({
8989
created,
9090
modified,
9191
importedFromRid,
92+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
93+
status, //destructuring status to not upload it to the database
9294
...otherData
9395
} = relationType;
9496
// eslint-disable-next-line @typescript-eslint/naming-convention

apps/obsidian/src/utils/importRelations.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ const mapRelationTypeToLocal = async ({
9898
created: now,
9999
modified: now,
100100
importedFromRid,
101+
status: "provisional",
101102
};
102103
plugin.settings.relationTypes = [
103104
...(plugin.settings.relationTypes ?? []),
@@ -154,6 +155,7 @@ const findOrCreateTriple = async ({
154155
created,
155156
modified,
156157
...(importedFromRid && { importedFromRid }),
158+
status: "provisional",
157159
};
158160
plugin.settings.discourseRelations = [
159161
...(plugin.settings.discourseRelations ?? []),

0 commit comments

Comments
 (0)