This PR adds a filetree to the left side of the files/diff view. Initially the filetree will not be shown and may be shown via a new "Show file tree" button. Showing and hiding is using the same icon as github. Folders are collapsible. On small devices (max-width 991 PX) the file tree will be hidden. Close #18192 Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>tags/v1.18.0-rc0
@@ -1,7 +1,7 @@ | |||
{{template "base/head" .}} | |||
<div class="page-content repository diff"> | |||
{{template "repo/header" .}} | |||
<div class="ui container {{if .IsSplitStyle}}fluid padded{{end}}"> | |||
<div class="ui container fluid padded"> | |||
{{$class := ""}} | |||
{{if .Commit.Signature}} | |||
{{$class = (printf "%s%s" $class " isSigned")}} |
@@ -14,6 +14,11 @@ | |||
{{else}} | |||
<div> | |||
<div class="diff-detail-box diff-box sticky df sb ac fw"> | |||
<a class="diff-toggle-file-tree-button"> | |||
{{/* the icon meaning is reversed here, "octicon-sidebar-collapse" means show the file tree */}} | |||
{{svg "octicon-sidebar-collapse" 16 "icon hide"}} | |||
{{svg "octicon-sidebar-expand" 16 "icon"}} | |||
</a> | |||
<div class="diff-detail-stats df ac"> | |||
{{svg "octicon-diff" 16 "mr-2"}}{{.locale.Tr "repo.diff.stats_desc" .Diff.NumFiles .Diff.TotalAddition .Diff.TotalDeletion | Str2html}} | |||
</div> | |||
@@ -31,145 +36,151 @@ | |||
{{end}} | |||
</div> | |||
</div> | |||
<ol class="diff-detail-box diff-stats m-0 hide" id="diff-files"> | |||
{{range .Diff.Files}} | |||
<li> | |||
<div class="bold df ac pull-right"> | |||
{{if .IsBin}} | |||
<span class="ml-1 mr-3"> | |||
{{$.locale.Tr "repo.diff.bin"}} | |||
</span> | |||
{{else}} | |||
{{template "repo/diff/stats" dict "file" . "root" $}} | |||
{{end}} | |||
</div> | |||
<!-- todo finish all file status, now modify, add, delete and rename --> | |||
<span class="status {{DiffTypeToStr .GetType}} tooltip" data-content="{{DiffTypeToStr .GetType}}" data-position="right center"> </span> | |||
<a class="file mono" href="#diff-{{.NameHash}}">{{.Name}}</a> | |||
</li> | |||
{{end}} | |||
{{if .Diff.IsIncomplete}} | |||
<li id="diff-too-many-files-stats" class="pt-2"> | |||
<span class="file df ac sb">{{$.locale.Tr "repo.diff.too_many_files"}} | |||
<a class="ui basic tiny button" id="diff-show-more-files-stats" data-href="{{$.Link}}?skip-to={{.Diff.End}}&file-only=true">{{.locale.Tr "repo.diff.show_more"}}</a> | |||
</span> | |||
</li> | |||
{{end}} | |||
</ol> | |||
<div id="diff-file-boxes"> | |||
{{range $file := .Diff.Files}} | |||
{{/*notice: the index of Diff.Files should not be used for element ID, because the index will be restarted from 0 when doing load-more for PRs with a lot of files*/}} | |||
{{$blobBase := call $.GetBlobByPathForCommit $.BaseCommit $file.OldName}} | |||
{{$blobHead := call $.GetBlobByPathForCommit $.HeadCommit $file.Name}} | |||
{{$isImage := or (call $.IsBlobAnImage $blobBase) (call $.IsBlobAnImage $blobHead)}} | |||
{{$isCsv := (call $.IsCsvFile $file)}} | |||
{{$showFileViewToggle := or $isImage (and (not $file.IsIncomplete) $isCsv)}} | |||
<div class="diff-file-box diff-box file-content {{TabSizeClass $.Editorconfig $file.Name}} mt-3" id="diff-{{$file.NameHash}}" data-old-filename="{{$file.OldName}}" data-new-filename="{{$file.Name}}" {{if $file.ShouldBeHidden}}data-folded="true"{{end}}> | |||
<h4 class="diff-file-header sticky-2nd-row ui top attached normal header df ac sb"> | |||
<div class="df ac"> | |||
<a role="button" class="fold-file muted mr-2"> | |||
{{if $file.ShouldBeHidden}} | |||
{{svg "octicon-chevron-right" 18}} | |||
{{else}} | |||
{{svg "octicon-chevron-down" 18}} | |||
{{end}} | |||
</a> | |||
<div class="bold df ac"> | |||
{{if $file.IsBin}} | |||
<span class="ml-1 mr-3"> | |||
{{$.locale.Tr "repo.diff.bin"}} | |||
</span> | |||
{{else}} | |||
{{template "repo/diff/stats" dict "file" . "root" $}} | |||
{{end}} | |||
</div> | |||
<span class="file mono"><a class="muted" href="#diff-{{$file.NameHash}}">{{if $file.IsRenamed}}{{$file.OldName}} → {{end}}{{$file.Name}}</a>{{if .IsLFSFile}} ({{$.locale.Tr "repo.stored_lfs"}}){{end}}</span> | |||
{{if $file.IsGenerated}} | |||
<span class="ui label ml-3">{{$.locale.Tr "repo.diff.generated"}}</span> | |||
{{end}} | |||
{{if $file.IsVendored}} | |||
<span class="ui label ml-3">{{$.locale.Tr "repo.diff.vendored"}}</span> | |||
{{end}} | |||
</div> | |||
<div class="diff-file-header-actions df ac"> | |||
{{if $showFileViewToggle}} | |||
<div class="ui compact icon buttons"> | |||
<span class="ui tiny basic button tooltip file-view-toggle" data-toggle-selector="#diff-source-{{$file.NameHash}}" data-content="{{$.locale.Tr "repo.file_view_source"}}" data-position="bottom center">{{svg "octicon-code"}}</span> | |||
<span class="ui tiny basic button tooltip file-view-toggle active" data-toggle-selector="#diff-rendered-{{$file.NameHash}}" data-content="{{$.locale.Tr "repo.file_view_rendered"}}" data-position="bottom center">{{svg "octicon-file"}}</span> | |||
</div> | |||
{{end}} | |||
{{if $file.IsProtected}} | |||
<span class="ui basic label">{{$.locale.Tr "repo.diff.protected"}}</span> | |||
{{end}} | |||
{{if not (or $file.IsIncomplete $file.IsBin $file.IsSubmodule)}} | |||
<a class="ui basic tiny button unescape-button">{{$.locale.Tr "repo.unescape_control_characters"}}</a> | |||
<a class="ui basic tiny button escape-button" style="display: none;">{{$.locale.Tr "repo.escape_control_characters"}}</a> | |||
{{end}} | |||
{{if and (not $file.IsSubmodule) (not $.PageIsWiki)}} | |||
{{if $file.IsDeleted}} | |||
<a class="ui basic tiny button" rel="nofollow" href="{{$.BeforeSourcePath}}/{{PathEscapeSegments .Name}}">{{$.locale.Tr "repo.diff.view_file"}}</a> | |||
{{else}} | |||
<a class="ui basic tiny button" rel="nofollow" href="{{$.SourcePath}}/{{PathEscapeSegments .Name}}">{{$.locale.Tr "repo.diff.view_file"}}</a> | |||
{{end}} | |||
{{end}} | |||
{{if and $.IsSigned $.PageIsPullFiles (not $.IsArchived)}} | |||
{{if $file.HasChangedSinceLastReview}} | |||
<span class="changed-since-last-review unselectable">{{$.locale.Tr "repo.pulls.has_changed_since_last_review"}}</span> | |||
{{end}} | |||
<label data-link="{{$.Issue.Link}}/viewed-files" data-headcommit="{{$.PullHeadCommitID}}" class="viewed-file-form unselectable{{if $file.IsViewed}} viewed-file-checked-form{{end}}"> | |||
<input type="checkbox" name="{{$file.GetDiffFileName}}" autocomplete="off"{{if $file.IsViewed}} checked{{end}}> {{$.locale.Tr "repo.pulls.has_viewed_file"}} | |||
</label> | |||
{{end}} | |||
</div> | |||
</h4> | |||
<div class="diff-file-body ui attached unstackable table segment" {{if $file.IsViewed}}data-folded="true"{{end}}> | |||
<div id="diff-source-{{$file.NameHash}}" class="file-body file-code unicode-escaped code-diff{{if $.IsSplitStyle}} code-diff-split{{else}} code-diff-unified{{end}}{{if $showFileViewToggle}} hide{{end}}"> | |||
{{if or $file.IsIncomplete $file.IsBin}} | |||
<div class="diff-file-body binary" style="padding: 5px 10px;"> | |||
{{if $file.IsIncomplete}} | |||
{{if $file.IsIncompleteLineTooLong}} | |||
{{$.locale.Tr "repo.diff.file_suppressed_line_too_long"}} | |||
<script id="diff-data-script"> | |||
(() => { | |||
const diffData = { | |||
files: [{{range $i, $file := .Diff.Files}}{Name:"{{$file.Name}}",NameHash:"{{$file.NameHash}}",Type:{{$file.Type}},IsBin:{{$file.IsBin}},Addition:{{$file.Addition}},Deletion:{{$file.Deletion}}},{{end}}], | |||
isIncomplete: {{.Diff.IsIncomplete}}, | |||
tooManyFilesMessage: "{{$.locale.Tr "repo.diff.too_many_files"}}", | |||
binaryFileMessage: "{{$.locale.Tr "repo.diff.bin"}}", | |||
showMoreMessage: "{{.locale.Tr "repo.diff.show_more"}}", | |||
statisticsMessage: "{{.locale.Tr "repo.diff.stats_desc_file"}}", | |||
fileTreeIsVisible: false, | |||
fileListIsVisible: false, | |||
isLoadingNewData: false, | |||
diffEnd: {{.Diff.End}}, | |||
link: "{{$.Link}}" | |||
}; | |||
if(window.config.pageData.diffFileInfo) { | |||
// Page is already loaded - add the data to our existing data | |||
window.config.pageData.diffFileInfo.files.push(...diffData.files); | |||
window.config.pageData.diffFileInfo.isIncomplete = diffData.isIncomplete; | |||
window.config.pageData.diffFileInfo.diffEnd = diffData.diffEnd; | |||
window.config.pageData.diffFileInfo.link = diffData.link; | |||
} else { | |||
// new load of page - populate initial data | |||
window.config.pageData.diffFileInfo = diffData; | |||
} | |||
})(); | |||
</script> | |||
<div id="diff-file-list-container"></div> | |||
<div id="diff-container"> | |||
<div id="diff-file-tree-container"></div> | |||
<div id="diff-file-boxes" class="sixteen wide column"> | |||
{{range $i, $file := .Diff.Files}} | |||
{{/*notice: the index of Diff.Files should not be used for element ID, because the index will be restarted from 0 when doing load-more for PRs with a lot of files*/}} | |||
{{$blobBase := call $.GetBlobByPathForCommit $.BaseCommit $file.OldName}} | |||
{{$blobHead := call $.GetBlobByPathForCommit $.HeadCommit $file.Name}} | |||
{{$isImage := or (call $.IsBlobAnImage $blobBase) (call $.IsBlobAnImage $blobHead)}} | |||
{{$isCsv := (call $.IsCsvFile $file)}} | |||
{{$showFileViewToggle := or $isImage (and (not $file.IsIncomplete) $isCsv)}} | |||
<div class="diff-file-box diff-box file-content {{TabSizeClass $.Editorconfig $file.Name}} mt-3" id="diff-{{$file.NameHash}}" data-old-filename="{{$file.OldName}}" data-new-filename="{{$file.Name}}" {{if $file.ShouldBeHidden}}data-folded="true"{{end}}> | |||
<h4 class="diff-file-header sticky-2nd-row ui top attached normal header df ac sb"> | |||
<div class="df ac"> | |||
<a role="button" class="fold-file muted mr-2"> | |||
{{if $file.ShouldBeHidden}} | |||
{{svg "octicon-chevron-right" 18}} | |||
{{else}} | |||
{{$.locale.Tr "repo.diff.file_suppressed"}} | |||
<a class="ui basic tiny button diff-show-more-button" data-href="{{$.Link}}?file-only=true&files={{$file.Name}}&files={{$file.OldName}}">{{$.locale.Tr "repo.diff.load"}}</a> | |||
{{svg "octicon-chevron-down" 18}} | |||
{{end}} | |||
{{else}} | |||
{{$.locale.Tr "repo.diff.bin_not_shown"}} | |||
</a> | |||
<div class="bold df ac"> | |||
{{if $file.IsBin}} | |||
<span class="ml-1 mr-3"> | |||
{{$.locale.Tr "repo.diff.bin"}} | |||
</span> | |||
{{else}} | |||
{{template "repo/diff/stats" dict "file" . "root" $}} | |||
{{end}} | |||
</div> | |||
<span class="file mono"><a class="muted" href="#diff-{{$file.NameHash}}">{{if $file.IsRenamed}}{{$file.OldName}} → {{end}}{{$file.Name}}</a>{{if .IsLFSFile}} ({{$.locale.Tr "repo.stored_lfs"}}){{end}}</span> | |||
{{if $file.IsGenerated}} | |||
<span class="ui label ml-3">{{$.locale.Tr "repo.diff.generated"}}</span> | |||
{{end}} | |||
{{if $file.IsVendored}} | |||
<span class="ui label ml-3">{{$.locale.Tr "repo.diff.vendored"}}</span> | |||
{{end}} | |||
</div> | |||
{{else}} | |||
<table class="chroma" data-new-comment-url="{{$.Issue.HTMLURL}}/files/reviews/new_comment" data-path="{{$file.Name}}"> | |||
{{if $.IsSplitStyle}} | |||
{{template "repo/diff/section_split" dict "file" . "root" $}} | |||
{{else}} | |||
{{template "repo/diff/section_unified" dict "file" . "root" $}} | |||
<div class="diff-file-header-actions df ac"> | |||
{{if $showFileViewToggle}} | |||
<div class="ui compact icon buttons"> | |||
<span class="ui tiny basic button tooltip file-view-toggle" data-toggle-selector="#diff-source-{{$file.NameHash}}" data-content="{{$.locale.Tr "repo.file_view_source"}}" data-position="bottom center">{{svg "octicon-code"}}</span> | |||
<span class="ui tiny basic button tooltip file-view-toggle active" data-toggle-selector="#diff-rendered-{{$file.NameHash}}" data-content="{{$.locale.Tr "repo.file_view_rendered"}}" data-position="bottom center">{{svg "octicon-file"}}</span> | |||
</div> | |||
{{end}} | |||
</table> | |||
{{end}} | |||
</div> | |||
{{if $showFileViewToggle}} | |||
<div id="diff-rendered-{{$file.NameHash}}" class="file-body file-code {{if $.IsSplitStyle}} code-diff-split{{else}} code-diff-unified{{end}}"> | |||
<table class="chroma w-100"> | |||
{{if $isImage}} | |||
{{template "repo/diff/image_diff" dict "file" . "root" $ "blobBase" $blobBase "blobHead" $blobHead}} | |||
{{if $file.IsProtected}} | |||
<span class="ui basic label">{{$.locale.Tr "repo.diff.protected"}}</span> | |||
{{end}} | |||
{{if not (or $file.IsIncomplete $file.IsBin $file.IsSubmodule)}} | |||
<a class="ui basic tiny button unescape-button">{{$.locale.Tr "repo.unescape_control_characters"}}</a> | |||
<a class="ui basic tiny button escape-button" style="display: none;">{{$.locale.Tr "repo.escape_control_characters"}}</a> | |||
{{end}} | |||
{{if and (not $file.IsSubmodule) (not $.PageIsWiki)}} | |||
{{if $file.IsDeleted}} | |||
<a class="ui basic tiny button" rel="nofollow" href="{{$.BeforeSourcePath}}/{{PathEscapeSegments .Name}}">{{$.locale.Tr "repo.diff.view_file"}}</a> | |||
{{else}} | |||
<a class="ui basic tiny button" rel="nofollow" href="{{$.SourcePath}}/{{PathEscapeSegments .Name}}">{{$.locale.Tr "repo.diff.view_file"}}</a> | |||
{{end}} | |||
{{end}} | |||
{{if and $.IsSigned $.PageIsPullFiles (not $.IsArchived)}} | |||
{{if $file.HasChangedSinceLastReview}} | |||
<span class="changed-since-last-review unselectable">{{$.locale.Tr "repo.pulls.has_changed_since_last_review"}}</span> | |||
{{end}} | |||
<label data-link="{{$.Issue.Link}}/viewed-files" data-headcommit="{{$.PullHeadCommitID}}" class="viewed-file-form unselectable{{if $file.IsViewed}} viewed-file-checked-form{{end}}"> | |||
<input type="checkbox" name="{{$file.GetDiffFileName}}" autocomplete="off"{{if $file.IsViewed}} checked{{end}}> {{$.locale.Tr "repo.pulls.has_viewed_file"}} | |||
</label> | |||
{{end}} | |||
</div> | |||
</h4> | |||
<div class="diff-file-body ui attached unstackable table segment" {{if $file.IsViewed}}data-folded="true"{{end}}> | |||
<div id="diff-source-{{$file.NameHash}}" class="file-body file-code unicode-escaped code-diff{{if $.IsSplitStyle}} code-diff-split{{else}} code-diff-unified{{end}}{{if $showFileViewToggle}} hide{{end}}"> | |||
{{if or $file.IsIncomplete $file.IsBin}} | |||
<div class="diff-file-body binary" style="padding: 5px 10px;"> | |||
{{if $file.IsIncomplete}} | |||
{{if $file.IsIncompleteLineTooLong}} | |||
{{$.locale.Tr "repo.diff.file_suppressed_line_too_long"}} | |||
{{else}} | |||
{{$.locale.Tr "repo.diff.file_suppressed"}} | |||
<a class="ui basic tiny button diff-show-more-button" data-href="{{$.Link}}?file-only=true&files={{$file.Name}}&files={{$file.OldName}}">{{$.locale.Tr "repo.diff.load"}}</a> | |||
{{end}} | |||
{{else}} | |||
{{$.locale.Tr "repo.diff.bin_not_shown"}} | |||
{{end}} | |||
</div> | |||
{{else}} | |||
{{template "repo/diff/csv_diff" dict "file" . "root" $ "blobBase" $blobBase "blobHead" $blobHead}} | |||
<table class="chroma" data-new-comment-url="{{$.Issue.HTMLURL}}/files/reviews/new_comment" data-path="{{$file.Name}}"> | |||
{{if $.IsSplitStyle}} | |||
{{template "repo/diff/section_split" dict "file" . "root" $}} | |||
{{else}} | |||
{{template "repo/diff/section_unified" dict "file" . "root" $}} | |||
{{end}} | |||
</table> | |||
{{end}} | |||
</table> | |||
</div> | |||
{{if $showFileViewToggle}} | |||
<div id="diff-rendered-{{$file.NameHash}}" class="file-body file-code {{if $.IsSplitStyle}} code-diff-split{{else}} code-diff-unified{{end}}"> | |||
<table class="chroma w-100"> | |||
{{if $isImage}} | |||
{{template "repo/diff/image_diff" dict "file" . "root" $ "blobBase" $blobBase "blobHead" $blobHead}} | |||
{{else}} | |||
{{template "repo/diff/csv_diff" dict "file" . "root" $ "blobBase" $blobBase "blobHead" $blobHead}} | |||
{{end}} | |||
</table> | |||
</div> | |||
{{end}} | |||
</div> | |||
{{end}} | |||
</div> | |||
</div> | |||
{{end}} | |||
</div> | |||
{{end}} | |||
{{if .Diff.IsIncomplete}} | |||
<div class="diff-file-box diff-box file-content mt-3" id="diff-incomplete"> | |||
<h4 class="ui top attached normal header df ac sb"> | |||
{{$.locale.Tr "repo.diff.too_many_files"}} | |||
<a class="ui basic tiny button" id="diff-show-more-files" data-href="{{$.Link}}?skip-to={{.Diff.End}}&file-only=true">{{.locale.Tr "repo.diff.show_more"}}</a> | |||
</h4> | |||
{{if .Diff.IsIncomplete}} | |||
<div class="diff-file-box diff-box file-content mt-3" id="diff-incomplete"> | |||
<h4 class="ui top attached normal header df ac sb"> | |||
{{$.locale.Tr "repo.diff.too_many_files"}} | |||
<a class="ui basic tiny button" id="diff-show-more-files" data-href="{{$.Link}}?skip-to={{.Diff.End}}&file-only=true">{{.locale.Tr "repo.diff.show_more"}}</a> | |||
</h4> | |||
</div> | |||
{{end}} | |||
</div> | |||
{{end}} | |||
</div> | |||
{{if not $.Repository.IsArchived}} |
@@ -1,7 +1,7 @@ | |||
{{template "base/head" .}} | |||
<div class="page-content repository diff {{if .PageIsComparePull}}compare pull{{end}}"> | |||
{{template "repo/header" .}} | |||
<div class="ui container {{if .IsSplitStyle}}fluid padded{{end}}"> | |||
<div class="ui container fluid padded"> | |||
<h2 class="ui header"> | |||
{{if and $.PageIsComparePull $.IsSigned (not .Repository.IsArchived)}} |
@@ -1,7 +1,7 @@ | |||
<div class="ui dropdown tiny basic button icon-button tooltip" data-content="{{.locale.Tr "repo.diff.options_button"}}"> | |||
{{svg "octicon-kebab-horizontal"}} | |||
<div class="menu"> | |||
<a class="item tiny basic toggle button" data-target="#diff-files">{{.locale.Tr "repo.diff.show_diff_stats"}}</a> | |||
<a class="item tiny basic toggle button" id="show-file-list-btn">{{.locale.Tr "repo.diff.show_diff_stats"}}</a> | |||
{{if .Issue.Index}} | |||
<a class="item" href="{{$.RepoLink}}/pulls/{{.Issue.Index}}.patch" download="{{.Issue.Index}}.patch">{{.locale.Tr "repo.diff.download_patch"}}</a> | |||
<a class="item" href="{{$.RepoLink}}/pulls/{{.Issue.Index}}.diff" download="{{.Issue.Index}}.diff">{{.locale.Tr "repo.diff.download_diff"}}</a> |
@@ -5,7 +5,7 @@ | |||
<div class="page-content repository view issue pull files diff"> | |||
{{template "repo/header" .}} | |||
<div class="ui container {{if .IsSplitStyle}}fluid padded{{end}}"> | |||
<div class="ui container fluid padded"> | |||
<div class="navbar"> | |||
{{template "repo/issue/navbar" .}} | |||
<div class="ui right"> |
@@ -0,0 +1,81 @@ | |||
<template> | |||
<ol class="diff-detail-box diff-stats m-0" id="diff-files" v-if="fileListIsVisible"> | |||
<li v-for="file in files" :key="file.NameHash"> | |||
<div class="bold df ac pull-right"> | |||
<span v-if="file.IsBin" class="ml-1 mr-3">{{ binaryFileMessage }}</span> | |||
{{ file.IsBin ? '' : file.Addition + file.Deletion }} | |||
<span v-if="!file.IsBin" class="diff-stats-bar tooltip mx-3" :data-content="statisticsMessage.replace('%d', (file.Addition + file.Deletion)).replace('%d', file.Addition).replace('%d', file.Deletion)"> | |||
<div class="diff-stats-add-bar" :style="{ 'width': diffStatsWidth(file.Addition, file.Deletion) }" /> | |||
</span> | |||
</div> | |||
<!-- todo finish all file status, now modify, add, delete and rename --> | |||
<span :class="['status', diffTypeToString(file.Type), 'tooltip']" :data-content="diffTypeToString(file.Type)" data-position="right center"> </span> | |||
<a class="file mono" :href="'#diff-' + file.NameHash">{{ file.Name }}</a> | |||
</li> | |||
<li v-if="isIncomplete" id="diff-too-many-files-stats" class="pt-2"> | |||
<span class="file df ac sb">{{ tooManyFilesMessage }} | |||
<a :class="['ui', 'basic', 'tiny', 'button', isLoadingNewData === true ? 'disabled' : '']" id="diff-show-more-files-stats" @click.stop="loadMoreData">{{ showMoreMessage }}</a> | |||
</span> | |||
</li> | |||
</ol> | |||
</template> | |||
<script> | |||
import {initTooltip} from '../modules/tippy.js'; | |||
import {doLoadMoreFiles} from '../features/repo-diff.js'; | |||
const {pageData} = window.config; | |||
export default { | |||
name: 'DiffFileList', | |||
data: () => { | |||
return pageData.diffFileInfo; | |||
}, | |||
watch: { | |||
fileListIsVisible(newValue) { | |||
if (newValue === true) { | |||
this.$nextTick(() => { | |||
for (const el of this.$el.querySelectorAll('.tooltip')) { | |||
initTooltip(el); | |||
} | |||
}); | |||
} | |||
} | |||
}, | |||
mounted() { | |||
document.getElementById('show-file-list-btn').addEventListener('click', this.toggleFileList); | |||
}, | |||
unmounted() { | |||
document.getElementById('show-file-list-btn').removeEventListener('click', this.toggleFileList); | |||
}, | |||
methods: { | |||
toggleFileList() { | |||
this.fileListIsVisible = !this.fileListIsVisible; | |||
}, | |||
diffTypeToString(pType) { | |||
const diffTypes = { | |||
1: 'add', | |||
2: 'modify', | |||
3: 'del', | |||
4: 'rename', | |||
5: 'copy', | |||
}; | |||
return diffTypes[pType]; | |||
}, | |||
diffStatsWidth(adds, dels) { | |||
return `${adds / (adds + dels) * 100}%`; | |||
}, | |||
loadMoreData() { | |||
this.isLoadingNewData = true; | |||
doLoadMoreFiles(this.link, this.diffEnd, () => { | |||
this.isLoadingNewData = false; | |||
}); | |||
} | |||
}, | |||
}; | |||
</script> |
@@ -0,0 +1,129 @@ | |||
<template> | |||
<div | |||
v-show="fileTreeIsVisible" | |||
id="diff-file-tree" | |||
class="mr-3 mt-3 diff-detail-box" | |||
> | |||
<!-- only render the tree if we're visible. in many cases this is something that doesn't change very often --> | |||
<div class="ui list" v-if="fileTreeIsVisible"> | |||
<DiffFileTreeItem v-for="item in fileTree" :key="item.name" :item="item" /> | |||
</div> | |||
<div v-if="isIncomplete" id="diff-too-many-files-stats" class="pt-2"> | |||
<span>{{ tooManyFilesMessage }}</span><a :class="['ui', 'basic', 'tiny', 'button', isLoadingNewData === true ? 'disabled' : '']" id="diff-show-more-files-stats" @click.stop="loadMoreData">{{ showMoreMessage }}</a> | |||
</div> | |||
</div> | |||
</template> | |||
<script> | |||
import DiffFileTreeItem from './DiffFileTreeItem.vue'; | |||
import {doLoadMoreFiles} from '../features/repo-diff.js'; | |||
const {pageData} = window.config; | |||
const LOCAL_STORAGE_KEY = 'diff_file_tree_visible'; | |||
export default { | |||
name: 'DiffFileTree', | |||
components: {DiffFileTreeItem}, | |||
data: () => { | |||
const fileTreeIsVisible = localStorage.getItem(LOCAL_STORAGE_KEY) === 'true'; | |||
pageData.diffFileInfo.fileTreeIsVisible = fileTreeIsVisible; | |||
return pageData.diffFileInfo; | |||
}, | |||
computed: { | |||
fileTree() { | |||
const result = []; | |||
for (const file of this.files) { | |||
// Split file into directories | |||
const splits = file.Name.split('/'); | |||
let index = 0; | |||
let parent = null; | |||
let isFile = false; | |||
for (const split of splits) { | |||
index += 1; | |||
// reached the end | |||
if (index === splits.length) { | |||
isFile = true; | |||
} | |||
let newParent = { | |||
name: split, | |||
children: [], | |||
isFile | |||
}; | |||
if (isFile === true) { | |||
newParent.file = file; | |||
} | |||
if (parent) { | |||
// check if the folder already exists | |||
const existingFolder = parent.children.find( | |||
(x) => x.name === split | |||
); | |||
if (existingFolder) { | |||
newParent = existingFolder; | |||
} else { | |||
parent.children.push(newParent); | |||
} | |||
} else { | |||
const existingFolder = result.find((x) => x.name === split); | |||
if (existingFolder) { | |||
newParent = existingFolder; | |||
} else { | |||
result.push(newParent); | |||
} | |||
} | |||
parent = newParent; | |||
} | |||
} | |||
const mergeChildIfOnlyOneDir = (entries) => { | |||
for (const entry of entries) { | |||
if (entry.children) { | |||
mergeChildIfOnlyOneDir(entry.children); | |||
} | |||
if (entry.children.length === 1 && entry.children[0].isFile === false) { | |||
// Merge it to the parent | |||
entry.name = `${entry.name}/${entry.children[0].name}`; | |||
entry.children = entry.children[0].children; | |||
} | |||
} | |||
}; | |||
// Merge folders with just a folder as children in order to | |||
// reduce the depth of our tree. | |||
mergeChildIfOnlyOneDir(result); | |||
return result; | |||
} | |||
}, | |||
mounted() { | |||
// ensure correct buttons when we are mounted to the dom | |||
this.adjustToggleButton(this.fileTreeIsVisible); | |||
document.querySelector('.diff-toggle-file-tree-button').addEventListener('click', this.toggleVisibility); | |||
}, | |||
unmounted() { | |||
document.querySelector('.diff-toggle-file-tree-button').removeEventListener('click', this.toggleVisibility); | |||
}, | |||
methods: { | |||
toggleVisibility() { | |||
this.updateVisibility(!this.fileTreeIsVisible); | |||
}, | |||
updateVisibility(visible) { | |||
this.fileTreeIsVisible = visible; | |||
localStorage.setItem(LOCAL_STORAGE_KEY, this.fileTreeIsVisible); | |||
this.adjustToggleButton(this.fileTreeIsVisible); | |||
}, | |||
adjustToggleButton(visible) { | |||
const [toShow, toHide] = document.querySelectorAll('.diff-toggle-file-tree-button .icon'); | |||
toShow.classList.toggle('hide', visible); // hide the toShow icon if the tree is visible | |||
toHide.classList.toggle('hide', !visible); // similarly | |||
}, | |||
loadMoreData() { | |||
this.isLoadingNewData = true; | |||
doLoadMoreFiles(this.link, this.diffEnd, () => { | |||
this.isLoadingNewData = false; | |||
}); | |||
} | |||
}, | |||
}; | |||
</script> |
@@ -0,0 +1,150 @@ | |||
<template> | |||
<div v-show="show"> | |||
<div class="item" :class="item.isFile ? 'filewrapper p-1' : ''"> | |||
<!-- Files --> | |||
<SvgIcon | |||
v-if="item.isFile" | |||
data-position="right center" | |||
name="octicon-file" | |||
class="svg-icon file" | |||
/> | |||
<a | |||
v-if="item.isFile" | |||
class="file ellipsis" | |||
:href="item.isFile ? '#diff-' + item.file.NameHash : ''" | |||
>{{ item.name }}</a> | |||
<SvgIcon | |||
v-if="item.isFile" | |||
data-position="right center" | |||
:name="getIconForDiffType(item.file.Type)" | |||
:class="['svg-icon', getIconForDiffType(item.file.Type), 'status']" | |||
/> | |||
<!-- Directories --> | |||
<div v-if="!item.isFile" class="directory p-1" @click.stop="handleClick(item.isFile)"> | |||
<SvgIcon | |||
class="svg-icon" | |||
:name="collapsed ? 'octicon-chevron-right' : 'octicon-chevron-down'" | |||
/> | |||
<SvgIcon | |||
class="svg-icon directory" | |||
name="octicon-file-directory-fill" | |||
/> | |||
<span class="ellipsis">{{ item.name }}</span> | |||
</div> | |||
<div v-show="!collapsed"> | |||
<DiffFileTreeItem v-for="childItem in item.children" :key="childItem.name" :item="childItem" class="list" /> | |||
</div> | |||
</div> | |||
</div> | |||
</template> | |||
<script> | |||
import {SvgIcon} from '../svg.js'; | |||
export default { | |||
name: 'DiffFileTreeItem', | |||
components: { | |||
SvgIcon, | |||
}, | |||
props: { | |||
item: { | |||
type: Object, | |||
required: true | |||
}, | |||
show: { | |||
type: Boolean, | |||
required: false, | |||
default: true | |||
} | |||
}, | |||
data: () => ({ | |||
collapsed: false, | |||
}), | |||
methods: { | |||
handleClick(itemIsFile) { | |||
if (itemIsFile) { | |||
return; | |||
} | |||
this.$set(this, 'collapsed', !this.collapsed); | |||
}, | |||
getIconForDiffType(pType) { | |||
const diffTypes = { | |||
1: 'octicon-diff-added', | |||
2: 'octicon-diff-modified', | |||
3: 'octicon-diff-removed', | |||
4: 'octicon-diff-renamed', | |||
5: 'octicon-diff-modified', // there is no octicon for copied, so modified should be ok | |||
}; | |||
return diffTypes[pType]; | |||
}, | |||
}, | |||
}; | |||
</script> | |||
<style scoped> | |||
span.svg-icon.status { | |||
float: right; | |||
} | |||
span.svg-icon.file { | |||
color: var(--color-secondary-dark-7); | |||
} | |||
span.svg-icon.directory { | |||
color: var(--color-primary); | |||
} | |||
span.svg-icon.octicon-diff-modified { | |||
color: var(--color-yellow); | |||
} | |||
span.svg-icon.octicon-diff-added { | |||
color: var(--color-green); | |||
} | |||
span.svg-icon.octicon-diff-removed { | |||
color: var(--color-red); | |||
} | |||
span.svg-icon.octicon-diff-renamed { | |||
color: var(--color-teal); | |||
} | |||
.item.filewrapper { | |||
display: grid !important; | |||
grid-template-columns: 20px 7fr 1fr; | |||
padding-left: 18px !important; | |||
} | |||
.item.filewrapper:hover { | |||
color: var(--color-text); | |||
background: var(--color-hover); | |||
border-radius: 4px; | |||
} | |||
div.directory { | |||
display: grid; | |||
grid-template-columns: 18px 20px auto; | |||
} | |||
div.directory:hover { | |||
color: var(--color-text); | |||
background: var(--color-hover); | |||
border-radius: 4px; | |||
} | |||
div.list { | |||
padding-bottom: 0 !important; | |||
padding-top: inherit !important; | |||
} | |||
a { | |||
text-decoration: none; | |||
} | |||
a:hover { | |||
text-decoration: none; | |||
} | |||
</style> |
@@ -0,0 +1,21 @@ | |||
import Vue from 'vue'; | |||
import DiffFileTree from '../components/DiffFileTree.vue'; | |||
import DiffFileList from '../components/DiffFileList.vue'; | |||
export default function initDiffFileTree() { | |||
const el = document.getElementById('diff-file-tree-container'); | |||
if (!el) return; | |||
const View = Vue.extend({ | |||
render: (createElement) => createElement(DiffFileTree), | |||
}); | |||
new View().$mount(el); | |||
const fileListElement = document.getElementById('diff-file-list-container'); | |||
if (!fileListElement) return; | |||
const fileListView = Vue.extend({ | |||
render: (createElement) => createElement(DiffFileList), | |||
}); | |||
new fileListView().$mount(fileListElement); | |||
} |
@@ -68,7 +68,6 @@ export function initRepoDiffConversationForm() { | |||
initCompReactionSelector($newConversationHolder); | |||
}); | |||
$(document).on('click', '.resolve-conversation', async function (e) { | |||
e.preventDefault(); | |||
const comment_id = $(this).data('comment-id'); | |||
@@ -118,32 +117,27 @@ function onShowMoreFiles() { | |||
countAndUpdateViewedFiles(); | |||
} | |||
export function initRepoDiffShowMore() { | |||
$('#diff-files, #diff-file-boxes').on('click', '#diff-show-more-files, #diff-show-more-files-stats', (e) => { | |||
e.preventDefault(); | |||
if ($(e.target).hasClass('disabled')) { | |||
export function doLoadMoreFiles(link, diffEnd, callback) { | |||
const url = `${link}?skip-to=${diffEnd}&file-only=true`; | |||
$.ajax({ | |||
type: 'GET', | |||
url, | |||
}).done((resp) => { | |||
if (!resp) { | |||
callback(resp); | |||
return; | |||
} | |||
$('#diff-show-more-files, #diff-show-more-files-stats').addClass('disabled'); | |||
const url = $('#diff-show-more-files, #diff-show-more-files-stats').data('href'); | |||
$.ajax({ | |||
type: 'GET', | |||
url, | |||
}).done((resp) => { | |||
if (!resp) { | |||
$('#diff-show-more-files, #diff-show-more-files-stats').removeClass('disabled'); | |||
return; | |||
} | |||
$('#diff-too-many-files-stats').remove(); | |||
$('#diff-files').append($(resp).find('#diff-files li')); | |||
$('#diff-incomplete').replaceWith($(resp).find('#diff-file-boxes').children()); | |||
onShowMoreFiles(); | |||
}).fail(() => { | |||
$('#diff-show-more-files, #diff-show-more-files-stats').removeClass('disabled'); | |||
}); | |||
// By simply rerunning the script we add the new data to our existing | |||
// pagedata object. this triggers vue and the filetree and filelist will | |||
// render the new elements. | |||
$('body').append($(resp).find('script#diff-data-script')); | |||
callback(resp); | |||
}).fail(() => { | |||
callback(); | |||
}); | |||
} | |||
export function initRepoDiffShowMore() { | |||
$(document).on('click', 'a.diff-show-more-button', (e) => { | |||
e.preventDefault(); | |||
const $target = $(e.target); | |||
@@ -163,7 +157,6 @@ export function initRepoDiffShowMore() { | |||
$target.removeClass('disabled'); | |||
return; | |||
} | |||
$target.parent().replaceWith($(resp).find('#diff-file-boxes .diff-file-body .file-body').children()); | |||
onShowMoreFiles(); | |||
}).fail(() => { |
@@ -23,6 +23,7 @@ import {initRepoIssueContentHistory} from './features/repo-issue-content.js'; | |||
import {initStopwatch} from './features/stopwatch.js'; | |||
import {initFindFileInRepo} from './features/repo-findfile.js'; | |||
import {initCommentContent, initMarkupContent} from './markup/content.js'; | |||
import initDiffFileTree from './features/repo-diff-filetree.js'; | |||
import {initUserAuthLinkAccountView, initUserAuthOauth2} from './features/user-auth.js'; | |||
import { | |||
@@ -158,6 +159,7 @@ $(document).ready(() => { | |||
initRepoDiffFileViewToggle(); | |||
initRepoDiffReviewButton(); | |||
initRepoDiffShowMore(); | |||
initDiffFileTree(); | |||
initRepoEditor(); | |||
initRepoGraphGit(); | |||
initRepoIssueContentHistory(); |
@@ -2,6 +2,11 @@ import octiconChevronDown from '../../public/img/svg/octicon-chevron-down.svg'; | |||
import octiconChevronRight from '../../public/img/svg/octicon-chevron-right.svg'; | |||
import octiconCopy from '../../public/img/svg/octicon-copy.svg'; | |||
import octiconClock from '../../public/img/svg/octicon-clock.svg'; | |||
import octiconDiffAdded from '../../public/img/svg/octicon-diff-added.svg'; | |||
import octiconDiffModified from '../../public/img/svg/octicon-diff-modified.svg'; | |||
import octiconDiffRemoved from '../../public/img/svg/octicon-diff-removed.svg'; | |||
import octiconDiffRenamed from '../../public/img/svg/octicon-diff-renamed.svg'; | |||
import octiconFileDirectoryFill from '../../public/img/svg/octicon-file-directory-fill.svg'; | |||
import octiconGitMerge from '../../public/img/svg/octicon-git-merge.svg'; | |||
import octiconGitPullRequest from '../../public/img/svg/octicon-git-pull-request.svg'; | |||
import octiconIssueClosed from '../../public/img/svg/octicon-issue-closed.svg'; | |||
@@ -17,6 +22,9 @@ import octiconRepoForked from '../../public/img/svg/octicon-repo-forked.svg'; | |||
import octiconRepoTemplate from '../../public/img/svg/octicon-repo-template.svg'; | |||
import octiconTriangleDown from '../../public/img/svg/octicon-triangle-down.svg'; | |||
import octiconFile from '../../public/img/svg/octicon-file.svg'; | |||
import octiconSidebarExpand from '../../public/img/svg/octicon-sidebar-expand.svg'; | |||
import octiconSidebarCollapse from '../../public/img/svg/octicon-sidebar-collapse.svg'; | |||
import Vue from 'vue'; | |||
@@ -40,8 +48,16 @@ export const svgs = { | |||
'octicon-repo-template': octiconRepoTemplate, | |||
'octicon-triangle-down': octiconTriangleDown, | |||
'octicon-file': octiconFile, | |||
'octicon-file-directory-fill': octiconFileDirectoryFill, | |||
'octicon-sidebar-expand': octiconSidebarExpand, | |||
'octicon-sidebar-collapse': octiconSidebarCollapse, | |||
'octicon-diff-added': octiconDiffAdded, | |||
'octicon-diff-modified': octiconDiffModified, | |||
'octicon-diff-removed': octiconDiffRemoved, | |||
'octicon-diff-renamed': octiconDiffRenamed, | |||
}; | |||
const parser = new DOMParser(); | |||
const serializer = new XMLSerializer(); | |||
@@ -3068,6 +3068,35 @@ td.blob-excerpt { | |||
padding-left: 8px; | |||
} | |||
#diff-container { | |||
display: flex; | |||
} | |||
#diff-file-boxes { | |||
flex: 1; | |||
} | |||
#diff-file-tree { | |||
width: 20%; | |||
max-width: 380px; | |||
line-height: inherit; | |||
position: sticky; | |||
padding-top: 0; | |||
top: 47px; | |||
max-height: calc(100vh - 50px); | |||
height: 100%; | |||
overflow-y: auto; | |||
} | |||
@media @mediaMdAndDown { | |||
#diff-file-tree { | |||
display: none; | |||
} | |||
.diff-toggle-file-tree-button { | |||
display: none; | |||
} | |||
} | |||
.ui.message.unicode-escape-prompt { | |||
margin-bottom: 0; | |||
border-radius: 0; |