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}
-
- {new URL(item.href).hostname}
-
-
-
-)};
+
+
+
+ {item.name}
+
+ {new URL(item.href).hostname}
+
+
+
+ );
+};
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) {
>
+ >
{item.name}
@@ -180,53 +183,54 @@ function MediaRequestListTile({ widget }: MediaRequestListWidgetProps) {
/>
{item.userName}
- {item.status === MediaRequestStatus.PendingApproval && sessionData?.user?.isAdmin && (
-
-
- {
- notifications.show({
- id: `approve ${item.id}`,
- color: 'yellow',
- title: t('tooltips.approving'),
- message: undefined,
- loading: true,
- });
+ {item.status === MediaRequestStatus.PendingApproval &&
+ sessionData?.user?.isAdmin && (
+
+
+ {
+ notifications.show({
+ id: `approve ${item.id}`,
+ color: 'yellow',
+ title: t('tooltips.approving'),
+ message: undefined,
+ loading: true,
+ });
- await decideAsync({
- request: item,
- isApproved: true,
- });
- }}
- >
-
-
-
-
- {
- await decideAsync({
- request: item,
- isApproved: false,
- });
- }}
- >
-
-
-
-
- )}
+ await decideAsync({
+ request: item,
+ isApproved: true,
+ });
+ }}
+ >
+
+
+
+
+ {
+ await decideAsync({
+ request: item,
+ isApproved: false,
+ });
+ }}
+ >
+
+
+
+
+ )}
diff --git a/src/widgets/media-requests/MediaRequestStatsTile.tsx b/src/widgets/media-requests/MediaRequestStatsTile.tsx
index b8bfc9acf..0abcc1a94 100644
--- a/src/widgets/media-requests/MediaRequestStatsTile.tsx
+++ b/src/widgets/media-requests/MediaRequestStatsTile.tsx
@@ -15,7 +15,7 @@ import { useTranslation } from 'next-i18next';
import { defineWidget } from '../helper';
import { WidgetLoading } from '../loading';
-import { IWidget } from '../widgets';
+import { InferWidget, InferWidgetOptions } from '../widgets';
import { useMediaRequestQuery, useUsersQuery } from './media-request-query';
import { MediaRequestStatus } from './media-request-types';
@@ -41,7 +41,8 @@ const definition = defineWidget({
component: MediaRequestStatsTile,
});
-export type MediaRequestStatsWidget = IWidget<(typeof definition)['id'], typeof definition>;
+export type MediaRequestStatsWidget = InferWidget;
+export type MediaRequestStatsWidgetOptions = InferWidgetOptions;
interface MediaRequestStatsWidgetProps {
widget: MediaRequestStatsWidget;
@@ -57,7 +58,7 @@ function MediaRequestStatsTile({ widget }: MediaRequestStatsWidgetProps) {
const {
data: usersData,
isFetching: usersFetching,
- isLoading: usersLoading
+ isLoading: usersLoading,
} = useUsersQuery(widget);
const { ref, height } = useElementSize();
const { colorScheme } = useMantineTheme();
@@ -128,7 +129,7 @@ function MediaRequestStatsTile({ widget }: MediaRequestStatsWidgetProps) {
p={0}
component="a"
href={user.userLink}
- target={widget.properties.openInNewTab ? "_blank" : "_self"}
+ target={widget.options.openInNewTab ? '_blank' : '_self'}
mah={95}
mih={55}
radius="md"
diff --git a/src/widgets/media-requests/media-request-query.tsx b/src/widgets/media-requests/media-request-query.tsx
index 15a149da0..81ed77e1d 100644
--- a/src/widgets/media-requests/media-request-query.tsx
+++ b/src/widgets/media-requests/media-request-query.tsx
@@ -1,22 +1,23 @@
import { useConfigContext } from '~/config/provider';
-import { MediaRequestListWidget } from './MediaRequestListTile';
-import { MediaRequestStatsWidget } from './MediaRequestStatsTile';
import { api } from '~/utils/api';
-export const useMediaRequestQuery = (widget: MediaRequestListWidget|MediaRequestStatsWidget) => {
+import { MediaRequestListWidget } from './MediaRequestListTile';
+import { MediaRequestStatsWidget } from './MediaRequestStatsTile';
+
+export const useMediaRequestQuery = (widget: MediaRequestListWidget | MediaRequestStatsWidget) => {
const { name: configName } = useConfigContext();
return api.mediaRequest.allMedia.useQuery(
- { configName: configName!, widget: widget },
+ { configName: configName!, options: widget.options },
{
refetchInterval: 3 * 60 * 1000,
}
);
};
-export const useUsersQuery = (widget: MediaRequestListWidget|MediaRequestStatsWidget) => {
+export const useUsersQuery = (widget: MediaRequestListWidget | MediaRequestStatsWidget) => {
const { name: configName } = useConfigContext();
return api.mediaRequest.users.useQuery(
- { configName: configName!, widget: widget },
+ { configName: configName!, options: widget.options },
{
refetchInterval: 3 * 60 * 1000,
}
diff --git a/src/widgets/media-server/MediaServerTile.tsx b/src/widgets/media-server/MediaServerTile.tsx
index bbb347e36..0ed15aac8 100644
--- a/src/widgets/media-server/MediaServerTile.tsx
+++ b/src/widgets/media-server/MediaServerTile.tsx
@@ -11,13 +11,13 @@ import {
} from '@mantine/core';
import { IconAlertTriangle, IconMovie } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
-
import { AppAvatar } from '~/components/AppAvatar';
import { useConfigContext } from '~/config/provider';
-import { useGetMediaServers } from './useGetMediaServers';
+
import { defineWidget } from '../helper';
-import { IWidget } from '../widgets';
+import { IWidget, InferWidget } from '../widgets';
import { TableRow } from './TableRow';
+import { useGetMediaServers } from './useGetMediaServers';
const definition = defineWidget({
id: 'media-server',
@@ -32,7 +32,7 @@ const definition = defineWidget({
},
});
-export type MediaServerWidget = IWidget<(typeof definition)['id'], typeof definition>;
+export type MediaServerWidget = InferWidget;
interface MediaServerWidgetProps {
widget: MediaServerWidget;
diff --git a/src/widgets/rss/RssWidgetTile.tsx b/src/widgets/rss/RssWidgetTile.tsx
index 233b04dbd..68f0d7459 100644
--- a/src/widgets/rss/RssWidgetTile.tsx
+++ b/src/widgets/rss/RssWidgetTile.tsx
@@ -22,7 +22,7 @@ import { useConfigContext } from '~/config/provider';
import { api } from '~/utils/api';
import { defineWidget } from '../helper';
-import { IWidget } from '../widgets';
+import { IWidget, InferWidget, InferWidgetOptions } from '../widgets';
const definition = defineWidget({
id: 'rss',
@@ -61,7 +61,8 @@ const definition = defineWidget({
component: RssTile,
});
-export type IRssWidget = IWidget<(typeof definition)['id'], typeof definition>;
+export type IRssWidget = InferWidget;
+export type RssWidgetOptions = InferWidgetOptions;
interface RssTileProps {
widget: IRssWidget;
@@ -72,8 +73,8 @@ function RssTile({ widget }: RssTileProps) {
const { name: configName } = useConfigContext();
const { data, isLoading, isFetching, isError, refetch } = useGetRssFeeds(
configName,
- widget.properties.rssFeedUrl,
- widget.properties.refreshInterval,
+ widget.options.rssFeedUrl,
+ widget.options.refreshInterval,
widget.id
);
const { classes } = useStyles();
@@ -164,7 +165,7 @@ function RssTile({ widget }: RssTileProps) {
className={classes.itemContent}
color="dimmed"
size="xs"
- lineClamp={widget.properties.textLinesClamp}
+ lineClamp={widget.options.textLinesClamp}
dangerouslySetInnerHTML={{ __html: item.content }}
/>
diff --git a/src/widgets/torrent/TorrentTile.tsx b/src/widgets/torrent/TorrentTile.tsx
index c9b6a738e..9ac212970 100644
--- a/src/widgets/torrent/TorrentTile.tsx
+++ b/src/widgets/torrent/TorrentTile.tsx
@@ -18,13 +18,13 @@ import duration from 'dayjs/plugin/duration';
import relativeTime from 'dayjs/plugin/relativeTime';
import { useTranslation } from 'next-i18next';
import { useCardStyles } from '~/components/layout/Common/useCardStyles';
-
import { MIN_WIDTH_MOBILE } from '~/constants/constants';
import { NormalizedDownloadQueueResponse } from '~/types/api/downloads/queue/NormalizedDownloadQueueResponse';
import { AppIntegrationType } from '~/types/app';
+
import { useGetDownloadClientsQueue } from '../download-speed/useGetNetworkSpeed';
import { defineWidget } from '../helper';
-import { IWidget } from '../widgets';
+import { IWidget, InferWidget } from '../widgets';
import { BitTorrentQueueItem } from './TorrentQueueItem';
dayjs.extend(duration);
@@ -62,7 +62,7 @@ const definition = defineWidget({
component: TorrentTile,
});
-export type ITorrent = IWidget<(typeof definition)['id'], typeof definition>;
+export type ITorrent = InferWidget;
interface TorrentTileProps {
widget: ITorrent;
@@ -193,15 +193,15 @@ function TorrentTile({ widget }: TorrentTileProps) {
export const filterTorrents = (widget: ITorrent, torrents: NormalizedTorrent[]) => {
let result = torrents;
- if (!widget.properties.displayCompletedTorrents) {
+ if (!widget.options.displayCompletedTorrents) {
result = result.filter((torrent) => !torrent.isCompleted);
}
- if (widget.properties.labelFilter.length > 0) {
+ if (widget.options.labelFilter.length > 0) {
result = filterTorrentsByLabels(
result,
- widget.properties.labelFilter,
- widget.properties.labelFilterIsWhitelist
+ widget.options.labelFilter,
+ widget.options.labelFilterIsWhitelist
);
}
@@ -211,7 +211,7 @@ export const filterTorrents = (widget: ITorrent, torrents: NormalizedTorrent[])
};
const filterStaleTorrent = (widget: ITorrent, torrents: NormalizedTorrent[]) => {
- if (widget.properties.displayStaleTorrents) {
+ if (widget.options.displayStaleTorrents) {
return torrents;
}
diff --git a/src/widgets/useNet/UseNetTile.tsx b/src/widgets/useNet/UseNetTile.tsx
index 1bd91602c..07a40a2cf 100644
--- a/src/widgets/useNet/UseNetTile.tsx
+++ b/src/widgets/useNet/UseNetTile.tsx
@@ -6,18 +6,18 @@ import duration from 'dayjs/plugin/duration';
import { useSession } from 'next-auth/react';
import { useTranslation } from 'next-i18next';
import { useEffect, useState } from 'react';
-
import { useConfigContext } from '~/config/provider';
import { MIN_WIDTH_MOBILE } from '~/constants/constants';
import { humanFileSize } from '~/tools/humanFileSize';
import { AppIntegrationType } from '~/types/app';
+
import {
useGetUsenetInfo,
usePauseUsenetQueueMutation,
useResumeUsenetQueueMutation,
} from '../dashDot/api';
import { defineWidget } from '../helper';
-import { IWidget } from '../widgets';
+import { InferWidget } from '../widgets';
import { UsenetHistoryList } from './UsenetHistoryList';
import { UsenetQueueList } from './UsenetQueueList';
@@ -38,7 +38,7 @@ const definition = defineWidget({
},
});
-export type IUsenetWidget = IWidget<(typeof definition)['id'], typeof definition>;
+export type IUsenetWidget = InferWidget;
interface UseNetTileProps {
widget: IUsenetWidget;
diff --git a/src/widgets/video/VideoStreamTile.tsx b/src/widgets/video/VideoStreamTile.tsx
index a3bbfc7c1..873ec2eaf 100644
--- a/src/widgets/video/VideoStreamTile.tsx
+++ b/src/widgets/video/VideoStreamTile.tsx
@@ -4,7 +4,7 @@ import { useTranslation } from 'next-i18next';
import dynamic from 'next/dynamic';
import { defineWidget } from '../helper';
-import { IWidget } from '../widgets';
+import { IWidget, InferWidget } from '../widgets';
const VideoFeed = dynamic(() => import('./VideoFeed'), { ssr: false });
@@ -38,7 +38,7 @@ const definition = defineWidget({
component: VideoStreamWidget,
});
-export type VideoStreamWidget = IWidget<(typeof definition)['id'], typeof definition>;
+export type VideoStreamWidget = InferWidget;
interface VideoStreamWidgetProps {
widget: VideoStreamWidget;
@@ -46,7 +46,7 @@ interface VideoStreamWidgetProps {
function VideoStreamWidget({ widget }: VideoStreamWidgetProps) {
const { t } = useTranslation('modules/video-stream');
- if (!widget.properties.FeedUrl) {
+ if (!widget.options.FeedUrl) {
return (
@@ -59,10 +59,10 @@ function VideoStreamWidget({ widget }: VideoStreamWidgetProps) {
return (
);
diff --git a/src/widgets/widgets.ts b/src/widgets/widgets.ts
index 6c2723b62..ff69abf6e 100644
--- a/src/widgets/widgets.ts
+++ b/src/widgets/widgets.ts
@@ -26,11 +26,13 @@ export type IWidget
};
export type InferWidget = WidgetItem & {
- options: {
- [key in keyof TDefinition['options']]: MakeLessSpecific<
- TDefinition['options'][key]['defaultValue']
- >;
- };
+ options: InferWidgetOptions;
+};
+
+export type InferWidgetOptions = {
+ [key in keyof TDefinition['options']]: MakeLessSpecific<
+ TDefinition['options'][key]['defaultValue']
+ >;
};
// Makes the type less specific