Spaces:
Sleeping
Sleeping
| // Copyright (c) 2025-2026, RTE (https://www.rte-france.com) | |
| // This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. | |
| // If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, | |
| // you can obtain one at http://mozilla.org/MPL/2.0/. | |
| // SPDX-License-Identifier: MPL-2.0 | |
| // This file is part of Co-Study4Grid a Power Grid Study tool Assistant Interface to help solve contigencies for a grid state under study. | |
| import React, { useCallback, type CSSProperties, type Ref } from 'react'; | |
| import type { ActionDetail, MetadataIndex } from '../types'; | |
| import ActionCard from './ActionCard'; | |
| import { colors } from '../styles/tokens'; | |
| /** | |
| * Floating ActionCard popover — shared between the action | |
| * overview view (single-click on a pin) and any future callsite | |
| * that needs a read-only-ish "peek" at an action's details | |
| * without opening the full drill-down diagram. | |
| * | |
| * The popover intentionally reuses the SAME `ActionCard` | |
| * component that the sidebar feed renders, so card visuals, | |
| * severity palette, badge resolution and tooltip text all stay | |
| * in lock-step with the feed — we only need to change the | |
| * component in ONE place. Re-simulate / edit controls are wired | |
| * to no-op stubs because the popover is a preview, not an | |
| * editor; callers that need those interactions should open the | |
| * drill-down view (which is what a double-click on the pin | |
| * already does). | |
| * | |
| * The chrome (floating card frame + close button) is owned by | |
| * this component so every callsite gets identical affordances. | |
| */ | |
| interface ActionCardPopoverProps { | |
| actionId: string; | |
| details: ActionDetail; | |
| /** Visual index shown in the card header (e.g. "#3"). */ | |
| index: number; | |
| /** | |
| * Absolute-positioning style (including `position: fixed`, | |
| * top/bottom/left/right anchors). Typically produced by | |
| * `utils/popoverPlacement.computePopoverStyle` so the | |
| * popover lands next to the anchor element. | |
| */ | |
| style: CSSProperties; | |
| /** Overloaded lines used for the "Loading after" rendering. */ | |
| linesOverloaded: readonly string[]; | |
| /** Monitoring factor — drives the severity badge. */ | |
| monitoringFactor: number; | |
| /** N-1 metadata index used by badge resolution. */ | |
| metaIndex: MetadataIndex | null; | |
| /** Selected-action set (for the card's `isSelected` styling). */ | |
| selectedActionIds?: Set<string>; | |
| /** Rejected-action set (for the card's `isRejected` styling). */ | |
| rejectedActionIds?: Set<string>; | |
| /** | |
| * Called when the user activates the card (clicks the card | |
| * body or the dedicated "view" affordance). Parent typically | |
| * closes the popover and switches to the action drill-down | |
| * view. | |
| */ | |
| onActivateAction: (actionId: string) => void; | |
| /** Toggle favourite status. */ | |
| onActionFavorite?: (actionId: string) => void; | |
| /** Reject the action. */ | |
| onActionReject?: (actionId: string) => void; | |
| /** Close the popover. */ | |
| onClose: () => void; | |
| /** | |
| * Optional ref on the popover root so the parent can detect | |
| * outside clicks. | |
| */ | |
| popoverRef?: Ref<HTMLDivElement>; | |
| /** | |
| * Optional test id on the popover root (defaults to | |
| * "action-card-popover"). Kept overridable so multiple | |
| * callsites can disambiguate their popovers in tests. | |
| */ | |
| testId?: string; | |
| /** | |
| * Optional data-attributes to forward onto the popover root — | |
| * used by `ActionOverviewDiagram` to record the placement | |
| * decision on the DOM for its unit tests. | |
| */ | |
| extraDataAttributes?: Record<string, string>; | |
| /** Resolve an element/VL ID to its human-readable display name. */ | |
| displayName?: (id: string) => string; | |
| } | |
| const NOOP = () => { /* intentional no-op */ }; | |
| const ActionCardPopover: React.FC<ActionCardPopoverProps> = ({ | |
| actionId, | |
| details, | |
| index, | |
| style, | |
| linesOverloaded, | |
| monitoringFactor, | |
| metaIndex, | |
| selectedActionIds, | |
| rejectedActionIds, | |
| onActivateAction, | |
| onActionFavorite, | |
| onActionReject, | |
| onClose, | |
| popoverRef, | |
| testId = 'action-card-popover', | |
| extraDataAttributes, | |
| displayName, | |
| }) => { | |
| // Wrap the "activate the action" intent so both the card | |
| // body click and any future explicit affordance flow through | |
| // the same path — and the popover always auto-dismisses. | |
| const handleActivate = useCallback((id: string | null) => { | |
| onClose(); | |
| if (id) onActivateAction(id); | |
| }, [onActivateAction, onClose]); | |
| return ( | |
| <div | |
| ref={popoverRef} | |
| data-testid={testId} | |
| data-action-id={actionId} | |
| {...(extraDataAttributes ?? {})} | |
| style={{ | |
| ...style, | |
| background: colors.surface, | |
| border: `1px solid ${colors.border}`, | |
| borderRadius: 8, | |
| boxShadow: '0 10px 24px rgba(0, 0, 0, 0.25)', | |
| zIndex: 300, | |
| }} | |
| onMouseDown={e => e.stopPropagation()} | |
| > | |
| <button | |
| data-testid={`${testId}-close`} | |
| onClick={onClose} | |
| title="Close" | |
| style={{ | |
| position: 'absolute', | |
| top: 6, | |
| right: 8, | |
| zIndex: 2, | |
| background: colors.surface, | |
| border: `1px solid ${colors.border}`, | |
| borderRadius: '50%', | |
| width: 22, | |
| height: 22, | |
| cursor: 'pointer', | |
| fontSize: 12, | |
| color: colors.textSecondary, | |
| boxShadow: '0 1px 3px rgba(0,0,0,0.15)', | |
| lineHeight: 1, | |
| }} | |
| > | |
| {'\u2715'} | |
| </button> | |
| <ActionCard | |
| id={actionId} | |
| details={details} | |
| index={index} | |
| isViewing={false} | |
| isSelected={selectedActionIds?.has(actionId) ?? false} | |
| isRejected={rejectedActionIds?.has(actionId) ?? false} | |
| linesOverloaded={Array.from(linesOverloaded)} | |
| monitoringFactor={monitoringFactor} | |
| nodesByEquipmentId={metaIndex?.nodesByEquipmentId ?? null} | |
| edgesByEquipmentId={metaIndex?.edgesByEquipmentId ?? null} | |
| cardEditMw={{}} | |
| cardEditTap={{}} | |
| resimulating={null} | |
| onActionSelect={handleActivate} | |
| onActionFavorite={onActionFavorite ?? NOOP} | |
| onActionReject={onActionReject ?? NOOP} | |
| onAssetClick={NOOP} | |
| onCardEditMwChange={NOOP} | |
| onCardEditTapChange={NOOP} | |
| onResimulate={NOOP} | |
| onResimulateTap={NOOP} | |
| displayName={displayName} | |
| /> | |
| </div> | |
| ); | |
| }; | |
| export default React.memo(ActionCardPopover); | |