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
21 changes: 21 additions & 0 deletions apps/obsidian/src/components/GeneralSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { setIcon } from "obsidian";
import SuggestInput from "./SuggestInput";
import { DiscourseGraphLogoIcon, SlackLogoIcon } from "./Icons";
import { openExportSpecsModal } from "./ExportSpecsModal";
import { openImportSpecsModal } from "./ImportSpecsModal";
import { getDgSchemaFileName } from "~/utils/specValidation";

const DOCS_URL = "https://discoursegraphs.com/docs/obsidian";
Expand Down Expand Up @@ -273,6 +274,26 @@ const GeneralSettings = () => {
</div>
</div>

<div className="setting-item">
<div className="setting-item-info">
<div className="setting-item-name">Import discourse graph schema</div>
<div className="setting-item-description">
Choose a schema JSON file from your computer and preview how it maps
to your existing node types, relation types, relation triples, and
templates.
</div>
</div>
<div className="setting-item-control">
<button
type="button"
className="rounded border px-3 py-1.5 text-sm"
onClick={() => openImportSpecsModal(plugin)}
>
Open import modal
</button>
</div>
</div>

<div className="setting-item">
<div className="setting-item-info">
<div className="setting-item-name">Node tag hotkey</div>
Expand Down
59 changes: 59 additions & 0 deletions apps/obsidian/src/components/ImportSchemaPreviewSummary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { ImportPreviewStats, LoadedSchemaFile } from "~/utils/specImport";

export const ImportSchemaPreviewSummary = ({
loadedSchemaFile,
previewStats,
}: {
loadedSchemaFile: LoadedSchemaFile;
previewStats: ImportPreviewStats;
}) => {
return (
<>
<div className="mb-4 rounded border p-3 text-sm">
<div className="font-medium">Schema file metadata</div>
<div className="text-muted mt-1">
Vault:{" "}
<span className="font-medium">
{loadedSchemaFile.schemaFile.vaultName}
</span>
</div>
<div className="text-muted">
Exported at:{" "}
<span className="font-medium">
{loadedSchemaFile.schemaFile.exportedAt}
</span>
</div>
<div className="text-muted">
Plugin version:{" "}
<span className="font-medium">
{loadedSchemaFile.schemaFile.pluginVersion}
</span>
</div>
</div>

<div className="mb-4 rounded border p-3 text-sm">
<div className="font-medium">Preview (full schema file)</div>
<div className="text-muted mt-1">
Node types: {previewStats.nodeTypes.total} total (
{previewStats.nodeTypes.new} new, {previewStats.nodeTypes.existing}{" "}
existing)
</div>
<div className="text-muted">
Relation types: {previewStats.relationTypes.total} total (
{previewStats.relationTypes.new} new,{" "}
{previewStats.relationTypes.existing} existing)
</div>
<div className="text-muted">
Relation triples: {previewStats.discourseRelations.total} total (
{previewStats.discourseRelations.new} new,{" "}
{previewStats.discourseRelations.existing} existing)
</div>
<div className="text-muted">
Templates: {previewStats.templates.total} total (
{previewStats.templates.new} new, {previewStats.templates.existing}{" "}
existing)
</div>
</div>
</>
);
};
213 changes: 213 additions & 0 deletions apps/obsidian/src/components/ImportSpecsModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import { App, Notice } from "obsidian";
import { useMemo, useState } from "react";
import type DiscourseGraphPlugin from "~/index";
import {
applySchemaImportSelection,
pickAndPreviewSchemaImport,
type ImportPreviewStats,
type LoadedSchemaFile,
type SpecImportPreview,
} from "~/utils/specImport";
import { NativeFileDialogCancelledError } from "~/utils/nativeJsonFileDialogs";
import {
useSchemaSelection,
type SchemaSelectionSource,
} from "~/components/useSchemaSelection";
import { SchemaSelectionModalBody } from "~/components/SchemaSelectionModalBody";
import { ImportSchemaPreviewSummary } from "~/components/ImportSchemaPreviewSummary";
import { ReactRootModal } from "~/components/ReactRootModal";

type ImportSpecsModalProps = {
plugin: DiscourseGraphPlugin;
onClose: () => void;
};

export const openImportSpecsModal = (plugin: DiscourseGraphPlugin): void => {
new ImportSpecsModal(plugin.app, plugin).open();
};

const ImportPreviewSelection = ({
plugin,
loadedSchemaFile,
previewStats,
isApplyingImport,
setIsApplyingImport,
onResetPreview,
onClose,
}: {
plugin: DiscourseGraphPlugin;
loadedSchemaFile: LoadedSchemaFile;
previewStats: ImportPreviewStats;
isApplyingImport: boolean;
setIsApplyingImport: (value: boolean) => void;
onResetPreview: () => void;
onClose: () => void;
}) => {
const source = useMemo<SchemaSelectionSource>(() => {
const schemaFile = loadedSchemaFile.schemaFile;
return {
nodeTypes: schemaFile.nodeTypes,
relationTypes: schemaFile.relationTypes,
relationTriples: schemaFile.discourseRelations,
templateNames: schemaFile.templates.map((template) => template.name),
};
}, [loadedSchemaFile]);

const selection = useSchemaSelection({
source,
resetKey: loadedSchemaFile.sourcePath,
});

const handleApplyImport = async (): Promise<void> => {
const selected = selection.asSelectionPayload();
const hasAnySelection =
selected.nodeTypeIds.length > 0 ||
selected.relationTypeIds.length > 0 ||
selected.relationIds.length > 0 ||
selected.templateNames.length > 0;
if (!hasAnySelection) {
new Notice("Select at least one item to import.");
return;
}

setIsApplyingImport(true);
try {
const result = await applySchemaImportSelection({
plugin,
loadedSchemaFile,
selection: {
nodeTypeIds: selected.nodeTypeIds,
relationTypeIds: selected.relationTypeIds,
discourseRelationIds: selected.relationIds,
templateNames: selected.templateNames,
},
});

const { created } = result;
new Notice(
`Import complete: ${created.nodeTypes} node type(s), ${created.relationTypes} relation type(s), ${created.discourseRelations} relation triple(s), and ${created.templates} template(s) created.`,
7000,
);

if (result.warnings.length > 0) {
new Notice(
`Import completed with ${result.warnings.length} warning(s).`,
6000,
);
for (const warning of result.warnings) {
new Notice(warning, 6000);
}
}
onClose();
} catch (error) {
console.error("Failed to apply schema import:", error);
const message = error instanceof Error ? error.message : String(error);
new Notice(`Failed to import schema: ${message}`, 6000);
} finally {
setIsApplyingImport(false);
}
};

return (
<SchemaSelectionModalBody
title="Import schema preview"
description={`Source file: ${loadedSchemaFile.sourcePath}`}
source={source}
selection={selection}
emptyTemplateText="No templates found in this schema file."
onDependencyViolation={(message) => new Notice(message)}
beforePanel={
<ImportSchemaPreviewSummary
loadedSchemaFile={loadedSchemaFile}
previewStats={previewStats}
/>
}
footerSecondaryLabel="Choose another file"
onFooterSecondaryClick={onResetPreview}
footerPrimaryLabel={isApplyingImport ? "Importing..." : "Import selected"}
onFooterPrimaryClick={() => void handleApplyImport()}
isFooterSecondaryDisabled={isApplyingImport}
isFooterPrimaryDisabled={isApplyingImport}
/>
);
};

const ImportSpecsContent = ({ plugin, onClose }: ImportSpecsModalProps) => {
const [preview, setPreview] = useState<SpecImportPreview | null>(null);
const [isSelectingFile, setIsSelectingFile] = useState(false);
const [isApplyingImport, setIsApplyingImport] = useState(false);

const handleSelectSchemaFile = async (): Promise<void> => {
setIsSelectingFile(true);
try {
const nextPreview = await pickAndPreviewSchemaImport({ plugin });
setPreview(nextPreview);
} catch (error) {
if (error instanceof NativeFileDialogCancelledError) {
return;
}
console.error("Failed to load schema import file:", error);
const message = error instanceof Error ? error.message : String(error);
new Notice(`Failed to load schema file: ${message}`, 6000);
} finally {
setIsSelectingFile(false);
}
};

if (!preview) {
return (
<div>
<h3 className="mb-2">Import discourse graph schema</h3>
<p className="text-muted mb-4 text-sm">
Pick a <code>dg-schema-*.json</code> file from your computer to
preview and choose exactly what to import.
</p>

<div className="mb-4 rounded border p-3 text-sm">
Same dependency rules as export apply here during selection.
</div>

<div className="flex justify-between">
<button type="button" className="px-4 py-2" onClick={onClose}>
Cancel
</button>
<button
type="button"
className="!bg-accent !text-on-accent rounded px-4 py-2"
onClick={() => void handleSelectSchemaFile()}
disabled={isSelectingFile}
>
{isSelectingFile ? "Opening..." : "Choose schema file"}
</button>
</div>
</div>
);
}

return (
<ImportPreviewSelection
plugin={plugin}
loadedSchemaFile={preview.loadedSchemaFile}
previewStats={preview.previewStats}
isApplyingImport={isApplyingImport}
setIsApplyingImport={setIsApplyingImport}
onResetPreview={() => setPreview(null)}
onClose={onClose}
/>
);
};

export class ImportSpecsModal extends ReactRootModal {
private plugin: DiscourseGraphPlugin;

constructor(app: App, plugin: DiscourseGraphPlugin) {
super(app);
this.plugin = plugin;
}

protected renderContent() {
return (
<ImportSpecsContent plugin={this.plugin} onClose={() => this.close()} />
);
}
}
9 changes: 9 additions & 0 deletions apps/obsidian/src/utils/registerCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { addRelationIfRequested } from "~/components/canvas/utils/relationJsonUt
import type { DiscourseNode } from "~/types";
import { TldrawView } from "~/components/canvas/TldrawView";
import { createBaseForNodeType } from "./baseForNodeType";
import { openImportSpecsModal } from "~/components/ImportSpecsModal";

type ModifyNodeSubmitParams = {
nodeType: DiscourseNode;
Expand Down Expand Up @@ -203,6 +204,14 @@ export const registerCommands = (plugin: DiscourseGraphPlugin) => {
},
});

plugin.addCommand({
id: "import-dg-schema",
name: "Import discourse graph schema",
callback: () => {
openImportSpecsModal(plugin);
},
});

plugin.addCommand({
id: "toggle-discourse-context",
name: "Toggle discourse context",
Expand Down
Loading