mirror of
https://github.com/ajnart/homarr.git
synced 2026-02-26 16:30:57 +01:00
chore: update prettier configuration for print width (#519)
* feat: update prettier configuration for print width * chore: apply code formatting to entire repository * fix: remove build files * fix: format issue --------- Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -51,4 +51,7 @@ yarn-error.log*
|
||||
db.sqlite
|
||||
|
||||
# logs
|
||||
*.log
|
||||
*.log
|
||||
|
||||
apps/tasks/tasks.cjs
|
||||
apps/websocket/wssServer.cjs
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -6,9 +6,10 @@
|
||||
],
|
||||
"typescript.tsdk": "node_modules\\typescript\\lib",
|
||||
"js/ts.implicitProjectConfig.experimentalDecorators": true,
|
||||
"prettier.configPath": "./tooling/prettier/index.mjs",
|
||||
"cSpell.words": [
|
||||
"superjson",
|
||||
"homarr",
|
||||
"trpc"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -10,19 +10,9 @@ const config = {
|
||||
eslint: { ignoreDuringBuilds: true },
|
||||
typescript: { ignoreBuildErrors: true },
|
||||
experimental: {
|
||||
optimizePackageImports: [
|
||||
"@mantine/core",
|
||||
"@mantine/hooks",
|
||||
"@tabler/icons-react",
|
||||
],
|
||||
optimizePackageImports: ["@mantine/core", "@mantine/hooks", "@tabler/icons-react"],
|
||||
},
|
||||
transpilePackages: [
|
||||
"@homarr/ui",
|
||||
"@homarr/notifications",
|
||||
"@homarr/modals",
|
||||
"@homarr/spotlight",
|
||||
"@homarr/widgets",
|
||||
],
|
||||
transpilePackages: ["@homarr/ui", "@homarr/notifications", "@homarr/modals", "@homarr/spotlight", "@homarr/widgets"],
|
||||
images: {
|
||||
domains: ["cdn.jsdelivr.net"],
|
||||
},
|
||||
|
||||
@@ -3,10 +3,7 @@ import type { PropsWithChildren } from "react";
|
||||
import { defaultLocale } from "@homarr/translation";
|
||||
import { I18nProviderClient } from "@homarr/translation/client";
|
||||
|
||||
export const NextInternationalProvider = ({
|
||||
children,
|
||||
locale,
|
||||
}: PropsWithChildren<{ locale: string }>) => {
|
||||
export const NextInternationalProvider = ({ children, locale }: PropsWithChildren<{ locale: string }>) => {
|
||||
return (
|
||||
<I18nProviderClient locale={locale} fallback={defaultLocale}>
|
||||
{children}
|
||||
|
||||
@@ -9,9 +9,6 @@ interface AuthProviderProps {
|
||||
session: Session | null;
|
||||
}
|
||||
|
||||
export const AuthProvider = ({
|
||||
children,
|
||||
session,
|
||||
}: PropsWithChildren<AuthProviderProps>) => {
|
||||
export const AuthProvider = ({ children, session }: PropsWithChildren<AuthProviderProps>) => {
|
||||
return <SessionProvider session={session}>{children}</SessionProvider>;
|
||||
};
|
||||
|
||||
@@ -5,12 +5,7 @@ import { useState } from "react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import { ReactQueryStreamedHydration } from "@tanstack/react-query-next-experimental";
|
||||
import {
|
||||
createWSClient,
|
||||
loggerLink,
|
||||
unstable_httpBatchStreamLink,
|
||||
wsLink,
|
||||
} from "@trpc/client";
|
||||
import { createWSClient, loggerLink, unstable_httpBatchStreamLink, wsLink } from "@trpc/client";
|
||||
import superjson from "superjson";
|
||||
|
||||
import type { AppRouter } from "@homarr/api";
|
||||
@@ -37,8 +32,7 @@ export function TRPCReactProvider(props: PropsWithChildren) {
|
||||
links: [
|
||||
loggerLink({
|
||||
enabled: (opts) =>
|
||||
process.env.NODE_ENV === "development" ||
|
||||
(opts.direction === "down" && opts.result instanceof Error),
|
||||
process.env.NODE_ENV === "development" || (opts.direction === "down" && opts.result instanceof Error),
|
||||
}),
|
||||
(args) => {
|
||||
return ({ op, next }) => {
|
||||
@@ -69,9 +63,7 @@ export function TRPCReactProvider(props: PropsWithChildren) {
|
||||
return (
|
||||
<clientApi.Provider client={trpcClient} queryClient={queryClient}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ReactQueryStreamedHydration transformer={superjson}>
|
||||
{props.children}
|
||||
</ReactQueryStreamedHydration>
|
||||
<ReactQueryStreamedHydration transformer={superjson}>{props.children}</ReactQueryStreamedHydration>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
</clientApi.Provider>
|
||||
|
||||
@@ -5,10 +5,7 @@ import { Button, PasswordInput, Stack, TextInput } from "@mantine/core";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import {
|
||||
showErrorNotification,
|
||||
showSuccessNotification,
|
||||
} from "@homarr/notifications";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import type { z } from "@homarr/validation";
|
||||
import { validation } from "@homarr/validation";
|
||||
@@ -32,9 +29,7 @@ export const RegistrationForm = ({ invite }: RegistrationFormProps) => {
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (
|
||||
values: z.infer<typeof validation.user.registration>,
|
||||
) => {
|
||||
const handleSubmit = (values: z.infer<typeof validation.user.registration>) => {
|
||||
mutate(
|
||||
{
|
||||
...values,
|
||||
@@ -63,11 +58,7 @@ export const RegistrationForm = ({ invite }: RegistrationFormProps) => {
|
||||
<Stack gap="xl">
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack gap="lg">
|
||||
<TextInput
|
||||
label={t("field.username.label")}
|
||||
autoComplete="off"
|
||||
{...form.getInputProps("username")}
|
||||
/>
|
||||
<TextInput label={t("field.username.label")} autoComplete="off" {...form.getInputProps("username")} />
|
||||
<PasswordInput
|
||||
label={t("field.password.label")}
|
||||
autoComplete="new-password"
|
||||
|
||||
@@ -18,18 +18,12 @@ interface InviteUsagePageProps {
|
||||
};
|
||||
}
|
||||
|
||||
export default async function InviteUsagePage({
|
||||
params,
|
||||
searchParams,
|
||||
}: InviteUsagePageProps) {
|
||||
export default async function InviteUsagePage({ params, searchParams }: InviteUsagePageProps) {
|
||||
const session = await auth();
|
||||
if (session) notFound();
|
||||
|
||||
const invite = await db.query.invites.findFirst({
|
||||
where: and(
|
||||
eq(invites.id, params.id),
|
||||
eq(invites.token, searchParams.token),
|
||||
),
|
||||
where: and(eq(invites.id, params.id), eq(invites.token, searchParams.token)),
|
||||
columns: {
|
||||
id: true,
|
||||
token: true,
|
||||
|
||||
@@ -2,22 +2,12 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
PasswordInput,
|
||||
rem,
|
||||
Stack,
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
import { Alert, Button, PasswordInput, rem, Stack, TextInput } from "@mantine/core";
|
||||
import { IconAlertTriangle } from "@tabler/icons-react";
|
||||
|
||||
import { signIn } from "@homarr/auth/client";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import {
|
||||
showErrorNotification,
|
||||
showSuccessNotification,
|
||||
} from "@homarr/notifications";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import type { z } from "@homarr/validation";
|
||||
import { validation } from "@homarr/validation";
|
||||
@@ -34,9 +24,7 @@ export const LoginForm = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmitAsync = async (
|
||||
values: z.infer<typeof validation.user.signIn>,
|
||||
) => {
|
||||
const handleSubmitAsync = async (values: z.infer<typeof validation.user.signIn>) => {
|
||||
setIsLoading(true);
|
||||
setError(undefined);
|
||||
await signIn("credentials", {
|
||||
@@ -67,18 +55,10 @@ export const LoginForm = () => {
|
||||
|
||||
return (
|
||||
<Stack gap="xl">
|
||||
<form
|
||||
onSubmit={form.onSubmit((values) => void handleSubmitAsync(values))}
|
||||
>
|
||||
<form onSubmit={form.onSubmit((values) => void handleSubmitAsync(values))}>
|
||||
<Stack gap="lg">
|
||||
<TextInput
|
||||
label={t("field.username.label")}
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
<PasswordInput
|
||||
label={t("field.password.label")}
|
||||
{...form.getInputProps("password")}
|
||||
/>
|
||||
<TextInput label={t("field.username.label")} {...form.getInputProps("name")} />
|
||||
<PasswordInput label={t("field.password.label")} {...form.getInputProps("password")} />
|
||||
<Button type="submit" fullWidth loading={isLoading}>
|
||||
{t("action.login.label")}
|
||||
</Button>
|
||||
|
||||
@@ -18,9 +18,7 @@ export const updateBoardName = (name: string | null) => {
|
||||
boardName = name;
|
||||
};
|
||||
|
||||
type UpdateCallback = (
|
||||
prev: RouterOutputs["board"]["getHomeBoard"],
|
||||
) => RouterOutputs["board"]["getHomeBoard"];
|
||||
type UpdateCallback = (prev: RouterOutputs["board"]["getHomeBoard"]) => RouterOutputs["board"]["getHomeBoard"];
|
||||
|
||||
export const useUpdateBoard = () => {
|
||||
const utils = clientApi.useUtils();
|
||||
@@ -46,9 +44,7 @@ export const ClientBoard = () => {
|
||||
const board = useRequiredBoard();
|
||||
const isReady = useIsBoardReady();
|
||||
|
||||
const sortedSections = board.sections.sort(
|
||||
(sectionA, sectionB) => sectionA.position - sectionB.position,
|
||||
);
|
||||
const sortedSections = board.sections.sort((sectionA, sectionB) => sectionA.position - sectionB.position);
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -61,24 +57,12 @@ export const ClientBoard = () => {
|
||||
loaderProps={{ size: "lg" }}
|
||||
h={fullHeightWithoutHeaderAndFooter}
|
||||
/>
|
||||
<Stack
|
||||
ref={ref}
|
||||
h="100%"
|
||||
style={{ visibility: isReady ? "visible" : "hidden" }}
|
||||
>
|
||||
<Stack ref={ref} h="100%" style={{ visibility: isReady ? "visible" : "hidden" }}>
|
||||
{sortedSections.map((section) =>
|
||||
section.kind === "empty" ? (
|
||||
<BoardEmptySection
|
||||
key={section.id}
|
||||
section={section}
|
||||
mainRef={ref}
|
||||
/>
|
||||
<BoardEmptySection key={section.id} section={section} mainRef={ref} />
|
||||
) : (
|
||||
<BoardCategorySection
|
||||
key={section.id}
|
||||
section={section}
|
||||
mainRef={ref}
|
||||
/>
|
||||
<BoardCategorySection key={section.id} section={section} mainRef={ref} />
|
||||
),
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { PropsWithChildren } from "react";
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { createContext, useCallback, useContext, useEffect, useState } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
@@ -52,18 +46,12 @@ export const BoardProvider = ({
|
||||
}, [pathname, utils, initialBoard.name]);
|
||||
|
||||
useEffect(() => {
|
||||
setReadySections((previous) =>
|
||||
previous.filter((id) =>
|
||||
data.sections.some((section) => section.id === id),
|
||||
),
|
||||
);
|
||||
setReadySections((previous) => previous.filter((id) => data.sections.some((section) => section.id === id)));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data.sections.length, setReadySections]);
|
||||
|
||||
const markAsReady = useCallback((id: string) => {
|
||||
setReadySections((previous) =>
|
||||
previous.includes(id) ? previous : [...previous, id],
|
||||
);
|
||||
setReadySections((previous) => (previous.includes(id) ? previous : [...previous, id]));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
||||
@@ -18,9 +18,7 @@ interface Props<TParams extends Params> {
|
||||
getInitialBoardAsync: (params: TParams) => Promise<Board>;
|
||||
}
|
||||
|
||||
export const createBoardContentPage = <
|
||||
TParams extends Record<string, unknown>,
|
||||
>({
|
||||
export const createBoardContentPage = <TParams extends Record<string, unknown>>({
|
||||
getInitialBoardAsync: getInitialBoard,
|
||||
}: Props<TParams>) => {
|
||||
return {
|
||||
@@ -32,21 +30,13 @@ export const createBoardContentPage = <
|
||||
page: () => {
|
||||
return <ClientBoard />;
|
||||
},
|
||||
generateMetadataAsync: async ({
|
||||
params,
|
||||
}: {
|
||||
params: TParams;
|
||||
}): Promise<Metadata> => {
|
||||
generateMetadataAsync: async ({ params }: { params: TParams }): Promise<Metadata> => {
|
||||
try {
|
||||
const board = await getInitialBoard(params);
|
||||
const t = await getI18n();
|
||||
|
||||
return {
|
||||
title:
|
||||
board.metaTitle ??
|
||||
createMetaTitle(
|
||||
t("board.content.metaTitle", { boardName: board.name }),
|
||||
),
|
||||
title: board.metaTitle ?? createMetaTitle(t("board.content.metaTitle", { boardName: board.name })),
|
||||
icons: {
|
||||
icon: board.faviconImageUrl ? board.faviconImageUrl : undefined,
|
||||
},
|
||||
|
||||
@@ -16,10 +16,7 @@ import { useAtom, useAtomValue } from "jotai";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useModalAction } from "@homarr/modals";
|
||||
import {
|
||||
showErrorNotification,
|
||||
showSuccessNotification,
|
||||
} from "@homarr/notifications";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||
@@ -54,8 +51,7 @@ export const BoardContentHeaderActions = () => {
|
||||
};
|
||||
|
||||
const AddMenu = () => {
|
||||
const { openModal: openCategoryEditModal } =
|
||||
useModalAction(CategoryEditModal);
|
||||
const { openModal: openCategoryEditModal } = useModalAction(CategoryEditModal);
|
||||
const { openModal: openItemSelectModal } = useModalAction(ItemSelectModal);
|
||||
const { addCategoryToEnd } = useCategoryActions();
|
||||
const t = useI18n();
|
||||
@@ -95,22 +91,14 @@ const AddMenu = () => {
|
||||
</HeaderButton>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown style={{ transform: "translate(-3px, 0)" }}>
|
||||
<Menu.Item
|
||||
leftSection={<IconBox size={20} />}
|
||||
onClick={handleSelectItem}
|
||||
>
|
||||
<Menu.Item leftSection={<IconBox size={20} />} onClick={handleSelectItem}>
|
||||
{t("item.action.create")}
|
||||
</Menu.Item>
|
||||
<Menu.Item leftSection={<IconPackageImport size={20} />}>
|
||||
{t("item.action.import")}
|
||||
</Menu.Item>
|
||||
<Menu.Item leftSection={<IconPackageImport size={20} />}>{t("item.action.import")}</Menu.Item>
|
||||
|
||||
<Menu.Divider />
|
||||
|
||||
<Menu.Item
|
||||
leftSection={<IconBoxAlignTop size={20} />}
|
||||
onClick={handleAddCategory}
|
||||
>
|
||||
<Menu.Item leftSection={<IconBoxAlignTop size={20} />} onClick={handleAddCategory}>
|
||||
{t("section.category.action.create")}
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
@@ -123,24 +111,23 @@ const EditModeMenu = () => {
|
||||
const board = useRequiredBoard();
|
||||
const utils = clientApi.useUtils();
|
||||
const t = useScopedI18n("board.action.edit");
|
||||
const { mutate: saveBoard, isPending } =
|
||||
clientApi.board.saveBoard.useMutation({
|
||||
onSuccess() {
|
||||
showSuccessNotification({
|
||||
title: t("notification.success.title"),
|
||||
message: t("notification.success.message"),
|
||||
});
|
||||
void utils.board.getBoardByName.invalidate({ name: board.name });
|
||||
void revalidatePathActionAsync(`/boards/${board.name}`);
|
||||
setEditMode(false);
|
||||
},
|
||||
onError() {
|
||||
showErrorNotification({
|
||||
title: t("notification.error.title"),
|
||||
message: t("notification.error.message"),
|
||||
});
|
||||
},
|
||||
});
|
||||
const { mutate: saveBoard, isPending } = clientApi.board.saveBoard.useMutation({
|
||||
onSuccess() {
|
||||
showSuccessNotification({
|
||||
title: t("notification.success.title"),
|
||||
message: t("notification.success.message"),
|
||||
});
|
||||
void utils.board.getBoardByName.invalidate({ name: board.name });
|
||||
void revalidatePathActionAsync(`/boards/${board.name}`);
|
||||
setEditMode(false);
|
||||
},
|
||||
onError() {
|
||||
showErrorNotification({
|
||||
title: t("notification.error.title"),
|
||||
message: t("notification.error.message"),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
if (isEditMode) return saveBoard(board);
|
||||
@@ -149,11 +136,7 @@ const EditModeMenu = () => {
|
||||
|
||||
return (
|
||||
<HeaderButton onClick={toggle} loading={isPending}>
|
||||
{isEditMode ? (
|
||||
<IconPencilOff stroke={1.5} />
|
||||
) : (
|
||||
<IconPencil stroke={1.5} />
|
||||
)}
|
||||
{isEditMode ? <IconPencilOff stroke={1.5} /> : <IconPencil stroke={1.5} />}
|
||||
</HeaderButton>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -22,9 +22,7 @@ export const BoardMantineProvider = ({ children }: PropsWithChildren) => {
|
||||
};
|
||||
|
||||
export const generateColors = (hex: string) => {
|
||||
const lightnessForColors = [
|
||||
-0.25, -0.2, -0.15, -0.1, -0.05, 0, 0.05, 0.1, 0.15, 0.2,
|
||||
] as const;
|
||||
const lightnessForColors = [-0.25, -0.2, -0.15, -0.1, -0.05, 0, 0.05, 0.1, 0.15, 0.2] as const;
|
||||
const rgbaColors = lightnessForColors.map((lightness) => {
|
||||
if (lightness < 0) {
|
||||
return lighten(hex, -lightness);
|
||||
|
||||
@@ -44,11 +44,7 @@ export const AccessSettingsContent = ({ board, initialPermissions }: Props) => {
|
||||
<Tabs.List grow>
|
||||
<TabItem value="user" count={counts.user} icon={IconUser} />
|
||||
<TabItem value="group" count={counts.group} icon={IconUsersGroup} />
|
||||
<TabItem
|
||||
value="inherited"
|
||||
count={initialPermissions.inherited.length}
|
||||
icon={IconUserDown}
|
||||
/>
|
||||
<TabItem value="inherited" count={initialPermissions.inherited.length} icon={IconUserDown} />
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Panel value="user">
|
||||
|
||||
@@ -1,21 +1,8 @@
|
||||
import { useCallback } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import type { SelectProps } from "@mantine/core";
|
||||
import {
|
||||
Button,
|
||||
Flex,
|
||||
Group,
|
||||
Select,
|
||||
TableTd,
|
||||
TableTr,
|
||||
Text,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
IconCheck,
|
||||
IconEye,
|
||||
IconPencil,
|
||||
IconSettings,
|
||||
} from "@tabler/icons-react";
|
||||
import { Button, Flex, Group, Select, TableTd, TableTr, Text } from "@mantine/core";
|
||||
import { IconCheck, IconEye, IconPencil, IconSettings } from "@tabler/icons-react";
|
||||
|
||||
import type { BoardPermission } from "@homarr/definitions";
|
||||
import { boardPermissions } from "@homarr/definitions";
|
||||
@@ -38,12 +25,7 @@ interface BoardAccessSelectRowProps {
|
||||
onCountChange: OnCountChange;
|
||||
}
|
||||
|
||||
export const BoardAccessSelectRow = ({
|
||||
itemContent,
|
||||
permission,
|
||||
index,
|
||||
onCountChange,
|
||||
}: BoardAccessSelectRowProps) => {
|
||||
export const BoardAccessSelectRow = ({ itemContent, permission, index, onCountChange }: BoardAccessSelectRowProps) => {
|
||||
const tRoot = useI18n();
|
||||
const tPermissions = useScopedI18n("board.setting.section.access.permission");
|
||||
const form = useFormContext();
|
||||
@@ -61,11 +43,7 @@ export const BoardAccessSelectRow = ({
|
||||
<TableTr>
|
||||
<TableTd w={{ sm: 128, lg: 256 }}>{itemContent}</TableTd>
|
||||
<TableTd>
|
||||
<Flex
|
||||
direction={{ base: "column", xs: "row" }}
|
||||
align={{ base: "end", xs: "center" }}
|
||||
wrap="nowrap"
|
||||
>
|
||||
<Flex direction={{ base: "column", xs: "row" }} align={{ base: "end", xs: "center" }} wrap="nowrap">
|
||||
<Select
|
||||
allowDeselect={false}
|
||||
flex="1"
|
||||
@@ -93,10 +71,7 @@ interface BoardAccessDisplayRowProps {
|
||||
permission: BoardPermission | "board-full";
|
||||
}
|
||||
|
||||
export const BoardAccessDisplayRow = ({
|
||||
itemContent,
|
||||
permission,
|
||||
}: BoardAccessDisplayRowProps) => {
|
||||
export const BoardAccessDisplayRow = ({ itemContent, permission }: BoardAccessDisplayRowProps) => {
|
||||
const tPermissions = useScopedI18n("board.setting.section.access.permission");
|
||||
const Icon = icons[permission];
|
||||
|
||||
@@ -106,10 +81,7 @@ export const BoardAccessDisplayRow = ({
|
||||
<TableTd>
|
||||
<Group gap={0}>
|
||||
<Flex w={34} h={34} align="center" justify="center">
|
||||
<Icon
|
||||
size="1rem"
|
||||
color="var(--input-section-color, var(--mantine-color-dimmed))"
|
||||
/>
|
||||
<Icon size="1rem" color="var(--input-section-color, var(--mantine-color-dimmed))" />
|
||||
</Flex>
|
||||
<Text size="sm">{tPermissions(`item.${permission}.label`)}</Text>
|
||||
</Group>
|
||||
@@ -131,9 +103,7 @@ const RenderOption: SelectProps["renderOption"] = ({ option, checked }) => {
|
||||
<Group flex="1" gap="xs" wrap="nowrap">
|
||||
<Icon {...iconProps} />
|
||||
{option.label}
|
||||
{checked && (
|
||||
<IconCheck style={{ marginInlineStart: "auto" }} {...iconProps} />
|
||||
)}
|
||||
{checked && <IconCheck style={{ marginInlineStart: "auto" }} {...iconProps} />}
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,7 +8,6 @@ export interface BoardAccessFormType {
|
||||
}[];
|
||||
}
|
||||
|
||||
export const [FormProvider, useFormContext, useForm] =
|
||||
createFormContext<BoardAccessFormType>();
|
||||
export const [FormProvider, useFormContext, useForm] = createFormContext<BoardAccessFormType>();
|
||||
|
||||
export type OnCountChange = (callback: (prev: number) => number) => void;
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
Anchor,
|
||||
Button,
|
||||
Group,
|
||||
Stack,
|
||||
Table,
|
||||
TableTbody,
|
||||
TableTh,
|
||||
TableThead,
|
||||
TableTr,
|
||||
} from "@mantine/core";
|
||||
import { Anchor, Button, Group, Stack, Table, TableTbody, TableTh, TableThead, TableTr } from "@mantine/core";
|
||||
import { IconPlus } from "@tabler/icons-react";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
@@ -24,30 +14,21 @@ import { FormProvider, useForm } from "./form";
|
||||
import { GroupSelectModal } from "./group-select-modal";
|
||||
import type { FormProps } from "./user-access";
|
||||
|
||||
export const GroupsForm = ({
|
||||
board,
|
||||
initialPermissions,
|
||||
onCountChange,
|
||||
}: FormProps) => {
|
||||
const { mutate, isPending } =
|
||||
clientApi.board.saveGroupBoardPermissions.useMutation();
|
||||
export const GroupsForm = ({ board, initialPermissions, onCountChange }: FormProps) => {
|
||||
const { mutate, isPending } = clientApi.board.saveGroupBoardPermissions.useMutation();
|
||||
const utils = clientApi.useUtils();
|
||||
const [groups, setGroups] = useState<Map<string, Group>>(
|
||||
new Map(
|
||||
initialPermissions.groupPermissions.map(({ group }) => [group.id, group]),
|
||||
),
|
||||
new Map(initialPermissions.groupPermissions.map(({ group }) => [group.id, group])),
|
||||
);
|
||||
const { openModal } = useModalAction(GroupSelectModal);
|
||||
const t = useI18n();
|
||||
const tPermissions = useScopedI18n("board.setting.section.access.permission");
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
items: initialPermissions.groupPermissions.map(
|
||||
({ group, permission }) => ({
|
||||
itemId: group.id,
|
||||
permission,
|
||||
}),
|
||||
),
|
||||
items: initialPermissions.groupPermissions.map(({ group, permission }) => ({
|
||||
itemId: group.id,
|
||||
permission,
|
||||
})),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -92,9 +73,7 @@ export const GroupsForm = ({
|
||||
<Table>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh style={{ whiteSpace: "nowrap" }}>
|
||||
{tPermissions("field.group.label")}
|
||||
</TableTh>
|
||||
<TableTh style={{ whiteSpace: "nowrap" }}>{tPermissions("field.group.label")}</TableTh>
|
||||
<TableTh>{tPermissions("field.permission.label")}</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
@@ -102,9 +81,7 @@ export const GroupsForm = ({
|
||||
{form.values.items.map((row, index) => (
|
||||
<BoardAccessSelectRow
|
||||
key={row.itemId}
|
||||
itemContent={
|
||||
<GroupItemContent group={groups.get(row.itemId)!} />
|
||||
}
|
||||
itemContent={<GroupItemContent group={groups.get(row.itemId)!} />}
|
||||
permission={row.permission}
|
||||
index={index}
|
||||
onCountChange={onCountChange}
|
||||
@@ -114,11 +91,7 @@ export const GroupsForm = ({
|
||||
</Table>
|
||||
|
||||
<Group justify="space-between">
|
||||
<Button
|
||||
rightSection={<IconPlus size="1rem" />}
|
||||
variant="light"
|
||||
onClick={handleAddUser}
|
||||
>
|
||||
<Button rightSection={<IconPlus size="1rem" />} variant="light" onClick={handleAddUser}>
|
||||
{t("common.action.add")}
|
||||
</Button>
|
||||
<Button type="submit" loading={isPending} color="teal">
|
||||
@@ -133,16 +106,10 @@ export const GroupsForm = ({
|
||||
|
||||
export const GroupItemContent = ({ group }: { group: Group }) => {
|
||||
return (
|
||||
<Anchor
|
||||
component={Link}
|
||||
href={`/manage/users/groups/${group.id}`}
|
||||
size="sm"
|
||||
style={{ whiteSpace: "nowrap" }}
|
||||
>
|
||||
<Anchor component={Link} href={`/manage/users/groups/${group.id}`} size="sm" style={{ whiteSpace: "nowrap" }}>
|
||||
{group.name}
|
||||
</Anchor>
|
||||
);
|
||||
};
|
||||
|
||||
type Group =
|
||||
RouterOutputs["board"]["getBoardPermissions"]["groupPermissions"][0]["group"];
|
||||
type Group = RouterOutputs["board"]["getBoardPermissions"]["groupPermissions"][0]["group"];
|
||||
|
||||
@@ -16,59 +16,52 @@ interface GroupSelectFormType {
|
||||
groupId: string;
|
||||
}
|
||||
|
||||
export const GroupSelectModal = createModal<InnerProps>(
|
||||
({ actions, innerProps }) => {
|
||||
const t = useI18n();
|
||||
const { data: groups, isPending } = clientApi.group.selectable.useQuery();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const form = useForm<GroupSelectFormType>();
|
||||
const handleSubmitAsync = async (values: GroupSelectFormType) => {
|
||||
const currentGroup = groups?.find((group) => group.id === values.groupId);
|
||||
if (!currentGroup) return;
|
||||
setLoading(true);
|
||||
await innerProps.onSelect({
|
||||
id: currentGroup.id,
|
||||
name: currentGroup.name,
|
||||
});
|
||||
export const GroupSelectModal = createModal<InnerProps>(({ actions, innerProps }) => {
|
||||
const t = useI18n();
|
||||
const { data: groups, isPending } = clientApi.group.selectable.useQuery();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const form = useForm<GroupSelectFormType>();
|
||||
const handleSubmitAsync = async (values: GroupSelectFormType) => {
|
||||
const currentGroup = groups?.find((group) => group.id === values.groupId);
|
||||
if (!currentGroup) return;
|
||||
setLoading(true);
|
||||
await innerProps.onSelect({
|
||||
id: currentGroup.id,
|
||||
name: currentGroup.name,
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
actions.closeModal();
|
||||
};
|
||||
setLoading(false);
|
||||
actions.closeModal();
|
||||
};
|
||||
|
||||
const confirmLabel = innerProps.confirmLabel ?? t("common.action.add");
|
||||
const confirmLabel = innerProps.confirmLabel ?? t("common.action.add");
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={form.onSubmit((values) => void handleSubmitAsync(values))}
|
||||
>
|
||||
<Stack>
|
||||
<Select
|
||||
{...form.getInputProps("groupId")}
|
||||
label={t("group.action.select.label")}
|
||||
clearable
|
||||
searchable
|
||||
leftSection={isPending ? <Loader size="xs" /> : undefined}
|
||||
nothingFoundMessage={t("group.action.select.notFound")}
|
||||
limit={5}
|
||||
data={groups
|
||||
?.filter(
|
||||
(group) => !innerProps.presentGroupIds.includes(group.id),
|
||||
)
|
||||
.map((group) => ({ value: group.id, label: group.name }))}
|
||||
/>
|
||||
<Group justify="end">
|
||||
<Button variant="default" onClick={actions.closeModal}>
|
||||
{t("common.action.cancel")}
|
||||
</Button>
|
||||
<Button type="submit" loading={loading}>
|
||||
{confirmLabel}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
},
|
||||
).withOptions({
|
||||
defaultTitle: (t) =>
|
||||
t("board.setting.section.access.permission.groupSelect.title"),
|
||||
return (
|
||||
<form onSubmit={form.onSubmit((values) => void handleSubmitAsync(values))}>
|
||||
<Stack>
|
||||
<Select
|
||||
{...form.getInputProps("groupId")}
|
||||
label={t("group.action.select.label")}
|
||||
clearable
|
||||
searchable
|
||||
leftSection={isPending ? <Loader size="xs" /> : undefined}
|
||||
nothingFoundMessage={t("group.action.select.notFound")}
|
||||
limit={5}
|
||||
data={groups
|
||||
?.filter((group) => !innerProps.presentGroupIds.includes(group.id))
|
||||
.map((group) => ({ value: group.id, label: group.name }))}
|
||||
/>
|
||||
<Group justify="end">
|
||||
<Button variant="default" onClick={actions.closeModal}>
|
||||
{t("common.action.cancel")}
|
||||
</Button>
|
||||
<Button type="submit" loading={loading}>
|
||||
{confirmLabel}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
}).withOptions({
|
||||
defaultTitle: (t) => t("board.setting.section.access.permission.groupSelect.title"),
|
||||
});
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
import {
|
||||
Stack,
|
||||
Table,
|
||||
TableTbody,
|
||||
TableTh,
|
||||
TableThead,
|
||||
TableTr,
|
||||
} from "@mantine/core";
|
||||
import { Stack, Table, TableTbody, TableTh, TableThead, TableTr } from "@mantine/core";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { getPermissionsWithChildren } from "@homarr/definitions";
|
||||
@@ -41,9 +34,7 @@ export const InheritTable = ({ initialPermissions }: InheritTableProps) => {
|
||||
const boardPermission =
|
||||
permission in mapPermissions
|
||||
? mapPermissions[permission as keyof typeof mapPermissions]
|
||||
: getPermissionsWithChildren([permission]).includes(
|
||||
"board-full-access",
|
||||
)
|
||||
: getPermissionsWithChildren([permission]).includes("board-full-access")
|
||||
? "board-full"
|
||||
: null;
|
||||
|
||||
|
||||
@@ -1,17 +1,6 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
Anchor,
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Stack,
|
||||
Table,
|
||||
TableTbody,
|
||||
TableTh,
|
||||
TableThead,
|
||||
TableTr,
|
||||
} from "@mantine/core";
|
||||
import { Anchor, Box, Button, Group, Stack, Table, TableTbody, TableTh, TableThead, TableTr } from "@mantine/core";
|
||||
import { IconPlus } from "@tabler/icons-react";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
@@ -21,10 +10,7 @@ import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
import { UserAvatar } from "@homarr/ui";
|
||||
|
||||
import type { Board } from "../../../_types";
|
||||
import {
|
||||
BoardAccessDisplayRow,
|
||||
BoardAccessSelectRow,
|
||||
} from "./board-access-table-rows";
|
||||
import { BoardAccessDisplayRow, BoardAccessSelectRow } from "./board-access-table-rows";
|
||||
import type { BoardAccessFormType, OnCountChange } from "./form";
|
||||
import { FormProvider, useForm } from "./form";
|
||||
import { UserSelectModal } from "./user-select-modal";
|
||||
@@ -35,18 +21,11 @@ export interface FormProps {
|
||||
onCountChange: OnCountChange;
|
||||
}
|
||||
|
||||
export const UsersForm = ({
|
||||
board,
|
||||
initialPermissions,
|
||||
onCountChange,
|
||||
}: FormProps) => {
|
||||
const { mutate, isPending } =
|
||||
clientApi.board.saveUserBoardPermissions.useMutation();
|
||||
export const UsersForm = ({ board, initialPermissions, onCountChange }: FormProps) => {
|
||||
const { mutate, isPending } = clientApi.board.saveUserBoardPermissions.useMutation();
|
||||
const utils = clientApi.useUtils();
|
||||
const [users, setUsers] = useState<Map<string, User>>(
|
||||
new Map(
|
||||
initialPermissions.userPermissions.map(({ user }) => [user.id, user]),
|
||||
),
|
||||
new Map(initialPermissions.userPermissions.map(({ user }) => [user.id, user])),
|
||||
);
|
||||
const { openModal } = useModalAction(UserSelectModal);
|
||||
const t = useI18n();
|
||||
@@ -81,9 +60,7 @@ export const UsersForm = ({
|
||||
const presentUserIds = form.values.items.map(({ itemId: id }) => id);
|
||||
|
||||
openModal({
|
||||
presentUserIds: board.creatorId
|
||||
? presentUserIds.concat(board.creatorId)
|
||||
: presentUserIds,
|
||||
presentUserIds: board.creatorId ? presentUserIds.concat(board.creatorId) : presentUserIds,
|
||||
onSelect: (user) => {
|
||||
setUsers((prev) => new Map(prev).set(user.id, user));
|
||||
form.setFieldValue("items", [
|
||||
@@ -111,17 +88,12 @@ export const UsersForm = ({
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{board.creator && (
|
||||
<BoardAccessDisplayRow
|
||||
itemContent={<UserItemContent user={board.creator} />}
|
||||
permission="board-full"
|
||||
/>
|
||||
<BoardAccessDisplayRow itemContent={<UserItemContent user={board.creator} />} permission="board-full" />
|
||||
)}
|
||||
{form.values.items.map((row, index) => (
|
||||
<BoardAccessSelectRow
|
||||
key={row.itemId}
|
||||
itemContent={
|
||||
<UserItemContent user={users.get(row.itemId)!} />
|
||||
}
|
||||
itemContent={<UserItemContent user={users.get(row.itemId)!} />}
|
||||
permission={row.permission}
|
||||
index={index}
|
||||
onCountChange={onCountChange}
|
||||
@@ -131,11 +103,7 @@ export const UsersForm = ({
|
||||
</Table>
|
||||
|
||||
<Group justify="space-between">
|
||||
<Button
|
||||
rightSection={<IconPlus size="1rem" />}
|
||||
variant="light"
|
||||
onClick={handleAddUser}
|
||||
>
|
||||
<Button rightSection={<IconPlus size="1rem" />} variant="light" onClick={handleAddUser}>
|
||||
{t("common.action.add")}
|
||||
</Button>
|
||||
<Button type="submit" loading={isPending} color="teal">
|
||||
@@ -154,12 +122,7 @@ const UserItemContent = ({ user }: { user: User }) => {
|
||||
<Box visibleFrom="xs">
|
||||
<UserAvatar user={user} size="sm" />
|
||||
</Box>
|
||||
<Anchor
|
||||
component={Link}
|
||||
href={`/manage/users/${user.id}`}
|
||||
size="sm"
|
||||
style={{ whiteSpace: "nowrap" }}
|
||||
>
|
||||
<Anchor component={Link} href={`/manage/users/${user.id}`} size="sm" style={{ whiteSpace: "nowrap" }}>
|
||||
{user.name}
|
||||
</Anchor>
|
||||
</Group>
|
||||
|
||||
@@ -12,11 +12,7 @@ import { UserAvatar } from "@homarr/ui";
|
||||
|
||||
interface InnerProps {
|
||||
presentUserIds: string[];
|
||||
onSelect: (props: {
|
||||
id: string;
|
||||
name: string;
|
||||
image: string;
|
||||
}) => void | Promise<void>;
|
||||
onSelect: (props: { id: string; name: string; image: string }) => void | Promise<void>;
|
||||
confirmLabel?: string;
|
||||
}
|
||||
|
||||
@@ -24,68 +20,59 @@ interface UserSelectFormType {
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export const UserSelectModal = createModal<InnerProps>(
|
||||
({ actions, innerProps }) => {
|
||||
const t = useI18n();
|
||||
const { data: users, isPending } = clientApi.user.selectable.useQuery();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const form = useForm<UserSelectFormType>();
|
||||
const handleSubmitAsync = async (values: UserSelectFormType) => {
|
||||
const currentUser = users?.find((user) => user.id === values.userId);
|
||||
if (!currentUser) return;
|
||||
setLoading(true);
|
||||
await innerProps.onSelect({
|
||||
id: currentUser.id,
|
||||
name: currentUser.name ?? "",
|
||||
image: currentUser.image ?? "",
|
||||
});
|
||||
export const UserSelectModal = createModal<InnerProps>(({ actions, innerProps }) => {
|
||||
const t = useI18n();
|
||||
const { data: users, isPending } = clientApi.user.selectable.useQuery();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const form = useForm<UserSelectFormType>();
|
||||
const handleSubmitAsync = async (values: UserSelectFormType) => {
|
||||
const currentUser = users?.find((user) => user.id === values.userId);
|
||||
if (!currentUser) return;
|
||||
setLoading(true);
|
||||
await innerProps.onSelect({
|
||||
id: currentUser.id,
|
||||
name: currentUser.name ?? "",
|
||||
image: currentUser.image ?? "",
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
actions.closeModal();
|
||||
};
|
||||
setLoading(false);
|
||||
actions.closeModal();
|
||||
};
|
||||
|
||||
const confirmLabel = innerProps.confirmLabel ?? t("common.action.add");
|
||||
const currentUser = users?.find((user) => user.id === form.values.userId);
|
||||
const confirmLabel = innerProps.confirmLabel ?? t("common.action.add");
|
||||
const currentUser = users?.find((user) => user.id === form.values.userId);
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={form.onSubmit((values) => void handleSubmitAsync(values))}
|
||||
>
|
||||
<Stack>
|
||||
<Select
|
||||
{...form.getInputProps("userId")}
|
||||
label={t("user.action.select.label")}
|
||||
searchable
|
||||
clearable
|
||||
leftSection={
|
||||
isPending ? (
|
||||
<Loader size="xs" />
|
||||
) : currentUser ? (
|
||||
<UserAvatar user={currentUser} size="xs" />
|
||||
) : undefined
|
||||
}
|
||||
nothingFoundMessage={t("user.action.select.notFound")}
|
||||
renderOption={createRenderOption(users ?? [])}
|
||||
limit={5}
|
||||
data={users
|
||||
?.filter((user) => !innerProps.presentUserIds.includes(user.id))
|
||||
.map((user) => ({ value: user.id, label: user.name ?? "" }))}
|
||||
/>
|
||||
<Group justify="end">
|
||||
<Button variant="default" onClick={actions.closeModal}>
|
||||
{t("common.action.cancel")}
|
||||
</Button>
|
||||
<Button type="submit" loading={loading}>
|
||||
{confirmLabel}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
},
|
||||
).withOptions({
|
||||
defaultTitle: (t) =>
|
||||
t("board.setting.section.access.permission.userSelect.title"),
|
||||
return (
|
||||
<form onSubmit={form.onSubmit((values) => void handleSubmitAsync(values))}>
|
||||
<Stack>
|
||||
<Select
|
||||
{...form.getInputProps("userId")}
|
||||
label={t("user.action.select.label")}
|
||||
searchable
|
||||
clearable
|
||||
leftSection={
|
||||
isPending ? <Loader size="xs" /> : currentUser ? <UserAvatar user={currentUser} size="xs" /> : undefined
|
||||
}
|
||||
nothingFoundMessage={t("user.action.select.notFound")}
|
||||
renderOption={createRenderOption(users ?? [])}
|
||||
limit={5}
|
||||
data={users
|
||||
?.filter((user) => !innerProps.presentUserIds.includes(user.id))
|
||||
.map((user) => ({ value: user.id, label: user.name ?? "" }))}
|
||||
/>
|
||||
<Group justify="end">
|
||||
<Button variant="default" onClick={actions.closeModal}>
|
||||
{t("common.action.cancel")}
|
||||
</Button>
|
||||
<Button type="submit" loading={loading}>
|
||||
{confirmLabel}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
}).withOptions({
|
||||
defaultTitle: (t) => t("board.setting.section.access.permission.userSelect.title"),
|
||||
});
|
||||
|
||||
const iconProps = {
|
||||
@@ -95,9 +82,7 @@ const iconProps = {
|
||||
size: "1rem",
|
||||
};
|
||||
|
||||
const createRenderOption = (
|
||||
users: RouterOutputs["user"]["selectable"],
|
||||
): SelectProps["renderOption"] =>
|
||||
const createRenderOption = (users: RouterOutputs["user"]["selectable"]): SelectProps["renderOption"] =>
|
||||
function InnerRenderRoot({ option, checked }) {
|
||||
const user = users.find((user) => user.id === option.value);
|
||||
if (!user) return null;
|
||||
@@ -106,9 +91,7 @@ const createRenderOption = (
|
||||
<Group flex="1" gap="xs">
|
||||
<UserAvatar user={user} size="xs" />
|
||||
{option.label}
|
||||
{checked && (
|
||||
<IconCheck style={{ marginInlineStart: "auto" }} {...iconProps} />
|
||||
)}
|
||||
{checked && <IconCheck style={{ marginInlineStart: "auto" }} {...iconProps} />}
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,11 +2,7 @@
|
||||
|
||||
import { Button, Grid, Group, Stack, TextInput } from "@mantine/core";
|
||||
|
||||
import {
|
||||
backgroundImageAttachments,
|
||||
backgroundImageRepeats,
|
||||
backgroundImageSizes,
|
||||
} from "@homarr/definitions";
|
||||
import { backgroundImageAttachments, backgroundImageRepeats, backgroundImageSizes } from "@homarr/definitions";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import type { TranslationObject } from "@homarr/translation";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
@@ -22,8 +18,7 @@ interface Props {
|
||||
}
|
||||
export const BackgroundSettingsContent = ({ board }: Props) => {
|
||||
const t = useI18n();
|
||||
const { mutate: savePartialSettings, isPending } =
|
||||
useSavePartialSettingsMutation(board);
|
||||
const { mutate: savePartialSettings, isPending } = useSavePartialSettingsMutation(board);
|
||||
const form = useZodForm(validation.board.savePartialSettings, {
|
||||
initialValues: {
|
||||
backgroundImageUrl: board.backgroundImageUrl ?? "",
|
||||
@@ -37,14 +32,8 @@ export const BackgroundSettingsContent = ({ board }: Props) => {
|
||||
"backgroundImageAttachment",
|
||||
backgroundImageAttachments,
|
||||
);
|
||||
const backgroundImageSizeData = useBackgroundOptionData(
|
||||
"backgroundImageSize",
|
||||
backgroundImageSizes,
|
||||
);
|
||||
const backgroundImageRepeatData = useBackgroundOptionData(
|
||||
"backgroundImageRepeat",
|
||||
backgroundImageRepeats,
|
||||
);
|
||||
const backgroundImageSizeData = useBackgroundOptionData("backgroundImageSize", backgroundImageSizes);
|
||||
const backgroundImageRepeatData = useBackgroundOptionData("backgroundImageRepeat", backgroundImageRepeats);
|
||||
|
||||
return (
|
||||
<form
|
||||
@@ -96,13 +85,9 @@ export const BackgroundSettingsContent = ({ board }: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
type BackgroundImageKey =
|
||||
| "backgroundImageAttachment"
|
||||
| "backgroundImageSize"
|
||||
| "backgroundImageRepeat";
|
||||
type BackgroundImageKey = "backgroundImageAttachment" | "backgroundImageSize" | "backgroundImageRepeat";
|
||||
|
||||
type inferOptions<TKey extends BackgroundImageKey> =
|
||||
TranslationObject["board"]["field"][TKey]["option"];
|
||||
type inferOptions<TKey extends BackgroundImageKey> = TranslationObject["board"]["field"][TKey]["option"];
|
||||
|
||||
const useBackgroundOptionData = <
|
||||
TKey extends BackgroundImageKey,
|
||||
@@ -120,9 +105,7 @@ const useBackgroundOptionData = <
|
||||
(value) =>
|
||||
({
|
||||
label: t(`board.field.${key}.option.${value as string}.label` as never),
|
||||
description: t(
|
||||
`board.field.${key}.option.${value as string}.description` as never,
|
||||
),
|
||||
description: t(`board.field.${key}.option.${value as string}.description` as never),
|
||||
value: value as string,
|
||||
badge:
|
||||
data.defaultValue === value
|
||||
|
||||
@@ -44,8 +44,7 @@ export const ColorSettingsContent = ({ board }: Props) => {
|
||||
const [showPreview, { toggle }] = useDisclosure(false);
|
||||
const t = useI18n();
|
||||
const theme = useMantineTheme();
|
||||
const { mutate: savePartialSettings, isPending } =
|
||||
useSavePartialSettingsMutation(board);
|
||||
const { mutate: savePartialSettings, isPending } = useSavePartialSettingsMutation(board);
|
||||
|
||||
return (
|
||||
<form
|
||||
@@ -77,11 +76,7 @@ export const ColorSettingsContent = ({ board }: Props) => {
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<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 span={12}>
|
||||
<Collapse in={showPreview}>
|
||||
@@ -121,10 +116,7 @@ interface ColorsPreviewProps {
|
||||
const ColorsPreview = ({ previewColor }: ColorsPreviewProps) => {
|
||||
const theme = useMantineTheme();
|
||||
|
||||
const colors =
|
||||
previewColor && hexRegex.test(previewColor)
|
||||
? generateColors(previewColor)
|
||||
: generateColors("#000000");
|
||||
const colors = previewColor && hexRegex.test(previewColor) ? generateColors(previewColor) : generateColors("#000000");
|
||||
|
||||
return (
|
||||
<Group gap={0} wrap="nowrap">
|
||||
|
||||
@@ -20,8 +20,7 @@ export const DangerZoneSettingsContent = () => {
|
||||
const { openModal } = useModalAction(BoardRenameModal);
|
||||
const { mutate: changeVisibility, isPending: isChangeVisibilityPending } =
|
||||
clientApi.board.changeBoardVisibility.useMutation();
|
||||
const { mutate: deleteBoard, isPending: isDeletePending } =
|
||||
clientApi.board.deleteBoard.useMutation();
|
||||
const { mutate: deleteBoard, isPending: isDeletePending } = clientApi.board.deleteBoard.useMutation();
|
||||
const utils = clientApi.useUtils();
|
||||
const visibility = board.isPublic ? "public" : "private";
|
||||
|
||||
@@ -37,12 +36,8 @@ export const DangerZoneSettingsContent = () => {
|
||||
|
||||
const onVisibilityClick = useCallback(() => {
|
||||
openConfirmModal({
|
||||
title: t(
|
||||
`section.dangerZone.action.visibility.confirm.${visibility}.title`,
|
||||
),
|
||||
children: t(
|
||||
`section.dangerZone.action.visibility.confirm.${visibility}.description`,
|
||||
),
|
||||
title: t(`section.dangerZone.action.visibility.confirm.${visibility}.title`),
|
||||
children: t(`section.dangerZone.action.visibility.confirm.${visibility}.description`),
|
||||
onConfirm: () => {
|
||||
changeVisibility(
|
||||
{
|
||||
@@ -98,12 +93,8 @@ export const DangerZoneSettingsContent = () => {
|
||||
<Divider />
|
||||
<DangerZoneRow
|
||||
label={t("section.dangerZone.action.visibility.label")}
|
||||
description={t(
|
||||
`section.dangerZone.action.visibility.description.${visibility}`,
|
||||
)}
|
||||
buttonText={t(
|
||||
`section.dangerZone.action.visibility.button.${visibility}`,
|
||||
)}
|
||||
description={t(`section.dangerZone.action.visibility.description.${visibility}`)}
|
||||
buttonText={t(`section.dangerZone.action.visibility.button.${visibility}`)}
|
||||
onClick={onVisibilityClick}
|
||||
isPending={isChangeVisibilityPending}
|
||||
/>
|
||||
@@ -127,13 +118,7 @@ interface DangerZoneRowProps {
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const DangerZoneRow = ({
|
||||
label,
|
||||
description,
|
||||
buttonText,
|
||||
onClick,
|
||||
isPending,
|
||||
}: DangerZoneRowProps) => {
|
||||
const DangerZoneRow = ({ label, description, buttonText, onClick, isPending }: DangerZoneRowProps) => {
|
||||
return (
|
||||
<Group justify="space-between" px="md" className={classes.dangerZoneGroup}>
|
||||
<Stack gap={0}>
|
||||
@@ -143,12 +128,7 @@ const DangerZoneRow = ({
|
||||
<Text size="sm">{description}</Text>
|
||||
</Stack>
|
||||
<Group justify="end" w={{ base: "100%", xs: "auto" }}>
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="red"
|
||||
loading={isPending}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Button variant="subtle" color="red" loading={isPending} onClick={onClick}>
|
||||
{buttonText}
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
@@ -1,20 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import {
|
||||
Button,
|
||||
Grid,
|
||||
Group,
|
||||
Loader,
|
||||
Stack,
|
||||
TextInput,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
useDebouncedValue,
|
||||
useDocumentTitle,
|
||||
useFavicon,
|
||||
} from "@mantine/hooks";
|
||||
import { Button, Grid, Group, Loader, Stack, TextInput, Tooltip } from "@mantine/core";
|
||||
import { useDebouncedValue, useDocumentTitle, useFavicon } from "@mantine/hooks";
|
||||
import { IconAlertTriangle } from "@tabler/icons-react";
|
||||
|
||||
import { useZodForm } from "@homarr/form";
|
||||
@@ -38,8 +26,7 @@ export const GeneralSettingsContent = ({ board }: Props) => {
|
||||
});
|
||||
const { updateBoard } = useUpdateBoard();
|
||||
|
||||
const { mutate: savePartialSettings, isPending } =
|
||||
useSavePartialSettingsMutation(board);
|
||||
const { mutate: savePartialSettings, isPending } = useSavePartialSettingsMutation(board);
|
||||
const form = useZodForm(
|
||||
validation.board.savePartialSettings
|
||||
.pick({
|
||||
@@ -106,9 +93,7 @@ export const GeneralSettingsContent = ({ board }: Props) => {
|
||||
<Grid.Col span={{ xs: 12, md: 6 }}>
|
||||
<TextInput
|
||||
label={t("board.field.metaTitle.label")}
|
||||
placeholder={createMetaTitle(
|
||||
t("board.content.metaTitle", { boardName: board.name }),
|
||||
)}
|
||||
placeholder={createMetaTitle(t("board.content.metaTitle", { boardName: board.name }))}
|
||||
rightSection={<PendingOrInvalidIndicator {...metaTitleStatus} />}
|
||||
{...form.getInputProps("metaTitle")}
|
||||
/>
|
||||
@@ -140,22 +125,12 @@ export const GeneralSettingsContent = ({ board }: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const PendingOrInvalidIndicator = ({
|
||||
isPending,
|
||||
isInvalid,
|
||||
}: {
|
||||
isPending: boolean;
|
||||
isInvalid?: boolean;
|
||||
}) => {
|
||||
const PendingOrInvalidIndicator = ({ isPending, isInvalid }: { isPending: boolean; isInvalid?: boolean }) => {
|
||||
const t = useI18n();
|
||||
|
||||
if (isInvalid) {
|
||||
return (
|
||||
<Tooltip
|
||||
multiline
|
||||
w={220}
|
||||
label={t("board.setting.section.general.unrecognizedLink")}
|
||||
>
|
||||
<Tooltip multiline w={220} label={t("board.setting.section.general.unrecognizedLink")}>
|
||||
<IconAlertTriangle size="1rem" color="red" />
|
||||
</Tooltip>
|
||||
);
|
||||
@@ -197,8 +172,7 @@ const useMetaTitlePreview = (title: string | null) => {
|
||||
|
||||
const validFaviconExtensions = ["ico", "png", "svg", "gif"];
|
||||
const isValidUrl = (url: string) =>
|
||||
url.includes("/") &&
|
||||
validFaviconExtensions.some((extension) => url.endsWith(`.${extension}`));
|
||||
url.includes("/") && validFaviconExtensions.some((extension) => url.endsWith(`.${extension}`));
|
||||
|
||||
const useFaviconPreview = (url: string | null) => {
|
||||
const [faviconDebounced] = useDebouncedValue(url ?? "", 500);
|
||||
|
||||
@@ -14,16 +14,12 @@ interface Props {
|
||||
}
|
||||
export const LayoutSettingsContent = ({ board }: Props) => {
|
||||
const t = useI18n();
|
||||
const { mutate: savePartialSettings, isPending } =
|
||||
useSavePartialSettingsMutation(board);
|
||||
const form = useZodForm(
|
||||
validation.board.savePartialSettings.pick({ columnCount: true }).required(),
|
||||
{
|
||||
initialValues: {
|
||||
columnCount: board.columnCount,
|
||||
},
|
||||
const { mutate: savePartialSettings, isPending } = useSavePartialSettingsMutation(board);
|
||||
const form = useZodForm(validation.board.savePartialSettings.pick({ columnCount: true }).required(), {
|
||||
initialValues: {
|
||||
columnCount: board.columnCount,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<form
|
||||
@@ -38,13 +34,7 @@ export const LayoutSettingsContent = ({ board }: Props) => {
|
||||
<Grid>
|
||||
<Grid.Col span={{ sm: 12, md: 6 }}>
|
||||
<Input.Wrapper label={t("board.field.columnCount.label")}>
|
||||
<Slider
|
||||
mt="xs"
|
||||
min={1}
|
||||
max={24}
|
||||
step={1}
|
||||
{...form.getInputProps("columnCount")}
|
||||
/>
|
||||
<Slider mt="xs" min={1} max={24} step={1} {...form.getInputProps("columnCount")} />
|
||||
</Input.Wrapper>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
import type { PropsWithChildren } from "react";
|
||||
import { notFound } from "next/navigation";
|
||||
import {
|
||||
AccordionControl,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Container,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { AccordionControl, AccordionItem, AccordionPanel, Container, Stack, Text, Title } from "@mantine/core";
|
||||
import {
|
||||
IconAlertTriangle,
|
||||
IconBrush,
|
||||
@@ -69,10 +61,7 @@ const getBoardAndPermissionsAsync = async (params: Props["params"]) => {
|
||||
}
|
||||
};
|
||||
|
||||
export default async function BoardSettingsPage({
|
||||
params,
|
||||
searchParams,
|
||||
}: Props) {
|
||||
export default async function BoardSettingsPage({ params, searchParams }: Props) {
|
||||
const { board, permissions } = await getBoardAndPermissionsAsync(params);
|
||||
const { hasFullAccess } = await getBoardPermissionsAsync(board);
|
||||
const t = await getScopedI18n("board.setting");
|
||||
@@ -81,10 +70,7 @@ export default async function BoardSettingsPage({
|
||||
<Container>
|
||||
<Stack>
|
||||
<Title>{t("title", { boardName: capitalize(board.name) })}</Title>
|
||||
<ActiveTabAccordion
|
||||
variant="separated"
|
||||
defaultValue={searchParams.tab ?? "general"}
|
||||
>
|
||||
<ActiveTabAccordion variant="separated" defaultValue={searchParams.tab ?? "general"}>
|
||||
<AccordionItemFor value="general" icon={IconSettings}>
|
||||
<GeneralSettingsContent board={board} />
|
||||
</AccordionItemFor>
|
||||
@@ -103,17 +89,9 @@ export default async function BoardSettingsPage({
|
||||
{hasFullAccess && (
|
||||
<>
|
||||
<AccordionItemFor value="access" icon={IconUser}>
|
||||
<AccessSettingsContent
|
||||
board={board}
|
||||
initialPermissions={permissions}
|
||||
/>
|
||||
<AccessSettingsContent board={board} initialPermissions={permissions} />
|
||||
</AccordionItemFor>
|
||||
<AccordionItemFor
|
||||
value="dangerZone"
|
||||
icon={IconAlertTriangle}
|
||||
danger
|
||||
noPadding
|
||||
>
|
||||
<AccordionItemFor value="dangerZone" icon={IconAlertTriangle} danger noPadding>
|
||||
<DangerZoneSettingsContent />
|
||||
</AccordionItemFor>
|
||||
</>
|
||||
@@ -131,13 +109,7 @@ type AccordionItemForProps = PropsWithChildren<{
|
||||
noPadding?: boolean;
|
||||
}>;
|
||||
|
||||
const AccordionItemFor = async ({
|
||||
value,
|
||||
children,
|
||||
icon: Icon,
|
||||
danger,
|
||||
noPadding,
|
||||
}: AccordionItemForProps) => {
|
||||
const AccordionItemFor = async ({ value, children, icon: Icon, danger, noPadding }: AccordionItemForProps) => {
|
||||
const t = await getScopedI18n("board.setting.section");
|
||||
return (
|
||||
<AccordionItem
|
||||
@@ -158,13 +130,7 @@ const AccordionItemFor = async ({
|
||||
{t(`${value}.title`)}
|
||||
</Text>
|
||||
</AccordionControl>
|
||||
<AccordionPanel
|
||||
styles={
|
||||
noPadding
|
||||
? { content: { paddingRight: 0, paddingLeft: 0 } }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<AccordionPanel styles={noPadding ? { content: { paddingRight: 0, paddingLeft: 0 } } : undefined}>
|
||||
{children}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
|
||||
@@ -41,10 +41,7 @@ export const createBoardLayout = <TParams extends Params>({
|
||||
});
|
||||
|
||||
return (
|
||||
<GlobalItemServerDataRunner
|
||||
board={initialBoard}
|
||||
shouldRun={isBoardContentPage}
|
||||
>
|
||||
<GlobalItemServerDataRunner board={initialBoard} shouldRun={isBoardContentPage}>
|
||||
<BoardProvider initialBoard={initialBoard}>
|
||||
<BoardMantineProvider>
|
||||
<ClientShell hasNavigation={false}>
|
||||
|
||||
@@ -8,7 +8,4 @@ export type Item = Section["items"][number];
|
||||
export type CategorySection = Extract<Section, { kind: "category" }>;
|
||||
export type EmptySection = Extract<Section, { kind: "empty" }>;
|
||||
|
||||
export type ItemOfKind<TKind extends WidgetKind> = Extract<
|
||||
Item,
|
||||
{ kind: TKind }
|
||||
>;
|
||||
export type ItemOfKind<TKind extends WidgetKind> = Extract<Item, { kind: TKind }>;
|
||||
|
||||
@@ -5,14 +5,12 @@ type PropsWithChildren = Required<React.PropsWithChildren>;
|
||||
export const composeWrappers = (
|
||||
wrappers: React.FunctionComponent<PropsWithChildren>[],
|
||||
): React.FunctionComponent<PropsWithChildren> => {
|
||||
return wrappers
|
||||
.reverse()
|
||||
.reduce((Acc, Current): React.FunctionComponent<PropsWithChildren> => {
|
||||
// eslint-disable-next-line react/display-name
|
||||
return (props) => (
|
||||
<Current>
|
||||
<Acc {...props} />
|
||||
</Current>
|
||||
);
|
||||
});
|
||||
return wrappers.reverse().reduce((Acc, Current): React.FunctionComponent<PropsWithChildren> => {
|
||||
// eslint-disable-next-line react/display-name
|
||||
return (props) => (
|
||||
<Current>
|
||||
<Acc {...props} />
|
||||
</Current>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -5,10 +5,7 @@ import { Button, PasswordInput, Stack, TextInput } from "@mantine/core";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import {
|
||||
showErrorNotification,
|
||||
showSuccessNotification,
|
||||
} from "@homarr/notifications";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import type { z } from "@homarr/validation";
|
||||
import { validation } from "@homarr/validation";
|
||||
@@ -16,8 +13,7 @@ import { validation } from "@homarr/validation";
|
||||
export const InitUserForm = () => {
|
||||
const router = useRouter();
|
||||
const t = useScopedI18n("user");
|
||||
const { mutateAsync, error, isPending } =
|
||||
clientApi.user.initUser.useMutation();
|
||||
const { mutateAsync, error, isPending } = clientApi.user.initUser.useMutation();
|
||||
const form = useZodForm(validation.user.init, {
|
||||
initialValues: {
|
||||
username: "",
|
||||
@@ -53,18 +49,9 @@ export const InitUserForm = () => {
|
||||
)}
|
||||
>
|
||||
<Stack gap="lg">
|
||||
<TextInput
|
||||
label={t("field.username.label")}
|
||||
{...form.getInputProps("username")}
|
||||
/>
|
||||
<PasswordInput
|
||||
label={t("field.password.label")}
|
||||
{...form.getInputProps("password")}
|
||||
/>
|
||||
<PasswordInput
|
||||
label={t("field.passwordConfirm.label")}
|
||||
{...form.getInputProps("confirmPassword")}
|
||||
/>
|
||||
<TextInput label={t("field.username.label")} {...form.getInputProps("username")} />
|
||||
<PasswordInput label={t("field.password.label")} {...form.getInputProps("password")} />
|
||||
<PasswordInput label={t("field.passwordConfirm.label")} {...form.getInputProps("confirmPassword")} />
|
||||
<Button type="submit" fullWidth loading={isPending}>
|
||||
{t("action.create")}
|
||||
</Button>
|
||||
|
||||
@@ -48,10 +48,7 @@ export const viewport: Viewport = {
|
||||
],
|
||||
};
|
||||
|
||||
export default function Layout(props: {
|
||||
children: React.ReactNode;
|
||||
params: { locale: string };
|
||||
}) {
|
||||
export default function Layout(props: { children: React.ReactNode; params: { locale: string } }) {
|
||||
const colorScheme = "dark";
|
||||
|
||||
const StackedProvider = composeWrappers([
|
||||
@@ -61,9 +58,7 @@ export default function Layout(props: {
|
||||
},
|
||||
(innerProps) => <JotaiProvider {...innerProps} />,
|
||||
(innerProps) => <TRPCReactProvider {...innerProps} />,
|
||||
(innerProps) => (
|
||||
<NextInternationalProvider {...innerProps} locale={props.params.locale} />
|
||||
),
|
||||
(innerProps) => <NextInternationalProvider {...innerProps} locale={props.params.locale} />,
|
||||
(innerProps) => (
|
||||
<MantineProvider
|
||||
{...innerProps}
|
||||
|
||||
@@ -48,13 +48,7 @@ export const HeroBanner = () => {
|
||||
<Title>Homarr Dashboard</Title>
|
||||
</Group>
|
||||
</Stack>
|
||||
<Box
|
||||
className={classes.scrollContainer}
|
||||
w={"30%"}
|
||||
top={0}
|
||||
right={0}
|
||||
pos="absolute"
|
||||
>
|
||||
<Box className={classes.scrollContainer} w={"30%"} top={0} right={0} pos="absolute">
|
||||
<Grid>
|
||||
{Array(countIconGroups)
|
||||
.fill(0)
|
||||
@@ -67,24 +61,12 @@ export const HeroBanner = () => {
|
||||
}}
|
||||
>
|
||||
{arrayInChunks[columnIndex]?.map((icon, index) => (
|
||||
<Image
|
||||
key={`grid-column-${columnIndex}-scroll-1-${index}`}
|
||||
src={icon}
|
||||
radius="md"
|
||||
w={50}
|
||||
h={50}
|
||||
/>
|
||||
<Image key={`grid-column-${columnIndex}-scroll-1-${index}`} src={icon} radius="md" w={50} h={50} />
|
||||
))}
|
||||
|
||||
{/* This is used for making the animation seem seamless */}
|
||||
{arrayInChunks[columnIndex]?.map((icon, index) => (
|
||||
<Image
|
||||
key={`grid-column-${columnIndex}-scroll-2-${index}`}
|
||||
src={icon}
|
||||
radius="md"
|
||||
w={50}
|
||||
h={50}
|
||||
/>
|
||||
<Image key={`grid-column-${columnIndex}-scroll-2-${index}`} src={icon} radius="md" w={50} h={50} />
|
||||
))}
|
||||
</Stack>
|
||||
</GridCol>
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
.contributorCard {
|
||||
background-color: light-dark(
|
||||
var(--mantine-color-gray-1),
|
||||
var(--mantine-color-dark-5)
|
||||
);
|
||||
background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5));
|
||||
}
|
||||
|
||||
@@ -55,9 +55,7 @@ export default async function AboutPage({ params: { locale } }: PageProps) {
|
||||
<Title order={1} tt="uppercase">
|
||||
Homarr
|
||||
</Title>
|
||||
<Title order={2}>
|
||||
{t("version", { version: attributes.version })}
|
||||
</Title>
|
||||
<Title order={2}>{t("version", { version: attributes.version })}</Title>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Center>
|
||||
@@ -150,20 +148,10 @@ interface GenericContributorLinkCardProps {
|
||||
image: string;
|
||||
}
|
||||
|
||||
const GenericContributorLinkCard = ({
|
||||
name,
|
||||
image,
|
||||
link,
|
||||
}: GenericContributorLinkCardProps) => {
|
||||
const GenericContributorLinkCard = ({ name, image, link }: GenericContributorLinkCardProps) => {
|
||||
return (
|
||||
<AspectRatio ratio={1}>
|
||||
<Card
|
||||
className={classes.contributorCard}
|
||||
component="a"
|
||||
href={link}
|
||||
target="_blank"
|
||||
w={100}
|
||||
>
|
||||
<Card className={classes.contributorCard} component="a" href={link} target="_blank" w={100}>
|
||||
<Stack align="center">
|
||||
<Avatar src={image} alt={name} size={40} display="block" />
|
||||
<Text lineClamp={1} size="sm">
|
||||
|
||||
@@ -7,10 +7,7 @@ import { IconTrash } from "@tabler/icons-react";
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useConfirmModal } from "@homarr/modals";
|
||||
import {
|
||||
showErrorNotification,
|
||||
showSuccessNotification,
|
||||
} from "@homarr/notifications";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { revalidatePathActionAsync } from "../../../revalidatePathAction";
|
||||
@@ -52,13 +49,7 @@ export const AppDeleteButton = ({ app }: AppDeleteButtonProps) => {
|
||||
}, [app, mutate, t, openConfirmModal]);
|
||||
|
||||
return (
|
||||
<ActionIcon
|
||||
loading={isPending}
|
||||
variant="subtle"
|
||||
color="red"
|
||||
onClick={onClick}
|
||||
aria-label="Delete app"
|
||||
>
|
||||
<ActionIcon loading={isPending} variant="subtle" color="red" onClick={onClick} aria-label="Delete app">
|
||||
<IconTrash color="red" size={16} stroke={1.5} />
|
||||
</ActionIcon>
|
||||
);
|
||||
|
||||
@@ -21,8 +21,7 @@ interface AppFormProps {
|
||||
}
|
||||
|
||||
export const AppForm = (props: AppFormProps) => {
|
||||
const { submitButtonTranslation, handleSubmit, initialValues, isPending } =
|
||||
props;
|
||||
const { submitButtonTranslation, handleSubmit, initialValues, isPending } = props;
|
||||
const t = useI18n();
|
||||
|
||||
const form = useZodForm(validation.app.manage, {
|
||||
@@ -38,10 +37,7 @@ export const AppForm = (props: AppFormProps) => {
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack>
|
||||
<TextInput {...form.getInputProps("name")} withAsterisk label="Name" />
|
||||
<IconPicker
|
||||
initialValue={initialValues?.iconUrl}
|
||||
{...form.getInputProps("iconUrl")}
|
||||
/>
|
||||
<IconPicker initialValue={initialValues?.iconUrl} {...form.getInputProps("iconUrl")} />
|
||||
<Textarea {...form.getInputProps("description")} label="Description" />
|
||||
<TextInput {...form.getInputProps("href")} label="URL" />
|
||||
|
||||
|
||||
@@ -5,10 +5,7 @@ import { useRouter } from "next/navigation";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import {
|
||||
showErrorNotification,
|
||||
showSuccessNotification,
|
||||
} from "@homarr/notifications";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import type { TranslationFunction } from "@homarr/translation";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import type { validation, z } from "@homarr/validation";
|
||||
@@ -52,10 +49,7 @@ export const AppEditForm = ({ app }: AppEditFormProps) => {
|
||||
[mutate, app.id],
|
||||
);
|
||||
|
||||
const submitButtonTranslation = useCallback(
|
||||
(t: TranslationFunction) => t("common.action.save"),
|
||||
[],
|
||||
);
|
||||
const submitButtonTranslation = useCallback((t: TranslationFunction) => t("common.action.save"), []);
|
||||
|
||||
return (
|
||||
<AppForm
|
||||
|
||||
@@ -4,10 +4,7 @@ import { useCallback } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import {
|
||||
showErrorNotification,
|
||||
showSuccessNotification,
|
||||
} from "@homarr/notifications";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import type { TranslationFunction } from "@homarr/translation";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import type { validation, z } from "@homarr/validation";
|
||||
@@ -44,16 +41,9 @@ export const AppNewForm = () => {
|
||||
[mutate],
|
||||
);
|
||||
|
||||
const submitButtonTranslation = useCallback(
|
||||
(t: TranslationFunction) => t("common.action.create"),
|
||||
[],
|
||||
);
|
||||
const submitButtonTranslation = useCallback((t: TranslationFunction) => t("common.action.create"), []);
|
||||
|
||||
return (
|
||||
<AppForm
|
||||
submitButtonTranslation={submitButtonTranslation}
|
||||
handleSubmit={handleSubmit}
|
||||
isPending={isPending}
|
||||
/>
|
||||
<AppForm submitButtonTranslation={submitButtonTranslation} handleSubmit={handleSubmit} isPending={isPending} />
|
||||
);
|
||||
};
|
||||
|
||||
@@ -107,9 +107,7 @@ const AppNoResults = async () => {
|
||||
<Text fw={500} size="lg">
|
||||
{t("app.page.list.noResults.title")}
|
||||
</Text>
|
||||
<Anchor href="/manage/apps/new">
|
||||
{t("app.page.list.noResults.description")}
|
||||
</Anchor>
|
||||
<Anchor href="/manage/apps/new">{t("app.page.list.noResults.description")}</Anchor>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -21,18 +21,11 @@ const iconProps = {
|
||||
interface BoardCardMenuDropdownProps {
|
||||
board: Pick<
|
||||
RouterOutputs["board"]["getAllBoards"][number],
|
||||
| "id"
|
||||
| "name"
|
||||
| "creator"
|
||||
| "userPermissions"
|
||||
| "groupPermissions"
|
||||
| "isPublic"
|
||||
"id" | "name" | "creator" | "userPermissions" | "groupPermissions" | "isPublic"
|
||||
>;
|
||||
}
|
||||
|
||||
export const BoardCardMenuDropdown = ({
|
||||
board,
|
||||
}: BoardCardMenuDropdownProps) => {
|
||||
export const BoardCardMenuDropdown = ({ board }: BoardCardMenuDropdownProps) => {
|
||||
const t = useScopedI18n("management.page.board.action");
|
||||
const tCommon = useScopedI18n("common");
|
||||
|
||||
@@ -73,10 +66,7 @@ export const BoardCardMenuDropdown = ({
|
||||
|
||||
return (
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item
|
||||
onClick={handleSetHomeBoard}
|
||||
leftSection={<IconHome {...iconProps} />}
|
||||
>
|
||||
<Menu.Item onClick={handleSetHomeBoard} leftSection={<IconHome {...iconProps} />}>
|
||||
{t("setHomeBoard.label")}
|
||||
</Menu.Item>
|
||||
{hasChangeAccess && (
|
||||
|
||||
@@ -37,11 +37,7 @@ export const CreateBoardButton = ({ boardNames }: CreateBoardButtonProps) => {
|
||||
}, [mutateAsync, boardNames, openModal]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
leftSection={<IconCategoryPlus size="1rem" />}
|
||||
onClick={onClick}
|
||||
loading={isPending}
|
||||
>
|
||||
<Button leftSection={<IconCategoryPlus size="1rem" />} onClick={onClick} loading={isPending}>
|
||||
{t("management.page.board.action.new.label")}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -14,12 +14,7 @@ import {
|
||||
Title,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
IconDotsVertical,
|
||||
IconHomeFilled,
|
||||
IconLock,
|
||||
IconWorld,
|
||||
} from "@tabler/icons-react";
|
||||
import { IconDotsVertical, IconHomeFilled, IconLock, IconWorld } from "@tabler/icons-react";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { api } from "@homarr/api/server";
|
||||
@@ -59,8 +54,7 @@ interface BoardCardProps {
|
||||
|
||||
const BoardCard = async ({ board }: BoardCardProps) => {
|
||||
const t = await getScopedI18n("management.page.board");
|
||||
const { hasChangeAccess: isMenuVisible } =
|
||||
await getBoardPermissionsAsync(board);
|
||||
const { hasChangeAccess: isMenuVisible } = await getBoardPermissionsAsync(board);
|
||||
const visibility = board.isPublic ? "public" : "private";
|
||||
const VisibilityIcon = board.isPublic ? IconWorld : IconLock;
|
||||
|
||||
@@ -80,12 +74,7 @@ const BoardCard = async ({ board }: BoardCardProps) => {
|
||||
<Group>
|
||||
{board.isHome && (
|
||||
<Tooltip label={t("action.setHomeBoard.badge.tooltip")}>
|
||||
<Badge
|
||||
tt="none"
|
||||
color="yellow"
|
||||
variant="light"
|
||||
leftSection={<IconHomeFilled size=".7rem" />}
|
||||
>
|
||||
<Badge tt="none" color="yellow" variant="light" leftSection={<IconHomeFilled size=".7rem" />}>
|
||||
{t("action.setHomeBoard.badge.label")}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
@@ -103,12 +92,7 @@ const BoardCard = async ({ board }: BoardCardProps) => {
|
||||
|
||||
<CardSection p="sm">
|
||||
<Group wrap="nowrap">
|
||||
<Button
|
||||
component={Link}
|
||||
href={`/boards/${board.name}`}
|
||||
variant="default"
|
||||
fullWidth
|
||||
>
|
||||
<Button component={Link} href={`/boards/${board.name}`} variant="default" fullWidth>
|
||||
{t("action.open.label")}
|
||||
</Button>
|
||||
{isMenuVisible && (
|
||||
|
||||
@@ -6,10 +6,7 @@ import { IconTrash } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useConfirmModal } from "@homarr/modals";
|
||||
import {
|
||||
showErrorNotification,
|
||||
showSuccessNotification,
|
||||
} from "@homarr/notifications";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { revalidatePathActionAsync } from "../../../revalidatePathAction";
|
||||
@@ -19,10 +16,7 @@ interface DeleteIntegrationActionButtonProps {
|
||||
integration: { id: string; name: string };
|
||||
}
|
||||
|
||||
export const DeleteIntegrationActionButton = ({
|
||||
count,
|
||||
integration,
|
||||
}: DeleteIntegrationActionButtonProps) => {
|
||||
export const DeleteIntegrationActionButton = ({ count, integration }: DeleteIntegrationActionButtonProps) => {
|
||||
const t = useScopedI18n("integration.page.delete");
|
||||
const router = useRouter();
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
|
||||
@@ -2,17 +2,7 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import {
|
||||
ActionIcon,
|
||||
Avatar,
|
||||
Button,
|
||||
Card,
|
||||
Collapse,
|
||||
Group,
|
||||
Kbd,
|
||||
Stack,
|
||||
Text,
|
||||
} from "@mantine/core";
|
||||
import { ActionIcon, Avatar, Button, Card, Collapse, Group, Kbd, Stack, Text } from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { IconEye, IconEyeOff } from "@tabler/icons-react";
|
||||
import dayjs from "dayjs";
|
||||
@@ -36,8 +26,7 @@ export const SecretCard = ({ secret, children, onCancel }: SecretCardProps) => {
|
||||
const params = useParams<{ locale: string }>();
|
||||
const t = useI18n();
|
||||
const { isPublic } = integrationSecretKindObject[secret.kind];
|
||||
const [publicSecretDisplayOpened, { toggle: togglePublicSecretDisplay }] =
|
||||
useDisclosure(false);
|
||||
const [publicSecretDisplayOpened, { toggle: togglePublicSecretDisplay }] = useDisclosure(false);
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
const DisplayIcon = publicSecretDisplayOpened ? IconEye : IconEyeOff;
|
||||
const KindIcon = integrationSecretIcons[secret.kind];
|
||||
@@ -50,9 +39,7 @@ export const SecretCard = ({ secret, children, onCancel }: SecretCardProps) => {
|
||||
<Avatar>
|
||||
<KindIcon size={16} />
|
||||
</Avatar>
|
||||
<Text fw={500}>
|
||||
{t(`integration.secrets.kind.${secret.kind}.label`)}
|
||||
</Text>
|
||||
<Text fw={500}>{t(`integration.secrets.kind.${secret.kind}.label`)}</Text>
|
||||
{publicSecretDisplayOpened ? <Kbd>{secret.value}</Kbd> : null}
|
||||
</Group>
|
||||
<Group>
|
||||
@@ -62,11 +49,7 @@ export const SecretCard = ({ secret, children, onCancel }: SecretCardProps) => {
|
||||
})}
|
||||
</Text>
|
||||
{isPublic ? (
|
||||
<ActionIcon
|
||||
color="gray"
|
||||
variant="subtle"
|
||||
onClick={togglePublicSecretDisplay}
|
||||
>
|
||||
<ActionIcon color="gray" variant="subtle" onClick={togglePublicSecretDisplay}>
|
||||
<DisplayIcon size={16} stroke={1.5} />
|
||||
</ActionIcon>
|
||||
) : null}
|
||||
|
||||
@@ -42,10 +42,7 @@ const PublicSecretInput = ({ kind, ...props }: IntegrationSecretInputProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const PrivateSecretInput = ({
|
||||
kind,
|
||||
...props
|
||||
}: IntegrationSecretInputProps) => {
|
||||
const PrivateSecretInput = ({ kind, ...props }: IntegrationSecretInputProps) => {
|
||||
const t = useI18n();
|
||||
const Icon = integrationSecretIcons[kind];
|
||||
|
||||
|
||||
@@ -6,10 +6,7 @@ import { IconCheck, IconInfoCircle, IconX } from "@tabler/icons-react";
|
||||
|
||||
import type { RouterInputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import {
|
||||
showErrorNotification,
|
||||
showSuccessNotification,
|
||||
} from "@homarr/notifications";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
interface UseTestConnectionDirtyProps {
|
||||
@@ -20,10 +17,7 @@ interface UseTestConnectionDirtyProps {
|
||||
};
|
||||
}
|
||||
|
||||
export const useTestConnectionDirty = ({
|
||||
defaultDirty,
|
||||
initialFormValue,
|
||||
}: UseTestConnectionDirtyProps) => {
|
||||
export const useTestConnectionDirty = ({ defaultDirty, initialFormValue }: UseTestConnectionDirtyProps) => {
|
||||
const [isDirty, setIsDirty] = useState(defaultDirty);
|
||||
const prevFormValueRef = useRef(initialFormValue);
|
||||
|
||||
@@ -36,10 +30,7 @@ export const useTestConnectionDirty = ({
|
||||
prevFormValueRef.current.url !== values.url ||
|
||||
!prevFormValueRef.current.secrets
|
||||
.map((secret) => secret.value)
|
||||
.every(
|
||||
(secretValue, index) =>
|
||||
values.secrets[index]?.value === secretValue,
|
||||
)
|
||||
.every((secretValue, index) => values.secrets[index]?.value === secretValue)
|
||||
) {
|
||||
setIsDirty(true);
|
||||
return;
|
||||
@@ -62,14 +53,9 @@ interface TestConnectionProps {
|
||||
integration: RouterInputs["integration"]["testConnection"] & { name: string };
|
||||
}
|
||||
|
||||
export const TestConnection = ({
|
||||
integration,
|
||||
removeDirty,
|
||||
isDirty,
|
||||
}: TestConnectionProps) => {
|
||||
export const TestConnection = ({ integration, removeDirty, isDirty }: TestConnectionProps) => {
|
||||
const t = useScopedI18n("integration.testConnection");
|
||||
const { mutateAsync, ...mutation } =
|
||||
clientApi.integration.testConnection.useMutation();
|
||||
const { mutateAsync, ...mutation } = clientApi.integration.testConnection.useMutation();
|
||||
|
||||
return (
|
||||
<Group>
|
||||
@@ -125,13 +111,7 @@ interface TestConnectionIconProps {
|
||||
size: number;
|
||||
}
|
||||
|
||||
const TestConnectionIcon = ({
|
||||
isDirty,
|
||||
isPending,
|
||||
isSuccess,
|
||||
isError,
|
||||
size,
|
||||
}: TestConnectionIconProps) => {
|
||||
const TestConnectionIcon = ({ isDirty, isPending, isSuccess, isError, size }: TestConnectionIconProps) => {
|
||||
if (isPending) return <Loader color="blue" size={size} />;
|
||||
if (isDirty) return null;
|
||||
if (isSuccess) return <IconCheck size={size} stroke={1.5} color="green" />;
|
||||
@@ -142,12 +122,7 @@ const TestConnectionIcon = ({
|
||||
export const TestConnectionNoticeAlert = () => {
|
||||
const t = useI18n();
|
||||
return (
|
||||
<Alert
|
||||
variant="light"
|
||||
color="yellow"
|
||||
title="Test Connection"
|
||||
icon={<IconInfoCircle />}
|
||||
>
|
||||
<Alert variant="light" color="yellow" title="Test Connection" icon={<IconInfoCircle />}>
|
||||
{t("integration.testConnection.alertNotice")}
|
||||
</Alert>
|
||||
);
|
||||
|
||||
@@ -6,16 +6,10 @@ import { Button, Fieldset, Group, Stack, TextInput } from "@mantine/core";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import {
|
||||
getAllSecretKindOptions,
|
||||
getDefaultSecretKinds,
|
||||
} from "@homarr/definitions";
|
||||
import { getAllSecretKindOptions, getDefaultSecretKinds } from "@homarr/definitions";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { useConfirmModal } from "@homarr/modals";
|
||||
import {
|
||||
showErrorNotification,
|
||||
showSuccessNotification,
|
||||
} from "@homarr/notifications";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import type { z } from "@homarr/validation";
|
||||
import { validation } from "@homarr/validation";
|
||||
@@ -23,11 +17,7 @@ import { validation } from "@homarr/validation";
|
||||
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||
import { SecretCard } from "../../_integration-secret-card";
|
||||
import { IntegrationSecretInput } from "../../_integration-secret-inputs";
|
||||
import {
|
||||
TestConnection,
|
||||
TestConnectionNoticeAlert,
|
||||
useTestConnectionDirty,
|
||||
} from "../../_integration-test-connection";
|
||||
import { TestConnection, TestConnectionNoticeAlert, useTestConnectionDirty } from "../../_integration-test-connection";
|
||||
|
||||
interface EditIntegrationForm {
|
||||
integration: RouterOutputs["integration"]["byId"];
|
||||
@@ -45,8 +35,7 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
|
||||
url: integration.url,
|
||||
secrets: secretsKinds.map((kind) => ({
|
||||
kind,
|
||||
value:
|
||||
integration.secrets.find((secret) => secret.kind === kind)?.value ?? "",
|
||||
value: integration.secrets.find((secret) => secret.kind === kind)?.value ?? "",
|
||||
})),
|
||||
};
|
||||
const { isDirty, onValuesChange, removeDirty } = useTestConnectionDirty({
|
||||
@@ -61,9 +50,7 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
|
||||
});
|
||||
const { mutateAsync, isPending } = clientApi.integration.update.useMutation();
|
||||
|
||||
const secretsMap = new Map(
|
||||
integration.secrets.map((secret) => [secret.kind, secret]),
|
||||
);
|
||||
const secretsMap = new Map(integration.secrets.map((secret) => [secret.kind, secret]));
|
||||
|
||||
const handleSubmitAsync = async (values: FormType) => {
|
||||
if (isDirty) return;
|
||||
@@ -82,9 +69,7 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
|
||||
title: t("integration.page.edit.notification.success.title"),
|
||||
message: t("integration.page.edit.notification.success.message"),
|
||||
});
|
||||
void revalidatePathActionAsync("/manage/integrations").then(() =>
|
||||
router.push("/manage/integrations"),
|
||||
);
|
||||
void revalidatePathActionAsync("/manage/integrations").then(() => router.push("/manage/integrations"));
|
||||
},
|
||||
onError: () => {
|
||||
showErrorNotification({
|
||||
@@ -101,17 +86,9 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
|
||||
<Stack>
|
||||
<TestConnectionNoticeAlert />
|
||||
|
||||
<TextInput
|
||||
withAsterisk
|
||||
label={t("integration.field.name.label")}
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
<TextInput withAsterisk label={t("integration.field.name.label")} {...form.getInputProps("name")} />
|
||||
|
||||
<TextInput
|
||||
withAsterisk
|
||||
label={t("integration.field.url.label")}
|
||||
{...form.getInputProps("url")}
|
||||
/>
|
||||
<TextInput withAsterisk label={t("integration.field.url.label")} {...form.getInputProps("url")} />
|
||||
|
||||
<Fieldset legend={t("integration.secrets.title")}>
|
||||
<Stack gap="sm">
|
||||
@@ -122,10 +99,7 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
|
||||
onCancel={() =>
|
||||
new Promise((res) => {
|
||||
// When nothing changed, just close the secret card
|
||||
if (
|
||||
(form.values.secrets[index]?.value ?? "") ===
|
||||
(secretsMap.get(kind)?.value ?? "")
|
||||
) {
|
||||
if ((form.values.secrets[index]?.value ?? "") === (secretsMap.get(kind)?.value ?? "")) {
|
||||
return res(true);
|
||||
}
|
||||
openConfirmModal({
|
||||
@@ -133,10 +107,7 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
|
||||
children: t("integration.secrets.reset.message"),
|
||||
onCancel: () => res(false),
|
||||
onConfirm: () => {
|
||||
form.setFieldValue(
|
||||
`secrets.${index}.value`,
|
||||
secretsMap.get(kind)!.value ?? "",
|
||||
);
|
||||
form.setFieldValue(`secrets.${index}.value`, secretsMap.get(kind)!.value ?? "");
|
||||
res(true);
|
||||
},
|
||||
});
|
||||
@@ -165,11 +136,7 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
|
||||
}}
|
||||
/>
|
||||
<Group>
|
||||
<Button
|
||||
variant="default"
|
||||
component={Link}
|
||||
href="/manage/integrations"
|
||||
>
|
||||
<Button variant="default" component={Link} href="/manage/integrations">
|
||||
{t("common.action.backToOverview")}
|
||||
</Button>
|
||||
<Button type="submit" loading={isPending} disabled={isDirty}>
|
||||
|
||||
@@ -11,9 +11,7 @@ interface EditIntegrationPageProps {
|
||||
params: { id: string };
|
||||
}
|
||||
|
||||
export default async function EditIntegrationPage({
|
||||
params,
|
||||
}: EditIntegrationPageProps) {
|
||||
export default async function EditIntegrationPage({ params }: EditIntegrationPageProps) {
|
||||
const t = await getScopedI18n("integration.page.edit");
|
||||
const integration = await api.integration.byId({ id: params.id });
|
||||
|
||||
@@ -22,9 +20,7 @@ export default async function EditIntegrationPage({
|
||||
<Stack>
|
||||
<Group align="center">
|
||||
<IntegrationAvatar kind={integration.kind} size="md" />
|
||||
<Title>
|
||||
{t("title", { name: getIntegrationName(integration.kind) })}
|
||||
</Title>
|
||||
<Title>{t("title", { name: getIntegrationName(integration.kind) })}</Title>
|
||||
</Group>
|
||||
<EditIntegrationForm integration={integration} />
|
||||
</Stack>
|
||||
|
||||
@@ -16,9 +16,7 @@ export const IntegrationCreateDropdownContent = () => {
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const filteredKinds = useMemo(() => {
|
||||
return integrationKinds.filter((kind) =>
|
||||
kind.includes(search.toLowerCase()),
|
||||
);
|
||||
return integrationKinds.filter((kind) => kind.includes(search.toLowerCase()));
|
||||
}, [search]);
|
||||
|
||||
const handleSearch = React.useCallback(
|
||||
@@ -38,11 +36,7 @@ export const IntegrationCreateDropdownContent = () => {
|
||||
{filteredKinds.length > 0 ? (
|
||||
<ScrollArea.Autosize mah={384}>
|
||||
{filteredKinds.map((kind) => (
|
||||
<Menu.Item
|
||||
component={Link}
|
||||
href={`/manage/integrations/new?kind=${kind}`}
|
||||
key={kind}
|
||||
>
|
||||
<Menu.Item component={Link} href={`/manage/integrations/new?kind=${kind}`} key={kind}>
|
||||
<Group>
|
||||
<IntegrationAvatar kind={kind} size="sm" />
|
||||
<Text size="sm">{getIntegrationName(kind)}</Text>
|
||||
|
||||
@@ -3,37 +3,20 @@
|
||||
import { useCallback } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
Button,
|
||||
Fieldset,
|
||||
Group,
|
||||
SegmentedControl,
|
||||
Stack,
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
import { Button, Fieldset, Group, SegmentedControl, Stack, TextInput } from "@mantine/core";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import type {
|
||||
IntegrationKind,
|
||||
IntegrationSecretKind,
|
||||
} from "@homarr/definitions";
|
||||
import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions";
|
||||
import { getAllSecretKindOptions } from "@homarr/definitions";
|
||||
import type { UseFormReturnType } from "@homarr/form";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import {
|
||||
showErrorNotification,
|
||||
showSuccessNotification,
|
||||
} from "@homarr/notifications";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
import type { z } from "@homarr/validation";
|
||||
import { validation } from "@homarr/validation";
|
||||
|
||||
import { IntegrationSecretInput } from "../_integration-secret-inputs";
|
||||
import {
|
||||
TestConnection,
|
||||
TestConnectionNoticeAlert,
|
||||
useTestConnectionDirty,
|
||||
} from "../_integration-test-connection";
|
||||
import { TestConnection, TestConnectionNoticeAlert, useTestConnectionDirty } from "../_integration-test-connection";
|
||||
import { revalidatePathActionAsync } from "../../../../revalidatePathAction";
|
||||
|
||||
interface NewIntegrationFormProps {
|
||||
@@ -42,9 +25,7 @@ interface NewIntegrationFormProps {
|
||||
};
|
||||
}
|
||||
|
||||
export const NewIntegrationForm = ({
|
||||
searchParams,
|
||||
}: NewIntegrationFormProps) => {
|
||||
export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) => {
|
||||
const t = useI18n();
|
||||
const secretKinds = getAllSecretKindOptions(searchParams.kind);
|
||||
const initialFormValues = {
|
||||
@@ -79,9 +60,7 @@ export const NewIntegrationForm = ({
|
||||
title: t("integration.page.create.notification.success.title"),
|
||||
message: t("integration.page.create.notification.success.message"),
|
||||
});
|
||||
void revalidatePathActionAsync("/manage/integrations").then(() =>
|
||||
router.push("/manage/integrations"),
|
||||
);
|
||||
void revalidatePathActionAsync("/manage/integrations").then(() => router.push("/manage/integrations"));
|
||||
},
|
||||
onError: () => {
|
||||
showErrorNotification({
|
||||
@@ -98,26 +77,13 @@ export const NewIntegrationForm = ({
|
||||
<Stack>
|
||||
<TestConnectionNoticeAlert />
|
||||
|
||||
<TextInput
|
||||
withAsterisk
|
||||
label={t("integration.field.name.label")}
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
<TextInput withAsterisk label={t("integration.field.name.label")} {...form.getInputProps("name")} />
|
||||
|
||||
<TextInput
|
||||
withAsterisk
|
||||
label={t("integration.field.url.label")}
|
||||
{...form.getInputProps("url")}
|
||||
/>
|
||||
<TextInput withAsterisk label={t("integration.field.url.label")} {...form.getInputProps("url")} />
|
||||
|
||||
<Fieldset legend={t("integration.secrets.title")}>
|
||||
<Stack gap="sm">
|
||||
{secretKinds.length > 1 && (
|
||||
<SecretKindsSegmentedControl
|
||||
secretKinds={secretKinds}
|
||||
form={form}
|
||||
/>
|
||||
)}
|
||||
{secretKinds.length > 1 && <SecretKindsSegmentedControl secretKinds={secretKinds} form={form} />}
|
||||
{form.values.secrets.map(({ kind }, index) => (
|
||||
<IntegrationSecretInput
|
||||
withAsterisk
|
||||
@@ -141,11 +107,7 @@ export const NewIntegrationForm = ({
|
||||
/>
|
||||
|
||||
<Group>
|
||||
<Button
|
||||
variant="default"
|
||||
component={Link}
|
||||
href="/manage/integrations"
|
||||
>
|
||||
<Button variant="default" component={Link} href="/manage/integrations">
|
||||
{t("common.action.backToOverview")}
|
||||
</Button>
|
||||
<Button type="submit" loading={isPending} disabled={isDirty}>
|
||||
@@ -163,10 +125,7 @@ interface SecretKindsSegmentedControlProps {
|
||||
form: UseFormReturnType<FormType, (values: FormType) => FormType>;
|
||||
}
|
||||
|
||||
const SecretKindsSegmentedControl = ({
|
||||
secretKinds,
|
||||
form,
|
||||
}: SecretKindsSegmentedControlProps) => {
|
||||
const SecretKindsSegmentedControl = ({ secretKinds, form }: SecretKindsSegmentedControlProps) => {
|
||||
const t = useScopedI18n("integration.secrets");
|
||||
|
||||
const secretKindGroups = secretKinds.map((kinds) => ({
|
||||
@@ -186,13 +145,7 @@ const SecretKindsSegmentedControl = ({
|
||||
[form],
|
||||
);
|
||||
|
||||
return (
|
||||
<SegmentedControl
|
||||
fullWidth
|
||||
data={secretKindGroups}
|
||||
onChange={onChange}
|
||||
></SegmentedControl>
|
||||
);
|
||||
return <SegmentedControl fullWidth data={secretKindGroups} onChange={onChange}></SegmentedControl>;
|
||||
};
|
||||
|
||||
type FormType = Omit<z.infer<typeof validation.integration.create>, "kind">;
|
||||
|
||||
@@ -16,12 +16,8 @@ interface NewIntegrationPageProps {
|
||||
};
|
||||
}
|
||||
|
||||
export default async function IntegrationsNewPage({
|
||||
searchParams,
|
||||
}: NewIntegrationPageProps) {
|
||||
const result = z
|
||||
.enum([integrationKinds[0]!, ...integrationKinds.slice(1)])
|
||||
.safeParse(searchParams.kind);
|
||||
export default async function IntegrationsNewPage({ searchParams }: NewIntegrationPageProps) {
|
||||
const result = z.enum([integrationKinds[0]!, ...integrationKinds.slice(1)]).safeParse(searchParams.kind);
|
||||
if (!result.success) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
@@ -43,9 +43,7 @@ interface IntegrationsPageProps {
|
||||
};
|
||||
}
|
||||
|
||||
export default async function IntegrationsPage({
|
||||
searchParams,
|
||||
}: IntegrationsPageProps) {
|
||||
export default async function IntegrationsPage({ searchParams }: IntegrationsPageProps) {
|
||||
const integrations = await api.integration.all();
|
||||
const t = await getScopedI18n("integration");
|
||||
|
||||
@@ -54,18 +52,9 @@ export default async function IntegrationsPage({
|
||||
<Stack>
|
||||
<Group justify="space-between" align="center">
|
||||
<Title>{t("page.list.title")}</Title>
|
||||
<Menu
|
||||
width={256}
|
||||
trapFocus
|
||||
position="bottom-start"
|
||||
withinPortal
|
||||
shadow="md"
|
||||
keepMounted={false}
|
||||
>
|
||||
<Menu width={256} trapFocus position="bottom-start" withinPortal shadow="md" keepMounted={false}>
|
||||
<MenuTarget>
|
||||
<Button rightSection={<IconChevronDown size={16} stroke={1.5} />}>
|
||||
{t("action.create")}
|
||||
</Button>
|
||||
<Button rightSection={<IconChevronDown size={16} stroke={1.5} />}>{t("action.create")}</Button>
|
||||
</MenuTarget>
|
||||
<MenuDropdown>
|
||||
<IntegrationCreateDropdownContent />
|
||||
@@ -73,10 +62,7 @@ export default async function IntegrationsPage({
|
||||
</Menu>
|
||||
</Group>
|
||||
|
||||
<IntegrationList
|
||||
integrations={integrations}
|
||||
activeTab={searchParams.tab}
|
||||
/>
|
||||
<IntegrationList integrations={integrations} activeTab={searchParams.tab} />
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
@@ -87,10 +73,7 @@ interface IntegrationListProps {
|
||||
activeTab?: IntegrationKind;
|
||||
}
|
||||
|
||||
const IntegrationList = async ({
|
||||
integrations,
|
||||
activeTab,
|
||||
}: IntegrationListProps) => {
|
||||
const IntegrationList = async ({ integrations, activeTab }: IntegrationListProps) => {
|
||||
const t = await getScopedI18n("integration");
|
||||
|
||||
if (integrations.length === 0) {
|
||||
@@ -134,12 +117,7 @@ const IntegrationList = async ({
|
||||
<TableTr key={integration.id}>
|
||||
<TableTd>{integration.name}</TableTd>
|
||||
<TableTd>
|
||||
<Anchor
|
||||
href={integration.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
size="sm"
|
||||
>
|
||||
<Anchor href={integration.url} target="_blank" rel="noreferrer" size="sm">
|
||||
{integration.url}
|
||||
</Anchor>
|
||||
</TableTd>
|
||||
@@ -155,10 +133,7 @@ const IntegrationList = async ({
|
||||
>
|
||||
<IconPencil size={16} stroke={1.5} />
|
||||
</ActionIcon>
|
||||
<DeleteIntegrationActionButton
|
||||
integration={integration}
|
||||
count={integrations.length}
|
||||
/>
|
||||
<DeleteIntegrationActionButton integration={integration} count={integrations.length} />
|
||||
</ActionIconGroup>
|
||||
</Group>
|
||||
</TableTd>
|
||||
|
||||
@@ -71,12 +71,7 @@ export default async function ManagementPage() {
|
||||
<Space h="md" />
|
||||
<SimpleGrid cols={{ xs: 1, sm: 2, md: 3 }}>
|
||||
{links.map((link, index) => (
|
||||
<Card
|
||||
component={Link}
|
||||
href={link.href}
|
||||
key={`link-${index}`}
|
||||
withBorder
|
||||
>
|
||||
<Card component={Link} href={link.href} key={`link-${index}`} withBorder>
|
||||
<Group justify="space-between">
|
||||
<Group>
|
||||
<Text size="2.4rem" fw="bolder">
|
||||
|
||||
@@ -3,16 +3,7 @@
|
||||
import type { ReactNode } from "react";
|
||||
import React from "react";
|
||||
import type { MantineSpacing } from "@mantine/core";
|
||||
import {
|
||||
Card,
|
||||
Group,
|
||||
LoadingOverlay,
|
||||
Stack,
|
||||
Switch,
|
||||
Text,
|
||||
Title,
|
||||
UnstyledButton,
|
||||
} from "@mantine/core";
|
||||
import { Card, Group, LoadingOverlay, Stack, Switch, Text, Title, UnstyledButton } from "@mantine/core";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import type { UseFormReturnType } from "@homarr/form";
|
||||
@@ -37,9 +28,7 @@ export const AnalyticsSettings = ({ initialData }: AnalyticsSettingsProps) => {
|
||||
|
||||
if (
|
||||
!updatedValues.enableGeneral &&
|
||||
(updatedValues.enableWidgetData ||
|
||||
updatedValues.enableIntegrationData ||
|
||||
updatedValues.enableUserData)
|
||||
(updatedValues.enableWidgetData || updatedValues.enableIntegrationData || updatedValues.enableUserData)
|
||||
) {
|
||||
updatedValues.enableIntegrationData = false;
|
||||
updatedValues.enableUserData = false;
|
||||
@@ -53,30 +42,20 @@ export const AnalyticsSettings = ({ initialData }: AnalyticsSettingsProps) => {
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync, isPending } =
|
||||
clientApi.serverSettings.saveSettings.useMutation({
|
||||
onSettled: async () => {
|
||||
await revalidatePathActionAsync("/manage/settings");
|
||||
},
|
||||
});
|
||||
const { mutateAsync, isPending } = clientApi.serverSettings.saveSettings.useMutation({
|
||||
onSettled: async () => {
|
||||
await revalidatePathActionAsync("/manage/settings");
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title order={2}>{t("title")}</Title>
|
||||
|
||||
<Card pos="relative" withBorder>
|
||||
<LoadingOverlay
|
||||
visible={isPending}
|
||||
zIndex={1000}
|
||||
overlayProps={{ radius: "sm", blur: 2 }}
|
||||
/>
|
||||
<LoadingOverlay visible={isPending} zIndex={1000} overlayProps={{ radius: "sm", blur: 2 }} />
|
||||
<Stack>
|
||||
<SwitchSetting
|
||||
form={form}
|
||||
formKey="enableGeneral"
|
||||
title={t("general.title")}
|
||||
text={t("general.text")}
|
||||
/>
|
||||
<SwitchSetting form={form} formKey="enableGeneral" title={t("general.title")} text={t("general.text")} />
|
||||
<SwitchSetting
|
||||
form={form}
|
||||
formKey="enableIntegrationData"
|
||||
@@ -122,13 +101,7 @@ const SwitchSetting = ({
|
||||
}, [form, formKey]);
|
||||
return (
|
||||
<UnstyledButton onClick={handleClick}>
|
||||
<Group
|
||||
ms={ms}
|
||||
justify="space-between"
|
||||
gap="lg"
|
||||
align="center"
|
||||
wrap="nowrap"
|
||||
>
|
||||
<Group ms={ms} justify="space-between" gap="lg" align="center" wrap="nowrap">
|
||||
<Stack gap={0}>
|
||||
<Text fw="bold">{title}</Text>
|
||||
<Text c="gray.5">{text}</Text>
|
||||
|
||||
@@ -23,12 +23,7 @@ const ClientSideTerminalComponent = dynamic(() => import("./terminal"), {
|
||||
|
||||
export default function LogsManagementPage() {
|
||||
return (
|
||||
<Box
|
||||
style={{ borderRadius: 6 }}
|
||||
h={fullHeightWithoutHeaderAndFooter}
|
||||
p="md"
|
||||
bg="black"
|
||||
>
|
||||
<Box style={{ borderRadius: 6 }} h={fullHeightWithoutHeaderAndFooter} p="md" bg="black">
|
||||
<ClientSideTerminalComponent />
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -16,9 +16,7 @@ export default function TerminalComponent() {
|
||||
const terminalRef = useRef<Terminal>();
|
||||
clientApi.log.subscribe.useSubscription(undefined, {
|
||||
onData(data) {
|
||||
terminalRef.current?.writeln(
|
||||
`${data.timestamp} ${data.level} ${data.message}`,
|
||||
);
|
||||
terminalRef.current?.writeln(`${data.timestamp} ${data.level} ${data.message}`);
|
||||
terminalRef.current?.refresh(0, terminalRef.current.rows - 1);
|
||||
},
|
||||
onError(err) {
|
||||
@@ -55,12 +53,5 @@ export default function TerminalComponent() {
|
||||
canvasAddon.dispose();
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
id="terminal"
|
||||
className={classes.outerTerminal}
|
||||
h="100%"
|
||||
></Box>
|
||||
);
|
||||
return <Box ref={ref} id="terminal" className={classes.outerTerminal} h="100%"></Box>;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import type { Session } from "@homarr/auth";
|
||||
|
||||
export const canAccessUserEditPage = (
|
||||
session: Session | null,
|
||||
userId: string,
|
||||
) => {
|
||||
export const canAccessUserEditPage = (session: Session | null, userId: string) => {
|
||||
if (!session) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -18,14 +18,11 @@ interface DeleteUserButtonProps {
|
||||
export const DeleteUserButton = ({ user }: DeleteUserButtonProps) => {
|
||||
const t = useI18n();
|
||||
const router = useRouter();
|
||||
const { mutateAsync: mutateUserDeletionAsync } =
|
||||
clientApi.user.delete.useMutation({
|
||||
async onSuccess() {
|
||||
await revalidatePathActionAsync("/manage/users").then(() =>
|
||||
router.push("/manage/users"),
|
||||
);
|
||||
},
|
||||
});
|
||||
const { mutateAsync: mutateUserDeletionAsync } = clientApi.user.delete.useMutation({
|
||||
async onSuccess() {
|
||||
await revalidatePathActionAsync("/manage/users").then(() => router.push("/manage/users"));
|
||||
},
|
||||
});
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
|
||||
const handleDelete = useCallback(
|
||||
|
||||
@@ -8,10 +8,7 @@ import { IconPencil, IconPhotoEdit, IconPhotoX } from "@tabler/icons-react";
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useConfirmModal } from "@homarr/modals";
|
||||
import {
|
||||
showErrorNotification,
|
||||
showSuccessNotification,
|
||||
} from "@homarr/notifications";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
import { UserAvatar } from "@homarr/ui";
|
||||
|
||||
@@ -46,25 +43,18 @@ export const UserProfileAvatarForm = ({ user }: UserProfileAvatarForm) => {
|
||||
// Revalidate all as the avatar is used in multiple places
|
||||
await revalidatePathActionAsync("/");
|
||||
showSuccessNotification({
|
||||
message: tManageAvatar(
|
||||
"changeImage.notification.success.message",
|
||||
),
|
||||
message: tManageAvatar("changeImage.notification.success.message"),
|
||||
});
|
||||
},
|
||||
onError(error) {
|
||||
if (error.shape?.data.code === "BAD_REQUEST") {
|
||||
showErrorNotification({
|
||||
title: tManageAvatar("changeImage.notification.toLarge.title"),
|
||||
message: tManageAvatar(
|
||||
"changeImage.notification.toLarge.message",
|
||||
{ size: "256KB" },
|
||||
),
|
||||
message: tManageAvatar("changeImage.notification.toLarge.message", { size: "256KB" }),
|
||||
});
|
||||
} else {
|
||||
showErrorNotification({
|
||||
message: tManageAvatar(
|
||||
"changeImage.notification.error.message",
|
||||
),
|
||||
message: tManageAvatar("changeImage.notification.error.message"),
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -89,16 +79,12 @@ export const UserProfileAvatarForm = ({ user }: UserProfileAvatarForm) => {
|
||||
// Revalidate all as the avatar is used in multiple places
|
||||
await revalidatePathActionAsync("/");
|
||||
showSuccessNotification({
|
||||
message: tManageAvatar(
|
||||
"removeImage.notification.success.message",
|
||||
),
|
||||
message: tManageAvatar("removeImage.notification.success.message"),
|
||||
});
|
||||
},
|
||||
onError() {
|
||||
showErrorNotification({
|
||||
message: tManageAvatar(
|
||||
"removeImage.notification.error.message",
|
||||
),
|
||||
message: tManageAvatar("removeImage.notification.error.message"),
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -109,13 +95,7 @@ export const UserProfileAvatarForm = ({ user }: UserProfileAvatarForm) => {
|
||||
|
||||
return (
|
||||
<Box pos="relative">
|
||||
<Menu
|
||||
opened={opened}
|
||||
keepMounted
|
||||
onChange={toggle}
|
||||
position="bottom-start"
|
||||
withArrow
|
||||
>
|
||||
<Menu opened={opened} keepMounted onChange={toggle} position="bottom-start" withArrow>
|
||||
<Menu.Target>
|
||||
<UnstyledButton onClick={toggle}>
|
||||
<UserAvatar user={user} size={200} />
|
||||
@@ -134,24 +114,15 @@ export const UserProfileAvatarForm = ({ user }: UserProfileAvatarForm) => {
|
||||
</UnstyledButton>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<FileButton
|
||||
onChange={handleAvatarChange}
|
||||
accept="image/png,image/jpeg,image/webp,image/gif"
|
||||
>
|
||||
<FileButton onChange={handleAvatarChange} accept="image/png,image/jpeg,image/webp,image/gif">
|
||||
{(props) => (
|
||||
<Menu.Item
|
||||
{...props}
|
||||
leftSection={<IconPhotoEdit size={16} stroke={1.5} />}
|
||||
>
|
||||
<Menu.Item {...props} leftSection={<IconPhotoEdit size={16} stroke={1.5} />}>
|
||||
{tManageAvatar("changeImage.label")}
|
||||
</Menu.Item>
|
||||
)}
|
||||
</FileButton>
|
||||
{user.image && (
|
||||
<Menu.Item
|
||||
onClick={handleRemoveAvatar}
|
||||
leftSection={<IconPhotoX size={16} stroke={1.5} />}
|
||||
>
|
||||
<Menu.Item onClick={handleRemoveAvatar} leftSection={<IconPhotoX size={16} stroke={1.5} />}>
|
||||
{tManageAvatar("removeImage.label")}
|
||||
</Menu.Item>
|
||||
)}
|
||||
|
||||
@@ -6,10 +6,7 @@ import { Button, Group, Stack, TextInput } from "@mantine/core";
|
||||
import type { RouterInputs, RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import {
|
||||
showErrorNotification,
|
||||
showSuccessNotification,
|
||||
} from "@homarr/notifications";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { validation } from "@homarr/validation";
|
||||
|
||||
@@ -58,15 +55,8 @@ export const UserProfileForm = ({ user }: UserProfileFormProps) => {
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack>
|
||||
<TextInput
|
||||
label={t("user.field.username.label")}
|
||||
withAsterisk
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
<TextInput
|
||||
label={t("user.field.email.label")}
|
||||
{...form.getInputProps("email")}
|
||||
/>
|
||||
<TextInput label={t("user.field.username.label")} withAsterisk {...form.getInputProps("name")} />
|
||||
<TextInput label={t("user.field.email.label")} {...form.getInputProps("email")} />
|
||||
|
||||
<Group justify="end">
|
||||
<Button type="submit" color="teal" loading={isPending}>
|
||||
|
||||
@@ -5,10 +5,7 @@ import { api } from "@homarr/api/server";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { getI18n, getScopedI18n } from "@homarr/translation/server";
|
||||
|
||||
import {
|
||||
DangerZoneItem,
|
||||
DangerZoneRoot,
|
||||
} from "~/components/manage/danger-zone";
|
||||
import { DangerZoneItem, DangerZoneRoot } from "~/components/manage/danger-zone";
|
||||
import { catchTrpcNotFound } from "~/errors/trpc-not-found";
|
||||
import { createMetaTitle } from "~/metadata";
|
||||
import { canAccessUserEditPage } from "../access";
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
import type { PropsWithChildren } from "react";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import {
|
||||
Button,
|
||||
Container,
|
||||
Grid,
|
||||
GridCol,
|
||||
Group,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { Button, Container, Grid, GridCol, Group, Stack, Text, Title } from "@mantine/core";
|
||||
import { IconSettings, IconShieldLock } from "@tabler/icons-react";
|
||||
|
||||
import { api } from "@homarr/api/server";
|
||||
@@ -26,16 +17,11 @@ interface LayoutProps {
|
||||
params: { userId: string };
|
||||
}
|
||||
|
||||
export default async function Layout({
|
||||
children,
|
||||
params,
|
||||
}: PropsWithChildren<LayoutProps>) {
|
||||
export default async function Layout({ children, params }: PropsWithChildren<LayoutProps>) {
|
||||
const session = await auth();
|
||||
const t = await getI18n();
|
||||
const tUser = await getScopedI18n("management.page.user");
|
||||
const user = await api.user
|
||||
.getById({ userId: params.userId })
|
||||
.catch(catchTrpcNotFound);
|
||||
const user = await api.user.getById({ userId: params.userId }).catch(catchTrpcNotFound);
|
||||
|
||||
if (!canAccessUserEditPage(session, user.id)) {
|
||||
notFound();
|
||||
@@ -54,12 +40,7 @@ export default async function Layout({
|
||||
</Stack>
|
||||
</Group>
|
||||
{session?.user.permissions.includes("admin") && (
|
||||
<Button
|
||||
component={Link}
|
||||
href="/manage/users"
|
||||
color="gray"
|
||||
variant="light"
|
||||
>
|
||||
<Button component={Link} href="/manage/users" color="gray" variant="light">
|
||||
{tUser("back")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -6,10 +6,7 @@ import type { RouterInputs, RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useSession } from "@homarr/auth/client";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import {
|
||||
showErrorNotification,
|
||||
showSuccessNotification,
|
||||
} from "@homarr/notifications";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { validation } from "@homarr/validation";
|
||||
|
||||
@@ -74,11 +71,7 @@ export const ChangePasswordForm = ({ user }: ChangePasswordFormProps) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
<PasswordInput
|
||||
withAsterisk
|
||||
label={t("user.field.password.label")}
|
||||
{...form.getInputProps("password")}
|
||||
/>
|
||||
<PasswordInput withAsterisk label={t("user.field.password.label")} {...form.getInputProps("password")} />
|
||||
|
||||
<PasswordInput
|
||||
withAsterisk
|
||||
|
||||
@@ -17,9 +17,7 @@ interface Props {
|
||||
|
||||
export default async function UserSecurityPage({ params }: Props) {
|
||||
const session = await auth();
|
||||
const tSecurity = await getScopedI18n(
|
||||
"management.page.user.setting.security",
|
||||
);
|
||||
const tSecurity = await getScopedI18n("management.page.user.setting.security");
|
||||
const user = await api.user
|
||||
.getById({
|
||||
userId: params.userId,
|
||||
|
||||
@@ -15,18 +15,14 @@ interface UserListComponentProps {
|
||||
initialUserList: RouterOutputs["user"]["getAll"];
|
||||
}
|
||||
|
||||
export const UserListComponent = ({
|
||||
initialUserList,
|
||||
}: UserListComponentProps) => {
|
||||
export const UserListComponent = ({ initialUserList }: UserListComponentProps) => {
|
||||
const tUserList = useScopedI18n("management.page.user.list");
|
||||
const t = useI18n();
|
||||
const { data, isLoading } = clientApi.user.getAll.useQuery(undefined, {
|
||||
initialData: initialUserList,
|
||||
});
|
||||
|
||||
const columns = useMemo<
|
||||
MRT_ColumnDef<RouterOutputs["user"]["getAll"][number]>[]
|
||||
>(
|
||||
const columns = useMemo<MRT_ColumnDef<RouterOutputs["user"]["getAll"][number]>[]>(
|
||||
() => [
|
||||
{
|
||||
accessorKey: "name",
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import {
|
||||
Avatar,
|
||||
Card,
|
||||
PasswordInput,
|
||||
Stack,
|
||||
Stepper,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { Avatar, Card, PasswordInput, Stack, Stepper, Text, TextInput, Title } from "@mantine/core";
|
||||
import { IconUserCheck } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
@@ -28,14 +19,10 @@ export const UserCreateStepperComponent = () => {
|
||||
const stepperMax = 4;
|
||||
const [active, setActive] = useState(0);
|
||||
const nextStep = useCallback(
|
||||
() =>
|
||||
setActive((current) => (current < stepperMax ? current + 1 : current)),
|
||||
[setActive],
|
||||
);
|
||||
const prevStep = useCallback(
|
||||
() => setActive((current) => (current > 0 ? current - 1 : current)),
|
||||
() => setActive((current) => (current < stepperMax ? current + 1 : current)),
|
||||
[setActive],
|
||||
);
|
||||
const prevStep = useCallback(() => setActive((current) => (current > 0 ? current - 1 : current)), [setActive]);
|
||||
const hasNext = active < stepperMax;
|
||||
const hasPrevious = active > 0;
|
||||
|
||||
@@ -72,14 +59,9 @@ export const UserCreateStepperComponent = () => {
|
||||
},
|
||||
);
|
||||
|
||||
const allForms = useMemo(
|
||||
() => [generalForm, securityForm],
|
||||
[generalForm, securityForm],
|
||||
);
|
||||
const allForms = useMemo(() => [generalForm, securityForm], [generalForm, securityForm]);
|
||||
|
||||
const isCurrentFormValid = allForms[active]
|
||||
? (allForms[active]!.isValid satisfies () => boolean)
|
||||
: () => true;
|
||||
const isCurrentFormValid = allForms[active] ? (allForms[active]!.isValid satisfies () => boolean) : () => true;
|
||||
const canNavigateToNextStep = isCurrentFormValid();
|
||||
|
||||
const controlledGoToNextStep = useCallback(async () => {
|
||||
@@ -104,12 +86,7 @@ export const UserCreateStepperComponent = () => {
|
||||
return (
|
||||
<>
|
||||
<Title mb="md">{t("title")}</Title>
|
||||
<Stepper
|
||||
active={active}
|
||||
onStepClick={setActive}
|
||||
allowNextStepsSelect={false}
|
||||
mb="md"
|
||||
>
|
||||
<Stepper active={active} onStepClick={setActive} allowNextStepsSelect={false} mb="md">
|
||||
<Stepper.Step
|
||||
label={t("step.personalInformation.label")}
|
||||
allowStepSelect={false}
|
||||
@@ -126,20 +103,12 @@ export const UserCreateStepperComponent = () => {
|
||||
{...generalForm.getInputProps("username")}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label={tUserField("email.label")}
|
||||
variant="filled"
|
||||
{...generalForm.getInputProps("email")}
|
||||
/>
|
||||
<TextInput label={tUserField("email.label")} variant="filled" {...generalForm.getInputProps("email")} />
|
||||
</Stack>
|
||||
</Card>
|
||||
</form>
|
||||
</Stepper.Step>
|
||||
<Stepper.Step
|
||||
label={t("step.security.label")}
|
||||
allowStepSelect={false}
|
||||
allowStepClick={false}
|
||||
>
|
||||
<Stepper.Step label={t("step.security.label")} allowStepSelect={false} allowStepClick={false}>
|
||||
<form>
|
||||
<Card p="xl">
|
||||
<Stack gap="md">
|
||||
@@ -167,11 +136,7 @@ export const UserCreateStepperComponent = () => {
|
||||
>
|
||||
3
|
||||
</Stepper.Step>
|
||||
<Stepper.Step
|
||||
label={t("step.review.label")}
|
||||
allowStepSelect={false}
|
||||
allowStepClick={false}
|
||||
>
|
||||
<Stepper.Step label={t("step.review.label")} allowStepSelect={false} allowStepClick={false}>
|
||||
<Card p="xl">
|
||||
<Stack maw={300} align="center" mx="auto">
|
||||
<Avatar size="xl">{generalForm.values.username}</Avatar>
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import Link from "next/link";
|
||||
import { Button, Card, Group } from "@mantine/core";
|
||||
import {
|
||||
IconArrowBackUp,
|
||||
IconArrowLeft,
|
||||
IconArrowRight,
|
||||
IconRotate,
|
||||
} from "@tabler/icons-react";
|
||||
import { IconArrowBackUp, IconArrowLeft, IconArrowRight, IconRotate } from "@tabler/icons-react";
|
||||
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
@@ -51,18 +46,10 @@ export const StepperNavigationComponent = ({
|
||||
</Group>
|
||||
) : (
|
||||
<Group justify="end" wrap="nowrap">
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<IconRotate size="1rem" />}
|
||||
onClick={reset}
|
||||
>
|
||||
<Button variant="light" leftSection={<IconRotate size="1rem" />} onClick={reset}>
|
||||
{t("management.page.user.create.action.createAnother")}
|
||||
</Button>
|
||||
<Button
|
||||
leftSection={<IconArrowBackUp size="1rem" />}
|
||||
component={Link}
|
||||
href="/manage/users"
|
||||
>
|
||||
<Button leftSection={<IconArrowBackUp size="1rem" />} component={Link} href="/manage/users">
|
||||
{t("management.page.user.create.action.back")}
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
@@ -6,10 +6,7 @@ import { Button } from "@mantine/core";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useConfirmModal } from "@homarr/modals";
|
||||
import {
|
||||
showErrorNotification,
|
||||
showSuccessNotification,
|
||||
} from "@homarr/notifications";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||
@@ -63,15 +60,7 @@ export const DeleteGroup = ({ group }: DeleteGroupProps) => {
|
||||
);
|
||||
},
|
||||
});
|
||||
}, [
|
||||
tDelete,
|
||||
tRoot,
|
||||
openConfirmModal,
|
||||
group.id,
|
||||
group.name,
|
||||
mutateAsync,
|
||||
router,
|
||||
]);
|
||||
}, [tDelete, tRoot, openConfirmModal, group.id, group.name, mutateAsync, router]);
|
||||
|
||||
return (
|
||||
<Button variant="subtle" color="red" onClick={handleDeletion}>
|
||||
|
||||
@@ -14,13 +14,5 @@ interface NavigationLinkProps {
|
||||
export const NavigationLink = ({ href, icon, label }: NavigationLinkProps) => {
|
||||
const pathName = usePathname();
|
||||
|
||||
return (
|
||||
<NavLink
|
||||
component={Link}
|
||||
href={href}
|
||||
active={pathName === href}
|
||||
label={label}
|
||||
leftSection={icon}
|
||||
/>
|
||||
);
|
||||
return <NavLink component={Link} href={href} active={pathName === href} label={label} leftSection={icon} />;
|
||||
};
|
||||
|
||||
@@ -5,10 +5,7 @@ import { Button, Group, Stack, TextInput } from "@mantine/core";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import {
|
||||
showErrorNotification,
|
||||
showSuccessNotification,
|
||||
} from "@homarr/notifications";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { validation } from "@homarr/validation";
|
||||
|
||||
@@ -64,10 +61,7 @@ export const RenameGroupForm = ({ group }: RenameGroupFormProps) => {
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack>
|
||||
<TextInput
|
||||
label={t("group.field.name")}
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
<TextInput label={t("group.field.name")} {...form.getInputProps("name")} />
|
||||
|
||||
<Group justify="end">
|
||||
<Button type="submit" color="teal" loading={isPending}>
|
||||
|
||||
@@ -5,10 +5,7 @@ import { Button } from "@mantine/core";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useConfirmModal, useModalAction } from "@homarr/modals";
|
||||
import {
|
||||
showErrorNotification,
|
||||
showSuccessNotification,
|
||||
} from "@homarr/notifications";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { UserSelectModal } from "~/app/[locale]/boards/[name]/settings/_access/user-select-modal";
|
||||
@@ -21,9 +18,7 @@ interface TransferGroupOwnershipProps {
|
||||
};
|
||||
}
|
||||
|
||||
export const TransferGroupOwnership = ({
|
||||
group,
|
||||
}: TransferGroupOwnershipProps) => {
|
||||
export const TransferGroupOwnership = ({ group }: TransferGroupOwnershipProps) => {
|
||||
const tTransfer = useScopedI18n("group.action.transfer");
|
||||
const tRoot = useI18n();
|
||||
const [innerOwnerId, setInnerOwnerId] = useState(group.ownerId);
|
||||
@@ -77,16 +72,7 @@ export const TransferGroupOwnership = ({
|
||||
title: tTransfer("label"),
|
||||
},
|
||||
);
|
||||
}, [
|
||||
group.id,
|
||||
group.name,
|
||||
innerOwnerId,
|
||||
mutateAsync,
|
||||
openConfirmModal,
|
||||
openModal,
|
||||
tRoot,
|
||||
tTransfer,
|
||||
]);
|
||||
}, [group.id, group.name, innerOwnerId, mutateAsync, openConfirmModal, openModal, tRoot, tTransfer]);
|
||||
|
||||
return (
|
||||
<Button variant="subtle" color="red" onClick={handleTransfer}>
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
import type { PropsWithChildren } from "react";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
Button,
|
||||
Container,
|
||||
Grid,
|
||||
GridCol,
|
||||
Group,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { Button, Container, Grid, GridCol, Group, Stack, Text, Title } from "@mantine/core";
|
||||
import { IconLock, IconSettings, IconUsersGroup } from "@tabler/icons-react";
|
||||
|
||||
import { api } from "@homarr/api/server";
|
||||
@@ -21,10 +12,7 @@ interface LayoutProps {
|
||||
params: { id: string };
|
||||
}
|
||||
|
||||
export default async function Layout({
|
||||
children,
|
||||
params,
|
||||
}: PropsWithChildren<LayoutProps>) {
|
||||
export default async function Layout({ children, params }: PropsWithChildren<LayoutProps>) {
|
||||
const t = await getI18n();
|
||||
const tGroup = await getScopedI18n("management.page.group");
|
||||
const group = await api.group.getById({ id: params.id });
|
||||
@@ -38,12 +26,7 @@ export default async function Layout({
|
||||
<Title order={3}>{group.name}</Title>
|
||||
<Text c="gray.5">{t("group.name")}</Text>
|
||||
</Stack>
|
||||
<Button
|
||||
component={Link}
|
||||
href="/manage/users/groups"
|
||||
color="gray"
|
||||
variant="light"
|
||||
>
|
||||
<Button component={Link} href="/manage/users/groups" color="gray" variant="light">
|
||||
{tGroup("back")}
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
@@ -15,10 +15,7 @@ interface AddGroupMemberProps {
|
||||
presentUserIds: string[];
|
||||
}
|
||||
|
||||
export const AddGroupMember = ({
|
||||
groupId,
|
||||
presentUserIds,
|
||||
}: AddGroupMemberProps) => {
|
||||
export const AddGroupMember = ({ groupId, presentUserIds }: AddGroupMemberProps) => {
|
||||
const tMembersAdd = useScopedI18n("group.action.addMember");
|
||||
const { mutateAsync } = clientApi.group.addMember.useMutation();
|
||||
const { openModal } = useModalAction(UserSelectModal);
|
||||
@@ -32,9 +29,7 @@ export const AddGroupMember = ({
|
||||
userId: id,
|
||||
groupId,
|
||||
});
|
||||
await revalidatePathActionAsync(
|
||||
`/manage/users/groups/${groupId}}/members`,
|
||||
);
|
||||
await revalidatePathActionAsync(`/manage/users/groups/${groupId}}/members`);
|
||||
},
|
||||
presentUserIds,
|
||||
},
|
||||
|
||||
@@ -14,10 +14,7 @@ interface RemoveGroupMemberProps {
|
||||
user: { id: string; name: string | null };
|
||||
}
|
||||
|
||||
export const RemoveGroupMember = ({
|
||||
groupId,
|
||||
user,
|
||||
}: RemoveGroupMemberProps) => {
|
||||
export const RemoveGroupMember = ({ groupId, user }: RemoveGroupMemberProps) => {
|
||||
const t = useI18n();
|
||||
const tRemoveMember = useScopedI18n("group.action.removeMember");
|
||||
const { mutateAsync } = clientApi.group.removeMember.useMutation();
|
||||
@@ -35,27 +32,13 @@ export const RemoveGroupMember = ({
|
||||
groupId,
|
||||
userId: user.id,
|
||||
});
|
||||
await revalidatePathActionAsync(
|
||||
`/manage/users/groups/${groupId}/members`,
|
||||
);
|
||||
await revalidatePathActionAsync(`/manage/users/groups/${groupId}/members`);
|
||||
},
|
||||
});
|
||||
}, [
|
||||
openConfirmModal,
|
||||
mutateAsync,
|
||||
groupId,
|
||||
user.id,
|
||||
user.name,
|
||||
tRemoveMember,
|
||||
]);
|
||||
}, [openConfirmModal, mutateAsync, groupId, user.id, user.name, tRemoveMember]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="red.9"
|
||||
size="compact-sm"
|
||||
onClick={handleRemove}
|
||||
>
|
||||
<Button variant="subtle" color="red.9" size="compact-sm" onClick={handleRemove}>
|
||||
{t("common.action.remove")}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -1,16 +1,5 @@
|
||||
import Link from "next/link";
|
||||
import {
|
||||
Anchor,
|
||||
Center,
|
||||
Group,
|
||||
Stack,
|
||||
Table,
|
||||
TableTbody,
|
||||
TableTd,
|
||||
TableTr,
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { Anchor, Center, Group, Stack, Table, TableTbody, TableTd, TableTr, Text, Title } from "@mantine/core";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { api } from "@homarr/api/server";
|
||||
@@ -29,20 +18,13 @@ interface GroupsDetailPageProps {
|
||||
};
|
||||
}
|
||||
|
||||
export default async function GroupsDetailPage({
|
||||
params,
|
||||
searchParams,
|
||||
}: GroupsDetailPageProps) {
|
||||
export default async function GroupsDetailPage({ params, searchParams }: GroupsDetailPageProps) {
|
||||
const t = await getI18n();
|
||||
const tMembers = await getScopedI18n("management.page.group.setting.members");
|
||||
const group = await api.group.getById({ id: params.id });
|
||||
|
||||
const filteredMembers = searchParams.search
|
||||
? group.members.filter((member) =>
|
||||
member.name
|
||||
?.toLowerCase()
|
||||
.includes(searchParams.search!.trim().toLowerCase()),
|
||||
)
|
||||
? group.members.filter((member) => member.name?.toLowerCase().includes(searchParams.search!.trim().toLowerCase()))
|
||||
: group.members;
|
||||
|
||||
return (
|
||||
@@ -56,10 +38,7 @@ export default async function GroupsDetailPage({
|
||||
})}
|
||||
defaultValue={searchParams.search}
|
||||
/>
|
||||
<AddGroupMember
|
||||
groupId={group.id}
|
||||
presentUserIds={group.members.map((member) => member.id)}
|
||||
/>
|
||||
<AddGroupMember groupId={group.id} presentUserIds={group.members.map((member) => member.id)} />
|
||||
</Group>
|
||||
{filteredMembers.length === 0 && (
|
||||
<Center py="sm">
|
||||
|
||||
@@ -3,10 +3,7 @@ import { Stack, Title } from "@mantine/core";
|
||||
import { api } from "@homarr/api/server";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
|
||||
import {
|
||||
DangerZoneItem,
|
||||
DangerZoneRoot,
|
||||
} from "~/components/manage/danger-zone";
|
||||
import { DangerZoneItem, DangerZoneRoot } from "~/components/manage/danger-zone";
|
||||
import { DeleteGroup } from "./_delete-group";
|
||||
import { RenameGroupForm } from "./_rename-group-form";
|
||||
import { TransferGroupOwnership } from "./_transfer-group-ownership";
|
||||
@@ -17,9 +14,7 @@ interface GroupsDetailPageProps {
|
||||
};
|
||||
}
|
||||
|
||||
export default async function GroupsDetailPage({
|
||||
params,
|
||||
}: GroupsDetailPageProps) {
|
||||
export default async function GroupsDetailPage({ params }: GroupsDetailPageProps) {
|
||||
const group = await api.group.getById({ id: params.id });
|
||||
const tGeneral = await getScopedI18n("management.page.group.setting.general");
|
||||
const tGroupAction = await getScopedI18n("group.action");
|
||||
|
||||
@@ -9,10 +9,7 @@ import { objectEntries } from "@homarr/common";
|
||||
import type { GroupPermissionKey } from "@homarr/definitions";
|
||||
import { groupPermissionKeys } from "@homarr/definitions";
|
||||
import { createFormContext } from "@homarr/form";
|
||||
import {
|
||||
showErrorNotification,
|
||||
showSuccessNotification,
|
||||
} from "@homarr/notifications";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
const [FormProvider, useFormContext, useForm] = createFormContext<FormType>();
|
||||
@@ -21,10 +18,7 @@ interface PermissionFormProps {
|
||||
initialPermissions: GroupPermissionKey[];
|
||||
}
|
||||
|
||||
export const PermissionForm = ({
|
||||
children,
|
||||
initialPermissions,
|
||||
}: PropsWithChildren<PermissionFormProps>) => {
|
||||
export const PermissionForm = ({ children, initialPermissions }: PropsWithChildren<PermissionFormProps>) => {
|
||||
const form = useForm({
|
||||
initialValues: groupPermissionKeys.reduce((acc, key) => {
|
||||
acc[key] = initialPermissions.includes(key);
|
||||
@@ -73,9 +67,7 @@ interface SaveAffixProps {
|
||||
export const SaveAffix = ({ groupId }: SaveAffixProps) => {
|
||||
const t = useI18n();
|
||||
const tForm = useScopedI18n("management.page.group.setting.permissions.form");
|
||||
const tNotification = useScopedI18n(
|
||||
"group.action.changePermissions.notification",
|
||||
);
|
||||
const tNotification = useScopedI18n("group.action.changePermissions.notification");
|
||||
const form = useFormContext();
|
||||
const { mutate, isPending } = clientApi.group.savePermissions.useMutation();
|
||||
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Card,
|
||||
CardSection,
|
||||
Divider,
|
||||
Group,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { Card, CardSection, Divider, Group, Stack, Text, Title } from "@mantine/core";
|
||||
|
||||
import { api } from "@homarr/api/server";
|
||||
import { objectKeys } from "@homarr/common";
|
||||
@@ -15,11 +7,7 @@ import type { GroupPermissionKey } from "@homarr/definitions";
|
||||
import { groupPermissions } from "@homarr/definitions";
|
||||
import { getI18n, getScopedI18n } from "@homarr/translation/server";
|
||||
|
||||
import {
|
||||
PermissionForm,
|
||||
PermissionSwitch,
|
||||
SaveAffix,
|
||||
} from "./_group-permission-form";
|
||||
import { PermissionForm, PermissionSwitch, SaveAffix } from "./_group-permission-form";
|
||||
|
||||
interface GroupPermissionsPageProps {
|
||||
params: {
|
||||
@@ -27,9 +15,7 @@ interface GroupPermissionsPageProps {
|
||||
};
|
||||
}
|
||||
|
||||
export default async function GroupPermissionsPage({
|
||||
params,
|
||||
}: GroupPermissionsPageProps) {
|
||||
export default async function GroupPermissionsPage({ params }: GroupPermissionsPageProps) {
|
||||
const group = await api.group.getById({ id: params.id });
|
||||
const tPermissions = await getScopedI18n("group.permission");
|
||||
const t = await getI18n();
|
||||
@@ -99,10 +85,7 @@ const PermissionCard = async ({ group, isDanger }: PermissionCardProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const createGroupPermissionKey = (
|
||||
group: keyof typeof groupPermissions,
|
||||
permission: string,
|
||||
): GroupPermissionKey => {
|
||||
const createGroupPermissionKey = (group: keyof typeof groupPermissions, permission: string): GroupPermissionKey => {
|
||||
if (typeof groupPermissions[group] === "boolean") {
|
||||
return group as GroupPermissionKey;
|
||||
}
|
||||
|
||||
@@ -6,10 +6,7 @@ import { Button, Group, Stack, TextInput } from "@mantine/core";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { createModal, useModalAction } from "@homarr/modals";
|
||||
import {
|
||||
showErrorNotification,
|
||||
showSuccessNotification,
|
||||
} from "@homarr/notifications";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { validation } from "@homarr/validation";
|
||||
|
||||
@@ -61,11 +58,7 @@ const AddGroupModal = createModal<void>(({ actions }) => {
|
||||
})}
|
||||
>
|
||||
<Stack>
|
||||
<TextInput
|
||||
label={t("group.field.name")}
|
||||
data-autofocus
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
<TextInput label={t("group.field.name")} data-autofocus {...form.getInputProps("name")} />
|
||||
<Group justify="right">
|
||||
<Button onClick={actions.closeModal} variant="subtle" color="gray">
|
||||
{t("common.action.cancel")}
|
||||
|
||||
@@ -27,25 +27,18 @@ const searchParamsSchema = z.object({
|
||||
page: z.string().regex(/\d+/).transform(Number).catch(1),
|
||||
});
|
||||
|
||||
type SearchParamsSchemaInputFromSchema<
|
||||
TSchema extends Record<string, unknown>,
|
||||
> = Partial<{
|
||||
[K in keyof TSchema]: Exclude<TSchema[K], undefined> extends unknown[]
|
||||
? string[]
|
||||
: string;
|
||||
type SearchParamsSchemaInputFromSchema<TSchema extends Record<string, unknown>> = Partial<{
|
||||
[K in keyof TSchema]: Exclude<TSchema[K], undefined> extends unknown[] ? string[] : string;
|
||||
}>;
|
||||
|
||||
interface GroupsListPageProps {
|
||||
searchParams: SearchParamsSchemaInputFromSchema<
|
||||
z.infer<typeof searchParamsSchema>
|
||||
>;
|
||||
searchParams: SearchParamsSchemaInputFromSchema<z.infer<typeof searchParamsSchema>>;
|
||||
}
|
||||
|
||||
export default async function GroupsListPage(props: GroupsListPageProps) {
|
||||
const t = await getI18n();
|
||||
const searchParams = searchParamsSchema.parse(props.searchParams);
|
||||
const { items: groups, totalCount } =
|
||||
await api.group.getPaginated(searchParams);
|
||||
const { items: groups, totalCount } = await api.group.getPaginated(searchParams);
|
||||
|
||||
return (
|
||||
<Container size="xl">
|
||||
@@ -76,9 +69,7 @@ export default async function GroupsListPage(props: GroupsListPageProps) {
|
||||
</Table>
|
||||
|
||||
<Group justify="end">
|
||||
<TablePagination
|
||||
total={Math.ceil(totalCount / searchParams.pageSize)}
|
||||
/>
|
||||
<TablePagination total={Math.ceil(totalCount / searchParams.pageSize)} />
|
||||
</Group>
|
||||
</Stack>
|
||||
</Container>
|
||||
|
||||
@@ -6,9 +6,7 @@ import type { RouterOutputs } from "@homarr/api";
|
||||
import { createModal } from "@homarr/modals";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
export const InviteCopyModal = createModal<
|
||||
RouterOutputs["invite"]["createInvite"]
|
||||
>(({ actions, innerProps }) => {
|
||||
export const InviteCopyModal = createModal<RouterOutputs["invite"]["createInvite"]>(({ actions, innerProps }) => {
|
||||
const t = useScopedI18n("management.page.user.invite");
|
||||
const inviteUrl = useInviteUrl(innerProps);
|
||||
|
||||
@@ -50,13 +48,9 @@ export const InviteCopyModal = createModal<
|
||||
},
|
||||
});
|
||||
|
||||
const createPath = ({ id, token }: RouterOutputs["invite"]["createInvite"]) =>
|
||||
`/auth/invite/${id}?token=${token}`;
|
||||
const createPath = ({ id, token }: RouterOutputs["invite"]["createInvite"]) => `/auth/invite/${id}?token=${token}`;
|
||||
|
||||
const useInviteUrl = ({
|
||||
id,
|
||||
token,
|
||||
}: RouterOutputs["invite"]["createInvite"]) => {
|
||||
const useInviteUrl = ({ id, token }: RouterOutputs["invite"]["createInvite"]) => {
|
||||
const pathname = usePathname();
|
||||
|
||||
return window.location.href.replace(pathname, createPath({ id, token }));
|
||||
|
||||
@@ -21,9 +21,7 @@ interface InviteListComponentProps {
|
||||
initialInvites: RouterOutputs["invite"]["getAll"];
|
||||
}
|
||||
|
||||
export const InviteListComponent = ({
|
||||
initialInvites,
|
||||
}: InviteListComponentProps) => {
|
||||
export const InviteListComponent = ({ initialInvites }: InviteListComponentProps) => {
|
||||
const t = useScopedI18n("management.page.user.invite");
|
||||
const { data, isLoading } = clientApi.invite.getAll.useQuery(undefined, {
|
||||
initialData: initialInvites,
|
||||
@@ -32,9 +30,7 @@ export const InviteListComponent = ({
|
||||
refetchOnReconnect: false,
|
||||
});
|
||||
|
||||
const columns = useMemo<
|
||||
MRT_ColumnDef<RouterOutputs["invite"]["getAll"][number]>[]
|
||||
>(
|
||||
const columns = useMemo<MRT_ColumnDef<RouterOutputs["invite"]["getAll"][number]>[]>(
|
||||
() => [
|
||||
{
|
||||
accessorKey: "id",
|
||||
@@ -100,11 +96,7 @@ const RenderTopToolbarCustomActions = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const RenderRowActions = ({
|
||||
row,
|
||||
}: {
|
||||
row: MRT_Row<RouterOutputs["invite"]["getAll"][number]>;
|
||||
}) => {
|
||||
const RenderRowActions = ({ row }: { row: MRT_Row<RouterOutputs["invite"]["getAll"][number]> }) => {
|
||||
const t = useScopedI18n("management.page.user.invite");
|
||||
const { mutate, isPending } = clientApi.invite.deleteInvite.useMutation();
|
||||
const utils = clientApi.useUtils();
|
||||
@@ -121,12 +113,7 @@ const RenderRowActions = ({
|
||||
}, [openConfirmModal, row.original.id, mutate, utils, t]);
|
||||
|
||||
return (
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="red"
|
||||
onClick={handleDelete}
|
||||
loading={isPending}
|
||||
>
|
||||
<ActionIcon variant="subtle" color="red" onClick={handleDelete} loading={isPending}>
|
||||
<IconTrash color="red" size={20} stroke={1.5} />
|
||||
</ActionIcon>
|
||||
);
|
||||
|
||||
@@ -2,12 +2,7 @@
|
||||
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { ActionIcon, Affix, Card } from "@mantine/core";
|
||||
import {
|
||||
IconDimensions,
|
||||
IconPencil,
|
||||
IconToggleLeft,
|
||||
IconToggleRight,
|
||||
} from "@tabler/icons-react";
|
||||
import { IconDimensions, IconPencil, IconToggleLeft, IconToggleRight } from "@tabler/icons-react";
|
||||
|
||||
import type { IntegrationKind, WidgetKind } from "@homarr/definitions";
|
||||
import { useModalAction } from "@homarr/modals";
|
||||
@@ -34,19 +29,11 @@ interface WidgetPreviewPageContentProps {
|
||||
}[];
|
||||
}
|
||||
|
||||
export const WidgetPreviewPageContent = ({
|
||||
kind,
|
||||
integrationData,
|
||||
}: WidgetPreviewPageContentProps) => {
|
||||
export const WidgetPreviewPageContent = ({ kind, integrationData }: WidgetPreviewPageContentProps) => {
|
||||
const t = useScopedI18n("widgetPreview");
|
||||
const { openModal: openWidgetEditModal } = useModalAction(WidgetEditModal);
|
||||
const { openModal: openPreviewDimensionsModal } = useModalAction(
|
||||
PreviewDimensionsModal,
|
||||
);
|
||||
const currentDefinition = useMemo(
|
||||
() => widgetImports[kind].definition,
|
||||
[kind],
|
||||
);
|
||||
const { openModal: openPreviewDimensionsModal } = useModalAction(PreviewDimensionsModal);
|
||||
const currentDefinition = useMemo(() => widgetImports[kind].definition, [kind]);
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
const [dimensions, setDimensions] = useState<Dimensions>({
|
||||
width: 128,
|
||||
@@ -70,9 +57,7 @@ export const WidgetPreviewPageContent = ({
|
||||
integrationData: integrationData.filter(
|
||||
(integration) =>
|
||||
"supportedIntegrations" in currentDefinition &&
|
||||
(currentDefinition.supportedIntegrations as string[]).some(
|
||||
(kind) => kind === integration.kind,
|
||||
),
|
||||
(currentDefinition.supportedIntegrations as string[]).some((kind) => kind === integration.kind),
|
||||
),
|
||||
integrationSupport: "supportedIntegrations" in currentDefinition,
|
||||
});
|
||||
@@ -96,19 +81,11 @@ export const WidgetPreviewPageContent = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
withBorder
|
||||
w={dimensions.width}
|
||||
h={dimensions.height}
|
||||
p={dimensions.height >= 96 ? undefined : 4}
|
||||
>
|
||||
<Card withBorder w={dimensions.width} h={dimensions.height} p={dimensions.height >= 96 ? undefined : 4}>
|
||||
<Comp
|
||||
options={state.options as never}
|
||||
integrations={state.integrations.map(
|
||||
(stateIntegration) =>
|
||||
integrationData.find(
|
||||
(integration) => integration.id === stateIntegration.id,
|
||||
)!,
|
||||
(stateIntegration) => integrationData.find((integration) => integration.id === stateIntegration.id)!,
|
||||
)}
|
||||
width={dimensions.width}
|
||||
height={dimensions.height}
|
||||
@@ -118,36 +95,17 @@ export const WidgetPreviewPageContent = ({
|
||||
/>
|
||||
</Card>
|
||||
<Affix bottom={12} right={72}>
|
||||
<ActionIcon
|
||||
size={48}
|
||||
variant="default"
|
||||
radius="xl"
|
||||
onClick={handleOpenEditWidgetModal}
|
||||
>
|
||||
<ActionIcon size={48} variant="default" radius="xl" onClick={handleOpenEditWidgetModal}>
|
||||
<IconPencil size={24} />
|
||||
</ActionIcon>
|
||||
</Affix>
|
||||
<Affix bottom={12} right={72 + 60}>
|
||||
<ActionIcon
|
||||
size={48}
|
||||
variant="default"
|
||||
radius="xl"
|
||||
onClick={toggleEditMode}
|
||||
>
|
||||
{editMode ? (
|
||||
<IconToggleLeft size={24} />
|
||||
) : (
|
||||
<IconToggleRight size={24} />
|
||||
)}
|
||||
<ActionIcon size={48} variant="default" radius="xl" onClick={toggleEditMode}>
|
||||
{editMode ? <IconToggleLeft size={24} /> : <IconToggleRight size={24} />}
|
||||
</ActionIcon>
|
||||
</Affix>
|
||||
<Affix bottom={12} right={72 + 120}>
|
||||
<ActionIcon
|
||||
size={48}
|
||||
variant="default"
|
||||
radius="xl"
|
||||
onClick={openDimensionsModal}
|
||||
>
|
||||
<ActionIcon size={48} variant="default" radius="xl" onClick={openDimensionsModal}>
|
||||
<IconDimensions size={24} />
|
||||
</ActionIcon>
|
||||
</Affix>
|
||||
|
||||
@@ -11,48 +11,36 @@ interface InnerProps {
|
||||
setDimensions: (dimensions: Dimensions) => void;
|
||||
}
|
||||
|
||||
export const PreviewDimensionsModal = createModal<InnerProps>(
|
||||
({ actions, innerProps }) => {
|
||||
const t = useI18n();
|
||||
const form = useForm({
|
||||
initialValues: innerProps.dimensions,
|
||||
});
|
||||
export const PreviewDimensionsModal = createModal<InnerProps>(({ actions, innerProps }) => {
|
||||
const t = useI18n();
|
||||
const form = useForm({
|
||||
initialValues: innerProps.dimensions,
|
||||
});
|
||||
|
||||
const handleSubmit = (values: Dimensions) => {
|
||||
innerProps.setDimensions(values);
|
||||
actions.closeModal();
|
||||
};
|
||||
const handleSubmit = (values: Dimensions) => {
|
||||
innerProps.setDimensions(values);
|
||||
actions.closeModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack>
|
||||
<InputWrapper label={t("item.move.field.width.label")}>
|
||||
<Slider
|
||||
min={64}
|
||||
max={1024}
|
||||
step={64}
|
||||
{...form.getInputProps("width")}
|
||||
/>
|
||||
</InputWrapper>
|
||||
<InputWrapper label={t("item.move.field.height.label")}>
|
||||
<Slider
|
||||
min={64}
|
||||
max={1024}
|
||||
step={64}
|
||||
{...form.getInputProps("height")}
|
||||
/>
|
||||
</InputWrapper>
|
||||
<Group justify="end">
|
||||
<Button variant="subtle" color="gray" onClick={actions.closeModal}>
|
||||
{t("common.action.cancel")}
|
||||
</Button>
|
||||
<Button type="submit">{t("common.action.confirm")}</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
},
|
||||
).withOptions({
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack>
|
||||
<InputWrapper label={t("item.move.field.width.label")}>
|
||||
<Slider min={64} max={1024} step={64} {...form.getInputProps("width")} />
|
||||
</InputWrapper>
|
||||
<InputWrapper label={t("item.move.field.height.label")}>
|
||||
<Slider min={64} max={1024} step={64} {...form.getInputProps("height")} />
|
||||
</InputWrapper>
|
||||
<Group justify="end">
|
||||
<Button variant="subtle" color="gray" onClick={actions.closeModal}>
|
||||
{t("common.action.cancel")}
|
||||
</Button>
|
||||
<Button type="submit">{t("common.action.confirm")}</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
}).withOptions({
|
||||
defaultTitle: (t) => t("widgetPreview.dimensions.title"),
|
||||
});
|
||||
|
||||
|
||||
@@ -28,12 +28,9 @@ const handler = auth(async (req) => {
|
||||
endpoint: "/api/trpc",
|
||||
router: appRouter,
|
||||
req,
|
||||
createContext: () =>
|
||||
createTRPCContext({ session: req.auth, headers: req.headers }),
|
||||
createContext: () => createTRPCContext({ session: req.auth, headers: req.headers }),
|
||||
onError({ error, path, type }) {
|
||||
logger.error(
|
||||
`tRPC Error with ${type} on '${path}': (${error.code}) - ${error.message}`,
|
||||
);
|
||||
logger.error(`tRPC Error with ${type} on '${path}': (${error.code}) - ${error.message}`);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -7,28 +7,16 @@ import type { AccordionProps } from "@mantine/core";
|
||||
import { Accordion } from "@mantine/core";
|
||||
import { useShallowEffect } from "@mantine/hooks";
|
||||
|
||||
type ActiveTabAccordionProps = PropsWithChildren<
|
||||
Omit<AccordionProps<false>, "onChange">
|
||||
>;
|
||||
type ActiveTabAccordionProps = PropsWithChildren<Omit<AccordionProps<false>, "onChange">>;
|
||||
|
||||
// Replace state without fetchign new data
|
||||
const replace = (newUrl: string) => {
|
||||
window.history.replaceState(
|
||||
{ ...window.history.state, as: newUrl, url: newUrl },
|
||||
"",
|
||||
newUrl,
|
||||
);
|
||||
window.history.replaceState({ ...window.history.state, as: newUrl, url: newUrl }, "", newUrl);
|
||||
};
|
||||
|
||||
export const ActiveTabAccordion = ({
|
||||
children,
|
||||
...props
|
||||
}: ActiveTabAccordionProps) => {
|
||||
export const ActiveTabAccordion = ({ children, ...props }: ActiveTabAccordionProps) => {
|
||||
const pathname = usePathname();
|
||||
const onChange = useCallback(
|
||||
(tab: string | null) => (tab ? replace(`?tab=${tab}`) : replace(pathname)),
|
||||
[pathname],
|
||||
);
|
||||
const onChange = useCallback((tab: string | null) => (tab ? replace(`?tab=${tab}`) : replace(pathname)), [pathname]);
|
||||
|
||||
useShallowEffect(() => {
|
||||
if (props.defaultValue) {
|
||||
|
||||
@@ -47,12 +47,8 @@ export const useItemActions = () => {
|
||||
({ kind }: CreateItem) => {
|
||||
updateBoard((previous) => {
|
||||
const lastSection = previous.sections
|
||||
.filter(
|
||||
(section): section is EmptySection => section.kind === "empty",
|
||||
)
|
||||
.sort(
|
||||
(sectionA, sectionB) => sectionB.position - sectionA.position,
|
||||
)[0];
|
||||
.filter((section): section is EmptySection => section.kind === "empty")
|
||||
.sort((sectionA, sectionB) => sectionB.position - sectionA.position)[0];
|
||||
|
||||
if (!lastSection) return previous;
|
||||
|
||||
@@ -91,8 +87,7 @@ export const useItemActions = () => {
|
||||
...previous,
|
||||
sections: previous.sections.map((section) => {
|
||||
// Return same section if item is not in it
|
||||
if (!section.items.some((item) => item.id === itemId))
|
||||
return section;
|
||||
if (!section.items.some((item) => item.id === itemId)) return section;
|
||||
return {
|
||||
...section,
|
||||
items: section.items.map((item) => {
|
||||
@@ -119,8 +114,7 @@ export const useItemActions = () => {
|
||||
...previous,
|
||||
sections: previous.sections.map((section) => {
|
||||
// Return same section if item is not in it
|
||||
if (!section.items.some((item) => item.id === itemId))
|
||||
return section;
|
||||
if (!section.items.some((item) => item.id === itemId)) return section;
|
||||
return {
|
||||
...section,
|
||||
items: section.items.map((item) => {
|
||||
@@ -128,9 +122,7 @@ export const useItemActions = () => {
|
||||
if (item.id !== itemId) return item;
|
||||
return {
|
||||
...item,
|
||||
...("integrations" in item
|
||||
? { integrations: newIntegrations }
|
||||
: {}),
|
||||
...("integrations" in item ? { integrations: newIntegrations } : {}),
|
||||
};
|
||||
}),
|
||||
};
|
||||
@@ -168,18 +160,14 @@ export const useItemActions = () => {
|
||||
const moveItemToSection = useCallback(
|
||||
({ itemId, sectionId, ...positionProps }: MoveItemToSection) => {
|
||||
updateBoard((previous) => {
|
||||
const currentSection = previous.sections.find((section) =>
|
||||
section.items.some((item) => item.id === itemId),
|
||||
);
|
||||
const currentSection = previous.sections.find((section) => section.items.some((item) => item.id === itemId));
|
||||
|
||||
// If item is in the same section (on initial loading) don't do anything
|
||||
if (!currentSection) {
|
||||
return previous;
|
||||
}
|
||||
|
||||
const currentItem = currentSection.items.find(
|
||||
(item) => item.id === itemId,
|
||||
);
|
||||
const currentItem = currentSection.items.find((item) => item.id === itemId);
|
||||
if (!currentItem) {
|
||||
return previous;
|
||||
}
|
||||
|
||||
@@ -13,14 +13,7 @@ export const ItemSelectModal = createModal<void>(({ actions }) => {
|
||||
return (
|
||||
<Grid>
|
||||
{objectEntries(widgetImports).map(([key, value]) => {
|
||||
return (
|
||||
<WidgetItem
|
||||
key={key}
|
||||
kind={key}
|
||||
definition={value.definition}
|
||||
closeModal={actions.closeModal}
|
||||
/>
|
||||
);
|
||||
return <WidgetItem key={key} kind={key} definition={value.definition} closeModal={actions.closeModal} />;
|
||||
})}
|
||||
</Grid>
|
||||
);
|
||||
@@ -56,13 +49,7 @@ const WidgetItem = ({
|
||||
<Text lh={1.2} style={{ whiteSpace: "normal" }} ta="center">
|
||||
{t(`widget.${kind}.name`)}
|
||||
</Text>
|
||||
<Text
|
||||
lh={1.2}
|
||||
style={{ whiteSpace: "normal" }}
|
||||
size="xs"
|
||||
ta="center"
|
||||
c="dimmed"
|
||||
>
|
||||
<Text lh={1.2} style={{ whiteSpace: "normal" }} size="xs" ta="center" c="dimmed">
|
||||
{t(`widget.${kind}.description`)}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
@@ -15,62 +15,55 @@ interface InnerProps {
|
||||
onSuccess?: (name: string) => void;
|
||||
}
|
||||
|
||||
export const BoardRenameModal = createModal<InnerProps>(
|
||||
({ actions, innerProps }) => {
|
||||
const utils = clientApi.useUtils();
|
||||
const t = useI18n();
|
||||
const { mutate, isPending } = clientApi.board.renameBoard.useMutation({
|
||||
onSettled() {
|
||||
void utils.board.getBoardByName.invalidate({
|
||||
name: innerProps.previousName,
|
||||
});
|
||||
void utils.board.getHomeBoard.invalidate();
|
||||
},
|
||||
});
|
||||
const form = useZodForm(validation.board.rename.omit({ id: true }), {
|
||||
initialValues: {
|
||||
export const BoardRenameModal = createModal<InnerProps>(({ actions, innerProps }) => {
|
||||
const utils = clientApi.useUtils();
|
||||
const t = useI18n();
|
||||
const { mutate, isPending } = clientApi.board.renameBoard.useMutation({
|
||||
onSettled() {
|
||||
void utils.board.getBoardByName.invalidate({
|
||||
name: innerProps.previousName,
|
||||
});
|
||||
void utils.board.getHomeBoard.invalidate();
|
||||
},
|
||||
});
|
||||
const form = useZodForm(validation.board.rename.omit({ id: true }), {
|
||||
initialValues: {
|
||||
name: innerProps.previousName,
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (values: FormType) => {
|
||||
mutate(
|
||||
{
|
||||
id: innerProps.id,
|
||||
name: values.name,
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (values: FormType) => {
|
||||
mutate(
|
||||
{
|
||||
id: innerProps.id,
|
||||
name: values.name,
|
||||
{
|
||||
onSuccess: () => {
|
||||
actions.closeModal();
|
||||
innerProps.onSuccess?.(values.name);
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
actions.closeModal();
|
||||
innerProps.onSuccess?.(values.name);
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack>
|
||||
<TextInput
|
||||
label={t("board.field.name.label")}
|
||||
{...form.getInputProps("name")}
|
||||
data-autofocus
|
||||
/>
|
||||
<Group justify="end">
|
||||
<Button variant="subtle" color="gray" onClick={actions.closeModal}>
|
||||
{t("common.action.cancel")}
|
||||
</Button>
|
||||
<Button type="submit" loading={isPending}>
|
||||
{t("common.action.confirm")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
},
|
||||
);
|
||||
},
|
||||
).withOptions({
|
||||
defaultTitle: (t) =>
|
||||
t("board.setting.section.dangerZone.action.rename.modal.title"),
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack>
|
||||
<TextInput label={t("board.field.name.label")} {...form.getInputProps("name")} data-autofocus />
|
||||
<Group justify="end">
|
||||
<Button variant="subtle" color="gray" onClick={actions.closeModal}>
|
||||
{t("common.action.cancel")}
|
||||
</Button>
|
||||
<Button type="submit" loading={isPending}>
|
||||
{t("common.action.confirm")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
}).withOptions({
|
||||
defaultTitle: (t) => t("board.setting.section.dangerZone.action.rename.modal.title"),
|
||||
});
|
||||
|
||||
type FormType = Omit<z.infer<(typeof validation)["board"]["rename"]>, "id">;
|
||||
|
||||
@@ -2,9 +2,7 @@ import { auth } from "@homarr/auth/next";
|
||||
import type { BoardPermissionsProps } from "@homarr/auth/shared";
|
||||
import { constructBoardPermissions } from "@homarr/auth/shared";
|
||||
|
||||
export const getBoardPermissionsAsync = async (
|
||||
board: BoardPermissionsProps,
|
||||
) => {
|
||||
export const getBoardPermissionsAsync = async (board: BoardPermissionsProps) => {
|
||||
const session = await auth();
|
||||
return constructBoardPermissions(board, session);
|
||||
};
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import type { RefObject } from "react";
|
||||
import {
|
||||
Card,
|
||||
Collapse,
|
||||
Group,
|
||||
Stack,
|
||||
Title,
|
||||
UnstyledButton,
|
||||
} from "@mantine/core";
|
||||
import { Card, Collapse, Group, Stack, Title, UnstyledButton } from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { IconChevronDown, IconChevronUp } from "@tabler/icons-react";
|
||||
|
||||
@@ -30,23 +23,14 @@ export const BoardCategorySection = ({ section, mainRef }: Props) => {
|
||||
<Group wrap="nowrap" gap="sm">
|
||||
<UnstyledButton w="100%" p="sm" onClick={toggle}>
|
||||
<Group wrap="nowrap">
|
||||
{opened ? (
|
||||
<IconChevronUp size={20} />
|
||||
) : (
|
||||
<IconChevronDown size={20} />
|
||||
)}
|
||||
{opened ? <IconChevronUp size={20} /> : <IconChevronDown size={20} />}
|
||||
<Title order={3}>{section.name}</Title>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
<CategoryMenu category={section} />
|
||||
</Group>
|
||||
<Collapse in={opened} p="sm" pt={0}>
|
||||
<div
|
||||
className="grid-stack grid-stack-category"
|
||||
data-category
|
||||
data-section-id={section.id}
|
||||
ref={refs.wrapper}
|
||||
>
|
||||
<div className="grid-stack grid-stack-category" data-category data-section-id={section.id} ref={refs.wrapper}>
|
||||
<SectionContent items={section.items} refs={refs} />
|
||||
</div>
|
||||
</Collapse>
|
||||
|
||||
@@ -2,11 +2,7 @@ import { useCallback } from "react";
|
||||
|
||||
import { createId } from "@homarr/db/client";
|
||||
|
||||
import type {
|
||||
CategorySection,
|
||||
EmptySection,
|
||||
Section,
|
||||
} from "~/app/[locale]/boards/_types";
|
||||
import type { CategorySection, EmptySection, Section } from "~/app/[locale]/boards/_types";
|
||||
import { useUpdateBoard } from "~/app/[locale]/boards/(content)/_client";
|
||||
|
||||
interface AddCategory {
|
||||
@@ -41,9 +37,7 @@ export const useCategoryActions = () => {
|
||||
sections: [
|
||||
// Place sections before the new category
|
||||
...previous.sections.filter(
|
||||
(section) =>
|
||||
(section.kind === "category" || section.kind === "empty") &&
|
||||
section.position < position,
|
||||
(section) => (section.kind === "category" || section.kind === "empty") && section.position < position,
|
||||
),
|
||||
{
|
||||
id: createId(),
|
||||
@@ -62,8 +56,7 @@ export const useCategoryActions = () => {
|
||||
...previous.sections
|
||||
.filter(
|
||||
(section): section is CategorySection | EmptySection =>
|
||||
(section.kind === "category" || section.kind === "empty") &&
|
||||
section.position >= position,
|
||||
(section.kind === "category" || section.kind === "empty") && section.position >= position,
|
||||
)
|
||||
.map((section) => ({
|
||||
...section,
|
||||
@@ -134,29 +127,19 @@ export const useCategoryActions = () => {
|
||||
({ id, direction }: MoveCategory) => {
|
||||
updateBoard((previous) => {
|
||||
const currentCategory = previous.sections.find(
|
||||
(section): section is CategorySection =>
|
||||
section.kind === "category" && section.id === id,
|
||||
(section): section is CategorySection => section.kind === "category" && section.id === id,
|
||||
);
|
||||
if (!currentCategory) return previous;
|
||||
if (currentCategory?.position === 1 && direction === "up")
|
||||
return previous;
|
||||
if (
|
||||
currentCategory?.position === previous.sections.length - 2 &&
|
||||
direction === "down"
|
||||
)
|
||||
return previous;
|
||||
if (currentCategory?.position === 1 && direction === "up") return previous;
|
||||
if (currentCategory?.position === previous.sections.length - 2 && direction === "down") return previous;
|
||||
|
||||
return {
|
||||
...previous,
|
||||
sections: previous.sections.map((section) => {
|
||||
if (section.kind !== "category" && section.kind !== "empty")
|
||||
return section;
|
||||
if (section.kind !== "category" && section.kind !== "empty") return section;
|
||||
const offset = direction === "up" ? -2 : 2;
|
||||
// Move category and empty section
|
||||
if (
|
||||
section.position === currentCategory.position ||
|
||||
section.position - 1 === currentCategory.position
|
||||
) {
|
||||
if (section.position === currentCategory.position || section.position - 1 === currentCategory.position) {
|
||||
return {
|
||||
...section,
|
||||
position: section.position + offset,
|
||||
@@ -165,8 +148,7 @@ export const useCategoryActions = () => {
|
||||
|
||||
if (
|
||||
direction === "up" &&
|
||||
(section.position === currentCategory.position - 2 ||
|
||||
section.position === currentCategory.position - 1)
|
||||
(section.position === currentCategory.position - 2 || section.position === currentCategory.position - 1)
|
||||
) {
|
||||
return {
|
||||
...section,
|
||||
@@ -176,8 +158,7 @@ export const useCategoryActions = () => {
|
||||
|
||||
if (
|
||||
direction === "down" &&
|
||||
(section.position === currentCategory.position + 2 ||
|
||||
section.position === currentCategory.position + 3)
|
||||
(section.position === currentCategory.position + 2 || section.position === currentCategory.position + 3)
|
||||
) {
|
||||
return {
|
||||
...section,
|
||||
@@ -197,21 +178,18 @@ export const useCategoryActions = () => {
|
||||
({ id: categoryId }: RemoveCategory) => {
|
||||
updateBoard((previous) => {
|
||||
const currentCategory = previous.sections.find(
|
||||
(section): section is CategorySection =>
|
||||
section.kind === "category" && section.id === categoryId,
|
||||
(section): section is CategorySection => section.kind === "category" && section.id === categoryId,
|
||||
);
|
||||
if (!currentCategory) return previous;
|
||||
|
||||
const aboveWrapper = previous.sections.find(
|
||||
(section): section is EmptySection =>
|
||||
section.kind === "empty" &&
|
||||
section.position === currentCategory.position - 1,
|
||||
section.kind === "empty" && section.position === currentCategory.position - 1,
|
||||
);
|
||||
|
||||
const removedWrapper = previous.sections.find(
|
||||
(section): section is EmptySection =>
|
||||
section.kind === "empty" &&
|
||||
section.position === currentCategory.position + 1,
|
||||
section.kind === "empty" && section.position === currentCategory.position + 1,
|
||||
);
|
||||
|
||||
if (!aboveWrapper || !removedWrapper) return previous;
|
||||
@@ -232,16 +210,10 @@ export const useCategoryActions = () => {
|
||||
return {
|
||||
...previous,
|
||||
sections: [
|
||||
...previous.sections.filter(
|
||||
(section) => section.position < currentCategory.position - 1,
|
||||
),
|
||||
...previous.sections.filter((section) => section.position < currentCategory.position - 1),
|
||||
{
|
||||
...aboveWrapper,
|
||||
items: [
|
||||
...aboveWrapper.items,
|
||||
...previousCategoryItems,
|
||||
...previousBelowWrapperItems,
|
||||
],
|
||||
items: [...aboveWrapper.items, ...previousCategoryItems, ...previousBelowWrapperItems],
|
||||
},
|
||||
...previous.sections
|
||||
.filter(
|
||||
|
||||
@@ -16,41 +16,35 @@ interface InnerProps {
|
||||
onSuccess: (category: Category) => void;
|
||||
}
|
||||
|
||||
export const CategoryEditModal = createModal<InnerProps>(
|
||||
({ actions, innerProps }) => {
|
||||
const t = useI18n();
|
||||
const form = useZodForm(z.object({ name: z.string().min(1) }), {
|
||||
initialValues: {
|
||||
name: innerProps.category.name,
|
||||
},
|
||||
});
|
||||
export const CategoryEditModal = createModal<InnerProps>(({ actions, innerProps }) => {
|
||||
const t = useI18n();
|
||||
const form = useZodForm(z.object({ name: z.string().min(1) }), {
|
||||
initialValues: {
|
||||
name: innerProps.category.name,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={form.onSubmit((values) => {
|
||||
void innerProps.onSuccess({
|
||||
...innerProps.category,
|
||||
name: values.name,
|
||||
});
|
||||
actions.closeModal();
|
||||
})}
|
||||
>
|
||||
<Stack>
|
||||
<TextInput
|
||||
label={t("section.category.field.name.label")}
|
||||
data-autofocus
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
<Group justify="right">
|
||||
<Button onClick={actions.closeModal} variant="subtle" color="gray">
|
||||
{t("common.action.cancel")}
|
||||
</Button>
|
||||
<Button type="submit" color="teal">
|
||||
{innerProps.submitLabel}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
},
|
||||
).withOptions({});
|
||||
return (
|
||||
<form
|
||||
onSubmit={form.onSubmit((values) => {
|
||||
void innerProps.onSuccess({
|
||||
...innerProps.category,
|
||||
name: values.name,
|
||||
});
|
||||
actions.closeModal();
|
||||
})}
|
||||
>
|
||||
<Stack>
|
||||
<TextInput label={t("section.category.field.name.label")} data-autofocus {...form.getInputProps("name")} />
|
||||
<Group justify="right">
|
||||
<Button onClick={actions.closeModal} variant="subtle" color="gray">
|
||||
{t("common.action.cancel")}
|
||||
</Button>
|
||||
<Button type="submit" color="teal">
|
||||
{innerProps.submitLabel}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
}).withOptions({});
|
||||
|
||||
@@ -11,8 +11,7 @@ import { CategoryEditModal } from "./category-edit-modal";
|
||||
export const useCategoryMenuActions = (category: CategorySection) => {
|
||||
const { openModal } = useModalAction(CategoryEditModal);
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
const { addCategory, moveCategory, removeCategory, renameCategory } =
|
||||
useCategoryActions();
|
||||
const { addCategory, moveCategory, removeCategory, renameCategory } = useCategoryActions();
|
||||
const t = useI18n();
|
||||
|
||||
const createCategoryAtPosition = useCallback(
|
||||
|
||||
@@ -67,14 +67,8 @@ const useActions = (category: CategorySection) => {
|
||||
};
|
||||
|
||||
const useEditModeActions = (category: CategorySection) => {
|
||||
const {
|
||||
addCategoryAbove,
|
||||
addCategoryBelow,
|
||||
moveCategoryUp,
|
||||
moveCategoryDown,
|
||||
edit,
|
||||
remove,
|
||||
} = useCategoryMenuActions(category);
|
||||
const { addCategoryAbove, addCategoryBelow, moveCategoryUp, moveCategoryDown, edit, remove } =
|
||||
useCategoryMenuActions(category);
|
||||
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -5,12 +5,7 @@ import { useMemo } from "react";
|
||||
import type { RefObject } from "react";
|
||||
import { ActionIcon, Card, Menu } from "@mantine/core";
|
||||
import { useElementSize } from "@mantine/hooks";
|
||||
import {
|
||||
IconDotsVertical,
|
||||
IconLayoutKanban,
|
||||
IconPencil,
|
||||
IconTrash,
|
||||
} from "@tabler/icons-react";
|
||||
import { IconDotsVertical, IconLayoutKanban, IconPencil, IconTrash } from "@tabler/icons-react";
|
||||
import combineClasses from "clsx";
|
||||
import { useAtomValue } from "jotai";
|
||||
|
||||
@@ -43,12 +38,7 @@ export const SectionContent = ({ items, refs }: Props) => {
|
||||
return (
|
||||
<>
|
||||
{items.map((item) => (
|
||||
<BoardItem
|
||||
key={item.id}
|
||||
refs={refs}
|
||||
item={item}
|
||||
opacity={board.opacity}
|
||||
/>
|
||||
<BoardItem key={item.id} refs={refs} item={item} opacity={board.opacity} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
@@ -133,14 +123,9 @@ const ItemMenu = ({ offset, item }: { offset: number; item: Item }) => {
|
||||
const { openModal } = useModalAction(WidgetEditModal);
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
const isEditMode = useAtomValue(editModeAtom);
|
||||
const { updateItemOptions, updateItemIntegrations, removeItem } =
|
||||
useItemActions();
|
||||
const { data: integrationData, isPending } =
|
||||
clientApi.integration.all.useQuery();
|
||||
const currentDefinition = useMemo(
|
||||
() => widgetImports[item.kind].definition,
|
||||
[item.kind],
|
||||
);
|
||||
const { updateItemOptions, updateItemIntegrations, removeItem } = useItemActions();
|
||||
const { data: integrationData, isPending } = clientApi.integration.all.useQuery();
|
||||
const currentDefinition = useMemo(() => widgetImports[item.kind].definition, [item.kind]);
|
||||
|
||||
if (!isEditMode || isPending) return null;
|
||||
|
||||
@@ -164,9 +149,7 @@ const ItemMenu = ({ offset, item }: { offset: number; item: Item }) => {
|
||||
integrationData: (integrationData ?? []).filter(
|
||||
(integration) =>
|
||||
"supportedIntegrations" in currentDefinition &&
|
||||
(currentDefinition.supportedIntegrations as string[]).some(
|
||||
(kind) => kind === integration.kind,
|
||||
),
|
||||
(currentDefinition.supportedIntegrations as string[]).some((kind) => kind === integration.kind),
|
||||
),
|
||||
integrationSupport: "supportedIntegrations" in currentDefinition,
|
||||
});
|
||||
@@ -185,34 +168,19 @@ const ItemMenu = ({ offset, item }: { offset: number; item: Item }) => {
|
||||
return (
|
||||
<Menu withinPortal withArrow position="right-start" arrowPosition="center">
|
||||
<Menu.Target>
|
||||
<ActionIcon
|
||||
variant="transparent"
|
||||
pos="absolute"
|
||||
top={offset}
|
||||
right={offset}
|
||||
style={{ zIndex: 1 }}
|
||||
>
|
||||
<ActionIcon variant="transparent" pos="absolute" top={offset} right={offset} style={{ zIndex: 1 }}>
|
||||
<IconDotsVertical />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown miw={128}>
|
||||
<Menu.Label>{tItem("menu.label.settings")}</Menu.Label>
|
||||
<Menu.Item
|
||||
leftSection={<IconPencil size={16} />}
|
||||
onClick={openEditModal}
|
||||
>
|
||||
<Menu.Item leftSection={<IconPencil size={16} />} onClick={openEditModal}>
|
||||
{tItem("action.edit")}
|
||||
</Menu.Item>
|
||||
<Menu.Item leftSection={<IconLayoutKanban size={16} />}>
|
||||
{tItem("action.move")}
|
||||
</Menu.Item>
|
||||
<Menu.Item leftSection={<IconLayoutKanban size={16} />}>{tItem("action.move")}</Menu.Item>
|
||||
<Menu.Divider />
|
||||
<Menu.Label c="red.6">{t("common.dangerZone")}</Menu.Label>
|
||||
<Menu.Item
|
||||
c="red.6"
|
||||
leftSection={<IconTrash size={16} />}
|
||||
onClick={openRemoveModal}
|
||||
>
|
||||
<Menu.Item c="red.6" leftSection={<IconTrash size={16} />} onClick={openRemoveModal}>
|
||||
{tItem("action.remove")}
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
|
||||
@@ -19,11 +19,7 @@ export const BoardEmptySection = ({ section, mainRef }: Props) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
section.items.length > 0 || isEditMode
|
||||
? defaultClasses
|
||||
: `${defaultClasses} gridstack-empty-wrapper`
|
||||
}
|
||||
className={section.items.length > 0 || isEditMode ? defaultClasses : `${defaultClasses} gridstack-empty-wrapper`}
|
||||
style={{ transitionDuration: "0s" }}
|
||||
data-empty
|
||||
data-section-id={section.id}
|
||||
|
||||
@@ -15,20 +15,14 @@ interface InitializeGridstackProps {
|
||||
sectionColumnCount: number;
|
||||
}
|
||||
|
||||
export const initializeGridstack = ({
|
||||
section,
|
||||
refs,
|
||||
sectionColumnCount,
|
||||
}: InitializeGridstackProps) => {
|
||||
export const initializeGridstack = ({ section, refs, sectionColumnCount }: InitializeGridstackProps) => {
|
||||
if (!refs.wrapper.current) return false;
|
||||
// initialize gridstack
|
||||
const newGrid = refs.gridstack;
|
||||
newGrid.current = GridStack.init(
|
||||
{
|
||||
column: sectionColumnCount,
|
||||
margin: Math.round(
|
||||
Math.max(Math.min(refs.wrapper.current.offsetWidth / 100, 10), 1),
|
||||
),
|
||||
margin: Math.round(Math.max(Math.min(refs.wrapper.current.offsetWidth / 100, 10), 1)),
|
||||
cellHeight: 128,
|
||||
float: true,
|
||||
alwaysShowResizeHandle: true,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user