forked from vikunja/frontend
Compare commits
35 Commits
b80f82c411
...
6083301d1f
Author | SHA1 | Date |
---|---|---|
kolaente | 6083301d1f | |
renovate | 3f04571e43 | |
kolaente | d7ac2ad697 | |
kolaente | 820823b5c3 | |
kolaente | d7fb1a1e14 | |
kolaente | 7e218e03b2 | |
kolaente | d7048d589e | |
kolaente | 80230069c6 | |
kolaente | a695719128 | |
kolaente | f61723dac2 | |
kolaente | ae27502022 | |
kolaente | 8fdd3e785d | |
drone | 5d038dc79f | |
renovate | b2ef66e5df | |
renovate | af819f9539 | |
drone | 9cf0a9a89e | |
renovate | 91b70c2de4 | |
kolaente | 643a5b6d7d | |
kolaente | e6f7ddc9ce | |
kolaente | 73575302de | |
kolaente | 4ed665fbd9 | |
renovate | f30e948abd | |
drone | ea758f0c58 | |
renovate | 617a48157d | |
renovate | 419c0b2d96 | |
renovate | 449b11c1ff | |
drone | f818c207c2 | |
renovate | 4ee8f600a3 | |
konrad | 29f68747bb | |
renovate | ce201e0880 | |
renovate | bd26b81318 | |
Dominik Pschenitschni | 5afafb7c82 | |
Dominik Pschenitschni | 9de20b4c54 | |
Dominik Pschenitschni | c4d7f6fdfa | |
Dominik Pschenitschni | c20de51a3c |
|
@ -193,4 +193,15 @@ describe('List View Kanban', () => {
|
|||
cy.get('.kanban .bucket')
|
||||
.should('not.contain', task.title)
|
||||
})
|
||||
|
||||
it('Shows a button to filter the kanban board', () => {
|
||||
const data = TaskFactory.create(10, {
|
||||
list_id: 1,
|
||||
bucket_id: 1,
|
||||
})
|
||||
cy.visit('/lists/1/kanban')
|
||||
|
||||
cy.get('.list-kanban .filter-container .base-button')
|
||||
.should('exist')
|
||||
})
|
||||
})
|
|
@ -128,4 +128,24 @@ describe('Home Page Task Overview', () => {
|
|||
.last()
|
||||
.should('contain.text', newTaskTitle)
|
||||
})
|
||||
|
||||
it('Should show the cta buttons for new list when there are no tasks', () => {
|
||||
TaskFactory.truncate()
|
||||
|
||||
cy.visit('/')
|
||||
|
||||
cy.get('.home.app-content .content')
|
||||
.should('contain.text', 'You can create a new list for your new tasks:')
|
||||
.should('contain.text', 'Or import your lists and tasks from other services into Vikunja:')
|
||||
})
|
||||
|
||||
it('Should not show the cta buttons for new list when there are tasks', () => {
|
||||
seedTasks()
|
||||
|
||||
cy.visit('/')
|
||||
|
||||
cy.get('.home.app-content .content')
|
||||
.should('not.contain.text', 'You can create a new list for your new tasks:')
|
||||
.should('not.contain.text', 'Or import your lists and tasks from other services into Vikunja:')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -546,5 +546,35 @@ describe('Task', () => {
|
|||
cy.get('.bucket .task .footer .icon svg.fa-paperclip')
|
||||
.should('exist')
|
||||
})
|
||||
|
||||
it('Can check items off a checklist', () => {
|
||||
const tasks = TaskFactory.create(1, {
|
||||
id: 1,
|
||||
description: `
|
||||
This is a checklist:
|
||||
|
||||
* [ ] one item
|
||||
* [ ] another item
|
||||
* [ ] third item
|
||||
* [ ] fourth item
|
||||
* [x] and this one is already done
|
||||
`,
|
||||
})
|
||||
cy.visit(`/tasks/${tasks[0].id}`)
|
||||
|
||||
cy.get('.task-view .checklist-summary')
|
||||
.should('contain.text', '1 of 5 tasks')
|
||||
cy.get('.editor .content ul > li input[type=checkbox]')
|
||||
.eq(2)
|
||||
.click()
|
||||
|
||||
cy.get('.editor .content ul > li input[type=checkbox]')
|
||||
.eq(2)
|
||||
.should('be.checked')
|
||||
cy.get('.editor .content input[type=checkbox]')
|
||||
.should('have.length', 5)
|
||||
cy.get('.task-view .checklist-summary')
|
||||
.should('contain.text', '2 of 5 tasks')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -55,4 +55,9 @@ context('Login', () => {
|
|||
|
||||
testAndAssertFailed(fixture)
|
||||
})
|
||||
|
||||
it('Should redirect to /login when no user is logged in', () => {
|
||||
cy.visit('/')
|
||||
cy.url().should('include', '/login')
|
||||
})
|
||||
})
|
||||
|
|
24
package.json
24
package.json
|
@ -24,8 +24,8 @@
|
|||
"@fortawesome/vue-fontawesome": "3.0.1",
|
||||
"@github/hotkey": "2.0.1",
|
||||
"@kyvg/vue3-notification": "2.4.1",
|
||||
"@sentry/tracing": "7.15.0",
|
||||
"@sentry/vue": "7.15.0",
|
||||
"@sentry/tracing": "7.16.0",
|
||||
"@sentry/vue": "7.16.0",
|
||||
"@types/is-touch-device": "1.0.0",
|
||||
"@types/lodash.clonedeep": "4.5.7",
|
||||
"@types/sortablejs": "1.15.0",
|
||||
|
@ -56,7 +56,7 @@
|
|||
"vue": "3.2.41",
|
||||
"vue-advanced-cropper": "2.8.6",
|
||||
"vue-drag-resize": "2.0.3",
|
||||
"vue-flatpickr-component": "9.0.8",
|
||||
"vue-flatpickr-component": "10.0.0",
|
||||
"vue-i18n": "9.2.2",
|
||||
"vue-router": "4.1.5",
|
||||
"workbox-precaching": "6.5.4",
|
||||
|
@ -72,10 +72,10 @@
|
|||
"@types/flexsearch": "0.7.3",
|
||||
"@types/lodash.debounce": "4.0.7",
|
||||
"@types/marked": "4.0.7",
|
||||
"@types/node": "16.11.66",
|
||||
"@types/postcss-preset-env": "^7.7.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.40.0",
|
||||
"@typescript-eslint/parser": "5.40.0",
|
||||
"@types/node": "16.11.68",
|
||||
"@types/postcss-preset-env": "7.7.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.40.1",
|
||||
"@typescript-eslint/parser": "5.40.1",
|
||||
"@vitejs/plugin-legacy": "2.2.0",
|
||||
"@vitejs/plugin-vue": "3.1.2",
|
||||
"@vue/eslint-config-typescript": "11.0.2",
|
||||
|
@ -85,15 +85,15 @@
|
|||
"browserslist": "4.21.4",
|
||||
"caniuse-lite": "1.0.30001420",
|
||||
"cypress": "10.10.0",
|
||||
"esbuild": "0.15.11",
|
||||
"eslint": "8.25.0",
|
||||
"esbuild": "0.15.12",
|
||||
"eslint": "8.26.0",
|
||||
"eslint-plugin-vue": "9.6.0",
|
||||
"express": "4.18.2",
|
||||
"happy-dom": "7.5.12",
|
||||
"netlify-cli": "12.0.9",
|
||||
"postcss": "8.4.18",
|
||||
"postcss-preset-env": "7.8.2",
|
||||
"rollup": "3.2.2",
|
||||
"rollup": "3.2.3",
|
||||
"rollup-plugin-visualizer": "5.8.3",
|
||||
"sass": "1.55.0",
|
||||
"typescript": "4.8.4",
|
||||
|
@ -101,10 +101,10 @@
|
|||
"vite-plugin-pwa": "0.13.1",
|
||||
"vite-svg-loader": "3.6.0",
|
||||
"vitest": "0.24.3",
|
||||
"vue-tsc": "1.0.8",
|
||||
"vue-tsc": "1.0.9",
|
||||
"wait-on": "6.0.1",
|
||||
"workbox-cli": "6.5.4"
|
||||
},
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"packageManager": "pnpm@7.13.5"
|
||||
"packageManager": "pnpm@7.14.0"
|
||||
}
|
||||
|
|
454
pnpm-lock.yaml
454
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -44,8 +44,8 @@
|
|||
variant="secondary"
|
||||
:shadow="false"
|
||||
>
|
||||
<img :src="userAvatar" alt="" class="avatar" width="40" height="40"/>
|
||||
<span class="username">{{ userInfo.name !== '' ? userInfo.name : userInfo.username }}</span>
|
||||
<img :src="authStore.avatarUrl" alt="" class="avatar" width="40" height="40"/>
|
||||
<span class="username">{{ authStore.userDisplayName }}</span>
|
||||
<span class="icon is-small">
|
||||
<icon icon="chevron-down"/>
|
||||
</span>
|
||||
|
@ -80,7 +80,7 @@
|
|||
{{ $t('about.title') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
@click="logout()"
|
||||
@click="authStore.logout()"
|
||||
>
|
||||
{{ $t('user.auth.logout') }}
|
||||
</dropdown-item>
|
||||
|
@ -117,8 +117,6 @@ const canWriteCurrentList = computed(() => baseStore.currentList.maxRight > Righ
|
|||
const menuActive = computed(() => baseStore.menuActive)
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const userInfo = computed(() => authStore.info)
|
||||
const userAvatar = computed(() => authStore.avatarUrl)
|
||||
|
||||
const configStore = useConfigStore()
|
||||
const imprintUrl = computed(() => configStore.legal.imprintUrl)
|
||||
|
@ -136,10 +134,6 @@ onMounted(async () => {
|
|||
listTitle.value.style.setProperty('--nav-username-width', `${usernameWidth}px`)
|
||||
})
|
||||
|
||||
function logout() {
|
||||
authStore.logout()
|
||||
}
|
||||
|
||||
function openQuickActions() {
|
||||
baseStore.setQuickActionsActive(true)
|
||||
}
|
||||
|
|
|
@ -285,9 +285,9 @@ function handleCheckboxClick(e: Event) {
|
|||
console.debug('no index found')
|
||||
return
|
||||
}
|
||||
console.debug(index, text.value.slice(index, 9))
|
||||
|
||||
const listPrefix = text.value.slice(index, 1)
|
||||
const listPrefix = text.value.substring(index, index + 1)
|
||||
|
||||
console.debug({index, listPrefix, checked, text: text.value})
|
||||
|
||||
text.value = replaceAt(text.value, index, `${listPrefix} ${checked ? '[x]' : '[ ]'} `)
|
||||
bubble()
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
:loading="loading"
|
||||
>
|
||||
<div class="p-4">
|
||||
<slot />
|
||||
<slot/>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
|
@ -30,10 +30,12 @@
|
|||
{{ $t('misc.cancel') }}
|
||||
</x-button>
|
||||
<x-button
|
||||
v-if="hasPrimaryAction"
|
||||
variant="primary"
|
||||
@click.prevent.stop="primary()"
|
||||
:icon="primaryIcon"
|
||||
:disabled="primaryDisabled || loading"
|
||||
class="ml-2"
|
||||
>
|
||||
{{ primaryLabel || $t('misc.create') }}
|
||||
</x-button>
|
||||
|
@ -60,6 +62,10 @@ defineProps({
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
hasPrimaryAction: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
tertiary: {
|
||||
type: String,
|
||||
default: '',
|
||||
|
|
|
@ -61,7 +61,7 @@ const route = useRoute()
|
|||
|
||||
const baseStore = useBaseStore()
|
||||
|
||||
const ready = ref(false)
|
||||
const ready = computed(() => baseStore.ready)
|
||||
const online = useOnline()
|
||||
|
||||
const error = ref('')
|
||||
|
@ -70,11 +70,11 @@ const showLoading = computed(() => !ready.value && error.value === '')
|
|||
async function load() {
|
||||
try {
|
||||
await baseStore.loadApp()
|
||||
const redirectTo = getAuthForRoute(route)
|
||||
baseStore.setReady(true)
|
||||
const redirectTo = await getAuthForRoute(route)
|
||||
if (typeof redirectTo !== 'undefined') {
|
||||
await router.push(redirectTo)
|
||||
}
|
||||
ready.value = true
|
||||
} catch (e: unknown) {
|
||||
error.value = String(e)
|
||||
}
|
||||
|
|
|
@ -1,30 +1,25 @@
|
|||
<template>
|
||||
<div
|
||||
tabindex="-1"
|
||||
@focus="focus"
|
||||
<Multiselect
|
||||
:loading="listUserService.loading"
|
||||
:placeholder="$t('task.assignee.placeholder')"
|
||||
:multiple="true"
|
||||
@search="findUser"
|
||||
:search-results="foundUsers"
|
||||
@select="addAssignee"
|
||||
label="name"
|
||||
:select-placeholder="$t('task.assignee.selectPlaceholder')"
|
||||
v-model="assignees"
|
||||
ref="userSearchInputRef"
|
||||
>
|
||||
<Multiselect
|
||||
:loading="listUserService.loading"
|
||||
:placeholder="$t('task.assignee.placeholder')"
|
||||
:multiple="true"
|
||||
@search="findUser"
|
||||
:search-results="foundUsers"
|
||||
@select="addAssignee"
|
||||
label="name"
|
||||
:select-placeholder="$t('task.assignee.selectPlaceholder')"
|
||||
v-model="assignees"
|
||||
ref="multiselect"
|
||||
>
|
||||
<template #tag="{item: user}">
|
||||
<span class="assignee">
|
||||
<user :avatar-size="32" :show-username="false" :user="user"/>
|
||||
<BaseButton @click="removeAssignee(user)" class="remove-assignee" v-if="!disabled">
|
||||
<icon icon="times"/>
|
||||
</BaseButton>
|
||||
</span>
|
||||
</template>
|
||||
</Multiselect>
|
||||
</div>
|
||||
<template #tag="{item: user}">
|
||||
<span class="assignee">
|
||||
<user :avatar-size="32" :show-username="false" :user="user"/>
|
||||
<BaseButton @click="removeAssignee(user)" class="remove-assignee" v-if="!disabled">
|
||||
<icon icon="times"/>
|
||||
</BaseButton>
|
||||
</span>
|
||||
</template>
|
||||
</Multiselect>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
@ -41,6 +36,7 @@ import {success} from '@/message'
|
|||
import {useTaskStore} from '@/stores/tasks'
|
||||
|
||||
import type {IUser} from '@/modelTypes/IUser'
|
||||
import {getDisplayName} from '@/models/user'
|
||||
|
||||
const props = defineProps({
|
||||
taskId: {
|
||||
|
@ -65,7 +61,7 @@ const taskStore = useTaskStore()
|
|||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const listUserService = shallowReactive(new ListUserService())
|
||||
const foundUsers = ref([])
|
||||
const foundUsers = ref<IUser[]>([])
|
||||
const assignees = ref<IUser[]>([])
|
||||
let isAdding = false
|
||||
|
||||
|
@ -114,13 +110,14 @@ async function findUser(query: string) {
|
|||
return
|
||||
}
|
||||
|
||||
const response = await listUserService.getAll({listId: props.listId}, {s: query})
|
||||
const response = await listUserService.getAll({listId: props.listId}, {s: query}) as IUser[]
|
||||
|
||||
// Filter the results to not include users who are already assigned
|
||||
foundUsers.value = response.filter(({id}) => !includesById(assignees.value, id))
|
||||
foundUsers.value = response
|
||||
.filter(({id}) => !includesById(assignees.value, id))
|
||||
.map(u => {
|
||||
// Users may not have a display name set, so we fall back on the username in that case
|
||||
u.name = u.name === '' ? u.username : u.name
|
||||
u.name = getDisplayName(u)
|
||||
return u
|
||||
})
|
||||
}
|
||||
|
@ -128,12 +125,6 @@ async function findUser(query: string) {
|
|||
function clearAllFoundUsers() {
|
||||
foundUsers.value = []
|
||||
}
|
||||
|
||||
const multiselect = ref()
|
||||
|
||||
function focus() {
|
||||
multiselect.value.focus()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
import {describe, it, expect} from 'vitest'
|
||||
import {hourToSalutation} from './useDateTimeSalutation'
|
||||
|
||||
const dateWithHour = (hours: number): Date => {
|
||||
const date = new Date()
|
||||
date.setHours(hours)
|
||||
return date
|
||||
}
|
||||
|
||||
describe('Salutation', () => {
|
||||
it('shows the right salutation in the night', () => {
|
||||
const salutation = hourToSalutation(dateWithHour(4))
|
||||
expect(salutation).toBe('home.welcomeNight')
|
||||
})
|
||||
it('shows the right salutation in the morning', () => {
|
||||
const salutation = hourToSalutation(dateWithHour(8))
|
||||
expect(salutation).toBe('home.welcomeMorning')
|
||||
})
|
||||
it('shows the right salutation in the day', () => {
|
||||
const salutation = hourToSalutation(dateWithHour(13))
|
||||
expect(salutation).toBe('home.welcomeDay')
|
||||
})
|
||||
it('shows the right salutation in the night', () => {
|
||||
const salutation = hourToSalutation(dateWithHour(20))
|
||||
expect(salutation).toBe('home.welcomeEvening')
|
||||
})
|
||||
it('shows the right salutation in the night again', () => {
|
||||
const salutation = hourToSalutation(dateWithHour(23))
|
||||
expect(salutation).toBe('home.welcomeNight')
|
||||
})
|
||||
})
|
|
@ -1,31 +0,0 @@
|
|||
import {computed} from 'vue'
|
||||
import {useNow} from '@vueuse/core'
|
||||
|
||||
const TRANSLATION_KEY_PREFIX = 'home.welcome'
|
||||
|
||||
export function hourToSalutation(now: Date) {
|
||||
const hours = now.getHours()
|
||||
|
||||
if (hours < 5) {
|
||||
return `${TRANSLATION_KEY_PREFIX}Night`
|
||||
}
|
||||
|
||||
if (hours < 11) {
|
||||
return `${TRANSLATION_KEY_PREFIX}Morning`
|
||||
}
|
||||
|
||||
if (hours < 18) {
|
||||
return `${TRANSLATION_KEY_PREFIX}Day`
|
||||
}
|
||||
|
||||
if (hours < 23) {
|
||||
return `${TRANSLATION_KEY_PREFIX}Evening`
|
||||
}
|
||||
|
||||
return `${TRANSLATION_KEY_PREFIX}Night`
|
||||
}
|
||||
|
||||
export function useDateTimeSalutation() {
|
||||
const now = useNow()
|
||||
return computed(() => hourToSalutation(now.value))
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import {computed} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {useNow} from '@vueuse/core'
|
||||
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {hourToDaytime} from '@/helpers/hourToDaytime'
|
||||
|
||||
export type Daytime = 'night' | 'morning' | 'day' | 'evening'
|
||||
|
||||
export function useDaytimeSalutation() {
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
const now = useNow()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const name = computed(() => authStore.userDisplayName)
|
||||
const daytime = computed(() => hourToDaytime(now.value))
|
||||
|
||||
const salutations = {
|
||||
'night': () => t('home.welcomeNight', {username: name.value}),
|
||||
'morning': () => t('home.welcomeMorning', {username: name.value}),
|
||||
'day': () => t('home.welcomeDay', {username: name.value}),
|
||||
'evening': () => t('home.welcomeEvening', {username: name.value}),
|
||||
} as Record<Daytime, () => string>
|
||||
|
||||
return computed(() => name.value ? salutations[daytime.value]() : undefined)
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import {useRouter} from 'vue-router'
|
||||
import {getLastVisited, clearLastVisited} from '@/helpers/saveLastVisited'
|
||||
|
||||
export function useRedirectToLastVisited() {
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
function redirectIfSaved() {
|
||||
const last = getLastVisited()
|
||||
if (last !== null) {
|
||||
router.push({
|
||||
name: last.name,
|
||||
params: last.params,
|
||||
query: last.query,
|
||||
})
|
||||
clearLastVisited()
|
||||
return
|
||||
}
|
||||
|
||||
router.push({name: 'home'})
|
||||
}
|
||||
|
||||
return {
|
||||
redirectIfSaved,
|
||||
}
|
||||
}
|
|
@ -16,7 +16,7 @@ export function useRenewTokenOnFocus() {
|
|||
authStore.renewToken()
|
||||
|
||||
// Check if the token is still valid if the window gets focus again to maybe renew it
|
||||
useEventListener('focus', () => {
|
||||
useEventListener('focus', async () => {
|
||||
if (!authenticated.value) {
|
||||
return
|
||||
}
|
||||
|
@ -26,8 +26,8 @@ export function useRenewTokenOnFocus() {
|
|||
// If the token expiry is negative, it is already expired and we have no choice but to redirect
|
||||
// the user to the login page
|
||||
if (expiresIn < 0) {
|
||||
authStore.checkAuth()
|
||||
router.push({name: 'user.login'})
|
||||
await authStore.checkAuth()
|
||||
await router.push({name: 'user.login'})
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
import {describe, it, expect} from 'vitest'
|
||||
import {hourToDaytime} from "./hourToDaytime"
|
||||
|
||||
function dateWithHour(hours: number): Date {
|
||||
const newDate = new Date()
|
||||
newDate.setHours(hours, 0, 0,0 )
|
||||
return newDate
|
||||
}
|
||||
|
||||
describe('Salutation', () => {
|
||||
it('shows the right salutation in the night', () => {
|
||||
const salutation = hourToDaytime(dateWithHour(4))
|
||||
expect(salutation).toBe('night')
|
||||
})
|
||||
it('shows the right salutation in the morning', () => {
|
||||
const salutation = hourToDaytime(dateWithHour(8))
|
||||
expect(salutation).toBe('morning')
|
||||
})
|
||||
it('shows the right salutation in the day', () => {
|
||||
const salutation = hourToDaytime(dateWithHour(13))
|
||||
expect(salutation).toBe('day')
|
||||
})
|
||||
it('shows the right salutation in the night', () => {
|
||||
const salutation = hourToDaytime(dateWithHour(20))
|
||||
expect(salutation).toBe('evening')
|
||||
})
|
||||
it('shows the right salutation in the night again', () => {
|
||||
const salutation = hourToDaytime(dateWithHour(23))
|
||||
expect(salutation).toBe('night')
|
||||
})
|
||||
})
|
|
@ -0,0 +1,14 @@
|
|||
import type { Daytime } from '@/composables/useDaytimeSalutation'
|
||||
|
||||
export function hourToDaytime(now: Date): Daytime {
|
||||
const hours = now.getHours()
|
||||
|
||||
const daytimeMap = {
|
||||
night: hours < 5 || hours > 23,
|
||||
morning: hours < 11,
|
||||
day: hours < 18,
|
||||
evening: hours < 23,
|
||||
} as Record<Daytime, boolean>
|
||||
|
||||
return (Object.keys(daytimeMap) as Daytime[]).find((daytime) => daytimeMap[daytime]) || 'night'
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
const LAST_VISITED_KEY = 'lastVisited'
|
||||
|
||||
export const saveLastVisited = (name: string, params: object) => {
|
||||
localStorage.setItem(LAST_VISITED_KEY, JSON.stringify({name, params}))
|
||||
export const saveLastVisited = (name: string, params: object, query: object) => {
|
||||
localStorage.setItem(LAST_VISITED_KEY, JSON.stringify({name, params, query}))
|
||||
}
|
||||
|
||||
export const getLastVisited = () => {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"home": {
|
||||
"welcomeNight": "Good Night {username}",
|
||||
"welcomeMorning": "Good Morning {username}",
|
||||
"welcomeDay": "Hi {username}",
|
||||
"welcomeEvening": "Good Evening {username}",
|
||||
"welcomeNight": "Good Night {username}!",
|
||||
"welcomeMorning": "Good Morning {username}!",
|
||||
"welcomeDay": "Hi {username}!",
|
||||
"welcomeEvening": "Good Evening {username}!",
|
||||
"lastViewed": "Last viewed",
|
||||
"list": {
|
||||
"newText": "You can create a new list for your new tasks:",
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"home": {
|
||||
"welcomeNight": "Dobrou noc {username}",
|
||||
"welcomeMorning": "Dobré ráno {username}",
|
||||
"welcomeDay": "Ahoj {username}",
|
||||
"welcomeEvening": "Dobrý večer {username}",
|
||||
"welcomeNight": "Dobrou noc {username}!",
|
||||
"welcomeMorning": "Dobré ráno {username}!",
|
||||
"welcomeDay": "Ahoj {username}!",
|
||||
"welcomeEvening": "Dobrý večer {username}!",
|
||||
"lastViewed": "Naposledy zobrazeno",
|
||||
"list": {
|
||||
"newText": "Můžete vytvořit nový seznam pro své nové úkoly:",
|
||||
|
@ -77,7 +77,7 @@
|
|||
"newName": "Nové jméno",
|
||||
"savedSuccess": "Nastavení bylo úspěšně aktualizováno.",
|
||||
"emailReminders": "Posílat mi připomenutí pro úkoly e-mailem",
|
||||
"overdueReminders": "Send me a summary of my undone overdue tasks every day",
|
||||
"overdueReminders": "Pošlete mi každý den shrnutí mých zpožděných úkolů",
|
||||
"discoverableByName": "Nechat ostatní uživatele mě najít podle jména",
|
||||
"discoverableByEmail": "Nechat ostatní uživatele mě najít podle e-mailu",
|
||||
"playSoundWhenDone": "Přehrát zvuk při označení úkolů jako hotovo",
|
||||
|
@ -87,7 +87,7 @@
|
|||
"language": "Jazyk",
|
||||
"defaultList": "Výchozí seznam",
|
||||
"timezone": "Časové pásmo",
|
||||
"overdueTasksRemindersTime": "Overdue tasks reminder email time"
|
||||
"overdueTasksRemindersTime": "Čas odeslání emailu o zpožděných úkolech"
|
||||
},
|
||||
"totp": {
|
||||
"title": "Dvoufaktorové ověření",
|
||||
|
@ -180,7 +180,7 @@
|
|||
"search": "Začni psát pro vyhledání seznamu…",
|
||||
"searchSelect": "Klikněte nebo stiskněte Enter pro výběr tohoto seznamu",
|
||||
"shared": "Sdílené seznamy",
|
||||
"noDescriptionAvailable": "No list description is available.",
|
||||
"noDescriptionAvailable": "Popis seznamu není k dispozici.",
|
||||
"create": {
|
||||
"header": "Nový seznam",
|
||||
"titlePlaceholder": "Název seznamu přijde sem…",
|
||||
|
@ -252,8 +252,8 @@
|
|||
"removeText": "Jste si jisti, že chcete odstranit tento sdílený odkaz? K tomuto seznamu již nebude možné přistupovat s tímto sdíleným odkazem. Tuto akci nelze vrátit zpět!",
|
||||
"createSuccess": "Sdílený odkaz byl úspěšně vytvořen.",
|
||||
"deleteSuccess": "Sdílený odkaz byl úspěšně smazán",
|
||||
"view": "View",
|
||||
"sharedBy": "Shared by {0}"
|
||||
"view": "Zobrazit",
|
||||
"sharedBy": "Sdíleno {0}"
|
||||
},
|
||||
"userTeam": {
|
||||
"typeUser": "uživatel | uživatelé",
|
||||
|
@ -297,23 +297,23 @@
|
|||
"title": "Kanban",
|
||||
"limit": "Limit: {limit}",
|
||||
"noLimit": "Nenastaveno",
|
||||
"doneBucket": "Kbelík \"Hotovo\"",
|
||||
"doneBucketHint": "Všechny úkoly přesunuté do tohoto kbelíku budou automaticky označeny jako dokončené.",
|
||||
"doneBucketHintExtended": "Všechny úkoly přesunuté do kbelíku \"Hotovo\" budou označeny jako dokončené automaticky. Všechny úkoly označené jako dokončené odjinud sem budou přesunuty také.",
|
||||
"doneBucketSavedSuccess": "Kbelík \"Hotovo\" byl úspěšně uložen.",
|
||||
"deleteLast": "Nelze odstranit poslední kbelík.",
|
||||
"doneBucket": "Sloupec \"Hotovo\"",
|
||||
"doneBucketHint": "Všechny úkoly přesunuté do tohoto sloupce budou automaticky označeny jako dokončené.",
|
||||
"doneBucketHintExtended": "Všechny úkoly přesunuté do sloupce \"Hotovo\" budou označeny jako dokončené automaticky. Všechny úkoly označené jako dokončené jinde sem budou přesunuty také.",
|
||||
"doneBucketSavedSuccess": "Sloupec \"Hotovo\" byl úspěšně uložen.",
|
||||
"deleteLast": "Nelze odstranit poslední sloupec.",
|
||||
"addTaskPlaceholder": "Zadejte nový název úkolu…",
|
||||
"addTask": "Přidat úkol",
|
||||
"addAnotherTask": "Přidat další úkol",
|
||||
"addBucket": "Vytvořit nový kbelík",
|
||||
"addBucketPlaceholder": "Zadejte název nového kbelíku…",
|
||||
"deleteHeaderBucket": "Smazat kbelík",
|
||||
"deleteBucketText1": "Opravdu chcete smazat tento kbelík?",
|
||||
"deleteBucketText2": "Toto neodstraní žádné úkoly, ale přesune je do výchozího kbelíku.",
|
||||
"deleteBucketSuccess": "Kbelík byl úspěšně smazán.",
|
||||
"bucketTitleSavedSuccess": "Název kbelíku byl úspěšně uložen.",
|
||||
"bucketLimitSavedSuccess": "Limit kbelíku byl úspěšně uložen.",
|
||||
"collapse": "Sbalit tento kbelík"
|
||||
"addBucket": "Vytvořit nový sloupec",
|
||||
"addBucketPlaceholder": "Zadejte název nového sloupce…",
|
||||
"deleteHeaderBucket": "Smazat sloupec",
|
||||
"deleteBucketText1": "Opravdu chcete smazat tento sloupec?",
|
||||
"deleteBucketText2": "Toto nesmaže žádné úkoly, ale přesune je do výchozího sloupce.",
|
||||
"deleteBucketSuccess": "Sloupec byl úspěšně smazán.",
|
||||
"bucketTitleSavedSuccess": "Název sloupce byl úspěšně uložen.",
|
||||
"bucketLimitSavedSuccess": "Limit sloupce byl úspěšně uložen.",
|
||||
"collapse": "Sbalit tento sloupec"
|
||||
},
|
||||
"pseudo": {
|
||||
"favorites": {
|
||||
|
@ -672,23 +672,23 @@
|
|||
"updated": "Aktualizováno"
|
||||
},
|
||||
"subscription": {
|
||||
"subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
|
||||
"subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
|
||||
"subscribedTaskThroughParentList": "You can't unsubscribe here because you are subscribed to this task through its list.",
|
||||
"subscribedNamespace": "You are currently subscribed to this namespace and will receive notifications for changes.",
|
||||
"notSubscribedNamespace": "You are not subscribed to this namespace and won't receive notifications for changes.",
|
||||
"subscribedList": "You are currently subscribed to this list and will receive notifications for changes.",
|
||||
"notSubscribedList": "You are not subscribed to this list and won't receive notifications for changes.",
|
||||
"subscribedTask": "You are currently subscribed to this task and will receive notifications for changes.",
|
||||
"notSubscribedTask": "You are not subscribed to this task and won't receive notifications for changes.",
|
||||
"subscribedListThroughParentNamespace": "Zde se nemůžete odhlásit, protože jste přihlášeni k odběru tohoto seznamu prostřednictvím jeho prostoru.",
|
||||
"subscribedTaskThroughParentNamespace": "Zde se nemůžete odhlásit, protože jste přihlášeni k odběru tohoto úkolu prostřednictvím jeho prostoru.",
|
||||
"subscribedTaskThroughParentList": "Zde se nemůžete odhlásit, protože jste přihlášeni k odběru tohoto úkolu prostřednictvím jeho seznamu.",
|
||||
"subscribedNamespace": "Nyní jste přihlášeni k odběru tohoto prostoru a budete dostávat oznámení o změnách.",
|
||||
"notSubscribedNamespace": "Nejste přihlášeni k odběru tohoto prostoru, takže nebudete dostávat upozornění na změny.",
|
||||
"subscribedList": "Nyní jste přihlášeni k odběru tohoto seznamu a budete dostávat oznámení o změnách.",
|
||||
"notSubscribedList": "Nejste přihlášeni k odběru tohoto seznamu, takže nebudete dostávat upozornění na změny.",
|
||||
"subscribedTask": "Nyní jste přihlášeni k odběru tohoto úkolu a budete dostávat oznámení o změnách.",
|
||||
"notSubscribedTask": "Nejste přihlášeni k odběru tohoto úkolu, takže nebudete dostávat upozornění na změny.",
|
||||
"subscribe": "Odebírat",
|
||||
"unsubscribe": "Odhlásit odběr",
|
||||
"subscribeSuccessNamespace": "You are now subscribed to this namespace",
|
||||
"unsubscribeSuccessNamespace": "You are now unsubscribed to this namespace",
|
||||
"subscribeSuccessList": "You are now subscribed to this list",
|
||||
"unsubscribeSuccessList": "You are now unsubscribed to this list",
|
||||
"subscribeSuccessTask": "You are now subscribed to this task",
|
||||
"unsubscribeSuccessTask": "You are now unsubscribed to this task"
|
||||
"subscribeSuccessNamespace": "Nyní jste přihlášeni k tomuto prostoru",
|
||||
"unsubscribeSuccessNamespace": "Nyní jste odhlášeni od tohoto prostoru",
|
||||
"subscribeSuccessList": "Nyní jste přihlášeni k tomuto seznamu",
|
||||
"unsubscribeSuccessList": "Nyní jste odhlášeni od tohoto seznamu",
|
||||
"subscribeSuccessTask": "Nyní jste přihlášeni k tomuto úkolu",
|
||||
"unsubscribeSuccessTask": "Nyní jste odhlášeni od tohoto úkolu"
|
||||
},
|
||||
"attachment": {
|
||||
"title": "Přílohy",
|
||||
|
@ -701,10 +701,10 @@
|
|||
"deleteText1": "Opravdu chcete odstranit přílohu {filename}?",
|
||||
"copyUrl": "Kopírovat URL",
|
||||
"copyUrlTooltip": "Kopírovat URL této přílohy pro použití v textu",
|
||||
"setAsCover": "Make cover",
|
||||
"unsetAsCover": "Remove cover",
|
||||
"successfullyChangedCoverImage": "The cover image was successfully changed.",
|
||||
"usedAsCover": "Cover image"
|
||||
"setAsCover": "Použij titulní obrázek",
|
||||
"unsetAsCover": "Odstraň titulní obrázek",
|
||||
"successfullyChangedCoverImage": "Titulní obrázek byl úspěšně změněn.",
|
||||
"usedAsCover": "Titulní obrázek"
|
||||
},
|
||||
"comment": {
|
||||
"title": "Komentáře",
|
||||
|
@ -715,7 +715,7 @@
|
|||
"comment": "Komentář",
|
||||
"delete": "Smazat tento komentář",
|
||||
"deleteText1": "Opravdu chcete smazat tento komentář?",
|
||||
"deleteSuccess": "The comment was deleted successfully.",
|
||||
"deleteSuccess": "Komentář byl úspěšně odstraněn.",
|
||||
"addedSuccess": "Komentář byl úspěšně přidán."
|
||||
},
|
||||
"deferDueDate": {
|
||||
|
@ -742,16 +742,16 @@
|
|||
"removeSuccess": "Štítek byl úspěšně odstraněn.",
|
||||
"addCreateSuccess": "Štítek byl úspěšně vytvořen a přidán.",
|
||||
"delete": {
|
||||
"header": "Delete this label",
|
||||
"text1": "Are you sure you want to delete this label?",
|
||||
"text2": "This will remove it from all tasks and cannot be restored."
|
||||
"header": "Smazat tento štítek",
|
||||
"text1": "Opravdu chcete smazat tento štítek?",
|
||||
"text2": "Odebere štítek ze všech úkolů a nelze to vrátit zpět."
|
||||
}
|
||||
},
|
||||
"priority": {
|
||||
"unset": "Zrušit nastavení",
|
||||
"low": "Nízká",
|
||||
"medium": "Střední",
|
||||
"high": "High",
|
||||
"high": "Vysoká",
|
||||
"urgent": "Naléhavé",
|
||||
"doNow": "UDĚLEJ TO HNED"
|
||||
},
|
||||
|
@ -795,7 +795,7 @@
|
|||
"weeks": "Týdny",
|
||||
"months": "Měsíce",
|
||||
"years": "Roky",
|
||||
"invalidAmount": "Please enter more than 0."
|
||||
"invalidAmount": "Zadejte prosím více než 0."
|
||||
},
|
||||
"quickAddMagic": {
|
||||
"hint": "Můžeš použít Kouzelné rychlé přidání",
|
||||
|
@ -805,15 +805,15 @@
|
|||
"multiple": "Toto můžete použít několikrát.",
|
||||
"label1": "Chcete-li přidat štítek, jednoduše napište před název štítku {prefix}.",
|
||||
"label2": "Vikunja nejprve zkontroluje, zda štítek již existuje a vytvoří jej, pokud ne.",
|
||||
"label3": "To use spaces, simply add a \" or ' around the label name.",
|
||||
"label3": "Chcete-li použít mezery, uzavřete název štítku do \" nebo '.",
|
||||
"label4": "Například: {prefix}\"Štítek s mezerami\".",
|
||||
"priority1": "Chcete-li nastavit prioritu úkolu, přidejte číslo 1-5, s prefixem {prefix}.",
|
||||
"priority2": "Čím vyšší číslo, tím vyšší priorita.",
|
||||
"assignees": "Chcete-li přímo přiřadit úkol k uživateli, přidejte k úkolu jejich uživatelské jméno s prefixem {prefix}.",
|
||||
"list1": "Chcete-li nastavit seznam, ve kterém se má úkol zobrazit, zadejte jeho název s předponou {prefix}.",
|
||||
"list2": "Toto vrátí chybu, pokud seznam neexistuje.",
|
||||
"list3": "To use spaces, simply add a \" or ' around the list name.",
|
||||
"list4": "For example: {prefix}\"List with spaces\".",
|
||||
"list3": "Chcete-li použít mezery, uzavřete název seznamu do \" nebo '.",
|
||||
"list4": "Například: {prefix}\"Štítek s mezerami\".",
|
||||
"dateAndTime": "Datum a čas",
|
||||
"date": "Jakékoliv datum bude použito jako datum dokončení nového úkolu. Můžete použít data v kterémkoli z těchto formátů:",
|
||||
"dateWeekday": "každý pracovní den použije další datum s tímto datem",
|
||||
|
@ -855,10 +855,10 @@
|
|||
"success": "Uživatel byl úspěšně odstraněn z týmu."
|
||||
},
|
||||
"leave": {
|
||||
"title": "Leave team",
|
||||
"text1": "Are you sure you want to leave this team?",
|
||||
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
|
||||
"success": "You have successfully left the team."
|
||||
"title": "Opustit tým",
|
||||
"text1": "Opravdu chcete opustit tento tým?",
|
||||
"text2": "Ztratíte přístup ke všem seznamům a prostorům, k nimž má tento tým přístup. Pokud změníte názor, budete potřebovat správce týmu, aby vás znovu přidal.",
|
||||
"success": "Úspěšně jste opustili tým."
|
||||
}
|
||||
},
|
||||
"attributes": {
|
||||
|
@ -890,8 +890,8 @@
|
|||
"related": "Upravit související úkoly tohoto úkolu",
|
||||
"color": "Změnit barvu tohoto úkolu",
|
||||
"move": "Přesunout tento úkol do jiného seznamu",
|
||||
"reminder": "Manage reminders of this task",
|
||||
"description": "Toggle editing of the task description"
|
||||
"reminder": "Spravovat připomenutí této úlohy",
|
||||
"description": "Přepnout úpravy popisu úkolu"
|
||||
},
|
||||
"list": {
|
||||
"title": "Zobrazení seznamů",
|
||||
|
@ -963,7 +963,7 @@
|
|||
}
|
||||
},
|
||||
"date": {
|
||||
"locale": "cs-CZ",
|
||||
"locale": "cs",
|
||||
"altFormatLong": "j M Y H:i",
|
||||
"altFormatShort": "j M Y"
|
||||
},
|
||||
|
@ -1033,11 +1033,11 @@
|
|||
"8002": "Štítek neexistuje.",
|
||||
"8003": "K tomuto štítku nemáte přístup.",
|
||||
"9001": "Právo je neplatné.",
|
||||
"10001": "Kbelík neexistuje.",
|
||||
"10002": "Kbelík nepatří do tohoto seznamu.",
|
||||
"10003": "Poslední kbelík v seznamu nelze odstranit.",
|
||||
"10004": "Nemůžete přidat úkol do tohoto kbelíku, protože již překročil limit úkolů, které do něj můžete uložit.",
|
||||
"10005": "Na seznam může být pouze jeden kbelík \"Hotovo\".",
|
||||
"10001": "Sloupec neexistuje.",
|
||||
"10002": "Sloupec nepatří do tohoto seznamu.",
|
||||
"10003": "Poslední sloupec v seznamu nelze odstranit.",
|
||||
"10004": "Nemůžete přidat úkol do tohoto sloupce, protože již překročil limit úkolů, které do něj můžete uložit.",
|
||||
"10005": "Pro seznam může být pouze jeden sloupec \"Hotovo\".",
|
||||
"11001": "Uložený filtr neexistuje.",
|
||||
"11002": "Uložené filtry nejsou k dispozici pro sdílení odkazů.",
|
||||
"12001": "Typ předplatného je neplatný.",
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"home": {
|
||||
"welcomeNight": "Gute Nacht, {username}",
|
||||
"welcomeMorning": "Guten Morgen, {username}",
|
||||
"welcomeDay": "Hallo, {username}",
|
||||
"welcomeEvening": "Guten Abend, {username}",
|
||||
"welcomeNight": "Gute Nacht, {username}!",
|
||||
"welcomeMorning": "Guten Morgen, {username}!",
|
||||
"welcomeDay": "Hallo {username}!",
|
||||
"welcomeEvening": "Guten Abend, {username}!",
|
||||
"lastViewed": "Zuletzt angesehen",
|
||||
"list": {
|
||||
"newText": "Du kannst eine neue Liste für deine neuen Aufgaben erstellen:",
|
||||
|
@ -335,7 +335,7 @@
|
|||
"create": {
|
||||
"title": "Neuer Namespace",
|
||||
"titleRequired": "Bitte gebe einen Titel an.",
|
||||
"explanation": "Ein Namespace ist eine Sammlung von Listen, die du teilen und zur Organisation verwenden kannst. Jede Liste zu einem Namespace.",
|
||||
"explanation": "Ein Namespace ist eine Sammlung von Listen, die du teilen und zur Organisation verwenden kannst. Jede Liste gehört zu einem Namespace.",
|
||||
"tooltip": "Was ist ein Namespace?",
|
||||
"success": "Der Namespace wurde erfolgreich erstellt."
|
||||
},
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"home": {
|
||||
"welcomeNight": "Guet Nacht, {username}",
|
||||
"welcomeMorning": "Guete Morgä, {username}",
|
||||
"welcomeDay": "Hoi {username}",
|
||||
"welcomeEvening": "Guete Abig, {username}",
|
||||
"welcomeNight": "Gute Nacht, {username}!",
|
||||
"welcomeMorning": "Guten Morgen, {username}!",
|
||||
"welcomeDay": "Hallo {username}!",
|
||||
"welcomeEvening": "Guten Abend, {username}!",
|
||||
"lastViewed": "Zletscht ahglueget",
|
||||
"list": {
|
||||
"newText": "Du chasch e Liste für dini neue Uufgabe erstelle:",
|
||||
|
@ -335,7 +335,7 @@
|
|||
"create": {
|
||||
"title": "Neuer Namespace",
|
||||
"titleRequired": "Bitte gib en Titl ah.",
|
||||
"explanation": "Ein Namespace ist eine Sammlung von Listen, die du teilen und zur Organisation verwenden kannst. Jede Liste zu einem Namespace.",
|
||||
"explanation": "Ein Namespace ist eine Sammlung von Listen, die du teilen und zur Organisation verwenden kannst. Jede Liste gehört zu einem Namespace.",
|
||||
"tooltip": "Was isch en Namensruum?",
|
||||
"success": "Namensruum erstellt."
|
||||
},
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"home": {
|
||||
"welcomeNight": "Good Night {username}",
|
||||
"welcomeMorning": "Good Morning {username}",
|
||||
"welcomeDay": "Hi {username}",
|
||||
"welcomeEvening": "Good Evening {username}",
|
||||
"welcomeNight": "Good Night {username}!",
|
||||
"welcomeMorning": "Good Morning {username}!",
|
||||
"welcomeDay": "Hi {username}!",
|
||||
"welcomeEvening": "Good Evening {username}!",
|
||||
"lastViewed": "Last viewed",
|
||||
"list": {
|
||||
"newText": "You can create a new list for your new tasks:",
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"home": {
|
||||
"welcomeNight": "Good Night {username}",
|
||||
"welcomeMorning": "Good Morning {username}",
|
||||
"welcomeDay": "Hi {username}",
|
||||
"welcomeEvening": "Good Evening {username}",
|
||||
"welcomeNight": "Good Night {username}!",
|
||||
"welcomeMorning": "Good Morning {username}!",
|
||||
"welcomeDay": "Hi {username}!",
|
||||
"welcomeEvening": "Good Evening {username}!",
|
||||
"lastViewed": "Last viewed",
|
||||
"list": {
|
||||
"newText": "You can create a new list for your new tasks:",
|
||||
|
@ -43,7 +43,7 @@
|
|||
"confirmEmailSuccess": "You successfully confirmed your email! You can log in now.",
|
||||
"totpTitle": "Two Factor Authentication Code",
|
||||
"totpPlaceholder": "e.g. 123456",
|
||||
"login": "Login",
|
||||
"login": "Iniciar sesión",
|
||||
"createAccount": "Create account",
|
||||
"loginWith": "Log in with {provider}",
|
||||
"authenticating": "Authenticating…",
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"home": {
|
||||
"welcomeNight": "Bonne nuit {username}",
|
||||
"welcomeMorning": "Bonjour {username}",
|
||||
"welcomeDay": "Salut {username}",
|
||||
"welcomeEvening": "Bonsoir {username}",
|
||||
"welcomeNight": "Good Night {username}!",
|
||||
"welcomeMorning": "Good Morning {username}!",
|
||||
"welcomeDay": "Hi {username}!",
|
||||
"welcomeEvening": "Good Evening {username}!",
|
||||
"lastViewed": "Dernière consultation",
|
||||
"list": {
|
||||
"newText": "Tu peux créer une nouvelle liste pour tes nouvelles tâches :",
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"home": {
|
||||
"welcomeNight": "Buonanotte {username}",
|
||||
"welcomeMorning": "Buongiorno {username}",
|
||||
"welcomeDay": "Ciao {username}",
|
||||
"welcomeEvening": "Buonasera {username}",
|
||||
"welcomeNight": "Good Night {username}!",
|
||||
"welcomeMorning": "Good Morning {username}!",
|
||||
"welcomeDay": "Hi {username}!",
|
||||
"welcomeEvening": "Good Evening {username}!",
|
||||
"lastViewed": "Ultima visualizzazione",
|
||||
"list": {
|
||||
"newText": "È possibile creare una nuova lista per le nuove attività:",
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"home": {
|
||||
"welcomeNight": "Goede nacht {username}",
|
||||
"welcomeMorning": "Goedemorgen {username}",
|
||||
"welcomeDay": "Hallo {username}",
|
||||
"welcomeEvening": "Goedenavond {username}",
|
||||
"welcomeNight": "Good Night {username}!",
|
||||
"welcomeMorning": "Good Morning {username}!",
|
||||
"welcomeDay": "Hi {username}!",
|
||||
"welcomeEvening": "Good Evening {username}!",
|
||||
"lastViewed": "Laatst bekeken",
|
||||
"list": {
|
||||
"newText": "Je kan een nieuwe lijst maken voor je nieuwe taken:",
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"home": {
|
||||
"welcomeNight": "Dobrej nocy {username}",
|
||||
"welcomeMorning": "Dzień dobry {username}",
|
||||
"welcomeDay": "Cześć {username}",
|
||||
"welcomeEvening": "Dobry wieczór {username}",
|
||||
"welcomeNight": "Good Night {username}!",
|
||||
"welcomeMorning": "Good Morning {username}!",
|
||||
"welcomeDay": "Hi {username}!",
|
||||
"welcomeEvening": "Good Evening {username}!",
|
||||
"lastViewed": "Ostatnio oglądane",
|
||||
"list": {
|
||||
"newText": "Możesz stworzyć nową listę dla swoich nowych zadań:",
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"home": {
|
||||
"welcomeNight": "Good Night {username}",
|
||||
"welcomeMorning": "Good Morning {username}",
|
||||
"welcomeDay": "Hi {username}",
|
||||
"welcomeEvening": "Good Evening {username}",
|
||||
"welcomeNight": "Good Night {username}!",
|
||||
"welcomeMorning": "Good Morning {username}!",
|
||||
"welcomeDay": "Hi {username}!",
|
||||
"welcomeEvening": "Good Evening {username}!",
|
||||
"lastViewed": "Last viewed",
|
||||
"list": {
|
||||
"newText": "You can create a new list for your new tasks:",
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"home": {
|
||||
"welcomeNight": "Boa Noite {username}",
|
||||
"welcomeMorning": "Bom Dia {username}",
|
||||
"welcomeDay": "Olá {username}",
|
||||
"welcomeEvening": "Boa Tarde {username}",
|
||||
"welcomeNight": "Boa Noite {username}!",
|
||||
"welcomeMorning": "Bom Dia {username}!",
|
||||
"welcomeDay": "Olá {username}!",
|
||||
"welcomeEvening": "Boa Tarde {username}!",
|
||||
"lastViewed": "Visto recentemente",
|
||||
"list": {
|
||||
"newText": "Podes criar uma nova lista para as tuas novas tarefas:",
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"home": {
|
||||
"welcomeNight": "Good Night {username}",
|
||||
"welcomeMorning": "Good Morning {username}",
|
||||
"welcomeDay": "Hi {username}",
|
||||
"welcomeEvening": "Good Evening {username}",
|
||||
"welcomeNight": "Good Night {username}!",
|
||||
"welcomeMorning": "Good Morning {username}!",
|
||||
"welcomeDay": "Hi {username}!",
|
||||
"welcomeEvening": "Good Evening {username}!",
|
||||
"lastViewed": "Last viewed",
|
||||
"list": {
|
||||
"newText": "You can create a new list for your new tasks:",
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"home": {
|
||||
"welcomeNight": "Доброй ночи, {username}",
|
||||
"welcomeMorning": "Доброе утро, {username}",
|
||||
"welcomeDay": "Привет, {username}",
|
||||
"welcomeEvening": "Добрый вечер, {username}",
|
||||
"welcomeNight": "Good Night {username}!",
|
||||
"welcomeMorning": "Good Morning {username}!",
|
||||
"welcomeDay": "Hi {username}!",
|
||||
"welcomeEvening": "Good Evening {username}!",
|
||||
"lastViewed": "Последние просмотренные",
|
||||
"list": {
|
||||
"newText": "Ты можешь создать новый список для своих задач:",
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"home": {
|
||||
"welcomeNight": "Good Night {username}",
|
||||
"welcomeMorning": "Good Morning {username}",
|
||||
"welcomeDay": "Hi {username}",
|
||||
"welcomeEvening": "Good Evening {username}",
|
||||
"welcomeNight": "Good Night {username}!",
|
||||
"welcomeMorning": "Good Morning {username}!",
|
||||
"welcomeDay": "Hi {username}!",
|
||||
"welcomeEvening": "Good Evening {username}!",
|
||||
"lastViewed": "Last viewed",
|
||||
"list": {
|
||||
"newText": "You can create a new list for your new tasks:",
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"home": {
|
||||
"welcomeNight": "Good Night {username}",
|
||||
"welcomeMorning": "Good Morning {username}",
|
||||
"welcomeDay": "Hi {username}",
|
||||
"welcomeEvening": "Good Evening {username}",
|
||||
"welcomeNight": "Good Night {username}!",
|
||||
"welcomeMorning": "Good Morning {username}!",
|
||||
"welcomeDay": "Hi {username}!",
|
||||
"welcomeEvening": "Good Evening {username}!",
|
||||
"lastViewed": "Last viewed",
|
||||
"list": {
|
||||
"newText": "You can create a new list for your new tasks:",
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"home": {
|
||||
"welcomeNight": "Good Night {username}",
|
||||
"welcomeMorning": "Good Morning {username}",
|
||||
"welcomeDay": "Hi {username}",
|
||||
"welcomeEvening": "Good Evening {username}",
|
||||
"welcomeNight": "Good Night {username}!",
|
||||
"welcomeMorning": "Good Morning {username}!",
|
||||
"welcomeDay": "Hi {username}!",
|
||||
"welcomeEvening": "Good Evening {username}!",
|
||||
"lastViewed": "Last viewed",
|
||||
"list": {
|
||||
"newText": "You can create a new list for your new tasks:",
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"home": {
|
||||
"welcomeNight": "Ngủ ngon nhé, {username}",
|
||||
"welcomeMorning": "Chào buổi sáng, {username}",
|
||||
"welcomeDay": "Hi {username}",
|
||||
"welcomeEvening": "Chào buổi tối, {username}",
|
||||
"welcomeNight": "Good Night {username}!",
|
||||
"welcomeMorning": "Good Morning {username}!",
|
||||
"welcomeDay": "Hi {username}!",
|
||||
"welcomeEvening": "Good Evening {username}!",
|
||||
"lastViewed": "Xem gần đây",
|
||||
"list": {
|
||||
"newText": "Bạn có thể tạo một danh sách công việc mới cho mình:",
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"home": {
|
||||
"welcomeNight": "Good Night {username}",
|
||||
"welcomeMorning": "Good Morning {username}",
|
||||
"welcomeDay": "Hi {username}",
|
||||
"welcomeEvening": "Good Evening {username}",
|
||||
"welcomeNight": "Good Night {username}!",
|
||||
"welcomeMorning": "Good Morning {username}!",
|
||||
"welcomeDay": "Hi {username}!",
|
||||
"welcomeEvening": "Good Evening {username}!",
|
||||
"lastViewed": "Last viewed",
|
||||
"list": {
|
||||
"newText": "You can create a new list for your new tasks:",
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -7,7 +7,7 @@ export const AUTH_TYPES = {
|
|||
'LINK_SHARE': 2,
|
||||
} as const
|
||||
|
||||
type AuthType = typeof AUTH_TYPES[keyof typeof AUTH_TYPES]
|
||||
export type AuthType = typeof AUTH_TYPES[keyof typeof AUTH_TYPES]
|
||||
|
||||
export interface IUser extends IAbstract {
|
||||
id: number
|
||||
|
|
|
@ -9,6 +9,7 @@ import {setTitle} from '@/helpers/setTitle'
|
|||
|
||||
import {useListStore} from '@/stores/lists'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
|
||||
import HomeComponent from '../views/Home.vue'
|
||||
import NotFoundComponent from '../views/404.vue'
|
||||
|
@ -464,11 +465,18 @@ const router = createRouter({
|
|||
],
|
||||
})
|
||||
|
||||
export function getAuthForRoute(route: RouteLocation) {
|
||||
export async function getAuthForRoute(route: RouteLocation) {
|
||||
const authStore = useAuthStore()
|
||||
if (authStore.authUser || authStore.authLinkShare) {
|
||||
return
|
||||
}
|
||||
|
||||
const baseStore = useBaseStore()
|
||||
// When trying this before the current user was fully loaded we might get a flash of the login screen
|
||||
// in the user shell. To make shure this does not happen we check if everything is ready before trying.
|
||||
if (!baseStore.ready) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the user is already logged in and redirect them to the home page if not
|
||||
if (
|
||||
|
@ -481,14 +489,23 @@ export function getAuthForRoute(route: RouteLocation) {
|
|||
'openid.auth',
|
||||
].includes(route.name as string) &&
|
||||
localStorage.getItem('passwordResetToken') === null &&
|
||||
localStorage.getItem('emailConfirmToken') === null
|
||||
localStorage.getItem('emailConfirmToken') === null &&
|
||||
!(route.name === 'home' && (typeof route.query.userPasswordReset !== 'undefined' || typeof route.query.userEmailConfirm !== 'undefined'))
|
||||
) {
|
||||
saveLastVisited(route.name as string, route.params)
|
||||
saveLastVisited(route.name as string, route.params, route.query)
|
||||
return {name: 'user.login'}
|
||||
}
|
||||
|
||||
if(localStorage.getItem('passwordResetToken') !== null && route.name !== 'user.password-reset.reset') {
|
||||
return {name: 'user.password-reset.reset'}
|
||||
}
|
||||
|
||||
if(localStorage.getItem('emailConfirmToken') !== null && route.name !== 'user.login') {
|
||||
return {name: 'user.login'}
|
||||
}
|
||||
}
|
||||
|
||||
router.beforeEach((to) => {
|
||||
router.beforeEach(async (to) => {
|
||||
return getAuthForRoute(to)
|
||||
})
|
||||
|
||||
|
|
|
@ -182,11 +182,11 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
|||
////////////////
|
||||
|
||||
/**
|
||||
* The modelFactory returns an model from an object.
|
||||
* The modelFactory returns a model from an object.
|
||||
* This one here is the default one, usually the service definitions for a model will override this.
|
||||
*/
|
||||
modelFactory(data : Partial<Model>) {
|
||||
return new AbstractModel(data)
|
||||
return data as Model
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -3,7 +3,7 @@ import {defineStore, acceptHMRUpdate} from 'pinia'
|
|||
import {HTTPFactory, AuthenticatedHTTPFactory} from '@/http-common'
|
||||
import {i18n, getCurrentLanguage, saveLanguage} from '@/i18n'
|
||||
import {objectToSnakeCase} from '@/helpers/case'
|
||||
import UserModel, { getAvatarUrl } from '@/models/user'
|
||||
import UserModel, { getAvatarUrl, getDisplayName } from '@/models/user'
|
||||
import UserSettingsService from '@/services/userSettings'
|
||||
import {getToken, refreshToken, removeToken, saveToken} from '@/helpers/auth'
|
||||
import {setModuleLoading} from '@/stores/helper'
|
||||
|
@ -54,6 +54,9 @@ export const useAuthStore = defineStore('auth', {
|
|||
state.info.type === AUTH_TYPES.LINK_SHARE
|
||||
)
|
||||
},
|
||||
userDisplayName(state) {
|
||||
return state.info ? getDisplayName(state.info) : undefined
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
setIsLoading(isLoading: boolean) {
|
||||
|
@ -114,7 +117,7 @@ export const useAuthStore = defineStore('auth', {
|
|||
saveToken(response.data.token, true)
|
||||
|
||||
// Tell others the user is autheticated
|
||||
this.checkAuth()
|
||||
await this.checkAuth()
|
||||
} catch (e) {
|
||||
if (
|
||||
e.response &&
|
||||
|
@ -165,7 +168,7 @@ export const useAuthStore = defineStore('auth', {
|
|||
saveToken(response.data.token, true)
|
||||
|
||||
// Tell others the user is autheticated
|
||||
this.checkAuth()
|
||||
await this.checkAuth()
|
||||
} finally {
|
||||
this.setIsLoading(false)
|
||||
}
|
||||
|
@ -177,12 +180,12 @@ export const useAuthStore = defineStore('auth', {
|
|||
password: password,
|
||||
})
|
||||
saveToken(response.data.token, false)
|
||||
this.checkAuth()
|
||||
await this.checkAuth()
|
||||
return response.data
|
||||
},
|
||||
|
||||
// Populates user information from jwt token saved in local storage in store
|
||||
checkAuth() {
|
||||
async checkAuth() {
|
||||
|
||||
// This function can be called from multiple places at the same time and shortly after one another.
|
||||
// To prevent hitting the api too frequently or race conditions, we check at most once per minute.
|
||||
|
@ -206,7 +209,7 @@ export const useAuthStore = defineStore('auth', {
|
|||
this.setUser(info)
|
||||
|
||||
if (authenticated) {
|
||||
this.refreshUserInfo()
|
||||
await this.refreshUserInfo()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -215,6 +218,8 @@ export const useAuthStore = defineStore('auth', {
|
|||
this.setUser(null)
|
||||
this.redirectToProviderIfNothingElseIsEnabled()
|
||||
}
|
||||
|
||||
return Promise.resolve(authenticated)
|
||||
},
|
||||
|
||||
redirectToProviderIfNothingElseIsEnabled() {
|
||||
|
@ -285,11 +290,11 @@ export const useAuthStore = defineStore('auth', {
|
|||
const stopLoading = setModuleLoading(this)
|
||||
try {
|
||||
await HTTPFactory().post('user/confirm', {token: emailVerifyToken})
|
||||
localStorage.removeItem('emailConfirmToken')
|
||||
return true
|
||||
} catch(e) {
|
||||
throw new Error(e.response.data.message)
|
||||
} finally {
|
||||
localStorage.removeItem('emailConfirmToken')
|
||||
stopLoading()
|
||||
}
|
||||
}
|
||||
|
@ -332,22 +337,22 @@ export const useAuthStore = defineStore('auth', {
|
|||
|
||||
try {
|
||||
await refreshToken(!this.isLinkShareAuth)
|
||||
this.checkAuth()
|
||||
await this.checkAuth()
|
||||
} catch (e) {
|
||||
// Don't logout on network errors as the user would then get logged out if they don't have
|
||||
// internet for a short period of time - such as when the laptop is still reconnecting
|
||||
if (e?.request?.status) {
|
||||
this.logout()
|
||||
await this.logout()
|
||||
}
|
||||
}
|
||||
}, 5000)
|
||||
},
|
||||
|
||||
logout() {
|
||||
async logout() {
|
||||
removeToken()
|
||||
window.localStorage.clear() // Clear all settings and history we might have saved in local storage.
|
||||
router.push({name: 'user.login'})
|
||||
this.checkAuth()
|
||||
await router.push({name: 'user.login'})
|
||||
await this.checkAuth()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
@ -11,6 +11,7 @@ import type {IList} from '@/modelTypes/IList'
|
|||
|
||||
export interface RootStoreState {
|
||||
loading: boolean,
|
||||
ready: boolean,
|
||||
|
||||
currentList: IList | null,
|
||||
background: string,
|
||||
|
@ -26,6 +27,7 @@ export interface RootStoreState {
|
|||
export const useBaseStore = defineStore('base', {
|
||||
state: () : RootStoreState => ({
|
||||
loading: false,
|
||||
ready: false,
|
||||
|
||||
// This is used to highlight the current list in menu for all list related views
|
||||
currentList: new ListModel({
|
||||
|
@ -95,6 +97,10 @@ export const useBaseStore = defineStore('base', {
|
|||
setLogoVisible(visible: boolean) {
|
||||
this.logoVisible = visible
|
||||
},
|
||||
|
||||
setReady(ready: boolean) {
|
||||
this.ready = ready
|
||||
},
|
||||
|
||||
async handleSetCurrentList({list, forceUpdate = false} : {list: IList | null, forceUpdate: boolean}) {
|
||||
if (list === null) {
|
||||
|
@ -133,7 +139,8 @@ export const useBaseStore = defineStore('base', {
|
|||
|
||||
async loadApp() {
|
||||
await checkAndSetApiUrl(window.API_URL)
|
||||
useAuthStore().checkAuth()
|
||||
await useAuthStore().checkAuth()
|
||||
this.ready = true
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
@ -28,6 +28,7 @@ import {useLabelStore} from '@/stores/labels'
|
|||
import {useListStore} from '@/stores/lists'
|
||||
import {useAttachmentStore} from '@/stores/attachments'
|
||||
import {useKanbanStore} from '@/stores/kanban'
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
|
||||
// IDEA: maybe use a small fuzzy search here to prevent errors
|
||||
function findPropertyByValue(object, key, value) {
|
||||
|
@ -105,6 +106,7 @@ export const useTaskStore = defineStore('task', {
|
|||
const cancel = setModuleLoading(this)
|
||||
try {
|
||||
this.tasks = await taskService.getAll({}, params)
|
||||
useBaseStore().setHasTasks(this.tasks.length > 0)
|
||||
return this.tasks
|
||||
} finally {
|
||||
cancel()
|
||||
|
@ -385,6 +387,7 @@ export const useTaskStore = defineStore('task', {
|
|||
})
|
||||
|
||||
if(foundListId === null || foundListId === 0) {
|
||||
cancel()
|
||||
throw new Error('NO_LIST')
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
<template>
|
||||
<div class="content has-text-centered">
|
||||
<h2 v-if="userInfo">
|
||||
{{ $t(welcome, {username: userInfo.name !== '' ? userInfo.name : userInfo.username}) }}!
|
||||
</h2>
|
||||
<h2 v-if="salutation">{{ salutation }}</h2>
|
||||
|
||||
<message variant="danger" v-if="deletionScheduledAt !== null" class="mb-4">
|
||||
{{
|
||||
$t('user.deletion.scheduled', {
|
||||
|
@ -69,7 +68,7 @@ import AddTask from '@/components/tasks/add-task.vue'
|
|||
import {getHistory} from '@/modules/listHistory'
|
||||
import {parseDateOrNull} from '@/helpers/parseDateOrNull'
|
||||
import {formatDateShort, formatDateSince} from '@/helpers/time/formatDate'
|
||||
import {useDateTimeSalutation} from '@/composables/useDateTimeSalutation'
|
||||
import {useDaytimeSalutation} from '@/composables/useDaytimeSalutation'
|
||||
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {useListStore} from '@/stores/lists'
|
||||
|
@ -78,7 +77,7 @@ import {useNamespaceStore} from '@/stores/namespaces'
|
|||
import {useAuthStore} from '@/stores/auth'
|
||||
import {useTaskStore} from '@/stores/tasks'
|
||||
|
||||
const welcome = useDateTimeSalutation()
|
||||
const salutation = useDaytimeSalutation()
|
||||
|
||||
const baseStore = useBaseStore()
|
||||
const authStore = useAuthStore()
|
||||
|
@ -99,7 +98,6 @@ const listHistory = computed(() => {
|
|||
})
|
||||
|
||||
const migratorsEnabled = computed(() => configStore.availableMigrators?.length > 0)
|
||||
const userInfo = computed(() => authStore.info)
|
||||
const hasTasks = computed(() => baseStore.hasTasks)
|
||||
const defaultListId = computed(() => authStore.settings.defaultListId)
|
||||
const defaultNamespaceId = computed(() => namespaceStore.namespaces?.[0]?.id || 0)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<ListWrapper class="list-kanban" :list-id="listId" viewName="kanban">
|
||||
<template #header>
|
||||
<div class="filter-container" v-if="isSavedFilter(list)">
|
||||
<div class="filter-container" v-if="!isSavedFilter(listId)">
|
||||
<div class="items">
|
||||
<filter-popup
|
||||
v-model="params"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<create-edit
|
||||
:title="$t('list.share.header')"
|
||||
primary-label=""
|
||||
:has-primary-action="false"
|
||||
>
|
||||
<template v-if="list">
|
||||
<userTeam
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<create-edit
|
||||
:title="title"
|
||||
primary-label=""
|
||||
:has-primary-action="false"
|
||||
>
|
||||
<template v-if="namespace">
|
||||
<manageSharing
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
:disabled="!canWrite"
|
||||
:list-id="task.listId"
|
||||
:task-id="task.id"
|
||||
ref="assignees"
|
||||
:ref="e => setFieldRef('assignees', e)"
|
||||
v-model="task.assignees"
|
||||
/>
|
||||
</div>
|
||||
|
@ -40,7 +40,7 @@
|
|||
<priority-select
|
||||
:disabled="!canWrite"
|
||||
@update:model-value="setPriority"
|
||||
ref="priority"
|
||||
:ref="e => setFieldRef('priority', e)"
|
||||
v-model="task.priority"/>
|
||||
</div>
|
||||
</transition>
|
||||
|
@ -57,7 +57,7 @@
|
|||
@close-on-change="() => saveTask()"
|
||||
:choose-date-label="$t('task.detail.chooseDueDate')"
|
||||
:disabled="taskService.loading || !canWrite"
|
||||
ref="dueDate"
|
||||
:ref="e => setFieldRef('dueDate', e)"
|
||||
/>
|
||||
<BaseButton
|
||||
@click="() => {task.dueDate = null;saveTask()}"
|
||||
|
@ -80,7 +80,7 @@
|
|||
<percent-done-select
|
||||
:disabled="!canWrite"
|
||||
@update:model-value="setPercentDone"
|
||||
ref="percentDone"
|
||||
:ref="e => setFieldRef('percentDone', e)"
|
||||
v-model="task.percentDone"/>
|
||||
</div>
|
||||
</transition>
|
||||
|
@ -97,7 +97,7 @@
|
|||
@close-on-change="() => saveTask()"
|
||||
:choose-date-label="$t('task.detail.chooseStartDate')"
|
||||
:disabled="taskService.loading || !canWrite"
|
||||
ref="startDate"
|
||||
:ref="e => setFieldRef('startDate', e)"
|
||||
/>
|
||||
<BaseButton
|
||||
@click="() => {task.startDate = null;saveTask()}"
|
||||
|
@ -124,7 +124,7 @@
|
|||
@close-on-change="() => saveTask()"
|
||||
:choose-date-label="$t('task.detail.chooseEndDate')"
|
||||
:disabled="taskService.loading || !canWrite"
|
||||
ref="endDate"
|
||||
:ref="e => setFieldRef('endDate', e)"
|
||||
/>
|
||||
<BaseButton
|
||||
@click="() => {task.endDate = null;saveTask()}"
|
||||
|
@ -146,7 +146,7 @@
|
|||
</div>
|
||||
<reminders
|
||||
:disabled="!canWrite"
|
||||
ref="reminders"
|
||||
:ref="e => setFieldRef('reminders', e)"
|
||||
v-model="task.reminderDates"
|
||||
@update:model-value="saveTask"
|
||||
/>
|
||||
|
@ -171,7 +171,7 @@
|
|||
</div>
|
||||
<repeat-after
|
||||
:disabled="!canWrite"
|
||||
ref="repeatAfter"
|
||||
:ref="e => setFieldRef('repeatAfter', e)"
|
||||
v-model="task"
|
||||
@update:model-value="saveTask"
|
||||
/>
|
||||
|
@ -186,7 +186,7 @@
|
|||
</div>
|
||||
<color-picker
|
||||
menu-position="bottom"
|
||||
ref="color"
|
||||
:ref="e => setFieldRef('color', e)"
|
||||
v-model="taskColor"
|
||||
@update:model-value="saveTask"
|
||||
/>
|
||||
|
@ -202,7 +202,11 @@
|
|||
</span>
|
||||
{{ $t('task.attributes.labels') }}
|
||||
</div>
|
||||
<edit-labels :disabled="!canWrite" :task-id="taskId" ref="labels" v-model="task.labels"/>
|
||||
<edit-labels
|
||||
:disabled="!canWrite"
|
||||
:task-id="taskId"
|
||||
:ref="e => setFieldRef('labels', e)"
|
||||
v-model="task.labels"/>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
|
@ -220,7 +224,7 @@
|
|||
:edit-enabled="canWrite"
|
||||
:task="task"
|
||||
@task-changed="({coverImageAttachmentId}) => task.coverImageAttachmentId = coverImageAttachmentId"
|
||||
ref="attachments"
|
||||
:ref="e => setFieldRef('attachments', e)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -238,7 +242,7 @@
|
|||
:list-id="task.listId"
|
||||
:show-no-relations-notice="true"
|
||||
:task-id="taskId"
|
||||
ref="relatedTasks"
|
||||
:ref="e => setFieldRef('relatedTasks', e)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -252,7 +256,10 @@
|
|||
</h3>
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded">
|
||||
<list-search @update:modelValue="changeList" ref="moveList"/>
|
||||
<list-search
|
||||
@update:modelValue="changeList"
|
||||
:ref="e => setFieldRef('moveList', e)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -659,10 +666,14 @@ const activeFieldElements : {[id in FieldType]: HTMLElement | null} = reactive({
|
|||
startDate: null,
|
||||
})
|
||||
|
||||
function setFieldRef(name, e) {
|
||||
activeFieldElements[name] = unrefElement(e)
|
||||
}
|
||||
|
||||
function setFieldActive(fieldName: keyof typeof activeFields) {
|
||||
activeFields[fieldName] = true
|
||||
nextTick(() => {
|
||||
const el = unrefElement(activeFieldElements[fieldName])
|
||||
const el = activeFieldElements[fieldName]
|
||||
|
||||
if (!el) {
|
||||
return
|
||||
|
@ -782,8 +793,6 @@ async function setPercentDone(percentDone: number) {
|
|||
$flash-background-duration: 750ms;
|
||||
|
||||
.task-view {
|
||||
// This is a workaround to hide the llama background from the top on the task detail page
|
||||
margin-top: -1.5rem;
|
||||
padding: 1rem;
|
||||
background-color: var(--site-background);
|
||||
|
||||
|
|
|
@ -104,7 +104,6 @@
|
|||
<script setup lang="ts">
|
||||
import {computed, onBeforeMount, ref} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {useRouter} from 'vue-router'
|
||||
import {useDebounceFn} from '@vueuse/core'
|
||||
|
||||
import Message from '@/components/misc/message.vue'
|
||||
|
@ -112,19 +111,19 @@ import Password from '@/components/input/password.vue'
|
|||
|
||||
import {getErrorText} from '@/message'
|
||||
import {redirectToProvider} from '@/helpers/redirectToProvider'
|
||||
import {getLastVisited, clearLastVisited} from '@/helpers/saveLastVisited'
|
||||
import {useRedirectToLastVisited} from '@/composables/useRedirectToLastVisited'
|
||||
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {useConfigStore} from '@/stores/config'
|
||||
|
||||
import {useTitle} from '@/composables/useTitle'
|
||||
|
||||
const router = useRouter()
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
useTitle(() => t('user.auth.login'))
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const configStore = useConfigStore()
|
||||
const {redirectIfSaved} = useRedirectToLastVisited()
|
||||
|
||||
const registrationEnabled = computed(() => configStore.registrationEnabled)
|
||||
const localAuthEnabled = computed(() => configStore.auth.local.enabled)
|
||||
|
@ -151,16 +150,7 @@ onBeforeMount(() => {
|
|||
|
||||
// Check if the user is already logged in, if so, redirect them to the homepage
|
||||
if (authenticated.value) {
|
||||
const last = getLastVisited()
|
||||
if (last !== null) {
|
||||
router.push({
|
||||
name: last.name,
|
||||
params: last.params,
|
||||
})
|
||||
clearLastVisited()
|
||||
} else {
|
||||
router.push({name: 'home'})
|
||||
}
|
||||
redirectIfSaved()
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -18,19 +18,19 @@ export default { name: 'Auth' }
|
|||
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, onMounted} from 'vue'
|
||||
import {useRoute, useRouter} from 'vue-router'
|
||||
import {useRoute} from 'vue-router'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
import {getErrorText} from '@/message'
|
||||
import Message from '@/components/misc/message.vue'
|
||||
import {clearLastVisited, getLastVisited} from '@/helpers/saveLastVisited'
|
||||
import {useRedirectToLastVisited} from '@/composables/useRedirectToLastVisited'
|
||||
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const {redirectIfSaved} = useRedirectToLastVisited()
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
|
@ -74,16 +74,7 @@ async function authenticateWithCode() {
|
|||
provider: route.params.provider,
|
||||
code: route.query.code,
|
||||
})
|
||||
const last = getLastVisited()
|
||||
if (last !== null) {
|
||||
router.push({
|
||||
name: last.name,
|
||||
params: last.params,
|
||||
})
|
||||
clearLastVisited()
|
||||
} else {
|
||||
router.push({name: 'home'})
|
||||
}
|
||||
redirectIfSaved()
|
||||
} catch(e) {
|
||||
const err = getErrorText(e)
|
||||
errorMessage.value = typeof err[1] !== 'undefined' ? err[1] : err[0]
|
||||
|
|
|
@ -7,41 +7,14 @@
|
|||
<message variant="success">
|
||||
{{ successMessage }}
|
||||
</message>
|
||||
<x-button :to="{ name: 'user.login' }">
|
||||
<x-button :to="{ name: 'user.login' }" class="mt-4">
|
||||
{{ $t('user.auth.login') }}
|
||||
</x-button>
|
||||
</div>
|
||||
<form @submit.prevent="submit" id="form" v-if="!successMessage">
|
||||
<div class="field">
|
||||
<label class="label" for="password1">{{ $t('user.auth.password') }}</label>
|
||||
<div class="control">
|
||||
<input
|
||||
class="input"
|
||||
id="password1"
|
||||
name="password1"
|
||||
:placeholder="$t('user.auth.passwordPlaceholder')"
|
||||
required
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
v-focus
|
||||
v-model="credentials.password"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="password2">{{ $t('user.auth.passwordRepeat') }}</label>
|
||||
<div class="control">
|
||||
<input
|
||||
class="input"
|
||||
id="password2"
|
||||
name="password2"
|
||||
:placeholder="$t('user.auth.passwordPlaceholder')"
|
||||
required
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
v-model="credentials.password2"
|
||||
@keyup.enter="submit"
|
||||
/>
|
||||
</div>
|
||||
<label class="label" for="password">{{ $t('user.auth.password') }}</label>
|
||||
<Password @submit="submit" @update:modelValue="v => credentials.password = v"/>
|
||||
</div>
|
||||
|
||||
<div class="field is-grouped">
|
||||
|
@ -60,17 +33,14 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import {ref, reactive} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
import PasswordResetModel from '@/models/passwordReset'
|
||||
import PasswordResetService from '@/services/passwordReset'
|
||||
import Message from '@/components/misc/message.vue'
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
import Password from '@/components/input/password.vue'
|
||||
|
||||
const credentials = reactive({
|
||||
password: '',
|
||||
password2: '',
|
||||
})
|
||||
|
||||
const passwordResetService = reactive(new PasswordResetService())
|
||||
|
@ -79,15 +49,14 @@ const successMessage = ref('')
|
|||
|
||||
async function submit() {
|
||||
errorMsg.value = ''
|
||||
|
||||
if (credentials.password2 !== credentials.password) {
|
||||
errorMsg.value = t('user.auth.passwordsDontMatch')
|
||||
|
||||
if(credentials.password === '') {
|
||||
return
|
||||
}
|
||||
|
||||
const passwordReset = new PasswordResetModel({newPassword: credentials.password})
|
||||
try {
|
||||
const {message} = passwordResetService.resetPassword(passwordReset)
|
||||
const {message} = await passwordResetService.resetPassword(passwordReset)
|
||||
successMessage.value = message
|
||||
localStorage.removeItem('passwordResetToken')
|
||||
} catch (e) {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<message variant="success">
|
||||
{{ $t('user.auth.resetPasswordSuccess') }}
|
||||
</message>
|
||||
<x-button :to="{ name: 'user.login' }">
|
||||
<x-button :to="{ name: 'user.login' }" class="mt-4">
|
||||
{{ $t('user.auth.login') }}
|
||||
</x-button>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue