mirror of
https://github.com/penpot/penpot.git
synced 2026-02-26 20:00:16 -05:00
Compare commits
21 Commits
alotor-plu
...
staging-re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed23c55550 | ||
|
|
5b5c868a87 | ||
|
|
1a3ac6bdf8 | ||
|
|
de5d4f4292 | ||
|
|
2bd7c10e09 | ||
|
|
495371c079 | ||
|
|
75b1c0c1b1 | ||
|
|
5ea4b03108 | ||
|
|
0fef5b7e5d | ||
|
|
8a1fdd9dd1 | ||
|
|
a080a9e646 | ||
|
|
a728d5a5f2 | ||
|
|
6072234230 | ||
|
|
41f2877801 | ||
|
|
e2576d049a | ||
|
|
4db9c373e6 | ||
|
|
09a9407867 | ||
|
|
7be03e2ea6 | ||
|
|
05165ce014 | ||
|
|
96677713fc | ||
|
|
a12b59d101 |
1
.github/workflows/build-develop.yml
vendored
1
.github/workflows/build-develop.yml
vendored
@@ -1,7 +1,6 @@
|
||||
name: _DEVELOP
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '16 5-20 * * 1-5'
|
||||
|
||||
|
||||
1
.github/workflows/build-staging-render.yml
vendored
1
.github/workflows/build-staging-render.yml
vendored
@@ -1,7 +1,6 @@
|
||||
name: _STAGING RENDER
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '36 5-20 * * 1-5'
|
||||
|
||||
|
||||
1
.github/workflows/build-staging.yml
vendored
1
.github/workflows/build-staging.yml
vendored
@@ -1,7 +1,6 @@
|
||||
name: _STAGING
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '36 5-20 * * 1-5'
|
||||
|
||||
|
||||
1
.github/workflows/build-tag.yml
vendored
1
.github/workflows/build-tag.yml
vendored
@@ -1,7 +1,6 @@
|
||||
name: _TAG
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
23
CHANGES.md
23
CHANGES.md
@@ -1,27 +1,5 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 2.15.0 (Unreleased)
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
### :sparkles: New features & Enhancements
|
||||
|
||||
- Allow duplicating color and typography styles (by @MkDev11) [Github #2912](https://github.com/penpot/penpot/issues/2912)
|
||||
- Add MCP server integration [Taiga #13112](https://tree.taiga.io/project/penpot/us/13112), [Taiga #13114](https://tree.taiga.io/project/penpot/us/13114)
|
||||
- Add woff2 support on user uploaded fonts (by @Nivl) [Github #8248](https://github.com/penpot/penpot/pull/8248)
|
||||
- Option to download custom fonts (by @dfelinto) [Github #8320](https://github.com/penpot/penpot/issues/8320)
|
||||
- Add copy as image to clipboard option to workspace context menu (by @dfelinto) [Github #8313](https://github.com/penpot/penpot/pull/8313)
|
||||
- Import Tokens from linked library [Github #8391](https://github.com/penpot/penpot/pull/8391)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix Alt/Option to draw shapes from center point (by @offreal) [Github #8361](https://github.com/penpot/penpot/pull/8361)
|
||||
|
||||
|
||||
## 2.14.0 (Unreleased)
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
@@ -53,7 +31,6 @@
|
||||
- Fix boolean operators in menu for boards [Taiga #13174](https://tree.taiga.io/project/penpot/issue/13174)
|
||||
- Fix viewer can update library [Taiga #13186](https://tree.taiga.io/project/penpot/issue/13186)
|
||||
- Fix remove fill affects different element than selected [Taiga #13128](https://tree.taiga.io/project/penpot/issue/13128)
|
||||
- Fix unable to finish the create account form using keyboard [Taiga #11333](https://tree.taiga.io/project/penpot/issue/11333)
|
||||
- Fix 45 rotated board titles rendered incorrectly [Taiga #13306](https://tree.taiga.io/project/penpot/issue/13306)
|
||||
|
||||
## 2.13.3
|
||||
|
||||
@@ -53,7 +53,6 @@
|
||||
::yres/status 200
|
||||
::yres/body (yres/stream-body
|
||||
(fn [_ output]
|
||||
|
||||
(let [channel (sp/chan :buf buf :xf (keep encode))
|
||||
listener (events/spawn-listener
|
||||
channel
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
[:path ::fs/path]
|
||||
[:mtype {:optional true} ::sm/text]])
|
||||
|
||||
(def check-input
|
||||
(def ^:private check-input
|
||||
(sm/check-fn schema:input))
|
||||
|
||||
(defn validate-media-type!
|
||||
@@ -381,22 +381,6 @@
|
||||
(when (zero? (:exit res))
|
||||
(:out res))))
|
||||
|
||||
(woff2->sfnt [data]
|
||||
;; woff2_decompress outputs to same directory with .ttf extension
|
||||
(let [finput (tmp/tempfile :prefix "penpot.font." :suffix ".woff2")
|
||||
foutput (fs/path (str/replace (str finput) #"\.woff2$" ".ttf"))]
|
||||
(try
|
||||
(io/write* finput data)
|
||||
(let [res (sh/sh "woff2_decompress" (str finput))]
|
||||
(if (zero? (:exit res))
|
||||
foutput
|
||||
(do
|
||||
(when (fs/exists? foutput)
|
||||
(fs/delete foutput))
|
||||
nil)))
|
||||
(finally
|
||||
(fs/delete finput)))))
|
||||
|
||||
;; Documented here:
|
||||
;; https://docs.microsoft.com/en-us/typography/opentype/spec/otff#table-directory
|
||||
(get-sfnt-type [data]
|
||||
@@ -446,27 +430,4 @@
|
||||
|
||||
(= stype :ttf)
|
||||
(-> (assoc "font/otf" (ttf->otf sfnt))
|
||||
(assoc "font/ttf" sfnt)))))
|
||||
|
||||
(contains? current "font/woff2")
|
||||
(let [data (get input "font/woff2")
|
||||
foutput (woff2->sfnt data)]
|
||||
(when-not foutput
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-woff2-file
|
||||
:hint "invalid woff2 file"))
|
||||
(try
|
||||
(let [sfnt (io/read* foutput)
|
||||
type (get-sfnt-type sfnt)]
|
||||
(cond-> input
|
||||
(= type :otf)
|
||||
(-> (assoc "font/otf" sfnt)
|
||||
(assoc "font/ttf" (otf->ttf sfnt))
|
||||
(update "font/woff" gen-if-nil #(ttf-or-otf->woff sfnt)))
|
||||
|
||||
(= type :ttf)
|
||||
(-> (assoc "font/ttf" sfnt)
|
||||
(assoc "font/otf" (ttf->otf sfnt))
|
||||
(update "font/woff" gen-if-nil #(ttf-or-otf->woff sfnt)))))
|
||||
(finally
|
||||
(fs/delete foutput))))))))
|
||||
(assoc "font/ttf" sfnt)))))))))
|
||||
|
||||
@@ -463,10 +463,8 @@
|
||||
:fn (mg/resource "app/migrations/sql/0144-mod-server-error-report-table.sql")}
|
||||
|
||||
{:name "0145-fix-plugins-uri-on-profile"
|
||||
:fn mg0145/migrate}
|
||||
:fn mg0145/migrate}])
|
||||
|
||||
{:name "0146-mod-access-token-table"
|
||||
:fn (mg/resource "app/migrations/sql/0146-mod-access-token-table.sql")}])
|
||||
|
||||
(defn apply-migrations!
|
||||
[pool name migrations]
|
||||
|
||||
@@ -58,4 +58,3 @@
|
||||
(when (nil? (:data file))
|
||||
(migrate-file conn file)))
|
||||
(db/exec-one! conn ["drop table page cascade;"])))
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
ALTER TABLE access_token
|
||||
ADD COLUMN type text NULL;
|
||||
@@ -87,10 +87,6 @@
|
||||
[:map
|
||||
[:valid ::sm/boolean]])
|
||||
|
||||
(def ^:private schema:connectivity
|
||||
[:map
|
||||
[:licenses ::sm/boolean]])
|
||||
|
||||
(defn- get-team-org
|
||||
[cfg {:keys [team-id] :as params}]
|
||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
||||
@@ -101,11 +97,6 @@
|
||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
||||
(request-to-nitrate cfg :get (str baseuri "/api/users/" (str profile-id)) schema:user params)))
|
||||
|
||||
(defn- get-connectivity
|
||||
[cfg params]
|
||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
||||
(request-to-nitrate cfg :get (str baseuri "/api/connectivity") schema:connectivity params)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; INITIALIZATION
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -114,8 +105,7 @@
|
||||
[_ cfg]
|
||||
(when (contains? cf/flags :nitrate)
|
||||
{:get-team-org (partial get-team-org cfg)
|
||||
:is-valid-user (partial is-valid-user cfg)
|
||||
:connectivity (partial get-connectivity cfg)}))
|
||||
:is-valid-user (partial is-valid-user cfg)}))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; UTILS
|
||||
@@ -126,8 +116,7 @@
|
||||
[cfg profile]
|
||||
(try
|
||||
(let [nitrate-licence (call cfg :is-valid-user {:profile-id (:id profile)})]
|
||||
(assoc-in profile [:props :nitrate-license]
|
||||
(select-keys nitrate-licence [:valid :created-at])))
|
||||
(assoc profile :nitrate-licence (:valid nitrate-licence)))
|
||||
(catch Throwable cause
|
||||
(l/error :hint "failed to get nitrate licence"
|
||||
:profile-id (:id profile)
|
||||
@@ -139,7 +128,3 @@
|
||||
(let [params (assoc (or params {}) :team-id (:id team))
|
||||
org (call cfg :get-team-org params)]
|
||||
(assoc team :organization-id (:id org) :organization-name (:name org))))
|
||||
|
||||
(defn connectivity
|
||||
[cfg]
|
||||
(call cfg :connectivity {}))
|
||||
|
||||
@@ -73,13 +73,9 @@
|
||||
(if (nil? result)
|
||||
204
|
||||
200))
|
||||
|
||||
headers (::http/headers mdata {})
|
||||
headers (cond-> headers
|
||||
(and (yres/stream-body? result)
|
||||
(not (contains? headers "content-type")))
|
||||
headers (cond-> (::http/headers mdata {})
|
||||
(yres/stream-body? result)
|
||||
(assoc "content-type" "application/octet-stream"))]
|
||||
|
||||
{::yres/status status
|
||||
::yres/headers headers
|
||||
::yres/body result}))]
|
||||
@@ -262,7 +258,6 @@
|
||||
'app.rpc.commands.ldap
|
||||
'app.rpc.commands.management
|
||||
'app.rpc.commands.media
|
||||
'app.rpc.commands.nitrate
|
||||
'app.rpc.commands.profile
|
||||
'app.rpc.commands.projects
|
||||
'app.rpc.commands.search
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
(dissoc row :perms))
|
||||
|
||||
(defn create-access-token
|
||||
[{:keys [::db/conn] :as cfg} profile-id name expiration type]
|
||||
[{:keys [::db/conn] :as cfg} profile-id name expiration]
|
||||
(let [token-id (uuid/next)
|
||||
expires-at (some-> expiration (ct/in-future))
|
||||
created-at (ct/now)
|
||||
@@ -36,7 +36,6 @@
|
||||
{:id token-id
|
||||
:name name
|
||||
:token token
|
||||
:type type
|
||||
:profile-id profile-id
|
||||
:created-at created-at
|
||||
:updated-at created-at
|
||||
@@ -51,18 +50,17 @@
|
||||
(def ^:private schema:create-access-token
|
||||
[:map {:title "create-access-token"}
|
||||
[:name [:string {:max 250 :min 1}]]
|
||||
[:expiration {:optional true} ::ct/duration]
|
||||
[:type {:optional true} :string]])
|
||||
[:expiration {:optional true} ::ct/duration]])
|
||||
|
||||
(sv/defmethod ::create-access-token
|
||||
{::doc/added "1.18"
|
||||
::sm/params schema:create-access-token}
|
||||
[cfg {:keys [::rpc/profile-id name expiration type]}]
|
||||
[cfg {:keys [::rpc/profile-id name expiration]}]
|
||||
|
||||
(quotes/check! cfg {::quotes/id ::quotes/access-tokens-per-profile
|
||||
::quotes/profile-id profile-id})
|
||||
|
||||
(db/tx-run! cfg create-access-token profile-id name expiration type))
|
||||
(db/tx-run! cfg create-access-token profile-id name expiration))
|
||||
|
||||
(def ^:private schema:delete-access-token
|
||||
[:map {:title "delete-access-token"}
|
||||
@@ -85,22 +83,5 @@
|
||||
(->> (db/query pool :access-token
|
||||
{:profile-id profile-id}
|
||||
{:order-by [[:expires-at :asc] [:created-at :asc]]
|
||||
:columns [:id :name :perms :type :created-at :updated-at :expires-at]})
|
||||
:columns [:id :name :perms :created-at :updated-at :expires-at]})
|
||||
(mapv decode-row)))
|
||||
|
||||
|
||||
(def ^:private schema:get-current-mcp-token
|
||||
[:map {:title "get-current-mcp-token"}])
|
||||
|
||||
(sv/defmethod ::get-current-mcp-token
|
||||
{::doc/added "2.15"
|
||||
::sm/params schema:get-current-mcp-token}
|
||||
[{:keys [::db/pool]} {:keys [::rpc/profile-id ::rpc/request-at]}]
|
||||
(->> (db/query pool :access-token
|
||||
{:profile-id profile-id
|
||||
:type "mcp"}
|
||||
{:order-by [[:expires-at :asc] [:created-at :asc]]
|
||||
:columns [:token :expires-at]})
|
||||
(remove #(ct/is-after? (:expires-at %) request-at))
|
||||
(map decode-row)
|
||||
(first)))
|
||||
|
||||
@@ -9,14 +9,12 @@
|
||||
[app.binfile.common :as bfc]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.media :as cmedia]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as-alias sql]
|
||||
[app.features.logical-deletion :as ldel]
|
||||
[app.http :as-alias http]
|
||||
[app.loggers.audit :as-alias audit]
|
||||
[app.loggers.webhooks :as-alias webhooks]
|
||||
[app.media :as media]
|
||||
@@ -36,9 +34,7 @@
|
||||
java.io.InputStream
|
||||
java.io.OutputStream
|
||||
java.io.SequenceInputStream
|
||||
java.util.Collections
|
||||
java.util.zip.ZipEntry
|
||||
java.util.zip.ZipOutputStream))
|
||||
java.util.Collections))
|
||||
|
||||
(set! *warn-on-reflection* true)
|
||||
|
||||
@@ -300,98 +296,3 @@
|
||||
(rph/with-meta (rph/wrap)
|
||||
{::audit/props {:font-family (:font-family variant)
|
||||
:font-id (:font-id variant)}})))
|
||||
|
||||
;; --- DOWNLOAD FONT
|
||||
|
||||
(defn- make-temporal-storage-object
|
||||
[cfg profile-id content]
|
||||
(let [storage (sto/resolve cfg)
|
||||
content (media/check-input content)
|
||||
hash (sto/calculate-hash (:path content))
|
||||
data (-> (sto/content (:path content))
|
||||
(sto/wrap-with-hash hash))
|
||||
mtype (:mtype content "application/octet-stream")
|
||||
content {::sto/content data
|
||||
::sto/deduplicate? true
|
||||
::sto/touched-at (ct/in-future {:minutes 30})
|
||||
:profile-id profile-id
|
||||
:content-type mtype
|
||||
:bucket "tempfile"}]
|
||||
|
||||
(sto/put-object! storage content)))
|
||||
|
||||
(defn- make-variant-filename
|
||||
[v mtype]
|
||||
(str (:font-family v) "-" (:font-weight v)
|
||||
(when-not (= "normal" (:font-style v)) (str "-" (:font-style v)))
|
||||
(cmedia/mtype->extension mtype)))
|
||||
|
||||
(def ^:private schema:download-font
|
||||
[:map {:title "download-font"}
|
||||
[:id ::sm/uuid]])
|
||||
|
||||
(sv/defmethod ::download-font
|
||||
"Download the font file. Returns a http redirect to the asset resource uri."
|
||||
{::doc/added "2.15"
|
||||
::sm/params schema:download-font}
|
||||
[{:keys [::sto/storage ::db/pool] :as cfg} {:keys [::rpc/profile-id id]}]
|
||||
(let [variant (db/get pool :team-font-variant {:id id})]
|
||||
(teams/check-read-permissions! pool profile-id (:team-id variant))
|
||||
|
||||
;; Try to get the best available font format (prefer TTF for broader compatibility).
|
||||
(let [media-id (or (:ttf-file-id variant)
|
||||
(:otf-file-id variant)
|
||||
(:woff2-file-id variant)
|
||||
(:woff1-file-id variant))
|
||||
sobj (sto/get-object storage media-id)
|
||||
mtype (-> sobj meta :content-type)]
|
||||
|
||||
{:id (:id sobj)
|
||||
:uri (files/resolve-public-uri (:id sobj))
|
||||
:name (make-variant-filename variant mtype)})))
|
||||
|
||||
(def ^:private schema:download-font-family
|
||||
[:map {:title "download-font-family"}
|
||||
[:font-id ::sm/uuid]])
|
||||
|
||||
(sv/defmethod ::download-font-family
|
||||
"Download the entire font family as a zip file. Returns the zip
|
||||
bytes on the body, without encoding it on transit or json."
|
||||
{::doc/added "2.15"
|
||||
::sm/params schema:download-font-family}
|
||||
[{:keys [::sto/storage ::db/pool] :as cfg} {:keys [::rpc/profile-id font-id]}]
|
||||
(let [variants (db/query pool :team-font-variant
|
||||
{:font-id font-id
|
||||
:deleted-at nil})]
|
||||
|
||||
(when-not (seq variants)
|
||||
(ex/raise :type :not-found
|
||||
:code :object-not-found))
|
||||
|
||||
(teams/check-read-permissions! pool profile-id (:team-id (first variants)))
|
||||
|
||||
(let [tempfile (tmp/tempfile :suffix ".zip")
|
||||
ffamily (-> variants first :font-family)]
|
||||
|
||||
(with-open [^OutputStream output (io/output-stream tempfile)
|
||||
^OutputStream output (ZipOutputStream. output)]
|
||||
(doseq [v variants]
|
||||
(let [media-id (or (:ttf-file-id v)
|
||||
(:otf-file-id v)
|
||||
(:woff2-file-id v)
|
||||
(:woff1-file-id v))
|
||||
sobj (sto/get-object storage media-id)
|
||||
mtype (-> sobj meta :content-type)
|
||||
name (make-variant-filename v mtype)]
|
||||
|
||||
(with-open [input (sto/get-object-data storage sobj)]
|
||||
(.putNextEntry ^ZipOutputStream output (ZipEntry. ^String name))
|
||||
(io/copy input output :size (:size sobj))
|
||||
(.closeEntry ^ZipOutputStream output)))))
|
||||
|
||||
(let [{:keys [id] :as sobj} (make-temporal-storage-object cfg profile-id
|
||||
{:mtype "application/zip"
|
||||
:path tempfile})]
|
||||
{:id id
|
||||
:uri (files/resolve-public-uri id)
|
||||
:name (str ffamily ".zip")}))))
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
(ns app.rpc.commands.nitrate
|
||||
(:require
|
||||
[app.common.schema :as sm]
|
||||
[app.nitrate :as nitrate]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.util.services :as sv]))
|
||||
|
||||
|
||||
(def schema:connectivity
|
||||
[:map {:title "nitrate-connectivity"}
|
||||
[:licenses ::sm/boolean]])
|
||||
|
||||
(sv/defmethod ::get-nitrate-connectivity
|
||||
{::rpc/auth false
|
||||
::doc/added "1.18"
|
||||
::sm/params [:map]
|
||||
::sm/result schema:connectivity}
|
||||
[cfg _params]
|
||||
(nitrate/connectivity cfg))
|
||||
@@ -48,7 +48,6 @@
|
||||
(def schema:props
|
||||
[:map {:title "ProfileProps"}
|
||||
[:plugins {:optional true} schema:plugin-registry]
|
||||
[:mcp-status {:optional true} ::sm/boolean]
|
||||
[:newsletter-updates {:optional true} ::sm/boolean]
|
||||
[:newsletter-news {:optional true} ::sm/boolean]
|
||||
[:onboarding-team-id {:optional true} ::sm/uuid]
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
|
||||
(t/deftest access-token-authz
|
||||
(let [profile (th/create-profile* 1)
|
||||
token (db/tx-run! th/*system* app.rpc.commands.access-token/create-access-token (:id profile) "test" nil nil)
|
||||
token (db/tx-run! th/*system* app.rpc.commands.access-token/create-access-token (:id profile) "test" nil)
|
||||
handler (#'app.http.access-token/wrap-authz identity th/*system*)]
|
||||
|
||||
(let [response (handler nil)]
|
||||
|
||||
@@ -107,18 +107,4 @@
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error out)))
|
||||
(let [results (:result out)]
|
||||
(t/is (= 2 (count results))))))
|
||||
|
||||
(t/testing "get mcp token"
|
||||
(let [_ (th/command! {::th/type :create-access-token
|
||||
::rpc/profile-id (:id prof)
|
||||
:type "mcp"
|
||||
:name "token 1"
|
||||
:perms ["get-profile"]})
|
||||
{:keys [error result]}
|
||||
(th/command! {::th/type :get-current-mcp-token
|
||||
::rpc/profile-id (:id prof)})]
|
||||
;; (th/print-result! result)
|
||||
(t/is (nil? error))
|
||||
(t/is (string? (:token result)))))))
|
||||
|
||||
(t/is (= 2 (count results))))))))
|
||||
|
||||
@@ -93,41 +93,6 @@
|
||||
:font-weight
|
||||
:font-style))))
|
||||
|
||||
(t/deftest woff2-font-upload-1
|
||||
(let [prof (th/create-profile* 1 {:is-active true})
|
||||
team-id (:default-team-id prof)
|
||||
proj-id (:default-project-id prof)
|
||||
font-id (uuid/custom 10 1)
|
||||
|
||||
data (-> (io/resource "backend_tests/test_files/font-1.woff2")
|
||||
(io/read*))
|
||||
|
||||
params {::th/type :create-font-variant
|
||||
::rpc/profile-id (:id prof)
|
||||
:team-id team-id
|
||||
:font-id font-id
|
||||
:font-family "somefont"
|
||||
:font-weight 400
|
||||
:font-style "normal"
|
||||
:data {"font/woff2" data}}
|
||||
out (th/command! params)]
|
||||
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error out)))
|
||||
(let [result (:result out)]
|
||||
(t/is (uuid? (:id result)))
|
||||
(t/is (uuid? (:ttf-file-id result)))
|
||||
(t/is (uuid? (:otf-file-id result)))
|
||||
(t/is (uuid? (:woff1-file-id result)))
|
||||
(t/is (uuid? (:woff2-file-id result)))
|
||||
(t/are [k] (= (get params k)
|
||||
(get result k))
|
||||
:team-id
|
||||
:font-id
|
||||
:font-family
|
||||
:font-weight
|
||||
:font-style))))
|
||||
|
||||
(t/deftest font-deletion-1
|
||||
(let [prof (th/create-profile* 1 {:is-active true})
|
||||
team-id (:default-team-id prof)
|
||||
|
||||
Binary file not shown.
@@ -760,21 +760,6 @@
|
||||
default
|
||||
v))))
|
||||
|
||||
(defn percent?
|
||||
[v]
|
||||
(str/numeric? (str/rtrim v "%")))
|
||||
|
||||
(defn parse-percent
|
||||
([v]
|
||||
(parse-percent v nil))
|
||||
([v default]
|
||||
(if (str/ends-with? v "%")
|
||||
(let [v (impl-parse-double (str/trim v "%"))]
|
||||
(if (or (nil? v) (nan? v))
|
||||
default
|
||||
(/ v 100)))
|
||||
(parse-double v default))))
|
||||
|
||||
(defn parse-uuid
|
||||
[v]
|
||||
(try
|
||||
|
||||
@@ -31,56 +31,18 @@
|
||||
(def schema:token-value-generic
|
||||
[::sm/text {:error/fn token-value-empty-fn}])
|
||||
|
||||
(def schema:token-value-numeric
|
||||
[:and
|
||||
[::sm/text {:error/fn token-value-empty-fn}]
|
||||
[:fn {:error/fn #(tr "workspace.tokens.invalid-value" (:value %))}
|
||||
(fn [value]
|
||||
(if (str/numeric? value)
|
||||
(let [n (d/parse-double value)]
|
||||
(some? n))
|
||||
true))]]) ;; Leave references or formulas to be checked by the resolver
|
||||
|
||||
(def schema:token-value-percent
|
||||
[:and
|
||||
[::sm/text {:error/fn token-value-empty-fn}]
|
||||
[:fn {:error/fn #(tr "workspace.tokens.value-with-percent" (:value %))}
|
||||
(fn [value]
|
||||
(if (d/percent? value)
|
||||
(let [v (d/parse-percent value)]
|
||||
(some? v))
|
||||
true))]]) ;; Leave references or formulas to be checked by the resolver
|
||||
|
||||
(def schema:token-value-composite-ref
|
||||
[::sm/text {:error/fn token-value-empty-fn}])
|
||||
|
||||
(def schema:token-value-opacity
|
||||
[:and
|
||||
[::sm/text {:error/fn token-value-empty-fn}]
|
||||
[:fn {:error/fn #(tr "workspace.tokens.opacity-range")}
|
||||
(fn [opacity]
|
||||
(if (str/numeric? opacity)
|
||||
(let [n (d/parse-percent opacity)]
|
||||
(and (some? n) (<= 0 n 1)))
|
||||
true))]]) ;; Leave references or formulas to be checked by the resolver
|
||||
|
||||
(def schema:token-value-font-family
|
||||
[:or
|
||||
[:vector ::sm/text]
|
||||
cto/schema:token-ref])
|
||||
|
||||
(def schema:token-value-font-weight
|
||||
[:or
|
||||
[:fn {:error/fn #(tr "workspace.tokens.invalid-font-weight-token-value")}
|
||||
cto/valid-font-weight-variant]
|
||||
::sm/text]) ;; Leave references or formulas to be checked by the resolver
|
||||
[:vector ::sm/text])
|
||||
|
||||
(def schema:token-value-typography-map
|
||||
[:map
|
||||
[:font-family {:optional true} schema:token-value-font-family]
|
||||
[:font-size {:optional true} schema:token-value-numeric]
|
||||
[:font-weight {:optional true} schema:token-value-font-weight]
|
||||
[:line-height {:optional true} schema:token-value-percent]
|
||||
[:font-weight {:optional true} schema:token-value-generic]
|
||||
[:font-size {:optional true} schema:token-value-generic]
|
||||
[:line-height {:optional true} schema:token-value-generic]
|
||||
[:letter-spacing {:optional true} schema:token-value-generic]
|
||||
[:paragraph-spacing {:optional true} schema:token-value-generic]
|
||||
[:text-decoration {:optional true} schema:token-value-generic]
|
||||
@@ -122,10 +84,7 @@
|
||||
[token-type]
|
||||
[:multi {:dispatch (constantly token-type)
|
||||
:title "Token Value"}
|
||||
[:opacity schema:token-value-opacity]
|
||||
[:font-family schema:token-value-font-family]
|
||||
[:font-size schema:token-value-numeric]
|
||||
[:font-weight schema:token-value-font-weight]
|
||||
[:typography schema:token-value-typography]
|
||||
[:shadow schema:token-value-shadow]
|
||||
[::m/default schema:token-value-generic]])
|
||||
@@ -210,7 +169,7 @@
|
||||
[tokens-lib set-id]
|
||||
[:and
|
||||
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
|
||||
[:fn {:error/fn #(tr "errors.token-set-already-exists")}
|
||||
[:fn {:error/fn #(tr "errors.token-set-already-exists" (:value %))}
|
||||
(fn [name]
|
||||
(or (nil? tokens-lib)
|
||||
(let [set (ctob/get-set-by-name tokens-lib name)]
|
||||
@@ -237,7 +196,7 @@
|
||||
[tokens-lib name theme-id]
|
||||
[:and
|
||||
[:string {:min 0 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
|
||||
[:fn {:error/fn #(tr "errors.token-theme-already-exists")}
|
||||
[:fn {:error/fn #(tr "errors.token-theme-already-exists" (:value %))}
|
||||
(fn [group]
|
||||
(or (nil? tokens-lib)
|
||||
(let [theme (ctob/get-theme-by-name tokens-lib group name)]
|
||||
|
||||
@@ -119,13 +119,12 @@
|
||||
:strict-session-cookies
|
||||
:telemetry
|
||||
:terms-and-privacy-checkbox
|
||||
;; Only for developtment.
|
||||
:tiered-file-data-storage
|
||||
:token-base-font-size
|
||||
:token-color
|
||||
:token-shadow
|
||||
:token-tokenscript
|
||||
:token-import-from-library
|
||||
;; Only for developtment.
|
||||
:transit-readable-response
|
||||
:user-feedback
|
||||
;; TODO: remove this flag.
|
||||
@@ -153,9 +152,7 @@
|
||||
:redis-cache
|
||||
|
||||
;; Activates the nitrate module
|
||||
:nitrate
|
||||
|
||||
:mcp})
|
||||
:nitrate})
|
||||
|
||||
(def all-flags
|
||||
(set/union email login varia))
|
||||
@@ -181,8 +178,7 @@
|
||||
:enable-token-color
|
||||
:enable-token-shadow
|
||||
:enable-inspect-styles
|
||||
:enable-feature-fdata-objects-map
|
||||
:enable-token-import-from-library])
|
||||
:enable-feature-fdata-objects-map])
|
||||
|
||||
(defn parse
|
||||
[& flags]
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
(def font-types
|
||||
#{"font/ttf"
|
||||
"font/woff"
|
||||
"font/woff2"
|
||||
"font/otf"
|
||||
"font/opentype"})
|
||||
|
||||
@@ -82,22 +81,21 @@
|
||||
(defn parse-font-weight
|
||||
[variant]
|
||||
(cond
|
||||
(re-seq #"(?i)(?:^|[-_\s])(hairline|thin)(?=(?:[-_\s]|$|italic\b))" variant) 100
|
||||
(re-seq #"(?i)(?:^|[-_\s])(extra\s*light|ultra\s*light)(?=(?:[-_\s]|$|italic\b))" variant) 200
|
||||
(re-seq #"(?i)(?:^|[-_\s])(light)(?=(?:[-_\s]|$|italic\b))" variant) 300
|
||||
(re-seq #"(?i)(?:^|[-_\s])(normal|regular)(?=(?:[-_\s]|$|italic\b))" variant) 400
|
||||
(re-seq #"(?i)(?:^|[-_\s])(medium)(?=(?:[-_\s]|$|italic\b))" variant) 500
|
||||
(re-seq #"(?i)(?:^|[-_\s])(semi\s*bold|demi\s*bold)(?=(?:[-_\s]|$|italic\b))" variant) 600
|
||||
(re-seq #"(?i)(?:^|[-_\s])(extra\s*bold|ultra\s*bold)(?=(?:[-_\s]|$|italic\b))" variant) 800
|
||||
(re-seq #"(?i)(?:^|[-_\s])(bold)(?=(?:[-_\s]|$|italic\b))" variant) 700
|
||||
(re-seq #"(?i)(?:^|[-_\s])(extra\s*black|ultra\s*black)(?=(?:[-_\s]|$|italic\b))" variant) 950
|
||||
(re-seq #"(?i)(?:^|[-_\s])(black|heavy|solid)(?=(?:[-_\s]|$|italic\b))" variant) 900
|
||||
:else 400))
|
||||
(re-seq #"(?i)(?:hairline|thin)" variant) 100
|
||||
(re-seq #"(?i)(?:extra\s*light|ultra\s*light)" variant) 200
|
||||
(re-seq #"(?i)(?:light)" variant) 300
|
||||
(re-seq #"(?i)(?:normal|regular)" variant) 400
|
||||
(re-seq #"(?i)(?:medium)" variant) 500
|
||||
(re-seq #"(?i)(?:semi\s*bold|demi\s*bold)" variant) 600
|
||||
(re-seq #"(?i)(?:extra\s*bold|ultra\s*bold)" variant) 800
|
||||
(re-seq #"(?i)(?:bold)" variant) 700
|
||||
(re-seq #"(?i)(?:extra\s*black|ultra\s*black)" variant) 950
|
||||
(re-seq #"(?i)(?:black|heavy|solid)" variant) 900
|
||||
:else 400))
|
||||
|
||||
(defn parse-font-style
|
||||
[variant]
|
||||
(if (or (re-seq #"(?i)(?:^|[-_\s])(italic)(?:[-_\s]|$)" variant)
|
||||
(re-seq #"(?i)italic$" variant))
|
||||
(if (re-seq #"(?i)(?:italic)" variant)
|
||||
"italic"
|
||||
"normal"))
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
["date-fns/locale/eu$default" :as dfn-eu]
|
||||
["date-fns/locale/fa-IR$default" :as dfn-fa-ir]
|
||||
["date-fns/locale/fr$default" :as dfn-fr]
|
||||
["date-fns/locale/fr-CA$default" :as dfn-fr-ca]
|
||||
["date-fns/locale/gl$default" :as dfn-gl]
|
||||
["date-fns/locale/he$default" :as dfn-he]
|
||||
["date-fns/locale/hr$default" :as dfn-hr]
|
||||
@@ -253,7 +252,6 @@
|
||||
:fa dfn-fa-ir
|
||||
:fa_ir dfn-fa-ir
|
||||
:fr dfn-fr
|
||||
:fr_ca dfn-fr-ca
|
||||
:he dfn-he
|
||||
:pt dfn-pt
|
||||
:pt_pt dfn-pt
|
||||
|
||||
@@ -143,15 +143,6 @@
|
||||
:gen/gen sg/text}
|
||||
token-name-validation-regex])
|
||||
|
||||
(def token-ref-validation-regex
|
||||
#"^\{[a-zA-Z0-9_-][a-zA-Z0-9$_-]*(\.[a-zA-Z0-9$_-]+)*\}$")
|
||||
|
||||
(def schema:token-ref
|
||||
"A token reference is a token name enclosed in {}."
|
||||
[:re {:title "TokenRef"
|
||||
:gen/gen sg/text}
|
||||
token-ref-validation-regex])
|
||||
|
||||
(def schema:token-type
|
||||
[::sm/one-of {:decode/json (fn [type]
|
||||
(if (string? type)
|
||||
|
||||
@@ -9,39 +9,6 @@
|
||||
[app.common.media :as media]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/deftest test-parse-font-weight
|
||||
(t/testing "matches weight tokens with proper boundaries"
|
||||
(t/is (= 700 (media/parse-font-weight "Roboto-Bold")))
|
||||
(t/is (= 700 (media/parse-font-weight "Roboto_Bold")))
|
||||
(t/is (= 700 (media/parse-font-weight "Roboto Bold")))
|
||||
(t/is (= 700 (media/parse-font-weight "Bold")))
|
||||
(t/is (= 800 (media/parse-font-weight "Roboto-ExtraBold")))
|
||||
(t/is (= 600 (media/parse-font-weight "OpenSans-SemiBold")))
|
||||
(t/is (= 300 (media/parse-font-weight "Lato-Light")))
|
||||
(t/is (= 100 (media/parse-font-weight "Roboto-Thin")))
|
||||
(t/is (= 200 (media/parse-font-weight "Roboto-ExtraLight")))
|
||||
(t/is (= 500 (media/parse-font-weight "Roboto-Medium")))
|
||||
(t/is (= 900 (media/parse-font-weight "Roboto-Black"))))
|
||||
|
||||
(t/testing "does not match weight tokens embedded in words"
|
||||
(t/is (= 400 (media/parse-font-weight "Boldini")))
|
||||
(t/is (= 400 (media/parse-font-weight "Lighthaus")))
|
||||
(t/is (= 400 (media/parse-font-weight "Blackwood")))
|
||||
(t/is (= 400 (media/parse-font-weight "Thinker")))
|
||||
(t/is (= 400 (media/parse-font-weight "Mediaeval")))))
|
||||
|
||||
(t/deftest test-parse-font-style
|
||||
(t/testing "matches italic with proper boundaries"
|
||||
(t/is (= "italic" (media/parse-font-style "Roboto-Italic")))
|
||||
(t/is (= "italic" (media/parse-font-style "Roboto_Italic")))
|
||||
(t/is (= "italic" (media/parse-font-style "Roboto Italic")))
|
||||
(t/is (= "italic" (media/parse-font-style "Italic")))
|
||||
(t/is (= "italic" (media/parse-font-style "Roboto-BoldItalic"))))
|
||||
|
||||
(t/testing "does not match italic embedded in words"
|
||||
(t/is (= "normal" (media/parse-font-style "Italica")))
|
||||
(t/is (= "normal" (media/parse-font-style "Roboto-Regular")))))
|
||||
|
||||
(t/deftest test-strip-image-extension
|
||||
(t/testing "removes extension from supported image files"
|
||||
(t/is (= (media/strip-image-extension "foo.png") "foo"))
|
||||
|
||||
@@ -100,14 +100,12 @@
|
||||
|
||||
(def browser-pool-factory
|
||||
(letfn [(create []
|
||||
(-> (p/let [opts #js {:args #js ["--allow-insecure-localhost" "--font-render-hinting=none"]}
|
||||
browser (.launch pw/chromium opts)
|
||||
id (swap! pool-browser-id inc)]
|
||||
(l/info :origin "factory" :action "create" :browser-id id)
|
||||
(unchecked-set browser "__id" id)
|
||||
browser)
|
||||
(p/catch (fn [cause]
|
||||
(l/error :hint "Cannot launch the headless browser" :cause cause)))))
|
||||
(p/let [opts #js {:args #js ["--allow-insecure-localhost" "--font-render-hinting=none"]}
|
||||
browser (.launch pw/chromium opts)
|
||||
id (swap! pool-browser-id inc)]
|
||||
(l/info :origin "factory" :action "create" :browser-id id)
|
||||
(unchecked-set browser "__id" id)
|
||||
browser))
|
||||
|
||||
(destroy [obj]
|
||||
(let [id (unchecked-get obj "__id")]
|
||||
|
||||
@@ -47,13 +47,12 @@
|
||||
|
||||
(s/def ::params
|
||||
(s/keys :req-un [::exports ::profile-id]
|
||||
:opt-un [::wait ::name ::skip-children ::force-multiple]))
|
||||
:opt-un [::wait ::name ::skip-children]))
|
||||
|
||||
(defn handler
|
||||
[{:keys [:request/auth-token] :as exchange} {:keys [exports force-multiple] :as params}]
|
||||
[{:keys [:request/auth-token] :as exchange} {:keys [exports] :as params}]
|
||||
(let [exports (prepare-exports exports auth-token)]
|
||||
(if (and (not force-multiple)
|
||||
(= 1 (count exports))
|
||||
(if (and (= 1 (count exports))
|
||||
(= 1 (count (-> exports first :objects))))
|
||||
(handle-single-export exchange (-> params
|
||||
(assoc :export (first exports))
|
||||
|
||||
@@ -404,6 +404,8 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
return content !== "";
|
||||
}, { timeout: 1000 });
|
||||
|
||||
await this.page.waitForTimeout(3000);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -417,7 +419,8 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
await this.viewport.click({ button: "right" });
|
||||
return this.page.getByText("Paste", { exact: true }).click();
|
||||
}
|
||||
return this.page.keyboard.press("ControlOrMeta+V");
|
||||
await this.page.keyboard.press("ControlOrMeta+V");
|
||||
await this.page.waitForTimeout(3000);
|
||||
}
|
||||
|
||||
async panOnViewportAt(x, y, width, height) {
|
||||
|
||||
@@ -97,7 +97,6 @@ test("Update an already created text shape by prepending text", async ({
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
await workspace.textEditor.startEditing();
|
||||
await workspace.textEditor.moveFromStart(0);
|
||||
await page.evaluate(() => new Promise((resolve) => globalThis.requestIdleCallback(resolve)));
|
||||
await page.keyboard.type("Dolor sit amet ");
|
||||
await workspace.textEditor.stopEditing();
|
||||
await workspace.waitForSelectedShapeName("Dolor sit amet Lorem ipsum");
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { WorkspacePage } from "../../pages/WorkspacePage";
|
||||
import { BaseWebSocketPage } from "../../pages/BaseWebSocketPage";
|
||||
import { WasmWorkspacePage } from "../../pages/WasmWorkspacePage";
|
||||
import {
|
||||
setupTokensFileRender,
|
||||
setupTypographyTokensFileRender,
|
||||
setupEmptyTokensFile,
|
||||
setupTokensFile,
|
||||
setupTypographyTokensFile,
|
||||
unfoldTokenTree,
|
||||
} from "./helpers";
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await WasmWorkspacePage.init(page);
|
||||
await WorkspacePage.init(page);
|
||||
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-tokens.json");
|
||||
});
|
||||
|
||||
test.describe("Tokens: Apply token", () => {
|
||||
test("User applies color token to a shape", async ({ page }) => {
|
||||
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFileRender(page);
|
||||
await setupTokensFile(page);
|
||||
|
||||
await page.getByRole("tab", { name: "Layers" }).click();
|
||||
|
||||
@@ -43,7 +44,7 @@ test.describe("Tokens: Apply token", () => {
|
||||
page,
|
||||
}) => {
|
||||
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFileRender(page);
|
||||
await setupTokensFile(page);
|
||||
|
||||
await page.getByRole("tab", { name: "Layers" }).click();
|
||||
|
||||
@@ -104,7 +105,7 @@ test.describe("Tokens: Apply token", () => {
|
||||
page,
|
||||
}) => {
|
||||
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFileRender(page);
|
||||
await setupTokensFile(page);
|
||||
|
||||
await page.getByRole("tab", { name: "Layers" }).click();
|
||||
|
||||
@@ -168,7 +169,7 @@ test.describe("Tokens: Apply token", () => {
|
||||
|
||||
test("User applies typography token to a text shape", async ({ page }) => {
|
||||
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTypographyTokensFileRender(page);
|
||||
await setupTypographyTokensFile(page);
|
||||
|
||||
await page.getByRole("tab", { name: "Layers" }).click();
|
||||
|
||||
@@ -202,7 +203,7 @@ test.describe("Tokens: Apply token", () => {
|
||||
tokensSidebar,
|
||||
workspacePage,
|
||||
tokenContextMenuForToken,
|
||||
} = await setupTokensFileRender(page, { flags: ["enable-token-shadow"] });
|
||||
} = await setupTokensFile(page, { flags: ["enable-token-shadow"] });
|
||||
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
|
||||
@@ -488,7 +489,7 @@ test.describe("Tokens: Apply token", () => {
|
||||
page,
|
||||
}) => {
|
||||
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFileRender(page);
|
||||
await setupTokensFile(page);
|
||||
|
||||
// Unfolds dimensions on token panel
|
||||
await page.getByRole("tab", { name: "Layers" }).click();
|
||||
@@ -539,7 +540,7 @@ test.describe("Tokens: Apply token", () => {
|
||||
page,
|
||||
}) => {
|
||||
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFileRender(page);
|
||||
await setupTokensFile(page);
|
||||
|
||||
// Unfolds dimensions on token panel
|
||||
await page.getByRole("tab", { name: "Layers" }).click();
|
||||
@@ -593,7 +594,7 @@ test.describe("Tokens: Apply token", () => {
|
||||
page,
|
||||
}) => {
|
||||
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFileRender(page);
|
||||
await setupTokensFile(page);
|
||||
|
||||
// Unfolds dimensions on token panel
|
||||
await page.getByRole("tab", { name: "Layers" }).click();
|
||||
@@ -647,7 +648,7 @@ test.describe("Tokens: Apply token", () => {
|
||||
page,
|
||||
}) => {
|
||||
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFileRender(page);
|
||||
await setupTokensFile(page);
|
||||
|
||||
// Unfolds dimensions on token panel
|
||||
await page.getByRole("tab", { name: "Layers" }).click();
|
||||
@@ -700,7 +701,7 @@ test.describe("Tokens: Apply token", () => {
|
||||
});
|
||||
|
||||
test("User applies stroke width token to a shape", async ({ page }) => {
|
||||
const workspace = new WasmWorkspacePage(page, {
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true,
|
||||
});
|
||||
// Set up
|
||||
@@ -760,7 +761,7 @@ test.describe("Tokens: Apply token", () => {
|
||||
});
|
||||
|
||||
test("User applies margin token to a shape", async ({ page }) => {
|
||||
const workspace = new WasmWorkspacePage(page, {
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true,
|
||||
});
|
||||
// Set up
|
||||
@@ -852,7 +853,7 @@ test.describe("Tokens: Detach token", () => {
|
||||
page,
|
||||
}) => {
|
||||
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFileRender(page);
|
||||
await setupTokensFile(page);
|
||||
|
||||
await page.getByRole("tab", { name: "Layers" }).click();
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { WasmWorkspacePage } from "../../pages/WasmWorkspacePage";
|
||||
import { WorkspacePage } from "../../pages/WorkspacePage";
|
||||
import { BaseWebSocketPage } from "../../pages/BaseWebSocketPage";
|
||||
import {
|
||||
setupEmptyTokensFileRender,
|
||||
setupTokensFileRender,
|
||||
setupTypographyTokensFileRender,
|
||||
setupEmptyTokensFile,
|
||||
setupTokensFile,
|
||||
setupTypographyTokensFile,
|
||||
testTokenCreationFlow,
|
||||
unfoldTokenTree,
|
||||
} from "./helpers";
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await WasmWorkspacePage.init(page);
|
||||
await WorkspacePage.init(page);
|
||||
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-tokens.json");
|
||||
});
|
||||
|
||||
@@ -158,7 +158,7 @@ test.describe("Tokens - creation", () => {
|
||||
const selfReferenceError = "Token has self reference";
|
||||
const missingReferenceError = "Missing token references";
|
||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } =
|
||||
await setupEmptyTokensFileRender(page);
|
||||
await setupEmptyTokensFile(page);
|
||||
|
||||
await tokensSidebar
|
||||
.getByRole("button", { name: "Add Token: Color" })
|
||||
@@ -320,7 +320,7 @@ test.describe("Tokens - creation", () => {
|
||||
const missingReferenceError = "Missing token references";
|
||||
|
||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
||||
await setupEmptyTokensFileRender(page);
|
||||
await setupEmptyTokensFile(page);
|
||||
|
||||
// Open modal
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
@@ -465,7 +465,7 @@ test.describe("Tokens - creation", () => {
|
||||
const missingReferenceError = "Missing token references";
|
||||
|
||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
||||
await setupEmptyTokensFileRender(page);
|
||||
await setupEmptyTokensFile(page);
|
||||
|
||||
// Open modal
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
@@ -601,7 +601,7 @@ test.describe("Tokens - creation", () => {
|
||||
const missingReferenceError = "Missing token references";
|
||||
|
||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
||||
await setupEmptyTokensFileRender(page);
|
||||
await setupEmptyTokensFile(page);
|
||||
|
||||
// Open modal
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
@@ -717,7 +717,7 @@ test.describe("Tokens - creation", () => {
|
||||
const missingReferenceError = "Missing token references";
|
||||
|
||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
||||
await setupEmptyTokensFileRender(page);
|
||||
await setupEmptyTokensFile(page);
|
||||
|
||||
// Open modal
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
@@ -831,7 +831,7 @@ test.describe("Tokens - creation", () => {
|
||||
const emptyNameError = "Name should be at least 1 character";
|
||||
|
||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
||||
await setupEmptyTokensFileRender(page, { flags: ["enable-token-shadow"] });
|
||||
await setupEmptyTokensFile(page, { flags: ["enable-token-shadow"] });
|
||||
|
||||
// Open modal
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
@@ -1012,7 +1012,7 @@ test.describe("Tokens - creation", () => {
|
||||
page,
|
||||
}) => {
|
||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } =
|
||||
await setupTypographyTokensFileRender(page);
|
||||
await setupTypographyTokensFile(page);
|
||||
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
await tokensTabPanel
|
||||
@@ -1047,7 +1047,7 @@ test.describe("Tokens - creation", () => {
|
||||
const emptyNameError = "Name should be at least 1 character";
|
||||
|
||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
||||
await setupEmptyTokensFileRender(page, { flags: ["enable-token-shadow"] });
|
||||
await setupEmptyTokensFile(page, { flags: ["enable-token-shadow"] });
|
||||
|
||||
// Open modal
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
@@ -1232,7 +1232,7 @@ test.describe("Tokens - creation", () => {
|
||||
test("User creates typography token", async ({ page }) => {
|
||||
const emptyNameError = "Name should be at least 1 character";
|
||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
||||
await setupEmptyTokensFileRender(page);
|
||||
await setupEmptyTokensFile(page);
|
||||
|
||||
// Open modal
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
@@ -1479,7 +1479,7 @@ test.describe("Tokens - creation", () => {
|
||||
|
||||
test("User adds typography token with reference", async ({ page }) => {
|
||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } =
|
||||
await setupTypographyTokensFileRender(page);
|
||||
await setupTypographyTokensFile(page);
|
||||
|
||||
const newTokenTitle = "NewReference";
|
||||
|
||||
@@ -1529,7 +1529,7 @@ test.describe("Tokens - creation", () => {
|
||||
|
||||
test("User creates grouped color token", async ({ page }) => {
|
||||
const { workspacePage, tokensUpdateCreateModal, tokensSidebar } =
|
||||
await setupEmptyTokensFileRender(page);
|
||||
await setupEmptyTokensFile(page);
|
||||
|
||||
await tokensSidebar
|
||||
.getByRole("button", { name: "Add Token: Color" })
|
||||
@@ -1562,7 +1562,7 @@ test.describe("Tokens - creation", () => {
|
||||
test("User cant create regular token with value missing", async ({
|
||||
page,
|
||||
}) => {
|
||||
const { tokensUpdateCreateModal } = await setupEmptyTokensFileRender(page);
|
||||
const { tokensUpdateCreateModal } = await setupEmptyTokensFile(page);
|
||||
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
await tokensTabPanel
|
||||
@@ -1589,7 +1589,7 @@ test.describe("Tokens - creation", () => {
|
||||
|
||||
test("User duplicate color token", async ({ page }) => {
|
||||
const { tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFileRender(page);
|
||||
await setupTokensFile(page);
|
||||
|
||||
await expect(tokensSidebar).toBeVisible();
|
||||
|
||||
@@ -1613,7 +1613,7 @@ test.describe("Tokens - creation", () => {
|
||||
|
||||
test("User creates grouped color token", async ({ page }) => {
|
||||
const { workspacePage, tokensUpdateCreateModal, tokensSidebar } =
|
||||
await setupEmptyTokensFileRender(page);
|
||||
await setupEmptyTokensFile(page);
|
||||
|
||||
await tokensSidebar.getByRole("button", { name: "Add Token: Color" }).click();
|
||||
|
||||
@@ -1642,7 +1642,7 @@ test("User creates grouped color token", async ({ page }) => {
|
||||
});
|
||||
|
||||
test("User cant create regular token with value missing", async ({ page }) => {
|
||||
const { tokensUpdateCreateModal } = await setupEmptyTokensFileRender(page);
|
||||
const { tokensUpdateCreateModal } = await setupEmptyTokensFile(page);
|
||||
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
await tokensTabPanel
|
||||
@@ -1669,7 +1669,7 @@ test("User cant create regular token with value missing", async ({ page }) => {
|
||||
|
||||
test("User duplicate color token", async ({ page }) => {
|
||||
const { tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFileRender(page);
|
||||
await setupTokensFile(page);
|
||||
|
||||
await expect(tokensSidebar).toBeVisible();
|
||||
|
||||
@@ -1695,7 +1695,7 @@ test.describe("Tokens tab - edition", () => {
|
||||
page,
|
||||
}) => {
|
||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } =
|
||||
await setupTypographyTokensFileRender(page);
|
||||
await setupTypographyTokensFile(page);
|
||||
|
||||
await tokensSidebar
|
||||
.getByRole("button")
|
||||
@@ -1791,7 +1791,7 @@ test.describe("Tokens tab - edition", () => {
|
||||
page,
|
||||
}) => {
|
||||
const { tokensUpdateCreateModal, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFileRender(page);
|
||||
await setupTokensFile(page);
|
||||
|
||||
await expect(tokensSidebar).toBeVisible();
|
||||
|
||||
@@ -1827,7 +1827,7 @@ test.describe("Tokens tab - edition", () => {
|
||||
page,
|
||||
}) => {
|
||||
const { workspacePage, tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
||||
await setupEmptyTokensFileRender(page);
|
||||
await setupEmptyTokensFile(page);
|
||||
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
await tokensTabPanel
|
||||
@@ -1882,7 +1882,7 @@ test.describe("Tokens tab - edition", () => {
|
||||
test.describe("Tokens tab - delete", () => {
|
||||
test("User delete color token", async ({ page }) => {
|
||||
const { tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFileRender(page);
|
||||
await setupTokensFile(page);
|
||||
|
||||
await expect(tokensSidebar).toBeVisible();
|
||||
|
||||
@@ -1902,7 +1902,7 @@ test.describe("Tokens tab - delete", () => {
|
||||
});
|
||||
|
||||
test("User removes node and all child tokens", async ({ page }) => {
|
||||
const { tokensSidebar } = await setupTokensFileRender(page);
|
||||
const { tokensSidebar } = await setupTokensFile(page);
|
||||
|
||||
await expect(tokensSidebar).toBeVisible();
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { WasmWorkspacePage } from "../../pages/WasmWorkspacePage";
|
||||
import { WorkspacePage } from "../../pages/WorkspacePage";
|
||||
import { BaseWebSocketPage } from "../../pages/BaseWebSocketPage";
|
||||
import { setupEmptyTokensFileRender } from "./helpers";
|
||||
import { setupEmptyTokensFile } from "./helpers";
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await WasmWorkspacePage.init(page);
|
||||
await WorkspacePage.init(page);
|
||||
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-tokens.json");
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ test.describe("Tokens tab - common tests", () => {
|
||||
test("Clicking tokens tab button opens tokens sidebar tab", async ({
|
||||
page,
|
||||
}) => {
|
||||
await setupEmptyTokensFileRender(page);
|
||||
await setupEmptyTokensFile(page);
|
||||
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { WorkspacePage } from "../../pages/WorkspacePage";
|
||||
import { WasmWorkspacePage } from "../../pages/WasmWorkspacePage";
|
||||
|
||||
const setupEmptyTokensFile = async (page, options = {}) => {
|
||||
const { flags = [] } = options;
|
||||
@@ -41,45 +40,6 @@ const setupEmptyTokensFile = async (page, options = {}) => {
|
||||
};
|
||||
};
|
||||
|
||||
const setupEmptyTokensFileRender = async (page, options = {}) => {
|
||||
const { flags = [] } = options;
|
||||
|
||||
const workspacePage = new WasmWorkspacePage(page);
|
||||
if (flags.length > 0) {
|
||||
await workspacePage.mockConfigFlags(flags);
|
||||
}
|
||||
|
||||
await workspacePage.setupEmptyFile();
|
||||
await workspacePage.mockRPC(
|
||||
"get-team?id=*",
|
||||
"workspace/get-team-tokens.json",
|
||||
);
|
||||
|
||||
await workspacePage.mockRPC(
|
||||
"update-file?id=*",
|
||||
"workspace/update-file-create-rect.json",
|
||||
);
|
||||
|
||||
await workspacePage.goToWorkspace({
|
||||
fileId: "c7ce0794-0992-8105-8004-38f280443849",
|
||||
pageId: "66697432-c33d-8055-8006-2c62cc084cad",
|
||||
});
|
||||
|
||||
const tokensTabButton = page.getByRole("tab", { name: "Tokens" });
|
||||
await tokensTabButton.click();
|
||||
|
||||
return {
|
||||
workspacePage,
|
||||
tokenThemeUpdateCreateModal: workspacePage.tokenThemeUpdateCreateModal,
|
||||
tokensUpdateCreateModal: workspacePage.tokensUpdateCreateModal,
|
||||
tokenThemesSetsSidebar: workspacePage.tokenThemesSetsSidebar,
|
||||
tokenSetItems: workspacePage.tokenSetItems,
|
||||
tokensSidebar: workspacePage.tokensSidebar,
|
||||
tokenSetGroupItems: workspacePage.tokenSetGroupItems,
|
||||
tokenContextMenuForSet: workspacePage.tokenContextMenuForSet,
|
||||
};
|
||||
};
|
||||
|
||||
const setupTokensFile = async (page, options = {}) => {
|
||||
const {
|
||||
file = "workspace/get-file-tokens.json",
|
||||
@@ -125,51 +85,6 @@ const setupTokensFile = async (page, options = {}) => {
|
||||
};
|
||||
};
|
||||
|
||||
const setupTokensFileRender = async (page, options = {}) => {
|
||||
const {
|
||||
file = "workspace/get-file-tokens.json",
|
||||
fileFragment = "workspace/get-file-fragment-tokens.json",
|
||||
flags = ["enable-feature-token-input"],
|
||||
} = options;
|
||||
|
||||
const workspacePage = new WasmWorkspacePage(page);
|
||||
if (flags.length > 0) {
|
||||
await workspacePage.mockConfigFlags(flags);
|
||||
}
|
||||
|
||||
await workspacePage.setupEmptyFile();
|
||||
await workspacePage.mockRPC(
|
||||
"get-team?id=*",
|
||||
"workspace/get-team-tokens.json",
|
||||
);
|
||||
await workspacePage.mockRPC(/get\-file\?/, file);
|
||||
await workspacePage.mockRPC(/get\-file\-fragment\?/, fileFragment);
|
||||
await workspacePage.mockRPC(
|
||||
"update-file?id=*",
|
||||
"workspace/update-file-create-rect.json",
|
||||
);
|
||||
|
||||
await workspacePage.goToWorkspace({
|
||||
fileId: "c7ce0794-0992-8105-8004-38f280443849",
|
||||
pageId: "66697432-c33d-8055-8006-2c62cc084cad",
|
||||
});
|
||||
|
||||
const tokensTabButton = page.getByRole("tab", { name: "Tokens" });
|
||||
await tokensTabButton.click();
|
||||
|
||||
return {
|
||||
workspacePage,
|
||||
tokensUpdateCreateModal: workspacePage.tokensUpdateCreateModal,
|
||||
tokenThemeUpdateCreateModal: workspacePage.tokenThemeUpdateCreateModal,
|
||||
tokenThemesSetsSidebar: workspacePage.tokenThemesSetsSidebar,
|
||||
tokenSetItems: workspacePage.tokenSetItems,
|
||||
tokenSetGroupItems: workspacePage.tokenSetGroupItems,
|
||||
tokensSidebar: workspacePage.tokensSidebar,
|
||||
tokenContextMenuForToken: workspacePage.tokenContextMenuForToken,
|
||||
tokenContextMenuForSet: workspacePage.tokenContextMenuForSet,
|
||||
};
|
||||
};
|
||||
|
||||
const setupTypographyTokensFile = async (page, options = {}) => {
|
||||
return setupTokensFile(page, {
|
||||
file: "workspace/get-file-typography-tokens.json",
|
||||
@@ -178,14 +93,6 @@ const setupTypographyTokensFile = async (page, options = {}) => {
|
||||
});
|
||||
};
|
||||
|
||||
const setupTypographyTokensFileRender = async (page, options = {}) => {
|
||||
return setupTokensFileRender(page, {
|
||||
file: "workspace/get-file-typography-tokens.json",
|
||||
fileFragment: "workspace/get-file-fragment-typography-tokens.json",
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
const testTokenCreationFlow = async (
|
||||
page,
|
||||
{
|
||||
@@ -207,7 +114,7 @@ const testTokenCreationFlow = async (
|
||||
const missingReferenceError = "Missing token references";
|
||||
|
||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
||||
await setupEmptyTokensFileRender(page);
|
||||
await setupEmptyTokensFile(page);
|
||||
|
||||
// Open modal
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
@@ -352,11 +259,8 @@ const unfoldTokenTree = async (tokensTabPanel, type, tokenName) => {
|
||||
|
||||
export {
|
||||
setupEmptyTokensFile,
|
||||
setupEmptyTokensFileRender,
|
||||
setupTokensFile,
|
||||
setupTokensFileRender,
|
||||
setupTypographyTokensFile,
|
||||
setupTypographyTokensFileRender,
|
||||
testTokenCreationFlow,
|
||||
unfoldTokenTree,
|
||||
};
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { WorkspacePage } from "../../pages/WorkspacePage";
|
||||
import { WasmWorkspacePage } from "../../pages/WasmWorkspacePage";
|
||||
import { BaseWebSocketPage } from "../../pages/BaseWebSocketPage";
|
||||
import {
|
||||
setupTokensFileRender,
|
||||
setupTypographyTokensFileRender,
|
||||
setupEmptyTokensFile,
|
||||
setupTokensFile,
|
||||
setupTypographyTokensFile,
|
||||
} from "./helpers";
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await WorkspacePage.init(page);
|
||||
await WasmWorkspacePage.mockConfigFlags(page, [
|
||||
"enable-feature-design-tokens-v1",
|
||||
]);
|
||||
await WasmWorkspacePage.mockRPC(page, "get-teams", "get-teams-tokens.json");
|
||||
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-tokens.json");
|
||||
});
|
||||
|
||||
const createToken = async (page, type, name, textFieldName, value) => {
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
|
||||
const { tokensUpdateCreateModal } = await setupTokensFileRender(page, {
|
||||
const { tokensUpdateCreateModal } = await setupTokensFile(page, {
|
||||
flags: ["enable-token-shadow"],
|
||||
});
|
||||
|
||||
@@ -44,7 +42,7 @@ const createToken = async (page, type, name, textFieldName, value) => {
|
||||
|
||||
const renameToken = async (page, oldName, newName) => {
|
||||
const { tokensUpdateCreateModal, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFileRender(page, { flags: ["enable-token-shadow"] });
|
||||
await setupTokensFile(page, { flags: ["enable-token-shadow"] });
|
||||
|
||||
const baseToken = tokensSidebar.getByRole("button", {
|
||||
name: oldName,
|
||||
@@ -66,7 +64,7 @@ const renameToken = async (page, oldName, newName) => {
|
||||
const createCompositeDerivedToken = async (page, type, name, reference) => {
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
|
||||
const { tokensUpdateCreateModal } = await setupTokensFileRender(page, {
|
||||
const { tokensUpdateCreateModal } = await setupTokensFile(page, {
|
||||
flags: ["enable-token-shadow"],
|
||||
});
|
||||
|
||||
@@ -100,7 +98,7 @@ test.describe("Remapping Tokens", () => {
|
||||
test("User renames box shadow token with alias references", async ({
|
||||
page,
|
||||
}) => {
|
||||
const { tokensSidebar } = await setupTokensFileRender(page, {
|
||||
const { tokensSidebar } = await setupTokensFile(page, {
|
||||
flags: ["enable-token-shadow"],
|
||||
});
|
||||
|
||||
@@ -146,7 +144,7 @@ test.describe("Remapping Tokens", () => {
|
||||
tokensSidebar,
|
||||
tokenContextMenuForToken,
|
||||
workspacePage,
|
||||
} = await setupTokensFileRender(page, { flags: ["enable-token-shadow"] });
|
||||
} = await setupTokensFile(page, { flags: ["enable-token-shadow"] });
|
||||
|
||||
// Create base shadow token
|
||||
await createToken(page, "Shadow", "primary-shadow", "Color", "#000000");
|
||||
@@ -251,7 +249,7 @@ test.describe("Remapping Tokens", () => {
|
||||
tokensUpdateCreateModal,
|
||||
tokensSidebar,
|
||||
tokenContextMenuForToken,
|
||||
} = await setupTypographyTokensFileRender(page);
|
||||
} = await setupTypographyTokensFile(page);
|
||||
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
|
||||
@@ -295,7 +293,7 @@ test.describe("Remapping Tokens", () => {
|
||||
tokensSidebar,
|
||||
tokenContextMenuForToken,
|
||||
workspacePage,
|
||||
} = await setupTypographyTokensFileRender(page);
|
||||
} = await setupTypographyTokensFile(page);
|
||||
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
|
||||
@@ -403,7 +401,7 @@ test.describe("Remapping Tokens", () => {
|
||||
test("User renames border radius token with alias references", async ({
|
||||
page,
|
||||
}) => {
|
||||
const { tokensSidebar } = await setupTokensFileRender(page);
|
||||
const { tokensSidebar } = await setupTokensFile(page);
|
||||
|
||||
// Create base border radius token
|
||||
await createToken(page, "Border Radius", "base-radius", "Value", "4");
|
||||
@@ -445,7 +443,7 @@ test.describe("Remapping Tokens", () => {
|
||||
tokensUpdateCreateModal,
|
||||
tokensSidebar,
|
||||
tokenContextMenuForToken,
|
||||
} = await setupTokensFileRender(page);
|
||||
} = await setupTokensFile(page);
|
||||
|
||||
// Create base border radius token
|
||||
await createToken(page, "Border Radius", "radius-sm", "Value", "4");
|
||||
@@ -514,7 +512,7 @@ test.describe("Remapping Tokens", () => {
|
||||
|
||||
test.describe("Cancel remap", () => {
|
||||
test("Only rename - breaks reference", async ({ page }) => {
|
||||
const { tokensSidebar } = await setupTokensFileRender(page, {
|
||||
const { tokensSidebar } = await setupTokensFile(page, {
|
||||
flags: ["enable-token-shadow"],
|
||||
});
|
||||
|
||||
@@ -553,7 +551,7 @@ test.describe("Remapping Tokens", () => {
|
||||
});
|
||||
|
||||
test("Cancel process - no changes applied", async ({ page }) => {
|
||||
const { tokensSidebar } = await setupTokensFileRender(page, {
|
||||
const { tokensSidebar } = await setupTokensFile(page, {
|
||||
flags: ["enable-token-shadow"],
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { BaseWebSocketPage } from "../../pages/BaseWebSocketPage";
|
||||
import { WasmWorkspacePage } from "../../pages/WasmWorkspacePage";
|
||||
import { setupEmptyTokensFileRender, setupTokensFileRender } from "./helpers";
|
||||
import { WorkspacePage } from "../../pages/WorkspacePage";
|
||||
import { setupEmptyTokensFile, setupTokensFile } from "./helpers";
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await WasmWorkspacePage.init(page);
|
||||
await WorkspacePage.init(page);
|
||||
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-tokens.json");
|
||||
});
|
||||
|
||||
@@ -42,7 +42,7 @@ test.describe("Tokens: Sets Tab", () => {
|
||||
page,
|
||||
}) => {
|
||||
const { tokenThemesSetsSidebar, tokenContextMenuForSet } =
|
||||
await setupEmptyTokensFileRender(page);
|
||||
await setupEmptyTokensFile(page);
|
||||
|
||||
const tokensTabButton = tokenThemesSetsSidebar
|
||||
.getByRole("button", { name: "Add set" })
|
||||
@@ -139,7 +139,7 @@ test.describe("Tokens: Sets Tab", () => {
|
||||
page,
|
||||
}) => {
|
||||
const { tokenThemesSetsSidebar, tokenContextMenuForSet } =
|
||||
await setupEmptyTokensFileRender(page);
|
||||
await setupEmptyTokensFile(page);
|
||||
|
||||
const tokensTabButton = tokenThemesSetsSidebar
|
||||
.getByRole("button", { name: "Add set" })
|
||||
@@ -176,7 +176,7 @@ test.describe("Tokens: Sets Tab", () => {
|
||||
|
||||
test("Fold/Unfold set", async ({ page }) => {
|
||||
const { tokenThemesSetsSidebar, tokenSetGroupItems } =
|
||||
await setupTokensFileRender(page);
|
||||
await setupTokensFile(page);
|
||||
|
||||
await expect(tokenThemesSetsSidebar).toBeVisible();
|
||||
|
||||
@@ -202,7 +202,7 @@ test.describe("Tokens: Sets Tab", () => {
|
||||
|
||||
test("Change current theme", async ({ page }) => {
|
||||
const { tokenThemesSetsSidebar, tokenSetItems } =
|
||||
await setupTokensFileRender(page);
|
||||
await setupTokensFile(page);
|
||||
|
||||
await expect(tokenSetItems.nth(1)).toHaveAttribute("aria-checked", "true");
|
||||
await expect(tokenSetItems.nth(2)).toHaveAttribute("aria-checked", "false");
|
||||
@@ -219,7 +219,7 @@ test.describe("Tokens: Sets Tab", () => {
|
||||
|
||||
test("Display active set and verify if is enabled", async ({ page }) => {
|
||||
const { tokenThemesSetsSidebar, tokensSidebar, tokenSetItems } =
|
||||
await setupTokensFileRender(page);
|
||||
await setupTokensFile(page);
|
||||
|
||||
// Create set
|
||||
await tokenThemesSetsSidebar
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { WasmWorkspacePage } from "../../pages/WasmWorkspacePage";
|
||||
import { WorkspacePage } from "../../pages/WorkspacePage";
|
||||
import { BaseWebSocketPage } from "../../pages/BaseWebSocketPage";
|
||||
import { setupEmptyTokensFileRender, setupTokensFileRender } from "./helpers";
|
||||
import { setupEmptyTokensFile, setupTokensFile } from "./helpers";
|
||||
|
||||
// THEMES HELPERS
|
||||
|
||||
@@ -23,17 +23,14 @@ const checkInputFieldWithoutError = async (inputLocator) => {
|
||||
};
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await WasmWorkspacePage.init(page);
|
||||
await WasmWorkspacePage.mockConfigFlags(page, [
|
||||
"enable-feature-design-tokens-v1",
|
||||
]);
|
||||
await WorkspacePage.init(page);
|
||||
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-tokens.json");
|
||||
});
|
||||
|
||||
test.describe("Tokens Themes", () => {
|
||||
test("User edits theme and activates it in the sidebar", async ({ page }) => {
|
||||
const { tokenThemesSetsSidebar, tokenThemeUpdateCreateModal } =
|
||||
await setupTokensFileRender(page);
|
||||
await setupTokensFile(page);
|
||||
|
||||
await expect(tokenThemesSetsSidebar).toBeVisible();
|
||||
|
||||
@@ -120,7 +117,7 @@ test.describe("Tokens Themes", () => {
|
||||
test.describe("Tokens: Themes modal", () => {
|
||||
test("Delete theme", async ({ page }) => {
|
||||
const { tokenThemeUpdateCreateModal, workspacePage } =
|
||||
await setupTokensFileRender(page);
|
||||
await setupTokensFile(page);
|
||||
|
||||
workspacePage.openTokenThemesModal();
|
||||
|
||||
@@ -140,7 +137,7 @@ test.describe("Tokens: Themes modal", () => {
|
||||
|
||||
test("Add new theme in empty file", async ({ page }) => {
|
||||
const { tokenThemesSetsSidebar, tokenThemeUpdateCreateModal } =
|
||||
await setupEmptyTokensFileRender(page);
|
||||
await setupEmptyTokensFile(page);
|
||||
|
||||
await tokenThemesSetsSidebar
|
||||
.getByRole("button", { name: "Create one." })
|
||||
@@ -173,7 +170,7 @@ test.describe("Tokens: Themes modal", () => {
|
||||
|
||||
test("Add new theme", async ({ page }) => {
|
||||
const { tokenThemeUpdateCreateModal, workspacePage } =
|
||||
await setupTokensFileRender(page);
|
||||
await setupTokensFile(page);
|
||||
|
||||
workspacePage.openTokenThemesModal();
|
||||
|
||||
@@ -213,7 +210,7 @@ test.describe("Tokens: Themes modal", () => {
|
||||
|
||||
test("Edit theme", async ({ page }) => {
|
||||
const { tokenThemeUpdateCreateModal, workspacePage } =
|
||||
await setupTokensFileRender(page);
|
||||
await setupTokensFile(page);
|
||||
|
||||
workspacePage.openTokenThemesModal();
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { WasmWorkspacePage } from "../../pages/WasmWorkspacePage";
|
||||
import { WorkspacePage } from "../../pages/WorkspacePage";
|
||||
import { BaseWebSocketPage } from "../../pages/BaseWebSocketPage";
|
||||
import { setupTokensFileRender, unfoldTokenTree } from "./helpers";
|
||||
import { setupTokensFile, unfoldTokenTree } from "./helpers";
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await WasmWorkspacePage.init(page);
|
||||
await WorkspacePage.init(page);
|
||||
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-tokens.json");
|
||||
});
|
||||
|
||||
test.describe("Tokens - node tree", () => {
|
||||
test("User fold/unfold color tokens", async ({ page }) => {
|
||||
const { tokensSidebar } = await setupTokensFileRender(page);
|
||||
const { tokensSidebar } = await setupTokensFile(page);
|
||||
|
||||
await expect(tokensSidebar).toBeVisible();
|
||||
|
||||
|
||||
@@ -383,24 +383,26 @@ test("User cut paste a component with path inside a variant", async ({
|
||||
|
||||
const variant = await findVariant(workspacePage, 0);
|
||||
|
||||
//Create a component
|
||||
// Create a component
|
||||
await workspacePage.ellipseShapeButton.click();
|
||||
await workspacePage.clickWithDragViewportAt(500, 500, 20, 20);
|
||||
await workspacePage.clickLeafLayer("Ellipse");
|
||||
await workspacePage.page.keyboard.press("ControlOrMeta+k");
|
||||
await workspacePage.page.waitForTimeout(3000);
|
||||
|
||||
//Rename the component
|
||||
// Rename the component
|
||||
await workspacePage.layers.getByText("Ellipse").dblclick();
|
||||
await workspacePage.page
|
||||
.getByTestId("layer-item")
|
||||
.getByRole("textbox")
|
||||
.pressSequentially("button / hover");
|
||||
await workspacePage.page.keyboard.press("Enter");
|
||||
await workspacePage.page.waitForTimeout(3000);
|
||||
|
||||
//Cut the component
|
||||
// Cut the component
|
||||
await workspacePage.cut("keyboard");
|
||||
|
||||
//Paste the component inside the variant
|
||||
// Paste the component inside the variant
|
||||
await variant.container.click();
|
||||
await workspacePage.paste("keyboard");
|
||||
|
||||
@@ -427,6 +429,7 @@ test("User drag and drop a component with path inside a variant", async ({
|
||||
await workspacePage.clickWithDragViewportAt(500, 500, 20, 20);
|
||||
await workspacePage.clickLeafLayer("Ellipse");
|
||||
await workspacePage.page.keyboard.press("ControlOrMeta+k");
|
||||
await workspacePage.page.waitForTimeout(3000);
|
||||
|
||||
//Rename the component
|
||||
await workspacePage.layers.getByText("Ellipse").dblclick();
|
||||
|
||||
@@ -288,7 +288,6 @@ export async function compileTranslations() {
|
||||
"es",
|
||||
"fa",
|
||||
"fr",
|
||||
"fr_CA",
|
||||
"he",
|
||||
"sr",
|
||||
"nb_NO",
|
||||
|
||||
@@ -119,10 +119,6 @@
|
||||
(normalize-uri (or (obj/get global "penpotPublicURI")
|
||||
(obj/get location "origin"))))
|
||||
|
||||
(def mcp-ws-uri
|
||||
(or (some-> (obj/get global "penpotMcpServerURI") u/uri)
|
||||
(u/join public-uri "mcp/ws")))
|
||||
|
||||
(def rasterizer-uri
|
||||
(or (some-> (obj/get global "penpotRasterizerURI") normalize-uri)
|
||||
public-uri))
|
||||
@@ -151,9 +147,6 @@
|
||||
(let [f (obj/get global "initializeExternalConfigInfo")]
|
||||
(when (fn? f) (f))))
|
||||
|
||||
(def mcp-server-url (-> public-uri u/ensure-path-slash (u/join "mcp/stream") str))
|
||||
(def mcp-help-center-uri "https://help.penpot.app/technical-guide/")
|
||||
|
||||
;; --- Helper Functions
|
||||
|
||||
(defn ^boolean check-browser? [candidate]
|
||||
|
||||
@@ -195,7 +195,7 @@
|
||||
params {:exports exports
|
||||
:cmd cmd
|
||||
:profile-id profile-id
|
||||
:force-multiple true}
|
||||
:wait false}
|
||||
|
||||
progress-stream
|
||||
(->> (ws/get-rcv-stream ws-conn)
|
||||
|
||||
@@ -99,65 +99,46 @@
|
||||
map with temporal ID's associated to each font entry."
|
||||
[blobs team-id]
|
||||
(letfn [(prepare [{:keys [font type name data] :as params}]
|
||||
(if font
|
||||
;; Font was parsed with opentype.js (ttf, otf, woff)
|
||||
(let [family (or (.getEnglishName ^js font "preferredFamily")
|
||||
(.getEnglishName ^js font "fontFamily"))
|
||||
variant (or (.getEnglishName ^js font "preferredSubfamily")
|
||||
(.getEnglishName ^js font "fontSubfamily"))
|
||||
(let [family (or (.getEnglishName ^js font "preferredFamily")
|
||||
(.getEnglishName ^js font "fontFamily"))
|
||||
variant (or (.getEnglishName ^js font "preferredSubfamily")
|
||||
(.getEnglishName ^js font "fontSubfamily"))
|
||||
|
||||
;; Vertical metrics determine the baseline in a text and the space between lines of
|
||||
;; text. For historical reasons, there are three pairs of ascender/descender
|
||||
;; values, known as hhea, OS/2 and uSWin metrics. Depending on the font, operating
|
||||
;; system and application a different set will be used to render text on the
|
||||
;; screen. On Mac, Safari and Chrome use the hhea values to render text. Firefox
|
||||
;; will respect the useTypoMetrics setting and will use the OS/2 if it is set. If
|
||||
;; the useTypoMetrics is not set, Firefox will also use metrics from the hhea
|
||||
;; table. On Windows, all browsers use the usWin metrics, but respect the
|
||||
;; useTypoMetrics setting and if set will use the OS/2 values.
|
||||
;; Vertical metrics determine the baseline in a text and the space between lines of
|
||||
;; text. For historical reasons, there are three pairs of ascender/descender
|
||||
;; values, known as hhea, OS/2 and uSWin metrics. Depending on the font, operating
|
||||
;; system and application a different set will be used to render text on the
|
||||
;; screen. On Mac, Safari and Chrome use the hhea values to render text. Firefox
|
||||
;; will respect the useTypoMetrics setting and will use the OS/2 if it is set. If
|
||||
;; the useTypoMetrics is not set, Firefox will also use metrics from the hhea
|
||||
;; table. On Windows, all browsers use the usWin metrics, but respect the
|
||||
;; useTypoMetrics setting and if set will use the OS/2 values.
|
||||
|
||||
hhea-ascender (abs (-> ^js font .-tables .-hhea .-ascender))
|
||||
hhea-descender (abs (-> ^js font .-tables .-hhea .-descender))
|
||||
hhea-ascender (abs (-> ^js font .-tables .-hhea .-ascender))
|
||||
hhea-descender (abs (-> ^js font .-tables .-hhea .-descender))
|
||||
|
||||
win-ascent (abs (-> ^js font .-tables .-os2 .-usWinAscent))
|
||||
win-descent (abs (-> ^js font .-tables .-os2 .-usWinDescent))
|
||||
win-ascent (abs (-> ^js font .-tables .-os2 .-usWinAscent))
|
||||
win-descent (abs (-> ^js font .-tables .-os2 .-usWinDescent))
|
||||
|
||||
os2-ascent (abs (-> ^js font .-tables .-os2 .-sTypoAscender))
|
||||
os2-descent (abs (-> ^js font .-tables .-os2 .-sTypoDescender))
|
||||
os2-ascent (abs (-> ^js font .-tables .-os2 .-sTypoAscender))
|
||||
os2-descent (abs (-> ^js font .-tables .-os2 .-sTypoDescender))
|
||||
|
||||
;; useTypoMetrics can be read from the 7th bit
|
||||
f-selection (-> ^js font .-tables .-os2 .-fsSelection (bit-test 7))
|
||||
;; useTypoMetrics can be read from the 7th bit
|
||||
f-selection (-> ^js font .-tables .-os2 .-fsSelection (bit-test 7))
|
||||
|
||||
height-warning? (or (not= hhea-ascender win-ascent)
|
||||
(not= hhea-descender win-descent)
|
||||
(and f-selection (or
|
||||
(not= hhea-ascender os2-ascent)
|
||||
(not= hhea-descender os2-descent))))
|
||||
data (js/Uint8Array. data)]
|
||||
{:content {:data (chunk-array data default-chunk-size)
|
||||
:name name
|
||||
:type type}
|
||||
:font-family (or family "")
|
||||
:font-weight (cm/parse-font-weight variant)
|
||||
:font-style (cm/parse-font-style variant)
|
||||
:height-warning? height-warning?})
|
||||
;; Font could not be parsed (woff2), extract metadata from filename
|
||||
(let [base-name (str/replace name #"\.[^.]+$" "")
|
||||
;; Strip known weight/style tokens and separators to derive family name
|
||||
;; Use word boundaries to avoid matching substrings (e.g. "Boldini" should not match "bold")
|
||||
raw-family-name (-> base-name
|
||||
(str/replace #"(?i)(^|[-_\s])(extra\s*black|ultra\s*black|extra\s*bold|ultra\s*bold|semi\s*bold|demi\s*bold|extra\s*light|ultra\s*light|hairline|thin|light|normal|regular|medium|bold|black|heavy|solid|italic)([-_\s]|$)" "$1$3")
|
||||
(str/replace #"[-_\s]+" " ")
|
||||
(str/trim))
|
||||
family-name (if (str/blank? raw-family-name) base-name raw-family-name)
|
||||
data (js/Uint8Array. data)]
|
||||
{:content {:data (chunk-array data default-chunk-size)
|
||||
:name name
|
||||
:type type}
|
||||
:font-family family-name
|
||||
:font-weight (cm/parse-font-weight base-name)
|
||||
:font-style (cm/parse-font-style base-name)
|
||||
:height-warning? false})))
|
||||
height-warning? (or (not= hhea-ascender win-ascent)
|
||||
(not= hhea-descender win-descent)
|
||||
(and f-selection (or
|
||||
(not= hhea-ascender os2-ascent)
|
||||
(not= hhea-descender os2-descent))))
|
||||
data (js/Uint8Array. data)]
|
||||
{:content {:data (chunk-array data default-chunk-size)
|
||||
:name name
|
||||
:type type}
|
||||
:font-family (or family "")
|
||||
:font-weight (cm/parse-font-weight variant)
|
||||
:font-style (cm/parse-font-style variant)
|
||||
:height-warning? height-warning?}))
|
||||
|
||||
(join [res {:keys [content] :as font}]
|
||||
(let [key-fn (juxt :font-family :font-weight :font-style)
|
||||
@@ -185,18 +166,14 @@
|
||||
(case sg
|
||||
"117 124 124 117" "font/otf"
|
||||
"0 1 0 0" "font/ttf"
|
||||
"167 117 106 106" "font/woff"
|
||||
"167 117 106 62" "font/woff2")))
|
||||
"167 117 106 106" "font/woff")))
|
||||
|
||||
(parse-font [{:keys [data type name] :as params}]
|
||||
(if (= type "font/woff2")
|
||||
;; woff2 cannot be parsed by opentype.js, extract metadata from filename
|
||||
(assoc params :font nil)
|
||||
(try
|
||||
(assoc params :font (ot/parse data))
|
||||
(catch :default _e
|
||||
(log/warn :msg (str/fmt "skipping file %s, unsupported format" name))
|
||||
nil))))
|
||||
(parse-font [{:keys [data] :as params}]
|
||||
(try
|
||||
(assoc params :font (ot/parse data))
|
||||
(catch :default _e
|
||||
(log/warn :msg (str/fmt "skipping file %s, unsupported format" (:name params)))
|
||||
nil)))
|
||||
|
||||
(read-blob [blob]
|
||||
(->> (wa/read-file-as-array-buffer blob)
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
(ns app.main.data.nitrate
|
||||
(:require
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.router :as rt]
|
||||
[app.main.store :as st]
|
||||
[app.util.dom :as dom]
|
||||
[beicon.v2.core :as rx]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(defn show-nitrate-popup
|
||||
[popup-type]
|
||||
(ptk/reify ::show-nitrate-popup
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (rp/cmd! ::get-nitrate-connectivity {})
|
||||
(rx/map (fn [connectivity]
|
||||
(modal/show popup-type (or connectivity {}))))))))
|
||||
|
||||
(defn go-to-nitrate-cc
|
||||
[]
|
||||
(st/emit! (dom/open-new-window "/control-center/")))
|
||||
|
||||
(defn go-to-nitrate-billing
|
||||
[]
|
||||
(st/emit! (rt/nav-raw :href "/control-center/licenses/billing")))
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.store :as st]
|
||||
[app.plugins.flags :as pflag]
|
||||
[app.plugins.register :as preg]
|
||||
[app.util.globals :as ug]
|
||||
[app.util.http :as http]
|
||||
@@ -45,6 +44,20 @@
|
||||
(update [_ state]
|
||||
(update-in state [:workspace-local :open-plugins] (fnil conj #{}) id))))
|
||||
|
||||
(defn reset-plugin-flags
|
||||
[id]
|
||||
(ptk/reify ::reset-plugin-flags
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:workspace-local :plugin-flags] assoc id {}))))
|
||||
|
||||
(defn set-plugin-flag
|
||||
[id key value]
|
||||
(ptk/reify ::set-plugin-flag
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:workspace-local :plugin-flags id] assoc key value))))
|
||||
|
||||
(defn remove-current-plugin
|
||||
[id]
|
||||
(ptk/reify ::remove-current-plugin
|
||||
@@ -52,26 +65,11 @@
|
||||
(update [_ state]
|
||||
(update-in state [:workspace-local :open-plugins] (fnil disj #{}) id))))
|
||||
|
||||
(defn start-plugin!
|
||||
[{:keys [plugin-id name version description host code permissions allow-background]} ^js extensions]
|
||||
(.ɵloadPlugin
|
||||
^js ug/global
|
||||
#js {:pluginId plugin-id
|
||||
:name name
|
||||
:version version
|
||||
:description description
|
||||
:host host
|
||||
:code code
|
||||
:allowBackground (boolean allow-background)
|
||||
:permissions (apply array permissions)}
|
||||
nil
|
||||
extensions))
|
||||
|
||||
(defn- load-plugin!
|
||||
[{:keys [plugin-id name description host code icon permissions] :as params}]
|
||||
[{:keys [plugin-id name description host code icon permissions]}]
|
||||
(try
|
||||
(st/emit! (pflag/clear plugin-id)
|
||||
(save-current-plugin plugin-id))
|
||||
(st/emit! (save-current-plugin plugin-id)
|
||||
(reset-plugin-flags plugin-id))
|
||||
|
||||
(.ɵloadPlugin
|
||||
^js ug/global
|
||||
|
||||
@@ -498,3 +498,4 @@
|
||||
(->> (rp/cmd! :delete-access-token params)
|
||||
(rx/tap on-success)
|
||||
(rx/catch on-error))))))
|
||||
|
||||
|
||||
@@ -69,10 +69,6 @@
|
||||
(and (number-with-unit-symbol? v)
|
||||
(= (.-unit v) "rem")))
|
||||
|
||||
(defn percent-number-with-unit? [v]
|
||||
(and (number-with-unit-symbol? v)
|
||||
(= (.-unit v) "%")))
|
||||
|
||||
(defn rem->px [^js v]
|
||||
(* (.-value v) 16))
|
||||
|
||||
@@ -91,12 +87,10 @@
|
||||
|
||||
(defn tokenscript-symbols->penpot-unit [^js v]
|
||||
(cond
|
||||
(nil? v) nil
|
||||
(structured-token? v) (structured-token->penpot-map v)
|
||||
(list-symbol? v) (structured-token->penpot-map v)
|
||||
(color-symbol? v) (.-value (.to v "hex"))
|
||||
(rem-number-with-unit? v) (rem->px v)
|
||||
(percent-number-with-unit? v) (/ (.-value v) 100)
|
||||
:else (.-value v)))
|
||||
|
||||
;; Processors ------------------------------------------------------------------
|
||||
|
||||
@@ -52,7 +52,6 @@
|
||||
[app.main.data.workspace.layers :as dwly]
|
||||
[app.main.data.workspace.layout :as layout]
|
||||
[app.main.data.workspace.libraries :as dwl]
|
||||
[app.main.data.workspace.mcp :as mcp]
|
||||
[app.main.data.workspace.notifications :as dwn]
|
||||
[app.main.data.workspace.pages :as dwpg]
|
||||
[app.main.data.workspace.path :as dwdp]
|
||||
@@ -213,8 +212,7 @@
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/of (dp/check-open-plugin)
|
||||
(fdf/fix-deleted-fonts-for-local-library file-id)
|
||||
(mcp/init-mcp-connexion)))))
|
||||
(fdf/fix-deleted-fonts-for-local-library file-id)))))
|
||||
|
||||
(defn- bundle-fetched
|
||||
[{:keys [file file-id thumbnails] :as bundle}]
|
||||
@@ -1448,7 +1446,6 @@
|
||||
(dm/export dwcp/paste-shapes)
|
||||
(dm/export dwcp/paste-data-valid?)
|
||||
(dm/export dwcp/copy-link-to-clipboard)
|
||||
(dm/export dwcp/copy-as-image)
|
||||
|
||||
;; Drawing
|
||||
(dm/export dwd/select-for-drawing)
|
||||
|
||||
@@ -1039,55 +1039,3 @@
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(clipboard/to-clipboard (rt/get-current-href)))))
|
||||
|
||||
(defn copy-as-image
|
||||
[]
|
||||
(ptk/reify ::copy-as-image
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [file-id (:current-file-id state)
|
||||
page-id (:current-page-id state)
|
||||
selected (first (dsh/lookup-selected state))
|
||||
|
||||
export {:file-id file-id
|
||||
:page-id page-id
|
||||
:object-id selected
|
||||
;; webp would be preferrable, but PNG is the most supported image MIME type by clipboard APIs.
|
||||
:type :png
|
||||
;; Always use 2 to ensure good enough quality for wireframes.
|
||||
:scale 2
|
||||
:suffix ""
|
||||
:enabled true
|
||||
:name ""}
|
||||
|
||||
params {:exports [export]
|
||||
:profile-id (:profile-id state)
|
||||
:cmd :export-shapes
|
||||
:wait true}]
|
||||
|
||||
(rx/concat
|
||||
;; Ensure current state persisted before exporting.
|
||||
(rx/of ::dps/force-persist)
|
||||
(->> (rx/from-atom refs/persistence-state {:emit-current-value? true})
|
||||
(rx/filter #(or (nil? %) (= :saved %)))
|
||||
(rx/first)
|
||||
(rx/timeout 400 (rx/empty)))
|
||||
|
||||
;; Exporting itself can time its time, better to notify that we are busy.
|
||||
(rx/of (ntf/info (tr "workspace.clipboard.copying")))
|
||||
|
||||
;; Call exporter to get image URI, then fetch and copy blob.
|
||||
(->> (rp/cmd! :export params)
|
||||
(rx/mapcat (fn [{:keys [uri]}]
|
||||
(http/send! {:method :get
|
||||
:uri uri
|
||||
:response-type :blob})))
|
||||
(rx/map :body)
|
||||
(rx/tap (fn [blob]
|
||||
(clipboard/to-clipboard-promise "image/png" (p/resolved blob))))
|
||||
(rx/map (fn [_]
|
||||
(ntf/success (tr "workspace.clipboard.image-copied"))))
|
||||
(rx/catch (fn [e]
|
||||
(js/console.error "clipboard blocked:" e)
|
||||
(ntf/error (tr "workspace.clipboard.image-copy-failed"))
|
||||
(rx/empty)))))))))
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
(ns app.main.data.workspace.drawing.box
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.rect :as grc]
|
||||
@@ -29,9 +28,9 @@
|
||||
[beicon.v2.core :as rx]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(defn- adjust-ratio
|
||||
(defn adjust-ratio
|
||||
[point initial]
|
||||
(let [v (gpt/to-vec point initial)
|
||||
(let [v (gpt/to-vec point initial)
|
||||
dx (mth/abs (:x v))
|
||||
dy (mth/abs (:y v))
|
||||
sx (mth/sign (:x v))
|
||||
@@ -44,43 +43,32 @@
|
||||
(> dy dx)
|
||||
(assoc :x (- (:x point) (* sx (- dy dx)))))))
|
||||
|
||||
(defn- resize-shape
|
||||
[{:keys [x y width height] :as shape} initial point lock? mod? snap-pixel?]
|
||||
(defn resize-shape [{:keys [x y width height] :as shape} initial point lock? mod? snap-pixel?]
|
||||
(if (and (some? x) (some? y) (some? width) (some? height))
|
||||
(let [p2
|
||||
(cond-> point lock? (adjust-ratio initial))
|
||||
(let [draw-rect (cond-> (grc/make-rect initial (cond-> point lock? (adjust-ratio initial)))
|
||||
snap-pixel?
|
||||
(-> (update :width max 1)
|
||||
(update :height max 1)))
|
||||
|
||||
p1
|
||||
(if mod?
|
||||
(gpt/point (- (* 2 (:x initial)) (:x p2))
|
||||
(- (* 2 (:y initial)) (:y p2)))
|
||||
initial)
|
||||
shape-rect (grc/make-rect x y width height)
|
||||
|
||||
draw-rect
|
||||
(cond-> (grc/make-rect p1 p2)
|
||||
snap-pixel?
|
||||
(-> (update :width d/max 1)
|
||||
(update :height d/max 1)))
|
||||
scalev (gpt/point (/ (:width draw-rect)
|
||||
(:width shape-rect))
|
||||
(/ (:height draw-rect)
|
||||
(:height shape-rect)))
|
||||
|
||||
shape-rect
|
||||
(grc/make-rect x y width height)
|
||||
|
||||
scalev
|
||||
(gpt/point (/ (:width draw-rect) (:width shape-rect))
|
||||
(/ (:height draw-rect) (:height shape-rect)))
|
||||
|
||||
movev
|
||||
(gpt/to-vec (gpt/point shape-rect) (gpt/point draw-rect))]
|
||||
movev (gpt/to-vec (gpt/point shape-rect)
|
||||
(gpt/point draw-rect))]
|
||||
|
||||
(-> shape
|
||||
(assoc :click-draw? false)
|
||||
(vary-meta merge {:mod? mod?})
|
||||
(gsh/transform-shape (-> (ctm/empty)
|
||||
(ctm/resize scalev (gpt/point x y))
|
||||
(ctm/move movev)))))
|
||||
shape))
|
||||
|
||||
(defn- update-drawing
|
||||
[state initial point lock? mod? snap-pixel?]
|
||||
(defn- update-drawing [state initial point lock? mod? snap-pixel?]
|
||||
(update-in state [:workspace-drawing :object] resize-shape initial point lock? mod? snap-pixel?))
|
||||
|
||||
(defn move-drawing
|
||||
@@ -140,7 +128,7 @@
|
||||
;; Take until before the snap calculation otherwise we could cancel the snap in the worker
|
||||
;; and its a problem for fast moving drawing
|
||||
(rx/take-until stopper)
|
||||
(rx/with-latest-from ms/mouse-position-shift ms/mouse-position-alt)
|
||||
(rx/with-latest-from ms/mouse-position-shift ms/mouse-position-mod)
|
||||
(rx/switch-map
|
||||
(fn [[point :as current]]
|
||||
(->> (snap/closest-snap-point page-id [shape] objects layout zoom focus point)
|
||||
|
||||
@@ -221,24 +221,6 @@
|
||||
(pcb/delete-color id))]
|
||||
(rx/of (dch/commit-changes changes))))))
|
||||
|
||||
(defn duplicate-color
|
||||
[file-id color-id]
|
||||
(assert (uuid? file-id) "expected valid uuid for `file-id`")
|
||||
(assert (uuid? color-id) "expected valid uuid for `color-id`")
|
||||
|
||||
(ptk/reify ::duplicate-color
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
color (ctl/get-color data color-id)
|
||||
new-color (-> color
|
||||
(assoc :id (uuid/next))
|
||||
(d/without-nils)
|
||||
(ctc/check-library-color))
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/add-color new-color))]
|
||||
(rx/of (dch/commit-changes changes))))))
|
||||
|
||||
;; FIXME: this should be deleted
|
||||
(defn add-media
|
||||
[media]
|
||||
@@ -368,23 +350,6 @@
|
||||
(pcb/delete-typography id))]
|
||||
(rx/of (dch/commit-changes changes))))))
|
||||
|
||||
(defn duplicate-typography
|
||||
[file-id typography-id]
|
||||
(assert (uuid? file-id) "expected valid uuid for `file-id`")
|
||||
(assert (uuid? typography-id) "expected valid uuid for `typography-id`")
|
||||
|
||||
(ptk/reify ::duplicate-typography
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
typography (get-in data [:typographies typography-id])
|
||||
new-typography (-> typography
|
||||
(assoc :id (uuid/next))
|
||||
(ctt/check-typography))
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/add-typography new-typography))]
|
||||
(rx/of (dch/commit-changes changes))))))
|
||||
|
||||
(defn- add-component2
|
||||
"This is the second step of the component creation."
|
||||
([selected]
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
;; 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 app.main.data.workspace.mcp
|
||||
(:require
|
||||
[app.common.logging :as log]
|
||||
[app.common.uri :as u]
|
||||
[app.config :as cf]
|
||||
[app.main.data.plugins :as dp]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.store :as st]
|
||||
[app.plugins.register :refer [mcp-plugin-id]]
|
||||
[beicon.v2.core :as rx]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(log/set-level! :info)
|
||||
|
||||
(def ^:private default-manifest
|
||||
{:code "plugin.js"
|
||||
:name "Penpot MCP Plugin"
|
||||
:version 2
|
||||
:plugin-id mcp-plugin-id
|
||||
:description "This plugin enables interaction with the Penpot MCP server"
|
||||
:allow-background true
|
||||
:permissions
|
||||
#{"library:read" "library:write"
|
||||
"comment:read" "comment:write"
|
||||
"content:write" "content:read"}})
|
||||
|
||||
(defn finalize-workspace?
|
||||
[event]
|
||||
(= (ptk/type event) :app.main.data.workspace/finalize-workspace))
|
||||
|
||||
(defn init-mcp!
|
||||
[stream]
|
||||
(->> (rp/cmd! :get-current-mcp-token)
|
||||
(rx/subs!
|
||||
(fn [{:keys [token]}]
|
||||
(when token
|
||||
(dp/start-plugin!
|
||||
(assoc default-manifest
|
||||
:url (str (u/join cf/public-uri "plugins/mcp/manifest.json"))
|
||||
:host (str (u/join cf/public-uri "plugins/mcp/")))
|
||||
|
||||
;; API extension for MCP server
|
||||
#js {:mcp
|
||||
#js
|
||||
{:getToken (constantly token)
|
||||
:getServerUrl #(str cf/mcp-ws-uri)
|
||||
:setMcpStatus
|
||||
(fn [status]
|
||||
;; TODO: Visual feedback
|
||||
(log/info :hint "MCP STATUS" :status status))
|
||||
|
||||
:on
|
||||
(fn [event cb]
|
||||
(when-let [event
|
||||
(case event
|
||||
"disconnect" ::disconnect
|
||||
"connect" ::connect
|
||||
nil)]
|
||||
|
||||
(let [stopper (rx/filter finalize-workspace? stream)]
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? event))
|
||||
(rx/take-until stopper)
|
||||
(rx/subs! #(cb))))))}}))))))
|
||||
|
||||
(defn disconnect-mcp
|
||||
[]
|
||||
(st/emit! (ptk/data-event ::disconnect)))
|
||||
|
||||
(defn connect-mcp
|
||||
[]
|
||||
(st/emit! (ptk/data-event ::connect)))
|
||||
|
||||
(defn init-mcp-connexion
|
||||
[]
|
||||
(ptk/reify ::init-mcp-connexion
|
||||
ptk/EffectEvent
|
||||
(effect [_ state stream]
|
||||
(when (and (contains? cf/flags :mcp)
|
||||
(-> state :profile :props :mcp-status))
|
||||
(init-mcp! stream)))))
|
||||
@@ -620,61 +620,68 @@
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
;; We do not allow to apply tokens while text editor is open.
|
||||
(when (empty? (get state :workspace-editor-state))
|
||||
(let [attributes-to-remove
|
||||
;; Remove atomic typography tokens when applying composite and vice-verca
|
||||
(cond
|
||||
(ctt/typography-token-keys (:type token)) (set/union attributes-to-remove ctt/typography-keys)
|
||||
(ctt/typography-keys (:type token)) (set/union attributes-to-remove ctt/typography-token-keys)
|
||||
:else attributes-to-remove)]
|
||||
(when-let [tokens (some-> (dsh/lookup-file-data state)
|
||||
(get :tokens-lib)
|
||||
(ctob/get-tokens-in-active-sets))]
|
||||
(->> (if (contains? cf/flags :tokenscript)
|
||||
(rx/of (ts/resolve-tokens tokens))
|
||||
(sd/resolve-tokens tokens))
|
||||
(rx/mapcat
|
||||
(fn [resolved-tokens]
|
||||
(let [undo-id (js/Symbol)
|
||||
objects (dsh/lookup-page-objects state)
|
||||
selected-shapes (select-keys objects shape-ids)
|
||||
;; The classic text editor sets :workspace-editor-state; the WASM text editor
|
||||
;; does not, so we also check :workspace-local :edition for text shapes.
|
||||
(let [edition (get-in state [:workspace-local :edition])
|
||||
objects (dsh/lookup-page-objects state)
|
||||
text-editing? (and (some? edition)
|
||||
(= :text (:type (get objects edition))))]
|
||||
(when (and (empty? (get state :workspace-editor-state))
|
||||
(not text-editing?))
|
||||
(let [attributes-to-remove
|
||||
;; Remove atomic typography tokens when applying composite and vice-verca
|
||||
(cond
|
||||
(ctt/typography-token-keys (:type token)) (set/union attributes-to-remove ctt/typography-keys)
|
||||
(ctt/typography-keys (:type token)) (set/union attributes-to-remove ctt/typography-token-keys)
|
||||
:else attributes-to-remove)]
|
||||
(when-let [tokens (some-> (dsh/lookup-file-data state)
|
||||
(get :tokens-lib)
|
||||
(ctob/get-tokens-in-active-sets))]
|
||||
(->> (if (contains? cf/flags :tokenscript)
|
||||
(rx/of (ts/resolve-tokens tokens))
|
||||
(sd/resolve-tokens tokens))
|
||||
(rx/mapcat
|
||||
(fn [resolved-tokens]
|
||||
(let [undo-id (js/Symbol)
|
||||
objects (dsh/lookup-page-objects state)
|
||||
selected-shapes (select-keys objects shape-ids)
|
||||
|
||||
shapes (->> selected-shapes
|
||||
(filter (fn [[_ shape]]
|
||||
(or
|
||||
(and (ctsl/any-layout-immediate-child? objects shape)
|
||||
(some ctt/spacing-margin-keys attributes))
|
||||
(and (ctt/any-appliable-attr-for-shape? attributes (:type shape) (:layout shape))
|
||||
(all-attrs-appliable-for-token? attributes (:type token)))))))
|
||||
shape-ids (d/nilv (keys shapes) [])
|
||||
any-variant? (->> shapes vals (some ctk/is-variant?) boolean)
|
||||
shapes (->> selected-shapes
|
||||
(filter (fn [[_ shape]]
|
||||
(or
|
||||
(and (ctsl/any-layout-immediate-child? objects shape)
|
||||
(some ctt/spacing-margin-keys attributes))
|
||||
(and (ctt/any-appliable-attr-for-shape? attributes (:type shape) (:layout shape))
|
||||
(all-attrs-appliable-for-token? attributes (:type token)))))))
|
||||
shape-ids (d/nilv (keys shapes) [])
|
||||
any-variant? (->> shapes vals (some ctk/is-variant?) boolean)
|
||||
|
||||
resolved-value (get-in resolved-tokens [(cfo/token-identifier token) :resolved-value])
|
||||
resolved-value (if (contains? cf/flags :tokenscript)
|
||||
(ts/tokenscript-symbols->penpot-unit resolved-value)
|
||||
resolved-value)
|
||||
tokenized-attributes (cfo/attributes-map attributes token)
|
||||
type (:type token)]
|
||||
(rx/concat
|
||||
(rx/of
|
||||
(st/emit! (ev/event {::ev/name "apply-tokens"
|
||||
:type type
|
||||
:applied-to attributes
|
||||
:applied-to-variant any-variant?}))
|
||||
(dwu/start-undo-transaction undo-id)
|
||||
(dwsh/update-shapes shape-ids (fn [shape]
|
||||
(cond-> shape
|
||||
attributes-to-remove
|
||||
(update :applied-tokens #(apply (partial dissoc %) attributes-to-remove))
|
||||
:always
|
||||
(update :applied-tokens merge tokenized-attributes)))))
|
||||
(when on-update-shape
|
||||
(let [res (on-update-shape resolved-value shape-ids attributes)]
|
||||
;; Composed updates return observables and need to be executed differently
|
||||
(if (rx/observable? res)
|
||||
res
|
||||
(rx/of res))))
|
||||
(rx/of (dwu/commit-undo-transaction undo-id)))))))))))))
|
||||
resolved-value (get-in resolved-tokens [(cfo/token-identifier token) :resolved-value])
|
||||
resolved-value (if (contains? cf/flags :tokenscript)
|
||||
(ts/tokenscript-symbols->penpot-unit resolved-value)
|
||||
resolved-value)
|
||||
tokenized-attributes (cfo/attributes-map attributes token)
|
||||
type (:type token)]
|
||||
(rx/concat
|
||||
(rx/of
|
||||
(st/emit! (ev/event {::ev/name "apply-tokens"
|
||||
:type type
|
||||
:applied-to attributes
|
||||
:applied-to-variant any-variant?}))
|
||||
(dwu/start-undo-transaction undo-id)
|
||||
(dwsh/update-shapes shape-ids (fn [shape]
|
||||
(cond-> shape
|
||||
attributes-to-remove
|
||||
(update :applied-tokens #(apply (partial dissoc %) attributes-to-remove))
|
||||
:always
|
||||
(update :applied-tokens merge tokenized-attributes)))))
|
||||
(when on-update-shape
|
||||
(let [res (on-update-shape resolved-value shape-ids attributes)]
|
||||
;; Composed updates return observables and need to be executed differently
|
||||
(if (rx/observable? res)
|
||||
res
|
||||
(rx/of res))))
|
||||
(rx/of (dwu/commit-undo-transaction undo-id))))))))))))))
|
||||
|
||||
(defn apply-spacing-token-separated
|
||||
"Handles edge-case for spacing token when applying token via toggle button.
|
||||
|
||||
@@ -8,11 +8,10 @@
|
||||
(:require
|
||||
[app.common.json :as json]
|
||||
[app.common.path-names :as cpn]
|
||||
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.config :as cf]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.data.style-dictionary :as sd]
|
||||
[app.main.data.tokenscript :as ts]
|
||||
[app.main.data.workspace.tokens.errors :as wte]
|
||||
[app.main.store :as st]
|
||||
[app.util.i18n :refer [tr]]
|
||||
@@ -75,18 +74,15 @@
|
||||
(when unknown-tokens
|
||||
(st/emit! (show-unknown-types-warning unknown-tokens)))
|
||||
(try
|
||||
(let [tokens-tree (ctob/get-all-tokens-map tokens-lib)
|
||||
resolved-tokens (if (contains? cf/flags :tokenscript)
|
||||
(rx/of (ts/resolve-tokens tokens-tree))
|
||||
(sd/resolve-tokens-with-verbose-errors tokens-tree))]
|
||||
(->> resolved-tokens
|
||||
(rx/map (fn [_]
|
||||
tokens-lib))
|
||||
(rx/catch (fn [sd-error]
|
||||
(let [reference-errors (extract-reference-errors sd-error)]
|
||||
(if reference-errors
|
||||
(rx/of tokens-lib)
|
||||
(throw (wte/error-ex-info :error.import/style-dictionary-unknown-error sd-error sd-error))))))))
|
||||
(->> (ctob/get-all-tokens-map tokens-lib)
|
||||
(sd/resolve-tokens-with-verbose-errors)
|
||||
(rx/map (fn [_]
|
||||
tokens-lib))
|
||||
(rx/catch (fn [sd-error]
|
||||
(let [reference-errors (extract-reference-errors sd-error)]
|
||||
(if reference-errors
|
||||
(rx/of tokens-lib)
|
||||
(throw (wte/error-ex-info :error.import/style-dictionary-unknown-error sd-error sd-error)))))))
|
||||
(catch js/Error e
|
||||
(throw (wte/error-ex-info :error.import/style-dictionary-unknown-error "" e)))))
|
||||
|
||||
|
||||
@@ -6,16 +6,13 @@
|
||||
|
||||
(ns app.main.data.workspace.tokens.propagation
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.logging :as l]
|
||||
[app.common.time :as ct]
|
||||
[app.common.types.token :as ctt]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.config :as cf]
|
||||
[app.main.data.helpers :as dsh]
|
||||
[app.main.data.style-dictionary :as sd]
|
||||
[app.main.data.tokenscript :as ts]
|
||||
[app.main.data.workspace.shapes :as dwsh]
|
||||
[app.main.data.workspace.thumbnails :as dwt]
|
||||
[app.main.data.workspace.tokens.application :as dwta]
|
||||
@@ -213,13 +210,10 @@
|
||||
(ptk/reify ::propagate-workspace-tokens
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(when-let [tokens-tree (-> (dsh/lookup-file-data state)
|
||||
(get :tokens-lib)
|
||||
(ctob/get-tokens-in-active-sets))]
|
||||
(->> (if (contains? cf/flags :tokenscript)
|
||||
(rx/of (-> (ts/resolve-tokens tokens-tree)
|
||||
(d/update-vals #(update % :resolved-value ts/tokenscript-symbols->penpot-unit))))
|
||||
(sd/resolve-tokens tokens-tree))
|
||||
(when-let [tokens-lib (-> (dsh/lookup-file-data state)
|
||||
(get :tokens-lib))]
|
||||
(->> (ctob/get-tokens-in-active-sets tokens-lib)
|
||||
(sd/resolve-tokens)
|
||||
(rx/mapcat (fn [sd-tokens]
|
||||
(let [undo-id (js/Symbol)]
|
||||
(rx/concat
|
||||
|
||||
@@ -548,7 +548,7 @@
|
||||
modif-tree
|
||||
(dwm/build-modif-tree ids objects get-modifier)]
|
||||
|
||||
(rx/of (dwm/apply-wasm-modifiers modif-tree)))
|
||||
(rx/of (dwm/apply-wasm-modifiers modif-tree :ignore-touched (:ignore-touched options))))
|
||||
|
||||
(let [page-id (or (:page-id options)
|
||||
(:current-page-id state))
|
||||
@@ -1173,8 +1173,7 @@
|
||||
(when add-component-to-variant?
|
||||
(rx/of (ev/event {::ev/name "add-component-to-variant"})))
|
||||
(when add-new-variant?
|
||||
(rx/of (ev/event {::ev/name "add-new-variant"
|
||||
::ev/origin "workspace:move-shapes-to-frame"}))))))))
|
||||
(rx/of (ev/event {::ev/name "add-new-variant" ::ev/origin "workspace:move-shapes-to-frame"}))))))))
|
||||
|
||||
(defn- get-displacement
|
||||
"Retrieve the correct displacement delta point for the
|
||||
|
||||
@@ -613,7 +613,7 @@
|
||||
vec))
|
||||
|
||||
(defn combine-as-variants
|
||||
[ids {:keys [page-id trigger variant-id] :or {variant-id nil}}]
|
||||
[ids {:keys [page-id trigger]}]
|
||||
(ptk/reify ::combine-as-variants
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
@@ -647,7 +647,7 @@
|
||||
:shapes
|
||||
count
|
||||
inc)
|
||||
variant-id (or variant-id (uuid/next))
|
||||
variant-id (uuid/next)
|
||||
undo-id (js/Symbol)]
|
||||
|
||||
(rx/concat
|
||||
|
||||
@@ -183,9 +183,6 @@
|
||||
[id]
|
||||
(l/derived #(contains? % id) selected-shapes))
|
||||
|
||||
(def highlighted-shapes
|
||||
(l/derived :highlighted workspace-local))
|
||||
|
||||
(def export-in-progress?
|
||||
(l/derived :export-in-progress? export))
|
||||
|
||||
|
||||
@@ -197,7 +197,7 @@
|
||||
:settings-options
|
||||
:settings-feedback
|
||||
:settings-subscription
|
||||
:settings-integrations
|
||||
:settings-access-tokens
|
||||
:settings-notifications)
|
||||
(let [params (get params :query)
|
||||
error-report-id (some-> params :error-report-id uuid/parse*)]
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
(kbd/enter? event)
|
||||
(let [selected (dom/get-active)]
|
||||
(dom/prevent-default event)
|
||||
(dom/click selected))
|
||||
(dom/click! selected))
|
||||
|
||||
(kbd/tab? event)
|
||||
(on-close)))))]
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
input-name (get props :name)
|
||||
more-classes (get props :class)
|
||||
auto-focus? (get props :auto-focus? false)
|
||||
input-ref (mf/use-ref nil)
|
||||
|
||||
data-testid (d/nilv data-testid input-name)
|
||||
|
||||
@@ -83,6 +82,7 @@
|
||||
(swap! form assoc-in [:touched input-name] true)
|
||||
(fm/on-input-change form input-name value trim)
|
||||
(on-change-value name value)))
|
||||
|
||||
on-blur
|
||||
(fn [_]
|
||||
(reset! focus? false))
|
||||
@@ -92,18 +92,9 @@
|
||||
(when-not (get-in @form [:touched input-name])
|
||||
(swap! form assoc-in [:touched input-name] true)))
|
||||
|
||||
on-key-press
|
||||
(mf/use-fn
|
||||
(mf/deps input-ref)
|
||||
(fn [e]
|
||||
(dom/prevent-default e)
|
||||
(when (kbd/space? e)
|
||||
(dom/click (mf/ref-val input-ref)))))
|
||||
|
||||
props (-> props
|
||||
(dissoc :help-icon :form :trim :children :show-success? :auto-focus? :label)
|
||||
(assoc :id (name input-name)
|
||||
:ref input-ref
|
||||
:value value
|
||||
:auto-focus auto-focus?
|
||||
:on-click (when (or is-radio? is-checkbox?) on-click)
|
||||
@@ -140,7 +131,7 @@
|
||||
:for (name input-name)} label
|
||||
|
||||
(when is-checkbox?
|
||||
[:span {:class (stl/css-case :global/checked checked?) :tab-index "0" :on-key-press on-key-press} (when checked? deprecated-icon/status-tick)])
|
||||
[:span {:class (stl/css-case :global/checked checked?)} (when checked? deprecated-icon/status-tick)])
|
||||
|
||||
(if is-checkbox?
|
||||
[:> :input props]
|
||||
|
||||
@@ -9,17 +9,14 @@
|
||||
(:require
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]]
|
||||
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.keyboard :as k]
|
||||
[goog.events :as events]
|
||||
[rumext.v2 :as mf])
|
||||
(:import
|
||||
goog.events.EventType))
|
||||
(:import goog.events.EventType))
|
||||
|
||||
(mf/defc confirm-dialog
|
||||
{::mf/register modal/components
|
||||
@@ -71,11 +68,8 @@
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h2 {:class (stl/css :modal-title)} title]
|
||||
[:div {:class (stl/css :modal-close-btn)}
|
||||
[:> icon-button* {:variant "ghost"
|
||||
:aria-label (tr "labels.close")
|
||||
:on-click cancel-fn
|
||||
:icon i/close}]]]
|
||||
[:button {:class (stl/css :modal-close-btn)
|
||||
:on-click cancel-fn} deprecated-icon/close]]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
(when (and (string? message) (not= message ""))
|
||||
@@ -93,19 +87,24 @@
|
||||
[:ul {:class (stl/css :component-list)}
|
||||
(for [item items]
|
||||
[:li {:class (stl/css :modal-item-element)}
|
||||
[:> icon* {:icon-id i/component
|
||||
:class (stl/css :modal-component-icon)
|
||||
:size "s"}]
|
||||
[:span {:class (stl/css :modal-component-icon)}
|
||||
deprecated-icon/component]
|
||||
[:span {:class (stl/css :modal-component-name)}
|
||||
(:name item)]])]])]
|
||||
|
||||
[:div {:class (stl/css :modal-footer)}
|
||||
[:div {:class (stl/css :action-buttons)}
|
||||
(when-not (= cancel-label :omit)
|
||||
[:> button* {:variant "secondary"
|
||||
:on-click cancel-fn}
|
||||
cancel-label])
|
||||
[:> button* {:variant (cond (= accept-style :danger) "destructive"
|
||||
(= accept-style :primary) "primary")
|
||||
:on-click accept-fn}
|
||||
accept-label]]]]]))
|
||||
[:input
|
||||
{:class (stl/css :cancel-button)
|
||||
:type "button"
|
||||
:value cancel-label
|
||||
:on-click cancel-fn}])
|
||||
|
||||
[:input
|
||||
{:class (stl/css-case :accept-btn true
|
||||
:danger (= accept-style :danger)
|
||||
:primary (= accept-style :primary))
|
||||
:type "button"
|
||||
:value accept-label
|
||||
:on-click accept-fn}]]]]]))
|
||||
|
||||
@@ -15,9 +15,10 @@
|
||||
|
||||
.modal-container {
|
||||
@extend .modal-container-base;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-xxl);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
margin-bottom: deprecated.$s-24;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
@@ -26,13 +27,12 @@
|
||||
}
|
||||
|
||||
.modal-close-btn {
|
||||
position: absolute;
|
||||
top: var(--sp-m);
|
||||
right: var(--sp-m);
|
||||
@extend .modal-close-btn-base;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
@include deprecated.bodyLargeTypography;
|
||||
margin-bottom: deprecated.$s-24;
|
||||
}
|
||||
|
||||
.modal-item-element {
|
||||
@@ -41,18 +41,32 @@
|
||||
|
||||
.modal-component-icon {
|
||||
@include deprecated.flexCenter;
|
||||
color: var(--color-foreground-secondary);
|
||||
height: deprecated.$s-16;
|
||||
width: deprecated.$s-16;
|
||||
svg {
|
||||
@extend .button-icon-small;
|
||||
stroke: var(--color);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-component-name {
|
||||
@include deprecated.bodyLargeTypography;
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
@extend .modal-action-btns;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
@extend .modal-cancel-btn;
|
||||
}
|
||||
|
||||
.accept-btn {
|
||||
@extend .modal-accept-btn;
|
||||
&.danger {
|
||||
@extend .modal-danger-btn;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-scd-msg,
|
||||
.modal-subtitle,
|
||||
.modal-msg {
|
||||
|
||||
@@ -7,9 +7,7 @@
|
||||
(ns app.main.ui.dashboard.fonts
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.media :as cm]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
@@ -24,7 +22,6 @@
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.main.ui.notifications.context-notification :refer [context-notification]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.http :as http]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[beicon.v2.core :as rx]
|
||||
@@ -35,7 +32,7 @@
|
||||
(def ^:private accept-font-types
|
||||
(str (str/join "," cm/font-types)
|
||||
;; A workaround to solve a problem with chrome input selector
|
||||
",.ttf,application/font-woff,.woff,.woff2,.otf"))
|
||||
",.ttf,application/font-woff,woff,.otf"))
|
||||
|
||||
(defn- use-page-title
|
||||
[team section]
|
||||
@@ -119,10 +116,10 @@
|
||||
(swap! fonts* dissoc id)
|
||||
(swap! uploading* disj id)
|
||||
(st/emit! (df/add-font font)))
|
||||
(fn [cause]
|
||||
(fn [error]
|
||||
(st/emit! (ntf/error (tr "errors.bad-font" (first (:names item)))))
|
||||
(swap! fonts* dissoc id)
|
||||
(ex/print-throwable cause))))))
|
||||
(js/console.log "error" error))))))
|
||||
|
||||
on-upload
|
||||
(mf/use-fn
|
||||
@@ -262,14 +259,11 @@
|
||||
(mf/defc installed-font-context-menu
|
||||
{::mf/props :obj
|
||||
::mf/private true}
|
||||
[{:keys [is-open on-close on-edit on-download on-delete]}]
|
||||
(let [options (mf/with-memo [on-edit on-download on-delete]
|
||||
[{:keys [is-open on-close on-edit on-delete]}]
|
||||
(let [options (mf/with-memo [on-edit on-delete]
|
||||
[{:name (tr "labels.edit")
|
||||
:id "font-edit"
|
||||
:handler on-edit}
|
||||
{:name (tr "labels.download-simple")
|
||||
:id "font-download"
|
||||
:handler on-download}
|
||||
{:name (tr "labels.delete")
|
||||
:id "font-delete"
|
||||
:handler on-delete}])]
|
||||
@@ -351,26 +345,6 @@
|
||||
(st/emit! (df/delete-font font-id)))}]
|
||||
(st/emit! (modal/show options)))))
|
||||
|
||||
on-download
|
||||
(mf/use-fn
|
||||
(mf/deps variants)
|
||||
(fn [_event]
|
||||
(let [variant (first variants)
|
||||
variant-id (:id variant)
|
||||
multiple? (> (count variants) 1)
|
||||
cmd (if multiple? :download-font-family :download-font)
|
||||
params (if multiple? {:font-id font-id} {:id variant-id})]
|
||||
(->> (rp/cmd! cmd params)
|
||||
(rx/mapcat (fn [{:keys [name uri]}]
|
||||
(->> (http/send! {:uri uri :method :get :response-type :blob})
|
||||
(rx/map :body)
|
||||
(rx/map (fn [blob] (d/vec2 name blob))))))
|
||||
(rx/subs! (fn [[filename blob]]
|
||||
(dom/trigger-download filename blob))
|
||||
(fn [error]
|
||||
(js/console.error "error downloading font" error)
|
||||
(st/emit! (ntf/error (tr "errors.generic")))))))))
|
||||
|
||||
on-delete-variant
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
@@ -433,7 +407,6 @@
|
||||
{:on-close on-menu-close
|
||||
:is-open menu-open?
|
||||
:on-delete on-delete-font
|
||||
:on-download on-download
|
||||
:on-edit on-edit}]]))]))
|
||||
|
||||
(mf/defc installed-fonts*
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
(mf/use-ref nil)
|
||||
|
||||
on-import-files
|
||||
(fn [] (dom/click (mf/ref-val file-input)))
|
||||
(fn [] (dom/click! (mf/ref-val file-input)))
|
||||
|
||||
on-finish-import
|
||||
(mf/use-fn
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
[app.main.data.dashboard :as dd]
|
||||
[app.main.data.event :as ev]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.nitrate :as dnt]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.data.team :as dtm]
|
||||
[app.main.refs :as refs]
|
||||
@@ -31,13 +30,10 @@
|
||||
[app.main.ui.dashboard.subscription :refer [dashboard-cta*
|
||||
get-subscription-type
|
||||
menu-team-icon*
|
||||
nitrate-sidebar*
|
||||
show-subscription-dashboard-banner?
|
||||
subscription-sidebar*]]
|
||||
[app.main.ui.dashboard.team-form]
|
||||
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
|
||||
[app.main.ui.ds.foundations.assets.raw-svg :refer [raw-svg*]]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.main.ui.nitrate.nitrate-form]
|
||||
[app.util.dom :as dom]
|
||||
@@ -78,8 +74,6 @@
|
||||
(def ^:private exit-icon
|
||||
(deprecated-icon/icon-xref :exit (stl/css :exit-icon)))
|
||||
|
||||
(def ^:private ^:svg-id penpot-logo-icon "penpot-logo-icon")
|
||||
|
||||
(mf/defc sidebar-project*
|
||||
{::mf/private true}
|
||||
[{:keys [item is-selected]}]
|
||||
@@ -302,9 +296,10 @@
|
||||
on-create-org-click
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(if (dm/get-in profile [:props :nitrate-license :valid])
|
||||
(dnt/go-to-nitrate-cc)
|
||||
(st/emit! (dnt/show-nitrate-popup :nitrate-form)))))]
|
||||
(if (:nitrate-licence profile)
|
||||
;; TODO update when org creation route is ready
|
||||
(dom/open-new-window "/control-center/org/create")
|
||||
(st/emit! (modal/show :nitrate-form {})))))]
|
||||
|
||||
[:> dropdown-menu* props
|
||||
|
||||
@@ -502,23 +497,18 @@
|
||||
|
||||
(mf/defc sidebar-org-switch*
|
||||
[{:keys [team profile]}]
|
||||
(let [teams (mf/deref refs/teams)
|
||||
orgs (mf/with-memo [teams]
|
||||
(let [orgs (->> teams
|
||||
vals
|
||||
(group-by :organization-id)
|
||||
(map (fn [[_group entries]] (first entries)))
|
||||
vec
|
||||
(d/index-by :id))]
|
||||
(update-vals orgs
|
||||
(fn [t]
|
||||
(assoc t :name (str "ORG: " (:organization-name t)))))))
|
||||
(let [teams (->> (mf/deref refs/teams)
|
||||
vals
|
||||
(group-by :organization-id)
|
||||
(map (fn [[_group entries]] (first entries)))
|
||||
vec
|
||||
(d/index-by :id))
|
||||
|
||||
empty? (= (count orgs) 1)
|
||||
teams (update-vals teams
|
||||
(fn [t]
|
||||
(assoc t :name (str "ORG: " (:organization-name t)))))
|
||||
|
||||
|
||||
current-org (mf/with-memo [team]
|
||||
(assoc team :name (str "ORG: " (:organization-name team))))
|
||||
team (assoc team :name (str "ORG: " (:organization-name team)))
|
||||
|
||||
show-teams-menu*
|
||||
(mf/use-state false)
|
||||
@@ -540,52 +530,36 @@
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(some-> (dom/get-current-target event)
|
||||
(dom/click)))))
|
||||
(dom/click!)))))
|
||||
close-teams-menu
|
||||
(mf/use-fn #(reset! show-teams-menu* false))
|
||||
(mf/use-fn #(reset! show-teams-menu* false))]
|
||||
|
||||
on-create-org-click
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(if (dm/get-in profile [:props :nitrate-license :valid])
|
||||
(dnt/go-to-nitrate-cc)
|
||||
(st/emit! (dnt/show-nitrate-popup :nitrate-form)))))]
|
||||
(if empty?
|
||||
[:div {:class (stl/css :nitrate-orgs-empty)}
|
||||
[:span {:class (stl/css :nitrate-penpot-icon)}
|
||||
[:> raw-svg* {:id penpot-logo-icon}]]
|
||||
"Penpot"
|
||||
[:> button* {:variant "ghost"
|
||||
:type "button"
|
||||
:class (stl/css :nitrate-create-org)
|
||||
:on-click on-create-org-click} (tr "dashboard.create-new-org")]]
|
||||
[:div {:class (stl/css :sidebar-team-switch)}
|
||||
[:div {:class (stl/css :switch-content)}
|
||||
[:button {:class (stl/css :current-team)
|
||||
:on-click on-show-teams-click
|
||||
:on-key-down on-show-teams-keydown}
|
||||
|
||||
[:div {:class (stl/css :sidebar-team-switch)}
|
||||
[:div {:class (stl/css :switch-content)}
|
||||
[:button {:class (stl/css :current-team)
|
||||
:on-click on-show-teams-click
|
||||
:on-key-down on-show-teams-keydown}
|
||||
[:div {:class (stl/css :team-name)}
|
||||
[:img {:src (cf/resolve-team-photo-url team)
|
||||
:class (stl/css :team-picture)
|
||||
:alt (:name team)}]
|
||||
[:span {:class (stl/css :team-text) :title (:name team)} (:name team)]]
|
||||
|
||||
[:div {:class (stl/css :team-name)}
|
||||
[:img {:src (cf/resolve-team-photo-url current-org)
|
||||
:class (stl/css :team-picture)
|
||||
:alt (:name current-org)}]
|
||||
[:span {:class (stl/css :team-text) :title (:name current-org)} (:name current-org)]]
|
||||
arrow-icon]]
|
||||
|
||||
arrow-icon]]
|
||||
;; Teams Dropdown
|
||||
|
||||
;; Teams Dropdown
|
||||
|
||||
[:> teams-selector-dropdown* {:show show-teams-menu?
|
||||
:on-close close-teams-menu
|
||||
:id "organizations-list"
|
||||
:class (stl/css :dropdown :teams-dropdown)
|
||||
:team current-org
|
||||
:profile profile
|
||||
:teams orgs
|
||||
:show-default-team false
|
||||
:allow-create-teams false
|
||||
:allow-create-org true}]])))
|
||||
[:> teams-selector-dropdown* {:show show-teams-menu?
|
||||
:on-close close-teams-menu
|
||||
:id "organizations-list"
|
||||
:class (stl/css :dropdown :teams-dropdown)
|
||||
:team team
|
||||
:profile profile
|
||||
:teams teams
|
||||
:show-default-team false
|
||||
:allow-create-teams false
|
||||
:allow-create-org true}]]))
|
||||
|
||||
(mf/defc sidebar-team-switch*
|
||||
[{:keys [team profile]}]
|
||||
@@ -627,7 +601,7 @@
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(some-> (dom/get-current-target event)
|
||||
(dom/click)))))
|
||||
(dom/click!)))))
|
||||
|
||||
close-team-options-menu
|
||||
(mf/use-fn #(reset! show-team-options-menu* false))
|
||||
@@ -647,7 +621,7 @@
|
||||
(dom/stop-propagation event)
|
||||
|
||||
(some-> (dom/get-current-target event)
|
||||
(dom/click)))))
|
||||
(dom/click!)))))
|
||||
|
||||
close-teams-menu
|
||||
(mf/use-fn #(reset! show-teams-menu* false))]
|
||||
@@ -731,8 +705,6 @@
|
||||
overflow* (mf/use-state false)
|
||||
overflow? (deref overflow*)
|
||||
|
||||
nitrate? (contains? cf/flags :nitrate)
|
||||
|
||||
go-projects
|
||||
(mf/use-fn #(st/emit! (dcm/go-to-dashboard-recent)))
|
||||
|
||||
@@ -821,71 +793,70 @@
|
||||
(reset! overflow* (> scroll-height client-height))))
|
||||
|
||||
[:*
|
||||
[:div {:ref container}
|
||||
(when nitrate?
|
||||
[:div {:class (stl/css :nitrate-orgs-container)}
|
||||
[:> sidebar-org-switch* {:team team :profile profile}]])
|
||||
[:div {:class (stl/css-case :sidebar-content true :sidebar-content-nitrate nitrate?)}
|
||||
[:> sidebar-team-switch* {:team team :profile profile}]
|
||||
[:div {:class (stl/css-case :sidebar-content true)
|
||||
:ref container}
|
||||
(when (contains? cf/flags :nitrate)
|
||||
[:> sidebar-org-switch* {:team team :profile profile}])
|
||||
[:> sidebar-team-switch* {:team team :profile profile}]
|
||||
|
||||
[:> sidebar-search* {:search-term search-term
|
||||
:team-id (:id team)}]
|
||||
[:> sidebar-search* {:search-term search-term
|
||||
:team-id (:id team)}]
|
||||
|
||||
[:div {:class (stl/css :sidebar-content-section)}
|
||||
[:ul {:class (stl/css :sidebar-nav)}
|
||||
[:li {:class (stl/css-case :recent-projects true
|
||||
:sidebar-nav-item true
|
||||
:current projects?)}
|
||||
[:& link {:action go-projects
|
||||
:class (stl/css :sidebar-link)
|
||||
:keyboard-action go-projects-with-key}
|
||||
[:span {:class (stl/css :element-title)} (tr "labels.projects")]]]
|
||||
[:div {:class (stl/css :sidebar-content-section)}
|
||||
[:ul {:class (stl/css :sidebar-nav)}
|
||||
[:li {:class (stl/css-case :recent-projects true
|
||||
:sidebar-nav-item true
|
||||
:current projects?)}
|
||||
[:& link {:action go-projects
|
||||
:class (stl/css :sidebar-link)
|
||||
:keyboard-action go-projects-with-key}
|
||||
[:span {:class (stl/css :element-title)} (tr "labels.projects")]]]
|
||||
|
||||
[:li {:class (stl/css-case :current drafts?
|
||||
:sidebar-nav-item true)}
|
||||
[:& link {:action go-drafts
|
||||
:class (stl/css :sidebar-link)
|
||||
:keyboard-action go-drafts-with-key}
|
||||
[:span {:class (stl/css :element-title)} (tr "labels.drafts")]]]]]
|
||||
[:li {:class (stl/css-case :current drafts?
|
||||
:sidebar-nav-item true)}
|
||||
[:& link {:action go-drafts
|
||||
:class (stl/css :sidebar-link)
|
||||
:keyboard-action go-drafts-with-key}
|
||||
[:span {:class (stl/css :element-title)} (tr "labels.drafts")]]]]]
|
||||
|
||||
|
||||
[:div {:class (stl/css :sidebar-content-section)}
|
||||
[:div {:class (stl/css :sidebar-section-title)}
|
||||
(tr "labels.sources")]
|
||||
[:ul {:class (stl/css :sidebar-nav)}
|
||||
[:li {:class (stl/css-case :sidebar-nav-item true
|
||||
:current fonts?)}
|
||||
[:& link {:action go-fonts
|
||||
:class (stl/css :sidebar-link)
|
||||
:keyboard-action go-fonts-with-key
|
||||
:data-testid "fonts"}
|
||||
[:span {:class (stl/css :element-title)} (tr "labels.fonts")]]]
|
||||
[: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")]]]]]
|
||||
[:div {:class (stl/css :sidebar-content-section)}
|
||||
[:div {:class (stl/css :sidebar-section-title)}
|
||||
(tr "labels.sources")]
|
||||
[:ul {:class (stl/css :sidebar-nav)}
|
||||
[:li {:class (stl/css-case :sidebar-nav-item true
|
||||
:current fonts?)}
|
||||
[:& link {:action go-fonts
|
||||
:class (stl/css :sidebar-link)
|
||||
:keyboard-action go-fonts-with-key
|
||||
:data-testid "fonts"}
|
||||
[:span {:class (stl/css :element-title)} (tr "labels.fonts")]]]
|
||||
[: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")]]]]]
|
||||
|
||||
|
||||
[:div {:class (stl/css :sidebar-content-section)
|
||||
:data-testid "pinned-projects"}
|
||||
[:div {:class (stl/css :sidebar-section-title)}
|
||||
(tr "labels.pinned-projects")]
|
||||
(if (some? pinned-projects)
|
||||
[:ul {:class (stl/css :sidebar-nav :pinned-projects)}
|
||||
(for [item pinned-projects]
|
||||
[:> sidebar-project*
|
||||
{:item item
|
||||
:key (dm/str (:id item))
|
||||
:id (:id item)
|
||||
:team-id (:id team)
|
||||
:is-selected (= (:id item) (:id project))}])]
|
||||
[:div {:class (stl/css :sidebar-empty-placeholder)}
|
||||
pin-icon
|
||||
[:span {:class (stl/css :empty-text)} (tr "dashboard.no-projects-placeholder")]])]]
|
||||
[:div {:class (stl/css-case :separator true :overflow-separator overflow?)}]]]))
|
||||
[:div {:class (stl/css :sidebar-content-section)
|
||||
:data-testid "pinned-projects"}
|
||||
[:div {:class (stl/css :sidebar-section-title)}
|
||||
(tr "labels.pinned-projects")]
|
||||
(if (some? pinned-projects)
|
||||
[:ul {:class (stl/css :sidebar-nav :pinned-projects)}
|
||||
(for [item pinned-projects]
|
||||
[:> sidebar-project*
|
||||
{:item item
|
||||
:key (dm/str (:id item))
|
||||
:id (:id item)
|
||||
:team-id (:id team)
|
||||
:is-selected (= (:id item) (:id project))}])]
|
||||
[:div {:class (stl/css :sidebar-empty-placeholder)}
|
||||
pin-icon
|
||||
[:span {:class (stl/css :empty-text)} (tr "dashboard.no-projects-placeholder")]])]]
|
||||
[:div {:class (stl/css-case :separator true :overflow-separator overflow?)}]]))
|
||||
|
||||
(mf/defc help-learning-menu*
|
||||
{::mf/props :obj
|
||||
@@ -1085,13 +1056,10 @@
|
||||
(dom/open-new-window "https://penpot.app/pricing")))]
|
||||
|
||||
[:*
|
||||
(if (contains? cf/flags :nitrate)
|
||||
(when-not (dm/get-in profile [:props :nitrate-license :valid])
|
||||
[:> nitrate-sidebar* {:profile profile}])
|
||||
(when (contains? cf/flags :subscriptions)
|
||||
(if (show-subscription-dashboard-banner? profile)
|
||||
[:> dashboard-cta* {:profile profile}]
|
||||
[:> subscription-sidebar* {:profile profile}])))
|
||||
(when (contains? cf/flags :subscriptions)
|
||||
(if (show-subscription-dashboard-banner? profile)
|
||||
[:> dashboard-cta* {:profile profile}]
|
||||
[:> subscription-sidebar* {:profile profile}]))
|
||||
|
||||
;; TODO remove this block when subscriptions is full implemented
|
||||
(when (contains? cf/flags :subscriptions-old)
|
||||
|
||||
@@ -40,11 +40,6 @@
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidebar-content-nitrate {
|
||||
padding: var(--sp-m) 0 0 0;
|
||||
border-block-start: $b-1 solid var(--color-background-quaternary);
|
||||
}
|
||||
|
||||
.separator {
|
||||
height: var(--sp-xxs);
|
||||
width: 94%;
|
||||
@@ -519,44 +514,3 @@
|
||||
@include t.use-typography("body-small");
|
||||
color: var(--color-accent-tertiary);
|
||||
}
|
||||
|
||||
.nitrate-orgs-container {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: calc(2 * var(--sp-xxxl));
|
||||
max-height: calc(2 * var(--sp-xxxl));
|
||||
justify-content: space-between;
|
||||
padding: var(--sp-xs) var(--sp-l) var(--sp-xs) var(--sp-s);
|
||||
// border-block-end: $b-1 solid var(--color-background-quaternary);
|
||||
}
|
||||
|
||||
.nitrate-orgs-empty {
|
||||
@include t.use-typography("body-medium");
|
||||
color: var(--color-foreground-primary);
|
||||
width: 100%;
|
||||
margin: var(--sp-xs) var(--sp-l);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
|
||||
.nitrate-create-org {
|
||||
margin-inline-start: auto;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.nitrate-penpot-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
height: var(--sp-xxxl);
|
||||
width: var(--sp-xxxl);
|
||||
background-color: var(--color-foreground-primary);
|
||||
|
||||
svg {
|
||||
fill: var(--icon-stroke-color);
|
||||
width: var(--sp-xxl);
|
||||
height: var(--sp-xxl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.config :as cf]
|
||||
[app.main.data.event :as ev]
|
||||
[app.main.data.nitrate :as dnt]
|
||||
[app.main.router :as rt]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown-menu :refer [dropdown-menu-item*]]
|
||||
@@ -116,26 +115,6 @@
|
||||
:has-dropdown false
|
||||
:is-highlighted false}]))))
|
||||
|
||||
(mf/defc nitrate-sidebar*
|
||||
[]
|
||||
(let [handle-click
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(st/emit! (dnt/show-nitrate-popup :nitrate-form))))]
|
||||
|
||||
;; TODO add translations for this texts when we have the definitive ones
|
||||
[:div {:class (stl/css :nitrate-banner :highlighted)}
|
||||
|
||||
[:div {:class (stl/css :nitrate-content)}
|
||||
[:span {:class (stl/css :nitrate-title)} "Unlock Nitrate features"]]
|
||||
[:div {:class (stl/css :nitrate-content)}
|
||||
|
||||
[:span {:class (stl/css :nitrate-info)} "Some further information and explanation."]
|
||||
[:> button* {:variant "primary"
|
||||
:type "button"
|
||||
:class (stl/css :cta-bottom-button :nitrate-bottom-button)
|
||||
:on-click handle-click} "UPGRADE TO NITRATE"]]]))
|
||||
|
||||
(mf/defc team*
|
||||
[{:keys [is-owner team]}]
|
||||
(let [subscription (:subscription team)
|
||||
|
||||
@@ -205,32 +205,3 @@
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.nitrate-banner {
|
||||
display: flex;
|
||||
border-radius: var(--sp-s);
|
||||
flex-direction: column;
|
||||
margin: var(--sp-m);
|
||||
background: var(--color-background-quaternary);
|
||||
border: $b-1 solid var(--color-accent-primary-muted);
|
||||
padding: var(--sp-l);
|
||||
}
|
||||
|
||||
.nitrate-title {
|
||||
@include t.use-typography("body-large");
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
.nitrate-info {
|
||||
@include t.use-typography("body-medium");
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.nitrate-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.nitrate-bottom-button {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ $sz-32: px2rem(32);
|
||||
$sz-36: px2rem(36);
|
||||
$sz-40: px2rem(40);
|
||||
$sz-48: px2rem(48);
|
||||
$sz-64: px2rem(64);
|
||||
$sz-88: px2rem(88);
|
||||
$sz-96: px2rem(96);
|
||||
$sz-120: px2rem(120);
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.main.constants :refer [max-input-length]]
|
||||
[app.main.ui.ds.controls.utilities.hint-message :refer [hint-message*]]
|
||||
[app.main.ui.ds.controls.utilities.input-field :refer [input-field*]]
|
||||
@@ -51,11 +52,10 @@
|
||||
:has-hint has-hint
|
||||
:hint-type hint-type
|
||||
:variant variant})]
|
||||
|
||||
[:div {:class [class (stl/css-case :input-wrapper true
|
||||
:variant-dense (= variant "dense")
|
||||
:variant-comfortable (= variant "comfortable")
|
||||
:has-hint has-hint)]}
|
||||
[:div {:class (dm/str class " " (stl/css-case :input-wrapper true
|
||||
:variant-dense (= variant "dense")
|
||||
:variant-comfortable (= variant "comfortable")
|
||||
:has-hint has-hint))}
|
||||
(when has-label
|
||||
[:> label* {:for id :is-optional is-optional} label])
|
||||
[:> input-field* props]
|
||||
@@ -64,3 +64,4 @@
|
||||
:class hint-class
|
||||
:message hint-message
|
||||
:type hint-type}])]))
|
||||
|
||||
|
||||
@@ -84,7 +84,6 @@
|
||||
:on-click on-icon-click}])
|
||||
(if aria-label
|
||||
[:> tooltip* {:content aria-label
|
||||
:class (stl/css :tooltip-wrapper)
|
||||
:id tooltip-id}
|
||||
[:> "input" props]]
|
||||
[:> "input" props])
|
||||
|
||||
@@ -120,7 +120,3 @@
|
||||
color: var(--color-foreground-secondary);
|
||||
min-inline-size: var(--sp-l);
|
||||
}
|
||||
|
||||
.tooltip-wrapper {
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
(:require
|
||||
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||
[app.main.ui.ds.controls.input :refer [input*]]
|
||||
[app.main.ui.ds.controls.select :refer [select*]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.forms :as fm]
|
||||
[app.util.keyboard :as k]
|
||||
@@ -48,23 +47,6 @@
|
||||
|
||||
[:> input* props]))
|
||||
|
||||
(mf/defc form-select*
|
||||
[{:keys [name] :as props}]
|
||||
(let [select-name name
|
||||
form (mf/use-ctx context)
|
||||
value (get-in @form [:data select-name] "")
|
||||
|
||||
handle-change
|
||||
(fn [event]
|
||||
(let [value (if (string? event) event (dom/get-target-val event))]
|
||||
(fm/on-input-change form select-name value)))
|
||||
|
||||
props
|
||||
(mf/spread-props props {:on-change handle-change
|
||||
:value value})]
|
||||
|
||||
[:> select* props]))
|
||||
|
||||
(mf/defc form-submit*
|
||||
[{:keys [disabled on-submit] :rest props}]
|
||||
(let [form (mf/use-ctx context)
|
||||
@@ -97,4 +79,4 @@
|
||||
(when (fn? on-submit)
|
||||
(on-submit form event))))]
|
||||
[:> (mf/provider context) {:value form}
|
||||
[:form {:class class :on-submit on-submit'} children]]))
|
||||
[:form {:class class :on-submit on-submit'} children]]))
|
||||
@@ -12,7 +12,7 @@
|
||||
[app.common.types.component :as ctk]
|
||||
[app.main.data.viewer :as dv]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.workspace.sidebar.layer-item :refer [layer-item-inner]]
|
||||
[app.main.ui.workspace.sidebar.layer-item :refer [layer-item-inner*]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.keyboard :as kbd]
|
||||
[okulary.core :as l]
|
||||
@@ -26,7 +26,6 @@
|
||||
(mf/defc layer-item
|
||||
[{:keys [item selected objects depth component-child? hide-toggle?] :as props}]
|
||||
(let [id (:id item)
|
||||
hidden? (:hidden item)
|
||||
selected? (contains? selected id)
|
||||
item-ref (mf/use-ref nil)
|
||||
depth (+ depth 1)
|
||||
@@ -68,18 +67,17 @@
|
||||
(when (and (= (count selected) 1) selected?)
|
||||
(dom/scroll-into-view-if-needed! (mf/ref-val item-ref) true))))
|
||||
|
||||
[:& layer-item-inner
|
||||
[:> layer-item-inner*
|
||||
{:ref item-ref
|
||||
:item item
|
||||
:depth depth
|
||||
:read-only? true
|
||||
:highlighted? false
|
||||
:selected? selected?
|
||||
:component-tree? component-tree?
|
||||
:hidden? hidden?
|
||||
:filtered? false
|
||||
:expanded? expanded?
|
||||
:hide-toggle? hide-toggle?
|
||||
:is-read-only true
|
||||
:is-highlighted false
|
||||
:is-selected selected?
|
||||
:is-component-tree component-tree?
|
||||
:is-filtered false
|
||||
:is-expanded expanded?
|
||||
:hide-toggle hide-toggle?
|
||||
:on-select-shape select-shape
|
||||
:on-toggle-collapse toggle-collapse}
|
||||
|
||||
|
||||
@@ -7,86 +7,42 @@
|
||||
(ns app.main.ui.nitrate.nitrate-form
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.schema :as sm]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.ui.components.forms :as fm]
|
||||
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]]
|
||||
[app.main.ui.ds.foundations.assets.raw-svg :refer [raw-svg*]]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.util.dom :as dom]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def ^:private schema:nitrate-form
|
||||
[:map {:title "NitrateForm"}
|
||||
[:subscription [::sm/one-of #{:monthly :yearly}]]])
|
||||
;; FIXME: rename to `form` (remove the nitrate prefix from namespace,
|
||||
;; because it is already under nitrate)
|
||||
|
||||
(mf/defc nitrate-form-modal*
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :nitrate-form
|
||||
::mf/wrap-props true}
|
||||
[connectivity]
|
||||
|
||||
(let [online? (:licenses connectivity)
|
||||
initial (mf/with-memo []
|
||||
{:subscription "yearly"})
|
||||
form (fm/use-form :schema schema:nitrate-form
|
||||
:initial initial)
|
||||
on-click
|
||||
::mf/register-as :nitrate-form}
|
||||
[]
|
||||
(let [on-click
|
||||
(mf/use-fn
|
||||
(mf/deps form)
|
||||
(fn []
|
||||
(let [params (:clean-data @form)]
|
||||
(dom/open-new-window (str "/control-center/licenses/start?subscription=" (name (:subscription params)))))))]
|
||||
(dom/open-new-window "/control-center/licenses/start")))]
|
||||
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div {:class (stl/css :modal-dialog :subscription-success)}
|
||||
[:button {:class (stl/css :close-btn) :on-click modal/hide!}
|
||||
[:> icon* {:icon-id "close"
|
||||
:size "m"}]]
|
||||
[:div {:class (stl/css :modal-success-content)}
|
||||
[:div {:class (stl/css :modal-start)}
|
||||
;; TODO this svg is a placeholder. Use the proper one when created
|
||||
[:> raw-svg* {:id "logo-subscription"}]]
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:div {:class (stl/css :nitrate-form)}
|
||||
|
||||
[:div {:class (stl/css :modal-end)}
|
||||
[:div {:class (stl/css :modal-title)}
|
||||
"Unlock Nitrate Features"]
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h2 {:class (stl/css :modal-title)}
|
||||
"BUY NITRATE"]
|
||||
|
||||
[:p {:class (stl/css :modal-text-large)}
|
||||
"Prow scuttle parrel provost."]
|
||||
[:p {:class (stl/css :modal-text-large)}
|
||||
"Sail ho shrouds spirits boom mizzenmast yardarm. Pinnace holystone mizzenmast quarter crow's nest nipperkin grog yardarm hempen halter furl."]
|
||||
[:p {:class (stl/css :modal-text-large)}
|
||||
"Deadlights jack lad schooner scallywag dance the hempen jig carouser broadside cable strike colors."]
|
||||
(if online?
|
||||
[:& fm/form {:form form}
|
||||
[:p {:class (stl/css :modal-text-large)}
|
||||
[:button {:class (stl/css :modal-close-btn)
|
||||
:on-click modal/hide!} deprecated-icon/close]]
|
||||
|
||||
[:& fm/radio-buttons
|
||||
{:options [{:label "Price Tag Montly" :value "monthly"}
|
||||
{:label "Price Tag Yearly (Discount)" :value "yearly"}]
|
||||
:name :subscription
|
||||
:class (stl/css :radio-btns)}]]
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
"Nitrate is so cool! You should buy it!"]
|
||||
|
||||
[:p {:class (stl/css :modal-text-large :modal-buttons-section)}
|
||||
[:div {:class (stl/css :modal-buttons-section)}
|
||||
[:> button* {:variant "primary"
|
||||
:on-click on-click
|
||||
:class (stl/css :modal-button)}
|
||||
"UPGRADE TO NITRATE"]
|
||||
[:div {:class (stl/css :modal-text-small :modal-info)}
|
||||
"Cancel anytime before your next billing cycle."]]]
|
||||
|
||||
|
||||
[:p {:class (stl/css :modal-text-medium)}
|
||||
[:a {:class (stl/css :link)}
|
||||
"See my current plan"]]]
|
||||
|
||||
[:div {:class (stl/css :contact)}
|
||||
[:p {:class (stl/css :modal-text-large)}
|
||||
"Contact us to upgrade to Nitrate:"]
|
||||
[:p {:class (stl/css :modal-text-large)}
|
||||
[:a {:class (stl/css :link) :href "mailto:sales@penpot.app"}
|
||||
"sales@penpot.app"]]])]]]]))
|
||||
[:div {:class (stl/css :modal-footer)}
|
||||
[:div {:class (stl/css :action-buttons)}
|
||||
[:> button* {:variant "primary"
|
||||
:on-click on-click}
|
||||
"BUY NOW!"]]]]]]))
|
||||
|
||||
|
||||
|
||||
@@ -5,94 +5,48 @@
|
||||
// Copyright (c) KALEIDOS INC
|
||||
|
||||
@use "refactor/common-refactor.scss" as deprecated;
|
||||
@use "ds/typography.scss" as t;
|
||||
@use "ds/_borders.scss" as *;
|
||||
@use "ds/spacing.scss" as *;
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@use "ds/_utils.scss" as *;
|
||||
|
||||
.modal-overlay {
|
||||
@extend .modal-overlay-base;
|
||||
z-index: var(--z-index-notifications);
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
.modal-container {
|
||||
@extend .modal-container-base;
|
||||
max-block-size: initial;
|
||||
min-inline-size: px2rem(648);
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
@extend .modal-close-btn-base;
|
||||
.modal-header {
|
||||
margin-bottom: deprecated.$s-24;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
@include t.use-typography("title-large");
|
||||
margin-block-end: var(--sp-xxxl);
|
||||
@include deprecated.uppercaseTitleTipography;
|
||||
color: var(--modal-title-foreground-color);
|
||||
display: flex;
|
||||
gap: var(--sp-m);
|
||||
}
|
||||
|
||||
.modal-text-large {
|
||||
@include t.use-typography("body-large");
|
||||
.modal-close-btn {
|
||||
@extend .modal-close-btn-base;
|
||||
}
|
||||
|
||||
.modal-text-medium {
|
||||
@include t.use-typography("body-medium");
|
||||
.modal-content {
|
||||
margin-bottom: deprecated.$s-24;
|
||||
}
|
||||
|
||||
.modal-text-small {
|
||||
@include t.use-typography("body-small");
|
||||
.nitrate-form {
|
||||
min-width: deprecated.$s-400;
|
||||
}
|
||||
|
||||
.modal-info {
|
||||
margin-block-start: var(--sp-s);
|
||||
width: 40%;
|
||||
.action-buttons {
|
||||
@extend .modal-action-btns;
|
||||
}
|
||||
|
||||
.modal-content,
|
||||
.modal-end {
|
||||
color: var(--color-foreground-secondary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.cancel-button {
|
||||
@extend .modal-cancel-btn;
|
||||
}
|
||||
|
||||
.modal-success-content {
|
||||
display: flex;
|
||||
gap: $sz-40;
|
||||
}
|
||||
.accept-btn {
|
||||
@extend .modal-accept-btn;
|
||||
|
||||
.modal-start {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
max-inline-size: $sz-224;
|
||||
|
||||
svg {
|
||||
inline-size: 100%;
|
||||
block-size: auto;
|
||||
}
|
||||
|
||||
@media (max-inline-size: 992px) {
|
||||
display: none;
|
||||
&.danger {
|
||||
@extend .modal-danger-btn;
|
||||
}
|
||||
}
|
||||
|
||||
.radio-btns {
|
||||
label {
|
||||
@include t.use-typography("body-large");
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--sp-l) 0 0 0;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.contact {
|
||||
margin-block-start: $sz-96;
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
["/feedback" :settings-feedback]
|
||||
["/options" :settings-options]
|
||||
["/subscriptions" :settings-subscription]
|
||||
["/integrations" :settings-integrations]
|
||||
["/access-tokens" :settings-access-tokens]
|
||||
["/notifications" :settings-notifications]]
|
||||
|
||||
["/frame-preview" :frame-preview]
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[app.main.ui.modal :refer [modal-container*]]
|
||||
[app.main.ui.settings.access-tokens :refer [access-tokens-page]]
|
||||
[app.main.ui.settings.change-email]
|
||||
[app.main.ui.settings.delete-account]
|
||||
[app.main.ui.settings.feedback :refer [feedback-page*]]
|
||||
[app.main.ui.settings.integrations :refer [integrations-page*]]
|
||||
[app.main.ui.settings.notifications :refer [notifications-page*]]
|
||||
[app.main.ui.settings.options :refer [options-page]]
|
||||
[app.main.ui.settings.password :refer [password-page]]
|
||||
@@ -73,8 +73,8 @@
|
||||
:settings-subscription
|
||||
[:> subscription-page* {:profile profile}]
|
||||
|
||||
:settings-integrations
|
||||
[:> integrations-page*]
|
||||
:settings-access-tokens
|
||||
[:& access-tokens-page]
|
||||
|
||||
:settings-notifications
|
||||
[:& notifications-page* {:profile profile}])]]]]))
|
||||
|
||||
291
frontend/src/app/main/ui/settings/access_tokens.cljs
Normal file
291
frontend/src/app/main/ui/settings/access_tokens.cljs
Normal file
@@ -0,0 +1,291 @@
|
||||
;; 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 app.main.ui.settings.access-tokens
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.data.profile :as du]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.context-menu-a11y :refer [context-menu*]]
|
||||
[app.main.ui.components.forms :as fm]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.util.clipboard :as clipboard]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[okulary.core :as l]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def ^:private clipboard-icon
|
||||
(deprecated-icon/icon-xref :clipboard (stl/css :clipboard-icon)))
|
||||
|
||||
(def ^:private close-icon
|
||||
(deprecated-icon/icon-xref :close (stl/css :close-icon)))
|
||||
|
||||
(def ^:private menu-icon
|
||||
(deprecated-icon/icon-xref :menu (stl/css :menu-icon)))
|
||||
|
||||
(def tokens-ref
|
||||
(l/derived :access-tokens st/state))
|
||||
|
||||
(def token-created-ref
|
||||
(l/derived :access-token-created st/state))
|
||||
|
||||
(def ^:private schema:form
|
||||
[:map {:title "AccessTokenForm"}
|
||||
[:name [::sm/text {:max 250}]]
|
||||
[:expiration-date [::sm/text {:max 250}]]])
|
||||
|
||||
(def initial-data
|
||||
{:name "" :expiration-date "never"})
|
||||
|
||||
(mf/defc access-token-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :access-token}
|
||||
[]
|
||||
(let [form (fm/use-form
|
||||
:initial initial-data
|
||||
:schema schema:form)
|
||||
|
||||
created (mf/deref token-created-ref)
|
||||
created? (mf/use-state false)
|
||||
|
||||
on-success
|
||||
(mf/use-fn
|
||||
(mf/deps created)
|
||||
(fn [_]
|
||||
(let [message (tr "dashboard.access-tokens.create.success")]
|
||||
(st/emit! (du/fetch-access-tokens)
|
||||
(ntf/success message)
|
||||
(reset! created? true)))))
|
||||
|
||||
on-close
|
||||
(mf/use-fn
|
||||
(mf/deps created)
|
||||
(fn [_]
|
||||
(reset! created? false)
|
||||
(st/emit! (modal/hide))))
|
||||
|
||||
on-error
|
||||
(mf/use-fn
|
||||
(fn [_]
|
||||
(st/emit! (ntf/error (tr "errors.generic"))
|
||||
(modal/hide))))
|
||||
|
||||
on-submit
|
||||
(mf/use-fn
|
||||
(fn [form]
|
||||
(let [cdata (:clean-data @form)
|
||||
mdata {:on-success (partial on-success form)
|
||||
:on-error (partial on-error form)}
|
||||
expiration (:expiration-date cdata)
|
||||
params (cond-> {:name (:name cdata)
|
||||
:perms (:perms cdata)}
|
||||
(not= "never" expiration) (assoc :expiration expiration))]
|
||||
(st/emit! (du/create-access-token
|
||||
(with-meta params mdata))))))
|
||||
|
||||
copy-token
|
||||
(mf/use-fn
|
||||
(mf/deps created)
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(clipboard/to-clipboard (:token created))
|
||||
(st/emit! (ntf/show {:level :info
|
||||
:type :toast
|
||||
:content (tr "dashboard.access-tokens.copied-success")
|
||||
:timeout 7000}))))]
|
||||
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:& fm/form {:form form :on-submit on-submit}
|
||||
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h2 {:class (stl/css :modal-title)} (tr "modals.create-access-token.title")]
|
||||
|
||||
[:button {:class (stl/css :modal-close-btn)
|
||||
:on-click on-close}
|
||||
close-icon]]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :fields-row)}
|
||||
[:& fm/input {:type "text"
|
||||
:auto-focus? true
|
||||
:form form
|
||||
:name :name
|
||||
:disabled @created?
|
||||
:label (tr "modals.create-access-token.name.label")
|
||||
:show-success? true
|
||||
:placeholder (tr "modals.create-access-token.name.placeholder")}]]
|
||||
|
||||
[:div {:class (stl/css :fields-row)}
|
||||
[:div {:class (stl/css :select-title)}
|
||||
(tr "modals.create-access-token.expiration-date.label")]
|
||||
[:& fm/select {:options [{:label (tr "dashboard.access-tokens.expiration-never") :value "never" :key "never"}
|
||||
{:label (tr "dashboard.access-tokens.expiration-30-days") :value "720h" :key "720h"}
|
||||
{:label (tr "dashboard.access-tokens.expiration-60-days") :value "1440h" :key "1440h"}
|
||||
{:label (tr "dashboard.access-tokens.expiration-90-days") :value "2160h" :key "2160h"}
|
||||
{:label (tr "dashboard.access-tokens.expiration-180-days") :value "4320h" :key "4320h"}]
|
||||
:default "never"
|
||||
:disabled @created?
|
||||
:name :expiration-date}]
|
||||
(when @created?
|
||||
[:span {:class (stl/css :token-created-info)}
|
||||
(if (:expires-at created)
|
||||
(tr "dashboard.access-tokens.token-will-expire" (ct/format-inst (:expires-at created) "PPP"))
|
||||
(tr "dashboard.access-tokens.token-will-not-expire"))])]
|
||||
|
||||
[:div {:class (stl/css :fields-row)}
|
||||
(when @created?
|
||||
[:div {:class (stl/css :custon-input-wrapper)}
|
||||
[:input {:type "text"
|
||||
:value (:token created "")
|
||||
:class (stl/css :custom-input-token)
|
||||
:read-only true}]
|
||||
[:button {:title (tr "modals.create-access-token.copy-token")
|
||||
:class (stl/css :copy-btn)
|
||||
:on-click copy-token}
|
||||
clipboard-icon]])
|
||||
#_(when @created?
|
||||
[:button {:class (stl/css :copy-btn)
|
||||
:title (tr "modals.create-access-token.copy-token")
|
||||
:on-click copy-token}
|
||||
[:span {:class (stl/css :token-value)} (:token created "")]
|
||||
[:span {:class (stl/css :icon)}
|
||||
i/clipboard]])]]
|
||||
|
||||
[:div {:class (stl/css :modal-footer)}
|
||||
[:div {:class (stl/css :action-buttons)}
|
||||
|
||||
(if @created?
|
||||
[:input {:class (stl/css :cancel-button)
|
||||
:type "button"
|
||||
:value (tr "labels.close")
|
||||
:on-click modal/hide!}]
|
||||
[:*
|
||||
[:input {:class (stl/css :cancel-button)
|
||||
:type "button"
|
||||
:value (tr "labels.cancel")
|
||||
:on-click modal/hide!}]
|
||||
[:> fm/submit-button*
|
||||
{:large? false :label (tr "modals.create-access-token.submit-label")}]])]]]]]))
|
||||
|
||||
(mf/defc access-tokens-hero
|
||||
[]
|
||||
(let [on-click (mf/use-fn #(st/emit! (modal/show :access-token {})))]
|
||||
[:div {:class (stl/css :access-tokens-hero)}
|
||||
[:h2 {:class (stl/css :hero-title)} (tr "dashboard.access-tokens.personal")]
|
||||
[:p {:class (stl/css :hero-desc)} (tr "dashboard.access-tokens.personal.description")]
|
||||
|
||||
[:button {:class (stl/css :hero-btn)
|
||||
:on-click on-click}
|
||||
(tr "dashboard.access-tokens.create")]]))
|
||||
|
||||
(mf/defc access-token-actions
|
||||
[{:keys [on-delete]}]
|
||||
(let [local (mf/use-state {:menu-open false})
|
||||
show? (:menu-open @local)
|
||||
options (mf/with-memo [on-delete]
|
||||
[{:name (tr "labels.delete")
|
||||
:id "access-token-delete"
|
||||
:handler on-delete}])
|
||||
|
||||
menu-ref (mf/use-ref)
|
||||
|
||||
on-menu-close
|
||||
(mf/use-fn #(swap! local assoc :menu-open false))
|
||||
|
||||
on-menu-click
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(swap! local assoc :menu-open true)))
|
||||
|
||||
on-keydown
|
||||
(mf/use-fn
|
||||
(mf/deps on-menu-click)
|
||||
(fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(dom/stop-propagation event)
|
||||
(on-menu-click event))))]
|
||||
|
||||
[:button {:class (stl/css :menu-btn)
|
||||
:tab-index "0"
|
||||
:ref menu-ref
|
||||
:on-click on-menu-click
|
||||
:on-key-down on-keydown}
|
||||
menu-icon
|
||||
[:> context-menu*
|
||||
{:on-close on-menu-close
|
||||
:show show?
|
||||
:fixed true
|
||||
:min-width true
|
||||
:top "auto"
|
||||
:left "auto"
|
||||
:options options}]]))
|
||||
|
||||
(mf/defc access-token-item
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [token] :as props}]
|
||||
(let [expires-at (:expires-at token)
|
||||
expires-txt (some-> expires-at (ct/format-inst "PPP"))
|
||||
expired? (and (some? expires-at) (> (ct/now) expires-at))
|
||||
|
||||
delete-fn
|
||||
(mf/use-fn
|
||||
(mf/deps token)
|
||||
(fn []
|
||||
(let [params {:id (:id token)}
|
||||
mdata {:on-success #(st/emit! (du/fetch-access-tokens))}]
|
||||
(st/emit! (du/delete-access-token (with-meta params mdata))))))
|
||||
|
||||
on-delete
|
||||
(mf/use-fn
|
||||
(mf/deps delete-fn)
|
||||
(fn []
|
||||
(st/emit! (modal/show
|
||||
{:type :confirm
|
||||
:title (tr "modals.delete-acces-token.title")
|
||||
:message (tr "modals.delete-acces-token.message")
|
||||
:accept-label (tr "modals.delete-acces-token.accept")
|
||||
:on-accept delete-fn}))))]
|
||||
|
||||
[:div {:class (stl/css :table-row)}
|
||||
[:div {:class (stl/css :table-field :field-name)}
|
||||
(str (:name token))]
|
||||
|
||||
[:div {:class (stl/css-case :expiration-date true
|
||||
:expired expired?)}
|
||||
(cond
|
||||
(nil? expires-at) (tr "dashboard.access-tokens.no-expiration")
|
||||
expired? (tr "dashboard.access-tokens.expired-on" expires-txt)
|
||||
:else (tr "dashboard.access-tokens.expires-on" expires-txt))]
|
||||
[:div {:class (stl/css :table-field :actions)}
|
||||
[:& access-token-actions
|
||||
{:on-delete on-delete}]]]))
|
||||
|
||||
(mf/defc access-tokens-page
|
||||
[]
|
||||
(let [tokens (mf/deref tokens-ref)]
|
||||
(mf/with-effect []
|
||||
(dom/set-html-title (tr "title.settings.access-tokens"))
|
||||
(st/emit! (du/fetch-access-tokens)))
|
||||
|
||||
[:div {:class (stl/css :dashboard-access-tokens)}
|
||||
[:& access-tokens-hero]
|
||||
(if (empty? tokens)
|
||||
[:div {:class (stl/css :access-tokens-empty)}
|
||||
[:div (tr "dashboard.access-tokens.empty.no-access-tokens")]
|
||||
[:div (tr "dashboard.access-tokens.empty.add-one")]]
|
||||
[:div {:class (stl/css :dashboard-table)}
|
||||
[:div {:class (stl/css :table-rows)}
|
||||
(for [token tokens]
|
||||
[:& access-token-item {:token token :key (:id token)}])]])]))
|
||||
|
||||
202
frontend/src/app/main/ui/settings/access_tokens.scss
Normal file
202
frontend/src/app/main/ui/settings/access_tokens.scss
Normal file
@@ -0,0 +1,202 @@
|
||||
// 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
|
||||
|
||||
@use "refactor/common-refactor.scss" as deprecated;
|
||||
|
||||
// ACCESS TOKENS PAGE
|
||||
.dashboard-access-tokens {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
margin: deprecated.$s-80 auto deprecated.$s-120 auto;
|
||||
gap: deprecated.$s-32;
|
||||
width: deprecated.$s-800;
|
||||
}
|
||||
|
||||
// hero
|
||||
.access-tokens-hero {
|
||||
display: grid;
|
||||
grid-template-rows: auto auto 1fr;
|
||||
gap: deprecated.$s-32;
|
||||
width: deprecated.$s-500;
|
||||
font-size: deprecated.$fs-14;
|
||||
margin: deprecated.$s-16 auto 0 auto;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
@include deprecated.bigTitleTipography;
|
||||
color: var(--title-foreground-color-hover);
|
||||
}
|
||||
|
||||
.hero-desc {
|
||||
color: var(--title-foreground-color);
|
||||
margin-bottom: 0;
|
||||
font-size: deprecated.$fs-14;
|
||||
}
|
||||
|
||||
.hero-btn {
|
||||
@extend .button-primary;
|
||||
}
|
||||
|
||||
// table empty
|
||||
.access-tokens-empty {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
align-content: center;
|
||||
height: deprecated.$s-156;
|
||||
max-width: deprecated.$s-1000;
|
||||
width: 100%;
|
||||
padding: deprecated.$s-32;
|
||||
border: deprecated.$s-1 solid var(--panel-border-color);
|
||||
border-radius: deprecated.$br-8;
|
||||
color: var(--dashboard-list-text-foreground-color);
|
||||
}
|
||||
|
||||
// Access tokens table
|
||||
.dashboard-table {
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.table-rows {
|
||||
display: grid;
|
||||
grid-auto-rows: deprecated.$s-64;
|
||||
gap: deprecated.$s-16;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: deprecated.$s-1000;
|
||||
margin-top: deprecated.$s-16;
|
||||
color: var(--title-foreground-color);
|
||||
}
|
||||
|
||||
.table-row {
|
||||
display: grid;
|
||||
grid-template-columns: 43% 1fr auto;
|
||||
align-items: center;
|
||||
height: deprecated.$s-64;
|
||||
width: 100%;
|
||||
padding: 0 deprecated.$s-16;
|
||||
border-radius: deprecated.$br-8;
|
||||
background-color: var(--dashboard-list-background-color);
|
||||
color: var(--dashboard-list-foreground-color);
|
||||
}
|
||||
|
||||
.field-name {
|
||||
@include deprecated.textEllipsis;
|
||||
display: grid;
|
||||
width: 43%;
|
||||
min-width: deprecated.$s-300;
|
||||
}
|
||||
|
||||
.expiration-date {
|
||||
@include deprecated.flexCenter;
|
||||
min-width: deprecated.$s-76;
|
||||
width: fit-content;
|
||||
height: deprecated.$s-24;
|
||||
border-radius: deprecated.$br-8;
|
||||
color: var(--dashboard-list-text-foreground-color);
|
||||
}
|
||||
|
||||
.expired {
|
||||
@include deprecated.headlineSmallTypography;
|
||||
padding: 0 deprecated.$s-6;
|
||||
color: var(--pill-foreground-color);
|
||||
background-color: var(--status-widget-background-color-warning);
|
||||
}
|
||||
|
||||
.actions {
|
||||
position: relative;
|
||||
}
|
||||
.menu-icon {
|
||||
@extend .button-icon;
|
||||
stroke: var(--icon-foreground);
|
||||
}
|
||||
|
||||
.menu-btn {
|
||||
@include deprecated.buttonStyle;
|
||||
}
|
||||
|
||||
// Create access token modal
|
||||
.modal-overlay {
|
||||
@extend .modal-overlay-base;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
@extend .modal-container-base;
|
||||
min-width: deprecated.$s-408;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
margin-bottom: deprecated.$s-24;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
@include deprecated.uppercaseTitleTipography;
|
||||
color: var(--modal-title-foreground-color);
|
||||
}
|
||||
.modal-close-btn {
|
||||
@extend .modal-close-btn-base;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
@include deprecated.flexColumn;
|
||||
gap: deprecated.$s-24;
|
||||
@include deprecated.bodySmallTypography;
|
||||
margin-bottom: deprecated.$s-24;
|
||||
}
|
||||
|
||||
.select-title {
|
||||
@include deprecated.bodySmallTypography;
|
||||
color: var(--modal-title-foreground-color);
|
||||
}
|
||||
|
||||
.custon-input-wrapper {
|
||||
@include deprecated.flexRow;
|
||||
border-radius: deprecated.$br-8;
|
||||
height: deprecated.$s-32;
|
||||
background-color: var(--input-background-color);
|
||||
}
|
||||
|
||||
.custom-input-token {
|
||||
@extend .input-element;
|
||||
@include deprecated.bodySmallTypography;
|
||||
margin: 0;
|
||||
flex-grow: 1;
|
||||
&:focus {
|
||||
outline: none;
|
||||
border: deprecated.$s-1 solid var(--input-border-color-active);
|
||||
}
|
||||
}
|
||||
|
||||
.token-value {
|
||||
@include deprecated.textEllipsis;
|
||||
@include deprecated.bodySmallTypography;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
@include deprecated.flexCenter;
|
||||
@extend .button-secondary;
|
||||
height: deprecated.$s-28;
|
||||
width: deprecated.$s-28;
|
||||
}
|
||||
|
||||
.clipboard-icon {
|
||||
@extend .button-icon-small;
|
||||
}
|
||||
|
||||
.token-created-info {
|
||||
color: var(--modal-text-foreground-color);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
@extend .modal-action-btns;
|
||||
button {
|
||||
@extend .modal-accept-btn;
|
||||
}
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
@extend .modal-cancel-btn;
|
||||
}
|
||||
@@ -1,619 +0,0 @@
|
||||
;; 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 app.main.ui.settings.integrations
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.config :as cf]
|
||||
[app.main.data.event :as ev]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.data.profile :as du]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.context-menu-a11y :refer [context-menu*]]
|
||||
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.controls.input :refer [input*]]
|
||||
[app.main.ui.ds.controls.switch :refer [switch*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]]
|
||||
[app.main.ui.ds.foundations.typography :as t]
|
||||
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
|
||||
[app.main.ui.ds.foundations.typography.text :refer [text*]]
|
||||
[app.main.ui.ds.notifications.shared.notification-pill :refer [notification-pill*]]
|
||||
[app.main.ui.ds.tooltip :refer [tooltip*]]
|
||||
[app.main.ui.forms :as fc]
|
||||
[app.util.clipboard :as clipboard]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.forms :as fm]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[okulary.core :as l]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def tokens-ref
|
||||
(l/derived :access-tokens st/state))
|
||||
|
||||
(def token-created-ref
|
||||
(l/derived :access-token-created st/state))
|
||||
|
||||
(def notification-timeout 7000)
|
||||
|
||||
(def ^:private schema:form-access-token
|
||||
[:map
|
||||
[:name [::sm/text {:max 250}]]
|
||||
[:expiration-date [::sm/text {:max 250}]]])
|
||||
|
||||
(def ^:private schema:form-mcp-key
|
||||
[:map
|
||||
[:expiration-date [::sm/text {:max 250}]]])
|
||||
|
||||
(def form-initial-data-access-token
|
||||
{:name ""
|
||||
:expiration-date "never"})
|
||||
|
||||
(def form-initial-data-mcp-key
|
||||
{:expiration-date "never"})
|
||||
|
||||
(mf/defc input-copy*
|
||||
{::mf/private true}
|
||||
[{:keys [value on-copy-to-clipboard]}]
|
||||
[:div {:class (stl/css :input-copy)}
|
||||
[:> input* {:type "text"
|
||||
:default-value value
|
||||
:read-only true}]
|
||||
[:div {:class (stl/css :input-copy-button-wrapper)}
|
||||
[:> icon-button* {:variant "secondary"
|
||||
:class (stl/css :input-copy-button)
|
||||
:aria-label (tr "integrations.copy-to-clipboard")
|
||||
:on-click on-copy-to-clipboard
|
||||
:icon i/clipboard}]]])
|
||||
|
||||
(mf/defc token-created*
|
||||
{::mf/private true}
|
||||
[{:keys [title mcp-key?]}]
|
||||
(let [token-created (mf/deref token-created-ref)
|
||||
|
||||
on-copy-to-clipboard
|
||||
(mf/use-fn
|
||||
(mf/deps token-created)
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(clipboard/to-clipboard (:token token-created))
|
||||
(st/emit! (ntf/show {:level :info
|
||||
:type :toast
|
||||
:content (tr "integrations.notification.success.copied")
|
||||
:timeout notification-timeout}))))]
|
||||
|
||||
[:div {:class (stl/css :modal-form)}
|
||||
[:> text* {:as "h2"
|
||||
:typography t/headline-large
|
||||
:class (stl/css :color-primary)}
|
||||
title]
|
||||
|
||||
[:> notification-pill* {:level :info
|
||||
:type :context}
|
||||
[:> text* {:as "div"
|
||||
:typography t/body-small
|
||||
:class (stl/css :color-primary)}
|
||||
(tr "integrations.info.non-recuperable")]]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:> input-copy* {:value (:token token-created "")
|
||||
:on-copy-to-clipboard on-copy-to-clipboard}]
|
||||
|
||||
[:> text* {:as "div"
|
||||
:typography t/body-small
|
||||
:class (stl/css :color-secondary)}
|
||||
(if (:expires-at token-created)
|
||||
(tr "integrations.token-will-expire" (ct/format-inst (:expires-at token-created) "PPP"))
|
||||
(tr "integrations.token-will-not-expire"))]]
|
||||
|
||||
(when mcp-key?
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:> text* {:as "div"
|
||||
:typography t/body-small
|
||||
:class (stl/css :color-primary)}
|
||||
(tr "integrations.info.mcp-client-config")]
|
||||
[:textarea {:class (stl/css :textarea)
|
||||
:wrap "off"
|
||||
:rows 7
|
||||
:read-only true}
|
||||
(dm/str
|
||||
"{\n"
|
||||
" \"mcpServers\": {\n"
|
||||
" \"penpot\": {\n"
|
||||
" \"url\": \"" cf/mcp-server-url "?userToken=" (:token token-created "") "\"\n"
|
||||
" }\n"
|
||||
" }"
|
||||
"\n}")]])
|
||||
|
||||
[:div {:class (stl/css :modal-footer)}
|
||||
[:> button* {:variant "secondary"
|
||||
:on-click modal/hide!}
|
||||
(tr "labels.close")]]]))
|
||||
|
||||
(mf/defc create-token*
|
||||
{::mf/private true}
|
||||
[{:keys [title info mcp-key? on-created]}]
|
||||
(let [form (fm/use-form
|
||||
:initial (if mcp-key?
|
||||
form-initial-data-mcp-key
|
||||
form-initial-data-access-token)
|
||||
:schema (if mcp-key?
|
||||
schema:form-mcp-key
|
||||
schema:form-access-token))
|
||||
|
||||
on-error
|
||||
(mf/use-fn
|
||||
#(st/emit! (ntf/error (tr "errors.generic"))
|
||||
(modal/hide)))
|
||||
|
||||
on-success
|
||||
(mf/use-fn
|
||||
#(st/emit! (du/fetch-access-tokens)
|
||||
(ntf/success (tr "integrations.notification.success.created"))
|
||||
(on-created)))
|
||||
|
||||
on-submit
|
||||
(mf/use-fn
|
||||
(fn [form]
|
||||
(let [cdata (:clean-data @form)
|
||||
mdata {:on-success (partial on-success form)
|
||||
:on-error (partial on-error form)}
|
||||
expiration (:expiration-date cdata)
|
||||
params (cond-> {:name (:name cdata)
|
||||
:perms (:perms cdata)}
|
||||
(not= "never" expiration) (assoc :expiration expiration)
|
||||
(true? mcp-key?) (assoc :type "mcp"
|
||||
:name "MCP key"))]
|
||||
(st/emit! (du/create-access-token (with-meta params mdata))))))]
|
||||
|
||||
[:> fc/form* {:form form
|
||||
:class (stl/css :modal-form)
|
||||
:on-submit on-submit}
|
||||
|
||||
[:> text* {:as "h2"
|
||||
:typography t/headline-large
|
||||
:class (stl/css :color-primary)}
|
||||
title]
|
||||
|
||||
(when (some? info)
|
||||
[:> notification-pill* {:level :info
|
||||
:type :context}
|
||||
[:> text* {:as "div"
|
||||
:typography t/body-small
|
||||
:class (stl/css :color-primary)}
|
||||
info]])
|
||||
|
||||
(if mcp-key?
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:> text* {:as "div"
|
||||
:typography t/body-medium
|
||||
:class (stl/css :color-secondary)}
|
||||
(tr "integrations.info.mcp-server")]]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:> fc/form-input* {:type "text"
|
||||
:auto-focus? true
|
||||
:form form
|
||||
:name :name
|
||||
:label (tr "integrations.name.label")
|
||||
:placeholder (tr "integrations.name.placeholder")}]])
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:> text* {:as "label"
|
||||
:typography t/body-small
|
||||
:for :expiration-date
|
||||
:class (stl/css :color-primary)}
|
||||
(tr "integrations.expiration-date.label")]
|
||||
[:> fc/form-select* {:options [{:label (tr "integrations.expiration-never") :value "never" :id "never"}
|
||||
{:label (tr "integrations.expiration-30-days") :value "720h" :id "720h"}
|
||||
{:label (tr "integrations.expiration-60-days") :value "1440h" :id "1440h"}
|
||||
{:label (tr "integrations.expiration-90-days") :value "2160h" :id "2160h"}
|
||||
{:label (tr "integrations.expiration-180-days") :value "4320h" :id "4320h"}]
|
||||
:default-selected "never"
|
||||
:name :expiration-date}]]
|
||||
|
||||
[:div {:class (stl/css :modal-footer)}
|
||||
[:> button* {:variant "secondary"
|
||||
:on-click modal/hide!}
|
||||
(tr "labels.cancel")]
|
||||
[:> fc/form-submit* {:variant "primary"}
|
||||
title]]]))
|
||||
|
||||
(mf/defc create-access-token-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :create-access-token}
|
||||
[]
|
||||
(let [created? (mf/use-state false)
|
||||
|
||||
on-close
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(reset! created? false)
|
||||
(st/emit! (modal/hide))))
|
||||
|
||||
on-created
|
||||
(mf/use-fn
|
||||
#(reset! created? true))]
|
||||
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:div {:class (stl/css :modal-close-button)}
|
||||
[:> icon-button* {:variant "ghost"
|
||||
:aria-label (tr "labels.close")
|
||||
:on-click on-close
|
||||
:icon i/close}]]
|
||||
|
||||
(if @created?
|
||||
[:> token-created* {:title (tr "integrations.create-access-token.title.created")}]
|
||||
[:> create-token* {:title (tr "integrations.create-access-token.title")
|
||||
:on-created on-created}])]]))
|
||||
|
||||
(mf/defc generate-mcp-key-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :generate-mcp-key}
|
||||
[]
|
||||
(let [created? (mf/use-state false)
|
||||
|
||||
on-close
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(reset! created? false)
|
||||
(st/emit! (modal/hide))))
|
||||
|
||||
on-created
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(st/emit! (du/update-profile-props {:mcp-status true})
|
||||
(ev/event {::ev/name "generate-mcp-key"
|
||||
::ev/origin "integrations"})
|
||||
(ev/event {::ev/name "enable-mcp"
|
||||
::ev/origin "integrations"
|
||||
:source "key-creation"}))
|
||||
(reset! created? true)))]
|
||||
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:div {:class (stl/css :modal-close-button)}
|
||||
[:> icon-button* {:variant "ghost"
|
||||
:aria-label (tr "labels.close")
|
||||
:on-click on-close
|
||||
:icon i/close}]]
|
||||
|
||||
(if @created?
|
||||
[:> token-created* {:title (tr "integrations.generate-mcp-key.title.created")
|
||||
:mcp-key? true}]
|
||||
[:> create-token* {:title (tr "integrations.generate-mcp-key.title")
|
||||
:mcp-key? true
|
||||
:on-created on-created}])]]))
|
||||
|
||||
(mf/defc regenerate-mcp-key-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :regenerate-mcp-key}
|
||||
[]
|
||||
(let [created? (mf/use-state false)
|
||||
|
||||
tokens (mf/deref tokens-ref)
|
||||
mcp-key (some #(when (= (:type %) "mcp") %) tokens)
|
||||
mcp-key-id (:id mcp-key)
|
||||
|
||||
on-close
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(reset! created? false)
|
||||
(st/emit! (modal/hide))))
|
||||
|
||||
on-created
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(st/emit! (du/delete-access-token {:id mcp-key-id})
|
||||
(du/update-profile-props {:mcp-status true})
|
||||
(ev/event {::ev/name "regenerate-mcp-key"
|
||||
::ev/origin "integrations"}))
|
||||
(reset! created? true)))]
|
||||
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:div {:class (stl/css :modal-close-button)}
|
||||
[:> icon-button* {:variant "ghost"
|
||||
:aria-label (tr "labels.close")
|
||||
:on-click on-close
|
||||
:icon i/close}]]
|
||||
|
||||
(if @created?
|
||||
[:> token-created* {:title (tr "integrations.regenerate-mcp-key.title.created")
|
||||
:mcp-key? true}]
|
||||
[:> create-token* {:title (tr "integrations.regenerate-mcp-key.title")
|
||||
:info (tr "integrations.regenerate-mcp-key.info")
|
||||
:mcp-key? true
|
||||
:on-created on-created}])]]))
|
||||
|
||||
(mf/defc token-item*
|
||||
{::mf/private true
|
||||
::mf/wrap [mf/memo]}
|
||||
[{:keys [name expires-at on-delete]}]
|
||||
(let [expires-txt (some-> expires-at (ct/format-inst "PPP"))
|
||||
expired? (and (some? expires-at) (> (ct/now) expires-at))
|
||||
|
||||
menu-open* (mf/use-state false)
|
||||
menu-open? (deref menu-open*)
|
||||
|
||||
handle-menu-close
|
||||
(mf/use-fn
|
||||
#(reset! menu-open* false))
|
||||
|
||||
handle-menu-click
|
||||
(mf/use-fn
|
||||
#(reset! menu-open* (not menu-open?)))
|
||||
|
||||
handle-open-confirm-modal
|
||||
(mf/use-fn
|
||||
(mf/deps on-delete)
|
||||
(fn []
|
||||
(st/emit! (modal/show {:type :confirm
|
||||
:title (tr "integrations.delete-token.title")
|
||||
:message (tr "integrations.delete-token.message")
|
||||
:accept-label (tr "integrations.delete-token.accept")
|
||||
:on-accept on-delete}))))
|
||||
|
||||
options
|
||||
(mf/with-memo [on-delete]
|
||||
[{:name (tr "labels.delete")
|
||||
:id "token-delete"
|
||||
:handler handle-open-confirm-modal}])]
|
||||
|
||||
[:div {:class (stl/css :item)}
|
||||
[:> text* {:as "div"
|
||||
:typography t/body-medium
|
||||
:title name
|
||||
:class (stl/css :item-title)}
|
||||
name]
|
||||
|
||||
[:> text* {:as "div"
|
||||
:typography t/body-small
|
||||
:class (stl/css-case :item-subtitle true
|
||||
:warning expired?)}
|
||||
(cond
|
||||
(nil? expires-at) (tr "integrations.no-expiration")
|
||||
expired? (tr "integrations.expired-on" expires-txt)
|
||||
:else (tr "integrations.expires-on" expires-txt))]
|
||||
|
||||
[:div {:class (stl/css :item-actions)}
|
||||
[:> icon-button* {:variant "ghost"
|
||||
:class (stl/css :item-button)
|
||||
:aria-pressed menu-open?
|
||||
:aria-label (tr "labels.options")
|
||||
:on-click handle-menu-click
|
||||
:icon i/menu}]
|
||||
[:> context-menu* {:on-close handle-menu-close
|
||||
:show menu-open?
|
||||
:min-width true
|
||||
:top -10
|
||||
:left -138
|
||||
:options options}]]]))
|
||||
|
||||
(mf/defc mcp-server-section*
|
||||
{::mf/private true}
|
||||
[]
|
||||
(let [tokens (mf/deref tokens-ref)
|
||||
profile (mf/deref refs/profile)
|
||||
|
||||
mcp-key (some #(when (= (:type %) "mcp") %) tokens)
|
||||
mcp-active? (d/nilv (-> profile :props :mcp-status) false)
|
||||
|
||||
expires-at (:expires-at mcp-key)
|
||||
expired? (and (some? expires-at) (> (ct/now) expires-at))
|
||||
|
||||
tooltip-id
|
||||
(mf/use-id)
|
||||
|
||||
handle-mcp-status-change
|
||||
(mf/use-fn
|
||||
(fn [mcp-status]
|
||||
(st/emit! (du/update-profile-props {:mcp-status mcp-status})
|
||||
(ntf/show {:level :info
|
||||
:type :toast
|
||||
:content (if (true? mcp-status)
|
||||
(tr "integrations.notification.success.mcp-server-enabled")
|
||||
(tr "integrations.notification.success.mcp-server-disabled"))
|
||||
:timeout notification-timeout})
|
||||
(ev/event {::ev/name (if (true? mcp-status) "enable-mcp" "disable-mcp")
|
||||
::ev/origin "integrations"
|
||||
:source "toggle"}))))
|
||||
|
||||
handle-initial-mcp-status
|
||||
(mf/use-fn
|
||||
#(st/emit! (modal/show {:type :generate-mcp-key})))
|
||||
|
||||
handle-regenerate-mcp-key
|
||||
(mf/use-fn
|
||||
#(st/emit! (modal/show {:type :regenerate-mcp-key})))
|
||||
|
||||
handle-delete
|
||||
(mf/use-fn
|
||||
(mf/deps mcp-key)
|
||||
(fn []
|
||||
(let [params {:id (:id mcp-key)}
|
||||
mdata {:on-success #(st/emit! (du/fetch-access-tokens))}]
|
||||
(st/emit! (du/delete-access-token (with-meta params mdata))
|
||||
(du/update-profile-props {:mcp-status false})))))
|
||||
|
||||
on-copy-to-clipboard
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(clipboard/to-clipboard cf/mcp-server-url)
|
||||
(st/emit! (ntf/show {:level :info
|
||||
:type :toast
|
||||
:content (tr "integrations.notification.success.copied-link")
|
||||
:timeout notification-timeout})
|
||||
(ev/event {::ev/name "copy-mcp-url"
|
||||
::ev/origin "integrations"}))))]
|
||||
|
||||
[:section {:class (stl/css :mcp-server-section)}
|
||||
[:div
|
||||
[:div {:class (stl/css :title)}
|
||||
[:> heading* {:level 2
|
||||
:typography t/title-medium
|
||||
:class (stl/css :color-primary :mcp-server-title)}
|
||||
(tr "integrations.mcp-server.title")]
|
||||
[:> text* {:as "span"
|
||||
:typography t/body-small
|
||||
:class (stl/css :beta)}
|
||||
(tr "integrations.mcp-server.title.beta")]]
|
||||
|
||||
[:> text* {:as "div"
|
||||
:typography t/body-medium
|
||||
:class (stl/css :color-secondary)}
|
||||
(tr "integrations.mcp-server.description")]]
|
||||
|
||||
[:div
|
||||
[:> text* {:as "h3"
|
||||
:typography t/headline-small
|
||||
:class (stl/css :color-primary)}
|
||||
(tr "integrations.mcp-server.status")]
|
||||
|
||||
[:div {:class (stl/css :mcp-server-block)}
|
||||
(when expired?
|
||||
[:> notification-pill* {:level :error
|
||||
:type :context}
|
||||
[:div {:class (stl/css :mcp-server-notification)}
|
||||
[:> text* {:as "div"
|
||||
:typography t/body-medium
|
||||
:class (stl/css :color-primary)}
|
||||
(tr "integrations.mcp-server.status.expired.0")]
|
||||
|
||||
[:> text* {:as "div"
|
||||
:typography t/body-medium
|
||||
:class (stl/css :color-primary)}
|
||||
(tr "integrations.mcp-server.status.expired.1")]]])
|
||||
|
||||
[:div {:class (stl/css :mcp-server-switch)}
|
||||
[:> switch* {:label (if mcp-active?
|
||||
(tr "integrations.mcp-server.status.enabled")
|
||||
(tr "integrations.mcp-server.status.disabled"))
|
||||
:default-checked mcp-active?
|
||||
:on-change handle-mcp-status-change}]
|
||||
(when (and (false? mcp-active?) (nil? mcp-key))
|
||||
[:div {:class (stl/css :mcp-server-switch-cover)
|
||||
:on-click handle-initial-mcp-status}])]]]
|
||||
|
||||
(when (some? mcp-key)
|
||||
[:div {:class (stl/css :mcp-server-key)}
|
||||
[:> text* {:as "h3"
|
||||
:typography t/headline-small
|
||||
:class (stl/css :color-primary)}
|
||||
(tr "integrations.mcp-server.mcp-keys.title")]
|
||||
|
||||
[:div {:class (stl/css :mcp-server-block)}
|
||||
[:div {:class (stl/css :mcp-server-regenerate)}
|
||||
[:> button* {:variant "primary"
|
||||
:class (stl/css :fit-content)
|
||||
:on-click handle-regenerate-mcp-key}
|
||||
(tr "integrations.mcp-server.mcp-keys.regenerate")]
|
||||
[:> tooltip* {:content (tr "integrations.mcp-server.mcp-keys.tootip")
|
||||
:id tooltip-id}
|
||||
[:> icon* {:icon-id i/info
|
||||
:class (stl/css :color-secondary)}]]]
|
||||
|
||||
[:div {:class (stl/css :list)}
|
||||
[:> token-item* {:key (:id mcp-key)
|
||||
:name (:name mcp-key)
|
||||
:expires-at (:expires-at mcp-key)
|
||||
:on-delete handle-delete}]]]])
|
||||
|
||||
[:> notification-pill* {:level :default
|
||||
:type :context}
|
||||
[:div {:class (stl/css :mcp-server-notification)}
|
||||
[:> text* {:as "div"
|
||||
:typography t/body-medium
|
||||
:class (stl/css :color-secondary)}
|
||||
(tr "integrations.mcp-server.mcp-keys.info")]
|
||||
|
||||
[:> input-copy* {:value (dm/str cf/mcp-server-url "?userToken=")
|
||||
:on-copy-to-clipboard on-copy-to-clipboard}]
|
||||
|
||||
[:> text* {:as "div"
|
||||
:typography t/body-medium
|
||||
:class (stl/css :color-secondary)}
|
||||
[:a {:href cf/mcp-help-center-uri
|
||||
:class (stl/css :mcp-server-notification-link)}
|
||||
(tr "integrations.mcp-server.mcp-keys.help") [:> icon* {:icon-id i/open-link}]]]]]]))
|
||||
|
||||
(mf/defc access-tokens-section*
|
||||
{::mf/private true}
|
||||
[]
|
||||
(let [tokens (mf/deref tokens-ref)
|
||||
|
||||
handle-click
|
||||
(mf/use-fn
|
||||
#(st/emit! (modal/show {:type :create-access-token})))
|
||||
|
||||
handle-delete
|
||||
(mf/use-fn
|
||||
(fn [token-id]
|
||||
(let [params {:id token-id}
|
||||
mdata {:on-success #(st/emit! (du/fetch-access-tokens))}]
|
||||
(st/emit! (du/delete-access-token (with-meta params mdata))))))]
|
||||
|
||||
[:section {:class (stl/css :access-tokens-section)}
|
||||
[:> heading* {:level 2
|
||||
:typography t/title-medium
|
||||
:class (stl/css :color-primary)}
|
||||
(tr "integrations.access-tokens.personal")]
|
||||
|
||||
[:> text* {:as "div"
|
||||
:typography t/body-medium
|
||||
:class (stl/css :color-secondary)}
|
||||
(tr "integrations.access-tokens.personal.description")]
|
||||
|
||||
[:> button* {:variant "primary"
|
||||
:class (stl/css :fit-content)
|
||||
:on-click handle-click}
|
||||
(tr "integrations.access-tokens.create")]
|
||||
|
||||
(if (empty? tokens)
|
||||
[:div {:class (stl/css :frame)}
|
||||
[:> text* {:as "div"
|
||||
:typography t/body-medium
|
||||
:class (stl/css :color-secondary :text-center)}
|
||||
[:div (tr "integrations.access-tokens.empty.no-access-tokens")]
|
||||
[:div (tr "integrations.access-tokens.empty.add-one")]]]
|
||||
|
||||
[:div {:class (stl/css :list)}
|
||||
(for [token tokens]
|
||||
(when (nil? (:type token))
|
||||
[:> token-item* {:key (:id token)
|
||||
:name (:name token)
|
||||
:expires-at (:expires-at token)
|
||||
:on-delete (partial handle-delete (:id token))}]))])]))
|
||||
|
||||
(mf/defc integrations-page*
|
||||
[]
|
||||
(mf/with-effect []
|
||||
(dom/set-html-title (tr "title.settings.integrations"))
|
||||
(st/emit! (du/fetch-access-tokens)))
|
||||
|
||||
[:div {:class (stl/css :integrations)}
|
||||
[:> heading* {:level 1
|
||||
:typography t/title-large
|
||||
:class (stl/css :color-primary)}
|
||||
(tr "integrations.title")]
|
||||
|
||||
(when (contains? cf/flags :mcp)
|
||||
[:> mcp-server-section*])
|
||||
|
||||
(when (and (contains? cf/flags :mcp)
|
||||
(contains? cf/flags :access-tokens))
|
||||
[:hr {:class (stl/css :separator)}])
|
||||
|
||||
(when (contains? cf/flags :access-tokens)
|
||||
[:> access-tokens-section*])])
|
||||
@@ -1,239 +0,0 @@
|
||||
// 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
|
||||
|
||||
@use "refactor/common-refactor.scss" as deprecated;
|
||||
|
||||
@use "ds/_borders.scss" as *;
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@use "ds/mixins.scss" as *;
|
||||
@use "ds/spacing.scss" as *;
|
||||
@use "ds/typography.scss" as t;
|
||||
|
||||
.color-primary {
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
.color-secondary {
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.fit-content {
|
||||
inline-size: fit-content;
|
||||
}
|
||||
|
||||
.beta {
|
||||
color: var(--color-accent-primary);
|
||||
border: $b-1 solid var(--color-accent-primary);
|
||||
inline-size: fit-content;
|
||||
padding: var(--sp-xxs) var(--sp-s);
|
||||
border-radius: $br-4;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
@extend .modal-overlay-base;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
@extend .modal-container-base;
|
||||
inline-size: $sz-400;
|
||||
max-block-size: fit-content;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-xs);
|
||||
}
|
||||
|
||||
.modal-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-xxxl);
|
||||
}
|
||||
|
||||
.modal-close-button {
|
||||
position: absolute;
|
||||
top: var(--sp-s);
|
||||
right: var(--sp-s);
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
|
||||
.input-copy {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.input-copy-button-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
border-start-start-radius: 0;
|
||||
border-end-start-radius: 0;
|
||||
}
|
||||
|
||||
.input-copy-button {
|
||||
border-radius: 0 $br-8 $br-8 0;
|
||||
}
|
||||
|
||||
.integrations {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
margin: $sz-88 auto $sz-120 auto;
|
||||
gap: $sz-32;
|
||||
inline-size: $sz-500;
|
||||
}
|
||||
|
||||
.access-tokens-section {
|
||||
display: grid;
|
||||
grid-template-rows: auto auto 1fr;
|
||||
gap: var(--sp-m);
|
||||
}
|
||||
|
||||
.mcp-server-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-l);
|
||||
}
|
||||
|
||||
.mcp-server-key {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.mcp-server-notification {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-m);
|
||||
padding-right: var(--sp-xxl);
|
||||
}
|
||||
|
||||
.mcp-server-notification-link {
|
||||
cursor: pointer;
|
||||
color: var(--color-accent-primary);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--sp-xs);
|
||||
}
|
||||
|
||||
.mcp-server-title {
|
||||
margin: var(--sp-s) 0;
|
||||
}
|
||||
|
||||
.mcp-server-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-l);
|
||||
}
|
||||
|
||||
.mcp-server-regenerate {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
|
||||
.mcp-server-switch {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mcp-server-switch-cover {
|
||||
position: absolute;
|
||||
inset-block: 0;
|
||||
inset-inline: 0;
|
||||
}
|
||||
|
||||
.separator {
|
||||
border: $b-1 solid var(--color-background-quaternary);
|
||||
margin: var(--sp-s) 0;
|
||||
}
|
||||
|
||||
.frame {
|
||||
border: $b-1 solid var(--color-background-quaternary);
|
||||
padding: var(--sp-m);
|
||||
border-radius: $br-8;
|
||||
}
|
||||
|
||||
.list {
|
||||
display: grid;
|
||||
grid-auto-rows: $sz-64;
|
||||
gap: var(--sp-m);
|
||||
}
|
||||
|
||||
.item {
|
||||
display: grid;
|
||||
grid-template-columns: 45% 1fr auto;
|
||||
align-items: center;
|
||||
background-color: var(--color-background-tertiary);
|
||||
border-radius: $br-8;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
@include textEllipsis;
|
||||
align-content: center;
|
||||
block-size: $sz-64;
|
||||
padding: 0 var(--sp-l);
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
.item-subtitle {
|
||||
align-content: center;
|
||||
block-size: $sz-64;
|
||||
color: var(--color-foreground-secondary);
|
||||
|
||||
&.warning {
|
||||
padding: var(--sp-s) var(--sp-m);
|
||||
block-size: fit-content;
|
||||
inline-size: fit-content;
|
||||
color: var(--color-foreground-primary);
|
||||
background-color: var(--color-background-warning);
|
||||
border: $b-1 solid var(--color-accent-warning);
|
||||
border-radius: $br-8;
|
||||
}
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.item-button {
|
||||
block-size: $sz-64;
|
||||
inline-size: $sz-48;
|
||||
border-radius: 0 var(--sp-s) var(--sp-s) 0;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
@include t.use-typography("body-small");
|
||||
border-radius: $br-8;
|
||||
background-color: var(--color-background-tertiary);
|
||||
color: var(--color-foreground-secondary);
|
||||
padding: var(--sp-xs) var(--sp-s);
|
||||
border: 0;
|
||||
resize: none;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-background-quaternary);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: $b-1 solid var(--color-accent-primary);
|
||||
}
|
||||
}
|
||||
@@ -43,8 +43,8 @@
|
||||
(def ^:private go-settings-subscription
|
||||
#(st/emit! (rt/nav :settings-subscription)))
|
||||
|
||||
(def ^:private go-settings-integrations
|
||||
#(st/emit! (rt/nav :settings-integrations)))
|
||||
(def ^:private go-settings-access-tokens
|
||||
#(st/emit! (rt/nav :settings-access-tokens)))
|
||||
|
||||
(def ^:private go-settings-notifications
|
||||
#(st/emit! (rt/nav :settings-notifications)))
|
||||
@@ -66,7 +66,7 @@
|
||||
options? (= section :settings-options)
|
||||
feedback? (= section :settings-feedback)
|
||||
subscription? (= section :settings-subscription)
|
||||
integrations? (= section :settings-integrations)
|
||||
access-tokens? (= section :settings-access-tokens)
|
||||
notifications? (= section :settings-notifications)
|
||||
team-id (or (dtm/get-last-team-id)
|
||||
(:default-team-id profile))
|
||||
@@ -115,13 +115,12 @@
|
||||
:data-testid "settings-subscription"}
|
||||
[:span {:class (stl/css :element-title)} (tr "subscription.labels")]])
|
||||
|
||||
(when (or (contains? cf/flags :access-tokens)
|
||||
(contains? cf/flags :mcp))
|
||||
[:li {:class (stl/css-case :current integrations?
|
||||
(when (contains? cf/flags :access-tokens)
|
||||
[:li {:class (stl/css-case :current access-tokens?
|
||||
:settings-item true)
|
||||
:on-click go-settings-integrations
|
||||
:data-testid "settings-integrations"}
|
||||
[:span {:class (stl/css :element-title)} (tr "labels.integrations")]])
|
||||
:on-click go-settings-access-tokens
|
||||
:data-testid "settings-access-tokens"}
|
||||
[:span {:class (stl/css :element-title)} (tr "labels.access-tokens")]])
|
||||
|
||||
[:hr {:class (stl/css :sidebar-separator)}]
|
||||
|
||||
|
||||
@@ -4,11 +4,9 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.config :as cf]
|
||||
[app.main.data.auth :as da]
|
||||
[app.main.data.event :as ev]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.nitrate :as dnt]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.router :as rt]
|
||||
[app.main.store :as st]
|
||||
@@ -360,11 +358,6 @@
|
||||
(let [route (mf/deref refs/route)
|
||||
authenticated? (da/is-authenticated? profile)
|
||||
|
||||
nitrate-license (dm/get-in profile [:props :nitrate-license])
|
||||
|
||||
nitrate? (and (contains? cf/flags :nitrate)
|
||||
(:valid nitrate-license))
|
||||
|
||||
params-subscription
|
||||
(-> route :params :query :subscription)
|
||||
|
||||
@@ -395,9 +388,7 @@
|
||||
(ct/format-inst (:created-at profile) "d MMMM, yyyy")
|
||||
|
||||
subscribed-since
|
||||
(if nitrate?
|
||||
(ct/format-inst (:created-at nitrate-license) "d MMMM, yyyy")
|
||||
(ct/format-inst (:start-date subscription) "d MMMM, yyyy"))
|
||||
(ct/format-inst (:start-date subscription) "d MMMM, yyyy")
|
||||
|
||||
go-to-pricing-page
|
||||
(mf/use-fn
|
||||
@@ -424,13 +415,11 @@
|
||||
(fn [subscription-type current-subscription]
|
||||
(st/emit! (ev/event {::ev/name "open-subscription-modal"
|
||||
::ev/origin "settings:in-app"}))
|
||||
(if (= subscription-type "nitrate")
|
||||
(st/emit! (dnt/show-nitrate-popup :nitrate-dialog))
|
||||
(st/emit!
|
||||
(modal/show :management-dialog
|
||||
{:subscription-type subscription-type
|
||||
:current-subscription current-subscription
|
||||
:editors subscription-editors :subscribe-to-trial (not (:type subscription))})))))]
|
||||
(st/emit!
|
||||
(modal/show :management-dialog
|
||||
{:subscription-type subscription-type
|
||||
:current-subscription current-subscription
|
||||
:editors subscription-editors :subscribe-to-trial (not (:type subscription))}))))]
|
||||
|
||||
(mf/with-effect []
|
||||
(dom/set-html-title (tr "subscription.labels")))
|
||||
@@ -475,73 +464,60 @@
|
||||
|
||||
[:div {:class (stl/css :your-subscription)}
|
||||
[:h3 {:class (stl/css :plan-section-title)} (tr "subscription.settings.section-plan")]
|
||||
(if nitrate?
|
||||
;; TODO add translations for this texts when we have the definitive ones
|
||||
[:> plan-card* {:card-title "Business Nitrate"
|
||||
:card-title-icon i/character-b
|
||||
:benefits-title "Loren ipsum",
|
||||
:benefits ["Loren ipsum",
|
||||
"Loren ipsum",
|
||||
"Loren ipsum"]
|
||||
:cta-text-with-icon "Control Center"
|
||||
:cta-link-with-icon dnt/go-to-nitrate-cc
|
||||
:cta-text (tr "subscription.settings.manage-your-subscription")
|
||||
:cta-link dnt/go-to-nitrate-billing}]
|
||||
(case subscription-type
|
||||
"professional"
|
||||
[:> plan-card* {:card-title (tr "subscription.settings.professional")
|
||||
:benefits [(tr "subscription.settings.professional.storage-benefit"),
|
||||
(tr "subscription.settings.professional.autosave-benefit"),
|
||||
(tr "subscription.settings.professional.teams-editors-benefit")]}]
|
||||
(case subscription-type
|
||||
"professional"
|
||||
[:> plan-card* {:card-title (tr "subscription.settings.professional")
|
||||
:benefits [(tr "subscription.settings.professional.storage-benefit"),
|
||||
(tr "subscription.settings.professional.autosave-benefit"),
|
||||
(tr "subscription.settings.professional.teams-editors-benefit")]}]
|
||||
|
||||
"unlimited"
|
||||
(if subscription-is-trial?
|
||||
[:> plan-card* {:card-title (tr "subscription.settings.unlimited-trial")
|
||||
:card-title-icon i/character-u
|
||||
:benefits-title (tr "subscription.settings.benefits.all-professional-benefits"),
|
||||
:benefits [(tr "subscription.settings.unlimited.storage-benefit")
|
||||
(tr "subscription.settings.unlimited.autosave-benefit"),
|
||||
(tr "subscription.settings.unlimited.bill")]
|
||||
:cta-text (tr "subscription.settings.manage-your-subscription")
|
||||
:cta-link go-to-payments
|
||||
:cta-text-trial (tr "subscription.settings.add-payment-to-continue")
|
||||
:cta-link-trial go-to-payments
|
||||
:editors (-> profile :props :subscription :quantity)}]
|
||||
"unlimited"
|
||||
(if subscription-is-trial?
|
||||
[:> plan-card* {:card-title (tr "subscription.settings.unlimited-trial")
|
||||
:card-title-icon i/character-u
|
||||
:benefits-title (tr "subscription.settings.benefits.all-professional-benefits"),
|
||||
:benefits [(tr "subscription.settings.unlimited.storage-benefit")
|
||||
(tr "subscription.settings.unlimited.autosave-benefit"),
|
||||
(tr "subscription.settings.unlimited.bill")]
|
||||
:cta-text (tr "subscription.settings.manage-your-subscription")
|
||||
:cta-link go-to-payments
|
||||
:cta-text-trial (tr "subscription.settings.add-payment-to-continue")
|
||||
:cta-link-trial go-to-payments
|
||||
:editors (-> profile :props :subscription :quantity)}]
|
||||
|
||||
[:> plan-card* {:card-title (tr "subscription.settings.unlimited")
|
||||
:card-title-icon i/character-u
|
||||
:benefits-title (tr "subscription.settings.benefits.all-unlimited-benefits")
|
||||
:benefits [(tr "subscription.settings.unlimited.storage-benefit"),
|
||||
(tr "subscription.settings.unlimited.autosave-benefit"),
|
||||
(tr "subscription.settings.unlimited.bill")]
|
||||
:cta-text (tr "subscription.settings.manage-your-subscription")
|
||||
:cta-link go-to-payments
|
||||
:editors (-> profile :props :subscription :quantity)}])
|
||||
[:> plan-card* {:card-title (tr "subscription.settings.unlimited")
|
||||
:card-title-icon i/character-u
|
||||
:benefits-title (tr "subscription.settings.benefits.all-unlimited-benefits")
|
||||
:benefits [(tr "subscription.settings.unlimited.storage-benefit"),
|
||||
(tr "subscription.settings.unlimited.autosave-benefit"),
|
||||
(tr "subscription.settings.unlimited.bill")]
|
||||
:cta-text (tr "subscription.settings.manage-your-subscription")
|
||||
:cta-link go-to-payments
|
||||
:editors (-> profile :props :subscription :quantity)}])
|
||||
|
||||
"enterprise"
|
||||
(if subscription-is-trial?
|
||||
[:> plan-card* {:card-title (tr "subscription.settings.enterprise-trial")
|
||||
:card-title-icon i/character-e
|
||||
:benefits-title (tr "subscription.settings.benefits.all-unlimited-benefits"),
|
||||
:benefits [(tr "subscription.settings.enterprise.unlimited-storage-benefit"),
|
||||
(tr "subscription.settings.enterprise.autosave"),
|
||||
(tr "subscription.settings.enterprise.capped-bill")]
|
||||
:cta-text (tr "subscription.settings.manage-your-subscription")
|
||||
:cta-link go-to-payments
|
||||
:cta-text-trial (tr "subscription.settings.add-payment-to-continue")
|
||||
:cta-link-trial go-to-payments}]
|
||||
[:> plan-card* {:card-title (tr "subscription.settings.enterprise")
|
||||
:card-title-icon i/character-e
|
||||
:benefits-title (tr "subscription.settings.benefits.all-unlimited-benefits"),
|
||||
:benefits [(tr "subscription.settings.enterprise.unlimited-storage-benefit"),
|
||||
(tr "subscription.settings.enterprise.autosave"),
|
||||
(tr "subscription.settings.enterprise.capped-bill")]
|
||||
:cta-text (tr "subscription.settings.manage-your-subscription")
|
||||
:cta-link go-to-payments}])))
|
||||
"enterprise"
|
||||
(if subscription-is-trial?
|
||||
[:> plan-card* {:card-title (tr "subscription.settings.enterprise-trial")
|
||||
:card-title-icon i/character-e
|
||||
:benefits-title (tr "subscription.settings.benefits.all-unlimited-benefits"),
|
||||
:benefits [(tr "subscription.settings.enterprise.unlimited-storage-benefit"),
|
||||
(tr "subscription.settings.enterprise.autosave"),
|
||||
(tr "subscription.settings.enterprise.capped-bill")]
|
||||
:cta-text (tr "subscription.settings.manage-your-subscription")
|
||||
:cta-link go-to-payments
|
||||
:cta-text-trial (tr "subscription.settings.add-payment-to-continue")
|
||||
:cta-link-trial go-to-payments}]
|
||||
[:> plan-card* {:card-title (tr "subscription.settings.enterprise")
|
||||
:card-title-icon i/character-e
|
||||
:benefits-title (tr "subscription.settings.benefits.all-unlimited-benefits"),
|
||||
:benefits [(tr "subscription.settings.enterprise.unlimited-storage-benefit"),
|
||||
(tr "subscription.settings.enterprise.autosave"),
|
||||
(tr "subscription.settings.enterprise.capped-bill")]
|
||||
:cta-text (tr "subscription.settings.manage-your-subscription")
|
||||
:cta-link go-to-payments}]))
|
||||
|
||||
[:div {:class (stl/css :membership-container)}
|
||||
(when (or nitrate?
|
||||
(and subscribed-since (not= subscription-type "professional")))
|
||||
(when (and subscribed-since (not= subscription-type "professional"))
|
||||
[:div {:class (stl/css :membership)}
|
||||
[:> icon* {:class (stl/css :subscription-member)
|
||||
:icon-id "crown"
|
||||
@@ -586,7 +562,7 @@
|
||||
:recommended (= subscription-type "professional")
|
||||
:show-button-cta (= subscription-type "professional")}])
|
||||
|
||||
(when (and (not= subscription-type "enterprise") (not (contains? cf/flags :nitrate)))
|
||||
(when (not= subscription-type "enterprise")
|
||||
[:> plan-card* {:card-title (tr "subscription.settings.enterprise")
|
||||
:card-title-icon i/character-e
|
||||
:price-value "$950"
|
||||
@@ -599,104 +575,5 @@
|
||||
:cta-link #(open-subscription-modal "enterprise" subscription)
|
||||
:cta-text-with-icon (tr "subscription.settings.more-information")
|
||||
:cta-link-with-icon go-to-pricing-page
|
||||
:show-button-cta (= subscription-type "professional")}])
|
||||
|
||||
;; TODO add translations for this texts when we have the definitive ones
|
||||
(when (and (contains? cf/flags :nitrate) (not nitrate?))
|
||||
[:> plan-card* {:card-title "Business Nitrate"
|
||||
:card-title-icon i/character-n
|
||||
:price-value "$25"
|
||||
:price-period "org member"
|
||||
:benefits-title (tr "subscription.settings.benefits.all-unlimited-benefits")
|
||||
:benefits ["Crea organizaciones y añade personas, que usarán Penpot con las reglas que configures."
|
||||
"Acceso exclusivo al Control Center"
|
||||
"Lorem ipsum"]
|
||||
:cta-text (tr "subscription.settings.subscribe")
|
||||
:cta-link #(open-subscription-modal "nitrate" subscription)
|
||||
:cta-text-with-icon (tr "subscription.settings.more-information")
|
||||
:cta-link-with-icon go-to-pricing-page}])]]]))
|
||||
|
||||
|
||||
(def ^:private schema:nitrate-form
|
||||
[:map {:title "NitrateForm"}
|
||||
[:subscription [::sm/one-of #{:monthly :yearly}]]])
|
||||
|
||||
(mf/defc subscribe-nitrate-dialog
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :nitrate-dialog}
|
||||
[connectivity]
|
||||
;; TODO add translations for this texts when we have the definitive ones
|
||||
(let [online? (:licenses connectivity)
|
||||
initial (mf/with-memo []
|
||||
{:subscription "yearly"})
|
||||
form (fm/use-form :schema schema:nitrate-form
|
||||
:initial initial)
|
||||
|
||||
handle-close-dialog
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(modal/hide!)))
|
||||
|
||||
on-submit
|
||||
(mf/use-fn
|
||||
(mf/deps form)
|
||||
(fn []
|
||||
(let [params (:clean-data @form)]
|
||||
(dom/open-new-window (str "/control-center/licenses/start?subscription=" (name (:subscription params)))))))]
|
||||
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div {:class (stl/css :modal-dialog)}
|
||||
[:button {:class (stl/css :close-btn) :on-click handle-close-dialog}
|
||||
[:> icon* {:icon-id "close"
|
||||
:size "m"}]]
|
||||
[:div {:class (stl/css :modal-title :subscription-title)}
|
||||
"Subcribe to the Business Nitrate plan"]
|
||||
|
||||
(if online?
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
|
||||
|
||||
|
||||
[:div {:class (stl/css :modal-text)}
|
||||
"Lorem ipsum lorem ipsum:"]
|
||||
|
||||
|
||||
[:& fm/form {:on-submit on-submit
|
||||
:class (stl/css :seats-form)
|
||||
:form form}
|
||||
|
||||
[:*
|
||||
[:div {:class (stl/css :editors-wrapper)}
|
||||
[:div {:class (stl/css :fields-row)}
|
||||
[:& fm/radio-buttons
|
||||
{:options [{:label "Price Tag Yearly (Discount)" :value "yearly"}
|
||||
{:label "Price Tag Montly" :value "monthly"}]
|
||||
:name :subscription
|
||||
:class (stl/css :radio-btns)}]]]
|
||||
[:div {:class (stl/css :modal-text)}
|
||||
"You won’t be charged right now. Payment will be processed at the end of the trial. Cancel anytime."]
|
||||
|
||||
|
||||
|
||||
[:div {:class (stl/css :modal-footer)}
|
||||
[:div {:class (stl/css :action-buttons)}
|
||||
[:input
|
||||
{:class (stl/css :cancel-button)
|
||||
:type "button"
|
||||
:value (tr "ds.confirm-cancel")
|
||||
:on-click handle-close-dialog}]
|
||||
|
||||
[:> fm/submit-button*
|
||||
{:label "TRY 14 DAYS FOR FREE"
|
||||
:class (stl/css :primary-button)}]]]]]]
|
||||
[:div {:class (stl/css :modal-content :modal-contact-content)}
|
||||
[:div {:class (stl/css :modal-text)}
|
||||
"Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum"]
|
||||
[:div {:class (stl/css :modal-text)}
|
||||
"Contact us to upgrade to Nitrate:"]
|
||||
[:div {:class (stl/css :modal-text)}
|
||||
[:a {:class (stl/css :link) :href "mailto:sales@penpot.app"}
|
||||
"sales@penpot.app"]]])]]))
|
||||
|
||||
|
||||
:show-button-cta (= subscription-type "professional")}])]]]))
|
||||
|
||||
|
||||
@@ -329,22 +329,3 @@
|
||||
.show-editors-button {
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
.radio-btns {
|
||||
label {
|
||||
@include t.use-typography("body-large");
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 0 var(--sp-xl) 0;
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
|
||||
.modal-contact-content {
|
||||
gap: var(--sp-xl);
|
||||
}
|
||||
|
||||
@@ -150,9 +150,7 @@
|
||||
{::mf/props :obj
|
||||
::mf/private true}
|
||||
[{:keys [shapes]}]
|
||||
(let [multiple? (> (count shapes) 1)
|
||||
|
||||
do-copy #(st/emit! (dw/copy-selected))
|
||||
(let [do-copy #(st/emit! (dw/copy-selected))
|
||||
do-copy-link #(st/emit! (dw/copy-link-to-clipboard))
|
||||
|
||||
do-cut #(st/emit! (dw/copy-selected)
|
||||
@@ -180,9 +178,6 @@
|
||||
handle-copy-text
|
||||
(mf/use-callback #(st/emit! (dw/copy-selected-text)))
|
||||
|
||||
handle-copy-as-image
|
||||
(mf/use-callback #(st/emit! (dw/copy-as-image)))
|
||||
|
||||
handle-hover-copy-paste
|
||||
(mf/use-callback
|
||||
(fn []
|
||||
@@ -227,11 +222,6 @@
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.copy-svg")
|
||||
:on-click handle-copy-svg}]
|
||||
|
||||
(when (some cfh/frame-shape? shapes)
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.copy-as-image")
|
||||
:disabled multiple?
|
||||
:on-click handle-copy-as-image}])
|
||||
|
||||
[:> menu-separator* {}]
|
||||
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.copy-text")
|
||||
@@ -239,7 +229,7 @@
|
||||
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.copy-props")
|
||||
:shortcut (sc/get-tooltip :copy-props)
|
||||
:disabled multiple?
|
||||
:disabled (> (count shapes) 1)
|
||||
:on-click handle-copy-props}]
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.paste-props")
|
||||
:shortcut (sc/get-tooltip :paste-props)
|
||||
|
||||
@@ -13,10 +13,8 @@
|
||||
[app.common.types.components-list :as ctkl]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.types.library :as ctl]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.common.types.typographies-list :as ctyl]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.main.data.dashboard :as dd]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.notifications :as ntf]
|
||||
@@ -38,7 +36,6 @@
|
||||
[app.main.ui.ds.product.empty-state :refer [empty-state*]]
|
||||
[app.main.ui.hooks :as h]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.main.ui.workspace.tokens.import-from-library]
|
||||
[app.util.color :as uc]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :refer [c tr]]
|
||||
@@ -183,12 +180,6 @@
|
||||
[summary]
|
||||
(boolean (:is-empty summary)))
|
||||
|
||||
(defn- has-tokens?
|
||||
"Check if library has tokens to be imported"
|
||||
[{:keys [data]}]
|
||||
(when-let [tokens-lib (get data :tokens-lib)]
|
||||
(not (ctob/empty-lib? tokens-lib))))
|
||||
|
||||
(mf/defc libraries-tab*
|
||||
{::mf/props :obj
|
||||
::mf/private true}
|
||||
@@ -239,18 +230,14 @@
|
||||
(keep library-names))))
|
||||
(sort-by (comp str/lower :name))))
|
||||
|
||||
linked-libraries-ids
|
||||
(mf/with-memo [linked-libraries]
|
||||
(into #{} d/xf:map-id linked-libraries))
|
||||
linked-libraries-ids (mf/with-memo [linked-libraries]
|
||||
(into #{} (map :id) linked-libraries))
|
||||
|
||||
importing*
|
||||
(mf/use-state nil)
|
||||
|
||||
sample-libraries
|
||||
(mf/with-memo []
|
||||
[{:id "penpot-design-system", :name "Design system example"}
|
||||
{:id "wireframing-kit", :name "Wireframe library"}
|
||||
{:id "whiteboarding-kit", :name "Whiteboarding Kit"}])
|
||||
importing* (mf/use-state nil)
|
||||
sample-libraries [{:id "penpot-design-system", :name "Design system example"}
|
||||
{:id "wireframing-kit", :name "Wireframe library"}
|
||||
{:id "whiteboarding-kit", :name "Whiteboarding Kit"}]
|
||||
|
||||
|
||||
change-search-term
|
||||
@@ -280,17 +267,6 @@
|
||||
(st/emit! (dwl/unlink-file-from-library file-id library-id)
|
||||
(dwl/sync-file file-id library-id)))))
|
||||
|
||||
import-tokens
|
||||
(mf/use-fn
|
||||
(mf/deps file-id)
|
||||
(fn [event]
|
||||
(let [library-id (some-> (dom/get-current-target event)
|
||||
(dom/get-data "library-id")
|
||||
(uuid/parse))]
|
||||
(st/emit! (modal/show
|
||||
:tokens/import-from-library {:file-id file-id
|
||||
:library-id library-id})))))
|
||||
|
||||
on-delete-accept
|
||||
(mf/use-fn
|
||||
(mf/deps file-id)
|
||||
@@ -356,12 +332,8 @@
|
||||
:on-click publish}])]
|
||||
|
||||
(for [{:keys [id name data connected-to connected-to-names] :as library} linked-libraries]
|
||||
(let [disabled? (some #(contains? linked-libraries-ids %) connected-to)
|
||||
has-tokens? (and (has-tokens? library)
|
||||
(contains? cf/flags :token-import-from-library))]
|
||||
[:div {:class (if has-tokens?
|
||||
(stl/css :section-list-item-double-icon)
|
||||
(stl/css :section-list-item))
|
||||
(let [disabled? (some #(contains? linked-libraries-ids %) connected-to)]
|
||||
[:div {:class (stl/css :section-list-item)
|
||||
:key (dm/str id)
|
||||
:data-testid "library-item"}
|
||||
[:div {:class (stl/css :item-content)}
|
||||
@@ -376,15 +348,6 @@
|
||||
[:span {:class (stl/css :connected-to-values)} (str/join ", " connected-to-names)]
|
||||
[:span ")"]])])]]
|
||||
|
||||
(when ^boolean has-tokens?
|
||||
[:> icon-button*
|
||||
{:type "button"
|
||||
:aria-label (tr "workspace.tokens.import-tokens")
|
||||
:icon i/import-export
|
||||
:data-library-id (dm/str id)
|
||||
:variant "secondary"
|
||||
:on-click import-tokens}])
|
||||
|
||||
[:> icon-button* {:type "button"
|
||||
:aria-label (tr "workspace.libraries.unlink-library-btn")
|
||||
:icon i/detach
|
||||
|
||||
@@ -116,11 +116,6 @@
|
||||
border-radius: $br-8;
|
||||
}
|
||||
|
||||
.section-list-item-double-icon {
|
||||
@extend .section-list-item;
|
||||
grid-template-columns: 1fr auto auto;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
@@ -93,12 +93,6 @@
|
||||
(dwl/sync-file file-id file-id :colors color-id)
|
||||
(dwu/commit-undo-transaction undo-id))))))
|
||||
|
||||
duplicate-color
|
||||
(mf/use-fn
|
||||
(mf/deps file-id color-id)
|
||||
(fn []
|
||||
(st/emit! (dwl/duplicate-color file-id color-id))))
|
||||
|
||||
rename-color-clicked
|
||||
(mf/use-fn
|
||||
(mf/deps read-only? local?)
|
||||
@@ -253,10 +247,7 @@
|
||||
{:name (tr "workspace.assets.edit")
|
||||
:id "assets-edit-color"
|
||||
:handler edit-color-clicked})
|
||||
(when-not (or multi-colors? multi-assets?)
|
||||
{:name (tr "workspace.assets.duplicate")
|
||||
:id "assets-duplicate-color"
|
||||
:handler duplicate-color})
|
||||
|
||||
{:name (tr "workspace.assets.delete")
|
||||
:id "assets-delete-color"
|
||||
:handler delete-color}
|
||||
|
||||
@@ -377,12 +377,6 @@
|
||||
(dwl/sync-file file-id file-id :typographies (:id @state))
|
||||
(dwu/commit-undo-transaction undo-id))))))
|
||||
|
||||
handle-duplicate-typography
|
||||
(mf/use-fn
|
||||
(mf/deps file-id @state)
|
||||
(fn []
|
||||
(st/emit! (dwl/duplicate-typography file-id (:id @state)))))
|
||||
|
||||
editing-id (:edit-typography local-data)
|
||||
|
||||
renaming-id (:rename-typography local-data)
|
||||
@@ -446,11 +440,6 @@
|
||||
:id "assets-edit-typography"
|
||||
:handler handle-edit-typography-clicked})
|
||||
|
||||
(when-not (or multi-typographies? multi-assets?)
|
||||
{:name (tr "workspace.assets.duplicate")
|
||||
:id "assets-duplicate-typography"
|
||||
:handler handle-duplicate-typography})
|
||||
|
||||
{:name (tr "workspace.assets.delete")
|
||||
:id "assets-delete-typography"
|
||||
:handler handle-delete-typography}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.math :as mth]
|
||||
[app.common.types.component :as ctk]
|
||||
[app.common.types.components-list :as ctkl]
|
||||
[app.common.types.container :as ctn]
|
||||
@@ -37,6 +38,8 @@
|
||||
(defonce ^:private sidebar-hover-queue (atom {:enter #{} :leave #{}}))
|
||||
(defonce ^:private sidebar-hover-pending? (atom false))
|
||||
|
||||
(def ^:const default-chunk-size 50)
|
||||
|
||||
(defn- schedule-sidebar-hover-flush []
|
||||
(when (compare-and-set! sidebar-hover-pending? false true)
|
||||
(ts/raf
|
||||
@@ -48,12 +51,11 @@
|
||||
(when (seq enter)
|
||||
(apply st/emit! (map dw/highlight-shape enter))))))))
|
||||
|
||||
(mf/defc layer-item-inner
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [item depth parent-size name-ref children ref style
|
||||
(mf/defc layer-item-inner*
|
||||
[{:keys [item depth parent-size name-ref children ref style rename-id
|
||||
;; Flags
|
||||
read-only? highlighted? selected? component-tree?
|
||||
filtered? expanded? dnd-over? dnd-over-top? dnd-over-bot? hide-toggle?
|
||||
is-read-only is-highlighted is-selected is-component-tree
|
||||
is-filtered is-expanded dnd-over dnd-over-top dnd-over-bot hide-toggle
|
||||
;; Callbacks
|
||||
on-select-shape on-context-menu on-pointer-enter on-pointer-leave on-zoom-to-selected
|
||||
on-toggle-collapse on-enable-drag on-disable-drag on-toggle-visibility on-toggle-blocking]}]
|
||||
@@ -64,7 +66,7 @@
|
||||
hidden? (:hidden item)
|
||||
has-shapes? (-> item :shapes seq boolean)
|
||||
touched? (-> item :touched seq boolean)
|
||||
parent-board? (and (cfh/frame-shape? item)
|
||||
root-board? (and (cfh/frame-shape? item)
|
||||
(= uuid/zero (:parent-id item)))
|
||||
absolute? (ctl/item-absolute? item)
|
||||
is-variant? (ctk/is-variant? item)
|
||||
@@ -73,9 +75,11 @@
|
||||
variant-name (when is-variant? (:variant-name item))
|
||||
variant-error (when is-variant? (:variant-error item))
|
||||
|
||||
data (deref refs/workspace-data)
|
||||
component (ctkl/get-component data (:component-id item))
|
||||
variant-properties (:variant-properties component)
|
||||
component-id (get item :component-id)
|
||||
data (mf/deref refs/workspace-data)
|
||||
variant-properties (-> (ctkl/get-component data component-id)
|
||||
(get :variant-properties))
|
||||
|
||||
icon-shape (usi/get-shape-icon item)]
|
||||
|
||||
[:*
|
||||
@@ -85,30 +89,30 @@
|
||||
:on-context-menu on-context-menu
|
||||
:data-testid "layer-row"
|
||||
:role "checkbox"
|
||||
:aria-checked selected?
|
||||
:aria-checked is-selected
|
||||
:class (stl/css-case
|
||||
:layer-row true
|
||||
:highlight highlighted?
|
||||
:highlight is-highlighted
|
||||
:component (ctk/instance-head? item)
|
||||
:masked (:masked-group item)
|
||||
:selected selected?
|
||||
:selected is-selected
|
||||
:type-frame (cfh/frame-shape? item)
|
||||
:type-bool (cfh/bool-shape? item)
|
||||
:type-comp (or component-tree? is-variant-container?)
|
||||
:type-comp (or is-component-tree is-variant-container?)
|
||||
:hidden hidden?
|
||||
:dnd-over dnd-over?
|
||||
:dnd-over-top dnd-over-top?
|
||||
:dnd-over-bot dnd-over-bot?
|
||||
:root-board parent-board?)
|
||||
:dnd-over dnd-over
|
||||
:dnd-over-top dnd-over-top
|
||||
:dnd-over-bot dnd-over-bot
|
||||
:root-board root-board?)
|
||||
:style style}
|
||||
[:span {:class (stl/css-case
|
||||
:tab-indentation true
|
||||
:filtered filtered?)
|
||||
:filtered is-filtered)
|
||||
:style {"--depth" depth}}]
|
||||
[:div {:class (stl/css-case
|
||||
:element-list-body true
|
||||
:filtered filtered?
|
||||
:selected selected?
|
||||
:filtered is-filtered
|
||||
:selected is-selected
|
||||
:icon-layer (= (:type item) :icon))
|
||||
:style {"--depth" depth}
|
||||
:on-pointer-enter on-pointer-enter
|
||||
@@ -117,12 +121,12 @@
|
||||
|
||||
(if (< 0 (count (:shapes item)))
|
||||
[:div {:class (stl/css :button-content)}
|
||||
(when (and (not hide-toggle?) (not filtered?))
|
||||
(when (and (not hide-toggle) (not is-filtered))
|
||||
[:button {:class (stl/css-case
|
||||
:toggle-content true
|
||||
:inverse expanded?)
|
||||
:inverse is-expanded)
|
||||
:data-testid "toggle-content"
|
||||
:aria-expanded expanded?
|
||||
:aria-expanded is-expanded
|
||||
:on-click on-toggle-collapse}
|
||||
deprecated-icon/arrow])
|
||||
|
||||
@@ -133,7 +137,7 @@
|
||||
[:> icon* {:icon-id icon-shape :size "s" :data-testid (str "icon-" icon-shape)}]]]
|
||||
|
||||
[:div {:class (stl/css :button-content)}
|
||||
(when (not ^boolean filtered?)
|
||||
(when (not ^boolean is-filtered)
|
||||
[:span {:class (stl/css :toggle-content)}])
|
||||
[:div {:class (stl/css :icon-shape)
|
||||
:on-double-click on-zoom-to-selected}
|
||||
@@ -142,25 +146,26 @@
|
||||
[:> icon* {:icon-id icon-shape :size "s" :data-testid (str "icon-" icon-shape)}]]])
|
||||
|
||||
[:> layer-name* {:ref name-ref
|
||||
:rename-id rename-id
|
||||
:shape-id id
|
||||
:shape-name name
|
||||
:is-shape-touched touched?
|
||||
:disabled-double-click read-only?
|
||||
:disabled-double-click is-read-only
|
||||
:on-start-edit on-disable-drag
|
||||
:on-stop-edit on-enable-drag
|
||||
:depth depth
|
||||
:is-blocked blocked?
|
||||
:parent-size parent-size
|
||||
:is-selected selected?
|
||||
:type-comp (or component-tree? is-variant-container?)
|
||||
:is-selected is-selected
|
||||
:type-comp (or is-component-tree is-variant-container?)
|
||||
:type-frame (cfh/frame-shape? item)
|
||||
:variant-id variant-id
|
||||
:variant-name variant-name
|
||||
:variant-properties variant-properties
|
||||
:variant-error variant-error
|
||||
:component-id (:id component)
|
||||
:component-id component-id
|
||||
:is-hidden hidden?}]]
|
||||
(when (not read-only?)
|
||||
(when (not ^boolean is-read-only)
|
||||
[:div {:class (stl/css-case
|
||||
:element-actions true
|
||||
:is-parent has-shapes?
|
||||
@@ -185,41 +190,86 @@
|
||||
|
||||
children]))
|
||||
|
||||
;; Memoized for performance
|
||||
(mf/defc layer-item
|
||||
{::mf/props :obj
|
||||
::mf/wrap [mf/memo]}
|
||||
[{:keys [index item selected objects sortable? filtered? depth parent-size component-child? highlighted style render-children?]
|
||||
:or {render-children? true}}]
|
||||
(let [id (:id item)
|
||||
blocked? (:blocked item)
|
||||
hidden? (:hidden item)
|
||||
(mf/defc layer-item*
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [index item selected objects rename-id
|
||||
is-sortable is-filtered depth is-component-child
|
||||
highlighted style render-children parent-size]
|
||||
:or {render-children true}}]
|
||||
(let [id (get item :id)
|
||||
blocked? (get item :blocked)
|
||||
hidden? (get item :hidden)
|
||||
|
||||
shapes (get item :shapes)
|
||||
shapes (mf/with-memo [shapes objects]
|
||||
(loop [counter 0
|
||||
shapes (seq shapes)
|
||||
result (list)]
|
||||
|
||||
(if-let [id (first shapes)]
|
||||
(if-let [obj (get objects id)]
|
||||
(do
|
||||
;; NOTE: this is a bit hacky, but reduces substantially
|
||||
;; the allocation; If we use enumeration, we allocate
|
||||
;; new sequence and add one iteration on each render,
|
||||
;; independently if objects are changed or not. If we
|
||||
;; store counter on metadata, we still need to create a
|
||||
;; new allocation for each shape; with this method we
|
||||
;; bypass this by mutating a private property on the
|
||||
;; object removing extra allocation and extra iteration
|
||||
;; on every request.
|
||||
(unchecked-set obj "__$__counter" counter)
|
||||
(recur (inc counter)
|
||||
(rest shapes)
|
||||
(conj result obj)))
|
||||
(recur (inc counter)
|
||||
(rest shapes)
|
||||
result))
|
||||
|
||||
(-> result vec not-empty))))
|
||||
|
||||
drag-disabled* (mf/use-state false)
|
||||
drag-disabled? (deref drag-disabled*)
|
||||
|
||||
scroll-to-middle? (mf/use-var true)
|
||||
scroll-middle-ref (mf/use-ref true)
|
||||
expanded-iref (mf/with-memo [id]
|
||||
(-> (l/in [:expanded id])
|
||||
(l/derived refs/workspace-local)))
|
||||
expanded? (mf/deref expanded-iref)
|
||||
(l/derived #(dm/get-in % [:expanded id]) refs/workspace-local))
|
||||
is-expanded (mf/deref expanded-iref)
|
||||
|
||||
selected? (contains? selected id)
|
||||
highlighted? (contains? highlighted id)
|
||||
is-selected (contains? selected id)
|
||||
is-highlighted (contains? highlighted id)
|
||||
|
||||
container? (or (cfh/frame-shape? item)
|
||||
(cfh/group-shape? item))
|
||||
|
||||
read-only? (mf/use-ctx ctx/workspace-read-only?)
|
||||
parent-board? (and (cfh/frame-shape? item)
|
||||
is-read-only (mf/use-ctx ctx/workspace-read-only?)
|
||||
root-board? (and (cfh/frame-shape? item)
|
||||
(= uuid/zero (:parent-id item)))
|
||||
|
||||
name-node-ref (mf/use-ref)
|
||||
|
||||
depth (+ depth 1)
|
||||
|
||||
is-component-tree (or ^boolean is-component-child
|
||||
^boolean (ctk/instance-root? item)
|
||||
^boolean (ctk/instance-head? item))
|
||||
|
||||
enable-drag (mf/use-fn #(reset! drag-disabled* false))
|
||||
disable-drag (mf/use-fn #(reset! drag-disabled* true))
|
||||
|
||||
;; Lazy loading of child elements via IntersectionObserver
|
||||
children-count* (mf/use-state 0)
|
||||
children-count (deref children-count*)
|
||||
|
||||
lazy-ref (mf/use-ref nil)
|
||||
observer-ref (mf/use-ref nil)
|
||||
|
||||
toggle-collapse
|
||||
(mf/use-fn
|
||||
(mf/deps expanded?)
|
||||
(mf/deps is-expanded)
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(if (and expanded? (kbd/shift? event))
|
||||
(if (and is-expanded (kbd/shift? event))
|
||||
(st/emit! (dwc/collapse-all))
|
||||
(st/emit! (dwc/toggle-collapse id)))))
|
||||
|
||||
@@ -244,13 +294,13 @@
|
||||
|
||||
select-shape
|
||||
(mf/use-fn
|
||||
(mf/deps id filtered? objects)
|
||||
(mf/deps id is-filtered objects)
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(reset! scroll-to-middle? false)
|
||||
(mf/set-ref-val! scroll-middle-ref false)
|
||||
(cond
|
||||
(kbd/shift? event)
|
||||
(if filtered?
|
||||
(if is-filtered
|
||||
(st/emit! (dw/shift-select-shapes id objects))
|
||||
(st/emit! (dw/shift-select-shapes id)))
|
||||
|
||||
@@ -285,11 +335,11 @@
|
||||
|
||||
on-context-menu
|
||||
(mf/use-fn
|
||||
(mf/deps item read-only?)
|
||||
(mf/deps item is-read-only)
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(when-not read-only?
|
||||
(when-not is-read-only
|
||||
(let [pos (dom/get-client-position event)]
|
||||
(st/emit! (dw/show-shape-context-menu {:position pos :shape item}))))))
|
||||
|
||||
@@ -302,7 +352,7 @@
|
||||
|
||||
on-drop
|
||||
(mf/use-fn
|
||||
(mf/deps id objects expanded? selected)
|
||||
(mf/deps id objects is-expanded selected)
|
||||
(fn [side _data]
|
||||
(let [single? (= (count selected) 1)
|
||||
same? (and single? (= (first selected) id))]
|
||||
@@ -315,32 +365,34 @@
|
||||
(= side :center)
|
||||
id
|
||||
|
||||
(and expanded? (= side :bot) (d/not-empty? (:shapes shape)))
|
||||
(and is-expanded (= side :bot) (d/not-empty? (:shapes shape)))
|
||||
id
|
||||
|
||||
:else
|
||||
(cfh/get-parent-id objects id))
|
||||
|
||||
[parent-id _] (ctn/find-valid-parent-and-frame-ids parent-id objects (map #(get objects %) selected) false files)
|
||||
[parent-id _]
|
||||
(ctn/find-valid-parent-and-frame-ids parent-id objects (map #(get objects %) selected) false files)
|
||||
|
||||
parent (get objects parent-id)
|
||||
current-index (d/index-of (:shapes parent) id)
|
||||
|
||||
to-index (cond
|
||||
(= side :center) 0
|
||||
(and expanded? (= side :bot) (d/not-empty? (:shapes shape))) (count (:shapes parent))
|
||||
(and is-expanded (= side :bot) (d/not-empty? (:shapes shape))) (count (:shapes parent))
|
||||
;; target not found in parent (while lazy loading)
|
||||
(neg? current-index) nil
|
||||
(= side :top) (inc current-index)
|
||||
:else current-index)]
|
||||
|
||||
(when (some? to-index)
|
||||
(st/emit! (dw/relocate-selected-shapes parent-id to-index))))))))
|
||||
|
||||
on-hold
|
||||
(mf/use-fn
|
||||
(mf/deps id expanded?)
|
||||
(mf/deps id is-expanded)
|
||||
(fn []
|
||||
(when-not expanded?
|
||||
(when-not is-expanded
|
||||
(st/emit! (dwc/toggle-collapse id)))))
|
||||
|
||||
zoom-to-selected
|
||||
@@ -361,112 +413,114 @@
|
||||
:data {:id (:id item)
|
||||
:index index
|
||||
:name (:name item)}
|
||||
:draggable? (and
|
||||
sortable?
|
||||
(not read-only?)
|
||||
(not (ctn/has-any-copy-parent? objects item)))) ;; We don't want to change the structure of component copies
|
||||
;; We don't want to change the structure of component copies
|
||||
:draggable? (and ^boolean is-sortable
|
||||
^boolean (not is-read-only)
|
||||
^boolean (not (ctn/has-any-copy-parent? objects item))))]
|
||||
|
||||
ref (mf/use-ref)
|
||||
depth (+ depth 1)
|
||||
component-tree? (or component-child? (ctk/instance-root? item) (ctk/instance-head? item))
|
||||
|
||||
enable-drag (mf/use-fn #(reset! drag-disabled* false))
|
||||
disable-drag (mf/use-fn #(reset! drag-disabled* true))
|
||||
|
||||
;; Lazy loading of child elements via IntersectionObserver
|
||||
children-count* (mf/use-state 0)
|
||||
children-count (deref children-count*)
|
||||
lazy-ref (mf/use-ref nil)
|
||||
observer-var (mf/use-var nil)
|
||||
chunk-size 50]
|
||||
|
||||
(mf/with-effect [selected? selected]
|
||||
(mf/with-effect [is-selected selected]
|
||||
(let [single? (= (count selected) 1)
|
||||
node (mf/ref-val ref)
|
||||
scroll-node (dom/get-parent-with-data node "scroll-container")
|
||||
parent-node (dom/get-parent-at node 2)
|
||||
first-child-node (dom/get-first-child parent-node)
|
||||
node (mf/ref-val name-node-ref)
|
||||
scroll-node (dom/get-parent-with-data node "scroll-container")
|
||||
parent-node (dom/get-parent-at node 2)
|
||||
first-child-node (dom/get-first-child parent-node)
|
||||
scroll-to-middle? (mf/ref-val scroll-middle-ref)
|
||||
|
||||
subid
|
||||
(when (and single? selected? @scroll-to-middle?)
|
||||
(when (and ^boolean single?
|
||||
^boolean is-selected
|
||||
^boolean scroll-to-middle?)
|
||||
(ts/schedule
|
||||
100
|
||||
#(when (and node scroll-node)
|
||||
(let [scroll-distance-ratio (dom/get-scroll-distance-ratio node scroll-node)
|
||||
scroll-behavior (if (> scroll-distance-ratio 1) "instant" "smooth")]
|
||||
(dom/scroll-into-view-if-needed! first-child-node #js {:block "center" :behavior scroll-behavior :inline "start"})
|
||||
(reset! scroll-to-middle? true)))))]
|
||||
(mf/set-ref-val! scroll-middle-ref true)))))]
|
||||
|
||||
#(when (some? subid)
|
||||
(rx/dispose! subid))))
|
||||
|
||||
;; Setup scroll-driven lazy loading when expanded
|
||||
;; and ensures selected children are loaded immediately
|
||||
(mf/with-effect [expanded? (:shapes item) selected]
|
||||
(let [shapes-vec (:shapes item)
|
||||
total (count shapes-vec)]
|
||||
(if expanded?
|
||||
(mf/with-effect [is-expanded shapes selected]
|
||||
(let [total (count shapes)]
|
||||
(if ^boolean is-expanded
|
||||
(let [;; Children are rendered in reverse order, so index 0 in render = last in shapes-vec
|
||||
;; Find if any selected id is a direct child and get its render index
|
||||
selected-child-render-idx
|
||||
(when (and (> total chunk-size) (seq selected))
|
||||
(let [shapes-reversed (vec (reverse shapes-vec))]
|
||||
(some (fn [sel-id]
|
||||
(let [idx (.indexOf shapes-reversed sel-id)]
|
||||
(when (>= idx 0) idx)))
|
||||
selected)))
|
||||
(when (> total default-chunk-size)
|
||||
(some (fn [sel-id]
|
||||
(let [idx (.indexOf shapes sel-id)]
|
||||
(when (>= idx 0) idx)))
|
||||
selected))
|
||||
|
||||
;; Load at least enough to include the selected child plus extra
|
||||
;; for context (so it can be centered in the scroll view)
|
||||
min-count (if selected-child-render-idx
|
||||
(+ selected-child-render-idx chunk-size)
|
||||
chunk-size)
|
||||
current @children-count*
|
||||
new-count (min total (max current chunk-size min-count))]
|
||||
min-count
|
||||
(if selected-child-render-idx
|
||||
(+ selected-child-render-idx default-chunk-size)
|
||||
default-chunk-size)
|
||||
|
||||
current-count
|
||||
@children-count*
|
||||
|
||||
new-count
|
||||
(mth/min total (mth/max current-count default-chunk-size min-count))]
|
||||
|
||||
(reset! children-count* new-count))
|
||||
(reset! children-count* 0))))
|
||||
|
||||
(reset! children-count* 0))
|
||||
|
||||
(fn []
|
||||
(when-let [obs (mf/ref-val observer-ref)]
|
||||
(.disconnect obs)
|
||||
(mf/set-ref-val! obs nil)))))
|
||||
|
||||
;; Re-observe sentinel whenever children-count changes (sentinel moves)
|
||||
;; and (shapes item) to reconnect observer after shape changes
|
||||
(mf/with-effect [children-count expanded? (:shapes item)]
|
||||
(let [total (count (:shapes item))
|
||||
node (mf/ref-val ref)
|
||||
scroll-node (dom/get-parent-with-data node "scroll-container")
|
||||
lazy-node (mf/ref-val lazy-ref)]
|
||||
(mf/with-effect [children-count is-expanded shapes]
|
||||
(let [total (count shapes)
|
||||
name-node (mf/ref-val name-node-ref)
|
||||
scroll-node (dom/get-parent-with-data name-node "scroll-container")
|
||||
lazy-node (mf/ref-val lazy-ref)]
|
||||
|
||||
;; Disconnect previous observer
|
||||
(when-let [obs ^js @observer-var]
|
||||
(when-let [obs (mf/ref-val observer-ref)]
|
||||
(.disconnect obs)
|
||||
(reset! observer-var nil))
|
||||
(mf/set-ref-val! observer-ref nil))
|
||||
|
||||
;; Setup new observer if there are more children to load
|
||||
(when (and expanded?
|
||||
(< children-count total)
|
||||
scroll-node
|
||||
lazy-node)
|
||||
(when (and ^boolean is-expanded
|
||||
^boolean (< children-count total)
|
||||
^boolean scroll-node
|
||||
^boolean lazy-node)
|
||||
(let [cb (fn [entries]
|
||||
(when (and (seq entries)
|
||||
(.-isIntersecting (first entries)))
|
||||
(when (and (pos? (alength entries))
|
||||
(.-isIntersecting ^js (aget entries 0)))
|
||||
;; Load next chunk when sentinel intersects
|
||||
(let [current @children-count*
|
||||
next-count (min total (+ current chunk-size))]
|
||||
(let [next-count (mth/min total (+ children-count default-chunk-size))]
|
||||
(reset! children-count* next-count))))
|
||||
observer (js/IntersectionObserver. cb #js {:root scroll-node})]
|
||||
(.observe observer lazy-node)
|
||||
(reset! observer-var observer)))))
|
||||
(mf/set-ref-val! observer-ref observer)))))
|
||||
|
||||
[:& layer-item-inner
|
||||
[:> layer-item-inner*
|
||||
{:ref dref
|
||||
:item item
|
||||
:depth depth
|
||||
:parent-size parent-size
|
||||
:name-ref ref
|
||||
:read-only? read-only?
|
||||
:highlighted? highlighted?
|
||||
:selected? selected?
|
||||
:component-tree? component-tree?
|
||||
:filtered? filtered?
|
||||
:expanded? expanded?
|
||||
:dnd-over? (= (:over dprops) :center)
|
||||
:dnd-over-top? (= (:over dprops) :top)
|
||||
:dnd-over-bot? (= (:over dprops) :bot)
|
||||
:name-ref name-node-ref
|
||||
:rename-id rename-id
|
||||
:is-read-only is-read-only
|
||||
:is-highlighted is-highlighted
|
||||
:is-selected is-selected
|
||||
:is-component-tree is-component-tree
|
||||
:is-filtered is-filtered
|
||||
:is-expanded is-expanded
|
||||
:dnd-over (= (:over dprops) :center)
|
||||
:dnd-over-top (= (:over dprops) :top)
|
||||
:dnd-over-bot (= (:over dprops) :bot)
|
||||
:on-select-shape select-shape
|
||||
:on-context-menu on-context-menu
|
||||
:on-pointer-enter on-pointer-enter
|
||||
@@ -479,29 +533,28 @@
|
||||
:on-toggle-blocking toggle-blocking
|
||||
:style style}
|
||||
|
||||
(when (and render-children?
|
||||
(:shapes item)
|
||||
expanded?)
|
||||
(when (and ^boolean render-children
|
||||
^boolean shapes
|
||||
^boolean is-expanded)
|
||||
[:div {:class (stl/css-case
|
||||
:element-children true
|
||||
:parent-selected selected?
|
||||
:sticky-children parent-board?)
|
||||
:parent-selected is-selected
|
||||
:sticky-children root-board?)
|
||||
:data-testid (dm/str "children-" id)}
|
||||
(let [all-children (reverse (d/enumerate (:shapes item)))
|
||||
visible (take children-count all-children)]
|
||||
(for [[index id] visible]
|
||||
(when-let [item (get objects id)]
|
||||
[:& layer-item
|
||||
{:item item
|
||||
:highlighted highlighted
|
||||
:selected selected
|
||||
:index index
|
||||
:objects objects
|
||||
:key (dm/str id)
|
||||
:sortable? sortable?
|
||||
:depth depth
|
||||
:parent-size parent-size
|
||||
:component-child? component-tree?}])))
|
||||
(when (< children-count (count (:shapes item)))
|
||||
(for [item (take children-count shapes)]
|
||||
[:> layer-item*
|
||||
{:item item
|
||||
:rename-id rename-id
|
||||
:highlighted highlighted
|
||||
:selected selected
|
||||
:index (unchecked-get item "__$__counter")
|
||||
:objects objects
|
||||
:key (dm/str (get item :id))
|
||||
:is-sortable is-sortable
|
||||
:depth depth
|
||||
:parent-size parent-size
|
||||
:is-component-child is-component-tree}])
|
||||
|
||||
(when (< children-count (count shapes))
|
||||
[:div {:ref lazy-ref
|
||||
:class (stl/css :lazy-load-sentinel)}])])]))
|
||||
|
||||
@@ -16,39 +16,35 @@
|
||||
[app.util.dom :as dom]
|
||||
[app.util.keyboard :as kbd]
|
||||
[cuerdas.core :as str]
|
||||
[okulary.core :as l]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def ^:private space-for-icons 110)
|
||||
|
||||
(def lens:shape-for-rename
|
||||
(-> (l/in [:workspace-local :shape-for-rename])
|
||||
(l/derived st/state)))
|
||||
(def ^:private ^:const space-for-icons 110)
|
||||
|
||||
(mf/defc layer-name*
|
||||
{::mf/wrap-props false
|
||||
::mf/forward-ref true}
|
||||
[{:keys [shape-id shape-name is-shape-touched disabled-double-click
|
||||
[{:keys [shape-id rename-id shape-name is-shape-touched disabled-double-click
|
||||
on-start-edit on-stop-edit depth parent-size is-selected
|
||||
type-comp type-frame component-id is-hidden is-blocked
|
||||
variant-id variant-name variant-properties variant-error]} external-ref]
|
||||
variant-id variant-name variant-properties variant-error ref]}]
|
||||
|
||||
(let [edition* (mf/use-state false)
|
||||
edition? (deref edition*)
|
||||
|
||||
local-ref (mf/use-ref)
|
||||
ref (d/nilv external-ref local-ref)
|
||||
ref (d/nilv ref local-ref)
|
||||
|
||||
shape-for-rename (mf/deref lens:shape-for-rename)
|
||||
shape-name
|
||||
(if variant-id
|
||||
(d/nilv variant-error variant-name)
|
||||
shape-name)
|
||||
|
||||
shape-name (if variant-id
|
||||
(d/nilv variant-error variant-name)
|
||||
shape-name)
|
||||
default-value
|
||||
(mf/with-memo [variant-id variant-error variant-properties]
|
||||
(if variant-id
|
||||
(or variant-error (ctv/properties-map->formula variant-properties))
|
||||
shape-name))
|
||||
|
||||
default-value (if variant-id
|
||||
(or variant-error (ctv/properties-map->formula variant-properties))
|
||||
shape-name)
|
||||
|
||||
has-path? (str/includes? shape-name "/")
|
||||
has-path?
|
||||
(str/includes? shape-name "/")
|
||||
|
||||
start-edit
|
||||
(mf/use-fn
|
||||
@@ -85,10 +81,11 @@
|
||||
(when (kbd/enter? event) (accept-edit))
|
||||
(when (kbd/esc? event) (cancel-edit))))
|
||||
|
||||
parent-size (dm/str (- parent-size space-for-icons) "px")]
|
||||
parent-size
|
||||
(dm/str (- parent-size space-for-icons) "px")]
|
||||
|
||||
(mf/with-effect [shape-for-rename edition? start-edit shape-id]
|
||||
(when (and (= shape-for-rename shape-id)
|
||||
(mf/with-effect [rename-id edition? start-edit shape-id]
|
||||
(when (and (= rename-id shape-id)
|
||||
(not ^boolean edition?))
|
||||
(start-edit)))
|
||||
|
||||
@@ -110,21 +107,24 @@
|
||||
:auto-focus true
|
||||
:id (dm/str "layer-name-" shape-id)
|
||||
:default-value (d/nilv default-value "")}]
|
||||
|
||||
[:*
|
||||
[:span
|
||||
{:class (stl/css-case
|
||||
:element-name true
|
||||
:left-ellipsis has-path?
|
||||
:selected is-selected
|
||||
:hidden is-hidden
|
||||
:type-comp type-comp
|
||||
:type-frame type-frame)
|
||||
:id (dm/str "layer-name-" shape-id)
|
||||
:style {"--depth" depth "--parent-size" parent-size}
|
||||
:ref ref
|
||||
:on-double-click start-edit}
|
||||
(if (dbg/enabled? :show-ids)
|
||||
(str (d/nilv shape-name "") " | " (str/slice (str shape-id) 24))
|
||||
[:span {:class (stl/css-case
|
||||
:element-name true
|
||||
:left-ellipsis has-path?
|
||||
:selected is-selected
|
||||
:hidden is-hidden
|
||||
:type-comp type-comp
|
||||
:type-frame type-frame)
|
||||
:id (dm/str "layer-name-" shape-id)
|
||||
:style {"--depth" depth "--parent-size" parent-size}
|
||||
:ref ref
|
||||
:on-double-click start-edit}
|
||||
|
||||
(if ^boolean (dbg/enabled? :show-ids)
|
||||
(dm/str (d/nilv shape-name "") " | " (str/slice (str shape-id) 24))
|
||||
(d/nilv shape-name ""))]
|
||||
(when (and (dbg/enabled? :show-touched) ^boolean is-shape-touched)
|
||||
|
||||
(when (and ^boolean (dbg/enabled? :show-touched)
|
||||
^boolean is-shape-touched)
|
||||
[:span {:class (stl/css :element-name-touched)} "*"])])))
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[app.main.ui.notifications.badge :refer [badge-notification]]
|
||||
[app.main.ui.workspace.sidebar.layer-item :refer [layer-item]]
|
||||
[app.main.ui.workspace.sidebar.layer-item :refer [layer-item*]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.globals :as globals]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
@@ -31,92 +31,160 @@
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[goog.events :as events]
|
||||
[rumext.v2 :as mf])
|
||||
(:import
|
||||
goog.events.EventType))
|
||||
[okulary.core :as l]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def ^:private ref:highlighted-shapes
|
||||
(l/derived (fn [local]
|
||||
(-> local
|
||||
(get :highlighted)
|
||||
(not-empty)))
|
||||
refs/workspace-local))
|
||||
|
||||
(def ^:private ref:shape-for-rename
|
||||
(l/derived (l/key :shape-for-rename) refs/workspace-local))
|
||||
|
||||
(defn- use-selected-shapes
|
||||
"A convencience hook wrapper for get selected shapes"
|
||||
[]
|
||||
(let [selected (mf/deref refs/selected-shapes)]
|
||||
(hooks/use-equal-memo selected)))
|
||||
|
||||
;; This components is a piece for sharding equality check between top
|
||||
;; level frames and try to avoid rerender frames that are does not
|
||||
;; affected by the selected set.
|
||||
(mf/defc frame-wrapper
|
||||
{::mf/props :obj}
|
||||
(mf/defc frame-wrapper*
|
||||
[{:keys [selected] :as props}]
|
||||
(let [pending-selected (mf/use-var selected)
|
||||
current-selected (mf/use-state selected)
|
||||
props (mf/spread-object props {:selected @current-selected})
|
||||
(let [pending-selected-ref
|
||||
(mf/use-ref selected)
|
||||
|
||||
current-selected
|
||||
(mf/use-state selected)
|
||||
|
||||
props
|
||||
(mf/spread-object props {:selected @current-selected})
|
||||
|
||||
set-selected
|
||||
(mf/use-memo
|
||||
(fn []
|
||||
(throttle-fn
|
||||
50
|
||||
#(when-let [pending-selected @pending-selected]
|
||||
(reset! current-selected pending-selected)))))]
|
||||
(mf/with-memo []
|
||||
(throttle-fn 50 #(when-let [pending-selected (mf/ref-val pending-selected-ref)]
|
||||
(reset! current-selected pending-selected))))]
|
||||
|
||||
(mf/with-effect [selected set-selected]
|
||||
(reset! pending-selected selected)
|
||||
(set-selected)
|
||||
(mf/set-ref-val! pending-selected-ref selected)
|
||||
(^function set-selected)
|
||||
(fn []
|
||||
(reset! pending-selected nil)
|
||||
#(rx/dispose! set-selected)))
|
||||
(mf/set-ref-val! pending-selected-ref nil)
|
||||
(rx/dispose! set-selected)))
|
||||
|
||||
[:> layer-item props]))
|
||||
[:> layer-item* props]))
|
||||
|
||||
(mf/defc layers-tree*
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [objects is-filtered parent-size] :as props}]
|
||||
(let [selected (use-selected-shapes)
|
||||
highlighted (mf/deref ref:highlighted-shapes)
|
||||
root (get objects uuid/zero)
|
||||
|
||||
rename-id (mf/deref ref:shape-for-rename)
|
||||
|
||||
shapes (get root :shapes)
|
||||
shapes (mf/with-memo [shapes objects]
|
||||
(loop [counter 0
|
||||
shapes (seq shapes)
|
||||
result (list)]
|
||||
(if-let [id (first shapes)]
|
||||
(if-let [obj (get objects id)]
|
||||
(do
|
||||
;; NOTE: this is a bit hacky, but reduces substantially
|
||||
;; the allocation; If we use enumeration, we allocate
|
||||
;; new sequence and add one iteration on each render,
|
||||
;; independently if objects are changed or not. If we
|
||||
;; store counter on metadata, we still need to create a
|
||||
;; new allocation for each shape; with this method we
|
||||
;; bypass this by mutating a private property on the
|
||||
;; object removing extra allocation and extra iteration
|
||||
;; on every request.
|
||||
(unchecked-set obj "__$__counter" counter)
|
||||
(recur (inc counter)
|
||||
(rest shapes)
|
||||
(conj result obj)))
|
||||
(recur (inc counter)
|
||||
(rest shapes)
|
||||
result))
|
||||
result)))]
|
||||
|
||||
(mf/defc layers-tree
|
||||
{::mf/wrap [mf/memo #(mf/throttle % 200)]
|
||||
::mf/wrap-props false}
|
||||
[{:keys [objects filtered? parent-size] :as props}]
|
||||
(let [selected (mf/deref refs/selected-shapes)
|
||||
selected (hooks/use-equal-memo selected)
|
||||
highlighted (mf/deref refs/highlighted-shapes)
|
||||
highlighted (hooks/use-equal-memo highlighted)
|
||||
root (get objects uuid/zero)]
|
||||
[:div {:class (stl/css :element-list) :data-testid "layer-item"}
|
||||
[:> hooks/sortable-container* {}
|
||||
(for [[index id] (reverse (d/enumerate (:shapes root)))]
|
||||
(when-let [obj (get objects id)]
|
||||
(if (cfh/frame-shape? obj)
|
||||
[:& frame-wrapper
|
||||
{:item obj
|
||||
:selected selected
|
||||
:highlighted highlighted
|
||||
:index index
|
||||
:objects objects
|
||||
:key id
|
||||
:sortable? true
|
||||
:filtered? filtered?
|
||||
:parent-size parent-size
|
||||
:depth -1}]
|
||||
[:& layer-item
|
||||
{:item obj
|
||||
:selected selected
|
||||
:highlighted highlighted
|
||||
:index index
|
||||
:objects objects
|
||||
:key id
|
||||
:sortable? true
|
||||
:filtered? filtered?
|
||||
:depth -1
|
||||
:parent-size parent-size}])))]]))
|
||||
(for [obj shapes]
|
||||
(if (cfh/frame-shape? obj)
|
||||
[:> frame-wrapper*
|
||||
{:item obj
|
||||
:rename-id rename-id
|
||||
:selected selected
|
||||
:highlighted highlighted
|
||||
:index (unchecked-get obj "__$__counter")
|
||||
:objects objects
|
||||
:key (dm/str (get obj :id))
|
||||
:is-sortable true
|
||||
:is-filtered is-filtered
|
||||
:parent-size parent-size
|
||||
:depth -1}]
|
||||
[:> layer-item*
|
||||
{:item obj
|
||||
:rename-id rename-id
|
||||
:selected selected
|
||||
:highlighted highlighted
|
||||
:index (unchecked-get obj "__$__counter")
|
||||
:objects objects
|
||||
:key (dm/str (get obj :id))
|
||||
:is-sortable true
|
||||
:is-filtered is-filtered
|
||||
:depth -1
|
||||
:parent-size parent-size}]))]]))
|
||||
|
||||
(mf/defc filters-tree
|
||||
{::mf/wrap [mf/memo #(mf/throttle % 200)]
|
||||
::mf/wrap-props false}
|
||||
(mf/defc layers-tree-wrapper*
|
||||
{::mf/private true}
|
||||
[{:keys [objects] :as props}]
|
||||
;; This is a performance sensitive componet, so we use lower-level primitives for
|
||||
;; reduce residual allocation for this specific case
|
||||
(let [state-tmp (mf/useState objects)
|
||||
objects' (aget state-tmp 0)
|
||||
set-objects (aget state-tmp 1)
|
||||
|
||||
subject-s (mf/with-memo []
|
||||
(rx/subject))
|
||||
changes-s (mf/with-memo [subject-s]
|
||||
(->> subject-s
|
||||
(rx/debounce 500)))
|
||||
|
||||
props (mf/spread-props props {:objects objects'})]
|
||||
|
||||
(mf/with-effect [objects subject-s]
|
||||
(rx/push! subject-s objects))
|
||||
|
||||
(mf/with-effect [changes-s]
|
||||
(let [sub (rx/subscribe changes-s set-objects)]
|
||||
#(rx/dispose! sub)))
|
||||
|
||||
[:> layers-tree* props]))
|
||||
|
||||
(mf/defc filters-tree*
|
||||
{::mf/wrap [mf/memo #(mf/throttle % 300)]
|
||||
::mf/private true}
|
||||
[{:keys [objects parent-size]}]
|
||||
(let [selected (mf/deref refs/selected-shapes)
|
||||
selected (hooks/use-equal-memo selected)
|
||||
root (get objects uuid/zero)]
|
||||
(let [selected (use-selected-shapes)
|
||||
root (get objects uuid/zero)]
|
||||
[:ul {:class (stl/css :element-list)}
|
||||
(for [[index id] (d/enumerate (:shapes root))]
|
||||
(when-let [obj (get objects id)]
|
||||
[:& layer-item
|
||||
[:> layer-item*
|
||||
{:item obj
|
||||
:selected selected
|
||||
:index index
|
||||
:objects objects
|
||||
:key id
|
||||
:sortable? false
|
||||
:filtered? true
|
||||
:is-sortable false
|
||||
:is-filtered true
|
||||
:depth -1
|
||||
:parent-size parent-size}]))]))
|
||||
|
||||
@@ -132,6 +200,7 @@
|
||||
keys
|
||||
(filter #(not= uuid/zero %))
|
||||
vec)]
|
||||
|
||||
(update reparented-objects uuid/zero assoc :shapes reparented-shapes)))
|
||||
|
||||
;; --- Layers Toolbox
|
||||
@@ -277,9 +346,11 @@
|
||||
(swap! state* update :num-items + 100))))]
|
||||
|
||||
(mf/with-effect []
|
||||
(let [keys [(events/listen globals/document EventType.KEYDOWN on-key-down)
|
||||
(events/listen globals/document EventType.CLICK hide-menu)]]
|
||||
(fn [] (doseq [key keys] (events/unlistenByKey key)))))
|
||||
(let [key1 (events/listen globals/document "keydown" on-key-down)
|
||||
key2 (events/listen globals/document "click" hide-menu)]
|
||||
(fn []
|
||||
(events/unlistenByKey key1)
|
||||
(events/unlistenByKey key2))))
|
||||
|
||||
[filtered-objects
|
||||
handle-show-more
|
||||
@@ -464,6 +535,8 @@
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [size-parent]}]
|
||||
(let [page (mf/deref refs/workspace-page)
|
||||
page-id (get page :id)
|
||||
|
||||
focus (mf/deref refs/workspace-focus-selected)
|
||||
|
||||
objects (hooks/with-focus-objects (:objects page) focus)
|
||||
@@ -473,7 +546,8 @@
|
||||
observer-var (mf/use-var nil)
|
||||
lazy-load-ref (mf/use-ref nil)
|
||||
|
||||
[filtered-objects show-more filter-component] (use-search page objects)
|
||||
[filtered-objects show-more filter-component]
|
||||
(use-search page objects)
|
||||
|
||||
intersection-callback
|
||||
(fn [entries]
|
||||
@@ -519,25 +593,25 @@
|
||||
[:div {:class (stl/css :tool-window-content)
|
||||
:data-scroll-container true
|
||||
:ref on-render-container}
|
||||
[:& filters-tree {:objects filtered-objects
|
||||
:key (dm/str (:id page))
|
||||
:parent-size size-parent}]
|
||||
[:> filters-tree* {:objects filtered-objects
|
||||
:key (dm/str page-id)
|
||||
:parent-size size-parent}]
|
||||
[:div {:ref lazy-load-ref}]]
|
||||
[:div {:on-scroll on-scroll
|
||||
:class (stl/css :tool-window-content)
|
||||
:data-scroll-container true
|
||||
:style {:display (when (some? filtered-objects) "none")}}
|
||||
|
||||
[:& layers-tree {:objects filtered-objects
|
||||
:key (dm/str (:id page))
|
||||
:filtered? true
|
||||
:parent-size size-parent}]]]
|
||||
[:> layers-tree-wrapper* {:objects filtered-objects
|
||||
:key (dm/str page-id)
|
||||
:is-filtered true
|
||||
:parent-size size-parent}]]]
|
||||
|
||||
[:div {:on-scroll on-scroll
|
||||
:class (stl/css :tool-window-content)
|
||||
:data-scroll-container true
|
||||
:style {:display (when (some? filtered-objects) "none")}}
|
||||
[:& layers-tree {:objects objects
|
||||
:key (dm/str (:id page))
|
||||
:filtered? false
|
||||
:parent-size size-parent}]])]))
|
||||
[:> layers-tree-wrapper* {:objects objects
|
||||
:key (dm/str page-id)
|
||||
:is-filtered false
|
||||
:parent-size size-parent}]])]))
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
;; 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 app.main.ui.workspace.tokens.import-from-library
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.workspace.tokens.library-edit :as dwtl]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
[app.main.ui.ds.foundations.typography :as t]
|
||||
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
|
||||
[app.main.ui.ds.foundations.typography.text :refer [text*]]
|
||||
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[okulary.core :as l]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
|
||||
(mf/defc import-modal-library*
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :tokens/import-from-library}
|
||||
[all-props]
|
||||
(let [{:keys [file-id library-id]}
|
||||
(js->clj all-props :keywordize-keys true)
|
||||
|
||||
library-file-ref (mf/with-memo [library-id]
|
||||
(l/derived (fn [state]
|
||||
(dm/get-in state [:files library-id :data]))
|
||||
st/state))
|
||||
library-data (mf/deref library-file-ref)
|
||||
|
||||
show-libraries-dialog
|
||||
(mf/use-fn
|
||||
(mf/deps file-id)
|
||||
(fn []
|
||||
(modal/hide!)
|
||||
(modal/show! :libraries-dialog {:file-id file-id})))
|
||||
|
||||
cancel
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(show-libraries-dialog)))
|
||||
|
||||
import
|
||||
(mf/use-fn
|
||||
(mf/deps file-id library-id library-data)
|
||||
(fn []
|
||||
(let [tokens-lib (:tokens-lib library-data)]
|
||||
(st/emit! (dwtl/import-tokens-lib tokens-lib)))
|
||||
(show-libraries-dialog)))]
|
||||
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div {:class (stl/css :modal-dialog)}
|
||||
[:> icon-button* {:class (stl/css :close-btn)
|
||||
:on-click cancel
|
||||
:aria-label (tr "labels.close")
|
||||
:variant "ghost"
|
||||
:icon i/close}]
|
||||
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:> heading* {:level 2
|
||||
:id "modal-title"
|
||||
:typography "headline-large"
|
||||
:class (stl/css :modal-title)}
|
||||
(tr "modals.import-library-tokens.title")]]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:> text* {:as "p" :typography t/body-medium} (tr "modals.import-library-tokens.description")]]
|
||||
|
||||
[:> context-notification* {:type :context
|
||||
:appearance "neutral"
|
||||
:level "default"
|
||||
:is-html true}
|
||||
(tr "workspace.tokens.import-warning")]
|
||||
|
||||
[:div {:class (stl/css :modal-footer)}
|
||||
[:div {:class (stl/css :action-buttons)}
|
||||
[:> button* {:on-click cancel
|
||||
:type "button"
|
||||
:variant "secondary"}
|
||||
(tr "labels.cancel")]
|
||||
[:> button* {:on-click import
|
||||
:type "button"
|
||||
:variant "primary"}
|
||||
(tr "modals.import-library-tokens.import")]]]]]))
|
||||
@@ -1,70 +0,0 @@
|
||||
// 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
|
||||
|
||||
@use "refactor/common-refactor.scss" as deprecated;
|
||||
|
||||
@use "ds/typography.scss" as t;
|
||||
@use "ds/_borders.scss" as *;
|
||||
@use "ds/_sizes.scss" as *;
|
||||
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
inset-block-start: var(--sp-s);
|
||||
inset-inline-end: var(--sp-s);
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
--modal-title-foreground-color: var(--color-foreground-primary);
|
||||
--modal-text-foreground-color: var(--color-foreground-secondary);
|
||||
|
||||
@extend .modal-overlay-base;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: fixed;
|
||||
inset-inline-start: 0;
|
||||
inset-block-start: 0;
|
||||
block-size: 100%;
|
||||
inline-size: 100%;
|
||||
background-color: var(--overlay-color);
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
@extend .modal-container-base;
|
||||
inline-size: 100%;
|
||||
max-inline-size: 32rem;
|
||||
max-block-size: unset;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
margin-block-end: var(--sp-xxl);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
@include t.use-typography("headline-medium");
|
||||
color: var(--modal-title-foreground-color);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
@include t.use-typography("body-large");
|
||||
color: var(--modal-text-foreground-color);
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
margin-block-start: var(--sp-xxl);
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
@extend .modal-action-btns;
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
@@ -143,7 +143,8 @@
|
||||
(let [token-ids (set tokens-in-path-ids)
|
||||
remaining-tokens (filter (fn [token]
|
||||
(not (contains? token-ids (:id token))))
|
||||
selected-token-set-tokens)]
|
||||
selected-token-set-tokens)
|
||||
_ (prn "Remaining tokens:" remaining-tokens)]
|
||||
(seq remaining-tokens))))
|
||||
|
||||
delete-token
|
||||
|
||||
@@ -13,10 +13,8 @@
|
||||
[app.common.types.color :as cl]
|
||||
[app.common.types.token :as cto]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.config :as cf]
|
||||
[app.main.data.style-dictionary :as sd]
|
||||
[app.main.data.tinycolor :as tinycolor]
|
||||
[app.main.data.tokenscript :as ts]
|
||||
[app.main.data.workspace.tokens.format :as dwtf]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.ui.ds.controls.input :as ds]
|
||||
@@ -72,15 +70,11 @@
|
||||
(dissoc (:name prev-token))
|
||||
(update (:name token) #(ctob/make-token (merge % prev-token token))))]
|
||||
|
||||
(->> (if (contains? cf/flags :tokenscript)
|
||||
(rx/of (ts/resolve-tokens tokens))
|
||||
(sd/resolve-tokens-interactive tokens))
|
||||
(->> tokens
|
||||
(sd/resolve-tokens-interactive)
|
||||
(rx/mapcat
|
||||
(fn [resolved-tokens]
|
||||
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))
|
||||
resolved-value (if (contains? cf/flags :tokenscript)
|
||||
(ts/tokenscript-symbols->penpot-unit resolved-value)
|
||||
resolved-value)]
|
||||
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))]
|
||||
(if resolved-value
|
||||
(rx/of {:value resolved-value})
|
||||
(rx/of {:error (first errors)}))))))))
|
||||
|
||||
@@ -10,9 +10,7 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.types.token :as cto]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.config :as cf]
|
||||
[app.main.data.style-dictionary :as sd]
|
||||
[app.main.data.tokenscript :as ts]
|
||||
[app.main.fonts :as fonts]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.controls.input :refer [input*]]
|
||||
@@ -51,30 +49,28 @@
|
||||
;; validate data within the form state.
|
||||
|
||||
(defn- resolve-value
|
||||
[tokens prev-token _token-name value]
|
||||
(let [tmp-value (cto/split-font-family value)
|
||||
tmp-name "__PENPOT__FONT_FAMILY__PLACEHOLDER__"
|
||||
[tokens prev-token token-name value]
|
||||
(let [valid-token-name?
|
||||
(and (string? token-name)
|
||||
(re-matches cto/token-name-validation-regex token-name))
|
||||
|
||||
;; Create a temporary font-family token to validate the value
|
||||
token
|
||||
{:name tmp-name
|
||||
:type :font-family
|
||||
:value (if (= (:type prev-token) :typography)
|
||||
(assoc (:value prev-token) :font-family tmp-value)
|
||||
tmp-value)}
|
||||
{:value (cto/split-font-family value)
|
||||
:name (if (or (not valid-token-name?) (str/blank? token-name))
|
||||
"__PENPOT__TOKEN__NAME__PLACEHOLDER__"
|
||||
token-name)}
|
||||
|
||||
tokens
|
||||
(update tokens (:name token) #(ctob/make-token (merge % prev-token token)))]
|
||||
(-> tokens
|
||||
;; Remove previous token when renaming a token
|
||||
(dissoc (:name prev-token))
|
||||
(update (:name token) #(ctob/make-token (merge % prev-token token))))]
|
||||
|
||||
(->> (if (contains? cf/flags :tokenscript)
|
||||
(rx/of (ts/resolve-tokens tokens))
|
||||
(sd/resolve-tokens-interactive tokens))
|
||||
(->> tokens
|
||||
(sd/resolve-tokens-interactive)
|
||||
(rx/mapcat
|
||||
(fn [resolved-tokens]
|
||||
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))
|
||||
resolved-value (if (contains? cf/flags :tokenscript)
|
||||
(ts/tokenscript-symbols->penpot-unit resolved-value)
|
||||
resolved-value)]
|
||||
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))]
|
||||
(if resolved-value
|
||||
(rx/of {:value resolved-value})
|
||||
(rx/of {:error (first errors)}))))))))
|
||||
@@ -180,6 +176,7 @@
|
||||
(let [message (tr "workspace.tokens.resolved-value" value)]
|
||||
(swap! form update :extra-errors dissoc input-name)
|
||||
(reset! hint* {:message message :type "hint"})))))))]
|
||||
|
||||
(fn []
|
||||
(rx/dispose! subs))))
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user