logancyang_obsidian-copilot/src/components/modals/ExtensionInputModal.tsx
Zero Liu 6a71cf8586
chore(react): centralize React root creation via createPluginRoot helper (#2467)
* chore(react): centralize React root creation via createPluginRoot helper

Every standalone React root in the plugin must wrap its tree in
AppContext.Provider so descendants can rely on useApp() — PR #2466 fixed
one missing wrap (QuickAskOverlay) but the contract was implicit and any
new createRoot callsite could silently re-introduce the same crash.
Introduce a createPluginRoot(container, app) helper that injects the
provider automatically, migrate all 17 createRoot callsites to use it,
and add a Jest guardrail that fails if any non-helper file imports
createRoot from react-dom/client.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(lint): replace createRoot Jest guardrail with ESLint rule

Switch the createPluginRoot enforcement from a Jest test (walks src/
and greps imports) to an ESLint no-restricted-syntax rule. Same
invariant, faster feedback (in-editor instead of on test run), and one
fewer test file to maintain. The helper file is exempted via the
config block's `ignores`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 11:49:03 -07:00

95 lines
2.4 KiB
TypeScript

import { App, Modal } from "obsidian";
import React, { useState } from "react";
import { Root } from "react-dom/client";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { createPluginRoot } from "@/utils/react/createPluginRoot";
function ExtensionInputModalContent({
onConfirm,
onCancel,
}: {
onConfirm: (extension: string) => void;
onCancel: () => void;
}) {
const [extension, setExtension] = useState("");
const [error, setError] = useState<string | null>(null);
const validateAndConfirm = (value: string) => {
if (value.includes(" ")) {
setError("Extension cannot contain spaces");
return;
}
setError(null);
onConfirm(value);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
e.preventDefault();
e.stopPropagation();
validateAndConfirm(extension);
}
};
return (
<div className="tw-flex tw-flex-col tw-gap-4">
<div className="tw-flex tw-flex-col tw-gap-2">
<Input
placeholder="Enter the extension (e.g. txt, excalidraw.md)"
value={extension}
onChange={(e) => {
setExtension(e.target.value);
setError(null);
}}
onKeyDown={handleKeyDown}
/>
{error && <p className="tw-text-sm tw-text-error">{error}</p>}
</div>
<div className="tw-flex tw-justify-end tw-gap-2">
<Button variant="secondary" onClick={onCancel}>
Cancel
</Button>
<Button variant="default" onClick={() => validateAndConfirm(extension)}>
Confirm
</Button>
</div>
</div>
);
}
export class ExtensionInputModal extends Modal {
private root: Root;
constructor(
app: App,
private onConfirm: (extension: string) => void
) {
super(app);
// https://docs.obsidian.md/Reference/TypeScript+API/Modal/setTitle
// @ts-ignore
this.setTitle("Add Extension");
}
onOpen() {
const { contentEl } = this;
this.root = createPluginRoot(contentEl, this.app);
const handleConfirm = (extension: string) => {
this.onConfirm(extension);
this.close();
};
const handleCancel = () => {
this.close();
};
this.root.render(
<ExtensionInputModalContent onConfirm={handleConfirm} onCancel={handleCancel} />
);
}
onClose() {
this.root.unmount();
}
}