fix: address PR review feedback

- Show error notification on setup failure instead of silent reset
- Fix toggleKind stale closure by using prop array directly
- Remove duplicate translations from en-gb.json (falls back to en.json)
- Align seed board columnCount to 12 (codebase default)
- Extract shared placeAllWidgetsAsync helper to @homarr/db/queries
- Remove redundant onboardingFormSchema; reuse formSchema for both paths
- Fix lint errors (optional chain, unused imports, unnecessary conditions)
This commit is contained in:
Thomas Camlong committed 2026-05-27 15:53:14 +02:00
1 parent c97513a61c
commit a6566f74c9
15 files changed
+233 -338

No files matched your search

@@ -14,7 +14,7 @@ import {
ThemeIcon,
UnstyledButton,
} from "@mantine/core";
import { IconChevronDown, IconBrandDocker } from "@tabler/icons-react";
import { IconBrandDocker, IconChevronDown } from "@tabler/icons-react";
import type { IntegrationKind } from "@homarr/definitions";
import { getIntegrationName } from "@homarr/definitions";
@@ -128,21 +128,17 @@ export const DockerDiscoveryIndicator = ({
onChange={() => onToggleAllApps(!appsEnabled)}
styles={{ label: { fontSize: "var(--mantine-font-size-xs)" } }}
/>
{appsEnabled && apps.map((app) => (
<Group key={app.containerId} gap="xs" wrap="nowrap" py={2}>
<Avatar
size="xs"
radius="sm"
src={app.iconUrl}
styles={{ image: { objectFit: "contain" } }}
>
{app.containerName.at(0)?.toUpperCase()}
</Avatar>
<Text size="xs" c="dimmed" lineClamp={1} style={{ flex: 1, minWidth: 0 }}>
{app.containerName}
</Text>
</Group>
))}
{appsEnabled &&
apps.map((app) => (
<Group key={app.containerId} gap="xs" wrap="nowrap" py={2}>
<Avatar size="xs" radius="sm" src={app.iconUrl} styles={{ image: { objectFit: "contain" } }}>
{app.containerName.at(0)?.toUpperCase()}
</Avatar>
<Text size="xs" c="dimmed" lineClamp={1} style={{ flex: 1, minWidth: 0 }}>
{app.containerName}
</Text>
</Group>
))}
</>
)}
</Stack>
@@ -21,15 +21,14 @@ import { useDisclosure } from "@mantine/hooks";
import { IconArrowRight, IconSettings } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import type { IntegrationKind } from "@homarr/definitions";
import type { IntegrationKind, UrlTemplateMode } from "@homarr/definitions";
import { buildIntegrationUrl, getIntegrationName } from "@homarr/definitions";
import type { UrlTemplateMode } from "@homarr/definitions";
import { showErrorNotification } from "@homarr/notifications";
import { useScopedI18n } from "@homarr/translation/client";
import { IntegrationAvatar } from "@homarr/ui";
import { NewIntegrationForm } from "~/app/[locale]/manage/integrations/new/_integration-new-form";
import { IntegrationMultiSelectGrid } from "~/components/integration/integration-multi-select-grid";
import { DockerDiscoveryIndicator } from "./docker-discovery-panel";
type Phase = "select" | "configure" | "done";
@@ -69,7 +68,7 @@ export const InitIntegrations = () => {
const dockerApplied = useRef(false);
useEffect(() => {
if (!dockerData || dockerData.status !== "success" || dockerApplied.current) return;
if (dockerData?.status !== "success" || dockerApplied.current) return;
dockerApplied.current = true;
const newDrafts = new Map(drafts);
@@ -132,6 +131,10 @@ export const InitIntegrations = () => {
await setupIntegrations();
router.refresh();
} catch {
showErrorNotification({
title: t("configure.error.title"),
message: t("configure.error.message"),
});
setPhase("select");
}
};
@@ -192,7 +195,9 @@ export const InitIntegrations = () => {
/>
<Stack gap="xs">
<Text size="sm" fw={500}>{t("urlTemplate.label")}</Text>
<Text size="sm" fw={500}>
{t("urlTemplate.label")}
</Text>
<Group gap="sm" grow>
<TextInput
placeholder={t("urlTemplate.placeholder")}
@@ -216,7 +221,11 @@ export const InitIntegrations = () => {
</Text>
</Stack>
<IntegrationMultiSelectGrid selectedKinds={selectedKinds} onSelectionChange={handleSelectionChange} onboarding />
<IntegrationMultiSelectGrid
selectedKinds={selectedKinds}
onSelectionChange={handleSelectionChange}
onboarding
/>
<Group justify="space-between">
<Button variant="subtle" onClick={() => void finishSetupAsync()}>
@@ -280,11 +289,7 @@ const ConfigurePhase = ({
<Text fw={500}>{getIntegrationName(currentKind)}</Text>
</Group>
<Tooltip label={t("configure.helpers.tooltip")}>
<ActionIcon
variant={helpersOpen ? "light" : "subtle"}
color="gray"
onClick={toggleHelpers}
>
<ActionIcon variant={helpersOpen ? "light" : "subtle"} color="gray" onClick={toggleHelpers}>
<IconSettings size={18} stroke={1.5} />
</ActionIcon>
</Tooltip>
@@ -298,7 +303,11 @@ const ConfigurePhase = ({
</Stack>
<Collapse in={helpersOpen}>
<Stack gap="xs" p="xs" style={{ background: "var(--mantine-color-default-hover)", borderRadius: "var(--mantine-radius-sm)" }}>
<Stack
gap="xs"
p="xs"
style={{ background: "var(--mantine-color-default-hover)", borderRadius: "var(--mantine-radius-sm)" }}
>
<Text size="xs" c="dimmed">
{t("configure.helpers.description")}
</Text>
@@ -322,7 +331,9 @@ const ConfigurePhase = ({
</Group>
{baseHost && (
<Text size="xs" c="dimmed">
{t("urlTemplate.preview", { example: buildIntegrationUrl(currentKind, baseHost, urlMode, draft?.dockerPort ?? undefined) })}
{t("urlTemplate.preview", {
example: buildIntegrationUrl(currentKind, baseHost, urlMode, draft?.dockerPort ?? undefined),
})}
</Text>
)}
</Stack>
@@ -63,17 +63,6 @@ const formSchema = integrationCreateSchema.omit({ kind: true, app: true }).and(
}),
);
const onboardingFormSchema = z
.object({
name: z.string().nonempty().max(127),
url: z.string().nonempty(),
secrets: z.array(z.object({ kind: z.string(), value: z.string() })),
attemptSearchEngineCreation: z.boolean(),
hasApp: z.boolean(),
appHref: appHrefSchema,
appId: z.string().nullable(),
});
export const NewIntegrationForm = ({
kind,
initialUrl,
@@ -93,7 +82,7 @@ export const NewIntegrationForm = ({
if (hasUrlSecret) {
url = "http://localhost";
}
const form = useZodForm(isOnboarding ? onboardingFormSchema : formSchema, {
const form = useZodForm(formSchema, {
initialValues: {
name: initialName ?? getIntegrationName(kind),
url,
@@ -68,7 +68,9 @@ export interface IntegrationGridItem {
widgets: WidgetKind[];
}
export const buildSortedIntegrations = (options: { enableMockIntegration?: boolean; onboarding?: boolean } = {}): IntegrationGridItem[] =>
export const buildSortedIntegrations = (
options: { enableMockIntegration?: boolean; onboarding?: boolean } = {},
): IntegrationGridItem[] =>
integrationKinds
.filter((kind) => {
if (options.onboarding && hiddenFromOnboarding.has(kind)) return false;
@@ -34,18 +34,21 @@ export const IntegrationMultiSelectGrid = ({
const toggleKind = useCallback(
(kind: IntegrationKind) => {
const next = new Set(selectedKinds);
if (next.has(kind)) {
next.delete(kind);
const current = new Set(selectedKindsArray);
if (current.has(kind)) {
current.delete(kind);
} else {
next.add(kind);
current.add(kind);
}
onSelectionChange(Array.from(next));
onSelectionChange(Array.from(current));
},
[selectedKinds, onSelectionChange],
[selectedKindsArray, onSelectionChange],
);
const integrations = useMemo(() => buildSortedIntegrations({ enableMockIntegration, onboarding }), [enableMockIntegration, onboarding]);
const integrations = useMemo(
() => buildSortedIntegrations({ enableMockIntegration, onboarding }),
[enableMockIntegration, onboarding],
);
const filtered = useMemo(() => filterIntegrations(integrations, search), [integrations, search]);
return (