feat: mobile ui (#1096)

This commit is contained in:
Jonas Plum
2024-08-07 22:18:59 +02:00
committed by GitHub
parent 96b7a9604c
commit a2dd6c05e6
68 changed files with 668 additions and 1315 deletions

View File

@@ -7,7 +7,7 @@
<title>Catalyst</title>
</head>
<body>
<div id="app" class="h-screen w-screen"></div>
<div id="app" class="h-screen w-screen overflow-hidden"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -7,17 +7,16 @@ defineProps<{
isPending: boolean
isError: boolean
error: Error | null
value: any
}>()
</script>
<template>
<div v-if="isPending" class="flex justify-center">
<LoaderCircle class="h-16 w-16 animate-spin text-primary" />
<div v-if="isPending" class="flex h-full w-full">
<LoaderCircle class="m-auto h-16 w-16 animate-spin text-primary" />
</div>
<Alert v-else-if="isError" variant="destructive" class="mb-4">
<AlertTitle>Error</AlertTitle>
<AlertDescription>{{ error }}</AlertDescription>
</Alert>
<slot v-else-if="value" />
<slot v-else />
</template>

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import PanelListElement from '@/components/common/PanelListElement.vue'
import TanView from '@/components/TanView.vue'
import PanelListElement from '@/components/layout/PanelListElement.vue'
import { buttonVariants } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
@@ -32,24 +33,31 @@ const {
<template>
<div class="flex flex-col gap-2">
<Card>
<div v-if="tasks && tasks.length === 0" class="p-2 text-center text-sm text-gray-500">
No open tasks
</div>
<PanelListElement v-else v-for="task in tasks" :key="task.id" class="pr-1">
<span>{{ task.name }}</span>
<RouterLink
:to="{
name: 'tickets',
params: { type: task.expand.ticket.type, id: task.expand.ticket.id }
}"
:class="cn(buttonVariants({ variant: 'outline', size: 'sm' }), 'ml-auto h-8')"
>
<span class="flex flex-row items-center text-sm text-gray-500">
Go to {{ task.expand.ticket.name }}
<ChevronRight class="ml-2 h-4 w-4" />
</span>
</RouterLink>
</PanelListElement>
<TanView :isError="isError" :isPending="isPending" :error="error">
<div v-if="tasks && tasks.length === 0" class="p-2 text-center text-sm text-gray-500">
No open tasks
</div>
<PanelListElement v-else v-for="task in tasks" :key="task.id" class="pr-1">
<span>{{ task.name }}</span>
<RouterLink
:to="{
name: 'tickets',
params: { type: task.expand.ticket.type, id: task.expand.ticket.id }
}"
:class="
cn(
buttonVariants({ variant: 'outline', size: 'sm' }),
'h-8 w-full sm:ml-auto sm:w-auto'
)
"
>
<span class="flex flex-row items-center text-sm text-gray-500">
Go to {{ task.expand.ticket.name }}
<ChevronRight class="ml-2 h-4 w-4" />
</span>
</RouterLink>
</PanelListElement>
</TanView>
</Card>
</div>
</template>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import PanelListElement from '@/components/common/PanelListElement.vue'
import PanelListElement from '@/components/layout/PanelListElement.vue'
import { buttonVariants } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import { Separator } from '@/components/ui/separator'
@@ -42,16 +42,21 @@ const age = (ticket: Ticket) =>
</div>
<PanelListElement v-else v-for="ticket in tickets" :key="ticket.id" class="gap-2 pr-1">
<span>{{ ticket.name }}</span>
<Separator orientation="vertical" class="h-4" />
<Separator orientation="vertical" class="hidden h-4 sm:block" />
<span class="text-sm text-muted-foreground">{{ ticket.expand.type.singular }}</span>
<Separator orientation="vertical" class="h-4" />
<Separator orientation="vertical" class="hidden h-4 sm:block" />
<span class="text-sm text-muted-foreground">Open since {{ age(ticket) }} days</span>
<RouterLink
:to="{
name: 'tickets',
params: { type: ticket.type, id: ticket.id }
}"
:class="cn(buttonVariants({ variant: 'outline', size: 'sm' }), 'ml-auto h-8')"
:class="
cn(
buttonVariants({ variant: 'outline', size: 'sm' }),
'h-8 w-full sm:ml-auto sm:w-auto'
)
"
>
<span class="flex flex-row items-center text-sm text-gray-500">
Go to {{ ticket.name }}

View File

@@ -43,7 +43,7 @@ const ticketsPerWeek = computed(() => {
</script>
<template>
<TanView :isError="isError" :isPending="isPending" :error="error" :value="tickets">
<TanView :isError="isError" :isPending="isPending" :error="error">
<LineChart class="h-40" :data="ticketsPerWeek" index="week" :categories="['count']" />
</TanView>
</template>

View File

@@ -30,7 +30,7 @@ const namedTypes = computed(() => {
</script>
<template>
<TanView :isError="isError" :isPending="isPending" :error="error" :value="namedTypes">
<TanView :isError="isError" :isPending="isPending" :error="error">
<div v-if="namedTypes" class="flex flex-1 items-center">
<DonutChart index="plural" type="donut" category="count" :data="namedTypes" />
</div>

View File

@@ -3,9 +3,6 @@ import ShortCut from '@/components/ShortCut.vue'
import { ref } from 'vue'
// import { Textarea } from '@/components/ui/textarea'
// import { Input } from '@/components/ui/input'
const model = defineModel({
type: String
})

View File

@@ -0,0 +1,5 @@
<template>
<div class="flex flex-1 items-start justify-start overflow-y-auto overflow-x-hidden">
<slot />
</div>
</template>

View File

@@ -0,0 +1,13 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
defineProps<{
small?: boolean
}>()
</script>
<template>
<div :class="cn('mx-auto flex w-full max-w-[72rem] gap-4 p-4', small && 'max-w-[47rem]')">
<slot />
</div>
</template>

View File

@@ -0,0 +1,25 @@
<script setup lang="ts">
import { Separator } from '@/components/ui/separator'
import { cn } from '@/lib/utils'
defineProps<{
title?: string
nowrap?: boolean
hideSeparator?: boolean
}>()
</script>
<template>
<div
:class="
cn('flex min-h-14 flex-wrap items-center gap-2 bg-background p-2', nowrap && 'flex-nowrap')
"
>
<h1 v-if="title" class="text-xl font-bold">
{{ title }}
</h1>
<slot />
</div>
<Separator v-if="!hideSeparator" />
</template>

View File

@@ -12,7 +12,7 @@ const props = defineProps<{
<div
:class="
cn(
'flex w-full items-center border-t px-2 py-1 first:rounded-t first:border-none last:rounded-b',
'flex w-full flex-col items-start border-t px-2 py-1 first:rounded-t first:border-none last:rounded-b sm:flex-row sm:items-center',
props.class
)
"

View File

@@ -8,57 +8,78 @@ import { Separator } from '@/components/ui/separator'
import { Menu } from 'lucide-vue-next'
import { cn } from '@/lib/utils'
import { useCatalystStore } from '@/store/catalyst'
const catalystStore = useCatalystStore()
</script>
<template>
<div class="flex h-[57px] items-center border-b bg-background">
<CatalystLogo
class="size-8"
:class="{ 'flex-1': catalystStore.sidebarCollapsed, 'mx-3': !catalystStore.sidebarCollapsed }"
/>
<h1 class="text-xl font-bold" v-if="!catalystStore.sidebarCollapsed">Catalyst</h1>
</div>
<NavList
:is-collapsed="catalystStore.sidebarCollapsed"
:links="[
{
title: 'Dashboard',
icon: 'PanelsTopLeft',
variant: 'ghost',
to: '/dashboard'
}
]"
/>
<Separator />
<IncidentNav :is-collapsed="catalystStore.sidebarCollapsed" />
<div class="flex-1" />
<Separator />
<NavList
:is-collapsed="catalystStore.sidebarCollapsed"
:links="[
{
title: 'Reactions',
icon: 'Zap',
variant: 'ghost',
to: '/reactions'
}
]"
/>
<Separator />
<UserDropDown :is-collapsed="catalystStore.sidebarCollapsed" />
<Separator />
<Button
variant="ghost"
@click="catalystStore.toggleSidebar()"
size="sm"
class="m-2 justify-start px-3.5"
<div
:class="
cn(
'flex min-w-48 shrink-0 flex-col border-r bg-popover', // transition-all duration-300 ease-in-out',
catalystStore.sidebarCollapsed && 'min-w-[50px]'
)
"
>
<Menu class="size-4" />
<span v-if="!catalystStore.sidebarCollapsed" class="ml-2">Toggle Sidebar</span>
</Button>
<div class="flex h-[57px] items-center border-b bg-background">
<CatalystLogo
class="size-8"
:class="{
'flex-1': catalystStore.sidebarCollapsed,
'mx-3': !catalystStore.sidebarCollapsed
}"
/>
<h1 class="text-xl font-bold" v-if="!catalystStore.sidebarCollapsed">Catalyst</h1>
</div>
<NavList
:is-collapsed="catalystStore.sidebarCollapsed"
:links="[
{
title: 'Dashboard',
icon: 'PanelsTopLeft',
variant: 'ghost',
to: '/dashboard'
}
]"
/>
<Separator />
<IncidentNav :is-collapsed="catalystStore.sidebarCollapsed" />
<div class="flex-1" />
<Separator />
<NavList
:is-collapsed="catalystStore.sidebarCollapsed"
:links="[
{
title: 'Reactions',
icon: 'Zap',
variant: 'ghost',
to: '/reactions'
}
]"
/>
<Separator />
<UserDropDown :is-collapsed="catalystStore.sidebarCollapsed" />
<Separator />
<div :class="cn('flex h-14 items-center px-3', !catalystStore.sidebarCollapsed && 'px-2')">
<Button
variant="ghost"
@click="catalystStore.toggleSidebar()"
size="default"
:class="
cn(
'p-0',
catalystStore.sidebarCollapsed && 'w-9',
!catalystStore.sidebarCollapsed && 'w-full justify-start px-3'
)
"
>
<Menu class="size-4" />
<span v-if="!catalystStore.sidebarCollapsed" class="ml-2">Toggle Sidebar</span>
</Button>
</div>
</div>
</template>

View File

@@ -3,29 +3,37 @@ import SideBar from '@/components/layout/SideBar.vue'
import { TooltipProvider } from '@/components/ui/tooltip'
import { cn } from '@/lib/utils'
import { useCatalystStore } from '@/store/catalyst'
const catalystStore = useCatalystStore()
defineProps<{
showDetails?: boolean
}>()
</script>
<template>
<TooltipProvider :delay-duration="0">
<div class="flex h-full flex-row items-stretch bg-muted/40">
<SideBar />
<div
:class="
cn(
'flex min-w-48 flex-col border-r bg-popover', // transition-all duration-300 ease-in-out',
catalystStore.sidebarCollapsed && 'min-w-[50px]'
'w-full flex-initial border-r sm:w-72',
!showDetails && 'flex',
showDetails && 'hidden sm:flex'
)
"
>
<SideBar />
<div class="flex h-full w-full flex-col">
<slot name="list" />
</div>
</div>
<div class="w-72 flex-initial border-r">
<slot name="list" />
</div>
<div class="flex-1">
<slot name="single" />
<div
:class="
cn('flex-1 overflow-hidden', !showDetails && 'hidden sm:flex', showDetails && 'flex')
"
>
<div class="flex h-full w-full flex-1 flex-col">
<slot name="single" />
</div>
</div>
</div>
</TooltipProvider>

View File

@@ -1,27 +1,15 @@
<script lang="ts" setup>
import SideBar from '@/components/layout/SideBar.vue'
import { TooltipProvider } from '@/components/ui/tooltip'
import { cn } from '@/lib/utils'
import { useCatalystStore } from '@/store/catalyst'
const catalystStore = useCatalystStore()
</script>
<template>
<TooltipProvider :delay-duration="0">
<div class="flex h-full flex-row items-stretch bg-muted/40">
<div
:class="
cn(
'flex min-w-48 flex-col border-r bg-popover', // transition-all duration-300 ease-in-out',
catalystStore.sidebarCollapsed && 'min-w-[50px]'
)
"
>
<SideBar />
<SideBar />
<div class="flex h-full w-full flex-col">
<slot />
</div>
<slot />
</div>
</TooltipProvider>
</template>

View File

@@ -1,11 +1,15 @@
<script setup lang="ts">
import TanView from '@/components/TanView.vue'
import DeleteDialog from '@/components/common/DeleteDialog.vue'
import ColumnBody from '@/components/layout/ColumnBody.vue'
import ColumnBodyContainer from '@/components/layout/ColumnBodyContainer.vue'
import ColumnHeader from '@/components/layout/ColumnHeader.vue'
import ReactionForm from '@/components/reaction/ReactionForm.vue'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Separator } from '@/components/ui/separator'
import { Button } from '@/components/ui/button'
import { toast } from '@/components/ui/toast'
import { ChevronLeft } from 'lucide-vue-next'
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
import { onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
@@ -72,28 +76,29 @@ onUnmounted(() => {
</script>
<template>
<TanView :isError="isError" :isPending="isPending" :error="error" :value="reaction">
<div class="flex h-full flex-1 flex-col overflow-hidden">
<div class="flex items-center bg-background px-4 py-2">
<div class="ml-auto">
<DeleteDialog
v-if="reaction"
collection="reactions"
:id="reaction.id"
:name="reaction.name"
:singular="'Reaction'"
:to="{ name: 'reactions' }"
:queryKey="['reactions']"
/>
</div>
<TanView :isError="isError" :isPending="isPending" :error="error">
<ColumnHeader>
<Button @click="router.push({ name: 'reactions' })" variant="outline" class="sm:hidden">
<ChevronLeft class="mr-2 size-4" />
Back
</Button>
<div class="ml-auto">
<DeleteDialog
v-if="reaction"
collection="reactions"
:id="reaction.id"
:name="reaction.name"
:singular="'Reaction'"
:to="{ name: 'reactions' }"
:queryKey="['reactions']"
/>
</div>
<Separator />
</ColumnHeader>
<ScrollArea v-if="reaction" class="flex-1">
<div class="flex max-w-[640px] flex-col gap-4 p-4">
<ReactionForm :reaction="reaction" @submit="updateReactionMutation.mutate" />
</div>
</ScrollArea>
</div>
<ColumnBody v-if="reaction">
<ColumnBodyContainer small>
<ReactionForm :reaction="reaction" @submit="updateReactionMutation.mutate" />
</ColumnBodyContainer>
</ColumnBody>
</TanView>
</template>

View File

@@ -239,7 +239,7 @@ const curlExample = computed(() => {
</script>
<template>
<form @submit="onSubmit" class="flex flex-col items-start gap-4">
<form @submit="onSubmit" class="flex w-full flex-col items-start gap-4">
<FormField name="name" v-slot="{ componentField }" validate-on-input>
<FormItem class="w-full">
<FormLabel for="name" class="text-right">Name</FormLabel>

View File

@@ -1,8 +1,8 @@
<script setup lang="ts">
import TanView from '@/components/TanView.vue'
import ResourceListElement from '@/components/common/ResourceListElement.vue'
import ColumnHeader from '@/components/layout/ColumnHeader.vue'
import ResourceListElement from '@/components/layout/ResourceListElement.vue'
import { Button } from '@/components/ui/button'
import { Separator } from '@/components/ui/separator'
import { useQuery, useQueryClient } from '@tanstack/vue-query'
import { onMounted } from 'vue'
@@ -63,32 +63,28 @@ onMounted(() => {
</script>
<template>
<TanView :isError="isError" :isPending="isPending" :error="error" :value="reactions">
<div class="flex h-screen flex-col">
<div class="flex items-center bg-background px-4 py-2">
<h1 class="text-xl font-bold">Reactions</h1>
<div class="ml-auto">
<Button variant="ghost" @click="openNew"> New Reaction</Button>
</div>
</div>
<Separator />
<div class="mt-2 flex flex-1 flex-col gap-2 p-4 pt-0">
<TransitionGroup name="list" appear>
<ResourceListElement
v-for="reaction in reactions"
:key="reaction.id"
:title="reaction.name"
:created="reaction.created"
:subtitle="subtitle(reaction)"
description=""
:active="route.params.id === reaction.id"
:to="{ name: 'reactions', params: { id: reaction.id } }"
:open="false"
>
{{ reaction.name }}
</ResourceListElement>
</TransitionGroup>
<TanView :isError="isError" :isPending="isPending" :error="error">
<ColumnHeader title="Reactions">
<div class="ml-auto">
<Button variant="ghost" @click="openNew">New Reaction</Button>
</div>
</ColumnHeader>
<div class="mt-2 flex flex-1 flex-col gap-2 p-2 pt-0">
<TransitionGroup name="list" appear>
<ResourceListElement
v-for="reaction in reactions"
:key="reaction.id"
:title="reaction.name"
:created="reaction.created"
:subtitle="subtitle(reaction)"
description=""
:active="route.params.id === reaction.id"
:to="{ name: 'reactions', params: { id: reaction.id } }"
:open="false"
>
{{ reaction.name }}
</ResourceListElement>
</TransitionGroup>
</div>
</TanView>
</template>

View File

@@ -1,7 +1,11 @@
<script setup lang="ts">
import ColumnBody from '@/components/layout/ColumnBody.vue'
import ColumnBodyContainer from '@/components/layout/ColumnBodyContainer.vue'
import ColumnHeader from '@/components/layout/ColumnHeader.vue'
import ReactionForm from '@/components/reaction/ReactionForm.vue'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Separator } from '@/components/ui/separator'
import { Button } from '@/components/ui/button'
import { ChevronLeft } from 'lucide-vue-next'
import { useMutation, useQueryClient } from '@tanstack/vue-query'
import { useRouter } from 'vue-router'
@@ -24,14 +28,16 @@ const addReactionMutation = useMutation({
</script>
<template>
<div class="flex h-full flex-1 flex-col overflow-hidden">
<div class="flex min-h-14 items-center bg-background px-4 py-2"></div>
<Separator />
<ColumnHeader>
<Button @click="router.push({ name: 'reactions' })" variant="outline" class="sm:hidden">
<ChevronLeft class="mr-2 size-4" />
Back
</Button>
</ColumnHeader>
<ScrollArea class="flex-1">
<div class="flex max-w-[640px] flex-col gap-4 p-4">
<ReactionForm @submit="addReactionMutation.mutate" />
</div>
</ScrollArea>
</div>
<ColumnBody>
<ColumnBodyContainer small>
<ReactionForm @submit="addReactionMutation.mutate" />
</ColumnBodyContainer>
</ColumnBody>
</template>

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import Icon from '@/components/Icon.vue'
import DeleteDialog from '@/components/common/DeleteDialog.vue'
import ColumnHeader from '@/components/layout/ColumnHeader.vue'
import TicketCloseDialog from '@/components/ticket/TicketCloseDialog.vue'
import TicketUserSelect from '@/components/ticket/TicketUserSelect.vue'
import { Button } from '@/components/ui/button'
@@ -12,7 +13,7 @@ import {
} from '@/components/ui/dropdown-menu'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { Check, CircleDot, Repeat } from 'lucide-vue-next'
import { Check, ChevronLeft, CircleDot, Repeat } from 'lucide-vue-next'
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
import { computed, ref } from 'vue'
@@ -49,7 +50,7 @@ const changeTypeMutation = useMutation({
}),
onSuccess: (data: Ticket) => {
queryClient.invalidateQueries({ queryKey: ['tickets'] })
router.push({ name: 'tickets', params: { type: data.type, id: props.ticket.id } })
// router.push({ name: 'tickets', params: { type: data.type, id: props.ticket.id } })
},
onError: handleError
})
@@ -74,74 +75,81 @@ const closeTicketDialogOpen = ref(false)
</script>
<template>
<div class="flex items-center justify-between bg-background p-2">
<div class="flex items-center gap-2">
<Tooltip>
<TooltipTrigger as-child>
<div>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="outline" :disabled="!ticket">
<Icon :name="ticket.expand.type.icon" class="mr-2 size-4" />
{{ ticket.expand.type.singular }}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
v-for="type in otherTypes"
:key="type.id"
class="cursor-pointer"
@click="changeTypeMutation.mutate(type.id)"
>
<Icon :name="type.icon" class="mr-2 size-4" />
Convert to {{ type.singular }}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</TooltipTrigger>
<TooltipContent>Change Type</TooltipContent>
</Tooltip>
<TicketCloseDialog v-model="closeTicketDialogOpen" :ticket="ticket" />
<Tooltip>
<TooltipTrigger as-child>
<div>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="outline" :disabled="!ticket">
<CircleDot v-if="ticket.open" class="mr-2 h-4 w-4" />
<Check v-else class="mr-2 h-4 w-4" />
{{ ticket?.open ? 'Open' : 'Closed' }}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
v-if="ticket.open"
class="cursor-pointer"
@click="closeTicketDialogOpen = true"
>
<Check class="mr-2 size-4" />
Close Ticket
</DropdownMenuItem>
<DropdownMenuItem v-else class="cursor-pointer" @click="closeTicketMutation.mutate">
<Repeat class="mr-2 size-4" />
Reopen Ticket
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</TooltipTrigger>
<TooltipContent>Change Status</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<div>
<TicketUserSelect :key="ticket.owner" :uID="ticket.owner" :ticket="ticket" />
</div>
</TooltipTrigger>
<TooltipContent>Change User</TooltipContent>
</Tooltip>
</div>
<ColumnHeader>
<Button
@click="router.push({ name: 'tickets', params: { type: ticket.type } })"
variant="outline"
class="sm:hidden"
>
<ChevronLeft class="mr-2 size-4" />
Back
</Button>
<Tooltip>
<TooltipTrigger as-child>
<div>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="outline" :disabled="!ticket">
<Icon :name="ticket.expand.type.icon" class="mr-2 size-4" />
{{ ticket.expand.type.singular }}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
v-for="type in otherTypes"
:key="type.id"
class="cursor-pointer"
@click="changeTypeMutation.mutate(type.id)"
>
<Icon :name="type.icon" class="mr-2 size-4" />
Convert to {{ type.singular }}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</TooltipTrigger>
<TooltipContent>Change Type</TooltipContent>
</Tooltip>
<TicketCloseDialog v-model="closeTicketDialogOpen" :ticket="ticket" />
<Tooltip>
<TooltipTrigger as-child>
<div>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="outline" :disabled="!ticket">
<CircleDot v-if="ticket.open" class="mr-2 h-4 w-4" />
<Check v-else class="mr-2 h-4 w-4" />
{{ ticket?.open ? 'Open' : 'Closed' }}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
v-if="ticket.open"
class="cursor-pointer"
@click="closeTicketDialogOpen = true"
>
<Check class="mr-2 size-4" />
Close Ticket
</DropdownMenuItem>
<DropdownMenuItem v-else class="cursor-pointer" @click="closeTicketMutation.mutate">
<Repeat class="mr-2 size-4" />
Reopen Ticket
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</TooltipTrigger>
<TooltipContent>Change Status</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<div>
<TicketUserSelect :key="ticket.owner" :uID="ticket.owner" :ticket="ticket" />
</div>
</TooltipTrigger>
<TooltipContent>Change User</TooltipContent>
</Tooltip>
<div class="-mx-1 flex-1" />
<DeleteDialog
v-if="ticket"
:collection="'tickets'"
@@ -151,5 +159,5 @@ const closeTicketDialogOpen = ref(false)
:to="{ name: 'tickets' }"
:queryKey="['tickets']"
/>
</div>
</ColumnHeader>
</template>

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import ColumnHeader from '@/components/layout/ColumnHeader.vue'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
@@ -38,7 +39,7 @@ const closeButtonDisabled = false // computed(() => !props.ticket.open || messag
</script>
<template>
<div class="flex items-center justify-between gap-2 bg-background p-2">
<ColumnHeader nowrap hideSeparator>
<Input v-if="ticket.open" v-model="resolution" placeholder="Closing reason" />
<div v-else class="flex-1">
<p class="ml-2 text-gray-500">Closed: {{ ticket.resolution }}</p>
@@ -56,5 +57,5 @@ const closeButtonDisabled = false // computed(() => !props.ticket.open || messag
: 'Reopen ' + props.ticket.expand.type.singular
}}
</Button>
</div>
</ColumnHeader>
</template>

View File

@@ -2,6 +2,8 @@
import TanView from '@/components/TanView.vue'
import JSONSchemaFormFields from '@/components/form/JSONSchemaFormFields.vue'
import DynamicMDEditor from '@/components/input/DynamicMDEditor.vue'
import ColumnBody from '@/components/layout/ColumnBody.vue'
import ColumnBodyContainer from '@/components/layout/ColumnBodyContainer.vue'
import StatusIcon from '@/components/ticket/StatusIcon.vue'
import TicketActionBar from '@/components/ticket/TicketActionBar.vue'
import TicketCloseBar from '@/components/ticket/TicketCloseBar.vue'
@@ -15,14 +17,13 @@ import TicketTimeline from '@/components/ticket/timeline/TicketTimeline.vue'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Separator } from '@/components/ui/separator'
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Edit } from 'lucide-vue-next'
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { computed, ref } from 'vue'
import { useRoute } from 'vue-router'
import { pb } from '@/lib/pocketbase'
@@ -100,87 +101,97 @@ const updateDescription = (value: string) => (message.value = value)
</script>
<template>
<TanView :isError="isError" :isPending="isPending" :error="error" :value="ticket">
<div v-if="ticket" class="flex h-full flex-col">
<TicketActionBar :ticket="ticket" />
<Separator />
<div class="flex w-full max-w-7xl flex-1 flex-col overflow-hidden xl:m-auto xl:flex-row">
<div class="flex flex-1 flex-col gap-4 px-4 pt-4">
<TicketHeader :ticket="ticket" />
<Card class="relative p-4">
<Button v-if="!editMode" variant="outline" class="float-right h-8 gap-2" @click="edit">
<Edit class="h-3.5 w-3.5" />
<span>Edit</span>
</Button>
<DynamicMDEditor
:modelValue="ticket.description"
@update:modelValue="updateDescription"
v-model:edit="editMode"
autofocus
placeholder="Type a description..."
@save="editDescriptionMutation.mutate"
class="min-h-14"
/>
</Card>
<Separator />
<Tabs default-value="timeline" class="flex flex-1 flex-col overflow-hidden">
<TabsList>
<TabsTrigger value="timeline">
Timeline
<Badge
v-if="
ticket.expand.timeline_via_ticket &&
ticket.expand.timeline_via_ticket.length > 0
"
variant="outline"
class="ml-2"
>
{{
ticket.expand.timeline_via_ticket ? ticket.expand.timeline_via_ticket.length : 0
}}
</Badge>
</TabsTrigger>
<TabsTrigger value="tasks">
Tasks
<Badge
v-if="ticket.expand.tasks_via_ticket && ticket.expand.tasks_via_ticket.length > 0"
variant="outline"
class="ml-2"
>
{{ ticket.expand.tasks_via_ticket ? ticket.expand.tasks_via_ticket.length : 0 }}
<StatusIcon :status="taskStatus" class="size-6" />
</Badge>
</TabsTrigger>
<TabsTrigger value="comments">
Comments
<Badge
v-if="
ticket.expand.comments_via_ticket &&
ticket.expand.comments_via_ticket.length > 0
"
variant="outline"
class="ml-2"
>
{{
ticket.expand.comments_via_ticket ? ticket.expand.comments_via_ticket.length : 0
}}
</Badge>
</TabsTrigger>
</TabsList>
<TicketTab value="timeline">
<TicketTimeline :ticket="ticket" :timeline="ticket.expand.timeline_via_ticket" />
</TicketTab>
<TicketTab value="tasks">
<TicketTasks :ticket="ticket" :tasks="ticket.expand.tasks_via_ticket" />
</TicketTab>
<TicketTab value="comments">
<TicketComments :ticket="ticket" :comments="ticket.expand.comments_via_ticket" />
</TicketTab>
</Tabs>
<Separator class="xl:hidden" />
</div>
<ScrollArea>
<div class="flex flex-initial flex-col gap-4 p-4 xl:w-96">
<TanView :isError="isError" :isPending="isPending" :error="error">
<template v-if="ticket">
<TicketActionBar :ticket="ticket" class="shrink-0" />
<ColumnBody>
<ColumnBodyContainer class="flex-col gap-4 xl:flex-row">
<div class="flex flex-1 flex-col gap-4">
<TicketHeader :ticket="ticket" />
<Card class="relative p-4">
<Button
v-if="!editMode"
variant="outline"
class="float-right h-8 gap-2"
@click="edit"
>
<Edit class="h-3.5 w-3.5" />
<span>Edit</span>
</Button>
<DynamicMDEditor
:modelValue="ticket.description"
@update:modelValue="updateDescription"
v-model:edit="editMode"
autofocus
placeholder="Type a description..."
@save="editDescriptionMutation.mutate"
class="min-h-14"
/>
</Card>
<Separator />
<Tabs default-value="timeline" class="flex flex-1 flex-col">
<TabsList>
<TabsTrigger value="timeline">
Timeline
<Badge
v-if="
ticket.expand.timeline_via_ticket &&
ticket.expand.timeline_via_ticket.length > 0
"
variant="outline"
class="ml-2 hidden sm:inline-flex"
>
{{
ticket.expand.timeline_via_ticket
? ticket.expand.timeline_via_ticket.length
: 0
}}
</Badge>
</TabsTrigger>
<TabsTrigger value="tasks">
Tasks
<Badge
v-if="
ticket.expand.tasks_via_ticket && ticket.expand.tasks_via_ticket.length > 0
"
variant="outline"
class="ml-2 hidden sm:inline-flex"
>
{{ ticket.expand.tasks_via_ticket ? ticket.expand.tasks_via_ticket.length : 0 }}
<StatusIcon :status="taskStatus" class="size-6" />
</Badge>
</TabsTrigger>
<TabsTrigger value="comments">
Comments
<Badge
v-if="
ticket.expand.comments_via_ticket &&
ticket.expand.comments_via_ticket.length > 0
"
variant="outline"
class="ml-2 hidden sm:inline-flex"
>
{{
ticket.expand.comments_via_ticket
? ticket.expand.comments_via_ticket.length
: 0
}}
</Badge>
</TabsTrigger>
</TabsList>
<TicketTab value="timeline">
<TicketTimeline :ticket="ticket" :timeline="ticket.expand.timeline_via_ticket" />
</TicketTab>
<TicketTab value="tasks">
<TicketTasks :ticket="ticket" :tasks="ticket.expand.tasks_via_ticket" />
</TicketTab>
<TicketTab value="comments">
<TicketComments :ticket="ticket" :comments="ticket.expand.comments_via_ticket" />
</TicketTab>
</Tabs>
<Separator class="xl:hidden" />
</div>
<div class="flex flex-col gap-4 xl:w-96 xl:flex-initial">
<div>
<div class="flex h-10 flex-row items-center justify-between text-muted-foreground">
<span class="text-sm font-semibold"> Details </span>
@@ -196,10 +207,10 @@ const updateDescription = (value: string) => (message.value = value)
<Separator />
<TicketFiles :ticket="ticket" :files="ticket.expand.files_via_ticket" />
</div>
</ScrollArea>
</div>
</ColumnBodyContainer>
</ColumnBody>
<Separator />
<TicketCloseBar :ticket="ticket" />
</div>
<TicketCloseBar :ticket="ticket" class="shrink-0" />
</template>
</TanView>
</template>

View File

@@ -38,13 +38,13 @@ const updateName = (value: string) => {
<DynamicInput :modelValue="ticket.name" @update:modelValue="updateName" class="-mx-1" />
</span>
<div class="flex flex-row space-x-2 px-1 text-xs">
<div class="flex items-center gap-1 text-muted-foreground">
<div class="flex flex-col items-stretch gap-1 text-xs text-muted-foreground md:h-4 md:flex-row">
<div>
Created:
{{ format(new Date(ticket.created), 'PPpp') }}
</div>
<Separator orientation="vertical" />
<div class="flex items-center gap-1 text-muted-foreground">
<Separator orientation="vertical" class="hidden md:block" />
<div>
Updated:
{{ format(new Date(ticket.updated), 'PPpp') }}
</div>

View File

@@ -1,4 +1,5 @@
<script lang="ts" setup>
import ColumnHeader from '@/components/layout/ColumnHeader.vue'
import TicketListList from '@/components/ticket/TicketListList.vue'
import TicketNewDialog from '@/components/ticket/TicketNewDialog.vue'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
@@ -14,7 +15,6 @@ import {
PaginationNext,
PaginationPrev
} from '@/components/ui/pagination'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Separator } from '@/components/ui/separator'
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
@@ -106,7 +106,7 @@ watch(
if (!route.params.id && ticketItems.value && ticketItems.value.items.length > 0) {
router.push({
name: 'tickets',
params: { type: props.selectedType.id, id: ticketItems.value.items[0].id }
params: { type: props.selectedType.id }
})
}
}
@@ -121,90 +121,81 @@ watch([tab, props.selectedType, page, perPage], () => refetch())
</script>
<template>
<div class="flex h-screen flex-col">
<div class="flex items-center bg-background px-4 py-2">
<h1 class="text-xl font-bold">
{{ selectedType?.plural }}
</h1>
<div class="ml-auto">
<TicketNewDialog :selectedType="selectedType" />
</div>
<ColumnHeader :title="selectedType?.plural">
<div class="ml-auto">
<TicketNewDialog :selectedType="selectedType" />
</div>
</ColumnHeader>
<Tabs v-model="tab" class="flex flex-1 flex-col overflow-hidden">
<div class="flex items-center justify-between px-2 pt-2">
<TabsList>
<TabsTrigger value="all">All</TabsTrigger>
<TabsTrigger value="open">Open</TabsTrigger>
<TabsTrigger value="closed">Closed</TabsTrigger>
</TabsList>
<!-- Button variant="outline" size="sm" class="h-7 gap-1 rounded-md px-3">
<ListFilter class="h-3.5 w-3.5" />
<span class="sr-only sm:not-sr-only">Filter</span>
</Button-->
</div>
<div class="p-2">
<form>
<div class="relative flex flex-row items-center">
<Input v-model="searchValue" placeholder="Search" @keydown.enter.prevent class="pl-8" />
<span class="absolute inset-y-0 start-0 flex items-center justify-center px-2">
<Search class="size-4 text-muted-foreground" />
</span>
</div>
</form>
</div>
<Separator />
<Tabs v-model="tab" class="flex flex-1 flex-col overflow-hidden">
<div class="flex items-center justify-between px-4 pt-2">
<TabsList>
<TabsTrigger value="all">All</TabsTrigger>
<TabsTrigger value="open">Open</TabsTrigger>
<TabsTrigger value="closed">Closed</TabsTrigger>
</TabsList>
<!-- Button variant="outline" size="sm" class="h-7 gap-1 rounded-md px-3">
<ListFilter class="h-3.5 w-3.5" />
<span class="sr-only sm:not-sr-only">Filter</span>
</Button-->
</div>
<div class="px-4 py-2">
<form>
<div class="relative flex flex-row items-center">
<Input v-model="searchValue" placeholder="Search" @keydown.enter.prevent class="pl-8" />
<span class="absolute inset-y-0 start-0 flex items-center justify-center px-2">
<Search class="size-4 text-muted-foreground" />
</span>
</div>
</form>
</div>
<Separator />
<div v-if="isPending" class="flex h-full w-full items-center justify-center">
<LoaderCircle class="h-16 w-16 animate-spin text-primary" />
</div>
<Alert v-else-if="isError" variant="destructive" class="mb-4 h-screen w-screen">
<AlertTitle>Error</AlertTitle>
<AlertDescription>{{ error }}</AlertDescription>
</Alert>
<ScrollArea v-else-if="ticketItems" class="flex-1">
<TicketListList :tickets="ticketItems.items" />
</ScrollArea>
<Separator />
<div class="my-2 flex items-center justify-center">
<span class="text-xs text-muted-foreground">
{{ ticketItems ? ticketItems.items.length : '?' }} of
{{ ticketItems ? ticketItems.totalItems : '?' }} tickets
</span>
</div>
<div class="mb-4 flex items-center justify-center">
<Pagination
v-slot="{ page }"
:total="ticketItems ? ticketItems.totalItems : 0"
:itemsPerPage="perPage"
:sibling-count="0"
:default-page="1"
@update:page="page = $event"
>
<PaginationList v-slot="{ items }" class="flex items-center gap-1">
<PaginationFirst />
<PaginationPrev />
<div v-if="isPending" class="flex h-full w-full items-center justify-center">
<LoaderCircle class="h-16 w-16 animate-spin text-primary" />
</div>
<Alert v-else-if="isError" variant="destructive" class="mb-2 h-screen w-screen">
<AlertTitle>Error</AlertTitle>
<AlertDescription>{{ error }}</AlertDescription>
</Alert>
<div v-else-if="ticketItems" class="flex-1 overflow-y-auto overflow-x-hidden">
<TicketListList :tickets="ticketItems.items" />
</div>
<Separator />
<div class="my-2 flex items-center justify-center">
<span class="text-xs text-muted-foreground">
{{ ticketItems ? ticketItems.items.length : '?' }} of
{{ ticketItems ? ticketItems.totalItems : '?' }} tickets
</span>
</div>
<div class="mb-2 flex items-center justify-center">
<Pagination
v-slot="{ page }"
:total="ticketItems ? ticketItems.totalItems : 0"
:itemsPerPage="perPage"
:sibling-count="0"
:default-page="1"
@update:page="page = $event"
>
<PaginationList v-slot="{ items }" class="flex items-center gap-1">
<PaginationFirst />
<PaginationPrev />
<template v-for="(item, index) in items">
<PaginationListItem
v-if="item.type === 'page'"
:key="index"
:value="item.value"
as-child
>
<Button
class="h-10 w-10 p-0"
:variant="item.value === page ? 'default' : 'outline'"
>
{{ item.value }}
</Button>
</PaginationListItem>
<PaginationEllipsis v-else :key="item.type" :index="index" />
</template>
<PaginationNext />
<PaginationLast />
</PaginationList>
</Pagination>
</div>
</Tabs>
</div>
<template v-for="(item, index) in items">
<PaginationListItem
v-if="item.type === 'page'"
:key="index"
:value="item.value"
as-child
>
<Button class="h-10 w-10 p-0" :variant="item.value === page ? 'default' : 'outline'">
{{ item.value }}
</Button>
</PaginationListItem>
<PaginationEllipsis v-else :key="item.type" :index="index" />
</template>
<PaginationNext />
<PaginationLast />
</PaginationList>
</Pagination>
</div>
</Tabs>
</template>

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup>
import ResourceListElement from '@/components/common/ResourceListElement.vue'
import ResourceListElement from '@/components/layout/ResourceListElement.vue'
import { useRoute } from 'vue-router'
@@ -13,7 +13,7 @@ defineProps<{
</script>
<template>
<div class="mt-2 flex w-full flex-1 flex-col gap-2 p-4 pt-0">
<div class="mt-2 flex w-full flex-1 flex-col gap-2 p-2 pt-0">
<ResourceListElement
v-for="item of tickets"
:key="item.id"

View File

@@ -1,5 +1,4 @@
<script setup lang="ts">
import { ScrollArea } from '@/components/ui/scroll-area'
import { Separator } from '@/components/ui/separator'
import { TabsContent } from '@/components/ui/tabs'
@@ -12,10 +11,8 @@ defineProps<{
<TabsContent :value="value" class="flex-1 overflow-hidden">
<div class="flex h-full flex-col overflow-hidden">
<Separator class="mt-2" />
<ScrollArea class="flex-1">
<slot />
<div class="h-4" />
</ScrollArea>
<slot />
<div class="h-4" />
</div>
</TabsContent>
</template>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import DeleteDialog from '@/components/common/DeleteDialog.vue'
import PanelListElement from '@/components/common/PanelListElement.vue'
import PanelListElement from '@/components/layout/PanelListElement.vue'
import TicketPanel from '@/components/ticket/TicketPanel.vue'
import LinkAddDialog from '@/components/ticket/link/LinkAddDialog.vue'
import { Button } from '@/components/ui/button'
@@ -28,7 +28,12 @@ const dialogOpen = ref(false)
>
No links added yet.
</div>
<PanelListElement v-for="link in links" :key="link.id" :title="link.url" class="pr-1">
<PanelListElement
v-for="link in links"
:key="link.id"
:title="link.url"
class="flex-row items-center pr-1"
>
<a :href="link.url" target="_blank" class="flex flex-1 items-center overflow-hidden">
<span class="mr-2 text-blue-500 underline">
{{ link.name }}

View File

@@ -1,8 +1,8 @@
<script setup lang="ts">
import DeleteDialog from '@/components/common/DeleteDialog.vue'
import PanelListElement from '@/components/common/PanelListElement.vue'
import UserSelect from '@/components/common/UserSelect.vue'
import DynamicInput from '@/components/input/DynamicInput.vue'
import PanelListElement from '@/components/layout/PanelListElement.vue'
import TaskAddDialog from '@/components/ticket/task/TaskAddDialog.vue'
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
@@ -67,12 +67,14 @@ const updateTaskName = (id: string, name: string) => updateTaskNameMutation.muta
</Card>
<Card v-else>
<PanelListElement v-for="task in tasks" :key="task.id" class="pr-1">
<Checkbox :checked="!task.open" class="mr-2" @click="check(task)" />
<DynamicInput
:modelValue="task.name"
@update:modelValue="updateTaskName(task.id, $event)"
class="mr-2 flex-1"
/>
<div class="flex flex-row items-center">
<Checkbox :checked="!task.open" class="mr-2" @click="check(task)" />
<DynamicInput
:modelValue="task.name"
@update:modelValue="updateTaskName(task.id, $event)"
class="mr-2 flex-1"
/>
</div>
<div class="ml-auto flex items-center">
<UserSelect v-if="!task.expand.owner" @update:modelValue="update(task.id, $event)">
<Button variant="outline" role="combobox" class="h-8">

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import PanelListElement from '@/components/common/PanelListElement.vue'
import DynamicMDEditor from '@/components/input/DynamicMDEditor.vue'
import PanelListElement from '@/components/layout/PanelListElement.vue'
import { Button } from '@/components/ui/button'
import {
Dialog,

View File

@@ -1,19 +0,0 @@
<script setup lang="ts">
import {
AccordionRoot,
type AccordionRootEmits,
type AccordionRootProps,
useForwardPropsEmits
} from 'radix-vue'
const props = defineProps<AccordionRootProps>()
const emits = defineEmits<AccordionRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<AccordionRoot v-bind="forwarded">
<slot />
</AccordionRoot>
</template>

View File

@@ -1,25 +0,0 @@
<script setup lang="ts">
import { AccordionContent, type AccordionContentProps } from 'radix-vue'
import { type HTMLAttributes, computed } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<AccordionContentProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<AccordionContent
v-bind="delegatedProps"
class="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
>
<div :class="cn('pb-4 pt-0', props.class)">
<slot />
</div>
</AccordionContent>
</template>

View File

@@ -1,22 +0,0 @@
<script setup lang="ts">
import { AccordionItem, type AccordionItemProps, useForwardProps } from 'radix-vue'
import { type HTMLAttributes, computed } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<AccordionItemProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<AccordionItem v-bind="forwardedProps" :class="cn('border-b', props.class)">
<slot />
</AccordionItem>
</template>

View File

@@ -1,33 +0,0 @@
<script setup lang="ts">
import { AccordionHeader, AccordionTrigger, type AccordionTriggerProps } from 'radix-vue'
import { type HTMLAttributes, computed } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<AccordionTriggerProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<AccordionHeader class="flex">
<AccordionTrigger
v-bind="delegatedProps"
:class="
cn(
'flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
props.class
)
"
>
<slot />
<!-- slot name="icon">
<ChevronDown class="h-4 w-4 shrink-0 transition-transform duration-200" />
</slot-->
</AccordionTrigger>
</AccordionHeader>
</template>

View File

@@ -1,4 +0,0 @@
export { default as Accordion } from './Accordion.vue'
export { default as AccordionContent } from './AccordionContent.vue'
export { default as AccordionItem } from './AccordionItem.vue'
export { default as AccordionTrigger } from './AccordionTrigger.vue'

View File

@@ -1,25 +0,0 @@
<script setup lang="ts">
import { type AvatarVariants, avatarVariant } from '.'
import { AvatarRoot } from 'radix-vue'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = withDefaults(
defineProps<{
class?: HTMLAttributes['class']
size?: AvatarVariants['size']
shape?: AvatarVariants['shape']
}>(),
{
size: 'sm',
shape: 'circle'
}
)
</script>
<template>
<AvatarRoot :class="cn(avatarVariant({ size, shape }), props.class)">
<slot />
</AvatarRoot>
</template>

View File

@@ -1,11 +0,0 @@
<script setup lang="ts">
import { AvatarFallback, type AvatarFallbackProps } from 'radix-vue'
const props = defineProps<AvatarFallbackProps>()
</script>
<template>
<AvatarFallback v-bind="props">
<slot />
</AvatarFallback>
</template>

View File

@@ -1,9 +0,0 @@
<script setup lang="ts">
import { AvatarImage, type AvatarImageProps } from 'radix-vue'
const props = defineProps<AvatarImageProps>()
</script>
<template>
<AvatarImage v-bind="props" class="h-full w-full object-cover" />
</template>

View File

@@ -1,24 +0,0 @@
import { type VariantProps, cva } from 'class-variance-authority'
export { default as Avatar } from './Avatar.vue'
export { default as AvatarImage } from './AvatarImage.vue'
export { default as AvatarFallback } from './AvatarFallback.vue'
export const avatarVariant = cva(
'inline-flex items-center justify-center font-normal text-foreground select-none shrink-0 bg-secondary overflow-hidden',
{
variants: {
size: {
sm: 'h-10 w-10 text-xs',
base: 'h-16 w-16 text-2xl',
lg: 'h-32 w-32 text-5xl'
},
shape: {
circle: 'rounded-full',
square: 'rounded-md'
}
}
}
)
export type AvatarVariants = VariantProps<typeof avatarVariant>

View File

@@ -1,69 +0,0 @@
<script lang="ts" setup>
import {
CalendarCell,
CalendarCellTrigger,
CalendarGrid,
CalendarGridBody,
CalendarGridHead,
CalendarGridRow,
CalendarHeadCell,
CalendarHeader,
CalendarHeading,
CalendarNextButton,
CalendarPrevButton
} from '.'
import {
CalendarRoot,
type CalendarRootEmits,
type CalendarRootProps,
useForwardPropsEmits
} from 'radix-vue'
import { type HTMLAttributes, computed } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarRootProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<CalendarRootEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<CalendarRoot v-slot="{ grid, weekDays }" :class="cn('p-3', props.class)" v-bind="forwarded">
<CalendarHeader>
<CalendarPrevButton />
<CalendarHeading />
<CalendarNextButton />
</CalendarHeader>
<div class="mt-4 flex flex-col gap-y-4 sm:flex-row sm:gap-x-4 sm:gap-y-0">
<CalendarGrid v-for="month in grid" :key="month.value.toString()">
<CalendarGridHead>
<CalendarGridRow>
<CalendarHeadCell v-for="day in weekDays" :key="day">
{{ day }}
</CalendarHeadCell>
</CalendarGridRow>
</CalendarGridHead>
<CalendarGridBody>
<CalendarGridRow
v-for="(weekDates, index) in month.rows"
:key="`weekDate-${index}`"
class="mt-2 w-full"
>
<CalendarCell v-for="weekDate in weekDates" :key="weekDate.toString()" :date="weekDate">
<CalendarCellTrigger :day="weekDate" :month="month.value" />
</CalendarCell>
</CalendarGridRow>
</CalendarGridBody>
</CalendarGrid>
</div>
</CalendarRoot>
</template>

View File

@@ -1,30 +0,0 @@
<script lang="ts" setup>
import { CalendarCell, type CalendarCellProps, useForwardProps } from 'radix-vue'
import { type HTMLAttributes, computed } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarCellProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarCell
:class="
cn(
'relative h-9 w-9 p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([data-selected])]:rounded-md [&:has([data-selected])]:bg-accent [&:has([data-selected][data-outside-month])]:bg-accent/50',
props.class
)
"
v-bind="forwardedProps"
>
<slot />
</CalendarCell>
</template>

View File

@@ -1,42 +0,0 @@
<script lang="ts" setup>
import { buttonVariants } from '@/components/ui/button'
import { CalendarCellTrigger, type CalendarCellTriggerProps, useForwardProps } from 'radix-vue'
import { type HTMLAttributes, computed } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarCellTriggerProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarCellTrigger
:class="
cn(
buttonVariants({ variant: 'ghost' }),
'h-9 w-9 p-0 font-normal',
'[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground',
// Selected
'data-[selected]:bg-primary data-[selected]:text-primary-foreground data-[selected]:opacity-100 data-[selected]:hover:bg-primary data-[selected]:hover:text-primary-foreground data-[selected]:focus:bg-primary data-[selected]:focus:text-primary-foreground',
// Disabled
'data-[disabled]:text-muted-foreground data-[disabled]:opacity-50',
// Unavailable
'data-[unavailable]:text-destructive-foreground data-[unavailable]:line-through',
// Outside months
'data-[outside-month]:pointer-events-none data-[outside-month]:text-muted-foreground data-[outside-month]:opacity-50 [&[data-outside-month][data-selected]]:bg-accent/50 [&[data-outside-month][data-selected]]:text-muted-foreground [&[data-outside-month][data-selected]]:opacity-30',
props.class
)
"
v-bind="forwardedProps"
>
<slot />
</CalendarCellTrigger>
</template>

View File

@@ -1,25 +0,0 @@
<script lang="ts" setup>
import { CalendarGrid, type CalendarGridProps, useForwardProps } from 'radix-vue'
import { type HTMLAttributes, computed } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarGridProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarGrid
:class="cn('w-full border-collapse space-y-1', props.class)"
v-bind="forwardedProps"
>
<slot />
</CalendarGrid>
</template>

View File

@@ -1,11 +0,0 @@
<script lang="ts" setup>
import { CalendarGridBody, type CalendarGridBodyProps } from 'radix-vue'
const props = defineProps<CalendarGridBodyProps>()
</script>
<template>
<CalendarGridBody v-bind="props">
<slot />
</CalendarGridBody>
</template>

View File

@@ -1,11 +0,0 @@
<script lang="ts" setup>
import { CalendarGridHead, type CalendarGridHeadProps } from 'radix-vue'
const props = defineProps<CalendarGridHeadProps>()
</script>
<template>
<CalendarGridHead v-bind="props">
<slot />
</CalendarGridHead>
</template>

View File

@@ -1,22 +0,0 @@
<script lang="ts" setup>
import { CalendarGridRow, type CalendarGridRowProps, useForwardProps } from 'radix-vue'
import { type HTMLAttributes, computed } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarGridRowProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarGridRow :class="cn('flex', props.class)" v-bind="forwardedProps">
<slot />
</CalendarGridRow>
</template>

View File

@@ -1,25 +0,0 @@
<script lang="ts" setup>
import { CalendarHeadCell, type CalendarHeadCellProps, useForwardProps } from 'radix-vue'
import { type HTMLAttributes, computed } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarHeadCellProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarHeadCell
:class="cn('w-9 rounded-md text-[0.8rem] font-normal text-muted-foreground', props.class)"
v-bind="forwardedProps"
>
<slot />
</CalendarHeadCell>
</template>

View File

@@ -1,25 +0,0 @@
<script lang="ts" setup>
import { CalendarHeader, type CalendarHeaderProps, useForwardProps } from 'radix-vue'
import { type HTMLAttributes, computed } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarHeaderProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarHeader
:class="cn('relative flex w-full items-center justify-between pt-1', props.class)"
v-bind="forwardedProps"
>
<slot />
</CalendarHeader>
</template>

View File

@@ -1,28 +0,0 @@
<script lang="ts" setup>
import { CalendarHeading, type CalendarHeadingProps, useForwardProps } from 'radix-vue'
import { type HTMLAttributes, computed } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarHeadingProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarHeading
v-slot="{ headingValue }"
:class="cn('text-sm font-medium', props.class)"
v-bind="forwardedProps"
>
<slot :heading-value>
{{ headingValue }}
</slot>
</CalendarHeading>
</template>

View File

@@ -1,37 +0,0 @@
<script lang="ts" setup>
import { buttonVariants } from '@/components/ui/button'
import { ChevronRight } from 'lucide-vue-next'
import { CalendarNext, type CalendarNextProps, useForwardProps } from 'radix-vue'
import { type HTMLAttributes, computed } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarNextProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarNext
:class="
cn(
buttonVariants({ variant: 'outline' }),
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
props.class
)
"
v-bind="forwardedProps"
>
<slot>
<ChevronRight class="h-4 w-4" />
</slot>
</CalendarNext>
</template>

View File

@@ -1,37 +0,0 @@
<script lang="ts" setup>
import { buttonVariants } from '@/components/ui/button'
import { ChevronLeft } from 'lucide-vue-next'
import { CalendarPrev, type CalendarPrevProps, useForwardProps } from 'radix-vue'
import { type HTMLAttributes, computed } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarPrevProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarPrev
:class="
cn(
buttonVariants({ variant: 'outline' }),
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
props.class
)
"
v-bind="forwardedProps"
>
<slot>
<ChevronLeft class="h-4 w-4" />
</slot>
</CalendarPrev>
</template>

View File

@@ -1,12 +0,0 @@
export { default as Calendar } from './Calendar.vue'
export { default as CalendarCell } from './CalendarCell.vue'
export { default as CalendarCellTrigger } from './CalendarCellTrigger.vue'
export { default as CalendarGrid } from './CalendarGrid.vue'
export { default as CalendarGridBody } from './CalendarGridBody.vue'
export { default as CalendarGridHead } from './CalendarGridHead.vue'
export { default as CalendarGridRow } from './CalendarGridRow.vue'
export { default as CalendarHeadCell } from './CalendarHeadCell.vue'
export { default as CalendarHeader } from './CalendarHeader.vue'
export { default as CalendarHeading } from './CalendarHeading.vue'
export { default as CalendarNextButton } from './CalendarNextButton.vue'
export { default as CalendarPrevButton } from './CalendarPrevButton.vue'

View File

@@ -1,15 +0,0 @@
<script setup lang="ts">
import { CollapsibleRoot, useForwardPropsEmits } from 'radix-vue'
import type { CollapsibleRootEmits, CollapsibleRootProps } from 'radix-vue'
const props = defineProps<CollapsibleRootProps>()
const emits = defineEmits<CollapsibleRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<CollapsibleRoot v-slot="{ open }" v-bind="forwarded">
<slot :open="open" />
</CollapsibleRoot>
</template>

View File

@@ -1,14 +0,0 @@
<script setup lang="ts">
import { CollapsibleContent, type CollapsibleContentProps } from 'radix-vue'
const props = defineProps<CollapsibleContentProps>()
</script>
<template>
<CollapsibleContent
v-bind="props"
class="overflow-hidden transition-all data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down"
>
<slot />
</CollapsibleContent>
</template>

View File

@@ -1,11 +0,0 @@
<script setup lang="ts">
import { CollapsibleTrigger, type CollapsibleTriggerProps } from 'radix-vue'
const props = defineProps<CollapsibleTriggerProps>()
</script>
<template>
<CollapsibleTrigger v-bind="props">
<slot />
</CollapsibleTrigger>
</template>

View File

@@ -1,3 +0,0 @@
export { default as Collapsible } from './Collapsible.vue'
export { default as CollapsibleTrigger } from './CollapsibleTrigger.vue'
export { default as CollapsibleContent } from './CollapsibleContent.vue'

View File

@@ -1,43 +0,0 @@
<script setup lang="ts">
import { GripVertical } from 'lucide-vue-next'
import {
SplitterResizeHandle,
type SplitterResizeHandleEmits,
type SplitterResizeHandleProps,
useForwardPropsEmits
} from 'radix-vue'
import { type HTMLAttributes, computed } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<
SplitterResizeHandleProps & { class?: HTMLAttributes['class']; withHandle?: boolean }
>()
const emits = defineEmits<SplitterResizeHandleEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<SplitterResizeHandle
v-bind="forwarded"
:class="
cn(
'relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 [&[data-orientation=vertical]>div]:rotate-90 [&[data-orientation=vertical]]:h-px [&[data-orientation=vertical]]:w-full [&[data-orientation=vertical]]:after:left-0 [&[data-orientation=vertical]]:after:h-1 [&[data-orientation=vertical]]:after:w-full [&[data-orientation=vertical]]:after:-translate-y-1/2 [&[data-orientation=vertical]]:after:translate-x-0',
props.class
)
"
>
<template v-if="props.withHandle">
<div class="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
<GripVertical class="h-2.5 w-2.5" />
</div>
</template>
</SplitterResizeHandle>
</template>

View File

@@ -1,30 +0,0 @@
<script setup lang="ts">
import {
SplitterGroup,
type SplitterGroupEmits,
type SplitterGroupProps,
useForwardPropsEmits
} from 'radix-vue'
import { type HTMLAttributes, computed } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<SplitterGroupProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<SplitterGroupEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<SplitterGroup
v-bind="forwarded"
:class="cn('flex h-full w-full data-[panel-group-direction=vertical]:flex-col', props.class)"
>
<slot />
</SplitterGroup>
</template>

View File

@@ -1,3 +0,0 @@
export { default as ResizablePanelGroup } from './ResizablePanelGroup.vue'
export { default as ResizableHandle } from './ResizableHandle.vue'
export { SplitterPanel as ResizablePanel } from 'radix-vue'

View File

@@ -1,30 +0,0 @@
<script setup lang="ts">
import ScrollBar from './ScrollBar.vue'
import {
ScrollAreaCorner,
ScrollAreaRoot,
type ScrollAreaRootProps,
ScrollAreaViewport
} from 'radix-vue'
import { type HTMLAttributes, computed } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<ScrollAreaRootProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<ScrollAreaRoot v-bind="delegatedProps" :class="cn('relative overflow-hidden', props.class)">
<ScrollAreaViewport class="h-full w-full rounded-[inherit]">
<slot />
</ScrollAreaViewport>
<ScrollBar />
<ScrollAreaCorner />
</ScrollAreaRoot>
</template>

View File

@@ -1,35 +0,0 @@
<script setup lang="ts">
import { ScrollAreaScrollbar, type ScrollAreaScrollbarProps, ScrollAreaThumb } from 'radix-vue'
import { type HTMLAttributes, computed } from 'vue'
import { cn } from '@/lib/utils'
const props = withDefaults(
defineProps<ScrollAreaScrollbarProps & { class?: HTMLAttributes['class'] }>(),
{
orientation: 'vertical'
}
)
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<ScrollAreaScrollbar
v-bind="delegatedProps"
:class="
cn(
'flex touch-none select-none transition-colors',
orientation === 'vertical' && 'h-full w-2.5 border-l border-l-transparent p-px',
orientation === 'horizontal' && 'h-2.5 flex-col border-t border-t-transparent p-px',
props.class
)
"
>
<ScrollAreaThumb class="relative flex-1 rounded-full bg-border" />
</ScrollAreaScrollbar>
</template>

View File

@@ -1,2 +0,0 @@
export { default as ScrollArea } from './ScrollArea.vue'
export { default as ScrollBar } from './ScrollBar.vue'

View File

@@ -1,44 +0,0 @@
<script setup lang="ts">
import {
SwitchRoot,
type SwitchRootEmits,
type SwitchRootProps,
SwitchThumb,
useForwardPropsEmits
} from 'radix-vue'
import { type HTMLAttributes, computed } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<SwitchRootProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<SwitchRootEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<SwitchRoot
v-bind="forwarded"
:class="
cn(
'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
props.class
)
"
>
<SwitchThumb
:class="
cn(
'pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0'
)
"
/>
</SwitchRoot>
</template>

View File

@@ -1 +0,0 @@
export { default as Switch } from './Switch.vue'

View File

@@ -3,10 +3,11 @@ import OpenTasks from '@/components/dashboard/OpenTasks.vue'
import OpenTickets from '@/components/dashboard/OpenTickets.vue'
import TicketOverTime from '@/components/dashboard/TicketOverTime.vue'
import TicketTypes from '@/components/dashboard/TicketTypes.vue'
import ColumnBody from '@/components/layout/ColumnBody.vue'
import ColumnBodyContainer from '@/components/layout/ColumnBodyContainer.vue'
import ColumnHeader from '@/components/layout/ColumnHeader.vue'
import TwoColumn from '@/components/layout/TwoColumn.vue'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Separator } from '@/components/ui/separator'
import { ExternalLink } from 'lucide-vue-next'
@@ -46,96 +47,91 @@ onMounted(() => {
<template>
<TwoColumn>
<div class="flex h-screen flex-1 flex-col">
<div class="flex h-14 min-h-14 items-center bg-background px-4 py-2">
<h1 class="text-xl font-bold">Dashboard</h1>
</div>
<Separator class="shrink-0" />
<ScrollArea>
<div
class="m-auto grid max-w-7xl grid-cols-1 grid-rows-[100px_100px_100px_100px] gap-4 p-4 md:grid-cols-2 md:grid-rows-[100px_100px] xl:grid-cols-4 xl:grid-rows-[100px]"
>
<Card>
<CardHeader>
<CardTitle>{{ count('tasks') }}</CardTitle>
<CardDescription>Tasks</CardDescription>
</CardHeader>
</Card>
<Card>
<CardHeader>
<CardTitle>{{ count('tickets') }}</CardTitle>
<CardDescription>Tickets</CardDescription>
</CardHeader>
</Card>
<Card>
<CardHeader>
<CardTitle>{{ count('users') }}</CardTitle>
<CardDescription>Users</CardDescription>
</CardHeader>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
</Card>
<Card>
<CardHeader>
<CardTitle> Catalyst</CardTitle>
</CardHeader>
<CardContent class="flex flex-1 flex-col gap-1">
<a
href="https://catalyst-soar.com/docs/category/catalyst-handbook"
target="_blank"
class="flex items-center rounded border p-2 text-blue-500 hover:bg-accent"
>
Open Catalyst Handbook
<ExternalLink class="ml-2 h-4 w-4" />
</a>
<a
href="/_/"
target="_blank"
class="flex items-center rounded border p-2 text-blue-500 hover:bg-accent"
>
Open Admin Interface
<ExternalLink class="ml-2 h-4 w-4" />
</a>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle> Tickets by Type</CardTitle>
</CardHeader>
<CardContent>
<TicketTypes />
</CardContent>
</Card>
<Card class="xl:col-span-2">
<CardHeader>
<CardTitle>Tickets Per Week</CardTitle>
</CardHeader>
<CardContent>
<TicketOverTime />
</CardContent>
</Card>
<Card class="xl:col-span-2">
<CardHeader>
<CardTitle>Your Open Tickets</CardTitle>
</CardHeader>
<CardContent>
<OpenTickets />
</CardContent>
</Card>
<Card class="xl:col-span-2">
<CardHeader>
<CardTitle>Your Open Tasks</CardTitle>
</CardHeader>
<CardContent>
<OpenTasks />
</CardContent>
</Card>
</div>
</ScrollArea>
</div>
<ColumnHeader title="Dashboard" />
<ColumnBody>
<ColumnBodyContainer
class="grid grid-cols-1 grid-rows-[100px_100px_100px_100px] md:grid-cols-2 md:grid-rows-[100px_100px] xl:grid-cols-4 xl:grid-rows-[100px]"
>
<Card>
<CardHeader>
<CardTitle>{{ count('tasks') }}</CardTitle>
<CardDescription>Tasks</CardDescription>
</CardHeader>
</Card>
<Card>
<CardHeader>
<CardTitle>{{ count('tickets') }}</CardTitle>
<CardDescription>Tickets</CardDescription>
</CardHeader>
</Card>
<Card>
<CardHeader>
<CardTitle>{{ count('users') }}</CardTitle>
<CardDescription>Users</CardDescription>
</CardHeader>
</Card>
<Card>
<CardHeader>
<CardTitle>{{ count('reactions') }}</CardTitle>
<CardDescription>Reactions</CardDescription>
</CardHeader>
</Card>
<Card>
<CardHeader>
<CardTitle> Catalyst</CardTitle>
</CardHeader>
<CardContent class="flex flex-1 flex-col gap-1">
<a
href="https://catalyst.security-brewery.com/docs/category/catalyst-handbook"
target="_blank"
class="flex items-center rounded border p-2 text-blue-500 hover:bg-accent"
>
Open Catalyst Handbook
<ExternalLink class="ml-2 h-4 w-4" />
</a>
<a
href="/_/"
target="_blank"
class="flex items-center rounded border p-2 text-blue-500 hover:bg-accent"
>
Open Admin Interface
<ExternalLink class="ml-2 h-4 w-4" />
</a>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle> Tickets by Type</CardTitle>
</CardHeader>
<CardContent>
<TicketTypes />
</CardContent>
</Card>
<Card class="xl:col-span-2">
<CardHeader>
<CardTitle>Tickets Per Week</CardTitle>
</CardHeader>
<CardContent>
<TicketOverTime />
</CardContent>
</Card>
<Card class="xl:col-span-2">
<CardHeader>
<CardTitle>Your Open Tickets</CardTitle>
</CardHeader>
<CardContent>
<OpenTickets />
</CardContent>
</Card>
<Card class="xl:col-span-2">
<CardHeader>
<CardTitle>Your Open Tasks</CardTitle>
</CardHeader>
<CardContent>
<OpenTasks />
</CardContent>
</Card>
</ColumnBodyContainer>
</ColumnBody>
</TwoColumn>
</template>

View File

@@ -1,4 +1,5 @@
<script setup lang="ts" xmlns="http://www.w3.org/1999/html">
<script setup lang="ts">
import ColumnBody from '@/components/layout/ColumnBody.vue'
import ThreeColumn from '@/components/layout/ThreeColumn.vue'
import ReactionDisplay from '@/components/reaction/ReactionDisplay.vue'
import ReactionList from '@/components/reaction/ReactionList.vue'
@@ -22,14 +23,14 @@ onMounted(() => {
</script>
<template>
<ThreeColumn>
<ThreeColumn :show-details="!!id">
<template #list>
<ReactionList />
</template>
<template #single>
<div v-if="!id" class="flex h-full w-full items-center justify-center text-lg text-gray-500">
<ColumnBody v-if="!id" class="items-center justify-center text-lg text-gray-500">
No reaction selected
</div>
</ColumnBody>
<ReactionNew v-else-if="id === 'new'" key="new" />
<ReactionDisplay v-else :key="id" :id="id" />
</template>

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import TanView from '@/components/TanView.vue'
import ColumnBody from '@/components/layout/ColumnBody.vue'
import ThreeColumn from '@/components/layout/ThreeColumn.vue'
import TicketDisplay from '@/components/ticket/TicketDisplay.vue'
import TicketList from '@/components/ticket/TicketList.vue'
@@ -41,20 +42,17 @@ onMounted(() => {
</script>
<template>
<ThreeColumn>
<ThreeColumn :show-details="!!id">
<template #list>
<TanView :isError="isError" :isPending="isPending" :error="error" :value="selectedType">
<TanView :isError="isError" :isPending="isPending" :error="error">
<TicketList v-if="selectedType" :key="selectedType.id" :selectedType="selectedType" />
</TanView>
</template>
<template #single>
<TanView :isError="isError" :isPending="isPending" :error="error" :value="selectedType">
<div
v-if="!id"
class="flex h-full w-full items-center justify-center text-lg text-gray-500"
>
<TanView :isError="isError" :isPending="isPending" :error="error">
<ColumnBody v-if="!id" class="items-center justify-center text-lg text-gray-500">
No ticket selected
</div>
</ColumnBody>
<TicketDisplay v-else-if="selectedType" :key="id" :selectedType="selectedType" />
</TanView>
</template>