Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63861449cf | ||
|
|
ff0008d346 | ||
|
|
81dd976d8f | ||
|
|
9303a66aa8 |
No files matched your search
+26
-6
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useEventSource } from '@vueuse/core'
|
import { useEventSource } from '@vueuse/core'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { computed, onMounted, watch } from 'vue'
|
import { computed, watch } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { useStatusStore } from '@/stores/status.ts'
|
import { useStatusStore } from '@/stores/status.ts'
|
||||||
import { useIssueTypeStore } from '@/stores/issue-type.ts'
|
import { useIssueTypeStore } from '@/stores/issue-type.ts'
|
||||||
@@ -25,7 +25,7 @@ interface IssueSseEvent {
|
|||||||
type: string
|
type: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data, event } = useEventSource<string[], IssueSseEvent>(
|
const { event, open, close } = useEventSource<string[], IssueSseEvent>(
|
||||||
`${window.location.origin}/api/issues/events`,
|
`${window.location.origin}/api/issues/events`,
|
||||||
['issue.created', 'issue.updated', 'issue.deleted'],
|
['issue.created', 'issue.updated', 'issue.deleted'],
|
||||||
{
|
{
|
||||||
@@ -61,10 +61,30 @@ const layout = computed(() => {
|
|||||||
return layout ? `${layout}Layout` : 'DefaultLayout'
|
return layout ? `${layout}Layout` : 'DefaultLayout'
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
// Все эти ресурсы требуют авторизации: грузим при появлении сессии и чистим при выходе,
|
||||||
await Promise.all([statusStore.list(), issueTypeStore.list()])
|
// иначе запросы, ушедшие до логина, падают с 401 и больше не повторяются
|
||||||
await issueStore.getStat(currentProjectId.value)
|
watch(
|
||||||
})
|
() => session.value.data?.user.id,
|
||||||
|
async (userId) => {
|
||||||
|
if (!userId) {
|
||||||
|
close()
|
||||||
|
statusStore.reset()
|
||||||
|
issueTypeStore.reset()
|
||||||
|
issueStore.reset()
|
||||||
|
projectStore.reset()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
open()
|
||||||
|
try {
|
||||||
|
await Promise.all([statusStore.list(), issueTypeStore.list()])
|
||||||
|
await issueStore.getStat(currentProjectId.value)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load dictionaries', e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { apiClient } from '@/lib/api-client.ts'
|
|||||||
import type { User } from '@/api/user.api.ts'
|
import type { User } from '@/api/user.api.ts'
|
||||||
|
|
||||||
/** created — a pseudo-field: the issue creation record, both values are null */
|
/** created — a pseudo-field: the issue creation record, both values are null */
|
||||||
export type IssueChangeField = 'created' | 'title' | 'content' | 'status' | 'priority' | 'assignee' | 'dueDate'
|
export type IssueChangeField = 'created' | 'title' | 'content' | 'status' | 'priority' | 'assignee' | 'dueDate' | 'type'
|
||||||
|
|
||||||
/** Values are serialized: priority — number, dueDate — ISO date, assignee — user id */
|
/** Values are serialized: priority — number, dueDate — ISO date, assignee — user id */
|
||||||
export interface IssueFieldChange {
|
export interface IssueFieldChange {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const FIELD_LABELS: Record<IssueFieldChange['field'], string> = {
|
|||||||
priority: 'Priority',
|
priority: 'Priority',
|
||||||
assignee: 'Assignee',
|
assignee: 'Assignee',
|
||||||
dueDate: 'Due Date',
|
dueDate: 'Due Date',
|
||||||
|
type: 'Type',
|
||||||
}
|
}
|
||||||
|
|
||||||
const PRIORITY_LABELS: Record<string, string> = {
|
const PRIORITY_LABELS: Record<string, string> = {
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ const onAssigneeChanged = async (assignee: string) => {
|
|||||||
:model-value="selected"
|
:model-value="selected"
|
||||||
@update:model-value="emit('toggleSelect', issue.id)"
|
@update:model-value="emit('toggleSelect', issue.id)"
|
||||||
/>
|
/>
|
||||||
<issue-type :value="issue.type" @changed="onTypeChanged" />
|
<issue-type v-if="visible.type" :value="issue.type" @changed="onTypeChanged" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -23,11 +23,11 @@ const menu = ref([
|
|||||||
value: 3,
|
value: 3,
|
||||||
props: { prependIcon: 'mdi-calendar', to: '/calendar', requiresAuth: true },
|
props: { prependIcon: 'mdi-calendar', to: '/calendar', requiresAuth: true },
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
title: 'Games',
|
// title: 'Games',
|
||||||
value: 4,
|
// value: 4,
|
||||||
props: { prependIcon: 'mdi-view-dashboard', to: '/games', requiresAuth: false },
|
// props: { prependIcon: 'mdi-view-dashboard', to: '/games', requiresAuth: false },
|
||||||
},
|
// },
|
||||||
])
|
])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -99,12 +99,16 @@ onMounted(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<v-sheet border class="pa-4 d-flex flex-column ga-2">
|
||||||
<div class="text-uppercase">Notifications</div>
|
<div class="text-uppercase text-title-large">Notifications</div>
|
||||||
|
|
||||||
<v-skeleton-loader v-if="loading && !preferences.length" type="list-item, list-item" />
|
<v-skeleton-loader v-if="loading && !preferences.length" type="list-item, list-item" />
|
||||||
<div v-else class="d-flex flex-column">
|
<div v-else class="d-flex flex-column">
|
||||||
<v-switch
|
<v-switch
|
||||||
|
inset="material"
|
||||||
|
size="x-small"
|
||||||
|
true-icon="mdi-check"
|
||||||
|
false-icon="mdi-close"
|
||||||
v-for="preference in preferences"
|
v-for="preference in preferences"
|
||||||
:key="preference.channel"
|
:key="preference.channel"
|
||||||
:model-value="preference.enabled"
|
:model-value="preference.enabled"
|
||||||
@@ -118,36 +122,43 @@ onMounted(async () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-uppercase mt-4">Push in this browser</div>
|
<div>
|
||||||
<div v-if="permission === 'unsupported'" class="text-medium-emphasis">
|
<div class="text-uppercase">Push in this browser</div>
|
||||||
This browser does not support push notifications
|
<div v-if="permission === 'unsupported'" class="text-medium-emphasis">
|
||||||
|
This browser does not support push notifications
|
||||||
|
</div>
|
||||||
|
<div v-else-if="permission === 'denied'" class="text-medium-emphasis">
|
||||||
|
Notifications are blocked — allow them in the browser site settings
|
||||||
|
</div>
|
||||||
|
<v-switch
|
||||||
|
v-else
|
||||||
|
inset="material"
|
||||||
|
size="x-small"
|
||||||
|
true-icon="mdi-check"
|
||||||
|
false-icon="mdi-close"
|
||||||
|
:model-value="deviceEnabled"
|
||||||
|
:label="deviceEnabled ? 'Enabled' : 'Disabled'"
|
||||||
|
:loading="deviceSaving"
|
||||||
|
:disabled="deviceSaving"
|
||||||
|
color="primary"
|
||||||
|
density="compact"
|
||||||
|
hide-details
|
||||||
|
@update:model-value="toggleDevice"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="permission === 'denied'" class="text-medium-emphasis">
|
|
||||||
Notifications are blocked — allow them in the browser site settings
|
|
||||||
</div>
|
|
||||||
<v-switch
|
|
||||||
v-else
|
|
||||||
:model-value="deviceEnabled"
|
|
||||||
:label="deviceEnabled ? 'Enabled' : 'Disabled'"
|
|
||||||
:loading="deviceSaving"
|
|
||||||
:disabled="deviceSaving"
|
|
||||||
color="primary"
|
|
||||||
density="compact"
|
|
||||||
hide-details
|
|
||||||
@update:model-value="toggleDevice"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<v-btn
|
<div>
|
||||||
class="mt-2"
|
<v-btn
|
||||||
variant="tonal"
|
class="mt-2"
|
||||||
size="small"
|
variant="tonal"
|
||||||
prepend-icon="mdi-bell-ring-outline"
|
prepend-icon="mdi-bell-ring-outline"
|
||||||
:loading="testSending"
|
:loading="testSending"
|
||||||
@click="sendTestPush"
|
@click="sendTestPush"
|
||||||
>
|
>
|
||||||
Send test push
|
Send test push
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
|
</v-sheet>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
<style scoped lang="scss"></style>
|
||||||
@@ -17,12 +17,19 @@ const changePassword = async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-sheet>
|
<v-sheet border class="pa-4 d-flex flex-column ga-2">
|
||||||
<div class="text-uppercase">Change password</div>
|
<div class="text-uppercase text-title-large">Change password</div>
|
||||||
<div class="d-flex ga-2">
|
<div class="d-flex flex-column ga-2">
|
||||||
<v-text-field placeholder="current password" v-model="passwordData.currentPassword" />
|
<v-text-field
|
||||||
<v-text-field placeholder="new password" v-model="passwordData.newPassword" />
|
clearable
|
||||||
<v-icon-btn variant="tonal" icon="mdi-check" @click="changePassword" />
|
bg-color="background"
|
||||||
|
placeholder="current password"
|
||||||
|
v-model="passwordData.currentPassword"
|
||||||
|
/>
|
||||||
|
<v-text-field clearable bg-color="background" placeholder="new password" v-model="passwordData.newPassword" />
|
||||||
|
<div>
|
||||||
|
<v-btn variant="tonal" prepend-icon="mdi-check" text="Update" @click="changePassword" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</v-sheet>
|
</v-sheet>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -44,20 +44,28 @@ watchEffect(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<v-sheet border class="pa-4 d-flex flex-column ga-2">
|
||||||
<div class="text-uppercase">Active sessions</div>
|
<div class="text-uppercase text-title-large">Active sessions</div>
|
||||||
<div class="mb-2">
|
|
||||||
<v-btn prepend-icon="mdi-skull" @click="revokeOtherSessions()">revoke other sessions</v-btn>
|
<v-list border class="pa-0">
|
||||||
</div>
|
|
||||||
<v-list border density="compact">
|
|
||||||
<v-list-item class="hover:bg-primary-300" v-for="session in sessions" :key="session.id">
|
<v-list-item class="hover:bg-primary-300" v-for="session in sessions" :key="session.id">
|
||||||
<div class="text-truncate">{{ session.userAgent }}</div>
|
<div class="text-truncate">{{ session.userAgent }}</div>
|
||||||
<template #append>
|
<template #append>
|
||||||
<v-icon-btn size="small" @click="revokeSession(session.token)" color="error" icon="mdi-trash-can" />
|
<v-btn
|
||||||
|
@click="revokeSession(session.token)"
|
||||||
|
variant="flat"
|
||||||
|
density="compact"
|
||||||
|
color="error"
|
||||||
|
prepend-icon="mdi-trash-can"
|
||||||
|
text="Revoke"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</v-list>
|
</v-list>
|
||||||
</div>
|
<div class="mb-2">
|
||||||
|
<v-btn prepend-icon="mdi-skull" @click="revokeOtherSessions()">Revoke other sessions</v-btn>
|
||||||
|
</div>
|
||||||
|
</v-sheet>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
<style scoped lang="scss"></style>
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { reactive } from 'vue'
|
import { reactive } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { authClient } from '@/lib/auth-client.js'
|
import { authClient } from '@/lib/auth-client.js'
|
||||||
|
import { redirectTarget } from '@/lib/redirect.ts'
|
||||||
|
|
||||||
const REMEMBERED_EMAIL_KEY = 'rememberedEmail'
|
const REMEMBERED_EMAIL_KEY = 'rememberedEmail'
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
const signInData = reactive({
|
const signInData = reactive({
|
||||||
email: localStorage.getItem(REMEMBERED_EMAIL_KEY) ?? '',
|
email: localStorage.getItem(REMEMBERED_EMAIL_KEY) ?? '',
|
||||||
@@ -25,6 +29,7 @@ const signIn = async () => {
|
|||||||
} else {
|
} else {
|
||||||
localStorage.removeItem(REMEMBERED_EMAIL_KEY)
|
localStorage.removeItem(REMEMBERED_EMAIL_KEY)
|
||||||
}
|
}
|
||||||
|
router.push(redirectTarget(route.query.redirect))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { reactive, ref, watch } from 'vue'
|
import { reactive, ref, watch } from 'vue'
|
||||||
import { authClient } from '@/lib/auth-client.js'
|
import { authClient } from '@/lib/auth-client.js'
|
||||||
import { useRouter } from 'vue-router'
|
import { redirectTarget } from '@/lib/redirect.ts'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
const REMEMBERED_EMAIL_KEY = 'rememberedEmail'
|
const REMEMBERED_EMAIL_KEY = 'rememberedEmail'
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
const otpError = ref('')
|
const otpError = ref('')
|
||||||
const signIn = reactive({
|
const signIn = reactive({
|
||||||
@@ -43,7 +45,7 @@ const checkOTP = async () => {
|
|||||||
email: signIn.email,
|
email: signIn.email,
|
||||||
otp: signIn.OTP,
|
otp: signIn.OTP,
|
||||||
})
|
})
|
||||||
if (data) router.push('/')
|
if (data) router.push(redirectTarget(route.query.redirect))
|
||||||
if (error) {
|
if (error) {
|
||||||
if (error.status === 400 && error.message) otpError.value = error.message
|
if (error.status === 400 && error.message) otpError.value = error.message
|
||||||
if (error.status === 403 && error.message) {
|
if (error.status === 403 && error.message) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const snackbarStore = useSnackbarStore()
|
|||||||
<v-snackbar-queue
|
<v-snackbar-queue
|
||||||
ref="queue"
|
ref="queue"
|
||||||
v-model="snackbarStore.snackbarQueue"
|
v-model="snackbarStore.snackbarQueue"
|
||||||
location="bottom end"
|
location="top end"
|
||||||
timeout="5000"
|
timeout="5000"
|
||||||
total-visible="5"
|
total-visible="5"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ const selected = ref<string[]>([...status])
|
|||||||
const onUpdate = (val: unknown) => {
|
const onUpdate = (val: unknown) => {
|
||||||
if (!Array.isArray(val)) return
|
if (!Array.isArray(val)) return
|
||||||
const order = statusStore.statuses?.map((el) => el.alias) ?? []
|
const order = statusStore.statuses?.map((el) => el.alias) ?? []
|
||||||
emit('changed', [...val].sort((a, b) => order.indexOf(a) - order.indexOf(b)))
|
emit(
|
||||||
|
'changed',
|
||||||
|
[...val].sort((a, b) => order.indexOf(a) - order.indexOf(b)),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -27,16 +30,7 @@ watch(
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-btn-toggle
|
<v-btn-toggle v-model="selected" multiple border density="compact" variant="flat" @update:model-value="onUpdate">
|
||||||
v-model="selected"
|
|
||||||
divided
|
|
||||||
multiple
|
|
||||||
border
|
|
||||||
size="default"
|
|
||||||
density="comfortable"
|
|
||||||
variant="text"
|
|
||||||
@update:model-value="onUpdate"
|
|
||||||
>
|
|
||||||
<v-btn slim v-for="el in statusStore.statuses" :color="el.color" :value="el.alias" :key="el.alias" rounded="0">
|
<v-btn slim v-for="el in statusStore.statuses" :color="el.color" :value="el.alias" :key="el.alias" rounded="0">
|
||||||
<div class="text-default">{{ el.name }}</div>
|
<div class="text-default">{{ el.name }}</div>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|||||||
@@ -21,18 +21,18 @@ const avatar = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<v-sheet border class="pa-4 d-flex flex-column ga-2">
|
||||||
<div v-if="user" class="d-flex ga-4">
|
<div v-if="user" class="d-flex ga-4">
|
||||||
<v-avatar size="x-large" color="info" :image="avatar" />
|
<v-avatar size="x-large" color="info" :image="avatar" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div>name: {{ user.name }}</div>
|
<div>Name: {{ user.name }}</div>
|
||||||
<div>email: {{ user.email }}</div>
|
<div>E-mail: {{ user.email }}</div>
|
||||||
<div>created at: {{ user.createdAt }}</div>
|
<div>Created At: {{ user.createdAt }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<v-skeleton-loader v-else type="avatar, list-item-three-line" />
|
<v-skeleton-loader v-else type="avatar, list-item-three-line" />
|
||||||
</div>
|
</v-sheet>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
<style scoped lang="scss"></style>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createGlobalState, useLocalStorage } from '@vueuse/core'
|
import { createGlobalState, useLocalStorage } from '@vueuse/core'
|
||||||
|
|
||||||
export const ISSUE_LIST_FIELDS = [
|
export const ISSUE_LIST_FIELDS = [
|
||||||
|
{ title: 'Type', alias: 'type' },
|
||||||
{ title: 'Title', alias: 'title' },
|
{ title: 'Title', alias: 'title' },
|
||||||
{ title: 'Content', alias: 'content' },
|
{ title: 'Content', alias: 'content' },
|
||||||
{ title: 'Status', alias: 'status' },
|
{ title: 'Status', alias: 'status' },
|
||||||
@@ -14,7 +15,7 @@ export type IssueListField = (typeof ISSUE_LIST_FIELDS)[number]['alias']
|
|||||||
export const useIssueListOptions = createGlobalState(() =>
|
export const useIssueListOptions = createGlobalState(() =>
|
||||||
useLocalStorage<Record<IssueListField, boolean>>(
|
useLocalStorage<Record<IssueListField, boolean>>(
|
||||||
'issue-list:visible-fields',
|
'issue-list:visible-fields',
|
||||||
{ title: true, content: true, status: true, priority: true, assignee: true },
|
{ title: true, content: true, status: true, priority: true, assignee: true, type: true },
|
||||||
// Fields added later stay visible for users who already have saved options
|
// Fields added later stay visible for users who already have saved options
|
||||||
{ mergeDefaults: true },
|
{ mergeDefaults: true },
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ async function goToSignOut() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const goToSettings = () => {
|
const goToSettings = () => {
|
||||||
router.push({ name: 'settings' })
|
router.push({ path: '/settings' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const fluid = computed(() => {
|
const fluid = computed(() => {
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import type { LocationQueryValue } from 'vue-router'
|
||||||
|
|
||||||
|
/** Path to return to after sign-in; only same-origin paths are accepted to avoid open redirects */
|
||||||
|
export function redirectTarget(redirect: LocationQueryValue | LocationQueryValue[] | undefined): string {
|
||||||
|
const value = Array.isArray(redirect) ? redirect[0] : redirect
|
||||||
|
return value && value.startsWith('/') && !value.startsWith('//') ? value : '/'
|
||||||
|
}
|
||||||
+1
-1
@@ -46,7 +46,7 @@ const router = createRouter({
|
|||||||
{
|
{
|
||||||
path: '/profile',
|
path: '/profile',
|
||||||
component: ProfileView,
|
component: ProfileView,
|
||||||
meta: { requiresAuth: true },
|
meta: { requiresAuth: true, breadcrumb: 'Profile' },
|
||||||
},
|
},
|
||||||
issuesRoutes,
|
issuesRoutes,
|
||||||
calendarRoutes,
|
calendarRoutes,
|
||||||
|
|||||||
@@ -9,5 +9,9 @@ export const useIssueTypeStore = defineStore('issueType', () => {
|
|||||||
issueTypes.value = await issueTypesList()
|
issueTypes.value = await issueTypesList()
|
||||||
}
|
}
|
||||||
|
|
||||||
return { issueTypes, list }
|
function reset() {
|
||||||
|
issueTypes.value = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return { issueTypes, list, reset }
|
||||||
})
|
})
|
||||||
+8
-1
@@ -120,7 +120,14 @@ export const useIssueStore = defineStore('item', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { stats, dailyStats, list, calendar, getByKey, create, update, remove, getStat, getDailyStat }
|
function reset() {
|
||||||
|
issues.value = []
|
||||||
|
stats.value = undefined
|
||||||
|
dailyStats.value = undefined
|
||||||
|
error.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
return { stats, dailyStats, list, calendar, getByKey, create, update, remove, getStat, getDailyStat, reset }
|
||||||
})
|
})
|
||||||
|
|
||||||
/** Local YYYY-MM-DD `days` days before today */
|
/** Local YYYY-MM-DD `days` days before today */
|
||||||
|
|||||||
@@ -96,6 +96,13 @@ export const useProjectStore = defineStore('project', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Clears loaded data on sign-out; the stored project id is kept and re-validated on next sign-in */
|
||||||
|
function reset() {
|
||||||
|
projects.value = []
|
||||||
|
current.value = null
|
||||||
|
error.value = null
|
||||||
|
}
|
||||||
|
|
||||||
async function restoreCurrent() {
|
async function restoreCurrent() {
|
||||||
const id = readStoredProjectId()
|
const id = readStoredProjectId()
|
||||||
if (!id) return
|
if (!id) return
|
||||||
@@ -118,5 +125,6 @@ export const useProjectStore = defineStore('project', () => {
|
|||||||
clearError,
|
clearError,
|
||||||
select,
|
select,
|
||||||
restoreCurrent,
|
restoreCurrent,
|
||||||
|
reset,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -9,5 +9,9 @@ export const useStatusStore = defineStore('counter', () => {
|
|||||||
statuses.value = await statusesList()
|
statuses.value = await statusesList()
|
||||||
}
|
}
|
||||||
|
|
||||||
return { statuses, list }
|
function reset() {
|
||||||
|
statuses.value = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return { statuses, list, reset }
|
||||||
})
|
})
|
||||||
@@ -337,21 +337,27 @@ function goToday() {
|
|||||||
<v-list-item
|
<v-list-item
|
||||||
v-for="issue in unscheduled"
|
v-for="issue in unscheduled"
|
||||||
:key="issue.id"
|
:key="issue.id"
|
||||||
class="cursor-grab"
|
class="cursor-grab ma-1 border bg-surface-light px-2 py-0"
|
||||||
:class="{ 'opacity-50': savingIds.includes(issue.id) }"
|
:class="{ 'opacity-50': savingIds.includes(issue.id) }"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
@dragstart="onDragStart($event, issue)"
|
@dragstart="onDragStart($event, issue)"
|
||||||
@dragend="onDragEnd"
|
@dragend="onDragEnd"
|
||||||
>
|
>
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<v-icon icon="mdi-drag-vertical" size="small" class="mr-1" />
|
<v-icon icon="mdi-drag-vertical" size="small" />
|
||||||
</template>
|
</template>
|
||||||
<v-list-item-title>
|
|
||||||
<router-link :to="`/issues/${issue.key}`" class="text-link font-weight-bold mr-1" draggable="false">
|
<div>
|
||||||
|
<div :title="issue.title">{{ issue.title }}</div>
|
||||||
|
<router-link
|
||||||
|
:to="`/issues/${issue.key}`"
|
||||||
|
class="text-link text-decoration-none font-weight-bold text-label-medium"
|
||||||
|
draggable="false"
|
||||||
|
>
|
||||||
{{ issue.key }}
|
{{ issue.key }}
|
||||||
</router-link>
|
</router-link>
|
||||||
<span :title="issue.title">{{ issue.title }}</span>
|
</div>
|
||||||
</v-list-item-title>
|
|
||||||
<template #append>
|
<template #append>
|
||||||
<v-btn
|
<v-btn
|
||||||
density="compact"
|
density="compact"
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const tab = ref(0)
|
|||||||
<v-tab value="projects" to="/settings/projects">Projects</v-tab>
|
<v-tab value="projects" to="/settings/projects">Projects</v-tab>
|
||||||
</v-tabs>
|
</v-tabs>
|
||||||
|
|
||||||
<div class="pa-6">
|
<div class="pa-4">
|
||||||
<router-view />
|
<router-view />
|
||||||
</div>
|
</div>
|
||||||
</v-sheet>
|
</v-sheet>
|
||||||
|
|||||||
@@ -9,19 +9,17 @@ const session = authClient.useSession()
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-sheet rounded border class="pa-4 d-flex flex-column ga-4">
|
<div class="d-flex flex-column ga-2">
|
||||||
<div class="text-headline-medium text-uppercase">profile</div>
|
<div class="text-headline-medium text-uppercase">profile</div>
|
||||||
|
|
||||||
<user-profile :user="session.data?.user" />
|
<user-profile :user="session.data?.user" />
|
||||||
|
|
||||||
<v-divider />
|
|
||||||
<password-change />
|
<password-change />
|
||||||
|
|
||||||
<v-divider />
|
|
||||||
<notification-settings />
|
<notification-settings />
|
||||||
|
|
||||||
<sessions-list />
|
<sessions-list />
|
||||||
</v-sheet>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
<style scoped lang="scss"></style>
|
||||||
Reference in new issue
Block a user