mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-03 20:36:07 +01:00 
			
		
		
		
	Rearrange Clone Panel (#31142)
Rearrange the clone panel to use less horizontal space. The following changes have been made to achieve this: - Moved everything into the dropdown menu - Moved the HTTPS/SSH Switch to a separate line - Moved the "Clone in VS Code"-Button up and added a divider - Named the dropdown button "Code", added appropriate icon --------- Co-authored-by: techknowlogick <techknowlogick@gitea.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
		@@ -74,9 +74,9 @@ func prepareOpenWithEditorApps(ctx *context.Context) {
 | 
				
			|||||||
		schema, _, _ := strings.Cut(app.OpenURL, ":")
 | 
							schema, _, _ := strings.Cut(app.OpenURL, ":")
 | 
				
			||||||
		var iconHTML template.HTML
 | 
							var iconHTML template.HTML
 | 
				
			||||||
		if schema == "vscode" || schema == "vscodium" || schema == "jetbrains" {
 | 
							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 {
 | 
							} 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{
 | 
							tmplApps = append(tmplApps, map[string]any{
 | 
				
			||||||
			"DisplayName": app.DisplayName,
 | 
								"DisplayName": app.DisplayName,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,15 +1,13 @@
 | 
				
			|||||||
<!-- there is always at least one button (by context/repo.go) -->
 | 
					<!-- there is always at least one button (guaranteed by context/repo.go) -->
 | 
				
			||||||
{{if $.CloneButtonShowHTTPS}}
 | 
					<div class="ui action small input clone-buttons-combo">
 | 
				
			||||||
	<button class="ui small button" id="repo-clone-https" data-link="{{$.CloneButtonOriginLink.HTTPS}}">
 | 
						{{if $.CloneButtonShowHTTPS}}
 | 
				
			||||||
		HTTPS
 | 
							<button class="ui small button repo-clone-https" data-link="{{$.CloneButtonOriginLink.HTTPS}}">HTTPS</button>
 | 
				
			||||||
	</button>
 | 
						{{end}}
 | 
				
			||||||
{{end}}
 | 
						{{if $.CloneButtonShowSSH}}
 | 
				
			||||||
{{if $.CloneButtonShowSSH}}
 | 
							<button class="ui small button repo-clone-ssh" data-link="{{$.CloneButtonOriginLink.SSH}}">SSH</button>
 | 
				
			||||||
	<button class="ui small button" id="repo-clone-ssh" data-link="{{$.CloneButtonOriginLink.SSH}}">
 | 
						{{end}}
 | 
				
			||||||
		SSH
 | 
						<input size="10" class="repo-clone-url js-clone-url" value="{{$.CloneButtonOriginLink.HTTPS}}" readonly>
 | 
				
			||||||
	</button>
 | 
						<button class="ui small icon button" data-clipboard-target=".repo-clone-url" data-tooltip-content="{{ctx.Locale.Tr "copy_url"}}">
 | 
				
			||||||
{{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}}
 | 
							{{svg "octicon-copy" 14}}
 | 
				
			||||||
</button>
 | 
						</button>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										44
									
								
								templates/repo/clone_panel.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								templates/repo/clone_panel.tmpl
									
									
									
									
									
										Normal file
									
								
							@@ -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>
 | 
				
			||||||
@@ -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>
 | 
					 | 
				
			||||||
@@ -37,11 +37,9 @@
 | 
				
			|||||||
									</a>
 | 
														</a>
 | 
				
			||||||
									{{end}}
 | 
														{{end}}
 | 
				
			||||||
								{{end}}
 | 
													{{end}}
 | 
				
			||||||
								<div class="clone-panel ui action small input tw-flex-1">
 | 
					 | 
				
			||||||
								{{template "repo/clone_buttons" .}}
 | 
													{{template "repo/clone_buttons" .}}
 | 
				
			||||||
							</div>
 | 
												</div>
 | 
				
			||||||
						</div>
 | 
											</div>
 | 
				
			||||||
						</div>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
						{{if not .Repository.IsArchived}}
 | 
											{{if not .Repository.IsArchived}}
 | 
				
			||||||
							<div class="divider tw-my-0"></div>
 | 
												<div class="divider tw-my-0"></div>
 | 
				
			||||||
@@ -73,7 +71,6 @@ git push -u origin {{.Repository.DefaultBranch}}</code></pre>
 | 
				
			|||||||
							{{ctx.Locale.Tr "repo.empty_message"}}
 | 
												{{ctx.Locale.Tr "repo.empty_message"}}
 | 
				
			||||||
						</div>
 | 
											</div>
 | 
				
			||||||
					{{end}}
 | 
										{{end}}
 | 
				
			||||||
					{{template "repo/clone_script" .}}
 | 
					 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -106,23 +106,7 @@
 | 
				
			|||||||
					<div class="repo-button-row-right {{if not $isTreePathRoot}}tw-flex-grow-0{{end}}">
 | 
										<div class="repo-button-row-right {{if not $isTreePathRoot}}tw-flex-grow-0{{end}}">
 | 
				
			||||||
						<!-- Only show clone panel in repository home page -->
 | 
											<!-- Only show clone panel in repository home page -->
 | 
				
			||||||
						{{if $isTreePathRoot}}
 | 
											{{if $isTreePathRoot}}
 | 
				
			||||||
							<div class="clone-panel ui action tiny input">
 | 
												{{template "repo/clone_panel" .}}
 | 
				
			||||||
								{{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>
 | 
					 | 
				
			||||||
						{{end}}
 | 
											{{end}}
 | 
				
			||||||
						{{if and (not $isTreePathRoot) (not .IsViewFile) (not .IsBlame)}}{{/* IsViewDirectory (not home), TODO: split the templates, avoid using "if" tricks */}}
 | 
											{{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}}">
 | 
												<a class="ui button" href="{{.RepoLink}}/commits/{{.BranchNameSubURL}}/{{.TreePath | PathEscapeSegments}}">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,10 +15,7 @@
 | 
				
			|||||||
				</div>
 | 
									</div>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
			<div class="ui eight wide column text right">
 | 
								<div class="ui eight wide column text right">
 | 
				
			||||||
				<div class="clone-panel ui action small input">
 | 
									{{template "repo/clone_panel" .}}
 | 
				
			||||||
					{{template "repo/clone_buttons" .}}
 | 
					 | 
				
			||||||
					{{template "repo/clone_script" .}}
 | 
					 | 
				
			||||||
				</div>
 | 
					 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
		<h2 class="ui top header">{{ctx.Locale.Tr "repo.wiki.wiki_page_revisions"}}</h2>
 | 
							<h2 class="ui top header">{{ctx.Locale.Tr "repo.wiki.wiki_page_revisions"}}</h2>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -28,10 +28,7 @@
 | 
				
			|||||||
					</div>
 | 
										</div>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
			<div class="clone-panel ui action small input">
 | 
								{{template "repo/clone_panel" .}}
 | 
				
			||||||
				{{template "repo/clone_buttons" .}}
 | 
					 | 
				
			||||||
				{{template "repo/clone_script" .}}
 | 
					 | 
				
			||||||
			</div>
 | 
					 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
		<div class="ui dividing header">
 | 
							<div class="ui dividing header">
 | 
				
			||||||
			<div class="flex-text-block tw-flex-wrap tw-justify-end">
 | 
								<div class="flex-text-block tw-flex-wrap tw-justify-end">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -127,10 +127,10 @@ func TestViewRepo1CloneLinkAnonymous(t *testing.T) {
 | 
				
			|||||||
	resp := MakeRequest(t, req, http.StatusOK)
 | 
						resp := MakeRequest(t, req, http.StatusOK)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	htmlDoc := NewHTMLParser(t, resp.Body)
 | 
						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.True(t, exists, "The template has changed")
 | 
				
			||||||
	assert.Equal(t, setting.AppURL+"user2/repo1.git", link)
 | 
						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)
 | 
						assert.False(t, exists)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -143,10 +143,10 @@ func TestViewRepo1CloneLinkAuthorized(t *testing.T) {
 | 
				
			|||||||
	resp := session.MakeRequest(t, req, http.StatusOK)
 | 
						resp := session.MakeRequest(t, req, http.StatusOK)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	htmlDoc := NewHTMLParser(t, resp.Body)
 | 
						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.True(t, exists, "The template has changed")
 | 
				
			||||||
	assert.Equal(t, setting.AppURL+"user2/repo1.git", link)
 | 
						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")
 | 
						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)
 | 
						sshURL := fmt.Sprintf("ssh://%s@%s:%d/user2/repo1.git", setting.SSH.User, setting.SSH.Domain, setting.SSH.Port)
 | 
				
			||||||
	assert.Equal(t, sshURL, link)
 | 
						assert.Equal(t, sshURL, link)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -67,6 +67,7 @@
 | 
				
			|||||||
@import "./repo/header.css";
 | 
					@import "./repo/header.css";
 | 
				
			||||||
@import "./repo/home.css";
 | 
					@import "./repo/home.css";
 | 
				
			||||||
@import "./repo/reactions.css";
 | 
					@import "./repo/reactions.css";
 | 
				
			||||||
 | 
					@import "./repo/clone.css";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@import "./editor/fileeditor.css";
 | 
					@import "./editor/fileeditor.css";
 | 
				
			||||||
@import "./editor/combomarkdowneditor.css";
 | 
					@import "./editor/combomarkdowneditor.css";
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -101,42 +101,6 @@
 | 
				
			|||||||
  margin-bottom: 12px;
 | 
					  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 {
 | 
					.repository .repo-description {
 | 
				
			||||||
  font-size: 16px;
 | 
					  font-size: 16px;
 | 
				
			||||||
  margin-bottom: 5px;
 | 
					  margin-bottom: 5px;
 | 
				
			||||||
@@ -1615,14 +1579,6 @@ td .commit-summary {
 | 
				
			|||||||
  font-weight: var(--font-weight-normal);
 | 
					  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 {
 | 
					.empty-placeholder {
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  flex-direction: column;
 | 
					  flex-direction: column;
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										32
									
								
								web_src/css/repo/clone.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								web_src/css/repo/clone.css
									
									
									
									
									
										Normal file
									
								
							@@ -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;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -59,9 +59,6 @@
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@media (max-width: 767.98px) {
 | 
					@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-main.with-sidebar,
 | 
				
			||||||
  .repository.wiki .wiki-content-sidebar {
 | 
					  .repository.wiki .wiki-content-sidebar {
 | 
				
			||||||
    float: none;
 | 
					    float: none;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,6 +5,8 @@ import {showErrorToast} from '../modules/toast.ts';
 | 
				
			|||||||
import {sleep} from '../utils.ts';
 | 
					import {sleep} from '../utils.ts';
 | 
				
			||||||
import RepoActivityTopAuthors from '../components/RepoActivityTopAuthors.vue';
 | 
					import RepoActivityTopAuthors from '../components/RepoActivityTopAuthors.vue';
 | 
				
			||||||
import {createApp} from 'vue';
 | 
					import {createApp} from 'vue';
 | 
				
			||||||
 | 
					import {toOriginUrl} from '../utils/url.ts';
 | 
				
			||||||
 | 
					import {createTippy} from '../modules/tippy.ts';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function onDownloadArchive(e) {
 | 
					async function onDownloadArchive(e) {
 | 
				
			||||||
  e.preventDefault();
 | 
					  e.preventDefault();
 | 
				
			||||||
@@ -41,27 +43,68 @@ export function initRepoActivityTopAuthorsChart() {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function initRepoCloneLink() {
 | 
					function initCloneSchemeUrlSelection(parent: Element) {
 | 
				
			||||||
  const $repoCloneSsh = $('#repo-clone-ssh');
 | 
					  const elCloneUrlInput = parent.querySelector<HTMLInputElement>('.repo-clone-url');
 | 
				
			||||||
  const $repoCloneHttps = $('#repo-clone-https');
 | 
					 | 
				
			||||||
  const $inputLink = $('#repo-clone-url');
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if ((!$repoCloneSsh.length && !$repoCloneHttps.length) || !$inputLink.length) {
 | 
					  const tabSsh = parent.querySelector('.repo-clone-ssh');
 | 
				
			||||||
    return;
 | 
					  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;
 | 
				
			||||||
    localStorage.setItem('repo-clone-protocol', 'ssh');
 | 
					    if (!tab) return;
 | 
				
			||||||
    window.updateCloneStates();
 | 
					    const link = toOriginUrl(tab.getAttribute('data-link'));
 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
  $repoCloneHttps.on('click', () => {
 | 
					 | 
				
			||||||
    localStorage.setItem('repo-clone-protocol', 'https');
 | 
					 | 
				
			||||||
    window.updateCloneStates();
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  $inputLink.on('focus', () => {
 | 
					    for (const el of document.querySelectorAll('.js-clone-url')) {
 | 
				
			||||||
    $inputLink.trigger('select');
 | 
					      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');
 | 
				
			||||||
 | 
					    updateClonePanelUi();
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					  tabHttps.addEventListener('click', () => {
 | 
				
			||||||
 | 
					    localStorage.setItem('repo-clone-protocol', 'https');
 | 
				
			||||||
 | 
					    updateClonePanelUi();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  elCloneUrlInput.addEventListener('focus', () => {
 | 
				
			||||||
 | 
					    elCloneUrlInput.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) {
 | 
					export function initRepoCommonBranchOrTagDropdown(selector: string) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,7 +9,7 @@ import {
 | 
				
			|||||||
import {initUnicodeEscapeButton} from './repo-unicode-escape.ts';
 | 
					import {initUnicodeEscapeButton} from './repo-unicode-escape.ts';
 | 
				
			||||||
import {initRepoBranchTagSelector} from '../components/RepoBranchTagSelector.vue';
 | 
					import {initRepoBranchTagSelector} from '../components/RepoBranchTagSelector.vue';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  initRepoCloneLink, initRepoCommonBranchOrTagDropdown, initRepoCommonFilterSearchDropdown,
 | 
					  initRepoCloneButtons, initRepoCommonBranchOrTagDropdown, initRepoCommonFilterSearchDropdown,
 | 
				
			||||||
} from './repo-common.ts';
 | 
					} from './repo-common.ts';
 | 
				
			||||||
import {initCitationFileCopyContent} from './citation.ts';
 | 
					import {initCitationFileCopyContent} from './citation.ts';
 | 
				
			||||||
import {initCompLabelEdit} from './comp/LabelEdit.ts';
 | 
					import {initCompLabelEdit} from './comp/LabelEdit.ts';
 | 
				
			||||||
@@ -54,7 +54,7 @@ export function initRepository() {
 | 
				
			|||||||
    initRepoCommonFilterSearchDropdown('.choose.branch .dropdown');
 | 
					    initRepoCommonFilterSearchDropdown('.choose.branch .dropdown');
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  initRepoCloneLink();
 | 
					  initRepoCloneButtons();
 | 
				
			||||||
  initCitationFileCopyContent();
 | 
					  initCitationFileCopyContent();
 | 
				
			||||||
  initRepoSettings();
 | 
					  initRepoSettings();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
import {pathEscapeSegments, isUrl} from './url.ts';
 | 
					import {pathEscapeSegments, isUrl, toOriginUrl} from './url.ts';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('pathEscapeSegments', () => {
 | 
					test('pathEscapeSegments', () => {
 | 
				
			||||||
  expect(pathEscapeSegments('a/b/c')).toEqual('a/b/c');
 | 
					  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('https://example.com/index.html')).toEqual(true);
 | 
				
			||||||
  expect(isUrl('/index.html')).toEqual(false);
 | 
					  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);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,3 +13,19 @@ export function isUrl(url: string): boolean {
 | 
				
			|||||||
    return false;
 | 
					    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;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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);
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
@@ -1,19 +1,4 @@
 | 
				
			|||||||
// Convert an absolute or relative URL to an absolute URL with the current origin. It only
 | 
					import {toOriginUrl} from '../utils/url.ts';
 | 
				
			||||||
// 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;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
window.customElements.define('origin-url', class extends HTMLElement {
 | 
					window.customElements.define('origin-url', class extends HTMLElement {
 | 
				
			||||||
  connectedCallback() {
 | 
					  connectedCallback() {
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user