diff --git a/routers/web/repo/view_home.go b/routers/web/repo/view_home.go index e0539f53b0..b318c4a621 100644 --- a/routers/web/repo/view_home.go +++ b/routers/web/repo/view_home.go @@ -74,9 +74,9 @@ func prepareOpenWithEditorApps(ctx *context.Context) { schema, _, _ := strings.Cut(app.OpenURL, ":") var iconHTML template.HTML if schema == "vscode" || schema == "vscodium" || schema == "jetbrains" { - iconHTML = svg.RenderHTML(fmt.Sprintf("gitea-%s", schema), 16, "tw-mr-2") + iconHTML = svg.RenderHTML(fmt.Sprintf("gitea-%s", schema), 16) } else { - iconHTML = svg.RenderHTML("gitea-git", 16, "tw-mr-2") // TODO: it could support user's customized icon in the future + iconHTML = svg.RenderHTML("gitea-git", 16) // TODO: it could support user's customized icon in the future } tmplApps = append(tmplApps, map[string]any{ "DisplayName": app.DisplayName, diff --git a/templates/repo/clone_buttons.tmpl b/templates/repo/clone_buttons.tmpl index 91952c8a06..03b7a561da 100644 --- a/templates/repo/clone_buttons.tmpl +++ b/templates/repo/clone_buttons.tmpl @@ -1,15 +1,13 @@ - -{{if $.CloneButtonShowHTTPS}} - + {{end}} + {{if $.CloneButtonShowSSH}} + + {{end}} + + -{{end}} -{{if $.CloneButtonShowSSH}} - -{{end}} - - + diff --git a/templates/repo/clone_panel.tmpl b/templates/repo/clone_panel.tmpl new file mode 100644 index 0000000000..8cbeda132d --- /dev/null +++ b/templates/repo/clone_panel.tmpl @@ -0,0 +1,44 @@ + +
+
{{svg "octicon-terminal"}} Clone
+ +
+ + {{if $.CloneButtonShowHTTPS}} + + {{end}} + {{if $.CloneButtonShowSSH}} + + {{end}} +
+
+ +
+
+ +
+ {{svg "octicon-copy" 14}} +
+
+
+ + {{if not .PageIsWiki}} +
+ {{range .OpenWithEditorApps}} + {{.IconHTML}}{{ctx.Locale.Tr "repo.open_with_editor" .DisplayName}} + {{end}} +
+ + {{if and (not $.DisableDownloadSourceArchives) $.RefName}} +
+
+ {{svg "octicon-file-zip"}} {{ctx.Locale.Tr "repo.download_zip"}} + {{svg "octicon-file-zip"}} {{ctx.Locale.Tr "repo.download_tar"}} + {{svg "octicon-package"}} {{ctx.Locale.Tr "repo.download_bundle"}} +
+ {{end}} + {{end}} +
diff --git a/templates/repo/clone_script.tmpl b/templates/repo/clone_script.tmpl deleted file mode 100644 index 40dae76dc7..0000000000 --- a/templates/repo/clone_script.tmpl +++ /dev/null @@ -1,50 +0,0 @@ - diff --git a/templates/repo/empty.tmpl b/templates/repo/empty.tmpl index d3a81bc51d..7170fe3602 100644 --- a/templates/repo/empty.tmpl +++ b/templates/repo/empty.tmpl @@ -37,9 +37,7 @@ {{end}} {{end}} -
- {{template "repo/clone_buttons" .}} -
+ {{template "repo/clone_buttons" .}} @@ -73,7 +71,6 @@ git push -u origin {{.Repository.DefaultBranch}} {{ctx.Locale.Tr "repo.empty_message"}} {{end}} - {{template "repo/clone_script" .}} diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl index 46d0398c21..cc36fa4eea 100644 --- a/templates/repo/home.tmpl +++ b/templates/repo/home.tmpl @@ -106,23 +106,7 @@
{{if $isTreePathRoot}} -
- {{template "repo/clone_buttons" .}} - - {{template "repo/clone_script" .}}{{/* the script will update `.js-clone-url` and related elements */}} -
+ {{template "repo/clone_panel" .}} {{end}} {{if and (not $isTreePathRoot) (not .IsViewFile) (not .IsBlame)}}{{/* IsViewDirectory (not home), TODO: split the templates, avoid using "if" tricks */}} diff --git a/templates/repo/wiki/revision.tmpl b/templates/repo/wiki/revision.tmpl index 045cc41d81..ca8954928d 100644 --- a/templates/repo/wiki/revision.tmpl +++ b/templates/repo/wiki/revision.tmpl @@ -15,10 +15,7 @@
-
- {{template "repo/clone_buttons" .}} - {{template "repo/clone_script" .}} -
+ {{template "repo/clone_panel" .}}

{{ctx.Locale.Tr "repo.wiki.wiki_page_revisions"}}

diff --git a/templates/repo/wiki/view.tmpl b/templates/repo/wiki/view.tmpl index c8e0b4254c..68933b0bcf 100644 --- a/templates/repo/wiki/view.tmpl +++ b/templates/repo/wiki/view.tmpl @@ -28,10 +28,7 @@ -
- {{template "repo/clone_buttons" .}} - {{template "repo/clone_script" .}} -
+ {{template "repo/clone_panel" .}}
diff --git a/tests/integration/repo_test.go b/tests/integration/repo_test.go index b967ccad1e..1b9f6887fd 100644 --- a/tests/integration/repo_test.go +++ b/tests/integration/repo_test.go @@ -127,10 +127,10 @@ func TestViewRepo1CloneLinkAnonymous(t *testing.T) { resp := MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) - link, exists := htmlDoc.doc.Find("#repo-clone-https").Attr("data-link") + link, exists := htmlDoc.doc.Find(".repo-clone-https").Attr("data-link") assert.True(t, exists, "The template has changed") assert.Equal(t, setting.AppURL+"user2/repo1.git", link) - _, exists = htmlDoc.doc.Find("#repo-clone-ssh").Attr("data-link") + _, exists = htmlDoc.doc.Find(".repo-clone-ssh").Attr("data-link") assert.False(t, exists) } @@ -143,10 +143,10 @@ func TestViewRepo1CloneLinkAuthorized(t *testing.T) { resp := session.MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) - link, exists := htmlDoc.doc.Find("#repo-clone-https").Attr("data-link") + link, exists := htmlDoc.doc.Find(".repo-clone-https").Attr("data-link") assert.True(t, exists, "The template has changed") assert.Equal(t, setting.AppURL+"user2/repo1.git", link) - link, exists = htmlDoc.doc.Find("#repo-clone-ssh").Attr("data-link") + link, exists = htmlDoc.doc.Find(".repo-clone-ssh").Attr("data-link") assert.True(t, exists, "The template has changed") sshURL := fmt.Sprintf("ssh://%s@%s:%d/user2/repo1.git", setting.SSH.User, setting.SSH.Domain, setting.SSH.Port) assert.Equal(t, sshURL, link) diff --git a/web_src/css/index.css b/web_src/css/index.css index 158ae42d3e..43648268c5 100644 --- a/web_src/css/index.css +++ b/web_src/css/index.css @@ -67,6 +67,7 @@ @import "./repo/header.css"; @import "./repo/home.css"; @import "./repo/reactions.css"; +@import "./repo/clone.css"; @import "./editor/fileeditor.css"; @import "./editor/combomarkdowneditor.css"; diff --git a/web_src/css/repo.css b/web_src/css/repo.css index f5785c41a7..cf637e1c48 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -101,42 +101,6 @@ margin-bottom: 12px; } -.repository .clone-panel { - display: flex; - flex: 1; -} - -.repository.wiki .clone-panel { - flex: 0; -} - -.repository.wiki .clone-panel input { - width: 20ch; -} - -.repository .clone-panel #repo-clone-url { - border-radius: 0; - flex: 1; -} - -.repository .ui.action.input.clone-panel > button + button, -.repository .ui.action.input.clone-panel > button + input { - margin-left: -1px; /* make the borders overlap to avoid double borders */ -} - -.repository .clone-panel > button:first-of-type { - border-radius: var(--border-radius) 0 0 var(--border-radius) !important; -} - -.repository .clone-panel > button:last-of-type { - border-radius: 0 var(--border-radius) var(--border-radius) 0 !important; -} - -.repository .clone-panel .dropdown .menu { - right: 0 !important; - left: auto !important; -} - .repository .repo-description { font-size: 16px; margin-bottom: 5px; @@ -1615,14 +1579,6 @@ td .commit-summary { font-weight: var(--font-weight-normal); } -.repository.quickstart .guide #repo-clone-url { - border-radius: 0; - padding: 5px 10px; - font-size: 1.2em; - line-height: 1.4; - flex: 1 -} - .empty-placeholder { display: flex; flex-direction: column; diff --git a/web_src/css/repo/clone.css b/web_src/css/repo/clone.css new file mode 100644 index 0000000000..15709a78f6 --- /dev/null +++ b/web_src/css/repo/clone.css @@ -0,0 +1,32 @@ +/* only used by "repo/empty.tmpl" */ +.clone-buttons-combo { + flex: 1; +} + +.clone-buttons-combo input { + border-left: none !important; + border-radius: 0 !important; +} + +/* used by the clone-panel popup */ +.clone-panel-field, +.clone-panel-list { + margin: 10px; +} + +.clone-panel-tab .item { + padding: 5px 10px; + background: none; +} + +.clone-panel-tab .item.active { + border-bottom: 3px solid var(--color-secondary); +} + +.clone-panel-tab + .divider { + margin: -1px 0 0; +} + +.clone-panel-list .item { + margin: 5px 0; +} diff --git a/web_src/css/repo/wiki.css b/web_src/css/repo/wiki.css index ba502d3216..ca59dadb9c 100644 --- a/web_src/css/repo/wiki.css +++ b/web_src/css/repo/wiki.css @@ -59,9 +59,6 @@ } @media (max-width: 767.98px) { - .repository.wiki .clone-panel #repo-clone-url { - width: 160px; - } .repository.wiki .wiki-content-main.with-sidebar, .repository.wiki .wiki-content-sidebar { float: none; diff --git a/web_src/js/features/repo-common.ts b/web_src/js/features/repo-common.ts index 5185a7ca43..336deb125f 100644 --- a/web_src/js/features/repo-common.ts +++ b/web_src/js/features/repo-common.ts @@ -5,6 +5,8 @@ import {showErrorToast} from '../modules/toast.ts'; import {sleep} from '../utils.ts'; import RepoActivityTopAuthors from '../components/RepoActivityTopAuthors.vue'; import {createApp} from 'vue'; +import {toOriginUrl} from '../utils/url.ts'; +import {createTippy} from '../modules/tippy.ts'; async function onDownloadArchive(e) { e.preventDefault(); @@ -41,27 +43,68 @@ export function initRepoActivityTopAuthorsChart() { } } -export function initRepoCloneLink() { - const $repoCloneSsh = $('#repo-clone-ssh'); - const $repoCloneHttps = $('#repo-clone-https'); - const $inputLink = $('#repo-clone-url'); +function initCloneSchemeUrlSelection(parent: Element) { + const elCloneUrlInput = parent.querySelector('.repo-clone-url'); - if ((!$repoCloneSsh.length && !$repoCloneHttps.length) || !$inputLink.length) { - return; - } + const tabSsh = parent.querySelector('.repo-clone-ssh'); + const tabHttps = parent.querySelector('.repo-clone-https'); + const updateClonePanelUi = function() { + const scheme = localStorage.getItem('repo-clone-protocol') || 'https'; + const isSSH = scheme === 'ssh' && Boolean(tabSsh) || scheme !== 'ssh' && !tabHttps; + if (tabHttps) { + tabHttps.textContent = window.origin.split(':')[0].toUpperCase(); // show "HTTP" or "HTTPS" + tabHttps.classList.toggle('active', !isSSH); + } + if (tabSsh) { + tabSsh.classList.toggle('active', isSSH); + } - $repoCloneSsh.on('click', () => { + const tab = isSSH ? tabSsh : tabHttps; + if (!tab) return; + const link = toOriginUrl(tab.getAttribute('data-link')); + + for (const el of document.querySelectorAll('.js-clone-url')) { + if (el.nodeName === 'INPUT') { + (el as HTMLInputElement).value = link; + } else { + el.textContent = link; + } + } + for (const el of parent.querySelectorAll('.js-clone-url-editor')) { + el.href = el.getAttribute('data-href-template').replace('{url}', encodeURIComponent(link)); + } + }; + + updateClonePanelUi(); + + tabSsh.addEventListener('click', () => { localStorage.setItem('repo-clone-protocol', 'ssh'); - window.updateCloneStates(); + updateClonePanelUi(); }); - $repoCloneHttps.on('click', () => { + tabHttps.addEventListener('click', () => { localStorage.setItem('repo-clone-protocol', 'https'); - window.updateCloneStates(); + updateClonePanelUi(); }); + elCloneUrlInput.addEventListener('focus', () => { + elCloneUrlInput.select(); + }); +} - $inputLink.on('focus', () => { - $inputLink.trigger('select'); +function initClonePanelButton(btn: HTMLButtonElement) { + const elPanel = btn.nextElementSibling; + createTippy(btn, { + content: elPanel, + trigger: 'click', + placement: 'bottom-end', + interactive: true, + hideOnClick: true, }); + initCloneSchemeUrlSelection(elPanel); +} + +export function initRepoCloneButtons() { + queryElems(document, '.js-btn-clone-panel', initClonePanelButton); + queryElems(document, '.clone-buttons-combo', initCloneSchemeUrlSelection); } export function initRepoCommonBranchOrTagDropdown(selector: string) { diff --git a/web_src/js/features/repo-legacy.ts b/web_src/js/features/repo-legacy.ts index dfea66c7ad..2f760f1d15 100644 --- a/web_src/js/features/repo-legacy.ts +++ b/web_src/js/features/repo-legacy.ts @@ -9,7 +9,7 @@ import { import {initUnicodeEscapeButton} from './repo-unicode-escape.ts'; import {initRepoBranchTagSelector} from '../components/RepoBranchTagSelector.vue'; import { - initRepoCloneLink, initRepoCommonBranchOrTagDropdown, initRepoCommonFilterSearchDropdown, + initRepoCloneButtons, initRepoCommonBranchOrTagDropdown, initRepoCommonFilterSearchDropdown, } from './repo-common.ts'; import {initCitationFileCopyContent} from './citation.ts'; import {initCompLabelEdit} from './comp/LabelEdit.ts'; @@ -54,7 +54,7 @@ export function initRepository() { initRepoCommonFilterSearchDropdown('.choose.branch .dropdown'); } - initRepoCloneLink(); + initRepoCloneButtons(); initCitationFileCopyContent(); initRepoSettings(); diff --git a/web_src/js/utils/url.test.ts b/web_src/js/utils/url.test.ts index 25fda79b19..bb331a6b49 100644 --- a/web_src/js/utils/url.test.ts +++ b/web_src/js/utils/url.test.ts @@ -1,4 +1,4 @@ -import {pathEscapeSegments, isUrl} from './url.ts'; +import {pathEscapeSegments, isUrl, toOriginUrl} from './url.ts'; test('pathEscapeSegments', () => { expect(pathEscapeSegments('a/b/c')).toEqual('a/b/c'); @@ -11,3 +11,19 @@ test('isUrl', () => { expect(isUrl('https://example.com/index.html')).toEqual(true); expect(isUrl('/index.html')).toEqual(false); }); + +test('toOriginUrl', () => { + const oldLocation = String(window.location); + for (const origin of ['https://example.com', 'https://example.com:3000']) { + window.location.assign(`${origin}/`); + expect(toOriginUrl('/')).toEqual(`${origin}/`); + expect(toOriginUrl('/org/repo.git')).toEqual(`${origin}/org/repo.git`); + expect(toOriginUrl('https://another.com')).toEqual(`${origin}/`); + expect(toOriginUrl('https://another.com/')).toEqual(`${origin}/`); + expect(toOriginUrl('https://another.com/org/repo.git')).toEqual(`${origin}/org/repo.git`); + expect(toOriginUrl('https://another.com:4000')).toEqual(`${origin}/`); + expect(toOriginUrl('https://another.com:4000/')).toEqual(`${origin}/`); + expect(toOriginUrl('https://another.com:4000/org/repo.git')).toEqual(`${origin}/org/repo.git`); + } + window.location.assign(oldLocation); +}); diff --git a/web_src/js/utils/url.ts b/web_src/js/utils/url.ts index c5a28774a9..a7d61c5e83 100644 --- a/web_src/js/utils/url.ts +++ b/web_src/js/utils/url.ts @@ -13,3 +13,19 @@ export function isUrl(url: string): boolean { return false; } } + +// Convert an absolute or relative URL to an absolute URL with the current origin. It only +// processes absolute HTTP/HTTPS URLs or relative URLs like '/xxx' or '//host/xxx'. +export function toOriginUrl(urlStr: string) { + try { + if (urlStr.startsWith('http://') || urlStr.startsWith('https://') || urlStr.startsWith('/')) { + const {origin, protocol, hostname, port} = window.location; + const url = new URL(urlStr, origin); + url.protocol = protocol; + url.hostname = hostname; + url.port = port || (protocol === 'https:' ? '443' : '80'); + return url.toString(); + } + } catch {} + return urlStr; +} diff --git a/web_src/js/webcomponents/origin-url.test.ts b/web_src/js/webcomponents/origin-url.test.ts deleted file mode 100644 index 19cc467d7d..0000000000 --- a/web_src/js/webcomponents/origin-url.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {toOriginUrl} from './origin-url.ts'; - -test('toOriginUrl', () => { - const oldLocation = String(window.location); - for (const origin of ['https://example.com', 'https://example.com:3000']) { - window.location.assign(`${origin}/`); - expect(toOriginUrl('/')).toEqual(`${origin}/`); - expect(toOriginUrl('/org/repo.git')).toEqual(`${origin}/org/repo.git`); - expect(toOriginUrl('https://another.com')).toEqual(`${origin}/`); - expect(toOriginUrl('https://another.com/')).toEqual(`${origin}/`); - expect(toOriginUrl('https://another.com/org/repo.git')).toEqual(`${origin}/org/repo.git`); - expect(toOriginUrl('https://another.com:4000')).toEqual(`${origin}/`); - expect(toOriginUrl('https://another.com:4000/')).toEqual(`${origin}/`); - expect(toOriginUrl('https://another.com:4000/org/repo.git')).toEqual(`${origin}/org/repo.git`); - } - window.location.assign(oldLocation); -}); diff --git a/web_src/js/webcomponents/origin-url.ts b/web_src/js/webcomponents/origin-url.ts index d407fe0dff..dbb910ce6c 100644 --- a/web_src/js/webcomponents/origin-url.ts +++ b/web_src/js/webcomponents/origin-url.ts @@ -1,19 +1,4 @@ -// Convert an absolute or relative URL to an absolute URL with the current origin. It only -// processes absolute HTTP/HTTPS URLs or relative URLs like '/xxx' or '//host/xxx'. -// NOTE: Keep this function in sync with clone_script.tmpl -export function toOriginUrl(urlStr: string) { - try { - if (urlStr.startsWith('http://') || urlStr.startsWith('https://') || urlStr.startsWith('/')) { - const {origin, protocol, hostname, port} = window.location; - const url = new URL(urlStr, origin); - url.protocol = protocol; - url.hostname = hostname; - url.port = port || (protocol === 'https:' ? '443' : '80'); - return url.toString(); - } - } catch {} - return urlStr; -} +import {toOriginUrl} from '../utils/url.ts'; window.customElements.define('origin-url', class extends HTMLElement { connectedCallback() {