mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 21:28:11 +09:00 
			
		
		
		
	support the open-icon of folder (#34168)
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
		| @@ -6,22 +6,26 @@ package fileicon | ||||
| import ( | ||||
| 	"html/template" | ||||
|  | ||||
| 	"code.gitea.io/gitea/modules/git" | ||||
| 	"code.gitea.io/gitea/modules/svg" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| ) | ||||
|  | ||||
| func BasicThemeIcon(entry *git.TreeEntry) template.HTML { | ||||
| func BasicEntryIconName(entry *EntryInfo) string { | ||||
| 	svgName := "octicon-file" | ||||
| 	switch { | ||||
| 	case entry.IsLink(): | ||||
| 	case entry.EntryMode.IsLink(): | ||||
| 		svgName = "octicon-file-symlink-file" | ||||
| 		if te, err := entry.FollowLink(); err == nil && te.IsDir() { | ||||
| 		if entry.SymlinkToMode.IsDir() { | ||||
| 			svgName = "octicon-file-directory-symlink" | ||||
| 		} | ||||
| 	case entry.IsDir(): | ||||
| 		svgName = "octicon-file-directory-fill" | ||||
| 	case entry.IsSubModule(): | ||||
| 	case entry.EntryMode.IsDir(): | ||||
| 		svgName = util.Iif(entry.IsOpen, "octicon-file-directory-open-fill", "octicon-file-directory-fill") | ||||
| 	case entry.EntryMode.IsSubModule(): | ||||
| 		svgName = "octicon-file-submodule" | ||||
| 	} | ||||
| 	return svg.RenderHTML(svgName) | ||||
| 	return svgName | ||||
| } | ||||
|  | ||||
| func BasicEntryIconHTML(entry *EntryInfo) template.HTML { | ||||
| 	return svg.RenderHTML(BasicEntryIconName(entry)) | ||||
| } | ||||
|   | ||||
							
								
								
									
										31
									
								
								modules/fileicon/entry.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								modules/fileicon/entry.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| // Copyright 2025 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
|  | ||||
| package fileicon | ||||
|  | ||||
| import "code.gitea.io/gitea/modules/git" | ||||
|  | ||||
| type EntryInfo struct { | ||||
| 	FullName      string | ||||
| 	EntryMode     git.EntryMode | ||||
| 	SymlinkToMode git.EntryMode | ||||
| 	IsOpen        bool | ||||
| } | ||||
|  | ||||
| func EntryInfoFromGitTreeEntry(gitEntry *git.TreeEntry) *EntryInfo { | ||||
| 	ret := &EntryInfo{FullName: gitEntry.Name(), EntryMode: gitEntry.Mode()} | ||||
| 	if gitEntry.IsLink() { | ||||
| 		if te, err := gitEntry.FollowLink(); err == nil && te.IsDir() { | ||||
| 			ret.SymlinkToMode = te.Mode() | ||||
| 		} | ||||
| 	} | ||||
| 	return ret | ||||
| } | ||||
|  | ||||
| func EntryInfoFolder() *EntryInfo { | ||||
| 	return &EntryInfo{EntryMode: git.EntryModeTree} | ||||
| } | ||||
|  | ||||
| func EntryInfoFolderOpen() *EntryInfo { | ||||
| 	return &EntryInfo{EntryMode: git.EntryModeTree, IsOpen: true} | ||||
| } | ||||
| @@ -9,11 +9,12 @@ import ( | ||||
| 	"strings" | ||||
| 	"sync" | ||||
|  | ||||
| 	"code.gitea.io/gitea/modules/git" | ||||
| 	"code.gitea.io/gitea/modules/json" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/options" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/svg" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| ) | ||||
|  | ||||
| type materialIconRulesData struct { | ||||
| @@ -69,42 +70,52 @@ func (m *MaterialIconProvider) renderFileIconSVG(p *RenderedIconPool, name, svg, | ||||
| 	} | ||||
| 	svgID := "svg-mfi-" + name | ||||
| 	svgCommonAttrs := `class="svg git-entry-icon ` + extraClass + `" width="16" height="16" aria-hidden="true"` | ||||
| 	svgHTML := template.HTML(`<svg id="` + svgID + `" ` + svgCommonAttrs + svg[4:]) | ||||
| 	if p == nil { | ||||
| 		return svgHTML | ||||
| 	} | ||||
| 	if p.IconSVGs[svgID] == "" { | ||||
| 		p.IconSVGs[svgID] = template.HTML(`<svg id="` + svgID + `" ` + svgCommonAttrs + svg[4:]) | ||||
| 		p.IconSVGs[svgID] = svgHTML | ||||
| 	} | ||||
| 	return template.HTML(`<svg ` + svgCommonAttrs + `><use xlink:href="#` + svgID + `"></use></svg>`) | ||||
| } | ||||
|  | ||||
| func (m *MaterialIconProvider) FileIcon(p *RenderedIconPool, entry *git.TreeEntry) template.HTML { | ||||
| func (m *MaterialIconProvider) EntryIconHTML(p *RenderedIconPool, entry *EntryInfo) template.HTML { | ||||
| 	if m.rules == nil { | ||||
| 		return BasicThemeIcon(entry) | ||||
| 		return BasicEntryIconHTML(entry) | ||||
| 	} | ||||
|  | ||||
| 	if entry.IsLink() { | ||||
| 		if te, err := entry.FollowLink(); err == nil && te.IsDir() { | ||||
| 	if entry.EntryMode.IsLink() { | ||||
| 		if entry.SymlinkToMode.IsDir() { | ||||
| 			// keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work | ||||
| 			return svg.RenderHTML("material-folder-symlink", 16, "octicon-file-directory-symlink") | ||||
| 		} | ||||
| 		return svg.RenderHTML("octicon-file-symlink-file") // TODO: find some better icons for them | ||||
| 	} | ||||
|  | ||||
| 	name := m.findIconNameByGit(entry) | ||||
| 	// the material icon pack's "folder" icon doesn't look good, so use our built-in one | ||||
| 	// keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work | ||||
| 	if iconSVG, ok := m.svgs[name]; ok && name != "folder" && iconSVG != "" { | ||||
| 	name := m.FindIconName(entry) | ||||
| 	iconSVG := m.svgs[name] | ||||
| 	if iconSVG == "" { | ||||
| 		name = "file" | ||||
| 		if entry.EntryMode.IsDir() { | ||||
| 			name = util.Iif(entry.IsOpen, "folder-open", "folder") | ||||
| 		} | ||||
| 		iconSVG = m.svgs[name] | ||||
| 		if iconSVG == "" { | ||||
| 			setting.PanicInDevOrTesting("missing file icon for %s", name) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work | ||||
| 	extraClass := "octicon-file" | ||||
| 	switch { | ||||
| 		case entry.IsDir(): | ||||
| 			extraClass = "octicon-file-directory-fill" | ||||
| 		case entry.IsSubModule(): | ||||
| 	case entry.EntryMode.IsDir(): | ||||
| 		extraClass = BasicEntryIconName(entry) | ||||
| 	case entry.EntryMode.IsSubModule(): | ||||
| 		extraClass = "octicon-file-submodule" | ||||
| 	} | ||||
| 	return m.renderFileIconSVG(p, name, iconSVG, extraClass) | ||||
| } | ||||
| 	// TODO: use an interface or wrapper for git.Entry to make the code testable. | ||||
| 	return BasicThemeIcon(entry) | ||||
| } | ||||
|  | ||||
| func (m *MaterialIconProvider) findIconNameWithLangID(s string) string { | ||||
| 	if _, ok := m.svgs[s]; ok { | ||||
| @@ -118,13 +129,17 @@ func (m *MaterialIconProvider) findIconNameWithLangID(s string) string { | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| func (m *MaterialIconProvider) FindIconName(name string, isDir bool) string { | ||||
| 	fileNameLower := strings.ToLower(path.Base(name)) | ||||
| 	if isDir { | ||||
| func (m *MaterialIconProvider) FindIconName(entry *EntryInfo) string { | ||||
| 	if entry.EntryMode.IsSubModule() { | ||||
| 		return "folder-git" | ||||
| 	} | ||||
|  | ||||
| 	fileNameLower := strings.ToLower(path.Base(entry.FullName)) | ||||
| 	if entry.EntryMode.IsDir() { | ||||
| 		if s, ok := m.rules.FolderNames[fileNameLower]; ok { | ||||
| 			return s | ||||
| 		} | ||||
| 		return "folder" | ||||
| 		return util.Iif(entry.IsOpen, "folder-open", "folder") | ||||
| 	} | ||||
|  | ||||
| 	if s, ok := m.rules.FileNames[fileNameLower]; ok { | ||||
| @@ -146,10 +161,3 @@ func (m *MaterialIconProvider) FindIconName(name string, isDir bool) string { | ||||
|  | ||||
| 	return "file" | ||||
| } | ||||
|  | ||||
| func (m *MaterialIconProvider) findIconNameByGit(entry *git.TreeEntry) string { | ||||
| 	if entry.IsSubModule() { | ||||
| 		return "folder-git" | ||||
| 	} | ||||
| 	return m.FindIconName(entry.Name(), entry.IsDir()) | ||||
| } | ||||
|   | ||||
| @@ -8,6 +8,7 @@ import ( | ||||
|  | ||||
| 	"code.gitea.io/gitea/models/unittest" | ||||
| 	"code.gitea.io/gitea/modules/fileicon" | ||||
| 	"code.gitea.io/gitea/modules/git" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
| @@ -19,8 +20,8 @@ func TestMain(m *testing.M) { | ||||
| func TestFindIconName(t *testing.T) { | ||||
| 	unittest.PrepareTestEnv(t) | ||||
| 	p := fileicon.DefaultMaterialIconProvider() | ||||
| 	assert.Equal(t, "php", p.FindIconName("foo.php", false)) | ||||
| 	assert.Equal(t, "php", p.FindIconName("foo.PHP", false)) | ||||
| 	assert.Equal(t, "javascript", p.FindIconName("foo.js", false)) | ||||
| 	assert.Equal(t, "visualstudio", p.FindIconName("foo.vba", false)) | ||||
| 	assert.Equal(t, "php", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.php", EntryMode: git.EntryModeBlob})) | ||||
| 	assert.Equal(t, "php", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.PHP", EntryMode: git.EntryModeBlob})) | ||||
| 	assert.Equal(t, "javascript", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.js", EntryMode: git.EntryModeBlob})) | ||||
| 	assert.Equal(t, "visualstudio", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.vba", EntryMode: git.EntryModeBlob})) | ||||
| } | ||||
|   | ||||
| @@ -7,7 +7,6 @@ import ( | ||||
| 	"html/template" | ||||
| 	"strings" | ||||
|  | ||||
| 	"code.gitea.io/gitea/modules/git" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| ) | ||||
|  | ||||
| @@ -34,19 +33,9 @@ func (p *RenderedIconPool) RenderToHTML() template.HTML { | ||||
| 	return template.HTML(sb.String()) | ||||
| } | ||||
|  | ||||
| // TODO: use an interface or struct to replace "*git.TreeEntry", to decouple the fileicon module from git module | ||||
|  | ||||
| func RenderEntryIcon(renderedIconPool *RenderedIconPool, entry *git.TreeEntry) template.HTML { | ||||
| func RenderEntryIconHTML(renderedIconPool *RenderedIconPool, entry *EntryInfo) template.HTML { | ||||
| 	if setting.UI.FileIconTheme == "material" { | ||||
| 		return DefaultMaterialIconProvider().FileIcon(renderedIconPool, entry) | ||||
| 		return DefaultMaterialIconProvider().EntryIconHTML(renderedIconPool, entry) | ||||
| 	} | ||||
| 	return BasicThemeIcon(entry) | ||||
| } | ||||
|  | ||||
| func RenderEntryIconOpen(renderedIconPool *RenderedIconPool, entry *git.TreeEntry) template.HTML { | ||||
| 	// TODO: add "open icon" support | ||||
| 	if setting.UI.FileIconTheme == "material" { | ||||
| 		return DefaultMaterialIconProvider().FileIcon(renderedIconPool, entry) | ||||
| 	} | ||||
| 	return BasicThemeIcon(entry) | ||||
| 	return BasicEntryIconHTML(entry) | ||||
| } | ||||
|   | ||||
| @@ -30,6 +30,31 @@ func (e EntryMode) String() string { | ||||
| 	return strconv.FormatInt(int64(e), 8) | ||||
| } | ||||
|  | ||||
| // IsSubModule if the entry is a sub module | ||||
| func (e EntryMode) IsSubModule() bool { | ||||
| 	return e == EntryModeCommit | ||||
| } | ||||
|  | ||||
| // IsDir if the entry is a sub dir | ||||
| func (e EntryMode) IsDir() bool { | ||||
| 	return e == EntryModeTree | ||||
| } | ||||
|  | ||||
| // IsLink if the entry is a symlink | ||||
| func (e EntryMode) IsLink() bool { | ||||
| 	return e == EntryModeSymlink | ||||
| } | ||||
|  | ||||
| // IsRegular if the entry is a regular file | ||||
| func (e EntryMode) IsRegular() bool { | ||||
| 	return e == EntryModeBlob | ||||
| } | ||||
|  | ||||
| // IsExecutable if the entry is an executable file (not necessarily binary) | ||||
| func (e EntryMode) IsExecutable() bool { | ||||
| 	return e == EntryModeExec | ||||
| } | ||||
|  | ||||
| func ParseEntryMode(mode string) (EntryMode, error) { | ||||
| 	switch mode { | ||||
| 	case "000000": | ||||
|   | ||||
| @@ -59,27 +59,27 @@ func (te *TreeEntry) Size() int64 { | ||||
|  | ||||
| // IsSubModule if the entry is a sub module | ||||
| func (te *TreeEntry) IsSubModule() bool { | ||||
| 	return te.entryMode == EntryModeCommit | ||||
| 	return te.entryMode.IsSubModule() | ||||
| } | ||||
|  | ||||
| // IsDir if the entry is a sub dir | ||||
| func (te *TreeEntry) IsDir() bool { | ||||
| 	return te.entryMode == EntryModeTree | ||||
| 	return te.entryMode.IsDir() | ||||
| } | ||||
|  | ||||
| // IsLink if the entry is a symlink | ||||
| func (te *TreeEntry) IsLink() bool { | ||||
| 	return te.entryMode == EntryModeSymlink | ||||
| 	return te.entryMode.IsLink() | ||||
| } | ||||
|  | ||||
| // IsRegular if the entry is a regular file | ||||
| func (te *TreeEntry) IsRegular() bool { | ||||
| 	return te.entryMode == EntryModeBlob | ||||
| 	return te.entryMode.IsRegular() | ||||
| } | ||||
|  | ||||
| // IsExecutable if the entry is an executable file (not necessarily binary) | ||||
| func (te *TreeEntry) IsExecutable() bool { | ||||
| 	return te.entryMode == EntryModeExec | ||||
| 	return te.entryMode.IsExecutable() | ||||
| } | ||||
|  | ||||
| // Blob returns the blob object the entry | ||||
|   | ||||
| @@ -21,6 +21,7 @@ import ( | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	"code.gitea.io/gitea/modules/base" | ||||
| 	"code.gitea.io/gitea/modules/charset" | ||||
| 	"code.gitea.io/gitea/modules/fileicon" | ||||
| 	"code.gitea.io/gitea/modules/git" | ||||
| 	"code.gitea.io/gitea/modules/gitrepo" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| @@ -369,7 +370,11 @@ func Diff(ctx *context.Context) { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(diffTree, nil) | ||||
| 		renderedIconPool := fileicon.NewRenderedIconPool() | ||||
| 		ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(renderedIconPool, diffTree, nil) | ||||
| 		ctx.PageData["FolderIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolder()) | ||||
| 		ctx.PageData["FolderOpenIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolderOpen()) | ||||
| 		ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML() | ||||
| 	} | ||||
|  | ||||
| 	statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, commitID, db.ListOptionsAll) | ||||
|   | ||||
| @@ -26,6 +26,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/base" | ||||
| 	"code.gitea.io/gitea/modules/charset" | ||||
| 	csv_module "code.gitea.io/gitea/modules/csv" | ||||
| 	"code.gitea.io/gitea/modules/fileicon" | ||||
| 	"code.gitea.io/gitea/modules/git" | ||||
| 	"code.gitea.io/gitea/modules/gitrepo" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| @@ -639,7 +640,11 @@ func PrepareCompareDiff( | ||||
| 			return false | ||||
| 		} | ||||
|  | ||||
| 		ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(diffTree, nil) | ||||
| 		renderedIconPool := fileicon.NewRenderedIconPool() | ||||
| 		ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(renderedIconPool, diffTree, nil) | ||||
| 		ctx.PageData["FolderIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolder()) | ||||
| 		ctx.PageData["FolderOpenIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolderOpen()) | ||||
| 		ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML() | ||||
| 	} | ||||
|  | ||||
| 	headCommit, err := ci.HeadGitRepo.GetCommit(headCommitID) | ||||
|   | ||||
| @@ -24,6 +24,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/models/unit" | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	"code.gitea.io/gitea/modules/emoji" | ||||
| 	"code.gitea.io/gitea/modules/fileicon" | ||||
| 	"code.gitea.io/gitea/modules/git" | ||||
| 	"code.gitea.io/gitea/modules/gitrepo" | ||||
| 	issue_template "code.gitea.io/gitea/modules/issue/template" | ||||
| @@ -823,7 +824,12 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi | ||||
| 		if reviewState != nil { | ||||
| 			filesViewedState = reviewState.UpdatedFiles | ||||
| 		} | ||||
| 		ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(diffTree, filesViewedState) | ||||
|  | ||||
| 		renderedIconPool := fileicon.NewRenderedIconPool() | ||||
| 		ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(renderedIconPool, diffTree, filesViewedState) | ||||
| 		ctx.PageData["FolderIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolder()) | ||||
| 		ctx.PageData["FolderOpenIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolderOpen()) | ||||
| 		ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML() | ||||
| 	} | ||||
|  | ||||
| 	ctx.Data["Diff"] = diff | ||||
|   | ||||
| @@ -4,6 +4,7 @@ | ||||
| package repo | ||||
|  | ||||
| import ( | ||||
| 	"html/template" | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
|  | ||||
| @@ -67,7 +68,7 @@ type WebDiffFileItem struct { | ||||
| 	EntryMode   string | ||||
| 	IsViewed    bool | ||||
| 	Children    []*WebDiffFileItem | ||||
| 	// TODO: add icon support in the future | ||||
| 	FileIcon    template.HTML | ||||
| } | ||||
|  | ||||
| // WebDiffFileTree is used by frontend, check the field names in frontend before changing | ||||
| @@ -77,7 +78,7 @@ type WebDiffFileTree struct { | ||||
|  | ||||
| // transformDiffTreeForWeb transforms a gitdiff.DiffTree into a WebDiffFileTree for Web UI rendering | ||||
| // it also takes a map of file names to their viewed state, which is used to mark files as viewed | ||||
| func transformDiffTreeForWeb(diffTree *gitdiff.DiffTree, filesViewedState map[string]pull_model.ViewedState) (dft WebDiffFileTree) { | ||||
| func transformDiffTreeForWeb(renderedIconPool *fileicon.RenderedIconPool, diffTree *gitdiff.DiffTree, filesViewedState map[string]pull_model.ViewedState) (dft WebDiffFileTree) { | ||||
| 	dirNodes := map[string]*WebDiffFileItem{"": &dft.TreeRoot} | ||||
| 	addItem := func(item *WebDiffFileItem) { | ||||
| 		var parentPath string | ||||
| @@ -110,6 +111,7 @@ func transformDiffTreeForWeb(diffTree *gitdiff.DiffTree, filesViewedState map[st | ||||
| 		item := &WebDiffFileItem{FullName: file.HeadPath, DiffStatus: file.Status} | ||||
| 		item.IsViewed = filesViewedState[item.FullName] == pull_model.Viewed | ||||
| 		item.NameHash = git.HashFilePathForWebUI(item.FullName) | ||||
| 		item.FileIcon = fileicon.RenderEntryIconHTML(renderedIconPool, &fileicon.EntryInfo{FullName: file.HeadPath, EntryMode: file.HeadMode}) | ||||
|  | ||||
| 		switch file.HeadMode { | ||||
| 		case git.EntryModeTree: | ||||
|   | ||||
| @@ -4,9 +4,11 @@ | ||||
| package repo | ||||
|  | ||||
| import ( | ||||
| 	"html/template" | ||||
| 	"testing" | ||||
|  | ||||
| 	pull_model "code.gitea.io/gitea/models/pull" | ||||
| 	"code.gitea.io/gitea/modules/fileicon" | ||||
| 	"code.gitea.io/gitea/modules/git" | ||||
| 	"code.gitea.io/gitea/services/gitdiff" | ||||
|  | ||||
| @@ -14,7 +16,8 @@ import ( | ||||
| ) | ||||
|  | ||||
| func TestTransformDiffTreeForWeb(t *testing.T) { | ||||
| 	ret := transformDiffTreeForWeb(&gitdiff.DiffTree{Files: []*gitdiff.DiffTreeRecord{ | ||||
| 	renderedIconPool := fileicon.NewRenderedIconPool() | ||||
| 	ret := transformDiffTreeForWeb(renderedIconPool, &gitdiff.DiffTree{Files: []*gitdiff.DiffTreeRecord{ | ||||
| 		{ | ||||
| 			Status:   "changed", | ||||
| 			HeadPath: "dir-a/dir-a-x/file-deep", | ||||
| @@ -29,6 +32,9 @@ func TestTransformDiffTreeForWeb(t *testing.T) { | ||||
| 		"dir-a/dir-a-x/file-deep": pull_model.Viewed, | ||||
| 	}) | ||||
|  | ||||
| 	mockIconForFile := func(id string) template.HTML { | ||||
| 		return template.HTML(`<svg class="svg git-entry-icon octicon-file" width="16" height="16" aria-hidden="true"><use xlink:href="#` + id + `"></use></svg>`) | ||||
| 	} | ||||
| 	assert.Equal(t, WebDiffFileTree{ | ||||
| 		TreeRoot: WebDiffFileItem{ | ||||
| 			Children: []*WebDiffFileItem{ | ||||
| @@ -44,6 +50,7 @@ func TestTransformDiffTreeForWeb(t *testing.T) { | ||||
| 							NameHash:    "4acf7eef1c943a09e9f754e93ff190db8583236b", | ||||
| 							DiffStatus:  "changed", | ||||
| 							IsViewed:    true, | ||||
| 							FileIcon:    mockIconForFile(`svg-mfi-file`), | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| @@ -53,6 +60,7 @@ func TestTransformDiffTreeForWeb(t *testing.T) { | ||||
| 					FullName:    "file1", | ||||
| 					NameHash:    "60b27f004e454aca81b0480209cce5081ec52390", | ||||
| 					DiffStatus:  "added", | ||||
| 					FileIcon:    mockIconForFile(`svg-mfi-file`), | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
|   | ||||
| @@ -257,8 +257,9 @@ func prepareDirectoryFileIcons(ctx *context.Context, files []git.CommitInfo) { | ||||
| 	renderedIconPool := fileicon.NewRenderedIconPool() | ||||
| 	fileIcons := map[string]template.HTML{} | ||||
| 	for _, f := range files { | ||||
| 		fileIcons[f.Entry.Name()] = fileicon.RenderEntryIcon(renderedIconPool, f.Entry) | ||||
| 		fileIcons[f.Entry.Name()] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFromGitTreeEntry(f.Entry)) | ||||
| 	} | ||||
| 	fileIcons[".."] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolder()) | ||||
| 	ctx.Data["FileIcons"] = fileIcons | ||||
| 	ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML() | ||||
| } | ||||
|   | ||||
| @@ -165,19 +165,11 @@ func newTreeViewNodeFromEntry(ctx context.Context, renderedIconPool *fileicon.Re | ||||
| 		FullPath:  path.Join(parentDir, entry.Name()), | ||||
| 	} | ||||
|  | ||||
| 	if entry.IsLink() { | ||||
| 		// TODO: symlink to a folder or a file, the icon differs | ||||
| 		target, err := entry.FollowLink() | ||||
| 		if err == nil { | ||||
| 			_ = target.IsDir() | ||||
| 			// if target.IsDir() { } else { } | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if node.EntryIcon == "" { | ||||
| 		node.EntryIcon = fileicon.RenderEntryIcon(renderedIconPool, entry) | ||||
| 		// TODO: no open icon support yet | ||||
| 		// node.EntryIconOpen = fileicon.RenderEntryIconOpen(renderedIconPool, entry) | ||||
| 	entryInfo := fileicon.EntryInfoFromGitTreeEntry(entry) | ||||
| 	node.EntryIcon = fileicon.RenderEntryIconHTML(renderedIconPool, entryInfo) | ||||
| 	if entryInfo.EntryMode.IsDir() { | ||||
| 		entryInfo.IsOpen = true | ||||
| 		node.EntryIconOpen = fileicon.RenderEntryIconHTML(renderedIconPool, entryInfo) | ||||
| 	} | ||||
|  | ||||
| 	if node.EntryMode == "commit" { | ||||
|   | ||||
| @@ -71,6 +71,9 @@ func TestGetTreeViewNodes(t *testing.T) { | ||||
| 	mockIconForFolder := func(id string) template.HTML { | ||||
| 		return template.HTML(`<svg class="svg git-entry-icon octicon-file-directory-fill" width="16" height="16" aria-hidden="true"><use xlink:href="#` + id + `"></use></svg>`) | ||||
| 	} | ||||
| 	mockOpenIconForFolder := func(id string) template.HTML { | ||||
| 		return template.HTML(`<svg class="svg git-entry-icon octicon-file-directory-open-fill" width="16" height="16" aria-hidden="true"><use xlink:href="#` + id + `"></use></svg>`) | ||||
| 	} | ||||
| 	treeNodes, err := GetTreeViewNodes(ctx, renderedIconPool, ctx.Repo.Commit, "", "") | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, []*TreeViewNode{ | ||||
| @@ -79,6 +82,7 @@ func TestGetTreeViewNodes(t *testing.T) { | ||||
| 			EntryMode:     "tree", | ||||
| 			FullPath:      "docs", | ||||
| 			EntryIcon:     mockIconForFolder(`svg-mfi-folder-docs`), | ||||
| 			EntryIconOpen: mockOpenIconForFolder(`svg-mfi-folder-docs`), | ||||
| 		}, | ||||
| 	}, treeNodes) | ||||
|  | ||||
| @@ -90,6 +94,7 @@ func TestGetTreeViewNodes(t *testing.T) { | ||||
| 			EntryMode:     "tree", | ||||
| 			FullPath:      "docs", | ||||
| 			EntryIcon:     mockIconForFolder(`svg-mfi-folder-docs`), | ||||
| 			EntryIconOpen: mockOpenIconForFolder(`svg-mfi-folder-docs`), | ||||
| 			Children: []*TreeViewNode{ | ||||
| 				{ | ||||
| 					EntryName: "README.md", | ||||
|   | ||||
| @@ -60,6 +60,7 @@ | ||||
| 	{{end}} | ||||
| 	<div id="diff-container"> | ||||
| 		{{if $showFileTree}} | ||||
| 			{{$.FileIconPoolHTML}} | ||||
| 			<div id="diff-file-tree" class="tw-hidden not-mobile"></div> | ||||
| 			<script> | ||||
| 				if (diffTreeVisible) document.getElementById('diff-file-tree').classList.remove('tw-hidden'); | ||||
|   | ||||
| @@ -4,12 +4,12 @@ | ||||
| 		{{template "repo/latest_commit" .}} | ||||
| 		<div>{{if and .LatestCommit .LatestCommit.Committer}}{{DateUtils.TimeSince .LatestCommit.Committer.When}}{{end}}</div> | ||||
| 	</div> | ||||
| 	{{$.FileIconPoolHTML}} | ||||
| 	{{if .HasParentPath}} | ||||
| 	<a class="repo-file-line parent-link silenced" href="{{.BranchLink}}{{if .ParentPath}}{{PathEscapeSegments .ParentPath}}{{end}}"> | ||||
| 		{{svg "octicon-file-directory-fill"}} .. | ||||
| 		{{index $.FileIcons ".."}} .. | ||||
| 	</a> | ||||
| 	{{end}} | ||||
| 	{{$.FileIconPoolHTML}} | ||||
| 	{{range $item := .Files}} | ||||
| 		<div class="repo-file-item"> | ||||
| 			{{$entry := $item.Entry}} | ||||
|   | ||||
| @@ -22,13 +22,6 @@ function getIconForDiffStatus(pType: DiffStatus) { | ||||
|   }; | ||||
|   return diffTypes[pType] ?? diffTypes['']; | ||||
| } | ||||
|  | ||||
| function entryIcon(entry: DiffTreeEntry) { | ||||
|   if (entry.EntryMode === 'commit') { | ||||
|     return 'octicon-file-submodule'; | ||||
|   } | ||||
|   return 'octicon-file'; | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
| @@ -36,10 +29,8 @@ function entryIcon(entry: DiffTreeEntry) { | ||||
|     <div class="item-directory" :class="{ 'viewed': item.IsViewed }" :title="item.DisplayName" @click.stop="collapsed = !collapsed"> | ||||
|       <!-- directory --> | ||||
|       <SvgIcon :name="collapsed ? 'octicon-chevron-right' : 'octicon-chevron-down'"/> | ||||
|       <SvgIcon | ||||
|         class="text primary" | ||||
|         :name="collapsed ? 'octicon-file-directory-fill' : 'octicon-file-directory-open-fill'" | ||||
|       /> | ||||
|       <!-- eslint-disable-next-line vue/no-v-html --> | ||||
|       <span class="tw-contents" v-html="collapsed ? store.folderIcon : store.folderOpenIcon"/> | ||||
|       <span class="gt-ellipsis">{{ item.DisplayName }}</span> | ||||
|     </div> | ||||
|  | ||||
| @@ -53,7 +44,8 @@ function entryIcon(entry: DiffTreeEntry) { | ||||
|     :title="item.DisplayName" :href="'#diff-' + item.NameHash" | ||||
|   > | ||||
|     <!-- file --> | ||||
|     <SvgIcon :name="entryIcon(item)"/> | ||||
|     <!-- eslint-disable-next-line vue/no-v-html --> | ||||
|     <span class="tw-contents" v-html="item.FileIcon"/> | ||||
|     <span class="gt-ellipsis tw-flex-1">{{ item.DisplayName }}</span> | ||||
|     <SvgIcon | ||||
|       :name="getIconForDiffStatus(item.DiffStatus).name" | ||||
|   | ||||
| @@ -9,6 +9,7 @@ test('diff-tree', () => { | ||||
|       'IsViewed': false, | ||||
|       'NameHash': '....', | ||||
|       'DiffStatus': '', | ||||
|       'FileIcon': '', | ||||
|       'Children': [ | ||||
|         { | ||||
|           'FullName': 'dir1', | ||||
| @@ -17,6 +18,7 @@ test('diff-tree', () => { | ||||
|           'IsViewed': false, | ||||
|           'NameHash': '....', | ||||
|           'DiffStatus': '', | ||||
|           'FileIcon': '', | ||||
|           'Children': [ | ||||
|             { | ||||
|               'FullName': 'dir1/test.txt', | ||||
| @@ -25,6 +27,7 @@ test('diff-tree', () => { | ||||
|               'NameHash': '....', | ||||
|               'EntryMode': '', | ||||
|               'IsViewed': false, | ||||
|               'FileIcon': '', | ||||
|               'Children': null, | ||||
|             }, | ||||
|           ], | ||||
| @@ -36,11 +39,12 @@ test('diff-tree', () => { | ||||
|           'DiffStatus': 'added', | ||||
|           'EntryMode': '', | ||||
|           'IsViewed': false, | ||||
|           'FileIcon': '', | ||||
|           'Children': null, | ||||
|         }, | ||||
|       ], | ||||
|     }, | ||||
|   }); | ||||
|   }, '', ''); | ||||
|   diffTreeStoreSetViewed(store, 'dir1/test.txt', true); | ||||
|   expect(store.fullNameMap['dir1/test.txt'].IsViewed).toBe(true); | ||||
|   expect(store.fullNameMap['dir1'].IsViewed).toBe(true); | ||||
|   | ||||
| @@ -13,7 +13,7 @@ export type DiffTreeEntry = { | ||||
|   EntryMode: string, | ||||
|   IsViewed: boolean, | ||||
|   Children: DiffTreeEntry[], | ||||
|  | ||||
|   FileIcon: string, | ||||
|   ParentEntry?: DiffTreeEntry, | ||||
| } | ||||
|  | ||||
| @@ -22,6 +22,8 @@ type DiffFileTreeData = { | ||||
| }; | ||||
|  | ||||
| type DiffFileTree = { | ||||
|   folderIcon: string; | ||||
|   folderOpenIcon: string; | ||||
|   diffFileTree: DiffFileTreeData; | ||||
|   fullNameMap?: Record<string, DiffTreeEntry> | ||||
|   fileTreeIsVisible: boolean; | ||||
| @@ -31,7 +33,7 @@ type DiffFileTree = { | ||||
| let diffTreeStoreReactive: Reactive<DiffFileTree>; | ||||
| export function diffTreeStore() { | ||||
|   if (!diffTreeStoreReactive) { | ||||
|     diffTreeStoreReactive = reactiveDiffTreeStore(pageData.DiffFileTree); | ||||
|     diffTreeStoreReactive = reactiveDiffTreeStore(pageData.DiffFileTree, pageData.FolderIcon, pageData.FolderOpenIcon); | ||||
|   } | ||||
|   return diffTreeStoreReactive; | ||||
| } | ||||
| @@ -55,9 +57,11 @@ function fillFullNameMap(map: Record<string, DiffTreeEntry>, entry: DiffTreeEntr | ||||
|   } | ||||
| } | ||||
|  | ||||
| export function reactiveDiffTreeStore(data: DiffFileTreeData): Reactive<DiffFileTree> { | ||||
| export function reactiveDiffTreeStore(data: DiffFileTreeData, folderIcon: string, folderOpenIcon: string): Reactive<DiffFileTree> { | ||||
|   const store = reactive({ | ||||
|     diffFileTree: data, | ||||
|     folderIcon, | ||||
|     folderOpenIcon, | ||||
|     fileTreeIsVisible: false, | ||||
|     selectedItem: '', | ||||
|     fullNameMap: {}, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user