diff --git a/modules/fileicon/material.go b/modules/fileicon/material.go index cbdb962ee3..557f7ca9e4 100644 --- a/modules/fileicon/material.go +++ b/modules/fileicon/material.go @@ -13,7 +13,6 @@ import ( "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/options" - "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/svg" ) @@ -62,13 +61,7 @@ func (m *MaterialIconProvider) loadData() { log.Debug("Loaded material icon rules and SVG images") } -func (m *MaterialIconProvider) renderFileIconSVG(ctx reqctx.RequestContext, name, svg, extraClass string) template.HTML { - data := ctx.GetData() - renderedSVGs, _ := data["_RenderedSVGs"].(map[string]bool) - if renderedSVGs == nil { - renderedSVGs = make(map[string]bool) - data["_RenderedSVGs"] = renderedSVGs - } +func (m *MaterialIconProvider) renderFileIconSVG(p *RenderedIconPool, name, svg, extraClass string) template.HTML { // This part is a bit hacky, but it works really well. It should be safe to do so because all SVG icons are generated by us. // Will try to refactor this in the future. if !strings.HasPrefix(svg, "<svg") { @@ -76,16 +69,13 @@ func (m *MaterialIconProvider) renderFileIconSVG(ctx reqctx.RequestContext, name } svgID := "svg-mfi-" + name svgCommonAttrs := `class="svg git-entry-icon ` + extraClass + `" width="16" height="16" aria-hidden="true"` - posOuterBefore := strings.IndexByte(svg, '>') - if renderedSVGs[svgID] && posOuterBefore != -1 { - return template.HTML(`<svg ` + svgCommonAttrs + `><use xlink:href="#` + svgID + `"></use></svg>`) + if p.IconSVGs[svgID] == "" { + p.IconSVGs[svgID] = template.HTML(`<svg id="` + svgID + `" ` + svgCommonAttrs + svg[4:]) } - svg = `<svg id="` + svgID + `" ` + svgCommonAttrs + svg[4:] - renderedSVGs[svgID] = true - return template.HTML(svg) + return template.HTML(`<svg ` + svgCommonAttrs + `><use xlink:href="#` + svgID + `"></use></svg>`) } -func (m *MaterialIconProvider) FileIcon(ctx reqctx.RequestContext, entry *git.TreeEntry) template.HTML { +func (m *MaterialIconProvider) FileIcon(p *RenderedIconPool, entry *git.TreeEntry) template.HTML { if m.rules == nil { return BasicThemeIcon(entry) } @@ -110,7 +100,7 @@ func (m *MaterialIconProvider) FileIcon(ctx reqctx.RequestContext, entry *git.Tr case entry.IsSubModule(): extraClass = "octicon-file-submodule" } - return m.renderFileIconSVG(ctx, name, iconSVG, extraClass) + return m.renderFileIconSVG(p, name, iconSVG, extraClass) } // TODO: use an interface or wrapper for git.Entry to make the code testable. return BasicThemeIcon(entry) diff --git a/modules/fileicon/render.go b/modules/fileicon/render.go new file mode 100644 index 0000000000..1d014693fd --- /dev/null +++ b/modules/fileicon/render.go @@ -0,0 +1,52 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package fileicon + +import ( + "html/template" + "strings" + + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/setting" +) + +type RenderedIconPool struct { + IconSVGs map[string]template.HTML +} + +func NewRenderedIconPool() *RenderedIconPool { + return &RenderedIconPool{ + IconSVGs: make(map[string]template.HTML), + } +} + +func (p *RenderedIconPool) RenderToHTML() template.HTML { + if len(p.IconSVGs) == 0 { + return "" + } + sb := &strings.Builder{} + sb.WriteString(`<div class=tw-hidden>`) + for _, icon := range p.IconSVGs { + sb.WriteString(string(icon)) + } + sb.WriteString(`</div>`) + 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 { + if setting.UI.FileIconTheme == "material" { + return DefaultMaterialIconProvider().FileIcon(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) +} diff --git a/modules/git/error.go b/modules/git/error.go index 10fb37be07..6c86d1b04d 100644 --- a/modules/git/error.go +++ b/modules/git/error.go @@ -32,19 +32,19 @@ func (err ErrNotExist) Unwrap() error { return util.ErrNotExist } -// ErrBadLink entry.FollowLink error -type ErrBadLink struct { +// ErrSymlinkUnresolved entry.FollowLink error +type ErrSymlinkUnresolved struct { Name string Message string } -func (err ErrBadLink) Error() string { +func (err ErrSymlinkUnresolved) Error() string { return fmt.Sprintf("%s: %s", err.Name, err.Message) } -// IsErrBadLink if some error is ErrBadLink -func IsErrBadLink(err error) bool { - _, ok := err.(ErrBadLink) +// IsErrSymlinkUnresolved if some error is ErrSymlinkUnresolved +func IsErrSymlinkUnresolved(err error) bool { + _, ok := err.(ErrSymlinkUnresolved) return ok } diff --git a/modules/git/tree_entry.go b/modules/git/tree_entry.go index 9513121487..a2e1579290 100644 --- a/modules/git/tree_entry.go +++ b/modules/git/tree_entry.go @@ -8,6 +8,8 @@ import ( "io" "sort" "strings" + + "code.gitea.io/gitea/modules/util" ) // Type returns the type of the entry (commit, tree, blob) @@ -25,7 +27,7 @@ func (te *TreeEntry) Type() string { // FollowLink returns the entry pointed to by a symlink func (te *TreeEntry) FollowLink() (*TreeEntry, error) { if !te.IsLink() { - return nil, ErrBadLink{te.Name(), "not a symlink"} + return nil, ErrSymlinkUnresolved{te.Name(), "not a symlink"} } // read the link @@ -56,13 +58,13 @@ func (te *TreeEntry) FollowLink() (*TreeEntry, error) { } if t == nil { - return nil, ErrBadLink{te.Name(), "points outside of repo"} + return nil, ErrSymlinkUnresolved{te.Name(), "points outside of repo"} } target, err := t.GetTreeEntryByPath(lnk) if err != nil { if IsErrNotExist(err) { - return nil, ErrBadLink{te.Name(), "broken link"} + return nil, ErrSymlinkUnresolved{te.Name(), "broken link"} } return nil, err } @@ -70,33 +72,27 @@ func (te *TreeEntry) FollowLink() (*TreeEntry, error) { } // FollowLinks returns the entry ultimately pointed to by a symlink -func (te *TreeEntry) FollowLinks() (*TreeEntry, error) { +func (te *TreeEntry) FollowLinks(optLimit ...int) (*TreeEntry, error) { if !te.IsLink() { - return nil, ErrBadLink{te.Name(), "not a symlink"} + return nil, ErrSymlinkUnresolved{te.Name(), "not a symlink"} } + limit := util.OptionalArg(optLimit, 10) entry := te - for i := 0; i < 999; i++ { - if entry.IsLink() { - next, err := entry.FollowLink() - if err != nil { - return nil, err - } - if next.ID == entry.ID { - return nil, ErrBadLink{ - entry.Name(), - "recursive link", - } - } - entry = next - } else { + for i := 0; i < limit; i++ { + if !entry.IsLink() { break } + next, err := entry.FollowLink() + if err != nil { + return nil, err + } + if next.ID == entry.ID { + return nil, ErrSymlinkUnresolved{entry.Name(), "recursive link"} + } + entry = next } if entry.IsLink() { - return nil, ErrBadLink{ - te.Name(), - "too many levels of symbolic links", - } + return nil, ErrSymlinkUnresolved{te.Name(), "too many levels of symbolic links"} } return entry, nil } diff --git a/modules/git/tree_entry_mode.go b/modules/git/tree_entry_mode.go index ec4487549d..1193bec4f1 100644 --- a/modules/git/tree_entry_mode.go +++ b/modules/git/tree_entry_mode.go @@ -17,16 +17,12 @@ const ( // EntryModeNoEntry is possible if the file was added or removed in a commit. In the case of // added the base commit will not have the file in its tree so a mode of 0o000000 is used. EntryModeNoEntry EntryMode = 0o000000 - // EntryModeBlob - EntryModeBlob EntryMode = 0o100644 - // EntryModeExec - EntryModeExec EntryMode = 0o100755 - // EntryModeSymlink + + EntryModeBlob EntryMode = 0o100644 + EntryModeExec EntryMode = 0o100755 EntryModeSymlink EntryMode = 0o120000 - // EntryModeCommit - EntryModeCommit EntryMode = 0o160000 - // EntryModeTree - EntryModeTree EntryMode = 0o040000 + EntryModeCommit EntryMode = 0o160000 + EntryModeTree EntryMode = 0o040000 ) // String converts an EntryMode to a string @@ -34,12 +30,6 @@ func (e EntryMode) String() string { return strconv.FormatInt(int64(e), 8) } -// ToEntryMode converts a string to an EntryMode -func ToEntryMode(value string) EntryMode { - v, _ := strconv.ParseInt(value, 8, 32) - return EntryMode(v) -} - func ParseEntryMode(mode string) (EntryMode, error) { switch mode { case "000000": diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go index 27316bbfec..ae397d87c9 100644 --- a/modules/templates/util_render.go +++ b/modules/templates/util_render.go @@ -15,8 +15,6 @@ import ( issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/modules/emoji" - "code.gitea.io/gitea/modules/fileicon" - "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/htmlutil" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" @@ -181,13 +179,6 @@ func (ut *RenderUtils) RenderLabel(label *issues_model.Label) template.HTML { textColor, itemColor, itemHTML) } -func (ut *RenderUtils) RenderFileIcon(entry *git.TreeEntry) template.HTML { - if setting.UI.FileIconTheme == "material" { - return fileicon.DefaultMaterialIconProvider().FileIcon(ut.ctx, entry) - } - return fileicon.BasicThemeIcon(entry) -} - // RenderEmoji renders html text with emoji post processors func (ut *RenderUtils) RenderEmoji(text string) template.HTML { renderedText, err := markup.PostProcessEmoji(markup.NewRenderContext(ut.ctx), template.HTMLEscapeString(text)) diff --git a/routers/web/repo/treelist.go b/routers/web/repo/treelist.go index ab74741e61..9c5ec8f206 100644 --- a/routers/web/repo/treelist.go +++ b/routers/web/repo/treelist.go @@ -8,6 +8,7 @@ import ( pull_model "code.gitea.io/gitea/models/pull" "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/fileicon" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/gitdiff" @@ -87,10 +88,11 @@ func transformDiffTreeForUI(diffTree *gitdiff.DiffTree, filesViewedState map[str } func TreeViewNodes(ctx *context.Context) { - results, err := files_service.GetTreeViewNodes(ctx, ctx.Repo.Commit, ctx.Repo.TreePath, ctx.FormString("sub_path")) + renderedIconPool := fileicon.NewRenderedIconPool() + results, err := files_service.GetTreeViewNodes(ctx, renderedIconPool, ctx.Repo.Commit, ctx.Repo.TreePath, ctx.FormString("sub_path")) if err != nil { ctx.ServerError("GetTreeViewNodes", err) return } - ctx.JSON(http.StatusOK, map[string]any{"fileTreeNodes": results}) + ctx.JSON(http.StatusOK, map[string]any{"fileTreeNodes": results, "renderedIconPool": renderedIconPool.IconSVGs}) } diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 6ed5801d10..77240f0431 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -29,6 +29,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/lfs" "code.gitea.io/gitea/modules/log" @@ -252,6 +253,16 @@ func LastCommit(ctx *context.Context) { ctx.HTML(http.StatusOK, tplRepoViewList) } +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) + } + ctx.Data["FileIcons"] = fileIcons + ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML() +} + func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entries { tree, err := ctx.Repo.Commit.SubTree(ctx.Repo.TreePath) if err != nil { @@ -293,6 +304,7 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri return nil } ctx.Data["Files"] = files + prepareDirectoryFileIcons(ctx, files) for _, f := range files { if f.Commit == nil { ctx.Data["HasFilesWithoutLatestCommit"] = true diff --git a/routers/web/repo/view_readme.go b/routers/web/repo/view_readme.go index 48befe47f8..606ee7ff79 100644 --- a/routers/web/repo/view_readme.go +++ b/routers/web/repo/view_readme.go @@ -69,7 +69,7 @@ func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, try if readmeFiles[i] == nil || base.NaturalSortLess(readmeFiles[i].Name(), entry.Blob().Name()) { if entry.IsLink() { target, err := entry.FollowLinks() - if err != nil && !git.IsErrBadLink(err) { + if err != nil && !git.IsErrSymlinkUnresolved(err) { return "", nil, err } else if target != nil && (target.IsExecutable() || target.IsRegular()) { readmeFiles[i] = entry diff --git a/services/repository/files/tree.go b/services/repository/files/tree.go index 9142416347..faeb85a046 100644 --- a/services/repository/files/tree.go +++ b/services/repository/files/tree.go @@ -6,12 +6,14 @@ package files import ( "context" "fmt" + "html/template" "net/url" "path" "sort" "strings" 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/log" "code.gitea.io/gitea/modules/setting" @@ -140,8 +142,13 @@ func entryModeString(entryMode git.EntryMode) string { } type TreeViewNode struct { - EntryName string `json:"entryName"` - EntryMode string `json:"entryMode"` + EntryName string `json:"entryName"` + EntryMode string `json:"entryMode"` + EntryIcon template.HTML `json:"entryIcon"` + EntryIconOpen template.HTML `json:"entryIconOpen,omitempty"` + + SymLinkedToMode string `json:"symLinkedToMode,omitempty"` // TODO: for the EntryMode="symlink" + FullPath string `json:"fullPath"` SubmoduleURL string `json:"submoduleUrl,omitempty"` Children []*TreeViewNode `json:"children,omitempty"` @@ -151,13 +158,28 @@ func (node *TreeViewNode) sortLevel() int { return util.Iif(node.EntryMode == "tree" || node.EntryMode == "commit", 0, 1) } -func newTreeViewNodeFromEntry(ctx context.Context, commit *git.Commit, parentDir string, entry *git.TreeEntry) *TreeViewNode { +func newTreeViewNodeFromEntry(ctx context.Context, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, parentDir string, entry *git.TreeEntry) *TreeViewNode { node := &TreeViewNode{ EntryName: entry.Name(), EntryMode: entryModeString(entry.Mode()), 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) + } + if node.EntryMode == "commit" { if subModule, err := commit.GetSubModule(node.FullPath); err != nil { log.Error("GetSubModule: %v", err) @@ -182,7 +204,7 @@ func sortTreeViewNodes(nodes []*TreeViewNode) { }) } -func listTreeNodes(ctx context.Context, commit *git.Commit, tree *git.Tree, treePath, subPath string) ([]*TreeViewNode, error) { +func listTreeNodes(ctx context.Context, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, tree *git.Tree, treePath, subPath string) ([]*TreeViewNode, error) { entries, err := tree.ListEntries() if err != nil { return nil, err @@ -191,14 +213,14 @@ func listTreeNodes(ctx context.Context, commit *git.Commit, tree *git.Tree, tree subPathDirName, subPathRemaining, _ := strings.Cut(subPath, "/") nodes := make([]*TreeViewNode, 0, len(entries)) for _, entry := range entries { - node := newTreeViewNodeFromEntry(ctx, commit, treePath, entry) + node := newTreeViewNodeFromEntry(ctx, renderedIconPool, commit, treePath, entry) nodes = append(nodes, node) if entry.IsDir() && subPathDirName == entry.Name() { subTreePath := treePath + "/" + node.EntryName if subTreePath[0] == '/' { subTreePath = subTreePath[1:] } - subNodes, err := listTreeNodes(ctx, commit, entry.Tree(), subTreePath, subPathRemaining) + subNodes, err := listTreeNodes(ctx, renderedIconPool, commit, entry.Tree(), subTreePath, subPathRemaining) if err != nil { log.Error("listTreeNodes: %v", err) } else { @@ -210,10 +232,10 @@ func listTreeNodes(ctx context.Context, commit *git.Commit, tree *git.Tree, tree return nodes, nil } -func GetTreeViewNodes(ctx context.Context, commit *git.Commit, treePath, subPath string) ([]*TreeViewNode, error) { +func GetTreeViewNodes(ctx context.Context, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, treePath, subPath string) ([]*TreeViewNode, error) { entry, err := commit.GetTreeEntryByPath(treePath) if err != nil { return nil, err } - return listTreeNodes(ctx, commit, entry.Tree(), treePath, subPath) + return listTreeNodes(ctx, renderedIconPool, commit, entry.Tree(), treePath, subPath) } diff --git a/services/repository/files/tree_test.go b/services/repository/files/tree_test.go index cbb800da01..2657c49977 100644 --- a/services/repository/files/tree_test.go +++ b/services/repository/files/tree_test.go @@ -4,9 +4,11 @@ package files import ( + "html/template" "testing" "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/fileicon" "code.gitea.io/gitea/modules/git" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/services/contexttest" @@ -62,40 +64,51 @@ func TestGetTreeViewNodes(t *testing.T) { contexttest.LoadGitRepo(t, ctx) defer ctx.Repo.GitRepo.Close() - treeNodes, err := GetTreeViewNodes(ctx, ctx.Repo.Commit, "", "") + renderedIconPool := fileicon.NewRenderedIconPool() + 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>`) + } + 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>`) + } + treeNodes, err := GetTreeViewNodes(ctx, renderedIconPool, ctx.Repo.Commit, "", "") assert.NoError(t, err) assert.Equal(t, []*TreeViewNode{ { EntryName: "docs", EntryMode: "tree", FullPath: "docs", + EntryIcon: mockIconForFolder(`svg-mfi-folder-docs`), }, }, treeNodes) - treeNodes, err = GetTreeViewNodes(ctx, ctx.Repo.Commit, "", "docs/README.md") + treeNodes, err = GetTreeViewNodes(ctx, renderedIconPool, ctx.Repo.Commit, "", "docs/README.md") assert.NoError(t, err) assert.Equal(t, []*TreeViewNode{ { EntryName: "docs", EntryMode: "tree", FullPath: "docs", + EntryIcon: mockIconForFolder(`svg-mfi-folder-docs`), Children: []*TreeViewNode{ { EntryName: "README.md", EntryMode: "blob", FullPath: "docs/README.md", + EntryIcon: mockIconForFile(`svg-mfi-readme`), }, }, }, }, treeNodes) - treeNodes, err = GetTreeViewNodes(ctx, ctx.Repo.Commit, "docs", "README.md") + treeNodes, err = GetTreeViewNodes(ctx, renderedIconPool, ctx.Repo.Commit, "docs", "README.md") assert.NoError(t, err) assert.Equal(t, []*TreeViewNode{ { EntryName: "README.md", EntryMode: "blob", FullPath: "docs/README.md", + EntryIcon: mockIconForFile(`svg-mfi-readme`), }, }, treeNodes) } diff --git a/templates/repo/view_list.tmpl b/templates/repo/view_list.tmpl index 612a2405e4..572987a986 100644 --- a/templates/repo/view_list.tmpl +++ b/templates/repo/view_list.tmpl @@ -9,13 +9,14 @@ {{svg "octicon-file-directory-fill"}} .. </a> {{end}} + {{$.FileIconPoolHTML}} {{range $item := .Files}} <div class="repo-file-item"> {{$entry := $item.Entry}} {{$commit := $item.Commit}} {{$submoduleFile := $item.SubmoduleFile}} <div class="repo-file-cell name muted-links {{if not $commit}}notready{{end}}"> - {{ctx.RenderUtils.RenderFileIcon $entry}} + {{index $.FileIcons $entry.Name}} {{if $entry.IsSubModule}} {{$submoduleLink := $submoduleFile.SubmoduleWebLink ctx}} {{if $submoduleLink}} diff --git a/web_src/js/components/ViewFileTree.vue b/web_src/js/components/ViewFileTree.vue index 1820c47e7a..c692142792 100644 --- a/web_src/js/components/ViewFileTree.vue +++ b/web_src/js/components/ViewFileTree.vue @@ -3,6 +3,7 @@ import ViewFileTreeItem from './ViewFileTreeItem.vue'; import {onMounted, ref} from 'vue'; import {pathEscapeSegments} from '../utils/url.ts'; import {GET} from '../modules/fetch.ts'; +import {createElementFromHTML} from '../utils/dom.ts'; const elRoot = ref<HTMLElement | null>(null); @@ -18,6 +19,15 @@ const selectedItem = ref(''); async function loadChildren(treePath: string, subPath: string = '') { const response = await GET(`${props.repoLink}/tree-view/${props.currentRefNameSubURL}/${pathEscapeSegments(treePath)}?sub_path=${encodeURIComponent(subPath)}`); const json = await response.json(); + const poolSvgs = []; + for (const [svgId, svgContent] of Object.entries(json.renderedIconPool ?? {})) { + if (!document.querySelector(`.global-svg-icon-pool #${svgId}`)) poolSvgs.push(svgContent); + } + if (poolSvgs.length) { + const svgContainer = createElementFromHTML('<div class="global-svg-icon-pool tw-hidden"></div>'); + svgContainer.innerHTML = poolSvgs.join(''); + document.body.append(svgContainer); + } return json.fileTreeNodes ?? null; } diff --git a/web_src/js/components/ViewFileTreeItem.vue b/web_src/js/components/ViewFileTreeItem.vue index 4dffc86a1b..69e26dbc33 100644 --- a/web_src/js/components/ViewFileTreeItem.vue +++ b/web_src/js/components/ViewFileTreeItem.vue @@ -5,6 +5,8 @@ import {ref} from 'vue'; type Item = { entryName: string; entryMode: string; + entryIcon: string; + entryIconOpen: string; fullPath: string; submoduleUrl?: string; children?: Item[]; @@ -80,7 +82,8 @@ const doGotoSubModule = () => { > <!-- file --> <div class="item-content"> - <SvgIcon name="octicon-file"/> + <!-- eslint-disable-next-line vue/no-v-html --> + <span v-html="item.entryIcon"/> <span class="gt-ellipsis tw-flex-1">{{ item.entryName }}</span> </div> </div> @@ -92,11 +95,13 @@ const doGotoSubModule = () => { > <!-- directory --> <div class="item-toggle"> + <!-- FIXME: use a general and global class for this animation --> <SvgIcon v-if="isLoading" name="octicon-sync" class="job-status-rotate"/> <SvgIcon v-else :name="collapsed ? 'octicon-chevron-right' : 'octicon-chevron-down'" @click.stop="doLoadChildren"/> </div> <div class="item-content"> - <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="text primary" v-html="(!collapsed && item.entryIconOpen) ? item.entryIconOpen : item.entryIcon"/> <span class="gt-ellipsis">{{ item.entryName }}</span> </div> </div>