diff options
-rw-r--r-- | templates/repo/commit_page.tmpl | 2 | ||||
-rw-r--r-- | templates/repo/diff/box.tmpl | 269 | ||||
-rw-r--r-- | templates/repo/diff/compare.tmpl | 2 | ||||
-rw-r--r-- | templates/repo/diff/options_dropdown.tmpl | 2 | ||||
-rw-r--r-- | templates/repo/pulls/files.tmpl | 2 | ||||
-rw-r--r-- | web_src/js/components/DiffFileList.vue | 81 | ||||
-rw-r--r-- | web_src/js/components/DiffFileTree.vue | 129 | ||||
-rw-r--r-- | web_src/js/components/DiffFileTreeItem.vue | 150 | ||||
-rw-r--r-- | web_src/js/features/repo-diff-filetree.js | 21 | ||||
-rw-r--r-- | web_src/js/features/repo-diff.js | 43 | ||||
-rw-r--r-- | web_src/js/index.js | 2 | ||||
-rw-r--r-- | web_src/js/svg.js | 16 | ||||
-rw-r--r-- | web_src/less/_repository.less | 29 |
13 files changed, 590 insertions, 158 deletions
diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl index 8ece768832..da37aaa9b3 100644 --- a/templates/repo/commit_page.tmpl +++ b/templates/repo/commit_page.tmpl @@ -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")}} diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl index 51769eb953..4624a53ba3 100644 --- a/templates/repo/diff/box.tmpl +++ b/templates/repo/diff/box.tmpl @@ -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}} diff --git a/templates/repo/diff/compare.tmpl b/templates/repo/diff/compare.tmpl index 029e7717a4..e0e6837203 100644 --- a/templates/repo/diff/compare.tmpl +++ b/templates/repo/diff/compare.tmpl @@ -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)}} diff --git a/templates/repo/diff/options_dropdown.tmpl b/templates/repo/diff/options_dropdown.tmpl index 18fa100dcf..2cb5abcb36 100644 --- a/templates/repo/diff/options_dropdown.tmpl +++ b/templates/repo/diff/options_dropdown.tmpl @@ -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> diff --git a/templates/repo/pulls/files.tmpl b/templates/repo/pulls/files.tmpl index 9b24000002..7b72f7f815 100644 --- a/templates/repo/pulls/files.tmpl +++ b/templates/repo/pulls/files.tmpl @@ -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"> diff --git a/web_src/js/components/DiffFileList.vue b/web_src/js/components/DiffFileList.vue new file mode 100644 index 0000000000..2a9aa77377 --- /dev/null +++ b/web_src/js/components/DiffFileList.vue @@ -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> diff --git a/web_src/js/components/DiffFileTree.vue b/web_src/js/components/DiffFileTree.vue new file mode 100644 index 0000000000..d0962254cd --- /dev/null +++ b/web_src/js/components/DiffFileTree.vue @@ -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> diff --git a/web_src/js/components/DiffFileTreeItem.vue b/web_src/js/components/DiffFileTreeItem.vue new file mode 100644 index 0000000000..4f20f1e66a --- /dev/null +++ b/web_src/js/components/DiffFileTreeItem.vue @@ -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> diff --git a/web_src/js/features/repo-diff-filetree.js b/web_src/js/features/repo-diff-filetree.js new file mode 100644 index 0000000000..9eba3cf887 --- /dev/null +++ b/web_src/js/features/repo-diff-filetree.js @@ -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); +} diff --git a/web_src/js/features/repo-diff.js b/web_src/js/features/repo-diff.js index 0a59bcf1c2..0fb7ee22d7 100644 --- a/web_src/js/features/repo-diff.js +++ b/web_src/js/features/repo-diff.js @@ -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(() => { diff --git a/web_src/js/index.js b/web_src/js/index.js index bd56fff773..b13ad0e13a 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -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(); diff --git a/web_src/js/svg.js b/web_src/js/svg.js index 9c39852c30..6677bf83cb 100644 --- a/web_src/js/svg.js +++ b/web_src/js/svg.js @@ -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(); diff --git a/web_src/less/_repository.less b/web_src/less/_repository.less index 44e9a0430e..f30bafa4cc 100644 --- a/web_src/less/_repository.less +++ b/web_src/less/_repository.less @@ -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; |