mirror of
https://github.com/penpot/penpot.git
synced 2026-01-03 11:58:46 -05:00
Compare commits
158 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1eaf7b2b44 | ||
|
|
903f064e87 | ||
|
|
a23d1908e9 | ||
|
|
1e8226a3fc | ||
|
|
b7459726f5 | ||
|
|
b8179d0e35 | ||
|
|
e36b49b4f0 | ||
|
|
92ff5de538 | ||
|
|
c83d028466 | ||
|
|
56a0d522dc | ||
|
|
a3495800b5 | ||
|
|
750cf05784 | ||
|
|
1384219ae7 | ||
|
|
d2d9aeff25 | ||
|
|
95d80c9578 | ||
|
|
b523bef8ba | ||
|
|
0c5c04e58a | ||
|
|
a0973b9ddf | ||
|
|
c53b6117c0 | ||
|
|
bd3ddebcc4 | ||
|
|
0441f28880 | ||
|
|
288030888a | ||
|
|
203c0ed87d | ||
|
|
09e28076cd | ||
|
|
ad4e489312 | ||
|
|
50932dea54 | ||
|
|
da3c829b1b | ||
|
|
d4b4e6be7d | ||
|
|
cc5b1c950b | ||
|
|
52851f4c6f | ||
|
|
9bd42be771 | ||
|
|
5f65960d42 | ||
|
|
dc813732c3 | ||
|
|
661e4a001a | ||
|
|
53d1624f3f | ||
|
|
514ba6604b | ||
|
|
0aa361013a | ||
|
|
ddbc828342 | ||
|
|
67cff1ed74 | ||
|
|
22c88a19e2 | ||
|
|
159ac92021 | ||
|
|
1a92657c7c | ||
|
|
8669207086 | ||
|
|
b82ce671b9 | ||
|
|
ff14208a95 | ||
|
|
8593ca1310 | ||
|
|
f69e141ac1 | ||
|
|
b0497f1352 | ||
|
|
aaf9c6e50b | ||
|
|
d80aa7593b | ||
|
|
5275c35002 | ||
|
|
f02b5765d7 | ||
|
|
1f31722571 | ||
|
|
834c18323e | ||
|
|
1d2f5b6c0b | ||
|
|
ab87db099a | ||
|
|
661a916a5f | ||
|
|
b8dee17075 | ||
|
|
c8d5e4ef35 | ||
|
|
a7f39e89f6 | ||
|
|
70bb34118c | ||
|
|
f409dfd3d1 | ||
|
|
e1954b5dd7 | ||
|
|
196d57dd5c | ||
|
|
a1ac839b2a | ||
|
|
1e9a4d74eb | ||
|
|
7a9777419c | ||
|
|
28836d82cd | ||
|
|
da62a6809c | ||
|
|
5d5d238fec | ||
|
|
e5dedb1e3d | ||
|
|
4c7cd02f56 | ||
|
|
b3128bd32b | ||
|
|
15a9035ed1 | ||
|
|
82e51d358b | ||
|
|
fbcc2494b4 | ||
|
|
4a016dce14 | ||
|
|
53f40043aa | ||
|
|
937dd5a857 | ||
|
|
36b167956c | ||
|
|
695152274c | ||
|
|
486c638076 | ||
|
|
81facd58c9 | ||
|
|
2a0031d23c | ||
|
|
63a3186e6d | ||
|
|
fcdf33b134 | ||
|
|
19d88cc1a6 | ||
|
|
1f68c6164a | ||
|
|
c39702fbf7 | ||
|
|
b3f0683d02 | ||
|
|
211de1bb9c | ||
|
|
fe80aab394 | ||
|
|
a494b89bba | ||
|
|
6e313dff84 | ||
|
|
766040198a | ||
|
|
7afaa9d31f | ||
|
|
cf68a9cf1e | ||
|
|
c69f6da2d7 | ||
|
|
259b05db51 | ||
|
|
2ba7996116 | ||
|
|
66e877ed40 | ||
|
|
f3bf04e1c9 | ||
|
|
79e3aadfcf | ||
|
|
0527c55398 | ||
|
|
54bb89b2bb | ||
|
|
9334f935eb | ||
|
|
fed31d366f | ||
|
|
55b7bba944 | ||
|
|
3ff13f1d8f | ||
|
|
4b28685a6d | ||
|
|
53001921d5 | ||
|
|
046f501152 | ||
|
|
00f7c94377 | ||
|
|
eae5dfc828 | ||
|
|
88261c2ec3 | ||
|
|
1bfc28f63d | ||
|
|
e7a82579c1 | ||
|
|
30c786741f | ||
|
|
3eb2569465 | ||
|
|
7efeeec9b1 | ||
|
|
67f56dd0f8 | ||
|
|
2ec5a3ba6a | ||
|
|
958931d264 | ||
|
|
e3f69bcc98 | ||
|
|
9c53a33bac | ||
|
|
f72206bba3 | ||
|
|
37a19aa6b5 | ||
|
|
17ea8300ed | ||
|
|
aac044fa0a | ||
|
|
e935ccae76 | ||
|
|
13312dc467 | ||
|
|
0ec49e5e95 | ||
|
|
a49999186f | ||
|
|
fc416ee4af | ||
|
|
37bd537bfd | ||
|
|
1a92bd0478 | ||
|
|
cd55adefb8 | ||
|
|
3006ed7966 | ||
|
|
53ed1404e7 | ||
|
|
f691f8d5b5 | ||
|
|
2c68e8309e | ||
|
|
dce8b5b37c | ||
|
|
6546bfc889 | ||
|
|
b915abb2d2 | ||
|
|
5cb5df63d9 | ||
|
|
74552a4989 | ||
|
|
b72b8a6d53 | ||
|
|
0a74696874 | ||
|
|
6548fe069e | ||
|
|
22d852fca8 | ||
|
|
17c2f44780 | ||
|
|
40286c81d4 | ||
|
|
3b262f2ae5 | ||
|
|
80dd910d58 | ||
|
|
21a066ec64 | ||
|
|
29c091a26b | ||
|
|
b249cd1b72 | ||
|
|
e2a0a40704 |
@@ -6,7 +6,6 @@
|
||||
rumext.v2/defc clojure.core/defn
|
||||
rumext.v2/fnc clojure.core/fn
|
||||
app.common.data/export clojure.core/def
|
||||
app.db/with-atomic clojure.core/with-open
|
||||
app.common.data.macros/get-in clojure.core/get-in
|
||||
app.common.data.macros/with-open clojure.core/with-open
|
||||
app.common.data.macros/select-keys clojure.core/select-keys
|
||||
@@ -17,6 +16,7 @@
|
||||
{app.common.data.macros/export hooks.export/export
|
||||
potok.core/reify hooks.export/potok-reify
|
||||
app.util.services/defmethod hooks.export/service-defmethod
|
||||
app.db/with-atomic hooks.export/penpot-with-atomic
|
||||
}}
|
||||
|
||||
:output
|
||||
|
||||
@@ -39,6 +39,43 @@
|
||||
other))]
|
||||
{:node result})))
|
||||
|
||||
(defn penpot-with-atomic
|
||||
[{:keys [node]}]
|
||||
(let [[_ params & other] (:children node)
|
||||
|
||||
result (if (api/vector-node? params)
|
||||
(api/list-node
|
||||
(into [(api/token-node (symbol "clojure.core" "with-open")) params] other))
|
||||
(api/list-node
|
||||
(into [(api/token-node (symbol "clojure.core" "with-open"))
|
||||
(api/vector-node [params params])]
|
||||
other)))
|
||||
|
||||
]
|
||||
{:node result}))
|
||||
|
||||
(defn penpot-defrecord
|
||||
[{:keys [:node]}]
|
||||
(let [[rnode rtype rparams & other] (:children node)
|
||||
|
||||
nodes [(api/token-node (symbol "do"))
|
||||
(api/list-node
|
||||
(into [(api/token-node (symbol (name (:value rnode)))) rtype rparams] other))
|
||||
(api/list-node
|
||||
[(api/token-node (symbol "defn"))
|
||||
(api/token-node (symbol (str "pos->" (:string-value rtype))))
|
||||
(api/vector-node
|
||||
(->> (:children rparams)
|
||||
(mapv (fn [t]
|
||||
(api/token-node (symbol (str "_" (:string-value t))))))))
|
||||
(api/token-node nil)])]
|
||||
|
||||
result (api/list-node nodes)]
|
||||
|
||||
;; (prn "=====>" (into {} rparams))
|
||||
;; (prn (api/sexpr result))
|
||||
{:node result}))
|
||||
|
||||
(defn clojure-specify
|
||||
[{:keys [:node]}]
|
||||
(let [[rnode rtype & other] (:children node)
|
||||
@@ -48,7 +85,6 @@
|
||||
other))]
|
||||
{:node result}))
|
||||
|
||||
|
||||
(defn service-defmethod
|
||||
[{:keys [:node]}]
|
||||
(let [[rnode rtype ?meta & other] (:children node)
|
||||
|
||||
36
CHANGES.md
36
CHANGES.md
@@ -1,5 +1,41 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 1.19.3
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- Remember last color mode in colorpicker [Taiga #5508](https://tree.taiga.io/project/penpot/issue/5508)
|
||||
- Improve layers multiselection behaviour [Github #5741](https://github.com/penpot/penpot/issues/5741)
|
||||
- Remember last active team across logouts / sessions [Github #3325](https://github.com/penpot/penpot/issues/3325)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- List view is discarded on tab change on Workspace Assets Sidebar tab [Github #3547](https://github.com/penpot/penpot/issues/3547)
|
||||
- Fix message popup remains open when exiting workspace with browser back button [Taiga #5747](https://tree.taiga.io/project/penpot/issue/5747)
|
||||
- When editing text if font is changed, the proportions of the rendered shape are wrong [Taiga #5786](https://tree.taiga.io/project/penpot/issue/5786)
|
||||
|
||||
## 1.19.2
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- Navigate up in layer hierarchy with Shift+Enter shortcut [Taiga #5734](https://tree.taiga.io/project/penpot/us/5734)
|
||||
- Click on the flow tags open viewer with the selected frame [Taiga #5044](https://tree.taiga.io/project/penpot/us/5044)
|
||||
- Add Dutch language & update translation files with weblate
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix unexpected output on get-page rpc method when invalid object-id is provided [Github #3546](https://github.com/penpot/penpot/issues/3546)
|
||||
- Fix Invalid files amount after moving file from Project to Drafts [Taiga #5638](https://tree.taiga.io/project/penpot/us/5638)
|
||||
- Fix deleted pages comments shown in right sidebar [Taiga #5648](https://tree.taiga.io/project/penpot/us/5648)
|
||||
- Fix tooltip on toggle visibility and toggle lock buttons [Taiga #5141](https://tree.taiga.io/project/penpot/issue/5141)
|
||||
|
||||
|
||||
## 1.19.1
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix components not registered as updated [Taiga #5725](https://tree.taiga.io/project/penpot/issue/5725)
|
||||
|
||||
## 1.19.0
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
|
||||
|
||||
funcool/yetti
|
||||
{:git/tag "v9.15"
|
||||
:git/sha "aa9b967"
|
||||
{:git/tag "v9.16"
|
||||
:git/sha "7df3e08"
|
||||
:git/url "https://github.com/funcool/yetti.git"
|
||||
:exclusions [org.slf4j/slf4j-api]}
|
||||
|
||||
|
||||
@@ -22,11 +22,7 @@
|
||||
{% endif %}
|
||||
{% if item.params-schema-js %}
|
||||
<span class="tag">
|
||||
<span>SC</span>
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="tag">
|
||||
<span>SP</span>
|
||||
<span>SCHEMA</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<h2>GENERAL NOTES</h2>
|
||||
|
||||
<h3>Authentication</h3>
|
||||
<p>The penpot backend right now offerts two way for authenticate the request:
|
||||
<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
|
||||
web application) and <b>access tokens</b>.</p>
|
||||
|
||||
|
||||
@@ -6,13 +6,19 @@ penpot - error list
|
||||
|
||||
{% block content %}
|
||||
<nav>
|
||||
<h1>Latest error reports:</h1>
|
||||
<div class="title">
|
||||
<h1>Error reports (last 200)</h1>
|
||||
</div>
|
||||
</nav>
|
||||
<main class="horizontal-list">
|
||||
<ul>
|
||||
{% for item in items %}
|
||||
<li><a class="date" href="/dbg/error/{{item.id}}">{{item.created-at}}</a>
|
||||
<span class="title">{{item.hint|abbreviate:150}}</span></li>
|
||||
<li>
|
||||
<a class="date" href="/dbg/error/{{item.id}}">{{item.created-at}}</a>
|
||||
<a class="hint" href="/dbg/error/{{item.id}}">
|
||||
<span class="title">{{item.hint|abbreviate:150}}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</main>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{% extends "app/templates/base.tmpl" %}
|
||||
|
||||
{% block title %}
|
||||
penpot - error report v2 {{id}}
|
||||
Report: {{hint|abbreviate:150}} - {{id}} - Penpot Error Report (v3)
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<nav>
|
||||
<div>[<a href="/dbg/error">⮜</a>]</div>
|
||||
<div>[<a href="#message">message</a>]</div>
|
||||
<div>[<a href="#head">head</a>]</div>
|
||||
<div>[<a href="#props">props</a>]</div>
|
||||
<div>[<a href="#context">context</a>]</div>
|
||||
{% if params %}
|
||||
@@ -29,10 +29,11 @@ penpot - error report v2 {{id}}
|
||||
<main>
|
||||
<div class="table">
|
||||
<div class="table-row multiline">
|
||||
<div id="message" class="table-key">MESSAGE: </div>
|
||||
|
||||
<div id="head" class="table-key">HEAD</div>
|
||||
<div class="table-val">
|
||||
<h1>{{hint}}</h1>
|
||||
<h1><span class="not-important">Hint:</span> <br/> {{hint}}</h1>
|
||||
<h2><span class="not-important">Reported at:</span> <br/> {{created-at}}</h2>
|
||||
<h2><span class="not-important">Report ID:</span> <br/> {{id}}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -71,7 +72,7 @@ penpot - error report v2 {{id}}
|
||||
|
||||
{% if value %}
|
||||
<div class="table-row multiline">
|
||||
<div id="value" class="table-key">VALIDATION VALUE: </div>
|
||||
<div id="value" class="table-key">VALUE: </div>
|
||||
<div class="table-val">
|
||||
<pre>{{value}}</pre>
|
||||
</div>
|
||||
|
||||
@@ -36,6 +36,11 @@ small {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.not-important {
|
||||
color: #888;
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
small > strong {
|
||||
font-size: 9px;
|
||||
}
|
||||
@@ -50,7 +55,13 @@ nav {
|
||||
background: #e3e3e3;
|
||||
}
|
||||
|
||||
nav > h1 {
|
||||
nav > .title {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
nav > .title > h1 {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
font-size: 11px;
|
||||
@@ -151,7 +162,6 @@ nav > div:not(:last-child) {
|
||||
line-height: 18px;
|
||||
min-width: 210px;
|
||||
margin: 0px 20px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
<Logger name="app.rpc.commands.binfile" level="debug" />
|
||||
<Logger name="app.storage.tmp" level="debug" />
|
||||
<Logger name="app.worker" level="info" />
|
||||
<Logger name="app.worker" level="trace" />
|
||||
<Logger name="app.msgbus" level="info" />
|
||||
<Logger name="app.http.websocket" level="info" />
|
||||
<Logger name="app.util.websocket" level="info" />
|
||||
|
||||
@@ -15,7 +15,7 @@ export PENPOT_FLAGS="\
|
||||
enable-fdata-storage-objets-map \
|
||||
disable-secure-session-cookies \
|
||||
enable-smtp \
|
||||
enable-webhooks";
|
||||
enable-access-tokens";
|
||||
|
||||
set -ex
|
||||
|
||||
|
||||
@@ -8,14 +8,13 @@
|
||||
(:require
|
||||
[app.config :as cf]
|
||||
[buddy.hashers :as hashers]
|
||||
[cuerdas.core :as str]
|
||||
[promesa.exec :as px]))
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(def default-params
|
||||
{:alg :argon2id
|
||||
:memory (* 32768 2) ;; 64 MiB
|
||||
:iterations 7
|
||||
:parallelism (px/get-available-processors)})
|
||||
:memory 32768 ;; 32 MiB
|
||||
:iterations 3
|
||||
:parallelism 2})
|
||||
|
||||
(defn derive-password
|
||||
[password]
|
||||
|
||||
@@ -391,13 +391,14 @@
|
||||
(defn- get-user-info
|
||||
[{:keys [provider]} tdata]
|
||||
(try
|
||||
(let [{:keys [kid alg] :as theader} (jwt/decode-header (:token/id tdata))]
|
||||
(when-let [key (if (str/starts-with? (name alg) "hs")
|
||||
(:client-secret provider)
|
||||
(get-in provider [:jwks kid]))]
|
||||
(when (:token/id tdata)
|
||||
(let [{:keys [kid alg] :as theader} (jwt/decode-header (:token/id tdata))]
|
||||
(when-let [key (if (str/starts-with? (name alg) "hs")
|
||||
(:client-secret provider)
|
||||
(get-in provider [:jwks kid]))]
|
||||
|
||||
(let [claims (jwt/unsign (:token/id tdata) key {:alg alg})]
|
||||
(dissoc claims :exp :iss :iat :sid :aud :sub))))
|
||||
(let [claims (jwt/unsign (:token/id tdata) key {:alg alg})]
|
||||
(dissoc claims :exp :iss :iat :sid :aud :sub)))))
|
||||
(catch Throwable cause
|
||||
(l/warn :hint "unable to get user info from JWT token (unexpected exception)"
|
||||
:cause cause))))
|
||||
@@ -566,7 +567,7 @@
|
||||
profile (get-profile cfg info)]
|
||||
(generate-redirect cfg request info profile))
|
||||
(catch Throwable cause
|
||||
(l/error :hint "error on oauth process" :cause cause)
|
||||
(l/warn :hint "error on oauth process" :cause cause)
|
||||
(generate-error-redirect cfg cause))))
|
||||
|
||||
(def provider-lookup
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.db
|
||||
(:refer-clojure :exclude [get])
|
||||
(:refer-clojure :exclude [get run!])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
@@ -218,7 +218,13 @@
|
||||
|
||||
(defmacro with-atomic
|
||||
[& args]
|
||||
`(jdbc/with-transaction ~@args))
|
||||
(if (symbol? (first args))
|
||||
(let [cfgs (first args)
|
||||
body (rest args)]
|
||||
`(jdbc/with-transaction [conn# (::pool ~cfgs)]
|
||||
(let [~cfgs (assoc ~cfgs ::conn conn#)]
|
||||
~@body)))
|
||||
`(jdbc/with-transaction ~@args)))
|
||||
|
||||
(defn open
|
||||
[pool]
|
||||
@@ -293,6 +299,10 @@
|
||||
:hint "database object not found"))
|
||||
row))
|
||||
|
||||
(defn plan
|
||||
[ds sql]
|
||||
(jdbc/plan ds sql sql/default-opts))
|
||||
|
||||
(defn get-by-id
|
||||
[ds table id & {:as opts}]
|
||||
(get ds table {:id id} opts))
|
||||
@@ -381,6 +391,52 @@
|
||||
([^Connection conn ^Savepoint sp]
|
||||
(.rollback conn sp)))
|
||||
|
||||
(defn tx-run!
|
||||
[cfg f]
|
||||
(cond
|
||||
(connection? cfg)
|
||||
(tx-run! {::conn cfg} f)
|
||||
|
||||
(pool? cfg)
|
||||
(tx-run! {::pool cfg} f)
|
||||
|
||||
(::conn cfg)
|
||||
(let [conn (::conn cfg)
|
||||
sp (savepoint conn)]
|
||||
(try
|
||||
(let [result (f cfg)]
|
||||
(release! conn sp)
|
||||
result)
|
||||
(catch Throwable cause
|
||||
(rollback! sp)
|
||||
(throw cause))))
|
||||
|
||||
(::pool cfg)
|
||||
(with-atomic [conn (::pool cfg)]
|
||||
(f (assoc cfg ::conn conn)))
|
||||
|
||||
:else
|
||||
(throw (IllegalArgumentException. "invalid arguments"))))
|
||||
|
||||
(defn run!
|
||||
[cfg f]
|
||||
(cond
|
||||
(connection? cfg)
|
||||
(run! {::conn cfg} f)
|
||||
|
||||
(pool? cfg)
|
||||
(run! {::pool cfg} f)
|
||||
|
||||
(::conn cfg)
|
||||
(f cfg)
|
||||
|
||||
(::pool cfg)
|
||||
(with-open [^Connection conn (open (::pool cfg))]
|
||||
(f (assoc cfg ::conn conn)))
|
||||
|
||||
:else
|
||||
(throw (IllegalArgumentException. "invalid arguments"))))
|
||||
|
||||
(defn interval
|
||||
[o]
|
||||
(cond
|
||||
|
||||
@@ -238,9 +238,11 @@
|
||||
(-> (io/resource "app/templates/error-report.v2.tmpl")
|
||||
(tmpl/render report)))
|
||||
|
||||
(render-template-v3 [{report :content}]
|
||||
(render-template-v3 [{:keys [content id created-at]}]
|
||||
(-> (io/resource "app/templates/error-report.v3.tmpl")
|
||||
(tmpl/render report)))
|
||||
(tmpl/render (-> content
|
||||
(assoc :id id)
|
||||
(assoc :created-at (dt/format-instant created-at :rfc1123))))))
|
||||
]
|
||||
|
||||
(when-not (authorized? pool request)
|
||||
@@ -264,7 +266,7 @@
|
||||
content->>'~:hint' AS hint
|
||||
FROM server_error_report
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 100")
|
||||
LIMIT 200")
|
||||
|
||||
(defn error-list-handler
|
||||
[{:keys [::db/pool]} request]
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.config :as cf]
|
||||
[app.http :as-alias http]
|
||||
[app.http.access-token :as-alias actoken]
|
||||
[app.http.session :as-alias session]
|
||||
@@ -30,14 +31,14 @@
|
||||
(let [claims (-> {}
|
||||
(into (::session/token-claims request))
|
||||
(into (::actoken/token-claims request)))]
|
||||
{:path (:path request)
|
||||
:method (:method request)
|
||||
:params (:params request)
|
||||
:ip-addr (parse-client-ip request)
|
||||
:user-agent (yrq/get-header request "user-agent")
|
||||
:profile-id (:uid claims)
|
||||
:version (or (yrq/get-header request "x-frontend-version")
|
||||
"unknown")}))
|
||||
{:request/path (:path request)
|
||||
:request/method (:method request)
|
||||
:request/params (:params request)
|
||||
:request/user-agent (yrq/get-header request "user-agent")
|
||||
:request/ip-addr (parse-client-ip request)
|
||||
:request/profile-id (:uid claims)
|
||||
:version/frontend (or (yrq/get-header request "x-frontend-version") "unknown")
|
||||
:version/backend (:full cf/version)}))
|
||||
|
||||
(defmulti handle-exception
|
||||
(fn [err & _rest]
|
||||
@@ -73,14 +74,14 @@
|
||||
::yrs/headers headers}))
|
||||
|
||||
(defmethod handle-exception :validation
|
||||
[err _]
|
||||
[err request]
|
||||
(let [{:keys [code] :as data} (ex-data err)]
|
||||
(cond
|
||||
(= code :spec-validation)
|
||||
(let [explain (ex/explain data)]
|
||||
{::yrs/status 400
|
||||
::yrs/body (-> data
|
||||
(dissoc ::s/problems ::s/value)
|
||||
(dissoc ::s/problems ::s/value ::s/spec)
|
||||
(cond-> explain (assoc :explain explain)))})
|
||||
|
||||
(= code :params-validation)
|
||||
@@ -94,6 +95,11 @@
|
||||
(= code :request-body-too-large)
|
||||
{::yrs/status 413 ::yrs/body data}
|
||||
|
||||
(= code :invalid-image)
|
||||
(binding [l/*context* (request->context request)]
|
||||
(l/error :hint "unexpected error on processing image" :cause err)
|
||||
{::yrs/status 400 ::yrs/body data})
|
||||
|
||||
:else
|
||||
{::yrs/status 400 ::yrs/body data})))
|
||||
|
||||
|
||||
@@ -22,9 +22,10 @@
|
||||
(:import
|
||||
com.fasterxml.jackson.core.JsonParseException
|
||||
com.fasterxml.jackson.core.io.JsonEOFException
|
||||
com.fasterxml.jackson.databind.exc.MismatchedInputException
|
||||
io.undertow.server.RequestTooBigException
|
||||
java.io.OutputStream
|
||||
java.io.InputStream))
|
||||
java.io.InputStream
|
||||
java.io.OutputStream))
|
||||
|
||||
(set! *warn-on-reflection* true)
|
||||
|
||||
@@ -78,11 +79,13 @@
|
||||
|
||||
|
||||
(or (instance? JsonEOFException cause)
|
||||
(instance? JsonParseException cause))
|
||||
(instance? JsonParseException cause)
|
||||
(instance? MismatchedInputException cause))
|
||||
(raise (ex/error :type :validation
|
||||
:code :malformed-json
|
||||
:hint (ex-message cause)
|
||||
:cause cause))
|
||||
|
||||
:else
|
||||
(raise cause)))]
|
||||
|
||||
@@ -118,7 +121,9 @@
|
||||
(t/write! tw data)))
|
||||
(catch java.io.IOException _)
|
||||
(catch Throwable cause
|
||||
(l/error :hint "unexpected error on encoding response" :cause cause))
|
||||
(binding [l/*context* {:value data}]
|
||||
(l/error :hint "unexpected error on encoding response"
|
||||
:cause cause)))
|
||||
(finally
|
||||
(.close ^OutputStream output-stream))))))
|
||||
|
||||
@@ -131,8 +136,9 @@
|
||||
|
||||
(catch java.io.IOException _)
|
||||
(catch Throwable cause
|
||||
(l/error :hint "unexpected error on encoding response"
|
||||
:cause cause))
|
||||
(binding [l/*context* {:value data}]
|
||||
(l/error :hint "unexpected error on encoding response"
|
||||
:cause cause)))
|
||||
(finally
|
||||
(.close ^OutputStream output-stream))))))
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
[app.common.logging :as l]
|
||||
[app.common.pprint :as pp]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[app.http.session :as session]
|
||||
[app.metrics :as mtx]
|
||||
@@ -99,7 +100,10 @@
|
||||
(sp/pipe ch output-ch false)
|
||||
|
||||
;; Subscribe to the profile topic on msgbus/redis
|
||||
(mbus/sub! msgbus :topic profile-id :chan ch)))
|
||||
(mbus/sub! msgbus :topic profile-id :chan ch)
|
||||
|
||||
;; Subscribe to the system topic on msgbus/redis
|
||||
(mbus/sub! msgbus :topic (str uuid/zero) :chan ch)))
|
||||
|
||||
(defmethod handle-message :close
|
||||
[{:keys [::mbus/msgbus]} {:keys [::ws/id ::ws/state ::profile-id ::session-id]} _]
|
||||
|
||||
@@ -40,35 +40,33 @@
|
||||
[{:keys [::l/context ::l/message ::l/props ::l/logger ::l/level ::l/cause] :as record}]
|
||||
(us/assert! ::l/record record)
|
||||
|
||||
(let [data (ex-data cause)]
|
||||
(let [data (ex-data cause)
|
||||
ctx (-> context
|
||||
(assoc :tenant (cf/get :tenant))
|
||||
(assoc :host (cf/get :host))
|
||||
(assoc :public-uri (cf/get :public-uri))
|
||||
(assoc :logger/name logger)
|
||||
(assoc :logger/level level)
|
||||
(dissoc :request/params :value :params :data))]
|
||||
(merge
|
||||
{:context (-> context
|
||||
(assoc :tenant (cf/get :tenant))
|
||||
(assoc :host (cf/get :host))
|
||||
(assoc :public-uri (cf/get :public-uri))
|
||||
(assoc :version (:full cf/version))
|
||||
(assoc :logger-name logger)
|
||||
(assoc :logger-level level)
|
||||
(dissoc :params)
|
||||
(pp/pprint-str :width 200))
|
||||
|
||||
:props (pp/pprint-str props :width 200)
|
||||
{:context (-> (into (sorted-map) ctx)
|
||||
(pp/pprint-str :width 200 :length 50 :level 10))
|
||||
:props (pp/pprint-str props :width 200 :length 50)
|
||||
:hint (or (ex-message cause) @message)
|
||||
:trace (ex/format-throwable cause :data? false :explain? false :header? false :summary? false)}
|
||||
|
||||
(when-let [params (:params context)]
|
||||
{:params (pp/pprint-str params :width 200)})
|
||||
(when-let [params (or (:request/params context) (:params context))]
|
||||
{:params (pp/pprint-str params :width 200 :length 50 :level 10)})
|
||||
|
||||
(when-let [value (:value context)]
|
||||
{:value (pp/pprint-str value :width 200 :length 50 :level 10)})
|
||||
|
||||
(when-let [data (some-> data (dissoc ::s/problems ::s/value ::s/spec ::sm/explain :hint))]
|
||||
{:data (pp/pprint-str data :width 200)})
|
||||
|
||||
(when-let [value (-> data ::sm/explain :value)]
|
||||
{:value (pp/pprint-str value :width 200)})
|
||||
|
||||
(when-let [explain (ex/explain data)]
|
||||
(when-let [explain (ex/explain data {:level 10 :length 50})]
|
||||
{:explain explain}))))
|
||||
|
||||
|
||||
(defn error-record?
|
||||
[{:keys [::l/level ::l/cause]}]
|
||||
(and (= :error level)
|
||||
|
||||
@@ -30,7 +30,9 @@
|
||||
"```\n"
|
||||
"- host: `" (:host report) "`\n"
|
||||
"- tenant: `" (:tenant report) "`\n"
|
||||
"- version: `" (:version report) "`\n"
|
||||
"- request-path: `" (:request-path report) "`\n"
|
||||
"- frontend-version: `" (:frontend-version report) "`\n"
|
||||
"- backend-version: `" (:backend-version report) "`\n"
|
||||
"\n"
|
||||
"Trace:\n"
|
||||
(:trace report)
|
||||
@@ -50,13 +52,15 @@
|
||||
(defn record->report
|
||||
[{:keys [::l/context ::l/id ::l/cause] :as record}]
|
||||
(us/assert! ::l/record record)
|
||||
{:id id
|
||||
:tenant (cf/get :tenant)
|
||||
:host (cf/get :host)
|
||||
:public-uri (cf/get :public-uri)
|
||||
:version (:full cf/version)
|
||||
:profile-id (:profile-id context)
|
||||
:trace (ex/format-throwable cause :detail? false :header? false)})
|
||||
{:id id
|
||||
:tenant (cf/get :tenant)
|
||||
:host (cf/get :host)
|
||||
:public-uri (cf/get :public-uri)
|
||||
:backend-version (or (:version/backend context) (:full cf/version))
|
||||
:frontend-version (:version/frontend context)
|
||||
:profile-id (:request/profile-id context)
|
||||
:request-path (:request/path context)
|
||||
:trace (ex/format-throwable cause :detail? false :header? false)})
|
||||
|
||||
(defn handle-event
|
||||
[cfg record]
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.media :as cm]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.generators :as sg]
|
||||
@@ -227,7 +226,6 @@
|
||||
|
||||
(defmethod process-error org.im4java.core.InfoException
|
||||
[error]
|
||||
(l/error :hint "unexpected error on processing image" :cause error)
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-image
|
||||
:hint "invalid image"
|
||||
|
||||
@@ -324,6 +324,9 @@
|
||||
{:name "0104-mod-file-thumbnail-table"
|
||||
:fn (mg/resource "app/migrations/sql/0104-mod-file-thumbnail-table.sql")}
|
||||
|
||||
{:name "0105-mod-server-error-report-table"
|
||||
:fn (mg/resource "app/migrations/sql/0105-mod-server-error-report-table.sql")}
|
||||
|
||||
])
|
||||
|
||||
(defn apply-migrations!
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
CREATE INDEX server_error_report__created_at__idx
|
||||
ON server_error_report ( created_at );
|
||||
@@ -68,6 +68,7 @@
|
||||
::climit/key-fn ::rpc/profile-id
|
||||
::sm/params schema:push-audit-events
|
||||
::audit/skip true
|
||||
::doc/skip true
|
||||
::doc/added "1.17"}
|
||||
[{:keys [::db/pool] :as cfg} params]
|
||||
(if (or (db/read-only? pool)
|
||||
|
||||
@@ -294,28 +294,40 @@
|
||||
[output & {:keys [level] :or {level 0}}]
|
||||
(ZstdOutputStream. ^OutputStream output (int level)))
|
||||
|
||||
(defn- retrieve-file
|
||||
[pool file-id]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(binding [pmap/*load-fn* (partial files/load-pointer conn file-id)]
|
||||
(some-> (db/get* conn :file {:id file-id})
|
||||
(files/decode-row)
|
||||
(files/process-pointers deref)))))
|
||||
(defn- get-files
|
||||
[cfg ids]
|
||||
(letfn [(get-files* [{:keys [::db/conn]}]
|
||||
(let [sql (str "SELECT id FROM file "
|
||||
" WHERE id = ANY(?) ")
|
||||
ids (db/create-array conn "uuid" ids)]
|
||||
(->> (db/exec! conn [sql ids])
|
||||
(into [] (map :id))
|
||||
(not-empty))))]
|
||||
|
||||
(def ^:private sql:file-media-objects
|
||||
"SELECT * FROM file_media_object WHERE id = ANY(?)")
|
||||
(db/run! cfg get-files*)))
|
||||
|
||||
(defn- retrieve-file-media
|
||||
[pool {:keys [data id] :as file}]
|
||||
(defn- get-file
|
||||
[cfg file-id]
|
||||
(letfn [(get-file* [{:keys [::db/conn]}]
|
||||
(binding [pmap/*load-fn* (partial files/load-pointer conn file-id)]
|
||||
(some-> (db/get* conn :file {:id file-id} {::db/remove-deleted? false})
|
||||
(files/decode-row)
|
||||
(files/process-pointers deref))))]
|
||||
|
||||
(db/run! cfg get-file*)))
|
||||
|
||||
(defn- get-file-media
|
||||
[{:keys [::db/pool]} {:keys [data id] :as file}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(let [ids (app.tasks.file-gc/collect-used-media data)
|
||||
ids (db/create-array conn "uuid" ids)]
|
||||
ids (db/create-array conn "uuid" ids)
|
||||
sql (str "SELECT * FROM file_media_object WHERE id = ANY(?)")]
|
||||
|
||||
;; We assoc the file-id again to the file-media-object row
|
||||
;; because there are cases that used objects refer to other
|
||||
;; files and we need to ensure in the exportation process that
|
||||
;; all ids matches
|
||||
(->> (db/exec! conn [sql:file-media-objects ids])
|
||||
(->> (db/exec! conn [sql ids])
|
||||
(mapv #(assoc % :file-id id))))))
|
||||
|
||||
(def ^:private storage-object-id-xf
|
||||
@@ -325,34 +337,32 @@
|
||||
|
||||
(def ^:private sql:file-libraries
|
||||
"WITH RECURSIVE libs AS (
|
||||
SELECT fl.id, fl.deleted_at
|
||||
SELECT fl.id
|
||||
FROM file AS fl
|
||||
JOIN file_library_rel AS flr ON (flr.library_file_id = fl.id)
|
||||
WHERE flr.file_id = ANY(?)
|
||||
UNION
|
||||
SELECT fl.id, fl.deleted_at
|
||||
SELECT fl.id
|
||||
FROM file AS fl
|
||||
JOIN file_library_rel AS flr ON (flr.library_file_id = fl.id)
|
||||
JOIN libs AS l ON (flr.file_id = l.id)
|
||||
)
|
||||
SELECT DISTINCT l.id
|
||||
FROM libs AS l
|
||||
WHERE l.deleted_at IS NULL OR l.deleted_at > now();")
|
||||
FROM libs AS l")
|
||||
|
||||
(defn- retrieve-libraries
|
||||
[pool ids]
|
||||
(defn- get-libraries
|
||||
[{:keys [::db/pool]} ids]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(let [ids (db/create-array conn "uuid" ids)]
|
||||
(map :id (db/exec! pool [sql:file-libraries ids])))))
|
||||
|
||||
(def ^:private sql:file-library-rels
|
||||
"SELECT * FROM file_library_rel
|
||||
WHERE file_id = ANY(?)")
|
||||
|
||||
(defn- retrieve-library-relations
|
||||
[pool ids]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(db/exec! conn [sql:file-library-rels (db/create-array conn "uuid" ids)])))
|
||||
(defn- get-library-relations
|
||||
[cfg ids]
|
||||
(db/run! cfg (fn [{:keys [::db/conn]}]
|
||||
(let [ids (db/create-array conn "uuid" ids)
|
||||
sql (str "SELECT flr.* FROM file_library_rel AS flr "
|
||||
" WHERE flr.file_id = ANY(?)")]
|
||||
(db/exec! conn [sql ids])))))
|
||||
|
||||
(defn- create-or-update-file
|
||||
[conn params]
|
||||
@@ -372,13 +382,13 @@
|
||||
|
||||
;; --- GENERAL PURPOSE DYNAMIC VARS
|
||||
|
||||
(def ^:dynamic *state*)
|
||||
(def ^:dynamic *options*)
|
||||
(def ^:dynamic *state* nil)
|
||||
(def ^:dynamic *options* nil)
|
||||
|
||||
;; --- EXPORT WRITER
|
||||
|
||||
(defn- embed-file-assets
|
||||
[data conn file-id]
|
||||
[data cfg file-id]
|
||||
(letfn [(walk-map-form [form state]
|
||||
(cond
|
||||
(uuid? (:fill-color-ref-file form))
|
||||
@@ -408,7 +418,7 @@
|
||||
;; NOTE: there is a possibility that shape refers to an
|
||||
;; non-existant file because the file was removed. In this
|
||||
;; case we just ignore the asset.
|
||||
(if-let [lib (retrieve-file conn lib-id)]
|
||||
(if-let [lib (get-file cfg lib-id)]
|
||||
(reduce (partial process-asset lib) data items)
|
||||
data))
|
||||
|
||||
@@ -476,31 +486,39 @@
|
||||
[:v1/metadata :v1/files :v1/rels :v1/sobjects])))))
|
||||
|
||||
(defmethod write-section :v1/metadata
|
||||
[{:keys [::db/pool ::output ::file-ids ::include-libraries?]}]
|
||||
(let [libs (when include-libraries?
|
||||
(retrieve-libraries pool file-ids))
|
||||
files (into file-ids libs)]
|
||||
(write-obj! output {:version cf/version :files files})
|
||||
(vswap! *state* assoc :files files)))
|
||||
[{:keys [::output ::file-ids ::include-libraries?] :as cfg}]
|
||||
(if-let [fids (get-files cfg file-ids)]
|
||||
(let [lids (when include-libraries?
|
||||
(get-libraries cfg file-ids))
|
||||
ids (into fids lids)]
|
||||
(write-obj! output {:version cf/version :files ids})
|
||||
(vswap! *state* assoc :files ids))
|
||||
(ex/raise :type :not-found
|
||||
:code :files-not-found
|
||||
:hint "unable to retrieve files for export")))
|
||||
|
||||
(defmethod write-section :v1/files
|
||||
[{:keys [::db/pool ::output ::embed-assets?]}]
|
||||
[{:keys [::output ::embed-assets?] :as cfg}]
|
||||
|
||||
;; Initialize SIDS with empty vector
|
||||
(vswap! *state* assoc :sids [])
|
||||
|
||||
(doseq [file-id (-> *state* deref :files)]
|
||||
(let [file (cond-> (retrieve-file pool file-id)
|
||||
(let [file (cond-> (get-file cfg file-id)
|
||||
embed-assets?
|
||||
(update :data embed-file-assets pool file-id))
|
||||
(update :data embed-file-assets cfg file-id))
|
||||
|
||||
media (retrieve-file-media pool file)]
|
||||
media (get-file-media cfg file)]
|
||||
|
||||
(l/debug :hint "write penpot file"
|
||||
:id file-id
|
||||
:name (:name file)
|
||||
:media (count media)
|
||||
::l/sync? true)
|
||||
|
||||
(doseq [item media]
|
||||
(l/debug :hint "write penpot file media object" :id (:id item) ::l/sync? true))
|
||||
|
||||
(doto output
|
||||
(write-obj! file)
|
||||
(write-obj! media))
|
||||
@@ -508,9 +526,10 @@
|
||||
(vswap! *state* update :sids into storage-object-id-xf media))))
|
||||
|
||||
(defmethod write-section :v1/rels
|
||||
[{:keys [::db/pool ::output ::include-libraries?]}]
|
||||
(let [rels (when include-libraries?
|
||||
(retrieve-library-relations pool (-> *state* deref :files)))]
|
||||
[{:keys [::output ::include-libraries?] :as cfg}]
|
||||
(let [ids (-> *state* deref :files)
|
||||
rels (when include-libraries?
|
||||
(get-library-relations cfg ids))]
|
||||
(l/debug :hint "found rels" :total (count rels) ::l/sync? true)
|
||||
(write-obj! output rels)))
|
||||
|
||||
@@ -518,6 +537,7 @@
|
||||
[{:keys [::sto/storage ::output]}]
|
||||
(let [sids (-> *state* deref :sids)
|
||||
storage (media/configure-assets-storage storage)]
|
||||
|
||||
(l/debug :hint "found sobjects"
|
||||
:items (count sids)
|
||||
::l/sync? true)
|
||||
@@ -630,6 +650,8 @@
|
||||
(when (not= file-id expected-file-id)
|
||||
(ex/raise :type :validation
|
||||
:code :inconsistent-penpot-file
|
||||
:found-id file-id
|
||||
:expected-id expected-file-id
|
||||
:hint "the penpot file seems corrupt, found unexpected uuid (file-id)"))
|
||||
|
||||
;; Update index using with media
|
||||
@@ -679,18 +701,27 @@
|
||||
|
||||
(defmethod read-section :v1/rels
|
||||
[{:keys [::db/conn ::input ::timestamp]}]
|
||||
(let [rels (read-obj! input)]
|
||||
(let [rels (read-obj! input)
|
||||
ids (into #{} (-> *state* deref :files))]
|
||||
;; Insert all file relations
|
||||
(doseq [rel rels]
|
||||
(doseq [{:keys [library-file-id] :as rel} rels]
|
||||
(let [rel (-> rel
|
||||
(assoc :synced-at timestamp)
|
||||
(update :file-id lookup-index)
|
||||
(update :library-file-id lookup-index))]
|
||||
(l/debug :hint "create file library link"
|
||||
:file-id (:file-id rel)
|
||||
:lib-id (:library-file-id rel)
|
||||
::l/sync? true)
|
||||
(db/insert! conn :file-library-rel rel)))))
|
||||
|
||||
(if (contains? ids library-file-id)
|
||||
(do
|
||||
(l/debug :hint "create file library link"
|
||||
:file-id (:file-id rel)
|
||||
:lib-id (:library-file-id rel)
|
||||
::l/sync? true)
|
||||
(db/insert! conn :file-library-rel rel))
|
||||
|
||||
(l/warn :hint "ignoring file library link"
|
||||
:file-id (:file-id rel)
|
||||
:lib-id (:library-file-id rel)
|
||||
::l/sync? true))))))
|
||||
|
||||
(defmethod read-section :v1/sobjects
|
||||
[{:keys [::sto/storage ::db/conn ::input ::overwrite?]}]
|
||||
@@ -742,7 +773,7 @@
|
||||
(defn- lookup-index
|
||||
[id]
|
||||
(let [val (get-in @*state* [:index id])]
|
||||
(l/trace :fn "lookup-index" :id id :val val ::l/sync? true)
|
||||
(l/trc :fn "lookup-index" :id id :val val ::l/sync? true)
|
||||
(when (and (not (::ignore-index-errors? *options*)) (not val))
|
||||
(ex/raise :type :validation
|
||||
:code :incomplete-index
|
||||
@@ -755,7 +786,7 @@
|
||||
index index]
|
||||
(if-let [id (first items)]
|
||||
(let [new-id (if (::overwrite? *options*) id (uuid/next))]
|
||||
(l/trace :fn "update-index" :id id :new-id new-id ::l/sync? true)
|
||||
(l/debug :fn "update-index" :id id :new-id new-id ::l/sync? true)
|
||||
(recur (rest items)
|
||||
(assoc index id new-id)))
|
||||
index)))
|
||||
@@ -773,8 +804,7 @@
|
||||
(update-in [:metadata :id] lookup-index)
|
||||
|
||||
;; Relink paths with fill image
|
||||
(and (map? (:fill-image form))
|
||||
(= :path (:type form)))
|
||||
(map? (:fill-image form))
|
||||
(update-in [:fill-image :id] lookup-index)
|
||||
|
||||
;; This covers old shapes and the new :fills.
|
||||
|
||||
@@ -498,7 +498,8 @@
|
||||
other not needed objects removed from the `:objects` data
|
||||
structure."
|
||||
[{:keys [objects] :as page} object-id]
|
||||
(let [objects (cph/get-children-with-self objects object-id)]
|
||||
(let [objects (->> (cph/get-children-with-self objects object-id)
|
||||
(filter some?))]
|
||||
(assoc page :objects (d/index-by :id objects))))
|
||||
|
||||
(defn- prune-thumbnails
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
(sv/defmethod ::get-file-object-thumbnails
|
||||
"Retrieve a file object thumbnails."
|
||||
{::doc/added "1.17"
|
||||
::doc/module :files
|
||||
::sm/params [:map {:title "get-file-object-thumbnails"}
|
||||
[:file-id ::sm/uuid]]
|
||||
::sm/result [:map-of :string :string]
|
||||
@@ -112,6 +113,7 @@
|
||||
|
||||
(sv/defmethod ::get-file-thumbnail
|
||||
{::doc/added "1.17"
|
||||
::doc/module :files
|
||||
::doc/deprecated "1.19"}
|
||||
[{:keys [::db/pool]} {:keys [::rpc/profile-id file-id revn]}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
@@ -220,6 +222,7 @@
|
||||
mainly for render thumbnails on dashboard."
|
||||
|
||||
{::doc/added "1.17"
|
||||
::doc/module :files
|
||||
::sm/params [:map {:title "get-file-data-for-thumbnail"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:features {:optional true} ::files/features]]
|
||||
@@ -267,6 +270,7 @@
|
||||
|
||||
(sv/defmethod ::upsert-file-object-thumbnail
|
||||
{::doc/added "1.17"
|
||||
::doc/module :files
|
||||
::doc/deprecated "1.19"
|
||||
::audit/skip true}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
@@ -312,6 +316,7 @@
|
||||
|
||||
(sv/defmethod ::create-file-object-thumbnail
|
||||
{:doc/added "1.19"
|
||||
::doc/module :files
|
||||
::audit/skip true
|
||||
::sm/params schema:create-file-object-thumbnail}
|
||||
|
||||
@@ -350,6 +355,7 @@
|
||||
|
||||
(sv/defmethod ::delete-file-object-thumbnail
|
||||
{:doc/added "1.19"
|
||||
::doc/module :files
|
||||
::audit/skip true}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id object-id]}]
|
||||
|
||||
@@ -388,6 +394,7 @@
|
||||
"Creates or updates the file thumbnail. Mainly used for paint the
|
||||
grid thumbnails."
|
||||
{::doc/added "1.17"
|
||||
::doc/module :files
|
||||
::doc/deprecated "1.19"
|
||||
::audit/skip true}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
@@ -430,6 +437,7 @@
|
||||
"Creates or updates the file thumbnail. Mainly used for paint the
|
||||
grid thumbnails."
|
||||
{::doc/added "1.19"
|
||||
::doc/module :files
|
||||
::audit/skip true
|
||||
::sm/params [:map {:title "create-file-thumbnail"}
|
||||
[:file-id ::sm/uuid]
|
||||
|
||||
@@ -38,7 +38,8 @@
|
||||
"Performs the authentication using LDAP backend. Only works if LDAP
|
||||
is properly configured and enabled with `login-with-ldap` flag."
|
||||
{::rpc/auth false
|
||||
::doc/added "1.15"}
|
||||
::doc/added "1.15"
|
||||
::doc/module :auth}
|
||||
[{:keys [::main/props ::ldap/provider] :as cfg} params]
|
||||
(when-not provider
|
||||
(ex/raise :type :restriction
|
||||
|
||||
@@ -171,7 +171,8 @@
|
||||
:opt-un [::id ::name]))
|
||||
|
||||
(sv/defmethod ::create-file-media-object-from-url
|
||||
{::doc/added "1.17"}
|
||||
{::doc/added "1.17"
|
||||
::doc/deprecated "1.19"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
|
||||
(files/check-edition-permissions! pool profile-id file-id)
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
:opt-un [::search-term]))
|
||||
|
||||
(sv/defmethod ::search-files
|
||||
{::doc/added "1.17"}
|
||||
{::doc/added "1.17"
|
||||
::doc/module :files}
|
||||
[{:keys [::db/pool]} {:keys [::rpc/profile-id team-id search-term]}]
|
||||
(some->> search-term (search-files pool profile-id team-id)))
|
||||
|
||||
@@ -34,7 +34,8 @@
|
||||
|
||||
(sv/defmethod ::verify-token
|
||||
{::rpc/auth false
|
||||
::doc/added "1.15"}
|
||||
::doc/added "1.15"
|
||||
::doc/module :auth}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [token] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [claims (tokens/verify (::main/props cfg) {:token token})
|
||||
|
||||
@@ -54,14 +54,14 @@
|
||||
{:name (::sv/name mdata)
|
||||
:module (or (some-> (::module mdata) d/name)
|
||||
(-> (:ns mdata) (str/split ".") last))
|
||||
:auth (:auth mdata true)
|
||||
:auth (::rpc/auth mdata true)
|
||||
:webhook (::webhooks/event? mdata false)
|
||||
:docs (::sv/docstring mdata)
|
||||
:deprecated (::deprecated mdata)
|
||||
:added (::added mdata)
|
||||
:changes (some->> (::changes mdata) (partition-all 2) (map vec))
|
||||
:spec (fmt-spec mdata)
|
||||
:entrypoint (str (cf/get :public-uri) "/api/rpc/commands/" (::sv/name mdata))
|
||||
: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)
|
||||
@@ -75,6 +75,7 @@
|
||||
(->> methods
|
||||
(map val)
|
||||
(map first)
|
||||
(remove ::skip)
|
||||
(map get-context)
|
||||
(sort-by (juxt :module :name)))}))
|
||||
|
||||
@@ -155,7 +156,7 @@
|
||||
(map (partial gen-method-doc options))
|
||||
(sort-by (juxt :module :name))
|
||||
(map (fn [doc]
|
||||
[(str/ffmt "/commands/%" (:name doc)) (:repr doc)]))
|
||||
[(str/ffmt "/command/%" (:name doc)) (:repr doc)]))
|
||||
(into {})))]
|
||||
{:openapi "3.0.0"
|
||||
:info {:version (:main cf/version)}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
[cuerdas.core :as str]
|
||||
[expound.alpha :as expound]))
|
||||
|
||||
(def ^:dynamic *conn*)
|
||||
(def ^:dynamic *conn* nil)
|
||||
|
||||
(defn reset-password!
|
||||
"Reset a password to a specific one for a concrete user or all users
|
||||
|
||||
@@ -8,10 +8,15 @@
|
||||
"A collection of adhoc fixes scripts."
|
||||
#_:clj-kondo/ignore
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.logging :as l]
|
||||
[app.common.pprint :as p]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.msgbus :as mbus]
|
||||
[app.rpc.commands.auth :as auth]
|
||||
[app.rpc.commands.profile :as profile]
|
||||
[app.srepl.fixes :as f]
|
||||
@@ -164,3 +169,106 @@
|
||||
(alter-var-root var (fn [f]
|
||||
(or (::original (meta f)) f))))
|
||||
|
||||
(defn notify!
|
||||
[{:keys [::mbus/msgbus ::db/pool]} & {:keys [dest code message level]
|
||||
:or {code :generic level :info}
|
||||
:as params}]
|
||||
(dm/verify!
|
||||
["invalid level %" level]
|
||||
(contains? #{:success :error :info :warning} level))
|
||||
|
||||
(dm/verify!
|
||||
["invalid code: %" code]
|
||||
(contains? #{:generic :upgrade-version} code))
|
||||
|
||||
(letfn [(send [dest]
|
||||
(l/inf :hint "sending notification" :dest (str dest))
|
||||
(let [message {:type :notification
|
||||
:code code
|
||||
:level level
|
||||
:version (:full cf/version)
|
||||
:subs-id dest
|
||||
:message message}
|
||||
message (->> (dissoc params :dest :code :message :level)
|
||||
(merge message))]
|
||||
(mbus/pub! msgbus
|
||||
:topic (str dest)
|
||||
:message message)))
|
||||
|
||||
(resolve-profile [email]
|
||||
(some-> (db/get* pool :profile {:email (str/lower email)} {:columns [:id]}) :id vector))
|
||||
|
||||
(resolve-team [team-id]
|
||||
(->> (db/query pool :team-profile-rel
|
||||
{:team-id team-id}
|
||||
{:columns [:profile-id]})
|
||||
(map :profile-id)))
|
||||
|
||||
(parse-uuid [v]
|
||||
(if (uuid? v)
|
||||
v
|
||||
(d/parse-uuid v)))
|
||||
|
||||
(resolve-dest [dest]
|
||||
(cond
|
||||
(uuid? dest)
|
||||
[dest]
|
||||
|
||||
(string? dest)
|
||||
(some-> dest parse-uuid resolve-dest)
|
||||
|
||||
(nil? dest)
|
||||
(resolve-dest uuid/zero)
|
||||
|
||||
(map? dest)
|
||||
(sequence (comp
|
||||
(map vec)
|
||||
(mapcat resolve-dest))
|
||||
dest)
|
||||
|
||||
(and (coll? dest)
|
||||
(every? coll? dest))
|
||||
(sequence (comp
|
||||
(map vec)
|
||||
(mapcat resolve-dest))
|
||||
dest)
|
||||
|
||||
(vector? dest)
|
||||
(let [[op param] dest]
|
||||
(cond
|
||||
(= op :email)
|
||||
(cond
|
||||
(and (coll? param)
|
||||
(every? string? param))
|
||||
(sequence (comp
|
||||
(keep resolve-profile)
|
||||
(mapcat identity))
|
||||
param)
|
||||
|
||||
(string? param)
|
||||
(resolve-profile param))
|
||||
|
||||
(= op :team-id)
|
||||
(cond
|
||||
(coll? param)
|
||||
(sequence (comp
|
||||
(mapcat resolve-team)
|
||||
(keep parse-uuid))
|
||||
param)
|
||||
|
||||
(uuid? param)
|
||||
(resolve-team param)
|
||||
|
||||
(string? param)
|
||||
(some-> param parse-uuid resolve-team))
|
||||
|
||||
(= op :profile-id)
|
||||
(if (coll? param)
|
||||
(sequence (keep parse-uuid) param)
|
||||
(resolve-dest param))))))
|
||||
]
|
||||
|
||||
(->> (resolve-dest dest)
|
||||
(filter some?)
|
||||
(into #{})
|
||||
(run! send))))
|
||||
|
||||
@@ -251,53 +251,59 @@
|
||||
|
||||
(defmethod ig/init-key ::gc-deleted-task
|
||||
[_ {:keys [::db/pool ::storage ::min-age]}]
|
||||
(letfn [(retrieve-deleted-objects-chunk [conn min-age cursor]
|
||||
(let [min-age (db/interval min-age)
|
||||
rows (db/exec! conn [sql:retrieve-deleted-objects-chunk min-age cursor])]
|
||||
[(some-> rows peek :created-at)
|
||||
(letfn [(get-to-delete-chunk [cursor]
|
||||
(let [sql (str "select s.* "
|
||||
" from storage_object as s "
|
||||
" where s.deleted_at is not null "
|
||||
" and s.deleted_at < ? "
|
||||
" order by s.deleted_at desc "
|
||||
" limit 25")
|
||||
rows (db/exec! pool [sql cursor])]
|
||||
[(some-> rows peek :deleted-at)
|
||||
(some->> (seq rows) (d/group-by #(-> % :backend keyword) :id #{}) seq)]))
|
||||
|
||||
(retrieve-deleted-objects [conn min-age]
|
||||
(d/iteration (partial retrieve-deleted-objects-chunk conn min-age)
|
||||
:initk (dt/now)
|
||||
(get-to-delete-chunks [min-age]
|
||||
(d/iteration get-to-delete-chunk
|
||||
:initk (dt/minus (dt/now) min-age)
|
||||
:vf second
|
||||
:kf first))
|
||||
|
||||
(delete-in-bulk [backend-id ids]
|
||||
(let [backend (impl/resolve-backend storage backend-id)]
|
||||
(delete-in-bulk! [backend-id ids]
|
||||
(try
|
||||
(db/with-atomic [conn pool]
|
||||
(let [sql "delete from storage_object where id = ANY(?)"
|
||||
ids' (db/create-array conn "uuid" ids)
|
||||
|
||||
(doseq [id ids]
|
||||
(l/debug :hint "gc-deleted: permanently delete storage object" :backend backend-id :id id))
|
||||
total (-> (db/exec-one! conn [sql ids'])
|
||||
(db/get-update-count))]
|
||||
|
||||
(impl/del-objects-in-bulk backend ids)))]
|
||||
(-> (impl/resolve-backend storage backend-id)
|
||||
(impl/del-objects-in-bulk ids))
|
||||
|
||||
(doseq [id ids]
|
||||
(l/dbg :hint "gc-deleted: permanently delete storage object" :backend backend-id :id id))
|
||||
|
||||
total))
|
||||
|
||||
(catch Throwable cause
|
||||
(l/err :hint "gc-deleted: unexpected error on bulk deletion"
|
||||
:ids (vec ids)
|
||||
:cause cause)
|
||||
0)))]
|
||||
|
||||
(fn [params]
|
||||
(let [min-age (or (:min-age params) min-age)]
|
||||
(db/with-atomic [conn pool]
|
||||
(loop [total 0
|
||||
groups (retrieve-deleted-objects conn min-age)]
|
||||
(if-let [[backend-id ids] (first groups)]
|
||||
(do
|
||||
(delete-in-bulk backend-id ids)
|
||||
(recur (+ total (count ids))
|
||||
(rest groups)))
|
||||
(do
|
||||
(l/info :hint "gc-deleted: task finished" :min-age (dt/format-duration min-age) :total total)
|
||||
{:deleted total}))))))))
|
||||
|
||||
(def sql:retrieve-deleted-objects-chunk
|
||||
"with items_part as (
|
||||
select s.id
|
||||
from storage_object as s
|
||||
where s.deleted_at is not null
|
||||
and s.deleted_at < (now() - ?::interval)
|
||||
and s.created_at < ?
|
||||
order by s.created_at desc
|
||||
limit 25
|
||||
)
|
||||
delete from storage_object
|
||||
where id in (select id from items_part)
|
||||
returning *;")
|
||||
(let [min-age (or (some-> params :min-age dt/duration) min-age)]
|
||||
(loop [total 0
|
||||
chunks (get-to-delete-chunks min-age)]
|
||||
(if-let [[backend-id ids] (first chunks)]
|
||||
(let [deleted (delete-in-bulk! backend-id ids)]
|
||||
(recur (+ total deleted)
|
||||
(rest chunks)))
|
||||
(do
|
||||
(l/inf :hint "gc-deleted: task finished"
|
||||
:min-age (dt/format-duration min-age)
|
||||
:total total)
|
||||
{:deleted total})))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Garbage Collection: Analyze touched objects
|
||||
|
||||
@@ -113,8 +113,15 @@
|
||||
(mapcat vals)
|
||||
(keep (fn [{:keys [type] :as obj}]
|
||||
(case type
|
||||
:path (get-in obj [:fill-image :id])
|
||||
:path (get-in obj [:fill-image :id])
|
||||
:bool (get-in obj [:fill-image :id])
|
||||
;; NOTE: because of some bug, we ended with
|
||||
;; many shape types having the ability to
|
||||
;; have fill-image attribute (which initially
|
||||
;; designed for :path shapes).
|
||||
:group (get-in obj [:fill-image :id])
|
||||
:image (get-in obj [:metadata :id])
|
||||
|
||||
nil))))
|
||||
pages (concat
|
||||
(vals (:pages-index data))
|
||||
|
||||
@@ -87,10 +87,10 @@
|
||||
|
||||
(defmethod ig/init-key ::registry
|
||||
[_ {:keys [::mtx/metrics ::tasks]}]
|
||||
(l/info :hint "registry initialized" :tasks (count tasks))
|
||||
(l/inf :hint "registry initialized" :tasks (count tasks))
|
||||
(reduce-kv (fn [registry k v]
|
||||
(let [tname (name k)]
|
||||
(l/trace :hint "register task" :name tname)
|
||||
(l/trc :hint "register task" :name tname)
|
||||
(assoc registry tname (wrap-task-handler metrics tname v))))
|
||||
{}
|
||||
tasks))
|
||||
@@ -141,18 +141,18 @@
|
||||
|
||||
(px/thread
|
||||
{:name "penpot/executors-monitor" :virtual true}
|
||||
(l/info :hint "monitor: started" :name name)
|
||||
(l/inf :hint "monitor: started" :name name)
|
||||
(try
|
||||
(loop [steals 0]
|
||||
(when-not (px/shutdown? executor)
|
||||
(px/sleep interval)
|
||||
(recur (long (monitor! executor steals)))))
|
||||
(catch InterruptedException _cause
|
||||
(l/debug :hint "monitor: interrupted" :name name))
|
||||
(l/trc :hint "monitor: interrupted" :name name))
|
||||
(catch Throwable cause
|
||||
(l/error :hint "monitor: unexpected error" :name name :cause cause))
|
||||
(l/err :hint "monitor: unexpected error" :name name :cause cause))
|
||||
(finally
|
||||
(l/info :hint "monitor: terminated" :name name))))))
|
||||
(l/inf :hint "monitor: terminated" :name name))))))
|
||||
|
||||
(defmethod ig/halt-key! ::monitor
|
||||
[_ thread]
|
||||
@@ -207,10 +207,10 @@
|
||||
(db/create-array conn "uuid" ids)]]
|
||||
|
||||
(db/exec-one! conn sql)
|
||||
(l/debug :hist "dispatcher: queue tasks"
|
||||
:queue queue
|
||||
:tasks (count ids)
|
||||
:queued res)))
|
||||
(l/trc :hist "dispatcher: queue tasks"
|
||||
:queue queue
|
||||
:tasks (count ids)
|
||||
:queued res)))
|
||||
|
||||
(run-batch! [rconn]
|
||||
(try
|
||||
@@ -225,35 +225,35 @@
|
||||
(cond
|
||||
(rds/exception? cause)
|
||||
(do
|
||||
(l/warn :hint "dispatcher: redis exception (will retry in an instant)" :cause cause)
|
||||
(l/wrn :hint "dispatcher: redis exception (will retry in an instant)" :cause cause)
|
||||
(px/sleep (::rds/timeout rconn)))
|
||||
|
||||
(db/sql-exception? cause)
|
||||
(do
|
||||
(l/warn :hint "dispatcher: database exception (will retry in an instant)" :cause cause)
|
||||
(l/wrn :hint "dispatcher: database exception (will retry in an instant)" :cause cause)
|
||||
(px/sleep (::rds/timeout rconn)))
|
||||
|
||||
:else
|
||||
(do
|
||||
(l/error :hint "dispatcher: unhandled exception (will retry in an instant)" :cause cause)
|
||||
(l/err :hint "dispatcher: unhandled exception (will retry in an instant)" :cause cause)
|
||||
(px/sleep (::rds/timeout rconn)))))))
|
||||
|
||||
(dispatcher []
|
||||
(l/info :hint "dispatcher: started")
|
||||
(l/inf :hint "dispatcher: started")
|
||||
(try
|
||||
(dm/with-open [rconn (rds/connect redis)]
|
||||
(loop []
|
||||
(run-batch! rconn)
|
||||
(recur)))
|
||||
(catch InterruptedException _
|
||||
(l/trace :hint "dispatcher: interrupted"))
|
||||
(l/trc :hint "dispatcher: interrupted"))
|
||||
(catch Throwable cause
|
||||
(l/error :hint "dispatcher: unexpected exception" :cause cause))
|
||||
(l/err :hint "dispatcher: unexpected exception" :cause cause))
|
||||
(finally
|
||||
(l/info :hint "dispatcher: terminated"))))]
|
||||
(l/inf :hint "dispatcher: terminated"))))]
|
||||
|
||||
(if (db/read-only? pool)
|
||||
(l/warn :hint "dispatcher: not started (db is read-only)")
|
||||
(l/wrn :hint "dispatcher: not started (db is read-only)")
|
||||
(px/fn->thread dispatcher :name "penpot/worker/dispatcher" :virtual true))))
|
||||
|
||||
(defmethod ig/halt-key! ::dispatcher
|
||||
@@ -286,7 +286,7 @@
|
||||
(let [queue (d/name queue)
|
||||
cfg (assoc cfg ::queue queue)]
|
||||
(if (db/read-only? pool)
|
||||
(l/warn :hint "worker: not started (db is read-only)" :queue queue :parallelism parallelism)
|
||||
(l/wrn :hint "worker: not started (db is read-only)" :queue queue :parallelism parallelism)
|
||||
(doall
|
||||
(->> (range parallelism)
|
||||
(map #(assoc cfg ::worker-id %))
|
||||
@@ -300,7 +300,7 @@
|
||||
[{:keys [::rds/redis ::worker-id ::queue] :as cfg}]
|
||||
(px/thread
|
||||
{:name (format "penpot/worker/runner:%s" worker-id)}
|
||||
(l/info :hint "worker: started" :worker-id worker-id :queue queue)
|
||||
(l/inf :hint "worker: started" :worker-id worker-id :queue queue)
|
||||
(try
|
||||
(dm/with-open [rconn (rds/connect redis)]
|
||||
(let [tenant (cf/get :tenant "main")
|
||||
@@ -320,14 +320,14 @@
|
||||
:worker-id worker-id
|
||||
:queue queue))
|
||||
(catch Throwable cause
|
||||
(l/error :hint "worker: unexpected exception"
|
||||
:worker-id worker-id
|
||||
:queue queue
|
||||
:cause cause))
|
||||
(l/err :hint "worker: unexpected exception"
|
||||
:worker-id worker-id
|
||||
:queue queue
|
||||
:cause cause))
|
||||
(finally
|
||||
(l/info :hint "worker: terminated"
|
||||
:worker-id worker-id
|
||||
:queue queue)))))
|
||||
(l/inf :hint "worker: terminated"
|
||||
:worker-id worker-id
|
||||
:queue queue)))))
|
||||
|
||||
(defn- run-worker-loop!
|
||||
[{:keys [::db/pool ::rds/rconn ::timeout ::queue ::registry ::worker-id]}]
|
||||
@@ -368,19 +368,19 @@
|
||||
(let [task-id (t/decode payload)]
|
||||
(if (uuid? task-id)
|
||||
task-id
|
||||
(l/error :hint "worker: received unexpected payload (uuid expected)"
|
||||
:payload task-id)))
|
||||
(l/err :hint "worker: received unexpected payload (uuid expected)"
|
||||
:payload task-id)))
|
||||
(catch Throwable cause
|
||||
(l/error :hint "worker: unable to decode payload"
|
||||
:payload payload
|
||||
:length (alength payload)
|
||||
:cause cause))))
|
||||
(l/err :hint "worker: unable to decode payload"
|
||||
:payload payload
|
||||
:length (alength payload)
|
||||
:cause cause))))
|
||||
|
||||
(handle-task [{:keys [name] :as task}]
|
||||
(let [task-fn (get registry name)]
|
||||
(if task-fn
|
||||
(task-fn task)
|
||||
(l/warn :hint "no task handler found" :name name))
|
||||
(l/wrn :hint "no task handler found" :name name))
|
||||
{:status :completed :task task}))
|
||||
|
||||
(handle-task-exception [cause task]
|
||||
@@ -395,9 +395,9 @@
|
||||
(= ::noop (:strategy edata))
|
||||
(assoc :inc-by 0))
|
||||
(do
|
||||
(l/error :hint "worker: unhandled exception on task"
|
||||
::l/context (get-error-context cause task)
|
||||
:cause cause)
|
||||
(l/err :hint "worker: unhandled exception on task"
|
||||
::l/context (get-error-context cause task)
|
||||
:cause cause)
|
||||
(if (>= (:retry-num task) (:max-retries task))
|
||||
{:status :failed :task task :error cause}
|
||||
{:status :retry :task task :error cause})))))
|
||||
@@ -414,31 +414,31 @@
|
||||
(if (or (db/connection-error? task)
|
||||
(db/serialization-error? task))
|
||||
(do
|
||||
(l/warn :hint "worker: connection error on retrieving task from database (retrying in some instants)"
|
||||
:worker-id worker-id
|
||||
:cause task)
|
||||
(l/wrn :hint "worker: connection error on retrieving task from database (retrying in some instants)"
|
||||
:worker-id worker-id
|
||||
:cause task)
|
||||
(px/sleep (::rds/timeout rconn))
|
||||
(recur (get-task task-id)))
|
||||
(do
|
||||
(l/error :hint "worker: unhandled exception on retrieving task from database (retrying in some instants)"
|
||||
:worker-id worker-id
|
||||
:cause task)
|
||||
(l/err :hint "worker: unhandled exception on retrieving task from database (retrying in some instants)"
|
||||
:worker-id worker-id
|
||||
:cause task)
|
||||
(px/sleep (::rds/timeout rconn))
|
||||
(recur (get-task task-id))))
|
||||
|
||||
(nil? task)
|
||||
(l/warn :hint "worker: no task found on the database"
|
||||
:worker-id worker-id
|
||||
:task-id task-id)
|
||||
(l/wrn :hint "worker: no task found on the database"
|
||||
:worker-id worker-id
|
||||
:task-id task-id)
|
||||
|
||||
:else
|
||||
(try
|
||||
(l/debug :hint "worker: executing task"
|
||||
:name (:name task)
|
||||
:id (:id task)
|
||||
:queue queue
|
||||
:worker-id worker-id
|
||||
:retry (:retry-num task))
|
||||
(l/trc :hint "executing task"
|
||||
:name (:name task)
|
||||
:id (str (:id task))
|
||||
:queue queue
|
||||
:worker-id worker-id
|
||||
:retry (:retry-num task))
|
||||
(handle-task task)
|
||||
(catch InterruptedException cause
|
||||
(throw cause))
|
||||
@@ -459,13 +459,13 @@
|
||||
(if (or (db/connection-error? cause)
|
||||
(db/serialization-error? cause))
|
||||
(do
|
||||
(l/warn :hint "worker: database exeption on processing task result (retrying in some instants)"
|
||||
:cause cause)
|
||||
(l/wrn :hint "worker: database exeption on processing task result (retrying in some instants)"
|
||||
:cause cause)
|
||||
(px/sleep (::rds/timeout rconn))
|
||||
(recur result))
|
||||
(do
|
||||
(l/error :hint "worker: unhandled exception on processing task result (retrying in some instants)"
|
||||
:cause cause)
|
||||
(l/err :hint "worker: unhandled exception on processing task result (retrying in some instants)"
|
||||
:cause cause)
|
||||
(px/sleep (::rds/timeout rconn))
|
||||
(recur result))))))]
|
||||
|
||||
@@ -481,24 +481,16 @@
|
||||
(catch Exception cause
|
||||
(if (rds/timeout-exception? cause)
|
||||
(do
|
||||
(l/error :hint "worker: redis pop operation timeout, consider increasing redis timeout (will retry in some instants)"
|
||||
:timeout timeout
|
||||
:cause cause)
|
||||
(l/err :hint "worker: redis pop operation timeout, consider increasing redis timeout (will retry in some instants)"
|
||||
:timeout timeout
|
||||
:cause cause)
|
||||
(px/sleep timeout))
|
||||
|
||||
(l/error :hint "worker: unhandled exception" :cause cause))))))
|
||||
(l/err :hint "worker: unhandled exception" :cause cause))))))
|
||||
|
||||
(defn- get-error-context
|
||||
[error item]
|
||||
(let [data (ex-data error)]
|
||||
(merge
|
||||
{:hint (ex-message error)
|
||||
:spec-problems (some->> data ::s/problems (take 10) seq vec)
|
||||
:spec-value (some->> data ::s/value)
|
||||
:data (some-> data (dissoc ::s/problems ::s/value ::s/spec))
|
||||
:params item}
|
||||
(when-let [explain (ex/explain data)]
|
||||
{:spec-explain explain}))))
|
||||
[_ item]
|
||||
{:params item})
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; CRON
|
||||
@@ -525,7 +517,7 @@
|
||||
(defmethod ig/init-key ::cron
|
||||
[_ {:keys [::entries ::registry ::db/pool] :as cfg}]
|
||||
(if (db/read-only? pool)
|
||||
(l/warn :hint "cron: not started (db is read-only)")
|
||||
(l/wrn :hint "cron: not started (db is read-only)")
|
||||
(let [running (atom #{})
|
||||
entries (->> entries
|
||||
(filter some?)
|
||||
@@ -548,22 +540,22 @@
|
||||
|
||||
cfg (assoc cfg ::entries entries ::running running)]
|
||||
|
||||
(l/info :hint "cron: started" :tasks (count entries))
|
||||
(synchronize-cron-entries! cfg)
|
||||
(l/inf :hint "cron: started" :tasks (count entries))
|
||||
(synchronize-cron-entries! cfg)
|
||||
|
||||
(->> (filter some? entries)
|
||||
(run! (partial schedule-cron-task cfg)))
|
||||
(->> (filter some? entries)
|
||||
(run! (partial schedule-cron-task cfg)))
|
||||
|
||||
(reify
|
||||
clojure.lang.IDeref
|
||||
(deref [_] @running)
|
||||
(reify
|
||||
clojure.lang.IDeref
|
||||
(deref [_] @running)
|
||||
|
||||
java.lang.AutoCloseable
|
||||
(close [_]
|
||||
(l/info :hint "cron: terminated")
|
||||
(doseq [item @running]
|
||||
(when-not (.isDone ^Future item)
|
||||
(.cancel ^Future item true))))))))
|
||||
java.lang.AutoCloseable
|
||||
(close [_]
|
||||
(l/inf :hint "cron: terminated")
|
||||
(doseq [item @running]
|
||||
(when-not (.isDone ^Future item)
|
||||
(.cancel ^Future item true))))))))
|
||||
|
||||
(defmethod ig/halt-key! ::cron
|
||||
[_ instance]
|
||||
@@ -579,11 +571,14 @@
|
||||
[{:keys [::db/pool ::entries]}]
|
||||
(db/with-atomic [conn pool]
|
||||
(doseq [{:keys [id cron]} entries]
|
||||
(l/trace :hint "register cron task" :id id :cron (str cron))
|
||||
(l/trc :hint "register cron task" :id id :cron (str cron))
|
||||
(db/exec-one! conn [sql:upsert-cron-task id (str cron) (str cron)]))))
|
||||
|
||||
(def sql:lock-cron-task
|
||||
"select id from scheduled_task where id=? for update skip locked")
|
||||
(defn- lock-scheduled-task!
|
||||
[conn id]
|
||||
(let [sql (str "SELECT id FROM scheduled_task "
|
||||
" WHERE id=? FOR UPDATE SKIP LOCKED")]
|
||||
(some? (db/exec-one! conn [sql (d/name id)]))))
|
||||
|
||||
(defn- execute-cron-task
|
||||
[{:keys [::db/pool] :as cfg} {:keys [id] :as task}]
|
||||
@@ -591,16 +586,21 @@
|
||||
{:name (str "penpot/cront-task/" id)}
|
||||
(try
|
||||
(db/with-atomic [conn pool]
|
||||
(when (db/exec-one! conn [sql:lock-cron-task (d/name id)])
|
||||
(l/trace :hint "cron: execute task" :task-id id)
|
||||
((:fn task) task)))
|
||||
(db/exec-one! conn ["SET statement_timeout=0;"])
|
||||
(db/exec-one! conn ["SET idle_in_transaction_session_timeout=0;"])
|
||||
(when (lock-scheduled-task! conn id)
|
||||
(l/dbg :hint "cron: execute task" :task-id id)
|
||||
((:fn task) task))
|
||||
(db/rollback! conn))
|
||||
|
||||
(catch InterruptedException _
|
||||
(l/debug :hint "cron: task interrupted" :task-id id))
|
||||
|
||||
(catch Throwable cause
|
||||
(l/error :hint "cron: unhandled exception on running task"
|
||||
::l/context (get-error-context cause task)
|
||||
(binding [l/*context* (get-error-context cause task)]
|
||||
(l/err :hint "cron: unhandled exception on running task"
|
||||
:task-id id
|
||||
:cause cause))
|
||||
:cause cause)))
|
||||
(finally
|
||||
(when-not (px/interrupted? :current)
|
||||
(schedule-cron-task cfg task))))))
|
||||
@@ -610,12 +610,16 @@
|
||||
(s/assert dt/cron? cron)
|
||||
(let [now (dt/now)
|
||||
next (dt/next-valid-instant-from cron now)]
|
||||
(inst-ms (dt/diff now next))))
|
||||
(dt/diff now next)))
|
||||
|
||||
(defn- schedule-cron-task
|
||||
[{:keys [::running] :as cfg} {:keys [cron] :as task}]
|
||||
(let [ft (px/schedule! (ms-until-valid cron)
|
||||
(partial execute-cron-task cfg task))]
|
||||
[{:keys [::running] :as cfg} {:keys [cron id] :as task}]
|
||||
(let [ts (ms-until-valid cron)
|
||||
ft (px/schedule! ts (partial execute-cron-task cfg task))]
|
||||
|
||||
(l/dbg :hint "cron: schedule task" :task-id id
|
||||
:ts (dt/format-duration ts)
|
||||
:at (dt/format-instant (dt/in-future ts)))
|
||||
(swap! running #(into #{ft} (filter p/pending?) %))))
|
||||
|
||||
|
||||
@@ -678,13 +682,13 @@
|
||||
(-> (db/exec-one! conn [sql:remove-not-started-tasks task queue label])
|
||||
:next.jdbc/update-count))]
|
||||
|
||||
(l/debug :hint "submit task"
|
||||
:name task
|
||||
:queue queue
|
||||
:label label
|
||||
:dedupe (boolean dedupe)
|
||||
:deleted (or deleted 0)
|
||||
:in (dt/format-duration duration))
|
||||
(l/trc :hint "submit task"
|
||||
:name task
|
||||
:queue queue
|
||||
:label label
|
||||
:dedupe (boolean dedupe)
|
||||
:deleted (or deleted 0)
|
||||
:in (dt/format-duration duration))
|
||||
|
||||
(db/exec-one! conn [sql:insert-new-task id task props queue
|
||||
label priority max-retries interval])
|
||||
|
||||
@@ -100,6 +100,7 @@
|
||||
(configure-storage-backend))
|
||||
content1 (sto/content "content1")
|
||||
content2 (sto/content "content2")
|
||||
content3 (sto/content "content3")
|
||||
object1 (sto/put-object! storage {::sto/content content1
|
||||
::sto/expired-at (dt/now)
|
||||
:content-type "text/plain"
|
||||
@@ -107,16 +108,20 @@
|
||||
object2 (sto/put-object! storage {::sto/content content2
|
||||
::sto/expired-at (dt/in-past {:hours 2})
|
||||
:content-type "text/plain"
|
||||
})
|
||||
object3 (sto/put-object! storage {::sto/content content3
|
||||
::sto/expired-at (dt/in-past {:hours 1})
|
||||
:content-type "text/plain"
|
||||
})]
|
||||
|
||||
|
||||
(th/sleep 200)
|
||||
|
||||
(let [task (:app.storage/gc-deleted-task th/*system*)
|
||||
res (task {})]
|
||||
(let [res (th/run-task! :storage-gc-deleted {})]
|
||||
(t/is (= 1 (:deleted res))))
|
||||
|
||||
(let [res (db/exec-one! th/*pool* ["select count(*) from storage_object;"])]
|
||||
(t/is (= 1 (:count res))))))
|
||||
(t/is (= 2 (:count res))))))
|
||||
|
||||
(t/deftest test-touched-gc-task-1
|
||||
(let [storage (-> (:app.storage/storage th/*system*)
|
||||
|
||||
@@ -93,6 +93,13 @@
|
||||
[:component-id {:optional true} ::sm/uuid]
|
||||
[:ignore-touched {:optional true} :boolean]]]
|
||||
|
||||
[:fix-obj
|
||||
[:map {:title "FixObjChange"}
|
||||
[:type [:= :fix-obj]]
|
||||
[:id ::sm/uuid]
|
||||
[:page-id {:optional true} ::sm/uuid]
|
||||
[:component-id {:optional true} ::sm/uuid]]]
|
||||
|
||||
[:mov-objects
|
||||
[:map {:title "MovObjectsChange"}
|
||||
[:type [:= :mov-objects]]
|
||||
@@ -218,7 +225,7 @@
|
||||
|
||||
(sm/def! ::changes
|
||||
[:sequential {:gen/max 2} ::change])
|
||||
|
||||
|
||||
(def change?
|
||||
(sm/pred-fn ::change))
|
||||
|
||||
@@ -322,7 +329,9 @@
|
||||
component-root (ctn/get-component-shape objects shape {:allow-main? true})]
|
||||
(if (and (some? component-root) (ctk/main-instance? component-root))
|
||||
(ctkl/set-component-modified data (:component-id component-root))
|
||||
data))
|
||||
(if (some? component-id)
|
||||
(ctkl/set-component-modified data component-id)
|
||||
data)))
|
||||
data))]
|
||||
|
||||
(as-> data $
|
||||
@@ -337,6 +346,12 @@
|
||||
(d/update-in-when data [:pages-index page-id] ctst/delete-shape id ignore-touched)
|
||||
(d/update-in-when data [:components component-id] ctst/delete-shape id ignore-touched)))
|
||||
|
||||
(defmethod process-change :fix-obj
|
||||
[data {:keys [page-id component-id] :as params}]
|
||||
(if page-id
|
||||
(d/update-in-when data [:pages-index page-id] ctst/fix-shape-children params)
|
||||
(d/update-in-when data [:components component-id] ctst/fix-shape-children params)))
|
||||
|
||||
;; FIXME: remove, seems like this method is already unused
|
||||
;; reg-objects operation "regenerates" the geometry and selrect of the parent groups
|
||||
(defmethod process-change :reg-objects
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
[app.common.schema :as sm]
|
||||
[app.common.uuid :as uuid]))
|
||||
|
||||
(def file-version 21)
|
||||
(def file-version 22)
|
||||
(def default-color clr/gray-20)
|
||||
(def root uuid/zero)
|
||||
|
||||
|
||||
@@ -12,10 +12,11 @@
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.geom.shapes.path :as gsp]
|
||||
[app.common.geom.shapes.text :as gsht]
|
||||
[app.common.logging :as log]
|
||||
[app.common.logging :as l]
|
||||
[app.common.math :as mth]
|
||||
[app.common.pages :as cp]
|
||||
[app.common.pages.helpers :as cph]
|
||||
[app.common.text :as txt]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.uuid :as uuid]
|
||||
[cuerdas.core :as str]))
|
||||
@@ -24,7 +25,7 @@
|
||||
|
||||
(defmulti migrate :version)
|
||||
|
||||
(log/set-level! :info)
|
||||
#?(:cljs (l/set-level! :info))
|
||||
|
||||
(defn migrate-data
|
||||
([data] (migrate-data data cp/file-version))
|
||||
@@ -32,7 +33,7 @@
|
||||
(if (= (:version data) to-version)
|
||||
data
|
||||
(let [migrate-fn #(do
|
||||
(log/trace :hint "migrate file" :id (:id %) :version-from %2 :version-to (inc %2))
|
||||
(l/trc :hint "migrate file" :id (:id %) :version-from %2 :version-to (inc %2))
|
||||
(migrate (assoc %1 :version (inc %2))))]
|
||||
(reduce migrate-fn data (range (:version data 0) to-version))))))
|
||||
|
||||
@@ -449,11 +450,11 @@
|
||||
;; If we cannot find any we let the frame-id as it was before
|
||||
frame-id)]
|
||||
(when (not= frame-id calculated-frame-id)
|
||||
(log/info :hint "Fix wrong frame-id"
|
||||
:shape (:name object)
|
||||
:id (:id object)
|
||||
:current (dm/get-in objects [frame-id :name])
|
||||
:calculated (get-in objects [calculated-frame-id :name])))
|
||||
(l/trc :hint "Fix wrong frame-id"
|
||||
:shape (:name object)
|
||||
:id (:id object)
|
||||
:current (dm/get-in objects [frame-id :name])
|
||||
:calculated (get-in objects [calculated-frame-id :name])))
|
||||
(assoc object :frame-id calculated-frame-id)))
|
||||
|
||||
(update-container [container]
|
||||
@@ -465,3 +466,38 @@
|
||||
|
||||
;; TODO: pending to do a migration for delete already not used fill
|
||||
;; and stroke props. This should be done for >1.14.x version.
|
||||
|
||||
(defmethod migrate 22
|
||||
[data]
|
||||
(letfn [(valid-ref? [ref]
|
||||
(or (uuid? ref)
|
||||
(nil? ref)))
|
||||
|
||||
(valid-node? [node]
|
||||
(and (valid-ref? (:typography-ref-file node))
|
||||
(valid-ref? (:typography-ref-id node))
|
||||
(valid-ref? (:fill-color-ref-file node))
|
||||
(valid-ref? (:fill-color-ref-id node))))
|
||||
|
||||
(fix-ref [ref]
|
||||
(if (valid-ref? ref) ref nil))
|
||||
|
||||
(fix-node [node]
|
||||
(-> node
|
||||
(d/update-when :typography-ref-file fix-ref)
|
||||
(d/update-when :typography-ref-id fix-ref)
|
||||
(d/update-when :fill-color-ref-file fix-ref)
|
||||
(d/update-when :fill-color-ref-id fix-ref)))
|
||||
|
||||
(update-object [object]
|
||||
(let [invalid-node? (complement valid-node?)]
|
||||
(cond-> object
|
||||
(cph/text-shape? object)
|
||||
(update :content #(txt/transform-nodes invalid-node? fix-node %)))))
|
||||
|
||||
(update-container [container]
|
||||
(update container :objects update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index update-vals update-container)
|
||||
(update :components update-vals update-container))))
|
||||
|
||||
@@ -382,8 +382,10 @@
|
||||
keyword
|
||||
identity)}}))})
|
||||
|
||||
(def max-safe-int (int 1e6))
|
||||
(def min-safe-int (int -1e6))
|
||||
;; Integer/MAX_VALUE
|
||||
(def max-safe-int 2147483647)
|
||||
;; Integer/MIN_VALUE
|
||||
(def min-safe-int -2147483648)
|
||||
|
||||
(def! ::safe-int
|
||||
{:type ::safe-int
|
||||
|
||||
@@ -29,8 +29,10 @@
|
||||
(def uuid-rx
|
||||
#"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$")
|
||||
|
||||
(def max-safe-int (int 1e6))
|
||||
(def min-safe-int (int -1e6))
|
||||
;; Integer/MAX_VALUE
|
||||
(def max-safe-int 2147483647)
|
||||
;; Integer/MIN_VALUE
|
||||
(def min-safe-int -2147483648)
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; DEFAULT SPECS
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
(sm/def! ::column-params
|
||||
[:map
|
||||
[:color ::grid-color]
|
||||
[:type [::sm/one-of #{:stretch :left :center :right}]]
|
||||
[:type {:optional true} [::sm/one-of #{:stretch :left :center :right}]]
|
||||
[:size {:optional true} [:maybe ::sm/safe-number]]
|
||||
[:margin {:optional true} [:maybe ::sm/safe-number]]
|
||||
[:item-length {:optional true} [:maybe ::sm/safe-number]]
|
||||
|
||||
@@ -72,10 +72,10 @@
|
||||
[:vector {:gen/max 5} ::gpt/point])
|
||||
|
||||
(sm/def! ::fill
|
||||
[:map {:title "Fill" :min 1}
|
||||
[:map {:title "Fill"}
|
||||
[:fill-color {:optional true} ::ctc/rgb-color]
|
||||
[:fill-opacity {:optional true} ::sm/safe-number]
|
||||
[:fill-color-gradient {:optional true} ::ctc/gradient]
|
||||
[:fill-color-gradient {:optional true} [:maybe ::ctc/gradient]]
|
||||
[:fill-color-ref-file {:optional true} [:maybe ::sm/uuid]]
|
||||
[:fill-color-ref-id {:optional true} [:maybe ::sm/uuid]]])
|
||||
|
||||
@@ -156,7 +156,7 @@
|
||||
[:map {:title "GroupAttrs"}
|
||||
[:type [:= :group]]
|
||||
[:id ::sm/uuid]
|
||||
[:shapes [:vector {:min 1 :gen/max 10 :gen/min 1} ::sm/uuid]]])
|
||||
[:shapes {:optional true} [:maybe [:vector {:gen/max 10 :gen/min 1} ::sm/uuid]]]])
|
||||
|
||||
(sm/def! ::frame-attrs
|
||||
[:map {:title "FrameAttrs"}
|
||||
@@ -172,18 +172,20 @@
|
||||
[:map {:title "BoolAttrs"}
|
||||
[:type [:= :bool]]
|
||||
[:id ::sm/uuid]
|
||||
[:shapes [:vector {:min 1 :gen/max 10 :gen/min 1} ::sm/uuid]]
|
||||
[:shapes {:optional true} [:maybe [:vector {:gen/max 10 :gen/min 1} ::sm/uuid]]]
|
||||
|
||||
;; FIXME: improve this schema
|
||||
[:bool-type :keyword]
|
||||
|
||||
;; FIXME: improve this schema
|
||||
[:bool-content
|
||||
[:vector {:gen/max 2}
|
||||
[:map
|
||||
[:command :keyword]
|
||||
[:relative {:optional true} :boolean]
|
||||
[:params [:map-of {:gen/max 5} :keyword ::sm/safe-number]]]]]])
|
||||
[:prev-pos {:optional true} ::gpt/point]
|
||||
[:params {:optional true}
|
||||
[:maybe
|
||||
[:map-of {:gen/max 5} :keyword ::sm/safe-number]]]]]]])
|
||||
|
||||
(sm/def! ::rect-attrs
|
||||
[:map {:title "RectAttrs"}
|
||||
@@ -208,14 +210,19 @@
|
||||
[:map
|
||||
[:width :int]
|
||||
[:height :int]
|
||||
[:mtype :string]
|
||||
[:mtype {:optional true} [:maybe :string]]
|
||||
[:id ::sm/uuid]]]])
|
||||
|
||||
(sm/def! ::path-attrs
|
||||
[:map {:title "PathAttrs"}
|
||||
[:type [:= :path]]
|
||||
[:id ::sm/uuid]
|
||||
[:x {:optional true} [:maybe ::sm/safe-number]]
|
||||
[:y {:optional true} [:maybe ::sm/safe-number]]
|
||||
[:width {:optional true} [:maybe ::sm/safe-number]]
|
||||
[:height {:optional true} [:maybe ::sm/safe-number]]
|
||||
[:content
|
||||
{:optional true}
|
||||
[:vector
|
||||
[:map
|
||||
[:command :keyword]
|
||||
|
||||
@@ -21,44 +21,46 @@
|
||||
[:type [:= "root"]]
|
||||
[:key {:optional true} :string]
|
||||
[:children
|
||||
[:vector {:min 1 :gen/max 2 :gen/min 1}
|
||||
[:map
|
||||
[:type [:= "paragraph-set"]]
|
||||
[:key {:optional true} :string]
|
||||
[:children
|
||||
[:vector {:min 1 :gen/max 2 :gen/min 1}
|
||||
[:map
|
||||
[:type [:= "paragraph"]]
|
||||
[:key {:optional true} :string]
|
||||
[:fills {:optional true}
|
||||
[:maybe
|
||||
[:vector {:gen/max 2} ::shape/fill]]]
|
||||
[:font-family {:optional true} :string]
|
||||
[:font-size {:optional true} :string]
|
||||
[:font-style {:optional true} :string]
|
||||
[:font-weight {:optional true} :string]
|
||||
[:direction {:optional true} :string]
|
||||
[:text-decoration {:optional true} :string]
|
||||
[:text-transform {:optional true} :string]
|
||||
[:typography-ref-id {:optional true} [:maybe ::sm/uuid]]
|
||||
[:typography-ref-file {:optional true} [:maybe ::sm/uuid]]
|
||||
[:children
|
||||
[:vector {:min 1 :gen/max 2 :gen/min 1}
|
||||
[:map
|
||||
[:text :string]
|
||||
[:key {:optional true} :string]
|
||||
[:fills {:optional true}
|
||||
[:maybe
|
||||
[:vector {:gen/max 2} ::shape/fill]]]
|
||||
[:font-family {:optional true} :string]
|
||||
[:font-size {:optional true} :string]
|
||||
[:font-style {:optional true} :string]
|
||||
[:font-weight {:optional true} :string]
|
||||
[:direction {:optional true} :string]
|
||||
[:text-decoration {:optional true} :string]
|
||||
[:text-transform {:optional true} :string]
|
||||
[:typography-ref-id {:optional true} [:maybe ::sm/uuid]]
|
||||
[:typography-ref-file {:optional true} [:maybe ::sm/uuid]]]]]]]]]]]])
|
||||
{:optional true}
|
||||
[:maybe
|
||||
[:vector {:min 1 :gen/max 2 :gen/min 1}
|
||||
[:map
|
||||
[:type [:= "paragraph-set"]]
|
||||
[:key {:optional true} :string]
|
||||
[:children
|
||||
[:vector {:min 1 :gen/max 2 :gen/min 1}
|
||||
[:map
|
||||
[:type [:= "paragraph"]]
|
||||
[:key {:optional true} :string]
|
||||
[:fills {:optional true}
|
||||
[:maybe
|
||||
[:vector {:gen/max 2} ::shape/fill]]]
|
||||
[:font-family {:optional true} :string]
|
||||
[:font-size {:optional true} :string]
|
||||
[:font-style {:optional true} :string]
|
||||
[:font-weight {:optional true} :string]
|
||||
[:direction {:optional true} :string]
|
||||
[:text-decoration {:optional true} :string]
|
||||
[:text-transform {:optional true} :string]
|
||||
[:typography-ref-id {:optional true} [:maybe ::sm/uuid]]
|
||||
[:typography-ref-file {:optional true} [:maybe ::sm/uuid]]
|
||||
[:children
|
||||
[:vector {:min 1 :gen/max 2 :gen/min 1}
|
||||
[:map
|
||||
[:text :string]
|
||||
[:key {:optional true} :string]
|
||||
[:fills {:optional true}
|
||||
[:maybe
|
||||
[:vector {:gen/max 2} ::shape/fill]]]
|
||||
[:font-family {:optional true} :string]
|
||||
[:font-size {:optional true} :string]
|
||||
[:font-style {:optional true} :string]
|
||||
[:font-weight {:optional true} :string]
|
||||
[:direction {:optional true} :string]
|
||||
[:text-decoration {:optional true} :string]
|
||||
[:text-transform {:optional true} :string]
|
||||
[:typography-ref-id {:optional true} [:maybe ::sm/uuid]]
|
||||
[:typography-ref-file {:optional true} [:maybe ::sm/uuid]]]]]]]]]]]]])
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -90,16 +90,24 @@
|
||||
|
||||
(delete-from-objects [objects]
|
||||
(if-let [target (get objects shape-id)]
|
||||
(let [parent-id (or (:parent-id target)
|
||||
(:frame-id target))
|
||||
children-ids (cph/get-children-ids objects shape-id)]
|
||||
(-> (reduce dissoc objects children-ids)
|
||||
(dissoc shape-id)
|
||||
(let [parent-id (or (:parent-id target)
|
||||
(:frame-id target))
|
||||
children-ids (cph/get-children-ids objects shape-id)]
|
||||
(-> (reduce dissoc objects (cons shape-id children-ids))
|
||||
(d/update-when parent-id delete-from-parent)))
|
||||
objects))]
|
||||
|
||||
(update container :objects delete-from-objects))))
|
||||
|
||||
(defn fix-shape-children
|
||||
"Checks and fix the children relations of the shape. If a children does not
|
||||
exists on the objects tree, it will be removed from shape."
|
||||
[{:keys [objects] :as container} {:keys [id] :as params}]
|
||||
(let [contains? (partial contains? objects)]
|
||||
(d/update-in-when container [:objects id :shapes]
|
||||
(fn [shapes]
|
||||
(into [] (filter contains?) shapes)))))
|
||||
|
||||
(defn get-frames
|
||||
"Retrieves all frame objects as vector"
|
||||
([objects] (get-frames objects nil))
|
||||
@@ -350,6 +358,7 @@
|
||||
(some? force-id) force-id
|
||||
keep-ids? (:id object)
|
||||
:else (uuid/next))]
|
||||
|
||||
(loop [child-ids (seq (:shapes object))
|
||||
new-direct-children []
|
||||
new-children []
|
||||
|
||||
@@ -104,7 +104,7 @@ WORKDIR /opt/penpot/exporter
|
||||
USER penpot:penpot
|
||||
|
||||
RUN set -ex; \
|
||||
yarn; \
|
||||
yarn run playwright install chromium;
|
||||
yarn --network-timeout 1000000; \
|
||||
yarn --network-timeout 1000000 run playwright install chromium;
|
||||
|
||||
CMD ["node", "app.js"]
|
||||
|
||||
@@ -49,7 +49,7 @@ function readLocales() {
|
||||
const langs = ["ar", "ca", "de", "el", "en", "eu", "it", "es",
|
||||
"fa", "fr", "he", "nb_NO", "pl", "pt_BR", "ro", "id",
|
||||
"ru", "tr", "zh_CN", "zh_Hant", "hr", "gl", "pt_PT",
|
||||
"cs", "fo", "ko", "lv",
|
||||
"cs", "fo", "ko", "lv", "nl",
|
||||
// this happens when file does not matches correct
|
||||
// iso code for the language.
|
||||
["ja_jp", "jpn_JP"],
|
||||
|
||||
@@ -1157,6 +1157,7 @@ input[type="range"]:focus::-ms-fill-upper {
|
||||
|
||||
.wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.icon {
|
||||
padding: $size-2;
|
||||
@@ -1173,6 +1174,9 @@ input[type="range"]:focus::-ms-fill-upper {
|
||||
padding: $size-2;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
|
||||
padding: 10px 15px;
|
||||
min-height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -176,6 +176,7 @@
|
||||
}
|
||||
|
||||
.flow-badge {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
|
||||
& .content {
|
||||
@@ -199,7 +200,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.selected .content {
|
||||
&.selected .content,
|
||||
&:hover .content {
|
||||
background-color: $color-primary;
|
||||
|
||||
& svg {
|
||||
|
||||
@@ -50,6 +50,23 @@
|
||||
(mf/mount (mf/element ui/app) (dom/get-element "app"))
|
||||
(mf/mount (mf/element modal) (dom/get-element "modal")))
|
||||
|
||||
(defn- initialize-profile
|
||||
"Event used mainly on application bootstrap; it fetches the profile
|
||||
and if and only if the fetched profile corresponds to an
|
||||
authenticated user; proceed to fetch teams."
|
||||
[stream]
|
||||
(rx/merge
|
||||
(rx/of (du/fetch-profile))
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::profile-fetched))
|
||||
(rx/take 1)
|
||||
(rx/map deref)
|
||||
(rx/mapcat (fn [profile]
|
||||
(if (du/is-authenticated? profile)
|
||||
(rx/of (du/fetch-teams))
|
||||
(rx/empty))))
|
||||
(rx/observe-on :async))))
|
||||
|
||||
(defn initialize
|
||||
[]
|
||||
(ptk/reify ::initialize
|
||||
@@ -61,14 +78,19 @@
|
||||
(watch [_ _ stream]
|
||||
(rx/merge
|
||||
(rx/of (ev/initialize)
|
||||
(feat/initialize)
|
||||
(du/initialize-profile))
|
||||
(feat/initialize))
|
||||
|
||||
(initialize-profile stream)
|
||||
|
||||
;; Once profile is fetched, initialize all penpot application
|
||||
;; routes
|
||||
(->> stream
|
||||
(rx/filter du/profile-fetched?)
|
||||
(rx/take 1)
|
||||
(rx/map #(rt/init-routes)))
|
||||
|
||||
;; Once profile fetched and the current user is authenticated,
|
||||
;; proceed to initialize the websockets connection.
|
||||
(->> stream
|
||||
(rx/filter du/profile-fetched?)
|
||||
(rx/map deref)
|
||||
|
||||
@@ -279,7 +279,10 @@
|
||||
(assoc-in (conj path :position) (:position comment-thread))
|
||||
(assoc-in (conj path :frame-id) (:frame-id comment-thread))))))
|
||||
(fetched [[users comments] state]
|
||||
(let [state (-> state
|
||||
(let [pages (-> (get-in state [:workspace-data :pages])
|
||||
set)
|
||||
comments (filter #(contains? pages (:page-id %)) comments)
|
||||
state (-> state
|
||||
(assoc :comment-threads (d/index-by :id comments))
|
||||
(update :current-file-comments-users merge (d/index-by :id users)))]
|
||||
(reduce set-comment-threds state comments)))]
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
(ns app.main.data.common
|
||||
"A general purpose events."
|
||||
(:require
|
||||
[app.config :as cf]
|
||||
[app.main.data.messages :as msg]
|
||||
[app.main.repo :as rp]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[beicon.core :as rx]
|
||||
[potok.core :as ptk]))
|
||||
|
||||
@@ -43,3 +46,33 @@
|
||||
(watch [_ _ _]
|
||||
(->> (rp/cmd! :delete-share-link {:id id})
|
||||
(rx/ignore)))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; NOTIFICATIONS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn force-reload!
|
||||
[]
|
||||
(.reload js/location))
|
||||
|
||||
(defn handle-notification
|
||||
[{:keys [message code level] :as params}]
|
||||
(ptk/reify ::show-notification
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(case code
|
||||
:upgrade-version
|
||||
(when (or (not= (:version params) (:full cf/version))
|
||||
(true? (:force params)))
|
||||
(rx/of (msg/dialog
|
||||
:content (tr "notifications.by-code.upgrade-version")
|
||||
:controls :inline-actions
|
||||
:type level
|
||||
:actions [{:label "Refresh" :callback force-reload!}]
|
||||
:tag :notification)))
|
||||
|
||||
(rx/of (msg/dialog
|
||||
:content message
|
||||
:controls :close
|
||||
:type level
|
||||
:tag :notification))))))
|
||||
|
||||
@@ -13,10 +13,12 @@
|
||||
[app.common.uri :as u]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.main.data.common :refer [handle-notification]]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.fonts :as df]
|
||||
[app.main.data.media :as di]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.data.websocket :as dws]
|
||||
[app.main.features :as features]
|
||||
[app.main.repo :as rp]
|
||||
[app.util.dom :as dom]
|
||||
@@ -57,11 +59,28 @@
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(rx/merge
|
||||
;;fetch teams must be first in case the team doesn't exist
|
||||
(ptk/watch (du/fetch-teams) state stream)
|
||||
(ptk/watch (df/load-team-fonts id) state stream)
|
||||
(ptk/watch (fetch-projects) state stream)
|
||||
(ptk/watch (fetch-team-members) state stream)
|
||||
(ptk/watch (du/fetch-teams) state stream)
|
||||
(ptk/watch (du/fetch-users {:team-id id}) state stream)))))
|
||||
(ptk/watch (du/fetch-users {:team-id id}) state stream)
|
||||
|
||||
(let [stoper (rx/filter (ptk/type? ::finalize) stream)
|
||||
profile-id (:profile-id state)]
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::dws/message))
|
||||
(rx/map deref)
|
||||
(rx/filter (fn [{:keys [subs-id type] :as msg}]
|
||||
(and (or (= subs-id uuid/zero)
|
||||
(= subs-id profile-id))
|
||||
(= :notification type))))
|
||||
(rx/map handle-notification)
|
||||
(rx/take-until stoper)))))))
|
||||
|
||||
(defn finalize
|
||||
[params]
|
||||
(ptk/data-event ::finalize params))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Data Fetching (context aware: current team)
|
||||
@@ -873,13 +892,13 @@
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [origin-project (get-in state [:dashboard-files (first ids) :project-id])
|
||||
update-project (fn [project delta]
|
||||
update-project (fn [project delta op]
|
||||
(-> project
|
||||
(update :count #(+ % (count ids)))
|
||||
(update :count #(op % (count ids)))
|
||||
(assoc :modified-at (dt/plus (dt/now) {:milliseconds delta}))))]
|
||||
(-> state
|
||||
(d/update-in-when [:dashboard-projects origin-project] update-project 0)
|
||||
(d/update-in-when [:dashboard-projects project-id] update-project 10))))
|
||||
(d/update-in-when [:dashboard-projects origin-project] update-project 0 -)
|
||||
(d/update-in-when [:dashboard-projects project-id] update-project 10 +))))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
|
||||
@@ -120,6 +120,17 @@
|
||||
:position :fixed
|
||||
:timeout timeout})))
|
||||
|
||||
(defn dialog
|
||||
[& {:keys [content controls actions position tag type]
|
||||
:or {controls :none position :floating type :info}}]
|
||||
(show (d/without-nils
|
||||
{:content content
|
||||
:type type
|
||||
:position position
|
||||
:controls controls
|
||||
:actions actions
|
||||
:tag tag})))
|
||||
|
||||
(defn info-dialog
|
||||
([content controls actions]
|
||||
(info-dialog content controls actions nil))
|
||||
|
||||
@@ -117,28 +117,6 @@
|
||||
(->> (rp/cmd! :get-profile)
|
||||
(rx/map profile-fetched)))))
|
||||
|
||||
;; --- EVENT: INITIALIZE PROFILE
|
||||
|
||||
(defn initialize-profile
|
||||
"Event used mainly on application bootstrap; it fetches the profile
|
||||
and if and only if the fetched profile corresponds to an
|
||||
authenticated user; proceed to fetch teams."
|
||||
[]
|
||||
(ptk/reify ::initialize-profile
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ stream]
|
||||
(rx/merge
|
||||
(rx/of (fetch-profile))
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::profile-fetched))
|
||||
(rx/take 1)
|
||||
(rx/map deref)
|
||||
(rx/mapcat (fn [profile]
|
||||
(if (= uuid/zero (:id profile))
|
||||
(rx/empty)
|
||||
(rx/of (fetch-teams)))))
|
||||
(rx/observe-on :async))))))
|
||||
|
||||
;; --- EVENT: login
|
||||
|
||||
(defn- logged-in
|
||||
@@ -147,7 +125,7 @@
|
||||
accepting invitation, or third party auth signup or singin."
|
||||
[profile]
|
||||
(letfn [(get-redirect-event []
|
||||
(let [team-id (:default-team-id profile)
|
||||
(let [team-id (get-current-team-id profile)
|
||||
redirect-url (:redirect-url @storage)]
|
||||
(if (some? redirect-url)
|
||||
(do
|
||||
@@ -164,7 +142,8 @@
|
||||
(when (is-authenticated? profile)
|
||||
(->> (rx/of (profile-fetched profile)
|
||||
(fetch-teams)
|
||||
(get-redirect-event))
|
||||
(get-redirect-event)
|
||||
(ws/initialize))
|
||||
(rx/observe-on :async)))))))
|
||||
|
||||
(declare login-from-register)
|
||||
@@ -268,7 +247,9 @@
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ _ _]
|
||||
(reset! storage {})
|
||||
;; We prefer to keek some stuff in the storage like the current-team-id
|
||||
(swap! storage dissoc :redirect-url)
|
||||
(swap! storage dissoc :profile)
|
||||
(i18n/reset-locale)))))
|
||||
|
||||
(defn logout
|
||||
|
||||
@@ -155,6 +155,8 @@
|
||||
(rx/of (df/fonts-fetched fonts)
|
||||
(bundle-fetched (merge bundle params))))))))))
|
||||
|
||||
(declare go-to-frame)
|
||||
(declare go-to-frame-by-index)
|
||||
(declare go-to-frame-auto)
|
||||
|
||||
(defn bundle-fetched
|
||||
@@ -182,16 +184,20 @@
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [route (:route state)
|
||||
qparams (:query-params route)
|
||||
index (:index qparams)]
|
||||
(let [route (:route state)
|
||||
qparams (:query-params route)
|
||||
index (:index qparams)
|
||||
frame-id (:frame-id qparams)]
|
||||
(rx/merge
|
||||
(rx/of (case (:zoom qparams)
|
||||
"fit" zoom-to-fit
|
||||
"fill" zoom-to-fill
|
||||
nil))
|
||||
(when (nil? index)
|
||||
(rx/of (go-to-frame-auto)))))))))
|
||||
(rx/of
|
||||
(cond
|
||||
(some? frame-id) (go-to-frame (uuid frame-id))
|
||||
(some? index) (go-to-frame-by-index index)
|
||||
:else (go-to-frame-auto)))))))))
|
||||
|
||||
(defn fetch-comment-threads
|
||||
[{:keys [file-id page-id share-id] :as params}]
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
[app.main.data.workspace.drawing.common :as dwdc]
|
||||
[app.main.data.workspace.edition :as dwe]
|
||||
[app.main.data.workspace.fix-bool-contents :as fbc]
|
||||
[app.main.data.workspace.fix-broken-shape-links :as fbs]
|
||||
[app.main.data.workspace.fix-deleted-fonts :as fdf]
|
||||
[app.main.data.workspace.groups :as dwg]
|
||||
[app.main.data.workspace.guides :as dwgu]
|
||||
@@ -130,8 +131,10 @@
|
||||
has-graphics? (-> file :media seq)
|
||||
components-v2 (features/active-feature? state :components-v2)]
|
||||
(rx/merge
|
||||
(rx/of (fbc/fix-bool-contents))
|
||||
(rx/of (fdf/fix-deleted-fonts))
|
||||
(rx/of (fbc/fix-bool-contents)
|
||||
(fdf/fix-deleted-fonts)
|
||||
(fbs/fix-broken-shapes))
|
||||
|
||||
(if (and has-graphics? components-v2)
|
||||
(rx/of (remove-graphics (:id file) (:name file)))
|
||||
(rx/empty)))))))
|
||||
@@ -985,6 +988,23 @@
|
||||
(rx/of (dwe/start-edition-mode id)
|
||||
(dwdp/start-path-edit id)))))))))
|
||||
|
||||
(defn select-parent-layer
|
||||
[]
|
||||
(ptk/reify ::select-parent-layer
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [selected (wsh/lookup-selected state)
|
||||
objects (wsh/lookup-page-objects state)
|
||||
shapes-to-select
|
||||
(->> selected
|
||||
(reduce
|
||||
(fn [result shape-id]
|
||||
(let [parent-id (dm/get-in objects [shape-id :parent-id])]
|
||||
(if (and (some? parent-id) (not= parent-id uuid/zero))
|
||||
(conj result parent-id)
|
||||
(conj result shape-id))))
|
||||
(d/ordered-set)))]
|
||||
(rx/of (dws/select-shapes shapes-to-select))))))
|
||||
|
||||
;; --- Change Page Order (D&D Ordering)
|
||||
|
||||
@@ -1099,7 +1119,7 @@
|
||||
|
||||
(defn toggle-proportion-lock
|
||||
[]
|
||||
(ptk/reify ::toggle-propotion-lock
|
||||
(ptk/reify ::toggle-proportion-lock
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [page-id (:current-page-id state)
|
||||
@@ -1356,7 +1376,7 @@
|
||||
|
||||
(defn go-to-viewer
|
||||
([] (go-to-viewer {}))
|
||||
([{:keys [file-id page-id section]}]
|
||||
([{:keys [file-id page-id section frame-id]}]
|
||||
(ptk/reify ::go-to-viewer
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
@@ -1364,7 +1384,9 @@
|
||||
pparams {:file-id (or file-id current-file-id)}
|
||||
qparams (cond-> {:page-id (or page-id current-page-id)}
|
||||
(some? section)
|
||||
(assoc :section section))]
|
||||
(assoc :section section)
|
||||
(some? frame-id)
|
||||
(assoc :frame-id frame-id))]
|
||||
(rx/of ::dwp/force-persist
|
||||
(rt/nav-new-window* {:rname :viewer
|
||||
:path-params pparams
|
||||
@@ -1817,9 +1839,14 @@
|
||||
detach? (or (foreign-instance? shape paste-objects state)
|
||||
(and (ctk/in-component-copy-not-root? shape)
|
||||
(not= (:id component-shape)
|
||||
(:id component-shape-parent))))]
|
||||
(:id component-shape-parent))))
|
||||
assign-shapes? (and (or (cph/group-shape? shape)
|
||||
(cph/bool-shape? shape))
|
||||
(nil? (:shapes shape)))]
|
||||
(-> shape
|
||||
(assoc :frame-id frame-id :parent-id parent-id)
|
||||
(cond-> assign-shapes?
|
||||
(assoc :shapes []))
|
||||
(cond-> detach?
|
||||
(->
|
||||
;; this is used later, if the paste needs to create a new component from the detached shape
|
||||
|
||||
28
frontend/src/app/main/data/workspace/assets.cljs
Normal file
28
frontend/src/app/main/data/workspace/assets.cljs
Normal file
@@ -0,0 +1,28 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.main.data.workspace.assets
|
||||
"Workspace assets management events and helpers."
|
||||
(:require
|
||||
[app.util.storage :refer [storage]]))
|
||||
|
||||
(defn get-current-assets-ordering
|
||||
[]
|
||||
(let [ordering (::ordering @storage)]
|
||||
(or ordering :asc)))
|
||||
|
||||
(defn set-current-assets-ordering!
|
||||
[ordering]
|
||||
(swap! storage assoc ::ordering ordering))
|
||||
|
||||
(defn get-current-assets-list-style
|
||||
[]
|
||||
(let [list-style (::list-style @storage)]
|
||||
(or list-style :thumbs)))
|
||||
|
||||
(defn set-current-assets-list-style!
|
||||
[list-style]
|
||||
(swap! storage assoc ::list-style list-style))
|
||||
@@ -8,6 +8,7 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as log]
|
||||
[app.common.pages :as cp]
|
||||
[app.common.pages.changes :as cpc]
|
||||
@@ -205,6 +206,7 @@
|
||||
path (if (= file-id current-file-id)
|
||||
[:workspace-data]
|
||||
[:workspace-libraries file-id :data])]
|
||||
|
||||
(try
|
||||
(dm/assert!
|
||||
"expect valid vector of changes"
|
||||
@@ -217,7 +219,11 @@
|
||||
(ctst/update-object-indices page-id))))
|
||||
|
||||
(catch :default err
|
||||
(log/error :js/error err)
|
||||
(when-let [data (ex-data err)]
|
||||
(js/console.log (ex/explain data)))
|
||||
|
||||
(when (ex/error? err)
|
||||
(js/console.log (.-stack ^js err)))
|
||||
(vreset! error err)
|
||||
state))))
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
[app.main.data.workspace.texts :as dwt]
|
||||
[app.main.data.workspace.undo :as dwu]
|
||||
[app.util.color :as uc]
|
||||
[app.util.storage :refer [storage]]
|
||||
[beicon.core :as rx]
|
||||
[potok.core :as ptk]))
|
||||
|
||||
@@ -353,9 +354,12 @@
|
||||
(-> state
|
||||
(assoc-in [:workspace-global :picking-color?] true)
|
||||
(assoc ::md/modal {:id (random-uuid)
|
||||
:data {:color colors/black :opacity 1}
|
||||
:type :colorpicker
|
||||
:props {:on-change handle-change-color}
|
||||
:props {:data {:color colors/black
|
||||
:opacity 1}
|
||||
:disable-opacity false
|
||||
:disable-gradient false
|
||||
:on-change handle-change-color}
|
||||
:allow-click-outside true})))))))
|
||||
|
||||
(defn color-att->text
|
||||
@@ -644,3 +648,12 @@
|
||||
:position :right})
|
||||
(ptk/event ::ev/event {::ev/name "add-asset-to-library"
|
||||
:asset-type "color"}))))))
|
||||
|
||||
(defn get-active-color-tab
|
||||
[]
|
||||
(let [tab (::tab @storage)]
|
||||
(or tab :ramp)))
|
||||
|
||||
(defn set-active-color-tab!
|
||||
[tab]
|
||||
(swap! storage assoc ::tab tab))
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.main.data.workspace.fix-broken-shape-links
|
||||
(:require
|
||||
[app.main.data.workspace.changes :as dch]
|
||||
[beicon.core :as rx]
|
||||
[potok.core :as ptk]))
|
||||
|
||||
(defn- generate-changes
|
||||
[attr {:keys [objects id] :as container}]
|
||||
(let [base {:type :fix-obj attr id}
|
||||
contains? (partial contains? objects)
|
||||
xform (comp
|
||||
;; FIXME: Ensure all obj have id field (this is needed
|
||||
;; because some bug adds an ephimeral shape with id ZERO,
|
||||
;; with a single attr `:shapes` having a vector of ids
|
||||
;; pointing to not existing shapes). That happens on
|
||||
;; components. THIS IS A WORKAOURD
|
||||
(map (fn [[id obj]]
|
||||
(if (some? (:id obj))
|
||||
obj
|
||||
(assoc obj :id id))))
|
||||
|
||||
;; Remove all valid shapes
|
||||
(remove (fn [obj]
|
||||
(every? contains? (:shapes obj))))
|
||||
|
||||
(map (fn [obj]
|
||||
(assoc base :id (:id obj)))))]
|
||||
|
||||
(sequence xform objects)))
|
||||
|
||||
(defn fix-broken-shapes
|
||||
[]
|
||||
(ptk/reify ::fix-broken-shape-links
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [data (get state :workspace-data)
|
||||
changes (concat
|
||||
(mapcat (partial generate-changes :page-id)
|
||||
(vals (:pages-index data)))
|
||||
(mapcat (partial generate-changes :component-id)
|
||||
(vals (:components data))))]
|
||||
|
||||
(if (seq changes)
|
||||
(rx/of (dch/commit-changes
|
||||
{:origin it
|
||||
:redo-changes (vec changes)
|
||||
:undo-changes []
|
||||
:save-undo? false}))
|
||||
(rx/empty))))))
|
||||
@@ -73,16 +73,20 @@
|
||||
(rx/map #(svg/add-svg-shapes (assoc svg-data :image-data %) position))))))
|
||||
|
||||
(defn- process-uris
|
||||
[{:keys [file-id local? name uris mtype on-image on-svg]}]
|
||||
[{:keys [file-id local? name uris mtype on-image on-svg] }]
|
||||
(letfn [(svg-url? [url]
|
||||
(or (and mtype (= mtype "image/svg+xml"))
|
||||
(str/ends-with? url ".svg")))
|
||||
|
||||
(prepare [uri]
|
||||
{:file-id file-id
|
||||
:is-local local?
|
||||
:name (or name (svg/extract-name uri))
|
||||
:url uri})
|
||||
(upload [uri]
|
||||
(->> (http/send! {:method :get :uri uri :mode :no-cors :response-type :blob})
|
||||
(rx/map :body)
|
||||
(rx/map (fn [content]
|
||||
{:file-id file-id
|
||||
:name (or name (svg/extract-name uri))
|
||||
:is-local local?
|
||||
:content content}))
|
||||
(rx/mapcat #(rp/cmd! :upload-file-media-object %))))
|
||||
|
||||
(fetch-svg [name uri]
|
||||
(->> (http/send! {:method :get :uri uri :mode :no-cors})
|
||||
@@ -93,8 +97,7 @@
|
||||
(rx/merge
|
||||
(->> (rx/from uris)
|
||||
(rx/filter (comp not svg-url?))
|
||||
(rx/map prepare)
|
||||
(rx/mapcat #(rp/cmd! :create-file-media-object-from-url %))
|
||||
(rx/mapcat upload)
|
||||
(rx/do on-image))
|
||||
|
||||
(->> (rx/from uris)
|
||||
@@ -142,7 +145,7 @@
|
||||
[:local? :boolean]
|
||||
[:name {:optional true} :string]
|
||||
[:data {:optional true} :any] ; FIXME
|
||||
[:uris {:optional true} [:vector :string]]
|
||||
[:uris {:optional true} [:sequential :string]]
|
||||
[:mtype {:optional true} :string]])
|
||||
|
||||
(defn- process-media-objects
|
||||
@@ -213,8 +216,6 @@
|
||||
:on-image #(st/emit! (dwl/add-media %)))]
|
||||
(process-media-objects params)))
|
||||
|
||||
;; TODO: it is really need handle SVG here, looks like it already
|
||||
;; handled separately
|
||||
(defn upload-media-workspace
|
||||
[{:keys [position file-id] :as params}]
|
||||
(let [params (assoc params
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.pages.changes :as cpc]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.common :refer [handle-notification]]
|
||||
[app.main.data.websocket :as dws]
|
||||
[app.main.data.workspace.changes :as dch]
|
||||
[app.main.data.workspace.libraries :as dwl]
|
||||
@@ -57,8 +59,9 @@
|
||||
(rx/filter (ptk/type? ::dws/message))
|
||||
(rx/map deref)
|
||||
(rx/filter (fn [{:keys [subs-id] :as msg}]
|
||||
(or (= subs-id team-id)
|
||||
(or (= subs-id uuid/zero)
|
||||
(= subs-id profile-id)
|
||||
(= subs-id team-id)
|
||||
(= subs-id file-id))))
|
||||
(rx/map process-message))
|
||||
|
||||
@@ -96,6 +99,7 @@
|
||||
:pointer-update (handle-pointer-update msg)
|
||||
:file-change (handle-file-change msg)
|
||||
:library-change (handle-library-change msg)
|
||||
:notification (handle-notification msg)
|
||||
nil))
|
||||
|
||||
(defn- handle-pointer-send
|
||||
|
||||
@@ -122,7 +122,9 @@
|
||||
(ptk/reify ::select-shape
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:workspace-local :selected] d/toggle-selection id toggle?))
|
||||
(-> state
|
||||
(update-in [:workspace-local :selected] d/toggle-selection id toggle?)
|
||||
(assoc-in [:workspace-local :last-selected] id)))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
@@ -185,7 +187,9 @@
|
||||
(ptk/reify ::deselect-shape
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:workspace-local :selected] disj id))))
|
||||
(-> state
|
||||
(update-in [:workspace-local :selected] disj id)
|
||||
(update :workspace-local dissoc :last-selected)))))
|
||||
|
||||
(defn shift-select-shapes
|
||||
([id]
|
||||
@@ -196,12 +200,14 @@
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [objects (or objects (wsh/lookup-page-objects state))
|
||||
append-to-selection (cph/expand-region-selection objects (into #{} [(get-in state [:workspace-local :last-selected]) id]))
|
||||
selection (-> state
|
||||
wsh/lookup-selected
|
||||
(conj id))]
|
||||
(-> state
|
||||
(assoc-in [:workspace-local :selected]
|
||||
(cph/expand-region-selection objects selection))))))))
|
||||
(set/union selection append-to-selection))
|
||||
(update :workspace-local assoc :last-selected id)))))))
|
||||
|
||||
(defn select-shapes
|
||||
[ids]
|
||||
@@ -409,6 +415,8 @@
|
||||
|
||||
:else
|
||||
(let [frame? (cph/frame-shape? obj)
|
||||
group? (cph/group-shape? obj)
|
||||
bool? (cph/bool-shape? obj)
|
||||
new-id (ids-map (:id obj))
|
||||
parent-id (or parent-id frame-id)
|
||||
name (:name obj)
|
||||
@@ -427,9 +435,15 @@
|
||||
:name name
|
||||
:parent-id parent-id
|
||||
:frame-id frame-id)
|
||||
|
||||
(dissoc :shapes
|
||||
:main-instance?
|
||||
:use-for-thumbnail?)
|
||||
|
||||
(cond->
|
||||
(or group? bool?)
|
||||
(assoc :shapes []))
|
||||
|
||||
(gsh/move delta)
|
||||
(d/update-when :interactions #(ctsi/remap-interactions % ids-map objects)))
|
||||
|
||||
|
||||
@@ -57,6 +57,11 @@
|
||||
cts/default-frame-attrs
|
||||
cts/default-shape-attrs)
|
||||
|
||||
default-attrs (if (or (= :group (:type attrs))
|
||||
(= :bool (:type attrs)))
|
||||
(assoc default-attrs :shapes [])
|
||||
default-attrs)
|
||||
|
||||
selected-non-frames
|
||||
(into #{} (comp (map (d/getf objects))
|
||||
(remove cph/frame-shape?))
|
||||
@@ -288,7 +293,7 @@
|
||||
(let [all-ids (into empty-parents ids)
|
||||
contains? (partial contains? all-ids)
|
||||
xform (comp (map lookup)
|
||||
(filter cph/group-shape?)
|
||||
(filter #(or (cph/group-shape? %) (cph/bool-shape? %)))
|
||||
(remove #(->> (:shapes %) (remove contains?) seq))
|
||||
(map :id))
|
||||
parents (into #{} xform all-parents)]
|
||||
|
||||
@@ -279,8 +279,8 @@
|
||||
:subsections [:tools]
|
||||
:fn #(emit-when-no-readonly (dw/toggle-lock-selected))}
|
||||
|
||||
:toggle-lock-size {:tooltip (ds/meta (ds/alt "L"))
|
||||
:command (ds/c-mod "alt+l")
|
||||
:toggle-lock-size {:tooltip (ds/shift "L")
|
||||
:command "shift+l"
|
||||
:subsections [:tools]
|
||||
:fn #(emit-when-no-readonly (dw/toggle-proportion-lock))}
|
||||
|
||||
@@ -509,6 +509,10 @@
|
||||
:subsections [:navigation-workspace]
|
||||
:fn #(st/emit! (dw/select-next-shape))}
|
||||
|
||||
:select-parent-layer {:tooltip (ds/shift ds/enter)
|
||||
:command "shift+enter"
|
||||
:subsections [:navigation-workspace]
|
||||
:fn #(emit-when-no-readonly (dw/select-parent-layer))}
|
||||
;; SHAPE
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
(:require
|
||||
[app.common.colors :as clr]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
@@ -123,56 +124,65 @@
|
||||
(assoc-in [:fills 0 :fill-opacity] (-> (get-in shape [:svg-attrs :style :fill-opacity])
|
||||
(d/parse-double 1)))))))
|
||||
|
||||
(defn setup-stroke [shape]
|
||||
(let [stroke-linecap (-> (or (get-in shape [:svg-attrs :stroke-linecap])
|
||||
(get-in shape [:svg-attrs :style :stroke-linecap]))
|
||||
((d/nilf str/trim))
|
||||
((d/nilf keyword)))
|
||||
color-attr (str/trim (get-in shape [:svg-attrs :stroke]))
|
||||
color-attr (if (= color-attr "currentColor") clr/black color-attr)
|
||||
color-style (str/trim (get-in shape [:svg-attrs :style :stroke]))
|
||||
color-style (if (= color-style "currentColor") clr/black color-style)
|
||||
|
||||
shape
|
||||
(cond-> shape
|
||||
;; Color present as attribute
|
||||
(uc/color? color-attr)
|
||||
(-> (update :svg-attrs dissoc :stroke)
|
||||
(assoc-in [:strokes 0 :stroke-color] (uc/parse-color color-attr)))
|
||||
(defn- setup-stroke
|
||||
[shape]
|
||||
(let [attrs (get shape :svg-attrs)
|
||||
style (get attrs :style)
|
||||
|
||||
;; Color present as style
|
||||
(uc/color? color-style)
|
||||
(-> (update-in [:svg-attrs :style] dissoc :stroke)
|
||||
(assoc-in [:strokes 0 :stroke-color] (uc/parse-color color-style)))
|
||||
stroke (or (str/trim (:stroke attrs))
|
||||
(str/trim (:stroke style)))
|
||||
|
||||
(get-in shape [:svg-attrs :stroke-opacity])
|
||||
(-> (update :svg-attrs dissoc :stroke-opacity)
|
||||
(assoc-in [:strokes 0 :stroke-opacity] (-> (get-in shape [:svg-attrs :stroke-opacity])
|
||||
(d/parse-double 1))))
|
||||
color (cond
|
||||
(= stroke "currentColor") clr/black
|
||||
(= stroke "none") nil
|
||||
:else (uc/parse-color stroke))
|
||||
|
||||
(get-in shape [:svg-attrs :style :stroke-opacity])
|
||||
(-> (update-in [:svg-attrs :style] dissoc :stroke-opacity)
|
||||
(assoc-in [:strokes 0 :stroke-opacity] (-> (get-in shape [:svg-attrs :style :stroke-opacity])
|
||||
(d/parse-double 1))))
|
||||
opacity (when (some? color)
|
||||
(d/parse-double
|
||||
(or (:stroke-opacity attrs)
|
||||
(:stroke-opacity style))
|
||||
1))
|
||||
|
||||
(get-in shape [:svg-attrs :stroke-width])
|
||||
(-> (update :svg-attrs dissoc :stroke-width)
|
||||
(assoc-in [:strokes 0 :stroke-width] (-> (get-in shape [:svg-attrs :stroke-width])
|
||||
(d/parse-double))))
|
||||
width (when (some? color)
|
||||
(d/parse-double
|
||||
(or (:stroke-width attrs)
|
||||
(:stroke-width style))
|
||||
1))
|
||||
|
||||
(get-in shape [:svg-attrs :style :stroke-width])
|
||||
(-> (update-in [:svg-attrs :style] dissoc :stroke-width)
|
||||
(assoc-in [:strokes 0 :stroke-width] (-> (get-in shape [:svg-attrs :style :stroke-width])
|
||||
(d/parse-double))))
|
||||
linecap (or (get attrs :stroke-linecap)
|
||||
(get style :stroke-linecap))
|
||||
linecap (some-> linecap str/trim keyword)
|
||||
|
||||
(and stroke-linecap (= (:type shape) :path))
|
||||
(-> (update-in [:svg-attrs :style] dissoc :stroke-linecap)
|
||||
(cond-> (#{:round :square} stroke-linecap)
|
||||
(assoc :stroke-cap-start stroke-linecap
|
||||
:stroke-cap-end stroke-linecap))))]
|
||||
attrs (-> attrs
|
||||
(dissoc :stroke)
|
||||
(dissoc :stroke-width)
|
||||
(dissoc :stroke-opacity)
|
||||
(update :style (fn [style]
|
||||
(-> style
|
||||
(dissoc :stroke)
|
||||
(dissoc :stroke-linecap)
|
||||
(dissoc :stroke-width)
|
||||
(dissoc :stroke-opacity)))))]
|
||||
|
||||
(cond-> shape
|
||||
(d/any-key? (get-in shape [:strokes 0]) :stroke-color :stroke-opacity :stroke-width :stroke-cap-start :stroke-cap-end)
|
||||
(cond-> (assoc shape :svg-attrs attrs)
|
||||
(some? color)
|
||||
(assoc-in [:strokes 0 :stroke-color] color)
|
||||
|
||||
(and (some? color) (some? opacity))
|
||||
(assoc-in [:strokes 0 :stroke-opacity] opacity)
|
||||
|
||||
(and (some? color) (some? width))
|
||||
(assoc-in [:strokes 0 :stroke-width] width)
|
||||
|
||||
(and (some? linecap) (= (:type shape) :path)
|
||||
(or (= linecap :round) (= linecap :square)))
|
||||
(assoc :stroke-cap-start linecap
|
||||
:stroke-cap-end linecap)
|
||||
|
||||
(d/any-key? (dm/get-in shape [:strokes 0])
|
||||
:stroke-color :stroke-opacity :stroke-width
|
||||
:stroke-cap-start :stroke-cap-end)
|
||||
(assoc-in [:strokes 0 :stroke-style] :svg))))
|
||||
|
||||
(defn setup-opacity [shape]
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
|
||||
:viewer
|
||||
(let [{:keys [query-params path-params]} route
|
||||
{:keys [index share-id section page-id interactions-mode] :or {section :interactions interactions-mode :show-on-click}} query-params
|
||||
{:keys [index share-id section page-id interactions-mode frame-id] :or {section :interactions interactions-mode :show-on-click}} query-params
|
||||
{:keys [file-id]} path-params]
|
||||
(if (:token query-params)
|
||||
[:& viewer/breaking-change-notice]
|
||||
@@ -119,7 +119,8 @@
|
||||
:interactions-show? (case (keyword interactions-mode)
|
||||
:hide false
|
||||
:show true
|
||||
:show-on-click false)}]))
|
||||
:show-on-click false)
|
||||
:frame-id frame-id}]))
|
||||
|
||||
:workspace
|
||||
(let [project-id (some-> params :path :project-id uuid)
|
||||
|
||||
@@ -153,19 +153,21 @@
|
||||
|
||||
(hooks/use-shortcuts ::dashboard sc/shortcuts)
|
||||
|
||||
(mf/with-effect [team-id]
|
||||
(st/emit! (dd/initialize {:id team-id})))
|
||||
(mf/with-effect [profile team-id]
|
||||
(when profile
|
||||
;; When doing logout we must avoid reinitializing the dashboard
|
||||
(st/emit! (dd/initialize {:id team-id})))
|
||||
(fn []
|
||||
(dd/finalize {:id team-id})))
|
||||
|
||||
(mf/use-effect
|
||||
(fn []
|
||||
(let [events [(events/listen goog/global "keydown"
|
||||
(fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (dd/open-selected-file)))))]]
|
||||
(fn []
|
||||
(doseq [key events]
|
||||
(events/unlistenByKey key))))))
|
||||
(mf/with-effect []
|
||||
(let [key (events/listen goog/global "keydown"
|
||||
(fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (dd/open-selected-file)))))]
|
||||
(fn []
|
||||
(events/unlistenByKey key))))
|
||||
|
||||
[:& (mf/provider ctx/current-team-id) {:value team-id}
|
||||
[:& (mf/provider ctx/current-project-id) {:value project-id}
|
||||
|
||||
@@ -230,8 +230,8 @@
|
||||
|
||||
]
|
||||
|
||||
(mf/with-effect [collapsed]
|
||||
(when-not collapsed
|
||||
(mf/with-effect [profile collapsed]
|
||||
(when (and profile (not collapsed))
|
||||
(st/emit! (dd/fetch-builtin-templates))))
|
||||
|
||||
[:div.dashboard-templates-section
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
|
||||
(mf/set-ref-val! prev-val-ref node))))]
|
||||
|
||||
(mf/with-effect []
|
||||
(mf/with-layout-effect []
|
||||
;; On dismount we need to disconnect the current observer
|
||||
(fn []
|
||||
(when-let [observer (mf/ref-val observer-ref)]
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
(obj/merge! attrs (clj->js fill-attrs)))))
|
||||
|
||||
(defn add-stroke [attrs stroke-data render-id index]
|
||||
(let [stroke-style (:stroke-style stroke-data :none)
|
||||
(let [stroke-style (:stroke-style stroke-data :solid)
|
||||
stroke-color-gradient-id (str "stroke-color-gradient_" render-id "_" index)
|
||||
stroke-width (:stroke-width stroke-data 1)]
|
||||
(if (not= stroke-style :none)
|
||||
|
||||
@@ -38,11 +38,10 @@
|
||||
|
||||
(defn- find-relative-to-base-frame
|
||||
[shape objects overlays-ids base-frame]
|
||||
(if (or (empty? overlays-ids) (nil? shape) (cph/root? shape))
|
||||
base-frame
|
||||
(if (contains? overlays-ids (:id shape))
|
||||
shape
|
||||
(find-relative-to-base-frame (cph/get-parent objects (:id shape)) objects overlays-ids base-frame))))
|
||||
(cond
|
||||
(cph/frame-shape? shape) shape
|
||||
(or (empty? overlays-ids) (nil? shape) (cph/root? shape)) base-frame
|
||||
:else (find-relative-to-base-frame (cph/get-parent objects (:id shape)) objects overlays-ids base-frame)))
|
||||
|
||||
(defn- activate-interaction
|
||||
[interaction shape base-frame frame-offset objects overlays]
|
||||
|
||||
@@ -8,8 +8,10 @@
|
||||
(:require-macros [app.main.style :refer [css]])
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.main.data.messages :as msg]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.colors :as dc]
|
||||
[app.main.data.workspace.persistence :as dwp]
|
||||
[app.main.features :as features]
|
||||
[app.main.refs :as refs]
|
||||
@@ -185,6 +187,9 @@
|
||||
(st/emit! (dw/initialize-file project-id file-id))
|
||||
(fn []
|
||||
(st/emit! ::dwp/force-persist
|
||||
(dc/stop-picker)
|
||||
(modal/hide)
|
||||
msg/hide
|
||||
(dw/finalize-file project-id file-id))))
|
||||
|
||||
[:& (mf/provider ctx/current-file-id) {:value file-id}
|
||||
|
||||
@@ -56,12 +56,17 @@
|
||||
|
||||
current-color (:current-color state)
|
||||
|
||||
active-tab (mf/use-state :ramp #_:harmony #_:hsva)
|
||||
set-ramp-tab! (mf/use-fn #(reset! active-tab :ramp))
|
||||
set-harmony-tab! (mf/use-fn #(reset! active-tab :harmony))
|
||||
set-hsva-tab! (mf/use-fn #(reset! active-tab :hsva))
|
||||
|
||||
active-tab (mf/use-state (dc/get-active-color-tab))
|
||||
drag? (mf/use-state false)
|
||||
|
||||
set-tab!
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(let [tab (-> (dom/get-current-target event)
|
||||
(dom/get-data "tab")
|
||||
(keyword))]
|
||||
(reset! active-tab tab)
|
||||
(dc/set-active-color-tab! tab))))
|
||||
|
||||
handle-change-color
|
||||
(mf/use-fn
|
||||
@@ -81,9 +86,9 @@
|
||||
(fn []
|
||||
(if picking-color?
|
||||
(do (modal/disallow-click-outside!)
|
||||
(st/emit! (dc/stop-picker)))
|
||||
(st/emit! (dc/stop-picker)))
|
||||
(do (modal/allow-click-outside!)
|
||||
(st/emit! (dc/start-picker))))))
|
||||
(st/emit! (dc/start-picker))))))
|
||||
|
||||
handle-change-stop
|
||||
(mf/use-fn
|
||||
@@ -225,15 +230,18 @@
|
||||
[:div.colorpicker-tab.tooltip.tooltip-bottom.tooltip-expand
|
||||
{:class (when (= @active-tab :ramp) "active")
|
||||
:alt (tr "workspace.libraries.colors.rgba")
|
||||
:on-click set-ramp-tab!} i/picker-ramp]
|
||||
:on-click set-tab!
|
||||
:data-tab "ramp"} i/picker-ramp]
|
||||
[:div.colorpicker-tab.tooltip.tooltip-bottom.tooltip-expand
|
||||
{:class (when (= @active-tab :harmony) "active")
|
||||
:alt (tr "workspace.libraries.colors.rgb-complementary")
|
||||
:on-click set-harmony-tab!} i/picker-harmony]
|
||||
:on-click set-tab!
|
||||
:data-tab "harmony"} i/picker-harmony]
|
||||
[:div.colorpicker-tab.tooltip.tooltip-bottom.tooltip-expand
|
||||
{:class (when (= @active-tab :hsva) "active")
|
||||
:alt (tr "workspace.libraries.colors.hsv")
|
||||
:on-click set-hsva-tab!} i/picker-hsv]]
|
||||
:on-click set-tab!
|
||||
:data-tab "hsva"} i/picker-hsv]]
|
||||
|
||||
(if picking-color?
|
||||
[:div.picker-detail-wrapper
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
[app.main.data.exports :as de]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.colors :as dc]
|
||||
[app.main.data.workspace.common :as dwc]
|
||||
[app.main.data.workspace.libraries :as dwl]
|
||||
[app.main.data.workspace.shortcuts :as sc]
|
||||
@@ -162,9 +161,7 @@
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "show-release-notes" :version version}))
|
||||
(if (and (kbd/alt? event) (kbd/mod? event))
|
||||
(st/emit! (modal/show {:type :onboarding}))
|
||||
(st/emit! (modal/show {:type :release-notes :version version}))))))
|
||||
|
||||
]
|
||||
(st/emit! (modal/show {:type :release-notes :version version}))))))]
|
||||
|
||||
[:& dropdown {:show true :on-close on-close}
|
||||
[:ul.sub-menu.help-info
|
||||
@@ -582,16 +579,10 @@
|
||||
(dom/prevent-default event)
|
||||
(reset! editing* true)))
|
||||
|
||||
close-modals
|
||||
(mf/use-fn
|
||||
#(st/emit! (dc/stop-picker)
|
||||
(modal/hide)))
|
||||
|
||||
go-back
|
||||
(mf/use-fn
|
||||
(mf/deps project)
|
||||
(fn []
|
||||
(close-modals)
|
||||
(st/emit! (dw/go-to-dashboard project))))
|
||||
|
||||
nav-to-viewer
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
[app.util.object :as obj]
|
||||
[app.util.text-editor :as ted]
|
||||
[app.util.text-svg-position :as tsp]
|
||||
[app.util.timers :as ts]
|
||||
[promesa.core :as p]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
@@ -79,25 +80,29 @@
|
||||
|
||||
(defn- update-text-modifier
|
||||
[{:keys [grow-type id] :as shape} node]
|
||||
(->> (tsp/calc-position-data id)
|
||||
(p/fmap (fn [position-data]
|
||||
(let [props {:position-data position-data}]
|
||||
(if (contains? #{:auto-height :auto-width} grow-type)
|
||||
(let [{:keys [width height]} (-> (dom/query node ".paragraph-set") (dom/get-client-size))
|
||||
width (mth/ceil width)
|
||||
height (mth/ceil height)]
|
||||
(if (and (not (mth/almost-zero? width)) (not (mth/almost-zero? height)))
|
||||
(cond-> props
|
||||
(= grow-type :auto-width)
|
||||
(assoc :width width)
|
||||
|
||||
(p/let [position-data (tsp/calc-position-data id)
|
||||
props {:position-data position-data}
|
||||
|
||||
props
|
||||
(if (contains? #{:auto-height :auto-width} grow-type)
|
||||
(let [{:keys [width height]} (-> (dom/query node ".paragraph-set") (dom/get-client-size))
|
||||
width (mth/ceil width)
|
||||
height (mth/ceil height)]
|
||||
(if (and (not (mth/almost-zero? width)) (not (mth/almost-zero? height)))
|
||||
(cond-> props
|
||||
(= grow-type :auto-width)
|
||||
(assoc :width width)
|
||||
|
||||
(or (= grow-type :auto-height) (= grow-type :auto-width))
|
||||
(assoc :height height))
|
||||
props))
|
||||
props)]
|
||||
(st/emit! (dwt/update-text-modifier id props))))
|
||||
(or (= grow-type :auto-height) (= grow-type :auto-width))
|
||||
(assoc :height height))
|
||||
props))
|
||||
props))))
|
||||
(p/fmap (fn [props]
|
||||
;; We need to wait for the text modifier to be updated before
|
||||
;; we can update the position data. Otherwise the position data
|
||||
;; will be wrong.
|
||||
;; TODO: This is a hack. We need to find a better way to do this.
|
||||
(st/emit! (dwt/update-text-modifier id props))
|
||||
(ts/schedule 30 #(update-text-shape shape node))))))
|
||||
|
||||
(mf/defc text-container
|
||||
{::mf/wrap-props false
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.assets :as dwa]
|
||||
[app.main.data.workspace.colors :as dc]
|
||||
[app.main.data.workspace.libraries :as dwl]
|
||||
[app.main.data.workspace.media :as dwm]
|
||||
@@ -2424,19 +2425,32 @@
|
||||
[]
|
||||
(let [components-v2 (mf/use-ctx ctx/components-v2)
|
||||
read-only? (mf/use-ctx ctx/workspace-read-only?)
|
||||
|
||||
filters* (mf/use-state
|
||||
{:term ""
|
||||
:section :all
|
||||
:ordering :asc
|
||||
:list-style :thumbs})
|
||||
:ordering (dwa/get-current-assets-ordering)
|
||||
:list-style (dwa/get-current-assets-list-style)})
|
||||
filters (deref filters*)
|
||||
term (:term filters)
|
||||
ordering (:ordering filters)
|
||||
list-style (:list-style filters)
|
||||
|
||||
toggle-ordering
|
||||
(mf/use-fn #(swap! filters* update :ordering toggle-values [:asc :desc]))
|
||||
(mf/use-fn
|
||||
(mf/deps ordering)
|
||||
(fn []
|
||||
(let [new-value (toggle-values ordering [:asc :desc])]
|
||||
(swap! filters* assoc :ordering new-value)
|
||||
(dwa/set-current-assets-ordering! new-value))))
|
||||
|
||||
toggle-list-style
|
||||
(mf/use-fn #(swap! filters* update :list-style toggle-values [:thumbs :list]))
|
||||
(mf/use-fn
|
||||
(mf/deps list-style)
|
||||
(fn []
|
||||
(let [new-value (toggle-values list-style [:thumbs :list])]
|
||||
(swap! filters* assoc :list-style new-value)
|
||||
(dwa/set-current-assets-list-style! new-value))))
|
||||
|
||||
on-search-term-change
|
||||
(mf/use-fn
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.workspace.sidebar.layer-name :refer [layer-name]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.timers :as ts]
|
||||
[beicon.core :as rx]
|
||||
@@ -175,6 +176,7 @@
|
||||
;; seek for an alternate solution. Maybe use-context?
|
||||
scroll-node (dom/get-parent-with-data node "scrollContainer")
|
||||
parent-node (dom/get-parent-at node 2)
|
||||
first-child-node (dom/get-first-child parent-node)
|
||||
|
||||
subid
|
||||
(when (and single? selected?)
|
||||
@@ -184,9 +186,9 @@
|
||||
#(let [scroll-distance-ratio (dom/get-scroll-distance-ratio node scroll-node)
|
||||
scroll-behavior (if (> scroll-distance-ratio 1) "instant" "smooth")]
|
||||
(if scroll-to
|
||||
(dom/scroll-into-view! parent-node #js {:block "center" :behavior scroll-behavior :inline "start"})
|
||||
(dom/scroll-into-view! first-child-node #js {:block "center" :behavior scroll-behavior :inline "start"})
|
||||
(do
|
||||
(dom/scroll-into-view-if-needed! parent-node #js {:block "center" :behavior scroll-behavior :inline "start"})
|
||||
(dom/scroll-into-view-if-needed! first-child-node #js {:block "center" :behavior scroll-behavior :inline "start"})
|
||||
(reset! scroll-to-middle? true)))))))]
|
||||
|
||||
#(when (some? subid)
|
||||
@@ -268,10 +270,16 @@
|
||||
(css :selected) (:blocked item))}
|
||||
[:button {:class (dom/classnames (css :toggle-element) true
|
||||
(css :selected) (:hidden item))
|
||||
:title (if (:hidden item)
|
||||
(tr "workspace.shape.menu.show")
|
||||
(tr "workspace.shape.menu.hide"))
|
||||
:on-click toggle-visibility}
|
||||
(if (:hidden item) i/hide-refactor i/shown-refactor)]
|
||||
[:button {:class (dom/classnames (css :block-element) true
|
||||
(css :selected) (:blocked item))
|
||||
:title (if (:blocked item)
|
||||
(tr "workspace.shape.menu.unlock")
|
||||
(tr "workspace.shape.menu.lock"))
|
||||
:on-click toggle-blocking}
|
||||
(if (:blocked item) i/lock-refactor i/unlock-refactor)]]]]
|
||||
(when (and (:shapes item) expanded?)
|
||||
@@ -328,10 +336,16 @@
|
||||
|
||||
[:div.element-actions {:class (when (:shapes item) "is-parent")}
|
||||
[:div.toggle-element {:class (when (:hidden item) "selected")
|
||||
:title (if (:hidden item)
|
||||
(tr "workspace.shape.menu.show")
|
||||
(tr "workspace.shape.menu.hide"))
|
||||
:on-click toggle-visibility}
|
||||
(if (:hidden item) i/eye-closed i/eye)]
|
||||
[:div.block-element {:class (when (:blocked item) "selected")
|
||||
:on-click toggle-blocking}
|
||||
:on-click toggle-blocking
|
||||
:title (if (:blocked item)
|
||||
(tr "workspace.shape.menu.unlock")
|
||||
(tr "workspace.shape.menu.lock"))}
|
||||
(if (:blocked item) i/lock i/unlock)]]
|
||||
|
||||
(when (:shapes item)
|
||||
|
||||
@@ -319,7 +319,8 @@
|
||||
[:div.interactions-element.separator
|
||||
[:span.element-set-subtitle.wide (tr "workspace.options.interaction-trigger")]
|
||||
[:select.input-select
|
||||
{:value (str (:event-type interaction))
|
||||
{:data-mousetrap-dont-stop true ;; makes mousetrap to not stop at this element
|
||||
:value (str (:event-type interaction))
|
||||
:on-change change-event-type}
|
||||
(for [[value name] (event-type-names)]
|
||||
(when-not (and (= value :after-delay)
|
||||
@@ -342,7 +343,8 @@
|
||||
[:div.interactions-element.separator
|
||||
[:span.element-set-subtitle.wide (tr "workspace.options.interaction-action")]
|
||||
[:select.input-select
|
||||
{:value (str (:action-type interaction))
|
||||
{:data-mousetrap-dont-stop true ;; makes mousetrap to not stop at this element
|
||||
:value (str (:action-type interaction))
|
||||
:on-change change-action-type}
|
||||
(for [[value name] (action-type-names)]
|
||||
[:option {:key (dm/str "action-" value)
|
||||
@@ -353,7 +355,8 @@
|
||||
[:div.interactions-element
|
||||
[:span.element-set-subtitle.wide (tr "workspace.options.interaction-destination")]
|
||||
[:select.input-select
|
||||
{:value (str (:destination interaction))
|
||||
{:data-mousetrap-dont-stop true ;; makes mousetrap to not stop at this element
|
||||
:value (str (:destination interaction))
|
||||
:on-change change-destination}
|
||||
(if (= (:action-type interaction) :close-overlay)
|
||||
[:option {:value ""} (tr "workspace.options.interaction-self")]
|
||||
@@ -390,7 +393,8 @@
|
||||
[:div.interactions-element
|
||||
[:span.element-set-subtitle.wide (tr "workspace.options.interaction-relative-to")]
|
||||
[:select.input-select
|
||||
{:value (str (:position-relative-to interaction))
|
||||
{:data-mousetrap-dont-stop true ;; makes mousetrap to not stop at this element
|
||||
:value (str (:position-relative-to interaction))
|
||||
:on-change change-position-relative-to}
|
||||
(when (not= (:overlay-pos-type interaction) :manual)
|
||||
[:*
|
||||
@@ -405,7 +409,8 @@
|
||||
[:div.interactions-element
|
||||
[:span.element-set-subtitle.wide (tr "workspace.options.interaction-position")]
|
||||
[:select.input-select
|
||||
{:value (str (:overlay-pos-type interaction))
|
||||
{:data-mousetrap-dont-stop true ;; makes mousetrap to not stop at this element
|
||||
:value (str (:overlay-pos-type interaction))
|
||||
:on-change (partial change-overlay-pos-type (:id shape))}
|
||||
(for [[value name] (overlay-pos-type-names)]
|
||||
[:option {:value (str value)} name])]]
|
||||
@@ -467,7 +472,8 @@
|
||||
[:div.interactions-element.separator
|
||||
[:span.element-set-subtitle.wide (tr "workspace.options.interaction-animation")]
|
||||
[:select.input-select
|
||||
{:value (str (-> interaction :animation :animation-type))
|
||||
{:data-mousetrap-dont-stop true ;; makes mousetrap to not stop at this element
|
||||
:value (str (-> interaction :animation :animation-type))
|
||||
:on-change change-animation-type}
|
||||
[:option {:value ""} (tr "workspace.options.interaction-animation-none")]
|
||||
(for [[value name] (animation-type-names interaction)]
|
||||
@@ -529,7 +535,8 @@
|
||||
[:div.interactions-element
|
||||
[:span.element-set-subtitle.wide (tr "workspace.options.interaction-easing")]
|
||||
[:select.input-select
|
||||
{:value (str (-> interaction :animation :easing))
|
||||
{:data-mousetrap-dont-stop true ;; makes mousetrap to not stop at this element
|
||||
:value (str (-> interaction :animation :easing))
|
||||
:on-change change-easing}
|
||||
(for [[value name] (easing-names)]
|
||||
[:option {:value (str value)} name])]
|
||||
|
||||
@@ -319,7 +319,7 @@
|
||||
layout-container-ids layout-container-values
|
||||
layout-item-ids layout-item-values]
|
||||
(mf/use-memo
|
||||
(mf/deps objects-no-measures)
|
||||
(mf/deps shapes objects-no-measures)
|
||||
(fn []
|
||||
(into
|
||||
[]
|
||||
|
||||
@@ -451,8 +451,8 @@
|
||||
(dnd/has-type? event "text/uri-list")
|
||||
(let [data (dnd/get-data event "text/uri-list")
|
||||
lines (str/lines data)
|
||||
uris (->> lines (filter #(str/starts-with? % "http")))
|
||||
data (->> lines (filter #(str/starts-with? % "data:image/")))
|
||||
uris (filterv #(str/starts-with? % "http") lines)
|
||||
data (filterv #(str/starts-with? % "data:image/") lines)
|
||||
params {:file-id (:id file)
|
||||
:position viewport-coord}
|
||||
params (if (seq uris)
|
||||
|
||||
@@ -222,11 +222,13 @@
|
||||
(mf/use-callback
|
||||
(mf/deps (:id frame) on-frame-select)
|
||||
(fn [bevent]
|
||||
(let [event (.-nativeEvent bevent)]
|
||||
(let [event (.-nativeEvent bevent)
|
||||
params {:section "interactions"
|
||||
:frame-id (:id frame)}]
|
||||
(when (= 1 (.-which event))
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(on-frame-select event (:id frame))))))
|
||||
(st/emit! (dw/go-to-viewer params))))))
|
||||
|
||||
on-double-click
|
||||
(mf/use-callback
|
||||
|
||||
@@ -151,9 +151,9 @@
|
||||
(rx/tap (fn [[fonts]]
|
||||
(when (seq fonts)
|
||||
(st/emit! (df/fonts-fetched fonts)))))
|
||||
(rx/map (comp :objects second))))))
|
||||
(rx/map (fn [[_ page]] {:objects (:objects page)}))))))
|
||||
|
||||
objects (use-resource fetch-state)]
|
||||
{:keys [objects]} (use-resource fetch-state)]
|
||||
|
||||
(when objects
|
||||
(for [object-id object-ids]
|
||||
|
||||
@@ -97,11 +97,12 @@
|
||||
(defn- svg-update-image!
|
||||
"Updates an image in an SVG to a Data URI."
|
||||
[image]
|
||||
(when-let [href (dom/get-attribute image "href")]
|
||||
(if-let [href (dom/get-attribute image "href")]
|
||||
(->> (fetch-as-data-uri href)
|
||||
(rx/map (fn [url]
|
||||
(dom/set-attribute! image "href" url)
|
||||
image)))))
|
||||
image)))
|
||||
(rx/empty)))
|
||||
|
||||
(defn- svg-resolve-images!
|
||||
"Resolves all images in an SVG to Data URIs."
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"Color conversion utils."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.object :as obj]
|
||||
[app.util.strings :as ust]
|
||||
@@ -176,14 +177,16 @@
|
||||
(= id :multiple)
|
||||
(= file-id :multiple)))
|
||||
|
||||
(defn color? [^string color-str]
|
||||
(and (not (nil? color-str))
|
||||
(seq color-str)
|
||||
(gcolor/isValidColor color-str)))
|
||||
(defn color?
|
||||
[color]
|
||||
(and (string? color)
|
||||
(gcolor/isValidColor color)))
|
||||
|
||||
(defn parse-color [^string color-str]
|
||||
(let [result (gcolor/parse color-str)]
|
||||
(str (.-hex ^js result))))
|
||||
(defn parse-color
|
||||
[color]
|
||||
(when (color? color)
|
||||
(let [result (gcolor/parse color)]
|
||||
(dm/str (.-hex ^js result)))))
|
||||
|
||||
(def color-names
|
||||
(obj/get-keys ^js gcolor/names))
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
{:label "Español" :value "es"}
|
||||
{:label "Català" :value "ca"}
|
||||
{:label "Deutsch (community)" :value "de"}
|
||||
{:label "Dutch (community)" :value "nl"}
|
||||
{:label "Euskera (community)" :value "eu"}
|
||||
{:label "Français (community)" :value "fr"}
|
||||
{:label "Gallego (Community)" :value "gl"}
|
||||
|
||||
@@ -737,12 +737,13 @@
|
||||
:fill-color-ref-file (get-meta fill-node :fill-color-ref-file uuid/uuid)
|
||||
:fill-color-ref-id (get-meta fill-node :fill-color-ref-id uuid/uuid)
|
||||
:fill-opacity (get-meta fill-node :fill-opacity d/parse-double)}))
|
||||
(mapv d/without-nils))]
|
||||
(mapv d/without-nils)
|
||||
(filterv #(not= (:fill-color %) "none")))]
|
||||
(if (seq fills)
|
||||
fills
|
||||
(->> [(-> (add-fill {} node svg-data)
|
||||
(d/without-nils))]
|
||||
(filterv not-empty)))))
|
||||
(filterv #(and (not-empty %) (not= (:fill-color %) "none")))))))
|
||||
|
||||
(defn parse-strokes
|
||||
[node svg-data]
|
||||
@@ -761,12 +762,13 @@
|
||||
:stroke-alignment (get-meta stroke-node :stroke-alignment keyword)
|
||||
:stroke-cap-start (get-meta stroke-node :stroke-cap-start keyword)
|
||||
:stroke-cap-end (get-meta stroke-node :stroke-cap-end keyword)}))
|
||||
(mapv d/without-nils))]
|
||||
(mapv d/without-nils)
|
||||
(filterv #(not= (:stroke-color %) "none")))]
|
||||
(if (seq strokes)
|
||||
strokes
|
||||
(->> [(-> (add-stroke {} node svg-data)
|
||||
(d/without-nils))]
|
||||
(filterv #(and (not-empty %) (not= (:stroke-style %) :none)))))))
|
||||
(filterv #(and (not-empty %) (not= (:stroke-color %) "none") (not= (:stroke-style %) :none)))))))
|
||||
|
||||
(defn add-svg-content
|
||||
[props node]
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
(progress! context type file nil nil))
|
||||
|
||||
([context type current total]
|
||||
(keyword? type)
|
||||
(assert (keyword? type))
|
||||
(assert (number? current))
|
||||
(assert (number? total))
|
||||
(progress! context type nil current total))
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"PO-Revision-Date: 2022-10-04 18:22+0000\n"
|
||||
"Last-Translator: Youkho <youkho@gmail.com>\n"
|
||||
"Language-Team: Arabic "
|
||||
"<https://hosted.weblate.org/projects/penpot/frontend/ar/>\n"
|
||||
"PO-Revision-Date: 2023-07-02 17:52+0000\n"
|
||||
"Last-Translator: Amine Gdoura <amine2gdoura@gmail.com>\n"
|
||||
"Language-Team: Arabic <https://hosted.weblate.org/projects/penpot/frontend/"
|
||||
"ar/>\n"
|
||||
"Language: ar\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 "
|
||||
"&& n%100<=10 ? 3 : n%100>=11 ? 4 : 5;\n"
|
||||
"X-Generator: Weblate 4.14.1\n"
|
||||
"X-Generator: Weblate 5.0-dev\n"
|
||||
|
||||
#: src/app/main/ui/auth/register.cljs
|
||||
msgid "auth.already-have-account"
|
||||
@@ -515,10 +515,6 @@ msgstr "+ مشروع جديد"
|
||||
msgid "dashboard.new-project-prefix"
|
||||
msgstr "مشروع جديد"
|
||||
|
||||
#: src/app/main/ui/settings/profile.cljs
|
||||
msgid "dashboard.newsletter-title"
|
||||
msgstr "الإشتراك في"
|
||||
|
||||
#: src/app/main/ui/dashboard/search.cljs
|
||||
msgid "dashboard.no-matches-for"
|
||||
msgstr "لم يتم العثور على مطابقات ل \"%s\""
|
||||
@@ -825,10 +821,6 @@ msgstr "يبدو أن اسم المستخدم أو كلمة المرور خاط
|
||||
msgid "errors.wrong-old-password"
|
||||
msgstr "كلمة المرور القديمة غير صحيحة"
|
||||
|
||||
#: src/app/main/ui/settings/feedback.cljs
|
||||
msgid "feedback.chat-subtitle"
|
||||
msgstr "ترغب في الكلام؟ تحدث معنا في Gitter"
|
||||
|
||||
#: src/app/main/ui/settings/feedback.cljs
|
||||
msgid "feedback.description"
|
||||
msgstr "وصف"
|
||||
@@ -944,14 +936,6 @@ msgstr "عرض"
|
||||
msgid "inspect.attributes.shadow"
|
||||
msgstr "ظل"
|
||||
|
||||
#: src/app/main/ui/inspect/attributes/shadow.cljs
|
||||
msgid "inspect.attributes.shadow.shorthand.offset-x"
|
||||
msgstr "X"
|
||||
|
||||
#: src/app/main/ui/inspect/attributes/shadow.cljs
|
||||
msgid "inspect.attributes.shadow.shorthand.spread"
|
||||
msgstr "S"
|
||||
|
||||
#: src/app/main/ui/inspect/attributes/stroke.cljs
|
||||
msgid "inspect.attributes.stroke"
|
||||
msgstr "لون الحدّ"
|
||||
@@ -1248,9 +1232,6 @@ msgstr "مركز المساعدة"
|
||||
msgid "labels.hide-resolved-comments"
|
||||
msgstr "إخفاء التعليقات التي تم حلها"
|
||||
|
||||
msgid "labels.images"
|
||||
msgstr "الصور"
|
||||
|
||||
msgid "labels.installed-fonts"
|
||||
msgstr "الخطوط المتوفرة"
|
||||
|
||||
@@ -1458,9 +1439,6 @@ msgstr "قائمة التعليقات"
|
||||
msgid "labels.show-your-comments"
|
||||
msgstr "إظهار تعليقاتك فقط"
|
||||
|
||||
msgid "labels.skip"
|
||||
msgstr "تخطي"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs
|
||||
msgid "labels.status"
|
||||
msgstr "الحالة"
|
||||
@@ -1884,15 +1862,6 @@ msgstr "أنشئ فريقًا وأرسل الدعوات"
|
||||
msgid "onboarding.choice.team-up.roles"
|
||||
msgstr "دعوة مع الدور:"
|
||||
|
||||
msgid "onboarding.contrib.alt"
|
||||
msgstr "مصدر مفتوح"
|
||||
|
||||
msgid "onboarding.contrib.desc2.1"
|
||||
msgstr "يمكنك الوصول إلى"
|
||||
|
||||
msgid "onboarding.contrib.link"
|
||||
msgstr "مشروع على github"
|
||||
|
||||
msgid "onboarding.newsletter.accept"
|
||||
msgstr "نعم ، اشترك"
|
||||
|
||||
@@ -1905,28 +1874,9 @@ msgstr "سياسة الخصوصية."
|
||||
msgid "onboarding.newsletter.title"
|
||||
msgstr "هل تريد تلقي أخبار Penpot؟"
|
||||
|
||||
msgid "onboarding.slide.0.desc1"
|
||||
msgstr "قم بإنشاء واجهات مستخدم جميلة بالتعاون مع جميع أعضاء الفريق."
|
||||
|
||||
msgid "onboarding.slide.0.title"
|
||||
msgstr "مكتبات التصميم والأنماط والمكونات"
|
||||
|
||||
msgid "onboarding.slide.1.desc1"
|
||||
msgstr "أنشئ تفاعلات غنية لتقليد سلوك المنتج."
|
||||
|
||||
msgid "onboarding.slide.1.title"
|
||||
msgstr "اجعل تصميماتك تنبض بالحياة من خلال التفاعلات"
|
||||
|
||||
msgid "onboarding.slide.2.desc1"
|
||||
msgstr ""
|
||||
"يعمل جميع أعضاء الفريق في وقت واحد مع تصميمات متعددة اللاعبين في الوقت "
|
||||
"الفعلي وتعليقات وأفكار وتعليقات مركزية مباشرة على التصميمات."
|
||||
|
||||
msgid "onboarding.slide.3.desc1"
|
||||
msgstr ""
|
||||
"قم بمزامنة التصميم والرمز لجميع المكونات والأنماط الخاصة بك واحصل على "
|
||||
"مقتطفات التعليمات البرمجية."
|
||||
|
||||
msgid "onboarding.team-modal.create-team"
|
||||
msgstr "أنشئ فريقًا"
|
||||
|
||||
@@ -2343,14 +2293,6 @@ msgstr "التفاعلات"
|
||||
msgid "viewer.header.share.copy-link"
|
||||
msgstr "نسخ الرابط"
|
||||
|
||||
#: src/app/main/ui/viewer/header.cljs
|
||||
msgid "viewer.header.share.placeholder"
|
||||
msgstr "سيظهر رابط المشاركة هنا"
|
||||
|
||||
#: src/app/main/ui/viewer/header.cljs
|
||||
msgid "viewer.header.share.subtitle"
|
||||
msgstr "أي شخص لديه الرابط سيكون لديه حق الوصول"
|
||||
|
||||
#: src/app/main/ui/viewer/header.cljs
|
||||
msgid "viewer.header.show-interactions"
|
||||
msgstr "إظهار التفاعلات"
|
||||
@@ -2697,15 +2639,6 @@ msgstr "تحديث"
|
||||
msgid "workspace.libraries.updates"
|
||||
msgstr "التحديثات"
|
||||
|
||||
msgid "workspace.library.libraries"
|
||||
msgstr "المكتبات"
|
||||
|
||||
msgid "workspace.library.store"
|
||||
msgstr "المكتبات المخزنة"
|
||||
|
||||
msgid "workspace.options.blur-options.layer-blur"
|
||||
msgstr "طبقة"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs
|
||||
msgid "workspace.options.blur-options.title"
|
||||
msgstr "الضبابية"
|
||||
@@ -2906,4 +2839,71 @@ msgid "workspace.updates.update"
|
||||
msgstr "تحديث"
|
||||
|
||||
msgid "workspace.viewport.click-to-close-path"
|
||||
msgstr "انقر لإغلاق المسار"
|
||||
msgstr "انقر لإغلاق المسار"
|
||||
|
||||
#, markdown
|
||||
msgid "dashboard.fonts.warning-text"
|
||||
msgstr ""
|
||||
"لقد اكتشفنا مشكلة محتملة في الخطوط الخاصة بك تتعلق بالمقاييس الرأسية لأنظمة "
|
||||
"التشغيل المختلفة. للتحقق من ذلك ، يمكنك استخدام خدمات المقاييس العمودية "
|
||||
"للخطوط مثل [هذه] (https://vertical-metrics.netlify.app/). بالإضافة إلى ذلك ، "
|
||||
"نوصي باستخدام [Transfonter] (https://transfonter.org/) لإنشاء خطوط الويب "
|
||||
"وإصلاح الأخطاء. "
|
||||
|
||||
msgid "dashboard.webhooks.active"
|
||||
msgstr "نشط"
|
||||
|
||||
msgid "dashboard.webhooks.active.explain"
|
||||
msgstr "عندما يتم تشغيل هذا الخطاف ، سيتم تسليم تفاصيل الحدث"
|
||||
|
||||
#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, src/app/main/ui/auth/recovery_request.cljs
|
||||
msgid "errors.email-invalid"
|
||||
msgstr "أدخل بريدًا إلكترونيًا صالحًا من فضلك"
|
||||
|
||||
#: src/app/main/errors.cljs
|
||||
msgid "errors.feature-mismatch"
|
||||
msgstr ""
|
||||
"يبدو أنك تفتح ملفًا تم تمكين الميزة \"٪ s\" فيه ولكن الواجهة الأمامية لـ "
|
||||
"penpot لا تدعمه أو تم تعطيله."
|
||||
|
||||
msgid "errors.webhooks.unexpected-status"
|
||||
msgstr "حالة غير متوقعة٪ s"
|
||||
|
||||
#: src/app/main/ui/inspect/attributes/text.cljs
|
||||
msgid "inspect.attributes.typography.font-weight"
|
||||
msgstr "وزن الخط"
|
||||
|
||||
msgid "inspect.empty.help"
|
||||
msgstr ""
|
||||
"إذا كنت تريد معرفة المزيد عن فحص التصميم ، فتفضل بزيارة مركز مساعدة لPenpot"
|
||||
|
||||
msgid "dashboard.webhooks.create"
|
||||
msgstr "إنشاء الرد التلقائي على الويب"
|
||||
|
||||
msgid "errors.bad-font"
|
||||
msgstr "تعذر تحميل الخط٪ s"
|
||||
|
||||
msgid "errors.bad-font-plural"
|
||||
msgstr "تعذر تحميل الخطوط٪ s"
|
||||
|
||||
msgid "dashboard.webhooks.content-type"
|
||||
msgstr "نوع المحتوى"
|
||||
|
||||
#: src/app/main/errors.cljs
|
||||
msgid "errors.feature-not-supported"
|
||||
msgstr "الميزة '٪ s' غير مدعومة."
|
||||
|
||||
msgid "errors.profile-blocked"
|
||||
msgstr "هذا الملف الشخصي محظور"
|
||||
|
||||
msgid "errors.webhooks.connection"
|
||||
msgstr "خطأ في الاتصال ، عنوان إلكتروني لا يمكن الوصول إليه"
|
||||
|
||||
msgid "errors.webhooks.last-delivery"
|
||||
msgstr "آخر تسليم لم يكن ناجحًا."
|
||||
|
||||
msgid "errors.webhooks.timeout"
|
||||
msgstr "نفذ الوقت"
|
||||
|
||||
msgid "errors.webhooks.unexpected"
|
||||
msgstr "خطأ غير متوقع في التحقق"
|
||||
|
||||
@@ -522,10 +522,6 @@ msgstr "+ Projecte nou"
|
||||
msgid "dashboard.new-project-prefix"
|
||||
msgstr "Projecte nou"
|
||||
|
||||
#: src/app/main/ui/settings/profile.cljs
|
||||
msgid "dashboard.newsletter-title"
|
||||
msgstr "Subscripció al butlletí"
|
||||
|
||||
#: src/app/main/ui/dashboard/search.cljs
|
||||
msgid "dashboard.no-matches-for"
|
||||
msgstr "No s'ha trobat cap coincidència amb “%s“"
|
||||
@@ -823,10 +819,6 @@ msgstr "El nom d'usuari o la contrasenya sembla incorrecte."
|
||||
msgid "errors.wrong-old-password"
|
||||
msgstr "La contrasenya anterior no és correcta"
|
||||
|
||||
#: src/app/main/ui/settings/feedback.cljs
|
||||
msgid "feedback.chat-subtitle"
|
||||
msgstr "Voleu parlar? Xategeu amb nosaltres a Gitter"
|
||||
|
||||
#: src/app/main/ui/settings/feedback.cljs
|
||||
msgid "feedback.description"
|
||||
msgstr "Descripció"
|
||||
@@ -942,10 +934,6 @@ msgstr "Amplada"
|
||||
msgid "inspect.attributes.shadow"
|
||||
msgstr "Ombra"
|
||||
|
||||
#: src/app/main/ui/inspect/attributes/shadow.cljs
|
||||
msgid "inspect.attributes.shadow.shorthand.offset-x"
|
||||
msgstr "X"
|
||||
|
||||
#: src/app/main/ui/inspect/attributes/shadow.cljs
|
||||
msgid "inspect.attributes.shadow.shorthand.spread"
|
||||
msgstr "S"
|
||||
@@ -1242,9 +1230,6 @@ msgstr "Centre d'ajuda"
|
||||
msgid "labels.hide-resolved-comments"
|
||||
msgstr "Amaga els comentaris resolts"
|
||||
|
||||
msgid "labels.images"
|
||||
msgstr "Imatges"
|
||||
|
||||
msgid "labels.installed-fonts"
|
||||
msgstr "Tipografies instal·lades"
|
||||
|
||||
@@ -1438,9 +1423,6 @@ msgstr "Mostra la llista de comentaris"
|
||||
msgid "labels.show-your-comments"
|
||||
msgstr "Mostra només els meus comentaris"
|
||||
|
||||
msgid "labels.skip"
|
||||
msgstr "Omet"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs
|
||||
msgid "labels.status"
|
||||
msgstr "Estat"
|
||||
@@ -1821,15 +1803,9 @@ msgstr "Crea l'equip ara i convida membres en un altre moment"
|
||||
msgid "onboarding.choice.team-up.invite-members-submit"
|
||||
msgstr "Crea l'equip i envia les invitacions"
|
||||
|
||||
msgid "onboarding.contrib.alt"
|
||||
msgstr "Codi obert"
|
||||
|
||||
msgid "onboarding.contrib.desc2.1"
|
||||
msgstr "Podeu accedir al"
|
||||
|
||||
msgid "onboarding.contrib.link"
|
||||
msgstr "projecte a github"
|
||||
|
||||
msgid "onboarding.newsletter.accept"
|
||||
msgstr "Sí, subscriu-m'hi"
|
||||
|
||||
@@ -1844,33 +1820,15 @@ msgstr "Política de privacitat."
|
||||
msgid "onboarding.newsletter.title"
|
||||
msgstr "Voleu rebre les novetats de Penpot?"
|
||||
|
||||
msgid "onboarding.slide.0.desc1"
|
||||
msgstr ""
|
||||
"Creeu interfícies d'usuari boniques en col·laboració amb tots els membres "
|
||||
"de l'equip."
|
||||
|
||||
msgid "onboarding.slide.0.title"
|
||||
msgstr "Biblioteques de disseny, estils i components"
|
||||
|
||||
msgid "onboarding.slide.1.desc1"
|
||||
msgstr "Creeu interaccions enriquides per a imitar el comportament del producte."
|
||||
|
||||
msgid "onboarding.slide.1.title"
|
||||
msgstr "Doneu vida als vostres dissenys amb interaccions"
|
||||
|
||||
msgid "onboarding.slide.2.desc1"
|
||||
msgstr ""
|
||||
"Tot l'equip treballant simultàniament amb disseny en temps real i "
|
||||
"comentaris, idees i opinions sobre els dissenys de forma centralitzada."
|
||||
|
||||
msgid "onboarding.slide.3.alt"
|
||||
msgstr "Lliurament i codi baix"
|
||||
|
||||
msgid "onboarding.slide.3.desc2"
|
||||
msgstr ""
|
||||
"Obteniu i proporcioneu especificacions de codi d'etiquetatge (SVG, HTML) o "
|
||||
"d'estils (CSS, Less, Stylus...)."
|
||||
|
||||
msgid "onboarding.team-modal.create-team"
|
||||
msgstr "Crea un equip"
|
||||
|
||||
@@ -2410,10 +2368,6 @@ msgstr "Interaccions (%s)"
|
||||
msgid "viewer.header.share.copy-link"
|
||||
msgstr "Copia l'enllaç"
|
||||
|
||||
#: src/app/main/ui/viewer/header.cljs
|
||||
msgid "viewer.header.share.placeholder"
|
||||
msgstr "L'enllaç compartit apareixerà aquí"
|
||||
|
||||
#: src/app/main/ui/viewer/header.cljs
|
||||
msgid "viewer.header.share.subtitle"
|
||||
msgstr "Qualsevol persona amb l'enllaç hi tindrà accés"
|
||||
@@ -2850,9 +2804,6 @@ msgstr "Actualitza"
|
||||
msgid "workspace.libraries.updates"
|
||||
msgstr "ACTUALITZACIONS"
|
||||
|
||||
msgid "workspace.library.libraries"
|
||||
msgstr "Biblioteques"
|
||||
|
||||
msgid "workspace.library.store"
|
||||
msgstr "Predeterminades"
|
||||
|
||||
@@ -2860,9 +2811,6 @@ msgstr "Predeterminades"
|
||||
msgid "workspace.options.add-interaction"
|
||||
msgstr "Feu clic en el botó de + per a afegir interaccions."
|
||||
|
||||
msgid "workspace.options.blur-options.layer-blur"
|
||||
msgstr "Capa"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs
|
||||
msgid "workspace.options.blur-options.title"
|
||||
msgstr "Difuminat"
|
||||
@@ -4149,4 +4097,4 @@ msgid "workspace.updates.update"
|
||||
msgstr "Actualitza"
|
||||
|
||||
msgid "workspace.viewport.click-to-close-path"
|
||||
msgstr "Feu clic per a tancar el camí"
|
||||
msgstr "Feu clic per a tancar el camí"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,15 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"PO-Revision-Date: 2023-02-08 14:36+0000\n"
|
||||
"PO-Revision-Date: 2023-08-19 11:55+0000\n"
|
||||
"Last-Translator: Stas Haas <stas@girafic.de>\n"
|
||||
"Language-Team: German "
|
||||
"<https://hosted.weblate.org/projects/penpot/frontend/de/>\n"
|
||||
"Language-Team: German <https://hosted.weblate.org/projects/penpot/frontend/"
|
||||
"de/>\n"
|
||||
"Language: de\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 4.16-dev\n"
|
||||
"X-Generator: Weblate 5.0-dev\n"
|
||||
|
||||
#: src/app/main/ui/auth/register.cljs
|
||||
msgid "auth.already-have-account"
|
||||
@@ -445,9 +445,6 @@ msgstr ""
|
||||
"Beim Importieren der Datei ist ein Fehler aufgetreten. Die Datei wurde "
|
||||
"nicht importiert."
|
||||
|
||||
msgid "dashboard.import.import-message"
|
||||
msgstr "%s Dateien wurden erfolgreich importiert."
|
||||
|
||||
msgid "dashboard.import.import-warning"
|
||||
msgstr "Einige Dateien enthielten ungültige Objekte, die entfernt wurden."
|
||||
|
||||
@@ -1182,7 +1179,7 @@ msgstr "Info"
|
||||
|
||||
#: src/app/main/ui/workspace/header.cljs
|
||||
msgid "label.shortcuts"
|
||||
msgstr "Tastenkürzel"
|
||||
msgstr "Tastaturkürzel"
|
||||
|
||||
msgid "labels.accept"
|
||||
msgstr "Akzeptieren"
|
||||
@@ -1780,21 +1777,25 @@ msgstr[1] "Dateien löschen"
|
||||
msgid "modals.delete-shared-confirm.hint"
|
||||
msgid_plural "modals.delete-shared-confirm.hint"
|
||||
msgstr[0] ""
|
||||
"Wenn Sie es löschen, werden diese Assets in die lokale Bibliothek dieser "
|
||||
"Datei verschoben. Unbenutzte Assets gehen verloren."
|
||||
"Wenn Sie es löschen, werden diese Assets nicht mehr in anderen Dateien "
|
||||
"verfügbar sein. Die bereits verwendeten Assets, bleiben in dieser Datei "
|
||||
"erhalten (das Design wird dadurch nicht zerstört!)."
|
||||
msgstr[1] ""
|
||||
"Wenn Sie sie löschen, werden diese Assets in die lokale Bibliothek dieser "
|
||||
"Datei verschoben. Unbenutzte Assets gehen verloren."
|
||||
"Wenn Sie die Assets löschen, werden diese Assets nicht mehr in anderen "
|
||||
"Dateien verfügbar sein. Die bereits verwendeten Assets, bleiben in dieser "
|
||||
"Datei erhalten (das Design wird dadurch nicht zerstört!)."
|
||||
|
||||
#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs
|
||||
msgid "modals.delete-shared-confirm.hint-many"
|
||||
msgid_plural "modals.delete-shared-confirm.hint-many"
|
||||
msgstr[0] ""
|
||||
"Wenn Sie es löschen, werden diese Assets in die lokalen Bibliotheken dieser "
|
||||
"Dateien verschoben. Unbenutzte Assets gehen verloren."
|
||||
"Wenn Sie es löschen, werden diese Assets nicht mehr in anderen Dateien "
|
||||
"verfügbar sein. Die bereits verwendeten Assets, bleiben in dieser Datei "
|
||||
"erhalten (das Design wird dadurch nicht zerstört!)."
|
||||
msgstr[1] ""
|
||||
"Wenn Sie sie löschen, werden diese Assets in die lokalen Bibliotheken "
|
||||
"dieser Dateien verschoben. Unbenutzte Assets gehen verloren."
|
||||
"Wenn Sie die Assets löschen, werden diese Assets nicht mehr in anderen "
|
||||
"Dateien verfügbar sein. Die bereits verwendeten Assets, bleiben in dieser "
|
||||
"Datei erhalten (das Design wird dadurch nicht zerstört!)."
|
||||
|
||||
#: src/app/main/ui/workspace/header.cljs,
|
||||
#: src/app/main/ui/dashboard/file_menu.cljs
|
||||
@@ -1977,21 +1978,25 @@ msgstr "Minimal"
|
||||
msgid "modals.unpublish-shared-confirm.hint"
|
||||
msgid_plural "modals.unpublish-shared-confirm.hint"
|
||||
msgstr[0] ""
|
||||
"Wenn Sie die Veröffentlichung aufheben, werden diese Assets in die lokale "
|
||||
"Bibliothek dieser Datei verschoben."
|
||||
"Wenn Sie die Veröffentlichung aufheben, sind diese Assets nicht mehr in "
|
||||
"anderen Dateien verfügbar. Bereits verwendete Assets bleiben in dieser Datei "
|
||||
"erhalten (das Design wird nicht beeinträchtigt!)."
|
||||
msgstr[1] ""
|
||||
"Wenn Sie die Veröffentlichung aufheben, werden diese Assets in die lokale "
|
||||
"Bibliothek dieser Datei verschoben."
|
||||
"Wenn Sie die Veröffentlichung aufheben, sind diese Assets nicht mehr in "
|
||||
"anderen Dateien verfügbar. Bereits verwendete Assets bleiben in dieser Datei "
|
||||
"erhalten (das Design wird nicht beeinträchtigt!)."
|
||||
|
||||
#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs
|
||||
msgid "modals.unpublish-shared-confirm.hint-many"
|
||||
msgid_plural "modals.unpublish-shared-confirm.hint-many"
|
||||
msgstr[0] ""
|
||||
"Wenn Sie die Veröffentlichung aufheben, werden diese Assets in die lokalen "
|
||||
"Bibliotheken dieser Dateien verschoben."
|
||||
"Wenn Sie die Veröffentlichung aufheben, sind diese Assets nicht mehr in "
|
||||
"anderen Dateien verfügbar. Bereits verwendete Assets bleiben in diesen "
|
||||
"Dateien erhalten (das Design wird nicht beeinträchtigt!)."
|
||||
msgstr[1] ""
|
||||
"Wenn Sie die Veröffentlichung aufheben, werden diese Assets in die lokalen "
|
||||
"Bibliotheken dieser Dateien verschoben."
|
||||
"Wenn Sie die Veröffentlichung aufheben, sind diese Assets nicht mehr in "
|
||||
"anderen Dateien verfügbar. Bereits verwendete Assets bleiben in diesen "
|
||||
"Dateien erhalten (das Design wird nicht beeinträchtigt!)."
|
||||
|
||||
#: src/app/main/ui/workspace/header.cljs,
|
||||
#: src/app/main/ui/dashboard/file_menu.cljs
|
||||
@@ -2494,7 +2499,7 @@ msgid "shortcuts.next-frame"
|
||||
msgstr "Nächstes Board"
|
||||
|
||||
msgid "shortcuts.not-found"
|
||||
msgstr "Kein Tastenkürzel gefunden"
|
||||
msgstr "Kein Tastaturkürzel gefunden"
|
||||
|
||||
msgid "shortcuts.opacity-0"
|
||||
msgstr "Deckkraft auf 100% setzen"
|
||||
@@ -2563,7 +2568,7 @@ msgid "shortcuts.reset-zoom"
|
||||
msgstr "Zoom zurücksetzen"
|
||||
|
||||
msgid "shortcuts.search-placeholder"
|
||||
msgstr "Tastenkürzel suchen"
|
||||
msgstr "Tastaturkürzel suchen"
|
||||
|
||||
msgid "shortcuts.select-all"
|
||||
msgstr "Alles auswählen"
|
||||
@@ -2575,7 +2580,7 @@ msgid "shortcuts.show-pixel-grid"
|
||||
msgstr "Pixelraster ein-/ausblenden"
|
||||
|
||||
msgid "shortcuts.show-shortcuts"
|
||||
msgstr "Tastenkürzel ein-/ausblenden"
|
||||
msgstr "Tastaturkürzel ein-/ausblenden"
|
||||
|
||||
msgid "shortcuts.snap-nodes"
|
||||
msgstr "An den Punkten ausrichten"
|
||||
@@ -2597,7 +2602,7 @@ msgstr "Miniaturansichten festlegen"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs
|
||||
msgid "shortcuts.title"
|
||||
msgstr "Tastatürkürzel"
|
||||
msgstr "Tastaturkürzel"
|
||||
|
||||
msgid "shortcuts.toggle-alignment"
|
||||
msgstr "Dynamische Ausrichtung umschalten"
|
||||
@@ -3800,7 +3805,7 @@ msgstr "Spalte"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs
|
||||
msgid "workspace.options.layout.direction.column-reverse"
|
||||
msgstr "Spalte-umgekehrt"
|
||||
msgstr "Spalte umkehren"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs
|
||||
msgid "workspace.options.layout.direction.row"
|
||||
@@ -3808,7 +3813,7 @@ msgstr "Reihe"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs
|
||||
msgid "workspace.options.layout.direction.row-reverse"
|
||||
msgstr "Reihe-umgekehrt"
|
||||
msgstr "Reihe umkehren"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/layout.cljs
|
||||
msgid "workspace.options.layout.gap"
|
||||
@@ -3891,7 +3896,7 @@ msgstr "Alle Ecken"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
|
||||
msgid "workspace.options.radius.single-corners"
|
||||
msgstr "Individuelle Ecken"
|
||||
msgstr "Ecken einzeln anpassen"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
|
||||
msgid "workspace.options.radius-top-left"
|
||||
@@ -4515,7 +4520,7 @@ msgstr "Rechteck (%s)"
|
||||
|
||||
#: src/app/main/ui/workspace/left_toolbar.cljs
|
||||
msgid "workspace.toolbar.shortcuts"
|
||||
msgstr "Tastenkürzel (%s)"
|
||||
msgstr "Tastaturkürzel (%s)"
|
||||
|
||||
#: src/app/main/ui/workspace/left_toolbar.cljs
|
||||
msgid "workspace.toolbar.text"
|
||||
@@ -4653,4 +4658,107 @@ msgid "workspace.updates.update"
|
||||
msgstr "Aktualisieren"
|
||||
|
||||
msgid "workspace.viewport.click-to-close-path"
|
||||
msgstr "Klicken Sie, um den Pfad zu schließen"
|
||||
msgstr "Klicken Sie, um den Pfad zu schließen"
|
||||
|
||||
#: src/app/main/ui/dashboard/file_menu.cljs
|
||||
msgid "dashboard.success-duplicate-file"
|
||||
msgid_plural "dashboard.success-delete-file"
|
||||
msgstr[0] "Ihre Datei wurde erfolgreich dupliziert"
|
||||
msgstr[1] "Ihre Dateien wurden erfolgreich dupliziert"
|
||||
|
||||
#: src/app/main/ui/inspect/attributes/text.cljs
|
||||
msgid "inspect.attributes.typography.font-weight"
|
||||
msgstr "Strichstärke"
|
||||
|
||||
#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs
|
||||
msgid "modals.unpublish-shared-confirm.accept"
|
||||
msgid_plural "modals.unpublish-shared-confirm.accept"
|
||||
msgstr[0] "Veröffentlichung aufheben"
|
||||
msgstr[1] "Veröffentlichung aufheben"
|
||||
|
||||
msgid "shortcuts.font-size-inc"
|
||||
msgstr "Schriftgröße erhöhen"
|
||||
|
||||
msgid "shortcut-subsection.text-editor"
|
||||
msgstr "Texte"
|
||||
|
||||
msgid "shortcuts.align-center"
|
||||
msgstr "Zentrieren"
|
||||
|
||||
msgid "shortcuts.font-size-dec"
|
||||
msgstr "Schriftgröße verkleinern"
|
||||
|
||||
#: src/app/main/ui/dashboard/file_menu.cljs
|
||||
msgid "dashboard.success-delete-file"
|
||||
msgid_plural "dashboard.success-delete-file"
|
||||
msgstr[0] "Ihre Datei wurde erfolgreich gelöscht"
|
||||
msgstr[1] "Ihre Dateien wurden erfolgreich gelöscht"
|
||||
|
||||
msgid "workspace.header.menu.undo"
|
||||
msgstr "Rückgängig"
|
||||
|
||||
msgid "workspace.header.menu.disable-scale-content"
|
||||
msgstr "Proportionale Skalierung deaktivieren"
|
||||
|
||||
msgid "workspace.header.menu.enable-scale-content"
|
||||
msgstr "Proportionale Skalierung aktivieren"
|
||||
|
||||
msgid "modals.invite-member.repeated-invitation"
|
||||
msgstr ""
|
||||
"Einige E-Mails stammen von aktuellen Teammitgliedern. Ihre Einladungen "
|
||||
"werden nicht versendet."
|
||||
|
||||
msgid "shortcuts.letter-spacing-inc"
|
||||
msgstr "Buchstabenabstand erhöhen"
|
||||
|
||||
msgid "shortcuts.select-prev"
|
||||
msgstr "Vorherige Ebene auswählen"
|
||||
|
||||
msgid "shortcuts.select-next"
|
||||
msgstr "Nächste Ebene auswählen"
|
||||
|
||||
msgid "shortcuts.align-justify"
|
||||
msgstr "Blocksatz"
|
||||
|
||||
msgid "shortcuts.bold"
|
||||
msgstr "Umschalten auf Fettdruck"
|
||||
|
||||
msgid "shortcuts.italic"
|
||||
msgstr "Umschalten auf Kursivdruck"
|
||||
|
||||
msgid "shortcuts.letter-spacing-dec"
|
||||
msgstr "Buchstabenabstand verringern"
|
||||
|
||||
msgid "shortcuts.line-height-inc"
|
||||
msgstr "Zeilenhöhe erhöhen"
|
||||
|
||||
msgid "shortcuts.line-height-dec"
|
||||
msgstr "Zeilenhöhe verringern"
|
||||
|
||||
msgid "workspace.header.menu.redo"
|
||||
msgstr "Wiederherstellen"
|
||||
|
||||
#, markdown
|
||||
msgid "dashboard.fonts.warning-text"
|
||||
msgstr ""
|
||||
"Wir haben ein mögliches Problem in Ihren Schriften festgestellt, das mit den "
|
||||
"vertikalen Metriken für verschiedene Betriebssysteme zusammenhängt. Um dies "
|
||||
"zu überprüfen, können Sie Online-Dienste wie [diesen](https://vertical-"
|
||||
"metrics.netlify.app/) verwenden. Außerdem empfehlen wir die Verwendung von "
|
||||
"[Transfonter](https://transfonter.org/), um Webfonts zu generieren und "
|
||||
"Fehler zu beheben. "
|
||||
|
||||
msgid "shortcuts.line-through"
|
||||
msgstr "Durchgestrichen"
|
||||
|
||||
msgid "shortcuts.underline"
|
||||
msgstr "Unterstrichen"
|
||||
|
||||
msgid "shortcuts.zoom-lense-decrease"
|
||||
msgstr "Ansicht mit Zoomwerkzeug verkleinern"
|
||||
|
||||
msgid "shortcuts.zoom-lense-increase"
|
||||
msgstr "Ansicht mit Zoomwerkzeug vergrößern"
|
||||
|
||||
msgid "workspace.assets.duplicate-main"
|
||||
msgstr "Hauptkomponente duplizieren"
|
||||
|
||||
@@ -443,10 +443,6 @@ msgstr "Το όνομα χρήστη ή ο κωδικός πρόσβασης φ
|
||||
msgid "errors.wrong-old-password"
|
||||
msgstr "Ο παλιός κωδικός πρόσβασης είναι λάθος "
|
||||
|
||||
#: src/app/main/ui/settings/feedback.cljs
|
||||
msgid "feedback.chat-subtitle"
|
||||
msgstr "Νιώθετε σαν να μιλάτε; Συνομιλήστε μαζί μας στο Gitter"
|
||||
|
||||
#: src/app/main/ui/settings/feedback.cljs
|
||||
msgid "feedback.description"
|
||||
msgstr "Περιγραφή"
|
||||
@@ -538,10 +534,6 @@ msgstr "Πλάτος"
|
||||
msgid "inspect.attributes.shadow"
|
||||
msgstr "Σκιά "
|
||||
|
||||
#: src/app/main/ui/inspect/attributes/shadow.cljs
|
||||
msgid "inspect.attributes.shadow.shorthand.offset-x"
|
||||
msgstr "X"
|
||||
|
||||
#: src/app/main/ui/inspect/attributes/shadow.cljs
|
||||
msgid "inspect.attributes.shadow.shorthand.spread"
|
||||
msgstr "S"
|
||||
@@ -754,9 +746,6 @@ msgstr "Πίσω"
|
||||
msgid "labels.hide-resolved-comments"
|
||||
msgstr "Απόκρυψη επιλυμένων σχολίων"
|
||||
|
||||
msgid "labels.images"
|
||||
msgstr "εικόνες"
|
||||
|
||||
#: src/app/main/ui/static.cljs
|
||||
msgid "labels.internal-error.desc-message"
|
||||
msgstr ""
|
||||
@@ -1161,10 +1150,6 @@ msgstr "Πλήρης οθόνη"
|
||||
msgid "viewer.header.share.copy-link"
|
||||
msgstr "Αντιγραφή link"
|
||||
|
||||
#: src/app/main/ui/viewer/header.cljs
|
||||
msgid "viewer.header.share.placeholder"
|
||||
msgstr "Μοιραστείτε το link θα εμφανιστεί εδώ"
|
||||
|
||||
#: src/app/main/ui/viewer/header.cljs
|
||||
msgid "viewer.header.share.subtitle"
|
||||
msgstr "Όποιος έχει τον link θα έχει πρόσβαση"
|
||||
@@ -1457,15 +1442,9 @@ msgstr "Ενημέρωση"
|
||||
msgid "workspace.libraries.updates"
|
||||
msgstr "ΕΝΗΜΕΡΩΣΕΙΣ"
|
||||
|
||||
msgid "workspace.library.libraries"
|
||||
msgstr "βιβλιοθήκες"
|
||||
|
||||
msgid "workspace.library.store"
|
||||
msgstr "Προκαθορισμένες"
|
||||
|
||||
msgid "workspace.options.blur-options.layer-blur"
|
||||
msgstr "Στρώμα"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs
|
||||
msgid "workspace.options.blur-options.title"
|
||||
msgstr "Θολούρα"
|
||||
@@ -1815,10 +1794,6 @@ msgstr "Για ευθυγράμμιση προς τα δεξιά (%s)"
|
||||
msgid "workspace.options.text-options.align-top"
|
||||
msgstr "Ευθυγραμμίστε την κορυφή"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs
|
||||
msgid "workspace.options.text-options.google"
|
||||
msgstr "Google"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
|
||||
msgid "workspace.options.text-options.grow-auto-height"
|
||||
msgstr "Αυτόματο ύψος"
|
||||
@@ -2165,4 +2140,4 @@ msgid "workspace.updates.update"
|
||||
msgstr "Ενημέρωση"
|
||||
|
||||
msgid "workspace.viewport.click-to-close-path"
|
||||
msgstr "Κάντε κλικ για να κλείσετε τη διαδρομή"
|
||||
msgstr "Κάντε κλικ για να κλείσετε τη διαδρομή"
|
||||
|
||||
@@ -2704,6 +2704,9 @@ msgstr "Snap to pixel grid"
|
||||
msgid "shortcuts.start-editing"
|
||||
msgstr "Start editing"
|
||||
|
||||
msgid "shortcuts.select-parent-layer"
|
||||
msgstr "Select parent layer"
|
||||
|
||||
msgid "shortcuts.start-measure"
|
||||
msgstr "Start measurement"
|
||||
|
||||
@@ -4960,3 +4963,7 @@ msgstr "Marketing"
|
||||
#: src/app/main/ui/onboarding/questions.cljs
|
||||
msgid "questions.student-teacher"
|
||||
msgstr "Student or teacher"
|
||||
|
||||
#: src/app/main/data/common.cljs
|
||||
msgid "notifications.by-code.upgrade-version"
|
||||
msgstr "A new version is available, please refresh the page"
|
||||
|
||||
@@ -2778,6 +2778,9 @@ msgstr "Activar alineación a rejilla de pixel"
|
||||
msgid "shortcuts.start-editing"
|
||||
msgstr "Comenzar edición"
|
||||
|
||||
msgid "shortcuts.select-parent-layer"
|
||||
msgstr "Seleccionar capa padre"
|
||||
|
||||
msgid "shortcuts.start-measure"
|
||||
msgstr "Comenzar medida"
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"PO-Revision-Date: 2023-01-24 14:27+0000\n"
|
||||
"PO-Revision-Date: 2023-07-01 12:52+0000\n"
|
||||
"Last-Translator: Mikel Larreategi <mlarreategi@codesyntax.com>\n"
|
||||
"Language-Team: Basque "
|
||||
"<https://hosted.weblate.org/projects/penpot/frontend/eu/>\n"
|
||||
"Language-Team: Basque <https://hosted.weblate.org/projects/penpot/frontend/"
|
||||
"eu/>\n"
|
||||
"Language: eu\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 4.16-dev\n"
|
||||
"X-Generator: Weblate 5.0-dev\n"
|
||||
|
||||
#: src/app/main/ui/auth/register.cljs
|
||||
msgid "auth.already-have-account"
|
||||
@@ -77,11 +77,11 @@ msgstr "Google"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-with-ldap-submit"
|
||||
msgstr "Sartu LDAP erabiliz"
|
||||
msgstr "LDAP"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-with-oidc-submit"
|
||||
msgstr "OpenID Connect"
|
||||
msgstr "OpenID"
|
||||
|
||||
#: src/app/main/ui/auth/recovery.cljs
|
||||
msgid "auth.new-password"
|
||||
@@ -231,7 +231,7 @@ msgstr "Taldearen kudeaketa"
|
||||
msgid "dasboard.team-hero.text"
|
||||
msgstr ""
|
||||
"Penpot taldeentzat sortuta dago. Gonbidatu beste pertsona batzuk proiektu "
|
||||
"eta fitxategietan batera lan egiteko."
|
||||
"eta fitxategietan batera lan egiteko"
|
||||
|
||||
#: src/app/main/ui/dashboard/projects.cljs
|
||||
msgid "dasboard.team-hero.title"
|
||||
@@ -306,8 +306,8 @@ msgstr "%s fitxategi bizkoiztu"
|
||||
msgid "dashboard.empty-placeholder-drafts"
|
||||
msgstr ""
|
||||
"Oh ez! Oraindik ez duzu fitxategirik! Txantiloi batekin proba egin nahi "
|
||||
"baduzu joan [Liburutegi eta "
|
||||
"txantiloiak](https://penpot.app/libraries-templates.html) atalera"
|
||||
"baduzu joan [Liburutegi eta txantiloiak](https://penpot.app/libraries-"
|
||||
"templates.html) atalera."
|
||||
|
||||
msgid "dashboard.export-binary-multi"
|
||||
msgstr "Deskargatu %s Penpot fitxategi (.penpot)"
|
||||
@@ -412,12 +412,11 @@ msgstr ""
|
||||
#, markdown
|
||||
msgid "dashboard.fonts.hero-text2"
|
||||
msgstr ""
|
||||
"Zurak diren edo Penpoten erabiltzeko lizentzia duzun letra-tipoak bakarrik "
|
||||
"kargatu ditzakezu. Informazio gehiago lortzeko irakurri Edukiaren "
|
||||
"eskubideen atala: [Penpoten erabilpen "
|
||||
"baldintzak](https://penpot.app/terms.html). Letra-tipoen lizentzien "
|
||||
"inguruan irakurtzea ere interesgarria izan daiteke: [letra-tipoen "
|
||||
"lizentziak](https://www.typography.com/faq)."
|
||||
"Zureak diren edo Penpoten erabiltzeko lizentzia duzun letra-tipoak bakarrik "
|
||||
"kargatu ditzakezu. Informazio gehiago lortzeko irakurri Edukiaren eskubideen "
|
||||
"atala: [Penpoten erabilpen baldintzak](https://penpot.app/terms.html). Letra-"
|
||||
"tipoen lizentzien inguruan irakurtzea ere interesgarria izan daiteke: [letra-"
|
||||
"tipoen lizentziak](https://www.typography.com/faq)."
|
||||
|
||||
#: src/app/main/ui/dashboard/fonts.cljs
|
||||
msgid "dashboard.fonts.upload-all"
|
||||
@@ -432,9 +431,6 @@ msgstr "Ezin izan dugu fitxategia inportatu"
|
||||
msgid "dashboard.import.import-error"
|
||||
msgstr "Errorea gertatu da fitxategia inportatzean. Ezin izan da inportatu."
|
||||
|
||||
msgid "dashboard.import.import-message"
|
||||
msgstr "%s fitxategi ondo inportatu dira."
|
||||
|
||||
msgid "dashboard.import.import-warning"
|
||||
msgstr "Fitxategi batzuk inportatu ez diren objektu akasdunak dituzte."
|
||||
|
||||
@@ -515,10 +511,6 @@ msgstr "+ Proiektu berria"
|
||||
msgid "dashboard.new-project-prefix"
|
||||
msgstr "Proiektu berria"
|
||||
|
||||
#: src/app/main/ui/settings/profile.cljs
|
||||
msgid "dashboard.newsletter-title"
|
||||
msgstr "Buletineko harpidetza"
|
||||
|
||||
#: src/app/main/ui/dashboard/search.cljs
|
||||
msgid "dashboard.no-matches-for"
|
||||
msgstr "Ez da \"%s\" aurkitu"
|
||||
@@ -594,18 +586,10 @@ msgstr "Aukeratu gaia"
|
||||
msgid "dashboard.show-all-files"
|
||||
msgstr "Ikusi fitxategi guztiak"
|
||||
|
||||
#: src/app/main/ui/dashboard/file_menu.cljs
|
||||
msgid "dashboard.success-delete-file"
|
||||
msgstr "Zure fitxategia ondo ezabatu da"
|
||||
|
||||
#: src/app/main/ui/dashboard/project_menu.cljs
|
||||
msgid "dashboard.success-delete-project"
|
||||
msgstr "Zure proiektua ondo ezabatu da"
|
||||
|
||||
#: src/app/main/ui/dashboard/file_menu.cljs
|
||||
msgid "dashboard.success-duplicate-file"
|
||||
msgstr "Zure fitxategia ondo bikoiztu da"
|
||||
|
||||
#: src/app/main/ui/dashboard/project_menu.cljs
|
||||
msgid "dashboard.success-duplicate-project"
|
||||
msgstr "Zure proiektua ondo bikoiztu da"
|
||||
@@ -820,10 +804,6 @@ msgstr "Izena edo pasahitza ez dira zuzenak."
|
||||
msgid "errors.wrong-old-password"
|
||||
msgstr "Aurreko pasahitza ez da zuzena"
|
||||
|
||||
#: src/app/main/ui/settings/feedback.cljs
|
||||
msgid "feedback.chat-subtitle"
|
||||
msgstr "Hitz egin nahi duzu? Zatuz gure komunitatearen Gitter txatera"
|
||||
|
||||
#: src/app/main/ui/settings/feedback.cljs
|
||||
msgid "feedback.description"
|
||||
msgstr "Deskribapena"
|
||||
@@ -938,14 +918,6 @@ msgstr "Zabalera"
|
||||
msgid "inspect.attributes.shadow"
|
||||
msgstr "Itzala"
|
||||
|
||||
#: src/app/main/ui/inspect/attributes/shadow.cljs
|
||||
msgid "inspect.attributes.shadow.shorthand.offset-x"
|
||||
msgstr "X"
|
||||
|
||||
#: src/app/main/ui/inspect/attributes/shadow.cljs
|
||||
msgid "inspect.attributes.shadow.shorthand.spread"
|
||||
msgstr "S"
|
||||
|
||||
#: src/app/main/ui/inspect/attributes/stroke.cljs
|
||||
msgid "inspect.attributes.stroke"
|
||||
msgstr "Ertza"
|
||||
@@ -1238,9 +1210,6 @@ msgstr "Laguntza zentroa"
|
||||
msgid "labels.hide-resolved-comments"
|
||||
msgstr "Ezkutatu ebatzitzako iruzkinak"
|
||||
|
||||
msgid "labels.images"
|
||||
msgstr "Irudiak"
|
||||
|
||||
msgid "labels.installed-fonts"
|
||||
msgstr "Instalatutako letra-tipoak"
|
||||
|
||||
@@ -1287,7 +1256,7 @@ msgstr "Pasahitz berria"
|
||||
|
||||
#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/dashboard/comments.cljs
|
||||
msgid "labels.no-comments-available"
|
||||
msgstr "Ez duzu iruzkinen inguruko jakinarazpenik"
|
||||
msgstr "Ez duzu iruzkinen inguruko jakinarazpenik."
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs
|
||||
msgid "labels.no-invitations"
|
||||
@@ -1430,9 +1399,6 @@ msgstr "Erakutsi iruzkinen zerrenda"
|
||||
msgid "labels.show-your-comments"
|
||||
msgstr "Erakutsi zure iruzkinak bakarrik"
|
||||
|
||||
msgid "labels.skip"
|
||||
msgstr "Alde batera utzi"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs
|
||||
msgid "labels.status"
|
||||
msgstr "Egoera"
|
||||
@@ -1758,19 +1724,17 @@ msgstr "Gehitu \"%s\" partekatutako liburutegi bezala"
|
||||
msgid "modals.small-nudge"
|
||||
msgstr "Gutxienekoa"
|
||||
|
||||
#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs
|
||||
msgid "modals.unpublish-shared-confirm.accept"
|
||||
msgstr "Argitaratzea atzera bota"
|
||||
|
||||
#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs
|
||||
msgid "modals.unpublish-shared-confirm.hint"
|
||||
msgid_plural "modals.unpublish-shared-confirm.hint"
|
||||
msgstr[0] ""
|
||||
"Argitaratzea atzera botatzen baduzu, elementuak fitxategiaren liburutegira "
|
||||
"pasatuko dira."
|
||||
"Argitaratzea atzera botatzen baduzu, elementu horiek ez dira beste "
|
||||
"fitxategietan agertuko. Elementuak erabiltzen ari bazara fitxategi honetan "
|
||||
"geldituko dira (diseinurik ez da apurtuko!)."
|
||||
msgstr[1] ""
|
||||
"Argitaratzea atzera botatzen baduzu, elementuak fitxategien liburutegietara "
|
||||
"pasatuko dira."
|
||||
"Argitaratzea atzera botatzen baduzu, elementu horiek ez dira beste "
|
||||
"fitxategietan agertuko. Elementuak erabiltzen ari bazara fitxategi honetan "
|
||||
"geldituko dira (diseinurik ez da apurtuko!)."
|
||||
|
||||
#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs
|
||||
msgid "modals.unpublish-shared-confirm.message"
|
||||
@@ -1833,8 +1797,8 @@ msgstr "Profila ondo gorde da!"
|
||||
#: src/app/main/ui/settings/change_email.cljs
|
||||
msgid "notifications.validation-email-sent"
|
||||
msgstr ""
|
||||
"Posta elektronikoa egiaztatzeko mezua ondo bidali da %s helbidera. "
|
||||
"Egiaztatu zure eposta."
|
||||
"Posta elektronikoa egiaztatzeko mezua ondo bidali da %s helbidera. Egiaztatu "
|
||||
"zure helbidea!"
|
||||
|
||||
msgid "onboarding-v2.before-start.desc1"
|
||||
msgstr ""
|
||||
@@ -1935,15 +1899,6 @@ msgstr "Sortu taldea eta bidali gonbidapenak"
|
||||
msgid "onboarding.choice.team-up.roles"
|
||||
msgstr "Gonbidatu rol honekin:"
|
||||
|
||||
msgid "onboarding.contrib.alt"
|
||||
msgstr "Kode Irekia"
|
||||
|
||||
msgid "onboarding.contrib.desc2.1"
|
||||
msgstr "Hona sar zaitezke"
|
||||
|
||||
msgid "onboarding.contrib.link"
|
||||
msgstr "proiektua githuben"
|
||||
|
||||
msgid "onboarding.newsletter.accept"
|
||||
msgstr "Bai, harpidetu"
|
||||
|
||||
@@ -1958,31 +1913,6 @@ msgstr "Pribatutasun politika."
|
||||
msgid "onboarding.newsletter.title"
|
||||
msgstr "Penpoti buruzko albisteak jaso nahi dituzu?"
|
||||
|
||||
msgid "onboarding.slide.0.desc1"
|
||||
msgstr "Sortu interfaze ederrak taldeko beste kideekin batera."
|
||||
|
||||
msgid "onboarding.slide.0.title"
|
||||
msgstr "Diseinu-, estilo- eta osagai-liburutegiak"
|
||||
|
||||
msgid "onboarding.slide.1.desc1"
|
||||
msgstr "Sortu produktuaren portaera imitatzeko interakzio osoak."
|
||||
|
||||
msgid "onboarding.slide.1.title"
|
||||
msgstr "Eman bizia zure diseinuen interakzioak erabiliz"
|
||||
|
||||
msgid "onboarding.slide.2.desc1"
|
||||
msgstr ""
|
||||
"Taldekide guztiak fitxategi berberen gainean lanean, aldi berean eta "
|
||||
"diseinuen gainean iruzkinak egiteko aukerarekin."
|
||||
|
||||
msgid "onboarding.slide.3.alt"
|
||||
msgstr "Kodearen ezarpenak"
|
||||
|
||||
msgid "onboarding.slide.3.desc2"
|
||||
msgstr ""
|
||||
"Lortu eta eman kode (SVG, HTML) eta estilorako (CSS, Less, Stylus...) "
|
||||
"kodearen zehaztapenak."
|
||||
|
||||
msgid "onboarding.team-modal.create-team"
|
||||
msgstr "Sortu talde bat"
|
||||
|
||||
@@ -2542,14 +2472,6 @@ msgstr "Interakzioak (%s)"
|
||||
msgid "viewer.header.share.copy-link"
|
||||
msgstr "Kopiatu esteka"
|
||||
|
||||
#: src/app/main/ui/viewer/header.cljs
|
||||
msgid "viewer.header.share.placeholder"
|
||||
msgstr "Partekatzeko esteka hemen agertuko da"
|
||||
|
||||
#: src/app/main/ui/viewer/header.cljs
|
||||
msgid "viewer.header.share.subtitle"
|
||||
msgstr "Esteka duen edonor sar daiteke"
|
||||
|
||||
#: src/app/main/ui/viewer/header.cljs
|
||||
msgid "viewer.header.show-interactions"
|
||||
msgstr "Erakutsi interakzioak"
|
||||
@@ -2985,19 +2907,10 @@ msgstr "Eguneratu"
|
||||
msgid "workspace.libraries.updates"
|
||||
msgstr "EGUNERAKETAK"
|
||||
|
||||
msgid "workspace.library.libraries"
|
||||
msgstr "Liburutegiak"
|
||||
|
||||
msgid "workspace.library.store"
|
||||
msgstr "Gorde liburutegiak"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
|
||||
msgid "workspace.options.add-interaction"
|
||||
msgstr "Sakatu + botoia interakzioak gehitzeko."
|
||||
|
||||
msgid "workspace.options.blur-options.layer-blur"
|
||||
msgstr "Geruza"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs
|
||||
msgid "workspace.options.blur-options.title"
|
||||
msgstr "Lausotu"
|
||||
@@ -3633,7 +3546,7 @@ msgstr "Ertz guztiak"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
|
||||
msgid "workspace.options.radius.single-corners"
|
||||
msgstr "Ertz bakarrak"
|
||||
msgstr "Ertz independenteak"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
|
||||
msgid "workspace.options.radius-top-left"
|
||||
@@ -4337,4 +4250,358 @@ msgid "workspace.updates.update"
|
||||
msgstr "Eguneratu"
|
||||
|
||||
msgid "workspace.viewport.click-to-close-path"
|
||||
msgstr "Egin klik bidea ixteko"
|
||||
msgstr "Egin klik bidea ixteko"
|
||||
|
||||
msgid "errors.webhooks.invalid-uri"
|
||||
msgstr "URLak ez du balidazioa gainditu."
|
||||
|
||||
msgid "dashboard.webhooks.content-type"
|
||||
msgstr "Elementu mota"
|
||||
|
||||
#, markdown
|
||||
msgid "dashboard.fonts.warning-text"
|
||||
msgstr ""
|
||||
"Zure letra-tipoek sistema eragile desberdinetan metrika bertikalekin arazoak "
|
||||
"izan ditzaketela detektatu dugu. Zure letra-tipoa nola ikusten den zerbitzu "
|
||||
"[hau](https://vertical-metrics.netlify.app) erabiliz egiaztatu dezakezu. "
|
||||
"Gainera, weberako letra-tipoak sortzeko [Transfonter](https://transfonter."
|
||||
"org/) erabiltzea gomendatzen dugu. "
|
||||
|
||||
msgid "dashboard.webhooks.active"
|
||||
msgstr "Aktibo"
|
||||
|
||||
msgid "dashboard.webhooks.description"
|
||||
msgstr ""
|
||||
"Webhookak beste webgune batzuei Penpoten zerbait gertatu dela jakinarazteko "
|
||||
"modu bat dira. Adierazitako URLtara POST eskaera bat bidaliko dugu."
|
||||
|
||||
#: src/app/main/errors.cljs
|
||||
msgid "errors.feature-not-supported"
|
||||
msgstr "Ezaugarria ezin da erabili: '%s'."
|
||||
|
||||
msgid "errors.webhooks.unexpected-status"
|
||||
msgstr "Espero ez zen egoera %s"
|
||||
|
||||
#: src/app/main/errors.cljs
|
||||
msgid "errors.max-quote-reached"
|
||||
msgstr ""
|
||||
"Kuotaren maximora heldu zara: '%s'. Jarri kontaktuan laguntza zerbitzuarekin."
|
||||
|
||||
msgid "errors.webhooks.ssl-validation"
|
||||
msgstr "Errorea gertatu da SSL balidazioan."
|
||||
|
||||
msgid "errors.webhooks.unexpected"
|
||||
msgstr "Errore ezezaguna balidazioan"
|
||||
|
||||
#: src/app/main/ui/inspect/attributes/layout.cljs
|
||||
msgid "inspect.attributes.size"
|
||||
msgstr "Tamaina eta posizioa"
|
||||
|
||||
#: src/app/main/ui/inspect/attributes/text.cljs
|
||||
msgid "inspect.attributes.typography.font-weight"
|
||||
msgstr "Letra tipoaren lodiera"
|
||||
|
||||
msgid "inspect.empty.help"
|
||||
msgstr ""
|
||||
"Diseinua ikuskatzeari buruz gehiago jakin nahi baduzu zoaz Penpoten laguntza "
|
||||
"zentrora"
|
||||
|
||||
msgid "inspect.empty.more-info"
|
||||
msgstr "Informazio gehiago ikuskatzeari buruz"
|
||||
|
||||
msgid "inspect.empty.select"
|
||||
msgstr ""
|
||||
"Aukeratu forma bat, taula bat edo talde bat beren propietateak eta kodea "
|
||||
"ikuskatzeko"
|
||||
|
||||
msgid "labels.view-only"
|
||||
msgstr "IKUSTEKO BAKARRIK"
|
||||
|
||||
msgid "modals.create-webhook.url.label"
|
||||
msgstr "Informazioaren URLa"
|
||||
|
||||
#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs
|
||||
msgid "modals.delete-shared-confirm.hint"
|
||||
msgid_plural "modals.delete-shared-confirm.hint"
|
||||
msgstr[0] ""
|
||||
"Ezabatzen baduzu, bere elementuak ezingo dira beste fitxategietan erabili. "
|
||||
"Jada erabiltzen ari zaren elementuak fitxategi honetan geldituko dira (ez da "
|
||||
"diseinurik apurtuko!)."
|
||||
msgstr[1] ""
|
||||
"Ezabatzen badituzu, bere elementuak ezingo dira beste fitxategietan erabili. "
|
||||
"Jada erabiltzen ari zaren elementuak fitxategi honetan geldituko dira (ez da "
|
||||
"diseinurik apurtuko!)."
|
||||
|
||||
msgid "labels.active"
|
||||
msgstr "Aktibo"
|
||||
|
||||
#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs
|
||||
msgid "modals.delete-shared-confirm.hint-many"
|
||||
msgid_plural "modals.delete-shared-confirm.hint-many"
|
||||
msgstr[0] ""
|
||||
"Ezabatzen baduzu, bere elementuak ezingo dira beste fitxategietan erabili. "
|
||||
"Jada erabiltzen ari zaren elementuak fitxategi honetan geldituko dira (ez da "
|
||||
"diseinurik apurtuko!)."
|
||||
msgstr[1] ""
|
||||
"Ezabatzen badituzu, bere elementuak ezingo dira beste fitxategietan erabili. "
|
||||
"Jada erabiltzen ari zaren elementuak fitxategi honetan geldituko dira (ez da "
|
||||
"diseinurik apurtuko!)."
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs
|
||||
msgid "labels.copy-invitation-link"
|
||||
msgstr "Kopiatu esteka"
|
||||
|
||||
#: src/app/main/ui/workspace.cljs
|
||||
msgid "labels.reload-file"
|
||||
msgstr "Birkargatu fitxategia"
|
||||
|
||||
msgid "labels.inactive"
|
||||
msgstr "Inaktibo"
|
||||
|
||||
#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs
|
||||
msgid "modals.unpublish-shared-confirm.hint-many"
|
||||
msgid_plural "modals.unpublish-shared-confirm.hint-many"
|
||||
msgstr[0] ""
|
||||
"Argitaratzea atzera botatzen baduzu, elementu horiek ezingo dira beste "
|
||||
"fitxategietan erabili. Jada erabiltzen diren elementuak fitxategi horietan "
|
||||
"geldituko dira (diseinurik ez da apurtuko!)."
|
||||
msgstr[1] ""
|
||||
"Argitaratzea atzera botatzen baduzu, elementu horiek ezingo dira beste "
|
||||
"fitxategietan erabili. Jada erabiltzen diren elementuak fitxategi horietan "
|
||||
"geldituko dira (diseinurik ez da apurtuko!)."
|
||||
|
||||
msgid "shortcuts.align-center"
|
||||
msgstr "Erdian lerrokatu"
|
||||
|
||||
msgid "shortcuts.toggle-layout-flex"
|
||||
msgstr "Gehitu/kendu flex diseinua"
|
||||
|
||||
#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs
|
||||
msgid "modals.unpublish-shared-confirm.scd-message-many"
|
||||
msgid_plural "modals.unpublish-shared-confirm.scd-message-many"
|
||||
msgstr[0] "Liburutegi honetako elementuak hemen ari dira erabiltzen:"
|
||||
msgstr[1] "Liburutegi hauetako elementuak hemen ari dira erabiltzen:"
|
||||
|
||||
msgid "shortcut-subsection.text-editor"
|
||||
msgstr "Testuak"
|
||||
|
||||
msgid "shortcuts.italic"
|
||||
msgstr "Aktibatu/desaktibatu etzana"
|
||||
|
||||
msgid "shortcuts.letter-spacing-dec"
|
||||
msgstr "Hizkien arteko espazioa txikitu"
|
||||
|
||||
msgid "shortcuts.letter-spacing-inc"
|
||||
msgstr "Hizkien arteko espazioa handitu"
|
||||
|
||||
msgid "workspace.header.menu.redo"
|
||||
msgstr "Berregin"
|
||||
|
||||
msgid "shortcuts.select-next"
|
||||
msgstr "Aukeratu hurrengo geruza"
|
||||
|
||||
msgid "workspace.assets.duplicate-main"
|
||||
msgstr "Bikoiztu nagusia"
|
||||
|
||||
msgid "shortcuts.zoom-lense-increase"
|
||||
msgstr "Zooma handitu"
|
||||
|
||||
msgid "workspace.header.menu.disable-scale-content"
|
||||
msgstr "Desaktibatu eskala proportzionala"
|
||||
|
||||
msgid "workspace.header.menu.undo"
|
||||
msgstr "Desegin"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs
|
||||
msgid "workspace.options.interaction-relative-to"
|
||||
msgstr "Honekiko erlatiboa"
|
||||
|
||||
#: src/app/main/ui/workspace.cljs
|
||||
msgid "workspace.remove-graphics.error-hint"
|
||||
msgstr ""
|
||||
"Berriz saiatzeko, fitxategi hau berriz kargatu dezakezu. Hala ere arazoa "
|
||||
"izaten jarraitzen baduzu, begiratu zerrenda eta ezabatu apurtutako grafikoak."
|
||||
|
||||
#: src/app/main/ui/workspace.cljs
|
||||
msgid "workspace.remove-graphics.text1"
|
||||
msgstr ""
|
||||
"Liburutegiko grafikoak osagaiak izango dira orain, horrek ahaltsuago egingo "
|
||||
"ditu."
|
||||
|
||||
#: src/app/main/ui/workspace/context_menu.cljs
|
||||
msgid "workspace.shape.menu.remove-flex"
|
||||
msgstr "Ezabatu flex diseinua"
|
||||
|
||||
msgid "labels.webhooks"
|
||||
msgstr "Webhookak"
|
||||
|
||||
msgid "title.team-webhooks"
|
||||
msgstr "Webhookak - %s - Penpot"
|
||||
|
||||
msgid "webhooks.last-delivery.success"
|
||||
msgstr "Azken bidalketa ondo joan da."
|
||||
|
||||
msgid "dashboard.webhooks.active.explain"
|
||||
msgstr "Webhook hau aktibatzen denean gertaeraren xehetasunak bidaliko dira"
|
||||
|
||||
msgid "dashboard.webhooks.create"
|
||||
msgstr "Sortu webhooka"
|
||||
|
||||
msgid "dashboard.webhooks.create.success"
|
||||
msgstr "Webhooka ondo sortu da."
|
||||
|
||||
msgid "dashboard.webhooks.empty.add-one"
|
||||
msgstr "Sakatu \"Sortu webhooka\" botoia bat gehitzeko."
|
||||
|
||||
msgid "dashboard.webhooks.empty.no-webhooks"
|
||||
msgstr "Ez dago webhookik."
|
||||
|
||||
msgid "dashboard.webhooks.update.success"
|
||||
msgstr "Webhooka ondo aldatu da."
|
||||
|
||||
#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs, src/app/main/ui/auth/recovery_request.cljs
|
||||
msgid "errors.email-invalid"
|
||||
msgstr "Mesedez, idatzi eposta helbide zuzen bat"
|
||||
|
||||
#: src/app/main/errors.cljs
|
||||
msgid "errors.feature-mismatch"
|
||||
msgstr ""
|
||||
"Badirudi '%s' ezaugarria aktibo duen fitxategi bat irekitzen ari zarela "
|
||||
"baina zure penpot frontendak ezin du hori egin edo ezaugarri hori "
|
||||
"desaktibatuta du."
|
||||
|
||||
#: src/app/main/ui/dashboard/file_menu.cljs
|
||||
msgid "labels.unpublish-multi-files"
|
||||
msgstr "%s fitxategi argitaratzeari utzi"
|
||||
|
||||
msgid "modals.create-webhook.submit-label"
|
||||
msgstr "Sortu webhooka"
|
||||
|
||||
msgid "modals.create-webhook.title"
|
||||
msgstr "Sortu webhooka"
|
||||
|
||||
msgid "modals.create-webhook.url.placeholder"
|
||||
msgstr "https://example.com/postreceive"
|
||||
|
||||
#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs
|
||||
msgid "modals.delete-shared-confirm.no-files-message"
|
||||
msgid_plural "modals.delete-shared-confirm.no-files-message"
|
||||
msgstr[0] ""
|
||||
"Fitxategi honetako liburutegiko elementuak ez dira inon erabiltzen. "
|
||||
"Fitxategiarekin batera ezabatuko dira."
|
||||
msgstr[1] ""
|
||||
"Fitxategi hautetako liburutegiko elementuak ez dira inon erabiltzen. "
|
||||
"Fitxategiarekin batera ezabatuko dira."
|
||||
|
||||
#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs
|
||||
msgid "modals.delete-shared-confirm.scd-message-many"
|
||||
msgid_plural "modals.delete-shared-confirm.scd-message-many"
|
||||
msgstr[0] "Fitxategi honen liburutegiko elementu batzuk hemen erabiltzen ari zara:"
|
||||
msgstr[1] ""
|
||||
"Fitxategi hauen liburutegietako elementu batzuk hemen erabiltzen ari zara:"
|
||||
|
||||
msgid "modals.delete-webhook.accept"
|
||||
msgstr "Ezabatu webhooka"
|
||||
|
||||
msgid "modals.delete-webhook.message"
|
||||
msgstr "Benetan webhook hau ezabatu egin nahi duzu?"
|
||||
|
||||
msgid "modals.delete-webhook.title"
|
||||
msgstr "Webhooka ezabatzen"
|
||||
|
||||
msgid "modals.edit-webhook.submit-label"
|
||||
msgstr "Aldatu webhooka"
|
||||
|
||||
msgid "modals.edit-webhook.title"
|
||||
msgstr "Aldatu webhooka"
|
||||
|
||||
msgid "modals.invite-member.repeated-invitation"
|
||||
msgstr ""
|
||||
"Eposta helbide batzuk jada taldekideenak dira. Ez da gonbidapenik bidaliko."
|
||||
|
||||
#: src/app/main/ui/workspace/header.cljs, src/app/main/ui/dashboard/file_menu.cljs
|
||||
msgid "modals.unpublish-shared-confirm.no-files-message"
|
||||
msgid_plural "modals.unpublish-shared-confirm.no-files-message"
|
||||
msgstr[0] "Liburutegi honetako elementuak ez dira erabiltzen ari."
|
||||
msgstr[1] "Liburutegi hauetako elementuak ez dira erabiltzen ari."
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs
|
||||
msgid "notifications.invitation-link-copied"
|
||||
msgstr "Gonbidapenaren esteka kopiatu da"
|
||||
|
||||
msgid "shortcuts.align-justify"
|
||||
msgstr "Justifikatuta lerrokatu"
|
||||
|
||||
msgid "shortcuts.bold"
|
||||
msgstr "Aktibatu/desaktibatu beltza"
|
||||
|
||||
msgid "shortcuts.font-size-dec"
|
||||
msgstr "Letra tipoaren tamaina txikitu"
|
||||
|
||||
msgid "shortcuts.font-size-inc"
|
||||
msgstr "Letra tipoaren tamaina handitu"
|
||||
|
||||
msgid "shortcuts.line-height-dec"
|
||||
msgstr "Lerroen arteko tartea txikitu"
|
||||
|
||||
msgid "shortcuts.line-height-inc"
|
||||
msgstr "Lerroen arteko tartea handitu"
|
||||
|
||||
msgid "shortcuts.line-through"
|
||||
msgstr "Aktibatu/desaktibatu marratzea"
|
||||
|
||||
msgid "shortcuts.open-inspect"
|
||||
msgstr "Ikuskagailura joan"
|
||||
|
||||
msgid "shortcuts.select-prev"
|
||||
msgstr "Aukeratu aurreko geruza"
|
||||
|
||||
msgid "shortcuts.underline"
|
||||
msgstr "Aktibatu/desaktibatu azpimarraketa"
|
||||
|
||||
msgid "shortcuts.zoom-lense-decrease"
|
||||
msgstr "Zooma txikitu"
|
||||
|
||||
msgid "viewer.header.inspect-section"
|
||||
msgstr "Ikuskagailua (%s)"
|
||||
|
||||
msgid "workspace.assets.typography.text-styles"
|
||||
msgstr "Testuen estiloak"
|
||||
|
||||
msgid "workspace.header.menu.enable-scale-content"
|
||||
msgstr "Aktibatu eskala proportzionala"
|
||||
|
||||
msgid "workspace.options.inspect"
|
||||
msgstr "Ikuskatu"
|
||||
|
||||
msgid "workspace.options.interaction-auto"
|
||||
msgstr "automatikoa"
|
||||
|
||||
#: src/app/main/ui/workspace.cljs
|
||||
msgid "workspace.remove-graphics.error-msg"
|
||||
msgstr "Grafiko batzuk ezin izan dira eguneratu."
|
||||
|
||||
#: src/app/main/ui/workspace.cljs
|
||||
msgid "workspace.remove-graphics.progress"
|
||||
msgstr "Bihurtzen %s/%s"
|
||||
|
||||
#: src/app/main/ui/workspace.cljs
|
||||
msgid "workspace.remove-graphics.text2"
|
||||
msgstr "Eguneraketa hau behin bakarrik gertatuko da."
|
||||
|
||||
#: src/app/main/ui/workspace.cljs
|
||||
msgid "workspace.remove-graphics.title"
|
||||
msgstr "Eguneratzen %s..."
|
||||
|
||||
#: src/app/main/ui/workspace/context_menu.cljs
|
||||
msgid "workspace.shape.menu.add-flex"
|
||||
msgstr "Gehitu flex diseinua"
|
||||
|
||||
msgid "errors.webhooks.last-delivery"
|
||||
msgstr "Errore bat gertatu da azken bidalketan."
|
||||
|
||||
msgid "errors.webhooks.timeout"
|
||||
msgstr "Denbora muga gainditu da"
|
||||
|
||||
msgid "errors.webhooks.connection"
|
||||
msgstr "Konexio errorea, URLa ezin da ireki"
|
||||
|
||||
@@ -519,10 +519,6 @@ msgstr "+ پروژه جدید"
|
||||
msgid "dashboard.new-project-prefix"
|
||||
msgstr "پروژه جدید"
|
||||
|
||||
#: src/app/main/ui/settings/profile.cljs
|
||||
msgid "dashboard.newsletter-title"
|
||||
msgstr "اشتراک خبرنامه"
|
||||
|
||||
#: src/app/main/ui/dashboard/search.cljs
|
||||
#, fuzzy
|
||||
msgid "dashboard.no-matches-for"
|
||||
@@ -866,10 +862,6 @@ msgstr "حساب پشتیبانی در توییتر"
|
||||
msgid "generic.error"
|
||||
msgstr "خطایی رخ داده است"
|
||||
|
||||
#, fuzzy
|
||||
msgid "handoff.attributes.typography.text-transform.titlecase"
|
||||
msgstr "مورد عنوان"
|
||||
|
||||
#: src/app/main/ui/inspect/attributes/blur.cljs
|
||||
msgid "inspect.attributes.blur"
|
||||
msgstr "محو"
|
||||
@@ -938,10 +930,6 @@ msgstr "عرض"
|
||||
msgid "inspect.attributes.shadow"
|
||||
msgstr "سایه"
|
||||
|
||||
#: src/app/main/ui/inspect/attributes/shadow.cljs
|
||||
msgid "inspect.attributes.shadow.shorthand.offset-x"
|
||||
msgstr "X"
|
||||
|
||||
#: src/app/main/ui/inspect/attributes/shadow.cljs
|
||||
msgid "inspect.attributes.shadow.shorthand.spread"
|
||||
msgstr "S"
|
||||
@@ -1232,9 +1220,6 @@ msgstr "مرکز کمک"
|
||||
msgid "labels.hide-resolved-comments"
|
||||
msgstr "پنهان کردن نظرات حل شده"
|
||||
|
||||
msgid "labels.images"
|
||||
msgstr "تصاویر"
|
||||
|
||||
msgid "labels.installed-fonts"
|
||||
msgstr "فونتهای نصبشده"
|
||||
|
||||
@@ -1427,9 +1412,6 @@ msgstr "نمایش لیست نظرات"
|
||||
msgid "labels.show-your-comments"
|
||||
msgstr "فقط نظرات خودتان را نشان دهید"
|
||||
|
||||
msgid "labels.skip"
|
||||
msgstr "رد"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs
|
||||
msgid "labels.status"
|
||||
msgstr "وضعیت"
|
||||
@@ -1987,10 +1969,6 @@ msgstr "تعاملات (%s)"
|
||||
msgid "viewer.header.share.copy-link"
|
||||
msgstr "کپی کردن لینک"
|
||||
|
||||
#: src/app/main/ui/viewer/header.cljs
|
||||
msgid "viewer.header.share.placeholder"
|
||||
msgstr "لینک اشتراکگذاری در اینجا ظاهر میشود"
|
||||
|
||||
#: src/app/main/ui/viewer/header.cljs
|
||||
msgid "viewer.header.share.subtitle"
|
||||
msgstr "هر کسی که لینک را داشته باشد دسترسی خواهد داشت"
|
||||
@@ -2212,9 +2190,6 @@ msgstr "بهروزرسانی"
|
||||
msgid "workspace.libraries.updates"
|
||||
msgstr "بهروزرسانیها"
|
||||
|
||||
msgid "workspace.options.blur-options.background-blur"
|
||||
msgstr "پسزمینه"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs
|
||||
msgid "workspace.options.blur-options.title"
|
||||
msgstr "محو"
|
||||
@@ -3050,4 +3025,4 @@ msgid "workspace.updates.update"
|
||||
msgstr "بهروزرسانی"
|
||||
|
||||
msgid "workspace.viewport.click-to-close-path"
|
||||
msgstr "برای بستن مسیر کلیک کنید"
|
||||
msgstr "برای بستن مسیر کلیک کنید"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user