forked from NYANDEV/forgejo
Add filetree on left of diff view (#21012)
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>
This commit is contained in:
parent
525751243e
commit
31f934c1d8
13 changed files with 591 additions and 159 deletions
|
@ -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}}
|
||||
{{else}}
|
||||
{{template "repo/diff/csv_diff" dict "file" . "root" $ "blobBase" $blobBase "blobHead" $blobHead}}
|
||||
{{if $file.IsProtected}}
|
||||
<span class="ui basic label">{{$.locale.Tr "repo.diff.protected"}}</span>
|
||||
{{end}}
|
||||
</table>
|
||||
{{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}}
|
||||
<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}}
|
||||
</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">
|
||||
|
|
81
web_src/js/components/DiffFileList.vue
Normal file
81
web_src/js/components/DiffFileList.vue
Normal file
|
@ -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>
|
129
web_src/js/components/DiffFileTree.vue
Normal file
129
web_src/js/components/DiffFileTree.vue
Normal file
|
@ -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>
|
150
web_src/js/components/DiffFileTreeItem.vue
Normal file
150
web_src/js/components/DiffFileTreeItem.vue
Normal file
|
@ -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>
|
21
web_src/js/features/repo-diff-filetree.js
Normal file
21
web_src/js/features/repo-diff-filetree.js
Normal file
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue