Merge branch 'staging' into develop

This commit is contained in:
Andrey Antukh 2025-04-07 11:32:41 +02:00
commit 17f7f920c4
24 changed files with 309 additions and 246 deletions

View file

@ -65,6 +65,7 @@
- Fix available size of resize handler [Taiga #10639](https://tree.taiga.io/project/penpot/issue/10639) - Fix available size of resize handler [Taiga #10639](https://tree.taiga.io/project/penpot/issue/10639)
- Internal error when install a plugin by penpothub - Try plugin [Taiga #10542](https://tree.taiga.io/project/penpot/issue/10542) - Internal error when install a plugin by penpothub - Try plugin [Taiga #10542](https://tree.taiga.io/project/penpot/issue/10542)
- Add character limitation to asset inputs [Taiga #10669](https://tree.taiga.io/project/penpot/issue/10669) - Add character limitation to asset inputs [Taiga #10669](https://tree.taiga.io/project/penpot/issue/10669)
- Fix Storybook link 'list of all available icons' wrong path [Taiga #10705](https://tree.taiga.io/project/penpot/issue/10705)
## 2.5.4 ## 2.5.4

View file

@ -55,7 +55,7 @@
(contains? cf/flags :login-with-password)) (contains? cf/flags :login-with-password))
(ex/raise :type :restriction (ex/raise :type :restriction
:code :login-disabled :code :login-disabled
:hint "login is disabled in this instance")) :hint "login is disabled"))
(letfn [(check-password [cfg profile password] (letfn [(check-password [cfg profile password]
(if (= (:password profile) "!") (if (= (:password profile) "!")
@ -79,7 +79,8 @@
:code :wrong-credentials)) :code :wrong-credentials))
(when (:is-blocked profile) (when (:is-blocked profile)
(ex/raise :type :restriction (ex/raise :type :restriction
:code :profile-blocked)) :code :profile-blocked
:hint "profile is marked as blocked"))
(when-not (check-password cfg profile password) (when-not (check-password cfg profile password)
(ex/raise :type :validation (ex/raise :type :validation
:code :wrong-credentials)) :code :wrong-credentials))
@ -183,11 +184,11 @@
(defn- validate-register-attempt! (defn- validate-register-attempt!
[cfg params] [cfg params]
(when (or (when (or (not (contains? cf/flags :registration))
(not (contains? cf/flags :registration)) (not (contains? cf/flags :login-with-password)))
(not (contains? cf/flags :login-with-password)))
(ex/raise :type :restriction (ex/raise :type :restriction
:code :registration-disabled)) :code :registration-disabled
:hint "registration disabled"))
(when (contains? params :invitation-token) (when (contains? params :invitation-token)
(let [invitation (tokens/verify (::setup/props cfg) (let [invitation (tokens/verify (::setup/props cfg)
@ -201,12 +202,14 @@
(when (and (email.blacklist/enabled? cfg) (when (and (email.blacklist/enabled? cfg)
(email.blacklist/contains? cfg (:email params))) (email.blacklist/contains? cfg (:email params)))
(ex/raise :type :restriction (ex/raise :type :restriction
:code :email-domain-is-not-allowed)) :code :email-domain-is-not-allowed
:hint "email domain in blacklist"))
(when (and (email.whitelist/enabled? cfg) (when (and (email.whitelist/enabled? cfg)
(not (email.whitelist/contains? cfg (:email params)))) (not (email.whitelist/contains? cfg (:email params))))
(ex/raise :type :restriction (ex/raise :type :restriction
:code :email-domain-is-not-allowed)) :code :email-domain-is-not-allowed
:hint "email domain not in whitelist"))
;; Perform a basic validation of email & password ;; Perform a basic validation of email & password
(when (= (str/lower (:email params)) (when (= (str/lower (:email params))
@ -219,13 +222,13 @@
(ex/raise :type :restriction (ex/raise :type :restriction
:code :email-has-permanent-bounces :code :email-has-permanent-bounces
:email (:email params) :email (:email params)
:hint "looks like the email has bounce reports")) :hint "email has bounce reports"))
(when (eml/has-complaint-reports? cfg (:email params)) (when (eml/has-complaint-reports? cfg (:email params))
(ex/raise :type :restriction (ex/raise :type :restriction
:code :email-has-complaints :code :email-has-complaints
:email (:email params) :email (:email params)
:hint "looks like the email has complaint reports"))) :hint "email has complaint reports")))
(defn prepare-register (defn prepare-register
[{:keys [::db/pool] :as cfg} {:keys [email] :as params}] [{:keys [::db/pool] :as cfg} {:keys [email] :as params}]

View file

@ -328,7 +328,7 @@
(-> (cfeat/get-team-enabled-features cf/flags team) (-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-client-features! (:features params)) (cfeat/check-client-features! (:features params))
(cfeat/check-file-features! (:features file) (:features params))) (cfeat/check-file-features! (:features file)))
;; This operation is needed for backward comapatibility with frontends that ;; This operation is needed for backward comapatibility with frontends that
;; does not support pointer-map resolution mechanism; this just resolves the ;; does not support pointer-map resolution mechanism; this just resolves the
@ -490,7 +490,7 @@
_ (-> (cfeat/get-team-enabled-features cf/flags team) _ (-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-client-features! (:features params)) (cfeat/check-client-features! (:features params))
(cfeat/check-file-features! (:features file) (:features params))) (cfeat/check-file-features! (:features file)))
page (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg file-id)] page (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg file-id)]
(let [page-id (or page-id (-> file :data :pages first)) (let [page-id (or page-id (-> file :data :pages first))
@ -737,7 +737,7 @@
(-> (cfeat/get-team-enabled-features cf/flags team) (-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-client-features! (:features params)) (cfeat/check-client-features! (:features params))
(cfeat/check-file-features! (:features file) (:features params))) (cfeat/check-file-features! (:features file)))
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)] (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
{:name (:name file) {:name (:name file)

View file

@ -91,9 +91,6 @@
:project-id project-id) :project-id project-id)
team-id (:id team) team-id (:id team)
;; When we create files, we only need to respect the team
;; features, because some features can be enabled
;; globally, but the team is still not migrated properly.
features (-> (cfeat/get-team-enabled-features cf/flags team) features (-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-client-features! (:features params))) (cfeat/check-client-features! (:features params)))
@ -107,7 +104,7 @@
params (-> params params (-> params
(assoc :profile-id profile-id) (assoc :profile-id profile-id)
(assoc :features (set/difference features cfeat/frontend-only-features)))] (assoc :features features))]
(quotes/check! cfg {::quotes/id ::quotes/files-per-project (quotes/check! cfg {::quotes/id ::quotes/files-per-project
::quotes/team-id team-id ::quotes/team-id team-id
@ -120,7 +117,7 @@
;; to lost team features updating ;; to lost team features updating
;; When newly computed features does not match exactly with ;; When newly computed features does not match exactly with
;; the features defined on team row, we update it. ;; the features defined on team row, we update it
(when (not= features (:features team)) (when (not= features (:features team))
(let [features (db/create-array conn "text" features)] (let [features (db/create-array conn "text" features)]
(db/update! conn :team (db/update! conn :team

View file

@ -212,7 +212,7 @@
(-> (cfeat/get-team-enabled-features cf/flags team) (-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-client-features! (:features params)) (cfeat/check-client-features! (:features params))
(cfeat/check-file-features! (:features file) (:features params))) (cfeat/check-file-features! (:features file)))
{:file-id file-id {:file-id file-id
:revn (:revn file) :revn (:revn file)

View file

@ -142,7 +142,7 @@
features (-> (cfeat/get-team-enabled-features cf/flags team) features (-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-client-features! (:features params)) (cfeat/check-client-features! (:features params))
(cfeat/check-file-features! (:features file) (:features params))) (cfeat/check-file-features! (:features file)))
changes (if changes-with-metadata changes (if changes-with-metadata
(->> changes-with-metadata (mapcat :changes) vec) (->> changes-with-metadata (mapcat :changes) vec)

View file

@ -209,100 +209,116 @@
This method allows send flash notifications to specified target destinations. This method allows send flash notifications to specified target destinations.
The message can be a free text or a preconfigured one. The message can be a free text or a preconfigured one.
The destination can be: all, profile-id, team-id, or a coll of them." The destination can be: all, profile-id, team-id, or a coll of them.
[{:keys [::mbus/msgbus ::db/pool]} & {:keys [dest code message level] It also can be:
:or {code :generic level :info}
:as params}] {:email \"some@example.com\"}
[[:email \"some@example.com\"], ...]
Command examples:
(notify! :dest :all :code :maintenance)
(notify! :dest :all :code :upgrade-version)
"
[& {:keys [dest code message level]
:or {code :generic level :info}
:as params}]
(when-not (contains? #{:success :error :info :warning} level) (when-not (contains? #{:success :error :info :warning} level)
(ex/raise :type :assertion (ex/raise :type :assertion
:code :incorrect-level :code :incorrect-level
:hint (str "level '" level "' not supported"))) :hint (str "level '" level "' not supported")))
(letfn [(send [dest] (let [{:keys [::mbus/msgbus ::db/pool]} main/system
(l/inf :hint "sending notification" :dest (str dest))
(let [message {:type :notification
:code code
:level level
:version (:full cf/version)
:subs-id dest
:message message}
message (->> (dissoc params :dest :code :message :level)
(merge message))]
(mbus/pub! msgbus
:topic (str dest)
:message message)))
(resolve-profile [email] send
(some-> (db/get* pool :profile {:email (str/lower email)} {:columns [:id]}) :id vector)) (fn [dest]
(l/inf :hint "sending notification" :dest (str dest))
(let [message {:type :notification
:code code
:level level
:version (:full cf/version)
:subs-id dest
:message message}
message (->> (dissoc params :dest :code :message :level)
(merge message))]
(mbus/pub! msgbus
:topic dest
:message message)))
(resolve-team [team-id] resolve-profile
(->> (db/query pool :team-profile-rel (fn [email]
{:team-id team-id} (some-> (db/get* pool :profile {:email (str/lower email)} {:columns [:id]}) :id vector))
{:columns [:profile-id]})
(map :profile-id)))
(resolve-dest [dest] resolve-team
(cond (fn [team-id]
(= :all dest) (->> (db/query pool :team-profile-rel
[uuid/zero] {:team-id team-id}
{:columns [:profile-id]})
(map :profile-id)))
(uuid? dest) resolve-dest
[dest] (fn resolve-dest [dest]
(cond
(= :all dest)
[uuid/zero]
(string? dest) (uuid? dest)
(some-> dest h/parse-uuid resolve-dest) [dest]
(nil? dest) (string? dest)
(resolve-dest uuid/zero) (some-> dest h/parse-uuid resolve-dest)
(map? dest) (nil? dest)
(sequence (comp [uuid/zero]
(map vec)
(mapcat resolve-dest))
dest)
(and (vector? dest) (map? dest)
(every? vector? dest)) (sequence (comp
(sequence (comp (map vec)
(map vec) (mapcat resolve-dest))
(mapcat resolve-dest)) dest)
dest)
(and (vector? dest) (and (vector? dest)
(keyword? (first dest))) (every? vector? dest))
(let [[op param] dest] (sequence (comp
(map vec)
(mapcat resolve-dest))
dest)
(and (vector? dest)
(keyword? (first dest)))
(let [[op param] dest]
(cond
(= op :email)
(cond (cond
(= op :email) (and (coll? param)
(cond (every? string? param))
(and (coll? param) (sequence (comp
(every? string? param)) (keep resolve-profile)
(sequence (comp (mapcat identity))
(keep resolve-profile) param)
(mapcat identity))
param)
(string? param) (string? param)
(resolve-profile param)) (resolve-profile param))
(= op :team-id) (= op :team-id)
(cond (cond
(coll? param) (coll? param)
(sequence (comp (sequence (comp
(mapcat resolve-team) (mapcat resolve-team)
(keep h/parse-uuid)) (keep h/parse-uuid))
param) param)
(uuid? param) (uuid? param)
(resolve-team param) (resolve-team param)
(string? param) (string? param)
(some-> param h/parse-uuid resolve-team)) (some-> param h/parse-uuid resolve-team))
(= op :profile-id) (= op :profile-id)
(if (coll? param) (if (coll? param)
(sequence (keep h/parse-uuid) param) (sequence (keep h/parse-uuid) param)
(resolve-dest param))))))] (resolve-dest param))))))]
(->> (resolve-dest dest) (->> (resolve-dest dest)
(filter some?) (filter some?)

View file

@ -85,12 +85,11 @@
;; be applied (per example backend can operate in both modes with or ;; be applied (per example backend can operate in both modes with or
;; without migration applied) ;; without migration applied)
(def no-migration-features (def no-migration-features
(-> #{"fdata/objects-map" (-> #{"layout/grid"
"fdata/pointer-map"
"layout/grid"
"fdata/shape-data-type" "fdata/shape-data-type"
"design-tokens/v1"} "design-tokens/v1"}
(into frontend-only-features))) (into frontend-only-features)
(into backend-only-features)))
(sm/register! (sm/register!
^{::sm/type ::features} ^{::sm/type ::features}
@ -156,7 +155,6 @@
team-features (into #{} xf-remove-ephimeral (:features team))] team-features (into #{} xf-remove-ephimeral (:features team))]
(-> enabled-features (-> enabled-features
(set/intersection no-migration-features) (set/intersection no-migration-features)
(set/difference frontend-only-features)
(set/union team-features)))) (set/union team-features))))
(defn check-client-features! (defn check-client-features!
@ -165,6 +163,8 @@
frontend client" frontend client"
[enabled-features client-features] [enabled-features client-features]
(when (set? client-features) (when (set? client-features)
;; Check if client declares support for features enabled on
;; backend side
(let [not-supported (-> enabled-features (let [not-supported (-> enabled-features
(set/difference client-features) (set/difference client-features)
(set/difference frontend-only-features) (set/difference frontend-only-features)
@ -174,14 +174,6 @@
:code :feature-not-supported :code :feature-not-supported
:feature (first not-supported) :feature (first not-supported)
:hint (str/ffmt "client declares no support for '%' features" :hint (str/ffmt "client declares no support for '%' features"
(str/join "," not-supported)))))
(let [not-supported (set/difference client-features supported-features)]
(when (seq not-supported)
(ex/raise :type :restriction
:code :feature-not-supported
:feature (first not-supported)
:hint (str/ffmt "backend does not support '%' features requested by client"
(str/join "," not-supported)))))) (str/join "," not-supported))))))
enabled-features) enabled-features)
@ -192,63 +184,55 @@
supported by the current backend" supported by the current backend"
[enabled-features] [enabled-features]
(let [not-supported (set/difference enabled-features supported-features)] (let [not-supported (set/difference enabled-features supported-features)]
(when (seq not-supported) (when-let [not-supported (first not-supported)]
(ex/raise :type :restriction (ex/raise :type :restriction
:code :feature-not-supported :code :feature-not-supported
:feature (first not-supported) :feature not-supported
:hint (str/ffmt "features '%' not supported" :hint (str/ffmt "feature '%' not supported on this backend" not-supported)))
(str/join "," not-supported))))) enabled-features))
enabled-features)
(defn check-file-features! (defn check-file-features!
"Function used for check feature compability between currently "Function used for check feature compability between currently
enabled features set on backend with the provided featured set by enabled features set on backend with the provided featured set by
the penpot file" the penpot file"
([enabled-features file-features] [enabled-features file-features]
(check-file-features! enabled-features file-features #{})) (let [file-features (into #{} xf-remove-ephimeral file-features)
([enabled-features file-features client-features] not-supported (-> enabled-features
(let [file-features (into #{} xf-remove-ephimeral file-features) (set/difference file-features)
;; We should ignore all features that does not match with the ;; NOTE: we don't want to raise a feature-mismatch
;; `no-migration-features` set because we can't enable them ;; exception for features which don't require an
;; as-is, because they probably need migrations ;; explicit file migration process or has no real
client-features (set/intersection client-features no-migration-features)] ;; effect on file data structure
(let [not-supported (-> enabled-features (set/difference no-migration-features))]
(set/union client-features)
(set/difference file-features)
;; NOTE: we don't want to raise a feature-mismatch
;; exception for features which don't require an
;; explicit file migration process or has no real
;; effect on file data structure
(set/difference no-migration-features))]
(when (seq not-supported)
(ex/raise :type :restriction
:code :file-feature-mismatch
:feature (first not-supported)
:hint (str/ffmt "enabled features '%' not present in file (missing migration)"
(str/join "," not-supported)))))
(check-supported-features! file-features) (when-let [not-supported (first not-supported)]
(ex/raise :type :restriction
:code :file-feature-mismatch
:feature not-supported
:hint (str/ffmt "enabled feature '%' not present in file (missing migration)"
not-supported)))
(let [not-supported (-> file-features (check-supported-features! file-features)
(set/difference enabled-features)
(set/difference client-features)
(set/difference backend-only-features)
(set/difference frontend-only-features))]
(when (seq not-supported) ;; Components v1 is deprecated
(ex/raise :type :restriction (when-not (contains? file-features "components/v2")
:code :file-feature-mismatch (ex/raise :type :restriction
:feature (first not-supported) :code :file-in-components-v1
:hint (str/ffmt "file features '%' not enabled" :hint "components v1 is deprecated"))
(str/join "," not-supported)))))
;; Components v1 is deprecated (let [not-supported (-> file-features
(when-not (contains? file-features "components/v2") (set/difference enabled-features)
(ex/raise :type :restriction (set/difference backend-only-features)
:code :file-in-components-v1 (set/difference frontend-only-features))]
:hint "components v1 is deprecated")))
enabled-features)) ;; Check if file has a feature but that feature is not enabled
(when-let [not-supported (first not-supported)]
(ex/raise :type :restriction
:code :file-feature-mismatch
:feature not-supported
:hint (str/ffmt "file feature '%' not enabled" not-supported))))
enabled-features))
(defn check-teams-compatibility! (defn check-teams-compatibility!
[{source-features :features} {destination-features :features}] [{source-features :features} {destination-features :features}]

View file

@ -848,7 +848,7 @@ title: Shortcuts
</table> </table>
<h2 id="viewer-section"> View mode </h2> <h2 id="viewer-section"> View mode </h2>
<p>The View mode is the area to present and share designs and play the proptotype interactions. <a href="/user-guide/the-interface/#interface-viewmode">More about the View mode</a>.</p> <p>The View mode is the area to present and share designs and play the prototype interactions. <a href="/user-guide/the-interface/#interface-viewmode">More about the View mode</a>.</p>
<h3 id="generic-viewer">Generic</h3> <h3 id="generic-viewer">Generic</h3>
<table cellspacing="0" cellpadding="1" border="1" width="100%"> <table cellspacing="0" cellpadding="1" border="1" width="100%">

View file

@ -72,7 +72,7 @@
(st/emit! (ntf/hide))) (st/emit! (ntf/hide)))
(defn handle-notification (defn handle-notification
[{:keys [message code level] :as params}] [{:keys [message code] :as params}]
(ptk/reify ::show-notification (ptk/reify ::show-notification
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
@ -80,9 +80,6 @@
:upgrade-version :upgrade-version
(rx/of (ntf/dialog (rx/of (ntf/dialog
:content (tr "notifications.by-code.upgrade-version") :content (tr "notifications.by-code.upgrade-version")
:controls :inline-actions
:type :inline
:level level
:accept {:label (tr "labels.refresh") :accept {:label (tr "labels.refresh")
:callback force-reload!} :callback force-reload!}
:tag :notification)) :tag :notification))
@ -90,16 +87,14 @@
:maintenance :maintenance
(rx/of (ntf/dialog (rx/of (ntf/dialog
:content (tr "notifications.by-code.maintenance") :content (tr "notifications.by-code.maintenance")
:controls :inline-actions
:type level
:accept {:label (tr "labels.accept") :accept {:label (tr "labels.accept")
:callback hide-notifications!} :callback hide-notifications!}
:tag :notification)) :tag :notification))
(rx/of (ntf/dialog (rx/of (ntf/dialog
:content message :content message
:controls :close :accept {:label (tr "labels.close")
:type level :callback hide-notifications!}
:tag :notification)))))) :tag :notification))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

@ -19,7 +19,7 @@
(def ^:private schema:notification (def ^:private schema:notification
[:map {:title "Notification"} [:map {:title "Notification"}
[:level [::sm/one-of #{:success :error :info :warning}]] [:level {:optional true} [::sm/one-of #{:success :error :info :warning}]]
[:status {:optional true} [:status {:optional true}
[::sm/one-of #{:visible :hide}]] [::sm/one-of #{:visible :hide}]]
[:position {:optional true} [:position {:optional true}
@ -129,15 +129,11 @@
:timeout timeout}))) :timeout timeout})))
(defn dialog (defn dialog
[& {:keys [content controls actions accept cancel position tag level links] [& {:keys [content accept cancel tag links]}]
:or {controls :none position :floating level :info}}]
(show (d/without-nils (show (d/without-nils
{:content content {:content content
:level level :type :inline
:links links
:position position
:controls controls
:actions actions
:accept accept :accept accept
:cancel cancel :cancel cancel
:links links
:tag tag}))) :tag tag})))

View file

@ -305,7 +305,8 @@
(rx/take-until stopper-s)))))) (rx/take-until stopper-s))))))
(defn initialize-workspace (defn initialize-workspace
[file-id] [team-id file-id]
(assert (uuid? team-id) "expected valud uuid for `team-id`")
(assert (uuid? file-id) "expected valud uuid for `file-id`") (assert (uuid? file-id) "expected valud uuid for `file-id`")
(ptk/reify ::initialize-workspace (ptk/reify ::initialize-workspace
@ -321,8 +322,7 @@
(watch [_ state stream] (watch [_ state stream]
(let [stoper-s (rx/filter (ptk/type? ::finalize-workspace) stream) (let [stoper-s (rx/filter (ptk/type? ::finalize-workspace) stream)
rparams (rt/get-params state) rparams (rt/get-params state)
team-id (get state :current-team-id) features (features/get-enabled-features state team-id)
features (get state :features)
render-wasm? (contains? features "render-wasm/v1")] render-wasm? (contains? features "render-wasm/v1")]
(log/debug :hint "initialize-workspace" (log/debug :hint "initialize-workspace"
@ -417,7 +417,7 @@
(unchecked-set ug/global "name" name))))) (unchecked-set ug/global "name" name)))))
(defn finalize-workspace (defn finalize-workspace
[file-id] [_team-id file-id]
(ptk/reify ::finalize-workspace (ptk/reify ::finalize-workspace
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
@ -450,8 +450,9 @@
(ptk/reify ::reload-current-file (ptk/reify ::reload-current-file
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [file-id (:current-file-id state)] (let [file-id (:current-file-id state)
(rx/of (initialize-workspace file-id)))))) team-id (:current-team-id state)]
(rx/of (initialize-workspace team-id file-id))))))
;; Make this event callable through dynamic resolution ;; Make this event callable through dynamic resolution
(defmethod ptk/resolve ::reload-current-file [_ _] (reload-current-file)) (defmethod ptk/resolve ::reload-current-file [_ _] (reload-current-file))
@ -492,18 +493,25 @@
(defn initialize-page (defn initialize-page
[file-id page-id] [file-id page-id]
(assert (uuid? file-id) "expected valid uuid for `file-id`") (assert (uuid? file-id) "expected valid uuid for `file-id`")
(assert (uuid? page-id) "expected valid uuid for `page-id`")
(ptk/reify ::initialize-page (ptk/reify ::initialize-page
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(if-let [page (dsh/lookup-page state file-id page-id)] (if-let [page (dsh/lookup-page state file-id page-id)]
(rx/concat (rx/of (initialize-page* file-id page-id page) (rx/concat
(dwth/watch-state-changes file-id page-id) (rx/of (initialize-page* file-id page-id page)
(dwl/watch-component-changes)) (dwth/watch-state-changes file-id page-id)
(let [profile (:profile state) (dwl/watch-component-changes))
props (get profile :props)] (let [profile (:profile state)
(when (not (:workspace-visited props)) props (get profile :props)]
(rx/of (select-frame-tool file-id page-id))))) (when (not (:workspace-visited props))
(rx/of (select-frame-tool file-id page-id)))))
;; NOTE: this redirect is necessary for cases where user
;; explicitly passes an non-existing page-id on the url
;; params, so on check it we can detect that there are no data
;; for the page and redirect user to an existing page
(rx/of (dcm/go-to-workspace :file-id file-id ::rt/replace true)))))) (rx/of (dcm/go-to-workspace :file-id file-id ::rt/replace true))))))
(defn finalize-page (defn finalize-page

View file

@ -65,7 +65,6 @@
(->> (rx/from initmsg) (->> (rx/from initmsg)
(rx/map dws/send)) (rx/map dws/send))
;; Subscribe to notifications of the subscription ;; Subscribe to notifications of the subscription
(->> stream (->> stream
(rx/filter (ptk/type? ::dws/message)) (rx/filter (ptk/type? ::dws/message))

View file

@ -10,6 +10,7 @@
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.main.data.event :as ev] [app.main.data.event :as ev]
[app.main.data.helpers :as dsh]
[app.main.data.persistence :as dwp] [app.main.data.persistence :as dwp]
[app.main.data.workspace :as dw] [app.main.data.workspace :as dw]
[app.main.data.workspace.thumbnails :as th] [app.main.data.workspace.thumbnails :as th]
@ -97,7 +98,8 @@
(ptk/reify ::restore-version (ptk/reify ::restore-version
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [file-id (:current-file-id state)] (let [file-id (:current-file-id state)
team-id (:current-team-id state)]
(rx/concat (rx/concat
(rx/of ::dwp/force-persist (rx/of ::dwp/force-persist
(dw/remove-layout-flag :document-history)) (dw/remove-layout-flag :document-history))
@ -106,7 +108,7 @@
(rx/take 1) (rx/take 1)
(rx/mapcat #(rp/cmd! :restore-file-snapshot {:file-id file-id :id id})) (rx/mapcat #(rp/cmd! :restore-file-snapshot {:file-id file-id :id id}))
(rx/tap #(th/clear-queue!)) (rx/tap #(th/clear-queue!))
(rx/map #(dw/initialize-workspace file-id))) (rx/map #(dw/initialize-workspace team-id file-id)))
(case origin (case origin
:version :version
(rx/of (ptk/event ::ev/event {::ev/name "restore-pin-version"})) (rx/of (ptk/event ::ev/event {::ev/name "restore-pin-version"}))
@ -200,21 +202,23 @@
(ptk/reify ::restore-version-from-plugins (ptk/reify ::restore-version-from-plugins
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ state _]
(rx/concat (let [file (dsh/lookup-file state file-id)
(rx/of (ptk/event ::ev/event {::ev/name "restore-version-plugin"}) team-id (or (:team-id file) (:current-file-id state))]
::dwp/force-persist) (rx/concat
(rx/of (ptk/event ::ev/event {::ev/name "restore-version-plugin"})
::dwp/force-persist)
;; FIXME: we should abstract this ;; FIXME: we should abstract this
(->> (rx/from-atom refs/persistence-state {:emit-current-value? true}) (->> (rx/from-atom refs/persistence-state {:emit-current-value? true})
(rx/filter #(or (nil? %) (= :saved %))) (rx/filter #(or (nil? %) (= :saved %)))
(rx/take 1) (rx/take 1)
(rx/mapcat #(rp/cmd! :restore-file-snapshot {:file-id file-id :id id})) (rx/mapcat #(rp/cmd! :restore-file-snapshot {:file-id file-id :id id}))
(rx/map #(dw/initialize-workspace file-id))) (rx/map #(dw/initialize-workspace team-id file-id)))
(->> (rx/of 1) (->> (rx/of 1)
(rx/tap resolve) (rx/tap resolve)
(rx/ignore)))))) (rx/ignore)))))))

View file

@ -8,6 +8,7 @@
"A thin, frontend centric abstraction layer and collection of "A thin, frontend centric abstraction layer and collection of
helpers for `app.common.features` namespace." helpers for `app.common.features` namespace."
(:require (:require
[app.common.data.macros :as dm]
[app.common.features :as cfeat] [app.common.features :as cfeat]
[app.common.logging :as log] [app.common.logging :as log]
[app.config :as cf] [app.config :as cf]
@ -24,6 +25,15 @@
(def global-enabled-features (def global-enabled-features
(cfeat/get-enabled-features cf/flags)) (cfeat/get-enabled-features cf/flags))
(defn get-enabled-features
"An explicit lookup of enabled features for the current team"
[state team-id]
(let [team (dm/get-in state [:teams team-id])]
(-> global-enabled-features
(set/union (get state :features-runtime #{}))
(set/difference cfeat/no-migration-features)
(set/union (get team :features)))))
(defn active-feature? (defn active-feature?
"Given a state and feature, check if feature is enabled." "Given a state and feature, check if feature is enabled."
[state feature] [state feature]

View file

@ -370,6 +370,6 @@
(if edata (if edata
[:> static/exception-page* {:data edata :route route}] [:> static/exception-page* {:data edata :route route}]
[:> error-boundary* {:fallback static/internal-error*} [:> error-boundary* {:fallback static/internal-error*}
[:& notifications/current-notification] [:> notifications/current-notification*]
(when route (when route
[:> page* {:route route :profile profile}])])]])) [:> page* {:route route :profile profile}])])]]))

View file

@ -5,7 +5,7 @@ import * as IconStories from "./icon.stories"
# Iconography # Iconography
See the [list of all available icons](?path=/story/foundations-icons--all-icons). See the [list of all available icons](?path=/story/foundations-assets-icon--all).
## Variants ## Variants

View file

@ -17,10 +17,10 @@
[:class {:optional true} :string] [:class {:optional true} :string]
[:variant {:optional true} [:variant {:optional true}
[:maybe [:enum "default" "error"]]] [:maybe [:enum "default" "error"]]]
[:accept-label {:optional true} :string] [:accept-label {:optional true} [:maybe :string]]
[:cancel-label {:optional true} :string] [:cancel-label {:optional true} [:maybe :string]]
[:on-accept {:optional true} [:fn fn?]] [:on-accept {:optional true} [:maybe [:fn fn?]]]
[:on-cancel {:optional true} [:fn fn?]]]) [:on-cancel {:optional true} [:maybe [:fn fn?]]]])
(mf/defc actionable* (mf/defc actionable*
{::mf/schema schema:actionable} {::mf/schema schema:actionable}
@ -45,9 +45,13 @@
[:> :aside props [:> :aside props
[:div {:class (stl/css :notification-message)} children] [:div {:class (stl/css :notification-message)} children]
[:> button* {:variant "secondary"
:on-click on-cancel} (when cancel-label
cancel-label] [:> button* {:variant "secondary"
[:> button* {:variant (if (= variant "default") "primary" "destructive") :on-click on-cancel}
:on-click on-accept} cancel-label])
accept-label]]))
(when accept-label
[:> button* {:variant (if (= variant "default") "primary" "destructive")
:on-click on-accept}
accept-label])]))

View file

@ -14,10 +14,10 @@
[okulary.core :as l] [okulary.core :as l]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(def ref:notification (def ^:private ref:notification
(l/derived :notification st/state)) (l/derived :notification st/state))
(mf/defc current-notification (mf/defc current-notification*
[] []
(let [notification (mf/deref ref:notification) (let [notification (mf/deref ref:notification)
on-close (mf/use-fn #(st/emit! (ntf/hide))) on-close (mf/use-fn #(st/emit! (ntf/hide)))

View file

@ -9,6 +9,7 @@
(:require (:require
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.main.data.common :as dcm] [app.main.data.common :as dcm]
[app.main.data.helpers :as dsh]
[app.main.data.persistence :as dps] [app.main.data.persistence :as dps]
[app.main.data.plugins :as dpl] [app.main.data.plugins :as dpl]
[app.main.data.workspace :as dw] [app.main.data.workspace :as dw]
@ -45,9 +46,10 @@
(mf/defc workspace-content* (mf/defc workspace-content*
{::mf/private true} {::mf/private true}
[{:keys [file layout page wglobal]}] [{:keys [file layout page wglobal]}]
(let [palete-size (mf/use-state nil) (let [palete-size (mf/use-state nil)
selected (mf/deref refs/selected-shapes) selected (mf/deref refs/selected-shapes)
page-id (:id page) page-id (get page :id)
{:keys [vport] :as wlocal} (mf/deref refs/workspace-local) {:keys [vport] :as wlocal} (mf/deref refs/workspace-local)
{:keys [options-mode]} wglobal {:keys [options-mode]} wglobal
@ -120,10 +122,46 @@
:overlay true :overlay true
:file-loading true}]) :file-loading true}])
(defn- make-team-ref
[team-id]
(l/derived (fn [state]
(let [teams (get state :teams)]
(get teams team-id)))
st/state))
(defn- make-file-ref
[file-id]
(l/derived (fn [state]
;; NOTE: for ensure ordering of execution, we need to
;; wait the file initialization completly success until
;; mark this file availablea and unlock the rendering
;; of the following components
(when (= (get state :current-file-id) file-id)
(let [files (get state :files)
file (get files file-id)]
(-> file
(dissoc :data)
(assoc ::has-data (contains? file :data))))))
st/state))
(defn- make-page-ref
[file-id page-id]
(l/derived (fn [state]
(let [current-page-id (get state :current-page-id)]
;; NOTE: for ensure ordering of execution, we need to
;; wait the page initialization completly success until
;; mark this file availablea and unlock the rendering
;; of the following components
(when (= current-page-id page-id)
(dsh/lookup-page state file-id page-id))))
st/state))
(mf/defc workspace-page* (mf/defc workspace-page*
{::mf/private true} {::mf/private true}
[{:keys [page-id file-id file layout wglobal]}] [{:keys [page-id file-id file layout wglobal]}]
(let [page (mf/deref refs/workspace-page)] (let [page-ref (mf/with-memo [file-id page-id]
(make-page-ref file-id page-id))
page (mf/deref page-ref)]
(mf/with-effect [] (mf/with-effect []
(let [focus-out #(st/emit! (dw/workspace-focus-lost)) (let [focus-out #(st/emit! (dw/workspace-focus-lost))
@ -133,8 +171,7 @@
(mf/with-effect [file-id page-id] (mf/with-effect [file-id page-id]
(st/emit! (dw/initialize-page file-id page-id)) (st/emit! (dw/initialize-page file-id page-id))
(fn [] (fn []
(when page-id (st/emit! (dw/finalize-page file-id page-id))))
(st/emit! (dw/finalize-page file-id page-id)))))
(if (some? page) (if (some? page)
[:> workspace-content* {:file file [:> workspace-content* {:file file
@ -143,18 +180,9 @@
:layout layout}] :layout layout}]
[:> workspace-loader*]))) [:> workspace-loader*])))
(def ^:private ref:file-without-data
(l/derived (fn [file]
(-> file
(dissoc :data)
(assoc ::has-data (contains? file :data))))
refs/file
=))
(mf/defc workspace* (mf/defc workspace*
{::mf/props :obj {::mf/wrap [mf/memo]}
::mf/wrap [mf/memo]} [{:keys [team-id project-id file-id page-id layout-name]}]
[{:keys [project-id file-id page-id layout-name]}]
(let [file-id (hooks/use-equal-memo file-id) (let [file-id (hooks/use-equal-memo file-id)
page-id (hooks/use-equal-memo page-id) page-id (hooks/use-equal-memo page-id)
@ -162,8 +190,15 @@
layout (mf/deref refs/workspace-layout) layout (mf/deref refs/workspace-layout)
wglobal (mf/deref refs/workspace-global) wglobal (mf/deref refs/workspace-global)
team (mf/deref refs/team) team-ref (mf/with-memo [team-id]
file (mf/deref ref:file-without-data) (make-team-ref team-id))
file-ref (mf/with-memo [file-id]
(make-file-ref file-id))
team (mf/deref team-ref)
file (mf/deref file-ref)
file-loaded? (get file ::has-data)
file-name (:name file) file-name (:name file)
permissions (:permissions team) permissions (:permissions team)
@ -187,14 +222,14 @@
(when file-name (when file-name
(dom/set-html-title (tr "title.workspace" file-name)))) (dom/set-html-title (tr "title.workspace" file-name))))
(mf/with-effect [file-id] (mf/with-effect [team-id file-id]
(st/emit! (dw/initialize-workspace file-id)) (st/emit! (dw/initialize-workspace team-id file-id))
(fn [] (fn []
(st/emit! ::dps/force-persist (st/emit! ::dps/force-persist
(dw/finalize-workspace file-id)))) (dw/finalize-workspace team-id file-id))))
(mf/with-effect [file page-id] (mf/with-effect [file-id page-id file-loaded?]
(when-not page-id (when (and file-loaded? (not page-id))
(st/emit! (dcm/go-to-workspace :file-id file-id ::rt/replace true)))) (st/emit! (dcm/go-to-workspace :file-id file-id ::rt/replace true))))
[:> (mf/provider ctx/current-project-id) {:value project-id} [:> (mf/provider ctx/current-project-id) {:value project-id}
@ -207,8 +242,7 @@
:style {:background-color background-color :style {:background-color background-color
:touch-action "none"}} :touch-action "none"}}
[:> context-menu*] [:> context-menu*]
(if (and file-loaded? page-id)
(if (::has-data file)
[:> workspace-page* [:> workspace-page*
{:page-id page-id {:page-id page-id
:file-id file-id :file-id file-id

View file

@ -17,8 +17,10 @@
[] []
(let [worker (uw/init cf/worker-uri err/on-error)] (let [worker (uw/init cf/worker-uri err/on-error)]
(uw/ask! worker {:cmd :configure (uw/ask! worker {:cmd :configure
:key :public-uri :config {:public-uri cf/public-uri
:val cf/public-uri}) :build-data cf/build-date
:version cf/version}})
(set! instance worker))) (set! instance worker)))
(defn ask! (defn ask!

View file

@ -104,7 +104,9 @@
(defn send! (defn send!
[ws msg] [ws msg]
(-send ws (t/encode-str msg))) (if *assert*
(-send ws (t/encode-str msg {:type :json-verbose}))
(-send ws (t/encode-str msg))))
(defn close! (defn close!
[ws] [ws]

View file

@ -135,15 +135,15 @@
(rx/debounce 1) (rx/debounce 1)
(rx/subs! (fn [[messages dropped last]] (rx/subs! (fn [[messages dropped last]]
;; Send back the dropped messages replies ;; Send back the dropped messages replies
(doseq [msg dropped] (doseq [msg dropped]
(drop-message msg)) (drop-message msg))
;; Process the message ;; Process the message
(doseq [msg (vals messages)] (doseq [msg (vals messages)]
(handle-message msg)) (handle-message msg))
;; After process the buffer we send a clear ;; After process the buffer we send a clear
(when-not (= last ::clear) (when-not (= last ::clear)
(rx/push! buffer ::clear))))))) (rx/push! buffer ::clear)))))))

View file

@ -50,8 +50,16 @@
(handler (assoc message :cmd :snaps/update-page-index)))) (handler (assoc message :cmd :snaps/update-page-index))))
(defmethod handler :configure (defmethod handler :configure
[{:keys [key val]}] [{:keys [config]}]
(log/info :hint "configure worker" :key key :val (dm/str val)) (log/info :hint "configure worker" :keys (keys config))
(case key
:public-uri (when-let [public-uri (get config :public-uri)]
(set! cf/public-uri val))) (set! cf/public-uri public-uri))
(when-let [version (get config :version)]
(set! cf/version version))
(when-let [build-date (get config :build-data)]
(set! cf/build-date build-date))
nil)