mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2025-01-16 04:25:30 +00:00
Merge branch 'origin/main' into 'next-release/main'
This commit is contained in:
commit
4e826e509f
4 changed files with 122 additions and 18 deletions
|
@ -48,6 +48,7 @@
|
||||||
<SvgIcon v-else-if="name === 'pause'" :path="mdiPause" size="1.3rem" />
|
<SvgIcon v-else-if="name === 'pause'" :path="mdiPause" size="1.3rem" />
|
||||||
<SvgIcon v-else-if="name === 'play'" :path="mdiPlay" size="1.3rem" />
|
<SvgIcon v-else-if="name === 'play'" :path="mdiPlay" size="1.3rem" />
|
||||||
<SvgIcon v-else-if="name === 'play-outline'" :path="mdiPlayOutline" size="1.3rem" />
|
<SvgIcon v-else-if="name === 'play-outline'" :path="mdiPlayOutline" size="1.3rem" />
|
||||||
|
<SvgIcon v-else-if="name === 'dots'" :path="mdiDotsVertical" size="1.3rem" />
|
||||||
|
|
||||||
<SvgIcon v-else-if="name === 'visibility-private'" :path="mdiLockOutline" size="1.3rem" />
|
<SvgIcon v-else-if="name === 'visibility-private'" :path="mdiLockOutline" size="1.3rem" />
|
||||||
<SvgIcon v-else-if="name === 'visibility-internal'" :path="mdiLockOpenOutline" size="1.3rem" />
|
<SvgIcon v-else-if="name === 'visibility-internal'" :path="mdiLockOpenOutline" size="1.3rem" />
|
||||||
|
@ -93,6 +94,7 @@ import {
|
||||||
mdiCloseCircle,
|
mdiCloseCircle,
|
||||||
mdiCog,
|
mdiCog,
|
||||||
mdiCogOutline,
|
mdiCogOutline,
|
||||||
|
mdiDotsVertical,
|
||||||
mdiDownloadOutline,
|
mdiDownloadOutline,
|
||||||
mdiEyeOffOutline,
|
mdiEyeOffOutline,
|
||||||
mdiEyeOutline,
|
mdiEyeOutline,
|
||||||
|
@ -180,7 +182,8 @@ export type IconNames =
|
||||||
| 'alert'
|
| 'alert'
|
||||||
| 'spinner'
|
| 'spinner'
|
||||||
| 'visibility-private'
|
| 'visibility-private'
|
||||||
| 'visibility-internal';
|
| 'visibility-internal'
|
||||||
|
| 'dots';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
name: IconNames;
|
name: IconNames;
|
||||||
|
|
|
@ -41,9 +41,9 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="enableTabs" class="flex flex-col py-2 md:flex-row md:items-center md:justify-between md:py-0">
|
<div v-if="enableTabs" class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between md:py-0">
|
||||||
<Tabs class="order-2 md:order-none" />
|
<Tabs class="order-2 md:order-none" />
|
||||||
<div v-if="$slots.headerActions" class="flex content-start md:justify-end">
|
<div v-if="$slots.headerActions" class="flex flex-wrap content-start md:justify-end">
|
||||||
<slot name="tabActions" />
|
<slot name="tabActions" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,35 +1,136 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="mt-2 flex flex-wrap md:gap-4">
|
<!-- Main tabs container -->
|
||||||
|
<div ref="tabsRef" class="flex min-w-0 flex-auto gap-4">
|
||||||
<router-link
|
<router-link
|
||||||
v-for="tab in tabs"
|
v-for="tab in visibleTabs"
|
||||||
:key="tab.title"
|
:key="tab.title"
|
||||||
v-slot="{ isActive, isExactActive }"
|
|
||||||
:to="tab.to"
|
:to="tab.to"
|
||||||
class="flex w-full cursor-pointer items-center border-transparent py-1 text-wp-text-100 md:w-auto md:border-b-2"
|
class="flex cursor-pointer items-center whitespace-nowrap border-b-2 border-transparent py-1 text-wp-text-100"
|
||||||
:active-class="tab.matchChildren ? '!border-wp-text-100' : ''"
|
:active-class="tab.matchChildren ? '!border-wp-text-100' : ''"
|
||||||
:exact-active-class="tab.matchChildren ? '' : '!border-wp-text-100'"
|
:exact-active-class="tab.matchChildren ? '' : '!border-wp-text-100'"
|
||||||
>
|
>
|
||||||
<Icon
|
|
||||||
v-if="isExactActive || (isActive && tab.matchChildren)"
|
|
||||||
name="chevron-right"
|
|
||||||
class="flex-shrink-0 md:hidden"
|
|
||||||
/>
|
|
||||||
<Icon v-else name="blank" class="md:hidden" />
|
|
||||||
<span
|
<span
|
||||||
class="flex w-full min-w-20 flex-row items-center gap-2 rounded-md px-2 py-1 hover:bg-wp-background-200 dark:hover:bg-wp-background-100 md:justify-center"
|
class="flex w-full min-w-20 flex-row items-center justify-center gap-2 rounded-md px-2 py-1 hover:bg-wp-background-200 dark:hover:bg-wp-background-100"
|
||||||
>
|
>
|
||||||
<Icon v-if="tab.icon" :name="tab.icon" :class="tab.iconClass" class="flex-shrink-0" />
|
<Icon v-if="tab.icon" :name="tab.icon" :class="tab.iconClass" class="flex-shrink-0" />
|
||||||
<span>{{ tab.title }}</span>
|
<span>{{ tab.title }}</span>
|
||||||
<CountBadge v-if="tab.count" :value="tab.count" />
|
<CountBadge v-if="tab.count" :value="tab.count" />
|
||||||
</span>
|
</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
|
<!-- Overflow dropdown -->
|
||||||
|
<div v-if="hiddenTabs.length" class="relative border-b-2 border-transparent py-1">
|
||||||
|
<IconButton icon="dots" class="tabs-more-button h-8 w-8" @click="toggleDropdown" />
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="isDropdownOpen"
|
||||||
|
class="tabs-dropdown absolute z-20 mt-1 rounded-md border border-wp-background-400 bg-wp-background-100 shadow-lg dark:bg-wp-background-200"
|
||||||
|
:class="[visibleTabs.length === 0 ? 'left-0' : 'right-0']"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
v-for="tab in hiddenTabs"
|
||||||
|
:key="tab.title"
|
||||||
|
:to="tab.to"
|
||||||
|
class="block w-full whitespace-nowrap p-1 text-left"
|
||||||
|
@click="isDropdownOpen = false"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="flex w-full min-w-20 flex-row items-center justify-center gap-2 rounded-md px-2 py-1 hover:bg-wp-background-200 dark:hover:bg-wp-background-100"
|
||||||
|
>
|
||||||
|
<Icon v-if="tab.icon" :name="tab.icon" :class="tab.iconClass" class="flex-shrink-0" />
|
||||||
|
<span>{{ tab.title }}</span>
|
||||||
|
</span>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
import CountBadge from '~/components/atomic/CountBadge.vue';
|
import CountBadge from '~/components/atomic/CountBadge.vue';
|
||||||
import Icon from '~/components/atomic/Icon.vue';
|
import Icon from '~/components/atomic/Icon.vue';
|
||||||
|
import IconButton from '~/components/atomic/IconButton.vue';
|
||||||
import { useTabsClient } from '~/compositions/useTabs';
|
import { useTabsClient } from '~/compositions/useTabs';
|
||||||
|
|
||||||
const { tabs } = useTabsClient();
|
const { tabs } = useTabsClient();
|
||||||
|
const tabsRef = ref<HTMLElement | null>(null);
|
||||||
|
const isDropdownOpen = ref(false);
|
||||||
|
const visibleCount = ref(tabs.value.length);
|
||||||
|
|
||||||
|
const visibleTabs = computed(() => tabs.value.slice(0, visibleCount.value));
|
||||||
|
const hiddenTabs = computed(() => tabs.value.slice(visibleCount.value));
|
||||||
|
|
||||||
|
const toggleDropdown = () => {
|
||||||
|
isDropdownOpen.value = !isDropdownOpen.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeDropdown = (event: MouseEvent) => {
|
||||||
|
const dropdown = tabsRef.value?.querySelector('.tabs-dropdown');
|
||||||
|
const moreButton = tabsRef.value?.querySelector('.tabs-more-button');
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
|
||||||
|
if (moreButton?.contains(target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dropdown && !dropdown.contains(target)) {
|
||||||
|
isDropdownOpen.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(isDropdownOpen, (isOpen) => {
|
||||||
|
if (isOpen) {
|
||||||
|
window.addEventListener('click', closeDropdown);
|
||||||
|
} else {
|
||||||
|
window.removeEventListener('click', closeDropdown);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateVisibleItems = () => {
|
||||||
|
visibleCount.value = tabs.value.length;
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
const availableWidth = tabsRef.value!.clientWidth || 0;
|
||||||
|
const moreButtonWidth = 64; // This need to match 2x the width of the IconButton (w-8)
|
||||||
|
const gapWidth = 16; // This need to match the gap between the tabs (gap-4)
|
||||||
|
let totalWidth = 0;
|
||||||
|
|
||||||
|
const items = Array.from(tabsRef.value!.children);
|
||||||
|
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
const itemWidth = items[i].getBoundingClientRect().width;
|
||||||
|
totalWidth += itemWidth;
|
||||||
|
if (i > 0) totalWidth += gapWidth;
|
||||||
|
|
||||||
|
if (totalWidth > availableWidth - (moreButtonWidth + gapWidth)) {
|
||||||
|
visibleCount.value = i;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visibleCount.value = tabs.value.length;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
|
requestAnimationFrame(updateVisibleItems);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (tabsRef.value!) {
|
||||||
|
resizeObserver.observe(tabsRef.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('resize', updateVisibleItems);
|
||||||
|
|
||||||
|
nextTick(updateVisibleItems);
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
window.removeEventListener('resize', updateVisibleItems);
|
||||||
|
window.removeEventListener('click', closeDropdown);
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #headerActions>
|
<template #headerActions>
|
||||||
<div class="flex min-w-0 flex-col gap-2 md:flex-row md:items-center md:justify-between">
|
<div class="flex w-full items-center justify-between gap-2">
|
||||||
<div class="flex min-w-0 content-start gap-2">
|
<div class="flex min-w-0 content-start gap-2">
|
||||||
<PipelineStatusIcon :status="pipeline.status" class="flex flex-shrink-0" />
|
<PipelineStatusIcon :status="pipeline.status" class="flex flex-shrink-0" />
|
||||||
<span class="flex-shrink-0 text-center">{{ $t('repo.pipeline.pipeline', { pipelineId }) }}</span>
|
<span class="flex-shrink-0 text-center">{{ $t('repo.pipeline.pipeline', { pipelineId }) }}</span>
|
||||||
|
@ -61,12 +61,12 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #tabActions>
|
<template #tabActions>
|
||||||
<div class="flex gap-x-4">
|
<div class="flex flex-wrap gap-4 md:flex-nowrap">
|
||||||
<div class="flex flex-shrink-0 items-center space-x-1" :title="$t('repo.pipeline.created', { created })">
|
<div class="flex flex-shrink-0 items-center gap-2" :title="$t('repo.pipeline.created', { created })">
|
||||||
<Icon name="since" />
|
<Icon name="since" />
|
||||||
<span>{{ since }}</span>
|
<span>{{ since }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-shrink-0 items-center space-x-1" :title="$t('repo.pipeline.duration')">
|
<div class="flex flex-shrink-0 items-center gap-2" :title="$t('repo.pipeline.duration')">
|
||||||
<Icon name="duration" />
|
<Icon name="duration" />
|
||||||
<span>{{ duration }}</span>
|
<span>{{ duration }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in a new issue