0
0
mirror of https://github.com/go-gitea/gitea.git synced 2025-05-18 00:49:09 -04:00

Merge 5da1c01840a5de4550922aa835d281d6b64b79a5 into dd0caf7e163bff3ecd951a045d9cea47efaa7ed5

This commit is contained in:
Kerwin Bryant 2025-04-17 09:43:35 -03:00 committed by GitHub
commit aa72c7bd73
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 201 additions and 95 deletions

View File

@ -10,17 +10,51 @@ import (
"code.gitea.io/gitea/modules/svg" "code.gitea.io/gitea/modules/svg"
) )
func BasicThemeIcon(entry *git.TreeEntry) template.HTML { type FileEntry struct {
Name string
EntryMode git.EntryMode
FollowEntryMode git.EntryMode
}
func GetFileEntryByTreeEntry(entry *git.TreeEntry) *FileEntry {
if entry.IsLink() {
if te, err := entry.FollowLink(); err == nil && te.IsDir() {
return &FileEntry{
Name: entry.Name(),
EntryMode: entry.Mode(),
FollowEntryMode: te.Mode(),
}
}
}
return &FileEntry{
Name: entry.Name(),
EntryMode: entry.Mode(),
}
}
func BasicThemeFolderIconName(isOpen bool) string {
if isOpen {
return "octicon-file-directory-open-fill"
}
return "octicon-file-directory-fill"
}
func BasicThemeFolderIconWithOpenStatus(isOpen bool) template.HTML {
return svg.RenderHTML(BasicThemeFolderIconName(isOpen))
}
func BasicThemeIconWithOpenStatus(entry *FileEntry, isOpen bool) template.HTML {
// TODO: add "open icon" support
svgName := "octicon-file" svgName := "octicon-file"
switch { switch {
case entry.IsLink(): case entry.EntryMode.IsLink():
svgName = "octicon-file-symlink-file" svgName = "octicon-file-symlink-file"
if te, err := entry.FollowLink(); err == nil && te.IsDir() { if entry.FollowEntryMode.IsDir() {
svgName = "octicon-file-directory-symlink" svgName = "octicon-file-directory-symlink"
} }
case entry.IsDir(): case entry.EntryMode.IsDir():
svgName = "octicon-file-directory-fill" svgName = BasicThemeFolderIconName(isOpen)
case entry.IsSubModule(): case entry.EntryMode.IsSubModule():
svgName = "octicon-file-submodule" svgName = "octicon-file-submodule"
} }
return svg.RenderHTML(svgName) return svg.RenderHTML(svgName)

View File

@ -9,7 +9,6 @@ import (
"strings" "strings"
"sync" "sync"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/options" "code.gitea.io/gitea/modules/options"
@ -69,41 +68,51 @@ func (m *MaterialIconProvider) renderFileIconSVG(p *RenderedIconPool, name, svg,
} }
svgID := "svg-mfi-" + name svgID := "svg-mfi-" + name
svgCommonAttrs := `class="svg git-entry-icon ` + extraClass + `" width="16" height="16" aria-hidden="true"` 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] == "" { 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>`) 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) FolderIconWithOpenStatus(p *RenderedIconPool, isOpen bool) template.HTML {
name := m.FindIconName("folder", true, isOpen)
return m.renderFileIconSVG(p, name, m.svgs[name], BasicThemeFolderIconName(isOpen))
}
func (m *MaterialIconProvider) FileIconWithOpenStatus(p *RenderedIconPool, entry *FileEntry, isOpen bool) template.HTML {
if m.rules == nil { if m.rules == nil {
return BasicThemeIcon(entry) return BasicThemeIconWithOpenStatus(entry, isOpen)
} }
if entry.IsLink() { if entry.EntryMode.IsLink() {
if te, err := entry.FollowLink(); err == nil && te.IsDir() { if entry.FollowEntryMode.IsDir() {
// keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work // 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("material-folder-symlink", 16, "octicon-file-directory-symlink")
} }
return svg.RenderHTML("octicon-file-symlink-file") // TODO: find some better icons for them return svg.RenderHTML("octicon-file-symlink-file") // TODO: find some better icons for them
} }
name := m.findIconNameByGit(entry) // TODO: add "open icon" support
name := m.findIconNameByGit(entry, isOpen)
// the material icon pack's "folder" icon doesn't look good, so use our built-in one // 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 // 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 != "" { if iconSVG, ok := m.svgs[name]; ok && iconSVG != "" {
// keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work // keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work
extraClass := "octicon-file" extraClass := "octicon-file"
switch { switch {
case entry.IsDir(): case entry.EntryMode.IsDir():
extraClass = "octicon-file-directory-fill" extraClass = BasicThemeFolderIconName(isOpen)
case entry.IsSubModule(): case entry.EntryMode.IsSubModule():
extraClass = "octicon-file-submodule" extraClass = "octicon-file-submodule"
} }
return m.renderFileIconSVG(p, name, iconSVG, extraClass) return m.renderFileIconSVG(p, name, iconSVG, extraClass)
} }
// TODO: use an interface or wrapper for git.Entry to make the code testable. // TODO: use an interface or wrapper for git.Entry to make the code testable.
return BasicThemeIcon(entry) return BasicThemeIconWithOpenStatus(entry, isOpen)
} }
func (m *MaterialIconProvider) findIconNameWithLangID(s string) string { func (m *MaterialIconProvider) findIconNameWithLangID(s string) string {
@ -118,12 +127,15 @@ func (m *MaterialIconProvider) findIconNameWithLangID(s string) string {
return "" return ""
} }
func (m *MaterialIconProvider) FindIconName(name string, isDir bool) string { func (m *MaterialIconProvider) FindIconName(name string, isDir, isOpen bool) string {
fileNameLower := strings.ToLower(path.Base(name)) fileNameLower := strings.ToLower(path.Base(name))
if isDir { if isDir {
if s, ok := m.rules.FolderNames[fileNameLower]; ok { if s, ok := m.rules.FolderNames[fileNameLower]; ok {
return s return s
} }
if isOpen {
return "folder-open"
}
return "folder" return "folder"
} }
@ -147,9 +159,9 @@ func (m *MaterialIconProvider) FindIconName(name string, isDir bool) string {
return "file" return "file"
} }
func (m *MaterialIconProvider) findIconNameByGit(entry *git.TreeEntry) string { func (m *MaterialIconProvider) findIconNameByGit(entry *FileEntry, isOpen bool) string {
if entry.IsSubModule() { if entry.EntryMode.IsSubModule() {
return "folder-git" return "folder-git"
} }
return m.FindIconName(entry.Name(), entry.IsDir()) return m.FindIconName(entry.Name, entry.EntryMode.IsDir(), isOpen)
} }

View File

@ -19,8 +19,8 @@ func TestMain(m *testing.M) {
func TestFindIconName(t *testing.T) { func TestFindIconName(t *testing.T) {
unittest.PrepareTestEnv(t) unittest.PrepareTestEnv(t)
p := fileicon.DefaultMaterialIconProvider() p := fileicon.DefaultMaterialIconProvider()
assert.Equal(t, "php", p.FindIconName("foo.php", false)) assert.Equal(t, "php", p.FindIconName("foo.php", false, false))
assert.Equal(t, "php", p.FindIconName("foo.PHP", false)) assert.Equal(t, "php", p.FindIconName("foo.PHP", false, false))
assert.Equal(t, "javascript", p.FindIconName("foo.js", false)) assert.Equal(t, "javascript", p.FindIconName("foo.js", false, false))
assert.Equal(t, "visualstudio", p.FindIconName("foo.vba", false)) assert.Equal(t, "visualstudio", p.FindIconName("foo.vba", false, false))
} }

View File

@ -7,7 +7,6 @@ import (
"html/template" "html/template"
"strings" "strings"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
) )
@ -34,19 +33,9 @@ func (p *RenderedIconPool) RenderToHTML() template.HTML {
return template.HTML(sb.String()) return template.HTML(sb.String())
} }
// TODO: use an interface or struct to replace "*git.TreeEntry", to decouple the fileicon module from git module func RenderEntryIconWithOpenStatus(renderedIconPool *RenderedIconPool, entry *FileEntry, isOpen bool) template.HTML {
func RenderEntryIcon(renderedIconPool *RenderedIconPool, entry *git.TreeEntry) template.HTML {
if setting.UI.FileIconTheme == "material" { if setting.UI.FileIconTheme == "material" {
return DefaultMaterialIconProvider().FileIcon(renderedIconPool, entry) return DefaultMaterialIconProvider().FileIconWithOpenStatus(renderedIconPool, entry, isOpen)
} }
return BasicThemeIcon(entry) return BasicThemeIconWithOpenStatus(entry, isOpen)
}
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)
} }

View File

@ -30,6 +30,31 @@ func (e EntryMode) String() string {
return strconv.FormatInt(int64(e), 8) 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) { func ParseEntryMode(mode string) (EntryMode, error) {
switch mode { switch mode {
case "000000": case "000000":

View File

@ -59,27 +59,27 @@ func (te *TreeEntry) Size() int64 {
// IsSubModule if the entry is a sub module // IsSubModule if the entry is a sub module
func (te *TreeEntry) IsSubModule() bool { func (te *TreeEntry) IsSubModule() bool {
return te.entryMode == EntryModeCommit return te.entryMode.IsSubModule()
} }
// IsDir if the entry is a sub dir // IsDir if the entry is a sub dir
func (te *TreeEntry) IsDir() bool { func (te *TreeEntry) IsDir() bool {
return te.entryMode == EntryModeTree return te.entryMode.IsDir()
} }
// IsLink if the entry is a symlink // IsLink if the entry is a symlink
func (te *TreeEntry) IsLink() bool { func (te *TreeEntry) IsLink() bool {
return te.entryMode == EntryModeSymlink return te.entryMode.IsLink()
} }
// IsRegular if the entry is a regular file // IsRegular if the entry is a regular file
func (te *TreeEntry) IsRegular() bool { func (te *TreeEntry) IsRegular() bool {
return te.entryMode == EntryModeBlob return te.entryMode.IsRegular()
} }
// IsExecutable if the entry is an executable file (not necessarily binary) // IsExecutable if the entry is an executable file (not necessarily binary)
func (te *TreeEntry) IsExecutable() bool { func (te *TreeEntry) IsExecutable() bool {
return te.entryMode == EntryModeExec return te.entryMode.IsExecutable()
} }
// Blob returns the blob object the entry // Blob returns the blob object the entry

View File

@ -59,11 +59,12 @@ func NewFuncMap() template.FuncMap {
// ----------------------------------------------------------------- // -----------------------------------------------------------------
// svg / avatar / icon / color // svg / avatar / icon / color
"svg": svg.RenderHTML, "svg": svg.RenderHTML,
"MigrationIcon": migrationIcon, "folderIconHTMLByOpenStatus": FolderIconHTMLByOpenStatus,
"ActionIcon": actionIcon, "MigrationIcon": migrationIcon,
"SortArrow": sortArrow, "ActionIcon": actionIcon,
"ContrastColor": util.ContrastColor, "SortArrow": sortArrow,
"ContrastColor": util.ContrastColor,
// ----------------------------------------------------------------- // -----------------------------------------------------------------
// time / number / format // time / number / format

View File

@ -14,11 +14,13 @@ import (
activities_model "code.gitea.io/gitea/models/activities" activities_model "code.gitea.io/gitea/models/activities"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/fileicon"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
giturl "code.gitea.io/gitea/modules/git/url" giturl "code.gitea.io/gitea/modules/git/url"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/svg" "code.gitea.io/gitea/modules/svg"
"github.com/editorconfig/editorconfig-core-go/v2" "github.com/editorconfig/editorconfig-core-go/v2"
@ -192,3 +194,10 @@ func tabSizeClass(ec *editorconfig.Editorconfig, filename string) string {
} }
return "tab-size-4" return "tab-size-4"
} }
func FolderIconHTMLByOpenStatus(isOpen bool) template.HTML {
if setting.UI.FileIconTheme == "material" {
return fileicon.DefaultMaterialIconProvider().FolderIconWithOpenStatus(nil, isOpen)
}
return fileicon.BasicThemeFolderIconWithOpenStatus(isOpen)
}

View File

@ -21,6 +21,7 @@ import (
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/fileicon"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
@ -369,7 +370,11 @@ func Diff(ctx *context.Context) {
return return
} }
ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(diffTree, nil) renderedIconPool := fileicon.NewRenderedIconPool()
ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(renderedIconPool, diffTree, nil)
ctx.PageData["FolderIcon"] = templates.FolderIconHTMLByOpenStatus(false)
ctx.PageData["FolderOpenIcon"] = templates.FolderIconHTMLByOpenStatus(true)
ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML()
} }
statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, commitID, db.ListOptionsAll) statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, commitID, db.ListOptionsAll)

View File

@ -26,6 +26,7 @@ import (
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/charset"
csv_module "code.gitea.io/gitea/modules/csv" 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/git"
"code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
@ -639,7 +640,11 @@ func PrepareCompareDiff(
return false return false
} }
ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(diffTree, nil) renderedIconPool := fileicon.NewRenderedIconPool()
ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(renderedIconPool, diffTree, nil)
ctx.PageData["FolderIcon"] = templates.FolderIconHTMLByOpenStatus(false)
ctx.PageData["FolderOpenIcon"] = templates.FolderIconHTMLByOpenStatus(true)
ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML()
} }
headCommit, err := ci.HeadGitRepo.GetCommit(headCommitID) headCommit, err := ci.HeadGitRepo.GetCommit(headCommitID)

View File

@ -24,6 +24,7 @@ import (
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/emoji" "code.gitea.io/gitea/modules/emoji"
"code.gitea.io/gitea/modules/fileicon"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/gitrepo"
issue_template "code.gitea.io/gitea/modules/issue/template" issue_template "code.gitea.io/gitea/modules/issue/template"
@ -824,7 +825,12 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
if reviewState != nil { if reviewState != nil {
filesViewedState = reviewState.UpdatedFiles filesViewedState = reviewState.UpdatedFiles
} }
ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(diffTree, filesViewedState)
renderedIconPool := fileicon.NewRenderedIconPool()
ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(renderedIconPool, diffTree, filesViewedState)
ctx.PageData["FolderIcon"] = templates.FolderIconHTMLByOpenStatus(false)
ctx.PageData["FolderOpenIcon"] = templates.FolderIconHTMLByOpenStatus(true)
ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML()
} }
ctx.Data["Diff"] = diff ctx.Data["Diff"] = diff

View File

@ -4,6 +4,7 @@
package repo package repo
import ( import (
"html/template"
"net/http" "net/http"
"strings" "strings"
@ -67,7 +68,7 @@ type WebDiffFileItem struct {
EntryMode string EntryMode string
IsViewed bool IsViewed bool
Children []*WebDiffFileItem 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 // 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 // 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 // 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} dirNodes := map[string]*WebDiffFileItem{"": &dft.TreeRoot}
addItem := func(item *WebDiffFileItem) { addItem := func(item *WebDiffFileItem) {
var parentPath string var parentPath string
@ -110,6 +111,10 @@ func transformDiffTreeForWeb(diffTree *gitdiff.DiffTree, filesViewedState map[st
item := &WebDiffFileItem{FullName: file.HeadPath, DiffStatus: file.Status} item := &WebDiffFileItem{FullName: file.HeadPath, DiffStatus: file.Status}
item.IsViewed = filesViewedState[item.FullName] == pull_model.Viewed item.IsViewed = filesViewedState[item.FullName] == pull_model.Viewed
item.NameHash = git.HashFilePathForWebUI(item.FullName) item.NameHash = git.HashFilePathForWebUI(item.FullName)
item.FileIcon = fileicon.RenderEntryIconWithOpenStatus(renderedIconPool, &fileicon.FileEntry{
Name: file.HeadPath,
EntryMode: file.HeadMode,
}, false)
switch file.HeadMode { switch file.HeadMode {
case git.EntryModeTree: case git.EntryModeTree:

View File

@ -4,9 +4,11 @@
package repo package repo
import ( import (
"html/template"
"testing" "testing"
pull_model "code.gitea.io/gitea/models/pull" pull_model "code.gitea.io/gitea/models/pull"
"code.gitea.io/gitea/modules/fileicon"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/services/gitdiff" "code.gitea.io/gitea/services/gitdiff"
@ -14,7 +16,8 @@ import (
) )
func TestTransformDiffTreeForWeb(t *testing.T) { 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", Status: "changed",
HeadPath: "dir-a/dir-a-x/file-deep", 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, "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{ assert.Equal(t, WebDiffFileTree{
TreeRoot: WebDiffFileItem{ TreeRoot: WebDiffFileItem{
Children: []*WebDiffFileItem{ Children: []*WebDiffFileItem{
@ -44,6 +50,7 @@ func TestTransformDiffTreeForWeb(t *testing.T) {
NameHash: "4acf7eef1c943a09e9f754e93ff190db8583236b", NameHash: "4acf7eef1c943a09e9f754e93ff190db8583236b",
DiffStatus: "changed", DiffStatus: "changed",
IsViewed: true, IsViewed: true,
FileIcon: mockIconForFile(`svg-mfi-file`),
}, },
}, },
}, },
@ -53,6 +60,7 @@ func TestTransformDiffTreeForWeb(t *testing.T) {
FullName: "file1", FullName: "file1",
NameHash: "60b27f004e454aca81b0480209cce5081ec52390", NameHash: "60b27f004e454aca81b0480209cce5081ec52390",
DiffStatus: "added", DiffStatus: "added",
FileIcon: mockIconForFile(`svg-mfi-file`),
}, },
}, },
}, },

View File

@ -257,7 +257,7 @@ func prepareDirectoryFileIcons(ctx *context.Context, files []git.CommitInfo) {
renderedIconPool := fileicon.NewRenderedIconPool() renderedIconPool := fileicon.NewRenderedIconPool()
fileIcons := map[string]template.HTML{} fileIcons := map[string]template.HTML{}
for _, f := range files { for _, f := range files {
fileIcons[f.Entry.Name()] = fileicon.RenderEntryIcon(renderedIconPool, f.Entry) fileIcons[f.Entry.Name()] = fileicon.RenderEntryIconWithOpenStatus(renderedIconPool, fileicon.GetFileEntryByTreeEntry(f.Entry), false)
} }
ctx.Data["FileIcons"] = fileIcons ctx.Data["FileIcons"] = fileIcons
ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML() ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML()

View File

@ -175,9 +175,8 @@ func newTreeViewNodeFromEntry(ctx context.Context, renderedIconPool *fileicon.Re
} }
if node.EntryIcon == "" { if node.EntryIcon == "" {
node.EntryIcon = fileicon.RenderEntryIcon(renderedIconPool, entry) node.EntryIcon = fileicon.RenderEntryIconWithOpenStatus(renderedIconPool, fileicon.GetFileEntryByTreeEntry(entry), false)
// TODO: no open icon support yet node.EntryIconOpen = fileicon.RenderEntryIconWithOpenStatus(renderedIconPool, fileicon.GetFileEntryByTreeEntry(entry), true)
// node.EntryIconOpen = fileicon.RenderEntryIconOpen(renderedIconPool, entry)
} }
if node.EntryMode == "commit" { if node.EntryMode == "commit" {

View File

@ -71,14 +71,18 @@ func TestGetTreeViewNodes(t *testing.T) {
mockIconForFolder := func(id string) template.HTML { 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>`) 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, "", "") treeNodes, err := GetTreeViewNodes(ctx, renderedIconPool, ctx.Repo.Commit, "", "")
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, []*TreeViewNode{ assert.Equal(t, []*TreeViewNode{
{ {
EntryName: "docs", EntryName: "docs",
EntryMode: "tree", EntryMode: "tree",
FullPath: "docs", FullPath: "docs",
EntryIcon: mockIconForFolder(`svg-mfi-folder-docs`), EntryIcon: mockIconForFolder(`svg-mfi-folder-docs`),
EntryIconOpen: mockOpenIconForFolder(`svg-mfi-folder-docs`),
}, },
}, treeNodes) }, treeNodes)
@ -86,16 +90,18 @@ func TestGetTreeViewNodes(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, []*TreeViewNode{ assert.Equal(t, []*TreeViewNode{
{ {
EntryName: "docs", EntryName: "docs",
EntryMode: "tree", EntryMode: "tree",
FullPath: "docs", FullPath: "docs",
EntryIcon: mockIconForFolder(`svg-mfi-folder-docs`), EntryIcon: mockIconForFolder(`svg-mfi-folder-docs`),
EntryIconOpen: mockOpenIconForFolder(`svg-mfi-folder-docs`),
Children: []*TreeViewNode{ Children: []*TreeViewNode{
{ {
EntryName: "README.md", EntryName: "README.md",
EntryMode: "blob", EntryMode: "blob",
FullPath: "docs/README.md", FullPath: "docs/README.md",
EntryIcon: mockIconForFile(`svg-mfi-readme`), EntryIcon: mockIconForFile(`svg-mfi-readme`),
EntryIconOpen: mockIconForFile(`svg-mfi-readme`),
}, },
}, },
}, },
@ -105,10 +111,11 @@ func TestGetTreeViewNodes(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, []*TreeViewNode{ assert.Equal(t, []*TreeViewNode{
{ {
EntryName: "README.md", EntryName: "README.md",
EntryMode: "blob", EntryMode: "blob",
FullPath: "docs/README.md", FullPath: "docs/README.md",
EntryIcon: mockIconForFile(`svg-mfi-readme`), EntryIcon: mockIconForFile(`svg-mfi-readme`),
EntryIconOpen: mockIconForFile(`svg-mfi-readme`),
}, },
}, treeNodes) }, treeNodes)
} }

View File

@ -21,7 +21,7 @@
<div class="ui aligned divided list"> <div class="ui aligned divided list">
{{range $dirI, $dir := .Dirs}} {{range $dirI, $dir := .Dirs}}
<div class="item tw-flex tw-items-center"> <div class="item tw-flex tw-items-center">
<span class="tw-flex-1"> {{svg "octicon-file-directory-fill"}} {{$dir}}</span> <span class="tw-flex-1"> {{folderIconHTMLByOpenStatus false}} {{$dir}}</span>
<div> <div>
<button class="ui button primary show-modal tw-p-2" data-modal="#adopt-unadopted-modal-{{$dirI}}">{{svg "octicon-plus"}} {{ctx.Locale.Tr "repo.adopt_preexisting_label"}}</button> <button class="ui button primary show-modal tw-p-2" data-modal="#adopt-unadopted-modal-{{$dirI}}">{{svg "octicon-plus"}} {{ctx.Locale.Tr "repo.adopt_preexisting_label"}}</button>
<div class="ui g-modal-confirm modal" id="adopt-unadopted-modal-{{$dirI}}"> <div class="ui g-modal-confirm modal" id="adopt-unadopted-modal-{{$dirI}}">

View File

@ -60,6 +60,7 @@
{{end}} {{end}}
<div id="diff-container"> <div id="diff-container">
{{if $showFileTree}} {{if $showFileTree}}
{{$.FileIconPoolHTML}}
<div id="diff-file-tree" class="tw-hidden not-mobile"></div> <div id="diff-file-tree" class="tw-hidden not-mobile"></div>
<script> <script>
if (diffTreeVisible) document.getElementById('diff-file-tree').classList.remove('tw-hidden'); if (diffTreeVisible) document.getElementById('diff-file-tree').classList.remove('tw-hidden');

View File

@ -6,7 +6,7 @@
</div> </div>
{{if .HasParentPath}} {{if .HasParentPath}}
<a class="repo-file-line parent-link silenced" href="{{.BranchLink}}{{if .ParentPath}}{{PathEscapeSegments .ParentPath}}{{end}}"> <a class="repo-file-line parent-link silenced" href="{{.BranchLink}}{{if .ParentPath}}{{PathEscapeSegments .ParentPath}}{{end}}">
{{svg "octicon-file-directory-fill"}} .. {{folderIconHTMLByOpenStatus false}} ..
</a> </a>
{{end}} {{end}}
{{$.FileIconPoolHTML}} {{$.FileIconPoolHTML}}

View File

@ -30,7 +30,7 @@
<span><a href="{{$repo.BaseRepo.Link}}">{{$repo.BaseRepo.OwnerName}}/{{$repo.BaseRepo.Name}}</a></span> <span><a href="{{$repo.BaseRepo.Link}}">{{$repo.BaseRepo.OwnerName}}/{{$repo.BaseRepo.Name}}</a></span>
{{end}} {{end}}
{{else}} {{else}}
<span class="icon tw-inline-block tw-pt-2">{{svg "octicon-file-directory-fill"}}</span> <span class="icon tw-inline-block tw-pt-2">{{folderIconHTMLByOpenStatus false}}</span>
<span class="name tw-inline-block tw-pt-2">{{$.ContextUser.Name}}/{{$dir}}</span> <span class="name tw-inline-block tw-pt-2">{{$.ContextUser.Name}}/{{$dir}}</span>
<div class="tw-float-right"> <div class="tw-float-right">
{{if $.allowAdopt}} {{if $.allowAdopt}}

View File

@ -22,13 +22,6 @@ function getIconForDiffStatus(pType: DiffStatus) {
}; };
return diffTypes[pType] ?? diffTypes['']; return diffTypes[pType] ?? diffTypes[''];
} }
function entryIcon(entry: DiffTreeEntry) {
if (entry.EntryMode === 'commit') {
return 'octicon-file-submodule';
}
return 'octicon-file';
}
</script> </script>
<template> <template>
@ -36,10 +29,8 @@ function entryIcon(entry: DiffTreeEntry) {
<div class="item-directory" :class="{ 'viewed': item.IsViewed }" :title="item.DisplayName" @click.stop="collapsed = !collapsed"> <div class="item-directory" :class="{ 'viewed': item.IsViewed }" :title="item.DisplayName" @click.stop="collapsed = !collapsed">
<!-- directory --> <!-- directory -->
<SvgIcon :name="collapsed ? 'octicon-chevron-right' : 'octicon-chevron-down'"/> <SvgIcon :name="collapsed ? 'octicon-chevron-right' : 'octicon-chevron-down'"/>
<SvgIcon <!-- eslint-disable-next-line vue/no-v-html -->
class="text primary" <span class="tw-contents" v-html="collapsed ? store.folderIcon : store.folderOpenIcon"/>
:name="collapsed ? 'octicon-file-directory-fill' : 'octicon-file-directory-open-fill'"
/>
<span class="gt-ellipsis">{{ item.DisplayName }}</span> <span class="gt-ellipsis">{{ item.DisplayName }}</span>
</div> </div>
@ -53,7 +44,8 @@ function entryIcon(entry: DiffTreeEntry) {
:title="item.DisplayName" :href="'#diff-' + item.NameHash" :title="item.DisplayName" :href="'#diff-' + item.NameHash"
> >
<!-- file --> <!-- 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> <span class="gt-ellipsis tw-flex-1">{{ item.DisplayName }}</span>
<SvgIcon <SvgIcon
:name="getIconForDiffStatus(item.DiffStatus).name" :name="getIconForDiffStatus(item.DiffStatus).name"

View File

@ -9,6 +9,7 @@ test('diff-tree', () => {
'IsViewed': false, 'IsViewed': false,
'NameHash': '....', 'NameHash': '....',
'DiffStatus': '', 'DiffStatus': '',
'FileIcon': '',
'Children': [ 'Children': [
{ {
'FullName': 'dir1', 'FullName': 'dir1',
@ -17,6 +18,7 @@ test('diff-tree', () => {
'IsViewed': false, 'IsViewed': false,
'NameHash': '....', 'NameHash': '....',
'DiffStatus': '', 'DiffStatus': '',
'FileIcon': '',
'Children': [ 'Children': [
{ {
'FullName': 'dir1/test.txt', 'FullName': 'dir1/test.txt',
@ -25,6 +27,7 @@ test('diff-tree', () => {
'NameHash': '....', 'NameHash': '....',
'EntryMode': '', 'EntryMode': '',
'IsViewed': false, 'IsViewed': false,
'FileIcon': '',
'Children': null, 'Children': null,
}, },
], ],
@ -36,11 +39,12 @@ test('diff-tree', () => {
'DiffStatus': 'added', 'DiffStatus': 'added',
'EntryMode': '', 'EntryMode': '',
'IsViewed': false, 'IsViewed': false,
'FileIcon': '',
'Children': null, 'Children': null,
}, },
], ],
}, },
}); }, '', '');
diffTreeStoreSetViewed(store, 'dir1/test.txt', true); diffTreeStoreSetViewed(store, 'dir1/test.txt', true);
expect(store.fullNameMap['dir1/test.txt'].IsViewed).toBe(true); expect(store.fullNameMap['dir1/test.txt'].IsViewed).toBe(true);
expect(store.fullNameMap['dir1'].IsViewed).toBe(true); expect(store.fullNameMap['dir1'].IsViewed).toBe(true);

View File

@ -13,7 +13,7 @@ export type DiffTreeEntry = {
EntryMode: string, EntryMode: string,
IsViewed: boolean, IsViewed: boolean,
Children: DiffTreeEntry[], Children: DiffTreeEntry[],
FileIcon: string,
ParentEntry?: DiffTreeEntry, ParentEntry?: DiffTreeEntry,
} }
@ -22,6 +22,8 @@ type DiffFileTreeData = {
}; };
type DiffFileTree = { type DiffFileTree = {
folderIcon: string;
folderOpenIcon: string;
diffFileTree: DiffFileTreeData; diffFileTree: DiffFileTreeData;
fullNameMap?: Record<string, DiffTreeEntry> fullNameMap?: Record<string, DiffTreeEntry>
fileTreeIsVisible: boolean; fileTreeIsVisible: boolean;
@ -31,7 +33,7 @@ type DiffFileTree = {
let diffTreeStoreReactive: Reactive<DiffFileTree>; let diffTreeStoreReactive: Reactive<DiffFileTree>;
export function diffTreeStore() { export function diffTreeStore() {
if (!diffTreeStoreReactive) { if (!diffTreeStoreReactive) {
diffTreeStoreReactive = reactiveDiffTreeStore(pageData.DiffFileTree); diffTreeStoreReactive = reactiveDiffTreeStore(pageData.DiffFileTree, pageData.FolderIcon, pageData.FolderOpenIcon);
} }
return diffTreeStoreReactive; 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({ const store = reactive({
diffFileTree: data, diffFileTree: data,
folderIcon,
folderOpenIcon,
fileTreeIsVisible: false, fileTreeIsVisible: false,
selectedItem: '', selectedItem: '',
fullNameMap: {}, fullNameMap: {},