feat: overhaul project workspace navigation
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
<script setup lang="ts">
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import {
|
||||
BellRing,
|
||||
GitBranch,
|
||||
KanbanSquare,
|
||||
LayoutTemplate,
|
||||
Settings2,
|
||||
ShieldCheck,
|
||||
Tags,
|
||||
} from 'lucide-vue-next';
|
||||
import { computed } from 'vue';
|
||||
import ProjectTabs from '@/components/projects/ProjectTabs.vue';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import AppLayout from '@/layouts/AppLayout.vue';
|
||||
import { index as projectsIndex } from '@/routes/projects';
|
||||
import { index as boardIndex } from '@/routes/projects/board';
|
||||
import { index as settingsIndex } from '@/routes/projects/settings';
|
||||
import { create as projectTemplateCreate } from '@/routes/projects/templates';
|
||||
import type {
|
||||
BreadcrumbItem,
|
||||
EmbeddedProjectSettingsSectionProps,
|
||||
ProjectAccessPageProps,
|
||||
ProjectBoardSettingsPageProps,
|
||||
ProjectGeneralSettingsPageProps,
|
||||
ProjectNotificationSettingsPageProps,
|
||||
ProjectRepositorySettingsPageProps,
|
||||
ProjectSettingsPageProps,
|
||||
ProjectSettingsSection,
|
||||
ProjectTicketMetadataSettingsPageProps,
|
||||
} from '@/types';
|
||||
import AccessSection from './Access.vue';
|
||||
import BoardSection from './settings/Board.vue';
|
||||
import GeneralSection from './settings/General.vue';
|
||||
import MetadataSection from './settings/Metadata.vue';
|
||||
import NotificationsSection from './settings/Notifications.vue';
|
||||
import RepositoriesSection from './settings/Repositories.vue';
|
||||
|
||||
const props = defineProps<ProjectSettingsPageProps>();
|
||||
|
||||
const breadcrumbs: BreadcrumbItem[] = [
|
||||
{
|
||||
title: 'Projects',
|
||||
href: projectsIndex(),
|
||||
},
|
||||
{
|
||||
title: props.project.name,
|
||||
href: boardIndex(props.project.id),
|
||||
},
|
||||
{
|
||||
title: 'Settings',
|
||||
href: settingsIndex(props.project.id),
|
||||
},
|
||||
];
|
||||
|
||||
const settingsSections = [
|
||||
{
|
||||
description: 'Project identity, templates, and deletion',
|
||||
icon: Settings2,
|
||||
label: 'General',
|
||||
value: 'general' as const,
|
||||
},
|
||||
{
|
||||
description: 'Columns, statuses, and transitions',
|
||||
icon: KanbanSquare,
|
||||
label: 'Board',
|
||||
value: 'board' as const,
|
||||
},
|
||||
{
|
||||
description: 'Types, priorities, fields, and links',
|
||||
icon: Tags,
|
||||
label: 'Metadata',
|
||||
value: 'metadata' as const,
|
||||
},
|
||||
{
|
||||
description: 'Linked source repositories',
|
||||
icon: GitBranch,
|
||||
label: 'Repositories',
|
||||
value: 'repositories' as const,
|
||||
},
|
||||
{
|
||||
description: 'Webhook and Discord automation',
|
||||
icon: BellRing,
|
||||
label: 'Notifications',
|
||||
value: 'notifications' as const,
|
||||
},
|
||||
{
|
||||
description: 'User and team permissions',
|
||||
icon: ShieldCheck,
|
||||
label: 'Access',
|
||||
value: 'access' as const,
|
||||
},
|
||||
];
|
||||
|
||||
const activeSectionDetails = computed(
|
||||
() =>
|
||||
settingsSections.find(
|
||||
(section) => section.value === props.activeSection,
|
||||
) ?? settingsSections[0],
|
||||
);
|
||||
|
||||
const accessSectionData = computed(
|
||||
() =>
|
||||
props.sectionData as EmbeddedProjectSettingsSectionProps<ProjectAccessPageProps>,
|
||||
);
|
||||
const boardSectionData = computed(
|
||||
() =>
|
||||
props.sectionData as EmbeddedProjectSettingsSectionProps<ProjectBoardSettingsPageProps>,
|
||||
);
|
||||
const generalSectionData = computed(
|
||||
() =>
|
||||
props.sectionData as EmbeddedProjectSettingsSectionProps<ProjectGeneralSettingsPageProps>,
|
||||
);
|
||||
const metadataSectionData = computed(
|
||||
() =>
|
||||
props.sectionData as EmbeddedProjectSettingsSectionProps<ProjectTicketMetadataSettingsPageProps>,
|
||||
);
|
||||
const notificationsSectionData = computed(
|
||||
() =>
|
||||
props.sectionData as EmbeddedProjectSettingsSectionProps<ProjectNotificationSettingsPageProps>,
|
||||
);
|
||||
const repositoriesSectionData = computed(
|
||||
() =>
|
||||
props.sectionData as EmbeddedProjectSettingsSectionProps<ProjectRepositorySettingsPageProps>,
|
||||
);
|
||||
|
||||
const sectionHref = (section: ProjectSettingsSection) =>
|
||||
settingsIndex(props.project.id, { query: { section } });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout :breadcrumbs="breadcrumbs" :project="project">
|
||||
<Head :title="`${project.name} settings`" />
|
||||
|
||||
<div class="app-page max-w-none gap-5">
|
||||
<header
|
||||
class="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between"
|
||||
>
|
||||
<div class="space-y-2">
|
||||
<Badge variant="outline" class="rounded px-2.5 py-1">
|
||||
{{ project.key }}
|
||||
</Badge>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">
|
||||
{{ project.name }}
|
||||
</h1>
|
||||
<p class="text-muted-foreground mt-1 text-sm">
|
||||
Project configuration and access controls.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
v-if="
|
||||
activeSection === 'general' &&
|
||||
$page.props.auth.permissions.projectCreate
|
||||
"
|
||||
as-child
|
||||
variant="outline"
|
||||
>
|
||||
<Link :href="projectTemplateCreate(project.id)">
|
||||
<LayoutTemplate class="size-4" />
|
||||
Save as template
|
||||
</Link>
|
||||
</Button>
|
||||
</header>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<ProjectTabs
|
||||
active="settings"
|
||||
:can-manage-project="true"
|
||||
:project="project"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="status"
|
||||
:class="
|
||||
error
|
||||
? 'rounded border border-red-300 bg-red-50 px-4 py-3 text-sm text-red-800 dark:border-red-900 dark:bg-red-950/40 dark:text-red-200'
|
||||
: 'status-success'
|
||||
"
|
||||
>
|
||||
{{ status }}
|
||||
</div>
|
||||
|
||||
<div class="grid min-w-0 gap-5 xl:grid-cols-[15rem_minmax(0,1fr)]">
|
||||
<nav
|
||||
aria-label="Project settings"
|
||||
class="border-outline-variant/70 bg-surface-container-lowest flex gap-1 overflow-x-auto rounded-md border p-1.5 xl:sticky xl:top-20 xl:flex-col xl:self-start xl:overflow-visible"
|
||||
>
|
||||
<Link
|
||||
v-for="section in settingsSections"
|
||||
:key="section.value"
|
||||
:href="sectionHref(section.value)"
|
||||
preserve-scroll
|
||||
:class="[
|
||||
'flex min-w-max items-center gap-3 rounded px-3 py-2.5 text-left text-sm transition-colors xl:min-w-0',
|
||||
activeSection === section.value
|
||||
? 'bg-primary/10 text-primary font-semibold'
|
||||
: 'text-muted-foreground hover:bg-muted hover:text-foreground',
|
||||
]"
|
||||
>
|
||||
<component :is="section.icon" class="size-4 shrink-0" />
|
||||
<span class="min-w-0">
|
||||
<span class="block">{{ section.label }}</span>
|
||||
<span
|
||||
class="mt-0.5 hidden text-xs font-normal opacity-75 xl:block"
|
||||
>
|
||||
{{ section.description }}
|
||||
</span>
|
||||
</span>
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
<section class="min-w-0 space-y-5">
|
||||
<div class="border-outline-variant/70 border-b pb-4">
|
||||
<h2 class="text-lg font-bold tracking-tight">
|
||||
{{ activeSectionDetails.label }}
|
||||
</h2>
|
||||
<p class="text-muted-foreground mt-1 text-sm">
|
||||
{{ activeSectionDetails.description }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<GeneralSection
|
||||
v-if="activeSection === 'general'"
|
||||
v-bind="generalSectionData"
|
||||
:project="project"
|
||||
:status="status"
|
||||
embedded
|
||||
/>
|
||||
<BoardSection
|
||||
v-else-if="activeSection === 'board'"
|
||||
v-bind="boardSectionData"
|
||||
:project="project"
|
||||
:status="status"
|
||||
embedded
|
||||
/>
|
||||
<MetadataSection
|
||||
v-else-if="activeSection === 'metadata'"
|
||||
v-bind="metadataSectionData"
|
||||
:project="project"
|
||||
:status="status"
|
||||
:error="error"
|
||||
embedded
|
||||
/>
|
||||
<RepositoriesSection
|
||||
v-else-if="activeSection === 'repositories'"
|
||||
v-bind="repositoriesSectionData"
|
||||
:project="project"
|
||||
:status="status"
|
||||
embedded
|
||||
/>
|
||||
<NotificationsSection
|
||||
v-else-if="activeSection === 'notifications'"
|
||||
v-bind="notificationsSectionData"
|
||||
:project="project"
|
||||
:status="status"
|
||||
embedded
|
||||
/>
|
||||
<AccessSection
|
||||
v-else
|
||||
v-bind="accessSectionData"
|
||||
:project="project"
|
||||
:status="status"
|
||||
embedded
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
</template>
|
||||
Reference in New Issue
Block a user