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} + )} - + )} @@ -110,12 +114,18 @@ export function PackageOperations({ {t("saveBeforeActivate")} - + )} {!dirty && !matchesReviewedSource && {t("savedSourceChanged")}} - onTrustChange(event.currentTarget.checked)} label={t("trustAcknowledgement")} /> + onTrustChange(event.currentTarget.checked)} + label={t("trustAcknowledgement")} + /> + {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}} - + ); } - 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 && ( - - setPath(event.currentTarget.value)} - disabled={busy} - /> - - - - - + setPath(event.currentTarget.value)} + disabled={busy} + /> + + + + + @@ -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} + - @@ -149,7 +158,9 @@ export function PackageWorkspace({ {t("gettingStarted")} - {t("gettingStartedDescription")} + + {t("gettingStartedDescription")} + {t("manualSteps")} @@ -168,7 +179,12 @@ export function PackageWorkspace({ {t("connectionsUnsaved")} - @@ -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")}} - + )} @@ -288,7 +306,9 @@ export function PackageWorkspace({ {visitedTabs.has("workshop") && ( - {t("workshopDescription")} + + {t("workshopDescription")} + {current ? ( <> @@ -299,7 +319,11 @@ export function PackageWorkspace({ onError={setError} /> - ) : } + ) : ( + + )} )} @@ -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" && ( + + × + + )}