Merge remote-tracking branch 'origin/staging' into develop

This commit is contained in:
Alejandro Alonso 2024-06-13 11:04:58 +02:00
commit 7e87362a39
48 changed files with 995 additions and 145 deletions

View file

@ -26,9 +26,13 @@
- Allow library colors as recent colors [Taiga #7640](https://tree.taiga.io/project/penpot/issue/7640) - 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) - 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) - 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 ### :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 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) - 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) - 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 "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 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 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 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 ## 2.0.3

View file

@ -2,13 +2,19 @@
We want to thank to the amazing people that help us! Thank you! You're the best! 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 ## Security
* Husnain Iqbal (CEO OF ALPHA INFERNO PVT LTD) * Husnain Iqbal (CEO OF ALPHA INFERNO PVT LTD)
* [Shiraz Ali Khan](https://www.linkedin.com/in/shiraz-ali-khan-1ba508180/) * [Shiraz Ali Khan](https://www.linkedin.com/in/shiraz-ali-khan-1ba508180/)
* Vaibhav Shukla * Vaibhav Shukla
* Hassan Ahmed (Alias Xen Lee) * Hassan Ahmed (Alias Xen Lee)
* Michal Biesiada (@mbiesiad)
## Internationalization ## Internationalization
* [00ff88](https://hosted.weblate.org/user/00ff88) * [00ff88](https://hosted.weblate.org/user/00ff88)
* [AhmadHB](https://hosted.weblate.org/user/AhmadHB) * [AhmadHB](https://hosted.weblate.org/user/AhmadHB)
* [Aimee](https://hosted.weblate.org/user/Aimee) * [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) * [zcraber](https://hosted.weblate.org/user/zcraber)
## Libraries & templates ## Libraries & templates
* systxema * systxema
* plumilla * plumilla
* victor crespo * victor crespo

View file

@ -87,7 +87,10 @@
:ldap-attrs-fullname "cn" :ldap-attrs-fullname "cn"
;; a server prop key where initial project is stored. ;; 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 ::default-rpc-rlimit ::us/vector-of-strings)
(s/def ::rpc-rlimit-config ::fs/path) (s/def ::rpc-rlimit-config ::fs/path)
@ -213,6 +216,7 @@
(s/def ::telemetry-uri ::us/string) (s/def ::telemetry-uri ::us/string)
(s/def ::telemetry-with-taiga ::us/boolean) (s/def ::telemetry-with-taiga ::us/boolean)
(s/def ::tenant ::us/string) (s/def ::tenant ::us/string)
(s/def ::email-verify-threshold ::dt/duration)
(s/def ::config (s/def ::config
(s/keys :opt-un [::secret-key (s/keys :opt-un [::secret-key
@ -334,7 +338,8 @@
::telemetry-uri ::telemetry-uri
::telemetry-referer ::telemetry-referer
::telemetry-with-taiga ::telemetry-with-taiga
::tenant])) ::tenant
::email-verify-threshold]))
(def default-flags (def default-flags
[:enable-backend-api-doc [:enable-backend-api-doc

View file

@ -38,13 +38,11 @@
(def schema:token (def schema:token
[::sm/word-string {:max 6000}]) [::sm/word-string {:max 6000}])
(def ^:private default-verify-threshold
(dt/duration "15m"))
(defn- elapsed-verify-threshold? (defn- elapsed-verify-threshold?
[profile] [profile]
(let [elapsed (dt/diff (:modified-at profile) (dt/now))] (let [elapsed (dt/diff (:modified-at profile) (dt/now))
(pos? (compare elapsed default-verify-threshold)))) verify-threshold (cf/get :email-verify-threshold)]
(pos? (compare elapsed verify-threshold))))
;; ---- COMMAND: login with password ;; ---- COMMAND: login with password
@ -130,12 +128,21 @@
;; ---- COMMAND: Logout ;; ---- COMMAND: Logout
(def ^:private schema:logout
[:map {:title "logoug"}
[:profile-id {:optional true} ::sm/uuid]])
(sv/defmethod ::logout (sv/defmethod ::logout
"Clears the authentication cookie and logout the current session." "Clears the authentication cookie and logout the current session."
{::rpc/auth false {::rpc/auth false
::doc/added "1.15"} ::doc/changes [["2.1" "Now requires profile-id passed in the body"]]
[cfg _] ::doc/added "1.0"
(rph/with-transform {} (session/delete-fn cfg))) ::sm/params schema:logout}
[cfg params]
(if (= (:profile-id params)
(::rpc/profile-id params))
(rph/with-transform {} (session/delete-fn cfg))
{}))
;; ---- COMMAND: Recover Profile ;; ---- COMMAND: Recover Profile

View file

@ -184,10 +184,7 @@
(ctk/instance-head? child)) (ctk/instance-head? child))
(let [slot (guess-swap-slot component-child component-container)] (let [slot (guess-swap-slot component-child component-container)]
(l/dbg :hint "child" :id (:id child) :name (:name child) :slot slot) (l/dbg :hint "child" :id (:id child) :name (:name child) :slot slot)
(ctn/update-shape container (:id child) (ctn/update-shape container (:id child) #(ctk/set-swap-slot % slot)))
#(update % :touched
cfh/set-touched-group
(ctk/build-swap-slot-group slot))))
container)] container)]
(recur (process-copy-head container child) (recur (process-copy-head container child)
(rest children) (rest children)

View file

@ -481,7 +481,7 @@
(let [slot (:swap-slot args)] (let [slot (:swap-slot args)]
(when (some? slot) (when (some? slot)
(log/debug :hint (str " -> set swap-slot to " 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) (log/dbg :hint "repairing shape :missing-slot" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id) (-> (pcb/empty-changes nil page-id)

View file

@ -284,9 +284,17 @@
(let [children (cfh/get-children-with-self (:objects container) shape-id) (let [children (cfh/get-children-with-self (:objects container) shape-id)
skip-near (fn [changes shape] skip-near (fn [changes shape]
(let [ref-shape (ctf/find-ref-shape file container libraries shape {:include-deleted? true})] (let [ref-shape (ctf/find-ref-shape file container libraries shape {:include-deleted? true})]
(if (some? (:shape-ref ref-shape)) (cond-> changes
(pcb/update-shapes changes [(:id shape)] #(assoc % :shape-ref (:shape-ref ref-shape))) (some? (:shape-ref ref-shape))
changes)))] (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))) (reduce skip-near changes children)))
(defn prepare-restore-component (defn prepare-restore-component
@ -1194,7 +1202,7 @@
:shapes all-parents})) :shapes all-parents}))
changes' (reduce del-obj-change changes' new-shapes)] 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
changes'))) changes')))
@ -1349,7 +1357,7 @@
changes' changes'
ids)] ids)]
(if (and (cfh/touched-group? parent :shapes-group) omit-touched?) (if (and (ctk/touched-group? parent :shapes-group) omit-touched?)
changes changes
changes'))) changes')))
@ -1385,7 +1393,7 @@
:ignore-touched true :ignore-touched true
:syncing 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
changes'))) changes')))
@ -1846,12 +1854,11 @@
;; if the shape isn't inside a main component, it shouldn't have a swap slot ;; if the shape isn't inside a main component, it shouldn't have a swap slot
(and (nil? (ctk/get-swap-slot new-shape)) (and (nil? (ctk/get-swap-slot new-shape))
inside-comp?) inside-comp?)
(update :touched cfh/set-touched-group (-> (ctf/find-swap-slot shape (ctk/set-swap-slot (ctf/find-swap-slot shape
page page
{:id (:id file) {:id (:id file)
:data file} :data file}
libraries) libraries)))]
(ctk/build-swap-slot-group))))]
[new-shape (-> changes [new-shape (-> changes
;; Restore the properties ;; Restore the properties

View file

@ -183,6 +183,15 @@
(and (= shape-id (:main-instance-id component)) (and (= shape-id (:main-instance-id component))
(= page-id (:main-instance-page 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 (defn build-swap-slot-group
"Convert a swap-slot into a :touched group" "Convert a swap-slot into a :touched group"
[swap-slot] [swap-slot]
@ -204,6 +213,13 @@
(when group (when group
(group->swap-slot group)))) (group->swap-slot group))))
(defn set-swap-slot
"Add a touched group with a form :swap-slot-<uuid>."
[shape swap-slot]
(cond-> shape
(some? swap-slot)
(update :touched set-touched-group (build-swap-slot-group swap-slot))))
(defn match-swap-slot? (defn match-swap-slot?
[shape-main shape-inst] [shape-main shape-inst]
(let [slot-main (get-swap-slot shape-main) (let [slot-main (get-swap-slot shape-main)

View file

@ -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
}
]
}
}

View file

@ -4,11 +4,6 @@ export class DashboardPage extends BaseWebSocketPage {
static async init(page) { static async init(page) {
await BaseWebSocketPage.initWebSockets(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, "get-teams", "logged-in-user/get-teams-default.json");
await BaseWebSocketPage.mockRPC( await BaseWebSocketPage.mockRPC(
page, page,

View file

@ -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;

View file

@ -43,13 +43,15 @@ export class WorkspacePage extends BaseWebSocketPage {
this.presentUserListItems = page.getByTestId("active-users-list").getByAltText("Princesa Leia"); this.presentUserListItems = page.getByTestId("active-users-list").getByAltText("Princesa Leia");
this.viewport = page.getByTestId("viewport"); this.viewport = page.getByTestId("viewport");
this.rootShape = page.locator(`[id="shape-00000000-0000-0000-0000-000000000000"]`); 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.rectShapeButton = page.getByRole("button", { name: "Rectangle (R)" });
this.toggleToolbarButton = page.getByRole("button", { name: "Toggle toolbar" });
this.colorpicker = page.getByTestId("colorpicker"); this.colorpicker = page.getByTestId("colorpicker");
this.layers = page.getByTestId("layers"); this.layers = page.getByTestId("layer-tree");
this.palette = page.getByTestId("palette"); this.palette = page.getByTestId("palette");
this.assets = page.getByTestId("assets"); this.sidebar = page.getByTestId("left-sidebar");
this.libraries = page.getByTestId("libraries"); this.selectionRect = page.getByTestId("workspace-selection-rect");
this.closeLibraries = page.getByTestId("close-libraries"); this.horizontalScrollbar = page.getByTestId("horizontal-scrollbar");
this.librariesModal = page.getByTestId("libraries-modal"); this.librariesModal = page.getByTestId("libraries-modal");
} }
@ -102,6 +104,19 @@ export class WorkspacePage extends BaseWebSocketPage {
await this.page.mouse.up(); 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) { async moveSelectionToShape(name) {
await this.page.locator('rect.viewport-selrect').hover(); await this.page.locator('rect.viewport-selrect').hover();
await this.page.mouse.down(); await this.page.mouse.down();
@ -120,15 +135,21 @@ export class WorkspacePage extends BaseWebSocketPage {
} }
async expectSelectedLayer(name) { 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 = {}) { async clickAssets(clickOptions = {}) {
await this.assets.click(clickOptions); await this.sidebar.getByText("Assets").click(clickOptions);
} }
async clickLibraries(clickOptions = {}) { async openLibrariesModal(clickOptions = {}) {
await this.libraries.click(clickOptions); await this.sidebar.getByText("Libraries").click(clickOptions);
} }
async clickLibrary(name, clickOptions = {}) { async clickLibrary(name, clickOptions = {}) {
@ -136,11 +157,15 @@ export class WorkspacePage extends BaseWebSocketPage {
.getByTestId("library-item") .getByTestId("library-item")
.filter({ hasText: name }) .filter({ hasText: name })
.getByRole("button") .getByRole("button")
.click(clickOptions); .click(clickOptions);
} }
async clickCloseLibraries(clickOptions = {}) { async closeLibrariesModal(clickOptions = {}) {
await this.closeLibraries.click(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 = {}) { async clickColorPalette(clickOptions = {}) {

View file

@ -3,6 +3,11 @@ import DashboardPage from "../pages/DashboardPage";
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
await DashboardPage.init(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 }) => { test("Dashboad page has title ", async ({ page }) => {

View file

@ -7,6 +7,8 @@ test.beforeEach(async ({ page }) => {
const multipleConstraintsFileId = `03bff843-920f-81a1-8004-756365e1eb6a`; const multipleConstraintsFileId = `03bff843-920f-81a1-8004-756365e1eb6a`;
const multipleConstraintsPageId = `03bff843-920f-81a1-8004-756365e1eb6b`; 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) => { const setupFileWithMultipeConstraints = async (workspace) => {
await workspace.setupEmptyFile(); 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.describe("Constraints", () => {
test("Constraint dropdown shows 'Mixed' when multiple layers are selected with different constraints", async ({ test("Constraint dropdown shows 'Mixed' when multiple layers are selected with different constraints", async ({
page, 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 }) => { test("BUG 7760 - Layout losing properties when changing parents", async ({ page }) => {
const workspacePage = new WorkspacePage(page); const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile(); await workspacePage.setupEmptyFile();

View file

@ -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();
});

View file

@ -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();
});
});

View file

@ -39,6 +39,60 @@ test("User draws a rect", async ({ page }) => {
await expect(shape).toHaveAttribute("height", "100"); 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 }) => { test("User adds a library and its automatically selected in the color palette", async ({ page }) => {
const workspacePage = new WorkspacePage(page); const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile(); await workspacePage.setupEmptyFile();
@ -53,31 +107,16 @@ test("User adds a library and its automatically selected in the color palette",
await workspacePage.clickAssets(); await workspacePage.clickAssets();
// Now the get-file call should return a library // Now the get-file call should return a library
await workspacePage.mockRPC(/get\-file\?/, "workspace/get-file-library.json"); await workspacePage.mockRPC(/get\-file\?/, "workspace/get-file-library.json");
await workspacePage.clickLibraries(); await workspacePage.openLibrariesModal();
await workspacePage.clickLibrary("Testing library 1") await workspacePage.clickLibrary("Testing library 1")
await workspacePage.clickCloseLibraries(); await workspacePage.closeLibrariesModal();
await expect(workspacePage.palette.getByRole("button", { name: "test-color-187cd5" })).toBeVisible(); await expect(workspacePage.palette.getByRole("button", { name: "test-color-187cd5" })).toBeVisible();
// Remove Testing library 1 // Remove Testing library 1
await workspacePage.clickLibraries(); await workspacePage.openLibrariesModal();
await workspacePage.clickLibrary("Testing library 1") 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(); 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");
});

View file

@ -328,11 +328,15 @@
(-data [_] {}) (-data [_] {})
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ state _]
(->> (rp/cmd! :logout) (let [profile-id (:profile-id state)]
(rx/delay-at-least 300) (->> (rx/interval 500)
(rx/catch (constantly (rx/of 1))) (rx/take 1)
(rx/map #(logged-out params))))))) (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 ;; --- Update Profile

View file

@ -248,7 +248,7 @@
(assoc :stroke-style :solid) (assoc :stroke-style :solid)
(not (contains? new-attrs :stroke-alignment)) (not (contains? new-attrs :stroke-alignment))
(assoc :stroke-alignment :inner) (assoc :stroke-alignment :center)
:always :always
(d/without-nils))] (d/without-nils))]

View file

@ -198,7 +198,8 @@
(dws/select-shapes (d/ordered-set (:id group)))) (dws/select-shapes (d/ordered-set (:id group))))
(ptk/data-event :layout/update {:ids parents})))))))) (ptk/data-event :layout/update {:ids parents}))))))))
(def group-selected (defn group-selected
[]
(ptk/reify ::group-selected (ptk/reify ::group-selected
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
@ -258,7 +259,8 @@
(when change-selection? (when change-selection?
(dws/select-shapes child-ids)))))))) (dws/select-shapes child-ids))))))))
(def ungroup-selected (defn ungroup-selected
[]
(ptk/reify ::ungroup-selected (ptk/reify ::ungroup-selected
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]

View file

@ -79,20 +79,21 @@
(rx/from (->> guides (mapv #(remove-guide %)))))))) (rx/from (->> guides (mapv #(remove-guide %))))))))
(defmethod ptk/resolve ::move-frame-guides (defmethod ptk/resolve ::move-frame-guides
[_ ids] [_ args]
(dm/assert! (dm/assert!
"expected a coll of uuids" "expected a coll of uuids"
(every? uuid? ids)) (every? uuid? (:ids args)))
(ptk/reify ::move-frame-guides (ptk/reify ::move-frame-guides
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (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]))) is-frame? (fn [id] (= :frame (get-in objects [id :type])))
frame-ids? (into #{} (filter is-frame?) ids) frame-ids? (into #{} (filter is-frame?) ids)
object-modifiers (get state :workspace-modifiers)
build-move-event build-move-event
(fn [guide] (fn [guide]
(let [frame (get objects (:frame-id guide)) (let [frame (get objects (:frame-id guide))

View file

@ -497,7 +497,7 @@
(if undo-transation? (if undo-transation?
(rx/of (dwu/start-undo-transaction undo-id)) (rx/of (dwu/start-undo-transaction undo-id))
(rx/empty)) (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) (ptk/event ::dwcm/move-frame-comment-threads ids-with-children)
(dwsh/update-shapes (dwsh/update-shapes
ids ids

View file

@ -22,6 +22,7 @@
(or (= type ::common/finish-path) (or (= type ::common/finish-path)
(= type :app.main.data.workspace.path.shortcuts/esc-pressed) (= type :app.main.data.workspace.path.shortcuts/esc-pressed)
(= type :app.main.data.workspace.common/clear-edition-mode) (= 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) (= type :app.main.data.workspace/finalize-page)
(= event :interrupt) ;; ESC (= event :interrupt) ;; ESC
(and ^boolean (mse/mouse-event? event) (and ^boolean (mse/mouse-event? event)

View file

@ -119,12 +119,12 @@
:group {:tooltip (ds/meta "G") :group {:tooltip (ds/meta "G")
:command (ds/c-mod "g") :command (ds/c-mod "g")
:subsections [:modify-layers] :subsections [:modify-layers]
:fn #(emit-when-no-readonly dw/group-selected)} :fn #(emit-when-no-readonly (dw/group-selected))}
:ungroup {:tooltip (ds/shift "G") :ungroup {:tooltip (ds/shift "G")
:command "shift+g" :command "shift+g"
:subsections [:modify-layers] :subsections [:modify-layers]
:fn #(emit-when-no-readonly dw/ungroup-selected)} :fn #(emit-when-no-readonly (dw/ungroup-selected))}
:mask {:tooltip (ds/meta "M") :mask {:tooltip (ds/meta "M")
:command (ds/c-mod "m") :command (ds/c-mod "m")

View file

@ -20,6 +20,7 @@
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.util.i18n :refer [tr tr-html]] [app.util.i18n :refer [tr tr-html]]
[app.util.router :as rt] [app.util.router :as rt]
[app.util.storage :as sto]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
[cljs.spec.alpha :as s] [cljs.spec.alpha :as s]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
@ -163,11 +164,7 @@
;; --- PAGE: register validation ;; --- PAGE: register validation
(defn- handle-register-error (defn- on-register-success
[_form _data]
(st/emit! (msg/error (tr "errors.generic"))))
(defn- handle-register-success
[data] [data]
(cond (cond
(some? (:invitation-token data)) (some? (:invitation-token data))
@ -178,7 +175,9 @@
(st/emit! (du/login-from-register)) (st/emit! (du/login-from-register))
:else :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-terms-and-privacy (s/and ::us/boolean true?))
(s/def ::accept-newsletter-subscription ::us/boolean) (s/def ::accept-newsletter-subscription ::us/boolean)
@ -192,31 +191,63 @@
:opt-un [::accept-terms-and-privacy :opt-un [::accept-terms-and-privacy
::accept-newsletter-subscription]))) ::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/defc register-validate-form
{::mf/props :obj}
[{:keys [params on-success-callback]}] [{:keys [params on-success-callback]}]
(let [form (fm/use-form :spec ::register-validate-form (let [validators (mf/with-memo []
:validators [(fm/validate-not-empty :fullname (tr "auth.name.not-all-space")) [(fm/validate-not-empty :fullname (tr "auth.name.not-all-space"))
(fm/validate-length :fullname fm/max-length-allowed (tr "auth.name.too-long"))] (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) :initial params)
submitted? (mf/use-state false) submitted? (mf/use-state false)
on-success (fn [p] on-success
(if (nil? on-success-callback) (mf/use-fn
(handle-register-success p) (mf/deps on-success-callback)
(on-success-callback (:email p)))) (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 on-submit
(mf/use-fn (mf/use-fn
(fn [form _event] (fn [form _]
(reset! submitted? true) (reset! submitted? true)
(let [params (:clean-data @form)] (let [params (:clean-data @form)]
(->> (rp/cmd! :register-profile params) (->> (rp/cmd! :register-profile params)
(rx/finalize #(reset! submitted? false)) (rx/finalize #(reset! submitted? false))
(rx/subs! on-success (rx/subs! on-success on-error)))))]
(partial handle-register-error form))))))]
[:& fm/form {:on-submit on-submit :form form [:& fm/form {:on-submit on-submit
:form form
:class (stl/css :register-validate-form)} :class (stl/css :register-validate-form)}
[:div {:class (stl/css :fields-row)} [:div {:class (stl/css :fields-row)}
[:& fm/input {:name :fullname [:& fm/input {:name :fullname
:label (tr "auth.fullname") :label (tr "auth.fullname")
@ -225,18 +256,7 @@
:class (stl/css :form-field)}]] :class (stl/css :form-field)}]]
(when (contains? cf/flags :terms-and-privacy-checkbox) (when (contains? cf/flags :terms-and-privacy-checkbox)
(let [terms-label [:& terms-and-privacy])
(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}]]))
[:> fm/submit-button* [:> fm/submit-button*
{:label (tr "auth.register-submit") {:label (tr "auth.register-submit")
@ -245,6 +265,7 @@
(mf/defc register-validate-page (mf/defc register-validate-page
{::mf/props :obj}
[{:keys [params]}] [{:keys [params]}]
[:div {:class (stl/css :auth-form-wrapper)} [:div {:class (stl/css :auth-form-wrapper)}
[:h1 {:class (stl/css :logo-container)} [:h1 {:class (stl/css :logo-container)}
@ -263,13 +284,15 @@
(tr "labels.go-back")]]]]) (tr "labels.go-back")]]]])
(mf/defc register-success-page (mf/defc register-success-page
[{:keys [params]}] {::mf/props :obj}
[:div {:class (stl/css :auth-form-wrapper :register-success)} []
[:h1 {:class (stl/css :logo-container)} (let [email (::email @sto/storage)]
[:a {:href "#/" :title "Penpot" :class (stl/css :logo-btn)} i/logo]] [:div {:class (stl/css :auth-form-wrapper :register-success)}
[:div {:class (stl/css :auth-title-wrapper)} [:h1 {:class (stl/css :logo-container)}
[:h2 {:class (stl/css :auth-title)} [:a {:href "#/" :title "Penpot" :class (stl/css :logo-btn)} i/logo]]
(tr "auth.check-mail")] [:div {:class (stl/css :auth-title-wrapper)}
[:div {:class (stl/css :notification-text)} (tr "auth.verification-email-sent")]] [:h2 {:class (stl/css :auth-title)}
[:div {:class (stl/css :notification-text-email)} (:email params "")] (tr "auth.check-mail")]
[:div {:class (stl/css :notification-text)} (tr "auth.check-your-email")]]) [: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")]]))

View file

@ -142,11 +142,10 @@
// thread-content // thread-content
.thread-content { .thread-content {
position: absolute; position: absolute;
overflow-y: scroll; overflow-y: auto;
scrollbar-gutter: stable;
width: $s-284; width: $s-284;
padding: $s-12; padding: $s-12;
padding-inline-end: 0; padding-inline-end: $s-8;
pointer-events: auto; pointer-events: auto;
user-select: text; user-select: text;

View file

@ -44,9 +44,17 @@
(or (empty? overlays-ids) (nil? shape) (cfh/root? shape)) base-frame (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))) :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 (defn- activate-interaction
[interaction shape base-frame frame-offset objects overlays] [interaction shape base-frame frame-offset objects overlays]
(case (:action-type interaction) (case (:action-type interaction)
:navigate :navigate
(when-let [frame-id (:destination interaction)] (when-let [frame-id (:destination interaction)]
@ -58,9 +66,11 @@
(dv/go-to-frame frame-id (:animation interaction))))) (dv/go-to-frame frame-id (:animation interaction)))))
:open-overlay :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) 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" (if (= (:type shape) :frame) ;; manual interactions are always from "self"
(:frame-id shape) (:frame-id shape)
(:id shape)) (:id shape))
@ -88,7 +98,9 @@
fixed-base?)))) fixed-base?))))
:toggle-overlay :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) dest-frame (get objects dest-frame-id)
relative-to-id (if (= :manual (:overlay-pos-type interaction)) relative-to-id (if (= :manual (:overlay-pos-type interaction))
(if (= (:type shape) :frame) ;; manual interactions are always from "self" (if (= (:type shape) :frame) ;; manual interactions are always from "self"
@ -146,7 +158,9 @@
(st/emit! (dv/close-overlay frame-id))) (st/emit! (dv/close-overlay frame-id)))
:toggle-overlay :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) dest-frame (get objects dest-frame-id)
relative-to-id (if (= :manual (:overlay-pos-type interaction)) relative-to-id (if (= :manual (:overlay-pos-type interaction))
(if (= (:type shape) :frame) ;; manual interactions are always from "self" (if (= (:type shape) :frame) ;; manual interactions are always from "self"
@ -178,7 +192,9 @@
:close-overlay :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) dest-frame (get objects dest-frame-id)
relative-to-id (if (= :manual (:overlay-pos-type interaction)) relative-to-id (if (= :manual (:overlay-pos-type interaction))
(if (= (:type shape) :frame) ;; manual interactions are always from "self" (if (= (:type shape) :frame) ;; manual interactions are always from "self"

View file

@ -243,8 +243,8 @@
is-group? (and single? has-group?) is-group? (and single? has-group?)
is-bool? (and single? has-bool?) is-bool? (and single? has-bool?)
do-create-group #(st/emit! dw/group-selected) do-create-group #(st/emit! (dw/group-selected))
do-remove-group #(st/emit! dw/ungroup-selected) do-remove-group #(st/emit! (dw/ungroup-selected))
do-mask-group #(st/emit! (dw/mask-group)) do-mask-group #(st/emit! (dw/mask-group))
do-unmask-group #(st/emit! (dw/unmask-group)) do-unmask-group #(st/emit! (dw/unmask-group))
do-create-artboard-from-selection do-create-artboard-from-selection

View file

@ -519,6 +519,7 @@
[:div {:class (stl/css :modal-dialog)} [:div {:class (stl/css :modal-dialog)}
[:button {:class (stl/css :close-btn) [:button {:class (stl/css :close-btn)
:on-click close-dialog :on-click close-dialog
:aria-label (tr "labels.close")
:data-testid "close-libraries"} :data-testid "close-libraries"}
close-icon] close-icon]
[:div {:class (stl/css :modal-title)} [:div {:class (stl/css :modal-title)}

View file

@ -38,8 +38,7 @@
(let [options-mode (mf/deref refs/options-mode-global) (let [options-mode (mf/deref refs/options-mode-global)
mode-inspect? (= options-mode :inspect) mode-inspect? (= options-mode :inspect)
project (mf/deref refs/workspace-project) 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 section (cond (or mode-inspect? (contains? layout :layers)) :layers
(contains? layout :assets) :assets) (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} {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) (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) (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 handle-collapse
(mf/use-fn #(st/emit! (dw/toggle-layout-flag :collapse-left-sidebar))) (mf/use-fn #(st/emit! (dw/toggle-layout-flag :collapse-left-sidebar)))
@ -63,6 +65,7 @@
[:& (mf/provider muc/sidebar) {:value :left} [:& (mf/provider muc/sidebar) {:value :left}
[:aside {:ref parent-ref [:aside {:ref parent-ref
:id "left-sidebar-aside" :id "left-sidebar-aside"
:data-testid "left-sidebar"
:data-size (str size) :data-size (str size)
:class (stl/css-case :left-settings-bar true :class (stl/css-case :left-settings-bar true
:global/two-row (<= size 300) :global/two-row (<= size 300)

View file

@ -84,10 +84,8 @@ $width-settings-bar-max: $s-500;
.resize-area-horiz { .resize-area-horiz {
position: absolute; position: absolute;
// top: calc($s-88 + var(--height, 200px));
left: 0; left: 0;
width: 100%; width: 100%;
// height: $s-8;
border-bottom: $s-2 solid var(--resize-area-border-color); border-bottom: $s-2 solid var(--resize-area-border-color);
cursor: ns-resize; cursor: ns-resize;
} }

View file

@ -510,7 +510,7 @@
(mf/use-fn (mf/use-fn
#(st/emit! (dw/toggle-focus-mode)))] #(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) (if (d/not-empty? focus)
[:div {:class (stl/css :tool-window-bar)} [:div {:class (stl/css :tool-window-bar)}
[:button {:class (stl/css :focus-title) [:button {:class (stl/css :focus-title)

View file

@ -87,6 +87,7 @@
:class (stl/css-case :title-spacing-blur (not has-value?))} :class (stl/css-case :title-spacing-blur (not has-value?))}
(when-not has-value? (when-not has-value?
[:button {:class (stl/css :add-blur) [:button {:class (stl/css :add-blur)
:data-testid "add-blur"
:on-click handle-add} i/add])]] :on-click handle-add} i/add])]]
(when (and open? has-value?) (when (and open? has-value?)
[:div {:class (stl/css :element-set-content)} [:div {:class (stl/css :element-set-content)}

View file

@ -86,6 +86,7 @@
(mf/deps adjust-textarea-size creating?) (mf/deps adjust-textarea-size creating?)
(fn [event] (fn [event]
(dom/stop-propagation event) (dom/stop-propagation event)
(rerender-fn)
(when-let [textarea (mf/ref-val textarea-ref)] (when-let [textarea (mf/ref-val textarea-ref)]
(dom/set-value! textarea annotation) (dom/set-value! textarea annotation)
(reset! editing* false) (reset! editing* false)
@ -98,6 +99,7 @@
(mf/use-fn (mf/use-fn
(fn [event] (fn [event]
(dom/stop-propagation event) (dom/stop-propagation event)
(rerender-fn)
(when ^boolean main-instance? (when ^boolean main-instance?
(when-let [textarea (mf/ref-val textarea-ref)] (when-let [textarea (mf/ref-val textarea-ref)]
(reset! editing* true) (reset! editing* true)
@ -109,6 +111,7 @@
(mf/deps creating?) (mf/deps creating?)
(fn [event] (fn [event]
(dom/stop-propagation event) (dom/stop-propagation event)
(rerender-fn)
(when-let [textarea (mf/ref-val textarea-ref)] (when-let [textarea (mf/ref-val textarea-ref)]
(let [text (dom/get-value textarea)] (let [text (dom/get-value textarea)]
(when-not (str/blank? text) (when-not (str/blank? text)
@ -124,6 +127,7 @@
(fn [event] (fn [event]
(dom/stop-propagation event) (dom/stop-propagation event)
(let [on-accept (fn [] (let [on-accept (fn []
(rerender-fn)
(st/emit! (st/emit!
;; (ptk/data-event {::ev/name "delete-component-annotation"}) ;; (ptk/data-event {::ev/name "delete-component-annotation"})
(when creating? (when creating?

View file

@ -146,6 +146,7 @@
(when (and (not disable-remove?) (not (= :multiple fills))) (when (and (not disable-remove?) (not (= :multiple fills)))
[:button {:class (stl/css :add-fill) [:button {:class (stl/css :add-fill)
:data-testid "add-fill"
:on-click on-add} i/add])]] :on-click on-add} i/add])]]
(when open? (when open?

View file

@ -298,6 +298,7 @@
(when-not (= :multiple shadows) (when-not (= :multiple shadows)
[:button {:class (stl/css :add-shadow) [:button {:class (stl/css :add-shadow)
:data-testid "add-shadow"
:on-click on-add-shadow} i/add])]] :on-click on-add-shadow} i/add])]]
(when open? (when open?

View file

@ -169,9 +169,10 @@
:on-collapsed toggle-content :on-collapsed toggle-content
:title label :title label
:class (stl/css-case :title-spacing-stroke (not has-strokes?))} :class (stl/css-case :title-spacing-stroke (not has-strokes?))}
(when (not (= :multiple strokes))
[:button {:class (stl/css :add-stroke) [:button {:class (stl/css :add-stroke)
:on-click on-add-stroke} i/add]]] :data-testid "add-stroke"
:on-click on-add-stroke} i/add])]]
(when open? (when open?
[:div {:class (stl/css-case :element-content true [:div {:class (stl/css-case :element-content true
:empty-content (not has-strokes?))} :empty-content (not has-strokes?))}

View file

@ -205,7 +205,6 @@
(fn [event] (fn [event]
(st/emit! (dw/create-page {:file-id file-id :project-id project-id})) (st/emit! (dw/create-page {:file-id file-id :project-id project-id}))
(-> event dom/get-current-target dom/blur!))) (-> event dom/get-current-target dom/blur!)))
size (if show-pages? size 32)
read-only? (mf/use-ctx ctx/workspace-read-only?)] read-only? (mf/use-ctx ctx/workspace-read-only?)]
[:div {:class (stl/css :sitemap) [:div {:class (stl/css :sitemap)

View file

@ -115,13 +115,16 @@
toggle-toolbar toggle-toolbar
(mf/use-fn (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? (when-not ^boolean read-only?
[:aside {:class (stl/css-case :main-toolbar true [:aside {:class (stl/css-case :main-toolbar true
:main-toolbar-no-rulers (not rulers?) :main-toolbar-no-rulers (not rulers?)
:main-toolbar-hidden hide-toolbar?)} :main-toolbar-hidden hide-toolbar?)}
[:ul {:class (stl/css :main-toolbar-options)} [:ul {:class (stl/css :main-toolbar-options)
:data-testid "toolbar-options"}
[:li [:li
[:button [:button
{:title (tr "workspace.toolbar.move" (sc/get-tooltip :move)) {:title (tr "workspace.toolbar.move" (sc/get-tooltip :move))
@ -197,7 +200,9 @@
:on-click toggle-debug-panel} :on-click toggle-debug-panel}
i/bug]])]] 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} :on-click toggle-toolbar}
[:div {:class (stl/css :toolbar-handler-btn)}]]]))) [:div {:class (stl/css :toolbar-handler-btn)}]]])))

View file

@ -636,8 +636,8 @@
:objects base-objects :objects base-objects
:modifiers modifiers :modifiers modifiers
:shape frame :shape frame
:view-only true}])) :view-only true}]))]
[:g.scrollbar-wrapper {:clipPath "url(#clip-handlers)"}
[:& scroll-bars/viewport-scrollbars [:& scroll-bars/viewport-scrollbars
{:objects base-objects {:objects base-objects
:zoom zoom :zoom zoom

View file

@ -196,7 +196,8 @@
[:* [:*
(when show-v-scroll? (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) [:rect {:on-pointer-move #(on-pointer-move % :y)
:on-pointer-down #(on-pointer-down % :y) :on-pointer-down #(on-pointer-down % :y)
:on-pointer-up on-pointer-up :on-pointer-up on-pointer-up
@ -210,7 +211,8 @@
:style {:stroke "white" :style {:stroke "white"
:stroke-width (/ 0.15 zoom)}}]]) :stroke-width (/ 0.15 zoom)}}]])
(when show-h-scroll? (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) [:rect {:on-pointer-move #(on-pointer-move % :x)
:on-pointer-down #(on-pointer-down % :x) :on-pointer-down #(on-pointer-down % :x)
:on-pointer-up on-pointer-up :on-pointer-up on-pointer-up

View file

@ -67,6 +67,7 @@
[:rect.selection-rect [:rect.selection-rect
{:x (:x data) {:x (:x data)
:y (:y data) :y (:y data)
:data-testid "workspace-selection-rect"
:width (:width data) :width (:width data)
:height (:height data) :height (:height data)
:style {;; Primary with 0.1 opacity :style {;; Primary with 0.1 opacity

View file

@ -9,6 +9,7 @@
[app.common.test-helpers.files :as cthf] [app.common.test-helpers.files :as cthf]
[app.common.test-helpers.ids-map :as cthi] [app.common.test-helpers.ids-map :as cthi]
[app.common.test-helpers.shapes :as cths] [app.common.test-helpers.shapes :as cths]
[app.main.data.workspace.colors :as dc]
[app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.shapes :as dwsh]
[cljs.test :as t :include-macros true] [cljs.test :as t :include-macros true]
[frontend-tests.helpers.state :as ths])) [frontend-tests.helpers.state :as ths]))
@ -46,3 +47,36 @@
(t/is (= (count fills') 1)) (t/is (= (count fills') 1))
(t/is (= (:fill-color fill') "#fabada")) (t/is (= (:fill-color fill') "#fabada"))
(t/is (= (:fill-opacity fill') 1)))))))) (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))))))))

View file

@ -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))))))))

View file

@ -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}))))))))

View file

@ -5109,6 +5109,10 @@ msgstr "Text (%s)"
msgid "workspace.toolbar.text-palette" msgid "workspace.toolbar.text-palette"
msgstr "Typographies (%s)" 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" msgid "workspace.top-bar.read-only.done"
msgstr "Done" msgstr "Done"

View file

@ -5271,6 +5271,10 @@ msgstr "Texto (%s)"
msgid "workspace.toolbar.text-palette" msgid "workspace.toolbar.text-palette"
msgstr "Tipografías (%s)" 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" msgid "workspace.top-bar.read-only.done"
msgstr "Hecho" msgstr "Hecho"