mirror of
https://github.com/penpot/penpot.git
synced 2026-01-27 15:51:32 -05:00
Compare commits
5 Commits
eva-extrac
...
luis-13112
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
596cba5a0f | ||
|
|
2523096fdd | ||
|
|
8e63c4e3e8 | ||
|
|
d5abc52dac | ||
|
|
3b96eb5476 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -21,6 +21,7 @@
|
||||
.rebel_readline_history
|
||||
.repl
|
||||
.shadow-cljs
|
||||
.pnpm-store/
|
||||
/*.jpg
|
||||
/*.md
|
||||
/*.png
|
||||
@@ -44,6 +45,7 @@
|
||||
/backend/resources/public/media
|
||||
/backend/target/
|
||||
/backend/experiments
|
||||
/backend/scripts/_env.local
|
||||
/bundle*
|
||||
/cd.md
|
||||
/clj-profiler/
|
||||
@@ -74,6 +76,7 @@
|
||||
/library/target/
|
||||
/library/*.zip
|
||||
/external
|
||||
/penpot-nitrate
|
||||
|
||||
clj-profiler/
|
||||
node_modules
|
||||
|
||||
@@ -13,6 +13,7 @@ export PENPOT_FLAGS="\
|
||||
disable-login-with-google \
|
||||
disable-login-with-github \
|
||||
disable-login-with-gitlab \
|
||||
disable-telemetry \
|
||||
enable-backend-worker \
|
||||
enable-backend-asserts \
|
||||
disable-feature-fdata-pointer-map \
|
||||
@@ -55,6 +56,8 @@ export PENPOT_OBJECTS_STORAGE_BACKEND=s3
|
||||
export PENPOT_OBJECTS_STORAGE_S3_ENDPOINT=http://minio:9000
|
||||
export PENPOT_OBJECTS_STORAGE_S3_BUCKET=penpot
|
||||
|
||||
export PENPOT_NITRATE_BACKEND_URI=http://localhost:3000/control-center
|
||||
|
||||
export JAVA_OPTS="\
|
||||
-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
|
||||
-Djdk.attach.allowAttachSelf \
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
SCRIPT_DIR=$(dirname $0);
|
||||
source $SCRIPT_DIR/_env;
|
||||
|
||||
if [ -f $SCRIPT_DIR/_env.local ]; then
|
||||
source $SCRIPT_DIR/_env.local;
|
||||
fi
|
||||
|
||||
# Initialize MINIO config
|
||||
setup_minio;
|
||||
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
SCRIPT_DIR=$(dirname $0);
|
||||
|
||||
source $SCRIPT_DIR/_env;
|
||||
|
||||
if [ -f $SCRIPT_DIR/_env.local ]; then
|
||||
source $SCRIPT_DIR/_env.local;
|
||||
fi
|
||||
|
||||
export OPTIONS="-A:dev"
|
||||
|
||||
entrypoint=${1:-app.main};
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
SCRIPT_DIR=$(dirname $0);
|
||||
source $SCRIPT_DIR/_env;
|
||||
|
||||
if [ -f $SCRIPT_DIR/_env.local ]; then
|
||||
source $SCRIPT_DIR/_env.local;
|
||||
fi
|
||||
|
||||
# Initialize MINIO config
|
||||
setup_minio;
|
||||
|
||||
|
||||
@@ -225,6 +225,8 @@
|
||||
[:netty-io-threads {:optional true} ::sm/int]
|
||||
[:executor-threads {:optional true} ::sm/int]
|
||||
|
||||
[:nitrate-backend-uri {:optional true} ::sm/uri]
|
||||
|
||||
;; DEPRECATED
|
||||
[:assets-storage-backend {:optional true} :keyword]
|
||||
[:storage-assets-fs-directory {:optional true} :string]
|
||||
|
||||
@@ -323,6 +323,7 @@
|
||||
{::http.client/client (ig/ref ::http.client/client)
|
||||
::db/pool (ig/ref ::db/pool)
|
||||
::rds/pool (ig/ref ::rds/pool)
|
||||
:app.nitrate/client (ig/ref :app.nitrate/client)
|
||||
::wrk/executor (ig/ref ::wrk/netty-executor)
|
||||
::session/manager (ig/ref ::session/manager)
|
||||
::ldap/provider (ig/ref ::ldap/provider)
|
||||
@@ -339,6 +340,9 @@
|
||||
::email/blacklist (ig/ref ::email/blacklist)
|
||||
::email/whitelist (ig/ref ::email/whitelist)}
|
||||
|
||||
:app.nitrate/client
|
||||
{::http.client/client (ig/ref ::http.client/client)}
|
||||
|
||||
:app.rpc/management-methods
|
||||
{::http.client/client (ig/ref ::http.client/client)
|
||||
::db/pool (ig/ref ::db/pool)
|
||||
@@ -348,6 +352,7 @@
|
||||
::sto/storage (ig/ref ::sto/storage)
|
||||
::mtx/metrics (ig/ref ::mtx/metrics)
|
||||
::mbus/msgbus (ig/ref ::mbus/msgbus)
|
||||
:app.nitrate/client (ig/ref :app.nitrate/client)
|
||||
::rds/client (ig/ref ::rds/client)
|
||||
::setup/props (ig/ref ::setup/props)}
|
||||
|
||||
|
||||
@@ -456,7 +456,10 @@
|
||||
:fn (mg/resource "app/migrations/sql/0142-add-sso-provider-table.sql")}
|
||||
|
||||
{:name "0143-http-session-v2-table"
|
||||
:fn (mg/resource "app/migrations/sql/0143-add-http-session-v2-table.sql")}])
|
||||
:fn (mg/resource "app/migrations/sql/0143-add-http-session-v2-table.sql")}
|
||||
|
||||
{:name "0144-mod-access-token-table"
|
||||
:fn (mg/resource "app/migrations/sql/0144-mod-access-token-table.sql")}])
|
||||
|
||||
(defn apply-migrations!
|
||||
[pool name migrations]
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE access_token
|
||||
ADD COLUMN type text NULL;
|
||||
147
backend/src/app/nitrate.clj
Normal file
147
backend/src/app/nitrate.clj
Normal file
@@ -0,0 +1,147 @@
|
||||
;; 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.nitrate
|
||||
"Module that make calls to the external nitrate aplication"
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.config :as cf]
|
||||
[app.http.client :as http]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.setup :as-alias setup]
|
||||
[app.util.json :as json]
|
||||
[clojure.core :as c]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; HELPERS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- request-builder
|
||||
[cfg method uri management-key profile-id]
|
||||
(fn []
|
||||
(http/req! cfg {:method method
|
||||
:headers {"content-type" "application/json"
|
||||
"accept" "application/json"
|
||||
"x-shared-key" management-key
|
||||
"x-profile-id" (str profile-id)}
|
||||
:uri uri
|
||||
:version :http1.1})))
|
||||
|
||||
|
||||
(defn- with-retries
|
||||
[handler max-retries]
|
||||
(fn []
|
||||
(loop [attempt 1]
|
||||
(let [result (try
|
||||
(handler)
|
||||
(catch Exception e
|
||||
(if (< attempt max-retries)
|
||||
::retry
|
||||
(do
|
||||
;; TODO Error handling
|
||||
(l/error :hint "request fail after multiple retries" :cause e)
|
||||
nil))))]
|
||||
(if (= result ::retry)
|
||||
(recur (inc attempt))
|
||||
result)))))
|
||||
|
||||
|
||||
(defn- with-validate [handler uri schema]
|
||||
(fn []
|
||||
(let [coercer-http (sm/coercer schema
|
||||
:type :validation
|
||||
:hint (str "invalid data received calling " uri))]
|
||||
(try
|
||||
(coercer-http (-> (handler) :body json/decode))
|
||||
(catch Exception e
|
||||
;; TODO Error handling
|
||||
(l/error :hint "error validating json response" :cause e)
|
||||
nil)))))
|
||||
|
||||
(defn- request-to-nitrate
|
||||
[{:keys [::management-key] :as cfg} method uri schema {:keys [::rpc/profile-id] :as params}]
|
||||
(let [full-http-call (-> (request-builder cfg method uri management-key profile-id)
|
||||
(with-retries 3)
|
||||
(with-validate uri schema))]
|
||||
(full-http-call)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; API
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn call
|
||||
[cfg method params]
|
||||
(when (contains? cf/flags :nitrate)
|
||||
(let [client (get cfg ::client)
|
||||
method (get client method)]
|
||||
(method params))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private schema:organization
|
||||
[:map
|
||||
[:id ::sm/text]
|
||||
[:name ::sm/text]])
|
||||
|
||||
(def ^:private schema:user
|
||||
[:map
|
||||
[:valid ::sm/boolean]])
|
||||
|
||||
(defn- get-team-org
|
||||
[cfg {:keys [team-id] :as params}]
|
||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
||||
(request-to-nitrate cfg :get (str baseuri "/api/teams/" (str team-id)) schema:organization params)))
|
||||
|
||||
(defn- is-valid-user
|
||||
[cfg {:keys [profile-id] :as params}]
|
||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
||||
(request-to-nitrate cfg :get (str baseuri "/api/users/" (str profile-id)) schema:user params)))
|
||||
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; INITIALIZATION
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defmethod ig/init-key ::client
|
||||
[_ {:keys [::setup/props] :as cfg}]
|
||||
(if (contains? cf/flags :nitrate)
|
||||
(let [management-key (or (cf/get :management-api-key)
|
||||
(get props :management-key))
|
||||
cfg (assoc cfg ::management-key management-key)]
|
||||
{:get-team-org (partial get-team-org cfg)
|
||||
:is-valid-user (partial is-valid-user cfg)})
|
||||
{}))
|
||||
|
||||
(defmethod ig/halt-key! ::client
|
||||
[_ {:keys []}]
|
||||
(do :stuff))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; UTILS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
|
||||
(defn add-nitrate-licence-to-profile
|
||||
[cfg profile]
|
||||
(try
|
||||
(let [nitrate-licence (call cfg :is-valid-user {:profile-id (:id profile)})]
|
||||
(assoc profile :nitrate-licence (:valid nitrate-licence)))
|
||||
(catch Throwable cause
|
||||
(l/error :hint "failed to get nitrate licence"
|
||||
:profile-id (:id profile)
|
||||
:cause cause)
|
||||
profile)))
|
||||
|
||||
(defn add-org-to-team
|
||||
[cfg team params]
|
||||
(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))))
|
||||
@@ -301,6 +301,7 @@
|
||||
(let [cfg (assoc cfg ::type "management" ::metrics-id :rpc-management-timing)]
|
||||
(->> (sv/scan-ns
|
||||
'app.rpc.management.subscription
|
||||
'app.rpc.management.nitrate
|
||||
'app.rpc.management.exporter)
|
||||
(map (partial process-method cfg "management" wrap-management))
|
||||
(into {}))))
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
(dissoc row :perms))
|
||||
|
||||
(defn create-access-token
|
||||
[{:keys [::db/conn] :as cfg} profile-id name expiration]
|
||||
[{:keys [::db/conn] :as cfg} profile-id name expiration type]
|
||||
(let [token-id (uuid/next)
|
||||
expires-at (some-> expiration (ct/in-future))
|
||||
created-at (ct/now)
|
||||
@@ -36,6 +36,7 @@
|
||||
{:id token-id
|
||||
:name name
|
||||
:token token
|
||||
:type type
|
||||
:profile-id profile-id
|
||||
:created-at created-at
|
||||
:updated-at created-at
|
||||
@@ -50,17 +51,18 @@
|
||||
(def ^:private schema:create-access-token
|
||||
[:map {:title "create-access-token"}
|
||||
[:name [:string {:max 250 :min 1}]]
|
||||
[:expiration {:optional true} ::ct/duration]])
|
||||
[:expiration {:optional true} ::ct/duration]
|
||||
[:type {:optional true} :string]])
|
||||
|
||||
(sv/defmethod ::create-access-token
|
||||
{::doc/added "1.18"
|
||||
::sm/params schema:create-access-token}
|
||||
[cfg {:keys [::rpc/profile-id name expiration]}]
|
||||
[cfg {:keys [::rpc/profile-id name expiration type]}]
|
||||
|
||||
(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))
|
||||
(db/tx-run! cfg create-access-token profile-id name expiration type))
|
||||
|
||||
(def ^:private schema:delete-access-token
|
||||
[:map {:title "delete-access-token"}
|
||||
@@ -83,5 +85,5 @@
|
||||
(->> (db/query pool :access-token
|
||||
{:profile-id profile-id}
|
||||
{:order-by [[:expires-at :asc] [:created-at :asc]]
|
||||
:columns [:id :name :perms :created-at :updated-at :expires-at]})
|
||||
:columns [:id :name :perms :type :created-at :updated-at :expires-at]})
|
||||
(mapv decode-row)))
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
[app.loggers.audit :as audit]
|
||||
[app.main :as-alias main]
|
||||
[app.media :as media]
|
||||
[app.nitrate :as nitrate]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.climit :as climit]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
@@ -47,6 +48,7 @@
|
||||
(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]
|
||||
@@ -88,6 +90,8 @@
|
||||
|
||||
;; --- QUERY: Get profile (own)
|
||||
|
||||
|
||||
|
||||
(sv/defmethod ::get-profile
|
||||
{::rpc/auth false
|
||||
::doc/added "1.18"
|
||||
@@ -98,9 +102,13 @@
|
||||
;; no profile-id is in session, and when db call raises not found. In all other
|
||||
;; cases we need to reraise the exception.
|
||||
(try
|
||||
(-> (get-profile pool profile-id)
|
||||
(strip-private-attrs)
|
||||
(update :props filter-props))
|
||||
(let [profile (-> (get-profile pool profile-id)
|
||||
(strip-private-attrs)
|
||||
(update :props filter-props))]
|
||||
(if (contains? cf/flags :nitrate)
|
||||
(nitrate/add-nitrate-licence-to-profile cfg profile)
|
||||
profile))
|
||||
|
||||
(catch Throwable _
|
||||
{:id uuid/zero :fullname "Anonymous User"})))
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
[app.main :as-alias main]
|
||||
[app.media :as media]
|
||||
[app.msgbus :as mbus]
|
||||
[app.nitrate :as nitrate]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.commands.profile :as profile]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
@@ -190,7 +191,9 @@
|
||||
::sm/params schema:get-teams}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(get-teams conn profile-id)))
|
||||
(cond->> (get-teams conn profile-id)
|
||||
(contains? cf/flags :nitrate)
|
||||
(map #(nitrate/add-org-to-team cfg % params)))))
|
||||
|
||||
(def ^:private sql:get-owned-teams
|
||||
"SELECT t.id, t.name,
|
||||
|
||||
112
backend/src/app/rpc/management/nitrate.clj
Normal file
112
backend/src/app/rpc/management/nitrate.clj
Normal file
@@ -0,0 +1,112 @@
|
||||
;; 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.rpc.management.nitrate
|
||||
"Internal Nitrate HTTP API.
|
||||
Provides authenticated access to organization management and token validation endpoints.
|
||||
All requests must include a valid shared key token in the `x-shared-key` header, and
|
||||
a cookie `auth-token` with the user token.
|
||||
They will return `401 Unauthorized` if the shared key or user token are invalid."
|
||||
(:require
|
||||
[app.common.schema :as sm]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.msgbus :as mbus]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.commands.files :as files]
|
||||
[app.rpc.commands.profile :as profile]
|
||||
[app.rpc.doc :as doc]
|
||||
[app.util.services :as sv]))
|
||||
|
||||
;; ---- API: authenticate
|
||||
(def ^:private schema:profile
|
||||
[:map
|
||||
[:id ::sm/uuid]
|
||||
[:name :string]
|
||||
[:email :string]
|
||||
[:photo-url :string]])
|
||||
|
||||
(sv/defmethod ::authenticate
|
||||
"Authenticate an user
|
||||
@api GET /authenticate
|
||||
@returns
|
||||
200 OK: Returns the authenticated user."
|
||||
{::doc/added "2.12"
|
||||
::sm/result schema:profile}
|
||||
[cfg {:keys [::rpc/profile-id] :as params}]
|
||||
(let [profile (profile/get-profile cfg profile-id)]
|
||||
{:id (get profile :id)
|
||||
:name (get profile :fullname)
|
||||
:email (get profile :email)
|
||||
:photo-url (files/resolve-public-uri (get profile :photo-id))}))
|
||||
|
||||
;; ---- API: get-teams
|
||||
|
||||
(def ^:private sql:get-teams
|
||||
"SELECT t.*
|
||||
FROM team AS t
|
||||
JOIN team_profile_rel AS tpr ON t.id = tpr.team_id
|
||||
WHERE tpr.profile_id = ?
|
||||
AND tpr.is_owner = 't'
|
||||
AND t.is_default = 'f'
|
||||
AND t.deleted_at is null;")
|
||||
|
||||
(def ^:private schema:team
|
||||
[:map
|
||||
[:id ::sm/uuid]
|
||||
[:name :string]])
|
||||
|
||||
(def ^:private schema:get-teams-result
|
||||
[:vector schema:team])
|
||||
|
||||
(sv/defmethod ::get-teams
|
||||
"List teams for which current user is owner.
|
||||
@api GET /get-teams
|
||||
@returns
|
||||
200 OK: Returns the list of teams for the user."
|
||||
{::doc/added "2.12"
|
||||
::sm/result schema:get-teams-result}
|
||||
[cfg {:keys [::rpc/profile-id]}]
|
||||
(when (contains? cf/flags :nitrate)
|
||||
(let [current-user-id (-> (profile/get-profile cfg profile-id) :id)]
|
||||
(->> (db/exec! cfg [sql:get-teams current-user-id])
|
||||
(map #(select-keys % [:id :name]))))))
|
||||
|
||||
;; ---- API: notify-team-change
|
||||
|
||||
(def ^:private schema:notify-team-change
|
||||
[:map
|
||||
[:id ::sm/uuid]
|
||||
[:organization-id ::sm/text]])
|
||||
|
||||
|
||||
(sv/defmethod ::notify-team-change
|
||||
"Notify to Penpot a team change from nitrate
|
||||
@api POST /notify-team-change
|
||||
@returns
|
||||
200 OK"
|
||||
{::doc/added "2.12"
|
||||
::sm/params schema:notify-team-change
|
||||
::rpc/auth false}
|
||||
[cfg {:keys [id organization-id organization-name]}]
|
||||
(when (contains? cf/flags :nitrate)
|
||||
(let [msgbus (::mbus/msgbus cfg)]
|
||||
(mbus/pub! msgbus
|
||||
;;TODO There is a bug on dashboard with teams notifications.
|
||||
;;For now we send it to uuid/zero instead of team-id
|
||||
:topic uuid/zero
|
||||
:message {:type :team-org-change
|
||||
:team-id id
|
||||
:organization-id organization-id
|
||||
:organization-name organization-name}))))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -148,7 +148,10 @@
|
||||
|
||||
;; A temporal flag, enables backend code use more extensivelly
|
||||
;; redis for caching data
|
||||
:redis-cache})
|
||||
:redis-cache
|
||||
|
||||
;; Activates the nitrate module
|
||||
:nitrate})
|
||||
|
||||
(def all-flags
|
||||
(set/union email login varia))
|
||||
|
||||
@@ -474,8 +474,6 @@
|
||||
:height #{:sizing :dimensions}
|
||||
:max-width #{:sizing :dimensions}
|
||||
:max-height #{:sizing :dimensions}
|
||||
:min-width #{:sizing :dimensions}
|
||||
:min-height #{:sizing :dimensions}
|
||||
:x #{:dimensions}
|
||||
:y #{:dimensions}
|
||||
:rotation #{:number :rotation}
|
||||
|
||||
@@ -141,8 +141,14 @@ http {
|
||||
proxy_pass http://127.0.0.1:5000;
|
||||
}
|
||||
|
||||
location /nitrate/ {
|
||||
proxy_pass http://127.0.0.1:3000/;
|
||||
location /control-center {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /wasm-playground {
|
||||
|
||||
@@ -29,8 +29,9 @@ update_flags /var/www/app/js/config.js
|
||||
|
||||
export PENPOT_BACKEND_URI=${PENPOT_BACKEND_URI:-http://penpot-backend:6060}
|
||||
export PENPOT_EXPORTER_URI=${PENPOT_EXPORTER_URI:-http://penpot-exporter:6061}
|
||||
export PENPOT_NITRATE_URI=${PENPOT_NITRATE_URI:-http://penpot-nitrate:3000}
|
||||
export PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE=${PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE:-367001600} # Default to 350MiB
|
||||
envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE" \
|
||||
envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_NITRATE_URI,\$PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE" \
|
||||
< /tmp/nginx.conf.template > /etc/nginx/nginx.conf
|
||||
|
||||
PENPOT_DEFAULT_INTERNAL_RESOLVER="$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf)"
|
||||
|
||||
@@ -139,6 +139,14 @@ http {
|
||||
proxy_pass $PENPOT_BACKEND_URI/ws/notifications;
|
||||
}
|
||||
|
||||
location /control-center {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $http_cf_connecting_ip;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_pass $PENPOT_NITRATE_URI$request_uri;
|
||||
}
|
||||
|
||||
include /etc/nginx/overrides/server.d/*.conf;
|
||||
|
||||
location / {
|
||||
|
||||
@@ -759,87 +759,4 @@ test.describe("Tokens: Apply token", () => {
|
||||
});
|
||||
await expect(StrokeWidthPillSmall).toBeVisible();
|
||||
});
|
||||
|
||||
test("User applies margin token to a shape", async ({ page }) => {
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true,
|
||||
});
|
||||
// Set up
|
||||
await workspace.mockConfigFlags(["enable-feature-token-input"]);
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("workspace/get-file-layout-stroke-token-json");
|
||||
await workspace.goToWorkspace();
|
||||
|
||||
// Select shape apply stroke
|
||||
await workspace.layers
|
||||
.getByTestId("layer-row")
|
||||
.nth(1)
|
||||
.getByRole("button", { name: "Toggle layer" })
|
||||
.click();
|
||||
|
||||
await workspace.layers.getByTestId("layer-row").nth(2).click();
|
||||
|
||||
const rightSidebar = page.getByTestId("right-sidebar");
|
||||
await expect(rightSidebar).toBeVisible();
|
||||
await rightSidebar.getByTestId("add-stroke").click();
|
||||
|
||||
// Apply margin token from token panel
|
||||
const tokensTab = page.getByRole("tab", { name: "Tokens" });
|
||||
await expect(tokensTab).toBeVisible();
|
||||
await tokensTab.click();
|
||||
await page.getByRole("button", { name: "Dimensions 4" }).click();
|
||||
await page.getByRole("button", { name: "dim", exact: true }).click();
|
||||
const tokensSidebar = workspace.tokensSidebar;
|
||||
await expect(
|
||||
tokensSidebar.getByRole("button", { name: "dim.md" }),
|
||||
).toBeVisible();
|
||||
await tokensSidebar
|
||||
.getByRole("button", { name: "dim.md" })
|
||||
.click({ button: "right" });
|
||||
await page
|
||||
.getByTestId("tokens-context-menu-for-token")
|
||||
.getByText("Spacing")
|
||||
.hover();
|
||||
await page
|
||||
.getByTestId("tokens-context-menu-for-token")
|
||||
.getByText("Horizontal")
|
||||
.click();
|
||||
|
||||
// Check if token pill is visible on right sidebar
|
||||
const layoutItemSectionSidebar = rightSidebar.getByRole("region", {
|
||||
name: "layout item menu",
|
||||
});
|
||||
await expect(layoutItemSectionSidebar).toBeVisible();
|
||||
const marginPillMd = layoutItemSectionSidebar.getByRole("button", {
|
||||
name: "dim.md",
|
||||
});
|
||||
await expect(marginPillMd).toBeVisible();
|
||||
|
||||
await marginPillMd.click();
|
||||
const dimensionTokenOptionXl = page.getByRole("option", { name: "dim.xl" });
|
||||
await expect(dimensionTokenOptionXl).toBeVisible();
|
||||
await dimensionTokenOptionXl.click();
|
||||
|
||||
const marginPillXL = layoutItemSectionSidebar.getByRole("button", {
|
||||
name: "dim.xl",
|
||||
});
|
||||
await expect(marginPillXL).toBeVisible();
|
||||
|
||||
// Detach token from right sidebar and apply another from dropdown
|
||||
const detachButton = layoutItemSectionSidebar.getByRole("button", {
|
||||
name: "Detach token",
|
||||
});
|
||||
await detachButton.click();
|
||||
await expect(marginPillXL).not.toBeVisible();
|
||||
const horizontalMarginInput = layoutItemSectionSidebar.getByText('Horizontal marginOpen token');
|
||||
await expect(horizontalMarginInput).toBeVisible();
|
||||
|
||||
const tokenDropdown = horizontalMarginInput.getByRole('button', { name: 'Open token list' });
|
||||
await tokenDropdown.click();
|
||||
|
||||
await expect(dimensionTokenOptionXl).toBeVisible();
|
||||
await dimensionTokenOptionXl.click();
|
||||
|
||||
await expect(marginPillXL).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,88 +12,118 @@ test.beforeEach(async ({ page }) => {
|
||||
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-tokens.json");
|
||||
});
|
||||
|
||||
test.describe("Tokens: Remapping Feature", () => {
|
||||
const createToken = async (page, type, name, textFieldName, value) => {
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
|
||||
const { tokensUpdateCreateModal } = await setupTokensFile(page, {
|
||||
flags: ["enable-token-shadow"],
|
||||
});
|
||||
|
||||
// Create base token
|
||||
await tokensTabPanel
|
||||
.getByRole("button", { name: `Add Token: ${type}` })
|
||||
.click();
|
||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||
|
||||
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||
await nameField.fill(name);
|
||||
|
||||
const colorField = tokensUpdateCreateModal.getByRole("textbox", {
|
||||
name: textFieldName,
|
||||
});
|
||||
await colorField.fill(value);
|
||||
|
||||
const submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||
name: "Save",
|
||||
});
|
||||
await submitButton.click();
|
||||
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
||||
};
|
||||
|
||||
const renameToken = async (page, oldName, newName) => {
|
||||
const { tokensUpdateCreateModal, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFile(page, { flags: ["enable-token-shadow"] });
|
||||
|
||||
const baseToken = tokensSidebar.getByRole("button", {
|
||||
name: oldName,
|
||||
});
|
||||
await baseToken.click({ button: "right" });
|
||||
await tokenContextMenuForToken.getByText("Edit token").click();
|
||||
|
||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||
|
||||
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||
await nameField.fill(newName);
|
||||
|
||||
const submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||
name: "Save",
|
||||
});
|
||||
await submitButton.click();
|
||||
};
|
||||
|
||||
const createCompositeDerivedToken = async (page, type, name, reference) => {
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
|
||||
const { tokensUpdateCreateModal } = await setupTokensFile(page, {
|
||||
flags: ["enable-token-shadow"],
|
||||
});
|
||||
|
||||
await tokensTabPanel
|
||||
.getByRole("button", { name: `Add Token: ${type}` })
|
||||
.click();
|
||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||
|
||||
const nameField = tokensUpdateCreateModal.getByRole("textbox", {
|
||||
name: "Name",
|
||||
});
|
||||
await nameField.fill(name);
|
||||
|
||||
const referenceToggle = tokensUpdateCreateModal.getByTestId("reference-opt");
|
||||
await referenceToggle.click();
|
||||
|
||||
const referenceField = tokensUpdateCreateModal.getByRole("textbox", {
|
||||
name: "Reference",
|
||||
});
|
||||
await referenceField.fill(reference);
|
||||
|
||||
const submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||
name: "Save",
|
||||
});
|
||||
await submitButton.click();
|
||||
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
||||
};
|
||||
|
||||
test.describe("Remapping Tokens", () => {
|
||||
test.describe("Box Shadow Token Remapping", () => {
|
||||
test("User renames box shadow token with alias references", async ({
|
||||
page,
|
||||
}) => {
|
||||
const {
|
||||
tokensUpdateCreateModal,
|
||||
tokensSidebar,
|
||||
tokenContextMenuForToken,
|
||||
} = await setupTokensFile(page, { flags: ["enable-token-shadow"] });
|
||||
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
const { tokensSidebar } = await setupTokensFile(page, {
|
||||
flags: ["enable-token-shadow"],
|
||||
});
|
||||
|
||||
// Create base shadow token
|
||||
await tokensTabPanel
|
||||
.getByRole("button", { name: "Add Token: Shadow" })
|
||||
.click();
|
||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||
|
||||
let nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||
await nameField.fill("base-shadow");
|
||||
|
||||
const colorField = tokensUpdateCreateModal.getByRole("textbox", {
|
||||
name: "Color",
|
||||
});
|
||||
await colorField.fill("#000000");
|
||||
|
||||
let submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||
name: "Save",
|
||||
});
|
||||
await submitButton.click();
|
||||
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
||||
await createToken(page, "Shadow", "base-shadow", "Color", "#000000");
|
||||
|
||||
// Create derived shadow token that references base-shadow
|
||||
await tokensTabPanel
|
||||
.getByRole("button", { name: "Add Token: Shadow" })
|
||||
.click();
|
||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||
|
||||
nameField = tokensUpdateCreateModal.getByRole("textbox", {
|
||||
name: "Name",
|
||||
});
|
||||
await nameField.fill("derived-shadow");
|
||||
|
||||
const referenceToggle =
|
||||
tokensUpdateCreateModal.getByTestId("reference-opt");
|
||||
await referenceToggle.click();
|
||||
|
||||
const referenceField = tokensUpdateCreateModal.getByRole("textbox", {
|
||||
name: "Reference",
|
||||
});
|
||||
await referenceField.fill("{base-shadow}");
|
||||
|
||||
submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||
name: "Save",
|
||||
});
|
||||
await submitButton.click();
|
||||
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
||||
await createCompositeDerivedToken(
|
||||
page,
|
||||
"Shadow",
|
||||
"derived-shadow",
|
||||
"{base-shadow}",
|
||||
);
|
||||
|
||||
// Rename base-shadow token
|
||||
const baseToken = tokensSidebar.getByRole("button", {
|
||||
name: "base-shadow",
|
||||
});
|
||||
await baseToken.click({ button: "right" });
|
||||
await tokenContextMenuForToken.getByText("Edit token").click();
|
||||
|
||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||
nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||
await nameField.fill("foundation-shadow");
|
||||
|
||||
submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||
name: "Save",
|
||||
});
|
||||
await submitButton.click();
|
||||
await renameToken(page, "base-shadow", "foundation-shadow");
|
||||
|
||||
// Check for remapping modal
|
||||
const remappingModal = page.getByTestId("token-remapping-modal");
|
||||
await expect(remappingModal).toBeVisible({ timeout: 5000 });
|
||||
await expect(remappingModal).toContainText("1");
|
||||
await expect(remappingModal).toContainText("base-shadow");
|
||||
await expect(remappingModal).toContainText("foundation-shadow");
|
||||
|
||||
const confirmButton = remappingModal.getByRole("button", {
|
||||
name: /remap/i,
|
||||
name: "remap tokens",
|
||||
});
|
||||
await confirmButton.click();
|
||||
|
||||
@@ -116,51 +146,16 @@ test.describe("Tokens: Remapping Feature", () => {
|
||||
workspacePage,
|
||||
} = await setupTokensFile(page, { flags: ["enable-token-shadow"] });
|
||||
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
|
||||
// Create base shadow token
|
||||
await tokensTabPanel
|
||||
.getByRole("button", { name: "Add Token: Shadow" })
|
||||
.click();
|
||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||
|
||||
let nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||
await nameField.fill("primary-shadow");
|
||||
|
||||
let colorField = tokensUpdateCreateModal.getByRole("textbox", {
|
||||
name: "Color",
|
||||
});
|
||||
await colorField.fill("#000000");
|
||||
|
||||
let submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||
name: "Save",
|
||||
});
|
||||
await submitButton.click();
|
||||
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
||||
await createToken(page, "Shadow", "primary-shadow", "Color", "#000000");
|
||||
|
||||
// Create derived shadow token that references base
|
||||
await tokensTabPanel
|
||||
.getByRole("button", { name: "Add Token: Shadow" })
|
||||
.click();
|
||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||
|
||||
nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||
await nameField.fill("card-shadow");
|
||||
|
||||
const referenceToggle =
|
||||
tokensUpdateCreateModal.getByTestId("reference-opt");
|
||||
await referenceToggle.click();
|
||||
|
||||
const referenceField = tokensUpdateCreateModal.getByRole("textbox", {
|
||||
name: "Reference",
|
||||
});
|
||||
await referenceField.fill("{primary-shadow}");
|
||||
|
||||
submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||
name: "Save",
|
||||
});
|
||||
await submitButton.click();
|
||||
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
||||
await createCompositeDerivedToken(
|
||||
page,
|
||||
"Shadow",
|
||||
"card-shadow",
|
||||
"{primary-shadow}",
|
||||
);
|
||||
|
||||
// Apply the referenced token to a shape
|
||||
await page.getByRole("tab", { name: "Layers" }).click();
|
||||
@@ -183,16 +178,16 @@ test.describe("Tokens: Remapping Feature", () => {
|
||||
await tokenContextMenuForToken.getByText("Edit token").click();
|
||||
|
||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||
nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||
await nameField.fill("main-shadow");
|
||||
|
||||
// Update the color value
|
||||
colorField = tokensUpdateCreateModal.getByRole("textbox", {
|
||||
const colorField = tokensUpdateCreateModal.getByRole("textbox", {
|
||||
name: "Color",
|
||||
});
|
||||
await colorField.fill("#FF0000");
|
||||
|
||||
submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||
const submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||
name: "Save",
|
||||
});
|
||||
await submitButton.click();
|
||||
@@ -202,7 +197,7 @@ test.describe("Tokens: Remapping Feature", () => {
|
||||
await expect(remappingModal).toBeVisible({ timeout: 5000 });
|
||||
|
||||
const confirmButton = remappingModal.getByRole("button", {
|
||||
name: /remap/i,
|
||||
name: "remap tokens",
|
||||
});
|
||||
await confirmButton.click();
|
||||
|
||||
@@ -259,73 +254,25 @@ test.describe("Tokens: Remapping Feature", () => {
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
|
||||
// Create base typography token
|
||||
await tokensTabPanel
|
||||
.getByRole("button", { name: "Add Token: Typography" })
|
||||
.click();
|
||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||
|
||||
let nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||
await nameField.fill("base-text");
|
||||
|
||||
const fontSizeField = tokensUpdateCreateModal.getByRole("textbox", {
|
||||
name: "Font size",
|
||||
});
|
||||
await fontSizeField.fill("16");
|
||||
|
||||
let submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||
name: "Save",
|
||||
});
|
||||
await submitButton.click();
|
||||
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
||||
await createToken(page, "Typography", "base-text", "Font size", "16");
|
||||
|
||||
// Create derived typography token
|
||||
await tokensTabPanel
|
||||
.getByRole("button", { name: "Add Token: Typography" })
|
||||
.click();
|
||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||
|
||||
nameField = tokensUpdateCreateModal.getByRole("textbox", {
|
||||
name: "Name",
|
||||
});
|
||||
await nameField.fill("body-text");
|
||||
|
||||
const referenceToggle =
|
||||
tokensUpdateCreateModal.getByTestId("reference-opt");
|
||||
await referenceToggle.click();
|
||||
|
||||
const referenceField = tokensUpdateCreateModal.getByRole("textbox", {
|
||||
name: "Reference",
|
||||
});
|
||||
await referenceField.fill("{base-text}");
|
||||
|
||||
submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||
name: "Save",
|
||||
});
|
||||
await submitButton.click();
|
||||
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
||||
await createCompositeDerivedToken(
|
||||
page,
|
||||
"Typography",
|
||||
"body-text",
|
||||
"{base-text}",
|
||||
);
|
||||
|
||||
// Rename base token
|
||||
const baseToken = tokensSidebar.getByRole("button", {
|
||||
name: "base-text",
|
||||
});
|
||||
await baseToken.click({ button: "right" });
|
||||
await tokenContextMenuForToken.getByText("Edit token").click();
|
||||
|
||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||
nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||
await nameField.fill("default-text");
|
||||
|
||||
submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||
name: "Save",
|
||||
});
|
||||
await submitButton.click();
|
||||
await renameToken(page, "base-text", "default-text");
|
||||
|
||||
// Check for remapping modal
|
||||
const remappingModal = page.getByTestId("token-remapping-modal");
|
||||
await expect(remappingModal).toBeVisible({ timeout: 5000 });
|
||||
|
||||
const confirmButton = remappingModal.getByRole("button", {
|
||||
name: /remap/i,
|
||||
name: "remap tokens",
|
||||
});
|
||||
await confirmButton.click();
|
||||
|
||||
@@ -351,24 +298,7 @@ test.describe("Tokens: Remapping Feature", () => {
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
|
||||
// Create base typography token
|
||||
await tokensTabPanel
|
||||
.getByRole("button", { name: "Add Token: Typography" })
|
||||
.click();
|
||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||
|
||||
let nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||
await nameField.fill("body-style");
|
||||
|
||||
let fontSizeField = tokensUpdateCreateModal.getByRole("textbox", {
|
||||
name: "Font size",
|
||||
});
|
||||
await fontSizeField.fill("16");
|
||||
|
||||
let submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||
name: "Save",
|
||||
});
|
||||
await submitButton.click();
|
||||
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
||||
await createToken(page, "Typography", "body-style", "Font size", "16");
|
||||
|
||||
// Create derived typography token
|
||||
await tokensTabPanel
|
||||
@@ -376,7 +306,7 @@ test.describe("Tokens: Remapping Feature", () => {
|
||||
.click();
|
||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||
|
||||
nameField = tokensUpdateCreateModal.getByRole("textbox", {
|
||||
let nameField = tokensUpdateCreateModal.getByRole("textbox", {
|
||||
name: "Name",
|
||||
});
|
||||
await nameField.fill("paragraph-style");
|
||||
@@ -390,7 +320,7 @@ test.describe("Tokens: Remapping Feature", () => {
|
||||
});
|
||||
await referenceField.fill("{body-style}");
|
||||
|
||||
submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||
let submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||
name: "Save",
|
||||
});
|
||||
await submitButton.click();
|
||||
@@ -421,7 +351,7 @@ test.describe("Tokens: Remapping Feature", () => {
|
||||
await nameField.fill("text-base");
|
||||
|
||||
// Update the font size value
|
||||
fontSizeField = tokensUpdateCreateModal.getByRole("textbox", {
|
||||
const fontSizeField = tokensUpdateCreateModal.getByRole("textbox", {
|
||||
name: "Font size",
|
||||
});
|
||||
await fontSizeField.fill("18");
|
||||
@@ -436,7 +366,7 @@ test.describe("Tokens: Remapping Feature", () => {
|
||||
await expect(remappingModal).toBeVisible({ timeout: 5000 });
|
||||
|
||||
const confirmButton = remappingModal.getByRole("button", {
|
||||
name: /remap/i,
|
||||
name: "remap tokens",
|
||||
});
|
||||
await confirmButton.click();
|
||||
|
||||
@@ -471,72 +401,29 @@ test.describe("Tokens: Remapping Feature", () => {
|
||||
test("User renames border radius token with alias references", async ({
|
||||
page,
|
||||
}) => {
|
||||
const {
|
||||
tokensUpdateCreateModal,
|
||||
tokensSidebar,
|
||||
tokenContextMenuForToken,
|
||||
} = await setupTokensFile(page);
|
||||
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
const { tokensSidebar } = await setupTokensFile(page);
|
||||
|
||||
// Create base border radius token
|
||||
await tokensTabPanel
|
||||
.getByRole("button", { name: "Add Token: Border Radius" })
|
||||
.click();
|
||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||
|
||||
let nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||
await nameField.fill("base-radius");
|
||||
|
||||
const valueField = tokensUpdateCreateModal.getByLabel("Value");
|
||||
await valueField.fill("4");
|
||||
|
||||
let submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||
name: "Save",
|
||||
});
|
||||
await submitButton.click();
|
||||
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
||||
await createToken(page, "Border Radius", "base-radius", "Value", "4");
|
||||
|
||||
// Create derived border radius token
|
||||
await tokensTabPanel
|
||||
.getByRole("button", { name: "Add Token: Border Radius" })
|
||||
.click();
|
||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||
|
||||
nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||
await nameField.fill("card-radius");
|
||||
|
||||
const valueField2 = tokensUpdateCreateModal.getByLabel("Value");
|
||||
await valueField2.fill("{base-radius}");
|
||||
|
||||
submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||
name: "Save",
|
||||
});
|
||||
await submitButton.click();
|
||||
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
||||
await createToken(
|
||||
page,
|
||||
"Border Radius",
|
||||
"card-radius",
|
||||
"Value",
|
||||
"{base-radius}",
|
||||
);
|
||||
|
||||
// Rename base token
|
||||
const baseToken = tokensSidebar.getByRole("button", {
|
||||
name: "base-radius",
|
||||
});
|
||||
await baseToken.click({ button: "right" });
|
||||
await tokenContextMenuForToken.getByText("Edit token").click();
|
||||
|
||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||
nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||
await nameField.fill("primary-radius");
|
||||
|
||||
submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||
name: "Save",
|
||||
});
|
||||
await submitButton.click();
|
||||
await renameToken(page, "base-radius", "primary-radius");
|
||||
|
||||
// Check for remapping modal
|
||||
const remappingModal = page.getByTestId("token-remapping-modal");
|
||||
await expect(remappingModal).toBeVisible({ timeout: 5000 });
|
||||
|
||||
const confirmButton = remappingModal.getByRole("button", {
|
||||
name: /remap/i,
|
||||
name: "remap tokens",
|
||||
});
|
||||
await confirmButton.click();
|
||||
|
||||
@@ -558,43 +445,17 @@ test.describe("Tokens: Remapping Feature", () => {
|
||||
tokenContextMenuForToken,
|
||||
} = await setupTokensFile(page);
|
||||
|
||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||
|
||||
// Create base border radius token
|
||||
await tokensTabPanel
|
||||
.getByRole("button", { name: "Add Token: Border Radius" })
|
||||
.click();
|
||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||
|
||||
let nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||
await nameField.fill("radius-sm");
|
||||
|
||||
let valueField = tokensUpdateCreateModal.getByLabel("Value");
|
||||
await valueField.fill("4");
|
||||
|
||||
let submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||
name: "Save",
|
||||
});
|
||||
await submitButton.click();
|
||||
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
||||
await createToken(page, "Border Radius", "radius-sm", "Value", "4");
|
||||
|
||||
// Create derived border radius token
|
||||
await tokensTabPanel
|
||||
.getByRole("button", { name: "Add Token: Border Radius" })
|
||||
.click();
|
||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||
|
||||
nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||
await nameField.fill("button-radius");
|
||||
|
||||
const valueField2 = tokensUpdateCreateModal.getByLabel("Value");
|
||||
await valueField2.fill("{radius-sm}");
|
||||
|
||||
submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||
name: "Save",
|
||||
});
|
||||
await submitButton.click();
|
||||
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
||||
await createToken(
|
||||
page,
|
||||
"Border Radius",
|
||||
"button-radius",
|
||||
"Value",
|
||||
"{radius-sm}",
|
||||
);
|
||||
|
||||
// Rename and update value of base token
|
||||
const radiusToken = tokensSidebar.getByRole("button", {
|
||||
@@ -604,14 +465,14 @@ test.describe("Tokens: Remapping Feature", () => {
|
||||
await tokenContextMenuForToken.getByText("Edit token").click();
|
||||
|
||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||
nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||
await nameField.fill("radius-base");
|
||||
|
||||
// Update the value
|
||||
valueField = tokensUpdateCreateModal.getByLabel("Value");
|
||||
const valueField = tokensUpdateCreateModal.getByLabel("Value");
|
||||
await valueField.fill("8");
|
||||
|
||||
submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||
const submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||
name: "Save",
|
||||
});
|
||||
await submitButton.click();
|
||||
@@ -621,7 +482,7 @@ test.describe("Tokens: Remapping Feature", () => {
|
||||
await expect(remappingModal).toBeVisible({ timeout: 5000 });
|
||||
|
||||
const confirmButton = remappingModal.getByRole("button", {
|
||||
name: /remap/i,
|
||||
name: "remap tokens",
|
||||
});
|
||||
await confirmButton.click();
|
||||
|
||||
@@ -648,4 +509,82 @@ test.describe("Tokens: Remapping Feature", () => {
|
||||
await expect(currentValue).toHaveValue("{radius-base}");
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Cancel remap", () => {
|
||||
test("Only rename - breaks reference", async ({ page }) => {
|
||||
const { tokensSidebar } = await setupTokensFile(page, {
|
||||
flags: ["enable-token-shadow"],
|
||||
});
|
||||
|
||||
// Create base shadow token
|
||||
await createToken(page, "Shadow", "base-shadow", "Color", "#000000");
|
||||
|
||||
// Create derived shadow token that references base-shadow
|
||||
await createCompositeDerivedToken(
|
||||
page,
|
||||
"Shadow",
|
||||
"derived-shadow",
|
||||
"{base-shadow}",
|
||||
);
|
||||
|
||||
// Rename base-shadow token
|
||||
await renameToken(page, "base-shadow", "foundation-shadow");
|
||||
|
||||
// Check for remapping modal
|
||||
const remappingModal = page.getByTestId("token-remapping-modal");
|
||||
await expect(remappingModal).toBeVisible({ timeout: 5000 });
|
||||
|
||||
const cancelButton = remappingModal.getByRole("button", {
|
||||
name: "don't remap",
|
||||
});
|
||||
await cancelButton.click();
|
||||
|
||||
// Verify token was renamed
|
||||
await expect(
|
||||
tokensSidebar.getByRole("button", {
|
||||
name: "foundation-shadow",
|
||||
}),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
tokensSidebar.locator('[aria-label="Missing reference"]'),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("Cancel process - no changes applied", async ({ page }) => {
|
||||
const { tokensSidebar } = await setupTokensFile(page, {
|
||||
flags: ["enable-token-shadow"],
|
||||
});
|
||||
|
||||
// Create base shadow token
|
||||
await createToken(page, "Shadow", "base-shadow", "Color", "#000000");
|
||||
|
||||
// Create derived shadow token that references base-shadow
|
||||
await createCompositeDerivedToken(
|
||||
page,
|
||||
"Shadow",
|
||||
"derived-shadow",
|
||||
"{base-shadow}",
|
||||
);
|
||||
|
||||
// Rename base-shadow token
|
||||
await renameToken(page, "base-shadow", "foundation-shadow");
|
||||
|
||||
// Check for remapping modal
|
||||
const remappingModal = page.getByTestId("token-remapping-modal");
|
||||
await expect(remappingModal).toBeVisible({ timeout: 5000 });
|
||||
|
||||
const closeButton = remappingModal.getByRole("button", {
|
||||
name: "close",
|
||||
});
|
||||
await closeButton.click();
|
||||
|
||||
// Verify original token name still exists
|
||||
await expect(
|
||||
tokensSidebar.getByRole("button", { name: "base-shadow" }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
tokensSidebar.getByRole("button", { name: "derived-shadow" }),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -216,4 +216,32 @@ test.describe("Tokens: Sets Tab", () => {
|
||||
await expect(tokenSetItems.nth(1)).toHaveAttribute("aria-checked", "false");
|
||||
await expect(tokenSetItems.nth(2)).toHaveAttribute("aria-checked", "true");
|
||||
});
|
||||
|
||||
test("Display active set and verify if is enabled", async ({ page }) => {
|
||||
const { tokenThemesSetsSidebar, tokensSidebar, tokenSetItems } =
|
||||
await setupTokensFile(page);
|
||||
|
||||
// Create set
|
||||
await tokenThemesSetsSidebar
|
||||
.getByRole("button", { name: "Add set" })
|
||||
.click();
|
||||
await changeSetInput(tokenThemesSetsSidebar, "Inactive set");
|
||||
await tokenThemesSetsSidebar
|
||||
.getByRole("button", { name: "Inactive set" })
|
||||
.click();
|
||||
let activeSetTitle = await tokensSidebar.getByTestId(
|
||||
"active-token-set-title",
|
||||
);
|
||||
await expect(activeSetTitle).toHaveText("TOKENS - Inactive set");
|
||||
const inactiveSetInfo = await tokensSidebar.getByTitle(
|
||||
"This set is not active.",
|
||||
);
|
||||
await expect(inactiveSetInfo).toBeVisible();
|
||||
|
||||
// Switch active set
|
||||
|
||||
await tokenThemesSetsSidebar.getByRole("button", { name: "theme" }).click();
|
||||
await expect(activeSetTitle).toHaveText("TOKENS - theme");
|
||||
await expect(inactiveSetInfo).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
[app.common.time :as ct]
|
||||
[app.common.types.project :refer [valid-project?]]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.main.constants :as mconst]
|
||||
[app.main.data.common :as dcm]
|
||||
[app.main.data.event :as ev]
|
||||
@@ -683,12 +684,25 @@
|
||||
(rx/of (dcm/change-team-role params)
|
||||
(modal/hide)))))
|
||||
|
||||
(defn handle-change-team-org
|
||||
[{:keys [team-id organization-id organization-name]}]
|
||||
(ptk/reify ::handle-change-team-org
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(if (contains? cf/flags :nitrate)
|
||||
(d/update-in-when state [:teams team-id] assoc
|
||||
:organization-id organization-id
|
||||
:organization-name organization-name)
|
||||
state))))
|
||||
|
||||
|
||||
(defn- process-message
|
||||
[{:keys [type] :as msg}]
|
||||
(case type
|
||||
:notification (dcm/handle-notification msg)
|
||||
:team-role-change (handle-change-team-role msg)
|
||||
:team-membership-change (dcm/team-membership-change msg)
|
||||
:team-org-change (handle-change-team-org msg)
|
||||
nil))
|
||||
|
||||
|
||||
|
||||
@@ -539,11 +539,10 @@
|
||||
value shape-ids
|
||||
#{:stroke-width}
|
||||
page-id))
|
||||
|
||||
(some attributes #{:max-width :max-height :layout-item-max-h :layout-item-max-w :layout-item-min-h :layout-item-min-w})
|
||||
(some attributes #{:max-width :max-height})
|
||||
(conj #(update-layout-sizing-limits
|
||||
value shape-ids
|
||||
(set (filter attributes #{:max-width :max-height :layout-item-max-h :layout-item-max-w :layout-item-min-h :layout-item-min-w}))
|
||||
(set (filter attributes #{:max-width :max-height}))
|
||||
page-id))))
|
||||
|
||||
(defn apply-dimensions-token
|
||||
|
||||
@@ -197,7 +197,7 @@
|
||||
:settings-options
|
||||
:settings-feedback
|
||||
:settings-subscription
|
||||
:settings-access-tokens
|
||||
:settings-integrations
|
||||
:settings-notifications)
|
||||
(let [params (get params :query)
|
||||
error-report-id (some-> params :error-report-id uuid/parse*)]
|
||||
|
||||
@@ -9,14 +9,17 @@
|
||||
(: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
|
||||
@@ -68,8 +71,11 @@
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h2 {:class (stl/css :modal-title)} title]
|
||||
[:button {:class (stl/css :modal-close-btn)
|
||||
:on-click cancel-fn} deprecated-icon/close]]
|
||||
[:> icon-button* {:variant "ghost"
|
||||
:class (stl/css :modal-close-btn)
|
||||
:aria-label (tr "labels.close")
|
||||
:on-click cancel-fn
|
||||
:icon i/close}]]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
(when (and (string? message) (not= message ""))
|
||||
@@ -87,24 +93,19 @@
|
||||
[:ul {:class (stl/css :component-list)}
|
||||
(for [item items]
|
||||
[:li {:class (stl/css :modal-item-element)}
|
||||
[:span {:class (stl/css :modal-component-icon)}
|
||||
deprecated-icon/component]
|
||||
[:> icon* {:icon-id i/component
|
||||
:class (stl/css :modal-component-icon)
|
||||
:size "s"}]
|
||||
[: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)
|
||||
[: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}]]]]]))
|
||||
[:> 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]]]]]))
|
||||
|
||||
@@ -15,10 +15,9 @@
|
||||
|
||||
.modal-container {
|
||||
@extend .modal-container-base;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
margin-bottom: deprecated.$s-24;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-xxl);
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
@@ -27,12 +26,13 @@
|
||||
}
|
||||
|
||||
.modal-close-btn {
|
||||
@extend .modal-close-btn-base;
|
||||
position: absolute;
|
||||
top: var(--sp-m);
|
||||
right: var(--sp-m);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
@include deprecated.bodyLargeTypography;
|
||||
margin-bottom: deprecated.$s-24;
|
||||
}
|
||||
|
||||
.modal-item-element {
|
||||
@@ -41,32 +41,18 @@
|
||||
|
||||
.modal-component-icon {
|
||||
@include deprecated.flexCenter;
|
||||
height: deprecated.$s-16;
|
||||
width: deprecated.$s-16;
|
||||
svg {
|
||||
@extend .button-icon-small;
|
||||
stroke: var(--color);
|
||||
}
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.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 {
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
[app.main.ui.dashboard.team-form]
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.main.ui.nitrate.nitrate-form]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.dom.dnd :as dnd]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
@@ -280,8 +281,8 @@
|
||||
|
||||
(mf/defc teams-selector-dropdown*
|
||||
{::mf/private true}
|
||||
[{:keys [team profile teams] :rest props}]
|
||||
(let [on-create-click
|
||||
[{:keys [team profile teams show-default-team allow-create-teams allow-create-org] :rest props}]
|
||||
(let [on-create-team-click
|
||||
(mf/use-fn #(st/emit! (modal/show :team-form {})))
|
||||
|
||||
on-team-click
|
||||
@@ -290,18 +291,27 @@
|
||||
(let [team-id (-> (dom/get-current-target event)
|
||||
(dom/get-data "value")
|
||||
(uuid/parse))]
|
||||
(st/emit! (dcm/go-to-dashboard-recent :team-id team-id)))))]
|
||||
(st/emit! (dcm/go-to-dashboard-recent :team-id team-id)))))
|
||||
|
||||
on-create-org-click
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(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
|
||||
|
||||
[:> dropdown-menu-item* {:on-click on-team-click
|
||||
:data-value (:default-team-id profile)
|
||||
:class (stl/css :team-dropdown-item)}
|
||||
[:span {:class (stl/css :penpot-icon)} deprecated-icon/logo-icon]
|
||||
(when show-default-team
|
||||
[:> dropdown-menu-item* {:on-click on-team-click
|
||||
:data-value (:default-team-id profile)
|
||||
:class (stl/css :team-dropdown-item)}
|
||||
[:span {:class (stl/css :penpot-icon)} deprecated-icon/logo-icon]
|
||||
|
||||
[:span {:class (stl/css :team-text)} (tr "dashboard.your-penpot")]
|
||||
(when (= (:default-team-id profile) (:id team))
|
||||
tick-icon)]
|
||||
[:span {:class (stl/css :team-text)} (tr "dashboard.your-penpot")]
|
||||
(when (= (:default-team-id profile) (:id team))
|
||||
tick-icon)])
|
||||
|
||||
(for [team-item (remove :is-default (vals teams))]
|
||||
[:> dropdown-menu-item* {:on-click on-team-click
|
||||
@@ -322,11 +332,19 @@
|
||||
(when (= (:id team-item) (:id team))
|
||||
tick-icon)])
|
||||
|
||||
[:hr {:role "separator" :class (stl/css :team-separator)}]
|
||||
[:> dropdown-menu-item* {:on-click on-create-click
|
||||
:class (stl/css :team-dropdown-item :action)}
|
||||
[:span {:class (stl/css :icon-wrapper)} add-icon]
|
||||
[:span {:class (stl/css :team-text)} (tr "dashboard.create-new-team")]]]))
|
||||
(when allow-create-teams
|
||||
[:hr {:role "separator" :class (stl/css :team-separator)}]
|
||||
[:> dropdown-menu-item* {:on-click on-create-team-click
|
||||
:class (stl/css :team-dropdown-item :action)}
|
||||
[:span {:class (stl/css :icon-wrapper)} add-icon]
|
||||
[:span {:class (stl/css :team-text)} (tr "dashboard.create-new-team")]])
|
||||
|
||||
(when allow-create-org
|
||||
[:hr {:role "separator" :class (stl/css :team-separator)}]
|
||||
[:> dropdown-menu-item* {:on-click on-create-org-click
|
||||
:class (stl/css :team-dropdown-item :action)}
|
||||
[:span {:class (stl/css :icon-wrapper)} add-icon]
|
||||
[:span {:class (stl/css :team-text)} (tr "dashboard.create-new-org")]])]))
|
||||
|
||||
(mf/defc team-options-dropdown*
|
||||
{::mf/private true}
|
||||
@@ -476,9 +494,80 @@
|
||||
:data-testid "delete-team"}
|
||||
(tr "dashboard.delete-team")])]))
|
||||
|
||||
|
||||
(mf/defc sidebar-org-switch*
|
||||
[{:keys [team profile]}]
|
||||
(let [teams (->> (mf/deref refs/teams)
|
||||
vals
|
||||
(group-by :organization-id)
|
||||
(map (fn [[_group entries]] (first entries)))
|
||||
vec
|
||||
(d/index-by :id))
|
||||
|
||||
teams (update-vals teams
|
||||
(fn [t]
|
||||
(assoc t :name (str "ORG: " (:organization-name t)))))
|
||||
|
||||
team (assoc team :name (str "ORG: " (:organization-name team)))
|
||||
|
||||
show-teams-menu*
|
||||
(mf/use-state false)
|
||||
|
||||
show-teams-menu?
|
||||
(deref show-teams-menu*)
|
||||
|
||||
on-show-teams-click
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(swap! show-teams-menu* not)))
|
||||
|
||||
on-show-teams-keydown
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(when (or (kbd/space? event)
|
||||
(kbd/enter? event))
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(some-> (dom/get-current-target event)
|
||||
(dom/click!)))))
|
||||
close-teams-menu
|
||||
(mf/use-fn #(reset! show-teams-menu* false))]
|
||||
|
||||
[: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)]]
|
||||
|
||||
arrow-icon]]
|
||||
|
||||
;; Teams Dropdown
|
||||
|
||||
[:> 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]}]
|
||||
(let [teams (mf/deref refs/teams)
|
||||
(let [nitrate? (contains? cf/flags :nitrate)
|
||||
org-id (when nitrate? (:organization-id team))
|
||||
teams (cond->> (mf/deref refs/teams)
|
||||
nitrate?
|
||||
(filter #(= (-> % val :organization-id) org-id)))
|
||||
|
||||
subscription
|
||||
(get team :subscription)
|
||||
@@ -586,7 +675,10 @@
|
||||
:class (stl/css :dropdown :teams-dropdown)
|
||||
:team team
|
||||
:profile profile
|
||||
:teams teams}]
|
||||
:teams teams
|
||||
:show-default-team true
|
||||
:allow-create-teams true
|
||||
:allow-create-org false}]
|
||||
|
||||
[:> team-options-dropdown* {:show show-team-options-menu?
|
||||
:on-close close-team-options-menu
|
||||
@@ -703,6 +795,8 @@
|
||||
[:*
|
||||
[: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
|
||||
|
||||
@@ -18,6 +18,7 @@ $sz-32: px2rem(32);
|
||||
$sz-36: px2rem(36);
|
||||
$sz-40: px2rem(40);
|
||||
$sz-48: px2rem(48);
|
||||
$sz-64: px2rem(64);
|
||||
$sz-88: px2rem(88);
|
||||
$sz-96: px2rem(96);
|
||||
$sz-120: px2rem(120);
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
(: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*]]
|
||||
@@ -52,10 +51,11 @@
|
||||
:has-hint has-hint
|
||||
:hint-type hint-type
|
||||
:variant variant})]
|
||||
[:div {:class (dm/str class " " (stl/css-case :input-wrapper true
|
||||
:variant-dense (= variant "dense")
|
||||
:variant-comfortable (= variant "comfortable")
|
||||
:has-hint has-hint))}
|
||||
|
||||
[:div {:class [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,4 +64,3 @@
|
||||
:class hint-class
|
||||
:message hint-message
|
||||
:type hint-type}])]))
|
||||
|
||||
|
||||
@@ -189,7 +189,6 @@
|
||||
:float
|
||||
:string
|
||||
[:= :multiple]]]]
|
||||
[:text-icon {:optional true} :string]
|
||||
[:default {:optional true} [:maybe :string]]
|
||||
[:placeholder {:optional true} :string]
|
||||
[:icon {:optional true} [:maybe schema:icon]]
|
||||
@@ -217,8 +216,7 @@
|
||||
is-selected-on-focus nillable
|
||||
tokens applied-token empty-to-end
|
||||
on-change on-blur on-focus on-detach
|
||||
property align ref name
|
||||
text-icon]
|
||||
property align ref name]
|
||||
:rest props}]
|
||||
|
||||
(let [;; NOTE: we use mfu/bean here for transparently handle
|
||||
@@ -639,23 +637,14 @@
|
||||
:on-change store-raw-value
|
||||
:variant "comfortable"
|
||||
:disabled disabled
|
||||
:slot-start (when (or icon text-icon)
|
||||
(mf/html
|
||||
[:> tooltip*
|
||||
{:content property
|
||||
:id property}
|
||||
(cond
|
||||
icon
|
||||
[:> icon*
|
||||
{:icon-id icon
|
||||
:size "s"
|
||||
:aria-labelledby property
|
||||
:class (stl/css :icon)}]
|
||||
|
||||
text-icon
|
||||
[:div {:class (stl/css :text-icon)
|
||||
:aria-labelledby property}
|
||||
text-icon])]))
|
||||
:slot-start (when icon
|
||||
(mf/html [:> tooltip*
|
||||
{:content property
|
||||
:id property}
|
||||
[:> icon* {:icon-id icon
|
||||
:size "s"
|
||||
:aria-labelledby property
|
||||
:class (stl/css :icon)}]]))
|
||||
:slot-end (when-not disabled
|
||||
(when (some? tokens)
|
||||
(mf/html [:> icon-button* {:variant "ghost"
|
||||
@@ -687,23 +676,14 @@
|
||||
:disabled disabled
|
||||
:on-blur on-blur
|
||||
:class inner-class
|
||||
:slot-start (when (or icon text-icon)
|
||||
(mf/html
|
||||
[:> tooltip*
|
||||
{:content property
|
||||
:id property}
|
||||
(cond
|
||||
icon
|
||||
[:> icon*
|
||||
{:icon-id icon
|
||||
:size "s"
|
||||
:aria-labelledby property
|
||||
:class (stl/css :icon)}]
|
||||
|
||||
text-icon
|
||||
[:div {:class (stl/css :text-icon)
|
||||
:aria-labelledby property}
|
||||
text-icon])]))
|
||||
:slot-start (when icon
|
||||
(mf/html [:> tooltip*
|
||||
{:content property
|
||||
:id property}
|
||||
[:> icon* {:icon-id icon
|
||||
:size "s"
|
||||
:aria-labelledby property
|
||||
:class (stl/css :icon)}]]))
|
||||
:token-wrapper-ref token-wrapper-ref
|
||||
:token-detach-btn-ref token-detach-btn-ref
|
||||
:detach-token detach-token})))]
|
||||
@@ -738,41 +718,21 @@
|
||||
(mf/with-effect [dropdown-options]
|
||||
(mf/set-ref-val! options-ref dropdown-options))
|
||||
|
||||
(if (some? icon)
|
||||
[:div {:class (dm/str class " " (stl/css :input-wrapper))
|
||||
:ref wrapper-ref}
|
||||
[:div {:class (dm/str class " " (stl/css :input-wrapper))
|
||||
:ref wrapper-ref}
|
||||
|
||||
(if (and (some? token-applied)
|
||||
(not= :multiple token-applied))
|
||||
[:> token-field* token-props]
|
||||
[:> input-field* input-props])
|
||||
(if (and (some? token-applied)
|
||||
(not= :multiple token-applied))
|
||||
[:> token-field* token-props]
|
||||
[:> input-field* input-props])
|
||||
|
||||
(when ^boolean is-open
|
||||
(let [options (if (delay? dropdown-options) @dropdown-options dropdown-options)]
|
||||
[:> options-dropdown* {:on-click on-option-click
|
||||
:id listbox-id
|
||||
:options options
|
||||
:selected selected-id
|
||||
:focused focused-id
|
||||
:align align
|
||||
:empty-to-end empty-to-end
|
||||
:ref set-option-ref}]))]
|
||||
[:div {:class (dm/str class " " (stl/css :input-wrapper))
|
||||
:aria-labelledby property
|
||||
:ref wrapper-ref}
|
||||
|
||||
(if (and (some? token-applied)
|
||||
(not= :multiple token-applied))
|
||||
[:> token-field* token-props]
|
||||
[:> input-field* input-props])
|
||||
|
||||
(when ^boolean is-open
|
||||
(let [options (if (delay? dropdown-options) @dropdown-options dropdown-options)]
|
||||
[:> options-dropdown* {:on-click on-option-click
|
||||
:id listbox-id
|
||||
:options options
|
||||
:selected selected-id
|
||||
:focused focused-id
|
||||
:align align
|
||||
:empty-to-end empty-to-end
|
||||
:ref set-option-ref}]))])))
|
||||
(when ^boolean is-open
|
||||
(let [options (if (delay? dropdown-options) @dropdown-options dropdown-options)]
|
||||
[:> options-dropdown* {:on-click on-option-click
|
||||
:id listbox-id
|
||||
:options options
|
||||
:selected selected-id
|
||||
:focused focused-id
|
||||
:align align
|
||||
:empty-to-end empty-to-end
|
||||
:ref set-option-ref}]))]))
|
||||
|
||||
@@ -29,14 +29,7 @@
|
||||
|
||||
.icon {
|
||||
color: var(--color-foreground-secondary);
|
||||
min-inline-size: var(--sp-l);
|
||||
}
|
||||
|
||||
.text-icon {
|
||||
color: var(--color-foreground-secondary);
|
||||
@include t.use-typography("code-font");
|
||||
inline-size: fit-content;
|
||||
min-inline-size: px2rem(40);
|
||||
min-width: var(--sp-l);
|
||||
}
|
||||
|
||||
.invisible-button {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
--token-field-outline-color: none;
|
||||
--token-field-height: var(--sp-xxxl);
|
||||
--token-field-margin: unset;
|
||||
display: inline-flex;
|
||||
display: grid;
|
||||
column-gap: var(--sp-xs);
|
||||
align-items: center;
|
||||
position: relative;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
(: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]
|
||||
@@ -47,6 +48,23 @@
|
||||
|
||||
[:> 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}]
|
||||
|
||||
@@ -80,4 +98,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]]))
|
||||
|
||||
48
frontend/src/app/main/ui/nitrate/nitrate_form.cljs
Normal file
48
frontend/src/app/main/ui/nitrate/nitrate_form.cljs
Normal file
@@ -0,0 +1,48 @@
|
||||
;; 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.nitrate.nitrate-form
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.util.dom :as dom]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
;; 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}
|
||||
[]
|
||||
(let [on-click
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(dom/open-new-window "/control-center/licenses/start")))]
|
||||
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:div {:class (stl/css :nitrate-form)}
|
||||
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h2 {:class (stl/css :modal-title)}
|
||||
"BUY NITRATE"]
|
||||
|
||||
[:button {:class (stl/css :modal-close-btn)
|
||||
:on-click modal/hide!} deprecated-icon/close]]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
"Nitrate is so cool! You should buy it!"]
|
||||
|
||||
[:div {:class (stl/css :modal-footer)}
|
||||
[:div {:class (stl/css :action-buttons)}
|
||||
[:> button* {:variant "primary"
|
||||
:on-click on-click}
|
||||
"BUY NOW!"]]]]]]))
|
||||
|
||||
|
||||
52
frontend/src/app/main/ui/nitrate/nitrate_form.scss
Normal file
52
frontend/src/app/main/ui/nitrate/nitrate_form.scss
Normal file
@@ -0,0 +1,52 @@
|
||||
// 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;
|
||||
|
||||
.modal-overlay {
|
||||
@extend .modal-overlay-base;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
@extend .modal-container-base;
|
||||
}
|
||||
|
||||
.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 {
|
||||
margin-bottom: deprecated.$s-24;
|
||||
}
|
||||
|
||||
.nitrate-form {
|
||||
min-width: deprecated.$s-400;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
@extend .modal-action-btns;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
@extend .modal-cancel-btn;
|
||||
}
|
||||
|
||||
.accept-btn {
|
||||
@extend .modal-accept-btn;
|
||||
|
||||
&.danger {
|
||||
@extend .modal-danger-btn;
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
["/feedback" :settings-feedback]
|
||||
["/options" :settings-options]
|
||||
["/subscriptions" :settings-subscription]
|
||||
["/access-tokens" :settings-access-tokens]
|
||||
["/integrations" :settings-integrations]
|
||||
["/notifications" :settings-notifications]]
|
||||
|
||||
["/frame-preview" :frame-preview]
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[app.main.ui.modal :refer [modal-container*]]
|
||||
[app.main.ui.settings.access-tokens :refer [access-tokens-page]]
|
||||
[app.main.ui.settings.change-email]
|
||||
[app.main.ui.settings.delete-account]
|
||||
[app.main.ui.settings.feedback :refer [feedback-page*]]
|
||||
[app.main.ui.settings.integrations :refer [integrations-page*]]
|
||||
[app.main.ui.settings.notifications :refer [notifications-page*]]
|
||||
[app.main.ui.settings.options :refer [options-page]]
|
||||
[app.main.ui.settings.password :refer [password-page]]
|
||||
@@ -73,8 +73,8 @@
|
||||
:settings-subscription
|
||||
[:> subscription-page* {:profile profile}]
|
||||
|
||||
:settings-access-tokens
|
||||
[:& access-tokens-page]
|
||||
:settings-integrations
|
||||
[:> integrations-page*]
|
||||
|
||||
:settings-notifications
|
||||
[:& notifications-page* {:profile profile}])]]]]))
|
||||
|
||||
@@ -1,291 +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.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)}])]])]))
|
||||
|
||||
@@ -1,202 +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;
|
||||
|
||||
// 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;
|
||||
}
|
||||
404
frontend/src/app/main/ui/settings/integrations.cljs
Normal file
404
frontend/src/app/main/ui/settings/integrations.cljs
Normal file
@@ -0,0 +1,404 @@
|
||||
;; 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.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.config :as cf]
|
||||
[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.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 ^: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 create-token-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :create-token}
|
||||
[{:keys [token-type title-create title-created]}]
|
||||
(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)
|
||||
(some? token-type) (assoc :type token-type))]
|
||||
(st/emit! (du/create-access-token (with-meta params mdata))))))
|
||||
|
||||
on-copy-to-clipboard
|
||||
(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)}
|
||||
[:> icon-button* {:variant "ghost"
|
||||
:class (stl/css :modal-close-button)
|
||||
:aria-label (tr "labels.close")
|
||||
:on-click on-close
|
||||
:icon i/close}]
|
||||
|
||||
(if @created?
|
||||
[:div {:class (stl/css :modal-form)}
|
||||
[:> text* {:as "h2"
|
||||
:typography t/headline-large
|
||||
:class (stl/css :color-primary)}
|
||||
title-created]
|
||||
|
||||
[:> notification-pill* {:level :info
|
||||
:type :context}
|
||||
"This unique token is non-recuperable. If you lose it, you will need to create a new one."]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-token)}
|
||||
[:> input* {:type "text"
|
||||
:default-value (:token created "")
|
||||
:read-only true}]
|
||||
[:> icon-button* {:variant "secondary"
|
||||
:class (stl/css :modal-token-button)
|
||||
:aria-label (tr "modals.create-access-token.copy-token")
|
||||
:on-click on-copy-to-clipboard
|
||||
:icon i/clipboard}]]
|
||||
|
||||
[:> text* {:as "div"
|
||||
:typography t/body-small
|
||||
:class (stl/css :color-secondary)}
|
||||
(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 :modal-footer)}
|
||||
[:> button* {:variant "secondary"
|
||||
:on-click modal/hide!}
|
||||
(tr "labels.close")]]]
|
||||
|
||||
[:> 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-create]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:> fc/form-input* {:type "text"
|
||||
:auto-focus? true
|
||||
:form form
|
||||
:name :name
|
||||
:label (tr "modals.create-access-token.name.label")
|
||||
:placeholder (tr "modals.create-access-token.name.placeholder")}]]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:> text* {:as "label"
|
||||
:typography t/body-small
|
||||
:for :expiration-date
|
||||
:class (stl/css :color-primary)}
|
||||
(tr "modals.create-access-token.expiration-date.label")]
|
||||
[:> fc/form-select* {:options [{:label (tr "dashboard.access-tokens.expiration-never") :value "never" :id "never"}
|
||||
{:label (tr "dashboard.access-tokens.expiration-30-days") :value "720h" :id "720h"}
|
||||
{:label (tr "dashboard.access-tokens.expiration-60-days") :value "1440h" :id "1440h"}
|
||||
{:label (tr "dashboard.access-tokens.expiration-90-days") :value "2160h" :id "2160h"}
|
||||
{:label (tr "dashboard.access-tokens.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-create]]])]]))
|
||||
|
||||
(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 "modals.delete-acces-token.title")
|
||||
:message (tr "modals.delete-acces-token.message")
|
||||
:accept-label (tr "modals.delete-acces-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 "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 :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?
|
||||
:fixed true
|
||||
:min-width true
|
||||
:top "auto"
|
||||
:left "auto"
|
||||
:options options}]]]))
|
||||
|
||||
(mf/defc mcp-server-section*
|
||||
{::mf/private true}
|
||||
[]
|
||||
(let [tokens (mf/deref tokens-ref)
|
||||
profile (mf/deref refs/profile)
|
||||
|
||||
active? (d/nilv (-> profile :props :mcp-status) false)
|
||||
|
||||
handle-mcp-status-change
|
||||
(mf/use-fn
|
||||
(fn [value]
|
||||
(st/emit! (du/update-profile-props {:mcp-status value}))
|
||||
(when (true? value)
|
||||
(st/emit! (modal/show {:type :create-token
|
||||
:token-type "mcp"
|
||||
:title-create (tr "modals.create-access-token.title")
|
||||
:title-created (tr "modals.create-access-token.title.created")})))))
|
||||
|
||||
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 :mcp-server-section)}
|
||||
[:div
|
||||
[:> heading* {:level 2
|
||||
:typography t/title-medium
|
||||
:class (stl/css :color-primary :mcp-server-title)}
|
||||
"MCP Server"]
|
||||
|
||||
[:> text* {:as "p"
|
||||
:typography t/body-medium
|
||||
:class (stl/css :color-secondary)}
|
||||
"The Penpot MCP Server enables MCP clients to interact directly with Penpot design files."]]
|
||||
|
||||
[:div
|
||||
[:> text* {:as "h3"
|
||||
:typography t/headline-small
|
||||
:class (stl/css :color-primary)}
|
||||
"Status"]
|
||||
|
||||
[:> switch* {:label (if active? "Enabled" "Disabled")
|
||||
:default-checked active?
|
||||
:on-change handle-mcp-status-change}]]
|
||||
|
||||
[:div
|
||||
[:> text* {:as "h3"
|
||||
:typography t/headline-small
|
||||
:class (stl/css :color-primary)}
|
||||
"MCP keys"]
|
||||
|
||||
[:> button* {:variant "primary"}
|
||||
"Regenerate MCP key"]]
|
||||
|
||||
[:div {:class (stl/css :list)}
|
||||
(for [token tokens]
|
||||
(when (= (:type token) "mcp")
|
||||
[:> token-item* {:key (:id token)
|
||||
:name (:name token)
|
||||
:expires-at (:expires-at token)
|
||||
:on-delete (partial handle-delete (:id token))}]))]
|
||||
|
||||
[:> 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)}
|
||||
"This is the server url you'll need to configure your MCP client in order to connect it to the Penpot MCP server."]
|
||||
[:> text* {:as "div"
|
||||
:typography t/body-medium
|
||||
:class (stl/css :color-secondary)}
|
||||
"https://penpot.dev/mcp" [:> icon* {:icon-id i/clipboard :size "m"}] "Copy link"]
|
||||
[:> text* {:as "div"
|
||||
:typography t/body-medium
|
||||
:class (stl/css :color-secondary)}
|
||||
[:a {:href "http://"}
|
||||
"How to configure MCP clients" [:> icon* {:icon-id i/open-link :size "m"}]]]]]]))
|
||||
|
||||
(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-token
|
||||
:title-create (tr "modals.create-access-token.title")
|
||||
:title-created (tr "modals.create-access-token.title.created")})))
|
||||
|
||||
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 "dashboard.access-tokens.personal")]
|
||||
|
||||
[:> text* {:as "div"
|
||||
:typography t/body-medium
|
||||
:class (stl/css :color-secondary)}
|
||||
(tr "dashboard.access-tokens.personal.description")]
|
||||
|
||||
[:> button* {:variant "primary"
|
||||
:class (stl/css :fit-content)
|
||||
:on-click handle-click}
|
||||
(tr "dashboard.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 "dashboard.access-tokens.empty.no-access-tokens")]
|
||||
[:div (tr "dashboard.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)}
|
||||
"Integrations"]
|
||||
|
||||
(when (contains? cf/flags :mcp-server)
|
||||
[:> mcp-server-section*])
|
||||
|
||||
(when (and (contains? cf/flags :mcp-server)
|
||||
(contains? cf/flags :access-tokens))
|
||||
[:hr {:class (stl/css :separator)}])
|
||||
|
||||
(when (contains? cf/flags :access-tokens)
|
||||
[:> access-tokens-section*])])
|
||||
158
frontend/src/app/main/ui/settings/integrations.scss
Normal file
158
frontend/src/app/main/ui/settings/integrations.scss
Normal file
@@ -0,0 +1,158 @@
|
||||
// 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/spacing.scss" as *;
|
||||
@use "ds/mixins.scss" as *;
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
@extend .modal-overlay-base;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
@extend .modal-container-base;
|
||||
inline-size: $sz-400;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.modal-token {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.modal-token-button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
border-start-start-radius: 0;
|
||||
border-end-start-radius: 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-s);
|
||||
}
|
||||
|
||||
.mcp-server-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-l);
|
||||
}
|
||||
|
||||
.mcp-server-notification {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
|
||||
.mcp-server-title {
|
||||
margin: var(--sp-s) 0;
|
||||
}
|
||||
|
||||
.separator {
|
||||
border: 1px solid var(--color-background-quaternary);
|
||||
margin: var(--sp-s) 0;
|
||||
}
|
||||
|
||||
.frame {
|
||||
border: 1px 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: 40% 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;
|
||||
padding: 0 var(--sp-l);
|
||||
color: var(--color-foreground-secondary);
|
||||
|
||||
&.warning {
|
||||
padding: 0 var(--sp-m);
|
||||
block-size: $sz-32;
|
||||
inline-size: fit-content;
|
||||
color: var(--color-foreground-primary);
|
||||
background-color: var(--color-background-warning);
|
||||
border: 1px 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;
|
||||
}
|
||||
@@ -43,8 +43,8 @@
|
||||
(def ^:private go-settings-subscription
|
||||
#(st/emit! (rt/nav :settings-subscription)))
|
||||
|
||||
(def ^:private go-settings-access-tokens
|
||||
#(st/emit! (rt/nav :settings-access-tokens)))
|
||||
(def ^:private go-settings-integrations
|
||||
#(st/emit! (rt/nav :settings-integrations)))
|
||||
|
||||
(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)
|
||||
access-tokens? (= section :settings-access-tokens)
|
||||
integrations? (= section :settings-integrations)
|
||||
notifications? (= section :settings-notifications)
|
||||
team-id (or (dtm/get-last-team-id)
|
||||
(:default-team-id profile))
|
||||
@@ -115,12 +115,13 @@
|
||||
:data-testid "settings-subscription"}
|
||||
[:span {:class (stl/css :element-title)} (tr "subscription.labels")]])
|
||||
|
||||
(when (contains? cf/flags :access-tokens)
|
||||
[:li {:class (stl/css-case :current access-tokens?
|
||||
(when (or (contains? cf/flags :access-tokens)
|
||||
(contains? cf/flags :mcp-server))
|
||||
[:li {:class (stl/css-case :current integrations?
|
||||
:settings-item true)
|
||||
:on-click go-settings-access-tokens
|
||||
:data-testid "settings-access-tokens"}
|
||||
[:span {:class (stl/css :element-title)} (tr "labels.access-tokens")]])
|
||||
:on-click go-settings-integrations
|
||||
:data-testid "settings-integrations"}
|
||||
[:span {:class (stl/css :element-title)} (tr "labels.integrations")]])
|
||||
|
||||
[:hr {:class (stl/css :sidebar-separator)}]
|
||||
|
||||
|
||||
@@ -119,7 +119,6 @@
|
||||
[:button {:class (stl/css-case
|
||||
:toggle-content true
|
||||
:inverse expanded?)
|
||||
:aria-label "Toggle layer"
|
||||
:on-click on-toggle-collapse}
|
||||
deprecated-icon/arrow])
|
||||
|
||||
|
||||
@@ -3,15 +3,17 @@
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.types.shape.radius :as ctsr]
|
||||
[app.common.types.token :as tk]
|
||||
[app.main.data.workspace.shapes :as dwsh]
|
||||
[app.main.data.workspace.tokens.application :as dwta]
|
||||
[app.main.features :as features]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.numeric-input :as deprecated-input]
|
||||
[app.main.ui.context :as muc]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.controls.numeric-input :refer [numeric-input*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[app.main.ui.workspace.sidebar.options.menus.input-wrapper-tokens :refer [numeric-input-wrapper*]]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[beicon.v2.core :as rx]
|
||||
[potok.v2.core :as ptk]
|
||||
@@ -44,6 +46,63 @@
|
||||
(identical? (get old-values :r4)
|
||||
(get new-values :r4)))))
|
||||
|
||||
(mf/defc numeric-input-wrapper*
|
||||
{::mf/private true}
|
||||
[{:keys [values name applied-tokens align on-detach radius] :rest props}]
|
||||
(let [tokens (mf/use-ctx muc/active-tokens-by-type)
|
||||
tokens (mf/with-memo [tokens name]
|
||||
(delay
|
||||
(-> (deref tokens)
|
||||
(select-keys (get tk/tokens-by-input name))
|
||||
(not-empty))))
|
||||
on-detach-attr
|
||||
(mf/use-fn
|
||||
(mf/deps on-detach name)
|
||||
#(on-detach % name))
|
||||
|
||||
r1-value (get applied-tokens :r1)
|
||||
all-token-equal? (and (seq applied-tokens) (all-equal? applied-tokens))
|
||||
all-values-equal? (all-equal? values)
|
||||
|
||||
applied-token (cond
|
||||
(not (seq applied-tokens))
|
||||
nil
|
||||
|
||||
(and (= radius :all) (or (not all-values-equal?) (not all-token-equal?)))
|
||||
:multiple
|
||||
|
||||
(and all-token-equal? all-values-equal? (= radius :all))
|
||||
r1-value
|
||||
|
||||
:else
|
||||
(get applied-tokens radius))
|
||||
|
||||
|
||||
placeholder (if (= radius :all)
|
||||
(cond
|
||||
(or (not all-values-equal?)
|
||||
(not all-token-equal?))
|
||||
(tr "settings.multiple")
|
||||
:else
|
||||
"--")
|
||||
|
||||
(cond
|
||||
(or (= :multiple (:applied-tokens values))
|
||||
(= :multiple (get values name)))
|
||||
(tr "settings.multiple")
|
||||
:else
|
||||
"--"))
|
||||
|
||||
|
||||
props (mf/spread-props props
|
||||
{:placeholder placeholder
|
||||
:applied-token applied-token
|
||||
:tokens (if (delay? tokens) @tokens tokens)
|
||||
:align align
|
||||
:on-detach on-detach-attr
|
||||
:value values})]
|
||||
[:> numeric-input* props]))
|
||||
|
||||
(mf/defc border-radius-menu*
|
||||
{::mf/wrap [#(mf/memo' % check-border-radius-menu-props)]}
|
||||
[{:keys [class ids values applied-tokens]}]
|
||||
@@ -51,7 +110,6 @@
|
||||
(features/use-feature "tokens/numeric-input")
|
||||
|
||||
all-values-equal? (all-equal? values)
|
||||
all-token-equal? (and (seq applied-tokens) (all-equal? applied-tokens))
|
||||
|
||||
radius-expanded* (mf/use-state false)
|
||||
radius-expanded (deref radius-expanded*)
|
||||
@@ -177,31 +235,18 @@
|
||||
:on-detach on-detach-all
|
||||
:icon i/corner-radius
|
||||
:min 0
|
||||
:attr :border-radius
|
||||
:name :border-radius
|
||||
:nillable true
|
||||
:property (tr "workspace.options.radius")
|
||||
:class (stl/css :radius-wrapper)
|
||||
:applied-token (cond
|
||||
(not (seq applied-tokens))
|
||||
nil
|
||||
|
||||
(or (not all-values-equal?) (not all-token-equal?))
|
||||
:multiple
|
||||
|
||||
:else
|
||||
(get applied-tokens :r1))
|
||||
:applied-tokens applied-tokens
|
||||
:radius :all
|
||||
:align :right
|
||||
:placeholder (cond
|
||||
(or (not all-values-equal?)
|
||||
(not all-token-equal?))
|
||||
(tr "settings.multiple")
|
||||
:else
|
||||
"--")
|
||||
:value (if all-values-equal?
|
||||
(if (nil? (:r1 values))
|
||||
0
|
||||
(:r1 values))
|
||||
nil)}]
|
||||
:values (if all-values-equal?
|
||||
(if (nil? (:r1 values))
|
||||
0
|
||||
(:r1 values))
|
||||
nil)}]
|
||||
|
||||
[:div {:class (stl/css :radius-1)
|
||||
:title (tr "workspace.options.radius")}
|
||||
@@ -231,76 +276,56 @@
|
||||
{:on-change on-radius-r1-change
|
||||
:on-detach on-detach-r1
|
||||
:min 0
|
||||
:attr :border-radius
|
||||
:name :border-radius
|
||||
:property (tr "workspace.options.radius-top-left")
|
||||
:applied-token (get applied-tokens :r1)
|
||||
:applied-tokens applied-tokens
|
||||
:radius :r1
|
||||
:align :right
|
||||
:placeholder (cond
|
||||
(or (= :multiple (:applied-tokens values))
|
||||
(= :multiple (get values :r1)))
|
||||
(tr "settings.multiple")
|
||||
:else
|
||||
"--")
|
||||
:class (stl/css :radius-wrapper :dropdown-offset)
|
||||
:inner-class (stl/css :no-icon-input)
|
||||
:value (:r1 values)}]
|
||||
:values (:r1 values)}]
|
||||
|
||||
[:> numeric-input-wrapper*
|
||||
{:on-change on-radius-r2-change
|
||||
:on-detach on-detach-r2
|
||||
:min 0
|
||||
:attr :border-radius
|
||||
:name :border-radius
|
||||
:nillable true
|
||||
:property (tr "workspace.options.radius-top-right")
|
||||
:applied-token (get applied-tokens :r2)
|
||||
:applied-tokens applied-tokens
|
||||
:align :right
|
||||
:class (stl/css :radius-wrapper)
|
||||
:inner-class (stl/css :no-icon-input)
|
||||
:placeholder (cond
|
||||
(or (= :multiple (:applied-tokens values))
|
||||
(= :multiple (get values :r2)))
|
||||
(tr "settings.multiple")
|
||||
:else
|
||||
"--")
|
||||
:value (:r2 values)}]
|
||||
:radius :r2
|
||||
:values (:r2 values)}]
|
||||
|
||||
[:> numeric-input-wrapper*
|
||||
{:on-change on-radius-r4-change
|
||||
:on-detach on-detach-r4
|
||||
:min 0
|
||||
:attr :border-radius
|
||||
:name :border-radius
|
||||
:nillable true
|
||||
:property (tr "workspace.options.radius-bottom-left")
|
||||
:applied-token (get applied-tokens :r4)
|
||||
:applied-tokens applied-tokens
|
||||
:class (stl/css :radius-wrapper :dropdown-offset)
|
||||
:inner-class (stl/css :no-icon-input)
|
||||
:placeholder (cond
|
||||
(or (= :multiple (:applied-tokens values))
|
||||
(= :multiple (get values :r4)))
|
||||
(tr "settings.multiple")
|
||||
:else
|
||||
"--")
|
||||
:radius :r4
|
||||
:align :right
|
||||
:value (:r4 values)}]
|
||||
:values (:r4 values)}]
|
||||
|
||||
[:> numeric-input-wrapper*
|
||||
{:on-change on-radius-r3-change
|
||||
:on-detach on-detach-r3
|
||||
:min 0
|
||||
:attr :border-radius
|
||||
:name :border-radius
|
||||
:nillable true
|
||||
:property (tr "workspace.options.radius-bottom-right")
|
||||
:applied-token (get applied-tokens :r3)
|
||||
:placeholder (cond
|
||||
(or (= :multiple (:applied-tokens values))
|
||||
(= :multiple (get values :r3)))
|
||||
(tr "settings.multiple")
|
||||
:else
|
||||
"--")
|
||||
:applied-tokens applied-tokens
|
||||
:radius :r3
|
||||
:align :right
|
||||
:class (stl/css :radius-wrapper)
|
||||
:inner-class (stl/css :no-icon-input)
|
||||
:value (:r3 values)}]]
|
||||
:values (:r3 values)}]]
|
||||
|
||||
[:div {:class (stl/css :radius-4)}
|
||||
[:div {:class (stl/css :small-input)}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
(ns app.main.ui.workspace.sidebar.options.menus.input-wrapper-tokens
|
||||
(:require
|
||||
[app.common.types.token :as tk]
|
||||
[app.main.ui.context :as muc]
|
||||
[app.main.ui.ds.controls.numeric-input :refer [numeric-input*]]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc numeric-input-wrapper*
|
||||
[{:keys [value attr applied-token align on-detach placeholder input-type] :rest props}]
|
||||
(let [tokens (mf/use-ctx muc/active-tokens-by-type)
|
||||
|
||||
tokens (mf/with-memo [tokens input-type]
|
||||
(delay
|
||||
(-> (deref tokens)
|
||||
(select-keys (get tk/tokens-by-input (or input-type attr)))
|
||||
(not-empty))))
|
||||
|
||||
on-detach-attr
|
||||
(mf/use-fn
|
||||
(mf/deps on-detach attr)
|
||||
#(on-detach % attr))
|
||||
|
||||
props (mf/spread-props props
|
||||
{:placeholder (or placeholder
|
||||
(if (= :multiple value)
|
||||
(tr "settings.multiple")
|
||||
"--"))
|
||||
:applied-token applied-token
|
||||
:tokens (if (delay? tokens) @tokens tokens)
|
||||
:align align
|
||||
:on-detach on-detach-attr
|
||||
:name attr
|
||||
:value value})]
|
||||
[:> numeric-input* props]))
|
||||
@@ -9,6 +9,7 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.types.token :as tk]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.shapes :as dwsh]
|
||||
[app.main.data.workspace.tokens.application :as dwta]
|
||||
@@ -16,9 +17,10 @@
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.numeric-input :as deprecated-input]
|
||||
[app.main.ui.components.select :refer [select]]
|
||||
[app.main.ui.context :as muc]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.controls.numeric-input :refer [numeric-input*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
[app.main.ui.workspace.sidebar.options.menus.input-wrapper-tokens :refer [numeric-input-wrapper*]]
|
||||
[app.render-wasm.api :as wasm.api]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[rumext.v2 :as mf]))
|
||||
@@ -60,6 +62,36 @@
|
||||
(identical? (get old-values :hidden)
|
||||
(get new-values :hidden)))))
|
||||
|
||||
(mf/defc numeric-input-wrapper*
|
||||
{::mf/private true}
|
||||
[{:keys [values name applied-tokens align on-detach] :rest props}]
|
||||
(let [tokens (mf/use-ctx muc/active-tokens-by-type)
|
||||
tokens (mf/with-memo [tokens name]
|
||||
(delay
|
||||
(-> (deref tokens)
|
||||
(select-keys (get tk/tokens-by-input name))
|
||||
(not-empty))))
|
||||
|
||||
on-detach-attr (mf/use-fn
|
||||
(mf/deps on-detach name)
|
||||
#(on-detach % name))
|
||||
|
||||
applied-token (get applied-tokens name)
|
||||
opacity-value (or (get values name) 1)
|
||||
|
||||
props (mf/spread-props props
|
||||
{:placeholder (if (or (= :multiple (:applied-tokens values))
|
||||
(= :multiple opacity-value))
|
||||
(tr "settings.multiple")
|
||||
"--")
|
||||
:applied-token applied-token
|
||||
:tokens (if (delay? tokens) @tokens tokens)
|
||||
:align align
|
||||
:on-detach on-detach-attr
|
||||
:name name
|
||||
:value (* 100 opacity-value)})]
|
||||
[:> numeric-input* props]))
|
||||
|
||||
(mf/defc layer-menu*
|
||||
{::mf/wrap [#(mf/memo' % check-layer-menu-props)]}
|
||||
[{:keys [ids values applied-tokens]}]
|
||||
@@ -218,24 +250,22 @@
|
||||
:on-pointer-enter-option handle-blend-mode-enter
|
||||
:on-pointer-leave-option handle-blend-mode-leave}]]
|
||||
|
||||
|
||||
|
||||
(if token-numeric-inputs
|
||||
|
||||
[:> numeric-input-wrapper*
|
||||
{:on-change on-opacity-change
|
||||
:on-detach on-detach-token
|
||||
:icon i/percentage
|
||||
:min 0
|
||||
:max 100
|
||||
:attr :opacity
|
||||
:name :opacity
|
||||
:property (tr "workspace.options.opacity")
|
||||
:applied-token (get applied-tokens :opacity)
|
||||
:placeholder (if (or (= :multiple (:applied-tokens values))
|
||||
(= :multiple (or (get values name) 1)))
|
||||
(tr "settings.multiple")
|
||||
"--")
|
||||
:applied-tokens applied-tokens
|
||||
:align :right
|
||||
:class (stl/css :numeric-input-wrapper)
|
||||
:value (* 100
|
||||
(or (get values name) 1))}]
|
||||
:values values}]
|
||||
|
||||
[:div {:class (stl/css :input)
|
||||
:title (tr "workspace.options.opacity")}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.math :as mth]
|
||||
[app.common.types.shape.layout :as ctl]
|
||||
[app.common.types.token :as tk]
|
||||
[app.config :as cf]
|
||||
[app.main.data.event :as-alias ev]
|
||||
[app.main.data.workspace :as udw]
|
||||
@@ -24,14 +25,15 @@
|
||||
[app.main.ui.components.numeric-input :as deprecated-input]
|
||||
[app.main.ui.components.select :refer [select]]
|
||||
[app.main.ui.components.title-bar :refer [title-bar*]]
|
||||
[app.main.ui.context :as muc]
|
||||
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.controls.numeric-input :refer [numeric-input*]]
|
||||
[app.main.ui.ds.controls.radio-buttons :refer [radio-buttons*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
[app.main.ui.formats :as fmt]
|
||||
[app.main.ui.hooks :as h]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.main.ui.workspace.sidebar.options.menus.input-wrapper-tokens :refer [numeric-input-wrapper*]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
@@ -46,6 +48,44 @@
|
||||
:column i/column
|
||||
:column-reverse i/column-reverse))
|
||||
|
||||
|
||||
(mf/defc numeric-input-wrapper*
|
||||
{::mf/private true}
|
||||
[{:keys [values name applied-tokens align on-detach] :rest props}]
|
||||
(let [tokens (mf/use-ctx muc/active-tokens-by-type)
|
||||
input-type (cond
|
||||
(some #{:p2 :p4} [name])
|
||||
:horizontal-padding
|
||||
|
||||
(some #{:p1 :p3} [name])
|
||||
:vertical-padding
|
||||
:else
|
||||
name)
|
||||
|
||||
tokens (mf/with-memo [tokens input-type]
|
||||
(delay
|
||||
(-> (deref tokens)
|
||||
(select-keys (get tk/tokens-by-input input-type))
|
||||
(not-empty))))
|
||||
on-detach-attr
|
||||
(mf/use-fn
|
||||
(mf/deps on-detach name)
|
||||
#(on-detach % name))
|
||||
|
||||
props (mf/spread-props props
|
||||
{:placeholder (if (or (= :multiple (:applied-tokens values))
|
||||
(= :multiple (get values name))
|
||||
(nil? (get values name)))
|
||||
(tr "settings.multiple")
|
||||
"--")
|
||||
:class (stl/css :numeric-input-layout)
|
||||
:applied-token (get applied-tokens name)
|
||||
:tokens tokens
|
||||
:align align
|
||||
:on-detach on-detach-attr
|
||||
:value (get values name)})]
|
||||
[:> numeric-input* props]))
|
||||
|
||||
;; FLEX COMPONENTS
|
||||
|
||||
(def layout-container-flex-attrs
|
||||
@@ -375,17 +415,11 @@
|
||||
:on-focus on-focus-p1
|
||||
:icon i/padding-top-bottom
|
||||
:min 0
|
||||
:attr :p1
|
||||
:input-type :vertical-padding
|
||||
:name :p1
|
||||
:property (tr "workspace.layout-grid.editor.padding.vertical")
|
||||
:nillable true
|
||||
:placeholder (if (or (= :multiple (:applied-tokens p1))
|
||||
(= :multiple p1)
|
||||
(nil? p1))
|
||||
(tr "settings.multiple")
|
||||
"--")
|
||||
:applied-token applied-to-p1
|
||||
:value p1}]
|
||||
:applied-tokens {:p1 applied-to-p1}
|
||||
:values {:p1 p1}}]
|
||||
|
||||
[:div {:class (stl/css :padding-simple)
|
||||
:title (tr "workspace.layout-grid.editor.padding.vertical")}
|
||||
@@ -410,18 +444,12 @@
|
||||
:on-focus on-focus-p2
|
||||
:icon i/padding-left-right
|
||||
:min 0
|
||||
:attr :p2
|
||||
:input-type :horizontal-padding
|
||||
:name :p2
|
||||
:align :right
|
||||
:property (tr "workspace.layout-grid.editor.padding.horizontal")
|
||||
:nillable true
|
||||
:applied-token applied-to-p2
|
||||
:placeholder (if (or (= :multiple (:applied-tokens p2))
|
||||
(= :multiple p2)
|
||||
(nil? p2))
|
||||
(tr "settings.multiple")
|
||||
"--")
|
||||
:value p2}]
|
||||
:applied-tokens {:p2 applied-to-p2}
|
||||
:values {:p2 p2}}]
|
||||
|
||||
[:div {:class (stl/css :padding-simple)
|
||||
:title (tr "workspace.layout-grid.editor.padding.horizontal")}
|
||||
@@ -507,15 +535,10 @@
|
||||
:on-focus on-focus-p1
|
||||
:icon i/padding-top
|
||||
:min 0
|
||||
:attr :p1
|
||||
:input-type :vertical-padding
|
||||
:name :p1
|
||||
:property (tr "workspace.layout-grid.editor.padding.top")
|
||||
:placeholder (if (or (= :multiple (:applied-tokens p1))
|
||||
(= :multiple p1))
|
||||
(tr "settings.multiple")
|
||||
"--")
|
||||
:applied-token p1
|
||||
:value p1}]
|
||||
:applied-tokens applied-tokens
|
||||
:values value}]
|
||||
|
||||
[:div {:class (stl/css :padding-multiple)
|
||||
:title (tr "workspace.layout-grid.editor.padding.top")}
|
||||
@@ -540,16 +563,11 @@
|
||||
:on-focus on-focus-p2
|
||||
:icon i/padding-right
|
||||
:min 0
|
||||
:attr :p2
|
||||
:input-type :horizontal-padding
|
||||
:name :p2
|
||||
:align :right
|
||||
:property (tr "workspace.layout-grid.editor.padding.right")
|
||||
:placeholder (if (or (= :multiple (:applied-tokens p2))
|
||||
(= :multiple p2))
|
||||
(tr "settings.multiple")
|
||||
"--")
|
||||
:applied-token p2
|
||||
:value p2}]
|
||||
:applied-tokens applied-tokens
|
||||
:values value}]
|
||||
|
||||
[:div {:class (stl/css :padding-multiple)
|
||||
:title (tr "workspace.layout-grid.editor.padding.right")}
|
||||
@@ -574,15 +592,10 @@
|
||||
:on-focus on-focus-p3
|
||||
:icon i/padding-bottom
|
||||
:min 0
|
||||
:attr :p3
|
||||
:input-type :vertical-padding
|
||||
:name :p3
|
||||
:property (tr "workspace.layout-grid.editor.padding.bottom")
|
||||
:placeholder (if (or (= :multiple (:applied-tokens p3))
|
||||
(= :multiple p3))
|
||||
(tr "settings.multiple")
|
||||
"--")
|
||||
:applied-token p3
|
||||
:value p3}]
|
||||
:applied-tokens applied-tokens
|
||||
:values value}]
|
||||
|
||||
[:div {:class (stl/css :padding-multiple)
|
||||
:title (tr "workspace.layout-grid.editor.padding.bottom")}
|
||||
@@ -608,15 +621,10 @@
|
||||
:icon i/padding-left
|
||||
:min 0
|
||||
:align :right
|
||||
:attr :p4
|
||||
:input-type :horizontal-padding
|
||||
:name :p4
|
||||
:property (tr "workspace.layout-grid.editor.padding.left")
|
||||
:placeholder (if (or (= :multiple (:applied-tokens p3))
|
||||
(= :multiple p3))
|
||||
(tr "settings.multiple")
|
||||
"--")
|
||||
:applied-token p3
|
||||
:value p3}]
|
||||
:applied-tokens applied-tokens
|
||||
:values value}]
|
||||
|
||||
[:div {:class (stl/css :padding-multiple)
|
||||
:title (tr "workspace.layout-grid.editor.padding.left")}
|
||||
@@ -749,16 +757,11 @@
|
||||
:icon i/gap-vertical
|
||||
:nillable true
|
||||
:min 0
|
||||
:attr :row-gap
|
||||
:name :row-gap
|
||||
:applied-tokens applied-tokens
|
||||
:property "Row gap"
|
||||
:values {:row-gap (:row-gap value)}
|
||||
:disabled row-gap-disabled?
|
||||
:placeholder (if (or (= :multiple (:applied-tokens (:row-gap value)))
|
||||
(= :multiple (:row-gap value)))
|
||||
(tr "settings.multiple")
|
||||
"--")
|
||||
:applied-token (:row-gap applied-tokens)
|
||||
:value (:row-gap value)}]
|
||||
:disabled row-gap-disabled?}]
|
||||
|
||||
[:div {:class (stl/css-case
|
||||
:row-gap true
|
||||
@@ -788,15 +791,11 @@
|
||||
:icon i/gap-horizontal
|
||||
:nillable true
|
||||
:min 0
|
||||
:attr :column-gap
|
||||
:name :column-gap
|
||||
:align :right
|
||||
:applied-tokens applied-tokens
|
||||
:property "Column gap"
|
||||
:placeholder (if (or (= :multiple (:applied-tokens (:column-gap value)))
|
||||
(= :multiple (:column-gap value)))
|
||||
(tr "settings.multiple")
|
||||
"--")
|
||||
:applied-token (:column-gap applied-tokens)
|
||||
:value (:column-gap value)
|
||||
:values {:column-gap (:column-gap value)}
|
||||
:disabled col-gap-disabled?}]
|
||||
|
||||
[:div {:class (stl/css-case
|
||||
|
||||
@@ -11,8 +11,6 @@
|
||||
[app.common.types.shape.layout :as ctl]
|
||||
[app.main.data.workspace :as udw]
|
||||
[app.main.data.workspace.shape-layout :as dwsl]
|
||||
[app.main.data.workspace.tokens.application :as dwta]
|
||||
[app.main.features :as features]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.numeric-input :as deprecated-input]
|
||||
@@ -21,7 +19,6 @@
|
||||
[app.main.ui.ds.controls.radio-buttons :refer [radio-buttons*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.main.ui.workspace.sidebar.options.menus.input-wrapper-tokens :refer [numeric-input-wrapper*]]
|
||||
[app.main.ui.workspace.sidebar.options.menus.layout-container :refer [get-layout-flex-icon]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
@@ -49,345 +46,147 @@
|
||||
(select-margins (= prop :m1) (= prop :m2) (= prop :m3) (= prop :m4)))
|
||||
|
||||
(mf/defc margin-simple*
|
||||
[{:keys [value on-change on-blur applied-tokens ids]}]
|
||||
(let [token-numeric-inputs
|
||||
(features/use-feature "tokens/numeric-input")
|
||||
|
||||
m1 (:m1 value)
|
||||
[{:keys [value on-change on-blur]}]
|
||||
(let [m1 (:m1 value)
|
||||
m2 (:m2 value)
|
||||
m3 (:m3 value)
|
||||
m4 (:m4 value)
|
||||
|
||||
m1-placeholder (if (and (not= value :multiple) (not= m1 m3)) (tr "settings.multiple") "--")
|
||||
m2-placeholder (if (and (not= value :multiple) (not= m2 m4)) (tr "settings.multiple") "--")
|
||||
|
||||
m1 (when (and (not= value :multiple) (= m1 m3)) m1)
|
||||
m2 (when (and (not= value :multiple) (= m2 m4)) m2)
|
||||
|
||||
token-applied-m1 (:m1 applied-tokens)
|
||||
token-applied-m2 (:m2 applied-tokens)
|
||||
token-applied-m3 (:m3 applied-tokens)
|
||||
token-applied-m4 (:m4 applied-tokens)
|
||||
|
||||
token-applied-m1 (if (and (not= applied-tokens :multiple) (= token-applied-m1 token-applied-m3)) token-applied-m1
|
||||
:multiple)
|
||||
|
||||
token-applied-m2 (if (and (not= applied-tokens :multiple) (= token-applied-m2 token-applied-m4)) token-applied-m2
|
||||
:multiple)
|
||||
|
||||
m1-placeholder (if (and (not= value :multiple)
|
||||
(= m1 m3)
|
||||
(= token-applied-m1 token-applied-m3))
|
||||
"--"
|
||||
(tr "settings.multiple"))
|
||||
|
||||
m2-placeholder (if (and (not= value :multiple)
|
||||
(= m2 m4)
|
||||
(= token-applied-m2 token-applied-m4))
|
||||
"--"
|
||||
(tr "settings.multiple"))
|
||||
|
||||
on-focus
|
||||
(mf/use-fn
|
||||
(fn [attr event]
|
||||
(case attr
|
||||
:m1 (select-margins true false true false)
|
||||
:m2 (select-margins false true false true))
|
||||
(dom/select-target event)))
|
||||
(fn [event]
|
||||
(let [attr (-> (dom/get-current-target event)
|
||||
(dom/get-data "name")
|
||||
(keyword))]
|
||||
(case attr
|
||||
:m1 (select-margins true false true false)
|
||||
:m2 (select-margins false true false true))
|
||||
|
||||
on-detach-token
|
||||
(mf/use-fn
|
||||
(mf/deps ids)
|
||||
(fn [token attr]
|
||||
(st/emit! (dwta/unapply-token {:token (first token)
|
||||
:attributes #{attr}
|
||||
:shape-ids ids}))))
|
||||
|
||||
on-detach-horizontal
|
||||
(mf/use-fn
|
||||
(mf/deps on-detach-token)
|
||||
(fn [token]
|
||||
(prn "token" token)
|
||||
(run! #(on-detach-token token %) [:m2 :m4])))
|
||||
|
||||
on-detach-vertical
|
||||
(mf/use-fn
|
||||
(mf/deps on-detach-token)
|
||||
(fn [token]
|
||||
(run! #(on-detach-token token %) [:m1 :m3])))
|
||||
(dom/select-target event))))
|
||||
|
||||
on-change'
|
||||
(mf/use-fn
|
||||
(mf/deps on-change ids)
|
||||
(fn [value attr]
|
||||
(if (or (string? value) (int? value))
|
||||
(on-change :simple attr value)
|
||||
(do
|
||||
(st/emit!
|
||||
(dwta/toggle-token {:token (first value)
|
||||
:attrs (if (= :m1 attr)
|
||||
#{:m1 :m3}
|
||||
#{:m2 :m4})
|
||||
:shape-ids ids}))))))
|
||||
|
||||
on-focus-m1
|
||||
(mf/use-fn (mf/deps on-focus) #(on-focus :m1))
|
||||
|
||||
on-focus-m2
|
||||
(mf/use-fn (mf/deps on-focus) #(on-focus :m2))
|
||||
|
||||
on-m1-change
|
||||
(mf/use-fn (mf/deps on-change') #(on-change' % :m1))
|
||||
|
||||
on-m2-change
|
||||
(mf/use-fn (mf/deps on-change') #(on-change' % :m2))]
|
||||
(mf/deps on-change)
|
||||
(fn [value event]
|
||||
(let [attr (-> (dom/get-current-target event)
|
||||
(dom/get-data "name")
|
||||
(keyword))]
|
||||
(on-change :simple attr value))))]
|
||||
|
||||
[:div {:class (stl/css :margin-simple)}
|
||||
(if token-numeric-inputs
|
||||
[:> numeric-input-wrapper*
|
||||
{:on-change on-m1-change
|
||||
:on-detach on-detach-vertical
|
||||
:class (stl/css :vertical-margin-wrapper)
|
||||
:on-blur on-blur
|
||||
:on-focus on-focus-m1
|
||||
:placeholder m1-placeholder
|
||||
:icon i/margin-top-bottom
|
||||
:min 0
|
||||
:attr :m1
|
||||
:input-type :vertical-margin
|
||||
:property "Vertical margin "
|
||||
:nillable true
|
||||
:applied-token token-applied-m1
|
||||
:value m1}]
|
||||
[:div {:class (stl/css :vertical-margin)
|
||||
:title "Vertical margin"}
|
||||
[:span {:class (stl/css :icon)}
|
||||
deprecated-icon/margin-top-bottom]
|
||||
[:> deprecated-input/numeric-input* {:class (stl/css :numeric-input)
|
||||
:placeholder m1-placeholder
|
||||
:data-name "m1"
|
||||
:on-focus on-focus
|
||||
:on-change on-change'
|
||||
:on-blur on-blur
|
||||
:nillable true
|
||||
:value m1}]]
|
||||
|
||||
[:div {:class (stl/css :vertical-margin)
|
||||
:title "Vertical margin"}
|
||||
[:span {:class (stl/css :icon)}
|
||||
deprecated-icon/margin-top-bottom]
|
||||
[:> deprecated-input/numeric-input* {:class (stl/css :numeric-input)
|
||||
:placeholder m1-placeholder
|
||||
:data-name "m1"
|
||||
:on-focus on-focus-m1
|
||||
:on-change on-m1-change
|
||||
:on-blur on-blur
|
||||
:nillable true
|
||||
:value m1}]])
|
||||
|
||||
(if token-numeric-inputs
|
||||
[:> numeric-input-wrapper*
|
||||
{:on-change on-m2-change
|
||||
:on-detach on-detach-horizontal
|
||||
:on-blur on-blur
|
||||
:on-focus on-focus-m2
|
||||
:placeholder m2-placeholder
|
||||
:icon i/margin-left-right
|
||||
:class (stl/css :horizontal-margin-wrapper)
|
||||
:min 0
|
||||
:attr :m2
|
||||
:align :right
|
||||
:input-type :horizontal-margin
|
||||
:property "Horizontal margin"
|
||||
:nillable true
|
||||
:applied-token token-applied-m2
|
||||
:value m2}]
|
||||
|
||||
[:div {:class (stl/css :horizontal-margin)
|
||||
:title "Horizontal margin"}
|
||||
[:span {:class (stl/css :icon)}
|
||||
deprecated-icon/margin-left-right]
|
||||
[:> deprecated-input/numeric-input* {:class (stl/css :numeric-input)
|
||||
:placeholder m2-placeholder
|
||||
:data-name "m2"
|
||||
:on-focus on-focus-m2
|
||||
:on-change on-m2-change
|
||||
:on-blur on-blur
|
||||
:nillable true
|
||||
:value m2}]])]))
|
||||
[:div {:class (stl/css :horizontal-margin)
|
||||
:title "Horizontal margin"}
|
||||
[:span {:class (stl/css :icon)}
|
||||
deprecated-icon/margin-left-right]
|
||||
[:> deprecated-input/numeric-input* {:class (stl/css :numeric-input)
|
||||
:placeholder m2-placeholder
|
||||
:data-name "m2"
|
||||
:on-focus on-focus
|
||||
:on-change on-change'
|
||||
:on-blur on-blur
|
||||
:nillable true
|
||||
:value m2}]]]))
|
||||
|
||||
(mf/defc margin-multiple*
|
||||
[{:keys [value on-change on-blur applied-tokens ids]}]
|
||||
(let [token-numeric-inputs
|
||||
(features/use-feature "tokens/numeric-input")
|
||||
|
||||
m1 (:m1 value)
|
||||
[{:keys [value on-change on-blur]}]
|
||||
(let [m1 (:m1 value)
|
||||
m2 (:m2 value)
|
||||
m3 (:m3 value)
|
||||
m4 (:m4 value)
|
||||
|
||||
applied-token-to-m1 (:m1 applied-tokens)
|
||||
applied-token-to-m2 (:m2 applied-tokens)
|
||||
applied-token-to-m3 (:m3 applied-tokens)
|
||||
applied-token-to-m4 (:m4 applied-tokens)
|
||||
|
||||
on-detach-token
|
||||
(mf/use-fn
|
||||
(mf/deps ids)
|
||||
(fn [token attr]
|
||||
(st/emit! (dwta/unapply-token {:token (first token)
|
||||
:attributes #{attr}
|
||||
:shape-ids ids}))))
|
||||
|
||||
on-focus
|
||||
(mf/use-fn
|
||||
(fn [attr event]
|
||||
(select-margin attr)
|
||||
(dom/select-target event)))
|
||||
|
||||
on-focus-m1
|
||||
(mf/use-fn (mf/deps on-focus) #(on-focus :m1))
|
||||
|
||||
on-focus-m2
|
||||
(mf/use-fn (mf/deps on-focus) #(on-focus :m2))
|
||||
|
||||
on-focus-m3
|
||||
(mf/use-fn (mf/deps on-focus) #(on-focus :m1))
|
||||
|
||||
on-focus-m4
|
||||
(mf/use-fn (mf/deps on-focus) #(on-focus :m2))
|
||||
(fn [event]
|
||||
(let [attr (-> (dom/get-current-target event)
|
||||
(dom/get-data "name")
|
||||
(keyword))]
|
||||
(select-margin attr)
|
||||
(dom/select-target event))))
|
||||
|
||||
on-change'
|
||||
(mf/use-fn
|
||||
(mf/deps on-change ids)
|
||||
(fn [value attr]
|
||||
(if (or (string? value) (int? value))
|
||||
(on-change :multiple attr value)
|
||||
(do
|
||||
(st/emit!
|
||||
(dwta/toggle-token {:token (first value)
|
||||
:attrs #{attr}
|
||||
:shape-ids ids}))))))
|
||||
|
||||
|
||||
on-m1-change
|
||||
(mf/use-fn (mf/deps on-change') #(on-change' % :m1))
|
||||
|
||||
on-m2-change
|
||||
(mf/use-fn (mf/deps on-change') #(on-change' % :m2))
|
||||
|
||||
on-m3-change
|
||||
(mf/use-fn (mf/deps on-change') #(on-change' % :m3))
|
||||
|
||||
on-m4-change
|
||||
(mf/use-fn (mf/deps on-change') #(on-change' % :m4))]
|
||||
(mf/deps on-change)
|
||||
(fn [value event]
|
||||
(let [attr (-> (dom/get-current-target event)
|
||||
(dom/get-data "name")
|
||||
(keyword))]
|
||||
(on-change :multiple attr value))))]
|
||||
|
||||
[:div {:class (stl/css :margin-multiple)}
|
||||
(if token-numeric-inputs
|
||||
[:> numeric-input-wrapper*
|
||||
{:on-change on-m1-change
|
||||
:on-detach on-detach-token
|
||||
:on-blur on-blur
|
||||
:on-focus on-focus-m1
|
||||
:icon i/margin-top
|
||||
:class (stl/css :top-margin-wrapper)
|
||||
:min 0
|
||||
:attr :m1
|
||||
:input-type :vertical-margin
|
||||
:property "Top margin"
|
||||
:nillable true
|
||||
:applied-token applied-token-to-m1
|
||||
:value m1}]
|
||||
[:div {:class (stl/css :top-margin)
|
||||
:title "Top margin"}
|
||||
[:span {:class (stl/css :icon)}
|
||||
deprecated-icon/margin-top]
|
||||
[:> deprecated-input/numeric-input* {:class (stl/css :numeric-input)
|
||||
:placeholder "--"
|
||||
:data-name "m1"
|
||||
:on-focus on-focus
|
||||
:on-change on-change'
|
||||
:on-blur on-blur
|
||||
:nillable true
|
||||
:value m1}]]
|
||||
[:div {:class (stl/css :right-margin)
|
||||
:title "Right margin"}
|
||||
[:span {:class (stl/css :icon)}
|
||||
deprecated-icon/margin-right]
|
||||
[:> deprecated-input/numeric-input* {:class (stl/css :numeric-input)
|
||||
:placeholder "--"
|
||||
:data-name "m2"
|
||||
:on-focus on-focus
|
||||
:on-change on-change'
|
||||
:on-blur on-blur
|
||||
:nillable true
|
||||
:value m2}]]
|
||||
|
||||
[:div {:class (stl/css :top-margin)
|
||||
:title "Top margin"}
|
||||
[:span {:class (stl/css :icon)}
|
||||
deprecated-icon/margin-top]
|
||||
[:> deprecated-input/numeric-input* {:class (stl/css :numeric-input)
|
||||
:placeholder "--"
|
||||
:data-name "m1"
|
||||
:on-focus on-focus-m1
|
||||
:on-change on-m1-change
|
||||
:on-blur on-blur
|
||||
:nillable true
|
||||
:value m1}]])
|
||||
(if token-numeric-inputs
|
||||
[:> numeric-input-wrapper*
|
||||
{:on-change on-m2-change
|
||||
:on-detach on-detach-token
|
||||
:on-blur on-blur
|
||||
:on-focus on-focus-m2
|
||||
:icon i/margin-right
|
||||
:class (stl/css :right-margin-wrapper)
|
||||
:min 0
|
||||
:attr :m2
|
||||
:align :right
|
||||
:input-type :horizontal-margin
|
||||
:property "Right margin"
|
||||
:nillable true
|
||||
:applied-token applied-token-to-m2
|
||||
:value m2}]
|
||||
[:div {:class (stl/css :bottom-margin)
|
||||
:title "Bottom margin"}
|
||||
[:span {:class (stl/css :icon)}
|
||||
deprecated-icon/margin-bottom]
|
||||
[:> deprecated-input/numeric-input* {:class (stl/css :numeric-input)
|
||||
:placeholder "--"
|
||||
:data-name "m3"
|
||||
:on-focus on-focus
|
||||
:on-change on-change'
|
||||
:on-blur on-blur
|
||||
:nillable true
|
||||
:value m3}]]
|
||||
|
||||
[:div {:class (stl/css :right-margin)
|
||||
:title "Right margin"}
|
||||
[:span {:class (stl/css :icon)}
|
||||
deprecated-icon/margin-right]
|
||||
[:> deprecated-input/numeric-input* {:class (stl/css :numeric-input)
|
||||
:placeholder "--"
|
||||
:data-name "m2"
|
||||
:on-focus on-focus-m2
|
||||
:on-change on-m2-change
|
||||
:on-blur on-blur
|
||||
:nillable true
|
||||
:value m2}]])
|
||||
[:div {:class (stl/css :left-margin)
|
||||
:title "Left margin"}
|
||||
[:span {:class (stl/css :icon)}
|
||||
deprecated-icon/margin-left]
|
||||
[:> deprecated-input/numeric-input* {:class (stl/css :numeric-input)
|
||||
:placeholder "--"
|
||||
:data-name "m4"
|
||||
:on-focus on-focus
|
||||
:on-change on-change'
|
||||
:on-blur on-blur
|
||||
:nillable true
|
||||
:value m4}]]]))
|
||||
|
||||
(if token-numeric-inputs
|
||||
[:> numeric-input-wrapper*
|
||||
{:on-change on-m3-change
|
||||
:on-detach on-detach-token
|
||||
:on-blur on-blur
|
||||
:on-focus on-focus-m3
|
||||
:icon i/margin-bottom
|
||||
:class (stl/css :bottom-margin-wrapper)
|
||||
:min 0
|
||||
:attr :m3
|
||||
:align :right
|
||||
:input-type :vertical-margin
|
||||
:property "Bottom margin"
|
||||
:nillable true
|
||||
:applied-token applied-token-to-m3
|
||||
:value m3}]
|
||||
|
||||
[:div {:class (stl/css :bottom-margin)
|
||||
:title "Bottom margin"}
|
||||
[:span {:class (stl/css :icon)}
|
||||
deprecated-icon/margin-bottom]
|
||||
[:> deprecated-input/numeric-input* {:class (stl/css :numeric-input)
|
||||
:placeholder "--"
|
||||
:data-name "m3"
|
||||
:on-focus on-focus-m3
|
||||
:on-change on-m3-change
|
||||
:on-blur on-blur
|
||||
:nillable true
|
||||
:value m3}]])
|
||||
|
||||
(if token-numeric-inputs
|
||||
[:> numeric-input-wrapper*
|
||||
{:on-change on-m4-change
|
||||
:on-detach on-detach-token
|
||||
:on-blur on-blur
|
||||
:on-focus on-focus-m4
|
||||
:icon i/margin-left
|
||||
:class (stl/css :left-margin-wrapper)
|
||||
:min 0
|
||||
:attr :m4
|
||||
:property "Left margin"
|
||||
:input-type :horizontal-margin
|
||||
:nillable true
|
||||
:applied-token applied-token-to-m4
|
||||
:value m4}]
|
||||
|
||||
[:div {:class (stl/css :left-margin)
|
||||
:title "Left margin"}
|
||||
[:span {:class (stl/css :icon)}
|
||||
deprecated-icon/margin-left]
|
||||
[:> deprecated-input/numeric-input* {:class (stl/css :numeric-input)
|
||||
:placeholder "--"
|
||||
:data-name "m4"
|
||||
:on-focus on-focus-m4
|
||||
:on-change on-m4-change
|
||||
:on-blur on-blur
|
||||
:nillable true
|
||||
:value m4}]])]))
|
||||
|
||||
(mf/defc margin-section*
|
||||
{::mf/private true
|
||||
::mf/expect-props #{:value :type :on-type-change :on-change :applied-tokens :ids}}
|
||||
::mf/expect-props #{:value :type :on-type-change :on-change}}
|
||||
[{:keys [type on-type-change] :as props}]
|
||||
(let [type (d/nilv type :simple)
|
||||
on-blur (mf/use-fn #(select-margins false false false false))
|
||||
@@ -493,191 +292,8 @@
|
||||
:label "Align self end"
|
||||
:value "end"}]}])
|
||||
|
||||
(mf/defc layout-size-constraints*
|
||||
{::mf/private true
|
||||
::mf/expect-props #{:value :applied-tokens :ids :v-sizing}}
|
||||
[{:keys [values v-sizing ids applied-tokens] :as props}]
|
||||
(let [token-numeric-inputs
|
||||
(features/use-feature "tokens/numeric-input")
|
||||
|
||||
min-w (get values :layout-item-min-w)
|
||||
|
||||
max-w (get values :layout-item-max-w)
|
||||
|
||||
min-h (get values :layout-item-min-h)
|
||||
|
||||
max-h (get values :layout-item-max-h)
|
||||
|
||||
applied-token-to-min-w (get applied-tokens :layout-item-min-w)
|
||||
|
||||
applied-token-to-max-w (get applied-tokens :layout-item-max-w)
|
||||
|
||||
applied-token-to-min-h (get applied-tokens :layout-item-min-h)
|
||||
|
||||
applied-token-to-max-h (get applied-tokens :layout-item-max-h)
|
||||
|
||||
on-detach-token
|
||||
(mf/use-fn
|
||||
(mf/deps ids)
|
||||
(fn [token attr]
|
||||
(st/emit! (dwta/unapply-token {:token (first token)
|
||||
:attributes #{attr}
|
||||
:shape-ids ids}))))
|
||||
|
||||
on-size-change
|
||||
(mf/use-fn
|
||||
(mf/deps ids)
|
||||
(fn [value attr]
|
||||
(if (or (string? value) (int? value))
|
||||
(st/emit! (dwsl/update-layout-child ids {attr value}))
|
||||
(do
|
||||
(st/emit!
|
||||
(dwta/toggle-token {:token (first value)
|
||||
:attrs #{attr}
|
||||
:shape-ids ids}))))))
|
||||
|
||||
on-layout-item-min-w-change
|
||||
(mf/use-fn (mf/deps on-size-change) #(on-size-change % :layout-item-min-w))
|
||||
|
||||
on-layout-item-max-w-change
|
||||
(mf/use-fn (mf/deps on-size-change) #(on-size-change % :layout-item-max-w))
|
||||
|
||||
on-layout-item-min-h-change
|
||||
(mf/use-fn (mf/deps on-size-change) #(on-size-change % :layout-item-min-h))
|
||||
|
||||
on-layout-item-max-h-change
|
||||
(mf/use-fn (mf/deps on-size-change) #(on-size-change % :layout-item-max-h))]
|
||||
|
||||
[:div {:class (stl/css :advanced-options)}
|
||||
(when (= (:layout-item-h-sizing values) :fill)
|
||||
[:div {:class (stl/css :horizontal-fill)}
|
||||
(if token-numeric-inputs
|
||||
[:> numeric-input-wrapper*
|
||||
{:on-change on-layout-item-min-w-change
|
||||
:on-detach on-detach-token
|
||||
:class (stl/css :min-w-wrapper)
|
||||
:min 0
|
||||
:attr :layout-item-min-w
|
||||
:property (tr "workspace.options.layout-item.layout-item-min-w")
|
||||
:text-icon "MIN W"
|
||||
:input-type :min-width
|
||||
:nillable true
|
||||
:applied-token applied-token-to-min-w
|
||||
:tooltip-class (stl/css :tooltip-wrapper)
|
||||
:value min-w}]
|
||||
|
||||
[:div {:class (stl/css :layout-item-min-w)
|
||||
:title (tr "workspace.options.layout-item.layout-item-min-w")}
|
||||
|
||||
[:span {:class (stl/css :icon-text)} "MIN W"]
|
||||
[:> deprecated-input/numeric-input*
|
||||
{:class (stl/css :numeric-input)
|
||||
:no-validate true
|
||||
:min 0
|
||||
:data-wrap true
|
||||
:placeholder "--"
|
||||
:data-attr "layout-item-min-w"
|
||||
:on-focus dom/select-target
|
||||
:on-change on-layout-item-min-w-change
|
||||
:value (get values :layout-item-min-w)
|
||||
:nillable true}]])
|
||||
|
||||
(if token-numeric-inputs
|
||||
[:> numeric-input-wrapper*
|
||||
{:on-change on-layout-item-max-w-change
|
||||
:on-detach on-detach-token
|
||||
:text-icon "MAX W"
|
||||
:class (stl/css :max-w-wrapper)
|
||||
:min 0
|
||||
:input-type :max-width
|
||||
:attr :layout-item-max-w
|
||||
:property (tr "workspace.options.layout-item.layout-item-max-w")
|
||||
:nillable true
|
||||
:tooltip-class (stl/css :tooltip-wrapper)
|
||||
:applied-token applied-token-to-max-w
|
||||
:value max-w}]
|
||||
|
||||
[:div {:class (stl/css :layout-item-max-w)
|
||||
:title (tr "workspace.options.layout-item.layout-item-max-w")}
|
||||
[:span {:class (stl/css :icon-text)} "MAX W"]
|
||||
[:> deprecated-input/numeric-input*
|
||||
{:class (stl/css :numeric-input)
|
||||
:no-validate true
|
||||
:min 0
|
||||
:data-wrap true
|
||||
:placeholder "--"
|
||||
:data-attr "layout-item-max-w"
|
||||
:on-focus dom/select-target
|
||||
:on-change on-layout-item-max-w-change
|
||||
:value (get values :layout-item-max-w)
|
||||
:nillable true}]])])
|
||||
|
||||
(when (= v-sizing :fill)
|
||||
[:div {:class (stl/css :vertical-fill)}
|
||||
(if token-numeric-inputs
|
||||
[:> numeric-input-wrapper*
|
||||
{:on-change on-layout-item-min-h-change
|
||||
:on-detach on-detach-token
|
||||
:text-icon "MIN H"
|
||||
:input-type :max-height
|
||||
:class (stl/css :min-h-wrapper)
|
||||
:min 0
|
||||
:attr :layout-item-min-h
|
||||
:property (tr "workspace.options.layout-item.layout-item-min-h")
|
||||
:nillable true
|
||||
:tooltip-class (stl/css :tooltip-wrapper)
|
||||
|
||||
:applied-token applied-token-to-min-h
|
||||
:value min-h}]
|
||||
|
||||
[:div {:class (stl/css :layout-item-min-h)
|
||||
:title (tr "workspace.options.layout-item.layout-item-min-h")}
|
||||
[:span {:class (stl/css :icon-text)} "MIN H"]
|
||||
[:> deprecated-input/numeric-input*
|
||||
{:class (stl/css :numeric-input)
|
||||
:no-validate true
|
||||
:min 0
|
||||
:data-wrap true
|
||||
:placeholder "--"
|
||||
:data-attr "layout-item-min-h"
|
||||
:on-focus dom/select-target
|
||||
:on-change on-layout-item-min-h-change
|
||||
:value (get values :layout-item-min-h)
|
||||
:nillable true}]])
|
||||
|
||||
(if token-numeric-inputs
|
||||
[:> numeric-input-wrapper*
|
||||
{:on-change on-layout-item-max-h-change
|
||||
:on-detach on-detach-token
|
||||
:class (stl/css :max-h-wrapper)
|
||||
:min 0
|
||||
:text-icon "MAX H"
|
||||
:input-type :max-height
|
||||
:attr :layout-item-max-h
|
||||
:property (tr "workspace.options.layout-item.layout-item-max-h")
|
||||
:nillable true
|
||||
:tooltip-class (stl/css :tooltip-wrapper)
|
||||
:applied-token applied-token-to-max-h
|
||||
:value max-h}]
|
||||
|
||||
[:div {:class (stl/css :layout-item-max-h)
|
||||
:title (tr "workspace.options.layout-item.layout-item-max-h")}
|
||||
|
||||
[:span {:class (stl/css :icon-text)} "MAX H"]
|
||||
[:> deprecated-input/numeric-input*
|
||||
{:class (stl/css :numeric-input)
|
||||
:no-validate true
|
||||
:min 0
|
||||
:data-wrap true
|
||||
:placeholder "--"
|
||||
:data-attr "layout-item-max-h"
|
||||
:on-focus dom/select-target
|
||||
:on-change on-layout-item-max-h-change
|
||||
:value (get values :layout-item-max-h)
|
||||
:nillable true}]])])]))
|
||||
|
||||
(mf/defc layout-item-menu
|
||||
{::mf/memo #{:ids :values :type :is-layout-child? :is-grid-parent :is-flex-parent? :is-grid-layout? :is-flex-layout? :applied-tokens}
|
||||
{::mf/memo #{:ids :values :type :is-layout-child? :is-grid-parent :is-flex-parent? :is-grid-layout? :is-flex-layout?}
|
||||
::mf/props :obj}
|
||||
[{:keys [ids values
|
||||
^boolean is-layout-child?
|
||||
@@ -685,8 +301,7 @@
|
||||
^boolean is-grid-parent?
|
||||
^boolean is-flex-parent?
|
||||
^boolean is-flex-layout?
|
||||
^boolean is-grid-layout?
|
||||
applied-tokens]}]
|
||||
^boolean is-grid-layout?]}]
|
||||
|
||||
(let [selection-parents* (mf/use-memo (mf/deps ids) #(refs/parents-by-ids ids))
|
||||
selection-parents (mf/deref selection-parents*)
|
||||
@@ -782,7 +397,16 @@
|
||||
(fn [value]
|
||||
(st/emit! (dwsl/update-layout-child ids {:layout-item-v-sizing (keyword value)}))))
|
||||
|
||||
;; Position
|
||||
;; Size and position
|
||||
on-size-change
|
||||
(mf/use-fn
|
||||
(mf/deps ids)
|
||||
(fn [value event]
|
||||
(let [attr (-> (dom/get-current-target event)
|
||||
(dom/get-data "attr")
|
||||
(keyword))]
|
||||
(st/emit! (dwsl/update-layout-child ids {attr value})))))
|
||||
|
||||
on-change-position
|
||||
(mf/use-fn
|
||||
(mf/deps ids)
|
||||
@@ -799,8 +423,7 @@
|
||||
(fn [value]
|
||||
(st/emit! (dwsl/update-layout-child ids {:layout-item-z-index value}))))]
|
||||
|
||||
[:section {:class (stl/css :element-set)
|
||||
:aria-label "layout item menu"}
|
||||
[:div {:class (stl/css :element-set)}
|
||||
[:div {:class (stl/css :element-title)}
|
||||
[:> title-bar* {:collapsable has-content?
|
||||
:collapsed (not open?)
|
||||
@@ -860,13 +483,74 @@
|
||||
[:> margin-section* {:value (:layout-item-margin values)
|
||||
:type (:layout-item-margin-type values)
|
||||
:on-type-change on-margin-type-change
|
||||
:applied-tokens applied-tokens
|
||||
:ids ids
|
||||
:on-change on-margin-change}])
|
||||
|
||||
(when (or (= h-sizing :fill)
|
||||
(= v-sizing :fill))
|
||||
[:> layout-size-constraints* {:ids ids
|
||||
:values values
|
||||
:applied-tokens applied-tokens
|
||||
:v-sizing v-sizing}])])]))
|
||||
[:div {:class (stl/css :advanced-options)}
|
||||
(when (= (:layout-item-h-sizing values) :fill)
|
||||
[:div {:class (stl/css :horizontal-fill)}
|
||||
[:div {:class (stl/css :layout-item-min-w)
|
||||
:title (tr "workspace.options.layout-item.layout-item-min-w")}
|
||||
|
||||
[:span {:class (stl/css :icon-text)} "MIN W"]
|
||||
[:> deprecated-input/numeric-input*
|
||||
{:class (stl/css :numeric-input)
|
||||
:no-validate true
|
||||
:min 0
|
||||
:data-wrap true
|
||||
:placeholder "--"
|
||||
:data-attr "layout-item-min-w"
|
||||
:on-focus dom/select-target
|
||||
:on-change on-size-change
|
||||
:value (get values :layout-item-min-w)
|
||||
:nillable true}]]
|
||||
|
||||
[:div {:class (stl/css :layout-item-max-w)
|
||||
:title (tr "workspace.options.layout-item.layout-item-max-w")}
|
||||
[:span {:class (stl/css :icon-text)} "MAX W"]
|
||||
[:> deprecated-input/numeric-input*
|
||||
{:class (stl/css :numeric-input)
|
||||
:no-validate true
|
||||
:min 0
|
||||
:data-wrap true
|
||||
:placeholder "--"
|
||||
:data-attr "layout-item-max-w"
|
||||
:on-focus dom/select-target
|
||||
:on-change on-size-change
|
||||
:value (get values :layout-item-max-w)
|
||||
:nillable true}]]])
|
||||
|
||||
(when (= v-sizing :fill)
|
||||
[:div {:class (stl/css :vertical-fill)}
|
||||
[:div {:class (stl/css :layout-item-min-h)
|
||||
:title (tr "workspace.options.layout-item.layout-item-min-h")}
|
||||
|
||||
[:span {:class (stl/css :icon-text)} "MIN H"]
|
||||
[:> deprecated-input/numeric-input*
|
||||
{:class (stl/css :numeric-input)
|
||||
:no-validate true
|
||||
:min 0
|
||||
:data-wrap true
|
||||
:placeholder "--"
|
||||
:data-attr "layout-item-min-h"
|
||||
:on-focus dom/select-target
|
||||
:on-change on-size-change
|
||||
:value (get values :layout-item-min-h)
|
||||
:nillable true}]]
|
||||
|
||||
[:div {:class (stl/css :layout-item-max-h)
|
||||
:title (tr "workspace.options.layout-item.layout-item-max-h")}
|
||||
|
||||
[:span {:class (stl/css :icon-text)} "MAX H"]
|
||||
[:> deprecated-input/numeric-input*
|
||||
{:class (stl/css :numeric-input)
|
||||
:no-validate true
|
||||
:min 0
|
||||
:data-wrap true
|
||||
:placeholder "--"
|
||||
:data-attr "layout-item-max-h"
|
||||
:on-focus dom/select-target
|
||||
:on-change on-size-change
|
||||
:value (get values :layout-item-max-h)
|
||||
:nillable true}]]])])])]))
|
||||
|
||||
@@ -6,21 +6,17 @@
|
||||
|
||||
@use "refactor/common-refactor.scss" as deprecated;
|
||||
@use "../../../sidebar/common/sidebar.scss" as sidebar;
|
||||
@use "ds/_borders.scss" as *;
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@use "ds/typography.scss" as *;
|
||||
@use "ds/_utils.scss" as *;
|
||||
|
||||
.element-set {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.title-spacing-layout-element {
|
||||
margin: 0 0 var(--sp-xs) 0;
|
||||
margin: 0 0 deprecated.$s-4 0;
|
||||
}
|
||||
|
||||
.title-spacing-empty {
|
||||
padding-inline-start: var(--sp-xxs);
|
||||
padding-left: deprecated.$s-2;
|
||||
}
|
||||
|
||||
.flex-element-menu {
|
||||
@@ -39,8 +35,8 @@
|
||||
}
|
||||
|
||||
.z-index-wrapper {
|
||||
@include use-typography("body-small");
|
||||
@extend .input-element;
|
||||
@include deprecated.bodySmallTypography;
|
||||
grid-column: 6 / span 3;
|
||||
}
|
||||
|
||||
@@ -59,7 +55,7 @@
|
||||
}
|
||||
|
||||
.position-options {
|
||||
inline-size: 100%;
|
||||
width: 100%;
|
||||
grid-column: 1 / span 5;
|
||||
}
|
||||
|
||||
@@ -79,7 +75,7 @@
|
||||
.vertical-margin,
|
||||
.horizontal-margin {
|
||||
@extend .input-element;
|
||||
@include use-typography("body-small");
|
||||
@include deprecated.bodySmallTypography;
|
||||
}
|
||||
.vertical-margin {
|
||||
grid-column: 1;
|
||||
@@ -89,16 +85,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.vertical-margin-wrapper {
|
||||
grid-column: 1;
|
||||
--dropdown-width: var(--7-columns-dropdown-width);
|
||||
}
|
||||
|
||||
.horizontal-margin-wrapper {
|
||||
grid-column: 2;
|
||||
--dropdown-width: var(--7-columns-dropdown-width);
|
||||
}
|
||||
|
||||
.margin-multiple {
|
||||
display: grid;
|
||||
grid-template-columns: subgrid;
|
||||
@@ -110,33 +96,25 @@
|
||||
.left-margin,
|
||||
.right-margin {
|
||||
@extend .input-element;
|
||||
@include use-typography("body-small");
|
||||
@include deprecated.bodySmallTypography;
|
||||
}
|
||||
|
||||
.top-margin,
|
||||
.top-margin-wrapper {
|
||||
--dropdown-width: var(--7-columns-dropdown-width);
|
||||
.top-margin {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.bottom-margin,
|
||||
.bottom-margin-wrapper {
|
||||
--dropdown-width: var(--7-columns-dropdown-width);
|
||||
.bottom-margin {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.left-margin,
|
||||
.left-margin-wrapper {
|
||||
--dropdown-width: var(--7-columns-dropdown-width);
|
||||
.left-margin {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.right-margin,
|
||||
.right-margin-wrapper {
|
||||
--dropdown-width: var(--7-columns-dropdown-width);
|
||||
.right-margin {
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
}
|
||||
@@ -162,18 +140,14 @@
|
||||
.layout-item-max-w,
|
||||
.layout-item-max-h {
|
||||
@extend .input-element;
|
||||
@include use-typography("body-small");
|
||||
@include deprecated.bodySmallTypography;
|
||||
.icon-text {
|
||||
justify-content: flex-start;
|
||||
inline-size: px2rem(80);
|
||||
padding-block-start: var(--sp-xxs);
|
||||
width: deprecated.$s-80;
|
||||
padding-top: deprecated.$s-2;
|
||||
}
|
||||
}
|
||||
|
||||
.inputs-wrapper {
|
||||
grid-column: 1 / span 2;
|
||||
}
|
||||
|
||||
.tooltip-wrapper {
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.logic.shapes :as cls]
|
||||
[app.common.types.shape.layout :as ctl]
|
||||
[app.common.types.token :as tk]
|
||||
[app.main.constants :refer [size-presets]]
|
||||
[app.main.data.workspace :as udw]
|
||||
[app.main.data.workspace.interactions :as dwi]
|
||||
@@ -25,12 +26,13 @@
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[app.main.ui.components.numeric-input :as deprecated-input]
|
||||
[app.main.ui.context :as muc]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.controls.numeric-input :refer [numeric-input*]]
|
||||
[app.main.ui.ds.controls.radio-buttons :refer [radio-buttons*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.main.ui.workspace.sidebar.options.menus.border-radius :refer [border-radius-menu*]]
|
||||
[app.main.ui.workspace.sidebar.options.menus.input-wrapper-tokens :refer [numeric-input-wrapper*]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[clojure.set :as set]
|
||||
@@ -89,6 +91,32 @@
|
||||
shape)]
|
||||
(select-keys shape measure-attrs)))
|
||||
|
||||
(mf/defc numeric-input-wrapper*
|
||||
{::mf/private true}
|
||||
[{:keys [values name applied-tokens align on-detach] :rest props}]
|
||||
(let [tokens (mf/use-ctx muc/active-tokens-by-type)
|
||||
tokens (mf/with-memo [tokens name]
|
||||
(delay
|
||||
(-> (deref tokens)
|
||||
(select-keys (get tk/tokens-by-input name))
|
||||
(not-empty))))
|
||||
on-detach-attr
|
||||
(mf/use-fn
|
||||
(mf/deps on-detach name)
|
||||
#(on-detach % name))
|
||||
|
||||
props (mf/spread-props props
|
||||
{:placeholder (if (or (= :multiple (:applied-tokens values))
|
||||
(= :multiple (get values name)))
|
||||
(tr "settings.multiple") "--")
|
||||
:class (stl/css :numeric-input-measures)
|
||||
:applied-token (get applied-tokens name)
|
||||
:tokens (if (delay? tokens) @tokens tokens)
|
||||
:align align
|
||||
:on-detach on-detach-attr
|
||||
:value (get values name)})]
|
||||
[:> numeric-input* props]))
|
||||
|
||||
(def ^:private xf:map-type (map :type))
|
||||
(def ^:private xf:mapcat-type-to-options (mapcat type->options))
|
||||
|
||||
@@ -416,13 +444,10 @@
|
||||
:on-detach on-detach-token
|
||||
:icon i/character-w
|
||||
:min 0.01
|
||||
:attr :width
|
||||
:name :width
|
||||
:property (tr "workspace.options.width")
|
||||
:applied-token (get applied-tokens :width)
|
||||
:placeholder (if (or (= :multiple (:applied-tokens values))
|
||||
(= :multiple (get values :width)))
|
||||
(tr "settings.multiple") "--")
|
||||
:value (get values :width)}]
|
||||
:applied-tokens applied-tokens
|
||||
:values values}]
|
||||
|
||||
[:> numeric-input-wrapper*
|
||||
{:disabled disabled-height-sizing?
|
||||
@@ -430,11 +455,11 @@
|
||||
:on-detach on-detach-token
|
||||
:min 0.01
|
||||
:icon i/character-h
|
||||
:attr :height
|
||||
:name :height
|
||||
:align :right
|
||||
:property (tr "workspace.options.height")
|
||||
:applied-token (get applied-tokens :height)
|
||||
:value (get values :height)}]]
|
||||
:applied-tokens applied-tokens
|
||||
:values values}]]
|
||||
|
||||
[:*
|
||||
[:div {:class (stl/css-case :width true
|
||||
@@ -478,26 +503,20 @@
|
||||
:on-change on-pos-x-change
|
||||
:on-detach on-detach-token
|
||||
:icon i/character-x
|
||||
:attr :x
|
||||
:name :x
|
||||
:property (tr "workspace.options.x")
|
||||
:applied-token (get applied-tokens :x)
|
||||
:placeholder (if (or (= :multiple (:applied-tokens values))
|
||||
(= :multiple (get values :x)))
|
||||
(tr "settings.multiple") "--")
|
||||
:value (get values :x)}]
|
||||
:applied-tokens applied-tokens
|
||||
:values values}]
|
||||
[:> numeric-input-wrapper*
|
||||
{:disabled disabled-position?
|
||||
:on-change on-pos-y-change
|
||||
:on-detach on-detach-token
|
||||
:icon i/character-y
|
||||
:attr :y
|
||||
:name :y
|
||||
:align :right
|
||||
:property (tr "workspace.options.y")
|
||||
:applied-token (get applied-tokens :y)
|
||||
:placeholder (if (or (= :multiple (:applied-tokens values))
|
||||
(= :multiple (get values :y)))
|
||||
(tr "settings.multiple") "--")
|
||||
:value (get values :y)}]]
|
||||
:applied-tokens applied-tokens
|
||||
:values values}]]
|
||||
|
||||
[:*
|
||||
[:div {:class (stl/css-case :x-position true
|
||||
@@ -532,13 +551,10 @@
|
||||
:icon i/rotation
|
||||
:min -359
|
||||
:max 359
|
||||
:attr :rotation
|
||||
:name :rotation
|
||||
:property (tr "workspace.options.rotation")
|
||||
:applied-token (get applied-tokens :rotation)
|
||||
:placeholder (if (or (= :multiple (:applied-tokens values))
|
||||
(= :multiple (get values :rotation)))
|
||||
(tr "settings.multiple") "--")
|
||||
:value (get values :rotation)}]
|
||||
:applied-tokens applied-tokens
|
||||
:values values}]
|
||||
|
||||
[:div {:class (stl/css :rotation)
|
||||
:title (tr "workspace.options.rotation")}
|
||||
|
||||
@@ -9,20 +9,50 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.types.color :as ctc]
|
||||
[app.common.types.token :as tk]
|
||||
[app.main.data.workspace.tokens.application :as dwta]
|
||||
[app.main.features :as features]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.numeric-input :as deprecated-input]
|
||||
[app.main.ui.components.reorder-handler :refer [reorder-handler*]]
|
||||
[app.main.ui.components.select :refer [select]]
|
||||
[app.main.ui.context :as muc]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.controls.numeric-input :refer [numeric-input*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
|
||||
[app.main.ui.hooks :as h]
|
||||
[app.main.ui.workspace.sidebar.options.menus.input-wrapper-tokens :refer [numeric-input-wrapper*]]
|
||||
[app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row*]]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc numeric-input-wrapper*
|
||||
{::mf/private true}
|
||||
[{:keys [values name applied-tokens align on-detach] :rest props}]
|
||||
(let [tokens (mf/use-ctx muc/active-tokens-by-type)
|
||||
tokens (mf/with-memo [tokens name]
|
||||
(delay
|
||||
(-> (deref tokens)
|
||||
(select-keys (get tk/tokens-by-input name))
|
||||
(not-empty))))
|
||||
|
||||
on-detach-attr (mf/use-fn
|
||||
(mf/deps on-detach name)
|
||||
#(on-detach % name))
|
||||
|
||||
applied-token (get applied-tokens name)
|
||||
|
||||
props (mf/spread-props props
|
||||
{:placeholder (if (= :multiple values)
|
||||
(tr "settings.multiple")
|
||||
"--")
|
||||
:applied-token applied-token
|
||||
:tokens (if (delay? tokens) @tokens tokens)
|
||||
:align align
|
||||
:on-detach on-detach-attr
|
||||
:name name
|
||||
:value values})]
|
||||
[:> numeric-input* props]))
|
||||
|
||||
(mf/defc stroke-row*
|
||||
[{:keys [index
|
||||
stroke
|
||||
@@ -220,11 +250,11 @@
|
||||
:min 0
|
||||
:on-focus on-focus
|
||||
:on-blur on-blur
|
||||
:attr :stroke-width
|
||||
:name :stroke-width
|
||||
:class (stl/css :numeric-input-wrapper)
|
||||
:property (tr "workspace.options.stroke-width")
|
||||
:applied-token (get applied-tokens :stroke-width)
|
||||
:value stroke-width}]
|
||||
:applied-tokens applied-tokens
|
||||
:values stroke-width}]
|
||||
|
||||
[:div {:class (stl/css :stroke-width-input)
|
||||
:title (tr "workspace.options.stroke-width")}
|
||||
|
||||
@@ -114,7 +114,6 @@
|
||||
:is-layout-child? true
|
||||
:is-flex-parent? is-flex-parent?
|
||||
:is-grid-parent? is-grid-parent?
|
||||
:applied-tokens applied-tokens
|
||||
:shape shape}])
|
||||
|
||||
(when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?)
|
||||
|
||||
@@ -113,7 +113,6 @@
|
||||
:is-layout-container? false
|
||||
:is-flex-parent? is-flex-parent?
|
||||
:is-grid-parent? is-grid-parent?
|
||||
:applied-tokens applied-tokens
|
||||
:shape shape}])
|
||||
|
||||
(when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?)
|
||||
|
||||
@@ -135,7 +135,6 @@
|
||||
:is-flex-layout? is-flex-layout?
|
||||
:is-grid-layout? is-grid-layout?
|
||||
:is-layout-child? is-layout-child?
|
||||
:applied-tokens applied-tokens
|
||||
:is-layout-container? is-layout-container?
|
||||
:shape shape}])
|
||||
|
||||
|
||||
@@ -139,7 +139,6 @@
|
||||
:is-layout-container? false
|
||||
:is-flex-parent? is-flex-parent?
|
||||
:is-grid-parent? is-grid-parent?
|
||||
:applied-tokens applied-tokens
|
||||
:values layout-item-values}])
|
||||
|
||||
(when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?)
|
||||
|
||||
@@ -409,7 +409,7 @@
|
||||
[layout-container-ids layout-container-values layout-container-tokens]
|
||||
(get-attrs shapes objects :layout-container)
|
||||
|
||||
[layout-item-ids layout-item-values layout-item-tokens]
|
||||
[layout-item-ids layout-item-values {}]
|
||||
(get-attrs shapes objects :layout-item)
|
||||
|
||||
components
|
||||
@@ -471,7 +471,6 @@
|
||||
:is-layout-container? all-flex-layout-container?
|
||||
:is-flex-parent? is-flex-parent?
|
||||
:is-grid-parent? is-grid-parent?
|
||||
:applied-tokens layout-item-tokens
|
||||
:values layout-item-values}])
|
||||
|
||||
(when-not (or (empty? constraint-ids) ^boolean is-layout-child?)
|
||||
|
||||
@@ -113,7 +113,6 @@
|
||||
:is-layout-container? false
|
||||
:is-flex-parent? is-flex-parent?
|
||||
:is-grid-parent? is-grid-parent?
|
||||
:applied-tokens applied-tokens
|
||||
:shape shape}])
|
||||
|
||||
(when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?)
|
||||
|
||||
@@ -112,7 +112,6 @@
|
||||
:values layout-item-values
|
||||
:is-layout-child? true
|
||||
:is-flex-parent? is-flex-parent?
|
||||
:applied-tokens applied-tokens
|
||||
:is-grid-parent? is-grid-parent?
|
||||
:shape shape}])
|
||||
|
||||
|
||||
@@ -180,7 +180,6 @@
|
||||
:is-layout-child? true
|
||||
:is-flex-parent? is-flex-parent?
|
||||
:is-grid-parent? is-grid-parent?
|
||||
:applied-tokens applied-tokens
|
||||
:shape shape}])
|
||||
|
||||
(when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?)
|
||||
|
||||
@@ -154,7 +154,6 @@
|
||||
:is-layout-child? true
|
||||
:is-flex-parent? is-flex-parent?
|
||||
:is-grid-parent? is-grid-parent?
|
||||
:applied-tokens applied-tokens
|
||||
:shape shape}])
|
||||
|
||||
(when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?)
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
{::mf/private true}
|
||||
[{:keys [tokens-lib selected-token-set-id]}]
|
||||
(let [selected-token-set
|
||||
(mf/with-memo [tokens-lib]
|
||||
(mf/with-memo [tokens-lib selected-token-set-id]
|
||||
(when selected-token-set-id
|
||||
(some-> tokens-lib (ctob/get-set selected-token-set-id))))
|
||||
|
||||
@@ -62,18 +62,20 @@
|
||||
[:div {:class (stl/css :sets-header-container)}
|
||||
[:> text* {:as "span"
|
||||
:typography "headline-small"
|
||||
:class (stl/css :sets-header)}
|
||||
:class (stl/css :sets-header)
|
||||
:data-testid "active-token-set-title"}
|
||||
(tr "workspace.tokens.tokens-section-title" (ctob/get-name selected-token-set))]
|
||||
[:div {:class (stl/css :sets-header-status) :title (tr "workspace.tokens.inactive-set-description")}
|
||||
(when (and (some? selected-token-set-id)
|
||||
(not (token-set-active? (ctob/get-name selected-token-set))))
|
||||
[:div {:class (stl/css :sets-header-status) :title (tr "workspace.tokens.inactive-set-description")}
|
||||
;; NOTE: when no set in tokens-lib, the selected-token-set-id
|
||||
;; will be `nil`, so for properly hide the inactive message we
|
||||
;; check that at least `selected-token-set-id` has a value
|
||||
(when (and (some? selected-token-set-id)
|
||||
(not (token-set-active? (ctob/get-name selected-token-set))))
|
||||
|
||||
[:*
|
||||
[:> icon* {:class (stl/css :sets-header-status-icon) :icon-id i/eye-off}]
|
||||
[:> text* {:as "span" :typography "body-small" :class (stl/css :sets-header-status-text)}
|
||||
(tr "workspace.tokens.inactive-set")]])]]))
|
||||
(tr "workspace.tokens.inactive-set")]]])]))
|
||||
|
||||
(mf/defc tokens-section*
|
||||
{::mf/private true}
|
||||
@@ -158,7 +160,7 @@
|
||||
[:& token-context-menu]
|
||||
[:> token-node-context-menu* {:on-delete-node delete-node}]
|
||||
|
||||
[:& selected-set-info* {:tokens-lib tokens-lib
|
||||
[:> selected-set-info* {:tokens-lib tokens-lib
|
||||
:selected-token-set-id selected-token-set-id}]
|
||||
|
||||
(for [type filled-group]
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
[app.main.ui.forms :as fc]
|
||||
[app.main.ui.workspace.tokens.management.forms.controls :as token.controls]
|
||||
[app.main.ui.workspace.tokens.management.forms.validators :refer [default-validate-token]]
|
||||
[app.main.ui.workspace.tokens.remapping-modal :as remapping-modal]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.forms :as fm]
|
||||
[app.util.i18n :refer [tr]]
|
||||
@@ -182,9 +181,33 @@
|
||||
(when (or (k/enter? e) (k/space? e))
|
||||
(on-cancel e))))
|
||||
|
||||
on-remap-token
|
||||
(mf/use-fn
|
||||
(mf/deps token)
|
||||
(fn [valid-token name old-name description]
|
||||
(st/emit!
|
||||
(dwtl/update-token (:id token)
|
||||
{:name name
|
||||
:value (:value valid-token)
|
||||
:description description})
|
||||
(remap/remap-tokens old-name name)
|
||||
(dwtp/propagate-workspace-tokens)
|
||||
(modal/hide!))))
|
||||
|
||||
on-rename-token
|
||||
(mf/use-fn
|
||||
(mf/deps token)
|
||||
(fn [valid-token name description]
|
||||
(st/emit!
|
||||
(dwtl/update-token (:id token)
|
||||
{:name name
|
||||
:value (:value valid-token)
|
||||
:description description})
|
||||
(modal/hide!))))
|
||||
|
||||
on-submit
|
||||
(mf/use-fn
|
||||
(mf/deps validate-token token tokens token-type value-subfield type active-tab)
|
||||
(mf/deps validate-token token tokens token-type value-subfield type active-tab on-remap-token on-rename-token is-create)
|
||||
(fn [form _event]
|
||||
(let [name (get-in @form [:clean-data :name])
|
||||
path (str (d/name token-type) "." name)
|
||||
@@ -202,22 +225,15 @@
|
||||
file-data (dh/lookup-file-data state)
|
||||
old-name (:name token)
|
||||
is-rename (and (= action "edit") (not= name old-name))
|
||||
references-count (remap/count-token-references file-data old-name)]
|
||||
references-count (remap/count-token-references file-data old-name)
|
||||
on-remap #(on-remap-token valid-token name old-name description)
|
||||
on-rename #(on-rename-token valid-token name description)]
|
||||
(if (and is-rename (> references-count 0))
|
||||
(remapping-modal/show-remapping-modal
|
||||
{:old-token-name old-name
|
||||
:new-token-name name
|
||||
:references-count references-count
|
||||
:on-confirm (fn []
|
||||
(st/emit!
|
||||
(dwtl/update-token (:id token)
|
||||
{:name name
|
||||
:value (:value valid-token)
|
||||
:description description})
|
||||
(remap/remap-tokens old-name name)
|
||||
(dwtp/propagate-workspace-tokens)
|
||||
(modal/hide!)))
|
||||
:on-cancel #(modal/hide!)})
|
||||
(st/emit! (modal/show :tokens/remapping-confirmation {:old-token-name old-name
|
||||
:new-token-name name
|
||||
:references-count references-count
|
||||
:on-remap on-remap
|
||||
:on-rename on-rename}))
|
||||
(st/emit!
|
||||
(if is-create
|
||||
(dwtl/create-token (ctob/make-token {:name name
|
||||
|
||||
@@ -295,7 +295,8 @@
|
||||
errors?
|
||||
[:> icon*
|
||||
{:icon-id i/broken-link
|
||||
:class (stl/css :token-pill-icon)}]
|
||||
:class (stl/css :token-pill-icon)
|
||||
:aria-label (tr "workspace.tokens.missing-reference")}]
|
||||
|
||||
color
|
||||
[:> swatch* {:background color
|
||||
|
||||
@@ -11,22 +11,15 @@
|
||||
[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]
|
||||
[app.main.ui.ds.foundations.typography :as t]
|
||||
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
|
||||
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
|
||||
[app.util.dom :as dom]
|
||||
[app.main.ui.ds.foundations.typography.text :refer [text*]]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn show-remapping-modal
|
||||
"Show the token remapping confirmation modal"
|
||||
[{:keys [old-token-name new-token-name references-count on-confirm on-cancel]}]
|
||||
(let [props {:old-token-name old-token-name
|
||||
:new-token-name new-token-name
|
||||
:references-count references-count
|
||||
:on-confirm on-confirm
|
||||
:on-cancel on-cancel}]
|
||||
(st/emit! (modal/show :tokens/remapping-confirmation props))))
|
||||
|
||||
(defn hide-remapping-modal
|
||||
"Hide the token remapping confirmation modal"
|
||||
[]
|
||||
@@ -34,73 +27,73 @@
|
||||
|
||||
;; Remapping Modal Component
|
||||
(mf/defc token-remapping-modal
|
||||
{::mf/wrap-props false
|
||||
::mf/register modal/components
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :tokens/remapping-confirmation}
|
||||
[{:keys [old-token-name new-token-name references-count on-confirm on-cancel]}]
|
||||
(let [remapping-in-progress* (mf/use-state false)
|
||||
remapping-in-progress? (deref remapping-in-progress*)
|
||||
[{:keys [old-token-name new-token-name on-remap on-rename]}]
|
||||
(let [remap-modal (get @st/state :remap-modal)
|
||||
|
||||
;; Remap logic on confirm
|
||||
on-confirm-remap
|
||||
confirm-remap
|
||||
(mf/use-fn
|
||||
(mf/deps on-confirm remapping-in-progress*)
|
||||
(fn [e]
|
||||
(dom/prevent-default e)
|
||||
(dom/stop-propagation e)
|
||||
(reset! remapping-in-progress* true)
|
||||
(mf/deps on-remap remap-modal)
|
||||
(fn []
|
||||
;; Call shared remapping logic
|
||||
(let [state @st/state
|
||||
remap-modal (:remap-modal state)
|
||||
old-token-name (:old-token-name remap-modal)
|
||||
(let [old-token-name (:old-token-name remap-modal)
|
||||
new-token-name (:new-token-name remap-modal)]
|
||||
(st/emit! [:tokens/remap-tokens old-token-name new-token-name]))
|
||||
(when (fn? on-confirm)
|
||||
(on-confirm))))
|
||||
(when (fn? on-remap)
|
||||
(on-remap))))
|
||||
|
||||
on-cancel-remap
|
||||
rename-token
|
||||
(mf/use-fn
|
||||
(mf/deps on-cancel)
|
||||
(fn [e]
|
||||
(dom/prevent-default e)
|
||||
(dom/stop-propagation e)
|
||||
(modal/hide!)
|
||||
(when (fn? on-cancel)
|
||||
(on-cancel))))]
|
||||
(mf/deps on-rename)
|
||||
(fn []
|
||||
(when (fn? on-rename)
|
||||
(on-rename))))
|
||||
|
||||
cancel-action
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(hide-remapping-modal)))
|
||||
|
||||
;; Close modal on Escape key if not in progress
|
||||
on-key-down
|
||||
(mf/use-fn
|
||||
(mf/deps cancel-action)
|
||||
(fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(cancel-action))))]
|
||||
|
||||
[:div {:class (stl/css :modal-overlay)
|
||||
:on-key-down on-key-down
|
||||
:role "alertdialog"
|
||||
:aria-modal "true"
|
||||
:aria-labelledby "modal-title"}
|
||||
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div {:class (stl/css :modal-dialog)
|
||||
:data-testid "token-remapping-modal"}
|
||||
[:> icon-button* {:on-click cancel-action
|
||||
:class (stl/css :close-btn)
|
||||
:icon i/close
|
||||
:variant "action"
|
||||
:aria-label (tr "labels.close")}]
|
||||
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:> heading* {:level 2
|
||||
:typography "headline-medium"
|
||||
:id "modal-title"
|
||||
:typography "headline-large"
|
||||
:class (stl/css :modal-title)}
|
||||
(tr "workspace.tokens.remap-token-references")]]
|
||||
(tr "workspace.tokens.remap-token-references-title" old-token-name new-token-name)]]
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:> heading* {:level 3
|
||||
:typography "title-medium"
|
||||
:class (stl/css :modal-msg)}
|
||||
(tr "workspace.tokens.renaming-token-from-to" old-token-name new-token-name)]
|
||||
[:div {:class (stl/css :modal-scd-msg)}
|
||||
(if (> references-count 0)
|
||||
(tr "workspace.tokens.references-found" references-count)
|
||||
(tr "workspace.tokens.no-references-found"))]
|
||||
(when remapping-in-progress?
|
||||
[:> context-notification*
|
||||
{:level :info
|
||||
:appearance :ghost}
|
||||
(tr "workspace.tokens.remapping-in-progress")])]
|
||||
[:> text* {:as "p" :typography t/body-medium} (tr "workspace.tokens.remap-warning-effects")]
|
||||
[:> text* {:as "p" :typography t/body-medium} (tr "workspace.tokens.remap-warning-time")]]
|
||||
[:div {:class (stl/css :modal-footer)}
|
||||
[:div {:class (stl/css :action-buttons)}
|
||||
[:> button* {:on-click on-cancel-remap
|
||||
[:> button* {:on-click rename-token
|
||||
:type "button"
|
||||
:variant "secondary"
|
||||
:disabled remapping-in-progress?}
|
||||
(tr "labels.cancel")]
|
||||
[:> button* {:on-click on-confirm-remap
|
||||
:variant "secondary"}
|
||||
(tr "workspace.tokens.not-remap")]
|
||||
[:> button* {:on-click confirm-remap
|
||||
:type "button"
|
||||
:variant "primary"
|
||||
:disabled remapping-in-progress?}
|
||||
(if (> references-count 0)
|
||||
(tr "workspace.tokens.remap-and-rename")
|
||||
(tr "workspace.tokens.rename-only"))]]]]]))
|
||||
:variant "primary"}
|
||||
(tr "workspace.tokens.remap")]]]]]))
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
@use "refactor/common-refactor.scss" as deprecated;
|
||||
|
||||
.modal-overlay {
|
||||
--modal-title-foreground-color: var(--color-foreground-primary);
|
||||
--modal-text-foreground-color: var(--color-foreground-secondary);
|
||||
|
||||
@extend .modal-overlay-base;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -17,16 +20,22 @@
|
||||
position: fixed;
|
||||
inset-inline-start: 0;
|
||||
inset-block-start: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
block-size: 100%;
|
||||
inline-size: 100%;
|
||||
background-color: var(--overlay-color);
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
inset-block-start: $sz-6;
|
||||
inset-inline-end: $sz-6;
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
@extend .modal-container-base;
|
||||
width: 100%;
|
||||
max-width: 32rem;
|
||||
max-height: unset;
|
||||
inline-size: 100%;
|
||||
max-inline-size: 32rem;
|
||||
max-block-size: unset;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
}
|
||||
@@ -45,11 +54,7 @@
|
||||
|
||||
.modal-content {
|
||||
@include t.use-typography("body-large");
|
||||
margin-block-end: var(--sp-xxl);
|
||||
padding: var(--sp-xxl) 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-l);
|
||||
color: var(--modal-text-foreground-color);
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
|
||||
@@ -225,118 +225,83 @@
|
||||
get-expr)])
|
||||
|
||||
(when set-expr
|
||||
(concat
|
||||
(when (and schema-n (not (list? schema-n)))
|
||||
[coercer-sym `(sm/coercer ~schema-n)])
|
||||
[schema-sym schema-n
|
||||
|
||||
(when (and schema-n (list? schema-n))
|
||||
[schema-sym schema-n])
|
||||
coercer-sym `(if (and (some? ~schema-sym)
|
||||
(not (fn? ~schema-sym)))
|
||||
(sm/coercer ~schema-sym)
|
||||
nil)
|
||||
|
||||
[decode-sym decode-expr]
|
||||
decode-sym decode-expr
|
||||
|
||||
[(make-sym pname "set-fn")
|
||||
(if this?
|
||||
`(fn [~val-sym]
|
||||
(let [~@(if (and schema-n (list? schema-n))
|
||||
[schema-sym `(~schema-sym ~val-sym)
|
||||
coercer-sym `(sm/coercer ~schema-sym)]
|
||||
[])
|
||||
~this-sym (~'js* "this")
|
||||
~fn-sym ~set-expr
|
||||
~val-sym ~(if schema-n
|
||||
(if decode-expr
|
||||
`(~decode-sym ~val-sym)
|
||||
`(~decode-sym ~val-sym ~decode-options))
|
||||
val-sym)
|
||||
~val-sym ~(if schema-n
|
||||
`(~coercer-sym ~val-sym)
|
||||
val-sym)]
|
||||
(.call ~fn-sym ~this-sym ~this-sym ~val-sym)))
|
||||
`(fn [~val-sym]
|
||||
(let [~@(if (and schema-n (list? schema-n))
|
||||
[schema-sym `(~schema-sym ~val-sym)
|
||||
coercer-sym `(sm/coercer ~schema-sym)]
|
||||
[])
|
||||
~this-sym (~'js* "this")
|
||||
~fn-sym ~set-expr
|
||||
~val-sym ~(if schema-n
|
||||
(if decode-expr
|
||||
`(~decode-sym ~val-sym)
|
||||
`(~decode-sym ~val-sym ~decode-options))
|
||||
val-sym)
|
||||
~val-sym ~(if schema-n
|
||||
`(~coercer-sym ~val-sym)
|
||||
val-sym)]
|
||||
(.call ~fn-sym ~this-sym ~val-sym))))]))
|
||||
(make-sym pname "set-fn")
|
||||
`(fn [~val-sym]
|
||||
(let [~this-sym (~'js* "this")
|
||||
~fn-sym ~set-expr
|
||||
|
||||
;; We only emit schema and coercer bindings if
|
||||
;; schema-n is provided
|
||||
~@(if (some? schema-n)
|
||||
[schema-sym `(if (fn? ~schema-sym)
|
||||
(~schema-sym ~val-sym)
|
||||
~schema-sym)
|
||||
|
||||
coercer-sym `(if (nil? ~coercer-sym)
|
||||
(sm/coercer ~schema-sym)
|
||||
~coercer-sym)
|
||||
val-sym (if (not= decode-expr 'app.common.json/->clj)
|
||||
`(~decode-sym ~val-sym)
|
||||
`(~decode-sym ~val-sym ~decode-options))
|
||||
val-sym `(~coercer-sym ~val-sym)]
|
||||
[])]
|
||||
|
||||
~(if this?
|
||||
`(.call ~fn-sym ~this-sym ~this-sym ~val-sym)
|
||||
`(.call ~fn-sym ~this-sym ~val-sym))))])
|
||||
|
||||
(when fn-expr
|
||||
(concat
|
||||
(cond
|
||||
(and schema-n (not (list? schema-n)))
|
||||
[coercer-sym `(sm/coercer ~schema-n)]
|
||||
[schema-sym (or schema-n schema-1)
|
||||
coercer-sym `(if (and (some? ~schema-sym)
|
||||
(not (fn? ~schema-sym)))
|
||||
(sm/coercer ~schema-sym)
|
||||
nil)
|
||||
decode-sym decode-expr
|
||||
|
||||
(and schema-n (list? schema-n))
|
||||
[schema-sym schema-n]
|
||||
(make-sym pname "get-fn")
|
||||
`(fn []
|
||||
(let [~this-sym (~'js* "this")
|
||||
~fn-sym ~fn-expr
|
||||
~fn-sym ~(if this?
|
||||
`(.bind ~fn-sym ~this-sym ~this-sym)
|
||||
`(.bind ~fn-sym ~this-sym))
|
||||
|
||||
(and schema-1 (not (list? schema-1)))
|
||||
[coercer-sym `(sm/coercer ~schema-1)]
|
||||
;; We only emit schema and coercer bindings if
|
||||
;; schema-n or schema-1 is provided
|
||||
~@(if (or schema-n schema-1)
|
||||
[fn-sym `(fn* [~@(if schema-1 [val-sym] [])]
|
||||
(let [~@(if schema-n
|
||||
[val-sym `(into-array (cljs.core/js-arguments))]
|
||||
[])
|
||||
~val-sym ~(if (not= decode-expr 'app.common.json/->clj)
|
||||
`(~decode-sym ~val-sym)
|
||||
`(~decode-sym ~val-sym ~decode-options))
|
||||
|
||||
(and schema-1 (list? schema-1))
|
||||
[schema-sym schema-1])
|
||||
~schema-sym (if (fn? ~schema-sym)
|
||||
(~schema-sym ~val-sym)
|
||||
~schema-sym)
|
||||
|
||||
[decode-sym decode-expr]
|
||||
~coercer-sym (if (nil? ~coercer-sym)
|
||||
(sm/coercer ~schema-sym)
|
||||
~coercer-sym)
|
||||
|
||||
[(make-sym pname "get-fn")
|
||||
`(fn []
|
||||
(let [~this-sym (~'js* "this")
|
||||
~fn-sym ~fn-expr
|
||||
~fn-sym ~(if this?
|
||||
`(.bind ~fn-sym ~this-sym ~this-sym)
|
||||
`(.bind ~fn-sym ~this-sym))
|
||||
|
||||
~@(if schema-1
|
||||
[fn-sym `(fn* [~val-sym]
|
||||
(let [~val-sym
|
||||
~(if decode-expr
|
||||
`(~decode-sym ~val-sym)
|
||||
`(~decode-sym ~val-sym ~decode-options))
|
||||
|
||||
~@(if (or (and schema-n (list? schema-n))
|
||||
(and schema-1 (list? schema-1)))
|
||||
[schema-sym `(~schema-sym ~val-sym)
|
||||
coercer-sym `(sm/coercer ~schema-sym)]
|
||||
[])
|
||||
|
||||
~val-sym
|
||||
(~coercer-sym ~val-sym)]
|
||||
|
||||
(~fn-sym ~val-sym)))]
|
||||
[])
|
||||
~@(if schema-n
|
||||
[fn-sym `(fn* []
|
||||
(let [~val-sym
|
||||
(into-array (cljs.core/js-arguments))
|
||||
|
||||
~val-sym
|
||||
~(if decode-expr
|
||||
`(~decode-sym ~val-sym)
|
||||
`(~decode-sym ~val-sym ~decode-options))
|
||||
|
||||
~@(if (or (and schema-n (list? schema-n))
|
||||
(and schema-1 (list? schema-1)))
|
||||
[schema-sym `(~schema-sym ~val-sym)
|
||||
coercer-sym `(sm/coercer ~schema-sym)]
|
||||
[])
|
||||
|
||||
~val-sym
|
||||
(~coercer-sym ~val-sym)]
|
||||
(apply ~fn-sym ~val-sym)))]
|
||||
[])
|
||||
~@(if wrap
|
||||
[fn-sym `(~wrap-sym ~fn-sym)]
|
||||
[])]
|
||||
|
||||
~fn-sym))])))))))]
|
||||
~val-sym (~coercer-sym ~val-sym)]
|
||||
~(if schema-1
|
||||
`(~fn-sym ~val-sym)
|
||||
`(apply ~fn-sym ~val-sym))))]
|
||||
[])]
|
||||
~(if wrap
|
||||
`(~wrap-sym ~fn-sym)
|
||||
fn-sym)))]))))))]
|
||||
|
||||
`(let [~target-sym ~rsym
|
||||
~@bindings]
|
||||
|
||||
@@ -344,7 +344,7 @@ msgstr "Copied token"
|
||||
|
||||
#: src/app/main/ui/settings/access_tokens.cljs:189
|
||||
msgid "dashboard.access-tokens.create"
|
||||
msgstr "Generate new token"
|
||||
msgstr "Create new access token"
|
||||
|
||||
#: src/app/main/ui/settings/access_tokens.cljs:64
|
||||
msgid "dashboard.access-tokens.create.success"
|
||||
@@ -352,7 +352,7 @@ msgstr "Access token created successfully."
|
||||
|
||||
#: src/app/main/ui/settings/access_tokens.cljs:286
|
||||
msgid "dashboard.access-tokens.empty.add-one"
|
||||
msgstr "Press the button \"Generate new token\" to generate one."
|
||||
msgstr "Press the button \"Create new access token\" to generate one."
|
||||
|
||||
#: src/app/main/ui/settings/access_tokens.cljs:285
|
||||
msgid "dashboard.access-tokens.empty.no-access-tokens"
|
||||
@@ -433,6 +433,9 @@ msgstr "(copy)"
|
||||
msgid "dashboard.create-new-team"
|
||||
msgstr "Create new team"
|
||||
|
||||
msgid "dashboard.create-new-org"
|
||||
msgstr "Create new org"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:661
|
||||
msgid "dashboard.create-version-menu"
|
||||
msgstr "Pin this version"
|
||||
@@ -2468,6 +2471,10 @@ msgstr "Info"
|
||||
msgid "labels.installed-fonts"
|
||||
msgstr "Installed fonts"
|
||||
|
||||
#: src/app/main/ui/settings/sidebar.cljs:123
|
||||
msgid "labels.integrations"
|
||||
msgstr "Integrations"
|
||||
|
||||
#: src/app/main/ui/static.cljs:396
|
||||
msgid "labels.internal-error.desc-message-first"
|
||||
msgstr "Something bad happened."
|
||||
@@ -3144,13 +3151,13 @@ msgstr "Name"
|
||||
msgid "modals.create-access-token.name.placeholder"
|
||||
msgstr "The name can help to know what's the token for"
|
||||
|
||||
#: src/app/main/ui/settings/access_tokens.cljs:178
|
||||
msgid "modals.create-access-token.submit-label"
|
||||
msgstr "Create token"
|
||||
|
||||
#: src/app/main/ui/settings/access_tokens.cljs:111
|
||||
msgid "modals.create-access-token.title"
|
||||
msgstr "Generate access token"
|
||||
msgstr "Create access token"
|
||||
|
||||
#: src/app/main/ui/settings/access_tokens.cljs:111
|
||||
msgid "modals.create-access-token.title.created"
|
||||
msgstr "Access token created"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:1127
|
||||
msgid "modals.create-webhook.submit-label"
|
||||
@@ -5073,14 +5080,14 @@ msgstr "Shared Libraries - %s - Penpot"
|
||||
msgid "title.default"
|
||||
msgstr "Penpot - Design Freedom for Teams"
|
||||
|
||||
#: src/app/main/ui/settings/access_tokens.cljs:278
|
||||
msgid "title.settings.access-tokens"
|
||||
msgstr "Profile - Access tokens"
|
||||
|
||||
#: src/app/main/ui/settings/feedback.cljs:161
|
||||
msgid "title.settings.feedback"
|
||||
msgstr "Give feedback - Penpot"
|
||||
|
||||
#: src/app/main/ui/settings/access_tokens.cljs:278
|
||||
msgid "title.settings.integrations"
|
||||
msgstr "Integrations - Penpot"
|
||||
|
||||
#: src/app/main/ui/settings/notifications.cljs:45
|
||||
msgid "title.settings.notifications"
|
||||
msgstr "Notifications - Penpot"
|
||||
@@ -6713,19 +6720,19 @@ msgstr "Advanced options"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:543
|
||||
msgid "workspace.options.layout-item.layout-item-max-h"
|
||||
msgstr "Max height"
|
||||
msgstr "Max.Height"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:510
|
||||
msgid "workspace.options.layout-item.layout-item-max-w"
|
||||
msgstr "Max width"
|
||||
msgstr "Max.Width"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:527
|
||||
msgid "workspace.options.layout-item.layout-item-min-h"
|
||||
msgstr "Min height"
|
||||
msgstr "Min.Height"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:494
|
||||
msgid "workspace.options.layout-item.layout-item-min-w"
|
||||
msgstr "Min width"
|
||||
msgstr "Min.Width"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs
|
||||
#, unused
|
||||
@@ -8069,6 +8076,10 @@ msgid "workspace.tokens.missing-references"
|
||||
msgstr "Missing token references: "
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:123
|
||||
msgid "workspace.tokens.missing-reference"
|
||||
msgstr "Missing reference"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:299
|
||||
msgid "workspace.tokens.more-options"
|
||||
msgstr "Right click to see options"
|
||||
|
||||
@@ -8159,36 +8170,29 @@ msgstr "Enter a token shadow alias"
|
||||
msgid "workspace.tokens.reference-error"
|
||||
msgstr "Reference Errors: "
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:86
|
||||
msgid "workspace.tokens.references-found"
|
||||
msgstr "%s references found in your design"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:105
|
||||
msgid "workspace.tokens.remap-and-rename"
|
||||
msgstr "Remap & Rename"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs
|
||||
#, unused
|
||||
msgid "workspace.tokens.remap-explanation"
|
||||
msgstr ""
|
||||
"All references to this token will be automatically updated to use the new "
|
||||
"name."
|
||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:78
|
||||
msgid "workspace.tokens.remap-token-references-title"
|
||||
msgstr "Remap all tokens that use `%s` to `%s`?"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:78
|
||||
msgid "workspace.tokens.remap-token-references"
|
||||
msgstr "Remap Token References"
|
||||
msgid "workspace.tokens.remap-warning-effects"
|
||||
msgstr "This will change all layers and references that use the old token name."
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:78
|
||||
msgid "workspace.tokens.remap-warning-time"
|
||||
msgstr "This action could take a while."
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:92
|
||||
msgid "workspace.tokens.remapping-in-progress"
|
||||
msgstr "Remapping token references..."
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:106
|
||||
msgid "workspace.tokens.rename-only"
|
||||
msgstr "Rename"
|
||||
msgid "workspace.tokens.not-remap"
|
||||
msgstr "Don't remap"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:83
|
||||
msgid "workspace.tokens.renaming-token-from-to"
|
||||
msgstr "Renaming token from '%s' to '%s'"
|
||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:105
|
||||
msgid "workspace.tokens.remap"
|
||||
msgstr "Remap tokens"
|
||||
|
||||
#: src/app/main/data/workspace/tokens/warnings.cljs:15, src/app/main/data/workspace/tokens/warnings.cljs:19, src/app/main/ui/workspace/colorpicker/color_tokens.cljs:56, src/app/main/ui/workspace/colorpicker/color_tokens.cljs:84, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:103, src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs:271, src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs:445, src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs:169, src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs:304, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:225, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:332, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:432, src/app/main/ui/workspace/tokens/management/token_pill.cljs:121
|
||||
#, fuzzy
|
||||
|
||||
@@ -353,7 +353,7 @@ msgstr "Token copiado"
|
||||
|
||||
#: src/app/main/ui/settings/access_tokens.cljs:189
|
||||
msgid "dashboard.access-tokens.create"
|
||||
msgstr "Generar nuevo token"
|
||||
msgstr "Crear nuevo token de acceso"
|
||||
|
||||
#: src/app/main/ui/settings/access_tokens.cljs:64
|
||||
msgid "dashboard.access-tokens.create.success"
|
||||
@@ -361,7 +361,7 @@ msgstr "Access token creado con éxito."
|
||||
|
||||
#: src/app/main/ui/settings/access_tokens.cljs:286
|
||||
msgid "dashboard.access-tokens.empty.add-one"
|
||||
msgstr "Pulsa el botón \"Generar nuevo token\" para generar uno."
|
||||
msgstr "Pulsa el botón \"Crear nuevo token de accesso\" para generar uno."
|
||||
|
||||
#: src/app/main/ui/settings/access_tokens.cljs:285
|
||||
msgid "dashboard.access-tokens.empty.no-access-tokens"
|
||||
@@ -442,6 +442,9 @@ msgstr "(copia)"
|
||||
msgid "dashboard.create-new-team"
|
||||
msgstr "Crear nuevo equipo"
|
||||
|
||||
msgid "dashboard.create-new-org"
|
||||
msgstr "Crear nueva organización"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:661
|
||||
msgid "dashboard.create-version-menu"
|
||||
msgstr "Guardar esta versión"
|
||||
@@ -2439,6 +2442,10 @@ msgstr "Información"
|
||||
msgid "labels.installed-fonts"
|
||||
msgstr "Fuentes instaladas"
|
||||
|
||||
#: src/app/main/ui/settings/sidebar.cljs:123
|
||||
msgid "labels.integrations"
|
||||
msgstr "Integraciones"
|
||||
|
||||
#: src/app/main/ui/static.cljs:396
|
||||
msgid "labels.internal-error.desc-message-first"
|
||||
msgstr "Ha ocurrido algo extraño."
|
||||
@@ -3111,13 +3118,13 @@ msgstr "Nombre"
|
||||
msgid "modals.create-access-token.name.placeholder"
|
||||
msgstr "El nombre te pude ayudar a saber para qué se utiliza el token"
|
||||
|
||||
#: src/app/main/ui/settings/access_tokens.cljs:178
|
||||
msgid "modals.create-access-token.submit-label"
|
||||
msgstr "Crear token"
|
||||
|
||||
#: src/app/main/ui/settings/access_tokens.cljs:111
|
||||
msgid "modals.create-access-token.title"
|
||||
msgstr "Generar access token"
|
||||
msgstr "Crear token de accesso"
|
||||
|
||||
#: src/app/main/ui/settings/access_tokens.cljs:111
|
||||
msgid "modals.create-access-token.title.created"
|
||||
msgstr "Token de acceso creado"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:1127
|
||||
msgid "modals.create-webhook.submit-label"
|
||||
@@ -5056,14 +5063,14 @@ msgstr "Bibliotecas Compartidas - %s - Penpot"
|
||||
msgid "title.default"
|
||||
msgstr "Penpot - Diseño Libre para Equipos"
|
||||
|
||||
#: src/app/main/ui/settings/access_tokens.cljs:278
|
||||
msgid "title.settings.access-tokens"
|
||||
msgstr "Perfil - Access tokens"
|
||||
|
||||
#: src/app/main/ui/settings/feedback.cljs:161
|
||||
msgid "title.settings.feedback"
|
||||
msgstr "Danos tu opinión - Penpot"
|
||||
|
||||
#: src/app/main/ui/settings/access_tokens.cljs:278
|
||||
msgid "title.settings.integrations"
|
||||
msgstr "Integraciones - Penpot"
|
||||
|
||||
#: src/app/main/ui/settings/notifications.cljs:45
|
||||
msgid "title.settings.notifications"
|
||||
msgstr "Notificaciones - Penpot"
|
||||
@@ -7164,11 +7171,11 @@ msgstr "Ancho"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:535, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:552
|
||||
msgid "workspace.options.x"
|
||||
msgstr "Eje X"
|
||||
msgstr "eje X"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:545, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:563
|
||||
msgid "workspace.options.y"
|
||||
msgstr "Eje Y"
|
||||
msgstr "eje Y"
|
||||
|
||||
#: src/app/main/ui/workspace/viewport/path_actions.cljs:140
|
||||
msgid "workspace.path.actions.add-node"
|
||||
@@ -7991,6 +7998,10 @@ msgstr "Line height (multiplicador, px o %) o {alias}"
|
||||
msgid "workspace.tokens.missing-references"
|
||||
msgstr "Referencias de tokens no encontradas: "
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:123
|
||||
msgid "workspace.tokens.missing-reference"
|
||||
msgstr "Referencia no encontrada"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:123
|
||||
msgid "workspace.tokens.more-options"
|
||||
msgstr "Click derecho para ver opciones"
|
||||
@@ -8060,36 +8071,29 @@ msgstr "La referencia no es válida o no se encuentra en ningún set activo."
|
||||
msgid "workspace.tokens.reference-error"
|
||||
msgstr "Errores en referencias: "
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:86
|
||||
msgid "workspace.tokens.references-found"
|
||||
msgstr "%s referencias encontradas en tu diseño"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:105
|
||||
msgid "workspace.tokens.remap-and-rename"
|
||||
msgstr "Actualizar referencias y renombrar"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs
|
||||
#, unused
|
||||
msgid "workspace.tokens.remap-explanation"
|
||||
msgstr ""
|
||||
"Todas las referencias a este token se actualizarán automáticamente para "
|
||||
"usar el nuevo nombre."
|
||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:78
|
||||
msgid "workspace.tokens.remap-token-references-title"
|
||||
msgstr "¿Actualizar todas las referencias de `%s` a `%s`?"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:78
|
||||
msgid "workspace.tokens.remap-token-references"
|
||||
msgstr "Actualizar referencias de token"
|
||||
msgid "workspace.tokens.remap-warning-effects"
|
||||
msgstr "Esta acción actualizará todas las capas y referencias que usen el token antiguo"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:78
|
||||
msgid "workspace.tokens.remap-warning-time"
|
||||
msgstr "Este proceso puede durar un poco"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:92
|
||||
msgid "workspace.tokens.remapping-in-progress"
|
||||
msgstr "Actualizando referencias de token..."
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:106
|
||||
msgid "workspace.tokens.rename-only"
|
||||
msgstr "Renombrar"
|
||||
msgid "workspace.tokens.not-remap"
|
||||
msgstr "No actualizar"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:83
|
||||
msgid "workspace.tokens.renaming-token-from-to"
|
||||
msgstr "Renombrando el token de '%s' a '%s'"
|
||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:105
|
||||
msgid "workspace.tokens.remap"
|
||||
msgstr "Actualizar tokens"
|
||||
|
||||
#: src/app/main/data/workspace/tokens/warnings.cljs:15, src/app/main/data/workspace/tokens/warnings.cljs:19, src/app/main/ui/workspace/colorpicker/color_tokens.cljs:56, src/app/main/ui/workspace/colorpicker/color_tokens.cljs:84, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:103, src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs:271, src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs:445, src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs:169, src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs:304, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:225, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:332, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:432, src/app/main/ui/workspace/tokens/management/token_pill.cljs:121
|
||||
#, fuzzy
|
||||
|
||||
Reference in New Issue
Block a user