From 1b4070c9cec41cea30b0f4b0e6ca7e857f2925b5 Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Sun, 1 Oct 2023 14:40:18 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20database=20board=20on=20overv?= =?UTF-8?q?iew,=20Migrate=20all=20widgets=20to=20new=20widget=20system,=20?= =?UTF-8?q?Improve=20gridstack,=20Migrate=20sidebar=20to=20new=20system?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/locales/en/manage/boards.json | 1 + src/components/Board/item-actions.ts | 109 +++++++ .../Dashboard/Mobile/Ribbon/MobileRibbon.tsx | 23 +- .../Ribbon/MobileRibbonSidebarDrawer.tsx | 13 +- .../ChangeWidgetPositionModal.tsx | 10 +- .../Components/Shared/GenericElementType.tsx | 1 - .../Dashboard/Tiles/Apps/AppMenu.tsx | 6 +- .../Dashboard/Tiles/Apps/AppPing.tsx | 1 - .../Dashboard/Tiles/Widgets/WidgetsMenu.tsx | 2 +- .../Dashboard/Views/DashboardView.tsx | 69 +++- .../Dashboard/Wrappers/Sidebar/Sidebar.tsx | 17 +- .../Wrappers/gridstack/init-gridstack.ts | 58 ++-- .../Wrappers/gridstack/use-gridstack.ts | 306 +++++------------- .../layout/Templates/BoardLayout.tsx | 4 +- src/pages/_app.tsx | 18 +- src/pages/manage/boards/index.tsx | 34 +- src/server/api/routers/app.ts | 103 +++--- src/server/api/routers/board.ts | 24 +- src/server/api/routers/media-request.ts | 31 +- src/server/api/routers/notebook.ts | 9 +- src/server/api/routers/rss.ts | 14 +- src/widgets/bookmark/BookmarkWidgetTile.tsx | 112 +++---- src/widgets/boundary.tsx | 5 +- src/widgets/calendar/CalendarDay.tsx | 9 +- src/widgets/calendar/CalendarTile.tsx | 18 +- src/widgets/dashDot/DashDotTile.tsx | 8 +- src/widgets/dnshole/DnsHoleControls.tsx | 48 +-- src/widgets/dnshole/DnsHoleSummary.tsx | 10 +- src/widgets/download-speed/Tile.tsx | 6 +- .../TorrentNetworkTrafficTile.tsx | 4 +- src/widgets/iframe/IFrameTile.tsx | 22 +- .../media-requests/MediaRequestListTile.tsx | 96 +++--- .../media-requests/MediaRequestStatsTile.tsx | 9 +- .../media-requests/media-request-query.tsx | 13 +- src/widgets/media-server/MediaServerTile.tsx | 8 +- src/widgets/rss/RssWidgetTile.tsx | 11 +- src/widgets/torrent/TorrentTile.tsx | 16 +- src/widgets/useNet/UseNetTile.tsx | 6 +- src/widgets/video/VideoStreamTile.tsx | 14 +- src/widgets/widgets.ts | 12 +- 40 files changed, 675 insertions(+), 605 deletions(-) create mode 100644 src/components/Board/item-actions.ts diff --git a/public/locales/en/manage/boards.json b/public/locales/en/manage/boards.json index 29b5a3012..d69658b9e 100644 --- a/public/locales/en/manage/boards.json +++ b/public/locales/en/manage/boards.json @@ -19,6 +19,7 @@ }, "badges": { "fileSystem": "File system", + "database": "Database", "default": "Default" } }, diff --git a/src/components/Board/item-actions.ts b/src/components/Board/item-actions.ts new file mode 100644 index 000000000..70631a677 --- /dev/null +++ b/src/components/Board/item-actions.ts @@ -0,0 +1,109 @@ +import { useCallback } from 'react'; +import { api } from '~/utils/api'; + +import { useRequiredBoard } from './context'; + +type MoveAndResizeItem = { + itemId: string; + x: number; + y: number; + width: number; + height: number; +}; +type MoveItemToSection = { + itemId: string; + sectionId: string; + x: number; + y: number; + width: number; + height: number; +}; + +export const useItemActions = () => { + const board = useRequiredBoard(); + const utils = api.useContext(); + const moveAndResizeItem = useCallback( + ({ itemId, ...positionProps }: MoveAndResizeItem) => { + utils.boards.byName.setData({ boardName: board.name }, (prev) => { + if (!prev) return prev; + return { + ...prev, + sections: prev.sections.map((section) => { + // Return same section if item is not in it + if (!section.items.some((item) => item.id === itemId)) return section; + return { + ...section, + items: section.items.map((item) => { + // Return same item if item is not the one we're moving + if (item.id !== itemId) return item; + return { + ...item, + ...positionProps, + }; + }), + }; + }), + }; + }); + }, + [board.name, utils] + ); + + const moveItemToSection = useCallback( + ({ itemId, sectionId, ...positionProps }: MoveItemToSection) => { + utils.boards.byName.setData({ boardName: board.name }, (prev) => { + if (!prev) return prev; + + const currentSection = prev.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 || currentSection.id === sectionId) return prev; + + let currentItem = currentSection?.items.find((item) => item.id === itemId); + if (!currentItem) return prev; + + return { + ...prev, + sections: prev.sections.map((section) => { + // Return sections without item if not section where it is moved to + if (section.id !== sectionId) + return { + ...section, + items: section.items.filter((item) => item.id !== itemId), + }; + + // Return section and add item to it + return { + ...section, + items: section.items.concat({ + ...currentItem!, + ...positionProps, + }), + }; + }), + }; + }); + }, + [board.name, utils] + ); + + return { + moveAndResizeItem, + moveItemToSection, + }; +}; + +/* +- Add category (on top, below, above) +- Rename category +- Move category (down & up) +- Remove category +- Add widget +- Edit widget +- Remove widget +- Add app +- Edit app +- Remove app +*/ diff --git a/src/components/Dashboard/Mobile/Ribbon/MobileRibbon.tsx b/src/components/Dashboard/Mobile/Ribbon/MobileRibbon.tsx index 6f8ddce20..f48dc3fdc 100644 --- a/src/components/Dashboard/Mobile/Ribbon/MobileRibbon.tsx +++ b/src/components/Dashboard/Mobile/Ribbon/MobileRibbon.tsx @@ -1,27 +1,32 @@ import { ActionIcon, Space, createStyles } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; import { IconChevronLeft, IconChevronRight } from '@tabler/icons-react'; - -import { useConfigContext } from '~/config/provider'; +import { SidebarSection, useRequiredBoard } from '~/components/Board/context'; import { useScreenLargerThan } from '~/hooks/useScreenLargerThan'; + import { MobileRibbonSidebarDrawer } from './MobileRibbonSidebarDrawer'; export const MobileRibbons = () => { const { classes, cx } = useStyles(); - const { config } = useConfigContext(); + const board = useRequiredBoard(); const [openedRight, rightSidebar] = useDisclosure(false); const [openedLeft, leftSidebar] = useDisclosure(false); const screenLargerThanMd = useScreenLargerThan('md'); - if (screenLargerThanMd || !config) { + if (screenLargerThanMd) { return <>; } - const layoutSettings = config.settings.customization.layout; + const leftSection = board.sections.find( + (x): x is SidebarSection => x.type === 'sidebar' && x.position === 'left' + )!; + const rightSection = board.sections.find( + (x): x is SidebarSection => x.type === 'sidebar' && x.position === 'right' + )!; return (
- {layoutSettings.enabledLeftSidebar ? ( + {board.isLeftSidebarVisible && leftSection ? ( <> { ) : ( )} - {layoutSettings.enabledRightSidebar ? ( + {board.isRightSidebarVisible && rightSection ? ( <> { ) : null} diff --git a/src/components/Dashboard/Mobile/Ribbon/MobileRibbonSidebarDrawer.tsx b/src/components/Dashboard/Mobile/Ribbon/MobileRibbonSidebarDrawer.tsx index d4cafd2c4..c1c934140 100644 --- a/src/components/Dashboard/Mobile/Ribbon/MobileRibbonSidebarDrawer.tsx +++ b/src/components/Dashboard/Mobile/Ribbon/MobileRibbonSidebarDrawer.tsx @@ -1,24 +1,25 @@ import { Drawer, Title } from '@mantine/core'; import { useTranslation } from 'next-i18next'; +import { SidebarSection } from '~/components/Board/context'; import { DashboardSidebar } from '../../Wrappers/Sidebar/Sidebar'; interface MobileRibbonSidebarDrawerProps { onClose: () => void; opened: boolean; - location: 'left' | 'right'; + section: SidebarSection; } export const MobileRibbonSidebarDrawer = ({ - location, + section, ...props }: MobileRibbonSidebarDrawerProps) => { const { t } = useTranslation('layout/mobile/drawer'); return ( {t('title', { position: location })}} + position={section.position} + title={{t('title', { position: section.position })}} style={{ display: 'flex', justifyContent: 'center', @@ -28,10 +29,10 @@ export const MobileRibbonSidebarDrawer = ({ width: '100%', }, }} - transitionProps={{ transition: `slide-${location === 'right' ? 'left' : 'right'}` }} + transitionProps={{ transition: `slide-${section.position === 'right' ? 'left' : 'right'}` }} {...props} > - + ); }; diff --git a/src/components/Dashboard/Modals/ChangePosition/ChangeWidgetPositionModal.tsx b/src/components/Dashboard/Modals/ChangePosition/ChangeWidgetPositionModal.tsx index 43a68101f..efa5b574c 100644 --- a/src/components/Dashboard/Modals/ChangePosition/ChangeWidgetPositionModal.tsx +++ b/src/components/Dashboard/Modals/ChangePosition/ChangeWidgetPositionModal.tsx @@ -1,8 +1,8 @@ import { SelectItem } from '@mantine/core'; import { ContextModalProps, closeModal } from '@mantine/modals'; - import { useConfigContext } from '~/config/provider'; import { useConfigStore } from '~/config/store'; + import widgets from '../../../../widgets'; import { WidgetChangePositionModalInnerProps } from '../../Tiles/Widgets/WidgetsMenu'; import { useGridstackStore, useWrapperColumnCount } from '../../Wrappers/gridstack/store'; @@ -64,10 +64,10 @@ export const ChangeWidgetPositionModal = ({ onCancel={handleCancel} heightData={heightData} widthData={widthData} - 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} + initialX={innerProps.widget.x} + initialY={innerProps.widget.y} + initialWidth={innerProps.widget.width} + initialHeight={innerProps.widget.height} /> ); }; diff --git a/src/components/Dashboard/Modals/SelectElement/Components/Shared/GenericElementType.tsx b/src/components/Dashboard/Modals/SelectElement/Components/Shared/GenericElementType.tsx index 45c0807e9..c87e5ef80 100644 --- a/src/components/Dashboard/Modals/SelectElement/Components/Shared/GenericElementType.tsx +++ b/src/components/Dashboard/Modals/SelectElement/Components/Shared/GenericElementType.tsx @@ -2,7 +2,6 @@ import { Button, Card, Center, Grid, Stack, Text } from '@mantine/core'; import { Icon } from '@tabler/icons-react'; import { useTranslation } from 'next-i18next'; import Image from 'next/image'; -import React from 'react'; import { useStyles } from './styles'; diff --git a/src/components/Dashboard/Tiles/Apps/AppMenu.tsx b/src/components/Dashboard/Tiles/Apps/AppMenu.tsx index ca95b35f7..9764ea8cb 100644 --- a/src/components/Dashboard/Tiles/Apps/AppMenu.tsx +++ b/src/components/Dashboard/Tiles/Apps/AppMenu.tsx @@ -1,11 +1,13 @@ +import { AppItem } from '~/components/Board/context'; import { useConfigContext } from '~/config/provider'; import { useConfigStore } from '~/config/store'; import { openContextModalGeneric } from '~/tools/mantineModalManagerExtensions'; import { AppType } from '~/types/app'; + import { GenericTileMenu } from '../GenericTileMenu'; interface TileMenuProps { - app: AppType; + app: AppItem; } export const AppMenu = ({ app }: TileMenuProps) => { @@ -13,7 +15,7 @@ export const AppMenu = ({ app }: TileMenuProps) => { const { updateConfig } = useConfigStore(); const handleClickEdit = () => { - openContextModalGeneric<{ app: AppType; allowAppNamePropagation: boolean }>({ + openContextModalGeneric<{ app: AppItem; allowAppNamePropagation: boolean }>({ modal: 'editApp', size: 'xl', innerProps: { diff --git a/src/components/Dashboard/Tiles/Apps/AppPing.tsx b/src/components/Dashboard/Tiles/Apps/AppPing.tsx index a75bc7db9..46f2f325a 100644 --- a/src/components/Dashboard/Tiles/Apps/AppPing.tsx +++ b/src/components/Dashboard/Tiles/Apps/AppPing.tsx @@ -6,7 +6,6 @@ import { useSession } from 'next-auth/react'; import { useTranslation } from 'next-i18next'; import { AppItem } from '~/components/Board/context'; import { useConfigContext } from '~/config/provider'; -import { AppType } from '~/types/app'; import { RouterOutputs, api } from '~/utils/api'; interface AppPingProps { diff --git a/src/components/Dashboard/Tiles/Widgets/WidgetsMenu.tsx b/src/components/Dashboard/Tiles/Widgets/WidgetsMenu.tsx index abc52c417..23574876e 100644 --- a/src/components/Dashboard/Tiles/Widgets/WidgetsMenu.tsx +++ b/src/components/Dashboard/Tiles/Widgets/WidgetsMenu.tsx @@ -13,7 +13,7 @@ import { WidgetsRemoveModalInnerProps } from './WidgetsRemoveModal'; export type WidgetChangePositionModalInnerProps = { widgetId: string; widgetType: string; - widget: IWidget; + widget: WidgetItem; wrapperColumnCount: number; }; diff --git a/src/components/Dashboard/Views/DashboardView.tsx b/src/components/Dashboard/Views/DashboardView.tsx index a59a8c3b9..21091f2f5 100644 --- a/src/components/Dashboard/Views/DashboardView.tsx +++ b/src/components/Dashboard/Views/DashboardView.tsx @@ -1,43 +1,71 @@ -import { Group, Stack } from '@mantine/core'; +import { Box, Group, LoadingOverlay, Stack } from '@mantine/core'; import { useEffect, useRef } from 'react'; -import { CategorySection, EmptySection, useRequiredBoard } from '~/components/Board/context'; +import { + CategorySection, + EmptySection, + SidebarSection, + useRequiredBoard, +} from '~/components/Board/context'; import { useResize } from '~/hooks/use-resize'; import { useScreenLargerThan } from '~/hooks/useScreenLargerThan'; +import { DashboardCategory } from '../Wrappers/Category/Category'; import { DashboardSidebar } from '../Wrappers/Sidebar/Sidebar'; import { DashboardWrapper } from '../Wrappers/Wrapper/Wrapper'; import { useGridstackStore } from '../Wrappers/gridstack/store'; export const BoardView = () => { - const sections = useStackedSections(); + const stackedSections = useStackedSections(); const sidebarsVisible = useSidebarVisibility(); const { isReady, mainAreaRef } = usePrepareGridstack(); + const leftSidebarSection = useSidebarSection('left'); + const rightSidebarSection = useSidebarSection('right'); return ( - - {sidebarsVisible.left ? ( - - ) : null} + + + + {sidebarsVisible.left && leftSidebarSection ? ( + + ) : null} - - {isReady && - sections.map((item) => + + {stackedSections.map((item) => item.type === 'category' ? ( - {item.name} + ) : ( ) )} - + - {sidebarsVisible.right ? ( - - ) : null} - + {sidebarsVisible.right && rightSidebarSection ? ( + + ) : null} + + ); }; // // +/* +{sidebarsVisible.left ? ( + + ) : null} + +{sidebarsVisible.right ? ( + + ) : null} +*/ const usePrepareGridstack = () => { const mainAreaRef = useRef(null); @@ -51,7 +79,7 @@ const usePrepareGridstack = () => { }, [width]); return { - isReady: Boolean(mainAreaWidth), + isReady: !!mainAreaWidth, mainAreaRef, }; }; @@ -75,3 +103,10 @@ const useStackedSections = () => { (s): s is CategorySection | EmptySection => s.type === 'category' || s.type === 'empty' ); }; + +const useSidebarSection = (position: 'left' | 'right') => { + const board = useRequiredBoard(); + return board.sections.find( + (s): s is SidebarSection => s.type === 'sidebar' && s.position === position + ); +}; diff --git a/src/components/Dashboard/Wrappers/Sidebar/Sidebar.tsx b/src/components/Dashboard/Wrappers/Sidebar/Sidebar.tsx index 95b955150..968398196 100644 --- a/src/components/Dashboard/Wrappers/Sidebar/Sidebar.tsx +++ b/src/components/Dashboard/Wrappers/Sidebar/Sidebar.tsx @@ -1,16 +1,17 @@ import { Card } from '@mantine/core'; import { RefObject } from 'react'; +import { SidebarSection } from '~/components/Board/context'; import { useCardStyles } from '../../../layout/Common/useCardStyles'; import { WrapperContent } from '../WrapperContent'; import { useGridstack } from '../gridstack/use-gridstack'; interface DashboardSidebarProps extends DashboardSidebarInnerProps { - location: 'right' | 'left'; + section: SidebarSection; isGridstackReady: boolean; } -export const DashboardSidebar = ({ location, isGridstackReady }: DashboardSidebarProps) => { +export const DashboardSidebar = ({ section, isGridstackReady }: DashboardSidebarProps) => { const { cx, classes: { card: cardClass }, @@ -18,18 +19,18 @@ export const DashboardSidebar = ({ location, isGridstackReady }: DashboardSideba return ( - {isGridstackReady && } + {isGridstackReady && } ); }; interface DashboardSidebarInnerProps { - location: 'right' | 'left'; + section: SidebarSection; } // Is Required because of the gridstack main area width. -const SidebarInner = ({ location }: DashboardSidebarInnerProps) => { - const { refs, apps, widgets } = useGridstack('sidebar', location); +const SidebarInner = ({ section }: DashboardSidebarInnerProps) => { + const { refs } = useGridstack({ section }); const minRow = useMinRowForFullHeight(refs.wrapper); @@ -43,11 +44,11 @@ const SidebarInner = ({ location }: DashboardSidebarInnerProps) => { height: '100%', width: '100%', }} - data-sidebar={location} + data-sidebar={section.id} // eslint-disable-next-line react/no-unknown-property gs-min-row={minRow} > - +
); }; diff --git a/src/components/Dashboard/Wrappers/gridstack/init-gridstack.ts b/src/components/Dashboard/Wrappers/gridstack/init-gridstack.ts index 25ea258e1..f144f1c81 100644 --- a/src/components/Dashboard/Wrappers/gridstack/init-gridstack.ts +++ b/src/components/Dashboard/Wrappers/gridstack/init-gridstack.ts @@ -1,33 +1,40 @@ import { GridStack, GridStackNode } from 'fily-publish-gridstack'; import { MutableRefObject, RefObject } from 'react'; -import { Item } from '~/components/Board/context'; +import { Item, Section } from '~/components/Board/context'; -export const initializeGridstack = ( - areaType: 'wrapper' | 'category' | 'sidebar', - wrapperRef: RefObject, - gridRef: MutableRefObject, - itemRefs: MutableRefObject>>, - areaId: string, - items: Item[], - isEditMode: boolean, - wrapperColumnCount: number, - shapeSize: 'sm' | 'md' | 'lg', - tilesWithUnknownLocation: TileWithUnknownLocation[], +type InitializeGridstackProps = { + section: Section; + refs: { + wrapper: RefObject; + items: MutableRefObject>>; + gridstack: MutableRefObject; + }; + isEditMode: boolean; + sectionColumnCount: number; events: { onChange: (changedNode: GridStackNode) => void; onAdd: (addedNode: GridStackNode) => void; - } -) => { - if (!wrapperRef.current) return; + }; +}; + +export const initializeGridstack = ({ + section, + refs, + isEditMode, + sectionColumnCount, + events, +}: InitializeGridstackProps) => { + if (!refs.wrapper.current) return; // calculates the currently available count of columns - const columnCount = areaType === 'sidebar' ? 2 : wrapperColumnCount; - const minRow = areaType !== 'sidebar' ? 1 : Math.floor(wrapperRef.current.offsetHeight / 128); + const columnCount = section.type === 'sidebar' ? 2 : sectionColumnCount; + const minRow = + section.type !== 'sidebar' ? 1 : Math.floor(refs.wrapper.current.offsetHeight / 128); // initialize gridstack - const newGrid = gridRef; + const newGrid = refs.gridstack; newGrid.current = GridStack.init( { column: columnCount, - margin: areaType === 'sidebar' ? 5 : 10, + margin: section.type === 'sidebar' ? 5 : 10, cellHeight: 128, float: true, alwaysShowResizeHandle: 'mobile', @@ -38,7 +45,7 @@ export const initializeGridstack = ( animate: false, }, // selector of the gridstack item (it's eather category or wrapper) - `.grid-stack-${areaType}[data-${areaType}='${areaId}']` + `.grid-stack-${section.type}[data-${section.type}='${section.id}']` ); const grid = newGrid.current; if (!grid) return; @@ -63,17 +70,10 @@ export const initializeGridstack = ( grid.batchUpdate(); grid.removeAll(false); - items.forEach((item) => { - const ref = itemRefs.current[item.id]?.current; + section.items.forEach((item) => { + const ref = refs.items.current[item.id]?.current; setAttributesFromShape(ref, item); ref && grid.makeWidget(ref as HTMLDivElement); - /*if (!item && ref) { - const gridItemElement = ref as GridItemHTMLElement; - if (gridItemElement.gridstackNode) { - const { x, y, w, h } = gridItemElement.gridstackNode; - tilesWithUnknownLocation.push({ x, y, w, h, type: 'app', id: item.id }); - } - }*/ }); grid.batchUpdate(false); }; diff --git a/src/components/Dashboard/Wrappers/gridstack/use-gridstack.ts b/src/components/Dashboard/Wrappers/gridstack/use-gridstack.ts index bf0fbb4d8..9b486a55a 100644 --- a/src/components/Dashboard/Wrappers/gridstack/use-gridstack.ts +++ b/src/components/Dashboard/Wrappers/gridstack/use-gridstack.ts @@ -1,14 +1,18 @@ import { GridStack, GridStackNode } from 'fily-publish-gridstack'; -import { MutableRefObject, RefObject, createRef, useEffect, useMemo, useRef } from 'react'; -import { Item, Section } from '~/components/Board/context'; -import { useConfigContext } from '~/config/provider'; -import { useConfigStore } from '~/config/store'; -import { AppType } from '~/types/app'; -import { AreaType } from '~/types/area'; -import { IWidget } from '~/widgets/widgets'; +import { + MutableRefObject, + RefObject, + createRef, + useCallback, + useEffect, + useMemo, + useRef, +} from 'react'; +import { Section } from '~/components/Board/context'; +import { useItemActions } from '~/components/Board/item-actions'; import { useEditModeStore } from '../../Views/useEditModeStore'; -import { TileWithUnknownLocation, initializeGridstack } from './init-gridstack'; +import { initializeGridstack } from './init-gridstack'; import { useGridstackStore, useWrapperColumnCount } from './store'; interface UseGristackReturnType { @@ -25,20 +29,22 @@ type UseGridstackProps = { export const useGridstack = ({ section }: UseGridstackProps): UseGristackReturnType => { const isEditMode = useEditModeStore((x) => x.enabled); - const updateConfig = useConfigStore((x) => x.updateConfig); + const { moveAndResizeItem, moveItemToSection } = useItemActions(); // define reference for wrapper - is used to calculate the width of the wrapper const wrapperRef = useRef(null); // references to the diffrent items contained in the gridstack 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 sectionColumnCount = useWrapperColumnCount(); const mainAreaWidth = useGridstackStore((x) => x.mainAreaWidth); // width of the wrapper (updating on page resize) - const root: HTMLHtmlElement = useMemo(() => document.querySelector(':root')!, []); + const root = useMemo(() => { + if (typeof document === 'undefined') return; + return document.querySelector(':root')! as HTMLHtmlElement; + }, [typeof document]); - if (!mainAreaWidth || !shapeSize || !wrapperColumnCount) { + if (/*!mainAreaWidth ||*/ !sectionColumnCount) { throw new Error('UseGridstack should not be executed before mainAreaWidth has been set!'); } @@ -52,239 +58,81 @@ export const useGridstack = ({ section }: UseGridstackProps): UseGristackReturnT } useEffect(() => { - if (section.type === 'sidebar') return; - const widgetWidth = mainAreaWidth / wrapperColumnCount; + if (section.type === 'sidebar' || !mainAreaWidth) return; + const widgetWidth = mainAreaWidth / sectionColumnCount; // widget width is used to define sizes of gridstack items within global.scss - root.style.setProperty('--gridstack-widget-width', widgetWidth.toString()); + root?.style.setProperty('--gridstack-widget-width', widgetWidth.toString()); gridRef.current?.cellHeight(widgetWidth); - }, [mainAreaWidth, wrapperColumnCount, gridRef.current]); + }, [mainAreaWidth, sectionColumnCount, gridRef.current]); useEffect(() => { // column count is used to define count of columns of gridstack within global.scss - root.style.setProperty('--gridstack-column-count', wrapperColumnCount.toString()); - }, [wrapperColumnCount]); + root?.style.setProperty('--gridstack-column-count', sectionColumnCount.toString()); + }, [sectionColumnCount]); - const configName = 'default'; - const onChange = isEditMode - ? (changedNode: GridStackNode) => { - if (!configName) return; + const onChange = useCallback( + (changedNode: GridStackNode) => { + if (!isEditMode) return; - const itemType = changedNode.el?.getAttribute('data-type'); - const itemId = changedNode.el?.getAttribute('data-id'); - if (!itemType || !itemId) return; + const itemType = changedNode.el?.getAttribute('data-type'); + const itemId = changedNode.el?.getAttribute('data-id'); + if (!itemType || !itemId) return; - // Updates the config and defines the new position of the item - updateConfig(configName, (previous) => { - const currentItem = - itemType === 'app' - ? previous.apps.find((x) => x.id === itemId) - : previous.widgets.find((x) => x.id === itemId); - if (!currentItem) return previous; + // Updates the config and defines the new position of the item + moveAndResizeItem({ + itemId, + x: changedNode.x!, + y: changedNode.y!, + width: changedNode.w!, + height: changedNode.h!, + }); + }, + [isEditMode] + ); - currentItem.shape[shapeSize] = { - location: { - x: changedNode.x!, - y: changedNode.y!, - }, - size: { - width: changedNode.w!, - height: changedNode.h!, - }, - }; + const onAdd = useCallback( + (addedNode: GridStackNode) => { + if (!isEditMode) return; - if (itemType === 'app') { - return { - ...previous, - apps: [ - ...previous.apps.filter((x) => x.id !== itemId), - { ...(currentItem as AppType) }, - ], - }; - } + const itemType = addedNode.el?.getAttribute('data-type'); + const itemId = addedNode.el?.getAttribute('data-id'); + if (!itemType || !itemId) return; - return { - ...previous, - widgets: [ - ...previous.widgets.filter((x) => x.id !== itemId), - { ...(currentItem as IWidget) }, - ], - }; - }); - } - : () => {}; - - const onAdd = isEditMode - ? (addedNode: GridStackNode) => { - if (!configName) return; - - const itemType = addedNode.el?.getAttribute('data-type'); - const itemId = addedNode.el?.getAttribute('data-id'); - if (!itemType || !itemId) return; - - // Updates the config and defines the new position and wrapper of the item - updateConfig( - configName, - (previous) => { - const currentItem = - itemType === 'app' - ? previous.apps.find((x) => x.id === itemId) - : previous.widgets.find((x) => x.id === itemId); - if (!currentItem) return previous; - - if (section.type === 'sidebar') { - currentItem.area = { - type: section.type, - properties: { - location: section.position, - }, - }; - } else { - currentItem.area = { - type: section.type as any, - properties: { - id: section.id, - }, - }; - } - - currentItem.shape[shapeSize] = { - location: { - x: addedNode.x!, - y: addedNode.y!, - }, - size: { - width: addedNode.w!, - height: addedNode.h!, - }, - }; - - if (itemType === 'app') { - return { - ...previous, - apps: [ - ...previous.apps.filter((x) => x.id !== itemId), - { ...(currentItem as AppType) }, - ], - }; - } - - return { - ...previous, - widgets: [ - ...previous.widgets.filter((x) => x.id !== itemId), - { ...(currentItem as IWidget) }, - ], - }; - }, - (prev, curr) => { - const isApp = itemType === 'app'; - - if (isApp) { - const currItem = curr.apps.find((x) => x.id === itemId); - const prevItem = prev.apps.find((x) => x.id === itemId); - if (!currItem || !prevItem) return false; - - return ( - currItem.area.type !== prevItem.area.type || - Object.entries(currItem.area.properties).some( - ([key, value]) => - prevItem.area.properties[key as keyof AreaType['properties']] !== value - ) - ); - } - - const currItem = curr.widgets.find((x) => x.id === itemId); - const prevItem = prev.widgets.find((x) => x.id === itemId); - if (!currItem || !prevItem) return false; - - return ( - currItem.area.type !== prevItem.area.type || - Object.entries(currItem.area.properties).some( - ([key, value]) => - prevItem.area.properties[key as keyof AreaType['properties']] !== value - ) - ); - } - ); - } - : () => {}; + moveItemToSection({ + itemId, + sectionId: section.id, + x: addedNode.x!, + y: addedNode.y!, + width: addedNode.w!, + height: addedNode.h!, + }); + }, + [isEditMode] + ); // initialize the gridstack useEffect(() => { - const removeEventHandlers = () => { + initializeGridstack({ + events: { + onChange, + onAdd, + }, + isEditMode, + section, + refs: { + items: itemRefs, + wrapper: wrapperRef, + gridstack: gridRef, + }, + sectionColumnCount, + }); + + // Remove event listeners on unmount + return () => { gridRef.current?.off('change'); gridRef.current?.off('added'); }; - - const tilesWithUnknownLocation: TileWithUnknownLocation[] = []; - initializeGridstack( - section.type as any, - wrapperRef, - gridRef, - itemRefs, - section.id, - items, - isEditMode, - wrapperColumnCount, - shapeSize, - tilesWithUnknownLocation, - { - onChange, - onAdd, - } - ); - if (!configName) return removeEventHandlers; - updateConfig(configName, (prev) => ({ - ...prev, - apps: prev.apps.map((app) => { - const currentUnknownLocation = tilesWithUnknownLocation.find( - (x) => x.type === 'app' && x.id === app.id - ); - if (!currentUnknownLocation) return app; - - return { - ...app, - shape: { - ...app.shape, - [shapeSize]: { - location: { - x: currentUnknownLocation.x, - y: currentUnknownLocation.y, - }, - size: { - width: currentUnknownLocation.w, - height: currentUnknownLocation.h, - }, - }, - }, - }; - }), - widgets: prev.widgets.map((widget) => { - const currentUnknownLocation = tilesWithUnknownLocation.find( - (x) => x.type === 'widget' && x.id === widget.id - ); - if (!currentUnknownLocation) return widget; - - return { - ...widget, - shape: { - ...widget.shape, - [shapeSize]: { - location: { - x: currentUnknownLocation.x, - y: currentUnknownLocation.y, - }, - size: { - width: currentUnknownLocation.w, - height: currentUnknownLocation.h, - }, - }, - }, - }; - }), - })); - return removeEventHandlers; - }, [items, wrapperRef.current, wrapperColumnCount]); + }, [items, wrapperRef.current, sectionColumnCount]); return { refs: { diff --git a/src/components/layout/Templates/BoardLayout.tsx b/src/components/layout/Templates/BoardLayout.tsx index 6e9e3c78b..a87899856 100644 --- a/src/components/layout/Templates/BoardLayout.tsx +++ b/src/components/layout/Templates/BoardLayout.tsx @@ -96,7 +96,7 @@ const ToggleEditModeButton = () => { const { enabled, toggleEditMode } = useEditModeStore(); const board = useRequiredBoard(); const { name } = board; - const { mutateAsync: saveConfig } = api.config.save.useMutation(); + //const { mutateAsync: saveConfig } = api.config.save.useMutation(); const namedWrapperColumnCount = useNamedWrapperColumnCount(); const { t } = useTranslation(['layout/header/actions/toggle-edit-mode', 'common']); const translatedSize = @@ -119,7 +119,7 @@ const ToggleEditModeButton = () => { const save = async () => { toggleEditMode(); if (!board || !name) return; - await saveConfig({ name, config: {} as any }); + //await saveConfig({ name, config: {} as any }); Consola.log('Saved config to server', name); hideNotification(editModeNotificationId); }; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index b3ce556fc..c17c2db96 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,18 +1,22 @@ -import { ColorScheme as MantineColorScheme, MantineProvider, MantineTheme } from '@mantine/core'; +import { + type ColorScheme as MantineColorScheme, + MantineProvider, + type MantineTheme, +} from '@mantine/core'; import { ModalsProvider } from '@mantine/modals'; import { Notifications } from '@mantine/notifications'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; -import Consola from 'consola'; +import { consola } from 'consola'; import { getCookie, setCookie } from 'cookies-next'; import dayjs from 'dayjs'; import locale from 'dayjs/plugin/localeData'; import utc from 'dayjs/plugin/utc'; import 'flag-icons/css/flag-icons.min.css'; -import { GetServerSidePropsContext } from 'next'; -import { Session } from 'next-auth'; +import { type GetServerSidePropsContext } from 'next'; +import { type Session } from 'next-auth'; import { SessionProvider, getSession } from 'next-auth/react'; import { appWithTranslation } from 'next-i18next'; -import { AppProps } from 'next/app'; +import { type AppProps } from 'next/app'; import { useEffect, useState } from 'react'; import 'video.js/dist/video-js.css'; import { CommonHead } from '~/components/layout/Meta/CommonHead'; @@ -23,7 +27,7 @@ import { modals } from '~/modals'; import { ColorTheme } from '~/tools/color'; import { getLanguageByCode } from '~/tools/language'; import { - ServerSidePackageAttributesType, + type ServerSidePackageAttributesType, getServiceSidePackageAttributes, } from '~/tools/server/getPackageVersion'; import { theme } from '~/tools/server/theme/theme'; @@ -140,7 +144,7 @@ function App( App.getInitialProps = async ({ ctx }: { ctx: GetServerSidePropsContext }) => { if (env.NEXT_PUBLIC_DEFAULT_COLOR_SCHEME !== 'light') { - Consola.debug( + consola.debug( `Overriding the default color scheme with ${env.NEXT_PUBLIC_DEFAULT_COLOR_SCHEME}` ); } diff --git a/src/pages/manage/boards/index.tsx b/src/pages/manage/boards/index.tsx index d79e73947..54d5eff94 100644 --- a/src/pages/manage/boards/index.tsx +++ b/src/pages/manage/boards/index.tsx @@ -15,6 +15,7 @@ import { useListState } from '@mantine/hooks'; import { IconBox, IconCategory, + IconDatabase, IconDeviceFloppy, IconDotsVertical, IconFolderFilled, @@ -23,7 +24,7 @@ import { IconStarFilled, IconTrash, } from '@tabler/icons-react'; -import { GetServerSideProps } from 'next'; +import { GetServerSideProps, InferGetServerSidePropsType } from 'next'; import { useSession } from 'next-auth/react'; import { useTranslation } from 'next-i18next'; import Head from 'next/head'; @@ -31,6 +32,7 @@ import Link from 'next/link'; import { openCreateBoardModal } from '~/components/Manage/Board/create-board.modal'; import { openDeleteBoardModal } from '~/components/Manage/Board/delete-board.modal'; import { ManageLayout } from '~/components/layout/Templates/ManageLayout'; +import { createTrpcServersideHelpers } from '~/server/api/helper'; import { getServerAuthSession } from '~/server/auth'; import { sleep } from '~/tools/client/time'; import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations'; @@ -38,10 +40,12 @@ import { checkForSessionOrAskForLogin } from '~/tools/server/loginBuilder'; import { manageNamespaces } from '~/tools/server/translation-namespaces'; import { api } from '~/utils/api'; -const BoardsPage = () => { +const BoardsPage = ({ initialBoards }: InferGetServerSidePropsType) => { const context = api.useContext(); const { data: sessionData } = useSession(); - const { data } = api.boards.all.useQuery(); + const { data } = api.boards.all.useQuery(undefined, { + initialData: initialBoards, + }); const { mutateAsync } = api.user.makeDefaultDashboard.useMutation({ onSettled: () => { void context.boards.invalidate(); @@ -91,13 +95,19 @@ const BoardsPage = () => { {board.name} - } - color="pink" - variant="light" - > - {t('cards.badges.fileSystem')} - + {board.type === 'file' ? ( + } + color="pink" + variant="light" + > + {t('cards.badges.fileSystem')} + + ) : ( + } color="blue" variant="light"> + {t('cards.badges.database')} + + )} {board.isDefaultForUser && ( } @@ -209,6 +219,9 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => { return result; } + const helpers = await createTrpcServersideHelpers(ctx); + const initialBoards = await helpers.boards.all.fetch(); + const translations = await getServerSideTranslations( manageNamespaces, ctx.locale, @@ -217,6 +230,7 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => { ); return { props: { + initialBoards, ...translations, }, }; diff --git a/src/server/api/routers/app.ts b/src/server/api/routers/app.ts index 8049df3a9..86c9de658 100644 --- a/src/server/api/routers/app.ts +++ b/src/server/api/routers/app.ts @@ -3,61 +3,70 @@ import axios, { AxiosError } from 'axios'; import Consola from 'consola'; import https from 'https'; import { z } from 'zod'; -import { isStatusOk } from '~/components/Dashboard/Tiles/Apps/AppPing'; import { getConfig } from '~/tools/config/getConfig'; import { AppType } from '~/types/app'; import { createTRPCRouter, publicProcedure } from '../trpc'; export const appRouter = createTRPCRouter({ - ping: publicProcedure.input(z.object({ - id: z.string(), - configName: z.string() - })).query(async ({ input }) => { - const agent = new https.Agent({ rejectUnauthorized: false }); - const config = getConfig(input.configName); - const app = config.apps.find((app) => app.id === input.id); + ping: publicProcedure + .input( + z.object({ + id: z.string(), + configName: z.string(), + }) + ) + .query(async ({ input }) => { + const agent = new https.Agent({ rejectUnauthorized: false }); + const config = getConfig(input.configName); + const app = config.apps.find((app) => app.id === input.id); - if (!app?.url) { - Consola.error(`App ${input} not found`); - throw new TRPCError({ - code: 'NOT_FOUND', - cause: input, - message: `App ${input.id} was not found`, - }); - } - const res = await axios - .get(app.url, { httpsAgent: agent, timeout: 10000 }) - .then((response) => ({ - status: response.status, - statusText: response.statusText, - state: isStatusOk(app as AppType, response.status) ? 'online' : 'offline' - })) - .catch((error: AxiosError) => { - if (error.response) { - return { - state: isStatusOk(app as AppType, error.response.status) ? 'online' : 'offline', - status: error.response.status, - statusText: error.response.statusText, - }; - } - - if (error.code === 'ECONNABORTED') { - Consola.error(`Ping timed out for app with id '${input.id}' in config '${input.configName}' -> url: ${app.url})`); - throw new TRPCError({ - code: 'TIMEOUT', - cause: input, - message: `Ping timed out`, - }); - } - - Consola.error(`Unexpected response: ${error.message}`); + if (!app?.url) { + Consola.error(`App ${input} not found`); throw new TRPCError({ - code: 'UNPROCESSABLE_CONTENT', + code: 'NOT_FOUND', cause: input, - message: `Unexpected response: ${error.message}`, + message: `App ${input.id} was not found`, }); - }); - return res; - }), + } + const res = await axios + .get(app.url, { httpsAgent: agent, timeout: 10000 }) + .then((response) => ({ + status: response.status, + statusText: response.statusText, + state: app.network.statusCodes?.includes(response.status.toString()) + ? 'online' + : 'offline', + })) + .catch((error: AxiosError) => { + if (error.response) { + return { + state: app.network.statusCodes?.includes(error.response.status.toString()) + ? 'online' + : 'offline', + status: error.response.status, + statusText: error.response.statusText, + }; + } + + if (error.code === 'ECONNABORTED') { + Consola.error( + `Ping timed out for app with id '${input.id}' in config '${input.configName}' -> url: ${app.url})` + ); + throw new TRPCError({ + code: 'TIMEOUT', + cause: input, + message: `Ping timed out`, + }); + } + + Consola.error(`Unexpected response: ${error.message}`); + throw new TRPCError({ + code: 'UNPROCESSABLE_CONTENT', + cause: input, + message: `Unexpected response: ${error.message}`, + }); + }); + return res; + }), }); diff --git a/src/server/api/routers/board.ts b/src/server/api/routers/board.ts index afcd25d33..1486c2dfa 100644 --- a/src/server/api/routers/board.ts +++ b/src/server/api/routers/board.ts @@ -28,10 +28,18 @@ import { configNameSchema } from './config'; export const boardRouter = createTRPCRouter({ all: protectedProcedure.query(async ({ ctx }) => { const files = fs.readdirSync('./data/configs').filter((file) => file.endsWith('.json')); + const dbBoards = await db.query.boards.findMany({ + columns: { + name: true, + }, + with: { + items: true, + }, + }); const defaultBoard = await getDefaultBoardAsync(ctx.session.user.id, 'default'); - return await Promise.all( + const result = await Promise.all( files.map(async (file) => { const name = file.replace('.json', ''); const config = await getFrontendConfig(name); @@ -44,9 +52,21 @@ export const boardRouter = createTRPCRouter({ countWidgets: config.widgets.length, countCategories: config.categories.length, isDefaultForUser: name === defaultBoard, + type: 'file', }; }) ); + + return result.concat( + dbBoards.map((x) => ({ + name: x.name, + countApps: x.items.filter((x) => x.type === 'app').length, + countWidgets: x.items.filter((x) => x.type === 'widget').length, + countCategories: 0, // TODO: Is different depending on layout + isDefaultForUser: x.name === defaultBoard, + type: 'db', + })) + ); }), addAppsForContainers: adminProcedure .input( @@ -514,7 +534,7 @@ const mapSection = ( return { ...sectionProps, type, - position: section.position === 0 ? ('left' as const) : ('right' as const), + position: position === 0 ? ('left' as const) : ('right' as const), items, }; }; diff --git a/src/server/api/routers/media-request.ts b/src/server/api/routers/media-request.ts index 65dc38d6b..dbb87ac13 100644 --- a/src/server/api/routers/media-request.ts +++ b/src/server/api/routers/media-request.ts @@ -1,20 +1,28 @@ import Consola from 'consola'; +import { removeTrailingSlash } from 'next/dist/shared/lib/router/utils/remove-trailing-slash'; import { z } from 'zod'; import { checkIntegrationsType } from '~/tools/client/app-properties'; import { getConfig } from '~/tools/config/getConfig'; -import { MediaRequestListWidget } from '~/widgets/media-requests/MediaRequestListTile'; +import { + MediaRequestListWidget, + MediaRequestListWidgetOptions, +} from '~/widgets/media-requests/MediaRequestListTile'; +import { + MediaRequestStatsWidget, + MediaRequestStatsWidgetOptions, +} from '~/widgets/media-requests/MediaRequestStatsTile'; import { MediaRequest, Users } from '~/widgets/media-requests/media-request-types'; import { createTRPCRouter, publicProcedure } from '../trpc'; -import { MediaRequestStatsWidget } from '~/widgets/media-requests/MediaRequestStatsTile'; -import { removeTrailingSlash } from 'next/dist/shared/lib/router/utils/remove-trailing-slash'; export const mediaRequestsRouter = createTRPCRouter({ allMedia: publicProcedure .input( z.object({ configName: z.string(), - widget: z.custom().or(z.custom()), + options: z + .custom() + .or(z.custom()), }) ) .query(async ({ input }) => { @@ -33,9 +41,10 @@ export const mediaRequestsRouter = createTRPCRouter({ }) .then(async (response) => { const body = (await response.json()) as OverseerrResponse; - let appUrl = input.widget.properties.replaceLinksWithExternalHost && app.behaviour.externalUrl?.length > 0 - ? app.behaviour.externalUrl - : app.url; + let appUrl = + input.options.replaceLinksWithExternalHost && app.behaviour.externalUrl?.length > 0 + ? app.behaviour.externalUrl + : app.url; appUrl = removeTrailingSlash(appUrl); @@ -86,7 +95,9 @@ export const mediaRequestsRouter = createTRPCRouter({ .input( z.object({ configName: z.string(), - widget: z.custom().or(z.custom()), + options: z + .custom() + .or(z.custom()), }) ) .query(async ({ input }) => { @@ -105,7 +116,7 @@ export const mediaRequestsRouter = createTRPCRouter({ }) .then(async (response) => { const body = (await response.json()) as OverseerrUsers; - const appUrl = input.widget.properties.replaceLinksWithExternalHost + const appUrl = input.options.replaceLinksWithExternalHost ? app.behaviour.externalUrl : app.url; @@ -163,7 +174,7 @@ const retrieveDetailsForItem = async ( backdropPath: series.backdropPath, posterPath: series.backdropPath, }; - }; + } const movieResponse = await fetch(`${baseUrl}/api/v1/movie/${id}`, { headers, diff --git a/src/server/api/routers/notebook.ts b/src/server/api/routers/notebook.ts index 1209e6986..a5bcf6d13 100644 --- a/src/server/api/routers/notebook.ts +++ b/src/server/api/routers/notebook.ts @@ -16,7 +16,7 @@ export const notebookRouter = createTRPCRouter({ if (process.env.DISABLE_EDIT_MODE?.toLowerCase() === 'true') { throw new TRPCError({ code: 'METHOD_NOT_SUPPORTED', - message: 'Edit is not allowed, because edit mode is disabled' + message: 'Edit is not allowed, because edit mode is disabled', }); } @@ -32,14 +32,15 @@ export const notebookRouter = createTRPCRouter({ }); } - widget.properties.content = input.content; + widget.options.content = input.content; - const newConfig: BackendConfigType = { + // TODO: Make this work + /*const newConfig: BackendConfigType = { ...config, widgets: [...config.widgets.filter((w) => w.id !== widget.id), widget], }; const targetPath = path.join('data/configs', `${input.configName}.json`); - fs.writeFileSync(targetPath, JSON.stringify(newConfig, null, 2), 'utf8'); + fs.writeFileSync(targetPath, JSON.stringify(newConfig, null, 2), 'utf8');*/ }), }); diff --git a/src/server/api/routers/rss.ts b/src/server/api/routers/rss.ts index e62dc0e3e..f71e5c540 100644 --- a/src/server/api/routers/rss.ts +++ b/src/server/api/routers/rss.ts @@ -6,7 +6,7 @@ import xss from 'xss'; import { z } from 'zod'; import { getConfig } from '~/tools/config/getConfig'; import { Stopwatch } from '~/tools/shared/time/stopwatch.tool'; -import { IRssWidget } from '~/widgets/rss/RssWidgetTile'; +import { IRssWidget, RssWidgetOptions } from '~/widgets/rss/RssWidgetTile'; import { createTRPCRouter, publicProcedure } from '../trpc'; @@ -58,11 +58,11 @@ export const rssRouter = createTRPCRouter({ .query(async ({ input }) => { const config = getConfig(input.configName); - const rssWidget = config.widgets.find((x) => x.type === 'rss' && x.id === input.widgetId) as - | IRssWidget - | undefined; + const rssWidgetOptions = config.widgets.find( + (x) => x.type === 'rss' && x.id === input.widgetId + )?.properties as RssWidgetOptions | undefined; - if (!rssWidget) { + if (!rssWidgetOptions) { throw new TRPCError({ code: 'NOT_FOUND', message: 'required widget does not exist', @@ -70,12 +70,12 @@ export const rssRouter = createTRPCRouter({ } if (input.feedUrls.length === 0) { - return [] + return []; } const result = await Promise.all( input.feedUrls.map(async (feedUrl) => - getFeedUrl(feedUrl, rssWidget.properties.dangerousAllowSanitizedItemContent) + getFeedUrl(feedUrl, rssWidgetOptions.dangerousAllowSanitizedItemContent) ) ); diff --git a/src/widgets/bookmark/BookmarkWidgetTile.tsx b/src/widgets/bookmark/BookmarkWidgetTile.tsx index 01b74b405..c1e6b7cb5 100644 --- a/src/widgets/bookmark/BookmarkWidgetTile.tsx +++ b/src/widgets/bookmark/BookmarkWidgetTile.tsx @@ -3,11 +3,11 @@ import { Box, Button, Card, + Divider, Flex, Group, Image, ScrollArea, - Divider, Stack, Switch, Text, @@ -27,14 +27,14 @@ import { } from '@tabler/icons-react'; import { useTranslation } from 'next-i18next'; import { useEffect } from 'react'; +import React from 'react'; import { v4 } from 'uuid'; import { z } from 'zod'; -import React from 'react'; - import { useEditModeStore } from '~/components/Dashboard/Views/useEditModeStore'; import { IconSelector } from '~/components/IconSelector/IconSelector'; + import { defineWidget } from '../helper'; -import { IDraggableEditableListInputValue, IWidget } from '../widgets'; +import { IDraggableEditableListInputValue, IWidget, InferWidget } from '../widgets'; interface BookmarkItem { id: string; @@ -54,7 +54,7 @@ const definition = defineWidget({ type: 'text', defaultValue: '', info: true, - infoLink: "https://homarr.dev/docs/widgets/bookmarks/", + infoLink: 'https://homarr.dev/docs/widgets/bookmarks/', }, items: { type: 'draggable-editable-list', @@ -84,11 +84,11 @@ const definition = defineWidget({ return undefined; } - return t('item.validation.length', {shortest: "1", longest: "100"}); + return t('item.validation.length', { shortest: '1', longest: '100' }); }, href: (value) => { if (!z.string().min(1).max(200).safeParse(value).success) { - return t('item.validation.length', {shortest: "1", longest: "200"}); + return t('item.validation.length', { shortest: '1', longest: '200' }); } if (!z.string().url().safeParse(value).success) { @@ -102,7 +102,7 @@ const definition = defineWidget({ return undefined; } - return t('item.validation.length', {shortest: "1", longest: "400"}); + return t('item.validation.length', { shortest: '1', longest: '400' }); }, }, validateInputOnChange: true, @@ -174,11 +174,7 @@ const definition = defineWidget({ } satisfies IDraggableEditableListInputValue, layout: { type: 'select', - data: [ - { value: 'autoGrid', }, - { value: 'horizontal', }, - { value: 'vertical', }, - ], + data: [{ value: 'autoGrid' }, { value: 'horizontal' }, { value: 'vertical' }], defaultValue: 'autoGrid', }, }, @@ -191,7 +187,7 @@ const definition = defineWidget({ component: BookmarkWidgetTile, }); -export type IBookmarkWidget = IWidget<(typeof definition)['id'], typeof definition>; +export type IBookmarkWidget = InferWidget; interface BookmarkWidgetTileProps { widget: IBookmarkWidget; @@ -203,7 +199,7 @@ function BookmarkWidgetTile({ widget }: BookmarkWidgetTileProps) { const { fn, colors, colorScheme } = useMantineTheme(); const { t } = useTranslation('modules/bookmark'); - if (widget.properties.items.length === 0) { + if (widget.options.items.length === 0) { return ( @@ -219,17 +215,19 @@ function BookmarkWidgetTile({ widget }: BookmarkWidgetTileProps) { ); } - switch (widget.properties.layout) { + switch (widget.options.layout) { case 'autoGrid': return ( - {widget.properties.name} + + {widget.options.name} + - {widget.properties.items.map((item: BookmarkItem, index) => ( + {widget.options.items.map((item: BookmarkItem, index) => ( @@ -257,38 +257,38 @@ function BookmarkWidgetTile({ widget }: BookmarkWidgetTileProps) { return ( - {widget.properties.name} + {widget.options.name} - {widget.properties.items.map((item: BookmarkItem, index) => ( + {widget.options.items.map((item: BookmarkItem, index) => ( <> - {index > 0 && + {index > 0 && ( - } + )} - + ))} @@ -321,26 +321,28 @@ function BookmarkWidgetTile({ widget }: BookmarkWidgetTileProps) { const BookmarkItemContent = ({ item }: { item: BookmarkItem }) => { const { colorScheme } = useMantineTheme(); return ( - - - - {item.name} - - - -)}; + + + + {item.name} + + + + ); +}; const useStyles = createStyles(() => ({ grid: { diff --git a/src/widgets/boundary.tsx b/src/widgets/boundary.tsx index c023291fc..cd55f0323 100644 --- a/src/widgets/boundary.tsx +++ b/src/widgets/boundary.tsx @@ -4,8 +4,9 @@ import { IconBrandGithub, IconBug, IconInfoCircle, IconRefresh } from '@tabler/i import Consola from 'consola'; import { withTranslation } from 'next-i18next'; import React, { ReactNode } from 'react'; - +import { WidgetItem } from '~/components/Board/context'; import { WidgetsMenu } from '~/components/Dashboard/Tiles/Widgets/WidgetsMenu'; + import { IWidget } from './widgets'; type ErrorBoundaryState = { @@ -17,7 +18,7 @@ type ErrorBoundaryProps = { t: (key: string) => string; children: ReactNode; integration: string; - widget: IWidget; + widget: WidgetItem; }; /** diff --git a/src/widgets/calendar/CalendarDay.tsx b/src/widgets/calendar/CalendarDay.tsx index e3b6563fc..851e5a973 100644 --- a/src/widgets/calendar/CalendarDay.tsx +++ b/src/widgets/calendar/CalendarDay.tsx @@ -1,11 +1,4 @@ -import { - Button, - Container, - Indicator, - IndicatorProps, - Popover, - useMantineTheme, -} from '@mantine/core'; +import { Container, Indicator, IndicatorProps, Popover, useMantineTheme } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; import { MediaList } from './MediaList'; diff --git a/src/widgets/calendar/CalendarTile.tsx b/src/widgets/calendar/CalendarTile.tsx index b1951ed89..e41879776 100644 --- a/src/widgets/calendar/CalendarTile.tsx +++ b/src/widgets/calendar/CalendarTile.tsx @@ -9,7 +9,7 @@ import { getLanguageByCode } from '~/tools/language'; import { RouterOutputs, api } from '~/utils/api'; import { defineWidget } from '../helper'; -import { IWidget } from '../widgets'; +import { IWidget, InferWidget } from '../widgets'; import { CalendarDay } from './CalendarDay'; import { getBgColorByDateAndTheme } from './bg-calculator'; import { MediasType } from './type'; @@ -50,7 +50,7 @@ const definition = defineWidget({ component: CalendarTile, }); -export type ICalendarWidget = IWidget<(typeof definition)['id'], typeof definition>; +export type ICalendarWidget = InferWidget; interface CalendarTileProps { widget: ICalendarWidget; @@ -74,8 +74,8 @@ function CalendarTile({ widget }: CalendarTileProps) { month: month.getMonth() + 1, year: month.getFullYear(), options: { - useSonarrv4: widget.properties.useSonarrv4, - showUnmonitored: widget.properties.showUnmonitored, + useSonarrv4: widget.options.useSonarrv4, + showUnmonitored: widget.options.showUnmonitored, }, }, { @@ -91,10 +91,10 @@ function CalendarTile({ widget }: CalendarTileProps) { defaultDate={new Date()} onPreviousMonth={setMonth} onNextMonth={setMonth} - size={widget.properties.fontSize} + size={widget.options.fontSize} locale={language.locale} firstDayOfWeek={getFirstDayOfWeek(firstDayOfWeek)} - hideWeekdays={widget.properties.hideWeekDays} + hideWeekdays={widget.options.hideWeekDays} style={{ position: 'relative' }} date={month} maxLevel="month" @@ -132,7 +132,7 @@ function CalendarTile({ widget }: CalendarTileProps) { flex: 1, }, day: { - borderRadius: ['xs', 'sm'].includes(widget.properties.fontSize) ? radius.md : radius.lg, + borderRadius: ['xs', 'sm'].includes(widget.options.fontSize) ? radius.md : radius.lg, }, }} getDayProps={(date) => ({ @@ -142,7 +142,7 @@ function CalendarTile({ widget }: CalendarTileProps) { )} /> @@ -161,7 +161,7 @@ const getReleasedMediasForDate = ( date: Date, widget: ICalendarWidget ): MediasType => { - const { radarrReleaseType } = widget.properties; + const { radarrReleaseType } = widget.options; const books = medias?.books.filter((b) => new Date(b.releaseDate).toDateString() === date.toDateString()) ?? diff --git a/src/widgets/dashDot/DashDotTile.tsx b/src/widgets/dashDot/DashDotTile.tsx index 7a1d873fb..0736504cf 100644 --- a/src/widgets/dashDot/DashDotTile.tsx +++ b/src/widgets/dashDot/DashDotTile.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'next-i18next'; import { api } from '~/utils/api'; import { defineWidget } from '../helper'; -import { IWidget } from '../widgets'; +import { IWidget, InferWidget } from '../widgets'; import { DashDotGraph } from './DashDotGraph'; const definition = defineWidget({ @@ -144,7 +144,7 @@ const definition = defineWidget({ component: DashDotTile, }); -export type IDashDotTile = IWidget<(typeof definition)['id'], typeof definition>; +export type IDashDotTile = InferWidget; interface DashDotTileProps { widget: IDashDotTile; @@ -154,7 +154,7 @@ function DashDotTile({ widget }: DashDotTileProps) { const { classes } = useDashDotTileStyles(); const { t } = useTranslation('modules/dashdot'); - const dashDotUrl = widget.properties.url; + const dashDotUrl = widget.options.url; const locationProtocol = window.location.protocol; const detectedProtocolDowngrade = locationProtocol === 'https:' && dashDotUrl.toLowerCase().startsWith('http:'); @@ -178,7 +178,7 @@ function DashDotTile({ widget }: DashDotTileProps) { ); } - const { dashName, graphsOrder, usePercentages, columns, graphHeight } = widget.properties; + const { dashName, graphsOrder, usePercentages, columns, graphHeight } = widget.options; return ( diff --git a/src/widgets/dnshole/DnsHoleControls.tsx b/src/widgets/dnshole/DnsHoleControls.tsx index 664088088..8477b5f30 100644 --- a/src/widgets/dnshole/DnsHoleControls.tsx +++ b/src/widgets/dnshole/DnsHoleControls.tsx @@ -14,12 +14,12 @@ import { useElementSize } from '@mantine/hooks'; import { IconDeviceGamepad, IconPlayerPlay, IconPlayerStop } from '@tabler/icons-react'; import { useSession } from 'next-auth/react'; import { useTranslation } from 'next-i18next'; +import { useConfigContext } from '~/config/provider'; import { api } from '~/utils/api'; -import { useConfigContext } from '~/config/provider'; import { defineWidget } from '../helper'; import { WidgetLoading } from '../loading'; -import { IWidget } from '../widgets'; +import { IWidget, InferWidget } from '../widgets'; import { useDnsHoleSummeryQuery } from './DnsHoleSummary'; const definition = defineWidget({ @@ -40,7 +40,7 @@ const definition = defineWidget({ component: DnsHoleControlsWidgetTile, }); -export type IDnsHoleControlsWidget = IWidget<(typeof definition)['id'], typeof definition>; +export type IDnsHoleControlsWidget = InferWidget; interface DnsHoleControlsWidgetProps { widget: IDnsHoleControlsWidget; @@ -111,7 +111,7 @@ function DnsHoleControlsWidgetTile({ widget }: DnsHoleControlsWidgetProps) { return ( - {sessionData?.user?.isAdmin && widget.properties.showToggleAllButtons && ( + {sessionData?.user?.isAdmin && widget.options.showToggleAllButtons && ( 275 ? 2 : 1} @@ -120,15 +120,18 @@ function DnsHoleControlsWidgetTile({ widget }: DnsHoleControlsWidgetProps) { >