diff options
author | sebastian-sauer <sauer.sebastian@gmail.com> | 2022-09-27 07:22:19 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-09-27 13:22:19 +0800 |
commit | 31f934c1d8005cdd87c47ce104d00c0efaec374b (patch) | |
tree | a71a387a73fd9a5506e539ad491982dd0a6e0756 /web_src/js/components | |
parent | 525751243efbaed86d6992ca6c7a7e4be229841b (diff) | |
download | gitea-31f934c1d8005cdd87c47ce104d00c0efaec374b.tar.gz gitea-31f934c1d8005cdd87c47ce104d00c0efaec374b.zip |
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>
Diffstat (limited to 'web_src/js/components')
-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 |
3 files changed, 360 insertions, 0 deletions
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> |