diff --git a/apps/nextjs/src/app/[locale]/manage/custom-widgets/packages/_package-assistant.tsx b/apps/nextjs/src/app/[locale]/manage/custom-widgets/packages/_package-assistant.tsx
index db384dc1f..d3eac0067 100644
--- a/apps/nextjs/src/app/[locale]/manage/custom-widgets/packages/_package-assistant.tsx
+++ b/apps/nextjs/src/app/[locale]/manage/custom-widgets/packages/_package-assistant.tsx
@@ -61,13 +61,19 @@ export function PackageAssistant() {
- {t("manual")}
+
+ {t("manual")}
+
)}
{assistant?.enabled && (
<>
{assistant.isRunning && {t("busy")}}
- {sendFailed && {t("sendFailed")}}
+ {sendFailed && (
+
+ {t("sendFailed")}
+
+ )}
{t("context")}
@@ -87,16 +93,40 @@ export function PackageAssistant() {
-
diff --git a/apps/nextjs/src/app/[locale]/manage/custom-widgets/packages/_package-connections.tsx b/apps/nextjs/src/app/[locale]/manage/custom-widgets/packages/_package-connections.tsx
index 4462b266c..88a95622e 100644
--- a/apps/nextjs/src/app/[locale]/manage/custom-widgets/packages/_package-connections.tsx
+++ b/apps/nextjs/src/app/[locale]/manage/custom-widgets/packages/_package-connections.tsx
@@ -62,7 +62,9 @@ export function PackageConnections({
/>
{getWidgetConnectionBindingNames(name, requirement, bindings).map((bindingName) => (
- {connections.data?.find((entry) => entry.id === bindings[bindingName])?.name ?? bindingName}
+
+ {connections.data?.find((entry) => entry.id === bindings[bindingName])?.name ?? bindingName}
+
{integrations.error.message}}
{Object.values(requirements).some((requirement) => requirement.kind === "integration") && (
-
+
{t("createIntegration")}
)}
- { void connections.refetch(); void integrations.refetch(); }}>
+ {
+ void connections.refetch();
+ void integrations.refetch();
+ }}
+ >
{t("refresh")}
bindings[key]!))];
- const compatible = connections.filter((connection) => isWidgetConnectionCompatible(requirement, connection.configuration));
+ const ids = [
+ ...new Set(
+ getWidgetConnectionBindingNames(name, requirement, bindings)
+ .map((key) => bindings[key])
+ .filter((id) => typeof id === "string"),
+ ),
+ ];
+ const compatible = connections.filter((connection) =>
+ isWidgetConnectionCompatible(requirement, connection.configuration),
+ );
const choices = compatible.map((connection) => ({ value: connection.id, label: connection.name }));
if (requirement.kind === "integration") {
for (const integration of integrations) {
@@ -189,7 +211,10 @@ function PackageBindingPicker({
// Keep unavailable bindings visible so a removed integration can be diagnosed or replaced.
for (const id of ids) {
if (!choices.some((choice) => choice.value === id))
- choices.push({ value: id, label: connections.find((connection) => connection.id === id)?.name ?? t("unavailableConnection") });
+ choices.push({
+ value: id,
+ label: connections.find((connection) => connection.id === id)?.name ?? t("unavailableConnection"),
+ });
}
const change = async (values: string[]) => {
if (preparing.current || disabled) return;
diff --git a/apps/nextjs/src/app/[locale]/manage/custom-widgets/packages/_package-operations.tsx b/apps/nextjs/src/app/[locale]/manage/custom-widgets/packages/_package-operations.tsx
index 9d7ffc938..98386775d 100644
--- a/apps/nextjs/src/app/[locale]/manage/custom-widgets/packages/_package-operations.tsx
+++ b/apps/nextjs/src/app/[locale]/manage/custom-widgets/packages/_package-operations.tsx
@@ -91,12 +91,16 @@ export function PackageOperations({
};
return (
- {t("useDescription")}
+
+ {t("useDescription")}
+
{!current && (
{t("saveFirst")}
- {t("saveDraft")}
+
+ {t("saveDraft")}
+
)}
@@ -110,12 +114,18 @@ export function PackageOperations({
{t("saveBeforeActivate")}
- {t("saveDraft")}
+
+ {t("saveDraft")}
+
)}
{!dirty && !matchesReviewedSource && {t("savedSourceChanged")}}
- onTrustChange(event.currentTarget.checked)} label={t("trustAcknowledgement")} />
+ onTrustChange(event.currentTarget.checked)}
+ label={t("trustAcknowledgement")}
+ />
- void run(() => enable.mutateAsync({ id: current.id, enabled: !current.enabled }), t("stateSaved"))
+ void run(
+ () => enable.mutateAsync({ id: current.id, enabled: !current.enabled }),
+ t("stateSaved"),
+ )
}
>
{current.enabled ? t("disable") : t("enable")}
diff --git a/apps/nextjs/src/app/[locale]/manage/custom-widgets/packages/_package-preview.tsx b/apps/nextjs/src/app/[locale]/manage/custom-widgets/packages/_package-preview.tsx
index 1dab014e2..50077c474 100644
--- a/apps/nextjs/src/app/[locale]/manage/custom-widgets/packages/_package-preview.tsx
+++ b/apps/nextjs/src/app/[locale]/manage/custom-widgets/packages/_package-preview.tsx
@@ -176,7 +176,11 @@ export function PackagePreview({ state }: { state: ReturnType{buildStatus}
{previewMutation.error && {previewMutation.error.message}}
{t("browserExecution")}
- {parsed.success && parsed.data.manifest.entrypoints.server ? t("serverExecution") : t("browserOnlyExecution")}
+
+ {parsed.success && parsed.data.manifest.entrypoints.server
+ ? t("serverExecution")
+ : t("browserOnlyExecution")}
+
{t("trustedCode")}
{t("runtimeLogs")}
{detailsOpen && state.installationId && }
@@ -227,7 +231,9 @@ function PackagePreviewControls({ state }: { state: ReturnType{t("customViewport")}
- {t("customViewportDescription")}
+
+ {t("customViewportDescription")}
+
- {t("title")}
- {t("description")}
+
+ {t("title")}
+
+
+ {t("description")}
+
{data && {t(data.state)}}
- void status.refetch()}>{t("refresh")}
+ void status.refetch()}>
+ {t("refresh")}
+
{data && "pid" in data && (
- {t("process", { pid: data.pid ?? "—", active: data.activeRequests, queued: data.queuedRequests })}
+
+ {t("process", { pid: data.pid ?? "—", active: data.activeRequests, queued: data.queuedRequests })}
+
)}
{data && "error" in data && {data.error}}
{status.error && {status.error.message}}
- {t("logs")}
+
+ {t("logs")}
+
);
}
-
export function PackageSessionPreview({ previewId }: { previewId: string }) {
const t = useI18n("customWidget.package");
const { data, error, isLoading } = clientApi.customWidget.package.previewGet.useQuery(
diff --git a/apps/nextjs/src/app/[locale]/manage/custom-widgets/packages/_package-source-editor.tsx b/apps/nextjs/src/app/[locale]/manage/custom-widgets/packages/_package-source-editor.tsx
index 570a9c621..6ff5756d7 100644
--- a/apps/nextjs/src/app/[locale]/manage/custom-widgets/packages/_package-source-editor.tsx
+++ b/apps/nextjs/src/app/[locale]/manage/custom-widgets/packages/_package-source-editor.tsx
@@ -69,47 +69,52 @@ export function PackageSourceEditor({
{filesOpen && (
- setManageFiles((value) => !value)} aria-expanded={manageFiles}>
+ setManageFiles((value) => !value)}
+ aria-expanded={manageFiles}
+ >
{t("manageFiles")}
- setPath(event.currentTarget.value)}
- disabled={busy}
- />
-
- }
- onClick={addFile}
- disabled={busy || !path.trim()}
- >
- {t("addFile")}
-
-
- {t("renameFile")}
-
- }
- onClick={removeFile}
- disabled={busy || selected === "/widget.json"}
- >
- {t("removeFile")}
-
-
+ setPath(event.currentTarget.value)}
+ disabled={busy}
+ />
+
+ }
+ onClick={addFile}
+ disabled={busy || !path.trim()}
+ >
+ {t("addFile")}
+
+
+ {t("renameFile")}
+
+ }
+ onClick={removeFile}
+ disabled={busy || selected === "/widget.json"}
+ >
+ {t("removeFile")}
+
+
@@ -131,7 +136,11 @@ export function PackageSourceEditor({
);
}
-function PackageFileTree({ files, selected, onSelect }: {
+function PackageFileTree({
+ files,
+ selected,
+ onSelect,
+}: {
files: Record;
selected: string;
onSelect(path: string): void;
diff --git a/apps/nextjs/src/app/[locale]/manage/custom-widgets/packages/_package-workspace.tsx b/apps/nextjs/src/app/[locale]/manage/custom-widgets/packages/_package-workspace.tsx
index 661a449b5..3ca09b1b5 100644
--- a/apps/nextjs/src/app/[locale]/manage/custom-widgets/packages/_package-workspace.tsx
+++ b/apps/nextjs/src/app/[locale]/manage/custom-widgets/packages/_package-workspace.tsx
@@ -116,7 +116,9 @@ export function PackageWorkspace({
else editor.undo();
}}
>
- {t("workbenchTitle")}
+
+ {t("workbenchTitle")}
+
{status}
- {draftStatus}
+
+ {draftStatus}
+
{t("saveDraft")}
- { selectTab("installation"); setPane("edit"); }}>
+ {
+ selectTab("installation");
+ setPane("edit");
+ }}
+ >
{t("installation")}
@@ -149,7 +158,9 @@ export function PackageWorkspace({
{t("gettingStarted")}
- {t("gettingStartedDescription")}
+
+ {t("gettingStartedDescription")}
+
{t("manualSteps")}
@@ -168,7 +179,12 @@ export function PackageWorkspace({
{t("connectionsUnsaved")}
-
+
{t("saveBindings")}
@@ -227,11 +243,7 @@ export function PackageWorkspace({
{ value: "preview", label: t("previewPane") },
]}
/>
- }
- >
+ }>
@@ -260,10 +272,16 @@ export function PackageWorkspace({
{visitedTabs.has("options") && (
- {t("optionsDescription")}
- {parsed.success && }
+
+ {t("optionsDescription")}
+
+ {parsed.success && (
+
+ )}
{parsed.success && Object.keys(parsed.data.options).length === 0 && {t("noOptions")}}
- setOptions({})}>{t("resetOptions")}
+ setOptions({})}>
+ {t("resetOptions")}
+
)}
@@ -288,7 +306,9 @@ export function PackageWorkspace({
{visitedTabs.has("workshop") && (
- {t("workshopDescription")}
+
+ {t("workshopDescription")}
+
{current ? (
<>
@@ -299,7 +319,11 @@ export function PackageWorkspace({
onError={setError}
/>
>
- ) : void saveDraft()} loading={busy}>{t("saveDraft")}}
+ ) : (
+ void saveDraft()} loading={busy}>
+ {t("saveDraft")}
+
+ )}
)}
@@ -312,9 +336,6 @@ export function PackageWorkspace({
/* oxlint-disable jsx-a11y/prefer-tag-over-role -- A focusable window splitter is an interactive separator, not a thematic break. */
-
-
-
const defaultWidth = 60;
const clampWidth = (value: number, maximum = 75) => Math.min(maximum, Math.max(30, value));
diff --git a/packages/api/src/router/custom-widget/package/portable-collection-transfer.ts b/packages/api/src/router/custom-widget/package/portable-collection-transfer.ts
index a3aac00d4..8499ab040 100644
--- a/packages/api/src/router/custom-widget/package/portable-collection-transfer.ts
+++ b/packages/api/src/router/custom-widget/package/portable-collection-transfer.ts
@@ -8,7 +8,11 @@ import type {
WidgetCollectionContents,
WidgetCollectionManifest,
} from "@homarr/custom-widgets/package";
-import { getWidgetConnectionBindingNames, getWidgetConnectionRequirement, isWidgetConnectionCompatible } from "@homarr/custom-widgets/package";
+import {
+ getWidgetConnectionBindingNames,
+ getWidgetConnectionRequirement,
+ isWidgetConnectionCompatible,
+} from "@homarr/custom-widgets/package";
import { createWidgetCollectionArchive, validateWidgetCollectionArchive } from "@homarr/custom-widgets/package/server";
import { withWidgetArtifactReferenceLock } from "./artifact-references";
@@ -145,8 +149,16 @@ export async function exportWidgetCollection(ctx: PackageContext, manifest: Widg
const bindings = parseBindings(row.bindings);
const mapping: Record = {};
for (const [name, requirement] of Object.entries(source.connections)) {
- const members = getWidgetConnectionBindingNames(name, requirement, bindings).map((key) => bindings[key]).toSorted();
- const identity = JSON.stringify([requirement.kind, requirement.serviceType, requirement.integrationKind, requirement.multiple, members.length ? members : [id, name]]);
+ const members = getWidgetConnectionBindingNames(name, requirement, bindings)
+ .map((key) => bindings[key])
+ .toSorted();
+ const identity = JSON.stringify([
+ requirement.kind,
+ requirement.serviceType,
+ requirement.integrationKind,
+ requirement.multiple,
+ members.length ? members : [id, name],
+ ]);
let slot = shared.get(identity);
if (!slot) {
slot = `connection${shared.size + 1}`;
diff --git a/packages/api/src/router/custom-widget/package/sdk-bridge.ts b/packages/api/src/router/custom-widget/package/sdk-bridge.ts
index d3fddadda..465faa000 100644
--- a/packages/api/src/router/custom-widget/package/sdk-bridge.ts
+++ b/packages/api/src/router/custom-widget/package/sdk-bridge.ts
@@ -6,7 +6,13 @@ import { constructIntegrationPermissions } from "@homarr/auth/shared";
import { decryptSecret } from "@homarr/common/server";
import { createId } from "@homarr/common";
import { eq, inArray } from "@homarr/db";
-import { customWidgetConnections, groupMembers, integrationGroupPermissions, integrations, integrationUserPermissions } from "@homarr/db/schema";
+import {
+ customWidgetConnections,
+ groupMembers,
+ integrationGroupPermissions,
+ integrations,
+ integrationUserPermissions,
+} from "@homarr/db/schema";
import type { WidgetSdkInvocation } from "@homarr/custom-widgets/package/server";
import { readPackageHistory, widgetHistoryInputSchema } from "./history";
@@ -69,7 +75,9 @@ export function createWidgetSdkBridge(
return {
options: resolved.configuration,
bindingNames: Object.keys(resolved.bindings),
- bindingLabels: Object.fromEntries(Object.entries(resolved.bindings).map(([name, id]) => [name, labels.get(id) ?? name])),
+ bindingLabels: Object.fromEntries(
+ Object.entries(resolved.bindings).map(([name, id]) => [name, labels.get(id) ?? name]),
+ ),
installationId: resolved.installation.id,
isPreview: Boolean(resolved.preview),
};
diff --git a/packages/custom-widgets/src/workbench/package-document.ts b/packages/custom-widgets/src/workbench/package-document.ts
index aad5f59e6..cba0c0296 100644
--- a/packages/custom-widgets/src/workbench/package-document.ts
+++ b/packages/custom-widgets/src/workbench/package-document.ts
@@ -57,11 +57,7 @@ export function downloadPackage(name: string, value: unknown) {
const maximumHistoryBytes = 2_000_000;
const maximumRecoveryBytes = 1_000_000;
-export function usePackageDocument(
- initial: PackageDocument,
- userId: string,
- installationId?: string,
-) {
+export function usePackageDocument(initial: PackageDocument, userId: string, installationId?: string) {
const [document, setDocument] = useState(initial);
const [saved, setSaved] = useState(initial);
const [recovery, setRecovery] = useState(null);
diff --git a/packages/widget-sdk/examples/sonarr.json b/packages/widget-sdk/examples/sonarr.json
index bb0e2fee7..824dcbf74 100644
--- a/packages/widget-sdk/examples/sonarr.json
+++ b/packages/widget-sdk/examples/sonarr.json
@@ -21,10 +21,7 @@
"permission": "modify",
"inputSchema": {
"type": "object",
- "required": [
- "connection",
- "episodeIds"
- ],
+ "required": ["connection", "episodeIds"],
"additionalProperties": false,
"properties": {
"connection": {
diff --git a/packages/widgets/src/custom-api/package-connection-bindings.tsx b/packages/widgets/src/custom-api/package-connection-bindings.tsx
index 05f5cffc0..1eba6c00e 100644
--- a/packages/widgets/src/custom-api/package-connection-bindings.tsx
+++ b/packages/widgets/src/custom-api/package-connection-bindings.tsx
@@ -41,7 +41,9 @@ export function PackageConnectionBindings({
);
if (connection.multiple) {
const names = getWidgetConnectionBindingNames(name, connection, effectiveBindings);
- const selected = [...new Set(names.map((key) => effectiveBindings[key]!))];
+ const selected = [
+ ...new Set(names.map((key) => effectiveBindings[key]).filter((id) => typeof id === "string")),
+ ];
const choices = candidates.map((row) => ({ value: row.id, label: row.name }));
for (const id of selected) {
if (!choices.some((choice) => choice.value === id))
diff --git a/packages/widgets/src/widget-preview.tsx b/packages/widgets/src/widget-preview.tsx
index b9096f725..ef20394f9 100644
--- a/packages/widgets/src/widget-preview.tsx
+++ b/packages/widgets/src/widget-preview.tsx
@@ -24,7 +24,11 @@ import classes from "./modals/widget-edit-modal.module.css";
type Size = { width: number; height: number };
/** The same board grid controls and fitted canvas used by Edit Widget and package authoring. */
-export function WidgetPreviewFrame({ dimensions, resize, children }: {
+export function WidgetPreviewFrame({
+ dimensions,
+ resize,
+ children,
+}: {
dimensions: Size & { scale?: number };
resize?: { size: Size; maximumSize: Size; onChange(size: Size): void };
children(dimensions: Size & { scale: number }): ReactNode;
@@ -39,17 +43,26 @@ export function WidgetPreviewFrame({ dimensions, resize, children }: {
if (width > 0 && height > 0) scale = Math.min(width / sourceWidth, height / sourceHeight, scale);
const change = (dimension: keyof Size, value: string | number) => {
if (!resize || typeof value !== "number" || !Number.isFinite(value)) return;
- resize.onChange({ ...resize.size, [dimension]: Math.max(1, Math.min(Math.round(value), resize.maximumSize[dimension])) });
+ resize.onChange({
+ ...resize.size,
+ [dimension]: Math.max(1, Math.min(Math.round(value), resize.maximumSize[dimension])),
+ });
};
return (
{resize && (
- {t("item.edit.preview.size")}
+
+ {t("item.edit.preview.size")}
+
{(["width", "height"] as const).map((dimension) => (
- {dimension === "height" && ×}
+ {dimension === "height" && (
+
+ ×
+
+ )}