🐛 Fix problem with plugins creating interactions always added a new flow (#10231)

This commit is contained in:
Alonso Torres 2026-06-18 18:00:19 +02:00 committed by GitHub
parent 6b50e2d822
commit 75e23cb9a3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 230 additions and 8 deletions

View File

@ -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"}))

View File

@ -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"))))))

View 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"))))))

View File

@ -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

View File

@ -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.