diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 57e13c2dfc..52501b1061 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -42,6 +42,9 @@ You have to add `tableWidgetDocumentTransform` to your extensionRegistry to use - https://github.com/eclipse-sirius/sirius-web/issues/4588[#4588] [core] Added support for custom ordering in representation creation modal. See the new interface `org.eclipse.sirius.components.emf.services.api.IRepresentationDescriptionMetadataSorter`. - https://github.com/eclipse-sirius/sirius-web/issues/4616[#4616] [sirius-web] Allow end users to see the content of a library in a workbench, the path of the new page is `/libraries/:namespace/:name/:version` +- https://github.com/eclipse-sirius/sirius-web/issues/4597[#4597] [sirius-web] Add a command to publish libraries from studios +The command creates a library for each _RepresentationDescription_ and _Domain_ in the studio, and creates the dependencies between them. +A _shared components_ library can be created in the process to store elements that are needed by other libraries but are not stored in libraries themselves. === Improvements diff --git a/packages/core/frontend/sirius-components-omnibox/src/Omnibox.tsx b/packages/core/frontend/sirius-components-omnibox/src/Omnibox.tsx index 6382b33e50..bb6a66ebd8 100644 --- a/packages/core/frontend/sirius-components-omnibox/src/Omnibox.tsx +++ b/packages/core/frontend/sirius-components-omnibox/src/Omnibox.tsx @@ -10,7 +10,7 @@ * Contributors: * Obeo - initial API and implementation *******************************************************************************/ -import { useData, useSelection } from '@eclipse-sirius/sirius-components-core'; +import { useSelection } from '@eclipse-sirius/sirius-components-core'; import ChevronRightIcon from '@mui/icons-material/ChevronRight'; import SubdirectoryArrowLeftIcon from '@mui/icons-material/SubdirectoryArrowLeft'; import CircularProgress from '@mui/material/CircularProgress'; @@ -22,12 +22,9 @@ import IconButton from '@mui/material/IconButton'; import Input from '@mui/material/Input'; import { useEffect, useRef, useState } from 'react'; import { makeStyles } from 'tss-react/mui'; -import { OmniboxAction, OmniboxProps, OmniboxState } from './Omnibox.types'; +import { OmniboxMode, OmniboxProps, OmniboxState } from './Omnibox.types'; import { OmniboxCommandList } from './OmniboxCommandList'; -import { omniboxCommandOverrideContributionExtensionPoint } from './OmniboxExtensionPoints'; -import { OmniboxCommandOverrideContribution } from './OmniboxExtensionPoints.types'; import { OmniboxObjectList } from './OmniboxObjectList'; -import { isExecuteOmniboxCommandSuccessPayload, useExecuteOmniboxCommand } from './useExecuteOmniboxCommand'; import { useOmniboxCommands } from './useOmniboxCommands'; import { GQLGetOmniboxCommandsQueryVariables } from './useOmniboxCommands.types'; import { useOmniboxSearch } from './useOmniboxSearch'; @@ -76,7 +73,6 @@ export const Omnibox = ({ open, editingContextId, onClose }: OmniboxProps) => { const { getOmniboxCommands, loading: commandLoading, data: commandData } = useOmniboxCommands(); const { getOmniboxSearchResults, loading: searchResultsLoading, data: searchResultsData } = useOmniboxSearch(); - const { executeOmniboxCommand, data: executeOmniboxCommandData } = useExecuteOmniboxCommand(); const { selection } = useSelection(); const selectedObjectIds: string[] = selection.entries.map((entry) => entry.id); @@ -90,10 +86,6 @@ export const Omnibox = ({ open, editingContextId, onClose }: OmniboxProps) => { getOmniboxCommands({ variables }); }, []); - const { data: omniboxCommandOverrideContributions } = useData( - omniboxCommandOverrideContributionExtensionPoint - ); - const inputRef = useRef(null); const listRef = useRef(null); @@ -142,44 +134,27 @@ export const Omnibox = ({ open, editingContextId, onClose }: OmniboxProps) => { } }; - const handleOnActionClick = (action: OmniboxAction) => { - const commandOverride: OmniboxCommandOverrideContribution | undefined = omniboxCommandOverrideContributions.filter( - (contribution) => contribution.canHandle(action) - )[0]; - if (commandOverride) { - commandOverride.handle(action); - onClose(); - } else if (action.id === 'search') { - setState((prevState) => ({ - ...prevState, - mode: 'Search', - queryHasChanged: true, - })); - if (inputRef.current) { - inputRef.current.value = ''; - } - inputRef.current?.focus(); - } else { - executeOmniboxCommand(editingContextId, selectedObjectIds, action.id); + const onModeChanged = (mode: OmniboxMode) => { + setState((prevState) => ({ + ...prevState, + mode, + queryHasChanged: true, + })); + if (inputRef.current) { + inputRef.current.value = ''; } + inputRef.current?.focus(); }; - useEffect(() => { - if ( - executeOmniboxCommandData && - isExecuteOmniboxCommandSuccessPayload(executeOmniboxCommandData.executeOmniboxCommand) - ) { - onClose(); - } - }, [executeOmniboxCommandData]); - let omniboxResult: JSX.Element | null = null; if (state.mode === 'Command') { omniboxResult = ( ); diff --git a/packages/core/frontend/sirius-components-omnibox/src/Omnibox.types.ts b/packages/core/frontend/sirius-components-omnibox/src/Omnibox.types.ts index aac3e0c621..cad4fce0a0 100644 --- a/packages/core/frontend/sirius-components-omnibox/src/Omnibox.types.ts +++ b/packages/core/frontend/sirius-components-omnibox/src/Omnibox.types.ts @@ -23,9 +23,3 @@ export interface OmniboxState { } export type OmniboxMode = 'Command' | 'Search'; - -export interface OmniboxAction { - id: string; - icon: JSX.Element; - label: string; -} diff --git a/packages/core/frontend/sirius-components-omnibox/src/OmniboxCommandList.tsx b/packages/core/frontend/sirius-components-omnibox/src/OmniboxCommandList.tsx index 4698cbc87e..b56c1dc79f 100644 --- a/packages/core/frontend/sirius-components-omnibox/src/OmniboxCommandList.tsx +++ b/packages/core/frontend/sirius-components-omnibox/src/OmniboxCommandList.tsx @@ -10,17 +10,35 @@ * Contributors: * Obeo - initial API and implementation *******************************************************************************/ -import { IconOverlay } from '@eclipse-sirius/sirius-components-core'; +import { IconOverlay, useData, useSelection } from '@eclipse-sirius/sirius-components-core'; import List from '@mui/material/List'; import ListItem from '@mui/material/ListItem'; import ListItemButton from '@mui/material/ListItemButton'; import ListItemIcon from '@mui/material/ListItemIcon'; import ListItemText from '@mui/material/ListItemText'; -import { forwardRef } from 'react'; +import { forwardRef, useEffect } from 'react'; import { OmniboxCommandListProps } from './OmniboxCommandList.types'; +import { isExecuteOmniboxCommandSuccessPayload, useExecuteOmniboxCommand } from './useExecuteOmniboxCommand'; +import { OmniboxCommandOverrideContribution } from './OmniboxExtensionPoints.types'; +import { omniboxCommandOverrideContributionExtensionPoint } from './OmniboxExtensionPoints'; +import { GQLOmniboxCommand } from './useOmniboxCommands.types'; export const OmniboxCommandList = forwardRef( - ({ loading, data, onActionClick }: OmniboxCommandListProps, ref: React.ForwardedRef) => { + ( + { loading, data, editingContextId, onClose, onModeChanged }: OmniboxCommandListProps, + ref: React.ForwardedRef + ) => { + const { executeOmniboxCommand, data: executeOmniboxCommandData } = useExecuteOmniboxCommand(); + + useEffect(() => { + if ( + executeOmniboxCommandData && + isExecuteOmniboxCommandSuccessPayload(executeOmniboxCommandData.executeOmniboxCommand) + ) { + onClose(); + } + }, [executeOmniboxCommandData]); + const handleListItemKeyDown: React.KeyboardEventHandler = (event) => { if (event.key === 'ArrowDown') { const nextListItemButton = event.currentTarget.nextSibling; @@ -35,6 +53,17 @@ export const OmniboxCommandList = forwardRef( } }; + const { selection } = useSelection(); + const selectedObjectIds: string[] = selection.entries.map((entry) => entry.id); + + const handleOnActionClick = (command: GQLOmniboxCommand) => { + if (command.id === 'search') { + onModeChanged('Search'); + } else { + executeOmniboxCommand(editingContextId, selectedObjectIds, command.id); + } + }; + let listItems: JSX.Element[] = []; if (loading) { listItems = [ @@ -46,29 +75,36 @@ export const OmniboxCommandList = forwardRef( ]; } + const { data: omniboxCommandOverrideContributions } = useData( + omniboxCommandOverrideContributionExtensionPoint + ); + if (!loading && data) { const commands = data.viewer.omniboxCommands.edges.map((edge) => edge.node); if (commands.length > 0) { - listItems = commands - .map((node) => { - return { - id: node.id, - icon: , - label: node.label, - }; - }) - .map((action) => { + listItems = commands.map((command) => { + const CommandOverride = omniboxCommandOverrideContributions + .filter((contribution) => contribution.canHandle(command)) + .map((contribution) => contribution.component)[0]; + if (CommandOverride) { + return ( + + ); + } else { return ( onActionClick(action)} + key={command.id} + data-testid={command.label} + onClick={() => handleOnActionClick(command)} onKeyDown={handleListItemKeyDown}> - {action.icon} - {action.label} + + + + {command.label} ); - }); + } + }); } else { listItems = [ diff --git a/packages/core/frontend/sirius-components-omnibox/src/OmniboxCommandList.types.ts b/packages/core/frontend/sirius-components-omnibox/src/OmniboxCommandList.types.ts index 7af66b2092..3129254cf9 100644 --- a/packages/core/frontend/sirius-components-omnibox/src/OmniboxCommandList.types.ts +++ b/packages/core/frontend/sirius-components-omnibox/src/OmniboxCommandList.types.ts @@ -11,11 +11,13 @@ * Obeo - initial API and implementation *******************************************************************************/ -import { OmniboxAction } from './Omnibox.types'; +import { OmniboxMode } from './Omnibox.types'; import { GQLGetOmniboxCommandsQueryData } from './useOmniboxCommands.types'; export interface OmniboxCommandListProps { data: GQLGetOmniboxCommandsQueryData | null; loading: boolean; - onActionClick: (action: OmniboxAction) => void; + editingContextId: string; + onClose: () => void; + onModeChanged: (mode: OmniboxMode) => void; } diff --git a/packages/core/frontend/sirius-components-omnibox/src/OmniboxExtensionPoints.types.ts b/packages/core/frontend/sirius-components-omnibox/src/OmniboxExtensionPoints.types.ts index ceaa0c7f01..04bfff4496 100644 --- a/packages/core/frontend/sirius-components-omnibox/src/OmniboxExtensionPoints.types.ts +++ b/packages/core/frontend/sirius-components-omnibox/src/OmniboxExtensionPoints.types.ts @@ -10,9 +10,15 @@ * Contributors: * Obeo - initial API and implementation *******************************************************************************/ -import { OmniboxAction } from './Omnibox.types'; +import { GQLOmniboxCommand } from './useOmniboxCommands.types'; export interface OmniboxCommandOverrideContribution { - canHandle: (action: OmniboxAction) => boolean; - handle: (action: OmniboxAction) => void; + canHandle: (action: GQLOmniboxCommand) => boolean; + component: React.ComponentType; +} + +export interface OmniboxCommandComponentProps { + command: GQLOmniboxCommand; + onKeyDown: React.KeyboardEventHandler; + onClose: () => void; } diff --git a/packages/core/frontend/sirius-components-omnibox/src/index.ts b/packages/core/frontend/sirius-components-omnibox/src/index.ts index 2055b199c5..4db1653aeb 100644 --- a/packages/core/frontend/sirius-components-omnibox/src/index.ts +++ b/packages/core/frontend/sirius-components-omnibox/src/index.ts @@ -11,8 +11,8 @@ * Obeo - initial API and implementation *******************************************************************************/ -export { type OmniboxAction } from './Omnibox.types'; export * from './OmniboxButton'; export * from './OmniboxExtensionPoints'; export * from './OmniboxExtensionPoints.types'; export * from './OmniboxProvider'; +export { type GQLOmniboxCommand } from './useOmniboxCommands.types'; diff --git a/packages/core/frontend/sirius-components-omnibox/src/useOmniboxCommands.types.ts b/packages/core/frontend/sirius-components-omnibox/src/useOmniboxCommands.types.ts index 281e5d37c2..363cbf7270 100644 --- a/packages/core/frontend/sirius-components-omnibox/src/useOmniboxCommands.types.ts +++ b/packages/core/frontend/sirius-components-omnibox/src/useOmniboxCommands.types.ts @@ -39,10 +39,10 @@ export interface GQLViewerOmniboxCommandsConnection { } export interface GQLViewerOmniboxCommandsEdge { - node: GQLViewerOmniboxCommand; + node: GQLOmniboxCommand; } -export interface GQLViewerOmniboxCommand { +export interface GQLOmniboxCommand { id: string; label: string; iconURLs: string[]; diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/library/controllers/MutationPublishLibrariesDataFetcher.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/library/controllers/MutationPublishLibrariesDataFetcher.java new file mode 100644 index 0000000000..7cbba656e2 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/library/controllers/MutationPublishLibrariesDataFetcher.java @@ -0,0 +1,54 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.library.controllers; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.sirius.components.annotations.spring.graphql.QueryDataFetcher; +import org.eclipse.sirius.components.core.api.IPayload; +import org.eclipse.sirius.components.graphql.api.IDataFetcherWithFieldCoordinates; +import org.eclipse.sirius.web.application.library.dto.PublishLibrariesInput; +import org.eclipse.sirius.web.application.library.services.api.ILibraryApplicationService; + +import graphql.schema.DataFetchingEnvironment; + +/** + * Data fetcher for the field Mutation#publishLibraries. + * + * @author gdaniel + */ +@QueryDataFetcher(type = "Mutation", field = "publishLibraries") +public class MutationPublishLibrariesDataFetcher implements IDataFetcherWithFieldCoordinates> { + + private static final String INPUT_ARGUMENT = "input"; + + private final ObjectMapper objectMapper; + + private final ILibraryApplicationService libraryApplicationService; + + public MutationPublishLibrariesDataFetcher(ObjectMapper objectMapper, ILibraryApplicationService libraryApplicationService) { + this.objectMapper = Objects.requireNonNull(objectMapper); + this.libraryApplicationService = Objects.requireNonNull(libraryApplicationService); + } + + @Override + public CompletableFuture get(DataFetchingEnvironment environment) throws Exception { + Object argument = environment.getArgument(INPUT_ARGUMENT); + var input = this.objectMapper.convertValue(argument, PublishLibrariesInput.class); + + return CompletableFuture.supplyAsync(() -> this.libraryApplicationService.publishLibraries(input)); + } +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/library/dto/PublishLibrariesInput.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/library/dto/PublishLibrariesInput.java new file mode 100644 index 0000000000..f2169bdc17 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/library/dto/PublishLibrariesInput.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.library.dto; + +import java.util.UUID; + +import org.eclipse.sirius.components.core.api.IInput; + +import jakarta.validation.constraints.NotNull; + +/** + * Input used to publish libraries. + * + * @author gdaniel + */ +public record PublishLibrariesInput(@NotNull UUID id, @NotNull String projectId, @NotNull String publicationKind, @NotNull String version, @NotNull String description) implements IInput { + +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/library/services/DependencyGraph.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/library/services/DependencyGraph.java new file mode 100644 index 0000000000..178b1ee28d --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/library/services/DependencyGraph.java @@ -0,0 +1,142 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.library.services; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Stream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Graph used to store and manipulate dependencies. + * + * @param the type of the nodes in the graph + * + * @author gdaniel + */ +public class DependencyGraph { + + private final Logger logger = LoggerFactory.getLogger(DependencyGraph.class); + + private Set nodes = new LinkedHashSet<>(); + + private List> edges = new ArrayList<>(); + + public void addNode(T node) { + Objects.requireNonNull(node); + this.nodes.add(node); + } + + public void removeNode(T node) { + Objects.requireNonNull(node); + if (this.nodes.remove(node)) { + this.edges.removeIf(e -> Objects.equals(e.source(), node) || Objects.equals(e.target(), node)); + } + } + + public void addEdge(T source, T target) { + this.addNode(source); + this.addNode(target); + Objects.requireNonNull(source); + Objects.requireNonNull(target); + if (this.edges.stream().noneMatch(edge -> Objects.equals(edge.source(), source) && Objects.equals(edge.target(), target))) { + this.edges.add(new Edge<>(source, target)); + } + } + + public List getDependencies(T source) { + return this.getOutgoingEdges(source).stream().map(Edge::target).toList(); + } + + public List computeTopologicalOrdering() { + List result = new ArrayList<>(); + List> localEdges = new ArrayList<>(this.edges); + List nodesWithoutIncomingEdges = this.getRootNodes(); + while (!nodesWithoutIncomingEdges.isEmpty()) { + T node = nodesWithoutIncomingEdges.remove(0); + result.add(node); + for (Edge outgoingEdge : this.getOutgoingEdges(node)) { + localEdges.remove(outgoingEdge); + if (localEdges.stream().noneMatch(e -> Objects.equals(e.target(), outgoingEdge.target()))) { + nodesWithoutIncomingEdges.add(outgoingEdge.target()); + } + } + } + if (!localEdges.isEmpty()) { + this.logger.warn("Cannot compute dependency ordering: there is at least one cycle in the dependency graph"); + result = List.of(); + } else { + // Reverse the list to ensure dependencies are before elements that depend on them. + Collections.reverse(result); + } + return result; + } + + public Set> getComponents() { + Set> result = new LinkedHashSet<>(); + List localNodes = new ArrayList<>(this.nodes); + + while (!localNodes.isEmpty()) { + T node = localNodes.remove(0); + result.add(this.findComponentNodes(node, localNodes)); + } + return result; + } + + public List findComponentNodes(T startNode, List graphNodes) { + List componentNodes = new ArrayList<>(); + List connectedNodes = Stream.concat(this.getIncomingEdges(startNode).stream().map(Edge::source), this.getOutgoingEdges(startNode).stream().map(Edge::target)).toList(); + for (T connectedNode : connectedNodes) { + if (graphNodes.contains(connectedNode)) { + graphNodes.remove(connectedNode); + componentNodes.add(connectedNode); + componentNodes.addAll(this.findComponentNodes(connectedNode, graphNodes)); + } + } + return componentNodes; + } + + private List getRootNodes() { + List result = new ArrayList<>(); + for (T node : this.nodes) { + if (this.getIncomingEdges(node).isEmpty()) { + result.add(node); + } + } + return result; + } + + private List> getIncomingEdges(T target) { + return this.edges.stream().filter(e -> Objects.equals(e.target(), target)).toList(); + } + + private List> getOutgoingEdges(T source) { + return this.edges.stream().filter(e -> Objects.equals(e.source(), source)).toList(); + } + + /** + * An edge in the dependency graph. + * + * @author gdaniel + */ + private record Edge(T source, T target) { + + } + +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/library/services/LibraryApplicationService.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/library/services/LibraryApplicationService.java index aa0f00005d..cc9b39c5ba 100644 --- a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/library/services/LibraryApplicationService.java +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/library/services/LibraryApplicationService.java @@ -12,13 +12,21 @@ *******************************************************************************/ package org.eclipse.sirius.web.application.library.services; +import java.util.List; import java.util.Objects; import java.util.Optional; +import org.eclipse.sirius.components.core.api.ErrorPayload; +import org.eclipse.sirius.components.core.api.IPayload; import org.eclipse.sirius.web.application.library.dto.LibraryDTO; +import org.eclipse.sirius.web.application.library.dto.PublishLibrariesInput; import org.eclipse.sirius.web.application.library.services.api.ILibraryApplicationService; import org.eclipse.sirius.web.application.library.services.api.ILibraryMapper; +import org.eclipse.sirius.web.application.library.services.api.ILibraryPublicationHandler; import org.eclipse.sirius.web.domain.boundedcontexts.library.services.api.ILibrarySearchService; +import org.eclipse.sirius.web.domain.services.api.IMessageService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -36,9 +44,17 @@ public class LibraryApplicationService implements ILibraryApplicationService { private final ILibraryMapper libraryMapper; - public LibraryApplicationService(ILibrarySearchService librarySearchService, ILibraryMapper libraryMapper) { + private final List libraryPublicationHandlers; + + private final IMessageService messageService; + + private final Logger logger = LoggerFactory.getLogger(LibraryApplicationService.class); + + public LibraryApplicationService(ILibrarySearchService librarySearchService, ILibraryMapper libraryMapper, List libraryPublicationHandlers, IMessageService messageService) { this.librarySearchService = Objects.requireNonNull(librarySearchService); this.libraryMapper = Objects.requireNonNull(libraryMapper); + this.libraryPublicationHandlers = Objects.requireNonNull(libraryPublicationHandlers); + this.messageService = Objects.requireNonNull(messageService); } @Override @@ -53,5 +69,18 @@ public Optional findByNamespaceAndNameAndVersion(String namespace, S return this.librarySearchService.findByNamespaceAndNameAndVersion(namespace, name, version).map(this.libraryMapper::toDTO); } - + @Override + @Transactional + public IPayload publishLibraries(PublishLibrariesInput input) { + IPayload payload = new ErrorPayload(input.id(), this.messageService.unexpectedError()); + Optional optionalHandler = this.libraryPublicationHandlers.stream() + .filter(handler -> handler.canHandle(input)) + .findFirst(); + if (optionalHandler.isPresent()) { + payload = optionalHandler.get().handle(input); + } else { + this.logger.warn("No handler found for event: {}", input); + } + return payload; + } } diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/library/services/api/ILibraryApplicationService.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/library/services/api/ILibraryApplicationService.java index 0b7814757a..02bcc91de0 100644 --- a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/library/services/api/ILibraryApplicationService.java +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/library/services/api/ILibraryApplicationService.java @@ -14,7 +14,9 @@ import java.util.Optional; +import org.eclipse.sirius.components.core.api.IPayload; import org.eclipse.sirius.web.application.library.dto.LibraryDTO; +import org.eclipse.sirius.web.application.library.dto.PublishLibrariesInput; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -29,4 +31,5 @@ public interface ILibraryApplicationService { Optional findByNamespaceAndNameAndVersion(String namespace, String name, String version); + IPayload publishLibraries(PublishLibrariesInput input); } diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/library/services/api/ILibraryPublicationHandler.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/library/services/api/ILibraryPublicationHandler.java new file mode 100644 index 0000000000..5541c92c6d --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/library/services/api/ILibraryPublicationHandler.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.library.services.api; + +import org.eclipse.sirius.components.core.api.IPayload; +import org.eclipse.sirius.web.application.library.dto.PublishLibrariesInput; + +/** + * Handles the publication of libraries. + * + * @author gdaniel + */ +public interface ILibraryPublicationHandler { + + boolean canHandle(PublishLibrariesInput input); + + IPayload handle(PublishLibrariesInput input); + +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/studio/services/StudioPublicationCommandProvider.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/studio/services/StudioPublicationCommandProvider.java new file mode 100644 index 0000000000..2f045535ad --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/studio/services/StudioPublicationCommandProvider.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.studio.services; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import org.eclipse.sirius.components.collaborative.omnibox.api.IOmniboxCommandProvider; +import org.eclipse.sirius.components.collaborative.omnibox.dto.OmniboxCommand; +import org.eclipse.sirius.web.application.studio.services.api.IStudioCapableEditingContextPredicate; +import org.springframework.stereotype.Service; + +/** + * Provides the publish studio command in the omnibox. + * + * @author gdaniel + */ +@Service +public class StudioPublicationCommandProvider implements IOmniboxCommandProvider { + + public static final String PUBLISH_STUDIO_COMMAND_ID = "publishStudio"; + + private final IStudioCapableEditingContextPredicate studioCapableEditingContextPredicate; + + public StudioPublicationCommandProvider(IStudioCapableEditingContextPredicate studioCapableEditingContextPredicate) { + this.studioCapableEditingContextPredicate = Objects.requireNonNull(studioCapableEditingContextPredicate); + } + + @Override + public List getCommands(String editingContextId, List selectedObjectIds, String query) { + List result = new ArrayList<>(); + if (this.studioCapableEditingContextPredicate.test(editingContextId)) { + result.add(new OmniboxCommand(PUBLISH_STUDIO_COMMAND_ID, "Publish Studio", List.of("/omnibox/publish.svg"), "Publish all the domains and representation descriptions as individual libraries")); + } + return result; + } + +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/studio/services/library/StudioLibraryDependencyCollector.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/studio/services/library/StudioLibraryDependencyCollector.java new file mode 100644 index 0000000000..47b23eb1a9 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/studio/services/library/StudioLibraryDependencyCollector.java @@ -0,0 +1,121 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.studio.services.library; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import org.eclipse.emf.ecore.EAttribute; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.EReference; +import org.eclipse.emf.ecore.resource.ResourceSet; +import org.eclipse.emf.ecore.util.ECrossReferenceAdapter; +import org.eclipse.sirius.components.domain.Domain; +import org.eclipse.sirius.components.view.RepresentationDescription; +import org.eclipse.sirius.web.application.library.services.DependencyGraph; +import org.eclipse.sirius.web.application.studio.services.library.api.IStudioLibraryDependencyCollector; +import org.springframework.stereotype.Service; + +/** + * Collects the dependencies between studio libraries. + * + * @author gdaniel + */ +@Service +public class StudioLibraryDependencyCollector implements IStudioLibraryDependencyCollector { + + @Override + public DependencyGraph collectDependencies(ResourceSet rSet) { + + DependencyGraph dependencyGraph = new DependencyGraph<>(); + List domains = new ArrayList<>(); + List eObjectsWithDomainType = new ArrayList<>(); + rSet.getAllContents() + .forEachRemaining(notifier -> { + if (notifier instanceof EObject eObject) { + if (eObject instanceof Domain domain) { + // Cache the domains, we need them to resolve domainTypes. + domains.add(domain); + dependencyGraph.addNode(domain); + } else if (eObject instanceof RepresentationDescription representationDescription) { + dependencyGraph.addNode(representationDescription); + } else if (this.getDomainTypeAttribute(eObject).isPresent()) { + eObjectsWithDomainType.add(eObject); + } + EObject eObjectLibrary = this.getContainingLibraryCandidate(eObject).orElse(eObject); + ECrossReferenceAdapter.getCrossReferenceAdapter(eObject) + .getInverseReferences(eObject) + .forEach(setting -> { + if (setting.getEStructuralFeature() instanceof EReference eReference + && !eReference.isContainment()) { + EObject dependentLibrary = this.getContainingLibraryCandidate(setting.getEObject()).orElse(setting.getEObject()); + if (dependentLibrary != eObjectLibrary) { + dependencyGraph.addEdge(dependentLibrary, eObjectLibrary); + } + } + }); + } + }); + + // Add domainType-based dependencies. + for (EObject eObjectWithDomainType : eObjectsWithDomainType) { + this.getDomainTypeAttribute(eObjectWithDomainType) + .ifPresent(domainTypeAttribute -> { + String domainType = (String) eObjectWithDomainType.eGet(domainTypeAttribute); + if (domainType != null) { + String[] splittedDomainType = domainType.split("::"); + if (splittedDomainType.length > 0) { + String domainName = splittedDomainType[0]; + domains.stream() + .filter(domain -> Objects.equals(domain.getName(), domainName)) + .findFirst() + .ifPresent(domain -> { + EObject eObjectLibrary = this.getContainingLibraryCandidate(eObjectWithDomainType).orElse(eObjectWithDomainType); + dependencyGraph.addEdge(eObjectLibrary, domain); + }); + } + } + }); + } + + // Remove non-library elements that aren't connected to a library + for (List component : dependencyGraph.getComponents()) { + if (component.stream().noneMatch(element -> element instanceof Domain || element instanceof RepresentationDescription)) { + component.forEach(dependencyGraph::removeNode); + } + } + + return dependencyGraph; + } + + private Optional getDomainTypeAttribute(EObject eObject) { + return eObject.eClass().getEAllAttributes().stream() + .filter(eAttribute -> Objects.equals(eAttribute.getName(), "domainType")) + .findFirst(); + } + + private Optional getContainingLibraryCandidate(EObject eObject) { + final Optional result; + if (eObject instanceof RepresentationDescription || eObject instanceof Domain) { + result = Optional.of(eObject); + } else if (eObject == null) { + result = Optional.empty(); + } else { + result = this.getContainingLibraryCandidate(eObject.eContainer()); + } + return result; + } + +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/studio/services/library/StudioLibraryPublicationHandler.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/studio/services/library/StudioLibraryPublicationHandler.java new file mode 100644 index 0000000000..a1c9c6594b --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/studio/services/library/StudioLibraryPublicationHandler.java @@ -0,0 +1,210 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.studio.services.library; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.resource.ResourceSet; +import org.eclipse.sirius.components.core.api.ErrorPayload; +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.components.core.api.IEditingContextSearchService; +import org.eclipse.sirius.components.core.api.IPayload; +import org.eclipse.sirius.components.core.api.SuccessPayload; +import org.eclipse.sirius.components.domain.Domain; +import org.eclipse.sirius.components.emf.ResourceMetadataAdapter; +import org.eclipse.sirius.components.emf.services.JSONResourceFactory; +import org.eclipse.sirius.components.emf.services.api.IEMFEditingContext; +import org.eclipse.sirius.components.events.ICause; +import org.eclipse.sirius.components.representations.Message; +import org.eclipse.sirius.components.representations.MessageLevel; +import org.eclipse.sirius.components.view.RepresentationDescription; +import org.eclipse.sirius.web.application.editingcontext.EditingContext; +import org.eclipse.sirius.web.application.editingcontext.services.DocumentData; +import org.eclipse.sirius.web.application.editingcontext.services.EPackageEntry; +import org.eclipse.sirius.web.application.editingcontext.services.api.IResourceToDocumentService; +import org.eclipse.sirius.web.application.library.dto.PublishLibrariesInput; +import org.eclipse.sirius.web.application.library.services.DependencyGraph; +import org.eclipse.sirius.web.application.library.services.api.ILibraryPublicationHandler; +import org.eclipse.sirius.web.application.studio.services.library.api.IStudioLibraryDependencyCollector; +import org.eclipse.sirius.web.domain.boundedcontexts.library.Library; +import org.eclipse.sirius.web.domain.boundedcontexts.library.services.api.ILibraryCreationService; +import org.eclipse.sirius.web.domain.boundedcontexts.projectsemanticdata.services.api.IProjectSemanticDataSearchService; +import org.eclipse.sirius.web.domain.boundedcontexts.semanticdata.SemanticData; +import org.eclipse.sirius.web.domain.boundedcontexts.semanticdata.services.api.ISemanticDataCreationService; +import org.eclipse.sirius.web.domain.services.IResult; +import org.eclipse.sirius.web.domain.services.Success; +import org.eclipse.sirius.web.domain.services.api.IMessageService; +import org.springframework.data.jdbc.core.mapping.AggregateReference; +import org.springframework.stereotype.Service; + +/** + * Handles the publication of libraries from studios. + * + * @author gdaniel + */ +@Service +public class StudioLibraryPublicationHandler implements ILibraryPublicationHandler { + + private final IEditingContextSearchService editingContextSearchService; + + private final ISemanticDataCreationService semanticDataCreationService; + + private final IResourceToDocumentService resourceToDocumentService; + + private final IStudioLibraryDependencyCollector studioLibraryDependencyCollector; + + private final ILibraryCreationService libraryCreationService; + + private final IProjectSemanticDataSearchService projectSemanticDataSearchService; + + private final IMessageService messageService; + + public StudioLibraryPublicationHandler(IEditingContextSearchService editingContextSearchService, ISemanticDataCreationService semanticDataCreationService, IResourceToDocumentService resourceToDocumentService, IStudioLibraryDependencyCollector studioLibraryDependencyCollector, ILibraryCreationService libraryCreationService, IProjectSemanticDataSearchService projectSemanticDataSearchService, IMessageService messageService) { + this.editingContextSearchService = Objects.requireNonNull(editingContextSearchService); + this.semanticDataCreationService = Objects.requireNonNull(semanticDataCreationService); + this.resourceToDocumentService = Objects.requireNonNull(resourceToDocumentService); + this.studioLibraryDependencyCollector = Objects.requireNonNull(studioLibraryDependencyCollector); + this.libraryCreationService = Objects.requireNonNull(libraryCreationService); + this.projectSemanticDataSearchService = Objects.requireNonNull(projectSemanticDataSearchService); + this.messageService = Objects.requireNonNull(messageService); + } + + @Override + public boolean canHandle(PublishLibrariesInput input) { + return Objects.equals(input.publicationKind(), "studio-all"); + } + + @Override + public IPayload handle(PublishLibrariesInput input) { + IPayload result = new ErrorPayload(input.id(), this.messageService.unexpectedError()); + Optional optionalEditingContext = this.projectSemanticDataSearchService.findByProjectId(AggregateReference.to(input.projectId())) + .flatMap(projectSemanticData -> this.editingContextSearchService.findById(projectSemanticData.getSemanticData().getId().toString())); + + if (optionalEditingContext.isPresent() && optionalEditingContext.get() instanceof EditingContext editingContext) { + ResourceSet rSet = editingContext.getDomain().getResourceSet(); + rSet.getResourceFactoryRegistry().getProtocolToFactoryMap().put(IEMFEditingContext.RESOURCE_SCHEME, new JSONResourceFactory()); + + DependencyGraph dependencyGraph = this.studioLibraryDependencyCollector.collectDependencies(rSet); + + // Compute the topological ordering to ensure that all the dependencies of a library have been created before it. + List libraryCandidates = dependencyGraph.computeTopologicalOrdering(); + + for (EObject libraryCandidate : libraryCandidates) { + if (libraryCandidate instanceof Domain || libraryCandidate instanceof RepresentationDescription) { + Optional optionalLibraryName = this.getUniqueLibraryName(libraryCandidate, libraryCandidates); + if (optionalLibraryName.isPresent()) { + Resource libraryResource = this.getOrCreateLibraryResource(input.projectId(), optionalLibraryName.get(), input.version(), rSet); + libraryResource.getContents().add(libraryCandidate); + } + } else { + Resource sharedComponentsResource = this.getOrCreateLibraryResource(input.projectId(), "shared_components", input.version(), rSet); + sharedComponentsResource.getContents().add(libraryCandidate); + } + } + + + Map createdLibraries = new HashMap<>(); + for (EObject libraryCandidate : libraryCandidates) { + if (!createdLibraries.containsKey(this.getResourceName(libraryCandidate.eResource()))) { + Optional optionalSemanticData = this.toSemanticData(input, libraryCandidate.eResource()); + if (optionalSemanticData.isPresent()) { + String libraryName = this.getResourceName(libraryCandidate.eResource()); + Library library = Library.newLibrary() + .namespace(input.projectId()) + .name(libraryName) + .semanticData(AggregateReference.to(optionalSemanticData.get().getId())) + .dependencies(dependencyGraph.getDependencies(libraryCandidate).stream() + .map(dependency -> createdLibraries.get(this.getResourceName(dependency.eResource())).getId()) + .distinct() + .map(AggregateReference::to) + .toList()) + .version(input.version()) + .description(input.description()) + .build(input); + this.libraryCreationService.createLibrary(library); + createdLibraries.put(libraryName, library); + } + } + } + + result = new SuccessPayload(input.id(), List.of(new Message(createdLibraries.keySet().size() + " libraries published", MessageLevel.SUCCESS))); + } + return result; + } + + + private Optional toSemanticData(ICause event, Resource resource) { + Optional result = Optional.empty(); + Optional optionalDocumentData = this.resourceToDocumentService.toDocument(resource, false); + if (optionalDocumentData.isPresent()) { + DocumentData documentData = optionalDocumentData.get(); + IResult semanticData = this.semanticDataCreationService.create(event, List.of(documentData.document()), documentData.ePackageEntries().stream().map(EPackageEntry::nsURI).toList()); + if (semanticData instanceof Success success) { + result = Optional.ofNullable(success.data()); + } + } + return result; + } + + private Optional getUniqueLibraryName(EObject eObject, List libraryCandidates) { + Optional optLibraryName = this.getLibraryName(eObject); + if (optLibraryName.isPresent()) { + String libraryName = optLibraryName.get(); + List librariesWithSameName = libraryCandidates.stream() + .filter(libraryCandidate -> this.getLibraryName(libraryCandidate).map(name -> Objects.equals(name, libraryName)).orElse(false)) + .toList(); + if (librariesWithSameName.size() > 1) { + optLibraryName = Optional.of(libraryName + librariesWithSameName.indexOf(eObject)); + } + } + return optLibraryName; + } + + private Optional getLibraryName(EObject eObject) { + Optional result = Optional.empty(); + if (eObject instanceof RepresentationDescription representationDescription) { + result = Optional.ofNullable(representationDescription.getName()); + } else if (eObject instanceof Domain domain) { + result = Optional.ofNullable(domain.getName()); + } + return result; + } + + private String getResourceName(Resource resource) { + return resource.eAdapters().stream() + .filter(ResourceMetadataAdapter.class::isInstance) + .map(ResourceMetadataAdapter.class::cast) + .map(ResourceMetadataAdapter::getName) + .findFirst() + .orElse(null); + } + + private Resource getOrCreateLibraryResource(String projectId, String name, String version, ResourceSet rSet) { + String resourceId = projectId + ":" + name + ":" + version; + URI resourceURI = new JSONResourceFactory().createResourceURI(UUID.nameUUIDFromBytes(resourceId.getBytes()).toString()); + Resource resource = rSet.getResource(resourceURI, false); + if (resource == null) { + resource = rSet.createResource(resourceURI); + resource.eAdapters().add(new ResourceMetadataAdapter(name)); + } + return resource; + } +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/studio/services/library/api/IStudioLibraryDependencyCollector.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/studio/services/library/api/IStudioLibraryDependencyCollector.java new file mode 100644 index 0000000000..a4d1589250 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/studio/services/library/api/IStudioLibraryDependencyCollector.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.studio.services.library.api; + +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.resource.ResourceSet; +import org.eclipse.sirius.web.application.library.services.DependencyGraph; + +/** + * Collects the dependencies between studio libraries. + * + * @author gdaniel + */ +public interface IStudioLibraryDependencyCollector { + + DependencyGraph collectDependencies(ResourceSet rSet); + +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/resources/omnibox/publish.svg b/packages/sirius-web/backend/sirius-web-application/src/main/resources/omnibox/publish.svg new file mode 100644 index 0000000000..6df8f591b2 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/resources/omnibox/publish.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/resources/schema/libraries.graphqls b/packages/sirius-web/backend/sirius-web-application/src/main/resources/schema/libraries.graphqls index ed3829a37f..cc4c9705a5 100644 --- a/packages/sirius-web/backend/sirius-web-application/src/main/resources/schema/libraries.graphqls +++ b/packages/sirius-web/backend/sirius-web-application/src/main/resources/schema/libraries.graphqls @@ -20,3 +20,17 @@ type Library { createdOn: Instant! currentEditingContext: EditingContext! } + +extend type Mutation { + publishLibraries(input: PublishLibrariesInput!): PublishLibrariesPayload! +} + +input PublishLibrariesInput { + id: ID! + projectId: ID! + publicationKind: String! + version: String! + description: String! +} + +union PublishLibrariesPayload = ErrorPayload | SuccessPayload diff --git a/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/library/Library.java b/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/library/Library.java index 4b163d941c..2c818ac062 100644 --- a/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/library/Library.java +++ b/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/library/Library.java @@ -13,13 +13,11 @@ package org.eclipse.sirius.web.domain.boundedcontexts.library; import java.time.Instant; +import java.util.ArrayList; import java.util.Collections; -import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; -import java.util.Set; import java.util.UUID; -import java.util.stream.Collectors; import org.eclipse.sirius.components.events.ICause; import org.eclipse.sirius.web.domain.boundedcontexts.AbstractValidatingAggregateRoot; @@ -57,7 +55,7 @@ public class Library extends AbstractValidatingAggregateRoot implements private AggregateReference semanticData; @MappedCollection(idColumn = "library_id", keyColumn = "index") - private Set dependencies = new LinkedHashSet<>(); + private List dependencies = new ArrayList<>(); private String description; @@ -86,8 +84,8 @@ public AggregateReference getSemanticData() { return this.semanticData; } - public Set getDependencies() { - return Collections.unmodifiableSet(this.dependencies); + public List getDependencies() { + return Collections.unmodifiableList(this.dependencies); } public String getDescription() { @@ -127,7 +125,7 @@ public static final class Builder { private AggregateReference semanticData; - private Set dependencies = new LinkedHashSet<>(); + private List dependencies = new ArrayList<>(); private String description; @@ -154,7 +152,7 @@ public Builder semanticData(AggregateReference semanticData) public Builder dependencies(List> dependencies) { this.dependencies = dependencies.stream() .map(LibraryDependency::new) - .collect(Collectors.toSet()); + .toList(); return this; } diff --git a/packages/sirius-web/backend/sirius-web-tests/src/main/java/org/eclipse/sirius/web/tests/graphql/PublishLibrariesMutationRunner.java b/packages/sirius-web/backend/sirius-web-tests/src/main/java/org/eclipse/sirius/web/tests/graphql/PublishLibrariesMutationRunner.java new file mode 100644 index 0000000000..a34cf3a283 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-tests/src/main/java/org/eclipse/sirius/web/tests/graphql/PublishLibrariesMutationRunner.java @@ -0,0 +1,61 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.tests.graphql; + +import java.util.Objects; + +import org.eclipse.sirius.components.graphql.tests.api.IGraphQLRequestor; +import org.eclipse.sirius.components.graphql.tests.api.IMutationRunner; +import org.eclipse.sirius.web.application.library.dto.PublishLibrariesInput; +import org.springframework.stereotype.Service; + +/** + * Used to create a project with the GraphQL API. + * + * @author gdaniel + */ +@Service +public class PublishLibrariesMutationRunner implements IMutationRunner { + + private static final String PUBLISH_LIBRARIES = """ + mutation publishLibraries($input: PublishLibrariesInput!) { + publishLibraries(input: $input) { + __typename + ... on SuccessPayload { + messages { + level + body + } + } + ... on ErrorPayload { + messages { + level + body + } + } + } + } + """; + + private final IGraphQLRequestor graphQLRequestor; + + public PublishLibrariesMutationRunner(IGraphQLRequestor graphQLRequestor) { + this.graphQLRequestor = Objects.requireNonNull(graphQLRequestor); + } + + @Override + public String run(PublishLibrariesInput input) { + return this.graphQLRequestor.execute(PUBLISH_LIBRARIES, input); + } + +} diff --git a/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/libraries/LibraryControllerIntegrationTests.java b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/libraries/LibraryControllerIntegrationTests.java index 23e19a3b74..7ad8f040a4 100644 --- a/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/libraries/LibraryControllerIntegrationTests.java +++ b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/libraries/LibraryControllerIntegrationTests.java @@ -18,14 +18,30 @@ import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.eclipse.sirius.components.core.api.SuccessPayload; import org.eclipse.sirius.web.AbstractIntegrationTests; +import org.eclipse.sirius.web.application.library.dto.PublishLibrariesInput; +import org.eclipse.sirius.web.data.StudioIdentifiers; +import org.eclipse.sirius.web.domain.boundedcontexts.library.Library; +import org.eclipse.sirius.web.domain.boundedcontexts.library.LibraryDependency; +import org.eclipse.sirius.web.domain.boundedcontexts.library.services.api.ILibrarySearchService; +import org.eclipse.sirius.web.papaya.services.library.InitializeStandardLibraryEvent; +import org.eclipse.sirius.web.papaya.services.library.api.IStandardLibrarySemanticDataInitializer; import org.eclipse.sirius.web.tests.data.GivenSiriusWebServer; import org.eclipse.sirius.web.tests.graphql.LibrariesQueryRunner; +import org.eclipse.sirius.web.tests.graphql.PublishLibrariesMutationRunner; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.jdbc.core.mapping.AggregateReference; +import org.springframework.test.context.transaction.TestTransaction; import org.springframework.transaction.annotation.Transactional; /** @@ -40,6 +56,25 @@ public class LibraryControllerIntegrationTests extends AbstractIntegrationTests @Autowired private LibrariesQueryRunner librariesQueryRunner; + @Autowired + private PublishLibrariesMutationRunner publishLibrariesMutationRunner; + + @Autowired + private ILibrarySearchService librarySearchService; + + @Autowired + private IStandardLibrarySemanticDataInitializer standardLibrarySemanticDataInitializer; + + @BeforeEach + public void beforeEach() { + // Re-create the Papaya standard library before each test: it will be deleted by the cleanup script. + var initializeJavaStandardLibraryEvent = new InitializeStandardLibraryEvent(UUID.randomUUID(), "java", "Java Standard Library", "17.0.0", "The standard library of the Java programming language"); + this.standardLibrarySemanticDataInitializer.initializeStandardLibrary(initializeJavaStandardLibraryEvent); + TestTransaction.flagForCommit(); + TestTransaction.end(); + TestTransaction.start(); + } + @Test @GivenSiriusWebServer @DisplayName("Given a set of libraries, when a query is performed, then the libraries are returned") @@ -68,4 +103,59 @@ public void givenSetOfLibrariesWhenQueryIsPerformedThenTheLibrariesAreReturned() .anySatisfy(namespace -> assertThat(namespace).isEqualTo("java")); } + @Test + @GivenSiriusWebServer + @DisplayName("Given a valid studio project ID, when the publication mutation is performed, then the libraries are published") + public void givenValidStudioProjectIdWhenPublicationMutationIsPerformedThenLibrariesArePublished() { + var page = this.librarySearchService.findAll(PageRequest.of(0, 10)); + long initialLibraryCount = page.getTotalElements(); + + String version = "0.0.1"; + String description = "Initial version"; + + var input = new PublishLibrariesInput(UUID.randomUUID(), StudioIdentifiers.SAMPLE_STUDIO_PROJECT, "studio-all", version, description); + var result = this.publishLibrariesMutationRunner.run(input); + + String typename = JsonPath.read(result, "$.data.publishLibraries.__typename"); + assertThat(typename).isEqualTo(SuccessPayload.class.getSimpleName()); + + long updatedLibraryCount = this.librarySearchService.findAll(PageRequest.of(1, 1)).getTotalElements(); + assertThat(updatedLibraryCount).isEqualTo(initialLibraryCount + 6); + + Optional sharedComponentsLibrary = this.librarySearchService.findByNamespaceAndNameAndVersion(StudioIdentifiers.SAMPLE_STUDIO_PROJECT, "shared_components", version); + assertThat(sharedComponentsLibrary).isPresent(); + this.assertThatLibraryHasCorrectDescriptionAndDependencies(sharedComponentsLibrary.get(), description, List.of()); + + Optional buckLibrary = this.librarySearchService.findByNamespaceAndNameAndVersion(StudioIdentifiers.SAMPLE_STUDIO_PROJECT, "buck", version); + assertThat(buckLibrary.isPresent()); + this.assertThatLibraryHasCorrectDescriptionAndDependencies(buckLibrary.get(), description, List.of()); + + Optional humanFormLibrary = this.librarySearchService.findByNamespaceAndNameAndVersion(StudioIdentifiers.SAMPLE_STUDIO_PROJECT, "Human Form", version); + assertThat(humanFormLibrary).isPresent(); + this.assertThatLibraryHasCorrectDescriptionAndDependencies(humanFormLibrary.get(), description, List.of(buckLibrary.get().getId())); + + Optional newTableDescriptionLibrary = this.librarySearchService.findByNamespaceAndNameAndVersion(StudioIdentifiers.SAMPLE_STUDIO_PROJECT, "New Table Description", version); + assertThat(newTableDescriptionLibrary).isPresent(); + this.assertThatLibraryHasCorrectDescriptionAndDependencies(newTableDescriptionLibrary.get(), description, List.of(buckLibrary.get().getId())); + + Optional rootDiagramDescriptionLibrary = this.librarySearchService.findByNamespaceAndNameAndVersion(StudioIdentifiers.SAMPLE_STUDIO_PROJECT, "Root Diagram0", version); + assertThat(rootDiagramDescriptionLibrary).isPresent(); + this.assertThatLibraryHasCorrectDescriptionAndDependencies(rootDiagramDescriptionLibrary.get(), description, List.of(buckLibrary.get().getId(), sharedComponentsLibrary.get().getId())); + + Optional rootDiagram1DescriptionLibrary = this.librarySearchService.findByNamespaceAndNameAndVersion(StudioIdentifiers.SAMPLE_STUDIO_PROJECT, "Root Diagram1", version); + assertThat(rootDiagram1DescriptionLibrary).isPresent(); + this.assertThatLibraryHasCorrectDescriptionAndDependencies(rootDiagram1DescriptionLibrary.get(), description, List.of(buckLibrary.get().getId(), sharedComponentsLibrary.get().getId())); + } + + private void assertThatLibraryHasCorrectDescriptionAndDependencies(Library library, String description, List dependencyIds) { + assertThat(library) + .returns(description, Library::getDescription) + .extracting(Library::getDependencies) + .asInstanceOf(InstanceOfAssertFactories.list(LibraryDependency.class)) + .hasSize(dependencyIds.size()) + .map(LibraryDependency::dependencyLibraryId) + .map(AggregateReference::getId) + .containsAll(dependencyIds); + } + } diff --git a/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/omnibox/OmniboxControllerIntegrationTests.java b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/omnibox/OmniboxControllerIntegrationTests.java index 9ae5fcbe4d..b15427bf1d 100644 --- a/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/omnibox/OmniboxControllerIntegrationTests.java +++ b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/omnibox/OmniboxControllerIntegrationTests.java @@ -33,7 +33,6 @@ import org.eclipse.sirius.components.core.api.SuccessPayload; import org.eclipse.sirius.components.emf.ResourceMetadataAdapter; import org.eclipse.sirius.components.emf.services.api.IEMFEditingContext; -import org.eclipse.sirius.components.graphql.tests.EditingContextEventSubscriptionRunner; import org.eclipse.sirius.components.graphql.tests.ExecuteEditingContextFunctionInput; import org.eclipse.sirius.components.graphql.tests.ExecuteOmniboxCommandMutationRunner; import org.eclipse.sirius.components.graphql.tests.OmniboxCommandsQueryRunner; @@ -70,9 +69,6 @@ public class OmniboxControllerIntegrationTests extends AbstractIntegrationTests @Autowired private OmniboxSearchQueryRunner omniboxSearchQueryRunner; - @Autowired - private EditingContextEventSubscriptionRunner editingContextEventSubscriptionRunner; - @Autowired private ExecuteOmniboxCommandMutationRunner executeOmniboxCommandMutationRunner; @@ -90,7 +86,7 @@ public void givenQueryWhenAQueryIsPerformedThenCommandsAreReturned() { ); var firstQueryResult = this.omniboxCommandsQueryRunner.run(firstQueryVariables); List allCommandLabels = JsonPath.read(firstQueryResult, "$.data.viewer.omniboxCommands.edges[*].node.label"); - assertThat(allCommandLabels).hasSize(1).anyMatch(label -> Objects.equals(label, "Search")); + assertThat(allCommandLabels).hasSize(2).contains("Search", "Publish Studio"); Map secondQueryVariables = Map.of( "editingContextId", StudioIdentifiers.SAMPLE_STUDIO_EDITING_CONTEXT_ID, diff --git a/packages/sirius-web/frontend/sirius-web-application/src/extension/DefaultExtensionRegistry.tsx b/packages/sirius-web/frontend/sirius-web-application/src/extension/DefaultExtensionRegistry.tsx index ee199a6426..9e3e0aad87 100644 --- a/packages/sirius-web/frontend/sirius-web-application/src/extension/DefaultExtensionRegistry.tsx +++ b/packages/sirius-web/frontend/sirius-web-application/src/extension/DefaultExtensionRegistry.tsx @@ -34,7 +34,7 @@ import { widgetContributionExtensionPoint, } from '@eclipse-sirius/sirius-components-forms'; import { GanttRepresentation } from '@eclipse-sirius/sirius-components-gantt'; -import { OmniboxButton } from '@eclipse-sirius/sirius-components-omnibox'; +import { GQLOmniboxCommand, OmniboxButton } from '@eclipse-sirius/sirius-components-omnibox'; import { PortalRepresentation } from '@eclipse-sirius/sirius-components-portals'; import { SelectionDialog } from '@eclipse-sirius/sirius-components-selection'; import { TableRepresentation } from '@eclipse-sirius/sirius-components-tables'; @@ -96,6 +96,9 @@ import { projectSettingsTabExtensionPoint } from '../views/project-settings/Proj import { ellipseNodeStyleDocumentTransform } from './EllipseNodeDocumentTransform'; import { referenceWidgetDocumentTransform } from './ReferenceWidgetDocumentTransform'; import { tableWidgetDocumentTransform } from './TableWidgetDocumentTransform'; +import { OmniboxCommandOverrideContribution } from '@eclipse-sirius/sirius-components-omnibox'; +import { omniboxCommandOverrideContributionExtensionPoint } from '@eclipse-sirius/sirius-components-omnibox'; +import { PublishStudioLibraryCommand } from '../omnibox/PublishStudioLibraryCommand'; const getType = (representation: RepresentationMetadata): string | null => { const query = representation.kind.substring(representation.kind.indexOf('?') + 1, representation.kind.length); @@ -465,4 +468,29 @@ defaultExtensionRegistry.addComponent(projectContextMenuEntryExtensionPoint, { Component: ProjectDownloadMenuItemExtension, }); +/******************************************************************************* + * + * Omnibox command overrides + * + * Used to override the default rendering of omnibox commands + * + *******************************************************************************/ + +const omniboxCommandOverrides: OmniboxCommandOverrideContribution[] = [ + { + canHandle: (action: GQLOmniboxCommand) => { + return action.id === 'publishStudio'; + }, + component: PublishStudioLibraryCommand, + }, +]; + +defaultExtensionRegistry.putData( + omniboxCommandOverrideContributionExtensionPoint, + { + identifier: `siriusweb_${omniboxCommandOverrideContributionExtensionPoint.identifier}`, + data: omniboxCommandOverrides, + } +); + export { defaultExtensionRegistry }; diff --git a/packages/sirius-web/frontend/sirius-web-application/src/extension/DefaultExtensionRegistryMergeStrategy.ts b/packages/sirius-web/frontend/sirius-web-application/src/extension/DefaultExtensionRegistryMergeStrategy.ts index 7d794da635..c861396453 100644 --- a/packages/sirius-web/frontend/sirius-web-application/src/extension/DefaultExtensionRegistryMergeStrategy.ts +++ b/packages/sirius-web/frontend/sirius-web-application/src/extension/DefaultExtensionRegistryMergeStrategy.ts @@ -27,13 +27,27 @@ export class DefaultExtensionRegistryMergeStrategy implements ExtensionRegistryM } public mergeDataExtensions( - _identifier: string, + identifier: string, + existingValue: DataExtension, + newValue: DataExtension + ): DataExtension { + if (identifier === 'omnibox#commandOverrideContribution') { + return this.mergeOmniboxCommandOverrideContributions(existingValue, newValue); + } else { + console.debug( + `The extension with identifier ${existingValue.identifier} has been overwritten by ${newValue.identifier}` + ); + return newValue; + } + } + + private mergeOmniboxCommandOverrideContributions( existingValue: DataExtension, newValue: DataExtension ): DataExtension { - console.debug( - `The extension with identifier ${existingValue.identifier} has been overwritten by ${newValue.identifier}` - ); - return newValue; + return { + identifier: 'siriusweb_omnibox#commandOverrideContribution', + data: [...existingValue.data, ...newValue.data], + }; } } diff --git a/packages/sirius-web/frontend/sirius-web-application/src/index.ts b/packages/sirius-web/frontend/sirius-web-application/src/index.ts index 969c6280d6..f326404208 100644 --- a/packages/sirius-web/frontend/sirius-web-application/src/index.ts +++ b/packages/sirius-web/frontend/sirius-web-application/src/index.ts @@ -62,6 +62,8 @@ export { navigationBarMenuHelpURLExtensionPoint, navigationBarMenuIconExtensionPoint, } from './navigationBar/NavigationBarMenuExtensionPoints'; +export { PublishLibraryDialog } from './omnibox/PublishLibraryDialog'; +export { type PublishLibraryDialogProps } from './omnibox/PublishLibraryDialog.types'; export { routerExtensionPoint } from './router/RouterExtensionPoints'; export type { EditProjectNavbarMenuContainerProps, diff --git a/packages/sirius-web/frontend/sirius-web-application/src/omnibox/PublishLibraryDialog.tsx b/packages/sirius-web/frontend/sirius-web-application/src/omnibox/PublishLibraryDialog.tsx new file mode 100644 index 0000000000..d7a9d5b967 --- /dev/null +++ b/packages/sirius-web/frontend/sirius-web-application/src/omnibox/PublishLibraryDialog.tsx @@ -0,0 +1,122 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +import DialogContentText from '@mui/material/DialogContentText'; +import DialogContent from '@mui/material/DialogContent'; +import DialogTitle from '@mui/material/DialogTitle'; +import DialogActions from '@mui/material/DialogActions'; +import Button from '@mui/material/Button'; +import TextField from '@mui/material/TextField'; +import { makeStyles } from 'tss-react/mui'; +import { useState } from 'react'; +import { usePublishLibraries } from './usePublishLibraries'; +import { useCurrentProject } from '../views/edit-project/useCurrentProject'; +import Link from '@mui/material/Link'; +import Dialog from '@mui/material/Dialog'; +import { PublishLibraryDialogProps, PublishLibraryDialogState } from './PublishLibraryDialog.types'; +import { Link as RouterLink } from 'react-router-dom'; + +const usePublishCommandStyles = makeStyles()((theme) => ({ + form: { + display: 'flex', + flexDirection: 'column', + '& > *': { + marginBottom: theme.spacing(1), + }, + }, + link: { + fontStyle: 'italic', + }, +})); + +export const PublishLibraryDialog = ({ open, title, message, publicationKind, onClose }: PublishLibraryDialogProps) => { + const { classes } = usePublishCommandStyles(); + + const [state, setState] = useState({ + version: '', + versionIsInvalid: true, + pristine: true, + description: '', + }); + + const onVersionChange: React.ChangeEventHandler = (event) => { + const value = event.target.value; + const versionIsInvalid = value.trim().length === 0; + setState((prevState) => ({ ...prevState, version: value, versionIsInvalid, pristine: false })); + }; + + const onDescriptionChange: React.ChangeEventHandler = (event) => { + const value = event.target.value; + setState((prevState) => ({ ...prevState, description: value.toString() })); + }; + + const { publishLibraries } = usePublishLibraries(); + + const { project } = useCurrentProject(); + + const handlePublish = () => { + publishLibraries(project.id, publicationKind, state.version, state.description); + onClose(); + }; + + return ( + + {title} + + {message} +
+ + + See published libraries + + +
+
+ + + +
+ ); +}; diff --git a/packages/sirius-web/frontend/sirius-web-application/src/omnibox/PublishLibraryDialog.types.ts b/packages/sirius-web/frontend/sirius-web-application/src/omnibox/PublishLibraryDialog.types.ts new file mode 100644 index 0000000000..771b080530 --- /dev/null +++ b/packages/sirius-web/frontend/sirius-web-application/src/omnibox/PublishLibraryDialog.types.ts @@ -0,0 +1,27 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ + +export interface PublishLibraryDialogProps { + open: boolean; + title: string; + message: string; + publicationKind: string; + onClose: () => void; +} + +export interface PublishLibraryDialogState { + version: string; + versionIsInvalid: boolean; + pristine: boolean; + description: string; +} diff --git a/packages/sirius-web/frontend/sirius-web-application/src/omnibox/PublishStudioLibraryCommand.tsx b/packages/sirius-web/frontend/sirius-web-application/src/omnibox/PublishStudioLibraryCommand.tsx new file mode 100644 index 0000000000..f99d304866 --- /dev/null +++ b/packages/sirius-web/frontend/sirius-web-application/src/omnibox/PublishStudioLibraryCommand.tsx @@ -0,0 +1,49 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ + +import { OmniboxCommandComponentProps } from '@eclipse-sirius/sirius-components-omnibox'; +import { useState } from 'react'; +import { PublishLibraryCommandState } from './PublishStudioLibraryCommand.types'; +import { PublishLibraryDialog } from './PublishLibraryDialog'; +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import { IconOverlay } from '@eclipse-sirius/sirius-components-core'; + +export const PublishStudioLibraryCommand = ({ command, onKeyDown, onClose }: OmniboxCommandComponentProps) => { + const [state, setState] = useState({ + open: false, + }); + + const handleClick = () => setState((prevState) => ({ ...prevState, open: true })); + + return ( + <> + + + + + {command.label} + + {state.open && ( + + )} + + ); +}; diff --git a/packages/sirius-web/frontend/sirius-web-application/src/omnibox/PublishStudioLibraryCommand.types.ts b/packages/sirius-web/frontend/sirius-web-application/src/omnibox/PublishStudioLibraryCommand.types.ts new file mode 100644 index 0000000000..a7c02bbe46 --- /dev/null +++ b/packages/sirius-web/frontend/sirius-web-application/src/omnibox/PublishStudioLibraryCommand.types.ts @@ -0,0 +1,16 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ + +export interface PublishLibraryCommandState { + open: boolean; +} diff --git a/packages/sirius-web/frontend/sirius-web-application/src/omnibox/usePublishLibraries.ts b/packages/sirius-web/frontend/sirius-web-application/src/omnibox/usePublishLibraries.ts new file mode 100644 index 0000000000..bfa0b9f33a --- /dev/null +++ b/packages/sirius-web/frontend/sirius-web-application/src/omnibox/usePublishLibraries.ts @@ -0,0 +1,89 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ + +import { gql, useMutation } from '@apollo/client'; +import { useMultiToast } from '@eclipse-sirius/sirius-components-core'; +import { + GQLErrorPayload, + GQLPublishLibrariesMutationData, + GQLPublishLibrariesMutationVariables, + GQLPublishLibrariesPayload, + GQLSuccessPayload, + UsePublishLibrariesValue, +} from './usePublishLibraries.types'; + +const publishLibrariesMutation = gql` + mutation publishLibraries($input: PublishLibrariesInput!) { + publishLibraries(input: $input) { + __typename + ... on SuccessPayload { + messages { + level + body + } + } + ... on ErrorPayload { + messages { + level + body + } + } + } + } +`; + +const isSuccessPayload = (payload: GQLPublishLibrariesPayload): payload is GQLSuccessPayload => + payload.__typename === 'SuccessPayload'; + +const isErrorPayload = (payload: GQLPublishLibrariesPayload): payload is GQLErrorPayload => + payload.__typename === 'ErrorPayload'; + +export const usePublishLibraries = (): UsePublishLibrariesValue => { + const { addErrorMessage, addMessages } = useMultiToast(); + const [performPublishLibraries, { loading, data }] = useMutation< + GQLPublishLibrariesMutationData, + GQLPublishLibrariesMutationVariables + >(publishLibrariesMutation, { + onCompleted: (data) => { + const { publishLibraries } = data; + if (isErrorPayload(publishLibraries)) { + addMessages(publishLibraries.messages); + } + if (isSuccessPayload(publishLibraries)) { + addMessages(publishLibraries.messages); + } + }, + onError: () => { + addErrorMessage('An unexpected error has occurred, please refresh the page'); + }, + }); + + const publishLibraries = (projectId: string, publicationKind: string, version: string, description: string) => { + const variables: GQLPublishLibrariesMutationVariables = { + input: { + id: crypto.randomUUID(), + projectId, + publicationKind, + version, + description, + }, + }; + performPublishLibraries({ variables }); + }; + + return { + publishLibraries, + loading, + commandData: data?.publishLibraries ?? null, + }; +}; diff --git a/packages/sirius-web/frontend/sirius-web-application/src/omnibox/usePublishLibraries.types.ts b/packages/sirius-web/frontend/sirius-web-application/src/omnibox/usePublishLibraries.types.ts new file mode 100644 index 0000000000..96198c8a3e --- /dev/null +++ b/packages/sirius-web/frontend/sirius-web-application/src/omnibox/usePublishLibraries.types.ts @@ -0,0 +1,48 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ + +import { GQLMessage } from '@eclipse-sirius/sirius-components-core'; + +export interface UsePublishLibrariesValue { + publishLibraries: (projectId: string, publicationKind: string, version: string, description: string) => void; + loading: boolean; + commandData: GQLPublishLibrariesPayload | null; +} + +export interface GQLPublishLibrariesMutationVariables { + input: GQLPublishLibrariesMutationInput; +} + +export interface GQLPublishLibrariesMutationInput { + id: string; + projectId: string; + publicationKind: string; + version: string; + description: string; +} + +export interface GQLPublishLibrariesMutationData { + publishLibraries: GQLPublishLibrariesPayload; +} + +export interface GQLPublishLibrariesPayload { + __typename: string; +} + +export interface GQLSuccessPayload extends GQLPublishLibrariesPayload { + messages: GQLMessage[]; +} + +export interface GQLErrorPayload extends GQLPublishLibrariesPayload { + messages: GQLMessage[]; +} diff --git a/packages/sirius-web/frontend/sirius-web-papaya/src/PapayaExtensionRegistry.tsx b/packages/sirius-web/frontend/sirius-web-papaya/src/PapayaExtensionRegistry.tsx index 39278f18b2..868bf33653 100644 --- a/packages/sirius-web/frontend/sirius-web-papaya/src/PapayaExtensionRegistry.tsx +++ b/packages/sirius-web/frontend/sirius-web-papaya/src/PapayaExtensionRegistry.tsx @@ -11,7 +11,12 @@ * Obeo - initial API and implementation *******************************************************************************/ -import { ComponentExtension, DataExtension, ExtensionRegistry } from '@eclipse-sirius/sirius-components-core'; +import { + ComponentExtension, + DataExtension, + ExtensionRegistry, + IconOverlay, +} from '@eclipse-sirius/sirius-components-core'; import { DiagramPaletteToolContributionProps, EdgeData, @@ -21,7 +26,7 @@ import { diagramRendererReactFlowPropsCustomizerExtensionPoint, } from '@eclipse-sirius/sirius-components-diagrams'; import { - OmniboxAction, + GQLOmniboxCommand, OmniboxCommandOverrideContribution, omniboxCommandOverrideContributionExtensionPoint, } from '@eclipse-sirius/sirius-components-omnibox'; @@ -36,6 +41,10 @@ import { PapayaDiagramInformationPanel } from './diagrams/PapayaDiagramInformati import { PapayaDiagramLegendPanel } from './diagrams/PapayaDiagramLegendPanel'; import { PapayaComponentDiagramToolContribution } from './tools/PapayaComponentDiagramToolContribution'; import { PapayaComponentLabelDetailToolContribution } from './tools/PapayaComponentLabelDetailToolContribution'; +import { OmniboxCommandComponentProps } from '@eclipse-sirius/sirius-components-omnibox'; +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; const papayaExtensionRegistry = new ExtensionRegistry(); @@ -112,15 +121,28 @@ papayaExtensionRegistry.putData(diagramPa * Used to override the default rendering of omnibox commands * *******************************************************************************/ +const ShowDocumentationCommand = ({ command, onKeyDown, onClose }: OmniboxCommandComponentProps) => { + const handleClick = () => { + window.open('https://www.github.com/eclipse-sirius/sirius-web', '_blank')?.focus(); + onClose(); + }; + + return ( + + + + + {command.label} + + ); +}; const omniboxCommandOverrides: OmniboxCommandOverrideContribution[] = [ { - canHandle: (action: OmniboxAction) => { - return action.id === 'showDocumentation'; - }, - handle: (_action: OmniboxAction) => { - window.open('https://www.github.com/eclipse-sirius/sirius-web', '_blank')?.focus(); + canHandle: (command: GQLOmniboxCommand) => { + return command.id === 'showDocumentation'; }, + component: ShowDocumentationCommand, }, ];