Compare commits

..

22 Commits

Author SHA1 Message Date
Aitor Moreno
740e790585 🎉 Add active-features? helper function (#8490) 2026-02-27 12:12:27 +01:00
Elena Torró
ed23c55550 Merge pull request #8483 from penpot/superalex-fix-opacity-for-dotted-strokes
🐛 Fix opacity for dotted strokes
2026-02-26 13:41:43 +01:00
Alejandro Alonso
5b5c868a87 🐛 Fix opacity for dotted strokes 2026-02-26 13:31:12 +01:00
Alejandro Alonso
1a3ac6bdf8 Merge pull request #8475 from penpot/elenatorro-13524-fix-token-highlight
🐛 Fix rotation token highlight and its application on the text-ed…
2026-02-26 13:00:45 +01:00
Elena Torró
de5d4f4292 Merge pull request #8460 from penpot/azazeln28-refactor-text-cursor
♻️ Refactor TextCursor and TextPositionWithAffinity
2026-02-26 12:29:43 +01:00
Elena Torro
2bd7c10e09 🔧 Fix variable name from wrong merge 2026-02-26 12:19:20 +01:00
Elena Torro
495371c079 🐛 Fix rotation token highlight and its application on the text-editor-v2 2026-02-26 11:57:11 +01:00
Elena Torró
75b1c0c1b1 Merge pull request #8280 from penpot/niwinz-layers-sidebar-changes
 Add serveral performance optimization to layers sidebar
2026-02-26 11:37:57 +01:00
Andrey Antukh
5ea4b03108 📎 Fix e2e tests 2026-02-26 11:13:31 +01:00
Andrey Antukh
0fef5b7e5d Memoize variant props on layer-item 2026-02-26 11:13:31 +01:00
Andrey Antukh
8a1fdd9dd1 Reduce watchers for layer-item rename mechanism 2026-02-26 11:13:31 +01:00
Andrey Antukh
a080a9e646 Add micro optimizations to layer-item component 2026-02-26 11:13:31 +01:00
Andrey Antukh
a728d5a5f2 💄 Add minor cosmetic changes to filters-tree component 2026-02-26 11:13:30 +01:00
Andrey Antukh
6072234230 Add more selective debouncing for layers-tree 2026-02-26 11:13:30 +01:00
Andrey Antukh
41f2877801 Reduce allocation on layers-tree component 2026-02-26 11:13:30 +01:00
Andrey Antukh
e2576d049a 💄 Add minor cosmetic changes on event listening 2026-02-26 11:13:30 +01:00
Andrey Antukh
4db9c373e6 💄 Fix component naming style related to layer-item 2026-02-26 11:13:30 +01:00
Andrey Antukh
09a9407867 💄 Change props naming on layer-item and related components 2026-02-26 11:13:30 +01:00
Andrey Antukh
7be03e2ea6 Remove usage of use-var on layer-item
Focus on use more basic primitves on performance
sensitive components
2026-02-26 11:13:30 +01:00
Aitor Moreno
05165ce014 🐛 Fix board title cropped using wrong side 2026-02-26 09:35:56 +01:00
Aitor Moreno
96677713fc 🐛 Fix 45 rotated board doesn't show title properly 2026-02-26 09:34:15 +01:00
Aitor Moreno
a12b59d101 ♻️ Refactor TextCursor and TextPositionWithAffinity 2026-02-25 09:59:02 +01:00
153 changed files with 2134 additions and 4218 deletions

View File

@@ -1,7 +1,6 @@
name: _DEVELOP
on:
workflow_dispatch:
schedule:
- cron: '16 5-20 * * 1-5'

View File

@@ -1,7 +1,6 @@
name: _STAGING RENDER
on:
workflow_dispatch:
schedule:
- cron: '36 5-20 * * 1-5'

View File

@@ -1,7 +1,6 @@
name: _STAGING
on:
workflow_dispatch:
schedule:
- cron: '36 5-20 * * 1-5'

View File

@@ -1,7 +1,6 @@
name: _TAG
on:
workflow_dispatch:
push:
tags:
- '*'

2
.gitignore vendored
View File

@@ -69,7 +69,7 @@
/frontend/test-results/
/other/
/scripts/
/nexus/
/telemetry/
/tmp/
/vendor/**/target
/vendor/svgclean/bundle*.js

View File

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

View File

@@ -2,7 +2,6 @@
export PENPOT_NITRATE_SHARED_KEY=super-secret-nitrate-api-key
export PENPOT_EXPORTER_SHARED_KEY=super-secret-exporter-api-key
export PENPOT_NEXUS_SHARED_KEY=super-secret-nexus-api-key
export PENPOT_SECRET_KEY=super-secret-devenv-key
# DEPRECATED: only used for subscriptions

View File

@@ -103,7 +103,6 @@
[:exporter-shared-key {:optional true} :string]
[:nitrate-shared-key {:optional true} :string]
[:nexus-shared-key {:optional true} :string]
[:management-api-key {:optional true} :string]
[:telemetry-uri {:optional true} :string]

View File

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

View File

@@ -120,7 +120,7 @@
;; an external storage and data cleared.
(def ^:private schema:event
[:map {:title "AuditEvent"}
[:map {:title "event"}
[::type ::sm/text]
[::name ::sm/text]
[::profile-id ::sm/uuid]

View File

@@ -10,11 +10,14 @@
[app.common.logging :as l]
[app.common.schema :as sm]
[app.common.transit :as t]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.http.client :as http]
[app.setup :as-alias setup]
[app.tokens :as tokens]
[integrant.core :as ig]
[lambdaisland.uri :as u]
[promesa.exec :as px]))
;; This is a task responsible to send the accumulated events to
@@ -49,18 +52,19 @@
(defn- send!
[{:keys [::uri] :as cfg} events]
(let [skey (-> cfg ::setup/shared-keys :nexus)
(let [token (tokens/generate cfg
{:iss "authentication"
:uid uuid/zero})
body (t/encode {:events events})
headers {"content-type" "application/transit+json"
"origin" (str (cf/get :public-uri))
"x-shared-key" (str "nexus " skey)}
"cookie" (u/map->query-string {:auth-token token})}
params {:uri uri
:timeout 12000
:method :post
:headers headers
:body body}
resp (http/req! cfg params)]
(if (= (:status resp) 204)
true
(do
@@ -105,7 +109,7 @@
(def ^:private schema:handler-params
[:map
::db/pool
::setup/shared-keys
::setup/props
::http/client])
(defmethod ig/assert-key ::handler

View File

@@ -466,17 +466,16 @@
::setup/shared-keys
{::setup/props (ig/ref ::setup/props)
:nexus (cf/get :nexus-shared-key)
:nitrate (cf/get :nitrate-shared-key)
:exporter (cf/get :exporter-shared-key)}
:nitrate (cf/get :nitrate-shared-key)
:exporter (cf/get :exporter-shared-key)}
::setup/clock
{}
:app.loggers.audit.archive-task/handler
{::setup/shared-keys (ig/ref ::setup/shared-keys)
::http.client/client (ig/ref ::http.client/client)
::db/pool (ig/ref ::db/pool)}
{::setup/props (ig/ref ::setup/props)
::db/pool (ig/ref ::db/pool)
::http.client/client (ig/ref ::http.client/client)}
:app.loggers.audit.gc-task/handler
{::db/pool (ig/ref ::db/pool)}

View File

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

View File

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

View File

@@ -58,4 +58,3 @@
(when (nil? (:data file))
(migrate-file conn file)))
(db/exec-one! conn ["drop table page cascade;"])))

View File

@@ -1,2 +0,0 @@
ALTER TABLE access_token
ADD COLUMN type text NULL;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -82,37 +82,45 @@
(db/tx-run! cfg (fn [{:keys [::db/conn]}]
(db/xact-lock! conn 0)
(when-not key
(l/wrn :hint (str "using autogenerated secret-key, it will change "
"on each restart and will invalidate "
"all sessions on each restart, it is highly "
"recommended setting up the "
"PENPOT_SECRET_KEY environment variable")))
(l/warn :hint (str "using autogenerated secret-key, it will change on each restart and will invalidate "
"all sessions on each restart, it is highly recommended setting up the "
"PENPOT_SECRET_KEY environment variable")))
(let [secret (or key (generate-random-key))]
(-> (get-all-props conn)
(assoc :secret-key secret)
(assoc :tokens-key (keys/derive secret :salt "tokens"))
(update :instance-id handle-instance-id conn (db/read-only? pool)))))))
(sm/register! ::props [:map-of :keyword ::sm/any])
(defmethod ig/init-key ::shared-keys
[_ {:keys [::props] :as cfg}]
(let [secret (get props :secret-key)]
(reduce (fn [keys id]
(let [key (or (get cfg id)
(-> (keys/derive secret :salt (name id))
(bc/bytes->b64-str true)))]
(if (or (str/empty? key)
(str/blank? key))
(do
(l/wrn :id (name id) :hint "key is disabled because empty string found")
keys)
(do
(l/inf :id (name id) :hint "key initialized" :key (d/obfuscate-string key))
(assoc keys id key)))))
{}
[:exporter
:nitrate
:nexus])))
(d/without-nils
{:exporter
(let [key (or (get cfg :exporter)
(-> (keys/derive secret :salt "exporter")
(bc/bytes->b64-str true)))]
(if (or (str/empty? key)
(str/blank? key))
(do
(l/wrn :hint "exporter key is disabled because empty string found")
nil)
(do
(l/inf :hint "exporter key initialized" :key (d/obfuscate-string key))
key)))
(sm/register! ::props [:map-of :keyword ::sm/any])
(sm/register! ::shared-keys [:map-of :keyword ::sm/text])
:nitrate
(let [key (or (get cfg :nitrate)
(-> (keys/derive secret :salt "nitrate")
(bc/bytes->b64-str true)))]
(if (or (str/empty? key)
(str/blank? key))
(do
(l/wrn :hint "nitrate key is disabled because empty string found")
nil)
(do
(l/inf :hint "nitrate key initialized" :key (d/obfuscate-string key))
key)))})))

View File

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

View File

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

View File

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

View File

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@
;; Copyright (c) KALEIDOS INC
(ns app.common.schema
(:refer-clojure :exclude [deref merge parse-uuid parse-long parse-double parse-boolean type keys select-keys])
(:refer-clojure :exclude [deref merge parse-uuid parse-long parse-double parse-boolean type keys])
#?(:cljs (:require-macros [app.common.schema :refer [ignoring]]))
(:require
#?(:clj [malli.dev.pretty :as mdp])
@@ -93,11 +93,6 @@
[& items]
(apply mu/merge (map schema items)))
(defn select-keys
[s keys & {:as opts}]
(let [s (schema s)]
(mu/select-keys s keys opts)))
(defn assoc-key
"Add a key & value to a schema of type [:map]. If the first level node of the schema
is not a map, will do a depth search to find the first map node and add the key there."
@@ -143,10 +138,10 @@
(mu/optional-keys schema keys default-options)))
(defn required-keys
([s]
(mu/required-keys (schema s) nil default-options))
([s keys]
(mu/required-keys (schema s) keys default-options)))
([schema]
(mu/required-keys schema nil default-options))
([schema keys]
(mu/required-keys schema keys default-options)))
(defn transformer
[& transformers]
@@ -651,7 +646,7 @@
{:title "set"
:description "Set of Strings"
:error/message "should be a set of strings"
:gen/gen (sg/mcat (fn [_] (sg/generator kind)) sg/int)
:gen/gen (-> kind sg/generator sg/set)
:decode/string decode
:decode/json decode
:encode/string encode-string

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"],
});

View File

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

View File

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

View File

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

View File

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

View File

@@ -288,7 +288,6 @@ export async function compileTranslations() {
"es",
"fa",
"fr",
"fr_CA",
"he",
"sr",
"nb_NO",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -498,3 +498,4 @@
(->> (rp/cmd! :delete-access-token params)
(rx/tap on-success)
(rx/catch on-error))))))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -86,6 +86,24 @@
:else
(enabled-by-flags? state feature))))
(defn active-features?
"Given a state and a set of features, check if the features are all enabled."
([state a]
(js/console.warn "Please, use active-feature? instead")
(active-feature? state a))
([state a b]
(and ^boolean (active-feature? state a)
^boolean (active-feature? state b)))
([state a b c]
(and ^boolean (active-feature? state a)
^boolean (active-feature? state b)
^boolean (active-feature? state c)))
([state a b c & others]
(and ^boolean (active-feature? state a)
^boolean (active-feature? state b)
^boolean (active-feature? state c)
^boolean (every? #(active-feature? state %) others))))
(def ^:private features-ref
(l/derived (l/key :features) st/state))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -120,7 +120,3 @@
color: var(--color-foreground-secondary);
min-inline-size: var(--sp-l);
}
.tooltip-wrapper {
inline-size: 100%;
}

View File

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

View File

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

View File

@@ -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!"]]]]]]))

View File

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

View File

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

View File

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

View 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)}])]])]))

View 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;
}

View File

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

View File

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

View File

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

View File

@@ -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 wont 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")}])]]]))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More