mirror of
https://github.com/SecurityBrewery/catalyst.git
synced 2025-12-07 07:42:45 +01:00
feat: mobile ui (#1096)
This commit is contained in:
43
migrations/8_dashboardview.go
Normal file
43
migrations/8_dashboardview.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
)
|
||||
|
||||
const dashboardCountsViewUpdateQuery = `SELECT id, count FROM (
|
||||
SELECT 'users' as id, COUNT(users.id) as count FROM users
|
||||
UNION
|
||||
SELECT 'tickets' as id, COUNT(tickets.id) as count FROM tickets
|
||||
UNION
|
||||
SELECT 'tasks' as id, COUNT(tasks.id) as count FROM tasks
|
||||
UNION
|
||||
SELECT 'reactions' as id, COUNT(reactions.id) as count FROM reactions
|
||||
) as counts;`
|
||||
|
||||
func dashboardCountsViewUpdateUp(db dbx.Builder) error {
|
||||
dao := daos.New(db)
|
||||
|
||||
collection, err := dao.FindCollectionByNameOrId(dashboardCountsViewName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collection.Options = types.JsonMap{"query": dashboardCountsViewUpdateQuery}
|
||||
|
||||
return dao.SaveCollection(collection)
|
||||
}
|
||||
|
||||
func dashboardCountsViewUpdateDown(db dbx.Builder) error {
|
||||
dao := daos.New(db)
|
||||
|
||||
collection, err := dao.FindCollectionByNameOrId(dashboardCountsViewName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collection.Options = types.JsonMap{"query": dashboardCountsViewQuery}
|
||||
|
||||
return dao.SaveCollection(collection)
|
||||
}
|
||||
@@ -12,4 +12,5 @@ func Register() {
|
||||
migrations.Register(reactionsUp, reactionsDown, "1700000005_reactions.go")
|
||||
migrations.Register(systemuserUp, systemuserDown, "1700000006_systemuser.go")
|
||||
migrations.Register(searchViewUp, searchViewDown, "1700000007_search_view.go")
|
||||
migrations.Register(dashboardCountsViewUpdateUp, dashboardCountsViewUpdateDown, "1700000008_dashboardview.go")
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
5
ui/src/components/layout/ColumnBody.vue
Normal file
5
ui/src/components/layout/ColumnBody.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div class="flex flex-1 items-start justify-start overflow-y-auto overflow-x-hidden">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
13
ui/src/components/layout/ColumnBodyContainer.vue
Normal file
13
ui/src/components/layout/ColumnBodyContainer.vue
Normal 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>
|
||||
25
ui/src/components/layout/ColumnHeader.vue
Normal file
25
ui/src/components/layout/ColumnHeader.vue
Normal 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>
|
||||
@@ -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
|
||||
)
|
||||
"
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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'
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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'
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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'
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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'
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -1,2 +0,0 @@
|
||||
export { default as ScrollArea } from './ScrollArea.vue'
|
||||
export { default as ScrollBar } from './ScrollBar.vue'
|
||||
@@ -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>
|
||||
@@ -1 +0,0 @@
|
||||
export { default as Switch } from './Switch.vue'
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user