fix: resolve widget platform CI lint and formatting failures
This commit is contained in:
1 parent
9ff1019a3d
commit
a29ec26c27
12 files changed
+248
-100
No files matched your search
@@ -61,13 +61,19 @@ export function PackageAssistant() {
|
||||
<Button component={Link} href="/manage/assistant" variant="light" size="xs">
|
||||
{t("setup")}
|
||||
</Button>
|
||||
<Text size="xs" c="dimmed">{t("manual")}</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{t("manual")}
|
||||
</Text>
|
||||
</Stack>
|
||||
)}
|
||||
{assistant?.enabled && (
|
||||
<>
|
||||
{assistant.isRunning && <Alert mb="xs">{t("busy")}</Alert>}
|
||||
{sendFailed && <Alert color="yellow" mb="xs">{t("sendFailed")}</Alert>}
|
||||
{sendFailed && (
|
||||
<Alert color="yellow" mb="xs">
|
||||
{t("sendFailed")}
|
||||
</Alert>
|
||||
)}
|
||||
<Text size="xs" c="dimmed" mb="xs">
|
||||
{t("context")}
|
||||
</Text>
|
||||
@@ -87,16 +93,40 @@ export function PackageAssistant() {
|
||||
</Group>
|
||||
<Divider my="xs" />
|
||||
<Stack gap={4}>
|
||||
<Button variant="subtle" size="xs" justify="start" disabled={assistant.isRunning} onClick={() => send(t("createPrompt"))}>
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="xs"
|
||||
justify="start"
|
||||
disabled={assistant.isRunning}
|
||||
onClick={() => send(t("createPrompt"))}
|
||||
>
|
||||
{t("create")}
|
||||
</Button>
|
||||
<Button variant="subtle" size="xs" justify="start" disabled={assistant.isRunning} onClick={() => send(t("improvePrompt"))}>
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="xs"
|
||||
justify="start"
|
||||
disabled={assistant.isRunning}
|
||||
onClick={() => send(t("improvePrompt"))}
|
||||
>
|
||||
{t("improve")}
|
||||
</Button>
|
||||
<Button variant="subtle" size="xs" justify="start" disabled={assistant.isRunning} onClick={() => send(t("advancedPrompt"))}>
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="xs"
|
||||
justify="start"
|
||||
disabled={assistant.isRunning}
|
||||
onClick={() => send(t("advancedPrompt"))}
|
||||
>
|
||||
{t("advanced")}
|
||||
</Button>
|
||||
<Button variant="subtle" size="xs" justify="start" disabled={assistant.isRunning} onClick={() => send(t("fixPrompt"))}>
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="xs"
|
||||
justify="start"
|
||||
disabled={assistant.isRunning}
|
||||
onClick={() => send(t("fixPrompt"))}
|
||||
>
|
||||
{t("fix")}
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
+31
-6
@@ -62,7 +62,9 @@ export function PackageConnections({
|
||||
/>
|
||||
{getWidgetConnectionBindingNames(name, requirement, bindings).map((bindingName) => (
|
||||
<Group key={bindingName} gap="xs">
|
||||
<Text size="sm" flex={1}>{connections.data?.find((entry) => entry.id === bindings[bindingName])?.name ?? bindingName}</Text>
|
||||
<Text size="sm" flex={1}>
|
||||
{connections.data?.find((entry) => entry.id === bindings[bindingName])?.name ?? bindingName}
|
||||
</Text>
|
||||
<Button
|
||||
variant="default"
|
||||
size="xs"
|
||||
@@ -104,11 +106,23 @@ export function PackageConnections({
|
||||
{integrations.error && <Alert color="red">{integrations.error.message}</Alert>}
|
||||
<Group>
|
||||
{Object.values(requirements).some((requirement) => requirement.kind === "integration") && (
|
||||
<Button component={Link} href="/manage/integrations/new" target="_blank" rel="noopener noreferrer" variant="light">
|
||||
<Button
|
||||
component={Link}
|
||||
href="/manage/integrations/new"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="light"
|
||||
>
|
||||
{t("createIntegration")}
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="subtle" onClick={() => { void connections.refetch(); void integrations.refetch(); }}>
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => {
|
||||
void connections.refetch();
|
||||
void integrations.refetch();
|
||||
}}
|
||||
>
|
||||
{t("refresh")}
|
||||
</Button>
|
||||
<Button
|
||||
@@ -176,8 +190,16 @@ function PackageBindingPicker({
|
||||
mounted.current = false;
|
||||
};
|
||||
}, []);
|
||||
const ids = [...new Set(getWidgetConnectionBindingNames(name, requirement, bindings).map((key) => 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;
|
||||
|
||||
+18
-5
@@ -91,12 +91,16 @@ export function PackageOperations({
|
||||
};
|
||||
return (
|
||||
<Stack>
|
||||
<Text size="sm" c="dimmed">{t("useDescription")}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("useDescription")}
|
||||
</Text>
|
||||
{!current && (
|
||||
<Paper withBorder p="md">
|
||||
<Stack>
|
||||
<Text>{t("saveFirst")}</Text>
|
||||
<Button onClick={onSave} loading={saving}>{t("saveDraft")}</Button>
|
||||
<Button onClick={onSave} loading={saving}>
|
||||
{t("saveDraft")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Paper>
|
||||
)}
|
||||
@@ -110,12 +114,18 @@ export function PackageOperations({
|
||||
<Alert color="yellow">
|
||||
<Stack gap="xs">
|
||||
<Text size="sm">{t("saveBeforeActivate")}</Text>
|
||||
<Button size="xs" onClick={onSave} loading={saving}>{t("saveDraft")}</Button>
|
||||
<Button size="xs" onClick={onSave} loading={saving}>
|
||||
{t("saveDraft")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Alert>
|
||||
)}
|
||||
{!dirty && !matchesReviewedSource && <Alert color="yellow">{t("savedSourceChanged")}</Alert>}
|
||||
<Checkbox checked={trusted} onChange={(event) => onTrustChange(event.currentTarget.checked)} label={t("trustAcknowledgement")} />
|
||||
<Checkbox
|
||||
checked={trusted}
|
||||
onChange={(event) => onTrustChange(event.currentTarget.checked)}
|
||||
label={t("trustAcknowledgement")}
|
||||
/>
|
||||
<Group>
|
||||
<Button
|
||||
disabled={dirty || connectionsDirty || !trusted || !source || !matchesReviewedSource}
|
||||
@@ -151,7 +161,10 @@ export function PackageOperations({
|
||||
variant="default"
|
||||
loading={enable.isPending}
|
||||
onClick={() =>
|
||||
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")}
|
||||
|
||||
@@ -176,7 +176,11 @@ export function PackagePreview({ state }: { state: ReturnType<typeof usePackageW
|
||||
<Badge variant="light">{buildStatus}</Badge>
|
||||
{previewMutation.error && <Alert color="red">{previewMutation.error.message}</Alert>}
|
||||
<Text size="sm">{t("browserExecution")}</Text>
|
||||
<Text size="sm">{parsed.success && parsed.data.manifest.entrypoints.server ? t("serverExecution") : t("browserOnlyExecution")}</Text>
|
||||
<Text size="sm">
|
||||
{parsed.success && parsed.data.manifest.entrypoints.server
|
||||
? t("serverExecution")
|
||||
: t("browserOnlyExecution")}
|
||||
</Text>
|
||||
<Text size="sm">{t("trustedCode")}</Text>
|
||||
<Text size="sm">{t("runtimeLogs")}</Text>
|
||||
{detailsOpen && state.installationId && <PackageRuntimeStatus id={state.installationId} />}
|
||||
@@ -227,7 +231,9 @@ function PackagePreviewControls({ state }: { state: ReturnType<typeof usePackage
|
||||
<Accordion.Control>{t("customViewport")}</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<Stack gap="xs">
|
||||
<Text size="xs" c="dimmed">{t("customViewportDescription")}</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{t("customViewportDescription")}
|
||||
</Text>
|
||||
<Group gap="xs" grow>
|
||||
<NumberInput
|
||||
size="xs"
|
||||
@@ -276,23 +282,39 @@ function PackageRuntimeStatus({ id }: { id: string }) {
|
||||
const data = status.data;
|
||||
return (
|
||||
<Stack gap="xs">
|
||||
<Text size="sm" fw={600}>{t("title")}</Text>
|
||||
<Text size="xs" c="dimmed">{t("description")}</Text>
|
||||
<Text size="sm" fw={600}>
|
||||
{t("title")}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{t("description")}
|
||||
</Text>
|
||||
<Group>
|
||||
{data && <Badge variant="light">{t(data.state)}</Badge>}
|
||||
<Button variant="subtle" size="xs" loading={status.isFetching} onClick={() => void status.refetch()}>{t("refresh")}</Button>
|
||||
<Button variant="subtle" size="xs" loading={status.isFetching} onClick={() => void status.refetch()}>
|
||||
{t("refresh")}
|
||||
</Button>
|
||||
</Group>
|
||||
{data && "pid" in data && (
|
||||
<Text size="xs">{t("process", { pid: data.pid ?? "—", active: data.activeRequests, queued: data.queuedRequests })}</Text>
|
||||
<Text size="xs">
|
||||
{t("process", { pid: data.pid ?? "—", active: data.activeRequests, queued: data.queuedRequests })}
|
||||
</Text>
|
||||
)}
|
||||
{data && "error" in data && <Alert color="red">{data.error}</Alert>}
|
||||
{status.error && <Alert color="red">{status.error.message}</Alert>}
|
||||
<Button component={Link} href="/manage/tools/logs" target="_blank" rel="noopener noreferrer" variant="default" size="xs">{t("logs")}</Button>
|
||||
<Button
|
||||
component={Link}
|
||||
href="/manage/tools/logs"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="default"
|
||||
size="xs"
|
||||
>
|
||||
{t("logs")}
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export function PackageSessionPreview({ previewId }: { previewId: string }) {
|
||||
const t = useI18n("customWidget.package");
|
||||
const { data, error, isLoading } = clientApi.customWidget.package.previewGet.useQuery(
|
||||
|
||||
+47
-38
@@ -69,47 +69,52 @@ export function PackageSourceEditor({
|
||||
{filesOpen && (
|
||||
<Stack gap="xs" className={classes.files}>
|
||||
<PackageFileTree files={document.files} selected={selected} onSelect={setSelected} />
|
||||
<Button size="compact-xs" variant="subtle" onClick={() => setManageFiles((value) => !value)} aria-expanded={manageFiles}>
|
||||
<Button
|
||||
size="compact-xs"
|
||||
variant="subtle"
|
||||
onClick={() => setManageFiles((value) => !value)}
|
||||
aria-expanded={manageFiles}
|
||||
>
|
||||
{t("manageFiles")}
|
||||
</Button>
|
||||
<Collapse expanded={manageFiles}>
|
||||
<Stack gap="xs">
|
||||
<TextInput
|
||||
aria-label={t("filePath")}
|
||||
placeholder={t("filePathExample")}
|
||||
value={path}
|
||||
onChange={(event) => setPath(event.currentTarget.value)}
|
||||
disabled={busy}
|
||||
/>
|
||||
<Group gap={4}>
|
||||
<Button
|
||||
size="compact-xs"
|
||||
variant="subtle"
|
||||
leftSection={<IconPlus size={12} />}
|
||||
onClick={addFile}
|
||||
disabled={busy || !path.trim()}
|
||||
>
|
||||
{t("addFile")}
|
||||
</Button>
|
||||
<Button
|
||||
size="compact-xs"
|
||||
variant="subtle"
|
||||
onClick={renameFile}
|
||||
disabled={busy || !path.trim() || selected === "/widget.json"}
|
||||
>
|
||||
{t("renameFile")}
|
||||
</Button>
|
||||
<Button
|
||||
size="compact-xs"
|
||||
color="red"
|
||||
variant="subtle"
|
||||
leftSection={<IconTrash size={12} />}
|
||||
onClick={removeFile}
|
||||
disabled={busy || selected === "/widget.json"}
|
||||
>
|
||||
{t("removeFile")}
|
||||
</Button>
|
||||
</Group>
|
||||
<TextInput
|
||||
aria-label={t("filePath")}
|
||||
placeholder={t("filePathExample")}
|
||||
value={path}
|
||||
onChange={(event) => setPath(event.currentTarget.value)}
|
||||
disabled={busy}
|
||||
/>
|
||||
<Group gap={4}>
|
||||
<Button
|
||||
size="compact-xs"
|
||||
variant="subtle"
|
||||
leftSection={<IconPlus size={12} />}
|
||||
onClick={addFile}
|
||||
disabled={busy || !path.trim()}
|
||||
>
|
||||
{t("addFile")}
|
||||
</Button>
|
||||
<Button
|
||||
size="compact-xs"
|
||||
variant="subtle"
|
||||
onClick={renameFile}
|
||||
disabled={busy || !path.trim() || selected === "/widget.json"}
|
||||
>
|
||||
{t("renameFile")}
|
||||
</Button>
|
||||
<Button
|
||||
size="compact-xs"
|
||||
color="red"
|
||||
variant="subtle"
|
||||
leftSection={<IconTrash size={12} />}
|
||||
onClick={removeFile}
|
||||
disabled={busy || selected === "/widget.json"}
|
||||
>
|
||||
{t("removeFile")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Collapse>
|
||||
</Stack>
|
||||
@@ -131,7 +136,11 @@ export function PackageSourceEditor({
|
||||
);
|
||||
}
|
||||
|
||||
function PackageFileTree({ files, selected, onSelect }: {
|
||||
function PackageFileTree({
|
||||
files,
|
||||
selected,
|
||||
onSelect,
|
||||
}: {
|
||||
files: Record<string, string>;
|
||||
selected: string;
|
||||
onSelect(path: string): void;
|
||||
|
||||
+39
-18
@@ -116,7 +116,9 @@ export function PackageWorkspace({
|
||||
else editor.undo();
|
||||
}}
|
||||
>
|
||||
<Title order={1} size="h2">{t("workbenchTitle")}</Title>
|
||||
<Title order={1} size="h2">
|
||||
{t("workbenchTitle")}
|
||||
</Title>
|
||||
<Group justify="space-between" align="end" data-package-document>
|
||||
<TextInput
|
||||
flex={1}
|
||||
@@ -131,7 +133,9 @@ export function PackageWorkspace({
|
||||
{t("library")}
|
||||
</Button>
|
||||
<Badge variant="light">{status}</Badge>
|
||||
<Text size="xs" c="dimmed">{draftStatus}</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{draftStatus}
|
||||
</Text>
|
||||
<Button
|
||||
variant="default"
|
||||
loading={save.isPending}
|
||||
@@ -140,7 +144,12 @@ export function PackageWorkspace({
|
||||
>
|
||||
{t("saveDraft")}
|
||||
</Button>
|
||||
<Button onClick={() => { selectTab("installation"); setPane("edit"); }}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
selectTab("installation");
|
||||
setPane("edit");
|
||||
}}
|
||||
>
|
||||
{t("installation")}
|
||||
</Button>
|
||||
</Group>
|
||||
@@ -149,7 +158,9 @@ export function PackageWorkspace({
|
||||
<Paper withBorder p="md">
|
||||
<Stack gap="xs">
|
||||
<Text fw={600}>{t("gettingStarted")}</Text>
|
||||
<Text size="sm" c="dimmed">{t("gettingStartedDescription")}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("gettingStartedDescription")}
|
||||
</Text>
|
||||
<Text size="sm">{t("manualSteps")}</Text>
|
||||
</Stack>
|
||||
</Paper>
|
||||
@@ -168,7 +179,12 @@ export function PackageWorkspace({
|
||||
<Alert color="yellow">
|
||||
<Group justify="space-between">
|
||||
<Text size="sm">{t("connectionsUnsaved")}</Text>
|
||||
<Button size="xs" loading={saveBindings.isPending || save.isPending} disabled={busy} onClick={saveLocalBindings}>
|
||||
<Button
|
||||
size="xs"
|
||||
loading={saveBindings.isPending || save.isPending}
|
||||
disabled={busy}
|
||||
onClick={saveLocalBindings}
|
||||
>
|
||||
{t("saveBindings")}
|
||||
</Button>
|
||||
</Group>
|
||||
@@ -227,11 +243,7 @@ export function PackageWorkspace({
|
||||
{ value: "preview", label: t("previewPane") },
|
||||
]}
|
||||
/>
|
||||
<PackageSplit
|
||||
userId={userId}
|
||||
pane={pane}
|
||||
preview={<PackagePreview state={workspace} />}
|
||||
>
|
||||
<PackageSplit userId={userId} pane={pane} preview={<PackagePreview state={workspace} />}>
|
||||
<Stack className={classes.authoring}>
|
||||
<Tabs value={tab} onChange={selectTab}>
|
||||
<Tabs.List>
|
||||
@@ -260,10 +272,16 @@ export function PackageWorkspace({
|
||||
<Tabs.Panel value="options" pt="md">
|
||||
{visitedTabs.has("options") && (
|
||||
<Stack>
|
||||
<Text size="sm" c="dimmed">{t("optionsDescription")}</Text>
|
||||
{parsed.success && <PackageOptionFields schema={parsed.data.options} value={effectiveOptions} onChange={setOptions} />}
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("optionsDescription")}
|
||||
</Text>
|
||||
{parsed.success && (
|
||||
<PackageOptionFields schema={parsed.data.options} value={effectiveOptions} onChange={setOptions} />
|
||||
)}
|
||||
{parsed.success && Object.keys(parsed.data.options).length === 0 && <Alert>{t("noOptions")}</Alert>}
|
||||
<Button variant="subtle" onClick={() => setOptions({})}>{t("resetOptions")}</Button>
|
||||
<Button variant="subtle" onClick={() => setOptions({})}>
|
||||
{t("resetOptions")}
|
||||
</Button>
|
||||
</Stack>
|
||||
)}
|
||||
</Tabs.Panel>
|
||||
@@ -288,7 +306,9 @@ export function PackageWorkspace({
|
||||
<Tabs.Panel value="workshop" pt="md">
|
||||
{visitedTabs.has("workshop") && (
|
||||
<Stack>
|
||||
<Text size="sm" c="dimmed">{t("workshopDescription")}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("workshopDescription")}
|
||||
</Text>
|
||||
{current ? (
|
||||
<>
|
||||
<PackageWorkshop id={current.id} dirty={editor.dirty || workspace.bindingsDirty || busy} />
|
||||
@@ -299,7 +319,11 @@ export function PackageWorkspace({
|
||||
onError={setError}
|
||||
/>
|
||||
</>
|
||||
) : <Button onClick={() => void saveDraft()} loading={busy}>{t("saveDraft")}</Button>}
|
||||
) : (
|
||||
<Button onClick={() => void saveDraft()} loading={busy}>
|
||||
{t("saveDraft")}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</Tabs.Panel>
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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<string, string> = {};
|
||||
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}`;
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
@@ -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<PackageDocument | null>(null);
|
||||
|
||||
@@ -21,10 +21,7 @@
|
||||
"permission": "modify",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"connection",
|
||||
"episodeIds"
|
||||
],
|
||||
"required": ["connection", "episodeIds"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"connection": {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 (
|
||||
<Stack className={classes.previewPanel} gap={0}>
|
||||
<Center ref={ref} className={classes.previewCanvas}>
|
||||
{resize && (
|
||||
<Group className={classes.previewSizeControls} gap={6} wrap="nowrap">
|
||||
<Text size="xs" fw={600} c="dimmed">{t("item.edit.preview.size")}</Text>
|
||||
<Text size="xs" fw={600} c="dimmed">
|
||||
{t("item.edit.preview.size")}
|
||||
</Text>
|
||||
{(["width", "height"] as const).map((dimension) => (
|
||||
<Group key={dimension} gap={6} wrap="nowrap">
|
||||
{dimension === "height" && <Text size="xs" c="dimmed" aria-hidden>×</Text>}
|
||||
{dimension === "height" && (
|
||||
<Text size="xs" c="dimmed" aria-hidden>
|
||||
×
|
||||
</Text>
|
||||
)}
|
||||
<Tooltip label={t(`item.moveResize.field.${dimension}.label`)}>
|
||||
<NumberInput
|
||||
className={classes.previewSizeInput}
|
||||
|
||||
Reference in new issue
Block a user