mirror of
https://github.com/SecurityBrewery/catalyst.git
synced 2025-12-08 00:02:49 +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(reactionsUp, reactionsDown, "1700000005_reactions.go")
|
||||||
migrations.Register(systemuserUp, systemuserDown, "1700000006_systemuser.go")
|
migrations.Register(systemuserUp, systemuserDown, "1700000006_systemuser.go")
|
||||||
migrations.Register(searchViewUp, searchViewDown, "1700000007_search_view.go")
|
migrations.Register(searchViewUp, searchViewDown, "1700000007_search_view.go")
|
||||||
|
migrations.Register(dashboardCountsViewUpdateUp, dashboardCountsViewUpdateDown, "1700000008_dashboardview.go")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<title>Catalyst</title>
|
<title>Catalyst</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -7,17 +7,16 @@ defineProps<{
|
|||||||
isPending: boolean
|
isPending: boolean
|
||||||
isError: boolean
|
isError: boolean
|
||||||
error: Error | null
|
error: Error | null
|
||||||
value: any
|
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="isPending" class="flex justify-center">
|
<div v-if="isPending" class="flex h-full w-full">
|
||||||
<LoaderCircle class="h-16 w-16 animate-spin text-primary" />
|
<LoaderCircle class="m-auto h-16 w-16 animate-spin text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<Alert v-else-if="isError" variant="destructive" class="mb-4">
|
<Alert v-else-if="isError" variant="destructive" class="mb-4">
|
||||||
<AlertTitle>Error</AlertTitle>
|
<AlertTitle>Error</AlertTitle>
|
||||||
<AlertDescription>{{ error }}</AlertDescription>
|
<AlertDescription>{{ error }}</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
<slot v-else-if="value" />
|
<slot v-else />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<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 { buttonVariants } from '@/components/ui/button'
|
||||||
import { Card } from '@/components/ui/card'
|
import { Card } from '@/components/ui/card'
|
||||||
|
|
||||||
@@ -32,24 +33,31 @@ const {
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<Card>
|
<Card>
|
||||||
<div v-if="tasks && tasks.length === 0" class="p-2 text-center text-sm text-gray-500">
|
<TanView :isError="isError" :isPending="isPending" :error="error">
|
||||||
No open tasks
|
<div v-if="tasks && tasks.length === 0" class="p-2 text-center text-sm text-gray-500">
|
||||||
</div>
|
No open tasks
|
||||||
<PanelListElement v-else v-for="task in tasks" :key="task.id" class="pr-1">
|
</div>
|
||||||
<span>{{ task.name }}</span>
|
<PanelListElement v-else v-for="task in tasks" :key="task.id" class="pr-1">
|
||||||
<RouterLink
|
<span>{{ task.name }}</span>
|
||||||
:to="{
|
<RouterLink
|
||||||
name: 'tickets',
|
:to="{
|
||||||
params: { type: task.expand.ticket.type, id: task.expand.ticket.id }
|
name: 'tickets',
|
||||||
}"
|
params: { type: task.expand.ticket.type, id: task.expand.ticket.id }
|
||||||
:class="cn(buttonVariants({ variant: 'outline', size: 'sm' }), 'ml-auto h-8')"
|
}"
|
||||||
>
|
:class="
|
||||||
<span class="flex flex-row items-center text-sm text-gray-500">
|
cn(
|
||||||
Go to {{ task.expand.ticket.name }}
|
buttonVariants({ variant: 'outline', size: 'sm' }),
|
||||||
<ChevronRight class="ml-2 h-4 w-4" />
|
'h-8 w-full sm:ml-auto sm:w-auto'
|
||||||
</span>
|
)
|
||||||
</RouterLink>
|
"
|
||||||
</PanelListElement>
|
>
|
||||||
|
<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>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<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 { buttonVariants } from '@/components/ui/button'
|
||||||
import { Card } from '@/components/ui/card'
|
import { Card } from '@/components/ui/card'
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
@@ -42,16 +42,21 @@ const age = (ticket: Ticket) =>
|
|||||||
</div>
|
</div>
|
||||||
<PanelListElement v-else v-for="ticket in tickets" :key="ticket.id" class="gap-2 pr-1">
|
<PanelListElement v-else v-for="ticket in tickets" :key="ticket.id" class="gap-2 pr-1">
|
||||||
<span>{{ ticket.name }}</span>
|
<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>
|
<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>
|
<span class="text-sm text-muted-foreground">Open since {{ age(ticket) }} days</span>
|
||||||
<RouterLink
|
<RouterLink
|
||||||
:to="{
|
:to="{
|
||||||
name: 'tickets',
|
name: 'tickets',
|
||||||
params: { type: ticket.type, id: ticket.id }
|
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">
|
<span class="flex flex-row items-center text-sm text-gray-500">
|
||||||
Go to {{ ticket.name }}
|
Go to {{ ticket.name }}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const ticketsPerWeek = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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']" />
|
<LineChart class="h-40" :data="ticketsPerWeek" index="week" :categories="['count']" />
|
||||||
</TanView>
|
</TanView>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const namedTypes = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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">
|
<div v-if="namedTypes" class="flex flex-1 items-center">
|
||||||
<DonutChart index="plural" type="donut" category="count" :data="namedTypes" />
|
<DonutChart index="plural" type="donut" category="count" :data="namedTypes" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,9 +3,6 @@ import ShortCut from '@/components/ShortCut.vue'
|
|||||||
|
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
// import { Textarea } from '@/components/ui/textarea'
|
|
||||||
// import { Input } from '@/components/ui/input'
|
|
||||||
|
|
||||||
const model = defineModel({
|
const model = defineModel({
|
||||||
type: String
|
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
|
<div
|
||||||
:class="
|
:class="
|
||||||
cn(
|
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
|
props.class
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
@@ -8,57 +8,78 @@ import { Separator } from '@/components/ui/separator'
|
|||||||
|
|
||||||
import { Menu } from 'lucide-vue-next'
|
import { Menu } from 'lucide-vue-next'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
import { useCatalystStore } from '@/store/catalyst'
|
import { useCatalystStore } from '@/store/catalyst'
|
||||||
|
|
||||||
const catalystStore = useCatalystStore()
|
const catalystStore = useCatalystStore()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex h-[57px] items-center border-b bg-background">
|
<div
|
||||||
<CatalystLogo
|
:class="
|
||||||
class="size-8"
|
cn(
|
||||||
:class="{ 'flex-1': catalystStore.sidebarCollapsed, 'mx-3': !catalystStore.sidebarCollapsed }"
|
'flex min-w-48 shrink-0 flex-col border-r bg-popover', // transition-all duration-300 ease-in-out',
|
||||||
/>
|
catalystStore.sidebarCollapsed && 'min-w-[50px]'
|
||||||
<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"
|
|
||||||
>
|
>
|
||||||
<Menu class="size-4" />
|
<div class="flex h-[57px] items-center border-b bg-background">
|
||||||
<span v-if="!catalystStore.sidebarCollapsed" class="ml-2">Toggle Sidebar</span>
|
<CatalystLogo
|
||||||
</Button>
|
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>
|
</template>
|
||||||
|
|||||||
@@ -3,29 +3,37 @@ import SideBar from '@/components/layout/SideBar.vue'
|
|||||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useCatalystStore } from '@/store/catalyst'
|
|
||||||
|
|
||||||
const catalystStore = useCatalystStore()
|
defineProps<{
|
||||||
|
showDetails?: boolean
|
||||||
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TooltipProvider :delay-duration="0">
|
<TooltipProvider :delay-duration="0">
|
||||||
<div class="flex h-full flex-row items-stretch bg-muted/40">
|
<div class="flex h-full flex-row items-stretch bg-muted/40">
|
||||||
|
<SideBar />
|
||||||
<div
|
<div
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'flex min-w-48 flex-col border-r bg-popover', // transition-all duration-300 ease-in-out',
|
'w-full flex-initial border-r sm:w-72',
|
||||||
catalystStore.sidebarCollapsed && 'min-w-[50px]'
|
!showDetails && 'flex',
|
||||||
|
showDetails && 'hidden sm:flex'
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<SideBar />
|
<div class="flex h-full w-full flex-col">
|
||||||
|
<slot name="list" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-72 flex-initial border-r">
|
<div
|
||||||
<slot name="list" />
|
:class="
|
||||||
</div>
|
cn('flex-1 overflow-hidden', !showDetails && 'hidden sm:flex', showDetails && 'flex')
|
||||||
<div class="flex-1">
|
"
|
||||||
<slot name="single" />
|
>
|
||||||
|
<div class="flex h-full w-full flex-1 flex-col">
|
||||||
|
<slot name="single" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|||||||
@@ -1,27 +1,15 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import SideBar from '@/components/layout/SideBar.vue'
|
import SideBar from '@/components/layout/SideBar.vue'
|
||||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
import { useCatalystStore } from '@/store/catalyst'
|
|
||||||
|
|
||||||
const catalystStore = useCatalystStore()
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TooltipProvider :delay-duration="0">
|
<TooltipProvider :delay-duration="0">
|
||||||
<div class="flex h-full flex-row items-stretch bg-muted/40">
|
<div class="flex h-full flex-row items-stretch bg-muted/40">
|
||||||
<div
|
<SideBar />
|
||||||
:class="
|
<div class="flex h-full w-full flex-col">
|
||||||
cn(
|
<slot />
|
||||||
'flex min-w-48 flex-col border-r bg-popover', // transition-all duration-300 ease-in-out',
|
|
||||||
catalystStore.sidebarCollapsed && 'min-w-[50px]'
|
|
||||||
)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<SideBar />
|
|
||||||
</div>
|
</div>
|
||||||
<slot />
|
|
||||||
</div>
|
</div>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import TanView from '@/components/TanView.vue'
|
import TanView from '@/components/TanView.vue'
|
||||||
import DeleteDialog from '@/components/common/DeleteDialog.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 ReactionForm from '@/components/reaction/ReactionForm.vue'
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Separator } from '@/components/ui/separator'
|
|
||||||
import { toast } from '@/components/ui/toast'
|
import { toast } from '@/components/ui/toast'
|
||||||
|
|
||||||
|
import { ChevronLeft } from 'lucide-vue-next'
|
||||||
|
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||||
import { onMounted, onUnmounted } from 'vue'
|
import { onMounted, onUnmounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
@@ -72,28 +76,29 @@ onUnmounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TanView :isError="isError" :isPending="isPending" :error="error" :value="reaction">
|
<TanView :isError="isError" :isPending="isPending" :error="error">
|
||||||
<div class="flex h-full flex-1 flex-col overflow-hidden">
|
<ColumnHeader>
|
||||||
<div class="flex items-center bg-background px-4 py-2">
|
<Button @click="router.push({ name: 'reactions' })" variant="outline" class="sm:hidden">
|
||||||
<div class="ml-auto">
|
<ChevronLeft class="mr-2 size-4" />
|
||||||
<DeleteDialog
|
Back
|
||||||
v-if="reaction"
|
</Button>
|
||||||
collection="reactions"
|
<div class="ml-auto">
|
||||||
:id="reaction.id"
|
<DeleteDialog
|
||||||
:name="reaction.name"
|
v-if="reaction"
|
||||||
:singular="'Reaction'"
|
collection="reactions"
|
||||||
:to="{ name: 'reactions' }"
|
:id="reaction.id"
|
||||||
:queryKey="['reactions']"
|
:name="reaction.name"
|
||||||
/>
|
:singular="'Reaction'"
|
||||||
</div>
|
:to="{ name: 'reactions' }"
|
||||||
|
:queryKey="['reactions']"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
</ColumnHeader>
|
||||||
|
|
||||||
<ScrollArea v-if="reaction" class="flex-1">
|
<ColumnBody v-if="reaction">
|
||||||
<div class="flex max-w-[640px] flex-col gap-4 p-4">
|
<ColumnBodyContainer small>
|
||||||
<ReactionForm :reaction="reaction" @submit="updateReactionMutation.mutate" />
|
<ReactionForm :reaction="reaction" @submit="updateReactionMutation.mutate" />
|
||||||
</div>
|
</ColumnBodyContainer>
|
||||||
</ScrollArea>
|
</ColumnBody>
|
||||||
</div>
|
|
||||||
</TanView>
|
</TanView>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ const curlExample = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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>
|
<FormField name="name" v-slot="{ componentField }" validate-on-input>
|
||||||
<FormItem class="w-full">
|
<FormItem class="w-full">
|
||||||
<FormLabel for="name" class="text-right">Name</FormLabel>
|
<FormLabel for="name" class="text-right">Name</FormLabel>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import TanView from '@/components/TanView.vue'
|
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 { Button } from '@/components/ui/button'
|
||||||
import { Separator } from '@/components/ui/separator'
|
|
||||||
|
|
||||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||||
import { onMounted } from 'vue'
|
import { onMounted } from 'vue'
|
||||||
@@ -63,32 +63,28 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TanView :isError="isError" :isPending="isPending" :error="error" :value="reactions">
|
<TanView :isError="isError" :isPending="isPending" :error="error">
|
||||||
<div class="flex h-screen flex-col">
|
<ColumnHeader title="Reactions">
|
||||||
<div class="flex items-center bg-background px-4 py-2">
|
<div class="ml-auto">
|
||||||
<h1 class="text-xl font-bold">Reactions</h1>
|
<Button variant="ghost" @click="openNew">New Reaction</Button>
|
||||||
<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>
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</TanView>
|
</TanView>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<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 ReactionForm from '@/components/reaction/ReactionForm.vue'
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Separator } from '@/components/ui/separator'
|
|
||||||
|
import { ChevronLeft } from 'lucide-vue-next'
|
||||||
|
|
||||||
import { useMutation, useQueryClient } from '@tanstack/vue-query'
|
import { useMutation, useQueryClient } from '@tanstack/vue-query'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
@@ -24,14 +28,16 @@ const addReactionMutation = useMutation({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex h-full flex-1 flex-col overflow-hidden">
|
<ColumnHeader>
|
||||||
<div class="flex min-h-14 items-center bg-background px-4 py-2"></div>
|
<Button @click="router.push({ name: 'reactions' })" variant="outline" class="sm:hidden">
|
||||||
<Separator />
|
<ChevronLeft class="mr-2 size-4" />
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
</ColumnHeader>
|
||||||
|
|
||||||
<ScrollArea class="flex-1">
|
<ColumnBody>
|
||||||
<div class="flex max-w-[640px] flex-col gap-4 p-4">
|
<ColumnBodyContainer small>
|
||||||
<ReactionForm @submit="addReactionMutation.mutate" />
|
<ReactionForm @submit="addReactionMutation.mutate" />
|
||||||
</div>
|
</ColumnBodyContainer>
|
||||||
</ScrollArea>
|
</ColumnBody>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Icon from '@/components/Icon.vue'
|
import Icon from '@/components/Icon.vue'
|
||||||
import DeleteDialog from '@/components/common/DeleteDialog.vue'
|
import DeleteDialog from '@/components/common/DeleteDialog.vue'
|
||||||
|
import ColumnHeader from '@/components/layout/ColumnHeader.vue'
|
||||||
import TicketCloseDialog from '@/components/ticket/TicketCloseDialog.vue'
|
import TicketCloseDialog from '@/components/ticket/TicketCloseDialog.vue'
|
||||||
import TicketUserSelect from '@/components/ticket/TicketUserSelect.vue'
|
import TicketUserSelect from '@/components/ticket/TicketUserSelect.vue'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
@@ -12,7 +13,7 @@ import {
|
|||||||
} from '@/components/ui/dropdown-menu'
|
} from '@/components/ui/dropdown-menu'
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
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 { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
@@ -49,7 +50,7 @@ const changeTypeMutation = useMutation({
|
|||||||
}),
|
}),
|
||||||
onSuccess: (data: Ticket) => {
|
onSuccess: (data: Ticket) => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['tickets'] })
|
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
|
onError: handleError
|
||||||
})
|
})
|
||||||
@@ -74,74 +75,81 @@ const closeTicketDialogOpen = ref(false)
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex items-center justify-between bg-background p-2">
|
<ColumnHeader>
|
||||||
<div class="flex items-center gap-2">
|
<Button
|
||||||
<Tooltip>
|
@click="router.push({ name: 'tickets', params: { type: ticket.type } })"
|
||||||
<TooltipTrigger as-child>
|
variant="outline"
|
||||||
<div>
|
class="sm:hidden"
|
||||||
<DropdownMenu>
|
>
|
||||||
<DropdownMenuTrigger as-child>
|
<ChevronLeft class="mr-2 size-4" />
|
||||||
<Button variant="outline" :disabled="!ticket">
|
Back
|
||||||
<Icon :name="ticket.expand.type.icon" class="mr-2 size-4" />
|
</Button>
|
||||||
{{ ticket.expand.type.singular }}
|
<Tooltip>
|
||||||
</Button>
|
<TooltipTrigger as-child>
|
||||||
</DropdownMenuTrigger>
|
<div>
|
||||||
<DropdownMenuContent>
|
<DropdownMenu>
|
||||||
<DropdownMenuItem
|
<DropdownMenuTrigger as-child>
|
||||||
v-for="type in otherTypes"
|
<Button variant="outline" :disabled="!ticket">
|
||||||
:key="type.id"
|
<Icon :name="ticket.expand.type.icon" class="mr-2 size-4" />
|
||||||
class="cursor-pointer"
|
{{ ticket.expand.type.singular }}
|
||||||
@click="changeTypeMutation.mutate(type.id)"
|
</Button>
|
||||||
>
|
</DropdownMenuTrigger>
|
||||||
<Icon :name="type.icon" class="mr-2 size-4" />
|
<DropdownMenuContent>
|
||||||
Convert to {{ type.singular }}
|
<DropdownMenuItem
|
||||||
</DropdownMenuItem>
|
v-for="type in otherTypes"
|
||||||
</DropdownMenuContent>
|
:key="type.id"
|
||||||
</DropdownMenu>
|
class="cursor-pointer"
|
||||||
</div>
|
@click="changeTypeMutation.mutate(type.id)"
|
||||||
</TooltipTrigger>
|
>
|
||||||
<TooltipContent>Change Type</TooltipContent>
|
<Icon :name="type.icon" class="mr-2 size-4" />
|
||||||
</Tooltip>
|
Convert to {{ type.singular }}
|
||||||
<TicketCloseDialog v-model="closeTicketDialogOpen" :ticket="ticket" />
|
</DropdownMenuItem>
|
||||||
<Tooltip>
|
</DropdownMenuContent>
|
||||||
<TooltipTrigger as-child>
|
</DropdownMenu>
|
||||||
<div>
|
</div>
|
||||||
<DropdownMenu>
|
</TooltipTrigger>
|
||||||
<DropdownMenuTrigger as-child>
|
<TooltipContent>Change Type</TooltipContent>
|
||||||
<Button variant="outline" :disabled="!ticket">
|
</Tooltip>
|
||||||
<CircleDot v-if="ticket.open" class="mr-2 h-4 w-4" />
|
<TicketCloseDialog v-model="closeTicketDialogOpen" :ticket="ticket" />
|
||||||
<Check v-else class="mr-2 h-4 w-4" />
|
<Tooltip>
|
||||||
{{ ticket?.open ? 'Open' : 'Closed' }}
|
<TooltipTrigger as-child>
|
||||||
</Button>
|
<div>
|
||||||
</DropdownMenuTrigger>
|
<DropdownMenu>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuTrigger as-child>
|
||||||
<DropdownMenuItem
|
<Button variant="outline" :disabled="!ticket">
|
||||||
v-if="ticket.open"
|
<CircleDot v-if="ticket.open" class="mr-2 h-4 w-4" />
|
||||||
class="cursor-pointer"
|
<Check v-else class="mr-2 h-4 w-4" />
|
||||||
@click="closeTicketDialogOpen = true"
|
{{ ticket?.open ? 'Open' : 'Closed' }}
|
||||||
>
|
</Button>
|
||||||
<Check class="mr-2 size-4" />
|
</DropdownMenuTrigger>
|
||||||
Close Ticket
|
<DropdownMenuContent>
|
||||||
</DropdownMenuItem>
|
<DropdownMenuItem
|
||||||
<DropdownMenuItem v-else class="cursor-pointer" @click="closeTicketMutation.mutate">
|
v-if="ticket.open"
|
||||||
<Repeat class="mr-2 size-4" />
|
class="cursor-pointer"
|
||||||
Reopen Ticket
|
@click="closeTicketDialogOpen = true"
|
||||||
</DropdownMenuItem>
|
>
|
||||||
</DropdownMenuContent>
|
<Check class="mr-2 size-4" />
|
||||||
</DropdownMenu>
|
Close Ticket
|
||||||
</div>
|
</DropdownMenuItem>
|
||||||
</TooltipTrigger>
|
<DropdownMenuItem v-else class="cursor-pointer" @click="closeTicketMutation.mutate">
|
||||||
<TooltipContent>Change Status</TooltipContent>
|
<Repeat class="mr-2 size-4" />
|
||||||
</Tooltip>
|
Reopen Ticket
|
||||||
<Tooltip>
|
</DropdownMenuItem>
|
||||||
<TooltipTrigger as-child>
|
</DropdownMenuContent>
|
||||||
<div>
|
</DropdownMenu>
|
||||||
<TicketUserSelect :key="ticket.owner" :uID="ticket.owner" :ticket="ticket" />
|
</div>
|
||||||
</div>
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipContent>Change Status</TooltipContent>
|
||||||
<TooltipContent>Change User</TooltipContent>
|
</Tooltip>
|
||||||
</Tooltip>
|
<Tooltip>
|
||||||
</div>
|
<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
|
<DeleteDialog
|
||||||
v-if="ticket"
|
v-if="ticket"
|
||||||
:collection="'tickets'"
|
:collection="'tickets'"
|
||||||
@@ -151,5 +159,5 @@ const closeTicketDialogOpen = ref(false)
|
|||||||
:to="{ name: 'tickets' }"
|
:to="{ name: 'tickets' }"
|
||||||
:queryKey="['tickets']"
|
:queryKey="['tickets']"
|
||||||
/>
|
/>
|
||||||
</div>
|
</ColumnHeader>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import ColumnHeader from '@/components/layout/ColumnHeader.vue'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
|
|
||||||
@@ -38,7 +39,7 @@ const closeButtonDisabled = false // computed(() => !props.ticket.open || messag
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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" />
|
<Input v-if="ticket.open" v-model="resolution" placeholder="Closing reason" />
|
||||||
<div v-else class="flex-1">
|
<div v-else class="flex-1">
|
||||||
<p class="ml-2 text-gray-500">Closed: {{ ticket.resolution }}</p>
|
<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
|
: 'Reopen ' + props.ticket.expand.type.singular
|
||||||
}}
|
}}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</ColumnHeader>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
import TanView from '@/components/TanView.vue'
|
import TanView from '@/components/TanView.vue'
|
||||||
import JSONSchemaFormFields from '@/components/form/JSONSchemaFormFields.vue'
|
import JSONSchemaFormFields from '@/components/form/JSONSchemaFormFields.vue'
|
||||||
import DynamicMDEditor from '@/components/input/DynamicMDEditor.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 StatusIcon from '@/components/ticket/StatusIcon.vue'
|
||||||
import TicketActionBar from '@/components/ticket/TicketActionBar.vue'
|
import TicketActionBar from '@/components/ticket/TicketActionBar.vue'
|
||||||
import TicketCloseBar from '@/components/ticket/TicketCloseBar.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 { Badge } from '@/components/ui/badge'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Card } from '@/components/ui/card'
|
import { Card } from '@/components/ui/card'
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||||
|
|
||||||
import { Edit } from 'lucide-vue-next'
|
import { Edit } from 'lucide-vue-next'
|
||||||
|
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
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 { useRoute } from 'vue-router'
|
||||||
|
|
||||||
import { pb } from '@/lib/pocketbase'
|
import { pb } from '@/lib/pocketbase'
|
||||||
@@ -100,87 +101,97 @@ const updateDescription = (value: string) => (message.value = value)
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TanView :isError="isError" :isPending="isPending" :error="error" :value="ticket">
|
<TanView :isError="isError" :isPending="isPending" :error="error">
|
||||||
<div v-if="ticket" class="flex h-full flex-col">
|
<template v-if="ticket">
|
||||||
<TicketActionBar :ticket="ticket" />
|
<TicketActionBar :ticket="ticket" class="shrink-0" />
|
||||||
<Separator />
|
<ColumnBody>
|
||||||
<div class="flex w-full max-w-7xl flex-1 flex-col overflow-hidden xl:m-auto xl:flex-row">
|
<ColumnBodyContainer class="flex-col gap-4 xl:flex-row">
|
||||||
<div class="flex flex-1 flex-col gap-4 px-4 pt-4">
|
<div class="flex flex-1 flex-col gap-4">
|
||||||
<TicketHeader :ticket="ticket" />
|
<TicketHeader :ticket="ticket" />
|
||||||
<Card class="relative p-4">
|
<Card class="relative p-4">
|
||||||
<Button v-if="!editMode" variant="outline" class="float-right h-8 gap-2" @click="edit">
|
<Button
|
||||||
<Edit class="h-3.5 w-3.5" />
|
v-if="!editMode"
|
||||||
<span>Edit</span>
|
variant="outline"
|
||||||
</Button>
|
class="float-right h-8 gap-2"
|
||||||
<DynamicMDEditor
|
@click="edit"
|
||||||
:modelValue="ticket.description"
|
>
|
||||||
@update:modelValue="updateDescription"
|
<Edit class="h-3.5 w-3.5" />
|
||||||
v-model:edit="editMode"
|
<span>Edit</span>
|
||||||
autofocus
|
</Button>
|
||||||
placeholder="Type a description..."
|
<DynamicMDEditor
|
||||||
@save="editDescriptionMutation.mutate"
|
:modelValue="ticket.description"
|
||||||
class="min-h-14"
|
@update:modelValue="updateDescription"
|
||||||
/>
|
v-model:edit="editMode"
|
||||||
</Card>
|
autofocus
|
||||||
<Separator />
|
placeholder="Type a description..."
|
||||||
<Tabs default-value="timeline" class="flex flex-1 flex-col overflow-hidden">
|
@save="editDescriptionMutation.mutate"
|
||||||
<TabsList>
|
class="min-h-14"
|
||||||
<TabsTrigger value="timeline">
|
/>
|
||||||
Timeline
|
</Card>
|
||||||
<Badge
|
<Separator />
|
||||||
v-if="
|
<Tabs default-value="timeline" class="flex flex-1 flex-col">
|
||||||
ticket.expand.timeline_via_ticket &&
|
<TabsList>
|
||||||
ticket.expand.timeline_via_ticket.length > 0
|
<TabsTrigger value="timeline">
|
||||||
"
|
Timeline
|
||||||
variant="outline"
|
<Badge
|
||||||
class="ml-2"
|
v-if="
|
||||||
>
|
ticket.expand.timeline_via_ticket &&
|
||||||
{{
|
ticket.expand.timeline_via_ticket.length > 0
|
||||||
ticket.expand.timeline_via_ticket ? ticket.expand.timeline_via_ticket.length : 0
|
"
|
||||||
}}
|
variant="outline"
|
||||||
</Badge>
|
class="ml-2 hidden sm:inline-flex"
|
||||||
</TabsTrigger>
|
>
|
||||||
<TabsTrigger value="tasks">
|
{{
|
||||||
Tasks
|
ticket.expand.timeline_via_ticket
|
||||||
<Badge
|
? ticket.expand.timeline_via_ticket.length
|
||||||
v-if="ticket.expand.tasks_via_ticket && ticket.expand.tasks_via_ticket.length > 0"
|
: 0
|
||||||
variant="outline"
|
}}
|
||||||
class="ml-2"
|
</Badge>
|
||||||
>
|
</TabsTrigger>
|
||||||
{{ ticket.expand.tasks_via_ticket ? ticket.expand.tasks_via_ticket.length : 0 }}
|
<TabsTrigger value="tasks">
|
||||||
<StatusIcon :status="taskStatus" class="size-6" />
|
Tasks
|
||||||
</Badge>
|
<Badge
|
||||||
</TabsTrigger>
|
v-if="
|
||||||
<TabsTrigger value="comments">
|
ticket.expand.tasks_via_ticket && ticket.expand.tasks_via_ticket.length > 0
|
||||||
Comments
|
"
|
||||||
<Badge
|
variant="outline"
|
||||||
v-if="
|
class="ml-2 hidden sm:inline-flex"
|
||||||
ticket.expand.comments_via_ticket &&
|
>
|
||||||
ticket.expand.comments_via_ticket.length > 0
|
{{ ticket.expand.tasks_via_ticket ? ticket.expand.tasks_via_ticket.length : 0 }}
|
||||||
"
|
<StatusIcon :status="taskStatus" class="size-6" />
|
||||||
variant="outline"
|
</Badge>
|
||||||
class="ml-2"
|
</TabsTrigger>
|
||||||
>
|
<TabsTrigger value="comments">
|
||||||
{{
|
Comments
|
||||||
ticket.expand.comments_via_ticket ? ticket.expand.comments_via_ticket.length : 0
|
<Badge
|
||||||
}}
|
v-if="
|
||||||
</Badge>
|
ticket.expand.comments_via_ticket &&
|
||||||
</TabsTrigger>
|
ticket.expand.comments_via_ticket.length > 0
|
||||||
</TabsList>
|
"
|
||||||
<TicketTab value="timeline">
|
variant="outline"
|
||||||
<TicketTimeline :ticket="ticket" :timeline="ticket.expand.timeline_via_ticket" />
|
class="ml-2 hidden sm:inline-flex"
|
||||||
</TicketTab>
|
>
|
||||||
<TicketTab value="tasks">
|
{{
|
||||||
<TicketTasks :ticket="ticket" :tasks="ticket.expand.tasks_via_ticket" />
|
ticket.expand.comments_via_ticket
|
||||||
</TicketTab>
|
? ticket.expand.comments_via_ticket.length
|
||||||
<TicketTab value="comments">
|
: 0
|
||||||
<TicketComments :ticket="ticket" :comments="ticket.expand.comments_via_ticket" />
|
}}
|
||||||
</TicketTab>
|
</Badge>
|
||||||
</Tabs>
|
</TabsTrigger>
|
||||||
<Separator class="xl:hidden" />
|
</TabsList>
|
||||||
</div>
|
<TicketTab value="timeline">
|
||||||
<ScrollArea>
|
<TicketTimeline :ticket="ticket" :timeline="ticket.expand.timeline_via_ticket" />
|
||||||
<div class="flex flex-initial flex-col gap-4 p-4 xl:w-96">
|
</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>
|
||||||
<div class="flex h-10 flex-row items-center justify-between text-muted-foreground">
|
<div class="flex h-10 flex-row items-center justify-between text-muted-foreground">
|
||||||
<span class="text-sm font-semibold"> Details </span>
|
<span class="text-sm font-semibold"> Details </span>
|
||||||
@@ -196,10 +207,10 @@ const updateDescription = (value: string) => (message.value = value)
|
|||||||
<Separator />
|
<Separator />
|
||||||
<TicketFiles :ticket="ticket" :files="ticket.expand.files_via_ticket" />
|
<TicketFiles :ticket="ticket" :files="ticket.expand.files_via_ticket" />
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ColumnBodyContainer>
|
||||||
</div>
|
</ColumnBody>
|
||||||
<Separator />
|
<Separator />
|
||||||
<TicketCloseBar :ticket="ticket" />
|
<TicketCloseBar :ticket="ticket" class="shrink-0" />
|
||||||
</div>
|
</template>
|
||||||
</TanView>
|
</TanView>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -38,13 +38,13 @@ const updateName = (value: string) => {
|
|||||||
<DynamicInput :modelValue="ticket.name" @update:modelValue="updateName" class="-mx-1" />
|
<DynamicInput :modelValue="ticket.name" @update:modelValue="updateName" class="-mx-1" />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div class="flex flex-row space-x-2 px-1 text-xs">
|
<div class="flex flex-col items-stretch gap-1 text-xs text-muted-foreground md:h-4 md:flex-row">
|
||||||
<div class="flex items-center gap-1 text-muted-foreground">
|
<div>
|
||||||
Created:
|
Created:
|
||||||
{{ format(new Date(ticket.created), 'PPpp') }}
|
{{ format(new Date(ticket.created), 'PPpp') }}
|
||||||
</div>
|
</div>
|
||||||
<Separator orientation="vertical" />
|
<Separator orientation="vertical" class="hidden md:block" />
|
||||||
<div class="flex items-center gap-1 text-muted-foreground">
|
<div>
|
||||||
Updated:
|
Updated:
|
||||||
{{ format(new Date(ticket.updated), 'PPpp') }}
|
{{ format(new Date(ticket.updated), 'PPpp') }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import ColumnHeader from '@/components/layout/ColumnHeader.vue'
|
||||||
import TicketListList from '@/components/ticket/TicketListList.vue'
|
import TicketListList from '@/components/ticket/TicketListList.vue'
|
||||||
import TicketNewDialog from '@/components/ticket/TicketNewDialog.vue'
|
import TicketNewDialog from '@/components/ticket/TicketNewDialog.vue'
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||||
@@ -14,7 +15,6 @@ import {
|
|||||||
PaginationNext,
|
PaginationNext,
|
||||||
PaginationPrev
|
PaginationPrev
|
||||||
} from '@/components/ui/pagination'
|
} from '@/components/ui/pagination'
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ watch(
|
|||||||
if (!route.params.id && ticketItems.value && ticketItems.value.items.length > 0) {
|
if (!route.params.id && ticketItems.value && ticketItems.value.items.length > 0) {
|
||||||
router.push({
|
router.push({
|
||||||
name: 'tickets',
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex h-screen flex-col">
|
<ColumnHeader :title="selectedType?.plural">
|
||||||
<div class="flex items-center bg-background px-4 py-2">
|
<div class="ml-auto">
|
||||||
<h1 class="text-xl font-bold">
|
<TicketNewDialog :selectedType="selectedType" />
|
||||||
{{ selectedType?.plural }}
|
</div>
|
||||||
</h1>
|
</ColumnHeader>
|
||||||
<div class="ml-auto">
|
<Tabs v-model="tab" class="flex flex-1 flex-col overflow-hidden">
|
||||||
<TicketNewDialog :selectedType="selectedType" />
|
<div class="flex items-center justify-between px-2 pt-2">
|
||||||
</div>
|
<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>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
<Tabs v-model="tab" class="flex flex-1 flex-col overflow-hidden">
|
<div v-if="isPending" class="flex h-full w-full items-center justify-center">
|
||||||
<div class="flex items-center justify-between px-4 pt-2">
|
<LoaderCircle class="h-16 w-16 animate-spin text-primary" />
|
||||||
<TabsList>
|
</div>
|
||||||
<TabsTrigger value="all">All</TabsTrigger>
|
<Alert v-else-if="isError" variant="destructive" class="mb-2 h-screen w-screen">
|
||||||
<TabsTrigger value="open">Open</TabsTrigger>
|
<AlertTitle>Error</AlertTitle>
|
||||||
<TabsTrigger value="closed">Closed</TabsTrigger>
|
<AlertDescription>{{ error }}</AlertDescription>
|
||||||
</TabsList>
|
</Alert>
|
||||||
<!-- Button variant="outline" size="sm" class="h-7 gap-1 rounded-md px-3">
|
<div v-else-if="ticketItems" class="flex-1 overflow-y-auto overflow-x-hidden">
|
||||||
<ListFilter class="h-3.5 w-3.5" />
|
<TicketListList :tickets="ticketItems.items" />
|
||||||
<span class="sr-only sm:not-sr-only">Filter</span>
|
</div>
|
||||||
</Button-->
|
<Separator />
|
||||||
</div>
|
<div class="my-2 flex items-center justify-center">
|
||||||
<div class="px-4 py-2">
|
<span class="text-xs text-muted-foreground">
|
||||||
<form>
|
{{ ticketItems ? ticketItems.items.length : '?' }} of
|
||||||
<div class="relative flex flex-row items-center">
|
{{ ticketItems ? ticketItems.totalItems : '?' }} tickets
|
||||||
<Input v-model="searchValue" placeholder="Search" @keydown.enter.prevent class="pl-8" />
|
</span>
|
||||||
<span class="absolute inset-y-0 start-0 flex items-center justify-center px-2">
|
</div>
|
||||||
<Search class="size-4 text-muted-foreground" />
|
<div class="mb-2 flex items-center justify-center">
|
||||||
</span>
|
<Pagination
|
||||||
</div>
|
v-slot="{ page }"
|
||||||
</form>
|
:total="ticketItems ? ticketItems.totalItems : 0"
|
||||||
</div>
|
:itemsPerPage="perPage"
|
||||||
<Separator />
|
:sibling-count="0"
|
||||||
<div v-if="isPending" class="flex h-full w-full items-center justify-center">
|
:default-page="1"
|
||||||
<LoaderCircle class="h-16 w-16 animate-spin text-primary" />
|
@update:page="page = $event"
|
||||||
</div>
|
>
|
||||||
<Alert v-else-if="isError" variant="destructive" class="mb-4 h-screen w-screen">
|
<PaginationList v-slot="{ items }" class="flex items-center gap-1">
|
||||||
<AlertTitle>Error</AlertTitle>
|
<PaginationFirst />
|
||||||
<AlertDescription>{{ error }}</AlertDescription>
|
<PaginationPrev />
|
||||||
</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 />
|
|
||||||
|
|
||||||
<template v-for="(item, index) in items">
|
<template v-for="(item, index) in items">
|
||||||
<PaginationListItem
|
<PaginationListItem
|
||||||
v-if="item.type === 'page'"
|
v-if="item.type === 'page'"
|
||||||
:key="index"
|
:key="index"
|
||||||
:value="item.value"
|
:value="item.value"
|
||||||
as-child
|
as-child
|
||||||
>
|
>
|
||||||
<Button
|
<Button class="h-10 w-10 p-0" :variant="item.value === page ? 'default' : 'outline'">
|
||||||
class="h-10 w-10 p-0"
|
{{ item.value }}
|
||||||
:variant="item.value === page ? 'default' : 'outline'"
|
</Button>
|
||||||
>
|
</PaginationListItem>
|
||||||
{{ item.value }}
|
<PaginationEllipsis v-else :key="item.type" :index="index" />
|
||||||
</Button>
|
</template>
|
||||||
</PaginationListItem>
|
<PaginationNext />
|
||||||
<PaginationEllipsis v-else :key="item.type" :index="index" />
|
<PaginationLast />
|
||||||
</template>
|
</PaginationList>
|
||||||
<PaginationNext />
|
</Pagination>
|
||||||
<PaginationLast />
|
</div>
|
||||||
</PaginationList>
|
</Tabs>
|
||||||
</Pagination>
|
|
||||||
</div>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import ResourceListElement from '@/components/common/ResourceListElement.vue'
|
import ResourceListElement from '@/components/layout/ResourceListElement.vue'
|
||||||
|
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ defineProps<{
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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
|
<ResourceListElement
|
||||||
v-for="item of tickets"
|
v-for="item of tickets"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import { TabsContent } from '@/components/ui/tabs'
|
import { TabsContent } from '@/components/ui/tabs'
|
||||||
|
|
||||||
@@ -12,10 +11,8 @@ defineProps<{
|
|||||||
<TabsContent :value="value" class="flex-1 overflow-hidden">
|
<TabsContent :value="value" class="flex-1 overflow-hidden">
|
||||||
<div class="flex h-full flex-col overflow-hidden">
|
<div class="flex h-full flex-col overflow-hidden">
|
||||||
<Separator class="mt-2" />
|
<Separator class="mt-2" />
|
||||||
<ScrollArea class="flex-1">
|
<slot />
|
||||||
<slot />
|
<div class="h-4" />
|
||||||
<div class="h-4" />
|
|
||||||
</ScrollArea>
|
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import DeleteDialog from '@/components/common/DeleteDialog.vue'
|
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 TicketPanel from '@/components/ticket/TicketPanel.vue'
|
||||||
import LinkAddDialog from '@/components/ticket/link/LinkAddDialog.vue'
|
import LinkAddDialog from '@/components/ticket/link/LinkAddDialog.vue'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
@@ -28,7 +28,12 @@ const dialogOpen = ref(false)
|
|||||||
>
|
>
|
||||||
No links added yet.
|
No links added yet.
|
||||||
</div>
|
</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">
|
<a :href="link.url" target="_blank" class="flex flex-1 items-center overflow-hidden">
|
||||||
<span class="mr-2 text-blue-500 underline">
|
<span class="mr-2 text-blue-500 underline">
|
||||||
{{ link.name }}
|
{{ link.name }}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import DeleteDialog from '@/components/common/DeleteDialog.vue'
|
import DeleteDialog from '@/components/common/DeleteDialog.vue'
|
||||||
import PanelListElement from '@/components/common/PanelListElement.vue'
|
|
||||||
import UserSelect from '@/components/common/UserSelect.vue'
|
import UserSelect from '@/components/common/UserSelect.vue'
|
||||||
import DynamicInput from '@/components/input/DynamicInput.vue'
|
import DynamicInput from '@/components/input/DynamicInput.vue'
|
||||||
|
import PanelListElement from '@/components/layout/PanelListElement.vue'
|
||||||
import TaskAddDialog from '@/components/ticket/task/TaskAddDialog.vue'
|
import TaskAddDialog from '@/components/ticket/task/TaskAddDialog.vue'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Card } from '@/components/ui/card'
|
import { Card } from '@/components/ui/card'
|
||||||
@@ -67,12 +67,14 @@ const updateTaskName = (id: string, name: string) => updateTaskNameMutation.muta
|
|||||||
</Card>
|
</Card>
|
||||||
<Card v-else>
|
<Card v-else>
|
||||||
<PanelListElement v-for="task in tasks" :key="task.id" class="pr-1">
|
<PanelListElement v-for="task in tasks" :key="task.id" class="pr-1">
|
||||||
<Checkbox :checked="!task.open" class="mr-2" @click="check(task)" />
|
<div class="flex flex-row items-center">
|
||||||
<DynamicInput
|
<Checkbox :checked="!task.open" class="mr-2" @click="check(task)" />
|
||||||
:modelValue="task.name"
|
<DynamicInput
|
||||||
@update:modelValue="updateTaskName(task.id, $event)"
|
:modelValue="task.name"
|
||||||
class="mr-2 flex-1"
|
@update:modelValue="updateTaskName(task.id, $event)"
|
||||||
/>
|
class="mr-2 flex-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div class="ml-auto flex items-center">
|
<div class="ml-auto flex items-center">
|
||||||
<UserSelect v-if="!task.expand.owner" @update:modelValue="update(task.id, $event)">
|
<UserSelect v-if="!task.expand.owner" @update:modelValue="update(task.id, $event)">
|
||||||
<Button variant="outline" role="combobox" class="h-8">
|
<Button variant="outline" role="combobox" class="h-8">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import PanelListElement from '@/components/common/PanelListElement.vue'
|
|
||||||
import DynamicMDEditor from '@/components/input/DynamicMDEditor.vue'
|
import DynamicMDEditor from '@/components/input/DynamicMDEditor.vue'
|
||||||
|
import PanelListElement from '@/components/layout/PanelListElement.vue'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
Dialog,
|
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 OpenTickets from '@/components/dashboard/OpenTickets.vue'
|
||||||
import TicketOverTime from '@/components/dashboard/TicketOverTime.vue'
|
import TicketOverTime from '@/components/dashboard/TicketOverTime.vue'
|
||||||
import TicketTypes from '@/components/dashboard/TicketTypes.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 TwoColumn from '@/components/layout/TwoColumn.vue'
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
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'
|
import { ExternalLink } from 'lucide-vue-next'
|
||||||
|
|
||||||
@@ -46,96 +47,91 @@ onMounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TwoColumn>
|
<TwoColumn>
|
||||||
<div class="flex h-screen flex-1 flex-col">
|
<ColumnHeader title="Dashboard" />
|
||||||
<div class="flex h-14 min-h-14 items-center bg-background px-4 py-2">
|
<ColumnBody>
|
||||||
<h1 class="text-xl font-bold">Dashboard</h1>
|
<ColumnBodyContainer
|
||||||
</div>
|
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]"
|
||||||
<Separator class="shrink-0" />
|
>
|
||||||
<ScrollArea>
|
<Card>
|
||||||
<div
|
<CardHeader>
|
||||||
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]"
|
<CardTitle>{{ count('tasks') }}</CardTitle>
|
||||||
>
|
<CardDescription>Tasks</CardDescription>
|
||||||
<Card>
|
</CardHeader>
|
||||||
<CardHeader>
|
</Card>
|
||||||
<CardTitle>{{ count('tasks') }}</CardTitle>
|
<Card>
|
||||||
<CardDescription>Tasks</CardDescription>
|
<CardHeader>
|
||||||
</CardHeader>
|
<CardTitle>{{ count('tickets') }}</CardTitle>
|
||||||
</Card>
|
<CardDescription>Tickets</CardDescription>
|
||||||
<Card>
|
</CardHeader>
|
||||||
<CardHeader>
|
</Card>
|
||||||
<CardTitle>{{ count('tickets') }}</CardTitle>
|
<Card>
|
||||||
<CardDescription>Tickets</CardDescription>
|
<CardHeader>
|
||||||
</CardHeader>
|
<CardTitle>{{ count('users') }}</CardTitle>
|
||||||
</Card>
|
<CardDescription>Users</CardDescription>
|
||||||
<Card>
|
</CardHeader>
|
||||||
<CardHeader>
|
</Card>
|
||||||
<CardTitle>{{ count('users') }}</CardTitle>
|
<Card>
|
||||||
<CardDescription>Users</CardDescription>
|
<CardHeader>
|
||||||
</CardHeader>
|
<CardTitle>{{ count('reactions') }}</CardTitle>
|
||||||
</Card>
|
<CardDescription>Reactions</CardDescription>
|
||||||
<Card>
|
</CardHeader>
|
||||||
<CardHeader>
|
</Card>
|
||||||
<CardTitle></CardTitle>
|
<Card>
|
||||||
<CardDescription></CardDescription>
|
<CardHeader>
|
||||||
</CardHeader>
|
<CardTitle> Catalyst</CardTitle>
|
||||||
</Card>
|
</CardHeader>
|
||||||
<Card>
|
<CardContent class="flex flex-1 flex-col gap-1">
|
||||||
<CardHeader>
|
<a
|
||||||
<CardTitle> Catalyst</CardTitle>
|
href="https://catalyst.security-brewery.com/docs/category/catalyst-handbook"
|
||||||
</CardHeader>
|
target="_blank"
|
||||||
<CardContent class="flex flex-1 flex-col gap-1">
|
class="flex items-center rounded border p-2 text-blue-500 hover:bg-accent"
|
||||||
<a
|
>
|
||||||
href="https://catalyst-soar.com/docs/category/catalyst-handbook"
|
Open Catalyst Handbook
|
||||||
target="_blank"
|
<ExternalLink class="ml-2 h-4 w-4" />
|
||||||
class="flex items-center rounded border p-2 text-blue-500 hover:bg-accent"
|
</a>
|
||||||
>
|
<a
|
||||||
Open Catalyst Handbook
|
href="/_/"
|
||||||
<ExternalLink class="ml-2 h-4 w-4" />
|
target="_blank"
|
||||||
</a>
|
class="flex items-center rounded border p-2 text-blue-500 hover:bg-accent"
|
||||||
<a
|
>
|
||||||
href="/_/"
|
Open Admin Interface
|
||||||
target="_blank"
|
<ExternalLink class="ml-2 h-4 w-4" />
|
||||||
class="flex items-center rounded border p-2 text-blue-500 hover:bg-accent"
|
</a>
|
||||||
>
|
</CardContent>
|
||||||
Open Admin Interface
|
</Card>
|
||||||
<ExternalLink class="ml-2 h-4 w-4" />
|
<Card>
|
||||||
</a>
|
<CardHeader>
|
||||||
</CardContent>
|
<CardTitle> Tickets by Type</CardTitle>
|
||||||
</Card>
|
</CardHeader>
|
||||||
<Card>
|
<CardContent>
|
||||||
<CardHeader>
|
<TicketTypes />
|
||||||
<CardTitle> Tickets by Type</CardTitle>
|
</CardContent>
|
||||||
</CardHeader>
|
</Card>
|
||||||
<CardContent>
|
<Card class="xl:col-span-2">
|
||||||
<TicketTypes />
|
<CardHeader>
|
||||||
</CardContent>
|
<CardTitle>Tickets Per Week</CardTitle>
|
||||||
</Card>
|
</CardHeader>
|
||||||
<Card class="xl:col-span-2">
|
<CardContent>
|
||||||
<CardHeader>
|
<TicketOverTime />
|
||||||
<CardTitle>Tickets Per Week</CardTitle>
|
</CardContent>
|
||||||
</CardHeader>
|
</Card>
|
||||||
<CardContent>
|
<Card class="xl:col-span-2">
|
||||||
<TicketOverTime />
|
<CardHeader>
|
||||||
</CardContent>
|
<CardTitle>Your Open Tickets</CardTitle>
|
||||||
</Card>
|
</CardHeader>
|
||||||
<Card class="xl:col-span-2">
|
<CardContent>
|
||||||
<CardHeader>
|
<OpenTickets />
|
||||||
<CardTitle>Your Open Tickets</CardTitle>
|
</CardContent>
|
||||||
</CardHeader>
|
</Card>
|
||||||
<CardContent>
|
<Card class="xl:col-span-2">
|
||||||
<OpenTickets />
|
<CardHeader>
|
||||||
</CardContent>
|
<CardTitle>Your Open Tasks</CardTitle>
|
||||||
</Card>
|
</CardHeader>
|
||||||
<Card class="xl:col-span-2">
|
<CardContent>
|
||||||
<CardHeader>
|
<OpenTasks />
|
||||||
<CardTitle>Your Open Tasks</CardTitle>
|
</CardContent>
|
||||||
</CardHeader>
|
</Card>
|
||||||
<CardContent>
|
</ColumnBodyContainer>
|
||||||
<OpenTasks />
|
</ColumnBody>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
</div>
|
|
||||||
</TwoColumn>
|
</TwoColumn>
|
||||||
</template>
|
</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 ThreeColumn from '@/components/layout/ThreeColumn.vue'
|
||||||
import ReactionDisplay from '@/components/reaction/ReactionDisplay.vue'
|
import ReactionDisplay from '@/components/reaction/ReactionDisplay.vue'
|
||||||
import ReactionList from '@/components/reaction/ReactionList.vue'
|
import ReactionList from '@/components/reaction/ReactionList.vue'
|
||||||
@@ -22,14 +23,14 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ThreeColumn>
|
<ThreeColumn :show-details="!!id">
|
||||||
<template #list>
|
<template #list>
|
||||||
<ReactionList />
|
<ReactionList />
|
||||||
</template>
|
</template>
|
||||||
<template #single>
|
<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
|
No reaction selected
|
||||||
</div>
|
</ColumnBody>
|
||||||
<ReactionNew v-else-if="id === 'new'" key="new" />
|
<ReactionNew v-else-if="id === 'new'" key="new" />
|
||||||
<ReactionDisplay v-else :key="id" :id="id" />
|
<ReactionDisplay v-else :key="id" :id="id" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import TanView from '@/components/TanView.vue'
|
import TanView from '@/components/TanView.vue'
|
||||||
|
import ColumnBody from '@/components/layout/ColumnBody.vue'
|
||||||
import ThreeColumn from '@/components/layout/ThreeColumn.vue'
|
import ThreeColumn from '@/components/layout/ThreeColumn.vue'
|
||||||
import TicketDisplay from '@/components/ticket/TicketDisplay.vue'
|
import TicketDisplay from '@/components/ticket/TicketDisplay.vue'
|
||||||
import TicketList from '@/components/ticket/TicketList.vue'
|
import TicketList from '@/components/ticket/TicketList.vue'
|
||||||
@@ -41,20 +42,17 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ThreeColumn>
|
<ThreeColumn :show-details="!!id">
|
||||||
<template #list>
|
<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" />
|
<TicketList v-if="selectedType" :key="selectedType.id" :selectedType="selectedType" />
|
||||||
</TanView>
|
</TanView>
|
||||||
</template>
|
</template>
|
||||||
<template #single>
|
<template #single>
|
||||||
<TanView :isError="isError" :isPending="isPending" :error="error" :value="selectedType">
|
<TanView :isError="isError" :isPending="isPending" :error="error">
|
||||||
<div
|
<ColumnBody v-if="!id" class="items-center justify-center text-lg text-gray-500">
|
||||||
v-if="!id"
|
|
||||||
class="flex h-full w-full items-center justify-center text-lg text-gray-500"
|
|
||||||
>
|
|
||||||
No ticket selected
|
No ticket selected
|
||||||
</div>
|
</ColumnBody>
|
||||||
<TicketDisplay v-else-if="selectedType" :key="id" :selectedType="selectedType" />
|
<TicketDisplay v-else-if="selectedType" :key="id" :selectedType="selectedType" />
|
||||||
</TanView>
|
</TanView>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user