Custom column counts for gridstack #613 #660

This commit is contained in:
Manuel
2023-02-05 17:16:03 +01:00
committed by GitHub
parent 5296ce88d2
commit 2539e8cec1
37 changed files with 2064 additions and 745 deletions

View File

@@ -0,0 +1,112 @@
import { Alert, Button, Grid, Input, LoadingOverlay, Slider } from '@mantine/core';
import { useForm } from '@mantine/form';
import { IconCheck, IconReload } from '@tabler/icons';
import { useTranslation } from 'next-i18next';
import { useState } from 'react';
import { useConfigContext } from '../../../../config/provider';
import { useConfigStore } from '../../../../config/store';
import { GridstackBreakpoints } from '../../../../constants/gridstack-breakpoints';
import { sleep } from '../../../../tools/client/time';
import { GridstackSettingsType } from '../../../../types/settings';
export const GridstackConfiguration = () => {
const { t } = useTranslation(['settings/customization/gridstack', 'common']);
const { config, name: configName } = useConfigContext();
const updateConfig = useConfigStore((x) => x.updateConfig);
if (!config || !configName) {
return null;
}
const initialValue = config.settings.customization?.gridstack ?? {
columnCountSmall: 3,
columnCountMedium: 6,
columnCountLarge: 12,
};
const form = useForm({
initialValues: initialValue,
});
const [isSaving, setIsSaving] = useState(false);
const handleSubmit = async (values: GridstackSettingsType) => {
setIsSaving(true);
await sleep(250);
await updateConfig(
configName,
(previousConfig) => ({
...previousConfig,
settings: {
...previousConfig.settings,
customization: {
...previousConfig.settings.customization,
gridstack: values,
},
},
}),
true,
true
);
form.resetDirty();
setIsSaving(false);
};
return (
<form onSubmit={form.onSubmit(handleSubmit)} style={{ position: 'relative' }}>
<LoadingOverlay overlayBlur={2} visible={isSaving} radius="md" />
<Input.Wrapper
label={t('columnsCount.labelPreset', { size: t('common:breakPoints.small') })}
description={t('columnsCount.descriptionPreset', { pixels: GridstackBreakpoints.medium })}
mb="md"
>
<Slider min={1} max={8} mt="xs" {...form.getInputProps('columnCountSmall')} />
</Input.Wrapper>
<Input.Wrapper
label={t('columnsCount.labelPreset', { size: t('common:breakPoints.medium') })}
description={t('columnsCount.descriptionPreset', { pixels: GridstackBreakpoints.large })}
mb="md"
>
<Slider min={3} max={16} mt="xs" {...form.getInputProps('columnCountMedium')} />
</Input.Wrapper>
<Input.Wrapper
label={t('columnsCount.labelPreset', { size: t('common:breakPoints.large') })}
description={t('columnsCount.descriptionExceedsPreset', {
pixels: GridstackBreakpoints.large,
})}
>
<Slider min={5} max={20} mt="xs" {...form.getInputProps('columnCountLarge')} />
</Input.Wrapper>
{form.isDirty() && (
<Alert variant="light" color="yellow" title="Unsaved changes" my="md">
{t('unsavedChanges')}
</Alert>
)}
<Grid mt="md">
<Grid.Col md={6} xs={12}>
<Button variant="light" leftIcon={<IconCheck size={18} />} type="submit" fullWidth>
{t('applyChanges')}
</Button>
</Grid.Col>
<Grid.Col md={6} xs={12}>
<Button
variant="light"
leftIcon={<IconReload size={18} />}
onClick={() =>
form.setValues({
columnCountSmall: 3,
columnCountMedium: 6,
columnCountLarge: 12,
})
}
fullWidth
>
{t('defaultValues')}
</Button>
</Grid.Col>
</Grid>
</form>
);
};

View File

@@ -1,43 +1,39 @@
import {
ActionIcon,
Checkbox,
createStyles,
Divider,
Flex,
Group,
Indicator,
Paper,
Stack,
Text,
Title,
useMantineTheme,
} from '@mantine/core';
import { useTranslation } from 'next-i18next';
import { ChangeEvent, Dispatch, SetStateAction, useState } from 'react';
import { useConfigContext } from '../../../../config/provider';
import { useConfigStore } from '../../../../config/store';
import { createDummyArray } from '../../../../tools/client/arrays';
import { CustomizationSettingsType } from '../../../../types/settings';
import { Logo } from '../../../layout/Logo';
interface LayoutSelectorProps {
defaultLayout: CustomizationSettingsType['layout'] | undefined;
}
// TODO: add translations
export const LayoutSelector = ({ defaultLayout }: LayoutSelectorProps) => {
export const LayoutSelector = () => {
const { classes } = useStyles();
const { name: configName } = useConfigContext();
const { config, name: configName } = useConfigContext();
const updateConfig = useConfigStore((x) => x.updateConfig);
const [leftSidebar, setLeftSidebar] = useState(defaultLayout?.enabledLeftSidebar ?? true);
const [rightSidebar, setRightSidebar] = useState(defaultLayout?.enabledRightSidebar ?? true);
const [docker, setDocker] = useState(defaultLayout?.enabledDocker ?? false);
const [ping, setPing] = useState(defaultLayout?.enabledPing ?? false);
const [searchBar, setSearchBar] = useState(defaultLayout?.enabledSearchbar ?? false);
const layoutSettings = config?.settings.customization.layout;
const { colors, colorScheme } = useMantineTheme();
const [leftSidebar, setLeftSidebar] = useState(layoutSettings?.enabledLeftSidebar ?? true);
const [rightSidebar, setRightSidebar] = useState(layoutSettings?.enabledRightSidebar ?? true);
const [docker, setDocker] = useState(layoutSettings?.enabledDocker ?? false);
const [ping, setPing] = useState(layoutSettings?.enabledPing ?? false);
const [searchBar, setSearchBar] = useState(layoutSettings?.enabledSearchbar ?? false);
const { t } = useTranslation('settings/common');
if (!configName) return null;
if (!configName || !config) return null;
const handleChange = (
key: keyof CustomizationSettingsType['layout'],
@@ -68,99 +64,140 @@ export const LayoutSelector = ({ defaultLayout }: LayoutSelectorProps) => {
);
};
const enabledPing = layoutSettings?.enabledPing ?? false;
return (
<Stack spacing="xs">
<Title order={6}>{t('layout.title')}</Title>
<Paper px="xs" py={4} withBorder>
<Group position="apart">
<Logo size="xs" />
<Group spacing={5}>
{searchBar ? (
<Paper
style={{
height: 10,
backgroundColor: colorScheme === 'dark' ? colors.gray[8] : colors.gray[1],
}}
p={2}
w={60}
/>
) : null}
{docker ? <ActionIcon size={10} disabled /> : null}
<>
<Stack spacing={0} mb="md">
<Title order={6}>{t('layout.preview.title')}</Title>
<Text color="dimmed" size="xs">
{t('layout.preview.subtitle')}
</Text>
</Stack>
<Stack spacing="xs">
<Paper px="xs" py={4} withBorder>
<Group position="apart">
<Logo size="xs" />
<Group spacing={5}>
{searchBar && <PlaceholderElement width={60} height={10} />}
{docker && <PlaceholderElement width={10} height={10} />}
</Group>
</Group>
</Group>
</Paper>
<Group align="stretch">
{leftSidebar && (
<Paper className={classes.secondaryWrapper} p="xs" withBorder>
<Flex align="center" justify="center" direction="column">
<Text align="center">{t('layout.sidebar')}</Text>
<Text color="dimmed" size="xs" align="center">
Only for
<br />
apps &<br />
integrations
</Text>
</Flex>
</Paper>
)}
<Paper className={classes.primaryWrapper} p="xs" withBorder>
<Text align="center">{t('layout.main')}</Text>
<Text color="dimmed" size="xs" align="center">
{t('layout.cannotturnoff')}
</Text>
</Paper>
{rightSidebar && (
<Paper className={classes.secondaryWrapper} p="xs" withBorder>
<Flex align="center" justify="center" direction="column">
<Text align="center">{t('layout.sidebar')}</Text>
<Text color="dimmed" size="xs" align="center">
Only for
<br />
apps &<br />
integrations
</Text>
<Flex gap={6}>
{leftSidebar && (
<Paper className={classes.secondaryWrapper} p="xs" withBorder>
<Flex gap={5} wrap="wrap">
{createDummyArray(5).map((item, index) => (
<PlaceholderElement
height={index % 4 === 0 ? 60 + 5 : 30}
width={30}
key={`example-item-right-sidebard-${index}`}
index={index}
hasPing={enabledPing}
/>
))}
</Flex>
</Paper>
)}
<Paper className={classes.primaryWrapper} p="xs" withBorder>
<Flex gap={5} wrap="wrap">
{createDummyArray(10).map((item, index) => (
<PlaceholderElement
height={30}
width={index % 5 === 0 ? 60 : 30}
key={`example-item-main-${index}`}
index={index}
hasPing={enabledPing}
/>
))}
</Flex>
</Paper>
)}
</Group>
<Stack spacing="xs">
<Checkbox
label={t('layout.enablelsidebar')}
description={t('layout.enablelsidebardesc')}
checked={leftSidebar}
onChange={(ev) => handleChange('enabledLeftSidebar', ev, setLeftSidebar)}
/>
<Checkbox
label={t('layout.enablersidebar')}
description={t('layout.enablersidebardesc')}
checked={rightSidebar}
onChange={(ev) => handleChange('enabledRightSidebar', ev, setRightSidebar)}
/>
<Checkbox
label={t('layout.enablesearchbar')}
checked={searchBar}
onChange={(ev) => handleChange('enabledSearchbar', ev, setSearchBar)}
/>
<Checkbox
label={t('layout.enabledocker')}
checked={docker}
onChange={(ev) => handleChange('enabledDocker', ev, setDocker)}
/>
<Checkbox
label={t('layout.enableping')}
checked={ping}
onChange={(ev) => handleChange('enabledPing', ev, setPing)}
/>
{rightSidebar && (
<Paper className={classes.secondaryWrapper} p="xs" withBorder>
<Flex gap={5} align="start" wrap="wrap">
{createDummyArray(5).map((item, index) => (
<PlaceholderElement
height={30}
width={index % 4 === 0 ? 60 + 5 : 30}
key={`example-item-right-sidebard-${index}`}
index={index}
hasPing={enabledPing}
/>
))}
</Flex>
</Paper>
)}
</Flex>
<Divider label={t('layout.divider')} labelPosition="center" mt="md" mb="xs" />
<Stack spacing="xs">
<Checkbox
label={t('layout.enablelsidebar')}
description={t('layout.enablelsidebardesc')}
checked={leftSidebar}
onChange={(ev) => handleChange('enabledLeftSidebar', ev, setLeftSidebar)}
/>
<Checkbox
label={t('layout.enablersidebar')}
description={t('layout.enablersidebardesc')}
checked={rightSidebar}
onChange={(ev) => handleChange('enabledRightSidebar', ev, setRightSidebar)}
/>
<Checkbox
label={t('layout.enablesearchbar')}
checked={searchBar}
onChange={(ev) => handleChange('enabledSearchbar', ev, setSearchBar)}
/>
<Checkbox
label={t('layout.enabledocker')}
checked={docker}
onChange={(ev) => handleChange('enabledDocker', ev, setDocker)}
/>
<Checkbox
label={t('layout.enableping')}
checked={ping}
onChange={(ev) => handleChange('enabledPing', ev, setPing)}
/>
</Stack>
</Stack>
</Stack>
</>
);
};
const BaseElement = ({ height, width }: { height: number; width: number }) => (
<Paper
sx={(theme) => ({
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.gray[8] : theme.colors.gray[1],
})}
h={height}
p={2}
w={width}
/>
);
const PlaceholderElement = (props: any) => {
const { height, width, hasPing, index } = props;
if (hasPing) {
return (
<Indicator
position="bottom-end"
size={5}
offset={10}
color={index % 4 === 0 ? 'red' : 'green'}
>
<BaseElement width={width} height={height} />
</Indicator>
);
}
return <BaseElement width={width} height={height} />;
};
const useStyles = createStyles((theme) => ({
primaryWrapper: {
flexGrow: 2,