Merge branch 'manuel-rw-gridstack' into gridstack-wip-meierschlumpf
This commit is contained in:
@@ -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 ? (
|
||||
<>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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`)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -97,8 +97,8 @@ export const AvailableElementTypes = ({
|
||||
iconUrl: '/imgs/logo/logo.png',
|
||||
},
|
||||
network: {
|
||||
enabledStatusChecker: false,
|
||||
okStatus: [],
|
||||
enabledStatusChecker: true,
|
||||
okStatus: [200],
|
||||
},
|
||||
behaviour: {
|
||||
isOpeningNewTab: true,
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user