-
Notifications
You must be signed in to change notification settings - Fork 7
ENG-2084 Add schema import UI for Obsidian #1265
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
trangdoan982
wants to merge
10
commits into
eng-1977-add-schema-import-command-to-obsidian
Choose a base branch
from
eng-2084-add-schema-import-ui-for-obsidian
base: eng-1977-add-schema-import-command-to-obsidian
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+283
−0
Open
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
aeac259
ENG-2084 Add schema import UI for Obsidian
trangdoan982 5105e04
ENG-2084 Inline Modal boilerplate in ImportSpecsModal (remove ReactRo…
trangdoan982 044c346
ENG-2084 Show human-readable error when schema file fails Zod validation
trangdoan982 3326453
ENG-2084 Remove console.error (violates Obsidian plugin guidelines)
trangdoan982 de4f384
ENG-2084 Remove emptyTemplateText/beforePanel props; compose ImportSc…
trangdoan982 8ff65bc
ENG-2084 Address review: simplify ImportSpecsModal constructor, use S…
trangdoan982 cd7e537
ENG-2084 Remove useMemo from source — schema file is stable for compo…
trangdoan982 69deb70
ENG-2084 Use onWarning callback in applySchemaImportSelection
trangdoan982 f8d4c97
ENG-2084 Drop schema import entry from settings
trangdoan982 b75cbd1
ENG-2084 Reset applying flag only on the failure path
trangdoan982 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
59 changes: 59 additions & 0 deletions
59
apps/obsidian/src/components/ImportSchemaPreviewSummary.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> | ||
| </> | ||
| ); | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,215 @@ | ||
| import { Modal, Notice } from "obsidian"; | ||
| import { StrictMode, useState } from "react"; | ||
| import { createRoot, type Root } from "react-dom/client"; | ||
| import { ZodError } from "zod"; | ||
| import type DiscourseGraphPlugin from "~/index"; | ||
| import { | ||
| applySchemaImportSelection, | ||
| pickAndPreviewSchemaImport, | ||
| type ImportPreviewStats, | ||
| type LoadedSchemaFile, | ||
| type SpecImportPreview, | ||
| } from "~/utils/specImport"; | ||
| import { NativeFileDialogCancelledError } from "~/utils/nativeJsonFileDialogs"; | ||
| import { useSchemaSelection } from "~/components/useSchemaSelection"; | ||
| import { SchemaSelectionModalBody } from "~/components/SchemaSelectionModalBody"; | ||
| import { ImportSchemaPreviewSummary } from "~/components/ImportSchemaPreviewSummary"; | ||
|
|
||
| type ImportSpecsModalProps = { | ||
| plugin: DiscourseGraphPlugin; | ||
| onClose: () => void; | ||
| }; | ||
|
|
||
| export const openImportSpecsModal = (plugin: DiscourseGraphPlugin): void => { | ||
| new ImportSpecsModal(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 schemaFile = loadedSchemaFile.schemaFile; | ||
| const source = { | ||
| nodeTypes: schemaFile.nodeTypes, | ||
| relationTypes: schemaFile.relationTypes, | ||
| relationTriples: schemaFile.discourseRelations, | ||
| templateNames: schemaFile.templates.map((template) => template.name), | ||
| }; | ||
|
|
||
| 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.discourseRelationIds.length > 0 || | ||
| selected.templateNames.length > 0; | ||
| if (!hasAnySelection) { | ||
| new Notice("Select at least one item to import."); | ||
| return; | ||
| } | ||
|
|
||
| setIsApplyingImport(true); | ||
| const warnings: string[] = []; | ||
| try { | ||
| const { created } = await applySchemaImportSelection({ | ||
| plugin, | ||
| loadedSchemaFile, | ||
| selection: selected, | ||
| onWarning: (message) => warnings.push(message), | ||
| }); | ||
|
|
||
| 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 (warnings.length > 0) { | ||
| new Notice(`Import warnings:\n${warnings.join("\n")}`, 6000); | ||
| } | ||
| onClose(); | ||
| } catch (error) { | ||
| const message = error instanceof Error ? error.message : String(error); | ||
| new Notice(`Failed to import schema: ${message}`, 6000); | ||
| // Only the failure path stays mounted; the success path unmounted at onClose() | ||
| setIsApplyingImport(false); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <> | ||
| <ImportSchemaPreviewSummary | ||
| loadedSchemaFile={loadedSchemaFile} | ||
| previewStats={previewStats} | ||
| /> | ||
| <SchemaSelectionModalBody | ||
| title="Import schema preview" | ||
| description={`Source file: ${loadedSchemaFile.sourcePath}`} | ||
| source={source} | ||
| selection={selection} | ||
| onDependencyViolation={(message) => new Notice(message)} | ||
| 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; | ||
| if (error instanceof ZodError) { | ||
| const fields = error.issues.map((i) => i.path.join(".")).join(", "); | ||
| new Notice( | ||
| `Schema file is incompatible with this version of the plugin. Invalid or missing fields: ${fields}`, | ||
| 8000, | ||
| ); | ||
| return; | ||
| } | ||
| 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 Modal { | ||
| private plugin: DiscourseGraphPlugin; | ||
| private root: Root | null = null; | ||
|
|
||
| constructor(plugin: DiscourseGraphPlugin) { | ||
| super(plugin.app); | ||
| this.plugin = plugin; | ||
| } | ||
|
|
||
| onOpen(): void { | ||
| this.contentEl.empty(); | ||
| this.root = createRoot(this.contentEl); | ||
| this.root.render( | ||
| <StrictMode> | ||
| <ImportSpecsContent plugin={this.plugin} onClose={() => this.close()} /> | ||
| </StrictMode>, | ||
| ); | ||
| } | ||
|
|
||
| onClose(): void { | ||
| if (this.root) { | ||
| this.root.unmount(); | ||
| this.root = null; | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.