Files
homarr/apps/nextjs/src/components/board/items/item-menu.tsx
T

162 lines
5.8 KiB
TypeScript

import { useEffect, useRef, useState } from "react";
import { ActionIcon, Menu } from "@mantine/core";
import { IconCopy, IconDotsVertical, IconLayoutKanban, IconPencil, IconTrash } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { useSession } from "@homarr/auth/client";
import { useEditMode } from "@homarr/boards/edit-mode";
import { useConfirmModal, useModalAction } from "@homarr/modals";
import { useSettings } from "@homarr/settings";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
import type { WidgetDefinition } from "@homarr/widgets/definition";
import type { SectionItem } from "~/app/[locale]/boards/_types";
import { useSectionContext } from "../sections/section-context";
import { useItemActions } from "./item-actions";
import { LazyItemMoveModal, preloadItemMoveModal } from "./lazy-item-move-modal";
import { LazyWidgetEditModal, preloadWidgetEditModal } from "./lazy-widget-edit-modal";
interface BoardItemMenuProps {
offset: number;
item: SectionItem;
definition: WidgetDefinition;
resetErrorBoundary?: () => void;
}
export const BoardItemMenu = (props: BoardItemMenuProps) => {
const { data: session } = useSession();
const [isEditMode] = useEditMode();
if (!session || !isEditMode) return null;
return <BoardItemMenuInner {...props} />;
};
const BoardItemMenuInner = ({ offset, item, definition, resetErrorBoundary }: BoardItemMenuProps) => {
const refResetErrorBoundaryOnNextRender = useRef(false);
const tItem = useScopedI18n("item");
const t = useI18n();
const { openModal } = useModalAction(LazyWidgetEditModal);
const { openModal: openMoveModal } = useModalAction(LazyItemMoveModal);
const { openConfirmModal } = useConfirmModal();
const [isMenuOpen, setIsMenuOpen] = useState(false);
const hasSupportedIntegrations =
"supportedIntegrations" in definition && (definition.supportedIntegrations?.length ?? 0) > 0;
const { updateItemOptions, updateItemAdvancedOptions, updateItemIntegrations, duplicateItem, removeItem } =
useItemActions();
const { data: integrationData, isPending } = clientApi.integration.all.useQuery(undefined, {
enabled: isMenuOpen && hasSupportedIntegrations,
});
const { gridstack } = useSectionContext().refs;
const settings = useSettings();
// Reset error boundary on next render if item has been edited
useEffect(() => {
if (refResetErrorBoundaryOnNextRender.current) {
resetErrorBoundary?.();
refResetErrorBoundaryOnNextRender.current = false;
}
}, [item, resetErrorBoundary]);
const openEditModal = () => {
openModal(
{
kind: item.kind,
definition,
value: {
advancedOptions: item.advancedOptions,
options: item.options,
integrationIds: item.integrationIds,
},
onSuccessfulEdit: ({ options, integrationIds, advancedOptions }) => {
updateItemOptions({
itemId: item.id,
newOptions: options,
});
updateItemAdvancedOptions({
itemId: item.id,
newAdvancedOptions: advancedOptions,
});
updateItemIntegrations({
itemId: item.id,
newIntegrations: integrationIds,
});
refResetErrorBoundaryOnNextRender.current = true;
},
integrationData: (integrationData ?? []).filter(
(integration) =>
"supportedIntegrations" in definition &&
(definition.supportedIntegrations as string[]).some((kind) => kind === integration.kind),
),
integrationSupport: "supportedIntegrations" in definition,
settings,
itemId: item.id,
appId: item.kind === "app" ? (item.options.appId as string | undefined) : undefined,
},
{
title(t) {
return `${t("item.edit.title")} - ${t(`widget.${item.kind}.name`)}`;
},
},
);
};
const openRemoveModal = () => {
openConfirmModal({
title: tItem("remove.title"),
children: tItem("remove.message"),
onConfirm: () => {
removeItem({ itemId: item.id });
},
});
};
return (
<Menu withinPortal position="right-start" arrowPosition="center" opened={isMenuOpen} onChange={setIsMenuOpen}>
<Menu.Target>
<ActionIcon
variant="default"
radius={"xl"}
pos="absolute"
top={offset}
right={offset}
style={{ zIndex: 10 }}
aria-label={tItem("menu.label.settings")}
>
<IconDotsVertical size={"1rem"} />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown miw={128}>
<Menu.Label>{tItem("menu.label.settings")}</Menu.Label>
<Menu.Item
leftSection={<IconPencil size={16} />}
onClick={openEditModal}
onFocus={preloadWidgetEditModal}
onPointerEnter={preloadWidgetEditModal}
disabled={hasSupportedIntegrations && isPending}
>
{tItem("action.edit")}
</Menu.Item>
<Menu.Item
leftSection={<IconLayoutKanban size={16} />}
onFocus={preloadItemMoveModal}
onPointerEnter={preloadItemMoveModal}
onClick={() => {
if (!gridstack.current) return;
openMoveModal({ item, columnCount: gridstack.current.getColumn(), gridStack: gridstack.current });
}}
>
{tItem("action.moveResize")}
</Menu.Item>
<Menu.Item leftSection={<IconCopy size={16} />} onClick={() => duplicateItem({ itemId: item.id })}>
{tItem("action.duplicate")}
</Menu.Item>
<Menu.Divider />
<Menu.Label c="red.6">{t("common.dangerZone")}</Menu.Label>
<Menu.Item c="red.6" leftSection={<IconTrash size={16} />} onClick={openRemoveModal}>
{tItem("action.remove")}
</Menu.Item>
</Menu.Dropdown>
</Menu>
);
};