Skip to content

Commit

Permalink
[1915] Add page concept in form view DSL
Browse files Browse the repository at this point in the history
Bug: eclipse-sirius#1915
Signed-off-by: Florian Rouëné <florian.rouene@obeosoft.com>
  • Loading branch information
frouene committed May 22, 2023
1 parent 50f4926 commit 9f32b5a
Show file tree
Hide file tree
Showing 105 changed files with 6,333 additions and 2,410 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.adoc
Expand Up @@ -33,6 +33,7 @@ In the _styleDescription_, the definition of a color are now a select list of al
For that, the `DiagramCreationService` will have less responsabilities.
Starting now, it will stop persisting the diagram after its creation.
It will start by improving a bit performances since diagrams were persisted twice in some use cases.
- https://github.com/eclipse-sirius/sirius-components/issues/1915[#1915] [view] Add the page support in the view DSL.

=== Dependency update
- https://github.com/eclipse-sirius/sirius-components/issues/1936[#1936] [releng] Switch to Cypress 12.11.0
Expand Down
27 changes: 27 additions & 0 deletions doc/adrs/098_add_page_description_in_view.adoc
@@ -0,0 +1,27 @@
= ADR-098 - Add `PageDescription` definition in the View DSL

== Context

In the View DSL definition (in `view.ecore`), it's only possible to organize `FormDescription` in separate groups.

== Decision

We will add a new level to organize groups in different pages. This new object `PageDescription` will be contained in a `FormDescription` and may contain any number of
`GroupDescription`.

=== Frontend

The Material-UI `Tabs` representation will be used to display the pages of the FormDescription.
A new drop area will be added to drop new page elements from the editor.
To change the page order, the `Tab` will be _draggable._
The `Tab` are Material-UI component, and it's not recommended to add a new drop area to `Tabs` between them to offer the possibility to place `Tab` at any position.
To get around that, it will be possible to drop elements directly on the `Tab` element in order to place it before.

=== Backend

The new structure of the class `FormDescriptionEditor` will be just like the ecore definition i.e., the new layer `List<Page>` will be introduced in place of `List<Group>` which
is moved in the `Page` object.

== Status

Accepted.
Expand Up @@ -20,13 +20,9 @@ describe('/projects/:projectId/edit - FormDescriptionEditor', () => {
cy.visit(`/projects/${projectId}/edit`);
});
});
});

it('try to move a toolbar action into another empty group', () => {
cy.getByTestId('ViewDocument').dblclick();
cy.getByTestId('View').dblclick();
cy.getByTestId('View-more').click();

// create the form description
cy.getByTestId('treeitem-contextmenu').findByTestId('new-object').click();
//make sure the data are fetched before selecting
Expand All @@ -39,48 +35,73 @@ describe('/projects/:projectId/edit - FormDescriptionEditor', () => {
cy.getByTestId('New Form Description-more').click();
cy.getByTestId('treeitem-contextmenu').findByTestId('new-representation').click();
cy.getByTestId('create-representation').click();
// create a second group
});

it('try to move a toolbar action into another empty group', () => {
// create another group
const dataTransfer = new DataTransfer();
cy.getByTestId('FormDescriptionEditor-Group').trigger('dragstart', { dataTransfer });
cy.getByTestId('FormDescriptionEditor-DropArea').trigger('drop', { dataTransfer });
cy.getByTestId('Page-DropArea').trigger('drop', { dataTransfer });
// create a toolbar action in the first group
cy.get('[data-testid^="Group-ToolbarActions-NewAction-"]').eq(0).click();
// move the toolbar action from the first group to the second one
cy.getByTestId('ToolbarAction').trigger('dragstart', { dataTransfer });
cy.get('[data-testid^="Group-ToolbarActions-DropArea-"]').eq(1).trigger('drop', { dataTransfer });
});

it('rename a form description editor', () => {
cy.getByTestId('ViewDocument').dblclick();
cy.getByTestId('View').dblclick();
cy.getByTestId('View-more').click();
// create the form description
cy.getByTestId('treeitem-contextmenu').findByTestId('new-object').click();
//make sure the data are fetched before selecting
cy.getByTestId('create-object').should('be.enabled');
cy.getByTestId('childCreationDescription').click();
cy.get('[data-value="Form Description"]').click();
cy.getByTestId('create-object').click();
// create a button widget under the group of the form description editor
cy.getByTestId('New Form Description').dblclick();
cy.getByTestId('GroupDescription').dblclick();
cy.getByTestId('GroupDescription-more').click();
cy.getByTestId('treeitem-contextmenu').findByTestId('new-object').click();
//make sure the data are fetched before selecting
cy.getByTestId('create-object').should('be.enabled');
cy.getByTestId('childCreationDescription').click();
cy.get('[data-value="Widgets Button Description"]').click();
cy.getByTestId('create-object').click();
// create the form description editor
cy.getByTestId('New Form Description').click();
cy.getByTestId('New Form Description-more').click();
cy.getByTestId('treeitem-contextmenu').findByTestId('new-representation').click();
cy.getByTestId('create-representation').click();
// rename the form description editor
cy.getByTestId('FormDescriptionEditor').click();
cy.getByTestId('FormDescriptionEditor-more').click();
cy.getByTestId('treeitem-contextmenu').findByTestId('rename-tree-item').click();
cy.getByTestId('name-edit').type('Renamed-FormDescriptionEditor{enter}');
cy.getByTestId('Renamed-FormDescriptionEditor').should('exist');
it('try to create an empty page', () => {
cy.get('[data-testid^="Page-"]').not('[data-testid="Page-DropArea"]').should('have.lengthOf', 1);
const dataTransfer = new DataTransfer();
cy.getByTestId('FormDescriptionEditor-Page').trigger('dragstart', { dataTransfer });
cy.getByTestId('PageList-DropArea').trigger('drop', { dataTransfer });
cy.wait(500); // Wait for representation to refresh
cy.get('[data-testid^="Page-"]').not('[data-testid="Page-DropArea"]').should('have.lengthOf', 2);
cy.get('[data-testid^="Page-"]').not('[data-testid="Page-DropArea"]').eq(1).click();
cy.get('[title="Group"]').should('exist');
});

it('try to rename a page', () => {
cy.get('[data-testid^="Page-"]').not('[data-testid="Page-DropArea"]').eq(0).click();
cy.getByTestId('Label Expression').click().type('Page Rename{enter}');
cy.get('[data-testid^="Page-"]').not('[data-testid="Page-DropArea"]').first().should('have.text', 'Page Rename');
});

it('try to move a page', () => {
cy.get('[data-testid^="Page-"]').not('[data-testid="Page-DropArea"]').eq(0).click();
cy.getByTestId('Label Expression').click().type('Page 1{enter}');
const dataTransfer = new DataTransfer();
cy.getByTestId('FormDescriptionEditor-Page').trigger('dragstart', { dataTransfer });
cy.getByTestId('PageList-DropArea').trigger('drop', { dataTransfer });
cy.wait(500); // Wait for representation to refresh
cy.get('[data-testid^="Page-"]').not('[data-testid="Page-DropArea"]').should('have.lengthOf', 2);
cy.get('[data-testid^="Page-"]').not('[data-testid="Page-DropArea"]').eq(1).click();
cy.wait(500); // Wait for representation to refresh
cy.getByTestId('Label Expression').click().type('Page 2{enter}');
cy.get('[data-testid^="Page-"]').not('[data-testid="Page-DropArea"]').eq(0).should('have.text', 'Page 1');
cy.get('[data-testid^="Page-"]').not('[data-testid="Page-DropArea"]').eq(1).should('have.text', 'Page 2');
cy.get('[data-testid^="Page-"]').not('[data-testid="Page-DropArea"]').eq(0).trigger('dragstart', { dataTransfer });
cy.getByTestId('PageList-DropArea').trigger('drop', { dataTransfer });
cy.wait(500); // Wait for representation to refresh
cy.get('[data-testid^="Page-"]').not('[data-testid="Page-DropArea"]').eq(0).should('have.text', 'Page 2');
cy.get('[data-testid^="Page-"]').not('[data-testid="Page-DropArea"]').eq(1).should('have.text', 'Page 1');
});

it('try to delete a page', () => {
const dataTransfer = new DataTransfer();
cy.getByTestId('FormDescriptionEditor-Page').trigger('dragstart', { dataTransfer });
cy.getByTestId('PageList-DropArea').trigger('drop', { dataTransfer });
cy.wait(500); // Wait for representation to refresh
cy.get('[data-testid^="Page-"]').not('[data-testid="Page-DropArea"]').should('have.lengthOf', 2);
cy.get('[data-testid^="Page-"]').eq(0).click().type('{del}');
cy.wait(500); // Wait for representation to refresh
cy.get('[data-testid^="Page-"]').not('[data-testid="Page-DropArea"]').should('have.lengthOf', 1);
});

it('try to add group and widget to a page', () => {
const dataTransfer = new DataTransfer();
cy.getByTestId('FormDescriptionEditor-BarChart').trigger('dragstart', { dataTransfer });
cy.get('[data-testid^="Group-Widgets-DropArea-"]').eq(0).trigger('drop', { dataTransfer });
cy.getByTestId('BarChart').should('exist');
});

});
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2019, 2022 Obeo.
* Copyright (c) 2019, 2023 Obeo.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
Expand Down Expand Up @@ -97,7 +97,6 @@ public FormDescription getFormDescription() {
.targetObjectIdProvider(targetObjectIdProvider)
.canCreatePredicate(variableManager -> false)
.pageDescriptions(pageDescriptions)
.groupDescriptions(groupDescriptions)
.build();
// @formatter:on
}
Expand Down
Expand Up @@ -39,6 +39,7 @@
import org.eclipse.sirius.components.forms.description.FormDescription;
import org.eclipse.sirius.components.forms.description.RadioDescription;
import org.eclipse.sirius.components.forms.description.SelectDescription;
import org.eclipse.sirius.components.forms.description.TextareaDescription;
import org.eclipse.sirius.components.forms.description.TextfieldDescription;
import org.eclipse.sirius.components.forms.renderer.FormRenderer;
import org.eclipse.sirius.components.interpreter.AQLInterpreter;
Expand Down Expand Up @@ -210,23 +211,28 @@ private org.eclipse.sirius.properties.GroupDescription createGroupDescription()
private void checkResult(FormDescription description) {
// test SiriusViewExtensionDescriptionConverter
assertThat(description).isNotNull();
assertThat(description.getGroupDescriptions()).hasSize(1);
assertThat(description.getPageDescriptions()).hasSize(1);
assertThat(description.getPageDescriptions().get(0).getGroupDescriptions()).hasSize(1);
assertThat(description.getPageDescriptions()).hasSize(1);
assertThat(description.getPageDescriptions().get(0).getGroupDescriptions().get(0)).isEqualTo(description.getGroupDescriptions().get(0));
assertThat(description.getGroupDescriptions().stream().flatMap(g -> g.getControlDescriptions().stream())).hasSize(6);
assertThat(description.getGroupDescriptions().stream().flatMap(g -> g.getControlDescriptions().stream()).filter(CheckboxDescription.class::isInstance)).hasSize(1);
assertThat(description.getGroupDescriptions().stream().flatMap(g -> g.getControlDescriptions().stream()).filter(RadioDescription.class::isInstance)).hasSize(1);
assertThat(description.getGroupDescriptions().stream().flatMap(g -> g.getControlDescriptions().stream()).filter(SelectDescription.class::isInstance)).hasSize(1);
assertThat(description.getGroupDescriptions().stream().flatMap(g -> g.getControlDescriptions().stream()).filter(TextfieldDescription.class::isInstance)).hasSize(1);
assertThat(description.getGroupDescriptions().stream().flatMap(g -> g.getControlDescriptions().stream()).filter(TextfieldDescription.class::isInstance)).hasSize(1);
Optional<ForDescription> forOptional = description.getGroupDescriptions().stream().flatMap(g -> g.getControlDescriptions().stream()).filter(ForDescription.class::isInstance)
assertThat(description.getPageDescriptions().stream().flatMap(g -> g.getGroupDescriptions().stream()).flatMap(g -> g.getControlDescriptions().stream())).hasSize(6);
assertThat(description.getPageDescriptions().stream().flatMap(g -> g.getGroupDescriptions().stream()).flatMap(g -> g.getControlDescriptions().stream())
.filter(CheckboxDescription.class::isInstance)).hasSize(1);
assertThat(description.getPageDescriptions().stream().flatMap(g -> g.getGroupDescriptions().stream()).flatMap(g -> g.getControlDescriptions().stream())
.filter(RadioDescription.class::isInstance)).hasSize(1);
assertThat(description.getPageDescriptions().stream().flatMap(g -> g.getGroupDescriptions().stream()).flatMap(g -> g.getControlDescriptions().stream())
.filter(SelectDescription.class::isInstance)).hasSize(1);
assertThat(description.getPageDescriptions().stream().flatMap(g -> g.getGroupDescriptions().stream()).flatMap(g -> g.getControlDescriptions().stream())
.filter(TextfieldDescription.class::isInstance)).hasSize(1);
assertThat(description.getPageDescriptions().stream().flatMap(g -> g.getGroupDescriptions().stream()).flatMap(g -> g.getControlDescriptions().stream())
.filter(TextareaDescription.class::isInstance)).hasSize(1);
Optional<ForDescription> forOptional = description.getPageDescriptions()
.stream()
.flatMap(g -> g.getGroupDescriptions().stream())
.flatMap(g -> g.getControlDescriptions().stream())
.filter(ForDescription.class::isInstance)
.map(ForDescription.class::cast).findFirst();
assertThat(forOptional).isNotEmpty();
assertThat(forOptional.get().getIfDescriptions()).hasSize(1);
assertThat(forOptional.get().getIfDescriptions().stream().findFirst().get().getWidgetDescription()).isNotNull();

// Test FormRenderer
VariableManager variableManager = new VariableManager();
variableManager.put(VariableManager.SELF, List.of(EcorePackage.eINSTANCE));
Expand Down
Expand Up @@ -36,7 +36,7 @@
import org.springframework.stereotype.Service;

/**
* This class is used to convert a Sirius {@link ViewExtensionDescription} to an Sirius Web {@link FormDescription}.
* This class is used to convert a Sirius {@link ViewExtensionDescription} to a Sirius Web {@link FormDescription}.
*
* @author fbarbin
*/
Expand Down Expand Up @@ -72,13 +72,12 @@ public FormDescription convert(ViewExtensionDescription viewExtensionDescription
PageDescriptionConverter pageDescriptionConverter = new PageDescriptionConverter(interpreter, this.identifierProvider, this.semanticCandidatesProviderFactory);
GroupDescriptionConverter groupDescriptionConverter = new GroupDescriptionConverter(interpreter, this.objectService, this.identifierProvider, this.modelOperationHandlerSwitchProvider);

// @formatter:off
Map<org.eclipse.sirius.properties.GroupDescription, GroupDescription> siriusGroup2SiriusWebGroup = new HashMap<>();
List<GroupDescription> groupDescriptions = viewExtensionDescription.getCategories().stream()

viewExtensionDescription.getCategories().stream()
.flatMap(category -> category.getPages().stream())
.flatMap(page -> page.getGroups().stream())
.map(groupDescription -> groupDescriptionConverter.convert(groupDescription, siriusGroup2SiriusWebGroup))
.toList();
.forEach(groupDescription -> groupDescriptionConverter.convert(groupDescription, siriusGroup2SiriusWebGroup));

List<PageDescription> pageDescriptions = viewExtensionDescription.getCategories().stream()
.flatMap(category -> category.getPages().stream())
Expand Down Expand Up @@ -106,8 +105,6 @@ public FormDescription convert(ViewExtensionDescription viewExtensionDescription
.canCreatePredicate(variableManager -> false)
.targetObjectIdProvider(targetObjectIdProvider)
.pageDescriptions(pageDescriptions)
.groupDescriptions(groupDescriptions)
.build();
// @formatter:on
}
}
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2023, 2023 Obeo.
* Copyright (c) 2023 Obeo.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
Expand Down
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2022 Obeo.
* Copyright (c) 2022, 2023 Obeo.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
Expand Down Expand Up @@ -60,23 +60,19 @@ public FormDescriptionEditorCreationService(IRepresentationDescriptionSearchServ
this.representationPersistenceService = Objects.requireNonNull(representationPersistenceService);
this.objectService = Objects.requireNonNull(objectService);

// @formatter:off
this.timer = Timer.builder(Monitoring.REPRESENTATION_EVENT_PROCESSOR_REFRESH)
.tag(Monitoring.NAME, "formdescriptioneditor")
.register(meterRegistry);
// @formatter:on
}

@Override
public FormDescriptionEditor create(String label, Object targetObject, FormDescriptionEditorDescription formDescriptionEditorDescription, IEditingContext editingContext) {
// @formatter:off
FormDescriptionEditor newFormDescriptionEditor = FormDescriptionEditor.newFormDescriptionEditor(UUID.randomUUID().toString())
.label(label)
.targetObjectId(this.objectService.getId(targetObject))
.descriptionId(formDescriptionEditorDescription.getId())
.groups(List.of()) // We don't store form description editor groups, it will be re-render by the FormDescriptionEditorProcessor.
.pages(List.of()) // We don't store form description editor pages, it will be re-render by the FormDescriptionEditorProcessor.
.build();
// @formatter:on

this.representationPersistenceService.save(editingContext, newFormDescriptionEditor);

Expand All @@ -87,18 +83,15 @@ public FormDescriptionEditor create(String label, Object targetObject, FormDescr
public FormDescriptionEditor refresh(IEditingContext editingContext, IFormDescriptionEditorContext formDescriptionEditorContext) {
FormDescriptionEditor previousFormDescriptionEditor = formDescriptionEditorContext.getFormDescriptionEditor();
var optionalObject = this.objectService.getObject(editingContext, previousFormDescriptionEditor.getTargetObjectId());
// @formatter:off
var optionalFormDescriptionEditorDescription = this.representationDescriptionSearchService.findById(editingContext, previousFormDescriptionEditor.getDescriptionId())
.filter(FormDescriptionEditorDescription.class::isInstance)
.map(FormDescriptionEditorDescription.class::cast);
// @formatter:on

if (optionalObject.isPresent() && optionalFormDescriptionEditorDescription.isPresent()) {
Object object = optionalObject.get();
FormDescriptionEditorDescription formDescriptionEditorDescription = optionalFormDescriptionEditorDescription.get();
FormDescriptionEditor formDescriptionEditor = this.doRender(previousFormDescriptionEditor.getLabel(), object, editingContext, formDescriptionEditorDescription,
return this.doRender(previousFormDescriptionEditor.getLabel(), object, editingContext, formDescriptionEditorDescription,
Optional.of(formDescriptionEditorContext));
return formDescriptionEditor;
}

return null;
Expand Down
Expand Up @@ -21,5 +21,6 @@
*
* @author arichard
*/
public record AddGroupInput(UUID id, String editingContextId, String representationId, int index) implements IFormDescriptionEditorInput {
public record AddGroupInput(UUID id, String editingContextId, String representationId, String pageId, int index) implements IFormDescriptionEditorInput {

}

0 comments on commit 9f32b5a

Please sign in to comment.