Files
KanbanGit/resources/js/pages/projects/Settings.vue
T
Twyco 5f917e93eb
linter / quality (pull_request) Successful in 1m21s
tests / ci (8.4) (pull_request) Successful in 1m44s
fix: polish project workspace interactions
2026-07-18 18:49:23 +02:00

264 lines
9.3 KiB
Vue

<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 ProjectPageHeader from '@/components/projects/ProjectPageHeader.vue';
import ProjectTabs from '@/components/projects/ProjectTabs.vue';
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">
<ProjectPageHeader
:project="project"
context="Project settings"
description="Project configuration and access controls."
>
<Button
v-if="
activeSection === 'general' &&
$page.props.auth.permissions.projectCreate
"
as-child
size="sm"
variant="outline"
>
<Link :href="projectTemplateCreate(project.id)">
<LayoutTemplate class="size-4" />
Save as template
</Link>
</Button>
</ProjectPageHeader>
<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>