Co-Study4Grid / frontend /src /components /ActionCardPopover.tsx
github-actions[bot]
Deploy 7688ef1
13d4e44
Raw
History Blame Contribute Delete
6.99 kB
// 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);