diff --git a/backend/src/app/migrations.clj b/backend/src/app/migrations.clj
index 94d3aaa3ac..c1ec7bd664 100644
--- a/backend/src/app/migrations.clj
+++ b/backend/src/app/migrations.clj
@@ -115,6 +115,9 @@
{:name "0033-mod-comment-thread-table"
:fn (mg/resource "app/migrations/sql/0033-mod-comment-thread-table.sql")}
+
+ {:name "0034-mod-profile-table-add-props-field"
+ :fn (mg/resource "app/migrations/sql/0034-mod-profile-table-add-props-field.sql")}
]})
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
diff --git a/backend/src/app/migrations/sql/0034-mod-profile-table-add-props-field.sql b/backend/src/app/migrations/sql/0034-mod-profile-table-add-props-field.sql
new file mode 100644
index 0000000000..11f80ec320
--- /dev/null
+++ b/backend/src/app/migrations/sql/0034-mod-profile-table-add-props-field.sql
@@ -0,0 +1 @@
+ALTER TABLE profile ADD COLUMN props jsonb NULL DEFAULT NULL;
diff --git a/backend/src/app/services/mutations/profile.clj b/backend/src/app/services/mutations/profile.clj
index a45ddcbca9..01f11d69c7 100644
--- a/backend/src/app/services/mutations/profile.clj
+++ b/backend/src/app/services/mutations/profile.clj
@@ -280,7 +280,7 @@
(defn- validate-password!
[conn {:keys [profile-id old-password] :as params}]
- (let [profile (profile/retrieve-profile-data conn profile-id)]
+ (let [profile (db/get-by-id conn :profile profile-id)]
(when-not (:valid (verify-password old-password (:password profile)))
(ex/raise :type :validation
:code :old-password-not-match))))
@@ -310,7 +310,7 @@
[{:keys [profile-id file] :as params}]
(media/validate-media-type (:content-type file))
(db/with-atomic [conn db/pool]
- (let [profile (profile/retrieve-profile conn profile-id)
+ (let [profile (db/get-by-id conn :profile profile-id)
_ (media/run {:cmd :info :input {:path (:tempfile file)
:mtype (:content-type file)}})
photo (teams/upload-photo conn params)]
@@ -409,6 +409,27 @@
(update-password conn))
nil)))
+;; --- Mutation: Update Profile Props
+
+(s/def ::props map?)
+(s/def ::update-profile-props
+ (s/keys :req-un [::profile-id ::props]))
+
+(sm/defmutation ::update-profile-props
+ [{:keys [profile-id props]}]
+ (db/with-atomic [conn db/pool]
+ (let [profile (profile/retrieve-profile-data conn profile-id)
+ props (reduce-kv (fn [props k v]
+ (if (nil? v)
+ (dissoc props k)
+ (assoc props k v)))
+ (:props profile)
+ props)]
+ (db/update! conn :profile
+ {:props (db/tjson props)}
+ {:id profile-id})
+ nil)))
+
;; --- Mutation: Delete Profile
diff --git a/backend/src/app/services/queries/profile.clj b/backend/src/app/services/queries/profile.clj
index 14da3b4535..db954905d7 100644
--- a/backend/src/app/services/queries/profile.clj
+++ b/backend/src/app/services/queries/profile.clj
@@ -73,9 +73,15 @@
{:default-team-id (:id team)
:default-project-id (:id project)}))
+(defn decode-profile-row
+ [{:keys [props] :as row}]
+ (cond-> row
+ (db/pgobject? props) (assoc :props (db/decode-transit-pgobject props))))
+
(defn retrieve-profile-data
[conn id]
- (db/get-by-id conn :profile id))
+ (-> (db/get-by-id conn :profile id)
+ (decode-profile-row)))
(defn retrieve-profile
[conn id]
@@ -97,7 +103,8 @@
(defn retrieve-profile-data-by-email
[conn email]
(let [email (str/lower email)]
- (db/exec-one! conn [sql:profile-by-email email])))
+ (-> (db/exec-one! conn [sql:profile-by-email email])
+ (decode-profile-row))))
;; --- Attrs Helpers
diff --git a/frontend/resources/images/dashboard-img.svg b/frontend/resources/images/dashboard-img.svg
deleted file mode 100644
index 00544e925d..0000000000
--- a/frontend/resources/images/dashboard-img.svg
+++ /dev/null
@@ -1,119 +0,0 @@
-
-
-
-
diff --git a/frontend/resources/images/deco-left.png b/frontend/resources/images/deco-left.png
new file mode 100644
index 0000000000..bd14661c7a
Binary files /dev/null and b/frontend/resources/images/deco-left.png differ
diff --git a/frontend/resources/images/deco-right.png b/frontend/resources/images/deco-right.png
new file mode 100644
index 0000000000..cc108924e5
Binary files /dev/null and b/frontend/resources/images/deco-right.png differ
diff --git a/frontend/resources/images/login-bg.jpg b/frontend/resources/images/login-bg.jpg
deleted file mode 100644
index e670a377fe..0000000000
Binary files a/frontend/resources/images/login-bg.jpg and /dev/null differ
diff --git a/frontend/resources/images/on-design.gif b/frontend/resources/images/on-design.gif
new file mode 100644
index 0000000000..94a3925b54
Binary files /dev/null and b/frontend/resources/images/on-design.gif differ
diff --git a/frontend/resources/images/on-feed.gif b/frontend/resources/images/on-feed.gif
new file mode 100644
index 0000000000..a850edc58f
Binary files /dev/null and b/frontend/resources/images/on-feed.gif differ
diff --git a/frontend/resources/images/on-handoff.gif b/frontend/resources/images/on-handoff.gif
new file mode 100644
index 0000000000..e5feb0af9c
Binary files /dev/null and b/frontend/resources/images/on-handoff.gif differ
diff --git a/frontend/resources/images/on-proto.gif b/frontend/resources/images/on-proto.gif
new file mode 100644
index 0000000000..9ccb7fbf86
Binary files /dev/null and b/frontend/resources/images/on-proto.gif differ
diff --git a/frontend/resources/images/onboarding-start.jpg b/frontend/resources/images/onboarding-start.jpg
new file mode 100644
index 0000000000..f089ba4a15
Binary files /dev/null and b/frontend/resources/images/onboarding-start.jpg differ
diff --git a/frontend/resources/images/onboarding-team.jpg b/frontend/resources/images/onboarding-team.jpg
new file mode 100644
index 0000000000..dbd28f9d47
Binary files /dev/null and b/frontend/resources/images/onboarding-team.jpg differ
diff --git a/frontend/resources/images/open-source.svg b/frontend/resources/images/open-source.svg
new file mode 100644
index 0000000000..7bc4f583e3
--- /dev/null
+++ b/frontend/resources/images/open-source.svg
@@ -0,0 +1,38 @@
+
diff --git a/frontend/resources/images/penpot-login2.jpg b/frontend/resources/images/penpot-login2.jpg
deleted file mode 100644
index 3c9409fb5c..0000000000
Binary files a/frontend/resources/images/penpot-login2.jpg and /dev/null differ
diff --git a/frontend/resources/images/pot.png b/frontend/resources/images/pot.png
new file mode 100644
index 0000000000..ab8a96da49
Binary files /dev/null and b/frontend/resources/images/pot.png differ
diff --git a/frontend/resources/styles/main/partials/modal.scss b/frontend/resources/styles/main/partials/modal.scss
index 12a9ab1d04..8638e05ca3 100644
--- a/frontend/resources/styles/main/partials/modal.scss
+++ b/frontend/resources/styles/main/partials/modal.scss
@@ -362,3 +362,190 @@
}
}
+//- ONBOARDING
+.onboarding {
+ background-color: $color-white;
+ box-shadow: 0 10px 10px rgba(0,0,0,.2);
+ display: flex;
+ height: 350px;
+ flex-direction: row;
+ font-family: "sourcesanspro", sans-serif;
+ min-width: 620px;
+ position: relative;
+
+ .modal-left {
+ align-items: center;
+ background-color: $color-primary;
+ border-top-left-radius: 5px;
+ border-bottom-left-radius: 5px;
+ display: flex;
+ flex-shrink: 0;
+ justify-content: center;
+ padding: $x-big;
+ width: 230px;
+ }
+
+ .modal-right {
+ border-top-right-radius: 5px;
+ border-bottom-right-radius: 5px;
+ display: flex;
+ flex-direction: column;
+ padding: $x-big;
+
+ .modal-title h2 {
+ color: $color-black;
+ font-size: $fs28;
+ font-weight: 900;
+ }
+
+ .release {
+ background-color: $color-primary;
+ color: $color-black;
+ font-size: $fs12;
+ font-weight: bold;
+ margin-top: $small;
+ padding: 2px $x-small;
+ width: max-content;
+ }
+
+ .modal-content {
+ border: none;
+ padding: $big 0;
+
+ p {
+ color: $color-black;
+ font-size: 16px;
+ margin-top: $small;
+ }
+ }
+
+ .modal-navigation {
+ align-items: center;
+ display: flex;
+ margin-top: auto;
+
+ .skip {
+ cursor: pointer;
+ font-family: "worksans", sans-serif;
+ font-size: $fs13;
+ margin-left: $big;
+
+ &:hover {
+ color: $color-black;
+ }
+ }
+ }
+
+ .step-dots {
+ align-items: center;
+ display: flex;
+ margin-bottom: 0;
+ margin-left: auto;
+
+ li {
+ background-color: $color-gray-10;
+ border-radius: 50%;
+ height: $small;
+ margin-left: $small;
+ width: $small;
+
+ &.current {
+ background-color: $color-primary;
+ }
+ }
+ }
+ }
+
+ &.black {
+ .modal-left {
+ background-color: $color-black;
+ }
+ }
+
+ button {
+ font-family: "worksans", sans-serif;
+ }
+
+ &.feature {
+ .modal-left {
+ padding: 0;
+
+ img {
+ border-top-left-radius: 5px;
+ border-bottom-left-radius: 5px;
+ }
+ }
+ }
+
+ &.final {
+ padding: $big 0 0 0;
+
+ .modal-left,
+ .modal-right {
+ align-items: center;
+ background-color: $color-white;
+ color: $color-black;
+ flex: 1;
+ flex-direction: column;
+ padding: $x-big 40px;
+ text-align: center;
+
+ h2 {
+ font-weight: 900;
+ margin-bottom: $big;
+ font-size: $fs24;
+ }
+
+ p {
+ font-size: $fs14;
+ }
+
+ .btn-primary {
+ margin-bottom: 0;
+ margin-top: auto;
+ width: 200px;
+ }
+
+ img {
+ box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.25);
+ border-radius: $br-medium;
+ margin-bottom: $x-big;
+ margin-top: -90px;
+ width: 150px;
+ }
+ }
+
+ .modal-left {
+ border-right: 1px solid $color-gray-10;
+
+ form {
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ margin-top: auto;
+
+ .custom-input {
+ margin-bottom: $medium;
+
+ input {
+ width: 200px;
+ }
+ }
+ }
+ }
+
+ }
+}
+
+.deco {
+ left: -10px;
+ position: absolute;
+ top: -18px;
+ width: 60px;
+
+ &.right {
+ left: 590px;
+ top: 0;
+ }
+}
+
diff --git a/frontend/src/app/main/data/users.cljs b/frontend/src/app/main/data/users.cljs
index 31da7e8a38..a3f5904bc3 100644
--- a/frontend/src/app/main/data/users.cljs
+++ b/frontend/src/app/main/data/users.cljs
@@ -156,6 +156,16 @@
(rx/ignore))))))
+(defn mark-onboarding-as-viewed
+ []
+ (ptk/reify ::mark-oboarding-as-viewed
+ ptk/WatchEvent
+ (watch [_ state stream]
+ (let [{:keys [id] :as profile} (:profile state)]
+ (->> (rp/mutation :update-profile-props {:props {:onboarding-viewed true}})
+ (rx/map (constantly fetch-profile)))))))
+
+
;; --- Update Photo
(defn update-photo
@@ -165,7 +175,6 @@
ptk/WatchEvent
(watch [_ state stream]
(let [on-success di/notify-finished-loading
-
on-error #(do (di/notify-finished-loading)
(di/process-error %))
diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs
index 8d194113c0..635d2afa64 100644
--- a/frontend/src/app/main/ui.cljs
+++ b/frontend/src/app/main/ui.cljs
@@ -22,6 +22,7 @@
[app.main.ui.auth.verify-token :refer [verify-token]]
[app.main.ui.cursors :as c]
[app.main.ui.context :as ctx]
+ [app.main.ui.onboarding]
[app.main.ui.dashboard :refer [dashboard]]
[app.main.ui.icons :as i]
[app.main.ui.messages :as msgs]
diff --git a/frontend/src/app/main/ui/dashboard.cljs b/frontend/src/app/main/ui/dashboard.cljs
index d1abccf4e7..6c5350f0c6 100644
--- a/frontend/src/app/main/ui/dashboard.cljs
+++ b/frontend/src/app/main/ui/dashboard.cljs
@@ -13,6 +13,7 @@
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.main.data.dashboard :as dd]
+ [app.main.data.modal :as modal]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.dashboard.files :refer [files-section]]
@@ -24,6 +25,7 @@
[app.main.ui.icons :as i]
[app.util.i18n :as i18n :refer [t]]
[app.util.router :as rt]
+ [app.util.storage :refer [storage]]
[cuerdas.core :as str]
[okulary.core :as l]
[rumext.alpha :as mf]))
@@ -101,6 +103,11 @@
projects (mf/deref projects-ref)
project (get projects project-id)]
+ (mf/use-effect
+ (fn []
+ (when (and profile (not (get-in profile [:props :onboarding-viewed])))
+ (st/emit! (modal/show {:type :onboarding})))))
+
(mf/use-effect
(mf/deps team-id)
(st/emitf (dd/fetch-bundle {:id team-id})))
diff --git a/frontend/src/app/main/ui/modal.cljs b/frontend/src/app/main/ui/modal.cljs
index 87d7ad5982..e9fa25c7a0 100644
--- a/frontend/src/app/main/ui/modal.cljs
+++ b/frontend/src/app/main/ui/modal.cljs
@@ -61,6 +61,7 @@
[props]
(let [data (unchecked-get props "data")
wrapper-ref (mf/use-ref nil)
+ components (mf/deref dm/components)
allow-click-outside (:allow-click-outside data)
@@ -86,9 +87,7 @@
(events/unlistenByKey key)))))
[:div.modal-wrapper {:ref wrapper-ref}
- (mf/element
- (get @dm/components (:type data))
- (:props data))]))
+ (mf/element (get components (:type data)) (:props data))]))
(def modal-ref
@@ -97,5 +96,6 @@
(mf/defc modal
[]
(let [modal (mf/deref modal-ref)]
- (when modal [:& modal-wrapper {:data modal
- :key (:id modal)}])))
+ (when modal
+ [:& modal-wrapper {:data modal
+ :key (:id modal)}])))
diff --git a/frontend/src/app/main/ui/onboarding.cljs b/frontend/src/app/main/ui/onboarding.cljs
new file mode 100644
index 0000000000..77b88823aa
--- /dev/null
+++ b/frontend/src/app/main/ui/onboarding.cljs
@@ -0,0 +1,230 @@
+;; 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/.
+;;
+;; This Source Code Form is "Incompatible With Secondary Licenses", as
+;; defined by the Mozilla Public License, v. 2.0.
+;;
+;; Copyright (c) 2020 UXBOX Labs SL
+
+(ns app.main.ui.onboarding
+ (:require
+ [app.common.spec :as us]
+ [app.main.data.dashboard :as dd]
+ [app.main.data.messages :as dm]
+ [app.main.data.modal :as modal]
+ [app.main.data.users :as du]
+ [app.main.store :as st]
+ [app.main.ui.components.forms :as fm :refer [input submit-button form]]
+ [app.util.router :as rt]
+ [app.util.timers :as tm]
+ [cljs.spec.alpha :as s]
+ [rumext.alpha :as mf]))
+
+(defmulti render-slide :slide)
+
+(defmethod render-slide :start
+ [{:keys [navigate] :as props}]
+ (mf/html
+ [:div.modal-container.onboarding
+ [:div.modal-left
+ [:img {:src "images/pot.png" :border "0" :alt "Penpot"}]]
+ [:div.modal-right
+ [:div.modal-title
+ [:h2 "Welcome to Penpot!"]]
+ [:span.release "Alpha version 1.0"]
+ [:div.modal-content
+ [:p "We are very happy to introduce you to the very first Alpha 1.0 release."]
+ [:p "Penpot is still at development stage and there will be constant updates. We hope you enjoy the first stable version."]]
+ [:div.modal-navigation
+ [:button.btn-secondary {:on-click #(navigate :opensource)} "Continue"]]]
+ [:img.deco {:src "images/deco-left.png" :border "0"}]
+ [:img.deco.right {:src "images/deco-right.png" :border "0"}]]))
+
+
+(defmethod render-slide :opensource
+ [{:keys [navigate] :as props}]
+ (mf/html
+ [:div.modal-container.onboarding.black
+ [:div.modal-left
+ [:img {:src "images/open-source.svg" :border "0" :alt "Open Source"}]]
+ [:div.modal-right
+ [:div.modal-title
+ [:h2 "Open Source Contributor?"]]
+ [:div.modal-content
+ [:p "Penpot is Open Source, made by and for the community. If you want to collaborate, you are more than welcome!"]
+ [:p "You can access the " [:a {:href "https://github.com/penpot" :target "_blank"} "project on github"] " and follow the contribution instructions :)"]]
+ [:div.modal-navigation
+ [:button.btn-secondary {:on-click #(navigate :feature1)} "Continue"]]]]))
+
+(defmethod render-slide :feature1
+ [{:keys [navigate skip] :as props}]
+ (mf/html
+ [:div.modal-container.onboarding.feature
+ [:div.modal-left
+ [:img {:src "images/on-design.gif" :border "0" :alt "Create designs"}]]
+ [:div.modal-right
+ [:div.modal-title
+ [:h2 "Design libraries, styles and components"]]
+ [:div.modal-content
+ [:p "Create beautiful user interfaces in collaboration with all team members."]
+ [:p "Maintain consistency at scale with components, libraries and design systems."]]
+ [:div.modal-navigation
+ [:button.btn-secondary {:on-click #(navigate :feature2)} "Continue"]
+ [:span.skip {:on-click skip} "Skip"]
+ [:ul.step-dots
+ [:li.current]
+ [:li]
+ [:li]
+ [:li]]]]]))
+
+(defmethod render-slide :feature2
+ [{:keys [navigate skip] :as props}]
+ (mf/html
+ [:div.modal-container.onboarding.feature
+ [:div.modal-left
+ [:img {:src "images/on-proto.gif" :border "0" :alt "Interactive prototypes"}]]
+ [:div.modal-right
+ [:div.modal-title
+ [:h2 "Bring your designs to life with interactions"]]
+ [:div.modal-content
+ [:p "Create rich interactions to mimic the product behaviour."]
+ [:p "Share to stakeholders, present proposals to your team and start user testing with your designs, all in one place."]]
+ [:div.modal-navigation
+ [:button.btn-secondary {:on-click #(navigate :feature3)} "Continue"]
+ [:span.skip {:on-click skip} "Skip"]
+ [:ul.step-dots
+ [:li]
+ [:li.current]
+ [:li]
+ [:li]]]]]))
+
+(defmethod render-slide :feature3
+ [{:keys [navigate skip] :as props}]
+ (mf/html
+ [:div.modal-container.onboarding.feature
+ [:div.modal-left
+ [:img {:src "images/on-feed.gif" :border "0" :alt "Get feedback"}]]
+ [:div.modal-right
+ [:div.modal-title
+ [:h2 "Get feedback, present and share your work"]]
+ [:div.modal-content
+ [:p "All team members working simultaneously with real time design multiplayer and centralised comments, ideas and feedback right over the designs."]]
+ [:div.modal-navigation
+ [:button.btn-secondary {:on-click #(navigate :feature4)} "Continue"]
+ [:span.skip {:on-click skip} "Skip"]
+ [:ul.step-dots
+ [:li]
+ [:li]
+ [:li.current]
+ [:li]]]]]))
+
+(defmethod render-slide :feature4
+ [{:keys [navigate skip] :as props}]
+ (mf/html
+ [:div.modal-container.onboarding.feature
+ [:div.modal-left
+ [:img {:src "images/on-handoff.gif" :border "0" :alt "Handoff and lowcode"}]]
+ [:div.modal-right
+ [:div.modal-title
+ [:h2 "One shared source of truth"]]
+ [:div.modal-content
+ [:p "Sync the design and code of all your components and styles and get code snippets."]
+ [:p "Get and provide code specifications like markup (SVG, HTML) or styles (CSS, Less, Stylus…)."]]
+ [:div.modal-navigation
+ [:button.btn-secondary {:on-click skip} "Continue"]
+ [:span.skip {:on-click skip} "Skip"]
+ [:ul.step-dots
+ [:li]
+ [:li]
+ [:li]
+ [:li.current]]]]]))
+
+(mf/defc onboarding-modal
+ {::mf/register modal/components
+ ::mf/register-as :onboarding}
+ [props]
+ (let [slide (mf/use-state :start)
+ klass (mf/use-state "fadeInDown")
+
+ navigate
+ (mf/use-callback #(reset! slide %))
+
+ skip
+ (mf/use-callback
+ (st/emitf (modal/hide)
+ (modal/show {:type :onboarding-team})
+ (du/mark-onboarding-as-viewed)))]
+
+ (mf/use-layout-effect
+ (mf/deps @slide)
+ (fn []
+ (when (not= :start @slide)
+ (reset! klass "fadeIn"))
+ (let [sem (tm/schedule 300 #(reset! klass nil))]
+ (fn []
+ (reset! klass nil)
+ (tm/dispose! sem)))))
+
+ [:div.modal-overlay
+ [:div.animated {:class @klass}
+ (render-slide
+ (assoc props
+ :slide @slide
+ :navigate navigate
+ :skip skip))]]))
+
+(s/def ::name ::us/not-empty-string)
+(s/def ::team-form
+ (s/keys :req-un [::name]))
+
+(defn- on-success
+ [form response]
+ (st/emit! (modal/hide)
+ (rt/nav :dashboard-projects {:team-id (:id response)})))
+
+(defn- on-error
+ [form response]
+ (st/emit! (dm/error "Error on creating team.")))
+
+(defn- on-submit
+ [form event]
+ (let [mdata {:on-success (partial on-success form)
+ :on-error (partial on-error form)}
+ params {:name (get-in @form [:clean-data :name])}]
+ (st/emit! (dd/create-team (with-meta params mdata)))))
+
+(mf/defc onboarding-team-modal
+ {::mf/register modal/components
+ ::mf/register-as :onboarding-team}
+ [props]
+ (let [close (mf/use-fn (st/emitf (modal/hide)))
+ form (fm/use-form :spec ::team-form
+ :initial {})
+
+ on-submit
+ (mf/use-callback (partial on-submit form))]
+ [:div.modal-overlay
+ [:div.modal-container.onboarding.final.animated.fadeInUp
+ [:div.modal-left
+ [:img {:src "images/onboarding-team.jpg" :border "0" :alt "Create a team"}]
+ [:h2 "Create a team"]
+ [:p "Are you working with someone? Create a team to work together on projects and share design assets."]
+
+ [:& fm/form {:form form
+ :on-submit on-submit}
+ [:& fm/input {:type "text"
+ :name :name
+ :label "Enter new team name"}]
+ [:& fm/submit-button
+ {:label "Create team"}]]]
+ [:div.modal-right
+ [:img {:src "images/onboarding-start.jpg" :border "0" :alt "Start designing"}]
+ [:h2 "Start designing"]
+ [:p "Jump right away into Penpot and start designing by your own. You will still have the chance to create teams later."]
+ [:button.btn-primary.btn-large {:on-click close} "Start right away"]]
+
+
+ [:img.deco {:src "images/deco-left.png" :border "0"}]
+ [:img.deco.right {:src "images/deco-right.png" :border "0"}]]]))
+
diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs
index 2095bae52b..1dad84d487 100644
--- a/frontend/src/app/util/dom.cljs
+++ b/frontend/src/app/util/dom.cljs
@@ -29,6 +29,7 @@
[e]
(.-target e))
+
(defn classnames
[& params]
(assert (even? (count params)))
@@ -39,6 +40,7 @@
[]
(partition 2 params))))
+
;; --- New methods
(defn get-element-by-class
diff --git a/frontend/src/app/util/timers.cljs b/frontend/src/app/util/timers.cljs
index 1861613449..a7afaff2b4 100644
--- a/frontend/src/app/util/timers.cljs
+++ b/frontend/src/app/util/timers.cljs
@@ -21,6 +21,10 @@
(-dispose [_]
(js/clearTimeout sem))))))
+(defn dispose!
+ [v]
+ (rx/dispose! v))
+
(defn asap
[f]
(-> (p/resolved nil)