diff --git a/frontend/src/app/main/ui/workspace/sidebar/branch_review/common.cljs b/frontend/src/app/main/ui/workspace/sidebar/branch_review/common.cljs index 7a978ec177..bcfab4a8fd 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/branch_review/common.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/branch_review/common.cljs @@ -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))))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/branch_review/compare.cljs b/frontend/src/app/main/ui/workspace/sidebar/branch_review/compare.cljs index 0759626733..e036de710e 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/branch_review/compare.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/branch_review/compare.cljs @@ -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) diff --git a/frontend/src/app/main/ui/workspace/sidebar/branch_review/conflicts.cljs b/frontend/src/app/main/ui/workspace/sidebar/branch_review/conflicts.cljs index a66760e6fd..2ce146d6ec 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/branch_review/conflicts.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/branch_review/conflicts.cljs @@ -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}])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/branch_review/facts.cljs b/frontend/src/app/main/ui/workspace/sidebar/branch_review/facts.cljs index 035f2473a8..bb2c75543d 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/branch_review/facts.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/branch_review/facts.cljs @@ -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))])]))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/branch_review/facts.scss b/frontend/src/app/main/ui/workspace/sidebar/branch_review/facts.scss index bdcc4827bb..f7d9594c92 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/branch_review/facts.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/branch_review/facts.scss @@ -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); +} diff --git a/frontend/translations/en.po b/frontend/translations/en.po index fc6403a3d2..7d24ccb4a5 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -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."