Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import React, { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Box, Table, TableBody, TableCell, TableHead, TableRow, useTheme } from '@mui/material';
import {
ColumnDef,
Expand All @@ -21,7 +21,7 @@ import { useVirtualizer } from '@tanstack/react-virtual';
import { UUID } from 'node:crypto';
import { NetworkModificationEditorNameHeaderProps } from './renderers';
import {
ExcludedNetworkModifications,
NetworkModificationApplicabilities,
RootNetworkRowInfo,
ComposedModificationMetadata,
NetworkModificationMetadata,
Expand All @@ -35,6 +35,7 @@ import { AUTO_EXTENSIBLE_COLUMNS } from './columns-definition';
import { useModificationsDragAndDrop } from './use-modifications-drag-and-drop';
import { useModificationsSelection } from './use-modifications-selection';
import {
collectApplicabilities,
fetchSubModificationsForExpandedRows,
findAllLoadedCompositeModifications,
findDepth,
Expand Down Expand Up @@ -62,8 +63,6 @@ interface NetworkModificationsTableProps extends Omit<NetworkModificationEditorN
currentNodeId?: UUID;
currentRootNetworkUuid?: UUID;
rootNetworks?: RootNetworkRowInfo[];
modificationsToExclude?: ExcludedNetworkModifications[];
setModificationsToExclude?: Dispatch<SetStateAction<ExcludedNetworkModifications[]>>;
isDisabled?: boolean;
}

Expand All @@ -82,8 +81,6 @@ export function NetworkModificationsTable({
currentNodeId = undefined,
currentRootNetworkUuid,
rootNetworks,
modificationsToExclude,
setModificationsToExclude,
isDisabled = false,
isImpactedByNotification,
notificationMessageId,
Expand All @@ -99,6 +96,20 @@ export function NetworkModificationsTable({
const [composedModifications, setComposedModifications] = useState<ComposedModificationMetadata[]>(
formatToComposedModification(modifications)
);

// The tags are read from a ref on purpose: renaming a root network must not retrigger the collect, or the
// modifications in hand, still carrying the previous tag, would resolve to no root network at all and read
// back as applicable. The next fetch of the modifications brings both sides in step again.
const rootNetworksRef = useRef(rootNetworks);
useEffect(() => {
rootNetworksRef.current = rootNetworks;
}, [rootNetworks]);

const [applicabilities, setApplicabilities] = useState<NetworkModificationApplicabilities>({});
useEffect(() => {
setApplicabilities(collectApplicabilities(composedModifications, rootNetworksRef.current));
}, [composedModifications]);

// composedModificationsRef is used to access composedModifications data from other useEffects
// without having to add composedModifications to their dependencies (so it doesn't trigger them)
const composedModificationsRef = useRef(composedModifications);
Expand Down Expand Up @@ -200,8 +211,8 @@ export function NetworkModificationsTable({
},
modifications: {
count: modifications.length,
toExclude: modificationsToExclude,
setToExclude: setModificationsToExclude,
applicabilities,
setApplicabilities,
},
interaction: {
lastClickedRowId,
Expand All @@ -223,8 +234,8 @@ export function NetworkModificationsTable({
currentRootNetworkUuid,
rootNetworks,
modifications.length,
modificationsToExclude,
setModificationsToExclude,
applicabilities,
setApplicabilities,
lastClickedRowId,
handleRowSelected,
modificationToEditLabelRef,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export function RootNetworkCellRenderer({ row, column, table }: CCtx) {
const { meta } = table.options;
// `column.id` is the rootNetworkUuid (set in createRootNetworksColumns).
const rootNetwork = meta?.context.rootNetworks?.find((r) => r.rootNetworkUuid === column.id);
if (!rootNetwork || !meta?.modifications.toExclude || !meta.modifications.setToExclude) {
if (!rootNetwork || !meta?.modifications.applicabilities || !meta.modifications.setApplicabilities) {
return null;
}
return (
Expand All @@ -132,8 +132,8 @@ export function RootNetworkCellRenderer({ row, column, table }: CCtx) {
studyUuid={meta?.context.studyUuid ?? null}
currentNodeId={meta?.context.currentNodeId}
rootNetwork={rootNetwork}
modificationsToExclude={meta.modifications.toExclude}
setModificationsToExclude={meta.modifications.setToExclude}
applicabilities={meta.modifications.applicabilities}
setApplicabilities={meta.modifications.setApplicabilities}
isDisabled={meta?.status.isDisabled}
/>
</Box>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
networkModificationTableStyles,
} from '../network-modification-table-styles';
import { DepthBox } from './depth-box';
import { isCompositeModification } from '../utils';
import { isCompositeModification, toMessageValues } from '../utils';
import { useModificationLabelComputer, useSnackMessage } from '../../../hooks';
import { ComposedModificationMetadata, mergeSx, NetworkModificationMetadata, snackWithFallback } from '../../../utils';

Expand Down Expand Up @@ -54,7 +54,10 @@ export function NameCell({ row, table, onChange }: Readonly<NameCellProps>) {
(modification: ComposedModificationMetadata, formatBold: boolean = true) => {
return intl.formatMessage(
{ id: `network_modifications.${modification.messageType}` },
{ ...(modification as NetworkModificationMetadata), ...computeLabel(modification, formatBold) }
{
...toMessageValues(modification as NetworkModificationMetadata),
...computeLabel(modification, formatBold),
}
);
},
[computeLabel, intl]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,51 +12,36 @@ import { updateModificationStatusByRootNetwork } from '../../../services';
import { useSnackMessage } from '../../../hooks';
import {
ComposedModificationMetadata,
ExcludedNetworkModifications,
ModificationType,
NetworkModificationApplicabilities,
RootNetworkRowInfo,
snackWithFallback,
} from '../../../utils';

function getUpdatedExcludedModifications(
prev: ExcludedNetworkModifications[],
rootNetworkUuid: UUID,
modificationUuid: UUID
): { nextExcluded: ExcludedNetworkModifications[]; newStatus: boolean } {
const exists = prev.some((item) => item.rootNetworkUuid === rootNetworkUuid);

if (exists) {
let newStatus = false;
const nextExcluded = prev.map((modif) => {
if (modif.rootNetworkUuid !== rootNetworkUuid) {
return modif;
}

const isExcluded = modif.modificationUuidsToExclude.includes(modificationUuid);
const newModificationUuidsToExclude = isExcluded
? modif.modificationUuidsToExclude.filter((id) => id !== modificationUuid)
: [...modif.modificationUuidsToExclude, modificationUuid];

// If previously excluded, now it is activated (true), else deactivated (false)
newStatus = isExcluded;

return {
...modif,
modificationUuidsToExclude: newModificationUuidsToExclude,
};
});
/**
* A modification is applicable on a root network unless its applicability for it is explicitly false:
* a root network without an entry is applicable.
*/
function isApplicableOn(
applicabilities: NetworkModificationApplicabilities,
modificationUuid: UUID,
rootNetworkUuid: UUID
) {
return applicabilities[modificationUuid]?.[rootNetworkUuid] ?? true;
}

return { nextExcluded, newStatus };
}
function withApplicability(
applicabilities: NetworkModificationApplicabilities,
modificationUuid: UUID,
rootNetworkUuid: UUID,
applicable: boolean
): NetworkModificationApplicabilities {
Comment on lines +33 to +38

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is fine but I am a bit confused by the naming. Why 'with' ?

Suggested change
function withApplicability(
applicabilities: NetworkModificationApplicabilities,
modificationUuid: UUID,
tag: string,
applicable: boolean
): NetworkModificationApplicabilities {
function updateAnApplicability(
prevApplicabilities: NetworkModificationApplicabilities,
modificationUuid: UUID,
tag: string,
applicable: boolean
): NetworkModificationApplicabilities {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about setApplicability ?

return {
nextExcluded: [
...prev,
{
rootNetworkUuid,
modificationUuidsToExclude: [modificationUuid],
},
],
newStatus: false,
...applicabilities,
[modificationUuid]: {
...applicabilities[modificationUuid],
[rootNetworkUuid]: applicable,
},
};
}

Expand All @@ -65,8 +50,8 @@ export interface RootNetworkChipCellProps {
studyUuid: UUID | null;
currentNodeId?: UUID;
rootNetwork: RootNetworkRowInfo;
modificationsToExclude: ExcludedNetworkModifications[];
setModificationsToExclude: React.Dispatch<SetStateAction<ExcludedNetworkModifications[]>>;
applicabilities: NetworkModificationApplicabilities;
setApplicabilities: React.Dispatch<SetStateAction<NetworkModificationApplicabilities>>;
isDisabled?: boolean;
}

Expand All @@ -76,8 +61,8 @@ export function RootNetworkChipCell(props: RootNetworkChipCellProps) {
studyUuid,
currentNodeId,
rootNetwork,
modificationsToExclude,
setModificationsToExclude,
applicabilities,
setApplicabilities,
isDisabled = false,
} = props;
const [isLoading, setIsLoading] = useState(false);
Expand All @@ -88,17 +73,8 @@ export function RootNetworkChipCell(props: RootNetworkChipCellProps) {
data.type === ModificationType.MODIFICATION_REFERENCE || data.childFromShared;

const isModificationActivated = useMemo(() => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be called "applicable" or "applied", instead of activated : to avoid any confusion with the activated boolean that all modifications have.

Suggested change
const isModificationActivated = useMemo(() => {
const isModificationApplied = useMemo(() => {

@flomillot flomillot Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I did not know which name to choose...
I don't think applied is correct either. Because the modification is not necessarily applied yet.
And we have the same issue if we choose applicable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai Any suggestion ?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@flomillot, use isModificationApplicable.

applicable matches the state stored in NetworkModificationApplicabilities and the applicable backend parameter. It also avoids the implication that the modification has already been applied.

const isModificationApplicable = useMemo(() => {
    return isApplicableOn(applicabilities, modificationUuid, rootNetwork.rootNetworkUuid);
}, [modificationUuid, applicabilities, rootNetwork.rootNetworkUuid]);

isModificationActivated is only required at the ActivableChip API boundary, where it is the component prop name.

You are interacting with an AI system.

if (rootNetwork.isCreating) {
return true;
}

const excludedSet = new Set(
modificationsToExclude.find((item) => item.rootNetworkUuid === rootNetwork.rootNetworkUuid)
?.modificationUuidsToExclude || []
);

return !excludedSet.has(modificationUuid);
}, [modificationUuid, modificationsToExclude, rootNetwork.rootNetworkUuid, rootNetwork.isCreating]);
return isApplicableOn(applicabilities, modificationUuid, rootNetwork.rootNetworkUuid);
}, [modificationUuid, applicabilities, rootNetwork.rootNetworkUuid]);

const handleModificationActivationByRootNetwork = useCallback(() => {
if (!studyUuid || !currentNodeId) {
Expand All @@ -107,30 +83,26 @@ export function RootNetworkChipCell(props: RootNetworkChipCellProps) {

setIsLoading(true);

// Compute next state (pure, no side effects)
const { nextExcluded, newStatus } = getUpdatedExcludedModifications(
modificationsToExclude,
rootNetwork.rootNetworkUuid,
modificationUuid
);
// toggle the current applicability
const newApplicability = !isModificationActivated;

// Apply optimistic update
setModificationsToExclude(nextExcluded);
setApplicabilities((prev) =>
withApplicability(prev, modificationUuid, rootNetwork.rootNetworkUuid, newApplicability)
);

// Perform backend call
updateModificationStatusByRootNetwork(
studyUuid,
currentNodeId,
rootNetwork.rootNetworkUuid,
modificationUuid,
newStatus
newApplicability
)
.catch((error) => {
// Rollback on failure by toggling back
setModificationsToExclude(
(prev) =>
getUpdatedExcludedModifications(prev, rootNetwork.rootNetworkUuid, modificationUuid)
.nextExcluded
// Rollback on failure to the value shown when the user clicked
setApplicabilities((prev) =>
withApplicability(prev, modificationUuid, rootNetwork.rootNetworkUuid, isModificationActivated)
);
snackWithFallback(snackError, error, { headerId: 'modificationActivationByRootNetworkError' });
})
Expand All @@ -141,9 +113,9 @@ export function RootNetworkChipCell(props: RootNetworkChipCellProps) {
modificationUuid,
studyUuid,
currentNodeId,
modificationsToExclude,
isModificationActivated,
rootNetwork.rootNetworkUuid,
setModificationsToExclude,
setApplicabilities,
snackError,
]);

Expand All @@ -152,7 +124,7 @@ export function RootNetworkChipCell(props: RootNetworkChipCellProps) {
label={rootNetwork.tag}
tooltipMessage={rootNetwork.name}
isActivated={isModificationActivated}
isDisabled={isLoading || isDisabled || isReferenceModificationOrInsideOne}
isDisabled={isLoading || isDisabled || isReferenceModificationOrInsideOne || rootNetwork.isCreating}
Comment thread
Mathieu-Deharbe marked this conversation as resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that this disable when rootNetwork.isCreating won't be approved by Stephane. It gives the impression that the applicability changed when they are enabled because the loading is over. And given that gridstudy is multi user it will be confusing.

A loading mode/icon for chip will probably be needed. But it could be in a separate ticket.

onClick={handleModificationActivationByRootNetwork}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { useIntl } from 'react-intl';
import { networkModificationTableStyles } from '../network-modification-table-styles';
import { useModificationLabelComputer } from '../../../hooks';
import { ComposedModificationMetadata, mergeSx, NetworkModificationMetadata } from '../../../utils';
import { toMessageValues } from '../utils';

interface DragCloneRowProps {
row: Row<ComposedModificationMetadata>;
Expand All @@ -26,7 +27,10 @@ export function DragCloneRow({ row }: Readonly<DragCloneRowProps>) {
(modification: ComposedModificationMetadata, formatBold: boolean = true) => {
return intl.formatMessage(
{ id: `network_modifications.${modification.messageType}` },
{ ...(modification as NetworkModificationMetadata), ...computeLabel(modification, formatBold) }
{
...toMessageValues(modification as NetworkModificationMetadata),
...computeLabel(modification, formatBold),
}
);
},
[computeLabel, intl]
Expand Down
47 changes: 47 additions & 0 deletions src/features/network-modification-table/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,54 @@ import { fetchNetworkModification, getNetworkModificationsFromComposite } from '
import {
ComposedModificationMetadata,
MODIFICATION_TYPES,
NetworkModificationApplicabilities,
NetworkModificationMetadata,
ReferencedCompositeModifications,
ReferenceModificationInfos,
RootNetworkRowInfo,
} from '../../utils';

export const MAX_COMPOSITE_NESTING_DEPTH = 5;

// Convert applicability by tag to applicability by root network id.
function toApplicabilityByRootNetworkUuid(
applicabilityByRootNetworkTag: Record<string, boolean> | undefined,
uuidByTag: Map<string, UUID>
): Record<UUID, boolean> {
return Object.entries(applicabilityByRootNetworkTag ?? {}).reduce((applicability, [tag, applicable]) => {
const rootNetworkUuid = uuidByTag.get(tag);
return rootNetworkUuid ? { ...applicability, [rootNetworkUuid]: applicable } : applicability;
}, {});
}

function collectApplicabilitiesByUuid(
modifications: ComposedModificationMetadata[],
uuidByTag: Map<string, UUID>
): NetworkModificationApplicabilities {
return modifications.reduce(
(applicabilities, modification) => ({
...applicabilities,
[modification.uuid]: toApplicabilityByRootNetworkUuid(
modification.applicabilityByRootNetworkTag,
uuidByTag
),
...collectApplicabilitiesByUuid(modification.subModifications, uuidByTag),
}),
{}
);
}

// Indexes by uuid the applicability every modification of the tree carries, sub modifications included.
// The modifications key it by root network tag; it is resolved here to the root network uuid.
// A tag no root network claims is dropped.
export function collectApplicabilities(
modifications: ComposedModificationMetadata[],
rootNetworks: RootNetworkRowInfo[] = []
): NetworkModificationApplicabilities {
const uuidByTag = new Map(rootNetworks.map((rootNetwork) => [rootNetwork.tag, rootNetwork.rootNetworkUuid]));
return collectApplicabilitiesByUuid(modifications, uuidByTag);
}

// Every ComposedModificationMetadata carries a `rowKey`: a random id generated once when the node
// is created, decorrelated from the business `uuid`. It is the ONLY identity used to locate a
// specific node's *position* in the tree
Expand All @@ -31,6 +72,12 @@ export const formatToComposedModification = (
}));
};

// Remove the applicabilities
export function toMessageValues(modification: NetworkModificationMetadata) {
const { applicabilityByRootNetworkTag, ...messageValues } = modification;
return messageValues;
}

export function isCompositeModification(modification: ComposedModificationMetadata | undefined) {
return modification?.type === MODIFICATION_TYPES.COMPOSITE_MODIFICATION.type;
}
Expand Down
Loading
Loading