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
83 changes: 83 additions & 0 deletions apps/obsidian/src/components/nodeTypeIdPropertyWidget.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import type {
AppWithUnofficialApis,
PropertyWidget,
PropertyWidgetComponentBase,
} from "~/utils/obsidianUnofficialTypes";
import type DiscourseGraphPlugin from "~/index";
import { getNodeTypeById } from "~/utils/typeUtils";

export const NODE_TYPE_ID_PROPERTY_KEY = "nodeTypeId";
const WIDGET_TYPE = "dg-node-type-id";

type NodeTypeIdPropertyWidgetComponent = PropertyWidgetComponentBase;

type NodeTypeIdPropertyWidget =
PropertyWidget<NodeTypeIdPropertyWidgetComponent>;

/**
* Obsidian's frontmatter Properties UI (reading view + live preview) renders each
* property via a widget looked up by `metadataTypeManager`. This is unofficial/
* internal API (see `obsidian-typings`), so it degrades gracefully: if Obsidian
* ever drops support, the widget type is simply unrecognized and the property
* renders as its raw text value, same as before this widget existed.
*/
const createWidget = (
plugin: DiscourseGraphPlugin,
): NodeTypeIdPropertyWidget => ({
type: WIDGET_TYPE,
icon: "shapes",
name: () => "Discourse node type",
validate: (value: unknown) => typeof value === "string",
render: (containerEl: HTMLElement, data: unknown) => {
const nodeTypeId = typeof data === "string" ? data : String(data ?? "");
const nodeType = getNodeTypeById(plugin, nodeTypeId);

const el = containerEl.createSpan({
cls: "dg-node-type-id-value",
text: nodeType?.name ?? nodeTypeId,
});
el.setAttr("title", nodeTypeId);
Comment thread
maparent marked this conversation as resolved.
el.tabIndex = 0;

return {
type: WIDGET_TYPE,
focus: () => el.focus(),
};
},
});

export const registerNodeTypeIdPropertyWidget = (
plugin: DiscourseGraphPlugin,
): void => {
const metadataTypeManager = (plugin.app as AppWithUnofficialApis)
.metadataTypeManager;

if (metadataTypeManager)
try {
metadataTypeManager.registeredTypeWidgets[WIDGET_TYPE] =
createWidget(plugin);
void metadataTypeManager.setType(NODE_TYPE_ID_PROPERTY_KEY, WIDGET_TYPE);
} catch (error) {
console.error(error);
}
};
Comment thread
maparent marked this conversation as resolved.

export const unregisterNodeTypeIdPropertyWidget = (
plugin: DiscourseGraphPlugin,
): void => {
const metadataTypeManager = (plugin.app as AppWithUnofficialApis)
.metadataTypeManager;

if (metadataTypeManager)
try {
if (
metadataTypeManager.getAssignedWidget(NODE_TYPE_ID_PROPERTY_KEY) ===
WIDGET_TYPE
) {
void metadataTypeManager.unsetType(NODE_TYPE_ID_PROPERTY_KEY);
}
delete metadataTypeManager.registeredTypeWidgets[WIDGET_TYPE];
} catch (error) {
console.error(error);
}
};
6 changes: 6 additions & 0 deletions apps/obsidian/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ import {
} from "~/utils/relationsStore";
import { migrateImportFolderMetadata } from "./utils/importFolderMetadata";
import { registerTemplateSettingsSync } from "~/utils/templateSettingsSync";
import {
registerNodeTypeIdPropertyWidget,
unregisterNodeTypeIdPropertyWidget,
} from "~/components/nodeTypeIdPropertyWidget";

export default class DiscourseGraphPlugin extends Plugin {
settings: Settings = { ...DEFAULT_SETTINGS };
Expand Down Expand Up @@ -68,6 +72,7 @@ export default class DiscourseGraphPlugin extends Plugin {
});

registerTemplateSettingsSync(this);
registerNodeTypeIdPropertyWidget(this);

if (this.settings.syncModeEnabled === true) {
void initializeSupabaseSync(this).catch((error) => {
Expand Down Expand Up @@ -437,6 +442,7 @@ export default class DiscourseGraphPlugin extends Plugin {
}

onunload() {
unregisterNodeTypeIdPropertyWidget(this);
this.activeNodePopover?.close();
this.activeNodePopover = null;
this.cleanupViewActions();
Expand Down
14 changes: 10 additions & 4 deletions apps/obsidian/src/services/QueryEngine.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { TFile, App } from "obsidian";
import { TFile, App, Plugin } from "obsidian";
import type DiscourseGraphPlugin from "~/index";
import { BulkImportPattern, BulkImportCandidate, DiscourseNode } from "~/types";
import { getDiscourseNodeFormatExpression } from "~/utils/getDiscourseNodeFormatExpression";
import { extractContentFromTitle } from "~/utils/extractContentFromTitle";
import { AppWithUnofficialApis } from "~/utils/obsidianUnofficialTypes";

// This is a workaround to get the datacore API.
// TODO: Remove once we can use datacore npm package
Expand Down Expand Up @@ -31,10 +32,15 @@ export class QueryEngine {
private readonly MIN_QUERY_LENGTH = 2;

constructor(app: App) {
const appWithPlugins = app as AppWithPlugins;
this.dc = appWithPlugins.plugins?.plugins?.["datacore"]?.api as
| { query: (query: string) => DatacorePage[] }
const appWithPlugins = app as AppWithUnofficialApis;
const datacorePlugin = appWithPlugins.plugins?.plugins?.["datacore"] as
| (Plugin & {
api: {
query: (query: string) => DatacorePage[];
};
})
| undefined;
this.dc = datacorePlugin?.api;
this.app = app;
}

Expand Down
174 changes: 174 additions & 0 deletions apps/obsidian/src/utils/obsidianUnofficialTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { App, Events, Debouncer, Plugin, Component } from "obsidian";

Check warning on line 1 in apps/obsidian/src/utils/obsidianUnofficialTypes.ts

View workflow job for this annotation

GitHub Actions / eslint (apps/obsidian)

[eslint (apps/obsidian)] apps/obsidian/src/utils/obsidianUnofficialTypes.ts#L1

'Debouncer' is defined but never used @typescript-eslint/no-unused-vars
Raw output
    1:23  warning  'Debouncer' is defined but never used            @typescript-eslint/no-unused-vars

// extracted the MetaadataTypeManager from "obsidian-types",
// and parts of internalPlugins, plugins.

type FocusMode = "both" | "end" | "start";

export type PropertyWidgetComponentBase = {
/**
* The type of the property widget.
*/
type: string;
/**
* Focus the property widget.
*
* @param mode - The focus mode.
*/
focus(mode?: FocusMode): void;
};

type PropertyRenderContext = {
/**
* Reference to the app.
*/
app: App;
/**
* Key of the property field.
*/
key: string;
/**
* Determine the source path of current context.
*/
sourcePath: string;
/**
* Callback called on property field unfocus.
*/
blur(): void;
/**
* Callback called on property value change.
*
* @param value - The new property value.
*/
onChange(value: unknown): void;
};

export type PropertyWidget<
ComponentType extends
PropertyWidgetComponentBase = PropertyWidgetComponentBase,
> = {
/**
* Lucide-dev icon associated with the widget.
*/
icon: string;
/**
* Reserved keys for the widget.
*/
reservedKeys?: string[];
/**
* Identifier for the widget.
*/
type: string;
/**
* Returns the I18N name of the widget.
*
* @returns The localized name of the widget.
*/
name(): string;
/**
* Render function for the widget on field container given context and data.
*
* @param containerEl - The container element to render the widget into.
* @param data - The property data to render.
* @param context - The rendering context for the property.
* @returns The rendered widget component.
*/
render(
containerEl: HTMLElement,
data: unknown,
context: PropertyRenderContext,
): ComponentType;
/**
* Validate whether the input value to the widget is correct.
*
* @param value - The value to validate.
* @returns Whether the value is valid.
*/
validate(value: unknown): boolean;
};

type PropertyWidgetEntry = {

Check warning on line 90 in apps/obsidian/src/utils/obsidianUnofficialTypes.ts

View workflow job for this annotation

GitHub Actions / eslint (apps/obsidian)

[eslint (apps/obsidian)] apps/obsidian/src/utils/obsidianUnofficialTypes.ts#L90

'PropertyWidgetEntry' is defined but never used @typescript-eslint/no-unused-vars
Raw output
   90:6   warning  'PropertyWidgetEntry' is defined but never used  @typescript-eslint/no-unused-vars
/**
* Display name of the property widget.
*/
name: string;
/**
* The property widget type.
*/
widget: string;
};

type PropertyInfo = {

Check warning on line 101 in apps/obsidian/src/utils/obsidianUnofficialTypes.ts

View workflow job for this annotation

GitHub Actions / eslint (apps/obsidian)

[eslint (apps/obsidian)] apps/obsidian/src/utils/obsidianUnofficialTypes.ts#L101

'PropertyInfo' is defined but never used @typescript-eslint/no-unused-vars
Raw output
  101:6   warning  'PropertyInfo' is defined but never used         @typescript-eslint/no-unused-vars
/**
* Name of property.
*/
name: string;
/**
* Usage count of property.
*/
occurrences: number;
/**
* Type of property.
*/
widget: string;
};

type TypeInfo = {

Check warning on line 116 in apps/obsidian/src/utils/obsidianUnofficialTypes.ts

View workflow job for this annotation

GitHub Actions / eslint (apps/obsidian)

[eslint (apps/obsidian)] apps/obsidian/src/utils/obsidianUnofficialTypes.ts#L116

'TypeInfo' is defined but never used @typescript-eslint/no-unused-vars
Raw output
  116:6   warning  'TypeInfo' is defined but never used             @typescript-eslint/no-unused-vars
/**
* The explicitly assigned property widget type.
*/
expected: PropertyWidget;
/**
* The property widget type inferred from the value.
*/
inferred: PropertyWidget;
};

type PropertyWidgetType = string;

type MetadataTypeManager = {
/**
* Registered type widgets.
*/
registeredTypeWidgets: Record<PropertyWidgetType, PropertyWidget>;
/**
* Get assigned widget type for property.
*
* @param property - Property name.
* @returns The assigned widget type, or `null`.
*/
getAssignedWidget(property: string): null | PropertyWidgetType;
/**
* Set widget type for property.
*
* @param property - Property name.
* @param type - Widget type to assign.
* @returns A promise that resolves when the widget type is set.
*/
setType(property: string, type: PropertyWidgetType): Promise<void>;
/**
* Unset widget type for property.
*
* @param property - Property name.
* @returns A promise that resolves when the widget type is unset.
*/
unsetType(property: string): Promise<void>;
} & Events;

export type InternalPluginInstance = {
plugin: InternalPlugin;
};

type InternalPlugin = {
enabled: boolean;
instance: InternalPluginInstance;
} & Component;

export type AppWithUnofficialApis = App & {
appId: string;
metadataTypeManager?: MetadataTypeManager;
plugins?: Events & { plugins?: Record<string, Plugin> };
internalPlugins?: Events & {
plugins?: Record<string, InternalPlugin>;
};
};
4 changes: 2 additions & 2 deletions apps/obsidian/src/utils/supabaseContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
FatalError,
} from "@repo/database/lib/contextFunctions";
import type DiscourseGraphPlugin from "~/index";

import type { AppWithUnofficialApis } from "./obsidianUnofficialTypes";
type Platform = Enums<"Platform">;

export type SupabaseContext = {
Expand Down Expand Up @@ -58,7 +58,7 @@ const getOrCreateAccountLocalId = async (
* @see https://help.obsidian.md/Extending+Obsidian/Obsidian+URI
*/
export const getVaultId = (app: DiscourseGraphPlugin["app"]): string => {
return (app as unknown as { appId: string }).appId;
return (app as AppWithUnofficialApis).appId;
};

/** Canonical space URL for an Obsidian vault; stored as Space.url in the DB. */
Expand Down
13 changes: 11 additions & 2 deletions apps/obsidian/src/utils/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import {
getFrontMatterInfo,
} from "obsidian";
import type { Settings } from "~/types";
import type {
AppWithUnofficialApis,
InternalPluginInstance,
} from "./obsidianUnofficialTypes";

type TemplatePluginInfo = {
isEnabled: boolean;
Expand Down Expand Up @@ -47,13 +51,18 @@ const mergeFrontmatter = (

export const getTemplatePluginInfo = (app: App): TemplatePluginInfo => {
try {
const templatesPlugin = (app as any).internalPlugins?.plugins?.templates;
const templatesPlugin = (app as AppWithUnofficialApis).internalPlugins
?.plugins?.templates;

if (!templatesPlugin || !templatesPlugin.enabled) {
return { isEnabled: false, folderPath: "" };
}

const folderPath = templatesPlugin.instance?.options?.folder || "";
const instance = templatesPlugin.instance as InternalPluginInstance & {
options?: { folder?: string };
};

const folderPath = instance.options?.folder || "";

return {
isEnabled: true,
Expand Down
Loading