Skip to content

Commit

Permalink
[2942] Add the Card DnD capability on Deck
Browse files Browse the repository at this point in the history
- Make it possible to specify the card drop behavior on a Lane in the
Deck view model.
- Add the DropCard mutation to handle the front-end drag and drop
and execute the tool.

Bug: #2942
Signed-off-by: Florian Barbin <florian.barbin@obeo.fr>
Signed-off-by: Florian ROUËNÉ <florian.rouene@obeosoft.com>
  • Loading branch information
florianbarbin authored and frouene committed Jan 29, 2024
1 parent d7ab5dc commit 0e72358
Show file tree
Hide file tree
Showing 48 changed files with 1,150 additions and 58 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,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.

=== Improvements

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.eclipse.sirius.components.collaborative.deck.dto.input.CreateDeckCardInput;
import org.eclipse.sirius.components.collaborative.deck.dto.input.DeleteDeckCardInput;
import org.eclipse.sirius.components.collaborative.deck.dto.input.EditDeckCardInput;
import org.eclipse.sirius.components.collaborative.deck.dto.input.DropDeckCardInput;
import org.eclipse.sirius.components.core.api.IEditingContext;
import org.eclipse.sirius.components.core.api.IPayload;
import org.eclipse.sirius.components.deck.Deck;
Expand All @@ -41,6 +42,11 @@ public interface IDeckCardService {
*/
IPayload editCard(EditDeckCardInput editDeckCardInput, IEditingContext editingContext, Deck deck);

/**
* Move an existing card.
*/
IPayload dropCard(DropDeckCardInput dropDeckCardInput, IEditingContext editingContext, Deck deck);


/**
* Implementation which does nothing, used for mocks in unit tests.
Expand All @@ -63,6 +69,11 @@ public IPayload editCard(EditDeckCardInput editDeckCardInput, IEditingContext ed
public IPayload deleteCard(DeleteDeckCardInput deleteDeckCardInput, IEditingContext editingContext, Deck deck) {
return null;
}

@Override
public IPayload dropCard(DropDeckCardInput dropDeckCardInput, IEditingContext editingContext, Deck deck) {
return null;
}
}


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*******************************************************************************
* 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 "drop deck card" mutation.
*
* @author fbarbin
*/
public record DropDeckCardInput(UUID id, String editingContextId, String representationId, String oldLaneId, String newLaneId, String cardId, int addedIndex) implements IDeckInput {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*******************************************************************************
* 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.IDeckCardService;
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.dto.input.DropDeckCardInput;
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 "Drop Deck Card" events.
*
* @author fbarbin
*/
@Service
public class DropDeckCardEventHandler implements IDeckEventHandler {

private final IDeckCardService deckCardService;

private final ICollaborativeDeckMessageService messageService;

private final Counter counter;

public DropDeckCardEventHandler(IDeckCardService deckCardService, ICollaborativeDeckMessageService messageService, MeterRegistry meterRegistry) {
this.deckCardService = Objects.requireNonNull(deckCardService);
this.messageService = Objects.requireNonNull(messageService);

this.counter = Counter.builder(Monitoring.EVENT_HANDLER).tag(Monitoring.NAME, this.getClass().getSimpleName()).register(meterRegistry);
}

@Override
public boolean canHandle(IDeckInput deckInput) {
return deckInput instanceof DropDeckCardInput;
}

@Override
public void handle(One<IPayload> payloadSink, Many<ChangeDescription> changeDescriptionSink, IEditingContext editingContext, Deck deck, IDeckInput deckInput) {
this.counter.increment();

String message = this.messageService.invalidInput(deckInput.getClass().getSimpleName(), DropDeckCardInput.class.getSimpleName());
IPayload payload = new ErrorPayload(deckInput.id(), message);
ChangeDescription changeDescription = new ChangeDescription(ChangeKind.NOTHING, deckInput.representationId(), deckInput);

if (deckInput instanceof DropDeckCardInput input) {
payload = this.deckCardService.dropCard(input, editingContext, deck);

changeDescription = new ChangeDescription(ChangeKind.SEMANTIC_CHANGE, deckInput.representationId(), deckInput);
}

payloadSink.tryEmitValue(payload);
changeDescriptionSink.tryEmitNext(changeDescription);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.eclipse.sirius.components.collaborative.deck.api.IDeckCardService;
import org.eclipse.sirius.components.collaborative.deck.dto.input.CreateDeckCardInput;
import org.eclipse.sirius.components.collaborative.deck.dto.input.DeleteDeckCardInput;
import org.eclipse.sirius.components.collaborative.deck.dto.input.DropDeckCardInput;
import org.eclipse.sirius.components.collaborative.deck.dto.input.EditDeckCardInput;
import org.eclipse.sirius.components.core.api.ErrorPayload;
import org.eclipse.sirius.components.core.api.IEditingContext;
Expand Down Expand Up @@ -74,9 +75,7 @@ public IPayload createCard(CreateDeckCardInput createDeckCardInput, IEditingCont
VariableManager variableManager = new VariableManager();
Optional<Object> optionalTargetObject;
if (currentLaneId != null) {
optionalTargetObject = optionalParentLane
.map(card -> this.objectService.getObject(editingContext, card.targetObjectId()))
.map(Optional::get);
optionalTargetObject = optionalParentLane.map(card -> this.objectService.getObject(editingContext, card.targetObjectId())).map(Optional::get);
if (optionalTargetObject.isEmpty()) {
this.feedbackMessageService.addFeedbackMessage(new Message(MessageFormat.format("The current lane of id ''{0}'' is not found", currentLaneId), MessageLevel.ERROR));
}
Expand Down Expand Up @@ -142,6 +141,38 @@ public IPayload editCard(EditDeckCardInput editDeckCardInput, IEditingContext ed
return payload;
}

@Override
public IPayload dropCard(DropDeckCardInput dropDeckCardInput, IEditingContext editingContext, Deck deck) {
IPayload payload = new ErrorPayload(dropDeckCardInput.id(), "Move card failed");

Optional<Card> optionalCard = this.findCard(card -> Objects.equals(card.id(), dropDeckCardInput.cardId()), deck);
Optional<Lane> optionalOldLane = this.findLane(lane -> Objects.equals(lane.id(), dropDeckCardInput.oldLaneId()), deck);
Optional<Lane> optionalNewLane = this.findLane(lane -> Objects.equals(lane.id(), dropDeckCardInput.newLaneId()), deck);
Optional<LaneDescription> optionalLaneDescription = optionalNewLane.flatMap(lane -> this.findLaneDescription(lane.descriptionId(), deck, editingContext));

if (optionalCard.isPresent() && optionalLaneDescription.isPresent()) {
Optional<Object> optionalTargetObject = this.objectService.getObject(editingContext, optionalCard.get().targetObjectId());
if (optionalTargetObject.isPresent()) {
VariableManager variableManager = new VariableManager();
variableManager.put(VariableManager.SELF, optionalTargetObject.get());
variableManager.put(LaneDescription.OLD_LANE, optionalOldLane.orElse(null));
variableManager.put(LaneDescription.OLD_LANE_TARGET, optionalOldLane.flatMap(lane -> {
return this.objectService.getObject(editingContext, lane.targetObjectId());
}).orElse(null));
variableManager.put(LaneDescription.NEW_LANE, optionalNewLane.orElse(null));
variableManager.put(LaneDescription.NEW_LANE_TARGET, optionalNewLane.flatMap(lane -> {
return this.objectService.getObject(editingContext, lane.targetObjectId());
}).orElse(null));
variableManager.put(LaneDescription.INDEX, dropDeckCardInput.addedIndex());
optionalLaneDescription.get().dropCardProvider().accept(variableManager);

payload = this.getPayload(dropDeckCardInput.id());
}
}

return payload;
}

private IPayload getPayload(UUID payloadId) {
IPayload payload = null;
List<Message> feedbackMessages = this.feedbackMessageService.getFeedbackMessages();
Expand All @@ -153,6 +184,7 @@ private IPayload getPayload(UUID payloadId) {
}
return payload;
}

private Optional<Lane> findLane(Predicate<Lane> condition, Deck deck) {
return deck.lanes().stream()
.filter(condition)
Expand All @@ -173,24 +205,21 @@ private Optional<DeckDescription> findDeckDescription(String descriptionId, IEdi
}

private Optional<LaneDescription> findLaneDescription(String descriptionId, Deck deck, IEditingContext editingContext) {
return this.findDeckDescription(deck.descriptionId(), editingContext)
.stream()
return this.findDeckDescription(deck.descriptionId(), editingContext).stream()
.map(DeckDescription::laneDescriptions)
.flatMap(Collection::stream)
.filter(laneDesc -> laneDesc.id().equals(descriptionId))
.findFirst();
}

private Optional<CardDescription> findCardDescription(String descriptionId, Deck deck, IEditingContext editingContext) {
List<LaneDescription> laneDescriptions = this.findDeckDescription(deck.descriptionId(), editingContext)
.map(DeckDescription::laneDescriptions)
.orElse(List.of());
List<LaneDescription> laneDescriptions = this.findDeckDescription(deck.descriptionId(), editingContext).map(DeckDescription::laneDescriptions).orElse(List.of());

return laneDescriptions.stream()
.map(LaneDescription::cardDescriptions)
.flatMap(Collection::stream)
.filter(cardDesc -> cardDesc.id().equals(descriptionId))
.findFirst();
.map(LaneDescription::cardDescriptions)
.flatMap(Collection::stream)
.filter(cardDesc -> cardDesc.id().equals(descriptionId))
.findFirst();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ extend type Mutation {
createDeckCard(input: CreateDeckCardInput!): CreateDeckCardPayload
deleteDeckCard(input: DeleteDeckCardInput!): DeleteDeckCardPayload
editDeckCard(input: EditDeckCardInput!): EditDeckCardPayload
dropDeckCard(input: DropDeckCardInput!): DropDeckCardPayload
}

input CreateDeckCardInput {
Expand All @@ -65,6 +66,7 @@ input DeleteDeckCardInput {
representationId: ID!
cardId: ID!
}

union DeleteDeckCardPayload = SuccessPayload | ErrorPayload

input EditDeckCardInput {
Expand All @@ -76,4 +78,17 @@ input EditDeckCardInput {
newLabel: String!
newDescription: String!
}

union EditDeckCardPayload = SuccessPayload | ErrorPayload

input DropDeckCardInput {
id: ID!
editingContextId: ID!
representationId: ID!
oldLaneId: ID!
newLaneId: ID!
cardId: ID!
addedIndex: Int!
}

union DropDeckCardPayload = SuccessPayload | ErrorPayload
Original file line number Diff line number Diff line change
@@ -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.DropDeckCardInput;
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 drop a Deck card.
*
* @author fbarbin
*/
@MutationDataFetcher(type = "Mutation", field = "dropDeckCard")
public class MutationDropDeckCardDataFetcher implements IDataFetcherWithFieldCoordinates<CompletableFuture<IPayload>> {

private static final String INPUT_ARGUMENT = "input";

private final ObjectMapper objectMapper;

private final IExceptionWrapper exceptionWrapper;

private final IEditingContextDispatcher editingContextDispatcher;

public MutationDropDeckCardDataFetcher(ObjectMapper objectMapper, IExceptionWrapper exceptionWrapper, IEditingContextDispatcher editingContextDispatcher) {
this.objectMapper = Objects.requireNonNull(objectMapper);
this.exceptionWrapper = Objects.requireNonNull(exceptionWrapper);
this.editingContextDispatcher = Objects.requireNonNull(editingContextDispatcher);
}

@Override
public CompletableFuture<IPayload> get(DataFetchingEnvironment environment) throws Exception {
Object argument = environment.getArgument(INPUT_ARGUMENT);
var input = this.objectMapper.convertValue(argument, DropDeckCardInput.class);

return this.exceptionWrapper.wrapMono(() -> this.editingContextDispatcher.dispatchMutation(input.editingContextId(), input), input).toFuture();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,13 @@
@PublicApi
public record LaneDescription(String id, Function<VariableManager, String> targetObjectKindProvider, Function<VariableManager, String> targetObjectLabelProvider,
Function<VariableManager, String> targetObjectIdProvider, Function<VariableManager, List<Object>> semanticElementsProvider, Function<VariableManager, String> titleProvider,
Function<VariableManager, String> labelProvider, List<CardDescription> cardDescriptions, Consumer<VariableManager> editLaneProvider, Consumer<VariableManager> createCardProvider) {
Function<VariableManager, String> labelProvider, List<CardDescription> cardDescriptions, Consumer<VariableManager> editLaneProvider, Consumer<VariableManager> createCardProvider, Consumer<VariableManager> dropCardProvider) {

public static final String OLD_LANE = "oldLane";
public static final String OLD_LANE_TARGET = "oldLaneTarget";
public static final String NEW_LANE = "newLane";
public static final String INDEX = "index";
public static final String NEW_LANE_TARGET = "newLaneTarget";

public LaneDescription {
Objects.requireNonNull(id);
Expand All @@ -42,6 +48,7 @@ public record LaneDescription(String id, Function<VariableManager, String> targe
Objects.requireNonNull(cardDescriptions);
Objects.requireNonNull(editLaneProvider);
Objects.requireNonNull(createCardProvider);
Objects.requireNonNull(dropCardProvider);
}

@Override
Expand Down
2 changes: 2 additions & 0 deletions packages/deck/frontend/sirius-components-deck/src/Deck.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const Deck = ({
onCardDelete,
onCardAdd,
onCardUpdate,
onCardMoveAcrossLanes,
}: DeckProps) => {
const theme: Theme = useTheme();
const boardStyle = {
Expand All @@ -43,6 +44,7 @@ export const Deck = ({
onCardDelete={onCardDelete}
onCardAdd={onCardAdd}
onCardUpdate={onCardUpdate}
onCardMoveAcrossLanes={onCardMoveAcrossLanes}
data-testid={`deck-representation`}
style={boardStyle}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface DeckProps {
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;
}
export interface OnCardClickProps {
cardId: String;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ export const DeckCard = ({
style={cardStyle}
className={className}
onKeyDown={handleKeyDown}
data-testid={`card-${title}`}>
data-testid={`card-${title}`}
onDragStart={(e) => e.preventDefault()}>
<DeckCardHeader>
<DeckCardTitle draggable={cardDraggable} style={cardTitleFontStyle}>
{editable ? (
Expand Down

0 comments on commit 0e72358

Please sign in to comment.