Compare commits

...
Author SHA1 Message Date
Thomas Camlong 946dc8ac23 fix: shell syntax error in lint-format-autofix job with paths containing parentheses
When a changed file path contains parentheses (e.g. boards/(content)/_theme.tsx),
GitHub Actions expands ${{ steps.changed.outputs.files }} directly into the
shell script, causing bash to interpret '(' as subshell syntax and fail.

Fix: write changed files to a temp file and use xargs -a to read them,
completely bypassing shell interpretation of the paths.
2026-05-27 09:57:50 +02:00
Thomas Camlong 4c479e4550 Merge branch 'dev' into renovate/major-mantine-monorepo 2026-05-27 09:46:49 +02:00
Thomas Camlong eb5c4375d6 Format widget dynamic select input after Mantine v9 fixes. 2026-05-20 14:35:07 +02:00
Thomas Camlong 9bbce7a20f Fix Mantine v9 CI failures for typecheck, lint, and e2e.
Align form generics with v9 TransformedValues typing, merge element-size refs for gridstack, and tighten selectors and lint fixes from the migration.
2026-05-20 14:24:26 +02:00
Thomas Camlong cb5b73ddbd chore: migrate to mantine v9
Followed the migration guide
2026-05-19 14:58:51 +02:00
homarr-renovate[bot] df56eeab95 chore(deps): update mantine monorepo to v9 2026-05-18 20:31:33 +00:00
29 changed files with 707 additions and 455 deletions

No files matched your search

+5 -5
View File
@@ -42,14 +42,14 @@ jobs:
- name: Get changed files - name: Get changed files
id: changed id: changed
run: | run: |
FILES=$(git diff --name-only --diff-filter=ACMR origin/${{ github.base_ref }}...HEAD -- '*.ts' '*.tsx' '*.js' '*.jsx' | tr '\n' ' ') git diff --name-only --diff-filter=ACMR origin/${{ github.base_ref }}...HEAD -- '*.ts' '*.tsx' '*.js' '*.jsx' > /tmp/changed-files
echo "files=$FILES" >> $GITHUB_OUTPUT echo "count=$(wc -l < /tmp/changed-files | tr -d '[:space:]')" >> $GITHUB_OUTPUT
- name: Auto-fix lint + format - name: Auto-fix lint + format
if: steps.changed.outputs.files != '' if: steps.changed.outputs.count != '0'
run: | run: |
pnpm exec oxlint --fix ${{ steps.changed.outputs.files }} || true xargs -r -a /tmp/changed-files pnpm exec oxlint --fix || true
pnpm exec oxfmt --write ${{ steps.changed.outputs.files }} xargs -r -a /tmp/changed-files pnpm exec oxfmt --write
- name: Commit fixes - name: Commit fixes
uses: stefanzweifel/git-auto-commit-action@v5 uses: stefanzweifel/git-auto-commit-action@v5
@@ -23,6 +23,7 @@ export const CustomMantineProvider = ({
theme={createTheme({ theme={createTheme({
primaryColor: "red", primaryColor: "red",
autoContrast: true, autoContrast: true,
defaultRadius: "sm",
})} })}
> >
{children} {children}
@@ -9,7 +9,7 @@ import { z } from "zod/v4";
import { signIn } from "@homarr/auth/client"; import { signIn } from "@homarr/auth/client";
import { revalidatePathActionAsync } from "@homarr/common/client"; import { revalidatePathActionAsync } from "@homarr/common/client";
import type { useForm } from "@homarr/form"; import type { UseFormReturnType } from "@homarr/form";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useScopedI18n } from "@homarr/translation/client"; import { useScopedI18n } from "@homarr/translation/client";
@@ -162,7 +162,7 @@ export const LoginForm = ({ providers, oidcClientName, isOidcAutoLoginEnabled, c
interface SubmitButtonProps { interface SubmitButtonProps {
isPending: boolean; isPending: boolean;
form: ReturnType<typeof useForm<FormType, (values: FormType) => FormType>>; form: UseFormReturnType<FormType>;
provider: "credentials" | "ldap"; provider: "credentials" | "ldap";
} }
@@ -198,7 +198,7 @@ const PasswordForgottenCollapse = ({ username }: PasswordForgottenCollapseProps)
{tForgotPassword("label")} {tForgotPassword("label")}
</Anchor> </Anchor>
<Collapse in={visible}> <Collapse expanded={visible}>
<Card> <Card>
<Stack gap="xs"> <Stack gap="xs">
<Text size="sm">{tForgotPassword("description")}</Text> <Text size="sm">{tForgotPassword("description")}</Text>
@@ -24,6 +24,7 @@ export const BoardMantineProvider = ({
}, },
primaryColor: "primaryColor", primaryColor: "primaryColor",
autoContrast: true, autoContrast: true,
defaultRadius: "sm",
fontSizes: { fontSizes: {
"2xl": rem(24), "2xl": rem(24),
"3xl": rem(28), "3xl": rem(28),
@@ -83,7 +83,7 @@ export const ColorSettingsContent = ({ board }: Props) => {
<Anchor onClick={toggle}>{showPreview ? t("common.preview.hide") : t("common.preview.show")}</Anchor> <Anchor onClick={toggle}>{showPreview ? t("common.preview.hide") : t("common.preview.show")}</Anchor>
</Grid.Col> </Grid.Col>
<Grid.Col span={12}> <Grid.Col span={12}>
<Collapse in={showPreview}> <Collapse expanded={showPreview}>
<Stack> <Stack>
<ColorsPreview previewColor={form.values.primaryColor} /> <ColorsPreview previewColor={form.values.primaryColor} />
<ColorsPreview previewColor={form.values.secondaryColor} /> <ColorsPreview previewColor={form.values.secondaryColor} />
+1 -1
View File
@@ -142,7 +142,7 @@ export default async function Layout(props: {
</head> </head>
<body className={["font-sans", fontSans.variable].join(" ")}> <body className={["font-sans", fontSans.variable].join(" ")}>
<StackedProvider> <StackedProvider>
<Notifications /> <Notifications pauseResetOnHover="notification" />
<ServiceWorkerRegistration /> <ServiceWorkerRegistration />
{props.children} {props.children}
</StackedProvider> </StackedProvider>
@@ -79,7 +79,7 @@ export const SecretCard = ({ secret, children, onCancel }: SecretCardProps) => {
</Button> </Button>
</Group> </Group>
</Group> </Group>
<Collapse in={editMode}>{children}</Collapse> <Collapse expanded={editMode}>{children}</Collapse>
</Stack> </Stack>
</Card> </Card>
); );
@@ -12,7 +12,7 @@ interface FormType {
interface SecretKindsSegmentedControlProps<TFormType extends FormType> { interface SecretKindsSegmentedControlProps<TFormType extends FormType> {
defaultKinds?: IntegrationSecretKind[]; defaultKinds?: IntegrationSecretKind[];
secretKinds: IntegrationSecretKind[][]; secretKinds: IntegrationSecretKind[][];
form: UseFormReturnType<TFormType, (values: TFormType) => TFormType>; form: UseFormReturnType<TFormType, TFormType>;
} }
export const SecretKindsSegmentedControl = <TFormType extends FormType>({ export const SecretKindsSegmentedControl = <TFormType extends FormType>({
@@ -226,7 +226,7 @@ const AppForm = ({ form, canCreateApps }: { form: UseFormReturnType<FormType>; c
description={t("integration.field.createApp.description")} description={t("integration.field.createApp.description")}
/> />
<Collapse in={form.values.hasApp}> <Collapse expanded={form.values.hasApp}>
<Fieldset legend={t("integration.field.app.sectionTitle")}> <Fieldset legend={t("integration.field.app.sectionTitle")}>
<Stack gap="sm"> <Stack gap="sm">
{canCreateApps && ( {canCreateApps && (
@@ -15,7 +15,7 @@ export const SwitchSetting = <TFormValue extends Record<string, boolean>>({
disabled, disabled,
}: { }: {
form: Modify< form: Modify<
UseFormReturnType<TFormValue, () => TFormValue>, UseFormReturnType<TFormValue, TFormValue>,
{ {
setFieldValue: (key: keyof TFormValue, value: (previous: boolean) => boolean) => void; setFieldValue: (key: keyof TFormValue, value: (previous: boolean) => boolean) => void;
} }
@@ -2,12 +2,14 @@
import { Select } from "@mantine/core"; import { Select } from "@mantine/core";
import type { LogLevel } from "@homarr/core/infrastructure/logs/constants";
import { logLevelConfiguration, logLevels } from "@homarr/core/infrastructure/logs/constants"; import { logLevelConfiguration, logLevels } from "@homarr/core/infrastructure/logs/constants";
import type { LogLevel } from "@homarr/core/infrastructure/logs/constants";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { useLogContext } from "./log-context"; import { useLogContext } from "./log-context";
const levelByValue = Object.fromEntries(logLevels.map((logLevel) => [logLevel, logLevel])) as Record<string, LogLevel>;
export const LogLevelSelection = () => { export const LogLevelSelection = () => {
const { level, setLevel } = useLogContext(); const { level, setLevel } = useLogContext();
const t = useI18n(); const t = useI18n();
@@ -19,7 +21,12 @@ export const LogLevelSelection = () => {
label: `${logLevelConfiguration[level].prefix} ${t(`log.level.option.${level}`)}`, label: `${logLevelConfiguration[level].prefix} ${t(`log.level.option.${level}`)}`,
}))} }))}
value={level} value={level}
onChange={(value) => setLevel(value as LogLevel)} onChange={(value) => {
const nextLevel = value && levelByValue[value];
if (nextLevel) {
setLevel(nextLevel);
}
}}
checkIconPosition="right" checkIconPosition="right"
/> />
); );
@@ -62,7 +62,7 @@ export const FirstDayOfWeek = ({ user }: FirstDayOfWeekProps) => {
return ( return (
<form onSubmit={form.onSubmit(handleSubmit)}> <form onSubmit={form.onSubmit(handleSubmit)}>
<Stack gap="md"> <Stack gap="md">
<Radio.Group {...inputProps} value={value} onChange={(value) => onChange(parseInt(value))}> <Radio.Group {...inputProps} value={value} onChange={(nextValue: string) => onChange(parseInt(nextValue, 10))}>
<Group mt="xs"> <Group mt="xs">
<Radio value="1" label={weekDays[1]} /> <Radio value="1" label={weekDays[1]} />
<Radio value="6" label={weekDays[6]} /> <Radio value="6" label={weekDays[6]} />
@@ -62,7 +62,7 @@ export const BoardCategorySection = ({ section }: Props) => {
</UnstyledButton> </UnstyledButton>
<CategoryMenu category={section} /> <CategoryMenu category={section} />
</Group> </Group>
<Collapse in={opened} p="sm" pt={0}> <Collapse expanded={opened} p="sm" pt={0}>
<GridStack section={section} /> <GridStack section={section} />
</Collapse> </Collapse>
</Stack> </Stack>
@@ -1,6 +1,6 @@
import type { RefObject } from "react"; import type { Ref, RefObject } from "react";
import { createRef, useCallback, useEffect, useRef } from "react"; import { createRef, useCallback, useEffect, useRef } from "react";
import { useElementSize } from "@mantine/hooks"; import { useElementSize, useMergedRef } from "@mantine/hooks";
import { useCurrentLayout, useRequiredBoard } from "@homarr/boards/context"; import { useCurrentLayout, useRequiredBoard } from "@homarr/boards/context";
import { useEditMode } from "@homarr/boards/edit-mode"; import { useEditMode } from "@homarr/boards/edit-mode";
@@ -13,7 +13,7 @@ import { useSectionActions } from "../section-actions";
import { initializeGridstack } from "./init-gridstack"; import { initializeGridstack } from "./init-gridstack";
export interface UseGridstackRefs { export interface UseGridstackRefs {
wrapper: RefObject<HTMLDivElement | null>; wrapper: Ref<HTMLDivElement | null>;
items: RefObject<Record<string, RefObject<GridItemHTMLElement | null>>>; items: RefObject<Record<string, RefObject<GridItemHTMLElement | null>>>;
gridstack: RefObject<GridStack | null>; gridstack: RefObject<GridStack | null>;
} }
@@ -59,8 +59,9 @@ export const useGridstack = (section: Omit<Section, "items">, itemIds: string[])
const { moveAndResizeItem, moveItemToSection } = useItemActions(); const { moveAndResizeItem, moveItemToSection } = useItemActions();
const { moveAndResizeInnerSection, moveInnerSectionToSection } = useSectionActions(); const { moveAndResizeInnerSection, moveInnerSectionToSection } = useSectionActions();
// define reference for wrapper - is used to calculate the width of the wrapper const wrapperElementRef = useRef<HTMLDivElement | null>(null);
const { ref: wrapperRef, width, height } = useElementSize<HTMLDivElement>(); const { ref: measureWrapperRef, width, height } = useElementSize<HTMLDivElement>();
const wrapperRef = useMergedRef(measureWrapperRef, wrapperElementRef);
// references to the diffrent items contained in the gridstack // references to the diffrent items contained in the gridstack
const itemRefs = useRef<Record<string, RefObject<GridItemHTMLElement | null>>>({}); const itemRefs = useRef<Record<string, RefObject<GridItemHTMLElement | null>>>({});
// reference of the gridstack object for modifications after initialization // reference of the gridstack object for modifications after initialization
@@ -193,7 +194,7 @@ export const useGridstack = (section: Omit<Section, "items">, itemIds: string[])
itemIds, itemIds,
refs: { refs: {
items: itemRefs, items: itemRefs,
wrapper: wrapperRef, wrapper: wrapperElementRef,
gridstack: gridRef, gridstack: gridRef,
}, },
sectionColumnCount: columnCount, sectionColumnCount: columnCount,
@@ -276,7 +277,7 @@ export const useGridstack = (section: Omit<Section, "items">, itemIds: string[])
useCssVariableConfiguration({ useCssVariableConfiguration({
columnCount, columnCount,
gridRef, gridRef,
wrapperRef, wrapperRef: wrapperElementRef,
width, width,
height, height,
isDynamic: section.kind === "dynamic", isDynamic: section.kind === "dynamic",
@@ -293,7 +294,7 @@ export const useGridstack = (section: Omit<Section, "items">, itemIds: string[])
interface UseCssVariableConfiguration { interface UseCssVariableConfiguration {
gridRef: UseGridstackRefs["gridstack"]; gridRef: UseGridstackRefs["gridstack"];
wrapperRef: UseGridstackRefs["wrapper"]; wrapperRef: RefObject<HTMLDivElement | null>;
width: number; width: number;
height: number; height: number;
columnCount: number; columnCount: number;
+1 -1
View File
@@ -45,7 +45,7 @@ describe("LLDAP authorization", () => {
// Act // Act
await page.goto(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}/auth/login`); await page.goto(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}/auth/login`);
await page.getByLabel("Username").fill(defaultCredentials.username); await page.getByLabel("Username").fill(defaultCredentials.username);
await page.getByLabel("Password").fill(defaultCredentials.password); await page.locator("#password").fill(defaultCredentials.password);
await page.locator("css=button[type='submit']").click(); await page.locator("css=button[type='submit']").click();
// Assert // Assert
-1
View File
@@ -26,7 +26,6 @@
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@mantine/form": "catalog:", "@mantine/form": "catalog:",
"mantine-form-zod-resolver": "catalog:",
"zod": "catalog:" "zod": "catalog:"
}, },
"devDependencies": { "devDependencies": {
+2 -3
View File
@@ -1,5 +1,4 @@
import { useForm } from "@mantine/form"; import { schemaResolver, useForm } from "@mantine/form";
import { zod4Resolver } from "mantine-form-zod-resolver";
import type { ZodDiscriminatedUnion, ZodIntersection, ZodObject, ZodPipe } from "zod/v4"; import type { ZodDiscriminatedUnion, ZodIntersection, ZodObject, ZodPipe } from "zod/v4";
import { z } from "zod/v4"; import { z } from "zod/v4";
@@ -34,6 +33,6 @@ export const useZodForm = <
...options, ...options,
validateInputOnBlur: true, validateInputOnBlur: true,
validateInputOnChange: true, validateInputOnChange: true,
validate: zod4Resolver(schema), validate: schemaResolver(schema, { sync: true }),
}); });
}; };
@@ -114,7 +114,7 @@ export const AppForm = ({
mt="md" mt="md"
/> />
<Collapse in={opened}> <Collapse expanded={opened}>
<TextInput {...form.getInputProps("pingUrl")} /> <TextInput {...form.getInputProps("pingUrl")} />
</Collapse> </Collapse>
+1
View File
@@ -9,4 +9,5 @@ export const theme = createTheme({
secondaryColor, secondaryColor,
}, },
primaryColor: "primaryColor", primaryColor: "primaryColor",
defaultRadius: "sm",
}); });
+1 -1
View File
@@ -49,6 +49,7 @@
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@mantine/charts": "catalog:", "@mantine/charts": "catalog:",
"@mantine/core": "catalog:", "@mantine/core": "catalog:",
"@mantine/form": "catalog:",
"@mantine/hooks": "catalog:", "@mantine/hooks": "catalog:",
"@tabler/icons-react": "catalog:", "@tabler/icons-react": "catalog:",
"@tiptap/extension-color": "catalog:", "@tiptap/extension-color": "catalog:",
@@ -69,7 +70,6 @@
"@tiptap/starter-kit": "catalog:", "@tiptap/starter-kit": "catalog:",
"clsx": "catalog:", "clsx": "catalog:",
"dayjs": "catalog:", "dayjs": "catalog:",
"mantine-form-zod-resolver": "catalog:",
"mantine-react-table": "catalog:", "mantine-react-table": "catalog:",
"next": "catalog:", "next": "catalog:",
"react": "catalog:", "react": "catalog:",
@@ -25,9 +25,22 @@ export const WidgetDynamicSelectInput = ({ property, kind, options }: CommonWidg
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [debouncedSearch] = useDebouncedValue(search, 300); const [debouncedSearch] = useDebouncedValue(search, 300);
const { isPending, options: selectOptions } = options.useOptions(debouncedSearch, form.values.integrationIds); const { isPending, options: selectOptions } = options.useOptions(debouncedSearch, form.values.integrationIds);
const value = inputProps.value as DynamicSelectOption | null; const currentOption = inputProps.value as DynamicSelectOption | null;
const onChange = inputProps.onChange as (value: DynamicSelectOption | null) => void; const onChange = inputProps.onChange as (value: DynamicSelectOption | null) => void;
const getLabelForValue = (selectedValue: string) => {
const matched = selectOptions.find((option) =>
typeof option === "string" ? option === selectedValue : option.value === selectedValue,
);
if (!matched) {
return selectedValue;
}
if (typeof matched === "string") {
return matched;
}
return translateIfNecessary(t, matched.label) ?? selectedValue;
};
return ( return (
<Select <Select
label={tWidget("label")} label={tWidget("label")}
@@ -41,14 +54,13 @@ export const WidgetDynamicSelectInput = ({ property, kind, options }: CommonWidg
}, },
) )
.concat( .concat(
typeof value === "object" && currentOption !== null &&
value !== null &&
!selectOptions.some((option) => !selectOptions.some((option) =>
typeof option === "string" ? option === value.value : option.value === value.value, typeof option === "string" ? option === currentOption.value : option.value === currentOption.value,
) )
? { ? {
value: value.value, value: currentOption.value,
label: value.label, label: currentOption.label,
} }
: [], : [],
)} )}
@@ -74,19 +86,17 @@ export const WidgetDynamicSelectInput = ({ property, kind, options }: CommonWidg
description={options.withDescription ? tWidget("description") : undefined} description={options.withDescription ? tWidget("description") : undefined}
searchable searchable
{...inputProps} {...inputProps}
value={value?.value ?? null} value={currentOption === null ? null : currentOption.value}
onChange={(value) => onChange={(selectedValue: string | null) => {
onChange( if (selectedValue === null) {
value onChange(null);
? { return;
value, }
label: onChange({
selectOptions.filter((option) => typeof option === "object").find((option) => option.value === value) value: selectedValue,
?.label ?? value, label: getLabelForValue(selectedValue),
} });
: null, }}
)
}
/> />
); );
}; };
@@ -53,7 +53,7 @@ export const WidgetSelectInput = ({ property, kind, options }: CommonWidgetInput
)} )}
leftSection={CurrentIcon && <CurrentIcon size={16} stroke={1.5} />} leftSection={CurrentIcon && <CurrentIcon size={16} stroke={1.5} />}
renderOption={({ option, checked }) => { renderOption={({ option, checked }) => {
const Icon = getIconFor(options.options, option.value); const Icon = getIconFor(options.options, String(option.value));
return ( return (
<Group flex="1" gap="xs"> <Group flex="1" gap="xs">
+1 -1
View File
@@ -805,7 +805,7 @@ const ClientsControl = ({ clients, filters, setFilters, availableStatuses }: Cli
<Chip.Group <Chip.Group
multiple multiple
value={filters.statuses} value={filters.statuses}
onChange={(statuses) => setFilters({ ...filters, statuses: statuses as typeof filters.statuses })} onChange={(statuses) => setFilters({ ...filters, statuses: statuses })}
> >
{availableStatuses.map((status) => ( {availableStatuses.map((status) => (
<Chip key={status} value={status}> <Chip key={status} value={status}>
+2 -2
View File
@@ -153,7 +153,7 @@ export default function FirewallWidget({ integrationIds, width, itemId }: Widget
style={{ justifyContent: "flex-start" }} style={{ justifyContent: "flex-start" }}
> >
<IconArrowBarUp size={isTiny ? "8" : "12"} color="lightgreen" /> <IconArrowBarUp size={isTiny ? "8" : "12"} color="lightgreen" />
<Text size={isTiny ? "8px" : "xs"} color="lightgreen" style={{ textAlign: "left" }}> <Text size={isTiny ? "8px" : "xs"} c="lightgreen" style={{ textAlign: "left" }}>
{formatBitsPerSec(transmit, 2)} {formatBitsPerSec(transmit, 2)}
</Text> </Text>
</Flex> </Flex>
@@ -164,7 +164,7 @@ export default function FirewallWidget({ integrationIds, width, itemId }: Widget
style={{ justifyContent: "flex-start" }} style={{ justifyContent: "flex-start" }}
> >
<IconArrowBarDown size={isTiny ? "8" : "12"} color="yellow" /> <IconArrowBarDown size={isTiny ? "8" : "12"} color="yellow" />
<Text size={isTiny ? "8px" : "xs"} color="yellow" style={{ textAlign: "left" }}> <Text size={isTiny ? "8px" : "xs"} c="yellow" style={{ textAlign: "left" }}>
{formatBitsPerSec(receive, 2)} {formatBitsPerSec(receive, 2)}
</Text> </Text>
</Flex> </Flex>
@@ -103,7 +103,7 @@ export default function MediaServerWidget({
<Text className="mediaRequests-stats-stats-title" fw="bold" ta="center" size={isTiny ? "xs" : "sm"}> <Text className="mediaRequests-stats-stats-title" fw="bold" ta="center" size={isTiny ? "xs" : "sm"}>
{t("titles.stats.main")} {t("titles.stats.main")}
</Text> </Text>
<Grid className="mediaRequests-stats-stats-grid" gutter={4} w="100%"> <Grid className="mediaRequests-stats-stats-grid" gap={4} w="100%">
{data.map((stat) => ( {data.map((stat) => (
<Grid.Col <Grid.Col
className={combineClasses("mediaRequests-stats-stat-wrapper", `mediaRequests-stats-stat-${stat.name}`)} className={combineClasses("mediaRequests-stats-stat-wrapper", `mediaRequests-stats-stat-${stat.name}`)}
@@ -17,6 +17,8 @@ import { WorkersPanel } from "./panels/workers.panel";
type View = (typeof views)[number]; type View = (typeof views)[number];
const viewBySegmentValue = Object.fromEntries(views.map((view) => [view, view])) as Record<string, View>;
const viewIcons = { const viewIcons = {
workers: IconCpu2, workers: IconCpu2,
queue: IconClipboardList, queue: IconClipboardList,
@@ -83,7 +85,12 @@ export default function MediaTranscodingWidget({
}; };
})} })}
value={view} value={view}
onChange={(value) => setView(value as View)} onChange={(value) => {
const nextView = viewBySegmentValue[value];
if (nextView) {
setView(nextView);
}
}}
size="xs" size="xs"
/> />
@@ -2,7 +2,7 @@
import { useState } from "react"; import { useState } from "react";
import { Button, Group, Stack } from "@mantine/core"; import { Button, Group, Stack } from "@mantine/core";
import { zod4Resolver } from "mantine-form-zod-resolver"; import { schemaResolver } from "@mantine/form";
import { z } from "zod/v4"; import { z } from "zod/v4";
import { objectEntries } from "@homarr/common"; import { objectEntries } from "@homarr/common";
@@ -50,7 +50,7 @@ export const WidgetEditModal = createModal<ModalProps<WidgetKind>>(({ actions, i
const form = useForm({ const form = useForm({
mode: "controlled", mode: "controlled",
initialValues: innerProps.value, initialValues: innerProps.value,
validate: zod4Resolver( validate: schemaResolver(
z.object({ z.object({
options: z.object( options: z.object(
objectEntries(options).reduce( objectEntries(options).reduce(
@@ -70,6 +70,7 @@ export const WidgetEditModal = createModal<ModalProps<WidgetKind>>(({ actions, i
borderColor: z.string(), borderColor: z.string(),
}), }),
}), }),
{ sync: true },
), ),
validateInputOnBlur: true, validateInputOnBlur: true,
validateInputOnChange: true, validateInputOnChange: true,
+608 -383
View File
File diff suppressed because it is too large. Load diff
+12 -12
View File
@@ -20,17 +20,18 @@ catalog:
"@immich/sdk": ^2.7.5 "@immich/sdk": ^2.7.5
"@jellyfin/sdk": ^0.13.0 "@jellyfin/sdk": ^0.13.0
"@kubernetes/client-node": ^1.4.0 "@kubernetes/client-node": ^1.4.0
"@mantine/charts": ^8.3.18 "@mantine/charts": ^9.2.0
"@mantine/colors-generator": ^8.3.18 "@mantine/colors-generator": ^9.2.0
"@mantine/core": ^8.3.18 "@mantine/core": ^9.2.0
"@mantine/dates": ^8.3.18 "@mantine/dates": ^9.2.0
"@mantine/dropzone": ^8.3.18 "@mantine/dropzone": ^9.2.0
"@mantine/form": ^8.3.18 "@mantine/form": ^9.2.0
"@mantine/hooks": ^8.3.18 "@mantine/hooks": ^9.2.0
"@mantine/modals": ^8.3.18 "@mantine/modals": ^9.2.0
"@mantine/notifications": ^8.3.18 "@mantine/notifications": ^9.2.0
"@mantine/spotlight": ^8.3.18 "@mantine/spotlight": ^9.2.0
"@mantine/tiptap": ^8.3.18 "@mantine/tiptap": ^9.2.0
"@next/eslint-plugin-next": 16.2.6
"@octokit/auth-app": ^8.2.0 "@octokit/auth-app": ^8.2.0
"@paralleldrive/cuid2": ^3.3.0 "@paralleldrive/cuid2": ^3.3.0
"@playwright/test": ^1.60.0 "@playwright/test": ^1.60.0
@@ -130,7 +131,6 @@ catalog:
json5: ^2.2.3 json5: ^2.2.3
ldapts: 8.1.8 ldapts: 8.1.8
lodash.clonedeep: ^4.5.0 lodash.clonedeep: ^4.5.0
mantine-form-zod-resolver: ^1.3.0
mantine-react-table: 2.0.0-beta.9 mantine-react-table: 2.0.0-beta.9
maria2: ^0.4.1 maria2: ^0.4.1
mysql2: 3.22.3 mysql2: 3.22.3