From 9608452bede645dd7746fe551eeafd4acf126372 Mon Sep 17 00:00:00 2001 From: Meierschlumpf Date: Fri, 6 Jan 2023 22:46:07 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Implemented=20different=20layout=20?= =?UTF-8?q?sizes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/configs/default.json | 514 +++++++++++++----- .../ChangeWidgetPositionModal.tsx | 12 +- .../WidgetsTab/WidgetElementType.tsx | 34 +- .../Dashboard/Tiles/Widgets/WidgetsMenu.tsx | 6 +- .../Dashboard/Views/DashboardView.tsx | 28 +- .../Dashboard/Wrappers/Category/Category.tsx | 1 - .../Dashboard/Wrappers/WrapperContent.tsx | 13 +- .../Wrappers/gridstack/column-sorting.ts | 99 ---- .../Wrappers/gridstack/init-gridstack.ts | 17 +- .../Dashboard/Wrappers/gridstack/store.tsx | 31 ++ .../Wrappers/gridstack/use-gridstack.ts | 176 +----- .../Actions/Docker/ContainerActionBar.tsx | 34 +- src/hooks/use-resize.ts | 4 +- src/styles/global.scss | 51 +- src/tools/config/migrateConfig.ts | 22 +- src/types/shape.ts | 6 + 16 files changed, 564 insertions(+), 484 deletions(-) delete mode 100644 src/components/Dashboard/Wrappers/gridstack/column-sorting.ts create mode 100644 src/components/Dashboard/Wrappers/gridstack/store.tsx diff --git a/data/configs/default.json b/data/configs/default.json index 660013340..d4f9186a6 100644 --- a/data/configs/default.json +++ b/data/configs/default.json @@ -17,84 +17,6 @@ } ], "apps": [ - { - "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33a", - "name": "Documentation", - "url": "https://homarr.dev", - "behaviour": { - "onClickUrl": "https://homarr.dev", - "externalUrl": "https://homarr.dev", - "isOpeningNewTab": true - }, - "network": { - "enabledStatusChecker": false, - "okStatus": [ - 200 - ] - }, - "appearance": { - "iconUrl": "/imgs/logo/logo.png" - }, - "integration": { - "type": null, - "properties": [] - }, - "area": { - "type": "wrapper", - "properties": { - "id": "default" - } - }, - "shape": { - "location": { - "x": 0, - "y": 11 - }, - "size": { - "width": 4, - "height": 3 - } - } - }, - { - "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a990", - "name": "Donate", - "url": "https://ko-fi.com/ajnart", - "behaviour": { - "onClickUrl": "https://ko-fi.com/ajnart", - "externalUrl": "https://ko-fi.com/ajnart", - "isOpeningNewTab": true - }, - "network": { - "enabledStatusChecker": false, - "okStatus": [ - 200 - ] - }, - "appearance": { - "iconUrl": "https://uploads-ssl.webflow.com/5c14e387dab576fe667689cf/61e1116779fc0a9bd5bdbcc7_Frame%206.png" - }, - "integration": { - "type": null, - "properties": [] - }, - "area": { - "type": "category", - "properties": { - "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f" - } - }, - "shape": { - "location": { - "x": 6, - "y": 1 - }, - "size": { - "width": 1, - "height": 2 - } - } - }, { "id": "76217a87-7151-42d0-b0cf-1b72aef63f83", "name": "Small app", @@ -117,13 +39,35 @@ } }, "shape": { - "location": { - "x": 4, - "y": 0 + "md": { + "location": { + "x": 1, + "y": 1 + }, + "size": { + "width": 1, + "height": 1 + } }, - "size": { - "width": 1, - "height": 1 + "sm": { + "location": { + "x": 0, + "y": 3 + }, + "size": { + "width": 1, + "height": 1 + } + }, + "lg": { + "location": { + "x": 4, + "y": 0 + }, + "size": { + "width": 1, + "height": 1 + } } }, "integration": { @@ -160,13 +104,35 @@ } }, "shape": { - "location": { - "x": 5, - "y": 2 + "md": { + "location": { + "x": 0, + "y": 1 + }, + "size": { + "width": 1, + "height": 1 + } }, - "size": { - "width": 1, - "height": 2 + "sm": { + "location": { + "x": 1, + "y": 2 + }, + "size": { + "width": 1, + "height": 1 + } + }, + "lg": { + "location": { + "x": 0, + "y": 0 + }, + "size": { + "width": 4, + "height": 3 + } } } }, @@ -199,50 +165,158 @@ } }, "shape": { - "location": { - "x": 7, - "y": 0 + "md": { + "location": { + "x": 3, + "y": 1 + }, + "size": { + "width": 1, + "height": 1 + } }, - "size": { - "width": 1, - "height": 1 + "sm": { + "location": { + "x": 1, + "y": 4 + }, + "size": { + "width": 1, + "height": 1 + } + }, + "lg": { + "location": { + "x": 4, + "y": 2 + }, + "size": { + "width": 1, + "height": 1 + } } } }, { - "id": "5df743d9-5cb1-457c-85d2-64ff86855652", - "name": "Your app", - "url": "https://homarr.dev", - "appearance": { - "iconUrl": "/imgs/logo/logo.png" + "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a990", + "name": "Donate", + "url": "https://ko-fi.com/ajnart", + "behaviour": { + "onClickUrl": "https://ko-fi.com/ajnart", + "externalUrl": "https://ko-fi.com/ajnart", + "isOpeningNewTab": true }, "network": { "enabledStatusChecker": false, - "okStatus": [] + "okStatus": [ + 200 + ] }, - "behaviour": { - "isOpeningNewTab": true, - "externalUrl": "https://homarr.dev" - }, - "area": { - "type": "wrapper", - "properties": { - "id": "default" - } - }, - "shape": { - "location": { - "x": 0, - "y": 7 - }, - "size": { - "width": 6, - "height": 4 - } + "appearance": { + "iconUrl": "https://uploads-ssl.webflow.com/5c14e387dab576fe667689cf/61e1116779fc0a9bd5bdbcc7_Frame%206.png" }, "integration": { "type": null, "properties": [] + }, + "area": { + "type": "category", + "properties": { + "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f" + } + }, + "shape": { + "md": { + "location": { + "x": 2, + "y": 0 + }, + "size": { + "width": 1, + "height": 1 + } + }, + "sm": { + "location": { + "x": 2, + "y": 0 + }, + "size": { + "width": 1, + "height": 1 + } + }, + "lg": { + "location": { + "x": 5, + "y": 0 + }, + "size": { + "width": 1, + "height": 1 + } + } + } + }, + { + "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33a", + "name": "Documentation", + "url": "https://homarr.dev", + "behaviour": { + "onClickUrl": "https://homarr.dev", + "externalUrl": "https://homarr.dev", + "isOpeningNewTab": true + }, + "network": { + "enabledStatusChecker": false, + "okStatus": [ + 200 + ] + }, + "appearance": { + "iconUrl": "/imgs/logo/logo.png" + }, + "integration": { + "type": null, + "properties": [] + }, + "area": { + "type": "category", + "properties": { + "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f" + } + }, + "shape": { + "md": { + "location": { + "x": 0, + "y": 10 + }, + "size": { + "width": 2, + "height": 1 + } + }, + "sm": { + "location": { + "x": 0, + "y": 10 + }, + "size": { + "width": 2, + "height": 1 + } + }, + "lg": { + "location": { + "x": 0, + "y": 10 + }, + "size": { + "width": 2, + "height": 1 + } + } } }, { @@ -267,13 +341,93 @@ } }, "shape": { - "location": { - "x": 8, - "y": 9 + "md": { + "location": { + "x": 0, + "y": 6 + }, + "size": { + "width": 1, + "height": 1 + } }, - "size": { - "width": 4, - "height": 2 + "sm": { + "location": { + "x": 0, + "y": 6 + }, + "size": { + "width": 1, + "height": 1 + } + }, + "lg": { + "location": { + "x": 0, + "y": 6 + }, + "size": { + "width": 1, + "height": 1 + } + } + }, + "integration": { + "type": null, + "properties": [] + } + }, + { + "id": "5df743d9-5cb1-457c-85d2-64ff86855652", + "name": "Your app", + "url": "https://homarr.dev", + "appearance": { + "iconUrl": "/imgs/logo/logo.png" + }, + "network": { + "enabledStatusChecker": false, + "okStatus": [] + }, + "behaviour": { + "isOpeningNewTab": true, + "externalUrl": "https://homarr.dev" + }, + "area": { + "type": "category", + "properties": { + "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f" + } + }, + "shape": { + "md": { + "location": { + "x": 3, + "y": 0 + }, + "size": { + "width": 1, + "height": 1 + } + }, + "sm": { + "location": { + "x": 2, + "y": 3 + }, + "size": { + "width": 1, + "height": 1 + } + }, + "lg": { + "location": { + "x": 6, + "y": 0 + }, + "size": { + "width": 1, + "height": 1 + } } }, "integration": { @@ -295,13 +449,35 @@ } }, "shape": { - "location": { - "x": 0, - "y": 0 + "md": { + "location": { + "x": 0, + "y": 1 + }, + "size": { + "width": 3, + "height": 5 + } }, - "size": { - "width": 3, - "height": 5 + "sm": { + "location": { + "x": 0, + "y": 0 + }, + "size": { + "width": 3, + "height": 5 + } + }, + "lg": { + "location": { + "x": 0, + "y": 0 + }, + "size": { + "width": 3, + "height": 5 + } } } }, @@ -318,13 +494,35 @@ } }, "shape": { - "location": { - "x": 0, - "y": 0 + "md": { + "location": { + "x": 0, + "y": 0 + }, + "size": { + "width": 3, + "height": 1 + } }, - "size": { - "width": 3, - "height": 2 + "sm": { + "location": { + "x": 0, + "y": 0 + }, + "size": { + "width": 2, + "height": 1 + } + }, + "lg": { + "location": { + "x": 4, + "y": 1 + }, + "size": { + "width": 3, + "height": 1 + } } } }, @@ -340,13 +538,35 @@ } }, "shape": { - "location": { - "x": 9, - "y": 0 + "sm": { + "location": { + "x": 0, + "y": 1 + }, + "size": { + "width": 3, + "height": 1 + } }, - "size": { - "width": 3, - "height": 2 + "md": { + "location": { + "x": 4, + "y": 0 + }, + "size": { + "width": 2, + "height": 2 + } + }, + "lg": { + "location": { + "x": 5, + "y": 2 + }, + "size": { + "width": 2, + "height": 1 + } } } } @@ -360,8 +580,8 @@ }, "customization": { "layout": { - "enabledLeftSidebar": false, - "enabledRightSidebar": false, + "enabledLeftSidebar": true, + "enabledRightSidebar": true, "enabledDocker": false, "enabledPing": false, "enabledSearchbar": true diff --git a/src/components/Dashboard/Modals/ChangePosition/ChangeWidgetPositionModal.tsx b/src/components/Dashboard/Modals/ChangePosition/ChangeWidgetPositionModal.tsx index 6e1d92f95..fb384c951 100644 --- a/src/components/Dashboard/Modals/ChangePosition/ChangeWidgetPositionModal.tsx +++ b/src/components/Dashboard/Modals/ChangePosition/ChangeWidgetPositionModal.tsx @@ -4,6 +4,7 @@ import { useConfigContext } from '../../../../config/provider'; import { useConfigStore } from '../../../../config/store'; import widgets from '../../../../widgets'; import { WidgetChangePositionModalInnerProps } from '../../Tiles/Widgets/WidgetsMenu'; +import { useGridstackStore } from '../../Wrappers/gridstack/store'; import { ChangePositionModal } from './ChangePositionModal'; export const ChangeWidgetPositionModal = ({ @@ -13,6 +14,7 @@ export const ChangeWidgetPositionModal = ({ }: ContextModalProps) => { const { name: configName } = useConfigContext(); const updateConfig = useConfigStore((x) => x.updateConfig); + const shapeSize = useGridstackStore(x => x.currentShapeSize); const handleSubmit = (x: number, y: number, width: number, height: number) => { if (!configName) { @@ -23,7 +25,7 @@ export const ChangeWidgetPositionModal = ({ configName, (prev) => { const currentWidget = prev.widgets.find((x) => x.id === innerProps.widgetId); - currentWidget!.shape = { + currentWidget!.shape[shapeSize] = { location: { x, y, @@ -57,10 +59,10 @@ export const ChangeWidgetPositionModal = ({ onCancel={handleCancel} heightData={heightData} widthData={widthData} - initialX={innerProps.widget.shape.location.x} - initialY={innerProps.widget.shape.location.y} - initialWidth={innerProps.widget.shape.size.width} - initialHeight={innerProps.widget.shape.size.height} + initialX={innerProps.widget.shape[shapeSize].location.x} + initialY={innerProps.widget.shape[shapeSize].location.y} + initialWidth={innerProps.widget.shape[shapeSize].size.width} + initialHeight={innerProps.widget.shape[shapeSize].size.height} /> ); }; diff --git a/src/components/Dashboard/Modals/SelectElement/Components/WidgetsTab/WidgetElementType.tsx b/src/components/Dashboard/Modals/SelectElement/Components/WidgetsTab/WidgetElementType.tsx index 01caf6c8b..abb890ece 100644 --- a/src/components/Dashboard/Modals/SelectElement/Components/WidgetsTab/WidgetElementType.tsx +++ b/src/components/Dashboard/Modals/SelectElement/Components/WidgetsTab/WidgetElementType.tsx @@ -46,13 +46,35 @@ export const WidgetElementType = ({ id, image, disabled, widget }: WidgetElement }, }, shape: { - location: { - x: 0, - y: 0, + sm: { + location: { + x: 0, + y: 0, + }, + size: { + width: widget.gridstack.minWidth, + height: widget.gridstack.minHeight, + }, }, - size: { - width: widget.gridstack.minWidth, - height: widget.gridstack.minHeight, + md: { + location: { + x: 0, + y: 0, + }, + size: { + width: widget.gridstack.minWidth, + height: widget.gridstack.minHeight, + }, + }, + lg: { + location: { + x: 0, + y: 0, + }, + size: { + width: widget.gridstack.minWidth, + height: widget.gridstack.minHeight, + }, }, }, }, diff --git a/src/components/Dashboard/Tiles/Widgets/WidgetsMenu.tsx b/src/components/Dashboard/Tiles/Widgets/WidgetsMenu.tsx index ced815e2f..37b9768b9 100644 --- a/src/components/Dashboard/Tiles/Widgets/WidgetsMenu.tsx +++ b/src/components/Dashboard/Tiles/Widgets/WidgetsMenu.tsx @@ -2,6 +2,7 @@ import { Title } from '@mantine/core'; import { useTranslation } from 'next-i18next'; import { openContextModalGeneric } from '../../../../tools/mantineModalManagerExtensions'; import { IWidget } from '../../../../widgets/widgets'; +import { useWrapperColumnCount } from '../../Wrappers/gridstack/store'; import { GenericTileMenu } from '../GenericTileMenu'; import { WidgetEditModalInnerProps } from './WidgetsEditModal'; import { WidgetsRemoveModalInnerProps } from './WidgetsRemoveModal'; @@ -9,6 +10,7 @@ import { WidgetsRemoveModalInnerProps } from './WidgetsRemoveModal'; export type WidgetChangePositionModalInnerProps = { widgetId: string; widget: IWidget; + wrapperColumnCount: number; }; interface WidgetsMenuProps { @@ -18,8 +20,9 @@ interface WidgetsMenuProps { export const WidgetsMenu = ({ integration, widget }: WidgetsMenuProps) => { const { t } = useTranslation(`modules/${integration}`); + const wrapperColumnCount = useWrapperColumnCount(); - if (!widget) return null; + if (!widget || !wrapperColumnCount) return null; const handleDeleteClick = () => { openContextModalGeneric({ @@ -39,6 +42,7 @@ export const WidgetsMenu = ({ integration, widget }: WidgetsMenuProps) => { innerProps: { widgetId: integration, widget, + wrapperColumnCount, }, }); }; diff --git a/src/components/Dashboard/Views/DashboardView.tsx b/src/components/Dashboard/Views/DashboardView.tsx index cacb40263..ae2388017 100644 --- a/src/components/Dashboard/Views/DashboardView.tsx +++ b/src/components/Dashboard/Views/DashboardView.tsx @@ -1,10 +1,12 @@ -import { Group, Stack } from '@mantine/core'; -import { useMemo } from 'react'; +import { Center, Group, Loader, Stack } from '@mantine/core'; +import { useEffect, useMemo, useRef } from 'react'; import { useConfigContext } from '../../../config/provider'; +import { useResize } from '../../../hooks/use-resize'; import { useScreenSmallerThan } from '../../../hooks/useScreenSmallerThan'; import { CategoryType } from '../../../types/category'; import { WrapperType } from '../../../types/wrapper'; import { DashboardCategory } from '../Wrappers/Category/Category'; +import { useGridstackStore } from '../Wrappers/gridstack/store'; import { DashboardSidebar } from '../Wrappers/Sidebar/Sidebar'; import { DashboardWrapper } from '../Wrappers/Wrapper/Wrapper'; @@ -12,14 +14,26 @@ export const DashboardView = () => { const wrappers = useWrapperItems(); const layoutSettings = useConfigContext()?.config?.settings.customization.layout; const doNotShowSidebar = useScreenSmallerThan('md'); + const notReady = typeof doNotShowSidebar === 'undefined'; + const mainAreaRef = useRef(null); + const { width } = useResize(mainAreaRef, [doNotShowSidebar]); + const setMainAreaWidth = useGridstackStore(x => x.setMainAreaWidth); + const mainAreaWidth = useGridstackStore(x => x.mainAreaWidth); + + useEffect(() => { + setMainAreaWidth(width); + }, [width]); return ( - {layoutSettings?.enabledLeftSidebar && !doNotShowSidebar ? ( + {notReady ?
+ +
: <> + {layoutSettings?.enabledLeftSidebar && !doNotShowSidebar && mainAreaWidth ? ( ) : null} - - {wrappers.map((item) => + + {!mainAreaWidth ? null : wrappers.map((item) => item.type === 'category' ? ( ) : ( @@ -27,9 +41,11 @@ export const DashboardView = () => { ) )} - {layoutSettings?.enabledRightSidebar && !doNotShowSidebar ? ( + {layoutSettings?.enabledRightSidebar && !doNotShowSidebar && mainAreaWidth ? ( ) : null} + +}
); }; diff --git a/src/components/Dashboard/Wrappers/Category/Category.tsx b/src/components/Dashboard/Wrappers/Category/Category.tsx index 89da45c00..9034a2e1a 100644 --- a/src/components/Dashboard/Wrappers/Category/Category.tsx +++ b/src/components/Dashboard/Wrappers/Category/Category.tsx @@ -22,7 +22,6 @@ export const DashboardCategory = ({ category }: DashboardCategoryProps) => {
diff --git a/src/components/Dashboard/Wrappers/WrapperContent.tsx b/src/components/Dashboard/Wrappers/WrapperContent.tsx index 579bbd7c8..b846d2979 100644 --- a/src/components/Dashboard/Wrappers/WrapperContent.tsx +++ b/src/components/Dashboard/Wrappers/WrapperContent.tsx @@ -6,6 +6,7 @@ import { IWidget, IWidgetDefinition } from '../../../widgets/widgets'; import { WidgetWrapper } from '../../../widgets/WidgetWrapper'; import { appTileDefinition } from '../Tiles/Apps/AppTile'; import { GridstackTileWrapper } from '../Tiles/TileWrapper'; +import { useGridstackStore } from './gridstack/store'; interface WrapperContentProps { apps: AppType[]; @@ -18,6 +19,10 @@ interface WrapperContentProps { } export function WrapperContent({ apps, refs, widgets }: WrapperContentProps) { + const shapeSize = useGridstackStore(x => x.currentShapeSize); + + if (!shapeSize) return null; + return ( <> {apps?.map((app) => { @@ -29,8 +34,8 @@ export function WrapperContent({ apps, refs, widgets }: WrapperContentProps) { key={app.id} itemRef={refs.items.current[app.id]} {...tile} - {...app.shape.location} - {...app.shape.size} + {...app.shape[shapeSize]?.location} + {...app.shape[shapeSize]?.size} > @@ -49,8 +54,8 @@ export function WrapperContent({ apps, refs, widgets }: WrapperContentProps) { itemRef={refs.items.current[widget.id]} id={definition.id} {...definition.gridstack} - {...widget.shape.location} - {...widget.shape.size} + {...widget.shape[shapeSize]?.location} + {...widget.shape[shapeSize]?.size} > diff --git a/src/components/Dashboard/Wrappers/gridstack/column-sorting.ts b/src/components/Dashboard/Wrappers/gridstack/column-sorting.ts deleted file mode 100644 index 7d958e7ee..000000000 --- a/src/components/Dashboard/Wrappers/gridstack/column-sorting.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { GridStackNode } from 'fily-publish-gridstack'; - -type GridstackColumnSortingFn = ( - column: number, - prevColumn: number, - newNodes: GridStackNode[], - nodes: GridStackNode[] -) => void; - -const getGridstackAttribute = (node: GridStackNode, path: 'x' | 'y' | 'w' | 'h'): number => - parseInt(node.el!.getAttribute(`data-gridstack-${path}`)!, 10); - -const getGridstackAttributes = (node: GridStackNode) => ({ - width: getGridstackAttribute(node, 'w'), - height: getGridstackAttribute(node, 'h'), - x: getGridstackAttribute(node, 'x'), - y: getGridstackAttribute(node, 'y'), -}); - -type Type = ReturnType & { node: GridStackNode }; - -const nextItem = (start: number, end: number, nodes: Type[]): number => { - const next = nodes - .filter((x) => x.y <= end && x.y + x.height - 1 > end) - .sort((a, b) => a.y + a.height - (b.y + b.height)) - .at(0); - if (!next) return end; - return nextItem(start, next.height - 1 + next.y, nodes); -}; - -const nextRowHeight = ( - nodes: Type[], - values: { height: number; items: Type[] }[], - maxHeight: number, - current = 0 -) => { - const item = nodes.find((x) => x.y >= current); - if (!item) return; - if (current < item.y) { - values.push({ height: item.y - current, items: [] }); - } - const next = nextItem(item.y, item.y + item.height - 1, nodes); - values.push({ - height: next + 1 - item.y, - items: nodes.filter((x) => x.y >= current - 2 && x.y + x.height <= current + next + 1 - item.y), - }); - nextRowHeight(nodes, values, maxHeight, next + 1); -}; - -const getRowHeights = (nodes: Type[]) => { - const maxHeightElement = nodes.sort((a, b) => a.y + a.height - (b.y + b.height)).at(-1); - if (!maxHeightElement) return []; - const maxHeight = maxHeightElement.height + maxHeightElement.y; - const rowHeights: { height: number; items: Type[] }[] = []; - nextRowHeight(nodes, rowHeights, maxHeight); - return rowHeights; -}; - -const sortNodesByYAndX = (a: GridStackNode, b: GridStackNode) => { - const aAttributes = getGridstackAttributes(a); - const bAttributes = getGridstackAttributes(b); - - const differenceY = aAttributes.y - bAttributes.y; - - return differenceY !== 0 ? differenceY : aAttributes.x - bAttributes.x; -}; - -export const commonColumnSorting: GridstackColumnSortingFn = ( - column, - prevColumn, - newNodes, - nodes -) => { - if (column === prevColumn) { - newNodes.concat(nodes); - return; - } - - let nextRow = 0; - let available = column; - - const sortedNodes = nodes.sort(sortNodesByYAndX); - const mappedNodes = sortedNodes.map((node) => ({ - ...getGridstackAttributes(node), - node, - })); - const rowHeights = getRowHeights(mappedNodes); - - const rowItems: Type[][] = []; - - // TODO: fix issue with spaces between. - let rowTotal = 0; - rowHeights.forEach(({ height }) => { - rowItems.push(mappedNodes.filter((node) => node.y >= rowTotal && node.y < rowTotal + height)); - rowTotal += height; - }); - - console.log(rowHeights); -}; diff --git a/src/components/Dashboard/Wrappers/gridstack/init-gridstack.ts b/src/components/Dashboard/Wrappers/gridstack/init-gridstack.ts index 3ae5923cc..c3a739468 100644 --- a/src/components/Dashboard/Wrappers/gridstack/init-gridstack.ts +++ b/src/components/Dashboard/Wrappers/gridstack/init-gridstack.ts @@ -20,7 +20,7 @@ export const initializeGridstack = ( ) => { if (!wrapperRef.current) return; // calculates the currently available count of columns - const columnCount = areaType === 'sidebar' ? 4 : wrapperColumnCount; + const columnCount = areaType === 'sidebar' ? 2 : wrapperColumnCount; const minRow = areaType !== 'sidebar' ? 1 : Math.floor(wrapperRef.current.offsetHeight / 64); // initialize gridstack const newGrid = gridRef; @@ -35,11 +35,14 @@ export const initializeGridstack = ( disableOneColumnMode: true, staticGrid: !isEditMode, minRow, + animate: false, }, // selector of the gridstack item (it's eather category or wrapper) `.grid-stack-${areaType}[data-${areaType}='${areaId}']` ); const grid = newGrid.current; + // Must be used to update the column count after the initialization + grid.column(columnCount); // Add listener for moving items around in a wrapper grid.on('change', (_, el) => { @@ -60,12 +63,16 @@ export const initializeGridstack = ( grid.batchUpdate(); grid.removeAll(false); items.forEach( - ({ id }) => - itemRefs.current[id] && grid.makeWidget(itemRefs.current[id].current as HTMLDivElement) + ({ id }) => { + const item = itemRefs.current[id]?.current; + item && grid.makeWidget(item as HTMLDivElement); + } ); widgets.forEach( - ({ id }) => - itemRefs.current[id] && grid.makeWidget(itemRefs.current[id].current as HTMLDivElement) + ({ id }) => { + const item = itemRefs.current[id]?.current; + item && grid.makeWidget(item as HTMLDivElement); + } ); grid.batchUpdate(false); }; diff --git a/src/components/Dashboard/Wrappers/gridstack/store.tsx b/src/components/Dashboard/Wrappers/gridstack/store.tsx new file mode 100644 index 000000000..881c0ee39 --- /dev/null +++ b/src/components/Dashboard/Wrappers/gridstack/store.tsx @@ -0,0 +1,31 @@ +import { useMantineTheme } from '@mantine/core'; +import create from 'zustand'; + +export const useGridstackStore = create((set, get) => ({ + mainAreaWidth: null, + currentShapeSize: null, + setMainAreaWidth: (w: number) => + set((v) => ({ ...v, mainAreaWidth: w, currentShapeSize: getCurrentShapeSize(w) })), +})); + +interface GridstackStoreType { + mainAreaWidth: null | number; + currentShapeSize: null | 'sm' | 'md' | 'lg'; + setMainAreaWidth: (width: number) => void; +} + +export const useWrapperColumnCount = () => { + const mainAreaWidth = useGridstackStore((x) => x.mainAreaWidth); + const { sm, xl } = useMantineTheme().breakpoints; + if (!mainAreaWidth) return null; + + if (mainAreaWidth >= xl) return 12; + + if (mainAreaWidth >= sm) return 6; + + return 3; +}; + +function getCurrentShapeSize(size: number) { + return size >= 1400 ? 'lg' : size >= 768 ? 'md' : 'sm'; +} diff --git a/src/components/Dashboard/Wrappers/gridstack/use-gridstack.ts b/src/components/Dashboard/Wrappers/gridstack/use-gridstack.ts index 2d05233eb..5f5b74a5f 100644 --- a/src/components/Dashboard/Wrappers/gridstack/use-gridstack.ts +++ b/src/components/Dashboard/Wrappers/gridstack/use-gridstack.ts @@ -3,58 +3,17 @@ import { createRef, MutableRefObject, RefObject, - useEffect, - useLayoutEffect, - useMemo, + useEffect, useMemo, useRef, } from 'react'; import { useConfigContext } from '../../../../config/provider'; import { useConfigStore } from '../../../../config/store'; -import { useResize } from '../../../../hooks/use-resize'; -import { useScreenLargerThan } from '../../../../hooks/useScreenLargerThan'; import { AppType } from '../../../../types/app'; import { AreaType } from '../../../../types/area'; import { IWidget } from '../../../../widgets/widgets'; import { useEditModeStore } from '../../Views/useEditModeStore'; -import { commonColumnSorting } from './column-sorting'; import { initializeGridstack } from './init-gridstack'; - -const getGridstackAttribute = (node: GridStackNode, path: 'x' | 'y' | 'w' | 'h'): number => parseInt(node.el!.getAttribute(`data-gridstack-${path}`)!, 10); - - const getGridstackAttributes = (node: GridStackNode) => ({ - width: getGridstackAttribute(node, 'w'), - height: getGridstackAttribute(node, 'h'), - x: getGridstackAttribute(node, 'x'), - y: getGridstackAttribute(node, 'y'), - }); - -type Type = (ReturnType & { node: GridStackNode }); - -const nextItem = (start: number, end: number, nodes: Type[]): number => { - const next = nodes - .filter(x => x.y <= end && x.y + x.height - 1 > end) - .sort((a, b) => (a.y + a.height) - (b.y + b.height)) - .at(0); -if (!next) return end; -return nextItem(start, next.height - 1 + next.y, nodes); -}; - -const nextRowHeight = (nodes: Type[], values: number[], current = 0) => { - const item = nodes.find(x => x.y >= current); - if (!item) return; - const next = nextItem(item.y, item.y + item.height - 1, nodes); - values.push(next + 1 - item.y); - nextRowHeight(nodes, values, next); -}; - -const getRowHeights = (nodes: GridStackNode[]) => { - const rowHeights: number[] = []; - nextRowHeight(nodes.map((node) => ({ - ...getGridstackAttributes(node), - node, - })), rowHeights); - return rowHeights; -}; +import { useGridstackStore, useWrapperColumnCount } from './store'; interface UseGristackReturnType { apps: AppType[]; @@ -66,18 +25,10 @@ interface UseGristackReturnType { }; } -const useWrapperColumnCount = () => { - const isLargerThanSm = useScreenLargerThan('sm'); - const isLargerThanXl = useScreenLargerThan('xl'); - - return typeof isLargerThanXl === 'undefined' || isLargerThanXl ? 12 : isLargerThanSm ? 6 : 3; -}; - export const useGridstack = ( areaType: 'wrapper' | 'category' | 'sidebar', areaId: string ): UseGristackReturnType => { - const wrapperColumnCount = useWrapperColumnCount(); const isEditMode = useEditModeStore((x) => x.enabled); const { config, configVersion, name: configName } = useConfigContext(); const updateConfig = useConfigStore((x) => x.updateConfig); @@ -87,10 +38,14 @@ export const useGridstack = ( const itemRefs = useRef>>({}); // reference of the gridstack object for modifications after initialization const gridRef = useRef(); + const wrapperColumnCount = useWrapperColumnCount(); + const shapeSize = useGridstackStore(x => x.currentShapeSize); + const mainAreaWidth = useGridstackStore(x => x.mainAreaWidth); // width of the wrapper (updating on page resize) - const { width } = useResize(wrapperRef); const root: HTMLHtmlElement = useMemo(() => document.querySelector(':root')!, []); + if (!mainAreaWidth || !shapeSize || !wrapperColumnCount) throw new Error('UseGridstack should not be executed before mainAreaWidth has been set!'); + const items = useMemo( () => config?.apps.filter( @@ -123,99 +78,14 @@ export const useGridstack = ( }); } - // change column count depending on the width and the gridRef useEffect(() => { - if (areaType === 'sidebar') return; - gridRef.current?.column( - wrapperColumnCount, - /*(column, prevColumn, newNodes, nodes) => { - let nextRow = 0; - let available = column; - let maxHeightInRow = 1; - - if (column === prevColumn) { - newNodes.concat(nodes); - return; - } - - const sortNodes = (a: GridStackNode, b: GridStackNode) => { - const aAttributes = getGridstackAttributes(a); - const bAttributes = getGridstackAttributes(b); - - const differenceY = aAttributes.y - bAttributes.y; - - return differenceY !== 0 ? differenceY : aAttributes.x - bAttributes.x; - }; - - const sortedNodes = nodes.sort(sortNodes); - const rowHeights = getRowHeights(sortedNodes); - - sortedNodes.forEach((node) => { - const newnode = node; - const width = parseInt(newnode.el!.getAttribute('data-gridstack-w')!, 10); - const height = parseInt(newnode.el!.getAttribute('data-gridstack-h')!, 10); - const x = parseInt(newnode.el!.getAttribute('data-gridstack-x')!, 10); - const y = parseInt(newnode.el!.getAttribute('data-gridstack-y')!, 10); - maxHeightInRow = height > maxHeightInRow ? height : maxHeightInRow; - - const continueInNextRow = () => { - nextRow += maxHeightInRow; - maxHeightInRow = 1; - available = column; - return nextRow; - }; - - if (column === 3) { - newnode.x = available >= width ? 3 - available : 0; - newnode.y = available === 3 || available >= width ? nextRow : continueInNextRow(); - - if (width > 3) { - newnode.w = 3; - continueInNextRow(); - } else if (available >= width) { - available -= width; - if (available === 0) { - continueInNextRow(); - } - } else if (available < width) { - newnode.y = continueInNextRow(); - available = 3 - width; - } - } else if (column === 6) { - newnode.x = available >= width ? 6 - available : 0; - newnode.y = nextRow; - - if (width > 6) { - newnode.w = 6; - continueInNextRow(); - } else if (available >= width) { - available -= width; - if (available === 0) { - continueInNextRow(); - } - } else if (available < width) { - newnode.y = continueInNextRow(); - available = 6 - width; - } - } else { - newnode.x = y % 2 === 1 ? x + 6 : x; - newnode.y = Math.floor(y / 2); - } - - newNodes.push(newnode); - }); - }*/ - commonColumnSorting - ); - }, [wrapperColumnCount]); - - useEffect(() => { - if (width === 0) return; - const widgetWidth = width / wrapperColumnCount; + const widgetWidth = mainAreaWidth / wrapperColumnCount; // widget width is used to define sizes of gridstack items within global.scss + // TODO: improve root.style.setProperty('--gridstack-widget-width', widgetWidth.toString()); + root.style.setProperty('--gridstack-column-count', wrapperColumnCount.toString()); gridRef.current?.cellHeight(widgetWidth); - }, [width, wrapperColumnCount]); + }, [mainAreaWidth, wrapperColumnCount]); const onChange = isEditMode ? (changedNode: GridStackNode) => { @@ -233,14 +103,14 @@ export const useGridstack = ( : previous.widgets.find((x) => x.id === itemId); if (!currentItem) return previous; - currentItem.shape = { + currentItem.shape[shapeSize] = { location: { - x: changedNode.x ?? currentItem.shape.location.x, - y: changedNode.y ?? currentItem.shape.location.y, + x: changedNode.x ?? currentItem.shape[shapeSize].location.x, + y: changedNode.y ?? currentItem.shape[shapeSize].location.y, }, size: { - width: changedNode.w ?? currentItem.shape.size.width, - height: changedNode.h ?? currentItem.shape.size.height, + width: changedNode.w ?? currentItem.shape[shapeSize].size.width, + height: changedNode.h ?? currentItem.shape[shapeSize].size.height, }, }; @@ -299,14 +169,14 @@ export const useGridstack = ( }; } - currentItem.shape = { + currentItem.shape[shapeSize] = { location: { - x: addedNode.x ?? currentItem.shape.location.x, - y: addedNode.y ?? currentItem.shape.location.y, + x: addedNode.x ?? currentItem.shape[shapeSize].location.x, + y: addedNode.y ?? currentItem.shape[shapeSize].location.y, }, size: { - width: addedNode.w ?? currentItem.shape.size.width, - height: addedNode.h ?? currentItem.shape.size.height, + width: addedNode.w ?? currentItem.shape[shapeSize].size.width, + height: addedNode.h ?? currentItem.shape[shapeSize].size.height, }, }; @@ -362,7 +232,7 @@ export const useGridstack = ( : () => {}; // initialize the gridstack - useLayoutEffect(() => { + useEffect(() => { initializeGridstack( areaType, wrapperRef, @@ -378,7 +248,7 @@ export const useGridstack = ( onAdd, } ); - }, [items, wrapperRef.current, widgets]); + }, [items, wrapperRef.current, widgets, wrapperColumnCount]); return { apps: items, diff --git a/src/components/layout/header/Actions/Docker/ContainerActionBar.tsx b/src/components/layout/header/Actions/Docker/ContainerActionBar.tsx index d24662c89..1b59ebd69 100644 --- a/src/components/layout/header/Actions/Docker/ContainerActionBar.tsx +++ b/src/components/layout/header/Actions/Docker/ContainerActionBar.tsx @@ -184,13 +184,35 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction }, }, shape: { - location: { - x: 0, - y: 0, + lg: { + location: { + x: 0, + y: 0, + }, + size: { + height: 1, + width: 1, + }, }, - size: { - height: 1, - width: 1, + md: { + location: { + x: 0, + y: 0, + }, + size: { + height: 1, + width: 1, + }, + }, + sm: { + location: { + x: 0, + y: 0, + }, + size: { + height: 1, + width: 1, + }, }, }, integration: { diff --git a/src/hooks/use-resize.ts b/src/hooks/use-resize.ts index ca425277a..6f479348d 100644 --- a/src/hooks/use-resize.ts +++ b/src/hooks/use-resize.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect, useState, MutableRefObject } from 'react'; -export const useResize = (myRef: MutableRefObject) => { +export const useResize = (myRef: MutableRefObject, dependencies: any[]) => { const [width, setWidth] = useState(0); const [height, setHeight] = useState(0); @@ -21,7 +21,7 @@ export const useResize = (myRef: MutableRefObject) => { useEffect(() => { handleResize(); - }, [myRef]); + }, [myRef, dependencies]); return { width, height }; }; diff --git a/src/styles/global.scss b/src/styles/global.scss index 5c605a390..0f84d11a9 100644 --- a/src/styles/global.scss +++ b/src/styles/global.scss @@ -2,6 +2,7 @@ :root { --gridstack-widget-width: 64; + --gridstack-column-count: 12; } .grid-stack-placeholder > .placeholder-content { @@ -17,24 +18,24 @@ } @for $i from 1 to 13 { - .grid-stack>.grid-stack-item[gs-w="#{$i}"] { width: ($i / 12) * 100 + "%" } - .grid-stack>.grid-stack-item[gs-min-w="#{$i}"] { min-width: ($i / 12) * 100 + "%" } - .grid-stack>.grid-stack-item[gs-max-w="#{$i}"] { max-width: ($i / 12) * 100 + "%" } + .grid-stack>.grid-stack-item[gs-w="#{$i}"] { width: calc(100% / #{var(--gridstack-column-count)} * #{$i}) } + .grid-stack>.grid-stack-item[gs-min-w="#{$i}"] { min-width: calc(100% / #{var(--gridstack-column-count)} * #{$i}) } + .grid-stack>.grid-stack-item[gs-max-w="#{$i}"] { max-width: calc(100% / #{var(--gridstack-column-count)} * #{$i}) } } @for $i from 1 to 96 { - .grid-stack>.grid-stack-item[gs-h="#{$i}"] { height: calc($i * #{var(--gridstack-widget-width)}) } - .grid-stack>.grid-stack-item[gs-min-h="#{$i}"] { min-height: calc($i * #{var(--gridstack-widget-width)}) } - .grid-stack>.grid-stack-item[gs-max-h="#{$i}"] { max-height: calc($i * #{var(--gridstack-widget-width)}) } + .grid-stack>.grid-stack-item[gs-h="#{$i}"] { height: calc(#{$i} * #{var(--gridstack-widget-width)}) } + .grid-stack>.grid-stack-item[gs-min-h="#{$i}"] { min-height: calc(#{$i} * #{var(--gridstack-widget-width)}) } + .grid-stack>.grid-stack-item[gs-max-h="#{$i}"] { max-height: calc(#{$i} * #{var(--gridstack-widget-width)}) } } @for $i from 1 to 13 { - .grid-stack>.grid-stack-item[gs-x="#{$i}"] { left: ($i / 12) * 100 + "%" } + .grid-stack>.grid-stack-item[gs-x="#{$i}"] { left: calc(100% / #{var(--gridstack-column-count)} * #{$i}) } } @for $i from 1 to 96 { - .grid-stack>.grid-stack-item[gs-y="#{$i}"] { top: calc($i * #{var(--gridstack-widget-width)}) } + .grid-stack>.grid-stack-item[gs-y="#{$i}"] { top: calc(#{$i} * #{var(--gridstack-widget-width)}) } } .grid-stack>.grid-stack-item>.grid-stack-item-content, @@ -48,7 +49,7 @@ } .grid-stack>.grid-stack-item { - min-width: (1/12)+'%'; + min-width: calc(percentage(1) * #{var(--gridstack-widget-width)}); } .grid-stack > .grid-stack-item > .grid-stack-item-content { @@ -58,35 +59,3 @@ .grid-stack.grid-stack-animate { transition: none; } - -@media screen and (max-width: 1400px) { - @for $i from 1 to 7 { - .grid-stack>.grid-stack-item[gs-w="#{$i}"] { width: percentage(($i / 6)) !important } - .grid-stack>.grid-stack-item[gs-min-w="#{$i}"] { min-width: percentage(($i / 6)) !important } - .grid-stack>.grid-stack-item[gs-max-w="#{$i}"] { max-width: percentage(($i / 6)) !important } - } - - @for $i from 1 to 7 { - .grid-stack>.grid-stack-item[gs-x="#{$i}"] { left: percentage(($i / 6)) } - } - - .grid-stack>.grid-stack-item { - min-width: percentage(1/6) !important; - } -} - -@media screen and (max-width: 768px) { - @for $i from 1 to 4 { - .grid-stack>.grid-stack-item[gs-w="#{$i}"] { width: percentage(($i / 3)) !important } - .grid-stack>.grid-stack-item[gs-min-w="#{$i}"] { min-width: percentage(($i / 3)) !important } - .grid-stack>.grid-stack-item[gs-max-w="#{$i}"] { max-width: percentage(($i / 3)) !important } - } - - @for $i from 1 to 4 { - .grid-stack>.grid-stack-item[gs-x="#{$i}"] { left: percentage(($i / 3)) } - } - - .grid-stack>.grid-stack-item { - min-width: percentage(1/3) !important; - } -} \ No newline at end of file diff --git a/src/tools/config/migrateConfig.ts b/src/tools/config/migrateConfig.ts index 9648c1fd1..13d811523 100644 --- a/src/tools/config/migrateConfig.ts +++ b/src/tools/config/migrateConfig.ts @@ -92,6 +92,17 @@ const getConfigAndCreateIfNotExsists = ( return category; }; +const getShapeForColumnCount = (index: number, columnCount: number) => ({ + location: { + x: index % columnCount, + y: Math.floor(index / columnCount), + }, + size: { + width: 1, + height: 1, + }, +}); + const migrateService = ( oldService: serviceItem, serviceIndex: number, @@ -117,13 +128,8 @@ const migrateService = ( }, area: areaType, shape: { - location: { - x: (serviceIndex * 3) % 18, - y: Math.floor(serviceIndex / 6) * 3, - }, - size: { - width: 3, - height: 3, - }, + lg: getShapeForColumnCount(serviceIndex, 12), + md: getShapeForColumnCount(serviceIndex, 6), + sm: getShapeForColumnCount(serviceIndex, 3), }, }); diff --git a/src/types/shape.ts b/src/types/shape.ts index 0bc27df0e..a8a6e6823 100644 --- a/src/types/shape.ts +++ b/src/types/shape.ts @@ -1,4 +1,10 @@ export interface ShapeType { + lg?: SizedShapeType; + md?: SizedShapeType; + sm?: SizedShapeType; +} + +export interface SizedShapeType { location: { x: number; y: number;