fix: address onboarding review findings

This commit is contained in:
ajnart committed 2026-08-14 11:32:13 +02:00
1 parent 9b7f189b02
commit 741e641bd2
6 files changed
+77 -22

No files matched your search

@@ -2,7 +2,11 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { waitForServerReadinessAsync } from "./database-restore-flow";
import { getStepAfterRestorePreview, waitForServerReadinessAsync } from "./database-restore-flow";
it("requires confirmation after every restore preview", () => {
expect(getStepAfterRestorePreview()).toBe("confirm");
});
describe("waitForServerReadinessAsync", () => {
beforeEach(() => {
@@ -29,6 +29,8 @@ const RESTART_REQUEST_TIMEOUT_MS = 5_000;
export type ServerReadinessResult = "ready" | "timedOut" | "aborted";
export const getStepAfterRestorePreview = (): RestoreStep => "confirm";
interface WaitForServerReadinessOptions {
restartAfterMs: number;
signal: AbortSignal;
@@ -289,11 +291,8 @@ export const DatabaseRestoreFlow = ({ variant = "card", onRestoreComplete }: Dat
<>
<BackupPreviewPanel analysis={analysis} />
<Group justify="flex-end">
<Button
rightSection={<IconArrowRight size={16} />}
onClick={() => (variant === "standalone" ? void handleConfirm() : setStep("confirm"))}
>
{variant === "standalone" ? t("confirm.submit") : t("continueToRestore")}
<Button rightSection={<IconArrowRight size={16} />} onClick={() => setStep(getStepAfterRestorePreview())}>
{t("continueToRestore")}
</Button>
</Group>
</>
@@ -0,0 +1,36 @@
import { describe, expect, it } from "vitest";
import { getBoardValidationErrors } from "./board-validation";
describe("getBoardValidationErrors", () => {
it("keeps attempted board errors visible until each value is corrected", () => {
expect(
getBoardValidationErrors({
attempted: true,
hasExistingBoards: true,
selectedBoardId: null,
boardName: "",
}),
).toEqual({ target: true, name: true });
expect(
getBoardValidationErrors({
attempted: true,
hasExistingBoards: true,
selectedBoardId: "board-id",
boardName: "dashboard",
}),
).toEqual({ target: false, name: false });
});
it("does not require a target when setup has no existing board", () => {
expect(
getBoardValidationErrors({
attempted: true,
hasExistingBoards: false,
selectedBoardId: null,
boardName: "dashboard",
}).target,
).toBe(false);
});
});
@@ -0,0 +1,16 @@
interface BoardValidationInput {
attempted: boolean;
hasExistingBoards: boolean;
selectedBoardId: string | null;
boardName: string;
}
export const getBoardValidationErrors = ({
attempted,
hasExistingBoards,
selectedBoardId,
boardName,
}: BoardValidationInput) => ({
target: attempted && hasExistingBoards && !selectedBoardId,
name: attempted && !boardName.trim(),
});
+14 -16
View File
@@ -96,6 +96,7 @@ import { BoardColorInput, ColorSchemeCombobox, IntegrationAvatar, LanguageCombob
import { IntegrationMultiSelectGrid } from "@homarr/ui/integration-select-grid";
import type { OnboardingStudioProps } from "./types";
import { getBoardValidationErrors } from "./board-validation";
import { OnboardingBackdrop } from "./onboarding-backdrop";
import { OnboardingWordmark } from "./onboarding-wordmark";
import { useOnboardingSounds } from "./use-onboarding-sounds";
@@ -230,6 +231,7 @@ export const SetupStudio = ({ environment, assistantConfiguration }: OnboardingS
const [columnCount, setColumnCount] = useState(10);
const [leftSidebar, setLeftSidebar] = useState(false);
const [rightSidebar, setRightSidebar] = useState(false);
const [boardValidationAttempted, setBoardValidationAttempted] = useState(false);
const [isApplying, setIsApplying] = useState(false);
const [applyProgress, setApplyProgress] = useState(0);
const [applyMessage, setApplyMessage] = useState("");
@@ -255,8 +257,6 @@ export const SetupStudio = ({ environment, assistantConfiguration }: OnboardingS
showErrorNotification({ title: t("review.errorTitle"), message: error.message });
},
});
const validateIntegration = clientApi.onboard.testIntegration.useMutation();
const dockerData = docker.data;
const discoveredIntegrations = dockerData?.integrations ?? emptyDiscoveredIntegrations;
const discoveredApps = dockerData?.apps ?? emptyDiscoveredApps;
@@ -423,6 +423,7 @@ export const SetupStudio = ({ environment, assistantConfiguration }: OnboardingS
setAppError(null);
setAppErrorSourceId(null);
setApplyProgress(5);
setBoardValidationAttempted(true);
try {
if (!selectedBoardId && environment.availableBoards.length > 0) {
setActiveSection("board");
@@ -444,18 +445,6 @@ export const SetupStudio = ({ environment, assistantConfiguration }: OnboardingS
}
setApplyMessage(t("review.progress.applying"));
for (const [index, draft] of draftsToApply.entries()) {
setApplyProgress(5 + Math.round(((index + 1) / Math.max(1, draftsToApply.length)) * 15));
await validateIntegration
.mutateAsync({
sourceId: draft.sourceId ?? draft.id,
name: draft.name,
url: normalizeServiceUrl(draft.url) ?? draft.url,
kind: draft.kind,
secrets: draft.secrets,
})
.catch(() => undefined);
}
setApplyProgress(20);
await new Promise((resolve) => setTimeout(resolve, 300));
const selectedSourceIds = new Set(draftsToApply.flatMap((draft) => (draft.sourceId ? [draft.sourceId] : [])));
@@ -607,6 +596,7 @@ export const SetupStudio = ({ environment, assistantConfiguration }: OnboardingS
boardName,
setBoardName,
selectedBoardId,
boardValidationAttempted,
setSelectedBoardId: (value) => {
sounds.click();
setSelectedBoardId(value);
@@ -852,6 +842,7 @@ interface StudioSectionProps {
boardName: string;
setBoardName: (value: string) => void;
selectedBoardId: string | null;
boardValidationAttempted: boolean;
setSelectedBoardId: (value: string | null) => void;
primaryColor: string;
setPrimaryColor: (value: string) => void;
@@ -1449,6 +1440,12 @@ const formatSecretKind = (kind: IntegrationSecretKind) =>
const BoardBuilder = (props: StudioSectionProps) => {
const t = useScopedI18n("init.studio.board");
const sounds = useOnboardingSounds();
const validationErrors = getBoardValidationErrors({
attempted: props.boardValidationAttempted,
hasExistingBoards: props.environment.availableBoards.length > 0,
selectedBoardId: props.selectedBoardId,
boardName: props.boardName,
});
const previewWidgetKinds = getPreviewWidgetKinds(props);
const previewAppCount = getPreviewAppCount(props);
const colorsCustomized =
@@ -1474,7 +1471,7 @@ const BoardBuilder = (props: StudioSectionProps) => {
allowDeselect={false}
searchable
withAsterisk
error={!props.selectedBoardId && props.applyProgress > 0 ? t("targetRequired") : undefined}
error={validationErrors.target ? t("targetRequired") : undefined}
/>
) : null}
<TextInput
@@ -1483,6 +1480,7 @@ const BoardBuilder = (props: StudioSectionProps) => {
value={props.boardName}
onChange={(event) => props.setBoardName(event.currentTarget.value.replace(/[^A-Za-z0-9-_]/g, ""))}
withAsterisk
error={validationErrors.name ? t("nameRequired") : undefined}
/>
<Stack gap="xs">
<SimpleGrid cols={2}>
@@ -1511,7 +1509,7 @@ const BoardBuilder = (props: StudioSectionProps) => {
}}
w="fit-content"
>
Reset colors
{t("resetColors")}
</Button>
) : null}
</Stack>
+2
View File
@@ -144,8 +144,10 @@
"targetRequired": "Select the board to configure before continuing.",
"name": "Board name",
"nameDescription": "Used in the board URL; letters, numbers, dashes, and underscores only.",
"nameRequired": "Enter a board name before continuing.",
"primaryColor": "Primary color",
"secondaryColor": "Secondary color",
"resetColors": "Reset colors",
"radius": "Widget corner radius",
"columns": "Board columns",
"columnsDescription": "Columns define the horizontal grid used to size and position items. 12 is plenty for most people.",