155 lines
5.6 KiB
TypeScript
155 lines
5.6 KiB
TypeScript
import { useEffect, useRef, useState } from "react";
|
|
import { ActionIcon, Menu } from "@mantine/core";
|
|
import { IconCopy, IconDotsVertical, IconLayoutKanban, IconPencil } from "@tabler/icons-react";
|
|
|
|
import { useSession } from "@homarr/auth/client";
|
|
import { useEditMode } from "@homarr/boards/edit-mode";
|
|
import { 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 { BoardRemoveConfirmationMenuItem } from "../remove-confirmation-menu-item";
|
|
import { useSectionContext } from "../sections/section-context";
|
|
import { useItemActions } from "./item-actions";
|
|
import { useOpenItemMoveModal } from "./item-move-modal";
|
|
import itemContentClasses from "./item-content.module.css";
|
|
import { LazyWidgetEditModal, preloadWidgetEditModal } from "./lazy-widget-edit-modal";
|
|
|
|
interface BoardItemMenuProps {
|
|
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 = ({ item, definition, resetErrorBoundary }: BoardItemMenuProps) => {
|
|
const { data: session } = useSession();
|
|
const canDuplicate = item.kind !== "customApi" || (session?.user.permissions.includes("admin") ?? false);
|
|
const refResetErrorBoundaryOnNextRender = useRef(false);
|
|
const tItem = useScopedI18n("item");
|
|
const t = useI18n();
|
|
const { openModal } = useModalAction(LazyWidgetEditModal);
|
|
const openMoveModal = useOpenItemMoveModal();
|
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
|
const { updateItemOptions, updateItemAdvancedOptions, updateItemIntegrations, duplicateItem, removeItem } =
|
|
useItemActions();
|
|
const { integrations: integrationData, section } = useSectionContext();
|
|
const settings = useSettings();
|
|
const label = item.advancedOptions.title?.trim() || t(`widget.${item.kind}.name`);
|
|
|
|
// 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;
|
|
},
|
|
onIntegrationSaved: resetErrorBoundary,
|
|
integrationData: (integrationData ?? []).filter(
|
|
(integration) =>
|
|
integration.permissions.hasUseAccess &&
|
|
"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(translate) {
|
|
return `${translate("item.edit.title")} - ${translate(`widget.${item.kind}.name`)}`;
|
|
},
|
|
},
|
|
);
|
|
};
|
|
|
|
return (
|
|
<Menu withinPortal position="right-start" arrowPosition="center" opened={isMenuOpen} onChange={setIsMenuOpen}>
|
|
<Menu.Target>
|
|
<ActionIcon
|
|
variant="default"
|
|
size={24}
|
|
radius="sm"
|
|
pos="absolute"
|
|
top={4}
|
|
right={4}
|
|
style={{ zIndex: 30 }}
|
|
className={itemContentClasses.settingsButton}
|
|
data-menu-open={isMenuOpen || undefined}
|
|
data-board-widget-settings
|
|
aria-label={tItem("menu.label.settingsFor", { name: label })}
|
|
>
|
|
<IconDotsVertical size="1rem" />
|
|
</ActionIcon>
|
|
</Menu.Target>
|
|
<Menu.Dropdown miw={128}>
|
|
<Menu.Item
|
|
leftSection={<IconPencil size={16} />}
|
|
onClick={openEditModal}
|
|
onFocus={preloadWidgetEditModal}
|
|
onPointerEnter={preloadWidgetEditModal}
|
|
>
|
|
{tItem("action.edit")}
|
|
</Menu.Item>
|
|
<Menu.Item
|
|
leftSection={<IconLayoutKanban size={16} />}
|
|
onClick={() => {
|
|
openMoveModal({
|
|
entry: item,
|
|
sourceSectionId: section.id,
|
|
});
|
|
}}
|
|
>
|
|
{tItem("action.moveResize")}
|
|
</Menu.Item>
|
|
{canDuplicate && (
|
|
<Menu.Item leftSection={<IconCopy size={16} />} onClick={() => duplicateItem({ itemId: item.id })}>
|
|
{tItem("action.duplicate")}
|
|
</Menu.Item>
|
|
)}
|
|
<Menu.Divider />
|
|
<BoardRemoveConfirmationMenuItem
|
|
label={tItem("action.remove")}
|
|
confirmationLabel={tItem("remove.message")}
|
|
onConfirm={() => removeItem({ itemId: item.id })}
|
|
/>
|
|
</Menu.Dropdown>
|
|
</Menu>
|
|
);
|
|
};
|