Header and Tabs UI Improvements (#1290)

Some improvements to the Page Header and Tab UI.

Original |  New
:--------:|:-------:

![image](https://user-images.githubusercontent.com/62170586/197360886-046f1016-ca39-4b69-8134-99ba88e3a0c2.png)
|
![image](https://user-images.githubusercontent.com/62170586/197360819-7efd0d82-1412-465d-aefa-039164f97465.png)

![image](https://user-images.githubusercontent.com/62170586/197360872-f2ece5fd-7c0b-4e2c-8629-31524a412af5.png)
|
![image](https://user-images.githubusercontent.com/62170586/197360830-49f09e0d-619e-4fa9-8e38-8d05d9404185.png)

![image](https://user-images.githubusercontent.com/62170586/197281776-e3de6441-9417-4614-8b25-1aaef0b8da61.png)
|
![image](https://user-images.githubusercontent.com/62170586/197281698-40c66d34-76f3-4fd5-97e3-1c422b74844c.png)

![image](https://user-images.githubusercontent.com/62170586/196609248-ff150c6e-2995-4bcc-8573-49ffaf388446.png)
|
![image](https://user-images.githubusercontent.com/62170586/197323734-7c1a1b79-0f41-4bf2-96a3-dd38df9e1415.png)

![image](https://user-images.githubusercontent.com/62170586/196609329-b7a6f37e-e8c2-4004-a98b-73f837122ff8.png)
|
![image](https://user-images.githubusercontent.com/62170586/197323882-10141ffd-7411-4493-8291-b8000adc3cc5.png)


What?
- Create a new Scaffold component, which includes the header and tabs
required for a page.
- Use this component to wrap all the views that have a header.
- Ensures consistency in headers between different pages.
- [x] Add support to use custom html/component in place of title (for
repo page, pipeline page, etc)
- [x] Add support of right icon buttons (for repo page, pipeline page,
etc)
- [x] Refactor tabs handling using compositions (useTabsProvider, useTabsClient)
- [x] Make new header ui resposive
This commit is contained in:
Divya Jain 2022-10-28 04:25:07 +05:30 committed by GitHub
parent 8b7ca464cf
commit e2ab8a46ed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 469 additions and 355 deletions

6
web/components.d.ts vendored
View file

@ -18,6 +18,7 @@ declare module '@vue/runtime-core' {
DocsLink: typeof import('./src/components/atomic/DocsLink.vue')['default']
FluidContainer: typeof import('./src/components/layout/FluidContainer.vue')['default']
GeneralTab: typeof import('./src/components/repo/settings/GeneralTab.vue')['default']
Header: typeof import('./src/components/layout/scaffold/Header.vue')['default']
IBxBxPowerOff: typeof import('~icons/bx/bx-power-off')['default']
ICarbonCloseOutline: typeof import('~icons/carbon/close-outline')['default']
IClarityDeployLine: typeof import('~icons/clarity/deploy-line')['default']
@ -84,12 +85,13 @@ declare module '@vue/runtime-core' {
RegistriesTab: typeof import('./src/components/repo/settings/RegistriesTab.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
Scaffold: typeof import('./src/components/layout/scaffold/Scaffold.vue')['default']
SecretEdit: typeof import('./src/components/secrets/SecretEdit.vue')['default']
SecretList: typeof import('./src/components/secrets/SecretList.vue')['default']
SecretsTab: typeof import('./src/components/repo/settings/SecretsTab.vue')['default']
SelectField: typeof import('./src/components/form/SelectField.vue')['default']
Tab: typeof import('./src/components/tabs/Tab.vue')['default']
Tabs: typeof import('./src/components/tabs/Tabs.vue')['default']
Tab: typeof import('./src/components/layout/scaffold/Tab.vue')['default']
Tabs: typeof import('./src/components/layout/scaffold/Tabs.vue')['default']
TextField: typeof import('./src/components/form/TextField.vue')['default']
Warning: typeof import('./src/components/atomic/Warning.vue')['default']
}

View file

@ -3,7 +3,7 @@
type="button"
class="relative flex items-center py-1 px-2 rounded-md border shadow-sm cursor-pointer transition-all duration-150 focus:outline-none overflow-hidden disabled:opacity-50 disabled:cursor-not-allowed"
:class="{
'bg-white hover:bg-gray-200 border-gray-300 text-color dark:bg-dark-gray-700 dark:border-dark-400 dark:hover:bg-dark-gray-800':
'bg-white hover:bg-gray-200 border-gray-300 text-color dark:bg-dark-gray-600 dark:border-dark-400 dark:hover:bg-dark-gray-800':
color === 'gray',
'bg-lime-600 hover:bg-lime-700 border-lime-800 text-white dark:text-gray-400 dark:bg-lime-900 dark:hover:bg-lime-800':
color === 'green',

View file

@ -7,6 +7,7 @@
v-if="lines === 1"
v-model="innerValue"
class="w-full bg-transparent text-color focus:outline-none focus:border-blue-400"
:class="inputClass"
:disabled="disabled"
:type="type"
:placeholder="placeholder"
@ -15,6 +16,7 @@
v-else
v-model="innerValue"
class="w-full bg-transparent text-color focus:outline-none focus:border-blue-400"
:class="inputClass"
:disabled="disabled"
:placeholder="placeholder"
:rows="lines"
@ -52,6 +54,11 @@ export default defineComponent({
disabled: {
type: Boolean,
},
inputClass: {
type: String,
default: '',
},
},
emits: {

View file

@ -1,6 +1,6 @@
<template>
<!-- Navbar -->
<div class="flex shadow-lg dark:shadow-sm bg-lime-600 text-neutral-content p-4 dark:bg-dark-gray-900">
<div class="flex bg-lime-600 text-neutral-content p-4 dark:bg-dark-gray-800 dark:border-b dark:border-gray-700">
<!-- Left Links Box -->
<div class="flex text-white dark:text-gray-400 items-center space-x-2">
<!-- Logo -->

View file

@ -0,0 +1,64 @@
<template>
<div class="bg-white dark:bg-dark-gray-900 border-b dark:border-gray-700">
<FluidContainer class="!py-0">
<div class="flex flex-wrap items-center justify-between py-4 <md:flex-row <md:gap-y-4">
<div
class="flex flex-wrap items-center justify-start <md:w-full <md:justify-center"
:class="{
'md:flex-1': searchBoxPresent,
}"
>
<IconButton v-if="goBack" icon="back" :title="$t('back')" class="mr-2 <md:hidden" @click="goBack" />
<h1 class="flex flex-wrap text-xl text-color items-center gap-x-2">
<slot name="title" />
</h1>
</div>
<TextField
v-if="searchBoxPresent"
class="w-auto !bg-gray-100 !dark:bg-dark-gray-600 <md:w-full <md:order-3"
input-class="!placeholder-gray-500"
:placeholder="$t('search')"
:model-value="search"
@update:model-value="(value: string) => $emit('update:search', value)"
/>
<div
v-if="$slots.titleActions"
class="flex flex-wrap items-center justify-end gap-x-2 <md:w-full <md:justify-center"
:class="{
'md:flex-1': searchBoxPresent,
}"
>
<slot name="titleActions" />
</div>
</div>
<div v-if="enableTabs" class="flex flex-wrap justify-between">
<Tabs class="<md:order-2" />
<div
v-if="$slots.titleActions"
class="flex items-center justify-end gap-x-2 md:mb-2 <md:w-full <md:justify-center <md:order-1"
>
<slot name="tabActions" />
</div>
</div>
</FluidContainer>
</div>
</template>
<script setup lang="ts">
import TextField from '~/components/form/TextField.vue';
import FluidContainer from '~/components/layout/FluidContainer.vue';
import Tabs from './Tabs.vue';
export interface Props {
goBack?: () => void;
enableTabs?: boolean;
search?: string;
}
const props = defineProps<Props>();
defineEmits(['update:search']);
const searchBoxPresent = props.search !== undefined;
</script>

View file

@ -0,0 +1,56 @@
<template>
<Header
:go-back="goBack"
:enable-tabs="enableTabs"
:search="search"
@update:search="(value) => $emit('update:search', value)"
>
<template #title><slot name="title" /></template>
<template v-if="$slots.titleActions" #titleActions><slot name="titleActions" /></template>
<template v-if="$slots.tabActions" #tabActions><slot name="tabActions" /></template>
</Header>
<FluidContainer>
<slot />
</FluidContainer>
</template>
<script setup lang="ts">
import { toRef } from 'vue';
import FluidContainer from '~/components/layout/FluidContainer.vue';
import { useTabsProvider } from '~/compositions/useTabs';
import Header from './Header.vue';
export interface Props {
// Header
goBack?: () => void;
search?: string;
// Tabs
enableTabs?: boolean;
disableHashMode?: boolean;
activeTab: string;
}
const props = withDefaults(defineProps<Props>(), {
goBack: undefined,
search: undefined,
// eslint-disable-next-line vue/no-boolean-default
disableHashMode: false,
// eslint-disable-next-line vue/no-boolean-default
enableTabs: false,
activeTab: '',
});
const emit = defineEmits(['update:activeTab', 'update:search']);
if (props.enableTabs) {
useTabsProvider({
activeTabProp: toRef(props, 'activeTab'),
disableHashMode: toRef(props, 'disableHashMode'),
updateActiveTabProp: (value) => emit('update:activeTab', value),
});
}
</script>

View file

@ -0,0 +1,31 @@
<template>
<div v-if="$slots.default" v-show="isActive" :aria-hidden="!isActive">
<slot />
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { Tab, useTabsClient } from '~/compositions/useTabs';
export interface Props {
id?: string;
title: string;
}
const props = defineProps<Props>();
const { tabs, activeTab } = useTabsClient();
const tab = ref<Tab>();
onMounted(() => {
tab.value = {
id: props.id || props.title.toLocaleLowerCase().replace(' ', '-') || tabs.value.length.toString(),
title: props.title,
};
tabs.value.push(tab.value);
});
const isActive = computed(() => tab.value && tab.value.id === activeTab.value);
</script>

View file

@ -0,0 +1,41 @@
<template>
<div class="flex flex-wrap">
<button
v-for="tab in tabs"
:key="tab.id"
class="w-full py-2 md:w-auto md:py-2 md:px-8 flex cursor-pointer md:border-b-2 text-color hover:text-gray-700 dark:hover:text-gray-400 items-center"
:class="{
'border-gray-400 dark:border-gray-600': activeTab === tab.id,
'border-transparent': activeTab !== tab.id,
}"
type="button"
@click="selectTab(tab)"
>
<Icon v-if="activeTab === tab.id" name="chevron-right" class="md:hidden" />
<Icon v-else name="blank" class="md:hidden" />
<span>{{ tab.title }}</span>
</button>
</div>
</template>
<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router';
import { Tab, useTabsClient } from '~/compositions/useTabs';
const router = useRouter();
const route = useRoute();
const { activeTab, tabs, disableHashMode } = useTabsClient();
async function selectTab(tab: Tab) {
if (tab.id === undefined) {
return;
}
activeTab.value = tab.id;
if (!disableHashMode.value) {
await router.replace({ params: route.params, hash: `#${tab.id}` });
}
}
</script>

View file

@ -1,49 +0,0 @@
<template>
<div v-if="$slots.default" v-show="isActive" :aria-hidden="!isActive" class="mt-4">
<slot />
</div>
</template>
<script lang="ts">
import { computed, defineComponent, inject, onMounted, Ref, ref } from 'vue';
import { Tab } from './types';
export default defineComponent({
name: 'Tab',
props: {
id: {
type: String,
default: undefined,
},
title: {
type: String,
required: true,
},
},
setup(props) {
const activeTab = inject<Ref<string>>('active-tab');
const tabs = inject<Ref<Tab[]>>('tabs');
if (activeTab === undefined || tabs === undefined) {
throw new Error('Please wrap this "Tab"-component inside a "Tabs" list.');
}
const tab = ref<Tab>();
onMounted(() => {
tab.value = {
id: props.id || props.title.toLocaleLowerCase().replace(' ', '-') || tabs.value.length.toString(),
title: props.title,
};
tabs.value.push(tab.value);
});
const isActive = computed(() => tab.value && tab.value.id === activeTab.value);
return { isActive };
},
});
</script>

View file

@ -1,91 +0,0 @@
<template>
<div class="flex flex-col">
<div class="flex w-full md:pt-4 flex-wrap">
<button
v-for="tab in tabs"
:key="tab.id"
class="w-full py-2 md:w-auto md:pt-0 md:pb-2 md:px-8 flex cursor-pointer md:border-b-2 text-color hover:text-gray-700 dark:hover:text-gray-400 items-center"
:class="{
'border-gray-400 dark:border-gray-600': activeTab === tab.id,
'border-transparent': activeTab !== tab.id,
}"
type="button"
@click="selectTab(tab)"
>
<Icon v-if="activeTab === tab.id" name="chevron-right" class="md:hidden" />
<Icon v-else name="blank" class="md:hidden" />
<span>{{ tab.title }}</span>
</button>
</div>
<div>
<slot />
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, onMounted, provide, ref, toRef } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import Icon from '~/components/atomic/Icon.vue';
import { Tab } from './types';
export default defineComponent({
name: 'Tabs',
components: { Icon },
props: {
disableHashMode: {
type: Boolean,
},
modelValue: {
type: String,
default: '',
},
},
emits: {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
'update:modelValue': (_value: string): boolean => true,
},
setup(props, { emit }) {
const router = useRouter();
const route = useRoute();
const disableHashMode = toRef(props, 'disableHashMode');
const modelValue = toRef(props, 'modelValue');
const tabs = ref<Tab[]>([]);
const activeTab = ref();
provide('tabs', tabs);
provide(
'active-tab',
computed(() => activeTab.value),
);
async function selectTab(tab: Tab) {
if (tab.id === undefined) {
return;
}
activeTab.value = tab.id;
emit('update:modelValue', activeTab.value);
if (!disableHashMode.value) {
await router.replace({ params: route.params, hash: `#${tab.id}` });
}
}
onMounted(() => {
if (modelValue.value) {
activeTab.value = modelValue.value;
return;
}
const hashTab = route.hash.replace(/^#/, '');
if (hashTab) {
activeTab.value = hashTab;
return;
}
activeTab.value = tabs.value[0].id;
});
return { tabs, activeTab, selectTab };
},
});
</script>

View file

@ -1,4 +0,0 @@
export type Tab = {
id: string;
title: string;
};

View file

@ -0,0 +1,64 @@
import { computed, inject, onMounted, provide, Ref, ref } from 'vue';
import { useRoute } from 'vue-router';
export type Tab = {
id: string;
title: string;
};
export function useTabsProvider({
activeTabProp,
disableHashMode,
updateActiveTabProp,
}: {
activeTabProp: Ref<string>;
updateActiveTabProp: (tab: string) => void;
disableHashMode: Ref<boolean>;
}) {
const route = useRoute();
const tabs = ref<Tab[]>([]);
const activeTab = ref<string>('');
provide('tabs', tabs);
provide(
'disable-hash-mode',
computed(() => disableHashMode.value),
);
provide(
'active-tab',
computed({
get: () => activeTab.value,
set: (value) => {
activeTab.value = value;
updateActiveTabProp(value);
},
}),
);
onMounted(() => {
if (activeTabProp.value) {
activeTab.value = activeTabProp.value;
return;
}
const hashTab = route.hash.replace(/^#/, '');
if (hashTab) {
activeTab.value = hashTab;
return;
}
activeTab.value = tabs.value[0].id;
});
}
export function useTabsClient() {
const tabs = inject<Ref<Tab[]>>('tabs');
const disableHashMode = inject<Ref<boolean>>('disable-hash-mode');
const activeTab = inject<Ref<string>>('active-tab');
if (activeTab === undefined || tabs === undefined || disableHashMode === undefined) {
throw new Error('Please use this "useTabsClient" composition inside a compoent running "useTabsProvider".');
}
return { activeTab, tabs, disableHashMode };
}

View file

@ -1,17 +1,12 @@
<template>
<FluidContainer class="flex flex-col">
<div class="flex flex-row border-b mb-4 pb-4 items-center dark:border-dark-200">
<IconButton :to="{ name: 'repos' }" :title="$t('back')" icon="back" />
<h1 class="text-xl ml-2 text-color">{{ $t('repo.add') }}</h1>
<TextField v-model="search" class="w-auto ml-auto" :placeholder="$t('search')" />
<Button
class="ml-auto"
start-icon="sync"
:text="$t('repo.enable.reload')"
:is-loading="isReloadingRepos"
@click="reloadRepos"
/>
</div>
<Scaffold v-model:search="search" :go-back="goBack">
<template #title>
{{ $t('repo.add') }}
</template>
<template #titleActions>
<Button start-icon="sync" :text="$t('repo.enable.reload')" :is-loading="isReloadingRepos" @click="reloadRepos" />
</template>
<div class="space-y-4">
<ListItem
@ -32,7 +27,7 @@
/>
</ListItem>
</div>
</FluidContainer>
</Scaffold>
</template>
<script lang="ts">
@ -41,14 +36,13 @@ import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import Button from '~/components/atomic/Button.vue';
import IconButton from '~/components/atomic/IconButton.vue';
import ListItem from '~/components/atomic/ListItem.vue';
import TextField from '~/components/form/TextField.vue';
import FluidContainer from '~/components/layout/FluidContainer.vue';
import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
import useApiClient from '~/compositions/useApiClient';
import { useAsyncAction } from '~/compositions/useAsyncAction';
import useNotifications from '~/compositions/useNotifications';
import { useRepoSearch } from '~/compositions/useRepoSearch';
import { useRouteBackOrDefault } from '~/compositions/useRouteBackOrDefault';
import { Repo } from '~/lib/api/types';
export default defineComponent({
@ -56,10 +50,8 @@ export default defineComponent({
components: {
Button,
FluidContainer,
ListItem,
IconButton,
TextField,
Scaffold,
},
setup() {
@ -91,10 +83,13 @@ export default defineComponent({
await router.push({ name: 'repo', params: { repoName: repo.name, repoOwner: repo.owner } });
});
const goBack = useRouteBackOrDefault({ name: 'repos' });
return {
isReloadingRepos,
isActivatingRepo,
repoToActivate,
goBack,
reloadRepos,
activateRepo,
searchedRepos,

View file

@ -1,10 +1,12 @@
<template>
<FluidContainer class="flex flex-col">
<div class="flex flex-row flex-wrap md:grid md:grid-cols-3 border-b pb-4 mb-4 dark:border-dark-200">
<h1 class="text-xl text-color">{{ $t('repositories') }}</h1>
<TextField v-model="search" class="w-auto md:ml-auto md:mr-auto" :placeholder="$t('search')" />
<Button class="md:ml-auto" :to="{ name: 'repo-add' }" start-icon="plus" :text="$t('repo.add')" />
</div>
<Scaffold v-model:search="search">
<template #title>
{{ $t('repositories') }}
</template>
<template #titleActions>
<Button :to="{ name: 'repo-add' }" start-icon="plus" :text="$t('repo.add')" />
</template>
<div class="space-y-4">
<ListItem
@ -16,7 +18,7 @@
<span class="text-color">{{ `${repo.owner} / ${repo.name}` }}</span>
</ListItem>
</div>
</FluidContainer>
</Scaffold>
</template>
<script lang="ts">
@ -24,8 +26,7 @@ import { computed, defineComponent, onMounted, ref } from 'vue';
import Button from '~/components/atomic/Button.vue';
import ListItem from '~/components/atomic/ListItem.vue';
import TextField from '~/components/form/TextField.vue';
import FluidContainer from '~/components/layout/FluidContainer.vue';
import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
import { useRepoSearch } from '~/compositions/useRepoSearch';
import RepoStore from '~/store/repos';
@ -34,9 +35,8 @@ export default defineComponent({
components: {
Button,
FluidContainer,
ListItem,
TextField,
Scaffold,
},
setup() {

View file

@ -1,16 +1,17 @@
<template>
<FluidContainer class="flex flex-col">
<div class="flex flex-row flex-wrap md:grid md:grid-cols-3 border-b pb-4 mb-4 dark:border-dark-200">
<h1 class="text-xl text-color">{{ repoOwner }}</h1>
<TextField v-model="search" class="w-auto md:ml-auto md:mr-auto" :placeholder="$t('search')" />
<Scaffold v-model:search="search">
<template #title>
{{ repoOwner }}
</template>
<template #titleActions>
<IconButton
v-if="orgPermissions.admin"
icon="settings"
:to="{ name: 'org-settings' }"
:title="$t('org.settings.settings')"
class="ml-auto"
/>
</div>
</template>
<div class="space-y-4">
<ListItem
@ -25,7 +26,7 @@
<div v-if="(searchedRepos || []).length <= 0" class="text-center">
<span class="text-color m-auto">{{ $t('repo.user_none') }}</span>
</div>
</FluidContainer>
</Scaffold>
</template>
<script lang="ts">
@ -33,8 +34,7 @@ import { computed, defineComponent, onMounted, ref } from 'vue';
import IconButton from '~/components/atomic/IconButton.vue';
import ListItem from '~/components/atomic/ListItem.vue';
import TextField from '~/components/form/TextField.vue';
import FluidContainer from '~/components/layout/FluidContainer.vue';
import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
import useApiClient from '~/compositions/useApiClient';
import { useRepoSearch } from '~/compositions/useRepoSearch';
import { OrgPermissions } from '~/lib/api/types';
@ -44,10 +44,9 @@ export default defineComponent({
name: 'ReposOwner',
components: {
FluidContainer,
ListItem,
TextField,
IconButton,
Scaffold,
},
props: {

View file

@ -1,32 +1,34 @@
<template>
<FluidContainer class="space-y-4 flex flex-col my-0">
<Button class="ml-auto" :text="$t('logout')" :to="`${address}/logout`" />
<Scaffold>
<template #title>{{ $t('user.settings') }}</template>
<template #titleActions><Button :text="$t('logout')" :to="`${address}/logout`" /></template>
<div class="space-y-4 flex flex-col">
<SelectField v-model="selectedLocale" :options="localeOptions" />
<SelectField v-model="selectedLocale" :options="localeOptions" />
<div>
<h2 class="text-lg text-color">{{ $t('user.token') }}</h2>
<pre class="cli-box">{{ token }}</pre>
</div>
<div>
<h2 class="text-lg text-color">{{ $t('user.shell_setup') }}</h2>
<pre class="cli-box">{{ usageWithShell }}</pre>
</div>
<div>
<h2 class="text-lg text-color">{{ $t('user.api_usage') }}</h2>
<pre class="cli-box">{{ usageWithCurl }}</pre>
</div>
<div>
<div class="flex items-center">
<h2 class="text-lg text-color">{{ $t('user.cli_usage') }}</h2>
<a :href="cliDownload" target="_blank" class="ml-4 text-link">{{ $t('user.dl_cli') }}</a>
<div>
<h2 class="text-lg text-color">{{ $t('user.token') }}</h2>
<pre class="cli-box">{{ token }}</pre>
</div>
<div>
<h2 class="text-lg text-color">{{ $t('user.shell_setup') }}</h2>
<pre class="cli-box">{{ usageWithShell }}</pre>
</div>
<div>
<h2 class="text-lg text-color">{{ $t('user.api_usage') }}</h2>
<pre class="cli-box">{{ usageWithCurl }}</pre>
</div>
<div>
<div class="flex items-center">
<h2 class="text-lg text-color">{{ $t('user.cli_usage') }}</h2>
<a :href="cliDownload" target="_blank" class="ml-4 text-link">{{ $t('user.dl_cli') }}</a>
</div>
<pre class="cli-box">{{ usageWithCli }}</pre>
</div>
<pre class="cli-box">{{ usageWithCli }}</pre>
</div>
</FluidContainer>
</Scaffold>
</template>
<script lang="ts" setup>
@ -38,7 +40,7 @@ import { useI18n } from 'vue-i18n';
import Button from '~/components/atomic/Button.vue';
import SelectField from '~/components/form/SelectField.vue';
import FluidContainer from '~/components/layout/FluidContainer.vue';
import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
import useApiClient from '~/compositions/useApiClient';
const { t, availableLocales, locale } = useI18n();

View file

@ -1,16 +1,12 @@
<template>
<FluidContainer>
<div class="flex border-b items-center pb-4 mb-4 dark:border-gray-600">
<IconButton icon="back" :title="$t('back')" @click="goBack" />
<h1 class="text-xl ml-2 text-color">{{ $t('admin.settings.settings') }}</h1>
</div>
<Tabs>
<Tab id="secrets" :title="$t('admin.settings.secrets.secrets')">
<AdminSecretsTab />
</Tab>
</Tabs>
</FluidContainer>
<Scaffold enable-tabs>
<template #title>
{{ $t('repo.settings.settings') }}
</template>
<Tab id="secrets" :title="$t('admin.settings.secrets.secrets')">
<AdminSecretsTab />
</Tab>
</Scaffold>
</template>
<script lang="ts">
@ -19,23 +15,18 @@ import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import AdminSecretsTab from '~/components/admin/settings/AdminSecretsTab.vue';
import IconButton from '~/components/atomic/IconButton.vue';
import FluidContainer from '~/components/layout/FluidContainer.vue';
import Tab from '~/components/tabs/Tab.vue';
import Tabs from '~/components/tabs/Tabs.vue';
import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
import Tab from '~/components/layout/scaffold/Tab.vue';
import useAuthentication from '~/compositions/useAuthentication';
import useNotifications from '~/compositions/useNotifications';
import { useRouteBackOrDefault } from '~/compositions/useRouteBackOrDefault';
export default defineComponent({
name: 'AdminSettings',
components: {
FluidContainer,
IconButton,
Tabs,
Tab,
AdminSecretsTab,
Scaffold,
},
setup() {
@ -50,10 +41,6 @@ export default defineComponent({
await router.replace({ name: 'home' });
}
});
return {
goBack: useRouteBackOrDefault({ name: 'home' }),
};
},
});
</script>

View file

@ -1,16 +1,19 @@
<template>
<FluidContainer>
<div class="flex border-b items-center pb-4 mb-4 dark:border-gray-600">
<IconButton icon="back" :title="$t('back')" @click="goBack" />
<h1 class="text-xl ml-2 text-color">{{ $t('org.settings.settings') }}</h1>
</div>
<Scaffold enable-tabs :go-back="goBack">
<template #title>
<span>
<router-link :to="{ name: 'repos-owner', params: { repoOwner: org.name } }" class="hover:underline">
{{ org.name }}
</router-link>
/
{{ $t('org.settings.settings') }}
</span>
</template>
<Tabs>
<Tab id="secrets" :title="$t('org.settings.secrets.secrets')">
<OrgSecretsTab />
</Tab>
</Tabs>
</FluidContainer>
<Tab id="secrets" :title="$t('org.settings.secrets.secrets')">
<OrgSecretsTab />
</Tab>
</Scaffold>
</template>
<script lang="ts">
@ -18,22 +21,16 @@ import { defineComponent, inject, onMounted, Ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import IconButton from '~/components/atomic/IconButton.vue';
import FluidContainer from '~/components/layout/FluidContainer.vue';
import Tab from '~/components/layout/scaffold/Tab.vue';
import OrgSecretsTab from '~/components/org/settings/OrgSecretsTab.vue';
import Tab from '~/components/tabs/Tab.vue';
import Tabs from '~/components/tabs/Tabs.vue';
import useNotifications from '~/compositions/useNotifications';
import { useRouteBackOrDefault } from '~/compositions/useRouteBackOrDefault';
import { OrgPermissions } from '~/lib/api/types';
import { Org, OrgPermissions } from '~/lib/api/types';
export default defineComponent({
name: 'OrgSettings',
components: {
FluidContainer,
IconButton,
Tabs,
Tab,
OrgSecretsTab,
},
@ -48,6 +45,11 @@ export default defineComponent({
throw new Error('Unexpected: "orgPermissions" should be provided at this place');
}
const org = inject<Ref<Org>>('org');
if (!org) {
throw new Error('Unexpected: "org" should be provided at this place');
}
onMounted(async () => {
if (!orgPermissions.value.admin) {
notifications.notify({ type: 'error', title: i18n.t('org.settings.not_allowed') });
@ -56,6 +58,7 @@ export default defineComponent({
});
return {
org,
goBack: useRouteBackOrDefault({ name: 'repos-owner' }),
};
},

View file

@ -1,20 +1,20 @@
<template>
<FluidContainer v-if="org && orgPermissions && $route.meta.orgHeader">
<div class="flex flex-wrap border-b items-center pb-4 mb-4 dark:border-gray-600 justify-center">
<h1 class="text-xl text-color w-full md:w-auto text-center mb-4 md:mb-0">
{{ org.name }}
</h1>
<Scaffold v-if="org && orgPermissions && $route.meta.orgHeader">
<template #title>
{{ org.name }}
</template>
<template #titleActions>
<IconButton
v-if="orgPermissions.admin"
class="ml-2"
:to="{ name: 'repo-settings' }"
:title="$t('org.settings.settings')"
icon="settings"
/>
</div>
</template>
<router-view />
</FluidContainer>
</Scaffold>
<router-view v-else-if="org && orgPermissions" />
</template>
@ -22,14 +22,14 @@
import { computed, defineComponent, onMounted, provide, ref, toRef, watch } from 'vue';
import IconButton from '~/components/atomic/IconButton.vue';
import FluidContainer from '~/components/layout/FluidContainer.vue';
import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
import useApiClient from '~/compositions/useApiClient';
import { Org, OrgPermissions } from '~/lib/api/types';
export default defineComponent({
name: 'OrgWrapper',
components: { FluidContainer, IconButton },
components: { IconButton, Scaffold },
props: {
repoOwner: {

View file

@ -1,31 +1,41 @@
<template>
<FluidContainer>
<div class="flex border-b items-center pb-4 mb-4 dark:border-gray-600">
<IconButton icon="back" :title="$t('back')" @click="goBack" />
<h1 class="text-xl ml-2 text-color">{{ $t('repo.settings.settings') }}</h1>
</div>
<Scaffold enable-tabs :go-back="goBack">
<template #title>
<span>
<router-link :to="{ name: 'repos-owner', params: { repoOwner: repo.owner } }" class="hover:underline">
{{ repo.owner }}
</router-link>
/
<router-link
:to="{ name: 'repo', params: { repoOwner: repo.owner, repoName: repo.name } }"
class="hover:underline"
>
{{ repo.name }}
</router-link>
/
{{ $t('repo.settings.settings') }}
</span>
</template>
<Tabs>
<Tab id="general" :title="$t('repo.settings.general.general')">
<GeneralTab />
</Tab>
<Tab id="secrets" :title="$t('repo.settings.secrets.secrets')">
<SecretsTab />
</Tab>
<Tab id="registries" :title="$t('repo.settings.registries.registries')">
<RegistriesTab />
</Tab>
<Tab id="crons" :title="$t('repo.settings.crons.crons')">
<CronTab />
</Tab>
<Tab id="badge" :title="$t('repo.settings.badge.badge')">
<BadgeTab />
</Tab>
<Tab id="actions" :title="$t('repo.settings.actions.actions')">
<ActionsTab />
</Tab>
</Tabs>
</FluidContainer>
<Tab id="general" :title="$t('repo.settings.general.general')">
<GeneralTab />
</Tab>
<Tab id="secrets" :title="$t('repo.settings.secrets.secrets')">
<SecretsTab />
</Tab>
<Tab id="registries" :title="$t('repo.settings.registries.registries')">
<RegistriesTab />
</Tab>
<Tab id="crons" :title="$t('repo.settings.crons.crons')">
<CronTab />
</Tab>
<Tab id="badge" :title="$t('repo.settings.badge.badge')">
<BadgeTab />
</Tab>
<Tab id="actions" :title="$t('repo.settings.actions.actions')">
<ActionsTab />
</Tab>
</Scaffold>
</template>
<script lang="ts" setup>
@ -33,19 +43,17 @@ import { inject, onMounted, Ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import IconButton from '~/components/atomic/IconButton.vue';
import FluidContainer from '~/components/layout/FluidContainer.vue';
import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
import Tab from '~/components/layout/scaffold/Tab.vue';
import ActionsTab from '~/components/repo/settings/ActionsTab.vue';
import BadgeTab from '~/components/repo/settings/BadgeTab.vue';
import CronTab from '~/components/repo/settings/CronTab.vue';
import GeneralTab from '~/components/repo/settings/GeneralTab.vue';
import RegistriesTab from '~/components/repo/settings/RegistriesTab.vue';
import SecretsTab from '~/components/repo/settings/SecretsTab.vue';
import Tab from '~/components/tabs/Tab.vue';
import Tabs from '~/components/tabs/Tabs.vue';
import useNotifications from '~/compositions/useNotifications';
import { useRouteBackOrDefault } from '~/compositions/useRouteBackOrDefault';
import { RepoPermissions } from '~/lib/api/types';
import { Repo, RepoPermissions } from '~/lib/api/types';
const notifications = useNotifications();
const router = useRouter();
@ -56,6 +64,11 @@ if (!repoPermissions) {
throw new Error('Unexpected: "repoPermissions" should be provided at this place');
}
const repo = inject<Ref<Repo>>('repo');
if (!repo) {
throw new Error('Unexpected: "repo" should be provided at this place');
}
onMounted(async () => {
if (!repoPermissions.value.admin) {
notifications.notify({ type: 'error', title: i18n.t('repo.settings.not_allowed') });

View file

@ -1,19 +1,26 @@
<template>
<FluidContainer v-if="repo && repoPermissions && $route.meta.repoHeader">
<div class="flex flex-wrap border-b items-center pb-4 mb-4 dark:border-gray-600 justify-center">
<h1 class="text-xl text-color w-full md:w-auto text-center mb-4 md:mb-0">
<Scaffold
v-if="repo && repoPermissions && $route.meta.repoHeader"
v-model:activeTab="activeTab"
enable-tabs
disable-hash-mode
>
<template #title>
<span class="flex">
<router-link :to="{ name: 'repos-owner', params: { repoOwner } }" class="hover:underline">{{
repoOwner
}}</router-link>
{{ ` / ${repo.name}` }}
</h1>
<a v-if="badgeUrl" :href="badgeUrl" target="_blank" class="md:ml-auto">
{{ `&nbsp;/&nbsp;${repo.name}` }}
</span>
</template>
<template #titleActions>
<a v-if="badgeUrl" :href="badgeUrl" target="_blank" class="ml-2">
<img :src="badgeUrl" />
</a>
<a
:href="repo.link_url"
target="_blank"
class="flex ml-4 p-1 rounded-full text-color hover:bg-gray-200 hover:text-gray-700 dark:hover:bg-gray-600"
class="flex p-1 rounded-full text-color hover:bg-gray-200 hover:text-gray-700 dark:hover:bg-gray-600"
>
<Icon v-if="forge === 'github'" name="github" />
<Icon v-else-if="forge === 'gitea'" name="gitea" />
@ -23,28 +30,26 @@
</a>
<IconButton
v-if="repoPermissions.admin"
class="ml-2"
:to="{ name: 'repo-settings' }"
:title="$t('repo.settings.settings')"
icon="settings"
/>
</div>
<div class="flex flex-wrap gap-y-2 items-center justify-between">
<Tabs v-model="activeTab" disable-hash-mode class="mb-4">
<Tab id="activity" :title="$t('repo.activity')" />
<Tab id="branches" :title="$t('repo.branches')" />
</Tabs>
</template>
<template #tabActions>
<Button
v-if="repoPermissions.push"
:text="$t('repo.manual_pipeline.trigger')"
class="ml-auto"
@click="showManualPipelinePopup = true"
/>
<ManualPipelinePopup :open="showManualPipelinePopup" @close="showManualPipelinePopup = false" />
</div>
</template>
<Tab id="activity" :title="$t('repo.activity')" />
<Tab id="branches" :title="$t('repo.branches')" />
<router-view />
</FluidContainer>
</Scaffold>
<router-view v-else-if="repo && repoPermissions" />
</template>
@ -55,10 +60,9 @@ import { useRoute, useRouter } from 'vue-router';
import Icon from '~/components/atomic/Icon.vue';
import IconButton from '~/components/atomic/IconButton.vue';
import FluidContainer from '~/components/layout/FluidContainer.vue';
import ManualPipelinePopup from '~/components/layout/popups/ManualPipelinePopup.vue';
import Tab from '~/components/tabs/Tab.vue';
import Tabs from '~/components/tabs/Tabs.vue';
import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
import Tab from '~/components/layout/scaffold/Tab.vue';
import useApiClient from '~/compositions/useApiClient';
import useAuthentication from '~/compositions/useAuthentication';
import useConfig from '~/compositions/useConfig';

View file

@ -1,48 +1,34 @@
<template>
<template v-if="pipeline && repo">
<FluidContainer class="flex flex-col min-w-0 dark:border-gray-600">
<div class="flex mb-2 items-center <md:flex-wrap">
<IconButton icon="back" :title="$t('back')" class="flex-shrink-0" @click="goBack" />
<Scaffold v-model:activeTab="activeTab" enable-tabs disable-hash-mode :go-back="goBack">
<template #title>
<span class="w-full md:w-auto text-center">{{ $t('repo.pipeline.pipeline', { pipelineId }) }}</span>
<span class="<md:hidden">-</span>
<span class="w-full md:w-auto text-center truncate">{{ message }}</span>
</template>
<h1
class="order-3 w-full <md:flex-wrap md:order-none md:w-auto md:ml-2 flex text-center text-xl text-color whitespace-nowrap overflow-hidden overflow-ellipsis"
>
<span class="w-full md:w-auto text-center">{{ $t('repo.pipeline.pipeline', { pipelineId }) }}</span>
<span class="<md:hidden mx-2">-</span>
<span class="w-full md:w-auto text-center truncate">{{ message }}</span>
</h1>
<PipelineStatusIcon :pipeline="pipeline" class="flex flex-shrink-0 ml-auto" />
<template #titleActions>
<PipelineStatusIcon :pipeline="pipeline" class="flex flex-shrink-0" />
<template v-if="repoPermissions.push">
<Button
v-if="pipeline.status === 'pending' || pipeline.status === 'running'"
class="ml-4 flex-shrink-0"
class="flex-shrink-0"
:text="$t('repo.pipeline.actions.cancel')"
:is-loading="isCancelingPipeline"
@click="cancelPipeline"
/>
<Button
v-else-if="pipeline.status !== 'blocked' && pipeline.status !== 'declined'"
class="ml-4 flex-shrink-0"
class="flex-shrink-0"
:text="$t('repo.pipeline.actions.restart')"
:is-loading="isRestartingPipeline"
@click="restartPipeline"
/>
</template>
</div>
<div class="flex flex-wrap gap-y-2 items-center justify-between">
<Tabs v-model="activeTab" disable-hash-mode class="order-2 md:order-none">
<Tab id="tasks" :title="$t('repo.pipeline.tasks')" />
<Tab id="config" :title="$t('repo.pipeline.config')" />
<Tab
v-if="pipeline.event === 'push' || pipeline.event === 'pull_request'"
id="changed-files"
:title="$t('repo.pipeline.files', { files: pipeline.changed_files?.length || 0 })"
/>
</Tabs>
</template>
<template #tabActions>
<div class="flex justify-between gap-x-4 text-color flex-shrink-0 pb-2 md:p-0 mx-auto md:mr-0">
<div class="flex space-x-1 items-center flex-shrink-0">
<Icon name="since" />
@ -58,8 +44,16 @@
<span>{{ duration }}</span>
</div>
</div>
</div>
</FluidContainer>
</template>
<Tab id="tasks" :title="$t('repo.pipeline.tasks')" />
<Tab id="config" :title="$t('repo.pipeline.config')" />
<Tab
v-if="pipeline.event === 'push' || pipeline.event === 'pull_request'"
id="changed-files"
:title="$t('repo.pipeline.files', { files: pipeline.changed_files?.length || 0 })"
/>
</Scaffold>
<router-view />
</template>
@ -72,11 +66,9 @@ import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import Button from '~/components/atomic/Button.vue';
import IconButton from '~/components/atomic/IconButton.vue';
import FluidContainer from '~/components/layout/FluidContainer.vue';
import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
import Tab from '~/components/layout/scaffold/Tab.vue';
import PipelineStatusIcon from '~/components/repo/pipeline/PipelineStatusIcon.vue';
import Tab from '~/components/tabs/Tab.vue';
import Tabs from '~/components/tabs/Tabs.vue';
import useApiClient from '~/compositions/useApiClient';
import { useAsyncAction } from '~/compositions/useAsyncAction';
import { useFavicon } from '~/compositions/useFavicon';
@ -90,13 +82,11 @@ export default defineComponent({
name: 'PipelineWrapper',
components: {
FluidContainer,
Button,
PipelineStatusIcon,
IconButton,
Tabs,
Tab,
Tooltip,
Scaffold,
},
props: {