Add client side widget modification, improve switching between edit mode

This commit is contained in:
Meier Lukas
2023-10-01 15:54:43 +02:00
parent 1b4070c9ce
commit 0745921ed1
10 changed files with 96 additions and 65 deletions

View 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,
};
};

View File

@@ -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 />
</>
);

View File

@@ -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

View File

@@ -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;

View File

@@ -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,
});

View File

@@ -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>

View File

@@ -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());

View File

@@ -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;

View File

@@ -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>

View File

@@ -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">