Spaces:
Paused
Paused
File size: 6,992 Bytes
0643cf5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | // 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);
|