From 7761dc29c809b28cc8275dcabf77c453cea3621c Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Sun, 23 Feb 2025 17:34:56 +0100 Subject: [PATCH] feat(boards): add responsive layout system (#2271) --- .../app/[locale]/boards/(content)/_client.tsx | 8 +- .../[locale]/boards/(content)/_creator.tsx | 4 +- .../boards/(content)/_dynamic-client.tsx | 7 + .../boards/[name]/settings/_layout.tsx | 92 +- apps/nextjs/src/app/[locale]/boards/_types.ts | 6 +- .../board/items/actions/create-item.ts | 53 +- .../board/items/actions/duplicate-item.ts | 133 +- .../board/items/actions/empty-position.ts | 4 +- .../items/actions/move-and-resize-item.ts | 36 + .../items/actions/move-item-to-section.ts | 37 + .../board/items/actions/remove-item.ts | 12 + .../board/items/actions/section-elements.ts | 18 + .../items/actions/test/create-item.spec.ts | 136 +- .../items/actions/test/duplicate-item.spec.ts | 80 +- .../items/actions/test/empty-position.spec.ts | 2 +- .../items/actions/test/mocks/board-mock.ts | 90 + .../test/mocks/category-section-mock.ts | 23 + .../test/mocks/dynamic-section-mock.ts | 33 + .../actions/test/mocks/empty-section-mock.ts | 21 + .../items/actions/test/mocks/item-mock.ts | 38 + .../items/actions/test/mocks/layout-mock.ts | 21 + .../actions/test/move-and-resize-item.spec.ts | 65 + .../actions/test/move-item-to-section.spec.ts | 67 + .../items/actions/test/remove-item.spec.ts | 37 + .../board/items/actions/test/shared.ts | 32 - .../components/board/items/item-actions.tsx | 183 +- .../components/board/items/item-content.tsx | 6 +- .../src/components/board/items/item-menu.tsx | 4 +- .../board/items/item-move-modal.tsx | 6 +- .../category/actions/remove-category.ts | 179 +- .../actions/test/move-category.spec.ts | 4 +- .../actions/test/remove-category.spec.ts | 199 +- .../sections/category/category-actions.ts | 10 +- .../category/category-menu-actions.tsx | 13 +- .../src/components/board/sections/content.tsx | 72 +- .../board/sections/dynamic-section.tsx | 10 +- .../dynamic/actions/add-dynamic-section.ts | 51 + .../dynamic/actions/remove-dynamic-section.ts | 62 + .../board/sections/dynamic/dynamic-actions.ts | 74 +- .../board/sections/dynamic/dynamic-menu.tsx | 4 +- .../board/sections/empty-section.tsx | 5 +- .../board/sections/gridstack/gridstack.tsx | 9 +- .../board/sections/gridstack/use-gridstack.ts | 7 +- .../board/sections/section-actions.tsx | 27 +- .../board/sections/section-context.ts | 7 +- .../board/sections/use-section-items.ts | 51 +- packages/api/src/middlewares/integration.ts | 20 +- .../api/src/router/app/app-access-control.ts | 13 +- packages/api/src/router/board.ts | 762 +++++-- .../api/src/router/board/grid-algorithm.ts | 186 ++ .../router/board/test/grid-algorithm.spec.ts | 378 ++++ packages/api/src/router/test/board.spec.ts | 619 ++++- packages/api/src/router/widgets/notebook.ts | 9 +- .../integration-query-permissions.ts | 6 +- .../integration-query-permissions.spec.ts | 106 +- packages/boards/src/context.tsx | 44 +- .../common.ts => db/collection.ts} | 32 +- .../db/migrations/mysql/0029_add_layouts.sql | 50 + ...0_migrate_item_and_section_for_layouts.sql | 36 + .../migrations/mysql/meta/0029_snapshot.json | 2012 +++++++++++++++++ .../migrations/mysql/meta/0030_snapshot.json | 2012 +++++++++++++++++ .../db/migrations/mysql/meta/_journal.json | 14 + .../db/migrations/sqlite/0029_add_layouts.sql | 42 + ...0_migrate_item_and_section_for_layouts.sql | 47 + .../migrations/sqlite/meta/0029_snapshot.json | 1932 ++++++++++++++++ .../migrations/sqlite/meta/0030_snapshot.json | 1932 ++++++++++++++++ .../db/migrations/sqlite/meta/_journal.json | 14 + packages/db/package.json | 1 + packages/db/schema/index.ts | 3 + packages/db/schema/mysql.ts | 135 +- packages/db/schema/sqlite.ts | 135 +- .../src/boards/import-board-modal.tsx | 19 +- .../src/analyse/analyse-oldmarr-import.ts | 2 +- .../src/components/initial-oldmarr-import.tsx | 22 +- .../initial/board-selection-card.tsx | 110 +- packages/old-import/src/import-apps.ts | 160 -- packages/old-import/src/import-board.ts | 35 - packages/old-import/src/import-error.ts | 6 +- packages/old-import/src/import-items.ts | 101 - .../import/collections/board-collection.ts | 42 +- .../collections/integration-collection.ts | 4 +- .../src/import/collections/user-collection.ts | 9 +- packages/old-import/src/import/input.ts | 9 +- packages/old-import/src/mappers/map-board.ts | 2 - .../old-import/src/mappers/map-breakpoint.ts | 18 + .../src/mappers/map-column-count.ts | 6 +- packages/old-import/src/mappers/map-item.ts | 73 +- .../src/move-widgets-and-apps-merge.ts | 86 +- .../old-import/src/prepare/prepare-boards.ts | 30 +- .../old-import/src/prepare/prepare-items.ts | 7 +- .../old-import/src/prepare/prepare-single.ts | 10 +- packages/old-import/src/settings.ts | 4 +- packages/old-schema/src/index.ts | 2 +- packages/old-schema/src/tile.ts | 12 + packages/translation/src/lang/en.json | 22 +- packages/validation/src/board.ts | 19 +- packages/validation/src/index.ts | 2 +- packages/validation/src/shared.ts | 74 +- 98 files changed, 11770 insertions(+), 1694 deletions(-) create mode 100644 apps/nextjs/src/app/[locale]/boards/(content)/_dynamic-client.tsx create mode 100644 apps/nextjs/src/components/board/items/actions/move-and-resize-item.ts create mode 100644 apps/nextjs/src/components/board/items/actions/move-item-to-section.ts create mode 100644 apps/nextjs/src/components/board/items/actions/remove-item.ts create mode 100644 apps/nextjs/src/components/board/items/actions/section-elements.ts create mode 100644 apps/nextjs/src/components/board/items/actions/test/mocks/board-mock.ts create mode 100644 apps/nextjs/src/components/board/items/actions/test/mocks/category-section-mock.ts create mode 100644 apps/nextjs/src/components/board/items/actions/test/mocks/dynamic-section-mock.ts create mode 100644 apps/nextjs/src/components/board/items/actions/test/mocks/empty-section-mock.ts create mode 100644 apps/nextjs/src/components/board/items/actions/test/mocks/item-mock.ts create mode 100644 apps/nextjs/src/components/board/items/actions/test/mocks/layout-mock.ts create mode 100644 apps/nextjs/src/components/board/items/actions/test/move-and-resize-item.spec.ts create mode 100644 apps/nextjs/src/components/board/items/actions/test/move-item-to-section.spec.ts create mode 100644 apps/nextjs/src/components/board/items/actions/test/remove-item.spec.ts delete mode 100644 apps/nextjs/src/components/board/items/actions/test/shared.ts create mode 100644 apps/nextjs/src/components/board/sections/dynamic/actions/add-dynamic-section.ts create mode 100644 apps/nextjs/src/components/board/sections/dynamic/actions/remove-dynamic-section.ts create mode 100644 packages/api/src/router/board/grid-algorithm.ts create mode 100644 packages/api/src/router/board/test/grid-algorithm.spec.ts rename packages/{old-import/src/import/collections/common.ts => db/collection.ts} (56%) create mode 100644 packages/db/migrations/mysql/0029_add_layouts.sql create mode 100644 packages/db/migrations/mysql/0030_migrate_item_and_section_for_layouts.sql create mode 100644 packages/db/migrations/mysql/meta/0029_snapshot.json create mode 100644 packages/db/migrations/mysql/meta/0030_snapshot.json create mode 100644 packages/db/migrations/sqlite/0029_add_layouts.sql create mode 100644 packages/db/migrations/sqlite/0030_migrate_item_and_section_for_layouts.sql create mode 100644 packages/db/migrations/sqlite/meta/0029_snapshot.json create mode 100644 packages/db/migrations/sqlite/meta/0030_snapshot.json delete mode 100644 packages/old-import/src/import-apps.ts delete mode 100644 packages/old-import/src/import-board.ts delete mode 100644 packages/old-import/src/import-items.ts create mode 100644 packages/old-import/src/mappers/map-breakpoint.ts diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx index 0f3454653..e9551af52 100644 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx @@ -5,7 +5,7 @@ import { Box, LoadingOverlay, Stack } from "@mantine/core"; import type { RouterOutputs } from "@homarr/api"; import { clientApi } from "@homarr/api/client"; -import { useRequiredBoard } from "@homarr/boards/context"; +import { useCurrentLayout, useRequiredBoard } from "@homarr/boards/context"; import { BoardCategorySection } from "~/components/board/sections/category-section"; import { BoardEmptySection } from "~/components/board/sections/empty-section"; @@ -43,6 +43,7 @@ export const useUpdateBoard = () => { export const ClientBoard = () => { const board = useRequiredBoard(); + const currentLayoutId = useCurrentLayout(); const isReady = useIsBoardReady(); const fullWidthSortedSections = board.sections @@ -63,9 +64,10 @@ export const ClientBoard = () => { {fullWidthSortedSections.map((section) => section.kind === "empty" ? ( - + // Unique keys per layout to always reinitialize the gridstack + ) : ( - + ), )} diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx index f501e95f9..49be8c37c 100644 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx @@ -13,7 +13,7 @@ import { getI18n } from "@homarr/translation/server"; import { createMetaTitle } from "~/metadata"; import { createBoardLayout } from "../_layout-creator"; import type { Board } from "../_types"; -import { ClientBoard } from "./_client"; +import { DynamicClientBoard } from "./_dynamic-client"; import { BoardContentHeaderActions } from "./_header-actions"; export type Params = Record; @@ -37,7 +37,7 @@ export const createBoardContentPage = >( return ( - + ); }, diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_dynamic-client.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_dynamic-client.tsx new file mode 100644 index 000000000..50b0aff66 --- /dev/null +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_dynamic-client.tsx @@ -0,0 +1,7 @@ +"use client"; + +import dynamic from "next/dynamic"; + +export const DynamicClientBoard = dynamic(() => import("./_client").then((mod) => mod.ClientBoard), { + ssr: false, +}); diff --git a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_layout.tsx b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_layout.tsx index f0c2eeb9f..99c0a7f41 100644 --- a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_layout.tsx +++ b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_layout.tsx @@ -1,43 +1,109 @@ "use client"; -import { Button, Grid, Group, Input, Slider, Stack } from "@mantine/core"; +import { Button, Fieldset, Grid, Group, Input, NumberInput, Slider, Stack, Text, TextInput } from "@mantine/core"; +import { clientApi } from "@homarr/api/client"; +import { createId } from "@homarr/db/client"; import { useZodForm } from "@homarr/form"; import { useI18n } from "@homarr/translation/client"; import { validation } from "@homarr/validation"; import type { Board } from "../../_types"; -import { useSavePartialSettingsMutation } from "./_shared"; interface Props { board: Board; } export const LayoutSettingsContent = ({ board }: Props) => { const t = useI18n(); - const { mutate: savePartialSettings, isPending } = useSavePartialSettingsMutation(board); - const form = useZodForm(validation.board.savePartialSettings.pick({ columnCount: true }).required(), { + const utils = clientApi.useUtils(); + const { mutate: saveLayouts, isPending } = clientApi.board.saveLayouts.useMutation({ + onSettled() { + void utils.board.getBoardByName.invalidate({ name: board.name }); + void utils.board.getHomeBoard.invalidate(); + }, + }); + const form = useZodForm(validation.board.saveLayouts.omit({ id: true }).required(), { initialValues: { - columnCount: board.columnCount, + layouts: board.layouts, }, }); return (
{ - savePartialSettings({ + saveLayouts({ id: board.id, ...values, }); })} > - - - - - - - + + + {t("board.setting.section.layout.responsive.title")} + + + + {form.values.layouts.map((layout, index) => ( +
+ + + + + + + + + + + + + + + + {form.values.layouts.length >= 2 && ( + + + + )} +
+ ))} +
+