feat(custom-widgets): export legacy definitions
This commit is contained in:
3 files changed
+82
-1
No files matched your search
@@ -62,6 +62,22 @@ export const CustomWidgetRowActions = ({ widget }: { widget: WidgetRef }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const copyLegacyDefinition = async () => {
|
||||
try {
|
||||
const data = await utils.customWidget.exportLegacy.fetch({ id: widget.id });
|
||||
await navigator.clipboard.writeText(JSON.stringify(data, null, 2));
|
||||
showSuccessNotification({
|
||||
title: t("action.copyLegacyDefinition"),
|
||||
message: t("notification.legacyDefinitionCopied"),
|
||||
});
|
||||
} catch (error) {
|
||||
showErrorNotification({
|
||||
title: t("action.copyLegacyDefinition"),
|
||||
message: error instanceof Error ? error.message : t("notification.legacyDefinitionCopyError"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const queueMigratedWidget = (text: string) => {
|
||||
const result = parseCustomWidgetClipboardDetailed(text);
|
||||
if (!result.success) {
|
||||
@@ -122,6 +138,15 @@ export const CustomWidgetRowActions = ({ widget }: { widget: WidgetRef }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const exportLegacyDefinition = async () => {
|
||||
try {
|
||||
const data = await utils.customWidget.exportLegacy.fetch({ id: widget.id });
|
||||
downloadJson(data, `${widget.name}.legacy.json`);
|
||||
} catch {
|
||||
showErrorNotification({ title: t("action.exportLegacyDefinition"), message: t("notification.exportError") });
|
||||
}
|
||||
};
|
||||
|
||||
const handleDuplicate = () => {
|
||||
duplicateMutation.mutate(
|
||||
{ id: widget.id },
|
||||
@@ -195,6 +220,13 @@ export const CustomWidgetRowActions = ({ widget }: { widget: WidgetRef }) => {
|
||||
)}
|
||||
{widget.migrationRequired && (
|
||||
<>
|
||||
<Menu.Item onClick={() => void copyLegacyDefinition()} leftSection={<IconCopy {...iconProps} />}>
|
||||
{t("action.copyLegacyDefinition")}
|
||||
</Menu.Item>
|
||||
<Menu.Item onClick={() => void exportLegacyDefinition()} leftSection={<IconDownload {...iconProps} />}>
|
||||
{t("action.exportLegacyDefinition")}
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
<Menu.Item onClick={() => void copyMigrationPrompt()} leftSection={<IconSparkles {...iconProps} />}>
|
||||
{t("action.copyMigrationPrompt")}
|
||||
</Menu.Item>
|
||||
@@ -247,3 +279,13 @@ export const CustomWidgetRowActions = ({ widget }: { widget: WidgetRef }) => {
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function downloadJson(data: unknown, filename: string) {
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = url;
|
||||
anchor.download = filename;
|
||||
anchor.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { parse as parseSuperJson } from "superjson";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { encryptSecret } from "@homarr/common/server";
|
||||
@@ -26,6 +27,32 @@ export const transferProcedures = {
|
||||
return parseStoredCustomWidgetDefinition(definition);
|
||||
}),
|
||||
|
||||
exportLegacy: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.input(z.object({ id: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const definition = await ctx.db.query.legacyCustomWidgetDefinitions.findFirst({
|
||||
where: eq(legacyCustomWidgetDefinitions.id, input.id),
|
||||
with: { secrets: true },
|
||||
});
|
||||
if (!definition) throw new TRPCError({ code: "NOT_FOUND", message: "Legacy custom widget not found" });
|
||||
|
||||
return {
|
||||
$schema: "homarr-custom-widget-v1" as const,
|
||||
name: definition.name,
|
||||
description: definition.description,
|
||||
iconUrl: definition.iconUrl,
|
||||
url: definition.url,
|
||||
authType: definition.authType,
|
||||
headerName: definition.headerName,
|
||||
method: definition.method,
|
||||
requestBody: definition.requestBody,
|
||||
displayType: definition.displayType,
|
||||
displayConfig: parseLegacyDisplayConfig(definition.displayConfig),
|
||||
configuredSecretKinds: definition.secrets.map(({ kind }) => kind),
|
||||
};
|
||||
}),
|
||||
|
||||
import: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.input(z.object({ widget: customWidgetImportSchema, secrets: customWidgetSecretsInputSchema.default([]) }))
|
||||
@@ -117,6 +144,14 @@ export const transferProcedures = {
|
||||
}),
|
||||
};
|
||||
|
||||
function parseLegacyDisplayConfig(value: string): unknown {
|
||||
try {
|
||||
return parseSuperJson(value);
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function canPreserveLegacySecrets(
|
||||
legacy: typeof legacyCustomWidgetDefinitions.$inferSelect,
|
||||
widget: z.infer<typeof customWidgetImportSchema>,
|
||||
|
||||
@@ -836,7 +836,7 @@
|
||||
"invalidDefinitionDescription": "This stored widget no longer matches the current Custom Widget schema. Delete it and recreate or import a valid definition.",
|
||||
"migrationRequired": "Migration required",
|
||||
"migrationDescription": "This v1 custom widget is preserved but cannot run until it is migrated to Custom JSX v2.",
|
||||
"migrationInstructions": "Open Actions, copy the redacted migration prompt into an LLM, then copy its single JSON response and choose Paste migrated widget. Existing board placements keep working after migration.",
|
||||
"migrationInstructions": "Open Actions to copy or export the original v1 definition for manual recreation. You can also use the migration prompt with an LLM. Existing board placements keep working after migration.",
|
||||
"missingCredentials": "{count, plural, one {Credential missing} other {# credentials missing}}",
|
||||
"missingCredentialsDescription": "{count, plural, one {This widget still needs one credential.} other {This widget still needs # credentials.}} Edit it to configure the encrypted values."
|
||||
},
|
||||
@@ -866,6 +866,8 @@
|
||||
"downloadSkill": "View Custom Widget Skill",
|
||||
"publishWorkshop": "Publish to Workshop",
|
||||
"migrate": "Migrate Custom Widget",
|
||||
"copyLegacyDefinition": "Copy original widget content",
|
||||
"exportLegacyDefinition": "Export original widget content",
|
||||
"copyMigrationPrompt": "Copy LLM migration prompt",
|
||||
"pasteMigration": "Paste migrated widget",
|
||||
"importMigration": "Import migrated JSON file",
|
||||
@@ -1224,6 +1226,8 @@
|
||||
"imported": "Widget imported successfully.",
|
||||
"migrated": "Legacy widget migrated successfully. Existing board placements now use the v2 definition.",
|
||||
"migrationError": "Failed to migrate the legacy widget. Ensure the file uses the homarr-custom-widget-v2 schema.",
|
||||
"legacyDefinitionCopied": "The original widget content was copied. Stored credential values are not included.",
|
||||
"legacyDefinitionCopyError": "Failed to copy the original widget content.",
|
||||
"migrationPromptCopied": "A redacted migration prompt was copied. Paste it into an LLM, copy its single JSON response, then choose Paste migrated widget here.",
|
||||
"migrationPromptCopyError": "Failed to copy the migration prompt.",
|
||||
"aiPromptCopied": "AI prompt copied to clipboard",
|
||||
|
||||
Reference in new issue
Block a user