mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2025-09-02 12:13:48 +00:00
Show changed files as file-tree (#5379)
This commit is contained in:
parent
f6b3e16e4c
commit
c659128f31
3 changed files with 110 additions and 4 deletions
70
web/src/components/FileTree.vue
Normal file
70
web/src/components/FileTree.vue
Normal file
|
@ -0,0 +1,70 @@
|
|||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="group hover:bg-wp-background-200 dark:hover:bg-wp-background-100 flex cursor-pointer items-center rounded-md px-2 py-1.5 transition-all duration-150"
|
||||
:class="{ 'font-medium': node.isDirectory }"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
:aria-expanded="node.isDirectory ? !collapsed : undefined"
|
||||
:aria-label="node.isDirectory ? `${collapsed ? 'Expand' : 'Collapse'} folder ${node.name}` : `File ${node.name}`"
|
||||
@click="collapsed = !collapsed"
|
||||
@keydown.enter="collapsed = !collapsed"
|
||||
@keydown.space="collapsed = !collapsed"
|
||||
>
|
||||
<div class="mr-1 flex w-4 items-center justify-start">
|
||||
<Icon
|
||||
v-if="node.isDirectory"
|
||||
name="chevron-right"
|
||||
class="text-wp-text-alt-100 group-hover:text-wp-text-200 h-6 min-w-6 transition-transform duration-150"
|
||||
:class="{ 'rotate-90 transform': !collapsed }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Icon
|
||||
:name="iconName"
|
||||
class="text-wp-text-alt-100 group-hover:text-wp-text-200 mr-3 transition-colors duration-150"
|
||||
/>
|
||||
|
||||
<span
|
||||
class="text-wp-text-200 group-hover:text-wp-text-100 truncate text-sm transition-colors duration-150"
|
||||
:class="{ 'text-wp-text-100': node.isDirectory }"
|
||||
:title="node.name"
|
||||
>
|
||||
{{ node.name }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="node.isDirectory && !collapsed"
|
||||
class="border-wp-background-300 mt-1 ml-2 border-l pl-1 transition-all duration-200"
|
||||
>
|
||||
<FileTree v-for="child in node.children" :key="child.path" :node="child" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="FileTreeNode">
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import Icon from '~/components/atomic/Icon.vue';
|
||||
|
||||
export interface TreeNode {
|
||||
name: string;
|
||||
path: string;
|
||||
isDirectory: boolean;
|
||||
children: TreeNode[];
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
node: TreeNode;
|
||||
}>();
|
||||
|
||||
const collapsed = ref(false);
|
||||
|
||||
const iconName = computed(() => {
|
||||
if (props.node.isDirectory) {
|
||||
return collapsed.value ? 'folder' : 'folder-open';
|
||||
}
|
||||
return 'file';
|
||||
});
|
||||
</script>
|
|
@ -61,6 +61,9 @@
|
|||
<SvgIcon v-else-if="name === 'tray-full'" :path="mdiTrayFull" size="1.3rem" />
|
||||
<SvgIcon v-else-if="name === 'file-cog-outline'" :path="mdiFileCogOutline" size="1.3rem" />
|
||||
<SvgIcon v-else-if="name === 'file-edit-outline'" :path="mdiFileEditOutline" size="1.3rem" />
|
||||
<SvgIcon v-else-if="name === 'folder'" :path="mdiFolderOutline" size="1.3rem" />
|
||||
<SvgIcon v-else-if="name === 'folder-open'" :path="mdiFolderOpenOutline" size="1.3rem" />
|
||||
<SvgIcon v-else-if="name === 'file'" :path="mdiFileOutline" size="1.3rem" />
|
||||
<SvgIcon v-else-if="name === 'bug-outline'" :path="mdiBugOutline" size="1.3rem" />
|
||||
<SvgIcon v-else-if="name === 'docker'" :path="mdiDocker" size="1.3rem" />
|
||||
<SvgIcon v-else-if="name === 'forge'" :path="mdiCodeBraces" size="1.3rem" />
|
||||
|
@ -124,6 +127,9 @@ import {
|
|||
mdiEyeOutline,
|
||||
mdiFileCogOutline,
|
||||
mdiFileEditOutline,
|
||||
mdiFileOutline,
|
||||
mdiFolderOpenOutline,
|
||||
mdiFolderOutline,
|
||||
mdiFormatListBulleted,
|
||||
mdiFormatListGroup,
|
||||
mdiGestureTap,
|
||||
|
@ -230,7 +236,10 @@ export type IconNames =
|
|||
| 'org'
|
||||
| 'cron'
|
||||
| 'toolbox'
|
||||
| 'forge';
|
||||
| 'forge'
|
||||
| 'folder'
|
||||
| 'folder-open'
|
||||
| 'file';
|
||||
|
||||
const props = defineProps<{
|
||||
name: IconNames;
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<Panel>
|
||||
<ul class="w-full list-inside list-disc">
|
||||
<li v-for="file in pipeline.changed_files" :key="file">{{ file }}</li>
|
||||
</ul>
|
||||
<div class="w-full">
|
||||
<FileTree v-for="node in fileTree" :key="node.name" :node="node" :depth="0" />
|
||||
</div>
|
||||
</Panel>
|
||||
</template>
|
||||
|
||||
|
@ -10,6 +10,8 @@
|
|||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import FileTree from '~/components/FileTree.vue';
|
||||
import type { TreeNode } from '~/components/FileTree.vue';
|
||||
import Panel from '~/components/layout/Panel.vue';
|
||||
import { requiredInject } from '~/compositions/useInjectProvide';
|
||||
import { useWPTitle } from '~/compositions/useWPTitle';
|
||||
|
@ -25,4 +27,29 @@ useWPTitle(
|
|||
repo.value.full_name,
|
||||
]),
|
||||
);
|
||||
|
||||
const fileTree = computed(() =>
|
||||
(pipeline.value.changed_files ?? []).reduce((acc, file) => {
|
||||
const parts = file.split('/');
|
||||
let currentLevel = acc;
|
||||
|
||||
parts.forEach((part, index) => {
|
||||
const existingNode = currentLevel.find((node) => node.name === part);
|
||||
if (existingNode) {
|
||||
currentLevel = existingNode.children;
|
||||
} else {
|
||||
const newNode = {
|
||||
name: part,
|
||||
path: parts.slice(0, index + 1).join('/'),
|
||||
isDirectory: index < parts.length - 1,
|
||||
children: [],
|
||||
};
|
||||
currentLevel.push(newNode);
|
||||
currentLevel = newNode.children;
|
||||
}
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, [] as TreeNode[]),
|
||||
);
|
||||
</script>
|
||||
|
|
Loading…
Reference in a new issue