Integration Credentials: - Add IntegrationCredential model with encrypted payload - Add full CRUD: ManageIntegrationCredentialsAction, 4 Tasks, Controller, Requests - Add migration for integration_credentials table - Add Vue pages: Index, Create, Edit - AI tasks now read API keys from DB instead of .env - Add sidebar menu item Deploy UI: - Add DeployTask with full deploy lifecycle management - Extend DeployController with index, log, history endpoints - Add Deploy/Index.vue with live polling and deploy log viewer - Add sidebar menu item and QuickAction for Sveden UI: - Add 'key' icon to SidebarNavItem - Change deploy button color to amber in Main.vue
132 lines
4.1 KiB
Vue
132 lines
4.1 KiB
Vue
<template>
|
|
<!-- Simple link (no children) -->
|
|
<Link
|
|
v-if="!item.children"
|
|
:href="route(item.route)"
|
|
class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200 group"
|
|
:class="linkClass"
|
|
:aria-current="isActiveItem ? 'page' : null"
|
|
>
|
|
<DashboardIcon :name="iconComponent" size="5" class="flex-shrink-0" />
|
|
<span>{{ item.label }}</span>
|
|
</Link>
|
|
|
|
<!-- Collapsible section -->
|
|
<div v-else class="space-y-1">
|
|
<button
|
|
@click="toggle"
|
|
class="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200 group"
|
|
:class="linkClass"
|
|
:aria-expanded="isExpanded"
|
|
:aria-controls="`sidebar-section-${item.key}`"
|
|
>
|
|
<DashboardIcon :name="iconComponent" size="5" class="flex-shrink-0" />
|
|
<span class="flex-1 text-left">{{ item.label }}</span>
|
|
<DashboardIcon
|
|
name="chevron-down"
|
|
size="4"
|
|
class="transition-transform duration-200"
|
|
:class="{ 'rotate-180': isExpanded }"
|
|
aria-hidden="true"
|
|
/>
|
|
</button>
|
|
|
|
<div v-show="isExpanded" :id="`sidebar-section-${item.key}`" class="ml-8 space-y-1">
|
|
<Link
|
|
v-for="child in item.children"
|
|
:key="child.route"
|
|
:href="route(child.route)"
|
|
class="flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-all duration-200"
|
|
:class="getChildClass(child.route)"
|
|
@click="onChildClick"
|
|
>
|
|
<span class="w-1.5 h-1.5 rounded-full bg-current" aria-hidden="true"></span>
|
|
{{ child.label }}
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, ref, watch } from 'vue';
|
|
import { Link, usePage } from '@inertiajs/vue3';
|
|
import DashboardIcon from './DashboardIcon.vue';
|
|
|
|
const props = defineProps({
|
|
item: { type: Object, required: true },
|
|
mobile: { type: Boolean, default: false },
|
|
isExpanded: { type: Boolean, default: false },
|
|
});
|
|
|
|
const emit = defineEmits(['child-click', 'toggle']);
|
|
|
|
const iconMap = {
|
|
home: 'home',
|
|
document: 'document-text',
|
|
book: 'book-open',
|
|
calendar: 'calendar',
|
|
image: 'photo',
|
|
building: 'building-office',
|
|
cog: 'cog-6-tooth',
|
|
upload: 'document-text',
|
|
folder: 'folder',
|
|
beaker: 'beaker',
|
|
'user-circle': 'user-circle',
|
|
'rectangle-stack': 'rectangle-stack',
|
|
'arrow-path': 'arrow-path',
|
|
'academic-cap': 'academic-cap',
|
|
'clipboard-document-check': 'clipboard-document-check',
|
|
key: 'key',
|
|
};
|
|
|
|
const iconComponent = computed(() => iconMap[props.item.icon] || 'home');
|
|
|
|
const isActiveItem = computed(() => {
|
|
if (!props.item.route) return false;
|
|
return route().current(props.item.route);
|
|
});
|
|
|
|
const isSectionActive = computed(() => {
|
|
if (!props.item.activePrefixes) return false;
|
|
return props.item.activePrefixes.some((prefix) => route().current(prefix + '*'));
|
|
});
|
|
|
|
// Локальное состояние для ручного переключения
|
|
const localExpanded = ref(props.isExpanded);
|
|
|
|
// При изменении пропса обновляем локальное состояние
|
|
watch(() => props.isExpanded, (val) => {
|
|
localExpanded.value = val;
|
|
});
|
|
|
|
// При смене роута сбрасываем на значение пропса (только активный аккордеон)
|
|
watch(() => route().current(), () => {
|
|
localExpanded.value = props.isExpanded;
|
|
});
|
|
|
|
const toggle = () => {
|
|
localExpanded.value = !localExpanded.value;
|
|
};
|
|
|
|
// Используем локальное состояние в template
|
|
const isExpanded = computed(() => localExpanded.value);
|
|
|
|
const linkClass = computed(() => {
|
|
const active = isActiveItem.value || isSectionActive.value;
|
|
const hasChildren = !!props.item.children;
|
|
return active
|
|
? (hasChildren ? 'bg-primary/5 text-primary' : 'bg-primary/10 text-primary')
|
|
: 'text-foreground hover:bg-primary-50 hover:text-primary';
|
|
});
|
|
|
|
const getChildClass = (childRoute) => {
|
|
return route().current(childRoute)
|
|
? 'bg-primary/10 text-primary font-medium'
|
|
: 'text-muted-foreground-1 hover:bg-primary-50 hover:text-primary';
|
|
};
|
|
|
|
const onChildClick = () => {
|
|
emit('child-click');
|
|
};
|
|
</script>
|