0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-13 16:21:57 -05:00

Merge pull request #1449 from penpot/bugfixes

Bugfixes
This commit is contained in:
Andrey Antukh 2022-01-10 15:36:59 +01:00 committed by GitHub
commit 0eb2336bc6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 648 additions and 327 deletions

View file

@ -32,6 +32,14 @@
- After team onboarding importing a file will import into the team drafts [Taiga #2408](https://tree.taiga.io/project/penpot/issue/2408)
- Fix problem exporting shapes from handoff mode [Taiga #2386](https://tree.taiga.io/project/penpot/issue/2386)
- Fix lock/hide elements in context menu when multiples shapes selected [Taiga #2340](https://tree.taiga.io/project/penpot/issue/2340)
- Fix problem with booleans [Taiga #2356](https://tree.taiga.io/project/penpot/issue/2356)
- Fix line-height/letter-spacing inputs behaviour [Taiga #2331](https://tree.taiga.io/project/penpot/issue/2331)
- Fix dotted style in strokes [Taiga #2312](https://tree.taiga.io/project/penpot/issue/2312)
- Fix problem when resizing texts inside groups [Taiga #2310](https://tree.taiga.io/project/penpot/issue/2310)
- Fix problem with multiple exports [Taiga #2468](https://tree.taiga.io/project/penpot/issue/2468)
- Allow import to continue from recoverable failures [#1412](https://github.com/penpot/penpot/issues/1412)
- Improved behaviour on text options when not text is selected [Taiga #2390](https://tree.taiga.io/project/penpot/issue/2390)
- Fix decimal numbers in export viewbox [Taiga #2290](https://tree.taiga.io/project/penpot/issue/2290)
### :arrow_up: Deps updates
### :heart: Community contributions by (Thank you!)
@ -75,8 +83,6 @@
### :arrow_up: Deps updates
- Update log4j2 dependency.
>>>>>>> main
# 1.10.2-beta
@ -134,6 +140,8 @@
- Add placeholder to create shareable link
- Fix project files count not refreshing correctly after import [Taiga #2216](https://tree.taiga.io/project/penpot/issue/2216)
- Remove button after import process finish [Taiga #2215](https://tree.taiga.io/project/penpot/issue/2215)
- Fix problem with styles in the viewer [Taiga #2467](https://tree.taiga.io/project/penpot/issue/2467)
- Fix default state in viewer [Taiga #2465](https://tree.taiga.io/project/penpot/issue/2465)
### :heart: Community contributions by (Thank you!)

View file

@ -12,7 +12,6 @@
[app.common.geom.shapes :as gsh]
[app.common.pages.changes :as ch]
[app.common.pages.init :as init]
[app.common.pages.spec :as spec]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[cuerdas.core :as str]))
@ -21,15 +20,14 @@
(def conjv (fnil conj []))
(def conjs (fnil conj #{}))
;; This flag controls if we should execute spec validation after every commit
(def verify-on-commit? true)
(defn- commit-change
([file change]
(commit-change file change nil))
([file change {:keys [add-container?]
:or {add-container? false}}]
([file change {:keys [add-container?
fail-on-spec?]
:or {add-container? false
fail-on-spec? false}}]
(let [component-id (:current-component-id file)
change (cond-> change
(and add-container? (some? component-id))
@ -39,11 +37,20 @@
(assoc :page-id (:current-page-id file)
:frame-id (:current-frame-id file)))]
(when verify-on-commit?
(us/assert ::spec/change change))
(-> file
(update :changes conjv change)
(update :data ch/process-changes [change] verify-on-commit?)))))
(when fail-on-spec?
(us/verify :app.common.pages.spec/change change))
(let [valid? (us/valid? :app.common.pages.spec/change change)]
#?(:cljs
(when-not valid? (.warn js/console "Invalid shape" (clj->js change))))
(cond-> file
valid?
(-> (update :changes conjv change)
(update :data ch/process-changes [change] false))
(not valid?)
(update :errors conjv change))))))
(defn- lookup-objects
([file]
@ -56,15 +63,16 @@
(get shape-id)))
(defn- commit-shape [file obj]
(let [parent-id (-> file :parent-stack peek)]
(-> file
(commit-change
{:type :add-obj
:id (:id obj)
:obj obj
:parent-id parent-id}
(let [parent-id (-> file :parent-stack peek)
change {:type :add-obj
:id (:id obj)
:obj obj
:parent-id parent-id}
{:add-container? true}))))
fail-on-spec? (or (= :group (:type obj))
(= :frame (:type obj)))]
(commit-change file change {:add-container? true :fail-on-spec? fail-on-spec?})))
(defn setup-rect-selrect [obj]
(let [rect (select-keys obj [:x :y :width :height])
@ -421,35 +429,36 @@
(defn add-interaction
[file from-id interaction-src]
(assert (some? (lookup-shape file from-id)) (str "Cannot locate shape with id " from-id))
(let [shape (lookup-shape file from-id)]
(if (nil? shape)
file
(let [{:keys [event-type action-type]} (read-classifier interaction-src)
{:keys [delay]} (read-event-opts interaction-src)
{:keys [destination overlay-pos-type overlay-position url
close-click-outside background-overlay preserve-scroll]}
(read-action-opts interaction-src)
(let [{:keys [event-type action-type]} (read-classifier interaction-src)
{:keys [delay]} (read-event-opts interaction-src)
{:keys [destination overlay-pos-type overlay-position url
close-click-outside background-overlay preserve-scroll]}
(read-action-opts interaction-src)
interactions (-> shape
:interactions
(conjv
(d/without-nils {:event-type event-type
:action-type action-type
:delay delay
:destination destination
:overlay-pos-type overlay-pos-type
:overlay-position overlay-position
:url url
:close-click-outside close-click-outside
:background-overlay background-overlay
:preserve-scroll preserve-scroll})))]
(commit-change
file
{:type :mod-obj
:page-id (:current-page-id file)
:id from-id
interactions (-> (lookup-shape file from-id)
:interactions
(conjv
(d/without-nils {:event-type event-type
:action-type action-type
:delay delay
:destination destination
:overlay-pos-type overlay-pos-type
:overlay-position overlay-position
:url url
:close-click-outside close-click-outside
:background-overlay background-overlay
:preserve-scroll preserve-scroll})))]
(commit-change
file
{:type :mod-obj
:page-id (:current-page-id file)
:id from-id
:operations
[{:type :set :attr :interactions :val interactions}]})))
:operations
[{:type :set :attr :interactions :val interactions}]})))))
(defn generate-changes
[file]

View file

@ -451,9 +451,6 @@
rt-modif (:rotation modifiers)]
(cond-> (gmt/matrix)
(some? displacement)
(gmt/multiply displacement)
(some? resize-1)
(-> (gmt/translate origin-1)
(gmt/multiply resize-transform)
@ -468,6 +465,9 @@
(gmt/multiply resize-transform-inverse)
(gmt/translate (gpt/negate origin-2)))
(some? displacement)
(gmt/multiply displacement)
(some? rt-modif)
(-> (gmt/translate center)
(gmt/multiply (gmt/rotate-matrix rt-modif))

View file

@ -212,6 +212,27 @@
(d/seek overlap-single?)
(some?))))
(defn fix-move-to
[content]
;; Remove the field `:prev` and makes the necessaries `move-to`
;; then clean the subpaths
(loop [current (first content)
content (rest content)
prev nil
result []]
(if (nil? current)
result
(let [result (if (not= (:prev current) prev)
(conj result (upc/make-move-to (:prev current)))
result)]
(recur (first content)
(rest content)
(gsp/command->point current)
(conj result (dissoc current :prev)))))))
(defn create-union [content-a content-a-split content-b content-b-split sr-a sr-b]
;; Pick all segments in content-a that are not inside content-b
;; Pick all segments in content-b that are not inside content-a
@ -225,7 +246,7 @@
content-geom (gsp/content->geom-data content)
content-sr (gsp/content->selrect content)
content-sr (gsp/content->selrect (fix-move-to content))
;; Overlapping segments should be added when they are part of the border
border-content
@ -265,36 +286,22 @@
;; Pick all segments
(d/concat-vec content-a content-b))
(defn fix-move-to
[content]
;; Remove the field `:prev` and makes the necessaries `move-to`
;; then clean the subpaths
(loop [current (first content)
content (rest content)
prev nil
result []]
(if (nil? current)
result
(let [result (if (not= (:prev current) prev)
(conj result (upc/make-move-to (:prev current)))
result)]
(recur (first content)
(rest content)
(gsp/command->point current)
(conj result (dissoc current :prev)))))))
(defn content-bool-pair
[bool-type content-a content-b]
(let [content-a (-> content-a (close-paths) (add-previous))
(let [;; We need to reverse the second path when making a difference/intersection/exclude
;; and both shapes are in the same direction
should-reverse? (and (not= :union bool-type)
(= (ups/clockwise? content-b)
(ups/clockwise? content-a)))
content-a (-> content-a
(close-paths)
(add-previous))
content-b (-> content-b
(close-paths)
(cond-> (ups/clockwise? content-b)
(ups/reverse-content))
(cond-> should-reverse? (ups/reverse-content))
(add-previous))
sr-a (gsp/content->selrect content-a)

View file

@ -31,6 +31,8 @@
(def max-safe-int (int 1e6))
(def min-safe-int (int -1e6))
(def valid? s/valid?)
;; --- Conformers
(defn uuid-conformer
@ -216,8 +218,11 @@
:code :spec-validation
:hint hint
:ctx ctx
:value val
::s/problems (::s/problems data)))))
(defmacro assert
"Development only assertion macro."
[spec x]

View file

@ -80,9 +80,11 @@
(p/then (fn [results]
(reduce #(zip/add! %1 (:filename %2) (:content %2)) (zip/create) results)))
(p/then (fn [fzip]
(.generateAsync ^js fzip #js {:type "uint8array"})))
(p/then (fn [data]
{:status 200
:headers {"content-type" "application/zip"}
:body (.generateNodeStream ^js fzip)})))))
:body data})))))
(defn- perform-export
[params]

View file

@ -322,6 +322,10 @@
fill: $color-success;
}
.icon-msg-warning {
fill: $color-warning;
}
.icon-close {
transform: rotate(45deg);
fill: $color-danger;
@ -392,6 +396,13 @@
fill: $color-white;
}
}
&.warning {
background: $color-warning-lighter;
.icon {
background: $color-warning;
}
}
}
.error-message {

View file

@ -25,7 +25,6 @@
grid-template-columns: 1fr;
justify-items: center;
align-items: center;
overflow: hidden;
.empty-state {
justify-content: center;

View file

@ -29,11 +29,13 @@
[app.main.ui.shapes.text :as text]
[app.main.ui.shapes.text.fontfaces :as ff]
[app.util.object :as obj]
[app.util.strings :as ust]
[app.util.timers :as ts]
[cuerdas.core :as str]
[debug :refer [debug?]]
[rumext.alpha :as mf]))
(def ^:const viewbox-decimal-precision 3)
(def ^:private default-color clr/canvas)
(mf/defc background
@ -139,8 +141,13 @@
;; Don't wrap svg elements inside a <g> otherwise some can break
[:> svg-raw-wrapper {:shape shape :frame frame}]))))))
(defn get-viewbox [{:keys [x y width height] :or {x 0 y 0 width 100 height 100}}]
(str/fmt "%s %s %s %s" x y width height))
(defn format-viewbox
"Format a viewbox given a rectangle"
[{:keys [x y width height] :or {x 0 y 0 width 100 height 100}}]
(str/join
" "
(->> [x y width height]
(map #(ust/format-precision % viewbox-decimal-precision)))))
(mf/defc page-svg
{::mf/wrap [mf/memo]}
@ -160,7 +167,7 @@
vport (when (and (some? width) (some? height))
{:width width :height height})
dim (calculate-dimensions data vport)
vbox (get-viewbox dim)
vbox (format-viewbox dim)
background-color (get-in data [:options :background] default-color)
frame-wrapper
(mf/use-memo
@ -221,15 +228,15 @@
width (* (:width frame) zoom)
height (* (:height frame) zoom)
vbox (str "0 0 " (:width frame 0)
" " (:height frame 0))
vbox (format-viewbox {:width (:width frame 0) :height (:height frame 0)})
wrapper (mf/use-memo
(mf/deps objects)
#(frame-wrapper-factory objects))]
[:svg {:view-box vbox
:width width
:height height
:width (ust/format-precision width viewbox-decimal-precision)
:height (ust/format-precision height viewbox-decimal-precision)
:version "1.1"
:xmlns "http://www.w3.org/2000/svg"
:xmlnsXlink "http://www.w3.org/1999/xlink"
@ -255,18 +262,20 @@
group (get objects group-id)
width (* (:width group) zoom)
height (* (:height group) zoom)
vbox (str "0 0 " (:width group 0)
" " (:height group 0))
vbox (format-viewbox {:width (:width group 0)
:height (:height group 0)})
group-wrapper
(mf/use-memo
(mf/deps objects)
#(group-wrapper-factory objects))]
[:svg {:view-box vbox
:width width
:height height
:width (ust/format-precision width viewbox-decimal-precision)
:height (ust/format-precision height viewbox-decimal-precision)
:version "1.1"
:xmlns "http://www.w3.org/2000/svg"
:xmlnsXlink "http://www.w3.org/1999/xlink"
@ -281,7 +290,7 @@
root (get objects id)
{:keys [width height]} (:selrect root)
vbox (str "0 0 " width " " height)
vbox (format-viewbox {:width width :height height})
modifier (-> (gpt/point (:x root) (:y root))
(gpt/negate)

View file

@ -13,6 +13,7 @@
[app.util.keyboard :as kbd]
[app.util.object :as obj]
[app.util.simple-math :as sm]
[app.util.strings :as ust]
[rumext.alpha :as mf]))
(defn num? [val]
@ -24,13 +25,16 @@
{::mf/wrap-props false
::mf/forward-ref true}
[props external-ref]
(let [value-str (obj/get props "value")
min-val-str (obj/get props "min")
max-val-str (obj/get props "max")
wrap-value? (obj/get props "data-wrap")
on-change (obj/get props "onChange")
title (obj/get props "title")
default-val (obj/get props "default" 0)
(let [value-str (obj/get props "value")
min-val-str (obj/get props "min")
max-val-str (obj/get props "max")
step-val-str (obj/get props "step")
wrap-value? (obj/get props "data-wrap")
on-change (obj/get props "onChange")
on-blur (obj/get props "onBlur")
title (obj/get props "title")
default-val (obj/get props "default" 0)
precision (obj/get props "precision")
;; We need a ref pointing to the input dom element, but the user
;; of this component may provide one (that is forwarded here).
@ -56,6 +60,15 @@
(string? max-val-str)
(d/parse-integer max-val-str))
step-val (cond
(number? step-val-str)
step-val-str
(string? step-val-str)
(d/parse-integer step-val-str)
:else 1)
parse-value
(mf/use-callback
(mf/deps ref min-val max-val value)
@ -65,7 +78,10 @@
(sm/expr-eval value))]
(when (num? new-value)
(-> new-value
(math/round)
(cond-> (number? precision)
(math/precision precision))
(cond-> (nil? precision)
(math/round))
(cljs.core/max us/min-safe-int)
(cljs.core/min us/max-safe-int)
(cond->
@ -80,7 +96,9 @@
(mf/deps ref)
(fn [new-value]
(let [input-node (mf/ref-val ref)]
(dom/set-value! input-node (str new-value)))))
(dom/set-value! input-node (if (some? precision)
(ust/format-precision new-value precision)
(str new-value))))))
apply-value
(mf/use-callback
@ -97,18 +115,18 @@
(let [current-value (parse-value)]
(when current-value
(let [increment (if (kbd/shift? event)
(if up? 10 -10)
(if up? 1 -1))
(if up? (* step-val 10) (* step-val -10))
(if up? step-val (- step-val)))
new-value (+ current-value increment)
new-value (cond
(and wrap-value? (num? max-val) (num? min-val)
(> new-value max-val) up?)
(-> new-value (- max-val) (+ min-val) (- 1))
(-> new-value (- max-val) (+ min-val) (- step-val))
(and wrap-value? (num? min-val) (num? max-val)
(< new-value min-val) down?)
(-> new-value (- min-val) (+ max-val) (+ 1))
(-> new-value (- min-val) (+ max-val) (+ step-val))
(and (num? min-val) (< new-value min-val))
min-val
@ -144,12 +162,13 @@
handle-blur
(mf/use-callback
(mf/deps parse-value apply-value update-input)
(mf/deps parse-value apply-value update-input on-blur)
(fn [_]
(let [new-value (or (parse-value) default-val)]
(if new-value
(apply-value new-value)
(update-input new-value)))))
(update-input new-value)))
(when on-blur (on-blur))))
props (-> props
(obj/without ["value" "onChange"])

View file

@ -98,7 +98,7 @@
(filter #(= :ready (:status %)))
(mapv #(assoc % :status :importing))))
(defn update-status [files file-id status progress]
(defn update-status [files file-id status progress errors]
(->> files
(mapv (fn [file]
(cond-> file
@ -106,7 +106,10 @@
(assoc :status status)
(and (= file-id (:file-id file)) (= status :import-progress))
(assoc :progress progress))))))
(assoc :progress progress)
(= file-id (:file-id file))
(assoc :errors errors))))))
(defn parse-progress-message
[message]
@ -139,9 +142,10 @@
(let [loading? (or (= :analyzing (:status file))
(= :importing (:status file)))
load-success? (= :import-success (:status file))
analyze-error? (= :analyze-error (:status file))
import-finish? (= :import-finish (:status file))
import-error? (= :import-error (:status file))
import-warn? (d/not-empty? (:errors file))
ready? (= :ready (:status file))
is-shared? (:shared file)
progress (:progress file)
@ -177,7 +181,8 @@
[:div.file-entry
{:class (dom/classnames
:loading loading?
:success load-success?
:success (and import-finish? (not import-warn?) (not import-error?))
:warning (and import-finish? import-warn? (not import-error?))
:error (or import-error? analyze-error?)
:editable (and ready? (not editing?)))}
@ -185,8 +190,9 @@
[:div.file-icon
(cond loading? i/loader-pencil
ready? i/logo-icon
load-success? i/tick
import-warn? i/msg-warning
import-error? i/close
import-finish? i/tick
analyze-error? i/close)]
(if editing?
@ -212,7 +218,7 @@
[:div.error-message
(tr "dashboard.import.import-error")]
(and (not load-success?) (some? progress))
(and (not import-finish?) (some? progress))
[:div.progress-message (parse-progress-message progress)])
[:div.linked-libraries
@ -258,9 +264,8 @@
:project-id project-id
:files files})
(rx/subs
(fn [{:keys [file-id status message] :as msg}]
(log/debug :msg msg)
(swap! state update :files update-status file-id status message))))))
(fn [{:keys [file-id status message errors] :as msg}]
(swap! state update :files update-status file-id status message errors))))))
handle-cancel
(mf/use-callback
@ -291,7 +296,8 @@
(st/emit! (modal/hide))
(when on-finish-import (on-finish-import))))
success-files (->> @state :files (filter #(= (:status %) :import-success)) count)
warning-files (->> @state :files (filter #(and (= (:status %) :import-finish) (d/not-empty? (:errors %)))) count)
success-files (->> @state :files (filter #(and (= (:status %) :import-finish) (empty? (:errors %)))) count)
pending-analysis? (> (->> @state :files (filter #(= (:status %) :analyzing)) count) 0)
pending-import? (> (->> @state :files (filter #(= (:status %) :importing)) count) 0)]
@ -316,11 +322,15 @@
{:on-click handle-cancel} i/close]]
[:div.modal-content
(when (and (= :importing (:status @state))
(not pending-import?))
[:div.feedback-banner
[:div.icon i/checkbox-checked]
[:div.message (tr "dashboard.import.import-message" success-files)]])
(when (and (= :importing (:status @state)) (not pending-import?))
(if (> warning-files 0)
[:div.feedback-banner.warning
[:div.icon i/msg-warning]
[:div.message (tr "dashboard.import.import-warning" warning-files success-files)]]
[:div.feedback-banner
[:div.icon i/checkbox-checked]
[:div.message (tr "dashboard.import.import-message" success-files)]]))
(for [file (->> (:files @state) (filterv (comp not :deleted?)))]
(let [editing? (and (some? (:file-id file))

View file

@ -18,7 +18,8 @@
[width style]
(let [values (case style
:mixed [5 5 1 5]
:dotted [5 5]
;; We want 0 so they are circles
:dotted [(- width) 5]
:dashed [10 10]
nil)]
@ -132,9 +133,13 @@
;; for inner or outer strokes.
(and (spec/stroke-caps-line (:stroke-cap-start shape))
(= (:stroke-cap-start shape) (:stroke-cap-end shape))
(not (#{:inner :outer} (:stroke-alignment shape))))
(not (#{:inner :outer} (:stroke-alignment shape)))
(not= :dotted stroke-style))
(assoc :strokeLinecap (:stroke-cap-start shape))
(= :dotted stroke-style)
(assoc :strokeLinecap "round")
;; For other cap types we use markers.
(and (or (spec/stroke-caps-marker (:stroke-cap-start shape))
(and (spec/stroke-caps-line (:stroke-cap-start shape))

View file

@ -14,11 +14,10 @@
[cuerdas.core :as str]))
(defn generate-root-styles
[shape node]
[_shape node]
(let [valign (:vertical-align node "top")
width (some-> (:width shape) (+ 1))
base #js {:height (or (:height shape) "100%")
:width (or width "100%")
base #js {:height "100%"
:width "100%"
:fontFamily "sourcesanspro"}]
(cond-> base
(= valign "top") (obj/set! "justifyContent" "flex-start")

View file

@ -299,7 +299,7 @@
[{:keys [file layout project page-id] :as props}]
(let [team-id (:team-id project)
zoom (mf/deref refs/selected-zoom)
params {:page-id page-id :file-id (:id file)}
params {:page-id page-id :file-id (:id file) :section "interactions"}
go-back
(mf/use-callback

View file

@ -7,7 +7,6 @@
(ns app.main.ui.workspace.shapes.text.editor
(:require
["draft-js" :as draft]
[app.common.data :as d]
[app.common.geom.shapes :as gsh]
[app.common.text :as txt]
[app.main.data.workspace :as dw]
@ -72,20 +71,18 @@
(def empty-editor-state
(ted/create-editor-state nil default-decorator))
(defn get-content-changes
[old-state state]
(let [old-blocks (js->clj (.toJS (.getBlockMap (.getCurrentContent ^js old-state)))
:keywordize-keys false)
new-blocks (js->clj (.toJS (.getBlockMap (.getCurrentContent ^js state)))
(defn get-blocks-to-setup [block-changes]
(->> block-changes
(filter (fn [[_ v]]
(nil? (:old v))))
(mapv first)))
:keywordize-keys false)]
(->> old-blocks
(d/mapm
(fn [bkey bstate]
{:old (get bstate "text")
:new (get-in new-blocks [bkey "text"])}))
(filter #(contains? new-blocks (first %)))
(into {}))))
(defn get-blocks-to-add-styles
[block-changes]
(->> block-changes
(filter (fn [[_ v]]
(and (not= (:old v) (:new v)) (= (:old v) ""))))
(mapv first)))
(mf/defc text-shape-edit-html
{::mf/wrap [mf/memo]
@ -143,25 +140,35 @@
(fn [state]
(let [old-state (mf/ref-val prev-value)]
(if (and (some? state) (some? old-state))
(let [block-states (get-content-changes old-state state)
block-to-add-styles
(->> block-states
(filter
(fn [[_ v]]
(and (not= (:old v) (:new v))
(= (:old v) ""))))
(mapv first))]
(ted/apply-block-styles-to-content state block-to-add-styles))
(let [block-changes (ted/get-content-changes old-state state)
prev-data (ted/get-editor-current-inline-styles old-state)
block-to-setup (get-blocks-to-setup block-changes)
block-to-add-styles (get-blocks-to-add-styles block-changes)]
(-> state
(ted/setup-block-styles block-to-setup prev-data)
(ted/apply-block-styles-to-content block-to-add-styles)))
state))))
on-change
(mf/use-callback
(fn [val]
(let [val (handle-change val)
val (if (true? @blurred)
(ted/add-editor-blur-selection val)
(ted/remove-editor-blur-selection val))]
(let [prev-val (mf/ref-val prev-value)
styleOverride (ted/get-style-override prev-val)
;; If the content and the selection are the same we keep the style override
keep-style? (and (some? styleOverride)
(ted/content-equals prev-val val)
(ted/selection-equals prev-val val))
val (cond-> (handle-change val)
@blurred
(ted/add-editor-blur-selection)
(not @blurred)
(ted/remove-editor-blur-selection)
keep-style?
(ted/set-style-override styleOverride))]
(st/emit! (dwt/update-editor-state shape val)))))
on-editor

View file

@ -15,6 +15,7 @@
[app.main.fonts :as fonts]
[app.main.store :as st]
[app.main.ui.components.editable-select :refer [editable-select]]
[app.main.ui.components.numeric-input :refer [numeric-input]]
[app.main.ui.icons :as i]
[app.main.ui.workspace.sidebar.options.common :refer [advanced-options]]
[app.util.dom :as dom]
@ -22,6 +23,7 @@
[app.util.keyboard :as kbd]
[app.util.object :as obj]
[app.util.router :as rt]
[app.util.strings :as ust]
[app.util.timers :as tm]
[cuerdas.core :as str]
[goog.events :as events]
@ -30,7 +32,7 @@
(defn- attr->string [value]
(if (= value :multiple)
""
(str value)))
(ust/format-precision value 2)))
(defn- get-next-font
[{:keys [id] :as current} fonts]
@ -350,20 +352,19 @@
letter-spacing (or letter-spacing "0")
handle-change
(fn [event attr]
(let [new-spacing (dom/get-target-val event)]
(on-change {attr new-spacing})))]
(fn [value attr]
(on-change {attr (str value)}))]
[:div.spacing-options
[:div.input-icon
[:span.icon-before.tooltip.tooltip-bottom
{:alt (tr "workspace.options.text-options.line-height")}
i/line-height]
[:input.input-text
{:type "number"
:step "0.1"
:min "-200"
:max "200"
[:> numeric-input
{:min -200
:max 200
:step 0.1
:precision 2
:value (attr->string line-height)
:placeholder (tr "settings.multiple")
:on-change #(handle-change % :line-height)
@ -373,11 +374,11 @@
[:span.icon-before.tooltip.tooltip-bottom
{:alt (tr "workspace.options.text-options.letter-spacing")}
i/letter-spacing]
[:input.input-text
{:type "number"
:step "0.1"
:min "-200"
:max "200"
[:> numeric-input
{:min -200
:max 200
:step 0.1
:precision 2
:value (attr->string letter-spacing)
:placeholder (tr "settings.multiple")
:on-change #(handle-change % :letter-spacing)

View file

@ -16,30 +16,54 @@
(defn- text-corrected-transform
"If we apply a scale directly to the texts it will show deformed so we need to create this
correction matrix to \"undo\" the resize but keep the other transformations."
[{:keys [points transform transform-inverse]} current-transform modifiers]
[{:keys [x y width height points transform transform-inverse] :as shape} current-transform modifiers]
(let [corner-pt (first points)
transform (or transform (gmt/matrix))
transform-inverse (or transform-inverse (gmt/matrix))
corner-pt (cond-> corner-pt (some? transform-inverse) (gpt/transform transform-inverse))
current-transform
(if (some? (:resize-vector modifiers))
(gmt/multiply
current-transform
transform
(gmt/scale-matrix (gpt/inverse (:resize-vector modifiers)) (gpt/transform corner-pt transform-inverse))
transform-inverse)
current-transform)
resize-x? (some? (:resize-vector modifiers))
resize-y? (some? (:resize-vector-2 modifiers))
current-transform
(if (some? (:resize-vector-2 modifiers))
(gmt/multiply
current-transform
transform
(gmt/scale-matrix (gpt/inverse (:resize-vector-2 modifiers)) (gpt/transform corner-pt transform-inverse))
transform-inverse)
current-transform)]
current-transform))
flip-x? (neg? (get-in modifiers [:resize-vector :x]))
flip-y? (or (neg? (get-in modifiers [:resize-vector :y]))
(neg? (get-in modifiers [:resize-vector-2 :y])))
result (cond-> (gmt/matrix)
(and (some? transform) (or resize-x? resize-y?))
(gmt/multiply transform)
resize-x?
(gmt/scale (gpt/inverse (:resize-vector modifiers)) corner-pt)
resize-y?
(gmt/scale (gpt/inverse (:resize-vector-2 modifiers)) corner-pt)
flip-x?
(gmt/scale (gpt/point -1 1) corner-pt)
flip-y?
(gmt/scale (gpt/point 1 -1) corner-pt)
(and (some? transform) (or resize-x? resize-y?))
(gmt/multiply transform-inverse))
[width height]
(if (or resize-x? resize-y?)
(let [pc (-> (gpt/point x y)
(gpt/transform transform)
(gpt/transform current-transform))
pw (-> (gpt/point (+ x width) y)
(gpt/transform transform)
(gpt/transform current-transform))
ph (-> (gpt/point x (+ y height))
(gpt/transform transform)
(gpt/transform current-transform))]
[(gpt/distance pc pw) (gpt/distance pc ph)])
[width height])]
[result width height]))
(defn get-nodes
"Retrieve the DOM nodes to apply the matrix transformation"
@ -48,6 +72,7 @@
frame? (= :frame type)
group? (= :group type)
text? (= :text type)
mask? (and group? masked-group?)
;; When the shape is a frame we maybe need to move its thumbnail
@ -68,6 +93,11 @@
group?
[]
text?
[shape-node
(dom/query shape-node "foreignObject")
(dom/query shape-node ".text-shape")]
:else
[shape-node])))
@ -76,11 +106,23 @@
(when-let [nodes (get-nodes shape)]
(let [transform (get transforms id)
modifiers (get-in modifiers [id :modifiers])
transform (case type
:text (text-corrected-transform shape transform modifiers)
transform)]
[text-transform text-width text-height]
(when (= :text type)
(text-corrected-transform shape transform modifiers))]
(doseq [node nodes]
(when (and (some? transform) (some? node))
(cond
(dom/class? node "text-shape")
(when (some? text-transform)
(dom/set-attribute node "transform" (str text-transform)))
(= (dom/get-tag-name node) "foreignObject")
(when (and (some? text-width) (some? text-height))
(dom/set-attribute node "width" text-width)
(dom/set-attribute node "height" text-height))
(and (some? transform) (some? node))
(dom/set-attribute node "transform" (str transform))))))))
(defn remove-transform [shapes]
@ -88,7 +130,13 @@
(when-let [nodes (get-nodes shape)]
(doseq [node nodes]
(when (some? node)
(dom/remove-attribute node "transform"))))))
(cond
(= (dom/get-tag-name node) "foreignObject")
;; The shape width/height will be automaticaly setup when the modifiers are applied
nil
:else
(dom/remove-attribute node "transform")))))))
(defn format-viewbox [vbox]
(str/join " " [(+ (:x vbox 0) (:left-offset vbox 0))

View file

@ -17,21 +17,24 @@
;; --- Deprecated methods
(defn event->inner-text
[e]
(.-innerText (.-target e)))
[^js e]
(when (some? e)
(.-innerText (.-target e))))
(defn event->value
[e]
(.-value (.-target e)))
[^js e]
(when (some? e)
(.-value (.-target e))))
(defn event->target
[e]
(.-target e))
[^js e]
(when (some? e)
(.-target e)))
;; --- New methods
(defn set-html-title
[title]
[^string title]
(set! (.-title globals/document) title))
(defn set-page-style
@ -61,98 +64,117 @@
(dom/getElement id))
(defn get-elements-by-tag
[node tag]
(.getElementsByTagName node tag))
[^js node tag]
(when (some? node)
(.getElementsByTagName node tag)))
(defn stop-propagation
[e]
(when e
(.stopPropagation e)))
[^js event]
(when event
(.stopPropagation event)))
(defn prevent-default
[e]
(when e
(.preventDefault e)))
[^js event]
(when event
(.preventDefault event)))
(defn get-target
"Extract the target from event instance."
[event]
(.-target event))
[^js event]
(when (some? event)
(.-target event)))
(defn get-current-target
"Extract the current target from event instance (different from target
when event triggered in a child of the subscribing element)."
[event]
(.-currentTarget event))
[^js event]
(when (some? event)
(.-currentTarget event)))
(defn get-parent
[dom]
(.-parentElement ^js dom))
[^js node]
(when (some? node)
(.-parentElement ^js node)))
(defn get-value
"Extract the value from dom node."
[node]
(.-value node))
[^js node]
(when (some? node)
(.-value node)))
(defn get-attribute
"Extract the value of one attribute of a dom node."
[node attr-name]
(.getAttribute ^js node attr-name))
[^js node ^string attr-name]
(when (some? node)
(.getAttribute ^js node attr-name)))
(def get-target-val (comp get-value get-target))
(defn click
"Click a node"
[node]
(.click node))
[^js node]
(when (some? node)
(.click node)))
(defn get-files
"Extract the files from dom node."
[node]
(array-seq (.-files node)))
[^js node]
(when (some? node)
(array-seq (.-files node))))
(defn checked?
"Check if the node that represents a radio
or checkbox is checked or not."
[node]
(.-checked node))
[^js node]
(when (some? node)
(.-checked node)))
(defn valid?
"Check if the node that is a form input
has a valid value, against html5 form validation
properties (required, min/max, pattern...)."
[node]
(.-valid (.-validity node)))
[^js node]
(when (some? node)
(when-let [validity (.-validity node)]
(.-valid validity))))
(defn set-validity!
"Manually set the validity status of a node that
is a form input. If the state is an empty string,
the input will be valid. If not, the string will
be set as the error message."
[node status]
(.setCustomValidity node status)
(.reportValidity node))
[^js node status]
(when (some? node)
(.setCustomValidity node status)
(.reportValidity node)))
(defn clean-value!
[node]
(set! (.-value node) ""))
[^js node]
(when (some? node)
(set! (.-value node) "")))
(defn set-value!
[node value]
(set! (.-value ^js node) value))
[^js node value]
(when (some? node)
(set! (.-value ^js node) value)))
(defn select-text!
[node]
(.select ^js node))
[^js node]
(when (some? node)
(.select ^js node)))
(defn ^boolean equals?
[node-a node-b]
(.isEqualNode ^js node-a node-b))
[^js node-a ^js node-b]
(or (and (nil? node-a) (nil? node-b))
(and (some? node-a)
(.isEqualNode ^js node-a node-b))))
(defn get-event-files
"Extract the files from event instance."
[event]
(get-files (get-target event)))
[^js event]
(when (some? event)
(get-files (get-target event))))
(defn create-element
([tag]
@ -161,50 +183,58 @@
(.createElementNS globals/document ns tag)))
(defn set-html!
[el html]
(set! (.-innerHTML el) html))
[^js el html]
(when (some? el)
(set! (.-innerHTML el) html)))
(defn append-child!
[el child]
(.appendChild ^js el child))
[^js el child]
(when (some? el)
(.appendChild ^js el child)))
(defn get-first-child
[el]
(.-firstChild el))
[^js el]
(when (some? el)
(.-firstChild el)))
(defn get-tag-name
[el]
(.-tagName el))
[^js el]
(when (some? el)
(.-tagName el)))
(defn get-outer-html
[el]
(.-outerHTML el))
[^js el]
(when (some? el)
(.-outerHTML el)))
(defn get-inner-text
[el]
(.-innerText el))
[^js el]
(when (some? el)
(.-innerText el)))
(defn query
[el query]
[^js el ^string query]
(when (some? el)
(.querySelector el query)))
(defn get-client-position
[event]
[^js event]
(let [x (.-clientX event)
y (.-clientY event)]
(gpt/point x y)))
(defn get-offset-position
[event]
(let [x (.-offsetX event)
y (.-offsetY event)]
(gpt/point x y)))
[^js event]
(when (some? event)
(let [x (.-offsetX event)
y (.-offsetY event)]
(gpt/point x y))))
(defn get-client-size
[node]
{:width (.-clientWidth ^js node)
:height (.-clientHeight ^js node)})
[^js node]
(when (some? node)
{:width (.-clientWidth ^js node)
:height (.-clientHeight ^js node)}))
(defn get-bounding-rect
[node]
@ -222,12 +252,12 @@
:height (.-innerHeight ^js js/window)})
(defn focus!
[node]
[^js node]
(when (some? node)
(.focus node)))
(defn blur!
[node]
[^js node]
(when (some? node)
(.blur node)))
@ -245,8 +275,9 @@
:hint "seems like the current browser does not support fullscreen api.")))
(defn ^boolean blob?
[v]
(instance? js/Blob v))
[^js v]
(when (some? v)
(instance? js/Blob v)))
(defn create-blob
"Create a blob from content."
@ -265,20 +296,24 @@
{:pre [(blob? b)]}
(js/URL.createObjectURL b))
(defn set-property! [node property value]
(.setAttribute node property value))
(defn set-property! [^js node property value]
(when (some? node)
(.setAttribute node property value)))
(defn set-text! [node text]
(set! (.-textContent node) text))
(defn set-text! [^js node text]
(when (some? node)
(set! (.-textContent node) text)))
(defn set-css-property! [node property value]
(.setProperty (.-style ^js node) property value))
(defn set-css-property! [^js node property value]
(when (some? node)
(.setProperty (.-style ^js node) property value)))
(defn capture-pointer [event]
(-> event get-target (.setPointerCapture (.-pointerId event))))
(defn capture-pointer [^js event]
(when (some? event)
(-> event get-target (.setPointerCapture (.-pointerId event)))))
(defn release-pointer [event]
(when (.-pointerId event)
(defn release-pointer [^js event]
(when (and (some? event) (.-pointerId event))
(-> event get-target (.releasePointerCapture (.-pointerId event)))))
(defn get-root []
@ -295,19 +330,23 @@
(partition 2 params))))
(defn ^boolean class? [node class-name]
(let [class-list (.-classList ^js node)]
(.contains ^js class-list class-name)))
(when (some? node)
(let [class-list (.-classList ^js node)]
(.contains ^js class-list class-name))))
(defn add-class! [node class-name]
(let [class-list (.-classList ^js node)]
(.add ^js class-list class-name)))
(defn add-class! [^js node class-name]
(when (some? node)
(let [class-list (.-classList ^js node)]
(.add ^js class-list class-name))))
(defn remove-class! [node class-name]
(let [class-list (.-classList ^js node)]
(.remove ^js class-list class-name)))
(defn remove-class! [^js node class-name]
(when (some? node)
(let [class-list (.-classList ^js node)]
(.remove ^js class-list class-name))))
(defn child? [node1 node2]
(.contains ^js node2 ^js node1))
(defn child? [^js node1 ^js node2]
(when (some? node1)
(.contains ^js node2 ^js node1)))
(defn get-user-agent []
(.-userAgent globals/navigator))
@ -315,11 +354,13 @@
(defn get-active []
(.-activeElement globals/document))
(defn active? [node]
(= (get-active) node))
(defn active? [^js node]
(when (some? node)
(= (get-active) node)))
(defn get-data [^js node ^string attr]
(.getAttribute node (str "data-" attr)))
(when (some? node)
(.getAttribute node (str "data-" attr))))
(defn mtype->extension [mtype]
;; https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types
@ -336,42 +377,53 @@
nil))
(defn set-attribute [^js node ^string attr value]
(.setAttribute node attr value))
(when (some? node)
(.setAttribute node attr value)))
(defn remove-attribute [^js node ^string attr]
(.removeAttribute node attr))
(when (some? node)
(.removeAttribute node attr)))
(defn get-scroll-pos
[element]
(.-scrollTop ^js element))
[^js element]
(when (some? element)
(.-scrollTop element)))
(defn set-scroll-pos!
[element scroll]
(obj/set! ^js element "scrollTop" scroll))
[^js element scroll]
(when (some? element)
(obj/set! element "scrollTop" scroll)))
(defn scroll-into-view!
([element]
(.scrollIntoView ^js element false))
([element scroll-top]
(.scrollIntoView ^js element scroll-top)))
([^js element]
(when (some? element)
(.scrollIntoView element false)))
([^js element scroll-top]
(when (some? element)
(.scrollIntoView element scroll-top))))
(defn scroll-into-view-if-needed!
([element]
(.scrollIntoViewIfNeeded ^js element false))
([element scroll-top]
(.scrollIntoViewIfNeeded ^js element scroll-top)))
([^js element]
(when (some? element)
(.scrollIntoViewIfNeeded ^js element false)))
([^js element scroll-top]
(when (some? element)
(.scrollIntoViewIfNeeded ^js element scroll-top))))
(defn is-in-viewport?
[element]
(let [rect (.getBoundingClientRect element)
height (or (.-innerHeight js/window)
(.. js/document -documentElement -clientHeight))
width (or (.-innerWidth js/window)
(.. js/document -documentElement -clientWidth))]
(and (>= (.-top rect) 0)
(>= (.-left rect) 0)
(<= (.-bottom rect) height)
(<= (.-right rect) width))))
[^js element]
(when (some? element)
(let [rect (.getBoundingClientRect element)
height (or (.-innerHeight js/window)
(.. js/document -documentElement -clientHeight))
width (or (.-innerWidth js/window)
(.. js/document -documentElement -clientWidth))]
(and (>= (.-top rect) 0)
(>= (.-left rect) 0)
(<= (.-bottom rect) height)
(<= (.-right rect) width)))))
(defn trigger-download-uri
[filename mtype uri]

View file

@ -0,0 +1,38 @@
;; 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) UXBOX Labs SL
(ns app.util.strings
(:require
[cuerdas.core :as str]))
(def ^:const trail-zeros-regex-1 #"\.0+$")
(def ^:const trail-zeros-regex-2 #"(\.\d*[^0])0+$")
(defn format-precision
"Creates a number with predetermined precision and then removes the trailing 0.
Examples:
12.0123, 0 => 12
12.0123, 1 => 12
12.0123, 2 => 12.01"
[num precision]
(try
(if (number? num)
(let [num-str (.toFixed num precision)
;; Remove all trailing zeros after the comma 100.00000
num-str (str/replace num-str trail-zeros-regex-1 "")
;; Remove trailing zeros after a decimal number: 0.001|00|
num-str (if-let [m (re-find trail-zeros-regex-2 num-str)]
(str/replace num-str (first m) (second m))
num-str)]
num-str)
(str num))
(catch :default _
(str num))))

View file

@ -111,6 +111,16 @@
[state]
(impl/cursorToEnd state))
(defn setup-block-styles
[state blocks attrs]
(if (empty? blocks)
state
(->> blocks
(reduce
(fn [state block-key]
(impl/updateBlockData state block-key (clj->js attrs)))
state))))
(defn apply-block-styles-to-content
[state blocks]
(if (empty? blocks)
@ -130,3 +140,37 @@
(defn insert-text [state text attrs]
(let [style (txt/attrs-to-styles attrs)]
(impl/insertText state text (clj->js attrs) (clj->js style))))
(defn get-style-override [state]
(.getInlineStyleOverride state))
(defn set-style-override [state inline-style]
(impl/setInlineStyleOverride state inline-style))
(defn content-equals [state other]
(.equals (.getCurrentContent state) (.getCurrentContent other)))
(defn selection-equals [state other]
(impl/selectionEquals (.getSelection state) (.getSelection other)))
(defn get-content-changes
[old-state state]
(let [old-blocks (js->clj (.toJS (.getBlockMap (.getCurrentContent ^js old-state)))
:keywordize-keys false)
new-blocks (js->clj (.toJS (.getBlockMap (.getCurrentContent ^js state)))
:keywordize-keys false)]
(merge
(into {}
(comp (filter #(contains? new-blocks (first %)))
(map (fn [[bkey bstate]]
[bkey
{:old (get bstate "text")
:new (get-in new-blocks [bkey "text"])}])))
old-blocks)
(into {}
(comp (filter #(not (contains? old-blocks (first %))))
(map (fn [[bkey bstate]]
[bkey
{:old nil
:new (get bstate "text")}])))
new-blocks))))

View file

@ -121,15 +121,33 @@ export function updateCurrentBlockData(state, attrs) {
return EditorState.push(state, content, "change-block-data");
}
function addStylesToOverride(styles, other) {
let result = styles;
for (let style of other) {
const [p, k, v] = style.split("$$$");
const prefix = [p, k, ""].join("$$$");
const curValue = result.find((it) => it.startsWith(prefix))
if (curValue) {
result = result.remove(curValue);
}
result = result.add(style);
}
return result
}
export function applyInlineStyle(state, styles) {
const userSelection = state.getSelection();
let selection = userSelection;
let result = state;
if (selection.isCollapsed()) {
selection = getSelectAllSelection(state);
const currentOverride = state.getCurrentInlineStyle() || new OrderedSet();
const styleOverride = addStylesToOverride(currentOverride, styles)
return EditorState.setInlineStyleOverride(state, styleOverride);
}
let result = state;
let content = null;
for (let style of styles) {
@ -300,6 +318,7 @@ export function getBlockData(state, blockKey) {
export function updateBlockData(state, blockKey, data) {
const userSelection = state.getSelection();
const inlineStyleOverride = state.getInlineStyleOverride();
const content = state.getCurrentContent();
const block = content.getBlockForKey(blockKey);
const newBlock = mergeBlockData(block, data);
@ -312,8 +331,10 @@ export function updateBlockData(state, blockKey, data) {
blockData
);
const result = EditorState.push(state, newContent, 'change-block-data');
return EditorState.acceptSelection(result, userSelection);
let result = EditorState.push(state, newContent, 'change-block-data');
result = EditorState.acceptSelection(result, userSelection);
result = EditorState.setInlineStyleOverride(result, inlineStyleOverride);
return result;
}
export function getSelection(state) {
@ -376,3 +397,15 @@ export function insertText(state, text, attrs, inlineStyles) {
const resultSelection = SelectionState.createEmpty(selection.getStartKey());
return EditorState.push(state, newContent, 'insert-fragment');
}
export function setInlineStyleOverride(state, inlineStyles) {
return EditorState.setInlineStyleOverride(state, inlineStyles);
}
export function selectionEquals(selection, other) {
return selection.getAnchorKey() === other.getAnchorKey() &&
selection.getAnchorOffset() === other.getAnchorOffset() &&
selection.getFocusKey() === other.getFocusKey() &&
selection.getFocusOffset() === other.getFocusOffset() &&
selection.getIsBackward() === other.getIsBackward();
}

View file

@ -176,7 +176,9 @@
(rx/tap #(reset! revn (:revn %)))
(rx/ignore))
(rp/mutation :persist-temp-file {:id file-id}))))
(->> (rp/mutation :persist-temp-file {:id file-id})
;; We use merge to keep some information not stored in back-end
(rx/map #(merge file %))))))
(defn upload-media-files
"Upload a image to the backend and returns its id"
@ -457,8 +459,7 @@
(let [progress-str (rx/subject)
context (assoc context :progress progress-str)]
(rx/merge
progress-str
[progress-str
(->> (rx/of file)
(rx/flat-map (partial process-pages context))
(rx/tap #(progress! context :process-colors))
@ -470,7 +471,7 @@
(rx/tap #(progress! context :process-components))
(rx/flat-map (partial process-library-components context))
(rx/flat-map (partial send-changes context))
(rx/tap #(rx/end! progress-str))))))
(rx/tap #(rx/end! progress-str)))]))
(defn create-files
[context files]
@ -482,7 +483,6 @@
(rx/flat-map
(fn [context]
(->> (create-file context)
(rx/tap #(.log js/console "create-file" (clj->js %)))
(rx/map #(vector % (first (get data (:file-id context)))))))))
(->> (rx/from files)
@ -509,20 +509,29 @@
(let [context {:project-id project-id
:resolve (resolve-factory)}]
(->> (create-files context files)
(rx/catch #(.error js/console "IMPORT ERROR" %))
(rx/catch #(.error js/console "IMPORT ERROR" (clj->js %)))
(rx/flat-map
(fn [[file data]]
(->> (rx/concat
(->> (uz/load-from-url (:uri data))
(rx/map #(-> context (assoc :zip %) (merge data)))
(rx/flat-map #(process-file % file)))
(rx/of
{:status :import-success
:file-id (:file-id data)}))
(rx/flat-map
(fn [context]
;; process file retrieves a stream that will emit progress notifications
;; and other that will emit the files once imported
(let [[progress-stream file-stream] (process-file context file)]
(rx/merge
progress-stream
(->> file-stream
(rx/map
(fn [file]
{:status :import-finish
:errors (:errors file)
:file-id (:file-id data)})))))))))
(rx/catch
(fn [err]
(.error js/console "ERROR" (:file-id data) err)
(.error js/console "ERROR" (str (:file-id data)) (clj->js err) (clj->js (.-data err)))
(rx/of {:status :import-error
:file-id (:file-id data)
:error (.-message err)

View file

@ -3322,3 +3322,6 @@ msgstr "Insufficient members to leave team, you probably want to delete it."
msgid "errors.auth.unable-to-login"
msgstr "Looks like you are not authenticated or session expired."
msgid "dashboard.import.import-warning"
msgstr "Some files containted invalid objects that have been removed."

View file

@ -3309,3 +3309,6 @@ msgstr "Actualizar"
msgid "workspace.viewport.click-to-close-path"
msgstr "Pulsar para cerrar la ruta"
msgid "dashboard.import.import-warning"
msgstr "Algunos ficheros contenían objetos erroneos que no han sido importados."