Add options to sort and resize graphs in dash. widget

This commit is contained in:
MauriceNino
2023-02-10 18:20:28 +01:00
parent bb010ff54a
commit a05e80bf26
11 changed files with 436 additions and 220 deletions

View File

@@ -0,0 +1,119 @@
import { Collapse, createStyles, Stack, Text } from '@mantine/core';
import { IconChevronDown, IconGripVertical } from '@tabler/icons';
import { Reorder, useDragControls } from 'framer-motion';
import { FC, ReactNode, useState } from 'react';
import { IDraggableListInputValue } from '../../../../widgets/widgets';
const useStyles = createStyles((theme) => ({
container: {
display: 'flex',
flexDirection: 'column',
borderRadius: theme.radius.md,
border: `1px solid ${
theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[2]
}`,
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.white,
marginBottom: theme.spacing.xs,
gap: theme.spacing.xs,
},
row: {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '12px 16px',
gap: theme.spacing.sm,
},
middle: {
flexGrow: 1,
},
symbol: {
fontSize: 16,
},
clickableIcons: {
color: theme.colorScheme === 'dark' ? theme.colors.dark[1] : theme.colors.gray[6],
cursor: 'pointer',
userSelect: 'none',
transition: 'transform .3s ease-in-out',
},
rotate: {
transform: 'rotate(180deg)',
},
collapseContent: {
padding: '12px 16px',
},
}));
type DraggableListParams = {
value: IDraggableListInputValue['defaultValue'];
onChange: (value: IDraggableListInputValue['defaultValue']) => void;
labels: Record<string, string>;
children?: Record<string, ReactNode>;
};
export const DraggableList: FC<DraggableListParams> = (props) => {
const keys = props.value.map((v) => v.key);
return (
<div>
<Reorder.Group
axis="y"
values={keys}
onReorder={(order) =>
props.onChange(order.map((key) => props.value.find((v) => v.key === key)!))
}
as="div"
>
{props.value.map((item) => (
<ListItem key={item.key} item={item} label={props.labels[item.key]}>
{props.children?.[item.key]}
</ListItem>
))}
</Reorder.Group>
</div>
);
};
const ListItem: FC<{
item: IDraggableListInputValue['defaultValue'][number];
label: string;
}> = (props) => {
const { classes, cx } = useStyles();
const controls = useDragControls();
const [showContent, setShowContent] = useState(false);
const hasContent = props.children != null && Object.keys(props.children).length !== 0;
return (
<Reorder.Item value={props.item.key} dragListener={false} dragControls={controls} as="div">
<div className={classes.container}>
<div className={classes.row}>
<IconGripVertical
className={classes.clickableIcons}
onPointerDown={(e) => controls.start(e)}
size={18}
stroke={1.5}
/>
<div className={classes.middle}>
<Text className={classes.symbol}>{props.label}</Text>
</div>
{hasContent && (
<IconChevronDown
className={cx(classes.clickableIcons, { [classes.rotate]: showContent })}
onClick={() => setShowContent(!showContent)}
size={18}
stroke={1.5}
/>
)}
</div>
{hasContent && (
<Collapse in={showContent}>
<Stack className={classes.collapseContent}>{props.children}</Stack>
</Collapse>
)}
</div>
</Reorder.Item>
);
};

View File

@@ -3,24 +3,25 @@ import {
Button,
Group,
MultiSelect,
NumberInput,
Select,
Slider,
Stack,
Switch,
TextInput,
Text,
NumberInput,
Slider,
Select,
TextInput,
} from '@mantine/core';
import { ContextModalProps } from '@mantine/modals';
import { IconAlertTriangle } from '@tabler/icons';
import { Trans, useTranslation } from 'next-i18next';
import { useState } from 'react';
import Widgets from '../../../../widgets';
import type { IWidgetOptionValue } from '../../../../widgets/widgets';
import { FC, useState } from 'react';
import { useConfigContext } from '../../../../config/provider';
import { useConfigStore } from '../../../../config/store';
import { IWidget } from '../../../../widgets/widgets';
import { useColorTheme } from '../../../../tools/color';
import Widgets from '../../../../widgets';
import type { IDraggableListInputValue, IWidgetOptionValue } from '../../../../widgets/widgets';
import { IWidget } from '../../../../widgets/widgets';
import { DraggableList } from './DraggableList';
export type WidgetEditModalInnerProps = {
widgetId: string;
@@ -93,7 +94,17 @@ export const WidgetsEditModal = ({
</Alert>
);
}
return WidgetOptionTypeSwitch(option, index, t, key, value, handleChange);
return (
<WidgetOptionTypeSwitch
key={`${key}.${index}`}
option={option}
widgetId={innerProps.widgetId}
propName={key}
value={value}
handleChange={handleChange}
/>
);
})}
<Group position="right">
<Button onClick={() => context.closeModal(id)} variant="light">
@@ -108,20 +119,20 @@ export const WidgetsEditModal = ({
// Widget switch
// Widget options are computed based on their type.
// here you can define new types for options (along with editing the widgets.d.ts file)
function WidgetOptionTypeSwitch(
option: IWidgetOptionValue,
index: number,
t: any,
key: string,
value: string | number | boolean | string[],
handleChange: (key: string, value: IntegrationOptionsValueType) => void
) {
const { primaryColor, secondaryColor } = useColorTheme();
const WidgetOptionTypeSwitch: FC<{
option: IWidgetOptionValue;
widgetId: string;
propName: string;
value: any;
handleChange: (key: string, value: IntegrationOptionsValueType) => void;
}> = ({ option, widgetId, propName: key, value, handleChange }) => {
const { t } = useTranslation([`modules/${widgetId}`, 'common']);
const { primaryColor } = useColorTheme();
switch (option.type) {
case 'switch':
return (
<Switch
key={`${option.type}-${index}`}
label={t(`descriptor.settings.${key}.label`)}
checked={value as boolean}
onChange={(ev) => handleChange(key, ev.currentTarget.checked)}
@@ -131,7 +142,6 @@ function WidgetOptionTypeSwitch(
return (
<TextInput
color={primaryColor}
key={`${option.type}-${index}`}
label={t(`descriptor.settings.${key}.label`)}
value={value as string}
onChange={(ev) => handleChange(key, ev.currentTarget.value)}
@@ -141,7 +151,6 @@ function WidgetOptionTypeSwitch(
return (
<MultiSelect
color={primaryColor}
key={`${option.type}-${index}`}
data={option.data}
label={t(`descriptor.settings.${key}.label`)}
value={value as string[]}
@@ -153,7 +162,6 @@ function WidgetOptionTypeSwitch(
return (
<Select
color={primaryColor}
key={`${option.type}-${index}`}
defaultValue={option.defaultValue}
data={option.data}
label={t(`descriptor.settings.${key}.label`)}
@@ -165,7 +173,6 @@ function WidgetOptionTypeSwitch(
return (
<NumberInput
color={primaryColor}
key={`${option.type}-${index}`}
label={t(`descriptor.settings.${key}.label`)}
value={value as number}
onChange={(v) => handleChange(key, v!)}
@@ -176,7 +183,6 @@ function WidgetOptionTypeSwitch(
<Stack spacing="xs">
<Slider
color={primaryColor}
key={`${option.type}-${index}`}
label={value}
value={value as number}
min={option.min}
@@ -186,7 +192,57 @@ function WidgetOptionTypeSwitch(
/>
</Stack>
);
case 'draggable-list':
// eslint-disable-next-line no-case-declarations
const typedVal = value as IDraggableListInputValue['defaultValue'];
return (
<Stack spacing="xs">
<Text>{t(`descriptor.settings.${key}.label`)}</Text>
<DraggableList
value={typedVal}
onChange={(v) => handleChange(key, v)}
labels={Object.fromEntries(
Object.entries(option.items).map(([graphName]) => [
graphName,
t(`descriptor.settings.${graphName}.label`),
])
)}
>
{Object.fromEntries(
Object.entries(option.items).map(([graphName, graph]) => [
graphName,
Object.entries(graph).map(([subKey, setting], i) => (
<WidgetOptionTypeSwitch
key={`${graphName}.${subKey}.${i}`}
option={setting as IWidgetOptionValue}
widgetId={widgetId}
propName={`${graphName}.${subKey}`}
value={typedVal.find((v) => v.key === graphName)?.subValues?.[subKey]}
handleChange={(_, newVal) =>
handleChange(
key,
typedVal.map((oldVal) =>
oldVal.key === graphName
? {
...oldVal,
subValues: {
...oldVal.subValues,
[subKey]: newVal,
},
}
: oldVal
)
)
}
/>
)),
])
)}
</DraggableList>
</Stack>
);
default:
return null;
}
}
};

View File

@@ -83,6 +83,7 @@ export const WidgetsMenu = ({ integration, widget }: WidgetsMenuProps) => {
inner: {
position: 'sticky',
top: 30,
maxHeight: '100%',
},
},
});

View File

@@ -228,33 +228,37 @@ export const useCategoryActions = (configName: string | undefined, category: Cat
...previous.apps.filter((x) => !isAppAffectedFilter(x)),
...previous.apps
.filter((x) => isAppAffectedFilter(x))
.map((app): AppType => ({
...app,
area: {
...app.area,
type: 'wrapper',
properties: {
...app.area.properties,
id: mainWrapperId,
.map(
(app): AppType => ({
...app,
area: {
...app.area,
type: 'wrapper',
properties: {
...app.area.properties,
id: mainWrapperId,
},
},
},
})),
})
),
],
widgets: [
...previous.widgets.filter((widget) => !isWidgetAffectedFilter(widget)),
...previous.widgets
.filter((widget) => isWidgetAffectedFilter(widget))
.map((widget): IWidget<string, any> => ({
...widget,
area: {
...widget.area,
type: 'wrapper',
properties: {
...widget.area.properties,
id: mainWrapperId,
.map(
(widget): IWidget<string, any> => ({
...widget,
area: {
...widget.area,
type: 'wrapper',
properties: {
...widget.area.properties,
id: mainWrapperId,
},
},
},
})),
})
),
],
categories: previous.categories.filter((x) => x.id !== category.id),
wrappers: previous.wrappers.filter((x) => x.position !== currentItem.position),