(null);
const [historyDepth, setHistoryDepth] = useState({ undo: 0, redo: 0 });
const colorScheme = useComputedColorScheme("light");
- useEffect(() => setMounted(true), []);
+ useEffect(() => {
+ let cancelled = false;
+ void import("@uiw/react-codemirror").then((module) => {
+ if (!cancelled) setCodeMirror(() => module.default);
+ });
+ return () => {
+ cancelled = true;
+ };
+ }, []);
const extensions = useMemo(() => {
const diagnosticsExtension = linter((view) =>
diagnostics.map((diagnostic) => {
@@ -77,6 +86,11 @@ export function CustomWidgetCodeEditor(props: CustomWidgetCodeEditorProps) {
});
editorView.focus();
}, [editorView, props.revealKey, props.revealText]);
+ const handleCreateEditor = useCallback((view: EditorViewType) => {
+ // @uiw invokes this while constructing CodeMirror. Deferring the parent update avoids
+ // re-entering React's renderer during initialisation in Next.js and Docusaurus.
+ window.setTimeout(() => setEditorView(view), 0);
+ }, []);
const formattedValue = useMemo(() => formatCode(props.value, props.language), [props.language, props.value]);
const errorCount = diagnostics.filter(({ severity }) => severity === "error").length;
const warningCount = diagnostics.length - errorCount;
@@ -115,37 +129,36 @@ export function CustomWidgetCodeEditor(props: CustomWidgetCodeEditorProps) {
)}
- {mounted ? (
- }>
- undefined : props.onChange}
- placeholder={props.placeholder}
- extensions={extensions}
- theme={colorScheme}
- height={props.height ?? (props.language === "jsx" ? "320px" : "220px")}
- basicSetup={{
- autocompletion: props.language !== "jsx" && !props.readOnly,
- bracketMatching: true,
- closeBrackets: !props.readOnly,
- foldGutter: true,
- history: !props.readOnly,
- highlightActiveLine: !props.readOnly,
- highlightActiveLineGutter: !props.readOnly,
- indentOnInput: !props.readOnly,
- lineNumbers: true,
- }}
- onCreateEditor={setEditorView}
- onUpdate={(update) => {
- if (!update.selectionSet && !update.docChanged) return;
- const head = update.state.selection.main.head;
- const line = update.state.doc.lineAt(head);
- setCursor({ line: line.number, column: head - line.from + 1 });
- setHistoryDepth({ undo: undoDepth(update.state), redo: redoDepth(update.state) });
- }}
- />
-
+ {CodeMirror ? (
+ undefined : props.onChange}
+ placeholder={props.placeholder}
+ extensions={extensions}
+ theme={colorScheme}
+ height={props.height ?? (props.language === "jsx" ? "320px" : "220px")}
+ basicSetup={{
+ autocompletion: props.language !== "jsx" && !props.readOnly,
+ bracketMatching: true,
+ closeBrackets: !props.readOnly,
+ foldGutter: true,
+ history: !props.readOnly,
+ highlightActiveLine: !props.readOnly,
+ highlightActiveLineGutter: !props.readOnly,
+ indentOnInput: !props.readOnly,
+ lineNumbers: true,
+ }}
+ onCreateEditor={handleCreateEditor}
+ onUpdate={(update) => {
+ if (!editorView) return;
+ if (!update.selectionSet && !update.docChanged) return;
+ const head = update.state.selection.main.head;
+ const line = update.state.doc.lineAt(head);
+ setCursor({ line: line.number, column: head - line.from + 1 });
+ setHistoryDepth({ undo: undoDepth(update.state), redo: redoDepth(update.state) });
+ }}
+ />
) : (
)}
diff --git a/packages/db/test/mysql-migration.spec.ts b/packages/db/test/mysql-migration.spec.ts
index a235f00fc..8b9cd880e 100644
--- a/packages/db/test/mysql-migration.spec.ts
+++ b/packages/db/test/mysql-migration.spec.ts
@@ -3,11 +3,13 @@ import { MySqlContainer } from "@testcontainers/mysql";
import { drizzle } from "drizzle-orm/mysql2";
import { migrate } from "drizzle-orm/mysql2/migrator";
import mysql from "mysql2";
-import { describe, test } from "vitest";
+import { describe, expect, test } from "vitest";
import { DB_CASING } from "@homarr/core/infrastructure/db/constants";
import * as mysqlSchema from "../schema/mysql";
+import type { Database } from "..";
+import { seedDataAsync } from "../migrations/seed";
describe("Mysql Migration", () => {
test("should add all tables and keys specified in migration files", async () => {
@@ -31,9 +33,15 @@ describe("Mysql Migration", () => {
await migrate(database, {
migrationsFolder: path.join(__dirname, "..", "migrations", "mysql"),
});
+ await seedDataAsync(database as unknown as Database);
// Check if users table exists
await database.query.users.findMany();
+ const bundledWidgets = await database.query.customWidgetDefinitions.findMany({
+ where: (table, { like }) => like(table.id, "seed-%"),
+ });
+ expect(bundledWidgets).toHaveLength(4);
+ expect(bundledWidgets.every(({ enabled }) => !enabled)).toBe(true);
connection.end();
await mysqlContainer.stop();
diff --git a/packages/db/test/postgresql-migration.spec.ts b/packages/db/test/postgresql-migration.spec.ts
index c7d4ce4ee..d8f7cc1e9 100644
--- a/packages/db/test/postgresql-migration.spec.ts
+++ b/packages/db/test/postgresql-migration.spec.ts
@@ -3,11 +3,13 @@ import { PostgreSqlContainer } from "@testcontainers/postgresql";
import { drizzle } from "drizzle-orm/node-postgres";
import { migrate } from "drizzle-orm/node-postgres/migrator";
import { Pool } from "pg";
-import { describe, test } from "vitest";
+import { describe, expect, test } from "vitest";
import { DB_CASING } from "@homarr/core/infrastructure/db/constants";
import * as pgSchema from "../schema/postgresql";
+import type { Database } from "..";
+import { seedDataAsync } from "../migrations/seed";
describe("PostgreSql Migration", () => {
test("should add all tables and keys specified in migration files", async () => {
@@ -36,9 +38,15 @@ describe("PostgreSql Migration", () => {
await migrate(database, {
migrationsFolder: path.join(__dirname, "..", "migrations", "postgresql"),
});
+ await seedDataAsync(database as unknown as Database);
// Check if users table exists
await database.query.users.findMany();
+ const bundledWidgets = await database.query.customWidgetDefinitions.findMany({
+ where: (table, { like }) => like(table.id, "seed-%"),
+ });
+ expect(bundledWidgets).toHaveLength(4);
+ expect(bundledWidgets.every(({ enabled }) => !enabled)).toBe(true);
// Close the pool to release resources
await pool.end();
diff --git a/packages/db/test/sqlite-migration.spec.ts b/packages/db/test/sqlite-migration.spec.ts
new file mode 100644
index 000000000..103d45bfd
--- /dev/null
+++ b/packages/db/test/sqlite-migration.spec.ts
@@ -0,0 +1,27 @@
+import path from "path";
+import BetterSqlite3 from "better-sqlite3";
+import { drizzle } from "drizzle-orm/better-sqlite3";
+import { migrate } from "drizzle-orm/better-sqlite3/migrator";
+import { expect, test } from "vitest";
+
+import { DB_CASING } from "@homarr/core/infrastructure/db/constants";
+
+import type { Database } from "..";
+import { seedDataAsync } from "../migrations/seed";
+import * as sqliteSchema from "../schema/sqlite";
+
+test("SQLite migrations seed the four disabled bundled custom widgets", async () => {
+ const connection = new BetterSqlite3(":memory:");
+ const database = drizzle(connection, { schema: sqliteSchema, casing: DB_CASING });
+
+ migrate(database, { migrationsFolder: path.join(__dirname, "..", "migrations", "sqlite") });
+ await seedDataAsync(database as unknown as Database);
+
+ const bundledWidgets = await database.query.customWidgetDefinitions.findMany({
+ where: (table, { like }) => like(table.id, "seed-%"),
+ });
+ expect(bundledWidgets).toHaveLength(4);
+ expect(bundledWidgets.every(({ enabled }) => !enabled)).toBe(true);
+
+ connection.close();
+});
diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json
index f08e9aa0a..335556d3c 100644
--- a/packages/translation/src/lang/en.json
+++ b/packages/translation/src/lang/en.json
@@ -743,6 +743,7 @@
"edit": "Edit",
"duplicate": "Duplicate",
"copyAiPrompt": "Copy AI prompt",
+ "copyAiPromptWithSkill": "Copy AI prompt with embedded skill",
"enable": "Enable",
"disable": "Disable",
"menu": "Actions",
diff --git a/vitest.config.mts b/vitest.config.mts
index 6f40c2b20..600fb91bc 100644
--- a/vitest.config.mts
+++ b/vitest.config.mts
@@ -26,13 +26,22 @@ export default defineConfig({
include: ["packages/api/**/*.spec.ts"],
},
},
+ {
+ extends: true,
+ test: {
+ name: "db-node",
+ environment: "node",
+ setupFiles: ["./vitest.setup.ts", "./vitest.setup.node.ts"],
+ include: ["packages/db/test/**/*.spec.ts"],
+ },
+ },
{
extends: true,
test: {
name: "dom",
environment: "jsdom",
include: ["**/*.spec.{ts,tsx}"],
- exclude: [...configDefaults.exclude, "apps/nextjs/.next", "packages/api/**", "e2e/**"],
+ exclude: [...configDefaults.exclude, "apps/nextjs/.next", "packages/api/**", "packages/db/**", "e2e/**"],
},
},
{