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')
.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()
.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 '../../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', () => {
let namespaces
let lists
let buckets
beforeEach(() => {
UserFactory.create(1)
namespaces = NamespaceFactory.create(1)
lists = ListFactory.create(1)
buckets = BucketFactory.create(1, {
list_id: lists[0].id,
})
TaskFactory.truncate()
UserListFactory.truncate()
})
@ -80,6 +116,7 @@ describe('Task', () => {
describe('Task Detail View', () => {
beforeEach(() => {
TaskCommentFactory.truncate()
LabelTaskFactory.truncate()
})
it('Shows all task details', () => {
@ -344,21 +381,31 @@ describe('Task', () => {
cy.visit(`/tasks/${tasks[0].id}`)
cy.get('.task-view .action-buttons .button')
.contains('Add Labels')
addLabelToTaskAndVerify(labels[0].title)
})
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()
cy.get('.task-view .details.labels-list .multiselect input')
.type(labels[0].title)
cy.get('.task-view .details.labels-list .multiselect .search-results')
.children()
.first()
addLabelToTaskAndVerify(labels[0].title)
cy.get('.modal-content .close')
.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', labels[0].title)
cy.get('.bucket .task')
.should('contain.text', labels[0].title)
})
it('Can remove a label from a task', () => {
@ -417,5 +464,117 @@ describe('Task', () => {
cy.get('.global-notification')
.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)
})
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": [
{
"matchPackageNames": ["netlify-cli"],
"matchPackageNames": ["netlify-cli", "happy-dom"],
"extends": ["schedule:weekly"]
},
{

View File

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

View File

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

View File

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

View File

@ -210,6 +210,7 @@ import ListService from '@/services/list'
import NamespaceService from '@/services/namespace'
import EditLabels from '@/components/tasks/partials/editLabels.vue'
import {dateIsValid, formatISO} from '@/helpers/time/formatDate'
import {objectToSnakeCase} from '@/helpers/case'
import {getDefaultParams} from '@/composables/taskList'
import {camelCase} from 'camel-case'
@ -391,7 +392,14 @@ export default defineComponent({
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()
return
}
@ -511,12 +519,12 @@ export default defineComponent({
if (typeof this.filters[filterName] === 'undefined' || this.filters[filterName] === '') {
return
}
// 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
// from the outside. It is still fine because we're not changing them from the outside, other than
// loading them initially.
if(this[kind].length > 0) {
if (this[kind].length > 0) {
return
}

View File

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

View File

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

View File

@ -69,17 +69,38 @@ const emit = defineEmits(['update:modelValue'])
const subscriptionService = shallowRef(new SubscriptionService())
const {t} = useI18n({useScope: 'global'})
const tooltipText = computed(() => {
if (disabled.value) {
return t('task.subscription.subscribedThroughParent', {
entity: props.entity,
parent: subscriptionEntity.value,
})
if (props.entity === 'list' && subscriptionEntity.value === 'namespace') {
return t('task.subscription.subscribedListThroughParentNamespace')
}
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 ?
t('task.subscription.subscribed', {entity: props.entity}) :
t('task.subscription.notSubscribed', {entity: props.entity})
switch (props.entity) {
case 'namespace':
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'))
@ -105,7 +126,20 @@ async function subscribe() {
})
await subscriptionService.value.create(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() {
@ -115,6 +149,19 @@ async function unsubscribe() {
})
await subscriptionService.value.delete(subscription)
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>

View File

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

View File

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

View File

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

View File

@ -11,62 +11,70 @@
@click.ctrl="() => toggleTaskDone(task)"
@click.meta="() => toggleTaskDone(task)"
>
<span class="task-id">
<Done class="kanban-card__done" :is-done="task.done" variant="small"/>
<template v-if="task.identifier === ''">
#{{ task.index }}
</template>
<template v-else>
{{ task.identifier }}
</template>
</span>
<span
:class="{'overdue': task.dueDate <= new Date() && !task.done}"
class="due-date"
v-if="task.dueDate > 0"
v-tooltip="formatDateLong(task.dueDate)">
<span class="icon">
<icon :icon="['far', 'calendar-alt']"/>
<img
v-if="coverImageBlobUrl"
:src="coverImageBlobUrl"
alt=""
class="cover-image"
/>
<div class="p-2">
<span class="task-id">
<Done class="kanban-card__done" :is-done="task.done" variant="small"/>
<template v-if="task.identifier === ''">
#{{ task.index }}
</template>
<template v-else>
{{ task.identifier }}
</template>
</span>
<time :datetime="formatISO(task.dueDate)">
{{ formatDateSince(task.dueDate) }}
</time>
</span>
<h3>{{ task.title }}</h3>
<progress
class="progress is-small"
v-if="task.percentDone > 0"
:value="task.percentDone * 100" max="100">
{{ task.percentDone * 100 }}%
</progress>
<div class="footer">
<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"
/>
<span
:class="{'overdue': task.dueDate <= new Date() && !task.done}"
class="due-date"
v-if="task.dueDate > 0"
v-tooltip="formatDateLong(task.dueDate)">
<span class="icon">
<icon :icon="['far', 'calendar-alt']"/>
</span>
<time :datetime="formatISO(task.dueDate)">
{{ formatDateSince(task.dueDate) }}
</time>
</span>
<h3>{{ task.title }}</h3>
<progress
class="progress is-small"
v-if="task.percentDone > 0"
:value="task.percentDone * 100" max="100">
{{ task.percentDone * 100 }}%
</progress>
<div class="footer">
<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>
<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>
</template>
<script lang="ts" setup>
import {ref, computed} from 'vue'
import {ref, computed, watch} from 'vue'
import {useRouter} from 'vue-router'
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 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 {colorIsDark} from '@/helpers/color/colorIsDark'
@ -114,6 +124,29 @@ function openTaskDetail() {
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>
<style lang="scss" scoped>
@ -125,12 +158,11 @@ $task-background: var(--white);
cursor: pointer;
box-shadow: var(--shadow-xs);
display: block;
border: 3px solid transparent;
font-size: .9rem;
padding: .4rem;
border-radius: $radius;
background: $task-background;
overflow: hidden;
&.loader-container.is-loading::after {
width: 1.5rem;

View File

@ -37,7 +37,11 @@
@create="createAndRelateTask"
>
<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
class="different-list"
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()
// Check if the token is still valid if the window gets focus again to maybe renew it
useEventListener('focus', () => {
useEventListener('focus', async () => {
if (!authenticated.value) {
return
}
@ -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
// the user to the login page
if (expiresIn < 0) {
authStore.checkAuth()
router.push({name: 'user.login'})
await authStore.checkAuth()
await router.push({name: 'user.login'})
return
}

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'
export const saveLastVisited = (name: string, params: object) => {
localStorage.setItem(LAST_VISITED_KEY, JSON.stringify({name, params}))
export const saveLastVisited = (name: string, params: object, query: object) => {
localStorage.setItem(LAST_VISITED_KEY, JSON.stringify({name, params, query}))
}
export const getLastVisited = () => {

View File

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

View File

@ -1,9 +1,9 @@
{
"home": {
"welcomeNight": "Good Night {username}",
"welcomeMorning": "Good Morning {username}",
"welcomeDay": "Hi {username}",
"welcomeEvening": "Good Evening {username}",
"welcomeNight": "Good Night {username}!",
"welcomeMorning": "Good Morning {username}!",
"welcomeDay": "Hi {username}!",
"welcomeEvening": "Good Evening {username}!",
"lastViewed": "Last viewed",
"list": {
"newText": "You can create a new list for your new tasks:",
@ -169,6 +169,14 @@
"title": "List Title",
"color": "Color",
"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…",
"searchSelect": "Click or press enter to select this list",
"shared": "Shared Lists",
@ -270,14 +278,6 @@
"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": {
"title": "Gantt",
"showTasksWithoutDates": "Show tasks which don't have dates set",
@ -672,13 +672,23 @@
"updated": "Updated"
},
"subscription": {
"subscribedThroughParent": "You can't unsubscribe here because you are subscribed to this {entity} through its {parent}.",
"subscribed": "You are currently subscribed to this {entity} and will receive notifications for changes.",
"notSubscribed": "You are not subscribed to this {entity} and won't receive notifications for changes.",
"subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"subscribedTaskThroughParentList": "You can't unsubscribe here because you are subscribed to this task through its list.",
"subscribedNamespace": "You are currently subscribed to this namespace and will receive notifications for changes.",
"notSubscribedNamespace": "You are not subscribed to this namespace and won't receive notifications for changes.",
"subscribedList": "You are currently subscribed to this list and will receive notifications for changes.",
"notSubscribedList": "You are not subscribed to this list and won't receive notifications for changes.",
"subscribedTask": "You are currently subscribed to this task and will receive notifications for changes.",
"notSubscribedTask": "You are not subscribed to this task and won't receive notifications for changes.",
"subscribe": "Subscribe",
"unsubscribe": "Unsubscribe",
"subscribeSuccess": "You are now subscribed to this {entity}",
"unsubscribeSuccess": "You are now unsubscribed to this {entity}"
"subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccessNamespace": "You are now unsubscribed to this namespace",
"subscribeSuccessList": "You are now subscribed to this list",
"unsubscribeSuccessList": "You are now unsubscribed to this list",
"subscribeSuccessTask": "You are now subscribed to this task",
"unsubscribeSuccessTask": "You are now unsubscribed to this task"
},
"attachment": {
"title": "Attachments",
@ -690,7 +700,11 @@
"deleteTooltip": "Delete this attachment",
"deleteText1": "Are you sure you want to delete the attachment {filename}?",
"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": {
"title": "Comments",
@ -839,6 +853,12 @@
"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!",
"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": {

View File

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

View File

@ -1,9 +1,9 @@
{
"home": {
"welcomeNight": "Gute Nacht, {username}",
"welcomeMorning": "Guten Morgen, {username}",
"welcomeDay": "Hallo, {username}",
"welcomeEvening": "Guten Abend, {username}",
"welcomeNight": "Gute Nacht, {username}!",
"welcomeMorning": "Guten Morgen, {username}!",
"welcomeDay": "Hallo {username}!",
"welcomeEvening": "Guten Abend, {username}!",
"lastViewed": "Zuletzt angesehen",
"list": {
"newText": "Du kannst eine neue Liste für deine neuen Aufgaben erstellen:",
@ -169,6 +169,14 @@
"title": "Listentitel",
"color": "Farbe",
"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…",
"searchSelect": "Klicke auf oder drücke die Eingabetaste, um diese Liste auszuwählen",
"shared": "Geteilte Listen",
@ -270,14 +278,6 @@
"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": {
"title": "Gantt",
"showTasksWithoutDates": "Aufgaben anzeigen, für die keine Daten festgelegt sind",
@ -335,7 +335,7 @@
"create": {
"title": "Neuer Namespace",
"titleRequired": "Bitte gebe einen Titel an.",
"explanation": "Ein Namespace ist eine Sammlung von Listen, die du teilen und zur Organisation verwenden kannst. Jede Liste zu einem Namespace.",
"explanation": "Ein Namespace ist eine Sammlung von Listen, die du teilen und zur Organisation verwenden kannst. Jede Liste gehört zu einem Namespace.",
"tooltip": "Was ist ein Namespace?",
"success": "Der Namespace wurde erfolgreich erstellt."
},
@ -672,13 +672,23 @@
"updated": "Aktualisiert"
},
"subscription": {
"subscribedThroughParent": "Du kannst hier nicht de-abonnieren, da du diese {entity} durch {parent} abonniert hast.",
"subscribed": "Du erhältst Benachrichtigungen zu dieser {entity}.",
"notSubscribed": "Du hast diese {entity} nicht abonniert und erhältst keine Benachrichtigungen über Änderungen.",
"subscribedListThroughParentNamespace": "Du kannst hier nicht de-abonnieren, da du diese Liste über ihren Namespace abonniert hast.",
"subscribedTaskThroughParentNamespace": "Du kannst hier nicht de-abonnieren, da du diese Aufgabe über ihren Namespace abonniert hast.",
"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",
"unsubscribe": "Abbestellen",
"subscribeSuccess": "Du hast jetzt diese {entity} abonniert",
"unsubscribeSuccess": "Du hast diese {entity} jetzt abbestellt"
"subscribeSuccessNamespace": "Du hast diesen Namespace jetzt abonniert",
"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": {
"title": "Anhänge",
@ -690,7 +700,11 @@
"deleteTooltip": "Diesen Anhang löschen",
"deleteText1": "Soll der Anhang {filename} gelöscht werden?",
"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": {
"title": "Kommentare",
@ -839,6 +853,12 @@
"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!",
"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": {

View File

@ -1,9 +1,9 @@
{
"home": {
"welcomeNight": "Guet Nacht, {username}",
"welcomeMorning": "Guete Morgä, {username}",
"welcomeDay": "Hoi {username}",
"welcomeEvening": "Guete Abig, {username}",
"welcomeNight": "Gute Nacht, {username}!",
"welcomeMorning": "Guten Morgen, {username}!",
"welcomeDay": "Hallo {username}!",
"welcomeEvening": "Guten Abend, {username}!",
"lastViewed": "Zletscht ahglueget",
"list": {
"newText": "Du chasch e Liste für dini neue Uufgabe erstelle:",
@ -169,6 +169,14 @@
"title": "Liste Titl",
"color": "Farb",
"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…",
"searchSelect": "Druck uf Enter um die Liste uuszwähle",
"shared": "Teilti Liste",
@ -270,14 +278,6 @@
"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": {
"title": "Gantt",
"showTasksWithoutDates": "Zeig Uufgabe, wo kei Date hend",
@ -335,7 +335,7 @@
"create": {
"title": "Neuer Namespace",
"titleRequired": "Bitte gib en Titl ah.",
"explanation": "Ein Namespace ist eine Sammlung von Listen, die du teilen und zur Organisation verwenden kannst. Jede Liste zu einem Namespace.",
"explanation": "Ein Namespace ist eine Sammlung von Listen, die du teilen und zur Organisation verwenden kannst. Jede Liste gehört zu einem Namespace.",
"tooltip": "Was isch en Namensruum?",
"success": "Namensruum erstellt."
},
@ -672,13 +672,23 @@
"updated": "Aktualisiert"
},
"subscription": {
"subscribedThroughParent": "Du chasch da nid deabonnierä, weil du zu dere {entity} durch {parent} dezue abonniert bisch.",
"subscribed": "Du bisch momentan zu dere {entity} abonniert und bechunsch Benachrichtigunge für Änderige.",
"notSubscribed": "Du bisch momentan nid zu dere {entity} abonniert und bechunsch kei Benachrichtigunge für Änderige.",
"subscribedListThroughParentNamespace": "Du kannst hier nicht de-abonnieren, da du diese Liste über ihren Namespace abonniert hast.",
"subscribedTaskThroughParentNamespace": "Du kannst hier nicht de-abonnieren, da du diese Aufgabe über ihren Namespace abonniert hast.",
"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",
"unsubscribe": "Deabonniere",
"subscribeSuccess": "Du hesch die {entity} abonniert",
"unsubscribeSuccess": "Du hesch die {entity} deabonniert"
"subscribeSuccessNamespace": "Du hast diesen Namespace jetzt abonniert",
"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": {
"title": "Aahhäng",
@ -690,7 +700,11 @@
"deleteTooltip": "De Aahhang lösche",
"deleteText1": "Bisch du dir sicher, dass du de Aahang {filename} lösche wetsch?",
"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": {
"title": "Kommentär",
@ -839,6 +853,12 @@
"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!",
"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": {

View File

@ -1,9 +1,9 @@
{
"home": {
"welcomeNight": "Good Night {username}",
"welcomeMorning": "Good Morning {username}",
"welcomeDay": "Hi {username}",
"welcomeEvening": "Good Evening {username}",
"welcomeNight": "Good Night {username}!",
"welcomeMorning": "Good Morning {username}!",
"welcomeDay": "Hi {username}!",
"welcomeEvening": "Good Evening {username}!",
"lastViewed": "Last viewed",
"list": {
"newText": "You can create a new list for your new tasks:",
@ -169,6 +169,7 @@
"title": "List Title",
"color": "Color",
"lists": "Lists",
"list": "List",
"search": "Type to search for a list…",
"searchSelect": "Click or press enter to select this list",
"shared": "Shared Lists",
@ -675,13 +676,23 @@
"updated": "Updated"
},
"subscription": {
"subscribedThroughParent": "You can't unsubscribe here because you are subscribed to this {entity} through its {parent}.",
"subscribed": "You are currently subscribed to this {entity} and will receive notifications for changes.",
"notSubscribed": "You are not subscribed to this {entity} and won't receive notifications for changes.",
"subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"subscribedTaskThroughParentList": "You can't unsubscribe here because you are subscribed to this task through its list.",
"subscribedNamespace": "You are currently subscribed to this namespace and will receive notifications for changes.",
"notSubscribedNamespace": "You are not subscribed to this namespace and won't receive notifications for changes.",
"subscribedList": "You are currently subscribed to this list and will receive notifications for changes.",
"notSubscribedList": "You are not subscribed to this list and won't receive notifications for changes.",
"subscribedTask": "You are currently subscribed to this task and will receive notifications for changes.",
"notSubscribedTask": "You are not subscribed to this task and won't receive notifications for changes.",
"subscribe": "Subscribe",
"unsubscribe": "Unsubscribe",
"subscribeSuccess": "You are now subscribed to this {entity}",
"unsubscribeSuccess": "You are now unsubscribed to this {entity}"
"subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccessNamespace": "You are now unsubscribed to this namespace",
"subscribeSuccessList": "You are now subscribed to this list",
"unsubscribeSuccessList": "You are now unsubscribed to this list",
"subscribeSuccessTask": "You are now subscribed to this task",
"unsubscribeSuccessTask": "You are now unsubscribed to this task"
},
"attachment": {
"title": "Attachments",
@ -693,7 +704,11 @@
"deleteTooltip": "Delete this attachment",
"deleteText1": "Are you sure you want to delete the attachment {filename}?",
"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": {
"title": "Comments",
@ -842,6 +857,12 @@
"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!",
"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": {

View File

@ -1,9 +1,9 @@
{
"home": {
"welcomeNight": "Good Night {username}",
"welcomeMorning": "Good Morning {username}",
"welcomeDay": "Hi {username}",
"welcomeEvening": "Good Evening {username}",
"welcomeNight": "Good Night {username}!",
"welcomeMorning": "Good Morning {username}!",
"welcomeDay": "Hi {username}!",
"welcomeEvening": "Good Evening {username}!",
"lastViewed": "Last viewed",
"list": {
"newText": "You can create a new list for your new tasks:",
@ -43,7 +43,7 @@
"confirmEmailSuccess": "You successfully confirmed your email! You can log in now.",
"totpTitle": "Two Factor Authentication Code",
"totpPlaceholder": "e.g. 123456",
"login": "Login",
"login": "Iniciar sesión",
"createAccount": "Create account",
"loginWith": "Log in with {provider}",
"authenticating": "Authenticating…",
@ -169,6 +169,14 @@
"title": "List Title",
"color": "Color",
"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…",
"searchSelect": "Click or press enter to select this list",
"shared": "Shared Lists",
@ -270,14 +278,6 @@
"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": {
"title": "Gantt",
"showTasksWithoutDates": "Show tasks which don't have dates set",
@ -672,13 +672,23 @@
"updated": "Updated"
},
"subscription": {
"subscribedThroughParent": "You can't unsubscribe here because you are subscribed to this {entity} through its {parent}.",
"subscribed": "You are currently subscribed to this {entity} and will receive notifications for changes.",
"notSubscribed": "You are not subscribed to this {entity} and won't receive notifications for changes.",
"subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"subscribedTaskThroughParentList": "You can't unsubscribe here because you are subscribed to this task through its list.",
"subscribedNamespace": "You are currently subscribed to this namespace and will receive notifications for changes.",
"notSubscribedNamespace": "You are not subscribed to this namespace and won't receive notifications for changes.",
"subscribedList": "You are currently subscribed to this list and will receive notifications for changes.",
"notSubscribedList": "You are not subscribed to this list and won't receive notifications for changes.",
"subscribedTask": "You are currently subscribed to this task and will receive notifications for changes.",
"notSubscribedTask": "You are not subscribed to this task and won't receive notifications for changes.",
"subscribe": "Subscribe",
"unsubscribe": "Unsubscribe",
"subscribeSuccess": "You are now subscribed to this {entity}",
"unsubscribeSuccess": "You are now unsubscribed to this {entity}"
"subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccessNamespace": "You are now unsubscribed to this namespace",
"subscribeSuccessList": "You are now subscribed to this list",
"unsubscribeSuccessList": "You are now unsubscribed to this list",
"subscribeSuccessTask": "You are now subscribed to this task",
"unsubscribeSuccessTask": "You are now unsubscribed to this task"
},
"attachment": {
"title": "Attachments",
@ -690,7 +700,11 @@
"deleteTooltip": "Delete this attachment",
"deleteText1": "Are you sure you want to delete the attachment {filename}?",
"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": {
"title": "Comments",
@ -839,6 +853,12 @@
"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!",
"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": {

View File

@ -1,9 +1,9 @@
{
"home": {
"welcomeNight": "Bonne nuit {username}",
"welcomeMorning": "Bonjour {username}",
"welcomeDay": "Salut {username}",
"welcomeEvening": "Bonsoir {username}",
"welcomeNight": "Good Night {username}!",
"welcomeMorning": "Good Morning {username}!",
"welcomeDay": "Hi {username}!",
"welcomeEvening": "Good Evening {username}!",
"lastViewed": "Dernière consultation",
"list": {
"newText": "Tu peux créer une nouvelle liste pour tes nouvelles tâches :",
@ -169,6 +169,14 @@
"title": "Nom de la liste",
"color": "Couleur",
"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…",
"searchSelect": "Clique ou appuie sur la touche Entrée pour sélectionner cette liste",
"shared": "Listes partagées",
@ -270,14 +278,6 @@
"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": {
"title": "Gantt",
"showTasksWithoutDates": "Afficher les tâches pour lesquelles aucune date na été fixée",
@ -672,13 +672,23 @@
"updated": "Mis à jour"
},
"subscription": {
"subscribedThroughParent": "Tu ne peux pas te désabonner ici car tu es abonné·e à cette {entity} par le biais de son {parent}.",
"subscribed": "Tu es actuellement abonné·e à cette {entity} et recevras des notifications pour les changements.",
"notSubscribed": "Tu nes pas abonné·e à cette {entity} et ne recevras pas de notifications pour les changements.",
"subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"subscribedTaskThroughParentList": "You can't unsubscribe here because you are subscribed to this task through its list.",
"subscribedNamespace": "You are currently subscribed to this namespace and will receive notifications for changes.",
"notSubscribedNamespace": "You are not subscribed to this namespace and won't receive notifications for changes.",
"subscribedList": "You are currently subscribed to this list and will receive notifications for changes.",
"notSubscribedList": "You are not subscribed to this list and won't receive notifications for changes.",
"subscribedTask": "You are currently subscribed to this task and will receive notifications for changes.",
"notSubscribedTask": "You are not subscribed to this task and won't receive notifications for changes.",
"subscribe": "Sabonner",
"unsubscribe": "Se désabonner",
"subscribeSuccess": "Tu es maintenant abonné·e à cette {entity}",
"unsubscribeSuccess": "Tu es maintenant désabonné·e de cette {entity}"
"subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccessNamespace": "You are now unsubscribed to this namespace",
"subscribeSuccessList": "You are now subscribed to this list",
"unsubscribeSuccessList": "You are now unsubscribed to this list",
"subscribeSuccessTask": "You are now subscribed to this task",
"unsubscribeSuccessTask": "You are now unsubscribed to this task"
},
"attachment": {
"title": "Pièces jointes",
@ -690,7 +700,11 @@
"deleteTooltip": "Supprimer cette pièce jointe",
"deleteText1": "Supprimer la pièce jointe {filename} ?",
"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": {
"title": "Commentaires",
@ -839,6 +853,12 @@
"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É !",
"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": {

View File

@ -1,9 +1,9 @@
{
"home": {
"welcomeNight": "Buonanotte {username}",
"welcomeMorning": "Buongiorno {username}",
"welcomeDay": "Ciao {username}",
"welcomeEvening": "Buonasera {username}",
"welcomeNight": "Good Night {username}!",
"welcomeMorning": "Good Morning {username}!",
"welcomeDay": "Hi {username}!",
"welcomeEvening": "Good Evening {username}!",
"lastViewed": "Ultima visualizzazione",
"list": {
"newText": "È possibile creare una nuova lista per le nuove attività:",
@ -169,6 +169,14 @@
"title": "Titolo della Lista",
"color": "Colore",
"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…",
"searchSelect": "Fare clic o premere invio per selezionare questa lista",
"shared": "Liste Condivise",
@ -270,14 +278,6 @@
"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": {
"title": "Gantt",
"showTasksWithoutDates": "Mostra attività che non hanno date impostate",
@ -672,13 +672,23 @@
"updated": "Aggiornato"
},
"subscription": {
"subscribedThroughParent": "Non puoi annullare l'iscrizione qui perché sei iscritto a questo {entity} attraverso il suo {parent}.",
"subscribed": "Sei attualmente iscritto a questo {entity} e riceverai notifiche per le modifiche.",
"notSubscribed": "Non sei iscritto a questo {entity} e non riceverai notifiche per le modifiche.",
"subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"subscribedTaskThroughParentList": "You can't unsubscribe here because you are subscribed to this task through its list.",
"subscribedNamespace": "You are currently subscribed to this namespace and will receive notifications for changes.",
"notSubscribedNamespace": "You are not subscribed to this namespace and won't receive notifications for changes.",
"subscribedList": "You are currently subscribed to this list and will receive notifications for changes.",
"notSubscribedList": "You are not subscribed to this list and won't receive notifications for changes.",
"subscribedTask": "You are currently subscribed to this task and will receive notifications for changes.",
"notSubscribedTask": "You are not subscribed to this task and won't receive notifications for changes.",
"subscribe": "Iscriviti",
"unsubscribe": "Disiscriviti",
"subscribeSuccess": "Ti sei iscritto a questo {entity}",
"unsubscribeSuccess": "Ti sei disiscritto a questo {entity}"
"subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccessNamespace": "You are now unsubscribed to this namespace",
"subscribeSuccessList": "You are now subscribed to this list",
"unsubscribeSuccessList": "You are now unsubscribed to this list",
"subscribeSuccessTask": "You are now subscribed to this task",
"unsubscribeSuccessTask": "You are now unsubscribed to this task"
},
"attachment": {
"title": "Allegati",
@ -690,7 +700,11 @@
"deleteTooltip": "Elimina questo allegato",
"deleteText1": "Sei sicuro di voler eliminare l'allegato {filename}?",
"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": {
"title": "Commenti",
@ -839,6 +853,12 @@
"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!",
"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": {

View File

@ -1,9 +1,9 @@
{
"home": {
"welcomeNight": "Goede nacht {username}",
"welcomeMorning": "Goedemorgen {username}",
"welcomeDay": "Hallo {username}",
"welcomeEvening": "Goedenavond {username}",
"welcomeNight": "Good Night {username}!",
"welcomeMorning": "Good Morning {username}!",
"welcomeDay": "Hi {username}!",
"welcomeEvening": "Good Evening {username}!",
"lastViewed": "Laatst bekeken",
"list": {
"newText": "Je kan een nieuwe lijst maken voor je nieuwe taken:",
@ -169,6 +169,14 @@
"title": "Lijst titel",
"color": "Kleur",
"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…",
"searchSelect": "Klik of druk op enter om deze lijst te selecteren",
"shared": "Gedeelde lijsten",
@ -270,14 +278,6 @@
"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": {
"title": "Gantt",
"showTasksWithoutDates": "Toon taken waarvoor geen datums zijn ingesteld",
@ -672,13 +672,23 @@
"updated": "Bijgewerkt"
},
"subscription": {
"subscribedThroughParent": "You can't unsubscribe here because you are subscribed to this {entity} through its {parent}.",
"subscribed": "You are currently subscribed to this {entity} and will receive notifications for changes.",
"notSubscribed": "You are not subscribed to this {entity} and won't receive notifications for changes.",
"subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"subscribedTaskThroughParentList": "You can't unsubscribe here because you are subscribed to this task through its list.",
"subscribedNamespace": "You are currently subscribed to this namespace and will receive notifications for changes.",
"notSubscribedNamespace": "You are not subscribed to this namespace and won't receive notifications for changes.",
"subscribedList": "You are currently subscribed to this list and will receive notifications for changes.",
"notSubscribedList": "You are not subscribed to this list and won't receive notifications for changes.",
"subscribedTask": "You are currently subscribed to this task and will receive notifications for changes.",
"notSubscribedTask": "You are not subscribed to this task and won't receive notifications for changes.",
"subscribe": "Subscribe",
"unsubscribe": "Unsubscribe",
"subscribeSuccess": "You are now subscribed to this {entity}",
"unsubscribeSuccess": "You are now unsubscribed to this {entity}"
"subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccessNamespace": "You are now unsubscribed to this namespace",
"subscribeSuccessList": "You are now subscribed to this list",
"unsubscribeSuccessList": "You are now unsubscribed to this list",
"subscribeSuccessTask": "You are now subscribed to this task",
"unsubscribeSuccessTask": "You are now unsubscribed to this task"
},
"attachment": {
"title": "Bijlagen",
@ -690,7 +700,11 @@
"deleteTooltip": "Verwijder deze bijlage",
"deleteText1": "Weet je zeker dat je de bijlage {filename} wilt verwijderen?",
"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": {
"title": "Reacties",
@ -839,6 +853,12 @@
"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!",
"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": {

View File

@ -1,9 +1,9 @@
{
"home": {
"welcomeNight": "Dobrej nocy {username}",
"welcomeMorning": "Dzień dobry {username}",
"welcomeDay": "Cześć {username}",
"welcomeEvening": "Dobry wieczór {username}",
"welcomeNight": "Good Night {username}!",
"welcomeMorning": "Good Morning {username}!",
"welcomeDay": "Hi {username}!",
"welcomeEvening": "Good Evening {username}!",
"lastViewed": "Ostatnio oglądane",
"list": {
"newText": "Możesz stworzyć nową listę dla swoich nowych zadań:",
@ -169,6 +169,14 @@
"title": "Tytuł listy",
"color": "Kolor",
"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ę…",
"searchSelect": "Kliknij lub naciśnij Enter, aby wybrać tę listę",
"shared": "Współdzielone listy",
@ -270,14 +278,6 @@
"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": {
"title": "Gantt",
"showTasksWithoutDates": "Pokaż zadania, które nie mają ustawionych dat",
@ -672,13 +672,23 @@
"updated": "Zaktualizowano"
},
"subscription": {
"subscribedThroughParent": "Nie możesz zrezygnować z subskrypcji, ponieważ subskrybujesz {entity} za pośrednictwem {parent}.",
"subscribed": "Obecnie subskrybujesz {entity} i będziesz otrzymywać powiadomienia o zmianach.",
"notSubscribed": "Nie subskrybujesz {entity} i nie będziesz otrzymywać powiadomień o zmianach.",
"subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"subscribedTaskThroughParentList": "You can't unsubscribe here because you are subscribed to this task through its list.",
"subscribedNamespace": "You are currently subscribed to this namespace and will receive notifications for changes.",
"notSubscribedNamespace": "You are not subscribed to this namespace and won't receive notifications for changes.",
"subscribedList": "You are currently subscribed to this list and will receive notifications for changes.",
"notSubscribedList": "You are not subscribed to this list and won't receive notifications for changes.",
"subscribedTask": "You are currently subscribed to this task and will receive notifications for changes.",
"notSubscribedTask": "You are not subscribed to this task and won't receive notifications for changes.",
"subscribe": "Subskrybuj",
"unsubscribe": "Anuluj subskrypcję",
"subscribeSuccess": "Od teraz subskrybujesz {entity}",
"unsubscribeSuccess": "Już nie subskrybujesz {entity}"
"subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccessNamespace": "You are now unsubscribed to this namespace",
"subscribeSuccessList": "You are now subscribed to this list",
"unsubscribeSuccessList": "You are now unsubscribed to this list",
"subscribeSuccessTask": "You are now subscribed to this task",
"unsubscribeSuccessTask": "You are now unsubscribed to this task"
},
"attachment": {
"title": "Załączniki",
@ -690,7 +700,11 @@
"deleteTooltip": "Usuń ten załącznik",
"deleteText1": "Czy na pewno chcesz usunąć załącznik {filename}?",
"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": {
"title": "Komentarze",
@ -839,6 +853,12 @@
"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ĄĆ!",
"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": {

View File

@ -1,9 +1,9 @@
{
"home": {
"welcomeNight": "Good Night {username}",
"welcomeMorning": "Good Morning {username}",
"welcomeDay": "Hi {username}",
"welcomeEvening": "Good Evening {username}",
"welcomeNight": "Good Night {username}!",
"welcomeMorning": "Good Morning {username}!",
"welcomeDay": "Hi {username}!",
"welcomeEvening": "Good Evening {username}!",
"lastViewed": "Last viewed",
"list": {
"newText": "You can create a new list for your new tasks:",
@ -169,6 +169,14 @@
"title": "List Title",
"color": "Color",
"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…",
"searchSelect": "Click or press enter to select this list",
"shared": "Shared Lists",
@ -270,14 +278,6 @@
"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": {
"title": "Gantt",
"showTasksWithoutDates": "Show tasks which don't have dates set",
@ -672,13 +672,23 @@
"updated": "Updated"
},
"subscription": {
"subscribedThroughParent": "You can't unsubscribe here because you are subscribed to this {entity} through its {parent}.",
"subscribed": "You are currently subscribed to this {entity} and will receive notifications for changes.",
"notSubscribed": "You are not subscribed to this {entity} and won't receive notifications for changes.",
"subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"subscribedTaskThroughParentList": "You can't unsubscribe here because you are subscribed to this task through its list.",
"subscribedNamespace": "You are currently subscribed to this namespace and will receive notifications for changes.",
"notSubscribedNamespace": "You are not subscribed to this namespace and won't receive notifications for changes.",
"subscribedList": "You are currently subscribed to this list and will receive notifications for changes.",
"notSubscribedList": "You are not subscribed to this list and won't receive notifications for changes.",
"subscribedTask": "You are currently subscribed to this task and will receive notifications for changes.",
"notSubscribedTask": "You are not subscribed to this task and won't receive notifications for changes.",
"subscribe": "Subscribe",
"unsubscribe": "Unsubscribe",
"subscribeSuccess": "You are now subscribed to this {entity}",
"unsubscribeSuccess": "You are now unsubscribed to this {entity}"
"subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccessNamespace": "You are now unsubscribed to this namespace",
"subscribeSuccessList": "You are now subscribed to this list",
"unsubscribeSuccessList": "You are now unsubscribed to this list",
"subscribeSuccessTask": "You are now subscribed to this task",
"unsubscribeSuccessTask": "You are now unsubscribed to this task"
},
"attachment": {
"title": "Attachments",
@ -690,7 +700,11 @@
"deleteTooltip": "Delete this attachment",
"deleteText1": "Are you sure you want to delete the attachment {filename}?",
"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": {
"title": "Comments",
@ -839,6 +853,12 @@
"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!",
"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": {

View File

@ -1,9 +1,9 @@
{
"home": {
"welcomeNight": "Boa Noite {username}",
"welcomeMorning": "Bom Dia {username}",
"welcomeDay": "Olá {username}",
"welcomeEvening": "Boa Tarde {username}",
"welcomeNight": "Boa Noite {username}!",
"welcomeMorning": "Bom Dia {username}!",
"welcomeDay": "Olá {username}!",
"welcomeEvening": "Boa Tarde {username}!",
"lastViewed": "Visto recentemente",
"list": {
"newText": "Podes criar uma nova lista para as tuas novas tarefas:",
@ -169,6 +169,14 @@
"title": "Título da Lista",
"color": "Cor",
"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…",
"searchSelect": "Clica ou pressiona Enter para selecionar esta lista",
"shared": "Listas Partilhadas",
@ -270,14 +278,6 @@
"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": {
"title": "Gantt",
"showTasksWithoutDates": "Mostrar tarefas que não têm datas atríbuidas",
@ -672,13 +672,23 @@
"updated": "Atualizado"
},
"subscription": {
"subscribedThroughParent": "Não podes cancelar a tua subscrição aqui porque estás subscrito nesta {entity} através de {parent}.",
"subscribed": "Estás atualmente subscrito a esta {entity} e serás notificado de alterações.",
"notSubscribed": "Não estás subscrito a esta {entity} e não serás notificado de alterações.",
"subscribedListThroughParentNamespace": "Não podes cancelar a tua subscrição aqui porque estás subscrito nesta lista através do seu espaço.",
"subscribedTaskThroughParentNamespace": "Não podes cancelar a tua subscrição aqui porque estás subscrito nesta tarefa através do seu espaço.",
"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",
"unsubscribe": "Remover Subscrição",
"subscribeSuccess": "Estás agora subscrito a esta {entity}",
"unsubscribeSuccess": "Não estás mais subcrito a esta {entity}"
"subscribeSuccessNamespace": "Estás agora subscrito a este espaço",
"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": {
"title": "Anexos",
@ -690,7 +700,11 @@
"deleteTooltip": "Eliminar este anexo",
"deleteText1": "Tens a certeza que pretendes eliminar o anexo {filename}?",
"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": {
"title": "Comentários",
@ -839,6 +853,12 @@
"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!",
"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": {

View File

@ -1,9 +1,9 @@
{
"home": {
"welcomeNight": "Good Night {username}",
"welcomeMorning": "Good Morning {username}",
"welcomeDay": "Hi {username}",
"welcomeEvening": "Good Evening {username}",
"welcomeNight": "Good Night {username}!",
"welcomeMorning": "Good Morning {username}!",
"welcomeDay": "Hi {username}!",
"welcomeEvening": "Good Evening {username}!",
"lastViewed": "Last viewed",
"list": {
"newText": "You can create a new list for your new tasks:",
@ -169,6 +169,14 @@
"title": "List Title",
"color": "Color",
"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…",
"searchSelect": "Click or press enter to select this list",
"shared": "Shared Lists",
@ -270,14 +278,6 @@
"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": {
"title": "Gantt",
"showTasksWithoutDates": "Show tasks which don't have dates set",
@ -672,13 +672,23 @@
"updated": "Updated"
},
"subscription": {
"subscribedThroughParent": "You can't unsubscribe here because you are subscribed to this {entity} through its {parent}.",
"subscribed": "You are currently subscribed to this {entity} and will receive notifications for changes.",
"notSubscribed": "You are not subscribed to this {entity} and won't receive notifications for changes.",
"subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"subscribedTaskThroughParentList": "You can't unsubscribe here because you are subscribed to this task through its list.",
"subscribedNamespace": "You are currently subscribed to this namespace and will receive notifications for changes.",
"notSubscribedNamespace": "You are not subscribed to this namespace and won't receive notifications for changes.",
"subscribedList": "You are currently subscribed to this list and will receive notifications for changes.",
"notSubscribedList": "You are not subscribed to this list and won't receive notifications for changes.",
"subscribedTask": "You are currently subscribed to this task and will receive notifications for changes.",
"notSubscribedTask": "You are not subscribed to this task and won't receive notifications for changes.",
"subscribe": "Subscribe",
"unsubscribe": "Unsubscribe",
"subscribeSuccess": "You are now subscribed to this {entity}",
"unsubscribeSuccess": "You are now unsubscribed to this {entity}"
"subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccessNamespace": "You are now unsubscribed to this namespace",
"subscribeSuccessList": "You are now subscribed to this list",
"unsubscribeSuccessList": "You are now unsubscribed to this list",
"subscribeSuccessTask": "You are now subscribed to this task",
"unsubscribeSuccessTask": "You are now unsubscribed to this task"
},
"attachment": {
"title": "Attachments",
@ -690,7 +700,11 @@
"deleteTooltip": "Delete this attachment",
"deleteText1": "Are you sure you want to delete the attachment {filename}?",
"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": {
"title": "Comments",
@ -839,6 +853,12 @@
"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!",
"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": {

View File

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

View File

@ -1,9 +1,9 @@
{
"home": {
"welcomeNight": "Good Night {username}",
"welcomeMorning": "Good Morning {username}",
"welcomeDay": "Hi {username}",
"welcomeEvening": "Good Evening {username}",
"welcomeNight": "Good Night {username}!",
"welcomeMorning": "Good Morning {username}!",
"welcomeDay": "Hi {username}!",
"welcomeEvening": "Good Evening {username}!",
"lastViewed": "Last viewed",
"list": {
"newText": "You can create a new list for your new tasks:",
@ -169,6 +169,14 @@
"title": "List Title",
"color": "Color",
"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…",
"searchSelect": "Click or press enter to select this list",
"shared": "Shared Lists",
@ -270,14 +278,6 @@
"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": {
"title": "Gantt",
"showTasksWithoutDates": "Show tasks which don't have dates set",
@ -672,13 +672,23 @@
"updated": "Updated"
},
"subscription": {
"subscribedThroughParent": "You can't unsubscribe here because you are subscribed to this {entity} through its {parent}.",
"subscribed": "You are currently subscribed to this {entity} and will receive notifications for changes.",
"notSubscribed": "You are not subscribed to this {entity} and won't receive notifications for changes.",
"subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"subscribedTaskThroughParentList": "You can't unsubscribe here because you are subscribed to this task through its list.",
"subscribedNamespace": "You are currently subscribed to this namespace and will receive notifications for changes.",
"notSubscribedNamespace": "You are not subscribed to this namespace and won't receive notifications for changes.",
"subscribedList": "You are currently subscribed to this list and will receive notifications for changes.",
"notSubscribedList": "You are not subscribed to this list and won't receive notifications for changes.",
"subscribedTask": "You are currently subscribed to this task and will receive notifications for changes.",
"notSubscribedTask": "You are not subscribed to this task and won't receive notifications for changes.",
"subscribe": "Subscribe",
"unsubscribe": "Unsubscribe",
"subscribeSuccess": "You are now subscribed to this {entity}",
"unsubscribeSuccess": "You are now unsubscribed to this {entity}"
"subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccessNamespace": "You are now unsubscribed to this namespace",
"subscribeSuccessList": "You are now subscribed to this list",
"unsubscribeSuccessList": "You are now unsubscribed to this list",
"subscribeSuccessTask": "You are now subscribed to this task",
"unsubscribeSuccessTask": "You are now unsubscribed to this task"
},
"attachment": {
"title": "Attachments",
@ -690,7 +700,11 @@
"deleteTooltip": "Delete this attachment",
"deleteText1": "Are you sure you want to delete the attachment {filename}?",
"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": {
"title": "Comments",
@ -839,6 +853,12 @@
"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!",
"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": {

View File

@ -1,9 +1,9 @@
{
"home": {
"welcomeNight": "Good Night {username}",
"welcomeMorning": "Good Morning {username}",
"welcomeDay": "Hi {username}",
"welcomeEvening": "Good Evening {username}",
"welcomeNight": "Good Night {username}!",
"welcomeMorning": "Good Morning {username}!",
"welcomeDay": "Hi {username}!",
"welcomeEvening": "Good Evening {username}!",
"lastViewed": "Last viewed",
"list": {
"newText": "You can create a new list for your new tasks:",
@ -169,6 +169,14 @@
"title": "List Title",
"color": "Color",
"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…",
"searchSelect": "Click or press enter to select this list",
"shared": "Shared Lists",
@ -270,14 +278,6 @@
"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": {
"title": "Gantt",
"showTasksWithoutDates": "Show tasks which don't have dates set",
@ -672,13 +672,23 @@
"updated": "Updated"
},
"subscription": {
"subscribedThroughParent": "You can't unsubscribe here because you are subscribed to this {entity} through its {parent}.",
"subscribed": "You are currently subscribed to this {entity} and will receive notifications for changes.",
"notSubscribed": "You are not subscribed to this {entity} and won't receive notifications for changes.",
"subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"subscribedTaskThroughParentList": "You can't unsubscribe here because you are subscribed to this task through its list.",
"subscribedNamespace": "You are currently subscribed to this namespace and will receive notifications for changes.",
"notSubscribedNamespace": "You are not subscribed to this namespace and won't receive notifications for changes.",
"subscribedList": "You are currently subscribed to this list and will receive notifications for changes.",
"notSubscribedList": "You are not subscribed to this list and won't receive notifications for changes.",
"subscribedTask": "You are currently subscribed to this task and will receive notifications for changes.",
"notSubscribedTask": "You are not subscribed to this task and won't receive notifications for changes.",
"subscribe": "Subscribe",
"unsubscribe": "Unsubscribe",
"subscribeSuccess": "You are now subscribed to this {entity}",
"unsubscribeSuccess": "You are now unsubscribed to this {entity}"
"subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccessNamespace": "You are now unsubscribed to this namespace",
"subscribeSuccessList": "You are now subscribed to this list",
"unsubscribeSuccessList": "You are now unsubscribed to this list",
"subscribeSuccessTask": "You are now subscribed to this task",
"unsubscribeSuccessTask": "You are now unsubscribed to this task"
},
"attachment": {
"title": "Attachments",
@ -690,7 +700,11 @@
"deleteTooltip": "Delete this attachment",
"deleteText1": "Are you sure you want to delete the attachment {filename}?",
"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": {
"title": "Comments",
@ -839,6 +853,12 @@
"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!",
"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": {

View File

@ -1,9 +1,9 @@
{
"home": {
"welcomeNight": "Good Night {username}",
"welcomeMorning": "Good Morning {username}",
"welcomeDay": "Hi {username}",
"welcomeEvening": "Good Evening {username}",
"welcomeNight": "Good Night {username}!",
"welcomeMorning": "Good Morning {username}!",
"welcomeDay": "Hi {username}!",
"welcomeEvening": "Good Evening {username}!",
"lastViewed": "Last viewed",
"list": {
"newText": "You can create a new list for your new tasks:",
@ -169,6 +169,14 @@
"title": "List Title",
"color": "Color",
"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…",
"searchSelect": "Click or press enter to select this list",
"shared": "Shared Lists",
@ -270,14 +278,6 @@
"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": {
"title": "Gantt",
"showTasksWithoutDates": "Show tasks which don't have dates set",
@ -672,13 +672,23 @@
"updated": "Updated"
},
"subscription": {
"subscribedThroughParent": "You can't unsubscribe here because you are subscribed to this {entity} through its {parent}.",
"subscribed": "You are currently subscribed to this {entity} and will receive notifications for changes.",
"notSubscribed": "You are not subscribed to this {entity} and won't receive notifications for changes.",
"subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"subscribedTaskThroughParentList": "You can't unsubscribe here because you are subscribed to this task through its list.",
"subscribedNamespace": "You are currently subscribed to this namespace and will receive notifications for changes.",
"notSubscribedNamespace": "You are not subscribed to this namespace and won't receive notifications for changes.",
"subscribedList": "You are currently subscribed to this list and will receive notifications for changes.",
"notSubscribedList": "You are not subscribed to this list and won't receive notifications for changes.",
"subscribedTask": "You are currently subscribed to this task and will receive notifications for changes.",
"notSubscribedTask": "You are not subscribed to this task and won't receive notifications for changes.",
"subscribe": "Subscribe",
"unsubscribe": "Unsubscribe",
"subscribeSuccess": "You are now subscribed to this {entity}",
"unsubscribeSuccess": "You are now unsubscribed to this {entity}"
"subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccessNamespace": "You are now unsubscribed to this namespace",
"subscribeSuccessList": "You are now subscribed to this list",
"unsubscribeSuccessList": "You are now unsubscribed to this list",
"subscribeSuccessTask": "You are now subscribed to this task",
"unsubscribeSuccessTask": "You are now unsubscribed to this task"
},
"attachment": {
"title": "Attachments",
@ -690,7 +700,11 @@
"deleteTooltip": "Delete this attachment",
"deleteText1": "Are you sure you want to delete the attachment {filename}?",
"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": {
"title": "Comments",
@ -839,6 +853,12 @@
"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!",
"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": {

View File

@ -1,9 +1,9 @@
{
"home": {
"welcomeNight": "Ngủ ngon nhé, {username}",
"welcomeMorning": "Chào buổi sáng, {username}",
"welcomeDay": "Hi {username}",
"welcomeEvening": "Chào buổi tối, {username}",
"welcomeNight": "Good Night {username}!",
"welcomeMorning": "Good Morning {username}!",
"welcomeDay": "Hi {username}!",
"welcomeEvening": "Good Evening {username}!",
"lastViewed": "Xem gần đây",
"list": {
"newText": "Bạn có thể tạo một danh sách công việc mới cho mình:",
@ -169,6 +169,14 @@
"title": "Tên Danh sách",
"color": "Màu sắc",
"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…",
"searchSelect": "Nhấp hoặc nhấn enter để chọn danh sách này",
"shared": "Đang tham gia",
@ -270,14 +278,6 @@
"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": {
"title": "Biểu đồ Gantt",
"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"
},
"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ó.",
"subscribed": "Bạn đang theo dõi {entity} này và sẽ nhận được thông báo về các thay đổi.",
"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.",
"subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"subscribedTaskThroughParentList": "You can't unsubscribe here because you are subscribed to this task through its list.",
"subscribedNamespace": "You are currently subscribed to this namespace and will receive notifications for changes.",
"notSubscribedNamespace": "You are not subscribed to this namespace and won't receive notifications for changes.",
"subscribedList": "You are currently subscribed to this list and will receive notifications for changes.",
"notSubscribedList": "You are not subscribed to this list and won't receive notifications for changes.",
"subscribedTask": "You are currently subscribed to this task and will receive notifications for changes.",
"notSubscribedTask": "You are not subscribed to this task and won't receive notifications for changes.",
"subscribe": "Theo dõi",
"unsubscribe": "Bỏ theo dõi",
"subscribeSuccess": "Bạn hiện đã theo dõi {entity} này",
"unsubscribeSuccess": "Bạn đã bỏ theo dõi {entity} này"
"subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccessNamespace": "You are now unsubscribed to this namespace",
"subscribeSuccessList": "You are now subscribed to this list",
"unsubscribeSuccessList": "You are now unsubscribed to this list",
"subscribeSuccessTask": "You are now subscribed to this task",
"unsubscribeSuccessTask": "You are now unsubscribed to this task"
},
"attachment": {
"title": "Tệp đính kèm",
@ -690,7 +700,11 @@
"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?",
"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": {
"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?",
"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."
},
"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": {

View File

@ -1,9 +1,9 @@
{
"home": {
"welcomeNight": "Good Night {username}",
"welcomeMorning": "Good Morning {username}",
"welcomeDay": "Hi {username}",
"welcomeEvening": "Good Evening {username}",
"welcomeNight": "Good Night {username}!",
"welcomeMorning": "Good Morning {username}!",
"welcomeDay": "Hi {username}!",
"welcomeEvening": "Good Evening {username}!",
"lastViewed": "Last viewed",
"list": {
"newText": "You can create a new list for your new tasks:",
@ -169,6 +169,14 @@
"title": "List Title",
"color": "Color",
"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…",
"searchSelect": "Click or press enter to select this list",
"shared": "Shared Lists",
@ -270,14 +278,6 @@
"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": {
"title": "Gantt",
"showTasksWithoutDates": "Show tasks which don't have dates set",
@ -672,13 +672,23 @@
"updated": "Updated"
},
"subscription": {
"subscribedThroughParent": "You can't unsubscribe here because you are subscribed to this {entity} through its {parent}.",
"subscribed": "You are currently subscribed to this {entity} and will receive notifications for changes.",
"notSubscribed": "You are not subscribed to this {entity} and won't receive notifications for changes.",
"subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"subscribedTaskThroughParentList": "You can't unsubscribe here because you are subscribed to this task through its list.",
"subscribedNamespace": "You are currently subscribed to this namespace and will receive notifications for changes.",
"notSubscribedNamespace": "You are not subscribed to this namespace and won't receive notifications for changes.",
"subscribedList": "You are currently subscribed to this list and will receive notifications for changes.",
"notSubscribedList": "You are not subscribed to this list and won't receive notifications for changes.",
"subscribedTask": "You are currently subscribed to this task and will receive notifications for changes.",
"notSubscribedTask": "You are not subscribed to this task and won't receive notifications for changes.",
"subscribe": "Subscribe",
"unsubscribe": "Unsubscribe",
"subscribeSuccess": "You are now subscribed to this {entity}",
"unsubscribeSuccess": "You are now unsubscribed to this {entity}"
"subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccessNamespace": "You are now unsubscribed to this namespace",
"subscribeSuccessList": "You are now subscribed to this list",
"unsubscribeSuccessList": "You are now unsubscribed to this list",
"subscribeSuccessTask": "You are now subscribed to this task",
"unsubscribeSuccessTask": "You are now unsubscribed to this task"
},
"attachment": {
"title": "Attachments",
@ -690,7 +700,11 @@
"deleteTooltip": "Delete this attachment",
"deleteText1": "Are you sure you want to delete the attachment {filename}?",
"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": {
"title": "Comments",
@ -839,6 +853,12 @@
"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!",
"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": {

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

View File

@ -7,7 +7,7 @@ export const AUTH_TYPES = {
'LINK_SHARE': 2,
} as const
type AuthType = typeof AUTH_TYPES[keyof typeof AUTH_TYPES]
export type AuthType = typeof AUTH_TYPES[keyof typeof AUTH_TYPES]
export interface IUser extends IAbstract {
id: number

View File

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

View File

@ -9,6 +9,7 @@ import {setTitle} from '@/helpers/setTitle'
import {useListStore} from '@/stores/lists'
import {useAuthStore} from '@/stores/auth'
import {useBaseStore} from '@/stores/base'
import HomeComponent from '../views/Home.vue'
import NotFoundComponent from '../views/404.vue'
@ -465,11 +466,18 @@ const router = createRouter({
],
})
export function getAuthForRoute(route: RouteLocation) {
export async function getAuthForRoute(route: RouteLocation) {
const authStore = useAuthStore()
if (authStore.authUser || authStore.authLinkShare) {
return
}
const baseStore = useBaseStore()
// When trying this before the current user was fully loaded we might get a flash of the login screen
// in the user shell. To make shure this does not happen we check if everything is ready before trying.
if (!baseStore.ready) {
return
}
// Check if the user is already logged in and redirect them to the home page if not
if (
@ -482,11 +490,24 @@ export function getAuthForRoute(route: RouteLocation) {
'openid.auth',
].includes(route.name as string) &&
localStorage.getItem('passwordResetToken') === null &&
localStorage.getItem('emailConfirmToken') === null
localStorage.getItem('emailConfirmToken') === null &&
!(route.name === 'home' && (typeof route.query.userPasswordReset !== 'undefined' || typeof route.query.userEmailConfirm !== 'undefined'))
) {
saveLastVisited(route.name as string, route.params)
saveLastVisited(route.name as string, route.params, route.query)
return {name: 'user.login'}
}
if(localStorage.getItem('passwordResetToken') !== null && route.name !== 'user.password-reset.reset') {
return {name: 'user.password-reset.reset'}
}
if(localStorage.getItem('emailConfirmToken') !== null && route.name !== 'user.login') {
return {name: 'user.login'}
}
}
router.beforeEach(async (to) => {
return getAuthForRoute(to)
})
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.
*/
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) {
console.log(file)
return this.uploadFile(
this.paths.create,
file,

View File

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

View File

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

View File

@ -75,12 +75,11 @@ export const useKanbanStore = defineStore('kanban', {
getTaskById(state) {
return (id: ITask['id']) => {
const { bucketIndex, taskIndex } = getTaskIndicesById(state, id)
return {
bucketIndex,
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']) => {
return state.namespaces.find(({id}) => id == namespaceId) || null
},
searchNamespace() {
return (query: string) => (
search(query)
@ -64,6 +64,13 @@ export const useNamespaceStore = defineStore('namespace', {
this.namespaces = namespaces
namespaces.forEach(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
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 {useAttachmentStore} from '@/stores/attachments'
import {useKanbanStore} from '@/stores/kanban'
import {useBaseStore} from '@/stores/base'
// IDEA: maybe use a small fuzzy search here to prevent errors
function findPropertyByValue(object, key, value) {
@ -105,6 +106,7 @@ export const useTaskStore = defineStore('task', {
const cancel = setModuleLoading(this)
try {
this.tasks = await taskService.getAll({}, params)
useBaseStore().setHasTasks(this.tasks.length > 0)
return this.tasks
} finally {
cancel()
@ -171,32 +173,39 @@ export const useTaskStore = defineStore('task', {
user: IUser,
taskId: ITask['id']
}) {
const kanbanStore = useKanbanStore()
const taskAssigneeService = new TaskAssigneeService()
const r = await taskAssigneeService.create(new TaskAssigneeModel({
userId: user.id,
taskId: taskId,
}))
const t = kanbanStore.getTaskById(taskId)
if (t.task === null) {
// 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.
// 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
}
const cancel = setModuleLoading(this)
try {
const kanbanStore = useKanbanStore()
const taskAssigneeService = new TaskAssigneeService()
const r = await taskAssigneeService.create(new TaskAssigneeModel({
userId: user.id,
taskId: taskId,
}))
const t = kanbanStore.getTaskById(taskId)
if (t.task === null) {
// 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.
// 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({
...t,
task: {
...t.task,
assignees: [
...t.task.assignees,
user,
],
},
})
return r
kanbanStore.setTaskInBucketByIndex({
...t,
task: {
...t.task,
assignees: [
...t.task.assignees,
user,
],
},
})
return r
} finally {
cancel()
}
},
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
// 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 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
}
@ -378,6 +387,7 @@ export const useTaskStore = defineStore('task', {
})
if(foundListId === null || foundListId === 0) {
cancel()
throw new Error('NO_LIST')
}
@ -409,6 +419,13 @@ export const useTaskStore = defineStore('task', {
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">
import {computed} from 'vue'
import {VERSION as frontendVersion} from '@/version.json'
import {VERSION} from '@/version.json'
import {useConfigStore} from '@/stores/config'
const configStore = useConfigStore()
const apiVersion = computed(() => configStore.version)
const frontendVersion = VERSION
</script>

View File

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

View File

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

View File

@ -1,7 +1,7 @@
<template>
<create-edit
:title="$t('list.share.header')"
primary-label=""
:has-primary-action="false"
>
<template v-if="list">
<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 microsoftTodoIcon from './icons/microsoft-todo.svg?url'
import vikunjaFileIcon from './icons/vikunja-file.png?url'
import tickTickIcon from './icons/ticktick.svg?url'
export interface Migrator {
id: string
@ -42,4 +43,10 @@ export const MIGRATORS: IMigratorRecord = {
icon: vikunjaFileIcon,
isFileMigrator: true,
},
ticktick: {
id: 'ticktick',
name: 'TickTick',
icon: tickTickIcon as string,
isFileMigrator: true,
},
}

View File

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

View File

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

View File

@ -127,6 +127,24 @@
</table>
</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 -->
<transition name="modal">
<modal
@ -202,13 +220,14 @@ const teamMemberService = ref<TeamMemberService>(new TeamMemberService())
const userService = ref<UserService>(new UserService())
const team = ref<ITeam>()
const teamId = computed(() => route.params.id)
const teamId = computed(() => Number(route.params.id))
const memberToDelete = ref<ITeamMember>()
const newMember = ref<IUser>()
const foundUsers = ref<IUser[]>()
const showDeleteModal = ref(false)
const showUserDeleteModal = ref(false)
const showLeaveModal = ref(false)
const showError = ref(false)
const title = ref('')
@ -287,6 +306,19 @@ async function findUser(query: string) {
const users = await userService.value.getAll({}, {s: query})
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>
<style lang="scss" scoped>

View File

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

View File

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

View File

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

View File

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

View File

@ -6,6 +6,8 @@ import legacyFn from '@vitejs/plugin-legacy'
import {VitePWA} from 'vite-plugin-pwa'
import {visualizer} from 'rollup-plugin-visualizer'
import svgLoader from 'vite-svg-loader'
import postcssPresetEnv from "postcss-preset-env";
import { fileURLToPath, URL } from '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
},
},
postcss: {
plugins: [
postcssPresetEnv(),
],
},
},
plugins: [
vue({