mirror of
https://github.com/penpot/penpot.git
synced 2026-10-29 05:50:35 +00:00
✨ Show in the merge dialogs what happens to component copies
The compare panel and the conflict dialog list the copies that will follow each changed component and the overrides each copy keeps. A conflict on a component's main says how many copies the choice also decides. A copy entry shows what the copy rule decided, and on a conflict notes that the choice decides only the open properties. Copies the rule settled entirely show as rows without a checkbox, outside the counts. AI-assisted-by: claude-opus-5-5
This commit is contained in:
parent
95ad841ed5
commit
313d37ce91
@ -7,11 +7,14 @@
|
||||
(ns app.main.ui.workspace.sidebar.branch-review.common
|
||||
"What the compare and conflict dialogs say about a diff entry, kept free
|
||||
of rendering: labels, values, categories, one-line summaries, the
|
||||
page/board a change lives on, and how a conflict splits into the
|
||||
properties that clash and the ones only one side touched."
|
||||
page/board a change lives on, how a conflict splits into the
|
||||
properties that clash and the ones only one side touched, and what the
|
||||
copy rule decides for the copies of a changed component."
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.branch-merge :as bm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.types.component :as ctk]
|
||||
[app.common.types.text :as txt]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.modal :as modal]
|
||||
@ -320,12 +323,14 @@
|
||||
:else :other)))
|
||||
|
||||
(defn status-tone
|
||||
"The colour of the bar on a change row: what happened to the entry."
|
||||
"The colour of the bar on a change row: what happened to the entry. A
|
||||
copy the copy rule settled is information, not a change."
|
||||
[status]
|
||||
(case status
|
||||
:added "success"
|
||||
:deleted "error"
|
||||
:conflict "warning"
|
||||
:settled "neutral"
|
||||
"info"))
|
||||
|
||||
;; --- One-line summaries
|
||||
@ -365,6 +370,7 @@
|
||||
(case status
|
||||
:added (tr "workspace.branches.compare.subtitle-new" (or lbl ""))
|
||||
:deleted (tr "workspace.branches.review.summary.deleted" (or lbl ""))
|
||||
:settled (tr "workspace.branches.review.copies.settled-row")
|
||||
(if (empty? phrases)
|
||||
(or lbl (tr "workspace.branches.review.summary.changed"))
|
||||
(cond-> (str/join ", " shown)
|
||||
@ -525,7 +531,8 @@
|
||||
`:pages` group for the pages added, deleted or reordered. Each
|
||||
group is `{:id :name :page-id :rows [[idx item]] :boards [{:id :name
|
||||
:rows}]}`, with `:rows` holding the entries outside any board. A board
|
||||
groups its changes only when it holds two or more of `rows`."
|
||||
groups its changes only when it holds two or more of `rows`. `:count`
|
||||
leaves out the copies the copy rule settled (`bm/settled?`)."
|
||||
[rows pages-index pages]
|
||||
(let [by-page (group-by (fn [[_ item]] (:page-id item)) rows)
|
||||
page-group
|
||||
@ -558,7 +565,7 @@
|
||||
:name (or (:name page) (tr "workspace.branches.review.unknown-page"))
|
||||
:rows (into [] (comp (remove second) (map first)) placed)
|
||||
:boards boards
|
||||
:count (count page-rows)}))
|
||||
:count (count (remove (comp bm/settled? second) page-rows))}))
|
||||
page-order (into {} (map-indexed (fn [idx id] [id idx])) pages)
|
||||
loose (get by-page nil)
|
||||
page-kind #(contains? #{:page :page-order} (:kind (second %)))
|
||||
@ -791,3 +798,143 @@
|
||||
(count (filter (fn [shape]
|
||||
(some #(= token-name %) (vals (:applied-tokens shape))))
|
||||
(all-shapes data))))
|
||||
|
||||
;; --- Component copies: what follows a changed component
|
||||
|
||||
(defn group-label
|
||||
"Human-readable name of a sync group (`app.common.types.component/sync-attrs`):
|
||||
the properties a copy overrides together. The layout groups read as the
|
||||
layout or the element's place in it, a swap slot as a swapped
|
||||
component."
|
||||
[group]
|
||||
(let [gname (name group)]
|
||||
(case group
|
||||
:name-group (tr "workspace.branches.review.copies.group.name")
|
||||
:fill-group (tr "workspace.branches.review.copies.group.fill")
|
||||
:stroke-group (tr "workspace.branches.review.copies.group.stroke")
|
||||
:content-group (tr "workspace.branches.review.copies.group.content")
|
||||
:geometry-group (tr "workspace.branches.review.copies.group.geometry")
|
||||
:radius-group (tr "workspace.branches.review.copies.group.radius")
|
||||
:visibility-group (tr "workspace.branches.review.copies.group.visibility")
|
||||
:modifiable-group (tr "workspace.branches.review.copies.group.lock")
|
||||
:text-font-group (tr "workspace.branches.review.copies.group.font")
|
||||
:text-display-group (tr "workspace.branches.review.copies.group.text-display")
|
||||
:layer-effects-group (tr "workspace.branches.review.copies.group.layer")
|
||||
:shadow-group (tr "workspace.branches.review.copies.group.shadow")
|
||||
:blur-group (tr "workspace.branches.review.copies.group.blur")
|
||||
:mask-group (tr "workspace.branches.review.copies.group.mask")
|
||||
:constraints-group (tr "workspace.branches.review.copies.group.constraints")
|
||||
:exports-group (tr "workspace.branches.review.copies.group.exports")
|
||||
:grids-group (tr "workspace.branches.review.copies.group.grids")
|
||||
:show-content (tr "workspace.branches.review.copies.group.clip")
|
||||
(cond
|
||||
(ctk/swap-slot? group) (tr "workspace.branches.review.copies.group.swap")
|
||||
(str/starts-with? gname "layout-item") (tr "workspace.branches.review.copies.group.layout-item")
|
||||
(str/starts-with? gname "layout") (tr "workspace.branches.review.copies.group.layout")
|
||||
:else (-> gname (str/replace #"-group$" "") (str/replace "-" " ") str/capital)))))
|
||||
|
||||
(defn token-slot-label
|
||||
"A token slot (token attr of `:applied-tokens`) in words: the property
|
||||
the token drives."
|
||||
[slot]
|
||||
(tr "workspace.branches.review.copies.token-slot" (str/lower (attr-label slot))))
|
||||
|
||||
(defn reason-label
|
||||
"Why the copy rule gave a property to a side, in words."
|
||||
[reason]
|
||||
(case reason
|
||||
:follows-component (tr "workspace.branches.review.copies.reason.follows-component")
|
||||
:override-main (tr "workspace.branches.review.copies.reason.override-main")
|
||||
:override-branch (tr "workspace.branches.review.copies.reason.override-branch")
|
||||
(tr "workspace.branches.review.copies.reason.override-both")))
|
||||
|
||||
(defn side-label
|
||||
[side]
|
||||
(if (= side :main)
|
||||
(tr "workspace.branches.review.side.main")
|
||||
(tr "workspace.branches.review.side.branch")))
|
||||
|
||||
(defn copies-entry
|
||||
"The `:copies` entry of the summary (`app.common.files.branch-merge/compute-merge`)
|
||||
for the component diff entry `item` belongs to: the component itself,
|
||||
its main instance, or a layer inside that main, found by walking up
|
||||
`objects`, the page of `item` as the open file holds it. Nil when
|
||||
`item` is about no changed component."
|
||||
[copies item objects]
|
||||
(when (seq copies)
|
||||
(case (:kind item)
|
||||
:component
|
||||
(some #(when (= (:id item) (:component-id %)) %) copies)
|
||||
|
||||
:shape
|
||||
(let [by-main (into {}
|
||||
(comp (filter #(= (:page-id item) (:main-page-id %)))
|
||||
(map (juxt :main-id identity)))
|
||||
copies)]
|
||||
(loop [id (:id item) depth 0]
|
||||
(cond
|
||||
(contains? by-main id) (get by-main id)
|
||||
(or (nil? id) (= id uuid/zero) (> depth 64)) nil
|
||||
:else (recur (get-in objects [id :parent-id]) (inc depth)))))
|
||||
|
||||
nil)))
|
||||
|
||||
(defn copy-follow-counts
|
||||
"How many copies follow each changed component, keyed both by the
|
||||
component id and by its main instance id, as the change rows know
|
||||
either one."
|
||||
[copies]
|
||||
(reduce (fn [acc {:keys [component-id main-id] :as entry}]
|
||||
(let [n (count (:copies entry))]
|
||||
(assoc acc component-id n main-id n)))
|
||||
{}
|
||||
copies))
|
||||
|
||||
(defn copy-keeps
|
||||
"What copy `copy` (one of a `:copies` entry's copies) keeps of its own,
|
||||
per side: `{side [label ...]}`, with the sync groups it keeps an
|
||||
override of and the token slots that side overrode."
|
||||
[copy]
|
||||
(let [groups (reduce (fn [acc [group side]]
|
||||
(update acc side (fnil conj []) (group-label group)))
|
||||
{}
|
||||
(sort-by (comp group-label key) (:keeps copy)))]
|
||||
(reduce (fn [acc [slot {:keys [side reason]}]]
|
||||
(if (and (contains? #{:main :branch} side)
|
||||
(contains? #{:override-main :override-branch} reason))
|
||||
(update acc side (fnil conj []) (token-slot-label slot))
|
||||
acc))
|
||||
groups
|
||||
(sort-by (comp name key) (:token-slots copy)))))
|
||||
|
||||
(defn copy-open-slots
|
||||
"The token slots of copy `copy` both sides overrode: the user decides
|
||||
them."
|
||||
[copy]
|
||||
(into []
|
||||
(comp (filter (fn [[_ {:keys [side]}]] (= :open side)))
|
||||
(map (comp token-slot-label key)))
|
||||
(sort-by (comp name key) (:token-slots copy))))
|
||||
|
||||
(defn settled-rows
|
||||
"The attrs and token slots of `settled` (a copy entry's `:settled`)
|
||||
the copy rule decided, each `{:label :side :reason}`, attrs first."
|
||||
[settled]
|
||||
(-> []
|
||||
(into (map (fn [[attr {:keys [side reason]}]]
|
||||
{:label (attr-label attr) :side side :reason reason}))
|
||||
(sort-by (comp attr-label key) (:attrs settled)))
|
||||
(into (map (fn [[slot {:keys [side reason]}]]
|
||||
{:label (token-slot-label slot) :side side :reason reason}))
|
||||
(sort-by (comp name key) (:token-slots settled)))))
|
||||
|
||||
(defn settled-open
|
||||
"What the user still decides on a copy conflict with `settled`: the
|
||||
open attrs, `:applied-tokens` aside, then the open token slots, in
|
||||
words."
|
||||
[settled]
|
||||
(-> []
|
||||
(into (comp (remove #{:applied-tokens}) (map attr-label))
|
||||
(sort-by attr-label (:open-attrs settled)))
|
||||
(into (map token-slot-label)
|
||||
(sort-by name (:open-token-slots settled)))))
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.branch-merge :as bm]
|
||||
[app.common.time :as ct]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.workspace.branches :as dwb]
|
||||
@ -77,6 +78,10 @@
|
||||
:class (stl/css :row-warning)
|
||||
:aria-label (tr "workspace.branches.review.row.choose")}]
|
||||
|
||||
;; information, not a change: nothing to choose or leave out
|
||||
(= :settled status)
|
||||
[:> tag* {:variant "pill"} (tr "workspace.branches.review.copies.settled-tag")]
|
||||
|
||||
excluded
|
||||
[:> tag* {:variant "pill"} (tr "workspace.branches.review.row.left-out")]
|
||||
|
||||
@ -114,18 +119,30 @@
|
||||
(let [folded (into #{} (comp (mapcat val) (map first)) cascades)]
|
||||
(filterv (fn [[idx _]] (not (contains? folded idx))) rows)))
|
||||
|
||||
(defn- follows-of
|
||||
"How many copies follow the component `item` is, or the main instance
|
||||
of (`brc/copy-follow-counts`), or nil."
|
||||
[follow-counts item]
|
||||
(when (or (= :component (:kind item)) (:component? item))
|
||||
(get follow-counts (:id item))))
|
||||
|
||||
(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."
|
||||
under it: the row counts them and its checkbox takes them along.
|
||||
`follows` is how many copies follow the component the row changes."
|
||||
{::mf/private true}
|
||||
[{:keys [index item inner selected excluded reviewed on-select on-toggle]}]
|
||||
[{:keys [index item inner follows 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)))))
|
||||
(i18n/c (count inner))))
|
||||
|
||||
(and follows (pos? follows))
|
||||
(dm/str " - " (tr "workspace.branches.review.copies.row-follow"
|
||||
(i18n/c follows))))
|
||||
on-click (mf/use-fn (mf/deps index on-select) #(on-select {:kind :item :index index}))
|
||||
start (when selectable?
|
||||
(mf/html
|
||||
@ -159,7 +176,8 @@
|
||||
[:*
|
||||
[:> change-row* {:title (or (:name board) "")
|
||||
:subtitle (tr "workspace.branches.review.board.inside"
|
||||
(i18n/c (count (:rows board))))
|
||||
(i18n/c (count (remove (comp bm/settled? second)
|
||||
(:rows board)))))
|
||||
;; a fold is not a change: no status colour
|
||||
:tone "neutral"
|
||||
:is-selected (= selected sel)
|
||||
@ -174,7 +192,7 @@
|
||||
|
||||
(mf/defc change-list*
|
||||
{::mf/private true}
|
||||
[{:keys [groups conflicts cascades selected excluded reviewed collapsed
|
||||
[{:keys [groups conflicts cascades follow-counts selected excluded reviewed collapsed
|
||||
on-select on-toggle on-fold]}]
|
||||
[:*
|
||||
(when (seq conflicts)
|
||||
@ -187,6 +205,7 @@
|
||||
(when open?
|
||||
(for [[idx item] conflicts]
|
||||
[:> item-row* {:key idx :index idx :item item
|
||||
:follows (follows-of follow-counts item)
|
||||
:selected selected
|
||||
:reviewed (contains? reviewed (review-key item))
|
||||
:on-select on-select :on-toggle on-toggle}]))]))
|
||||
@ -213,6 +232,7 @@
|
||||
(for [[idx item] rows]
|
||||
[:> item-row* {:key idx :index idx :item item
|
||||
:inner (get cascades idx)
|
||||
:follows (follows-of follow-counts item)
|
||||
:selected selected
|
||||
:excluded (contains? excluded (:key item))
|
||||
:reviewed (contains? reviewed (review-key item))
|
||||
@ -225,6 +245,7 @@
|
||||
(for [[idx item] (:rows board)]
|
||||
[:> item-row* {:key idx :index idx :item item
|
||||
:inner (get cascades idx)
|
||||
:follows (follows-of follow-counts item)
|
||||
:selected selected
|
||||
:excluded (contains? excluded (:key item))
|
||||
:reviewed (contains? reviewed (review-key item))
|
||||
@ -361,13 +382,16 @@
|
||||
|
||||
(mf/defc item-detail*
|
||||
{::mf/private true}
|
||||
[{:keys [item items indexed cascades excluded excluded-keys reviewed selected open-side
|
||||
[{:keys [item items indexed cascades copies 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)
|
||||
visible (brc/visible-attrs attrs)
|
||||
conflict? (= :conflict (:status item))
|
||||
copies-of (mf/with-memo [copies item data]
|
||||
(brc/copies-entry copies item
|
||||
(get-in data [:pages-index (:page-id item) :objects])))
|
||||
mode* (mf/use-state :side)
|
||||
mode (deref mode*)]
|
||||
[:*
|
||||
@ -405,19 +429,33 @@
|
||||
(tr "workspace.branches.review.decision.body")])
|
||||
|
||||
;; the summary heads the property table; a decision puts its way in
|
||||
;; on the same line
|
||||
[:div {:class (stl/css :detail-summary-row)}
|
||||
[:> text* {:as "p" :typography t/body-small :class (stl/css :detail-summary)}
|
||||
(brc/change-summary item)]
|
||||
(when (and conflict? on-resolve)
|
||||
[:> button* {:variant "secondary" :on-click on-resolve}
|
||||
(tr "workspace.branches.review.row.choose")])]
|
||||
|
||||
(if (seq visible)
|
||||
[:> brl/value-steps* {:attrs visible}]
|
||||
(when (= :modified (:status item))
|
||||
;; on the same line. A copy the copy rule settled changes nothing:
|
||||
;; its section below says what the rule decided instead
|
||||
(when-not (bm/settled? item)
|
||||
[:*
|
||||
[:div {:class (stl/css :detail-summary-row)}
|
||||
[:> text* {:as "p" :typography t/body-small :class (stl/css :detail-summary)}
|
||||
(tr "workspace.branches.compare.no-props")]))
|
||||
(brc/change-summary item)]
|
||||
(when (and conflict? on-resolve)
|
||||
[:> button* {:variant "secondary" :on-click on-resolve}
|
||||
(tr "workspace.branches.review.row.choose")])]
|
||||
|
||||
(if (seq visible)
|
||||
[:> brl/value-steps* {:attrs visible}]
|
||||
(when (= :modified (:status item))
|
||||
[:> text* {:as "p" :typography t/body-small :class (stl/css :detail-summary)}
|
||||
(tr "workspace.branches.compare.no-props")]))])
|
||||
|
||||
;; the copy rule: what it decided on this copy, how many copies a
|
||||
;; choice on a main decides, and which copies follow the component
|
||||
(when (:settled item)
|
||||
[:> brf/settled-facts* {:settled (:settled item)
|
||||
:is-conflict conflict?
|
||||
:is-settled (bm/settled? item)}])
|
||||
(when (and conflict? (:copies-affected item))
|
||||
[:> brf/copies-affected* {:n (:copies-affected item)}])
|
||||
(when copies-of
|
||||
[:> brf/copies-facts* {:entry copies-of :data data}])
|
||||
|
||||
;; an entry without a before and after says what it is, where it
|
||||
;; lands and what it affects instead
|
||||
@ -431,7 +469,7 @@
|
||||
:data data
|
||||
:open-side open-side
|
||||
:incoming incoming
|
||||
:change-count (count rows)}
|
||||
:change-count (count (remove (comp bm/settled? second) rows))}
|
||||
(for [[idx it] rows]
|
||||
[:> item-row* {:key idx :index idx :item it
|
||||
:inner (get cascades idx)
|
||||
@ -537,8 +575,14 @@
|
||||
(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))
|
||||
;; a copy the copy rule settled entirely is shown, never counted
|
||||
counted (mf/with-memo [listed-items]
|
||||
(filterv (complement bm/settled?) listed-items))
|
||||
counts (mf/with-memo [counted]
|
||||
(group-by brc/category counted))
|
||||
copies (:copies diff)
|
||||
follow-counts (mf/with-memo [copies]
|
||||
(brc/copy-follow-counts copies))
|
||||
|
||||
filter* (mf/use-state :all)
|
||||
active-filter (deref filter*)
|
||||
@ -574,7 +618,7 @@
|
||||
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))
|
||||
reviewed-n (count (filter #(contains? reviewed (review-key %)) counted))
|
||||
|
||||
sel-index (when (= :item (:kind selected)) (:index selected))
|
||||
sel-item (when (and (some? sel-index) (< sel-index (count items)))
|
||||
@ -616,6 +660,7 @@
|
||||
ahead (concat (drop start order) (take start order))
|
||||
next-ix (some (fn [idx]
|
||||
(when (and (not= idx (:index selected))
|
||||
(not (bm/settled? (nth items idx)))
|
||||
(not (contains? reviewed (review-key (nth items idx)))))
|
||||
idx))
|
||||
ahead)]
|
||||
@ -626,7 +671,8 @@
|
||||
(fn []
|
||||
(let [payload (clj->js {:stats (:stats diff)
|
||||
:changes (:changes diff)
|
||||
:conflicts (:conflicts diff)})
|
||||
:conflicts (:conflicts diff)
|
||||
:copies (:copies diff)})
|
||||
text (js/JSON.stringify payload nil 2)
|
||||
blob (js/Blob. #js [text] #js {:type "application/json"})
|
||||
url (js/URL.createObjectURL blob)
|
||||
@ -673,7 +719,7 @@
|
||||
:filters (when (and (= status :loaded) (seq items) (not expanded))
|
||||
(mf/html
|
||||
[:*
|
||||
[:> filter-chip* {:label (dm/str (tr "labels.all") " (" (count listed-items) ")")
|
||||
[:> filter-chip* {:label (dm/str (tr "labels.all") " (" (count counted) ")")
|
||||
:is-selected (= active-filter :all)
|
||||
:on-click #(on-filter :all)}]
|
||||
(for [[cat tone label icon] brc/categories
|
||||
@ -695,6 +741,7 @@
|
||||
[:> change-list* {:groups groups
|
||||
:conflicts conflicts
|
||||
:cascades cascades
|
||||
:follow-counts follow-counts
|
||||
:selected selected
|
||||
:excluded excluded
|
||||
:reviewed reviewed
|
||||
@ -738,6 +785,7 @@
|
||||
:items items
|
||||
:indexed indexed
|
||||
:cascades cascades
|
||||
:copies copies
|
||||
:excluded (contains? excluded (:key sel-item))
|
||||
:excluded-keys excluded
|
||||
:reviewed reviewed
|
||||
@ -768,7 +816,7 @@
|
||||
(str/join " - "
|
||||
(remove nil?
|
||||
[(tr "workspace.branches.review.footer.reviewed"
|
||||
(str reviewed-n) (str (count listed-items)))
|
||||
(str reviewed-n) (str (count counted)))
|
||||
(when (pos? decisions)
|
||||
(tr "workspace.branches.review.footer.blocked" (i18n/c decisions)))
|
||||
(when (pos? left-out)
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
[app.common.time :as ct]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.workspace.branches :as dwb]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
@ -28,6 +29,7 @@
|
||||
[app.main.ui.ds.product.preview-card :refer [preview-card*]]
|
||||
[app.main.ui.ds.product.tag :refer [tag*]]
|
||||
[app.main.ui.workspace.sidebar.branch-review.common :as brc]
|
||||
[app.main.ui.workspace.sidebar.branch-review.facts :as brf]
|
||||
[app.main.ui.workspace.sidebar.branch-review.layout :as brl]
|
||||
[app.main.ui.workspace.sidebar.branch-review.previews :as brp]
|
||||
[app.util.clipboard :as clipboard]
|
||||
@ -271,8 +273,11 @@
|
||||
|
||||
(mf/defc conflict-detail*
|
||||
{::mf/private true}
|
||||
[{:keys [conflict res diff-meta on-pick on-choose on-clear]}]
|
||||
[{:keys [conflict res diff-meta copies data on-pick on-choose on-clear]}]
|
||||
(let [rows (brc/conflict-attr-rows conflict)
|
||||
copies-of (mf/with-memo [copies conflict data]
|
||||
(brc/copies-entry copies conflict
|
||||
(get-in data [:pages-index (:page-id conflict) :objects])))
|
||||
per-attr? (brc/per-attr? conflict)
|
||||
text? (text-clash? conflict)
|
||||
table-rows (if text? (filterv #(not= :content (:attr %)) rows) rows)
|
||||
@ -351,6 +356,15 @@
|
||||
:on-click #(on-pick attr :branch)}
|
||||
(tr "workspace.branches.resolve.keep-branch" (brc/display-val branch))]]))
|
||||
|
||||
;; the copy rule: what it decided on this copy, how many copies a
|
||||
;; choice on a main decides, and which copies follow the component
|
||||
(when (:settled conflict)
|
||||
[:> brf/settled-facts* {:settled (:settled conflict) :is-conflict true}])
|
||||
(when (:copies-affected conflict)
|
||||
[:> brf/copies-affected* {:n (:copies-affected conflict)}])
|
||||
(when copies-of
|
||||
[:> brf/copies-facts* {:entry copies-of :data data}])
|
||||
|
||||
;; once decided, one quiet block says what the merge leaves behind,
|
||||
;; with the way back; until then, a note says how the choice works
|
||||
(if resolved?
|
||||
@ -390,6 +404,8 @@
|
||||
::mf/register-as :branch-conflicts}
|
||||
[{:keys [branch mode conflicts excluded comment]}]
|
||||
(let [{:keys [diff resolutions status]} (mf/deref branch-diff)
|
||||
data (mf/deref refs/workspace-data)
|
||||
copies (:copies diff)
|
||||
|
||||
loading? (= status :loading)
|
||||
selected* (mf/use-state 0)
|
||||
@ -540,6 +556,8 @@
|
||||
:conflict sel
|
||||
:res sel-res
|
||||
:diff-meta diff-meta
|
||||
:copies copies
|
||||
:data data
|
||||
:on-pick on-pick
|
||||
:on-choose on-choose
|
||||
:on-clear on-clear}]))
|
||||
|
||||
@ -8,13 +8,17 @@
|
||||
"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 and, for a layer it lacks, from the diff."
|
||||
affects, from the open file and, for a layer it lacks, from the diff.
|
||||
It also says what the copy rule does with the copies of a changed
|
||||
component."
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
[app.main.ui.ds.foundations.typography :as t]
|
||||
[app.main.ui.ds.foundations.typography.text :refer [text*]]
|
||||
[app.main.ui.ds.product.preview-card :refer [preview-card*]]
|
||||
[app.main.ui.ds.product.tag :refer [tag*]]
|
||||
[app.main.ui.workspace.sidebar.branch-review.common :as brc]
|
||||
[app.main.ui.workspace.sidebar.branch-review.layout :as brl]
|
||||
[app.main.ui.workspace.sidebar.branch-review.previews :as brp]
|
||||
@ -26,12 +30,6 @@
|
||||
"How many board names a page lists before it counts the rest."
|
||||
6)
|
||||
|
||||
(defn- side-label
|
||||
[side]
|
||||
(tr (if (= side :main)
|
||||
"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]
|
||||
@ -70,7 +68,7 @@
|
||||
[:*
|
||||
(if page
|
||||
[:*
|
||||
[:> preview-card* {:label (side-label open-side)
|
||||
[:> preview-card* {:label (brc/side-label open-side)
|
||||
:side (name open-side)
|
||||
:detail (:name page)}
|
||||
[:> brp/page-preview* {:objects (:objects page)}]]
|
||||
@ -93,7 +91,7 @@
|
||||
(tr "workspace.branches.review.facts.boards" (i18n/c (count boards)))
|
||||
(tr "workspace.branches.review.facts.layers" (i18n/c layers)))])]
|
||||
|
||||
[:> preview-card* {:label (side-label (if (= open-side :main) :branch :main))
|
||||
[:> preview-card* {:label (brc/side-label (if (= open-side :main) :branch :main))
|
||||
:side (if (= open-side :main) "branch" "main")}
|
||||
[:> brp/entity-preview* {:kind :page
|
||||
:value nil
|
||||
@ -271,3 +269,112 @@
|
||||
"workspace.branches.review.facts.copies"
|
||||
"workspace.branches.review.facts.layers")
|
||||
(i18n/c n))]]}]))))
|
||||
|
||||
;; --- Component copies
|
||||
|
||||
(defn- lower-list
|
||||
[labels]
|
||||
(str/lower (str/join ", " labels)))
|
||||
|
||||
(defn- copy-lines
|
||||
"What one copy does after the integration, a line each: the overrides
|
||||
it keeps and the side they come from, the token slots left to the
|
||||
user, and the groups that follow the component; or that it follows
|
||||
every change."
|
||||
[copy]
|
||||
(let [keeps (brc/copy-keeps copy)
|
||||
open (brc/copy-open-slots copy)
|
||||
follows (sort (map brc/group-label (:follows copy)))
|
||||
own? (or (seq keeps) (seq open))]
|
||||
(cond-> []
|
||||
(not own?)
|
||||
(conj (tr "workspace.branches.review.copies.follows-all"))
|
||||
|
||||
(seq (:main keeps))
|
||||
(conj (tr "workspace.branches.review.copies.keeps-main" (lower-list (:main keeps))))
|
||||
|
||||
(seq (:branch keeps))
|
||||
(conj (tr "workspace.branches.review.copies.keeps-branch" (lower-list (:branch keeps))))
|
||||
|
||||
(seq open)
|
||||
(conj (tr "workspace.branches.review.copies.slots-open" (lower-list open)))
|
||||
|
||||
(and own? (seq follows))
|
||||
(conj (tr "workspace.branches.review.copies.follows" (lower-list follows))))))
|
||||
|
||||
(defn- keeps-own?
|
||||
[copy]
|
||||
(or (seq (brc/copy-keeps copy)) (seq (brc/copy-open-slots copy))))
|
||||
|
||||
(mf/defc copies-facts*
|
||||
"The copies of a component the integration changes (`entry`, one of the
|
||||
summary's `:copies`), folded under how many follow it: each with its
|
||||
page and what it keeps of its own."
|
||||
[{:keys [entry data]}]
|
||||
(let [open* (mf/use-state false)
|
||||
open? (deref open*)
|
||||
on-toggle (mf/use-fn #(swap! open* not))
|
||||
copies (:copies entry)
|
||||
n (count copies)
|
||||
keeping (count (filter keeps-own? copies))]
|
||||
(when (pos? n)
|
||||
[:div {:class (stl/css :section)}
|
||||
[:> brl/group-head*
|
||||
{:label (cond-> (tr "workspace.branches.review.copies.follow" (i18n/c n))
|
||||
(pos? keeping)
|
||||
(dm/str " - " (tr "workspace.branches.review.copies.keeping" (i18n/c keeping))))
|
||||
:is-open open?
|
||||
:on-toggle on-toggle}]
|
||||
(when open?
|
||||
[:div {:class (stl/css :copy-list)}
|
||||
(for [copy copies]
|
||||
[:div {:class (stl/css :copy) :key (str (:id copy))}
|
||||
[:div {:class (stl/css :copy-head)}
|
||||
[:> i/icon* {:icon-id i/component-copy :size "s" :class (stl/css :copy-icon)}]
|
||||
[:> text* {:as "span" :typography t/body-small :class (stl/css :copy-name)}
|
||||
(:label copy)]
|
||||
[:> text* {:as "span" :typography t/body-small :class (stl/css :copy-page)}
|
||||
(or (get-in data [:pages-index (:page-id copy) :name])
|
||||
(tr "workspace.branches.review.unknown-page"))]]
|
||||
(for [[idx line] (map-indexed vector (copy-lines copy))]
|
||||
[:> text* {:key idx :as "span" :typography t/body-small
|
||||
:class (stl/css :copy-line)}
|
||||
line])])])])))
|
||||
|
||||
(mf/defc copies-affected*
|
||||
"On a conflict about a component's main: how many copies the choice
|
||||
also decides, since they follow the main the merge keeps."
|
||||
[{:keys [n]}]
|
||||
(when (pos? n)
|
||||
[:> brl/notice* {:level :info}
|
||||
(tr "workspace.branches.review.copies.affected" (i18n/c n))]))
|
||||
|
||||
(mf/defc settled-facts*
|
||||
"What the copy rule decided on a copy (`settled`, the entry's
|
||||
`:settled`): each property and token slot with the side it takes and
|
||||
why. On a conflict (`is-conflict`), a note says the user's choice
|
||||
decides only the open ones; on an entry the rule settled entirely,
|
||||
that there is nothing to choose. Nothing shows when the rule decided
|
||||
nothing: the conflict is then an ordinary one."
|
||||
[{:keys [settled is-conflict is-settled]}]
|
||||
(let [rows (brc/settled-rows settled)
|
||||
open (brc/settled-open settled)]
|
||||
(when (seq rows)
|
||||
[:div {:class (stl/css :section)}
|
||||
[:> section-title* {}
|
||||
(tr "workspace.branches.review.copies.settled-title")]
|
||||
(when is-settled
|
||||
[:> brl/notice* {:level :info}
|
||||
(tr "workspace.branches.review.copies.settled-notice")])
|
||||
[:> fact-list*
|
||||
{:rows (mapv (fn [{:keys [label side reason]}]
|
||||
[label
|
||||
(mf/html
|
||||
[:span {:class (stl/css :settled-value)}
|
||||
[:> tag* {:tone (if (= side :main) "purple" "accent")}
|
||||
(brc/side-label side)]
|
||||
[:span (brc/reason-label reason)]])])
|
||||
rows)}]
|
||||
(when (and is-conflict (seq open))
|
||||
[:> brl/notice* {:level :info}
|
||||
(tr "workspace.branches.review.copies.open-note" (lower-list open))])])))
|
||||
|
||||
@ -56,3 +56,55 @@
|
||||
flex-direction: column;
|
||||
gap: var(--sp-xxs);
|
||||
}
|
||||
|
||||
.copy-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-block: var(--sp-xs);
|
||||
border: $b-1 solid var(--color-background-quaternary);
|
||||
border-radius: $br-8;
|
||||
background-color: var(--color-background-tertiary);
|
||||
}
|
||||
|
||||
.copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-xxs);
|
||||
padding: var(--sp-xs) var(--sp-m);
|
||||
}
|
||||
|
||||
.copy-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-xs);
|
||||
min-inline-size: 0;
|
||||
}
|
||||
|
||||
.copy-icon {
|
||||
flex-shrink: 0;
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.copy-name {
|
||||
@include text-ellipsis;
|
||||
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
.copy-page {
|
||||
@include text-ellipsis;
|
||||
|
||||
margin-inline-start: auto;
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.copy-line {
|
||||
padding-inline-start: calc(var(--sp-xs) + $sz-16);
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.settled-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
|
||||
@ -11798,3 +11798,171 @@ msgstr "Expand preview"
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/compare.cljs
|
||||
msgid "workspace.branches.review.view.collapse"
|
||||
msgstr "Collapse preview"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/common.cljs
|
||||
msgid "workspace.branches.review.copies.settled-row"
|
||||
msgstr "Decided by the copy rule"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/compare.cljs
|
||||
msgid "workspace.branches.review.copies.settled-tag"
|
||||
msgstr "Copy rule"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/facts.cljs
|
||||
msgid "workspace.branches.review.copies.follows-all"
|
||||
msgstr "Follows every change"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/facts.cljs
|
||||
msgid "workspace.branches.review.copies.follows"
|
||||
msgstr "Follows the component for %s"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/facts.cljs
|
||||
msgid "workspace.branches.review.copies.keeps-main"
|
||||
msgstr "Keeps its own %s, overridden on main"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/facts.cljs
|
||||
msgid "workspace.branches.review.copies.keeps-branch"
|
||||
msgstr "Keeps its own %s, overridden on the branch"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/facts.cljs
|
||||
msgid "workspace.branches.review.copies.slots-open"
|
||||
msgstr "Both sides override the %s: your choice decides"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/facts.cljs
|
||||
msgid "workspace.branches.review.copies.settled-title"
|
||||
msgstr "Decided by the copy rule"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/facts.cljs
|
||||
msgid "workspace.branches.review.copies.settled-notice"
|
||||
msgstr "The copy rule decides every change on this copy, so there is nothing to choose or leave out."
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/facts.cljs
|
||||
msgid "workspace.branches.review.copies.open-note"
|
||||
msgstr "Your choice decides only %s. The copy rule decides the rest."
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/common.cljs
|
||||
msgid "workspace.branches.review.copies.reason.follows-component"
|
||||
msgstr "follows the component"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/common.cljs
|
||||
msgid "workspace.branches.review.copies.reason.override-main"
|
||||
msgstr "overridden on main"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/common.cljs
|
||||
msgid "workspace.branches.review.copies.reason.override-branch"
|
||||
msgstr "overridden on the branch"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/common.cljs
|
||||
msgid "workspace.branches.review.copies.reason.override-both"
|
||||
msgstr "overridden on both sides"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/common.cljs
|
||||
msgid "workspace.branches.review.copies.token-slot"
|
||||
msgstr "Token for %s"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/common.cljs
|
||||
msgid "workspace.branches.review.copies.group.name"
|
||||
msgstr "Name"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/common.cljs
|
||||
msgid "workspace.branches.review.copies.group.fill"
|
||||
msgstr "Fill"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/common.cljs
|
||||
msgid "workspace.branches.review.copies.group.stroke"
|
||||
msgstr "Stroke"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/common.cljs
|
||||
msgid "workspace.branches.review.copies.group.content"
|
||||
msgstr "Content"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/common.cljs
|
||||
msgid "workspace.branches.review.copies.group.geometry"
|
||||
msgstr "Size and position"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/common.cljs
|
||||
msgid "workspace.branches.review.copies.group.radius"
|
||||
msgstr "Radius"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/common.cljs
|
||||
msgid "workspace.branches.review.copies.group.visibility"
|
||||
msgstr "Visibility"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/common.cljs
|
||||
msgid "workspace.branches.review.copies.group.lock"
|
||||
msgstr "Lock"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/common.cljs
|
||||
msgid "workspace.branches.review.copies.group.font"
|
||||
msgstr "Font"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/common.cljs
|
||||
msgid "workspace.branches.review.copies.group.text-display"
|
||||
msgstr "Text spacing and alignment"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/common.cljs
|
||||
msgid "workspace.branches.review.copies.group.layer"
|
||||
msgstr "Opacity and blend mode"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/common.cljs
|
||||
msgid "workspace.branches.review.copies.group.shadow"
|
||||
msgstr "Shadow"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/common.cljs
|
||||
msgid "workspace.branches.review.copies.group.blur"
|
||||
msgstr "Blur"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/common.cljs
|
||||
msgid "workspace.branches.review.copies.group.mask"
|
||||
msgstr "Mask"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/common.cljs
|
||||
msgid "workspace.branches.review.copies.group.constraints"
|
||||
msgstr "Constraints"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/common.cljs
|
||||
msgid "workspace.branches.review.copies.group.exports"
|
||||
msgstr "Exports"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/common.cljs
|
||||
msgid "workspace.branches.review.copies.group.grids"
|
||||
msgstr "Guides"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/common.cljs
|
||||
msgid "workspace.branches.review.copies.group.clip"
|
||||
msgstr "Clip content"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/common.cljs
|
||||
msgid "workspace.branches.review.copies.group.swap"
|
||||
msgstr "Swapped component"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/common.cljs
|
||||
msgid "workspace.branches.review.copies.group.layout"
|
||||
msgstr "Layout"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/common.cljs
|
||||
msgid "workspace.branches.review.copies.group.layout-item"
|
||||
msgstr "Layout element"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/compare.cljs
|
||||
msgid "workspace.branches.review.copies.row-follow"
|
||||
msgid_plural "workspace.branches.review.copies.row-follow"
|
||||
msgstr[0] "1 copy follows"
|
||||
msgstr[1] "%s copies follow"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/facts.cljs
|
||||
msgid "workspace.branches.review.copies.follow"
|
||||
msgid_plural "workspace.branches.review.copies.follow"
|
||||
msgstr[0] "1 copy will follow this component"
|
||||
msgstr[1] "%s copies will follow this component"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/facts.cljs
|
||||
msgid "workspace.branches.review.copies.keeping"
|
||||
msgid_plural "workspace.branches.review.copies.keeping"
|
||||
msgstr[0] "1 keeps overrides"
|
||||
msgstr[1] "%s keep overrides"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branch_review/facts.cljs
|
||||
msgid "workspace.branches.review.copies.affected"
|
||||
msgid_plural "workspace.branches.review.copies.affected"
|
||||
msgstr[0] "Choosing a side here also decides 1 copy."
|
||||
msgstr[1] "Choosing a side here also decides %s copies."
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user