Merge pull request #788 from ajnart/allow-multiple-widgets-of-same-type

Allow multiple widgets of same type
This commit is contained in:
Thomas Camlong
2023-04-04 20:29:59 +09:00
committed by GitHub
27 changed files with 590 additions and 497 deletions

View File

@@ -1,387 +1,389 @@
{ {
"schemaVersion": 1, "schemaVersion": 1,
"configProperties": { "configProperties": {
"name": "default" "name": "default"
}, },
"categories": [ "categories": [
{ {
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f", "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f",
"position": 1, "position": 1,
"name": "Welcome to Homarr 🎉", "name": "Welcome to Homarr 🎉",
"type": "category" "type": "category"
}
],
"wrappers": [
{
"id": "default",
"position": 0
},
{
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a326",
"position": 1
}
],
"apps": [
{
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a337",
"name": "Discord",
"url": "https://discord.com/invite/aCsmEV5RgA",
"behaviour": {
"onClickUrl": "https://discord.com/invite/aCsmEV5RgA",
"isOpeningNewTab": true,
"externalUrl": "https://discord.com/invite/aCsmEV5RgA"
},
"network": {
"enabledStatusChecker": false,
"okStatus": [
200
]
},
"appearance": {
"iconUrl": "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/discord.png"
},
"integration": {
"type": null,
"properties": []
},
"area": {
"type": "category",
"properties": {
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
}
},
"shape": {
"md": {
"location": {
"x": 3,
"y": 1
},
"size": {
"width": 3,
"height": 1
}
},
"sm": {
"location": {
"x": 2,
"y": 1
},
"size": {
"width": 1,
"height": 1
}
},
"lg": {
"location": {
"x": 2,
"y": 1
},
"size": {
"width": 1,
"height": 1
}
}
}
},
{
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a990",
"name": "Donate",
"url": "https://ko-fi.com/ajnart",
"behaviour": {
"onClickUrl": "https://ko-fi.com/ajnart",
"externalUrl": "https://ko-fi.com/ajnart",
"isOpeningNewTab": true
},
"network": {
"enabledStatusChecker": false,
"okStatus": [
200
]
},
"appearance": {
"iconUrl": "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/ko-fi.png"
},
"integration": {
"type": null,
"properties": []
},
"area": {
"type": "category",
"properties": {
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
}
},
"shape": {
"md": {
"location": {
"x": 2,
"y": 1
},
"size": {
"width": 1,
"height": 1
}
},
"sm": {
"location": {
"x": 2,
"y": 2
},
"size": {
"width": 1,
"height": 1
}
},
"lg": {
"location": {
"x": 3,
"y": 1
},
"size": {
"width": 1,
"height": 1
}
}
}
},
{
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a330",
"name": "Contribute",
"url": "https://github.com/ajnart/homarr",
"behaviour": {
"onClickUrl": "https://github.com/ajnart/homarr",
"externalUrl": "https://github.com/ajnart/homarr",
"isOpeningNewTab": true
},
"network": {
"enabledStatusChecker": false,
"okStatus": []
},
"appearance": {
"iconUrl": "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/github.png"
},
"integration": {
"type": null,
"properties": []
},
"area": {
"type": "category",
"properties": {
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
}
},
"shape": {
"md": {
"location": {
"x": 2,
"y": 0
},
"size": {
"width": 2,
"height": 1
}
},
"sm": {
"location": {
"x": 0,
"y": 2
},
"size": {
"width": 2,
"height": 1
}
},
"lg": {
"location": {
"x": 4,
"y": 0
},
"size": {
"width": 2,
"height": 2
}
}
}
},
{
"id": "5df743d9-5cb1-457c-85d2-64ff86855652",
"name": "Documentation",
"url": "https://homarr.dev",
"behaviour": {
"onClickUrl": "https://homarr.dev",
"externalUrl": "https://homarr.dev",
"isOpeningNewTab": true
},
"network": {
"enabledStatusChecker": false,
"okStatus": [
200
]
},
"appearance": {
"iconUrl": "/imgs/logo/logo.png"
},
"integration": {
"type": null,
"properties": []
},
"area": {
"type": "category",
"properties": {
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
}
},
"shape": {
"md": {
"location": {
"x": 0,
"y": 1
},
"size": {
"width": 2,
"height": 1
}
},
"sm": {
"location": {
"x": 0,
"y": 0
},
"size": {
"width": 1,
"height": 1
}
},
"lg": {
"location": {
"x": 0,
"y": 1
},
"size": {
"width": 2,
"height": 1
}
}
}
}
],
"widgets": [
{
"id": "971aa859-8570-49a1-8d34-dd5c7b3638d1",
"type": "date",
"properties": {
"display24HourFormat": true
},
"area": {
"type": "category",
"properties": {
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
}
},
"shape": {
"sm": {
"location": {
"x": 0,
"y": 1
},
"size": {
"width": 2,
"height": 1
}
},
"md": {
"location": {
"x": 4,
"y": 0
},
"size": {
"width": 2,
"height": 1
}
},
"lg": {
"location": {
"x": 2,
"y": 0
},
"size": {
"width": 2,
"height": 1
}
}
}
},
{
"id": "e3004052-6b83-480e-b458-56e8ccdca5f0",
"type": "weather",
"properties": {
"displayInFahrenheit": false,
"location": "Paris"
},
"area": {
"type": "category",
"properties": {
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
}
},
"shape": {
"md": {
"location": {
"x": 0,
"y": 0
},
"size": {
"width": 2,
"height": 1
}
},
"sm": {
"location": {
"x": 1,
"y": 0
},
"size": {
"width": 2,
"height": 1
}
},
"lg": {
"location": {
"x": 0,
"y": 0
},
"size": {
"width": 2,
"height": 1
}
}
}
}
],
"settings": {
"common": {
"searchEngine": {
"type": "google",
"properties": {}
}
},
"customization": {
"layout": {
"enabledLeftSidebar": false,
"enabledRightSidebar": false,
"enabledDocker": false,
"enabledPing": false,
"enabledSearchbar": true
},
"pageTitle": "Homarr v0.11 ⭐️",
"logoImageUrl": "/imgs/logo/logo.png",
"faviconUrl": "/imgs/favicon/favicon-squared",
"backgroundImageUrl": "",
"customCss": "",
"colors": {
"primary": "red",
"secondary": "yellow",
"shade": 7
},
"appOpacity": 100
}
} }
], }
"wrappers": [
{
"id": "default",
"position": 0
},
{
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a326",
"position": 1
}
],
"apps": [
{
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a337",
"name": "Discord",
"url": "https://discord.com/invite/aCsmEV5RgA",
"behaviour": {
"onClickUrl": "https://discord.com/invite/aCsmEV5RgA",
"isOpeningNewTab": true,
"externalUrl": "https://discord.com/invite/aCsmEV5RgA"
},
"network": {
"enabledStatusChecker": false,
"okStatus": [
200
]
},
"appearance": {
"iconUrl": "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/discord.png"
},
"integration": {
"type": null,
"properties": []
},
"area": {
"type": "category",
"properties": {
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
}
},
"shape": {
"md": {
"location": {
"x": 3,
"y": 1
},
"size": {
"width": 3,
"height": 1
}
},
"sm": {
"location": {
"x": 2,
"y": 1
},
"size": {
"width": 1,
"height": 1
}
},
"lg": {
"location": {
"x": 2,
"y": 1
},
"size": {
"width": 1,
"height": 1
}
}
}
},
{
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a990",
"name": "Donate",
"url": "https://ko-fi.com/ajnart",
"behaviour": {
"onClickUrl": "https://ko-fi.com/ajnart",
"externalUrl": "https://ko-fi.com/ajnart",
"isOpeningNewTab": true
},
"network": {
"enabledStatusChecker": false,
"okStatus": [
200
]
},
"appearance": {
"iconUrl": "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/ko-fi.png"
},
"integration": {
"type": null,
"properties": []
},
"area": {
"type": "category",
"properties": {
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
}
},
"shape": {
"md": {
"location": {
"x": 2,
"y": 1
},
"size": {
"width": 1,
"height": 1
}
},
"sm": {
"location": {
"x": 2,
"y": 2
},
"size": {
"width": 1,
"height": 1
}
},
"lg": {
"location": {
"x": 3,
"y": 1
},
"size": {
"width": 1,
"height": 1
}
}
}
},
{
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a330",
"name": "Contribute",
"url": "https://github.com/ajnart/homarr",
"behaviour": {
"onClickUrl": "https://github.com/ajnart/homarr",
"externalUrl": "https://github.com/ajnart/homarr",
"isOpeningNewTab": true
},
"network": {
"enabledStatusChecker": false,
"okStatus": []
},
"appearance": {
"iconUrl": "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/github.png"
},
"integration": {
"type": null,
"properties": []
},
"area": {
"type": "category",
"properties": {
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
}
},
"shape": {
"md": {
"location": {
"x": 2,
"y": 0
},
"size": {
"width": 2,
"height": 1
}
},
"sm": {
"location": {
"x": 0,
"y": 2
},
"size": {
"width": 2,
"height": 1
}
},
"lg": {
"location": {
"x": 4,
"y": 0
},
"size": {
"width": 2,
"height": 2
}
}
}
},
{
"id": "5df743d9-5cb1-457c-85d2-64ff86855652",
"name": "Documentation",
"url": "https://homarr.dev",
"behaviour": {
"onClickUrl": "https://homarr.dev",
"externalUrl": "https://homarr.dev",
"isOpeningNewTab": true
},
"network": {
"enabledStatusChecker": false,
"okStatus": [
200
]
},
"appearance": {
"iconUrl": "/imgs/logo/logo.png"
},
"integration": {
"type": null,
"properties": []
},
"area": {
"type": "category",
"properties": {
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
}
},
"shape": {
"md": {
"location": {
"x": 0,
"y": 1
},
"size": {
"width": 2,
"height": 1
}
},
"sm": {
"location": {
"x": 0,
"y": 0
},
"size": {
"width": 1,
"height": 1
}
},
"lg": {
"location": {
"x": 0,
"y": 1
},
"size": {
"width": 2,
"height": 1
}
}
}
}
],
"widgets": [
{
"id": "date",
"properties": {
"display24HourFormat": true
},
"area": {
"type": "category",
"properties": {
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
}
},
"shape": {
"sm": {
"location": {
"x": 0,
"y": 1
},
"size": {
"width": 2,
"height": 1
}
},
"md": {
"location": {
"x": 4,
"y": 0
},
"size": {
"width": 2,
"height": 1
}
},
"lg": {
"location": {
"x": 2,
"y": 0
},
"size": {
"width": 2,
"height": 1
}
}
}
},
{
"id": "weather",
"properties": {
"displayInFahrenheit": false,
"location": "Paris"
},
"area": {
"type": "category",
"properties": {
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f"
}
},
"shape": {
"md": {
"location": {
"x": 0,
"y": 0
},
"size": {
"width": 2,
"height": 1
}
},
"sm": {
"location": {
"x": 1,
"y": 0
},
"size": {
"width": 2,
"height": 1
}
},
"lg": {
"location": {
"x": 0,
"y": 0
},
"size": {
"width": 2,
"height": 1
}
}
}
}
],
"settings": {
"common": {
"searchEngine": {
"type": "google",
"properties": {}
}
},
"customization": {
"layout": {
"enabledLeftSidebar": false,
"enabledRightSidebar": false,
"enabledDocker": false,
"enabledPing": false,
"enabledSearchbar": true
},
"pageTitle": "Homarr v0.11 ⭐️",
"logoImageUrl": "/imgs/logo/logo.png",
"faviconUrl": "/imgs/favicon/favicon-squared",
"backgroundImageUrl": "",
"customCss": "",
"colors": {
"primary": "red",
"secondary": "yellow",
"shade": 7
},
"appOpacity": 100
}
}
}

View File

@@ -67,6 +67,7 @@
"uuid": "^8.3.2", "uuid": "^8.3.2",
"xml-js": "^1.6.11", "xml-js": "^1.6.11",
"yarn": "^1.22.19", "yarn": "^1.22.19",
"zod": "^3.21.4",
"zustand": "^4.1.4" "zustand": "^4.1.4"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -54,8 +54,8 @@ export const ChangeWidgetPositionModal = ({
closeModal(id); closeModal(id);
}; };
const widthData = useWidthData(innerProps.widgetId); const widthData = useWidthData(innerProps.widgetType);
const heightData = useHeightData(innerProps.widgetId); const heightData = useHeightData(innerProps.widgetType);
return ( return (
<ChangePositionModal <ChangePositionModal

View File

@@ -1,6 +1,5 @@
import { Grid, Text } from '@mantine/core'; import { Grid, Text } from '@mantine/core';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useConfigContext } from '../../../../../../config/provider';
import widgets from '../../../../../../widgets'; import widgets from '../../../../../../widgets';
import { SelectorBackArrow } from '../Shared/SelectorBackArrow'; import { SelectorBackArrow } from '../Shared/SelectorBackArrow';
import { WidgetElementType } from './WidgetElementType'; import { WidgetElementType } from './WidgetElementType';
@@ -13,7 +12,6 @@ export const AvailableIntegrationElements = ({
onClickBack, onClickBack,
}: AvailableIntegrationElementsProps) => { }: AvailableIntegrationElementsProps) => {
const { t } = useTranslation('layout/element-selector/selector'); const { t } = useTranslation('layout/element-selector/selector');
const activeWidgets = useConfigContext().config?.widgets ?? [];
return ( return (
<> <>
<SelectorBackArrow onClickBack={onClickBack} /> <SelectorBackArrow onClickBack={onClickBack} />
@@ -23,11 +21,9 @@ export const AvailableIntegrationElements = ({
</Text> </Text>
<Grid> <Grid>
{Object.entries(widgets) {Object.entries(widgets).map(([k, v]) => (
.filter(([widgetId]) => !activeWidgets.some((aw) => aw.id === widgetId)) <WidgetElementType key={k} id={k} image={v.icon} widget={v} />
.map(([k, v]) => ( ))}
<WidgetElementType key={k} id={k} image={v.icon} widget={v} />
))}
</Grid> </Grid>
</> </>
); );

View File

@@ -2,6 +2,7 @@ import { useModals } from '@mantine/modals';
import { showNotification } from '@mantine/notifications'; import { showNotification } from '@mantine/notifications';
import { IconChecks, TablerIcon } from '@tabler/icons'; import { IconChecks, TablerIcon } from '@tabler/icons';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { v4 as uuidv4 } from 'uuid';
import { useConfigContext } from '../../../../../../config/provider'; import { useConfigContext } from '../../../../../../config/provider';
import { useConfigStore } from '../../../../../../config/store'; import { useConfigStore } from '../../../../../../config/store';
import { IWidget, IWidgetDefinition } from '../../../../../../widgets/widgets'; import { IWidget, IWidgetDefinition } from '../../../../../../widgets/widgets';
@@ -32,9 +33,10 @@ export const WidgetElementType = ({ id, image, disabled, widget }: WidgetElement
(prev) => ({ (prev) => ({
...prev, ...prev,
widgets: [ widgets: [
...prev.widgets.filter((w) => w.id !== widget.id), ...prev.widgets,
{ {
id: widget.id, id: uuidv4(),
type: widget.id,
properties: Object.entries(widget.options).reduce((prev, [k, v]) => { properties: Object.entries(widget.options).reduce((prev, [k, v]) => {
const newPrev = prev; const newPrev = prev;
newPrev[k] = v.defaultValue; newPrev[k] = v.defaultValue;

View File

@@ -26,6 +26,7 @@ import { DraggableList } from './DraggableList';
export type WidgetEditModalInnerProps = { export type WidgetEditModalInnerProps = {
widgetId: string; widgetId: string;
widgetType: string;
options: IWidget<string, any>['properties']; options: IWidget<string, any>['properties'];
widgetOptions: IWidget<string, any>['properties']; widgetOptions: IWidget<string, any>['properties'];
}; };
@@ -37,7 +38,7 @@ export const WidgetsEditModal = ({
id, id,
innerProps, innerProps,
}: ContextModalProps<WidgetEditModalInnerProps>) => { }: ContextModalProps<WidgetEditModalInnerProps>) => {
const { t } = useTranslation([`modules/${innerProps.widgetId}`, 'common']); const { t } = useTranslation([`modules/${innerProps.widgetType}`, 'common']);
const [moduleProperties, setModuleProperties] = useState(innerProps.options); const [moduleProperties, setModuleProperties] = useState(innerProps.options);
const items = Object.entries(innerProps.widgetOptions ?? {}) as [ const items = Object.entries(innerProps.widgetOptions ?? {}) as [
string, string,
@@ -45,7 +46,7 @@ export const WidgetsEditModal = ({
][]; ][];
// Find the Key in the "Widgets" Object that matches the widgetId // Find the Key in the "Widgets" Object that matches the widgetId
const currentWidgetDefinition = Widgets[innerProps.widgetId as keyof typeof Widgets]; const currentWidgetDefinition = Widgets[innerProps.widgetType as keyof typeof Widgets];
const { name: configName } = useConfigContext(); const { name: configName } = useConfigContext();
const updateConfig = useConfigStore((x) => x.updateConfig); const updateConfig = useConfigStore((x) => x.updateConfig);
@@ -100,7 +101,7 @@ export const WidgetsEditModal = ({
<WidgetOptionTypeSwitch <WidgetOptionTypeSwitch
key={`${key}.${index}`} key={`${key}.${index}`}
option={option} option={option}
widgetId={innerProps.widgetId} widgetId={innerProps.widgetType}
propName={key} propName={key}
value={value} value={value}
handleChange={handleChange} handleChange={handleChange}

View File

@@ -10,6 +10,7 @@ import { WidgetsRemoveModalInnerProps } from './WidgetsRemoveModal';
export type WidgetChangePositionModalInnerProps = { export type WidgetChangePositionModalInnerProps = {
widgetId: string; widgetId: string;
widgetType: string;
widget: IWidget<string, any>; widget: IWidget<string, any>;
wrapperColumnCount: number; wrapperColumnCount: number;
}; };
@@ -27,8 +28,8 @@ export const WidgetsMenu = ({ integration, widget }: WidgetsMenuProps) => {
// Match widget.id with WidgetsDefinitions // Match widget.id with WidgetsDefinitions
// First get the keys // First get the keys
const keys = Object.keys(WidgetsDefinitions); const keys = Object.keys(WidgetsDefinitions);
// Then find the key that matches the widget.id // Then find the key that matches the widget.type
const widgetDefinition = keys.find((key) => key === widget.id); const widgetDefinition = keys.find((key) => key === widget.type);
// Then get the widget definition // Then get the widget definition
const widgetDefinitionObject = const widgetDefinitionObject =
WidgetsDefinitions[widgetDefinition as keyof typeof WidgetsDefinitions]; WidgetsDefinitions[widgetDefinition as keyof typeof WidgetsDefinitions];
@@ -38,13 +39,8 @@ export const WidgetsMenu = ({ integration, widget }: WidgetsMenuProps) => {
modal: 'integrationRemove', modal: 'integrationRemove',
title: <Title order={4}>{t('common:remove')}</Title>, title: <Title order={4}>{t('common:remove')}</Title>,
innerProps: { innerProps: {
widgetId: integration, widgetId: widget.id,
}, widgetType: integration,
styles: {
inner: {
position: 'sticky',
top: 30,
},
}, },
}); });
}; };
@@ -55,16 +51,11 @@ export const WidgetsMenu = ({ integration, widget }: WidgetsMenuProps) => {
size: 'xl', size: 'xl',
title: null, title: null,
innerProps: { innerProps: {
widgetId: integration, widgetId: widget.id,
widgetType: integration,
widget, widget,
wrapperColumnCount, wrapperColumnCount,
}, },
styles: {
inner: {
position: 'sticky',
top: 30,
},
},
}); });
}; };
@@ -73,19 +64,13 @@ export const WidgetsMenu = ({ integration, widget }: WidgetsMenuProps) => {
modal: 'integrationOptions', modal: 'integrationOptions',
title: <Title order={4}>{t('descriptor.settings.title')}</Title>, title: <Title order={4}>{t('descriptor.settings.title')}</Title>,
innerProps: { innerProps: {
widgetId: integration, widgetId: widget.id,
widgetType: integration,
options: widget.properties, options: widget.properties,
// Cast as the right type for the correct widget // Cast as the right type for the correct widget
widgetOptions: widgetDefinitionObject.options as any, widgetOptions: widgetDefinitionObject.options as any,
}, },
zIndex: 5, zIndex: 5,
styles: {
inner: {
position: 'sticky',
top: 30,
maxHeight: '100%',
},
},
}); });
}; };

View File

@@ -7,6 +7,7 @@ import { useConfigStore } from '../../../../config/store';
export type WidgetsRemoveModalInnerProps = { export type WidgetsRemoveModalInnerProps = {
widgetId: string; widgetId: string;
widgetType: string;
}; };
export const WidgetsRemoveModal = ({ export const WidgetsRemoveModal = ({
@@ -14,7 +15,7 @@ export const WidgetsRemoveModal = ({
id, id,
innerProps, innerProps,
}: ContextModalProps<WidgetsRemoveModalInnerProps>) => { }: ContextModalProps<WidgetsRemoveModalInnerProps>) => {
const { t } = useTranslation([`modules/${innerProps.widgetId}`, 'common']); const { t } = useTranslation([`modules/${innerProps.widgetType}`, 'common']);
const { name: configName } = useConfigContext(); const { name: configName } = useConfigContext();
if (!configName) return null; if (!configName) return null;
const updateConfig = useConfigStore((x) => x.updateConfig); const updateConfig = useConfigStore((x) => x.updateConfig);
@@ -35,7 +36,7 @@ export const WidgetsRemoveModal = ({
<Trans <Trans
i18nKey="common:removeConfirm" i18nKey="common:removeConfirm"
components={[<Text weight={500} />]} components={[<Text weight={500} />]}
values={{ item: innerProps.widgetId }} values={{ item: innerProps.widgetType }}
/> />
<Group position="right"> <Group position="right">
<Button onClick={() => context.closeModal(id)} variant="light"> <Button onClick={() => context.closeModal(id)} variant="light">

View File

@@ -42,7 +42,7 @@ export function WrapperContent({ apps, refs, widgets }: WrapperContentProps) {
); );
})} })}
{widgets.map((widget) => { {widgets.map((widget) => {
const definition = Widgets[widget.id as keyof typeof Widgets] as const definition = Widgets[widget.type as keyof typeof Widgets] as
| IWidgetDefinition | IWidgetDefinition
| undefined; | undefined;
if (!definition) return null; if (!definition) return null;
@@ -52,7 +52,7 @@ export function WrapperContent({ apps, refs, widgets }: WrapperContentProps) {
type="widget" type="widget"
key={widget.id} key={widget.id}
itemRef={refs.items.current[widget.id]} itemRef={refs.items.current[widget.id]}
id={definition.id} id={widget.id}
{...definition.gridstack} {...definition.gridstack}
{...widget.shape[shapeSize]?.location} {...widget.shape[shapeSize]?.location}
{...widget.shape[shapeSize]?.size} {...widget.shape[shapeSize]?.size}
@@ -60,7 +60,7 @@ export function WrapperContent({ apps, refs, widgets }: WrapperContentProps) {
<WidgetWrapper <WidgetWrapper
className="grid-stack-item-content" className="grid-stack-item-content"
widget={widget} widget={widget}
widgetId={widget.id} widgetType={widget.type}
WidgetComponent={definition.component} WidgetComponent={definition.component}
/> />
</GridstackTileWrapper> </GridstackTileWrapper>

View File

@@ -48,6 +48,7 @@ export const useConfigStore = create<UseConfigStoreType>((set, get) => ({
const previousConfig: ConfigType = JSON.parse(JSON.stringify(currentConfig.value)); const previousConfig: ConfigType = JSON.parse(JSON.stringify(currentConfig.value));
const updatedConfig = updateCallback(currentConfig.value); const updatedConfig = updateCallback(currentConfig.value);
set((old) => ({ set((old) => ({
...old, ...old,
configs: [ configs: [

View File

@@ -153,7 +153,7 @@ export function RadarrMediaDisplay(props: any) {
export function SonarrMediaDisplay(props: any) { export function SonarrMediaDisplay(props: any) {
const { media }: { media: any } = props; const { media }: { media: any } = props;
const { config } = useConfigContext(); const { config } = useConfigContext();
const calendar = config?.widgets.find((w) => w.id === 'calendar'); const calendar = config?.widgets.find((w) => w.type === 'calendar');
const useSonarrv4 = calendar?.properties.useSonarrv4 ?? false; const useSonarrv4 = calendar?.properties.useSonarrv4 ?? false;
// Find a poster CoverType // Find a poster CoverType

View File

@@ -4,6 +4,7 @@ import Consola from 'consola';
import { NextApiRequest, NextApiResponse } from 'next'; import { NextApiRequest, NextApiResponse } from 'next';
import { z } from 'zod';
import { AppIntegrationType } from '../../../types/app'; import { AppIntegrationType } from '../../../types/app';
import { getConfig } from '../../../tools/config/getConfig'; import { getConfig } from '../../../tools/config/getConfig';
@@ -18,28 +19,36 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {
}); });
}; };
const getQuerySchema = z.object({
month: z
.string()
.regex(/^\d+$/)
.transform((x) => parseInt(x, 10)),
year: z
.string()
.regex(/^\d+$/)
.transform((x) => parseInt(x, 10)),
widgetId: z.string().uuid(),
configName: z.string(),
});
async function Get(req: NextApiRequest, res: NextApiResponse) { async function Get(req: NextApiRequest, res: NextApiResponse) {
// Parse req.body as a AppItem const parseResult = getQuerySchema.safeParse(req.query);
const {
month: monthString,
year: yearString,
configName,
} = req.query as { month: string; year: string; configName: string };
const month = parseInt(monthString, 10); if (!parseResult.success) {
const year = parseInt(yearString, 10);
if (Number.isNaN(month) || Number.isNaN(year) || !configName) {
return res.status(400).json({ return res.status(400).json({
statusCode: 400, statusCode: 400,
message: 'Missing required parameter in url: year, month or configName', message: 'Invalid query parameters, please specify the widgetId, month, year and configName',
}); });
} }
// Parse req.body as a AppItem
const { month, year, widgetId, configName } = parseResult.data;
const config = getConfig(configName); const config = getConfig(configName);
// Find the calendar widget in the config // Find the calendar widget in the config
const calendar = config.widgets.find((w) => w.id === 'calendar'); const calendar = config.widgets.find((w) => w.type === 'calendar' && w.id === widgetId);
const useSonarrv4 = calendar?.properties.useSonarrv4 ?? false; const useSonarrv4 = calendar?.properties.useSonarrv4 ?? false;
const mediaAppIntegrationTypes: AppIntegrationType['type'][] = [ const mediaAppIntegrationTypes: AppIntegrationType['type'][] = [

View File

@@ -1,20 +1,29 @@
import axios from 'axios'; import axios from 'axios';
import { NextApiRequest, NextApiResponse } from 'next'; import { NextApiRequest, NextApiResponse } from 'next';
import { z } from 'zod';
import { getConfig } from '../../../../tools/config/getConfig'; import { getConfig } from '../../../../tools/config/getConfig';
import { IDashDotTile } from '../../../../widgets/dashDot/DashDotTile'; import { IDashDotTile } from '../../../../widgets/dashDot/DashDotTile';
async function Get(req: NextApiRequest, res: NextApiResponse) { const getQuerySchema = z.object({
const { configName } = req.query; configName: z.string(),
widgetId: z.string().uuid(),
});
if (!configName || typeof configName !== 'string') { async function Get(req: NextApiRequest, res: NextApiResponse) {
const parseResult = getQuerySchema.safeParse(req.query);
if (!parseResult.success) {
return res.status(400).json({ return res.status(400).json({
message: 'Missing required configName in url', statusCode: 400,
message: 'Invalid query parameters, please specify the widgetId and configName',
}); });
} }
const { configName, widgetId } = parseResult.data;
const config = getConfig(configName); const config = getConfig(configName);
const dashDotWidget = config.widgets.find((x) => x.id === 'dashdot'); const dashDotWidget = config.widgets.find((x) => x.type === 'dashdot' && x.id === widgetId);
if (!dashDotWidget) { if (!dashDotWidget) {
return res.status(400).json({ return res.status(400).json({

View File

@@ -1,19 +1,28 @@
import axios from 'axios'; import axios from 'axios';
import { NextApiRequest, NextApiResponse } from 'next'; import { NextApiRequest, NextApiResponse } from 'next';
import { z } from 'zod';
import { getConfig } from '../../../../tools/config/getConfig'; import { getConfig } from '../../../../tools/config/getConfig';
import { IDashDotTile } from '../../../../widgets/dashDot/DashDotTile'; import { IDashDotTile } from '../../../../widgets/dashDot/DashDotTile';
async function Get(req: NextApiRequest, res: NextApiResponse) { const getQuerySchema = z.object({
const { configName } = req.query; configName: z.string(),
widgetId: z.string().uuid(),
});
if (!configName || typeof configName !== 'string') { async function Get(req: NextApiRequest, res: NextApiResponse) {
const parseResult = getQuerySchema.safeParse(req.query);
if (!parseResult.success) {
return res.status(400).json({ return res.status(400).json({
message: 'Missing required configName in url', statusCode: 400,
message: 'Invalid query parameters, please specify the widgetId and configName',
}); });
} }
const { configName, widgetId } = parseResult.data;
const config = getConfig(configName); const config = getConfig(configName);
const dashDotWidget = config.widgets.find((x) => x.id === 'dashdot'); const dashDotWidget = config.widgets.find((x) => x.type === 'dashdot' && x.id === widgetId);
if (!dashDotWidget) { if (!dashDotWidget) {
return res.status(400).json({ return res.status(400).json({

View File

@@ -8,6 +8,7 @@ import { NextApiRequest, NextApiResponse } from 'next';
import Parser from 'rss-parser'; import Parser from 'rss-parser';
import { z } from 'zod';
import { getConfig } from '../../../../tools/config/getConfig'; import { getConfig } from '../../../../tools/config/getConfig';
import { IRssWidget } from '../../../../widgets/rss/RssWidgetTile'; import { IRssWidget } from '../../../../widgets/rss/RssWidgetTile';
import { Stopwatch } from '../../../../tools/shared/time/stopwatch.tool'; import { Stopwatch } from '../../../../tools/shared/time/stopwatch.tool';
@@ -25,11 +26,24 @@ const parser: Parser<any, CustomItem> = new Parser({
}, },
}); });
const getQuerySchema = z.object({
widgetId: z.string().uuid(),
});
export const Get = async (request: NextApiRequest, response: NextApiResponse) => { export const Get = async (request: NextApiRequest, response: NextApiResponse) => {
const configName = getCookie('config-name', { req: request }); const configName = getCookie('config-name', { req: request });
const config = getConfig(configName?.toString() ?? 'default'); const config = getConfig(configName?.toString() ?? 'default');
const rssWidget = config.widgets.find((x) => x.id === 'rss') as IRssWidget | undefined; const parseResult = getQuerySchema.safeParse(request.query);
if (!parseResult.success) {
response.status(400).json({ message: 'invalid query parameters, please specify the widgetId' });
return;
}
const rssWidget = config.widgets.find(
(x) => x.type === 'rss' && x.id === parseResult.data.widgetId
) as IRssWidget | undefined;
if ( if (
!rssWidget || !rssWidget ||

View File

@@ -1,9 +1,13 @@
import Consola from 'consola'; import Consola from 'consola';
import { v4 as uuidv4 } from 'uuid';
import { BackendConfigType, ConfigType } from '../../types/config'; import { BackendConfigType, ConfigType } from '../../types/config';
import { backendMigrateConfig } from './backendMigrateConfig'; import { backendMigrateConfig } from './backendMigrateConfig';
import { configExists } from './configExists'; import { configExists } from './configExists';
import { getFallbackConfig } from './getFallbackConfig'; import { getFallbackConfig } from './getFallbackConfig';
import { readConfig } from './readConfig'; import { readConfig } from './readConfig';
import { writeConfig } from './writeConfig';
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
export const getConfig = (name: string): BackendConfigType => { export const getConfig = (name: string): BackendConfigType => {
if (!configExists(name)) return getFallbackConfig() as unknown as ConfigType; if (!configExists(name)) return getFallbackConfig() as unknown as ConfigType;
@@ -12,9 +16,29 @@ export const getConfig = (name: string): BackendConfigType => {
// to the new format. // to the new format.
const config = readConfig(name); const config = readConfig(name);
if (config.schemaVersion === undefined) { if (config.schemaVersion === undefined) {
Consola.log('Migrating config file...', config); Consola.log('Migrating config file...', config.name);
return backendMigrateConfig(config, name); return backendMigrateConfig(config, name);
} }
return config; let backendConfig = config as BackendConfigType;
if (backendConfig.widgets.some((widget) => !uuidRegex.test(widget.id))) {
backendConfig = {
...backendConfig,
widgets: backendConfig.widgets.map((widget) => ({
...widget,
id: uuidRegex.test(widget.id) ? widget.id : uuidv4(),
type: !uuidRegex.test(widget.id) ? widget.id : widget.type,
})),
};
Consola.log(
'Migrating config file to multiple widgets...',
backendConfig.configProperties.name
);
writeConfig(backendConfig);
}
return backendConfig;
}; };

View File

@@ -183,7 +183,8 @@ const migrateModules = (config: Config): IWidget<string, any>[] => {
case 'torrent-status': case 'torrent-status':
case 'Torrent': case 'Torrent':
return { return {
id: 'torrents-status', id: uuidv4(),
type: 'torrents-status',
properties: { properties: {
refreshInterval: 10, refreshInterval: 10,
displayCompletedTorrents: oldModule.options?.hideComplete?.value ?? false, displayCompletedTorrents: oldModule.options?.hideComplete?.value ?? false,
@@ -199,7 +200,8 @@ const migrateModules = (config: Config): IWidget<string, any>[] => {
} as ITorrent; } as ITorrent;
case 'weather': case 'weather':
return { return {
id: 'weather', id: uuidv4(),
type: 'weather',
properties: { properties: {
displayInFahrenheit: oldModule.options?.freedomunit?.value ?? false, displayInFahrenheit: oldModule.options?.freedomunit?.value ?? false,
location: oldModule.options?.location?.value ?? 'Paris', location: oldModule.options?.location?.value ?? 'Paris',
@@ -216,7 +218,8 @@ const migrateModules = (config: Config): IWidget<string, any>[] => {
case 'Dash.': { case 'Dash.': {
const oldDashDotService = config.services.find((service) => service.type === 'Dash.'); const oldDashDotService = config.services.find((service) => service.type === 'Dash.');
return { return {
id: 'dashdot', id: uuidv4(),
type: 'dashdot',
properties: { properties: {
url: oldModule.options?.url?.value ?? oldDashDotService?.url ?? '', url: oldModule.options?.url?.value ?? oldDashDotService?.url ?? '',
cpuMultiView: oldModule.options?.cpuMultiView?.value ?? false, cpuMultiView: oldModule.options?.cpuMultiView?.value ?? false,
@@ -235,7 +238,8 @@ const migrateModules = (config: Config): IWidget<string, any>[] => {
} }
case 'date': case 'date':
return { return {
id: 'date', id: uuidv4(),
type: 'date',
properties: { properties: {
display24HourFormat: oldModule.options?.full?.value ?? true, display24HourFormat: oldModule.options?.full?.value ?? true,
}, },
@@ -249,7 +253,8 @@ const migrateModules = (config: Config): IWidget<string, any>[] => {
} as IDateWidget; } as IDateWidget;
case 'Download Speed' || 'dlspeed': case 'Download Speed' || 'dlspeed':
return { return {
id: 'dlspeed', id: uuidv4(),
type: 'dlspeed',
properties: {}, properties: {},
area: { area: {
type: 'wrapper', type: 'wrapper',
@@ -261,7 +266,8 @@ const migrateModules = (config: Config): IWidget<string, any>[] => {
} as ITorrentNetworkTraffic; } as ITorrentNetworkTraffic;
case 'calendar': case 'calendar':
return { return {
id: 'calendar', id: uuidv4(),
type: 'calendar',
properties: {}, properties: {},
area: { area: {
type: 'wrapper', type: 'wrapper',
@@ -273,7 +279,8 @@ const migrateModules = (config: Config): IWidget<string, any>[] => {
} as ICalendarWidget; } as ICalendarWidget;
case 'usenet': case 'usenet':
return { return {
id: 'usenet', id: uuidv4(),
type: 'usenet',
properties: {}, properties: {},
area: { area: {
type: 'wrapper', type: 'wrapper',

View File

@@ -0,0 +1,10 @@
import fs from 'fs';
import { BackendConfigType } from '../../types/config';
import { generateConfigPath } from './generateConfigPath';
export function writeConfig(config: BackendConfigType) {
const path = generateConfigPath(config.configProperties.name);
return fs.writeFileSync(path, JSON.stringify(config, null, 4), {
encoding: 'utf8',
});
}

View File

@@ -1,4 +1,4 @@
import { ComponentType, useMemo } from 'react'; import { ComponentType } from 'react';
import Widgets from '.'; import Widgets from '.';
import { HomarrCardWrapper } from '../components/Dashboard/Tiles/HomarrCardWrapper'; import { HomarrCardWrapper } from '../components/Dashboard/Tiles/HomarrCardWrapper';
import { WidgetsMenu } from '../components/Dashboard/Tiles/Widgets/WidgetsMenu'; import { WidgetsMenu } from '../components/Dashboard/Tiles/Widgets/WidgetsMenu';
@@ -6,7 +6,7 @@ import ErrorBoundary from './boundary';
import { IWidget } from './widgets'; import { IWidget } from './widgets';
interface WidgetWrapperProps { interface WidgetWrapperProps {
widgetId: string; widgetType: string;
widget: IWidget<string, any>; widget: IWidget<string, any>;
className: string; className: string;
WidgetComponent: ComponentType<{ widget: IWidget<string, any> }>; WidgetComponent: ComponentType<{ widget: IWidget<string, any> }>;
@@ -14,26 +14,24 @@ interface WidgetWrapperProps {
// If a property has no value, set it to the default value // If a property has no value, set it to the default value
const useWidget = <T extends IWidget<string, any>>(widget: T): T => { const useWidget = <T extends IWidget<string, any>>(widget: T): T => {
const definition = Widgets[widget.id as keyof typeof Widgets]; const definition = Widgets[widget.type as keyof typeof Widgets];
return useMemo(() => { const newProps = { ...widget.properties };
const newProps = { ...widget.properties };
Object.entries(definition.options).forEach(([key, option]) => { Object.entries(definition.options).forEach(([key, option]) => {
if (newProps[key] == null) { if (newProps[key] == null) {
newProps[key] = option.defaultValue; newProps[key] = option.defaultValue;
} }
}); });
return { return {
...widget, ...widget,
properties: newProps, properties: newProps,
}; };
}, [widget]);
}; };
export const WidgetWrapper = ({ export const WidgetWrapper = ({
widgetId, widgetType,
widget, widget,
className, className,
WidgetComponent, WidgetComponent,
@@ -43,7 +41,7 @@ export const WidgetWrapper = ({
return ( return (
<ErrorBoundary> <ErrorBoundary>
<HomarrCardWrapper className={className}> <HomarrCardWrapper className={className}>
<WidgetsMenu integration={widgetId} widget={widgetWithDefaultProps} /> <WidgetsMenu integration={widgetType} widget={widgetWithDefaultProps} />
<WidgetComponent widget={widgetWithDefaultProps} /> <WidgetComponent widget={widgetWithDefaultProps} />
</HomarrCardWrapper> </HomarrCardWrapper>
</ErrorBoundary> </ErrorBoundary>

View File

@@ -94,7 +94,7 @@ class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundarySta
mt="md" mt="md"
fullWidth fullWidth
> >
{(this.props.t('modal.reportButton'))} {this.props.t('modal.reportButton')}
</Button> </Button>
</> </>
), ),

View File

@@ -63,7 +63,7 @@ function CalendarTile({ widget }: CalendarTileProps) {
await fetch( await fetch(
`/api/modules/calendar?year=${month.getFullYear()}&month=${ `/api/modules/calendar?year=${month.getFullYear()}&month=${
month.getMonth() + 1 month.getMonth() + 1
}&configName=${configName}` }&configName=${configName}&widgetId=${widget.id}`
) )
).json()) as MediasType, ).json()) as MediasType,
}); });

View File

@@ -9,11 +9,12 @@ import { DashDotInfo } from './DashDotCompactNetwork';
interface DashDotCompactStorageProps { interface DashDotCompactStorageProps {
info: DashDotInfo; info: DashDotInfo;
widgetId: string;
} }
export const DashDotCompactStorage = ({ info }: DashDotCompactStorageProps) => { export const DashDotCompactStorage = ({ info, widgetId }: DashDotCompactStorageProps) => {
const { t } = useTranslation('modules/dashdot'); const { t } = useTranslation('modules/dashdot');
const { data: storageLoad } = useDashDotStorage(); const { data: storageLoad } = useDashDotStorage(widgetId);
const totalUsed = calculateTotalLayoutSize({ const totalUsed = calculateTotalLayoutSize({
layout: storageLoad?.layout ?? [], layout: storageLoad?.layout ?? [],
@@ -50,7 +51,7 @@ interface CalculateTotalLayoutSizeProps<TLayoutItem> {
key: keyof TLayoutItem; key: keyof TLayoutItem;
} }
const useDashDotStorage = () => { const useDashDotStorage = (widgetId: string) => {
const { name: configName, config } = useConfigContext(); const { name: configName, config } = useConfigContext();
return useQuery({ return useQuery({
@@ -58,17 +59,18 @@ const useDashDotStorage = () => {
'dashdot/storage', 'dashdot/storage',
{ {
configName, configName,
url: config?.widgets.find((x) => x.id === 'dashdot')?.properties.url, url: config?.widgets.find((x) => x.type === 'dashdot')?.properties.url,
widgetId,
}, },
], ],
queryFn: () => fetchDashDotStorageLoad(configName), queryFn: () => fetchDashDotStorageLoad(configName, widgetId),
}); });
}; };
async function fetchDashDotStorageLoad(configName: string | undefined) { async function fetchDashDotStorageLoad(configName: string | undefined, widgetId: string) {
if (!configName) throw new Error('configName is undefined'); if (!configName) throw new Error('configName is undefined');
return (await ( return (await (
await axios.get('/api/modules/dashdot/storage', { params: { configName } }) await axios.get('/api/modules/dashdot/storage', { params: { configName, widgetId } })
).data) as DashDotStorageLoad; ).data) as DashDotStorageLoad;
} }

View File

@@ -11,6 +11,7 @@ interface DashDotGraphProps {
dashDotUrl: string; dashDotUrl: string;
usePercentages: boolean; usePercentages: boolean;
info: DashDotInfo; info: DashDotInfo;
widgetId: string;
} }
export const DashDotGraph = ({ export const DashDotGraph = ({
@@ -21,12 +22,13 @@ export const DashDotGraph = ({
dashDotUrl, dashDotUrl,
usePercentages, usePercentages,
info, info,
widgetId,
}: DashDotGraphProps) => { }: DashDotGraphProps) => {
const { t } = useTranslation('modules/dashdot'); const { t } = useTranslation('modules/dashdot');
const { classes } = useStyles(); const { classes } = useStyles();
if (graph === 'storage' && isCompact) { if (graph === 'storage' && isCompact) {
return <DashDotCompactStorage info={info} />; return <DashDotCompactStorage info={info} widgetId={widgetId} />;
} }
if (graph === 'network' && isCompact) { if (graph === 'network' && isCompact) {

View File

@@ -160,6 +160,7 @@ function DashDotTile({ widget }: DashDotTileProps) {
const { data: info } = useDashDotInfo({ const { data: info } = useDashDotInfo({
dashDotUrl, dashDotUrl,
enabled: !detectedProtocolDowngrade, enabled: !detectedProtocolDowngrade,
widgetId: widget.id,
}); });
if (detectedProtocolDowngrade) { if (detectedProtocolDowngrade) {
@@ -197,6 +198,7 @@ function DashDotTile({ widget }: DashDotTileProps) {
isCompact={g.subValues.compactView ?? false} isCompact={g.subValues.compactView ?? false}
multiView={g.subValues.multiView ?? false} multiView={g.subValues.multiView ?? false}
usePercentages={usePercentages} usePercentages={usePercentages}
widgetId={widget.id}
/> />
</Grid.Col> </Grid.Col>
))} ))}
@@ -207,7 +209,15 @@ function DashDotTile({ widget }: DashDotTileProps) {
); );
} }
const useDashDotInfo = ({ dashDotUrl, enabled }: { dashDotUrl: string; enabled: boolean }) => { const useDashDotInfo = ({
dashDotUrl,
enabled,
widgetId,
}: {
dashDotUrl: string;
enabled: boolean;
widgetId: string;
}) => {
const { name: configName } = useConfigContext(); const { name: configName } = useConfigContext();
return useQuery({ return useQuery({
refetchInterval: 50000, refetchInterval: 50000,
@@ -218,15 +228,15 @@ const useDashDotInfo = ({ dashDotUrl, enabled }: { dashDotUrl: string; enabled:
dashDotUrl, dashDotUrl,
}, },
], ],
queryFn: () => fetchDashDotInfo(configName), queryFn: () => fetchDashDotInfo(configName, widgetId),
enabled, enabled,
}); });
}; };
const fetchDashDotInfo = async (configName: string | undefined) => { const fetchDashDotInfo = async (configName: string | undefined, widgetId: string) => {
if (!configName) return {} as DashDotInfo; if (!configName) return {} as DashDotInfo;
return (await ( return (await (
await axios.get('/api/modules/dashdot/info', { params: { configName } }) await axios.get('/api/modules/dashdot/info', { params: { configName, widgetId } })
).data) as DashDotInfo; ).data) as DashDotInfo;
}; };

View File

@@ -29,8 +29,8 @@ import dayjs from 'dayjs';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import Link from 'next/link'; import Link from 'next/link';
import { useState } from 'react'; import { useState } from 'react';
import { defineWidget } from '../helper';
import { IWidget } from '../widgets'; import { IWidget } from '../widgets';
import { defineWidget } from '../helper';
const definition = defineWidget({ const definition = defineWidget({
id: 'rss', id: 'rss',
@@ -56,11 +56,11 @@ interface RssTileProps {
widget: IRssWidget; widget: IRssWidget;
} }
const useGetRssFeed = (feedUrl: string) => export const useGetRssFeed = (feedUrl: string, widgetId: string) =>
useQuery({ useQuery({
queryKey: ['rss-feed', feedUrl], queryKey: ['rss-feed', feedUrl],
queryFn: async () => { queryFn: async () => {
const response = await fetch('/api/modules/rss'); const response = await fetch(`/api/modules/rss?widgetId=${widgetId}`);
return response.json(); return response.json();
}, },
}); });
@@ -68,7 +68,8 @@ const useGetRssFeed = (feedUrl: string) =>
function RssTile({ widget }: RssTileProps) { function RssTile({ widget }: RssTileProps) {
const { t } = useTranslation('modules/rss'); const { t } = useTranslation('modules/rss');
const { data, isLoading, isFetching, isError, refetch } = useGetRssFeed( const { data, isLoading, isFetching, isError, refetch } = useGetRssFeed(
widget.properties.rssFeedUrl widget.properties.rssFeedUrl,
widget.id
); );
const { classes } = useStyles(); const { classes } = useStyles();
const [loadingOverlayVisible, setLoadingOverlayVisible] = useState(false); const [loadingOverlayVisible, setLoadingOverlayVisible] = useState(false);

View File

@@ -13,7 +13,8 @@ import { ShapeType } from '../types/shape';
// Type of widgets which are saved to config // Type of widgets which are saved to config
export type IWidget<TKey extends string, TDefinition extends IWidgetDefinition> = { export type IWidget<TKey extends string, TDefinition extends IWidgetDefinition> = {
id: TKey; id: string;
type: TKey;
properties: { properties: {
[key in keyof TDefinition['options']]: MakeLessSpecific< [key in keyof TDefinition['options']]: MakeLessSpecific<
TDefinition['options'][key]['defaultValue'] TDefinition['options'][key]['defaultValue']

View File

@@ -4914,6 +4914,7 @@ __metadata:
vitest-fetch-mock: ^0.2.2 vitest-fetch-mock: ^0.2.2
xml-js: ^1.6.11 xml-js: ^1.6.11
yarn: ^1.22.19 yarn: ^1.22.19
zod: ^3.21.4
zustand: ^4.1.4 zustand: ^4.1.4
languageName: unknown languageName: unknown
linkType: soft linkType: soft
@@ -8760,6 +8761,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"zod@npm:^3.21.4":
version: 3.21.4
resolution: "zod@npm:3.21.4"
checksum: f185ba87342ff16f7a06686767c2b2a7af41110c7edf7c1974095d8db7a73792696bcb4a00853de0d2edeb34a5b2ea6a55871bc864227dace682a0a28de33e1f
languageName: node
linkType: hard
"zustand@npm:^4.1.4": "zustand@npm:^4.1.4":
version: 4.3.6 version: 4.3.6
resolution: "zustand@npm:4.3.6" resolution: "zustand@npm:4.3.6"