diff --git a/common/src/app/common/data.cljc b/common/src/app/common/data.cljc index 09dd24310..77dcae010 100644 --- a/common/src/app/common/data.cljc +++ b/common/src/app/common/data.cljc @@ -159,6 +159,11 @@ ([mfn coll] (into {} (mapm mfn) coll))) +(defn removev + "Returns a vector of the items in coll for which (fn item) returns logical false" + [fn coll] + (filterv (comp not fn) coll)) + (defn filterm "Filter values of a map that satisfy a predicate" [pred coll] diff --git a/common/src/app/common/pages/spec.cljc b/common/src/app/common/pages/spec.cljc index a1db691f2..286148439 100644 --- a/common/src/app/common/pages/spec.cljc +++ b/common/src/app/common/pages/spec.cljc @@ -10,6 +10,7 @@ [app.common.geom.point :as gpt] [app.common.spec :as us] [app.common.types.interactions :as cti] + [app.common.types.page-options :as cto] [app.common.uuid :as uuid] [clojure.set :as set] [clojure.spec.alpha :as s])) @@ -144,43 +145,6 @@ :internal.blur/value :internal.blur/hidden])) -;; Page Options -(s/def :internal.page.grid.color/value string?) -(s/def :internal.page.grid.color/opacity ::us/safe-number) - -(s/def :internal.page.grid/size ::us/safe-integer) -(s/def :internal.page.grid/color - (s/keys :req-un [:internal.page.grid.color/value - :internal.page.grid.color/opacity])) - -(s/def :internal.page.grid/type #{:stretch :left :center :right}) -(s/def :internal.page.grid/item-length (s/nilable ::us/safe-integer)) -(s/def :internal.page.grid/gutter (s/nilable ::us/safe-integer)) -(s/def :internal.page.grid/margin (s/nilable ::us/safe-integer)) - -(s/def :internal.page.grid/square - (s/keys :req-un [:internal.page.grid/size - :internal.page.grid/color])) - -(s/def :internal.page.grid/column - (s/keys :req-un [:internal.page.grid/size - :internal.page.grid/color - :internal.page.grid/type - :internal.page.grid/item-length - :internal.page.grid/gutter - :internal.page.grid/margin])) - -(s/def :internal.page.grid/row :internal.page.grid/column) - -(s/def :internal.page.options/background string?) -(s/def :internal.page.options/saved-grids - (s/keys :req-un [:internal.page.grid/square - :internal.page.grid/row - :internal.page.grid/column])) - -(s/def :internal.page/options - (s/keys :opt-un [:internal.page.options/background])) - ;; Size constraints (s/def :internal.shape/constraints-h #{:left :right :leftright :center :scale}) @@ -370,7 +334,7 @@ (s/def ::page (s/keys :req-un [::id ::name - :internal.page/options + ::cto/options :internal.page/objects])) diff --git a/common/src/app/common/types/interactions.cljc b/common/src/app/common/types/interactions.cljc index b793e293b..f186abb16 100644 --- a/common/src/app/common/types/interactions.cljc +++ b/common/src/app/common/types/interactions.cljc @@ -10,9 +10,14 @@ [app.common.spec :as us] [clojure.spec.alpha :as s])) -(s/def ::point - (s/and (s/keys :req-un [::x ::y]) - gpt/point?)) +;; WARNING: options are not deleted when changing event or action type, so it can be +;; restored if the user changes it back later. +;; +;; But that means that an interaction may have for example a delay or +;; destination, even if its type does not require it (but a previous type did). +;; +;; So make sure to use has-delay/has-destination... functions, or similar, +;; before reading them. ;; -- Options depending on event type @@ -54,7 +59,7 @@ :bottom-left :bottom-right :bottom-center}) -(s/def ::overlay-position ::point) +(s/def ::overlay-position ::us/point) (s/def ::url ::us/string) (s/def ::close-click-outside ::us/boolean) (s/def ::background-overlay ::us/boolean) diff --git a/common/src/app/common/types/page_options.cljc b/common/src/app/common/types/page_options.cljc new file mode 100644 index 000000000..db8271693 --- /dev/null +++ b/common/src/app/common/types/page_options.cljc @@ -0,0 +1,94 @@ +;; 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) UXBOX Labs SL + +(ns app.common.types.page-options + (:require + [app.common.data :as d] + [app.common.spec :as us] + [clojure.spec.alpha :as s])) + +;; --- Grid options + +(s/def :artboard-grid.color/value ::us/string) +(s/def :artboard-grid.color/opacity ::us/safe-number) + +(s/def :artboard-grid/size ::us/safe-integer) +(s/def :artboard-grid/color (s/keys :req-un [:artboard-grid.color/value + :artboard-grid.color/opacity])) +(s/def :artboard-grid/type #{:stretch :left :center :right}) +(s/def :artboard-grid/item-length (s/nilable ::us/safe-integer)) +(s/def :artboard-grid/gutter (s/nilable ::us/safe-integer)) +(s/def :artboard-grid/margin (s/nilable ::us/safe-integer)) + +(s/def :artboard-grid/square + (s/keys :req-un [:artboard-grid/size + :artboard-grid/color])) + +(s/def :artboard-grid/column + (s/keys :req-un [:artboard-grid/size + :artboard-grid/color + :artboard-grid/type + :artboard-grid/item-length + :artboard-grid/gutter + :artboard-grid/margin])) + +(s/def :artboard-grid/row :artboard-grid/column) + +(s/def ::saved-grids + (s/keys :req-un [:artboard-grid/square + :artboard-grid/row + :artboard-grid/column])) + +;; --- Background options + +(s/def ::background string?) + +;; --- Flow options + +(s/def :interactions-flow/id ::us/uuid) +(s/def :interactions-flow/name ::us/string) +(s/def :interactions-flow/starting-frame ::us/uuid) + +(s/def ::flow + (s/keys :req-un [:interactions-flow/id + :interactions-flow/name + :interactions-flow/starting-frame])) + +(s/def ::flows + (s/coll-of ::flow :kind vector?)) + +;; --- Options + +(s/def ::options + (s/keys :opt-un [::background + ::saved-grids + ::flows])) + +;; --- Helpers for flow + +(defn rename-flow + [flow name] + (assoc flow :name name)) + +;; --- Helpers for flows + +(defn add-flow + [flows flow] + (conj flows flow)) + +(defn remove-flow + [flows flow-id] + (vec (remove #(= (:id %) flow-id) flows))) + +(defn update-flow + [flows flow-id update-fn] + (let [index (d/index-of-pred flows #(= (:id %) flow-id))] + (update flows index update-fn))) + +(defn get-frame-flow + [flows frame-id] + (d/seek #(= (:starting-frame %) frame-id) flows)) + diff --git a/frontend/resources/styles/main/partials/sidebar-element-options.scss b/frontend/resources/styles/main/partials/sidebar-element-options.scss index 8428d2b3e..a659d469c 100644 --- a/frontend/resources/styles/main/partials/sidebar-element-options.scss +++ b/frontend/resources/styles/main/partials/sidebar-element-options.scss @@ -937,6 +937,10 @@ .element-set-options-group { flex-wrap: wrap; } + + &:not(:first-child) { + border-top: 1px solid $color-gray-60; + } } .exports-options, diff --git a/frontend/resources/styles/main/partials/sidebar-interactions.scss b/frontend/resources/styles/main/partials/sidebar-interactions.scss index 295e59e15..568dd76e1 100644 --- a/frontend/resources/styles/main/partials/sidebar-interactions.scss +++ b/frontend/resources/styles/main/partials/sidebar-interactions.scss @@ -75,7 +75,84 @@ } svg { - width: 18px; height: 18px; + width: 18px; } } + +.flow-element { + display: flex; + align-items: center; + padding: $size-1; + + .element-label { + font-size: $fs11; + } + + .flow-name { + cursor: pointer; + } + + & input.element-name { + background: transparent; + border-color: $color-primary; + color: $color-white; + font-size: $fs11; + } +} + +.flow-button { + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + margin-right: $size-2; + + & svg { + height: 12px; + width: 12px; + fill: $color-gray-20; + } + + &:hover svg { + fill: $color-primary; + } +} + +.flow-badge { + display: flex; + + & .content { + align-items: center; + background-color: $color-gray-50; + border-radius: 4px; + display: flex; + height: 24px; + + & svg { + height: 12px; + margin: 0 $size-2; + width: 12px; + fill: $color-gray-20; + } + + & span { + color: $color-gray-20; + font-size: $fs12; + margin-right: $size-4; + } + } + + &.selected .content { + background-color: $color-primary; + + & svg { + fill: $color-gray-60; + } + + & span { + color: $color-gray-60; + } + } +} + diff --git a/frontend/resources/styles/main/partials/viewer-header.scss b/frontend/resources/styles/main/partials/viewer-header.scss index eca7e84ca..00012e383 100644 --- a/frontend/resources/styles/main/partials/viewer-header.scss +++ b/frontend/resources/styles/main/partials/viewer-header.scss @@ -68,7 +68,7 @@ align-items: center; cursor: pointer; display: flex; - width: 90px; + position: relative; > span { color: $color-gray-10; @@ -96,7 +96,7 @@ } .dropdown { - min-width: 260px; + min-width: 295px; top: 45px; left: -25px; } diff --git a/frontend/resources/styles/main/partials/workspace.scss b/frontend/resources/styles/main/partials/workspace.scss index 97eae7c69..f6d7816d0 100644 --- a/frontend/resources/styles/main/partials/workspace.scss +++ b/frontend/resources/styles/main/partials/workspace.scss @@ -238,10 +238,6 @@ font-size: $fs12; } -.selected .workspace-frame-label { - fill: $color-primary-dark; -} - .multiuser-cursor { align-items: center; display: flex; diff --git a/frontend/src/app/main/data/viewer.cljs b/frontend/src/app/main/data/viewer.cljs index 7220cfc2f..15f39ba1f 100644 --- a/frontend/src/app/main/data/viewer.cljs +++ b/frontend/src/app/main/data/viewer.cljs @@ -111,6 +111,7 @@ (rx/of (df/fonts-fetched fonts) (bundle-fetched (merge bundle params)))))))))) +(declare go-to-frame-auto) (defn bundle-fetched [{:keys [project file share-links libraries users permissions] :as bundle}] @@ -130,7 +131,15 @@ :permissions permissions :project project :pages pages - :file file})))))) + :file file}))) + + ptk/WatchEvent + (watch [_ state _] + (let [route (:route state) + qparams (:query-params route) + index (:index qparams)] + (when (nil? index) + (rx/of (go-to-frame-auto)))))))) (defn fetch-comment-threads [{:keys [file-id page-id] :as params}] @@ -329,6 +338,20 @@ (when index (rx/of (go-to-frame-by-index index))))))) +(defn go-to-frame-auto + [] + (ptk/reify ::go-to-frame-auto + ptk/WatchEvent + (watch [_ state _] + (let [route (:route state) + qparams (:query-params route) + page-id (:page-id qparams) + flows (get-in state [:viewer :pages page-id :options :flows])] + (if (seq flows) + (let [frame-id (:starting-frame (first flows))] + (rx/of (go-to-frame frame-id))) + (rx/of (go-to-frame-by-index 0))))))) + (defn go-to-section [section] (ptk/reify ::go-to-section @@ -391,7 +414,7 @@ :background-overlay background-overlay}) (update-in state [:viewer-local :overlays] (fn [overlays] - (remove #(= (:id (:frame %)) frame-id) overlays)))))))) + (d/removev #(= (:id (:frame %)) frame-id) overlays)))))))) (defn close-overlay [frame-id] @@ -400,7 +423,7 @@ (update [_ state] (update-in state [:viewer-local :overlays] (fn [overlays] - (remove #(= (:id (:frame %)) frame-id) overlays)))))) + (d/removev #(= (:id (:frame %)) frame-id) overlays)))))) ;; --- Objects selection diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 4c8ab2b0d..449bc5956 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -18,7 +18,6 @@ [app.common.pages.spec :as spec] [app.common.spec :as us] [app.common.transit :as t] - [app.common.types.interactions :as cti] [app.common.uuid :as uuid] [app.config :as cfg] [app.main.data.events :as ev] @@ -28,6 +27,7 @@ [app.main.data.workspace.common :as dwc] [app.main.data.workspace.drawing :as dwd] [app.main.data.workspace.groups :as dwg] + [app.main.data.workspace.interactions :as dwi] [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.notifications :as dwn] [app.main.data.workspace.path :as dwdp] @@ -1285,8 +1285,7 @@ (watch [_ state _] (let [{:keys [current-file-id current-page-id]} state pparams {:file-id (or file-id current-file-id)} - qparams {:page-id (or page-id current-page-id) - :index 0}] + qparams {:page-id (or page-id current-page-id)}] (rx/of ::dwp/force-persist (rt/nav-new-window* {:rname :viewer :path-params pparams @@ -1754,166 +1753,12 @@ ;; Interactions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(declare move-edit-interaction) -(declare finish-edit-interaction) - -(defn start-edit-interaction - [index] - (ptk/reify ::start-edit-interaction - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:workspace-local :editing-interaction-index] index)) - - ptk/WatchEvent - (watch [_ state stream] - (let [initial-pos @ms/mouse-position - selected (wsh/lookup-selected state) - stopper (rx/filter ms/mouse-up? stream)] - (when (= 1 (count selected)) - (rx/concat - (->> ms/mouse-position - (rx/take-until stopper) - (rx/map #(move-edit-interaction initial-pos %))) - (rx/of (finish-edit-interaction index initial-pos)))))))) - -(defn move-edit-interaction - [initial-pos position] - (ptk/reify ::move-edit-interaction - ptk/UpdateEvent - (update [_ state] - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - selected-shape-id (-> state wsh/lookup-selected first) - selected-shape (get objects selected-shape-id) - selected-shape-frame-id (:frame-id selected-shape) - start-frame (get objects selected-shape-frame-id) - end-frame (dwc/get-frame-at-point objects position)] - (cond-> state - (not= position initial-pos) (assoc-in [:workspace-local :draw-interaction-to] position) - (not= start-frame end-frame) (assoc-in [:workspace-local :draw-interaction-to-frame] end-frame)))))) - -(defn finish-edit-interaction - [index initial-pos] - (ptk/reify ::finish-edit-interaction - ptk/UpdateEvent - (update [_ state] - (-> state - (assoc-in [:workspace-local :editing-interaction-index] nil) - (assoc-in [:workspace-local :draw-interaction-to] nil) - (assoc-in [:workspace-local :draw-interaction-to-frame] nil))) - - ptk/WatchEvent - (watch [_ state _] - (let [position @ms/mouse-position - page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - frame (dwc/get-frame-at-point objects position) - - shape-id (-> state wsh/lookup-selected first) - shape (get objects shape-id)] - - (when (and shape (not (= position initial-pos))) - (rx/of (dch/update-shapes [shape-id] - (fn [shape] - (update shape :interactions - (fn [interactions] - (if-not frame - ;; Drop in an empty space -> remove interaction - (if index - (into (subvec interactions 0 index) - (subvec interactions (inc index))) - interactions) - (let [frame (if (or (= (:id frame) (:id shape)) - (= (:id frame) (:frame-id shape))) - nil ;; Drop onto self frame -> set destination to none - frame)] - ;; Update or create interaction - (if (and index (cti/has-destination (get interactions index))) - (update interactions index - #(cti/set-destination % (:id frame))) - (conj (or interactions []) - (cti/set-destination cti/default-interaction - (:id frame)))))))))))))))) - -(declare move-overlay-pos) -(declare finish-move-overlay-pos) - -(defn start-move-overlay-pos - [index] - (ptk/reify ::start-move-overlay-pos - ptk/UpdateEvent - (update [_ state] - (-> state - (assoc-in [:workspace-local :move-overlay-to] nil) - (assoc-in [:workspace-local :move-overlay-index] index))) - - ptk/WatchEvent - (watch [_ state stream] - (let [initial-pos @ms/mouse-position - selected (wsh/lookup-selected state) - stopper (rx/filter ms/mouse-up? stream)] - (when (= 1 (count selected)) - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - shape (->> state - wsh/lookup-selected - first - (get objects)) - overlay-pos (-> shape - (get-in [:interactions index]) - :overlay-position) - orig-frame (cph/get-frame shape objects) - frame-pos (gpt/point (:x orig-frame) (:y orig-frame)) - offset (-> initial-pos - (gpt/subtract overlay-pos) - (gpt/subtract frame-pos))] - (rx/concat - (->> ms/mouse-position - (rx/take-until stopper) - (rx/map #(move-overlay-pos % frame-pos offset))) - (rx/of (finish-move-overlay-pos index frame-pos offset))))))))) - -(defn move-overlay-pos - [pos frame-pos offset] - (ptk/reify ::move-overlay-pos - ptk/UpdateEvent - (update [_ state] - (let [pos (-> pos - (gpt/subtract frame-pos) - (gpt/subtract offset))] - (assoc-in state [:workspace-local :move-overlay-to] pos))))) - -(defn finish-move-overlay-pos - [index frame-pos offset] - (ptk/reify ::finish-move-overlay-pos - ptk/UpdateEvent - (update [_ state] - (-> state - (d/dissoc-in [:workspace-local :move-overlay-to]) - (d/dissoc-in [:workspace-local :move-overlay-index]))) - - ptk/WatchEvent - (watch [_ state _] - (let [pos @ms/mouse-position - overlay-pos (-> pos - (gpt/subtract frame-pos) - (gpt/subtract offset)) - - page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - shape (->> state - wsh/lookup-selected - first - (get objects)) - - interactions (:interactions shape) - - new-interactions - (update interactions index - #(cti/set-overlay-position % overlay-pos))] - - (rx/of (update-shape (:id shape) {:interactions new-interactions})))))) - +(d/export dwi/start-edit-interaction) +(d/export dwi/move-edit-interaction) +(d/export dwi/finish-edit-interaction) +(d/export dwi/start-move-overlay-pos) +(d/export dwi/move-overlay-pos) +(d/export dwi/finish-move-overlay-pos) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; CANVAS OPTIONS diff --git a/frontend/src/app/main/data/workspace/common.cljs b/frontend/src/app/main/data/workspace/common.cljs index fc206a538..6cb0af4f7 100644 --- a/frontend/src/app/main/data/workspace/common.cljs +++ b/frontend/src/app/main/data/workspace/common.cljs @@ -12,6 +12,8 @@ [app.common.logging :as log] [app.common.pages :as cp] [app.common.spec :as us] + [app.common.types.interactions :as cti] + [app.common.types.page-options :as cto] [app.common.uuid :as uuid] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.state-helpers :as wsh] @@ -379,8 +381,10 @@ (watch [it state _] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) + options (wsh/lookup-page-options state page-id) ids (cp/clean-loops objects ids) + flows (:flows options) groups-to-unmask (reduce (fn [group-ids id] @@ -399,9 +403,14 @@ interacting-shapes (filter (fn [shape] (let [interactions (:interactions shape)] - (some ids (map :destination interactions)))) + (some #(and (cti/has-destination %) + (contains? ids (:destination %))) + interactions))) (vals objects)) + starting-flows + (filter #(contains? ids (:starting-frame %)) flows) + empty-parents-xform (comp (map (fn [id] (get objects id))) @@ -467,7 +476,8 @@ :operations [{:type :set :attr :interactions :val (vec (remove (fn [interaction] - (contains? ids (:destination interaction))) + (and (cti/has-destination interaction) + (contains? ids (:destination interaction)))) (:interactions obj)))}]}))) mk-mod-int-add-xf (comp (filter some?) @@ -479,6 +489,22 @@ :attr :interactions :val (:interactions obj)}]}))) + mk-mod-del-flow-xf + (comp (filter some?) + (map (fn [flow] + {:type :set-option + :page-id page-id + :option :flows + :value (cto/remove-flow flows (:id flow))}))) + + mk-mod-add-flow-xf + (comp (filter some?) + (map (fn [_] + {:type :set-option + :page-id page-id + :option :flows + :value flows}))) + mk-mod-unmask-xf (comp (filter (partial contains? objects)) (map (fn [id] @@ -508,7 +534,8 @@ :page-id page-id :shapes (vec all-parents)}) (into mk-mod-unmask-xf groups-to-unmask) - (into mk-mod-int-del-xf interacting-shapes)) + (into mk-mod-int-del-xf interacting-shapes) + (into mk-mod-del-flow-xf starting-flows)) uchanges (-> [] @@ -520,8 +547,8 @@ :shapes (vec all-parents)}) (into mk-mod-touched-xf (reverse all-parents)) (into mk-mod-mask-xf groups-to-unmask) - (into mk-mod-int-add-xf interacting-shapes)) - ] + (into mk-mod-int-add-xf interacting-shapes) + (into mk-mod-add-flow-xf starting-flows))] ;; (println "================ rchanges") ;; (cljs.pprint/pprint rchanges) diff --git a/frontend/src/app/main/data/workspace/interactions.cljs b/frontend/src/app/main/data/workspace/interactions.cljs new file mode 100644 index 000000000..179cc03df --- /dev/null +++ b/frontend/src/app/main/data/workspace/interactions.cljs @@ -0,0 +1,284 @@ +;; 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) UXBOX Labs SL + +(ns app.main.data.workspace.interactions + (:require + [app.common.data :as d] + [app.common.geom.point :as gpt] + [app.common.pages.helpers :as cph] + [app.common.spec :as us] + [app.common.types.interactions :as cti] + [app.common.types.page-options :as cto] + [app.common.uuid :as uuid] + [app.main.data.workspace.changes :as dch] + [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.state-helpers :as wsh] + [app.main.streams :as ms] + [beicon.core :as rx] + [potok.core :as ptk])) + +;; --- Flows + +(defn add-flow-selected-frame + [] + (ptk/reify ::add-flow-selected-frame + ptk/WatchEvent + (watch [it state _] + (let [page-id (:current-page-id state) + flows (get-in state [:workspace-data + :pages-index + page-id + :options + :flows] []) + + selected (wsh/lookup-selected state) + + unames (into #{} (map :name flows)) + name (dwc/generate-unique-name unames "Flow-1") + + new-flow {:id (uuid/next) + :name name + :starting-frame (first selected)}] + + (rx/of (dch/commit-changes + {:redo-changes [{:type :set-option + :page-id page-id + :option :flows + :value (cto/add-flow flows new-flow)}] + :undo-changes [{:type :set-option + :page-id page-id + :option :flows + :value flows}] + :origin it})))))) + +(defn remove-flow + [flow-id] + (us/verify ::us/uuid flow-id) + (ptk/reify ::remove-flow + ptk/WatchEvent + (watch [it state _] + (let [page-id (:current-page-id state) + flows (get-in state [:workspace-data + :pages-index + page-id + :options + :flows] [])] + (rx/of (dch/commit-changes + {:redo-changes [{:type :set-option + :page-id page-id + :option :flows + :value (cto/remove-flow flows flow-id)}] + :undo-changes [{:type :set-option + :page-id page-id + :option :flows + :value flows}] + :origin it})))))) + +(defn rename-flow + [flow-id name] + (us/verify ::us/uuid flow-id) + (us/verify ::us/string name) + (ptk/reify ::rename-flow + ptk/WatchEvent + (watch [it state _] + (let [page-id (:current-page-id state) + flows (get-in state [:workspace-data + :pages-index + page-id + :options + :flows] [])] + (rx/of (dch/commit-changes + {:redo-changes [{:type :set-option + :page-id page-id + :option :flows + :value (cto/update-flow flows flow-id + #(cto/rename-flow % name))}] + :undo-changes [{:type :set-option + :page-id page-id + :option :flows + :value flows}] + :origin it})))))) + + +(defn start-rename-flow + [id] + (us/verify ::us/uuid id) + (ptk/reify ::start-rename-flow + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:workspace-local :flow-for-rename] id)))) + +(defn end-rename-flow + [] + (ptk/reify ::end-rename-flow + ptk/UpdateEvent + (update [_ state] + (update state :workspace-local dissoc :flow-for-rename)))) + +;; --- Interactions + +(declare move-edit-interaction) +(declare finish-edit-interaction) + +(defn start-edit-interaction + [index] + (ptk/reify ::start-edit-interaction + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:workspace-local :editing-interaction-index] index)) + + ptk/WatchEvent + (watch [_ state stream] + (let [initial-pos @ms/mouse-position + selected (wsh/lookup-selected state) + stopper (rx/filter ms/mouse-up? stream)] + (when (= 1 (count selected)) + (rx/concat + (->> ms/mouse-position + (rx/take-until stopper) + (rx/map #(move-edit-interaction initial-pos %))) + (rx/of (finish-edit-interaction index initial-pos)))))))) + +(defn move-edit-interaction + [initial-pos position] + (ptk/reify ::move-edit-interaction + ptk/UpdateEvent + (update [_ state] + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + selected-shape-id (-> state wsh/lookup-selected first) + selected-shape (get objects selected-shape-id) + selected-shape-frame-id (:frame-id selected-shape) + start-frame (get objects selected-shape-frame-id) + end-frame (dwc/get-frame-at-point objects position)] + (cond-> state + (not= position initial-pos) (assoc-in [:workspace-local :draw-interaction-to] position) + (not= start-frame end-frame) (assoc-in [:workspace-local :draw-interaction-to-frame] end-frame)))))) + +(defn finish-edit-interaction + [index initial-pos] + (ptk/reify ::finish-edit-interaction + ptk/UpdateEvent + (update [_ state] + (-> state + (assoc-in [:workspace-local :editing-interaction-index] nil) + (assoc-in [:workspace-local :draw-interaction-to] nil) + (assoc-in [:workspace-local :draw-interaction-to-frame] nil))) + + ptk/WatchEvent + (watch [_ state _] + (let [position @ms/mouse-position + page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + frame (dwc/get-frame-at-point objects position) + + shape-id (-> state wsh/lookup-selected first) + shape (get objects shape-id)] + + (when (and shape (not (= position initial-pos))) + (rx/of (dch/update-shapes [shape-id] + (fn [shape] + (update shape :interactions + (fn [interactions] + (if-not frame + ;; Drop in an empty space -> remove interaction + (if index + (into (subvec interactions 0 index) + (subvec interactions (inc index))) + interactions) + (let [frame (if (or (= (:id frame) (:id shape)) + (= (:id frame) (:frame-id shape))) + nil ;; Drop onto self frame -> set destination to none + frame)] + ;; Update or create interaction + (if (and index (cti/has-destination (get interactions index))) + (update interactions index + #(cti/set-destination % (:id frame))) + (conj (or interactions []) + (cti/set-destination cti/default-interaction + (:id frame)))))))))))))))) + +;; --- Overlays + +(declare move-overlay-pos) +(declare finish-move-overlay-pos) + +(defn start-move-overlay-pos + [index] + (ptk/reify ::start-move-overlay-pos + ptk/UpdateEvent + (update [_ state] + (-> state + (assoc-in [:workspace-local :move-overlay-to] nil) + (assoc-in [:workspace-local :move-overlay-index] index))) + + ptk/WatchEvent + (watch [_ state stream] + (let [initial-pos @ms/mouse-position + selected (wsh/lookup-selected state) + stopper (rx/filter ms/mouse-up? stream)] + (when (= 1 (count selected)) + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + shape (->> state + wsh/lookup-selected + first + (get objects)) + overlay-pos (-> shape + (get-in [:interactions index]) + :overlay-position) + orig-frame (cph/get-frame shape objects) + frame-pos (gpt/point (:x orig-frame) (:y orig-frame)) + offset (-> initial-pos + (gpt/subtract overlay-pos) + (gpt/subtract frame-pos))] + (rx/concat + (->> ms/mouse-position + (rx/take-until stopper) + (rx/map #(move-overlay-pos % frame-pos offset))) + (rx/of (finish-move-overlay-pos index frame-pos offset))))))))) + +(defn move-overlay-pos + [pos frame-pos offset] + (ptk/reify ::move-overlay-pos + ptk/UpdateEvent + (update [_ state] + (let [pos (-> pos + (gpt/subtract frame-pos) + (gpt/subtract offset))] + (assoc-in state [:workspace-local :move-overlay-to] pos))))) + +(defn finish-move-overlay-pos + [index frame-pos offset] + (ptk/reify ::finish-move-overlay-pos + ptk/UpdateEvent + (update [_ state] + (-> state + (d/dissoc-in [:workspace-local :move-overlay-to]) + (d/dissoc-in [:workspace-local :move-overlay-index]))) + + ptk/WatchEvent + (watch [_ state _] + (let [pos @ms/mouse-position + overlay-pos (-> pos + (gpt/subtract frame-pos) + (gpt/subtract offset)) + + page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + shape (->> state + wsh/lookup-selected + first + (get objects)) + + interactions (:interactions shape) + + new-interactions + (update interactions index + #(cti/set-overlay-position % overlay-pos))] + + (rx/of (dch/update-shapes [(:id shape)] #(merge % {:interactions new-interactions}))))))) + diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index 54b1e350e..9a939bd49 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -41,8 +41,7 @@ (s/keys :req-un [::file-id])) (s/def ::viewer-query-params - (s/keys :req-un [::index] - :opt-un [::share-id ::section ::page-id])) + (s/keys :opt-un [::index ::share-id ::section ::page-id])) (def routes [["/auth" diff --git a/frontend/src/app/main/ui/viewer/header.cljs b/frontend/src/app/main/ui/viewer/header.cljs index 7d37a3a14..c49f047f1 100644 --- a/frontend/src/app/main/ui/viewer/header.cljs +++ b/frontend/src/app/main/ui/viewer/header.cljs @@ -13,14 +13,14 @@ [app.main.ui.components.fullscreen :as fs] [app.main.ui.icons :as i] [app.main.ui.viewer.comments :refer [comments-menu]] - [app.main.ui.viewer.interactions :refer [interactions-menu]] + [app.main.ui.viewer.interactions :refer [flows-menu interactions-menu]] [app.main.ui.workspace.header :refer [zoom-widget]] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [rumext.alpha :as mf])) (mf/defc header-options - [{:keys [section zoom page file permissions]}] + [{:keys [section zoom page file index permissions]}] (let [fullscreen (mf/use-ctx fs/fullscreen-context) toggle-fullscreen @@ -43,7 +43,9 @@ [:div.options-zone (case section - :interactions [:& interactions-menu] + :interactions [:* + [:& flows-menu {:page page :index index}] + [:& interactions-menu]] :comments [:& comments-menu] [:div.view-options]) @@ -133,7 +135,6 @@ (fn [section] (st/emit! (dv/go-to-section section)))] - [:header.viewer-header [:div.main-icon [:a {:on-click go-to-dashboard @@ -167,5 +168,6 @@ :permissions permissions :page page :file file + :index index :zoom zoom}]])) diff --git a/frontend/src/app/main/ui/viewer/interactions.cljs b/frontend/src/app/main/ui/viewer/interactions.cljs index 1716dc7f8..5b2695d29 100644 --- a/frontend/src/app/main/ui/viewer/interactions.cljs +++ b/frontend/src/app/main/ui/viewer/interactions.cljs @@ -10,6 +10,7 @@ [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.pages :as cp] + [app.common.types.page-options :as cto] [app.main.data.comments :as dcm] [app.main.data.viewer :as dv] [app.main.refs :as refs] @@ -99,6 +100,38 @@ :view-box (:vbox size)}]]]])) +(mf/defc flows-menu + {::mf/wrap [mf/memo]} + [{:keys [page index]}] + (let [flows (get-in page [:options :flows]) + frames (:frames page) + frame (get frames index) + current-flow (cto/get-frame-flow flows (:id frame)) + + show-dropdown? (mf/use-state false) + toggle-dropdown (mf/use-fn #(swap! show-dropdown? not)) + hide-dropdown (mf/use-fn #(reset! show-dropdown? false)) + + select-flow + (mf/use-callback + (fn [flow] + (st/emit! (dv/go-to-frame (:starting-frame flow)))))] + + (when (seq flows) + [:div.view-options {:on-click toggle-dropdown} + [:span.icon i/play] + [:span.label (:name current-flow)] + [:span.icon i/arrow-down] + [:& dropdown {:show @show-dropdown? + :on-close hide-dropdown} + [:ul.dropdown.with-check + (for [flow flows] + [:li {:class (dom/classnames :selected (= (:id flow) (:id current-flow))) + :on-click #(select-flow flow)} + [:span.icon i/tick] + [:span.label (:name flow)]])]]]))) + + (mf/defc interactions-menu [] (let [local (mf/deref refs/viewer-local) diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index 3274be1b0..4f35630cc 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -7,8 +7,10 @@ (ns app.main.ui.workspace.context-menu "A workspace specific context menu (mouse right click)." (:require + [app.common.types.page-options :as cto] [app.main.data.modal :as modal] [app.main.data.workspace :as dw] + [app.main.data.workspace.interactions :as dwi] [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.shortcuts :as sc] [app.main.data.workspace.undo :as dwu] @@ -95,6 +97,11 @@ is-group? (and (some? shape) (= :group (:type shape))) is-bool? (and (some? shape) (= :bool (:type shape))) + options (mf/deref refs/workspace-page-options) + flows (:flows options) + + options-mode (mf/deref refs/options-mode) + set-bool (fn [bool-type] #(cond @@ -122,6 +129,8 @@ do-hide-shape (st/emitf (dw/update-shape-flags id {:hidden true})) do-lock-shape (st/emitf (dw/update-shape-flags id {:blocked true})) do-unlock-shape (st/emitf (dw/update-shape-flags id {:blocked false})) + do-add-flow (st/emitf (dwi/add-flow-selected-frame)) + do-remove-flow #(st/emitf (dwi/remove-flow (:id %))) do-create-group (st/emitf dw/group-selected) do-remove-group (st/emitf dw/ungroup-selected) do-mask-group (st/emitf dw/mask-group) @@ -262,6 +271,14 @@ [:& menu-entry {:title (tr "workspace.shape.menu.lock") :on-click do-lock-shape}]) + (when (and (= options-mode :prototype) (= (:type shape) :frame)) + (let [flow (cto/get-frame-flow flows (:id shape))] + (if (nil? flow) + [:& menu-entry {:title (tr "workspace.shape.menu.flow-start") + :on-click do-add-flow}] + [:& menu-entry {:title (tr "workspace.shape.menu.delete-flow-start") + :on-click (do-remove-flow flow)}]))) + (when (and (or (nil? (:shape-ref shape)) (> (count selected) 1)) (not= (:type shape) :frame)) diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs index f3c51c963..928a3e9d8 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs @@ -19,6 +19,7 @@ [app.util.keyboard :as kbd] [app.util.object :as obj] [app.util.timers :as ts] + [cuerdas.core :as str] [okulary.core :as l] [rumext.alpha :as mf])) @@ -68,7 +69,8 @@ (on-stop-edit) (swap! local assoc :edition false) (st/emit! (dw/end-rename-shape) - (dw/update-shape (:id shape) {:name name})))) + (when-not (str/empty? name) + (dw/update-shape (:id shape) {:name name}))))) cancel-edit (fn [] (on-stop-edit) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs index 14b96b2fb..74360b849 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs @@ -9,14 +9,19 @@ [app.common.data :as d] [app.common.pages :as cp] [app.common.types.interactions :as cti] + [app.common.types.page-options :as cto] [app.common.uuid :as uuid] [app.main.data.workspace :as dw] + [app.main.data.workspace.interactions :as dwi] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.numeric-input :refer [numeric-input]] [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] + [app.util.keyboard :as kbd] + [cuerdas.core :as str] + [okulary.core :as l] [rumext.alpha :as mf])) (defn- event-type-names @@ -68,6 +73,92 @@ :bottom-right (tr "workspace.options.interaction-pos-bottom-right") :bottom-center (tr "workspace.options.interaction-pos-bottom-center")}) +(def flow-for-rename-ref + (l/derived (l/in [:workspace-local :flow-for-rename]) st/state)) + +(mf/defc flow-item + [{:keys [flow]}] + (let [editing? (mf/use-state false) + flow-for-rename (mf/deref flow-for-rename-ref) + name-ref (mf/use-ref) + + start-edit (fn [] + (reset! editing? true)) + + accept-edit (fn [] + (let [name-input (mf/ref-val name-ref) + name (dom/get-value name-input)] + (reset! editing? false) + (st/emit! (dwi/end-rename-flow) + (when-not (str/empty? name) + (dwi/rename-flow (:id flow) name))))) + + cancel-edit (fn [] + (reset! editing? false) + (st/emit! (dwi/end-rename-flow))) + + on-key-down (fn [event] + (when (kbd/enter? event) (accept-edit)) + (when (kbd/esc? event) (cancel-edit)))] + + (mf/use-effect + (fn [] + #(when editing? + (cancel-edit)))) + + (mf/use-effect + (mf/deps flow-for-rename) + #(when (and (= flow-for-rename (:id flow)) + (not @editing?)) + (start-edit))) + + (mf/use-effect + (mf/deps @editing?) + #(when @editing? + (let [name-input (mf/ref-val name-ref)] + (dom/select-text! name-input)) + nil)) + + [:div.flow-element + [:div.flow-button {:on-click (st/emitf (dw/select-shape (:starting-frame flow)))} + i/play] + (if @editing? + [:input.element-name + {:type "text" + :ref name-ref + :on-blur accept-edit + :on-key-down on-key-down + :auto-focus true + :default-value (:name flow "")}] + [:span.element-label.flow-name + {:on-double-click (st/emitf (dwi/start-rename-flow (:id flow)))} + (:name flow)]) + [:div.add-page {:on-click (st/emitf (dwi/remove-flow (:id flow)))} + i/minus]])) + +(mf/defc page-flows + [{:keys [flows]}] + (when (seq flows) + [:div.element-set.interactions-options + [:div.element-set-title + [:span (tr "workspace.options.flows.flow-starts")]] + (for [flow flows] + [:& flow-item {:flow flow :key (str (:id flow))}])])) + +(mf/defc shape-flows + [{:keys [flows shape]}] + (when (= (:type shape) :frame) + (let [flow (cto/get-frame-flow flows (:id shape))] + [:div.element-set.interactions-options + [:div.element-set-title + [:span (tr "workspace.options.flows.flow-start")]] + (if (nil? flow) + [:div.flow-element + [:span.element-label (tr "workspace.options.flows.add-flow-start")] + [:div.add-page {:on-click (st/emitf (dwi/add-flow-selected-frame))} + i/plus]] + [:& flow-item {:flow flow :key (str (:id flow))}])]))) + (mf/defc interaction-entry [{:keys [index shape interaction update-interaction remove-interaction]}] (let [objects (deref refs/workspace-page-objects) @@ -184,7 +275,9 @@ [:select.input-select {:value (str (:destination interaction)) :on-change change-destination} - [:option {:value ""} (tr "workspace.options.interaction-none")] + (if (= (:action-type interaction) :close-overlay) + [:option {:value ""} (tr "workspace.options.interaction-self")] + [:option {:value ""} (tr "workspace.options.interaction-none")]) (for [frame frames] (when (and (not= (:id frame) (:id shape)) ; A frame cannot navigate to itself (not= (:id frame) (:frame-id shape))) ; nor a shape to its container frame @@ -263,6 +356,9 @@ [{:keys [shape] :as props}] (let [interactions (get shape :interactions []) + options (mf/deref refs/workspace-page-options) + flows (:flows options) + add-interaction (fn [_] (let [new-interactions (conj interactions cti/default-interaction)] @@ -280,29 +376,34 @@ (let [new-interactions (update interactions index update-fn)] (st/emit! (dw/update-shape (:id shape) {:interactions new-interactions})))) ] - [:div.element-set.interactions-options - (when shape - [:div.element-set-title - [:span (tr "workspace.options.interactions")] - [:div.add-page {:on-click add-interaction} - i/plus]]) + [:* + (if shape + [:& shape-flows {:flows flows + :shape shape}] + [:& page-flows {:flows flows}]) - [:div.element-set-content - (when (= (count interactions) 0) - [:* - (when shape - [:* - [:div.interactions-help-icon i/plus] - [:div.interactions-help.separator (tr "workspace.options.add-interaction")]]) - [:div.interactions-help-icon i/interaction] - [:div.interactions-help (tr "workspace.options.select-a-shape")] - [:div.interactions-help-icon i/play] - [:div.interactions-help (tr "workspace.options.use-play-button")]])] - [:div.groups - (for [[index interaction] (d/enumerate interactions)] - [:& interaction-entry {:index index - :shape shape - :interaction interaction - :update-interaction update-interaction - :remove-interaction remove-interaction}])]])) + [:div.element-set.interactions-options + (when shape + [:div.element-set-title + [:span (tr "workspace.options.interactions")] + [:div.add-page {:on-click add-interaction} + i/plus]]) + [:div.element-set-content + (when (= (count interactions) 0) + [:* + (when shape + [:* + [:div.interactions-help-icon i/plus] + [:div.interactions-help.separator (tr "workspace.options.add-interaction")]]) + [:div.interactions-help-icon i/interaction] + [:div.interactions-help (tr "workspace.options.select-a-shape")] + [:div.interactions-help-icon i/play] + [:div.interactions-help (tr "workspace.options.use-play-button")]])] + [:div.groups + (for [[index interaction] (d/enumerate interactions)] + [:& interaction-entry {:index index + :shape shape + :interaction interaction + :update-interaction update-interaction + :remove-interaction remove-interaction}])]]])) diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index e99495961..34c72e50a 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -159,13 +159,9 @@ (hooks/setup-shortcuts node-editing? drawing-path?) (hooks/setup-active-frames objects vbox hover active-frames) - - [:div.viewport [:div.viewport-overlays - - [:& wtr/frame-renderer {:objects objects :background background}] @@ -273,6 +269,17 @@ :on-frame-leave on-frame-leave :on-frame-select on-frame-select}] + (when show-prototypes? + [:& widgets/frame-flows + {:flows (:flows options) + :objects objects + :selected selected + :zoom zoom + :modifiers modifiers + :on-frame-enter on-frame-enter + :on-frame-leave on-frame-leave + :on-frame-select on-frame-select}]) + (when show-gradient-handlers? [:& gradients/gradient-handlers {:id (first selected) diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs index 9ca6c54a1..9d7f54ed4 100644 --- a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs @@ -10,10 +10,12 @@ [app.common.geom.shapes :as gsh] [app.common.pages :as cp] [app.main.data.workspace :as dw] + [app.main.data.workspace.interactions :as dwi] [app.main.refs :as refs] [app.main.store :as st] [app.main.streams :as ms] [app.main.ui.hooks :as hooks] + [app.main.ui.icons :as i] [app.main.ui.workspace.viewport.path-actions :refer [path-actions]] [app.util.dom :as dom] [rumext.alpha :as mf])) @@ -90,7 +92,7 @@ (mf/defc frame-title [{:keys [frame modifiers selected? zoom on-frame-enter on-frame-leave on-frame-select]}] (let [{:keys [width x y]} (gsh/transform-shape frame) - label-pos (gpt/point x (- y (/ 10 zoom))) + label-pos (gpt/point x (- y (/ 10 zoom))) on-mouse-down (mf/use-callback @@ -156,3 +158,75 @@ :on-frame-enter on-frame-enter :on-frame-leave on-frame-leave :on-frame-select on-frame-select}])])) + +(mf/defc frame-flow + [{:keys [flow frame modifiers selected? zoom on-frame-enter on-frame-leave on-frame-select]}] + (let [{:keys [x y]} (gsh/transform-shape frame) + flow-pos (gpt/point x (- y (/ 35 zoom))) + + on-mouse-down + (mf/use-callback + (mf/deps (:id frame) on-frame-select) + (fn [bevent] + (let [event (.-nativeEvent bevent)] + (when (= 1 (.-which event)) + (dom/prevent-default event) + (dom/stop-propagation event) + (on-frame-select event (:id frame)))))) + + on-double-click + (mf/use-callback + (mf/deps (:id frame)) + (st/emitf (dwi/start-rename-flow (:id flow)))) + + on-pointer-enter + (mf/use-callback + (mf/deps (:id frame) on-frame-enter) + (fn [_] + (on-frame-enter (:id frame)))) + + on-pointer-leave + (mf/use-callback + (mf/deps (:id frame) on-frame-leave) + (fn [_] + (on-frame-leave (:id frame))))] + + [:foreignObject {:x 0 + :y -15 + :width 100000 + :height 24 + :transform (str (when (and selected? modifiers) + (str (:displacement modifiers) " " )) + (text-transform flow-pos zoom))} + [:div.flow-badge {:class (dom/classnames :selected selected?)} + [:div.content {:on-mouse-down on-mouse-down + :on-double-click on-double-click + :on-pointer-enter on-pointer-enter + :on-pointer-leave on-pointer-leave} + i/play + [:span (:name flow)]]]])) + +(mf/defc frame-flows + {::mf/wrap-props false} + [props] + (let [flows (unchecked-get props "flows") + objects (unchecked-get props "objects") + zoom (unchecked-get props "zoom") + modifiers (unchecked-get props "modifiers") + selected (or (unchecked-get props "selected") #{}) + + on-frame-enter (unchecked-get props "on-frame-enter") + on-frame-leave (unchecked-get props "on-frame-leave") + on-frame-select (unchecked-get props "on-frame-select")] + [:g.frame-flows + (for [flow flows] + (let [frame (get objects (:starting-frame flow))] + [:& frame-flow {:flow flow + :frame frame + :selected? (contains? selected (:id frame)) + :zoom zoom + :modifiers modifiers + :on-frame-enter on-frame-enter + :on-frame-leave on-frame-leave + :on-frame-select on-frame-select}]))])) + diff --git a/frontend/translations/en.po b/frontend/translations/en.po index b70e40c3b..66e0c966e 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -2422,6 +2422,18 @@ msgstr "Rotation" msgid "workspace.options.add-interaction" msgstr "Click the + button to add interactions." +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.flows.add-flow-start" +msgstr "Add flow start" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.flows.flow-start" +msgstr "Flow start" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.flows.flow-starts" +msgstr "Flow starts" + #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.interaction-action" msgstr "Action" @@ -2857,6 +2869,10 @@ msgstr "Cut" msgid "workspace.shape.menu.delete" msgstr "Delete" +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.delete-flow-start" +msgstr "Delete flow start" + #: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.detach-instance" msgstr "Detach instance" @@ -2877,6 +2893,10 @@ msgstr "Flip horizontal" msgid "workspace.shape.menu.flip-vertical" msgstr "Flip vertical" +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.flow-start" +msgstr "Flow start" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.forward" msgstr "Bring forward" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 1eb849089..a0a538317 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -2308,6 +2308,18 @@ msgstr "Rotación" msgid "workspace.options.add-interaction" msgstr "Pulsa el botón + para añadir interacciones." +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.flows.add-flow-start" +msgstr "Añadir inicio de flujo" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.flows.flow-start" +msgstr "Inicio de flujo" + +#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs +msgid "workspace.options.flows.flow-starts" +msgstr "Inicios de flujo" + #: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs msgid "workspace.options.interaction-action" msgstr "Acción" @@ -2745,6 +2757,10 @@ msgstr "Cortar" msgid "workspace.shape.menu.delete" msgstr "Eliminar" +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.delete-flow-start" +msgstr "Eliminar inicio de flujo" + #: src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/sidebar/options/menus/component.cljs, src/app/main/ui/workspace/context_menu.cljs, src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.detach-instance" msgstr "Desacoplar instancia" @@ -2765,6 +2781,10 @@ msgstr "Voltear horizontal" msgid "workspace.shape.menu.flip-vertical" msgstr "Voltear vertical" +#: src/app/main/ui/workspace/context_menu.cljs +msgid "workspace.shape.menu.flow-start" +msgstr "Inicio de flujo" + #: src/app/main/ui/workspace/context_menu.cljs msgid "workspace.shape.menu.forward" msgstr "Mover hacia delante"