feat: onglets Réglages & Aide + icône Gitea
Réglages : préférences (langue, notifications push + test), gestion complète des tokens d'accès (liste/création/révocation, garde anti lock-out sur le dernier token), URL Gitea configurable, infos serveur en lecture seule (port/bind/origines/VAPID + flags CLI). Aide : documentation bilingue EN/FR de toutes les fonctionnalités, avec recherche. Icône Gitea (lien externe) dans la nav (sidebar + MoreSheet mobile). Backend : routes /api/v1/auth/tokens (GET/POST/DELETE) + tokenId dans /me ; routes/settings.ts (GET/PATCH, allow-list stricte gitea_url, aucun secret exposé, URL validée http/https anti-XSS) ; AuthService.listTokens/revokeToken (transaction). Front : NavItem gère les liens externes, nav primaire/secondaire, store settings, vues SettingsView/HelpView. 236 tests verts (+15 nouveaux : auth-tokens, settings-routes).
This commit is contained in:
@@ -23,6 +23,7 @@ import { onMounted, onUnmounted } from 'vue';
|
||||
import { useSessionsStore } from '../../stores/sessions';
|
||||
import { useWorktreesStore } from '../../stores/worktrees';
|
||||
import { useGroupsStore } from '../../stores/groups';
|
||||
import { useSettingsStore } from '../../stores/settings';
|
||||
import AppSidebar from './AppSidebar.vue';
|
||||
import MobileTabBar from './MobileTabBar.vue';
|
||||
|
||||
@@ -31,11 +32,14 @@ defineProps<{ fullbleed?: boolean }>();
|
||||
const sessions = useSessionsStore();
|
||||
const worktrees = useWorktreesStore();
|
||||
const groups = useGroupsStore();
|
||||
const settings = useSettingsStore();
|
||||
|
||||
onMounted(() => {
|
||||
void worktrees.fetchAll();
|
||||
void sessions.fetchSessions();
|
||||
void groups.fetchGroups();
|
||||
void settings.fetch(); // alimente l'icône de nav Gitea et la vue Réglages
|
||||
|
||||
worktrees.startRealtime();
|
||||
sessions.startRealtime();
|
||||
groups.startRealtime();
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</button>
|
||||
<nav class="flex flex-1 flex-col gap-1 px-2">
|
||||
<NavItem
|
||||
v-for="item in items"
|
||||
v-for="item in primary"
|
||||
:key="item.key"
|
||||
:to="item.to"
|
||||
:icon="item.icon"
|
||||
@@ -25,6 +25,19 @@
|
||||
orientation="row"
|
||||
/>
|
||||
</nav>
|
||||
<nav class="flex flex-col gap-1 px-2 pb-1">
|
||||
<NavItem
|
||||
v-for="item in secondary"
|
||||
:key="item.key"
|
||||
:to="item.to"
|
||||
:href="item.href"
|
||||
:icon="item.icon"
|
||||
:label="item.label"
|
||||
:badge="item.badge"
|
||||
:active="isActive(item.match)"
|
||||
orientation="row"
|
||||
/>
|
||||
</nav>
|
||||
<div class="border-t border-zinc-800/80 p-3">
|
||||
<AppShellFooter />
|
||||
</div>
|
||||
@@ -40,6 +53,6 @@ import NavItem from './NavItem.vue';
|
||||
import AppShellFooter from './AppShellFooter.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const { items, isActive } = useNav();
|
||||
const { primary, secondary, isActive } = useNav();
|
||||
const palette = useCommandPalette();
|
||||
</script>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
style="padding-bottom: env(safe-area-inset-bottom)"
|
||||
>
|
||||
<NavItem
|
||||
v-for="item in items"
|
||||
v-for="item in primary"
|
||||
:key="item.key"
|
||||
:to="item.to"
|
||||
:icon="item.icon"
|
||||
@@ -17,24 +17,27 @@
|
||||
<button
|
||||
type="button"
|
||||
class="flex flex-1 flex-col items-center gap-0.5 py-2 text-zinc-500 transition-colors hover:text-zinc-300 focus-visible:outline-none"
|
||||
:class="moreActive ? 'text-emerald-400' : ''"
|
||||
@click="showMore = true"
|
||||
>
|
||||
<Settings :size="20" :stroke-width="1.75" />
|
||||
<span class="text-[10px]">{{ t('nav.settings') }}</span>
|
||||
<Menu :size="20" :stroke-width="1.75" />
|
||||
<span class="text-[10px]">{{ t('nav.more') }}</span>
|
||||
</button>
|
||||
<MoreSheet v-if="showMore" @close="showMore = false" />
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { Settings } from '@lucide/vue';
|
||||
import { Menu } from '@lucide/vue';
|
||||
import { useNav } from '../../composables/useNav';
|
||||
import NavItem from './NavItem.vue';
|
||||
import MoreSheet from './MoreSheet.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const { items, isActive } = useNav();
|
||||
const { primary, secondary, isActive } = useNav();
|
||||
const showMore = ref(false);
|
||||
// « More » est actif quand la route courante appartient aux items secondaires (Réglages/Aide).
|
||||
const moreActive = computed(() => secondary.value.some((i) => isActive(i.match)));
|
||||
</script>
|
||||
|
||||
@@ -7,7 +7,22 @@
|
||||
style="padding-bottom: calc(1rem + env(safe-area-inset-bottom))"
|
||||
>
|
||||
<div class="mx-auto mb-3 h-1 w-10 rounded-full bg-zinc-700" />
|
||||
<AppShellFooter />
|
||||
<nav class="mb-3 flex flex-col gap-1">
|
||||
<NavItem
|
||||
v-for="item in secondary"
|
||||
:key="item.key"
|
||||
:to="item.to"
|
||||
:href="item.href"
|
||||
:icon="item.icon"
|
||||
:label="item.label"
|
||||
:active="isActive(item.match)"
|
||||
orientation="row"
|
||||
@click="emit('close')"
|
||||
/>
|
||||
</nav>
|
||||
<div class="border-t border-zinc-800/80 pt-3">
|
||||
<AppShellFooter />
|
||||
</div>
|
||||
<BaseButton class="mt-3 w-full" @click="emit('close')">{{ t('common.close') }}</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
@@ -16,9 +31,12 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useNav } from '../../composables/useNav';
|
||||
import NavItem from './NavItem.vue';
|
||||
import AppShellFooter from './AppShellFooter.vue';
|
||||
import BaseButton from '../ui/BaseButton.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const { secondary, isActive } = useNav();
|
||||
const emit = defineEmits<{ close: [] }>();
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
<template>
|
||||
<RouterLink
|
||||
:to="to"
|
||||
:aria-current="active ? 'page' : undefined"
|
||||
<!-- RouterLink pour une route interne ; <a target=_blank> pour un lien externe (ex. Gitea). -->
|
||||
<component
|
||||
:is="href ? 'a' : RouterLink"
|
||||
:to="href ? undefined : to"
|
||||
:href="href"
|
||||
:target="href ? '_blank' : undefined"
|
||||
:rel="href ? 'noopener noreferrer' : undefined"
|
||||
:aria-current="!href && active ? 'page' : undefined"
|
||||
:class="orientation === 'col' ? colClass : rowClass"
|
||||
>
|
||||
<span class="relative">
|
||||
@@ -20,16 +25,19 @@
|
||||
>
|
||||
{{ badge }}
|
||||
</span>
|
||||
</RouterLink>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, type Component } from 'vue';
|
||||
import type { RouteLocationRaw } from 'vue-router';
|
||||
import { RouterLink, type RouteLocationRaw } from 'vue-router';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
to: RouteLocationRaw;
|
||||
// `| undefined` requis par exactOptionalPropertyTypes : les items de nav passent `to`/`href`
|
||||
// potentiellement undefined (l'un ou l'autre selon route interne vs lien externe).
|
||||
to?: RouteLocationRaw | undefined;
|
||||
href?: string | undefined;
|
||||
icon: Component;
|
||||
label: string;
|
||||
badge?: number;
|
||||
|
||||
Reference in New Issue
Block a user