Skip to content

Commit

Permalink
Use separate routes instead of anchors (#4285)
Browse files Browse the repository at this point in the history
Co-authored-by: Anbraten <anton@ju60.de>
  • Loading branch information
2 people authored and pat-s committed Nov 19, 2024
1 parent b81bb8d commit 879c1e2
Show file tree
Hide file tree
Showing 39 changed files with 340 additions and 370 deletions.
27 changes: 2 additions & 25 deletions web/src/components/layout/scaffold/Scaffold.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@
</template>

<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import Container from '~/components/layout/Container.vue';
import { useTabsProvider } from '~/compositions/useTabs';
Expand All @@ -33,37 +31,16 @@ const props = defineProps<{
// Tabs
enableTabs?: boolean;
disableTabUrlHashMode?: boolean;
activeTab?: string;
// Content
fluidContent?: boolean;
}>();
const emit = defineEmits<{
(event: 'update:activeTab', value: string | undefined): void;
defineEmits<{
(event: 'update:search', value: string): void;
}>();
if (props.enableTabs) {
const internalActiveTab = ref(props.activeTab);
watch(
() => props.activeTab,
(activeTab) => {
internalActiveTab.value = activeTab;
},
);
useTabsProvider({
activeTab: computed({
get: () => internalActiveTab.value,
set: (value) => {
internalActiveTab.value = value;
emit('update:activeTab', value);
},
}),
disableUrlHashMode: computed(() => props.disableTabUrlHashMode || false),
});
useTabsProvider();
}
</script>
42 changes: 22 additions & 20 deletions web/src/components/layout/scaffold/Tab.vue
Original file line number Diff line number Diff line change
@@ -1,38 +1,40 @@
<template>
<div v-if="$slots.default" v-show="isActive" :aria-hidden="!isActive">
<slot />
</div>
</template>
<template><span /></template>

<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { onMounted } from 'vue';
import type { RouteLocationRaw } from 'vue-router';
import type { IconNames } from '~/components/atomic/Icon.vue';
import { useTabsClient, type Tab } from '~/compositions/useTabs';
import { useTabsClient } from '~/compositions/useTabs';
const props = defineProps<{
id?: string;
to: RouteLocationRaw;
title: string;
icon?: IconNames;
iconClass?: string;
matchChildren?: boolean;
}>();
const { tabs, activeTab } = useTabsClient();
const tab = ref<Tab>();
const { tabs } = useTabsClient();
// TODO: find a better way to compare routes like
// https://github.com/vuejs/router/blob/0eaaeb9697acd40ad524d913d0348748e9797acb/packages/router/src/utils/index.ts#L17
function isSameRoute(a: RouteLocationRaw, b: RouteLocationRaw): boolean {
return JSON.stringify(a) === JSON.stringify(b);
}
onMounted(() => {
tab.value = {
id: props.id || props.title.toLocaleLowerCase().replace(' ', '-') || tabs.value.length.toString(),
// don't add tab if tab id is already present
if (tabs.value.find(({ to }) => isSameRoute(to, props.to))) {
return;
}
tabs.value.push({
to: props.to,
title: props.title,
icon: props.icon,
iconClass: props.iconClass,
};
// don't add tab if tab id is already present
if (!tabs.value.find(({ id }) => id === props.id)) {
tabs.value.push(tab.value);
}
matchChildren: props.matchChildren,
});
});
const isActive = computed(() => tab.value && tab.value.id === activeTab.value);
</script>
41 changes: 11 additions & 30 deletions web/src/components/layout/scaffold/Tabs.vue
Original file line number Diff line number Diff line change
@@ -1,46 +1,27 @@
<template>
<div class="flex flex-wrap">
<button
<router-link
v-for="tab in tabs"
:key="tab.id"
class="w-full py-1 md:py-2 md:w-auto md:px-6 flex cursor-pointer md:border-b-2 text-wp-text-100 hover:text-wp-text-200 items-center"
:class="{
'border-wp-text-100': activeTab === tab.id,
'border-transparent': activeTab !== tab.id,
}"
type="button"
@click="selectTab(tab)"
:key="tab.title"
v-slot="{ isActive, isExactActive }"
:to="tab.to"
class="border-transparent w-full py-1 md:py-2 md:w-auto md:px-6 flex cursor-pointer md:border-b-2 text-wp-text-100 hover:text-wp-text-200 items-center"
:active-class="tab.matchChildren ? '!border-wp-text-100' : ''"
:exact-active-class="tab.matchChildren ? '' : '!border-wp-text-100'"
>
<Icon v-if="activeTab === tab.id" name="chevron-right" class="md:hidden" />
<Icon v-if="isExactActive || (isActive && tab.matchChildren)" name="chevron-right" class="md:hidden" />
<Icon v-else name="blank" class="md:hidden" />
<span class="flex gap-2 items-center flex-row-reverse md:flex-row">
<Icon v-if="tab.icon" :name="tab.icon" :class="tab.iconClass" />
<span>{{ tab.title }}</span>
</span>
</button>
</router-link>
</div>
</template>

<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router';
import Icon from '~/components/atomic/Icon.vue';
import { useTabsClient, type Tab } from '~/compositions/useTabs';
const router = useRouter();
const route = useRoute();
const { activeTab, tabs, disableUrlHashMode } = useTabsClient();
async function selectTab(tab: Tab) {
if (tab.id === undefined) {
return;
}
activeTab.value = tab.id;
import { useTabsClient } from '~/compositions/useTabs';
if (!disableUrlHashMode.value) {
await router.replace({ params: route.params, hash: `#${tab.id}` });
}
}
const { tabs } = useTabsClient();
</script>
7 changes: 6 additions & 1 deletion web/src/compositions/useInjectProvide.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import type { InjectionKey, Ref } from 'vue';
import { inject as vueInject, provide as vueProvide } from 'vue';

import type { Org, OrgPermissions, Repo } from '~/lib/api/types';
import type { Org, OrgPermissions, Pipeline, PipelineConfig, Repo } from '~/lib/api/types';

import type { Tab } from './useTabs';

export interface InjectKeys {
repo: Ref<Repo>;
org: Ref<Org | undefined>;
'org-permissions': Ref<OrgPermissions | undefined>;
pipeline: Ref<Pipeline | undefined>;
'pipeline-configs': Ref<PipelineConfig[] | undefined>;
tabs: Ref<Tab[]>;
}

export function inject<T extends keyof InjectKeys>(key: T): InjectKeys[T] {
Expand Down
43 changes: 9 additions & 34 deletions web/src/compositions/useTabs.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,24 @@
import { inject, onMounted, provide, ref, type Ref } from 'vue';
import { useRoute } from 'vue-router';
import { ref } from 'vue';
import type { RouteLocationRaw } from 'vue-router';

import type { IconNames } from '~/components/atomic/Icon.vue';

import { inject, provide } from './useInjectProvide';

export interface Tab {
id: string;
to: RouteLocationRaw;
title: string;
icon?: IconNames;
iconClass?: string;
matchChildren?: boolean;
}

export function useTabsProvider({
activeTab,
disableUrlHashMode,
}: {
activeTab: Ref<string | undefined>;
disableUrlHashMode: Ref<boolean>;
}) {
const route = useRoute();

export function useTabsProvider() {
const tabs = ref<Tab[]>([]);

provide('tabs', tabs);
provide('disable-url-hash-mode', disableUrlHashMode);
provide('active-tab', activeTab);

onMounted(() => {
if (activeTab.value !== undefined) {
return;
}

const hashTab = route.hash.replace(/^#/, '');

activeTab.value = hashTab || tabs.value[0].id;
});
}

export function useTabsClient() {
const tabs = inject<Ref<Tab[]>>('tabs');
const disableUrlHashMode = inject<Ref<boolean>>('disable-url-hash-mode');
const activeTab = inject<Ref<string>>('active-tab');

if (activeTab === undefined || tabs === undefined || disableUrlHashMode === undefined) {
throw new Error('Please use this "useTabsClient" composition inside a component running "useTabsProvider".');
}

return { activeTab, tabs, disableUrlHashMode };
const tabs = inject('tabs');
return { tabs };
}
Loading

0 comments on commit 879c1e2

Please sign in to comment.