Merge branch 'main' into feature/ganttastic

# Conflicts:
#	pnpm-lock.yaml
#	src/modelTypes/ITask.ts
#	src/stores/auth.ts
#	src/types/vue-flatpickr-component.d.ts
This commit is contained in:
Dominik Pschenitschni 2022-10-24 17:19:23 +02:00
commit b18640d688
Signed by: dpschen
GPG Key ID: B257AC0149F43A77
73 changed files with 2714 additions and 792 deletions

View File

@ -193,4 +193,15 @@ describe('List View Kanban', () => {
cy.get('.kanban .bucket') cy.get('.kanban .bucket')
.should('not.contain', task.title) .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')
})
}) })

View File

@ -128,4 +128,24 @@ describe('Home Page Task Overview', () => {
.last() .last()
.should('contain.text', newTaskTitle) .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:')
})
}) })

View File

@ -12,15 +12,51 @@ import {LabelTaskFactory} from '../../factories/label_task'
import {BucketFactory} from '../../factories/bucket' import {BucketFactory} from '../../factories/bucket'
import '../../support/authenticateUser' import '../../support/authenticateUser'
import {TaskAttachmentFactory} from '../../factories/task_attachments'
function addLabelToTaskAndVerify(labelTitle: string) {
cy.get('.task-view .action-buttons .button')
.contains('Add Labels')
.click()
cy.get('.task-view .details.labels-list .multiselect input')
.type(labelTitle)
cy.get('.task-view .details.labels-list .multiselect .search-results')
.children()
.first()
.click()
cy.get('.global-notification', { timeout: 4000 })
.should('contain', 'Success')
cy.get('.task-view .details.labels-list .multiselect .input-wrapper span.tag')
.should('exist')
.should('contain', labelTitle)
}
function uploadAttachmentAndVerify(taskId: number) {
cy.intercept(`${Cypress.env('API_URL')}/tasks/${taskId}/attachments`).as('uploadAttachment')
cy.get('.task-view .action-buttons .button')
.contains('Add Attachments')
.click()
cy.get('input[type=file]', {timeout: 1000})
.selectFile('cypress/fixtures/image.jpg', {force: true}) // The input is not visible, but on purpose
cy.wait('@uploadAttachment')
cy.get('.attachments .attachments .files a.attachment')
.should('exist')
}
describe('Task', () => { describe('Task', () => {
let namespaces let namespaces
let lists let lists
let buckets
beforeEach(() => { beforeEach(() => {
UserFactory.create(1) UserFactory.create(1)
namespaces = NamespaceFactory.create(1) namespaces = NamespaceFactory.create(1)
lists = ListFactory.create(1) lists = ListFactory.create(1)
buckets = BucketFactory.create(1, {
list_id: lists[0].id,
})
TaskFactory.truncate() TaskFactory.truncate()
UserListFactory.truncate() UserListFactory.truncate()
}) })
@ -80,6 +116,7 @@ describe('Task', () => {
describe('Task Detail View', () => { describe('Task Detail View', () => {
beforeEach(() => { beforeEach(() => {
TaskCommentFactory.truncate() TaskCommentFactory.truncate()
LabelTaskFactory.truncate()
}) })
it('Shows all task details', () => { it('Shows all task details', () => {
@ -344,21 +381,31 @@ describe('Task', () => {
cy.visit(`/tasks/${tasks[0].id}`) cy.visit(`/tasks/${tasks[0].id}`)
cy.get('.task-view .action-buttons .button') addLabelToTaskAndVerify(labels[0].title)
.contains('Add Labels') })
it('Can add a label to a task and it shows up on the kanban board afterwards', () => {
const tasks = TaskFactory.create(1, {
id: 1,
list_id: lists[0].id,
bucket_id: buckets[0].id,
})
const labels = LabelFactory.create(1)
LabelTaskFactory.truncate()
cy.visit(`/lists/${lists[0].id}/kanban`)
cy.get('.bucket .task')
.contains(tasks[0].title)
.click() .click()
cy.get('.task-view .details.labels-list .multiselect input')
.type(labels[0].title) addLabelToTaskAndVerify(labels[0].title)
cy.get('.task-view .details.labels-list .multiselect .search-results')
.children() cy.get('.modal-content .close')
.first()
.click() .click()
cy.get('.global-notification', { timeout: 4000 }) cy.get('.bucket .task')
.should('contain', 'Success') .should('contain.text', labels[0].title)
cy.get('.task-view .details.labels-list .multiselect .input-wrapper span.tag')
.should('exist')
.should('contain', labels[0].title)
}) })
it('Can remove a label from a task', () => { it('Can remove a label from a task', () => {
@ -417,5 +464,117 @@ describe('Task', () => {
cy.get('.global-notification') cy.get('.global-notification')
.should('contain', 'Success') .should('contain', 'Success')
}) })
it('Can set a priority for a task', () => {
const tasks = TaskFactory.create(1, {
id: 1,
})
cy.visit(`/tasks/${tasks[0].id}`)
cy.get('.task-view .action-buttons .button')
.contains('Set Priority')
.click()
cy.get('.task-view .columns.details .column')
.contains('Priority')
.get('.select select')
.select('Urgent')
cy.get('.global-notification')
.should('contain', 'Success')
cy.get('.task-view .columns.details .column')
.contains('Priority')
.get('.select select')
.should('have.value', '4')
})
it('Can set the progress for a task', () => {
const tasks = TaskFactory.create(1, {
id: 1,
})
cy.visit(`/tasks/${tasks[0].id}`)
cy.get('.task-view .action-buttons .button')
.contains('Set Progress')
.click()
cy.get('.task-view .columns.details .column')
.contains('Progress')
.get('.select select')
.select('50%')
cy.get('.global-notification')
.should('contain', 'Success')
cy.wait(200)
cy.get('.task-view .columns.details .column')
.contains('Progress')
.get('.select select')
.should('be.visible')
.should('have.value', '0.5')
})
it('Can add an attachment to a task', () => {
TaskAttachmentFactory.truncate()
const tasks = TaskFactory.create(1, {
id: 1,
})
cy.visit(`/tasks/${tasks[0].id}`)
uploadAttachmentAndVerify(tasks[0].id)
})
it('Can add an attachment to a task and see it appearing on kanban', () => {
TaskAttachmentFactory.truncate()
const tasks = TaskFactory.create(1, {
id: 1,
list_id: lists[0].id,
bucket_id: buckets[0].id,
})
const labels = LabelFactory.create(1)
LabelTaskFactory.truncate()
cy.visit(`/lists/${lists[0].id}/kanban`)
cy.get('.bucket .task')
.contains(tasks[0].title)
.click()
uploadAttachmentAndVerify(tasks[0].id)
cy.get('.modal-content .close')
.click()
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')
})
}) })
}) })

View File

@ -55,4 +55,9 @@ context('Login', () => {
testAndAssertFailed(fixture) testAndAssertFailed(fixture)
}) })
it('Should redirect to /login when no user is logged in', () => {
cy.visit('/')
cy.url().should('include', '/login')
})
}) })

View File

@ -0,0 +1,17 @@
import {Factory} from '../support/factory'
import {formatISO} from 'date-fns'
export class TaskAttachmentFactory extends Factory {
static table = 'task_attachments'
static factory() {
const now = new Date()
return {
id: '{increment}',
task_id: 1,
file_id: 1,
created: formatISO(now),
}
}
}

View File

@ -6,7 +6,7 @@
], ],
"packageRules": [ "packageRules": [
{ {
"matchPackageNames": ["netlify-cli"], "matchPackageNames": ["netlify-cli", "happy-dom"],
"extends": ["schedule:weekly"] "extends": ["schedule:weekly"]
}, },
{ {

View File

@ -44,8 +44,8 @@
variant="secondary" variant="secondary"
:shadow="false" :shadow="false"
> >
<img :src="userAvatar" alt="" class="avatar" width="40" height="40"/> <img :src="authStore.avatarUrl" alt="" class="avatar" width="40" height="40"/>
<span class="username">{{ userInfo.name !== '' ? userInfo.name : userInfo.username }}</span> <span class="username">{{ authStore.userDisplayName }}</span>
<span class="icon is-small"> <span class="icon is-small">
<icon icon="chevron-down"/> <icon icon="chevron-down"/>
</span> </span>
@ -80,7 +80,7 @@
{{ $t('about.title') }} {{ $t('about.title') }}
</dropdown-item> </dropdown-item>
<dropdown-item <dropdown-item
@click="logout()" @click="authStore.logout()"
> >
{{ $t('user.auth.logout') }} {{ $t('user.auth.logout') }}
</dropdown-item> </dropdown-item>
@ -117,8 +117,6 @@ const canWriteCurrentList = computed(() => baseStore.currentList.maxRight > Righ
const menuActive = computed(() => baseStore.menuActive) const menuActive = computed(() => baseStore.menuActive)
const authStore = useAuthStore() const authStore = useAuthStore()
const userInfo = computed(() => authStore.info)
const userAvatar = computed(() => authStore.avatarUrl)
const configStore = useConfigStore() const configStore = useConfigStore()
const imprintUrl = computed(() => configStore.legal.imprintUrl) const imprintUrl = computed(() => configStore.legal.imprintUrl)
@ -136,10 +134,6 @@ onMounted(async () => {
listTitle.value.style.setProperty('--nav-username-width', `${usernameWidth}px`) listTitle.value.style.setProperty('--nav-username-width', `${usernameWidth}px`)
}) })
function logout() {
authStore.logout()
}
function openQuickActions() { function openQuickActions() {
baseStore.setQuickActionsActive(true) baseStore.setQuickActionsActive(true)
} }

View File

@ -285,9 +285,9 @@ function handleCheckboxClick(e: Event) {
console.debug('no index found') console.debug('no index found')
return return
} }
console.debug(index, text.value.slice(index, 9)) const listPrefix = text.value.substring(index, index + 1)
const listPrefix = text.value.slice(index, 1) console.debug({index, listPrefix, checked, text: text.value})
text.value = replaceAt(text.value, index, `${listPrefix} ${checked ? '[x]' : '[ ]'} `) text.value = replaceAt(text.value, index, `${listPrefix} ${checked ? '[x]' : '[ ]'} `)
bubble() bubble()

View File

@ -55,13 +55,13 @@
> >
{{ $t('menu.archive') }} {{ $t('menu.archive') }}
</dropdown-item> </dropdown-item>
<task-subscription <Subscription
class="has-no-shadow" class="has-no-shadow"
:is-button="false" :is-button="false"
entity="list" entity="list"
:entity-id="list.id" :entity-id="list.id"
:model-value="list.subscription" :model-value="list.subscription"
@update:model-value="sub => subscription = sub" @update:model-value="setSubscriptionInStore"
type="dropdown" type="dropdown"
/> />
<dropdown-item <dropdown-item
@ -81,10 +81,12 @@ import {ref, computed, watchEffect, type PropType} from 'vue'
import {isSavedFilter} from '@/helpers/savedFilter' import {isSavedFilter} from '@/helpers/savedFilter'
import Dropdown from '@/components/misc/dropdown.vue' import Dropdown from '@/components/misc/dropdown.vue'
import DropdownItem from '@/components/misc/dropdown-item.vue' import DropdownItem from '@/components/misc/dropdown-item.vue'
import TaskSubscription from '@/components/misc/subscription.vue' import Subscription from '@/components/misc/subscription.vue'
import type {IList} from '@/modelTypes/IList' import type {IList} from '@/modelTypes/IList'
import type {ISubscription} from '@/modelTypes/ISubscription' import type {ISubscription} from '@/modelTypes/ISubscription'
import {useConfigStore} from '@/stores/config' import {useConfigStore} from '@/stores/config'
import {useListStore} from '@/stores/lists'
import {useNamespaceStore} from '@/stores/namespaces'
const props = defineProps({ const props = defineProps({
list: { list: {
@ -93,6 +95,8 @@ const props = defineProps({
}, },
}) })
const listStore = useListStore()
const namespaceStore = useNamespaceStore()
const subscription = ref<ISubscription | null>(null) const subscription = ref<ISubscription | null>(null)
watchEffect(() => { watchEffect(() => {
subscription.value = props.list.subscription ?? null subscription.value = props.list.subscription ?? null
@ -100,4 +104,14 @@ watchEffect(() => {
const configStore = useConfigStore() const configStore = useConfigStore()
const backgroundsEnabled = computed(() => configStore.enabledBackgroundProviders?.length > 0) const backgroundsEnabled = computed(() => configStore.enabledBackgroundProviders?.length > 0)
function setSubscriptionInStore(sub: ISubscription) {
subscription.value = sub
const updatedList = {
...props.list,
subscription: sub,
}
listStore.setList(updatedList)
namespaceStore.setListInNamespaceById(updatedList)
}
</script> </script>

View File

@ -210,6 +210,7 @@ import ListService from '@/services/list'
import NamespaceService from '@/services/namespace' import NamespaceService from '@/services/namespace'
import EditLabels from '@/components/tasks/partials/editLabels.vue' import EditLabels from '@/components/tasks/partials/editLabels.vue'
import {dateIsValid, formatISO} from '@/helpers/time/formatDate'
import {objectToSnakeCase} from '@/helpers/case' import {objectToSnakeCase} from '@/helpers/case'
import {getDefaultParams} from '@/composables/taskList' import {getDefaultParams} from '@/composables/taskList'
import {camelCase} from 'camel-case' import {camelCase} from 'camel-case'
@ -391,7 +392,14 @@ export default defineComponent({
this.params.filter_value.push(dateTo) this.params.filter_value.push(dateTo)
} }
this.filters[camelCase(filterName)] = {dateFrom, dateTo} this.filters[camelCase(filterName)] = {
// Passing the dates as string values avoids an endless loop between values changing
// in the datepicker (bubbling up to here) and changing here and bubbling down to the
// datepicker (because there's a new date instance every time this function gets called).
// See https://kolaente.dev/vikunja/frontend/issues/2384
dateFrom: dateIsValid(dateFrom) ? formatISO(dateFrom) : dateFrom,
dateTo: dateIsValid(dateTo) ? formatISO(dateTo) : dateTo,
}
this.change() this.change()
return return
} }
@ -511,12 +519,12 @@ export default defineComponent({
if (typeof this.filters[filterName] === 'undefined' || this.filters[filterName] === '') { if (typeof this.filters[filterName] === 'undefined' || this.filters[filterName] === '') {
return return
} }
// Don't load things if we already have something loaded. // Don't load things if we already have something loaded.
// This is not the most ideal solution because it prevents a re-population when filters are changed // This is not the most ideal solution because it prevents a re-population when filters are changed
// from the outside. It is still fine because we're not changing them from the outside, other than // from the outside. It is still fine because we're not changing them from the outside, other than
// loading them initially. // loading them initially.
if(this[kind].length > 0) { if (this[kind].length > 0) {
return return
} }

View File

@ -10,7 +10,7 @@
:loading="loading" :loading="loading"
> >
<div class="p-4"> <div class="p-4">
<slot /> <slot/>
</div> </div>
<template #footer> <template #footer>
@ -30,10 +30,12 @@
{{ $t('misc.cancel') }} {{ $t('misc.cancel') }}
</x-button> </x-button>
<x-button <x-button
v-if="hasPrimaryAction"
variant="primary" variant="primary"
@click.prevent.stop="primary()" @click.prevent.stop="primary()"
:icon="primaryIcon" :icon="primaryIcon"
:disabled="primaryDisabled || loading" :disabled="primaryDisabled || loading"
class="ml-2"
> >
{{ primaryLabel || $t('misc.create') }} {{ primaryLabel || $t('misc.create') }}
</x-button> </x-button>
@ -60,6 +62,10 @@ defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
hasPrimaryAction: {
type: Boolean,
default: true,
},
tertiary: { tertiary: {
type: String, type: String,
default: '', default: '',

View File

@ -61,7 +61,7 @@ const route = useRoute()
const baseStore = useBaseStore() const baseStore = useBaseStore()
const ready = ref(false) const ready = computed(() => baseStore.ready)
const online = useOnline() const online = useOnline()
const error = ref('') const error = ref('')
@ -70,11 +70,11 @@ const showLoading = computed(() => !ready.value && error.value === '')
async function load() { async function load() {
try { try {
await baseStore.loadApp() await baseStore.loadApp()
const redirectTo = getAuthForRoute(route) baseStore.setReady(true)
const redirectTo = await getAuthForRoute(route)
if (typeof redirectTo !== 'undefined') { if (typeof redirectTo !== 'undefined') {
await router.push(redirectTo) await router.push(redirectTo)
} }
ready.value = true
} catch (e: unknown) { } catch (e: unknown) {
error.value = String(e) error.value = String(e)
} }

View File

@ -69,17 +69,38 @@ const emit = defineEmits(['update:modelValue'])
const subscriptionService = shallowRef(new SubscriptionService()) const subscriptionService = shallowRef(new SubscriptionService())
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
const tooltipText = computed(() => { const tooltipText = computed(() => {
if (disabled.value) { if (disabled.value) {
return t('task.subscription.subscribedThroughParent', { if (props.entity === 'list' && subscriptionEntity.value === 'namespace') {
entity: props.entity, return t('task.subscription.subscribedListThroughParentNamespace')
parent: subscriptionEntity.value, }
}) if (props.entity === 'task' && subscriptionEntity.value === 'namespace') {
return t('task.subscription.subscribedTaskThroughParentNamespace')
}
if (props.entity === 'task' && subscriptionEntity.value === 'list') {
return t('task.subscription.subscribedTaskThroughParentList')
}
return ''
} }
return props.modelValue !== null ? switch (props.entity) {
t('task.subscription.subscribed', {entity: props.entity}) : case 'namespace':
t('task.subscription.notSubscribed', {entity: props.entity}) return props.modelValue !== null ?
t('task.subscription.subscribedNamespace') :
t('task.subscription.notSubscribedNamespace')
case 'list':
return props.modelValue !== null ?
t('task.subscription.subscribedList') :
t('task.subscription.notSubscribedList')
case 'task':
return props.modelValue !== null ?
t('task.subscription.subscribedTask') :
t('task.subscription.notSubscribedTask')
}
return ''
}) })
const buttonText = computed(() => props.modelValue ? t('task.subscription.unsubscribe') : t('task.subscription.subscribe')) const buttonText = computed(() => props.modelValue ? t('task.subscription.unsubscribe') : t('task.subscription.subscribe'))
@ -105,7 +126,20 @@ async function subscribe() {
}) })
await subscriptionService.value.create(subscription) await subscriptionService.value.create(subscription)
emit('update:modelValue', subscription) emit('update:modelValue', subscription)
success({message: t('task.subscription.subscribeSuccess', {entity: props.entity})})
let message = ''
switch (props.entity) {
case 'namespace':
message = t('task.subscription.subscribeSuccessNamespace')
break
case 'list':
message = t('task.subscription.subscribeSuccessList')
break
case 'task':
message = t('task.subscription.subscribeSuccessTask')
break
}
success({message})
} }
async function unsubscribe() { async function unsubscribe() {
@ -115,6 +149,19 @@ async function unsubscribe() {
}) })
await subscriptionService.value.delete(subscription) await subscriptionService.value.delete(subscription)
emit('update:modelValue', null) emit('update:modelValue', null)
success({message: t('task.subscription.unsubscribeSuccess', {entity: props.entity})})
let message = ''
switch (props.entity) {
case 'namespace':
message = t('task.subscription.unsubscribeSuccessNamespace')
break
case 'list':
message = t('task.subscription.unsubscribeSuccessList')
break
case 'task':
message = t('task.subscription.unsubscribeSuccessTask')
break
}
success({message})
} }
</script> </script>

View File

@ -33,14 +33,13 @@
> >
{{ $t('menu.archive') }} {{ $t('menu.archive') }}
</dropdown-item> </dropdown-item>
<task-subscription <Subscription
v-if="subscription"
class="has-no-shadow" class="has-no-shadow"
:is-button="false" :is-button="false"
entity="namespace" entity="namespace"
:entity-id="namespace.id" :entity-id="namespace.id"
:model-value="subscription" :model-value="subscription"
@update:model-value="sub => subscription = sub" @update:model-value="setSubscriptionInStore"
type="dropdown" type="dropdown"
/> />
<dropdown-item <dropdown-item
@ -59,9 +58,10 @@ import {ref, onMounted, type PropType} from 'vue'
import Dropdown from '@/components/misc/dropdown.vue' import Dropdown from '@/components/misc/dropdown.vue'
import DropdownItem from '@/components/misc/dropdown-item.vue' import DropdownItem from '@/components/misc/dropdown-item.vue'
import TaskSubscription from '@/components/misc/subscription.vue' import Subscription from '@/components/misc/subscription.vue'
import type {INamespace} from '@/modelTypes/INamespace' import type {INamespace} from '@/modelTypes/INamespace'
import type {ISubscription} from '@/modelTypes/ISubscription' import type {ISubscription} from '@/modelTypes/ISubscription'
import {useNamespaceStore} from '@/stores/namespaces'
const props = defineProps({ const props = defineProps({
namespace: { namespace: {
@ -70,8 +70,20 @@ const props = defineProps({
}, },
}) })
const namespaceStore = useNamespaceStore()
const subscription = ref<ISubscription | null>(null) const subscription = ref<ISubscription | null>(null)
onMounted(() => { onMounted(() => {
subscription.value = props.namespace.subscription subscription.value = props.namespace.subscription
}) })
function setSubscriptionInStore(sub: ISubscription) {
subscription.value = sub
namespaceStore.setNamespaces([
{
...props.namespace,
subscription: sub,
},
])
}
</script> </script>

View File

@ -9,7 +9,7 @@
<input <input
v-if="editEnabled" v-if="editEnabled"
:disabled="attachmentService.loading || undefined" :disabled="loading || undefined"
@change="uploadNewAttachment()" @change="uploadNewAttachment()"
id="files" id="files"
multiple multiple
@ -35,7 +35,15 @@
:key="a.id" :key="a.id"
@click="viewOrDownload(a)" @click="viewOrDownload(a)"
> >
<div class="filename">{{ a.file.name }}</div> <div class="filename">
{{ a.file.name }}
<span
v-if="task.coverImageAttachmentId === a.id"
class="is-task-cover"
>
{{ $t('task.attachment.usedAsCover') }}
</span>
</div>
<div class="info"> <div class="info">
<p class="attachment-info-meta"> <p class="attachment-info-meta">
<i18n-t keypath="task.attachment.createdBy" scope="global"> <i18n-t keypath="task.attachment.createdBy" scope="global">
@ -78,6 +86,17 @@
> >
{{ $t('misc.delete') }} {{ $t('misc.delete') }}
</BaseButton> </BaseButton>
<BaseButton
v-if="editEnabled"
class="attachment-info-meta-button"
@click.prevent.stop="setCoverImage(task.coverImageAttachmentId === a.id ? null : a)"
>
{{
task.coverImageAttachmentId === a.id
? $t('task.attachment.unsetAsCover')
: $t('task.attachment.setAsCover')
}}
</BaseButton>
</p> </p>
</div> </div>
</a> </a>
@ -85,7 +104,7 @@
<x-button <x-button
v-if="editEnabled" v-if="editEnabled"
:disabled="attachmentService.loading" :disabled="loading"
@click="filesRef?.click()" @click="filesRef?.click()"
class="mb-4" class="mb-4"
icon="cloud-upload-alt" icon="cloud-upload-alt"
@ -118,7 +137,7 @@
<template #header> <template #header>
<span>{{ $t('task.attachment.delete') }}</span> <span>{{ $t('task.attachment.delete') }}</span>
</template> </template>
<template #text> <template #text>
<p> <p>
{{ $t('task.attachment.deleteText1', {filename: attachmentToDelete.file.name}) }}<br/> {{ $t('task.attachment.deleteText1', {filename: attachmentToDelete.file.name}) }}<br/>
@ -138,13 +157,14 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {ref, shallowReactive, computed, type PropType} from 'vue' import {ref, shallowReactive, computed} from 'vue'
import {useDropZone} from '@vueuse/core' import {useDropZone} from '@vueuse/core'
import User from '@/components/misc/user.vue' import User from '@/components/misc/user.vue'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import AttachmentService from '@/services/attachment' import AttachmentService from '@/services/attachment'
import {SUPPORTED_IMAGE_SUFFIX} from '@/models/attachment'
import type AttachmentModel from '@/models/attachment' import type AttachmentModel from '@/models/attachment'
import type {IAttachment} from '@/modelTypes/IAttachment' import type {IAttachment} from '@/modelTypes/IAttachment'
import type {ITask} from '@/modelTypes/ITask' import type {ITask} from '@/modelTypes/ITask'
@ -155,38 +175,44 @@ import {uploadFiles, generateAttachmentUrl} from '@/helpers/attachments'
import {getHumanSize} from '@/helpers/getHumanSize' import {getHumanSize} from '@/helpers/getHumanSize'
import {useCopyToClipboard} from '@/composables/useCopyToClipboard' import {useCopyToClipboard} from '@/composables/useCopyToClipboard'
import {error, success} from '@/message' import {error, success} from '@/message'
import {useTaskStore} from '@/stores/tasks'
import {useI18n} from 'vue-i18n'
const props = defineProps({ const taskStore = useTaskStore()
taskId: { const {t} = useI18n({useScope: 'global'})
type: Number as PropType<ITask['id']>,
required: true, const props = withDefaults(defineProps<{
}, task: ITask,
initialAttachments: { initialAttachments?: IAttachment[],
type: Array, editEnabled: boolean,
}, }>(), {
editEnabled: { editEnabled: true,
default: true,
},
}) })
// FIXME: this should go through the store
const emit = defineEmits(['task-changed'])
const attachmentService = shallowReactive(new AttachmentService()) const attachmentService = shallowReactive(new AttachmentService())
const attachmentStore = useAttachmentStore() const attachmentStore = useAttachmentStore()
const attachments = computed(() => attachmentStore.attachments) const attachments = computed(() => attachmentStore.attachments)
const loading = computed(() => attachmentService.loading || taskStore.isLoading)
function onDrop(files: File[] | null) { function onDrop(files: File[] | null) {
if (files && files.length !== 0) { if (files && files.length !== 0) {
uploadFilesToTask(files) uploadFilesToTask(files)
} }
} }
const { isOverDropZone } = useDropZone(document, onDrop) const {isOverDropZone} = useDropZone(document, onDrop)
function downloadAttachment(attachment: IAttachment) { function downloadAttachment(attachment: IAttachment) {
attachmentService.download(attachment) attachmentService.download(attachment)
} }
const filesRef = ref<HTMLInputElement | null>(null) const filesRef = ref<HTMLInputElement | null>(null)
function uploadNewAttachment() { function uploadNewAttachment() {
const files = filesRef.value?.files const files = filesRef.value?.files
@ -198,7 +224,7 @@ function uploadNewAttachment() {
} }
function uploadFilesToTask(files: File[] | FileList) { function uploadFilesToTask(files: File[] | FileList) {
uploadFiles(attachmentService, props.taskId, files) uploadFiles(attachmentService, props.task.id, files)
} }
const attachmentToDelete = ref<AttachmentModel | null>(null) const attachmentToDelete = ref<AttachmentModel | null>(null)
@ -217,16 +243,15 @@ async function deleteAttachment() {
attachmentStore.removeById(attachmentToDelete.value.id) attachmentStore.removeById(attachmentToDelete.value.id)
success(r) success(r)
setAttachmentToDelete(null) setAttachmentToDelete(null)
} catch(e) { } catch (e) {
error(e) error(e)
} }
} }
const attachmentImageBlobUrl = ref<string | null>(null) const attachmentImageBlobUrl = ref<string | null>(null)
const SUPPORTED_SUFFIX = ['.jpg', '.png', '.bmp', '.gif']
async function viewOrDownload(attachment: AttachmentModel) { async function viewOrDownload(attachment: AttachmentModel) {
if (SUPPORTED_SUFFIX.some((suffix) => attachment.file.name.endsWith(suffix)) ) { if (SUPPORTED_IMAGE_SUFFIX.some((suffix) => attachment.file.name.endsWith(suffix))) {
attachmentImageBlobUrl.value = await attachmentService.getBlobUrl(attachment) attachmentImageBlobUrl.value = await attachmentService.getBlobUrl(attachment)
} else { } else {
downloadAttachment(attachment) downloadAttachment(attachment)
@ -234,8 +259,15 @@ async function viewOrDownload(attachment: AttachmentModel) {
} }
const copy = useCopyToClipboard() const copy = useCopyToClipboard()
function copyUrl(attachment: IAttachment) { function copyUrl(attachment: IAttachment) {
copy(generateAttachmentUrl(props.taskId, attachment.id)) copy(generateAttachmentUrl(props.task.id, attachment.id))
}
async function setCoverImage(attachment: IAttachment | null) {
const task = await taskStore.setCoverImage(props.task, attachment)
emit('task-changed', task)
success({message: t('task.attachment.successfullyChangedCoverImage')})
} }
</script> </script>
@ -316,7 +348,7 @@ function copyUrl(attachment: IAttachment) {
height: auto; height: auto;
text-shadow: var(--shadow-md); text-shadow: var(--shadow-md);
animation: bounce 2s infinite; animation: bounce 2s infinite;
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
animation: none; animation: none;
} }
@ -338,7 +370,7 @@ function copyUrl(attachment: IAttachment) {
.attachment-info-meta { .attachment-info-meta {
display: flex; display: flex;
align-items: center; align-items: center;
:deep(.user) { :deep(.user) {
display: flex !important; display: flex !important;
align-items: center; align-items: center;
@ -348,7 +380,7 @@ function copyUrl(attachment: IAttachment) {
@media screen and (max-width: $mobile) { @media screen and (max-width: $mobile) {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
:deep(.user) { :deep(.user) {
margin: .5rem 0; margin: .5rem 0;
} }
@ -394,5 +426,13 @@ function copyUrl(attachment: IAttachment) {
} }
} }
.is-task-cover {
background: var(--primary);
color: var(--white);
padding: .25rem .35rem;
border-radius: 4px;
font-size: .75rem;
}
@include modal-transition(); @include modal-transition();
</style> </style>

View File

@ -1,34 +1,29 @@
<template> <template>
<div <Multiselect
tabindex="-1" :loading="listUserService.loading"
@focus="focus" :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 <template #tag="{item: user}">
:loading="listUserService.loading" <span class="assignee">
:placeholder="$t('task.assignee.placeholder')" <user :avatar-size="32" :show-username="false" :user="user"/>
:multiple="true" <BaseButton @click="removeAssignee(user)" class="remove-assignee" v-if="!disabled">
@search="findUser" <icon icon="times"/>
:search-results="foundUsers" </BaseButton>
@select="addAssignee" </span>
label="name" </template>
:select-placeholder="$t('task.assignee.selectPlaceholder')" </Multiselect>
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> </template>
<script setup lang="ts"> <script setup lang="ts">
import {ref, shallowReactive, watch, type PropType} from 'vue' import {ref, shallowReactive, watch, nextTick, type PropType} from 'vue'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import User from '@/components/misc/user.vue' import User from '@/components/misc/user.vue'
@ -41,32 +36,34 @@ import {success} from '@/message'
import {useTaskStore} from '@/stores/tasks' import {useTaskStore} from '@/stores/tasks'
import type {IUser} from '@/modelTypes/IUser' import type {IUser} from '@/modelTypes/IUser'
import {getDisplayName} from '@/models/user'
const props = defineProps({ const props = defineProps({
taskId: { taskId: {
type: Number, type: Number,
required: true, required: true,
}, },
listId: { listId: {
type: Number, type: Number,
required: true, required: true,
}, },
disabled: { disabled: {
default: false, default: false,
}, },
modelValue: { modelValue: {
type: Array as PropType<IUser[]>, type: Array as PropType<IUser[]>,
default: () => [], default: () => [],
}, },
}) })
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const taskStore = useTaskStore() const taskStore = useTaskStore()
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
const listUserService = shallowReactive(new ListUserService()) const listUserService = shallowReactive(new ListUserService())
const foundUsers = ref([]) const foundUsers = ref<IUser[]>([])
const assignees = ref<IUser[]>([]) const assignees = ref<IUser[]>([])
let isAdding = false
watch( watch(
() => props.modelValue, () => props.modelValue,
@ -80,9 +77,19 @@ watch(
) )
async function addAssignee(user: IUser) { async function addAssignee(user: IUser) {
await taskStore.addAssignee({user: user, taskId: props.taskId}) if (isAdding) {
emit('update:modelValue', assignees.value) return
success({message: t('task.assignee.assignSuccess')}) }
try {
nextTick(() => isAdding = true)
await taskStore.addAssignee({user: user, taskId: props.taskId})
emit('update:modelValue', assignees.value)
success({message: t('task.assignee.assignSuccess')})
} finally {
nextTick(() => isAdding = false)
}
} }
async function removeAssignee(user: IUser) { async function removeAssignee(user: IUser) {
@ -103,13 +110,14 @@ async function findUser(query: string) {
return 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 // 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 => { .map(u => {
// Users may not have a display name set, so we fall back on the username in that case // 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 return u
}) })
} }
@ -117,11 +125,6 @@ async function findUser(query: string) {
function clearAllFoundUsers() { function clearAllFoundUsers() {
foundUsers.value = [] foundUsers.value = []
} }
const multiselect = ref()
function focus() {
multiselect.value.focus()
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -11,62 +11,70 @@
@click.ctrl="() => toggleTaskDone(task)" @click.ctrl="() => toggleTaskDone(task)"
@click.meta="() => toggleTaskDone(task)" @click.meta="() => toggleTaskDone(task)"
> >
<span class="task-id"> <img
<Done class="kanban-card__done" :is-done="task.done" variant="small"/> v-if="coverImageBlobUrl"
<template v-if="task.identifier === ''"> :src="coverImageBlobUrl"
#{{ task.index }} alt=""
</template> class="cover-image"
<template v-else> />
{{ task.identifier }} <div class="p-2">
</template> <span class="task-id">
</span> <Done class="kanban-card__done" :is-done="task.done" variant="small"/>
<span <template v-if="task.identifier === ''">
:class="{'overdue': task.dueDate <= new Date() && !task.done}" #{{ task.index }}
class="due-date" </template>
v-if="task.dueDate > 0" <template v-else>
v-tooltip="formatDateLong(task.dueDate)"> {{ task.identifier }}
<span class="icon"> </template>
<icon :icon="['far', 'calendar-alt']"/>
</span> </span>
<time :datetime="formatISO(task.dueDate)"> <span
{{ formatDateSince(task.dueDate) }} :class="{'overdue': task.dueDate <= new Date() && !task.done}"
</time> class="due-date"
</span> v-if="task.dueDate > 0"
<h3>{{ task.title }}</h3> v-tooltip="formatDateLong(task.dueDate)">
<progress <span class="icon">
class="progress is-small" <icon :icon="['far', 'calendar-alt']"/>
v-if="task.percentDone > 0" </span>
:value="task.percentDone * 100" max="100"> <time :datetime="formatISO(task.dueDate)">
{{ task.percentDone * 100 }}% {{ formatDateSince(task.dueDate) }}
</progress> </time>
<div class="footer"> </span>
<labels :labels="task.labels"/> <h3>{{ task.title }}</h3>
<priority-label :priority="task.priority" :done="task.done"/> <progress
<div class="assignees" v-if="task.assignees.length > 0"> class="progress is-small"
<user v-if="task.percentDone > 0"
v-for="u in task.assignees" :value="task.percentDone * 100" max="100">
:avatar-size="24" {{ task.percentDone * 100 }}%
:key="task.id + 'assignee' + u.id" </progress>
:show-username="false" <div class="footer">
:user="u" <labels :labels="task.labels"/>
/> <priority-label :priority="task.priority" :done="task.done"/>
<div class="assignees" v-if="task.assignees.length > 0">
<user
v-for="u in task.assignees"
:avatar-size="24"
:key="task.id + 'assignee' + u.id"
:show-username="false"
:user="u"
/>
</div>
<checklist-summary :task="task"/>
<span class="icon" v-if="task.attachments.length > 0">
<icon icon="paperclip"/>
</span>
<span v-if="task.description" class="icon">
<icon icon="align-left"/>
</span>
<span class="icon" v-if="task.repeatAfter.amount > 0">
<icon icon="history"/>
</span>
</div> </div>
<checklist-summary :task="task"/>
<span class="icon" v-if="task.attachments.length > 0">
<icon icon="paperclip"/>
</span>
<span v-if="task.description" class="icon">
<icon icon="align-left"/>
</span>
<span class="icon" v-if="task.repeatAfter.amount > 0">
<icon icon="history"/>
</span>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {ref, computed} from 'vue' import {ref, computed, watch} from 'vue'
import {useRouter} from 'vue-router' import {useRouter} from 'vue-router'
import PriorityLabel from '@/components/tasks/partials/priorityLabel.vue' import PriorityLabel from '@/components/tasks/partials/priorityLabel.vue'
@ -77,6 +85,8 @@ import ChecklistSummary from './checklist-summary.vue'
import {TASK_DEFAULT_COLOR, getHexColor} from '@/models/task' import {TASK_DEFAULT_COLOR, getHexColor} from '@/models/task'
import type {ITask} from '@/modelTypes/ITask' import type {ITask} from '@/modelTypes/ITask'
import {SUPPORTED_IMAGE_SUFFIX} from '@/models/attachment'
import AttachmentService from '@/services/attachment'
import {formatDateLong, formatISO, formatDateSince} from '@/helpers/time/formatDate' import {formatDateLong, formatISO, formatDateSince} from '@/helpers/time/formatDate'
import {colorIsDark} from '@/helpers/color/colorIsDark' import {colorIsDark} from '@/helpers/color/colorIsDark'
@ -114,6 +124,29 @@ function openTaskDetail() {
state: {backdropView: router.currentRoute.value.fullPath}, state: {backdropView: router.currentRoute.value.fullPath},
}) })
} }
const coverImageBlobUrl = ref<string | null>(null)
async function maybeDownloadCoverImage() {
if (!props.task.coverImageAttachmentId) {
coverImageBlobUrl.value = null
return
}
const attachment = props.task.attachments.find(a => a.id === props.task.coverImageAttachmentId)
if (!attachment || !SUPPORTED_IMAGE_SUFFIX.some((suffix) => attachment.file.name.endsWith(suffix))) {
return
}
const attachmentService = new AttachmentService()
coverImageBlobUrl.value = await attachmentService.getBlobUrl(attachment)
}
watch(
() => props.task.coverImageAttachmentId,
maybeDownloadCoverImage,
{immediate: true},
)
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -125,12 +158,11 @@ $task-background: var(--white);
cursor: pointer; cursor: pointer;
box-shadow: var(--shadow-xs); box-shadow: var(--shadow-xs);
display: block; display: block;
border: 3px solid transparent;
font-size: .9rem; font-size: .9rem;
padding: .4rem;
border-radius: $radius; border-radius: $radius;
background: $task-background; background: $task-background;
overflow: hidden;
&.loader-container.is-loading::after { &.loader-container.is-loading::after {
width: 1.5rem; width: 1.5rem;

View File

@ -37,7 +37,11 @@
@create="createAndRelateTask" @create="createAndRelateTask"
> >
<template #searchResult="{option: task}"> <template #searchResult="{option: task}">
<span v-if="typeof task !== 'string'" class="search-result"> <span
v-if="typeof task !== 'string'"
class="search-result"
:class="{'is-strikethrough': task.done}"
>
<span <span
class="different-list" class="different-list"
v-if="task.listId !== listId" v-if="task.listId !== listId"

View File

@ -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')
})
})

View File

@ -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))
}

View File

@ -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)
}

View File

@ -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,
}
}

View File

@ -19,7 +19,7 @@ export function useRenewTokenOnFocus() {
authStore.renewToken() authStore.renewToken()
// Check if the token is still valid if the window gets focus again to maybe renew it // 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) { if (!authenticated.value) {
return return
} }
@ -29,8 +29,8 @@ export function useRenewTokenOnFocus() {
// If the token expiry is negative, it is already expired and we have no choice but to redirect // If the token expiry is negative, it is already expired and we have no choice but to redirect
// the user to the login page // the user to the login page
if (expiresIn < 0) { if (expiresIn < 0) {
authStore.checkAuth() await authStore.checkAuth()
router.push({name: 'user.login'}) await router.push({name: 'user.login'})
return return
} }

View File

@ -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')
})
})

View File

@ -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'
}

View File

@ -1,7 +1,7 @@
const LAST_VISITED_KEY = 'lastVisited' const LAST_VISITED_KEY = 'lastVisited'
export const saveLastVisited = (name: string, params: object) => { export const saveLastVisited = (name: string, params: object, query: object) => {
localStorage.setItem(LAST_VISITED_KEY, JSON.stringify({name, params})) localStorage.setItem(LAST_VISITED_KEY, JSON.stringify({name, params, query}))
} }
export const getLastVisited = () => { export const getLastVisited = () => {

View File

@ -8,7 +8,7 @@ import {i18n} from '@/i18n'
const locales = {en: enGB, de, ch: de, fr, ru} const locales = {en: enGB, de, ch: de, fr, ru}
const dateIsValid = date => { export function dateIsValid(date) {
if (date === null) { if (date === null) {
return false return false
} }

View File

@ -1,9 +1,9 @@
{ {
"home": { "home": {
"welcomeNight": "Good Night {username}", "welcomeNight": "Good Night {username}!",
"welcomeMorning": "Good Morning {username}", "welcomeMorning": "Good Morning {username}!",
"welcomeDay": "Hi {username}", "welcomeDay": "Hi {username}!",
"welcomeEvening": "Good Evening {username}", "welcomeEvening": "Good Evening {username}!",
"lastViewed": "Last viewed", "lastViewed": "Last viewed",
"list": { "list": {
"newText": "You can create a new list for your new tasks:", "newText": "You can create a new list for your new tasks:",
@ -169,6 +169,14 @@
"title": "List Title", "title": "List Title",
"color": "Color", "color": "Color",
"lists": "Lists", "lists": "Lists",
"list": {
"title": "List",
"add": "Add",
"addPlaceholder": "Add a new task…",
"empty": "This list is currently empty.",
"newTaskCta": "Create a new task.",
"editTask": "Edit Task"
},
"search": "Type to search for a list…", "search": "Type to search for a list…",
"searchSelect": "Click or press enter to select this list", "searchSelect": "Click or press enter to select this list",
"shared": "Shared Lists", "shared": "Shared Lists",
@ -270,14 +278,6 @@
"delete": "Delete" "delete": "Delete"
} }
}, },
"list": {
"title": "List",
"add": "Add",
"addPlaceholder": "Add a new task…",
"empty": "This list is currently empty.",
"newTaskCta": "Create a new task.",
"editTask": "Edit Task"
},
"gantt": { "gantt": {
"title": "Gantt", "title": "Gantt",
"showTasksWithoutDates": "Show tasks which don't have dates set", "showTasksWithoutDates": "Show tasks which don't have dates set",
@ -672,13 +672,23 @@
"updated": "Updated" "updated": "Updated"
}, },
"subscription": { "subscription": {
"subscribedThroughParent": "You can't unsubscribe here because you are subscribed to this {entity} through its {parent}.", "subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribed": "You are currently subscribed to this {entity} and will receive notifications for changes.", "subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"notSubscribed": "You are not subscribed to this {entity} and won't receive notifications for changes.", "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.",
"subscribe": "Subscribe", "subscribe": "Subscribe",
"unsubscribe": "Unsubscribe", "unsubscribe": "Unsubscribe",
"subscribeSuccess": "You are now subscribed to this {entity}", "subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccess": "You are now unsubscribed to this {entity}" "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"
}, },
"attachment": { "attachment": {
"title": "Attachments", "title": "Attachments",
@ -690,7 +700,11 @@
"deleteTooltip": "Delete this attachment", "deleteTooltip": "Delete this attachment",
"deleteText1": "Are you sure you want to delete the attachment {filename}?", "deleteText1": "Are you sure you want to delete the attachment {filename}?",
"copyUrl": "Copy URL", "copyUrl": "Copy URL",
"copyUrlTooltip": "Copy the url of this attachment for usage in text" "copyUrlTooltip": "Copy the url of this attachment for usage in text",
"setAsCover": "Make cover",
"unsetAsCover": "Remove cover",
"successfullyChangedCoverImage": "The cover image was successfully changed.",
"usedAsCover": "Cover image"
}, },
"comment": { "comment": {
"title": "Comments", "title": "Comments",
@ -839,6 +853,12 @@
"text1": "Are you sure you want to remove this user from the team?", "text1": "Are you sure you want to remove this user from the team?",
"text2": "They will lose access to all lists and namespaces this team has access to. This CANNOT BE UNDONE!", "text2": "They will lose access to all lists and namespaces this team has access to. This CANNOT BE UNDONE!",
"success": "The user was successfully deleted from the team." "success": "The user was successfully deleted from the team."
},
"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."
} }
}, },
"attributes": { "attributes": {

View File

@ -1,9 +1,9 @@
{ {
"home": { "home": {
"welcomeNight": "Dobrou noc {username}", "welcomeNight": "Dobrou noc {username}!",
"welcomeMorning": "Dobré ráno {username}", "welcomeMorning": "Dobré ráno {username}!",
"welcomeDay": "Ahoj {username}", "welcomeDay": "Ahoj {username}!",
"welcomeEvening": "Dobrý večer {username}", "welcomeEvening": "Dobrý večer {username}!",
"lastViewed": "Naposledy zobrazeno", "lastViewed": "Naposledy zobrazeno",
"list": { "list": {
"newText": "Můžete vytvořit nový seznam pro své nové úkoly:", "newText": "Můžete vytvořit nový seznam pro své nové úkoly:",
@ -77,7 +77,7 @@
"newName": "Nové jméno", "newName": "Nové jméno",
"savedSuccess": "Nastavení bylo úspěšně aktualizováno.", "savedSuccess": "Nastavení bylo úspěšně aktualizováno.",
"emailReminders": "Posílat mi připomenutí pro úkoly e-mailem", "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", "discoverableByName": "Nechat ostatní uživatele mě najít podle jména",
"discoverableByEmail": "Nechat ostatní uživatele mě najít podle e-mailu", "discoverableByEmail": "Nechat ostatní uživatele mě najít podle e-mailu",
"playSoundWhenDone": "Přehrát zvuk při označení úkolů jako hotovo", "playSoundWhenDone": "Přehrát zvuk při označení úkolů jako hotovo",
@ -87,7 +87,7 @@
"language": "Jazyk", "language": "Jazyk",
"defaultList": "Výchozí seznam", "defaultList": "Výchozí seznam",
"timezone": "Časové pásmo", "timezone": "Časové pásmo",
"overdueTasksRemindersTime": "Overdue tasks reminder email time" "overdueTasksRemindersTime": "Čas odeslání emailu o zpožděných úkolech"
}, },
"totp": { "totp": {
"title": "Dvoufaktorové ověření", "title": "Dvoufaktorové ověření",
@ -169,10 +169,18 @@
"title": "Název seznamu", "title": "Název seznamu",
"color": "Barva", "color": "Barva",
"lists": "Seznamy", "lists": "Seznamy",
"list": {
"title": "Seznam",
"add": "Přidat",
"addPlaceholder": "Přidat nový úkol…",
"empty": "Tento seznam je nyní prázdný.",
"newTaskCta": "Vytvořit nový úkol.",
"editTask": "Upravit úkol"
},
"search": "Začni psát pro vyhledání seznamu…", "search": "Začni psát pro vyhledání seznamu…",
"searchSelect": "Klikněte nebo stiskněte Enter pro výběr tohoto seznamu", "searchSelect": "Klikněte nebo stiskněte Enter pro výběr tohoto seznamu",
"shared": "Sdílené seznamy", "shared": "Sdílené seznamy",
"noDescriptionAvailable": "No list description is available.", "noDescriptionAvailable": "Popis seznamu není k dispozici.",
"create": { "create": {
"header": "Nový seznam", "header": "Nový seznam",
"titlePlaceholder": "Název seznamu přijde sem…", "titlePlaceholder": "Název seznamu přijde sem…",
@ -244,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!", "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.", "createSuccess": "Sdílený odkaz byl úspěšně vytvořen.",
"deleteSuccess": "Sdílený odkaz byl úspěšně smazán", "deleteSuccess": "Sdílený odkaz byl úspěšně smazán",
"view": "View", "view": "Zobrazit",
"sharedBy": "Shared by {0}" "sharedBy": "Sdíleno {0}"
}, },
"userTeam": { "userTeam": {
"typeUser": "uživatel | uživatelé", "typeUser": "uživatel | uživatelé",
@ -270,14 +278,6 @@
"delete": "Smazat" "delete": "Smazat"
} }
}, },
"list": {
"title": "Seznam",
"add": "Přidat",
"addPlaceholder": "Přidat nový úkol…",
"empty": "Tento seznam je nyní prázdný.",
"newTaskCta": "Vytvořit nový úkol.",
"editTask": "Upravit úkol"
},
"gantt": { "gantt": {
"title": "Gantt", "title": "Gantt",
"showTasksWithoutDates": "Zobrazit úkoly, které nemají nastavené datum", "showTasksWithoutDates": "Zobrazit úkoly, které nemají nastavené datum",
@ -297,23 +297,23 @@
"title": "Kanban", "title": "Kanban",
"limit": "Limit: {limit}", "limit": "Limit: {limit}",
"noLimit": "Nenastaveno", "noLimit": "Nenastaveno",
"doneBucket": "Kbelík \"Hotovo\"", "doneBucket": "Sloupec \"Hotovo\"",
"doneBucketHint": "Všechny úkoly přesunuté do tohoto kbelíku budou automaticky označeny jako dokončené.", "doneBucketHint": "Všechny úkoly přesunuté do tohoto sloupce 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é.", "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": "Kbelík \"Hotovo\" byl úspěšně uložen.", "doneBucketSavedSuccess": "Sloupec \"Hotovo\" byl úspěšně uložen.",
"deleteLast": "Nelze odstranit poslední kbelík.", "deleteLast": "Nelze odstranit poslední sloupec.",
"addTaskPlaceholder": "Zadejte nový název úkolu…", "addTaskPlaceholder": "Zadejte nový název úkolu…",
"addTask": "Přidat úkol", "addTask": "Přidat úkol",
"addAnotherTask": "Přidat další úkol", "addAnotherTask": "Přidat další úkol",
"addBucket": "Vytvořit nový kbelík", "addBucket": "Vytvořit nový sloupec",
"addBucketPlaceholder": "Zadejte název nového kbelíku…", "addBucketPlaceholder": "Zadejte název nového sloupce…",
"deleteHeaderBucket": "Smazat kbelík", "deleteHeaderBucket": "Smazat sloupec",
"deleteBucketText1": "Opravdu chcete smazat tento kbelík?", "deleteBucketText1": "Opravdu chcete smazat tento sloupec?",
"deleteBucketText2": "Toto neodstraní žádné úkoly, ale přesune je do výchozího kbelíku.", "deleteBucketText2": "Toto nesmaže žádné úkoly, ale přesune je do výchozího sloupce.",
"deleteBucketSuccess": "Kbelík byl úspěšně smazán.", "deleteBucketSuccess": "Sloupec byl úspěšně smazán.",
"bucketTitleSavedSuccess": "Název kbelíku byl úspěšně uložen.", "bucketTitleSavedSuccess": "Název sloupce byl úspěšně uložen.",
"bucketLimitSavedSuccess": "Limit kbelíku byl úspěšně uložen.", "bucketLimitSavedSuccess": "Limit sloupce byl úspěšně uložen.",
"collapse": "Sbalit tento kbelík" "collapse": "Sbalit tento sloupec"
}, },
"pseudo": { "pseudo": {
"favorites": { "favorites": {
@ -672,13 +672,23 @@
"updated": "Aktualizováno" "updated": "Aktualizováno"
}, },
"subscription": { "subscription": {
"subscribedThroughParent": "Zde se nemůžete odhlásit, protože jste přihlášeni k odběru {entity} prostřednictvím jeho {parent}.", "subscribedListThroughParentNamespace": "Zde se nemůžete odhlásit, protože jste přihlášeni k odběru tohoto seznamu prostřednictvím jeho prostoru.",
"subscribed": "Aktuálně jste přihlášeni k odběru {entity} a budete dostávat oznámení o změnách.", "subscribedTaskThroughParentNamespace": "Zde se nemůžete odhlásit, protože jste přihlášeni k odběru tohoto úkolu prostřednictvím jeho prostoru.",
"notSubscribed": "Nejste přihlášeni k odběru {entity} a nebudete dostávat upozornění na změny.", "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", "subscribe": "Odebírat",
"unsubscribe": "Odhlásit odběr", "unsubscribe": "Odhlásit odběr",
"subscribeSuccess": "Nyní jste přihlášeni k odběru {entity}", "subscribeSuccessNamespace": "Nyní jste přihlášeni k tomuto prostoru",
"unsubscribeSuccess": "Nyní jste odhlášeni od odběru {entity}" "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": { "attachment": {
"title": "Přílohy", "title": "Přílohy",
@ -690,7 +700,11 @@
"deleteTooltip": "Smazat tuto přílohu", "deleteTooltip": "Smazat tuto přílohu",
"deleteText1": "Opravdu chcete odstranit přílohu {filename}?", "deleteText1": "Opravdu chcete odstranit přílohu {filename}?",
"copyUrl": "Kopírovat URL", "copyUrl": "Kopírovat URL",
"copyUrlTooltip": "Kopírovat URL této přílohy pro použití v textu" "copyUrlTooltip": "Kopírovat URL této přílohy pro použití v textu",
"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": { "comment": {
"title": "Komentáře", "title": "Komentáře",
@ -701,7 +715,7 @@
"comment": "Komentář", "comment": "Komentář",
"delete": "Smazat tento komentář", "delete": "Smazat tento komentář",
"deleteText1": "Opravdu chcete 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." "addedSuccess": "Komentář byl úspěšně přidán."
}, },
"deferDueDate": { "deferDueDate": {
@ -728,16 +742,16 @@
"removeSuccess": "Štítek byl úspěšně odstraněn.", "removeSuccess": "Štítek byl úspěšně odstraněn.",
"addCreateSuccess": "Štítek byl úspěšně vytvořen a přidán.", "addCreateSuccess": "Štítek byl úspěšně vytvořen a přidán.",
"delete": { "delete": {
"header": "Delete this label", "header": "Smazat tento štítek",
"text1": "Are you sure you want to delete this label?", "text1": "Opravdu chcete smazat tento štítek?",
"text2": "This will remove it from all tasks and cannot be restored." "text2": "Odebere štítek ze všech úkolů a nelze to vrátit zpět."
} }
}, },
"priority": { "priority": {
"unset": "Zrušit nastavení", "unset": "Zrušit nastavení",
"low": "Nízká", "low": "Nízká",
"medium": "Střední", "medium": "Střední",
"high": "High", "high": "Vysoká",
"urgent": "Naléhavé", "urgent": "Naléhavé",
"doNow": "UDĚLEJ TO HNED" "doNow": "UDĚLEJ TO HNED"
}, },
@ -781,7 +795,7 @@
"weeks": "Týdny", "weeks": "Týdny",
"months": "Měsíce", "months": "Měsíce",
"years": "Roky", "years": "Roky",
"invalidAmount": "Please enter more than 0." "invalidAmount": "Zadejte prosím více než 0."
}, },
"quickAddMagic": { "quickAddMagic": {
"hint": "Můžeš použít Kouzelné rychlé přidání", "hint": "Můžeš použít Kouzelné rychlé přidání",
@ -791,15 +805,15 @@
"multiple": "Toto můžete použít několikrát.", "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}.", "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.", "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\".", "label4": "Například: {prefix}\"Štítek s mezerami\".",
"priority1": "Chcete-li nastavit prioritu úkolu, přidejte číslo 1-5, s prefixem {prefix}.", "priority1": "Chcete-li nastavit prioritu úkolu, přidejte číslo 1-5, s prefixem {prefix}.",
"priority2": "Čím vyšší číslo, tím vyšší priorita.", "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}.", "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}.", "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.", "list2": "Toto vrátí chybu, pokud seznam neexistuje.",
"list3": "To use spaces, simply add a \" or ' around the list name.", "list3": "Chcete-li použít mezery, uzavřete název seznamu do \" nebo '.",
"list4": "For example: {prefix}\"List with spaces\".", "list4": "Například: {prefix}\"Štítek s mezerami\".",
"dateAndTime": "Datum a čas", "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ů:", "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", "dateWeekday": "každý pracovní den použije další datum s tímto datem",
@ -839,6 +853,12 @@
"text1": "Opravdu chcete odebrat tohoto uživatele z týmu?", "text1": "Opravdu chcete odebrat tohoto uživatele z týmu?",
"text2": "Ztratí přístup ke všem seznamům a prostorům, k nimž má tento tým přístup. To NEMŮŽE BÝT VZATO ZPĚT!", "text2": "Ztratí přístup ke všem seznamům a prostorům, k nimž má tento tým přístup. To NEMŮŽE BÝT VZATO ZPĚT!",
"success": "Uživatel byl úspěšně odstraněn z týmu." "success": "Uživatel byl úspěšně odstraněn z týmu."
},
"leave": {
"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": { "attributes": {
@ -870,8 +890,8 @@
"related": "Upravit související úkoly tohoto úkolu", "related": "Upravit související úkoly tohoto úkolu",
"color": "Změnit barvu tohoto úkolu", "color": "Změnit barvu tohoto úkolu",
"move": "Přesunout tento úkol do jiného seznamu", "move": "Přesunout tento úkol do jiného seznamu",
"reminder": "Manage reminders of this task", "reminder": "Spravovat připomenutí této úlohy",
"description": "Toggle editing of the task description" "description": "Přepnout úpravy popisu úkolu"
}, },
"list": { "list": {
"title": "Zobrazení seznamů", "title": "Zobrazení seznamů",
@ -943,7 +963,7 @@
} }
}, },
"date": { "date": {
"locale": "cs-CZ", "locale": "cs",
"altFormatLong": "j M Y H:i", "altFormatLong": "j M Y H:i",
"altFormatShort": "j M Y" "altFormatShort": "j M Y"
}, },
@ -1013,11 +1033,11 @@
"8002": "Štítek neexistuje.", "8002": "Štítek neexistuje.",
"8003": "K tomuto štítku nemáte přístup.", "8003": "K tomuto štítku nemáte přístup.",
"9001": "Právo je neplatné.", "9001": "Právo je neplatné.",
"10001": "Kbelík neexistuje.", "10001": "Sloupec neexistuje.",
"10002": "Kbelík nepatří do tohoto seznamu.", "10002": "Sloupec nepatří do tohoto seznamu.",
"10003": "Poslední kbelík v seznamu nelze odstranit.", "10003": "Poslední sloupec 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.", "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": "Na seznam může být pouze jeden kbelík \"Hotovo\".", "10005": "Pro seznam může být pouze jeden sloupec \"Hotovo\".",
"11001": "Uložený filtr neexistuje.", "11001": "Uložený filtr neexistuje.",
"11002": "Uložené filtry nejsou k dispozici pro sdílení odkazů.", "11002": "Uložené filtry nejsou k dispozici pro sdílení odkazů.",
"12001": "Typ předplatného je neplatný.", "12001": "Typ předplatného je neplatný.",

View File

@ -1,9 +1,9 @@
{ {
"home": { "home": {
"welcomeNight": "Gute Nacht, {username}", "welcomeNight": "Gute Nacht, {username}!",
"welcomeMorning": "Guten Morgen, {username}", "welcomeMorning": "Guten Morgen, {username}!",
"welcomeDay": "Hallo, {username}", "welcomeDay": "Hallo {username}!",
"welcomeEvening": "Guten Abend, {username}", "welcomeEvening": "Guten Abend, {username}!",
"lastViewed": "Zuletzt angesehen", "lastViewed": "Zuletzt angesehen",
"list": { "list": {
"newText": "Du kannst eine neue Liste für deine neuen Aufgaben erstellen:", "newText": "Du kannst eine neue Liste für deine neuen Aufgaben erstellen:",
@ -169,6 +169,14 @@
"title": "Listentitel", "title": "Listentitel",
"color": "Farbe", "color": "Farbe",
"lists": "Listen", "lists": "Listen",
"list": {
"title": "Liste",
"add": "Hinzufügen",
"addPlaceholder": "Eine neue Aufgabe hinzufügen …",
"empty": "Diese Liste ist derzeit leer.",
"newTaskCta": "Eine neue Aufgabe erstellen.",
"editTask": "Aufgabe bearbeiten"
},
"search": "Tippe, um nach einer Liste zu suchen…", "search": "Tippe, um nach einer Liste zu suchen…",
"searchSelect": "Klicke auf oder drücke die Eingabetaste, um diese Liste auszuwählen", "searchSelect": "Klicke auf oder drücke die Eingabetaste, um diese Liste auszuwählen",
"shared": "Geteilte Listen", "shared": "Geteilte Listen",
@ -270,14 +278,6 @@
"delete": "Löschen" "delete": "Löschen"
} }
}, },
"list": {
"title": "Liste",
"add": "Hinzufügen",
"addPlaceholder": "Eine neue Aufgabe hinzufügen …",
"empty": "Diese Liste ist derzeit leer.",
"newTaskCta": "Eine neue Aufgabe erstellen.",
"editTask": "Aufgabe bearbeiten"
},
"gantt": { "gantt": {
"title": "Gantt", "title": "Gantt",
"showTasksWithoutDates": "Aufgaben anzeigen, für die keine Daten festgelegt sind", "showTasksWithoutDates": "Aufgaben anzeigen, für die keine Daten festgelegt sind",
@ -335,7 +335,7 @@
"create": { "create": {
"title": "Neuer Namespace", "title": "Neuer Namespace",
"titleRequired": "Bitte gebe einen Titel an.", "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?", "tooltip": "Was ist ein Namespace?",
"success": "Der Namespace wurde erfolgreich erstellt." "success": "Der Namespace wurde erfolgreich erstellt."
}, },
@ -672,13 +672,23 @@
"updated": "Aktualisiert" "updated": "Aktualisiert"
}, },
"subscription": { "subscription": {
"subscribedThroughParent": "Du kannst hier nicht de-abonnieren, da du diese {entity} durch {parent} abonniert hast.", "subscribedListThroughParentNamespace": "Du kannst hier nicht de-abonnieren, da du diese Liste über ihren Namespace abonniert hast.",
"subscribed": "Du erhältst Benachrichtigungen zu dieser {entity}.", "subscribedTaskThroughParentNamespace": "Du kannst hier nicht de-abonnieren, da du diese Aufgabe über ihren Namespace abonniert hast.",
"notSubscribed": "Du hast diese {entity} nicht abonniert und erhältst keine Benachrichtigungen über Änderungen.", "subscribedTaskThroughParentList": "Du kannst hier nicht de-abonnieren, da du diese Aufgabe über ihre Liste abonniert hast.",
"subscribedNamespace": "Du hast diesen Namespace abonniert und erhältst Benachrichtigungen über Änderungen.",
"notSubscribedNamespace": "Du hast diesen Namespace nicht abonniert und erhältst keine Benachrichtigungen über Änderungen.",
"subscribedList": "Du hast diese Liste abonniert und erhältst Benachrichtigungen über Änderungen.",
"notSubscribedList": "Du hast diese Liste nicht abonniert und erhältst keine Benachrichtigungen über Änderungen.",
"subscribedTask": "Du hast diese Aufgabe abonniert und erhältst Benachrichtigungen über Änderungen.",
"notSubscribedTask": "Du hast diese Aufgabe nicht abonniert und erhältst keine Benachrichtigungen über Änderungen.",
"subscribe": "Abonnieren", "subscribe": "Abonnieren",
"unsubscribe": "Abbestellen", "unsubscribe": "Abbestellen",
"subscribeSuccess": "Du hast jetzt diese {entity} abonniert", "subscribeSuccessNamespace": "Du hast diesen Namespace jetzt abonniert",
"unsubscribeSuccess": "Du hast diese {entity} jetzt abbestellt" "unsubscribeSuccessNamespace": "Du hast diesen Namespace jetzt nicht mehr abonniert",
"subscribeSuccessList": "Du hast diese Liste jetzt abonniert",
"unsubscribeSuccessList": "Du hast diese Liste jetzt nicht mehr abonniert",
"subscribeSuccessTask": "Du hast diese Aufgabe jetzt abonniert",
"unsubscribeSuccessTask": "Du hast diese Aufgabe jetzt nicht mehr abonniert"
}, },
"attachment": { "attachment": {
"title": "Anhänge", "title": "Anhänge",
@ -690,7 +700,11 @@
"deleteTooltip": "Diesen Anhang löschen", "deleteTooltip": "Diesen Anhang löschen",
"deleteText1": "Soll der Anhang {filename} gelöscht werden?", "deleteText1": "Soll der Anhang {filename} gelöscht werden?",
"copyUrl": "URL kopieren", "copyUrl": "URL kopieren",
"copyUrlTooltip": "Die URL dieses Anhangs zur Verwendung im Text kopieren" "copyUrlTooltip": "Die URL dieses Anhangs zur Verwendung im Text kopieren",
"setAsCover": "Als Titelbild setzen",
"unsetAsCover": "Titelbild entfernen",
"successfullyChangedCoverImage": "Das Titelbild wurde erfolgreich geändert.",
"usedAsCover": "Titelbild"
}, },
"comment": { "comment": {
"title": "Kommentare", "title": "Kommentare",
@ -839,6 +853,12 @@
"text1": "Bist du sicher, dass du diese:n Benutzer:in aus dem Team entfernen willst?", "text1": "Bist du sicher, dass du diese:n Benutzer:in aus dem Team entfernen willst?",
"text2": "Diese:r Benutzer:in verliert den Zugriff auf alle Listen und Namespaces auf die dieses Team Zugriff hat. Dies kann nicht rückgängig gemacht werden!", "text2": "Diese:r Benutzer:in verliert den Zugriff auf alle Listen und Namespaces auf die dieses Team Zugriff hat. Dies kann nicht rückgängig gemacht werden!",
"success": "Der:die Benutzer:in wurde erfolgreich aus dem Team gelöscht." "success": "Der:die Benutzer:in wurde erfolgreich aus dem Team gelöscht."
},
"leave": {
"title": "Team verlassen",
"text1": "Bist du sicher, dass du dieses Team verlassen willst?",
"text2": "Du wirst Zugriff auf alle Listen und Namespaces verlieren, auf die dieses Team Zugriff hat. Wenn du deine Meinung änderst, musst du durch einen Team-Admin wieder hinzugefügt werden.",
"success": "Du hast das Team erfolgreich verlassen."
} }
}, },
"attributes": { "attributes": {

View File

@ -1,9 +1,9 @@
{ {
"home": { "home": {
"welcomeNight": "Guet Nacht, {username}", "welcomeNight": "Gute Nacht, {username}!",
"welcomeMorning": "Guete Morgä, {username}", "welcomeMorning": "Guten Morgen, {username}!",
"welcomeDay": "Hoi {username}", "welcomeDay": "Hallo {username}!",
"welcomeEvening": "Guete Abig, {username}", "welcomeEvening": "Guten Abend, {username}!",
"lastViewed": "Zletscht ahglueget", "lastViewed": "Zletscht ahglueget",
"list": { "list": {
"newText": "Du chasch e Liste für dini neue Uufgabe erstelle:", "newText": "Du chasch e Liste für dini neue Uufgabe erstelle:",
@ -169,6 +169,14 @@
"title": "Liste Titl", "title": "Liste Titl",
"color": "Farb", "color": "Farb",
"lists": "Listene", "lists": "Listene",
"list": {
"title": "Liste",
"add": "Hinzuefüege",
"addPlaceholder": "E neui Uufgab erstelle…",
"empty": "D'Liste isch momentan leer.",
"newTaskCta": "Neui Uufgab erstelle.",
"editTask": "Uufgab bearbeite"
},
"search": "Schriib, um nachere Liste z'sueche…", "search": "Schriib, um nachere Liste z'sueche…",
"searchSelect": "Druck uf Enter um die Liste uuszwähle", "searchSelect": "Druck uf Enter um die Liste uuszwähle",
"shared": "Teilti Liste", "shared": "Teilti Liste",
@ -270,14 +278,6 @@
"delete": "Chüble" "delete": "Chüble"
} }
}, },
"list": {
"title": "Liste",
"add": "Hinzuefüege",
"addPlaceholder": "E neui Uufgab erstelle…",
"empty": "D'Liste isch momentan leer.",
"newTaskCta": "Neui Uufgab erstelle.",
"editTask": "Uufgab bearbeite"
},
"gantt": { "gantt": {
"title": "Gantt", "title": "Gantt",
"showTasksWithoutDates": "Zeig Uufgabe, wo kei Date hend", "showTasksWithoutDates": "Zeig Uufgabe, wo kei Date hend",
@ -335,7 +335,7 @@
"create": { "create": {
"title": "Neuer Namespace", "title": "Neuer Namespace",
"titleRequired": "Bitte gib en Titl ah.", "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?", "tooltip": "Was isch en Namensruum?",
"success": "Namensruum erstellt." "success": "Namensruum erstellt."
}, },
@ -672,13 +672,23 @@
"updated": "Aktualisiert" "updated": "Aktualisiert"
}, },
"subscription": { "subscription": {
"subscribedThroughParent": "Du chasch da nid deabonnierä, weil du zu dere {entity} durch {parent} dezue abonniert bisch.", "subscribedListThroughParentNamespace": "Du kannst hier nicht de-abonnieren, da du diese Liste über ihren Namespace abonniert hast.",
"subscribed": "Du bisch momentan zu dere {entity} abonniert und bechunsch Benachrichtigunge für Änderige.", "subscribedTaskThroughParentNamespace": "Du kannst hier nicht de-abonnieren, da du diese Aufgabe über ihren Namespace abonniert hast.",
"notSubscribed": "Du bisch momentan nid zu dere {entity} abonniert und bechunsch kei Benachrichtigunge für Änderige.", "subscribedTaskThroughParentList": "Du kannst hier nicht de-abonnieren, da du diese Aufgabe über ihre Liste abonniert hast.",
"subscribedNamespace": "Du hast diesen Namespace abonniert und erhältst Benachrichtigungen über Änderungen.",
"notSubscribedNamespace": "Du hast diesen Namespace nicht abonniert und erhältst keine Benachrichtigungen über Änderungen.",
"subscribedList": "Du hast diese Liste abonniert und erhältst Benachrichtigungen über Änderungen.",
"notSubscribedList": "Du hast diese Liste nicht abonniert und erhältst keine Benachrichtigungen über Änderungen.",
"subscribedTask": "Du hast diese Aufgabe abonniert und erhältst Benachrichtigungen über Änderungen.",
"notSubscribedTask": "Du hast diese Aufgabe nicht abonniert und erhältst keine Benachrichtigungen über Änderungen.",
"subscribe": "Abooniere", "subscribe": "Abooniere",
"unsubscribe": "Deabonniere", "unsubscribe": "Deabonniere",
"subscribeSuccess": "Du hesch die {entity} abonniert", "subscribeSuccessNamespace": "Du hast diesen Namespace jetzt abonniert",
"unsubscribeSuccess": "Du hesch die {entity} deabonniert" "unsubscribeSuccessNamespace": "Du hast diesen Namespace jetzt nicht mehr abonniert",
"subscribeSuccessList": "Du hast diese Liste jetzt abonniert",
"unsubscribeSuccessList": "Du hast diese Liste jetzt nicht mehr abonniert",
"subscribeSuccessTask": "Du hast diese Aufgabe jetzt abonniert",
"unsubscribeSuccessTask": "Du hast diese Aufgabe jetzt nicht mehr abonniert"
}, },
"attachment": { "attachment": {
"title": "Aahhäng", "title": "Aahhäng",
@ -690,7 +700,11 @@
"deleteTooltip": "De Aahhang lösche", "deleteTooltip": "De Aahhang lösche",
"deleteText1": "Bisch du dir sicher, dass du de Aahang {filename} lösche wetsch?", "deleteText1": "Bisch du dir sicher, dass du de Aahang {filename} lösche wetsch?",
"copyUrl": "URL Kopierä", "copyUrl": "URL Kopierä",
"copyUrlTooltip": "D'Url vo dem Aahang kopiere, um sie im Text zbruuche" "copyUrlTooltip": "D'Url vo dem Aahang kopiere, um sie im Text zbruuche",
"setAsCover": "Als Titelbild setzen",
"unsetAsCover": "Titelbild entfernen",
"successfullyChangedCoverImage": "Das Titelbild wurde erfolgreich geändert.",
"usedAsCover": "Titelbild"
}, },
"comment": { "comment": {
"title": "Kommentär", "title": "Kommentär",
@ -839,6 +853,12 @@
"text1": "Bisch du dir sicher, dass du de Benutzer usm Team werfe wetsch?", "text1": "Bisch du dir sicher, dass du de Benutzer usm Team werfe wetsch?",
"text2": "Diese:r Benutzer:in verliert den Zugriff auf alle Listen und Namespaces auf die dieses Team Zugriff hat. Dies kann nicht rückgängig gemacht werden!", "text2": "Diese:r Benutzer:in verliert den Zugriff auf alle Listen und Namespaces auf die dieses Team Zugriff hat. Dies kann nicht rückgängig gemacht werden!",
"success": "Benutzer erfolgriich usegworfe." "success": "Benutzer erfolgriich usegworfe."
},
"leave": {
"title": "Team verlassen",
"text1": "Bist du sicher, dass du dieses Team verlassen willst?",
"text2": "Du wirst Zugriff auf alle Listen und Namespaces verlieren, auf die dieses Team Zugriff hat. Wenn du deine Meinung änderst, musst du durch einen Team-Admin wieder hinzugefügt werden.",
"success": "Du hast das Team erfolgreich verlassen."
} }
}, },
"attributes": { "attributes": {

View File

@ -1,9 +1,9 @@
{ {
"home": { "home": {
"welcomeNight": "Good Night {username}", "welcomeNight": "Good Night {username}!",
"welcomeMorning": "Good Morning {username}", "welcomeMorning": "Good Morning {username}!",
"welcomeDay": "Hi {username}", "welcomeDay": "Hi {username}!",
"welcomeEvening": "Good Evening {username}", "welcomeEvening": "Good Evening {username}!",
"lastViewed": "Last viewed", "lastViewed": "Last viewed",
"list": { "list": {
"newText": "You can create a new list for your new tasks:", "newText": "You can create a new list for your new tasks:",
@ -169,6 +169,7 @@
"title": "List Title", "title": "List Title",
"color": "Color", "color": "Color",
"lists": "Lists", "lists": "Lists",
"list": "List",
"search": "Type to search for a list…", "search": "Type to search for a list…",
"searchSelect": "Click or press enter to select this list", "searchSelect": "Click or press enter to select this list",
"shared": "Shared Lists", "shared": "Shared Lists",
@ -675,13 +676,23 @@
"updated": "Updated" "updated": "Updated"
}, },
"subscription": { "subscription": {
"subscribedThroughParent": "You can't unsubscribe here because you are subscribed to this {entity} through its {parent}.", "subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribed": "You are currently subscribed to this {entity} and will receive notifications for changes.", "subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"notSubscribed": "You are not subscribed to this {entity} and won't receive notifications for changes.", "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.",
"subscribe": "Subscribe", "subscribe": "Subscribe",
"unsubscribe": "Unsubscribe", "unsubscribe": "Unsubscribe",
"subscribeSuccess": "You are now subscribed to this {entity}", "subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccess": "You are now unsubscribed to this {entity}" "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"
}, },
"attachment": { "attachment": {
"title": "Attachments", "title": "Attachments",
@ -693,7 +704,11 @@
"deleteTooltip": "Delete this attachment", "deleteTooltip": "Delete this attachment",
"deleteText1": "Are you sure you want to delete the attachment {filename}?", "deleteText1": "Are you sure you want to delete the attachment {filename}?",
"copyUrl": "Copy URL", "copyUrl": "Copy URL",
"copyUrlTooltip": "Copy the url of this attachment for usage in text" "copyUrlTooltip": "Copy the url of this attachment for usage in text",
"setAsCover": "Make cover",
"unsetAsCover": "Remove cover",
"successfullyChangedCoverImage": "The cover image was successfully changed.",
"usedAsCover": "Cover image"
}, },
"comment": { "comment": {
"title": "Comments", "title": "Comments",
@ -842,6 +857,12 @@
"text1": "Are you sure you want to remove this user from the team?", "text1": "Are you sure you want to remove this user from the team?",
"text2": "They will lose access to all lists and namespaces this team has access to. This CANNOT BE UNDONE!", "text2": "They will lose access to all lists and namespaces this team has access to. This CANNOT BE UNDONE!",
"success": "The user was successfully deleted from the team." "success": "The user was successfully deleted from the team."
},
"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."
} }
}, },
"attributes": { "attributes": {

View File

@ -1,9 +1,9 @@
{ {
"home": { "home": {
"welcomeNight": "Good Night {username}", "welcomeNight": "Good Night {username}!",
"welcomeMorning": "Good Morning {username}", "welcomeMorning": "Good Morning {username}!",
"welcomeDay": "Hi {username}", "welcomeDay": "Hi {username}!",
"welcomeEvening": "Good Evening {username}", "welcomeEvening": "Good Evening {username}!",
"lastViewed": "Last viewed", "lastViewed": "Last viewed",
"list": { "list": {
"newText": "You can create a new list for your new tasks:", "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.", "confirmEmailSuccess": "You successfully confirmed your email! You can log in now.",
"totpTitle": "Two Factor Authentication Code", "totpTitle": "Two Factor Authentication Code",
"totpPlaceholder": "e.g. 123456", "totpPlaceholder": "e.g. 123456",
"login": "Login", "login": "Iniciar sesión",
"createAccount": "Create account", "createAccount": "Create account",
"loginWith": "Log in with {provider}", "loginWith": "Log in with {provider}",
"authenticating": "Authenticating…", "authenticating": "Authenticating…",
@ -169,6 +169,14 @@
"title": "List Title", "title": "List Title",
"color": "Color", "color": "Color",
"lists": "Lists", "lists": "Lists",
"list": {
"title": "List",
"add": "Add",
"addPlaceholder": "Add a new task…",
"empty": "This list is currently empty.",
"newTaskCta": "Create a new task.",
"editTask": "Edit Task"
},
"search": "Type to search for a list…", "search": "Type to search for a list…",
"searchSelect": "Click or press enter to select this list", "searchSelect": "Click or press enter to select this list",
"shared": "Shared Lists", "shared": "Shared Lists",
@ -270,14 +278,6 @@
"delete": "Delete" "delete": "Delete"
} }
}, },
"list": {
"title": "List",
"add": "Add",
"addPlaceholder": "Add a new task…",
"empty": "This list is currently empty.",
"newTaskCta": "Create a new task.",
"editTask": "Edit Task"
},
"gantt": { "gantt": {
"title": "Gantt", "title": "Gantt",
"showTasksWithoutDates": "Show tasks which don't have dates set", "showTasksWithoutDates": "Show tasks which don't have dates set",
@ -672,13 +672,23 @@
"updated": "Updated" "updated": "Updated"
}, },
"subscription": { "subscription": {
"subscribedThroughParent": "You can't unsubscribe here because you are subscribed to this {entity} through its {parent}.", "subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribed": "You are currently subscribed to this {entity} and will receive notifications for changes.", "subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"notSubscribed": "You are not subscribed to this {entity} and won't receive notifications for changes.", "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.",
"subscribe": "Subscribe", "subscribe": "Subscribe",
"unsubscribe": "Unsubscribe", "unsubscribe": "Unsubscribe",
"subscribeSuccess": "You are now subscribed to this {entity}", "subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccess": "You are now unsubscribed to this {entity}" "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"
}, },
"attachment": { "attachment": {
"title": "Attachments", "title": "Attachments",
@ -690,7 +700,11 @@
"deleteTooltip": "Delete this attachment", "deleteTooltip": "Delete this attachment",
"deleteText1": "Are you sure you want to delete the attachment {filename}?", "deleteText1": "Are you sure you want to delete the attachment {filename}?",
"copyUrl": "Copy URL", "copyUrl": "Copy URL",
"copyUrlTooltip": "Copy the url of this attachment for usage in text" "copyUrlTooltip": "Copy the url of this attachment for usage in text",
"setAsCover": "Make cover",
"unsetAsCover": "Remove cover",
"successfullyChangedCoverImage": "The cover image was successfully changed.",
"usedAsCover": "Cover image"
}, },
"comment": { "comment": {
"title": "Comments", "title": "Comments",
@ -839,6 +853,12 @@
"text1": "Are you sure you want to remove this user from the team?", "text1": "Are you sure you want to remove this user from the team?",
"text2": "They will lose access to all lists and namespaces this team has access to. This CANNOT BE UNDONE!", "text2": "They will lose access to all lists and namespaces this team has access to. This CANNOT BE UNDONE!",
"success": "The user was successfully deleted from the team." "success": "The user was successfully deleted from the team."
},
"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."
} }
}, },
"attributes": { "attributes": {

View File

@ -1,9 +1,9 @@
{ {
"home": { "home": {
"welcomeNight": "Bonne nuit {username}", "welcomeNight": "Good Night {username}!",
"welcomeMorning": "Bonjour {username}", "welcomeMorning": "Good Morning {username}!",
"welcomeDay": "Salut {username}", "welcomeDay": "Hi {username}!",
"welcomeEvening": "Bonsoir {username}", "welcomeEvening": "Good Evening {username}!",
"lastViewed": "Dernière consultation", "lastViewed": "Dernière consultation",
"list": { "list": {
"newText": "Tu peux créer une nouvelle liste pour tes nouvelles tâches :", "newText": "Tu peux créer une nouvelle liste pour tes nouvelles tâches :",
@ -169,6 +169,14 @@
"title": "Nom de la liste", "title": "Nom de la liste",
"color": "Couleur", "color": "Couleur",
"lists": "Listes", "lists": "Listes",
"list": {
"title": "Liste",
"add": "Ajouter",
"addPlaceholder": "Ajouter une nouvelle tâche…",
"empty": "Cette liste est actuellement vide.",
"newTaskCta": "Créer une nouvelle tâche.",
"editTask": "Modifier la tâche"
},
"search": "Écris pour rechercher une liste…", "search": "Écris pour rechercher une liste…",
"searchSelect": "Clique ou appuie sur la touche Entrée pour sélectionner cette liste", "searchSelect": "Clique ou appuie sur la touche Entrée pour sélectionner cette liste",
"shared": "Listes partagées", "shared": "Listes partagées",
@ -270,14 +278,6 @@
"delete": "Supprimer" "delete": "Supprimer"
} }
}, },
"list": {
"title": "Liste",
"add": "Ajouter",
"addPlaceholder": "Ajouter une nouvelle tâche…",
"empty": "Cette liste est actuellement vide.",
"newTaskCta": "Créer une nouvelle tâche.",
"editTask": "Modifier la tâche"
},
"gantt": { "gantt": {
"title": "Gantt", "title": "Gantt",
"showTasksWithoutDates": "Afficher les tâches pour lesquelles aucune date na été fixée", "showTasksWithoutDates": "Afficher les tâches pour lesquelles aucune date na été fixée",
@ -672,13 +672,23 @@
"updated": "Mis à jour" "updated": "Mis à jour"
}, },
"subscription": { "subscription": {
"subscribedThroughParent": "Tu ne peux pas te désabonner ici car tu es abonné·e à cette {entity} par le biais de son {parent}.", "subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribed": "Tu es actuellement abonné·e à cette {entity} et recevras des notifications pour les changements.", "subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"notSubscribed": "Tu nes pas abonné·e à cette {entity} et ne recevras pas de notifications pour les changements.", "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.",
"subscribe": "Sabonner", "subscribe": "Sabonner",
"unsubscribe": "Se désabonner", "unsubscribe": "Se désabonner",
"subscribeSuccess": "Tu es maintenant abonné·e à cette {entity}", "subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccess": "Tu es maintenant désabonné·e de cette {entity}" "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"
}, },
"attachment": { "attachment": {
"title": "Pièces jointes", "title": "Pièces jointes",
@ -690,7 +700,11 @@
"deleteTooltip": "Supprimer cette pièce jointe", "deleteTooltip": "Supprimer cette pièce jointe",
"deleteText1": "Supprimer la pièce jointe {filename} ?", "deleteText1": "Supprimer la pièce jointe {filename} ?",
"copyUrl": "Copier lURL", "copyUrl": "Copier lURL",
"copyUrlTooltip": "Copier lURL de cette pièce jointe pour lutiliser dans le texte" "copyUrlTooltip": "Copier lURL de cette pièce jointe pour lutiliser dans le texte",
"setAsCover": "Make cover",
"unsetAsCover": "Remove cover",
"successfullyChangedCoverImage": "The cover image was successfully changed.",
"usedAsCover": "Cover image"
}, },
"comment": { "comment": {
"title": "Commentaires", "title": "Commentaires",
@ -839,6 +853,12 @@
"text1": "Retirer cette personne de léquipe ?", "text1": "Retirer cette personne de léquipe ?",
"text2": "Ils perdront l'accès à toutes les listes et espaces de noms auxquels cette équipe a accès. Ceci NE PEUT PAS ÊTRE ANNULÉ !", "text2": "Ils perdront l'accès à toutes les listes et espaces de noms auxquels cette équipe a accès. Ceci NE PEUT PAS ÊTRE ANNULÉ !",
"success": "Utilisateur·rice retiré·e de léquipe." "success": "Utilisateur·rice retiré·e de léquipe."
},
"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."
} }
}, },
"attributes": { "attributes": {

View File

@ -1,9 +1,9 @@
{ {
"home": { "home": {
"welcomeNight": "Buonanotte {username}", "welcomeNight": "Good Night {username}!",
"welcomeMorning": "Buongiorno {username}", "welcomeMorning": "Good Morning {username}!",
"welcomeDay": "Ciao {username}", "welcomeDay": "Hi {username}!",
"welcomeEvening": "Buonasera {username}", "welcomeEvening": "Good Evening {username}!",
"lastViewed": "Ultima visualizzazione", "lastViewed": "Ultima visualizzazione",
"list": { "list": {
"newText": "È possibile creare una nuova lista per le nuove attività:", "newText": "È possibile creare una nuova lista per le nuove attività:",
@ -169,6 +169,14 @@
"title": "Titolo della Lista", "title": "Titolo della Lista",
"color": "Colore", "color": "Colore",
"lists": "Liste", "lists": "Liste",
"list": {
"title": "Lista",
"add": "Aggiungi",
"addPlaceholder": "Aggiungi una nuova attività…",
"empty": "Questa lista è attualmente vuota.",
"newTaskCta": "Crea una nuova attività.",
"editTask": "Modifica Attività"
},
"search": "Digita per cercare una lista…", "search": "Digita per cercare una lista…",
"searchSelect": "Fare clic o premere invio per selezionare questa lista", "searchSelect": "Fare clic o premere invio per selezionare questa lista",
"shared": "Liste Condivise", "shared": "Liste Condivise",
@ -270,14 +278,6 @@
"delete": "Elimina" "delete": "Elimina"
} }
}, },
"list": {
"title": "Lista",
"add": "Aggiungi",
"addPlaceholder": "Aggiungi una nuova attività…",
"empty": "Questa lista è attualmente vuota.",
"newTaskCta": "Crea una nuova attività.",
"editTask": "Modifica Attività"
},
"gantt": { "gantt": {
"title": "Gantt", "title": "Gantt",
"showTasksWithoutDates": "Mostra attività che non hanno date impostate", "showTasksWithoutDates": "Mostra attività che non hanno date impostate",
@ -672,13 +672,23 @@
"updated": "Aggiornato" "updated": "Aggiornato"
}, },
"subscription": { "subscription": {
"subscribedThroughParent": "Non puoi annullare l'iscrizione qui perché sei iscritto a questo {entity} attraverso il suo {parent}.", "subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribed": "Sei attualmente iscritto a questo {entity} e riceverai notifiche per le modifiche.", "subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"notSubscribed": "Non sei iscritto a questo {entity} e non riceverai notifiche per le modifiche.", "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.",
"subscribe": "Iscriviti", "subscribe": "Iscriviti",
"unsubscribe": "Disiscriviti", "unsubscribe": "Disiscriviti",
"subscribeSuccess": "Ti sei iscritto a questo {entity}", "subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccess": "Ti sei disiscritto a questo {entity}" "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"
}, },
"attachment": { "attachment": {
"title": "Allegati", "title": "Allegati",
@ -690,7 +700,11 @@
"deleteTooltip": "Elimina questo allegato", "deleteTooltip": "Elimina questo allegato",
"deleteText1": "Sei sicuro di voler eliminare l'allegato {filename}?", "deleteText1": "Sei sicuro di voler eliminare l'allegato {filename}?",
"copyUrl": "Copia URL", "copyUrl": "Copia URL",
"copyUrlTooltip": "Copia l'URL di questo allegato per usarlo nel testo" "copyUrlTooltip": "Copia l'URL di questo allegato per usarlo nel testo",
"setAsCover": "Make cover",
"unsetAsCover": "Remove cover",
"successfullyChangedCoverImage": "The cover image was successfully changed.",
"usedAsCover": "Cover image"
}, },
"comment": { "comment": {
"title": "Commenti", "title": "Commenti",
@ -839,6 +853,12 @@
"text1": "Confermi di voler rimuovere questo utente dal gruppo?", "text1": "Confermi di voler rimuovere questo utente dal gruppo?",
"text2": "Perderanno l'accesso a tutte le liste e i namespace a cui questo gruppo ha accesso. NON PUÒ ESSERE RIPRISTINATO!", "text2": "Perderanno l'accesso a tutte le liste e i namespace a cui questo gruppo ha accesso. NON PUÒ ESSERE RIPRISTINATO!",
"success": "Utente rimosso dal gruppo." "success": "Utente rimosso dal gruppo."
},
"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."
} }
}, },
"attributes": { "attributes": {

View File

@ -1,9 +1,9 @@
{ {
"home": { "home": {
"welcomeNight": "Goede nacht {username}", "welcomeNight": "Good Night {username}!",
"welcomeMorning": "Goedemorgen {username}", "welcomeMorning": "Good Morning {username}!",
"welcomeDay": "Hallo {username}", "welcomeDay": "Hi {username}!",
"welcomeEvening": "Goedenavond {username}", "welcomeEvening": "Good Evening {username}!",
"lastViewed": "Laatst bekeken", "lastViewed": "Laatst bekeken",
"list": { "list": {
"newText": "Je kan een nieuwe lijst maken voor je nieuwe taken:", "newText": "Je kan een nieuwe lijst maken voor je nieuwe taken:",
@ -169,6 +169,14 @@
"title": "Lijst titel", "title": "Lijst titel",
"color": "Kleur", "color": "Kleur",
"lists": "Lijsten", "lists": "Lijsten",
"list": {
"title": "Lijst",
"add": "Toevoegen",
"addPlaceholder": "Voeg een nieuwe taak toe…",
"empty": "Deze lijst is momenteel leeg.",
"newTaskCta": "Creëer een nieuwe taak.",
"editTask": "Taak bewerken"
},
"search": "Typ om naar een lijst te zoeken…", "search": "Typ om naar een lijst te zoeken…",
"searchSelect": "Klik of druk op enter om deze lijst te selecteren", "searchSelect": "Klik of druk op enter om deze lijst te selecteren",
"shared": "Gedeelde lijsten", "shared": "Gedeelde lijsten",
@ -270,14 +278,6 @@
"delete": "Verwijderen" "delete": "Verwijderen"
} }
}, },
"list": {
"title": "Lijst",
"add": "Toevoegen",
"addPlaceholder": "Voeg een nieuwe taak toe…",
"empty": "Deze lijst is momenteel leeg.",
"newTaskCta": "Creëer een nieuwe taak.",
"editTask": "Taak bewerken"
},
"gantt": { "gantt": {
"title": "Gantt", "title": "Gantt",
"showTasksWithoutDates": "Toon taken waarvoor geen datums zijn ingesteld", "showTasksWithoutDates": "Toon taken waarvoor geen datums zijn ingesteld",
@ -672,13 +672,23 @@
"updated": "Bijgewerkt" "updated": "Bijgewerkt"
}, },
"subscription": { "subscription": {
"subscribedThroughParent": "You can't unsubscribe here because you are subscribed to this {entity} through its {parent}.", "subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribed": "You are currently subscribed to this {entity} and will receive notifications for changes.", "subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"notSubscribed": "You are not subscribed to this {entity} and won't receive notifications for changes.", "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.",
"subscribe": "Subscribe", "subscribe": "Subscribe",
"unsubscribe": "Unsubscribe", "unsubscribe": "Unsubscribe",
"subscribeSuccess": "You are now subscribed to this {entity}", "subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccess": "You are now unsubscribed to this {entity}" "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"
}, },
"attachment": { "attachment": {
"title": "Bijlagen", "title": "Bijlagen",
@ -690,7 +700,11 @@
"deleteTooltip": "Verwijder deze bijlage", "deleteTooltip": "Verwijder deze bijlage",
"deleteText1": "Weet je zeker dat je de bijlage {filename} wilt verwijderen?", "deleteText1": "Weet je zeker dat je de bijlage {filename} wilt verwijderen?",
"copyUrl": "URL kopiëren", "copyUrl": "URL kopiëren",
"copyUrlTooltip": "Copy the url of this attachment for usage in text" "copyUrlTooltip": "Copy the url of this attachment for usage in text",
"setAsCover": "Make cover",
"unsetAsCover": "Remove cover",
"successfullyChangedCoverImage": "The cover image was successfully changed.",
"usedAsCover": "Cover image"
}, },
"comment": { "comment": {
"title": "Reacties", "title": "Reacties",
@ -839,6 +853,12 @@
"text1": "Weet je zeker dat je deze gebruiker wilt verwijderen uit het team?", "text1": "Weet je zeker dat je deze gebruiker wilt verwijderen uit het team?",
"text2": "They will lose access to all lists and namespaces this team has access to. This CANNOT BE UNDONE!", "text2": "They will lose access to all lists and namespaces this team has access to. This CANNOT BE UNDONE!",
"success": "The user was successfully deleted from the team." "success": "The user was successfully deleted from the team."
},
"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."
} }
}, },
"attributes": { "attributes": {

View File

@ -1,9 +1,9 @@
{ {
"home": { "home": {
"welcomeNight": "Dobrej nocy {username}", "welcomeNight": "Good Night {username}!",
"welcomeMorning": "Dzień dobry {username}", "welcomeMorning": "Good Morning {username}!",
"welcomeDay": "Cześć {username}", "welcomeDay": "Hi {username}!",
"welcomeEvening": "Dobry wieczór {username}", "welcomeEvening": "Good Evening {username}!",
"lastViewed": "Ostatnio oglądane", "lastViewed": "Ostatnio oglądane",
"list": { "list": {
"newText": "Możesz stworzyć nową listę dla swoich nowych zadań:", "newText": "Możesz stworzyć nową listę dla swoich nowych zadań:",
@ -169,6 +169,14 @@
"title": "Tytuł listy", "title": "Tytuł listy",
"color": "Kolor", "color": "Kolor",
"lists": "Listy", "lists": "Listy",
"list": {
"title": "Lista",
"add": "Dodaj",
"addPlaceholder": "Dodaj nowe zadanie…",
"empty": "Ta lista jest obecnie pusta.",
"newTaskCta": "Utwórz nowe zadanie.",
"editTask": "Edytuj zadanie"
},
"search": "Wpisz, aby wyszukać listę…", "search": "Wpisz, aby wyszukać listę…",
"searchSelect": "Kliknij lub naciśnij Enter, aby wybrać tę listę", "searchSelect": "Kliknij lub naciśnij Enter, aby wybrać tę listę",
"shared": "Współdzielone listy", "shared": "Współdzielone listy",
@ -270,14 +278,6 @@
"delete": "Usuń" "delete": "Usuń"
} }
}, },
"list": {
"title": "Lista",
"add": "Dodaj",
"addPlaceholder": "Dodaj nowe zadanie…",
"empty": "Ta lista jest obecnie pusta.",
"newTaskCta": "Utwórz nowe zadanie.",
"editTask": "Edytuj zadanie"
},
"gantt": { "gantt": {
"title": "Gantt", "title": "Gantt",
"showTasksWithoutDates": "Pokaż zadania, które nie mają ustawionych dat", "showTasksWithoutDates": "Pokaż zadania, które nie mają ustawionych dat",
@ -672,13 +672,23 @@
"updated": "Zaktualizowano" "updated": "Zaktualizowano"
}, },
"subscription": { "subscription": {
"subscribedThroughParent": "Nie możesz zrezygnować z subskrypcji, ponieważ subskrybujesz {entity} za pośrednictwem {parent}.", "subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribed": "Obecnie subskrybujesz {entity} i będziesz otrzymywać powiadomienia o zmianach.", "subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"notSubscribed": "Nie subskrybujesz {entity} i nie będziesz otrzymywać powiadomień o zmianach.", "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.",
"subscribe": "Subskrybuj", "subscribe": "Subskrybuj",
"unsubscribe": "Anuluj subskrypcję", "unsubscribe": "Anuluj subskrypcję",
"subscribeSuccess": "Od teraz subskrybujesz {entity}", "subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccess": "Już nie subskrybujesz {entity}" "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"
}, },
"attachment": { "attachment": {
"title": "Załączniki", "title": "Załączniki",
@ -690,7 +700,11 @@
"deleteTooltip": "Usuń ten załącznik", "deleteTooltip": "Usuń ten załącznik",
"deleteText1": "Czy na pewno chcesz usunąć załącznik {filename}?", "deleteText1": "Czy na pewno chcesz usunąć załącznik {filename}?",
"copyUrl": "Kopiuj URL", "copyUrl": "Kopiuj URL",
"copyUrlTooltip": "Skopiuj adres URL tego załącznika do użycia w tekście" "copyUrlTooltip": "Skopiuj adres URL tego załącznika do użycia w tekście",
"setAsCover": "Make cover",
"unsetAsCover": "Remove cover",
"successfullyChangedCoverImage": "The cover image was successfully changed.",
"usedAsCover": "Cover image"
}, },
"comment": { "comment": {
"title": "Komentarze", "title": "Komentarze",
@ -839,6 +853,12 @@
"text1": "Czy na pewno chcesz usunąć tego użytkownika z zespołu?", "text1": "Czy na pewno chcesz usunąć tego użytkownika z zespołu?",
"text2": "Utraci on dostęp do wszystkich list i sekcji, do których ma dostęp ten zespół. Tego NIE DA SIĘ COFNĄĆ!", "text2": "Utraci on dostęp do wszystkich list i sekcji, do których ma dostęp ten zespół. Tego NIE DA SIĘ COFNĄĆ!",
"success": "Użytkownik został pomyślnie usunięty z zespołu." "success": "Użytkownik został pomyślnie usunięty z zespołu."
},
"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."
} }
}, },
"attributes": { "attributes": {

View File

@ -1,9 +1,9 @@
{ {
"home": { "home": {
"welcomeNight": "Good Night {username}", "welcomeNight": "Good Night {username}!",
"welcomeMorning": "Good Morning {username}", "welcomeMorning": "Good Morning {username}!",
"welcomeDay": "Hi {username}", "welcomeDay": "Hi {username}!",
"welcomeEvening": "Good Evening {username}", "welcomeEvening": "Good Evening {username}!",
"lastViewed": "Last viewed", "lastViewed": "Last viewed",
"list": { "list": {
"newText": "You can create a new list for your new tasks:", "newText": "You can create a new list for your new tasks:",
@ -169,6 +169,14 @@
"title": "List Title", "title": "List Title",
"color": "Color", "color": "Color",
"lists": "Lists", "lists": "Lists",
"list": {
"title": "List",
"add": "Add",
"addPlaceholder": "Add a new task…",
"empty": "This list is currently empty.",
"newTaskCta": "Create a new task.",
"editTask": "Edit Task"
},
"search": "Type to search for a list…", "search": "Type to search for a list…",
"searchSelect": "Click or press enter to select this list", "searchSelect": "Click or press enter to select this list",
"shared": "Shared Lists", "shared": "Shared Lists",
@ -270,14 +278,6 @@
"delete": "Delete" "delete": "Delete"
} }
}, },
"list": {
"title": "List",
"add": "Add",
"addPlaceholder": "Add a new task…",
"empty": "This list is currently empty.",
"newTaskCta": "Create a new task.",
"editTask": "Edit Task"
},
"gantt": { "gantt": {
"title": "Gantt", "title": "Gantt",
"showTasksWithoutDates": "Show tasks which don't have dates set", "showTasksWithoutDates": "Show tasks which don't have dates set",
@ -672,13 +672,23 @@
"updated": "Updated" "updated": "Updated"
}, },
"subscription": { "subscription": {
"subscribedThroughParent": "You can't unsubscribe here because you are subscribed to this {entity} through its {parent}.", "subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribed": "You are currently subscribed to this {entity} and will receive notifications for changes.", "subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"notSubscribed": "You are not subscribed to this {entity} and won't receive notifications for changes.", "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.",
"subscribe": "Subscribe", "subscribe": "Subscribe",
"unsubscribe": "Unsubscribe", "unsubscribe": "Unsubscribe",
"subscribeSuccess": "You are now subscribed to this {entity}", "subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccess": "You are now unsubscribed to this {entity}" "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"
}, },
"attachment": { "attachment": {
"title": "Attachments", "title": "Attachments",
@ -690,7 +700,11 @@
"deleteTooltip": "Delete this attachment", "deleteTooltip": "Delete this attachment",
"deleteText1": "Are you sure you want to delete the attachment {filename}?", "deleteText1": "Are you sure you want to delete the attachment {filename}?",
"copyUrl": "Copy URL", "copyUrl": "Copy URL",
"copyUrlTooltip": "Copy the url of this attachment for usage in text" "copyUrlTooltip": "Copy the url of this attachment for usage in text",
"setAsCover": "Make cover",
"unsetAsCover": "Remove cover",
"successfullyChangedCoverImage": "The cover image was successfully changed.",
"usedAsCover": "Cover image"
}, },
"comment": { "comment": {
"title": "Comments", "title": "Comments",
@ -839,6 +853,12 @@
"text1": "Are you sure you want to remove this user from the team?", "text1": "Are you sure you want to remove this user from the team?",
"text2": "They will lose access to all lists and namespaces this team has access to. This CANNOT BE UNDONE!", "text2": "They will lose access to all lists and namespaces this team has access to. This CANNOT BE UNDONE!",
"success": "The user was successfully deleted from the team." "success": "The user was successfully deleted from the team."
},
"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."
} }
}, },
"attributes": { "attributes": {

View File

@ -1,9 +1,9 @@
{ {
"home": { "home": {
"welcomeNight": "Boa Noite {username}", "welcomeNight": "Boa Noite {username}!",
"welcomeMorning": "Bom Dia {username}", "welcomeMorning": "Bom Dia {username}!",
"welcomeDay": "Olá {username}", "welcomeDay": "Olá {username}!",
"welcomeEvening": "Boa Tarde {username}", "welcomeEvening": "Boa Tarde {username}!",
"lastViewed": "Visto recentemente", "lastViewed": "Visto recentemente",
"list": { "list": {
"newText": "Podes criar uma nova lista para as tuas novas tarefas:", "newText": "Podes criar uma nova lista para as tuas novas tarefas:",
@ -169,6 +169,14 @@
"title": "Título da Lista", "title": "Título da Lista",
"color": "Cor", "color": "Cor",
"lists": "Listas", "lists": "Listas",
"list": {
"title": "Lista",
"add": "Adicionar",
"addPlaceholder": "Adicionar uma nova tarefa…",
"empty": "Esta lista está atualmente vazia.",
"newTaskCta": "Cria uma nova tarefa.",
"editTask": "Editar Tarefa"
},
"search": "Escreve para pesquisar por uma lista…", "search": "Escreve para pesquisar por uma lista…",
"searchSelect": "Clica ou pressiona Enter para selecionar esta lista", "searchSelect": "Clica ou pressiona Enter para selecionar esta lista",
"shared": "Listas Partilhadas", "shared": "Listas Partilhadas",
@ -270,14 +278,6 @@
"delete": "Eliminar" "delete": "Eliminar"
} }
}, },
"list": {
"title": "Lista",
"add": "Adicionar",
"addPlaceholder": "Adicionar uma nova tarefa…",
"empty": "Esta lista está atualmente vazia.",
"newTaskCta": "Cria uma nova tarefa.",
"editTask": "Editar Tarefa"
},
"gantt": { "gantt": {
"title": "Gantt", "title": "Gantt",
"showTasksWithoutDates": "Mostrar tarefas que não têm datas atríbuidas", "showTasksWithoutDates": "Mostrar tarefas que não têm datas atríbuidas",
@ -672,13 +672,23 @@
"updated": "Atualizado" "updated": "Atualizado"
}, },
"subscription": { "subscription": {
"subscribedThroughParent": "Não podes cancelar a tua subscrição aqui porque estás subscrito nesta {entity} através de {parent}.", "subscribedListThroughParentNamespace": "Não podes cancelar a tua subscrição aqui porque estás subscrito nesta lista através do seu espaço.",
"subscribed": "Estás atualmente subscrito a esta {entity} e serás notificado de alterações.", "subscribedTaskThroughParentNamespace": "Não podes cancelar a tua subscrição aqui porque estás subscrito nesta tarefa através do seu espaço.",
"notSubscribed": "Não estás subscrito a esta {entity} e não serás notificado de alterações.", "subscribedTaskThroughParentList": "Não podes cancelar a tua subscrição aqui porque estás subscrito nesta tarefa através da sua lista.",
"subscribedNamespace": "Estás atualmente subscrito a este espaço e serás notificado de alterações.",
"notSubscribedNamespace": "Não estás subscrito a este espaço e não serás notificado de alterações.",
"subscribedList": "Estás atualmente subscrito a esta lista e serás notificado de alterações.",
"notSubscribedList": "Não estás subscrito a esta lista e não serás notificado de alterações.",
"subscribedTask": "Estás atualmente subscrito a esta tarefa e serás notificado de alterações.",
"notSubscribedTask": "Não estás subscrito a esta tarefa e não serás notificado de alterações.",
"subscribe": "Subscrever", "subscribe": "Subscrever",
"unsubscribe": "Remover Subscrição", "unsubscribe": "Remover Subscrição",
"subscribeSuccess": "Estás agora subscrito a esta {entity}", "subscribeSuccessNamespace": "Estás agora subscrito a este espaço",
"unsubscribeSuccess": "Não estás mais subcrito a esta {entity}" "unsubscribeSuccessNamespace": "Não estás mais subcrito a este espaço",
"subscribeSuccessList": "Estás agora subscrito a esta lista",
"unsubscribeSuccessList": "Não estás mais subcrito a esta lista",
"subscribeSuccessTask": "Estás agora subscrito a esta tarefa",
"unsubscribeSuccessTask": "Não estás mais subcrito a esta tarefa"
}, },
"attachment": { "attachment": {
"title": "Anexos", "title": "Anexos",
@ -690,7 +700,11 @@
"deleteTooltip": "Eliminar este anexo", "deleteTooltip": "Eliminar este anexo",
"deleteText1": "Tens a certeza que pretendes eliminar o anexo {filename}?", "deleteText1": "Tens a certeza que pretendes eliminar o anexo {filename}?",
"copyUrl": "Copiar URL", "copyUrl": "Copiar URL",
"copyUrlTooltip": "Copia o url deste anexo para o utilizar no texto" "copyUrlTooltip": "Copia o url deste anexo para o utilizar no texto",
"setAsCover": "Criar capa",
"unsetAsCover": "Remover capa",
"successfullyChangedCoverImage": "A imagem de capa foi alterada com sucesso.",
"usedAsCover": "Imagem de capa"
}, },
"comment": { "comment": {
"title": "Comentários", "title": "Comentários",
@ -839,6 +853,12 @@
"text1": "Tens a certeza que pretendes remover este utilizador da equipa?", "text1": "Tens a certeza que pretendes remover este utilizador da equipa?",
"text2": "Eles perderão o acesso a todas as listas e espaços a que esta equipa tem acesso. Isto NÃO PODER SER REVERTIDO!", "text2": "Eles perderão o acesso a todas as listas e espaços a que esta equipa tem acesso. Isto NÃO PODER SER REVERTIDO!",
"success": "O utilizador foi removido da equipa com sucesso." "success": "O utilizador foi removido da equipa com sucesso."
},
"leave": {
"title": "Sair da equipa",
"text1": "Tens a certeza de que queres sair desta equipa?",
"text2": "Vais perder acesso a todas as listas e espaços a que esta equipa tem acesso. Se mudares de ideias, vais necessitar que um administrador da equipa te adicione novamente.",
"success": "Saíste da equipa com sucesso."
} }
}, },
"attributes": { "attributes": {

View File

@ -1,9 +1,9 @@
{ {
"home": { "home": {
"welcomeNight": "Good Night {username}", "welcomeNight": "Good Night {username}!",
"welcomeMorning": "Good Morning {username}", "welcomeMorning": "Good Morning {username}!",
"welcomeDay": "Hi {username}", "welcomeDay": "Hi {username}!",
"welcomeEvening": "Good Evening {username}", "welcomeEvening": "Good Evening {username}!",
"lastViewed": "Last viewed", "lastViewed": "Last viewed",
"list": { "list": {
"newText": "You can create a new list for your new tasks:", "newText": "You can create a new list for your new tasks:",
@ -169,6 +169,14 @@
"title": "List Title", "title": "List Title",
"color": "Color", "color": "Color",
"lists": "Lists", "lists": "Lists",
"list": {
"title": "List",
"add": "Add",
"addPlaceholder": "Add a new task…",
"empty": "This list is currently empty.",
"newTaskCta": "Create a new task.",
"editTask": "Edit Task"
},
"search": "Type to search for a list…", "search": "Type to search for a list…",
"searchSelect": "Click or press enter to select this list", "searchSelect": "Click or press enter to select this list",
"shared": "Shared Lists", "shared": "Shared Lists",
@ -270,14 +278,6 @@
"delete": "Delete" "delete": "Delete"
} }
}, },
"list": {
"title": "List",
"add": "Add",
"addPlaceholder": "Add a new task…",
"empty": "This list is currently empty.",
"newTaskCta": "Create a new task.",
"editTask": "Edit Task"
},
"gantt": { "gantt": {
"title": "Gantt", "title": "Gantt",
"showTasksWithoutDates": "Show tasks which don't have dates set", "showTasksWithoutDates": "Show tasks which don't have dates set",
@ -672,13 +672,23 @@
"updated": "Updated" "updated": "Updated"
}, },
"subscription": { "subscription": {
"subscribedThroughParent": "You can't unsubscribe here because you are subscribed to this {entity} through its {parent}.", "subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribed": "You are currently subscribed to this {entity} and will receive notifications for changes.", "subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"notSubscribed": "You are not subscribed to this {entity} and won't receive notifications for changes.", "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.",
"subscribe": "Subscribe", "subscribe": "Subscribe",
"unsubscribe": "Unsubscribe", "unsubscribe": "Unsubscribe",
"subscribeSuccess": "You are now subscribed to this {entity}", "subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccess": "You are now unsubscribed to this {entity}" "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"
}, },
"attachment": { "attachment": {
"title": "Attachments", "title": "Attachments",
@ -690,7 +700,11 @@
"deleteTooltip": "Delete this attachment", "deleteTooltip": "Delete this attachment",
"deleteText1": "Are you sure you want to delete the attachment {filename}?", "deleteText1": "Are you sure you want to delete the attachment {filename}?",
"copyUrl": "Copy URL", "copyUrl": "Copy URL",
"copyUrlTooltip": "Copy the url of this attachment for usage in text" "copyUrlTooltip": "Copy the url of this attachment for usage in text",
"setAsCover": "Make cover",
"unsetAsCover": "Remove cover",
"successfullyChangedCoverImage": "The cover image was successfully changed.",
"usedAsCover": "Cover image"
}, },
"comment": { "comment": {
"title": "Comments", "title": "Comments",
@ -839,6 +853,12 @@
"text1": "Are you sure you want to remove this user from the team?", "text1": "Are you sure you want to remove this user from the team?",
"text2": "They will lose access to all lists and namespaces this team has access to. This CANNOT BE UNDONE!", "text2": "They will lose access to all lists and namespaces this team has access to. This CANNOT BE UNDONE!",
"success": "The user was successfully deleted from the team." "success": "The user was successfully deleted from the team."
},
"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."
} }
}, },
"attributes": { "attributes": {

View File

@ -1,9 +1,9 @@
{ {
"home": { "home": {
"welcomeNight": "Доброй ночи, {username}", "welcomeNight": "Good Night {username}!",
"welcomeMorning": "Доброе утро, {username}", "welcomeMorning": "Good Morning {username}!",
"welcomeDay": "Привет, {username}", "welcomeDay": "Hi {username}!",
"welcomeEvening": "Добрый вечер, {username}", "welcomeEvening": "Good Evening {username}!",
"lastViewed": "Последние просмотренные", "lastViewed": "Последние просмотренные",
"list": { "list": {
"newText": "Ты можешь создать новый список для своих задач:", "newText": "Ты можешь создать новый список для своих задач:",
@ -169,6 +169,14 @@
"title": "Название списка", "title": "Название списка",
"color": "Цвет", "color": "Цвет",
"lists": "Списки", "lists": "Списки",
"list": {
"title": "Список",
"add": "Добавить",
"addPlaceholder": "Добавить новую задачу…",
"empty": "Список сейчас пуст.",
"newTaskCta": "Создать новую задачу.",
"editTask": "Изменить задачу"
},
"search": "Введи запрос для поиска списка…", "search": "Введи запрос для поиска списка…",
"searchSelect": "Кликни или нажми Enter для выбора этого списка", "searchSelect": "Кликни или нажми Enter для выбора этого списка",
"shared": "Общие списки", "shared": "Общие списки",
@ -270,14 +278,6 @@
"delete": "Удалить" "delete": "Удалить"
} }
}, },
"list": {
"title": "Список",
"add": "Добавить",
"addPlaceholder": "Добавить новую задачу…",
"empty": "Список сейчас пуст.",
"newTaskCta": "Создать новую задачу.",
"editTask": "Изменить задачу"
},
"gantt": { "gantt": {
"title": "Гант", "title": "Гант",
"showTasksWithoutDates": "Показать задачи без установленной даты", "showTasksWithoutDates": "Показать задачи без установленной даты",
@ -672,13 +672,23 @@
"updated": "Дата изменения" "updated": "Дата изменения"
}, },
"subscription": { "subscription": {
"subscribedThroughParent": "Ты не можешь отписаться здесь, потому что ты подписан на {entity} через {parent}.", "subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribed": "Ты подписан на {entity} и будешь получать уведомления об изменениях.", "subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"notSubscribed": "Ты не подписан на {entity} и не будешь получать уведомления об изменениях.", "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.",
"subscribe": "Подписаться", "subscribe": "Подписаться",
"unsubscribe": "Отписаться", "unsubscribe": "Отписаться",
"subscribeSuccess": "Ты подписался на {entity}", "subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccess": "Ты отписался от {entity}" "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"
}, },
"attachment": { "attachment": {
"title": "Вложения", "title": "Вложения",
@ -690,7 +700,11 @@
"deleteTooltip": "Удалить это вложение", "deleteTooltip": "Удалить это вложение",
"deleteText1": "Удалить вложение {filename}?", "deleteText1": "Удалить вложение {filename}?",
"copyUrl": "Скопировать URL", "copyUrl": "Скопировать URL",
"copyUrlTooltip": "Скопировать ссылку на это вложение для использования в тексте" "copyUrlTooltip": "Скопировать ссылку на это вложение для использования в тексте",
"setAsCover": "Make cover",
"unsetAsCover": "Remove cover",
"successfullyChangedCoverImage": "The cover image was successfully changed.",
"usedAsCover": "Cover image"
}, },
"comment": { "comment": {
"title": "Комментарии", "title": "Комментарии",
@ -839,6 +853,12 @@
"text1": "Удалить этого пользователя из команды?", "text1": "Удалить этого пользователя из команды?",
"text2": "Пользователь потеряет доступ ко всем спискам и пространствам имён, к котором есть доступ у команды. Это действие отменить нельзя!", "text2": "Пользователь потеряет доступ ко всем спискам и пространствам имён, к котором есть доступ у команды. Это действие отменить нельзя!",
"success": "Пользователь удалён из команды." "success": "Пользователь удалён из команды."
},
"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."
} }
}, },
"attributes": { "attributes": {

View File

@ -1,9 +1,9 @@
{ {
"home": { "home": {
"welcomeNight": "Good Night {username}", "welcomeNight": "Good Night {username}!",
"welcomeMorning": "Good Morning {username}", "welcomeMorning": "Good Morning {username}!",
"welcomeDay": "Hi {username}", "welcomeDay": "Hi {username}!",
"welcomeEvening": "Good Evening {username}", "welcomeEvening": "Good Evening {username}!",
"lastViewed": "Last viewed", "lastViewed": "Last viewed",
"list": { "list": {
"newText": "You can create a new list for your new tasks:", "newText": "You can create a new list for your new tasks:",
@ -169,6 +169,14 @@
"title": "List Title", "title": "List Title",
"color": "Color", "color": "Color",
"lists": "Lists", "lists": "Lists",
"list": {
"title": "List",
"add": "Add",
"addPlaceholder": "Add a new task…",
"empty": "This list is currently empty.",
"newTaskCta": "Create a new task.",
"editTask": "Edit Task"
},
"search": "Type to search for a list…", "search": "Type to search for a list…",
"searchSelect": "Click or press enter to select this list", "searchSelect": "Click or press enter to select this list",
"shared": "Shared Lists", "shared": "Shared Lists",
@ -270,14 +278,6 @@
"delete": "Delete" "delete": "Delete"
} }
}, },
"list": {
"title": "List",
"add": "Add",
"addPlaceholder": "Add a new task…",
"empty": "This list is currently empty.",
"newTaskCta": "Create a new task.",
"editTask": "Edit Task"
},
"gantt": { "gantt": {
"title": "Gantt", "title": "Gantt",
"showTasksWithoutDates": "Show tasks which don't have dates set", "showTasksWithoutDates": "Show tasks which don't have dates set",
@ -672,13 +672,23 @@
"updated": "Updated" "updated": "Updated"
}, },
"subscription": { "subscription": {
"subscribedThroughParent": "You can't unsubscribe here because you are subscribed to this {entity} through its {parent}.", "subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribed": "You are currently subscribed to this {entity} and will receive notifications for changes.", "subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"notSubscribed": "You are not subscribed to this {entity} and won't receive notifications for changes.", "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.",
"subscribe": "Subscribe", "subscribe": "Subscribe",
"unsubscribe": "Unsubscribe", "unsubscribe": "Unsubscribe",
"subscribeSuccess": "You are now subscribed to this {entity}", "subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccess": "You are now unsubscribed to this {entity}" "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"
}, },
"attachment": { "attachment": {
"title": "Attachments", "title": "Attachments",
@ -690,7 +700,11 @@
"deleteTooltip": "Delete this attachment", "deleteTooltip": "Delete this attachment",
"deleteText1": "Are you sure you want to delete the attachment {filename}?", "deleteText1": "Are you sure you want to delete the attachment {filename}?",
"copyUrl": "Copy URL", "copyUrl": "Copy URL",
"copyUrlTooltip": "Copy the url of this attachment for usage in text" "copyUrlTooltip": "Copy the url of this attachment for usage in text",
"setAsCover": "Make cover",
"unsetAsCover": "Remove cover",
"successfullyChangedCoverImage": "The cover image was successfully changed.",
"usedAsCover": "Cover image"
}, },
"comment": { "comment": {
"title": "Comments", "title": "Comments",
@ -839,6 +853,12 @@
"text1": "Are you sure you want to remove this user from the team?", "text1": "Are you sure you want to remove this user from the team?",
"text2": "They will lose access to all lists and namespaces this team has access to. This CANNOT BE UNDONE!", "text2": "They will lose access to all lists and namespaces this team has access to. This CANNOT BE UNDONE!",
"success": "The user was successfully deleted from the team." "success": "The user was successfully deleted from the team."
},
"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."
} }
}, },
"attributes": { "attributes": {

View File

@ -1,9 +1,9 @@
{ {
"home": { "home": {
"welcomeNight": "Good Night {username}", "welcomeNight": "Good Night {username}!",
"welcomeMorning": "Good Morning {username}", "welcomeMorning": "Good Morning {username}!",
"welcomeDay": "Hi {username}", "welcomeDay": "Hi {username}!",
"welcomeEvening": "Good Evening {username}", "welcomeEvening": "Good Evening {username}!",
"lastViewed": "Last viewed", "lastViewed": "Last viewed",
"list": { "list": {
"newText": "You can create a new list for your new tasks:", "newText": "You can create a new list for your new tasks:",
@ -169,6 +169,14 @@
"title": "List Title", "title": "List Title",
"color": "Color", "color": "Color",
"lists": "Lists", "lists": "Lists",
"list": {
"title": "List",
"add": "Add",
"addPlaceholder": "Add a new task…",
"empty": "This list is currently empty.",
"newTaskCta": "Create a new task.",
"editTask": "Edit Task"
},
"search": "Type to search for a list…", "search": "Type to search for a list…",
"searchSelect": "Click or press enter to select this list", "searchSelect": "Click or press enter to select this list",
"shared": "Shared Lists", "shared": "Shared Lists",
@ -270,14 +278,6 @@
"delete": "Delete" "delete": "Delete"
} }
}, },
"list": {
"title": "List",
"add": "Add",
"addPlaceholder": "Add a new task…",
"empty": "This list is currently empty.",
"newTaskCta": "Create a new task.",
"editTask": "Edit Task"
},
"gantt": { "gantt": {
"title": "Gantt", "title": "Gantt",
"showTasksWithoutDates": "Show tasks which don't have dates set", "showTasksWithoutDates": "Show tasks which don't have dates set",
@ -672,13 +672,23 @@
"updated": "Updated" "updated": "Updated"
}, },
"subscription": { "subscription": {
"subscribedThroughParent": "You can't unsubscribe here because you are subscribed to this {entity} through its {parent}.", "subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribed": "You are currently subscribed to this {entity} and will receive notifications for changes.", "subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"notSubscribed": "You are not subscribed to this {entity} and won't receive notifications for changes.", "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.",
"subscribe": "Subscribe", "subscribe": "Subscribe",
"unsubscribe": "Unsubscribe", "unsubscribe": "Unsubscribe",
"subscribeSuccess": "You are now subscribed to this {entity}", "subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccess": "You are now unsubscribed to this {entity}" "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"
}, },
"attachment": { "attachment": {
"title": "Attachments", "title": "Attachments",
@ -690,7 +700,11 @@
"deleteTooltip": "Delete this attachment", "deleteTooltip": "Delete this attachment",
"deleteText1": "Are you sure you want to delete the attachment {filename}?", "deleteText1": "Are you sure you want to delete the attachment {filename}?",
"copyUrl": "Copy URL", "copyUrl": "Copy URL",
"copyUrlTooltip": "Copy the url of this attachment for usage in text" "copyUrlTooltip": "Copy the url of this attachment for usage in text",
"setAsCover": "Make cover",
"unsetAsCover": "Remove cover",
"successfullyChangedCoverImage": "The cover image was successfully changed.",
"usedAsCover": "Cover image"
}, },
"comment": { "comment": {
"title": "Comments", "title": "Comments",
@ -839,6 +853,12 @@
"text1": "Are you sure you want to remove this user from the team?", "text1": "Are you sure you want to remove this user from the team?",
"text2": "They will lose access to all lists and namespaces this team has access to. This CANNOT BE UNDONE!", "text2": "They will lose access to all lists and namespaces this team has access to. This CANNOT BE UNDONE!",
"success": "The user was successfully deleted from the team." "success": "The user was successfully deleted from the team."
},
"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."
} }
}, },
"attributes": { "attributes": {

View File

@ -1,9 +1,9 @@
{ {
"home": { "home": {
"welcomeNight": "Good Night {username}", "welcomeNight": "Good Night {username}!",
"welcomeMorning": "Good Morning {username}", "welcomeMorning": "Good Morning {username}!",
"welcomeDay": "Hi {username}", "welcomeDay": "Hi {username}!",
"welcomeEvening": "Good Evening {username}", "welcomeEvening": "Good Evening {username}!",
"lastViewed": "Last viewed", "lastViewed": "Last viewed",
"list": { "list": {
"newText": "You can create a new list for your new tasks:", "newText": "You can create a new list for your new tasks:",
@ -169,6 +169,14 @@
"title": "List Title", "title": "List Title",
"color": "Color", "color": "Color",
"lists": "Lists", "lists": "Lists",
"list": {
"title": "List",
"add": "Add",
"addPlaceholder": "Add a new task…",
"empty": "This list is currently empty.",
"newTaskCta": "Create a new task.",
"editTask": "Edit Task"
},
"search": "Type to search for a list…", "search": "Type to search for a list…",
"searchSelect": "Click or press enter to select this list", "searchSelect": "Click or press enter to select this list",
"shared": "Shared Lists", "shared": "Shared Lists",
@ -270,14 +278,6 @@
"delete": "Delete" "delete": "Delete"
} }
}, },
"list": {
"title": "List",
"add": "Add",
"addPlaceholder": "Add a new task…",
"empty": "This list is currently empty.",
"newTaskCta": "Create a new task.",
"editTask": "Edit Task"
},
"gantt": { "gantt": {
"title": "Gantt", "title": "Gantt",
"showTasksWithoutDates": "Show tasks which don't have dates set", "showTasksWithoutDates": "Show tasks which don't have dates set",
@ -672,13 +672,23 @@
"updated": "Updated" "updated": "Updated"
}, },
"subscription": { "subscription": {
"subscribedThroughParent": "You can't unsubscribe here because you are subscribed to this {entity} through its {parent}.", "subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribed": "You are currently subscribed to this {entity} and will receive notifications for changes.", "subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"notSubscribed": "You are not subscribed to this {entity} and won't receive notifications for changes.", "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.",
"subscribe": "Subscribe", "subscribe": "Subscribe",
"unsubscribe": "Unsubscribe", "unsubscribe": "Unsubscribe",
"subscribeSuccess": "You are now subscribed to this {entity}", "subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccess": "You are now unsubscribed to this {entity}" "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"
}, },
"attachment": { "attachment": {
"title": "Attachments", "title": "Attachments",
@ -690,7 +700,11 @@
"deleteTooltip": "Delete this attachment", "deleteTooltip": "Delete this attachment",
"deleteText1": "Are you sure you want to delete the attachment {filename}?", "deleteText1": "Are you sure you want to delete the attachment {filename}?",
"copyUrl": "Copy URL", "copyUrl": "Copy URL",
"copyUrlTooltip": "Copy the url of this attachment for usage in text" "copyUrlTooltip": "Copy the url of this attachment for usage in text",
"setAsCover": "Make cover",
"unsetAsCover": "Remove cover",
"successfullyChangedCoverImage": "The cover image was successfully changed.",
"usedAsCover": "Cover image"
}, },
"comment": { "comment": {
"title": "Comments", "title": "Comments",
@ -839,6 +853,12 @@
"text1": "Are you sure you want to remove this user from the team?", "text1": "Are you sure you want to remove this user from the team?",
"text2": "They will lose access to all lists and namespaces this team has access to. This CANNOT BE UNDONE!", "text2": "They will lose access to all lists and namespaces this team has access to. This CANNOT BE UNDONE!",
"success": "The user was successfully deleted from the team." "success": "The user was successfully deleted from the team."
},
"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."
} }
}, },
"attributes": { "attributes": {

View File

@ -1,9 +1,9 @@
{ {
"home": { "home": {
"welcomeNight": "Ngủ ngon nhé, {username}", "welcomeNight": "Good Night {username}!",
"welcomeMorning": "Chào buổi sáng, {username}", "welcomeMorning": "Good Morning {username}!",
"welcomeDay": "Hi {username}", "welcomeDay": "Hi {username}!",
"welcomeEvening": "Chào buổi tối, {username}", "welcomeEvening": "Good Evening {username}!",
"lastViewed": "Xem gần đây", "lastViewed": "Xem gần đây",
"list": { "list": {
"newText": "Bạn có thể tạo một danh sách công việc mới cho mình:", "newText": "Bạn có thể tạo một danh sách công việc mới cho mình:",
@ -169,6 +169,14 @@
"title": "Tên Danh sách", "title": "Tên Danh sách",
"color": "Màu sắc", "color": "Màu sắc",
"lists": "Danh sách", "lists": "Danh sách",
"list": {
"title": "Danh sách",
"add": "Thêm",
"addPlaceholder": "Thêm việc cần làm…",
"empty": "Danh sách này đang trống trơn.",
"newTaskCta": "Thêm một công việc mới.",
"editTask": "Chỉnh sửa Công việc"
},
"search": "Gõ để tìm kiếm danh sách…", "search": "Gõ để tìm kiếm danh sách…",
"searchSelect": "Nhấp hoặc nhấn enter để chọn danh sách này", "searchSelect": "Nhấp hoặc nhấn enter để chọn danh sách này",
"shared": "Đang tham gia", "shared": "Đang tham gia",
@ -270,14 +278,6 @@
"delete": "Xóa" "delete": "Xóa"
} }
}, },
"list": {
"title": "Danh sách",
"add": "Thêm",
"addPlaceholder": "Thêm việc cần làm…",
"empty": "Danh sách này đang trống trơn.",
"newTaskCta": "Thêm một công việc mới.",
"editTask": "Chỉnh sửa Công việc"
},
"gantt": { "gantt": {
"title": "Biểu đồ Gantt", "title": "Biểu đồ Gantt",
"showTasksWithoutDates": "Hiển thị các nhiệm vụ không cài đặt ngày", "showTasksWithoutDates": "Hiển thị các nhiệm vụ không cài đặt ngày",
@ -672,13 +672,23 @@
"updated": "Đã cập nhật" "updated": "Đã cập nhật"
}, },
"subscription": { "subscription": {
"subscribedThroughParent": "Bạn không thể hủy theo dõi ở đây vì bạn đã theo dõi {entity} này thông qua {parent} của nó.", "subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribed": "Bạn đang theo dõi {entity} này và sẽ nhận được thông báo về các thay đổi.", "subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"notSubscribed": "Bạn chưa theo dõi {entity} này và sẽ không nhận được thông báo về các thay đổi.", "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.",
"subscribe": "Theo dõi", "subscribe": "Theo dõi",
"unsubscribe": "Bỏ theo dõi", "unsubscribe": "Bỏ theo dõi",
"subscribeSuccess": "Bạn hiện đã theo dõi {entity} này", "subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccess": "Bạn đã bỏ theo dõi {entity} này" "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"
}, },
"attachment": { "attachment": {
"title": "Tệp đính kèm", "title": "Tệp đính kèm",
@ -690,7 +700,11 @@
"deleteTooltip": "Xóa tệp đính kèm này", "deleteTooltip": "Xóa tệp đính kèm này",
"deleteText1": "Bạn có chắc chắn muốn xóa tệp đính kèm {filename} không?", "deleteText1": "Bạn có chắc chắn muốn xóa tệp đính kèm {filename} không?",
"copyUrl": "Sao chép URL", "copyUrl": "Sao chép URL",
"copyUrlTooltip": "Sao chép url của tệp đính kèm này để sử dụng trong văn bản" "copyUrlTooltip": "Sao chép url của tệp đính kèm này để sử dụng trong văn bản",
"setAsCover": "Make cover",
"unsetAsCover": "Remove cover",
"successfullyChangedCoverImage": "The cover image was successfully changed.",
"usedAsCover": "Cover image"
}, },
"comment": { "comment": {
"title": "Bình luận", "title": "Bình luận",
@ -839,6 +853,12 @@
"text1": "Bạn có chắc muốn đưa thành viên này ra khỏi Team không?", "text1": "Bạn có chắc muốn đưa thành viên này ra khỏi Team không?",
"text2": "Họ sẽ mất quyền truy cập vào tất cả danh sách và góc làm việc mà Team này có quyền truy cập. Điều đó KHÔNG THỂ HOÀN TÁC!", "text2": "Họ sẽ mất quyền truy cập vào tất cả danh sách và góc làm việc mà Team này có quyền truy cập. Điều đó KHÔNG THỂ HOÀN TÁC!",
"success": "Thành viên đã rời khỏi Team." "success": "Thành viên đã rời khỏi Team."
},
"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."
} }
}, },
"attributes": { "attributes": {

View File

@ -1,9 +1,9 @@
{ {
"home": { "home": {
"welcomeNight": "Good Night {username}", "welcomeNight": "Good Night {username}!",
"welcomeMorning": "Good Morning {username}", "welcomeMorning": "Good Morning {username}!",
"welcomeDay": "Hi {username}", "welcomeDay": "Hi {username}!",
"welcomeEvening": "Good Evening {username}", "welcomeEvening": "Good Evening {username}!",
"lastViewed": "Last viewed", "lastViewed": "Last viewed",
"list": { "list": {
"newText": "You can create a new list for your new tasks:", "newText": "You can create a new list for your new tasks:",
@ -169,6 +169,14 @@
"title": "List Title", "title": "List Title",
"color": "Color", "color": "Color",
"lists": "Lists", "lists": "Lists",
"list": {
"title": "List",
"add": "Add",
"addPlaceholder": "Add a new task…",
"empty": "This list is currently empty.",
"newTaskCta": "Create a new task.",
"editTask": "Edit Task"
},
"search": "Type to search for a list…", "search": "Type to search for a list…",
"searchSelect": "Click or press enter to select this list", "searchSelect": "Click or press enter to select this list",
"shared": "Shared Lists", "shared": "Shared Lists",
@ -270,14 +278,6 @@
"delete": "Delete" "delete": "Delete"
} }
}, },
"list": {
"title": "List",
"add": "Add",
"addPlaceholder": "Add a new task…",
"empty": "This list is currently empty.",
"newTaskCta": "Create a new task.",
"editTask": "Edit Task"
},
"gantt": { "gantt": {
"title": "Gantt", "title": "Gantt",
"showTasksWithoutDates": "Show tasks which don't have dates set", "showTasksWithoutDates": "Show tasks which don't have dates set",
@ -672,13 +672,23 @@
"updated": "Updated" "updated": "Updated"
}, },
"subscription": { "subscription": {
"subscribedThroughParent": "You can't unsubscribe here because you are subscribed to this {entity} through its {parent}.", "subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribed": "You are currently subscribed to this {entity} and will receive notifications for changes.", "subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"notSubscribed": "You are not subscribed to this {entity} and won't receive notifications for changes.", "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.",
"subscribe": "Subscribe", "subscribe": "Subscribe",
"unsubscribe": "Unsubscribe", "unsubscribe": "Unsubscribe",
"subscribeSuccess": "You are now subscribed to this {entity}", "subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccess": "You are now unsubscribed to this {entity}" "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"
}, },
"attachment": { "attachment": {
"title": "Attachments", "title": "Attachments",
@ -690,7 +700,11 @@
"deleteTooltip": "Delete this attachment", "deleteTooltip": "Delete this attachment",
"deleteText1": "Are you sure you want to delete the attachment {filename}?", "deleteText1": "Are you sure you want to delete the attachment {filename}?",
"copyUrl": "Copy URL", "copyUrl": "Copy URL",
"copyUrlTooltip": "Copy the url of this attachment for usage in text" "copyUrlTooltip": "Copy the url of this attachment for usage in text",
"setAsCover": "Make cover",
"unsetAsCover": "Remove cover",
"successfullyChangedCoverImage": "The cover image was successfully changed.",
"usedAsCover": "Cover image"
}, },
"comment": { "comment": {
"title": "Comments", "title": "Comments",
@ -839,6 +853,12 @@
"text1": "Are you sure you want to remove this user from the team?", "text1": "Are you sure you want to remove this user from the team?",
"text2": "They will lose access to all lists and namespaces this team has access to. This CANNOT BE UNDONE!", "text2": "They will lose access to all lists and namespaces this team has access to. This CANNOT BE UNDONE!",
"success": "The user was successfully deleted from the team." "success": "The user was successfully deleted from the team."
},
"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."
} }
}, },
"attributes": { "attributes": {

1053
src/i18n/lang/zh-TW.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -13,6 +13,7 @@ import type {IRepeatAfter} from '@/types/IRepeatAfter'
import type {IRepeatMode} from '@/types/IRepeatMode' import type {IRepeatMode} from '@/types/IRepeatMode'
import type {PartialWithId} from '@/types/PartialWithId' import type {PartialWithId} from '@/types/PartialWithId'
export interface ITask extends IAbstract { export interface ITask extends IAbstract {
id: number id: number
title: string title: string
@ -33,8 +34,9 @@ export interface ITask extends IAbstract {
parentTaskId: ITask['id'] parentTaskId: ITask['id']
hexColor: string hexColor: string
percentDone: number percentDone: number
relatedTasks: Partial<Record<IRelationKind, ITask[]>>, relatedTasks: Partial<Record<IRelationKind, ITask[]>>
attachments: IAttachment[] attachments: IAttachment[]
coverImageAttachmentId: IAttachment['id']
identifier: string identifier: string
index: number index: number
isFavorite: boolean isFavorite: boolean

View File

@ -7,7 +7,7 @@ export const AUTH_TYPES = {
'LINK_SHARE': 2, 'LINK_SHARE': 2,
} as const } 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 { export interface IUser extends IAbstract {
id: number id: number

View File

@ -5,6 +5,8 @@ import type { IUser } from '@/modelTypes/IUser'
import type { IFile } from '@/modelTypes/IFile' import type { IFile } from '@/modelTypes/IFile'
import type { IAttachment } from '@/modelTypes/IAttachment' import type { IAttachment } from '@/modelTypes/IAttachment'
export const SUPPORTED_IMAGE_SUFFIX = ['.jpg', '.png', '.bmp', '.gif']
export default class AttachmentModel extends AbstractModel<IAttachment> implements IAttachment { export default class AttachmentModel extends AbstractModel<IAttachment> implements IAttachment {
id = 0 id = 0
taskId = 0 taskId = 0

View File

@ -9,6 +9,7 @@ import {setTitle} from '@/helpers/setTitle'
import {useListStore} from '@/stores/lists' import {useListStore} from '@/stores/lists'
import {useAuthStore} from '@/stores/auth' import {useAuthStore} from '@/stores/auth'
import {useBaseStore} from '@/stores/base'
import HomeComponent from '../views/Home.vue' import HomeComponent from '../views/Home.vue'
import NotFoundComponent from '../views/404.vue' import NotFoundComponent from '../views/404.vue'
@ -465,11 +466,18 @@ const router = createRouter({
], ],
}) })
export function getAuthForRoute(route: RouteLocation) { export async function getAuthForRoute(route: RouteLocation) {
const authStore = useAuthStore() const authStore = useAuthStore()
if (authStore.authUser || authStore.authLinkShare) { if (authStore.authUser || authStore.authLinkShare) {
return 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 // Check if the user is already logged in and redirect them to the home page if not
if ( if (
@ -482,11 +490,24 @@ export function getAuthForRoute(route: RouteLocation) {
'openid.auth', 'openid.auth',
].includes(route.name as string) && ].includes(route.name as string) &&
localStorage.getItem('passwordResetToken') === null && 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'} return {name: 'user.login'}
} }
} }
router.beforeEach(async (to) => {
return getAuthForRoute(to)
})
export default router export default router

View File

@ -183,11 +183,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. * This one here is the default one, usually the service definitions for a model will override this.
*/ */
modelFactory(data : Partial<Model>) { modelFactory(data : Partial<Model>) {
return new AbstractModel(data) return data as Model
} }
/** /**

View File

@ -22,7 +22,6 @@ export default class AbstractMigrationFileService extends AbstractService {
} }
migrate(file: IFile) { migrate(file: IFile) {
console.log(file)
return this.uploadFile( return this.uploadFile(
this.paths.create, this.paths.create,
file, file,

View File

@ -3,7 +3,7 @@ import {defineStore, acceptHMRUpdate} from 'pinia'
import {HTTPFactory, AuthenticatedHTTPFactory} from '@/http-common' import {HTTPFactory, AuthenticatedHTTPFactory} from '@/http-common'
import {i18n, getCurrentLanguage, saveLanguage} from '@/i18n' import {i18n, getCurrentLanguage, saveLanguage} from '@/i18n'
import {objectToSnakeCase} from '@/helpers/case' import {objectToSnakeCase} from '@/helpers/case'
import UserModel, { getAvatarUrl } from '@/models/user' import UserModel, { getAvatarUrl, getDisplayName } from '@/models/user'
import UserSettingsService from '@/services/userSettings' import UserSettingsService from '@/services/userSettings'
import {getToken, refreshToken, removeToken, saveToken} from '@/helpers/auth' import {getToken, refreshToken, removeToken, saveToken} from '@/helpers/auth'
import {setModuleLoading} from '@/stores/helper' import {setModuleLoading} from '@/stores/helper'
@ -55,6 +55,9 @@ export const useAuthStore = defineStore('auth', {
state.info.type === AUTH_TYPES.LINK_SHARE state.info.type === AUTH_TYPES.LINK_SHARE
) )
}, },
userDisplayName(state) {
return state.info ? getDisplayName(state.info) : undefined
},
}, },
actions: { actions: {
setIsLoading(isLoading: boolean) { setIsLoading(isLoading: boolean) {
@ -115,7 +118,7 @@ export const useAuthStore = defineStore('auth', {
saveToken(response.data.token, true) saveToken(response.data.token, true)
// Tell others the user is autheticated // Tell others the user is autheticated
this.checkAuth() await this.checkAuth()
} catch (e) { } catch (e) {
if ( if (
e.response && e.response &&
@ -168,7 +171,7 @@ export const useAuthStore = defineStore('auth', {
saveToken(response.data.token, true) saveToken(response.data.token, true)
// Tell others the user is autheticated // Tell others the user is autheticated
this.checkAuth() await this.checkAuth()
} finally { } finally {
this.setIsLoading(false) this.setIsLoading(false)
} }
@ -180,14 +183,14 @@ export const useAuthStore = defineStore('auth', {
password: password, password: password,
}) })
saveToken(response.data.token, false) saveToken(response.data.token, false)
this.checkAuth() await this.checkAuth()
return response.data return response.data
}, },
/** /**
* Populates user information from jwt token saved in local storage in store * Populates user information from jwt token saved in local storage in store
*/ */
checkAuth() { async checkAuth() {
const now = new Date() const now = new Date()
const inOneMinute = new Date(new Date().setMinutes(now.getMinutes() + 1)) const inOneMinute = new Date(new Date().setMinutes(now.getMinutes() + 1))
// This function can be called from multiple places at the same time and shortly after one another. // This function can be called from multiple places at the same time and shortly after one another.
@ -212,7 +215,7 @@ export const useAuthStore = defineStore('auth', {
this.setUser(info) this.setUser(info)
if (authenticated) { if (authenticated) {
this.refreshUserInfo() await this.refreshUserInfo()
} }
} }
@ -221,6 +224,8 @@ export const useAuthStore = defineStore('auth', {
this.setUser(null) this.setUser(null)
this.redirectToProviderIfNothingElseIsEnabled() this.redirectToProviderIfNothingElseIsEnabled()
} }
return Promise.resolve(authenticated)
}, },
redirectToProviderIfNothingElseIsEnabled() { redirectToProviderIfNothingElseIsEnabled() {
@ -290,11 +295,11 @@ export const useAuthStore = defineStore('auth', {
const stopLoading = setModuleLoading(this) const stopLoading = setModuleLoading(this)
try { try {
await HTTPFactory().post('user/confirm', {token: emailVerifyToken}) await HTTPFactory().post('user/confirm', {token: emailVerifyToken})
localStorage.removeItem('emailConfirmToken')
return true return true
} catch(e) { } catch(e) {
throw new Error(e.response.data.message) throw new Error(e.response.data.message)
} finally { } finally {
localStorage.removeItem('emailConfirmToken')
stopLoading() stopLoading()
} }
} }
@ -339,22 +344,22 @@ export const useAuthStore = defineStore('auth', {
try { try {
await refreshToken(!this.isLinkShareAuth) await refreshToken(!this.isLinkShareAuth)
this.checkAuth() await this.checkAuth()
} catch (e) { } catch (e) {
// Don't logout on network errors as the user would then get logged out if they don't have // 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 // internet for a short period of time - such as when the laptop is still reconnecting
if (e?.request?.status) { if (e?.request?.status) {
this.logout() await this.logout()
} }
} }
}, 5000) }, 5000)
}, },
logout() { async logout() {
removeToken() removeToken()
window.localStorage.clear() // Clear all settings and history we might have saved in local storage. window.localStorage.clear() // Clear all settings and history we might have saved in local storage.
router.push({name: 'user.login'}) await router.push({name: 'user.login'})
this.checkAuth() await this.checkAuth()
}, },
}, },
}) })

View File

@ -11,6 +11,7 @@ import type {IList} from '@/modelTypes/IList'
export interface RootStoreState { export interface RootStoreState {
loading: boolean, loading: boolean,
ready: boolean,
currentList: IList | null, currentList: IList | null,
background: string, background: string,
@ -26,6 +27,7 @@ export interface RootStoreState {
export const useBaseStore = defineStore('base', { export const useBaseStore = defineStore('base', {
state: () : RootStoreState => ({ state: () : RootStoreState => ({
loading: false, loading: false,
ready: false,
// This is used to highlight the current list in menu for all list related views // This is used to highlight the current list in menu for all list related views
currentList: new ListModel({ currentList: new ListModel({
@ -95,6 +97,10 @@ export const useBaseStore = defineStore('base', {
setLogoVisible(visible: boolean) { setLogoVisible(visible: boolean) {
this.logoVisible = visible this.logoVisible = visible
}, },
setReady(ready: boolean) {
this.ready = ready
},
async handleSetCurrentList({list, forceUpdate = false} : {list: IList | null, forceUpdate: boolean}) { async handleSetCurrentList({list, forceUpdate = false} : {list: IList | null, forceUpdate: boolean}) {
if (list === null) { if (list === null) {
@ -133,7 +139,8 @@ export const useBaseStore = defineStore('base', {
async loadApp() { async loadApp() {
await checkAndSetApiUrl(window.API_URL) await checkAndSetApiUrl(window.API_URL)
useAuthStore().checkAuth() await useAuthStore().checkAuth()
this.ready = true
}, },
}, },
}) })

View File

@ -75,12 +75,11 @@ export const useKanbanStore = defineStore('kanban', {
getTaskById(state) { getTaskById(state) {
return (id: ITask['id']) => { return (id: ITask['id']) => {
const { bucketIndex, taskIndex } = getTaskIndicesById(state, id) const { bucketIndex, taskIndex } = getTaskIndicesById(state, id)
return { return {
bucketIndex, bucketIndex,
taskIndex, taskIndex,
task: bucketIndex && taskIndex && state.buckets[bucketIndex]?.tasks?.[taskIndex] || null, task: bucketIndex !== null && taskIndex !== null && state.buckets[bucketIndex]?.tasks?.[taskIndex] || null,
} }
} }
}, },

View File

@ -43,7 +43,7 @@ export const useNamespaceStore = defineStore('namespace', {
getNamespaceById: state => (namespaceId: INamespace['id']) => { getNamespaceById: state => (namespaceId: INamespace['id']) => {
return state.namespaces.find(({id}) => id == namespaceId) || null return state.namespaces.find(({id}) => id == namespaceId) || null
}, },
searchNamespace() { searchNamespace() {
return (query: string) => ( return (query: string) => (
search(query) search(query)
@ -64,6 +64,13 @@ export const useNamespaceStore = defineStore('namespace', {
this.namespaces = namespaces this.namespaces = namespaces
namespaces.forEach(n => { namespaces.forEach(n => {
add(n) add(n)
// Check for each list in that namespace if it has a subscription and set it if not
n.lists.forEach(l => {
if (l.subscription === null || l.subscription.entity !== 'list') {
l.subscription = n.subscription
}
})
}) })
}, },
@ -203,5 +210,5 @@ export const useNamespaceStore = defineStore('namespace', {
// support hot reloading // support hot reloading
if (import.meta.hot) { if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useNamespaceStore, import.meta.hot)) import.meta.hot.accept(acceptHMRUpdate(useNamespaceStore, import.meta.hot))
} }

View File

@ -28,6 +28,7 @@ import {useLabelStore} from '@/stores/labels'
import {useListStore} from '@/stores/lists' import {useListStore} from '@/stores/lists'
import {useAttachmentStore} from '@/stores/attachments' import {useAttachmentStore} from '@/stores/attachments'
import {useKanbanStore} from '@/stores/kanban' import {useKanbanStore} from '@/stores/kanban'
import {useBaseStore} from '@/stores/base'
// IDEA: maybe use a small fuzzy search here to prevent errors // IDEA: maybe use a small fuzzy search here to prevent errors
function findPropertyByValue(object, key, value) { function findPropertyByValue(object, key, value) {
@ -105,6 +106,7 @@ export const useTaskStore = defineStore('task', {
const cancel = setModuleLoading(this) const cancel = setModuleLoading(this)
try { try {
this.tasks = await taskService.getAll({}, params) this.tasks = await taskService.getAll({}, params)
useBaseStore().setHasTasks(this.tasks.length > 0)
return this.tasks return this.tasks
} finally { } finally {
cancel() cancel()
@ -171,32 +173,39 @@ export const useTaskStore = defineStore('task', {
user: IUser, user: IUser,
taskId: ITask['id'] taskId: ITask['id']
}) { }) {
const kanbanStore = useKanbanStore() const cancel = setModuleLoading(this)
const taskAssigneeService = new TaskAssigneeService()
const r = await taskAssigneeService.create(new TaskAssigneeModel({ try {
userId: user.id, const kanbanStore = useKanbanStore()
taskId: taskId, const taskAssigneeService = new TaskAssigneeService()
})) const r = await taskAssigneeService.create(new TaskAssigneeModel({
const t = kanbanStore.getTaskById(taskId) userId: user.id,
if (t.task === null) { taskId: taskId,
// Don't try further adding a label if the task is not in kanban }))
// Usually this means the kanban board hasn't been accessed until now. const t = kanbanStore.getTaskById(taskId)
// Vuex seems to have its difficulties with that, so we just log the error and fail silently. if (t.task === null) {
console.debug('Could not add assignee to task in kanban, task not found', t) // Don't try further adding a label if the task is not in kanban
return r // Usually this means the kanban board hasn't been accessed until now.
} // Vuex seems to have its difficulties with that, so we just log the error and fail silently.
console.debug('Could not add assignee to task in kanban, task not found', t)
return r
}
kanbanStore.setTaskInBucketByIndex({ kanbanStore.setTaskInBucketByIndex({
...t, ...t,
task: { task: {
...t.task, ...t.task,
assignees: [ assignees: [
...t.task.assignees, ...t.task.assignees,
user, user,
], ],
}, },
}) })
return r
return r
} finally {
cancel()
}
}, },
async removeAssignee({ async removeAssignee({
@ -252,7 +261,7 @@ export const useTaskStore = defineStore('task', {
// Don't try further adding a label if the task is not in kanban // Don't try further adding a label if the task is not in kanban
// Usually this means the kanban board hasn't been accessed until now. // Usually this means the kanban board hasn't been accessed until now.
// Vuex seems to have its difficulties with that, so we just log the error and fail silently. // Vuex seems to have its difficulties with that, so we just log the error and fail silently.
console.debug('Could not add label to task in kanban, task not found', t) console.debug('Could not add label to task in kanban, task not found', {taskId, t})
return r return r
} }
@ -378,6 +387,7 @@ export const useTaskStore = defineStore('task', {
}) })
if(foundListId === null || foundListId === 0) { if(foundListId === null || foundListId === 0) {
cancel()
throw new Error('NO_LIST') throw new Error('NO_LIST')
} }
@ -409,6 +419,13 @@ export const useTaskStore = defineStore('task', {
cancel() cancel()
} }
}, },
async setCoverImage(task: ITask, attachment: IAttachment | null) {
return this.update({
...task,
coverImageAttachmentId: attachment ? attachment.id : 0,
})
},
}, },
}) })

View File

@ -1,4 +0,0 @@
declare module 'vue-flatpickr-component' {
import type {DefineComponent} from 'vue'
export default DefineComponent<>
}

View File

@ -34,9 +34,10 @@
<script setup lang="ts"> <script setup lang="ts">
import {computed} from 'vue' import {computed} from 'vue'
import {VERSION as frontendVersion} from '@/version.json' import {VERSION} from '@/version.json'
import {useConfigStore} from '@/stores/config' import {useConfigStore} from '@/stores/config'
const configStore = useConfigStore() const configStore = useConfigStore()
const apiVersion = computed(() => configStore.version) const apiVersion = computed(() => configStore.version)
const frontendVersion = VERSION
</script> </script>

View File

@ -1,8 +1,7 @@
<template> <template>
<div class="content has-text-centered"> <div class="content has-text-centered">
<h2 v-if="userInfo"> <h2 v-if="salutation">{{ salutation }}</h2>
{{ $t(welcome, {username: userInfo.name !== '' ? userInfo.name : userInfo.username}) }}!
</h2>
<message variant="danger" v-if="deletionScheduledAt !== null" class="mb-4"> <message variant="danger" v-if="deletionScheduledAt !== null" class="mb-4">
{{ {{
$t('user.deletion.scheduled', { $t('user.deletion.scheduled', {
@ -69,7 +68,7 @@ import AddTask from '@/components/tasks/add-task.vue'
import {getHistory} from '@/modules/listHistory' import {getHistory} from '@/modules/listHistory'
import {parseDateOrNull} from '@/helpers/parseDateOrNull' import {parseDateOrNull} from '@/helpers/parseDateOrNull'
import {formatDateShort, formatDateSince} from '@/helpers/time/formatDate' import {formatDateShort, formatDateSince} from '@/helpers/time/formatDate'
import {useDateTimeSalutation} from '@/composables/useDateTimeSalutation' import {useDaytimeSalutation} from '@/composables/useDaytimeSalutation'
import {useBaseStore} from '@/stores/base' import {useBaseStore} from '@/stores/base'
import {useListStore} from '@/stores/lists' import {useListStore} from '@/stores/lists'
@ -78,7 +77,7 @@ import {useNamespaceStore} from '@/stores/namespaces'
import {useAuthStore} from '@/stores/auth' import {useAuthStore} from '@/stores/auth'
import {useTaskStore} from '@/stores/tasks' import {useTaskStore} from '@/stores/tasks'
const welcome = useDateTimeSalutation() const salutation = useDaytimeSalutation()
const baseStore = useBaseStore() const baseStore = useBaseStore()
const authStore = useAuthStore() const authStore = useAuthStore()
@ -99,7 +98,6 @@ const listHistory = computed(() => {
}) })
const migratorsEnabled = computed(() => configStore.availableMigrators?.length > 0) const migratorsEnabled = computed(() => configStore.availableMigrators?.length > 0)
const userInfo = computed(() => authStore.info)
const hasTasks = computed(() => baseStore.hasTasks) const hasTasks = computed(() => baseStore.hasTasks)
const defaultListId = computed(() => authStore.settings.defaultListId) const defaultListId = computed(() => authStore.settings.defaultListId)
const defaultNamespaceId = computed(() => namespaceStore.namespaces?.[0]?.id || 0) const defaultNamespaceId = computed(() => namespaceStore.namespaces?.[0]?.id || 0)

View File

@ -1,7 +1,7 @@
<template> <template>
<ListWrapper class="list-kanban" :list-id="listId" viewName="kanban"> <ListWrapper class="list-kanban" :list-id="listId" viewName="kanban">
<template #header> <template #header>
<div class="filter-container" v-if="isSavedFilter(list)"> <div class="filter-container" v-if="!isSavedFilter(listId)">
<div class="items"> <div class="items">
<filter-popup <filter-popup
v-model="params" v-model="params"

View File

@ -1,7 +1,7 @@
<template> <template>
<create-edit <create-edit
:title="$t('list.share.header')" :title="$t('list.share.header')"
primary-label="" :has-primary-action="false"
> >
<template v-if="list"> <template v-if="list">
<userTeam <userTeam

View File

@ -0,0 +1 @@
<svg viewBox="0 0 88 88" xmlns="http://www.w3.org/2000/svg" class="logo_1OKcB"><g fill="none" fill-rule="evenodd"><rect></rect><path d="M30.755 33.292l-7.34 8.935L40.798 56.48a5.782 5.782 0 008.182-.854l31.179-38.93-9.026-7.228L43.614 43.83l-12.86-10.538z" fill="#FFB000"></path><path d="M44 78.1C25.197 78.1 9.9 62.803 9.9 44S25.197 9.9 44 9.9V0C19.738 0 0 19.738 0 44s19.738 44 44 44 44-19.738 44-44h-9.9c0 18.803-15.297 34.1-34.1 34.1" fill="#4772FA"></path></g></svg>

After

Width:  |  Height:  |  Size: 471 B

View File

@ -3,6 +3,7 @@ import todoistIcon from './icons/todoist.svg?url'
import trelloIcon from './icons/trello.svg?url' import trelloIcon from './icons/trello.svg?url'
import microsoftTodoIcon from './icons/microsoft-todo.svg?url' import microsoftTodoIcon from './icons/microsoft-todo.svg?url'
import vikunjaFileIcon from './icons/vikunja-file.png?url' import vikunjaFileIcon from './icons/vikunja-file.png?url'
import tickTickIcon from './icons/ticktick.svg?url'
export interface Migrator { export interface Migrator {
id: string id: string
@ -42,4 +43,10 @@ export const MIGRATORS: IMigratorRecord = {
icon: vikunjaFileIcon, icon: vikunjaFileIcon,
isFileMigrator: true, isFileMigrator: true,
}, },
ticktick: {
id: 'ticktick',
name: 'TickTick',
icon: tickTickIcon as string,
isFileMigrator: true,
},
} }

View File

@ -1,7 +1,7 @@
<template> <template>
<create-edit <create-edit
:title="title" :title="title"
primary-label="" :has-primary-action="false"
> >
<template v-if="namespace"> <template v-if="namespace">
<manageSharing <manageSharing

View File

@ -26,7 +26,7 @@
:disabled="!canWrite" :disabled="!canWrite"
:list-id="task.listId" :list-id="task.listId"
:task-id="task.id" :task-id="task.id"
ref="assignees" :ref="e => setFieldRef('assignees', e)"
v-model="task.assignees" v-model="task.assignees"
/> />
</div> </div>
@ -39,8 +39,8 @@
</div> </div>
<priority-select <priority-select
:disabled="!canWrite" :disabled="!canWrite"
@update:model-value="saveTask" @update:model-value="setPriority"
ref="priority" :ref="e => setFieldRef('priority', e)"
v-model="task.priority"/> v-model="task.priority"/>
</div> </div>
</transition> </transition>
@ -57,7 +57,7 @@
@close-on-change="() => saveTask()" @close-on-change="() => saveTask()"
:choose-date-label="$t('task.detail.chooseDueDate')" :choose-date-label="$t('task.detail.chooseDueDate')"
:disabled="taskService.loading || !canWrite" :disabled="taskService.loading || !canWrite"
ref="dueDate" :ref="e => setFieldRef('dueDate', e)"
/> />
<BaseButton <BaseButton
@click="() => {task.dueDate = null;saveTask()}" @click="() => {task.dueDate = null;saveTask()}"
@ -79,8 +79,8 @@
</div> </div>
<percent-done-select <percent-done-select
:disabled="!canWrite" :disabled="!canWrite"
@update:model-value="saveTask" @update:model-value="setPercentDone"
ref="percentDone" :ref="e => setFieldRef('percentDone', e)"
v-model="task.percentDone"/> v-model="task.percentDone"/>
</div> </div>
</transition> </transition>
@ -97,7 +97,7 @@
@close-on-change="() => saveTask()" @close-on-change="() => saveTask()"
:choose-date-label="$t('task.detail.chooseStartDate')" :choose-date-label="$t('task.detail.chooseStartDate')"
:disabled="taskService.loading || !canWrite" :disabled="taskService.loading || !canWrite"
ref="startDate" :ref="e => setFieldRef('startDate', e)"
/> />
<BaseButton <BaseButton
@click="() => {task.startDate = null;saveTask()}" @click="() => {task.startDate = null;saveTask()}"
@ -124,7 +124,7 @@
@close-on-change="() => saveTask()" @close-on-change="() => saveTask()"
:choose-date-label="$t('task.detail.chooseEndDate')" :choose-date-label="$t('task.detail.chooseEndDate')"
:disabled="taskService.loading || !canWrite" :disabled="taskService.loading || !canWrite"
ref="endDate" :ref="e => setFieldRef('endDate', e)"
/> />
<BaseButton <BaseButton
@click="() => {task.endDate = null;saveTask()}" @click="() => {task.endDate = null;saveTask()}"
@ -146,7 +146,7 @@
</div> </div>
<reminders <reminders
:disabled="!canWrite" :disabled="!canWrite"
ref="reminders" :ref="e => setFieldRef('reminders', e)"
v-model="task.reminderDates" v-model="task.reminderDates"
@update:model-value="saveTask" @update:model-value="saveTask"
/> />
@ -171,7 +171,7 @@
</div> </div>
<repeat-after <repeat-after
:disabled="!canWrite" :disabled="!canWrite"
ref="repeatAfter" :ref="e => setFieldRef('repeatAfter', e)"
v-model="task" v-model="task"
@update:model-value="saveTask" @update:model-value="saveTask"
/> />
@ -186,7 +186,7 @@
</div> </div>
<color-picker <color-picker
menu-position="bottom" menu-position="bottom"
ref="color" :ref="e => setFieldRef('color', e)"
v-model="taskColor" v-model="taskColor"
@update:model-value="saveTask" @update:model-value="saveTask"
/> />
@ -202,7 +202,11 @@
</span> </span>
{{ $t('task.attributes.labels') }} {{ $t('task.attributes.labels') }}
</div> </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> </div>
<!-- Description --> <!-- Description -->
@ -218,8 +222,9 @@
<div class="content attachments" v-if="activeFields.attachments || hasAttachments"> <div class="content attachments" v-if="activeFields.attachments || hasAttachments">
<attachments <attachments
:edit-enabled="canWrite" :edit-enabled="canWrite"
:task-id="taskId" :task="task"
ref="attachments" @task-changed="({coverImageAttachmentId}) => task.coverImageAttachmentId = coverImageAttachmentId"
:ref="e => setFieldRef('attachments', e)"
/> />
</div> </div>
@ -237,7 +242,7 @@
:list-id="task.listId" :list-id="task.listId"
:show-no-relations-notice="true" :show-no-relations-notice="true"
:task-id="taskId" :task-id="taskId"
ref="relatedTasks" :ref="e => setFieldRef('relatedTasks', e)"
/> />
</div> </div>
@ -251,7 +256,10 @@
</h3> </h3>
<div class="field has-addons"> <div class="field has-addons">
<div class="control is-expanded"> <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> </div>
</div> </div>
@ -442,7 +450,7 @@ import TaskModel, {TASK_DEFAULT_COLOR} from '@/models/task'
import type {ITask} from '@/modelTypes/ITask' import type {ITask} from '@/modelTypes/ITask'
import type {IList} from '@/modelTypes/IList' import type {IList} from '@/modelTypes/IList'
import {PRIORITIES} from '@/constants/priorities' import {PRIORITIES, type Priority} from '@/constants/priorities'
import {RIGHTS} from '@/constants/rights' import {RIGHTS} from '@/constants/rights'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
@ -500,7 +508,7 @@ const attachmentStore = useAttachmentStore()
const taskStore = useTaskStore() const taskStore = useTaskStore()
const kanbanStore = useKanbanStore() const kanbanStore = useKanbanStore()
const task = reactive(new TaskModel()) const task = reactive<ITask>(new TaskModel())
useTitle(toRef(task, 'title')) useTitle(toRef(task, 'title'))
// We doubled the task color property here because verte does not have a real change property, leading // We doubled the task color property here because verte does not have a real change property, leading
@ -658,10 +666,14 @@ const activeFieldElements : {[id in FieldType]: HTMLElement | null} = reactive({
startDate: null, startDate: null,
}) })
function setFieldRef(name, e) {
activeFieldElements[name] = unrefElement(e)
}
function setFieldActive(fieldName: keyof typeof activeFields) { function setFieldActive(fieldName: keyof typeof activeFields) {
activeFields[fieldName] = true activeFields[fieldName] = true
nextTick(() => { nextTick(() => {
const el = unrefElement(activeFieldElements[fieldName]) const el = activeFieldElements[fieldName]
if (!el) { if (!el) {
return return
@ -675,21 +687,19 @@ function setFieldActive(fieldName: keyof typeof activeFields) {
} }
async function saveTask(args?: { async function saveTask(args?: {
task: ITask, task: ITask,
showNotification?: boolean, undoCallback?: () => void,
undoCallback?: () => void, }) {
}) { const {
const { task: currentTask,
task: currentTask, undoCallback,
showNotification, } = {
undoCallback, ...{
} = { task: cloneDeep(task),
...{ },
task: cloneDeep(task), ...args,
showNotification: true, }
},
...args,
}
if (!canWrite.value) { if (!canWrite.value) {
return return
} }
@ -710,10 +720,6 @@ async function saveTask(args?: {
Object.assign(task, newTask) Object.assign(task, newTask)
setActiveFields() setActiveFields()
if (!showNotification) {
return
}
let actions = [] let actions = []
if (undoCallback !== null) { if (undoCallback !== null) {
actions = [{ actions = [{
@ -759,14 +765,34 @@ async function toggleFavorite() {
Object.assign(task, newTask) Object.assign(task, newTask)
await namespaceStore.loadNamespacesIfFavoritesDontExist() await namespaceStore.loadNamespacesIfFavoritesDontExist()
} }
async function setPriority(priority: Priority) {
const newTask: ITask = {
...task,
priority,
}
return saveTask({
task: newTask,
})
}
async function setPercentDone(percentDone: number) {
const newTask: ITask = {
...task,
percentDone,
}
return saveTask({
task: newTask,
})
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
$flash-background-duration: 750ms; $flash-background-duration: 750ms;
.task-view { .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; padding: 1rem;
background-color: var(--site-background); background-color: var(--site-background);

View File

@ -127,6 +127,24 @@
</table> </table>
</card> </card>
<x-button class="is-fullwidth is-danger" @click="showLeaveModal = true">
{{ $t('team.edit.leave.title') }}
</x-button>
<!-- Leave team modal -->
<modal
v-if="showLeaveModal"
@close="showLeaveModal = false"
@submit="leave()"
>
<template #header><span>{{ $t('team.edit.leave.title') }}</span></template>
<template #text>
<p>{{ $t('team.edit.leave.text1') }}<br/>
{{ $t('team.edit.leave.text2') }}</p>
</template>
</modal>
<!-- Team delete modal --> <!-- Team delete modal -->
<transition name="modal"> <transition name="modal">
<modal <modal
@ -202,13 +220,14 @@ const teamMemberService = ref<TeamMemberService>(new TeamMemberService())
const userService = ref<UserService>(new UserService()) const userService = ref<UserService>(new UserService())
const team = ref<ITeam>() const team = ref<ITeam>()
const teamId = computed(() => route.params.id) const teamId = computed(() => Number(route.params.id))
const memberToDelete = ref<ITeamMember>() const memberToDelete = ref<ITeamMember>()
const newMember = ref<IUser>() const newMember = ref<IUser>()
const foundUsers = ref<IUser[]>() const foundUsers = ref<IUser[]>()
const showDeleteModal = ref(false) const showDeleteModal = ref(false)
const showUserDeleteModal = ref(false) const showUserDeleteModal = ref(false)
const showLeaveModal = ref(false)
const showError = ref(false) const showError = ref(false)
const title = ref('') const title = ref('')
@ -287,6 +306,19 @@ async function findUser(query: string) {
const users = await userService.value.getAll({}, {s: query}) const users = await userService.value.getAll({}, {s: query})
foundUsers.value = users.filter((u: IUser) => u.id !== userInfo.value.id) foundUsers.value = users.filter((u: IUser) => u.id !== userInfo.value.id)
} }
async function leave() {
try {
await teamMemberService.value.delete({
teamId: teamId.value,
username: userInfo.value.username,
})
success({message: t('team.edit.leave.success')})
await router.push({name: 'home'})
} finally {
showUserDeleteModal.value = false
}
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -104,7 +104,6 @@
<script setup lang="ts"> <script setup lang="ts">
import {computed, onBeforeMount, ref} from 'vue' import {computed, onBeforeMount, ref} from 'vue'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import {useRouter} from 'vue-router'
import {useDebounceFn} from '@vueuse/core' import {useDebounceFn} from '@vueuse/core'
import Message from '@/components/misc/message.vue' import Message from '@/components/misc/message.vue'
@ -112,19 +111,19 @@ import Password from '@/components/input/password.vue'
import {getErrorText} from '@/message' import {getErrorText} from '@/message'
import {redirectToProvider} from '@/helpers/redirectToProvider' import {redirectToProvider} from '@/helpers/redirectToProvider'
import {getLastVisited, clearLastVisited} from '@/helpers/saveLastVisited' import {useRedirectToLastVisited} from '@/composables/useRedirectToLastVisited'
import {useAuthStore} from '@/stores/auth' import {useAuthStore} from '@/stores/auth'
import {useConfigStore} from '@/stores/config' import {useConfigStore} from '@/stores/config'
import {useTitle} from '@/composables/useTitle' import {useTitle} from '@/composables/useTitle'
const router = useRouter()
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
useTitle(() => t('user.auth.login')) useTitle(() => t('user.auth.login'))
const authStore = useAuthStore() const authStore = useAuthStore()
const configStore = useConfigStore() const configStore = useConfigStore()
const {redirectIfSaved} = useRedirectToLastVisited()
const registrationEnabled = computed(() => configStore.registrationEnabled) const registrationEnabled = computed(() => configStore.registrationEnabled)
const localAuthEnabled = computed(() => configStore.auth.local.enabled) 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 // Check if the user is already logged in, if so, redirect them to the homepage
if (authenticated.value) { if (authenticated.value) {
const last = getLastVisited() redirectIfSaved()
if (last !== null) {
router.push({
name: last.name,
params: last.params,
})
clearLastVisited()
} else {
router.push({name: 'home'})
}
} }
}) })

View File

@ -3,6 +3,9 @@
<message variant="danger" v-if="errorMessage"> <message variant="danger" v-if="errorMessage">
{{ errorMessage }} {{ errorMessage }}
</message> </message>
<message variant="danger" v-if="errorMessageFromQuery" class="mt-2">
{{ errorMessageFromQuery }}
</message>
<message v-if="loading"> <message v-if="loading">
{{ $t('user.auth.authenticating') }} {{ $t('user.auth.authenticating') }}
</message> </message>
@ -15,24 +18,25 @@ export default { name: 'Auth' }
<script setup lang="ts"> <script setup lang="ts">
import {ref, computed, onMounted} from 'vue' import {ref, computed, onMounted} from 'vue'
import {useRoute, useRouter} from 'vue-router' import {useRoute} from 'vue-router'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import {getErrorText} from '@/message' import {getErrorText} from '@/message'
import Message from '@/components/misc/message.vue' import Message from '@/components/misc/message.vue'
import {clearLastVisited, getLastVisited} from '@/helpers/saveLastVisited' import {useRedirectToLastVisited} from '@/composables/useRedirectToLastVisited'
import {useAuthStore} from '@/stores/auth' import {useAuthStore} from '@/stores/auth'
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
const router = useRouter()
const route = useRoute() const route = useRoute()
const {redirectIfSaved} = useRedirectToLastVisited()
const authStore = useAuthStore() const authStore = useAuthStore()
const loading = computed(() => authStore.isLoading) const loading = computed(() => authStore.isLoading)
const errorMessage = ref('') const errorMessage = ref('')
const errorMessageFromQuery = computed(() => route.query.error)
async function authenticateWithCode() { async function authenticateWithCode() {
// This component gets mounted twice: The first time when the actual auth request hits the frontend, // This component gets mounted twice: The first time when the actual auth request hits the frontend,
@ -70,16 +74,7 @@ async function authenticateWithCode() {
provider: route.params.provider, provider: route.params.provider,
code: route.query.code, code: route.query.code,
}) })
const last = getLastVisited() redirectIfSaved()
if (last !== null) {
router.push({
name: last.name,
params: last.params,
})
clearLastVisited()
} else {
router.push({name: 'home'})
}
} catch(e) { } catch(e) {
const err = getErrorText(e) const err = getErrorText(e)
errorMessage.value = typeof err[1] !== 'undefined' ? err[1] : err[0] errorMessage.value = typeof err[1] !== 'undefined' ? err[1] : err[0]

View File

@ -7,41 +7,14 @@
<message variant="success"> <message variant="success">
{{ successMessage }} {{ successMessage }}
</message> </message>
<x-button :to="{ name: 'user.login' }"> <x-button :to="{ name: 'user.login' }" class="mt-4">
{{ $t('user.auth.login') }} {{ $t('user.auth.login') }}
</x-button> </x-button>
</div> </div>
<form @submit.prevent="submit" id="form" v-if="!successMessage"> <form @submit.prevent="submit" id="form" v-if="!successMessage">
<div class="field"> <div class="field">
<label class="label" for="password1">{{ $t('user.auth.password') }}</label> <label class="label" for="password">{{ $t('user.auth.password') }}</label>
<div class="control"> <Password @submit="submit" @update:modelValue="v => credentials.password = v"/>
<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>
</div> </div>
<div class="field is-grouped"> <div class="field is-grouped">
@ -60,17 +33,14 @@
<script setup lang="ts"> <script setup lang="ts">
import {ref, reactive} from 'vue' import {ref, reactive} from 'vue'
import {useI18n} from 'vue-i18n'
import PasswordResetModel from '@/models/passwordReset' import PasswordResetModel from '@/models/passwordReset'
import PasswordResetService from '@/services/passwordReset' import PasswordResetService from '@/services/passwordReset'
import Message from '@/components/misc/message.vue' import Message from '@/components/misc/message.vue'
import Password from '@/components/input/password.vue'
const {t} = useI18n({useScope: 'global'})
const credentials = reactive({ const credentials = reactive({
password: '', password: '',
password2: '',
}) })
const passwordResetService = reactive(new PasswordResetService()) const passwordResetService = reactive(new PasswordResetService())
@ -79,15 +49,14 @@ const successMessage = ref('')
async function submit() { async function submit() {
errorMsg.value = '' errorMsg.value = ''
if (credentials.password2 !== credentials.password) { if(credentials.password === '') {
errorMsg.value = t('user.auth.passwordsDontMatch')
return return
} }
const passwordReset = new PasswordResetModel({newPassword: credentials.password}) const passwordReset = new PasswordResetModel({newPassword: credentials.password})
try { try {
const {message} = passwordResetService.resetPassword(passwordReset) const {message} = await passwordResetService.resetPassword(passwordReset)
successMessage.value = message successMessage.value = message
localStorage.removeItem('passwordResetToken') localStorage.removeItem('passwordResetToken')
} catch (e) { } catch (e) {

View File

@ -7,7 +7,7 @@
<message variant="success"> <message variant="success">
{{ $t('user.auth.resetPasswordSuccess') }} {{ $t('user.auth.resetPasswordSuccess') }}
</message> </message>
<x-button :to="{ name: 'user.login' }"> <x-button :to="{ name: 'user.login' }" class="mt-4">
{{ $t('user.auth.login') }} {{ $t('user.auth.login') }}
</x-button> </x-button>
</div> </div>

View File

@ -6,6 +6,8 @@ import legacyFn from '@vitejs/plugin-legacy'
import {VitePWA} from 'vite-plugin-pwa' import {VitePWA} from 'vite-plugin-pwa'
import {visualizer} from 'rollup-plugin-visualizer' import {visualizer} from 'rollup-plugin-visualizer'
import svgLoader from 'vite-svg-loader' import svgLoader from 'vite-svg-loader'
import postcssPresetEnv from "postcss-preset-env";
import { fileURLToPath, URL } from 'url' import { fileURLToPath, URL } from 'url'
const pathSrc = fileURLToPath(new URL('./src', import.meta.url)) const pathSrc = fileURLToPath(new URL('./src', import.meta.url))
@ -40,6 +42,11 @@ export default defineConfig({
charset: false, // fixes "@charset" must be the first rule in the file" warnings charset: false, // fixes "@charset" must be the first rule in the file" warnings
}, },
}, },
postcss: {
plugins: [
postcssPresetEnv(),
],
},
}, },
plugins: [ plugins: [
vue({ vue({