Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[4376] Add support for URLs to specify a selection of elements in the workbench #4461

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
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
2 changes: 1 addition & 1 deletion CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ The new endpoints are:
** getBranchById (`GET /api/rest/projects/{projectId}/branches/{branchId}`): Get the branch represented by its Id for the given project. There is only one branch per project in Sirius Web for now, and its Id is the same as the project Id. It represents the current state of the project, without taking care of data created/updated/deleted since the creation of the project.
** deleteBranch (`DELETE /api/rest/projects/{projectId}/branches/{branchId}`): Delete the branch with the given Id in the given project. There is only one branch per project in Sirius Web for now, so the default implementation of this method does nothing.
- https://github.com/eclipse-sirius/sirius-web/issues/4440[#4440] [sirius-web] Add the representation description name in details view

- https://github.com/eclipse-sirius/sirius-web/issues/4376[#4376] [sirius-web] Add current workbench selection to the URL. Reversely, when resolving a URL, the specified elements automatically get selected.

=== Improvements

Expand Down
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,10 @@ import {
RepresentationMetadata,
Selection,
SelectionContextProvider,
SelectionEntry,
useData,
useSelection,
UseSelectionValue,
Workbench,
} from '@eclipse-sirius/sirius-components-core';
import { OmniboxContextEntry, OmniboxProvider } from '@eclipse-sirius/sirius-components-omnibox';
Expand All @@ -25,7 +28,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 +67,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 }: UseSelectionValue = useSelection();

const [{ value, context }, dispatch] =
useMachine<StateMachine<EditProjectViewContext, EditProjectViewStateSchema, EditProjectViewEvent>>(
Expand All @@ -83,18 +147,34 @@ 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) {
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