mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2025-09-02 20:23:50 +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 === '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-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 === '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 === 'bug-outline'" :path="mdiBugOutline" size="1.3rem" />
|
||||||
<SvgIcon v-else-if="name === 'docker'" :path="mdiDocker" 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" />
|
<SvgIcon v-else-if="name === 'forge'" :path="mdiCodeBraces" size="1.3rem" />
|
||||||
|
@ -124,6 +127,9 @@ import {
|
||||||
mdiEyeOutline,
|
mdiEyeOutline,
|
||||||
mdiFileCogOutline,
|
mdiFileCogOutline,
|
||||||
mdiFileEditOutline,
|
mdiFileEditOutline,
|
||||||
|
mdiFileOutline,
|
||||||
|
mdiFolderOpenOutline,
|
||||||
|
mdiFolderOutline,
|
||||||
mdiFormatListBulleted,
|
mdiFormatListBulleted,
|
||||||
mdiFormatListGroup,
|
mdiFormatListGroup,
|
||||||
mdiGestureTap,
|
mdiGestureTap,
|
||||||
|
@ -230,7 +236,10 @@ export type IconNames =
|
||||||
| 'org'
|
| 'org'
|
||||||
| 'cron'
|
| 'cron'
|
||||||
| 'toolbox'
|
| 'toolbox'
|
||||||
| 'forge';
|
| 'forge'
|
||||||
|
| 'folder'
|
||||||
|
| 'folder-open'
|
||||||
|
| 'file';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
name: IconNames;
|
name: IconNames;
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<Panel>
|
<Panel>
|
||||||
<ul class="w-full list-inside list-disc">
|
<div class="w-full">
|
||||||
<li v-for="file in pipeline.changed_files" :key="file">{{ file }}</li>
|
<FileTree v-for="node in fileTree" :key="node.name" :node="node" :depth="0" />
|
||||||
</ul>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -10,6 +10,8 @@
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
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 Panel from '~/components/layout/Panel.vue';
|
||||||
import { requiredInject } from '~/compositions/useInjectProvide';
|
import { requiredInject } from '~/compositions/useInjectProvide';
|
||||||
import { useWPTitle } from '~/compositions/useWPTitle';
|
import { useWPTitle } from '~/compositions/useWPTitle';
|
||||||
|
@ -25,4 +27,29 @@ useWPTitle(
|
||||||
repo.value.full_name,
|
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>
|
</script>
|
||||||
|
|
Loading…
Reference in a new issue