mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-04 01:34:27 +00:00 
			
		
		
		
	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:
		
							
								
								
									
										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>
 | 
			
		||||
		Reference in New Issue
	
	Block a user