mirror of
https://github.com/penpot/penpot.git
synced 2026-03-01 05:09:31 -05:00
Compare commits
5 Commits
niwinz-dev
...
eva-create
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
155683dbe0 | ||
|
|
3e4b0124c5 | ||
|
|
f22633d18b | ||
|
|
942149ae87 | ||
|
|
ed379013ef |
49
.github/workflows/release.yml
vendored
49
.github/workflows/release.yml
vendored
@@ -37,43 +37,36 @@ jobs:
|
||||
ref: ${{ steps.vars.outputs.gh_ref }}
|
||||
|
||||
# --- Publicly release the docker images ---
|
||||
- name: Configure ECR credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
- name: Login to private registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.DOCKER_USERNAME }}
|
||||
aws-secret-access-key: ${{ secrets.DOCKER_PASSWORD }}
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
registry: ${{ secrets.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Install Skopeo
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y skopeo
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.PUB_DOCKER_USERNAME }}
|
||||
password: ${{ secrets.PUB_DOCKER_PASSWORD }}
|
||||
|
||||
- name: Copy images from AWS ECR to Docker Hub
|
||||
- name: Publish docker images to DockerHub
|
||||
env:
|
||||
AWS_REGION: ${{ secrets.AWS_REGION }}
|
||||
DOCKER_REGISTRY: ${{ secrets.DOCKER_REGISTRY }}
|
||||
PUB_DOCKER_USERNAME: ${{ secrets.PUB_DOCKER_USERNAME }}
|
||||
PUB_DOCKER_PASSWORD: ${{ secrets.PUB_DOCKER_PASSWORD }}
|
||||
TAG: ${{ steps.vars.outputs.gh_ref }}
|
||||
REGISTRY: ${{ secrets.DOCKER_REGISTRY }}
|
||||
HUB: ${{ secrets.PUB_DOCKER_HUB }}
|
||||
run: |
|
||||
aws ecr get-login-password --region $AWS_REGION | \
|
||||
skopeo login --username AWS --password-stdin \
|
||||
$DOCKER_REGISTRY
|
||||
|
||||
echo "$PUB_DOCKER_PASSWORD" | skopeo login --username "$PUB_DOCKER_USERNAME" --password-stdin docker.io
|
||||
|
||||
IMAGES=("frontend" "backend" "exporter" "storybook")
|
||||
IMAGES=("frontend" "backend" "exporter")
|
||||
EXTRA_TAGS=("main" "latest")
|
||||
|
||||
for image in "${IMAGES[@]}"; do
|
||||
skopeo copy --all \
|
||||
docker://$DOCKER_REGISTRY/$image:$TAG \
|
||||
docker://docker.io/$PUB_DOCKER_USERNAME/$image:$TAG
|
||||
docker pull "$REGISTRY/$image:$TAG"
|
||||
docker tag "$REGISTRY/$image:$TAG" "penpotapp/$image:$TAG"
|
||||
docker push "penpotapp/$image:$TAG"
|
||||
|
||||
for alias in main latest; do
|
||||
skopeo copy --all \
|
||||
docker://$DOCKER_REGISTRY/$image:$TAG \
|
||||
docker://docker.io/$PUB_DOCKER_USERNAME/$image:$alias
|
||||
for tag in "${EXTRA_TAGS[@]}"; do
|
||||
docker tag "$REGISTRY/$image:$TAG" "penpotapp/$image:$tag"
|
||||
docker push "penpotapp/$image:$tag"
|
||||
done
|
||||
done
|
||||
|
||||
|
||||
18
CHANGES.md
18
CHANGES.md
@@ -4,20 +4,12 @@
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
- The backend RPC API URLS are changed from `/api/rpc/command/<name>`
|
||||
to `/api/main/methods/<name>` (the previou PATH is preserved for
|
||||
backward compatibility; however, if you are a user of this API, it
|
||||
is strongly recommended that you adapt your code to use the new
|
||||
PATH.
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
### :sparkles: New features & Enhancements
|
||||
|
||||
- Select boards to export as PDF [Taiga #12320](https://tree.taiga.io/project/penpot/issue/12320)
|
||||
- Toggle for switching boolean property values [Taiga #12341](https://tree.taiga.io/project/penpot/us/12341)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
@@ -31,9 +23,8 @@
|
||||
- Fix search shortcut [Taiga #10265](https://tree.taiga.io/project/penpot/issue/10265)
|
||||
- Fix shortcut conflict in text editor (increase/decrease font size vs word selection)
|
||||
- Fix problem with plugins generating code for pages different than current one [Taiga #12312](https://tree.taiga.io/project/penpot/issue/12312)
|
||||
- Fix input confirmation behavior is not uniform [Taiga #12294](https://tree.taiga.io/project/penpot/issue/12294)
|
||||
|
||||
## 2.11.0
|
||||
## 2.11.0 (Unreleased)
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
@@ -61,6 +52,10 @@
|
||||
services which use netty internally (redis connection, S3 SDK client). This
|
||||
configuration is not very commonly used so don't expected real impact on any user.
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
### :sparkles: New features & Enhancements
|
||||
|
||||
- New composite token: Typography [Taiga #10200](https://tree.taiga.io/project/penpot/us/10200)
|
||||
@@ -105,9 +100,6 @@
|
||||
- Fix options button does not work for comments created in the lower part of the screen [Taiga #12422](https://tree.taiga.io/project/penpot/issue/12422)
|
||||
- Fix problem when checking usage with removed teams [Taiga #12442](https://tree.taiga.io/project/penpot/issue/12442)
|
||||
- Fix focus mode persisting across page/file navigation [Taiga #12469](https://tree.taiga.io/project/penpot/issue/12469)
|
||||
- Fix shadow color validation [Github #7705](https://github.com/penpot/penpot/pull/7705)
|
||||
- Fix exception on selection blend-mode using keyboard [Github #7710](https://github.com/penpot/penpot/pull/7710)
|
||||
- Fix crash when using decimal (floating-point) values for X/Y or width/height [Taiga #12543](https://tree.taiga.io/project/penpot/issue/12543)
|
||||
|
||||
## 2.10.1
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
[app.common.transit :as t]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.common.uri :as u]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.main :as main]
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="robots" content="noindex,nofollow">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<title>{{label|upper}} API Documentation</title>
|
||||
<title>Builtin API Documentation - Penpot</title>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
@@ -19,7 +19,7 @@
|
||||
<body>
|
||||
<main>
|
||||
<header>
|
||||
<h1>{{label|upper}}: API Documentation (v{{version}})</h1>
|
||||
<h1>Penpot API Documentation (v{{version}})</h1>
|
||||
<small class="menu">
|
||||
[
|
||||
<nav>
|
||||
@@ -31,10 +31,9 @@
|
||||
</header>
|
||||
<section class="doc-content">
|
||||
<h2>INTRODUCTION</h2>
|
||||
<p>This documentation is intended to be a general overview of
|
||||
the {{label}} API. If you prefer, you can
|
||||
use <a href="{{openapi}}">Swagger/OpenAPI</a> as
|
||||
alternative.</p>
|
||||
<p>This documentation is intended to be a general overview of the penpot RPC API.
|
||||
If you prefer, you can use <a href="/api/openapi.json">OpenAPI</a>
|
||||
and/or <a href="/api/openapi">SwaggerUI</a> as alternative.</p>
|
||||
|
||||
<h2>GENERAL NOTES</h2>
|
||||
|
||||
@@ -44,7 +43,7 @@
|
||||
that starts with <b>get-</b> in the name, can use GET HTTP
|
||||
method which in many cases benefits from the HTTP cache.</p>
|
||||
|
||||
{% block auth-section %}
|
||||
|
||||
<h3>Authentication</h3>
|
||||
<p>The penpot backend right now offers two way for authenticate the request:
|
||||
<b>cookies</b> (the same mechanism that we use ourselves on accessing the API from the
|
||||
@@ -57,10 +56,9 @@
|
||||
<p>The access token can be obtained on the appropriate section on profile settings
|
||||
and it should be provided using <b>`Authorization`</b> header with <b>`Token
|
||||
<token-string>`</b> value.</p>
|
||||
{% endblock %}
|
||||
|
||||
<h3>Content Negotiation</h3>
|
||||
<p>This API operates indistinctly with: <b>`application/json`</b>
|
||||
<p>The penpot API by default operates indistinctly with: <b>`application/json`</b>
|
||||
and <b>`application/transit+json`</b> content types. You should specify the
|
||||
desired content-type on the <b>`Accept`</b> header, the transit encoding is used
|
||||
by default.</p>
|
||||
@@ -77,16 +75,13 @@
|
||||
standard <a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API">Fetch
|
||||
API</a></p>
|
||||
|
||||
{% block limits-section %}
|
||||
<h3>Limits</h3>
|
||||
<p>The rate limit work per user basis (this means that different api keys share
|
||||
the same rate limit). For now the limits are not documented because we are
|
||||
studying and analyzing the data. As a general rule, it should not be abused, if an
|
||||
abusive use is detected, we will proceed to block the user's access to the
|
||||
API.</p>
|
||||
{% endblock %}
|
||||
|
||||
{% block webhooks-section %}
|
||||
<h3>Webhooks</h3>
|
||||
<p>All methods that emit webhook events are marked with flag <b>WEBHOOK</b>, the
|
||||
data structure defined on each method represents the <i>payload</i> of the
|
||||
@@ -102,11 +97,9 @@
|
||||
"profileId": "db601c95-045f-808b-8002-361312e63531"
|
||||
}
|
||||
</pre>
|
||||
{% endblock %}
|
||||
|
||||
</section>
|
||||
<section class="rpc-doc-content">
|
||||
<h2>METHODS REFERENCE:</h2>
|
||||
<h2>RPC METHODS REFERENCE:</h2>
|
||||
<ul class="rpc-items">
|
||||
{% for item in methods %}
|
||||
{% include "app/templates/api-doc-entry.tmpl" with item=item %}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{% extends "app/templates/api-doc.tmpl" %}
|
||||
@@ -1,10 +0,0 @@
|
||||
{% extends "app/templates/api-doc.tmpl" %}
|
||||
|
||||
{% block auth-section %}
|
||||
{% endblock %}
|
||||
|
||||
{% block limits-section %}
|
||||
{% endblock %}
|
||||
|
||||
{% block webhooks-section %}
|
||||
{% endblock %}
|
||||
@@ -7,7 +7,7 @@
|
||||
name="description"
|
||||
content="SwaggerUI"
|
||||
/>
|
||||
<title>{{label|upper}} API</title>
|
||||
<title>PENPOT Swagger UI</title>
|
||||
<style>{{swagger-css|safe}}</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -16,7 +16,7 @@
|
||||
<script>
|
||||
window.onload = () => {
|
||||
window.ui = SwaggerUIBundle({
|
||||
url: '{{uri}}',
|
||||
url: '{{public-uri}}/api/openapi.json',
|
||||
dom_id: '#swagger-ui',
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
|
||||
@@ -21,8 +21,6 @@
|
||||
[app.email.whitelist :as email.whitelist]
|
||||
[app.http.client :as http]
|
||||
[app.http.errors :as errors]
|
||||
[app.http.middleware :as mw]
|
||||
[app.http.security :as sec]
|
||||
[app.http.session :as session]
|
||||
[app.loggers.audit :as audit]
|
||||
[app.rpc :as rpc]
|
||||
@@ -692,9 +690,8 @@
|
||||
(defmethod ig/init-key ::routes
|
||||
[_ cfg]
|
||||
(let [cfg (update cfg :providers d/without-nils)]
|
||||
["/api" {:middleware [[mw/cors]
|
||||
[sec/client-header-check]
|
||||
[provider-lookup cfg]]}
|
||||
["" {:middleware [[session/authz cfg]
|
||||
[provider-lookup cfg]]}
|
||||
["/auth/oauth"
|
||||
["/:provider"
|
||||
{:handler auth-handler
|
||||
|
||||
@@ -47,7 +47,6 @@
|
||||
:auto-file-snapshot-timeout "3h"
|
||||
|
||||
:public-uri "http://localhost:3449"
|
||||
|
||||
:host "localhost"
|
||||
:tenant "default"
|
||||
|
||||
@@ -58,8 +57,6 @@
|
||||
:objects-storage-backend "fs"
|
||||
:objects-storage-fs-directory "assets"
|
||||
|
||||
:auth-token-cookie-name "auth-token"
|
||||
|
||||
:assets-path "/internal/assets/"
|
||||
:smtp-default-reply-to "Penpot <no-reply@example.com>"
|
||||
:smtp-default-from "Penpot <no-reply@example.com>"
|
||||
@@ -93,7 +90,7 @@
|
||||
[:secret-key {:optional true} :string]
|
||||
|
||||
[:tenant {:optional false} :string]
|
||||
[:public-uri {:optional false} ::sm/uri]
|
||||
[:public-uri {:optional false} :string]
|
||||
[:host {:optional false} :string]
|
||||
|
||||
[:http-server-port {:optional true} ::sm/int]
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
[app.main :as-alias main]
|
||||
[app.metrics :as mtx]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.doc :as-alias rpc.doc]
|
||||
[app.setup :as-alias setup]
|
||||
[integrant.core :as ig]
|
||||
[reitit.core :as r]
|
||||
@@ -148,6 +149,7 @@
|
||||
[:map
|
||||
[::ws/routes schema:routes]
|
||||
[::rpc/routes schema:routes]
|
||||
[::rpc.doc/routes schema:routes]
|
||||
[::oidc/routes schema:routes]
|
||||
[::assets/routes schema:routes]
|
||||
[::debug/routes schema:routes]
|
||||
@@ -169,9 +171,8 @@
|
||||
[sec/sec-fetch-metadata]
|
||||
[mw/params]
|
||||
[mw/format-response]
|
||||
[mw/auth {:bearer (partial session/decode-token cfg)
|
||||
:cookie (partial session/decode-token cfg)
|
||||
:token (partial actoken/decode-token cfg)}]
|
||||
[session/soft-auth cfg]
|
||||
[actoken/soft-auth cfg]
|
||||
[mw/parse-request]
|
||||
[mw/errors errors/handle]
|
||||
[mw/restrict-methods]]}
|
||||
@@ -187,5 +188,9 @@
|
||||
(::mgmt/routes cfg)]
|
||||
|
||||
(::ws/routes cfg)
|
||||
(::oidc/routes cfg)
|
||||
(::rpc/routes cfg)]]))
|
||||
|
||||
["/api" {:middleware [[mw/cors]
|
||||
[sec/client-header-check]]}
|
||||
(::oidc/routes cfg)
|
||||
(::rpc.doc/routes cfg)
|
||||
(::rpc/routes cfg)]]]))
|
||||
|
||||
@@ -9,19 +9,23 @@
|
||||
[app.common.logging :as l]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.http.auth :as-alias http.auth]
|
||||
[app.main :as-alias main]
|
||||
[app.setup :as-alias setup]
|
||||
[app.tokens :as tokens]))
|
||||
[app.tokens :as tokens]
|
||||
[yetti.request :as yreq]))
|
||||
|
||||
(defn decode-token
|
||||
(def header-re #"(?i)^Token\s+(.*)")
|
||||
|
||||
(defn get-token
|
||||
[request]
|
||||
(some->> (yreq/get-header request "authorization")
|
||||
(re-matches header-re)
|
||||
(second)))
|
||||
|
||||
(defn- decode-token
|
||||
[cfg token]
|
||||
(try
|
||||
(tokens/verify cfg {:token token :iss "access-token"})
|
||||
(catch Throwable cause
|
||||
(l/trc :hint "exception on decoding token"
|
||||
:token token
|
||||
:cause cause))))
|
||||
(when token
|
||||
(tokens/verify cfg {:token token :iss "access-token"})))
|
||||
|
||||
(def sql:get-token-data
|
||||
"SELECT perms, profile_id, expires_at
|
||||
@@ -31,27 +35,47 @@
|
||||
OR (expires_at > now()));")
|
||||
|
||||
(defn- get-token-data
|
||||
[pool claims]
|
||||
[pool token-id]
|
||||
(when-not (db/read-only? pool)
|
||||
(when-let [token-id (-> (deref claims) (get :tid))]
|
||||
(some-> (db/exec-one! pool [sql:get-token-data token-id])
|
||||
(update :perms db/decode-pgarray #{})))))
|
||||
(some-> (db/exec-one! pool [sql:get-token-data token-id])
|
||||
(update :perms db/decode-pgarray #{}))))
|
||||
|
||||
(defn- wrap-soft-auth
|
||||
"Soft Authentication, will be executed synchronously on the undertow
|
||||
worker thread."
|
||||
[handler cfg]
|
||||
(letfn [(handle-request [request]
|
||||
(try
|
||||
(let [token (get-token request)
|
||||
claims (decode-token cfg token)]
|
||||
(cond-> request
|
||||
(map? claims)
|
||||
(assoc ::id (:tid claims))))
|
||||
(catch Throwable cause
|
||||
(l/trace :hint "exception on decoding malformed token" :cause cause)
|
||||
request)))]
|
||||
|
||||
(fn [request]
|
||||
(handler (handle-request request)))))
|
||||
|
||||
(defn- wrap-authz
|
||||
"Authorization middleware, will be executed synchronously on vthread."
|
||||
[handler {:keys [::db/pool]}]
|
||||
(fn [{:keys [::http.auth/token-type] :as request}]
|
||||
(if (= :token token-type)
|
||||
(let [{:keys [perms profile-id expires-at]} (some->> (get request ::http.auth/claims)
|
||||
(get-token-data pool))]
|
||||
(handler (cond-> request
|
||||
(some? perms)
|
||||
(assoc ::perms perms)
|
||||
(some? profile-id)
|
||||
(assoc ::profile-id profile-id)
|
||||
(some? expires-at)
|
||||
(assoc ::expires-at expires-at))))
|
||||
(fn [request]
|
||||
(let [{:keys [perms profile-id expires-at]} (some->> (::id request) (get-token-data pool))]
|
||||
(handler (cond-> request
|
||||
(some? perms)
|
||||
(assoc ::perms perms)
|
||||
(some? profile-id)
|
||||
(assoc ::profile-id profile-id)
|
||||
(some? expires-at)
|
||||
(assoc ::expires-at expires-at))))))
|
||||
|
||||
(handler request))))
|
||||
(def soft-auth
|
||||
{:name ::soft-auth
|
||||
:compile (fn [& _]
|
||||
(when (contains? cf/flags :access-tokens)
|
||||
wrap-soft-auth))})
|
||||
|
||||
(def authz
|
||||
{:name ::authz
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
[app.config :as cf]
|
||||
[app.http :as-alias http]
|
||||
[app.http.access-token :as-alias actoken]
|
||||
[app.http.auth :as-alias auth]
|
||||
[app.http.session :as-alias session]
|
||||
[app.util.inet :as inet]
|
||||
[clojure.spec.alpha :as s]
|
||||
@@ -23,14 +22,16 @@
|
||||
(defn request->context
|
||||
"Extracts error report relevant context data from request."
|
||||
[request]
|
||||
(let [claims (some-> (get request ::auth/claims) deref)]
|
||||
(let [claims (-> {}
|
||||
(into (::session/token-claims request))
|
||||
(into (::actoken/token-claims request)))]
|
||||
(-> (cf/logging-context)
|
||||
(assoc :request/path (:path request))
|
||||
(assoc :request/method (:method request))
|
||||
(assoc :request/params (:params request))
|
||||
(assoc :request/user-agent (yreq/get-header request "user-agent"))
|
||||
(assoc :request/ip-addr (inet/parse-request request))
|
||||
(assoc :request/profile-id (get claims :uid))
|
||||
(assoc :request/profile-id (:uid claims))
|
||||
(assoc :version/frontend (or (yreq/get-header request "x-frontend-version") "unknown")))))
|
||||
|
||||
(defmulti handle-error
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
[app.common.time :as ct]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.http.middleware :as mw]
|
||||
[app.http.access-token :refer [get-token]]
|
||||
[app.main :as-alias main]
|
||||
[app.rpc.commands.profile :as cmd.profile]
|
||||
[app.setup :as-alias setup]
|
||||
@@ -32,6 +32,20 @@
|
||||
[_ params]
|
||||
(assert (db/pool? (::db/pool params)) "expect valid database pool"))
|
||||
|
||||
(def ^:private auth
|
||||
{:name ::auth
|
||||
:compile
|
||||
(fn [_ _]
|
||||
(fn [handler shared-key]
|
||||
(if shared-key
|
||||
(fn [request]
|
||||
(let [token (get-token request)]
|
||||
(if (= token shared-key)
|
||||
(handler request)
|
||||
{::yres/status 403})))
|
||||
(fn [_ _]
|
||||
{::yres/status 403}))))})
|
||||
|
||||
(def ^:private default-system
|
||||
{:name ::default-system
|
||||
:compile
|
||||
@@ -51,7 +65,7 @@
|
||||
|
||||
(defmethod ig/init-key ::routes
|
||||
[_ cfg]
|
||||
["" {:middleware [[mw/shared-key-auth (cf/get :management-api-shared-key)]
|
||||
["" {:middleware [[auth (cf/get :management-api-shared-key)]
|
||||
[default-system cfg]
|
||||
[transaction]]}
|
||||
["/authenticate"
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
[app.common.schema :as-alias sm]
|
||||
[app.common.transit :as t]
|
||||
[app.config :as cf]
|
||||
[app.http.auth :as-alias auth]
|
||||
[app.http.errors :as errors]
|
||||
[app.util.pointer-map :as pmap]
|
||||
[cuerdas.core :as str]
|
||||
@@ -241,61 +240,3 @@
|
||||
(if (contains? allowed method)
|
||||
(handler request)
|
||||
{::yres/status 405}))))))})
|
||||
|
||||
(defn- wrap-auth
|
||||
[handler decoders]
|
||||
(let [token-re
|
||||
#"(?i)^(Token|Bearer)\s+(.*)"
|
||||
|
||||
get-token-from-authorization
|
||||
(fn [request]
|
||||
(when-let [[_ token-type token] (some->> (yreq/get-header request "authorization")
|
||||
(re-matches token-re))]
|
||||
(if (= "token" (str/lower token-type))
|
||||
[:token token]
|
||||
[:bearer token])))
|
||||
|
||||
get-token-from-cookie
|
||||
(fn [request]
|
||||
(let [cname (cf/get :auth-token-cookie-name)
|
||||
token (some-> (yreq/get-cookie request cname) :value)]
|
||||
(when-not (str/empty? token)
|
||||
[:cookie token])))
|
||||
|
||||
get-token
|
||||
(some-fn get-token-from-cookie get-token-from-authorization)
|
||||
|
||||
process-request
|
||||
(fn [request]
|
||||
(if-let [[token-type token] (get-token request)]
|
||||
(let [request (-> request
|
||||
(assoc ::auth/token token)
|
||||
(assoc ::auth/token-type token-type))
|
||||
decoder (get decoders token-type)]
|
||||
|
||||
(if (fn? decoder)
|
||||
(assoc request ::auth/claims (delay (decoder token)))
|
||||
request))
|
||||
request))]
|
||||
|
||||
(fn [request]
|
||||
(-> request process-request handler))))
|
||||
|
||||
(def auth
|
||||
{:name ::auth
|
||||
:compile (constantly wrap-auth)})
|
||||
|
||||
(defn- wrap-shared-key-auth
|
||||
[handler shared-key]
|
||||
(if shared-key
|
||||
(fn [request]
|
||||
(let [key (yreq/get-header request "x-shared-key")]
|
||||
(if (= key shared-key)
|
||||
(handler request)
|
||||
{::yres/status 403})))
|
||||
(fn [_ _]
|
||||
{::yres/status 403})))
|
||||
|
||||
(def shared-key-auth
|
||||
{:name ::shared-key-auth
|
||||
:compile (constantly wrap-shared-key-auth)})
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as sql]
|
||||
[app.http.auth :as-alias http.auth]
|
||||
[app.http.session.tasks :as-alias tasks]
|
||||
[app.main :as-alias main]
|
||||
[app.setup :as-alias setup]
|
||||
@@ -27,6 +26,13 @@
|
||||
;; DEFAULTS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; A default cookie name for storing the session.
|
||||
(def default-auth-token-cookie-name "auth-token")
|
||||
|
||||
;; A cookie that we can use to check from other sites of the same
|
||||
;; domain if a user is authenticated.
|
||||
(def default-auth-data-cookie-name "auth-data")
|
||||
|
||||
;; Default value for cookie max-age
|
||||
(def default-cookie-max-age (ct/duration {:days 7}))
|
||||
|
||||
@@ -163,7 +169,7 @@
|
||||
[{:keys [::manager]}]
|
||||
(assert (manager? manager) "expected valid session manager")
|
||||
(fn [request response]
|
||||
(let [cname (cf/get :auth-token-cookie-name)
|
||||
(let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name)
|
||||
cookie (yreq/get-cookie request cname)]
|
||||
(l/trc :hint "delete" :profile-id (:profile-id request))
|
||||
(some->> (:value cookie) (delete! manager))
|
||||
@@ -177,14 +183,21 @@
|
||||
(tokens/generate cfg {:iss "authentication"
|
||||
:iat created-at
|
||||
:uid profile-id}))
|
||||
(defn decode-token
|
||||
(defn- decode-token
|
||||
[cfg token]
|
||||
(try
|
||||
(tokens/verify cfg {:token token :iss "authentication"})
|
||||
(catch Throwable cause
|
||||
(l/trc :hint "exception on decoding token"
|
||||
:token token
|
||||
:cause cause))))
|
||||
(when token
|
||||
(tokens/verify cfg {:token token :iss "authentication"})))
|
||||
|
||||
(defn- get-token
|
||||
[request]
|
||||
(let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name)
|
||||
cookie (some-> (yreq/get-cookie request cname) :value)]
|
||||
(when-not (str/empty? cookie)
|
||||
cookie)))
|
||||
|
||||
(defn- get-session
|
||||
[manager token]
|
||||
(some->> token (read manager)))
|
||||
|
||||
(defn- renew-session?
|
||||
[{:keys [updated-at] :as session}]
|
||||
@@ -192,38 +205,44 @@
|
||||
(let [elapsed (ct/diff updated-at (ct/now))]
|
||||
(neg? (compare default-renewal-max-age elapsed)))))
|
||||
|
||||
(defn- wrap-authz
|
||||
(defn- wrap-soft-auth
|
||||
[handler {:keys [::manager] :as cfg}]
|
||||
(assert (manager? manager) "expected valid session manager")
|
||||
(fn [{:keys [::http.auth/token-type] :as request}]
|
||||
(cond
|
||||
(= token-type :cookie)
|
||||
(let [session (some->> (get request ::http.auth/token)
|
||||
(read manager))
|
||||
request (cond-> request
|
||||
(some? session)
|
||||
(-> (assoc ::profile-id (:profile-id session))
|
||||
(assoc ::id (:id session))))
|
||||
(letfn [(handle-request [request]
|
||||
(try
|
||||
(let [token (get-token request)
|
||||
claims (decode-token cfg token)]
|
||||
(cond-> request
|
||||
(map? claims)
|
||||
(-> (assoc ::token-claims claims)
|
||||
(assoc ::token token))))
|
||||
(catch Throwable cause
|
||||
(l/trc :hint "exception on decoding malformed token" :cause cause)
|
||||
request)))]
|
||||
|
||||
response (handler request)]
|
||||
(fn [request]
|
||||
(handler (handle-request request)))))
|
||||
|
||||
(if (renew-session? session)
|
||||
(let [session (update! manager session)]
|
||||
(-> response
|
||||
(assign-auth-token-cookie session)))
|
||||
response))
|
||||
(defn- wrap-authz
|
||||
[handler {:keys [::manager]}]
|
||||
(assert (manager? manager) "expected valid session manager")
|
||||
(fn [request]
|
||||
(let [session (get-session manager (::token request))
|
||||
request (cond-> request
|
||||
(some? session)
|
||||
(assoc ::profile-id (:profile-id session)
|
||||
::id (:id session)))
|
||||
response (handler request)]
|
||||
|
||||
(= token-type :bearer)
|
||||
(let [session (some->> (get request ::http.auth/token)
|
||||
(read manager))
|
||||
request (cond-> request
|
||||
(some? session)
|
||||
(-> (assoc ::profile-id (:profile-id session))
|
||||
(assoc ::id (:id session))))]
|
||||
(handler request))
|
||||
(if (renew-session? session)
|
||||
(let [session (update! manager session)]
|
||||
(-> response
|
||||
(assign-auth-token-cookie session)))
|
||||
response))))
|
||||
|
||||
:else
|
||||
(handler request))))
|
||||
(def soft-auth
|
||||
{:name ::soft-auth
|
||||
:compile (constantly wrap-soft-auth)})
|
||||
|
||||
(def authz
|
||||
{:name ::authz
|
||||
@@ -240,7 +259,7 @@
|
||||
secure? (contains? cf/flags :secure-session-cookies)
|
||||
strict? (contains? cf/flags :strict-session-cookies)
|
||||
cors? (contains? cf/flags :cors)
|
||||
name (cf/get :auth-token-cookie-name)
|
||||
name (cf/get :auth-token-cookie-name default-auth-token-cookie-name)
|
||||
comment (str "Renewal at: " (ct/format-inst renewal :rfc1123))
|
||||
cookie {:path "/"
|
||||
:http-only true
|
||||
@@ -253,7 +272,7 @@
|
||||
|
||||
(defn- clear-auth-token-cookie
|
||||
[response]
|
||||
(let [cname (cf/get :auth-token-cookie-name)]
|
||||
(let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name)]
|
||||
(update response :cookies assoc cname {:path "/" :value "" :max-age 0})))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
[app.http.client :as-alias http.client]
|
||||
[app.http.debug :as-alias http.debug]
|
||||
[app.http.management :as mgmt]
|
||||
[app.http.session :as session]
|
||||
[app.http.session :as-alias session]
|
||||
[app.http.session.tasks :as-alias session.tasks]
|
||||
[app.http.websocket :as http.ws]
|
||||
[app.loggers.webhooks :as-alias webhooks]
|
||||
@@ -31,6 +31,7 @@
|
||||
[app.redis :as-alias rds]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.climit :as-alias climit]
|
||||
[app.rpc.doc :as-alias rpc.doc]
|
||||
[app.setup :as-alias setup]
|
||||
[app.srepl :as-alias srepl]
|
||||
[app.storage :as-alias sto]
|
||||
@@ -279,6 +280,7 @@
|
||||
{::session/manager (ig/ref ::session/manager)
|
||||
::db/pool (ig/ref ::db/pool)
|
||||
::rpc/routes (ig/ref ::rpc/routes)
|
||||
::rpc.doc/routes (ig/ref ::rpc.doc/routes)
|
||||
::setup/props (ig/ref ::setup/props)
|
||||
::mtx/routes (ig/ref ::mtx/routes)
|
||||
::oidc/routes (ig/ref ::oidc/routes)
|
||||
@@ -319,7 +321,6 @@
|
||||
{::http.client/client (ig/ref ::http.client/client)
|
||||
::db/pool (ig/ref ::db/pool)
|
||||
::rds/pool (ig/ref ::rds/pool)
|
||||
:app.nitrate/instance (ig/ref :app.nitrate/instance)
|
||||
::wrk/executor (ig/ref ::wrk/netty-executor)
|
||||
::session/manager (ig/ref ::session/manager)
|
||||
::ldap/provider (ig/ref ::ldap/provider)
|
||||
@@ -336,29 +337,14 @@
|
||||
::email/blacklist (ig/ref ::email/blacklist)
|
||||
::email/whitelist (ig/ref ::email/whitelist)}
|
||||
|
||||
:app.nitrate/instance
|
||||
{::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)
|
||||
::rds/pool (ig/ref ::rds/pool)
|
||||
::wrk/executor (ig/ref ::wrk/netty-executor)
|
||||
::session/manager (ig/ref ::session/manager)
|
||||
::sto/storage (ig/ref ::sto/storage)
|
||||
::mtx/metrics (ig/ref ::mtx/metrics)
|
||||
::mbus/msgbus (ig/ref ::mbus/msgbus)
|
||||
::rds/client (ig/ref ::rds/client)
|
||||
::setup/props (ig/ref ::setup/props)}
|
||||
:app.rpc.doc/routes
|
||||
{:app.rpc/methods (ig/ref :app.rpc/methods)}
|
||||
|
||||
::rpc/routes
|
||||
{::rpc/methods (ig/ref :app.rpc/methods)
|
||||
::rpc/management-methods (ig/ref :app.rpc/management-methods)
|
||||
|
||||
;; FIXME: revisit if db/pool is necessary here
|
||||
::db/pool (ig/ref ::db/pool)
|
||||
::session/manager (ig/ref ::session/manager)
|
||||
::setup/props (ig/ref ::setup/props)}
|
||||
{::rpc/methods (ig/ref :app.rpc/methods)
|
||||
::db/pool (ig/ref ::db/pool)
|
||||
::session/manager (ig/ref ::session/manager)
|
||||
::setup/props (ig/ref ::setup/props)}
|
||||
|
||||
::wrk/registry
|
||||
{::mtx/metrics (ig/ref ::mtx/metrics)
|
||||
|
||||
@@ -13,14 +13,11 @@
|
||||
[app.common.schema :as sm]
|
||||
[app.common.spec :as us]
|
||||
[app.common.time :as ct]
|
||||
[app.common.uri :as u]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.http :as-alias http]
|
||||
[app.http.access-token :as actoken]
|
||||
[app.http.client :as-alias http.client]
|
||||
[app.http.middleware :as mw]
|
||||
[app.http.security :as sec]
|
||||
[app.http.session :as session]
|
||||
[app.loggers.audit :as audit]
|
||||
[app.main :as-alias main]
|
||||
@@ -29,7 +26,6 @@
|
||||
[app.redis :as rds]
|
||||
[app.rpc.climit :as climit]
|
||||
[app.rpc.cond :as cond]
|
||||
[app.rpc.doc :as doc]
|
||||
[app.rpc.helpers :as rph]
|
||||
[app.rpc.retry :as retry]
|
||||
[app.rpc.rlimit :as rlimit]
|
||||
@@ -40,6 +36,7 @@
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig]
|
||||
[promesa.core :as p]
|
||||
[yetti.request :as yreq]
|
||||
[yetti.response :as yres]))
|
||||
|
||||
@@ -47,7 +44,7 @@
|
||||
|
||||
(defn- default-handler
|
||||
[_]
|
||||
(ex/raise :type :not-found))
|
||||
(p/rejected (ex/error :type :not-found)))
|
||||
|
||||
(defn- handle-response-transformation
|
||||
[response request mdata]
|
||||
@@ -95,46 +92,43 @@
|
||||
(str/blank? origin))
|
||||
origin)))
|
||||
|
||||
(defn- make-rpc-handler
|
||||
(defn- rpc-handler
|
||||
"Ring handler that dispatches cmd requests and convert between
|
||||
internal async flow into ring async flow."
|
||||
[methods]
|
||||
(let [methods (update-vals methods peek)]
|
||||
(fn [{:keys [params path-params method] :as request}]
|
||||
(let [handler-name (:type path-params)
|
||||
etag (yreq/get-header request "if-none-match")
|
||||
profile-id (or (::session/profile-id request)
|
||||
(::actoken/profile-id request))
|
||||
[methods {:keys [params path-params method] :as request}]
|
||||
(let [handler-name (:type path-params)
|
||||
etag (yreq/get-header request "if-none-match")
|
||||
profile-id (or (::session/profile-id request)
|
||||
(::actoken/profile-id request))
|
||||
|
||||
ip-addr (inet/parse-request request)
|
||||
session-id (get-external-session-id request)
|
||||
event-origin (get-external-event-origin request)
|
||||
ip-addr (inet/parse-request request)
|
||||
session-id (get-external-session-id request)
|
||||
event-origin (get-external-event-origin request)
|
||||
|
||||
data (-> params
|
||||
(assoc ::handler-name handler-name)
|
||||
(assoc ::ip-addr ip-addr)
|
||||
(assoc ::request-at (ct/now))
|
||||
(assoc ::external-session-id session-id)
|
||||
(assoc ::external-event-origin event-origin)
|
||||
(assoc ::session/id (::session/id request))
|
||||
(assoc ::cond/key etag)
|
||||
(cond-> (uuid? profile-id)
|
||||
(assoc ::profile-id profile-id)))
|
||||
data (-> params
|
||||
(assoc ::handler-name handler-name)
|
||||
(assoc ::ip-addr ip-addr)
|
||||
(assoc ::request-at (ct/now))
|
||||
(assoc ::external-session-id session-id)
|
||||
(assoc ::external-event-origin event-origin)
|
||||
(assoc ::session/id (::session/id request))
|
||||
(assoc ::cond/key etag)
|
||||
(cond-> (uuid? profile-id)
|
||||
(assoc ::profile-id profile-id)))
|
||||
|
||||
data (vary-meta data assoc ::http/request request)
|
||||
handler-fn (get methods (keyword handler-name) default-handler)]
|
||||
data (vary-meta data assoc ::http/request request)
|
||||
handler-fn (get methods (keyword handler-name) default-handler)]
|
||||
|
||||
(when (and (or (= method :get)
|
||||
(= method :head))
|
||||
(not (str/starts-with? handler-name "get-")))
|
||||
(ex/raise :type :restriction
|
||||
:code :method-not-allowed
|
||||
:hint "method not allowed for this request"))
|
||||
(when (and (or (= method :get)
|
||||
(= method :head))
|
||||
(not (str/starts-with? handler-name "get-")))
|
||||
(ex/raise :type :restriction
|
||||
:code :method-not-allowed
|
||||
:hint "method not allowed for this request"))
|
||||
|
||||
;; FIXME: why we have this cond enabled here, we need to move it outside this handler
|
||||
(binding [cond/*enabled* true]
|
||||
(let [response (handler-fn data)]
|
||||
(handle-response request response)))))))
|
||||
(binding [cond/*enabled* true]
|
||||
(let [response (handler-fn data)]
|
||||
(handle-response request response)))))
|
||||
|
||||
(defn- wrap-metrics
|
||||
"Wrap service method with metrics measurement."
|
||||
@@ -211,7 +205,7 @@
|
||||
::sm/explain (explain params)))))))
|
||||
f))
|
||||
|
||||
(defn- wrap
|
||||
(defn- wrap-all
|
||||
[cfg f mdata]
|
||||
(as-> f $
|
||||
(wrap-db-transaction cfg $ mdata)
|
||||
@@ -225,30 +219,17 @@
|
||||
(wrap-params-validation cfg $ mdata)
|
||||
(wrap-authentication cfg $ mdata)))
|
||||
|
||||
(defn- wrap-management
|
||||
(defn- wrap
|
||||
[cfg f mdata]
|
||||
(as-> f $
|
||||
(wrap-db-transaction cfg $ mdata)
|
||||
(retry/wrap-retry cfg $ mdata)
|
||||
(climit/wrap cfg $ mdata)
|
||||
(wrap-metrics cfg $ mdata)
|
||||
(wrap-audit cfg $ mdata)
|
||||
(wrap-spec-conform cfg $ mdata)
|
||||
(wrap-params-validation cfg $ mdata)
|
||||
(wrap-authentication cfg $ mdata)))
|
||||
(l/trc :hint "register method" :name (::sv/name mdata))
|
||||
(let [f (wrap-all cfg f mdata)]
|
||||
(partial f cfg)))
|
||||
|
||||
(defn- process-method
|
||||
[cfg module wrap-fn [f mdata]]
|
||||
(l/trc :hint "add method" :module module :name (::sv/name mdata))
|
||||
(let [f (wrap-fn cfg f mdata)
|
||||
k (keyword (::sv/name mdata))]
|
||||
[k [mdata (partial f cfg)]]))
|
||||
[cfg [vfn mdata]]
|
||||
[(keyword (::sv/name mdata)) [mdata (wrap cfg vfn mdata)]])
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; API METHODS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- resolve-methods
|
||||
(defn- resolve-command-methods
|
||||
[cfg]
|
||||
(let [cfg (assoc cfg ::type "command" ::metrics-id :rpc-command-timing)]
|
||||
(->> (sv/scan-ns
|
||||
@@ -277,7 +258,7 @@
|
||||
'app.rpc.commands.verify-token
|
||||
'app.rpc.commands.viewer
|
||||
'app.rpc.commands.webhooks)
|
||||
(map (partial process-method cfg "rpc" wrap))
|
||||
(map (partial process-method cfg))
|
||||
(into {}))))
|
||||
|
||||
(def ^:private schema:methods-params
|
||||
@@ -301,49 +282,7 @@
|
||||
(defmethod ig/init-key ::methods
|
||||
[_ cfg]
|
||||
(let [cfg (d/without-nils cfg)]
|
||||
(resolve-methods cfg)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; MANAGEMENT METHODS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- resolve-management-methods
|
||||
[cfg]
|
||||
(let [cfg (assoc cfg ::type "management" ::metrics-id :rpc-management-timing)]
|
||||
(->> (sv/scan-ns
|
||||
'app.rpc.management.subscription)
|
||||
(map (partial process-method cfg "management" wrap-management))
|
||||
(into {}))))
|
||||
|
||||
(def ^:private schema:management-methods-params
|
||||
[:map {:title "management-methods-params"}
|
||||
::session/manager
|
||||
::http.client/client
|
||||
::db/pool
|
||||
::rds/pool
|
||||
::mbus/msgbus
|
||||
::sto/storage
|
||||
::mtx/metrics
|
||||
::setup/props])
|
||||
|
||||
(defmethod ig/assert-key ::management-methods
|
||||
[_ params]
|
||||
(assert (sm/check schema:management-methods-params params)))
|
||||
|
||||
(defmethod ig/init-key ::management-methods
|
||||
[_ cfg]
|
||||
(let [cfg (d/without-nils cfg)]
|
||||
(resolve-management-methods cfg)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; ROUTES
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- redirect
|
||||
[href]
|
||||
(fn [_]
|
||||
{::yres/status 308
|
||||
::yres/headers {"location" (str href)}}))
|
||||
(resolve-command-methods cfg)))
|
||||
|
||||
(def ^:private schema:methods
|
||||
[:map-of :keyword [:tuple :map ::sm/fn]])
|
||||
@@ -358,49 +297,11 @@
|
||||
(assert (db/pool? (::db/pool params)) "expect valid database pool")
|
||||
(assert (some? (::setup/props params)))
|
||||
(assert (session/manager? (::session/manager params)) "expect valid session manager")
|
||||
(assert (valid-methods? (::methods params)) "expect valid methods map")
|
||||
(assert (valid-methods? (::management-methods params)) "expect valid methods map"))
|
||||
(assert (valid-methods? (::methods params)) "expect valid methods map"))
|
||||
|
||||
(defmethod ig/init-key ::routes
|
||||
[_ {:keys [::methods ::management-methods] :as cfg}]
|
||||
|
||||
(let [public-uri (cf/get :public-uri)]
|
||||
["/api"
|
||||
|
||||
|
||||
["/management"
|
||||
["/methods/:type"
|
||||
{:middleware [[mw/shared-key-auth (cf/get :management-api-shared-key)]
|
||||
[session/authz cfg]]
|
||||
:handler (make-rpc-handler management-methods)}]
|
||||
|
||||
(doc/routes :methods management-methods
|
||||
:label "management"
|
||||
:base-uri (u/join public-uri "/api/management")
|
||||
:description "MANAGEMENT API")]
|
||||
|
||||
["/main"
|
||||
["/methods/:type"
|
||||
{:middleware [[mw/cors]
|
||||
[sec/client-header-check]
|
||||
[session/authz cfg]
|
||||
[actoken/authz cfg]]
|
||||
:handler (make-rpc-handler methods)}]
|
||||
|
||||
(doc/routes :methods methods
|
||||
:label "main"
|
||||
:base-uri (u/join public-uri "/api/main")
|
||||
:description "MAIN API")]
|
||||
|
||||
;; BACKWARD COMPATIBILITY
|
||||
["/_doc" {:handler (redirect (u/join public-uri "/api/main/doc"))}]
|
||||
["/doc" {:handler (redirect (u/join public-uri "/api/main/doc"))}]
|
||||
["/openapi" {:handler (redirect (u/join public-uri "/api/main/doc/openapi"))}]
|
||||
["/openapi.join" {:handler (redirect (u/join public-uri "/api/main/doc/openapi.json"))}]
|
||||
|
||||
["/rpc/command/:type"
|
||||
{:middleware [[mw/cors]
|
||||
[sec/client-header-check]
|
||||
[session/authz cfg]
|
||||
[actoken/authz cfg]]
|
||||
:handler (make-rpc-handler methods)}]]))
|
||||
[_ {:keys [::methods] :as cfg}]
|
||||
(let [methods (update-vals methods peek)]
|
||||
[["/rpc" {:middleware [[session/authz cfg]
|
||||
[actoken/authz cfg]]}
|
||||
["/command/:type" {:handler (partial rpc-handler methods)}]]]))
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
expires-at (some-> expiration (ct/in-future))
|
||||
created-at (ct/now)
|
||||
token (tokens/generate cfg {:iss "access-token"
|
||||
:uid profile-id
|
||||
:iat created-at
|
||||
:tid token-id})
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
[app.db.sql :as-alias sql]
|
||||
[app.email :as eml]
|
||||
[app.http.session :as session]
|
||||
[app.nitrate :as nitrate]
|
||||
[app.loggers.audit :as audit]
|
||||
[app.main :as-alias main]
|
||||
[app.media :as media]
|
||||
@@ -99,14 +98,9 @@
|
||||
;; no profile-id is in session, and when db call raises not found. In all other
|
||||
;; cases we need to reraise the exception.
|
||||
(try
|
||||
(let [nitrate (get cfg ::nitrate/instance)
|
||||
|
||||
;; org ((get nitrate :get-organization) profile-id)
|
||||
;; org (nitrate/call cfg :get-organization {:profile-id profile-id})
|
||||
|
||||
(-> (get-profile pool profile-id)
|
||||
(strip-private-attrs)
|
||||
(update :props filter-props)))
|
||||
(-> (get-profile pool profile-id)
|
||||
(strip-private-attrs)
|
||||
(update :props filter-props))
|
||||
(catch Throwable _
|
||||
{:id uuid/zero :fullname "Anonymous User"})))
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
[app.common.schema.desc-native :as smdn]
|
||||
[app.common.schema.openapi :as oapi]
|
||||
[app.common.schema.registry :as sr]
|
||||
[app.common.uri :as u]
|
||||
[app.config :as cf]
|
||||
[app.http.sse :as-alias sse]
|
||||
[app.loggers.webhooks :as-alias webhooks]
|
||||
@@ -26,6 +25,7 @@
|
||||
[clojure.java.io :as io]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig]
|
||||
[pretty-spec.core :as ps]
|
||||
[yetti.response :as-alias yres]))
|
||||
|
||||
@@ -33,8 +33,8 @@
|
||||
;; DOC (human readable)
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- context
|
||||
[{:keys [methods entrypoint label openapi]}]
|
||||
(defn- prepare-doc-context
|
||||
[methods]
|
||||
(letfn [(fmt-spec [mdata]
|
||||
(when-let [spec (ex/ignoring (s/spec (::sv/spec mdata)))]
|
||||
(with-out-str
|
||||
@@ -62,10 +62,8 @@
|
||||
:added (::added mdata)
|
||||
:changes (some->> (::changes mdata) (partition-all 2) (map vec))
|
||||
:spec (fmt-spec mdata)
|
||||
:entrypoint (-> entrypoint
|
||||
(u/ensure-path-slash)
|
||||
(u/join (::sv/name mdata))
|
||||
(str))
|
||||
:entrypoint (str (cf/get :public-uri) "/api/rpc/command/" (::sv/name mdata))
|
||||
|
||||
:params-schema-js (fmt-schema :js mdata ::sm/params)
|
||||
:result-schema-js (fmt-schema :js mdata ::sm/result)
|
||||
:webhook-schema-js (fmt-schema :js mdata ::sm/webhook)
|
||||
@@ -74,9 +72,6 @@
|
||||
:webhook-schema-clj (fmt-schema :clj mdata ::sm/webhook)})]
|
||||
|
||||
{:version (:main cf/version)
|
||||
:label label
|
||||
:entrypoint (str entrypoint)
|
||||
:openapi (str openapi)
|
||||
:methods
|
||||
(->> methods
|
||||
(map val)
|
||||
@@ -85,19 +80,17 @@
|
||||
(map get-context)
|
||||
(sort-by (juxt :module :name)))}))
|
||||
|
||||
(defn- handler
|
||||
[& {:keys [template] :as options}]
|
||||
(defn- doc-handler
|
||||
[context]
|
||||
(if (contains? cf/flags :backend-api-doc)
|
||||
(let [context (delay (context options))
|
||||
template (or template "app/templates/api-doc.tmpl")]
|
||||
(fn [request]
|
||||
(let [params (:query-params request)
|
||||
pstyle (:type params "js")
|
||||
context (assoc @context :param-style pstyle)]
|
||||
(fn [request]
|
||||
(let [params (:query-params request)
|
||||
pstyle (:type params "js")
|
||||
context (assoc @context :param-style pstyle)]
|
||||
|
||||
{::yres/status 200
|
||||
::yres/body (-> (io/resource template)
|
||||
(tmpl/render context))})))
|
||||
{::yres/status 200
|
||||
::yres/body (-> (io/resource "app/templates/api-doc.tmpl")
|
||||
(tmpl/render context))}))
|
||||
(fn [_]
|
||||
{::yres/status 404})))
|
||||
|
||||
@@ -105,8 +98,8 @@
|
||||
;; OPENAPI / SWAGGER (v3.1)
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- openapi-context
|
||||
[{:keys [methods entrypoint description]}]
|
||||
(defn prepare-openapi-context
|
||||
[methods]
|
||||
(let [definitions (atom {})
|
||||
options {:registry sr/default-registry
|
||||
::oapi/definitions-path "#/components/schemas/"
|
||||
@@ -119,9 +112,7 @@
|
||||
(fn [tsx schema]
|
||||
(let [schema (sm/schema schema)
|
||||
example (sm/generate schema)
|
||||
example (sm/encode schema example output-transformer)
|
||||
example (json/encode example :key-fn json/write-camel-key)]
|
||||
|
||||
example (sm/encode schema example output-transformer)]
|
||||
{:default
|
||||
{:description "A default response"
|
||||
:content
|
||||
@@ -132,9 +123,7 @@
|
||||
gen-params-doc
|
||||
(fn [tsx schema]
|
||||
(let [example (sm/generate schema)
|
||||
example (sm/encode schema example output-transformer)
|
||||
example (json/encode example :key-fn json/write-camel-key)]
|
||||
|
||||
example (sm/encode schema example output-transformer)]
|
||||
{:required true
|
||||
:content
|
||||
{"application/json"
|
||||
@@ -169,35 +158,34 @@
|
||||
(map gen-method-doc)
|
||||
(sort-by (juxt :module :name))
|
||||
(map (fn [doc]
|
||||
[(:name doc) (:repr doc)]))
|
||||
[(str/ffmt "/command/%" (:name doc)) (:repr doc)]))
|
||||
(into {})))]
|
||||
|
||||
{:openapi "3.0.0"
|
||||
:info {:version (:main cf/version)}
|
||||
:servers [{:url (str entrypoint)
|
||||
:description (or description "")}]
|
||||
:servers [{:url (str/ffmt "%/api/rpc" (cf/get :public-uri))
|
||||
;; :description "penpot backend"
|
||||
}]
|
||||
:paths paths
|
||||
:components {:schemas @definitions}}))
|
||||
|
||||
(defn- openapi-json-handler
|
||||
[& {:as options}]
|
||||
(defn openapi-json-handler
|
||||
[context]
|
||||
(if (contains? cf/flags :backend-openapi-doc)
|
||||
(let [context (delay (openapi-context options))]
|
||||
(fn [_]
|
||||
{::yres/status 200
|
||||
::yres/headers {"content-type" "application/json; charset=utf-8"}
|
||||
::yres/body (json/encode @context)}))
|
||||
(fn [_]
|
||||
{::yres/status 200
|
||||
::yres/headers {"content-type" "application/json; charset=utf-8"}
|
||||
::yres/body (json/encode @context)})
|
||||
(fn [_]
|
||||
{::yres/status 404})))
|
||||
|
||||
(defn- openapi-handler
|
||||
[& {:keys [uri label]}]
|
||||
(defn openapi-handler
|
||||
[]
|
||||
(if (contains? cf/flags :backend-openapi-doc)
|
||||
(fn [_]
|
||||
(let [swagger-js (slurp (io/resource "app/assets/swagger-ui-4.18.3.js"))
|
||||
swagger-cs (slurp (io/resource "app/assets/swagger-ui-4.18.3.css"))
|
||||
context {:uri (str uri)
|
||||
:label label
|
||||
context {:public-uri (cf/get :public-uri)
|
||||
:swagger-js swagger-js
|
||||
:swagger-css swagger-cs}]
|
||||
{::yres/status 200
|
||||
@@ -208,43 +196,27 @@
|
||||
{::yres/status 404})))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; ROUTES HELPER
|
||||
;; MODULE INIT
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn routes
|
||||
[& {:keys [label base-uri description methods]}]
|
||||
(let [entrypoint
|
||||
(-> base-uri
|
||||
(u/ensure-path-slash)
|
||||
(u/join "methods"))
|
||||
(defmethod ig/assert-key ::routes
|
||||
[_ params]
|
||||
(assert (sm/valid? ::rpc/methods (::rpc/methods params)) "expected valid methods"))
|
||||
|
||||
openapi
|
||||
(-> base-uri
|
||||
(u/ensure-path-slash)
|
||||
(u/join "doc/openapi"))
|
||||
(defmethod ig/init-key ::routes
|
||||
[_ {:keys [::rpc/methods] :as cfg}]
|
||||
[(let [context (delay (prepare-doc-context methods))]
|
||||
[["/_doc"
|
||||
{:handler (doc-handler context)
|
||||
:allowed-methods #{:get}}]
|
||||
["/doc"
|
||||
{:handler (doc-handler context)
|
||||
:allowed-methods #{:get}}]])
|
||||
|
||||
template
|
||||
(case label
|
||||
"management" "app/templates/management-api-doc.tmpl"
|
||||
"main" "app/templates/main-api-doc.tmpl")]
|
||||
|
||||
["/doc"
|
||||
["" {:handler (handler :methods methods
|
||||
:label label
|
||||
:entrypoint entrypoint
|
||||
:openapi openapi
|
||||
:template template)
|
||||
:allowed-methods #{:get}}]
|
||||
|
||||
["/openapi"
|
||||
{:handler (openapi-handler
|
||||
:uri (u/join openapi "openapi.json")
|
||||
:label label)
|
||||
:allowed-methods #{:get}}]
|
||||
|
||||
["/openapi.json"
|
||||
{:handler (openapi-json-handler {:entrypoint entrypoint
|
||||
:description description
|
||||
:methods methods})
|
||||
|
||||
:allowed-methods #{:get}}]]))
|
||||
(let [context (delay (prepare-openapi-context methods))]
|
||||
[["/openapi"
|
||||
{:handler (openapi-handler)
|
||||
:allowed-methods #{:get}}]
|
||||
["/openapi.json"
|
||||
{:handler (openapi-json-handler context)
|
||||
:allowed-methods #{:get}}]])])
|
||||
|
||||
@@ -1,183 +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.rpc.management.subscription
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.generators :as sg]
|
||||
[app.common.time :as ct]
|
||||
[app.db :as db]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.commands.profile :as profile]
|
||||
[app.rpc.doc :as doc]
|
||||
[app.util.services :as sv]))
|
||||
|
||||
;; ---- RPC METHOD: AUTHENTICATE
|
||||
|
||||
(def ^:private
|
||||
schema:authenticate-params
|
||||
[:map {:title "authenticate-params"}])
|
||||
|
||||
(def ^:private
|
||||
schema:authenticate-result
|
||||
[:map {:title "authenticate-result"}
|
||||
[:profile-id ::sm/uuid]])
|
||||
|
||||
(sv/defmethod ::auth
|
||||
{::doc/added "2.12"
|
||||
::sm/params schema:authenticate-params
|
||||
::sm/result schema:authenticate-result}
|
||||
[_ {:keys [::rpc/profile-id]}]
|
||||
{:profile-id profile-id})
|
||||
|
||||
;; ---- RPC METHOD: GET-CUSTOMER
|
||||
|
||||
;; FIXME: move to app.common.time
|
||||
(def ^:private schema:timestamp
|
||||
(sm/type-schema
|
||||
{:type ::timestamp
|
||||
:pred ct/inst?
|
||||
:type-properties
|
||||
{:title "inst"
|
||||
:description "The same as :app.common.time/inst but encodes to epoch"
|
||||
:error/message "should be an instant"
|
||||
:gen/gen (->> (sg/small-int)
|
||||
(sg/fmap (fn [v] (ct/inst v))))
|
||||
:decode/string #(some-> % ct/inst)
|
||||
:encode/string #(some-> % inst-ms)
|
||||
:decode/json #(some-> % ct/inst)
|
||||
:encode/json #(some-> % inst-ms)}}))
|
||||
|
||||
(def ^:private schema:subscription
|
||||
[:map {:title "Subscription"}
|
||||
[:id ::sm/text]
|
||||
[:customer-id ::sm/text]
|
||||
[:type [:enum
|
||||
"unlimited"
|
||||
"professional"
|
||||
"enterprise"]]
|
||||
[:status [:enum
|
||||
"active"
|
||||
"canceled"
|
||||
"incomplete"
|
||||
"incomplete_expired"
|
||||
"past_due"
|
||||
"paused"
|
||||
"trialing"
|
||||
"unpaid"]]
|
||||
|
||||
[:billing-period [:enum
|
||||
"month"
|
||||
"day"
|
||||
"week"
|
||||
"year"]]
|
||||
[:quantity :int]
|
||||
[:description [:maybe ::sm/text]]
|
||||
[:created-at schema:timestamp]
|
||||
[:start-date [:maybe schema:timestamp]]
|
||||
[:ended-at [:maybe schema:timestamp]]
|
||||
[:trial-end [:maybe schema:timestamp]]
|
||||
[:trial-start [:maybe schema:timestamp]]
|
||||
[:cancel-at [:maybe schema:timestamp]]
|
||||
[:canceled-at [:maybe schema:timestamp]]
|
||||
[:current-period-end [:maybe schema:timestamp]]
|
||||
[:current-period-start [:maybe schema:timestamp]]
|
||||
[:cancel-at-period-end :boolean]
|
||||
|
||||
[:cancellation-details
|
||||
[:map {:title "CancellationDetails"}
|
||||
[:comment [:maybe ::sm/text]]
|
||||
[:reason [:maybe ::sm/text]]
|
||||
[:feedback [:maybe
|
||||
[:enum
|
||||
"customer_service"
|
||||
"low_quality"
|
||||
"missing_feature"
|
||||
"other"
|
||||
"switched_service"
|
||||
"too_complex"
|
||||
"too_expensive"
|
||||
"unused"]]]]]])
|
||||
|
||||
(def ^:private sql:get-customer-slots
|
||||
"WITH teams AS (
|
||||
SELECT tpr.team_id AS id,
|
||||
tpr.profile_id AS profile_id
|
||||
FROM team_profile_rel AS tpr
|
||||
WHERE tpr.is_owner IS true
|
||||
AND tpr.profile_id = ?
|
||||
), teams_with_slots AS (
|
||||
SELECT tpr.team_id AS id,
|
||||
count(*) AS total
|
||||
FROM team_profile_rel AS tpr
|
||||
WHERE tpr.team_id IN (SELECT id FROM teams)
|
||||
AND tpr.can_edit IS true
|
||||
GROUP BY 1
|
||||
ORDER BY 2
|
||||
)
|
||||
SELECT max(total) AS total FROM teams_with_slots;")
|
||||
|
||||
(defn- get-customer-slots
|
||||
[cfg profile-id]
|
||||
(let [result (db/exec-one! cfg [sql:get-customer-slots profile-id])]
|
||||
(:total result)))
|
||||
|
||||
(def ^:private schema:get-customer-params
|
||||
[:map])
|
||||
|
||||
(def ^:private schema:get-customer-result
|
||||
[:map
|
||||
[:id ::sm/uuid]
|
||||
[:name :string]
|
||||
[:num-editors ::sm/int]
|
||||
[:subscription {:optional true} schema:subscription]])
|
||||
|
||||
(sv/defmethod ::get-customer
|
||||
{::doc/added "2.12"
|
||||
::sm/params schema:get-customer-params
|
||||
::sm/result schema:get-customer-result}
|
||||
[cfg {:keys [::rpc/profile-id]}]
|
||||
(let [profile (profile/get-profile cfg profile-id)]
|
||||
{:id (get profile :id)
|
||||
:name (get profile :fullname)
|
||||
:email (get profile :email)
|
||||
:num-editors (get-customer-slots cfg profile-id)
|
||||
:subscription (-> profile :props :subscription)}))
|
||||
|
||||
|
||||
;; ---- RPC METHOD: GET-CUSTOMER
|
||||
|
||||
(def ^:private schema:update-customer-params
|
||||
[:map
|
||||
[:subscription [:maybe schema:subscription]]])
|
||||
|
||||
(def ^:private schema:update-customer-result
|
||||
[:map])
|
||||
|
||||
(sv/defmethod ::update-customer
|
||||
{::doc/added "2.12"
|
||||
::sm/params schema:update-customer-params
|
||||
::sm/result schema:update-customer-result}
|
||||
[cfg {:keys [::rpc/profile-id subscription]}]
|
||||
(let [{:keys [props] :as profile}
|
||||
(profile/get-profile cfg profile-id ::db/for-update true)
|
||||
|
||||
props
|
||||
(assoc props :subscription subscription)]
|
||||
|
||||
(l/dbg :hint "update customer"
|
||||
:profile-id (str profile-id)
|
||||
:subscription-type (get subscription :type)
|
||||
:subscription-status (get subscription :status)
|
||||
:subscription-quantity (get subscription :quantity))
|
||||
|
||||
(db/update! cfg :profile
|
||||
{:props (db/tjson props)}
|
||||
{:id profile-id}
|
||||
{::db/return-keys false})
|
||||
|
||||
nil))
|
||||
@@ -218,9 +218,6 @@
|
||||
(when (or (nil? revn) (= revn (:revn file)))
|
||||
file)))
|
||||
|
||||
;; FIXME: we should skip files that does not match the revn on the
|
||||
;; props and add proper schema for this task props
|
||||
|
||||
(defn- process-file!
|
||||
[cfg {:keys [file-id] :as props}]
|
||||
(if-let [file (get-file cfg props)]
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"A maintenance task that is responsible of properly scheduling the
|
||||
file-gc task for all files that matches the eligibility threshold."
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.common.time :as ct]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
@@ -22,24 +21,25 @@
|
||||
f.modified_at
|
||||
FROM file AS f
|
||||
WHERE f.has_media_trimmed IS false
|
||||
AND f.modified_at < ?
|
||||
AND f.modified_at < now() - ?::interval
|
||||
AND f.deleted_at IS NULL
|
||||
ORDER BY f.modified_at DESC
|
||||
FOR UPDATE OF f
|
||||
SKIP LOCKED")
|
||||
|
||||
(defn- get-candidates
|
||||
[{:keys [::db/conn ::min-age] :as cfg}]
|
||||
(let [min-age (db/interval min-age)]
|
||||
(db/plan conn [sql:get-candidates min-age] {:fetch-size 10})))
|
||||
|
||||
(defn- schedule!
|
||||
[{:keys [::db/conn] :as cfg} threshold]
|
||||
[cfg]
|
||||
(let [total (reduce (fn [total {:keys [id modified-at revn]}]
|
||||
(let [params {:file-id id :revn revn}]
|
||||
(l/trc :hint "schedule"
|
||||
:file-id (str id)
|
||||
:revn revn
|
||||
:modified-at (ct/format-inst modified-at))
|
||||
(let [params {:file-id id :modified-at modified-at :revn revn}]
|
||||
(wrk/submit! (assoc cfg ::wrk/params params))
|
||||
(inc total)))
|
||||
0
|
||||
(db/plan conn [sql:get-candidates threshold] {:fetch-size 10}))]
|
||||
(get-candidates cfg))]
|
||||
{:processed total}))
|
||||
|
||||
(defmethod ig/assert-key ::handler
|
||||
@@ -53,12 +53,12 @@
|
||||
(defmethod ig/init-key ::handler
|
||||
[_ cfg]
|
||||
(fn [{:keys [props] :as task}]
|
||||
(let [threshold (-> (ct/duration (or (:min-age props) (::min-age cfg)))
|
||||
(ct/in-past))]
|
||||
(let [min-age (ct/duration (or (:min-age props) (::min-age cfg)))]
|
||||
(-> cfg
|
||||
(assoc ::db/rollback (:rollback? props))
|
||||
(assoc ::min-age min-age)
|
||||
(assoc ::wrk/task :file-gc)
|
||||
(assoc ::wrk/priority 10)
|
||||
(assoc ::wrk/mark-retries 0)
|
||||
(assoc ::wrk/delay 10000)
|
||||
(db/tx-run! schedule! threshold)))))
|
||||
(assoc ::wrk/delay 1000)
|
||||
(db/tx-run! schedule!)))))
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
[app.common.exceptions :as ex]
|
||||
[selmer.parser :as sp]))
|
||||
|
||||
(sp/cache-off!)
|
||||
;; (sp/cache-off!)
|
||||
|
||||
(defn render
|
||||
[path context]
|
||||
|
||||
@@ -77,8 +77,8 @@
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private sql:insert-new-task
|
||||
"insert into task (id, name, props, queue, label, priority, max_retries, created_at, modified_at, scheduled_at)
|
||||
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"insert into task (id, name, props, queue, label, priority, max_retries, scheduled_at)
|
||||
values (?, ?, ?, ?, ?, ?, ?, now() + ?)
|
||||
returning id")
|
||||
|
||||
(def ^:private
|
||||
@@ -88,7 +88,7 @@
|
||||
AND queue=?
|
||||
AND label=?
|
||||
AND status = 'new'
|
||||
AND scheduled_at > ?")
|
||||
AND scheduled_at > now()")
|
||||
|
||||
(def ^:private schema:options
|
||||
[:map {:title "submit-options"}
|
||||
@@ -111,19 +111,17 @@
|
||||
|
||||
(check-options! options)
|
||||
|
||||
(let [delay (ct/duration delay)
|
||||
now (ct/now)
|
||||
scheduled-at (-> (ct/plus now delay)
|
||||
(ct/truncate :millisecond))
|
||||
props (db/tjson params)
|
||||
id (uuid/next)
|
||||
tenant (cf/get :tenant)
|
||||
task (d/name task)
|
||||
queue (str/ffmt "%:%" tenant (d/name queue))
|
||||
conn (db/get-connectable options)
|
||||
deleted (when dedupe
|
||||
(-> (db/exec-one! conn [sql:remove-not-started-tasks task queue label now])
|
||||
(db/get-update-count)))]
|
||||
(let [duration (ct/duration delay)
|
||||
interval (db/interval duration)
|
||||
props (db/tjson params)
|
||||
id (uuid/next)
|
||||
tenant (cf/get :tenant)
|
||||
task (d/name task)
|
||||
queue (str/ffmt "%:%" tenant (d/name queue))
|
||||
conn (db/get-connectable options)
|
||||
deleted (when dedupe
|
||||
(-> (db/exec-one! conn [sql:remove-not-started-tasks task queue label])
|
||||
:next.jdbc/update-count))]
|
||||
|
||||
(l/trc :hint "submit task"
|
||||
:name task
|
||||
@@ -131,13 +129,11 @@
|
||||
:queue queue
|
||||
:label label
|
||||
:dedupe (boolean dedupe)
|
||||
:delay (ct/format-duration delay)
|
||||
:delay (ct/format-duration duration)
|
||||
:replace (or deleted 0))
|
||||
|
||||
(db/exec-one! conn [sql:insert-new-task id task props queue
|
||||
label priority max-retries
|
||||
now now scheduled-at])
|
||||
|
||||
label priority max-retries interval])
|
||||
id))
|
||||
|
||||
(defn invoke!
|
||||
|
||||
@@ -158,9 +158,7 @@
|
||||
(inst-ms (:scheduled-at task)))
|
||||
(l/wrn :hint "skiping task, rescheduled"
|
||||
:task-id task-id
|
||||
:runner-id id
|
||||
:scheduled-at (ct/format-inst (:scheduled-at task))
|
||||
:expected-scheduled-at (ct/format-inst scheduled-at))
|
||||
:runner-id id)
|
||||
|
||||
:else
|
||||
(let [result (run-task cfg task)]
|
||||
@@ -181,8 +179,7 @@
|
||||
{:error explain
|
||||
:status "retry"
|
||||
:modified-at now
|
||||
:scheduled-at (-> (ct/plus now delay)
|
||||
(ct/truncate :millisecond))
|
||||
:scheduled-at (ct/plus now delay)
|
||||
:retry-num nretry}
|
||||
{:id (:id task)})
|
||||
nil))
|
||||
|
||||
@@ -549,44 +549,6 @@
|
||||
(io/copy r sw)
|
||||
(.toString sw))))
|
||||
|
||||
(defn parse-sse
|
||||
[content]
|
||||
(let [state
|
||||
(reduce (fn [{:keys [events data event id] :as state} line]
|
||||
(cond
|
||||
;; empty line → dispatch event if we have data
|
||||
(str/blank? line)
|
||||
(if (seq data)
|
||||
(-> state
|
||||
(update :events conj {:event (or event "message")
|
||||
:data (-> (str/join "\n" data))})
|
||||
(assoc :data [] :event nil))
|
||||
state)
|
||||
|
||||
;; comment line (starts with :)
|
||||
(str/starts-with? line ":")
|
||||
state
|
||||
|
||||
:else
|
||||
(let [[field raw-value] (str/split line #":" 2)
|
||||
value (some-> raw-value (str/replace #"^ " ""))]
|
||||
(case field
|
||||
"data" (update state :data conj (or value ""))
|
||||
"event" (assoc state :event value)
|
||||
;; ignore retry and unknown fields
|
||||
state))))
|
||||
{:events [] :data [] :event nil}
|
||||
(str/split content #"\r?\n"))
|
||||
|
||||
;; handle unterminated last event (no trailing blank line)
|
||||
state (if (seq (:data state))
|
||||
(update state :events conj
|
||||
{:event (or (:event state) "message")
|
||||
:data (str/join "\n" (:data state))})
|
||||
state)]
|
||||
|
||||
(:events state)))
|
||||
|
||||
(defn consume-sse
|
||||
[callback]
|
||||
(let [{:keys [::yres/status ::yres/body ::yres/headers] :as response} (callback {})
|
||||
@@ -596,9 +558,12 @@
|
||||
(try
|
||||
(px/exec! :virtual #(rcp/write-body-to-stream body nil output))
|
||||
(into []
|
||||
(map (fn [{:keys [event data]}]
|
||||
[(keyword event)
|
||||
(tr/decode-str data)]))
|
||||
(parse-sse (slurp' input)))
|
||||
(map (fn [event]
|
||||
(let [[item1 item2] (re-seq #"(.*): (.*)\n?" event)]
|
||||
|
||||
[(keyword (nth item1 2))
|
||||
(tr/decode-str (nth item2 2))])))
|
||||
(-> (slurp' input)
|
||||
(str/split "\n\n")))
|
||||
(finally
|
||||
(.close input)))))
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns backend-tests.http-middleware-access-token-test
|
||||
(:require
|
||||
[app.db :as db]
|
||||
[app.http.access-token]
|
||||
[app.main :as-alias main]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.commands.access-token]
|
||||
[app.tokens :as tokens]
|
||||
[backend-tests.helpers :as th]
|
||||
[clojure.test :as t]
|
||||
[mockery.core :refer [with-mocks]]))
|
||||
|
||||
(t/use-fixtures :once th/state-init)
|
||||
(t/use-fixtures :each th/database-reset)
|
||||
|
||||
(t/deftest soft-auth-middleware
|
||||
(let [profile (th/create-profile* 1)
|
||||
token (db/tx-run! th/*system* app.rpc.commands.access-token/create-access-token (:id profile) "test" nil)
|
||||
|
||||
request (volatile! nil)
|
||||
handler (#'app.http.access-token/wrap-soft-auth
|
||||
(fn [req] (vreset! request req))
|
||||
th/*system*)]
|
||||
|
||||
(with-mocks [m1 {:target 'app.http.access-token/get-token
|
||||
:return nil}]
|
||||
(handler {})
|
||||
(t/is (= {} @request)))
|
||||
|
||||
(with-mocks [m1 {:target 'app.http.access-token/get-token
|
||||
:return (:token token)}]
|
||||
(handler {})
|
||||
|
||||
(let [token-id (get @request :app.http.access-token/id)]
|
||||
(t/is (= token-id (:id token)))))))
|
||||
|
||||
(t/deftest authz-middleware
|
||||
(let [profile (th/create-profile* 1)
|
||||
token (db/tx-run! th/*system* app.rpc.commands.access-token/create-access-token (:id profile) "test" nil)
|
||||
request (volatile! {})
|
||||
handler (#'app.http.access-token/wrap-authz
|
||||
(fn [req] (vreset! request req))
|
||||
th/*system*)]
|
||||
|
||||
(handler nil)
|
||||
(t/is (nil? @request))
|
||||
|
||||
(handler {:app.http.access-token/id (:id token)})
|
||||
(t/is (= #{} (:app.http.access-token/perms @request)))
|
||||
(t/is (= (:id profile) (:app.http.access-token/profile-id @request)))))
|
||||
|
||||
@@ -1,161 +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 backend-tests.http-middleware-test
|
||||
(:require
|
||||
[app.common.time :as ct]
|
||||
[app.db :as db]
|
||||
[app.http.access-token]
|
||||
[app.http.auth :as-alias auth]
|
||||
[app.http.middleware :as mw]
|
||||
[app.http.session :as session]
|
||||
[app.main :as-alias main]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.commands.access-token]
|
||||
[app.tokens :as tokens]
|
||||
[backend-tests.helpers :as th]
|
||||
[clojure.test :as t]
|
||||
[mockery.core :refer [with-mocks]]
|
||||
[yetti.request :as yreq]
|
||||
[yetti.response :as yres]))
|
||||
|
||||
(t/use-fixtures :once th/state-init)
|
||||
(t/use-fixtures :each th/database-reset)
|
||||
|
||||
(defrecord DummyRequest [headers cookies]
|
||||
yreq/IRequestCookies
|
||||
(get-cookie [_ name]
|
||||
{:value (get cookies name)})
|
||||
|
||||
yreq/IRequest
|
||||
(get-header [_ name]
|
||||
(get headers name)))
|
||||
|
||||
(t/deftest auth-middleware-1
|
||||
(let [request (volatile! nil)
|
||||
handler (#'app.http.middleware/wrap-auth
|
||||
(fn [req] (vreset! request req))
|
||||
{})]
|
||||
|
||||
(handler (->DummyRequest {} {}))
|
||||
|
||||
(t/is (nil? (::auth/token-type @request)))
|
||||
(t/is (nil? (::auth/token @request)))
|
||||
|
||||
(handler (->DummyRequest {"authorization" "Token aaaa"} {}))
|
||||
|
||||
(t/is (= :token (::auth/token-type @request)))
|
||||
(t/is (= "aaaa" (::auth/token @request)))))
|
||||
|
||||
(t/deftest auth-middleware-2
|
||||
(let [request (volatile! nil)
|
||||
handler (#'app.http.middleware/wrap-auth
|
||||
(fn [req] (vreset! request req))
|
||||
{})]
|
||||
|
||||
(handler (->DummyRequest {} {}))
|
||||
|
||||
(t/is (nil? (::auth/token-type @request)))
|
||||
(t/is (nil? (::auth/token @request)))
|
||||
(t/is (nil? (::auth/claims @request)))
|
||||
|
||||
(handler (->DummyRequest {"authorization" "Bearer aaaa"} {}))
|
||||
|
||||
(t/is (= :bearer (::auth/token-type @request)))
|
||||
(t/is (= "aaaa" (::auth/token @request)))
|
||||
(t/is (nil? (::auth/claims @request)))))
|
||||
|
||||
(t/deftest auth-middleware-3
|
||||
(let [request (volatile! nil)
|
||||
handler (#'app.http.middleware/wrap-auth
|
||||
(fn [req] (vreset! request req))
|
||||
{})]
|
||||
|
||||
(handler (->DummyRequest {} {}))
|
||||
|
||||
(t/is (nil? (::auth/token-type @request)))
|
||||
(t/is (nil? (::auth/token @request)))
|
||||
(t/is (nil? (::auth/claims @request)))
|
||||
|
||||
(handler (->DummyRequest {} {"auth-token" "foobar"}))
|
||||
|
||||
(t/is (= :cookie (::auth/token-type @request)))
|
||||
(t/is (= "foobar" (::auth/token @request)))
|
||||
(t/is (nil? (::auth/claims @request)))))
|
||||
|
||||
(t/deftest auth-middleware-4
|
||||
(let [request (volatile! nil)
|
||||
handler (#'app.http.middleware/wrap-auth
|
||||
(fn [req] (vreset! request req))
|
||||
{:cookie (fn [_] "foobaz")})]
|
||||
|
||||
(handler (->DummyRequest {} {}))
|
||||
|
||||
(t/is (nil? (::auth/token-type @request)))
|
||||
(t/is (nil? (::auth/token @request)))
|
||||
(t/is (nil? (::auth/claims @request)))
|
||||
|
||||
(handler (->DummyRequest {} {"auth-token" "foobar"}))
|
||||
|
||||
(t/is (= :cookie (::auth/token-type @request)))
|
||||
(t/is (= "foobar" (::auth/token @request)))
|
||||
(t/is (delay? (::auth/claims @request)))
|
||||
(t/is (= "foobaz" (-> @request ::auth/claims deref)))))
|
||||
|
||||
(t/deftest shared-key-auth
|
||||
(let [handler (#'app.http.middleware/wrap-shared-key-auth
|
||||
(fn [req] {::yres/status 200})
|
||||
"secret-key")]
|
||||
|
||||
(let [response (handler (->DummyRequest {} {}))]
|
||||
(t/is (= 403 (::yres/status response))))
|
||||
|
||||
(let [response (handler (->DummyRequest {"x-shared-key" "secret-key2"} {}))]
|
||||
(t/is (= 403 (::yres/status response))))
|
||||
|
||||
(let [response (handler (->DummyRequest {"x-shared-key" "secret-key"} {}))]
|
||||
(t/is (= 200 (::yres/status response))))))
|
||||
|
||||
(t/deftest access-token-authz
|
||||
(let [profile (th/create-profile* 1)
|
||||
token (db/tx-run! th/*system* app.rpc.commands.access-token/create-access-token (:id profile) "test" nil)
|
||||
request (volatile! {})
|
||||
|
||||
handler (#'app.http.access-token/wrap-authz
|
||||
(fn [req] (vreset! request req))
|
||||
th/*system*)]
|
||||
|
||||
(handler nil)
|
||||
(t/is (nil? @request))
|
||||
|
||||
(handler {::auth/claims (delay {:tid (:id token)})
|
||||
::auth/token-type :token})
|
||||
|
||||
(t/is (= #{} (:app.http.access-token/perms @request)))
|
||||
(t/is (= (:id profile) (:app.http.access-token/profile-id @request)))))
|
||||
|
||||
(t/deftest session-authz
|
||||
(let [manager (session/inmemory-manager)
|
||||
profile (th/create-profile* 1)
|
||||
handler (-> (fn [req] req)
|
||||
(#'session/wrap-authz {::session/manager manager})
|
||||
(#'mw/wrap-auth {}))]
|
||||
|
||||
|
||||
(let [response (handler (->DummyRequest {} {"auth-token" "foobar"}))]
|
||||
(t/is (= :cookie (::auth/token-type response)))
|
||||
(t/is (= "foobar" (::auth/token response))))
|
||||
|
||||
|
||||
(session/write! manager "foobar" {:profile-id (:id profile)
|
||||
:user-agent "user agent"
|
||||
:created-at (ct/now)})
|
||||
|
||||
(let [response (handler (->DummyRequest {} {"auth-token" "foobar"}))]
|
||||
(t/is (= :cookie (::auth/token-type response)))
|
||||
(t/is (= "foobar" (::auth/token response)))
|
||||
(t/is (= (:id profile) (::session/profile-id response)))
|
||||
(t/is (= "foobar" (::session/id response))))))
|
||||
@@ -23,7 +23,7 @@
|
||||
(smt/check!
|
||||
(smt/for [context (->> sg/int
|
||||
(sg/fmap (fn [_]
|
||||
(#'rpc.doc/openapi-context (::rpc/methods th/*system*)))))]
|
||||
(rpc.doc/prepare-openapi-context (::rpc/methods th/*system*)))))]
|
||||
(try
|
||||
(json/encode context)
|
||||
true
|
||||
|
||||
@@ -1048,12 +1048,6 @@
|
||||
(into [elem])
|
||||
(into (subvec without-elem insert-pos)))))))
|
||||
|
||||
(defn invert-map
|
||||
"Returns a map with keys and values swapped.
|
||||
If the input map has duplicate values, later entries overwrite earlier ones."
|
||||
[m]
|
||||
(into {} (map (fn [[k v]] [v k]) m)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; String Functions
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
@@ -1357,6 +1357,38 @@
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "0004-clean-shadow-color"
|
||||
[data _]
|
||||
(let [decode-color (sm/decoder types.color/schema:color sm/json-transformer)
|
||||
|
||||
clean-shadow-color
|
||||
(fn [color]
|
||||
(let [ref-id (get color :id)
|
||||
ref-file (get color :file-id)]
|
||||
(-> (d/without-qualified color)
|
||||
(select-keys [:opacity :color :gradient :image :ref-id :ref-file])
|
||||
(cond-> ref-id
|
||||
(assoc :ref-id ref-id))
|
||||
(cond-> ref-file
|
||||
(assoc :ref-file ref-file))
|
||||
(decode-color))))
|
||||
|
||||
clean-shadow
|
||||
(fn [shadow]
|
||||
(update shadow :color clean-shadow-color))
|
||||
|
||||
update-object
|
||||
(fn [object]
|
||||
(d/update-when object :shadow #(mapv clean-shadow %)))
|
||||
|
||||
update-container
|
||||
(fn [container]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "0005-deprecate-image-type"
|
||||
[data _]
|
||||
(letfn [(update-object [object]
|
||||
@@ -1665,45 +1697,6 @@
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "0015-clean-shadow-color"
|
||||
[data _]
|
||||
(let [decode-shadow-color
|
||||
(sm/decoder ctss/schema:color sm/json-transformer)
|
||||
|
||||
clean-shadow-color
|
||||
(fn [color]
|
||||
(let [ref-id (get color :id)
|
||||
ref-file (get color :file-id)]
|
||||
(-> (d/without-qualified color)
|
||||
(select-keys ctss/color-attrs)
|
||||
(cond-> ref-id
|
||||
(assoc :ref-id ref-id))
|
||||
(cond-> ref-file
|
||||
(assoc :ref-file ref-file))
|
||||
(decode-shadow-color)
|
||||
(d/without-nils))))
|
||||
|
||||
clean-shadow
|
||||
(fn [shadow]
|
||||
(update shadow :color clean-shadow-color))
|
||||
|
||||
clean-xform
|
||||
(comp
|
||||
(keep clean-shadow)
|
||||
(filter ctss/valid-shadow?))
|
||||
|
||||
update-object
|
||||
(fn [object]
|
||||
(d/update-when object :shadow #(into [] clean-xform %)))
|
||||
|
||||
update-container
|
||||
(fn [container]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
;; Copy fills from position-data to text nodes when all text nodes lack fills,
|
||||
;; all position-data have fills, and the counts match
|
||||
(defmethod migrate-data "0016-copy-fills-from-position-data-to-text-node"
|
||||
@@ -1825,6 +1818,7 @@
|
||||
"0002-clean-shape-interactions"
|
||||
"0003-fix-root-shape"
|
||||
"0003-convert-path-content-v2"
|
||||
"0004-clean-shadow-color"
|
||||
"0005-deprecate-image-type"
|
||||
"0006-fix-old-texts-fills"
|
||||
"0008-fix-library-colors-v4"
|
||||
@@ -1838,5 +1832,4 @@
|
||||
"0014-fix-tokens-lib-duplicate-ids"
|
||||
"0014-clear-components-nil-objects"
|
||||
"0015-fix-text-attrs-blank-strings"
|
||||
"0015-clean-shadow-color"
|
||||
"0016-copy-fills-from-position-data-to-text-node"]))
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
(defn type
|
||||
[s]
|
||||
(m/type s default-options))
|
||||
(m/-type s))
|
||||
|
||||
(defn properties
|
||||
[s]
|
||||
@@ -46,10 +46,6 @@
|
||||
[s]
|
||||
(m/type-properties s))
|
||||
|
||||
(defn children
|
||||
[s]
|
||||
(m/children s default-options))
|
||||
|
||||
(defn schema
|
||||
[s]
|
||||
(if (schema? s)
|
||||
@@ -131,19 +127,9 @@
|
||||
|
||||
(defn keys
|
||||
"Given a map schema, return all keys as set"
|
||||
[schema']
|
||||
(let [schema' (m/schema schema' default-options)]
|
||||
(case (m/type schema')
|
||||
:map
|
||||
(->> (entries schema')
|
||||
(into #{} xf:map-key))
|
||||
|
||||
:merge
|
||||
(->> (m/children schema')
|
||||
(mapcat m/entries)
|
||||
(into #{} xf:map-key))
|
||||
|
||||
(throw (ex-info "not supported schema type" {:type (m/type schema')})))))
|
||||
[schema]
|
||||
(->> (entries schema)
|
||||
(into #{} xf:map-key)))
|
||||
|
||||
(defn update-properties
|
||||
[s f & args]
|
||||
|
||||
@@ -11,14 +11,6 @@
|
||||
|
||||
(def styles #{:drop-shadow :inner-shadow})
|
||||
|
||||
(def schema:color
|
||||
[:merge {:title "ShadowColor"}
|
||||
ctc/schema:color-attrs
|
||||
ctc/schema:plain-color])
|
||||
|
||||
(def color-attrs
|
||||
(sm/keys schema:color))
|
||||
|
||||
(def schema:shadow
|
||||
[:map {:title "Shadow"}
|
||||
[:id [:maybe ::sm/uuid]]
|
||||
@@ -28,7 +20,7 @@
|
||||
[:blur ::sm/safe-number]
|
||||
[:spread ::sm/safe-number]
|
||||
[:hidden :boolean]
|
||||
[:color schema:color]])
|
||||
[:color ctc/schema:color]])
|
||||
|
||||
(def check-shadow
|
||||
(sm/check-fn schema:shadow))
|
||||
|
||||
@@ -310,17 +310,3 @@
|
||||
the real name of the shape joined by the properties values separated by '/'"
|
||||
[variant]
|
||||
(cpn/merge-path-item (:name variant) (str/replace (:variant-name variant) #", " " / ")))
|
||||
|
||||
(defn find-boolean-pair
|
||||
"Given a vector, return the map from 'bool-values' that contains both as keys.
|
||||
Returns nil if none match."
|
||||
[v]
|
||||
(let [bool-values [{"on" true "off" false}
|
||||
{"yes" true "no" false}
|
||||
{"true" true "false" false}]]
|
||||
(when (= (count v) 2)
|
||||
(some (fn [b]
|
||||
(when (and (contains? b (first v))
|
||||
(contains? b (last v)))
|
||||
b))
|
||||
bool-values))))
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
(:refer-clojure :exclude [uri?])
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[cuerdas.core :as str]
|
||||
[lambdaisland.uri :as u]
|
||||
[lambdaisland.uri.normalize :as un])
|
||||
#?(:clj
|
||||
@@ -59,14 +58,6 @@
|
||||
(map (fn [[k v]] [(key-fn k) (value-fn v)]))))
|
||||
(u/map->query-string))))
|
||||
|
||||
(defn ensure-path-slash
|
||||
[u]
|
||||
(update (uri u) :path
|
||||
(fn [path]
|
||||
(if (str/ends-with? path "/")
|
||||
path
|
||||
(str path "/")))))
|
||||
|
||||
#?(:clj
|
||||
(defmethod print-method lambdaisland.uri.URI [^URI this ^java.io.Writer writer]
|
||||
(.write writer "#")
|
||||
|
||||
@@ -159,13 +159,3 @@
|
||||
|
||||
(t/testing "update-number-in-repeated-prop-names"
|
||||
(t/is (= (ctv/update-number-in-repeated-prop-names props) numbered-props)))))
|
||||
|
||||
|
||||
(t/deftest find-boolean-pair
|
||||
(t/testing "find-boolean-pair"
|
||||
(t/is (= (ctv/find-boolean-pair ["off" "on"]) {"on" true "off" false}))
|
||||
(t/is (= (ctv/find-boolean-pair ["on" "off"]) {"on" true "off" false}))
|
||||
(t/is (= (ctv/find-boolean-pair ["off" "on" "other"]) nil))
|
||||
(t/is (= (ctv/find-boolean-pair ["yes" "no"]) {"yes" true "no" false}))
|
||||
(t/is (= (ctv/find-boolean-pair ["false" "true"]) {"true" true "false" false}))
|
||||
(t/is (= (ctv/find-boolean-pair ["hello" "bye"]) nil))))
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
[
|
||||
{
|
||||
"~:features": {
|
||||
"~#set": [
|
||||
"plugins/runtime",
|
||||
"design-tokens/v1",
|
||||
"layout/grid",
|
||||
"styles/v2",
|
||||
"fdata/pointer-map",
|
||||
"fdata/objects-map",
|
||||
"components/v2",
|
||||
"fdata/shape-data-type"
|
||||
]
|
||||
},
|
||||
"~:permissions": {
|
||||
"~:type": "~:membership",
|
||||
"~:is-owner": true,
|
||||
"~:is-admin": true,
|
||||
"~:can-edit": true
|
||||
},
|
||||
"~:name": "Default",
|
||||
"~:modified-at": "~m1743598498553",
|
||||
"~:id": "~u5116481d-f4e1-80c0-8005-f8e885bdc14d",
|
||||
"~:created-at": "~m1743598498553",
|
||||
"~:is-default": true
|
||||
}
|
||||
]
|
||||
@@ -1,146 +0,0 @@
|
||||
{
|
||||
"~:features": {
|
||||
"~#set": [
|
||||
"fdata/path-data",
|
||||
"plugins/runtime",
|
||||
"design-tokens/v1",
|
||||
"variants/v1",
|
||||
"layout/grid",
|
||||
"styles/v2",
|
||||
"fdata/objects-map",
|
||||
"render-wasm/v1",
|
||||
"components/v2",
|
||||
"fdata/shape-data-type"
|
||||
]
|
||||
},
|
||||
"~:team-id": "~u6bd7c17d-4f59-815e-8006-5c1f6882469a",
|
||||
"~:permissions": {
|
||||
"~:type": "~:membership",
|
||||
"~:is-owner": true,
|
||||
"~:is-admin": true,
|
||||
"~:can-edit": true,
|
||||
"~:can-read": true,
|
||||
"~:is-logged": true
|
||||
},
|
||||
"~:has-media-trimmed": false,
|
||||
"~:comment-thread-seqn": 0,
|
||||
"~:name": "group_with_text_shadows",
|
||||
"~:revn": 31,
|
||||
"~:modified-at": "~m1762430368134",
|
||||
"~:vern": 0,
|
||||
"~:id": "~u58c5cc60-d124-81bd-8007-0f30f1ac452a",
|
||||
"~:is-shared": false,
|
||||
"~:migrations": {
|
||||
"~#ordered-set": [
|
||||
"legacy-2",
|
||||
"legacy-3",
|
||||
"legacy-5",
|
||||
"legacy-6",
|
||||
"legacy-7",
|
||||
"legacy-8",
|
||||
"legacy-9",
|
||||
"legacy-10",
|
||||
"legacy-11",
|
||||
"legacy-12",
|
||||
"legacy-13",
|
||||
"legacy-14",
|
||||
"legacy-16",
|
||||
"legacy-17",
|
||||
"legacy-18",
|
||||
"legacy-19",
|
||||
"legacy-25",
|
||||
"legacy-26",
|
||||
"legacy-27",
|
||||
"legacy-28",
|
||||
"legacy-29",
|
||||
"legacy-31",
|
||||
"legacy-32",
|
||||
"legacy-33",
|
||||
"legacy-34",
|
||||
"legacy-36",
|
||||
"legacy-37",
|
||||
"legacy-38",
|
||||
"legacy-39",
|
||||
"legacy-40",
|
||||
"legacy-41",
|
||||
"legacy-42",
|
||||
"legacy-43",
|
||||
"legacy-44",
|
||||
"legacy-45",
|
||||
"legacy-46",
|
||||
"legacy-47",
|
||||
"legacy-48",
|
||||
"legacy-49",
|
||||
"legacy-50",
|
||||
"legacy-51",
|
||||
"legacy-52",
|
||||
"legacy-53",
|
||||
"legacy-54",
|
||||
"legacy-55",
|
||||
"legacy-56",
|
||||
"legacy-57",
|
||||
"legacy-59",
|
||||
"legacy-62",
|
||||
"legacy-65",
|
||||
"legacy-66",
|
||||
"legacy-67",
|
||||
"0001-remove-tokens-from-groups",
|
||||
"0002-normalize-bool-content-v2",
|
||||
"0002-clean-shape-interactions",
|
||||
"0003-fix-root-shape",
|
||||
"0003-convert-path-content-v2",
|
||||
"0004-clean-shadow-color",
|
||||
"0005-deprecate-image-type",
|
||||
"0006-fix-old-texts-fills",
|
||||
"0008-fix-library-colors-v4",
|
||||
"0009-clean-library-colors",
|
||||
"0009-add-partial-text-touched-flags",
|
||||
"0010-fix-swap-slots-pointing-non-existent-shapes",
|
||||
"0011-fix-invalid-text-touched-flags",
|
||||
"0012-fix-position-data",
|
||||
"0013-fix-component-path",
|
||||
"0013-clear-invalid-strokes-and-fills",
|
||||
"0014-fix-tokens-lib-duplicate-ids",
|
||||
"0014-clear-components-nil-objects",
|
||||
"0015-fix-text-attrs-blank-strings",
|
||||
"0016-copy-fills-from-position-data-to-text-node"
|
||||
]
|
||||
},
|
||||
"~:version": 67,
|
||||
"~:project-id": "~u6bd7c17d-4f59-815e-8006-5c1f68846e43",
|
||||
"~:created-at": "~m1762273747633",
|
||||
"~:backend": "legacy-db",
|
||||
"~:data": {
|
||||
"~:pages": [
|
||||
"~u58c5cc60-d124-81bd-8007-0f30f1ac452b"
|
||||
],
|
||||
"~:pages-index": {
|
||||
"~u58c5cc60-d124-81bd-8007-0f30f1ac452b": {
|
||||
"~:objects": {
|
||||
"~#penpot/objects-map/v2": {
|
||||
"~u00000000-0000-0000-0000-000000000000": "[\"~#shape\",[\"^ \",\"~:y\",0,\"~:hide-fill-on-export\",false,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:name\",\"Root Frame\",\"~:width\",0.01,\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",0.0,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.01]],[\"^:\",[\"^ \",\"~:x\",0.0,\"~:y\",0.01]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^3\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",0,\"~:proportion\",1.0,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",0,\"~:y\",0,\"^6\",0.01,\"~:height\",0.01,\"~:x1\",0,\"~:y1\",0,\"~:x2\",0.01,\"~:y2\",0.01]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#FFFFFF\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^H\",0.01,\"~:flip-y\",null,\"~:shapes\",[\"~u22590301-48da-807a-8007-0f30f2c3c7a3\",\"~u457f223c-eff4-8043-8007-1186366cf83f\"]]]",
|
||||
"~u457f223c-eff4-8043-8007-1186366cf83f": "[\"~#shape\",[\"^ \",\"~:y\",1127.9999542236328,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:index\",3,\"~:name\",\"Text shadow\",\"~:width\",937.0000171528263,\"~:type\",\"~:group\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",182,\"~:y\",1127.9999542236328]],[\"^:\",[\"^ \",\"~:x\",1119.0000171528263,\"~:y\",1127.9999542236328]],[\"^:\",[\"^ \",\"~:x\",1119.0000171528263,\"~:y\",1434.9999937907205]],[\"^:\",[\"^ \",\"~:x\",182,\"~:y\",1434.9999937907205]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u4d2e83b3-f32c-80ca-8004-85fe278b278d\",\"~:constraints-v\",\"~:top\",\"~:constraints-h\",\"~:left\",\"~:id\",\"~u457f223c-eff4-8043-8007-1186366cf83f\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",182,\"~:blocked\",false,\"~:proportion\",1,\"~:shadow\",[[\"^ \",\"~:color\",[\"^ \",\"^I\",\"#d276ff\",\"~:opacity\",1],\"~:spread\",1,\"~:offset-y\",4,\"~:style\",\"~:drop-shadow\",\"~:blur\",0,\"~:hidden\",false,\"^B\",\"~u376e6303-a232-8017-8004-908433a7d495\",\"~:offset-x\",4]],\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",182,\"~:y\",1127.9999542236328,\"^6\",937.0000171528263,\"~:height\",307.0000395670877,\"~:x1\",182,\"~:y1\",1127.9999542236328,\"~:x2\",1119.0000171528263,\"~:y2\",1434.9999937907205]],\"~:fills\",[],\"~:flip-x\",false,\"^T\",307.0000395670877,\"~:flip-y\",false,\"~:shapes\",[\"~u457f223c-eff4-8043-8007-1186366cf840\",\"~u457f223c-eff4-8043-8007-1186366cf841\",\"~u457f223c-eff4-8043-8007-1186366cf842\"]]]",
|
||||
"~u22590301-48da-807a-8007-0f30f2c3c7a3": "[\"~#shape\",[\"^ \",\"~:y\",565.9999791979773,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:index\",3,\"~:name\",\"Text shadow\",\"~:width\",937.0000171528263,\"~:type\",\"~:group\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",182,\"~:y\",565.9999791979773]],[\"^:\",[\"^ \",\"~:x\",1119.0000171528263,\"~:y\",565.9999791979773]],[\"^:\",[\"^ \",\"~:x\",1119.0000171528263,\"~:y\",873.0000187650649]],[\"^:\",[\"^ \",\"~:x\",182,\"~:y\",873.0000187650649]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u4d2e83b3-f32c-80ca-8004-85fe278b278d\",\"~:constraints-v\",\"~:top\",\"~:constraints-h\",\"~:left\",\"~:id\",\"~u22590301-48da-807a-8007-0f30f2c3c7a3\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",182,\"~:blocked\",false,\"~:proportion\",1,\"~:shadow\",[[\"^ \",\"~:color\",[\"^ \",\"^I\",\"#166ada\",\"~:opacity\",1],\"~:spread\",40,\"~:offset-y\",4,\"~:style\",\"~:drop-shadow\",\"~:blur\",50,\"~:hidden\",false,\"^B\",\"~u376e6303-a232-8017-8004-908433a7d495\",\"~:offset-x\",4]],\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",182,\"~:y\",565.9999791979773,\"^6\",937.0000171528263,\"~:height\",307.0000395670876,\"~:x1\",182,\"~:y1\",565.9999791979773,\"~:x2\",1119.0000171528263,\"~:y2\",873.0000187650649]],\"~:fills\",[],\"~:flip-x\",false,\"^T\",307.0000395670876,\"~:flip-y\",false,\"~:shapes\",[\"~u22590301-48da-807a-8007-0f30f2c3c7a4\",\"~u22590301-48da-807a-8007-0f30f2c3c7a5\",\"~udbebc08f-fd4a-800a-8007-11852f2de796\"]]]",
|
||||
"~u22590301-48da-807a-8007-0f30f2c3c7a7": "[\"~#shape\",[\"^ \",\"~:y\",782.9999720950955,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:auto-height\",\"~:content\",[\"^ \",\"~:type\",\"root\",\"~:key\",\"7hmohksim0\",\"~:children\",[[\"^ \",\"^7\",\"paragraph-set\",\"^9\",[[\"^ \",\"~:line-height\",\"1.2\",\"~:font-style\",\"normal\",\"^9\",[[\"^ \",\"^:\",\"\",\"^;\",\"normal\",\"~:typography-ref-id\",null,\"~:text-transform\",\"none\",\"~:font-id\",\"gfont-karla\",\"^8\",\"2fc3qdybqwr\",\"~:font-size\",\"35\",\"~:font-weight\",\"500\",\"~:typography-ref-file\",null,\"~:font-variant-id\",\"500\",\"~:text-decoration\",\"none\",\"~:letter-spacing\",\"0\",\"~:fills\",[[\"^ \",\"~:fill-color\",\"#5E18AF\",\"~:fill-opacity\",1]],\"~:font-family\",\"Karla\",\"~:text\",\"This is the text body\"]],\"^<\",null,\"^=\",\"none\",\"~:text-align\",\"left\",\"^>\",\"gfont-karla\",\"^8\",\"29emf8fbblr\",\"^?\",\"0\",\"^@\",\"500\",\"^A\",null,\"~:text-direction\",\"ltr\",\"^7\",\"paragraph\",\"^B\",\"500\",\"^C\",\"none\",\"^D\",\"0\",\"^E\",[[\"^ \",\"^F\",\"#5E18AF\",\"^G\",1]],\"^H\",\"Karla\"]]]],\"~:vertical-align\",\"\"],\"~:hide-in-viewer\",false,\"~:name\",\"Example of UI design, flex + grid layouts, prototyping, light + dark mode colors, typographies and components.\",\"~:width\",702.99999999999,\"^7\",\"^I\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",415.99998795994475,\"~:y\",782.9999720950955]],[\"^Q\",[\"^ \",\"~:x\",1118.9999879599347,\"~:y\",782.9999720950955]],[\"^Q\",[\"^ \",\"~:x\",1118.9999879599347,\"~:y\",824.9999708433805]],[\"^Q\",[\"^ \",\"~:x\",415.99998795994475,\"~:y\",824.9999708433805]]],\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u4d2e83b3-f32c-80ca-8004-85fe278b278d\",\"~:constraints-v\",\"~:top\",\"~:constraints-h\",\"~:left\",\"~:id\",\"~u22590301-48da-807a-8007-0f30f2c3c7a7\",\"~:parent-id\",\"~u22590301-48da-807a-8007-0f30f2c3c7a5\",\"~:position-data\",[[\"^ \",\"~:y\",824.1999486982821,\"^;\",\"normal\",\"^=\",\"none\",\"^?\",\"35px\",\"^@\",\"500\",\"~:y1\",0.4000000059604645,\"^O\",662.6312866210938,\"^C\",\"none solid rgb(94, 24, 175)\",\"^D\",\"normal\",\"~:x\",416.00003051766544,\"~:x1\",0,\"~:y2\",41.19999924302101,\"^E\",[[\"^ \",\"^F\",\"#5E18AF\",\"^G\",1]],\"~:x2\",662.6312866210938,\"~:direction\",\"ltr\",\"^H\",\"Karla\",\"~:height\",40.79999923706055,\"^I\",\"Example of UI design, flex + grid layouts, \"],[\"^ \",\"~:y\",866.199954032898,\"^;\",\"normal\",\"^=\",\"none\",\"^?\",\"35px\",\"^@\",\"500\",\"^[\",42.400001525878906,\"^O\",615.8500366210938,\"^C\",\"none solid rgb(94, 24, 175)\",\"^D\",\"normal\",\"~:x\",416.00003051766544,\"^10\",0,\"^11\",83.20000457763672,\"^E\",[[\"^ \",\"^F\",\"#5E18AF\",\"^G\",1]],\"^12\",615.8500366210938,\"^13\",\"ltr\",\"^H\",\"Karla\",\"^14\",40.80000305175781,\"^I\",\"prototyping, light + dark mode colors, \"],[\"^ \",\"~:y\",908.199954032898,\"^;\",\"normal\",\"^=\",\"none\",\"^?\",\"35px\",\"^@\",\"500\",\"^[\",84.4000015258789,\"^O\",503.13751220703125,\"^C\",\"none solid rgb(94, 24, 175)\",\"^D\",\"normal\",\"~:x\",416.00003051766544,\"^10\",0,\"^11\",125.20000457763672,\"^E\",[[\"^ \",\"^F\",\"#5E18AF\",\"^G\",1]],\"^12\",503.13751220703125,\"^13\",\"ltr\",\"^H\",\"Karla\",\"^14\",40.80000305175781,\"^I\",\"typographies and components.\"]],\"~:frame-id\",\"~u22590301-48da-807a-8007-0f30f2c3c7a5\",\"~:strokes\",[],\"~:x\",415.99998795994475,\"~:blocked\",false,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",415.99998795994475,\"~:y\",782.9999720950955,\"^O\",702.99999999999,\"^14\",41.999998748285066,\"^10\",415.99998795994475,\"^[\",782.9999720950955,\"^12\",1118.9999879599347,\"^11\",824.9999708433805]],\"^E\",[],\"~:flip-x\",null,\"^14\",41.999998748285066,\"~:flip-y\",null]]",
|
||||
"~u22590301-48da-807a-8007-0f30f2c3c7a6": "[\"~#shape\",[\"^ \",\"~:y\",840.0000141561163,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:content\",[\"^ \",\"~:type\",\"root\",\"~:key\",\"dxn5loqivn\",\"~:children\",[[\"^ \",\"^7\",\"paragraph-set\",\"^9\",[[\"^ \",\"~:line-height\",\"1.2\",\"~:font-style\",\"normal\",\"^9\",[[\"^ \",\"^:\",\"1.2\",\"^;\",\"normal\",\"~:typography-ref-id\",null,\"~:text-transform\",\"none\",\"~:font-id\",\"gfont-karla\",\"^8\",\"11x47j94xkq\",\"~:font-size\",\"35\",\"~:font-weight\",\"500\",\"~:typography-ref-file\",null,\"~:font-variant-id\",\"500\",\"~:text-decoration\",\"none\",\"~:letter-spacing\",\"0\",\"~:fills\",[[\"^ \",\"~:fill-color\",\"#5E18AF\",\"~:fill-opacity\",1]],\"~:font-family\",\"Karla\",\"~:text\",\"This is just another text at the bottom\"]],\"^<\",null,\"^=\",\"none\",\"~:text-align\",\"left\",\"^>\",\"gfont-karla\",\"^8\",\"1rfpnyrk4it\",\"^?\",\"35\",\"^@\",\"500\",\"^A\",null,\"~:text-direction\",\"ltr\",\"^7\",\"paragraph\",\"^B\",\"500\",\"^C\",\"none\",\"^D\",\"0\",\"^E\",[[\"^ \",\"^F\",\"#5E18AF\",\"^G\",1]],\"^H\",\"Karla\"]]]],\"~:vertical-align\",\"\"],\"~:hide-in-viewer\",false,\"~:name\",\"Version 1. June 2024\",\"~:width\",702.99999999999,\"^7\",\"^I\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",415.9999879599491,\"~:y\",840.0000141561163]],[\"^Q\",[\"^ \",\"~:x\",1118.999987959939,\"~:y\",840.0000141561163]],[\"^Q\",[\"^ \",\"~:x\",1118.999987959939,\"~:y\",873.000022023929]],[\"^Q\",[\"^ \",\"~:x\",415.9999879599491,\"~:y\",873.000022023929]]],\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u4d2e83b3-f32c-80ca-8004-85fe278b278d\",\"~:constraints-v\",\"~:top\",\"~:constraints-h\",\"~:left\",\"~:hidden\",false,\"~:id\",\"~u22590301-48da-807a-8007-0f30f2c3c7a6\",\"~:parent-id\",\"~u22590301-48da-807a-8007-0f30f2c3c7a5\",\"~:position-data\",[[\"^ \",\"~:y\",881.199900329113,\"^;\",\"normal\",\"^=\",\"none\",\"^?\",\"35px\",\"^@\",\"500\",\"~:y1\",0.4000000059604645,\"^O\",323.5187683105469,\"^C\",\"none solid rgb(94, 24, 175)\",\"^D\",\"normal\",\"~:x\",416.00003051766544,\"~:x1\",0,\"~:y2\",41.19999924302101,\"^E\",[[\"^ \",\"^F\",\"#5E18AF\",\"^G\",1]],\"~:x2\",323.5187683105469,\"~:direction\",\"ltr\",\"^H\",\"Karla\",\"~:height\",40.79999923706055,\"^I\",\"Version 1. June 2024\"]],\"~:frame-id\",\"~u22590301-48da-807a-8007-0f30f2c3c7a5\",\"~:strokes\",[],\"~:x\",415.9999879599491,\"~:blocked\",false,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",415.9999879599491,\"~:y\",840.0000141561163,\"^O\",702.99999999999,\"^15\",33.000007867812656,\"^11\",415.9999879599491,\"^10\",840.0000141561163,\"^13\",1118.999987959939,\"^12\",873.000022023929]],\"^E\",[],\"~:flip-x\",null,\"^15\",33.000007867812656,\"~:flip-y\",null]]",
|
||||
"~u22590301-48da-807a-8007-0f30f2c3c7a5": "[\"~#shape\",[\"^ \",\"~:y\",782.9999810452875,\"~:hide-fill-on-export\",false,\"~:layout-gap-type\",\"~:multiple\",\"~:layout-padding\",[\"^ \",\"~:p1\",-2.842170943040401E-14,\"~:p2\",0,\"~:p3\",-2.842170943040401E-14,\"~:p4\",0],\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:layout-wrap-type\",\"~:nowrap\",\"~:layout\",\"~:flex\",\"~:hide-in-viewer\",true,\"~:name\",\"description\",\"~:layout-align-items\",\"~:start\",\"~:width\",703.0000076883839,\"~:layout-padding-type\",\"~:simple\",\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",416.0000094644423,\"~:y\",782.9999810452875]],[\"^J\",[\"^ \",\"~:x\",1119.0000171528263,\"~:y\",782.9999810452875]],[\"^J\",[\"^ \",\"~:x\",1119.0000171528263,\"~:y\",873.0000080957647]],[\"^J\",[\"^ \",\"~:x\",416.0000094644423,\"~:y\",873.0000080957647]]],\"~:show-content\",true,\"~:layout-item-h-sizing\",\"~:auto\",\"~:proportion-lock\",false,\"~:layout-gap\",[\"^ \",\"~:row-gap\",15,\"~:column-gap\",15],\"~:transform-inverse\",[\"^:\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u4d2e83b3-f32c-80ca-8004-85fe278b278d\",\"~:layout-item-v-sizing\",\"^M\",\"~:layout-justify-content\",\"^C\",\"~:id\",\"~u22590301-48da-807a-8007-0f30f2c3c7a5\",\"~:parent-id\",\"~u22590301-48da-807a-8007-0f30f2c3c7a3\",\"~:layout-flex-dir\",\"~:column\",\"~:layout-align-content\",\"~:stretch\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",416.00000946444237,\"~:blocked\",false,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",416.00000946444237,\"~:y\",782.9999810452875,\"^D\",703.0000076883839,\"~:height\",90.00002705047712,\"~:x1\",416.00000946444237,\"~:y1\",782.9999810452875,\"~:x2\",1119.0000171528263,\"~:y2\",873.0000080957647]],\"~:fills\",[],\"~:flip-x\",null,\"^16\",90.00002705047712,\"~:flip-y\",null,\"~:shapes\",[\"~u22590301-48da-807a-8007-0f30f2c3c7a6\",\"~u22590301-48da-807a-8007-0f30f2c3c7a7\"]]]",
|
||||
"~u22590301-48da-807a-8007-0f30f2c3c7a4": "[\"~#shape\",[\"^ \",\"~:y\",565.9999637007554,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:auto-width\",\"~:content\",[\"^ \",\"~:type\",\"root\",\"~:key\",\"8gvslj04p9\",\"~:children\",[[\"^ \",\"^7\",\"paragraph-set\",\"^9\",[[\"^ \",\"~:line-height\",\"1.2\",\"~:font-style\",\"normal\",\"^9\",[[\"^ \",\"^:\",\"1.2\",\"^;\",\"normal\",\"~:typography-ref-id\",null,\"~:text-transform\",\"none\",\"~:font-id\",\"gfont-karla\",\"^8\",\"2eqbmpbto3f\",\"~:font-size\",\"60\",\"~:font-weight\",\"700\",\"~:typography-ref-file\",null,\"~:font-variant-id\",\"700\",\"~:text-decoration\",\"none\",\"~:letter-spacing\",\"0\",\"~:fills\",[[\"^ \",\"~:fill-color\",\"#805ad5\",\"~:fill-opacity\",1]],\"~:font-family\",\"Karla\",\"~:text\",\"This is the title\"]],\"^<\",null,\"^=\",\"none\",\"~:text-align\",\"left\",\"^>\",\"gfont-karla\",\"^8\",\"yl00fqu977\",\"^?\",\"60\",\"^@\",\"700\",\"^A\",null,\"~:text-direction\",\"ltr\",\"^7\",\"paragraph\",\"^B\",\"700\",\"^C\",\"none\",\"^D\",\"0\",\"^E\",[[\"^ \",\"^F\",\"#805ad5\",\"^G\",1]],\"^H\",\"Karla\"]]]],\"~:vertical-align\",\"\"],\"~:hide-in-viewer\",false,\"~:name\",\"Sales dashboard example\",\"~:width\",428.000002264963,\"^7\",\"^I\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",386.99999294062314,\"~:y\",565.9999637007555]],[\"^Q\",[\"^ \",\"~:x\",814.9999952055861,\"~:y\",565.9999637007555]],[\"^Q\",[\"^ \",\"~:x\",814.9999952055861,\"~:y\",638.0000023244937]],[\"^Q\",[\"^ \",\"~:x\",386.99999294062314,\"~:y\",638.0000023244937]]],\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u4d2e83b3-f32c-80ca-8004-85fe278b278d\",\"~:hidden\",false,\"~:id\",\"~u22590301-48da-807a-8007-0f30f2c3c7a4\",\"~:parent-id\",\"~u22590301-48da-807a-8007-0f30f2c3c7a3\",\"~:position-data\",[[\"^ \",\"~:y\",601.1998026371002,\"^;\",\"normal\",\"^=\",\"none\",\"^?\",\"60px\",\"^@\",\"700\",\"~:y1\",0.800000011920929,\"^O\",736.3812866210938,\"^C\",\"none solid rgb(128, 90, 213)\",\"^D\",\"normal\",\"~:x\",306.9999465942383,\"~:x1\",0,\"~:y2\",71.20000153779984,\"^E\",[[\"^ \",\"^F\",\"#805ad5\",\"^G\",1]],\"~:x2\",736.3812866210938,\"~:direction\",\"ltr\",\"^H\",\"Karla\",\"~:height\",70.4000015258789,\"^I\",\"Sales dashboard example\"]],\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",386.9999929406232,\"~:blocked\",false,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",386.9999929406232,\"~:y\",565.9999637007554,\"^O\",428.000002264963,\"^11\",72.00003862373819,\"^Y\",386.9999929406232,\"^X\",565.9999637007554,\"^[\",814.9999952055862,\"^Z\",638.0000023244936]],\"^E\",[],\"~:flip-x\",null,\"^11\",72.00003862373819,\"~:flip-y\",null]]",
|
||||
"~udbebc08f-fd4a-800a-8007-11852f2de796": "[\"~#shape\",[\"^ \",\"~:y\",704.9999632537276,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",123,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",182,\"~:y\",704.9999632537276]],[\"^<\",[\"^ \",\"~:x\",305,\"~:y\",704.9999632537276]],[\"^<\",[\"^ \",\"~:x\",305,\"~:y\",758.9999020993622]],[\"^<\",[\"^ \",\"~:x\",182,\"~:y\",758.9999020993622]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:constraints-v\",\"~:scale\",\"~:constraints-h\",\"^B\",\"~:r1\",0,\"~:id\",\"~udbebc08f-fd4a-800a-8007-11852f2de796\",\"~:parent-id\",\"~u22590301-48da-807a-8007-0f30f2c3c7a3\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",182,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",182,\"~:y\",704.9999632537276,\"^8\",123,\"~:height\",53.999938845634574,\"~:x1\",182,\"~:y1\",704.9999632537276,\"~:x2\",305,\"~:y2\",758.9999020993622]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#476fe7\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^M\",53.999938845634574,\"~:flip-y\",null]]",
|
||||
"~u457f223c-eff4-8043-8007-1186366cf841": "[\"~#shape\",[\"^ \",\"~:y\",1344.9999721046204,\"~:hide-fill-on-export\",false,\"~:layout-gap-type\",\"~:multiple\",\"~:layout-padding\",[\"^ \",\"~:p1\",-2.842170943040401E-14,\"~:p2\",0,\"~:p3\",-2.842170943040401E-14,\"~:p4\",0],\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:layout-wrap-type\",\"~:nowrap\",\"~:layout\",\"~:flex\",\"~:hide-in-viewer\",true,\"~:name\",\"description\",\"~:layout-align-items\",\"~:start\",\"~:width\",703.0000076883839,\"~:layout-padding-type\",\"~:simple\",\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",416.0000094644423,\"~:y\",1344.9999721046204]],[\"^J\",[\"^ \",\"~:x\",1119.0000171528263,\"~:y\",1344.9999721046204]],[\"^J\",[\"^ \",\"~:x\",1119.0000171528263,\"~:y\",1434.9999937907205]],[\"^J\",[\"^ \",\"~:x\",416.0000094644423,\"~:y\",1434.9999937907205]]],\"~:show-content\",true,\"~:layout-item-h-sizing\",\"~:auto\",\"~:proportion-lock\",false,\"~:layout-gap\",[\"^ \",\"~:row-gap\",15,\"~:column-gap\",15],\"~:transform-inverse\",[\"^:\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u4d2e83b3-f32c-80ca-8004-85fe278b278d\",\"~:layout-item-v-sizing\",\"^M\",\"~:layout-justify-content\",\"^C\",\"~:id\",\"~u457f223c-eff4-8043-8007-1186366cf841\",\"~:parent-id\",\"~u457f223c-eff4-8043-8007-1186366cf83f\",\"~:layout-flex-dir\",\"~:column\",\"~:layout-align-content\",\"~:stretch\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",416.0000094644424,\"~:blocked\",false,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",416.0000094644424,\"~:y\",1344.9999721046204,\"^D\",703.0000076883839,\"~:height\",90.00002168610013,\"~:x1\",416.0000094644424,\"~:y1\",1344.9999721046204,\"~:x2\",1119.0000171528263,\"~:y2\",1434.9999937907205]],\"~:fills\",[],\"~:flip-x\",null,\"^16\",90.00002168610013,\"~:flip-y\",null,\"~:shapes\",[\"~u457f223c-eff4-8043-8007-1186366cf843\",\"~u457f223c-eff4-8043-8007-1186366cf844\"]]]",
|
||||
"~u457f223c-eff4-8043-8007-1186366cf840": "[\"~#shape\",[\"^ \",\"~:y\",1127.9999542236328,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:auto-width\",\"~:content\",[\"^ \",\"~:type\",\"root\",\"~:key\",\"8gvslj04p9\",\"~:children\",[[\"^ \",\"^7\",\"paragraph-set\",\"^9\",[[\"^ \",\"~:line-height\",\"1.2\",\"~:font-style\",\"normal\",\"^9\",[[\"^ \",\"^:\",\"1.2\",\"^;\",\"normal\",\"~:text-transform\",\"none\",\"~:font-id\",\"gfont-karla\",\"^8\",\"2eqbmpbto3f\",\"~:font-size\",\"60\",\"~:font-weight\",\"700\",\"~:font-variant-id\",\"700\",\"~:text-decoration\",\"none\",\"~:letter-spacing\",\"0\",\"~:fills\",[[\"^ \",\"~:fill-color\",\"#805ad5\",\"~:fill-opacity\",1]],\"~:font-family\",\"Karla\",\"~:text\",\"This is the title\"]],\"^<\",\"none\",\"~:text-align\",\"left\",\"^=\",\"gfont-karla\",\"^8\",\"yl00fqu977\",\"^>\",\"60\",\"^?\",\"700\",\"~:text-direction\",\"ltr\",\"^7\",\"paragraph\",\"^@\",\"700\",\"^A\",\"none\",\"^B\",\"0\",\"^C\",[[\"^ \",\"^D\",\"#805ad5\",\"^E\",1]],\"^F\",\"Karla\"]]]],\"~:vertical-align\",\"\"],\"~:hide-in-viewer\",false,\"~:name\",\"Sales dashboard example\",\"~:width\",428.000002264963,\"^7\",\"^G\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",386.9999929406231,\"~:y\",1127.9999542236328]],[\"^O\",[\"^ \",\"~:x\",814.999995205586,\"~:y\",1127.9999542236328]],[\"^O\",[\"^ \",\"~:x\",814.999995205586,\"~:y\",1199.9999928474213]],[\"^O\",[\"^ \",\"~:x\",386.9999929406231,\"~:y\",1199.9999928474213]]],\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u4d2e83b3-f32c-80ca-8004-85fe278b278d\",\"~:hidden\",false,\"~:id\",\"~u457f223c-eff4-8043-8007-1186366cf840\",\"~:parent-id\",\"~u457f223c-eff4-8043-8007-1186366cf83f\",\"~:position-data\",[[\"^ \",\"~:y\",1163.1997547745723,\"^;\",\"normal\",\"^<\",\"none\",\"^>\",\"60px\",\"^?\",\"700\",\"~:y1\",0.800000011920929,\"^M\",736.3812866210938,\"^A\",\"none solid rgb(128, 90, 213)\",\"^B\",\"normal\",\"~:x\",306.9999465942383,\"~:x1\",0,\"~:y2\",71.20000153779984,\"^C\",[[\"^ \",\"^D\",\"#805ad5\",\"^E\",1]],\"~:x2\",736.3812866210938,\"~:direction\",\"ltr\",\"^F\",\"Karla\",\"~:height\",70.4000015258789,\"^G\",\"Sales dashboard example\"]],\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",386.9999929406232,\"~:blocked\",false,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",386.9999929406232,\"~:y\",1127.9999542236328,\"^M\",428.000002264963,\"^[\",72.00003862378844,\"^W\",386.9999929406232,\"^V\",1127.9999542236328,\"^Y\",814.9999952055862,\"^X\",1199.9999928474213]],\"^C\",[],\"~:flip-x\",null,\"^[\",72.00003862378844,\"~:flip-y\",null]]",
|
||||
"~u457f223c-eff4-8043-8007-1186366cf843": "[\"~#shape\",[\"^ \",\"~:y\",1401.999989181772,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:content\",[\"^ \",\"~:type\",\"root\",\"~:key\",\"dxn5loqivn\",\"~:children\",[[\"^ \",\"^7\",\"paragraph-set\",\"^9\",[[\"^ \",\"~:line-height\",\"1.2\",\"~:font-style\",\"normal\",\"^9\",[[\"^ \",\"^:\",\"1.2\",\"^;\",\"normal\",\"~:text-transform\",\"none\",\"~:font-id\",\"gfont-karla\",\"^8\",\"11x47j94xkq\",\"~:font-size\",\"35\",\"~:font-weight\",\"500\",\"~:font-variant-id\",\"500\",\"~:text-decoration\",\"none\",\"~:letter-spacing\",\"0\",\"~:fills\",[[\"^ \",\"~:fill-color\",\"#5E18AF\",\"~:fill-opacity\",1]],\"~:font-family\",\"Karla\",\"~:text\",\"This is just another text at the bottom\"]],\"^<\",\"none\",\"~:text-align\",\"left\",\"^=\",\"gfont-karla\",\"^8\",\"1rfpnyrk4it\",\"^>\",\"35\",\"^?\",\"500\",\"~:text-direction\",\"ltr\",\"^7\",\"paragraph\",\"^@\",\"500\",\"^A\",\"none\",\"^B\",\"0\",\"^C\",[[\"^ \",\"^D\",\"#5E18AF\",\"^E\",1]],\"^F\",\"Karla\"]]]],\"~:vertical-align\",\"\"],\"~:hide-in-viewer\",false,\"~:name\",\"Version 1. June 2024\",\"~:width\",702.99999999999,\"^7\",\"^G\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",415.99998795994907,\"~:y\",1401.999989181772]],[\"^O\",[\"^ \",\"~:x\",1118.999987959939,\"~:y\",1401.999989181772]],[\"^O\",[\"^ \",\"~:x\",1118.999987959939,\"~:y\",1434.9999970495846]],[\"^O\",[\"^ \",\"~:x\",415.99998795994907,\"~:y\",1434.9999970495846]]],\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u4d2e83b3-f32c-80ca-8004-85fe278b278d\",\"~:constraints-v\",\"~:top\",\"~:constraints-h\",\"~:left\",\"~:hidden\",false,\"~:id\",\"~u457f223c-eff4-8043-8007-1186366cf843\",\"~:parent-id\",\"~u457f223c-eff4-8043-8007-1186366cf841\",\"~:position-data\",[[\"^ \",\"~:y\",1443.1998753547687,\"^;\",\"normal\",\"^<\",\"none\",\"^>\",\"35px\",\"^?\",\"500\",\"~:y1\",0.4000000059604645,\"^M\",323.5187683105469,\"^A\",\"none solid rgb(94, 24, 175)\",\"^B\",\"normal\",\"~:x\",416.00003051766544,\"~:x1\",0,\"~:y2\",41.19999924302101,\"^C\",[[\"^ \",\"^D\",\"#5E18AF\",\"^E\",1]],\"~:x2\",323.5187683105469,\"~:direction\",\"ltr\",\"^F\",\"Karla\",\"~:height\",40.79999923706055,\"^G\",\"Version 1. June 2024\"]],\"~:frame-id\",\"~u457f223c-eff4-8043-8007-1186366cf841\",\"~:strokes\",[],\"~:x\",415.99998795994907,\"~:blocked\",false,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",415.99998795994907,\"~:y\",1401.999989181772,\"^M\",702.99999999999,\"^13\",33.000007867812656,\"^[\",415.99998795994907,\"^Z\",1401.999989181772,\"^11\",1118.999987959939,\"^10\",1434.9999970495846]],\"^C\",[],\"~:flip-x\",null,\"^13\",33.000007867812656,\"~:flip-y\",null]]",
|
||||
"~u457f223c-eff4-8043-8007-1186366cf842": "[\"~#shape\",[\"^ \",\"~:y\",1266.9999993145393,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",123,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",182,\"~:y\",1266.9999993145393]],[\"^<\",[\"^ \",\"~:x\",305,\"~:y\",1266.9999993145393]],[\"^<\",[\"^ \",\"~:x\",305,\"~:y\",1320.999938160174]],[\"^<\",[\"^ \",\"~:x\",182,\"~:y\",1320.999938160174]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:constraints-v\",\"~:scale\",\"~:constraints-h\",\"^B\",\"~:r1\",0,\"~:id\",\"~u457f223c-eff4-8043-8007-1186366cf842\",\"~:parent-id\",\"~u457f223c-eff4-8043-8007-1186366cf83f\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",182,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",182,\"~:y\",1266.9999993145393,\"^8\",123,\"~:height\",53.99993884563446,\"~:x1\",182,\"~:y1\",1266.9999993145393,\"~:x2\",305,\"~:y2\",1320.9999381601738]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#476fe7\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^M\",53.99993884563446,\"~:flip-y\",null]]",
|
||||
"~u457f223c-eff4-8043-8007-1186366cf844": "[\"~#shape\",[\"^ \",\"~:y\",1345.0000691910636,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:auto-height\",\"~:content\",[\"^ \",\"~:type\",\"root\",\"~:key\",\"7hmohksim0\",\"~:children\",[[\"^ \",\"^7\",\"paragraph-set\",\"^9\",[[\"^ \",\"~:line-height\",\"1.2\",\"~:font-style\",\"normal\",\"^9\",[[\"^ \",\"^:\",\"\",\"^;\",\"normal\",\"~:text-transform\",\"none\",\"~:font-id\",\"gfont-karla\",\"^8\",\"2fc3qdybqwr\",\"~:font-size\",\"35\",\"~:font-weight\",\"500\",\"~:font-variant-id\",\"500\",\"~:text-decoration\",\"none\",\"~:letter-spacing\",\"0\",\"~:fills\",[[\"^ \",\"~:fill-color\",\"#5E18AF\",\"~:fill-opacity\",1]],\"~:font-family\",\"Karla\",\"~:text\",\"This is the text body\"]],\"^<\",\"none\",\"~:text-align\",\"left\",\"^=\",\"gfont-karla\",\"^8\",\"29emf8fbblr\",\"^>\",\"0\",\"^?\",\"500\",\"~:text-direction\",\"ltr\",\"^7\",\"paragraph\",\"^@\",\"500\",\"^A\",\"none\",\"^B\",\"0\",\"^C\",[[\"^ \",\"^D\",\"#5E18AF\",\"^E\",1]],\"^F\",\"Karla\"]]]],\"~:vertical-align\",\"\"],\"~:hide-in-viewer\",false,\"~:name\",\"Example of UI design, flex + grid layouts, prototyping, light + dark mode colors, typographies and components.\",\"~:width\",702.99999999999,\"^7\",\"^G\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",415.99998795994475,\"~:y\",1345.0000691910636]],[\"^O\",[\"^ \",\"~:x\",1118.9999879599347,\"~:y\",1345.0000691910636]],[\"^O\",[\"^ \",\"~:x\",1118.9999879599347,\"~:y\",1387.0000679393486]],[\"^O\",[\"^ \",\"~:x\",415.99998795994475,\"~:y\",1387.0000679393486]]],\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u4d2e83b3-f32c-80ca-8004-85fe278b278d\",\"~:constraints-v\",\"~:top\",\"~:constraints-h\",\"~:left\",\"~:id\",\"~u457f223c-eff4-8043-8007-1186366cf844\",\"~:parent-id\",\"~u457f223c-eff4-8043-8007-1186366cf841\",\"~:position-data\",[[\"^ \",\"~:y\",1386.2000457942502,\"^;\",\"normal\",\"^<\",\"none\",\"^>\",\"35px\",\"^?\",\"500\",\"~:y1\",0.4000000059604645,\"^M\",662.6312866210938,\"^A\",\"none solid rgb(94, 24, 175)\",\"^B\",\"normal\",\"~:x\",416.00003051766544,\"~:x1\",0,\"~:y2\",41.19999924302101,\"^C\",[[\"^ \",\"^D\",\"#5E18AF\",\"^E\",1]],\"~:x2\",662.6312866210938,\"~:direction\",\"ltr\",\"^F\",\"Karla\",\"~:height\",40.79999923706055,\"^G\",\"Example of UI design, flex + grid layouts, \"],[\"^ \",\"~:y\",1428.200051128866,\"^;\",\"normal\",\"^<\",\"none\",\"^>\",\"35px\",\"^?\",\"500\",\"^Y\",42.400001525878906,\"^M\",615.8500366210938,\"^A\",\"none solid rgb(94, 24, 175)\",\"^B\",\"normal\",\"~:x\",416.00003051766544,\"^Z\",0,\"^[\",83.20000457763672,\"^C\",[[\"^ \",\"^D\",\"#5E18AF\",\"^E\",1]],\"^10\",615.8500366210938,\"^11\",\"ltr\",\"^F\",\"Karla\",\"^12\",40.80000305175781,\"^G\",\"prototyping, light + dark mode colors, \"],[\"^ \",\"~:y\",1470.200051128866,\"^;\",\"normal\",\"^<\",\"none\",\"^>\",\"35px\",\"^?\",\"500\",\"^Y\",84.4000015258789,\"^M\",503.13751220703125,\"^A\",\"none solid rgb(94, 24, 175)\",\"^B\",\"normal\",\"~:x\",416.00003051766544,\"^Z\",0,\"^[\",125.20000457763672,\"^C\",[[\"^ \",\"^D\",\"#5E18AF\",\"^E\",1]],\"^10\",503.13751220703125,\"^11\",\"ltr\",\"^F\",\"Karla\",\"^12\",40.80000305175781,\"^G\",\"typographies and components.\"]],\"~:frame-id\",\"~u457f223c-eff4-8043-8007-1186366cf841\",\"~:strokes\",[],\"~:x\",415.99998795994475,\"~:blocked\",false,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",415.99998795994475,\"~:y\",1345.0000691910636,\"^M\",702.99999999999,\"^12\",41.99999874828518,\"^Z\",415.99998795994475,\"^Y\",1345.0000691910636,\"^10\",1118.9999879599347,\"^[\",1387.0000679393488]],\"^C\",[],\"~:flip-x\",null,\"^12\",41.99999874828518,\"~:flip-y\",null]]"
|
||||
}
|
||||
},
|
||||
"~:id": "~u58c5cc60-d124-81bd-8007-0f30f1ac452b",
|
||||
"~:name": "Page 1"
|
||||
}
|
||||
},
|
||||
"~:id": "~u58c5cc60-d124-81bd-8007-0f30f1ac452a",
|
||||
"~:options": {
|
||||
"~:components-v2": true,
|
||||
"~:base-font-size": "16px"
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,7 @@ export class BasePage {
|
||||
);
|
||||
}
|
||||
|
||||
const url = typeof path === "string" ? `**/api/main/methods/${path}` : path;
|
||||
const url = typeof path === "string" ? `**/api/rpc/command/${path}` : path;
|
||||
const interceptConfig = {
|
||||
status: 200,
|
||||
contentType: "application/transit+json",
|
||||
|
||||
@@ -387,42 +387,6 @@ test("Renders a file with texts with empty lines", async ({
|
||||
await expect(workspace.canvas).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("Renders a file with texts with breaking words", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = new WasmWorkspacePage(page);
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("render-wasm/get-file-empty-lines.json");
|
||||
|
||||
await workspace.goToWorkspace({
|
||||
id: "58c5cc60-d124-81bd-8007-0ecbaf9da983",
|
||||
pageId: "15222a7a-d3bc-80f1-8007-0d8e166e650f",
|
||||
});
|
||||
|
||||
await workspace.waitForFirstRender({ hideUI: false });
|
||||
await workspace.clickLeafLayer("text-with-empty-lines-3");
|
||||
await workspace.hideUI();
|
||||
await workspace.page.keyboard.press("Enter");
|
||||
|
||||
await expect(workspace.canvas).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("Renders a file with group with text with inherited shadows", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = new WasmWorkspacePage(page);
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("render-wasm/get-file-group-with-shadows.json");
|
||||
|
||||
await workspace.goToWorkspace({
|
||||
id: "58c5cc60-d124-81bd-8007-0f30f1ac452a",
|
||||
pageId: "58c5cc60-d124-81bd-8007-0f30f1ac452b",
|
||||
});
|
||||
|
||||
await workspace.waitForFirstRender();
|
||||
await expect(workspace.canvas).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test.skip("Updates text alignment edition - part 1", async ({ page }) => {
|
||||
const workspace = new WasmWorkspacePage(page);
|
||||
await workspace.setupEmptyFile();
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 114 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 43 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 124 KiB |
@@ -1,7 +1,7 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test("Has title", async ({ page }) => {
|
||||
await page.route("**/api/main/methods/get-profile", (route) => {
|
||||
await page.route("**/api/rpc/command/get-profile", (route) => {
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/transit+json",
|
||||
|
||||
@@ -1,672 +0,0 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { WorkspacePage } from "../pages/WorkspacePage";
|
||||
|
||||
const flags = ["enable-inspect-styles"];
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await WorkspacePage.init(page);
|
||||
});
|
||||
|
||||
const setupFile = async (workspacePage) => {
|
||||
await workspacePage.setupEmptyFile();
|
||||
await workspacePage.mockConfigFlags(flags);
|
||||
await workspacePage.mockRPC(
|
||||
/get\-file\?/,
|
||||
"workspace/get-file-inspect-tab.json",
|
||||
);
|
||||
await workspacePage.goToWorkspace({
|
||||
fileId: "7b2da435-6186-815a-8007-0daa95d2f26d",
|
||||
pageId: "ce79274b-11ab-8088-8007-0487ad43f789",
|
||||
});
|
||||
};
|
||||
|
||||
const shapeToLayerName = {
|
||||
flex: "shape - layout - flex",
|
||||
flexElement: "shape - layout - flex - element",
|
||||
grid: "shape - layout - grid",
|
||||
gridElement: "shape - layout - grid - element",
|
||||
shadow: "shape - shadow - single",
|
||||
shadowMultiple: "shape - shadow - multiple",
|
||||
shadowComposite: "shape - shadow - composite",
|
||||
blur: "shape - blur",
|
||||
borderRadius: {
|
||||
main: "shape - borderRadius",
|
||||
individual: "shape - borderRadius - individual",
|
||||
multiple: "shape - borderRadius - multiple",
|
||||
token: "shape - borderRadius - token",
|
||||
},
|
||||
fill: {
|
||||
solid: "shape - fill - single - solid",
|
||||
gradient: "shape - fill - single - gradient",
|
||||
image: "shape - fill - single - image",
|
||||
multiple: "shape - fill - multiple",
|
||||
style: "shape - fill - style",
|
||||
token: "shape - fill - token",
|
||||
},
|
||||
stroke: {
|
||||
solid: "shape - stroke - single - solid",
|
||||
gradient: "shape - stroke - single - gradient",
|
||||
image: "shape - stroke - single - image",
|
||||
multiple: "shape - stroke - multiple",
|
||||
style: "shape - stroke - style",
|
||||
token: "shape - stroke - token",
|
||||
},
|
||||
text: {
|
||||
simple: "shape - text",
|
||||
token: "shape - text - token - simple",
|
||||
compositeToken: "shape - text - token - composite",
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Copy the shorthand CSS from a full panel property
|
||||
* @param {object} panel - The style panel locator
|
||||
*/
|
||||
const copyShorthand = async (panel) => {
|
||||
const panelShorthandButton = panel.getByRole("button", {
|
||||
name: "Copy CSS shorthand to clipboard",
|
||||
});
|
||||
await panelShorthandButton.click();
|
||||
};
|
||||
|
||||
/**
|
||||
* Copy the CSS property from a property row by clicking its copy button
|
||||
* @param {object} panel - The style panel locator
|
||||
* @param {string} property - The property name to filter by
|
||||
*/
|
||||
const copyPropertyFromPropertyRow = async (panel, property) => {
|
||||
const propertyRow = panel
|
||||
.getByTestId("property-row")
|
||||
.filter({ hasText: property });
|
||||
const copyButton = propertyRow.getByRole("button");
|
||||
await copyButton.click();
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the style panel by its title
|
||||
* @param {WorkspacePage} workspacePage - The workspace page instance
|
||||
* @param {string} title - The title of the panel to retrieve
|
||||
*/
|
||||
const getPanelByTitle = async (workspacePage, title) => {
|
||||
const sidebar = workspacePage.page.getByTestId("right-sidebar");
|
||||
const article = sidebar.getByRole("article");
|
||||
const panel = article.filter({ hasText: title });
|
||||
return panel;
|
||||
};
|
||||
|
||||
/**
|
||||
* Selects a layer in the layers panel
|
||||
* @param {WorkspacePage} workspacePage - The workspace page instance
|
||||
* @param {string} layerName - The name of the layer to select
|
||||
* @param {string} parentLayerName - The name of the parent layer to expand (optional)
|
||||
*/
|
||||
const selectLayer = async (workspacePage, layerName, parentLayerName) => {
|
||||
await workspacePage.clickToggableLayer("Board");
|
||||
if (parentLayerName) {
|
||||
await workspacePage.clickToggableLayer(parentLayerName);
|
||||
}
|
||||
await workspacePage.clickLeafLayer(layerName);
|
||||
};
|
||||
|
||||
/**
|
||||
* Opens the Inspect tab
|
||||
* @param {WorkspacePage} workspacePage - The workspace page instance
|
||||
*/
|
||||
|
||||
const openInspectTab = async (workspacePage) => {
|
||||
const inspectButton = workspacePage.page.getByRole("tab", {
|
||||
name: "Inspect",
|
||||
});
|
||||
await inspectButton.click();
|
||||
};
|
||||
|
||||
test.describe("Inspect tab - Styles", () => {
|
||||
test("Open Inspect tab", async ({ page }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
await setupFile(workspacePage);
|
||||
|
||||
await selectLayer(workspacePage, shapeToLayerName.flex);
|
||||
await openInspectTab(workspacePage);
|
||||
|
||||
const switcherLabel = workspacePage.page.getByText("Layer info", {
|
||||
exact: true,
|
||||
});
|
||||
await expect(switcherLabel).toBeVisible();
|
||||
await expect(switcherLabel).toHaveText("Layer info");
|
||||
});
|
||||
test.describe("Inspect tab - Flex", () => {
|
||||
test("Shape Layout Flex ", async ({ page }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
await setupFile(workspacePage);
|
||||
|
||||
await selectLayer(workspacePage, shapeToLayerName.flex);
|
||||
await openInspectTab(workspacePage);
|
||||
|
||||
const panel = await getPanelByTitle(workspacePage, "Layout");
|
||||
await expect(panel).toBeVisible();
|
||||
|
||||
const propertyRow = panel.getByTestId("property-row");
|
||||
const propertyRowCount = await propertyRow.count();
|
||||
|
||||
expect(propertyRowCount).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
test("Shape Layout Flex Element", async ({ page }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
await setupFile(workspacePage);
|
||||
|
||||
await selectLayer(
|
||||
workspacePage,
|
||||
shapeToLayerName.flexElement,
|
||||
shapeToLayerName.flex,
|
||||
);
|
||||
await openInspectTab(workspacePage);
|
||||
|
||||
const panel = await getPanelByTitle(workspacePage, "Flex Element");
|
||||
await expect(panel).toBeVisible();
|
||||
|
||||
const propertyRow = panel.getByTestId("property-row");
|
||||
const propertyRowCount = await propertyRow.count();
|
||||
|
||||
expect(propertyRowCount).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
test("Shape Layout Grid", async ({ page }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
await setupFile(workspacePage);
|
||||
|
||||
await selectLayer(workspacePage, shapeToLayerName.grid);
|
||||
await openInspectTab(workspacePage);
|
||||
|
||||
const panel = await getPanelByTitle(workspacePage, "Layout");
|
||||
await expect(panel).toBeVisible();
|
||||
|
||||
const propertyRow = panel.getByTestId("property-row");
|
||||
const propertyRowCount = await propertyRow.count();
|
||||
|
||||
expect(propertyRowCount).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
test.describe("Inspect tab - Shadow", () => {
|
||||
test("Shape Shadow - Single shadow", async ({ page }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
await setupFile(workspacePage);
|
||||
|
||||
await selectLayer(workspacePage, shapeToLayerName.shadow);
|
||||
await openInspectTab(workspacePage);
|
||||
|
||||
const panel = await getPanelByTitle(workspacePage, "Shadow");
|
||||
await expect(panel).toBeVisible();
|
||||
|
||||
const propertyRow = panel.getByTestId("property-row");
|
||||
const propertyRowCount = await propertyRow.count();
|
||||
|
||||
expect(propertyRowCount).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
test("Shape Shadow - Multiple shadow", async ({ page }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
await setupFile(workspacePage);
|
||||
|
||||
await selectLayer(workspacePage, shapeToLayerName.shadowMultiple);
|
||||
await openInspectTab(workspacePage);
|
||||
|
||||
const panel = await getPanelByTitle(workspacePage, "Shadow");
|
||||
await expect(panel).toBeVisible();
|
||||
|
||||
const propertyRow = panel.getByTestId("property-row");
|
||||
const propertyRowCount = await propertyRow.count();
|
||||
|
||||
expect(propertyRowCount).toBeGreaterThanOrEqual(4);
|
||||
});
|
||||
|
||||
test("Shape Shadow - Composite shadow", async ({ page }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
await setupFile(workspacePage);
|
||||
|
||||
await selectLayer(workspacePage, shapeToLayerName.shadowComposite);
|
||||
await openInspectTab(workspacePage);
|
||||
|
||||
const panel = await getPanelByTitle(workspacePage, "Shadow");
|
||||
await expect(panel).toBeVisible();
|
||||
|
||||
const propertyRow = panel.getByTestId("property-row");
|
||||
const propertyRowCount = await propertyRow.count();
|
||||
|
||||
expect(propertyRowCount).toBeGreaterThanOrEqual(3);
|
||||
|
||||
const compositeShadowRow = propertyRow.first();
|
||||
await expect(compositeShadowRow).toBeVisible();
|
||||
|
||||
const compositeShadowTerm = compositeShadowRow.locator("dt");
|
||||
const compositeShadowDefinition = compositeShadowRow.locator("dd");
|
||||
|
||||
expect(compositeShadowTerm).toHaveText("Shadow", { exact: true });
|
||||
expect(compositeShadowDefinition).toContainText("shadowToken");
|
||||
});
|
||||
});
|
||||
|
||||
test("Shape - Blur", async ({ page }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
await setupFile(workspacePage);
|
||||
|
||||
await selectLayer(workspacePage, shapeToLayerName.blur);
|
||||
await openInspectTab(workspacePage);
|
||||
|
||||
const panel = await getPanelByTitle(workspacePage, "Blur");
|
||||
await expect(panel).toBeVisible();
|
||||
|
||||
const propertyRow = panel.getByTestId("property-row");
|
||||
const propertyRowCount = await propertyRow.count();
|
||||
|
||||
expect(propertyRowCount).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
test.describe("Inspect tab - Border radius", () => {
|
||||
test("Shape - Border radius - individual", async ({ page }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
await setupFile(workspacePage);
|
||||
|
||||
await selectLayer(
|
||||
workspacePage,
|
||||
shapeToLayerName.borderRadius.individual,
|
||||
shapeToLayerName.borderRadius.main,
|
||||
);
|
||||
await openInspectTab(workspacePage);
|
||||
|
||||
const panel = await getPanelByTitle(workspacePage, "Size & position");
|
||||
await expect(panel).toBeVisible();
|
||||
|
||||
const propertyRow = panel.getByTestId("property-row");
|
||||
const propertyRowCount = await propertyRow.count();
|
||||
|
||||
expect(propertyRowCount).toBeGreaterThanOrEqual(2);
|
||||
|
||||
const borderStartStartRadius = propertyRow.filter({
|
||||
hasText: "Border start start radius",
|
||||
});
|
||||
await expect(borderStartStartRadius).toBeVisible();
|
||||
|
||||
const borderEndEndRadius = propertyRow.filter({
|
||||
hasText: "Border end end radius",
|
||||
});
|
||||
await expect(borderEndEndRadius).toBeVisible();
|
||||
});
|
||||
|
||||
test("Shape - Border radius - multiple", async ({ page }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
await setupFile(workspacePage);
|
||||
|
||||
await selectLayer(
|
||||
workspacePage,
|
||||
shapeToLayerName.borderRadius.multiple,
|
||||
shapeToLayerName.borderRadius.main,
|
||||
);
|
||||
await openInspectTab(workspacePage);
|
||||
|
||||
const panel = await getPanelByTitle(workspacePage, "Size & position");
|
||||
await expect(panel).toBeVisible();
|
||||
|
||||
const propertyRow = panel.getByTestId("property-row");
|
||||
const propertyRowCount = await propertyRow.count();
|
||||
|
||||
expect(propertyRowCount).toBeGreaterThanOrEqual(4);
|
||||
|
||||
const borderStartStartRadius = propertyRow.filter({
|
||||
hasText: "Border start start radius",
|
||||
});
|
||||
await expect(borderStartStartRadius).toBeVisible();
|
||||
|
||||
const borderStartEndRadius = propertyRow.filter({
|
||||
hasText: "Border start end radius",
|
||||
});
|
||||
await expect(borderStartEndRadius).toBeVisible();
|
||||
|
||||
const borderEndEndRadius = propertyRow.filter({
|
||||
hasText: "Border end end radius",
|
||||
});
|
||||
await expect(borderEndEndRadius).toBeVisible();
|
||||
|
||||
const borderEndStartRadius = propertyRow.filter({
|
||||
hasText: "Border end start radius",
|
||||
});
|
||||
await expect(borderEndStartRadius).toBeVisible();
|
||||
});
|
||||
|
||||
test("Shape - Border radius - token", async ({ page }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
await setupFile(workspacePage);
|
||||
|
||||
await selectLayer(
|
||||
workspacePage,
|
||||
shapeToLayerName.borderRadius.token,
|
||||
shapeToLayerName.borderRadius.main,
|
||||
);
|
||||
await openInspectTab(workspacePage);
|
||||
|
||||
const panel = await getPanelByTitle(workspacePage, "Size & position");
|
||||
await expect(panel).toBeVisible();
|
||||
|
||||
const propertyRow = panel.getByTestId("property-row");
|
||||
const propertyRowCount = await propertyRow.count();
|
||||
|
||||
expect(propertyRowCount).toBeGreaterThanOrEqual(4);
|
||||
|
||||
const borderStartEndRadius = propertyRow.filter({
|
||||
hasText: "Border start end radius",
|
||||
});
|
||||
await expect(borderStartEndRadius).toBeVisible();
|
||||
expect(borderStartEndRadius).toContainText("radius");
|
||||
|
||||
const borderEndStartRadius = propertyRow.filter({
|
||||
hasText: "Border end start radius",
|
||||
});
|
||||
expect(borderEndStartRadius).toContainText("radius");
|
||||
await expect(borderEndStartRadius).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Inspect tab - Fill", () => {
|
||||
test("Shape - Fill - Solid", async ({ page }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
await setupFile(workspacePage);
|
||||
|
||||
await selectLayer(workspacePage, shapeToLayerName.fill.solid);
|
||||
await openInspectTab(workspacePage);
|
||||
const panel = await getPanelByTitle(workspacePage, "Fill");
|
||||
await expect(panel).toBeVisible();
|
||||
|
||||
const propertyRow = panel.getByTestId("property-row");
|
||||
const propertyRowCount = await propertyRow.count();
|
||||
|
||||
expect(propertyRowCount).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
test("Shape - Fill - Gradient", async ({ page }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
await setupFile(workspacePage);
|
||||
|
||||
await selectLayer(workspacePage, shapeToLayerName.fill.gradient);
|
||||
await openInspectTab(workspacePage);
|
||||
const panel = await getPanelByTitle(workspacePage, "Fill");
|
||||
await expect(panel).toBeVisible();
|
||||
|
||||
const propertyRow = panel.getByTestId("property-row");
|
||||
const propertyRowCount = await propertyRow.count();
|
||||
|
||||
expect(propertyRowCount).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
test("Shape - Fill - Image", async ({ page }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
await setupFile(workspacePage);
|
||||
|
||||
await selectLayer(workspacePage, shapeToLayerName.fill.image);
|
||||
await openInspectTab(workspacePage);
|
||||
const panel = await getPanelByTitle(workspacePage, "Fill");
|
||||
await expect(panel).toBeVisible();
|
||||
|
||||
const propertyRow = panel.getByTestId("property-row");
|
||||
const propertyRowCount = await propertyRow.count();
|
||||
|
||||
expect(propertyRowCount).toBeGreaterThanOrEqual(1);
|
||||
|
||||
const imagePreview = panel.getByRole("img", {
|
||||
name: "Preview of the shape's fill",
|
||||
});
|
||||
await expect(imagePreview).toBeVisible();
|
||||
});
|
||||
|
||||
test("Shape - Fill - Multiple", async ({ page }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
await setupFile(workspacePage);
|
||||
|
||||
await selectLayer(workspacePage, shapeToLayerName.fill.multiple);
|
||||
await openInspectTab(workspacePage);
|
||||
const panel = await getPanelByTitle(workspacePage, "Fill");
|
||||
await expect(panel).toBeVisible();
|
||||
|
||||
const propertyRow = panel.getByTestId("property-row");
|
||||
const propertyRowCount = await propertyRow.count();
|
||||
|
||||
expect(propertyRowCount).toBeGreaterThanOrEqual(3);
|
||||
|
||||
const imagePreview = panel.getByRole("img", {
|
||||
name: "Preview of the shape's fill",
|
||||
});
|
||||
await expect(imagePreview).toBeVisible();
|
||||
});
|
||||
|
||||
test("Shape - Fill - Token", async ({ page }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
await setupFile(workspacePage);
|
||||
|
||||
await selectLayer(workspacePage, shapeToLayerName.fill.token);
|
||||
await openInspectTab(workspacePage);
|
||||
const panel = await getPanelByTitle(workspacePage, "Fill");
|
||||
await expect(panel).toBeVisible();
|
||||
|
||||
const propertyRow = panel.getByTestId("property-row");
|
||||
const propertyRowCount = await propertyRow.count();
|
||||
|
||||
expect(propertyRowCount).toBeGreaterThanOrEqual(1);
|
||||
|
||||
const fillToken = propertyRow.filter({
|
||||
hasText: "Background",
|
||||
});
|
||||
expect(fillToken).toContainText("primary");
|
||||
await expect(fillToken).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Inspect tab - Stroke", () => {
|
||||
test("Shape - Stroke - Solid", async ({ page }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
await setupFile(workspacePage);
|
||||
|
||||
await selectLayer(workspacePage, shapeToLayerName.stroke.solid);
|
||||
await openInspectTab(workspacePage);
|
||||
const panel = await getPanelByTitle(workspacePage, "Stroke");
|
||||
await expect(panel).toBeVisible();
|
||||
|
||||
const propertyRow = panel.getByTestId("property-row");
|
||||
const propertyRowCount = await propertyRow.count();
|
||||
|
||||
expect(propertyRowCount).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
test("Shape - Stroke - Gradient", async ({ page }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
await setupFile(workspacePage);
|
||||
|
||||
await selectLayer(workspacePage, shapeToLayerName.stroke.gradient);
|
||||
await openInspectTab(workspacePage);
|
||||
const panel = await getPanelByTitle(workspacePage, "Stroke");
|
||||
await expect(panel).toBeVisible();
|
||||
|
||||
const propertyRow = panel.getByTestId("property-row");
|
||||
const propertyRowCount = await propertyRow.count();
|
||||
|
||||
expect(propertyRowCount).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
test("Shape - Stroke - Image", async ({ page }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
await setupFile(workspacePage);
|
||||
|
||||
await selectLayer(workspacePage, shapeToLayerName.stroke.image);
|
||||
await openInspectTab(workspacePage);
|
||||
const panel = await getPanelByTitle(workspacePage, "Stroke");
|
||||
await expect(panel).toBeVisible();
|
||||
|
||||
const propertyRow = panel.getByTestId("property-row");
|
||||
const propertyRowCount = await propertyRow.count();
|
||||
|
||||
expect(propertyRowCount).toBeGreaterThanOrEqual(1);
|
||||
|
||||
const imagePreview = panel.getByRole("img", {
|
||||
name: "Preview of the shape's fill",
|
||||
});
|
||||
await expect(imagePreview).toBeVisible();
|
||||
});
|
||||
|
||||
test("Shape - Stroke - Multiple", async ({ page }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
await setupFile(workspacePage);
|
||||
|
||||
await selectLayer(workspacePage, shapeToLayerName.stroke.multiple);
|
||||
await openInspectTab(workspacePage);
|
||||
const panel = await getPanelByTitle(workspacePage, "Stroke");
|
||||
await expect(panel).toBeVisible();
|
||||
|
||||
const propertyRow = panel.getByTestId("property-row");
|
||||
const propertyRowCount = await propertyRow.count();
|
||||
|
||||
expect(propertyRowCount).toBeGreaterThanOrEqual(3);
|
||||
|
||||
const imagePreview = panel.getByRole("img", {
|
||||
name: "Preview of the shape's fill",
|
||||
});
|
||||
await expect(imagePreview).toBeVisible();
|
||||
});
|
||||
|
||||
test("Shape - Stroke - Token", async ({ page }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
await setupFile(workspacePage);
|
||||
|
||||
await selectLayer(workspacePage, shapeToLayerName.stroke.token);
|
||||
await openInspectTab(workspacePage);
|
||||
const panel = await getPanelByTitle(workspacePage, "Stroke");
|
||||
await expect(panel).toBeVisible();
|
||||
|
||||
const propertyRow = panel.getByTestId("property-row");
|
||||
const propertyRowCount = await propertyRow.count();
|
||||
|
||||
expect(propertyRowCount).toBeGreaterThanOrEqual(1);
|
||||
|
||||
const fillToken = propertyRow.filter({
|
||||
hasText: "Border color",
|
||||
});
|
||||
expect(fillToken).toContainText("primary");
|
||||
await expect(fillToken).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Inspect tab - Typography", () => {
|
||||
test("Text - simple", async ({ page }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
await setupFile(workspacePage);
|
||||
|
||||
await selectLayer(workspacePage, shapeToLayerName.text.simple);
|
||||
await openInspectTab(workspacePage);
|
||||
const panel = await getPanelByTitle(workspacePage, "Text");
|
||||
await expect(panel).toBeVisible();
|
||||
|
||||
const propertyRow = panel.getByTestId("property-row");
|
||||
const propertyRowCount = await propertyRow.count();
|
||||
|
||||
expect(propertyRowCount).toBeGreaterThanOrEqual(1);
|
||||
|
||||
const textPreview = panel.getByRole("presentation");
|
||||
await expect(textPreview).toBeVisible();
|
||||
});
|
||||
|
||||
test("Text - token", async ({ page }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
await setupFile(workspacePage);
|
||||
|
||||
await selectLayer(workspacePage, shapeToLayerName.text.token);
|
||||
await openInspectTab(workspacePage);
|
||||
const panel = await getPanelByTitle(workspacePage, "Text");
|
||||
await expect(panel).toBeVisible();
|
||||
|
||||
const propertyRow = panel.getByTestId("property-row");
|
||||
const propertyRowCount = await propertyRow.count();
|
||||
|
||||
expect(propertyRowCount).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// Test with multiple tokens
|
||||
const fontFamilyToken = propertyRow.filter({
|
||||
hasText: "Font family",
|
||||
});
|
||||
await expect(fontFamilyToken).toBeVisible();
|
||||
expect(fontFamilyToken).toContainText("font.sans");
|
||||
|
||||
const fontSizeToken = propertyRow.filter({
|
||||
hasText: "Font size",
|
||||
});
|
||||
await expect(fontSizeToken).toBeVisible();
|
||||
expect(fontSizeToken).toContainText("medium");
|
||||
|
||||
const fontWeightToken = propertyRow.filter({
|
||||
hasText: "Font weight",
|
||||
});
|
||||
await expect(fontWeightToken).toBeVisible();
|
||||
expect(fontWeightToken).toContainText("bold");
|
||||
|
||||
const textPreview = panel.getByRole("presentation");
|
||||
await expect(textPreview).toBeVisible();
|
||||
});
|
||||
test("Text - composite token", async ({ page }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
await setupFile(workspacePage);
|
||||
|
||||
await selectLayer(workspacePage, shapeToLayerName.text.compositeToken);
|
||||
await openInspectTab(workspacePage);
|
||||
const panel = await getPanelByTitle(workspacePage, "Text");
|
||||
await expect(panel).toBeVisible();
|
||||
|
||||
const propertyRow = panel.getByTestId("property-row");
|
||||
const propertyRowCount = await propertyRow.count();
|
||||
|
||||
expect(propertyRowCount).toBeGreaterThanOrEqual(1);
|
||||
|
||||
const compositeTypographyRow = propertyRow.filter({
|
||||
hasText: "Typography",
|
||||
});
|
||||
await expect(compositeTypographyRow).toBeVisible();
|
||||
expect(compositeTypographyRow).toContainText("body");
|
||||
|
||||
const textPreview = panel.getByRole("presentation");
|
||||
await expect(textPreview).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Copy properties", () => {
|
||||
test("Copy single property", async ({ page }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
await setupFile(workspacePage);
|
||||
|
||||
await selectLayer(workspacePage, shapeToLayerName.flex);
|
||||
await openInspectTab(workspacePage);
|
||||
|
||||
const panel = await getPanelByTitle(workspacePage, "Layout");
|
||||
await expect(panel).toBeVisible();
|
||||
|
||||
await copyPropertyFromPropertyRow(panel, "Display");
|
||||
|
||||
const shorthand = await page.evaluate(() =>
|
||||
navigator.clipboard.readText(),
|
||||
);
|
||||
expect(shorthand).toBe("display: flex;");
|
||||
});
|
||||
test("Copy shorthand - multiple properties", async ({ page }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
await setupFile(workspacePage);
|
||||
|
||||
await selectLayer(workspacePage, shapeToLayerName.shadow);
|
||||
await openInspectTab(workspacePage);
|
||||
|
||||
const panel = await getPanelByTitle(workspacePage, "Shadow");
|
||||
await expect(panel).toBeVisible();
|
||||
|
||||
await copyShorthand(panel);
|
||||
|
||||
const shorthand = await page.evaluate(() =>
|
||||
navigator.clipboard.readText(),
|
||||
);
|
||||
expect(shorthand).toBe("box-shadow: 4px 4px 4px 0px #00000033;");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -243,7 +243,7 @@
|
||||
(defn- persist-events
|
||||
[events]
|
||||
(if (seq events)
|
||||
(let [uri (u/join cf/public-uri "api/main/methods/push-audit-events")
|
||||
(let [uri (u/join cf/public-uri "api/rpc/command/push-audit-events")
|
||||
params {:uri uri
|
||||
:method :post
|
||||
:credentials "include"
|
||||
|
||||
@@ -56,19 +56,16 @@
|
||||
([shape]
|
||||
(resize-wasm-text-modifiers shape (:content shape)))
|
||||
|
||||
([{:keys [id points selrect grow-type] :as shape} content]
|
||||
([{:keys [id points selrect] :as shape} content]
|
||||
(wasm.api/use-shape id)
|
||||
(wasm.api/set-shape-text-content id content)
|
||||
(wasm.api/set-shape-text-images id content)
|
||||
|
||||
(let [dimension (wasm.api/get-text-dimensions)
|
||||
width-scale (if (#{:fixed :auto-height} grow-type)
|
||||
1.0
|
||||
(/ (:width dimension) (:width selrect)))
|
||||
height-scale (if (= :fixed grow-type)
|
||||
1.0
|
||||
(/ (:height dimension) (:height selrect)))
|
||||
resize-v (gpt/point width-scale height-scale)
|
||||
resize-v (gpt/point
|
||||
(/ (:width dimension) (-> selrect :width))
|
||||
(/ (:height dimension) (-> selrect :height)))
|
||||
|
||||
origin (first points)]
|
||||
|
||||
{id
|
||||
|
||||
@@ -115,7 +115,7 @@
|
||||
|
||||
request
|
||||
{:method method
|
||||
:uri (u/join cf/public-uri "api/main/methods/" nid)
|
||||
:uri (u/join cf/public-uri "api/rpc/command/" nid)
|
||||
:credentials "include"
|
||||
:headers {"accept" "application/transit+json,text/event-stream,*/*"
|
||||
"x-external-session-id" (cf/external-session-id)
|
||||
@@ -207,7 +207,7 @@
|
||||
(defmethod cmd! ::multipart-upload
|
||||
[id params]
|
||||
(->> (http/send! {:method :post
|
||||
:uri (u/join cf/public-uri "api/main/methods/" (name id))
|
||||
:uri (u/join cf/public-uri "api/rpc/command/" (name id))
|
||||
:credentials "include"
|
||||
:headers {"x-external-session-id" (cf/external-session-id)
|
||||
"x-event-origin" (::ev/origin (meta params))}
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
:default-value value
|
||||
:on-key-up on-key-up
|
||||
:max-length max-input-length
|
||||
:on-blur accept-edition}]
|
||||
:on-blur cancel-edition}]
|
||||
|
||||
[:span {:class [(stl/css :editable-label-text) class-label]
|
||||
:title tooltip}
|
||||
|
||||
@@ -89,14 +89,14 @@
|
||||
(let [value (rotate-option-backward options index length)]
|
||||
(swap! state* assoc :current-value value)
|
||||
(when (fn? on-change)
|
||||
(on-change value)))
|
||||
(on-change (dm/str value))))
|
||||
|
||||
(or (kbd/right-arrow? e)
|
||||
(kbd/down-arrow? e))
|
||||
(let [value (rotate-option-forward options index)]
|
||||
(swap! state* assoc :current-value value)
|
||||
(when (fn? on-change)
|
||||
(on-change value)))
|
||||
(on-change (dm/str value))))
|
||||
|
||||
(or (kbd/enter? e)
|
||||
(kbd/space? e))
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
[app.main.ui.ds.controls.input :refer [input*]]
|
||||
[app.main.ui.ds.controls.numeric-input :refer [numeric-input*]]
|
||||
[app.main.ui.ds.controls.select :refer [select*]]
|
||||
[app.main.ui.ds.controls.switch :refer [switch*]]
|
||||
[app.main.ui.ds.controls.utilities.hint-message :refer [hint-message*]]
|
||||
[app.main.ui.ds.controls.utilities.input-field :refer [input-field*]]
|
||||
[app.main.ui.ds.controls.utilities.label :refer [label*]]
|
||||
@@ -61,7 +60,6 @@
|
||||
:Loader loader*
|
||||
:RawSvg raw-svg*
|
||||
:Select select*
|
||||
:Switch switch*
|
||||
:Combobox combobox*
|
||||
:Text text*
|
||||
:TabSwitcher tab-switcher*
|
||||
|
||||
@@ -7,10 +7,9 @@
|
||||
@use "ds/_utils.scss" as *;
|
||||
|
||||
// TODO: create actual tokens once we have them from design
|
||||
$br-8: px2rem(8);
|
||||
$br-4: px2rem(4);
|
||||
$br-6: px2rem(6);
|
||||
$br-8: px2rem(8);
|
||||
$br-12: px2rem(12);
|
||||
$br-circle: 50%;
|
||||
|
||||
$b-1: px2rem(1);
|
||||
|
||||
@@ -79,7 +79,6 @@ $grayish-red: #bfbfbf;
|
||||
--color-accent-select: #{$purple-600-10};
|
||||
--color-accent-action: #{$purple-400};
|
||||
--color-accent-action-hover: #{$purple-500};
|
||||
--color-accent-off: #{$gray-50};
|
||||
|
||||
--color-accent-success: #{$green-500};
|
||||
--color-background-success: #{$green-200};
|
||||
@@ -93,7 +92,6 @@ $grayish-red: #bfbfbf;
|
||||
--color-background-default: #{$white};
|
||||
--color-accent-default: #{$gray-100};
|
||||
--color-icon-default: #{$blue-teal-700};
|
||||
--color-background-disabled: #{$gray-200};
|
||||
|
||||
--color-background-primary: #{$white};
|
||||
--color-background-secondary: #{$gray-200};
|
||||
@@ -107,7 +105,6 @@ $grayish-red: #bfbfbf;
|
||||
--color-static-black: #{$black};
|
||||
|
||||
--color-shadow-dark: #{color.change($gray-200, $alpha: 0.6)};
|
||||
--color-shadow-light: #{color.change($black, $alpha: 0.3)};
|
||||
--color-overlay-default: #{$white-60};
|
||||
--color-overlay-onboarding: #{$white-90};
|
||||
--color-canvas: #{$grayish-red};
|
||||
@@ -130,7 +127,6 @@ $grayish-red: #bfbfbf;
|
||||
--color-accent-select: #{$mint-250-10};
|
||||
--color-accent-action: #{$purple-400};
|
||||
--color-accent-action-hover: #{$purple-500};
|
||||
--color-accent-off: #{$gray-50};
|
||||
|
||||
--color-accent-success: #{$green-500};
|
||||
--color-background-success: #{$green-950};
|
||||
@@ -144,7 +140,6 @@ $grayish-red: #bfbfbf;
|
||||
--color-background-default: #{$gray-950};
|
||||
--color-accent-default: #{$gray-800};
|
||||
--color-icon-default: #{$grayish-blue-500};
|
||||
--color-background-disabled: #{$gray-800};
|
||||
|
||||
--color-background-primary: #{$gray-950};
|
||||
--color-background-secondary: #{$black};
|
||||
@@ -158,7 +153,6 @@ $grayish-red: #bfbfbf;
|
||||
--color-static-black: #{$black};
|
||||
|
||||
--color-shadow-dark: #{color.change($black, $alpha: 0.6)};
|
||||
--color-shadow-light: #{color.change($black, $alpha: 0.3)};
|
||||
--color-overlay-default: #{$gray-950-60};
|
||||
--color-overlay-onboarding: #{$gray-950-90};
|
||||
--color-canvas: #{$grayish-red};
|
||||
|
||||
@@ -202,9 +202,7 @@
|
||||
dom/get-value)]
|
||||
(reset! selected-id* value)
|
||||
(reset! filter-id* value)
|
||||
(reset! focused-id* nil)
|
||||
(when (fn? on-change)
|
||||
(on-change value)))))
|
||||
(reset! focused-id* nil))))
|
||||
|
||||
selected-option
|
||||
(mf/with-memo [options selected-id]
|
||||
|
||||
@@ -58,24 +58,14 @@
|
||||
|
||||
.variant-ghost {
|
||||
--select-background-color: transparent;
|
||||
--select-text-color: var(--color-foreground-secondary);
|
||||
|
||||
inline-size: fit-content;
|
||||
padding-inline: var(--sp-xxs);
|
||||
|
||||
& .arrow {
|
||||
margin-inline-start: var(--sp-xs);
|
||||
}
|
||||
|
||||
&:is(:hover, [aria-expanded="true"]) {
|
||||
--select-text-color: var(--color-foreground-primary);
|
||||
--select-icon-color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
&:is(:focus-visible, :disabled) {
|
||||
--select-background-color: transparent;
|
||||
--select-text-color: var(--color-foreground-primary);
|
||||
--select-icon-color: var(--color-foreground-primary);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -100,7 +100,8 @@
|
||||
|
||||
|
||||
(mf/defc options-dropdown*
|
||||
{::mf/schema schema:options-dropdown}
|
||||
;; TODO: Review schema
|
||||
;; {::mf/schema schema:options-dropdown}
|
||||
[{:keys [ref on-click options selected focused empty-to-end align] :rest props}]
|
||||
(let [align
|
||||
(d/nilv align :left)
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
[:focused {:optional true} :boolean]])
|
||||
|
||||
(mf/defc token-option*
|
||||
{::mf/schema schema:token-option}
|
||||
;; {::mf/schema schema:token-option}
|
||||
[{:keys [id name on-click selected ref focused resolved] :rest props}]
|
||||
(let [internal-id (mf/use-id)
|
||||
id (d/nilv id internal-id)]
|
||||
@@ -56,5 +56,12 @@
|
||||
[:span {:aria-labelledby (dm/str id "-name")}
|
||||
name]]
|
||||
(when resolved
|
||||
[:> :span {:class (stl/css :option-pill)}
|
||||
resolved])]))
|
||||
(cond
|
||||
(map? resolved)
|
||||
(for [[k v] resolved]
|
||||
[:div {:key (str k)}
|
||||
[:span (dm/str (d/name k) ": ")]
|
||||
[:strong (str v)]])
|
||||
:else
|
||||
[:span {:class (stl/css :option-pill)}
|
||||
resolved]))]))
|
||||
|
||||
@@ -1,77 +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.ds.controls.switch
|
||||
(:require-macros
|
||||
[app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.keyboard :as kbd]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def ^:private schema:switch
|
||||
[:map
|
||||
[:id {:optional true} :string]
|
||||
[:class {:optional true} :string]
|
||||
[:label {:optional true} [:maybe :string]]
|
||||
[:aria-label {:optional true} [:maybe :string]]
|
||||
[:default-checked {:optional true} [:maybe :boolean]]
|
||||
[:on-change {:optional true} [:maybe fn?]]
|
||||
[:disabled {:optional true} :boolean]])
|
||||
|
||||
(mf/defc switch*
|
||||
{::mf/schema schema:switch}
|
||||
[{:keys [id class label aria-label default-checked on-change disabled] :rest props} ref]
|
||||
(let [checked* (mf/use-state default-checked)
|
||||
checked? (deref checked*)
|
||||
|
||||
disabled? (d/nilv disabled false)
|
||||
|
||||
has-label? (not (str/blank? label))
|
||||
|
||||
handle-toggle
|
||||
(mf/use-fn
|
||||
(mf/deps on-change checked? disabled?)
|
||||
#(when-not disabled?
|
||||
(let [updated-checked? (not checked?)]
|
||||
(reset! checked* updated-checked?)
|
||||
(when on-change
|
||||
(on-change updated-checked?)))))
|
||||
|
||||
handle-keydown
|
||||
(mf/use-fn
|
||||
(mf/deps handle-toggle)
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(when-not disabled?
|
||||
(when (or (kbd/space? event) (kbd/enter? event))
|
||||
(handle-toggle event)))))
|
||||
|
||||
props
|
||||
(mf/spread-props props {:ref ref
|
||||
:role "switch"
|
||||
:aria-label (when-not has-label?
|
||||
aria-label)
|
||||
:class [class (stl/css-case :switch true
|
||||
:off (false? checked?)
|
||||
:neutral (nil? checked?)
|
||||
:on (true? checked?))]
|
||||
:aria-checked checked?
|
||||
:tab-index (if disabled? -1 0)
|
||||
:on-click handle-toggle
|
||||
:on-key-down handle-keydown
|
||||
:disabled disabled?})]
|
||||
|
||||
[:> :div props
|
||||
[:div {:id id
|
||||
:class (stl/css :switch-track)}
|
||||
[:div {:class (stl/css :switch-thumb)}]]
|
||||
(when has-label?
|
||||
[:label {:for id
|
||||
:class (stl/css :switch-label)}
|
||||
label])]))
|
||||
@@ -1,52 +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 */ }
|
||||
|
||||
import { Canvas, Meta } from '@storybook/addon-docs/blocks';
|
||||
import * as Switch from "./switch.stories";
|
||||
|
||||
<Meta title="Controls/Switch" />
|
||||
|
||||
# Switch
|
||||
|
||||
The `switch*` component is a toggle control. It allows users to switch between two mutually exclusive states (`false` or `true`), while also accepting an initial third state (`nil`).
|
||||
|
||||
<Canvas of={Switch.Default} />
|
||||
|
||||
## Anatomy
|
||||
|
||||
The switch component consists of three main parts:
|
||||
|
||||
- **Label** (optional): the text that describes what the switch controls. Clicking on this text also works for toggling.
|
||||
- **Track**: the pill-shaped background.
|
||||
- **Thumb**: the knob that moves between positions.
|
||||
|
||||
## Basic usage
|
||||
|
||||
```clj
|
||||
[:> switch* {:label "Toggle something"
|
||||
:default-checked nil
|
||||
:on-change handle-change
|
||||
:disabled false}]
|
||||
```
|
||||
|
||||
## Accesibility
|
||||
|
||||
When no visible label is provided, use `:aria-label` for accessibility.
|
||||
|
||||
## Usage Guidelines
|
||||
|
||||
### When to Use
|
||||
|
||||
- For boolean settings that take effect immediately.
|
||||
- In preference panels and configuration screens.
|
||||
- For ternary states where the third state is only relevant at the beginning (e.g., selection of multiple elements with opposite states).
|
||||
|
||||
### When Not to Use
|
||||
|
||||
- For actions that require confirmation (use buttons instead).
|
||||
- For multiple choice selections (use radio buttons or select).
|
||||
- For temporary states that need explicit "Apply" action.
|
||||
- For ternary states that require passing through all three states.
|
||||
@@ -1,118 +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 "ds/_borders.scss" as *;
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@use "ds/_utils.scss" as *;
|
||||
@use "ds/colors.scss" as *;
|
||||
@use "ds/spacing.scss" as *;
|
||||
@use "ds/typography.scss" as t;
|
||||
|
||||
.switch {
|
||||
--switch-label-foreground-color: var(--color-foreground-primary);
|
||||
|
||||
--switch-track-outline-color: none;
|
||||
--switch-track-shadow: inset 0 1px 2px var(--color-shadow-light);
|
||||
|
||||
--switch-thumb-shadow: 0 1px 2px var(--color-shadow-light);
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: center;
|
||||
gap: var(--sp-s);
|
||||
inline-size: fit-content;
|
||||
outline: none;
|
||||
|
||||
&.off {
|
||||
--switch-track-justify-content: start;
|
||||
--switch-track-background-color: var(--color-foreground-secondary);
|
||||
|
||||
--switch-thumb-width: #{px2rem(14)};
|
||||
--switch-thumb-height: #{px2rem(14)};
|
||||
--switch-thumb-background-color: var(--color-accent-off);
|
||||
--switch-thumb-border-radius: #{$br-circle};
|
||||
}
|
||||
|
||||
&.neutral {
|
||||
--switch-track-justify-content: center;
|
||||
--switch-track-background-color: var(--color-accent-tertiary);
|
||||
|
||||
--switch-thumb-width: #{px2rem(14)};
|
||||
--switch-thumb-height: #{px2rem(4)};
|
||||
--switch-thumb-background-color: var(--color-accent-off);
|
||||
--switch-thumb-border-radius: #{$br-8};
|
||||
}
|
||||
|
||||
&.on {
|
||||
--switch-track-justify-content: end;
|
||||
--switch-track-background-color: var(--color-accent-tertiary);
|
||||
|
||||
--switch-thumb-width: #{px2rem(14)};
|
||||
--switch-thumb-height: #{px2rem(14)};
|
||||
--switch-thumb-background-color: var(--color-accent-off);
|
||||
--switch-thumb-border-radius: #{$br-circle};
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
pointer-events: none;
|
||||
--switch-label-foreground-color: var(--color-foreground-secondary);
|
||||
|
||||
--switch-track-shadow: none;
|
||||
|
||||
--switch-thumb-shadow: none;
|
||||
}
|
||||
|
||||
&.off[disabled] {
|
||||
--switch-track-background-color: var(--color-background-primary);
|
||||
--switch-track-border-color: var(--color-background-disabled);
|
||||
|
||||
--switch-thumb-background-color: var(--color-background-disabled);
|
||||
}
|
||||
|
||||
&.on[disabled],
|
||||
&.neutral[disabled] {
|
||||
--switch-track-background-color: var(--color-background-disabled);
|
||||
|
||||
--switch-thumb-background-color: var(--color-background-primary);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
--switch-track-outline-color: var(--color-accent-primary);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
--switch-thumb-background-color: var(--color-static-white);
|
||||
}
|
||||
}
|
||||
|
||||
.switch-label {
|
||||
@include t.use-typography("body-small");
|
||||
color: var(--switch-label-foreground-color);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.switch-track {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: var(--switch-track-justify-content);
|
||||
inline-size: px2rem(30);
|
||||
block-size: px2rem(18);
|
||||
padding: var(--sp-xxs);
|
||||
border-radius: $br-12;
|
||||
border: $b-1 solid var(--switch-track-border-color);
|
||||
outline: $b-1 solid var(--switch-track-outline-color);
|
||||
outline-offset: $b-1;
|
||||
box-shadow: var(--switch-track-shadow);
|
||||
background-color: var(--switch-track-background-color);
|
||||
}
|
||||
|
||||
.switch-thumb {
|
||||
inline-size: var(--switch-thumb-width);
|
||||
block-size: var(--switch-thumb-height);
|
||||
border-radius: var(--switch-thumb-border-radius);
|
||||
box-shadow: var(--switch-thumb-shadow);
|
||||
background-color: var(--switch-thumb-background-color);
|
||||
}
|
||||
@@ -1,65 +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
|
||||
|
||||
import * as React from "react";
|
||||
import Components from "@target/components";
|
||||
|
||||
const { Switch } = Components;
|
||||
|
||||
export default {
|
||||
title: "Controls/Switch",
|
||||
component: Switch,
|
||||
argTypes: {
|
||||
label: {
|
||||
control: { type: "text" },
|
||||
description: "Label text displayed next to the switch",
|
||||
},
|
||||
disabled: {
|
||||
control: { type: "boolean" },
|
||||
description: "Whether the switch is disabled",
|
||||
}
|
||||
},
|
||||
args: {
|
||||
disabled: false
|
||||
},
|
||||
parameters: {
|
||||
controls: { exclude: ["id", "class", "aria-label", "default-checked", "on-change"] },
|
||||
},
|
||||
render: ({ ...args }) => (
|
||||
<Switch {...args} />
|
||||
),
|
||||
};
|
||||
|
||||
export const Default = {
|
||||
args: {
|
||||
label: "Toggle something",
|
||||
disabled: false,
|
||||
},
|
||||
render: ({ ...args }) => (
|
||||
<Switch {...args} />
|
||||
),
|
||||
};
|
||||
|
||||
export const WithoutLabel = {
|
||||
args: {
|
||||
disabled: false,
|
||||
},
|
||||
render: ({ ...args }) => (
|
||||
<Switch {...args} />
|
||||
),
|
||||
};
|
||||
|
||||
export const WithLongLabel = {
|
||||
args: {
|
||||
label: "This is a very long label that demonstrates how the switch component handles text wrapping and layout when the label content is extensive",
|
||||
disabled: false,
|
||||
},
|
||||
render: ({ ...args }) => (
|
||||
<div style={{ maxWidth: "300px" }}>
|
||||
<Switch {...args} />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
@@ -176,7 +176,7 @@
|
||||
props
|
||||
(mf/spread-props props {:class [class (stl/css :tabs)]})]
|
||||
|
||||
[:> :div props
|
||||
[:> :article props
|
||||
[:div {:class (stl/css :padding-wrapper)}
|
||||
[:> tab-nav* {:button-position action-button-position
|
||||
:action-button action-button
|
||||
|
||||
@@ -25,25 +25,20 @@
|
||||
(mf/defc input-with-meta*
|
||||
{::mf/schema schema:input-with-meta}
|
||||
[{:keys [value meta max-length is-editing on-blur] :rest props}]
|
||||
(let [title (if meta (str value ": " meta) value)
|
||||
editing* (mf/use-state (d/nilv is-editing false))
|
||||
editing? (deref editing*)
|
||||
input-ref (mf/use-ref)
|
||||
last-node* (mf/use-ref nil)
|
||||
(let [editing* (mf/use-state (d/nilv is-editing false))
|
||||
editing? (deref editing*)
|
||||
|
||||
ref-cb (mf/use-fn
|
||||
(fn [node]
|
||||
;; We need to keep the last not-null node for cleanup
|
||||
(mf/set-ref-val! input-ref node)
|
||||
(when node
|
||||
(mf/set-ref-val! last-node* node))))
|
||||
input-ref (mf/use-ref)
|
||||
input (mf/ref-val input-ref)
|
||||
|
||||
title (if meta (str value ": " meta) value)
|
||||
|
||||
on-edit
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(reset! editing* true)
|
||||
(dom/focus! (mf/ref-val input-ref))))
|
||||
(dom/focus! input)))
|
||||
|
||||
on-stop-edit
|
||||
(mf/use-fn
|
||||
@@ -69,7 +64,7 @@
|
||||
(when ^boolean enter? (dom/blur! node))
|
||||
(when ^boolean esc? (dom/blur! node)))))
|
||||
|
||||
props (mf/spread-props props {:ref ref-cb
|
||||
props (mf/spread-props props {:ref input-ref
|
||||
:default-value value
|
||||
:max-length (d/nilv max-length max-input-length)
|
||||
:auto-focus true
|
||||
@@ -77,18 +72,6 @@
|
||||
:on-blur on-stop-edit
|
||||
:on-key-down handle-key-down})]
|
||||
|
||||
;; Cleanup: Simulate a blur event
|
||||
(mf/with-effect [on-blur last-node*]
|
||||
(fn []
|
||||
(let [input (mf/ref-val last-node*)
|
||||
fake-blur-event #js {:type "blur"
|
||||
:target input
|
||||
:currentTarget input
|
||||
:stopPropagation (fn [])
|
||||
:preventDefault (fn [])}]
|
||||
(when input
|
||||
(on-blur fake-blur-event)))))
|
||||
|
||||
(if editing?
|
||||
[:div {:class (stl/css :input-with-meta-edit-container)}
|
||||
[:> input* props]]
|
||||
|
||||
@@ -250,7 +250,6 @@
|
||||
[:> style-box* {:panel :shadow
|
||||
:shorthand (:shadow shorthands)}
|
||||
[:> shadow-panel* {:shapes shapes
|
||||
:resolved-tokens resolved-active-tokens
|
||||
:color-space color-space
|
||||
:on-shadow-shorthand set-shorthands}]]))
|
||||
|
||||
|
||||
@@ -15,17 +15,6 @@
|
||||
[app.util.code-gen.style-css-formats :as scf]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn- get-applied-tokens-in-shape
|
||||
[shape-tokens property]
|
||||
(get shape-tokens property))
|
||||
|
||||
(defn- get-resolved-token
|
||||
[property shape resolved-tokens]
|
||||
(let [shape-tokens (:applied-tokens shape)
|
||||
applied-tokens-in-shape (get-applied-tokens-in-shape shape-tokens property)
|
||||
token (get resolved-tokens applied-tokens-in-shape)]
|
||||
token))
|
||||
|
||||
(defn- generate-shadow-shorthand
|
||||
[shapes]
|
||||
(when (= (count shapes) 1)
|
||||
@@ -41,7 +30,7 @@
|
||||
(dm/str shorthand-property shorthand-value ";"))))
|
||||
|
||||
(mf/defc shadow-panel*
|
||||
[{:keys [shapes resolved-tokens color-space on-shadow-shorthand]}]
|
||||
[{:keys [shapes color-space on-shadow-shorthand]}]
|
||||
(let [shorthand* (mf/use-state #(generate-shadow-shorthand shapes))
|
||||
shorthand (deref shorthand*)]
|
||||
(mf/use-effect
|
||||
@@ -52,25 +41,16 @@
|
||||
:property shorthand})))
|
||||
[:div {:class (stl/css :shadow-panel)}
|
||||
(for [shape shapes]
|
||||
(let [composite-shadow-token (get-resolved-token :shadow shape resolved-tokens)]
|
||||
(for [[idx shadow] (map-indexed vector (:shadow shape))]
|
||||
[:div {:key (dm/str idx) :class (stl/css :shadow-shape)}
|
||||
(when composite-shadow-token
|
||||
[:> properties-row* {:term "Shadow"
|
||||
:detail (:name composite-shadow-token)
|
||||
:token composite-shadow-token
|
||||
:property (:name composite-shadow-token)
|
||||
:copiable true}])
|
||||
[:> color-properties-row* {:term "Shadow Color"
|
||||
:color (:color shadow)
|
||||
:format color-space
|
||||
:copiable true}]
|
||||
|
||||
(let [value (dm/str (:offset-x shadow) "px" " " (:offset-y shadow) "px" " " (:blur shadow) "px" " " (:spread shadow) "px")
|
||||
property-name (cmm/get-css-rule-humanized (:style shadow))
|
||||
property-value (css/shadow->css shadow)]
|
||||
|
||||
[:> properties-row* {:term property-name
|
||||
:detail (dm/str value)
|
||||
:property property-value
|
||||
:copiable true}])])))]))
|
||||
(for [[idx shadow] (map-indexed vector (:shadow shape))]
|
||||
[:div {:key (dm/str idx) :class (stl/css :shadow-shape)}
|
||||
[:> color-properties-row* {:term "Shadow Color"
|
||||
:color (:color shadow)
|
||||
:format color-space
|
||||
:copiable true}]
|
||||
(let [value (dm/str (:offset-x shadow) "px" " " (:offset-y shadow) "px" " " (:blur shadow) "px" " " (:spread shadow) "px")
|
||||
property-name (cmm/get-css-rule-humanized (:style shadow))
|
||||
property-value (css/shadow->css shadow)]
|
||||
[:> properties-row* {:term property-name
|
||||
:detail (dm/str value)
|
||||
:property property-value
|
||||
:copiable true}])]))]))
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
property-value (:name typography)]
|
||||
(when typography
|
||||
[:> properties-row* {:term "Typography"
|
||||
:detail property-value
|
||||
:detail (:name typography)
|
||||
:property property-value
|
||||
:copiable true}])))
|
||||
|
||||
@@ -197,8 +197,7 @@
|
||||
(reset! copied* true)
|
||||
(wapi/write-to-clipboard formatted-text)
|
||||
(tm/schedule 1000 #(reset! copied* false)))))]
|
||||
[:pre {:class (stl/css :text-content-wrapper)
|
||||
:role "presentation"}
|
||||
[:div {:class (stl/css :text-content-wrapper)}
|
||||
[:> property-detail-copiable* {:copied copied
|
||||
:on-click copy-text}
|
||||
[:span {:class (stl/css :text-content)
|
||||
|
||||
@@ -89,8 +89,7 @@
|
||||
(wapi/write-to-clipboard copiable-value)
|
||||
(tm/schedule 1000 #(reset! copied* false))))]
|
||||
[:*
|
||||
[:dl {:class [(stl/css :property-row) class]
|
||||
:data-testid "property-row"}
|
||||
[:dl {:class [(stl/css :property-row) class]}
|
||||
[:dt {:class (stl/css :property-term)} term]
|
||||
[:dd {:class (stl/css :property-detail)}
|
||||
(if token
|
||||
@@ -112,12 +111,12 @@
|
||||
:copied copied
|
||||
:on-click copy-attr} formatted-color-value])]]
|
||||
(when (:image color)
|
||||
[:figure {:class (stl/css :color-image-preview)}
|
||||
[:div {:class (stl/css :color-image-preview)}
|
||||
[:div {:class (stl/css :color-image-preview-wrapper)}
|
||||
[:img {:class (stl/css :color-image)
|
||||
:src color-image-url
|
||||
:title color-image-name
|
||||
:alt (tr "inspect.attributes.image.preview")}]]
|
||||
:alt ""}]]
|
||||
[:> button* {:variant "secondary"
|
||||
:to color-image-url
|
||||
:target "_blank"
|
||||
|
||||
@@ -45,37 +45,24 @@
|
||||
(reset! copied* true)
|
||||
(wapi/write-to-clipboard copiable-value)
|
||||
(tm/schedule 1000 #(reset! copied* false))))]
|
||||
[:dl {:class [(stl/css :property-row) class]
|
||||
:data-testid "property-row"}
|
||||
[:dl {:class [(stl/css :property-row) class]}
|
||||
[:dt {:class (stl/css :property-term)} term]
|
||||
[:dd {:class (stl/css :property-detail)}
|
||||
(if copiable?
|
||||
(if token
|
||||
(let [token-type (:type token)]
|
||||
[:> tooltip* {:id (:name token)
|
||||
:class (stl/css :tooltip-token-wrapper)
|
||||
:content #(mf/html
|
||||
[:div {:class (stl/css :tooltip-token)}
|
||||
[:div {:class (stl/css :tooltip-token-title)}
|
||||
(tr "inspect.tabs.styles.token.resolved-value")]
|
||||
[:div {:class (stl/css :tooltip-token-value)}
|
||||
(cond
|
||||
(= :typography token-type)
|
||||
[:ul {:class (stl/css :tooltip-token-resolved-values)}
|
||||
(for [[property value] (:resolved-value token)]
|
||||
[:li {:key property}
|
||||
(str (category-dictionary property) ": " (format-token-value value))])]
|
||||
(= :shadow token-type)
|
||||
[:ul {:class (stl/css :tooltip-token-resolved-values)}
|
||||
(for [property (:resolved-value token)
|
||||
[key value] property]
|
||||
[:li {:key key}
|
||||
(str (category-dictionary key) ": " (format-token-value value))])]
|
||||
:else
|
||||
(:resolved-value token))]])}
|
||||
[:> property-detail-copiable* {:token token
|
||||
:copied copied
|
||||
:on-click copy-attr} detail]])
|
||||
[:> tooltip* {:id (:name token)
|
||||
:class (stl/css :tooltip-token-wrapper)
|
||||
:content #(mf/html
|
||||
[:div {:class (stl/css :tooltip-token)}
|
||||
[:div {:class (stl/css :tooltip-token-title)} (tr "inspect.tabs.styles.token.resolved-value")]
|
||||
[:div {:class (stl/css :tooltip-token-value)} (if (= :typography (:type token))
|
||||
[:ul {:class (stl/css :tooltip-token-resolved-values)}
|
||||
(for [[property value] (:resolved-value token)]
|
||||
[:li {:key property} (str (category-dictionary property) ": " (format-token-value value))])]
|
||||
(:resolved-value token))]])}
|
||||
[:> property-detail-copiable* {:token token
|
||||
:copied copied
|
||||
:on-click copy-attr} detail]]
|
||||
[:> property-detail-copiable* {:copied copied
|
||||
:on-click copy-attr} detail])
|
||||
detail)]]))
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
[app.common.pprint :as pp]
|
||||
[app.common.uri :as u]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.main.data.auth :refer [is-authenticated?]]
|
||||
[app.main.data.common :as dcm]
|
||||
[app.main.data.event :as ev]
|
||||
@@ -65,7 +64,7 @@
|
||||
[:div {:class (stl/css :container)} children]]
|
||||
|
||||
[:div {:class (stl/css :deco-after2)}
|
||||
[:span (tr "labels.copyright-period")]
|
||||
[:span (tr "labels.copyright")]
|
||||
deprecated-icon/logo-error-screen
|
||||
[:span (tr "not-found.made-with-love")]]]))
|
||||
|
||||
@@ -316,10 +315,9 @@
|
||||
trace (:app.main.errors/trace data)
|
||||
instance (:app.main.errors/instance data)]
|
||||
(with-out-str
|
||||
(println "Hint: " (or (:hint data) (ex-message instance) "--"))
|
||||
(println "Prof ID: " (str (or profile-id "--")))
|
||||
(println "Team ID: " (str (or team-id "--")))
|
||||
(println "URI: " cf/public-uri)
|
||||
(println "Hint: " (or (:hint data) (ex-message instance) "--"))
|
||||
(println "Prof ID:" (str (or profile-id "--")))
|
||||
(println "Team ID:" (str (or team-id "--")))
|
||||
|
||||
(when-let [file-id (:file-id data)]
|
||||
(println "File ID:" (str file-id)))
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
(dom/set-css-property! node "--saturation-grad-to" (format-hsl hsl-to)))))
|
||||
|
||||
(mf/defc colorpicker
|
||||
[{:keys [data disable-gradient disable-opacity disable-image on-change on-accept origin combined-tokens color-origin on-token-change tab applied-token]}]
|
||||
[{:keys [data disable-gradient disable-opacity disable-image on-change on-accept origin combined-tokens color-origin on-token-change tab]}]
|
||||
(let [state (mf/deref refs/colorpicker)
|
||||
node-ref (mf/use-ref)
|
||||
|
||||
@@ -542,7 +542,6 @@
|
||||
|
||||
[:> token-section* {:combined-tokens combined-tokens
|
||||
:on-token-change on-token-change
|
||||
:applied-token applied-token
|
||||
:color-origin color-origin}])]
|
||||
(when (fn? on-accept)
|
||||
[:div {:class (stl/css :actions)}
|
||||
@@ -729,7 +728,6 @@
|
||||
on-token-change
|
||||
on-close
|
||||
tab
|
||||
applied-token
|
||||
on-accept]}]
|
||||
(let [vport (mf/deref viewport)
|
||||
dirty? (mf/use-var false)
|
||||
@@ -792,7 +790,6 @@
|
||||
:disable-opacity disable-opacity
|
||||
:disable-image disable-image
|
||||
:on-token-change on-token-change
|
||||
:applied-token applied-token
|
||||
:on-change on-change'
|
||||
:origin origin
|
||||
:tab tab
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
|
||||
(mf/defc set-section*
|
||||
{::mf/private true}
|
||||
[{:keys [collapsed toggle-sets-open group-or-set name color-origin on-token-change applied-token] :rest props}]
|
||||
[{:keys [collapsed toggle-sets-open group-or-set name color-origin on-token-change] :rest props}]
|
||||
|
||||
(let [list-style* (mf/use-state :list)
|
||||
list-style (deref list-style*)
|
||||
@@ -207,7 +207,8 @@
|
||||
(let [selected? (case color-origin
|
||||
:fill (= has-color-tokens? (:name token))
|
||||
:stroke-color (= has-stroke-tokens? (:name token))
|
||||
:color-selection (= applied-token (:name token))
|
||||
:color-selection (or (= has-color-tokens? (:name token))
|
||||
(= has-stroke-tokens? (:name token)))
|
||||
false)]
|
||||
(if (= :grid list-style)
|
||||
[:> grid-item* {:key (str "token-grid-" (:id token))
|
||||
@@ -265,7 +266,7 @@
|
||||
|
||||
(mf/defc token-section*
|
||||
{}
|
||||
[{:keys [combined-tokens color-origin on-token-change applied-token] :rest props}]
|
||||
[{:keys [combined-tokens color-origin on-token-change] :rest props}]
|
||||
(let [sets (set (mapv label-group-or-set combined-tokens))
|
||||
filter-term* (mf/use-state "")
|
||||
filter-term (deref filter-term*)
|
||||
@@ -310,7 +311,6 @@
|
||||
:color-origin color-origin
|
||||
:on-token-change on-token-change
|
||||
:name name
|
||||
:applied-token applied-token
|
||||
:group-or-set combined-sets}]))]
|
||||
[:> token-empty-state*])]
|
||||
[:> token-empty-state*])))
|
||||
|
||||
@@ -88,9 +88,6 @@
|
||||
|
||||
.color-tokens-inputs {
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
|
||||
// Title bar
|
||||
|
||||
@@ -35,13 +35,11 @@
|
||||
}
|
||||
|
||||
[data-itype="inline"] {
|
||||
box-sizing: content-box;
|
||||
display: inline;
|
||||
line-break: auto;
|
||||
line-height: inherit;
|
||||
caret-color: var(--text-editor-caret-color);
|
||||
white-space-collapse: pre;
|
||||
word-break: normal;
|
||||
overflow-wrap: break-word;
|
||||
tab-size: 2;
|
||||
-o-tab-size: 2;
|
||||
}
|
||||
|
||||
@@ -155,61 +155,65 @@
|
||||
|
||||
tabs-action-button
|
||||
(mf/with-memo []
|
||||
(mf/html [:> collapse-button* {}]))]
|
||||
(mf/html [:> collapse-button* {}]))
|
||||
|
||||
[:> (mf/provider muc/sidebar) {:value :left}
|
||||
[:aside {:ref parent-ref
|
||||
:id "left-sidebar-aside"
|
||||
:data-testid "left-sidebar"
|
||||
:data-left-sidebar-width (str width)
|
||||
:class aside-class
|
||||
:style {:--left-sidebar-width (dm/str width "px")}}
|
||||
active-tokens-by-type
|
||||
(mf/with-memo [resolved-active-tokens]
|
||||
(delay (ctob/group-by-type resolved-active-tokens)))]
|
||||
|
||||
[:> left-header*
|
||||
{:file file
|
||||
:layout layout
|
||||
:project project
|
||||
:page-id page-id
|
||||
:class (stl/css :left-header)}]
|
||||
[:> (mf/provider muc/active-tokens-by-type) {:value active-tokens-by-type}
|
||||
[:> (mf/provider muc/sidebar) {:value :left}
|
||||
[:aside {:ref parent-ref
|
||||
:id "left-sidebar-aside"
|
||||
:data-testid "left-sidebar"
|
||||
:data-left-sidebar-width (str width)
|
||||
:class aside-class
|
||||
:style {:--left-sidebar-width (dm/str width "px")}}
|
||||
|
||||
[:div {:on-pointer-down on-pointer-down
|
||||
:on-lost-pointer-capture on-lost-pointer-capture
|
||||
:on-pointer-move on-pointer-move
|
||||
:class (stl/css :resize-area)}]
|
||||
[:> left-header*
|
||||
{:file file
|
||||
:layout layout
|
||||
:project project
|
||||
:page-id page-id
|
||||
:class (stl/css :left-header)}]
|
||||
|
||||
(cond
|
||||
(true? shortcuts?)
|
||||
[:> shortcuts-container* {:class (stl/css :settings-bar-content)}]
|
||||
[:div {:on-pointer-down on-pointer-down
|
||||
:on-lost-pointer-capture on-lost-pointer-capture
|
||||
:on-pointer-move on-pointer-move
|
||||
:class (stl/css :resize-area)}]
|
||||
|
||||
(true? show-debug?)
|
||||
[:> debug-panel* {:class (stl/css :settings-bar-content)}]
|
||||
(cond
|
||||
(true? shortcuts?)
|
||||
[:> shortcuts-container* {:class (stl/css :settings-bar-content)}]
|
||||
|
||||
:else
|
||||
[:div {:class (stl/css :settings-bar-content)}
|
||||
[:> tab-switcher* {:tabs tabs
|
||||
:default "layers"
|
||||
:selected (name section)
|
||||
:on-change on-tab-change
|
||||
:class (stl/css :left-sidebar-tabs)
|
||||
:action-button-position "start"
|
||||
:action-button tabs-action-button}
|
||||
(true? show-debug?)
|
||||
[:> debug-panel* {:class (stl/css :settings-bar-content)}]
|
||||
|
||||
(case section
|
||||
:assets
|
||||
[:> assets-toolbox*
|
||||
{:size (- width 58)
|
||||
:file-id file-id}]
|
||||
:else
|
||||
[:div {:class (stl/css :settings-bar-content)}
|
||||
[:> tab-switcher* {:tabs tabs
|
||||
:default "layers"
|
||||
:selected (name section)
|
||||
:on-change on-tab-change
|
||||
:class (stl/css :left-sidebar-tabs)
|
||||
:action-button-position "start"
|
||||
:action-button tabs-action-button}
|
||||
|
||||
:tokens
|
||||
[:> tokens-sidebar-tab*
|
||||
{:tokens-lib tokens-lib
|
||||
:active-tokens active-tokens
|
||||
:resolved-active-tokens resolved-active-tokens}]
|
||||
(case section
|
||||
:assets
|
||||
[:> assets-toolbox*
|
||||
{:size (- width 58)
|
||||
:file-id file-id}]
|
||||
|
||||
:layers
|
||||
[:> layers-content*
|
||||
{:layout layout
|
||||
:width width}])]])]]))
|
||||
:tokens
|
||||
[:> tokens-sidebar-tab*
|
||||
{:tokens-lib tokens-lib
|
||||
:active-tokens active-tokens}]
|
||||
|
||||
:layers
|
||||
[:> layers-content*
|
||||
{:layout layout
|
||||
:width width}])]])]]]))
|
||||
|
||||
;; --- Right Sidebar (Component)
|
||||
|
||||
|
||||
@@ -38,7 +38,6 @@
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.controls.combobox :refer [combobox*]]
|
||||
[app.main.ui.ds.controls.select :refer [select*]]
|
||||
[app.main.ui.ds.controls.switch :refer [switch*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
|
||||
[app.main.ui.ds.product.input-with-meta :refer [input-with-meta*]]
|
||||
[app.main.ui.hooks :as h]
|
||||
@@ -502,26 +501,15 @@
|
||||
(reset! key* (uuid/next))
|
||||
(st/emit! (ntf/error error-msg)))}
|
||||
params {:shapes shapes :pos pos :val val}]
|
||||
(st/emit! (dwv/variants-switch (with-meta params mdata)))))))
|
||||
|
||||
switch-component-toggle
|
||||
(mf/use-fn
|
||||
(mf/deps shapes)
|
||||
(fn [pos boolean-pair val]
|
||||
(let [inverted-boolean-pair (d/invert-map boolean-pair)
|
||||
val (get inverted-boolean-pair val)]
|
||||
(switch-component pos val))))]
|
||||
(st/emit! (dwv/variants-switch (with-meta params mdata)))))))]
|
||||
|
||||
[:*
|
||||
[:div {:class (stl/css :variant-property-list)}
|
||||
(for [[pos prop] (map-indexed vector props-first)]
|
||||
(let [mixed-value? (not-every? #(= (:value prop) (:value (nth % pos))) properties)
|
||||
options (get-options (:name prop))
|
||||
boolean-pair (ctv/find-boolean-pair (mapv :id options))
|
||||
options (cond-> options
|
||||
mixed-value?
|
||||
(conj {:id mixed-label :label mixed-label :dimmed true}))]
|
||||
|
||||
options (cond-> (get-options (:name prop))
|
||||
mixed-value?
|
||||
(conj {:id mixed-label, :label mixed-label :dimmed true}))]
|
||||
[:div {:key (str pos mixed-value?)
|
||||
:class (stl/css :variant-property-container)}
|
||||
|
||||
@@ -530,17 +518,12 @@
|
||||
[:div {:class (stl/css :variant-property-name)}
|
||||
(:name prop)]]
|
||||
|
||||
(if boolean-pair
|
||||
[:div {:class (stl/css :variant-property-value-switch-wrapper)}
|
||||
[:> switch* {:default-checked (if mixed-value? nil (get boolean-pair (:value prop)))
|
||||
:on-change (partial switch-component-toggle pos boolean-pair)
|
||||
:key (str (:value prop) "-" key)}]]
|
||||
[:div {:class (stl/css :variant-property-value-wrapper)}
|
||||
[:> select* {:default-selected (if mixed-value? mixed-label (:value prop))
|
||||
:options options
|
||||
:empty-to-end true
|
||||
:on-change (partial switch-component pos)
|
||||
:key (str (:value prop) "-" key)}]])]))]
|
||||
[:div {:class (stl/css :variant-property-value-wrapper)}
|
||||
[:> select* {:default-selected (if mixed-value? mixed-label (:value prop))
|
||||
:options options
|
||||
:empty-to-end true
|
||||
:on-change (partial switch-component pos)
|
||||
:key (str (:value prop) "-" key)}]]]))]
|
||||
|
||||
(if (seq malformed-comps)
|
||||
[:div {:class (stl/css :variant-warning)}
|
||||
|
||||
@@ -662,17 +662,8 @@
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.variant-property-value-switch-wrapper {
|
||||
grid-column: span 5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
block-size: $sz-32;
|
||||
padding-inline-start: var(--sp-s);
|
||||
}
|
||||
|
||||
.variant-property-name {
|
||||
@include t.use-typography("body-small");
|
||||
margin-inline-start: var(--sp-s);
|
||||
color: var(--color-foreground-secondary);
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
|
||||
.flex-element-menu {
|
||||
@include sidebar.option-grid-structure;
|
||||
gap: var(--sp-xs);
|
||||
}
|
||||
|
||||
.behaviour-menu {
|
||||
|
||||
@@ -301,7 +301,7 @@
|
||||
(mf/use-fn
|
||||
(mf/deps ids shapes)
|
||||
(fn [value attr]
|
||||
(if (or (string? value) (number? value))
|
||||
(if (or (string? value) (int? value))
|
||||
(do
|
||||
(st/emit! (udw/trigger-bounding-box-cloaking ids))
|
||||
(run! #(do-size-change value attr) shapes))
|
||||
@@ -330,7 +330,7 @@
|
||||
(mf/use-fn
|
||||
(mf/deps ids)
|
||||
(fn [value attr]
|
||||
(if (or (string? value) (number? value))
|
||||
(if (or (string? value) (int? value))
|
||||
(do
|
||||
(st/emit! (udw/trigger-bounding-box-cloaking ids))
|
||||
(run! #(do-position-change %1 value attr) shapes))
|
||||
@@ -353,7 +353,7 @@
|
||||
(mf/use-fn
|
||||
(mf/deps ids)
|
||||
(fn [value]
|
||||
(if (or (string? value) (number? value))
|
||||
(if (or (string? value) (int? value))
|
||||
(do
|
||||
(st/emit! (udw/trigger-bounding-box-cloaking ids))
|
||||
(run! #(do-rotation-change value) shapes))
|
||||
|
||||
@@ -239,7 +239,7 @@
|
||||
|
||||
open-modal
|
||||
(mf/use-fn
|
||||
(mf/deps disable-gradient disable-opacity disable-image disable-picker on-change on-close on-open tokens index applied-token)
|
||||
(mf/deps disable-gradient disable-opacity disable-image disable-picker on-change on-close on-open tokens index)
|
||||
(fn [color pos tab]
|
||||
(let [color (cond
|
||||
^boolean has-multiple-colors
|
||||
@@ -263,7 +263,6 @@
|
||||
(when on-close
|
||||
(on-close value opacity id file-id)))
|
||||
:active-tokens tokens
|
||||
:applied-token applied-token
|
||||
:color-origin origin
|
||||
:tab tab
|
||||
:origin :sidebar
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
[app.main.data.workspace.tokens.library-edit :as dwtl]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.context :as muc]
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
|
||||
[app.main.ui.ds.foundations.typography.text :refer [text*]]
|
||||
[app.main.ui.workspace.tokens.management.context-menu :refer [token-context-menu]]
|
||||
@@ -115,7 +116,9 @@
|
||||
|
||||
[empty-group filled-group]
|
||||
(mf/with-memo [tokens-by-type]
|
||||
(get-sorted-token-groups tokens-by-type))]
|
||||
(get-sorted-token-groups tokens-by-type))
|
||||
|
||||
active-theme-tokens (mf/use-ctx muc/active-tokens-by-type)]
|
||||
|
||||
(mf/with-effect [tokens-lib selected-token-set-id]
|
||||
(when (and tokens-lib
|
||||
@@ -150,7 +153,7 @@
|
||||
:selected-ids selected
|
||||
:selected-shapes selected-shapes
|
||||
:is-selected-inside-layout is-selected-inside-layout
|
||||
:active-theme-tokens resolved-active-tokens
|
||||
:active-theme-tokens active-theme-tokens
|
||||
:tokens tokens}]))
|
||||
|
||||
(for [type empty-group]
|
||||
@@ -158,5 +161,5 @@
|
||||
:type type
|
||||
:selected-shapes selected-shapes
|
||||
:is-selected-inside-layout :is-selected-inside-layout
|
||||
:active-theme-tokens resolved-active-tokens
|
||||
:active-theme-tokens active-theme-tokens
|
||||
:tokens []}])]))
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
|
||||
(defn- validate-token-with [token validators]
|
||||
(if-let [error (some (fn [validate] (validate token)) validators)]
|
||||
(rx/throw {:errors [error]})
|
||||
(rx/throw {:errors [error] :pepito "kakota"})
|
||||
(rx/of token)))
|
||||
|
||||
(def ^:private default-validators
|
||||
@@ -151,7 +151,10 @@
|
||||
;; Simple validation of the editing token
|
||||
(rx/mapcat #(validate-token-with % validators))
|
||||
;; Resolving token via StyleDictionary
|
||||
(rx/mapcat #(validate-resolve-token % prev-token tokens)))))
|
||||
(rx/mapcat #(validate-resolve-token % prev-token tokens))
|
||||
(rx/catch (fn [e] (if (contains? e :errors)
|
||||
{:errors (:errors e)}
|
||||
(rx/throw e)))))))
|
||||
|
||||
(defn- check-coll-self-reference
|
||||
"Invalidate a collection of `token-vals` for a self-refernce against `token-name`.,"
|
||||
@@ -294,7 +297,7 @@
|
||||
selected-token-set-id
|
||||
action
|
||||
input-value-placeholder
|
||||
|
||||
tokens
|
||||
;; Callbacks
|
||||
validate-token
|
||||
on-value-resolve
|
||||
@@ -622,8 +625,11 @@
|
||||
:label label
|
||||
:default-value default-value
|
||||
:ref ref
|
||||
:tokens tokens
|
||||
:type token-type
|
||||
:on-blur on-update-value
|
||||
:on-change on-update-value
|
||||
:on-external-update-value on-external-update-value
|
||||
:token-resolve-result token-resolve-result}]))]
|
||||
[:div {:class (stl/css :input-row)}
|
||||
[:> input* {:label (tr "workspace.tokens.token-description")
|
||||
@@ -662,13 +668,15 @@
|
||||
;; Tabs Component --------------------------------------------------------------
|
||||
|
||||
(mf/defc composite-reference-input*
|
||||
[{:keys [default-value on-blur on-update-value token-resolve-result reference-label reference-icon is-reference-fn]}]
|
||||
[{:keys [default-value on-blur on-update-value token-resolve-result reference-label reference-icon is-reference-fn tokens]}]
|
||||
[:> input-token*
|
||||
{:aria-label (tr "labels.reference")
|
||||
:placeholder reference-label
|
||||
:icon reference-icon
|
||||
:default-value (when (is-reference-fn default-value) default-value)
|
||||
:on-blur on-blur
|
||||
:tokens tokens
|
||||
:type "composite-reference"
|
||||
:on-change on-update-value
|
||||
:token-resolve-result (when (or
|
||||
(:errors token-resolve-result)
|
||||
@@ -681,6 +689,7 @@
|
||||
on-external-update-value
|
||||
on-value-resolve
|
||||
clear-resolve-value
|
||||
tokens
|
||||
custom-input-token-value-props]
|
||||
:rest props}]
|
||||
(let [;; Active Tab State
|
||||
@@ -751,6 +760,7 @@
|
||||
:on-update-value on-update-value'
|
||||
:reference-icon reference-icon
|
||||
:reference-label reference-label
|
||||
:tokens tokens
|
||||
:is-reference-fn is-reference-fn})]
|
||||
[:> composite-tab
|
||||
(mf/spread-props props {:default-value default-value
|
||||
@@ -760,7 +770,7 @@
|
||||
(mf/defc composite-form*
|
||||
"Wrapper around form* that manages composite/reference tab state.
|
||||
Takes the same props as form* plus a function to determine if a token value is a reference."
|
||||
[{:keys [token is-reference-fn composite-tab reference-icon title update-composite-backup-value] :rest props}]
|
||||
[{:keys [token is-reference-fn composite-tab reference-icon title update-composite-backup-value tokens] :rest props}]
|
||||
(let [active-tab* (mf/use-state (if (is-reference-fn (:value token)) :reference :composite))
|
||||
active-tab (deref active-tab*)
|
||||
|
||||
@@ -772,6 +782,7 @@
|
||||
:set-active-tab #(reset! active-tab* %)
|
||||
:composite-tab composite-tab
|
||||
:reference-icon reference-icon
|
||||
:tokens tokens
|
||||
:reference-label (tr "workspace.tokens.reference-composite")
|
||||
:title title
|
||||
:update-composite-backup-value update-composite-backup-value
|
||||
@@ -848,8 +859,8 @@
|
||||
:on-change on-change'}]]))
|
||||
|
||||
(mf/defc color-picker*
|
||||
[{:keys [placeholder label default-value input-ref on-blur on-update-value on-external-update-value custom-input-token-value-props token-resolve-result]}]
|
||||
(let [{:keys [color on-display-colorpicker]} custom-input-token-value-props
|
||||
[{:keys [ placeholder label default-value input-ref on-blur on-update-value on-external-update-value custom-input-token-value-props token-resolve-result]}]
|
||||
(let [{:keys [color on-display-colorpicker tokens]} custom-input-token-value-props
|
||||
color-ramp-open* (mf/use-state false)
|
||||
color-ramp-open? (deref color-ramp-open*)
|
||||
|
||||
@@ -903,6 +914,8 @@
|
||||
:default-value default-value
|
||||
:ref input-ref
|
||||
:on-blur on-blur
|
||||
:tokens tokens
|
||||
:type "color"
|
||||
:on-change on-update-value
|
||||
:slot-start swatch}]
|
||||
(when color-ramp-open?
|
||||
@@ -912,7 +925,7 @@
|
||||
[:> token-value-hint* {:result token-resolve-result}]]))
|
||||
|
||||
(mf/defc color-form*
|
||||
[{:keys [token on-display-colorpicker] :rest props}]
|
||||
[{:keys [token on-display-colorpicker tokens] :rest props}]
|
||||
(let [color* (mf/use-state (:value token))
|
||||
color (deref color*)
|
||||
on-value-resolve (mf/use-fn
|
||||
@@ -926,6 +939,7 @@
|
||||
(mf/deps color on-display-colorpicker)
|
||||
(fn []
|
||||
{:color color
|
||||
:tokens tokens
|
||||
:on-display-colorpicker on-display-colorpicker}))
|
||||
|
||||
on-get-token-value
|
||||
@@ -1233,7 +1247,7 @@
|
||||
:full-size true}]]))
|
||||
|
||||
(mf/defc font-picker-combobox*
|
||||
[{:keys [default-value label aria-label input-ref on-blur on-update-value on-external-update-value token-resolve-result placeholder]}]
|
||||
[{:keys [tokens default-value label aria-label input-ref on-blur on-update-value on-external-update-value token-resolve-result placeholder]}]
|
||||
(let [font* (mf/use-state (fonts/find-font-family default-value))
|
||||
font (deref font*)
|
||||
set-font (mf/use-fn
|
||||
@@ -1287,6 +1301,8 @@
|
||||
:ref input-ref
|
||||
:on-blur on-blur
|
||||
:on-change on-update-value'
|
||||
:tokens tokens
|
||||
:type "font-family"
|
||||
:icon i/text-font-family
|
||||
:slot-end font-selector-button
|
||||
:token-resolve-result token-resolve-result}]
|
||||
@@ -1359,7 +1375,7 @@
|
||||
:placeholder (tr "workspace.tokens.text-decoration-value-enter")}))
|
||||
|
||||
(mf/defc typography-value-inputs*
|
||||
[{:keys [default-value on-blur on-update-value token-resolve-result]}]
|
||||
[{:keys [default-value on-blur on-update-value token-resolve-result tokens]}]
|
||||
(let [composite-token? (not (cto/typography-composite-token-reference? (:value token-resolve-result)))
|
||||
typography-inputs (mf/use-memo typography-inputs)
|
||||
errors-by-key (sd/collect-typography-errors token-resolve-result)]
|
||||
@@ -1407,6 +1423,7 @@
|
||||
:input-ref input-ref
|
||||
:default-value (when value (cto/join-font-family value))
|
||||
:on-blur on-blur
|
||||
:tokens tokens
|
||||
:on-update-value on-change
|
||||
:on-external-update-value on-external-update-value
|
||||
:token-resolve-result token-prop}]
|
||||
@@ -1415,12 +1432,14 @@
|
||||
:placeholder placeholder
|
||||
:default-value value
|
||||
:on-blur on-blur
|
||||
:tokens tokens
|
||||
:icon icon
|
||||
:type "typography-subvalue"
|
||||
:on-change on-change
|
||||
:token-resolve-result token-prop}])]))]))
|
||||
|
||||
(mf/defc typography-form*
|
||||
[{:keys [token] :rest props}]
|
||||
[{:keys [token tokens] :rest props}]
|
||||
(let [on-get-token-value
|
||||
(mf/use-fn
|
||||
(fn [e prev-composite-value]
|
||||
@@ -1451,13 +1470,15 @@
|
||||
:is-reference-fn cto/typography-composite-token-reference?
|
||||
:title (tr "labels.typography")
|
||||
:validate-token validate-typography-token
|
||||
:tokens tokens
|
||||
:on-get-token-value on-get-token-value
|
||||
:update-composite-backup-value update-composite-backup-value})]))
|
||||
|
||||
(mf/defc form-wrapper*
|
||||
[{:keys [token token-type] :rest props}]
|
||||
[{:keys [token token-type tokens] :rest props}]
|
||||
(let [token-type' (or (:type token) token-type)
|
||||
props (mf/spread-props props {:token-type token-type'
|
||||
props (mf/spread-props props {:token-type token-type'
|
||||
:tokens tokens
|
||||
:token token})]
|
||||
(case token-type'
|
||||
:color [:> color-form* props]
|
||||
|
||||
@@ -7,18 +7,117 @@
|
||||
(ns app.main.ui.workspace.tokens.management.create.input-tokens-value
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.main.data.workspace.tokens.errors :as wte]
|
||||
[app.main.data.workspace.tokens.format :as dwtf]
|
||||
[app.main.data.workspace.tokens.warnings :as wtw]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.controls.shared.options-dropdown :refer [options-dropdown*]]
|
||||
[app.main.ui.ds.controls.utilities.hint-message :refer [hint-message*]]
|
||||
[app.main.ui.ds.controls.utilities.input-field :refer [input-field*]]
|
||||
[app.main.ui.ds.controls.utilities.label :refer [label*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon-list]]
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon-list] :as i]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.object :as obj]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def token-type->reference-types
|
||||
{:color #{:color}
|
||||
:dimensions #{:dimensions}
|
||||
:spacing #{:spacing :dimensions}
|
||||
:border-radius #{:border-radius :dimensions :sizing}
|
||||
:font-family #{:font-family}
|
||||
:font-size #{:font-size :sizing :dimension}
|
||||
:opacity #{:opacity :number}
|
||||
:rotation #{:rotation :number}
|
||||
:stroke-width #{:stroke-width :dimension :sizing}
|
||||
:sizing #{:sizing :dimensions}
|
||||
:number #{:number}
|
||||
:letter-spacing #{:letter-spacing}
|
||||
:typography #{:typography}
|
||||
:text-case #{:text-case}
|
||||
:text-decoration #{:text-decoration}
|
||||
:line-height #{:number}})
|
||||
|
||||
;; TODO; duplucated code with numeric-input.cljs, consider refactoring
|
||||
(defn- sort-groups-and-tokens
|
||||
"Sorts both the groups and the tokens inside them alphabetically.
|
||||
|
||||
Input:
|
||||
A map where:
|
||||
- keys are groups (keywords or strings, e.g. :dimensions, :colors)
|
||||
- values are vectors of token maps, each containing at least a :name key
|
||||
|
||||
Example input:
|
||||
{:dimensions [{:name \"tres\"} {:name \"quini\"}]
|
||||
:colors [{:name \"azul\"} {:name \"rojo\"}]}
|
||||
|
||||
Output:
|
||||
A sorted map where:
|
||||
- groups are ordered alphabetically by key
|
||||
- tokens inside each group are sorted alphabetically by :name
|
||||
|
||||
Example output:
|
||||
{:colors [{:name \"azul\"} {:name \"rojo\"}]
|
||||
:dimensions [{:name \"quini\"} {:name \"tres\"}]}"
|
||||
|
||||
[groups->tokens]
|
||||
(into (sorted-map) ;; ensure groups are ordered alphabetically by their key
|
||||
(for [[group tokens] groups->tokens]
|
||||
[group (sort-by :name tokens)])))
|
||||
|
||||
(defn- extract-partial-brace-text
|
||||
[string]
|
||||
(when-let [start (str/last-index-of string "{")]
|
||||
(subs string (inc start))))
|
||||
|
||||
(defn- filter-token-groups-by-name
|
||||
[tokens filter-text]
|
||||
(let [lc-filter (str/lower filter-text)]
|
||||
(into {}
|
||||
(keep (fn [[group tokens]]
|
||||
(let [filtered (filter #(str/includes? (str/lower (:name %)) lc-filter) tokens)]
|
||||
(when (seq filtered)
|
||||
[group filtered]))))
|
||||
tokens)))
|
||||
|
||||
(defn- token->dropdown-option
|
||||
[token]
|
||||
{:id (str (get token :id))
|
||||
:type :token
|
||||
:resolved-value (get token :resolved-value)
|
||||
:name (get token :name)})
|
||||
|
||||
(defn- generate-dropdown-options
|
||||
[tokens no-sets]
|
||||
(if (empty? tokens)
|
||||
[{:type :empty
|
||||
:label (if no-sets
|
||||
(tr "ds.inputs.numeric-input.no-applicable-tokens")
|
||||
(tr "ds.inputs.numeric-input.no-matches"))}]
|
||||
(->> tokens
|
||||
(map (fn [[type items]]
|
||||
(cons {:group true
|
||||
:type :group
|
||||
:id (dm/str "group-" (name type))
|
||||
:name (name type)}
|
||||
(map token->dropdown-option items))))
|
||||
(interpose [{:separator true
|
||||
:id "separator"
|
||||
:type :separator}])
|
||||
(apply concat)
|
||||
(vec)
|
||||
(not-empty))))
|
||||
(defn get-option
|
||||
[options id]
|
||||
(let [options (if (delay? options) @options options)]
|
||||
(or (d/seek #(= id (get % :id)) options)
|
||||
(nth options 0))))
|
||||
|
||||
(def ^:private schema::input-token
|
||||
[:map
|
||||
[:label {:optional true} [:maybe :string]]
|
||||
@@ -58,21 +157,119 @@
|
||||
(mf/defc input-token*
|
||||
{::mf/forward-ref true
|
||||
::mf/schema schema::input-token}
|
||||
[{:keys [class label token-resolve-result] :rest props} ref]
|
||||
[{:keys [class label token-resolve-result tokens empty-to-end type on-external-update-value] :rest props} ref]
|
||||
(let [error (not (nil? (:errors token-resolve-result)))
|
||||
id (mf/use-id)
|
||||
input-ref (mf/use-ref)
|
||||
|
||||
is-open* (mf/use-state false)
|
||||
is-open (deref is-open*)
|
||||
|
||||
filter-term* (mf/use-state "")
|
||||
filter-term (deref filter-term*)
|
||||
|
||||
listbox-id (mf/use-id)
|
||||
|
||||
focused-id* (mf/use-state nil)
|
||||
focused-id (deref focused-id*)
|
||||
|
||||
selected-id* (mf/use-state (fn []))
|
||||
selected-id (deref selected-id*)
|
||||
|
||||
empty-to-end (d/nilv empty-to-end false)
|
||||
|
||||
internal-ref (mf/use-ref nil)
|
||||
ref (or ref internal-ref)
|
||||
nodes-ref (mf/use-ref nil)
|
||||
open-dropdown-ref (mf/use-ref nil)
|
||||
options-ref (mf/use-ref nil)
|
||||
set-option-ref
|
||||
(mf/use-fn
|
||||
(fn [node]
|
||||
(let [state (mf/ref-val nodes-ref)
|
||||
state (d/nilv state #js {})
|
||||
id (dom/get-data node "id")
|
||||
state (obj/set! state id node)]
|
||||
(mf/set-ref-val! nodes-ref state)
|
||||
(fn []
|
||||
(let [state (mf/ref-val nodes-ref)
|
||||
state (d/nilv state #js {})
|
||||
id (dom/get-data node "id")
|
||||
state (obj/unset! state id)]
|
||||
(mf/set-ref-val! nodes-ref state))))))
|
||||
|
||||
dropdown-options
|
||||
(mf/with-memo [tokens filter-term type]
|
||||
(delay
|
||||
(let [tokens (if (delay? tokens) @tokens tokens)
|
||||
allowed (get token-type->reference-types (keyword type) #{})
|
||||
tokens (select-keys tokens allowed)
|
||||
sorted-tokens (sort-groups-and-tokens tokens)
|
||||
partial (extract-partial-brace-text filter-term)
|
||||
|
||||
options (if (seq partial)
|
||||
(filter-token-groups-by-name sorted-tokens partial)
|
||||
sorted-tokens)
|
||||
no-sets? (nil? sorted-tokens)]
|
||||
(generate-dropdown-options options no-sets?))))
|
||||
|
||||
update-input
|
||||
(mf/use-fn
|
||||
(mf/deps ref)
|
||||
(fn [new-value]
|
||||
(when-let [node (mf/ref-val ref)]
|
||||
(dom/set-value! node new-value)
|
||||
(reset! is-open* false))))
|
||||
|
||||
on-option-click
|
||||
(mf/use-fn
|
||||
(mf/deps options-ref update-input)
|
||||
(fn [event]
|
||||
(let [node (dom/get-current-target event)
|
||||
id (dom/get-data node "id")
|
||||
options (mf/ref-val options-ref)
|
||||
options (if (delay? options) @options options)
|
||||
option (get-option options id)
|
||||
name (get option :name)
|
||||
new-value (str "{" name "}")]
|
||||
(on-external-update-value new-value)
|
||||
(reset! is-open* false))))
|
||||
|
||||
open-dropdown
|
||||
(mf/use-fn
|
||||
(fn [_]
|
||||
(swap! is-open* not)))
|
||||
|
||||
props (mf/spread-props props {:id id
|
||||
:type "text"
|
||||
:class (stl/css :input)
|
||||
:variant "comfortable"
|
||||
:hint-type (when error "error")
|
||||
:ref (or ref input-ref)})]
|
||||
:slot-end (when (some? tokens)
|
||||
(mf/html [:> icon-button* {:variant "action"
|
||||
:icon i/tokens
|
||||
:class (stl/css :invisible-button)
|
||||
:aria-label (tr "ds.inputs.numeric-input.open-token-list-dropdown")
|
||||
:ref open-dropdown-ref
|
||||
:on-click open-dropdown}]))
|
||||
:ref ref})]
|
||||
|
||||
(mf/with-effect [dropdown-options]
|
||||
(mf/set-ref-val! options-ref dropdown-options))
|
||||
[:*
|
||||
[:div {:class (dm/str class " " (stl/css-case :wrapper true
|
||||
:input-error error))}
|
||||
[:div {:class [class (stl/css-case :wrapper true
|
||||
:input-error error)]}
|
||||
(when label
|
||||
[:> label* {:for id} label])
|
||||
[:> input-field* props]]
|
||||
(when token-resolve-result
|
||||
[:> token-value-hint* {:result token-resolve-result}])]))
|
||||
[:> token-value-hint* {:result token-resolve-result}])
|
||||
(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 :left
|
||||
:empty-to-end empty-to-end
|
||||
:ref set-option-ref}]))]))
|
||||
|
||||
@@ -68,8 +68,8 @@
|
||||
(clj->js))))
|
||||
|
||||
(mf/defc token-update-create-modal
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [x y position token token-type action selected-token-set-id] :as _args}]
|
||||
{ ::mf/props :obj}
|
||||
[{:keys [x y position token token-type action selected-token-set-id tokens] :as _args}]
|
||||
(let [wrapper-style (use-viewport-position-style x y position (= token-type :color))
|
||||
modal-size-large* (mf/use-state (= token-type :typography))
|
||||
modal-size-large? (deref modal-size-large*)
|
||||
@@ -94,6 +94,7 @@
|
||||
:action action
|
||||
:selected-token-set-id selected-token-set-id
|
||||
:token-type token-type
|
||||
:tokens tokens
|
||||
:on-display-colorpicker update-modal-size}]]))
|
||||
|
||||
;; Modals ----------------------------------------------------------------------
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
|
||||
on-popover-open-click
|
||||
(mf/use-fn
|
||||
(mf/deps type title modal)
|
||||
(mf/deps type title modal active-theme-tokens)
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (dwtl/set-token-type-section-open type true)
|
||||
@@ -86,6 +86,7 @@
|
||||
{:x (:x pos)
|
||||
:y (:y pos)
|
||||
:position :right
|
||||
:tokens active-theme-tokens
|
||||
:fields (:fields modal)
|
||||
:title title
|
||||
:action "create"
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
[app.main.data.workspace.tokens.application :as dwta]
|
||||
[app.main.data.workspace.tokens.color :as dwtc]
|
||||
[app.main.data.workspace.tokens.format :as dwtf]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.ui.context :as ctx]
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
|
||||
[app.main.ui.ds.foundations.utilities.token.token-status :refer [token-status-icon*]]
|
||||
[app.main.ui.ds.utilities.swatch :refer [swatch*]]
|
||||
@@ -27,7 +27,6 @@
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
;; Translation dictionaries
|
||||
|
||||
(def ^:private attribute-dictionary
|
||||
{:rotation "Rotation"
|
||||
:opacity "Opacity"
|
||||
@@ -77,8 +76,7 @@
|
||||
:y :y})
|
||||
|
||||
;; Helper functions
|
||||
|
||||
(defn partially-applied-attr
|
||||
(defn- partially-applied-attr
|
||||
"Translates partially applied attributes based on the dictionary."
|
||||
[app-token-keys is-applied {:keys [attributes all-attributes]}]
|
||||
(let [filtered-keys (if all-attributes
|
||||
@@ -87,7 +85,7 @@
|
||||
(when is-applied
|
||||
(str/join ", " (map attribute-dictionary filtered-keys)))))
|
||||
|
||||
(defn translate-and-format
|
||||
(defn- translate-and-format
|
||||
"Translates and formats grouped values by category."
|
||||
[grouped-values]
|
||||
(str/join "\n"
|
||||
@@ -98,6 +96,18 @@
|
||||
(str/join ", " (map attribute-dictionary values)) ".")))
|
||||
grouped-values)))
|
||||
|
||||
(defn- token-exists?
|
||||
"Returns true if any token in the grouped token map has a name matching `token-name`."
|
||||
[tokens-by-type token-name]
|
||||
(let [clean-name (-> token-name
|
||||
(str/trim)
|
||||
(str/replace #"^\{" "")
|
||||
(str/replace #"\}$" "")
|
||||
(str/lower))]
|
||||
(some (fn [[_ tokens]]
|
||||
(some #(= clean-name (:name %)) tokens))
|
||||
tokens-by-type)))
|
||||
|
||||
(defn- generate-tooltip
|
||||
"Generates a tooltip for a given token"
|
||||
[is-viewer shape theme-token token half-applied no-valid-value ref-not-in-active-set]
|
||||
@@ -142,14 +152,6 @@
|
||||
;; Otherwise only show the base title
|
||||
:else base-title)))
|
||||
|
||||
;; FIXME: the token thould already have precalculated references, so
|
||||
;; we don't need to perform this regex operation on each rerender
|
||||
(defn contains-reference-value?
|
||||
"Extracts the value between `{}` in a string and checks if it's in the provided vector."
|
||||
[text active-tokens]
|
||||
(let [match (second (re-find #"\{([^}]+)\}" text))]
|
||||
(contains? active-tokens match)))
|
||||
|
||||
(def ^:private
|
||||
xf:map-id
|
||||
(map :id))
|
||||
@@ -176,7 +178,6 @@
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [on-click token on-context-menu selected-shapes is-selected-inside-layout active-theme-tokens]}]
|
||||
(let [{:keys [name value errors type]} token
|
||||
|
||||
has-selected? (pos? (count selected-shapes))
|
||||
is-reference? (cft/is-reference? token)
|
||||
contains-path? (str/includes? name ".")
|
||||
@@ -203,14 +204,14 @@
|
||||
(not half-applied?)
|
||||
(not (attributes-match-selection? selected-shapes attributes {:selected-inside-layout? is-selected-inside-layout})))
|
||||
|
||||
;; FIXME: move to context or props
|
||||
can-edit? (:can-edit (deref refs/permissions))
|
||||
can-edit?
|
||||
(mf/use-ctx ctx/can-edit?)
|
||||
|
||||
is-viewer? (not can-edit?)
|
||||
|
||||
ref-not-in-active-set
|
||||
(and is-reference?
|
||||
(not (contains-reference-value? value active-theme-tokens)))
|
||||
(not (token-exists? @active-theme-tokens value)))
|
||||
|
||||
no-valid-value (seq errors)
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.logging :as log]
|
||||
[app.common.math :as mth]
|
||||
[app.common.types.fills :as types.fills]
|
||||
[app.common.types.fills.impl :as types.fills.impl]
|
||||
@@ -316,14 +315,7 @@
|
||||
(aset heap32 (+ offset 11) height)
|
||||
|
||||
(h/call wasm/internal-module "_store_image_from_texture")
|
||||
true))))
|
||||
(rx/catch (fn [cause]
|
||||
(log/error :hint "Could not fetch image"
|
||||
:image-id image-id
|
||||
:thumbnail? thumbnail?
|
||||
:url url
|
||||
:cause cause)
|
||||
(rx/empty))))}))
|
||||
true)))))}))
|
||||
|
||||
(defn- get-fill-images
|
||||
[leaf]
|
||||
@@ -1109,28 +1101,13 @@
|
||||
(set! (.-width canvas) (* dpr (.-clientWidth ^js canvas)))
|
||||
(set! (.-height canvas) (* dpr (.-clientHeight ^js canvas))))
|
||||
|
||||
(defn- get-browser
|
||||
[]
|
||||
(when (exists? js/navigator)
|
||||
(let [user-agent (.-userAgent js/navigator)]
|
||||
(when user-agent
|
||||
(cond
|
||||
(re-find #"(?i)firefox" user-agent) :firefox
|
||||
(re-find #"(?i)chrome" user-agent) :chrome
|
||||
(re-find #"(?i)safari" user-agent) :safari
|
||||
(re-find #"(?i)edge" user-agent) :edge
|
||||
:else :unknown)))))
|
||||
|
||||
|
||||
(defn init-canvas-context
|
||||
[canvas]
|
||||
(let [gl (unchecked-get wasm/internal-module "GL")
|
||||
flags (debug-flags)
|
||||
context-id (if (dbg/enabled? :wasm-gl-context-init-error) "fail" "webgl2")
|
||||
context (.getContext ^js canvas context-id context-options)
|
||||
context-init? (not (nil? context))
|
||||
browser (get-browser)
|
||||
browser (sr/translate-browser browser)]
|
||||
context-init? (not (nil? context))]
|
||||
(when-not (nil? context)
|
||||
(let [handle (.registerContext ^js gl context #js {"majorVersion" 2})]
|
||||
(.makeContextCurrent ^js gl handle)
|
||||
@@ -1142,10 +1119,6 @@
|
||||
(h/call wasm/internal-module "_init" (/ (.-width ^js canvas) dpr) (/ (.-height ^js canvas) dpr))
|
||||
(h/call wasm/internal-module "_set_render_options" flags dpr))
|
||||
(set! wasm/context-initialized? true))
|
||||
|
||||
(h/call wasm/internal-module "_set_browser" browser)
|
||||
|
||||
(h/call wasm/internal-module "_set_render_options" flags dpr)
|
||||
(set-canvas-size canvas)
|
||||
context-init?))
|
||||
|
||||
|
||||
@@ -264,13 +264,3 @@
|
||||
"regular" (unchecked-get values "normal")
|
||||
"italic" (unchecked-get values "italic")
|
||||
default)))
|
||||
|
||||
(defn translate-browser
|
||||
[browser]
|
||||
(case browser
|
||||
:firefox 0
|
||||
:chrome 1
|
||||
:safari 2
|
||||
:edge 3
|
||||
:unknown 4
|
||||
4))
|
||||
|
||||
@@ -98,18 +98,12 @@
|
||||
styles (get-styles-from-attrs node txt/text-node-attrs txt/default-text-attrs)]
|
||||
(dissoc styles :line-height)))
|
||||
|
||||
(defn normalize-spaces
|
||||
"Add zero-width spaces after forward slashes to enable word breaking"
|
||||
[text]
|
||||
(when text
|
||||
(.replace text (js/RegExp "/" "g") "/\u200B")))
|
||||
|
||||
(defn get-inline-children
|
||||
[inline paragraph]
|
||||
[(if (and (= "" (:text inline))
|
||||
(= 1 (count (:children paragraph))))
|
||||
(dom/create-element "br")
|
||||
(dom/create-text (normalize-spaces (:text inline))))])
|
||||
(dom/create-text (:text inline)))])
|
||||
|
||||
(defn create-random-key
|
||||
[]
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
|
||||
(defn- request-data-for-thumbnail
|
||||
[file-id revn]
|
||||
(let [path "api/main/methods/get-file-data-for-thumbnail"
|
||||
(let [path "api/rpc/command/get-file-data-for-thumbnail"
|
||||
params {:file-id file-id
|
||||
:revn revn
|
||||
:strip-frames-with-thumbnails true}
|
||||
|
||||
@@ -1763,6 +1763,9 @@ msgstr "Můžete pokračovat s účtem Penpot"
|
||||
msgid "labels.copy-invitation-link"
|
||||
msgstr "Kopírovat odkaz"
|
||||
|
||||
#: src/app/main/ui/static.cljs:63
|
||||
msgid "labels.copyright"
|
||||
msgstr "Kaleidos @2024"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/assets/groups.cljs:167, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:205
|
||||
msgid "labels.create"
|
||||
|
||||
@@ -1995,6 +1995,10 @@ msgstr "Farbe kopieren"
|
||||
msgid "labels.copy-invitation-link"
|
||||
msgstr "Link kopieren"
|
||||
|
||||
#: src/app/main/ui/static.cljs:63
|
||||
msgid "labels.copyright"
|
||||
msgstr "Kaleidos @2024"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/assets/groups.cljs:167, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:205
|
||||
msgid "labels.create"
|
||||
msgstr "Erstellen"
|
||||
|
||||
@@ -1696,10 +1696,6 @@ msgstr "RGBA"
|
||||
msgid "inspect.attributes.fill"
|
||||
msgstr "Fill"
|
||||
|
||||
#: src/app/main/ui/inspect/attributes/fill.cljs:53
|
||||
msgid "inspect.attributes.image.preview"
|
||||
msgstr "Preview of the shape's fill image"
|
||||
|
||||
#: src/app/main/ui/inspect/attributes/common.cljs:78, src/app/main/ui/inspect/styles/rows/color_properties_row.cljs:127
|
||||
msgid "inspect.attributes.image.download"
|
||||
msgstr "Download source image"
|
||||
@@ -2139,8 +2135,8 @@ msgid "labels.copy-invitation-link"
|
||||
msgstr "Copy link"
|
||||
|
||||
#: src/app/main/ui/static.cljs:63
|
||||
msgid "labels.copyright-period"
|
||||
msgstr "Kaleidos © 2019-present"
|
||||
msgid "labels.copyright"
|
||||
msgstr "Kaleidos @2024"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/assets/groups.cljs:167, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:205
|
||||
msgid "labels.create"
|
||||
@@ -5578,8 +5574,8 @@ msgstr "Add"
|
||||
|
||||
#: src/app/main/ui/workspace/libraries.cljs:107, src/app/main/ui/workspace/libraries.cljs:133
|
||||
msgid "workspace.libraries.colors"
|
||||
msgid_plural "workspace.libraries.colors"
|
||||
msgstr[0] "1 color"
|
||||
msgid_plural "workspace.libraries.colors"
|
||||
msgstr[0] "1 color"
|
||||
msgstr[1] "%s colors"
|
||||
|
||||
#: src/app/main/ui/workspace/color_palette.cljs:147
|
||||
@@ -5618,8 +5614,8 @@ msgstr "Save color style"
|
||||
|
||||
#: src/app/main/ui/workspace/libraries.cljs:101, src/app/main/ui/workspace/libraries.cljs:125
|
||||
msgid "workspace.libraries.components"
|
||||
msgid_plural "workspace.libraries.components"
|
||||
msgstr[0] "1 component"
|
||||
msgid_plural "workspace.libraries.components"
|
||||
msgstr[0] "1 component"
|
||||
msgstr[1] "%s components"
|
||||
|
||||
#: src/app/main/ui/workspace/libraries.cljs:349
|
||||
@@ -5644,8 +5640,8 @@ msgstr "File library"
|
||||
|
||||
#: src/app/main/ui/workspace/libraries.cljs:104, src/app/main/ui/workspace/libraries.cljs:129
|
||||
msgid "workspace.libraries.graphics"
|
||||
msgid_plural "workspace.libraries.graphics"
|
||||
msgstr[0] "1 graphic"
|
||||
msgid_plural "workspace.libraries.graphics"
|
||||
msgstr[0] "1 graphic"
|
||||
msgstr[1] "%s graphics"
|
||||
|
||||
#: src/app/main/ui/workspace/libraries.cljs:316
|
||||
@@ -5704,8 +5700,8 @@ msgstr "Unlink all typographies"
|
||||
|
||||
#: src/app/main/ui/workspace/libraries.cljs:110, src/app/main/ui/workspace/libraries.cljs:137
|
||||
msgid "workspace.libraries.typography"
|
||||
msgid_plural "workspace.libraries.typography"
|
||||
msgstr[0] "1 typography"
|
||||
msgid_plural "workspace.libraries.typography"
|
||||
msgstr[0] "1 typography"
|
||||
msgstr[1] "%s typographies"
|
||||
|
||||
#: src/app/main/ui/workspace/libraries.cljs:354
|
||||
|
||||
@@ -1694,10 +1694,6 @@ msgstr "RGBA"
|
||||
msgid "inspect.attributes.fill"
|
||||
msgstr "Relleno"
|
||||
|
||||
#: src/app/main/ui/inspect/attributes/fill.cljs:53
|
||||
msgid "inspect.attributes.image.preview"
|
||||
msgstr "Previsualización de la imagen de relleno"
|
||||
|
||||
#: src/app/main/ui/inspect/attributes/common.cljs:78, src/app/main/ui/inspect/styles/rows/color_properties_row.cljs:127
|
||||
msgid "inspect.attributes.image.download"
|
||||
msgstr "Descargar imagen original"
|
||||
@@ -2134,8 +2130,8 @@ msgid "labels.copy-invitation-link"
|
||||
msgstr "Copiar enlace"
|
||||
|
||||
#: src/app/main/ui/static.cljs:63
|
||||
msgid "labels.copyright-period"
|
||||
msgstr "© Kaleidos, 2019-presente"
|
||||
msgid "labels.copyright"
|
||||
msgstr "Kaleidos @2024"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/assets/groups.cljs:167, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:205
|
||||
msgid "labels.create"
|
||||
|
||||
@@ -2022,6 +2022,10 @@ msgstr "Copier la couleur"
|
||||
msgid "labels.copy-invitation-link"
|
||||
msgstr "Copier le lien"
|
||||
|
||||
#: src/app/main/ui/static.cljs:63
|
||||
msgid "labels.copyright"
|
||||
msgstr "Kaleidos @2024"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/assets/groups.cljs:167, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:205
|
||||
msgid "labels.create"
|
||||
msgstr "Créer"
|
||||
|
||||
@@ -2031,6 +2031,10 @@ msgstr "העתקת צבע"
|
||||
msgid "labels.copy-invitation-link"
|
||||
msgstr "העתקת קישור"
|
||||
|
||||
#: src/app/main/ui/static.cljs:63
|
||||
msgid "labels.copyright"
|
||||
msgstr "Kaleidos @2024"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/assets/groups.cljs:167, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:205
|
||||
msgid "labels.create"
|
||||
msgstr "יצירה"
|
||||
|
||||
@@ -1932,6 +1932,10 @@ msgstr "रंग कॉपी करें"
|
||||
msgid "labels.copy-invitation-link"
|
||||
msgstr "लिंक कॉपी"
|
||||
|
||||
#: src/app/main/ui/static.cljs:63
|
||||
msgid "labels.copyright"
|
||||
msgstr "Kaleidos @2024"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/assets/groups.cljs:167, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:205
|
||||
msgid "labels.create"
|
||||
msgstr "निर्माण"
|
||||
|
||||
@@ -1755,6 +1755,10 @@ msgstr "Možeš nastaviti s Penpot računom"
|
||||
msgid "labels.copy-invitation-link"
|
||||
msgstr "Kopiraj vezu"
|
||||
|
||||
#: src/app/main/ui/static.cljs:63
|
||||
msgid "labels.copyright"
|
||||
msgstr "Kaleidos @2024"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/assets/groups.cljs:167, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:205
|
||||
msgid "labels.create"
|
||||
msgstr "Kreiraj"
|
||||
|
||||
@@ -1859,6 +1859,10 @@ msgstr "Salin warna"
|
||||
msgid "labels.copy-invitation-link"
|
||||
msgstr "Salin tautan"
|
||||
|
||||
#: src/app/main/ui/static.cljs:63
|
||||
msgid "labels.copyright"
|
||||
msgstr "Kaleidos @2024"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/assets/groups.cljs:167, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:205
|
||||
msgid "labels.create"
|
||||
msgstr "Buat"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user