mirror of
https://github.com/penpot/penpot.git
synced 2026-10-29 05:50:35 +00:00
Merge remote-tracking branch 'origin/juan-compare-conflicts-modal' into iop-vc-preview
This commit is contained in:
commit
90525f91a8
@ -783,11 +783,22 @@
|
||||
(sub-of ours pid :guides)
|
||||
{:kind :page-guide :page-id pid}))
|
||||
common)
|
||||
;; an added or deleted flow carries itself whole as `:flow`, as
|
||||
;; for shapes: the compare panel names the board it starts on,
|
||||
;; which the open file may lack
|
||||
flows-diffs (map (fn [pid]
|
||||
(three-way-entities (sub-of base pid :flows)
|
||||
(sub-of theirs pid :flows)
|
||||
(sub-of ours pid :flows)
|
||||
{:kind :page-flow :page-id pid}))
|
||||
(let [bf (sub-of base pid :flows)
|
||||
of (sub-of ours pid :flows)]
|
||||
(update (three-way-entities bf
|
||||
(sub-of theirs pid :flows)
|
||||
of
|
||||
{:kind :page-flow :page-id pid})
|
||||
:changes
|
||||
(partial mapv (fn [e]
|
||||
(case (:status e)
|
||||
:added (assoc e :flow (get of (:id e)))
|
||||
:deleted (assoc e :flow (get bf (:id e)))
|
||||
e))))))
|
||||
common)
|
||||
;; default-grids per common page (mergeable: :page-grid -> :set-default-grid)
|
||||
grids-diffs (map (fn [pid]
|
||||
@ -839,6 +850,19 @@
|
||||
:base (get bo (:id e))
|
||||
:main (get to (:id e))
|
||||
:branch (get oo (:id e))))
|
||||
;; an added or deleted shape is missing from
|
||||
;; one of the two documents, and the compare
|
||||
;; panel may have that one open: the entry
|
||||
;; carries the FULL shape as the side that
|
||||
;; holds it has it, so the panel can draw it
|
||||
;; and say what it is. `:shape` names no side,
|
||||
;; so `doc-sides` leaves it alone
|
||||
with-shape (fn [e]
|
||||
(case (:status e)
|
||||
:added (assoc e :shape (get oo (:id e)))
|
||||
:deleted (assoc e :shape (or (get bo (:id e))
|
||||
(get to (:id e))))
|
||||
e))
|
||||
passed (tree-policy-pass res bo to oo sbo sto reorders pid modes)]
|
||||
{:changes (mapv (fn [e]
|
||||
;; a `:refuse` entry names the
|
||||
@ -847,7 +871,7 @@
|
||||
;; display
|
||||
(if (= :unsupported (:status e))
|
||||
e
|
||||
(enrich e)))
|
||||
(-> e enrich with-shape)))
|
||||
(:changes passed))
|
||||
:conflicts (mapv (comp rehydrate enrich) (:conflicts passed))}))
|
||||
common)]
|
||||
|
||||
@ -154,6 +154,42 @@
|
||||
(t/is (= :deleted (:status c)))
|
||||
(t/is (= :s2 (:id c)))))
|
||||
|
||||
(t/deftest added-and-deleted-carry-the-shape
|
||||
;; the open file lacks the shape on one side, so the entry carries it
|
||||
;; whole, as the document that holds it has it, in both directions
|
||||
(let [base (mkdata {:s1 {:id :s1 :name "A" :selrect {:x 1}}
|
||||
:s2 {:id :s2 :name "B" :selrect {:x 2}}})
|
||||
branch (mkdata {:s1 {:id :s1 :name "A" :selrect {:x 1}}
|
||||
:s3 {:id :s3 :name "C" :selrect {:x 3}}})
|
||||
merge (into {} (map (juxt :id identity))
|
||||
(:changes (bm/compute-merge base base branch :branch->main)))
|
||||
update (into {} (map (juxt :id identity))
|
||||
(:changes (bm/compute-merge base branch base :main->branch)))]
|
||||
(t/is (= {:id :s2 :name "B" :selrect {:x 2}} (get-in merge [:s2 :shape])))
|
||||
(t/is (= {:id :s3 :name "C" :selrect {:x 3}} (get-in merge [:s3 :shape])))
|
||||
(t/is (= {:id :s2 :name "B" :selrect {:x 2}} (get-in update [:s2 :shape])))
|
||||
(t/is (= {:id :s3 :name "C" :selrect {:x 3}} (get-in update [:s3 :shape])))))
|
||||
|
||||
(t/deftest a-deleted-flow-carries-the-board-it-starts-on
|
||||
(let [flow {:id :f1 :name "Onboarding" :starting-frame :b1}
|
||||
base (assoc-in (mkdata {:b1 {:id :b1 :name "Home"}})
|
||||
[:pages-index :p1 :flows] {:f1 flow})
|
||||
branch (assoc-in base [:pages-index :p1 :flows] {})
|
||||
merge (first (filter #(= :page-flow (:kind %))
|
||||
(:changes (bm/compute-merge base base branch :branch->main))))
|
||||
update (first (filter #(= :page-flow (:kind %))
|
||||
(:changes (bm/compute-merge base branch base :main->branch))))]
|
||||
(t/is (= :deleted (:status merge)))
|
||||
(t/is (= flow (:flow merge)))
|
||||
(t/is (= :deleted (:status update)))
|
||||
(t/is (= flow (:flow update)))))
|
||||
|
||||
(t/deftest modified-carries-no-shape
|
||||
(let [base (mkdata {:s1 {:id :s1 :name "A" :fill "red"}})
|
||||
branch (mkdata {:s1 {:id :s1 :name "A" :fill "blue"}})
|
||||
r (bm/compute-merge base base branch :branch->main)]
|
||||
(t/is (not (contains? (first (:changes r)) :shape)))))
|
||||
|
||||
(t/deftest fast-forward-main-untouched
|
||||
;; main == base; branch changed -> all branch changes apply cleanly
|
||||
(let [base (mkdata {:s1 {:id :s1 :name "A" :fill "red"}})
|
||||
|
||||
@ -191,11 +191,15 @@
|
||||
(defn- fmt-number
|
||||
"At most two decimals, trailing zeros stripped (89.99999 -> \"90\",
|
||||
439.4999 -> \"439.5\", 386 -> \"386\"), grouped and punctuated the way
|
||||
the active locale writes numbers (\"439,5\" in Spanish)."
|
||||
the active locale writes numbers (\"439,5\" in Spanish). It goes
|
||||
through `Intl.NumberFormat`: once a plugin has loaded, the SES lockdown
|
||||
turns `Number.prototype.toLocaleString` into `toString`, options and
|
||||
all."
|
||||
[v]
|
||||
(let [locale (-> i18n/*current-locale*
|
||||
(str/replace "_" "-"))]
|
||||
(.toLocaleString v locale #js {:maximumFractionDigits 2})))
|
||||
(let [locale (or (some-> i18n/*current-locale*
|
||||
(str/replace "_" "-"))
|
||||
js/undefined)]
|
||||
(.format (js/Intl.NumberFormat. locale #js {:maximumFractionDigits 2}) v)))
|
||||
|
||||
(defn- short-str
|
||||
[v]
|
||||
@ -384,6 +388,133 @@
|
||||
(or frame-id (:parent-id shape))))
|
||||
(inc depth)))))))
|
||||
|
||||
(defn- cascade-entry?
|
||||
"An added or deleted layer that carries its shape: one that can take
|
||||
the layers added or deleted with it."
|
||||
[item]
|
||||
(and (= :shape (:kind item))
|
||||
(contains? #{:added :deleted} (:status item))
|
||||
(some? (:shape item))))
|
||||
|
||||
(defn fold-cascades
|
||||
"The added or deleted layers that only follow a container added or
|
||||
deleted with them, folded under the outermost one: deleting a group
|
||||
deletes its layers too, and listing each one apart would repeat the
|
||||
same change. Takes `[index item]` pairs and returns a map from the
|
||||
index of each outermost entry to the `[index item]` pairs it holds."
|
||||
[indexed]
|
||||
(let [by-id (into {}
|
||||
(keep (fn [[idx item]]
|
||||
(when (cascade-entry? item)
|
||||
[[(:page-id item) (:id item)] [idx item]])))
|
||||
indexed)
|
||||
root-of (fn [item]
|
||||
(loop [cur item root nil depth 0]
|
||||
(let [[pidx parent] (get by-id [(:page-id item) (get-in cur [:shape :parent-id])])]
|
||||
(if (and parent (= (:status parent) (:status item)) (< depth 64))
|
||||
(recur parent pidx (inc depth))
|
||||
root))))]
|
||||
(reduce (fn [acc [_ item :as row]]
|
||||
(if-let [root (when (cascade-entry? item) (root-of item))]
|
||||
(update acc root (fnil conj []) row)
|
||||
acc))
|
||||
{}
|
||||
indexed)))
|
||||
|
||||
;; --- What goes with an added or deleted layer
|
||||
|
||||
(defn- lost-interactions
|
||||
"The interactions of modified entry `entry` that lead into `ids` on the
|
||||
`from` side and are gone on the `to` side."
|
||||
[entry ids from to]
|
||||
(let [values (get-in entry [:changed-attrs :interactions])
|
||||
kept (set (get values to))]
|
||||
(filterv #(and (contains? ids (:destination %)) (not (contains? kept %)))
|
||||
(get values from))))
|
||||
|
||||
(defn- size-of
|
||||
"Width and height of `shape` on `side`, taking the changed ones from
|
||||
diff entry `entry`."
|
||||
[shape entry side]
|
||||
(let [attrs (:changed-attrs entry)]
|
||||
[(get-in attrs [:width side] (:width shape))
|
||||
(get-in attrs [:height side] (:height shape))]))
|
||||
|
||||
(defn shape-effects
|
||||
"What else changes with the added or deleted layer `item`, read from
|
||||
the other entries of the diff (`items`):
|
||||
|
||||
`:interactions` the layers whose prototype interactions led into it
|
||||
and go with it (`{:id :label :count}`);
|
||||
`:flows` the flows that started on it and go with it (names);
|
||||
`:layouts` the boards with a layout around it that resize
|
||||
(`{:id :name :layout :from [w h] :to [w h]}`) or
|
||||
whose other layers move (`{... :moved n}`).
|
||||
|
||||
Penpot makes these changes itself when a layer goes, so the diff lists
|
||||
them as entries of their own; this ties them to their cause. `objects`
|
||||
is the page as the side that holds the layer has it, `inner` the
|
||||
layers folded under it (`fold-cascades`), and `incoming` says the
|
||||
changes are main's, going to the branch, rather than the branch's
|
||||
going to main."
|
||||
[item items objects inner incoming]
|
||||
(let [from (if incoming :branch :main)
|
||||
to (if incoming :main :branch)
|
||||
deleted? (= :deleted (:status item))
|
||||
ids (into #{(:id item)} (map (comp :id second)) inner)
|
||||
page-id (:page-id item)
|
||||
on-page (filterv #(= page-id (:page-id %)) items)
|
||||
modified (into {}
|
||||
(comp (filter #(and (= :shape (:kind %)) (= :modified (:status %))))
|
||||
(map (juxt :id identity)))
|
||||
on-page)
|
||||
ancestors (loop [id (:parent-id (get objects (:id item))) out [] depth 0]
|
||||
(let [shape (get objects id)]
|
||||
(if (and shape (not= id uuid/zero) (< depth 64))
|
||||
(recur (:parent-id shape) (conj out shape) (inc depth))
|
||||
out)))]
|
||||
{:interactions
|
||||
(if deleted?
|
||||
(into []
|
||||
(keep (fn [[id entry]]
|
||||
(when-not (contains? ids id)
|
||||
(let [lost (lost-interactions entry ids from to)]
|
||||
(when (seq lost)
|
||||
{:id id :label (:label entry) :count (count lost)})))))
|
||||
modified)
|
||||
[])
|
||||
|
||||
:flows
|
||||
(if deleted?
|
||||
(into []
|
||||
(keep (fn [entry]
|
||||
(when (and (= :page-flow (:kind entry))
|
||||
(= :deleted (:status entry))
|
||||
(contains? ids (get-in entry [:flow :starting-frame])))
|
||||
(or (get-in entry [:flow :name]) (:label entry)))))
|
||||
on-page)
|
||||
[])
|
||||
|
||||
:layouts
|
||||
(into []
|
||||
(keep (fn [{:keys [id layout] :as shape}]
|
||||
(when (some? layout)
|
||||
(let [entry (get modified id)
|
||||
before (size-of shape entry from)
|
||||
after (size-of shape entry to)
|
||||
moved (count (filter (fn [[sid e]]
|
||||
(and (= id (:parent-id (get objects sid)))
|
||||
(not (contains? ids sid))
|
||||
(some #(contains? (:changed-attrs e) %) [:x :y])))
|
||||
modified))]
|
||||
(cond
|
||||
(not= before after)
|
||||
{:id id :name (:name shape) :layout layout :from before :to after}
|
||||
|
||||
(pos? moved)
|
||||
{:id id :name (:name shape) :layout layout :moved moved})))))
|
||||
ancestors)}))
|
||||
|
||||
(defn group-by-place
|
||||
"Group the clean changes (`[index item]` pairs) by page, and inside a
|
||||
page by the top-level board that holds them, reading names from the
|
||||
|
||||
@ -101,11 +101,31 @@
|
||||
|
||||
:else nil)))
|
||||
|
||||
(defn- row-keys
|
||||
"The change keys a row stands for: its own and those of the layers
|
||||
folded under it (`brc/fold-cascades`), which travel or stay with it."
|
||||
[item inner]
|
||||
(into #{(:key item)} (map (comp :key second)) inner))
|
||||
|
||||
(defn- remove-folded
|
||||
"`rows` (`[index item]` pairs) without the layers folded under another
|
||||
row (`brc/fold-cascades`)."
|
||||
[rows cascades]
|
||||
(let [folded (into #{} (comp (mapcat val) (map first)) cascades)]
|
||||
(filterv (fn [[idx _]] (not (contains? folded idx))) rows)))
|
||||
|
||||
(mf/defc item-row*
|
||||
"One change. `inner` are the layers added or deleted with it, folded
|
||||
under it: the row counts them and its checkbox takes them along."
|
||||
{::mf/private true}
|
||||
[{:keys [index item selected excluded reviewed on-select on-toggle]}]
|
||||
[{:keys [index item inner selected excluded reviewed on-select on-toggle]}]
|
||||
(let [selectable? (selectable-item? item)
|
||||
change-key (:key item)
|
||||
toggle-keys (row-keys item inner)
|
||||
subtitle (cond-> (brc/change-summary item)
|
||||
(seq inner)
|
||||
(dm/str " - " (tr "workspace.branches.review.facts.layers"
|
||||
(i18n/c (count inner)))))
|
||||
on-click (mf/use-fn (mf/deps index on-select) #(on-select {:kind :item :index index}))
|
||||
start (when selectable?
|
||||
(mf/html
|
||||
@ -114,9 +134,9 @@
|
||||
:on-click dom-stop}
|
||||
[:> checkbox* {:id (dm/str "change-" change-key)
|
||||
:checked (not excluded)
|
||||
:on-change #(on-toggle #{change-key})}]]))]
|
||||
:on-change #(on-toggle toggle-keys)}]]))]
|
||||
[:> change-row* {:title (or (:label item) "")
|
||||
:subtitle (brc/change-summary item)
|
||||
:subtitle subtitle
|
||||
:icon (brc/entry-icon item)
|
||||
:tone (if (and excluded selectable?) "neutral" (brc/status-tone (:status item)))
|
||||
:is-selected (= selected {:kind :item :index index})
|
||||
@ -154,7 +174,7 @@
|
||||
|
||||
(mf/defc change-list*
|
||||
{::mf/private true}
|
||||
[{:keys [groups conflicts selected excluded reviewed collapsed
|
||||
[{:keys [groups conflicts cascades selected excluded reviewed collapsed
|
||||
on-select on-toggle on-fold]}]
|
||||
[:*
|
||||
(when (seq conflicts)
|
||||
@ -174,7 +194,7 @@
|
||||
(for [{:keys [id name rows boards count] :as group} groups]
|
||||
(let [open? (not (contains? collapsed id))
|
||||
row-keys (into #{} (comp (filter (fn [[_ it]] (selectable-item? it)))
|
||||
(map (fn [[_ it]] (:key it))))
|
||||
(mapcat (fn [[idx it]] (row-keys it (get cascades idx)))))
|
||||
(concat rows (mapcat :rows boards)))
|
||||
included? (not-any? excluded row-keys)]
|
||||
[:div {:class (stl/css :group) :key id}
|
||||
@ -192,6 +212,7 @@
|
||||
[:*
|
||||
(for [[idx item] rows]
|
||||
[:> item-row* {:key idx :index idx :item item
|
||||
:inner (get cascades idx)
|
||||
:selected selected
|
||||
:excluded (contains? excluded (:key item))
|
||||
:reviewed (contains? reviewed (review-key item))
|
||||
@ -203,6 +224,7 @@
|
||||
:on-select on-select :on-fold on-fold}
|
||||
(for [[idx item] (:rows board)]
|
||||
[:> item-row* {:key idx :index idx :item item
|
||||
:inner (get cascades idx)
|
||||
:selected selected
|
||||
:excluded (contains? excluded (:key item))
|
||||
:reviewed (contains? reviewed (review-key item))
|
||||
@ -226,7 +248,7 @@
|
||||
"MAIN and BRANCH, side by side, each with what it holds for the change."
|
||||
{::mf/private true}
|
||||
[{:keys [kind main-value branch-value main-objects branch-objects
|
||||
main-detail branch-detail main-missing branch-missing large]}]
|
||||
main-detail branch-detail main-missing branch-missing large highlight]}]
|
||||
[:div {:class (stl/css :cards)}
|
||||
[:> preview-card* {:label (tr "workspace.branches.review.side.main")
|
||||
:side "main"
|
||||
@ -235,7 +257,8 @@
|
||||
[:> brp/entity-preview* {:kind kind
|
||||
:value main-value
|
||||
:objects main-objects
|
||||
:missing main-missing}]]
|
||||
:missing main-missing
|
||||
:highlight highlight}]]
|
||||
[:> preview-card* {:label (tr "workspace.branches.review.side.branch")
|
||||
:side "branch"
|
||||
:detail branch-detail
|
||||
@ -243,7 +266,8 @@
|
||||
[:> brp/entity-preview* {:kind kind
|
||||
:value branch-value
|
||||
:objects branch-objects
|
||||
:missing branch-missing}]]])
|
||||
:missing branch-missing
|
||||
:highlight highlight}]]])
|
||||
|
||||
(mf/defc view-modes*
|
||||
"Side by side or overlay for the previews of a change, as icon toggles,
|
||||
@ -282,12 +306,23 @@
|
||||
(when objects (brp/side-objects objects page-items :main open-side)))
|
||||
branch-objs (mf/with-memo [objects page-items open-side]
|
||||
(when objects (brp/side-objects objects page-items :branch open-side)))
|
||||
main-v (if (= :shape kind)
|
||||
(or (get main-objs (:id item)) (get item :main))
|
||||
(brp/side-entity item :main open-side (library-entity data item)))
|
||||
branch-v (if (= :shape kind)
|
||||
(or (get branch-objs (:id item)) (get item :branch))
|
||||
(brp/side-entity item :branch open-side (library-entity data item)))
|
||||
;; an added or deleted layer inside a board is shown in it: the
|
||||
;; board on both sides, the layer outlined on the side that holds
|
||||
;; it, so the change reads as what the board gains or loses
|
||||
stage (when (and (= :shape kind) (contains? #{:added :deleted} (:status item)))
|
||||
(let [holder (if (contains? main-objs (:id item)) main-objs branch-objs)
|
||||
fid (brp/context-frame-id holder (:id item))]
|
||||
(when (and (some? fid) (contains? main-objs fid) (contains? branch-objs fid))
|
||||
fid)))
|
||||
highlight (when stage {:id (:id item) :status (:status item)})
|
||||
main-v (cond
|
||||
stage (get main-objs stage)
|
||||
(= :shape kind) (or (get main-objs (:id item)) (get item :main))
|
||||
:else (brp/side-entity item :main open-side (library-entity data item)))
|
||||
branch-v (cond
|
||||
stage (get branch-objs stage)
|
||||
(= :shape kind) (or (get branch-objs (:id item)) (get item :branch))
|
||||
:else (brp/side-entity item :branch open-side (library-entity data item)))
|
||||
main-detail (some-> (:main-at diff-meta) ct/inst ct/timeago)
|
||||
branch-detail (some-> (:branch-at diff-meta) ct/inst ct/timeago)]
|
||||
(when (or main-v branch-v)
|
||||
@ -300,12 +335,15 @@
|
||||
[:div {:class (stl/css :overlay-stage)}
|
||||
(when main-v
|
||||
[:div {:class (stl/css :overlay-layer)}
|
||||
[:> brp/entity-preview* {:kind kind :value main-v :objects main-objs}]])
|
||||
[:> brp/entity-preview* {:kind kind :value main-v :objects main-objs
|
||||
:highlight highlight}]])
|
||||
(when branch-v
|
||||
[:div {:class (stl/css :overlay-layer :is-top)}
|
||||
[:> brp/entity-preview* {:kind kind :value branch-v :objects branch-objs}]])]]
|
||||
[:> brp/entity-preview* {:kind kind :value branch-v :objects branch-objs
|
||||
:highlight highlight}]])]]
|
||||
[:> side-cards* {:kind kind
|
||||
:large large
|
||||
:highlight highlight
|
||||
:main-value main-v
|
||||
:branch-value branch-v
|
||||
:main-objects main-objs
|
||||
@ -323,8 +361,8 @@
|
||||
|
||||
(mf/defc item-detail*
|
||||
{::mf/private true}
|
||||
[{:keys [item items indexed excluded excluded-keys reviewed selected open-side incoming
|
||||
data diff-meta on-resolve on-select on-toggle expanded on-expand]}]
|
||||
[{:keys [item items indexed cascades excluded excluded-keys reviewed selected open-side
|
||||
incoming data diff-meta on-resolve on-select on-toggle expanded on-expand]}]
|
||||
(let [type-lbl (brc/type-label-key item)
|
||||
cat (brc/category item)
|
||||
attrs (:changed-attrs item)
|
||||
@ -387,7 +425,8 @@
|
||||
(case (:kind item)
|
||||
:page
|
||||
(when (contains? #{:added :deleted} (:status item))
|
||||
(let [rows (filterv (fn [[_ it]] (= (:id item) (:page-id it))) indexed)]
|
||||
(let [rows (filterv (fn [[_ it]] (= (:id item) (:page-id it))) indexed)
|
||||
rows (remove-folded rows cascades)]
|
||||
[:> brf/page-facts* {:item item
|
||||
:data data
|
||||
:open-side open-side
|
||||
@ -395,6 +434,7 @@
|
||||
:change-count (count rows)}
|
||||
(for [[idx it] rows]
|
||||
[:> item-row* {:key idx :index idx :item it
|
||||
:inner (get cascades idx)
|
||||
:selected selected
|
||||
:excluded (contains? excluded-keys (:key it))
|
||||
:reviewed (contains? reviewed (review-key it))
|
||||
@ -402,7 +442,9 @@
|
||||
|
||||
:shape
|
||||
(when (contains? #{:added :deleted} (:status item))
|
||||
[:> brf/shape-facts* {:item item :data data :incoming incoming}])
|
||||
[:> brf/shape-facts* {:item item :items items :data data
|
||||
:inner (get cascades (:index selected))
|
||||
:open-side open-side :incoming incoming}])
|
||||
|
||||
(:component :color :typography :token)
|
||||
[:> brf/usage-facts* {:item item :data data}]
|
||||
@ -411,8 +453,8 @@
|
||||
|
||||
(mf/defc board-detail*
|
||||
{::mf/private true}
|
||||
[{:keys [page-id board-id groups items open-side data diff-meta selected excluded reviewed
|
||||
on-select on-toggle]}]
|
||||
[{:keys [page-id board-id groups items cascades open-side data diff-meta selected excluded
|
||||
reviewed on-select on-toggle]}]
|
||||
(let [group (some #(when (= page-id (:page-id %)) %) groups)
|
||||
board (some #(when (= board-id (:id %)) %) (:boards group))
|
||||
rows (:rows board)
|
||||
@ -450,6 +492,7 @@
|
||||
[:div {:class (stl/css :inner-list)}
|
||||
(for [[idx item] rows]
|
||||
[:> item-row* {:key idx :index idx :item item
|
||||
:inner (get cascades idx)
|
||||
:selected selected
|
||||
:excluded (contains? excluded (:key item))
|
||||
:reviewed (contains? reviewed (review-key item))
|
||||
@ -486,8 +529,16 @@
|
||||
(into (vec (:changes diff)) (:conflicts diff)))
|
||||
indexed (mf/with-memo [items]
|
||||
(into [] (map-indexed vector) items))
|
||||
counts (mf/with-memo [items]
|
||||
(group-by brc/category items))
|
||||
;; the layers added or deleted with a container fold under it:
|
||||
;; the list, the filters and the counts show the container alone
|
||||
cascades (mf/with-memo [indexed]
|
||||
(brc/fold-cascades indexed))
|
||||
listed (mf/with-memo [indexed cascades]
|
||||
(remove-folded indexed cascades))
|
||||
listed-items (mf/with-memo [listed]
|
||||
(mapv second listed))
|
||||
counts (mf/with-memo [listed-items]
|
||||
(group-by brc/category listed-items))
|
||||
|
||||
filter* (mf/use-state :all)
|
||||
active-filter (deref filter*)
|
||||
@ -501,13 +552,13 @@
|
||||
collapsed* (mf/use-state #{})
|
||||
collapsed (deref collapsed*)
|
||||
|
||||
visible (mf/with-memo [indexed active-filter excluded]
|
||||
visible (mf/with-memo [listed active-filter excluded]
|
||||
(filterv (fn [[_ item]]
|
||||
(case active-filter
|
||||
:all true
|
||||
:left-out (contains? excluded (:key item))
|
||||
(= active-filter (brc/category item))))
|
||||
indexed))
|
||||
listed))
|
||||
conflicts (mf/with-memo [visible]
|
||||
(filterv (fn [[_ it]] (= :conflict (:status it))) visible))
|
||||
decisions (count (filter #(= :conflict (:status %)) items))
|
||||
@ -518,10 +569,12 @@
|
||||
order (mf/with-memo [conflicts groups]
|
||||
(display-order conflicts groups))
|
||||
|
||||
all-keys (into #{} (comp (filter selectable-item?) (map :key)) items)
|
||||
kept-count (count (remove excluded all-keys))
|
||||
left-out (count (filter excluded all-keys))
|
||||
reviewed-n (count (filter #(contains? reviewed (review-key %)) items))
|
||||
;; a folded layer travels or stays with its container, so the
|
||||
;; counts are of the rows the list shows
|
||||
row-keys* (into #{} (comp (filter selectable-item?) (map :key)) listed-items)
|
||||
kept-count (count (remove excluded row-keys*))
|
||||
left-out (count (filter excluded row-keys*))
|
||||
reviewed-n (count (filter #(contains? reviewed (review-key %)) listed-items))
|
||||
|
||||
sel-index (when (= :item (:kind selected)) (:index selected))
|
||||
sel-item (when (and (some? sel-index) (< sel-index (count items)))
|
||||
@ -620,7 +673,7 @@
|
||||
:filters (when (and (= status :loaded) (seq items) (not expanded))
|
||||
(mf/html
|
||||
[:*
|
||||
[:> filter-chip* {:label (dm/str (tr "labels.all") " (" (count items) ")")
|
||||
[:> filter-chip* {:label (dm/str (tr "labels.all") " (" (count listed-items) ")")
|
||||
:is-selected (= active-filter :all)
|
||||
:on-click #(on-filter :all)}]
|
||||
(for [[cat tone label icon] brc/categories
|
||||
@ -641,6 +694,7 @@
|
||||
(mf/html
|
||||
[:> change-list* {:groups groups
|
||||
:conflicts conflicts
|
||||
:cascades cascades
|
||||
:selected selected
|
||||
:excluded excluded
|
||||
:reviewed reviewed
|
||||
@ -668,6 +722,7 @@
|
||||
:board-id (:board-id selected)
|
||||
:groups groups
|
||||
:items items
|
||||
:cascades cascades
|
||||
:open-side open-side
|
||||
:data data
|
||||
:diff-meta diff-meta
|
||||
@ -682,6 +737,7 @@
|
||||
:item sel-item
|
||||
:items items
|
||||
:indexed indexed
|
||||
:cascades cascades
|
||||
:excluded (contains? excluded (:key sel-item))
|
||||
:excluded-keys excluded
|
||||
:reviewed reviewed
|
||||
@ -712,7 +768,7 @@
|
||||
(str/join " - "
|
||||
(remove nil?
|
||||
[(tr "workspace.branches.review.footer.reviewed"
|
||||
(str reviewed-n) (str (count items)))
|
||||
(str reviewed-n) (str (count listed-items)))
|
||||
(when (pos? decisions)
|
||||
(tr "workspace.branches.review.footer.blocked" (i18n/c decisions)))
|
||||
(when (pos? left-out)
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
"What the detail pane says about an entry that has no before and after:
|
||||
a new or deleted page or layer, and the library entries other layers
|
||||
depend on. It answers what the entry is, where it lands and what it
|
||||
affects, from the open file."
|
||||
affects, from the open file and, for a layer it lacks, from the diff."
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
@ -32,13 +32,22 @@
|
||||
"workspace.branches.review.side.main"
|
||||
"workspace.branches.review.side.branch")))
|
||||
|
||||
(defn- names-line
|
||||
"`names` joined, the first `max-names` of them and a count of the rest."
|
||||
[names]
|
||||
(dm/str (str/join ", " (take max-names names))
|
||||
(when (> (count names) max-names)
|
||||
(dm/str " " (tr "workspace.branches.review.summary.more"
|
||||
(i18n/c (- (count names) max-names)))))))
|
||||
|
||||
(mf/defc fact-list*
|
||||
"Label and value rows, one per fact."
|
||||
{::mf/private true}
|
||||
[{:keys [rows]}]
|
||||
[:div {:class (stl/css :facts)}
|
||||
(for [[label value] (remove nil? rows)]
|
||||
[:div {:class (stl/css :fact) :key label}
|
||||
;; keyed by place: a label may repeat (a layout row per board)
|
||||
(for [[idx [label value]] (map-indexed vector (remove nil? rows))]
|
||||
[:div {:class (stl/css :fact) :key idx}
|
||||
[:> text* {:as "span" :typography t/body-small :class (stl/css :fact-label)} label]
|
||||
[:div {:class (stl/css :fact-value)} value]])])
|
||||
|
||||
@ -74,10 +83,7 @@
|
||||
(tr "workspace.branches.review.facts.layers" (i18n/c layers))))]
|
||||
(when (seq names)
|
||||
[(tr "workspace.branches.review.facts.board-names")
|
||||
(dm/str (str/join ", " (take max-names names))
|
||||
(when (> (count names) max-names)
|
||||
(dm/str " " (tr "workspace.branches.review.summary.more"
|
||||
(i18n/c (- (count names) max-names))))))])]}]
|
||||
(names-line names)])]}]
|
||||
(when (and deleted? (pos? layers))
|
||||
[:> brl/notice* {:level :warning
|
||||
:title (tr "workspace.branches.review.facts.page-goes")}
|
||||
@ -134,11 +140,72 @@
|
||||
component
|
||||
(conj [(tr "workspace.branches.compare.attr.component") component]))))
|
||||
|
||||
;; --- What goes with a layer
|
||||
|
||||
(defn- size-line
|
||||
[[w h]]
|
||||
(dm/str (brc/display-val w) " × " (brc/display-val h)))
|
||||
|
||||
(defn- layout-row
|
||||
"A board with a layout around the layer: how it resizes, or how many
|
||||
of its other layers move."
|
||||
[{:keys [name layout from to moved]}]
|
||||
[(tr "workspace.branches.review.category.layout")
|
||||
(dm/str name " (" (tr (if (= :grid layout) "labels.grid-layout" "labels.flex-layout")) ") - "
|
||||
(if moved
|
||||
(tr "workspace.branches.review.effects.moved" (i18n/c moved))
|
||||
(dm/str (size-line from) " → " (size-line to))))])
|
||||
|
||||
(mf/defc effects*
|
||||
"What else changes with an added or deleted layer (`brc/shape-effects`):
|
||||
the prototype links and flows that go with it first, as warnings, then
|
||||
the layouts around it that adapt and the layers it holds. Nothing
|
||||
shows when none of it applies."
|
||||
{::mf/private true}
|
||||
[{:keys [item items objects inner incoming]}]
|
||||
(let [{:keys [interactions flows layouts]}
|
||||
(mf/with-memo [item items objects inner incoming]
|
||||
(brc/shape-effects item items objects inner incoming))
|
||||
names (mapv (comp :label second) inner)
|
||||
deleted? (= :deleted (:status item))
|
||||
rows (cond-> (mapv layout-row layouts)
|
||||
(seq names)
|
||||
(conj [(tr "workspace.branches.review.facts.contents") (names-line names)]))]
|
||||
(when (or (seq interactions) (seq flows) (seq rows))
|
||||
[:div {:class (stl/css :section)}
|
||||
[:> section-title* {}
|
||||
(tr (if deleted?
|
||||
"workspace.branches.review.effects.title-deleted"
|
||||
"workspace.branches.review.effects.title-added"))]
|
||||
(when (seq interactions)
|
||||
[:> brl/notice* {:level :warning
|
||||
:title (tr "workspace.branches.review.effects.interactions"
|
||||
(i18n/c (reduce + (map :count interactions))))}
|
||||
(tr "workspace.branches.review.effects.interactions-from"
|
||||
(names-line (map :label interactions)))])
|
||||
(when (seq flows)
|
||||
[:> brl/notice* {:level :warning
|
||||
:title (tr "workspace.branches.review.effects.flows"
|
||||
(i18n/c (count flows)))}
|
||||
(names-line flows)])
|
||||
(when (seq rows)
|
||||
[:> fact-list* {:rows rows}])])))
|
||||
|
||||
(mf/defc shape-facts*
|
||||
"A new or deleted layer: where it lands (or was), what it is and, for a
|
||||
deleted main component, the copies it leaves behind."
|
||||
[{:keys [item data incoming]}]
|
||||
(let [objects (get-in data [:pages-index (:page-id item) :objects])
|
||||
deleted main component, the copies it leaves behind. `inner` are the
|
||||
layers added or deleted with it (`brc/fold-cascades`)."
|
||||
[{:keys [item items inner data open-side incoming]}]
|
||||
(let [open-objs (get-in data [:pages-index (:page-id item) :objects])
|
||||
;; a layer the open file lacks is read on the other side, where
|
||||
;; the changes of `items` put it and its deleted parents back
|
||||
objects (mf/with-memo [open-objs items item open-side]
|
||||
(if (or (contains? open-objs (:id item)) (nil? open-objs))
|
||||
open-objs
|
||||
(brp/side-objects open-objs
|
||||
(filterv #(= (:page-id item) (:page-id %)) items)
|
||||
(if (= open-side :main) :branch :main)
|
||||
open-side)))
|
||||
shape (get objects (:id item))
|
||||
page (get-in data [:pages-index (:page-id item) :name])
|
||||
deleted? (= :deleted (:status item))
|
||||
@ -159,7 +226,9 @@
|
||||
(tr (if incoming
|
||||
"workspace.branches.review.facts.copies-orphaned-update"
|
||||
"workspace.branches.review.facts.copies-orphaned-merge")
|
||||
(i18n/c copies))])])))
|
||||
(i18n/c copies))])
|
||||
[:> effects* {:item item :items items :objects objects
|
||||
:inner inner :incoming incoming}]])))
|
||||
|
||||
;; --- Library entries other layers depend on
|
||||
|
||||
|
||||
@ -6,12 +6,13 @@
|
||||
|
||||
(ns app.main.ui.workspace.sidebar.branch-review.previews
|
||||
"Visual previews of the two sides of a change. The diff carries whole
|
||||
entities only for conflicts; for a clean change the open file holds one
|
||||
side and the changed attrs give the other, which is enough to draw both
|
||||
the layer and the board it sits in."
|
||||
entities for conflicts and for added or deleted shapes; for a modified
|
||||
entity the open file holds one side and the changed attrs give the
|
||||
other, which is enough to draw both the layer and the board it sits in."
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.render :as render]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
@ -28,6 +29,11 @@
|
||||
icon instead."
|
||||
#{:rect :circle :frame :path :bool :text :group})
|
||||
|
||||
(def ^:private box-shape-types
|
||||
"Shape types whose geometry is their position and size; a path or a
|
||||
boolean takes it from its content, a group from its children."
|
||||
#{:rect :circle :frame :text :image :svg-raw})
|
||||
|
||||
(defn open-side
|
||||
"Which document the open file is for `branch`: `:branch` when it is the
|
||||
branch file, `:main` otherwise."
|
||||
@ -41,7 +47,8 @@
|
||||
both; a clean change carries the attrs that differ, so the side the open
|
||||
file does not hold is the open entity with those attrs swapped in. An
|
||||
entry that does not exist on a side (added on the other one, or
|
||||
deleted) has no entity there."
|
||||
deleted) has no entity there. An added or deleted shape the open file
|
||||
lacks comes whole in the entry's `:shape`."
|
||||
[item side open-side open-entity]
|
||||
(cond
|
||||
(= :conflict (:status item))
|
||||
@ -51,7 +58,7 @@
|
||||
open-entity
|
||||
|
||||
(nil? open-entity)
|
||||
nil
|
||||
(:shape item)
|
||||
|
||||
(and (= :added (:status item)) (= side :main))
|
||||
nil
|
||||
@ -60,18 +67,27 @@
|
||||
nil
|
||||
|
||||
:else
|
||||
(reduce-kv (fn [entity attr values]
|
||||
(let [v (get values side)]
|
||||
(if (nil? v)
|
||||
(dissoc entity attr)
|
||||
(assoc entity attr v))))
|
||||
open-entity
|
||||
(:changed-attrs item))))
|
||||
(let [attrs (:changed-attrs item)
|
||||
entity (reduce-kv (fn [entity attr values]
|
||||
(let [v (get values side)]
|
||||
(if (nil? v)
|
||||
(dissoc entity attr)
|
||||
(assoc entity attr v))))
|
||||
open-entity
|
||||
attrs)]
|
||||
;; the diff leaves out `:selrect` and `:points` (they follow from
|
||||
;; position and size), so a box that moved or resized gets them
|
||||
;; again from its own, or it would draw with the open side's
|
||||
(if (and (contains? box-shape-types (:type entity))
|
||||
(some #(contains? attrs %) [:x :y :width :height]))
|
||||
(cts/setup-rect (dissoc entity :selrect :points))
|
||||
entity))))
|
||||
|
||||
(defn side-objects
|
||||
"The page `objects` as they stand on `side`, with every shape change of
|
||||
`items` that lives on this page put on that side. A shape missing on
|
||||
that side is taken out of its parent, so boards draw without it."
|
||||
that side is taken out of its parent, so boards draw without it, and a
|
||||
shape only that side holds is put into its parent, so boards draw it."
|
||||
[objects items side open-side]
|
||||
(reduce
|
||||
(fn [objects item]
|
||||
@ -80,7 +96,12 @@
|
||||
(let [id (:id item)
|
||||
entity (side-entity item side open-side (get objects id))]
|
||||
(if (some? entity)
|
||||
(assoc objects id entity)
|
||||
(let [parent-id (:parent-id entity)]
|
||||
(cond-> (assoc objects id entity)
|
||||
(and (contains? objects parent-id)
|
||||
(not (contains? objects id))
|
||||
(not (some #(= id %) (get-in objects [parent-id :shapes]))))
|
||||
(update-in [parent-id :shapes] (fnil conj []) id)))
|
||||
(let [parent-id (:parent-id (get objects id))]
|
||||
(cond-> (dissoc objects id)
|
||||
(contains? objects parent-id)
|
||||
@ -100,23 +121,47 @@
|
||||
(let [sr (:selrect shape)]
|
||||
{:x (:x sr 0) :y (:y sr 0) :width (:width sr 1) :height (:height sr 1)}))))
|
||||
|
||||
(defn context-frame-id
|
||||
"The nearest board that holds shape `id` in `objects`, the page root
|
||||
left out: the stage an added or deleted layer is shown on."
|
||||
[objects id]
|
||||
(loop [parent-id (:parent-id (get objects id))
|
||||
depth 0]
|
||||
(let [parent (get objects parent-id)]
|
||||
(cond
|
||||
(or (nil? parent) (= parent-id uuid/zero) (>= depth 64)) nil
|
||||
(= :frame (:type parent)) parent-id
|
||||
:else (recur (:parent-id parent) (inc depth))))))
|
||||
|
||||
(mf/defc shape-preview*
|
||||
"Renders `shape` to a fitted SVG with the real shape renderers. `objects`
|
||||
supplies its children (a board or group draws them)."
|
||||
supplies its children (a board or group draws them). `highlight`
|
||||
(`{:id :status}`) outlines the layer an added or deleted change is
|
||||
about, when `objects` holds it."
|
||||
{::mf/private true}
|
||||
[{:keys [shape objects]}]
|
||||
[{:keys [shape objects highlight]}]
|
||||
(let [{:keys [x y width height]} (shape-bounds shape)
|
||||
width (max 1 width)
|
||||
height (max 1 height)
|
||||
objects (mf/with-memo [shape objects]
|
||||
(assoc (or objects {}) (:id shape) shape))
|
||||
marked (some->> (:id highlight) (get objects) shape-bounds)
|
||||
wrapper (mf/with-memo [objects] (render/shape-wrapper-factory objects))]
|
||||
[:svg {:class (stl/css :preview-svg)
|
||||
:viewBox (dm/str x " " y " " width " " height)
|
||||
:preserveAspectRatio "xMidYMid meet"
|
||||
:xmlns "http://www.w3.org/2000/svg"
|
||||
:fill "none"}
|
||||
[:> wrapper {:shape shape}]]))
|
||||
[:> wrapper {:shape shape}]
|
||||
(when marked
|
||||
[:rect {:class (stl/css-case :preview-highlight true
|
||||
:is-added (= :added (:status highlight))
|
||||
:is-deleted (= :deleted (:status highlight)))
|
||||
:x (:x marked)
|
||||
:y (:y marked)
|
||||
:width (max 1 (:width marked))
|
||||
:height (max 1 (:height marked))
|
||||
:vector-effect "non-scaling-stroke"}])]))
|
||||
|
||||
(mf/defc page-preview*
|
||||
"The whole page `objects` drawn to fit: every top-level layer, framed
|
||||
@ -192,8 +237,8 @@
|
||||
shapes render to SVG, library colors to a swatch, typographies to a text
|
||||
sample; everything else shows a type icon. A side with nothing to draw
|
||||
says why (`missing`: `{:reason :side :open-side}`, see
|
||||
`missing-reason`)."
|
||||
[{:keys [kind value objects missing]}]
|
||||
`missing-reason`). A shape `highlight` is passed on to `shape-preview*`."
|
||||
[{:keys [kind value objects missing highlight]}]
|
||||
(cond
|
||||
(nil? value)
|
||||
[:> missing-preview* {:reason (:reason missing :unavailable)
|
||||
@ -202,7 +247,7 @@
|
||||
|
||||
(= kind :shape)
|
||||
(if (contains? renderable-shape-types (:type value))
|
||||
[:> shape-preview* {:shape value :objects objects}]
|
||||
[:> shape-preview* {:shape value :objects objects :highlight highlight}]
|
||||
[:div {:class (stl/css :preview-empty)}
|
||||
[:> i/icon* {:icon-id (brc/shape-icon-id {:shape-type (:type value)})}]])
|
||||
|
||||
|
||||
@ -13,6 +13,21 @@
|
||||
block-size: 100%;
|
||||
}
|
||||
|
||||
// the outline of the layer an added or deleted change is about, drawn
|
||||
// over the board that holds it; the stroke keeps its width at any zoom
|
||||
.preview-highlight {
|
||||
--preview-highlight-color: var(--color-accent-success);
|
||||
|
||||
fill: color-mix(in srgb, var(--preview-highlight-color) 12%, transparent);
|
||||
stroke: var(--preview-highlight-color);
|
||||
stroke-width: 2;
|
||||
stroke-dasharray: 4 3;
|
||||
|
||||
&.is-deleted {
|
||||
--preview-highlight-color: var(--color-accent-error);
|
||||
}
|
||||
}
|
||||
|
||||
.preview-swatch {
|
||||
inline-size: 100%;
|
||||
block-size: 100%;
|
||||
|
||||
@ -270,10 +270,18 @@
|
||||
;; what goes in and what stays out, when the compare dialog of this
|
||||
;; branch is the one that asked for the merge
|
||||
diff-st (mf/deref branch-diff)
|
||||
;; counted as the compare list shows them: the layers added or
|
||||
;; deleted with a container go with it (`brc/fold-cascades`)
|
||||
changes (when (and (= (:id branch) (:branch-id diff-st))
|
||||
(= :loaded (:status diff-st)))
|
||||
(filterv #(contains? #{:added :modified :deleted} (:status %))
|
||||
(get-in diff-st [:diff :changes])))
|
||||
(let [indexed (into [] (map-indexed vector) (get-in diff-st [:diff :changes]))
|
||||
folded (into #{} (comp (mapcat val) (map first))
|
||||
(brc/fold-cascades indexed))]
|
||||
(into []
|
||||
(comp (remove (fn [[idx _]] (contains? folded idx)))
|
||||
(map second)
|
||||
(filter #(contains? #{:added :modified :deleted} (:status %))))
|
||||
indexed)))
|
||||
left-out (count (filter #(contains? (or excluded #{}) (:key %)) changes))
|
||||
going-in (- (count changes) left-out)
|
||||
|
||||
|
||||
@ -115,6 +115,7 @@
|
||||
[frontend-tests.tokens.tokenscript-test]
|
||||
[frontend-tests.tokens.workspace-tokens-remap-test]
|
||||
[frontend-tests.ui.branch-review-common-test]
|
||||
[frontend-tests.ui.branch-review-previews-test]
|
||||
[frontend-tests.ui.branches-compare-labels-test]
|
||||
[frontend-tests.ui.check-updates-test]
|
||||
[frontend-tests.ui.color-token-position-test]
|
||||
@ -283,6 +284,7 @@
|
||||
'frontend-tests.tokens.logic.tokens-status-test
|
||||
'frontend-tests.tokens.workspace-tokens-remap-test
|
||||
'frontend-tests.ui.branch-review-common-test
|
||||
'frontend-tests.ui.branch-review-previews-test
|
||||
'frontend-tests.ui.branches-compare-labels-test
|
||||
'frontend-tests.ui.check-updates-test
|
||||
'frontend-tests.ui.color-token-position-test
|
||||
|
||||
@ -142,3 +142,100 @@
|
||||
(t/is (= :components (brc/category {:kind :shape :status :modified :component? true})))
|
||||
(t/is (= #{"neutral"} (set (vals (dissoc brc/category->tone :decisions)))))
|
||||
(t/is (= "warning" (brc/category->tone :decisions))))
|
||||
|
||||
(defn- deleted
|
||||
[id parent-id]
|
||||
{:kind :shape :status :deleted :id id :page-id page-id
|
||||
:shape {:id id :parent-id parent-id}})
|
||||
|
||||
(t/deftest a-deleted-group-takes-its-layers-along
|
||||
(let [rows [[0 (deleted :path-a :icon)]
|
||||
[1 (deleted :bg :icon)]
|
||||
[2 (deleted :path-b :inner)]
|
||||
[3 (deleted :inner :icon)]
|
||||
[4 (deleted :icon :button)]
|
||||
[5 {:kind :shape :status :modified :id :label :page-id page-id}]]
|
||||
folds (brc/fold-cascades rows)]
|
||||
(t/is (= [4] (keys folds)) "the outermost deleted layer holds them all")
|
||||
(t/is (= #{0 1 2 3} (set (map first (get folds 4)))))))
|
||||
|
||||
(t/deftest only-the-same-change-folds
|
||||
(let [added {:kind :shape :status :added :id :new :page-id page-id
|
||||
:shape {:id :new :parent-id :icon}}
|
||||
rows [[0 (deleted :icon :button)]
|
||||
[1 added]
|
||||
[2 (deleted :lone :button)]
|
||||
[3 (assoc (deleted :other :icon) :page-id :elsewhere)]]]
|
||||
(t/is (= {} (brc/fold-cascades rows))
|
||||
"an addition, a layer whose parent stays and one on another page stay apart")))
|
||||
|
||||
;; --- What goes with an added or deleted layer
|
||||
|
||||
(def ^:private home-id (uuid/next))
|
||||
(def ^:private screen-id (uuid/next))
|
||||
(def ^:private cta-id (uuid/next))
|
||||
(def ^:private button-id (uuid/next))
|
||||
(def ^:private icon-id (uuid/next))
|
||||
(def ^:private label-id (uuid/next))
|
||||
|
||||
(def ^:private proto-objects
|
||||
"Main's page: a button with a flex layout on Home, and a Screen the
|
||||
CTA leads to."
|
||||
{uuid/zero {:id uuid/zero :type :frame :shapes [home-id screen-id]}
|
||||
home-id {:id home-id :type :frame :name "Home" :parent-id uuid/zero}
|
||||
screen-id {:id screen-id :type :frame :name "Screen" :parent-id uuid/zero}
|
||||
cta-id {:id cta-id :type :rect :name "CTA" :parent-id home-id}
|
||||
button-id {:id button-id :type :frame :name "Button" :parent-id home-id
|
||||
:layout :flex :width 163 :height 48}
|
||||
icon-id {:id icon-id :type :group :name "icon" :parent-id button-id}
|
||||
label-id {:id label-id :type :text :name "label" :parent-id button-id}})
|
||||
|
||||
(defn- gone
|
||||
[id]
|
||||
{:kind :shape :status :deleted :id id :page-id page-id})
|
||||
|
||||
(defn- edit
|
||||
[id attrs]
|
||||
{:kind :shape :status :modified :id id :page-id page-id :label (name id)
|
||||
:changed-attrs attrs})
|
||||
|
||||
(t/deftest a-board-with-a-layout-resizes-with-the-layer
|
||||
(let [items [(gone icon-id)
|
||||
(edit button-id {:width {:main 163 :branch 131}})]
|
||||
fx (brc/shape-effects (gone icon-id) items proto-objects [] false)]
|
||||
(t/is (= [{:id button-id :name "Button" :layout :flex :from [163 48] :to [131 48]}]
|
||||
(:layouts fx)))
|
||||
(t/is (= [] (:interactions fx)))
|
||||
(t/is (= [] (:flows fx)))))
|
||||
|
||||
(t/deftest an-update-reads-the-sides-the-other-way-round
|
||||
(let [items [(gone icon-id)
|
||||
(edit button-id {:width {:main 131 :branch 163}})]
|
||||
fx (brc/shape-effects (gone icon-id) items proto-objects [] true)]
|
||||
(t/is (= [[163 48] [131 48]] ((juxt :from :to) (first (:layouts fx)))))))
|
||||
|
||||
(t/deftest a-fixed-board-moves-its-other-layers
|
||||
(let [items [(gone icon-id)
|
||||
(edit label-id {:x {:main 40 :branch 20}})]
|
||||
fx (brc/shape-effects (gone icon-id) items proto-objects [] false)]
|
||||
(t/is (= [{:id button-id :name "Button" :layout :flex :moved 1}] (:layouts fx)))))
|
||||
|
||||
(t/deftest prototype-links-into-a-deleted-board-go-with-it
|
||||
(let [link {:event-type :click :action-type :navigate :destination screen-id}
|
||||
items [(gone screen-id)
|
||||
(assoc (edit cta-id {:interactions {:main [link] :branch []}}) :label "CTA")
|
||||
{:kind :page-flow :status :deleted :id :f1 :page-id page-id :label "f1"
|
||||
:flow {:id :f1 :name "Onboarding" :starting-frame screen-id}}
|
||||
{:kind :page-flow :status :deleted :id :f2 :page-id page-id :label "f2"
|
||||
:flow {:id :f2 :name "Other" :starting-frame home-id}}]
|
||||
fx (brc/shape-effects (gone screen-id) items proto-objects [] false)]
|
||||
(t/is (= [{:id cta-id :label "CTA" :count 1}] (:interactions fx)))
|
||||
(t/is (= ["Onboarding"] (:flows fx)))
|
||||
(t/is (= [] (:layouts fx)) "the page root is not a layout")))
|
||||
|
||||
(t/deftest an-added-layer-breaks-no-links
|
||||
(let [link {:event-type :click :action-type :navigate :destination screen-id}
|
||||
added (assoc (gone screen-id) :status :added)
|
||||
items [added (edit cta-id {:interactions {:main [] :branch [link]}})]
|
||||
fx (brc/shape-effects added items proto-objects [] false)]
|
||||
(t/is (= [] (:interactions fx)))))
|
||||
|
||||
@ -0,0 +1,59 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
|
||||
(ns frontend-tests.ui.branch-review-previews-test
|
||||
"How the compare previews rebuild the side the open file lacks: a
|
||||
deleted layer comes back from the diff, inside the board it was in,
|
||||
and a resized board draws at its own size."
|
||||
(:require
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.ui.workspace.sidebar.branch-review.previews :as brp]
|
||||
[cljs.test :as t :include-macros true]))
|
||||
|
||||
(def ^:private board-id (uuid/next))
|
||||
(def ^:private icon-id (uuid/next))
|
||||
(def ^:private path-id (uuid/next))
|
||||
|
||||
(def ^:private branch-objects
|
||||
"The branch, open: the button lost its icon and shrank."
|
||||
{uuid/zero {:id uuid/zero :type :frame :shapes [board-id]}
|
||||
board-id (cts/setup-rect {:id board-id :type :frame :name "Button"
|
||||
:parent-id uuid/zero :x 0 :y 0 :width 131 :height 48
|
||||
:shapes []})})
|
||||
|
||||
(def ^:private icon
|
||||
{:id icon-id :type :group :name "icon" :parent-id board-id
|
||||
:x 119 :y 12 :width 24 :height 24 :shapes [path-id]})
|
||||
|
||||
(def ^:private items
|
||||
[{:kind :shape :status :deleted :id path-id
|
||||
:shape {:id path-id :type :path :name "path" :parent-id icon-id}}
|
||||
{:kind :shape :status :deleted :id icon-id :shape icon}
|
||||
{:kind :shape :status :modified :id board-id
|
||||
:changed-attrs {:width {:main 163 :branch 131}}}])
|
||||
|
||||
(t/deftest a-deleted-layer-comes-back-on-the-side-that-has-it
|
||||
(let [main (brp/side-objects branch-objects items :main :branch)]
|
||||
(t/is (= icon (get main icon-id)))
|
||||
(t/is (contains? main path-id))
|
||||
(t/is (= [icon-id] (get-in main [board-id :shapes])))))
|
||||
|
||||
(t/deftest the-side-that-deleted-it-stays-without-it
|
||||
(let [branch (brp/side-objects branch-objects items :branch :branch)]
|
||||
(t/is (not (contains? branch icon-id)))
|
||||
(t/is (= [] (get-in branch [board-id :shapes])))))
|
||||
|
||||
(t/deftest a-resized-board-draws-at-its-own-size
|
||||
(let [main (brp/side-objects branch-objects items :main :branch)]
|
||||
(t/is (= 163 (get-in main [board-id :selrect :width])))
|
||||
(t/is (= 131 (get-in branch-objects [board-id :selrect :width])))))
|
||||
|
||||
(t/deftest a-layer-is-staged-in-the-nearest-board
|
||||
(let [main (brp/side-objects branch-objects items :main :branch)]
|
||||
(t/is (= board-id (brp/context-frame-id main path-id)))
|
||||
(t/is (= board-id (brp/context-frame-id main icon-id)))
|
||||
(t/is (nil? (brp/context-frame-id main board-id)))))
|
||||
@ -69,3 +69,17 @@
|
||||
(t/is (= "439.5" (display-val 439.5)))
|
||||
(t/is (= "90" (display-val 89.99999)) "at most two decimals")
|
||||
(t/is (= "386" (display-val 386)))))
|
||||
|
||||
(t/deftest numbers-keep-two-decimals-under-the-plugin-lockdown
|
||||
;; a loaded plugin's SES lockdown turns `toLocaleString` into
|
||||
;; `toString`, which ignores the options
|
||||
(let [proto (.-prototype js/Number)
|
||||
original (.-toLocaleString proto)]
|
||||
(set! (.-toLocaleString proto) (.-toString proto))
|
||||
(try
|
||||
(binding [i18n/*current-locale* "en"]
|
||||
(t/is (= "163" (display-val 162.99999952316284))))
|
||||
(binding [i18n/*current-locale* nil]
|
||||
(t/is (= "1.5" (display-val 1.5)) "no locale falls back to the default"))
|
||||
(finally
|
||||
(set! (.-toLocaleString proto) original)))))
|
||||
|
||||
@ -11731,6 +11731,36 @@ msgid_plural "workspace.branches.review.facts.deleted-used"
|
||||
msgstr[0] "1 layer in this file loses its link to it."
|
||||
msgstr[1] "%s layers in this file lose their link to it."
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/facts.cljs
|
||||
msgid "workspace.branches.review.effects.title-deleted"
|
||||
msgstr "What goes with it"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/facts.cljs
|
||||
msgid "workspace.branches.review.effects.title-added"
|
||||
msgstr "What comes with it"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/facts.cljs
|
||||
msgid "workspace.branches.review.effects.interactions"
|
||||
msgid_plural "workspace.branches.review.effects.interactions"
|
||||
msgstr[0] "1 prototype interaction leads here and goes too"
|
||||
msgstr[1] "%s prototype interactions lead here and go too"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/facts.cljs
|
||||
msgid "workspace.branches.review.effects.interactions-from"
|
||||
msgstr "On %s"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/facts.cljs
|
||||
msgid "workspace.branches.review.effects.flows"
|
||||
msgid_plural "workspace.branches.review.effects.flows"
|
||||
msgstr[0] "1 prototype flow starts here and goes too"
|
||||
msgstr[1] "%s prototype flows start here and go too"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/facts.cljs
|
||||
msgid "workspace.branches.review.effects.moved"
|
||||
msgid_plural "workspace.branches.review.effects.moved"
|
||||
msgstr[0] "1 other layer moves"
|
||||
msgstr[1] "%s other layers move"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/common.cljs
|
||||
msgid "workspace.branches.review.group.pages"
|
||||
msgstr "Pages"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user