feat: add dynamic section (#842)
* chore: add parent_section_id and change position to x and y_offset for sqlite section table * chore: rename existing positions to x_offset and y_offset * chore: add related mysql migration * chore: add missing height and width to section table * fix: missing width and height in migration copy script * fix: typecheck issues * fix: test not working caused by unsimilar schemas * wip: add dynamic section * refactor: improve structure of gridstack sections * feat: add rendering of dynamic sections * feat: add saving of moved sections * wip: add static row count, restrict min-width and height * chore: address pull request feedback * fix: format issues * fix: size calculation within dynamic sections * fix: on resize not called when min width or height is reached * fix: size of items while dragging is to big * chore: temporarly remove migration files * chore: readd migrations * fix: format and deepsource issues * chore: remove db_dev.sqlite file * chore: add *.sqlite to .gitignore * chore: address pull request feedback * feat: add dynamic section actions for adding and removing them
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
import { useCallback } from "react";
|
||||
|
||||
import { createId } from "@homarr/db/client";
|
||||
|
||||
import type { DynamicSection, EmptySection } from "~/app/[locale]/boards/_types";
|
||||
import { useUpdateBoard } from "~/app/[locale]/boards/(content)/_client";
|
||||
|
||||
interface RemoveDynamicSection {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export const useDynamicSectionActions = () => {
|
||||
const { updateBoard } = useUpdateBoard();
|
||||
|
||||
const addDynamicSection = useCallback(() => {
|
||||
updateBoard((previous) => {
|
||||
const lastSection = previous.sections
|
||||
.filter((section): section is EmptySection => section.kind === "empty")
|
||||
.sort((sectionA, sectionB) => sectionB.yOffset - sectionA.yOffset)[0];
|
||||
|
||||
if (!lastSection) return previous;
|
||||
|
||||
const newSection = {
|
||||
id: createId(),
|
||||
kind: "dynamic",
|
||||
height: 1,
|
||||
width: 1,
|
||||
items: [],
|
||||
parentSectionId: lastSection.id,
|
||||
// We omit xOffset and yOffset because gridstack will use the first available position
|
||||
} satisfies Omit<DynamicSection, "xOffset" | "yOffset">;
|
||||
|
||||
return {
|
||||
...previous,
|
||||
sections: previous.sections.concat(newSection as unknown as DynamicSection),
|
||||
};
|
||||
});
|
||||
}, [updateBoard]);
|
||||
|
||||
const removeDynamicSection = useCallback(
|
||||
({ id }: RemoveDynamicSection) => {
|
||||
updateBoard((previous) => {
|
||||
const sectionToRemove = previous.sections.find(
|
||||
(section): section is DynamicSection => section.id === id && section.kind === "dynamic",
|
||||
);
|
||||
if (!sectionToRemove) return previous;
|
||||
|
||||
return {
|
||||
...previous,
|
||||
sections: previous.sections
|
||||
.filter((section) => section.id !== id)
|
||||
.map((section) => {
|
||||
if (section.id === sectionToRemove.parentSectionId) {
|
||||
return {
|
||||
...section,
|
||||
// Add items from the removed section to the parent section
|
||||
items: section.items.concat(
|
||||
sectionToRemove.items.map((item) => ({
|
||||
...item,
|
||||
xOffset: sectionToRemove.xOffset + item.xOffset,
|
||||
yOffset: sectionToRemove.yOffset + item.yOffset,
|
||||
})),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (section.kind === "dynamic" && section.parentSectionId === sectionToRemove.id) {
|
||||
// Change xOffset and yOffset of sections that were below the removed section and set parentSectionId to the parent of the removed section
|
||||
return {
|
||||
...section,
|
||||
parentSectionId: sectionToRemove.parentSectionId,
|
||||
yOffset: section.yOffset + sectionToRemove.yOffset,
|
||||
xOffset: section.xOffset + sectionToRemove.xOffset,
|
||||
};
|
||||
}
|
||||
|
||||
return section;
|
||||
}),
|
||||
};
|
||||
});
|
||||
},
|
||||
[updateBoard],
|
||||
);
|
||||
|
||||
return {
|
||||
addDynamicSection,
|
||||
removeDynamicSection,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import { ActionIcon, Menu } from "@mantine/core";
|
||||
import { IconDotsVertical, IconTrash } from "@tabler/icons-react";
|
||||
|
||||
import { useConfirmModal } from "@homarr/modals";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { DynamicSection } from "~/app/[locale]/boards/_types";
|
||||
import { useEditMode } from "~/app/[locale]/boards/(content)/_context";
|
||||
import { useDynamicSectionActions } from "./dynamic-actions";
|
||||
|
||||
export const BoardDynamicSectionMenu = ({ section }: { section: DynamicSection }) => {
|
||||
const t = useI18n();
|
||||
const tDynamic = useScopedI18n("section.dynamic");
|
||||
const { removeDynamicSection } = useDynamicSectionActions();
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
const [isEditMode] = useEditMode();
|
||||
|
||||
if (!isEditMode) return null;
|
||||
|
||||
const openRemoveModal = () => {
|
||||
openConfirmModal({
|
||||
title: tDynamic("remove.title"),
|
||||
children: tDynamic("remove.message"),
|
||||
onConfirm: () => {
|
||||
removeDynamicSection({ id: section.id });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu withinPortal withArrow position="right-start" arrowPosition="center">
|
||||
<Menu.Target>
|
||||
<ActionIcon variant="default" radius={"xl"} pos="absolute" top={4} right={4} style={{ zIndex: 10 }}>
|
||||
<IconDotsVertical size={"1rem"} />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown miw={128}>
|
||||
<Menu.Label c="red.6">{t("common.dangerZone")}</Menu.Label>
|
||||
<Menu.Item c="red.6" leftSection={<IconTrash size={16} />} onClick={openRemoveModal}>
|
||||
{tDynamic("action.remove")}
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user