mirror of
https://github.com/ajnart/homarr.git
synced 2026-06-11 20:42:11 +02:00
✨ Add client side widget modification, improve switching between edit mode
This commit is contained in:
41
src/components/Board/widget-actions.ts
Normal file
41
src/components/Board/widget-actions.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useCallback } from 'react';
|
||||
import { api } from '~/utils/api';
|
||||
|
||||
type UpdateWidgetOptions = {
|
||||
itemId: string;
|
||||
newOptions: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export const useWidgetActions = ({ boardName }: { boardName: string }) => {
|
||||
const utils = api.useContext();
|
||||
const updateWidgetOptions = useCallback(
|
||||
({ itemId, newOptions }: UpdateWidgetOptions) => {
|
||||
utils.boards.byName.setData({ boardName }, (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 || item.type !== 'widget') return item;
|
||||
return {
|
||||
...item,
|
||||
options: newOptions,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
},
|
||||
[boardName, utils]
|
||||
);
|
||||
|
||||
return {
|
||||
updateWidgetOptions,
|
||||
};
|
||||
};
|
||||
@@ -1,7 +1,5 @@
|
||||
import { MobileRibbons } from './Mobile/Ribbon/MobileRibbon';
|
||||
import { BoardView } from './Views/DashboardView';
|
||||
import { DashboardDetailView } from './Views/DetailView';
|
||||
import { DashboardEditView } from './Views/EditView';
|
||||
import { useEditModeStore } from './Views/useEditModeStore';
|
||||
|
||||
export const Board = () => {
|
||||
@@ -10,7 +8,7 @@ export const Board = () => {
|
||||
return (
|
||||
<>
|
||||
{/* The following elemens are splitted because gridstack doesn't reinitialize them when using same item. */}
|
||||
<BoardView key={isEditMode.toString()} />
|
||||
<BoardView />
|
||||
<MobileRibbons />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -29,7 +29,7 @@ export const ChangeWidgetPositionModal = ({
|
||||
updateConfig(
|
||||
configName,
|
||||
(prev) => {
|
||||
const currentWidget = prev.widgets.find((x) => x.id === innerProps.widgetId);
|
||||
const currentWidget = prev.widgets.find((x) => x.id === innerProps.widget.id);
|
||||
currentWidget!.shape[shapeSize] = {
|
||||
location: {
|
||||
x,
|
||||
@@ -43,7 +43,7 @@ export const ChangeWidgetPositionModal = ({
|
||||
|
||||
return {
|
||||
...prev,
|
||||
widgets: [...prev.widgets.filter((x) => x.id !== innerProps.widgetId), currentWidget!],
|
||||
widgets: [...prev.widgets.filter((x) => x.id !== innerProps.widget.id), currentWidget!],
|
||||
};
|
||||
},
|
||||
true
|
||||
@@ -55,8 +55,8 @@ export const ChangeWidgetPositionModal = ({
|
||||
closeModal(id);
|
||||
};
|
||||
|
||||
const widthData = useWidthData(innerProps.widgetType);
|
||||
const heightData = useHeightData(innerProps.widgetType);
|
||||
const widthData = useWidthData(innerProps.widget.sort);
|
||||
const heightData = useHeightData(innerProps.widget.sort);
|
||||
|
||||
return (
|
||||
<ChangePositionModal
|
||||
|
||||
@@ -18,13 +18,19 @@ import { ContextModalProps } from '@mantine/modals';
|
||||
import { IconAlertTriangle, IconPlaylistX, IconPlus } from '@tabler/icons-react';
|
||||
import { Trans, useTranslation } from 'next-i18next';
|
||||
import { FC, useState } from 'react';
|
||||
|
||||
import { WidgetItem } from '~/components/Board/context';
|
||||
import { useWidgetActions } from '~/components/Board/widget-actions';
|
||||
import { useConfigContext } from '~/config/provider';
|
||||
import { useConfigStore } from '~/config/store';
|
||||
import { mapObject } from '~/tools/client/objects';
|
||||
import { objectEntries } from '~/tools/object';
|
||||
import type {
|
||||
IDraggableListInputValue,
|
||||
IWidgetDefinition,
|
||||
IWidgetOptionValue,
|
||||
} from '~/widgets/widgets';
|
||||
|
||||
import Widgets from '../../../../widgets';
|
||||
import type { IDraggableListInputValue, IWidgetOptionValue } from '~/widgets/widgets';
|
||||
import { IWidget } from '~/widgets/widgets';
|
||||
import { InfoCard } from '../../../InfoCard/InfoCard';
|
||||
import { DraggableList } from './Inputs/DraggableList';
|
||||
import { LocationSelection } from './Inputs/LocationSelection';
|
||||
@@ -33,12 +39,11 @@ import { StaticDraggableList } from './Inputs/StaticDraggableList';
|
||||
export type WidgetEditModalInnerProps = {
|
||||
widgetId: string;
|
||||
widgetType: string;
|
||||
options: IWidget<string, any>['properties'];
|
||||
widgetOptions: IWidget<string, any>['properties'];
|
||||
options: Record<string, unknown>;
|
||||
widgetOptions: IWidgetDefinition['options'];
|
||||
boardName: string;
|
||||
};
|
||||
|
||||
export type IntegrationOptionsValueType = IWidget<string, any>['properties'][string];
|
||||
|
||||
export const WidgetsEditModal = ({
|
||||
context,
|
||||
id,
|
||||
@@ -46,19 +51,15 @@ export const WidgetsEditModal = ({
|
||||
}: ContextModalProps<WidgetEditModalInnerProps>) => {
|
||||
const { t } = useTranslation([`modules/${innerProps.widgetType}`, 'common']);
|
||||
const [moduleProperties, setModuleProperties] = useState(innerProps.options);
|
||||
const items = Object.entries(innerProps.widgetOptions ?? {}) as [
|
||||
string,
|
||||
IntegrationOptionsValueType,
|
||||
][];
|
||||
const items = objectEntries(innerProps.widgetOptions ?? {});
|
||||
|
||||
// Find the Key in the "Widgets" Object that matches the widgetId
|
||||
const currentWidgetDefinition = Widgets[innerProps.widgetType as keyof typeof Widgets];
|
||||
const { name: configName } = useConfigContext();
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
const { updateWidgetOptions } = useWidgetActions({ boardName: innerProps.boardName });
|
||||
|
||||
if (!configName || !innerProps.options) return null;
|
||||
if (!innerProps.options) return null;
|
||||
|
||||
const handleChange = (key: string, value: IntegrationOptionsValueType) => {
|
||||
const handleChange = (key: string, value: unknown) => {
|
||||
setModuleProperties((prev) => {
|
||||
const copyOfPrev: any = { ...prev };
|
||||
copyOfPrev[key] = value;
|
||||
@@ -67,19 +68,11 @@ export const WidgetsEditModal = ({
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
updateConfig(
|
||||
configName,
|
||||
(prev) => {
|
||||
const currentWidget = prev.widgets.find((x) => x.id === innerProps.widgetId);
|
||||
currentWidget!.properties = moduleProperties;
|
||||
updateWidgetOptions({
|
||||
itemId: innerProps.widgetId,
|
||||
newOptions: moduleProperties,
|
||||
});
|
||||
|
||||
return {
|
||||
...prev,
|
||||
widgets: [...prev.widgets.filter((x) => x.id !== innerProps.widgetId), currentWidget!],
|
||||
};
|
||||
},
|
||||
true
|
||||
);
|
||||
context.closeModal(id);
|
||||
};
|
||||
|
||||
@@ -132,7 +125,7 @@ const WidgetOptionTypeSwitch: FC<{
|
||||
widgetId: string;
|
||||
propName: string;
|
||||
value: any;
|
||||
handleChange: (key: string, value: IntegrationOptionsValueType) => void;
|
||||
handleChange: (key: string, value: unknown) => void;
|
||||
}> = ({ option, widgetId, propName: key, value, handleChange }) => {
|
||||
const { t } = useTranslation([`modules/${widgetId}`, 'common']);
|
||||
const info = option.info ?? false;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Title } from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { WidgetItem } from '~/components/Board/context';
|
||||
import { WidgetItem, useRequiredBoard } from '~/components/Board/context';
|
||||
import { openContextModalGeneric } from '~/tools/mantineModalManagerExtensions';
|
||||
import { IWidget } from '~/widgets/widgets';
|
||||
|
||||
import WidgetsDefinitions from '../../../../widgets';
|
||||
import { useWrapperColumnCount } from '../../Wrappers/gridstack/store';
|
||||
@@ -11,30 +10,23 @@ import { WidgetEditModalInnerProps } from './WidgetsEditModal';
|
||||
import { WidgetsRemoveModalInnerProps } from './WidgetsRemoveModal';
|
||||
|
||||
export type WidgetChangePositionModalInnerProps = {
|
||||
widgetId: string;
|
||||
widgetType: string;
|
||||
widget: WidgetItem;
|
||||
wrapperColumnCount: number;
|
||||
};
|
||||
|
||||
interface WidgetsMenuProps {
|
||||
integration: string;
|
||||
type: string;
|
||||
widget: WidgetItem | undefined;
|
||||
}
|
||||
|
||||
export const WidgetsMenu = ({ integration, widget }: WidgetsMenuProps) => {
|
||||
const { t } = useTranslation(`modules/${integration}`);
|
||||
export const WidgetsMenu = ({ type, widget }: WidgetsMenuProps) => {
|
||||
const { t } = useTranslation(`modules/${type}`);
|
||||
const board = useRequiredBoard();
|
||||
const wrapperColumnCount = useWrapperColumnCount();
|
||||
|
||||
if (!widget || !wrapperColumnCount) return null;
|
||||
// Match widget.id with WidgetsDefinitions
|
||||
// First get the keys
|
||||
const keys = Object.keys(WidgetsDefinitions);
|
||||
// Then find the key that matches the widget.type
|
||||
const widgetDefinition = keys.find((key) => key === widget.type);
|
||||
// Then get the widget definition
|
||||
const widgetDefinitionObject =
|
||||
WidgetsDefinitions[widgetDefinition as keyof typeof WidgetsDefinitions];
|
||||
const widgetDefinitionObject = WidgetsDefinitions[widget.sort as keyof typeof WidgetsDefinitions];
|
||||
|
||||
const handleDeleteClick = () => {
|
||||
openContextModalGeneric<WidgetsRemoveModalInnerProps>({
|
||||
@@ -42,7 +34,7 @@ export const WidgetsMenu = ({ integration, widget }: WidgetsMenuProps) => {
|
||||
title: <Title order={4}>{t('common:remove')}</Title>,
|
||||
innerProps: {
|
||||
widgetId: widget.id,
|
||||
widgetType: integration,
|
||||
widgetType: type,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -53,8 +45,6 @@ export const WidgetsMenu = ({ integration, widget }: WidgetsMenuProps) => {
|
||||
size: 'xl',
|
||||
title: null,
|
||||
innerProps: {
|
||||
widgetId: widget.id,
|
||||
widgetType: integration,
|
||||
widget,
|
||||
wrapperColumnCount,
|
||||
},
|
||||
@@ -67,10 +57,10 @@ export const WidgetsMenu = ({ integration, widget }: WidgetsMenuProps) => {
|
||||
title: <Title order={4}>{t('descriptor.settings.title')}</Title>,
|
||||
innerProps: {
|
||||
widgetId: widget.id,
|
||||
widgetType: integration,
|
||||
widgetType: type,
|
||||
options: widget.options,
|
||||
// Cast as the right type for the correct widget
|
||||
widgetOptions: widgetDefinitionObject.options as any,
|
||||
boardName: board.name,
|
||||
widgetOptions: widgetDefinitionObject.options,
|
||||
},
|
||||
zIndex: 250,
|
||||
});
|
||||
|
||||
@@ -62,7 +62,7 @@ export function WrapperContent({ items, refs }: WrapperContentProps) {
|
||||
<WidgetWrapper
|
||||
className="grid-stack-item-content"
|
||||
widget={widget}
|
||||
widgetType={widget.type}
|
||||
widgetType={widget.sort}
|
||||
WidgetComponent={definition.component as any}
|
||||
/>
|
||||
</GridstackTileWrapper>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GridStack, GridStackNode } from 'fily-publish-gridstack';
|
||||
import { GridItemHTMLElement, GridStack, GridStackNode } from 'fily-publish-gridstack';
|
||||
import { MutableRefObject, RefObject } from 'react';
|
||||
import { Item, Section } from '~/components/Board/context';
|
||||
|
||||
@@ -6,7 +6,7 @@ type InitializeGridstackProps = {
|
||||
section: Section;
|
||||
refs: {
|
||||
wrapper: RefObject<HTMLDivElement>;
|
||||
items: MutableRefObject<Record<string, RefObject<HTMLDivElement>>>;
|
||||
items: MutableRefObject<Record<string, RefObject<GridItemHTMLElement>>>;
|
||||
gridstack: MutableRefObject<GridStack | undefined>;
|
||||
};
|
||||
isEditMode: boolean;
|
||||
@@ -73,12 +73,12 @@ export const initializeGridstack = ({
|
||||
section.items.forEach((item) => {
|
||||
const ref = refs.items.current[item.id]?.current;
|
||||
setAttributesFromShape(ref, item);
|
||||
ref && grid.makeWidget(ref as HTMLDivElement);
|
||||
ref && grid.makeWidget(ref);
|
||||
});
|
||||
grid.batchUpdate(false);
|
||||
};
|
||||
|
||||
function setAttributesFromShape(ref: HTMLDivElement | null, item: Item) {
|
||||
function setAttributesFromShape(ref: GridItemHTMLElement | null, item: Item) {
|
||||
if (!item || !ref) return;
|
||||
ref.setAttribute('gs-x', item.x.toString());
|
||||
ref.setAttribute('gs-y', item.y.toString());
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { GridStack, GridStackNode } from 'fily-publish-gridstack';
|
||||
import {
|
||||
GridItemHTMLElement,
|
||||
GridStack,
|
||||
GridStackElement,
|
||||
GridStackNode,
|
||||
} from 'fily-publish-gridstack';
|
||||
import {
|
||||
MutableRefObject,
|
||||
RefObject,
|
||||
@@ -18,7 +23,7 @@ import { useGridstackStore, useWrapperColumnCount } from './store';
|
||||
interface UseGristackReturnType {
|
||||
refs: {
|
||||
wrapper: RefObject<HTMLDivElement>;
|
||||
items: MutableRefObject<Record<string, RefObject<HTMLDivElement>>>;
|
||||
items: MutableRefObject<Record<string, RefObject<GridItemHTMLElement>>>;
|
||||
gridstack: MutableRefObject<GridStack | undefined>;
|
||||
};
|
||||
}
|
||||
@@ -33,7 +38,7 @@ export const useGridstack = ({ section }: UseGridstackProps): UseGristackReturnT
|
||||
// define reference for wrapper - is used to calculate the width of the wrapper
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
// references to the diffrent items contained in the gridstack
|
||||
const itemRefs = useRef<Record<string, RefObject<HTMLDivElement>>>({});
|
||||
const itemRefs = useRef<Record<string, RefObject<GridItemHTMLElement>>>({});
|
||||
// reference of the gridstack object for modifications after initialization
|
||||
const gridRef = useRef<GridStack>();
|
||||
const sectionColumnCount = useWrapperColumnCount();
|
||||
@@ -70,6 +75,10 @@ export const useGridstack = ({ section }: UseGridstackProps): UseGristackReturnT
|
||||
root?.style.setProperty('--gridstack-column-count', sectionColumnCount.toString());
|
||||
}, [sectionColumnCount]);
|
||||
|
||||
useEffect(() => {
|
||||
gridRef.current?.setStatic(!isEditMode);
|
||||
}, [isEditMode]);
|
||||
|
||||
const onChange = useCallback(
|
||||
(changedNode: GridStackNode) => {
|
||||
if (!isEditMode) return;
|
||||
|
||||
@@ -43,7 +43,7 @@ export const WidgetWrapper = ({
|
||||
return (
|
||||
<ErrorBoundary integration={widgetType} widget={widgetWithDefaultProps}>
|
||||
<HomarrCardWrapper className={className}>
|
||||
<WidgetsMenu integration={widgetType} widget={widgetWithDefaultProps} />
|
||||
<WidgetsMenu type={widgetType} widget={widgetWithDefaultProps} />
|
||||
<WidgetComponent widget={widgetWithDefaultProps} />
|
||||
</HomarrCardWrapper>
|
||||
</ErrorBoundary>
|
||||
|
||||
@@ -56,7 +56,7 @@ class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundarySta
|
||||
withBorder
|
||||
h="calc(100% - 20px)"
|
||||
>
|
||||
<WidgetsMenu integration={this.props.integration} widget={this.props.widget} />
|
||||
<WidgetsMenu type={this.props.integration} widget={this.props.widget} />
|
||||
<ScrollArea h="100%" type="auto" offsetScrollbars>
|
||||
<Center>
|
||||
<Stack align="center" spacing="xs">
|
||||
|
||||
Reference in New Issue
Block a user