From 2b497d98ad3823b0430dd8e664c0a04c7a70172e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20ROU=C3=8BN=C3=89?= Date: Thu, 13 Jun 2024 15:35:51 +0200 Subject: [PATCH] [3624] Fix issue with header separator not spanning entire node width MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug: https://github.com/eclipse-sirius/sirius-web/issues/3624 Signed-off-by: Florian ROUËNÉ --- CHANGELOG.adoc | 3 +- .../e2e/project/diagrams/collapse.cy.ts | 4 +- .../e2e/project/diagrams/diagram-label.cy.ts | 150 ++++++++++++++++++ .../src/converter/IconLabelNodeConverter.ts | 2 + .../src/converter/convertLabel.ts | 14 +- .../src/renderer/DiagramRenderer.types.ts | 4 + .../src/renderer/Label.tsx | 43 ++++- .../layout/FreeFormNodeLayoutHandler.ts | 7 +- .../src/renderer/layout/layoutNode.ts | 8 +- .../src/nodes/EllipseNodeLayoutHandler.ts | 2 +- 10 files changed, 217 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 031800075a..314887f3cc 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -57,6 +57,7 @@ More existing APIs will be migrated to this new common pattern. - https://github.com/eclipse-sirius/sirius-web/issues/3611[#3611] [diagram] Fix missing creation tool image in the contextual palette - https://github.com/eclipse-sirius/sirius-web/issues/3628[#3628] [sirius-web] Restore support for expand all and reveal in the explorer - https://github.com/eclipse-sirius/sirius-web/issues/3616[#3616] [diagram] Fix potential exceptions due to duplicate keys in diagram event processing +- https://github.com/eclipse-sirius/sirius-web/issues/3624[#3624] [diagram] Fix an issue where the header separator does not fill the entire width of the node === New Features @@ -94,7 +95,7 @@ image:doc/screenshots/diagramFilterView.png[Diagram Filter View, 70%] image:doc/screenshots/insideLabelPositions.png[Inside label positions, 70%] - https://github.com/eclipse-sirius/sirius-web/issues/3574[#3574] [diagram] Add a loading indicator during execution of an arrange-all - https://github.com/eclipse-sirius/sirius-web/issues/3586[#3586] [diagram] Fix an issue with keypress listener attached to the palette -- https://github.com/eclipse-sirius/sirius-web/issues/3623[#3623] [form] Remove unused form payload +- https://github.com/eclipse-sirius/sirius-web/issues/3623[#3623] [form] Remove unused form payload - https://github.com/eclipse-sirius/sirius-web/issues/3627[#3627] [form] Remove unused mutation in form - https://github.com/eclipse-sirius/sirius-web/issues/3606[#3606] [test] Improve error handling in ExecuteEditingContextFunctionRunner and ExecuteEditingContextFunctionEventHandler diff --git a/integration-tests/cypress/e2e/project/diagrams/collapse.cy.ts b/integration-tests/cypress/e2e/project/diagrams/collapse.cy.ts index 4150996849..1950f3e925 100644 --- a/integration-tests/cypress/e2e/project/diagrams/collapse.cy.ts +++ b/integration-tests/cypress/e2e/project/diagrams/collapse.cy.ts @@ -86,14 +86,14 @@ describe('Diagram - collapsible node', () => { diagram.fitToScreen(); diagram .getNodes('diagram', 'Entity1') - .findByTestId('Label - Entity1') + .findByTestId('Label bottom separator - Entity1') .should('have.css', 'border-bottom', '1px solid rgb(51, 176, 195)'); diagram.getNodes('diagram', 'Entity1').findByTestId('Label - Entity1').click(); diagram.getPalette().should('exist'); diagram.getPalette().findByTestId('Collapse - Tool').click(); diagram .getNodes('diagram', 'Entity1') - .findByTestId('Label - Entity1') + .findByTestId('Label bottom separator - Entity1') .should('have.css', 'border-bottom-width', '0px'); }); }); diff --git a/integration-tests/cypress/e2e/project/diagrams/diagram-label.cy.ts b/integration-tests/cypress/e2e/project/diagrams/diagram-label.cy.ts index 4df07e066f..10d5926661 100644 --- a/integration-tests/cypress/e2e/project/diagrams/diagram-label.cy.ts +++ b/integration-tests/cypress/e2e/project/diagrams/diagram-label.cy.ts @@ -402,4 +402,154 @@ describe('Diagram - inside outside labels', () => { }); }); }); + context('Given a studio with a top header label node', () => { + let studioProjectId: string = ''; + let domainName: string = ''; + + before(() => { + cy.createProjectFromTemplate('studio-template').then((res) => { + const payload = res.body.data.createProjectFromTemplate; + if (isCreateProjectFromTemplateSuccessPayload(payload)) { + const projectId = payload.project.id; + studioProjectId = projectId; + + const project = new Project(); + project.visit(projectId); + project.disableDeletionConfirmationDialog(); + + const explorer = new Explorer(); + const details = new Details(); + explorer.expand('DomainNewModel'); + cy.get('[title="domain::Domain"]').then(($div) => { + domainName = $div.data().testid; + explorer.expand(domainName); + explorer.createObject('Entity1', 'Relation'); + details.getCheckBox('Containment').check(); + details.openReferenceWidgetOptions('Target Type'); + details.selectReferenceWidgetOption('Entity2'); + explorer.expand('ViewNewModel'); + explorer.expand('View'); + explorer.expand(`${domainName} Diagram Description`); + explorer.select('Entity1 Node'); + details.getCheckBox('Collapsible').check(); + details.openReferenceWidgetOptions('Reused Child Node Descriptions'); + details.selectReferenceWidgetOption('Entity2 Node'); + explorer.expand('Entity1 Node'); + explorer.select('aql:self.name'); + details.getRadioOption('Position', 'TOP_LEFT').click(); + explorer.expand('aql:self.name'); + explorer.select('InsideLabelStyle'); + details.getCheckBox('With Header').check(); + details.getCheckBox('Display Header Separator').check(); + }); + } + }); + }); + + after(() => cy.deleteProject(studioProjectId)); + context('When we create a new instance project', () => { + let instanceProjectId: string = ''; + + beforeEach(() => { + const studio = new Studio(); + studio.createProjectFromDomain('Cypress - Studio Instance', domainName, 'Root').then((res) => { + instanceProjectId = res.projectId; + }); + }); + + afterEach(() => cy.deleteProject(instanceProjectId)); + + it('Then the separator is display under the label', () => { + const explorer = new Explorer(); + const diagram = new Diagram(); + const details = new Details(); + explorer.createObject('Root', 'Entity1s Entity1'); + explorer.createObject('Entity1', 'Relation Entity2'); + explorer.select('Entity1'); + details.getTextField('Name').type('Entity1{enter}'); + explorer.createRepresentation('Root', `${domainName} Diagram Description`, 'diagram'); + diagram.fitToScreen(); + diagram + .getNodes('diagram', 'Entity1') + .findByTestId('Label bottom separator - Entity1') + .should('have.css', 'border-bottom', '1px solid rgb(51, 176, 195)'); + diagram.getNodes('diagram', 'Entity1').findByTestId('Label top separator - Entity1').should('not.exist'); + }); + }); + }); + context('Given a studio with a bottom header label node', () => { + let studioProjectId: string = ''; + let domainName: string = ''; + + before(() => { + cy.createProjectFromTemplate('studio-template').then((res) => { + const payload = res.body.data.createProjectFromTemplate; + if (isCreateProjectFromTemplateSuccessPayload(payload)) { + const projectId = payload.project.id; + studioProjectId = projectId; + + const project = new Project(); + project.visit(projectId); + project.disableDeletionConfirmationDialog(); + + const explorer = new Explorer(); + const details = new Details(); + explorer.expand('DomainNewModel'); + cy.get('[title="domain::Domain"]').then(($div) => { + domainName = $div.data().testid; + explorer.expand(domainName); + explorer.createObject('Entity1', 'Relation'); + details.getCheckBox('Containment').check(); + details.openReferenceWidgetOptions('Target Type'); + details.selectReferenceWidgetOption('Entity2'); + explorer.expand('ViewNewModel'); + explorer.expand('View'); + explorer.expand(`${domainName} Diagram Description`); + explorer.select('Entity1 Node'); + details.getCheckBox('Collapsible').check(); + details.openReferenceWidgetOptions('Reused Child Node Descriptions'); + details.selectReferenceWidgetOption('Entity2 Node'); + explorer.expand('Entity1 Node'); + explorer.select('aql:self.name'); + details.getRadioOption('Position', 'BOTTOM_RIGHT').click(); + explorer.expand('aql:self.name'); + explorer.select('InsideLabelStyle'); + details.getCheckBox('With Header').check(); + details.getCheckBox('Display Header Separator').check(); + }); + } + }); + }); + + after(() => cy.deleteProject(studioProjectId)); + context('When we create a new instance project', () => { + let instanceProjectId: string = ''; + + beforeEach(() => { + const studio = new Studio(); + studio.createProjectFromDomain('Cypress - Studio Instance', domainName, 'Root').then((res) => { + instanceProjectId = res.projectId; + }); + }); + + afterEach(() => cy.deleteProject(instanceProjectId)); + + it('Then the separator is display under the label', () => { + const explorer = new Explorer(); + const diagram = new Diagram(); + const details = new Details(); + explorer.createObject('Root', 'Entity1s Entity1'); + explorer.createObject('Entity1', 'Relation Entity2'); + explorer.select('Entity1'); + details.getTextField('Name').type('Entity1{enter}'); + explorer.createRepresentation('Root', `${domainName} Diagram Description`, 'diagram'); + diagram.fitToScreen(); + diagram + .getNodes('diagram', 'Entity1') + .findByTestId('Label top separator - Entity1') + .should('have.css', 'border-top', '1px solid rgb(51, 176, 195)'); + diagram.getNodes('diagram', 'Entity1').findByTestId('Label bottom separator - Entity1').should('not.exist'); + }); + }); + }); }); diff --git a/packages/diagrams/frontend/sirius-components-diagrams/src/converter/IconLabelNodeConverter.ts b/packages/diagrams/frontend/sirius-components-diagrams/src/converter/IconLabelNodeConverter.ts index ce487101b9..2aa76e1197 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams/src/converter/IconLabelNodeConverter.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams/src/converter/IconLabelNodeConverter.ts @@ -99,6 +99,8 @@ const toIconLabelNode = ( isHeader: insideLabel.isHeader, displayHeaderSeparator: insideLabel.displayHeaderSeparator, overflowStrategy: insideLabel.overflowStrategy, + headerSeparatorStyle: {}, + headerPosition: undefined, }; } diff --git a/packages/diagrams/frontend/sirius-components-diagrams/src/converter/convertLabel.ts b/packages/diagrams/frontend/sirius-components-diagrams/src/converter/convertLabel.ts index df72189c3c..97f338d66f 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams/src/converter/convertLabel.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams/src/converter/convertLabel.ts @@ -45,19 +45,29 @@ export const convertInsideLabel = ( }, iconURL: labelStyle.iconURL, overflowStrategy: gqlInsideLabel.overflowStrategy, + headerSeparatorStyle: { + width: '100%', + }, + headerPosition: undefined, }; const alignement = AlignmentMap[gqlInsideLabel.insideLabelLocation]; if (alignement && alignement.isPrimaryVerticalAlignment) { if (alignement.primaryAlignment === 'TOP') { + if (insideLabel.isHeader) { + insideLabel.headerPosition = 'TOP'; + } if (insideLabel.displayHeaderSeparator && hasVisibleChild) { - insideLabel.style.borderBottom = borderStyle; + insideLabel.headerSeparatorStyle.borderBottom = borderStyle; } data.style = { ...data.style, display: 'flex', flexDirection: 'column', justifyContent: 'flex-start' }; } if (alignement.primaryAlignment === 'BOTTOM') { + if (insideLabel.isHeader) { + insideLabel.headerPosition = 'BOTTOM'; + } if (insideLabel.displayHeaderSeparator && hasVisibleChild) { - insideLabel.style.borderTop = borderStyle; + insideLabel.headerSeparatorStyle.borderTop = borderStyle; } data.style = { ...data.style, display: 'flex', flexDirection: 'column', justifyContent: 'flex-end' }; } diff --git a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/DiagramRenderer.types.ts b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/DiagramRenderer.types.ts index 8b9c1e29b4..433370326a 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/DiagramRenderer.types.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/DiagramRenderer.types.ts @@ -88,8 +88,12 @@ export interface InsideLabel { isHeader: boolean; displayHeaderSeparator: boolean; overflowStrategy: LabelOverflowStrategy; + headerSeparatorStyle: React.CSSProperties; + headerPosition: HeaderPosition | undefined; } +export type HeaderPosition = 'TOP' | 'BOTTOM'; + export type LabelOverflowStrategy = 'NONE' | 'WRAP' | 'ELLIPSIS'; export interface EdgeLabel { diff --git a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/Label.tsx b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/Label.tsx index ce48dd5907..1ebd814b0a 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/Label.tsx +++ b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/Label.tsx @@ -28,6 +28,27 @@ const getOverflowStrategy = (label: EdgeLabel | InsideLabel | OutsideLabel): Lab return undefined; }; +const isDisplayTopHeaderSeparator = (label: EdgeLabel | InsideLabel | OutsideLabel): boolean => { + if ('displayHeaderSeparator' in label) { + return label.displayHeaderSeparator && label.headerPosition === 'BOTTOM'; + } + return false; +}; + +const isDisplayBottomHeaderSeparator = (label: EdgeLabel | InsideLabel | OutsideLabel): boolean => { + if ('displayHeaderSeparator' in label) { + return label.displayHeaderSeparator && label.headerPosition === 'TOP'; + } + return false; +}; + +const getHeaderSeparatorStyle = (label: EdgeLabel | InsideLabel | OutsideLabel): React.CSSProperties | undefined => { + if ('headerSeparatorStyle' in label) { + return label.headerSeparatorStyle; + } + return undefined; +}; + const labelStyle = ( theme: Theme, style: React.CSSProperties, @@ -109,12 +130,20 @@ export const Label = memo(({ diagramElementId, label, faded, transform }: LabelP ); return ( -
- {content} -
+ <> + {isDisplayTopHeaderSeparator(label) && ( +
+ )} +
+ {content} +
+ {isDisplayBottomHeaderSeparator(label) && ( +
+ )} + ); }); diff --git a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/layout/FreeFormNodeLayoutHandler.ts b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/layout/FreeFormNodeLayoutHandler.ts index e8c4739550..55c3ba5f51 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/layout/FreeFormNodeLayoutHandler.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams/src/renderer/layout/FreeFormNodeLayoutHandler.ts @@ -84,8 +84,6 @@ export class FreeFormNodeLayoutHandler implements INodeLayoutHandler node.data.isBorderNode); const directNodesChildren = directChildren.filter((child) => !child.data.isBorderNode); @@ -95,7 +93,7 @@ export class FreeFormNodeLayoutHandler implements INodeLayoutHandler previouseNode.id === child.id); const previousPosition = computePreviousPosition(previousNode, child); const createdNode = newlyAddedNode?.id === child.id ? newlyAddedNode : undefined; - const headerHeightFootprint = getHeaderHeightFootprint(labelElement, withHeader, displayHeaderSeparator); + const headerHeightFootprint = getHeaderHeightFootprint(labelElement, node.data.insideLabel, 'TOP'); if (!!createdNode) { child.position = createdNode.position; @@ -128,6 +126,7 @@ export class FreeFormNodeLayoutHandler implements INodeLayoutHandler { let headerHeightFootprint = 0; + const withHeader: boolean = insideLabel?.isHeader ?? false; + const displayHeaderSeparator: boolean = insideLabel?.displayHeaderSeparator ?? false; if (!labelElement) { return headerHeightFootprint; } - if (withHeader) { + if (withHeader && insideLabel?.headerPosition === headerPosition) { headerHeightFootprint = labelElement.getBoundingClientRect().height; if (displayHeaderSeparator) { headerHeightFootprint += rectangularNodePadding; diff --git a/packages/sirius-web/frontend/sirius-web/src/nodes/EllipseNodeLayoutHandler.ts b/packages/sirius-web/frontend/sirius-web/src/nodes/EllipseNodeLayoutHandler.ts index edfc43d4fe..b94d339cdc 100644 --- a/packages/sirius-web/frontend/sirius-web/src/nodes/EllipseNodeLayoutHandler.ts +++ b/packages/sirius-web/frontend/sirius-web/src/nodes/EllipseNodeLayoutHandler.ts @@ -78,7 +78,7 @@ export class EllipseNodeLayoutHandler implements INodeLayoutHandler { const previousNode = (previousDiagram?.nodes ?? []).find((previouseNode) => previouseNode.id === child.id); const previousPosition = computePreviousPosition(previousNode, child); const createdNode = newlyAddedNode?.id === child.id ? newlyAddedNode : undefined; - const headerHeightFootprint = labelElement ? getHeaderHeightFootprint(labelElement, false, false) : 0; + const headerHeightFootprint = labelElement ? getHeaderHeightFootprint(labelElement, null, null) : 0; if (!!createdNode) { child.position = createdNode.position;