mirror of
https://github.com/penpot/penpot.git
synced 2025-06-02 15:11:39 +02:00
Merge remote-tracking branch 'penpot/develop' into token-studio-develop
This commit is contained in:
commit
5fbbdd36fd
710 changed files with 33332 additions and 24717 deletions
|
@ -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))
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
([]
|
||||
|
|
189
frontend/src/app/main/data/changes.cljs
Normal file
189
frontend/src/app/main/data/changes.cljs
Normal 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)))))))
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
231
frontend/src/app/main/data/persistence.cljs
Normal file
231
frontend/src/app/main/data/persistence.cljs
Normal 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)))))))
|
|
@ -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))))))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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})))))))
|
||||
|
|
|
@ -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?)))))))))))
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]))
|
||||
|
||||
|
|
|
@ -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}))))))))
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)))))))
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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})))))))
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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}))))))
|
||||
|
|
|
@ -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))))))))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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))))))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]))
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)))))))))))
|
||||
|
|
|
@ -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)))))
|
|
@ -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]
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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)))))))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}))))))))
|
||||
|
||||
|
|
|
@ -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))))))
|
||||
|
|
|
@ -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})))))))
|
||||
|
|
|
@ -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)))))))
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -45,6 +45,9 @@
|
|||
(def export
|
||||
(l/derived :export st/state))
|
||||
|
||||
(def persistence
|
||||
(l/derived :persistence st/state))
|
||||
|
||||
;; ---- Dashboard refs
|
||||
|
||||
(def dashboard-local
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)))
|
||||
|
|
|
@ -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 %)))
|
||||
|
|
|
@ -97,7 +97,7 @@
|
|||
(when cls
|
||||
(cond
|
||||
(true? v) cls
|
||||
(false? v) nil
|
||||
(false? v) ""
|
||||
:else `(if ~v ~cls ""))))))
|
||||
(interpose " ")))
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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")]])]]))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")]]]))
|
||||
|
|
|
@ -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")]]))
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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}])))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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%;
|
||||
|
|
|
@ -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})
|
|
@ -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}
|
||||
|
|
28
frontend/src/app/main/ui/components/button_link.scss
Normal file
28
frontend/src/app/main/ui/components/button_link.scss
Normal file
|
@ -0,0 +1,28 @@
|
|||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) KALEIDOS INC
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
|
@ -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])
|
||||
|
|
@ -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} />
|
|
@ -1,13 +0,0 @@
|
|||
.button {
|
||||
font-family: monospace;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 0.5rem;
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
stroke: #000;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
),
|
||||
}
|
|
@ -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)
|
||||
});
|
|
@ -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))))])))
|
||||
|
|
|
@ -86,8 +86,8 @@
|
|||
.big-text {
|
||||
@include inspectValue;
|
||||
@include twoLineTextEllipsis;
|
||||
line-height: 1;
|
||||
color: var(--palette-text-color);
|
||||
height: $s-28;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
|
|
@ -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]])]))))])])])))
|
||||
|
||||
|
|
|
@ -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"}]]))
|
||||
|
||||
|
|
|
@ -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})))))
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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]))
|
||||
|
|
|
@ -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}]))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]]))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)]]))]))
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
|
||||
.input-wrapper {
|
||||
@extend .input-with-label;
|
||||
@include bodySmallTypography;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
|
|
|
@ -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)))}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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)))}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue