mirror of
https://github.com/penpot/penpot.git
synced 2026-06-19 05:42:08 +00:00
🐛 Fix problem with plugins creating interactions always added a new flow (#10231)
This commit is contained in:
parent
6b50e2d822
commit
75e23cb9a3
@ -129,6 +129,20 @@
|
||||
(or (some ctsi/flow-origin? (map :interactions children))
|
||||
(some #(ctsi/flow-to? % frame-id) (map :interactions (vals objects))))))
|
||||
|
||||
(defn- new-flow-event
|
||||
"Returns an `add-flow` event that creates an implicit flow for the root
|
||||
frame containing `shape-id`, or nil when that frame is already part of a
|
||||
flow. Mirrors the editor behavior of creating a flow when an interaction
|
||||
is added outside of any existing flow."
|
||||
[state page-id shape-id]
|
||||
(let [page (dsh/lookup-page state page-id)
|
||||
objects (get page :objects)
|
||||
frame (cfh/get-root-frame objects shape-id)
|
||||
flow (ctp/get-frame-flow (get page :flows) (:id frame))]
|
||||
(when (and (not (connected-frame? objects (:id frame)))
|
||||
(nil? flow))
|
||||
(add-flow (:id frame)))))
|
||||
|
||||
(defn add-interaction
|
||||
[page-id shape-id interaction]
|
||||
(ptk/reify ::add-interaction
|
||||
@ -143,7 +157,9 @@
|
||||
(when (:destination interaction)
|
||||
(dwsh/update-shapes [(:destination interaction)]
|
||||
cls/show-in-viewer
|
||||
{:page-id page-id})))))))
|
||||
{:page-id page-id}))
|
||||
(when (ctsi/flow-origin? [interaction])
|
||||
(new-flow-event state page-id shape-id)))))))
|
||||
|
||||
(defn add-new-interaction
|
||||
([shape] (add-new-interaction shape nil))
|
||||
@ -154,11 +170,8 @@
|
||||
(let [page-id (:current-page-id state)
|
||||
page (dsh/lookup-page state page-id)
|
||||
objects (get page :objects)
|
||||
frame (cfh/get-root-frame objects (:id shape))
|
||||
|
||||
first? (not-any? #(seq (:interactions %)) (vals objects))
|
||||
flows (get page :flows)
|
||||
flow (ctp/get-frame-flow flows (:id frame))]
|
||||
first? (not-any? #(seq (:interactions %)) (vals objects))]
|
||||
(rx/concat
|
||||
(rx/of (dwsh/update-shapes
|
||||
[(:id shape)]
|
||||
@ -171,9 +184,8 @@
|
||||
(when destination
|
||||
(rx/of (dwsh/update-shapes [destination] cls/show-in-viewer)))
|
||||
|
||||
(when (and (not (connected-frame? objects (:id frame)))
|
||||
(nil? flow))
|
||||
(rx/of (add-flow (:id frame))))
|
||||
(when-let [flow-event (new-flow-event state page-id (:id shape))]
|
||||
(rx/of flow-event))
|
||||
(if first?
|
||||
;; When the first interaction of the page is created we emit the event "create-prototype"
|
||||
(rx/of (ev/event {::ev/name "create-prototype"}))
|
||||
|
||||
@ -0,0 +1,119 @@
|
||||
;; 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 Sucursal en España SL
|
||||
|
||||
(ns frontend-tests.data.workspace-interactions-test
|
||||
(:require
|
||||
[app.common.test-helpers.compositions :as ctho]
|
||||
[app.common.test-helpers.files :as cthf]
|
||||
[app.common.test-helpers.ids-map :as cthi]
|
||||
[app.common.test-helpers.shapes :as cths]
|
||||
[app.common.types.shape.interactions :as ctsi]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.workspace.interactions :as dwi]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[frontend-tests.helpers.state :as ths]))
|
||||
|
||||
(t/use-fixtures :each
|
||||
{:before cthi/reset-idmap!})
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Helpers
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defn- make-file
|
||||
"A file with two boards (frames)."
|
||||
[]
|
||||
(-> (cthf/sample-file :file1)
|
||||
(ctho/add-frame :board1 :name "Board 1")
|
||||
(ctho/add-frame :board2 :name "Board 2")))
|
||||
|
||||
(defn- navigate-interaction
|
||||
"A navigate interaction from `origin-id` to `dest-id` (a flow origin)."
|
||||
[origin-id dest-id]
|
||||
(-> ctsi/default-interaction
|
||||
(ctsi/set-destination dest-id)
|
||||
(assoc :position-relative-to origin-id)))
|
||||
|
||||
(defn- page-flows
|
||||
"The flows map of the current page in `state`."
|
||||
[state]
|
||||
(let [file-id (:current-file-id state)
|
||||
page-id (:current-page-id state)]
|
||||
(get-in state [:files file-id :data :pages-index page-id :flows])))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; add-interaction (plugin API path): implicit flow creation must match the
|
||||
;; editor behavior of creating a flow when an interaction is added outside of
|
||||
;; any existing flow.
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest add-interaction-creates-implicit-flow-outside-flow
|
||||
(t/async
|
||||
done
|
||||
(let [file (make-file)
|
||||
board1-id (:id (cths/get-shape file :board1))
|
||||
board2-id (:id (cths/get-shape file :board2))
|
||||
page-id (cthf/current-page-id file)
|
||||
interaction (navigate-interaction board1-id board2-id)
|
||||
store (ths/setup-store file)
|
||||
events [(dwi/add-interaction page-id board1-id interaction)]]
|
||||
|
||||
(ths/run-store
|
||||
store done events
|
||||
(fn [new-state]
|
||||
(let [flows (vals (page-flows new-state))]
|
||||
(t/is (= 1 (count flows))
|
||||
"an implicit flow is created for the origin board")
|
||||
(t/is (= board1-id (:starting-frame (first flows)))
|
||||
"the implicit flow starts at the origin board")))))))
|
||||
|
||||
(t/deftest add-interaction-does-not-duplicate-flow-for-same-frame
|
||||
(t/async
|
||||
done
|
||||
(let [file (make-file)
|
||||
board1-id (:id (cths/get-shape file :board1))
|
||||
board2-id (:id (cths/get-shape file :board2))
|
||||
page-id (cthf/current-page-id file)
|
||||
flow-id (uuid/next)
|
||||
;; board1 is already the starting frame of an explicit flow
|
||||
file (assoc-in file
|
||||
[:data :pages-index page-id :flows flow-id]
|
||||
{:id flow-id
|
||||
:name "My flow"
|
||||
:starting-frame board1-id})
|
||||
|
||||
interaction (navigate-interaction board1-id board2-id)
|
||||
store (ths/setup-store file)
|
||||
events [(dwi/add-interaction page-id board1-id interaction)]]
|
||||
|
||||
(ths/run-store
|
||||
store done events
|
||||
(fn [new-state]
|
||||
(let [flows (vals (page-flows new-state))]
|
||||
(t/is (= 1 (count flows))
|
||||
"no duplicate flow is created when the frame is already in a flow")
|
||||
(t/is (= "My flow" (:name (first flows)))
|
||||
"the existing explicit flow is preserved")))))))
|
||||
|
||||
(t/deftest add-interaction-without-destination-does-not-create-flow
|
||||
(t/async
|
||||
done
|
||||
(let [file (make-file)
|
||||
board1-id (:id (cths/get-shape file :board1))
|
||||
page-id (cthf/current-page-id file)
|
||||
;; An open-url interaction is not a flow origin (no destination)
|
||||
interaction (-> ctsi/default-interaction
|
||||
(assoc :action-type :open-url
|
||||
:url "https://example.com"
|
||||
:destination nil))
|
||||
store (ths/setup-store file)
|
||||
events [(dwi/add-interaction page-id board1-id interaction)]]
|
||||
|
||||
(ths/run-store
|
||||
store done events
|
||||
(fn [new-state]
|
||||
(t/is (empty? (page-flows new-state))
|
||||
"non flow-origin interactions do not create a flow"))))))
|
||||
82
frontend/test/frontend_tests/plugins/interactions_test.cljs
Normal file
82
frontend/test/frontend_tests/plugins/interactions_test.cljs
Normal file
@ -0,0 +1,82 @@
|
||||
;; 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 Sucursal en España SL
|
||||
|
||||
(ns frontend-tests.plugins.interactions-test
|
||||
(:require
|
||||
[app.common.test-helpers.files :as cthf]
|
||||
[app.main.store :as st]
|
||||
[app.plugins.api :as api]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[frontend-tests.helpers.state :as ths]
|
||||
[frontend-tests.helpers.wasm :as thw]))
|
||||
|
||||
(defn- flows-of
|
||||
"The vals of the current page flows from the store."
|
||||
[store ^js context ^js page]
|
||||
(let [file-id (aget (. context -currentFile) "$id")
|
||||
page-id (aget page "$id")]
|
||||
(vals (get-in @store [:files file-id :data :pages-index page-id :flows]))))
|
||||
|
||||
(t/deftest add-interaction-creates-implicit-flow
|
||||
(thw/with-wasm-mocks*
|
||||
(fn []
|
||||
(let [store (ths/setup-store (cthf/sample-file :file1 :page-label :page1))
|
||||
^js context (api/create-context "00000000-0000-0000-0000-000000000000")
|
||||
_ (set! st/state store)
|
||||
^js page (. context -currentPage)
|
||||
^js board1 (.createBoard context)
|
||||
^js board2 (.createBoard context)]
|
||||
|
||||
(t/testing "a page has no flows before any interaction is added"
|
||||
(t/is (empty? (flows-of store context page))))
|
||||
|
||||
(t/testing "addInteraction outside a flow creates an implicit flow"
|
||||
(.addInteraction board1 "click" #js {:type "navigate-to" :destination board2})
|
||||
(let [flows (flows-of store context page)]
|
||||
(t/is (= 1 (count flows))
|
||||
"an implicit flow is created for the origin board")
|
||||
(t/is (= (aget board1 "$id") (:starting-frame (first flows)))
|
||||
"the implicit flow starts at the origin board")))
|
||||
|
||||
(t/testing "adding another interaction from the same board does not duplicate the flow"
|
||||
(.addInteraction board1 "click" #js {:type "navigate-to" :destination board2})
|
||||
(t/is (= 1 (count (flows-of store context page)))
|
||||
"no duplicate flow is created"))))))
|
||||
|
||||
(t/deftest add-interaction-does-not-duplicate-explicit-flow
|
||||
(thw/with-wasm-mocks*
|
||||
(fn []
|
||||
(let [store (ths/setup-store (cthf/sample-file :file1 :page-label :page1))
|
||||
^js context (api/create-context "00000000-0000-0000-0000-000000000000")
|
||||
_ (set! st/state store)
|
||||
^js page (. context -currentPage)
|
||||
^js board1 (.createBoard context)
|
||||
^js board2 (.createBoard context)]
|
||||
|
||||
;; board1 is already the starting frame of an explicitly created flow
|
||||
(.createFlow page "My flow" board1)
|
||||
|
||||
(t/testing "addInteraction from a board already in a flow keeps a single flow"
|
||||
(.addInteraction board1 "click" #js {:type "navigate-to" :destination board2})
|
||||
(let [flows (flows-of store context page)]
|
||||
(t/is (= 1 (count flows))
|
||||
"no duplicate flow is created alongside the explicit one")
|
||||
(t/is (= "My flow" (:name (first flows)))
|
||||
"the explicit flow is preserved")))))))
|
||||
|
||||
(t/deftest add-interaction-without-destination-does-not-create-flow
|
||||
(thw/with-wasm-mocks*
|
||||
(fn []
|
||||
(let [store (ths/setup-store (cthf/sample-file :file1 :page-label :page1))
|
||||
^js context (api/create-context "00000000-0000-0000-0000-000000000000")
|
||||
_ (set! st/state store)
|
||||
^js page (. context -currentPage)
|
||||
^js board1 (.createBoard context)]
|
||||
|
||||
(t/testing "a non flow-origin interaction (open-url) creates no flow"
|
||||
(.addInteraction board1 "click" #js {:type "open-url" :url "https://example.com"})
|
||||
(t/is (empty? (flows-of store context page))
|
||||
"open-url interactions do not create a flow"))))))
|
||||
@ -12,6 +12,7 @@
|
||||
[frontend-tests.data.uploads-test]
|
||||
[frontend-tests.data.viewer-test]
|
||||
[frontend-tests.data.workspace-colors-test]
|
||||
[frontend-tests.data.workspace-interactions-test]
|
||||
[frontend-tests.data.workspace-mcp-test]
|
||||
[frontend-tests.data.workspace-media-test]
|
||||
[frontend-tests.data.workspace-shortcuts-test]
|
||||
@ -27,6 +28,7 @@
|
||||
[frontend-tests.logic.pasting-in-containers-test]
|
||||
[frontend-tests.main-errors-test]
|
||||
[frontend-tests.plugins.context-shapes-test]
|
||||
[frontend-tests.plugins.interactions-test]
|
||||
[frontend-tests.plugins.page-active-validation-test]
|
||||
[frontend-tests.plugins.page-test]
|
||||
[frontend-tests.plugins.parser-test]
|
||||
@ -67,6 +69,7 @@
|
||||
frontend-tests.data.uploads-test
|
||||
frontend-tests.data.viewer-test
|
||||
frontend-tests.data.workspace-colors-test
|
||||
frontend-tests.data.workspace-interactions-test
|
||||
frontend-tests.data.workspace-mcp-test
|
||||
frontend-tests.data.workspace-media-test
|
||||
frontend-tests.data.workspace-shortcuts-test
|
||||
@ -81,6 +84,7 @@
|
||||
frontend-tests.logic.pasting-in-containers-test
|
||||
frontend-tests.plugins.context-shapes-test
|
||||
frontend-tests.plugins.page-active-validation-test
|
||||
frontend-tests.plugins.interactions-test
|
||||
frontend-tests.plugins.page-test
|
||||
frontend-tests.plugins.parser-test
|
||||
frontend-tests.plugins.tokens-test
|
||||
|
||||
5
plugins/libs/plugin-types/index.d.ts
vendored
5
plugins/libs/plugin-types/index.d.ts
vendored
@ -3969,6 +3969,11 @@ export interface ShapeBase extends PluginData {
|
||||
|
||||
/**
|
||||
* Adds a new interaction to the shape.
|
||||
*
|
||||
* If the interaction starts a flow (for example a `navigate-to` action) and
|
||||
* the shape's board is not already part of any flow, a new flow starting at
|
||||
* that board is created automatically, matching the behavior of the editor.
|
||||
*
|
||||
* @param trigger defines the conditions under which the action will be triggered
|
||||
* @param action defines what will be executed when the trigger happens
|
||||
* @param delay for the type of trigger `after-delay` will specify the time after triggered. Ignored otherwise.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user