diff --git a/CHANGES.md b/CHANGES.md index eb5fa43d1..d4e659d92 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,6 +11,7 @@ ### :sparkles: New features - Fix clickable area of Penptot logo in the viewer [Taiga #7988](https://tree.taiga.io/project/penpot/issue/7988) +- Fix constraints dropdown when selecting multiple shapes [Taiga #7686](https://tree.taiga.io/project/penpot/issue/7686) - Improve auth process [Taiga #7094](https://tree.taiga.io/project/penpot/us/7094) - Add locking degrees increment (hold shift) on path edition [Taiga #7761](https://tree.taiga.io/project/penpot/issue/7761) - Persistence & Concurrent Edition Enhancements [Taiga #5657](https://tree.taiga.io/project/penpot/us/5657) diff --git a/frontend/playwright/data/design/get-file-fragment-multiple-constraints.json b/frontend/playwright/data/design/get-file-fragment-multiple-constraints.json new file mode 100644 index 000000000..1a055d7d1 --- /dev/null +++ b/frontend/playwright/data/design/get-file-fragment-multiple-constraints.json @@ -0,0 +1,363 @@ +{ + "~:id": "~u03bff843-920f-81a1-8004-7563acdc8ca1", + "~:file-id": "~u03bff843-920f-81a1-8004-756365e1eb6a", + "~:created-at": "~m1717592543081", + "~:content": { + "~:options": {}, + "~:objects": { + "~u00000000-0000-0000-0000-000000000000": { + "~#shape": { + "~:y": 0, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:name": "Root Frame", + "~:width": 0.01, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 0, + "~:y": 0 + } + }, + { + "~#point": { + "~:x": 0.01, + "~:y": 0 + } + }, + { + "~#point": { + "~:x": 0.01, + "~:y": 0.01 + } + }, + { + "~#point": { + "~:x": 0, + "~:y": 0.01 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:id": "~u00000000-0000-0000-0000-000000000000", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 0, + "~:proportion": 1.0, + "~:selrect": { + "~#rect": { + "~:x": 0, + "~:y": 0, + "~:width": 0.01, + "~:height": 0.01, + "~:x1": 0, + "~:y1": 0, + "~:x2": 0.01, + "~:y2": 0.01 + } + }, + "~:fills": [ + { + "~:fill-color": "#FFFFFF", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 0.01, + "~:flip-y": null, + "~:shapes": [ + "~ub574c052-1a31-80bb-8004-75636879759b" + ] + } + }, + "~ub574c052-1a31-80bb-8004-75636879759b": { + "~#shape": { + "~:y": 128, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": false, + "~:name": "Board", + "~:width": 256, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 128, + "~:y": 128 + } + }, + { + "~#point": { + "~:x": 384, + "~:y": 128 + } + }, + { + "~#point": { + "~:x": 384, + "~:y": 384 + } + }, + { + "~#point": { + "~:x": 128, + "~:y": 384 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:id": "~ub574c052-1a31-80bb-8004-75636879759b", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 128, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 128, + "~:y": 128, + "~:width": 256, + "~:height": 256, + "~:x1": 128, + "~:y1": 128, + "~:x2": 384, + "~:y2": 384 + } + }, + "~:fills": [ + { + "~:fill-color": "#FFFFFF", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 256, + "~:flip-y": null, + "~:shapes": [ + "~ub574c052-1a31-80bb-8004-75636a9b8205", + "~ub574c052-1a31-80bb-8004-756392461069" + ] + } + }, + "~ub574c052-1a31-80bb-8004-75636a9b8205": { + "~#shape": { + "~:y": 136, + "~:rx": 0, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": false, + "~:name": "Rectangle", + "~:width": 64, + "~:type": "~:rect", + "~:points": [ + { + "~#point": { + "~:x": 136, + "~:y": 136 + } + }, + { + "~#point": { + "~:x": 200, + "~:y": 136 + } + }, + { + "~#point": { + "~:x": 200, + "~:y": 199.99999999999997 + } + }, + { + "~#point": { + "~:x": 136, + "~:y": 199.99999999999997 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:id": "~ub574c052-1a31-80bb-8004-75636a9b8205", + "~:parent-id": "~ub574c052-1a31-80bb-8004-75636879759b", + "~:frame-id": "~ub574c052-1a31-80bb-8004-75636879759b", + "~:strokes": [], + "~:x": 136, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 136, + "~:y": 136, + "~:width": 64, + "~:height": 63.99999999999997, + "~:x1": 136, + "~:y1": 136, + "~:x2": 200, + "~:y2": 199.99999999999997 + } + }, + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:ry": 0, + "~:height": 63.99999999999997, + "~:flip-y": null + } + }, + "~ub574c052-1a31-80bb-8004-756392461069": { + "~#shape": { + "~:y": 136, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": false, + "~:name": "Ellipse", + "~:width": 64, + "~:type": "~:circle", + "~:points": [ + { + "~#point": { + "~:x": 256, + "~:y": 136 + } + }, + { + "~#point": { + "~:x": 320, + "~:y": 136 + } + }, + { + "~#point": { + "~:x": 320, + "~:y": 200 + } + }, + { + "~#point": { + "~:x": 256, + "~:y": 200 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:constraints-v": "~:bottom", + "~:constraints-h": "~:right", + "~:id": "~ub574c052-1a31-80bb-8004-756392461069", + "~:parent-id": "~ub574c052-1a31-80bb-8004-75636879759b", + "~:frame-id": "~ub574c052-1a31-80bb-8004-75636879759b", + "~:strokes": [], + "~:x": 256, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 256, + "~:y": 136, + "~:width": 64, + "~:height": 64, + "~:x1": 256, + "~:y1": 136, + "~:x2": 320, + "~:y2": 200 + } + }, + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 64, + "~:flip-y": null + } + } + }, + "~:id": "~u03bff843-920f-81a1-8004-756365e1eb6b", + "~:name": "Page 1" + } +} \ No newline at end of file diff --git a/frontend/playwright/data/design/get-file-multiple-constraints.json b/frontend/playwright/data/design/get-file-multiple-constraints.json new file mode 100644 index 000000000..76edd0baf --- /dev/null +++ b/frontend/playwright/data/design/get-file-multiple-constraints.json @@ -0,0 +1,49 @@ +{ + "~:features": { + "~#set": [ + "layout/grid", + "styles/v2", + "fdata/pointer-map", + "fdata/objects-map", + "components/v2", + "fdata/shape-data-type" + ] + }, + "~:permissions": { + "~:type": "~:membership", + "~:is-owner": true, + "~:is-admin": true, + "~:can-edit": true, + "~:can-read": true, + "~:is-logged": true + }, + "~:has-media-trimmed": false, + "~:comment-thread-seqn": 0, + "~:name": "New File 2", + "~:revn": 9, + "~:modified-at": "~m1717592543083", + "~:id": "~u03bff843-920f-81a1-8004-756365e1eb6a", + "~:is-shared": false, + "~:version": 48, + "~:project-id": "~u0515a066-e303-8169-8004-73eb401b5d55", + "~:created-at": "~m1717592470408", + "~:data": { + "~:pages": [ + "~u03bff843-920f-81a1-8004-756365e1eb6b" + ], + "~:pages-index": { + "~u03bff843-920f-81a1-8004-756365e1eb6b": { + "~#penpot/pointer": [ + "~u03bff843-920f-81a1-8004-7563acdc8ca1", + { + "~:created-at": "~m1717592543090" + } + ] + } + }, + "~:id": "~u03bff843-920f-81a1-8004-756365e1eb6a", + "~:options": { + "~:components-v2": true + } + } +} \ No newline at end of file diff --git a/frontend/playwright/data/design/get-file-object-thumbnails-multiple-constraints.json b/frontend/playwright/data/design/get-file-object-thumbnails-multiple-constraints.json new file mode 100644 index 000000000..f3a0d6d27 --- /dev/null +++ b/frontend/playwright/data/design/get-file-object-thumbnails-multiple-constraints.json @@ -0,0 +1,3 @@ +{ + "03bff843-920f-81a1-8004-756365e1eb6a/03bff843-920f-81a1-8004-756365e1eb6b/b574c052-1a31-80bb-8004-75636879759b/frame": "http://localhost:3449/assets/by-id/bdc9e592-f685-4b08-9a44-127ce20efee6" +} \ No newline at end of file diff --git a/frontend/playwright/ui/pages/WorkspacePage.js b/frontend/playwright/ui/pages/WorkspacePage.js index 5bcfa5f9f..d66447253 100644 --- a/frontend/playwright/ui/pages/WorkspacePage.js +++ b/frontend/playwright/ui/pages/WorkspacePage.js @@ -45,12 +45,11 @@ export class WorkspacePage extends BaseWebSocketPage { this.rootShape = page.locator(`[id="shape-00000000-0000-0000-0000-000000000000"]`); this.rectShapeButton = page.getByRole("button", { name: "Rectangle (R)" }); this.colorpicker = page.getByTestId("colorpicker"); + this.layers = page.getByTestId("layers"); } - async goToWorkspace() { - await this.page.goto( - `/#/workspace/${WorkspacePage.anyProjectId}/${WorkspacePage.anyFileId}?page-id=${WorkspacePage.anyPageId}`, - ); + async goToWorkspace({ fileId = WorkspacePage.anyFileId, pageId = WorkspacePage.anyPageId } = {}) { + await this.page.goto(`/#/workspace/${WorkspacePage.anyProjectId}/${fileId}?page-id=${pageId}`); this.#ws = await this.waitForNotificationsWebSocket(); await this.#ws.mockOpen(); @@ -97,4 +96,14 @@ export class WorkspacePage extends BaseWebSocketPage { await this.viewport.hover({ position: { x: x + width, y: y + height } }); await this.page.mouse.up(); } + + async clickLeafLayer(name, clickOptions = {}) { + const layer = this.layers.getByText(name); + await layer.click(clickOptions); + } + + async clickToggableLayer(name, clickOptions = {}) { + const layer = this.layers.getByTestId("layer-item").filter({ has: this.page.getByText(name) }); + await layer.getByRole("button").click(clickOptions); + } } diff --git a/frontend/playwright/ui/specs/design-tab.spec.js b/frontend/playwright/ui/specs/design-tab.spec.js new file mode 100644 index 000000000..0d637bace --- /dev/null +++ b/frontend/playwright/ui/specs/design-tab.spec.js @@ -0,0 +1,46 @@ +import { test, expect } from "@playwright/test"; +import { WorkspacePage } from "../pages/WorkspacePage"; + +test.beforeEach(async ({ page }) => { + await WorkspacePage.init(page); +}); + +const multipleConstraintsFileId = `03bff843-920f-81a1-8004-756365e1eb6a`; +const multipleConstraintsPageId = `03bff843-920f-81a1-8004-756365e1eb6b`; + +const setupFileWithMultipeConstraints = async (workspace) => { + await workspace.setupEmptyFile(); + await workspace.mockRPC(/get\-file\?/, "design/get-file-multiple-constraints.json"); + await workspace.mockRPC( + "get-file-object-thumbnails?file-id=*", + "design/get-file-object-thumbnails-multiple-constraints.json", + ); + await workspace.mockRPC( + "get-file-fragment?file-id=*", + "design/get-file-fragment-multiple-constraints.json", + ); +}; + +test.describe("Constraints", () => { + test("Constraint dropdown shows 'Mixed' when multiple layers are selected with different constraints", async ({ + page, + }) => { + const workspace = new WorkspacePage(page); + await setupFileWithMultipeConstraints(workspace); + await workspace.goToWorkspace({ + fileId: multipleConstraintsFileId, + pageId: multipleConstraintsPageId, + }); + + await workspace.clickToggableLayer("Board"); + await workspace.clickLeafLayer("Ellipse"); + await workspace.clickLeafLayer("Rectangle", { modifiers: ["Shift"] }); + + const constraintVDropdown = workspace.page.getByTestId("constraint-v-select"); + await expect(constraintVDropdown).toContainText("Mixed"); + const constraintHDropdown = workspace.page.getByTestId("constraint-h-select"); + await expect(constraintHDropdown).toContainText("Mixed"); + + expect(false); + }); +}); diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs index afe62a2b1..737f80fdd 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs @@ -69,7 +69,7 @@ highlighted (mf/deref refs/highlighted-shapes) highlighted (hooks/use-equal-memo highlighted) root (get objects uuid/zero)] - [:div {:class (stl/css :element-list)} + [:div {:class (stl/css :element-list) :data-testid "layer-item"} [:& hooks/sortable-container {} (for [[index id] (reverse (d/enumerate (:shapes root)))] (when-let [obj (get objects id)] @@ -510,7 +510,7 @@ (mf/use-fn #(st/emit! (dw/toggle-focus-mode)))] - [:div#layers {:class (stl/css :layers)} + [:div#layers {:class (stl/css :layers) :data-testid "layers"} (if (d/not-empty? focus) [:div {:class (stl/css :tool-window-bar)} [:button {:class (stl/css :focus-title) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs index c669ad445..f9ec1f889 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/constraints.cljs @@ -207,14 +207,14 @@ :on-click on-constraint-button-clicked} [:span {:class (stl/css :resalted-area)}]]]] [:div {:class (stl/css :contraints-selects)} - [:div {:class (stl/css :horizontal-select)} + [:div {:class (stl/css :horizontal-select) :data-testid "constraint-h-select"} [:& select - {:default-value (d/nilv (d/name constraints-h) "scale") + {:default-value (if (not= constraints-h :multiple) (d/nilv (d/name constraints-h) "scale") "") :options options-h :on-change on-constraint-h-select-changed}]] - [:div {:class (stl/css :vertical-select)} + [:div {:class (stl/css :vertical-select) :data-testid "constraint-v-select"} [:& select - {:default-value (d/nilv (d/name constraints-v) "scale") + {:default-value (if (not= constraints-v :multiple) (d/nilv (d/name constraints-v) "scale") "") :options options-v :on-change on-constraint-v-select-changed}]] (when first-level?