diff --git a/CHANGES.md b/CHANGES.md index ac5a0a54d7..fb4766f636 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -26,9 +26,13 @@ - Allow library colors as recent colors [Taiga #7640](https://tree.taiga.io/project/penpot/issue/7640) - Missing scroll in viewmode comments [Taiga #7427](https://tree.taiga.io/project/penpot/issue/7427) - Comments in View mode should mimic the positioning behavior of the Workspace [Taiga #7346](https://tree.taiga.io/project/penpot/issue/7346) +- Misaligned input on comments [Taiga #7461](https://tree.taiga.io/project/penpot/issue/7461) ### :bug: Bugs fixed +- Fix selection rectangle appears on scroll [Taiga #7525](https://tree.taiga.io/project/penpot/issue/7525) +- Fix layer tree not expanding to the bottom edge [Taiga #7466](https://tree.taiga.io/project/penpot/issue/7466) +- Fix guides move when board is moved by inputs [Taiga #8010](https://tree.taiga.io/project/penpot/issue/8010) - 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) - Layout and scrollign fixes for the bottom palette [Taiga #7559](https://tree.taiga.io/project/penpot/issue/7559) @@ -43,7 +47,12 @@ - Fix "Attribute overrides in copies are not exported in zip file" [Taiga #8072](https://tree.taiga.io/project/penpot/issue/8072) - Fix group not automatically selected in the Layers panel after creation [Taiga #8078](https://tree.taiga.io/project/penpot/issue/8078) - Fix export boards loses opacity [Taiga #7592](https://tree.taiga.io/project/penpot/issue/7592) +- Fix change color on imported svg also changes the stroke alignment[Taiga #7673](https://github.com/penpot/penpot/pull/7673) - Fix show in view mode and interactions workflow [Taiga #4711](https://github.com/penpot/penpot/pull/4711) +- Fix internal error when I set up a stroke for some objects without and with stroke [Taiga #7558](https://tree.taiga.io/project/penpot/issue/7558) +- Toolbar keeps toggling on and off on spacebar press [Taiga #7654](https://github.com/penpot/penpot/pull/7654) +- Fix toolbar keeps hiding when click outside workspace [Taiga #7776](https://tree.taiga.io/project/penpot/issue/7776) +- Fix open overlay relative to a frame [Taiga #7563](https://tree.taiga.io/project/penpot/issue/7563) ## 2.0.3 diff --git a/THANKYOU.md b/THANKYOU.md index 77a1483aab..8c075112b3 100644 --- a/THANKYOU.md +++ b/THANKYOU.md @@ -2,13 +2,19 @@ We want to thank to the amazing people that help us! Thank you! You're the best! +Feel free you make a PR updating this file if you miss you in the +list. + ## Security + * Husnain Iqbal (CEO OF ALPHA INFERNO PVT LTD) * [Shiraz Ali Khan](https://www.linkedin.com/in/shiraz-ali-khan-1ba508180/) * Vaibhav Shukla * Hassan Ahmed (Alias Xen Lee) +* Michal Biesiada (@mbiesiad) ## Internationalization + * [00ff88](https://hosted.weblate.org/user/00ff88) * [AhmadHB](https://hosted.weblate.org/user/AhmadHB) * [Aimee](https://hosted.weblate.org/user/Aimee) @@ -90,6 +96,7 @@ We want to thank to the amazing people that help us! Thank you! You're the best! * [zcraber](https://hosted.weblate.org/user/zcraber) ## Libraries & templates + * systxema * plumilla * victor crespo diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj index 1ca6373968..aed7806946 100644 --- a/backend/src/app/config.clj +++ b/backend/src/app/config.clj @@ -87,7 +87,10 @@ :ldap-attrs-fullname "cn" ;; a server prop key where initial project is stored. - :initial-project-skey "initial-project"}) + :initial-project-skey "initial-project" + + ;; time to avoid email sending after profile modification + :email-verify-threshold "15m"}) (s/def ::default-rpc-rlimit ::us/vector-of-strings) (s/def ::rpc-rlimit-config ::fs/path) @@ -213,6 +216,7 @@ (s/def ::telemetry-uri ::us/string) (s/def ::telemetry-with-taiga ::us/boolean) (s/def ::tenant ::us/string) +(s/def ::email-verify-threshold ::dt/duration) (s/def ::config (s/keys :opt-un [::secret-key @@ -334,7 +338,8 @@ ::telemetry-uri ::telemetry-referer ::telemetry-with-taiga - ::tenant])) + ::tenant + ::email-verify-threshold])) (def default-flags [:enable-backend-api-doc diff --git a/backend/src/app/rpc/commands/auth.clj b/backend/src/app/rpc/commands/auth.clj index 4073de2c39..e4b36e84b4 100644 --- a/backend/src/app/rpc/commands/auth.clj +++ b/backend/src/app/rpc/commands/auth.clj @@ -38,13 +38,11 @@ (def schema:token [::sm/word-string {:max 6000}]) -(def ^:private default-verify-threshold - (dt/duration "15m")) - (defn- elapsed-verify-threshold? [profile] - (let [elapsed (dt/diff (:modified-at profile) (dt/now))] - (pos? (compare elapsed default-verify-threshold)))) + (let [elapsed (dt/diff (:modified-at profile) (dt/now)) + verify-threshold (cf/get :email-verify-threshold)] + (pos? (compare elapsed verify-threshold)))) ;; ---- COMMAND: login with password @@ -130,12 +128,21 @@ ;; ---- COMMAND: Logout +(def ^:private schema:logout + [:map {:title "logoug"} + [:profile-id {:optional true} ::sm/uuid]]) + (sv/defmethod ::logout "Clears the authentication cookie and logout the current session." {::rpc/auth false - ::doc/added "1.15"} - [cfg _] - (rph/with-transform {} (session/delete-fn cfg))) + ::doc/changes [["2.1" "Now requires profile-id passed in the body"]] + ::doc/added "1.0" + ::sm/params schema:logout} + [cfg params] + (if (= (:profile-id params) + (::rpc/profile-id params)) + (rph/with-transform {} (session/delete-fn cfg)) + {})) ;; ---- COMMAND: Recover Profile diff --git a/backend/src/app/srepl/fixes.clj b/backend/src/app/srepl/fixes.clj index ee40421dfc..0db429aa34 100644 --- a/backend/src/app/srepl/fixes.clj +++ b/backend/src/app/srepl/fixes.clj @@ -184,10 +184,7 @@ (ctk/instance-head? child)) (let [slot (guess-swap-slot component-child component-container)] (l/dbg :hint "child" :id (:id child) :name (:name child) :slot slot) - (ctn/update-shape container (:id child) - #(update % :touched - cfh/set-touched-group - (ctk/build-swap-slot-group slot)))) + (ctn/update-shape container (:id child) #(ctk/set-swap-slot % slot))) container)] (recur (process-copy-head container child) (rest children) diff --git a/common/src/app/common/files/repair.cljc b/common/src/app/common/files/repair.cljc index cd2a656d28..c40b602275 100644 --- a/common/src/app/common/files/repair.cljc +++ b/common/src/app/common/files/repair.cljc @@ -481,7 +481,7 @@ (let [slot (:swap-slot args)] (when (some? slot) (log/debug :hint (str " -> set swap-slot to " slot)) - (update shape :touched cfh/set-touched-group (ctk/build-swap-slot-group slot)))))] + (ctk/set-swap-slot shape slot))))] (log/dbg :hint "repairing shape :missing-slot" :id (:id shape) :name (:name shape) :page-id page-id) (-> (pcb/empty-changes nil page-id) diff --git a/common/src/app/common/logic/libraries.cljc b/common/src/app/common/logic/libraries.cljc index 595ed57261..ec40277e0c 100644 --- a/common/src/app/common/logic/libraries.cljc +++ b/common/src/app/common/logic/libraries.cljc @@ -284,9 +284,17 @@ (let [children (cfh/get-children-with-self (:objects container) shape-id) skip-near (fn [changes shape] (let [ref-shape (ctf/find-ref-shape file container libraries shape {:include-deleted? true})] - (if (some? (:shape-ref ref-shape)) - (pcb/update-shapes changes [(:id shape)] #(assoc % :shape-ref (:shape-ref ref-shape))) - changes)))] + (cond-> changes + (some? (:shape-ref ref-shape)) + (pcb/update-shapes [(:id shape)] #(assoc % :shape-ref (:shape-ref ref-shape))) + + ;; When advancing level, if the referenced shape has a swap slot, it must be + ;; copied to the current shape, because the shape-ref now will not be pointing + ;; to a near main (except for first level subcopies). + (and (some? (ctk/get-swap-slot ref-shape)) + (nil? (ctk/get-swap-slot shape)) + (not= (:id shape) shape-id)) + (pcb/update-shapes [(:id shape)] #(ctk/set-swap-slot % (ctk/get-swap-slot ref-shape))))))] (reduce skip-near changes children))) (defn prepare-restore-component @@ -1194,7 +1202,7 @@ :shapes all-parents})) changes' (reduce del-obj-change changes' new-shapes)] - (if (and (cfh/touched-group? parent-shape :shapes-group) omit-touched?) + (if (and (ctk/touched-group? parent-shape :shapes-group) omit-touched?) changes changes'))) @@ -1349,7 +1357,7 @@ changes' ids)] - (if (and (cfh/touched-group? parent :shapes-group) omit-touched?) + (if (and (ctk/touched-group? parent :shapes-group) omit-touched?) changes changes'))) @@ -1385,7 +1393,7 @@ :ignore-touched true :syncing true})))] - (if (and (cfh/touched-group? parent :shapes-group) omit-touched?) + (if (and (ctk/touched-group? parent :shapes-group) omit-touched?) changes changes'))) @@ -1846,12 +1854,11 @@ ;; if the shape isn't inside a main component, it shouldn't have a swap slot (and (nil? (ctk/get-swap-slot new-shape)) inside-comp?) - (update :touched cfh/set-touched-group (-> (ctf/find-swap-slot shape - page - {:id (:id file) - :data file} - libraries) - (ctk/build-swap-slot-group))))] + (ctk/set-swap-slot (ctf/find-swap-slot shape + page + {:id (:id file) + :data file} + libraries)))] [new-shape (-> changes ;; Restore the properties diff --git a/common/src/app/common/types/component.cljc b/common/src/app/common/types/component.cljc index bcd6ef3b35..b4060abedc 100644 --- a/common/src/app/common/types/component.cljc +++ b/common/src/app/common/types/component.cljc @@ -183,6 +183,15 @@ (and (= shape-id (:main-instance-id component)) (= page-id (:main-instance-page component)))) +(defn set-touched-group + [touched group] + (when group + (conj (or touched #{}) group))) + +(defn touched-group? + [shape group] + ((or (:touched shape) #{}) group)) + (defn build-swap-slot-group "Convert a swap-slot into a :touched group" [swap-slot] @@ -204,6 +213,13 @@ (when group (group->swap-slot group)))) +(defn set-swap-slot + "Add a touched group with a form :swap-slot-." + [shape swap-slot] + (cond-> shape + (some? swap-slot) + (update :touched set-touched-group (build-swap-slot-group swap-slot)))) + (defn match-swap-slot? [shape-main shape-inst] (let [slot-main (get-swap-slot shape-main) diff --git a/frontend/playwright/data/design/get-file-multiple-attributes.json b/frontend/playwright/data/design/get-file-multiple-attributes.json new file mode 100644 index 0000000000..c0a67da95c --- /dev/null +++ b/frontend/playwright/data/design/get-file-multiple-attributes.json @@ -0,0 +1,343 @@ +{ + "~:features":{ + "~#set":[ + "layout/grid", + "styles/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 12", + "~:revn":2, + "~:modified-at":"~m1718012938567", + "~:id":"~u1795a568-0df0-8095-8004-7ba741f56be2", + "~:is-shared":false, + "~:version":48, + "~:project-id":"~u4dc640b0-5cbf-11ec-a7c5-91e9eb4f238d", + "~:created-at":"~m1718012912598", + "~:data":{ + "~:pages":[ + "~u1795a568-0df0-8095-8004-7ba741f56be3" + ], + "~:pages-index":{ + "~u1795a568-0df0-8095-8004-7ba741f56be3":{ + "~: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":[ + "~u2ace9ce8-8e01-8086-8004-7ba745d4305a", + "~u2ace9ce8-8e01-8086-8004-7ba748566e02" + ] + } + }, + "~u2ace9ce8-8e01-8086-8004-7ba745d4305a":{ + "~#shape":{ + "~:y":221, + "~: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":105, + "~:type":"~:rect", + "~:points":[ + { + "~#point":{ + "~:x":165, + "~:y":221 + } + }, + { + "~#point":{ + "~:x":270, + "~:y":221 + } + }, + { + "~#point":{ + "~:x":270, + "~:y":316 + } + }, + { + "~#point":{ + "~:x":165, + "~:y":316 + } + } + ], + "~: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":"~u2ace9ce8-8e01-8086-8004-7ba745d4305a", + "~:parent-id":"~u00000000-0000-0000-0000-000000000000", + "~:frame-id":"~u00000000-0000-0000-0000-000000000000", + "~:strokes":[ + + ], + "~:x":165, + "~:proportion":1, + "~:selrect":{ + "~#rect":{ + "~:x":165, + "~:y":221, + "~:width":105, + "~:height":95, + "~:x1":165, + "~:y1":221, + "~:x2":270, + "~:y2":316 + } + }, + "~:fills":[ + { + "~:fill-color":"#B1B2B5", + "~:fill-opacity":1 + } + ], + "~:flip-x":null, + "~:ry":0, + "~:height":95, + "~:flip-y":null + } + }, + "~u2ace9ce8-8e01-8086-8004-7ba748566e02":{ + "~#shape":{ + "~:y":228, + "~: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":85, + "~:type":"~:circle", + "~:points":[ + { + "~#point":{ + "~:x":344, + "~:y":228 + } + }, + { + "~#point":{ + "~:x":429, + "~:y":228 + } + }, + { + "~#point":{ + "~:x":429, + "~:y":308 + } + }, + { + "~#point":{ + "~:x":344, + "~:y":308 + } + } + ], + "~:proportion-lock":false, + "~:transform-inverse":{ + "~#matrix":{ + "~:a":1.0, + "~:b":0.0, + "~:c":0.0, + "~:d":1.0, + "~:e":0.0, + "~:f":0.0 + } + }, + "~:blur":{ + "~:id":"~u2ace9ce8-8e01-8086-8004-7ba757cdd271", + "~:type":"~:layer-blur", + "~:value":4, + "~:hidden":false + }, + "~:id":"~u2ace9ce8-8e01-8086-8004-7ba748566e02", + "~:parent-id":"~u00000000-0000-0000-0000-000000000000", + "~:frame-id":"~u00000000-0000-0000-0000-000000000000", + "~:strokes":[ + { + "~:stroke-alignment":"~:inner", + "~:stroke-style":"~:solid", + "~:stroke-color":"#000000", + "~:stroke-opacity":1, + "~:stroke-width":1 + } + ], + "~:x":344, + "~:proportion":1, + "~:shadow":[ + { + "~:color":{ + "~:color":"#000000", + "~:opacity":0.2 + }, + "~:spread":0, + "~:offset-y":4, + "~:style":"~:drop-shadow", + "~:blur":4, + "~:hidden":false, + "~:id":"~u2ace9ce8-8e01-8086-8004-7ba756ddebd5", + "~:offset-x":4 + } + ], + "~:selrect":{ + "~#rect":{ + "~:x":344, + "~:y":228, + "~:width":85, + "~:height":80, + "~:x1":344, + "~:y1":228, + "~:x2":429, + "~:y2":308 + } + }, + "~:fills":[ + { + "~:fill-color":"#1247e7", + "~:fill-opacity":1 + } + ], + "~:flip-x":null, + "~:height":80, + "~:flip-y":null + } + } + }, + "~:id":"~u1795a568-0df0-8095-8004-7ba741f56be3", + "~:name":"Page 1" + } + }, + "~:id":"~u1795a568-0df0-8095-8004-7ba741f56be2", + "~:recent-colors":[ + { + "~:color":"#1247e7", + "~:opacity":1 + } + ] + } +} \ No newline at end of file diff --git a/frontend/playwright/data/design/get-file-object-thumbnails-multiple-attributes.json b/frontend/playwright/data/design/get-file-object-thumbnails-multiple-attributes.json new file mode 100644 index 0000000000..0637a088a0 --- /dev/null +++ b/frontend/playwright/data/design/get-file-object-thumbnails-multiple-attributes.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/frontend/playwright/ui/pages/DashboardPage.js b/frontend/playwright/ui/pages/DashboardPage.js index 285e47d95e..e83c62dd97 100644 --- a/frontend/playwright/ui/pages/DashboardPage.js +++ b/frontend/playwright/ui/pages/DashboardPage.js @@ -4,11 +4,6 @@ export class DashboardPage extends BaseWebSocketPage { static async init(page) { await BaseWebSocketPage.initWebSockets(page); - await BaseWebSocketPage.mockRPC( - page, - "get-profile", - "logged-in-user/get-profile-logged-in-no-onboarding.json", - ); await BaseWebSocketPage.mockRPC(page, "get-teams", "logged-in-user/get-teams-default.json"); await BaseWebSocketPage.mockRPC( page, diff --git a/frontend/playwright/ui/pages/OnboardingPage.js b/frontend/playwright/ui/pages/OnboardingPage.js new file mode 100644 index 0000000000..0fe68e78a8 --- /dev/null +++ b/frontend/playwright/ui/pages/OnboardingPage.js @@ -0,0 +1,45 @@ +import { BaseWebSocketPage } from "./BaseWebSocketPage"; + +export class OnboardingPage extends BaseWebSocketPage { + constructor(page) { + super(page); + this.submitButton = page.getByRole("Button",{ name: "Next" }) + } + + async fillOnboardingInputsStep1() { + await this.page.getByText('Personal').click(); + await this.page.getByText('Select option').click(); + await this.page.getByText('Testing before self-hosting').click(); + + await this.submitButton.click(); + } + + async fillOnboardingInputsStep2() { + await this.page.getByText('Figma').click(); + + await this.submitButton.click(); + } + + async fillOnboardingInputsStep3() { + await this.page.getByText('Select option').first().click(); + await this.page.getByText('Product Managment').click(); + await this.page.getByText('Select option').first().click(); + await this.page.getByText('Director').click(); + await this.page.getByText('Select option').click(); + await this.page.getByText('11-30').click(); + + await this.submitButton.click(); + } + + async fillOnboardingInputsStep4() { + await this.page.getByText('Other').click(); + await this.page.getByPlaceholder('Other (specify)').fill("Another"); + await this.submitButton.click(); + } + + async fillOnboardingInputsStep5() { + await this.page.getByText('Event').click(); + } +} + +export default OnboardingPage; diff --git a/frontend/playwright/ui/pages/WorkspacePage.js b/frontend/playwright/ui/pages/WorkspacePage.js index 4f045344b7..c8f0c49cca 100644 --- a/frontend/playwright/ui/pages/WorkspacePage.js +++ b/frontend/playwright/ui/pages/WorkspacePage.js @@ -43,13 +43,15 @@ export class WorkspacePage extends BaseWebSocketPage { this.presentUserListItems = page.getByTestId("active-users-list").getByAltText("Princesa Leia"); this.viewport = page.getByTestId("viewport"); this.rootShape = page.locator(`[id="shape-00000000-0000-0000-0000-000000000000"]`); + this.toolbarOptions = page.getByTestId("toolbar-options"); this.rectShapeButton = page.getByRole("button", { name: "Rectangle (R)" }); + this.toggleToolbarButton = page.getByRole("button", { name: "Toggle toolbar" }); this.colorpicker = page.getByTestId("colorpicker"); - this.layers = page.getByTestId("layers"); + this.layers = page.getByTestId("layer-tree"); this.palette = page.getByTestId("palette"); - this.assets = page.getByTestId("assets"); - this.libraries = page.getByTestId("libraries"); - this.closeLibraries = page.getByTestId("close-libraries"); + this.sidebar = page.getByTestId("left-sidebar"); + this.selectionRect = page.getByTestId("workspace-selection-rect"); + this.horizontalScrollbar = page.getByTestId("horizontal-scrollbar"); this.librariesModal = page.getByTestId("libraries-modal"); } @@ -102,6 +104,19 @@ export class WorkspacePage extends BaseWebSocketPage { await this.page.mouse.up(); } + async panOnViewportAt(x, y, width, height) { + await this.page.waitForTimeout(100); + await this.viewport.hover({ position: { x, y } }); + await this.page.mouse.down({ button: "middle" }); + await this.viewport.hover({ position: { x: x + width, y: y + height } }); + await this.page.mouse.up({ button: "middle" }); + } + + async togglePages() { + const pagesToggle = this.page.getByText("Pages"); + await pagesToggle.click(); + } + async moveSelectionToShape(name) { await this.page.locator('rect.viewport-selrect').hover(); await this.page.mouse.down(); @@ -120,15 +135,21 @@ export class WorkspacePage extends BaseWebSocketPage { } async expectSelectedLayer(name) { - await expect(this.layers.getByTestId("layer-row").filter({ has: this.page.getByText(name) })).toHaveClass(/selected/); + await expect(this.layers.getByTestId("layer-row").filter({ has: this.page.getByText(name) })).toHaveClass( + /selected/, + ); + } + + async expectHiddenToolbarOptions() { + await expect(this.toolbarOptions).toHaveCSS("opacity", "0"); } async clickAssets(clickOptions = {}) { - await this.assets.click(clickOptions); + await this.sidebar.getByText("Assets").click(clickOptions); } - async clickLibraries(clickOptions = {}) { - await this.libraries.click(clickOptions); + async openLibrariesModal(clickOptions = {}) { + await this.sidebar.getByText("Libraries").click(clickOptions); } async clickLibrary(name, clickOptions = {}) { @@ -136,11 +157,15 @@ export class WorkspacePage extends BaseWebSocketPage { .getByTestId("library-item") .filter({ hasText: name }) .getByRole("button") - .click(clickOptions); + .click(clickOptions); } - async clickCloseLibraries(clickOptions = {}) { - await this.closeLibraries.click(clickOptions); + async closeLibrariesModal(clickOptions = {}) { + await this.librariesModal.getByRole("button", { name: "Close" }).click(clickOptions); + } + + async clickColorPalette(clickOptions = {}) { + await this.palette.getByRole("button", { name: "Color Palette (Alt+P)" }).click(clickOptions); } async clickColorPalette(clickOptions = {}) { diff --git a/frontend/playwright/ui/specs/dashboard.spec.js b/frontend/playwright/ui/specs/dashboard.spec.js index 145c1321a1..23e71efade 100644 --- a/frontend/playwright/ui/specs/dashboard.spec.js +++ b/frontend/playwright/ui/specs/dashboard.spec.js @@ -3,6 +3,11 @@ import DashboardPage from "../pages/DashboardPage"; test.beforeEach(async ({ page }) => { await DashboardPage.init(page); + await DashboardPage.mockRPC( + page, + "get-profile", + "logged-in-user/get-profile-logged-in-no-onboarding.json", + ); }); test("Dashboad page has title ", async ({ page }) => { diff --git a/frontend/playwright/ui/specs/design-tab.spec.js b/frontend/playwright/ui/specs/design-tab.spec.js index 7dc23cd1fb..7a43aa1661 100644 --- a/frontend/playwright/ui/specs/design-tab.spec.js +++ b/frontend/playwright/ui/specs/design-tab.spec.js @@ -7,6 +7,8 @@ test.beforeEach(async ({ page }) => { const multipleConstraintsFileId = `03bff843-920f-81a1-8004-756365e1eb6a`; const multipleConstraintsPageId = `03bff843-920f-81a1-8004-756365e1eb6b`; +const multipleAttributesFileId = `1795a568-0df0-8095-8004-7ba741f56be2`; +const multipleAttributesPageId = `1795a568-0df0-8095-8004-7ba741f56be3`; const setupFileWithMultipeConstraints = async (workspace) => { await workspace.setupEmptyFile(); @@ -21,6 +23,15 @@ const setupFileWithMultipeConstraints = async (workspace) => { ); }; +const setupFileWithMultipeAttributes = async (workspace) => { + await workspace.setupEmptyFile(); + await workspace.mockRPC(/get\-file\?/, "design/get-file-multiple-attributes.json"); + await workspace.mockRPC( + "get-file-object-thumbnails?file-id=*", + "design/get-file-object-thumbnails-multiple-attributes.json", + ); +}; + test.describe("Constraints", () => { test("Constraint dropdown shows 'Mixed' when multiple layers are selected with different constraints", async ({ page, @@ -45,6 +56,43 @@ test.describe("Constraints", () => { }); }); +test.describe("Multiple shapes attributes", () => { + test("User selects multiple shapes with sames fills, strokes, shadows and blur", 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"] }); + + await expect(workspace.page.getByTestId("add-fill")).toBeVisible(); + await expect(workspace.page.getByTestId("add-stroke")).toBeVisible(); + await expect(workspace.page.getByTestId("add-shadow")).toBeVisible(); + await expect(workspace.page.getByTestId("add-blur")).toBeVisible(); + }); + + test("User selects multiple shapes with different fills, strokes, shadows and blur", async ({ page }) => { + const workspace = new WorkspacePage(page); + await setupFileWithMultipeAttributes(workspace); + await workspace.goToWorkspace({ + fileId: multipleAttributesFileId, + pageId: multipleAttributesPageId, + }); + + await workspace.clickLeafLayer("Ellipse"); + await workspace.clickLeafLayer("Rectangle", { modifiers: ["Shift"] }); + + await expect(workspace.page.getByTestId("add-fill")).toBeHidden(); + await expect(workspace.page.getByTestId("add-stroke")).toBeHidden(); + await expect(workspace.page.getByTestId("add-shadow")).toBeHidden(); + await expect(workspace.page.getByTestId("add-blur")).toBeHidden(); + }); +}); + test("BUG 7760 - Layout losing properties when changing parents", async ({ page }) => { const workspacePage = new WorkspacePage(page); await workspacePage.setupEmptyFile(); diff --git a/frontend/playwright/ui/specs/onboarding.spec.js b/frontend/playwright/ui/specs/onboarding.spec.js new file mode 100644 index 0000000000..39efa967c0 --- /dev/null +++ b/frontend/playwright/ui/specs/onboarding.spec.js @@ -0,0 +1,32 @@ +import { test, expect } from "@playwright/test"; +import DashboardPage from "../pages/DashboardPage"; + import OnboardingPage from "../pages/OnboardingPage" + +test.beforeEach(async ({ page }) => { + await DashboardPage.init(page); + await DashboardPage.mockRPC(page, "get-profile", "logged-in-user/get-profile-logged-in.json"); +}); + + +test("User can complete the onboarding", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + const onboardingPage = new OnboardingPage(page); + + await dashboardPage.goToWorkspace(); + await expect(page.getByRole("heading", { name: "Help us get to know you" })).toBeVisible(); + + await onboardingPage.fillOnboardingInputsStep1(); + await expect(page.getByRole("heading", { name: "Which one of these tools do" })).toBeVisible(); + + await onboardingPage.fillOnboardingInputsStep2(); + await expect(page.getByRole("heading", { name: "Tell us about your job" })).toBeVisible(); + + await onboardingPage.fillOnboardingInputsStep3(); + await expect(page.getByRole("heading", { name: "Where would you like to get" })).toBeVisible(); + + await onboardingPage.fillOnboardingInputsStep4(); + await expect(page.getByRole("heading", { name: "How did you hear about Penpot?" })).toBeVisible(); + + await onboardingPage.fillOnboardingInputsStep5(); + await expect(page.getByRole("button", { name: "Start" })).toBeEnabled(); +}); diff --git a/frontend/playwright/ui/specs/sidebar.spec.js b/frontend/playwright/ui/specs/sidebar.spec.js new file mode 100644 index 0000000000..bb0a4d451f --- /dev/null +++ b/frontend/playwright/ui/specs/sidebar.spec.js @@ -0,0 +1,56 @@ +import { test, expect } from "@playwright/test"; +import { WorkspacePage } from "../pages/WorkspacePage"; + +test.beforeEach(async ({ page }) => { + await WorkspacePage.init(page); +}); + +test.describe("Layers tab", () => { + test("BUG 7466 - Layers tab height extends to the bottom when 'Pages' is collapsed", async ({ page }) => { + const workspace = new WorkspacePage(page); + await workspace.setupEmptyFile(); + + await workspace.goToWorkspace(); + + const { height: heightExpanded } = await workspace.layers.boundingBox(); + await workspace.togglePages(); + const { height: heightCollapsed } = await workspace.layers.boundingBox(); + + expect(heightExpanded > heightCollapsed); + }); +}); + +test.describe("Assets tab", () => { + test("User adds a library and its automatically selected in the color palette", async ({ page }) => { + const workspacePage = new WorkspacePage(page); + await workspacePage.setupEmptyFile(); + await workspacePage.mockRPC("link-file-to-library", "workspace/link-file-to-library.json"); + await workspacePage.mockRPC("unlink-file-from-library", "workspace/unlink-file-from-library.json"); + await workspacePage.mockRPC( + "get-team-shared-files?team-id=*", + "workspace/get-team-shared-libraries-non-empty.json", + ); + + await workspacePage.goToWorkspace(); + + // Add Testing library 1 + await workspacePage.clickColorPalette(); + await workspacePage.clickAssets(); + // Now the get-file call should return a library + await workspacePage.mockRPC(/get\-file\?/, "workspace/get-file-library.json"); + await workspacePage.openLibrariesModal(); + await workspacePage.clickLibrary("Testing library 1"); + await workspacePage.closeLibrariesModal(); + + await expect(workspacePage.palette.getByRole("button", { name: "test-color-187cd5" })).toBeVisible(); + + // Remove Testing library 1 + await workspacePage.openLibrariesModal(); + await workspacePage.clickLibrary("Testing library 1"); + await workspacePage.closeLibrariesModal(); + + await expect( + workspacePage.palette.getByText("There are no color styles in your library yet"), + ).toBeVisible(); + }); +}); diff --git a/frontend/playwright/ui/specs/workspace.spec.js b/frontend/playwright/ui/specs/workspace.spec.js index 8f5e5c6094..3682ecf68e 100644 --- a/frontend/playwright/ui/specs/workspace.spec.js +++ b/frontend/playwright/ui/specs/workspace.spec.js @@ -39,6 +39,60 @@ test("User draws a rect", async ({ page }) => { await expect(shape).toHaveAttribute("height", "100"); }); +test("User makes a group", async ({ page }) => { + const workspacePage = new WorkspacePage(page); + await workspacePage.setupEmptyFile(); + await workspacePage.mockRPC(/get\-file\?/, "workspace/get-file-not-empty.json"); + await workspacePage.mockRPC("update-file?id=*", "workspace/update-file-create-rect.json"); + + await workspacePage.goToWorkspace({ + fileId: "6191cd35-bb1f-81f7-8004-7cc63d087374", + pageId: "6191cd35-bb1f-81f7-8004-7cc63d087375", + }); + await workspacePage.clickLeafLayer("Rectangle"); + await workspacePage.page.keyboard.press("ControlOrMeta+g"); + await workspacePage.expectSelectedLayer("Group"); +}); + +test("Bug 7654 - Toolbar keeps toggling on and off on spacebar press", async ({ page }) => { + const workspacePage = new WorkspacePage(page); + await workspacePage.setupEmptyFile(); + await workspacePage.goToWorkspace(); + + await workspacePage.toggleToolbarButton.click(); + await workspacePage.page.keyboard.press("Backspace"); + await workspacePage.page.keyboard.press("Enter"); + await workspacePage.expectHiddenToolbarOptions(); +}); + +test("Bug 7525 - User moves a scrollbar and no selciont rectangle appears", async ({ page }) => { + const workspacePage = new WorkspacePage(page); + await workspacePage.setupEmptyFile(); + await workspacePage.mockRPC(/get\-file\?/, "workspace/get-file-not-empty.json"); + await workspacePage.mockRPC("update-file?id=*", "workspace/update-file-create-rect.json"); + + await workspacePage.goToWorkspace({ + fileId: "6191cd35-bb1f-81f7-8004-7cc63d087374", + pageId: "6191cd35-bb1f-81f7-8004-7cc63d087375", + }); + + // Move created rect to a corner, in orther to get scrollbars + await workspacePage.panOnViewportAt(128, 128, 300, 300); + + // Check scrollbars appear + const horizontalScrollbar = workspacePage.horizontalScrollbar; + await expect(horizontalScrollbar).toBeVisible(); + + // Grab scrollbar and move + const {x, y} = await horizontalScrollbar.boundingBox(); + await page.waitForTimeout(100); + await workspacePage.viewport.hover({ position: { x: x, y: y + 5 } }); + await page.mouse.down(); + await workspacePage.viewport.hover({ position: { x: x - 130, y: y - 95 } }); + + await expect(workspacePage.selectionRect).not.toBeInViewport(); +}); + test("User adds a library and its automatically selected in the color palette", async ({ page }) => { const workspacePage = new WorkspacePage(page); await workspacePage.setupEmptyFile(); @@ -53,31 +107,16 @@ test("User adds a library and its automatically selected in the color palette", await workspacePage.clickAssets(); // Now the get-file call should return a library await workspacePage.mockRPC(/get\-file\?/, "workspace/get-file-library.json"); - await workspacePage.clickLibraries(); + await workspacePage.openLibrariesModal(); await workspacePage.clickLibrary("Testing library 1") - await workspacePage.clickCloseLibraries(); + await workspacePage.closeLibrariesModal(); await expect(workspacePage.palette.getByRole("button", { name: "test-color-187cd5" })).toBeVisible(); // Remove Testing library 1 - await workspacePage.clickLibraries(); + await workspacePage.openLibrariesModal(); await workspacePage.clickLibrary("Testing library 1") - await workspacePage.clickCloseLibraries(); + await workspacePage.closeLibrariesModal(); await expect(workspacePage.palette.getByText('There are no color styles in your library yet')).toBeVisible(); }); - -test("User makes a group", async ({ page }) => { - const workspacePage = new WorkspacePage(page); - await workspacePage.setupEmptyFile(); - await workspacePage.mockRPC(/get\-file\?/, "workspace/get-file-not-empty.json"); - await workspacePage.mockRPC("update-file?id=*", "workspace/update-file-create-rect.json"); - - await workspacePage.goToWorkspace({ - fileId: "6191cd35-bb1f-81f7-8004-7cc63d087374", - pageId: "6191cd35-bb1f-81f7-8004-7cc63d087375" - }); - await workspacePage.clickLeafLayer("Rectangle"); - await workspacePage.page.keyboard.press("ControlOrMeta+g"); - await workspacePage.expectSelectedLayer("Group"); -}); diff --git a/frontend/src/app/main/data/users.cljs b/frontend/src/app/main/data/users.cljs index e49925038b..e4f8c4d078 100644 --- a/frontend/src/app/main/data/users.cljs +++ b/frontend/src/app/main/data/users.cljs @@ -328,11 +328,15 @@ (-data [_] {}) ptk/WatchEvent - (watch [_ _ _] - (->> (rp/cmd! :logout) - (rx/delay-at-least 300) - (rx/catch (constantly (rx/of 1))) - (rx/map #(logged-out params))))))) + (watch [_ state _] + (let [profile-id (:profile-id state)] + (->> (rx/interval 500) + (rx/take 1) + (rx/mapcat (fn [_] + (->> (rp/cmd! :logout {:profile-id profile-id}) + (rx/delay-at-least 300) + (rx/catch (constantly (rx/of 1)))))) + (rx/map #(logged-out params)))))))) ;; --- Update Profile diff --git a/frontend/src/app/main/data/workspace/colors.cljs b/frontend/src/app/main/data/workspace/colors.cljs index 70a19585f9..71a3903b8f 100644 --- a/frontend/src/app/main/data/workspace/colors.cljs +++ b/frontend/src/app/main/data/workspace/colors.cljs @@ -248,7 +248,7 @@ (assoc :stroke-style :solid) (not (contains? new-attrs :stroke-alignment)) - (assoc :stroke-alignment :inner) + (assoc :stroke-alignment :center) :always (d/without-nils))] diff --git a/frontend/src/app/main/data/workspace/groups.cljs b/frontend/src/app/main/data/workspace/groups.cljs index 384eed1009..04beaa5535 100644 --- a/frontend/src/app/main/data/workspace/groups.cljs +++ b/frontend/src/app/main/data/workspace/groups.cljs @@ -198,7 +198,8 @@ (dws/select-shapes (d/ordered-set (:id group)))) (ptk/data-event :layout/update {:ids parents})))))))) -(def group-selected +(defn group-selected + [] (ptk/reify ::group-selected ptk/WatchEvent (watch [_ state _] @@ -258,7 +259,8 @@ (when change-selection? (dws/select-shapes child-ids)))))))) -(def ungroup-selected +(defn ungroup-selected + [] (ptk/reify ::ungroup-selected ptk/WatchEvent (watch [_ state _] diff --git a/frontend/src/app/main/data/workspace/guides.cljs b/frontend/src/app/main/data/workspace/guides.cljs index 4047873413..4e2895bb20 100644 --- a/frontend/src/app/main/data/workspace/guides.cljs +++ b/frontend/src/app/main/data/workspace/guides.cljs @@ -79,20 +79,21 @@ (rx/from (->> guides (mapv #(remove-guide %)))))))) (defmethod ptk/resolve ::move-frame-guides - [_ ids] + [_ args] (dm/assert! "expected a coll of uuids" - (every? uuid? ids)) + (every? uuid? (:ids args))) (ptk/reify ::move-frame-guides ptk/WatchEvent (watch [_ state _] - (let [objects (wsh/lookup-page-objects state) + (let [ids (:ids args) + object-modifiers (:modifiers args) + + objects (wsh/lookup-page-objects state) is-frame? (fn [id] (= :frame (get-in objects [id :type]))) frame-ids? (into #{} (filter is-frame?) ids) - object-modifiers (get state :workspace-modifiers) - build-move-event (fn [guide] (let [frame (get objects (:frame-id guide)) diff --git a/frontend/src/app/main/data/workspace/modifiers.cljs b/frontend/src/app/main/data/workspace/modifiers.cljs index ace8816c50..74e243af84 100644 --- a/frontend/src/app/main/data/workspace/modifiers.cljs +++ b/frontend/src/app/main/data/workspace/modifiers.cljs @@ -497,7 +497,7 @@ (if undo-transation? (rx/of (dwu/start-undo-transaction undo-id)) (rx/empty)) - (rx/of (ptk/event ::dwg/move-frame-guides ids-with-children) + (rx/of (ptk/event ::dwg/move-frame-guides {:ids ids-with-children :modifiers object-modifiers}) (ptk/event ::dwcm/move-frame-comment-threads ids-with-children) (dwsh/update-shapes ids diff --git a/frontend/src/app/main/data/workspace/path/helpers.cljs b/frontend/src/app/main/data/workspace/path/helpers.cljs index 2facaf53a6..b52ab6e723 100644 --- a/frontend/src/app/main/data/workspace/path/helpers.cljs +++ b/frontend/src/app/main/data/workspace/path/helpers.cljs @@ -22,6 +22,7 @@ (or (= type ::common/finish-path) (= type :app.main.data.workspace.path.shortcuts/esc-pressed) (= type :app.main.data.workspace.common/clear-edition-mode) + (= type :app.main.data.workspace.edition/clear-edition-mode) (= type :app.main.data.workspace/finalize-page) (= event :interrupt) ;; ESC (and ^boolean (mse/mouse-event? event) diff --git a/frontend/src/app/main/data/workspace/shortcuts.cljs b/frontend/src/app/main/data/workspace/shortcuts.cljs index 2d808a9bd5..45a763ccac 100644 --- a/frontend/src/app/main/data/workspace/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/shortcuts.cljs @@ -119,12 +119,12 @@ :group {:tooltip (ds/meta "G") :command (ds/c-mod "g") :subsections [:modify-layers] - :fn #(emit-when-no-readonly dw/group-selected)} + :fn #(emit-when-no-readonly (dw/group-selected))} :ungroup {:tooltip (ds/shift "G") :command "shift+g" :subsections [:modify-layers] - :fn #(emit-when-no-readonly dw/ungroup-selected)} + :fn #(emit-when-no-readonly (dw/ungroup-selected))} :mask {:tooltip (ds/meta "M") :command (ds/c-mod "m") diff --git a/frontend/src/app/main/ui/auth/register.cljs b/frontend/src/app/main/ui/auth/register.cljs index 91ed32601c..54da6e9764 100644 --- a/frontend/src/app/main/ui/auth/register.cljs +++ b/frontend/src/app/main/ui/auth/register.cljs @@ -20,6 +20,7 @@ [app.main.ui.icons :as i] [app.util.i18n :refer [tr tr-html]] [app.util.router :as rt] + [app.util.storage :as sto] [beicon.v2.core :as rx] [cljs.spec.alpha :as s] [rumext.v2 :as mf])) @@ -163,11 +164,7 @@ ;; --- PAGE: register validation -(defn- handle-register-error - [_form _data] - (st/emit! (msg/error (tr "errors.generic")))) - -(defn- handle-register-success +(defn- on-register-success [data] (cond (some? (:invitation-token data)) @@ -178,7 +175,9 @@ (st/emit! (du/login-from-register)) :else - (st/emit! (rt/nav :auth-register-success {} {:email (:email data)})))) + (do + (swap! sto/storage assoc ::email (:email data)) + (st/emit! (rt/nav :auth-register-success))))) (s/def ::accept-terms-and-privacy (s/and ::us/boolean true?)) (s/def ::accept-newsletter-subscription ::us/boolean) @@ -192,31 +191,63 @@ :opt-un [::accept-terms-and-privacy ::accept-newsletter-subscription]))) +(mf/defc terms-and-privacy + {::mf/props :obj + ::mf/private true} + [] + (let [terms-label + (mf/html + [:& tr-html + {:tag-name "div" + :label "auth.terms-and-privacy-agreement" + :params [cf/terms-of-service-uri cf/privacy-policy-uri]}])] + + [:div {:class (stl/css :fields-row :input-visible :accept-terms-and-privacy-wrapper)} + [:& fm/input {:name :accept-terms-and-privacy + :class (stl/css :checkbox-terms-and-privacy) + :type "checkbox" + :default-checked false + :label terms-label}]])) + (mf/defc register-validate-form + {::mf/props :obj} [{:keys [params on-success-callback]}] - (let [form (fm/use-form :spec ::register-validate-form - :validators [(fm/validate-not-empty :fullname (tr "auth.name.not-all-space")) - (fm/validate-length :fullname fm/max-length-allowed (tr "auth.name.too-long"))] + (let [validators (mf/with-memo [] + [(fm/validate-not-empty :fullname (tr "auth.name.not-all-space")) + (fm/validate-length :fullname fm/max-length-allowed (tr "auth.name.too-long"))]) + + form (fm/use-form :spec ::register-validate-form + :validators validators :initial params) + submitted? (mf/use-state false) - on-success (fn [p] - (if (nil? on-success-callback) - (handle-register-success p) - (on-success-callback (:email p)))) + on-success + (mf/use-fn + (mf/deps on-success-callback) + (fn [params] + (if (nil? on-success-callback) + (on-register-success params) + (on-success-callback (:email params))))) + + on-error + (mf/use-fn + (fn [_cause] + (st/emit! (msg/error (tr "errors.generic"))))) on-submit (mf/use-fn - (fn [form _event] + (fn [form _] (reset! submitted? true) (let [params (:clean-data @form)] (->> (rp/cmd! :register-profile params) (rx/finalize #(reset! submitted? false)) - (rx/subs! on-success - (partial handle-register-error form))))))] + (rx/subs! on-success on-error)))))] - [:& fm/form {:on-submit on-submit :form form + [:& fm/form {:on-submit on-submit + :form form :class (stl/css :register-validate-form)} + [:div {:class (stl/css :fields-row)} [:& fm/input {:name :fullname :label (tr "auth.fullname") @@ -225,18 +256,7 @@ :class (stl/css :form-field)}]] (when (contains? cf/flags :terms-and-privacy-checkbox) - (let [terms-label - (mf/html - [:& tr-html - {:tag-name "div" - :label "auth.terms-and-privacy-agreement" - :params [cf/terms-of-service-uri cf/privacy-policy-uri]}])] - [:div {:class (stl/css :fields-row :input-visible :accept-terms-and-privacy-wrapper)} - [:& fm/input {:name :accept-terms-and-privacy - :class (stl/css :checkbox-terms-and-privacy) - :type "checkbox" - :default-checked false - :label terms-label}]])) + [:& terms-and-privacy]) [:> fm/submit-button* {:label (tr "auth.register-submit") @@ -245,6 +265,7 @@ (mf/defc register-validate-page + {::mf/props :obj} [{:keys [params]}] [:div {:class (stl/css :auth-form-wrapper)} [:h1 {:class (stl/css :logo-container)} @@ -263,13 +284,15 @@ (tr "labels.go-back")]]]]) (mf/defc register-success-page - [{:keys [params]}] - [:div {:class (stl/css :auth-form-wrapper :register-success)} - [:h1 {:class (stl/css :logo-container)} - [:a {:href "#/" :title "Penpot" :class (stl/css :logo-btn)} i/logo]] - [:div {:class (stl/css :auth-title-wrapper)} - [:h2 {:class (stl/css :auth-title)} - (tr "auth.check-mail")] - [:div {:class (stl/css :notification-text)} (tr "auth.verification-email-sent")]] - [:div {:class (stl/css :notification-text-email)} (:email params "")] - [:div {:class (stl/css :notification-text)} (tr "auth.check-your-email")]]) + {::mf/props :obj} + [] + (let [email (::email @sto/storage)] + [:div {:class (stl/css :auth-form-wrapper :register-success)} + [:h1 {:class (stl/css :logo-container)} + [:a {:href "#/" :title "Penpot" :class (stl/css :logo-btn)} i/logo]] + [:div {:class (stl/css :auth-title-wrapper)} + [:h2 {:class (stl/css :auth-title)} + (tr "auth.check-mail")] + [:div {:class (stl/css :notification-text)} (tr "auth.verification-email-sent")]] + [:div {:class (stl/css :notification-text-email)} email] + [:div {:class (stl/css :notification-text)} (tr "auth.check-your-email")]])) diff --git a/frontend/src/app/main/ui/comments.scss b/frontend/src/app/main/ui/comments.scss index c3237c10dc..a2d1fdc528 100644 --- a/frontend/src/app/main/ui/comments.scss +++ b/frontend/src/app/main/ui/comments.scss @@ -142,11 +142,10 @@ // thread-content .thread-content { position: absolute; - overflow-y: scroll; - scrollbar-gutter: stable; + overflow-y: auto; width: $s-284; padding: $s-12; - padding-inline-end: 0; + padding-inline-end: $s-8; pointer-events: auto; user-select: text; diff --git a/frontend/src/app/main/ui/viewer/shapes.cljs b/frontend/src/app/main/ui/viewer/shapes.cljs index 5832f28cea..833a9fd79b 100644 --- a/frontend/src/app/main/ui/viewer/shapes.cljs +++ b/frontend/src/app/main/ui/viewer/shapes.cljs @@ -44,9 +44,17 @@ (or (empty? overlays-ids) (nil? shape) (cfh/root? shape)) base-frame :else (find-relative-to-base-frame (cfh/get-parent objects (:id shape)) objects overlays-ids base-frame))) +(defn- ignore-frame-shape + [shape objects manual?] + (let [shape (cond-> shape ;; When the the interaction is not manual and its origin is a frame, + ;; we need to ignore it on all the find-frame calculations + (and (:frame-id shape) (not manual?)) + (assoc :type :rect)) + objects (assoc objects (:id shape) shape)] + [shape objects])) + (defn- activate-interaction [interaction shape base-frame frame-offset objects overlays] - (case (:action-type interaction) :navigate (when-let [frame-id (:destination interaction)] @@ -58,9 +66,11 @@ (dv/go-to-frame frame-id (:animation interaction))))) :open-overlay - (let [dest-frame-id (:destination interaction) + (let [manual? (= :manual (:overlay-pos-type interaction)) + [shape objects] (ignore-frame-shape shape objects manual?) + dest-frame-id (:destination interaction) dest-frame (get objects dest-frame-id) - relative-to-id (if (= :manual (:overlay-pos-type interaction)) + relative-to-id (if manual? (if (= (:type shape) :frame) ;; manual interactions are always from "self" (:frame-id shape) (:id shape)) @@ -88,7 +98,9 @@ fixed-base?)))) :toggle-overlay - (let [dest-frame-id (:destination interaction) + (let [manual? (= :manual (:overlay-pos-type interaction)) + [shape objects] (ignore-frame-shape shape objects manual?) + dest-frame-id (:destination interaction) dest-frame (get objects dest-frame-id) relative-to-id (if (= :manual (:overlay-pos-type interaction)) (if (= (:type shape) :frame) ;; manual interactions are always from "self" @@ -146,7 +158,9 @@ (st/emit! (dv/close-overlay frame-id))) :toggle-overlay - (let [dest-frame-id (:destination interaction) + (let [manual? (= :manual (:overlay-pos-type interaction)) + [shape objects] (ignore-frame-shape shape objects manual?) + dest-frame-id (:destination interaction) dest-frame (get objects dest-frame-id) relative-to-id (if (= :manual (:overlay-pos-type interaction)) (if (= (:type shape) :frame) ;; manual interactions are always from "self" @@ -178,7 +192,9 @@ :close-overlay - (let [dest-frame-id (:destination interaction) + (let [manual? (= :manual (:overlay-pos-type interaction)) + [shape objects] (ignore-frame-shape shape objects manual?) + dest-frame-id (:destination interaction) dest-frame (get objects dest-frame-id) relative-to-id (if (= :manual (:overlay-pos-type interaction)) (if (= (:type shape) :frame) ;; manual interactions are always from "self" diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index 4dd499d189..c25d5cdfe6 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -243,8 +243,8 @@ is-group? (and single? has-group?) is-bool? (and single? has-bool?) - do-create-group #(st/emit! dw/group-selected) - do-remove-group #(st/emit! dw/ungroup-selected) + do-create-group #(st/emit! (dw/group-selected)) + do-remove-group #(st/emit! (dw/ungroup-selected)) do-mask-group #(st/emit! (dw/mask-group)) do-unmask-group #(st/emit! (dw/unmask-group)) do-create-artboard-from-selection diff --git a/frontend/src/app/main/ui/workspace/libraries.cljs b/frontend/src/app/main/ui/workspace/libraries.cljs index d07517c7b8..5e354e423f 100644 --- a/frontend/src/app/main/ui/workspace/libraries.cljs +++ b/frontend/src/app/main/ui/workspace/libraries.cljs @@ -519,6 +519,7 @@ [:div {:class (stl/css :modal-dialog)} [:button {:class (stl/css :close-btn) :on-click close-dialog + :aria-label (tr "labels.close") :data-testid "close-libraries"} close-icon] [:div {:class (stl/css :modal-title)} diff --git a/frontend/src/app/main/ui/workspace/sidebar.cljs b/frontend/src/app/main/ui/workspace/sidebar.cljs index d648420755..1aaf88c7b0 100644 --- a/frontend/src/app/main/ui/workspace/sidebar.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar.cljs @@ -38,8 +38,7 @@ (let [options-mode (mf/deref refs/options-mode-global) mode-inspect? (= options-mode :inspect) project (mf/deref refs/workspace-project) - show-pages? (mf/use-state true) - toggle-pages (mf/use-callback #(reset! show-pages? not)) + section (cond (or mode-inspect? (contains? layout :layers)) :layers (contains? layout :assets) :assets) @@ -50,9 +49,12 @@ {on-pointer-down :on-pointer-down on-lost-pointer-capture :on-lost-pointer-capture on-pointer-move :on-pointer-move parent-ref :parent-ref size :size} (use-resize-hook :left-sidebar 275 275 500 :x false :left) - {on-pointer-down-pages :on-pointer-down on-lost-pointer-capture-pages :on-lost-pointer-capture on-pointer-move-pages :on-pointer-move size-pages :size} + {on-pointer-down-pages :on-pointer-down on-lost-pointer-capture-pages :on-lost-pointer-capture on-pointer-move-pages :on-pointer-move size-pages-opened :size} (use-resize-hook :sitemap 200 38 400 :y false nil) + show-pages? (mf/use-state true) + toggle-pages (mf/use-callback #(reset! show-pages? not)) + size-pages (mf/use-memo (mf/deps show-pages? size-pages-opened) (fn [] (if @show-pages? size-pages-opened 32))) handle-collapse (mf/use-fn #(st/emit! (dw/toggle-layout-flag :collapse-left-sidebar))) @@ -63,6 +65,7 @@ [:& (mf/provider muc/sidebar) {:value :left} [:aside {:ref parent-ref :id "left-sidebar-aside" + :data-testid "left-sidebar" :data-size (str size) :class (stl/css-case :left-settings-bar true :global/two-row (<= size 300) diff --git a/frontend/src/app/main/ui/workspace/sidebar.scss b/frontend/src/app/main/ui/workspace/sidebar.scss index 192a8416e6..f70e37e5e0 100644 --- a/frontend/src/app/main/ui/workspace/sidebar.scss +++ b/frontend/src/app/main/ui/workspace/sidebar.scss @@ -84,10 +84,8 @@ $width-settings-bar-max: $s-500; .resize-area-horiz { position: absolute; - // top: calc($s-88 + var(--height, 200px)); left: 0; width: 100%; - // height: $s-8; border-bottom: $s-2 solid var(--resize-area-border-color); cursor: ns-resize; } diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs index 737f80fddd..ec072f367e 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs @@ -510,7 +510,7 @@ (mf/use-fn #(st/emit! (dw/toggle-focus-mode)))] - [:div#layers {:class (stl/css :layers) :data-testid "layers"} + [:div#layers {:class (stl/css :layers) :data-testid "layer-tree"} (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/blur.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.cljs index cd1573982a..b943680e8f 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.cljs @@ -87,6 +87,7 @@ :class (stl/css-case :title-spacing-blur (not has-value?))} (when-not has-value? [:button {:class (stl/css :add-blur) + :data-testid "add-blur" :on-click handle-add} i/add])]] (when (and open? has-value?) [:div {:class (stl/css :element-set-content)} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs index b4725a77e3..aa40b01c00 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs @@ -86,6 +86,7 @@ (mf/deps adjust-textarea-size creating?) (fn [event] (dom/stop-propagation event) + (rerender-fn) (when-let [textarea (mf/ref-val textarea-ref)] (dom/set-value! textarea annotation) (reset! editing* false) @@ -98,6 +99,7 @@ (mf/use-fn (fn [event] (dom/stop-propagation event) + (rerender-fn) (when ^boolean main-instance? (when-let [textarea (mf/ref-val textarea-ref)] (reset! editing* true) @@ -109,6 +111,7 @@ (mf/deps creating?) (fn [event] (dom/stop-propagation event) + (rerender-fn) (when-let [textarea (mf/ref-val textarea-ref)] (let [text (dom/get-value textarea)] (when-not (str/blank? text) @@ -124,6 +127,7 @@ (fn [event] (dom/stop-propagation event) (let [on-accept (fn [] + (rerender-fn) (st/emit! ;; (ptk/data-event {::ev/name "delete-component-annotation"}) (when creating? diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs index 7dfc6f0758..3d8574ef1f 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs @@ -146,6 +146,7 @@ (when (and (not disable-remove?) (not (= :multiple fills))) [:button {:class (stl/css :add-fill) + :data-testid "add-fill" :on-click on-add} i/add])]] (when open? diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs index ab65f806ad..db3b28f85c 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs @@ -298,6 +298,7 @@ (when-not (= :multiple shadows) [:button {:class (stl/css :add-shadow) + :data-testid "add-shadow" :on-click on-add-shadow} i/add])]] (when open? diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs index 3539f693f1..9f7969f2cd 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs @@ -169,9 +169,10 @@ :on-collapsed toggle-content :title label :class (stl/css-case :title-spacing-stroke (not has-strokes?))} - - [:button {:class (stl/css :add-stroke) - :on-click on-add-stroke} i/add]]] + (when (not (= :multiple strokes)) + [:button {:class (stl/css :add-stroke) + :data-testid "add-stroke" + :on-click on-add-stroke} i/add])]] (when open? [:div {:class (stl/css-case :element-content true :empty-content (not has-strokes?))} diff --git a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs index 39064f7c7a..1d9cc9e4d5 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs @@ -205,7 +205,6 @@ (fn [event] (st/emit! (dw/create-page {:file-id file-id :project-id project-id})) (-> event dom/get-current-target dom/blur!))) - size (if show-pages? size 32) read-only? (mf/use-ctx ctx/workspace-read-only?)] [:div {:class (stl/css :sitemap) diff --git a/frontend/src/app/main/ui/workspace/top_toolbar.cljs b/frontend/src/app/main/ui/workspace/top_toolbar.cljs index 979c84e19b..a7546507be 100644 --- a/frontend/src/app/main/ui/workspace/top_toolbar.cljs +++ b/frontend/src/app/main/ui/workspace/top_toolbar.cljs @@ -115,13 +115,16 @@ toggle-toolbar (mf/use-fn - #(st/emit! (dwc/toggle-toolbar-visibility)))] + (fn [event] + (dom/blur! (dom/get-target event)) + (st/emit! (dwc/toggle-toolbar-visibility))))] (when-not ^boolean read-only? [:aside {:class (stl/css-case :main-toolbar true :main-toolbar-no-rulers (not rulers?) :main-toolbar-hidden hide-toolbar?)} - [:ul {:class (stl/css :main-toolbar-options)} + [:ul {:class (stl/css :main-toolbar-options) + :data-testid "toolbar-options"} [:li [:button {:title (tr "workspace.toolbar.move" (sc/get-tooltip :move)) @@ -197,7 +200,9 @@ :on-click toggle-debug-panel} i/bug]])]] - [:button {:class (stl/css :toolbar-handler) + [:button {:title (tr "workspace.toolbar.toggle-toolbar") + :aria-label (tr "workspace.toolbar.toggle-toolbar") + :class (stl/css :toolbar-handler) :on-click toggle-toolbar} [:div {:class (stl/css :toolbar-handler-btn)}]]]))) diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index d2697e0184..670bd2c4cd 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -636,8 +636,8 @@ :objects base-objects :modifiers modifiers :shape frame - :view-only true}])) - + :view-only true}]))] + [:g.scrollbar-wrapper {:clipPath "url(#clip-handlers)"} [:& scroll-bars/viewport-scrollbars {:objects base-objects :zoom zoom diff --git a/frontend/src/app/main/ui/workspace/viewport/scroll_bars.cljs b/frontend/src/app/main/ui/workspace/viewport/scroll_bars.cljs index 1dd7a6f79e..76640fd88d 100644 --- a/frontend/src/app/main/ui/workspace/viewport/scroll_bars.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/scroll_bars.cljs @@ -196,7 +196,8 @@ [:* (when show-v-scroll? - [:g.v-scroll {:fill clr/black} + [:g.v-scroll {:fill clr/black + :data-testid "vertical-scrollbar"} [:rect {:on-pointer-move #(on-pointer-move % :y) :on-pointer-down #(on-pointer-down % :y) :on-pointer-up on-pointer-up @@ -210,7 +211,8 @@ :style {:stroke "white" :stroke-width (/ 0.15 zoom)}}]]) (when show-h-scroll? - [:g.h-scroll {:fill clr/black} + [:g.h-scroll {:fill clr/black + :data-testid "horizontal-scrollbar"} [:rect {:on-pointer-move #(on-pointer-move % :x) :on-pointer-down #(on-pointer-down % :x) :on-pointer-up on-pointer-up diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs index 6674446fcb..a9da0fd7c0 100644 --- a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs @@ -67,6 +67,7 @@ [:rect.selection-rect {:x (:x data) :y (:y data) + :data-testid "workspace-selection-rect" :width (:width data) :height (:height data) :style {;; Primary with 0.1 opacity diff --git a/frontend/test/frontend_tests/basic_shapes_test.cljs b/frontend/test/frontend_tests/basic_shapes_test.cljs index 8f7700120a..3017738374 100644 --- a/frontend/test/frontend_tests/basic_shapes_test.cljs +++ b/frontend/test/frontend_tests/basic_shapes_test.cljs @@ -9,6 +9,7 @@ [app.common.test-helpers.files :as cthf] [app.common.test-helpers.ids-map :as cthi] [app.common.test-helpers.shapes :as cths] + [app.main.data.workspace.colors :as dc] [app.main.data.workspace.shapes :as dwsh] [cljs.test :as t :include-macros true] [frontend-tests.helpers.state :as ths])) @@ -46,3 +47,36 @@ (t/is (= (count fills') 1)) (t/is (= (:fill-color fill') "#fabada")) (t/is (= (:fill-opacity fill') 1)))))))) + +(t/deftest test-update-stroke + ;; Old shapes without stroke-alignment are rendered as if it is centered + (t/async + done + (let [;; ==== Setup + store + (ths/setup-store + (-> (cthf/sample-file :file1 :page-label :page1) + (cths/add-sample-shape :shape1 :strokes [{:stroke-color "#000000" + :stroke-opacity 1 + :stroke-width 2}]))) + + ;; ==== Action + events + [(dc/change-stroke #{(cthi/id :shape1)} {:color "#FABADA"} 0)]] + + (ths/run-store + store done events + (fn [new-state] + (let [;; ==== Get + shape1' (get-in new-state [:workspace-data + :pages-index + (cthi/id :page1) + :objects + (cthi/id :shape1)]) + stroke' (-> (:strokes shape1') + first)] + + ;; ==== Check + (println stroke') + (t/is (some? shape1')) + (t/is (= (:stroke-alignment stroke') :center)))))))) \ No newline at end of file diff --git a/frontend/test/frontend_tests/logic/frame_guides_test.cljs b/frontend/test/frontend_tests/logic/frame_guides_test.cljs new file mode 100644 index 0000000000..8d12179733 --- /dev/null +++ b/frontend/test/frontend_tests/logic/frame_guides_test.cljs @@ -0,0 +1,57 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC +(ns frontend-tests.logic.frame-guides-test + (:require + [app.common.test-helpers.compositions :as ctho] + [app.common.test-helpers.files :as cthf] + [app.common.test-helpers.shapes :as cths] + [app.common.uuid :as uuid] + [app.main.data.workspace :as dw] + [app.main.data.workspace.guides :as-alias dwg] + [cljs.test :as t :include-macros true] + [frontend-tests.helpers.pages :as thp] + [frontend-tests.helpers.state :as ths])) + +(t/use-fixtures :each + {:before thp/reset-idmap!}) + + +(t/deftest test-remove-swap-slot-copy-paste-blue1-to-root + (t/async + done + (let [;; ==== Setup + file (-> (cthf/sample-file :file1) + (ctho/add-frame :frame1)) + store (ths/setup-store file) + frame1 (cths/get-shape file :frame1) + + guide {:axis :x + :frame-id (:id frame1) + :id (uuid/next) + :position 0} + + ;; ==== Action + events + [(dw/update-guides guide) + (dw/update-position (:id frame1) {:x 100})]] + + (ths/run-store + store done events + (fn [new-state] + (let [;; ==== Get + file' (ths/get-file-from-store new-state) + page' (cthf/current-page file') + + guide' (-> page' + :options + :guides + (vals) + (first))] + ;; ==== Check + ;; guide has moved + (t/is (= (:position guide') 100)))))))) + + diff --git a/frontend/test/frontend_tests/logic/groups_test.cljs b/frontend/test/frontend_tests/logic/groups_test.cljs new file mode 100644 index 0000000000..a535a586f1 --- /dev/null +++ b/frontend/test/frontend_tests/logic/groups_test.cljs @@ -0,0 +1,51 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC +(ns frontend-tests.logic.groups-test + (:require + [app.common.data :as d] + [app.common.test-helpers.compositions :as ctho] + [app.common.test-helpers.files :as cthf] + [app.common.test-helpers.shapes :as cths] + [app.common.uuid :as uuid] + [app.main.data.workspace :as dw] + [app.main.data.workspace.groups :as dwgr] + [app.main.data.workspace.selection :as dws] + [cljs.test :as t :include-macros true] + [frontend-tests.helpers.pages :as thp] + [frontend-tests.helpers.state :as ths])) + +(t/use-fixtures :each + {:before thp/reset-idmap!}) + + +(t/deftest test-create-group + (t/async + done + (let [;; ==== Setup + file (-> (cthf/sample-file :file1) + (cths/add-sample-shape :test-shape)) + store (ths/setup-store file) + test-shape (cths/get-shape file :test-shape) + + ;; ==== Action + events + [(dws/select-shapes (d/ordered-set (:id test-shape))) + (dwgr/group-selected)]] + + (ths/run-store + store done events + (fn [new-state] + (let [;; ==== Get + file' (ths/get-file-from-store new-state) + page' (cthf/current-page file') + group-id (->> (:objects page') + vals + (filter #(= :group (:type %))) + first + :id)] + ;; ==== Check + ;; Group has been created and is selected + (t/is (= (get-in new-state [:workspace-local :selected]) #{group-id})))))))) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 46f507c17f..6f87ee268d 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -5109,6 +5109,10 @@ msgstr "Text (%s)" msgid "workspace.toolbar.text-palette" msgstr "Typographies (%s)" +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.toggle-toolbar" +msgstr "Toggle toolbar" + msgid "workspace.top-bar.read-only.done" msgstr "Done" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index ea0c9e5d6f..35c05984f0 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -5271,6 +5271,10 @@ msgstr "Texto (%s)" msgid "workspace.toolbar.text-palette" msgstr "Tipografías (%s)" +#: src/app/main/ui/workspace/left_toolbar.cljs +msgid "workspace.toolbar.toggle-toolbar" +msgstr "Alternar barra de herramientas" + msgid "workspace.top-bar.read-only.done" msgstr "Hecho"