init
This commit is contained in:
commit
fcec12e5ba
78 files changed
+9569
No files matched your search
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
const menu = [
|
||||
{ id: 1, title: 'home', to: '/' },
|
||||
{ id: 2, title: 'issues', to: '/issues' },
|
||||
{ id: 3, title: 'summary', to: '/summary' },
|
||||
{ id: 4, title: 'reports', to: '/reports' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-list class="mt-1" width="200" density="compact">
|
||||
<v-list-item v-for="item in menu" :key="item.id" :to="item.to">
|
||||
<v-list-item-title class="d-flex align-center ga-4">
|
||||
<v-icon icon="mdi-information-outline" />
|
||||
<div>{{ item.title }}</div>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,107 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { itemsList } from '@/api/user.api.ts'
|
||||
import type { User } from '@/api/user.api.ts'
|
||||
|
||||
interface Props {
|
||||
value?: User | null
|
||||
compact?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), { compact: true })
|
||||
const emit = defineEmits<{ changed: [value: string] }>()
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
const users = ref<User[]>([])
|
||||
const page = ref(0)
|
||||
const hasMore = ref(true)
|
||||
|
||||
function initials(user: Pick<User, 'name' | 'email'>) {
|
||||
return (user.name || user.email || '?')[0]!.toUpperCase()
|
||||
}
|
||||
|
||||
async function loadMore({ done }: { done: (status: 'ok' | 'empty' | 'error') => void }) {
|
||||
if (!hasMore.value) {
|
||||
done('empty')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const nextPage = page.value + 1
|
||||
const res = await itemsList({ page: nextPage, limit: PAGE_SIZE })
|
||||
users.value.push(...res.list)
|
||||
page.value = nextPage
|
||||
hasMore.value = nextPage < res.meta.totalPages
|
||||
done(hasMore.value ? 'ok' : 'empty')
|
||||
} catch {
|
||||
done('error')
|
||||
}
|
||||
}
|
||||
|
||||
const currentInitials = computed(() => (props.value ? initials(props.value) : '?'))
|
||||
|
||||
const select = (value: unknown) => {
|
||||
if (Array.isArray(value) && typeof value[0] === 'string') {
|
||||
emit('changed', value[0])
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="">
|
||||
<v-menu>
|
||||
<template v-slot:activator="{ props: activatorProps }">
|
||||
<div class="d-flex align-center ga-2">
|
||||
<v-btn rounded="circle" density="compact" icon variant="text" v-bind="activatorProps">
|
||||
<v-avatar
|
||||
:image="value?.image ?? undefined"
|
||||
:text="currentInitials"
|
||||
rounded="circle"
|
||||
density="compact"
|
||||
color="primary"
|
||||
border
|
||||
/>
|
||||
</v-btn>
|
||||
<div v-if="!compact">{{ value?.name || value?.email || 'Unassigned' }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<v-infinite-scroll height="16rem" @load="loadMore">
|
||||
<v-list
|
||||
border
|
||||
width="12rem"
|
||||
class="mt-1"
|
||||
select-strategy="single-leaf"
|
||||
density="compact"
|
||||
@update:selected="select"
|
||||
>
|
||||
<v-list-item value="">
|
||||
<div class="d-flex align-center ga-2">
|
||||
<v-avatar density="compact" border rounded="circle" icon="mdi-account" />
|
||||
<div>Unassigned</div>
|
||||
</div>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item v-for="user in users" :key="user.id" :value="user.id">
|
||||
<div class="d-flex align-center ga-2">
|
||||
<v-avatar
|
||||
:image="user.image ?? undefined"
|
||||
:text="initials(user)"
|
||||
rounded="circle"
|
||||
density="compact"
|
||||
border
|
||||
color="primary"
|
||||
/>
|
||||
<div>{{ user.name || user.email }}</div>
|
||||
</div>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<template #empty />
|
||||
</v-infinite-scroll>
|
||||
</v-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
}
|
||||
|
||||
const icons = ['mdi-chevron-right', 'mdi-chevron-down']
|
||||
const isOpen = ref(true)
|
||||
const { title } = defineProps<Props>()
|
||||
|
||||
const icon = computed(() => {
|
||||
return isOpen.value ? icons[1] : icons[0]
|
||||
})
|
||||
|
||||
const open = () => {
|
||||
isOpen.value = !isOpen.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="d-flex flex-column ga-1">
|
||||
<div class="d-flex align-center ga-1">
|
||||
<v-btn density="compact" size="small" :icon="icon" @click="open" />
|
||||
<div @click="open" class="font-weight-bold text-capitalize text-title-medium cursor-pointer">{{ title }}</div>
|
||||
</div>
|
||||
<div v-if="isOpen" class="ml-6 mt-2">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
interface Props {
|
||||
value?: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<{ changed: [value: string] }>()
|
||||
|
||||
const editing = ref(false)
|
||||
const draft = ref(props.value ?? '')
|
||||
|
||||
const startEdit = () => {
|
||||
draft.value = props.value ?? ''
|
||||
editing.value = true
|
||||
}
|
||||
|
||||
const save = () => {
|
||||
emit('changed', draft.value)
|
||||
editing.value = false
|
||||
}
|
||||
|
||||
const cancel = () => {
|
||||
draft.value = props.value ?? ''
|
||||
editing.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="issue-content">
|
||||
<template v-if="!editing">
|
||||
<v-sheet class="pa-2 cursor-pointer content-sheet" @click="startEdit">
|
||||
{{ value ?? 'click to add content' }}
|
||||
</v-sheet>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<v-textarea class="mb-1" hide-details v-model="draft" variant="outlined" auto-grow rows="4" />
|
||||
|
||||
<div class="d-flex justify-end ga-1">
|
||||
<v-btn density="compact" variant="text" text="Cancel" @click="cancel" />
|
||||
<v-btn density="compact" color="primary" variant="tonal" text="Save" @click="save" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.content-sheet:hover {
|
||||
background-color: rgba(var(--v-theme-on-surface), 0.06);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,69 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useIssueStore } from '@/stores/issue.ts'
|
||||
import { useProjectStore } from '@/stores/project.ts'
|
||||
import type { Issue } from '@/api/issue.api.ts'
|
||||
|
||||
const issueStore = useIssueStore()
|
||||
const projectStore = useProjectStore()
|
||||
const emit = defineEmits<{ created: [issue: Issue] }>()
|
||||
|
||||
const showForm = ref(false)
|
||||
const newIssue = reactive({
|
||||
key: 'type',
|
||||
title: '',
|
||||
})
|
||||
|
||||
const onCreate = async () => {
|
||||
const projectId = projectStore.current?.id
|
||||
if (!projectId) return
|
||||
|
||||
const created = await issueStore.create({ ...newIssue, projectId })
|
||||
if (!created) return
|
||||
|
||||
emit('created', created)
|
||||
newIssue.title = ''
|
||||
showForm.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-list-item v-if="showForm" class="">
|
||||
<template #prepend>
|
||||
<v-menu v-if="true">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-icon v-bind="props" icon="mdi-square" />
|
||||
</template>
|
||||
|
||||
<v-list select-strategy="single-leaf">
|
||||
<v-list-item v-for="(el, idx) in ['aaa', 'bbb']" :key="idx" :value="el">
|
||||
<v-list-item-title>{{ el }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</template>
|
||||
|
||||
<template #title>
|
||||
<div class="d-flex ga-2 align-center">
|
||||
<v-text-field
|
||||
v-model="newIssue.title"
|
||||
variant="filled"
|
||||
placeholder="Describe your issue"
|
||||
autofocus
|
||||
@keyup.enter="onCreate"
|
||||
/>
|
||||
<v-btn density="compact" variant="tonal" text="Create" @click="onCreate" />
|
||||
<v-btn density="compact" variant="text" icon="mdi-close" @click="showForm = false" />
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-list-item class="d-flex justify-center" v-else @click="showForm = !showForm">
|
||||
<div class="d-flex ga-2 align-center">
|
||||
<v-icon icon="mdi-plus" />
|
||||
<div>add issue</div>
|
||||
</div>
|
||||
<!-- <v-btn density="compact" variant="plain" prepend-icon="mdi-plus"> create </v-btn>-->
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,126 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watchEffect } from 'vue'
|
||||
import { useIssueStore } from '@/stores/issue.js'
|
||||
import { useStatusStore } from '@/stores/status.ts'
|
||||
import ExpansionPanel from '@/components/ExpansionPanel.vue'
|
||||
import IssueContent from '@/components/IssueContent.vue'
|
||||
import type { Issue } from '@/api/issue.api.ts'
|
||||
import StatusMenu from '@/components/StatusMenu.vue'
|
||||
import AssigneeMenu from '@/components/AssigneeMenu.vue'
|
||||
import ReporterInfo from '@/components/ReporterInfo.vue'
|
||||
|
||||
interface Props {
|
||||
ckey: string
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
statusChanged: [issue: Issue]
|
||||
assigneeChanged: [issue: Issue]
|
||||
}>()
|
||||
|
||||
const issueStore = useIssueStore()
|
||||
const statusStore = useStatusStore()
|
||||
const issue = ref<Issue>()
|
||||
|
||||
const { ckey } = defineProps<Props>()
|
||||
|
||||
const statusName = computed(() => {
|
||||
return statusStore.statuses?.find((el) => el.alias === issue.value?.status)?.name ?? issue.value?.status
|
||||
})
|
||||
|
||||
const createdAtFormatted = computed(() => {
|
||||
if (!issue.value?.createdAt) return ''
|
||||
return new Intl.DateTimeFormat('ru-RU', { dateStyle: 'medium', timeStyle: 'short' }).format(
|
||||
new Date(issue.value.createdAt),
|
||||
)
|
||||
})
|
||||
|
||||
async function fetch() {
|
||||
const res = await issueStore.getByKey(ckey)
|
||||
if (!res) {
|
||||
return
|
||||
}
|
||||
|
||||
issue.value = res
|
||||
}
|
||||
|
||||
watchEffect(async () => fetch())
|
||||
|
||||
const onContentChanged = async (content: string) => {
|
||||
if (!issue.value) return
|
||||
const updated = await issueStore.update(issue.value.id, { content })
|
||||
if (updated) issue.value = updated
|
||||
}
|
||||
|
||||
const onStatusChanged = async (status: Issue['status']) => {
|
||||
if (!issue.value) return
|
||||
|
||||
const updated = await issueStore.update(issue.value.id, { status })
|
||||
if (updated) {
|
||||
issue.value = updated
|
||||
emit('statusChanged', updated)
|
||||
}
|
||||
}
|
||||
|
||||
const onAssigneeChanged = async (assignee: string) => {
|
||||
if (!issue.value) return
|
||||
|
||||
const updated = await issueStore.update(issue.value.id, { assignee })
|
||||
if (updated) {
|
||||
issue.value = updated
|
||||
emit('assigneeChanged', updated)
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
applyUpdate: (updated: Issue) => {
|
||||
if (issue.value && issue.value.id === updated.id) {
|
||||
issue.value = updated
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="issue" class="d-flex flex-column ga-4 pa-4">
|
||||
<div>{{ ckey }}</div>
|
||||
<div class="text-uppercase text-title-large">{{ issue.title }}</div>
|
||||
|
||||
<v-card elevation="0" border>
|
||||
<v-card-text class="u-grid">
|
||||
<div class="text-medium-emphasis">Status:</div>
|
||||
<status-menu :value="issue.status" @changed="onStatusChanged" />
|
||||
|
||||
<div class="text-medium-emphasis">Reporter:</div>
|
||||
<reporter-info :user="issue.reporter" />
|
||||
|
||||
<div class="text-medium-emphasis">Assignee:</div>
|
||||
<assignee-menu :value="issue.assignee" :compact="false" @changed="onAssigneeChanged" />
|
||||
|
||||
<div class="text-medium-emphasis">Created At:</div>
|
||||
<div>{{ createdAtFormatted }}</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<expansion-panel title="Details">
|
||||
<issue-content :value="issue.content" @changed="onContentChanged" />
|
||||
</expansion-panel>
|
||||
|
||||
<expansion-panel title="Activity">
|
||||
<v-btn-group divided border density="compact">
|
||||
<v-btn>comments</v-btn>
|
||||
<v-btn>history</v-btn>
|
||||
</v-btn-group>
|
||||
</expansion-panel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.u-grid {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
align-items: center;
|
||||
column-gap: 1rem;
|
||||
row-gap: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const text = ref<string | null>(null)
|
||||
|
||||
async function onSearch() {
|
||||
console.log('search')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="u-grid2 ga-2">
|
||||
<v-text-field
|
||||
v-model="text"
|
||||
clearable
|
||||
placeholder="Search name..."
|
||||
@keydown.enter.prevent="onSearch"
|
||||
@click:clear="onSearch"
|
||||
/>
|
||||
<v-select variant="outlined" clearable :items="['abc', 'def']"></v-select>
|
||||
|
||||
<!-- <v-btn>filter</v-btn>-->
|
||||
|
||||
<!-- <v-btn variant="plain">clear filters</v-btn>-->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.u-grid2 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useIssueStore } from '@/stores/issue.ts'
|
||||
import { useStatusStore } from '@/stores/status.ts'
|
||||
|
||||
const issueStore = useIssueStore()
|
||||
const statusStore = useStatusStore()
|
||||
|
||||
const items = computed(() => {
|
||||
return (issueStore.stats?.byStatus ?? []).map((stat) => ({
|
||||
...stat,
|
||||
color: statusStore.statuses?.find((s) => s.alias === stat.status)?.color,
|
||||
}))
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="d-flex ga-1">
|
||||
<v-chip
|
||||
v-for="item in items"
|
||||
:key="item.status"
|
||||
:color="item.color"
|
||||
:title="item.label"
|
||||
variant="flat"
|
||||
label
|
||||
density="compact"
|
||||
>
|
||||
{{ item.count }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,78 @@
|
||||
<script setup lang="ts">
|
||||
import PriorityMenu from '@/components/PriorityMenu.vue'
|
||||
import StatusMenu from '@/components/StatusMenu.vue'
|
||||
import { useIssueStore } from '@/stores/issue.ts'
|
||||
import type { Issue } from '@/api/issue.api.ts'
|
||||
import AssigneeMenu from '@/components/AssigneeMenu.vue'
|
||||
|
||||
interface Props {
|
||||
issue: Issue
|
||||
selected?: boolean
|
||||
}
|
||||
|
||||
const { issue, selected } = defineProps<Props>()
|
||||
const emit = defineEmits<{
|
||||
statusChanged: [issue: Issue]
|
||||
priorityChanged: [issue: Issue]
|
||||
assigneeChanged: [issue: Issue]
|
||||
toggleSelect: [id: number]
|
||||
}>()
|
||||
|
||||
const issueStore = useIssueStore()
|
||||
|
||||
const onStatusChanged = async (status: Issue['status']) => {
|
||||
const updated = await issueStore.update(issue.id, { status })
|
||||
if (updated) emit('statusChanged', updated)
|
||||
}
|
||||
|
||||
const onPriorityChanged = async (priority: Issue['priority']) => {
|
||||
const updated = await issueStore.update(issue.id, { priority })
|
||||
if (updated) emit('priorityChanged', updated)
|
||||
}
|
||||
|
||||
const onAssigneeChanged = async (assignee: string) => {
|
||||
const updated = await issueStore.update(issue.id, { assignee })
|
||||
if (updated) emit('assigneeChanged', updated)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-list-item class="bg-on-surface-variant" :value="issue.key">
|
||||
<template #title>
|
||||
<div class="d-flex ga-4">
|
||||
<div>{{ issue.key }}</div>
|
||||
<div class="d-flex ga-1 align-center">
|
||||
<v-icon
|
||||
v-if="[3, 4].includes(issue.priority) && ['OPEN', 'IN_PROGRESS'].includes(issue.status)"
|
||||
icon="mdi-alert-box"
|
||||
color="error"
|
||||
></v-icon>
|
||||
<div :class="{ 'text-decoration-line-through': issue.status === 'DONE' }">{{ issue.title }}</div>
|
||||
</div>
|
||||
<!-- <div :class="{ 'text-decoration-line-through': issue.status === 'DONE' }">{{ issue.content }}</div>-->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #prepend>
|
||||
<div class="d-flex ga-1 align-center mr-2" @click.stop>
|
||||
<v-checkbox
|
||||
class="issue-checkbox"
|
||||
density="compact"
|
||||
:model-value="selected"
|
||||
@update:model-value="emit('toggleSelect', issue.id)"
|
||||
/>
|
||||
<v-icon icon="mdi-square" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #append>
|
||||
<div class="d-flex ga-4 align-center">
|
||||
<status-menu :value="issue.status" @changed="onStatusChanged" />
|
||||
<priority-menu :value="issue.priority" @changed="onPriorityChanged" />
|
||||
<assignee-menu :value="issue.assignee" @changed="onAssigneeChanged" />
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive } from 'vue'
|
||||
import { authClient } from '@/lib/auth-client.js'
|
||||
|
||||
const passwordData = reactive({
|
||||
newPassword: '',
|
||||
currentPassword: '',
|
||||
})
|
||||
|
||||
const changePassword = async () => {
|
||||
await authClient.changePassword({
|
||||
newPassword: passwordData.newPassword,
|
||||
currentPassword: passwordData.currentPassword,
|
||||
revokeOtherSessions: true,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-sheet>
|
||||
<div class="text-uppercase">Change password</div>
|
||||
<div class="d-flex ga-2">
|
||||
<v-text-field placeholder="current password" v-model="passwordData.currentPassword" />
|
||||
<v-text-field placeholder="new password" v-model="passwordData.newPassword" />
|
||||
<v-icon-btn variant="tonal" icon="mdi-check" @click="changePassword" />
|
||||
</div>
|
||||
</v-sheet>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,65 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import type { Issue } from '@/api/issue.api.ts'
|
||||
|
||||
enum Icons {
|
||||
highest = 'mdi-chevron-double-up',
|
||||
high = 'mdi-chevron-up',
|
||||
eq = 'mdi-equal',
|
||||
low = 'mdi-chevron-down',
|
||||
lowest = 'mdi-chevron-double-down',
|
||||
}
|
||||
|
||||
interface Props {
|
||||
value?: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<{ changed: [value: Issue['priority']] }>()
|
||||
|
||||
const priorityList = ref([
|
||||
{ icon: Icons.highest, value: 4 },
|
||||
{ icon: Icons.high, value: 3 },
|
||||
{ icon: Icons.eq, value: 2 },
|
||||
{ icon: Icons.low, value: 1 },
|
||||
{ icon: Icons.lowest, value: 0 },
|
||||
])
|
||||
|
||||
const currentIcon = computed(() => priorityList.value.find((el) => el.value === props.value)?.icon ?? Icons.eq)
|
||||
|
||||
const availablePriorities = computed(() => priorityList.value.filter((el) => el.value !== props.value))
|
||||
|
||||
function priorityColor(value?: number) {
|
||||
if (value === 3 || value === 4) return 'error'
|
||||
if (value === 2) return 'success'
|
||||
return 'primary'
|
||||
}
|
||||
|
||||
const currentColor = computed(() => priorityColor(props.value))
|
||||
|
||||
const select = (value: unknown) => {
|
||||
if (Array.isArray(value)) {
|
||||
emit('changed', value[0])
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="">
|
||||
<v-menu>
|
||||
<template v-slot:activator="{ props: activatorProps }">
|
||||
<v-btn density="compact" :icon="currentIcon" :color="currentColor" variant="text" v-bind="activatorProps" />
|
||||
</template>
|
||||
|
||||
<v-list border class="mt-1" select-strategy="single-leaf" density="compact" @update:selected="select">
|
||||
<v-list-item nav v-for="el in availablePriorities" :key="el.value" :value="el.value">
|
||||
<div class="d-flex justify-center align-center">
|
||||
<v-icon size="small" :icon="el.icon" :color="priorityColor(el.value)" />
|
||||
</div>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,61 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useProjectStore } from '@/stores/project.ts'
|
||||
|
||||
const projectStore = useProjectStore()
|
||||
const { projects, current } = storeToRefs(projectStore)
|
||||
|
||||
const currentProjectName = computed(() => current.value?.name ?? 'Select project')
|
||||
const selected = computed(() => (current.value ? [current.value.id] : []))
|
||||
|
||||
function select(value: unknown) {
|
||||
console.log(value)
|
||||
const id = Array.isArray(value) ? value[0] : value
|
||||
const project = projects.value.find((el) => el.id === id)
|
||||
|
||||
if (!project) {
|
||||
throw new Error(`Project ${id} not found`)
|
||||
}
|
||||
projectStore.select(project)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await projectStore.list({ page: 1, limit: 100 })
|
||||
await projectStore.restoreCurrent()
|
||||
|
||||
if (!current.value && projects.value[0]) {
|
||||
projectStore.select(projects.value[0])
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="projects.length" class="">
|
||||
<v-menu>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn border density="comfortable" variant="flat" :text="currentProjectName" v-bind="props" />
|
||||
</template>
|
||||
|
||||
<v-list
|
||||
width="12rem"
|
||||
class="mt-1"
|
||||
border
|
||||
select-strategy="single-leaf"
|
||||
density="compact"
|
||||
:selected="selected"
|
||||
mandatory
|
||||
@update:selected="select"
|
||||
>
|
||||
<v-list-item slim v-for="project in projects" :key="project.id" :value="project.id">
|
||||
<v-list-item-title>
|
||||
<v-chip size="small" color="primary" variant="flat" density="compact">{{ project.key }}</v-chip>
|
||||
{{ project.name }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import type { User } from '@/api/user.api.ts'
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface Props {
|
||||
user?: User | null
|
||||
}
|
||||
|
||||
const { user } = defineProps<Props>()
|
||||
|
||||
function initials(user: Pick<User, 'name' | 'email'>) {
|
||||
return (user.name || user.email || '?')[0]!.toUpperCase()
|
||||
}
|
||||
|
||||
const currentInitials = computed(() => (user ? initials(user) : '?'))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="">
|
||||
<div class="d-flex align-center ga-2">
|
||||
<v-btn rounded="circle" density="compact" icon variant="text">
|
||||
<v-avatar
|
||||
:image="user?.image ?? undefined"
|
||||
:text="currentInitials"
|
||||
color="primary"
|
||||
rounded="circle"
|
||||
density="compact"
|
||||
border
|
||||
/>
|
||||
</v-btn>
|
||||
<div>{{ user?.name || user?.email }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
import { authClient } from '@/lib/auth-client.ts'
|
||||
import { ref, watchEffect } from 'vue'
|
||||
|
||||
interface Session {
|
||||
id: string
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
userId: string
|
||||
expiresAt: Date
|
||||
token: string
|
||||
ipAddress?: string | null | undefined
|
||||
userAgent?: string | null | undefined
|
||||
}
|
||||
|
||||
const sessions = ref<Session[]>([])
|
||||
|
||||
const listSessions = async () => {
|
||||
const result = await authClient.listSessions()
|
||||
if (result.error) {
|
||||
return
|
||||
}
|
||||
|
||||
sessions.value = result.data
|
||||
}
|
||||
|
||||
const revokeSession = async (token: string) => {
|
||||
await authClient.revokeSession({
|
||||
token: token,
|
||||
})
|
||||
}
|
||||
|
||||
const revokeOtherSessions = async () => {
|
||||
await authClient.revokeOtherSessions()
|
||||
}
|
||||
|
||||
const revokeAllSessions = async () => {
|
||||
await authClient.revokeSessions()
|
||||
}
|
||||
|
||||
watchEffect(async () => {
|
||||
await listSessions()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="text-uppercase">Active sessions</div>
|
||||
<div class="mb-2">
|
||||
<v-btn prepend-icon="mdi-skull" @click="revokeOtherSessions()">revoke other sessions</v-btn>
|
||||
</div>
|
||||
<v-list border density="compact">
|
||||
<v-list-item class="hover:bg-primary-300" v-for="session in sessions" :key="session.id">
|
||||
<div class="text-truncate">{{ session.userAgent }}</div>
|
||||
<template #append>
|
||||
<v-icon-btn size="small" @click="revokeSession(session.token)" color="error" icon="mdi-trash-can" />
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import { useStatusStore } from '@/stores/status.ts'
|
||||
import { computed } from 'vue'
|
||||
import type { Issue } from '@/api/issue.api.ts'
|
||||
|
||||
interface Props {
|
||||
value?: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<{ changed: [value: Issue['status']] }>()
|
||||
|
||||
const statusStore = useStatusStore()
|
||||
|
||||
const currentStatusName = computed(() => {
|
||||
return statusStore.statuses?.find((el) => el.alias === props.value)?.name ?? props.value
|
||||
})
|
||||
|
||||
const currentStatusColor = computed(() => {
|
||||
return statusStore.statuses?.find((el) => el.alias === props.value)?.color ?? 'white'
|
||||
})
|
||||
|
||||
const select = (value: unknown) => {
|
||||
if (Array.isArray(value)) {
|
||||
emit('changed', value[0])
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="">
|
||||
<v-menu>
|
||||
<template v-slot:activator="{ props: activatorProps }">
|
||||
<v-btn
|
||||
slim
|
||||
:color="currentStatusColor"
|
||||
density="compact"
|
||||
variant="flat"
|
||||
v-bind="activatorProps"
|
||||
:text="currentStatusName"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<v-list border width="12rem" class="" select-strategy="single-leaf" density="compact" @update:selected="select">
|
||||
<v-list-item slim v-for="(el, idx) in statusStore.statuses" :key="idx" :value="el.alias">
|
||||
<v-list-item-title>{{ el.name }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface Props {
|
||||
user?: {
|
||||
readonly id: string
|
||||
readonly createdAt: Date
|
||||
readonly updatedAt: Date
|
||||
readonly email: string
|
||||
readonly emailVerified: boolean
|
||||
readonly name: string
|
||||
readonly image?: string | null | undefined
|
||||
}
|
||||
}
|
||||
|
||||
const { user } = defineProps<Props>()
|
||||
|
||||
const avatar = computed(() => {
|
||||
return user?.image ?? undefined
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="user" class="d-flex ga-4">
|
||||
<v-avatar size="x-large" color="info" :image="avatar" />
|
||||
|
||||
<div>
|
||||
<div>name: {{ user.name }}</div>
|
||||
<div>email: {{ user.email }}</div>
|
||||
<div>created at: {{ user.createdAt }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<v-skeleton-loader v-else type="avatar, list-item-three-line" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
Reference in new issue
Block a user