mirror of
https://github.com/penpot/penpot.git
synced 2026-10-29 05:50:35 +00:00
Merge remote-tracking branch 'origin/iop-vc-preview' into iop-vc-preview
This commit is contained in:
commit
1c8e4ffe9d
@ -0,0 +1,15 @@
|
||||
{
|
||||
"~:id": "~uc7ce0794-0992-8105-8004-38f280443998",
|
||||
"~:branch-file-id": "~uc7ce0794-0992-8105-8004-38f280443849",
|
||||
"~:source-file-id": "~uc7ce0794-0992-8105-8004-38f280443997",
|
||||
"~:name": "checkout",
|
||||
"~:description": null,
|
||||
"~:status": "open",
|
||||
"~:created-by": "~uc7ce0794-0992-8105-8004-38e630f29a9b",
|
||||
"~:created-at": "~m1713536343369",
|
||||
"~:updated-at": "~m1713873823633",
|
||||
"~:source-name": "New File 1",
|
||||
"~:ahead": null,
|
||||
"~:behind": null,
|
||||
"~:conflicts": null
|
||||
}
|
||||
@ -32,18 +32,22 @@ test("opening a file with pr-id shows the review sandbox banner", async ({
|
||||
|
||||
await expect(page.getByText("Reviewing pull request")).toBeVisible();
|
||||
await expect(page.getByText("Checkout redesign").first()).toBeVisible();
|
||||
await expect(page.getByText("checkout → New File 1")).toBeVisible();
|
||||
|
||||
// the route reads as two pills: the branch under review, then main
|
||||
const banner = page.getByText("Reviewing pull request").locator("../../..");
|
||||
await expect(banner.getByText("checkout", { exact: true })).toBeVisible();
|
||||
await expect(banner.getByText("Main", { exact: true })).toBeVisible();
|
||||
|
||||
// only the essential actions are visible buttons...
|
||||
await expect(page.getByRole("button", { name: "Details" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Compare" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Details" })).toBeHidden();
|
||||
|
||||
// ...the rest live behind the kebab menu
|
||||
const banner = page.getByText("Reviewing pull request").locator("../../..");
|
||||
await banner.getByRole("button", { name: "Options" }).click();
|
||||
await expect(page.getByText("Try interactions")).toBeVisible();
|
||||
await expect(page.getByText("Details")).toBeVisible();
|
||||
await expect(page.getByText("Merge to main")).toBeVisible();
|
||||
await expect(page.getByText("Exit review")).toBeVisible();
|
||||
await expect(page.getByText("Try interactions")).toBeHidden();
|
||||
});
|
||||
|
||||
test("the compare dialog shows the proposed changes", async ({ page }) => {
|
||||
@ -111,15 +115,16 @@ test("closing the pull request asks for confirmation", async ({ page }) => {
|
||||
);
|
||||
await workspacePage.goToWorkspace({ extraParams: `&pr-id=${PR_ID}` });
|
||||
|
||||
await page.getByRole("button", { name: "Details" }).click();
|
||||
await page.getByRole("button", { name: "Close pull request" }).click();
|
||||
const banner = page.getByText("Reviewing pull request").locator("../../..");
|
||||
await banner.getByRole("button", { name: "Options" }).click();
|
||||
await page.getByText("Cancel review").click();
|
||||
|
||||
// nothing is closed yet: a confirmation dialog takes over
|
||||
await expect(page.getByText("will disappear")).toBeVisible();
|
||||
await expect(page.getByText("will be cancelled")).toBeVisible();
|
||||
|
||||
// confirming closes the pull request; being inside its sandbox, the
|
||||
// user is taken back to the file (no pr-id in the url)
|
||||
await page.getByRole("button", { name: "Close pull request" }).click();
|
||||
await page.getByRole("button", { name: "Cancel pull request" }).click();
|
||||
await expect(page).not.toHaveURL(/pr-id/);
|
||||
});
|
||||
|
||||
@ -145,7 +150,7 @@ test("on a branch, requesting a review is the primary action", async ({
|
||||
page.getByRole("button", { name: "Merge to main" }),
|
||||
).toBeHidden();
|
||||
|
||||
const banner = page.getByText("On branch").locator("../../..");
|
||||
const banner = page.locator("#branch-banner");
|
||||
await banner.getByRole("button", { name: "Options" }).click();
|
||||
await page.getByText("Merge to main").click();
|
||||
await expect(page.getByText("Merge into main")).toBeVisible();
|
||||
@ -166,3 +171,56 @@ test("exiting the review takes the user back to the file", async ({ page }) => {
|
||||
await expect(page).not.toHaveURL(/pr-id/);
|
||||
await expect(page.getByText("Reviewing pull request")).toBeHidden();
|
||||
});
|
||||
|
||||
test("during a review the header names the branch under review", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
await setupPullRequestSandbox(workspacePage);
|
||||
// the review link opens the author's branch
|
||||
await workspacePage.mockRPC(
|
||||
"get-file-branch-info?file-id=*",
|
||||
"pull-requests/get-file-branch-info-branch.json",
|
||||
);
|
||||
await workspacePage.goToWorkspace({ extraParams: `&pr-id=${PR_ID}` });
|
||||
|
||||
// the review banner replaces the branch banner, so the header loads the
|
||||
// branch context itself
|
||||
await expect(page.getByText("Reviewing pull request")).toBeVisible();
|
||||
await expect(page.getByTitle("On branch checkout from main")).toBeVisible();
|
||||
});
|
||||
|
||||
test("a branch that cannot be compared with main says so", async ({ page }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
await setupPullRequestSandbox(workspacePage);
|
||||
// the backend answers nil counts when it cannot compare the branch
|
||||
await workspacePage.mockRPC(
|
||||
"get-file-branch-info?file-id=*",
|
||||
"pull-requests/get-file-branch-info-uncompared.json",
|
||||
);
|
||||
await workspacePage.goToWorkspace();
|
||||
|
||||
const banner = page.locator("#branch-banner");
|
||||
await expect(
|
||||
banner.getByText("Couldn't compare this branch with main"),
|
||||
).toBeVisible();
|
||||
await expect(banner.getByText("Up to date with main")).toBeHidden();
|
||||
});
|
||||
|
||||
test("on a branch, the all-branches dialog offers no new branch", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
await setupPullRequestSandbox(workspacePage);
|
||||
await workspacePage.mockRPC(
|
||||
"get-file-branch-info?file-id=*",
|
||||
"pull-requests/get-file-branch-info-branch.json",
|
||||
);
|
||||
await workspacePage.goToWorkspace();
|
||||
|
||||
await page.getByTitle("On branch checkout from main").click();
|
||||
await page.getByText("All branches").click();
|
||||
|
||||
// branches only grow from main: the button stays, disabled
|
||||
await expect(page.getByRole("button", { name: "New branch" })).toBeDisabled();
|
||||
});
|
||||
|
||||
@ -9,17 +9,14 @@
|
||||
patterns in `app.main.data.workspace.versions`."
|
||||
(:require
|
||||
[app.config :as cf]
|
||||
[app.main.broadcast :as mbc]
|
||||
[app.main.data.common :as dcm]
|
||||
[app.main.data.event :as ev]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.data.persistence :as dwp]
|
||||
[app.main.data.workspace.layout :as layout]
|
||||
[app.main.repo :as rp]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.storage :as storage]
|
||||
[beicon.v2.core :as rx]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
@ -56,7 +53,6 @@
|
||||
(declare fetch-branches)
|
||||
(declare fetch-branching-limits)
|
||||
(declare fetch-branch-context)
|
||||
(declare show-branches-panel)
|
||||
|
||||
(defn init-branches-state
|
||||
[]
|
||||
@ -243,8 +239,7 @@
|
||||
(defn create-branch
|
||||
"Create a branch from a file. With no `file-id`, branches the currently
|
||||
open file (force-persisting first so the merge base captures the latest
|
||||
edits) and jumps into the new branch, keeping the branches panel open
|
||||
so the user lands on the branch with full context. With an explicit
|
||||
edits) and jumps into the new branch. With an explicit
|
||||
`file-id` (e.g. from the dashboard, where the file is not open),
|
||||
creates it directly and opens the new branch. Errors surface as a
|
||||
toast; the caller closes the dialog."
|
||||
@ -282,65 +277,67 @@
|
||||
(rx/concat
|
||||
(rx/of (ntf/success (tr "workspace.branches.create.success" name)))
|
||||
(if from-ws?
|
||||
;; jump into the new branch keeping the branches
|
||||
;; panel open (the layout survives in-app navigation;
|
||||
;; show-branches-panel pins the tab selection), and
|
||||
;; refresh the list so the new branch is included
|
||||
(rx/of (open-branch branch-file-id)
|
||||
(show-branches-panel)
|
||||
(fetch-branches))
|
||||
;; jump into the new branch; the branch switcher in
|
||||
;; the header shows where the user landed
|
||||
(rx/of (open-branch branch-file-id))
|
||||
;; from dashboard: jump into the new branch
|
||||
(rx/of (dcm/go-to-workspace :file-id branch-file-id))))))
|
||||
(rx/catch
|
||||
(fn [cause]
|
||||
(rx/of (refusal-notification cause (tr "workspace.branches.create.error"))))))))))))
|
||||
|
||||
(defn toggle-branch-switcher
|
||||
"Open or close the branch switcher popover of the workspace header. With
|
||||
no argument it flips the current state; `open?` forces it."
|
||||
([] (toggle-branch-switcher nil))
|
||||
([open?]
|
||||
(ptk/reify ::toggle-branch-switcher
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [open? (if (some? open?)
|
||||
(boolean open?)
|
||||
(not (:workspace-branch-switcher state)))]
|
||||
(assoc state :workspace-branch-switcher open?))))))
|
||||
|
||||
(defn has-branches?
|
||||
"True when the open file takes part in branching: it is a branch, or it
|
||||
is main and has at least one branch (archived ones count, so they stay
|
||||
reachable). The header only shows the branch switcher then."
|
||||
[state]
|
||||
(or (some? (get-in state [:workspace-branch-context :info]))
|
||||
(boolean (seq (get-in state [:workspace-branches :data])))))
|
||||
|
||||
(defn open-branch
|
||||
"Navigate to the branch file as a normal workspace file. Switching
|
||||
branch must never lose the branches panel: the layout flag survives
|
||||
in-app navigation, but side effects of the transition can still close
|
||||
it (e.g. the version-history interrupt watcher armed by the header
|
||||
button), which makes the switch confusing. So once the target
|
||||
workspace finishes initializing, the panel + Branches tab are
|
||||
re-asserted; when they are already open this is an idempotent no-op."
|
||||
"Navigate to the branch file as a normal workspace file. The branch
|
||||
switcher and any open dialog close; once there, the header shows the
|
||||
branch."
|
||||
[branch-file-id]
|
||||
(assert (uuid? branch-file-id) "expected valid uuid for `branch-file-id`")
|
||||
(ptk/reify ::open-branch
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ stream]
|
||||
(rx/merge
|
||||
(rx/of (dcm/go-to-workspace :file-id branch-file-id))
|
||||
;; keyword literal to avoid requiring the whole app.main.data.workspace ns
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? :app.main.data.workspace/workspace-initialized))
|
||||
(rx/take 1)
|
||||
(rx/mapcat (fn [_] (rx/of (show-branches-panel)
|
||||
(fetch-branches)))))))))
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc state :workspace-branch-switcher false))
|
||||
|
||||
(def history-sidebar-tab-key
|
||||
"Storage + broadcast key for the selected tab of the version-history
|
||||
sidebar. Shared with `app.main.ui.workspace.sidebar`, which reads it
|
||||
through `hooks/use-shared-state` so this event can switch the active
|
||||
tab even when the panel is already open."
|
||||
::history-sidebar-tab)
|
||||
|
||||
(defn show-branches-panel
|
||||
"Open the version-history sidebar and switch it to the Branches tab.
|
||||
No-op when the branching feature flag is disabled (the tab does not
|
||||
exist in that case). Writing storage covers a closed→open panel; the
|
||||
broadcast emit covers an already-open one."
|
||||
[]
|
||||
(ptk/reify ::show-branches-panel
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(when (contains? cf/flags :branching)
|
||||
(rx/of (layout/toggle-layout-flag :document-history :force? true))))
|
||||
;; the all-branches dialog may be the one that asked
|
||||
(rx/of (modal/hide)
|
||||
(dcm/go-to-workspace :file-id branch-file-id)))))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ _ _]
|
||||
(defn show-branches
|
||||
"The entry point of the File menu and the Ctrl/Cmd+Alt+B shortcut. A
|
||||
file that has branches gets the branch switcher; one with none gets the
|
||||
create-branch dialog, since the switcher only exists once a branch does.
|
||||
No-op when the branching feature flag is disabled."
|
||||
[]
|
||||
(ptk/reify ::show-branches
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(when (contains? cf/flags :branching)
|
||||
(swap! storage/user assoc history-sidebar-tab-key "branches")
|
||||
(mbc/emit! history-sidebar-tab-key "branches")))))
|
||||
(if (has-branches? state)
|
||||
(rx/of (toggle-branch-switcher true))
|
||||
(let [file (get-in state [:files (:current-file-id state)])]
|
||||
(rx/of (modal/show :create-branch {:file-name (:name file)}))))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; COMPARE (read-only 3-way diff)
|
||||
|
||||
@ -14,8 +14,6 @@
|
||||
the outdated flag, not the canvas. Mirrors the patterns of
|
||||
`app.main.data.workspace.branches`."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.config :as cf]
|
||||
[app.main.data.event :as ev]
|
||||
[app.main.data.notifications :as ntf]
|
||||
@ -275,24 +273,6 @@
|
||||
(dissoc :page-id))]
|
||||
(rx/of (rt/nav :workspace params))))))
|
||||
|
||||
(defn open-pull-request-viewer
|
||||
"Open the review sandbox in the interactions viewer (new window): the
|
||||
viewer bundle serves the pull request's pinned snapshot, so reviewers
|
||||
can play the prototype exactly as it was published for review."
|
||||
[pr]
|
||||
(assert (uuid? (:id pr)) "expected a pull request row with a valid `:id`")
|
||||
(ptk/reify ::open-pull-request-viewer
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [params {:file-id (:source-file-id pr)
|
||||
:page-id (:current-page-id state)
|
||||
:pr-id (:id pr)
|
||||
:section :interactions}]
|
||||
(rx/of (rt/nav :viewer
|
||||
(d/without-nils params)
|
||||
{::rt/new-window true
|
||||
::rt/window-name (dm/str "viewer-" (:source-file-id pr))}))))))
|
||||
|
||||
(defn- set-pull-request-preview
|
||||
[preview]
|
||||
(ptk/reify ::set-pull-request-preview
|
||||
|
||||
@ -663,7 +663,7 @@
|
||||
:command (ds/ca-mod "b")
|
||||
:subsections [:panels]
|
||||
:fn #(emit-when-no-readonly
|
||||
(dwb/show-branches-panel))}
|
||||
(dwb/show-branches))}
|
||||
|
||||
:toggle-colorpalette {:tooltip (ds/alt "P")
|
||||
:label (fn [] (tr "shortcuts.toggle-colorpalette"))
|
||||
|
||||
@ -5,18 +5,18 @@
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.main.ui.dashboard.branches-popover
|
||||
"Dashboard file-card popover that lists a file's branches and lets you
|
||||
open one or create a new branch without entering the file."
|
||||
"Dashboard file-card popover that lists a file and its branches and
|
||||
opens the one picked. Branches are created from inside the file."
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.workspace.branches :as dwb]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
[app.main.ui.ds.product.branch-row :refer [branch-row*]]
|
||||
[app.main.ui.ds.product.branch-status :refer [branch-status*]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[okulary.core :as l]
|
||||
@ -43,14 +43,6 @@
|
||||
|
||||
on-close (mf/use-fn #(reset! show? false))
|
||||
|
||||
on-create
|
||||
(mf/use-fn
|
||||
(mf/deps file)
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(reset! show? false)
|
||||
(modal/show! :create-branch {:file-name (:name file) :file-id (:id file)})))
|
||||
|
||||
on-open-branch
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
@ -67,24 +59,25 @@
|
||||
[:> i/icon* {:icon-id i/git-branch :size "s"}]
|
||||
[:span (dm/str n)]]
|
||||
|
||||
;; a switcher only: branches are created from inside the file
|
||||
[:& dropdown {:show (deref show?) :on-close on-close}
|
||||
[:div {:class (stl/css :branches-popover)}
|
||||
[:div {:class (stl/css :branches-popover)
|
||||
:role "dialog"
|
||||
:aria-label (tr "dashboard.branches-badge" (dm/str n))}
|
||||
[:> branch-row* {:label (tr "workspace.branches.main")
|
||||
:data-branch (dm/str (:id file))
|
||||
:on-click on-open-branch}
|
||||
(tr "workspace.branches.main-subtitle")]
|
||||
[:div {:class (stl/css :branches-popover-section)}
|
||||
[:span (tr "workspace.branches.menu")]
|
||||
[:span (dm/str (count branches))]]
|
||||
[:ul {:class (stl/css :branches-popover-list)}
|
||||
(for [b branches]
|
||||
[:li {:key (dm/str (:id b))
|
||||
:class (stl/css :branches-popover-item)
|
||||
:role "button"
|
||||
:data-branch (dm/str (:branch-file-id b))
|
||||
:on-click on-open-branch}
|
||||
[:> i/icon* {:icon-id i/git-branch :size "s"}]
|
||||
[:span {:class (stl/css :branches-popover-name)} (:name b)]
|
||||
(if (or (:diff-error b) (nil? (:ahead b)))
|
||||
;; the comparison could not be made: the warning stands
|
||||
;; where the counts would, because zeros read as "in sync"
|
||||
[:span {:class (stl/css :branches-popover-error)
|
||||
:title (tr "workspace.branches.counts.error")}
|
||||
[:> i/icon* {:icon-id i/triangle-alert :size "s"}]]
|
||||
[:span {:class (stl/css :branches-popover-counts)}
|
||||
(dm/str "↑" (:ahead b) " ↓" (:behind b))])])]
|
||||
[:> button* {:variant "primary" :icon i/add :on-click on-create}
|
||||
(tr "workspace.branches.new")]]]]))
|
||||
[:li {:key (dm/str (:id b))}
|
||||
[:> branch-row* {:label (:name b)
|
||||
:data-branch (dm/str (:branch-file-id b))
|
||||
:on-click on-open-branch}
|
||||
[:> branch-status* {:ahead (:ahead b)
|
||||
:behind (:behind b)
|
||||
:conflicts (:conflicts b)
|
||||
:has-error (boolean (or (:diff-error b) (nil? (:ahead b))))}]]])]]]]))
|
||||
|
||||
@ -4,89 +4,68 @@
|
||||
//
|
||||
// Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
@use "ds/typography.scss" as t;
|
||||
@use "refactor/common-refactor.scss" as deprecated;
|
||||
@use "ds/_borders.scss" as *;
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@use "ds/typography.scss" as *;
|
||||
|
||||
// floats over the bottom-right corner of the file thumbnail
|
||||
.branches-popover-wrap {
|
||||
position: absolute;
|
||||
inset-inline-start: deprecated.$s-12;
|
||||
inset-block-start: deprecated.$s-12;
|
||||
inset-inline-start: var(--sp-m);
|
||||
inset-block-start: var(--sp-m);
|
||||
|
||||
// TODO: no DS z-index token for 1.
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.branches-badge {
|
||||
@include t.use-typography("body-small");
|
||||
@include use-typography("body-small");
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-xxs);
|
||||
block-size: $sz-32;
|
||||
padding: var(--sp-xs) var(--sp-s);
|
||||
border: 2px solid var(--color-background-quaternary);
|
||||
border-radius: deprecated.$br-8;
|
||||
height: 32px;
|
||||
border: $b-2 solid var(--color-background-quaternary);
|
||||
border-radius: $br-8;
|
||||
color: var(--color-foreground-secondary);
|
||||
background-color: var(--color-background-tertiary);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-background-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
// out of the flow, under the badge: the badge never moves when it opens,
|
||||
// and the popover is only as wide as its longest branch name allows
|
||||
.branches-popover {
|
||||
position: absolute;
|
||||
inset-block-start: calc(100% + var(--sp-xs));
|
||||
inset-inline-start: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-s);
|
||||
min-inline-size: 240px;
|
||||
gap: var(--sp-xxs);
|
||||
inline-size: max-content;
|
||||
min-inline-size: $sz-224;
|
||||
max-inline-size: $sz-284;
|
||||
padding: var(--sp-s);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--color-background-quaternary);
|
||||
background-color: var(--color-background-secondary);
|
||||
box-shadow: 0 4px 16px rgb(0 0 0 / 0.25);
|
||||
border: $b-1 solid var(--color-background-quaternary);
|
||||
border-radius: $br-8;
|
||||
background-color: var(--color-background-tertiary);
|
||||
box-shadow: var(--el-shadow-dark);
|
||||
}
|
||||
|
||||
.branches-popover-section {
|
||||
@include use-typography("headline-small");
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: var(--sp-s) var(--sp-s) var(--sp-xs);
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.branches-popover-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-xxs);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
max-block-size: 280px;
|
||||
max-block-size: $sz-284;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.branches-popover-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-s);
|
||||
padding: var(--sp-xs) var(--sp-s);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
color: var(--color-foreground-primary);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-background-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
.branches-popover-name {
|
||||
@include t.use-typography("body-medium");
|
||||
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.branches-popover-counts {
|
||||
@include t.use-typography("body-small");
|
||||
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.branches-popover-error {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: var(--color-foreground-warning);
|
||||
}
|
||||
|
||||
@ -4,6 +4,8 @@
|
||||
//
|
||||
// Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
@use "ds/_borders.scss" as *;
|
||||
@use "ds/_utils.scss" as *;
|
||||
@use "refactor/common-refactor.scss" as deprecated;
|
||||
|
||||
.dashboard-comments-section {
|
||||
@ -118,44 +120,53 @@
|
||||
.review-requests {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: deprecated.$s-4;
|
||||
padding: deprecated.$s-8 deprecated.$s-12;
|
||||
border-bottom: deprecated.$s-1 solid var(--color-background-quaternary);
|
||||
gap: var(--sp-xs);
|
||||
padding: var(--sp-s) var(--sp-m);
|
||||
border-block-end: $b-1 solid var(--color-background-quaternary);
|
||||
}
|
||||
|
||||
.review-requests-title {
|
||||
// TODO: No exact DS typography match found.
|
||||
// Closest match: headline-small (size 11 vs 12; family, weight and
|
||||
// line-height are inherited here).
|
||||
font-size: px2rem(11);
|
||||
color: var(--color-foreground-secondary);
|
||||
font-size: deprecated.$fs-11;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.review-request-item {
|
||||
--review-request-item-background-color: none;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: deprecated.$s-2;
|
||||
padding: deprecated.$s-8;
|
||||
gap: var(--sp-xxs);
|
||||
padding: var(--sp-s);
|
||||
border: none;
|
||||
border-radius: deprecated.$s-8;
|
||||
background: none;
|
||||
border-radius: $br-8;
|
||||
background: var(--review-request-item-background-color);
|
||||
cursor: pointer;
|
||||
text-align: start;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-background-tertiary);
|
||||
--review-request-item-background-color: var(--color-background-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
.review-request-name {
|
||||
// TODO: No exact DS typography match found.
|
||||
// Closest match: body-medium (only the size is set here).
|
||||
font-size: px2rem(14);
|
||||
color: var(--color-foreground-primary);
|
||||
font-size: deprecated.$fs-14;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.review-request-route {
|
||||
// TODO: No exact DS typography match found.
|
||||
// Closest match: headline-small (size 11 vs 12; only the size is set here).
|
||||
font-size: px2rem(11);
|
||||
color: var(--color-foreground-secondary);
|
||||
font-size: deprecated.$fs-11;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
@ -7,7 +7,6 @@
|
||||
(ns app.main.ui.dashboard.file-menu
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.config :as cf]
|
||||
[app.main.data.common :as dcm]
|
||||
[app.main.data.dashboard :as dd]
|
||||
[app.main.data.event :as-alias ev]
|
||||
@ -302,17 +301,6 @@
|
||||
[:> menu-item* {:id "file-duplicate" :on-action on-duplicate}
|
||||
(tr "dashboard.duplicate")])
|
||||
|
||||
(when (and (not is-lib-page?)
|
||||
(not is-search-page?)
|
||||
can-edit
|
||||
(contains? cf/flags :branching))
|
||||
[:> menu-item* {:id "file-create-branch"
|
||||
:on-action (fn []
|
||||
(modal/show! :create-branch
|
||||
{:file-name (:name file)
|
||||
:file-id (:id file)}))}
|
||||
(tr "dashboard.create-branch")])
|
||||
|
||||
(when (and (not is-lib-page?)
|
||||
(not is-search-page?)
|
||||
(or (seq current-projects) (seq other-teams))
|
||||
|
||||
@ -415,7 +415,7 @@ $thumbnail-default-height: px2rem(168);
|
||||
align-items: center;
|
||||
margin-inline-end: var(--sp-xs);
|
||||
padding: var(--sp-xxs) var(--sp-xs);
|
||||
border-radius: 8px;
|
||||
border-radius: $br-8;
|
||||
color: var(--color-accent-primary);
|
||||
background-color: var(--color-background-tertiary);
|
||||
}
|
||||
|
||||
@ -33,6 +33,10 @@
|
||||
[app.main.ui.ds.notifications.shared.notification-pill :refer [notification-pill*]]
|
||||
[app.main.ui.ds.notifications.toast :refer [toast*]]
|
||||
[app.main.ui.ds.product.avatar :refer [avatar*]]
|
||||
[app.main.ui.ds.product.branch-chip :refer [branch-chip*]]
|
||||
[app.main.ui.ds.product.branch-pill :refer [branch-pill*]]
|
||||
[app.main.ui.ds.product.branch-row :refer [branch-row*]]
|
||||
[app.main.ui.ds.product.branch-status :refer [branch-status*]]
|
||||
[app.main.ui.ds.product.cta :refer [cta*]]
|
||||
[app.main.ui.ds.product.empty-placeholder :refer [empty-placeholder*]]
|
||||
[app.main.ui.ds.product.empty-state :refer [empty-state*]]
|
||||
@ -81,6 +85,10 @@
|
||||
:Swatch swatch*
|
||||
:Cta cta*
|
||||
:Avatar avatar*
|
||||
:BranchChip branch-chip*
|
||||
:BranchPill branch-pill*
|
||||
:BranchRow branch-row*
|
||||
:BranchStatus branch-status*
|
||||
:Milestone milestone*
|
||||
:MilestoneGroup milestone-group*
|
||||
:Date date*
|
||||
|
||||
@ -78,11 +78,8 @@ $grayish-red: #bfbfbf;
|
||||
--color-accent-error: #{$red-400};
|
||||
--color-background-error: #{$red-200};
|
||||
--color-foreground-error: #{$red-500};
|
||||
--color-foreground-success: #{color.scale($green-500, $lightness: 30%)};
|
||||
--color-foreground-warning: #{color.scale($orange-500, $lightness: 10%)};
|
||||
--color-accent-info: #{$blue-500};
|
||||
--color-background-info: #{$blue-200};
|
||||
--color-foreground-info: #{color.scale($blue-500, $lightness: 28%)};
|
||||
--color-background-default: #{$white};
|
||||
--color-accent-default: #{$gray-100};
|
||||
--color-icon-default: #{$blue-teal-700};
|
||||
@ -126,11 +123,8 @@ $grayish-red: #bfbfbf;
|
||||
--color-accent-error: #{$red-400};
|
||||
--color-background-error: #{$red-950};
|
||||
--color-foreground-error: #{$red-500};
|
||||
--color-foreground-success: #{color.scale($green-500, $lightness: 30%)};
|
||||
--color-foreground-warning: #{color.scale($orange-500, $lightness: 10%)};
|
||||
--color-accent-info: #{$blue-500};
|
||||
--color-background-info: #{$blue-950};
|
||||
--color-foreground-info: #{color.scale($blue-500, $lightness: 28%)};
|
||||
--color-background-default: #{$gray-950};
|
||||
--color-accent-default: #{$gray-800};
|
||||
--color-icon-default: #{$grayish-blue-500};
|
||||
|
||||
45
frontend/src/app/main/ui/ds/product/branch_chip.cljs
Normal file
45
frontend/src/app/main/ui/ds/product/branch_chip.cljs
Normal file
@ -0,0 +1,45 @@
|
||||
;; 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 app.main.ui.ds.product.branch-chip
|
||||
(:require-macros
|
||||
[app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
|
||||
[app.main.ui.ds.foundations.typography :as t]
|
||||
[app.main.ui.ds.foundations.typography.text :refer [text*]]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def ^:private schema:branch-chip
|
||||
[:map
|
||||
[:class {:optional true} [:maybe :string]]
|
||||
[:label :string]
|
||||
[:is-open {:optional true} [:maybe :boolean]]])
|
||||
|
||||
(mf/defc branch-chip*
|
||||
"The button that names a branch and opens the branch switcher. It only
|
||||
draws the trigger: the popover it opens belongs to the caller."
|
||||
{::mf/schema schema:branch-chip}
|
||||
[{:keys [class label is-open] :rest props}]
|
||||
(let [is-open (d/nilv is-open false)
|
||||
props (mf/spread-props props
|
||||
{:class [class (stl/css-case :branch-chip true
|
||||
:is-open is-open)]
|
||||
:type "button"
|
||||
:aria-haspopup "dialog"
|
||||
:aria-expanded is-open})]
|
||||
[:> :button props
|
||||
[:> icon* {:icon-id i/git-branch
|
||||
:size "s"
|
||||
:class (stl/css :icon)}]
|
||||
[:> text* {:as "span"
|
||||
:typography t/body-small
|
||||
:class (stl/css :label)}
|
||||
label]
|
||||
[:> icon* {:icon-id i/arrow-down
|
||||
:size "s"
|
||||
:class (stl/css :icon)}]]))
|
||||
51
frontend/src/app/main/ui/ds/product/branch_chip.mdx
Normal file
51
frontend/src/app/main/ui/ds/product/branch_chip.mdx
Normal file
@ -0,0 +1,51 @@
|
||||
{ /* 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 */ }
|
||||
|
||||
import { Canvas, Meta } from '@storybook/addon-docs/blocks';
|
||||
import * as BranchChip from "./branch_chip.stories";
|
||||
|
||||
<Meta title="Product/Branching/BranchChip" />
|
||||
|
||||
# BranchChip
|
||||
|
||||
The button that names the branch the open file is and opens the branch
|
||||
switcher.
|
||||
|
||||
<Canvas of={BranchChip.Default} />
|
||||
|
||||
## Variants
|
||||
|
||||
`is-open` draws the chip pressed while its popover is shown.
|
||||
|
||||
<Canvas of={BranchChip.Open} />
|
||||
|
||||
Long names are truncated with an ellipsis; the chip never grows past its
|
||||
container.
|
||||
|
||||
<Canvas of={BranchChip.LongName} />
|
||||
|
||||
## Technical notes
|
||||
|
||||
The label is mandatory. Any other prop (`on-click`, `title`, `aria-*`)
|
||||
goes to the underlying `button`, which already carries
|
||||
`aria-haspopup="dialog"` and `aria-expanded`.
|
||||
|
||||
```clj
|
||||
[:> branch-chip* {:label (:name branch)
|
||||
:is-open open?
|
||||
:on-click on-toggle}]
|
||||
```
|
||||
|
||||
## Usage guidelines (design)
|
||||
|
||||
### Where to use
|
||||
|
||||
In the workspace header row, only when the file has branches.
|
||||
|
||||
### Interaction / Behavior
|
||||
|
||||
Clicking it opens or closes the branch switcher. The popover is the
|
||||
caller's: the chip only draws the trigger.
|
||||
48
frontend/src/app/main/ui/ds/product/branch_chip.scss
Normal file
48
frontend/src/app/main/ui/ds/product/branch_chip.scss
Normal file
@ -0,0 +1,48 @@
|
||||
// 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
|
||||
|
||||
@use "ds/_borders.scss" as *;
|
||||
|
||||
.branch-chip {
|
||||
--chip-bg-color: var(--color-background-tertiary);
|
||||
--chip-border-color: var(--color-background-quaternary);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-xs);
|
||||
min-inline-size: 0;
|
||||
max-inline-size: 100%;
|
||||
padding: var(--sp-xs) var(--sp-s);
|
||||
border: $b-1 solid var(--chip-border-color);
|
||||
border-radius: $br-8;
|
||||
background-color: var(--chip-bg-color);
|
||||
color: var(--color-foreground-primary);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
--chip-bg-color: var(--color-background-quaternary);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: $b-1 solid var(--color-accent-primary);
|
||||
}
|
||||
|
||||
&.is-open {
|
||||
--chip-bg-color: var(--color-background-quaternary);
|
||||
--chip-border-color: var(--color-accent-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
flex-shrink: 0;
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.label {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
53
frontend/src/app/main/ui/ds/product/branch_chip.stories.jsx
Normal file
53
frontend/src/app/main/ui/ds/product/branch_chip.stories.jsx
Normal file
@ -0,0 +1,53 @@
|
||||
// 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
|
||||
|
||||
import * as React from "react";
|
||||
import Components from "@target/components";
|
||||
|
||||
const { BranchChip } = Components;
|
||||
|
||||
export default {
|
||||
title: "Product/Branching/BranchChip",
|
||||
component: BranchChip,
|
||||
argTypes: {
|
||||
label: {
|
||||
control: { type: "text" },
|
||||
},
|
||||
isOpen: {
|
||||
control: { type: "boolean" },
|
||||
},
|
||||
},
|
||||
args: {
|
||||
label: "feature-login",
|
||||
isOpen: false,
|
||||
},
|
||||
render: ({ ...args }) => <BranchChip {...args} />,
|
||||
};
|
||||
|
||||
export const Default = {};
|
||||
|
||||
export const Main = {
|
||||
args: {
|
||||
label: "Main",
|
||||
},
|
||||
};
|
||||
|
||||
export const Open = {
|
||||
args: {
|
||||
isOpen: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const LongName = {
|
||||
args: {
|
||||
label: "redesign/checkout-flow-v2-with-a-very-long-name",
|
||||
},
|
||||
render: ({ ...args }) => (
|
||||
<div style={{ inlineSize: "200px" }}>
|
||||
<BranchChip {...args} />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
43
frontend/src/app/main/ui/ds/product/branch_pill.cljs
Normal file
43
frontend/src/app/main/ui/ds/product/branch_pill.cljs
Normal file
@ -0,0 +1,43 @@
|
||||
;; 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 app.main.ui.ds.product.branch-pill
|
||||
(:require-macros
|
||||
[app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
|
||||
[app.main.ui.ds.foundations.typography :as t]
|
||||
[app.main.ui.ds.foundations.typography.text :refer [text*]]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def ^:private schema:branch-pill
|
||||
[:map
|
||||
[:class {:optional true} [:maybe :string]]
|
||||
[:label :string]
|
||||
[:is-main {:optional true} [:maybe :boolean]]
|
||||
[:is-review {:optional true} [:maybe :boolean]]])
|
||||
|
||||
(mf/defc branch-pill*
|
||||
"A branch name inside running text, as a static pill. Main is drawn
|
||||
muted; any other branch in the accent color, or in the info blue when
|
||||
it is the branch of a pull request under review."
|
||||
{::mf/schema schema:branch-pill}
|
||||
[{:keys [class label is-main is-review] :rest props}]
|
||||
(let [is-main (d/nilv is-main false)
|
||||
is-review (d/nilv is-review false)
|
||||
props (mf/spread-props props
|
||||
{:class [class (stl/css-case :branch-pill true
|
||||
:is-main is-main
|
||||
:is-review (and is-review (not is-main)))]})]
|
||||
[:> :span props
|
||||
[:> icon* {:icon-id (if is-main i/git-commit-vertical i/git-branch)
|
||||
:size "s"
|
||||
:class (stl/css :icon)}]
|
||||
[:> text* {:as "span"
|
||||
:typography t/body-small
|
||||
:class (stl/css :label)}
|
||||
label]]))
|
||||
53
frontend/src/app/main/ui/ds/product/branch_pill.mdx
Normal file
53
frontend/src/app/main/ui/ds/product/branch_pill.mdx
Normal file
@ -0,0 +1,53 @@
|
||||
{ /* 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 */ }
|
||||
|
||||
import { Canvas, Meta } from '@storybook/addon-docs/blocks';
|
||||
import * as BranchPill from "./branch_pill.stories";
|
||||
|
||||
<Meta title="Product/Branching/BranchPill" />
|
||||
|
||||
# BranchPill
|
||||
|
||||
A branch name inside running text, drawn as a static pill so it reads
|
||||
apart from the words around it.
|
||||
|
||||
<Canvas of={BranchPill.Default} />
|
||||
|
||||
## Variants
|
||||
|
||||
`is-main` draws main muted, with the commit icon.
|
||||
|
||||
<Canvas of={BranchPill.Main} />
|
||||
|
||||
`is-review` draws the branch in the info blue that marks pull requests
|
||||
and design reviews. It has no effect on main.
|
||||
|
||||
<Canvas of={BranchPill.Review} />
|
||||
|
||||
Two pills and an arrow name where a change goes.
|
||||
|
||||
<Canvas of={BranchPill.Route} />
|
||||
|
||||
## Technical notes
|
||||
|
||||
The label is mandatory. Any other prop goes to the underlying `span`.
|
||||
|
||||
```clj
|
||||
[:> branch-pill* {:label (:branch-name pr) :is-review true}]
|
||||
[:> branch-pill* {:label (tr "workspace.branches.main") :is-main true}]
|
||||
```
|
||||
|
||||
## Usage guidelines (design)
|
||||
|
||||
### Where to use
|
||||
|
||||
Wherever a branch is named inside a line of text: the compare dialog
|
||||
header, the pull request banner.
|
||||
|
||||
### Interaction / Behavior
|
||||
|
||||
None: it is a label, not a control. For a branch that opens the
|
||||
switcher, use `BranchChip`.
|
||||
40
frontend/src/app/main/ui/ds/product/branch_pill.scss
Normal file
40
frontend/src/app/main/ui/ds/product/branch_pill.scss
Normal file
@ -0,0 +1,40 @@
|
||||
// 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
|
||||
|
||||
@use "ds/_borders.scss" as *;
|
||||
|
||||
.branch-pill {
|
||||
--pill-fg-color: var(--color-accent-primary);
|
||||
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
gap: var(--sp-xxs);
|
||||
min-inline-size: 0;
|
||||
max-inline-size: 100%;
|
||||
padding: var(--sp-xxs) var(--sp-s);
|
||||
border-radius: $br-8;
|
||||
background-color: var(--color-background-tertiary);
|
||||
color: var(--pill-fg-color);
|
||||
|
||||
&.is-main {
|
||||
--pill-fg-color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
&.is-review {
|
||||
--pill-fg-color: var(--color-accent-info);
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
57
frontend/src/app/main/ui/ds/product/branch_pill.stories.jsx
Normal file
57
frontend/src/app/main/ui/ds/product/branch_pill.stories.jsx
Normal file
@ -0,0 +1,57 @@
|
||||
// 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
|
||||
|
||||
import * as React from "react";
|
||||
import Components from "@target/components";
|
||||
|
||||
const { BranchPill } = Components;
|
||||
|
||||
export default {
|
||||
title: "Product/Branching/BranchPill",
|
||||
component: BranchPill,
|
||||
argTypes: {
|
||||
label: {
|
||||
control: { type: "text" },
|
||||
},
|
||||
isMain: {
|
||||
control: { type: "boolean" },
|
||||
},
|
||||
isReview: {
|
||||
control: { type: "boolean" },
|
||||
},
|
||||
},
|
||||
args: {
|
||||
label: "feature-login",
|
||||
isMain: false,
|
||||
isReview: false,
|
||||
},
|
||||
render: ({ ...args }) => <BranchPill {...args} />,
|
||||
};
|
||||
|
||||
export const Default = {};
|
||||
|
||||
export const Main = {
|
||||
args: {
|
||||
label: "Main",
|
||||
isMain: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const Review = {
|
||||
args: {
|
||||
isReview: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const Route = {
|
||||
render: () => (
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
|
||||
<BranchPill label="feature-login" isReview={true} />
|
||||
<span>→</span>
|
||||
<BranchPill label="Main" isMain={true} />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
46
frontend/src/app/main/ui/ds/product/branch_row.cljs
Normal file
46
frontend/src/app/main/ui/ds/product/branch_row.cljs
Normal file
@ -0,0 +1,46 @@
|
||||
;; 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 app.main.ui.ds.product.branch-row
|
||||
(:require-macros
|
||||
[app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
|
||||
[app.main.ui.ds.foundations.typography :as t]
|
||||
[app.main.ui.ds.foundations.typography.text :refer [text*]]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def ^:private schema:branch-row
|
||||
[:map
|
||||
[:class {:optional true} [:maybe :string]]
|
||||
[:label :string]
|
||||
[:is-current {:optional true} [:maybe :boolean]]])
|
||||
|
||||
(mf/defc branch-row*
|
||||
"A branch in a short list, as a button that opens it. The current one is
|
||||
marked and cannot be opened again. `children` go to the end of the row:
|
||||
usually a `branch-status*`."
|
||||
{::mf/schema schema:branch-row}
|
||||
[{:keys [class label is-current children] :rest props}]
|
||||
(let [is-current (d/nilv is-current false)
|
||||
props (mf/spread-props props
|
||||
{:class [class (stl/css-case :branch-row true
|
||||
:is-current is-current)]
|
||||
:type "button"
|
||||
:disabled is-current
|
||||
:aria-current (when is-current "true")})]
|
||||
[:> :button props
|
||||
[:> icon* {:icon-id (if is-current i/tick i/git-branch)
|
||||
:size "s"
|
||||
:class (stl/css :icon)}]
|
||||
[:> text* {:as "span"
|
||||
:typography t/body-small
|
||||
:class (stl/css :label)}
|
||||
label]
|
||||
(when (some? children)
|
||||
[:span {:class (stl/css :aside)}
|
||||
children])]))
|
||||
39
frontend/src/app/main/ui/ds/product/branch_row.mdx
Normal file
39
frontend/src/app/main/ui/ds/product/branch_row.mdx
Normal file
@ -0,0 +1,39 @@
|
||||
{ /* 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 */ }
|
||||
|
||||
import { Canvas, Meta } from '@storybook/addon-docs/blocks';
|
||||
import * as BranchRow from "./branch_row.stories";
|
||||
|
||||
<Meta title="Product/Branching/BranchRow" />
|
||||
|
||||
# BranchRow
|
||||
|
||||
A branch in a short list, such as the branch switcher or the dashboard
|
||||
branches popover. Clicking it opens the branch.
|
||||
|
||||
<Canvas of={BranchRow.Default} />
|
||||
|
||||
## Variants
|
||||
|
||||
The branch the open file is shows a tick and cannot be opened again.
|
||||
|
||||
<Canvas of={BranchRow.Current} />
|
||||
|
||||
Children go to the end of the row, usually a `BranchStatus`.
|
||||
|
||||
<Canvas of={BranchRow.WithStatus} />
|
||||
|
||||
## Technical notes
|
||||
|
||||
The label is mandatory. The row is a `button`; any other prop
|
||||
(`on-click`, `data-*`) goes to it.
|
||||
|
||||
```clj
|
||||
[:> branch-row* {:label (:name branch)
|
||||
:data-branch (dm/str (:branch-file-id branch))
|
||||
:on-click on-open}
|
||||
[:> branch-status* {:ahead (:ahead branch) :behind (:behind branch)}]]
|
||||
```
|
||||
64
frontend/src/app/main/ui/ds/product/branch_row.scss
Normal file
64
frontend/src/app/main/ui/ds/product/branch_row.scss
Normal file
@ -0,0 +1,64 @@
|
||||
// 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
|
||||
|
||||
@use "ds/_borders.scss" as *;
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@use "ds/typography.scss" as *;
|
||||
|
||||
.branch-row {
|
||||
--row-bg-color: transparent;
|
||||
--row-icon-color: var(--color-foreground-secondary);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-s);
|
||||
inline-size: 100%;
|
||||
min-block-size: $sz-32;
|
||||
padding: var(--sp-xs) var(--sp-s);
|
||||
border: none;
|
||||
border-radius: $br-8;
|
||||
background-color: var(--row-bg-color);
|
||||
color: var(--color-foreground-primary);
|
||||
text-align: start;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
--row-bg-color: var(--color-background-quaternary);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: $b-1 solid var(--color-accent-primary);
|
||||
}
|
||||
|
||||
&.is-current {
|
||||
--row-icon-color: var(--color-accent-primary);
|
||||
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
flex-shrink: 0;
|
||||
color: var(--row-icon-color);
|
||||
}
|
||||
|
||||
.label {
|
||||
flex: 1;
|
||||
min-inline-size: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.aside {
|
||||
@include use-typography("body-small");
|
||||
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
gap: var(--sp-s);
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
51
frontend/src/app/main/ui/ds/product/branch_row.stories.jsx
Normal file
51
frontend/src/app/main/ui/ds/product/branch_row.stories.jsx
Normal file
@ -0,0 +1,51 @@
|
||||
// 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
|
||||
|
||||
import * as React from "react";
|
||||
import Components from "@target/components";
|
||||
|
||||
const { BranchRow, BranchStatus } = Components;
|
||||
|
||||
export default {
|
||||
title: "Product/Branching/BranchRow",
|
||||
component: BranchRow,
|
||||
argTypes: {
|
||||
label: {
|
||||
control: { type: "text" },
|
||||
},
|
||||
isCurrent: {
|
||||
control: { type: "boolean" },
|
||||
},
|
||||
},
|
||||
args: {
|
||||
label: "onboarding-v2",
|
||||
isCurrent: false,
|
||||
},
|
||||
render: ({ ...args }) => (
|
||||
<div style={{ inlineSize: "270px" }}>
|
||||
<BranchRow {...args} />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const Default = {};
|
||||
|
||||
export const Current = {
|
||||
args: {
|
||||
label: "feature-login",
|
||||
isCurrent: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithStatus = {
|
||||
render: ({ ...args }) => (
|
||||
<div style={{ inlineSize: "270px" }}>
|
||||
<BranchRow {...args}>
|
||||
<BranchStatus ahead={5} review="approved" approvals={2} reviewers={2} />
|
||||
</BranchRow>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
79
frontend/src/app/main/ui/ds/product/branch_status.cljs
Normal file
79
frontend/src/app/main/ui/ds/product/branch_status.cljs
Normal file
@ -0,0 +1,79 @@
|
||||
;; 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 app.main.ui.ds.product.branch-status
|
||||
(:require-macros
|
||||
[app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
|
||||
[app.main.ui.ds.foundations.typography :as t]
|
||||
[app.main.ui.ds.foundations.typography.text :refer [text*]]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def ^:private schema:branch-status
|
||||
[:map
|
||||
[:class {:optional true} [:maybe :string]]
|
||||
[:ahead {:optional true} [:maybe :int]]
|
||||
[:behind {:optional true} [:maybe :int]]
|
||||
[:has-error {:optional true} [:maybe :boolean]]
|
||||
[:conflicts {:optional true} [:maybe :int]]
|
||||
[:review {:optional true} [:maybe [:enum "pending" "approved"]]]
|
||||
[:approvals {:optional true} [:maybe :int]]
|
||||
[:reviewers {:optional true} [:maybe :int]]])
|
||||
|
||||
(mf/defc branch-status*
|
||||
"How a branch stands against main: the changes it has and lacks, and one
|
||||
pill. Conflicts take the pill over the review state, because they block
|
||||
the merge whatever the reviewers say. A comparison that failed shows a
|
||||
warning instead of the counts, since zeros would read as in sync."
|
||||
{::mf/schema schema:branch-status}
|
||||
[{:keys [class ahead behind has-error conflicts review approvals reviewers] :rest props}]
|
||||
(let [ahead (d/nilv ahead 0)
|
||||
behind (d/nilv behind 0)
|
||||
conflicts (d/nilv conflicts 0)
|
||||
approvals (d/nilv approvals 0)
|
||||
reviewers (d/nilv reviewers 0)
|
||||
props (mf/spread-props props
|
||||
{:class [class (stl/css :branch-status)]})]
|
||||
[:> :div props
|
||||
(if ^boolean has-error
|
||||
[:span {:class (stl/css :error)
|
||||
:title (tr "workspace.branches.counts.error")}
|
||||
[:> icon* {:icon-id i/triangle-alert :size "s"}]]
|
||||
[:*
|
||||
(when (pos? ahead)
|
||||
[:span {:class (stl/css :count)
|
||||
:title (tr "workspace.branches.counts.ahead")}
|
||||
[:> icon* {:icon-id i/arrow-up :size "s"}]
|
||||
(dm/str ahead)])
|
||||
(when (pos? behind)
|
||||
[:span {:class (stl/css :count :count-behind)
|
||||
:title (tr "workspace.branches.counts.behind")}
|
||||
[:> icon* {:icon-id i/arrow-down :size "s"}]
|
||||
(dm/str behind)])])
|
||||
|
||||
(cond
|
||||
(pos? conflicts)
|
||||
[:> text* {:as "span"
|
||||
:typography t/body-small
|
||||
:class (stl/css :pill :pill-conflict)
|
||||
:title (tr "workspace.branches.banner.conflicts" (dm/str conflicts))}
|
||||
[:> icon* {:icon-id i/triangle-alert :size "s"}]
|
||||
(dm/str conflicts)]
|
||||
|
||||
(some? review)
|
||||
[:> text* {:as "span"
|
||||
:typography t/body-small
|
||||
:class (stl/css-case :pill true
|
||||
:pill-pending (= review "pending")
|
||||
:pill-approved (= review "approved"))
|
||||
:title (tr "workspace.pull-requests.info.approvals")}
|
||||
[:> icon* {:icon-id i/git-pull-request-arrow :size "s"}]
|
||||
(when (pos? reviewers)
|
||||
(dm/str approvals "/" reviewers))])]))
|
||||
56
frontend/src/app/main/ui/ds/product/branch_status.mdx
Normal file
56
frontend/src/app/main/ui/ds/product/branch_status.mdx
Normal file
@ -0,0 +1,56 @@
|
||||
{ /* 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 */ }
|
||||
|
||||
import { Canvas, Meta } from '@storybook/addon-docs/blocks';
|
||||
import * as BranchStatus from "./branch_status.stories";
|
||||
|
||||
<Meta title="Product/Branching/BranchStatus" />
|
||||
|
||||
# BranchStatus
|
||||
|
||||
How a branch stands against main: the changes it has (↑) and lacks (↓),
|
||||
and one pill with what blocks or moves its merge.
|
||||
|
||||
<Canvas of={BranchStatus.Default} />
|
||||
|
||||
## Variants
|
||||
|
||||
The pill shows the review state of an open pull request, pending or
|
||||
approved, with its approvals.
|
||||
|
||||
<Canvas of={BranchStatus.InReview} />
|
||||
|
||||
<Canvas of={BranchStatus.Approved} />
|
||||
|
||||
Conflicts take the pill over the review state, because they block the
|
||||
merge whatever the reviewers say.
|
||||
|
||||
<Canvas of={BranchStatus.WithConflicts} />
|
||||
|
||||
When the comparison with main fails, a warning replaces the counts:
|
||||
zeros would read as "in sync".
|
||||
|
||||
<Canvas of={BranchStatus.ComparisonFailed} />
|
||||
|
||||
## Technical notes
|
||||
|
||||
Every prop is optional. Counts at zero are not drawn.
|
||||
|
||||
```clj
|
||||
[:> branch-status* {:ahead (:ahead branch)
|
||||
:behind (:behind branch)
|
||||
:conflicts (:conflicts branch)
|
||||
:review "pending"
|
||||
:approvals 1
|
||||
:reviewers 2}]
|
||||
```
|
||||
|
||||
## Usage guidelines (design)
|
||||
|
||||
### Where to use
|
||||
|
||||
Next to a branch name: the workspace header row, the branch switcher and
|
||||
the branch lists.
|
||||
64
frontend/src/app/main/ui/ds/product/branch_status.scss
Normal file
64
frontend/src/app/main/ui/ds/product/branch_status.scss
Normal file
@ -0,0 +1,64 @@
|
||||
// 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
|
||||
|
||||
@use "ds/_borders.scss" as *;
|
||||
@use "ds/typography.scss" as *;
|
||||
|
||||
.branch-status {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
|
||||
// counts use the DS code font, as every branch figure does; they
|
||||
// only show when there are changes, so they stand out
|
||||
.count {
|
||||
@include use-typography("code-font");
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-xxs);
|
||||
color: var(--color-accent-primary);
|
||||
}
|
||||
|
||||
// what main has and the branch lacks reads in the error color
|
||||
.count-behind {
|
||||
color: var(--color-foreground-error);
|
||||
}
|
||||
|
||||
.error {
|
||||
display: flex;
|
||||
color: var(--color-accent-warning);
|
||||
}
|
||||
|
||||
.pill {
|
||||
--pill-fg-color: var(--color-foreground-secondary);
|
||||
--pill-bg-color: var(--color-background-tertiary);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-xxs);
|
||||
padding: 0 var(--sp-xs);
|
||||
border-radius: $br-4;
|
||||
color: var(--pill-fg-color);
|
||||
background-color: var(--pill-bg-color);
|
||||
}
|
||||
|
||||
.pill-conflict {
|
||||
--pill-fg-color: var(--color-accent-warning);
|
||||
--pill-bg-color: var(--color-background-warning);
|
||||
}
|
||||
|
||||
.pill-pending {
|
||||
--pill-fg-color: var(--color-accent-info);
|
||||
--pill-bg-color: var(--color-background-info);
|
||||
}
|
||||
|
||||
.pill-approved {
|
||||
--pill-fg-color: var(--color-accent-success);
|
||||
--pill-bg-color: var(--color-background-success);
|
||||
}
|
||||
@ -0,0 +1,82 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
import * as React from "react";
|
||||
import Components from "@target/components";
|
||||
|
||||
const { BranchStatus } = Components;
|
||||
|
||||
export default {
|
||||
title: "Product/Branching/BranchStatus",
|
||||
component: BranchStatus,
|
||||
argTypes: {
|
||||
ahead: {
|
||||
control: { type: "number", min: 0 },
|
||||
},
|
||||
behind: {
|
||||
control: { type: "number", min: 0 },
|
||||
},
|
||||
hasError: {
|
||||
control: { type: "boolean" },
|
||||
},
|
||||
conflicts: {
|
||||
control: { type: "number", min: 0 },
|
||||
},
|
||||
review: {
|
||||
control: "select",
|
||||
options: [undefined, "pending", "approved"],
|
||||
},
|
||||
approvals: {
|
||||
control: { type: "number", min: 0 },
|
||||
},
|
||||
reviewers: {
|
||||
control: { type: "number", min: 0 },
|
||||
},
|
||||
},
|
||||
args: {
|
||||
ahead: 3,
|
||||
behind: 1,
|
||||
hasError: false,
|
||||
conflicts: 0,
|
||||
},
|
||||
render: ({ ...args }) => <BranchStatus {...args} />,
|
||||
};
|
||||
|
||||
export const Default = {};
|
||||
|
||||
export const InReview = {
|
||||
args: {
|
||||
behind: 0,
|
||||
review: "pending",
|
||||
approvals: 1,
|
||||
reviewers: 2,
|
||||
},
|
||||
};
|
||||
|
||||
export const Approved = {
|
||||
args: {
|
||||
behind: 0,
|
||||
review: "approved",
|
||||
approvals: 2,
|
||||
reviewers: 2,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithConflicts = {
|
||||
args: {
|
||||
behind: 4,
|
||||
conflicts: 2,
|
||||
review: "pending",
|
||||
approvals: 1,
|
||||
reviewers: 2,
|
||||
},
|
||||
};
|
||||
|
||||
export const ComparisonFailed = {
|
||||
args: {
|
||||
hasError: true,
|
||||
},
|
||||
};
|
||||
309
frontend/src/app/main/ui/workspace/branch_switcher.cljs
Normal file
309
frontend/src/app/main/ui/workspace/branch_switcher.cljs
Normal file
@ -0,0 +1,309 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.main.ui.workspace.branch-switcher
|
||||
"The branch row under the workspace left header: a chip naming the
|
||||
branch the open file is, its status next to it, and a popover to switch
|
||||
branch, create one or reach the full list. It only exists once the file
|
||||
has branches; before that the first branch comes from the main menu."
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.workspace.branches :as dwb]
|
||||
[app.main.data.workspace.pull-requests :as dwpr]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[app.main.ui.ds.controls.input :refer [input*]]
|
||||
[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.branch-chip :refer [branch-chip*]]
|
||||
[app.main.ui.ds.product.branch-row :refer [branch-row*]]
|
||||
[app.main.ui.ds.product.branch-status :refer [branch-status*]]
|
||||
[app.main.ui.workspace.menu-shortcuts :refer [shortcuts*]]
|
||||
[app.main.ui.workspace.sidebar.pull-requests :as prui]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[cuerdas.core :as str]
|
||||
[okulary.core :as l]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def has-branches
|
||||
"True when the open file is a branch or has branches; the branch row and
|
||||
its chip only show then."
|
||||
(l/derived dwb/has-branches? st/state))
|
||||
|
||||
(def ^:private branches
|
||||
(l/derived :workspace-branches st/state))
|
||||
|
||||
(def ^:private switcher-open
|
||||
(l/derived #(boolean (:workspace-branch-switcher %)) st/state))
|
||||
|
||||
(defn- open-pr-by-branch
|
||||
"The open pull requests indexed by the id of their branch."
|
||||
[prs]
|
||||
(->> (:data prs)
|
||||
(filter #(= "open" (:status %)))
|
||||
(map (juxt :file-branch-id identity))
|
||||
(into {})))
|
||||
|
||||
(mf/defc row-status*
|
||||
"The `branch-status*` of a branch row and its open pull request (nil
|
||||
when it has none)."
|
||||
{::mf/private true}
|
||||
[{:keys [row pr]}]
|
||||
(let [[approvals reviewers] (if pr (prui/current-approvals pr) [0 0])
|
||||
review (when (some? pr)
|
||||
(if (and (pos? reviewers) (= approvals reviewers)) "approved" "pending"))]
|
||||
[:> branch-status* {:ahead (:ahead row)
|
||||
:behind (:behind row)
|
||||
:has-error (boolean (or (:diff-error row) (nil? (:ahead row))))
|
||||
:conflicts (:conflicts row)
|
||||
:review review
|
||||
:approvals approvals
|
||||
:reviewers reviewers}]))
|
||||
|
||||
(defn- in-sync?
|
||||
"True when a branch has nothing to report against main: no changes on
|
||||
either side, no conflicts, no open review and a comparison that worked."
|
||||
[row pr]
|
||||
(and (nil? pr)
|
||||
(not (:diff-error row))
|
||||
(some? (:ahead row))
|
||||
(zero? (:ahead row))
|
||||
(zero? (or (:behind row) 0))
|
||||
(zero? (or (:conflicts row) 0))))
|
||||
|
||||
;; --- Popover
|
||||
|
||||
(mf/defc switcher-row*
|
||||
{::mf/private true}
|
||||
[{:keys [row pr current on-open]}]
|
||||
[:li
|
||||
[:> branch-row* {:label (:name row)
|
||||
:is-current (boolean current)
|
||||
:data-branch (dm/str (:branch-file-id row))
|
||||
:on-click on-open}
|
||||
(if (:is-main row)
|
||||
(tr "workspace.branches.main-subtitle")
|
||||
[:> row-status* {:row row :pr pr}])]])
|
||||
|
||||
(mf/defc switcher-popover*
|
||||
{::mf/private true}
|
||||
[{:keys [file current prs-by-branch on-close]}]
|
||||
(let [{:keys [data]} (mf/deref branches)
|
||||
|
||||
filter* (mf/use-state "")
|
||||
filter-v (deref filter*)
|
||||
archived* (mf/use-state false)
|
||||
show-archived? (deref archived*)
|
||||
|
||||
matches?
|
||||
(fn [row]
|
||||
(or (str/blank? filter-v)
|
||||
(str/includes? (str/lower (or (:name row) "")) (str/lower filter-v))))
|
||||
|
||||
;; on a branch, main is a switch target too and goes first
|
||||
main-row
|
||||
(when-not (:is-main current)
|
||||
{:name (tr "workspace.branches.main")
|
||||
:is-main true
|
||||
:branch-file-id (:source-file-id current)})
|
||||
|
||||
rows
|
||||
(mf/with-memo [data current]
|
||||
(remove #(= (:id %) (:id current)) data))
|
||||
|
||||
others
|
||||
(cond->> (filter #(= "open" (:status %)) rows)
|
||||
main-row (cons main-row)
|
||||
:always (filter matches?))
|
||||
|
||||
archived
|
||||
(->> rows
|
||||
(remove #(= "open" (:status %)))
|
||||
(filter matches?))
|
||||
|
||||
on-filter-change
|
||||
(mf/use-fn #(reset! filter* (dom/get-target-val %)))
|
||||
|
||||
on-toggle-archived
|
||||
(mf/use-fn #(swap! archived* not))
|
||||
|
||||
on-open
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(let [id (-> event dom/get-current-target (dom/get-data "branch") uuid/parse)]
|
||||
(st/emit! (dwb/open-branch id)))))
|
||||
|
||||
on-create
|
||||
(mf/use-fn
|
||||
(mf/deps file on-close)
|
||||
(fn [_]
|
||||
(on-close)
|
||||
(modal/show! :create-branch {:file-name (:name file)})))
|
||||
|
||||
on-show-all
|
||||
(mf/use-fn
|
||||
(mf/deps on-close)
|
||||
(fn [_]
|
||||
(on-close)
|
||||
(modal/show! :branches-list {})))]
|
||||
|
||||
[:div {:class (stl/css :popover)
|
||||
:role "dialog"
|
||||
:aria-label (tr "workspace.branches.menu")}
|
||||
[:> input* {:variant "dense"
|
||||
:icon i/search
|
||||
:auto-focus true
|
||||
:placeholder (tr "workspace.branches.search.placeholder")
|
||||
:value filter-v
|
||||
:on-change on-filter-change}]
|
||||
|
||||
[:div {:class (stl/css :section)} (tr "workspace.branches.section.current")]
|
||||
[:ul {:class (stl/css :rows)}
|
||||
[:> switcher-row* {:row current
|
||||
:pr (get prs-by-branch (:id current))
|
||||
:current true}]]
|
||||
|
||||
(when (seq others)
|
||||
[:*
|
||||
[:div {:class (stl/css :section)}
|
||||
[:span (tr "workspace.branches.section.others")]
|
||||
[:span (dm/str (count others))]]
|
||||
[:ul {:class (stl/css :rows)}
|
||||
(for [row others]
|
||||
[:> switcher-row* {:key (dm/str (:branch-file-id row))
|
||||
:row row
|
||||
:pr (get prs-by-branch (:id row))
|
||||
:on-open on-open}])]])
|
||||
|
||||
(when (seq archived)
|
||||
[:*
|
||||
[:button {:class (stl/css :action)
|
||||
:aria-expanded show-archived?
|
||||
:on-click on-toggle-archived}
|
||||
[:> i/icon* {:icon-id (if show-archived? i/arrow-down i/arrow-right) :size "s"}]
|
||||
[:span {:class (stl/css :action-label)} (tr "workspace.branches.archived")]
|
||||
[:span {:class (stl/css :action-aside)} (dm/str (count archived))]]
|
||||
(when show-archived?
|
||||
[:ul {:class (stl/css :rows)}
|
||||
(for [row archived]
|
||||
[:> switcher-row* {:key (dm/str (:branch-file-id row))
|
||||
:row row
|
||||
:on-open on-open}])])])
|
||||
|
||||
[:hr {:class (stl/css :divider)}]
|
||||
|
||||
;; branches only grow from main: on a branch there is nothing to create
|
||||
(when (:is-main current)
|
||||
[:button {:class (stl/css :action) :on-click on-create}
|
||||
[:> i/icon* {:icon-id i/git-branch-plus :size "s"}]
|
||||
[:span {:class (stl/css :action-label)} (tr "workspace.branches.switcher.new")]])
|
||||
|
||||
[:button {:class (stl/css :action) :on-click on-show-all}
|
||||
[:> i/icon* {:icon-id i/menu :size "s"}]
|
||||
[:span {:class (stl/css :action-label)} (tr "workspace.branches.switcher.all")]
|
||||
[:> shortcuts* {:id :open-branches}]]]))
|
||||
|
||||
;; --- Branch row under the header
|
||||
|
||||
(mf/defc branch-bar*
|
||||
[{:keys [file]}]
|
||||
(let [file-id (:id file)
|
||||
visible? (mf/deref has-branches)
|
||||
open? (mf/deref switcher-open)
|
||||
ctx (mf/deref refs/branch-context)
|
||||
profile (mf/deref refs/profile)
|
||||
prs (mf/deref refs/pull-requests)
|
||||
|
||||
prs-by-branch
|
||||
(mf/with-memo [prs]
|
||||
(when (dwpr/enabled?)
|
||||
(open-pr-by-branch prs)))
|
||||
|
||||
;; what the open file is: its branch row, or main
|
||||
current
|
||||
(or ctx
|
||||
{:name (tr "workspace.branches.main")
|
||||
:is-main true
|
||||
:created-by (:id profile)
|
||||
:branch-file-id file-id})
|
||||
|
||||
merged? (contains? #{"merged" "archived"} (:status ctx))
|
||||
|
||||
;; clicks inside the row (chip and popover) must not close it
|
||||
bar-ref (mf/use-ref nil)
|
||||
|
||||
on-toggle
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
;; the dropdown closes itself on any outside click: the click
|
||||
;; that opens it must not reach it
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (dwb/toggle-branch-switcher))))
|
||||
|
||||
on-close
|
||||
(mf/use-fn #(st/emit! (dwb/toggle-branch-switcher false)))]
|
||||
|
||||
;; the list decides whether the row shows at all, so it loads with the
|
||||
;; file, and so does the branch context: during a review the bottom
|
||||
;; banner that also loads it is replaced by the review banner
|
||||
(mf/with-effect [file-id]
|
||||
(when (contains? cf/flags :branching)
|
||||
(st/emit! (dwb/init-branches-state)
|
||||
(dwb/fetch-branch-context)
|
||||
(dwb/toggle-branch-switcher false))))
|
||||
|
||||
;; the row makes the header taller; panels that size themselves against
|
||||
;; the viewport read this to keep their bottom edge in place
|
||||
(mf/with-effect [visible?]
|
||||
(let [root (dom/get-root)]
|
||||
(if visible?
|
||||
(dom/set-css-property! root "--branch-bar-block-size" "var(--branch-bar-size)")
|
||||
(dom/unset-css-property! root "--branch-bar-block-size"))
|
||||
(fn [] (dom/unset-css-property! root "--branch-bar-block-size"))))
|
||||
|
||||
(when ^boolean visible?
|
||||
[:div {:class (stl/css :branch-bar) :ref bar-ref}
|
||||
[:> branch-chip* {:label (:name current)
|
||||
:is-open open?
|
||||
:title (if (:is-main current)
|
||||
(tr "workspace.branches.switcher.tooltip")
|
||||
(tr "workspace.branches.header-badge-tooltip" (:name current)))
|
||||
:on-click on-toggle}]
|
||||
|
||||
;; the right side never stays empty: a plain note says where the
|
||||
;; file stands until there are changes to count
|
||||
(let [pr (get prs-by-branch (:id ctx))]
|
||||
(cond
|
||||
(nil? ctx)
|
||||
[:> text* {:as "span"
|
||||
:typography t/body-small
|
||||
:class (stl/css :status-note)}
|
||||
(tr "workspace.branches.main-subtitle")]
|
||||
|
||||
^boolean merged?
|
||||
nil
|
||||
|
||||
(in-sync? ctx pr)
|
||||
[:> text* {:as "span"
|
||||
:typography t/body-small
|
||||
:class (stl/css :status-note)}
|
||||
(tr "workspace.branches.banner.status.up-to-date")]
|
||||
|
||||
:else
|
||||
[:> row-status* {:row ctx :pr pr}]))
|
||||
|
||||
[:& dropdown {:show open? :on-close on-close :container bar-ref}
|
||||
[:> switcher-popover* {:file file
|
||||
:current current
|
||||
:prs-by-branch prs-by-branch
|
||||
:on-close on-close}]]])))
|
||||
121
frontend/src/app/main/ui/workspace/branch_switcher.scss
Normal file
121
frontend/src/app/main/ui/workspace/branch_switcher.scss
Normal file
@ -0,0 +1,121 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
@use "ds/_borders.scss" as *;
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@use "ds/typography.scss" as *;
|
||||
|
||||
:global(:root) {
|
||||
--branch-bar-size: #{$sz-36};
|
||||
}
|
||||
|
||||
// --- Branch row under the header
|
||||
|
||||
.branch-bar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--sp-s);
|
||||
block-size: var(--branch-bar-size);
|
||||
padding: var(--sp-xs) var(--sp-m) var(--sp-s);
|
||||
}
|
||||
|
||||
.status-note {
|
||||
flex-shrink: 0;
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
// --- Popover
|
||||
|
||||
.popover {
|
||||
position: absolute;
|
||||
inset-block-start: 100%;
|
||||
inset-inline-start: var(--sp-s);
|
||||
z-index: var(--z-index-dropdown);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-xxs);
|
||||
inline-size: $sz-284;
|
||||
max-block-size: $sz-480;
|
||||
overflow-y: auto;
|
||||
padding: var(--sp-s);
|
||||
border: $b-1 solid var(--color-background-quaternary);
|
||||
border-radius: $br-8;
|
||||
background-color: var(--color-background-tertiary);
|
||||
box-shadow: var(--el-shadow-dark);
|
||||
}
|
||||
|
||||
.section {
|
||||
@include use-typography("headline-small");
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: var(--sp-s) var(--sp-s) var(--sp-xs);
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.action {
|
||||
@include use-typography("body-small");
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-s);
|
||||
inline-size: 100%;
|
||||
min-block-size: $sz-32;
|
||||
padding: var(--sp-xs) var(--sp-s);
|
||||
border: none;
|
||||
border-radius: $br-8;
|
||||
background: none;
|
||||
color: var(--color-foreground-primary);
|
||||
text-align: start;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
--shortcut-foreground-color: var(--color-foreground-primary);
|
||||
|
||||
background-color: var(--color-background-quaternary);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: $b-1 solid var(--color-accent-primary);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.action-label {
|
||||
flex: 1;
|
||||
min-inline-size: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.action-aside {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
gap: var(--sp-s);
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.divider {
|
||||
inline-size: 100%;
|
||||
margin: var(--sp-xs) 0;
|
||||
border: none;
|
||||
border-block-start: $b-1 solid var(--color-background-quaternary);
|
||||
}
|
||||
@ -8,6 +8,7 @@
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.config :as cf]
|
||||
[app.main.data.common :as dcm]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.workspace :as dw]
|
||||
@ -18,6 +19,7 @@
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
|
||||
[app.main.ui.ds.tooltip :refer [tooltip*]]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.main.ui.workspace.branch-switcher :refer [branch-bar*]]
|
||||
[app.main.ui.workspace.main-menu :as main-menu]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
@ -36,8 +38,8 @@
|
||||
|
||||
;; When the open file is a branch, `branch-ctx` is non-nil (the
|
||||
;; main file is never a branch, so this is also how we tell them
|
||||
;; apart). On a branch we show "<main name> (<branch name>)" and a
|
||||
;; badge; on main we keep the plain file name.
|
||||
;; apart). On a branch the title is main's name: the branch itself
|
||||
;; is named by the chip in the branch row below.
|
||||
branch-ctx (mf/deref refs/branch-context)
|
||||
branch? (and (some? branch-ctx) (some? (:source-name branch-ctx)))
|
||||
|
||||
@ -54,7 +56,7 @@
|
||||
(tr "workspace.pull-requests.header-title" pr-title)
|
||||
|
||||
^boolean branch?
|
||||
(dm/str (:source-name branch-ctx) " (" (:name branch-ctx) ")")
|
||||
(:source-name branch-ctx)
|
||||
|
||||
:else
|
||||
file-name)
|
||||
@ -116,67 +118,69 @@
|
||||
(when ^boolean editing?
|
||||
(dom/select-text! (mf/ref-val input-ref))))
|
||||
|
||||
[:header {:class (dm/str class " " (stl/css :workspace-header-left))}
|
||||
[:a {:on-click go-back
|
||||
:class (stl/css :main-icon)} deprecated-icon/logo-icon]
|
||||
[:div {:alt (tr "workspace.sitemap")
|
||||
:class (stl/css :project-tree)}
|
||||
[:div
|
||||
{:class (stl/css :project-name)
|
||||
:on-click nav-to-project}
|
||||
(:name project)]
|
||||
(if ^boolean editing?
|
||||
[:input
|
||||
{:class (stl/css :file-name-input)
|
||||
:type "text"
|
||||
:ref input-ref
|
||||
:on-blur handle-blur
|
||||
:on-key-down handle-name-keydown
|
||||
:auto-focus true
|
||||
:default-value (:name file "")}]
|
||||
[:div
|
||||
{:class (stl/css :file-name)
|
||||
:title display-name
|
||||
;; on a branch the name is fixed ("File (Branch)"); renaming here
|
||||
;; would be confusing, so double-click editing is disabled.
|
||||
:on-double-click (when-not ^boolean branch? start-editing-name)}
|
||||
;; Persistence state widget
|
||||
[:div {:class (case persistence-status
|
||||
:pending (stl/css :status-notification :pending-status)
|
||||
:saving (stl/css :status-notification :saving-status)
|
||||
:saved (stl/css :status-notification :saved-status)
|
||||
:retrying (stl/css :status-notification :retrying-status)
|
||||
:error (stl/css :status-notification :error-status)
|
||||
(stl/css :status-notification))
|
||||
:title (case persistence-status
|
||||
:pending (tr "workspace.header.saving")
|
||||
:saving (tr "workspace.header.saving")
|
||||
:saved (tr "workspace.header.saved")
|
||||
:retrying (tr "workspace.header.retrying")
|
||||
:error (tr "workspace.header.save-error")
|
||||
nil)}
|
||||
(case persistence-status
|
||||
:pending deprecated-icon/status-alert
|
||||
:saving deprecated-icon/status-alert
|
||||
:saved deprecated-icon/status-tick
|
||||
:retrying deprecated-icon/status-alert
|
||||
:error deprecated-icon/status-wrong
|
||||
nil)]
|
||||
(cond
|
||||
^boolean review?
|
||||
[:> tooltip* {:content (tr "workspace.pull-requests.header-badge-tooltip" pr-title)
|
||||
:placement "bottom"}
|
||||
[:span {:class (stl/css :review-badge)}
|
||||
[:> icon* {:icon-id i/git-pull-request-arrow :size "s"}]]]
|
||||
|
||||
^boolean branch?
|
||||
[:> tooltip* {:content (tr "workspace.branches.header-badge-tooltip" (:name branch-ctx))
|
||||
:placement "bottom"}
|
||||
[:span {:class (stl/css :branch-badge)}
|
||||
[:> icon* {:icon-id i/git-branch :size "s"}]]])
|
||||
[:div {:class (stl/css :file-name-label)} display-name]])]
|
||||
(when ^boolean shared?
|
||||
[:span {:class (stl/css :shared-badge)} deprecated-icon/library])
|
||||
[:div {:class (stl/css :menu-section)}
|
||||
[:> main-menu/menu* {:layout layout
|
||||
:file file}]]]))
|
||||
[:div {:class (dm/str class " " (stl/css :workspace-header-left-wrapper))}
|
||||
[:header {:class (stl/css :workspace-header-left)}
|
||||
[:a {:on-click go-back
|
||||
:class (stl/css :main-icon)} deprecated-icon/logo-icon]
|
||||
[:div {:alt (tr "workspace.sitemap")
|
||||
:class (stl/css :project-tree)}
|
||||
[:div
|
||||
{:class (stl/css :project-name)
|
||||
:on-click nav-to-project}
|
||||
(:name project)]
|
||||
(if ^boolean editing?
|
||||
[:input
|
||||
{:class (stl/css :file-name-input)
|
||||
:type "text"
|
||||
:ref input-ref
|
||||
:on-blur handle-blur
|
||||
:on-key-down handle-name-keydown
|
||||
:auto-focus true
|
||||
:default-value (:name file "")}]
|
||||
[:div
|
||||
{:class (stl/css :file-name)
|
||||
:title display-name
|
||||
;; on a branch the title is main's name, which this file does not
|
||||
;; own, so double-click editing is disabled.
|
||||
:on-double-click (when-not ^boolean branch? start-editing-name)}
|
||||
;; Persistence state widget
|
||||
[:div {:class (case persistence-status
|
||||
:pending (stl/css :status-notification :pending-status)
|
||||
:saving (stl/css :status-notification :saving-status)
|
||||
:saved (stl/css :status-notification :saved-status)
|
||||
:retrying (stl/css :status-notification :retrying-status)
|
||||
:error (stl/css :status-notification :error-status)
|
||||
(stl/css :status-notification))
|
||||
:title (case persistence-status
|
||||
:pending (tr "workspace.header.saving")
|
||||
:saving (tr "workspace.header.saving")
|
||||
:saved (tr "workspace.header.saved")
|
||||
:retrying (tr "workspace.header.retrying")
|
||||
:error (tr "workspace.header.save-error")
|
||||
nil)}
|
||||
(case persistence-status
|
||||
:pending deprecated-icon/status-alert
|
||||
:saving deprecated-icon/status-alert
|
||||
:saved deprecated-icon/status-tick
|
||||
:retrying deprecated-icon/status-alert
|
||||
:error deprecated-icon/status-wrong
|
||||
nil)]
|
||||
(when ^boolean review?
|
||||
[:> tooltip* {:content (tr "workspace.pull-requests.header-badge-tooltip" pr-title)
|
||||
:placement "bottom"}
|
||||
[:span {:class (stl/css :review-badge)}
|
||||
[:> icon* {:icon-id i/git-pull-request-arrow :size "s"}]]])
|
||||
[:div {:class (stl/css :file-name-label)}
|
||||
(if ^boolean review?
|
||||
[:*
|
||||
[:span {:class (stl/css :review-label)}
|
||||
(tr "workspace.pull-requests.header-label")]
|
||||
" " pr-title]
|
||||
display-name)]])]
|
||||
(when ^boolean shared?
|
||||
[:span {:class (stl/css :shared-badge)} deprecated-icon/library])
|
||||
[:div {:class (stl/css :menu-section)}
|
||||
[:> main-menu/menu* {:layout layout
|
||||
:file file}]]]
|
||||
(when (contains? cf/flags :branching)
|
||||
[:> branch-bar* {:file file}])]))
|
||||
|
||||
@ -4,8 +4,17 @@
|
||||
//
|
||||
// Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
@use "ds/_borders.scss" as *;
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@use "ds/_utils.scss" as *;
|
||||
@use "refactor/common-refactor.scss" as deprecated;
|
||||
|
||||
.workspace-header-left-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-inline-size: 0;
|
||||
}
|
||||
|
||||
.workspace-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -82,30 +91,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
.branch-badge {
|
||||
@include deprecated.flex-center;
|
||||
|
||||
// pull request review: info blue, as every review mark (the banner, the
|
||||
// review pill), so an active review stands out from the plain branch badge
|
||||
.review-badge {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
width: deprecated.$s-16;
|
||||
height: deprecated.$s-16;
|
||||
margin-right: deprecated.$s-6;
|
||||
border-radius: deprecated.$br-4;
|
||||
background-color: var(--button-secondary-background-color-rest);
|
||||
color: var(--button-secondary-foreground-color-rest);
|
||||
inline-size: $sz-16;
|
||||
block-size: $sz-16;
|
||||
margin-inline-end: px2rem(6);
|
||||
border-radius: $br-4;
|
||||
background-color: var(--color-background-info);
|
||||
color: var(--color-accent-info);
|
||||
}
|
||||
|
||||
// pull request review: accent-colored so an active review stands out from
|
||||
// the plain branch badge (the file is still the live, editable branch)
|
||||
.review-badge {
|
||||
@include deprecated.flex-center;
|
||||
|
||||
flex-shrink: 0;
|
||||
width: deprecated.$s-16;
|
||||
height: deprecated.$s-16;
|
||||
margin-right: deprecated.$s-6;
|
||||
border-radius: deprecated.$br-4;
|
||||
background-color: var(--button-primary-background-color-rest);
|
||||
color: var(--button-primary-foreground-color-rest);
|
||||
.review-label {
|
||||
color: var(--color-accent-info);
|
||||
}
|
||||
|
||||
.shared-badge {
|
||||
|
||||
@ -20,13 +20,11 @@
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.data.plugins :as dp]
|
||||
[app.main.data.profile :as du]
|
||||
[app.main.data.shortcuts :as scd]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.branches :as dwb]
|
||||
[app.main.data.workspace.comments :as dwcm]
|
||||
[app.main.data.workspace.libraries :as dwl]
|
||||
[app.main.data.workspace.mcp :as mcp]
|
||||
[app.main.data.workspace.shortcuts :as sc]
|
||||
[app.main.data.workspace.undo :as dwu]
|
||||
[app.main.data.workspace.versions :as dwv]
|
||||
[app.main.features :as features]
|
||||
@ -41,6 +39,8 @@
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]]
|
||||
[app.main.ui.hooks.resize :as r]
|
||||
[app.main.ui.workspace.branch-switcher :as bsw]
|
||||
[app.main.ui.workspace.menu-shortcuts :refer [shortcuts*]]
|
||||
[app.plugins.register :as preg]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
@ -49,19 +49,6 @@
|
||||
[potok.v2.core :as ptk]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc shortcuts*
|
||||
{::mf/private true}
|
||||
[{:keys [id]}]
|
||||
(let [custom-shortcuts (mf/deref refs/custom-shortcuts)
|
||||
customized? (let [c (get-in custom-shortcuts [:workspace id])]
|
||||
(and c (not= c "")))]
|
||||
[:span {:class (stl/css :shortcut)}
|
||||
(for [sc (scd/split-sc (sc/get-effective-tooltip id custom-shortcuts))]
|
||||
[:span {:class (stl/css-case :shortcut-key true
|
||||
:customized-key customized?)
|
||||
:key sc}
|
||||
sc])]))
|
||||
|
||||
(mf/defc help-info-menu*
|
||||
{::mf/private true
|
||||
::mf/wrap [mf/memo]}
|
||||
@ -629,18 +616,6 @@
|
||||
(when (kbd/enter? event)
|
||||
(on-show-version-history event))))
|
||||
|
||||
on-show-branches
|
||||
(mf/use-fn
|
||||
(fn [_]
|
||||
(st/emit! (dwb/show-branches-panel))))
|
||||
|
||||
on-show-branches-key-down
|
||||
(mf/use-fn
|
||||
(mf/deps on-show-branches)
|
||||
(fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(on-show-branches event))))
|
||||
|
||||
on-pin-version
|
||||
(mf/use-fn
|
||||
(fn [_]
|
||||
@ -731,15 +706,6 @@
|
||||
(tr "dashboard.show-version-history")]
|
||||
[:> shortcuts* {:id :toggle-history}]]
|
||||
|
||||
(when (contains? cf/flags :branching)
|
||||
[:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item)
|
||||
:on-click on-show-branches
|
||||
:on-key-down on-show-branches-key-down
|
||||
:id "file-menu-show-branches"}
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(tr "workspace.branches.menu")]
|
||||
[:> shortcuts* {:id :open-branches}]])
|
||||
|
||||
[:div {:class (stl/css :separator)}]])
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item)
|
||||
@ -913,6 +879,7 @@
|
||||
selected-sub-menu* (mf/use-state nil)
|
||||
selected-sub-menu (deref selected-sub-menu*)
|
||||
mcp (mf/deref refs/mcp)
|
||||
has-branches? (mf/deref bsw/has-branches)
|
||||
|
||||
toggle-menu
|
||||
(mf/use-fn
|
||||
@ -1001,6 +968,16 @@
|
||||
::ev/origin "workspace:menu"})
|
||||
(modal/show :plugin-management {}))))
|
||||
|
||||
;; the switcher when the file has branches; the create dialog when
|
||||
;; it has none, since the switcher only exists once a branch does
|
||||
show-branches
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(reset! show-menu* false)
|
||||
(reset! selected-sub-menu* nil)
|
||||
(st/emit! (dwb/show-branches))))
|
||||
|
||||
show-shortcuts
|
||||
(mf/use-fn
|
||||
(mf/deps layout)
|
||||
@ -1145,6 +1122,22 @@
|
||||
[:> icon* {:icon-id i/arrow-right
|
||||
:class (stl/css :item-arrow)}]]
|
||||
|
||||
(when (contains? cf/flags :branching)
|
||||
[:*
|
||||
[:div {:class (stl/css :separator)}]
|
||||
[:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item :shortcut-item)
|
||||
:on-click show-branches
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(show-branches event)))
|
||||
:on-pointer-enter close-sub-menu
|
||||
:id "file-menu-branches"}
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(if ^boolean has-branches?
|
||||
(tr "workspace.branches.menu")
|
||||
(tr "workspace.branches.menu.create"))]
|
||||
[:> shortcuts* {:id :open-branches}]]])
|
||||
|
||||
(when (or (and (contains? cf/flags :subscriptions)
|
||||
(not= "enterprise" subscription-type))
|
||||
(contains? cf/flags :admin-console))
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
//
|
||||
// Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
@use "ds/typography.scss" as t;
|
||||
@use "ds/typography.scss" as *;
|
||||
@use "ds/z-index.scss" as *;
|
||||
@use "ds/_borders.scss" as *;
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@ -27,11 +27,14 @@
|
||||
.menu {
|
||||
top: $sz-48;
|
||||
left: calc(var(--left-sidebar-width) - px2rem(44));
|
||||
inline-size: $sz-192;
|
||||
|
||||
// wide enough for a top-level entry with its shortcut keys
|
||||
inline-size: $sz-224;
|
||||
}
|
||||
|
||||
// submenus open right after the menu's edge
|
||||
.sub-menu {
|
||||
left: calc(var(--left-sidebar-width) + px2rem(150));
|
||||
left: calc(var(--left-sidebar-width) + px2rem(182));
|
||||
min-width: $sz-364;
|
||||
|
||||
&.pos-1 {
|
||||
@ -78,7 +81,7 @@
|
||||
}
|
||||
|
||||
.base-menu-item {
|
||||
@include t.use-typography("body-small");
|
||||
@include use-typography("body-small");
|
||||
|
||||
display: grid;
|
||||
align-items: center;
|
||||
@ -94,7 +97,7 @@
|
||||
&:hover {
|
||||
--menu-foreground-color: var(--menu-foreground-color-hover);
|
||||
--menu-background-color: var(--menu-background-color-hover);
|
||||
--menu-shortcut-foreground-color: var(--menu-shortcut-foreground-color-hover);
|
||||
--shortcut-foreground-color: var(--color-foreground-primary);
|
||||
--menu-icon-foreground-color: var(--menu-foreground-color-hover);
|
||||
}
|
||||
|
||||
@ -122,6 +125,18 @@
|
||||
grid-area: name;
|
||||
}
|
||||
|
||||
// a top-level entry with a shortcut instead of a submenu arrow: the keys
|
||||
// keep their size and the name gives way
|
||||
.shortcut-item {
|
||||
gap: var(--sp-s);
|
||||
|
||||
.item-name {
|
||||
@include text-ellipsis;
|
||||
|
||||
min-inline-size: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.plugin-name {
|
||||
@include text-ellipsis;
|
||||
|
||||
@ -169,28 +184,3 @@
|
||||
left: calc(-1 * var(--sp-xs));
|
||||
margin-top: var(--sp-s);
|
||||
}
|
||||
|
||||
.shortcut {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--sp-xxs);
|
||||
color: var(--menu-shortcut-foreground-color);
|
||||
}
|
||||
|
||||
.shortcut-key {
|
||||
@include t.use-typography("body-small");
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: px2rem(20);
|
||||
padding: var(--sp-xxs) px2rem(6);
|
||||
border-radius: $br-6;
|
||||
background-color: var(--menu-shortcut-background-color);
|
||||
}
|
||||
|
||||
.customized-key {
|
||||
background-color: var(--color-background-info);
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
27
frontend/src/app/main/ui/workspace/menu_shortcuts.cljs
Normal file
27
frontend/src/app/main/ui/workspace/menu_shortcuts.cljs
Normal file
@ -0,0 +1,27 @@
|
||||
;; 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 app.main.ui.workspace.menu-shortcuts
|
||||
"The keys of a workspace shortcut, drawn at the end of a menu entry. A
|
||||
shortcut the user customized is tinted."
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.main.data.shortcuts :as scd]
|
||||
[app.main.data.workspace.shortcuts :as sc]
|
||||
[app.main.refs :as refs]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc shortcuts*
|
||||
[{:keys [id]}]
|
||||
(let [custom-shortcuts (mf/deref refs/custom-shortcuts)
|
||||
customized? (let [c (get-in custom-shortcuts [:workspace id])]
|
||||
(and c (not= c "")))]
|
||||
[:span {:class (stl/css :shortcut)}
|
||||
(for [sc (scd/split-sc (sc/get-effective-tooltip id custom-shortcuts))]
|
||||
[:span {:class (stl/css-case :shortcut-key true
|
||||
:customized-key customized?)
|
||||
:key sc}
|
||||
sc])]))
|
||||
37
frontend/src/app/main/ui/workspace/menu_shortcuts.scss
Normal file
37
frontend/src/app/main/ui/workspace/menu_shortcuts.scss
Normal file
@ -0,0 +1,37 @@
|
||||
// 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
|
||||
|
||||
@use "ds/typography.scss" as *;
|
||||
@use "ds/_borders.scss" as *;
|
||||
@use "ds/_utils.scss" as *;
|
||||
|
||||
// The menu entry that holds the keys sets `--shortcut-foreground-color`
|
||||
// on hover so the keys follow its text color.
|
||||
.shortcut {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--sp-xxs);
|
||||
color: var(--shortcut-foreground-color, var(--color-foreground-secondary));
|
||||
}
|
||||
|
||||
.shortcut-key {
|
||||
@include use-typography("body-small");
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
block-size: px2rem(20);
|
||||
padding: var(--sp-xxs) px2rem(6);
|
||||
border-radius: $br-6;
|
||||
background-color: var(--color-background-primary);
|
||||
}
|
||||
|
||||
.customized-key {
|
||||
background-color: var(--color-background-info);
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
@ -20,7 +20,6 @@
|
||||
[app.main.data.style-dictionary :as sd]
|
||||
[app.main.data.tokenscript :as ts]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.branches :as dwb]
|
||||
[app.main.features :as features]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
@ -34,7 +33,6 @@
|
||||
[app.main.ui.workspace.left-header :refer [left-header*]]
|
||||
[app.main.ui.workspace.right-header :refer [right-header*]]
|
||||
[app.main.ui.workspace.sidebar.assets :refer [assets-toolbox*]]
|
||||
[app.main.ui.workspace.sidebar.branches :refer [branches-toolbox*]]
|
||||
[app.main.ui.workspace.sidebar.debug :refer [debug-panel*]]
|
||||
[app.main.ui.workspace.sidebar.history :refer [history-toolbox*]]
|
||||
[app.main.ui.workspace.sidebar.layers :refer [layers-toolbox*]]
|
||||
@ -244,7 +242,7 @@
|
||||
::mf/memo true}
|
||||
[]
|
||||
(let [selected*
|
||||
(hooks/use-shared-state dwb/history-sidebar-tab-key "history")
|
||||
(hooks/use-persisted-state ::history-sidebar "history")
|
||||
|
||||
selected
|
||||
(deref selected*)
|
||||
@ -255,13 +253,10 @@
|
||||
|
||||
tabs
|
||||
(mf/with-memo []
|
||||
(cond-> [{:label (tr "workspace.versions.tab.history")
|
||||
:id "history"}
|
||||
{:label (tr "workspace.versions.tab.actions")
|
||||
:id "actions"}]
|
||||
(contains? cf/flags :branching)
|
||||
(conj {:label (tr "workspace.branches.tab")
|
||||
:id "branches"})))
|
||||
[{:label (tr "workspace.versions.tab.history")
|
||||
:id "history"}
|
||||
{:label (tr "workspace.versions.tab.actions")
|
||||
:id "actions"}])
|
||||
|
||||
button
|
||||
(mf/with-memo []
|
||||
@ -285,11 +280,7 @@
|
||||
|
||||
"actions"
|
||||
[:article {:class (stl/css :versions-tab)}
|
||||
[:> history-toolbox*]]
|
||||
|
||||
"branches"
|
||||
[:article {:class (stl/css :history-tab)}
|
||||
[:> branches-toolbox* {}]])]))
|
||||
[:> history-toolbox*]])]))
|
||||
|
||||
(mf/defc right-sidebar*
|
||||
[{:keys [layout section file-id page-id drawing-tool active-tokens] :as props}]
|
||||
|
||||
@ -103,7 +103,7 @@
|
||||
|
||||
.left-sidebar {
|
||||
display: grid;
|
||||
grid-template: "header header" px2rem(52) "content resize" 1fr / 1fr 0;
|
||||
grid-template: "header header" auto "content resize" 1fr / 1fr 0;
|
||||
position: relative;
|
||||
grid-area: left-sidebar;
|
||||
min-inline-size: var(--left-sidebar-width);
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
grid-auto-rows: max-content;
|
||||
|
||||
// TODO: ugly hack :( Fix this! we shouldn't be hardcoding this height
|
||||
height: calc(100vh - deprecated.$s-92);
|
||||
height: calc(100vh - deprecated.$s-92 - var(--branch-bar-block-size, 0px));
|
||||
scrollbar-gutter: stable;
|
||||
overflow-y: auto;
|
||||
padding-top: deprecated.$s-8;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -21,14 +21,18 @@
|
||||
[app.main.data.workspace.pull-requests :as dwpr]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown-menu :refer [dropdown-menu*
|
||||
dropdown-menu-item*]]
|
||||
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.controls.input :refer [input*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
[app.main.ui.ds.foundations.typography :as t]
|
||||
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
|
||||
[app.main.ui.ds.foundations.typography.text :refer [text*]]
|
||||
[app.main.ui.ds.layout.menu :refer [menu* menu-item*]]
|
||||
[app.main.ui.ds.layout.modal :refer [modal* modal-header* modal-content* modal-footer*]]
|
||||
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
|
||||
[app.main.ui.ds.product.avatar :refer [avatar*]]
|
||||
[app.main.ui.ds.product.branch-pill :refer [branch-pill*]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[cuerdas.core :as str]
|
||||
@ -93,7 +97,7 @@
|
||||
proposed changes can be inspected. Like merging, it diffs the LIVE
|
||||
branch against main."
|
||||
[pr]
|
||||
(modal/show! :branch-compare {:branch (pr->branch pr)}))
|
||||
(modal/show! :branch-compare {:branch (pr->branch pr) :is-review true}))
|
||||
|
||||
(defn confirm-merge!
|
||||
"Open the branching merge dialog for the pull request's branch:
|
||||
@ -116,6 +120,40 @@
|
||||
:accept-style :danger
|
||||
:on-accept (fn [_] (st/emit! (dwpr/close-pull-request (:id pr))))})))
|
||||
|
||||
;; --- Dialog header
|
||||
|
||||
(mf/defc dialog-header*
|
||||
"The header the branch and pull request dialogs share: an icon tile,
|
||||
the title and a subtitle (a plain string, or any element), then
|
||||
whatever the dialog adds after them, such as a status chip. The close
|
||||
button belongs to `modal*`."
|
||||
[{:keys [icon tone title subtitle children]}]
|
||||
[:> modal-header* {:class (stl/css :dialog-header)}
|
||||
(when icon
|
||||
[:div {:class (stl/css-case :dialog-header-icon true
|
||||
:is-warning (= tone :warning))}
|
||||
[:> i/icon* {:icon-id icon}]])
|
||||
[:div {:class (stl/css :dialog-header-text)}
|
||||
[:> heading* {:level 2
|
||||
:typography t/headline-medium
|
||||
:class (stl/css :dialog-title)}
|
||||
title]
|
||||
(if (string? subtitle)
|
||||
[:> text* {:as "span"
|
||||
:typography t/body-small
|
||||
:class (stl/css :dialog-subtitle)}
|
||||
subtitle]
|
||||
subtitle)]
|
||||
children])
|
||||
|
||||
(defn on-dismiss
|
||||
"The `on-open-change` of a dialog that `modal*` closes (Escape, the
|
||||
backdrop or its close button): run `on-close` when it closes."
|
||||
[on-close]
|
||||
(fn [open?]
|
||||
(when-not open?
|
||||
(on-close))))
|
||||
|
||||
;; --- Create pull request dialog (modal)
|
||||
|
||||
(mf/defc create-pull-request-dialog*
|
||||
@ -169,65 +207,58 @@
|
||||
(mf/with-effect []
|
||||
(st/emit! (dtm/fetch-members)))
|
||||
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:div {:class (stl/css :modal-header-icon)}
|
||||
[:> i/icon* {:icon-id i/git-pull-request-arrow}]]
|
||||
[:div {:class (stl/css :modal-header-text)}
|
||||
[:h2 {:class (stl/css :modal-title)} (tr "workspace.pull-requests.create.title")]
|
||||
[:span {:class (stl/css :modal-subtitle)}
|
||||
(tr "workspace.pull-requests.create.subtitle" (or (:name branch) ""))]]
|
||||
[:> button* {:variant "ghost"
|
||||
:icon i/close
|
||||
:aria-label (tr "labels.close")
|
||||
:on-click on-close}]]
|
||||
[:> modal* {:is-open true
|
||||
:on-open-change (on-dismiss on-close)
|
||||
:size "large"}
|
||||
[:> dialog-header* {:icon i/git-pull-request-arrow
|
||||
:title (tr "workspace.pull-requests.create.title")
|
||||
:subtitle (tr "workspace.pull-requests.create.subtitle" (or (:name branch) ""))}]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:> input* {:label (tr "workspace.pull-requests.create.name-label")
|
||||
[:> modal-content* {:class (stl/css :dialog-content)}
|
||||
[:> input* {:label (tr "workspace.pull-requests.create.name-label")
|
||||
:icon i/git-pull-request-arrow
|
||||
:placeholder (tr "workspace.pull-requests.create.name-placeholder")
|
||||
:value title
|
||||
:on-change on-title-change}]
|
||||
|
||||
[:div {:class (stl/css :field)}
|
||||
[:label {:class (stl/css :field-label)}
|
||||
(tr "workspace.pull-requests.create.description-label")]
|
||||
[:textarea {:class (stl/css :textarea)
|
||||
:value description
|
||||
:placeholder (tr "workspace.pull-requests.create.description-placeholder")
|
||||
:on-change on-description-change
|
||||
:rows 3}]]
|
||||
|
||||
[:div {:class (stl/css :field)}
|
||||
[:label {:class (stl/css :field-label)}
|
||||
(tr "workspace.pull-requests.create.reviewers-label")]
|
||||
(if (empty? members)
|
||||
[:span {:class (stl/css :reviewers-empty)}
|
||||
(tr "workspace.pull-requests.create.no-members")]
|
||||
[:ul {:class (stl/css :reviewers-list)}
|
||||
(for [member members]
|
||||
[:li {:key (dm/str (:id member))}
|
||||
[:button {:class (stl/css-case :reviewer-option true
|
||||
:is-selected (contains? selected (:id member)))
|
||||
:data-reviewer-id (dm/str (:id member))
|
||||
:on-click on-toggle-reviewer}
|
||||
[:> avatar* {:profile member :variant "S"}]
|
||||
[:span {:class (stl/css :reviewer-name)} (:fullname member)]
|
||||
(when (contains? selected (:id member))
|
||||
[:> i/icon* {:icon-id i/tick :size "s"}])]])])]
|
||||
|
||||
[:> context-notification* {:level :info :type :context}
|
||||
(tr "workspace.pull-requests.create.info")]]
|
||||
|
||||
[:> modal-footer* {:class (stl/css :dialog-footer)}
|
||||
[:> button* {:variant "secondary" :on-click on-close}
|
||||
(tr "labels.cancel")]
|
||||
[:> button* {:variant "primary"
|
||||
:icon i/git-pull-request-arrow
|
||||
:placeholder (tr "workspace.pull-requests.create.name-placeholder")
|
||||
:value title
|
||||
:on-change on-title-change}]
|
||||
|
||||
[:div {:class (stl/css :field)}
|
||||
[:label {:class (stl/css :field-label)}
|
||||
(tr "workspace.pull-requests.create.description-label")]
|
||||
[:textarea {:class (stl/css :textarea)
|
||||
:value description
|
||||
:placeholder (tr "workspace.pull-requests.create.description-placeholder")
|
||||
:on-change on-description-change
|
||||
:rows 3}]]
|
||||
|
||||
[:div {:class (stl/css :field)}
|
||||
[:label {:class (stl/css :field-label)}
|
||||
(tr "workspace.pull-requests.create.reviewers-label")]
|
||||
(if (empty? members)
|
||||
[:span {:class (stl/css :reviewers-empty)}
|
||||
(tr "workspace.pull-requests.create.no-members")]
|
||||
[:ul {:class (stl/css :reviewers-list)}
|
||||
(for [member members]
|
||||
[:li {:key (dm/str (:id member))}
|
||||
[:button {:class (stl/css-case :reviewer-option true
|
||||
:is-selected (contains? selected (:id member)))
|
||||
:data-reviewer-id (dm/str (:id member))
|
||||
:on-click on-toggle-reviewer}
|
||||
[:> avatar* {:profile member :variant "S"}]
|
||||
[:span {:class (stl/css :reviewer-name)} (:fullname member)]
|
||||
(when (contains? selected (:id member))
|
||||
[:> i/icon* {:icon-id i/tick :size "s"}])]])])]
|
||||
|
||||
[:> context-notification* {:level :info :type :context}
|
||||
(tr "workspace.pull-requests.create.info")]]
|
||||
|
||||
[:div {:class (stl/css :modal-footer)}
|
||||
[:> button* {:variant "secondary" :on-click on-close}
|
||||
(tr "labels.cancel")]
|
||||
[:> button* {:variant "primary"
|
||||
:icon i/git-pull-request-arrow
|
||||
:disabled (not valid?)
|
||||
:on-click on-submit}
|
||||
(tr "workspace.pull-requests.create.submit")]]]]))
|
||||
:disabled (not valid?)
|
||||
:on-click on-submit}
|
||||
(tr "workspace.pull-requests.create.submit")]]]))
|
||||
|
||||
;; --- Review dialog (approve / request changes)
|
||||
|
||||
@ -261,58 +292,52 @@
|
||||
(when-not (str/blank? c) c)))
|
||||
(modal/hide))))]
|
||||
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:div {:class (stl/css :modal-header-icon)}
|
||||
[:> i/icon* {:icon-id i/git-pull-request-arrow}]]
|
||||
[:div {:class (stl/css :modal-header-text)}
|
||||
[:h2 {:class (stl/css :modal-title)} (tr "workspace.pull-requests.review.title")]
|
||||
[:span {:class (stl/css :modal-subtitle)} (:title pr)]]
|
||||
[:> button* {:variant "ghost"
|
||||
:icon i/close
|
||||
:aria-label (tr "labels.close")
|
||||
:on-click on-close}]]
|
||||
[:> modal* {:is-open true
|
||||
:on-open-change (on-dismiss on-close)
|
||||
:size "large"}
|
||||
[:> dialog-header* {:icon i/git-pull-request-arrow
|
||||
:title (tr "workspace.pull-requests.review.title")
|
||||
:subtitle (:title pr)}]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :verdict-options)}
|
||||
[:button {:class (stl/css-case :verdict-option true
|
||||
:is-selected (= verdict "approved"))
|
||||
:data-verdict "approved"
|
||||
:on-click on-select-verdict}
|
||||
[:> i/icon* {:icon-id i/tick}]
|
||||
[:div {:class (stl/css :verdict-option-text)}
|
||||
[:span {:class (stl/css :verdict-option-title)}
|
||||
(tr "workspace.pull-requests.review.approve")]
|
||||
[:span {:class (stl/css :verdict-option-hint)}
|
||||
(tr "workspace.pull-requests.review.approve-hint")]]]
|
||||
[:> modal-content* {:class (stl/css :dialog-content)}
|
||||
[:div {:class (stl/css :verdict-options)}
|
||||
[:button {:class (stl/css-case :verdict-option true
|
||||
:is-selected (= verdict "approved"))
|
||||
:data-verdict "approved"
|
||||
:on-click on-select-verdict}
|
||||
[:> i/icon* {:icon-id i/tick}]
|
||||
[:div {:class (stl/css :verdict-option-text)}
|
||||
[:span {:class (stl/css :verdict-option-title)}
|
||||
(tr "workspace.pull-requests.review.approve")]
|
||||
[:span {:class (stl/css :verdict-option-hint)}
|
||||
(tr "workspace.pull-requests.review.approve-hint")]]]
|
||||
|
||||
[:button {:class (stl/css-case :verdict-option true
|
||||
:is-selected (= verdict "changes-requested"))
|
||||
:data-verdict "changes-requested"
|
||||
:on-click on-select-verdict}
|
||||
[:> i/icon* {:icon-id i/triangle-alert}]
|
||||
[:div {:class (stl/css :verdict-option-text)}
|
||||
[:span {:class (stl/css :verdict-option-title)}
|
||||
(tr "workspace.pull-requests.review.request-changes")]
|
||||
[:span {:class (stl/css :verdict-option-hint)}
|
||||
(tr "workspace.pull-requests.review.request-changes-hint")]]]]
|
||||
[:button {:class (stl/css-case :verdict-option true
|
||||
:is-selected (= verdict "changes-requested"))
|
||||
:data-verdict "changes-requested"
|
||||
:on-click on-select-verdict}
|
||||
[:> i/icon* {:icon-id i/triangle-alert}]
|
||||
[:div {:class (stl/css :verdict-option-text)}
|
||||
[:span {:class (stl/css :verdict-option-title)}
|
||||
(tr "workspace.pull-requests.review.request-changes")]
|
||||
[:span {:class (stl/css :verdict-option-hint)}
|
||||
(tr "workspace.pull-requests.review.request-changes-hint")]]]]
|
||||
|
||||
[:div {:class (stl/css :field)}
|
||||
[:label {:class (stl/css :field-label)}
|
||||
(tr "workspace.pull-requests.review.comment-label")]
|
||||
[:textarea {:class (stl/css :textarea)
|
||||
:value comment
|
||||
:placeholder (tr "workspace.pull-requests.review.comment-placeholder")
|
||||
:on-change on-comment-change
|
||||
:rows 3}]]]
|
||||
[:div {:class (stl/css :field)}
|
||||
[:label {:class (stl/css :field-label)}
|
||||
(tr "workspace.pull-requests.review.comment-label")]
|
||||
[:textarea {:class (stl/css :textarea)
|
||||
:value comment
|
||||
:placeholder (tr "workspace.pull-requests.review.comment-placeholder")
|
||||
:on-change on-comment-change
|
||||
:rows 3}]]]
|
||||
|
||||
[:div {:class (stl/css :modal-footer)}
|
||||
[:> button* {:variant "secondary" :on-click on-close}
|
||||
(tr "labels.cancel")]
|
||||
[:> button* {:variant "primary"
|
||||
:on-click on-submit}
|
||||
(tr "workspace.pull-requests.review.submit")]]]]))
|
||||
[:> modal-footer* {:class (stl/css :dialog-footer)}
|
||||
[:> button* {:variant "secondary" :on-click on-close}
|
||||
(tr "labels.cancel")]
|
||||
[:> button* {:variant "primary"
|
||||
:on-click on-submit}
|
||||
(tr "workspace.pull-requests.review.submit")]]]))
|
||||
|
||||
;; --- Details dialog
|
||||
|
||||
@ -366,16 +391,19 @@
|
||||
(st/emit! (modal/hide)
|
||||
(dwpr/reopen-pull-request (:id pr)))))]
|
||||
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:div {:class (stl/css :modal-header-icon)}
|
||||
[:> i/icon* {:icon-id i/git-pull-request-arrow}]]
|
||||
[:div {:class (stl/css :modal-header-text)}
|
||||
[:h2 {:class (stl/css :modal-title)} (:title pr)]
|
||||
[:div {:class (stl/css :modal-subtitle-row)}
|
||||
[:> modal* {:is-open true
|
||||
:on-open-change (on-dismiss on-close)
|
||||
:size "large"}
|
||||
[:> dialog-header*
|
||||
{:icon i/git-pull-request-arrow
|
||||
:title (:title pr)
|
||||
:subtitle
|
||||
(mf/html
|
||||
[:div {:class (stl/css :dialog-subtitle-row)}
|
||||
[:> pr-state-badge* {:pr pr}]
|
||||
[:span {:class (stl/css :modal-subtitle)}
|
||||
[:> text* {:as "span"
|
||||
:typography t/body-small
|
||||
:class (stl/css :dialog-subtitle)}
|
||||
;; the route reads branch → target BRANCH (reviews always merge
|
||||
;; into main); the target file name would be redundant here,
|
||||
;; inside the file itself
|
||||
@ -385,94 +413,90 @@
|
||||
(tr "workspace.pull-requests.badge.outdated")])
|
||||
(when (and open? (pos? (or (:conflicts pr) 0)))
|
||||
[:span {:class (stl/css :pr-badge :badge-conflicts)}
|
||||
(tr "workspace.pull-requests.badge.conflicts" (dm/str (:conflicts pr)))])]]
|
||||
[:> button* {:variant "ghost"
|
||||
:icon i/close
|
||||
:aria-label (tr "labels.close")
|
||||
:on-click on-close}]]
|
||||
(tr "workspace.pull-requests.badge.conflicts" (dm/str (:conflicts pr)))])])}]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :pr-info-card)}
|
||||
[:div {:class (stl/css :pr-info-grid)}
|
||||
[:div {:class (stl/css :pr-info-item)}
|
||||
[:span {:class (stl/css :field-label)} (tr "workspace.pull-requests.info.author")]
|
||||
[:div {:class (stl/css :pr-info-value)}
|
||||
(when author [:> avatar* {:profile author :variant "S"}])
|
||||
[:span (get author :fullname "—")]]]
|
||||
[:> modal-content* {:class (stl/css :dialog-content)}
|
||||
[:div {:class (stl/css :pr-info-card)}
|
||||
[:div {:class (stl/css :pr-info-grid)}
|
||||
[:div {:class (stl/css :pr-info-item)}
|
||||
[:span {:class (stl/css :field-label)} (tr "workspace.pull-requests.info.author")]
|
||||
[:div {:class (stl/css :pr-info-value)}
|
||||
(when author [:> avatar* {:profile author :variant "S"}])
|
||||
[:span (get author :fullname "—")]]]
|
||||
|
||||
[:div {:class (stl/css :pr-info-item)}
|
||||
[:span {:class (stl/css :field-label)} (tr "workspace.pull-requests.info.created-at")]
|
||||
[:div {:class (stl/css :pr-info-value)}
|
||||
[:span (ct/format-inst (:created-at pr) :localized-date-time)]]]
|
||||
[:div {:class (stl/css :pr-info-item)}
|
||||
[:span {:class (stl/css :field-label)} (tr "workspace.pull-requests.info.created-at")]
|
||||
[:div {:class (stl/css :pr-info-value)}
|
||||
[:span (ct/format-inst (:created-at pr) :localized-date-time)]]]
|
||||
|
||||
[:div {:class (stl/css :pr-info-item)}
|
||||
[:span {:class (stl/css :field-label)} (tr "workspace.pull-requests.info.updated-at")]
|
||||
[:div {:class (stl/css :pr-info-value)}
|
||||
[:span (ct/format-inst (:review-updated-at pr) :localized-date-time)]]]
|
||||
[:div {:class (stl/css :pr-info-item)}
|
||||
[:span {:class (stl/css :field-label)} (tr "workspace.pull-requests.info.updated-at")]
|
||||
[:div {:class (stl/css :pr-info-value)}
|
||||
[:span (ct/format-inst (:review-updated-at pr) :localized-date-time)]]]
|
||||
|
||||
[:div {:class (stl/css :pr-info-item)}
|
||||
[:span {:class (stl/css :field-label)} (tr "workspace.pull-requests.info.approvals")]
|
||||
[:div {:class (stl/css :pr-info-value)}
|
||||
[:span (dm/str approvals "/" total)]]]]
|
||||
[:div {:class (stl/css :pr-info-item)}
|
||||
[:span {:class (stl/css :field-label)} (tr "workspace.pull-requests.info.approvals")]
|
||||
[:div {:class (stl/css :pr-info-value)}
|
||||
[:span (dm/str approvals "/" total)]]]]
|
||||
|
||||
(when (pos? total)
|
||||
[:div {:class (stl/css :field)}
|
||||
[:label {:class (stl/css :field-label)}
|
||||
(tr "workspace.pull-requests.info.reviewers")]
|
||||
[:ul {:class (stl/css :pr-info-reviewers)}
|
||||
(for [review (:reviews pr)]
|
||||
(let [reviewer (get profiles (:profile-id review))]
|
||||
[:li {:key (dm/str (:profile-id review))
|
||||
:class (stl/css :pr-info-reviewer)}
|
||||
(when reviewer [:> avatar* {:profile reviewer :variant "S"}])
|
||||
[:span {:class (stl/css :reviewer-name)}
|
||||
(get reviewer :fullname "—")]
|
||||
[:span {:class (stl/css-case :reviewer-state true
|
||||
:reviewer-approved (and (= "approved" (:state review))
|
||||
(not (:stale review)))
|
||||
:reviewer-changes (and (= "changes-requested" (:state review))
|
||||
(not (:stale review)))
|
||||
:reviewer-stale (:stale review))}
|
||||
(cond
|
||||
(:stale review)
|
||||
(tr "workspace.pull-requests.review-state.stale")
|
||||
|
||||
(= "approved" (:state review))
|
||||
(tr "workspace.pull-requests.review-state.approved")
|
||||
|
||||
(= "changes-requested" (:state review))
|
||||
(tr "workspace.pull-requests.review-state.changes-requested")
|
||||
|
||||
:else
|
||||
(tr "workspace.pull-requests.review-state.pending"))]
|
||||
(when-not (str/blank? (:comment review))
|
||||
[:p {:class (stl/css :reviewer-comment)} (:comment review)])]))]])]
|
||||
|
||||
(when-not (str/blank? (:description pr))
|
||||
[:div {:class (stl/css :pr-info-description-block)}
|
||||
(when (pos? total)
|
||||
[:div {:class (stl/css :field)}
|
||||
[:label {:class (stl/css :field-label)}
|
||||
(tr "workspace.pull-requests.info.description")]
|
||||
[:p {:class (stl/css :pr-info-description)} (:description pr)]])]
|
||||
(tr "workspace.pull-requests.info.reviewers")]
|
||||
[:ul {:class (stl/css :pr-info-reviewers)}
|
||||
(for [review (:reviews pr)]
|
||||
(let [reviewer (get profiles (:profile-id review))]
|
||||
[:li {:key (dm/str (:profile-id review))
|
||||
:class (stl/css :pr-info-reviewer)}
|
||||
(when reviewer [:> avatar* {:profile reviewer :variant "S"}])
|
||||
[:span {:class (stl/css :reviewer-name)}
|
||||
(get reviewer :fullname "—")]
|
||||
[:span {:class (stl/css-case :reviewer-state true
|
||||
:reviewer-approved (and (= "approved" (:state review))
|
||||
(not (:stale review)))
|
||||
:reviewer-changes (and (= "changes-requested" (:state review))
|
||||
(not (:stale review)))
|
||||
:reviewer-stale (:stale review))}
|
||||
(cond
|
||||
(:stale review)
|
||||
(tr "workspace.pull-requests.review-state.stale")
|
||||
|
||||
[:div {:class (stl/css :modal-footer)}
|
||||
(when (and (or author? admin?) open?)
|
||||
[:> button* {:variant "secondary"
|
||||
:on-click on-close-pr}
|
||||
(tr "workspace.pull-requests.actions.close")])
|
||||
(when (and (or author? admin?) closed?)
|
||||
[:> button* {:variant "secondary"
|
||||
:on-click on-reopen-pr}
|
||||
(tr "workspace.pull-requests.actions.reopen")])
|
||||
(when (and open? (not reviewing?))
|
||||
[:> button* {:variant "primary"
|
||||
:icon i/arrow-up-right
|
||||
:on-click on-open-review}
|
||||
(tr "workspace.pull-requests.actions.open-review")])
|
||||
(when (and open? can-merge?)
|
||||
[:> button* {:variant "primary"
|
||||
:icon i/git-merge
|
||||
:on-click on-merge}
|
||||
(tr "workspace.pull-requests.actions.merge")])]]]))
|
||||
(= "approved" (:state review))
|
||||
(tr "workspace.pull-requests.review-state.approved")
|
||||
|
||||
(= "changes-requested" (:state review))
|
||||
(tr "workspace.pull-requests.review-state.changes-requested")
|
||||
|
||||
:else
|
||||
(tr "workspace.pull-requests.review-state.pending"))]
|
||||
(when-not (str/blank? (:comment review))
|
||||
[:p {:class (stl/css :reviewer-comment)} (:comment review)])]))]])]
|
||||
|
||||
(when-not (str/blank? (:description pr))
|
||||
[:div {:class (stl/css :pr-info-description-block)}
|
||||
[:label {:class (stl/css :field-label)}
|
||||
(tr "workspace.pull-requests.info.description")]
|
||||
[:p {:class (stl/css :pr-info-description)} (:description pr)]])]
|
||||
|
||||
[:> modal-footer* {:class (stl/css :dialog-footer)}
|
||||
(when (and (or author? admin?) open?)
|
||||
[:> button* {:variant "secondary"
|
||||
:on-click on-close-pr}
|
||||
(tr "workspace.pull-requests.actions.close")])
|
||||
(when (and (or author? admin?) closed?)
|
||||
[:> button* {:variant "secondary"
|
||||
:on-click on-reopen-pr}
|
||||
(tr "workspace.pull-requests.actions.reopen")])
|
||||
(when (and open? (not reviewing?))
|
||||
[:> button* {:variant "primary"
|
||||
:icon i/arrow-up-right
|
||||
:on-click on-open-review}
|
||||
(tr "workspace.pull-requests.actions.open-review")])
|
||||
(when (and open? can-merge?)
|
||||
[:> button* {:variant "primary"
|
||||
:icon i/git-merge
|
||||
:on-click on-merge}
|
||||
(tr "workspace.pull-requests.actions.merge")])]]))
|
||||
|
||||
;; --- Review sandbox banner (replaces the branch banner inside a sandbox)
|
||||
|
||||
@ -488,6 +512,9 @@
|
||||
;; branch file, so the team role tells whether this user could
|
||||
;; integrate the branch
|
||||
can-merge? (boolean (get-in team [:permissions :can-edit]))
|
||||
;; same rule as the details dialog's "Cancel pull request"
|
||||
can-cancel? (and (or author? (boolean (get-in team [:permissions :is-admin])))
|
||||
(= "open" (:status pr)))
|
||||
[approvals total] (current-approvals pr)
|
||||
|
||||
collapsed* (mf/use-state false)
|
||||
@ -495,19 +522,18 @@
|
||||
on-toggle (mf/use-fn #(swap! collapsed* not))
|
||||
|
||||
;; secondary actions live behind the kebab menu so the banner
|
||||
;; only exposes the essential ones (details / compare / review)
|
||||
menu-open* (mf/use-state false)
|
||||
menu-open? (deref menu-open*)
|
||||
on-open-menu (mf/use-fn
|
||||
(fn [event]
|
||||
;; the dropdown closes itself on any outside
|
||||
;; click: the opening one must not reach it
|
||||
(dom/stop-propagation event)
|
||||
(reset! menu-open* true)))
|
||||
on-close-menu (mf/use-fn #(reset! menu-open* false))
|
||||
;; only exposes the essential ones (compare / review)
|
||||
menu-open* (mf/use-state false)
|
||||
menu-open? (deref menu-open*)
|
||||
on-toggle-menu (mf/use-fn #(swap! menu-open* not))
|
||||
on-menu-change (mf/use-fn #(reset! menu-open* %))
|
||||
|
||||
on-info
|
||||
(mf/use-fn (mf/deps pr) #(modal/show! :pull-request-info {:pr pr}))
|
||||
(mf/use-fn
|
||||
(mf/deps pr)
|
||||
(fn [_]
|
||||
(reset! menu-open* false)
|
||||
(modal/show! :pull-request-info {:pr pr})))
|
||||
|
||||
on-compare
|
||||
(mf/use-fn (mf/deps pr) #(open-compare! pr))
|
||||
@ -528,12 +554,12 @@
|
||||
(reset! menu-open* false)
|
||||
(confirm-merge! pr)))
|
||||
|
||||
on-play
|
||||
on-cancel
|
||||
(mf/use-fn
|
||||
(mf/deps pr)
|
||||
(fn [_]
|
||||
(reset! menu-open* false)
|
||||
(st/emit! (dwpr/open-pull-request-viewer pr))))
|
||||
(confirm-close! pr)))
|
||||
|
||||
on-publish
|
||||
(mf/use-fn
|
||||
@ -563,7 +589,9 @@
|
||||
(tr "workspace.pull-requests.banner.reviewing")]
|
||||
[:span {:class (stl/css :pr-banner-name)} (:title pr)]
|
||||
[:span {:class (stl/css :pr-banner-route)}
|
||||
(dm/str (:branch-name pr) " → " (tr "workspace.branches.main"))]]
|
||||
[:> branch-pill* {:label (:branch-name pr) :is-review true}]
|
||||
[:> i/icon* {:icon-id i/arrow-long-right :size "s"}]
|
||||
[:> branch-pill* {:label (tr "workspace.branches.main") :is-main true}]]]
|
||||
|
||||
[:div {:class (stl/css :pr-banner-badges)}
|
||||
[:> pr-state-badge* {:pr pr}]
|
||||
@ -579,10 +607,6 @@
|
||||
(dm/str approvals "/" total)])]
|
||||
|
||||
[:div {:class (stl/css :pr-banner-actions)}
|
||||
[:> button* {:variant "secondary"
|
||||
:on-click on-info}
|
||||
(tr "workspace.pull-requests.menu.info")]
|
||||
|
||||
[:> button* {:variant "secondary"
|
||||
:icon i/switch
|
||||
:on-click on-compare}
|
||||
@ -594,21 +618,29 @@
|
||||
:on-click on-review}
|
||||
(tr "workspace.pull-requests.banner.review")])
|
||||
|
||||
;; the banner sits at the bottom of the canvas: its menu opens
|
||||
;; upwards
|
||||
[:div {:class (stl/css :pr-banner-menu)}
|
||||
[:> icon-button* {:variant "ghost"
|
||||
:icon i/menu
|
||||
:aria-label (tr "labels.options")
|
||||
:on-click on-open-menu}]
|
||||
[:> dropdown-menu* {:show menu-open?
|
||||
:on-close on-close-menu
|
||||
:class (stl/css :pr-banner-options-dropdown)}
|
||||
[:> dropdown-menu-item* {:class (stl/css :menu-option) :on-click on-play}
|
||||
(tr "workspace.pull-requests.banner.play")]
|
||||
[:> menu* {:is-open menu-open?
|
||||
:on-open-change on-menu-change
|
||||
:placement "top end"
|
||||
:trigger
|
||||
(mf/html
|
||||
[:> icon-button* {:variant "ghost"
|
||||
:icon i/menu
|
||||
:aria-label (tr "labels.options")
|
||||
:aria-pressed menu-open?
|
||||
:on-click on-toggle-menu}])}
|
||||
[:> menu-item* {:id "info" :on-action on-info}
|
||||
(tr "workspace.pull-requests.menu.info")]
|
||||
(when (and author? (:outdated pr))
|
||||
[:> dropdown-menu-item* {:class (stl/css :menu-option) :on-click on-publish}
|
||||
[:> menu-item* {:id "publish" :on-action on-publish}
|
||||
(tr "workspace.pull-requests.banner.publish")])
|
||||
(when can-merge?
|
||||
[:> dropdown-menu-item* {:class (stl/css :menu-option) :on-click on-merge}
|
||||
[:> menu-item* {:id "merge" :on-action on-merge}
|
||||
(tr "workspace.pull-requests.actions.merge")])
|
||||
[:> dropdown-menu-item* {:class (stl/css :menu-option) :on-click on-exit}
|
||||
(tr "workspace.pull-requests.banner.exit")]]]]]])))
|
||||
[:> menu-item* {:id "exit" :on-action on-exit}
|
||||
(tr "workspace.pull-requests.banner.exit")]
|
||||
(when can-cancel?
|
||||
[:> menu-item* {:id "cancel" :on-action on-cancel}
|
||||
(tr "workspace.pull-requests.banner.cancel")])]]]]])))
|
||||
|
||||
@ -4,229 +4,42 @@
|
||||
//
|
||||
// Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
@use "ds/_borders.scss" as *;
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@use "ds/typography.scss" as t;
|
||||
@use "refactor/common-refactor.scss" as deprecated;
|
||||
|
||||
// Same light-theme semantic-tint re-pointing the branches surfaces use
|
||||
// (see branches.scss for the rationale).
|
||||
.pr-section,
|
||||
.pr-banner,
|
||||
.modal-container {
|
||||
:global(.light) & {
|
||||
--color-foreground-success: var(--color-accent-success);
|
||||
--color-foreground-error: var(--color-accent-error);
|
||||
--color-foreground-warning: color-mix(in srgb, var(--color-accent-warning) 60%, #000);
|
||||
--color-foreground-info: var(--color-accent-info);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Sidebar section
|
||||
|
||||
.pr-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-xs);
|
||||
}
|
||||
|
||||
.pr-section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-xs);
|
||||
padding-block: var(--sp-s) var(--sp-xs);
|
||||
padding-inline: var(--sp-s);
|
||||
|
||||
@include t.use-typography("body-small");
|
||||
|
||||
color: var(--color-foreground-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
font-weight: 600;
|
||||
|
||||
> button {
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.pr-section-count {
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.pr-section-empty {
|
||||
@include t.use-typography("body-small");
|
||||
|
||||
padding-inline: var(--sp-s);
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.pr-entries {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-xs);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.pr-entry {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
align-items: start;
|
||||
gap: var(--sp-s);
|
||||
padding: var(--sp-s);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-background-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
.pr-entry.is-finished {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.pr-entry.is-menu-open {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.pr-entry-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
inline-size: 32px;
|
||||
block-size: 32px;
|
||||
border-radius: 8px;
|
||||
background-color: var(--color-background-tertiary);
|
||||
color: var(--color-accent-primary);
|
||||
}
|
||||
|
||||
.pr-entry-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-xxs);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pr-entry-title {
|
||||
@include t.use-typography("body-medium");
|
||||
|
||||
color: var(--color-foreground-primary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.pr-entry-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-xs);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pr-entry-route {
|
||||
@include t.use-typography("body-small");
|
||||
|
||||
color: var(--color-foreground-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.pr-entry-badges {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--sp-xs);
|
||||
}
|
||||
|
||||
.pr-entry-aside {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-xs);
|
||||
}
|
||||
|
||||
.pr-entry-approvals {
|
||||
@include t.use-typography("code-font");
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-xxs);
|
||||
font-weight: 600;
|
||||
color: var(--color-foreground-success);
|
||||
}
|
||||
|
||||
.pr-options-dropdown {
|
||||
@extend %menu-dropdown;
|
||||
|
||||
inset-block-start: 100%;
|
||||
inset-inline-end: 0;
|
||||
inline-size: $sz-160;
|
||||
}
|
||||
|
||||
.menu-option {
|
||||
@extend %menu-item-base;
|
||||
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pr-finished {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-xs);
|
||||
}
|
||||
|
||||
.pr-finished-toggle {
|
||||
@include t.use-typography("body-small");
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-xs);
|
||||
padding-block: var(--sp-s) var(--sp-xs);
|
||||
padding-inline: var(--sp-s);
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
color: var(--color-foreground-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
font-weight: 600;
|
||||
}
|
||||
@use "ds/typography.scss" as *;
|
||||
|
||||
// --- State badges
|
||||
|
||||
.pr-badge {
|
||||
@include t.use-typography("body-small");
|
||||
@include use-typography("body-small");
|
||||
|
||||
padding: var(--sp-xxs) var(--sp-s);
|
||||
border-radius: 12px;
|
||||
border-radius: $br-12;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.badge-open {
|
||||
color: var(--color-foreground-info);
|
||||
color: var(--color-accent-info);
|
||||
background-color: var(--color-background-info);
|
||||
}
|
||||
|
||||
.badge-in-review {
|
||||
color: var(--color-foreground-info);
|
||||
color: var(--color-accent-info);
|
||||
background-color: var(--color-background-info);
|
||||
}
|
||||
|
||||
.badge-approved {
|
||||
color: var(--color-foreground-success);
|
||||
color: var(--color-accent-success);
|
||||
background-color: var(--color-background-success);
|
||||
}
|
||||
|
||||
.badge-changes {
|
||||
color: var(--color-foreground-warning);
|
||||
color: var(--color-accent-warning);
|
||||
background-color: var(--color-background-warning);
|
||||
}
|
||||
|
||||
.badge-merged {
|
||||
color: var(--color-foreground-success);
|
||||
color: var(--color-accent-success);
|
||||
background-color: var(--color-background-success);
|
||||
}
|
||||
|
||||
@ -236,7 +49,7 @@
|
||||
}
|
||||
|
||||
.badge-outdated {
|
||||
color: var(--color-foreground-warning);
|
||||
color: var(--color-accent-warning);
|
||||
background-color: var(--color-background-warning);
|
||||
}
|
||||
|
||||
@ -245,91 +58,68 @@
|
||||
background-color: var(--color-background-error);
|
||||
}
|
||||
|
||||
// --- Modals (create / review / details)
|
||||
// --- Dialogs (create / review / details)
|
||||
//
|
||||
// `dialog-header*` lives here and the branch dialogs use it too.
|
||||
|
||||
.modal-overlay {
|
||||
@extend %modal-overlay-base;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
@extend %modal-container-base;
|
||||
|
||||
min-width: deprecated.$s-408;
|
||||
|
||||
// long pull request descriptions: let the card grow past the base
|
||||
// 512px cap up to the viewport, and scroll the CONTENT (header and
|
||||
// footer stay pinned) instead of spilling out of the card
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-block-size: min(90vh, 56rem);
|
||||
max-inline-size: min(90vw, 40rem);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
// room for the close button `modal*` places over the top-right corner
|
||||
.dialog-header {
|
||||
gap: var(--sp-s);
|
||||
margin-bottom: deprecated.$s-24;
|
||||
padding-inline-end: var(--sp-s);
|
||||
}
|
||||
|
||||
.modal-header-icon {
|
||||
.dialog-header-icon {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: $sz-32;
|
||||
height: $sz-32;
|
||||
border-radius: 8px;
|
||||
inline-size: $sz-32;
|
||||
block-size: $sz-32;
|
||||
border-radius: $br-8;
|
||||
color: var(--color-accent-primary);
|
||||
background-color: var(--color-background-tertiary);
|
||||
|
||||
&.is-warning {
|
||||
color: var(--color-accent-warning);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header-text {
|
||||
.dialog-header-text {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-xxs);
|
||||
flex: 1;
|
||||
min-inline-size: 0;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
@include deprecated.headline-medium-typography;
|
||||
|
||||
color: var(--modal-title-foreground-color);
|
||||
.dialog-title {
|
||||
color: var(--color-foreground-primary);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.modal-subtitle {
|
||||
@include t.use-typography("body-small");
|
||||
|
||||
.dialog-subtitle {
|
||||
color: var(--color-foreground-secondary);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.modal-subtitle-row {
|
||||
.dialog-subtitle-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--sp-xs);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
@include deprecated.flex-column;
|
||||
|
||||
gap: deprecated.$s-16;
|
||||
margin-bottom: deprecated.$s-24;
|
||||
flex: 1 1 auto;
|
||||
min-block-size: 0;
|
||||
overflow-y: auto;
|
||||
.dialog-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-l);
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
// long labels: the row of actions wraps, never the label inside a button
|
||||
.dialog-footer {
|
||||
flex-wrap: wrap;
|
||||
gap: var(--sp-s);
|
||||
|
||||
// action labels never wrap inside their button; the row wraps instead
|
||||
// button: the DS buttons in the footer take no class from this module
|
||||
button {
|
||||
white-space: nowrap;
|
||||
}
|
||||
@ -342,28 +132,28 @@
|
||||
}
|
||||
|
||||
.field-label {
|
||||
@include t.use-typography("body-small");
|
||||
@include use-typography("body-small");
|
||||
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.textarea {
|
||||
width: 100%;
|
||||
min-height: $sz-64;
|
||||
inline-size: 100%;
|
||||
min-block-size: $sz-64;
|
||||
padding: var(--sp-s);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--color-background-quaternary);
|
||||
border-radius: $br-8;
|
||||
border: $b-1 solid var(--color-background-quaternary);
|
||||
background-color: var(--color-background-tertiary);
|
||||
color: var(--color-foreground-primary);
|
||||
resize: vertical;
|
||||
|
||||
@include t.use-typography("body-medium");
|
||||
@include use-typography("body-medium");
|
||||
}
|
||||
|
||||
// --- Reviewer picker (create dialog)
|
||||
|
||||
.reviewers-empty {
|
||||
@include t.use-typography("body-small");
|
||||
@include use-typography("body-small");
|
||||
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
@ -385,8 +175,8 @@
|
||||
gap: var(--sp-s);
|
||||
inline-size: 100%;
|
||||
padding: var(--sp-xs) var(--sp-s);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
border: $b-1 solid transparent;
|
||||
border-radius: $br-8;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
color: var(--color-foreground-primary);
|
||||
@ -407,7 +197,7 @@
|
||||
}
|
||||
|
||||
.reviewer-name {
|
||||
@include t.use-typography("body-medium");
|
||||
@include use-typography("body-medium");
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@ -427,8 +217,8 @@
|
||||
align-items: center;
|
||||
gap: var(--sp-m);
|
||||
padding: var(--sp-m);
|
||||
border: 1px solid var(--color-background-quaternary);
|
||||
border-radius: 8px;
|
||||
border: $b-1 solid var(--color-background-quaternary);
|
||||
border-radius: $br-8;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
text-align: start;
|
||||
@ -451,13 +241,11 @@
|
||||
}
|
||||
|
||||
.verdict-option-title {
|
||||
@include t.use-typography("body-medium");
|
||||
|
||||
font-weight: 600;
|
||||
@include use-typography("body-medium");
|
||||
}
|
||||
|
||||
.verdict-option-hint {
|
||||
@include t.use-typography("body-small");
|
||||
@include use-typography("body-small");
|
||||
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
@ -467,15 +255,15 @@
|
||||
// the pull request metadata (author / dates / approvals / reviewers)
|
||||
// gets its own card, visually separated from the free-text description
|
||||
// (same surface treatment as the reviewer rows and the create dialog's
|
||||
// "derived from" box: tertiary background + 8px radius)
|
||||
// "derived from" box: tertiary background + $br-8 radius)
|
||||
.pr-info-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-m);
|
||||
flex-shrink: 0;
|
||||
padding: var(--sp-m);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--color-background-quaternary);
|
||||
border-radius: $br-8;
|
||||
border: $b-1 solid var(--color-background-quaternary);
|
||||
background-color: var(--color-background-tertiary);
|
||||
}
|
||||
|
||||
@ -491,7 +279,7 @@
|
||||
}
|
||||
|
||||
.pr-info-description {
|
||||
@include t.use-typography("body-medium");
|
||||
@include use-typography("body-medium");
|
||||
|
||||
margin: 0;
|
||||
color: var(--color-foreground-primary);
|
||||
@ -520,7 +308,7 @@
|
||||
align-items: center;
|
||||
gap: var(--sp-xs);
|
||||
|
||||
@include t.use-typography("body-medium");
|
||||
@include use-typography("body-medium");
|
||||
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
@ -542,7 +330,7 @@
|
||||
flex-wrap: wrap;
|
||||
gap: var(--sp-s);
|
||||
padding: var(--sp-xs) var(--sp-s);
|
||||
border-radius: 8px;
|
||||
border-radius: $br-8;
|
||||
|
||||
// sits INSIDE the tertiary metadata card: step one surface down so
|
||||
// the rows remain distinguishable
|
||||
@ -550,18 +338,18 @@
|
||||
}
|
||||
|
||||
.reviewer-state {
|
||||
@include t.use-typography("body-small");
|
||||
@include use-typography("body-small");
|
||||
|
||||
margin-inline-start: auto;
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.reviewer-approved {
|
||||
color: var(--color-foreground-success);
|
||||
color: var(--color-accent-success);
|
||||
}
|
||||
|
||||
.reviewer-changes {
|
||||
color: var(--color-foreground-warning);
|
||||
color: var(--color-accent-warning);
|
||||
}
|
||||
|
||||
.reviewer-stale {
|
||||
@ -570,7 +358,7 @@
|
||||
}
|
||||
|
||||
.reviewer-comment {
|
||||
@include t.use-typography("body-small");
|
||||
@include use-typography("body-small");
|
||||
|
||||
flex-basis: 100%;
|
||||
margin: 0;
|
||||
@ -586,21 +374,18 @@
|
||||
inset-inline-start: 50%;
|
||||
transform: translateX(-50%);
|
||||
max-inline-size: 90vw;
|
||||
border-radius: deprecated.$s-8;
|
||||
border-radius: $br-8;
|
||||
|
||||
// the review banner must stand out from the branch banner it replaces
|
||||
// (the canvas is still the live branch): accent-colored border instead
|
||||
// of the branch banner's neutral one
|
||||
border: deprecated.$s-2 solid var(--color-accent-primary);
|
||||
// (the canvas is still the live branch): info blue border, as every
|
||||
// review mark, instead of the branch banner's neutral one
|
||||
border: $b-2 solid var(--color-accent-info);
|
||||
background-color: var(--color-background-primary);
|
||||
|
||||
// unlike the branch banner this one hosts a dropdown that opens
|
||||
// upwards, outside the banner box: it must not be clipped (the
|
||||
// collapsed state hides its content via block-size + visibility) and
|
||||
// it must beat the viewport controls, which are raised to z-index 1
|
||||
// while a drawing tool is active
|
||||
overflow: visible;
|
||||
z-index: deprecated.$z-index-2;
|
||||
// TODO: no DS z-index token for 2.
|
||||
z-index: 2;
|
||||
transition:
|
||||
inset-block-end 0.2s,
|
||||
block-size 0.2s;
|
||||
@ -608,7 +393,7 @@
|
||||
&.is-collapsed {
|
||||
inset-block-end: 0;
|
||||
block-size: var(--sp-m);
|
||||
border-radius: 12px 12px 0 0;
|
||||
border-radius: $br-12 $br-12 0 0;
|
||||
|
||||
.pr-banner-content {
|
||||
block-size: 0;
|
||||
@ -619,13 +404,16 @@
|
||||
}
|
||||
|
||||
.pr-banner-handle {
|
||||
@include deprecated.flex-center;
|
||||
@include deprecated.button-style;
|
||||
|
||||
position: absolute;
|
||||
inset-inline: 0;
|
||||
inset-block-start: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
block-size: var(--sp-m);
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
inline-size: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
@ -633,10 +421,10 @@
|
||||
.pr-banner-handle-btn {
|
||||
block-size: var(--sp-xs);
|
||||
inline-size: 100%;
|
||||
max-inline-size: 64px;
|
||||
max-inline-size: $sz-64;
|
||||
padding: 0;
|
||||
border-radius: var(--sp-xs);
|
||||
background-color: var(--palette-handler-background-color);
|
||||
border-radius: $br-4;
|
||||
background-color: var(--color-background-quaternary);
|
||||
}
|
||||
|
||||
.pr-banner-content {
|
||||
@ -644,23 +432,26 @@
|
||||
align-items: center;
|
||||
gap: var(--sp-m);
|
||||
padding: var(--sp-m) var(--sp-m) var(--sp-s);
|
||||
|
||||
// no overflow clip: the kebab dropdown opens upwards past the banner
|
||||
// edge (the collapsed state hides this box via block-size + visibility)
|
||||
}
|
||||
|
||||
.pr-banner-icon {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
color: var(--color-accent-primary);
|
||||
justify-content: center;
|
||||
inline-size: $sz-24;
|
||||
block-size: $sz-24;
|
||||
border-radius: $br-4;
|
||||
background-color: var(--color-background-info);
|
||||
color: var(--color-accent-info);
|
||||
}
|
||||
|
||||
.pr-banner-text {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
align-items: center;
|
||||
gap: var(--sp-xs);
|
||||
|
||||
@include t.use-typography("body-small");
|
||||
@include use-typography("body-small");
|
||||
}
|
||||
|
||||
.pr-banner-title,
|
||||
@ -668,9 +459,16 @@
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
// branch -> main, as pills, the same way the compare dialog names them
|
||||
.pr-banner-route {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-xs);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pr-banner-name {
|
||||
color: var(--color-foreground-primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.pr-banner-badges {
|
||||
@ -680,13 +478,12 @@
|
||||
}
|
||||
|
||||
.pr-banner-approvals {
|
||||
@include t.use-typography("code-font");
|
||||
@include use-typography("code-font");
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-xxs);
|
||||
font-weight: 600;
|
||||
color: var(--color-foreground-success);
|
||||
color: var(--color-accent-success);
|
||||
}
|
||||
|
||||
.pr-banner-actions {
|
||||
@ -695,18 +492,8 @@
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
|
||||
// not positioned: the options menu anchors to the banner box itself
|
||||
.pr-banner-menu {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
// the banner sits at the bottom of the canvas, so its options menu
|
||||
// opens upwards
|
||||
.pr-banner-options-dropdown {
|
||||
@extend %menu-dropdown;
|
||||
|
||||
inset-block-end: 100%;
|
||||
inset-inline-end: 0;
|
||||
inline-size: $sz-192;
|
||||
}
|
||||
|
||||
@ -7,10 +7,14 @@
|
||||
(ns frontend-tests.data.workspace-branches-test
|
||||
(:require
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.main.data.common :as dcm]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.workspace.branches :as dwb]
|
||||
[app.main.repo :as rp]
|
||||
[beicon.v2.core :as rx]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[frontend-tests.helpers.async :as async]
|
||||
[frontend-tests.helpers.mock :as mock]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
@ -216,3 +220,91 @@
|
||||
(rx/dispose! store))))
|
||||
(done'))
|
||||
done))))
|
||||
|
||||
;;; --- Branch switcher: the entry point of the menu and the shortcut ---
|
||||
|
||||
(defn- make-store-with
|
||||
[state]
|
||||
(ptk/store {:state state
|
||||
:on-error (fn [cause] (t/is false (str cause)))}))
|
||||
|
||||
(def ^:private file-id (uuid/next))
|
||||
|
||||
(def ^:private main-without-branches
|
||||
{:current-file-id file-id
|
||||
:files {file-id {:id file-id :name "Checkout"}}
|
||||
:workspace-branches {:status :loaded :data []}
|
||||
:workspace-branch-context {:file-id file-id :loaded? true :info nil}})
|
||||
|
||||
(def ^:private main-with-branches
|
||||
(assoc main-without-branches :workspace-branches
|
||||
{:status :loaded :data [{:id (uuid/next) :name "feature-login" :status "open"}]}))
|
||||
|
||||
(def ^:private on-a-branch
|
||||
(assoc main-without-branches :workspace-branch-context
|
||||
{:file-id file-id :loaded? true :info {:id (uuid/next) :name "feature-login"}}))
|
||||
|
||||
(t/deftest the-switcher-only-exists-once-the-file-has-branches
|
||||
(t/is (false? (dwb/has-branches? main-without-branches)))
|
||||
(t/is (true? (dwb/has-branches? main-with-branches)))
|
||||
(t/is (true? (dwb/has-branches? on-a-branch))
|
||||
"a branch counts even before the list loads"))
|
||||
|
||||
(t/deftest the-switcher-toggles-and-can-be-forced
|
||||
(let [store (make-store-with main-with-branches)]
|
||||
(ptk/emit! store (dwb/toggle-branch-switcher))
|
||||
(t/is (true? (:workspace-branch-switcher @store)))
|
||||
(ptk/emit! store (dwb/toggle-branch-switcher))
|
||||
(t/is (false? (:workspace-branch-switcher @store)))
|
||||
(ptk/emit! store (dwb/toggle-branch-switcher true))
|
||||
(ptk/emit! store (dwb/toggle-branch-switcher true))
|
||||
(t/is (true? (:workspace-branch-switcher @store)) "forcing it open twice keeps it open")
|
||||
(rx/dispose! store)))
|
||||
|
||||
(t/deftest ^:async show-branches-opens-the-switcher-when-there-are-branches
|
||||
(await (mock/with-mocks*
|
||||
{cf/flags (conj cf/flags :branching)}
|
||||
(let [store (make-store-with main-with-branches)]
|
||||
(ptk/emit! store (dwb/show-branches))
|
||||
(await (async/wait-for #(true? (:workspace-branch-switcher @store))
|
||||
"the switcher opens"))
|
||||
(await (async/settle))
|
||||
(t/is (nil? (::modal/modal @store)) "no dialog opens")
|
||||
(rx/dispose! store)))))
|
||||
|
||||
(t/deftest ^:async show-branches-asks-for-the-first-branch-when-there-are-none
|
||||
(await (mock/with-mocks*
|
||||
{cf/flags (conj cf/flags :branching)}
|
||||
(let [store (make-store-with main-without-branches)]
|
||||
(ptk/emit! store (dwb/show-branches))
|
||||
(await (async/wait-for #(some? (::modal/modal @store))
|
||||
"the create dialog opens"))
|
||||
(t/is (= :create-branch (get-in @store [::modal/modal :type])))
|
||||
(t/is (= "Checkout" (get-in @store [::modal/modal :props :file-name])))
|
||||
(t/is (not (:workspace-branch-switcher @store)))
|
||||
(rx/dispose! store)))))
|
||||
|
||||
(t/deftest ^:async show-branches-does-nothing-without-the-flag
|
||||
(await (mock/with-mocks*
|
||||
{cf/flags (disj cf/flags :branching)}
|
||||
(let [store (make-store-with main-without-branches)]
|
||||
(ptk/emit! store (dwb/show-branches))
|
||||
(await (async/settle))
|
||||
(t/is (nil? (::modal/modal @store)))
|
||||
(t/is (nil? (:workspace-branch-switcher @store)))
|
||||
(rx/dispose! store)))))
|
||||
|
||||
(t/deftest ^:async opening-a-branch-closes-the-switcher-and-navigates
|
||||
(let [navigations (atom [])
|
||||
branch-id (uuid/next)]
|
||||
(await (mock/with-mocks*
|
||||
{dcm/go-to-workspace
|
||||
(fn [& {:as params}]
|
||||
(swap! navigations conj params)
|
||||
(ptk/data-event ::navigated params))}
|
||||
(let [store (make-store-with (assoc main-with-branches :workspace-branch-switcher true))]
|
||||
(ptk/emit! store (dwb/open-branch branch-id))
|
||||
(await (async/wait-for #(seq @navigations) "it navigates"))
|
||||
(t/is (false? (:workspace-branch-switcher @store)))
|
||||
(t/is (= {:file-id branch-id} (first @navigations)))
|
||||
(rx/dispose! store))))))
|
||||
|
||||
@ -368,9 +368,9 @@ msgstr "Clear trash"
|
||||
msgid "dashboard.copy-suffix"
|
||||
msgstr "(copy)"
|
||||
|
||||
#: src/app/main/ui/dashboard/file_menu.cljs:314
|
||||
#, unused
|
||||
msgid "dashboard.create-branch"
|
||||
msgstr "Create branch…"
|
||||
msgstr "Create branch"
|
||||
|
||||
#: src/app/main/ui/dashboard/organization_team_switch.cljs:223, src/app/main/ui/dashboard/organization_team_switch.cljs:304
|
||||
msgid "dashboard.create-new-organization"
|
||||
@ -6364,7 +6364,7 @@ msgstr "%s conflicts"
|
||||
msgid "workspace.branches.banner.expand"
|
||||
msgstr "Expand"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branches.cljs:1596
|
||||
#, unused
|
||||
msgid "workspace.branches.banner.from-main"
|
||||
msgstr "from main"
|
||||
|
||||
@ -6376,6 +6376,32 @@ msgstr "Merged branch"
|
||||
msgid "workspace.branches.banner.on-branch"
|
||||
msgstr "On branch"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branches.cljs:1853
|
||||
msgid "workspace.branches.banner.status.ahead"
|
||||
msgid_plural "workspace.branches.banner.status.ahead"
|
||||
msgstr[0] "1 change not in main"
|
||||
msgstr[1] "%s changes not in main"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branches.cljs:1841
|
||||
msgid "workspace.branches.banner.status.behind"
|
||||
msgid_plural "workspace.branches.banner.status.behind"
|
||||
msgstr[0] "main has 1 new change"
|
||||
msgstr[1] "main has %s new changes"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branches.cljs:1836
|
||||
msgid "workspace.branches.banner.status.conflicts"
|
||||
msgid_plural "workspace.branches.banner.status.conflicts"
|
||||
msgstr[0] "1 conflict with main"
|
||||
msgstr[1] "%s conflicts with main"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branches.cljs:1847
|
||||
msgid "workspace.branches.banner.status.in-review"
|
||||
msgstr "In review · %s of %s approved"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branches.cljs:1858
|
||||
msgid "workspace.branches.banner.status.up-to-date"
|
||||
msgstr "Up to date with main"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branches.cljs:1602
|
||||
msgid "workspace.branches.banner.view-main"
|
||||
msgstr "View in main"
|
||||
@ -6651,7 +6677,7 @@ msgstr "No conflicts to resolve"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branches.cljs:1520
|
||||
msgid "workspace.branches.conflicts.footer-pending"
|
||||
msgstr "%s conflict(s) pending · resolve all to merge"
|
||||
msgstr "%s conflict(s) pending. Resolve all to merge"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branches.cljs:1515
|
||||
msgid "workspace.branches.conflicts.footer-ready"
|
||||
@ -7012,7 +7038,23 @@ msgstr ""
|
||||
msgid "workspace.branches.limits.refuse-name"
|
||||
msgstr "A branch name can't be empty."
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branches.cljs:799
|
||||
#: src/app/main/ui/workspace/sidebar/branches.cljs:1000
|
||||
msgid "workspace.branches.list.empty"
|
||||
msgstr "No branches here"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branches.cljs:945
|
||||
msgid "workspace.branches.list.tab.active"
|
||||
msgstr "Active"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branches.cljs:944
|
||||
msgid "workspace.branches.list.tab.mine"
|
||||
msgstr "Mine"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branches.cljs:988
|
||||
msgid "workspace.branches.list.title"
|
||||
msgstr "Branches - %s"
|
||||
|
||||
#, unused
|
||||
msgid "workspace.branches.loading"
|
||||
msgstr "Loading branches…"
|
||||
|
||||
@ -7022,7 +7064,7 @@ msgstr "Main"
|
||||
|
||||
#: src/app/main/data/workspace/branches.cljs:336
|
||||
msgid "workspace.branches.main-advanced"
|
||||
msgstr "main has new changes — you can update this branch"
|
||||
msgstr "Main has new changes. You can update this branch"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branches.cljs:659
|
||||
msgid "workspace.branches.main-subtitle"
|
||||
@ -7058,6 +7100,10 @@ msgstr "Branches"
|
||||
msgid "workspace.branches.menu.archive"
|
||||
msgstr "Archive"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:1152
|
||||
msgid "workspace.branches.menu.create"
|
||||
msgstr "Create branch"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branches.cljs:682
|
||||
msgid "workspace.branches.menu.info"
|
||||
msgstr "Branch info"
|
||||
@ -7129,7 +7175,7 @@ msgstr ""
|
||||
"This branch changes elements that can't be merged automatically yet "
|
||||
"(components, pages or tokens)"
|
||||
|
||||
#: src/app/main/ui/dashboard/branches_popover.cljs:84, src/app/main/ui/workspace/sidebar/branches.cljs:793
|
||||
#, unused
|
||||
msgid "workspace.branches.new"
|
||||
msgstr "New"
|
||||
|
||||
@ -7161,7 +7207,23 @@ msgstr "Deleted"
|
||||
msgid "workspace.branches.status.modified"
|
||||
msgstr "Modified"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar.cljs:261
|
||||
#: src/app/main/ui/workspace/branch_switcher.cljs:242
|
||||
msgid "workspace.branches.switcher.all"
|
||||
msgstr "All branches"
|
||||
|
||||
#: src/app/main/ui/workspace/branch_switcher.cljs:238
|
||||
msgid "workspace.branches.switcher.new"
|
||||
msgstr "New branch"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branches.cljs:1010
|
||||
msgid "workspace.branches.switcher.new-from-main"
|
||||
msgstr "Branches are created from main"
|
||||
|
||||
#: src/app/main/ui/workspace/branch_switcher.cljs:309
|
||||
msgid "workspace.branches.switcher.tooltip"
|
||||
msgstr "Switch branch"
|
||||
|
||||
#, unused
|
||||
msgid "workspace.branches.tab"
|
||||
msgstr "Branches"
|
||||
|
||||
@ -9216,14 +9278,14 @@ msgstr "%s conflicts"
|
||||
msgid "workspace.pull-requests.badge.outdated"
|
||||
msgstr "New changes on branch"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/pull_requests.cljs:623
|
||||
msgid "workspace.pull-requests.banner.cancel"
|
||||
msgstr "Cancel review"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/pull_requests.cljs:759
|
||||
msgid "workspace.pull-requests.banner.exit"
|
||||
msgstr "Exit review"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/pull_requests.cljs:736
|
||||
msgid "workspace.pull-requests.banner.play"
|
||||
msgstr "Try interactions"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/pull_requests.cljs:742
|
||||
msgid "workspace.pull-requests.banner.publish"
|
||||
msgstr "Publish latest changes"
|
||||
@ -9265,10 +9327,9 @@ msgstr "An error occurred while creating the pull request"
|
||||
#: src/app/main/ui/workspace/sidebar/pull_requests.cljs:193
|
||||
msgid "workspace.pull-requests.create.info"
|
||||
msgstr ""
|
||||
"Reviewers will open the branch itself, so any edit they make changes the "
|
||||
"branch. Try interactions plays the branch as published now, and later edits "
|
||||
"show there only after newer changes are published. You can keep working on "
|
||||
"the branch and publish newer changes at any time."
|
||||
"Reviewers open the branch itself, so any edit they make changes the branch, "
|
||||
"and View mode plays the branch as it is now. You can keep working on the "
|
||||
"branch and publish newer changes at any time."
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/pull_requests.cljs:159
|
||||
msgid "workspace.pull-requests.create.name-label"
|
||||
@ -9306,6 +9367,10 @@ msgstr "Create pull request"
|
||||
msgid "workspace.pull-requests.header-badge-tooltip"
|
||||
msgstr "Reviewing pull request: %s"
|
||||
|
||||
#: src/app/main/ui/workspace/left_header.cljs:177
|
||||
msgid "workspace.pull-requests.header-label"
|
||||
msgstr "Review:"
|
||||
|
||||
#: src/app/main/ui/workspace/left_header.cljs:54
|
||||
msgid "workspace.pull-requests.header-title"
|
||||
msgstr "Review: %s"
|
||||
|
||||
@ -6416,6 +6416,68 @@ msgstr "Transformar texto"
|
||||
msgid "workspace.assets.ungroup"
|
||||
msgstr "Desagrupar"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branches.cljs:1853
|
||||
msgid "workspace.branches.banner.status.ahead"
|
||||
msgid_plural "workspace.branches.banner.status.ahead"
|
||||
msgstr[0] "1 cambio que no está en main"
|
||||
msgstr[1] "%s cambios que no están en main"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branches.cljs:1841
|
||||
msgid "workspace.branches.banner.status.behind"
|
||||
msgid_plural "workspace.branches.banner.status.behind"
|
||||
msgstr[0] "main tiene 1 cambio nuevo"
|
||||
msgstr[1] "main tiene %s cambios nuevos"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branches.cljs:1836
|
||||
msgid "workspace.branches.banner.status.conflicts"
|
||||
msgid_plural "workspace.branches.banner.status.conflicts"
|
||||
msgstr[0] "1 conflicto con main"
|
||||
msgstr[1] "%s conflictos con main"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branches.cljs:1847
|
||||
msgid "workspace.branches.banner.status.in-review"
|
||||
msgstr "En revisión · %s de %s aprobaciones"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branches.cljs:1858
|
||||
msgid "workspace.branches.banner.status.up-to-date"
|
||||
msgstr "Al día con main"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branches.cljs:1000
|
||||
msgid "workspace.branches.list.empty"
|
||||
msgstr "No hay ramas aquí"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branches.cljs:945
|
||||
msgid "workspace.branches.list.tab.active"
|
||||
msgstr "Activas"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branches.cljs:944
|
||||
msgid "workspace.branches.list.tab.mine"
|
||||
msgstr "Mías"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branches.cljs:988
|
||||
msgid "workspace.branches.list.title"
|
||||
msgstr "Ramas - %s"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:1152
|
||||
msgid "workspace.branches.menu.create"
|
||||
msgstr "Crear rama"
|
||||
|
||||
#: src/app/main/ui/workspace/branch_switcher.cljs:242
|
||||
msgid "workspace.branches.switcher.all"
|
||||
msgstr "Todas las ramas"
|
||||
|
||||
#: src/app/main/ui/workspace/branch_switcher.cljs:238
|
||||
msgid "workspace.branches.switcher.new"
|
||||
msgstr "Nueva rama"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/branches.cljs:1010
|
||||
msgid "workspace.branches.switcher.new-from-main"
|
||||
msgstr "Las ramas se crean desde main"
|
||||
|
||||
#: src/app/main/ui/workspace/branch_switcher.cljs:309
|
||||
msgid "workspace.branches.switcher.tooltip"
|
||||
msgstr "Cambiar de rama"
|
||||
|
||||
#: src/app/main/data/workspace/clipboard.cljs:1170
|
||||
msgid "workspace.clipboard.copying"
|
||||
msgstr "Copiando imagen…"
|
||||
@ -9692,3 +9754,11 @@ msgstr "Si quieres aumentar este límite, contáctanos en [support@penpot.app](%
|
||||
#: src/app/main/ui/workspace/sidebar/versions.cljs:462
|
||||
msgid "workspace.versions.warning.text"
|
||||
msgstr "Los autoguardados duran %s días."
|
||||
|
||||
#: src/app/main/ui/workspace/left_header.cljs:177
|
||||
msgid "workspace.pull-requests.header-label"
|
||||
msgstr "Revisión:"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/pull_requests.cljs:623
|
||||
msgid "workspace.pull-requests.banner.cancel"
|
||||
msgstr "Cancelar revisión"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user