Add gridstack dashboard layout

This commit is contained in:
Meierschlumpf
2022-12-10 22:14:31 +01:00
parent b7bb1302e4
commit 001890d763
39 changed files with 2822 additions and 918 deletions

View File

@@ -0,0 +1,66 @@
import { Card, Group, Title } from '@mantine/core';
import { CategoryType } from '../../../../types/category';
import { HomarrCardWrapper } from '../../Tiles/HomarrCardWrapper';
import { Tiles } from '../../Tiles/tilesDefinitions';
import { GridstackTileWrapper } from '../../Tiles/TileWrapper';
import { useEditModeStore } from '../../Views/useEditModeStore';
import { useGridstack } from '../gridstack/use-gridstack';
import { CategoryEditMenu } from './CategoryEditMenu';
interface DashboardCategoryProps {
category: CategoryType;
}
export const DashboardCategory = ({ category }: DashboardCategoryProps) => {
const { refs, items, integrations } = useGridstack('category', category.id);
const isEditMode = useEditModeStore((x) => x.enabled);
return (
<HomarrCardWrapper pt={10} mx={10}>
<Group position="apart" align="center">
<Title order={3}>{category.name}</Title>
{isEditMode ? <CategoryEditMenu category={category} /> : null}
</Group>
<div
className="grid-stack grid-stack-category"
style={{ transitionDuration: '0s' }}
data-category={category.id}
ref={refs.wrapper}
>
{items?.map((service) => {
const { component: TileComponent, ...tile } = Tiles['service'];
return (
<GridstackTileWrapper
id={service.id}
type="service"
key={service.id}
itemRef={refs.items.current[service.id]}
{...tile}
{...service.shape.location}
{...service.shape.size}
>
<TileComponent className="grid-stack-item-content" service={service} />
</GridstackTileWrapper>
);
})}
{Object.entries(integrations).map(([k, v]) => {
const { component: TileComponent, ...tile } = Tiles[k as keyof typeof Tiles];
return (
<GridstackTileWrapper
id={k}
type="module"
key={k}
itemRef={refs.items.current[k]}
{...tile}
{...v.shape.location}
{...v.shape.size}
>
<TileComponent className="grid-stack-item-content" module={v} />
</GridstackTileWrapper>
);
})}
</div>
</HomarrCardWrapper>
);
};

View File

@@ -0,0 +1,51 @@
import { ActionIcon, Menu } from '@mantine/core';
import {
IconDots,
IconTransitionTop,
IconTransitionBottom,
IconRowInsertTop,
IconRowInsertBottom,
IconEdit,
} from '@tabler/icons';
import { useConfigContext } from '../../../../config/provider';
import { CategoryType } from '../../../../types/category';
import { useCategoryActions } from './useCategoryActions';
interface CategoryEditMenuProps {
category: CategoryType;
}
export const CategoryEditMenu = ({ category }: CategoryEditMenuProps) => {
const { name: configName } = useConfigContext();
const { addCategoryAbove, addCategoryBelow, moveCategoryUp, moveCategoryDown, edit } =
useCategoryActions(configName, category);
return (
<Menu withinPortal>
<Menu.Target>
<ActionIcon>
<IconDots />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item icon={<IconEdit size={20} />} onClick={edit}>
Edit
</Menu.Item>
<Menu.Label>Change positon</Menu.Label>
<Menu.Item icon={<IconTransitionTop size={20} />} onClick={moveCategoryUp}>
Move up
</Menu.Item>
<Menu.Item icon={<IconTransitionBottom size={20} />} onClick={moveCategoryDown}>
Move down
</Menu.Item>
<Menu.Label>Add category</Menu.Label>
<Menu.Item icon={<IconRowInsertTop size={20} />} onClick={addCategoryAbove}>
Add category above
</Menu.Item>
<Menu.Item icon={<IconRowInsertBottom size={20} />} onClick={addCategoryBelow}>
Add category below
</Menu.Item>
</Menu.Dropdown>
</Menu>
);
};

View File

@@ -0,0 +1,50 @@
import { Button, Group, TextInput } from '@mantine/core';
import { useForm } from '@mantine/form';
import { ContextModalProps } from '@mantine/modals';
import { useConfigContext } from '../../../../config/provider';
import { useConfigStore } from '../../../../config/store';
import { CategoryType } from '../../../../types/category';
export interface CategoryEditModalInnerProps {
category: CategoryType;
onSuccess: (category: CategoryType) => Promise<void>;
}
export const CategoryEditModal = ({
context,
innerProps,
id,
}: ContextModalProps<CategoryEditModalInnerProps>) => {
const { name: configName } = useConfigContext();
const updateConfig = useConfigStore((x) => x.updateConfig);
const form = useForm<FormType>({
initialValues: {
name: innerProps.category.name,
},
validate: {
name: (val: string) => (!val || val.trim().length === 0 ? 'Name is required' : null),
},
});
const handleSubmit = async (values: FormType) => {
innerProps.onSuccess({ ...innerProps.category, name: values.name });
context.closeModal(id);
};
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<TextInput data-autoFocus {...form.getInputProps('name')} label="Name of category" />
<Group mt="md" grow>
<Button onClick={() => context.closeModal(id)} variant="light" color="gray">
Cancel
</Button>
<Button type="submit">Save</Button>
</Group>
</form>
);
};
type FormType = {
name: string;
};

View File

@@ -0,0 +1,186 @@
import { v4 as uuidv4 } from 'uuid';
import { useConfigStore } from '../../../../config/store';
import { openContextModalGeneric } from '../../../../tools/mantineModalManagerExtensions';
import { CategoryType } from '../../../../types/category';
import { WrapperType } from '../../../../types/wrapper';
import { CategoryEditModalInnerProps } from './CategoryEditModal';
export const useCategoryActions = (configName: string | undefined, category: CategoryType) => {
const updateConfig = useConfigStore((x) => x.updateConfig);
// creates a new category above the current
const addCategoryAbove = () => {
const abovePosition = category.position - 1;
openContextModalGeneric<CategoryEditModalInnerProps>({
modal: 'categoryEditModal',
innerProps: {
category: {
id: uuidv4(),
name: 'New category',
position: abovePosition + 1,
},
onSuccess: async (category) => {
if (!configName) return;
const newWrapper: WrapperType = {
id: uuidv4(),
position: abovePosition + 2,
};
// Adding category and wrapper and moving other items down
updateConfig(configName, (previous) => {
const aboveWrappers = previous.wrappers.filter((x) => x.position <= abovePosition);
const aboveCategories = previous.categories.filter((x) => x.position <= abovePosition);
const belowWrappers = previous.wrappers.filter((x) => x.position > abovePosition);
const belowCategories = previous.categories.filter((x) => x.position > abovePosition);
return {
...previous,
categories: [
...aboveCategories,
category,
// Move categories below down
...belowCategories.map((x) => ({ ...x, position: x.position + 2 })),
],
wrappers: [
...aboveWrappers,
newWrapper,
// Move wrappers below down
...belowWrappers.map((x) => ({ ...x, position: x.position + 2 })),
],
};
});
},
},
});
};
// creates a new category below the current
const addCategoryBelow = () => {
const belowPosition = category.position + 1;
openContextModalGeneric<CategoryEditModalInnerProps>({
modal: 'categoryEditModal',
innerProps: {
category: {
id: uuidv4(),
name: 'New category',
position: belowPosition + 1,
},
onSuccess: async (category) => {
if (!configName) return;
const newWrapper: WrapperType = {
id: uuidv4(),
position: belowPosition,
};
// Adding category and wrapper and moving other items down
updateConfig(configName, (previous) => {
const aboveWrappers = previous.wrappers.filter((x) => x.position < belowPosition);
const aboveCategories = previous.categories.filter((x) => x.position < belowPosition);
const belowWrappers = previous.wrappers.filter((x) => x.position >= belowPosition);
const belowCategories = previous.categories.filter((x) => x.position >= belowPosition);
return {
...previous,
categories: [
...aboveCategories,
category,
// Move categories below down
...belowCategories.map((x) => ({ ...x, position: x.position + 2 })),
],
wrappers: [
...aboveWrappers,
newWrapper,
// Move wrappers below down
...belowWrappers.map((x) => ({ ...x, position: x.position + 2 })),
],
};
});
},
},
});
};
const moveCategoryUp = () => {
if (!configName) return;
updateConfig(configName, (previous) => {
const currentItem = previous.categories.find((x) => x.id === category.id);
if (!currentItem) return previous;
const upperItem = previous.categories.find((x) => x.position === currentItem.position - 2);
if (!upperItem) return previous;
currentItem.position -= 2;
upperItem.position += 2;
return {
...previous,
categories: [
...previous.categories.filter((c) => ![currentItem.id, upperItem.id].includes(c.id)),
{ ...upperItem },
{ ...currentItem },
],
};
});
};
const moveCategoryDown = () => {
if (!configName) return;
updateConfig(configName, (previous) => {
const currentItem = previous.categories.find((x) => x.id === category.id);
if (!currentItem) return previous;
const belowItem = previous.categories.find((x) => x.position === currentItem.position + 2);
if (!belowItem) return previous;
currentItem.position += 2;
belowItem.position -= 2;
return {
...previous,
categories: [
...previous.categories.filter((c) => ![currentItem.id, belowItem.id].includes(c.id)),
{ ...currentItem },
{ ...belowItem },
],
};
});
};
const edit = async () => {
openContextModalGeneric<CategoryEditModalInnerProps>({
modal: 'categoryEditModal',
innerProps: {
category,
onSuccess: async (category) => {
if (!configName) return;
await updateConfig(configName, (prev) => {
const currentCategory = prev.categories.find((c) => c.id === category.id);
if (!currentCategory) return prev;
return {
...prev,
categories: [...prev.categories.filter((c) => c.id !== category.id), { ...category }],
};
});
},
},
});
};
return {
addCategoryAbove,
addCategoryBelow,
moveCategoryUp,
moveCategoryDown,
edit,
};
};

View File

@@ -0,0 +1,56 @@
import { WrapperType } from '../../../../types/wrapper';
import { Tiles } from '../../Tiles/tilesDefinitions';
import { GridstackTileWrapper } from '../../Tiles/TileWrapper';
import { useGridstack } from '../gridstack/use-gridstack';
interface DashboardWrapperProps {
wrapper: WrapperType;
}
export const DashboardWrapper = ({ wrapper }: DashboardWrapperProps) => {
const { refs, items, integrations } = useGridstack('wrapper', wrapper.id);
return (
<div
className="grid-stack grid-stack-wrapper"
style={{ transitionDuration: '0s' }}
data-wrapper={wrapper.id}
ref={refs.wrapper}
>
{items?.map((service) => {
const { component: TileComponent, ...tile } = Tiles['service'];
return (
<GridstackTileWrapper
id={service.id}
type="service"
key={service.id}
itemRef={refs.items.current[service.id]}
{...tile}
{...service.shape.location}
{...service.shape.size}
>
<TileComponent className="grid-stack-item-content" service={service} />
</GridstackTileWrapper>
);
})}
{Object.entries(integrations).map(([k, v]) => {
console.log(k);
const { component: TileComponent, ...tile } = Tiles[k as keyof typeof Tiles];
return (
<GridstackTileWrapper
id={k}
type="module"
key={k}
itemRef={refs.items.current[k]}
{...tile}
{...v.shape.location}
{...v.shape.size}
>
<TileComponent className="grid-stack-item-content" module={v} />
</GridstackTileWrapper>
);
})}
</div>
);
};