diff options
-rw-r--r-- | routers/web/repo/view_home.go | 4 | ||||
-rw-r--r-- | templates/repo/clone_buttons.tmpl | 26 | ||||
-rw-r--r-- | templates/repo/clone_panel.tmpl | 44 | ||||
-rw-r--r-- | templates/repo/clone_script.tmpl | 50 | ||||
-rw-r--r-- | templates/repo/empty.tmpl | 5 | ||||
-rw-r--r-- | templates/repo/home.tmpl | 18 | ||||
-rw-r--r-- | templates/repo/wiki/revision.tmpl | 5 | ||||
-rw-r--r-- | templates/repo/wiki/view.tmpl | 5 | ||||
-rw-r--r-- | tests/integration/repo_test.go | 8 | ||||
-rw-r--r-- | web_src/css/index.css | 1 | ||||
-rw-r--r-- | web_src/css/repo.css | 44 | ||||
-rw-r--r-- | web_src/css/repo/clone.css | 32 | ||||
-rw-r--r-- | web_src/css/repo/wiki.css | 3 | ||||
-rw-r--r-- | web_src/js/features/repo-common.ts | 69 | ||||
-rw-r--r-- | web_src/js/features/repo-legacy.ts | 4 | ||||
-rw-r--r-- | web_src/js/utils/url.test.ts | 18 | ||||
-rw-r--r-- | web_src/js/utils/url.ts | 16 | ||||
-rw-r--r-- | web_src/js/webcomponents/origin-url.test.ts | 17 | ||||
-rw-r--r-- | web_src/js/webcomponents/origin-url.ts | 17 |
19 files changed, 191 insertions, 195 deletions
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 @@ -<!-- there is always at least one button (by context/repo.go) --> -{{if $.CloneButtonShowHTTPS}} - <button class="ui small button" id="repo-clone-https" data-link="{{$.CloneButtonOriginLink.HTTPS}}"> - HTTPS +<!-- there is always at least one button (guaranteed by context/repo.go) --> +<div class="ui action small input clone-buttons-combo"> + {{if $.CloneButtonShowHTTPS}} + <button class="ui small button repo-clone-https" data-link="{{$.CloneButtonOriginLink.HTTPS}}">HTTPS</button> + {{end}} + {{if $.CloneButtonShowSSH}} + <button class="ui small button repo-clone-ssh" data-link="{{$.CloneButtonOriginLink.SSH}}">SSH</button> + {{end}} + <input size="10" class="repo-clone-url js-clone-url" value="{{$.CloneButtonOriginLink.HTTPS}}" readonly> + <button class="ui small icon button" data-clipboard-target=".repo-clone-url" data-tooltip-content="{{ctx.Locale.Tr "copy_url"}}"> + {{svg "octicon-copy" 14}} </button> -{{end}} -{{if $.CloneButtonShowSSH}} - <button class="ui small button" id="repo-clone-ssh" data-link="{{$.CloneButtonOriginLink.SSH}}"> - SSH - </button> -{{end}} -<input id="repo-clone-url" size="10" class="js-clone-url" value="{{$.CloneButtonOriginLink.HTTPS}}" readonly> -<button class="ui small icon button" id="clipboard-btn" data-tooltip-content="{{ctx.Locale.Tr "copy_url"}}" data-clipboard-target="#repo-clone-url" aria-label="{{ctx.Locale.Tr "copy_url"}}"> - {{svg "octicon-copy" 14}} -</button> +</div> 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 @@ +<button class="ui green button js-btn-clone-panel"> + <span>{{svg "octicon-code" 16}} Code</span> + {{svg "octicon-triangle-down" 14 "dropdown icon"}} +</button> +<div class="clone-panel-popup tippy-target"> + <div class="flex-text-block clone-panel-field">{{svg "octicon-terminal"}} Clone</div> + + <div class="clone-panel-tab"> + <!-- there is always at least one button (guaranteed by context/repo.go) --> + {{if $.CloneButtonShowHTTPS}} + <button class="item repo-clone-https" data-link="{{$.CloneButtonOriginLink.HTTPS}}">HTTPS</button> + {{end}} + {{if $.CloneButtonShowSSH}} + <button class="item repo-clone-ssh" data-link="{{$.CloneButtonOriginLink.SSH}}">SSH</button> + {{end}} + </div> + <div class="divider"></div> + + <div class="clone-panel-field"> + <div class="ui input tiny action"> + <input size="30" class="repo-clone-url js-clone-url" value="{{$.CloneButtonOriginLink.HTTPS}}" readonly> + <div class="ui small compact icon button" data-clipboard-target=".js-clone-url" data-tooltip-content="{{ctx.Locale.Tr "copy_url"}}"> + {{svg "octicon-copy" 14}} + </div> + </div> + </div> + + {{if not .PageIsWiki}} + <div class="flex-items-block clone-panel-list"> + {{range .OpenWithEditorApps}} + <a class="item muted js-clone-url-editor" data-href-template="{{.OpenURL}}">{{.IconHTML}}{{ctx.Locale.Tr "repo.open_with_editor" .DisplayName}}</a> + {{end}} + </div> + + {{if and (not $.DisableDownloadSourceArchives) $.RefName}} + <div class="divider"></div> + <div class="flex-items-block clone-panel-list"> + <a class="item muted archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.zip" rel="nofollow">{{svg "octicon-file-zip"}} {{ctx.Locale.Tr "repo.download_zip"}}</a> + <a class="item muted archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip"}} {{ctx.Locale.Tr "repo.download_tar"}}</a> + <a class="item muted archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.bundle" rel="nofollow">{{svg "octicon-package"}} {{ctx.Locale.Tr "repo.download_bundle"}}</a> + </div> + {{end}} + {{end}} +</div> 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 @@ -<script> - // synchronously set clone button states and urls here to avoid flickering - // on page load. initRepoCloneLink calls this when proto changes. - // this applies the protocol-dependant clone url to all elements with the - // `js-clone-url` and `js-clone-url-vsc` classes. - // TODO: This localStorage setting should be moved to backend user config - // so it's available during rendering, then this inline script can be removed. - (window.updateCloneStates = function() { - const httpsBtn = document.getElementById('repo-clone-https'); - const sshBtn = document.getElementById('repo-clone-ssh'); - const value = localStorage.getItem('repo-clone-protocol') || 'https'; - const isSSH = value === 'ssh' && sshBtn || value !== 'ssh' && !httpsBtn; - - if (httpsBtn) { - httpsBtn.textContent = window.origin.split(':')[0].toUpperCase(); - httpsBtn.classList.toggle('primary', !isSSH); - httpsBtn.classList.toggle('basic', isSSH); - } - if (sshBtn) { - sshBtn.classList.toggle('primary', isSSH); - sshBtn.classList.toggle('basic', !isSSH); - } - - const btn = isSSH ? sshBtn : httpsBtn; - if (!btn) return; - - // NOTE: Keep this function in sync with the one in the js folder - function toOriginUrl(urlStr) { - 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; - } - const link = toOriginUrl(btn.getAttribute('data-link')); - - for (const el of document.getElementsByClassName('js-clone-url')) { - el[el.nodeName === 'INPUT' ? 'value' : 'textContent'] = link; - } - for (const el of document.getElementsByClassName('js-clone-url-editor')) { - el.href = el.getAttribute('data-href-template').replace('{url}', encodeURIComponent(link)); - } - })(); -</script> 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 @@ </a> {{end}} {{end}} - <div class="clone-panel ui action small input tw-flex-1"> - {{template "repo/clone_buttons" .}} - </div> + {{template "repo/clone_buttons" .}} </div> </div> @@ -73,7 +71,6 @@ git push -u origin {{.Repository.DefaultBranch}}</code></pre> {{ctx.Locale.Tr "repo.empty_message"}} </div> {{end}} - {{template "repo/clone_script" .}} </div> </div> </div> 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 @@ <div class="repo-button-row-right {{if not $isTreePathRoot}}tw-flex-grow-0{{end}}"> <!-- Only show clone panel in repository home page --> {{if $isTreePathRoot}} - <div class="clone-panel ui action tiny input"> - {{template "repo/clone_buttons" .}} - <button class="ui small jump dropdown icon button" data-tooltip-content="{{ctx.Locale.Tr "repo.more_operations"}}"> - {{svg "octicon-kebab-horizontal"}} - <div class="menu"> - {{if not $.DisableDownloadSourceArchives}} - <a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.zip" rel="nofollow">{{svg "octicon-file-zip" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.download_zip"}}</a> - <a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.download_tar"}}</a> - <a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.bundle" rel="nofollow">{{svg "octicon-package" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.download_bundle"}}</a> - {{end}} - {{range .OpenWithEditorApps}} - <a class="item js-clone-url-editor" data-href-template="{{.OpenURL}}">{{.IconHTML}}{{ctx.Locale.Tr "repo.open_with_editor" .DisplayName}}</a> - {{end}} - </div> - </button> - {{template "repo/clone_script" .}}{{/* the script will update `.js-clone-url` and related elements */}} - </div> + {{template "repo/clone_panel" .}} {{end}} {{if and (not $isTreePathRoot) (not .IsViewFile) (not .IsBlame)}}{{/* IsViewDirectory (not home), TODO: split the templates, avoid using "if" tricks */}} <a class="ui button" href="{{.RepoLink}}/commits/{{.BranchNameSubURL}}/{{.TreePath | PathEscapeSegments}}"> 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 @@ </div> </div> <div class="ui eight wide column text right"> - <div class="clone-panel ui action small input"> - {{template "repo/clone_buttons" .}} - {{template "repo/clone_script" .}} - </div> + {{template "repo/clone_panel" .}} </div> </div> <h2 class="ui top header">{{ctx.Locale.Tr "repo.wiki.wiki_page_revisions"}}</h2> 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 @@ </div> </div> </div> - <div class="clone-panel ui action small input"> - {{template "repo/clone_buttons" .}} - {{template "repo/clone_script" .}} - </div> + {{template "repo/clone_panel" .}} </div> <div class="ui dividing header"> <div class="flex-text-block tw-flex-wrap tw-justify-end"> 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<HTMLInputElement>('.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); + } + + const tab = isSSH ? tabSsh : tabHttps; + if (!tab) return; + const link = toOriginUrl(tab.getAttribute('data-link')); - $repoCloneSsh.on('click', () => { + 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<HTMLAnchorElement>('.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() { |