diff --git a/backend/src/app/rpc/commands/files_branch.clj b/backend/src/app/rpc/commands/files_branch.clj index 9bbd09e371..d48d3cdc5e 100644 --- a/backend/src/app/rpc/commands/files_branch.clj +++ b/backend/src/app/rpc/commands/files_branch.clj @@ -1733,7 +1733,8 @@ fb.base_snapshot_id, bf.revn AS branch_revn, sf.revn AS source_revn, - sf.name AS source_name + sf.name AS source_name, + sf.is_shared AS source_is_shared FROM file_branch AS fb JOIN file AS bf ON (bf.id = fb.branch_file_id) JOIN file AS sf ON (sf.id = fb.source_file_id) diff --git a/frontend/playwright/data/pull-requests/get-file-blank-shared.json b/frontend/playwright/data/pull-requests/get-file-blank-shared.json new file mode 100644 index 0000000000..9a09260ac5 --- /dev/null +++ b/frontend/playwright/data/pull-requests/get-file-blank-shared.json @@ -0,0 +1,58 @@ +{ + "~:features": { + "~#set": [ + "layout/grid", + "styles/v2", + "fdata/pointer-map", + "fdata/objects-map", + "components/v2", + "fdata/shape-data-type" + ] + }, + "~:permissions": { + "~:type": "~:membership", + "~:is-owner": true, + "~:is-admin": true, + "~:can-edit": true, + "~:can-read": true, + "~:is-logged": true + }, + "~:has-media-trimmed": false, + "~:comment-thread-seqn": 0, + "~:name": "New File 1", + "~:revn": 11, + "~:modified-at": "~m1713873823633", + "~:id": "~uc7ce0794-0992-8105-8004-38f280443849", + "~:is-shared": true, + "~:version": 46, + "~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b", + "~:created-at": "~m1713536343369", + "~:data": { + "~:pages": [ + "~u66697432-c33d-8055-8006-2c62cc084cad" + ], + "~:pages-index": { + "~u66697432-c33d-8055-8006-2c62cc084cad": { + "~#penpot/pointer": [ + "~ude58c8f6-c5c2-8196-8004-3df9e2e52d88", + { + "~:created-at": "~m1713873823636" + } + ] + } + }, + "~:id": "~uc7ce0794-0992-8105-8004-38f280443849", + "~:options": { + "~:components-v2": true + }, + "~:recent-colors": [ + { + "~:color": "#0000ff", + "~:opacity": 1, + "~:id": null, + "~:file-id": null, + "~:image": null + } + ] + } +} \ No newline at end of file diff --git a/frontend/playwright/data/pull-requests/get-file-branch-info-library-branch.json b/frontend/playwright/data/pull-requests/get-file-branch-info-library-branch.json new file mode 100644 index 0000000000..848706e76a --- /dev/null +++ b/frontend/playwright/data/pull-requests/get-file-branch-info-library-branch.json @@ -0,0 +1,16 @@ +{ + "~: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": 1, + "~:behind": 0, + "~:conflicts": 0, + "~:source-is-shared": true +} diff --git a/frontend/playwright/data/pull-requests/get-file-branches-one.json b/frontend/playwright/data/pull-requests/get-file-branches-one.json new file mode 100644 index 0000000000..9fd1b73fb8 --- /dev/null +++ b/frontend/playwright/data/pull-requests/get-file-branches-one.json @@ -0,0 +1,17 @@ +[ + { + "~:id": "~uc7ce0794-0992-8105-8004-38f280443998", + "~:branch-file-id": "~uc7ce0794-0992-8105-8004-38f280443850", + "~:source-file-id": "~uc7ce0794-0992-8105-8004-38f280443849", + "~: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": 1, + "~:behind": 0, + "~:conflicts": 0 + } +] diff --git a/frontend/playwright/ui/specs/branches-library.spec.js b/frontend/playwright/ui/specs/branches-library.spec.js new file mode 100644 index 0000000000..ac135712e6 --- /dev/null +++ b/frontend/playwright/ui/specs/branches-library.spec.js @@ -0,0 +1,80 @@ +import { test, expect } from "@playwright/test"; +import { WorkspacePage } from "../pages/WorkspacePage"; + +test.beforeEach(async ({ page }) => { + await WorkspacePage.init(page); + await WorkspacePage.mockConfigFlags(page, ["enable-branching"]); +}); + +const setupBranching = async (workspacePage, rpcs) => { + await workspacePage.setupEmptyFile(); + await workspacePage.mockRPCs({ + "get-file-branch-info?file-id=*": + "pull-requests/get-file-branch-info-none.json", + "get-file-branches?file-id=*": "pull-requests/get-file-branches-empty.json", + "get-file-pull-requests?file-id=*": + "pull-requests/get-file-branches-empty.json", + ...rpcs, + }); +}; + +test("on a branch of a library, the header says consumers read main", async ({ + page, +}) => { + const workspacePage = new WorkspacePage(page); + await setupBranching(workspacePage, { + "get-file-branch-info?file-id=*": + "pull-requests/get-file-branch-info-library-branch.json", + }); + await workspacePage.goToWorkspace(); + + const chip = page.getByTitle( + "On branch checkout from main. Library consumers can adopt changes from main only.", + ); + await expect(chip).toBeVisible(); + + await chip.click(); + await expect( + page.getByText("Default branch (read by library consumers)"), + ).toBeVisible(); +}); + +test("on a library, the create dialog says consumers read main", async ({ + page, +}) => { + const workspacePage = new WorkspacePage(page); + await setupBranching(workspacePage, { + "get-file-branches?file-id=*": "pull-requests/get-file-branches-one.json", + }); + await workspacePage.mockGetFile("pull-requests/get-file-blank-shared.json"); + await workspacePage.goToWorkspace(); + + await expect( + page.getByText("Default branch (read by library consumers)"), + ).toBeVisible(); + + await page.getByTitle("Switch branch").click(); + await page.getByText("New branch").click(); + await expect( + page.getByText( + "files that use this library can adopt changes from main only", + ), + ).toBeVisible(); +}); + +test("on an ordinary file, the create dialog keeps its note", async ({ + page, +}) => { + const workspacePage = new WorkspacePage(page); + await setupBranching(workspacePage, { + "get-file-branches?file-id=*": "pull-requests/get-file-branches-one.json", + }); + await workspacePage.goToWorkspace(); + + await expect(page.getByText("Default branch", { exact: true })).toBeVisible(); + + await page.getByTitle("Switch branch").click(); + await page.getByText("New branch").click(); + await expect(page.getByText("isolated copy")).toBeVisible(); + await expect(page.getByText("files that use this library")).toBeHidden(); +}); diff --git a/frontend/src/app/main/ui/workspace/branch_switcher.cljs b/frontend/src/app/main/ui/workspace/branch_switcher.cljs index 15e1c027bb..50a6de071a 100644 --- a/frontend/src/app/main/ui/workspace/branch_switcher.cljs +++ b/frontend/src/app/main/ui/workspace/branch_switcher.cljs @@ -81,6 +81,14 @@ (zero? (or (:behind row) 0)) (zero? (or (:conflicts row) 0)))) +(defn- main-subtitle + "The note beside main. Files that use a shared library read its main, + so on a library it says so." + [library?] + (if library? + (tr "workspace.branches.main-subtitle-library") + (tr "workspace.branches.main-subtitle"))) + ;; --- Popover (mf/defc switcher-row* @@ -92,7 +100,7 @@ :data-branch (dm/str (:branch-file-id row)) :on-click on-open} (if (:is-main row) - (tr "workspace.branches.main-subtitle") + (main-subtitle (:is-library row)) [:> row-status* {:row row :pr pr}])]]) (mf/defc switcher-popover* @@ -115,6 +123,7 @@ (when-not (:is-main current) {:name (tr "workspace.branches.main") :is-main true + :is-library (boolean (:source-is-shared current)) :branch-file-id (:source-file-id current)}) rows @@ -234,6 +243,7 @@ (or ctx {:name (tr "workspace.branches.main") :is-main true + :is-library (boolean (:is-shared file)) :created-by (:id profile) :branch-file-id file-id}) @@ -275,8 +285,16 @@ [:div {:class (stl/css :branch-bar) :ref bar-ref} [:> branch-chip* {:label (:name current) :is-open open? - :title (if (:is-main current) + :title (cond + (:is-main current) (tr "workspace.branches.switcher.tooltip") + + ;; files that use the library read main, + ;; never this branch + (:source-is-shared current) + (tr "workspace.branches.header-badge-tooltip-library" (:name current)) + + :else (tr "workspace.branches.header-badge-tooltip" (:name current))) :on-click on-toggle}] @@ -288,7 +306,7 @@ [:> text* {:as "span" :typography t/body-small :class (stl/css :status-note)} - (tr "workspace.branches.main-subtitle")] + (main-subtitle (:is-shared file))] ^boolean merged? nil diff --git a/frontend/src/app/main/ui/workspace/sidebar/branches.cljs b/frontend/src/app/main/ui/workspace/sidebar/branches.cljs index b3fede1741..4a18539dba 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/branches.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/branches.cljs @@ -224,7 +224,9 @@ {::mf/register modal/components ::mf/register-as :create-branch} [{:keys [file-name file-id]}] - (let [branch-name* (mf/use-state "") + (let [;; a branch only grows from main, so the open file is main + library? (boolean (:is-shared (mf/deref refs/file))) + branch-name* (mf/use-state "") description* (mf/use-state "") branch-name (deref branch-name*) description (deref description*) @@ -279,7 +281,9 @@ (tr "workspace.branches.create.derive-from-latest")]]] [:> context-notification* {:level :info :type :context} - (tr "workspace.branches.create.info")]] + (if library? + (tr "workspace.branches.create.info-library") + (tr "workspace.branches.create.info"))]] [:> modal-footer* {} [:> button* {:variant "secondary" :on-click on-close} diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 3f7af349ea..022b3ae1c9 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -6808,6 +6808,12 @@ msgstr "" "Your branch is a complete, isolated copy. Nothing you do affects main until " "you merge it." +#: src/app/main/ui/workspace/sidebar/branches.cljs:283 +msgid "workspace.branches.create.info-library" +msgstr "" +"Your branch is a complete copy. Nothing you do affects main until you merge " +"it, and files that use this library can adopt changes from main only." + #: src/app/main/ui/workspace/sidebar/branches.cljs:283 msgid "workspace.branches.create.name-label" msgstr "Branch name" @@ -6849,6 +6855,10 @@ msgstr "No branches yet" msgid "workspace.branches.header-badge-tooltip" msgstr "On branch %s from main" +#: src/app/main/ui/workspace/branch_switcher.cljs:295 +msgid "workspace.branches.header-badge-tooltip-library" +msgstr "On branch %s from main. Library consumers can adopt changes from main only." + #: src/app/main/ui/workspace/sidebar/branches.cljs:485 msgid "workspace.branches.info.author" msgstr "Created by" @@ -7070,6 +7080,10 @@ msgstr "Main has new changes. You can update this branch" msgid "workspace.branches.main-subtitle" msgstr "Default branch" +#: src/app/main/ui/workspace/branch_switcher.cljs:88 +msgid "workspace.branches.main-subtitle-library" +msgstr "Default branch (read by library consumers)" + #: src/app/main/ui/workspace/sidebar/branches.cljs:779 msgid "workspace.branches.materialize.accept" msgstr "Turn it into a file"