Merge branch 'manuel-rw-gridstack' into gridstack-wip-meierschlumpf

This commit is contained in:
Meierschlumpf
2023-01-07 17:59:43 +01:00
56 changed files with 962 additions and 785 deletions

View File

@@ -1,4 +1,4 @@
import { ActionIcon, createStyles } from '@mantine/core';
import { ActionIcon, createStyles, Space } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { IconChevronLeft, IconChevronRight } from '@tabler/icons';
import { useConfigContext } from '../../../../config/provider';
@@ -35,7 +35,9 @@ export const MobileRibbons = () => {
location="left"
/>
</>
) : null}
) : (
<Space />
)}
{layoutSettings.enabledRightSidebar ? (
<>

View File

@@ -1,4 +1,5 @@
import { Drawer, Title } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import { DashboardSidebar } from '../../Wrappers/Sidebar/Sidebar';
interface MobileRibbonSidebarDrawerProps {
@@ -10,16 +11,25 @@ interface MobileRibbonSidebarDrawerProps {
export const MobileRibbonSidebarDrawer = ({
location,
...props
}: MobileRibbonSidebarDrawerProps) => (
<Drawer
position={location}
title={<Title order={4}>{location} sidebar</Title>}
style={{
display: 'flex',
justifyContent: 'center',
}}
{...props}
>
<DashboardSidebar location={location} isGridstackReady />
</Drawer>
);
}: MobileRibbonSidebarDrawerProps) => {
const { t } = useTranslation('layout/mobile/drawer');
return (
<Drawer
padding={10}
position={location}
title={<Title order={4}>{t('title', { position: location })}</Title>}
style={{
display: 'flex',
justifyContent: 'center',
}}
styles={{
title: {
width: '100%',
},
}}
{...props}
>
<DashboardSidebar location={location} isGridstackReady />
</Drawer>
);
};

View File

@@ -36,7 +36,7 @@ export const IconSelector = ({ onChange, allowAppNamePropagation, form }: IconSe
const { data, isLoading } = useRepositoryIconsQuery<WalkxcodeRepositoryIcon>({
url: 'https://api.github.com/repos/walkxcode/Dashboard-Icons/contents/png',
converter: (item) => ({
url: `https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${item.name}`,
url: `https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/${item.name}`,
fileName: item.name,
}),
});

View File

@@ -9,15 +9,21 @@ import {
Stack,
ThemeIcon,
Title,
Text,
Badge,
Tooltip,
} from '@mantine/core';
import { TablerIcon } from '@tabler/icons';
import { useTranslation } from 'next-i18next';
import { useState } from 'react';
import { AppIntegrationPropertyAccessabilityType } from '../../../../../../../../types/app';
interface GenericSecretInputProps {
label: string;
value: string;
setIcon: TablerIcon;
secretIsPresent: boolean;
type: AppIntegrationPropertyAccessabilityType;
onClickUpdateButton: (value: string | undefined) => void;
}
@@ -25,6 +31,8 @@ export const GenericSecretInput = ({
label,
value,
setIcon,
secretIsPresent,
type,
onClickUpdateButton,
...props
}: GenericSecretInputProps) => {
@@ -36,17 +44,61 @@ export const GenericSecretInput = ({
const { t } = useTranslation(['layout/modals/add-app', 'common']);
return (
<Card withBorder>
<Card p="xs" withBorder>
<Grid>
<Grid.Col className={classes.alignSelfCenter} xs={12} md={6}>
<Group spacing="sm">
<ThemeIcon color="green" variant="light">
<ThemeIcon color={secretIsPresent ? 'green' : 'red'} variant="light" size="lg">
<Icon size={18} />
</ThemeIcon>
<Stack spacing={0}>
<Title className={classes.subtitle} order={6}>
{t(label)}
</Title>
<Group spacing="xs">
<Title className={classes.subtitle} order={6}>
{t(label)}
</Title>
<Group spacing="xs">
{secretIsPresent ? (
<Badge className={classes.textTransformUnset} color="green" variant="dot">
{t('integration.type.defined')}
</Badge>
) : (
<Badge className={classes.textTransformUnset} color="red" variant="dot">
{t('integration.type.undefined')}
</Badge>
)}
{type === 'private' ? (
<Tooltip
label={t('integration.type.explanationPrivate')}
width={200}
multiline
withinPortal
withArrow
>
<Badge className={classes.textTransformUnset} color="orange" variant="dot">
{t('integration.type.private')}
</Badge>
</Tooltip>
) : (
<Tooltip
label={t('integration.type.explanationPublic')}
width={200}
multiline
withinPortal
withArrow
>
<Badge className={classes.textTransformUnset} color="red" variant="dot">
{t('integration.type.public')}
</Badge>
</Tooltip>
)}
</Group>
</Group>
<Text size="xs" color="dimmed">
{type === 'private'
? 'Private: Once saved, you cannot read out this value again'
: 'Public: Can be read out repeatedly'}
</Text>
</Stack>
</Group>
</Grid.Col>
@@ -80,4 +132,7 @@ const useStyles = createStyles(() => ({
alignSelfCenter: {
alignSelf: 'center',
},
textTransformUnset: {
textTransform: 'inherit',
},
}));

View File

@@ -36,7 +36,7 @@ export const IntegrationSelector = ({ form }: IntegrationSelectorProps) => {
label: 'Transmission',
},
{
value: 'qbittorrent',
value: 'qBittorrent',
image: 'https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/qbittorrent.png',
label: 'qBittorrent',
},
@@ -100,16 +100,20 @@ export const IntegrationSelector = ({ form }: IntegrationSelectorProps) => {
placeholder={t('integration.type.placeholder')}
itemComponent={SelectItemComponent}
data={data}
maxDropdownHeight={150}
maxDropdownHeight={250}
dropdownPosition="bottom"
clearable
variant="default"
searchable
filter={(value, item) =>
item.label?.toLowerCase().includes(value.toLowerCase().trim()) ||
item.description?.toLowerCase().includes(value.toLowerCase().trim())
}
icon={
form.values.integration?.type && (
<img
src={data.find((x) => x.value === form.values.integration?.type)?.image}
alt="test"
alt="integration"
width={20}
height={20}
/>
@@ -119,6 +123,7 @@ export const IntegrationSelector = ({ form }: IntegrationSelectorProps) => {
form.setFieldValue('integration.properties', getNewProperties(value));
inputProps.onChange(value);
}}
withinPortal
{...inputProps}
/>
);
@@ -126,17 +131,23 @@ export const IntegrationSelector = ({ form }: IntegrationSelectorProps) => {
interface ItemProps extends React.ComponentPropsWithoutRef<'div'> {
image: string;
description: string;
label: string;
}
const SelectItemComponent = forwardRef<HTMLDivElement, ItemProps>(
({ image, label, ...others }: ItemProps, ref) => (
({ image, label, description, ...others }: ItemProps, ref) => (
<div ref={ref} {...others}>
<Group noWrap>
<img src={image} alt="integration icon" width={20} height={20} />
<div>
<Text size="sm">{label}</Text>
{description && (
<Text size="xs" color="dimmed">
{description}
</Text>
)}
</div>
</Group>
</div>

View File

@@ -45,6 +45,7 @@ export const IntegrationOptionsRenderer = ({ form }: IntegrationOptionsRendererP
const formValue = form.values.integration?.properties[indexInFormValue];
const isPresent = formValue?.isDefined;
const accessabilityType = formValue?.type;
if (!definition) {
return (
@@ -57,6 +58,7 @@ export const IntegrationOptionsRenderer = ({ form }: IntegrationOptionsRendererP
secretIsPresent={isPresent}
setIcon={IconKey}
value={formValue.value}
type={accessabilityType}
{...form.getInputProps(`integration.properties.${index}.value`)}
/>
);
@@ -72,6 +74,7 @@ export const IntegrationOptionsRenderer = ({ form }: IntegrationOptionsRendererP
value=""
secretIsPresent={isPresent}
setIcon={definition.icon}
type={accessabilityType}
{...form.getInputProps(`integration.properties.${index}.value`)}
/>
);

View File

@@ -97,8 +97,8 @@ export const AvailableElementTypes = ({
iconUrl: '/imgs/logo/logo.png',
},
network: {
enabledStatusChecker: false,
okStatus: [],
enabledStatusChecker: true,
okStatus: [200],
},
behaviour: {
isOpeningNewTab: true,

View File

@@ -1,5 +1,6 @@
import { useModals } from '@mantine/modals';
import { TablerIcon } from '@tabler/icons';
import { showNotification } from '@mantine/notifications';
import { IconChecks, TablerIcon } from '@tabler/icons';
import { useTranslation } from 'next-i18next';
import { useConfigContext } from '../../../../../../config/provider';
import { useConfigStore } from '../../../../../../config/store';
@@ -83,8 +84,13 @@ export const WidgetElementType = ({ id, image, disabled, widget }: WidgetElement
true,
!isEditMode
);
closeModal('selectElement');
showNotification({
title: t('descriptor.name'),
message: t('descriptor.description'),
icon: <IconChecks stroke={1.5} />,
color: 'teal',
});
};
return (

View File

@@ -1,4 +1,15 @@
import { Alert, Button, Group, MultiSelect, Stack, Switch, TextInput, Text } from '@mantine/core';
import {
Alert,
Button,
Group,
MultiSelect,
Stack,
Switch,
TextInput,
Text,
NumberInput,
Slider,
} from '@mantine/core';
import { ContextModalProps } from '@mantine/modals';
import { IconAlertTriangle } from '@tabler/icons';
import { Trans, useTranslation } from 'next-i18next';
@@ -8,10 +19,12 @@ import type { IWidgetOptionValue } from '../../../../widgets/widgets';
import { useConfigContext } from '../../../../config/provider';
import { useConfigStore } from '../../../../config/store';
import { IWidget } from '../../../../widgets/widgets';
import { useColorTheme } from '../../../../tools/color';
export type WidgetEditModalInnerProps = {
widgetId: string;
options: IWidget<string, any>['properties'];
widgetOptions: IWidget<string, any>['properties'];
};
type IntegrationOptionsValueType = IWidget<string, any>['properties'][string];
@@ -23,7 +36,11 @@ export const WidgetsEditModal = ({
}: ContextModalProps<WidgetEditModalInnerProps>) => {
const { t } = useTranslation([`modules/${innerProps.widgetId}`, 'common']);
const [moduleProperties, setModuleProperties] = useState(innerProps.options);
const items = Object.entries(moduleProperties ?? {}) as [string, IntegrationOptionsValueType][];
// const items = Object.entries(moduleProperties ?? {}) as [string, IntegrationOptionsValueType][];
const items = Object.entries(innerProps.widgetOptions ?? {}) as [
string,
IntegrationOptionsValueType
][];
// Find the Key in the "Widgets" Object that matches the widgetId
const currentWidgetDefinition = Widgets[innerProps.widgetId as keyof typeof Widgets];
@@ -67,8 +84,9 @@ export const WidgetsEditModal = ({
return (
<Stack>
{items.map(([key, value], index) => {
{items.map(([key, defaultValue], index) => {
const option = (currentWidgetDefinition as any).options[key] as IWidgetOptionValue;
const value = moduleProperties[key] ?? defaultValue;
if (!option) {
return (
@@ -83,39 +101,15 @@ export const WidgetsEditModal = ({
</Alert>
);
}
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)}
/>
);
case 'text':
return (
<TextInput
key={`${option.type}-${index}`}
label={t(`descriptor.settings.${key}.label`)}
value={value as string}
onChange={(ev) => handleChange(key, ev.currentTarget.value)}
/>
);
case 'multi-select':
return (
<MultiSelect
key={`${option.type}-${index}`}
data={getMutliselectData(key)}
label={t(`descriptor.settings.${key}.label`)}
value={value as string[]}
onChange={(v) => handleChange(key, v)}
/>
);
default:
return null;
}
return WidgetOptionTypeSwitch(
option,
index,
t,
key,
value,
handleChange,
getMutliselectData
);
})}
<Group position="right">
<Button onClick={() => context.closeModal(id)} variant="light">
@@ -126,3 +120,77 @@ export const WidgetsEditModal = ({
</Stack>
);
};
// 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,
getMutliselectData: (option: string) => any
) {
const { primaryColor, secondaryColor } = 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)}
/>
);
case 'text':
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)}
/>
);
case 'multi-select':
return (
<MultiSelect
color={primaryColor}
key={`${option.type}-${index}`}
data={getMutliselectData(key)}
label={t(`descriptor.settings.${key}.label`)}
value={value as string[]}
onChange={(v) => handleChange(key, v)}
/>
);
case 'number':
return (
<NumberInput
color={primaryColor}
key={`${option.type}-${index}`}
label={t(`descriptor.settings.${key}.label`)}
value={value as number}
onChange={(v) => handleChange(key, v!)}
/>
);
case 'slider':
return (
<Stack spacing="xs">
<Text>{t(`descriptor.settings.${key}.label`)}</Text>
<Slider
color={primaryColor}
key={`${option.type}-${index}`}
value={value as number}
min={option.min}
max={option.max}
step={option.step}
onChange={(v) => handleChange(key, v)}
/>
</Stack>
);
default:
return null;
}
}

View File

@@ -6,6 +6,7 @@ import { useWrapperColumnCount } from '../../Wrappers/gridstack/store';
import { GenericTileMenu } from '../GenericTileMenu';
import { WidgetEditModalInnerProps } from './WidgetsEditModal';
import { WidgetsRemoveModalInnerProps } from './WidgetsRemoveModal';
import WidgetsDefinitions from '../../../../widgets';
export type WidgetChangePositionModalInnerProps = {
widgetId: string;
@@ -23,6 +24,14 @@ export const WidgetsMenu = ({ integration, widget }: WidgetsMenuProps) => {
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.id
const widgetDefinition = keys.find((key) => key === widget.id);
// Then get the widget definition
const widgetDefinitionObject =
WidgetsDefinitions[widgetDefinition as keyof typeof WidgetsDefinitions];
const handleDeleteClick = () => {
openContextModalGeneric<WidgetsRemoveModalInnerProps>({
@@ -54,7 +63,10 @@ export const WidgetsMenu = ({ integration, widget }: WidgetsMenuProps) => {
innerProps: {
widgetId: integration,
options: widget.properties,
// Cast as the right type for the correct widget
widgetOptions: widgetDefinitionObject.options as any,
},
zIndex: 5,
});
};

View File

@@ -1,5 +1,6 @@
import { Card } from '@mantine/core';
import { RefObject } from 'react';
import { useCardStyles } from '../../../layout/useCardStyles';
import { useGridstack } from '../gridstack/use-gridstack';
import { WrapperContent } from '../WrapperContent';
@@ -30,18 +31,23 @@ const SidebarInner = ({ location }: DashboardSidebarInnerProps) => {
const { refs, apps, widgets } = useGridstack('sidebar', location);
const minRow = useMinRowForFullHeight(refs.wrapper);
const {
cx,
classes: { card: cardClass },
} = useCardStyles(false);
return (
<div
className="grid-stack grid-stack-sidebar"
style={{ transitionDuration: '0s', height: '100%' }}
data-sidebar={location}
// eslint-disable-next-line react/no-unknown-property
gs-min-row={minRow}
ref={refs.wrapper}
>
<WrapperContent apps={apps} refs={refs} widgets={widgets} />
</div>
<Card withBorder mih="100%" p={0} radius="lg" className={cardClass} ref={refs.wrapper}>
<div
className="grid-stack grid-stack-sidebar"
style={{ transitionDuration: '0s', height: '100%' }}
data-sidebar={location}
// eslint-disable-next-line react/no-unknown-property
gs-min-row={minRow}
>
<WrapperContent apps={apps} refs={refs} widgets={widgets} />
</div>
</Card>
);
};