diff --git a/backend/src/app/rpc/commands/files_update.clj b/backend/src/app/rpc/commands/files_update.clj index c9bf2aea9..b071b3f1a 100644 --- a/backend/src/app/rpc/commands/files_update.clj +++ b/backend/src/app/rpc/commands/files_update.clj @@ -324,19 +324,21 @@ (update :data cpc/process-changes changes) (update :data d/without-nils))] - (when (contains? cf/flags :soft-file-validation) - (soft-validate-file! file libs)) - (when (contains? cf/flags :soft-file-schema-validation) - (soft-validate-file-schema! file)) + (binding [pmap/*tracked* nil] + (when (contains? cf/flags :soft-file-validation) + (soft-validate-file! file libs)) - (when (and (contains? cf/flags :file-validation) - (not skip-validate)) - (val/validate-file! file libs)) + (when (contains? cf/flags :soft-file-schema-validation) + (soft-validate-file-schema! file)) - (when (and (contains? cf/flags :file-schema-validation) - (not skip-validate)) - (val/validate-file-schema! file)) + (when (and (contains? cf/flags :file-validation) + (not skip-validate)) + (val/validate-file! file libs)) + + (when (and (contains? cf/flags :file-schema-validation) + (not skip-validate)) + (val/validate-file-schema! file))) (cond-> file (contains? cfeat/*current* "fdata/objects-map") diff --git a/common/src/app/common/files/repair.cljc b/common/src/app/common/files/repair.cljc index c40b60227..67f90dafe 100644 --- a/common/src/app/common/files/repair.cljc +++ b/common/src/app/common/files/repair.cljc @@ -473,6 +473,59 @@ (pcb/with-file-data file-data) (pcb/update-shapes [(:id shape)] repair-shape)))) +(defmethod repair-error :duplicate-slot + [_ {:keys [shape page-id] :as error} file-data _] + (let [page (ctpl/get-page file-data page-id) + childs (map #(get (:objects page) %) (:shapes shape)) + child-with-duplicate (let [result (reduce (fn [[seen duplicates] item] + (let [swap-slot (ctk/get-swap-slot item)] + (if (contains? seen swap-slot) + [seen (conj duplicates item)] + [(conj seen swap-slot) duplicates]))) + [#{} []] + childs)] + (second result)) + repair-shape + (fn [shape] + ;; Remove the swap slot + (log/debug :hint " -> remove swap-slot" :child-id (:id shape)) + (ctk/remove-swap-slot shape))] + + (log/dbg :hint "repairing shape :duplicated-slot" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes (map :id child-with-duplicate) repair-shape)))) + + + +(defmethod repair-error :component-duplicate-slot + [_ {:keys [shape] :as error} file-data _] + (let [main-shape (get-in shape [:objects (:main-instance-id shape)]) + childs (map #(get (:objects shape) %) (:shapes main-shape)) + childs-with-duplicate (let [result (reduce (fn [[seen duplicates] item] + (let [swap-slot (ctk/get-swap-slot item)] + (if (contains? seen swap-slot) + [seen (conj duplicates item)] + [(conj seen swap-slot) duplicates]))) + [#{} []] + childs)] + (second result)) + duplicated-ids (set (mapv :id childs-with-duplicate)) + repair-component + (fn [component] + (let [objects (reduce-kv (fn [acc k v] + (if (contains? duplicated-ids k) + (assoc acc k (ctk/remove-swap-slot v)) + (assoc acc k v))) + {} + (:objects component))] + (assoc component :objects objects)))] + + (log/dbg :hint "repairing component :component-duplicated-slot" :id (:id shape) :name (:name shape)) + (-> (pcb/empty-changes nil) + (pcb/with-library-data file-data) + (pcb/update-component (:id shape) repair-component)))) + (defmethod repair-error :missing-slot [_ {:keys [shape page-id args] :as error} file-data _] (let [repair-shape diff --git a/common/src/app/common/files/validate.cljc b/common/src/app/common/files/validate.cljc index 7caceed49..5eb708ab3 100644 --- a/common/src/app/common/files/validate.cljc +++ b/common/src/app/common/files/validate.cljc @@ -31,9 +31,11 @@ :child-not-found :frame-not-found :invalid-frame + :component-duplicate-slot :component-not-main :component-main-external :component-not-found + :duplicate-slot :invalid-main-instance-id :invalid-main-instance-page :invalid-main-instance @@ -64,7 +66,7 @@ [:shape {:optional true} :map] ; Cannot validate a shape because here it may be broken [:shape-id {:optional true} ::sm/uuid] [:file-id ::sm/uuid] - [:page-id ::sm/uuid]])) + [:page-id {:optional true} [:maybe ::sm/uuid]]])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ERROR HANDLING @@ -296,6 +298,22 @@ "This shape should not have swap slot" shape file page))) +(defn- has-duplicate-swap-slot? + [shape container] + (let [shapes (map #(get (:objects container) %) (:shapes shape)) + slots (->> (map #(ctk/get-swap-slot %) shapes) + (remove nil?)) + counts (frequencies slots)] + (some (fn [[_ count]] (> count 1)) counts))) + +(defn- check-duplicate-swap-slot + "Validate that the children of this shape does not have duplicated slots." + [shape file page] + (when (has-duplicate-swap-slot? shape page) + (report-error :duplicate-slot + "This shape has children with the same swap slot" + shape file page))) + (defn- check-shape-main-root-top "Root shape of a top main instance: @@ -308,6 +326,7 @@ (check-component-root shape file page) (check-component-not-ref shape file page) (check-empty-swap-slot shape file page) + (check-duplicate-swap-slot shape file page) (run! #(check-shape % file page libraries :context :main-top) (:shapes shape))) (defn- check-shape-main-root-nested @@ -335,6 +354,7 @@ (check-component-root shape file page) (check-component-ref shape file page libraries) (check-empty-swap-slot shape file page) + (check-duplicate-swap-slot shape file page) (run! #(check-shape % file page libraries :context :copy-top :library-exists library-exists) (:shapes shape)))) (defn- check-shape-copy-root-nested @@ -453,13 +473,24 @@ shape file page) (check-shape-not-component shape file page libraries)))))))) +(defn check-component-duplicate-swap-slot + [component file] + (let [shape (get-in component [:objects (:main-instance-id component)])] + (when (has-duplicate-swap-slot? shape component) + (report-error :component-duplicate-slot + "This deleted component has children with the same swap slot" + component file nil)))) + + (defn- check-component "Validate semantic coherence of a component. Report all errors found." [component file] (when (and (contains? component :objects) (nil? (:objects component))) (report-error :component-nil-objects-not-allowed "Objects list cannot be nil" - component file nil))) + component file nil)) + (when (:deleted component) + (check-component-duplicate-swap-slot component file))) (defn- get-orphan-shapes [{:keys [objects] :as page}] diff --git a/frontend/.gitignore b/frontend/.gitignore index d69ed5d6f..8d2f604e1 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -10,3 +10,5 @@ node_modules/ /playwright-report/ /blob-report/ /playwright/.cache/ +visual-dashboard.spec.js-snapshots + diff --git a/frontend/playwright/data/dashboard/create-access-token.json b/frontend/playwright/data/dashboard/create-access-token.json new file mode 100644 index 000000000..395e5a1a9 --- /dev/null +++ b/frontend/playwright/data/dashboard/create-access-token.json @@ -0,0 +1,8 @@ +{ + "~:id": "~u62edaeb8-e212-81ca-8004-80a6f8a42e8e", + "~:profile-id": "~uc7ce0794-0992-8105-8004-38e630f29a9b", + "~:created-at": "~m1718348381840", + "~:updated-at": "~m1718348381840", + "~:name": "new token", + "~:token": "eyJhbGciOiJBMjU2S1ciLCJlbmMiOiJBMjU2R0NNIn0.9aFN5YdOI-b-NQPos5uqF8J8b9iMyeri3yYhV5FlHuhNbRwk0YuftA.Dygx9O5-KsAHpuqD.ryTDCqelYOk1XYflTlDGFlzG8VLuElKHSGHdJyJvWqcCUANWzl8cVvezvU2GWg1Piin21KNrcV0TEcHPpDggySRbTn01MOIjw3vTVHdGrlHaVq5VpnWb5hCfs_P9kF7Y2IWOa4da4mM.IulvBQUllnay7clORd-NSg" +} diff --git a/frontend/playwright/data/dashboard/get-access-tokens-empty.json b/frontend/playwright/data/dashboard/get-access-tokens-empty.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/frontend/playwright/data/dashboard/get-access-tokens-empty.json @@ -0,0 +1 @@ +[] diff --git a/frontend/playwright/data/dashboard/get-access-tokens.json b/frontend/playwright/data/dashboard/get-access-tokens.json new file mode 100644 index 000000000..ec296ea8a --- /dev/null +++ b/frontend/playwright/data/dashboard/get-access-tokens.json @@ -0,0 +1,8 @@ +[ + { + "~:id": "~u62edaeb8-e212-81ca-8004-80a6f8a42e8e", + "~:name": "new token", + "~:created-at": "~m1718348381840", + "~:updated-at": "~m1718348381840" + } +] diff --git a/frontend/playwright/data/dashboard/get-font-variants.json b/frontend/playwright/data/dashboard/get-font-variants.json new file mode 100644 index 000000000..6a16ec574 --- /dev/null +++ b/frontend/playwright/data/dashboard/get-font-variants.json @@ -0,0 +1,15 @@ +[ + { + "~:font-style": "normal", + "~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d", + "~:font-id": "~u838cda51-c50f-8032-8004-6ac92ea6eaea", + "~:font-weight": 400, + "~:ttf-file-id": "~ue3710e43-7e40-405d-a4ea-8bb85443d44b", + "~:modified-at": "~m1716880956479", + "~:otf-file-id": "~u72bd3cda-478a-4e0e-a372-4a4f7cdc1371", + "~:id": "~u28f4b65f-3667-8087-8004-6ac93050433a", + "~:woff1-file-id": "~ua4c0a056-2eb6-47cc-bf80-3115d14e048d", + "~:created-at": "~m1716880956479", + "~:font-family": "Milligram Variable Trial" + } +] diff --git a/frontend/playwright/data/dashboard/get-projects-full.json b/frontend/playwright/data/dashboard/get-projects-full.json new file mode 100644 index 000000000..491351e86 --- /dev/null +++ b/frontend/playwright/data/dashboard/get-projects-full.json @@ -0,0 +1,19 @@ +[{ + "~:id": "~ue5a24d1b-ef1e-812f-8004-52bab84be6f7", + "~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d", + "~:created-at": "~m1715266551088", + "~:modified-at": "~m1715266551088", + "~:is-default": false, + "~:name": "New Project 1", + "~:is-pinned": false, + "~:count": 1 +}, +{ + "~:id": "~uc7ce0794-0992-8105-8004-38e630f7920b", + "~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d", + "~:created-at": "~m1713533116382", + "~:modified-at": "~m1713873823633", + "~:is-default": true, + "~:name": "Drafts", + "~:count": 1 +}] diff --git a/frontend/playwright/data/dashboard/get-shared-files-empty.json b/frontend/playwright/data/dashboard/get-shared-files-empty.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/frontend/playwright/data/dashboard/get-shared-files-empty.json @@ -0,0 +1 @@ +[] diff --git a/frontend/playwright/data/dashboard/get-shared-files.json b/frontend/playwright/data/dashboard/get-shared-files.json new file mode 100644 index 000000000..3fffa07f4 --- /dev/null +++ b/frontend/playwright/data/dashboard/get-shared-files.json @@ -0,0 +1,219 @@ +{ + "~#set": [ + { + "~:name": "New File 3", + "~:revn": 1, + "~:id": "~u28f4b65f-3667-8087-8004-69eca173cc07", + "~:is-shared": true, + "~:project-id": "~ue5a24d1b-ef1e-812f-8004-52bab84be6f7", + "~:created-at": "~m1713518796912", + "~:modified-at": "~m1713519762931", + "~:library-summary": { + "~:components": { + "~:count": 1, + "~:sample": [ + { + "~:id": "~ua30724ae-f8d8-8003-8004-69ecacfc8a4c", + "~:name": "Rectangle", + "~:path": "", + "~:modified-at": "~m1716823150739", + "~:main-instance-id": "~ua30724ae-f8d8-8003-8004-69ecacfa2045", + "~:main-instance-page": "~u28f4b65f-3667-8087-8004-69eca173cc08", + "~:objects": { + "~ua30724ae-f8d8-8003-8004-69ecacfa2045": { + "~#shape": { + "~:y": 168, + "~: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, + "~:hide-in-viewer": true, + "~:name": "Rectangle", + "~:width": 553, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 481, + "~:y": 168 + } + }, + { + "~#point": { + "~:x": 1034, + "~:y": 168 + } + }, + { + "~#point": { + "~:x": 1034, + "~:y": 550 + } + }, + { + "~#point": { + "~:x": 481, + "~:y": 550 + } + } + ], + "~:component-root": true, + "~:show-content": true, + "~: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": "~ua30724ae-f8d8-8003-8004-69ecacfa2045", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:component-id": "~ua30724ae-f8d8-8003-8004-69ecacfc8a4c", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 481, + "~:main-instance": true, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 481, + "~:y": 168, + "~:width": 553, + "~:height": 382, + "~:x1": 481, + "~:y1": 168, + "~:x2": 1034, + "~:y2": 550 + } + }, + "~:fills": [], + "~:flip-x": null, + "~:height": 382, + "~:component-file": "~u28f4b65f-3667-8087-8004-69eca173cc07", + "~:flip-y": null, + "~:shapes": [ + "~ua30724ae-f8d8-8003-8004-69eca9b27c8c" + ] + } + }, + "~ua30724ae-f8d8-8003-8004-69eca9b27c8c": { + "~#shape": { + "~:y": 168, + "~: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": 553, + "~:type": "~:rect", + "~:points": [ + { + "~#point": { + "~:x": 481, + "~:y": 168 + } + }, + { + "~#point": { + "~:x": 1034, + "~:y": 168 + } + }, + { + "~#point": { + "~:x": 1034, + "~:y": 550 + } + }, + { + "~#point": { + "~:x": 481, + "~:y": 550 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:constraints-v": "~:scale", + "~:constraints-h": "~:scale", + "~:id": "~ua30724ae-f8d8-8003-8004-69eca9b27c8c", + "~:parent-id": "~ua30724ae-f8d8-8003-8004-69ecacfa2045", + "~:frame-id": "~ua30724ae-f8d8-8003-8004-69ecacfa2045", + "~:strokes": [], + "~:x": 481, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 481, + "~:y": 168, + "~:width": 553, + "~:height": 382, + "~:x1": 481, + "~:y1": 168, + "~:x2": 1034, + "~:y2": 550 + } + }, + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:ry": 0, + "~:height": 382, + "~:flip-y": null + } + } + } + } + ] + }, + "~:media": { + "~:count": 0, + "~:sample": [] + }, + "~:colors": { + "~:count": 0, + "~:sample": [] + }, + "~:typographies": { + "~:count": 0, + "~:sample": [] + } + } + } + ] +} diff --git a/frontend/playwright/data/dashboard/get-team-invitations-empty.json b/frontend/playwright/data/dashboard/get-team-invitations-empty.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/frontend/playwright/data/dashboard/get-team-invitations-empty.json @@ -0,0 +1 @@ +[] diff --git a/frontend/playwright/data/dashboard/get-team-invitations.json b/frontend/playwright/data/dashboard/get-team-invitations.json new file mode 100644 index 000000000..f7ac77543 --- /dev/null +++ b/frontend/playwright/data/dashboard/get-team-invitations.json @@ -0,0 +1,6 @@ +[ + { "~:email": "test1@mail.com", "~:role": "~:editor", "~:expired": true }, + { "~:email": "test2@mail.com", "~:role": "~:editor", "~:expired": false }, + { "~:email": "test3@mail.com", "~:role": "~:admin", "~:expired": true }, + { "~:email": "test4@mail.com", "~:role": "~:admin", "~:expired": false } +] diff --git a/frontend/playwright/data/dashboard/get-team-members.json b/frontend/playwright/data/dashboard/get-team-members.json new file mode 100644 index 000000000..a869d5e34 --- /dev/null +++ b/frontend/playwright/data/dashboard/get-team-members.json @@ -0,0 +1,16 @@ +[ + { + "~:is-admin": true, + "~:email": "foo@example.com", + "~:team-id": "~udd33ff88-f4e5-8033-8003-8096cc07bdf3", + "~:name": "Princesa Leia", + "~:fullname": "Princesa Leia", + "~:is-owner": false, + "~:modified-at": "~m1713533116365", + "~:can-edit": true, + "~:is-active": true, + "~:id": "~uc7ce0794-0992-8105-8004-38e630f29a9b", + "~:profile-id": "~uf56647eb-19a7-8115-8003-b6bc939ecd1b", + "~:created-at": "~m1713533116365" + } +] diff --git a/frontend/playwright/data/dashboard/get-team-recent-files.json b/frontend/playwright/data/dashboard/get-team-recent-files.json new file mode 100644 index 000000000..920bb2df0 --- /dev/null +++ b/frontend/playwright/data/dashboard/get-team-recent-files.json @@ -0,0 +1,29 @@ +[ + { + "~:id": "~u8b479b80-e02d-8074-8004-4088dc6bfd11", + "~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b", + "~:created-at": "~m1714045521389", + "~:modified-at": "~m1714045654874", + "~:name": "New File 2", + "~:revn": 1, + "~:is-shared": false + }, + { + "~:id": "~u95d6fdd8-48d8-8148-8004-38af910d2dbe", + "~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b", + "~:created-at": "~m1713518796912", + "~:modified-at": "~m1713519762931", + "~:name": "New File 1", + "~:revn": 1, + "~:is-shared": false + }, + { + "~:id": "~u28f4b65f-3667-8087-8004-69eca173cc07", + "~:project-id": "~ue5a24d1b-ef1e-812f-8004-52bab84be6f7", + "~:created-at": "~m1713518796912", + "~:modified-at": "~m1713519762931", + "~:name": "New File 3", + "~:revn": 1, + "~:is-shared": true + } +] diff --git a/frontend/playwright/data/dashboard/get-team-stats.json b/frontend/playwright/data/dashboard/get-team-stats.json new file mode 100644 index 000000000..c984f1021 --- /dev/null +++ b/frontend/playwright/data/dashboard/get-team-stats.json @@ -0,0 +1 @@ +{"~:projects":1,"~:files":3} diff --git a/frontend/playwright/data/dashboard/get-webhooks-empty.json b/frontend/playwright/data/dashboard/get-webhooks-empty.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/frontend/playwright/data/dashboard/get-webhooks-empty.json @@ -0,0 +1 @@ +[] diff --git a/frontend/playwright/data/dashboard/get-webhooks.json b/frontend/playwright/data/dashboard/get-webhooks.json new file mode 100644 index 000000000..3849e3608 --- /dev/null +++ b/frontend/playwright/data/dashboard/get-webhooks.json @@ -0,0 +1,20 @@ +[ + { + "~:id": "~u29ce7ec9-e75d-81b4-8004-08100373558a", + "~:uri": { + "~#uri": "https://www.abc.es" + }, + "~:mtype": "application/json", + "~:is-active": false, + "~:error-count": 0 + }, + { + "~:id": "~u43d6b3b1-40f7-807b-8003-f9846292b4c7", + "~:uri": { + "~#uri": "https://www.google.com" + }, + "~:mtype": "application/json", + "~:is-active": true, + "~:error-count": 0 + } +] diff --git a/frontend/playwright/data/dashboard/search-files-empty.json b/frontend/playwright/data/dashboard/search-files-empty.json new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/playwright/data/dashboard/search-files.json b/frontend/playwright/data/dashboard/search-files.json new file mode 100644 index 000000000..920bb2df0 --- /dev/null +++ b/frontend/playwright/data/dashboard/search-files.json @@ -0,0 +1,29 @@ +[ + { + "~:id": "~u8b479b80-e02d-8074-8004-4088dc6bfd11", + "~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b", + "~:created-at": "~m1714045521389", + "~:modified-at": "~m1714045654874", + "~:name": "New File 2", + "~:revn": 1, + "~:is-shared": false + }, + { + "~:id": "~u95d6fdd8-48d8-8148-8004-38af910d2dbe", + "~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b", + "~:created-at": "~m1713518796912", + "~:modified-at": "~m1713519762931", + "~:name": "New File 1", + "~:revn": 1, + "~:is-shared": false + }, + { + "~:id": "~u28f4b65f-3667-8087-8004-69eca173cc07", + "~:project-id": "~ue5a24d1b-ef1e-812f-8004-52bab84be6f7", + "~:created-at": "~m1713518796912", + "~:modified-at": "~m1713519762931", + "~:name": "New File 3", + "~:revn": 1, + "~:is-shared": true + } +] diff --git a/frontend/playwright/data/logged-in-user/get-teams-complete.json b/frontend/playwright/data/logged-in-user/get-teams-complete.json new file mode 100644 index 000000000..910e1543f --- /dev/null +++ b/frontend/playwright/data/logged-in-user/get-teams-complete.json @@ -0,0 +1,48 @@ +[ + { + "~:features": { + "~#set": [ + "layout/grid", + "styles/v2", + "fdata/pointer-map", + "fdata/objects-map", + "components/v2", + "fdata/shape-data-type" + ] + }, + "~:permissions": { + "~:type": "~:membership", + "~:is-owner": true, + "~:is-admin": true, + "~:can-edit": true + }, + "~:name": "Default", + "~:modified-at": "~m1713533116375", + "~:id": "~uc7ce0794-0992-8105-8004-38e630f40f6d", + "~:created-at": "~m1713533116375", + "~:is-default": true +}, + { + "~:features": { + "~#set": [ + "layout/grid", + "styles/v2", + "fdata/pointer-map", + "fdata/objects-map", + "components/v2", + "fdata/shape-data-type" + ] + }, + "~:permissions": { + "~:type": "~:membership", + "~:is-owner": true, + "~:is-admin": true, + "~:can-edit": true + }, + "~:name": "Second team", + "~:modified-at": "~m1701164272671", + "~:id": "~udd33ff88-f4e5-8033-8003-8096cc07bdf3", + "~:created-at": "~m1701164272671", + "~:is-default": false + } +] diff --git a/frontend/playwright/ui/pages/DashboardPage.js b/frontend/playwright/ui/pages/DashboardPage.js index e83c62dd9..fbc373d3f 100644 --- a/frontend/playwright/ui/pages/DashboardPage.js +++ b/frontend/playwright/ui/pages/DashboardPage.js @@ -50,6 +50,8 @@ export class DashboardPage extends BaseWebSocketPage { static anyTeamId = "c7ce0794-0992-8105-8004-38e630f40f6d"; + static secondTeamId = "dd33ff88-f4e5-8033-8003-8096cc07bdf3"; + static draftProjectId = "c7ce0794-0992-8105-8004-38e630f7920b"; constructor(page) { @@ -60,12 +62,34 @@ export class DashboardPage extends BaseWebSocketPage { this.draftTitle = page.getByRole("heading", { name: "Drafts" }); this.draftLink = page.getByTestId("drafts-link-sidebar"); this.draftsFile = page.getByText(/New File 1/); + this.fontsLink = page.getByTestId("fonts-link-sidebar"); + this.fontsTitle = page.getByRole("heading", { name: "Fonts", level: 1 }); + this.libsLink = page.getByTestId("libs-link-sidebar"); + this.libsTitle = page.getByRole("heading", { name: "Libraries", level: 1 }); + this.searchButton = page.getByRole("button", { name: "dashboard-search" }); + this.searchTitle = page.getByRole("heading", { name: "Search results" }); + this.searchInput = page.getByPlaceholder('Search…'); + this.newFileName = page.getByText("New File 3"); + this.teamDropdown = page.getByRole('button', { name: 'Your Penpot' }); + this.userAccount = page.getByRole('button', { name: "Princesa Leia Princesa Leia" }); + this.userProfileOption = page.getByText("Your account"); + this.userAccountTitle = page.getByRole("heading", {name: "Your account"}); } async setupDraftsEmpty() { await this.mockRPC("get-project-files?project-id=*", "dashboard/get-project-files-empty.json"); } + async setupSearchEmpty() { + await this.mockRPC("search-files", "dashboard/search-files-empty.json", { + method: "POST", + }); + } + + async setupLibrariesEmpty() { + await this.mockRPC("get-team-shared-files?team-id=*", "dashboard/get-shared-files-empty.json"); + } + async setupDrafts() { await this.mockRPC("get-project-files?project-id=*", "dashboard/get-project-files.json"); } @@ -74,15 +98,95 @@ export class DashboardPage extends BaseWebSocketPage { await this.mockRPC("create-project", "dashboard/create-project.json", { method: "POST" }); await this.mockRPC("get-projects?team-id=*", "dashboard/get-projects-new.json"); } - async goToWorkspace() { + + async setupDashboardFull() { + await this.mockRPC("get-projects?team-id=*", "dashboard/get-projects-full.json"); + await this.mockRPC("get-project-files?project-id=*", "dashboard/get-project-files.json"); + await this.mockRPC("get-team-shared-files?team-id=*", "dashboard/get-shared-files.json"); + await this.mockRPC("get-team-shared-files?project-id=*", "dashboard/get-shared-files.json"); + await this.mockRPC("get-team-recent-files?team-id=*", "dashboard/get-team-recent-files.json"); + await this.mockRPC("get-font-variants?team-id=*", "dashboard/get-font-variants.json"); + await this.mockRPC("search-files", "dashboard/search-files.json", { method: "POST" }); + await this.mockRPC("search-files", "dashboard/search-files.json" ); + await this.mockRPC("get-teams", "logged-in-user/get-teams-complete.json"); + } + + async setupAccessTokensEmpty() { + await this.mockRPC("get-access-tokens", "dashboard/get-access-tokens-empty.json"); + } + + async createAccessToken() { + await this.mockRPC("create-access-token", "dashboard/create-access-token.json", { method: "POST" }); + } + + async setupAccessTokens() { + await this.mockRPC("get-access-tokens", "dashboard/get-access-tokens.json"); + } + + async setupTeamInvitationsEmpty() { + await this.mockRPC("get-team-invitations?team-id=*", "dashboard/get-team-invitations-empty.json"); + } + + async setupTeamInvitations() { + await this.mockRPC("get-team-invitations?team-id=*", "dashboard/get-team-invitations.json"); + } + + async setupTeamWebhooksEmpty() { + await this.mockRPC("get-webhooks?team-id=*", "dashboard/get-webhooks-empty.json"); + } + + async setupTeamWebhooks() { + await this.mockRPC("get-webhooks?team-id=*", "dashboard/get-webhooks.json"); + } + + async setupTeamSettings() { + await this.mockRPC("get-team-stats?team-id=*", "dashboard/get-team-stats.json"); + } + + async goToDashboard() { await this.page.goto(`#/dashboard/team/${DashboardPage.anyTeamId}/projects`); } + async goToSecondTeamDashboard() { + await this.page.goto(`#/dashboard/team/${DashboardPage.secondTeamId}/projects`); + } + + async goToSecondTeamMembersSection() { + await this.page.goto(`#/dashboard/team/${DashboardPage.secondTeamId}/members`); + } + + async goToSecondTeamInvitationsSection() { + await this.page.goto(`#/dashboard/team/${DashboardPage.secondTeamId}/invitations`); + } + + async goToSecondTeamWebhooksSection() { + await this.page.goto(`#/dashboard/team/${DashboardPage.secondTeamId}/webhooks`); + } + + async goToSecondTeamWebhooksSection() { + await this.page.goto(`#/dashboard/team/${DashboardPage.secondTeamId}/webhooks`); + } + + async goToSecondTeamSettingsSection() { + await this.page.goto(`#/dashboard/team/${DashboardPage.secondTeamId}/settings`); + } + + async goToSearch() { + await this.page.goto(`#/dashboard/team/${DashboardPage.anyTeamId}/search`); + } + async goToDrafts() { await this.page.goto( `#/dashboard/team/${DashboardPage.anyTeamId}/projects/${DashboardPage.draftProjectId}`, ); } + + async goToAccount() { + + await this.userAccount.click(); + + await this.userProfileOption.click(); + } } export default DashboardPage; diff --git a/frontend/playwright/ui/specs/dashboard.spec.js b/frontend/playwright/ui/specs/dashboard.spec.js index 23e71efad..4cb381b4a 100644 --- a/frontend/playwright/ui/specs/dashboard.spec.js +++ b/frontend/playwright/ui/specs/dashboard.spec.js @@ -13,7 +13,7 @@ test.beforeEach(async ({ page }) => { test("Dashboad page has title ", async ({ page }) => { const dashboardPage = new DashboardPage(page); - await dashboardPage.goToWorkspace(); + await dashboardPage.goToDashboard(); await expect(dashboardPage.page).toHaveURL(/dashboard/); await expect(dashboardPage.titleLabel).toBeVisible(); @@ -23,7 +23,7 @@ test("User can create a new project", async ({ page }) => { const dashboardPage = new DashboardPage(page); await dashboardPage.setupNewProject(); - await dashboardPage.goToWorkspace(); + await dashboardPage.goToDashboard(); await dashboardPage.addProjectBtn.click(); await expect(dashboardPage.projectName).toBeVisible(); @@ -33,7 +33,7 @@ test("User goes to draft page", async ({ page }) => { const dashboardPage = new DashboardPage(page); await dashboardPage.setupDraftsEmpty(); - await dashboardPage.goToWorkspace(); + await dashboardPage.goToDashboard(); await dashboardPage.draftLink.click(); await expect(dashboardPage.draftTitle).toBeVisible(); diff --git a/frontend/playwright/ui/specs/onboarding.spec.js b/frontend/playwright/ui/specs/onboarding.spec.js index 39efa967c..68e00fc50 100644 --- a/frontend/playwright/ui/specs/onboarding.spec.js +++ b/frontend/playwright/ui/specs/onboarding.spec.js @@ -12,7 +12,7 @@ test("User can complete the onboarding", async ({ page }) => { const dashboardPage = new DashboardPage(page); const onboardingPage = new OnboardingPage(page); - await dashboardPage.goToWorkspace(); + await dashboardPage.goToDashboard(); await expect(page.getByRole("heading", { name: "Help us get to know you" })).toBeVisible(); await onboardingPage.fillOnboardingInputsStep1(); diff --git a/frontend/playwright/ui/visual-specs/visual-dashboard.spec.js b/frontend/playwright/ui/visual-specs/visual-dashboard.spec.js new file mode 100644 index 000000000..faefbeb1b --- /dev/null +++ b/frontend/playwright/ui/visual-specs/visual-dashboard.spec.js @@ -0,0 +1,423 @@ +import { test, expect } from "@playwright/test"; +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("User goes to an empty dashboard", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + + await dashboardPage.goToDashboard(); + + await expect(dashboardPage.titleLabel).toBeVisible(); + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +// Empty dashboard pages + +test("User goes to an empty draft page", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDraftsEmpty(); + + await dashboardPage.goToDashboard(); + await dashboardPage.draftLink.click(); + + await expect(dashboardPage.draftTitle).toBeVisible(); + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User goes to an empty fonts page", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + + await dashboardPage.goToDashboard(); + await dashboardPage.fontsLink.click(); + + await expect(dashboardPage.fontsTitle).toBeVisible(); + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User goes to an empty libraries page", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupLibrariesEmpty(); + + await dashboardPage.goToDashboard(); + await dashboardPage.libsLink.click(); + + await expect(dashboardPage.libsTitle).toBeVisible(); + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User goes to an empty search page", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupSearchEmpty(); + + await dashboardPage.goToSearch(); + + await expect(dashboardPage.searchTitle).toBeVisible(); + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User goes to the dashboard with a new project", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupNewProject(); + + await dashboardPage.goToDashboard(); + + await expect(dashboardPage.projectName).toBeVisible(); + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +// Dashboard pages with content + +test("User goes to a full dashboard", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDashboardFull(); + + await dashboardPage.goToDashboard(); + + await expect(dashboardPage.draftsFile).toBeVisible(); + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User goes to an full draft page", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDashboardFull(); + + await dashboardPage.goToDashboard(); + await dashboardPage.draftLink.click(); + + await expect(dashboardPage.draftTitle).toBeVisible(); + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User goes to an full library page", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDashboardFull(); + + await dashboardPage.goToDashboard(); + await dashboardPage.libsLink.click(); + + await expect(dashboardPage.libsTitle).toBeVisible(); + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User goes to an full fonts page", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDashboardFull(); + + await dashboardPage.goToDashboard(); + await dashboardPage.fontsLink.click(); + + await expect(dashboardPage.fontsTitle).toBeVisible(); + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User goes to an full search page", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDashboardFull(); + + await dashboardPage.goToSearch(); + + await expect(dashboardPage.searchInput).toBeVisible(); + + await dashboardPage.searchInput.fill("New"); + + await expect(dashboardPage.searchTitle).toBeVisible(); + + await expect(dashboardPage.newFileName).toBeVisible(); + + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +// Account management + +test("User opens user account", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + + await dashboardPage.goToDashboard(); + + await expect(dashboardPage.userAccount).toBeVisible(); + + await dashboardPage.goToAccount(); + + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User goes to user profile", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + + await dashboardPage.goToDashboard(); + + await dashboardPage.goToAccount(); + + await expect(dashboardPage.userAccountTitle).toBeVisible(); + + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User goes to password management section", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + + await dashboardPage.goToDashboard(); + + await dashboardPage.goToAccount(); + + await page.getByText("Password").click(); + + await expect(page.getByRole("heading", { name: "Change Password" })).toBeVisible(); + + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User goes to settings section", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + + await dashboardPage.goToDashboard(); + + await dashboardPage.goToAccount(); + + await page.getByTestId("settings-profile").click(); + + await expect(page.getByRole("heading", { name: "Settings" })).toBeVisible(); + + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User goes to an empty access tokens secction", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + + await dashboardPage.goToDashboard(); + + await dashboardPage.setupAccessTokensEmpty(); + + await dashboardPage.goToAccount(); + + await page.getByText("Access tokens").click(); + + await expect(page.getByRole("heading", { name: "Personal access tokens" })).toBeVisible(); + + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User can create an access token", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + + await dashboardPage.goToDashboard(); + + await dashboardPage.setupAccessTokensEmpty(); + + await dashboardPage.goToAccount(); + + await page.getByText("Access tokens").click(); + + await expect(page.getByRole("heading", { name: "Personal access tokens" })).toBeVisible(); + + await page.getByRole("button", { name: "Generate New Token" }).click(); + + await dashboardPage.createAccessToken(); + + await expect(page.getByPlaceholder("The name can help to know")).toBeVisible(); + + await page.getByPlaceholder("The name can help to know").fill("New token"); + + await expect(page.getByRole("button", { name: "Create token" })).not.toBeDisabled(); + + await page.getByRole("button", { name: "Create token" }).click(); + + await expect(page.getByRole("button", { name: "Create token" })).not.toBeVisible(); + + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User goes to a full access tokens secction", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + + await dashboardPage.goToDashboard(); + + await dashboardPage.setupAccessTokens(); + + await dashboardPage.goToAccount(); + + await page.getByText("Access tokens").click(); + + await expect(page.getByRole("heading", { name: "Personal access tokens" })).toBeVisible(); + + await expect(page.getByText("new token", { exact: true })).toBeVisible(); + + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User goes to the feedback secction", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + + await dashboardPage.goToDashboard(); + + await dashboardPage.goToAccount(); + + await page.getByText("Give feedback").click(); + + await expect(page.getByRole("heading", { name: "Email" })).toBeVisible(); + + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +// Teams management + +test("User opens teams selector with only one team", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + + await dashboardPage.goToDashboard(); + + await expect(dashboardPage.titleLabel).toBeVisible(); + + await dashboardPage.teamDropdown.click(); + + await expect(page.getByText("Create new team")).toBeVisible(); + + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User opens teams selector with more than one team", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDashboardFull(); + + await dashboardPage.goToDashboard(); + + await expect(dashboardPage.titleLabel).toBeVisible(); + + await dashboardPage.teamDropdown.click(); + + await expect(page.getByText("Second Team")).toBeVisible(); + + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User goes to second team", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDashboardFull(); + + await dashboardPage.goToDashboard(); + + await dashboardPage.teamDropdown.click(); + + await expect(page.getByText("Second Team")).toBeVisible(); + + await page.getByText("Second Team").click(); + + await expect(page.getByText("Team Up")).toBeVisible(); + + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User opens team management dropdown", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDashboardFull(); + + await dashboardPage.goToSecondTeamDashboard(); + + await expect(page.getByText("Team Up")).toBeVisible(); + + await page.getByRole("button", { name: "team-management" }).click(); + + await expect(page.getByTestId("team-members")).toBeVisible(); + + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User goes to team management section", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDashboardFull(); + + await dashboardPage.goToSecondTeamMembersSection(); + + await expect(page.getByText("role")).toBeVisible(); + + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User goes to an empty invitations section", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDashboardFull(); + await dashboardPage.setupTeamInvitationsEmpty(); + + await dashboardPage.goToSecondTeamInvitationsSection(); + + await expect(page.getByText("No pending invitations")).toBeVisible(); + + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User goes to a complete invitations section", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDashboardFull(); + await dashboardPage.setupTeamInvitations(); + + await dashboardPage.goToSecondTeamInvitationsSection(); + + await expect(page.getByText("test1@mail.com")).toBeVisible(); + + await expect(dashboardPage.page).toHaveScreenshot(); +}); + + +test("User invite people to the team", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDashboardFull(); + await dashboardPage.setupTeamInvitationsEmpty(); + + await dashboardPage.goToSecondTeamInvitationsSection(); + + await expect(page.getByTestId("invite-member")).toBeVisible(); + + await page.getByTestId("invite-member").click(); + + await expect(page.getByText("Invite with the role")).toBeVisible(); + + await page.getByPlaceholder('Emails, comma separated').fill("test5@mail.com"); + + await expect(page.getByText("Send invitation")).not.toBeDisabled(); + + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User goes to an empty webhook section", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDashboardFull(); + await dashboardPage.setupTeamWebhooksEmpty(); + + await dashboardPage.goToSecondTeamWebhooksSection(); + + await expect(page.getByText("No webhooks created so far.")).toBeVisible(); + + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User goes to a complete webhook section", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDashboardFull(); + await dashboardPage.setupTeamWebhooks(); + + await dashboardPage.goToSecondTeamWebhooksSection(); + + await expect(page.getByText("https://www.google.com")).toBeVisible(); + + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User goes to the team settings section", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDashboardFull(); + await dashboardPage.setupTeamSettings(); + + await dashboardPage.goToSecondTeamSettingsSection(); + + await expect(page.getByText("TEAM INFO")).toBeVisible(); + + await expect(dashboardPage.page).toHaveScreenshot(); +}); diff --git a/frontend/resources/styles/common/refactor/basic-rules.scss b/frontend/resources/styles/common/refactor/basic-rules.scss index 61555e114..6f28edf69 100644 --- a/frontend/resources/styles/common/refactor/basic-rules.scss +++ b/frontend/resources/styles/common/refactor/basic-rules.scss @@ -590,9 +590,6 @@ width: 100%; z-index: $z-index-modal; background-color: var(--overlay-color); - &.onboarding-a-b-test { - background-color: var(--overlay-color-onboarding-a-b-test); - } } .modal-container-base { diff --git a/frontend/resources/styles/common/refactor/color-defs.scss b/frontend/resources/styles/common/refactor/color-defs.scss index c7048003d..da1f0d784 100644 --- a/frontend/resources/styles/common/refactor/color-defs.scss +++ b/frontend/resources/styles/common/refactor/color-defs.scss @@ -11,7 +11,6 @@ // Dark background --db-primary: #18181a; --db-primary-60: #{color.change(#18181a, $alpha: 0.6)}; - --db-primary-90: #{color.change(#18181a, $alpha: 0.9)}; --db-secondary: #000000; --db-secondary-30: #{color.change(#000000, $alpha: 0.3)}; --db-secondary-80: #{color.change(#000000, $alpha: 0.8)}; @@ -36,7 +35,6 @@ // Light background --lb-primary: #ffffff; --lb-primary-60: #{color.change(#ffffff, $alpha: 0.6)}; - --lb-primary-90: #{color.change(#ffffff, $alpha: 0.9)}; --lb-secondary: #e8eaee; --lb-secondary-30: #{color.change(#e8eaee, $alpha: 0.3)}; --lb-secondary-80: #{color.change(#e8eaee, $alpha: 0.8)}; diff --git a/frontend/resources/styles/common/refactor/themes/default-theme.scss b/frontend/resources/styles/common/refactor/themes/default-theme.scss index 2b5feb06a..432906c9e 100644 --- a/frontend/resources/styles/common/refactor/themes/default-theme.scss +++ b/frontend/resources/styles/common/refactor/themes/default-theme.scss @@ -37,7 +37,6 @@ --color-info-foreground: var(--status-color-info-500); --overlay-color: var(--db-primary-60); - --overlay-color-onboarding-a-b-test: var(--db-primary-90); --shadow-color: var(--db-secondary-30); --radio-button-box-shadow: 0 0 0 1px var(--db-secondary-30) inset; diff --git a/frontend/resources/styles/common/refactor/themes/light-theme.scss b/frontend/resources/styles/common/refactor/themes/light-theme.scss index 01e98c6cb..8f133cc72 100644 --- a/frontend/resources/styles/common/refactor/themes/light-theme.scss +++ b/frontend/resources/styles/common/refactor/themes/light-theme.scss @@ -37,7 +37,6 @@ --color-info-foreground: var(--status-color-info-500); --overlay-color: var(--lb-primary-60); - --overlay-color-onboarding-a-b-test: var(--lb-primary-90); --shadow-color: var(--lf-secondary-40); --radio-button-box-shadow: 0 0 0 1px var(--lb-secondary) inset; diff --git a/frontend/scripts/watch.js b/frontend/scripts/watch.js index bdbe53720..f2c6b376c 100644 --- a/frontend/scripts/watch.js +++ b/frontend/scripts/watch.js @@ -35,6 +35,7 @@ async function compileSass(path) { log.info("done:", `(${ppt(end)})`); } +await fs.mkdir("./resources/public/css/", { recursive: true }); await compileSassAll(); await h.copyAssets() await h.compileSvgSprites() diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index 10cfd2f5c..0862eb17e 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -256,11 +256,13 @@ (if (or @focused? (seq search-term)) [:button {:class (stl/css :search-btn :clear-search-btn) :tab-index "0" + :aria-label "dashboard-clear-search" :on-click on-clear-click :on-key-down handle-clear-search} clear-search-icon] [:button {:class (stl/css :search-btn) + :aria-label "dashboard-search" :on-click on-clear-click} search-icon])])) @@ -504,11 +506,13 @@ :on-key-down handle-members :className (stl/css :team-options-item) :id "teams-options-members" + :data-testid "team-members" :data-test "team-members"} (tr "labels.members")] [:> dropdown-menu-item* {:on-click go-invitations :on-key-down handle-invitations :className (stl/css :team-options-item) + :data-testid "team-invitations" :id "teams-options-invitations" :data-test "team-invitations"} (tr "labels.invitations")] @@ -524,6 +528,7 @@ :on-key-down handle-settings :className (stl/css :team-options-item) :id "teams-options-settings" + :data-testid "team-settings" :data-test "team-settings"} (tr "labels.settings")] @@ -533,6 +538,7 @@ :on-key-down handle-rename :id "teams-options-rename" :className (stl/css :team-options-item) + :data-testid "rename-team" :data-test "rename-team"} (tr "labels.rename")]) @@ -550,6 +556,7 @@ :on-key-down handle-leave-as-owner-clicked :id "teams-options-leave-team" :className (stl/css :team-options-item) + :data-testid "leave-team" :data-test "leave-team"} (tr "dashboard.leave-team")] @@ -654,6 +661,7 @@ (when-not (:is-default team) [:button {:class (stl/css :switch-options) :on-click handle-show-opts-click + :aria-label "team-management" :tab-index "0" :on-key-down handle-show-opts-keydown} menu-icon])] @@ -792,6 +800,7 @@ [:li {:class (stl/css-case :current libs? :sidebar-nav-item true)} [:& link {:action go-libs + :data-testid "libs-link-sidebar" :class (stl/css :sidebar-link) :keyboard-action go-libs-with-key} [:span {:class (stl/css :element-title)} (tr "labels.shared-libraries")]]]]] @@ -803,6 +812,7 @@ :current fonts?)} [:& link {:action go-fonts :class (stl/css :sidebar-link) + :data-testid "fonts-link-sidebar" :keyboard-action go-fonts-with-key :data-test "fonts"} [:span {:class (stl/css :element-title)} (tr "labels.fonts")]]]]] @@ -946,11 +956,11 @@ :on-hide-comments handle-hide-comments}]) [:div {:class (stl/css :profile-section)} - [:div {:class (stl/css :profile) - :tab-index "0" - :on-click handle-click - :on-key-down handle-key-down - :data-test "profile-btn"} + [:button {:class (stl/css :profile) + :tab-index "0" + :on-click handle-click + :on-key-down handle-key-down + :data-test "profile-btn"} [:img {:src photo :class (stl/css :profile-img) :alt (:fullname profile)}] diff --git a/frontend/src/app/main/ui/dashboard/sidebar.scss b/frontend/src/app/main/ui/dashboard/sidebar.scss index f939c8d44..22a0a3c9c 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.scss +++ b/frontend/src/app/main/ui/dashboard/sidebar.scss @@ -331,10 +331,12 @@ } .profile { + @include buttonStyle; display: grid; grid-template-columns: auto 1fr; gap: $s-8; cursor: pointer; + text-align: left; } .profile-fullname { diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index 76652e98a..380a07da2 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -105,7 +105,8 @@ [:a {:class (stl/css :btn-secondary :btn-small) :on-click on-invite-member - :data-test "invite-member"} + :data-test "invite-member" + :data-testid "invite-member"} (tr "dashboard.invite-profile")] [:div {:class (stl/css :blank-space)}])]])) diff --git a/frontend/src/app/main/ui/onboarding/newsletter.cljs b/frontend/src/app/main/ui/onboarding/newsletter.cljs index 0f2bfd2ce..e76cd0679 100644 --- a/frontend/src/app/main/ui/onboarding/newsletter.cljs +++ b/frontend/src/app/main/ui/onboarding/newsletter.cljs @@ -7,7 +7,6 @@ (ns app.main.ui.onboarding.newsletter (:require-macros [app.main.style :as stl]) (:require - [app.config :as cf] [app.main.data.events :as-alias ev] [app.main.data.messages :as msg] [app.main.data.users :as du] @@ -45,14 +44,10 @@ (assoc :label "newsletter:subscriptions") (assoc :step 6))] (st/emit! (ptk/data-event ::ev/event params) - (du/update-profile-props state))))) - - onboarding-a-b-test? - (cf/external-feature-flag "signup-background" "test")] + (du/update-profile-props state)))))] [:div {:class (stl/css-case - :modal-overlay true - :onboarding-a-b-test onboarding-a-b-test?)} + :modal-overlay true)} [:div.animated.fadeInDown {:class (stl/css :modal-container)} [:div {:class (stl/css :modal-left)} diff --git a/frontend/src/app/main/ui/onboarding/questions.cljs b/frontend/src/app/main/ui/onboarding/questions.cljs index 9904790db..3d715a185 100644 --- a/frontend/src/app/main/ui/onboarding/questions.cljs +++ b/frontend/src/app/main/ui/onboarding/questions.cljs @@ -10,7 +10,6 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] - [app.config :as cf] [app.main.data.events :as-alias ev] [app.main.data.users :as du] [app.main.store :as st] @@ -497,14 +496,10 @@ (fn [form] (let [data (merge @clean-data (:clean-data @form))] (reset! clean-data data) - (st/emit! (du/mark-questions-as-answered data))))) - - onboarding-a-b-test? - (cf/external-feature-flag "signup-background" "test")] + (st/emit! (du/mark-questions-as-answered data)))))] [:div {:class (stl/css-case - :modal-overlay true - :onboarding-a-b-test onboarding-a-b-test?)} + :modal-overlay true)} [:div {:class (stl/css :modal-container) :ref container} diff --git a/frontend/src/app/main/ui/onboarding/team_choice.cljs b/frontend/src/app/main/ui/onboarding/team_choice.cljs index 743c05184..c1b939e4f 100644 --- a/frontend/src/app/main/ui/onboarding/team_choice.cljs +++ b/frontend/src/app/main/ui/onboarding/team_choice.cljs @@ -9,7 +9,6 @@ (:require [app.common.data.macros :as dm] [app.common.spec :as us] - [app.config :as cf] [app.main.data.dashboard :as dd] [app.main.data.events :as ev] [app.main.data.messages :as msg] @@ -260,14 +259,10 @@ on-back (mf/use-fn (fn [] - (swap! name* (constantly nil)))) - - onboarding-a-b-test? - (cf/external-feature-flag "signup-background" "test")] + (swap! name* (constantly nil))))] [:div {:class (stl/css-case - :modal-overlay true - :onboarding-a-b-test onboarding-a-b-test?)} + :modal-overlay true)} [:div.animated.fadeIn {:class (stl/css :modal-container)} [:& left-sidebar] diff --git a/frontend/src/app/main/ui/releases/v2_0.cljs b/frontend/src/app/main/ui/releases/v2_0.cljs index 511307544..57f2b0847 100644 --- a/frontend/src/app/main/ui/releases/v2_0.cljs +++ b/frontend/src/app/main/ui/releases/v2_0.cljs @@ -8,203 +8,196 @@ (:require-macros [app.main.style :as stl]) (:require [app.common.data.macros :as dm] - [app.config :as cf] [app.main.ui.releases.common :as c] [rumext.v2 :as mf])) ;; TODO: Review all copies and alt text (defmethod c/render-release-notes "2.0" [{:keys [slide klass next finish navigate version]}] - (let [onboarding-a-b-test? (cf/external-feature-flag "signup-background" "test")] - (mf/html - (case slide - :start - [:div {:class (stl/css-case :modal-overlay true - :onboarding-a-b-test onboarding-a-b-test?)} - [:div.animated {:class klass} - [:div {:class (stl/css :modal-container)} - [:img {:src "images/features/2.0-intro-image.png" - :class (stl/css :start-image) - :border "0" - :alt "A graphic illustration with Penpot style"}] + (mf/html + (case slide + :start + [:div {:class (stl/css-case :modal-overlay true)} + [:div.animated {:class klass} + [:div {:class (stl/css :modal-container)} + [:img {:src "images/features/2.0-intro-image.png" + :class (stl/css :start-image) + :border "0" + :alt "A graphic illustration with Penpot style"}] - [:div {:class (stl/css :modal-content)} - [:div {:class (stl/css :modal-header)} - [:h1 {:class (stl/css :modal-title)} - "Welcome to Penpot 2.0! "] + [:div {:class (stl/css :modal-content)} + [:div {:class (stl/css :modal-header)} + [:h1 {:class (stl/css :modal-title)} + "Welcome to Penpot 2.0! "] - [:div {:class (stl/css :version-tag)} - (dm/str "Version " version)]] + [:div {:class (stl/css :version-tag)} + (dm/str "Version " version)]] - [:div {:class (stl/css :features-block)} - [:p {:class (stl/css :feature-content)} - [:spam {:class (stl/css :feature-title)} - "CSS Grid Layout: "] - "Bring your designs to life, knowing that what you create is what developers code."] + [:div {:class (stl/css :features-block)} + [:p {:class (stl/css :feature-content)} + [:spam {:class (stl/css :feature-title)} + "CSS Grid Layout: "] + "Bring your designs to life, knowing that what you create is what developers code."] - [:p {:class (stl/css :feature-content)} - [:spam {:class (stl/css :feature-title)} - "Sleeker UI: "] - "We’ve polished Penpot to make your experience smoother and more enjoyable."] + [:p {:class (stl/css :feature-content)} + [:spam {:class (stl/css :feature-title)} + "Sleeker UI: "] + "We’ve polished Penpot to make your experience smoother and more enjoyable."] - [:p {:class (stl/css :feature-content)} - [:spam {:class (stl/css :feature-title)} - "New Components System: "] - "Managing and using your design components got a whole lot better."] + [:p {:class (stl/css :feature-content)} + [:spam {:class (stl/css :feature-title)} + "New Components System: "] + "Managing and using your design components got a whole lot better."] - [:p {:class (stl/css :feature-content)} - "And that’s not all - we’ve fined tuned performance and " - "accessibility to give you a better and more fluid design experience."] + [:p {:class (stl/css :feature-content)} + "And that’s not all - we’ve fined tuned performance and " + "accessibility to give you a better and more fluid design experience."] - [:p {:class (stl/css :feature-content)} - " Ready to dive in? Let 's get started!"]] + [:p {:class (stl/css :feature-content)} + " Ready to dive in? Let 's get started!"]] - [:div {:class (stl/css :navigation)} - [:button {:class (stl/css :next-btn) - :on-click next} "Continue"]]]]]] + [:div {:class (stl/css :navigation)} + [:button {:class (stl/css :next-btn) + :on-click next} "Continue"]]]]]] - 0 - [:div {:class (stl/css-case :modal-overlay true - :onboarding-a-b-test onboarding-a-b-test?)} - [:div.animated {:class klass} - [:div {:class (stl/css :modal-container)} - [:img {:src "images/features/2.0-css-grid.gif" - :class (stl/css :start-image) - :border "0" - :alt "Penpot's CSS Grid Layout"}] + 0 + [:div {:class (stl/css-case :modal-overlay true)} + [:div.animated {:class klass} + [:div {:class (stl/css :modal-container)} + [:img {:src "images/features/2.0-css-grid.gif" + :class (stl/css :start-image) + :border "0" + :alt "Penpot's CSS Grid Layout"}] - [:div {:class (stl/css :modal-content)} - [:div {:class (stl/css :modal-header)} - [:h1 {:class (stl/css :modal-title)} - "CSS Grid Layout - Design Meets Development"]] + [:div {:class (stl/css :modal-content)} + [:div {:class (stl/css :modal-header)} + [:h1 {:class (stl/css :modal-title)} + "CSS Grid Layout - Design Meets Development"]] - [:div {:class (stl/css :feature)} - [:p {:class (stl/css :feature-content)} - "The much-awaited Grid Layout introduces 2-dimensional" - " layout capabilities to Penpot, allowing for the creation" - " of adaptive layouts by leveraging the power of CSS properties."] + [:div {:class (stl/css :feature)} + [:p {:class (stl/css :feature-content)} + "The much-awaited Grid Layout introduces 2-dimensional" + " layout capabilities to Penpot, allowing for the creation" + " of adaptive layouts by leveraging the power of CSS properties."] - [:p {:class (stl/css :feature-content)} - "It’s a host of new features, including columns and" - " rows management, flexible units such as FR (fractions)," - " the ability to create and name areas, and tons of new " - "and unique possibilities within a design tool."] + [:p {:class (stl/css :feature-content)} + "It’s a host of new features, including columns and" + " rows management, flexible units such as FR (fractions)," + " the ability to create and name areas, and tons of new " + "and unique possibilities within a design tool."] - [:p {:class (stl/css :feature-content)} - "Designers will learn CSS basics while working, " - "and as always with Penpot, developers can pick" - " up the design as code to take it from there."]] + [:p {:class (stl/css :feature-content)} + "Designers will learn CSS basics while working, " + "and as always with Penpot, developers can pick" + " up the design as code to take it from there."]] - [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets - {:slide slide - :navigate navigate - :total 4}] + [:div {:class (stl/css :navigation)} + [:& c/navigation-bullets + {:slide slide + :navigate navigate + :total 4}] - [:button {:on-click next - :class (stl/css :next-btn)} "Continue"]]]]]] + [:button {:on-click next + :class (stl/css :next-btn)} "Continue"]]]]]] - 1 - [:div {:class (stl/css-case :modal-overlay true - :onboarding-a-b-test onboarding-a-b-test?)} - [:div.animated {:class klass} - [:div {:class (stl/css :modal-container)} - [:img {:src "images/features/2.0-new-ui.gif" - :class (stl/css :start-image) - :border "0" - :alt "Penpot's UI Makeover"}] + 1 + [:div {:class (stl/css-case :modal-overlay true)} + [:div.animated {:class klass} + [:div {:class (stl/css :modal-container)} + [:img {:src "images/features/2.0-new-ui.gif" + :class (stl/css :start-image) + :border "0" + :alt "Penpot's UI Makeover"}] - [:div {:class (stl/css :modal-content)} - [:div {:class (stl/css :modal-header)} - [:h1 {:class (stl/css :modal-title)} - "UI Makeover - Smoother, Sharper, and Simply More Fun"]] + [:div {:class (stl/css :modal-content)} + [:div {:class (stl/css :modal-header)} + [:h1 {:class (stl/css :modal-title)} + "UI Makeover - Smoother, Sharper, and Simply More Fun"]] - [:div {:class (stl/css :feature)} - [:p {:class (stl/css :feature-content)} - "We've completely overhauled Penpot's user interface. " - "The improvements in consistency, the introduction of " - "new microinteractions, and attention to countless details" - " will significantly enhance the productivity and enjoyment of using Penpot."] - [:p {:class (stl/css :feature-content)} - "Furthermore, we’ve made several accessibility improvements, " - "with better color contrast, keyboard navigation," - " and adherence to other best practices."]] + [:div {:class (stl/css :feature)} + [:p {:class (stl/css :feature-content)} + "We've completely overhauled Penpot's user interface. " + "The improvements in consistency, the introduction of " + "new microinteractions, and attention to countless details" + " will significantly enhance the productivity and enjoyment of using Penpot."] + [:p {:class (stl/css :feature-content)} + "Furthermore, we’ve made several accessibility improvements, " + "with better color contrast, keyboard navigation," + " and adherence to other best practices."]] - [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets - {:slide slide - :navigate navigate - :total 4}] + [:div {:class (stl/css :navigation)} + [:& c/navigation-bullets + {:slide slide + :navigate navigate + :total 4}] - [:button {:on-click next - :class (stl/css :next-btn)} "Continue"]]]]]] + [:button {:on-click next + :class (stl/css :next-btn)} "Continue"]]]]]] - 2 - [:div {:class (stl/css-case :modal-overlay true - :onboarding-a-b-test onboarding-a-b-test?)} - [:div.animated {:class klass} - [:div {:class (stl/css :modal-container)} - [:img {:src "images/features/2.0-components.gif" - :class (stl/css :start-image) - :border "0" - :alt "Penpot's new components system"}] + 2 + [:div {:class (stl/css-case :modal-overlay true)} + [:div.animated {:class klass} + [:div {:class (stl/css :modal-container)} + [:img {:src "images/features/2.0-components.gif" + :class (stl/css :start-image) + :border "0" + :alt "Penpot's new components system"}] - [:div {:class (stl/css :modal-content)} - [:div {:class (stl/css :modal-header)} - [:h1 {:class (stl/css :modal-title)} - "New Components System"]] - [:div {:class (stl/css :feature)} - [:p {:class (stl/css :feature-content)} - "The new Penpot components system improves" - " control over instances, including their " - "inheritances and properties overrides. " - "Main components are now accessible as design" - " elements, allowing a better updating " - "workflow through instant changes synchronization."] - [:p {:class (stl/css :feature-content)} - "And that’s not all, there are new capabilities " - "such as component swapping and annotations " - "that will help you to better manage your design systems."]] + [:div {:class (stl/css :modal-content)} + [:div {:class (stl/css :modal-header)} + [:h1 {:class (stl/css :modal-title)} + "New Components System"]] + [:div {:class (stl/css :feature)} + [:p {:class (stl/css :feature-content)} + "The new Penpot components system improves" + " control over instances, including their " + "inheritances and properties overrides. " + "Main components are now accessible as design" + " elements, allowing a better updating " + "workflow through instant changes synchronization."] + [:p {:class (stl/css :feature-content)} + "And that’s not all, there are new capabilities " + "such as component swapping and annotations " + "that will help you to better manage your design systems."]] - [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets - {:slide slide - :navigate navigate - :total 4}] + [:div {:class (stl/css :navigation)} + [:& c/navigation-bullets + {:slide slide + :navigate navigate + :total 4}] - [:button {:on-click next - :class (stl/css :next-btn)} "Continue"]]]]]] + [:button {:on-click next + :class (stl/css :next-btn)} "Continue"]]]]]] - 3 - [:div {:class (stl/css-case :modal-overlay true - :onboarding-a-b-test onboarding-a-b-test?)} - [:div.animated {:class klass} - [:div {:class (stl/css :modal-container)} - [:img {:src "images/features/2.0-html.gif" - :class (stl/css :start-image) - :border "0" - :alt " Penpot's HTML code generator"}] + 3 + [:div {:class (stl/css-case :modal-overlay true)} + [:div.animated {:class klass} + [:div {:class (stl/css :modal-container)} + [:img {:src "images/features/2.0-html.gif" + :class (stl/css :start-image) + :border "0" + :alt " Penpot's HTML code generator"}] - [:div {:class (stl/css :modal-content)} - [:div {:class (stl/css :modal-header)} - [:h1 {:class (stl/css :modal-title)} - "And much more"]] - [:div {:class (stl/css :feature)} - [:p {:class (stl/css :feature-content)} - "In addition to all of this, we’ve included several other requested improvements:"] - [:ul {:class (stl/css :feature-list)} - [:li "Access HTML markup code directly in inspect mode"] - [:li "Images are now treated as element fills, maintaining their aspect ratio on resize, ideal for flexible designs"] - [:li "Enjoy new color themes with options for both dark and light modes"] - [:li "Feel the speed boost! Enjoy a smoother experience with a bunch of performance improvements"]]] + [:div {:class (stl/css :modal-content)} + [:div {:class (stl/css :modal-header)} + [:h1 {:class (stl/css :modal-title)} + "And much more"]] + [:div {:class (stl/css :feature)} + [:p {:class (stl/css :feature-content)} + "In addition to all of this, we’ve included several other requested improvements:"] + [:ul {:class (stl/css :feature-list)} + [:li "Access HTML markup code directly in inspect mode"] + [:li "Images are now treated as element fills, maintaining their aspect ratio on resize, ideal for flexible designs"] + [:li "Enjoy new color themes with options for both dark and light modes"] + [:li "Feel the speed boost! Enjoy a smoother experience with a bunch of performance improvements"]]] - [:div {:class (stl/css :navigation)} + [:div {:class (stl/css :navigation)} - [:& c/navigation-bullets - {:slide slide - :navigate navigate - :total 4}] + [:& c/navigation-bullets + {:slide slide + :navigate navigate + :total 4}] - [:button {:on-click finish - :class (stl/css :next-btn)} "Let's go"]]]]]])))) + [:button {:on-click finish + :class (stl/css :next-btn)} "Let's go"]]]]]]))) diff --git a/frontend/src/app/main/ui/settings/sidebar.cljs b/frontend/src/app/main/ui/settings/sidebar.cljs index c878c7b4b..6b6bed60d 100644 --- a/frontend/src/app/main/ui/settings/sidebar.cljs +++ b/frontend/src/app/main/ui/settings/sidebar.cljs @@ -89,6 +89,7 @@ [:li {:class (stl/css-case :current options? :settings-item true) :on-click go-settings-options + :data-testid "settings-profile" :data-test "settings-profile"} [:span {:class (stl/css :element-title)} (tr "labels.settings")]]