import { useCallback, useEffect, useMemo, type ComponentType } from 'react' import { IconButton, type SvgIconProps } from '@mui/material' import { Lock as LockIcon } from '@mui/icons-material' import { LockOpen as LockOpenIcon } from '@mui/icons-material' import { Tooltip } from '../../../components' import { useTransform, useWidgetId } from '../../stores' import type { TransformPair } from '../../stores' import { filterByLockedItems } from './transforms' import { DEFAULT_LOCK_SELECTION_LABELS, type LockSelectionLabels, } from './labels' import { styles } from './style' const LOCK_SELECTION_DESCRIPTOR = { id: 'lock-selection', type: 'data' as const, order: 30, } export type LockSelectionKey = string | number export interface LockSelectionProps { /** * Currently-selected items (destination-owned). On lock, this set is * snapshotted into `lockedItems` via `onLockChange`. */ selection: readonly LockSelectionKey[] /** * Currently-locked items (destination-owned). When non-empty, the widget's * data is filtered to these items via the registered transform. */ lockedItems: readonly LockSelectionKey[] /** * Fires when the user toggles the lock. The destination is responsible for * persisting the new locked set (and re-feeding it via `lockedItems`). */ onLockChange: (next: readonly LockSelectionKey[]) => void labels?: Partial /** Lock icon (default: `LockIcon`). */ lockIcon?: ComponentType /** Unlock icon (default: `LockOpenIcon`). */ unlockIcon?: ComponentType iconProps?: SvgIconProps } /** * Action that pins the current selection so the widget's data filters down * to those items even as the user changes selections. Selection and lock * state are destination-owned; this component is a controlled UI + a data * transform registered against the widget's pipeline. * * The trigger is disabled when there is nothing to lock and nothing locked. */ export function LockSelection({ selection, lockedItems, onLockChange, labels, lockIcon: LockSvg = LockIcon, unlockIcon: UnlockSvg = LockOpenIcon, iconProps, }: LockSelectionProps) { const id = useWidgetId() const _labels = { ...DEFAULT_LOCK_SELECTION_LABELS, ...labels } const isLocked = lockedItems.length > 0 const canToggle = isLocked || selection.length > 0 // Pair memoized on lockedItems identity — useTransform re-registers the fn // (which closes over the current locked set) whenever the destination feeds // a new array. setEnabled drives the middleware-level `enabled` flag. const pairs = useMemoPair(lockedItems) const { enabled, setEnabled } = useTransform(id, pairs, { initialEnabled: isLocked, }) // Keep middleware `enabled` in sync with destination-owned `isLocked`. The // pipeline only short-circuits the transform when enabled === false, so // toggling here is what makes lock/unlock actually take effect. useEffect(() => { if (enabled !== isLocked) setEnabled(isLocked) }, [enabled, isLocked, setEnabled]) const handleToggle = useCallback(() => { if (isLocked) { onLockChange([]) } else { onLockChange([...selection]) } }, [isLocked, onLockChange, selection]) const tooltip = !canToggle ? _labels.disabled : isLocked ? _labels.unlock : _labels.lock const Icon = isLocked ? UnlockSvg : LockSvg return ( ) } /** * Builds a stable `[{ descriptor, fn }]` pair where `fn` closes over the * latest `lockedItems`. Re-memoized whenever the array identity changes so * `useTransform` re-registers the closure with the freshest data. */ function useMemoPair( lockedItems: readonly LockSelectionKey[], ): readonly TransformPair[] { const fn = useCallback( (data: unknown) => filterByLockedItems(data, lockedItems), [lockedItems], ) return useMemo(() => [{ descriptor: LOCK_SELECTION_DESCRIPTOR, fn }], [fn]) }