diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 0368236992..d314d36d9b 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -36,6 +36,7 @@ The reference widget graphql schema and the frontend have been updated according - https://github.com/eclipse-sirius/sirius-web/issues/3010[#3010] [forms] Reference widget has been updated. Its body operations are now bind on set/add instead of click. - https://github.com/eclipse-sirius/sirius-web/issues/2942[#2942] [deck] Add the Card Drag and Drop capability in the Deck representation. +- https://github.com/eclipse-sirius/sirius-web/issues/3015[#3015] [deck] Add the Lane Selection and Direct edit capability. === Improvements diff --git a/packages/deck/backend/sirius-components-collaborative-deck/src/main/java/org/eclipse/sirius/components/collaborative/deck/api/IDeckLaneService.java b/packages/deck/backend/sirius-components-collaborative-deck/src/main/java/org/eclipse/sirius/components/collaborative/deck/api/IDeckLaneService.java new file mode 100644 index 0000000000..642b8623d0 --- /dev/null +++ b/packages/deck/backend/sirius-components-collaborative-deck/src/main/java/org/eclipse/sirius/components/collaborative/deck/api/IDeckLaneService.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * Copyright (c) 2024 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.components.collaborative.deck.api; + +import org.eclipse.sirius.components.collaborative.deck.dto.input.EditDeckLaneInput; +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.components.core.api.IPayload; +import org.eclipse.sirius.components.deck.Deck; + +/** + * Service used to manage deck lane. + * + * @author fbarbin + */ +public interface IDeckLaneService { + + IPayload editLane(EditDeckLaneInput editDeckLaneInput, IEditingContext editingContext, Deck deck); + + /** + * Implementation which does nothing, used for mocks in unit tests. + * + * @author fbarbin + */ + class NoOp implements IDeckLaneService { + + @Override + public IPayload editLane(EditDeckLaneInput editDeckLaneInput, IEditingContext editingContext, Deck deck) { + return null; + } + } + +} diff --git a/packages/deck/backend/sirius-components-collaborative-deck/src/main/java/org/eclipse/sirius/components/collaborative/deck/dto/input/EditDeckLaneInput.java b/packages/deck/backend/sirius-components-collaborative-deck/src/main/java/org/eclipse/sirius/components/collaborative/deck/dto/input/EditDeckLaneInput.java new file mode 100644 index 0000000000..21e1bbca58 --- /dev/null +++ b/packages/deck/backend/sirius-components-collaborative-deck/src/main/java/org/eclipse/sirius/components/collaborative/deck/dto/input/EditDeckLaneInput.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * Copyright (c) 2024 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.components.collaborative.deck.dto.input; + +import java.util.UUID; + +import org.eclipse.sirius.components.collaborative.deck.api.IDeckInput; + +/** + * The input of the "Edit lane" mutation. + * + * @author fbarbin + */ +public record EditDeckLaneInput(UUID id, String editingContextId, String representationId, String laneId, String newTitle, String newLabel, String newDescription) + implements IDeckInput { +} diff --git a/packages/deck/backend/sirius-components-collaborative-deck/src/main/java/org/eclipse/sirius/components/collaborative/deck/handlers/EditLaneEventHandler.java b/packages/deck/backend/sirius-components-collaborative-deck/src/main/java/org/eclipse/sirius/components/collaborative/deck/handlers/EditLaneEventHandler.java new file mode 100644 index 0000000000..eea0e7030b --- /dev/null +++ b/packages/deck/backend/sirius-components-collaborative-deck/src/main/java/org/eclipse/sirius/components/collaborative/deck/handlers/EditLaneEventHandler.java @@ -0,0 +1,81 @@ +/******************************************************************************* + * Copyright (c) 2024 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.components.collaborative.deck.handlers; + +import java.util.Objects; + +import org.eclipse.sirius.components.collaborative.api.ChangeDescription; +import org.eclipse.sirius.components.collaborative.api.ChangeKind; +import org.eclipse.sirius.components.collaborative.api.Monitoring; +import org.eclipse.sirius.components.collaborative.deck.api.IDeckEventHandler; +import org.eclipse.sirius.components.collaborative.deck.api.IDeckInput; +import org.eclipse.sirius.components.collaborative.deck.api.IDeckLaneService; +import org.eclipse.sirius.components.collaborative.deck.dto.input.EditDeckLaneInput; +import org.eclipse.sirius.components.collaborative.deck.message.ICollaborativeDeckMessageService; +import org.eclipse.sirius.components.core.api.ErrorPayload; +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.components.core.api.IPayload; +import org.eclipse.sirius.components.deck.Deck; +import org.springframework.stereotype.Service; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import reactor.core.publisher.Sinks.Many; +import reactor.core.publisher.Sinks.One; + +/** + * Handle "Edit Lane" events. + * + * @author fbarbin + */ +@Service +public class EditLaneEventHandler implements IDeckEventHandler { + + private final IDeckLaneService deckLaneService; + + private final ICollaborativeDeckMessageService messageService; + + private final Counter counter; + + public EditLaneEventHandler(IDeckLaneService deckLaneService, ICollaborativeDeckMessageService messageService, MeterRegistry meterRegistry) { + this.messageService = Objects.requireNonNull(messageService); + this.deckLaneService = Objects.requireNonNull(deckLaneService); + + this.counter = Counter.builder(Monitoring.EVENT_HANDLER) + .tag(Monitoring.NAME, this.getClass().getSimpleName()) + .register(meterRegistry); + } + + @Override + public boolean canHandle(IDeckInput deckInput) { + return deckInput instanceof EditDeckLaneInput; + } + + @Override + public void handle(One payloadSink, Many changeDescriptionSink, IEditingContext editingContext, Deck deck, IDeckInput deckInput) { + this.counter.increment(); + + String message = this.messageService.invalidInput(deckInput.getClass().getSimpleName(), EditDeckLaneInput.class.getSimpleName()); + IPayload payload = new ErrorPayload(deckInput.id(), message); + ChangeDescription changeDescription = new ChangeDescription(ChangeKind.NOTHING, deckInput.representationId(), deckInput); + + if (deckInput instanceof EditDeckLaneInput input) { + payload = this.deckLaneService.editLane(input, editingContext, deck); + + changeDescription = new ChangeDescription(ChangeKind.SEMANTIC_CHANGE, deckInput.representationId(), deckInput); + } + + payloadSink.tryEmitValue(payload); + changeDescriptionSink.tryEmitNext(changeDescription); + } +} diff --git a/packages/deck/backend/sirius-components-collaborative-deck/src/main/java/org/eclipse/sirius/components/collaborative/deck/service/DeckLaneService.java b/packages/deck/backend/sirius-components-collaborative-deck/src/main/java/org/eclipse/sirius/components/collaborative/deck/service/DeckLaneService.java new file mode 100644 index 0000000000..1b0cf77e5f --- /dev/null +++ b/packages/deck/backend/sirius-components-collaborative-deck/src/main/java/org/eclipse/sirius/components/collaborative/deck/service/DeckLaneService.java @@ -0,0 +1,116 @@ +/******************************************************************************* + * Copyright (c) 2024 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.components.collaborative.deck.service; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.function.Predicate; + +import org.eclipse.sirius.components.collaborative.deck.api.IDeckLaneService; +import org.eclipse.sirius.components.collaborative.deck.dto.input.EditDeckLaneInput; +import org.eclipse.sirius.components.core.api.ErrorPayload; +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.components.core.api.IFeedbackMessageService; +import org.eclipse.sirius.components.core.api.IObjectService; +import org.eclipse.sirius.components.core.api.IPayload; +import org.eclipse.sirius.components.core.api.IRepresentationDescriptionSearchService; +import org.eclipse.sirius.components.core.api.SuccessPayload; +import org.eclipse.sirius.components.deck.Deck; +import org.eclipse.sirius.components.deck.Lane; +import org.eclipse.sirius.components.deck.description.DeckDescription; +import org.eclipse.sirius.components.deck.description.LaneDescription; +import org.eclipse.sirius.components.representations.Message; +import org.eclipse.sirius.components.representations.MessageLevel; +import org.eclipse.sirius.components.representations.VariableManager; +import org.springframework.stereotype.Service; + +/** + * Service used to manage lanes. + * + * @author fbarbin + */ +@Service +public class DeckLaneService implements IDeckLaneService { + + private final IRepresentationDescriptionSearchService representationDescriptionSearchService; + + private final IFeedbackMessageService feedbackMessageService; + + private final IObjectService objectService; + + public DeckLaneService(IRepresentationDescriptionSearchService representationDescriptionSearchService, IFeedbackMessageService feedbackMessageService, IObjectService objectService) { + this.representationDescriptionSearchService = Objects.requireNonNull(representationDescriptionSearchService); + this.feedbackMessageService = Objects.requireNonNull(feedbackMessageService); + this.objectService = Objects.requireNonNull(objectService); + } + + + + @Override + public IPayload editLane(EditDeckLaneInput editDeckLaneInput, IEditingContext editingContext, Deck deck) { + IPayload payload = new ErrorPayload(editDeckLaneInput.id(), "Edit lane failed"); + + Optional optionalLane = this.findLane(lane -> Objects.equals(lane.id(), editDeckLaneInput.laneId()), deck); + Optional optionalLaneDescription = optionalLane.flatMap(lane -> this.findLaneDescription(lane.descriptionId(), deck, editingContext)); + + if (optionalLane.isPresent() && optionalLaneDescription.isPresent()) { + Optional optionalTargetObject = this.objectService.getObject(editingContext, optionalLane.get().targetObjectId()); + if (optionalTargetObject.isPresent()) { + VariableManager variableManager = new VariableManager(); + variableManager.put(VariableManager.SELF, optionalTargetObject.get()); + variableManager.put(DeckDescription.NEW_TITLE, editDeckLaneInput.newTitle()); + optionalLaneDescription.get().editLaneProvider().accept(variableManager); + + payload = this.getPayload(editDeckLaneInput.id()); + } + } + + return payload; + } + + private IPayload getPayload(UUID payloadId) { + IPayload payload = null; + List feedbackMessages = this.feedbackMessageService.getFeedbackMessages(); + Optional optionalErrorMsg = feedbackMessages.stream().filter(msg -> MessageLevel.ERROR.equals(msg.level())).findFirst(); + if (optionalErrorMsg.isPresent()) { + payload = new ErrorPayload(payloadId, optionalErrorMsg.get().body(), feedbackMessages); + } else { + payload = new SuccessPayload(payloadId, feedbackMessages); + } + return payload; + } + private Optional findLane(Predicate condition, Deck deck) { + return deck.lanes().stream() + .filter(condition) + .findFirst(); + } + + private Optional findDeckDescription(String descriptionId, IEditingContext editingContext) { + return this.representationDescriptionSearchService.findById(editingContext, descriptionId) + .filter(DeckDescription.class::isInstance) + .map(DeckDescription.class::cast); + } + + private Optional findLaneDescription(String descriptionId, Deck deck, IEditingContext editingContext) { + return this.findDeckDescription(deck.descriptionId(), editingContext) + .stream() + .map(DeckDescription::laneDescriptions) + .flatMap(Collection::stream) + .filter(laneDesc -> laneDesc.id().equals(descriptionId)) + .findFirst(); + } + +} diff --git a/packages/deck/backend/sirius-components-collaborative-deck/src/main/resources/schema/deck.graphqls b/packages/deck/backend/sirius-components-collaborative-deck/src/main/resources/schema/deck.graphqls index 8073875f91..e44484ce90 100644 --- a/packages/deck/backend/sirius-components-collaborative-deck/src/main/resources/schema/deck.graphqls +++ b/packages/deck/backend/sirius-components-collaborative-deck/src/main/resources/schema/deck.graphqls @@ -47,6 +47,7 @@ extend type Mutation { deleteDeckCard(input: DeleteDeckCardInput!): DeleteDeckCardPayload editDeckCard(input: EditDeckCardInput!): EditDeckCardPayload dropDeckCard(input: DropDeckCardInput!): DropDeckCardPayload + editDeckLane(input: EditDeckLaneInput!): EditDeckLanePayload } input CreateDeckCardInput { @@ -92,3 +93,12 @@ input DropDeckCardInput { } union DropDeckCardPayload = SuccessPayload | ErrorPayload + +input EditDeckLaneInput { + id: ID! + editingContextId: ID! + representationId: ID! + laneId: ID! + newTitle: String! +} +union EditDeckLanePayload = SuccessPayload | ErrorPayload diff --git a/packages/deck/backend/sirius-components-deck-graphql/src/main/java/org/eclipse/sirius/components/deck/graphql/datafetchers/mutation/MutationEditLaneDataFetcher.java b/packages/deck/backend/sirius-components-deck-graphql/src/main/java/org/eclipse/sirius/components/deck/graphql/datafetchers/mutation/MutationEditLaneDataFetcher.java new file mode 100644 index 0000000000..e3aee62c5b --- /dev/null +++ b/packages/deck/backend/sirius-components-deck-graphql/src/main/java/org/eclipse/sirius/components/deck/graphql/datafetchers/mutation/MutationEditLaneDataFetcher.java @@ -0,0 +1,59 @@ +/******************************************************************************* + * Copyright (c) 2024 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.components.deck.graphql.datafetchers.mutation; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.sirius.components.annotations.spring.graphql.MutationDataFetcher; +import org.eclipse.sirius.components.collaborative.deck.dto.input.EditDeckLaneInput; +import org.eclipse.sirius.components.core.api.IPayload; +import org.eclipse.sirius.components.graphql.api.IDataFetcherWithFieldCoordinates; +import org.eclipse.sirius.components.graphql.api.IEditingContextDispatcher; +import org.eclipse.sirius.components.graphql.api.IExceptionWrapper; + +import graphql.schema.DataFetchingEnvironment; + +/** + * The data fetcher used to change a Deck lane. + * + * @author fbarbin + */ +@MutationDataFetcher(type = "Mutation", field = "editDeckLane") +public class MutationEditLaneDataFetcher implements IDataFetcherWithFieldCoordinates> { + + private static final String INPUT_ARGUMENT = "input"; + + private final ObjectMapper objectMapper; + + private final IExceptionWrapper exceptionWrapper; + + private final IEditingContextDispatcher editingContextDispatcher; + + public MutationEditLaneDataFetcher(ObjectMapper objectMapper, IExceptionWrapper exceptionWrapper, IEditingContextDispatcher editingContextDispatcher) { + this.objectMapper = Objects.requireNonNull(objectMapper); + this.exceptionWrapper = Objects.requireNonNull(exceptionWrapper); + this.editingContextDispatcher = Objects.requireNonNull(editingContextDispatcher); + } + + @Override + public CompletableFuture get(DataFetchingEnvironment environment) throws Exception { + Object argument = environment.getArgument(INPUT_ARGUMENT); + var input = this.objectMapper.convertValue(argument, EditDeckLaneInput.class); + + return this.exceptionWrapper.wrapMono(() -> this.editingContextDispatcher.dispatchMutation(input.editingContextId(), input), input).toFuture(); + } + +} diff --git a/packages/deck/frontend/sirius-components-deck/src/Deck.tsx b/packages/deck/frontend/sirius-components-deck/src/Deck.tsx index 075e0d0959..46d9280a34 100644 --- a/packages/deck/frontend/sirius-components-deck/src/Deck.tsx +++ b/packages/deck/frontend/sirius-components-deck/src/Deck.tsx @@ -14,6 +14,7 @@ import Board from '@ObeoNetwork/react-trello'; import { Theme, useTheme } from '@material-ui/core/styles'; import { DeckProps } from './Deck.types'; import { DeckCard } from './card/DeckCard'; +import { DeckLaneHeader } from './laneHeader/DeckLaneHeader'; import { Toolbar } from './toolbar/Toolbar'; export const Deck = ({ @@ -21,10 +22,12 @@ export const Deck = ({ representationId, data, onCardClick, + onLaneClick, onCardDelete, onCardAdd, onCardUpdate, onCardMoveAcrossLanes, + onLaneUpdate, }: DeckProps) => { const theme: Theme = useTheme(); const boardStyle = { @@ -32,6 +35,7 @@ export const Deck = ({ }; const components = { Card: DeckCard, + LaneHeader: DeckLaneHeader, }; return (
@@ -40,10 +44,12 @@ export const Deck = ({ data={data} draggable={true} onCardClick={onCardClick} + onLaneClick={onLaneClick} components={components} onCardDelete={onCardDelete} onCardAdd={onCardAdd} onCardUpdate={onCardUpdate} + onLaneUpdate={onLaneUpdate} onCardMoveAcrossLanes={onCardMoveAcrossLanes} data-testid={`deck-representation`} style={boardStyle} diff --git a/packages/deck/frontend/sirius-components-deck/src/Deck.types.ts b/packages/deck/frontend/sirius-components-deck/src/Deck.types.ts index c3ddba7001..8bc0354685 100644 --- a/packages/deck/frontend/sirius-components-deck/src/Deck.types.ts +++ b/packages/deck/frontend/sirius-components-deck/src/Deck.types.ts @@ -12,16 +12,19 @@ *******************************************************************************/ import { SelectionEntry } from '@eclipse-sirius/sirius-components-core'; +import { CSSProperties } from 'react'; export interface DeckProps { editingContextId: string; representationId: string; data: DeckData; onCardClick: (cardId: string, metadata: CardMetadata, laneId: string) => void; + onLaneClick: (laneId: string) => void; onCardUpdate: (laneId: string, card: Card) => void; onCardAdd: (card: Card, laneId: string) => void; onCardDelete: (cardId: string, laneId: string) => void; onCardMoveAcrossLanes: (oldLaneId: string, newLaneId: string, cardId: string, addedIndex: number) => void; + onLaneUpdate: (laneId: string, newValue: { title: string }) => void; } export interface OnCardClickProps { cardId: String; @@ -37,7 +40,9 @@ export interface Lane { title: string; label: string; cards: Card[]; + editLaneTitle?: boolean; editable: boolean; + style?: CSSProperties; 'data-testid': string; } export interface Card { diff --git a/packages/deck/frontend/sirius-components-deck/src/card/DeckCard.tsx b/packages/deck/frontend/sirius-components-deck/src/card/DeckCard.tsx index 7b68fbf57c..dcda292226 100644 --- a/packages/deck/frontend/sirius-components-deck/src/card/DeckCard.tsx +++ b/packages/deck/frontend/sirius-components-deck/src/card/DeckCard.tsx @@ -11,22 +11,21 @@ * Obeo - initial API and implementation *******************************************************************************/ -import { useTheme } from '@material-ui/core/styles'; +import { Theme, useTheme } from '@material-ui/core/styles'; import { useRef } from 'react'; import { Card } from '../Deck.types'; -import { DeckCardProps } from './DeckCard.types'; -import { DeckCardInput } from './DeckCardInput'; +import { DeckInput } from '../common/DeckInput'; import { DeckCardHeader, DeckCardRightContent, - DeckCardTitle, DeckDraggableCardWrapper, Detail, Footer, cardDetailFontStyle, cardLabelFontStyle, - cardTitleFontStyle, -} from './DeckCardStyledComponents'; +} from '../styled/DeckCardStyledComponents'; +import { DeckTitle, titleFontStyle } from '../styled/DeckStyledComponents'; +import { DeckCardProps } from './DeckCard.types'; import { DeckDeleteButton } from './DeckDeleteButton'; import { DeckTag } from './DeckTag'; @@ -59,7 +58,7 @@ export const DeckCard = ({ e.stopPropagation(); }; - const theme = useTheme(); + const theme: Theme = useTheme(); const cardStyle: React.CSSProperties = { border: editable ? `2px solid ${theme.palette.selected}` : undefined, @@ -86,23 +85,23 @@ export const DeckCard = ({ data-testid={`card-${title}`} onDragStart={(e) => e.preventDefault()}> - + {editable ? ( - updateCard({ title: value, label, description, id })} - style={cardTitleFontStyle} + style={titleFontStyle} data-testid={'card-input-title'} /> ) : ( title )} - + {editable ? ( - updateCard({ title, label: value, description, id })} @@ -117,7 +116,7 @@ export const DeckCard = ({ {editable ? ( - updateCard({ title, label, description: value, id })} diff --git a/packages/deck/frontend/sirius-components-deck/src/card/DeckDeleteButton.tsx b/packages/deck/frontend/sirius-components-deck/src/card/DeckDeleteButton.tsx index 4152cbf79e..90cf65a9fb 100644 --- a/packages/deck/frontend/sirius-components-deck/src/card/DeckDeleteButton.tsx +++ b/packages/deck/frontend/sirius-components-deck/src/card/DeckDeleteButton.tsx @@ -12,7 +12,7 @@ *******************************************************************************/ import DeleteIcon from '@material-ui/icons/Delete'; -import { CardDeleteIconButton } from './DeckCardStyledComponents'; +import { CardDeleteIconButton } from '../styled/DeckCardStyledComponents'; export const DeckDeleteButton = (props) => { return ( diff --git a/packages/deck/frontend/sirius-components-deck/src/card/DeckTag.tsx b/packages/deck/frontend/sirius-components-deck/src/card/DeckTag.tsx index a6ea0ec2a5..a3f531b81d 100644 --- a/packages/deck/frontend/sirius-components-deck/src/card/DeckTag.tsx +++ b/packages/deck/frontend/sirius-components-deck/src/card/DeckTag.tsx @@ -10,7 +10,7 @@ * Contributors: * Obeo - initial API and implementation *******************************************************************************/ -import { TagSpan } from './DeckCardStyledComponents'; +import { TagSpan } from '../styled/DeckCardStyledComponents'; import { DeckTagProps } from './DeckTag.types'; export const DeckTag = ({ title, color, bgcolor, tagStyle }: DeckTagProps) => { diff --git a/packages/deck/frontend/sirius-components-deck/src/card/DeckCardInput.tsx b/packages/deck/frontend/sirius-components-deck/src/common/DeckInput.tsx similarity index 97% rename from packages/deck/frontend/sirius-components-deck/src/card/DeckCardInput.tsx rename to packages/deck/frontend/sirius-components-deck/src/common/DeckInput.tsx index b03eed23f1..fc3055144a 100644 --- a/packages/deck/frontend/sirius-components-deck/src/card/DeckCardInput.tsx +++ b/packages/deck/frontend/sirius-components-deck/src/common/DeckInput.tsx @@ -14,12 +14,12 @@ import TextField from '@material-ui/core/TextField'; import { styled } from '@material-ui/core/styles'; import { forwardRef, useImperativeHandle, useRef } from 'react'; -import { DeckCardInputProps } from './DeckCardInput.types'; +import { DeckCardInputProps } from './DeckInput.types'; /** * Inspired from react-trello InlineInput component. */ -export const DeckCardInput = forwardRef( +export const DeckInput = forwardRef( ({ value, placeholder, onSave, style, multiline, ...otherProps }: DeckCardInputProps, ref) => { const StyledTextField = styled(TextField)(({ theme }) => ({ '& .MuiInputBase-multiline': { diff --git a/packages/deck/frontend/sirius-components-deck/src/card/DeckCardInput.types.ts b/packages/deck/frontend/sirius-components-deck/src/common/DeckInput.types.ts similarity index 100% rename from packages/deck/frontend/sirius-components-deck/src/card/DeckCardInput.types.ts rename to packages/deck/frontend/sirius-components-deck/src/common/DeckInput.types.ts diff --git a/packages/deck/frontend/sirius-components-deck/src/laneHeader/DeckLaneHeader.tsx b/packages/deck/frontend/sirius-components-deck/src/laneHeader/DeckLaneHeader.tsx new file mode 100644 index 0000000000..160e57371f --- /dev/null +++ b/packages/deck/frontend/sirius-components-deck/src/laneHeader/DeckLaneHeader.tsx @@ -0,0 +1,61 @@ +/******************************************************************************* + * Copyright (c) 2024 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 { Theme, useTheme } from '@material-ui/core/styles'; +import { useEffect, useRef } from 'react'; +import { DeckInput } from '../common/DeckInput'; +import { LaneHeader } from '../styled/DeckLaneStyledComponents'; +import { DeckTitle, RightContent, titleFontStyle } from '../styled/DeckStyledComponents'; +import { DeckLaneHeaderProps } from './DeckLaneHeader.types'; + +export const DeckLaneHeader = ({ updateTitle, editLaneTitle, label, title, t, laneDraggable }: DeckLaneHeaderProps) => { + const theme: Theme = useTheme(); + const titleInputRef = useRef(null); + + const headerRef = useRef(null); + + useEffect(() => { + if (headerRef.current) { + if (editLaneTitle) { + headerRef.current.focus(); + } + } + }, [editLaneTitle]); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (titleInputRef.current) { + if (e.key === 'F2') { + titleInputRef.current.select(); + } + } + }; + return ( + + + {editLaneTitle ? ( + + ) : ( + title + )} + + {label && {label}} + + ); +}; diff --git a/packages/deck/frontend/sirius-components-deck/src/laneHeader/DeckLaneHeader.types.ts b/packages/deck/frontend/sirius-components-deck/src/laneHeader/DeckLaneHeader.types.ts new file mode 100644 index 0000000000..29c3b56d86 --- /dev/null +++ b/packages/deck/frontend/sirius-components-deck/src/laneHeader/DeckLaneHeader.types.ts @@ -0,0 +1,21 @@ +/******************************************************************************* + * Copyright (c) 2024 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 DeckLaneHeaderProps { + updateTitle: (value: string) => void; + editLaneTitle: boolean; + label: string; + title: string; + t: (value: string) => string; + laneDraggable: boolean; +} diff --git a/packages/deck/frontend/sirius-components-deck/src/representation/DeckRepresentation.tsx b/packages/deck/frontend/sirius-components-deck/src/representation/DeckRepresentation.tsx index e6c7f11909..d872838104 100644 --- a/packages/deck/frontend/sirius-components-deck/src/representation/DeckRepresentation.tsx +++ b/packages/deck/frontend/sirius-components-deck/src/representation/DeckRepresentation.tsx @@ -19,11 +19,17 @@ import { useSelection, } from '@eclipse-sirius/sirius-components-core'; import Typography from '@material-ui/core/Typography'; -import { makeStyles } from '@material-ui/core/styles'; +import { Theme, makeStyles, useTheme } from '@material-ui/core/styles'; import { useEffect, useState } from 'react'; import { Deck } from '../Deck'; import { Card, CardMetadata } from '../Deck.types'; -import { convertToTrelloDeckData, moveCardInDeckLanes } from '../utils/deckGQLHelper'; +import { + convertToTrelloDeckData, + findLaneById, + moveCardInDeckLanes, + updateCard, + updateLane, +} from '../utils/deckGQLHelper'; import { DeckRepresentationState } from './DeckRepresentation.types'; import { deckEventSubscription } from './deckSubscription'; import { @@ -31,6 +37,7 @@ import { GQLDeckEventSubscription, GQLDeckRefreshedEventPayload, GQLErrorPayload, + GQLLane, } from './deckSubscription.types'; import { @@ -49,10 +56,19 @@ import { GQLEditCardVariables, GQLEditDeckCardInput, GQLEditDeckCardPayload, + GQLEditDeckLaneInput, + GQLEditLaneData, + GQLEditLaneVariables, GQLSuccessPayload, } from './deckMutation.types'; -import { createCardMutation, deleteCardMutation, dropDeckCardMutation, editCardMutation } from './deckMutation'; +import { + createCardMutation, + deleteCardMutation, + dropDeckCardMutation, + editCardMutation, + editLaneMutation, +} from './deckMutation'; const useDeckRepresentationStyles = makeStyles(() => ({ complete: { display: 'flex', @@ -71,14 +87,14 @@ const isSuccessPayload = ( ): payload is GQLSuccessPayload => payload.__typename === 'SuccessPayload'; export const DeckRepresentation = ({ editingContextId, representationId }: RepresentationComponentProps) => { + const theme: Theme = useTheme(); const classes = useDeckRepresentationStyles(); const { selection, setSelection }: UseSelectionValue = useSelection(); const { addErrorMessage, addMessages } = useMultiToast(); - const [{ id, deck, complete, selectedCardIds }, setState] = useState({ + const [{ id, deck, complete }, setState] = useState({ id: crypto.randomUUID(), deck: undefined, complete: false, - selectedCardIds: [], }); const { error } = useSubscription(deckEventSubscription, { @@ -128,6 +144,11 @@ export const DeckRepresentation = ({ editingContextId, representationId }: Repre const [dropDeckCard, { loading: dropDeckCardLoading, data: dropDeckCardData, error: dropDeckCardError }] = useMutation(dropDeckCardMutation); + const [editDeckLane, { loading: editLaneLoading, data: editLaneData, error: editLaneError }] = useMutation< + GQLEditLaneData, + GQLEditLaneVariables + >(editLaneMutation); + useEffect(() => { if (error) { addErrorMessage(error.message); @@ -137,22 +158,29 @@ export const DeckRepresentation = ({ editingContextId, representationId }: Repre useEffect(() => { if (deck && selection.entries) { const selectionIds: string[] = selection.entries.map((entry) => entry.id); - const tempSelectedCardIds: string[] = []; + const tempselectedElementIds: string[] = []; deck.lanes .flatMap((lane) => lane.cards) .forEach((card) => { if (selectionIds.includes(card.targetObjectId)) { - tempSelectedCardIds.push(card.id); + tempselectedElementIds.push(card.id); } }); setState((prevState) => { - return { ...prevState, selectedCardIds: tempSelectedCardIds }; + return { ...prevState, selectedElementIds: tempselectedElementIds }; }); } }, [selection]); const handleError = ( loading: boolean, - data: GQLEditCardData | GQLDeleteCardData | GQLCreateCardData | GQLDropDeckCardData | null | undefined, + data: + | GQLEditCardData + | GQLDeleteCardData + | GQLCreateCardData + | GQLDropDeckCardData + | GQLEditLaneData + | null + | undefined, error: ApolloError | undefined ) => { if (!loading) { @@ -188,6 +216,10 @@ export const DeckRepresentation = ({ editingContextId, representationId }: Repre handleError(dropDeckCardLoading, dropDeckCardData, dropDeckCardError); }, [dropDeckCardLoading, dropDeckCardData, dropDeckCardError]); + useEffect(() => { + handleError(editLaneLoading, editLaneData, editLaneError); + }, [editLaneLoading, editLaneData, editLaneError]); + const handleEditCard = (_laneId: string, card: Card) => { const input: GQLEditDeckCardInput = { id: crypto.randomUUID(), @@ -199,10 +231,35 @@ export const DeckRepresentation = ({ editingContextId, representationId }: Repre newDescription: card.description, }; - // // to avoid blink because useMutation implies a re-render as the task value is the old one - // updateTask(gantt, task.id, newDetail); + if (deck) { + // to avoid blink because useMutation implies a re-render as the card value is the old one + const updatedDeck = updateCard(deck, card); + setState((prevState) => { + return { ...prevState, deck: updatedDeck }; + }); + } editDeckCard({ variables: { input } }); }; + + const handleEditLane = (laneId: string, newValue: { title: string }) => { + const input: GQLEditDeckLaneInput = { + id: crypto.randomUUID(), + editingContextId, + representationId, + laneId, + newTitle: newValue.title, + }; + + if (deck) { + // to avoid blink because useMutation implies a re-render as the lane value is the old one + const updatedDeck = updateLane(deck, laneId, newValue.title); + setState((prevState) => { + return { ...prevState, deck: updatedDeck }; + }); + } + editDeckLane({ variables: { input } }); + }; + const handleDeleteCard = (cardId: string, _laneId: string) => { const input: GQLDeleteDeckCardInput = { id: crypto.randomUUID(), @@ -247,6 +304,23 @@ export const DeckRepresentation = ({ editingContextId, representationId }: Repre } }; + const handleLaneClicked = (laneId: string) => { + if (deck) { + const lane: GQLLane | undefined = findLaneById(deck, laneId); + if (lane) { + setSelection({ + entries: [ + { + id: lane.targetObjectId, + kind: '', + label: '', + }, + ], + }); + } + } + }; + let content: JSX.Element | null = null; if (complete) { content = ( @@ -257,7 +331,8 @@ export const DeckRepresentation = ({ editingContextId, representationId }: Repre
); } else if (deck) { - const data = convertToTrelloDeckData(deck, selectedCardIds); + const selectedElementIds: string[] = selection.entries.map((entry) => entry.id); + const data = convertToTrelloDeckData(deck, selectedElementIds, theme); content = ( ); } diff --git a/packages/deck/frontend/sirius-components-deck/src/representation/DeckRepresentation.types.ts b/packages/deck/frontend/sirius-components-deck/src/representation/DeckRepresentation.types.ts index 6e35386b98..7a48de4f34 100644 --- a/packages/deck/frontend/sirius-components-deck/src/representation/DeckRepresentation.types.ts +++ b/packages/deck/frontend/sirius-components-deck/src/representation/DeckRepresentation.types.ts @@ -16,7 +16,6 @@ export interface DeckRepresentationState { id: string; deck: GQLDeck | undefined; complete: boolean; - selectedCardIds: string[]; } export interface Subscriber { diff --git a/packages/deck/frontend/sirius-components-deck/src/representation/deckMutation.ts b/packages/deck/frontend/sirius-components-deck/src/representation/deckMutation.ts index a6f5e9beef..fb85e74f6a 100644 --- a/packages/deck/frontend/sirius-components-deck/src/representation/deckMutation.ts +++ b/packages/deck/frontend/sirius-components-deck/src/representation/deckMutation.ts @@ -91,3 +91,23 @@ export const dropDeckCardMutation = gql` } } `; + +export const editLaneMutation = gql` + mutation editDeckLane($input: EditDeckLaneInput!) { + editDeckLane(input: $input) { + __typename + ... on ErrorPayload { + messages { + body + level + } + } + ... on SuccessPayload { + messages { + body + level + } + } + } + } +`; diff --git a/packages/deck/frontend/sirius-components-deck/src/representation/deckMutation.types.ts b/packages/deck/frontend/sirius-components-deck/src/representation/deckMutation.types.ts index 3d6c716bf0..55f3814972 100644 --- a/packages/deck/frontend/sirius-components-deck/src/representation/deckMutation.types.ts +++ b/packages/deck/frontend/sirius-components-deck/src/representation/deckMutation.types.ts @@ -97,10 +97,31 @@ export interface GQLDropDeckCardPayload { __typename: string; } +export interface GQLEditLaneVariables { + input: GQLEditDeckLaneInput; +} + +export interface GQLEditDeckLaneInput { + id: string; + editingContextId: string; + representationId: string; + laneId: string; + newTitle: string; +} + +export interface GQLEditLaneData { + editDeckLane: GQLEditDeckLanePayload; +} + +export interface GQLEditDeckLanePayload { + __typename: string; +} + export interface GQLSuccessPayload extends GQLCreateDeckCardPayload, GQLDeleteDeckCardPayload, GQLEditDeckCardPayload, - GQLDropDeckCardPayload { + GQLDropDeckCardPayload, + GQLEditDeckLanePayload { messages: GQLMessage[]; } diff --git a/packages/deck/frontend/sirius-components-deck/src/representation/deckSubscription.ts b/packages/deck/frontend/sirius-components-deck/src/representation/deckSubscription.ts index f4a1f65bd7..61adbaeda3 100644 --- a/packages/deck/frontend/sirius-components-deck/src/representation/deckSubscription.ts +++ b/packages/deck/frontend/sirius-components-deck/src/representation/deckSubscription.ts @@ -36,6 +36,7 @@ export const deckEventSubscription = gql` id title label + targetObjectId cards { id targetObjectId diff --git a/packages/deck/frontend/sirius-components-deck/src/representation/deckSubscription.types.ts b/packages/deck/frontend/sirius-components-deck/src/representation/deckSubscription.types.ts index 544de0d651..e0628a8186 100644 --- a/packages/deck/frontend/sirius-components-deck/src/representation/deckSubscription.types.ts +++ b/packages/deck/frontend/sirius-components-deck/src/representation/deckSubscription.types.ts @@ -56,6 +56,7 @@ export interface GQLLane { title: string; label: string; cards: GQLCard[]; + targetObjectId: string; } export interface GQLCard { diff --git a/packages/deck/frontend/sirius-components-deck/src/card/DeckCardStyledComponents.tsx b/packages/deck/frontend/sirius-components-deck/src/styled/DeckCardStyledComponents.tsx similarity index 87% rename from packages/deck/frontend/sirius-components-deck/src/card/DeckCardStyledComponents.tsx rename to packages/deck/frontend/sirius-components-deck/src/styled/DeckCardStyledComponents.tsx index 4cf6cdd7df..c34bad9039 100644 --- a/packages/deck/frontend/sirius-components-deck/src/card/DeckCardStyledComponents.tsx +++ b/packages/deck/frontend/sirius-components-deck/src/styled/DeckCardStyledComponents.tsx @@ -14,13 +14,6 @@ import IconButton from '@material-ui/core/IconButton'; import { styled } from '@material-ui/core/styles'; import { CSSProperties } from '@material-ui/core/styles/withStyles'; -import { DeckCardTitleProps } from './DeckCardStyledComponents.types'; - -export const cardTitleFontStyle: CSSProperties = { - fontWeight: 'bold', - fontSize: '14px', - lineHeight: '18px', -}; export const cardLabelFontStyle: CSSProperties = { fontSize: '10px', @@ -53,11 +46,6 @@ export const DeckCardHeader = styled('article')(({ theme }) => ({ color: theme.palette.text.primary, })); -export const DeckCardTitle = styled('span')({ - cursor: (props: DeckCardTitleProps) => (props.draggable ? 'grab' : `auto`), - width: '70%', -}); - export const DeckCardRightContent = styled('span')({ width: ' 38%', paddingRight: '10px', diff --git a/packages/deck/frontend/sirius-components-deck/src/styled/DeckLaneStyledComponents.tsx b/packages/deck/frontend/sirius-components-deck/src/styled/DeckLaneStyledComponents.tsx new file mode 100644 index 0000000000..cf84c511b3 --- /dev/null +++ b/packages/deck/frontend/sirius-components-deck/src/styled/DeckLaneStyledComponents.tsx @@ -0,0 +1,22 @@ +/******************************************************************************* + * Copyright (c) 2024 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 { styled } from '@material-ui/core/styles'; + +export const LaneHeader = styled('header')({ + marginBottom: ' 10px', + display: 'flex', + flexDirection: 'row', + alignItems: 'flex-start', + padding: '0px 5px', +}); diff --git a/packages/deck/frontend/sirius-components-deck/src/styled/DeckStyledComponents.tsx b/packages/deck/frontend/sirius-components-deck/src/styled/DeckStyledComponents.tsx new file mode 100644 index 0000000000..af1e2e1fdd --- /dev/null +++ b/packages/deck/frontend/sirius-components-deck/src/styled/DeckStyledComponents.tsx @@ -0,0 +1,38 @@ +/******************************************************************************* + * Copyright (c) 2024 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 { styled } from '@material-ui/core/styles'; +import { CSSProperties } from '@material-ui/core/styles/withStyles'; +import { DeckTitleProps } from './DeckStyledComponents.types'; + +export const titleFontStyle: CSSProperties = { + fontWeight: 'bold', + fontSize: '14px', + lineHeight: '18px', +}; + +export const Title = styled('span')(({ theme }) => ({ + width: '70%', + color: theme.palette.text.primary, +})); + +export const DeckTitle = styled(Title)({ + cursor: (props: DeckTitleProps) => (props.draggable ? 'grab' : `auto`), +}); + +export const RightContent = styled('span')({ + width: '38%', + textAlign: 'right', + paddingRight: '10px', + fontSize: '13px', +}); diff --git a/packages/deck/frontend/sirius-components-deck/src/card/DeckCardStyledComponents.types.tsx b/packages/deck/frontend/sirius-components-deck/src/styled/DeckStyledComponents.types.tsx similarity index 84% rename from packages/deck/frontend/sirius-components-deck/src/card/DeckCardStyledComponents.types.tsx rename to packages/deck/frontend/sirius-components-deck/src/styled/DeckStyledComponents.types.tsx index e307256f4a..9affa87cfc 100644 --- a/packages/deck/frontend/sirius-components-deck/src/card/DeckCardStyledComponents.types.tsx +++ b/packages/deck/frontend/sirius-components-deck/src/styled/DeckStyledComponents.types.tsx @@ -11,6 +11,9 @@ * Obeo - initial API and implementation *******************************************************************************/ -export interface DeckCardTitleProps { +import { Theme } from '@material-ui/core/styles'; + +export interface DeckTitleProps { draggable: boolean; + theme: Theme; } diff --git a/packages/deck/frontend/sirius-components-deck/src/utils/deckGQLHelper.tsx b/packages/deck/frontend/sirius-components-deck/src/utils/deckGQLHelper.tsx index c8efd2f41f..b92613d047 100644 --- a/packages/deck/frontend/sirius-components-deck/src/utils/deckGQLHelper.tsx +++ b/packages/deck/frontend/sirius-components-deck/src/utils/deckGQLHelper.tsx @@ -10,10 +10,11 @@ * Contributors: * Obeo - initial API and implementation *******************************************************************************/ -import { Card, DeckData } from '../Deck.types'; +import { Theme } from '@material-ui/core/styles'; +import { Card, DeckData, Lane } from '../Deck.types'; import { GQLCard, GQLDeck, GQLLane } from '../representation/deckSubscription.types'; -export const convertToTrelloDeckData = (deck: GQLDeck, selectedCardIds: string[]): DeckData => { +export const convertToTrelloDeckData = (deck: GQLDeck, selectedElementIds: string[], theme: Theme): DeckData => { const data: DeckData = { lanes: [], }; @@ -21,8 +22,6 @@ export const convertToTrelloDeckData = (deck: GQLDeck, selectedCardIds: string[] for (const lane of deck.lanes) { const cards: Card[] = lane.cards.map((card) => { let editable: boolean = false; - let className: string | undefined; - let style: object | undefined; const { targetObjectId, targetObjectLabel, targetObjectKind, ...otherCardProps } = card; const metadata = { selection: { @@ -31,23 +30,34 @@ export const convertToTrelloDeckData = (deck: GQLDeck, selectedCardIds: string[] kind: targetObjectKind, }, }; - if (selectedCardIds.includes(card.id)) { + if (selectedElementIds.includes(card.targetObjectId)) { editable = true; } return { ...otherCardProps, editable, metadata, - className, - style, }; }); - data.lanes.push({ - ...lane, + + const { id, label, title, targetObjectId } = lane; + const selectedLane: boolean = selectedElementIds.includes(targetObjectId); + + const style: React.CSSProperties = { + border: selectedLane ? `2px solid ${theme.palette.selected}` : undefined, + }; + + const convertedLane: Lane = { + id, + label, + title, + editLaneTitle: selectedLane, editable: true, cards, + style, 'data-testid': `lane-${lane.title}`, - }); + }; + data.lanes.push(convertedLane); } return data; }; @@ -84,3 +94,51 @@ export const moveCardInDeckLanes = ( } return deckToReturn; }; + +export const findLaneById = (deck: GQLDeck, laneId: string): GQLLane | undefined => { + return deck.lanes.find((lane) => lane.id === laneId); +}; + +export const updateCard = (deck: GQLDeck, newCard: Card): GQLDeck => { + const newDeck: GQLDeck = { + ...deck, + }; + + newDeck.lanes.forEach((lane) => { + const index: number = lane.cards.findIndex((card) => card.id === newCard.id); + if (index > -1) { + const removedCards: GQLCard[] = lane.cards.splice(index, 1); + if (removedCards.length === 1) { + const removedCard = removedCards[0] as GQLCard; + const cardToUpdate: GQLCard = { + ...removedCard, + description: newCard.description, + label: newCard.label, + title: newCard.title, + }; + lane.cards.splice(index, 0, cardToUpdate); + } + } + }); + return newDeck; +}; + +export const updateLane = (deck: GQLDeck, laneId: string, newTitle: string): GQLDeck => { + const newDeck: GQLDeck = { + ...deck, + }; + + const index: number = newDeck.lanes.findIndex((lane) => lane.id === laneId); + if (index > -1) { + const removedLanes: GQLLane[] = newDeck.lanes.splice(index, 1); + if (removedLanes.length === 1) { + const removedLane = removedLanes[0] as GQLLane; + const laneToUpdate: GQLLane = { + ...removedLane, + title: newTitle, + }; + newDeck.lanes.splice(index, 0, laneToUpdate); + } + } + return newDeck; +}; diff --git a/packages/starters/backend/sirius-components-task-starter/src/main/java/org/eclipse/sirius/components/task/starter/configuration/view/ViewDeckDescriptionBuilder.java b/packages/starters/backend/sirius-components-task-starter/src/main/java/org/eclipse/sirius/components/task/starter/configuration/view/ViewDeckDescriptionBuilder.java index c7c94215a3..e8853ec87f 100644 --- a/packages/starters/backend/sirius-components-task-starter/src/main/java/org/eclipse/sirius/components/task/starter/configuration/view/ViewDeckDescriptionBuilder.java +++ b/packages/starters/backend/sirius-components-task-starter/src/main/java/org/eclipse/sirius/components/task/starter/configuration/view/ViewDeckDescriptionBuilder.java @@ -28,6 +28,7 @@ import org.eclipse.sirius.components.view.deck.DeckDescription; import org.eclipse.sirius.components.view.deck.DeleteCardTool; import org.eclipse.sirius.components.view.deck.EditCardTool; +import org.eclipse.sirius.components.view.deck.EditLaneTool; import org.eclipse.sirius.components.view.deck.LaneDescription; /** @@ -79,12 +80,26 @@ private CardDescription createCardDescription() { private LaneDescription createLaneDescription() { CreateCardTool createCardTool = this.createCardTool(); CardDropTool cardDropTool = this.createCardDropTool(); + EditLaneTool editLaneTool = this.createEditLaneTool(); return this.deckBuilders.newLaneDescription() .semanticCandidatesExpression("aql:self.ownedTags->select(tag | tag.prefix == 'daily')") .labelExpression("aql:self.getTasksWithTag()->size() + ' / ' + self.eContainer().oclAsType(task::Project).ownedTasks->select(task | task.tags->exists(tag | tag.prefix == 'daily'))->size()") .titleExpression("aql:self.suffix") .createTool(createCardTool) .cardDropTool(cardDropTool) + .editTool(editLaneTool) + .build(); + } + + private EditLaneTool createEditLaneTool() { + SetValue setValue = this.viewBuilders.newSetValue() + .featureName("suffix") + .valueExpression("aql:newTitle") + .build(); + + return this.deckBuilders.newEditLaneTool() + .name("Edit Lane Title") + .body(setValue) .build(); }