✨ Add gridstack dashboard layout
This commit is contained in:
66
src/components/Dashboard/Wrappers/Category/Category.tsx
Normal file
66
src/components/Dashboard/Wrappers/Category/Category.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
56
src/components/Dashboard/Wrappers/Wrapper/Wrapper.tsx
Normal file
56
src/components/Dashboard/Wrappers/Wrapper/Wrapper.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user