feat: add layout fields to board settings
This commit is contained in:
5 files changed
+109
-11
No files matched your search
@@ -16,6 +16,7 @@ import {
|
||||
IconResize,
|
||||
IconSettings,
|
||||
} from "@tabler/icons-react";
|
||||
import { applyAutoLayoutToBoard } from "node_modules/@homarr/api/src/router/board/board-layout";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useSession } from "@homarr/auth/client";
|
||||
@@ -165,7 +166,7 @@ const EditModeMenu = () => {
|
||||
});
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
if (isEditMode) return saveBoard(board);
|
||||
if (isEditMode) return saveBoard(applyAutoLayoutToBoard(board));
|
||||
open();
|
||||
}, [board, isEditMode, saveBoard, open]);
|
||||
|
||||
|
||||
@@ -1,11 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import { Button, Fieldset, Grid, Group, Input, NumberInput, Slider, Stack, Text, TextInput } from "@mantine/core";
|
||||
import {
|
||||
Button,
|
||||
Chip,
|
||||
ChipGroup,
|
||||
Fieldset,
|
||||
Grid,
|
||||
Group,
|
||||
Input,
|
||||
InputWrapper,
|
||||
NumberInput,
|
||||
RadioGroup,
|
||||
SimpleGrid,
|
||||
Slider,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { createId } from "@homarr/common";
|
||||
import { boardLayoutModes } from "@homarr/definitions";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { CustomRadioCard } from "@homarr/ui";
|
||||
import { boardSaveLayoutsSchema } from "@homarr/validation/board";
|
||||
|
||||
import type { Board } from "../../_types";
|
||||
@@ -25,6 +43,8 @@ export const LayoutSettingsContent = ({ board }: Props) => {
|
||||
const form = useZodForm(boardSaveLayoutsSchema.omit({ id: true }).required(), {
|
||||
initialValues: {
|
||||
layouts: board.layouts,
|
||||
layoutMode: board.layoutMode,
|
||||
baseLayoutId: board.baseLayoutId,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -38,6 +58,47 @@ export const LayoutSettingsContent = ({ board }: Props) => {
|
||||
})}
|
||||
>
|
||||
<Stack>
|
||||
<RadioGroup {...form.getInputProps("layoutMode")} label={t("board.field.layoutMode.label")}>
|
||||
<SimpleGrid cols={{ sm: 2, xs: 1 }} spacing="md">
|
||||
{boardLayoutModes.values.map((layoutMode) => (
|
||||
<CustomRadioCard
|
||||
key={layoutMode}
|
||||
value={layoutMode}
|
||||
label={t(`board.field.layoutMode.option.${layoutMode}.label`)}
|
||||
description={t(`board.field.layoutMode.option.${layoutMode}.description`)}
|
||||
/>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</RadioGroup>
|
||||
|
||||
{form.values.layoutMode === "auto" && (
|
||||
<InputWrapper label={t("board.field.baseLayout.label")} description={t("board.field.baseLayout.description")}>
|
||||
<ChipGroup {...form.getInputProps("baseLayoutId")} multiple={false}>
|
||||
<SimpleGrid
|
||||
cols={{
|
||||
lg: Math.min(6, form.values.layouts.length),
|
||||
sm: Math.min(4, form.values.layouts.length),
|
||||
xs: Math.min(2, form.values.layouts.length),
|
||||
}}
|
||||
spacing="md"
|
||||
mt="sm"
|
||||
>
|
||||
{form.values.layouts.map((layout) => (
|
||||
<Chip
|
||||
key={layout.id}
|
||||
value={layout.id}
|
||||
w="100%"
|
||||
size="sm"
|
||||
styles={{ label: { width: "100%", justifyContent: "center" } }}
|
||||
>
|
||||
{layout.name}
|
||||
</Chip>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</ChipGroup>
|
||||
</InputWrapper>
|
||||
)}
|
||||
|
||||
<Stack gap="sm">
|
||||
<Group justify="space-between" align="center">
|
||||
<Text fw={500}>{t("board.setting.section.layout.responsive.title")}</Text>
|
||||
@@ -82,18 +143,21 @@ export const LayoutSettingsContent = ({ board }: Props) => {
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
{form.values.layouts.length >= 2 && (
|
||||
{form.values.layouts.length >= 2 && form.values.baseLayoutId !== layout.id && (
|
||||
<Group justify="end">
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => {
|
||||
form.setValues((previous) =>
|
||||
previous.layouts !== undefined && previous.layouts.length >= 2
|
||||
? {
|
||||
layouts: form.values.layouts.filter((filteredLayout) => filteredLayout.id !== layout.id),
|
||||
}
|
||||
: previous,
|
||||
);
|
||||
form.setValues((previous) => {
|
||||
if (!previous.layouts) return previous;
|
||||
if (previous.layouts.length < 2) return previous;
|
||||
if (previous.baseLayoutId === layout.id) return previous;
|
||||
|
||||
return {
|
||||
...previous,
|
||||
layouts: previous.layouts.filter((filteredLayout) => filteredLayout.id !== layout.id),
|
||||
};
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("common.action.remove")}
|
||||
|
||||
@@ -600,13 +600,32 @@ export const boardRouter = createTRPCRouter({
|
||||
const board = await getFullBoardWithWhereAsync(ctx.db, eq(boards.id, input.id), ctx.session.user.id);
|
||||
|
||||
const addedLayouts = filterAddedItems(input.layouts, board.layouts);
|
||||
const removedLayouts = filterRemovedItems(input.layouts, board.layouts);
|
||||
|
||||
if (input.layoutMode === "auto" && input.baseLayoutId === null) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Base layout must be set when using automatic layout mode",
|
||||
});
|
||||
}
|
||||
|
||||
if (removedLayouts.some((layout) => layout.id === input.baseLayoutId)) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Cannot remove base layout",
|
||||
});
|
||||
}
|
||||
|
||||
const layoutsToInsert: InferInsertModel<typeof layouts>[] = [];
|
||||
const itemSectionLayoutsToInsert: InferInsertModel<typeof itemLayouts>[] = [];
|
||||
const sectionLayoutsToInsert: InferInsertModel<typeof sectionLayouts>[] = [];
|
||||
|
||||
let baseLayoutId = input.baseLayoutId;
|
||||
for (const addedLayout of addedLayouts) {
|
||||
const layoutId = createId();
|
||||
if (addedLayout.id === input.baseLayoutId) {
|
||||
baseLayoutId = layoutId;
|
||||
}
|
||||
|
||||
layoutsToInsert.push({
|
||||
id: layoutId,
|
||||
@@ -713,11 +732,18 @@ export const boardRouter = createTRPCRouter({
|
||||
.where(eq(layouts.id, updatedLayout.id));
|
||||
}
|
||||
|
||||
const removedLayouts = filterRemovedItems(input.layouts, board.layouts);
|
||||
const removedLayoutIds = removedLayouts.map((layout) => layout.id);
|
||||
if (removedLayoutIds.length > 0) {
|
||||
await ctx.db.delete(layouts).where(inArray(layouts.id, removedLayoutIds));
|
||||
}
|
||||
|
||||
await ctx.db
|
||||
.update(boards)
|
||||
.set({
|
||||
baseLayoutId,
|
||||
layoutMode: input.layoutMode,
|
||||
})
|
||||
.where(eq(boards.id, input.id));
|
||||
}),
|
||||
savePartialBoardSettings: protectedProcedure
|
||||
.input(boardSavePartialSettingsSchema.and(z.object({ id: z.string() })))
|
||||
|
||||
@@ -2806,6 +2806,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"baseLayout": {
|
||||
"label": "Base layout",
|
||||
"description": "The layout will be used to calculate positions and sizes for other layouts."
|
||||
},
|
||||
"columnCount": {
|
||||
"label": "Column count"
|
||||
},
|
||||
|
||||
@@ -80,6 +80,9 @@ export const boardSaveLayoutsSchema = z.object({
|
||||
breakpoint: z.number().min(0).max(32767),
|
||||
}),
|
||||
),
|
||||
// If not send it should fallback to the legacy behavior which is "custom"
|
||||
layoutMode: z.enum(boardLayoutModes.values).default("custom"),
|
||||
baseLayoutId: z.string().nullish(),
|
||||
});
|
||||
|
||||
export const boardSaveSchema = z.object({
|
||||
|
||||
Reference in new issue
Block a user