"use client"; import { useCallback, useMemo } from "react"; import { Group, Menu, Switch } from "@mantine/core"; import { IconCopy, IconLayoutKanban, IconSettings, IconTrash } from "@tabler/icons-react"; import { clientApi } from "@homarr/api/client"; import { useSession } from "@homarr/auth/client"; import { useRequiredBoard } from "@homarr/boards/context"; import { useEditMode } from "@homarr/boards/edit-mode"; import { useConfirmModal, useModalAction } from "@homarr/modals"; import { useSettings } from "@homarr/settings"; import { translateIfNecessary } from "@homarr/translation"; import { useI18n, useScopedI18n } from "@homarr/translation/client"; import type { WidgetContextMenuAction } from "@homarr/widgets"; import { reduceWidgetOptionsWithDefaultValues, widgetImports } from "@homarr/widgets"; import { WidgetEditModal } from "@homarr/widgets/modals"; import type { SectionItem } from "~/app/[locale]/boards/_types"; import { useSectionContext } from "../sections/section-context"; import { useItemActions } from "./item-actions"; import { ItemMoveModal } from "./item-move-modal"; interface WidgetContextMenuProps { item: SectionItem; widgetStateRef: React.MutableRefObject | null>; children: React.ReactNode; } export const WidgetContextMenu = ({ item, widgetStateRef, children }: WidgetContextMenuProps) => { const { data: session } = useSession(); const [isEditMode] = useEditMode(); const board = useRequiredBoard(); const tItem = useScopedI18n("item"); const tMenu = useScopedI18n("item.menu.label"); const t = useI18n(); const settings = useSettings(); const { openModal } = useModalAction(WidgetEditModal); const { openModal: openMoveModal } = useModalAction(ItemMoveModal); const { openConfirmModal } = useConfirmModal(); const { updateItemOptions, updateItemAdvancedOptions, updateItemIntegrations, duplicateItem, removeItem } = useItemActions(); const { data: integrationData, isPending } = clientApi.integration.all.useQuery(); const { mutate: saveBoard } = clientApi.board.saveBoard.useMutation(); const currentDefinition = useMemo(() => widgetImports[item.kind].definition, [item.kind]); const { gridstack } = useSectionContext().refs; const options = useMemo( () => reduceWidgetOptionsWithDefaultValues(item.kind, settings, item.options) as Record, [item.kind, settings, item.options], ); type OptionDef = { type: string; skipContextMenu?: boolean }; const toggleOptions = useMemo(() => { const rawOptions = currentDefinition.createOptions(settings) as unknown as Record; return Object.entries(rawOptions).filter(([, def]) => def.type === "switch" && !def.skipContextMenu); }, [currentDefinition, settings]); const widgetContextActions = useMemo(() => { const def = currentDefinition as Record; if (typeof def.contextActions !== "function") return []; // eslint-disable-next-line @typescript-eslint/no-explicit-any const actions = (def.contextActions as any)({ options, setOptions: (partial: Record) => { updateItemOptions({ itemId: item.id, newOptions: { ...options, ...partial } }); }, integrationIds: item.integrationIds, context: { isEditMode, boardId: board.id, itemId: item.id }, widgetStateRef, }); return (Array.isArray(actions) ? actions : []) as WidgetContextMenuAction[]; }, [currentDefinition, options, item, updateItemOptions, isEditMode, board.id, widgetStateRef]); const persistBoard = useCallback( (updatedItems: typeof board.items) => { saveBoard({ ...board, items: updatedItems }); }, [board, saveBoard], ); const openEditModal = useCallback(() => { openModal( { kind: item.kind, value: { advancedOptions: item.advancedOptions, options: item.options, integrationIds: item.integrationIds, }, onSuccessfulEdit: (editResult) => { updateItemOptions({ itemId: item.id, newOptions: editResult.options }); updateItemAdvancedOptions({ itemId: item.id, newAdvancedOptions: editResult.advancedOptions }); updateItemIntegrations({ itemId: item.id, newIntegrations: editResult.integrationIds }); if (!isEditMode) { persistBoard( board.items.map((boardItem) => boardItem.id !== item.id ? boardItem : { ...boardItem, options: editResult.options, advancedOptions: editResult.advancedOptions, integrationIds: editResult.integrationIds }, ), ); } }, integrationData: (integrationData ?? []).filter( (integration) => "supportedIntegrations" in currentDefinition && (currentDefinition.supportedIntegrations as string[]).some((kind) => kind === integration.kind), ), integrationSupport: "supportedIntegrations" in currentDefinition, settings, appId: item.kind === "app" ? (item.options.appId as string | undefined) : undefined, }, // eslint-disable-next-line @typescript-eslint/no-explicit-any { title: (fn: any) => `${fn("item.edit.title")} - ${fn(`widget.${item.kind}.name`)}` }, ); }, [ openModal, item, updateItemOptions, updateItemAdvancedOptions, updateItemIntegrations, integrationData, currentDefinition, settings, isEditMode, persistBoard, board, ]); const handleToggle = useCallback( (key: string) => (checked: boolean) => { const newOptions = { ...options, [key]: checked }; updateItemOptions({ itemId: item.id, newOptions }); persistBoard( board.items.map((boardItem) => boardItem.id !== item.id ? boardItem : { ...boardItem, options: newOptions }, ), ); }, [options, item.id, updateItemOptions, persistBoard, board], ); if (!session) return <>{children}; const visibleWidgetActions = widgetContextActions.filter((a) => !a.hidden); return ( {children} {toggleOptions.length > 0 && ( <> {tMenu("options")} {toggleOptions.map(([key]) => ( handleToggle(key)(!options[key])}> {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} {String(translateIfNecessary(t, ((fn: any) => fn(`widget.${item.kind}.option.${key}.label`)) as any))} ))} )} {visibleWidgetActions.length > 0 && ( <> {toggleOptions.length > 0 && } {tMenu("actions")} {visibleWidgetActions.map((action) => { const Icon = action.icon; return ( : undefined} onClick={action.onClick} disabled={action.disabled} color={action.color} > {String(translateIfNecessary(t, action.label))} ); })} )} {isEditMode && ( <> {(toggleOptions.length > 0 || visibleWidgetActions.length > 0) && } } onClick={() => { if (!gridstack.current) return; openMoveModal({ item, columnCount: gridstack.current.getColumn(), gridStack: gridstack.current }); }} > {tItem("action.moveResize")} } onClick={() => duplicateItem({ itemId: item.id })} > {tItem("action.duplicate")} )} <> {!isEditMode && (toggleOptions.length > 0 || visibleWidgetActions.length > 0) && } } onClick={openEditModal} disabled={isPending} > {tMenu("settings")} {isEditMode && ( <> } onClick={() => { openConfirmModal({ title: tItem("remove.title"), children: tItem("remove.message"), onConfirm: () => removeItem({ itemId: item.id }), }); }} > {tItem("action.remove")} )} ); };