Merge remote-tracking branch 'penpot/develop' into token-studio-develop

This commit is contained in:
Florian Schroedl 2024-08-06 11:06:51 +02:00
commit 5fbbdd36fd
710 changed files with 33332 additions and 24717 deletions

View file

@ -69,7 +69,8 @@
;;:enable-onboarding-questions
;;:enable-onboarding-newsletter
:enable-dashboard-templates-section
:enable-google-fonts-provider])
:enable-google-fonts-provider
:enable-component-thumbnails])
(defn- parse-flags
[global]
@ -109,6 +110,7 @@
(def privacy-policy-uri (obj/get global "penpotPrivacyPolicyURI" "https://penpot.app/privacy"))
(def flex-help-uri (obj/get global "penpotGridHelpURI" "https://help.penpot.app/user-guide/flexible-layouts/"))
(def grid-help-uri (obj/get global "penpotGridHelpURI" "https://help.penpot.app/user-guide/flexible-layouts/"))
(def plugins-list-uri (obj/get global "penpotPluginsListUri" "https://penpot-docs-plugins.pages.dev/technical-guide/plugins/getting-started/#examples"))
(defn- normalize-uri
[uri-str]
@ -130,9 +132,16 @@
(def worker-uri
(obj/get global "penpotWorkerURI" "/js/worker.js"))
(defn external-feature-flag [flag value]
(when-let [fn (obj/get global "externalFeatureFlag")]
(fn flag value)))
(defn external-feature-flag
[flag value]
(let [f (obj/get global "externalFeatureFlag")]
(when (fn? f)
(f flag value))))
(defn external-session-id
[]
(let [f (obj/get global "externalSessionId")]
(when (fn? f) (f))))
;; --- Helper Functions
@ -158,6 +167,10 @@
(avatars/generate {:name name})
(dm/str (u/join public-uri "assets/by-id/" photo-id))))
(defn resolve-media
[id]
(dm/str (u/join public-uri "assets/by-id/" (str id))))
(defn resolve-file-media
([media]
(resolve-file-media media false))

View file

@ -12,13 +12,13 @@
[app.common.media :as cm]
[app.common.types.components-list :as ctkl]
[app.common.uuid :as uuid]
[app.util.dom :as dom]
[app.util.json :as json]
[app.util.webapi :as wapi]
[app.util.zip :as uz]
[app.worker.export :as e]
[beicon.v2.core :as rx]
[cuerdas.core :as str]))
[cuerdas.core :as str]
[promesa.core :as p]))
(defn parse-data [data]
(as-> data $
@ -262,12 +262,16 @@
(uuid/next))
(export [_]
(->> (export-file file)
(rx/subs!
(fn [value]
(when (not (contains? value :type))
(let [[file export-blob] value]
(dom/trigger-download (:name file) export-blob))))))))
(p/create
(fn [resolve reject]
(->> (export-file file)
(rx/take 1)
(rx/subs!
(fn [value]
(when (not (contains? value :type))
(let [[_ export-blob] value]
(resolve export-blob))))
reject))))))
(defn create-file-export [^string name]
(binding [cfeat/*current* cfeat/default-features]

View file

@ -105,8 +105,7 @@
(rx/map deref)
(rx/filter du/is-authenticated?)
(rx/take 1)
(rx/map #(ws/initialize))
(rx/tap #(plugins/init!)))))))
(rx/map #(ws/initialize)))))))
(defn ^:export init
[]
@ -116,7 +115,8 @@
(cur/init-styles)
(thr/init!)
(init-ui)
(st/emit! (initialize)))
(st/emit! (plugins/initialize)
(initialize)))
(defn ^:export reinit
([]

View file

@ -0,0 +1,189 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.main.data.changes
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.changes :as cpc]
[app.common.logging :as log]
[app.common.types.shape-tree :as ctst]
[app.common.uuid :as uuid]
[app.main.features :as features]
[app.main.worker :as uw]
[app.util.time :as dt]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
;; Change this to :info :debug or :trace to debug this module
(log/set-level! :debug)
(def page-change?
#{:add-page :mod-page :del-page :mov-page})
(def update-layout-attr?
#{:hidden})
(def commit?
(ptk/type? ::commit))
(defn- fix-page-id
"For events that modifies the page, page-id does not comes
as a property so we assign it from the `id` property."
[{:keys [id type page] :as change}]
(cond-> change
(and (page-change? type)
(nil? (:page-id change)))
(assoc :page-id (or id (:id page)))))
(defn- update-indexes
"Given a commit, send the changes to the worker for updating the
indexes."
[commit attr]
(ptk/reify ::update-indexes
ptk/WatchEvent
(watch [_ _ _]
(let [changes (->> (get commit attr)
(map fix-page-id)
(filter :page-id)
(group-by :page-id))]
(->> (rx/from changes)
(rx/merge-map (fn [[page-id changes]]
(log/debug :hint "update-indexes" :page-id page-id :changes (count changes))
(uw/ask! {:cmd :update-page-index
:page-id page-id
:changes changes})))
(rx/ignore))))))
(defn- get-pending-commits
[{:keys [persistence]}]
(->> (:queue persistence)
(map (d/getf (:index persistence)))
(not-empty)))
(def ^:private xf:map-page-id
(map :page-id))
(defn- apply-changes-localy
[{:keys [file-id redo-changes] :as commit} pending]
(ptk/reify ::apply-changes-localy
ptk/UpdateEvent
(update [_ state]
(let [current-file-id (get state :current-file-id)
path (if (= file-id current-file-id)
[:workspace-data]
[:workspace-libraries file-id :data])
undo-changes (if pending
(->> pending
(map :undo-changes)
(reverse)
(mapcat identity)
(vec))
nil)
redo-changes (if pending
(into redo-changes
(mapcat :redo-changes)
pending)
redo-changes)]
(d/update-in-when state path
(fn [file]
(let [file (cpc/process-changes file undo-changes false)
file (cpc/process-changes file redo-changes false)
pids (into #{} xf:map-page-id redo-changes)]
(reduce #(ctst/update-object-indices %1 %2) file pids))))))))
(defn commit
"Create a commit event instance"
[{:keys [commit-id redo-changes undo-changes origin save-undo? features
file-id file-revn undo-group tags stack-undo? source]}]
(dm/assert!
"expect valid vector of changes"
(and (cpc/check-changes! redo-changes)
(cpc/check-changes! undo-changes)))
(let [commit-id (or commit-id (uuid/next))
source (d/nilv source :local)
local? (= source :local)
commit {:id commit-id
:created-at (dt/now)
:source source
:origin (ptk/type origin)
:features features
:file-id file-id
:file-revn file-revn
:changes redo-changes
:redo-changes redo-changes
:undo-changes undo-changes
:save-undo? save-undo?
:undo-group undo-group
:tags tags
:stack-undo? stack-undo?}]
(ptk/reify ::commit
cljs.core/IDeref
(-deref [_] commit)
ptk/WatchEvent
(watch [_ state _]
(let [pending (when-not local?
(get-pending-commits state))]
(rx/concat
(rx/of (apply-changes-localy commit pending))
(if pending
(rx/concat
(->> (rx/from (reverse pending))
(rx/map (fn [commit] (update-indexes commit :undo-changes))))
(rx/of (update-indexes commit :redo-changes))
(->> (rx/from pending)
(rx/map (fn [commit] (update-indexes commit :redo-changes)))))
(rx/of (update-indexes commit :redo-changes)))))))))
(defn- resolve-file-revn
[state file-id]
(let [file (:workspace-file state)]
(if (= (:id file) file-id)
(:revn file)
(dm/get-in state [:workspace-libraries file-id :revn]))))
(defn commit-changes
"Schedules a list of changes to execute now, and add the corresponding undo changes to
the undo stack.
Options:
- save-undo?: if set to false, do not add undo changes.
- undo-group: if some consecutive changes (or even transactions) share the same
undo-group, they will be undone or redone in a single step
"
[{:keys [redo-changes undo-changes save-undo? undo-group tags stack-undo? file-id]
:or {save-undo? true
stack-undo? false
undo-group (uuid/next)
tags #{}}
:as params}]
(ptk/reify ::commit-changes
ptk/WatchEvent
(watch [_ state _]
(let [file-id (or file-id (:current-file-id state))
uchg (vec undo-changes)
rchg (vec redo-changes)
features (features/get-team-enabled-features state)]
(rx/of (-> params
(assoc :undo-group undo-group)
(assoc :features features)
(assoc :tags tags)
(assoc :stack-undo? stack-undo?)
(assoc :save-undo? save-undo?)
(assoc :file-id file-id)
(assoc :file-revn (resolve-file-revn state file-id))
(assoc :undo-changes uchg)
(assoc :redo-changes rchg)
(commit)))))))

View file

@ -13,6 +13,7 @@
[app.main.data.modal :as modal]
[app.main.features :as features]
[app.main.repo :as rp]
[app.main.store :as st]
[app.util.i18n :refer [tr]]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
@ -58,6 +59,10 @@
[]
(.reload js/location))
(defn hide-notifications!
[]
(st/emit! msg/hide))
(defn handle-notification
[{:keys [message code level] :as params}]
(ptk/reify ::show-notification
@ -75,6 +80,15 @@
:actions [{:label "Refresh" :callback force-reload!}]
:tag :notification)))
:maintenance
(rx/of (msg/dialog
:content (tr "notifications.by-code.maintenance")
:controls :inline-actions
:type level
:actions [{:label (tr "labels.accept")
:callback hide-notifications!}]
:tag :notification))
(rx/of (msg/dialog
:content message
:controls :close

View file

@ -405,12 +405,13 @@
(dm/assert! (string? name))
(ptk/reify ::create-team
ptk/WatchEvent
(watch [_ state _]
(watch [it state _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)
features (features/get-enabled-features state)]
(->> (rp/cmd! :create-team {:name name :features features})
features (features/get-enabled-features state)
params {:name name :features features}]
(->> (rp/cmd! :create-team (with-meta params (meta it)))
(rx/tap on-success)
(rx/map team-created)
(rx/catch on-error))))))
@ -421,7 +422,7 @@
[{:keys [name emails role] :as params}]
(ptk/reify ::create-team-with-invitations
ptk/WatchEvent
(watch [_ state _]
(watch [it state _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)
@ -430,7 +431,7 @@
:emails emails
:role role
:features features}]
(->> (rp/cmd! :create-team-with-invitations params)
(->> (rp/cmd! :create-team-with-invitations (with-meta params (meta it)))
(rx/tap on-success)
(rx/map team-created)
(rx/catch on-error))))))
@ -553,12 +554,12 @@
:resend resend?})
ptk/WatchEvent
(watch [_ _ _]
(watch [it _ _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)
params (dissoc params :resend?)]
(->> (rp/cmd! :create-team-invitations params)
(->> (rp/cmd! :create-team-invitations (with-meta params (meta it)))
(rx/tap on-success)
(rx/catch on-error))))))
@ -897,8 +898,7 @@
(-> state
(d/update-in-when [:dashboard-files id :is-shared] (constantly is-shared))
(d/update-in-when [:dashboard-recent-files id :is-shared] (constantly is-shared))
(cond->
(not is-shared)
(cond-> (not is-shared)
(d/update-when :dashboard-shared-files dissoc id))))
ptk/WatchEvent
@ -908,7 +908,7 @@
(rx/ignore))))))
(defn set-file-thumbnail
[file-id thumbnail-uri]
[file-id thumbnail-id]
(ptk/reify ::set-file-thumbnail
ptk/UpdateEvent
(update [_ state]
@ -916,10 +916,10 @@
(->> files
(mapv #(cond-> %
(= file-id (:id %))
(assoc :thumbnail-uri thumbnail-uri)))))]
(assoc :thumbnail-id thumbnail-id)))))]
(-> state
(d/update-in-when [:dashboard-files file-id] assoc :thumbnail-uri thumbnail-uri)
(d/update-in-when [:dashboard-recent-files file-id] assoc :thumbnail-uri thumbnail-uri)
(d/update-in-when [:dashboard-files file-id] assoc :thumbnail-id thumbnail-id)
(d/update-in-when [:dashboard-recent-files file-id] assoc :thumbnail-id thumbnail-id)
(d/update-when :dashboard-search-result update-search-files))))))
;; --- EVENT: create-file

View file

@ -168,7 +168,7 @@
ptk/EffectEvent
(effect [_ _ stream]
(let [session (atom nil)
stopper (rx/filter (ptk/type? ::initialize) stream)
stopper (rx/filter (ptk/type? ::initialize) stream)
buffer (atom #queue [])
profile (->> (rx/from-atom storage {:emit-current-value? true})
(rx/map :profile)
@ -213,7 +213,9 @@
(let [session* (or @session (dt/now))
context (-> @context
(merge (:context event))
(assoc :session session*))]
(assoc :session session*)
(assoc :external-session-id (cf/external-session-id))
(d/without-nils))]
(reset! session session*)
(-> event
(assoc :timestamp (dt/now))

View file

@ -8,7 +8,7 @@
(:require
[app.common.uuid :as uuid]
[app.main.data.modal :as modal]
[app.main.data.workspace.persistence :as dwp]
[app.main.data.persistence :as dwp]
[app.main.data.workspace.state-helpers :as wsh]
[app.main.refs :as refs]
[app.main.repo :as rp]

View file

@ -15,42 +15,42 @@
(declare hide)
(declare show)
(def default-animation-timeout 600)
(def default-timeout 7000)
(def ^:private
schema:message
(sm/define
[:map {:title "Message"}
[:type [::sm/one-of #{:success :error :info :warning}]]
[:status {:optional true}
[::sm/one-of #{:visible :hide}]]
[:position {:optional true}
[::sm/one-of #{:fixed :floating :inline}]]
[:notification-type {:optional true}
[::sm/one-of #{:inline :context :toast}]]
[:controls {:optional true}
[::sm/one-of #{:none :close :inline-actions :bottom-actions}]]
[:tag {:optional true}
[:or :string :keyword]]
[:timeout {:optional true}
[:maybe :int]]
[:actions {:optional true}
[:vector
[:map
[:label :string]
[:callback ::sm/fn]]]]
[:links {:optional true}
[:vector
[:map
[:label :string]
[:callback ::sm/fn]]]]]))
(def ^:private schema:message
[:map {:title "Message"}
[:type [::sm/one-of #{:success :error :info :warning}]]
[:status {:optional true}
[::sm/one-of #{:visible :hide}]]
[:position {:optional true}
[::sm/one-of #{:fixed :floating :inline}]]
[:notification-type {:optional true}
[::sm/one-of #{:inline :context :toast}]]
[:controls {:optional true}
[::sm/one-of #{:none :close :inline-actions :bottom-actions}]]
[:tag {:optional true}
[:or :string :keyword]]
[:timeout {:optional true}
[:maybe :int]]
[:actions {:optional true}
[:vector
[:map
[:label :string]
[:callback ::sm/fn]]]]
[:links {:optional true}
[:vector
[:map
[:label :string]
[:callback ::sm/fn]]]]])
(def ^:private valid-message?
(sm/validator schema:message))
(defn show
[data]
(dm/assert!
"expected valid message map"
(sm/check! schema:message data))
(valid-message? data))
(ptk/reify ::show
ptk/UpdateEvent
@ -76,14 +76,7 @@
(ptk/reify ::hide
ptk/UpdateEvent
(update [_ state]
(d/update-when state :message assoc :status :hide))
ptk/WatchEvent
(watch [_ _ stream]
(let [stopper (rx/filter (ptk/type? ::show) stream)]
(->> (rx/of #(dissoc % :message))
(rx/delay default-animation-timeout)
(rx/take-until stopper))))))
(dissoc state :message))))
(defn hide-tag
[tag]

View file

@ -0,0 +1,231 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.main.data.persistence
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.logging :as log]
[app.common.uuid :as uuid]
[app.main.data.changes :as dch]
[app.main.repo :as rp]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
(declare ^:private run-persistence-task)
(log/set-level! :warn)
(def running (atom false))
(def revn-data (atom {}))
(def queue-conj (fnil conj #queue []))
(defn- update-status
[status]
(ptk/reify ::update-status
ptk/UpdateEvent
(update [_ state]
(update state :persistence (fn [pstate]
(log/trc :hint "update-status"
:from (:status pstate)
:to status)
(let [status (if (and (= status :pending)
(= (:status pstate) :saving))
(:status pstate)
status)]
(-> (assoc pstate :status status)
(cond-> (= status :error)
(dissoc :run-id))
(cond-> (= status :saved)
(dissoc :run-id)))))))))
(defn- update-file-revn
[file-id revn]
(ptk/reify ::update-file-revn
ptk/UpdateEvent
(update [_ state]
(log/dbg :hint "update-file-revn" :file-id (dm/str file-id) :revn revn)
(if-let [current-file-id (:current-file-id state)]
(if (= file-id current-file-id)
(update-in state [:workspace-file :revn] max revn)
(d/update-in-when state [:workspace-libraries file-id :revn] max revn))
state))
ptk/EffectEvent
(effect [_ _ _]
(swap! revn-data update file-id (fnil max 0) revn))))
(defn- discard-commit
[commit-id]
(ptk/reify ::discard-commit
ptk/UpdateEvent
(update [_ state]
(update state :persistence (fn [pstate]
(-> pstate
(update :queue (fn [queue]
(if (= commit-id (peek queue))
(pop queue)
(throw (ex-info "invalid state" {})))))
(update :index dissoc commit-id)))))))
(defn- append-commit
"Event used internally to append the current change to the
persistence queue."
[{:keys [id] :as commit}]
(let [run-id (uuid/next)]
(ptk/reify ::append-commit
ptk/UpdateEvent
(update [_ state]
(log/trc :hint "append-commit" :method "update" :commit-id (dm/str id))
(update state :persistence
(fn [pstate]
(-> pstate
(update :run-id d/nilv run-id)
(update :queue queue-conj id)
(update :index assoc id commit)))))
ptk/WatchEvent
(watch [_ state _]
(let [pstate (:persistence state)]
(when (= run-id (:run-id pstate))
(rx/of (run-persistence-task)
(update-status :saving))))))))
(defn- discard-persistence-state
[]
(ptk/reify ::discard-persistence-state
ptk/UpdateEvent
(update [_ state]
(dissoc state :persistence))))
(defn- persist-commit
[commit-id]
(ptk/reify ::persist-commit
ptk/WatchEvent
(watch [_ state _]
(log/dbg :hint "persist-commit" :commit-id (dm/str commit-id))
(when-let [{:keys [file-id file-revn changes features] :as commit} (dm/get-in state [:persistence :index commit-id])]
(let [sid (:session-id state)
revn (max file-revn (get @revn-data file-id 0))
params {:id file-id
:revn revn
:session-id sid
:origin (:origin commit)
:created-at (:created-at commit)
:commit-id commit-id
:changes (vec changes)
:features features}]
(->> (rp/cmd! :update-file params)
(rx/mapcat (fn [{:keys [revn lagged] :as response}]
(log/debug :hint "changes persisted" :commit-id (dm/str commit-id) :lagged (count lagged))
(rx/of (ptk/data-event ::commit-persisted commit)
(update-file-revn file-id revn))))
(rx/catch (fn [cause]
(rx/concat
(if (= :authentication (:type cause))
(rx/empty)
(rx/of (ptk/data-event ::error cause)
(update-status :error)))
(rx/of (discard-persistence-state))
(rx/throw cause))))))))))
(defn- run-persistence-task
[]
(ptk/reify ::run-persistence-task
ptk/WatchEvent
(watch [_ state stream]
(let [queue (-> state :persistence :queue)]
(if-let [commit-id (peek queue)]
(let [stoper-s (rx/merge
(rx/filter (ptk/type? ::run-persistence-task) stream)
(rx/filter (ptk/type? ::error) stream))]
(log/dbg :hint "run-persistence-task" :commit-id (dm/str commit-id))
(->> (rx/merge
(rx/of (persist-commit commit-id))
(->> stream
(rx/filter (ptk/type? ::commit-persisted))
(rx/map deref)
(rx/filter #(= commit-id (:id %)))
(rx/take 1)
(rx/mapcat (fn [_]
(rx/of (discard-commit commit-id)
(run-persistence-task))))))
(rx/take-until stoper-s)))
(rx/of (update-status :saved)))))))
(def ^:private xf-mapcat-undo
(mapcat :undo-changes))
(def ^:private xf-mapcat-redo
(mapcat :redo-changes))
(defn- merge-commit
[buffer]
(->> (rx/from (group-by :file-id buffer))
(rx/map (fn [[_ [item :as commits]]]
(let [uchg (into [] xf-mapcat-undo commits)
rchg (into [] xf-mapcat-redo commits)]
(-> item
(assoc :undo-changes uchg)
(assoc :redo-changes rchg)
(assoc :changes rchg)))))))
(defn initialize-persistence
[]
(ptk/reify ::initialize-persistence
ptk/WatchEvent
(watch [_ _ stream]
(log/debug :hint "initialize persistence")
(let [stoper-s (rx/filter (ptk/type? ::initialize-persistence) stream)
local-commits-s
(->> stream
(rx/filter dch/commit?)
(rx/map deref)
(rx/filter #(= :local (:source %)))
(rx/filter (complement empty?))
(rx/share))
notifier-s
(rx/merge
(->> local-commits-s
(rx/debounce 3000)
(rx/tap #(log/trc :hint "persistence beat")))
(->> stream
(rx/filter #(= % ::force-persist))))]
(rx/merge
(->> local-commits-s
(rx/debounce 200)
(rx/map (fn [_]
(update-status :pending)))
(rx/take-until stoper-s))
;; Here we watch for local commits, buffer them in a small
;; chunks (very near in time commits) and append them to the
;; persistence queue
(->> local-commits-s
(rx/buffer-until notifier-s)
(rx/mapcat merge-commit)
(rx/map append-commit)
(rx/take-until (rx/delay 100 stoper-s))
(rx/finalize (fn []
(log/debug :hint "finalize persistence: changes watcher"))))
;; Here we track all incoming remote commits for maintain
;; updated the local state with the file revn
(->> stream
(rx/filter dch/commit?)
(rx/map deref)
(rx/filter #(= :remote (:source %)))
(rx/mapcat (fn [{:keys [file-id file-revn] :as commit}]
(rx/of (update-file-revn file-id file-revn))))
(rx/take-until stoper-s)))))))

View file

@ -317,8 +317,7 @@
(effect [_ _ _]
;; We prefer to keek some stuff in the storage like the current-team-id and the profile
(swap! storage dissoc :redirect-url)
(set-current-team! nil)
(i18n/reset-locale)))))
(set-current-team! nil)))))
(defn logout
([] (logout {}))
@ -328,11 +327,15 @@
(-data [_] {})
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/cmd! :logout)
(rx/delay-at-least 300)
(rx/catch (constantly (rx/of 1)))
(rx/map #(logged-out params)))))))
(watch [_ state _]
(let [profile-id (:profile-id state)]
(->> (rx/interval 500)
(rx/take 1)
(rx/mapcat (fn [_]
(->> (rp/cmd! :logout {:profile-id profile-id})
(rx/delay-at-least 300)
(rx/catch (constantly (rx/of 1))))))
(rx/map #(logged-out params))))))))
;; --- Update Profile
@ -565,10 +568,9 @@
on-success identity}} (meta params)]
(->> (rp/cmd! :delete-profile {})
(rx/tap on-success)
(rx/delay-at-least 300)
(rx/catch (constantly (rx/of 1)))
(rx/map logged-out)
(rx/catch on-error))))))
(rx/catch on-error)
(rx/delay-at-least 300))))))
;; --- EVENT: request-profile-recovery
@ -693,15 +695,20 @@
(ptk/reify ::show-redirect-error
ptk/WatchEvent
(watch [_ _ _]
(let [hint (case error
"registration-disabled"
(tr "errors.registration-disabled")
"profile-blocked"
(tr "errors.profile-blocked")
"auth-provider-not-allowed"
(tr "errors.auth-provider-not-allowed")
"email-domain-not-allowed"
(tr "errors.email-domain-not-allowed")
:else
(tr "errors.generic"))]
(when-let [hint (case error
"registration-disabled"
(tr "errors.registration-disabled")
"profile-blocked"
(tr "errors.profile-blocked")
"auth-provider-not-allowed"
(tr "errors.auth-provider-not-allowed")
"email-domain-not-allowed"
(tr "errors.email-domain-not-allowed")
;; We explicitly do not show any error here, it a explicit user operation.
"unable-to-auth"
nil
(tr "errors.generic"))]
(rx/of (msg/warn hint))))))

View file

@ -253,6 +253,18 @@
;; --- Zoom Management
(def update-zoom-querystring
(ptk/reify ::update-zoom-querystring
ptk/WatchEvent
(watch [_ state _]
(let [zoom-type (get-in state [:viewer-local :zoom-type])
route (:route state)
screen (-> route :data :name keyword)
qparams (:query-params route)
pparams (:path-params route)]
(rx/of (rt/nav screen pparams (assoc qparams :zoom zoom-type)))))))
(def increase-zoom
(ptk/reify ::increase-zoom
ptk/UpdateEvent
@ -293,7 +305,10 @@
minzoom (min wdiff hdiff)]
(-> state
(assoc-in [:viewer-local :zoom] minzoom)
(assoc-in [:viewer-local :zoom-type] :fit))))))
(assoc-in [:viewer-local :zoom-type] :fit))))
ptk/WatchEvent
(watch [_ _ _] (rx/of update-zoom-querystring))))
(def zoom-to-fill
(ptk/reify ::zoom-to-fill
@ -309,7 +324,9 @@
maxzoom (max wdiff hdiff)]
(-> state
(assoc-in [:viewer-local :zoom] maxzoom)
(assoc-in [:viewer-local :zoom-type] :fill))))))
(assoc-in [:viewer-local :zoom-type] :fill))))
ptk/WatchEvent
(watch [_ _ _] (rx/of update-zoom-querystring))))
(def toggle-zoom-style
(ptk/reify ::toggle-zoom-style

View file

@ -19,6 +19,7 @@
[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.logic.libraries :as cll]
[app.common.logic.shapes :as cls]
[app.common.schema :as sm]
@ -34,14 +35,15 @@
[app.common.types.typography :as ctt]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.data.changes :as dch]
[app.main.data.comments :as dcm]
[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.persistence :as dps]
[app.main.data.users :as du]
[app.main.data.workspace.bool :as dwb]
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.collapse :as dwco]
[app.main.data.workspace.drawing :as dwd]
[app.main.data.workspace.edition :as dwe]
@ -59,7 +61,6 @@
[app.main.data.workspace.notifications :as dwn]
[app.main.data.workspace.path :as dwdp]
[app.main.data.workspace.path.shapes-to-path :as dwps]
[app.main.data.workspace.persistence :as dwp]
[app.main.data.workspace.selection :as dws]
[app.main.data.workspace.shape-layout :as dwsl]
[app.main.data.workspace.shapes :as dwsh]
@ -87,6 +88,7 @@
[potok.v2.core :as ptk]))
(def default-workspace-local {:zoom 1})
(log/set-level! :debug)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Workspace Initialization
@ -341,15 +343,32 @@
:workspace-presence {}))
ptk/WatchEvent
(watch [_ _ _]
(rx/of msg/hide
(dcm/retrieve-comment-threads file-id)
(dwp/initialize-file-persistence file-id)
(fetch-bundle project-id file-id)))
(watch [_ _ stream]
(log/debug :hint "initialize-file" :file-id file-id)
(let [stoper-s (rx/filter (ptk/type? ::finalize-file) stream)]
(rx/merge
(rx/of msg/hide
(features/initialize)
(dcm/retrieve-comment-threads file-id)
(fetch-bundle project-id file-id))
(->> stream
(rx/filter dch/commit?)
(rx/map deref)
(rx/mapcat (fn [{:keys [save-undo? undo-changes redo-changes undo-group tags stack-undo?]}]
(if (and save-undo? (seq undo-changes))
(let [entry {:undo-changes undo-changes
:redo-changes redo-changes
:undo-group undo-group
:tags tags}]
(rx/of (dwu/append-undo entry stack-undo?)))
(rx/empty))))
(rx/take-until stoper-s)))))
ptk/EffectEvent
(effect [_ _ _]
(let [name (str "workspace-" file-id)]
(let [name (dm/str "workspace-" file-id)]
(unchecked-set ug/global "name" name)))))
(defn finalize-file
@ -460,8 +479,8 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn create-page
[{:keys [file-id]}]
(let [id (uuid/next)]
[{:keys [page-id file-id]}]
(let [id (or page-id (uuid/next))]
(ptk/reify ::create-page
ev/Event
(-data [_]
@ -549,6 +568,35 @@
(rx/of (dch/commit-changes changes))))))
(defn set-plugin-data
([file-id type namespace key value]
(set-plugin-data file-id type nil nil namespace key value))
([file-id type id namespace key value]
(set-plugin-data file-id type id nil namespace key value))
([file-id type id page-id namespace key value]
(dm/assert! (contains? #{:file :page :shape :color :typography :component} type))
(dm/assert! (or (nil? id) (uuid? id)))
(dm/assert! (or (nil? page-id) (uuid? page-id)))
(dm/assert! (uuid? file-id))
(dm/assert! (keyword? namespace))
(dm/assert! (string? key))
(dm/assert! (or (nil? value) (string? value)))
(ptk/reify ::set-file-plugin-data
ptk/WatchEvent
(watch [it state _]
(let [file-data
(if (= file-id (:current-file-id state))
(:workspace-data state)
(get-in state [:workspace-libraries file-id :data]))
changes
(-> (pcb/empty-changes it)
(pcb/with-file-data file-data)
(assoc :file-id file-id)
(pcb/mod-plugin-data type id page-id namespace key value))]
(rx/of (dch/commit-changes changes)))))))
(declare purge-page)
(declare go-to-file)
@ -671,7 +719,7 @@
(ptk/reify ::update-shape
ptk/WatchEvent
(watch [_ _ _]
(rx/of (dch/update-shapes [id] #(merge % attrs))))))
(rx/of (dwsh/update-shapes [id] #(merge % attrs))))))
(defn start-rename-shape
"Start shape renaming process"
@ -808,15 +856,14 @@
ids (filter #(not (cfh/is-parent? objects parent-id %)) ids)
all-parents (into #{parent-id} (map #(cfh/get-parent-id objects %)) ids)
parents (if ignore-parents? #{parent-id} all-parents)
changes (cls/generate-relocate-shapes (pcb/empty-changes it)
objects
parents
parent-id
page-id
to-index
ids)
changes (cls/generate-relocate (pcb/empty-changes it)
objects
parent-id
page-id
to-index
ids
:ignore-parents? ignore-parents?)
undo-id (js/Symbol)]
(rx/of (dwu/start-undo-transaction undo-id)
@ -982,7 +1029,7 @@
(assoc shape :proportion-lock false)
(-> (assoc shape :proportion-lock true)
(gpp/assign-proportions))))]
(rx/of (dch/update-shapes [id] assign-proportions))))))
(rx/of (dwsh/update-shapes [id] assign-proportions))))))
(defn toggle-proportion-lock
[]
@ -996,8 +1043,8 @@
multi (attrs/get-attrs-multi selected-obj [:proportion-lock])
multi? (= :multiple (:proportion-lock multi))]
(if multi?
(rx/of (dch/update-shapes selected #(assoc % :proportion-lock true)))
(rx/of (dch/update-shapes selected #(update % :proportion-lock not))))))))
(rx/of (dwsh/update-shapes selected #(assoc % :proportion-lock true)))
(rx/of (dwsh/update-shapes selected #(update % :proportion-lock not))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Navigation
@ -1077,6 +1124,14 @@
(update [_ state]
(assoc-in state [:workspace-assets :open-status file-id section] open?))))
(defn clear-assets-section-open
[]
(ptk/reify ::clear-assets-section-open
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-assets :open-status] {}))))
(defn set-assets-group-open
[file-id section path open?]
(ptk/reify ::set-assets-group-open
@ -1258,7 +1313,7 @@
(assoc :section section)
(some? frame-id)
(assoc :frame-id frame-id))]
(rx/of ::dwp/force-persist
(rx/of ::dps/force-persist
(rt/nav-new-window* {:rname :viewer
:path-params pparams
:query-params qparams
@ -1271,7 +1326,7 @@
ptk/WatchEvent
(watch [_ state _]
(when-let [team-id (or team-id (:current-team-id state))]
(rx/of ::dwp/force-persist
(rx/of ::dps/force-persist
(rt/nav :dashboard-projects {:team-id team-id})))))))
(defn go-to-dashboard-fonts
@ -1280,7 +1335,7 @@
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)]
(rx/of ::dwp/force-persist
(rx/of ::dps/force-persist
(rt/nav :dashboard-fonts {:team-id team-id}))))))
@ -1997,16 +2052,18 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn change-canvas-color
[color]
(ptk/reify ::change-canvas-color
ptk/WatchEvent
(watch [it state _]
(let [page (wsh/lookup-page state)
changes (-> (pcb/empty-changes it)
(pcb/with-page page)
(pcb/set-page-option :background (:color color)))]
(rx/of (dch/commit-changes changes))))))
([color]
(change-canvas-color nil color))
([page-id color]
(ptk/reify ::change-canvas-color
ptk/WatchEvent
(watch [it state _]
(let [page-id (or page-id (:current-page-id state))
page (wsh/lookup-page state page-id)
changes (-> (pcb/empty-changes it)
(pcb/with-page page)
(pcb/set-page-option :background (:color color)))]
(rx/of (dch/commit-changes changes)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Read only

View file

@ -15,8 +15,9 @@
[app.common.types.shape :as cts]
[app.common.types.shape.layout :as ctl]
[app.common.uuid :as uuid]
[app.main.data.workspace.changes :as dch]
[app.main.data.changes :as dch]
[app.main.data.workspace.selection :as dws]
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.state-helpers :as wsh]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
@ -82,31 +83,38 @@
(gsh/update-group-selrect children))))
(defn create-bool
[bool-type]
(ptk/reify ::create-bool-union
ptk/WatchEvent
(watch [it state _]
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state)
name (-> bool-type d/name str/capital)
ids (selected-shapes-idx state)
ordered-indexes (cph/order-by-indexed-shapes objects ids)
shapes (->> ordered-indexes
(map (d/getf objects))
(remove cph/frame-shape?)
(remove #(ctn/has-any-copy-parent? objects %)))]
([bool-type]
(create-bool bool-type nil nil))
([bool-type ids {:keys [id-ret]}]
(assert (or (nil? ids) (set? ids)))
(ptk/reify ::create-bool-union
ptk/WatchEvent
(watch [it state _]
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state)
name (-> bool-type d/name str/capital)
ids (->> (or ids (wsh/lookup-selected state))
(cph/clean-loops objects))
ordered-indexes (cph/order-by-indexed-shapes objects ids)
shapes (->> ordered-indexes
(map (d/getf objects))
(remove cph/frame-shape?)
(remove #(ctn/has-any-copy-parent? objects %)))]
(when-not (empty? shapes)
(let [[boolean-data index] (create-bool-data bool-type name (reverse shapes) objects)
index (inc index)
shape-id (:id boolean-data)
changes (-> (pcb/empty-changes it page-id)
(pcb/with-objects objects)
(pcb/add-object boolean-data {:index index})
(pcb/update-shapes (map :id shapes) ctl/remove-layout-item-data)
(pcb/change-parent shape-id shapes))]
(rx/of (dch/commit-changes changes)
(dws/select-shapes (d/ordered-set shape-id)))))))))
(when-not (empty? shapes)
(let [[boolean-data index] (create-bool-data bool-type name (reverse shapes) objects)
index (inc index)
shape-id (:id boolean-data)
changes (-> (pcb/empty-changes it page-id)
(pcb/with-objects objects)
(pcb/add-object boolean-data {:index index})
(pcb/update-shapes (map :id shapes) ctl/remove-layout-item-data)
(pcb/change-parent shape-id shapes))]
(when id-ret
(reset! id-ret shape-id))
(rx/of (dch/commit-changes changes)
(dws/select-shapes (d/ordered-set shape-id))))))))))
(defn group-to-bool
[shape-id bool-type]
@ -117,7 +125,7 @@
change-to-bool
(fn [shape] (group->bool shape bool-type objects))]
(when-not (ctn/has-any-copy-parent? objects (get objects shape-id))
(rx/of (dch/update-shapes [shape-id] change-to-bool {:reg-objects? true})))))))
(rx/of (dwsh/update-shapes [shape-id] change-to-bool {:reg-objects? true})))))))
(defn bool-to-group
[shape-id]
@ -128,7 +136,7 @@
change-to-group
(fn [shape] (bool->group shape objects))]
(when-not (ctn/has-any-copy-parent? objects (get objects shape-id))
(rx/of (dch/update-shapes [shape-id] change-to-group {:reg-objects? true})))))))
(rx/of (dwsh/update-shapes [shape-id] change-to-group {:reg-objects? true})))))))
(defn change-bool-type
@ -140,4 +148,4 @@
change-type
(fn [shape] (assoc shape :bool-type bool-type))]
(when-not (ctn/has-any-copy-parent? objects (get objects shape-id))
(rx/of (dch/update-shapes [shape-id] change-type {:reg-objects? true})))))))
(rx/of (dwsh/update-shapes [shape-id] change-type {:reg-objects? true})))))))

View file

@ -1,264 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.main.data.workspace.changes
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.files.changes :as cpc]
[app.common.files.changes-builder :as pcb]
[app.common.files.helpers :as cph]
[app.common.logging :as log]
[app.common.logic.shapes :as cls]
[app.common.schema :as sm]
[app.common.types.shape-tree :as ctst]
[app.common.uuid :as uuid]
[app.main.data.workspace.state-helpers :as wsh]
[app.main.data.workspace.undo :as dwu]
[app.main.store :as st]
[app.main.worker :as uw]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
;; Change this to :info :debug or :trace to debug this module
(log/set-level! :warn)
(defonce page-change? #{:add-page :mod-page :del-page :mov-page})
(defonce update-layout-attr? #{:hidden})
(declare commit-changes)
(defn- add-undo-group
[changes state]
(let [undo (:workspace-undo state)
items (:items undo)
index (or (:index undo) (dec (count items)))
prev-item (when-not (or (empty? items) (= index -1))
(get items index))
undo-group (:undo-group prev-item)
add-undo-group? (and
(not (nil? undo-group))
(= (get-in changes [:redo-changes 0 :type]) :mod-obj)
(= (get-in prev-item [:redo-changes 0 :type]) :add-obj)
(contains? (:tags prev-item) :alt-duplication))] ;; This is a copy-and-move with mouse+alt
(cond-> changes add-undo-group? (assoc :undo-group undo-group))))
(def commit-changes? (ptk/type? ::commit-changes))
(defn update-shapes
([ids update-fn] (update-shapes ids update-fn nil))
([ids update-fn {:keys [reg-objects? save-undo? stack-undo? attrs ignore-tree page-id ignore-remote? ignore-touched undo-group with-objects?]
:or {reg-objects? false save-undo? true stack-undo? false ignore-remote? false ignore-touched false with-objects? false}}]
(dm/assert!
"expected a valid coll of uuid's"
(sm/check-coll-of-uuid! ids))
(dm/assert! (fn? update-fn))
(ptk/reify ::update-shapes
ptk/WatchEvent
(watch [it state _]
(let [page-id (or page-id (:current-page-id state))
objects (wsh/lookup-page-objects state page-id)
ids (into [] (filter some?) ids)
update-layout-ids
(->> ids
(map (d/getf objects))
(filter #(some update-layout-attr? (pcb/changed-attrs % objects update-fn {:attrs attrs :with-objects? with-objects?})))
(map :id))
changes (-> (pcb/empty-changes it page-id)
(pcb/set-save-undo? save-undo?)
(pcb/set-stack-undo? stack-undo?)
(cls/generate-update-shapes ids
update-fn
objects
{:attrs attrs
:ignore-tree ignore-tree
:ignore-touched ignore-touched
:with-objects? with-objects?})
(cond-> undo-group
(pcb/set-undo-group undo-group)))
changes (add-undo-group changes state)]
(rx/concat
(if (seq (:redo-changes changes))
(let [changes (cond-> changes reg-objects? (pcb/resize-parents ids))
changes (cond-> changes ignore-remote? (pcb/ignore-remote))]
(rx/of (commit-changes changes)))
(rx/empty))
;; Update layouts for properties marked
(if (d/not-empty? update-layout-ids)
(rx/of (ptk/data-event :layout/update {:ids update-layout-ids}))
(rx/empty))))))))
(defn send-update-indices
[]
(ptk/reify ::send-update-indices
ptk/WatchEvent
(watch [_ _ _]
(->> (rx/of
(fn [state]
(-> state
(dissoc ::update-indices-debounce)
(dissoc ::update-changes))))
(rx/observe-on :async)))
ptk/EffectEvent
(effect [_ state _]
(doseq [[page-id changes] (::update-changes state)]
(uw/ask! {:cmd :update-page-index
:page-id page-id
:changes changes})))))
;; Update indices will debounce operations so we don't have to update
;; the index several times (which is an expensive operation)
(defn update-indices
[page-id changes]
(let [start (uuid/next)]
(ptk/reify ::update-indices
ptk/UpdateEvent
(update [_ state]
(if (nil? (::update-indices-debounce state))
(assoc state ::update-indices-debounce start)
(update-in state [::update-changes page-id] (fnil d/concat-vec []) changes)))
ptk/WatchEvent
(watch [_ state stream]
(if (= (::update-indices-debounce state) start)
(let [stopper (->> stream (rx/filter (ptk/type? :app.main.data.workspace/finalize)))]
(rx/merge
(->> stream
(rx/filter (ptk/type? ::update-indices))
(rx/debounce 50)
(rx/take 1)
(rx/map #(send-update-indices))
(rx/take-until stopper))
(rx/of (update-indices page-id changes))))
(rx/empty))))))
(defn changed-frames
"Extracts the frame-ids changed in the given changes"
[changes objects]
(let [change->ids
(fn [change]
(case (:type change)
:add-obj
[(:parent-id change)]
(:mod-obj :del-obj)
[(:id change)]
:mov-objects
(d/concat-vec (:shapes change) [(:parent-id change)])
[]))]
(into #{}
(comp (mapcat change->ids)
(keep #(cph/get-shape-id-root-frame objects %))
(remove #(= uuid/zero %)))
changes)))
(defn commit-changes
"Schedules a list of changes to execute now, and add the corresponding undo changes to
the undo stack.
Options:
- save-undo?: if set to false, do not add undo changes.
- undo-group: if some consecutive changes (or even transactions) share the same
undo-group, they will be undone or redone in a single step
"
[{:keys [redo-changes undo-changes
origin save-undo? file-id undo-group tags stack-undo?]
:or {save-undo? true stack-undo? false tags #{} undo-group (uuid/next)}}]
(let [error (volatile! nil)
page-id (:current-page-id @st/state)
frames (changed-frames redo-changes (wsh/lookup-page-objects @st/state))
undo-changes (vec undo-changes)
redo-changes (vec redo-changes)]
(ptk/reify ::commit-changes
cljs.core/IDeref
(-deref [_]
{:file-id file-id
:hint-events @st/last-events
:hint-origin (ptk/type origin)
:changes redo-changes
:page-id page-id
:frames frames
:save-undo? save-undo?
:undo-group undo-group
:tags tags
:stack-undo? stack-undo?})
ptk/UpdateEvent
(update [_ state]
(log/info :msg "commit-changes"
:js/undo-group (str undo-group)
:js/file-id (str (or file-id "nil"))
:js/redo-changes redo-changes
:js/undo-changes undo-changes)
(let [current-file-id (get state :current-file-id)
file-id (or file-id current-file-id)
path (if (= file-id current-file-id)
[:workspace-data]
[:workspace-libraries file-id :data])]
(try
(dm/assert!
"expect valid vector of changes"
(and (cpc/check-changes! redo-changes)
(cpc/check-changes! undo-changes)))
(update-in state path (fn [file]
(-> file
(cpc/process-changes redo-changes false)
(ctst/update-object-indices page-id))))
(catch :default err
(when-let [data (ex-data err)]
(js/console.log (ex/explain data)))
(when (ex/error? err)
(js/console.log (.-stack ^js err)))
(vreset! error err)
state))))
ptk/WatchEvent
(watch [_ _ _]
(when-not @error
(let [;; adds page-id to page changes (that have the `id` field instead)
add-page-id
(fn [{:keys [id type page] :as change}]
(cond-> change
(and (page-change? type) (nil? (:page-id change)))
(assoc :page-id (or id (:id page)))))
changes-by-pages
(->> redo-changes
(map add-page-id)
(remove #(nil? (:page-id %)))
(group-by :page-id))
process-page-changes
(fn [[page-id _changes]]
(update-indices page-id redo-changes))]
(rx/concat
(rx/from (map process-page-changes changes-by-pages))
(when (and save-undo? (seq undo-changes))
(let [entry {:undo-changes undo-changes
:redo-changes redo-changes
:undo-group undo-group
:tags tags}]
(rx/of (dwu/append-undo entry stack-undo?)))))))))))

View file

@ -15,15 +15,16 @@
[app.main.broadcast :as mbc]
[app.main.data.events :as ev]
[app.main.data.modal :as md]
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.layout :as layout]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.state-helpers :as wsh]
[app.main.data.workspace.texts :as dwt]
[app.main.data.workspace.undo :as dwu]
[app.util.color :as uc]
[app.util.storage :refer [storage]]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[potok.v2.core :as ptk]))
;; A set of keys that are used for shared state identifiers
@ -116,7 +117,7 @@
(rx/concat
(rx/of (dwu/start-undo-transaction undo-id))
(rx/from (map #(dwt/update-text-with-function % transform-attrs) text-ids))
(rx/of (dch/update-shapes shape-ids transform-attrs))
(rx/of (dwsh/update-shapes shape-ids transform-attrs))
(rx/of (dwu/commit-undo-transaction undo-id)))))
(defn swap-attrs [shape attr index new-index]
@ -140,7 +141,7 @@
(rx/concat
(rx/from (map #(dwt/update-text-with-function % transform-attrs) text-ids))
(rx/of (dch/update-shapes shape-ids transform-attrs)))))))
(rx/of (dwsh/update-shapes shape-ids transform-attrs)))))))
(defn change-fill
[ids color position]
@ -203,10 +204,10 @@
is-text? #(= :text (:type (get objects %)))
shape-ids (filter (complement is-text?) ids)
attrs {:hide-fill-on-export hide-fill-on-export}]
(rx/of (dch/update-shapes shape-ids (fn [shape]
(if (= (:type shape) :frame)
(d/merge shape attrs)
shape))))))))
(rx/of (dwsh/update-shapes shape-ids (fn [shape]
(if (= (:type shape) :frame)
(d/merge shape attrs)
shape))))))))
(defn change-stroke
[ids attrs index]
(ptk/reify ::change-stroke
@ -236,7 +237,7 @@
(dissoc :image)
(dissoc :gradient))]
(rx/of (dch/update-shapes
(rx/of (dwsh/update-shapes
ids
(fn [shape]
(let [new-attrs (merge (get-in shape [:strokes index]) attrs)
@ -248,7 +249,7 @@
(assoc :stroke-style :solid)
(not (contains? new-attrs :stroke-alignment))
(assoc :stroke-alignment :inner)
(assoc :stroke-alignment :center)
:always
(d/without-nils))]
@ -264,7 +265,7 @@
(ptk/reify ::change-shadow
ptk/WatchEvent
(watch [_ _ _]
(rx/of (dch/update-shapes
(rx/of (dwsh/update-shapes
ids
(fn [shape]
(let [;; If we try to set a gradient to a shadow (for
@ -288,7 +289,7 @@
(watch [_ _ _]
(let [add-shadow (fn [shape]
(update shape :shadow #(into [shadow] %)))]
(rx/of (dch/update-shapes ids add-shadow))))))
(rx/of (dwsh/update-shapes ids add-shadow))))))
(defn add-stroke
[ids stroke]
@ -296,7 +297,7 @@
ptk/WatchEvent
(watch [_ _ _]
(let [add-stroke (fn [shape] (update shape :strokes #(into [stroke] %)))]
(rx/of (dch/update-shapes ids add-stroke))))))
(rx/of (dwsh/update-shapes ids add-stroke))))))
(defn remove-stroke
[ids position]
@ -309,7 +310,7 @@
(mapv second)))
(remove-stroke [shape]
(update shape :strokes remove-fill-by-index position))]
(rx/of (dch/update-shapes ids remove-stroke))))))
(rx/of (dwsh/update-shapes ids remove-stroke))))))
(defn remove-all-strokes
[ids]
@ -317,14 +318,14 @@
ptk/WatchEvent
(watch [_ _ _]
(let [remove-all #(assoc % :strokes [])]
(rx/of (dch/update-shapes ids remove-all))))))
(rx/of (dwsh/update-shapes ids remove-all))))))
(defn reorder-shadows
[ids index new-index]
(ptk/reify ::reorder-shadow
ptk/WatchEvent
(watch [_ _ _]
(rx/of (dch/update-shapes
(rx/of (dwsh/update-shapes
ids
#(swap-attrs % :shadow index new-index))))))
@ -333,7 +334,7 @@
(ptk/reify ::reorder-strokes
ptk/WatchEvent
(watch [_ _ _]
(rx/of (dch/update-shapes
(rx/of (dwsh/update-shapes
ids
#(swap-attrs % :strokes index new-index))))))
@ -377,7 +378,7 @@
(defn color-att->text
[color]
{:fill-color (:color color)
{:fill-color (when (:color color) (str/lower (:color color)))
:fill-opacity (:opacity color)
:fill-color-ref-id (:id color)
:fill-color-ref-file (:file-id color)
@ -590,7 +591,7 @@
(update [_ state]
(update state :colorpicker
(fn [state]
(let [type (:type state)
(let [type (:type state)
state (-> state
(update :current-color merge changes)
(update :current-color materialize-color-components)
@ -605,12 +606,17 @@
(-> state
(dissoc :gradient :stops :editing-stop)
(cond-> (not= :image (:type state))
(cond-> (not= :image type)
(assoc :type :color))))))))
ptk/WatchEvent
(watch [_ state _]
(when add-recent?
(let [formated-color (get-color-from-colorpicker-state (:colorpicker state))]
(let [selected-type (-> state
:colorpicker
:type)
formated-color (get-color-from-colorpicker-state (:colorpicker state))
;; Type is set to color on closing the colorpicker, but we can can close it while still uploading an image fill
ignore-color? (and (= selected-type :color) (nil? (:color formated-color)))]
(when (and add-recent? (not ignore-color?))
(rx/of (dwl/add-recent-color formated-color)))))))
(defn update-colorpicker-gradient

View file

@ -12,9 +12,9 @@
[app.common.geom.shapes :as gsh]
[app.common.schema :as sm]
[app.common.types.shape-tree :as ctst]
[app.main.data.changes :as dch]
[app.main.data.comments :as dcm]
[app.main.data.events :as ev]
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.common :as dwco]
[app.main.data.workspace.drawing :as dwd]
[app.main.data.workspace.state-helpers :as wsh]

View file

@ -6,14 +6,7 @@
(ns app.main.data.workspace.common
(:require
[app.common.data.macros :as dm]
[app.common.logging :as log]
[app.common.types.shape.layout :as ctl]
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.state-helpers :as wsh]
[app.main.data.workspace.undo :as dwu]
[app.util.router :as rt]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
;; Change this to :info :debug or :trace to debug this module
@ -34,136 +27,11 @@
[e]
(= e :interrupt))
(defn- assure-valid-current-page
[]
(ptk/reify ::assure-valid-current-page
ptk/WatchEvent
(watch [_ state _]
(let [current_page (:current-page-id state)
pages (get-in state [:workspace-data :pages])
exists? (some #(= current_page %) pages)
project-id (:current-project-id state)
file-id (:current-file-id state)
pparams {:file-id file-id :project-id project-id}
qparams {:page-id (first pages)}]
(if exists?
(rx/empty)
(rx/of (rt/nav :workspace pparams qparams)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; UNDO
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(declare undo-to-index)
;; These functions should've been in
;; `src/app/main/data/workspace/undo.cljs` but doing that causes a
;; circular dependency with `src/app/main/data/workspace/changes.cljs`
(def undo
(ptk/reify ::undo
ptk/WatchEvent
(watch [it state _]
(let [objects (wsh/lookup-page-objects state)
edition (get-in state [:workspace-local :edition])
drawing (get state :workspace-drawing)]
;; Editors handle their own undo's
(when (or (and (nil? edition) (nil? (:object drawing)))
(ctl/grid-layout? objects edition))
(let [undo (:workspace-undo state)
items (:items undo)
index (or (:index undo) (dec (count items)))]
(when-not (or (empty? items) (= index -1))
(let [item (get items index)
changes (:undo-changes item)
undo-group (:undo-group item)
find-first-group-idx
(fn [index]
(if (= (dm/get-in items [index :undo-group]) undo-group)
(recur (dec index))
(inc index)))
undo-group-index
(when undo-group
(find-first-group-idx index))]
(if undo-group
(rx/of (undo-to-index (dec undo-group-index)))
(rx/of (dwu/materialize-undo changes (dec index))
(dch/commit-changes {:redo-changes changes
:undo-changes []
:save-undo? false
:origin it})
(assure-valid-current-page)))))))))))
(def redo
(ptk/reify ::redo
ptk/WatchEvent
(watch [it state _]
(let [objects (wsh/lookup-page-objects state)
edition (get-in state [:workspace-local :edition])
drawing (get state :workspace-drawing)]
(when (and (or (nil? edition) (ctl/grid-layout? objects edition))
(or (empty? drawing) (= :curve (:tool drawing))))
(let [undo (:workspace-undo state)
items (:items undo)
index (or (:index undo) (dec (count items)))]
(when-not (or (empty? items) (= index (dec (count items))))
(let [item (get items (inc index))
changes (:redo-changes item)
undo-group (:undo-group item)
find-last-group-idx (fn flgidx [index]
(let [item (get items index)]
(if (= (:undo-group item) undo-group)
(flgidx (inc index))
(dec index))))
redo-group-index (when undo-group
(find-last-group-idx (inc index)))]
(if undo-group
(rx/of (undo-to-index redo-group-index))
(rx/of (dwu/materialize-undo changes (inc index))
(dch/commit-changes {:redo-changes changes
:undo-changes []
:origin it
:save-undo? false})))))))))))
(defn undo-to-index
"Repeat undoing or redoing until dest-index is reached."
[dest-index]
(ptk/reify ::undo-to-index
ptk/WatchEvent
(watch [it state _]
(let [objects (wsh/lookup-page-objects state)
edition (get-in state [:workspace-local :edition])
drawing (get state :workspace-drawing)]
(when-not (and (or (some? edition) (some? (:object drawing)))
(not (ctl/grid-layout? objects edition)))
(let [undo (:workspace-undo state)
items (:items undo)
index (or (:index undo) (dec (count items)))]
(when (and (some? items)
(<= -1 dest-index (dec (count items))))
(let [changes (vec (apply concat
(cond
(< dest-index index)
(->> (subvec items (inc dest-index) (inc index))
(reverse)
(map :undo-changes))
(> dest-index index)
(->> (subvec items (inc index) (inc dest-index))
(map :redo-changes))
:else [])))]
(when (seq changes)
(rx/of (dwu/materialize-undo changes dest-index)
(dch/commit-changes {:redo-changes changes
:undo-changes []
:origin it
:save-undo? false})))))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Toolbar
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

@ -8,7 +8,8 @@
(:require
[app.common.data :as d]
[app.common.geom.shapes :as gsh]
[app.main.data.workspace.changes :as dch]
[app.main.data.changes :as dch]
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.state-helpers :as wsh]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
@ -82,9 +83,9 @@
:objects (-> component migrate-component :objects)}))
components)]
(rx/of (dch/update-shapes ids #(update-shape % objects) {:reg-objects? false
:save-undo? false
:ignore-tree true}))
(rx/of (dwsh/update-shapes ids #(update-shape % objects) {:reg-objects? false
:save-undo? false
:ignore-tree true}))
(if (empty? component-changes)
(rx/empty)

View file

@ -6,7 +6,7 @@
(ns app.main.data.workspace.fix-broken-shapes
(:require
[app.main.data.workspace.changes :as dch]
[app.main.data.changes :as dch]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))

View file

@ -9,7 +9,8 @@
[app.common.data :as d]
[app.common.files.helpers :as cfh]
[app.common.text :as txt]
[app.main.data.workspace.changes :as dch]
[app.main.data.changes :as dwc]
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.state-helpers :as wsh]
[app.main.fonts :as fonts]
[beicon.v2.core :as rx]
@ -111,19 +112,19 @@
typographies)]
(rx/concat
(rx/of (dch/update-shapes ids #(fix-deleted-font-shape %) {:reg-objects? false
:save-undo? false
:ignore-tree true}))
(rx/of (dwsh/update-shapes ids #(fix-deleted-font-shape %) {:reg-objects? false
:save-undo? false
:ignore-tree true}))
(if (empty? component-changes)
(rx/empty)
(rx/of (dch/commit-changes {:origin it
(rx/of (dwc/commit-changes {:origin it
:redo-changes component-changes
:undo-changes []
:save-undo? false})))
(if (empty? typography-changes)
(rx/empty)
(rx/of (dch/commit-changes {:origin it
(rx/of (dwc/commit-changes {:origin it
:redo-changes typography-changes
:undo-changes []
:save-undo? false}))))))))

View file

@ -10,7 +10,8 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.changes-builder :as pcb]
[app.main.data.workspace.changes :as dch]
[app.main.data.changes :as dch]
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.state-helpers :as wsh]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
@ -51,8 +52,8 @@
grid {:type :square
:params params
:display true}]
(rx/of (dch/update-shapes [frame-id]
(fn [obj] (update obj :grids (fnil #(conj % grid) [])))))))))
(rx/of (dwsh/update-shapes [frame-id]
(fn [obj] (update obj :grids (fnil #(conj % grid) [])))))))))
(defn remove-frame-grid
@ -60,14 +61,14 @@
(ptk/reify ::remove-frame-grid
ptk/WatchEvent
(watch [_ _ _]
(rx/of (dch/update-shapes [frame-id] (fn [o] (update o :grids (fnil #(d/remove-at-index % index) []))))))))
(rx/of (dwsh/update-shapes [frame-id] (fn [o] (update o :grids (fnil #(d/remove-at-index % index) []))))))))
(defn set-frame-grid
[frame-id index data]
(ptk/reify ::set-frame-grid
ptk/WatchEvent
(watch [_ _ _]
(rx/of (dch/update-shapes [frame-id] #(assoc-in % [:grids index] data))))))
(rx/of (dwsh/update-shapes [frame-id] #(assoc-in % [:grids index] data))))))
(defn set-default-grid
[type params]

View file

@ -8,7 +8,7 @@
(:require
[app.main.data.shortcuts :as ds]
[app.main.data.workspace :as dw]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.undo :as dwu]
[app.main.store :as st]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
@ -38,11 +38,11 @@
:undo {:tooltip (ds/meta "Z")
:command (ds/c-mod "z")
:fn #(st/emit! dwc/undo)}
:fn #(st/emit! dwu/undo)}
:redo {:tooltip (ds/meta "Y")
:command [(ds/c-mod "shift+z") (ds/c-mod "y")]
:fn #(st/emit! dwc/redo)}
:fn #(st/emit! dwu/redo)}
;; ZOOM

View file

@ -16,7 +16,7 @@
[app.common.types.shape :as cts]
[app.common.types.shape.layout :as ctl]
[app.common.uuid :as uuid]
[app.main.data.workspace.changes :as dch]
[app.main.data.changes :as dch]
[app.main.data.workspace.selection :as dws]
[app.main.data.workspace.state-helpers :as wsh]
[app.main.data.workspace.undo :as dwu]
@ -198,12 +198,13 @@
(dws/select-shapes (d/ordered-set (:id group))))
(ptk/data-event :layout/update {:ids parents}))))))))
(def group-selected
(defn group-selected
[]
(ptk/reify ::group-selected
ptk/WatchEvent
(watch [_ state _]
(let [selected (wsh/lookup-selected state)]
(rx/of (group-shapes nil selected))))))
(rx/of (group-shapes nil selected :change-selection? true))))))
(defn ungroup-shapes
[ids & {:keys [change-selection?] :or {change-selection? false}}]
@ -258,76 +259,84 @@
(when change-selection?
(dws/select-shapes child-ids))))))))
(def ungroup-selected
(defn ungroup-selected
[]
(ptk/reify ::ungroup-selected
ptk/WatchEvent
(watch [_ state _]
(let [selected (wsh/lookup-selected state)]
(rx/of (ungroup-shapes selected :change-selection? true))))))
(def mask-group
(ptk/reify ::mask-group
ptk/WatchEvent
(watch [it state _]
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
selected (->> (wsh/lookup-selected state)
(cfh/clean-loops objects)
(remove #(ctn/has-any-copy-parent? objects (get objects %))))
shapes (shapes-for-grouping objects selected)
first-shape (first shapes)]
(when-not (empty? shapes)
(let [;; If the selected shape is a group, we can use it. If not,
;; create a new group and set it as masked.
[group changes]
(if (and (= (count shapes) 1)
(= (:type (first shapes)) :group))
[first-shape (-> (pcb/empty-changes it page-id)
(pcb/with-objects objects))]
(prepare-create-group (pcb/empty-changes it) (uuid/next) objects page-id shapes "Mask" true))
(defn mask-group
([]
(mask-group nil))
([ids]
(ptk/reify ::mask-group
ptk/WatchEvent
(watch [it state _]
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
selected (->> (or ids (wsh/lookup-selected state))
(cfh/clean-loops objects)
(remove #(ctn/has-any-copy-parent? objects (get objects %))))
shapes (shapes-for-grouping objects selected)
first-shape (first shapes)]
(when-not (empty? shapes)
(let [;; If the selected shape is a group, we can use it. If not,
;; create a new group and set it as masked.
[group changes]
(if (and (= (count shapes) 1)
(= (:type (first shapes)) :group))
[first-shape (-> (pcb/empty-changes it page-id)
(pcb/with-objects objects))]
(prepare-create-group (pcb/empty-changes it) (uuid/next) objects page-id shapes "Mask" true))
changes (-> changes
(pcb/update-shapes (:shapes group)
(fn [shape]
(assoc shape
:constraints-h :scale
:constraints-v :scale)))
(pcb/update-shapes [(:id group)]
(fn [group]
(assoc group
:masked-group true
:selrect (:selrect first-shape)
:points (:points first-shape)
:transform (:transform first-shape)
:transform-inverse (:transform-inverse first-shape))))
(pcb/resize-parents [(:id group)]))
undo-id (js/Symbol)]
changes (-> changes
(pcb/update-shapes (:shapes group)
(fn [shape]
(assoc shape
:constraints-h :scale
:constraints-v :scale)))
(pcb/update-shapes [(:id group)]
(fn [group]
(assoc group
:masked-group true
:selrect (:selrect first-shape)
:points (:points first-shape)
:transform (:transform first-shape)
:transform-inverse (:transform-inverse first-shape))))
(pcb/resize-parents [(:id group)]))
undo-id (js/Symbol)]
(rx/of (dwu/start-undo-transaction undo-id)
(dch/commit-changes changes)
(dws/select-shapes (d/ordered-set (:id group)))
(ptk/data-event :layout/update {:ids [(:id group)]})
(dwu/commit-undo-transaction undo-id))))))))
(rx/of (dwu/start-undo-transaction undo-id)
(dch/commit-changes changes)
(dws/select-shapes (d/ordered-set (:id group)))
(ptk/data-event :layout/update {:ids [(:id group)]})
(dwu/commit-undo-transaction undo-id)))))))))
(def unmask-group
(ptk/reify ::unmask-group
ptk/WatchEvent
(watch [it state _]
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
(defn unmask-group
([]
(unmask-group nil))
masked-groups (->> (wsh/lookup-selected state)
(map #(get objects %))
(filter #(or (= :bool (:type %)) (= :group (:type %)))))
([ids]
(ptk/reify ::unmask-group
ptk/WatchEvent
(watch [it state _]
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
changes (reduce (fn [changes mask]
(-> changes
(pcb/update-shapes [(:id mask)]
(fn [shape]
(dissoc shape :masked-group)))
(pcb/resize-parents [(:id mask)])))
(-> (pcb/empty-changes it page-id)
(pcb/with-objects objects))
masked-groups)]
masked-groups (->> (d/nilv ids (wsh/lookup-selected state))
(map #(get objects %))
(filter #(or (= :bool (:type %)) (= :group (:type %)))))
(rx/of (dch/commit-changes changes))))))
changes (reduce (fn [changes mask]
(-> changes
(pcb/update-shapes [(:id mask)]
(fn [shape]
(dissoc shape :masked-group)))
(pcb/resize-parents [(:id mask)])))
(-> (pcb/empty-changes it page-id)
(pcb/with-objects objects))
masked-groups)]
(rx/of (dch/commit-changes changes)))))))

View file

@ -11,8 +11,8 @@
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.types.page :as ctp]
[app.main.data.changes :as dwc]
[app.main.data.events :as ev]
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.state-helpers :as wsh]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
@ -42,7 +42,7 @@
(-> (pcb/empty-changes it)
(pcb/with-page page)
(pcb/update-page-option :guides assoc (:id guide) guide))]
(rx/of (dch/commit-changes changes))))))
(rx/of (dwc/commit-changes changes))))))
(defn remove-guide
[guide]
@ -66,7 +66,7 @@
(-> (pcb/empty-changes it)
(pcb/with-page page)
(pcb/update-page-option :guides dissoc (:id guide)))]
(rx/of (dch/commit-changes changes))))))
(rx/of (dwc/commit-changes changes))))))
(defn remove-guides
[ids]
@ -79,20 +79,21 @@
(rx/from (->> guides (mapv #(remove-guide %))))))))
(defmethod ptk/resolve ::move-frame-guides
[_ ids]
[_ args]
(dm/assert!
"expected a coll of uuids"
(every? uuid? ids))
(every? uuid? (:ids args)))
(ptk/reify ::move-frame-guides
ptk/WatchEvent
(watch [_ state _]
(let [objects (wsh/lookup-page-objects state)
(let [ids (:ids args)
object-modifiers (:modifiers args)
objects (wsh/lookup-page-objects state)
is-frame? (fn [id] (= :frame (get-in objects [id :type])))
frame-ids? (into #{} (filter is-frame?) ids)
object-modifiers (get state :workspace-modifiers)
build-move-event
(fn [guide]
(let [frame (get objects (:frame-id guide))

View file

@ -11,11 +11,13 @@
[app.common.files.changes-builder :as pcb]
[app.common.files.helpers :as cfh]
[app.common.geom.point :as gpt]
[app.common.logic.shapes :as cls]
[app.common.types.page :as ctp]
[app.common.types.shape-tree :as ctst]
[app.common.types.shape.interactions :as ctsi]
[app.common.uuid :as uuid]
[app.main.data.workspace.changes :as dch]
[app.main.data.changes :as dch]
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.state-helpers :as wsh]
[app.main.data.workspace.undo :as dwu]
[app.main.streams :as ms]
@ -26,29 +28,33 @@
;; --- Flows
(defn add-flow
[starting-frame]
([starting-frame]
(add-flow nil nil nil starting-frame))
(dm/assert!
"expect uuid"
(uuid? starting-frame))
([flow-id page-id name starting-frame]
(dm/assert!
"expect uuid"
(uuid? starting-frame))
(ptk/reify ::add-flow
ptk/WatchEvent
(watch [it state _]
(let [page (wsh/lookup-page state)
(ptk/reify ::add-flow
ptk/WatchEvent
(watch [it state _]
(let [page (if page-id
(wsh/lookup-page state page-id)
(wsh/lookup-page state))
flows (get-in page [:options :flows] [])
unames (cfh/get-used-names flows)
name (cfh/generate-unique-name unames "Flow 1")
flows (get-in page [:options :flows] [])
unames (cfh/get-used-names flows)
name (or name (cfh/generate-unique-name unames "Flow 1"))
new-flow {:id (uuid/next)
:name name
:starting-frame starting-frame}]
new-flow {:id (or flow-id (uuid/next))
:name name
:starting-frame starting-frame}]
(rx/of (dch/commit-changes
(-> (pcb/empty-changes it)
(pcb/with-page page)
(pcb/update-page-option :flows ctp/add-flow new-flow))))))))
(rx/of (dch/commit-changes
(-> (pcb/empty-changes it)
(pcb/with-page page)
(pcb/update-page-option :flows ctp/add-flow new-flow)))))))))
(defn add-flow-selected-frame
[]
@ -59,16 +65,35 @@
(rx/of (add-flow (first selected)))))))
(defn remove-flow
[flow-id]
([flow-id]
(remove-flow nil flow-id))
([page-id flow-id]
(dm/assert! (uuid? flow-id))
(ptk/reify ::remove-flow
ptk/WatchEvent
(watch [it state _]
(let [page (if page-id
(wsh/lookup-page state page-id)
(wsh/lookup-page state))]
(rx/of (dch/commit-changes
(-> (pcb/empty-changes it)
(pcb/with-page page)
(pcb/update-page-option :flows ctp/remove-flow flow-id)))))))))
(defn update-flow
[page-id flow-id update-fn]
(dm/assert! (uuid? flow-id))
(ptk/reify ::remove-flow
(ptk/reify ::update-flow
ptk/WatchEvent
(watch [it state _]
(let [page (wsh/lookup-page state)]
(let [page (if page-id
(wsh/lookup-page state page-id)
(wsh/lookup-page state))]
(rx/of (dch/commit-changes
(-> (pcb/empty-changes it)
(pcb/with-page page)
(pcb/update-page-option :flows ctp/remove-flow flow-id))))))))
(pcb/update-page-option :flows ctp/update-flow flow-id update-fn))))))))
(defn rename-flow
[flow-id name]
@ -109,6 +134,18 @@
(or (some ctsi/flow-origin? (map :interactions children))
(some #(ctsi/flow-to? % frame-id) (map :interactions (vals objects))))))
(defn add-interaction
[page-id shape-id interaction]
(ptk/reify ::add-interaction
ptk/WatchEvent
(watch [_ state _]
(let [page-id (or page-id (:current-page-id state))]
(rx/of (dwsh/update-shapes
[shape-id]
(fn [shape]
(cls/add-new-interaction shape interaction))
{:page-id page-id}))))))
(defn add-new-interaction
([shape] (add-new-interaction shape nil))
([shape destination]
@ -125,36 +162,40 @@
:flows] [])
flow (ctp/get-frame-flow flows (:id frame))]
(rx/concat
(rx/of (dch/update-shapes [(:id shape)]
(fn [shape]
(let [new-interaction (-> ctsi/default-interaction
(ctsi/set-destination destination)
(assoc :position-relative-to (:id shape)))]
(update shape :interactions
ctsi/add-interaction new-interaction)))))
(rx/of (dwsh/update-shapes [(:id shape)]
(fn [shape]
(let [new-interaction (-> ctsi/default-interaction
(ctsi/set-destination destination)
(assoc :position-relative-to (:id shape)))]
(cls/add-new-interaction shape new-interaction)))))
(when (and (not (connected-frame? objects (:id frame)))
(nil? flow))
(rx/of (add-flow (:id frame))))))))))
(defn remove-interaction
[shape index]
(ptk/reify ::remove-interaction
ptk/WatchEvent
(watch [_ _ _]
(rx/of (dch/update-shapes [(:id shape)]
(fn [shape]
(update shape :interactions
ctsi/remove-interaction index)))))))
([shape index]
(remove-interaction nil shape index))
([page-id shape index]
(ptk/reify ::remove-interaction
ptk/WatchEvent
(watch [_ _ _]
(rx/of (dwsh/update-shapes [(:id shape)]
(fn [shape]
(update shape :interactions
ctsi/remove-interaction index))
{:page-id page-id}))))))
(defn update-interaction
[shape index update-fn]
(ptk/reify ::update-interaction
ptk/WatchEvent
(watch [_ _ _]
(rx/of (dch/update-shapes [(:id shape)]
(fn [shape]
(update shape :interactions
ctsi/update-interaction index update-fn)))))))
([shape index update-fn]
(update-interaction shape index update-fn nil))
([shape index update-fn options]
(ptk/reify ::update-interaction
ptk/WatchEvent
(watch [_ _ _]
(rx/of (dwsh/update-shapes [(:id shape)]
(fn [shape]
(update shape :interactions
ctsi/update-interaction index update-fn))
options))))))
(defn remove-all-interactions-nav-to
"Remove all interactions that navigate to the given frame."
@ -171,9 +212,9 @@
new-interactions (ctsi/remove-interactions #(ctsi/navs-to? % frame-id)
interactions)]
(when (not= (count interactions) (count new-interactions))
(dch/update-shapes [(:id shape)]
(fn [shape]
(assoc shape :interactions new-interactions))))))]
(dwsh/update-shapes [(:id shape)]
(fn [shape]
(assoc shape :interactions new-interactions))))))]
(rx/from (->> (vals objects)
(map remove-interactions-shape)
@ -260,20 +301,20 @@
(dwu/start-undo-transaction undo-id)
(when (:hide-in-viewer target-frame)
; If the target frame is hidden, we need to unhide it so
; users can navigate to it.
(dch/update-shapes [(:id target-frame)]
#(dissoc % :hide-in-viewer)))
;; If the target frame is hidden, we need to unhide it so
;; users can navigate to it.
(dwsh/update-shapes [(:id target-frame)]
#(dissoc % :hide-in-viewer)))
(cond
(or (nil? shape)
;; Didn't changed the position for the interaction
;; Didn't changed the position for the interaction
(= position initial-pos)
;; New interaction but invalid target
;; New interaction but invalid target
(and (nil? index) (nil? target-frame)))
nil
;; Dropped interaction in an invalid target. We remove it
;; Dropped interaction in an invalid target. We remove it
(and (some? index) (nil? target-frame))
(remove-interaction shape index)
@ -364,5 +405,5 @@
(update interactions index
#(ctsi/set-overlay-position % overlay-pos))]
(rx/of (dch/update-shapes [(:id shape)] #(merge % {:interactions new-interactions})))))))
(rx/of (dwsh/update-shapes [(:id shape)] #(merge % {:interactions new-interactions})))))))

View file

@ -9,7 +9,7 @@
(:require
[app.common.data :as d]
[app.common.math :as mth]
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.state-helpers :as wsh]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
@ -48,7 +48,7 @@
shapes (map #(get objects %) selected)
shapes-ids (->> shapes
(map :id))]
(rx/of (dch/update-shapes shapes-ids #(assoc % :opacity opacity)))))))
(rx/of (dwsh/update-shapes shapes-ids #(assoc % :opacity opacity)))))))
(defn pressed-opacity
[opacity]

View file

@ -20,6 +20,7 @@
:comments
:assets
:document-history
:hide-palettes
:colorpalette
:element-options
:rulers
@ -138,7 +139,8 @@
"A map of layout flags that should be persisted in local storage; the
value corresponds to the key that will be used for save the data in
storage object. It should be namespace qualified."
{:colorpalette :app.main.data.workspace/show-colorpalette?
{:hide-palettes :app.main.data.workspace/hide-palettes?
:colorpalette :app.main.data.workspace/show-colorpalette?
:textpalette :app.main.data.workspace/show-textpalette?})
(defn load-layout-flags

View file

@ -24,15 +24,17 @@
[app.common.types.shape.layout :as ctl]
[app.common.types.typography :as ctt]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.data.changes :as dch]
[app.main.data.comments :as dc]
[app.main.data.events :as ev]
[app.main.data.messages :as msg]
[app.main.data.modal :as modal]
[app.main.data.workspace :as-alias dw]
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.groups :as dwg]
[app.main.data.workspace.notifications :as-alias dwn]
[app.main.data.workspace.selection :as dws]
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.specialized-panel :as dwsp]
[app.main.data.workspace.state-helpers :as wsh]
[app.main.data.workspace.thumbnails :as dwt]
@ -54,7 +56,6 @@
;; Change this to :info :debug or :trace to debug this module, or :warn to reset to default
(log/set-level! :warn)
(defn- pretty-file
[file-id state]
(if (= file-id (:current-file-id state))
@ -106,24 +107,28 @@
(assoc item :path path :name name))))
(defn add-color
[color]
(let [id (uuid/next)
color (-> color
(assoc :id id)
(assoc :name (or (get-in color [:image :name])
(:color color)
(uc/gradient-type->string (get-in color [:gradient :type])))))]
(dm/assert! ::ctc/color color)
(ptk/reify ::add-color
ev/Event
(-data [_] color)
([color]
(add-color color nil))
ptk/WatchEvent
(watch [it _ _]
(let [changes (-> (pcb/empty-changes it)
(pcb/add-color color))]
(rx/of #(assoc-in % [:workspace-local :color-for-rename] id)
(dch/commit-changes changes)))))))
([color {:keys [rename?] :or {rename? true}}]
(let [color (-> color
(update :id #(or % (uuid/next)))
(assoc :name (or (get-in color [:image :name])
(:color color)
(uc/gradient-type->string (get-in color [:gradient :type])))))]
(dm/assert! ::ctc/color color)
(ptk/reify ::add-color
ev/Event
(-data [_] color)
ptk/WatchEvent
(watch [it _ _]
(let [changes (-> (pcb/empty-changes it)
(pcb/add-color color))]
(rx/of
(when rename?
(fn [state] (assoc-in state [:workspace-local :color-for-rename] (:id color))))
(dch/commit-changes changes))))))))
(defn add-recent-color
[color]
@ -336,49 +341,56 @@
(defn- add-component2
"This is the second step of the component creation."
[selected components-v2]
(ptk/reify ::add-component2
ev/Event
(-data [_]
{::ev/name "add-component"
:shapes (count selected)})
([selected components-v2]
(add-component2 nil selected components-v2))
([id-ref selected components-v2]
(ptk/reify ::add-component2
ev/Event
(-data [_]
{::ev/name "add-component"
:shapes (count selected)})
ptk/WatchEvent
(watch [it state _]
(let [file-id (:current-file-id state)
page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
shapes (dwg/shapes-for-grouping objects selected)
parents (into #{} (map :parent-id) shapes)]
(when-not (empty? shapes)
(let [[root _ changes]
(cll/generate-add-component (pcb/empty-changes it) shapes objects page-id file-id components-v2
dwg/prepare-create-group
cfsh/prepare-create-artboard-from-selection)]
(when-not (empty? (:redo-changes changes))
(rx/of (dch/commit-changes changes)
(dws/select-shapes (d/ordered-set (:id root)))
(ptk/data-event :layout/update {:ids parents})))))))))
ptk/WatchEvent
(watch [it state _]
(let [file-id (:current-file-id state)
page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
shapes (dwg/shapes-for-grouping objects selected)
parents (into #{} (map :parent-id) shapes)]
(when-not (empty? shapes)
(let [[root component-id changes]
(cll/generate-add-component (pcb/empty-changes it) shapes objects page-id file-id components-v2
dwg/prepare-create-group
cfsh/prepare-create-artboard-from-selection)]
(when id-ref
(reset! id-ref component-id))
(when-not (empty? (:redo-changes changes))
(rx/of (dch/commit-changes changes)
(dws/select-shapes (d/ordered-set (:id root)))
(ptk/data-event :layout/update {:ids parents}))))))))))
(defn add-component
"Add a new component to current file library, from the currently selected shapes.
This operation is made in two steps, first one for calculate the
shapes that will be part of the component and the second one with
the component creation."
[]
(ptk/reify ::add-component
ptk/WatchEvent
(watch [_ state _]
(let [objects (wsh/lookup-page-objects state)
selected (->> (wsh/lookup-selected state)
(cfh/clean-loops objects))
selected-objects (map #(get objects %) selected)
components-v2 (features/active-feature? state "components/v2")
;; We don't want to change the structure of component copies
can-make-component (every? true? (map #(ctn/valid-shape-for-component? objects %) selected-objects))]
([]
(add-component nil nil))
(when can-make-component
(rx/of (add-component2 selected components-v2)))))))
([id-ref ids]
(ptk/reify ::add-component
ptk/WatchEvent
(watch [_ state _]
(let [objects (wsh/lookup-page-objects state)
selected (->> (d/nilv ids (wsh/lookup-selected state))
(cfh/clean-loops objects))
selected-objects (map #(get objects %) selected)
components-v2 (features/active-feature? state "components/v2")
;; We don't want to change the structure of component copies
can-make-component (every? true? (map #(ctn/valid-shape-for-component? objects %) selected-objects))]
(when can-make-component
(rx/of (add-component2 id-ref selected components-v2))))))))
(defn add-multiple-components
"Add several new components to current file library, from the currently selected shapes."
@ -441,7 +453,7 @@
;; NOTE: only when components-v2 is enabled
(when (and shape-id page-id)
(rx/of (dch/update-shapes [shape-id] #(assoc % :name clean-name) {:page-id page-id :stack-undo? true}))))))))))
(rx/of (dwsh/update-shapes [shape-id] #(assoc % :name clean-name) {:page-id page-id :stack-undo? true}))))))))))
(defn duplicate-component
"Create a new component copied from the one with the given id."
@ -531,7 +543,7 @@
in the given file library. Then selects the newly created instance."
([file-id component-id position]
(instantiate-component file-id component-id position nil))
([file-id component-id position {:keys [start-move? initial-point]}]
([file-id component-id position {:keys [start-move? initial-point id-ref]}]
(dm/assert! (uuid? file-id))
(dm/assert! (uuid? component-id))
(dm/assert! (gpt/point? position))
@ -554,6 +566,10 @@
page
libraries)
undo-id (js/Symbol)]
(when id-ref
(reset! id-ref (:id new-shape)))
(rx/of (dwu/start-undo-transaction undo-id)
(dch/commit-changes changes)
(ptk/data-event :layout/update {:ids [(:id new-shape)]})
@ -599,7 +615,6 @@
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
file (wsh/get-local-file state)
container (cfh/get-container file :page page-id)
libraries (wsh/get-libraries state)
selected (->> state
(wsh/lookup-selected)
@ -611,7 +626,7 @@
changes (when can-detach?
(reduce
(fn [changes id]
(cll/generate-detach-instance changes container libraries id))
(cll/generate-detach-component changes id file page-id libraries))
(pcb/empty-changes it)
selected))]
@ -799,7 +814,7 @@
component (ctkl/get-component data component-id)
page-id (:main-instance-page component)
root-id (:main-instance-id component)]
(dwt/request-thumbnail file-id page-id root-id tag "update-component-thumbnail-sync")))
(dwt/update-thumbnail file-id page-id root-id tag "update-component-thumbnail-sync")))
(defn update-component-sync
([shape-id file-id] (update-component-sync shape-id file-id nil))
@ -1027,6 +1042,9 @@
{:file-id file-id
:library-id library-id}))))))))))
;; FIXME: the data should be set on the backend for clock consistency
(def ignore-sync
"Mark the file as ignore syncs. All library changes before this moment will not
ber notified to sync."
@ -1148,14 +1166,15 @@
changes-s
(->> stream
(rx/filter #(or (dch/commit-changes? %)
(ptk/type? % ::dwn/handle-file-change)))
(rx/filter dch/commit?)
(rx/map deref)
(rx/filter #(= :local (:source %)))
(rx/observe-on :async))
check-changes
(fn [[event [old-data _mid_data _new-data]]]
(when old-data
(let [{:keys [file-id changes save-undo? undo-group]} (deref event)
(let [{:keys [file-id changes save-undo? undo-group]} event
changed-components
(when (or (nil? file-id) (= file-id (:id old-data)))
@ -1165,7 +1184,7 @@
(if (d/not-empty? changed-components)
(if save-undo?
(do (log/info :msg "DETECTED COMPONENTS CHANGED"
(do (log/info :hint "detected component changes"
:ids (map str changed-components)
:undo-group undo-group)
@ -1174,7 +1193,8 @@
;; even if save-undo? is false, we need to update the :modified-date of the component
;; (for example, for undos)
(->> (rx/from changed-components)
(rx/map #(touch-component %))))
(rx/map touch-component)))
(rx/empty)))))
changes-s
@ -1188,7 +1208,7 @@
(rx/debounce 5000)
(rx/tap #(log/trc :hint "buffer initialized")))]
(when components-v2?
(when (and components-v2? (contains? cf/flags :component-thumbnails))
(->> (rx/merge
changes-s
@ -1266,18 +1286,20 @@
ptk/WatchEvent
(watch [_ state _]
(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))
(->> (rp/cmd! :get-file {:id library-id :features features})
(rx/merge-map fpmap/resolve-file)
(rx/map (fn [file]
(fn [state]
(assoc-in state [:workspace-libraries library-id] file)))))
(->> (rp/cmd! :get-file-object-thumbnails {:file-id library-id :tag "component"})
(rx/map (fn [thumbnails]
(fn [state]
(update state :workspace-thumbnails merge thumbnails))))))))))
(rx/concat
(rx/merge
(->> (rp/cmd! :link-file-to-library {:file-id file-id :library-id library-id})
(rx/ignore))
(->> (rp/cmd! :get-file {:id library-id :features features})
(rx/merge-map fpmap/resolve-file)
(rx/map (fn [file]
(fn [state]
(assoc-in state [:workspace-libraries library-id] file)))))
(->> (rp/cmd! :get-file-object-thumbnails {:file-id library-id :tag "component"})
(rx/map (fn [thumbnails]
(fn [state]
(update state :workspace-thumbnails merge thumbnails))))))
(rx/of (ptk/reify ::attach-library-finished)))))))
(defn unlink-file-from-library
[file-id library-id]

View file

@ -20,9 +20,9 @@
[app.common.types.shape :as cts]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.data.changes :as dch]
[app.main.data.media :as dmm]
[app.main.data.messages :as msg]
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.state-helpers :as wsh]
@ -131,7 +131,7 @@
(rx/merge-map svg->clj)
(rx/tap on-svg)))))
(defn- process-blobs
(defn process-blobs
[{:keys [file-id local? name blobs force-media on-image on-svg]}]
(letfn [(svg-blob? [blob]
(and (not force-media)
@ -467,4 +467,5 @@
(watch [_ _ _]
(->> (svg->clj [name svg-string])
(rx/take 1)
(rx/map #(svg/add-svg-shapes id % position {:change-selection? false}))))))
(rx/map #(svg/add-svg-shapes id % position {:ignore-selection? true
:change-selection? false}))))))

View file

@ -23,9 +23,9 @@
[app.common.types.shape.layout :as ctl]
[app.common.uuid :as uuid]
[app.main.constants :refer [zoom-half-pixel-precision]]
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.comments :as-alias dwcm]
[app.main.data.workspace.guides :as-alias dwg]
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.state-helpers :as wsh]
[app.main.data.workspace.undo :as dwu]
[beicon.v2.core :as rx]
@ -438,28 +438,28 @@
;; - It consideres the center for everyshape instead of the center of the total selrect
;; - The angle param is the desired final value, not a delta
(defn set-delta-rotation-modifiers
([angle shapes]
(ptk/reify ::set-delta-rotation-modifiers
ptk/UpdateEvent
(update [_ state]
(let [objects (wsh/lookup-page-objects state)
ids
(->> shapes
(remove #(get % :blocked false))
(filter #(contains? (get editable-attrs (:type %)) :rotation))
(map :id))
[angle shapes {:keys [center delta?] :or {center nil delta? false}}]
(ptk/reify ::set-delta-rotation-modifiers
ptk/UpdateEvent
(update [_ state]
(let [objects (wsh/lookup-page-objects state)
ids
(->> shapes
(remove #(get % :blocked false))
(filter #(contains? (get editable-attrs (:type %)) :rotation))
(map :id))
get-modifier
(fn [shape]
(let [delta (- angle (:rotation shape))
center (gsh/shape->center shape)]
(ctm/rotation-modifiers shape center delta)))
get-modifier
(fn [shape]
(let [delta (if delta? angle (- angle (:rotation shape)))
center (or center (gsh/shape->center shape))]
(ctm/rotation-modifiers shape center delta)))
modif-tree
(-> (build-modif-tree ids objects get-modifier)
(gm/set-objects-modifiers objects))]
modif-tree
(-> (build-modif-tree ids objects get-modifier)
(gm/set-objects-modifiers objects))]
(assoc state :workspace-modifiers modif-tree))))))
(assoc state :workspace-modifiers modif-tree)))))
(defn apply-modifiers
([]
@ -497,9 +497,9 @@
(if undo-transation?
(rx/of (dwu/start-undo-transaction undo-id))
(rx/empty))
(rx/of (ptk/event ::dwg/move-frame-guides ids-with-children)
(rx/of (ptk/event ::dwg/move-frame-guides {:ids ids-with-children :modifiers object-modifiers})
(ptk/event ::dwcm/move-frame-comment-threads ids-with-children)
(dch/update-shapes
(dwsh/update-shapes
ids
(fn [shape]
(let [modif (get-in object-modifiers [(:id shape) :modifiers])
@ -559,8 +559,10 @@
:layout-grid-rows]})
;; We've applied the text-modifier so we can dissoc the temporary data
(fn [state]
(update state :workspace-text-modifier #(apply dissoc % ids)))
(clear-local-transform))
(update state :workspace-text-modifier #(apply dissoc % ids))))
(if (nil? modifiers)
(rx/of (clear-local-transform))
(rx/empty))
(if undo-transation?
(rx/of (dwu/commit-undo-transaction undo-id))
(rx/empty))))))))

View file

@ -11,11 +11,10 @@
[app.common.files.changes :as cpc]
[app.common.schema :as sm]
[app.common.uuid :as uuid]
[app.main.data.changes :as dch]
[app.main.data.common :refer [handle-notification]]
[app.main.data.websocket :as dws]
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.persistence :as dwp]
[app.util.globals :refer [global]]
[app.util.mouse :as mse]
[app.util.object :as obj]
@ -84,7 +83,7 @@
(->> stream
(rx/filter mse/pointer-event?)
(rx/filter #(= :viewport (mse/get-pointer-source %)))
(rx/pipe (rxs/throttle 100))
(rx/pipe (rxs/throttle 50))
(rx/map #(handle-pointer-send file-id (:pt %)))))
(rx/take-until stopper))]
@ -110,9 +109,15 @@
ptk/WatchEvent
(watch [_ state _]
(let [page-id (:current-page-id state)
local (:workspace-local state)
message {:type :pointer-update
:file-id file-id
:page-id page-id
:zoom (:zoom local)
:zoom-inverse (:zoom-inverse local)
:vbox (:vbox local)
:vport (:vport local)
:position point}]
(rx/of (dws/send message))))))
@ -174,13 +179,17 @@
(update state :workspace-presence update-presence))))))
(defn handle-pointer-update
[{:keys [page-id session-id position] :as msg}]
[{:keys [page-id session-id position zoom zoom-inverse vbox vport] :as msg}]
(ptk/reify ::handle-pointer-update
ptk/UpdateEvent
(update [_ state]
(update-in state [:workspace-presence session-id]
(fn [session]
(assoc session
:zoom zoom
:zoom-inverse zoom-inverse
:vbox vbox
:vport vport
:point position
:updated-at (dt/now)
:page-id page-id))))))
@ -197,9 +206,10 @@
[:changes ::cpc/changes]]))
(defn handle-file-change
[{:keys [file-id changes] :as msg}]
[{:keys [file-id changes revn] :as msg}]
(dm/assert!
"expected valid arguments"
"expected valid parameters"
(sm/check! schema:handle-file-change msg))
(ptk/reify ::handle-file-change
@ -207,45 +217,16 @@
(-deref [_] {:changes changes})
ptk/WatchEvent
(watch [_ state _]
(let [page-id (:current-page-id state)
position-data-operation?
(fn [{:keys [type attr]}]
(and (= :set type) (= attr :position-data)))
;;add-origin-session-id
;;(fn [{:keys [] :as op}]
;; (cond-> op
;; (position-data-operation? op)
;; (update :val with-meta {:session-id (:session-id msg)})))
update-position-data
(fn [change]
;; Remove the position data from remote operations. Will be changed localy, otherwise
;; creates a strange "out-of-sync" behaviour.
(cond-> change
(and (= page-id (:page-id change))
(= :mod-obj (:type change)))
(update :operations #(d/removev position-data-operation? %))))
process-page-changes
(fn [[page-id changes]]
(dch/update-indices page-id changes))
;; We update `position-data` from the incoming message
changes (->> changes
(mapv update-position-data)
(d/removev (fn [change]
(and (= page-id (:page-id change))
(:ignore-remote? change)))))
changes-by-pages (group-by :page-id changes)]
(rx/merge
(rx/of (dwp/shapes-changes-persisted file-id (assoc msg :changes changes)))
(when-not (empty? changes-by-pages)
(rx/from (map process-page-changes changes-by-pages))))))))
(watch [_ _ _]
;; The commit event is responsible to apply the data localy
;; and update the persistence internal state with the updated
;; file-revn
(rx/of (dch/commit {:file-id file-id
:file-revn revn
:save-undo? false
:source :remote
:redo-changes (vec changes)
:undo-changes []})))))
(def ^:private
schema:handle-library-change

View file

@ -8,7 +8,7 @@
(:require
[app.common.data.macros :as dm]
[app.common.files.changes-builder :as pcb]
[app.main.data.workspace.changes :as dch]
[app.main.data.changes :as dch]
[app.main.data.workspace.path.common :refer [check-path-content!]]
[app.main.data.workspace.path.helpers :as helpers]
[app.main.data.workspace.path.state :as st]

View file

@ -15,7 +15,6 @@
[app.common.types.shape :as cts]
[app.common.types.shape-tree :as ctst]
[app.common.types.shape.layout :as ctl]
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.drawing.common :as dwdc]
[app.main.data.workspace.edition :as dwe]
[app.main.data.workspace.path.changes :as changes]
@ -24,6 +23,7 @@
[app.main.data.workspace.path.state :as st]
[app.main.data.workspace.path.streams :as streams]
[app.main.data.workspace.path.undo :as undo]
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.state-helpers :as wsh]
[app.util.mouse :as mse]
[beicon.v2.core :as rx]
@ -333,7 +333,7 @@
edit-mode (get-in state [:workspace-local :edit-path id :edit-mode])]
(if (= :draw edit-mode)
(rx/concat
(rx/of (dch/update-shapes [id] upsp/convert-to-path))
(rx/of (dwsh/update-shapes [id] upsp/convert-to-path))
(rx/of (handle-drawing id))
(->> stream
(rx/filter (ptk/type? ::common/finish-path))

View file

@ -14,7 +14,7 @@
[app.common.svg.path.command :as upc]
[app.common.svg.path.shapes-to-path :as upsp]
[app.common.svg.path.subpath :as ups]
[app.main.data.workspace.changes :as dch]
[app.main.data.changes :as dch]
[app.main.data.workspace.edition :as dwe]
[app.main.data.workspace.path.changes :as changes]
[app.main.data.workspace.path.drawing :as drawing]
@ -23,6 +23,7 @@
[app.main.data.workspace.path.state :as st]
[app.main.data.workspace.path.streams :as streams]
[app.main.data.workspace.path.undo :as undo]
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.state-helpers :as wsh]
[app.main.streams :as ms]
[app.util.mouse :as mse]
@ -114,6 +115,9 @@
(update [_ state]
(let [id (st/get-path-id state)
content (st/get-path state :content)
to-point (cond-> to-point
(:shift? to-point) (helpers/position-fixed-angle from-point))
delta (gpt/subtract to-point from-point)
modifiers-reducer (partial modify-content-point content delta)
@ -140,7 +144,7 @@
selected? (contains? selected-points position)]
(streams/drag-stream
(rx/of
(dch/update-shapes [id] upsp/convert-to-path)
(dwsh/update-shapes [id] upsp/convert-to-path)
(when-not selected? (selection/select-node position shift?))
(drag-selected-points @ms/mouse-position))
(rx/of (selection/select-node position shift?)))))))
@ -224,7 +228,7 @@
mov-vec (gpt/multiply (get-displacement direction) scale)]
(rx/concat
(rx/of (dch/update-shapes [id] upsp/convert-to-path))
(rx/of (dwsh/update-shapes [id] upsp/convert-to-path))
(rx/merge
(->> move-events
(rx/take-until stopper)
@ -262,7 +266,7 @@
(streams/drag-stream
(rx/concat
(rx/of (dch/update-shapes [id] upsp/convert-to-path))
(rx/of (dwsh/update-shapes [id] upsp/convert-to-path))
(->> (streams/move-handler-stream handler point handler opposite points)
(rx/map
(fn [{:keys [x y alt? shift?]}]
@ -351,5 +355,5 @@
ptk/WatchEvent
(watch [_ state _]
(let [id (st/get-path-id state)]
(rx/of (dch/update-shapes [id] upsp/convert-to-path)
(rx/of (dwsh/update-shapes [id] upsp/convert-to-path)
(split-segments event))))))

View file

@ -22,6 +22,7 @@
(or (= type ::common/finish-path)
(= type :app.main.data.workspace.path.shortcuts/esc-pressed)
(= type :app.main.data.workspace.common/clear-edition-mode)
(= type :app.main.data.workspace.edition/clear-edition-mode)
(= type :app.main.data.workspace/finalize-page)
(= event :interrupt) ;; ESC
(and ^boolean (mse/mouse-event? event)

View file

@ -10,7 +10,7 @@
[app.common.files.helpers :as cph]
[app.common.svg.path.shapes-to-path :as upsp]
[app.common.types.container :as ctn]
[app.main.data.workspace.changes :as dch]
[app.main.data.changes :as dch]
[app.main.data.workspace.state-helpers :as wsh]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))

View file

@ -101,7 +101,12 @@
(->> ms/mouse-position
(rx/map to-pixel-snap)
(rx/with-latest-from (snap-toggled-stream))
(rx/map check-path-snap))))
(rx/map check-path-snap)
(rx/with-latest-from
(fn [position shift? alt?]
(assoc position :shift? shift? :alt? alt?))
ms/mouse-position-shift
ms/mouse-position-alt))))
(defn get-angle [node handler opposite]
(when (and (some? node) (some? handler) (some? opposite))

View file

@ -8,10 +8,11 @@
(:require
[app.common.svg.path.shapes-to-path :as upsp]
[app.common.svg.path.subpath :as ups]
[app.main.data.workspace.changes :as dch]
[app.main.data.changes :as dch]
[app.main.data.workspace.edition :as dwe]
[app.main.data.workspace.path.changes :as changes]
[app.main.data.workspace.path.state :as st]
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.state-helpers :as wsh]
[app.util.path.tools :as upt]
[beicon.v2.core :as rx]
@ -37,7 +38,7 @@
changes (changes/generate-path-changes it objects page-id shape (:content shape) new-content)]
(rx/concat
(rx/of (dch/update-shapes [id] upsp/convert-to-path))
(rx/of (dwsh/update-shapes [id] upsp/convert-to-path))
(rx/of (dch/commit-changes changes)
(when (empty? new-content)
(dwe/clear-edition-mode)))))))))))

View file

@ -1,263 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.main.data.workspace.persistence
(:require
[app.common.data.macros :as dm]
[app.common.files.changes :as cpc]
[app.common.logging :as log]
[app.common.types.shape-tree :as ctst]
[app.common.uuid :as uuid]
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.thumbnails :as dwt]
[app.main.features :as features]
[app.main.repo :as rp]
[app.main.store :as st]
[app.util.time :as dt]
[beicon.v2.core :as rx]
[okulary.core :as l]
[potok.v2.core :as ptk]))
(log/set-level! :info)
(declare persist-changes)
(declare persist-synchronous-changes)
(declare shapes-changes-persisted)
(declare shapes-changes-persisted-finished)
(declare update-persistence-status)
;; --- Persistence
(defn initialize-file-persistence
[file-id]
(ptk/reify ::initialize-persistence
ptk/WatchEvent
(watch [_ _ stream]
(log/debug :hint "initialize persistence")
(let [stopper (rx/filter (ptk/type? ::initialize-persistence) stream)
commits (l/atom [])
saving? (l/atom false)
local-file?
#(as-> (:file-id %) event-file-id
(or (nil? event-file-id)
(= event-file-id file-id)))
library-file?
#(as-> (:file-id %) event-file-id
(and (some? event-file-id)
(not= event-file-id file-id)))
on-dirty
(fn []
;; Enable reload stopper
(swap! st/ongoing-tasks conj :workspace-change)
(st/emit! (update-persistence-status {:status :pending})))
on-saving
(fn []
(reset! saving? true)
(st/emit! (update-persistence-status {:status :saving})))
on-saved
(fn []
;; Disable reload stopper
(swap! st/ongoing-tasks disj :workspace-change)
(st/emit! (update-persistence-status {:status :saved}))
(reset! saving? false))]
(rx/merge
(->> stream
(rx/filter dch/commit-changes?)
(rx/map deref)
(rx/filter local-file?)
(rx/tap on-dirty)
(rx/filter (complement empty?))
(rx/map (fn [commit]
(-> commit
(assoc :id (uuid/next))
(assoc :file-id file-id))))
(rx/observe-on :async)
(rx/tap #(swap! commits conj %))
(rx/take-until (rx/delay 100 stopper))
(rx/finalize (fn []
(log/debug :hint "finalize persistence: changes watcher"))))
(->> (rx/from-atom commits)
(rx/filter (complement empty?))
(rx/sample-when
(rx/merge
(rx/filter #(= ::force-persist %) stream)
(->> (rx/merge
(rx/interval 5000)
(->> (rx/from-atom commits)
(rx/filter (complement empty?))
(rx/debounce 2000)))
;; Not sample while saving so there are no race conditions
(rx/filter #(not @saving?)))))
(rx/tap #(reset! commits []))
(rx/tap on-saving)
(rx/mapcat (fn [changes]
;; NOTE: this is needed for don't start the
;; next persistence before this one is
;; finished.
(if-let [file-revn (dm/get-in @st/state [:workspace-file :revn])]
(rx/merge
(->> (rx/of (persist-changes file-id file-revn changes commits))
(rx/observe-on :async))
(->> stream
;; We wait for every change to be persisted
(rx/filter (ptk/type? ::shapes-changes-persisted-finished))
(rx/take 1)
(rx/tap on-saved)
(rx/ignore)))
(rx/empty))))
(rx/take-until (rx/delay 100 stopper))
(rx/finalize (fn []
(log/debug :hint "finalize persistence: save loop"))))
;; Synchronous changes
(->> stream
(rx/filter dch/commit-changes?)
(rx/map deref)
(rx/filter library-file?)
(rx/filter (complement #(empty? (:changes %))))
(rx/map persist-synchronous-changes)
(rx/take-until (rx/delay 100 stopper))
(rx/finalize (fn []
(log/debug :hint "finalize persistence: synchronous save loop")))))))))
(defn persist-changes
[file-id file-revn changes pending-commits]
(log/debug :hint "persist changes" :changes (count changes))
(dm/assert! (uuid? file-id))
(ptk/reify ::persist-changes
ptk/WatchEvent
(watch [_ 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}]
(->> (rp/cmd! :update-file params)
(rx/mapcat (fn [lagged]
(log/debug :hint "changes persisted" :lagged (count lagged))
(let [frame-updates
(-> (group-by :page-id changes)
(update-vals #(into #{} (mapcat :frames) %)))
commits
(->> @pending-commits
(map #(assoc % :revn file-revn)))]
(rx/concat
(rx/merge
(->> (rx/from frame-updates)
(rx/mapcat (fn [[page-id frames]]
(->> frames (map (fn [frame-id] [file-id page-id frame-id])))))
(rx/map (fn [data]
(ptk/data-event ::dwt/update data))))
(->> (rx/from (concat lagged commits))
(rx/merge-map
(fn [{:keys [changes] :as entry}]
(rx/merge
(rx/from
(for [[page-id changes] (group-by :page-id changes)]
(dch/update-indices page-id changes)))
(rx/of (shapes-changes-persisted file-id entry)))))))
(rx/of (shapes-changes-persisted-finished))))))
(rx/catch (fn [cause]
(if (instance? js/TypeError cause)
(->> (rx/timer 2000)
(rx/map (fn [_]
(persist-changes file-id file-revn changes pending-commits))))
(rx/throw cause)))))))))
;; Event to be thrown after the changes have been persisted
(defn shapes-changes-persisted-finished
[]
(ptk/reify ::shapes-changes-persisted-finished))
(defn persist-synchronous-changes
[{:keys [file-id changes]}]
(dm/assert! (uuid? file-id))
(ptk/reify ::persist-synchronous-changes
ptk/WatchEvent
(watch [_ state _]
(let [features (features/get-team-enabled-features state)
sid (:session-id state)
file (dm/get-in state [:workspace-libraries file-id])
params {:id (:id file)
:revn (:revn file)
:session-id sid
:changes changes
:features features}]
(when (:id params)
(->> (rp/cmd! :update-file params)
(rx/ignore)))))))
(defn update-persistence-status
[{:keys [status reason]}]
(ptk/reify ::update-persistence-status
ptk/UpdateEvent
(update [_ state]
(update state :workspace-persistence
(fn [local]
(assoc local
:reason reason
:status status
:updated-at (dt/now)))))))
(defn shapes-persisted-event? [event]
(= (ptk/type event) ::changes-persisted))
(defn shapes-changes-persisted
[file-id {:keys [revn changes] persisted-session-id :session-id}]
(dm/assert! (uuid? file-id))
(dm/assert! (int? revn))
(dm/assert! (cpc/check-changes! changes))
(ptk/reify ::shapes-changes-persisted
ptk/UpdateEvent
(update [_ state]
;; NOTE: we don't set the file features context here because
;; there are no useful context for code that need to be executed
;; on the frontend side
(let [current-file-id (:current-file-id state)
current-session-id (:session-id state)]
(if (and (some? current-file-id)
;; If the remote change is from teh current session we skip
(not= persisted-session-id current-session-id))
(if (= file-id current-file-id)
(let [changes (group-by :page-id changes)]
(-> state
(update-in [:workspace-file :revn] max revn)
(update :workspace-data
(fn [file]
(loop [fdata file
entries (seq changes)]
(if-let [[page-id changes] (first entries)]
(recur (-> fdata
(cpc/process-changes changes)
(cond-> (some? page-id)
(ctst/update-object-indices page-id)))
(rest entries))
fdata))))))
(-> state
(update-in [:workspace-libraries file-id :revn] max revn)
(update-in [:workspace-libraries file-id :data] cpc/process-changes changes)))
state)))))

View file

@ -18,9 +18,9 @@
[app.common.record :as cr]
[app.common.types.component :as ctk]
[app.common.uuid :as uuid]
[app.main.data.changes :as dch]
[app.main.data.events :as ev]
[app.main.data.modal :as md]
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.collapse :as dwc]
[app.main.data.workspace.specialized-panel :as-alias dwsp]
[app.main.data.workspace.state-helpers :as wsh]

View file

@ -20,8 +20,8 @@
[app.common.types.modifiers :as ctm]
[app.common.types.shape.layout :as ctl]
[app.common.uuid :as uuid]
[app.main.data.changes :as dch]
[app.main.data.events :as ev]
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.colors :as cl]
[app.main.data.workspace.grid-layout.editor :as dwge]
[app.main.data.workspace.modifiers :as dwm]
@ -148,8 +148,8 @@
layout-initializer (get-layout-initializer type from-frame? calculate-params?)]
(rx/of (dwu/start-undo-transaction undo-id)
(dch/update-shapes [id] layout-initializer {:with-objects? true})
(dch/update-shapes (dm/get-prop parent :shapes) #(dissoc % :constraints-h :constraints-v))
(dwsh/update-shapes [id] layout-initializer {:with-objects? true})
(dwsh/update-shapes (dm/get-prop parent :shapes) #(dissoc % :constraints-h :constraints-v))
(ptk/data-event :layout/update {:ids [id]})
(dwu/commit-undo-transaction undo-id))))))
@ -188,8 +188,8 @@
(dwsh/create-artboard-from-selection new-shape-id parent-id group-index (:name (first selected-shapes)))
(cl/remove-all-fills [new-shape-id] {:color clr/black :opacity 1})
(create-layout-from-id new-shape-id type)
(dch/update-shapes [new-shape-id] #(assoc % :layout-item-h-sizing :auto :layout-item-v-sizing :auto))
(dch/update-shapes selected #(assoc % :layout-item-h-sizing :fix :layout-item-v-sizing :fix))
(dwsh/update-shapes [new-shape-id] #(assoc % :layout-item-h-sizing :auto :layout-item-v-sizing :auto))
(dwsh/update-shapes selected #(assoc % :layout-item-h-sizing :fix :layout-item-v-sizing :fix))
(dwsh/delete-shapes page-id selected)
(ptk/data-event :layout/update {:ids [new-shape-id]})
(dwu/commit-undo-transaction undo-id)))
@ -199,8 +199,8 @@
(dwsh/create-artboard-from-selection new-shape-id)
(cl/remove-all-fills [new-shape-id] {:color clr/black :opacity 1})
(create-layout-from-id new-shape-id type)
(dch/update-shapes [new-shape-id] #(assoc % :layout-item-h-sizing :auto :layout-item-v-sizing :auto))
(dch/update-shapes selected #(assoc % :layout-item-h-sizing :fix :layout-item-v-sizing :fix))))
(dwsh/update-shapes [new-shape-id] #(assoc % :layout-item-h-sizing :auto :layout-item-v-sizing :auto))
(dwsh/update-shapes selected #(assoc % :layout-item-h-sizing :fix :layout-item-v-sizing :fix))))
(rx/of (ptk/data-event :layout/update {:ids [new-shape-id]})
(dwu/commit-undo-transaction undo-id)))))))
@ -213,7 +213,7 @@
(let [undo-id (js/Symbol)]
(rx/of
(dwu/start-undo-transaction undo-id)
(dch/update-shapes ids #(apply dissoc % layout-keys))
(dwsh/update-shapes ids #(apply dissoc % layout-keys))
(ptk/data-event :layout/update {:ids ids})
(dwu/commit-undo-transaction undo-id))))))
@ -266,7 +266,7 @@
(watch [_ _ _]
(let [undo-id (js/Symbol)]
(rx/of (dwu/start-undo-transaction undo-id)
(dch/update-shapes ids (d/patch-object changes))
(dwsh/update-shapes ids (d/patch-object changes))
(ptk/data-event :layout/update {:ids ids})
(dwu/commit-undo-transaction undo-id))))))
@ -280,7 +280,7 @@
(watch [_ _ _]
(let [undo-id (js/Symbol)]
(rx/of (dwu/start-undo-transaction undo-id)
(dch/update-shapes
(dwsh/update-shapes
ids
(fn [shape]
(case type
@ -313,7 +313,7 @@
(if shapes-to-delete
(dwsh/delete-shapes shapes-to-delete)
(rx/empty))
(dch/update-shapes
(dwsh/update-shapes
ids
(fn [shape objects]
(case type
@ -387,7 +387,7 @@
(watch [_ _ _]
(let [undo-id (js/Symbol)]
(rx/of (dwu/start-undo-transaction undo-id)
(dch/update-shapes
(dwsh/update-shapes
ids
(fn [shape]
(case type
@ -433,7 +433,7 @@
:row :layout-grid-rows
:column :layout-grid-columns)]
(rx/of (dwu/start-undo-transaction undo-id)
(dch/update-shapes
(dwsh/update-shapes
ids
(fn [shape]
(-> shape
@ -525,9 +525,9 @@
parent-ids (->> ids (map #(cfh/get-parent-id objects %)))
undo-id (js/Symbol)]
(rx/of (dwu/start-undo-transaction undo-id)
(dch/update-shapes ids (d/patch-object changes))
(dch/update-shapes children-ids (partial fix-child-sizing objects changes))
(dch/update-shapes
(dwsh/update-shapes ids (d/patch-object changes))
(dwsh/update-shapes children-ids (partial fix-child-sizing objects changes))
(dwsh/update-shapes
parent-ids
(fn [parent objects]
(-> parent
@ -546,8 +546,7 @@
(let [undo-id (js/Symbol)]
(rx/of
(dwu/start-undo-transaction undo-id)
(dch/update-shapes
(dwsh/update-shapes
[layout-id]
(fn [shape]
(->> ids
@ -570,7 +569,7 @@
(let [undo-id (js/Symbol)]
(rx/of
(dwu/start-undo-transaction undo-id)
(dch/update-shapes
(dwsh/update-shapes
[layout-id]
(fn [shape objects]
(case mode
@ -636,7 +635,7 @@
(let [undo-id (js/Symbol)]
(rx/of
(dwu/start-undo-transaction undo-id)
(dch/update-shapes
(dwsh/update-shapes
[layout-id]
(fn [shape objects]
(let [cells (->> ids (map #(get-in shape [:layout-grid-cells %])))
@ -668,7 +667,7 @@
(let [undo-id (js/Symbol)]
(rx/of
(dwu/start-undo-transaction undo-id)
(dch/update-shapes
(dwsh/update-shapes
[layout-id]
(fn [shape objects]
(let [prev-data (-> (dm/get-in shape [:layout-grid-cells cell-id])

View file

@ -16,8 +16,8 @@
[app.common.types.container :as ctn]
[app.common.types.shape :as cts]
[app.common.types.shape-tree :as ctst]
[app.main.data.changes :as dch]
[app.main.data.comments :as dc]
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.edition :as dwe]
[app.main.data.workspace.selection :as dws]
[app.main.data.workspace.state-helpers :as wsh]
@ -26,6 +26,73 @@
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
(def ^:private update-layout-attr? #{:hidden})
(defn- add-undo-group
[changes state]
(let [undo (:workspace-undo state)
items (:items undo)
index (or (:index undo) (dec (count items)))
prev-item (when-not (or (empty? items) (= index -1))
(get items index))
undo-group (:undo-group prev-item)
add-undo-group? (and
(not (nil? undo-group))
(= (get-in changes [:redo-changes 0 :type]) :mod-obj)
(= (get-in prev-item [:redo-changes 0 :type]) :add-obj)
(contains? (:tags prev-item) :alt-duplication))] ;; This is a copy-and-move with mouse+alt
(cond-> changes add-undo-group? (assoc :undo-group undo-group))))
(defn update-shapes
([ids update-fn] (update-shapes ids update-fn nil))
([ids update-fn {:keys [reg-objects? save-undo? stack-undo? attrs ignore-tree page-id ignore-touched undo-group with-objects?]
:or {reg-objects? false save-undo? true stack-undo? false ignore-touched false with-objects? false}}]
(dm/assert!
"expected a valid coll of uuid's"
(sm/check-coll-of-uuid! ids))
(dm/assert! (fn? update-fn))
(ptk/reify ::update-shapes
ptk/WatchEvent
(watch [it state _]
(let [page-id (or page-id (:current-page-id state))
objects (wsh/lookup-page-objects state page-id)
ids (into [] (filter some?) ids)
update-layout-ids
(->> ids
(map (d/getf objects))
(filter #(some update-layout-attr? (pcb/changed-attrs % objects update-fn {:attrs attrs :with-objects? with-objects?})))
(map :id))
changes (-> (pcb/empty-changes it page-id)
(pcb/set-save-undo? save-undo?)
(pcb/set-stack-undo? stack-undo?)
(cls/generate-update-shapes ids
update-fn
objects
{:attrs attrs
:ignore-tree ignore-tree
:ignore-touched ignore-touched
:with-objects? with-objects?})
(cond-> undo-group
(pcb/set-undo-group undo-group)))
changes (add-undo-group changes state)]
(rx/concat
(if (seq (:redo-changes changes))
(let [changes (cond-> changes reg-objects? (pcb/resize-parents ids))]
(rx/of (dch/commit-changes changes)))
(rx/empty))
;; Update layouts for properties marked
(if (d/not-empty? update-layout-ids)
(rx/of (ptk/data-event :layout/update {:ids update-layout-ids}))
(rx/empty))))))))
(defn add-shape
([shape]
(add-shape shape {}))
@ -227,7 +294,7 @@
ids (if (boolean? blocked)
(into ids (->> ids (mapcat #(cfh/get-children-ids objects %))))
ids)]
(rx/of (dch/update-shapes ids update-fn {:attrs #{:blocked :hidden} :undo-group undo-group}))))))
(rx/of (update-shapes ids update-fn {:attrs #{:blocked :hidden} :undo-group undo-group}))))))
(defn toggle-visibility-selected
[]
@ -235,7 +302,7 @@
ptk/WatchEvent
(watch [_ state _]
(let [selected (wsh/lookup-selected state)]
(rx/of (dch/update-shapes selected #(update % :hidden not)))))))
(rx/of (update-shapes selected #(update % :hidden not)))))))
(defn toggle-lock-selected
[]
@ -243,7 +310,7 @@
ptk/WatchEvent
(watch [_ state _]
(let [selected (wsh/lookup-selected state)]
(rx/of (dch/update-shapes selected #(update % :blocked not)))))))
(rx/of (update-shapes selected #(update % :blocked not)))))))
;; FIXME: this need to be refactored
@ -273,8 +340,8 @@
(map (partial vector id)))))))
(d/group-by first second)
(map (fn [[page-id frame-ids]]
(dch/update-shapes frame-ids #(dissoc % :use-for-thumbnail) {:page-id page-id})))))
(update-shapes frame-ids #(dissoc % :use-for-thumbnail) {:page-id page-id})))))
;; And finally: toggle the flag value on all the selected shapes
(rx/of (dch/update-shapes selected #(update % :use-for-thumbnail not))
(rx/of (update-shapes selected #(update % :use-for-thumbnail not))
(dwu/commit-undo-transaction undo-id)))))))

View file

@ -14,7 +14,6 @@
[app.main.data.users :as du]
[app.main.data.workspace :as dw]
[app.main.data.workspace.colors :as mdc]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.drawing :as dwd]
[app.main.data.workspace.layers :as dwly]
[app.main.data.workspace.libraries :as dwl]
@ -28,7 +27,8 @@
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.hooks.resize :as r]
[app.util.dom :as dom]))
[app.util.dom :as dom]
[potok.v2.core :as ptk]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Shortcuts
@ -51,12 +51,12 @@
:undo {:tooltip (ds/meta "Z")
:command (ds/c-mod "z")
:subsections [:edit]
:fn #(emit-when-no-readonly dwc/undo)}
:fn #(emit-when-no-readonly dwu/undo)}
:redo {:tooltip (ds/meta "Y")
:command [(ds/c-mod "shift+z") (ds/c-mod "y")]
:subsections [:edit]
:fn #(emit-when-no-readonly dwc/redo)}
:fn #(emit-when-no-readonly dwu/redo)}
:clear-undo {:tooltip (ds/alt "Q")
:command "alt+q"
@ -120,22 +120,22 @@
:group {:tooltip (ds/meta "G")
:command (ds/c-mod "g")
:subsections [:modify-layers]
:fn #(emit-when-no-readonly dw/group-selected)}
:fn #(emit-when-no-readonly (dw/group-selected))}
:ungroup {:tooltip (ds/shift "G")
:command "shift+g"
:subsections [:modify-layers]
:fn #(emit-when-no-readonly dw/ungroup-selected)}
:fn #(emit-when-no-readonly (dw/ungroup-selected))}
:mask {:tooltip (ds/meta "M")
:command (ds/c-mod "m")
:subsections [:modify-layers]
:fn #(emit-when-no-readonly dw/mask-group)}
:fn #(emit-when-no-readonly (dw/mask-group))}
:unmask {:tooltip (ds/meta-shift "M")
:command (ds/c-mod "shift+m")
:subsections [:modify-layers]
:fn #(emit-when-no-readonly dw/unmask-group)}
:fn #(emit-when-no-readonly (dw/unmask-group))}
:create-component {:tooltip (ds/meta "K")
:command (ds/c-mod "k")
@ -437,14 +437,16 @@
:command (ds/a-mod "p")
:subsections [:panels]
:fn #(do (r/set-resize-type! :bottom)
(emit-when-no-readonly (dw/remove-layout-flag :textpalette)
(emit-when-no-readonly (dw/remove-layout-flag :hide-palettes)
(dw/remove-layout-flag :textpalette)
(toggle-layout-flag :colorpalette)))}
:toggle-textpalette {:tooltip (ds/alt "T")
:command (ds/a-mod "t")
:subsections [:panels]
:fn #(do (r/set-resize-type! :bottom)
(emit-when-no-readonly (dw/remove-layout-flag :colorpalette)
(emit-when-no-readonly (dw/remove-layout-flag :hide-palettes)
(dw/remove-layout-flag :colorpalette)
(toggle-layout-flag :textpalette)))}
:hide-ui {:tooltip "\\"
@ -562,7 +564,9 @@
:command (ds/c-mod "alt+p")
:subsections [:basics]
:fn #(when (features/active-feature? @st/state "plugins/runtime")
(st/emit! (modal/show :plugin-management {})))}})
(st/emit!
(ptk/event ::ev/event {::ev/name "open-plugins-manager" ::ev/origin "workspace:shortcuts"})
(modal/show :plugin-management {})))}})
(def debug-shortcuts
;; PREVIEW

View file

@ -14,7 +14,7 @@
[app.common.svg.shapes-builder :as csvg.shapes-builder]
[app.common.types.shape-tree :as ctst]
[app.common.uuid :as uuid]
[app.main.data.workspace.changes :as dch]
[app.main.data.changes :as dch]
[app.main.data.workspace.selection :as dws]
[app.main.data.workspace.state-helpers :as wsh]
[app.main.data.workspace.undo :as dwu]
@ -64,7 +64,8 @@
([svg-data position]
(add-svg-shapes nil svg-data position nil))
([id svg-data position {:keys [change-selection?] :or {change-selection? false}}]
([id svg-data position {:keys [change-selection? ignore-selection?]
:or {ignore-selection? false change-selection? true}}]
(ptk/reify ::add-svg-shapes
ptk/WatchEvent
(watch [it state _]
@ -73,7 +74,7 @@
page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
frame-id (ctst/top-nested-frame objects position)
selected (wsh/lookup-selected state)
selected (if ignore-selection? #{} (wsh/lookup-selected state))
base (cfh/get-base-shape objects selected)
selected-id (first selected)

View file

@ -17,7 +17,6 @@
[app.common.types.modifiers :as ctm]
[app.common.uuid :as uuid]
[app.main.data.events :as ev]
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.modifiers :as dwm]
@ -93,7 +92,7 @@
(some? (:current-page-id state))
(some? shape))
(rx/of
(dch/update-shapes
(dwsh/update-shapes
[id]
(fn [shape]
(let [{:keys [width height position-data]} modifiers]
@ -206,6 +205,102 @@
;; --- TEXT EDITION IMPL
(defn count-node-chars
([node]
(count-node-chars node false))
([node last?]
(case (:type node)
("root" "paragraph-set")
(apply + (concat (map count-node-chars (drop-last (:children node)))
(map #(count-node-chars % true) (take-last 1 (:children node)))))
"paragraph"
(+ (apply + (map count-node-chars (:children node))) (if last? 0 1))
(count (:text node)))))
(defn decorate-range-info
"Adds information about ranges inside the metadata of the text nodes"
[content]
(->> (with-meta content {:start 0 :end (count-node-chars content)})
(txt/transform-nodes
(fn [node]
(d/update-when
node
:children
(fn [children]
(let [start (-> node meta (:start 0))]
(->> children
(reduce (fn [[result start] node]
(let [end (+ start (count-node-chars node))]
[(-> result
(conj (with-meta node {:start start :end end})))
end]))
[[] start])
(first)))))))))
(defn split-content-at
[content position]
(->> content
(txt/transform-nodes
(fn [node]
(and (txt/is-paragraph-node? node)
(< (-> node meta :start) position (-> node meta :end))))
(fn [node]
(letfn
[(process-node [child]
(let [start (-> child meta :start)
end (-> child meta :end)]
(if (< start position end)
[(-> child
(vary-meta assoc :end position)
(update :text subs 0 (- position start)))
(-> child
(vary-meta assoc :start position)
(update :text subs (- position start)))]
[child])))]
(-> node
(d/update-when :children #(into [] (mapcat process-node) %))))))))
(defn update-content-range
[content start end attrs]
(->> content
(txt/transform-nodes
(fn [node]
(and (txt/is-text-node? node)
(and (>= (-> node meta :start) start)
(<= (-> node meta :end) end))))
#(d/patch-object % attrs))))
(defn- update-text-range-attrs
[shape start end attrs]
(let [new-content (-> (:content shape)
(decorate-range-info)
(split-content-at start)
(split-content-at end)
(update-content-range start end attrs))]
(assoc shape :content new-content)))
(defn update-text-range
[id start end attrs]
(ptk/reify ::update-text-range
ptk/WatchEvent
(watch [_ state _]
(let [objects (wsh/lookup-page-objects state)
shape (get objects id)
update-fn
(fn [shape]
(cond-> shape
(cfh/text-shape? shape)
(update-text-range-attrs start end attrs)))
shape-ids (cond (cfh/text-shape? shape) [id]
(cfh/group-shape? shape) (cfh/get-children-ids objects id))]
(rx/of (dwsh/update-shapes shape-ids update-fn))))))
(defn- update-text-content
[shape pred-fn update-fn attrs]
(let [update-attrs-fn #(update-fn % attrs)
@ -230,7 +325,7 @@
shape-ids (cond (cfh/text-shape? shape) [id]
(cfh/group-shape? shape) (cfh/get-children-ids objects id))]
(rx/of (dch/update-shapes shape-ids update-fn))))))
(rx/of (dwsh/update-shapes shape-ids update-fn))))))
(defn update-paragraph-attrs
[{:keys [id attrs]}]
@ -257,7 +352,7 @@
(cfh/text-shape? shape) [id]
(cfh/group-shape? shape) (cfh/get-children-ids objects id))]
(rx/of (dch/update-shapes shape-ids update-fn))))))))
(rx/of (dwsh/update-shapes shape-ids update-fn))))))))
(defn update-text-attrs
[{:keys [id attrs]}]
@ -277,8 +372,7 @@
shape-ids (cond
(cfh/text-shape? shape) [id]
(cfh/group-shape? shape) (cfh/get-children-ids objects id))]
(rx/of (dch/update-shapes shape-ids #(update-text-content % update-node? d/txt-merge attrs))))))))
(rx/of (dwsh/update-shapes shape-ids #(update-text-content % update-node? d/txt-merge attrs))))))))
(defn migrate-node
[node]
@ -337,7 +431,7 @@
(dissoc :fills)
(d/update-when :content update-content)))]
(rx/of (dch/update-shapes shape-ids update-shape)))))))
(rx/of (dwsh/update-shapes shape-ids update-shape)))))))
;; --- RESIZE UTILS
@ -390,10 +484,9 @@
(let [ids (into #{} (filter changed-text?) (keys props))]
(rx/of (dwu/start-undo-transaction undo-id)
(dch/update-shapes ids update-fn {:reg-objects? true
:stack-undo? true
:ignore-remote? true
:ignore-touched true})
(dwsh/update-shapes ids update-fn {:reg-objects? true
:stack-undo? true
:ignore-touched true})
(ptk/data-event :layout/update {:ids ids})
(dwu/commit-undo-transaction undo-id))))))))
@ -532,12 +625,12 @@
(watch [_ state _]
(let [position-data (::update-position-data state)]
(rx/concat
(rx/of (dch/update-shapes
(rx/of (dwsh/update-shapes
(keys position-data)
(fn [shape]
(-> shape
(assoc :position-data (get position-data (:id shape)))))
{:stack-undo? true :reg-objects? false :ignore-remote? true}))
{:stack-undo? true :reg-objects? false}))
(rx/of (fn [state]
(dissoc state ::update-position-data-debounce ::update-position-data))))))))
@ -600,29 +693,32 @@
(rx/map #(update-attrs % attrs)))
(rx/of (dwu/commit-undo-transaction undo-id)))))))
(defn apply-typography
"A higher level event that has the resposability of to apply the
specified typography to the selected shapes."
[typography file-id]
(ptk/reify ::apply-typography
ptk/WatchEvent
(watch [_ state _]
(let [editor-state (:workspace-editor-state state)
selected (wsh/lookup-selected state)
attrs (-> typography
(assoc :typography-ref-file file-id)
(assoc :typography-ref-id (:id typography))
(dissoc :id :name))
undo-id (js/Symbol)]
([typography file-id]
(apply-typography nil typography file-id))
(rx/concat
(rx/of (dwu/start-undo-transaction undo-id))
(->> (rx/from (seq selected))
(rx/map (fn [id]
(let [editor (get editor-state id)]
(update-text-attrs {:id id :editor editor :attrs attrs})))))
(rx/of (dwu/commit-undo-transaction undo-id)))))))
([ids typography file-id]
(assert (or (nil? ids) (and (set? ids) (every? uuid? ids))))
(ptk/reify ::apply-typography
ptk/WatchEvent
(watch [_ state _]
(let [editor-state (:workspace-editor-state state)
ids (d/nilv ids (wsh/lookup-selected state))
attrs (-> typography
(assoc :typography-ref-file file-id)
(assoc :typography-ref-id (:id typography))
(dissoc :id :name))
undo-id (js/Symbol)]
(rx/concat
(rx/of (dwu/start-undo-transaction undo-id))
(->> (rx/from (seq ids))
(rx/map (fn [id]
(let [editor (get editor-state id)]
(update-text-attrs {:id id :editor editor :attrs attrs})))))
(rx/of (dwu/commit-undo-transaction undo-id))))))))
(defn generate-typography-name
[{:keys [font-id font-variant-id] :as typography}]
@ -677,4 +773,3 @@
(rx/of (update-attrs (:id shape)
{:typography-ref-id typ-id
:typography-ref-file file-id}))))))))

View file

@ -10,74 +10,55 @@
[app.common.files.helpers :as cfh]
[app.common.logging :as l]
[app.common.thumbnails :as thc]
[app.main.data.workspace.changes :as dch]
[app.common.uuid :as uuid]
[app.main.data.changes :as dch]
[app.main.data.persistence :as-alias dps]
[app.main.data.workspace.notifications :as-alias wnt]
[app.main.data.workspace.state-helpers :as wsh]
[app.main.rasterizer :as thr]
[app.main.refs :as refs]
[app.main.render :as render]
[app.main.repo :as rp]
[app.main.store :as st]
[app.util.http :as http]
[app.util.queue :as q]
[app.util.time :as tp]
[app.util.timers :as tm]
[app.util.webapi :as wapi]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[potok.v2.core :as ptk]))
(l/set-level! :info)
(l/set-level! :warn)
(declare update-thumbnail)
(defn- find-request
[params item]
(and (= (unchecked-get params "file-id")
(unchecked-get item "file-id"))
(= (unchecked-get params "page-id")
(unchecked-get item "page-id"))
(= (unchecked-get params "shape-id")
(unchecked-get item "shape-id"))
(= (unchecked-get params "tag")
(unchecked-get item "tag"))))
(defn resolve-request
"Resolves the request to generate a thumbnail for the given ids."
[item]
(let [file-id (unchecked-get item "file-id")
page-id (unchecked-get item "page-id")
shape-id (unchecked-get item "shape-id")
tag (unchecked-get item "tag")]
(st/emit! (update-thumbnail file-id page-id shape-id tag))))
(defn- create-request
"Creates a request to generate a thumbnail for the given ids."
[file-id page-id shape-id tag]
#js {:file-id file-id
:page-id page-id
:shape-id shape-id
:tag tag})
;; Defines the thumbnail queue
(defonce queue
(q/create resolve-request (/ 1000 30)))
(defn create-request
"Creates a request to generate a thumbnail for the given ids."
[file-id page-id shape-id tag]
#js {:file-id file-id :page-id page-id :shape-id shape-id :tag tag})
(defn find-request
"Returns true if the given item matches the given ids."
[file-id page-id shape-id tag item]
(and (= file-id (unchecked-get item "file-id"))
(= page-id (unchecked-get item "page-id"))
(= shape-id (unchecked-get item "shape-id"))
(= tag (unchecked-get item "tag"))))
(defn request-thumbnail
"Enqueues a request to generate a thumbnail for the given ids."
([file-id page-id shape-id tag]
(request-thumbnail file-id page-id shape-id tag "unknown"))
([file-id page-id shape-id tag requester]
(ptk/reify ::request-thumbnail
ptk/EffectEvent
(effect [_ _ _]
(l/dbg :hint "request thumbnail" :requester requester :file-id file-id :page-id page-id :shape-id shape-id :tag tag)
(q/enqueue-unique
queue
(create-request file-id page-id shape-id tag)
(partial find-request file-id page-id shape-id tag))))))
(q/create find-request (/ 1000 30)))
;; This function first renders the HTML calling `render/render-frame` that
;; returns HTML as a string, then we send that data to the iframe rasterizer
;; that returns the image as a Blob. Finally we create a URI for that blob.
(defn get-thumbnail
(defn- render-thumbnail
"Returns the thumbnail for the given ids"
[state file-id page-id frame-id tag & {:keys [object-id]}]
(let [object-id (or object-id (thc/fmt-object-id file-id page-id frame-id tag))
[state file-id page-id frame-id tag]
(let [object-id (thc/fmt-object-id file-id page-id frame-id tag)
tp (tp/tpoint-ms)
objects (wsh/lookup-objects state file-id page-id)
shape (get objects frame-id)]
@ -86,30 +67,47 @@
(rx/take 1)
(rx/filter some?)
(rx/mapcat thr/render)
(rx/map (fn [blob] (wapi/create-uri blob)))
(rx/tap #(l/dbg :hint "thumbnail rendered"
:elapsed (dm/str (tp) "ms"))))))
(defn- request-thumbnail
"Enqueues a request to generate a thumbnail for the given ids."
[state file-id page-id shape-id tag]
(let [request (create-request file-id page-id shape-id tag)]
(q/enqueue-unique queue request (partial render-thumbnail state file-id page-id shape-id tag))))
(defn clear-thumbnail
([file-id page-id frame-id tag]
(clear-thumbnail (thc/fmt-object-id file-id page-id frame-id tag)))
([object-id]
(let [emit-rpc? (volatile! false)]
(clear-thumbnail file-id (thc/fmt-object-id file-id page-id frame-id tag)))
([file-id object-id]
(let [pending (volatile! false)]
(ptk/reify ::clear-thumbnail
cljs.core/IDeref
(-deref [_] object-id)
ptk/UpdateEvent
(update [_ state]
(let [uri (dm/get-in state [:workspace-thumbnails object-id])]
(if (some? uri)
(do
(l/dbg :hint "clear thumbnail" :object-id object-id)
(vreset! emit-rpc? true)
(tm/schedule-on-idle (partial wapi/revoke-uri uri))
(update state :workspace-thumbnails dissoc object-id))
(update state :workspace-thumbnails
(fn [thumbs]
(if-let [uri (get thumbs object-id)]
(do (vreset! pending uri)
(dissoc thumbs object-id))
thumbs))))
state)))))))
ptk/WatchEvent
(watch [_ _ _]
(if-let [uri @pending]
(do
(l/trc :hint "clear-thumbnail" :uri uri)
(when (str/starts-with? uri "blob:")
(tm/schedule-on-idle (partial wapi/revoke-uri uri)))
(let [params {:file-id file-id
:object-id object-id}]
(->> (rp/cmd! :delete-file-object-thumbnail params)
(rx/catch rx/empty)
(rx/ignore))))
(rx/empty)))))))
(defn- assoc-thumbnail
[object-id uri]
@ -141,8 +139,7 @@
(defn update-thumbnail
"Updates the thumbnail information for the given `id`"
[file-id page-id frame-id tag]
[file-id page-id frame-id tag requester]
(let [object-id (thc/fmt-object-id file-id page-id frame-id tag)]
(ptk/reify ::update-thumbnail
cljs.core/IDeref
@ -150,38 +147,40 @@
ptk/WatchEvent
(watch [_ state stream]
(l/dbg :hint "update thumbnail" :object-id object-id :tag tag)
;; Send the update to the back-end
(->> (get-thumbnail state file-id page-id frame-id tag)
(rx/mapcat (fn [uri]
(rx/merge
(rx/of (assoc-thumbnail object-id uri))
(->> (http/send! {:uri uri :response-type :blob :method :get})
(rx/map :body)
(rx/mapcat (fn [blob]
;; Send the data to backend
(let [params {:file-id file-id
:object-id object-id
:media blob
:tag (or tag "frame")}]
(rp/cmd! :create-file-object-thumbnail params))))
(rx/catch rx/empty)
(rx/ignore)))))
(rx/catch (fn [cause]
(.error js/console cause)
(rx/empty)))
(l/dbg :hint "update thumbnail" :requester requester :object-id object-id :tag tag)
(let [tp (tp/tpoint-ms)]
;; Send the update to the back-end
(->> (request-thumbnail state file-id page-id frame-id tag)
(rx/mapcat (fn [blob]
(let [uri (wapi/create-uri blob)
params {:file-id file-id
:object-id object-id
:media blob
:tag (or tag "frame")}]
;; We cancel all the stream if user starts editing while
;; thumbnail is generating
(rx/take-until
(->> stream
(rx/filter (ptk/type? ::clear-thumbnail))
(rx/filter #(= (deref %) object-id)))))))))
(rx/merge
(rx/of (assoc-thumbnail object-id uri))
(->> (rp/cmd! :create-file-object-thumbnail params)
(rx/catch rx/empty)
(rx/ignore))))))
(rx/catch (fn [cause]
(.error js/console cause)
(rx/empty)))
(rx/tap #(l/trc :hint "thumbnail updated" :elapsed (dm/str (tp) "ms")))
;; We cancel all the stream if user starts editing while
;; thumbnail is generating
(rx/take-until
(->> stream
(rx/filter (ptk/type? ::clear-thumbnail))
(rx/filter #(= (deref %) object-id))))))))))
(defn- extract-root-frame-changes
"Process a changes set in a commit to extract the frames that are changing"
[page-id [event [old-data new-data]]]
(let [changes (-> event deref :changes)
(let [changes (:changes event)
extract-ids
(fn [{:keys [page-id type] :as change}]
@ -192,8 +191,8 @@
:mov-objects (->> (:shapes change) (map #(vector page-id %)))
[]))
get-frame-id
(fn [[_ id]]
get-frame-ids
(fn get-frame-ids [id]
(let [old-objects (wsh/lookup-data-objects old-data page-id)
new-objects (wsh/lookup-data-objects new-data page-id)
@ -208,12 +207,21 @@
(conj old-frame-id)
(cfh/root-frame? new-objects new-frame-id)
(conj new-frame-id))))]
(conj new-frame-id)
(and (uuid? (:frame-id old-shape))
(not= uuid/zero (:frame-id old-shape)))
(into (get-frame-ids (:frame-id old-shape)))
(and (uuid? (:frame-id new-shape))
(not= uuid/zero (:frame-id new-shape)))
(into (get-frame-ids (:frame-id new-shape))))))]
(into #{}
(comp (mapcat extract-ids)
(filter (fn [[page-id']] (= page-id page-id')))
(mapcat get-frame-id))
(map (fn [[_ id]] id))
(mapcat get-frame-ids))
changes)))
(defn watch-state-changes
@ -239,60 +247,36 @@
(rx/buffer 2 1)
(rx/share))
local-changes-s
;; All commits stream, indepentendly of the source of the commit
all-commits-s
(->> stream
(rx/filter dch/commit-changes?)
(rx/with-latest-from workspace-data-s)
(rx/merge-map (partial extract-root-frame-changes page-id))
(rx/tap #(l/trc :hint "incoming change" :origin "local" :frame-id (dm/str %))))
notification-changes-s
(->> stream
(rx/filter (ptk/type? ::wnt/handle-file-change))
(rx/filter dch/commit?)
(rx/map deref)
(rx/observe-on :async)
(rx/with-latest-from workspace-data-s)
(rx/merge-map (partial extract-root-frame-changes page-id))
(rx/tap #(l/trc :hint "incoming change" :origin "notifications" :frame-id (dm/str %))))
persistence-changes-s
(->> stream
(rx/filter (ptk/type? ::update))
(rx/map deref)
(rx/filter (fn [[file-id page-id]]
(and (= file-id file-id)
(= page-id page-id))))
(rx/map (fn [[_ _ frame-id]] frame-id))
(rx/tap #(l/trc :hint "incoming change" :origin "persistence" :frame-id (dm/str %))))
all-changes-s
(->> (rx/merge
;; LOCAL CHANGES
local-changes-s
;; NOTIFICATIONS CHANGES
notification-changes-s
;; PERSISTENCE CHANGES
persistence-changes-s)
(rx/tap #(l/trc :hint "inconming change" :origin "all" :frame-id (dm/str %)))
(rx/share))
;; BUFFER NOTIFIER (window of 5s of inactivity)
notifier-s
(->> all-changes-s
(rx/debounce 1000)
(->> stream
(rx/filter (ptk/type? ::dps/commit-persisted))
(rx/debounce 5000)
(rx/tap #(l/trc :hint "buffer initialized")))]
(->> (rx/merge
;; Perform instant thumbnail cleaning of affected frames
;; and interrupt any ongoing update-thumbnail process
;; related to current frame-id
(->> all-changes-s
(rx/map #(clear-thumbnail file-id page-id % "frame")))
(->> all-commits-s
(rx/map (fn [frame-id]
(clear-thumbnail file-id page-id frame-id "frame"))))
;; Generate thumbnails in batchs, once user becomes
;; inactive for some instant
(->> all-changes-s
;; Generate thumbnails in batches, once user becomes
;; inactive for some instant.
(->> all-commits-s
(rx/buffer-until notifier-s)
(rx/mapcat #(into #{} %))
(rx/map #(request-thumbnail file-id page-id % "frame" "watch-state-changes"))))
(rx/map #(update-thumbnail file-id page-id % "frame" "watch-state-changes"))))
(rx/take-until stopper-s))))))

View file

@ -25,7 +25,7 @@
[app.common.types.modifiers :as ctm]
[app.common.types.shape-tree :as ctst]
[app.common.types.shape.layout :as ctl]
[app.main.data.workspace.changes :as dch]
[app.main.data.changes :as dch]
[app.main.data.workspace.collapse :as dwc]
[app.main.data.workspace.modifiers :as dwm]
[app.main.data.workspace.selection :as dws]
@ -400,17 +400,18 @@
(defn increase-rotation
"Rotate shapes a fixed angle, from a keyboard action."
[ids rotation]
(ptk/reify ::increase-rotation
ptk/WatchEvent
(watch [_ state _]
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
shapes (->> ids (map #(get objects %)))]
(rx/concat
(rx/of (dwm/set-delta-rotation-modifiers rotation shapes))
(rx/of (dwm/apply-modifiers)))))))
([ids rotation]
(increase-rotation ids rotation nil))
([ids rotation params]
(ptk/reify ::increase-rotation
ptk/WatchEvent
(watch [_ state _]
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
shapes (->> ids (map #(get objects %)))]
(rx/concat
(rx/of (dwm/set-delta-rotation-modifiers rotation shapes params))
(rx/of (dwm/apply-modifiers))))))))
;; -- Move ----------------------------------------------------------
@ -431,7 +432,7 @@
(watch [_ state stream]
(let [initial (deref ms/mouse-position)
stopper (mse/drag-stopper stream)
stopper (mse/drag-stopper stream {:interrupt? false})
zoom (get-in state [:workspace-local :zoom] 1)
;; We toggle the selection so we don't have to wait for the event
@ -832,6 +833,30 @@
:ignore-constraints false
:ignore-snap-pixel true}))))))
(defn- cleanup-invalid-moving-shapes [ids objects frame-id]
(let [lookup (d/getf objects)
frame (get objects frame-id)
layout? (:layout frame)
shapes (->> ids
set
(cfh/clean-loops objects)
(keep lookup)
;;remove shapes inside copies, because we can't change the structure of copies
(remove #(ctk/in-component-copy? (get objects (:parent-id %))))
;; remove absolute shapes that won't change parent
(remove #(and (ctl/position-absolute? %) (= frame-id (:parent-id %)))))
shapes
(cond->> shapes
(not layout?)
(remove #(= (:frame-id %) frame-id))
layout?
(remove #(and (= (:frame-id %) frame-id)
(not= (:parent-id %) frame-id))))]
(map :id shapes)))
(defn move-shapes-to-frame
[ids frame-id drop-index cell]
(ptk/reify ::move-shapes-to-frame
@ -839,7 +864,14 @@
(watch [it state _]
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
changes (cls/generate-move-shapes-to-frame (pcb/empty-changes it) ids frame-id page-id objects drop-index cell)]
ids (cleanup-invalid-moving-shapes ids objects frame-id)
changes (cls/generate-relocate (pcb/empty-changes it)
objects
frame-id
page-id
drop-index
ids
:cell cell)]
(when (and (some? frame-id) (d/not-empty? changes))
(rx/of (dch/commit-changes changes)
@ -858,26 +890,32 @@
;; -- Flip ----------------------------------------------------------
(defn flip-horizontal-selected []
(ptk/reify ::flip-horizontal-selected
ptk/WatchEvent
(watch [_ state _]
(let [objects (wsh/lookup-page-objects state)
selected (wsh/lookup-selected state {:omit-blocked? true})
shapes (map #(get objects %) selected)
selrect (gsh/shapes->rect shapes)
center (grc/rect->center selrect)
modifiers (dwm/create-modif-tree selected (ctm/resize-modifiers (gpt/point -1.0 1.0) center))]
(rx/of (dwm/apply-modifiers {:modifiers modifiers :ignore-snap-pixel true}))))))
(defn flip-horizontal-selected
([]
(flip-horizontal-selected nil))
([ids]
(ptk/reify ::flip-horizontal-selected
ptk/WatchEvent
(watch [_ state _]
(let [objects (wsh/lookup-page-objects state)
selected (or ids (wsh/lookup-selected state {:omit-blocked? true}))
shapes (map #(get objects %) selected)
selrect (gsh/shapes->rect shapes)
center (grc/rect->center selrect)
modifiers (dwm/create-modif-tree selected (ctm/resize-modifiers (gpt/point -1.0 1.0) center))]
(rx/of (dwm/apply-modifiers {:modifiers modifiers :ignore-snap-pixel true})))))))
(defn flip-vertical-selected []
(ptk/reify ::flip-vertical-selected
ptk/WatchEvent
(watch [_ state _]
(let [objects (wsh/lookup-page-objects state)
selected (wsh/lookup-selected state {:omit-blocked? true})
shapes (map #(get objects %) selected)
selrect (gsh/shapes->rect shapes)
center (grc/rect->center selrect)
modifiers (dwm/create-modif-tree selected (ctm/resize-modifiers (gpt/point 1.0 -1.0) center))]
(rx/of (dwm/apply-modifiers {:modifiers modifiers :ignore-snap-pixel true}))))))
(defn flip-vertical-selected
([]
(flip-vertical-selected nil))
([ids]
(ptk/reify ::flip-vertical-selected
ptk/WatchEvent
(watch [_ state _]
(let [objects (wsh/lookup-page-objects state)
selected (or ids (wsh/lookup-selected state {:omit-blocked? true}))
shapes (map #(get objects %) selected)
selrect (gsh/shapes->rect shapes)
center (grc/rect->center selrect)
modifiers (dwm/create-modif-tree selected (ctm/resize-modifiers (gpt/point 1.0 -1.0) center))]
(rx/of (dwm/apply-modifiers {:modifiers modifiers :ignore-snap-pixel true})))))))

View file

@ -11,18 +11,18 @@
[app.common.files.changes :as cpc]
[app.common.logging :as log]
[app.common.schema :as sm]
[app.common.types.shape.layout :as ctl]
[app.main.data.changes :as dch]
[app.main.data.workspace.state-helpers :as wsh]
[app.util.router :as rt]
[app.util.time :as dt]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
(def discard-transaction-time-millis (* 20 1000))
;; Change this to :info :debug or :trace to debug this module
(log/set-level! :warn)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Undo / Redo
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def discard-transaction-time-millis (* 20 1000))
(def ^:private
schema:undo-entry
@ -44,7 +44,6 @@
(subvec undo (- cnt MAX-UNDO-SIZE))
undo)))
;; TODO: Review the necessity of this method
(defn materialize-undo
[_changes index]
(ptk/reify ::materialize-undo
@ -84,8 +83,7 @@
(-> state
(update-in [:workspace-undo :transaction :undo-changes] #(into undo-changes %))
(update-in [:workspace-undo :transaction :redo-changes] #(into % redo-changes))
(cond->
(nil? (get-in state [:workspace-undo :transaction :undo-group]))
(cond-> (nil? (get-in state [:workspace-undo :transaction :undo-group]))
(assoc-in [:workspace-undo :transaction :undo-group] undo-group))
(assoc-in [:workspace-undo :transaction :tags] tags)))
@ -182,3 +180,125 @@
(rx/tap #(js/console.warn (dm/str "FORCE COMMIT TRANSACTION AFTER " (second %) "MS")))
(rx/map first)
(rx/map commit-undo-transaction))))))
(defn undo-to-index
"Repeat undoing or redoing until dest-index is reached."
[dest-index]
(ptk/reify ::undo-to-index
ptk/WatchEvent
(watch [it state _]
(let [objects (wsh/lookup-page-objects state)
edition (get-in state [:workspace-local :edition])
drawing (get state :workspace-drawing)]
(when-not (and (or (some? edition) (some? (:object drawing)))
(not (ctl/grid-layout? objects edition)))
(let [undo (:workspace-undo state)
items (:items undo)
index (or (:index undo) (dec (count items)))]
(when (and (some? items)
(<= -1 dest-index (dec (count items))))
(let [changes (vec (apply concat
(cond
(< dest-index index)
(->> (subvec items (inc dest-index) (inc index))
(reverse)
(map :undo-changes))
(> dest-index index)
(->> (subvec items (inc index) (inc dest-index))
(map :redo-changes))
:else [])))]
(when (seq changes)
(rx/of (materialize-undo changes dest-index)
(dch/commit-changes {:redo-changes changes
:undo-changes []
:origin it
:save-undo? false})))))))))))
(declare ^:private assure-valid-current-page)
(def undo
(ptk/reify ::undo
ptk/WatchEvent
(watch [it state _]
(let [objects (wsh/lookup-page-objects state)
edition (get-in state [:workspace-local :edition])
drawing (get state :workspace-drawing)]
;; Editors handle their own undo's
(when (or (and (nil? edition) (nil? (:object drawing)))
(ctl/grid-layout? objects edition))
(let [undo (:workspace-undo state)
items (:items undo)
index (or (:index undo) (dec (count items)))]
(when-not (or (empty? items) (= index -1))
(let [item (get items index)
changes (:undo-changes item)
undo-group (:undo-group item)
find-first-group-idx
(fn [index]
(if (= (dm/get-in items [index :undo-group]) undo-group)
(recur (dec index))
(inc index)))
undo-group-index
(when undo-group
(find-first-group-idx index))]
(if undo-group
(rx/of (undo-to-index (dec undo-group-index)))
(rx/of (materialize-undo changes (dec index))
(dch/commit-changes {:redo-changes changes
:undo-changes []
:save-undo? false
:origin it})
(assure-valid-current-page)))))))))))
(def redo
(ptk/reify ::redo
ptk/WatchEvent
(watch [it state _]
(let [objects (wsh/lookup-page-objects state)
edition (get-in state [:workspace-local :edition])
drawing (get state :workspace-drawing)]
(when (and (or (nil? edition) (ctl/grid-layout? objects edition))
(or (empty? drawing) (= :curve (:tool drawing))))
(let [undo (:workspace-undo state)
items (:items undo)
index (or (:index undo) (dec (count items)))]
(when-not (or (empty? items) (= index (dec (count items))))
(let [item (get items (inc index))
changes (:redo-changes item)
undo-group (:undo-group item)
find-last-group-idx (fn flgidx [index]
(let [item (get items index)]
(if (= (:undo-group item) undo-group)
(flgidx (inc index))
(dec index))))
redo-group-index (when undo-group
(find-last-group-idx (inc index)))]
(if undo-group
(rx/of (undo-to-index redo-group-index))
(rx/of (materialize-undo changes (inc index))
(dch/commit-changes {:redo-changes changes
:undo-changes []
:origin it
:save-undo? false})))))))))))
(defn- assure-valid-current-page
[]
(ptk/reify ::assure-valid-current-page
ptk/WatchEvent
(watch [_ state _]
(let [current_page (:current-page-id state)
pages (get-in state [:workspace-data :pages])
exists? (some #(= current_page %) pages)
project-id (:current-project-id state)
file-id (:current-file-id state)
pparams {:file-id file-id :project-id project-id}
qparams {:page-id (first pages)}]
(if exists?
(rx/empty)
(rx/of (rt/nav :workspace pparams qparams)))))))

View file

@ -71,6 +71,13 @@
(defn get-font-data [id]
(get @fontsdb id))
(defn find-font-data [data]
(d/seek
(fn [font]
(= (select-keys font (keys data))
data))
(vals @fontsdb)))
(defn resolve-variants
[id]
(get-in @fontsdb [id :variants]))
@ -249,6 +256,11 @@
(or (d/seek #(= (:id %) font-variant-id) variants)
(get-default-variant font)))
(defn find-variant
[{:keys [variants] :as font} variant-data]
(let [props (keys variant-data)]
(d/seek #(= (select-keys % props) variant-data) variants)))
;; Font embedding functions
(defn get-node-fonts
"Extracts the fonts used by some node"

View file

@ -45,6 +45,9 @@
(def export
(l/derived :export st/state))
(def persistence
(l/derived :persistence st/state))
;; ---- Dashboard refs
(def dashboard-local

View file

@ -23,6 +23,7 @@
[app.common.geom.shapes.bounds :as gsb]
[app.common.logging :as l]
[app.common.math :as mth]
[app.common.types.components-list :as ctkl]
[app.common.types.file :as ctf]
[app.common.types.modifiers :as ctm]
[app.common.types.shape-tree :as ctst]
@ -149,7 +150,7 @@
svg-raw-wrapper (mf/use-memo (mf/deps objects) #(svg-raw-wrapper-factory objects))
bool-wrapper (mf/use-memo (mf/deps objects) #(bool-wrapper-factory objects))
frame-wrapper (mf/use-memo (mf/deps objects) #(frame-wrapper-factory objects))]
(when (and shape (not (:hidden shape)))
(when shape
(let [opts #js {:shape shape}
svg-raw? (= :svg-raw (:type shape))]
(if-not svg-raw?
@ -484,15 +485,18 @@
path (:path component)
root-id (or (:main-instance-id component)
(:id component))
orig-root (get (:objects component) root-id)
objects (adapt-objects-for-shape (:objects component)
root-id)
root-shape (get objects root-id)
selrect (:selrect root-shape)
main-instance-id (:main-instance-id component)
main-instance-page (:main-instance-page component)
main-instance-x (:main-instance-x component)
main-instance-y (:main-instance-y component)
main-instance-id (:main-instance-id component)
main-instance-page (:main-instance-page component)
main-instance-x (when (:deleted component) (:x orig-root))
main-instance-y (when (:deleted component) (:y orig-root))
main-instance-parent (when (:deleted component) (:parent-id orig-root))
main-instance-frame (when (:deleted component) (:frame-id orig-root))
vbox
(format-viewbox
@ -516,7 +520,9 @@
"penpot:main-instance-id" main-instance-id
"penpot:main-instance-page" main-instance-page
"penpot:main-instance-x" main-instance-x
"penpot:main-instance-y" main-instance-y}
"penpot:main-instance-y" main-instance-y
"penpot:main-instance-parent" main-instance-parent
"penpot:main-instance-frame" main-instance-frame}
[:title name]
[:> shape-container {:shape root-shape}
(case (:type root-shape)
@ -525,8 +531,10 @@
(mf/defc components-svg
{::mf/wrap-props false}
[{:keys [data children embed include-metadata source]}]
(let [source (keyword (d/nilv source "components"))]
[{:keys [data children embed include-metadata deleted?]}]
(let [components (if (not deleted?)
(ctkl/components-seq data)
(ctkl/deleted-components-seq data))]
[:& (mf/provider embed/context) {:value embed}
[:& (mf/provider export/include-metadata-ctx) {:value include-metadata}
[:svg {:version "1.1"
@ -536,9 +544,9 @@
:style {:display (when-not (some? children) "none")}
:fill "none"}
[:defs
(for [[id component] (source data)]
(for [component components]
(let [component (ctf/load-component-objects data component)]
[:& component-symbol {:key (dm/str id) :component component}]))]
[:& component-symbol {:key (dm/str (:id component)) :component component}]))]
children]]]))
@ -595,10 +603,12 @@
(rds/renderToStaticMarkup elem)))))))
(defn render-components
[data source]
[data deleted?]
(let [;; Join all components objects into a single map
objects (->> (source data)
(vals)
components (if (not deleted?)
(ctkl/components-seq data)
(ctkl/deleted-components-seq data))
objects (->> components
(map (partial ctf/load-component-objects data))
(map :objects)
(reduce conj))]
@ -615,7 +625,7 @@
#js {:data data
:embed true
:include-metadata true
:source (name source)})]
:deleted? deleted?})]
(rds/renderToStaticMarkup elem))))))))
(defn render-frame

View file

@ -10,6 +10,7 @@
[app.common.transit :as t]
[app.common.uri :as u]
[app.config :as cf]
[app.main.data.events :as-alias ev]
[app.util.http :as http]
[app.util.sse :as sse]
[beicon.v2.core :as rx]
@ -93,11 +94,12 @@
(= query-params :all) :get
(str/starts-with? nid "get-") :get
:else :post)
request {:method method
:uri (u/join cf/public-uri "api/rpc/command/" nid)
:credentials "include"
:headers {"accept" "application/transit+json,text/event-stream,*/*"}
:headers {"accept" "application/transit+json,text/event-stream,*/*"
"x-external-session-id" (cf/external-session-id)
"x-event-origin" (::ev/origin (meta params))}
:body (when (= method :post)
(if form-data?
(http/form-data params)
@ -136,6 +138,8 @@
(->> (http/send! {:method :post
:uri uri
:credentials "include"
:headers {"x-external-session-id" (cf/external-session-id)
"x-event-origin" (::ev/origin (meta params))}
:query params})
(rx/map http/conditional-decode-transit)
(rx/mapcat handle-response))))
@ -145,6 +149,8 @@
(->> (http/send! {:method :post
:uri (u/join cf/public-uri "api/export")
:body (http/transit-data (dissoc params :blob?))
:headers {"x-external-session-id" (cf/external-session-id)
"x-event-origin" (::ev/origin (meta params))}
:credentials "include"
:response-type (if blob? :blob :text)})
(rx/map http/conditional-decode-transit)
@ -164,6 +170,8 @@
(->> (http/send! {:method :post
:uri (u/join cf/public-uri "api/rpc/command/" (name id))
:credentials "include"
:headers {"x-external-session-id" (cf/external-session-id)
"x-event-origin" (::ev/origin (meta params))}
:body (http/form-data params)})
(rx/map http/conditional-decode-transit)
(rx/mapcat handle-response)))

View file

@ -34,8 +34,6 @@
(def debug-exclude-events
#{:app.main.data.workspace.notifications/handle-pointer-update
:app.main.data.workspace.notifications/handle-pointer-send
:app.main.data.workspace.persistence/update-persistence-status
:app.main.data.workspace.changes/update-indices
:app.main.data.websocket/send-message
:app.main.data.workspace.selection/change-hover-state})
@ -65,7 +63,7 @@
:app.util.router/assign-exception}]
(->> (rx/merge
(->> stream
(rx/filter (ptk/type? :app.main.data.workspace.changes/commit-changes))
(rx/filter (ptk/type? :app.main.data.changes/commit))
(rx/map #(-> % deref :hint-origin)))
(rx/map ptk/type stream))
(rx/filter #(not (contains? omitset %)))

View file

@ -97,7 +97,7 @@
(when cls
(cond
(true? v) cls
(false? v) nil
(false? v) ""
:else `(if ~v ~cls ""))))))
(interpose " ")))

View file

@ -10,12 +10,14 @@
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.context :as ctx]
[app.main.ui.cursors :as c]
[app.main.ui.debug.components-preview :as cm]
[app.main.ui.debug.icons-preview :refer [icons-preview]]
[app.main.ui.frame-preview :as frame-preview]
[app.main.ui.icons :as i]
[app.main.ui.messages :as msgs]
[app.main.ui.onboarding :refer [onboarding-modal]]
[app.main.ui.onboarding.newsletter :refer [onboarding-newsletter]]
[app.main.ui.onboarding.questions :refer [questions-modal]]
[app.main.ui.onboarding.team-choice :refer [onboarding-team-modal]]
[app.main.ui.releases :refer [release-notes-modal]]
[app.main.ui.static :as static]
[app.util.dom :as dom]
@ -74,11 +76,7 @@
:debug-icons-preview
(when *assert*
[:div.debug-preview
[:h1 "Cursors"]
[:& c/debug-preview]
[:h1 "Icons"]
[:& i/debug-icons-preview]])
[:& icons-preview])
(:dashboard-search
:dashboard-projects
@ -96,19 +94,43 @@
#_[:& app.main.ui.onboarding/onboarding-modal]
#_[:& app.main.ui.onboarding.team-choice/onboarding-team-modal]
(when-let [props (get profile :props)]
(cond
(and (not (:onboarding-viewed props))
(contains? cf/flags :onboarding))
[:& onboarding-modal {}]
(let [show-question-modal?
(and (contains? cf/flags :onboarding)
(not (:onboarding-viewed props))
(not (contains? props :onboarding-questions)))
(and (contains? cf/flags :onboarding)
(:onboarding-viewed props)
(not= (:release-notes-viewed props) (:main cf/version))
(not= "0.0" (:main cf/version)))
[:& release-notes-modal {:version (:main cf/version)}]))
show-newsletter-modal?
(and (contains? cf/flags :onboarding)
(not (:onboarding-viewed props))
(not (contains? props :newsletter-updates))
(contains? props :onboarding-questions))
show-team-modal?
(and (contains? cf/flags :onboarding)
(not (:onboarding-viewed props))
(not (contains? props :onboarding-team-id))
(contains? props :newsletter-updates))
show-release-modal?
(and (contains? cf/flags :onboarding)
(:onboarding-viewed props)
(not= (:release-notes-viewed props) (:main cf/version))
(not= "0.0" (:main cf/version)))]
(cond
show-question-modal?
[:& questions-modal]
show-newsletter-modal?
[:& onboarding-newsletter]
show-team-modal?
[:& onboarding-team-modal]
show-release-modal?
[:& release-notes-modal {:version (:main cf/version)}])))
[:& dashboard-page {:route route :profile profile}]]
:viewer
(let [{:keys [query-params path-params]} route
{:keys [index share-id section page-id interactions-mode frame-id]

View file

@ -44,6 +44,9 @@
{::mf/props :obj}
[{:keys [route]}]
(let [section (dm/get-in route [:data :name])
show-login-icon (and
(not= section :auth-register-validate)
(not= section :auth-register-success))
params (:query-params route)
error (:error params)]
@ -55,8 +58,9 @@
(st/emit! (du/show-redirect-error error))))
[:main {:class (stl/css :auth-section)}
[:h1 {:class (stl/css :logo-container)}
[:a {:href "#/" :title "Penpot" :class (stl/css :logo-btn)} i/logo]]
(when show-login-icon
[:h1 {:class (stl/css :logo-container)}
[:a {:href "#/" :title "Penpot" :class (stl/css :logo-btn)} i/logo]])
[:div {:class (stl/css :login-illustration)}
i/login-illustration]

View file

@ -31,6 +31,7 @@
display: flex;
justify-content: flex-start;
width: $s-120;
height: $s-96;
margin-block-end: $s-52;
}
@ -43,7 +44,7 @@
svg {
width: 100%;
fill: $df-primary;
fill: var(--color-foreground-primary);
height: auto;
}

View file

@ -10,14 +10,22 @@
width: 100%;
padding-block-end: 0;
display: grid;
gap: $s-24;
gap: $s-12;
form {
display: flex;
flex-direction: column;
gap: $s-12;
margin-top: $s-12;
}
}
.auth-title-wrapper {
width: 100%;
padding-block-end: 0;
display: grid;
gap: $s-8;
}
.separator {
border-color: var(--modal-separator-backogrund-color);
margin: 0;

View file

@ -7,9 +7,8 @@
(ns app.main.ui.auth.login
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.logging :as log]
[app.common.spec :as us]
[app.common.schema :as sm]
[app.config :as cf]
[app.main.data.messages :as msg]
[app.main.data.users :as du]
@ -25,7 +24,6 @@
[app.util.keyboard :as k]
[app.util.router :as rt]
[beicon.v2.core :as rx]
[cljs.spec.alpha :as s]
[rumext.v2 :as mf]))
(def show-alt-login-buttons?
@ -64,28 +62,18 @@
:else
(st/emit! (msg/error (tr "errors.generic"))))))))
(s/def ::email ::us/email)
(s/def ::password ::us/not-empty-string)
(s/def ::invitation-token ::us/not-empty-string)
(s/def ::login-form
(s/keys :req-un [::email ::password]
:opt-un [::invitation-token]))
(defn handle-error-messages
[errors _data]
(d/update-when errors :email
(fn [{:keys [code] :as error}]
(cond-> error
(= code ::us/email)
(assoc :message (tr "errors.email-invalid"))))))
(def ^:private schema:login-form
[:map {:title "LoginForm"}
[:email [::sm/email {:error/code "errors.invalid-email"}]]
[:password [:string {:min 1}]]
[:invitation-token {:optional true}
[:string {:min 1}]]])
(mf/defc login-form
[{:keys [params on-success-callback origin] :as props}]
(let [initial (mf/use-memo (mf/deps params) (constantly params))
(let [initial (mf/with-memo [params] params)
error (mf/use-state false)
form (fm/use-form :spec ::login-form
:validators [handle-error-messages]
form (fm/use-form :schema schema:login-form
:initial initial)
on-error
@ -100,7 +88,6 @@
(= :ldap-not-initialized (:code cause)))
(st/emit! (msg/error (tr "errors.ldap-disabled")))
(and (= :restriction (:type cause))
(= :admin-only-profile (:code cause)))
(reset! error (tr "errors.profile-blocked"))
@ -160,7 +147,7 @@
[:& context-notification
{:type :error
:content message
:data-test "login-banner"
:data-testid "login-banner"
:role "alert"}])
[:& fm/form {:on-submit on-submit
@ -170,7 +157,7 @@
[:& fm/input
{:name :email
:type "email"
:label (tr "auth.email")
:label (tr "auth.work-email")
:class (stl/css :form-field)}]]
[:div {:class (stl/css :fields-row)}
@ -186,7 +173,7 @@
[:div {:class (stl/css :fields-row :forgot-password)}
[:& lk/link {:action on-recovery-request
:class (stl/css :forgot-pass-link)
:data-test "forgot-password"}
:data-testid "forgot-password"}
(tr "auth.forgot-password")]])
[:div {:class (stl/css :buttons-stack)}
@ -194,7 +181,7 @@
(contains? cf/flags :login-with-password))
[:> fm/submit-button*
{:label (tr "auth.login-submit")
:data-test "login-submit"
:data-testid "login-submit"
:class (stl/css :login-button)}])
(when (contains? cf/flags :login-with-ldap)
@ -280,7 +267,7 @@
[:div {:class (stl/css :auth-form-wrapper)}
[:h1 {:class (stl/css :auth-title)
:data-test "login-title"} (tr "auth.login-account-title")]
:data-testid "login-title"} (tr "auth.login-account-title")]
[:p {:class (stl/css :auth-tagline)}
(tr "auth.login-tagline")]
@ -299,14 +286,5 @@
(tr "auth.register") " "]
[:& lk/link {:action go-register
:class (stl/css :register-link)
:data-test "register-submit"}
(tr "auth.register-submit")]])
(when (contains? cf/flags :demo-users)
[:div {:class (stl/css :demo-account)}
[:span {:class (stl/css :demo-account-text)}
(tr "auth.create-demo-profile") " "]
[:& lk/link {:action create-demo-profile
:class (stl/css :demo-account-link)
:data-test "demo-account-link"}
(tr "auth.create-demo-account")]])]]))
:data-testid "register-submit"}
(tr "auth.register-submit")]])]]))

View file

@ -7,39 +7,29 @@
(ns app.main.ui.auth.recovery
(:require-macros [app.main.style :as stl])
(:require
[app.common.spec :as us]
[app.common.schema :as sm]
[app.main.data.messages :as msg]
[app.main.data.users :as du]
[app.main.store :as st]
[app.main.ui.components.forms :as fm]
[app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
[cljs.spec.alpha :as s]
[rumext.v2 :as mf]))
(s/def ::password-1 ::us/not-empty-string)
(s/def ::password-2 ::us/not-empty-string)
(s/def ::token ::us/not-empty-string)
(s/def ::recovery-form
(s/keys :req-un [::password-1
::password-2]))
(defn- password-equality
[errors data]
(let [password-1 (:password-1 data)
password-2 (:password-2 data)]
(cond-> errors
(and password-1 password-2
(not= password-1 password-2))
(assoc :password-2 {:message "errors.password-invalid-confirmation"})
(and password-1 (> 8 (count password-1)))
(assoc :password-1 {:message "errors.password-too-short"}))))
(def ^:private schema:recovery-form
[:and
[:map {:title "RecoveryForm"}
[:token ::sm/text]
[:password-1 ::sm/password]
[:password-2 ::sm/password]]
[:fn {:error/code "errors.password-invalid-confirmation"
:error/field :password-2}
(fn [{:keys [password-1 password-2]}]
(= password-1 password-2))]])
(defn- on-error
[_form _error]
(st/emit! (msg/error (tr "auth.notifications.invalid-token-error"))))
(st/emit! (msg/error (tr "errors.invalid-recovery-token"))))
(defn- on-success
[_]
@ -56,14 +46,13 @@
(mf/defc recovery-form
[{:keys [params] :as props}]
(let [form (fm/use-form :spec ::recovery-form
:validators [password-equality
(fm/validate-not-empty :password-1 (tr "auth.password-not-empty"))
(fm/validate-not-empty :password-2 (tr "auth.password-not-empty"))]
(let [form (fm/use-form :schema schema:recovery-form
:initial params)]
[:& fm/form {:on-submit on-submit
:class (stl/css :recovery-form)
:form form}
[:div {:class (stl/css :fields-row)}
[:& fm/input {:type "password"
:name :password-1

View file

@ -7,8 +7,7 @@
(ns app.main.ui.auth.recovery-request
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.spec :as us]
[app.common.schema :as sm]
[app.main.data.messages :as msg]
[app.main.data.users :as du]
[app.main.store :as st]
@ -17,30 +16,24 @@
[app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
[beicon.v2.core :as rx]
[cljs.spec.alpha :as s]
[rumext.v2 :as mf]))
(s/def ::email ::us/email)
(s/def ::recovery-request-form (s/keys :req-un [::email]))
(defn handle-error-messages
[errors _data]
(d/update-when errors :email
(fn [{:keys [code] :as error}]
(cond-> error
(= code :missing)
(assoc :message (tr "errors.email-invalid"))))))
(def ^:private schema:recovery-request-form
[:map {:title "RecoverRequestForm"}
[:email ::sm/email]])
(mf/defc recovery-form
[{:keys [on-success-callback] :as props}]
(let [form (fm/use-form :spec ::recovery-request-form
:validators [handle-error-messages]
(let [form (fm/use-form :schema schema:recovery-request-form
:initial {})
submitted (mf/use-state false)
default-success-finish #(st/emit! (msg/info (tr "auth.notifications.recovery-token-sent")))
default-success-finish
(mf/use-fn
#(st/emit! (msg/info (tr "auth.notifications.recovery-token-sent"))))
on-success
(mf/use-callback
(mf/use-fn
(fn [cdata _]
(reset! submitted false)
(if (nil? on-success-callback)
@ -48,7 +41,7 @@
(on-success-callback (:email cdata)))))
on-error
(mf/use-callback
(mf/use-fn
(fn [data cause]
(reset! submitted false)
(let [code (-> cause ex-data :code)]
@ -59,13 +52,14 @@
:profile-is-muted
(rx/of (msg/error (tr "errors.profile-is-muted")))
:email-has-permanent-bounces
(:email-has-permanent-bounces
:email-has-complaints)
(rx/of (msg/error (tr "errors.email-has-permanent-bounces" (:email data))))
(rx/throw cause)))))
on-submit
(mf/use-callback
(mf/use-fn
(fn []
(reset! submitted true)
(let [cdata (:clean-data @form)
@ -80,13 +74,13 @@
:form form}
[:div {:class (stl/css :fields-row)}
[:& fm/input {:name :email
:label (tr "auth.email")
:label (tr "auth.work-email")
:type "text"
:class (stl/css :form-field)}]]
[:> fm/submit-button*
{:label (tr "auth.recovery-request-submit")
:data-test "recovery-resquest-submit"
:data-testid "recovery-resquest-submit"
:class (stl/css :recover-btn)}]]))
@ -106,5 +100,5 @@
[:div {:class (stl/css :go-back)}
[:& lk/link {:action go-back
:class (stl/css :go-back-link)
:data-test "go-back-link"}
:data-testid "go-back-link"}
(tr "labels.go-back")]]]))

View file

@ -7,8 +7,7 @@
(ns app.main.ui.auth.register
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.spec :as us]
[app.common.schema :as sm]
[app.config :as cf]
[app.main.data.messages :as msg]
[app.main.data.users :as du]
@ -18,67 +17,52 @@
[app.main.ui.components.forms :as fm]
[app.main.ui.components.link :as lk]
[app.main.ui.icons :as i]
[app.util.i18n :refer [tr tr-html]]
[app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
[app.util.storage :as sto]
[beicon.v2.core :as rx]
[cljs.spec.alpha :as s]
[rumext.v2 :as mf]))
;; --- PAGE: Register
(defn- validate-password-length
[errors data]
(let [password (:password data)]
(cond-> errors
(> 8 (count password))
(assoc :password {:message "errors.password-too-short"}))))
(defn- validate-email
[errors _]
(d/update-when errors :email
(fn [{:keys [code] :as error}]
(cond-> error
(= code ::us/email)
(assoc :message (tr "errors.email-invalid"))))))
(s/def ::fullname ::us/not-empty-string)
(s/def ::password ::us/not-empty-string)
(s/def ::email ::us/email)
(s/def ::invitation-token ::us/not-empty-string)
(s/def ::terms-privacy ::us/boolean)
(s/def ::register-form
(s/keys :req-un [::password ::email]
:opt-un [::invitation-token]))
(defn- on-prepare-register-error
[form cause]
(let [{:keys [type code]} (ex-data cause)]
(condp = [type code]
[:restriction :registration-disabled]
(st/emit! (msg/error (tr "errors.registration-disabled")))
[:validation :email-as-password]
(swap! form assoc-in [:errors :password]
{:message "errors.email-as-password"})
(st/emit! (msg/error (tr "errors.generic"))))))
(defn- on-prepare-register-success
[params]
(st/emit! (rt/nav :auth-register-validate {} params)))
(def ^:private schema:register-form
[:map {:title "RegisterForm"}
[:password ::sm/password]
[:email ::sm/email]
[:invitation-token {:optional true} ::sm/text]])
(mf/defc register-form
{::mf/props :obj}
[{:keys [params on-success-callback]}]
(let [initial (mf/use-memo (mf/deps params) (constantly params))
form (fm/use-form :spec ::register-form
:validators [validate-password-length
validate-email
(fm/validate-not-empty :password (tr "auth.password-not-empty"))]
form (fm/use-form :schema schema:register-form
:initial initial)
submitted? (mf/use-state false)
on-error
(mf/use-fn
(fn [form cause]
(let [{:keys [type code] :as edata} (ex-data cause)]
(condp = [type code]
[:restriction :registration-disabled]
(st/emit! (msg/error (tr "errors.registration-disabled")))
[:restriction :email-domain-is-not-allowed]
(st/emit! (msg/error (tr "errors.email-domain-not-allowed")))
[:restriction :email-has-permanent-bounces]
(st/emit! (msg/error (tr "errors.email-has-permanent-bounces" (:email edata))))
[:restriction :email-has-complaints]
(st/emit! (msg/error (tr "errors.email-has-permanent-bounces" (:email edata))))
[:validation :email-as-password]
(swap! form assoc-in [:errors :password]
{:code "errors.email-as-password"})
(st/emit! (msg/error (tr "errors.generic")))))))
on-submit
(mf/use-fn
(mf/deps on-success-callback)
@ -86,23 +70,21 @@
(reset! submitted? true)
(let [cdata (:clean-data @form)
on-success (fn [data]
(if (nil? on-success-callback)
(on-prepare-register-success data)
(on-success-callback data)))
on-error (fn [data]
(on-prepare-register-error form data))]
(if (fn? on-success-callback)
(on-success-callback data)
(st/emit! (rt/nav :auth-register-validate {} data))))]
(->> (rp/cmd! :prepare-register-profile cdata)
(rx/map #(merge % params))
(rx/finalize #(reset! submitted? false))
(rx/subs! on-success on-error)))))]
(rx/subs! on-success (partial on-error form))))))]
[:& fm/form {:on-submit on-submit :form form}
[:div {:class (stl/css :fields-row)}
[:& fm/input {:type "text"
:name :email
:label (tr "auth.email")
:data-test "email-input"
:label (tr "auth.work-email")
:data-testid "email-input"
:show-success? true
:class (stl/css :form-field)}]]
[:div {:class (stl/css :fields-row)}
@ -116,7 +98,7 @@
[:> fm/submit-button*
{:label (tr "auth.register-submit")
:disabled @submitted?
:data-test "register-form-submit"
:data-testid "register-form-submit"
:class (stl/css :register-btn)}]]))
(mf/defc register-methods
@ -131,11 +113,11 @@
(mf/defc register-page
{::mf/props :obj}
[{:keys [params]}]
[:div {:class (stl/css :auth-form-wrapper)}
[:div {:class (stl/css :auth-form-wrapper :register-form)}
[:h1 {:class (stl/css :auth-title)
:data-test "registration-title"} (tr "auth.register-title")]
:data-testid "registration-title"} (tr "auth.register-title")]
[:p {:class (stl/css :auth-tagline)}
(tr "auth.login-tagline")]
(tr "auth.register-tagline")]
(when (contains? cf/flags :demo-warning)
[:& login/demo-warning])
@ -147,7 +129,7 @@
[:span {:class (stl/css :account-text)} (tr "auth.already-have-account") " "]
[:& lk/link {:action #(st/emit! (rt/nav :auth-login {} params))
:class (stl/css :account-link)
:data-test "login-here-link"}
:data-testid "login-here-link"}
(tr "auth.login-here")]]
(when (contains? cf/flags :demo-users)
@ -160,60 +142,78 @@
;; --- PAGE: register validation
(defn- handle-register-error
[_form _data]
(st/emit! (msg/error (tr "errors.generic"))))
(mf/defc terms-and-privacy
{::mf/props :obj
::mf/private true}
[]
(let [terms-label
(mf/html
[:> i18n/tr-html*
{:tag-name "div"
:content (tr "auth.terms-and-privacy-agreement"
cf/terms-of-service-uri
cf/privacy-policy-uri)}])]
(defn- handle-register-success
[data]
(cond
(some? (:invitation-token data))
(let [token (:invitation-token data)]
(st/emit! (rt/nav :auth-verify-token {} {:token token})))
[:div {:class (stl/css :fields-row :input-visible :accept-terms-and-privacy-wrapper)}
[:& fm/input {:name :accept-terms-and-privacy
:class (stl/css :checkbox-terms-and-privacy)
:type "checkbox"
:default-checked false
:label terms-label}]]))
(:is-active data)
(st/emit! (du/login-from-register))
:else
(st/emit! (rt/nav :auth-register-success {} {:email (:email data)}))))
(s/def ::accept-terms-and-privacy (s/and ::us/boolean true?))
(s/def ::accept-newsletter-subscription ::us/boolean)
(if (contains? cf/flags :terms-and-privacy-checkbox)
(s/def ::register-validate-form
(s/keys :req-un [::token ::fullname ::accept-terms-and-privacy]
:opt-un [::accept-newsletter-subscription]))
(s/def ::register-validate-form
(s/keys :req-un [::token ::fullname]
:opt-un [::accept-terms-and-privacy
::accept-newsletter-subscription])))
(def ^:private schema:register-validate-form
[:map {:title "RegisterValidateForm"}
[:token ::sm/text]
[:fullname [::sm/text {:max 250}]]
[:accept-terms-and-privacy {:optional (not (contains? cf/flags :terms-and-privacy-checkbox))}
[:and :boolean [:= true]]]])
(mf/defc register-validate-form
{::mf/props :obj
::mf/private true}
[{:keys [params on-success-callback]}]
(let [form (fm/use-form :spec ::register-validate-form
:validators [(fm/validate-not-empty :fullname (tr "auth.name.not-all-space"))
(fm/validate-length :fullname fm/max-length-allowed (tr "auth.name.too-long"))]
:initial params)
(let [form (fm/use-form :schema schema:register-validate-form :initial params)
submitted? (mf/use-state false)
on-success (fn [p]
(if (nil? on-success-callback)
(handle-register-success p)
(on-success-callback (:email p))))
on-success
(mf/use-fn
(mf/deps on-success-callback)
(fn [params]
(if (fn? on-success-callback)
(on-success-callback (:email params))
(cond
(some? (:invitation-token params))
(let [token (:invitation-token params)]
(st/emit! (rt/nav :auth-verify-token {} {:token token})))
(:is-active params)
(st/emit! (du/login-from-register))
:else
(do
(swap! sto/storage assoc ::email (:email params))
(st/emit! (rt/nav :auth-register-success)))))))
on-error
(mf/use-fn
(fn [_]
(st/emit! (msg/error (tr "errors.generic")))))
on-submit
(mf/use-fn
(fn [form _event]
(mf/deps on-success on-error)
(fn [form _]
(reset! submitted? true)
(let [params (:clean-data @form)]
(->> (rp/cmd! :register-profile params)
(rx/finalize #(reset! submitted? false))
(rx/subs! on-success
(partial handle-register-error form))))))]
(rx/subs! on-success on-error)))))]
[:& fm/form {:on-submit on-submit :form form
[:& fm/form {:on-submit on-submit
:form form
:class (stl/css :register-validate-form)}
[:div {:class (stl/css :fields-row)}
[:& fm/input {:name :fullname
:label (tr "auth.fullname")
@ -222,18 +222,7 @@
:class (stl/css :form-field)}]]
(when (contains? cf/flags :terms-and-privacy-checkbox)
(let [terms-label
(mf/html
[:& tr-html
{:tag-name "div"
:label "auth.terms-privacy-agreement-md"
:params [cf/terms-of-service-uri cf/privacy-policy-uri]}])]
[:div {:class (stl/css :fields-row :input-visible :accept-terms-and-privacy-wrapper)}
[:& fm/input {:name :accept-terms-and-privacy
:class "check-primary"
:type "checkbox"
:default-checked false
:label terms-label}]]))
[:& terms-and-privacy])
[:> fm/submit-button*
{:label (tr "auth.register-submit")
@ -242,13 +231,15 @@
(mf/defc register-validate-page
{::mf/props :obj}
[{:keys [params]}]
[:div {:class (stl/css :auth-form-wrapper)}
[:h1 {:class (stl/css :auth-title)
:data-test "register-title"} (tr "auth.register-title")]
[:div {:class (stl/css :auth-subtitle)} (tr "auth.register-subtitle")]
[:hr {:class (stl/css :separator)}]
[:h1 {:class (stl/css :logo-container)}
[:a {:href "#/" :title "Penpot" :class (stl/css :logo-btn)} i/logo]]
[:div {:class (stl/css :auth-title-wrapper)}
[:h2 {:class (stl/css :auth-title)
:data-testid "register-title"} (tr "auth.register-account-title")]
[:div {:class (stl/css :auth-subtitle)} (tr "auth.register-account-tagline")]]
[:& register-validate-form {:params params}]
@ -259,9 +250,15 @@
(tr "labels.go-back")]]]])
(mf/defc register-success-page
[{:keys [params]}]
[:div {:class (stl/css :auth-form-wrapper :register-success)}
[:div {:class (stl/css :notification-icon)} i/icon-verify]
[:div {:class (stl/css :notification-text)} (tr "auth.verification-email-sent")]
[:div {:class (stl/css :notification-text-email)} (:email params "")]
[:div {:class (stl/css :notification-text)} (tr "auth.check-your-email")]])
{::mf/props :obj}
[]
(let [email (::email @sto/storage)]
[:div {:class (stl/css :auth-form-wrapper :register-success)}
[:h1 {:class (stl/css :logo-container)}
[:a {:href "#/" :title "Penpot" :class (stl/css :logo-btn)} i/logo]]
[:div {:class (stl/css :auth-title-wrapper)}
[:h2 {:class (stl/css :auth-title)}
(tr "auth.check-mail")]
[:div {:class (stl/css :notification-text)} (tr "auth.verification-email-sent")]]
[:div {:class (stl/css :notification-text-email)} email]
[:div {:class (stl/css :notification-text)} (tr "auth.check-your-email")]]))

View file

@ -8,15 +8,24 @@
@use "./common.scss";
.accept-terms-and-privacy-wrapper {
margin: $s-16 0;
:global(a) {
color: $df-secondary;
color: var(--color-foreground-secondary);
font-weight: $fw700;
}
}
.checkbox-terms-and-privacy {
align-items: flex-start;
}
.register-form {
gap: $s-24;
}
.register-success {
padding-bottom: $s-32;
gap: $s-24;
.auth-title {
@include medTitleTipography;
}
}
.notification-icon {
@ -30,9 +39,30 @@
}
}
.notification-text-email,
.notification-text {
font-size: $fs-16;
color: var(--notification-foreground-color-default);
margin-bottom: $s-16;
@include bodyMediumTypography;
color: var(--title-foreground-color);
}
.notification-text-email {
@include medTitleTipography;
font-size: $fs-20;
color: var(--register-confirmation-color);
margin-inline: $s-36;
}
.logo-btn {
height: $s-40;
svg {
width: $s-120;
height: $s-40;
fill: var(--main-icon-foreground);
}
}
.logo-container {
display: flex;
justify-content: flex-start;
width: $s-120;
margin-block-end: $s-24;
}

View file

@ -5,13 +5,12 @@
;; Copyright (c) KALEIDOS INC
(ns app.main.ui.auth.verify-token
(:require-macros [app.main.style :as stl])
(:require
[app.main.data.messages :as msg]
[app.main.data.users :as du]
[app.main.repo :as rp]
[app.main.store :as st]
[app.main.ui.icons :as i]
[app.main.ui.ds.product.loader :refer [loader*]]
[app.main.ui.static :as static]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
@ -70,29 +69,30 @@
(rx/subs!
(fn [tdata]
(handle-token tdata))
(fn [{:keys [type code] :as error}]
(cond
(or (= :validation type)
(= :invalid-token code)
(= :token-expired (:reason error)))
(reset! bad-token true)
(fn [cause]
(let [{:keys [type code] :as error} (ex-data cause)]
(cond
(or (= :validation type)
(= :invalid-token code)
(= :token-expired (:reason error)))
(reset! bad-token true)
(= :email-already-exists code)
(let [msg (tr "errors.email-already-exists")]
(ts/schedule 100 #(st/emit! (msg/error msg)))
(st/emit! (rt/nav :auth-login)))
(= :email-already-exists code)
(let [msg (tr "errors.email-already-exists")]
(ts/schedule 100 #(st/emit! (msg/error msg)))
(st/emit! (rt/nav :auth-login)))
(= :email-already-validated code)
(let [msg (tr "errors.email-already-validated")]
(ts/schedule 100 #(st/emit! (msg/warn msg)))
(st/emit! (rt/nav :auth-login)))
(= :email-already-validated code)
(let [msg (tr "errors.email-already-validated")]
(ts/schedule 100 #(st/emit! (msg/warn msg)))
(st/emit! (rt/nav :auth-login)))
:else
(let [msg (tr "errors.generic")]
(ts/schedule 100 #(st/emit! (msg/error msg)))
(st/emit! (rt/nav :auth-login))))))))
:else
(let [msg (tr "errors.generic")]
(ts/schedule 100 #(st/emit! (msg/error msg)))
(st/emit! (rt/nav :auth-login)))))))))
(if @bad-token
[:> static/invalid-token {}]
[:div {:class (stl/css :verify-token)}
i/loader-pencil])))
[:> loader* {:title (tr "labels.loading")
:overlay true}])))

View file

@ -17,7 +17,6 @@
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.components.forms :as fm]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
@ -96,7 +95,7 @@
(let [show-buttons? (mf/use-state false)
content (mf/use-state "")
disabled? (or (fm/all-spaces? @content)
disabled? (or (str/blank? @content)
(str/empty-or-nil? @content))
on-focus
@ -155,7 +154,7 @@
pos-x (* (:x position) zoom)
pos-y (* (:y position) zoom)
disabled? (or (fm/all-spaces? content)
disabled? (or (str/blank? content)
(str/empty-or-nil? content))
on-esc
@ -181,6 +180,7 @@
[:*
[:div
{:class (stl/css :floating-thread-bubble)
:data-testid "floating-thread-bubble"
:style {:top (str pos-y "px")
:left (str pos-x "px")}
:on-click dom/stop-propagation}
@ -224,7 +224,7 @@
(mf/deps @content)
(fn [] (on-submit @content)))
disabled? (or (fm/all-spaces? @content)
disabled? (or (str/blank? @content)
(str/empty-or-nil? @content))]
[:div {:class (stl/css :edit-form)}
@ -435,9 +435,9 @@
[:* {:key (dm/str (:id item))}
[:& comment-item {:comment item
:users users
:origin origin}]])
[:div {:ref ref}]]
[:& reply-form {:thread thread}]])))
:origin origin}]])]
[:& reply-form {:thread thread}]
[:div {:ref ref}]])))
(defn use-buble
[zoom {:keys [position frame-id]}]
@ -558,6 +558,7 @@
:on-pointer-move on-pointer-move*
:on-click on-click*
:on-lost-pointer-capture on-lost-pointer-capture
:data-testid "floating-thread-bubble"
:class (stl/css-case
:floating-thread-bubble true
:resolved (:is-resolved thread)

View file

@ -142,11 +142,10 @@
// thread-content
.thread-content {
position: absolute;
overflow-y: scroll;
scrollbar-gutter: stable;
overflow-y: auto;
width: $s-284;
padding: $s-12;
padding-inline-end: 0;
padding-inline-end: $s-8;
pointer-events: auto;
user-select: text;
@ -236,6 +235,7 @@
.reply-form {
textarea {
@extend .input-element;
@include bodySmallTypography;
line-height: 1.45;
height: 100%;
width: 100%;

View file

@ -1,20 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.main.ui.components
(:require
[app.main.ui.components.buttons.simple-button :as sb]
[rumext.v2 :as mf]))
(mf/defc story-wrapper
{::mf/wrap-props false}
[{:keys [children]}]
[:.default children])
(def default
"A export used for storybook"
#js {:SimpleButton sb/simple-button
:StoryWrapper story-wrapper})

View file

@ -5,7 +5,9 @@
;; Copyright (c) KALEIDOS INC
(ns app.main.ui.components.button-link
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.util.keyboard :as kbd]
[rumext.v2 :as mf]))
@ -18,8 +20,8 @@
(when (kbd/enter? event)
(when (fn? on-click)
(on-click event)))))]
[:a.btn-primary.btn-large.button-link
{:class class
[:a
{:class (dm/str class " " (stl/css :button))
:tab-index "0"
:on-click on-click
:on-key-down on-key-down}

View file

@ -0,0 +1,28 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) KALEIDOS INC
@import "refactor/common-refactor.scss";
.button {
appearance: none;
align-items: center;
border: none;
cursor: pointer;
display: flex;
font-family: "worksans", "vazirmatn", sans-serif;
justify-content: center;
min-width: 25px;
padding: 0 1rem;
transition: all 0.4s;
text-decoration: none !important;
height: 40px;
svg {
height: 20px;
width: 20px;
}
}

View file

@ -1,10 +0,0 @@
(ns app.main.ui.components.buttons.simple-button
(:require-macros [app.main.style :as stl])
(:require
[rumext.v2 :as mf]))
(mf/defc simple-button
{::mf/wrap-props false}
[{:keys [on-click children]}]
[:button {:on-click on-click :class (stl/css :button)} children])

View file

@ -1,16 +0,0 @@
import { Canvas, Meta } from '@storybook/blocks';
import * as SimpleButtonStories from "./simple_button.stories"
<Meta of={SimpleButtonStories} />
# Lorem ipsum
This is an example of **markdown** docs within storybook, for the component `<SimpleButton>`.
Here's how we can render a simple button:
<Canvas of={SimpleButtonStories.Default} />
Simple buttons can also have **icons**:
<Canvas of={SimpleButtonStories.WithIcon} />

View file

@ -1,13 +0,0 @@
.button {
font-family: monospace;
display: flex;
align-items: center;
column-gap: 0.5rem;
svg {
width: 16px;
height: 16px;
stroke: #000;
}
}

View file

@ -1,30 +0,0 @@
import * as React from "react";
import Components from "@target/components";
import Icons from "@target/icons";
export default {
title: 'Buttons/Simple Button',
component: Components.SimpleButton,
};
export const Default = {
render: () => (
<Components.StoryWrapper>
<Components.SimpleButton>
Simple Button
</Components.SimpleButton>
</Components.StoryWrapper>
),
};
export const WithIcon = {
render: () => (
<Components.StoryWrapper>
<Components.SimpleButton>
{Icons.AddRefactor}
Simple Button
</Components.SimpleButton>
</Components.StoryWrapper>
),
}

View file

@ -1,10 +0,0 @@
import { expect, test } from 'vitest'
test('use jsdom in this test file', () => {
const element = document.createElement('div')
expect(element).not.toBeNull()
})
test('adds 1 + 2 to equal 3', () => {
expect(1 +2).toBe(3)
});

View file

@ -44,6 +44,10 @@
(some? image)
(tr "media.image")))))
(defn- breakable-color-title
[title]
(str/replace title "." ".\u200B"))
(mf/defc color-bullet
{::mf/wrap [mf/memo]
::mf/wrap-props false}
@ -112,4 +116,4 @@
:title name
:on-click on-click
:on-double-click on-double-click}
(or name color (uc/gradient-type->string (:type gradient)))])))
(breakable-color-title (or name color (uc/gradient-type->string (:type gradient))))])))

View file

@ -86,8 +86,8 @@
.big-text {
@include inspectValue;
@include twoLineTextEllipsis;
line-height: 1;
color: var(--palette-text-color);
height: $s-28;
text-align: center;
}

View file

@ -39,7 +39,7 @@
id (gobj/get props "id")
klass (gobj/get props "class")
key-index (gobj/get props "key-index")
data-test (gobj/get props "data-test")]
data-testid (gobj/get props "data-testid")]
[:li {:id id
:class klass
:tab-index "0"
@ -47,7 +47,7 @@
:on-click on-click
:key key-index
:role "menuitem"
:data-test data-test}
:data-testid data-testid}
children]))
(mf/defc context-menu-a11y'
@ -230,7 +230,7 @@
id (:id option)
sub-options (:sub-options option)
option-handler (:option-handler option)
data-test (:data-test option)]
data-testid (:data-testid option)]
(when option-name
(if (= option-name :separator)
[:li {:key (dm/str "context-item-" index)
@ -240,7 +240,7 @@
:key id
:class (stl/css-case
:is-selected (and selected (= option-name selected))
:selected (and selected (= data-test selected))
:selected (and selected (= data-testid selected))
:context-menu-item true)
:key-index (dm/str "context-item-" index)
:tab-index "0"
@ -251,18 +251,18 @@
:on-click #(do (dom/stop-propagation %)
(on-close)
(option-handler %))
:data-test data-test}
:data-testid data-testid}
(if (and in-dashboard? (= option-name "Default"))
(tr "dashboard.default-team-name")
option-name)
(when (and selected (= data-test selected))
(when (and selected (= data-testid selected))
[:span {:class (stl/css :selected-icon)} i/tick])]
[:a {:class (stl/css :context-menu-action :submenu)
:data-no-close true
:on-click (enter-submenu option-name sub-options)
:data-test data-test}
:data-testid data-testid}
option-name
[:span {:class (stl/css :submenu-icon)} i/arrow]])]))))])])])))

View file

@ -12,7 +12,7 @@
(mf/defc file-uploader
{::mf/forward-ref true}
[{:keys [accept multi label-text label-class input-id on-selected data-test] :as props} input-ref]
[{:keys [accept multi label-text label-class input-id on-selected data-testid] :as props} input-ref]
(let [opt-pick-one #(if multi % (first %))
on-files-selected
@ -38,6 +38,6 @@
:type "file"
:ref input-ref
:on-change on-files-selected
:data-test data-test
:data-testid data-testid
:aria-label "uploader"}]]))

View file

@ -18,7 +18,6 @@
[app.util.keyboard :as kbd]
[app.util.object :as obj]
[cljs.core :as c]
[clojure.string]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
@ -26,7 +25,9 @@
(def use-form fm/use-form)
(mf/defc input
[{:keys [label help-icon disabled form hint trim children data-test on-change-value placeholder show-success?] :as props}]
[{:keys [label help-icon disabled form hint trim children data-testid on-change-value placeholder show-success? show-error]
:or {show-error true}
:as props}]
(let [input-type (get props :type "text")
input-name (get props :name)
more-classes (get props :class)
@ -101,7 +102,7 @@
(cond-> (and value is-checkbox?) (assoc :default-checked value))
(cond-> (and touched? (:message error)) (assoc "aria-invalid" "true"
"aria-describedby" (dm/str "error-" input-name)))
(obj/clj->props))
(obj/map->obj obj/prop-key-fn))
checked? (and is-checkbox? (= value true))
show-valid? (and show-success? touched? (not error))
@ -117,7 +118,7 @@
[:*
(cond
(some? label)
[:label {:class (stl/css-case :input-with-label (not is-checkbox?)
[:label {:class (stl/css-case :input-with-label-form (not is-checkbox?)
:input-label is-text?
:radio-label is-radio?
:checkbox-label is-checkbox?)
@ -152,11 +153,14 @@
children])
(cond
(and touched? (:message error))
[:div {:id (dm/str "error-" input-name)
:class (stl/css :error)
:data-test (clojure.string/join [data-test "-error"])}
(tr (:message error))]
(and touched? (:code error) show-error)
(let [code (:code error)]
[:div {:id (dm/str "error-" input-name)
:class (stl/css :error)
:data-testid (dm/str data-testid "-error")}
(if (vector? code)
(tr (nth code 0) (i18n/c (nth code 1)))
(tr code))])
(string? hint)
[:div {:class (stl/css :hint)} hint])]]))
@ -201,20 +205,20 @@
:on-blur on-blur
;; :placeholder label
:on-change on-change)
(obj/clj->props))]
(obj/map->obj obj/prop-key-fn))]
[:div {:class (dm/str klass " " (stl/css :textarea-wrapper))}
[:label {:class (stl/css :textarea-label)} label]
[:> :textarea props]
(cond
(and touched? (:message error))
[:span {:class (stl/css :error)} (tr (:message error))]
(and touched? (:code error))
[:span {:class (stl/css :error)} (tr (:code error))]
(string? hint)
[:span {:class (stl/css :hint)} hint])]))
(mf/defc select
[{:keys [options disabled form default dropdown-class] :as props
[{:keys [options disabled form default dropdown-class select-class] :as props
:or {default ""}}]
(let [input-name (get props :name)
form (or form (mf/use-ctx form-ctx))
@ -230,6 +234,7 @@
{:default-value value
:disabled disabled
:options options
:class select-class
:dropdown-class dropdown-class
:on-change handle-change}]]))
@ -297,6 +302,71 @@
:value value'
:checked checked?}]]))]))
(mf/defc image-radio-buttons
{::mf/wrap-props false}
[props]
(let [form (or (unchecked-get props "form")
(mf/use-ctx form-ctx))
name (unchecked-get props "name")
image (unchecked-get props "image")
img-height (unchecked-get props "img-height")
img-width (unchecked-get props "img-width")
current-value (or (dm/get-in @form [:data name] "")
(unchecked-get props "value"))
on-change (unchecked-get props "on-change")
options (unchecked-get props "options")
trim? (unchecked-get props "trim")
class (unchecked-get props "class")
encode-fn (d/nilv (unchecked-get props "encode-fn") identity)
decode-fn (d/nilv (unchecked-get props "decode-fn") identity)
on-change'
(mf/use-fn
(mf/deps on-change form name)
(fn [event]
(let [value (-> event dom/get-target dom/get-value decode-fn)]
(when (some? form)
(swap! form assoc-in [:touched name] true)
(fm/on-input-change form name value trim?))
(when (fn? on-change)
(on-change name value)))))]
[:div {:class (if image
class
(dm/str class " " (stl/css :custom-radio)))}
(for [{:keys [image icon value label area]} options]
(let [icon? (some? icon)
value' (encode-fn value)
checked? (= value current-value)
key (str/ffmt "%-%" (d/name name) (d/name value'))]
[:label {:for key
:key key
:style {:grid-area area}
:class (stl/css-case :radio-label-image true
:global/checked checked?)}
(cond
icon?
[:span {:class (stl/css :icon-inside)
:style {:height img-height
:width img-width}} icon]
:else
[:span {:style {:background-image (str/ffmt "url(%)" image)
:height img-height
:width img-width}
:class (stl/css :image-inside)}])
[:span {:class (stl/css :image-text)} label]
[:input {:on-change on-change'
:type "radio"
:class (stl/css :radio-input)
:id key
:name name
:value value'
:checked checked?}]]))]))
(mf/defc submit-button*
{::mf/wrap-props false}
[{:keys [on-click children label form class name disabled] :as props}]
@ -378,6 +448,7 @@
:no-padding (pos? (count @items))
:invalid (and (some? valid-item-fn)
touched?
(not (str/empty? @value))
(not (valid-item-fn @value)))))
on-focus
@ -483,41 +554,3 @@
[:span {:class (stl/css :text)} (:text item)]
[:button {:class (stl/css :icon)
:on-click #(remove-item! item)} i/close]]])])]))
;; --- Validators
(defn all-spaces?
[value]
(let [trimmed (str/trim value)]
(str/empty? trimmed)))
(def max-length-allowed 250)
(def max-uri-length-allowed 2048)
(defn max-length?
[value length]
(> (count value) length))
(defn validate-length
[field length errors-msg]
(fn [errors data]
(cond-> errors
(max-length? (get data field) length)
(assoc field {:message errors-msg}))))
(defn validate-not-empty
[field error-msg]
(fn [errors data]
(cond-> errors
(all-spaces? (get data field))
(assoc field {:message error-msg}))))
(defn validate-not-all-spaces
[field error-msg]
(fn [errors data]
(let [value (get data field)]
(cond-> errors
(and
(all-spaces? value)
(> (count value) 0))
(assoc field {:message error-msg})))))

View file

@ -38,10 +38,9 @@
}
}
.input-with-label {
.input-with-label-form {
@include flexColumn;
gap: $s-8;
@include bodySmallTypography;
justify-content: flex-start;
align-items: flex-start;
height: 100%;
@ -55,6 +54,7 @@
color: var(--input-foreground-color-active);
margin-top: 0;
width: 100%;
max-width: 100%;
height: 100%;
padding: 0 $s-8;
@ -64,6 +64,7 @@
border-radius: $br-8;
}
}
// Input autofill
input:-webkit-autofill,
input:-webkit-autofill:hover,
@ -92,7 +93,7 @@
top: calc(50% - $s-8);
svg {
@extend .button-icon-small;
stroke: $df-secondary;
stroke: var(--color-foreground-secondary);
width: $s-16;
height: $s-16;
}
@ -169,6 +170,10 @@
border-color: var(--input-checkbox-border-color-hover);
}
}
a {
// Need for terms and conditions links on register checkbox
color: var(--link-foreground-color);
}
}
}
@ -259,11 +264,10 @@
// SUBMIT-BUTTON
.button-submit {
@extend .button-primary;
}
:disabled {
@extend .button-disabled;
min-height: $s-32;
&:disabled {
@extend .button-disabled;
min-height: $s-32;
}
}
// MULTI INPUT
@ -368,7 +372,7 @@
height: fit-content;
border-radius: $br-8;
padding: $s-8;
color: var(--input-foreground-color);
color: var(--input-foreground-color-rest);
border: $s-1 solid transparent;
&:focus,
&:focus-within {
@ -394,14 +398,12 @@
border-radius: $br-circle;
}
.radio-label.with-image {
.radio-label-image {
@include smallTitleTipography;
display: grid;
grid-template-rows: auto auto 0px;
justify-items: center;
gap: 0;
height: $s-116;
width: $s-92;
border-radius: $br-8;
margin: 0;
border: 1px solid var(--color-background-tertiary);
@ -414,22 +416,29 @@
outline: none;
border: $s-1 solid var(--input-border-color-active);
}
.image-text {
color: var(--input-foreground-color-rest);
display: grid;
align-self: center;
margin-bottom: $s-16;
padding-inline: $s-8;
text-align: center;
}
}
.image-inside {
width: $s-60;
height: $s-48;
background-size: $s-48;
margin: $s-16;
background-size: 100%;
background-repeat: no-repeat;
background-position: center;
}
.icon-inside {
width: $s-60;
height: $s-48;
margin: $s-16;
@include flexCenter;
svg {
width: $s-60;
height: $s-48;
width: 40px;
height: 60px;
stroke: var(--icon-foreground);
fill: none;
}

View file

@ -12,7 +12,7 @@
(mf/defc link
{::mf/wrap-props false}
[{:keys [action class data-test keyboard-action children data-testid]}]
[{:keys [action class data-testid keyboard-action children]}]
(let [keyboard-action (d/nilv keyboard-action action)]
[:a {:on-click action
:class class
@ -20,6 +20,5 @@
(when ^boolean (kbd/enter? event)
(keyboard-action event)))
:tab-index "0"
:data-testid data-testid
:data-test data-test}
:data-testid data-testid}
children]))

View file

@ -11,7 +11,7 @@
(mf/defc link-button
{::mf/wrap-props false}
[{:keys [on-click class value data-test]}]
[{:keys [on-click class value data-testid]}]
(let [on-key-down (mf/use-fn
(mf/deps on-click)
(fn [event]
@ -24,4 +24,4 @@
:tab-index "0"
:on-click on-click
:on-key-down on-key-down
:data-test data-test}]))
:data-testid data-testid}]))

View file

@ -36,7 +36,7 @@
title (unchecked-get props "title")
default (unchecked-get props "default")
nillable? (unchecked-get props "nillable")
class (d/nilv (unchecked-get props "className") "input-text")
class (d/nilv (unchecked-get props "className") "")
min-value (d/parse-double min-value)
max-value (d/parse-double max-value)

View file

@ -54,11 +54,11 @@
:name name
:disabled disabled
:value value
:checked checked?}]]))
:default-checked checked?}]]))
(mf/defc radio-buttons
{::mf/props :obj}
[{:keys [children on-change selected class wide encode-fn decode-fn allow-empty] :as props}]
[{:keys [name children on-change selected class wide encode-fn decode-fn allow-empty] :as props}]
(let [encode-fn (d/nilv encode-fn identity)
decode-fn (d/nilv decode-fn identity)
nitems (if (array? children)
@ -94,5 +94,6 @@
[:& (mf/provider context) {:value context-value}
[:div {:class (dm/str class " " (stl/css :radio-btn-wrapper))
:style {:width width}}
:style {:width width}
:key (dm/str name "-" selected)}
children]]))

View file

@ -59,6 +59,7 @@
[:div {:key (str/concat "tab-" sid)
:title tooltip
:data-id sid
:data-testid sid
:on-click on-click
:class (stl/css-case
:tab-container-tab-title true

View file

@ -11,7 +11,7 @@
[app.main.store :as st]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr t]]
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as k]
[goog.events :as events]
[rumext.v2 :as mf])
@ -30,15 +30,13 @@
cancel-label
accept-label
accept-style] :as props}]
(let [locale (mf/deref i18n/locale)
on-accept (or on-accept identity)
(let [on-accept (or on-accept identity)
on-cancel (or on-cancel identity)
message (or message (t locale "ds.confirm-title"))
message (or message (tr "ds.confirm-title"))
cancel-label (or cancel-label (tr "ds.confirm-cancel"))
accept-label (or accept-label (tr "ds.confirm-ok"))
accept-style (or accept-style :danger)
title (or title (t locale "ds.confirm-title"))
title (or title (tr "ds.confirm-title"))
accept-fn
(mf/use-callback

View file

@ -5,11 +5,7 @@
;; Copyright (c) KALEIDOS INC
(ns app.main.ui.cursors
(:require-macros [app.main.ui.cursors :refer [cursor-ref cursor-fn collect-cursors]])
(:require
[app.util.timers :as ts]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
(:require-macros [app.main.ui.cursors :refer [cursor-ref cursor-fn collect-cursors]]))
;; Static cursors
(def ^:cursor comments (cursor-ref :comments 0 2 20))
@ -53,28 +49,3 @@
(def default
"A collection of all icons"
(collect-cursors))
(mf/defc debug-preview
{::mf/wrap-props false}
[]
(let [rotation (mf/use-state 0)
entries (->> (seq (js/Object.entries default))
(sort-by first))]
(mf/with-effect []
(ts/interval 100 #(reset! rotation inc)))
[:section.debug-icons-preview
(for [[key value] entries]
(let [value (if (fn? value) (value @rotation) value)]
[:div.cursor-item {:key key}
[:div {:style {:width "100px"
:height "100px"
:background-image (-> value (str/replace #"(url\(.*\)).*" "$1"))
:background-size "contain"
:background-repeat "no-repeat"
:background-position "center"
:cursor value}}]
[:span {:style {:white-space "nowrap"
:margin-right "1rem"}} (pr-str key)]]))]))

View file

@ -13,11 +13,6 @@
grid-template-columns: $s-40 $s-256 1fr;
grid-template-rows: $s-52 1fr;
height: 100vh;
:global(svg#loader-pencil) {
fill: $df-secondary;
width: $s-32;
}
}
.dashboard-content {

View file

@ -7,25 +7,24 @@
(ns app.main.ui.dashboard.change-owner
(:require-macros [app.main.style :as stl])
(:require
[app.common.spec :as us]
[app.common.schema :as sm]
[app.main.data.modal :as modal]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.forms :as fm]
[app.main.ui.icons :as i]
[app.util.i18n :as i18n :refer [tr]]
[cljs.spec.alpha :as s]
[rumext.v2 :as mf]))
(s/def ::member-id ::us/uuid)
(s/def ::leave-modal-form
(s/keys :req-un [::member-id]))
(def ^:private schema:leave-modal-form
[:map {:title "LeaveModalForm"}
[:member-id ::sm/uuid]])
(mf/defc leave-and-reassign-modal
{::mf/register modal/components
::mf/register-as :leave-and-reassign}
[{:keys [profile team accept]}]
(let [form (fm/use-form :spec ::leave-modal-form :initial {})
(let [form (fm/use-form :schema schema:leave-modal-form :initial {})
members-map (mf/deref refs/dashboard-team-members)
members (vals members-map)

View file

@ -34,6 +34,7 @@
.input-wrapper {
@extend .input-with-label;
@include bodySmallTypography;
}
.action-buttons {

View file

@ -54,7 +54,7 @@
[:button {:tab-index "0"
:on-click on-show-comments
:on-key-down handle-keydown
:data-test "open-comments"
:data-testid "open-comments"
:class (stl/css-case :comment-button true
:open show?
:unread (boolean (seq tgroups)))}

View file

@ -32,7 +32,7 @@
font-size: $fs-12;
padding: $s-24;
text-align: center;
color: $df-secondary;
color: var(--color-foreground-secondary);
}
.comments-icon {
@ -57,7 +57,7 @@
}
&:hover {
background-color: $db-quaternary;
background-color: var(--color-background-quaternary);
--comment-icon-small-foreground-color: var(--icon-foreground-active);
}
}
@ -69,7 +69,7 @@
.dropdown {
@include menuShadow;
background-color: $db-tertiary;
background-color: var(--color-background-tertiary);
border-radius: $br-8;
border: $s-1 solid transparent;
bottom: $s-4;
@ -82,7 +82,7 @@
hr {
margin: 0;
border-color: $df-secondary;
border-color: var(--color-foreground-secondary);
}
}
@ -94,7 +94,7 @@
}
.header-title {
color: $df-secondary;
color: var(--color-foreground-secondary);
font-size: $fs-11;
line-height: 1.28;
flex-grow: 1;

View file

@ -240,12 +240,12 @@
[{:option-name (tr "dashboard.duplicate-multi" file-count)
:id "file-duplicate-multi"
:option-handler on-duplicate
:data-test "duplicate-multi"}
:data-testid "duplicate-multi"}
(when (or (seq current-projects) (seq other-teams))
{:option-name (tr "dashboard.move-to-multi" file-count)
:id "file-move-multi"
:sub-options sub-options
:data-test "move-to-multi"})
:data-testid "move-to-multi"})
{:option-name (tr "dashboard.export-binary-multi" file-count)
:id "file-binari-export-multi"
:option-handler on-export-binary-files}
@ -256,13 +256,13 @@
{:option-name (tr "labels.unpublish-multi-files" file-count)
:id "file-unpublish-multi"
:option-handler on-del-shared
:data-test "file-del-shared"})
:data-testid "file-del-shared"})
(when (not is-lib-page?)
{:option-name :separator}
{:option-name (tr "labels.delete-multi-files" file-count)
:id "file-delete-multi"
:option-handler on-delete
:data-test "delete-multi-files"})]
:data-testid "delete-multi-files"})]
[{:option-name (tr "dashboard.open-in-new-tab")
:id "file-open-new-tab"
@ -271,42 +271,42 @@
{:option-name (tr "labels.rename")
:id "file-rename"
:option-handler on-edit
:data-test "file-rename"})
:data-testid "file-rename"})
(when (not is-search-page?)
{:option-name (tr "dashboard.duplicate")
:id "file-duplicate"
:option-handler on-duplicate
:data-test "file-duplicate"})
:data-testid "file-duplicate"})
(when (and (not is-lib-page?) (not is-search-page?) (or (seq current-projects) (seq other-teams)))
{:option-name (tr "dashboard.move-to")
:id "file-move-to"
:sub-options sub-options
:data-test "file-move-to"})
:data-testid "file-move-to"})
(when (not is-search-page?)
(if (:is-shared file)
{:option-name (tr "dashboard.unpublish-shared")
:id "file-del-shared"
:option-handler on-del-shared
:data-test "file-del-shared"}
:data-testid "file-del-shared"}
{:option-name (tr "dashboard.add-shared")
:id "file-add-shared"
:option-handler on-add-shared
:data-test "file-add-shared"}))
:data-testid "file-add-shared"}))
{:option-name :separator}
{:option-name (tr "dashboard.download-binary-file")
:id "file-download-binary"
:option-handler on-export-binary-files
:data-test "download-binary-file"}
:data-testid "download-binary-file"}
{:option-name (tr "dashboard.download-standard-file")
:id "file-download-standard"
:option-handler on-export-standard-files
:data-test "download-standard-file"}
:data-testid "download-standard-file"}
(when (and (not is-lib-page?) (not is-search-page?))
{:option-name :separator}
{:option-name (tr "labels.delete")
:id "file-delete"
:option-handler on-delete
:data-test "file-delete"})])]
:data-testid "file-delete"})])]
[:& context-menu-a11y {:on-close on-menu-close
:show show?

View file

@ -66,7 +66,7 @@
(dd/clear-selected-files))))]
[:header {:class (stl/css :dashboard-header)}
[:header {:class (stl/css :dashboard-header) :data-testid "dashboard-header"}
(if (:is-default project)
[:div#dashboard-drafts-title {:class (stl/css :dashboard-title)}
[:h1 (tr "labels.drafts")]]
@ -82,7 +82,7 @@
(swap! local assoc :edition false)))}]
[:div {:class (stl/css :dashboard-title)}
[:h1 {:on-double-click on-edit
:data-test "project-title"
:data-testid "project-title"
:id (:id project)}
(:name project)]]))
@ -98,7 +98,7 @@
[:a {:class (stl/css :btn-secondary :btn-small :new-file)
:tab-index "0"
:on-click on-create-click
:data-test "new-file"
:data-testid "new-file"
:on-key-down (fn [event]
(when (kbd/enter? event)
(on-create-click event)))}

View file

@ -12,7 +12,7 @@
margin-right: $s-16;
overflow-y: auto;
width: 100%;
border-top: $s-1 solid $db-quaternary;
border-top: $s-1 solid var(--color-background-quaternary);
&.dashboard-projects {
user-select: none;

View file

@ -47,7 +47,7 @@
::mf/private true}
[{:keys [section team]}]
(use-page-title team section)
[:header {:class (stl/css :dashboard-header)}
[:header {:class (stl/css :dashboard-header) :data-testid "dashboard-header"}
[:div#dashboard-fonts-title {:class (stl/css :dashboard-title)}
[:h1 (tr "labels.fonts")]]])
@ -167,7 +167,7 @@
[:div {:class (stl/css :dashboard-fonts-hero)}
[:div {:class (stl/css :desc)}
[:h2 (tr "labels.upload-custom-fonts")]
[:& i18n/tr-html {:label "dashboard.fonts.hero-text1"}]
[:> i18n/tr-html* {:content (tr "dashboard.fonts.hero-text1")}]
[:button {:class (stl/css :btn-primary)
:on-click on-click
@ -197,12 +197,12 @@
:btn-primary true
:disabled disable-upload-all?)
:on-click on-upload-all
:data-test "upload-all"
:data-testid "upload-all"
:disabled disable-upload-all?}
[:span (tr "dashboard.fonts.upload-all")]]
[:button {:class (stl/css :btn-secondary)
:on-click on-dismis-all
:data-test "dismiss-all"}
:data-testid "dismiss-all"}
[:span (tr "dashboard.fonts.dismiss-all")]]]])
(for [{:keys [id] :as item} (sort-by :font-family font-vals)]

View file

@ -8,7 +8,7 @@
@use "common/refactor/common-dashboard";
.dashboard-fonts {
border-top: $s-1 solid $db-quaternary;
border-top: $s-1 solid var(--color-background-quaternary);
display: flex;
flex-direction: column;
padding-left: $s-120;
@ -31,18 +31,18 @@
h3 {
font-size: $fs-14;
color: $df-secondary;
color: var(--color-foreground-secondary);
margin: $s-4;
}
.font-item {
color: $db-secondary;
color: var(--color-background-secondary);
}
}
.installed-fonts-header {
align-items: center;
color: $df-secondary;
color: var(--color-foreground-secondary);
display: flex;
font-size: $fs-12;
height: $s-40;
@ -65,11 +65,11 @@
justify-content: flex-end;
input {
background-color: $db-tertiary;
background-color: var(--color-background-tertiary);
border-color: transparent;
border-radius: $br-8;
border: $s-1 solid transparent;
color: $df-primary;
color: var(--color-foreground-primary);
font-size: $fs-14;
height: $s-32;
margin: 0;
@ -77,19 +77,19 @@
width: $s-152;
&:focus {
outline: $s-1 solid $da-primary;
outline: $s-1 solid var(--color-accent-primary);
}
&::placeholder {
color: $df-secondary;
color: var(--color-foreground-secondary);
}
}
}
.font-item {
align-items: center;
background-color: $db-tertiary;
background-color: var(--color-background-tertiary);
border-radius: $br-4;
color: $df-secondary;
color: var(--color-foreground-secondary);
display: flex;
font-size: $fs-14;
justify-content: space-between;
@ -103,13 +103,13 @@
margin: 0;
padding: $s-8;
background-color: $db-tertiary;
background-color: var(--color-background-tertiary);
border-radius: $br-8;
color: $df-primary;
color: var(--color-foreground-primary);
font-size: $fs-14;
&:focus {
outline: $s-1 solid $da-primary;
outline: $s-1 solid var(--color-accent-primary);
}
}
@ -152,16 +152,16 @@
&:hover {
.icon svg {
stroke: $df-secondary;
stroke: var(--color-foreground-secondary);
}
}
}
}
.table-field {
color: $df-primary;
color: var(--color-foreground-primary);
.variant {
background-color: $db-quaternary;
background-color: var(--color-background-quaternary);
border-radius: $br-8;
margin-right: $s-4;
padding-right: $s-4;
@ -189,7 +189,7 @@
svg {
width: $s-16;
height: $s-16;
stroke: $df-secondary;
stroke: var(--color-foreground-secondary);
fill: none;
}
@ -204,7 +204,7 @@
background: none;
border: none;
svg {
stroke: $df-secondary;
stroke: var(--color-foreground-secondary);
}
}
}
@ -242,15 +242,15 @@
display: flex;
flex-direction: column;
gap: $s-24;
color: $db-secondary;
color: var(--color-background-secondary);
width: $s-500;
h2 {
color: $df-primary;
color: var(--color-foreground-primary);
font-weight: 400;
}
p {
color: $df-secondary;
color: var(--color-foreground-secondary);
font-size: $fs-16;
}
}
@ -263,7 +263,7 @@
.fonts-placeholder {
align-items: center;
border-radius: $br-8;
border: $s-1 solid $db-quaternary;
border: $s-1 solid var(--color-background-quaternary);
display: flex;
flex-direction: column;
height: $s-160;
@ -273,14 +273,14 @@
width: 100%;
.icon svg {
stroke: $df-secondary;
stroke: var(--color-foreground-secondary);
fill: none;
width: $s-32;
height: $s-32;
}
.label {
color: $df-secondary;
color: var(--color-foreground-secondary);
font-size: $fs-14;
}
}

View file

@ -11,6 +11,7 @@
[app.common.data.macros :as dm]
[app.common.geom.point :as gpt]
[app.common.logging :as log]
[app.config :as cf]
[app.main.data.dashboard :as dd]
[app.main.data.messages :as msg]
[app.main.features :as features]
@ -25,6 +26,7 @@
[app.main.ui.dashboard.import :refer [use-import-file]]
[app.main.ui.dashboard.inline-edition :refer [inline-edition]]
[app.main.ui.dashboard.placeholder :refer [empty-placeholder loading-placeholder]]
[app.main.ui.ds.product.loader :refer [loader*]]
[app.main.ui.hooks :as h]
[app.main.ui.icons :as i]
[app.main.worker :as wrk]
@ -47,7 +49,7 @@
[file-id revn blob]
(let [params {:file-id file-id :revn revn :media blob}]
(->> (rp/cmd! :create-file-thumbnail params)
(rx/map :uri))))
(rx/map :id))))
(defn render-thumbnail
[file-id revn]
@ -71,15 +73,15 @@
(mf/defc grid-item-thumbnail
{::mf/wrap-props false}
[{:keys [file-id revn thumbnail-uri background-color]}]
[{:keys [file-id revn thumbnail-id background-color]}]
(let [container (mf/use-ref)
visible? (h/use-visible container :once? true)]
(mf/with-effect [file-id revn visible? thumbnail-uri]
(when (and visible? (not thumbnail-uri))
(mf/with-effect [file-id revn visible? thumbnail-id]
(when (and visible? (not thumbnail-id))
(->> (ask-for-thumbnail file-id revn)
(rx/subs! (fn [url]
(st/emit! (dd/set-file-thumbnail file-id url)))
(rx/subs! (fn [thumbnail-id]
(st/emit! (dd/set-file-thumbnail file-id thumbnail-id)))
(fn [cause]
(log/error :hint "unable to render thumbnail"
:file-if file-id
@ -90,12 +92,14 @@
:style {:background-color background-color}
:ref container}
(when visible?
(if thumbnail-uri
(if thumbnail-id
[:img {:class (stl/css :grid-item-thumbnail-image)
:src thumbnail-uri
:src (cf/resolve-media thumbnail-id)
:loading "lazy"
:decoding "async"}]
i/loader-pencil))]))
[:> loader* {:class (stl/css :grid-loader)
:overlay true
:title (tr "labels.loading")}]))]))
;; --- Grid Item Library
@ -113,7 +117,9 @@
[:div {:class (stl/css :grid-item-th :library)}
(if (nil? file)
i/loader-pencil
[:> loader* {:class (stl/css :grid-loader)
:overlay true
:title (tr "labels.loading")}]
(let [summary (:library-summary file)
components (:components summary)
colors (:colors summary)
@ -365,7 +371,7 @@
[:& grid-item-thumbnail
{:file-id (:id file)
:revn (:revn file)
:thumbnail-uri (:thumbnail-uri file)
:thumbnail-id (:thumbnail-id file)
:background-color (dm/get-in file [:data :options :background])}])
(when (and (:is-shared file) (not library-view?))
@ -458,7 +464,6 @@
:on-drag-leave on-drag-leave
:on-drop on-drop
:ref node-ref}
(cond
(nil? files)
[:& loading-placeholder]

View file

@ -6,6 +6,9 @@
@import "refactor/common-refactor.scss";
// TODO: Legacy sass variables. We should remove them in favor of DS tokens.
$bp-max-1366: "(max-width: 1366px)";
$thumbnail-default-width: $s-252; // Default width
$thumbnail-default-height: $s-168; // Default width
@ -60,7 +63,7 @@ $thumbnail-default-height: $s-168; // Default width
&.dragged {
border-radius: $br-4;
outline: $br-4 solid $da-primary;
outline: $br-4 solid var(--color-accent-primary);
text-align: initial;
width: calc(var(--th-width) + $s-12);
height: var(--th-height, #{$thumbnail-default-height});
@ -68,7 +71,7 @@ $thumbnail-default-height: $s-168; // Default width
&.overlay {
border-radius: $br-4;
border: $s-2 solid $da-tertiary;
border: $s-2 solid var(--color-accent-tertiary);
height: 100%;
opacity: 0;
pointer-events: none;
@ -98,7 +101,7 @@ $thumbnail-default-height: $s-168; // Default width
h3 {
border: $s-1 solid transparent;
color: $df-primary;
color: var(--color-foreground-primary);
font-size: $fs-16;
font-weight: $fw400;
height: $s-28;
@ -117,7 +120,7 @@ $thumbnail-default-height: $s-168; // Default width
}
.date {
color: $df-secondary;
color: var(--color-foreground-secondary);
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
@ -133,7 +136,7 @@ $thumbnail-default-height: $s-168; // Default width
}
.item-badge {
background-color: $da-primary;
background-color: var(--color-accent-primary);
border: none;
border-radius: $br-6;
position: absolute;
@ -146,7 +149,7 @@ $thumbnail-default-height: $s-168; // Default width
justify-content: center;
svg {
stroke: $db-secondary;
stroke: var(--color-background-secondary);
fill: none;
height: $s-16;
width: $s-16;
@ -154,18 +157,18 @@ $thumbnail-default-height: $s-168; // Default width
}
&.add-file {
border: $s-1 dashed $df-secondary;
border: $s-1 dashed var(--color-foreground-secondary);
justify-content: center;
box-shadow: none;
span {
color: $db-primary;
color: var(--color-background-primary);
font-size: $fs-14;
}
&:hover {
background-color: $df-primary;
border: $s-2 solid $da-tertiary;
background-color: var(--color-foreground-primary);
border: $s-2 solid var(--color-accent-tertiary);
}
}
}
@ -176,9 +179,9 @@ $thumbnail-default-height: $s-168; // Default width
left: $s-4;
width: $s-32;
height: $s-32;
background-color: $da-tertiary;
background-color: var(--color-accent-tertiary);
border-radius: $br-circle;
color: $db-secondary;
color: var(--color-background-secondary);
font-size: $fs-16;
display: flex;
justify-content: center;
@ -194,7 +197,7 @@ $thumbnail-default-height: $s-168; // Default width
&:hover,
&:focus,
&:focus-within {
background-color: $db-tertiary;
background-color: var(--color-background-tertiary);
.project-th-actions {
opacity: 1;
}
@ -205,7 +208,7 @@ $thumbnail-default-height: $s-168; // Default width
.selected {
.grid-item-th {
outline: $s-4 solid $da-tertiary;
outline: $s-4 solid var(--color-accent-tertiary);
}
}
}
@ -220,7 +223,7 @@ $thumbnail-default-height: $s-168; // Default width
width: $s-32;
span {
color: $db-secondary;
color: var(--color-background-secondary);
}
}
@ -275,16 +278,6 @@ $thumbnail-default-height: $s-168; // Default width
height: auto;
width: 100%;
}
svg {
height: 100%;
width: 100%;
}
:global(svg#loader-pencil) {
stroke: $db-quaternary;
width: calc(var(--th-width, #{$thumbnail-default-width}) * 0.25);
}
}
// LIBRARY VIEW
@ -297,7 +290,7 @@ $thumbnail-default-height: $s-168; // Default width
}
.grid-item-th.library {
background-color: $db-tertiary;
background-color: var(--color-background-tertiary);
flex-direction: column;
height: 90%;
justify-content: flex-start;
@ -306,7 +299,7 @@ $thumbnail-default-height: $s-168; // Default width
.asset-section {
font-size: $fs-12;
color: $df-secondary;
color: var(--color-foreground-secondary);
&:not(:first-child) {
margin-top: $s-16;
@ -319,7 +312,7 @@ $thumbnail-default-height: $s-168; // Default width
text-transform: uppercase;
.num-assets {
color: $df-secondary;
color: var(--color-foreground-secondary);
}
}
@ -327,7 +320,7 @@ $thumbnail-default-height: $s-168; // Default width
align-items: center;
border-radius: $br-4;
border: $s-1 solid transparent;
color: $df-primary;
color: var(--color-foreground-primary);
display: flex;
font-size: $fs-12;
margin-top: $s-4;
@ -335,7 +328,7 @@ $thumbnail-default-height: $s-168; // Default width
position: relative;
.name-block {
color: $df-secondary;
color: var(--color-foreground-secondary);
width: calc(100% - $s-24 - $s-8);
}
@ -356,11 +349,11 @@ $thumbnail-default-height: $s-168; // Default width
}
.color-name {
color: $df-primary;
color: var(--color-foreground-primary);
}
.color-value {
color: $df-secondary;
color: var(--color-foreground-secondary);
margin-left: $s-4;
text-transform: uppercase;
}
@ -378,3 +371,7 @@ $thumbnail-default-height: $s-168; // Default width
grid-template-columns: auto 1fr;
gap: $s-8;
}
.grid-loader {
--icon-width: calc(var(--th-width, #{$thumbnail-default-width}) * 0.25);
}

Some files were not shown because too many files have changed in this diff Show more