🎉 Add features assignation for teams

This commit is contained in:
Andrey Antukh 2023-10-23 19:31:41 +02:00 committed by Andrés Moya
parent 7db8d7b7ab
commit 6f93b41920
84 changed files with 2390 additions and 1777 deletions

View file

@ -7,7 +7,6 @@
(ns app.main.data.common
"A general purpose events."
(:require
[app.common.files.features :as ffeat]
[app.common.types.components-list :as ctkl]
[app.config :as cf]
[app.main.data.messages :as msg]
@ -90,9 +89,7 @@
(ptk/reify ::show-shared-dialog
ptk/WatchEvent
(watch [_ state _]
(let [features (cond-> ffeat/enabled
(features/active-feature? state :components-v2)
(conj "components/v2"))
(let [features (features/get-team-enabled-features state)
data (:workspace-data state)
file (:workspace-file state)]
(->> (if (and data file)

View file

@ -8,6 +8,7 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.features :as cfeat]
[app.common.files.helpers :as cfh]
[app.common.schema :as sm]
[app.common.uri :as u]
@ -28,6 +29,7 @@
[app.util.timers :as tm]
[app.util.webapi :as wapi]
[beicon.core :as rx]
[clojure.set :as set]
[potok.core :as ptk]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -43,11 +45,10 @@
(ptk/reify ::initialize
ptk/UpdateEvent
(update [_ state]
(du/set-current-team! id)
(let [prev-team-id (:current-team-id state)]
(cond-> state
(not= prev-team-id id)
(-> (assoc :current-team-id id)
(-> (dissoc :current-team-id)
(dissoc :dashboard-files)
(dissoc :dashboard-projects)
(dissoc :dashboard-shared-files)
@ -58,27 +59,36 @@
ptk/WatchEvent
(watch [_ state stream]
(rx/concat
(rx/of (features/initialize))
(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-users {:team-id id}) state stream)
(let [stoper-s (rx/filter (ptk/type? ::finalize) stream)
profile-id (:profile-id state)]
(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))))))))
(->> (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 id) state stream)
(ptk/watch (fetch-team-members id) state stream)
(ptk/watch (du/fetch-users {:team-id id}) state stream)
(->> 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))
;; Once the teams are fecthed, initialize features related
;; to currently active team
(->> stream
(rx/filter (ptk/type? ::du/teams-fetched))
(rx/observe-on :async)
(rx/mapcat deref)
(rx/filter #(= id (:id %)))
(rx/map du/set-current-team)))
(rx/take-until stoper-s))))))
(defn finalize
[params]
@ -98,13 +108,12 @@
(assoc state :dashboard-team-members (d/index-by :id members)))))
(defn fetch-team-members
[]
[team-id]
(ptk/reify ::fetch-team-members
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)]
(->> (rp/cmd! :get-team-members {:team-id team-id})
(rx/map team-members-fetched))))))
(watch [_ _ _]
(->> (rp/cmd! :get-team-members {:team-id team-id})
(rx/map team-members-fetched)))))
;; --- EVENT: fetch-team-stats
@ -116,13 +125,12 @@
(assoc state :dashboard-team-stats stats))))
(defn fetch-team-stats
[]
[team-id]
(ptk/reify ::fetch-team-stats
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)]
(->> (rp/cmd! :get-team-stats {:team-id team-id})
(rx/map team-stats-fetched))))))
(watch [_ _ _]
(->> (rp/cmd! :get-team-stats {:team-id team-id})
(rx/map team-stats-fetched)))))
;; --- EVENT: fetch-team-invitations
@ -171,13 +179,12 @@
(assoc state :dashboard-projects projects)))))
(defn fetch-projects
[]
[team-id]
(ptk/reify ::fetch-projects
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)]
(->> (rp/cmd! :get-projects {:team-id team-id})
(rx/map projects-fetched))))))
(watch [_ _ _]
(->> (rp/cmd! :get-projects {:team-id team-id})
(rx/map projects-fetched)))))
;; --- EVENT: search
@ -344,11 +351,12 @@
(dm/assert! (string? name))
(ptk/reify ::create-team
ptk/WatchEvent
(watch [_ _ _]
(watch [_ state _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)]
(->> (rp/cmd! :create-team {:name name})
on-error rx/throw}} (meta params)
features (features/get-enabled-features state)]
(->> (rp/cmd! :create-team {:name name :features features})
(rx/tap on-success)
(rx/map team-created)
(rx/catch on-error))))))
@ -359,13 +367,15 @@
[{:keys [name emails role] :as params}]
(ptk/reify ::create-team-with-invitations
ptk/WatchEvent
(watch [_ _ _]
(watch [_ state _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)
params {:name name
:emails #{emails}
:role role}]
features (features/get-enabled-features state)]
params {:name name
:emails #{emails}
:role role
:features features}
(->> (rp/cmd! :create-team-with-invitations params)
(rx/tap on-success)
(rx/map team-created)
@ -419,7 +429,7 @@
params (assoc params :team-id team-id)]
(->> (rp/cmd! :update-team-member-role params)
(rx/mapcat (fn [_]
(rx/of (fetch-team-members)
(rx/of (fetch-team-members team-id)
(du/fetch-teams)))))))))
(defn delete-team-member
@ -432,7 +442,7 @@
params (assoc params :team-id team-id)]
(->> (rp/cmd! :delete-team-member params)
(rx/mapcat (fn [_]
(rx/of (fetch-team-members)
(rx/of (fetch-team-members team-id)
(du/fetch-teams)))))))))
(defn leave-team
@ -846,9 +856,8 @@
files (get state :dashboard-files)
unames (cfh/get-used-names files)
name (cfh/generate-unique-name unames (str (tr "dashboard.new-file-prefix") " 1"))
features (cond-> #{}
(features/active-feature? state :components-v2)
(conj "components/v2"))
features (-> (features/get-team-enabled-features state)
(set/difference cfeat/frontend-only-features))
params (-> params
(assoc :name name)
(assoc :features features))]

View file

@ -16,6 +16,7 @@
[app.main.data.events :as ev]
[app.main.data.media :as di]
[app.main.data.websocket :as ws]
[app.main.features :as features]
[app.main.repo :as rp]
[app.util.i18n :as i18n]
[app.util.router :as rt]
@ -56,21 +57,20 @@
(defn teams-fetched
[teams]
(let [teams (d/index-by :id teams)
ids (into #{} (keys teams))]
(ptk/reify ::teams-fetched
IDeref
(-deref [_] teams)
(ptk/reify ::teams-fetched
IDeref
(-deref [_] teams)
ptk/UpdateEvent
(update [_ state]
(assoc state :teams (d/index-by :id teams)))
ptk/UpdateEvent
(update [_ state]
(assoc state :teams teams))
ptk/EffectEvent
(effect [_ _ _]
;; Check if current team-id is part of available teams
;; if not, dissoc it from storage.
ptk/EffectEvent
(effect [_ _ _]
;; Check if current team-id is part of available teams
;; if not, dissoc it from storage.
(let [ids (into #{} (map :id) teams)]
(when-let [ctid (::current-team-id @storage)]
(when-not (contains? ids ctid)
(swap! storage dissoc ::current-team-id)))))))
@ -83,6 +83,23 @@
(->> (rp/cmd! :get-teams)
(rx/map teams-fetched)))))
(defn set-current-team
[team]
(ptk/reify ::set-current-team
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc :team team)
(assoc :current-team-id (:id team))))
ptk/WatchEvent
(watch [_ _ _]
(rx/of (features/initialize (:features team #{}))))
ptk/EffectEvent
(effect [_ _ _]
(set-current-team! (:id team)))))
;; --- EVENT: fetch-profile
(declare logout)

View file

@ -8,7 +8,6 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.features :as ffeat]
[app.common.geom.point :as gpt]
[app.common.pages.helpers :as cph]
[app.common.schema :as sm]
@ -108,12 +107,8 @@
(ptk/reify ::fetch-bundle
ptk/WatchEvent
(watch [_ state _]
(let [features (cond-> ffeat/enabled
(features/active-feature? state :components-v2)
(conj "components/v2")
(let [features (features/get-team-enabled-features state)
:always
(conj "storage/pointer-map"))
params' (cond-> {:file-id file-id :features features}
(uuid? share-id)
(assoc :share-id share-id))

View file

@ -9,17 +9,13 @@
[app.common.attrs :as attrs]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.features :as ffeat]
[app.common.files.helpers :as cfh]
[app.common.files.libraries-helpers :as cflh]
[app.common.files.shapes-helpers :as cfsh]
[app.common.geom.align :as gal]
[app.common.geom.point :as gpt]
[app.common.geom.proportions :as gpp]
[app.common.geom.rect :as grc]
[app.common.geom.shapes :as gsh]
[app.common.geom.shapes.grid-layout :as gslg]
[app.common.logging :as log]
[app.common.pages.changes-builder :as pcb]
[app.common.pages.helpers :as cph]
[app.common.text :as txt]
@ -27,8 +23,6 @@
[app.common.types.component :as ctk]
[app.common.types.components-list :as ctkl]
[app.common.types.container :as ctn]
[app.common.types.file :as ctf]
[app.common.types.pages-list :as ctpl]
[app.common.types.shape :as cts]
[app.common.types.shape-tree :as ctst]
[app.common.types.shape.layout :as ctl]
@ -39,7 +33,6 @@
[app.main.data.events :as ev]
[app.main.data.fonts :as df]
[app.main.data.messages :as msg]
[app.main.data.modal :as modal]
[app.main.data.users :as du]
[app.main.data.workspace.bool :as dwb]
[app.main.data.workspace.changes :as dch]
@ -94,18 +87,12 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(declare ^:private workspace-initialized)
(declare ^:private remove-graphics)
(declare ^:private libraries-fetched)
;; --- Initialize Workspace
(defn initialize-layout
[lname]
;; (dm/assert!
;; "expected valid layout"
;; (and (keyword? lname)
;; (contains? layout/presets lname)))
(ptk/reify ::initialize-layout
ptk/UpdateEvent
(update [_ state]
@ -129,18 +116,10 @@
(assoc :workspace-ready? true)))
ptk/WatchEvent
(watch [_ state _]
(let [file (:workspace-data state)
has-graphics? (-> file :media seq)
components-v2 (features/active-feature? state :components-v2)]
(rx/merge
(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)))))))
(watch [_ _ _]
(rx/of (fbc/fix-bool-contents)
(fdf/fix-deleted-fonts)
(fbs/fix-broken-shapes)))))
(defn- workspace-data-loaded
[data]
@ -171,39 +150,43 @@
(assoc data :pages-index pages-index))))))
(defn- bundle-fetched
[features [{:keys [id data] :as file} thumbnails project users comments-users]]
[{:keys [features file thumbnails project team team-users comments-users]}]
(ptk/reify ::bundle-fetched
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc :users (d/index-by :id team-users))
(assoc :workspace-thumbnails thumbnails)
(assoc :workspace-file (dissoc file :data))
(assoc :workspace-project project)
(assoc :current-team-id (:team-id project))
(assoc :users (d/index-by :id users))
(assoc :current-file-comments-users (d/index-by :id comments-users))))
ptk/WatchEvent
(watch [_ _ stream]
(let [team-id (:team-id project)
stoper (rx/filter (ptk/type? ::bundle-fetched) stream)]
(let [team-id (:id team)
file-id (:id file)
file-data (:data file)
stoper-s (rx/filter (ptk/type? ::bundle-fetched) stream)]
(->> (rx/concat
;; Initialize notifications
(rx/of (dwn/initialize team-id id)
(rx/of (dwn/initialize team-id file-id)
(dwsl/initialize))
;; Load team fonts. We must ensure custom fonts are
;; fully loadad before mark workspace as initialized
(rx/merge
(->> stream
(rx/filter (ptk/type? :app.main.data.fonts/team-fonts-loaded))
(rx/filter (ptk/type? ::df/team-fonts-loaded))
(rx/take 1)
(rx/ignore))
(rx/of (df/load-team-fonts team-id))
;; FIXME: move to bundle fetch stages
;; Load main file
(->> (resolve-file-data id data)
(->> (resolve-file-data file-id file-data)
(rx/mapcat (fn [{:keys [pages-index] :as data}]
(->> (rx/from (seq pages-index))
(rx/mapcat
@ -217,7 +200,7 @@
(rx/map workspace-data-loaded))
;; Load libraries
(->> (rp/cmd! :get-file-libraries {:file-id id})
(->> (rp/cmd! :get-file-libraries {:file-id file-id})
(rx/mapcat identity)
(rx/merge-map
(fn [{:keys [id synced-at]}]
@ -233,8 +216,10 @@
(rx/map #(assoc file :thumbnails %)))))
(rx/reduce conj [])
(rx/map libraries-fetched)))
(rx/of (with-meta (workspace-initialized) {:file-id id})))
(rx/take-until stoper))))))
(rx/of (with-meta (workspace-initialized)
{:file-id file-id})))
(rx/take-until stoper-s))))))
(defn- libraries-fetched
[libraries]
@ -255,7 +240,7 @@
(rx/concat (rx/timer 1000)
(rx/of (dwl/notify-sync-file file-id))))))))
(defn- fetch-thumbnail-blob-uri
(defn- datauri->blob-uri
[uri]
(->> (http/send! {:uri uri
:response-type :blob
@ -263,47 +248,86 @@
(rx/map :body)
(rx/map (fn [blob] (wapi/create-uri blob)))))
(defn- fetch-thumbnail-blobs
(defn- fetch-file-object-thumbnails
[file-id]
(->> (rp/cmd! :get-file-object-thumbnails {:file-id file-id})
(rx/mapcat (fn [thumbnails]
(->> (rx/from thumbnails)
(rx/mapcat (fn [[k v]]
;; we only need to fetch the thumbnail if
;; it is a data:uri, otherwise we can just
;; use the value as is.
(if (.startsWith v "data:")
(->> (fetch-thumbnail-blob-uri v)
(rx/map (fn [uri] [k uri])))
(rx/of [k v])))))))
(->> (rx/from thumbnails)
(rx/mapcat (fn [[k v]]
;; we only need to fetch the thumbnail if
;; it is a data:uri, otherwise we can just
;; use the value as is.
(if (str/starts-with? v "data:")
(->> (datauri->blob-uri v)
(rx/map (fn [uri] [k uri])))
(rx/of [k v])))))))
(rx/reduce conj {})))
(defn- fetch-bundle
(defn- fetch-bundle-stage-1
[project-id file-id]
(ptk/reify ::fetch-bundle
(ptk/reify ::fetch-bundle-stage-1
ptk/WatchEvent
(watch [_ _ stream]
(->> (rp/cmd! :get-project {:id project-id})
(rx/mapcat (fn [project]
(->> (rp/cmd! :get-team {:id (:team-id project)})
(rx/mapcat (fn [team]
(let [bundle {:team team
:project project
:file-id file-id
:project-id project-id}]
(rx/of (du/set-current-team team)
(ptk/data-event ::bundle-stage-1 bundle))))))))
(rx/take-until
(rx/filter (ptk/type? ::fetch-bundle) stream))))))
(defn- fetch-bundle-stage-2
[{:keys [file-id project-id] :as bundle}]
(ptk/reify ::fetch-bundle-stage-2
ptk/WatchEvent
(watch [_ state stream]
(let [features (cond-> ffeat/enabled
(features/active-feature? state :components-v2)
(conj "components/v2")
;; We still put the feature here and not in the
;; ffeat/enabled var because the pointers map is only
;; supported on workspace bundle fetching mechanism.
:always
(conj "storage/pointer-map"))
(let [features (features/get-team-enabled-features state)
;; WTF is this?
share-id (-> state :viewer-local :share-id)
stoper (rx/filter (ptk/type? ::fetch-bundle) stream)]
share-id (-> state :viewer-local :share-id)]
(->> (rx/zip (rp/cmd! :get-file {:id file-id :features features :project-id project-id})
(fetch-thumbnail-blobs file-id)
(rp/cmd! :get-project {:id project-id})
(fetch-file-object-thumbnails file-id)
(rp/cmd! :get-team-users {:file-id file-id})
(rp/cmd! :get-profiles-for-file-comments {:file-id file-id :share-id share-id}))
(rx/take 1)
(rx/map (partial bundle-fetched features))
(rx/take-until stoper))))))
(rx/map (fn [[file thumbnails team-users comments-users]]
(let [bundle (-> bundle
(assoc :file file)
(assoc :thumbnails thumbnails)
(assoc :team-users team-users)
(assoc :comments-users comments-users))]
(ptk/data-event ::bundle-stage-2 bundle))))
(rx/take-until
(rx/filter (ptk/type? ::fetch-bundle) stream)))))))
(defn- fetch-bundle
"Multi-stage file bundle fetch coordinator"
[project-id file-id]
(ptk/reify ::fetch-bundle
ptk/WatchEvent
(watch [_ _ stream]
(->> (rx/merge
(rx/of (fetch-bundle-stage-1 project-id file-id))
(->> stream
(rx/filter (ptk/type? ::bundle-stage-1))
(rx/observe-on :async)
(rx/map deref)
(rx/map fetch-bundle-stage-2))
(->> stream
(rx/filter (ptk/type? ::bundle-stage-2))
(rx/observe-on :async)
(rx/map deref)
(rx/map bundle-fetched)))
(rx/take-until
(rx/filter (ptk/type? ::fetch-bundle) stream))))))
(defn initialize-file
[project-id file-id]
@ -322,7 +346,6 @@
ptk/WatchEvent
(watch [_ _ _]
(rx/of msg/hide
(features/initialize)
(dcm/retrieve-comment-threads file-id)
(dwp/initialize-file-persistence file-id)
(fetch-bundle project-id file-id)))
@ -548,7 +571,7 @@
(ptk/reify ::delete-page
ptk/WatchEvent
(watch [it state _]
(let [components-v2 (features/active-feature? state :components-v2)
(let [components-v2 (features/active-feature? state "components/v2")
file-id (:current-file-id state)
file (wsh/get-file state file-id)
pages (get-in state [:workspace-data :pages])
@ -1326,7 +1349,7 @@
ptk/WatchEvent
(watch [_ state _]
(let [components-v2 (features/active-feature? state :components-v2)]
(let [components-v2 (features/active-feature? state "components/v2")]
(if components-v2
(rx/of (go-to-main-instance nil component-id))
(let [project-id (get-in state [:workspace-project :id])
@ -1341,7 +1364,7 @@
ptk/EffectEvent
(effect [_ state _]
(let [components-v2 (features/active-feature? state :components-v2)
(let [components-v2 (features/active-feature? state "components/v2")
wrapper-id (str "component-shape-id-" component-id)]
(when-not components-v2
(tm/schedule-on-idle #(dom/scroll-into-view-if-needed! (dom/get-element wrapper-id))))))))
@ -2007,143 +2030,6 @@
(rx/of (dch/commit-changes changes))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Remove graphics
;; TODO: this should be deprecated and removed together with components-v2
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- initialize-remove-graphics
[total]
(ptk/reify ::initialize-remove-graphics
ptk/UpdateEvent
(update [_ state]
(assoc state :remove-graphics {:total total
:current nil
:error false
:completed false}))))
(defn- update-remove-graphics
[current]
(ptk/reify ::update-remove-graphics
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:remove-graphics :current] current))))
(defn- error-in-remove-graphics
[]
(ptk/reify ::error-in-remove-graphics
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:remove-graphics :error] true))))
(defn clear-remove-graphics
[]
(ptk/reify ::clear-remove-graphics
ptk/UpdateEvent
(update [_ state]
(dissoc state :remove-graphics))))
(defn- complete-remove-graphics
[]
(ptk/reify ::complete-remove-graphics
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:remove-graphics :completed] true))
ptk/WatchEvent
(watch [_ state _]
(when-not (get-in state [:remove-graphics :error])
(rx/of (modal/hide))))))
(defn- remove-graphic
[it file-data page [index [media-obj pos]]]
(let [process-shapes
(fn [[shape children]]
(let [changes1 (-> (pcb/empty-changes it)
(pcb/set-save-undo? false)
(pcb/with-page page)
(pcb/with-objects (:objects page))
(pcb/with-library-data file-data)
(pcb/delete-media (:id media-obj))
(pcb/add-objects (cons shape children)))
page' (reduce (fn [page shape]
(ctst/add-shape (:id shape)
shape
page
uuid/zero
uuid/zero
nil
true))
page
(cons shape children))
[_ _ changes2] (cflh/generate-add-component it
[shape]
(:objects page')
(:id page)
(:id file-data)
true
nil
cfsh/prepare-create-artboard-from-selection)
changes (pcb/concat-changes changes1 changes2)]
(dch/commit-changes changes)))
shapes (if (= (:mtype media-obj) "image/svg+xml")
(->> (dwm/load-and-parse-svg media-obj)
(rx/mapcat (partial dwm/create-shapes-svg (:id file-data) (:objects page) pos)))
(dwm/create-shapes-img pos media-obj :wrapper-type :frame))]
(->> (rx/concat
(rx/of (update-remove-graphics index))
(rx/map process-shapes shapes))
(rx/catch #(do
(log/error :msg (str "Error removing " (:name media-obj))
:hint (ex-message %)
:error %)
(js/console.log (.-stack %))
(rx/of (error-in-remove-graphics)))))))
(defn- remove-graphics
[file-id file-name]
(ptk/reify ::remove-graphics
ptk/WatchEvent
(watch [it state stream]
(let [file-data (wsh/get-file state file-id)
grid-gap 50
[file-data' page-id start-pos]
(ctf/get-or-add-library-page file-data grid-gap)
new-page? (nil? (ctpl/get-page file-data page-id))
page (ctpl/get-page file-data' page-id)
media (vals (:media file-data'))
media-points
(map #(assoc % :points (-> (grc/make-rect 0 0 (:width %) (:height %))
(grc/rect->points)))
media)
shape-grid
(ctst/generate-shape-grid media-points start-pos grid-gap)
stoper (rx/filter (ptk/type? ::finalize-file) stream)]
(rx/concat
(rx/of (modal/show {:type :remove-graphics-dialog :file-name file-name})
(initialize-remove-graphics (count media)))
(when new-page?
(rx/of (dch/commit-changes (-> (pcb/empty-changes it)
(pcb/set-save-undo? false)
(pcb/add-page (:id page) page)))))
(->> (rx/mapcat (partial remove-graphic it file-data' page)
(rx/from (d/enumerate (d/zip media shape-grid))))
(rx/take-until stoper))
(rx/of (complete-remove-graphics)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Read only
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

@ -8,7 +8,6 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.features :as ffeat]
[app.common.files.libraries-helpers :as cflh]
[app.common.files.shapes-helpers :as cfsh]
[app.common.geom.point :as gpt]
@ -328,7 +327,7 @@
selected (->> (wsh/lookup-selected state)
(cph/clean-loops objects)
(remove #(ctn/has-any-copy-parent? objects (get objects %)))) ;; We don't want to change the structure of component copies
components-v2 (features/active-feature? state :components-v2)]
components-v2 (features/active-feature? state "components/v2")]
(rx/of (add-component2 selected components-v2))))))
(defn add-multiple-components
@ -337,7 +336,7 @@
(ptk/reify ::add-multiple-components
ptk/WatchEvent
(watch [_ state _]
(let [components-v2 (features/active-feature? state :components-v2)
(let [components-v2 (features/active-feature? state "components/v2")
objects (wsh/lookup-page-objects state)
selected (->> (wsh/lookup-selected state)
(cph/clean-loops objects)
@ -364,7 +363,7 @@
(rx/empty)
(let [data (get state :workspace-data)
[path name] (cph/parse-path-name new-name)
components-v2 (features/active-feature? state :components-v2)
components-v2 (features/active-feature? state "components/v2")
update-fn
(fn [component]
@ -411,7 +410,7 @@
component (ctkl/get-component (:data library) component-id)
new-name (:name component)
components-v2 (features/active-feature? state :components-v2)
components-v2 (features/active-feature? state "components/v2")
main-instance-page (when components-v2
(ctf/get-component-page (:data library) component))
@ -447,7 +446,7 @@
ptk/WatchEvent
(watch [it state _]
(let [data (get state :workspace-data)]
(if (features/active-feature? state :components-v2)
(if (features/active-feature? state "components/v2")
(let [component (ctkl/get-component data id)
page-id (:main-instance-page component)
root-id (:main-instance-id component)]
@ -639,7 +638,7 @@
container (cph/get-container file :page page-id)
components-v2
(features/active-feature? state :components-v2)
(features/active-feature? state "components/v2")
changes
(-> (pcb/empty-changes it)
@ -686,7 +685,7 @@
local-file (wsh/get-local-file state)
container (cph/get-container local-file :page page-id)
shape (ctn/get-shape container id)
components-v2 (features/active-feature? state :components-v2)]
components-v2 (features/active-feature? state "components/v2")]
(when (ctk/instance-head? shape)
(let [libraries (wsh/get-libraries state)
@ -1016,7 +1015,7 @@
(ptk/reify ::watch-component-changes
ptk/WatchEvent
(watch [_ state stream]
(let [components-v2? (features/active-feature? state :components-v2)
(let [components-v2? (features/active-feature? state "components/v2")
stopper-s
(->> stream
@ -1138,9 +1137,7 @@
ptk/WatchEvent
(watch [_ state _]
(let [features (cond-> ffeat/enabled
(features/active-feature? state :components-v2)
(conj "components/v2"))]
(let [features (features/get-team-enabled-features state)]
(rx/merge
(->> (rp/cmd! :link-file-to-library {:file-id file-id :library-id library-id})
(rx/ignore))

View file

@ -139,19 +139,15 @@
(ptk/reify ::persist-changes
ptk/WatchEvent
(watch [_ state _]
(let [;; this features set does not includes the ffeat/enabled
;; because they are already available on the backend and
;; this request provides a set of features to enable in
;; this request.
features (cond-> #{}
(features/active-feature? state :components-v2)
(conj "components/v2"))
sid (:session-id state)
(let [sid (:session-id state)
features (features/get-team-enabled-features state)
params {:id file-id
:revn file-revn
:session-id sid
:changes-with-metadata (into [] changes)
:features features}]
:features features
}]
(->> (rp/cmd! :update-file params)
(rx/mapcat (fn [lagged]
@ -209,7 +205,7 @@
ptk/WatchEvent
(watch [_ state _]
(let [features (cond-> #{}
(features/active-feature? state :components-v2)
(features/active-feature? state "components/v2")
(conj "components/v2"))
sid (:session-id state)
file (dm/get-in state [:workspace-libraries file-id])

View file

@ -104,7 +104,7 @@
page (wsh/lookup-page state page-id)
objects (wsh/lookup-page-objects state page-id)
components-v2 (features/active-feature? state :components-v2)
components-v2 (features/active-feature? state "components/v2")
ids (cph/clean-loops objects ids)

View file

@ -23,37 +23,41 @@
[cuerdas.core :as str]
[potok.core :as ptk]))
(defn extract-name [url]
(let [query-idx (str/last-index-of url "?")
url (if (> query-idx 0) (subs url 0 query-idx) url)
filename (->> (str/split url "/") (last))
(defn extract-name [href]
(let [query-idx (str/last-index-of href "?")
href (if (> query-idx 0) (subs href 0 query-idx) href)
filename (->> (str/split href "/") (last))
ext-idx (str/last-index-of filename ".")]
(if (> ext-idx 0) (subs filename 0 ext-idx) filename)))
(defn upload-images
"Extract all bitmap images inside the svg data, and upload them, associated to the file.
Return a map {<url> <image-data>}."
Return a map {<href> <image-data>}."
[svg-data file-id]
(->> (rx/from (csvg/collect-images svg-data))
(rx/map (fn [uri]
(merge
{:file-id file-id
:is-local true
:url uri}
(if (str/starts-with? uri "data:")
{:name "image"
:content (wapi/data-uri->blob uri)}
{:name (extract-name uri)}))))
(rx/mapcat (fn [uri-data]
(->> (rp/cmd! (if (contains? uri-data :content)
(rx/map (fn [{:keys [href] :as item}]
(let [item (-> item
(assoc :file-id file-id)
(assoc :is-local true)
(assoc :name "image"))]
(if (str/starts-with? href "data:")
(assoc item :content (wapi/data-uri->blob href))
(-> item
(assoc :name (extract-name href))
(assoc :url href))))))
(rx/mapcat (fn [item]
;; TODO: :create-file-media-object-from-url is
;; deprecated and this should be resolved in
;; frontend
(->> (rp/cmd! (if (contains? item :content)
:upload-file-media-object
:create-file-media-object-from-url)
uri-data)
(dissoc item :href))
;; When the image uploaded fail we skip the shape
;; returning `nil` will afterward not create the shape.
(rx/catch #(rx/of nil))
(rx/map #(vector (:url uri-data) %)))))
(rx/reduce (fn [acc [url image]] (assoc acc url image)) {})))
(rx/map #(vector (:href item) %)))))
(rx/reduce conj {})))
(defn add-svg-shapes
[svg-data position]

View file

@ -165,6 +165,25 @@
(defmethod ptk/handle-error :restriction
[{:keys [code] :as error}]
(cond
(= :migration-in-progress code)
(let [message (tr "errors.migration-in-progress" (:feature error))
on-accept (constantly nil)]
(st/emit! (modal/show {:type :alert :message message :on-accept on-accept})))
(= :team-feature-mismatch code)
(let [message (tr "errors.team-feature-mismatch" (:feature error))
on-accept (constantly nil)]
(st/emit! (modal/show {:type :alert :message message :on-accept on-accept})))
(= :file-feature-mismatch code)
(let [message (tr "errors.file-feature-mismatch" (:feature error))
team-id (:current-team-id @st/state)
project-id (:current-project-id @st/state)
on-accept #(if (and project-id team-id)
(st/emit! (rt/nav :dashboard-files {:team-id team-id :project-id project-id}))
(set! (.-href glob/location) ""))]
(st/emit! (modal/show {:type :alert :message message :on-accept on-accept})))
(= :feature-mismatch code)
(let [message (tr "errors.feature-mismatch" (:feature error))
team-id (:current-team-id @st/state)
@ -174,7 +193,7 @@
(set! (.-href glob/location) ""))]
(st/emit! (modal/show {:type :alert :message message :on-accept on-accept})))
(= :features-not-supported code)
(= :feature-not-supported code)
(let [message (tr "errors.feature-not-supported" (:feature error))
team-id (:current-team-id @st/state)
project-id (:current-project-id @st/state)

View file

@ -5,103 +5,116 @@
;; Copyright (c) KALEIDOS INC
(ns app.main.features
"A thin, frontend centric abstraction layer and collection of
helpers for `app.common.features` namespace."
(:require
[app.common.data :as d]
[app.common.features :as cfeat]
[app.common.logging :as log]
[app.config :as cf]
[app.main.store :as st]
[app.util.timers :as tm]
[beicon.core :as rx]
[clojure.set :as set]
[cuerdas.core :as str]
[okulary.core :as l]
[potok.core :as ptk]
[rumext.v2 :as mf]))
(log/set-level! :warn)
(log/set-level! :trace)
(def available-features
#{:components-v2 :new-css-system :grid-layout})
(def global-enabled-features
(cfeat/get-enabled-features cf/flags))
(defn- toggle-feature
(defn get-enabled-features
[state]
(-> (get state :features/runtime #{})
(set/union global-enabled-features)))
(defn get-team-enabled-features
[state]
(-> global-enabled-features
(set/union (get state :features/runtime #{}))
(set/intersection cfeat/no-migration-features)
(set/union (get state :features/team #{}))))
(def features-ref
(l/derived get-team-enabled-features st/state =))
(defn active-feature?
"Given a state and feature, check if feature is enabled"
[state feature]
(assert (contains? cfeat/supported-features feature) "not supported feature")
(or (contains? (get state :features/runtime) feature)
(if (contains? cfeat/no-migration-features feature)
(or (contains? global-enabled-features feature)
(contains? (get state :features/team) feature))
(contains? (get state :features/team state) feature))))
(defn use-feature
"A react hook that checks if feature is currently enabled"
[feature]
(assert (contains? cfeat/supported-features feature) "Not supported feature")
(let [enabled-features (mf/deref features-ref)]
(contains? enabled-features feature)))
(defn toggle-feature
"An event constructor for runtime feature toggle.
Warning: if a feature is active globally or by team, it can't be
disabled."
[feature]
(ptk/reify ::toggle-feature
ptk/UpdateEvent
(update [_ state]
(let [features (or (:features state) #{})]
(if (contains? features feature)
(do
(log/debug :hint "feature disabled" :feature (d/name feature))
(assoc state :features (disj features feature)))
(do
(log/debug :hint "feature enabled" :feature (d/name feature))
(assoc state :features (conj features feature))))))))
(assert (contains? cfeat/supported-features feature) "not supported feature")
(update state :features/runtime (fn [features]
(if (contains? features feature)
(do
(log/trc :hint "feature disabled" :feature feature)
(disj features feature))
(do
(log/trc :hint "feature enabled" :feature feature)
(conj features feature))))))))
(defn- enable-feature
(defn enable-feature
[feature]
(ptk/reify ::enable-feature
ptk/UpdateEvent
(update [_ state]
(let [features (or (:features state) #{})]
(if (contains? features feature)
state
(do
(log/debug :hint "feature enabled" :feature (d/name feature))
(assoc state :features (conj features feature))))))))
(defn toggle-feature!
[feature]
(assert (contains? available-features feature) "Not supported feature")
(tm/schedule-on-idle #(st/emit! (toggle-feature feature))))
(defn enable-feature!
[feature]
(assert (contains? available-features feature) "Not supported feature")
(tm/schedule-on-idle #(st/emit! (enable-feature feature))))
(defn active-feature?
([feature]
(active-feature? @st/state feature))
([state feature]
(assert (contains? available-features feature) "Not supported feature")
(contains? (get state :features) feature)))
(def features
(l/derived :features st/state))
(defn active-feature
[feature]
(l/derived #(contains? % feature) features))
(defn use-feature
[feature]
(assert (contains? available-features feature) "Not supported feature")
(let [active-feature-ref (mf/use-memo (mf/deps feature) #(active-feature feature))
active-feature? (mf/deref active-feature-ref)]
active-feature?))
(assert (contains? cfeat/supported-features feature) "not supported feature")
(if (active-feature? state feature)
state
(do
(log/trc :hint "feature enabled" :feature feature)
(update state :features/runtime (fnil conj #{}) feature))))))
(defn initialize
[]
(ptk/reify ::initialize
ptk/WatchEvent
(watch [_ _ _]
(log/trace :hint "event:initialize" :fn "features")
(rx/concat
;; Enable all features set on the configuration
(->> (rx/from cf/flags)
(rx/map name)
(rx/map (fn [flag]
(when (str/starts-with? flag "frontend-feature-")
(subs flag 17))))
(rx/filter some?)
(rx/map keyword)
(rx/map enable-feature))
([] (initialize #{}))
([team-features]
(assert (set? team-features) "expected a set of features")
(assert (every? string? team-features) "expected a set of strings")
(ptk/reify ::initialize
ptk/UpdateEvent
(update [_ state]
(let [runtime-features (get state :features/runtime #{})
team-features (into cfeat/default-enabled-features
cfeat/xf-supported-features
team-features)]
(-> state
(assoc :features/runtime runtime-features)
(assoc :features/team team-features))))
ptk/WatchEvent
(watch [_ _ _]
(when *assert*
(->> (rx/from cfeat/no-migration-features)
(rx/filter #(not (contains? cfeat/backend-only-features %)))
(rx/observe-on :async)
(rx/map enable-feature))))
ptk/EffectEvent
(effect [_ state _]
(log/trc :hint "initialized features"
:team (str/join "," (:features/team state))
:runtime (str/join "," (:features/runtime state)))))))
;; Enable the rest of available configuration if we are on development
;; environemnt (aka devenv).
(when *assert*
;; By default, all features disabled, except in development
;; environment, that are enabled except components-v2 and new css
(->> (rx/from available-features)
(rx/filter #(not= % :components-v2))
(rx/filter #(not= % :new-css-system))
(rx/map enable-feature)))))))

View file

@ -425,10 +425,6 @@
ids)))
st/state =))
;; Remove this when deprecating components-v2
(def remove-graphics
(l/derived :remove-graphics st/state))
;; ---- Viewer refs
(defn lookup-viewer-objects-by-id

View file

@ -40,7 +40,7 @@
{::mf/wrap [#(mf/catch % {:fallback on-main-error})]}
[{:keys [route profile]}]
(let [{:keys [data params]} route
new-css-system (features/use-feature :new-css-system)]
new-css-system (features/use-feature "styles/v2")]
[:& (mf/provider ctx/current-route) {:value route}
[:& (mf/provider ctx/new-css-system) {:value new-css-system}
(case (:name data)

View file

@ -157,7 +157,7 @@
(mf/with-effect [profile team-id]
(st/emit! (dd/initialize {:id team-id}))
(fn []
(dd/finalize {:id team-id})))
(st/emit! (dd/finalize {:id team-id}))))
(mf/with-effect []
(let [key (events/listen goog/global "keydown"

View file

@ -60,7 +60,7 @@
::mf/register-as :export
::mf/wrap-props false}
[{:keys [team-id files has-libraries? binary?]}]
(let [components-v2 (features/use-feature :components-v2)
(let [components-v2 (features/use-feature "components/v2")
state* (mf/use-state
#(let [files (mapv (fn [file] (assoc file :loading? true)) files)]
{:status :prepare

View file

@ -7,7 +7,6 @@
(ns app.main.ui.dashboard.grid
(:require
[app.common.data.macros :as dm]
[app.common.files.features :as ffeat]
[app.common.geom.point :as gpt]
[app.common.logging :as log]
[app.main.data.dashboard :as dd]
@ -51,22 +50,18 @@
(defn- ask-for-thumbnail
"Creates some hooks to handle the files thumbnails cache"
[file-id revn]
(let [features (cond-> ffeat/enabled
(features/active-feature? :components-v2)
(conj "components/v2"))]
(->> (wrk/ask! {:cmd :thumbnails/generate-for-file
:revn revn
:file-id file-id
:features features})
(rx/mapcat (fn [{:keys [fonts] :as result}]
(->> (fonts/render-font-styles fonts)
(rx/map (fn [styles]
(assoc result
:styles styles
:width 250))))))
(rx/mapcat thr/render)
(rx/mapcat (partial persist-thumbnail file-id revn)))))
(->> (wrk/ask! {:cmd :thumbnails/generate-for-file
:revn revn
:file-id file-id
:features (features/get-team-enabled-features @st/state)})
(rx/mapcat (fn [{:keys [fonts] :as result}]
(->> (fonts/render-font-styles fonts)
(rx/map (fn [styles]
(assoc result
:styles styles
:width 250))))))
(rx/mapcat thr/render)
(rx/mapcat (partial persist-thumbnail file-id revn))))
(mf/defc grid-item-thumbnail
{::mf/wrap-props false}

View file

@ -33,7 +33,7 @@
(sort-by :modified-at)
(reverse))))
components-v2 (features/use-feature :components-v2)
components-v2 (features/use-feature "components/v2")
width (mf/use-state nil)
rowref (mf/use-ref)

View file

@ -255,7 +255,7 @@
(fn []
(st/emit! (dd/fetch-files {:project-id project-id})
(dd/fetch-recent-files (:id team))
(dd/fetch-projects)
(dd/fetch-projects (:id team))
(dd/clear-selected-files))))]
(mf/with-effect

View file

@ -336,7 +336,7 @@
on-leave-as-owner-clicked
(fn []
(st/emit! (dd/fetch-team-members)
(st/emit! (dd/fetch-team-members (:id team))
(modal/show
{:type :leave-and-reassign
:profile profile

View file

@ -355,7 +355,7 @@
(mf/use-fn
(mf/deps profile team on-leave-accepted)
(fn []
(st/emit! (dd/fetch-team-members)
(st/emit! (dd/fetch-team-members (:id team))
(modal/show
{:type :leave-and-reassign
:profile profile
@ -452,8 +452,8 @@
(tr "dashboard.your-penpot")
(:name team)))))
(mf/with-effect []
(st/emit! (dd/fetch-team-members)))
(mf/with-effect [team]
(st/emit! (dd/fetch-team-members (:id team))))
[:*
[:& header {:section :dashboard-team-members :team team}]
@ -992,9 +992,10 @@
(:name team)))))
(mf/with-effect []
(st/emit! (dd/fetch-team-members)
(dd/fetch-team-stats)))
(mf/with-effect [team]
(let [team-id (:id team)]
(st/emit! (dd/fetch-team-members team-id)
(dd/fetch-team-stats team-id))))
[:*
[:& header {:section :dashboard-team-settings :team team}]

View file

@ -89,7 +89,7 @@
{::mf/wrap-props false}
[]
(let [modal (mf/deref modal-ref)
new-css-system (features/use-feature :new-css-system)]
new-css-system (features/use-feature "styles/v2")]
(when modal
[:& (mf/provider ctx/new-css-system) {:value new-css-system}
[:& modal-wrapper {:data modal

View file

@ -42,7 +42,7 @@
(update profile :lang #(or % "")))
form (fm/use-form :spec ::options-form
:initial initial)
new-css-system (features/use-feature :new-css-system)]
new-css-system (features/use-feature "styles/v2")]
[:& fm/form {:class "options-form"
:on-submit on-submit

View file

@ -38,7 +38,6 @@
[app.util.dom :as dom]
[app.util.globals :as globals]
[app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
[goog.events :as events]
[okulary.core :as l]
[rumext.v2 :as mf]))
@ -177,8 +176,8 @@
(make-file-ready-ref file-id))
file-ready? (mf/deref file-ready*)
components-v2? (features/use-feature :components-v2)
new-css-system (features/use-feature :new-css-system)
components-v2? (features/use-feature "components/v2")
new-css-system (features/use-feature "styles/v2")
background-color (:background-color wglobal)]
@ -236,49 +235,3 @@
:wglobal wglobal
:layout layout}]
[:& workspace-loader])])]]]]]]]))
(mf/defc remove-graphics-dialog
{::mf/register modal/components
::mf/register-as :remove-graphics-dialog}
[{:keys [] :as ctx}]
(let [remove-state (mf/deref refs/remove-graphics)
project (mf/deref refs/workspace-project)
close #(modal/hide!)
reload-file #(dom/reload-current-window)
nav-out #(st/emit! (rt/navigate :dashboard-files
{:team-id (:team-id project)
:project-id (:id project)}))]
(mf/use-effect
(fn []
#(st/emit! (dw/clear-remove-graphics))))
[:div.modal-overlay
[:div.modal-container.remove-graphics-dialog
[:div.modal-header
[:div.modal-header-title
[:h2 (tr "workspace.remove-graphics.title" (:file-name ctx))]]
(if (and (:completed remove-state) (:error remove-state))
[:div.modal-close-button
{:on-click close} i/close]
[:div.modal-close-button
{:on-click nav-out}
i/close])]
(if-not (and (:completed remove-state) (:error remove-state))
[:div.modal-content
[:p (tr "workspace.remove-graphics.text1")]
[:p (tr "workspace.remove-graphics.text2")]
[:p.progress-message (tr "workspace.remove-graphics.progress"
(:current remove-state)
(:total remove-state))]]
[:*
[:div.modal-content
[:p.error-message [:span i/close] (tr "workspace.remove-graphics.error-msg")]
[:p (tr "workspace.remove-graphics.error-hint")]]
[:div.modal-footer
[:div.action-buttons
[:input.button-secondary {:type "button"
:value (tr "labels.close")
:on-click close}]
[:input.button-primary {:type "button"
:value (tr "labels.reload-file")
:on-click reload-file}]]]])]]))

View file

@ -443,7 +443,7 @@
(mf/defc context-menu-component
[{:keys [shapes]}]
(let [components-v2 (features/use-feature :components-v2)
(let [components-v2 (features/use-feature "components/v2")
single? (= (count shapes) 1)
objects (deref refs/workspace-page-objects)
any-in-copy? (some true? (map #(ctn/has-any-copy-parent? objects %) shapes))

View file

@ -665,7 +665,7 @@
{::mf/register modal/components
::mf/register-as :libraries-dialog}
[{:keys [starting-tab] :as props :or {starting-tab :libraries}}]
(let [new-css-system (features/use-feature :new-css-system)
(let [new-css-system (features/use-feature "styles/v2")
project (mf/deref refs/workspace-project)
file-data (mf/deref refs/workspace-data)
file (mf/deref ref:workspace-file)

View file

@ -1207,6 +1207,8 @@
grid-justify-content-row (:layout-justify-content values)
grid-justify-content-column (:layout-align-content values)
grid-enabled? (features/use-feature "layout/grid")
set-justify-grid
(mf/use-fn
(mf/deps ids)
@ -1225,7 +1227,7 @@
:class (stl/css-case :title-spacing-layout (not has-layout?))}
(if (and (not multiple) (:layout values))
[:div {:class (stl/css :title-actions)}
(when (features/active-feature? :grid-layout)
(when ^boolean grid-enabled?
[:div {:class (stl/css :layout-options)}
[:& radio-buttons {:selected (d/name layout-type)
:on-change toggle-layout-style
@ -1317,7 +1319,7 @@
[:*
[:span "Layout"]
(if (features/active-feature? :grid-layout)
(if ^boolean grid-enabled?
[:div.title-actions
[:div.layout-btns
[:button {:on-click set-flex

View file

@ -100,7 +100,7 @@
(mf/defc object-svg
[{:keys [page-id file-id share-id object-id render-embed?]}]
(let [components-v2 (feat/use-feature :components-v2)
(let [components-v2 (feat/use-feature "components/v2")
fetch-state (mf/use-fn
(mf/deps file-id page-id share-id object-id components-v2)
(fn []
@ -141,7 +141,7 @@
(mf/defc objects-svg
[{:keys [page-id file-id share-id object-ids render-embed?]}]
(let [components-v2 (feat/use-feature :components-v2)
(let [components-v2 (feat/use-feature "components/v2")
fetch-state (mf/use-fn
(mf/deps file-id page-id share-id components-v2)
(fn []

View file

@ -99,21 +99,21 @@
(rf result input)))))
(defn prettify
"Prepare x fror cleaner output when logged."
"Prepare x for cleaner output when logged."
[x]
(cond
(map? x) (d/mapm #(prettify %2) x)
(vector? x) (mapv prettify x)
(seq? x) (map prettify x)
(set? x) (into #{} (map prettify x))
(set? x) (into #{} (map prettify) x)
(number? x) (mth/precision x 4)
(uuid? x) (str "#uuid " x)
(uuid? x) (str/concat "#uuid " x)
:else x))
(defn ^:export logjs
([str] (tap (partial logjs str)))
([str val]
(js/console.log str (clj->js (prettify val)))
(js/console.log str (clj->js (prettify val) :keyword-fn (fn [v] (str/concat v))))
val))
(when (exists? js/window)
@ -403,7 +403,7 @@
ptk/WatchEvent
(watch [_ state _]
(let [features (cond-> #{}
(features/active-feature? state :components-v2)
(features/active-feature? state "components/v2")
(conj "components/v2"))
sid (:session-id state)
file (get state :workspace-file)

View file

@ -7,18 +7,23 @@
;; This namespace is only to export the functions for toggle features
(ns features
(:require
[app.main.features :as features]))
(defn ^:export components-v2 []
(features/toggle-feature! :components-v2)
nil)
[app.main.features :as features]
[app.main.store :as st]
[app.util.timers :as tm]))
(defn ^:export is-components-v2 []
(let [active? (features/active-feature :components-v2)]
@active?))
(features/active-feature? @st/state "components/v2"))
(defn ^:export new-css-system []
(features/toggle-feature! :new-css-system))
(tm/schedule-on-idle #(st/emit! (features/toggle-feature "styles/v2")))
nil)
(defn ^:export grid []
(features/toggle-feature! :grid-layout))
(tm/schedule-on-idle #(st/emit! (features/toggle-feature "layout/grid")))
nil)
(defn ^:export get-enabled []
(clj->js (features/get-enabled-features @st/state)))
(defn ^:export get-team-enabled []
(clj->js (features/get-team-enabled-features @st/state)))

View file

@ -38,7 +38,7 @@
:pages []
:pages-index {}}
:workspace-libraries {}
:features {:components-v2 true}})
:features/team #{"components/v2"}})
(def ^:private idmap (atom {}))

View file

@ -4174,35 +4174,6 @@ msgstr "Oddělit uzly (%s)"
msgid "workspace.path.actions.snap-nodes"
msgstr "Přichytit uzly (%s)"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-hint"
msgstr ""
"Chcete-li to zkusit znovu, můžete tento soubor znovu načíst. Pokud problém "
"přetrvává, doporučujeme vám podívat se na seznam a zvážit odstranění "
"poškozené grafiky."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-msg"
msgstr "Některé grafiky nebylo možné aktualizovat."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.progress"
msgstr "Převádí se %s/%s"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text1"
msgstr ""
"Grafiky knihovny jsou od nynějška komponenty, díky čemuž budou mnohem "
"výkonnější."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text2"
msgstr "Tato aktualizace je jednorázová."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.title"
msgstr "Aktualizace %s..."
#: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex"
msgstr "Přidat flexibilní rozložení"

View file

@ -4453,35 +4453,6 @@ msgstr "Ankerpunkte trennen (%s)"
msgid "workspace.path.actions.snap-nodes"
msgstr "An Ankerpunkten ausrichten (%s)"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-hint"
msgstr ""
"Um es erneut zu versuchen, können Sie diese Datei neu laden. Wenn das "
"Problem weiterhin besteht, empfehlen wir Ihnen, einen Blick auf die Liste "
"zu werfen und zu überlegen, ob Sie defekte Grafiken löschen wollen."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-msg"
msgstr "Einige Grafiken konnten nicht aktualisiert werden."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.progress"
msgstr "Konvertieren von %s/%s"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text1"
msgstr ""
"Von nun an sind Grafiken in der Bibliothek auch Komponenten. Das macht sie "
"viel leistungsfähiger."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text2"
msgstr "Diese Aktualisierung ist eine einmalige Aktion."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.title"
msgstr "Aktualisierung von %s..."
#: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex"
msgstr "Flex-Layout hinzufügen"

View file

@ -879,10 +879,21 @@ msgstr ""
"Looks like you are opening a file that has the feature '%s' enabled but "
"your penpot frontend does not supports it or has it disabled."
#: src/app/main/errors.cljs
msgid "errors.file-feature-mismatch"
msgstr ""
"It seems that there is a mismatch between the enabled features and the "
"features of the file you are trying to open. Migrations for '%s' need "
"to be applied before the file can be opened."
#: src/app/main/errors.cljs
msgid "errors.feature-not-supported"
msgstr "Feature '%s' is not supported."
#: src/app/main/errors.cljs
msgid "errors.team-feature-mismatch"
msgstr "Detected incompatible feature '%s'"
#: src/app/main/ui/auth/verify_token.cljs, src/app/main/ui/settings/feedback.cljs, src/app/main/ui/dashboard/team.cljs
msgid "errors.generic"
msgstr "Something wrong has happened."
@ -4561,35 +4572,6 @@ msgstr "Separate nodes (%s)"
msgid "workspace.path.actions.snap-nodes"
msgstr "Snap nodes (%s)"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-hint"
msgstr ""
"To try it again, you can reload this file. If the problem persists, we "
"suggest you to take a look at the list and consider to delete broken "
"graphics."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-msg"
msgstr "Some graphics could not be updated."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.progress"
msgstr "Converting %s/%s"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text1"
msgstr ""
"Library Graphics are Components from now on, which will make them much more "
"powerful."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text2"
msgstr "This update is a one time action."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.title"
msgstr "Updating %s..."
#: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex"
msgstr "Add flex layout"

View file

@ -903,6 +903,13 @@ msgstr ""
"pero la aplicacion web de penpot que esta usando no tiene soporte para ella "
"o esta deshabilitada."
#: src/app/main/errors.cljs
msgid "errors.file-feature-mismatch"
msgstr ""
"Parece que hay discordancia entre las features habilitadas y las features "
"del fichero que se esta intentando abrir. Falta aplicar migraciones para "
"'%s' antes de poder abrir el fichero."
#: src/app/main/errors.cljs
msgid "errors.feature-not-supported"
msgstr "Caracteristica no soportada: '%s'."
@ -4650,35 +4657,6 @@ msgstr "Separar nodos (%s)"
msgid "workspace.path.actions.snap-nodes"
msgstr "Alinear nodos (%s)"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-hint"
msgstr ""
"Para intentarlo de nuevo, puedes recargar este archivo. Si el problema "
"persiste, te sugerimos que compruebes la lista y consideres borrar los "
"gráficos que estén mal."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-msg"
msgstr "Algunos gráficos no han podido ser actualizados."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.progress"
msgstr "Convirtiendo %s/%s"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text1"
msgstr ""
"Desde ahora los gráficos de la librería serán componentes, lo cual los hará "
"mucho más potentes."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text2"
msgstr "Esta actualización sólo ocurrirá una vez."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.title"
msgstr "Actualizando %s..."
#: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex"
msgstr "Añadir flex layout"

View file

@ -4173,35 +4173,6 @@ msgstr "Banatu nodoak (%s)"
msgid "workspace.path.actions.snap-nodes"
msgstr "Atxikitu nodoak (%s)"
#: 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.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.text1"
msgstr ""
"Liburutegiko grafikoak osagaiak izango dira orain, horrek ahaltsuago egingo "
"ditu."
#: 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"

View file

@ -4457,32 +4457,6 @@ msgstr "הפרדת מפרקים (%s)"
msgid "workspace.path.actions.snap-nodes"
msgstr "הצמדת מפרקים (%s)"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-hint"
msgstr ""
"כדי לנסות שוב, אפשר לרענן את הקובץ הזה. אם הבעיה נמשכת, אנו ממליצים לך "
"להביט ברשימה ולשקול למחוק גרפיקה פגומה."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-msg"
msgstr "לא ניתן לעדכן חלק מהגרפיקה."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.progress"
msgstr "מתבצעת המרה %s/%s"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text1"
msgstr "גרפיקות ספרייה הן רכיבים מעתה ואילך, מה שהופך אותן להרבה יותר עוצמתיות."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text2"
msgstr "העדכון הזה הוא חד־פעמי."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.title"
msgstr "%s מתעדכן…"
#: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex"
msgstr "הוספת פריסת flex"

View file

@ -4585,35 +4585,6 @@ msgstr "Simpul terpisah (%s)"
msgid "workspace.path.actions.snap-nodes"
msgstr "Tancap simpul (%s)"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-hint"
msgstr ""
"Untuk mencoba lagi, Anda dapat memuat ulang berkas ini. Jika masalah tetap "
"ada, kami menyarankan Anda untuk melihat daftar dan mempertimbangkan untuk "
"menghapus grafis yang rusak."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-msg"
msgstr "Beberapa grafis tidak dapat diperbarui."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.progress"
msgstr "Mengubah %s/%s"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text1"
msgstr ""
"Grafis Pustaka itu Komponen dari sekarang, yang akan membuatnya lebih "
"berdaya."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text2"
msgstr "Pembaruan ini adalah tindakan satu kali."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.title"
msgstr "Memperbarui %s..."
#: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex"
msgstr "Tambahkan tata letak flex"

View file

@ -4568,34 +4568,6 @@ msgstr "Atdalīt mezglus (%s)"
msgid "workspace.path.actions.snap-nodes"
msgstr "Pieķert mezglus (%s)"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-hint"
msgstr ""
"Lai to mēģinātu vēlreiz, varat atkārtoti ielādēt šo failu. Ja problēma "
"joprojām pastāv, ieteicams apskatīt sarakstu un dzēst bojātās grafikas."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-msg"
msgstr "Dažas grafikas nevar atjaunināt."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.progress"
msgstr "%s/%s pārvēršana"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text1"
msgstr ""
"Bibliotēkas grafikas turpmāk sauksies Komponentes, kas padarīs tās daudz "
"jaudīgākas."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text2"
msgstr "Šis atjauninājums ir vienreizēja darbība."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.title"
msgstr "Notiek %s atjaunināšana..."
#: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex"
msgstr "Pievienot elastīgo izkārtojumu"

View file

@ -4537,35 +4537,6 @@ msgstr "Verschillende knooppunten (%s)"
msgid "workspace.path.actions.snap-nodes"
msgstr "Snap knooppunten (%s)"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-hint"
msgstr ""
"Om het opnieuw te proberen, kun je dit bestand opnieuw laden. Als het "
"probleem zich blijft voordoen, raden we aan de lijst te bekijken en te "
"overwegen om kapotte afbeeldingen te verwijderen."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-msg"
msgstr "Sommige afbeeldingen kunnen niet worden bijgewerkt."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.progress"
msgstr "%s/%s converteren"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text1"
msgstr ""
"Bibliotheekafbeeldingen zijn vanaf nu componenten, waardoor ze veel "
"krachtiger worden."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text2"
msgstr "Deze update is een eenmalige actie."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.title"
msgstr "%s bijwerken..."
#: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex"
msgstr "Flex-indeling toevoegen"

View file

@ -4011,35 +4011,6 @@ msgstr "Rozłącz węzły (%s)"
msgid "workspace.path.actions.snap-nodes"
msgstr "Przyciągnij węzły (%s)"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-hint"
msgstr ""
"Aby spróbować ponownie, możesz ponownie załadować ten plik. Jeśli problem "
"będzie się powtarzał, sugerujemy przejrzenie listy i rozważenie usunięcia "
"uszkodzonej grafiki."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-msg"
msgstr "Niektórych grafik nie udało się zaktualizować."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.progress"
msgstr "Konwersja %s/%s"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text1"
msgstr ""
"Grafika biblioteczna jest od teraz komponentami, co sprawi, że będą "
"znacznie potężniejsze."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text2"
msgstr "Ta aktualizacja jest działaniem jednorazowym."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.title"
msgstr "Aktualizowanie %s..."
#: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex"
msgstr "Dodaj układ flex"

View file

@ -3994,35 +3994,6 @@ msgstr "Separar pontos (%s)"
msgid "workspace.path.actions.snap-nodes"
msgstr "Aderir aos pontos (%s)"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-hint"
msgstr ""
"Para tentar novamente, recarregue este arquivo. Se o problema persistir, "
"sugerimos olhar a lista e considerar excluir gráficos que não estejam "
"funcionando."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-msg"
msgstr "Alguns gráficos não puderam ser atualizados."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.progress"
msgstr "Convertendo %s/%s"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text1"
msgstr ""
"A partir de agora os gráficos da biblioteca são Componentes, o que os "
"tornarão bem mais poderosos."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text2"
msgstr "Essa atualização acontecerá apenas uma vez."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.title"
msgstr "Atualizando %s..."
#: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex"
msgstr "Adicionar Flex Layout"

View file

@ -4588,35 +4588,6 @@ msgstr "Separar nós (%s)"
msgid "workspace.path.actions.snap-nodes"
msgstr "Ajustar nós (%s)"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-hint"
msgstr ""
"Para tentar de novo, podes recarregar este ficheiro. Se o problema "
"persistir, sugerimos que observes a lista e consideres em apagar os "
"gráficos problemáticos."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-msg"
msgstr "Não foi possível atualizar alguns gráficos."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.progress"
msgstr "A converter %s/%s"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text1"
msgstr ""
"A partir de agora, os Gráficos da biblioteca passarão a ser Componentes, o "
"que os tornará mais poderosos."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text2"
msgstr "Esta atualização só ocorrerá uma única vez."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.title"
msgstr "A atualizar %s..."
#: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex"
msgstr "Adicionar layout flex"

View file

@ -4626,35 +4626,6 @@ msgstr "Separă noduri (%s)"
msgid "workspace.path.actions.snap-nodes"
msgstr "Trage noduri (%s)"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-hint"
msgstr ""
"Pentru a încerca din nou, puteți reîncărca acest fișier. Dacă problema "
"persistă, vă sugerăm să aruncați o privire pe listă și să luați în "
"considerare ștergerea graficii rupte."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-msg"
msgstr "Unele elemente grafice nu au putut fi actualizate."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.progress"
msgstr "Se convertește %s/%s"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text1"
msgstr ""
"Bibliotecile Grafice sunt Componente de acum înainte, ceea ce le va face "
"mult mai puternice."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text2"
msgstr "Această actualizare este o acțiune unică."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.title"
msgstr "Actualizare %s..."
#: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex"
msgstr "Adăugați aspect flexibil"

View file

@ -4087,35 +4087,6 @@ msgstr "Düğümleri ayır (%s)"
msgid "workspace.path.actions.snap-nodes"
msgstr "Düğümleri tuttur (%s)"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-hint"
msgstr ""
"Tekrar denemek için bu dosyayı yeniden yükleyebilirsiniz. Sorun devam "
"ederse, listeye bir göz atmanızı ve bozuk grafikleri silmeyi düşünmenizi "
"öneririz."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-msg"
msgstr "Bazı grafikler güncellenemedi."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.progress"
msgstr "%s/%s dönüştürülüyor"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text1"
msgstr ""
"Kütüphane Grafikleri bundan böyle Bileşenlerdir ve bu da onları çok daha "
"güçlü kılacaktır."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text2"
msgstr "Bu güncelleme tek seferlik bir işlemdir."
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.title"
msgstr "%s güncelleniyor..."
#: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex"
msgstr "Düzen esnekliği ekle"

View file

@ -4069,30 +4069,6 @@ msgstr "拆分节点(%s"
msgid "workspace.path.actions.snap-nodes"
msgstr "对接节点 (%s)"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-hint"
msgstr "要重试,您可以重新加载此文件。如果问题仍然存在,我们建议您查看列表并考虑删除损坏的图形。"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.error-msg"
msgstr "某些图形无法更新。"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.progress"
msgstr "转换%s/%s"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text1"
msgstr "从现在开始,库图形是组件,这将使它们更加强大。"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.text2"
msgstr "此更新是一次性操作。"
#: src/app/main/ui/workspace.cljs
msgid "workspace.remove-graphics.title"
msgstr "正在更新 %s..."
#: src/app/main/ui/workspace/context_menu.cljs
msgid "workspace.shape.menu.add-flex"
msgstr "添加弹性布局"