Skip to content

Commit

Permalink
[4376] Add current selection as a URL search parameter (WIP)
Browse files Browse the repository at this point in the history
* When the workbench selection changes, the URL search parameters are
now automatically updated to encode the contents of the selection.
* Reversely, when resolving a URL with such search parameters, the
workbench selection is set to those specified elements.

Known limitations/bugs:
* Start from a (diagram) representation with a selection of elements in
the diagram. Select either the diagram representation in the 'Explorer'
view, or the diagram background. The selection gets set to the diagram
representation, but quickly evolves into an empty selection. This does
not occur when starting from a selection of elements that are not shown
in the diagram, or from an empty selection.
* When loading a URL with an opened representation and a selection of
elements, the selection is properly applied in the 'Explorer' view, but
not in the diagram.

Bug: eclipse-sirius#4376
Signed-off-by: Florent Latombe <[email protected]>
  • Loading branch information
flatombe committed Jan 23, 2025
1 parent e11bd1b commit 4a3ad53
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 19 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2021, 2024 Obeo.
* Copyright (c) 2021, 2025 Obeo.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
Expand Down Expand Up @@ -79,6 +79,7 @@ export const Workbench = ({
editingContextId,
initialRepresentationSelected,
onRepresentationSelected,
onSelectionChanged,
readOnly,
}: WorkbenchProps) => {
const { classes } = useWorkbenchStyles();
Expand Down Expand Up @@ -156,6 +157,10 @@ export const Workbench = ({
}
}, [onRepresentationSelected, initialRepresentationSelected, displayedRepresentation]);

useEffect(() => {
onSelectionChanged(selection);
}, [selection]);

const workbenchViewLeftSideContributions: WorkbenchViewContribution[] = [];
const workbenchViewRightSideContributions: WorkbenchViewContribution[] = [];

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2021, 2024 Obeo and others.
* Copyright (c) 2021, 2025 Obeo and others.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
Expand All @@ -11,6 +11,7 @@
* Obeo - initial API and implementation
*******************************************************************************/
import React from 'react';
import { Selection } from '../selection/SelectionContext.types';

export interface GQLEditingContextEventPayload {
__typename: string;
Expand Down Expand Up @@ -56,6 +57,7 @@ export type WorkbenchProps = {
editingContextId: string;
initialRepresentationSelected: RepresentationMetadata | null;
onRepresentationSelected: (representation: RepresentationMetadata | null) => void;
onSelectionChanged: (selection: Selection | null) => void;
readOnly: boolean;
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2019, 2024 Obeo.
* Copyright (c) 2019, 2025 Obeo.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
Expand All @@ -14,7 +14,9 @@ import {
RepresentationMetadata,
Selection,
SelectionContextProvider,
SelectionEntry,
useData,
useSelection,
Workbench,
} from '@eclipse-sirius/sirius-components-core';
import { OmniboxContextEntry, OmniboxProvider } from '@eclipse-sirius/sirius-components-omnibox';
Expand All @@ -25,7 +27,15 @@ import {
} from '@eclipse-sirius/sirius-components-trees';
import { useMachine } from '@xstate/react';
import { useEffect } from 'react';
import { generatePath, Navigate, useNavigate, useParams, useResolvedPath } from 'react-router-dom';
import {
generatePath,
Navigate,
SetURLSearchParams,
useNavigate,
useParams,
useResolvedPath,
useSearchParams,
} from 'react-router-dom';
import { makeStyles } from 'tss-react/mui';
import { StateMachine } from 'xstate';
import { NavigationBar } from '../../navigationBar/NavigationBar';
Expand Down Expand Up @@ -56,11 +66,64 @@ const useEditProjectViewStyles = makeStyles()((_) => ({
},
}));

function createSelectionFromUrlSearchParams(urlSearchParams: URLSearchParams): Selection {
if (urlSearchParams !== null && urlSearchParams.has('selection')) {
const urlSelectionContents: Array<string> = urlSearchParams.get('selection').split(',');
const selectionEntries: Array<SelectionEntry> = urlSelectionContents.map((urlSelectionElement) => {
const [projectElementId, projectElementKind] = urlSelectionElement.split(';');
return { id: projectElementId, kind: projectElementKind };
});
return { entries: selectionEntries };
} else {
return null;
}
}

function updateUrlSearchParamsWithSelection(setUrlSearchParams: SetURLSearchParams, selection: Selection) {
setUrlSearchParams((previousSearchParams: URLSearchParams) => {
if (selection?.entries.length > 0) {
const selectionValue: string = selection.entries
.map((selectionEntry) => `${selectionEntry.id};${selectionEntry.kind}`)
.join(',');
previousSearchParams.set('selection', selectionValue);
} else {
if (previousSearchParams.has('selection')) {
previousSearchParams.delete('selection');
}
}
return previousSearchParams;
});
}

function updateSelectionBasedOnUrlSearchParamsIfNeeded(
urlSearchParams: URLSearchParams,
currentSelection: Selection,
setSelection: (selection: Selection) => void
) {
if (urlSearchParams?.has('selection')) {
const urlSelection: Selection = createSelectionFromUrlSearchParams(urlSearchParams);
if (!areSelectionContentsEqual(urlSelection, currentSelection)) {
setSelection(createSelectionFromUrlSearchParams(urlSearchParams));
}
}
}

function areSelectionContentsEqual(left: Selection, right: Selection): boolean {
return (
left?.entries.length == right?.entries.length &&
left?.entries.every(
(element, index) => element.id === right?.entries[index].id && element.kind === right?.entries[index].kind
)
);
}

export const EditProjectView = () => {
const navigate = useNavigate();
const routeMatch = useResolvedPath('.');
const { projectId, representationId } = useParams<EditProjectViewParams>();
const { classes } = useEditProjectViewStyles();
const [urlSearchParams, setUrlSearchParams]: [URLSearchParams, SetURLSearchParams] = useSearchParams();
const { selection, setSelection } = useSelection();

const [{ value, context }, dispatch] =
useMachine<StateMachine<EditProjectViewContext, EditProjectViewStateSchema, EditProjectViewEvent>>(
Expand All @@ -83,18 +146,35 @@ export const EditProjectView = () => {
dispatch(selectRepresentationEvent);
};

const workbenchOnSelectionChanged = (selection: Selection) => {
updateUrlSearchParamsWithSelection(setUrlSearchParams, selection);
};

useEffect(() => {
let pathname: string = null;
if (context.representation && context.representation.id !== representationId) {
const pathname = generatePath('/projects/:projectId/edit/:representationId', {
pathname = generatePath('/projects/:projectId/edit/:representationId', {
projectId,
representationId: context.representation.id,
});
navigate(pathname);
} else if (value === 'loaded' && context.representation === null && representationId) {
const pathname = generatePath('/projects/:projectId/edit/', { projectId });
navigate(pathname);
pathname = generatePath('/projects/:projectId/edit/', { projectId });
}
}, [value, projectId, routeMatch, history, context.representation, representationId]);

if (pathname !== null) {
if (urlSearchParams !== null && urlSearchParams.size > 0) {
// The navigation hook removes the search params, so we need to make sure we maintain them in the URL we navigate to.
navigate({
pathname: pathname,
search: `?${urlSearchParams}`,
});
} else {
navigate(pathname);
}
}
}, [value, projectId, routeMatch, history, context.representation, representationId, urlSearchParams]);

updateSelectionBasedOnUrlSearchParamsIfNeeded(urlSearchParams, selection, setSelection);

let content: React.ReactNode = null;

Expand All @@ -109,16 +189,7 @@ export const EditProjectView = () => {
const { data: readOnlyPredicate } = useData(editProjectViewReadOnlyPredicateExtensionPoint);

if (value === 'loaded' && context.project) {
const initialSelection: Selection = {
entries: context.representation
? [
{
id: context.representation.id,
kind: context.representation.kind,
},
]
: [],
};
const initialSelection: Selection = createSelectionFromUrlSearchParams(urlSearchParams);

const readOnly = readOnlyPredicate(context.project);
const initialContextEntries: OmniboxContextEntry[] = [
Expand All @@ -135,6 +206,7 @@ export const EditProjectView = () => {
editingContextId={context.project.currentEditingContext.id}
initialRepresentationSelected={context.representation}
onRepresentationSelected={onRepresentationSelected}
onSelectionChanged={workbenchOnSelectionChanged}
readOnly={readOnly}
/>
</TreeToolBarProvider>
Expand Down

0 comments on commit 4a3ad53

Please sign in to comment.