Merge branch 'main' into feature/login-improvements
# Conflicts: # src/components/misc/no-auth-wrapper.vue # src/styles/components/_index.scss # src/views/user/Login.vue # src/views/user/Register.vue
This commit is contained in:
commit
310578d349
93 changed files with 1761 additions and 2501 deletions
|
@ -98,6 +98,15 @@ steps:
|
|||
depends_on:
|
||||
- dependencies
|
||||
|
||||
- name: typecheck
|
||||
failure: ignore
|
||||
image: node:16
|
||||
pull: true
|
||||
commands:
|
||||
- yarn typecheck
|
||||
depends_on:
|
||||
- dependencies
|
||||
|
||||
- name: test-frontend
|
||||
image: cypress/browsers:node16.5.0-chrome94-ff93
|
||||
pull: true
|
||||
|
|
|
@ -31,7 +31,7 @@ describe('Lists', () => {
|
|||
cy.url()
|
||||
.should('contain', '/namespaces/1/list')
|
||||
cy.get('.card-header-title')
|
||||
.contains('Create a new list')
|
||||
.contains('New list')
|
||||
cy.get('input.input')
|
||||
.type('New List')
|
||||
cy.get('.button')
|
||||
|
@ -101,7 +101,7 @@ describe('Lists', () => {
|
|||
.click()
|
||||
cy.url()
|
||||
.should('contain', '/settings/delete')
|
||||
cy.get('.modal-mask .modal-container .modal-content .actions a.button')
|
||||
cy.get('[data-cy="modalPrimary"]')
|
||||
.contains('Do it')
|
||||
.click()
|
||||
|
||||
|
@ -392,7 +392,7 @@ describe('Lists', () => {
|
|||
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item .field input.input')
|
||||
.first()
|
||||
.type(3)
|
||||
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item .field a.button.is-primary')
|
||||
cy.get('[data-cy="setBucketLimit"]')
|
||||
.first()
|
||||
.click()
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ describe('Namepaces', () => {
|
|||
|
||||
it('Should be all there', () => {
|
||||
cy.visit('/namespaces')
|
||||
cy.get('.namespace h1 span')
|
||||
cy.get('[data-cy="namespace-title"]')
|
||||
.should('contain', namespaces[0].title)
|
||||
})
|
||||
|
||||
|
@ -23,14 +23,14 @@ describe('Namepaces', () => {
|
|||
const newNamespaceTitle = 'New Namespace'
|
||||
|
||||
cy.visit('/namespaces')
|
||||
cy.get('a.button')
|
||||
.contains('Create a new namespace')
|
||||
cy.get('[data-cy="new-namespace"]')
|
||||
.should('contain', 'New namespace')
|
||||
.click()
|
||||
|
||||
cy.url()
|
||||
.should('contain', '/namespaces/new')
|
||||
cy.get('.card-header-title')
|
||||
.should('contain', 'Create a new namespace')
|
||||
.should('contain', 'New namespace')
|
||||
cy.get('input.input')
|
||||
.type(newNamespaceTitle)
|
||||
cy.get('.button')
|
||||
|
@ -72,7 +72,7 @@ describe('Namepaces', () => {
|
|||
cy.get('.namespace-container .menu.namespaces-lists')
|
||||
.should('contain', newNamespaceName)
|
||||
.should('not.contain', newNamespaces[0].title)
|
||||
cy.get('.content.namespaces-list')
|
||||
cy.get('[data-cy="namespaces-list"]')
|
||||
.should('contain', newNamespaceName)
|
||||
.should('not.contain', newNamespaces[0].title)
|
||||
})
|
||||
|
@ -89,7 +89,7 @@ describe('Namepaces', () => {
|
|||
.click()
|
||||
cy.url()
|
||||
.should('contain', '/settings/delete')
|
||||
cy.get('.modal-mask .modal-container .modal-content .actions a.button')
|
||||
cy.get('[data-cy="modalPrimary"]')
|
||||
.contains('Do it')
|
||||
.click()
|
||||
|
||||
|
@ -116,30 +116,30 @@ describe('Namepaces', () => {
|
|||
|
||||
// Initial
|
||||
cy.visit('/namespaces')
|
||||
cy.get('.namespaces-list .namespace')
|
||||
cy.get('.namespace')
|
||||
.should('not.contain', 'Archived')
|
||||
|
||||
// Show archived
|
||||
cy.get('.namespaces-list .fancycheckbox.show-archived-check label.check span')
|
||||
cy.get('[data-cy="show-archived-check"] label.check span')
|
||||
.should('be.visible')
|
||||
.click()
|
||||
cy.get('.namespaces-list .fancycheckbox.show-archived-check input')
|
||||
cy.get('[data-cy="show-archived-check"] input')
|
||||
.should('be.checked')
|
||||
cy.get('.namespaces-list .namespace')
|
||||
cy.get('.namespace')
|
||||
.should('contain', 'Archived')
|
||||
|
||||
// Don't show archived
|
||||
cy.get('.namespaces-list .fancycheckbox.show-archived-check label.check span')
|
||||
cy.get('[data-cy="show-archived-check"] label.check span')
|
||||
.should('be.visible')
|
||||
.click()
|
||||
cy.get('.namespaces-list .fancycheckbox.show-archived-check input')
|
||||
cy.get('[data-cy="show-archived-check"] input')
|
||||
.should('not.be.checked')
|
||||
|
||||
// Second time visiting after unchecking
|
||||
cy.visit('/namespaces')
|
||||
cy.get('.namespaces-list .fancycheckbox.show-archived-check input')
|
||||
cy.get('[data-cy="show-archived-check"] input')
|
||||
.should('not.be.checked')
|
||||
cy.get('.namespaces-list .namespace')
|
||||
cy.get('.namespace')
|
||||
.should('not.contain', 'Archived')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
import '../../support/authenticateUser'
|
||||
|
||||
const setHours = hours => {
|
||||
const date = new Date()
|
||||
date.setHours(hours)
|
||||
cy.clock(+date)
|
||||
}
|
||||
|
||||
describe('Home Page', () => {
|
||||
it('shows the right salutation in the night', () => {
|
||||
setHours(4)
|
||||
cy.visit('/')
|
||||
cy.get('h2').should('contain', 'Good Night')
|
||||
})
|
||||
it('shows the right salutation in the morning', () => {
|
||||
setHours(8)
|
||||
cy.visit('/')
|
||||
cy.get('h2').should('contain', 'Good Morning')
|
||||
})
|
||||
it('shows the right salutation in the day', () => {
|
||||
setHours(13)
|
||||
cy.visit('/')
|
||||
cy.get('h2').should('contain', 'Hi')
|
||||
})
|
||||
it('shows the right salutation in the night', () => {
|
||||
setHours(20)
|
||||
cy.visit('/')
|
||||
cy.get('h2').should('contain', 'Good Evening')
|
||||
})
|
||||
it('shows the right salutation in the night again', () => {
|
||||
setHours(23)
|
||||
cy.visit('/')
|
||||
cy.get('h2').should('contain', 'Good Night')
|
||||
})
|
||||
})
|
|
@ -168,7 +168,7 @@ describe('Task', () => {
|
|||
.click()
|
||||
cy.get('.task-view .details.content.description .editor .vue-easymde .EasyMDEContainer .CodeMirror-scroll')
|
||||
.type('{selectall}New Description')
|
||||
cy.get('.task-view .details.content.description .editor a')
|
||||
cy.get('[data-cy="saveEditor"]')
|
||||
.contains('Save')
|
||||
.click()
|
||||
|
||||
|
@ -404,7 +404,7 @@ describe('Task', () => {
|
|||
cy.get('.datepicker .datepicker-popup a')
|
||||
.contains('Tomorrow')
|
||||
.click()
|
||||
cy.get('.datepicker .datepicker-popup a.button')
|
||||
cy.get('[data-cy="closeDatepicker"]')
|
||||
.contains('Confirm')
|
||||
.click()
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ describe('User Settings', () => {
|
|||
.trigger('mousedown', {which: 1})
|
||||
.trigger('mousemove', {clientY: 100})
|
||||
.trigger('mouseup')
|
||||
cy.get('a.button.is-primary')
|
||||
cy.get('[data-cy="uploadAvatar"]')
|
||||
.contains('Upload Avatar')
|
||||
.click()
|
||||
|
||||
|
@ -33,7 +33,7 @@ describe('User Settings', () => {
|
|||
cy.get('.general-settings .control input.input')
|
||||
.first()
|
||||
.type('Lorem Ipsum')
|
||||
cy.get('.card.general-settings .button.is-primary')
|
||||
cy.get('[data-cy="saveGeneralSettings"]')
|
||||
.contains('Save')
|
||||
.click()
|
||||
|
||||
|
|
72
package.json
72
package.json
|
@ -9,10 +9,10 @@
|
|||
"build": "vite build && workbox copyLibraries dist/",
|
||||
"build:modern-only": "BUILD_MODERN_ONLY=true vite build && workbox copyLibraries dist/",
|
||||
"build:dev": "vite build -m development --outDir dist-dev/",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"lint": "eslint --ignore-pattern '*.test.*' ./src --ext .vue,.js,.ts",
|
||||
"lint:markup": "vue-tsc --noEmit",
|
||||
"cypress:open": "cypress open",
|
||||
"test:unit": "jest",
|
||||
"test:unit": "vitest run",
|
||||
"test:frontend": "cypress run",
|
||||
"browserslist:update": "npx browserslist@latest --update-db"
|
||||
},
|
||||
|
@ -21,32 +21,33 @@
|
|||
"@kyvg/vue3-notification": "2.3.4",
|
||||
"@sentry/tracing": "6.16.1",
|
||||
"@sentry/vue": "6.16.1",
|
||||
"@types/is-touch-device": "1.0.0",
|
||||
"@vue/compat": "3.2.26",
|
||||
"@vueuse/core": "7.4.1",
|
||||
"@vueuse/router": "7.4.1",
|
||||
"@vueuse/core": "7.5.2",
|
||||
"@vueuse/router": "7.5.3",
|
||||
"bulma-css-variables": "0.9.33",
|
||||
"camel-case": "4.1.2",
|
||||
"codemirror": "5.65.0",
|
||||
"copy-to-clipboard": "3.3.1",
|
||||
"date-fns": "2.27.0",
|
||||
"date-fns": "2.28.0",
|
||||
"dompurify": "2.3.4",
|
||||
"easymde": "2.15.0",
|
||||
"flatpickr": "4.6.9",
|
||||
"flexsearch": "0.7.21",
|
||||
"highlight.js": "11.3.1",
|
||||
"highlight.js": "11.4.0",
|
||||
"is-touch-device": "1.0.1",
|
||||
"lodash.clonedeep": "4.5.0",
|
||||
"lodash.debounce": "4.0.8",
|
||||
"marked": "4.0.8",
|
||||
"marked": "4.0.9",
|
||||
"register-service-worker": "1.7.2",
|
||||
"snake-case": "3.0.4",
|
||||
"ufo": "0.7.9",
|
||||
"v-tooltip": "4.0.0-beta.2",
|
||||
"v-tooltip": "4.0.0-beta.13",
|
||||
"vue": "3.2.26",
|
||||
"vue-advanced-cropper": "2.7.0",
|
||||
"vue-advanced-cropper": "2.7.1",
|
||||
"vue-drag-resize": "2.0.3",
|
||||
"vue-flatpickr-component": "9.0.5",
|
||||
"vue-i18n": "9.2.0-beta.25",
|
||||
"vue-i18n": "9.2.0-beta.26",
|
||||
"vue-router": "4.0.12",
|
||||
"vuedraggable": "4.1.0",
|
||||
"vuex": "4.0.2",
|
||||
|
@ -59,37 +60,36 @@
|
|||
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
||||
"@fortawesome/vue-fontawesome": "3.0.0-5",
|
||||
"@types/flexsearch": "0.7.2",
|
||||
"@types/jest": "27.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "5.8.0",
|
||||
"@typescript-eslint/parser": "5.8.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.9.0",
|
||||
"@typescript-eslint/parser": "5.9.0",
|
||||
"@vitejs/plugin-legacy": "1.6.4",
|
||||
"@vitejs/plugin-vue": "2.0.1",
|
||||
"@vue/eslint-config-typescript": "9.1.0",
|
||||
"autoprefixer": "10.4.0",
|
||||
"@vue/eslint-config-typescript": "10.0.0",
|
||||
"autoprefixer": "10.4.2",
|
||||
"axios": "0.24.0",
|
||||
"browserslist": "4.19.1",
|
||||
"caniuse-lite": "1.0.30001292",
|
||||
"caniuse-lite": "1.0.30001298",
|
||||
"cypress": "9.2.0",
|
||||
"cypress-file-upload": "5.0.8",
|
||||
"esbuild": "0.14.8",
|
||||
"eslint": "8.5.0",
|
||||
"esbuild": "0.14.10",
|
||||
"eslint": "8.6.0",
|
||||
"eslint-plugin-vue": "8.2.0",
|
||||
"express": "4.17.2",
|
||||
"faker": "5.5.3",
|
||||
"jest": "27.4.5",
|
||||
"netlify-cli": "8.4.2",
|
||||
"netlify-cli": "8.6.15",
|
||||
"happy-dom": "2.25.1",
|
||||
"postcss": "8.4.5",
|
||||
"postcss-preset-env": "7.1.0",
|
||||
"rollup": "2.61.1",
|
||||
"postcss-preset-env": "7.2.0",
|
||||
"rollup": "2.63.0",
|
||||
"rollup-plugin-visualizer": "5.5.2",
|
||||
"sass": "1.45.1",
|
||||
"slugify": "1.6.4",
|
||||
"ts-jest": "27.1.2",
|
||||
"sass": "1.47.0",
|
||||
"slugify": "1.6.5",
|
||||
"typescript": "4.5.4",
|
||||
"vite": "2.7.7",
|
||||
"vite": "2.7.10",
|
||||
"vite-plugin-pwa": "0.11.12",
|
||||
"vite-svg-loader": "3.1.1",
|
||||
"vue-tsc": "0.30.0",
|
||||
"vitest": "0.0.139",
|
||||
"vue-tsc": "0.30.2",
|
||||
"wait-on": "6.0.0",
|
||||
"workbox-cli": "6.4.2"
|
||||
},
|
||||
|
@ -144,24 +144,6 @@
|
|||
"autoprefixer": {}
|
||||
}
|
||||
},
|
||||
"jest": {
|
||||
"testPathIgnorePatterns": [
|
||||
"cypress"
|
||||
],
|
||||
"testEnvironment": "jsdom",
|
||||
"preset": "ts-jest",
|
||||
"roots": [
|
||||
"<rootDir>/src"
|
||||
],
|
||||
"transform": {
|
||||
"^.+\\.(js|tsx?)$": "ts-jest"
|
||||
},
|
||||
"moduleFileExtensions": [
|
||||
"ts",
|
||||
"js",
|
||||
"json"
|
||||
]
|
||||
},
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"packageManager": "yarn@1.22.17"
|
||||
}
|
||||
|
|
14
src/App.vue
14
src/App.vue
|
@ -1,6 +1,5 @@
|
|||
<template>
|
||||
<ready :class="{'is-touch': isTouch}">
|
||||
<div :class="{'is-hidden': !online}">
|
||||
<ready>
|
||||
<template v-if="authUser">
|
||||
<top-navigation/>
|
||||
<content-auth/>
|
||||
|
@ -10,7 +9,6 @@
|
|||
<router-view/>
|
||||
</no-auth-wrapper>
|
||||
<Notification/>
|
||||
</div>
|
||||
|
||||
<transition name="fade">
|
||||
<keyboard-shortcuts v-if="keyboardShortcutsActive"/>
|
||||
|
@ -19,12 +17,11 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {computed, watch, watchEffect, Ref} from 'vue'
|
||||
import {computed, watch, Ref} from 'vue'
|
||||
import {useRouter} from 'vue-router'
|
||||
import {useRouteQuery} from '@vueuse/router'
|
||||
import {useStore} from 'vuex'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {useOnline} from '@vueuse/core'
|
||||
import isTouchDevice from 'is-touch-device'
|
||||
import {success} from '@/message'
|
||||
|
||||
|
@ -38,17 +35,14 @@ import Ready from '@/components/misc/ready.vue'
|
|||
|
||||
import {setLanguage} from './i18n'
|
||||
import AccountDeleteService from '@/services/accountDelete'
|
||||
import {ONLINE} from '@/store/mutation-types'
|
||||
|
||||
import {useColorScheme} from '@/composables/useColorScheme'
|
||||
import {useBodyClass} from '@/composables/useBodyClass'
|
||||
|
||||
const store = useStore()
|
||||
const online = useOnline()
|
||||
watchEffect(() => store.commit(ONLINE, online.value))
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const isTouch = computed(isTouchDevice)
|
||||
useBodyClass('is-touch', isTouchDevice)
|
||||
const keyboardShortcutsActive = computed(() => store.state.keyboardShortcutsActive)
|
||||
|
||||
const authUser = computed(() => store.getters['auth/authUser'])
|
||||
|
|
118
src/components/base/BaseButton.vue
Normal file
118
src/components/base/BaseButton.vue
Normal file
|
@ -0,0 +1,118 @@
|
|||
<template>
|
||||
<component
|
||||
:is="componentNodeName"
|
||||
class="base-button"
|
||||
:class="{ 'base-button--type-button': isButton }"
|
||||
v-bind="elementBindings"
|
||||
:disabled="disabled || undefined"
|
||||
>
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
// see https://v3.vuejs.org/api/sfc-script-setup.html#usage-alongside-normal-script
|
||||
export default {
|
||||
inheritAttrs: false,
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
// this component removes styling differences between links / vue-router links and button elements
|
||||
// by doing so we make it easy abstract the functionality from style and enable easier and semantic
|
||||
// correct button and link usage. Also see: https://css-tricks.com/a-complete-guide-to-links-and-buttons/#accessibility-considerations
|
||||
|
||||
// the component tries to heuristically determine what it should be checking the props (see the
|
||||
// componentNodeName and elementBindings ref for this).
|
||||
|
||||
// NOTE: Do NOT use buttons with @click to push routes. => Use router-links instead!
|
||||
|
||||
import { ref, watchEffect, computed, useAttrs, PropType } from 'vue'
|
||||
|
||||
const BASE_BUTTON_TYPES_MAP = Object.freeze({
|
||||
button: 'button',
|
||||
submit: 'submit',
|
||||
})
|
||||
|
||||
type BaseButtonTypes = keyof typeof BASE_BUTTON_TYPES_MAP
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String as PropType<BaseButtonTypes>,
|
||||
default: 'button',
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
const componentNodeName = ref<Node['nodeName']>('button')
|
||||
interface ElementBindings {
|
||||
type?: string;
|
||||
rel?: string,
|
||||
}
|
||||
|
||||
const elementBindings = ref({})
|
||||
|
||||
const attrs = useAttrs()
|
||||
watchEffect(() => {
|
||||
// by default this component is a button element with the attribute of the type "button" (default prop value)
|
||||
let nodeName = 'button'
|
||||
let bindings: ElementBindings = {type: props.type}
|
||||
|
||||
// if we find a "to" prop we set it as router-link
|
||||
if ('to' in attrs) {
|
||||
nodeName = 'router-link'
|
||||
bindings = {}
|
||||
}
|
||||
|
||||
// if there is a href we assume the user wants an external link via a link element
|
||||
// we also set the attribute rel to "noopener" but make it possible to overwrite this by the user.
|
||||
if ('href' in attrs) {
|
||||
nodeName = 'a'
|
||||
bindings = {rel: 'noopener'}
|
||||
}
|
||||
|
||||
componentNodeName.value = nodeName
|
||||
elementBindings.value = {
|
||||
...bindings,
|
||||
...attrs,
|
||||
}
|
||||
})
|
||||
|
||||
const isButton = computed(() => componentNodeName.value === 'button')
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
// NOTE: we do not use scoped styles to reduce specifity and make it easy to overwrite
|
||||
|
||||
// We reset the default styles of a button element to enable easier styling
|
||||
:where(.base-button--type-button) {
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-decoration: none;
|
||||
background-color: transparent;
|
||||
text-align: center;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
:where(.base-button) {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
user-select: none;
|
||||
pointer-events: auto; // disable possible resets
|
||||
|
||||
&:focus {
|
||||
outline: transparent;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div
|
||||
:class="[background ? 'has-background' : '', $route.name+'-view']"
|
||||
:class="[background ? 'has-background' : '', $route.name as string +'-view']"
|
||||
:style="{'background-image': `url(${background})`}"
|
||||
class="link-share-container"
|
||||
>
|
||||
|
|
|
@ -555,4 +555,8 @@ $vikunja-nav-selected-width: 0.4rem;
|
|||
width: 32px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.namespaces-list.loader-container.is-loading {
|
||||
min-height: calc(100vh - #{$navbar-height + 1.5rem + 1rem + 1.5rem});
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
<dropdown class="is-right" ref="usernameDropdown">
|
||||
<template #trigger>
|
||||
<x-button
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
:shadow="false">
|
||||
<span class="username">{{ userInfo.name !== '' ? userInfo.name : userInfo.username }}</span>
|
||||
<span class="icon is-small">
|
||||
|
|
|
@ -1,41 +1,45 @@
|
|||
<template>
|
||||
<a
|
||||
<BaseButton
|
||||
class="button"
|
||||
:class="{
|
||||
:class="[
|
||||
variantClass,
|
||||
{
|
||||
'is-loading': loading,
|
||||
'has-no-shadow': !shadow,
|
||||
'is-primary': type === 'primary',
|
||||
'is-outlined': type === 'secondary',
|
||||
'is-text is-inverted has-no-shadow underline-none':
|
||||
type === 'tertary',
|
||||
}"
|
||||
:disabled="disabled || null"
|
||||
@click="click"
|
||||
:href="href !== '' ? href : null"
|
||||
'has-no-shadow': !shadow || variant === 'tertiary',
|
||||
}
|
||||
]"
|
||||
>
|
||||
<icon :icon="icon" v-if="showIconOnly"/>
|
||||
<span class="icon is-small" v-else-if="icon !== ''">
|
||||
<icon :icon="icon"/>
|
||||
</span>
|
||||
<slot></slot>
|
||||
</a>
|
||||
<slot />
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'x-button',
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, useSlots, PropType} from 'vue'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
|
||||
const BUTTON_TYPES_MAP = Object.freeze({
|
||||
primary: 'is-primary',
|
||||
secondary: 'is-outlined',
|
||||
tertiary: 'is-text is-inverted underline-none',
|
||||
})
|
||||
|
||||
type ButtonTypes = keyof typeof BUTTON_TYPES_MAP
|
||||
|
||||
const props = defineProps({
|
||||
variant: {
|
||||
type: String as PropType<ButtonTypes>,
|
||||
default: 'primary',
|
||||
},
|
||||
href: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
to: {
|
||||
default: false,
|
||||
},
|
||||
icon: {
|
||||
default: '',
|
||||
},
|
||||
|
@ -47,31 +51,12 @@ export default {
|
|||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
computed: {
|
||||
showIconOnly() {
|
||||
return this.icon !== '' && typeof this.$slots.default === 'undefined'
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
click(e) {
|
||||
if (this.disabled) {
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
if (this.to !== false) {
|
||||
this.$router.push(this.to)
|
||||
}
|
||||
const variantClass = computed(() => BUTTON_TYPES_MAP[props.variant])
|
||||
|
||||
this.$emit('click', e)
|
||||
},
|
||||
},
|
||||
}
|
||||
const slots = useSlots()
|
||||
const showIconOnly = computed(() => props.icon !== '' && typeof slots.default === 'undefined')
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -83,8 +68,8 @@ export default {
|
|||
font-weight: bold;
|
||||
height: $button-height;
|
||||
box-shadow: var(--shadow-sm);
|
||||
display: inline-flex;
|
||||
|
||||
&.is-hovered,
|
||||
&:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
@ -106,9 +91,10 @@ export default {
|
|||
color: var(--white);
|
||||
}
|
||||
|
||||
&.is-small {
|
||||
}
|
||||
|
||||
.is-small {
|
||||
border-radius: $radius;
|
||||
}
|
||||
}
|
||||
|
||||
.underline-none {
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
@click="reset"
|
||||
class="is-small ml-2"
|
||||
:shadow="false"
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
>
|
||||
{{ $t('input.resetColor') }}
|
||||
</x-button>
|
||||
|
|
|
@ -101,6 +101,7 @@
|
|||
class="is-fullwidth"
|
||||
:shadow="false"
|
||||
@click="close"
|
||||
v-cy="'closeDatepicker'"
|
||||
>
|
||||
{{ $t('misc.confirm') }}
|
||||
</x-button>
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
<a @click="toggleEdit">{{ $t('input.editor.edit') }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<x-button v-else-if="isEditActive" @click="toggleEdit" type="secondary" :shadow="false">
|
||||
<x-button v-else-if="isEditActive" @click="toggleEdit" variant="secondary" :shadow="false" v-cy="'saveEditor'">
|
||||
{{ $t('misc.save') }}
|
||||
</x-button>
|
||||
</template>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<x-button
|
||||
v-if="hasFilters"
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
@click="clearFilters"
|
||||
>
|
||||
{{ $t('filters.clear') }}
|
||||
|
@ -10,7 +10,7 @@
|
|||
<template #trigger="{toggle}">
|
||||
<x-button
|
||||
@click.prevent.stop="toggle()"
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
icon="filter"
|
||||
>
|
||||
{{ $t('filters.title') }}
|
||||
|
|
|
@ -33,7 +33,9 @@ import {useStore} from 'vuex'
|
|||
|
||||
import ListService from '@/services/list'
|
||||
|
||||
const background = ref(null)
|
||||
import {colorIsDark} from '@/helpers/color/colorIsDark'
|
||||
|
||||
const background = ref<string | null>(null)
|
||||
const backgroundLoading = ref(false)
|
||||
|
||||
const props = defineProps({
|
||||
|
|
|
@ -14,25 +14,25 @@
|
|||
</div>
|
||||
<footer class="modal-card-foot is-flex is-justify-content-flex-end">
|
||||
<x-button
|
||||
v-if="tertiary !== ''"
|
||||
:shadow="false"
|
||||
type="tertary"
|
||||
@click.prevent.stop="$emit('tertary')"
|
||||
v-if="tertary !== ''"
|
||||
variant="tertiary"
|
||||
@click.prevent.stop="$emit('tertiary')"
|
||||
>
|
||||
{{ tertary }}
|
||||
{{ tertiary }}
|
||||
</x-button>
|
||||
<x-button
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
@click.prevent.stop="$router.back()"
|
||||
>
|
||||
{{ $t('misc.cancel') }}
|
||||
</x-button>
|
||||
<x-button
|
||||
type="primary"
|
||||
v-if="primaryLabel !== ''"
|
||||
variant="primary"
|
||||
@click.prevent.stop="primary"
|
||||
:icon="primaryIcon"
|
||||
:disabled="primaryDisabled"
|
||||
v-if="primaryLabel !== ''"
|
||||
>
|
||||
{{ primaryLabel }}
|
||||
</x-button>
|
||||
|
@ -65,7 +65,7 @@ export default {
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
tertary: {
|
||||
tertiary: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
@ -78,7 +78,7 @@ export default {
|
|||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['create', 'primary', 'tertary'],
|
||||
emits: ['create', 'primary', 'tertiary'],
|
||||
methods: {
|
||||
primary() {
|
||||
this.$emit('create')
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="dropdown is-right is-active" ref="dropdown">
|
||||
<div class="dropdown-trigger" @click="open = !open">
|
||||
<div class="dropdown-trigger is-flex" @click="open = !open">
|
||||
<slot name="trigger" :close="close">
|
||||
<icon :icon="triggerIcon" class="icon"/>
|
||||
</slot>
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
@click="action.callback"
|
||||
:shadow="false"
|
||||
class="is-small"
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
v-for="(action, i) in item.data.actions"
|
||||
>
|
||||
{{ action.title }}
|
||||
|
|
|
@ -50,11 +50,12 @@ import Message from '@/components/misc/message.vue'
|
|||
import NoAuthWrapper from '@/components/misc/no-auth-wrapper.vue'
|
||||
|
||||
import {ERROR_NO_API_URL} from '@/helpers/checkAndSetApiUrl'
|
||||
import {useOnline} from '@/composables/useOnline'
|
||||
|
||||
const store = useStore()
|
||||
|
||||
const ready = computed(() => store.state.vikunjaReady)
|
||||
const online = computed(() => store.state.online)
|
||||
const online = useOnline()
|
||||
|
||||
const error = ref('')
|
||||
const showLoading = computed(() => !ready.value && error.value === '')
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<x-button
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
:icon="icon"
|
||||
v-tooltip="tooltipText"
|
||||
@click="changeSubscription"
|
||||
|
|
|
@ -31,14 +31,15 @@
|
|||
<div class="actions">
|
||||
<x-button
|
||||
@click="$emit('close')"
|
||||
type="tertary"
|
||||
variant="tertiary"
|
||||
class="has-text-danger"
|
||||
>
|
||||
{{ $t('misc.cancel') }}
|
||||
</x-button>
|
||||
<x-button
|
||||
@click="$emit('submit')"
|
||||
type="primary"
|
||||
variant="primary"
|
||||
v-cy="'modalPrimary'"
|
||||
:shadow="false"
|
||||
>
|
||||
{{ $t('misc.doit') }}
|
||||
|
|
|
@ -42,7 +42,7 @@ import {useI18n} from 'vue-i18n'
|
|||
import {useStore} from 'vuex'
|
||||
import { tryOnMounted, debouncedWatch, useWindowSize, MaybeRef } from '@vueuse/core'
|
||||
|
||||
import TaskService from '../../services/task'
|
||||
import TaskService from '@/services/task'
|
||||
import QuickAddMagic from '@/components/tasks/partials/quick-add-magic.vue'
|
||||
|
||||
function cleanupTitle(title: string) {
|
||||
|
@ -117,9 +117,6 @@ function useAutoHeightTextarea(value: MaybeRef<string>) {
|
|||
return textarea
|
||||
}
|
||||
|
||||
const emit = defineEmits(['taskAdded'])
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
defaultPosition: {
|
||||
type: Number,
|
||||
|
@ -127,8 +124,7 @@ const props = defineProps({
|
|||
},
|
||||
})
|
||||
|
||||
const taskService = shallowReactive(new TaskService())
|
||||
const errorMessage = ref('')
|
||||
const emit = defineEmits(['taskAdded'])
|
||||
|
||||
const newTaskTitle = ref('')
|
||||
const newTaskInput = useAutoHeightTextarea(newTaskTitle)
|
||||
|
@ -136,6 +132,9 @@ const newTaskInput = useAutoHeightTextarea(newTaskTitle)
|
|||
const { t } = useI18n()
|
||||
const store = useStore()
|
||||
|
||||
const taskService = shallowReactive(new TaskService())
|
||||
const errorMessage = ref('')
|
||||
|
||||
async function addTask() {
|
||||
if (newTaskTitle.value === '') {
|
||||
errorMessage.value = t('list.create.addTitleRequired')
|
||||
|
|
|
@ -78,7 +78,6 @@
|
|||
<script>
|
||||
import AsyncEditor from '@/components/input/AsyncEditor'
|
||||
|
||||
import ListService from '../../services/list'
|
||||
import TaskService from '../../services/task'
|
||||
import TaskModel from '../../models/task'
|
||||
import priorities from '../../models/constants/priorities'
|
||||
|
@ -90,14 +89,10 @@ export default {
|
|||
name: 'edit-task',
|
||||
data() {
|
||||
return {
|
||||
listId: this.$route.params.id,
|
||||
listService: new ListService(),
|
||||
taskService: new TaskService(),
|
||||
|
||||
priorities: priorities,
|
||||
list: {},
|
||||
editorActive: false,
|
||||
newTask: new TaskModel(),
|
||||
isTaskEdit: false,
|
||||
taskEditTask: TaskModel,
|
||||
}
|
||||
|
|
|
@ -183,6 +183,8 @@ import {mapState} from 'vuex'
|
|||
import Rights from '../../models/constants/rights.json'
|
||||
import FilterPopup from '@/components/list/partials/filter-popup.vue'
|
||||
|
||||
import {colorIsDark} from '@/helpers/color/colorIsDark'
|
||||
|
||||
export default {
|
||||
name: 'GanttChart',
|
||||
components: {
|
||||
|
@ -201,10 +203,10 @@ export default {
|
|||
default: false,
|
||||
},
|
||||
dateFrom: {
|
||||
default: new Date(new Date().setDate(new Date().getDate() - 15)),
|
||||
default: () => new Date(new Date().setDate(new Date().getDate() - 15)),
|
||||
},
|
||||
dateTo: {
|
||||
default: new Date(new Date().setDate(new Date().getDate() + 30)),
|
||||
default: () => new Date(new Date().setDate(new Date().getDate() + 30)),
|
||||
},
|
||||
// The width of a day in pixels, used to calculate all sorts of things.
|
||||
dayWidth: {
|
||||
|
@ -252,6 +254,7 @@ export default {
|
|||
canWrite: (state) => state.currentList.maxRight > Rights.READ,
|
||||
}),
|
||||
methods: {
|
||||
colorIsDark,
|
||||
buildTheGanttChart() {
|
||||
this.setDates()
|
||||
this.prepareGanttDays()
|
||||
|
|
|
@ -83,7 +83,7 @@
|
|||
@click="$refs.files.click()"
|
||||
class="mb-4"
|
||||
icon="cloud-upload-alt"
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
:shadow="false"
|
||||
>
|
||||
{{ $t('task.attachment.upload') }}
|
||||
|
|
|
@ -8,21 +8,21 @@
|
|||
<x-button
|
||||
@click.prevent.stop="() => deferDays(1)"
|
||||
:shadow="false"
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
>
|
||||
{{ $t('task.deferDueDate.1day') }}
|
||||
</x-button>
|
||||
<x-button
|
||||
@click.prevent.stop="() => deferDays(3)"
|
||||
:shadow="false"
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
>
|
||||
{{ $t('task.deferDueDate.3days') }}
|
||||
</x-button>
|
||||
<x-button
|
||||
@click.prevent.stop="() => deferDays(7)"
|
||||
:shadow="false"
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
>
|
||||
{{ $t('task.deferDueDate.1week') }}
|
||||
</x-button>
|
||||
|
|
|
@ -73,6 +73,8 @@ import Done from '@/components/misc/Done.vue'
|
|||
import Labels from '../../../components/tasks/partials/labels'
|
||||
import ChecklistSummary from './checklist-summary'
|
||||
|
||||
import {colorIsDark} from '@/helpers/color/colorIsDark'
|
||||
|
||||
export default {
|
||||
name: 'kanban-card',
|
||||
components: {
|
||||
|
@ -98,6 +100,7 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
colorIsDark,
|
||||
async toggleTaskDone(task) {
|
||||
this.loadingInternal = true
|
||||
try {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
class="is-pulled-right add-task-relation-button"
|
||||
:class="{'is-active': showNewRelationForm}"
|
||||
v-tooltip="$t('task.relation.add')"
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
icon="plus"
|
||||
:shadow="false"
|
||||
/>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<template>
|
||||
<div class="control repeat-after-input">
|
||||
<div class="buttons has-addons is-centered mt-2">
|
||||
<x-button type="secondary" class="is-small" @click="() => setRepeatAfter(1, 'days')">{{ $t('task.repeat.everyDay') }}</x-button>
|
||||
<x-button type="secondary" class="is-small" @click="() => setRepeatAfter(1, 'weeks')">{{ $t('task.repeat.everyWeek') }}</x-button>
|
||||
<x-button type="secondary" class="is-small" @click="() => setRepeatAfter(1, 'months')">{{ $t('task.repeat.everyMonth') }}</x-button>
|
||||
<x-button variant="secondary" class="is-small" @click="() => setRepeatAfter(1, 'days')">{{ $t('task.repeat.everyDay') }}</x-button>
|
||||
<x-button variant="secondary" class="is-small" @click="() => setRepeatAfter(1, 'weeks')">{{ $t('task.repeat.everyWeek') }}</x-button>
|
||||
<x-button variant="secondary" class="is-small" @click="() => setRepeatAfter(1, 'months')">{{ $t('task.repeat.everyMonth') }}</x-button>
|
||||
</div>
|
||||
<div class="is-flex is-align-items-center mb-2">
|
||||
<label for="repeatMode" class="is-fullwidth">
|
||||
|
|
16
src/composables/useBodyClass.ts
Normal file
16
src/composables/useBodyClass.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import {ref, watchEffect} from 'vue'
|
||||
import {tryOnBeforeUnmount} from '@vueuse/core'
|
||||
|
||||
export function useBodyClass(className: string, defaultValue = false) {
|
||||
const isActive = ref(defaultValue)
|
||||
|
||||
watchEffect(() => {
|
||||
isActive.value
|
||||
? document.body.classList.add(className)
|
||||
: document.body.classList.remove(className)
|
||||
})
|
||||
|
||||
tryOnBeforeUnmount(() => isActive.value && document.body.classList.remove(className))
|
||||
|
||||
return isActive
|
||||
}
|
31
src/composables/useDateTimeSalutation.test.ts
Normal file
31
src/composables/useDateTimeSalutation.test.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
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')
|
||||
})
|
||||
})
|
31
src/composables/useDateTimeSalutation.ts
Normal file
31
src/composables/useDateTimeSalutation.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
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))
|
||||
}
|
14
src/composables/useOnline.ts
Normal file
14
src/composables/useOnline.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import {ref} from 'vue'
|
||||
import {useOnline as useNetworkOnline, ConfigurableWindow} from '@vueuse/core'
|
||||
|
||||
|
||||
export function useOnline(options?: ConfigurableWindow) {
|
||||
const fakeOnlineState = !!import.meta.env.VITE_IS_ONLINE
|
||||
if (fakeOnlineState) {
|
||||
console.log('Setting fake online state', fakeOnlineState)
|
||||
}
|
||||
|
||||
return fakeOnlineState
|
||||
? ref(true)
|
||||
: useNetworkOnline(options)
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
import {it, expect} from 'vitest'
|
||||
|
||||
import {calculateItemPosition} from './calculateItemPosition'
|
||||
|
||||
it('should calculate the task position', () => {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import {describe, it, expect} from 'vitest'
|
||||
|
||||
import {findCheckboxesInText, getChecklistStatistics} from './checklistFromText'
|
||||
|
||||
describe('Find checklists in text', () => {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import {test, expect} from 'vitest'
|
||||
|
||||
import {colorFromHex} from './colorFromHex'
|
||||
|
||||
test('hex', () => {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import {test, expect} from 'vitest'
|
||||
|
||||
import {colorIsDark} from './colorIsDark'
|
||||
|
||||
test('dark color', () => {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import {describe, it, expect} from 'vitest'
|
||||
|
||||
import {filterLabelsByQuery} from './labels'
|
||||
import {createNewIndexer} from '../indexes'
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import {test, expect} from 'vitest'
|
||||
|
||||
import {calculateDayInterval} from './calculateDayInterval'
|
||||
|
||||
const days = {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import {test, expect} from 'vitest'
|
||||
|
||||
import {calculateNearestHours} from './calculateNearestHours'
|
||||
|
||||
test('5:00', () => {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import {test, expect} from 'vitest'
|
||||
|
||||
import {createDateFromString} from './createDateFromString'
|
||||
|
||||
test('YYYY-MM-DD HH:MM', () => {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"lastViewed": "Naposledy zobrazeno",
|
||||
"list": {
|
||||
"newText": "Můžete vytvořit nový seznam pro své nové úkoly:",
|
||||
"new": "Vytvořit nový seznam",
|
||||
"new": "New list",
|
||||
"importText": "Nebo importujte své seznamy a úkoly z jiných služeb:",
|
||||
"import": "Importujte svá data do Vikunja"
|
||||
}
|
||||
|
@ -157,7 +157,7 @@
|
|||
"searchSelect": "Klikněte nebo stiskněte Enter pro výběr tohoto seznamu",
|
||||
"shared": "Sdílené seznamy",
|
||||
"create": {
|
||||
"header": "Vytvořit nový seznam",
|
||||
"header": "New list",
|
||||
"titlePlaceholder": "Název seznamu přijde sem…",
|
||||
"addTitleRequired": "Uveďte prosím název.",
|
||||
"createdSuccess": "Seznam byl úspěšně vytvořen.",
|
||||
|
@ -315,7 +315,7 @@
|
|||
"namespaces": "Prostory",
|
||||
"search": "Začni psát pro vyhledání prostoru…",
|
||||
"create": {
|
||||
"title": "Vytvořit nový prostor",
|
||||
"title": "New namespace",
|
||||
"titleRequired": "Uveďte prosím název.",
|
||||
"explanation": "Prostor je kolekce seznamů, které můžete sdílet a používat k organizaci seznamů. Každý seznam patří do nějakého prostoru.",
|
||||
"tooltip": "Co je prostor?",
|
||||
|
@ -383,7 +383,7 @@
|
|||
"reminderRange": "Připomínky - období"
|
||||
},
|
||||
"create": {
|
||||
"title": "Vytvořit uložený filtr",
|
||||
"title": "New Saved Filter",
|
||||
"description": "Uložený filtr je virtuální seznam, který se počítá ze sady filtrů pokaždé, když je přístupný. Jakmile bude vytvořen, objeví se ve speciálním prostoru.",
|
||||
"action": "Vytvořit uložený filtr"
|
||||
},
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"lastViewed": "Zuletzt angesehen",
|
||||
"list": {
|
||||
"newText": "Du kannst eine neue Liste für deine neuen Aufgaben erstellen:",
|
||||
"new": "Eine neue Liste erstellen",
|
||||
"new": "New list",
|
||||
"importText": "Oder importiere deine Listen und Aufgaben aus anderen Diensten in Vikunja:",
|
||||
"import": "Deine Daten in Vikunja importieren"
|
||||
}
|
||||
|
@ -36,7 +36,7 @@
|
|||
"password": "Passwort",
|
||||
"passwordRepeat": "Gib dein Passwort erneut ein",
|
||||
"passwordPlaceholder": "z.B. •••••••••••",
|
||||
"forgotPassword": "Forgot your password?",
|
||||
"forgotPassword": "Passwort vergessen?",
|
||||
"resetPassword": "Setze dein Passwort zurück",
|
||||
"resetPasswordAction": "Sende mir einen Link zum Zurücksetzen des Passworts",
|
||||
"resetPasswordSuccess": "Prüfe deinen Posteingang! Du solltest eine E-Mail mit Anweisungen zum Zurücksetzen deines Passworts erhalten haben.",
|
||||
|
@ -103,7 +103,7 @@
|
|||
"title": "Avatar",
|
||||
"initials": "Initialen",
|
||||
"gravatar": "Gravatar",
|
||||
"marble": "Marble",
|
||||
"marble": "Murmel",
|
||||
"upload": "Hochladen",
|
||||
"uploadAvatar": "Avatar hochladen",
|
||||
"statusUpdateSuccess": "Avatar-Status wurde erfolgreich aktualisiert.",
|
||||
|
@ -157,7 +157,7 @@
|
|||
"searchSelect": "Klicke auf oder drücke die Eingabetaste, um diese Liste auszuwählen",
|
||||
"shared": "Geteilte Listen",
|
||||
"create": {
|
||||
"header": "Eine neue Liste erstellen",
|
||||
"header": "New list",
|
||||
"titlePlaceholder": "Der Titel der Liste steht hier…",
|
||||
"addTitleRequired": "Bitte gebe einen Namen an.",
|
||||
"createdSuccess": "Die Liste wurde erfolgreich erstellt.",
|
||||
|
@ -315,7 +315,7 @@
|
|||
"namespaces": "Namespaces",
|
||||
"search": "Beginne zu schreiben, um einen Namespace zu suchen…",
|
||||
"create": {
|
||||
"title": "Einen neuen Namespace erstellen",
|
||||
"title": "New 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.",
|
||||
"tooltip": "Was ist ein Namespace?",
|
||||
|
@ -374,7 +374,7 @@
|
|||
"includeNulls": "Aufgaben ohne Werte einbeziehen",
|
||||
"requireAll": "Alle Filterkriterien müssen erfüllt sein, damit eine Aufgabe angezeigt wird",
|
||||
"showDoneTasks": "Erledigte Aufgaben anzeigen",
|
||||
"sortAlphabetically": "Sort Alphabetically",
|
||||
"sortAlphabetically": "Alphabetisch sortieren",
|
||||
"enablePriority": "Filter nach Priorität aktivieren",
|
||||
"enablePercentDone": "Filter nach % Erledigt aktivieren",
|
||||
"dueDateRange": "Fälligkeitsbereich",
|
||||
|
@ -383,7 +383,7 @@
|
|||
"reminderRange": "Erinnerungs-Datumsbereich"
|
||||
},
|
||||
"create": {
|
||||
"title": "Einen gespeicherten Filter erstellen",
|
||||
"title": "New Saved Filter",
|
||||
"description": "Ein gespeicherter Filter ist eine virtuelle Liste, die bei jedem Zugriff aus einem Satz von Filtern errechnet wird. Einmal erstellt, erscheint diese in einem speziellen Namespace.",
|
||||
"action": "Neuen gespeicherten Filter erstellen"
|
||||
},
|
||||
|
@ -475,8 +475,8 @@
|
|||
"download": "Herunterladen",
|
||||
"showMenu": "Menü anzeigen",
|
||||
"hideMenu": "Menü ausblenden",
|
||||
"forExample": "For example:",
|
||||
"welcomeBack": "Welcome Back!"
|
||||
"forExample": "Zum Beispiel:",
|
||||
"welcomeBack": "Willkommen zurück!"
|
||||
},
|
||||
"input": {
|
||||
"resetColor": "Farbe zurücksetzen",
|
||||
|
@ -726,8 +726,8 @@
|
|||
"dateCurrentYear": "wird das laufende Jahr nutzen",
|
||||
"dateNth": "wird den {day}. des aktuellen Monats verwenden",
|
||||
"dateTime": "Kombiniere eines der Datumsformate mit \"{time}\" (oder {timePM}), um eine Zeit festzulegen.",
|
||||
"repeats": "Repeating tasks",
|
||||
"repeatsDescription": "To set a task as repeating in an interval, simply add '{suffix}' to the task text. The amount needs to be a number and can be omitted to use just the type (see examples)."
|
||||
"repeats": "Wiederholende Aufgaben",
|
||||
"repeatsDescription": "Um eine Aufgabe als Wiederholung in einem Intervall festzulegen, füge einfach '{suffix}' dem Aufgabentext hinzu. Der Betrag muss eine Zahl sein und kann weggelassen werden, um nur den Typ zu verwenden (siehe Beispiele)."
|
||||
}
|
||||
},
|
||||
"team": {
|
||||
|
@ -814,7 +814,7 @@
|
|||
"url": "Vikunja-URL",
|
||||
"urlPlaceholder": "z.B. https://localhost:3456",
|
||||
"change": "ändern",
|
||||
"use": "Using Vikunja installation at {0}",
|
||||
"use": "Verwende die Vikunja-Installation unter „{0}“",
|
||||
"error": "Konnte keine Vikunja-Installation unter „{domain}“ finden oder verwenden. Bitte probiere eine andere Url.",
|
||||
"success": "Verwende die Vikunja-Installation unter „{domain}“.",
|
||||
"urlRequired": "Eine Url ist erforderlich."
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"lastViewed": "Zletscht ahglueget",
|
||||
"list": {
|
||||
"newText": "Du chasch e Liste für dini neue Uufgabe erstelle:",
|
||||
"new": "Neui Liste erstelle",
|
||||
"new": "New list",
|
||||
"importText": "Oder importier dini Liste und Uufgabe us anderne Dienst nach Vikunja:",
|
||||
"import": "Dini Date in Vikunja importiere"
|
||||
}
|
||||
|
@ -36,7 +36,7 @@
|
|||
"password": "Passwort",
|
||||
"passwordRepeat": "Gib dis Passwort nomal iih",
|
||||
"passwordPlaceholder": "z.B. •••••••••••",
|
||||
"forgotPassword": "Forgot your password?",
|
||||
"forgotPassword": "Passwort vergessen?",
|
||||
"resetPassword": "Setz diis Passwort zrugg",
|
||||
"resetPasswordAction": "Schick mir en Passwort zruggsetz Link",
|
||||
"resetPasswordSuccess": "Prüfe deinen Posteingang! Du solltest eine E-Mail mit Anweisungen zum Zurücksetzen deines Passworts erhalten haben.",
|
||||
|
@ -103,7 +103,7 @@
|
|||
"title": "Herr Der Elemente",
|
||||
"initials": "Initialä",
|
||||
"gravatar": "Gravatar",
|
||||
"marble": "Marble",
|
||||
"marble": "Murmel",
|
||||
"upload": "Ufeladä",
|
||||
"uploadAvatar": "Profiilbild ufeladä",
|
||||
"statusUpdateSuccess": "Avatar Zuestand erfolgriich aktualisiert!",
|
||||
|
@ -157,7 +157,7 @@
|
|||
"searchSelect": "Druck uf Enter um die Liste uuszwähle",
|
||||
"shared": "Teilti Liste",
|
||||
"create": {
|
||||
"header": "Neui Liste erstelle",
|
||||
"header": "New list",
|
||||
"titlePlaceholder": "Listetitl da ahgeh…",
|
||||
"addTitleRequired": "Bitte gib en Titl ah.",
|
||||
"createdSuccess": "Liste erfolgriich erstellt.",
|
||||
|
@ -315,7 +315,7 @@
|
|||
"namespaces": "Namensrüüm",
|
||||
"search": "Schriib, um nachemne Namensruum z'sueche…",
|
||||
"create": {
|
||||
"title": "Neue Namensruum erstelle",
|
||||
"title": "New namespace",
|
||||
"titleRequired": "Bitte gib en Titl ah.",
|
||||
"explanation": "En Namensruum isch e Gruppe vo Liste, wo du chasch zur Organisation benutze. Tatsächlich sind alli Listene emne Namensruum zuegwise.",
|
||||
"tooltip": "Was isch en Namensruum?",
|
||||
|
@ -374,7 +374,7 @@
|
|||
"includeNulls": "Uufgabe ohni Wert iihbezieh",
|
||||
"requireAll": "Alli Filter mend wahr sii, demits die Uufgab ahzeigt",
|
||||
"showDoneTasks": "Zeig die fertige Uufgabe",
|
||||
"sortAlphabetically": "Sort Alphabetically",
|
||||
"sortAlphabetically": "Alphabetisch sortieren",
|
||||
"enablePriority": "Filter nach Priorität aktiviere",
|
||||
"enablePercentDone": "Filter nach Prozent iihschalte",
|
||||
"dueDateRange": "Fälligkeitsberiich",
|
||||
|
@ -383,7 +383,7 @@
|
|||
"reminderRange": "Errinnerigs Datumbereich"
|
||||
},
|
||||
"create": {
|
||||
"title": "Neue gspeicherete Filter erstelle",
|
||||
"title": "New Saved Filter",
|
||||
"description": "En gspeicherete Filter isch e virtuelli Liste, welche vomene Satz a Filter zemmegsetzt wird, sobald me uf sie zuegriift. Wenn sie mal erstellt worde isch, erhaltet si ihren eigene Namensruum.",
|
||||
"action": "Neue gspeicherete Filter erstelle"
|
||||
},
|
||||
|
@ -475,8 +475,8 @@
|
|||
"download": "Herunterladen",
|
||||
"showMenu": "Menü anzeigen",
|
||||
"hideMenu": "Menü ausblenden",
|
||||
"forExample": "For example:",
|
||||
"welcomeBack": "Welcome Back!"
|
||||
"forExample": "Zum Beispiel:",
|
||||
"welcomeBack": "Willkommen zurück!"
|
||||
},
|
||||
"input": {
|
||||
"resetColor": "Farb zruggsetze",
|
||||
|
@ -726,8 +726,8 @@
|
|||
"dateCurrentYear": "nimmt das laufende Jahr",
|
||||
"dateNth": "nimmt de {day}ti vom jetzige Monet",
|
||||
"dateTime": "Kombiniere irgendeis vo dene Datumsformat mit \"{time}\" (oder {timePM}) um e Ziit z'setze.",
|
||||
"repeats": "Repeating tasks",
|
||||
"repeatsDescription": "To set a task as repeating in an interval, simply add '{suffix}' to the task text. The amount needs to be a number and can be omitted to use just the type (see examples)."
|
||||
"repeats": "Wiederholende Aufgaben",
|
||||
"repeatsDescription": "Um eine Aufgabe als Wiederholung in einem Intervall festzulegen, füge einfach '{suffix}' dem Aufgabentext hinzu. Der Betrag muss eine Zahl sein und kann weggelassen werden, um nur den Typ zu verwenden (siehe Beispiele)."
|
||||
}
|
||||
},
|
||||
"team": {
|
||||
|
@ -814,7 +814,7 @@
|
|||
"url": "Vikunja URL",
|
||||
"urlPlaceholder": "z.B. https://localhost:3456",
|
||||
"change": "ändere",
|
||||
"use": "Using Vikunja installation at {0}",
|
||||
"use": "Verwende die Vikunja-Installation unter „{0}“",
|
||||
"error": "Konnte keine Vikunja-Installation unter „{domain}“ finden oder verwenden. Bitte probiere eine andere Url.",
|
||||
"success": "Benutze d'Vikunja Installation uf \"{domain}\".",
|
||||
"urlRequired": "Eine Url ist erforderlich."
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"lastViewed": "Last viewed",
|
||||
"list": {
|
||||
"newText": "You can create a new list for your new tasks:",
|
||||
"new": "Create a new list",
|
||||
"new": "New list",
|
||||
"importText": "Or import your lists and tasks from other services into Vikunja:",
|
||||
"import": "Import your data into Vikunja"
|
||||
}
|
||||
|
@ -163,7 +163,7 @@
|
|||
"searchSelect": "Click or press enter to select this list",
|
||||
"shared": "Shared Lists",
|
||||
"create": {
|
||||
"header": "Create a new list",
|
||||
"header": "New list",
|
||||
"titlePlaceholder": "The list's title goes here…",
|
||||
"addTitleRequired": "Please specify a title.",
|
||||
"createdSuccess": "The list was successfully created.",
|
||||
|
@ -321,7 +321,7 @@
|
|||
"namespaces": "Namespaces",
|
||||
"search": "Type to search for a namespace…",
|
||||
"create": {
|
||||
"title": "Create a new namespace",
|
||||
"title": "New namespace",
|
||||
"titleRequired": "Please specify a title.",
|
||||
"explanation": "A namespace is a collection of lists you can share and use to organize your lists with. In fact, every list belongs to a namepace.",
|
||||
"tooltip": "What's a namespace?",
|
||||
|
@ -389,7 +389,7 @@
|
|||
"reminderRange": "Reminder Date Range"
|
||||
},
|
||||
"create": {
|
||||
"title": "Create A Saved Filter",
|
||||
"title": "New Saved Filter",
|
||||
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
|
||||
"action": "Create new saved filter"
|
||||
},
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"lastViewed": "Last viewed",
|
||||
"list": {
|
||||
"newText": "You can create a new list for your new tasks:",
|
||||
"new": "Create a new list",
|
||||
"new": "New list",
|
||||
"importText": "Or import your lists and tasks from other services into Vikunja:",
|
||||
"import": "Import your data into Vikunja"
|
||||
}
|
||||
|
@ -157,7 +157,7 @@
|
|||
"searchSelect": "Click or press enter to select this list",
|
||||
"shared": "Shared Lists",
|
||||
"create": {
|
||||
"header": "Create a new list",
|
||||
"header": "New list",
|
||||
"titlePlaceholder": "The list's title goes here…",
|
||||
"addTitleRequired": "Please specify a title.",
|
||||
"createdSuccess": "The list was successfully created.",
|
||||
|
@ -315,7 +315,7 @@
|
|||
"namespaces": "Namespaces",
|
||||
"search": "Type to search for a namespace…",
|
||||
"create": {
|
||||
"title": "Create a new namespace",
|
||||
"title": "New namespace",
|
||||
"titleRequired": "Please specify a title.",
|
||||
"explanation": "A namespace is a collection of lists you can share and use to organize your lists with. In fact, every list belongs to a namepace.",
|
||||
"tooltip": "What's a namespace?",
|
||||
|
@ -383,7 +383,7 @@
|
|||
"reminderRange": "Reminder Date Range"
|
||||
},
|
||||
"create": {
|
||||
"title": "Create A Saved Filter",
|
||||
"title": "New Saved Filter",
|
||||
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
|
||||
"action": "Create new saved filter"
|
||||
},
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"lastViewed": "Dernière consultation",
|
||||
"list": {
|
||||
"newText": "Tu peux créer une nouvelle liste pour tes nouvelles tâches :",
|
||||
"new": "Créer une nouvelle liste",
|
||||
"new": "New list",
|
||||
"importText": "Ou importe tes listes et tâches d’autres services dans Vikunja :",
|
||||
"import": "Importer tes données dans Vikunja"
|
||||
}
|
||||
|
@ -157,7 +157,7 @@
|
|||
"searchSelect": "Clique ou appuie sur la touche Entrée pour sélectionner cette liste",
|
||||
"shared": "Listes partagées",
|
||||
"create": {
|
||||
"header": "Créer une nouvelle liste",
|
||||
"header": "New list",
|
||||
"titlePlaceholder": "Entre le nom de la liste…",
|
||||
"addTitleRequired": "Indique un nom.",
|
||||
"createdSuccess": "Liste créée.",
|
||||
|
@ -315,7 +315,7 @@
|
|||
"namespaces": "Espaces de noms",
|
||||
"search": "Écris pour rechercher un espace de noms…",
|
||||
"create": {
|
||||
"title": "Créer un nouvel espace de noms",
|
||||
"title": "New namespace",
|
||||
"titleRequired": "Indique un nom.",
|
||||
"explanation": "Des collections de listes pour partager et organiser vos listes. En fait, chaque liste appartient à un espace de noms.",
|
||||
"tooltip": "Qu’est-ce qu’un espace de noms ?",
|
||||
|
@ -383,7 +383,7 @@
|
|||
"reminderRange": "Plage de dates de rappel"
|
||||
},
|
||||
"create": {
|
||||
"title": "Créer un filtre enregistré",
|
||||
"title": "New Saved Filter",
|
||||
"description": "Un filtre enregistré est une liste virtuelle qui est calculée à partir d’un ensemble de filtres à chaque fois qu’on y accède. Une fois créé, il apparaît dans un espace de noms spécial.",
|
||||
"action": "Créer un nouveau filtre enregistré"
|
||||
},
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"lastViewed": "Ultima visualizzazione",
|
||||
"list": {
|
||||
"newText": "È possibile creare una nuova lista per le nuove attività:",
|
||||
"new": "Crea una nuova lista",
|
||||
"new": "New list",
|
||||
"importText": "O importare le liste e le attività da altri servizi in Vikunja:",
|
||||
"import": "Importa i tuoi dati in Vikunja"
|
||||
}
|
||||
|
@ -157,7 +157,7 @@
|
|||
"searchSelect": "Fare clic o premere invio per selezionare questa lista",
|
||||
"shared": "Liste Condivise",
|
||||
"create": {
|
||||
"header": "Crea una nuova lista",
|
||||
"header": "New list",
|
||||
"titlePlaceholder": "Il titolo della lista va qui…",
|
||||
"addTitleRequired": "Specifica un titolo.",
|
||||
"createdSuccess": "La lista è stata creata correttamente.",
|
||||
|
@ -315,7 +315,7 @@
|
|||
"namespaces": "Namespaces",
|
||||
"search": "Type to search for a namespace…",
|
||||
"create": {
|
||||
"title": "Create a new namespace",
|
||||
"title": "New namespace",
|
||||
"titleRequired": "Please specify a title.",
|
||||
"explanation": "A namespace is a collection of lists you can share and use to organize your lists with. In fact, every list belongs to a namepace.",
|
||||
"tooltip": "What's a namespace?",
|
||||
|
@ -383,7 +383,7 @@
|
|||
"reminderRange": "Reminder Date Range"
|
||||
},
|
||||
"create": {
|
||||
"title": "Crea Un Filtro Salvato",
|
||||
"title": "New Saved Filter",
|
||||
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
|
||||
"action": "Crea nuovo filtro salvato"
|
||||
},
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"lastViewed": "Last viewed",
|
||||
"list": {
|
||||
"newText": "You can create a new list for your new tasks:",
|
||||
"new": "Create a new list",
|
||||
"new": "New list",
|
||||
"importText": "Or import your lists and tasks from other services into Vikunja:",
|
||||
"import": "Import your data into Vikunja"
|
||||
}
|
||||
|
@ -157,7 +157,7 @@
|
|||
"searchSelect": "Click or press enter to select this list",
|
||||
"shared": "Shared Lists",
|
||||
"create": {
|
||||
"header": "Create a new list",
|
||||
"header": "New list",
|
||||
"titlePlaceholder": "The list's title goes here…",
|
||||
"addTitleRequired": "Please specify a title.",
|
||||
"createdSuccess": "The list was successfully created.",
|
||||
|
@ -315,7 +315,7 @@
|
|||
"namespaces": "Namespaces",
|
||||
"search": "Type to search for a namespace…",
|
||||
"create": {
|
||||
"title": "Create a new namespace",
|
||||
"title": "New namespace",
|
||||
"titleRequired": "Please specify a title.",
|
||||
"explanation": "A namespace is a collection of lists you can share and use to organize your lists with. In fact, every list belongs to a namepace.",
|
||||
"tooltip": "What's a namespace?",
|
||||
|
@ -383,7 +383,7 @@
|
|||
"reminderRange": "Reminder Date Range"
|
||||
},
|
||||
"create": {
|
||||
"title": "Create A Saved Filter",
|
||||
"title": "New Saved Filter",
|
||||
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
|
||||
"action": "Create new saved filter"
|
||||
},
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"lastViewed": "Last viewed",
|
||||
"list": {
|
||||
"newText": "You can create a new list for your new tasks:",
|
||||
"new": "Create a new list",
|
||||
"new": "New list",
|
||||
"importText": "Or import your lists and tasks from other services into Vikunja:",
|
||||
"import": "Import your data into Vikunja"
|
||||
}
|
||||
|
@ -157,7 +157,7 @@
|
|||
"searchSelect": "Click or press enter to select this list",
|
||||
"shared": "Shared Lists",
|
||||
"create": {
|
||||
"header": "Create a new list",
|
||||
"header": "New list",
|
||||
"titlePlaceholder": "The list's title goes here…",
|
||||
"addTitleRequired": "Please specify a title.",
|
||||
"createdSuccess": "The list was successfully created.",
|
||||
|
@ -315,7 +315,7 @@
|
|||
"namespaces": "Namespaces",
|
||||
"search": "Type to search for a namespace…",
|
||||
"create": {
|
||||
"title": "Create a new namespace",
|
||||
"title": "New namespace",
|
||||
"titleRequired": "Please specify a title.",
|
||||
"explanation": "A namespace is a collection of lists you can share and use to organize your lists with. In fact, every list belongs to a namepace.",
|
||||
"tooltip": "What's a namespace?",
|
||||
|
@ -383,7 +383,7 @@
|
|||
"reminderRange": "Reminder Date Range"
|
||||
},
|
||||
"create": {
|
||||
"title": "Create A Saved Filter",
|
||||
"title": "New Saved Filter",
|
||||
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
|
||||
"action": "Create new saved filter"
|
||||
},
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"lastViewed": "Last viewed",
|
||||
"list": {
|
||||
"newText": "You can create a new list for your new tasks:",
|
||||
"new": "Create a new list",
|
||||
"new": "New list",
|
||||
"importText": "Or import your lists and tasks from other services into Vikunja:",
|
||||
"import": "Import your data into Vikunja"
|
||||
}
|
||||
|
@ -157,7 +157,7 @@
|
|||
"searchSelect": "Click or press enter to select this list",
|
||||
"shared": "Shared Lists",
|
||||
"create": {
|
||||
"header": "Create a new list",
|
||||
"header": "New list",
|
||||
"titlePlaceholder": "The list's title goes here…",
|
||||
"addTitleRequired": "Please specify a title.",
|
||||
"createdSuccess": "The list was successfully created.",
|
||||
|
@ -315,7 +315,7 @@
|
|||
"namespaces": "Namespaces",
|
||||
"search": "Type to search for a namespace…",
|
||||
"create": {
|
||||
"title": "Create a new namespace",
|
||||
"title": "New namespace",
|
||||
"titleRequired": "Please specify a title.",
|
||||
"explanation": "A namespace is a collection of lists you can share and use to organize your lists with. In fact, every list belongs to a namepace.",
|
||||
"tooltip": "What's a namespace?",
|
||||
|
@ -383,7 +383,7 @@
|
|||
"reminderRange": "Reminder Date Range"
|
||||
},
|
||||
"create": {
|
||||
"title": "Create A Saved Filter",
|
||||
"title": "New Saved Filter",
|
||||
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
|
||||
"action": "Create new saved filter"
|
||||
},
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"lastViewed": "Last viewed",
|
||||
"list": {
|
||||
"newText": "You can create a new list for your new tasks:",
|
||||
"new": "Create a new list",
|
||||
"new": "New list",
|
||||
"importText": "Or import your lists and tasks from other services into Vikunja:",
|
||||
"import": "Import your data into Vikunja"
|
||||
}
|
||||
|
@ -157,7 +157,7 @@
|
|||
"searchSelect": "Click or press enter to select this list",
|
||||
"shared": "Shared Lists",
|
||||
"create": {
|
||||
"header": "Create a new list",
|
||||
"header": "New list",
|
||||
"titlePlaceholder": "The list's title goes here…",
|
||||
"addTitleRequired": "Please specify a title.",
|
||||
"createdSuccess": "The list was successfully created.",
|
||||
|
@ -315,7 +315,7 @@
|
|||
"namespaces": "Namespaces",
|
||||
"search": "Type to search for a namespace…",
|
||||
"create": {
|
||||
"title": "Create a new namespace",
|
||||
"title": "New namespace",
|
||||
"titleRequired": "Please specify a title.",
|
||||
"explanation": "A namespace is a collection of lists you can share and use to organize your lists with. In fact, every list belongs to a namepace.",
|
||||
"tooltip": "What's a namespace?",
|
||||
|
@ -383,7 +383,7 @@
|
|||
"reminderRange": "Reminder Date Range"
|
||||
},
|
||||
"create": {
|
||||
"title": "Create A Saved Filter",
|
||||
"title": "New Saved Filter",
|
||||
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
|
||||
"action": "Create new saved filter"
|
||||
},
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"lastViewed": "Последние просмотренные",
|
||||
"list": {
|
||||
"newText": "Ты можешь создать новый список для своих задач:",
|
||||
"new": "Создать новый список",
|
||||
"new": "New list",
|
||||
"importText": "Или импортировать списки и задачи из других сервисов в Vikunja:",
|
||||
"import": "Импорт данных в Vikunja"
|
||||
}
|
||||
|
@ -157,7 +157,7 @@
|
|||
"searchSelect": "Кликни или нажми Enter для выбора этого списка",
|
||||
"shared": "Общие списки",
|
||||
"create": {
|
||||
"header": "Создать новый список",
|
||||
"header": "New list",
|
||||
"titlePlaceholder": "Введи имя списка…",
|
||||
"addTitleRequired": "Укажи название.",
|
||||
"createdSuccess": "Список создан.",
|
||||
|
@ -315,7 +315,7 @@
|
|||
"namespaces": "Пространства имён",
|
||||
"search": "Введи запрос для поиска пространства имён…",
|
||||
"create": {
|
||||
"title": "Создать новое пространство имён",
|
||||
"title": "New namespace",
|
||||
"titleRequired": "Укажи название.",
|
||||
"explanation": "Коллекции списков для совместного использования и организации ваших списков. Фактически, каждый список принадлежит какому-нибудь пространству имён.",
|
||||
"tooltip": "Что такое пространство имён?",
|
||||
|
@ -383,7 +383,7 @@
|
|||
"reminderRange": "Диапазон даты напоминания"
|
||||
},
|
||||
"create": {
|
||||
"title": "Создать сохранённый фильтр",
|
||||
"title": "New Saved Filter",
|
||||
"description": "Сохраненный фильтр это виртуальный список, построенный из набора фильтров. При создании отображается в специальном пространстве имен.",
|
||||
"action": "Создать новый сохранённый фильтр"
|
||||
},
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"lastViewed": "Last viewed",
|
||||
"list": {
|
||||
"newText": "You can create a new list for your new tasks:",
|
||||
"new": "Create a new list",
|
||||
"new": "New list",
|
||||
"importText": "Or import your lists and tasks from other services into Vikunja:",
|
||||
"import": "Import your data into Vikunja"
|
||||
}
|
||||
|
@ -157,7 +157,7 @@
|
|||
"searchSelect": "Click or press enter to select this list",
|
||||
"shared": "Shared Lists",
|
||||
"create": {
|
||||
"header": "Create a new list",
|
||||
"header": "New list",
|
||||
"titlePlaceholder": "The list's title goes here…",
|
||||
"addTitleRequired": "Please specify a title.",
|
||||
"createdSuccess": "The list was successfully created.",
|
||||
|
@ -315,7 +315,7 @@
|
|||
"namespaces": "Namespaces",
|
||||
"search": "Type to search for a namespace…",
|
||||
"create": {
|
||||
"title": "Create a new namespace",
|
||||
"title": "New namespace",
|
||||
"titleRequired": "Please specify a title.",
|
||||
"explanation": "A namespace is a collection of lists you can share and use to organize your lists with. In fact, every list belongs to a namepace.",
|
||||
"tooltip": "What's a namespace?",
|
||||
|
@ -383,7 +383,7 @@
|
|||
"reminderRange": "Reminder Date Range"
|
||||
},
|
||||
"create": {
|
||||
"title": "Create A Saved Filter",
|
||||
"title": "New Saved Filter",
|
||||
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
|
||||
"action": "Create new saved filter"
|
||||
},
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"lastViewed": "Last viewed",
|
||||
"list": {
|
||||
"newText": "You can create a new list for your new tasks:",
|
||||
"new": "Create a new list",
|
||||
"new": "New list",
|
||||
"importText": "Or import your lists and tasks from other services into Vikunja:",
|
||||
"import": "Import your data into Vikunja"
|
||||
}
|
||||
|
@ -157,7 +157,7 @@
|
|||
"searchSelect": "Click or press enter to select this list",
|
||||
"shared": "Shared Lists",
|
||||
"create": {
|
||||
"header": "Create a new list",
|
||||
"header": "New list",
|
||||
"titlePlaceholder": "The list's title goes here…",
|
||||
"addTitleRequired": "Please specify a title.",
|
||||
"createdSuccess": "The list was successfully created.",
|
||||
|
@ -315,7 +315,7 @@
|
|||
"namespaces": "Namespaces",
|
||||
"search": "Type to search for a namespace…",
|
||||
"create": {
|
||||
"title": "Create a new namespace",
|
||||
"title": "New namespace",
|
||||
"titleRequired": "Please specify a title.",
|
||||
"explanation": "A namespace is a collection of lists you can share and use to organize your lists with. In fact, every list belongs to a namepace.",
|
||||
"tooltip": "What's a namespace?",
|
||||
|
@ -383,7 +383,7 @@
|
|||
"reminderRange": "Reminder Date Range"
|
||||
},
|
||||
"create": {
|
||||
"title": "Create A Saved Filter",
|
||||
"title": "New Saved Filter",
|
||||
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
|
||||
"action": "Create new saved filter"
|
||||
},
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"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:",
|
||||
"new": "Tạo một danh sách mới",
|
||||
"new": "New list",
|
||||
"importText": "Hoặc nhập danh sách và nhiệm vụ của bạn từ các dịch vụ khác vào Vikunja:",
|
||||
"import": "Nhập dữ liệu của bạn vào Vikunja"
|
||||
}
|
||||
|
@ -157,7 +157,7 @@
|
|||
"searchSelect": "Nhấp hoặc nhấn enter để chọn danh sách này",
|
||||
"shared": "Đang tham gia",
|
||||
"create": {
|
||||
"header": "Tạo một danh sách mới",
|
||||
"header": "New list",
|
||||
"titlePlaceholder": "Tên danh sách ở đây…",
|
||||
"addTitleRequired": "Hãy xác định một tên.",
|
||||
"createdSuccess": "Danh sách đã được tạo thành công.",
|
||||
|
@ -315,7 +315,7 @@
|
|||
"namespaces": "Góc làm việc",
|
||||
"search": "Gõ để tìm kiếm một góc làm việc…",
|
||||
"create": {
|
||||
"title": "Tạo một góc làm việc mới",
|
||||
"title": "New namespace",
|
||||
"titleRequired": "Hãy đặt một tiêu đề.",
|
||||
"explanation": "Góc làm việc là một tập hợp các danh sách mà bạn có thể chia sẻ và sử dụng để sắp xếp các danh sách của mình. Trên thực tế, mọi danh sách đều thuộc về một góc làm việc.",
|
||||
"tooltip": "Góc làm việc là gì?",
|
||||
|
@ -383,7 +383,7 @@
|
|||
"reminderRange": "Phạm vi Ngày nhắc nhở"
|
||||
},
|
||||
"create": {
|
||||
"title": "Tạo một Bộ lọc sẵn",
|
||||
"title": "New Saved Filter",
|
||||
"description": "Bộ lọc sẵn là một danh sách ảo được chọn từ một tập hợp các bộ lọc. Sau khi được tạo, nó sẽ xuất hiện trong một không gian làm việc đặc biệt.",
|
||||
"action": "Tạo thêm bộ lọc sẵn"
|
||||
},
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
import {createApp, configureCompat} from 'vue'
|
||||
|
||||
// default everything to Vue 3 behavior
|
||||
configureCompat({
|
||||
COMPONENT_V_MODEL: false,
|
||||
COMPONENT_ASYNC: false,
|
||||
RENDER_FUNCTION: false,
|
||||
WATCH_ARRAY: false, // TODO: check this again; this might lead to some problemes
|
||||
TRANSITION_GROUP_ROOT: false,
|
||||
MODE: 3,
|
||||
})
|
||||
|
||||
import App from './App.vue'
|
||||
|
@ -79,7 +76,6 @@ app.component('card', Card)
|
|||
// Mixins
|
||||
import {getNamespaceTitle} from './helpers/getNamespaceTitle'
|
||||
import {getListTitle} from './helpers/getListTitle'
|
||||
import {colorIsDark} from './helpers/color/colorIsDark'
|
||||
import {setTitle} from './helpers/setTitle'
|
||||
|
||||
app.mixin({
|
||||
|
@ -90,7 +86,6 @@ app.mixin({
|
|||
formatDateShort: formatDateShort,
|
||||
getNamespaceTitle,
|
||||
getListTitle,
|
||||
colorIsDark,
|
||||
setTitle,
|
||||
},
|
||||
})
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import {test, expect, fn} from 'vitest'
|
||||
import {getHistory, removeListFromHistory, saveListToHistory} from './listHistory'
|
||||
|
||||
test('return an empty history when none was saved', () => {
|
||||
Storage.prototype.getItem = jest.fn(() => null)
|
||||
Storage.prototype.getItem = fn(() => null)
|
||||
const h = getHistory()
|
||||
expect(h).toStrictEqual([])
|
||||
})
|
||||
|
||||
test('return a saved history', () => {
|
||||
const saved = [{id: 1}, {id: 2}]
|
||||
Storage.prototype.getItem = jest.fn(() => JSON.stringify(saved))
|
||||
Storage.prototype.getItem = fn(() => JSON.stringify(saved))
|
||||
|
||||
const h = getHistory()
|
||||
expect(h).toStrictEqual(saved)
|
||||
|
@ -16,8 +17,8 @@ test('return a saved history', () => {
|
|||
|
||||
test('store list in history', () => {
|
||||
let saved = {}
|
||||
Storage.prototype.getItem = jest.fn(() => null)
|
||||
Storage.prototype.setItem = jest.fn((key, lists) => {
|
||||
Storage.prototype.getItem = fn(() => null)
|
||||
Storage.prototype.setItem = fn((key, lists) => {
|
||||
saved = lists
|
||||
})
|
||||
|
||||
|
@ -27,8 +28,8 @@ test('store list in history', () => {
|
|||
|
||||
test('store only the last 5 lists in history', () => {
|
||||
let saved: string | null = null
|
||||
Storage.prototype.getItem = jest.fn(() => saved)
|
||||
Storage.prototype.setItem = jest.fn((key: string, lists: string) => {
|
||||
Storage.prototype.getItem = fn(() => saved)
|
||||
Storage.prototype.setItem = fn((key: string, lists: string) => {
|
||||
saved = lists
|
||||
})
|
||||
|
||||
|
@ -43,8 +44,8 @@ test('store only the last 5 lists in history', () => {
|
|||
|
||||
test('don\'t store the same list twice', () => {
|
||||
let saved: string | null = null
|
||||
Storage.prototype.getItem = jest.fn(() => saved)
|
||||
Storage.prototype.setItem = jest.fn((key: string, lists: string) => {
|
||||
Storage.prototype.getItem = fn(() => saved)
|
||||
Storage.prototype.setItem = fn((key: string, lists: string) => {
|
||||
saved = lists
|
||||
})
|
||||
|
||||
|
@ -55,8 +56,8 @@ test('don\'t store the same list twice', () => {
|
|||
|
||||
test('move a list to the beginning when storing it multiple times', () => {
|
||||
let saved: string | null = null
|
||||
Storage.prototype.getItem = jest.fn(() => saved)
|
||||
Storage.prototype.setItem = jest.fn((key: string, lists: string) => {
|
||||
Storage.prototype.getItem = fn(() => saved)
|
||||
Storage.prototype.setItem = fn((key: string, lists: string) => {
|
||||
saved = lists
|
||||
})
|
||||
|
||||
|
@ -68,11 +69,11 @@ test('move a list to the beginning when storing it multiple times', () => {
|
|||
|
||||
test('remove list from history', () => {
|
||||
let saved: string | null = '[{"id": 1}]'
|
||||
Storage.prototype.getItem = jest.fn(() => null)
|
||||
Storage.prototype.setItem = jest.fn((key: string, lists: string) => {
|
||||
Storage.prototype.getItem = fn(() => null)
|
||||
Storage.prototype.setItem = fn((key: string, lists: string) => {
|
||||
saved = lists
|
||||
})
|
||||
Storage.prototype.removeItem = jest.fn((key: string) => {
|
||||
Storage.prototype.removeItem = fn((key: string) => {
|
||||
saved = null
|
||||
})
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import {describe, it, expect} from 'vitest'
|
||||
|
||||
import {parseTaskText} from './parseTaskText'
|
||||
import {getDateFromText, getDateFromTextIn} from '../helpers/time/parseDate'
|
||||
import {calculateDayInterval} from '../helpers/time/calculateDayInterval'
|
||||
|
|
|
@ -2,73 +2,73 @@ import { createRouter, createWebHistory, RouteLocation } from 'vue-router'
|
|||
import {saveLastVisited} from '@/helpers/saveLastVisited'
|
||||
import {store} from '@/store'
|
||||
|
||||
import HomeComponent from '../views/Home'
|
||||
import NotFoundComponent from '../views/404'
|
||||
import About from '../views/About'
|
||||
import HomeComponent from '../views/Home.vue'
|
||||
import NotFoundComponent from '../views/404.vue'
|
||||
import About from '../views/About.vue'
|
||||
// User Handling
|
||||
import LoginComponent from '../views/user/Login'
|
||||
import RegisterComponent from '../views/user/Register'
|
||||
import OpenIdAuth from '../views/user/OpenIdAuth'
|
||||
import DataExportDownload from '../views/user/DataExportDownload'
|
||||
import LoginComponent from '../views/user/Login.vue'
|
||||
import RegisterComponent from '../views/user/Register.vue'
|
||||
import OpenIdAuth from '../views/user/OpenIdAuth.vue'
|
||||
import DataExportDownload from '../views/user/DataExportDownload.vue'
|
||||
// Tasks
|
||||
import ShowTasksInRangeComponent from '../views/tasks/ShowTasksInRange'
|
||||
import LinkShareAuthComponent from '../views/sharing/LinkSharingAuth'
|
||||
import TaskDetailViewModal from '../views/tasks/TaskDetailViewModal'
|
||||
import TaskDetailView from '../views/tasks/TaskDetailView'
|
||||
import ListNamespaces from '../views/namespaces/ListNamespaces'
|
||||
import ShowTasksInRangeComponent from '../views/tasks/ShowTasksInRange.vue'
|
||||
import LinkShareAuthComponent from '../views/sharing/LinkSharingAuth.vue'
|
||||
import TaskDetailViewModal from '../views/tasks/TaskDetailViewModal.vue'
|
||||
import TaskDetailView from '../views/tasks/TaskDetailView.vue'
|
||||
import ListNamespaces from '../views/namespaces/ListNamespaces.vue'
|
||||
// Team Handling
|
||||
import ListTeamsComponent from '../views/teams/ListTeams'
|
||||
import ListTeamsComponent from '../views/teams/ListTeams.vue'
|
||||
// Label Handling
|
||||
import ListLabelsComponent from '../views/labels/ListLabels'
|
||||
import NewLabelComponent from '../views/labels/NewLabel'
|
||||
import ListLabelsComponent from '../views/labels/ListLabels.vue'
|
||||
import NewLabelComponent from '../views/labels/NewLabel.vue'
|
||||
// Migration
|
||||
import MigrationComponent from '../views/migrator/Migrate'
|
||||
import MigrateServiceComponent from '../views/migrator/MigrateService'
|
||||
import MigrationComponent from '../views/migrator/Migrate.vue'
|
||||
import MigrateServiceComponent from '../views/migrator/MigrateService.vue'
|
||||
// List Views
|
||||
import ShowListComponent from '../views/list/ShowList'
|
||||
import Kanban from '../views/list/views/Kanban'
|
||||
import List from '../views/list/views/List'
|
||||
import Gantt from '../views/list/views/Gantt'
|
||||
import Table from '../views/list/views/Table'
|
||||
import ShowListComponent from '../views/list/ShowList.vue'
|
||||
import Kanban from '../views/list/views/Kanban.vue'
|
||||
import List from '../views/list/views/List.vue'
|
||||
import Gantt from '../views/list/views/Gantt.vue'
|
||||
import Table from '../views/list/views/Table.vue'
|
||||
// List Settings
|
||||
import ListSettingEdit from '../views/list/settings/edit'
|
||||
import ListSettingBackground from '../views/list/settings/background'
|
||||
import ListSettingDuplicate from '../views/list/settings/duplicate'
|
||||
import ListSettingShare from '../views/list/settings/share'
|
||||
import ListSettingDelete from '../views/list/settings/delete'
|
||||
import ListSettingArchive from '../views/list/settings/archive'
|
||||
import ListSettingEdit from '../views/list/settings/edit.vue'
|
||||
import ListSettingBackground from '../views/list/settings/background.vue'
|
||||
import ListSettingDuplicate from '../views/list/settings/duplicate.vue'
|
||||
import ListSettingShare from '../views/list/settings/share.vue'
|
||||
import ListSettingDelete from '../views/list/settings/delete.vue'
|
||||
import ListSettingArchive from '../views/list/settings/archive.vue'
|
||||
|
||||
// Namespace Settings
|
||||
import NamespaceSettingEdit from '../views/namespaces/settings/edit'
|
||||
import NamespaceSettingShare from '../views/namespaces/settings/share'
|
||||
import NamespaceSettingArchive from '../views/namespaces/settings/archive'
|
||||
import NamespaceSettingDelete from '../views/namespaces/settings/delete'
|
||||
import NamespaceSettingEdit from '../views/namespaces/settings/edit.vue'
|
||||
import NamespaceSettingShare from '../views/namespaces/settings/share.vue'
|
||||
import NamespaceSettingArchive from '../views/namespaces/settings/archive.vue'
|
||||
import NamespaceSettingDelete from '../views/namespaces/settings/delete.vue'
|
||||
|
||||
// Saved Filters
|
||||
import FilterNew from '@/views/filters/FilterNew'
|
||||
import FilterEdit from '@/views/filters/FilterEdit'
|
||||
import FilterDelete from '@/views/filters/FilterDelete'
|
||||
import FilterNew from '@/views/filters/FilterNew.vue'
|
||||
import FilterEdit from '@/views/filters/FilterEdit.vue'
|
||||
import FilterDelete from '@/views/filters/FilterDelete.vue'
|
||||
|
||||
const PasswordResetComponent = () => import('../views/user/PasswordReset')
|
||||
const GetPasswordResetComponent = () => import('../views/user/RequestPasswordReset')
|
||||
const UserSettingsComponent = () => import('../views/user/Settings')
|
||||
const UserSettingsAvatarComponent = () => import('../views/user/settings/Avatar')
|
||||
const UserSettingsCaldavComponent = () => import('../views/user/settings/Caldav')
|
||||
const UserSettingsDataExportComponent = () => import('../views/user/settings/DataExport')
|
||||
const UserSettingsDeletionComponent = () => import('../views/user/settings/Deletion')
|
||||
const UserSettingsEmailUpdateComponent = () => import('../views/user/settings/EmailUpdate')
|
||||
const UserSettingsGeneralComponent = () => import('../views/user/settings/General')
|
||||
const UserSettingsPasswordUpdateComponent = () => import('../views/user/settings/PasswordUpdate')
|
||||
const UserSettingsTOTPComponent = () => import('../views/user/settings/TOTP')
|
||||
const PasswordResetComponent = () => import('../views/user/PasswordReset.vue')
|
||||
const GetPasswordResetComponent = () => import('../views/user/RequestPasswordReset.vue')
|
||||
const UserSettingsComponent = () => import('../views/user/Settings.vue')
|
||||
const UserSettingsAvatarComponent = () => import('../views/user/settings/Avatar.vue')
|
||||
const UserSettingsCaldavComponent = () => import('../views/user/settings/Caldav.vue')
|
||||
const UserSettingsDataExportComponent = () => import('../views/user/settings/DataExport.vue')
|
||||
const UserSettingsDeletionComponent = () => import('../views/user/settings/Deletion.vue')
|
||||
const UserSettingsEmailUpdateComponent = () => import('../views/user/settings/EmailUpdate.vue')
|
||||
const UserSettingsGeneralComponent = () => import('../views/user/settings/General.vue')
|
||||
const UserSettingsPasswordUpdateComponent = () => import('../views/user/settings/PasswordUpdate.vue')
|
||||
const UserSettingsTOTPComponent = () => import('../views/user/settings/TOTP.vue')
|
||||
|
||||
// List Handling
|
||||
const NewListComponent = () => import('../views/list/NewList')
|
||||
const NewListComponent = () => import('../views/list/NewList.vue')
|
||||
|
||||
// Namespace Handling
|
||||
const NewNamespaceComponent = () => import('../views/namespaces/NewNamespace')
|
||||
const NewNamespaceComponent = () => import('../views/namespaces/NewNamespace.vue')
|
||||
|
||||
const EditTeamComponent = () => import('../views/teams/EditTeam')
|
||||
const NewTeamComponent = () => import('../views/teams/NewTeam')
|
||||
const EditTeamComponent = () => import('../views/teams/EditTeam.vue')
|
||||
const NewTeamComponent = () => import('../views/teams/NewTeam.vue')
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
LOADING,
|
||||
LOADING_MODULE,
|
||||
MENU_ACTIVE,
|
||||
ONLINE, QUICK_ACTIONS_ACTIVE,
|
||||
QUICK_ACTIONS_ACTIVE,
|
||||
} from './mutation-types'
|
||||
import config from './modules/config'
|
||||
import auth from './modules/auth'
|
||||
|
@ -36,7 +36,6 @@ export const store = createStore({
|
|||
state: {
|
||||
loading: false,
|
||||
loadingModule: null,
|
||||
online: true,
|
||||
// This is used to highlight the current list in menu for all list related views
|
||||
currentList: {id: 0},
|
||||
background: '',
|
||||
|
@ -53,12 +52,6 @@ export const store = createStore({
|
|||
[LOADING_MODULE](state, module) {
|
||||
state.loadingModule = module
|
||||
},
|
||||
[ONLINE](state, online) {
|
||||
if (import.meta.env.VITE_IS_ONLINE) {
|
||||
console.log('Setting fake online state', import.meta.env.VITE_IS_ONLINE)
|
||||
}
|
||||
state.online = !!import.meta.env.VITE_IS_ONLINE || online
|
||||
},
|
||||
[CURRENT_LIST](state, currentList) {
|
||||
// Server updates don't return the right. Therefore the right is reset after updating the list which is
|
||||
// confusing because all the buttons will disappear in that case. To prevent this, we're keeping the right
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
import {HTTPFactory} from '@/http-common'
|
||||
import {getCurrentLanguage, saveLanguage} from '@/i18n'
|
||||
import {LOADING} from '../mutation-types'
|
||||
import UserModel from '../../models/user'
|
||||
import UserModel from '@/models/user'
|
||||
import UserSettingsService from '@/services/userSettings'
|
||||
import {getToken, refreshToken, removeToken, saveToken} from '@/helpers/auth'
|
||||
import {setLoading} from '@/store/helper'
|
||||
import {i18n} from '@/i18n'
|
||||
import {success} from '@/message'
|
||||
|
||||
const AUTH_TYPES = {
|
||||
'UNKNOWN': 0,
|
||||
|
@ -101,7 +106,7 @@ export default {
|
|||
|
||||
// Tell others the user is autheticated
|
||||
ctx.dispatch('checkAuth')
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
if (
|
||||
e.response &&
|
||||
e.response.data.code === 1017 &&
|
||||
|
@ -124,7 +129,7 @@ export default {
|
|||
try {
|
||||
await HTTP.post('register', credentials)
|
||||
return ctx.dispatch('login', credentials)
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
if (e.response?.data?.message) {
|
||||
throw e.response.data
|
||||
}
|
||||
|
@ -200,7 +205,7 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
async refreshUserInfo(ctx) {
|
||||
async refreshUserInfo({state, commit, dispatch}) {
|
||||
const jwt = getToken()
|
||||
if (!jwt) {
|
||||
return
|
||||
|
@ -208,22 +213,53 @@ export default {
|
|||
|
||||
const HTTP = HTTPFactory()
|
||||
try {
|
||||
|
||||
const response = await HTTP.get('user', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
})
|
||||
const info = new UserModel(response.data)
|
||||
info.type = ctx.state.info.type
|
||||
info.email = ctx.state.info.email
|
||||
info.exp = ctx.state.info.exp
|
||||
info.type = state.info.type
|
||||
info.email = state.info.email
|
||||
info.exp = state.info.exp
|
||||
|
||||
commit('info', info)
|
||||
commit('lastUserRefresh')
|
||||
|
||||
if (typeof info.settings.language !== 'undefined') {
|
||||
// save current language
|
||||
await dispatch('saveUserSettings', {
|
||||
settings: {
|
||||
...state.settings,
|
||||
language: getCurrentLanguage(),
|
||||
},
|
||||
showMessage: false,
|
||||
})
|
||||
}
|
||||
|
||||
ctx.commit('info', info)
|
||||
ctx.commit('lastUserRefresh')
|
||||
return info
|
||||
} catch(e) {
|
||||
throw new Error('Error while refreshing user info:', { cause: e })
|
||||
} catch (e) {
|
||||
throw new Error('Error while refreshing user info:', {cause: e})
|
||||
}
|
||||
},
|
||||
|
||||
async saveUserSettings(ctx, payload) {
|
||||
const {settings} = payload
|
||||
const showMessage = payload.showMessage ?? true
|
||||
const userSettingsService = new UserSettingsService()
|
||||
|
||||
const cancel = setLoading(ctx, 'general-settings')
|
||||
try {
|
||||
saveLanguage(settings.language)
|
||||
await userSettingsService.update(settings)
|
||||
ctx.commit('setUserSettings', {...settings})
|
||||
if (showMessage) {
|
||||
success({message: i18n.global.t('user.settings.general.savedSuccess')})
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error('Error while saving user settings:', {cause: e})
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -240,7 +276,7 @@ export default {
|
|||
try {
|
||||
await refreshToken(!ctx.state.isLinkShareAuth)
|
||||
ctx.dispatch('checkAuth')
|
||||
} catch(e) {
|
||||
} 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) {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
export const LOADING = 'loading'
|
||||
export const LOADING_MODULE = 'loadingModule'
|
||||
export const ONLINE = 'online'
|
||||
export const CURRENT_LIST = 'currentList'
|
||||
export const HAS_TASKS = 'hasTasks'
|
||||
export const MENU_ACTIVE = 'menuActive'
|
||||
|
|
|
@ -3,4 +3,3 @@
|
|||
@import "list";
|
||||
@import "task";
|
||||
@import "tasks";
|
||||
@import "namespaces";
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
// FIXME: used in navigation.vue and in ListNamespaces.vue
|
||||
.namespaces-list.loader-container.is-loading {
|
||||
min-height: calc(100vh - #{$navbar-height + 1.5rem + 1rem + 1.5rem});
|
||||
}
|
|
@ -24,3 +24,7 @@
|
|||
max-width: $widescreen;
|
||||
}
|
||||
|
||||
.content blockquote {
|
||||
background-color: var(--grey-200);
|
||||
border-left: .25rem solid var(--grey-300);
|
||||
}
|
||||
|
|
2
src/types/shims-vue.d.ts
vendored
2
src/types/shims-vue.d.ts
vendored
|
@ -1,4 +1,6 @@
|
|||
declare module 'vue' {
|
||||
import { CompatVue } from '@vue/runtime-dom'
|
||||
const Vue: CompatVue
|
||||
export default Vue
|
||||
export * from '@vue/runtime-dom'
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
</div>
|
||||
<footer class="modal-card-foot is-flex is-justify-content-flex-end">
|
||||
<x-button
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
@click.prevent.stop="$router.back()"
|
||||
>
|
||||
{{ $t('misc.close') }}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="content has-text-centered">
|
||||
<h2 v-if="userInfo">
|
||||
{{ $t(`home.welcome${welcome}`, {username: userInfo.name !== '' ? userInfo.name : userInfo.username}) }}!
|
||||
{{ $t(welcome, {username: userInfo.name !== '' ? userInfo.name : userInfo.username}) }}!
|
||||
</h2>
|
||||
<message variant="danger" v-if="deletionScheduledAt !== null" class="mb-4">
|
||||
{{
|
||||
|
@ -57,7 +57,6 @@
|
|||
<script lang="ts" setup>
|
||||
import {ref, computed} from 'vue'
|
||||
import {useStore} from 'vuex'
|
||||
import {useNow} from '@vueuse/core'
|
||||
|
||||
import Message from '@/components/misc/message.vue'
|
||||
import ShowTasks from '@/views/tasks/ShowTasks.vue'
|
||||
|
@ -67,36 +66,15 @@ 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'
|
||||
|
||||
const now = useNow()
|
||||
const welcome = computed(() => {
|
||||
const hours = new Date(now.value).getHours()
|
||||
|
||||
if (hours < 5) {
|
||||
return 'Night'
|
||||
}
|
||||
|
||||
if (hours < 11) {
|
||||
return 'Morning'
|
||||
}
|
||||
|
||||
if (hours < 18) {
|
||||
return 'Day'
|
||||
}
|
||||
|
||||
if (hours < 23) {
|
||||
return 'Evening'
|
||||
}
|
||||
|
||||
return 'Night'
|
||||
})
|
||||
const welcome = useDateTimeSalutation()
|
||||
|
||||
const store = useStore()
|
||||
const listHistory = computed(() => {
|
||||
const history = getHistory()
|
||||
return history.map(l => {
|
||||
return store.getters['lists/getListById'](l.id)
|
||||
}).filter(l => l !== null)
|
||||
return getHistory()
|
||||
.map(l => store.getters['lists/getListById'](l.id))
|
||||
.filter(l => l !== null)
|
||||
})
|
||||
|
||||
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
primary-icon=""
|
||||
:primary-label="$t('misc.save')"
|
||||
@primary="saveSavedFilter"
|
||||
:tertary="$t('misc.delete')"
|
||||
@tertary="$router.push({ name: 'filter.settings.delete', params: { id: $route.params.listId } })"
|
||||
:tertiary="$t('misc.delete')"
|
||||
@tertiary="$router.push({ name: 'filter.settings.delete', params: { id: $route.params.listId } })"
|
||||
>
|
||||
<form @submit.prevent="saveSavedFilter()">
|
||||
<div class="field">
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
class="list-background-setting"
|
||||
:wide="true"
|
||||
v-if="uploadBackgroundEnabled || unsplashBackgroundEnabled"
|
||||
:tertary="hasBackground ? $t('list.background.remove') : ''"
|
||||
@tertary="removeBackground()"
|
||||
:tertiary="hasBackground ? $t('list.background.remove') : ''"
|
||||
@tertiary="removeBackground()"
|
||||
>
|
||||
<div class="mb-4" v-if="uploadBackgroundEnabled">
|
||||
<input
|
||||
|
@ -20,7 +20,7 @@
|
|||
<x-button
|
||||
:loading="backgroundUploadService.loading"
|
||||
@click="$refs.backgroundUploadInput.click()"
|
||||
type="primary"
|
||||
variant="primary"
|
||||
>
|
||||
{{ $t('list.background.upload') }}
|
||||
</x-button>
|
||||
|
@ -54,7 +54,7 @@
|
|||
@click="() => searchBackgrounds(currentPage + 1)"
|
||||
class="is-load-more-button mt-4"
|
||||
:shadow="false"
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
v-if="backgroundSearchResult.length > 0"
|
||||
>
|
||||
{{ backgroundService.loading ? $t('misc.loading') : $t('list.background.loadMore') }}
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
primary-icon=""
|
||||
:primary-label="$t('misc.save')"
|
||||
@primary="save"
|
||||
:tertary="$t('misc.delete')"
|
||||
@tertary="$router.push({ name: 'list.list.settings.delete', params: { id: $route.params.listId } })"
|
||||
:tertiary="$t('misc.delete')"
|
||||
@tertiary="$router.push({ name: 'list.list.settings.delete', params: { id: $route.params.listId } })"
|
||||
>
|
||||
<div class="field">
|
||||
<label class="label" for="title">{{ $t('list.title') }}</label>
|
||||
|
|
|
@ -79,6 +79,7 @@
|
|||
:disabled="bucket.limit < 0"
|
||||
:icon="['far', 'save']"
|
||||
:shadow="false"
|
||||
v-cy="'setBucketLimit'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -165,7 +166,7 @@
|
|||
:shadow="false"
|
||||
v-if="!showNewTaskInput[bucket.id]"
|
||||
icon="plus"
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
>
|
||||
{{
|
||||
bucket.tasks.length === 0 ? $t('list.kanban.addTask') : $t('list.kanban.addAnotherTask')
|
||||
|
@ -195,7 +196,7 @@
|
|||
:shadow="false"
|
||||
class="is-transparent is-fullwidth has-text-centered"
|
||||
v-else
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
icon="plus"
|
||||
>
|
||||
{{ $t('list.kanban.addBucket') }}
|
||||
|
@ -701,7 +702,7 @@ $filter-container-height: '1rem - #{$switch-view-height}';
|
|||
height: $bucket-header-height;
|
||||
|
||||
.limit {
|
||||
padding-left: .5rem;
|
||||
padding: 0 .5rem;
|
||||
font-weight: bold;
|
||||
|
||||
&.is-max {
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
<x-button
|
||||
@click="showTaskSearch = !showTaskSearch"
|
||||
icon="search"
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
v-if="!showTaskSearch"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<x-button
|
||||
@click.prevent.stop="toggle()"
|
||||
icon="th"
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
>
|
||||
{{ $t('list.table.columns') }}
|
||||
</x-button>
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
</p>
|
||||
<div class="buttons">
|
||||
<x-button @click="migrate">{{ $t('migrate.confirm') }}</x-button>
|
||||
<x-button :to="{name: 'home'}" type="tertary" class="has-text-danger">{{ $t('misc.cancel') }}</x-button>
|
||||
<x-button :to="{name: 'home'}" variant="tertiary" class="has-text-danger">{{ $t('misc.cancel') }}</x-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
|
|
|
@ -24,17 +24,17 @@ export const MIGRATORS: IMigratorRecord = {
|
|||
todoist: {
|
||||
id: 'todoist',
|
||||
name: 'Todoist',
|
||||
icon: todoistIcon,
|
||||
icon: todoistIcon as string,
|
||||
},
|
||||
trello: {
|
||||
id: 'trello',
|
||||
name: 'Trello',
|
||||
icon: trelloIcon,
|
||||
icon: trelloIcon as string,
|
||||
},
|
||||
'microsoft-todo': {
|
||||
id: 'microsoft-todo',
|
||||
name: 'Microsoft Todo',
|
||||
icon: microsoftTodoIcon,
|
||||
icon: microsoftTodoIcon as string,
|
||||
},
|
||||
'vikunja-file': {
|
||||
id: 'vikunja-file',
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
<template>
|
||||
<div class="content namespaces-list loader-container" :class="{'is-loading': loading}">
|
||||
<x-button :to="{name: 'namespace.create'}" class="new-namespace" icon="plus">
|
||||
{{ $t('namespace.create.title') }}
|
||||
</x-button>
|
||||
<x-button :to="{name: 'filters.create'}" class="new-namespace" icon="filter">
|
||||
{{ $t('filters.create.title') }}
|
||||
</x-button>
|
||||
|
||||
<fancycheckbox class="show-archived-check" v-model="showArchived" @change="saveShowArchivedState">
|
||||
<div class="content loader-container" :class="{'is-loading': loading}" v-cy="'namespaces-list'">
|
||||
<header class="namespace-header">
|
||||
<fancycheckbox v-model="showArchived" @change="saveShowArchivedState" v-cy="'show-archived-check'">
|
||||
{{ $t('namespace.showArchived') }}
|
||||
</fancycheckbox>
|
||||
|
||||
<div class="action-buttons">
|
||||
<x-button :to="{name: 'filters.create'}" icon="filter">
|
||||
{{ $t('filters.create.title') }}
|
||||
</x-button>
|
||||
<x-button :to="{name: 'namespace.create'}" icon="plus" v-cy="'new-namespace'">
|
||||
{{ $t('namespace.create.title') }}
|
||||
</x-button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<p class="has-text-centered has-text-grey mt-4 is-italic" v-if="namespaces.length === 0">
|
||||
{{ $t('namespace.noneAvailable') }}
|
||||
<router-link :to="{name: 'namespace.create'}">
|
||||
|
@ -22,7 +26,7 @@
|
|||
<x-button
|
||||
:to="{name: 'list.create', params: {id: n.id}}"
|
||||
class="is-pulled-right"
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
v-if="n.id > 0 && n.lists.length > 0"
|
||||
icon="plus"
|
||||
>
|
||||
|
@ -31,19 +35,19 @@
|
|||
<x-button
|
||||
:to="{name: 'namespace.settings.archive', params: {id: n.id}}"
|
||||
class="is-pulled-right mr-4"
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
v-if="n.isArchived"
|
||||
icon="archive"
|
||||
>
|
||||
{{ $t('namespace.unarchive') }}
|
||||
</x-button>
|
||||
|
||||
<h1>
|
||||
<span>{{ getNamespaceTitle(n) }}</span>
|
||||
<h2 class="namespace-title">
|
||||
<span v-cy="'namespace-title'">{{ getNamespaceTitle(n) }}</span>
|
||||
<span class="is-archived" v-if="n.isArchived">
|
||||
{{ $t('namespace.archived') }}
|
||||
</span>
|
||||
</h1>
|
||||
</h2>
|
||||
|
||||
<p class="has-text-centered has-text-grey mt-4 is-italic" v-if="n.lists.length === 0">
|
||||
{{ $t('namespace.noLists') }}
|
||||
|
@ -103,33 +107,41 @@ export default {
|
|||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.namespaces-list {
|
||||
.button.new-namespace {
|
||||
float: right;
|
||||
margin-left: 1rem;
|
||||
.namespace-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
|
||||
@media screen and (max-width: $mobile) {
|
||||
float: none;
|
||||
@media screen and (max-width: $tablet) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
|
||||
.show-archived-check {
|
||||
margin-bottom: 1rem;
|
||||
.namespace {
|
||||
& + & {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.namespace {
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
.namespace-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.is-archived {
|
||||
.is-archived {
|
||||
font-size: 0.75rem;
|
||||
border: 1px solid var(--grey-500);
|
||||
color: $grey !important;
|
||||
|
@ -138,12 +150,10 @@ export default {
|
|||
font-family: $vikunja-font;
|
||||
background: var(--white-translucent);
|
||||
margin-left: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.lists {
|
||||
.lists {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -4,8 +4,8 @@
|
|||
primary-icon=""
|
||||
:primary-label="$t('misc.save')"
|
||||
@primary="save"
|
||||
:tertary="$t('misc.delete')"
|
||||
@tertary="$router.push({ name: 'namespace.settings.delete', params: { id: $route.params.id } })"
|
||||
:tertiary="$t('misc.delete')"
|
||||
@tertiary="$router.push({ name: 'namespace.settings.delete', params: { id: $route.params.id } })"
|
||||
>
|
||||
<form @submit.prevent="save()">
|
||||
<div class="field">
|
||||
|
|
|
@ -32,9 +32,9 @@
|
|||
/>
|
||||
</h3>
|
||||
<div v-if="!showAll" class="mb-4">
|
||||
<x-button type="secondary" @click="showTodaysTasks()" class="mr-2">{{ $t('task.show.today') }}</x-button>
|
||||
<x-button type="secondary" @click="setDatesToNextWeek()" class="mr-2">{{ $t('task.show.nextWeek') }}</x-button>
|
||||
<x-button type="secondary" @click="setDatesToNextMonth()">{{ $t('task.show.nextMonth') }}</x-button>
|
||||
<x-button variant="secondary" @click="showTodaysTasks()" class="mr-2">{{ $t('task.show.today') }}</x-button>
|
||||
<x-button variant="secondary" @click="setDatesToNextWeek()" class="mr-2">{{ $t('task.show.nextWeek') }}</x-button>
|
||||
<x-button variant="secondary" @click="setDatesToNextMonth()">{{ $t('task.show.nextMonth') }}</x-button>
|
||||
</div>
|
||||
<template v-if="!loading && (!tasks || tasks.length === 0) && showNothingToDo">
|
||||
<h3 class="nothing">{{ $t('task.show.noTasks') }}</h3>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import ShowTasks from './ShowTasks'
|
||||
import ShowTasks from './ShowTasks.vue'
|
||||
|
||||
function getNextWeekDate() {
|
||||
return new Date((new Date()).getTime() + 7 * 24 * 60 * 60 * 1000)
|
||||
|
|
|
@ -258,7 +258,7 @@
|
|||
@click="toggleTaskDone()"
|
||||
class="is-outlined has-no-border"
|
||||
icon="check-double"
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
>
|
||||
{{ task.done ? $t('task.detail.undone') : $t('task.detail.done') }}
|
||||
</x-button>
|
||||
|
@ -270,7 +270,7 @@
|
|||
/>
|
||||
<x-button
|
||||
@click="setFieldActive('assignees')"
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
v-shortcut="'a'"
|
||||
v-cy="'taskDetail.assign'"
|
||||
>
|
||||
|
@ -279,7 +279,7 @@
|
|||
</x-button>
|
||||
<x-button
|
||||
@click="setFieldActive('labels')"
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
icon="tags"
|
||||
v-shortcut="'l'"
|
||||
>
|
||||
|
@ -287,14 +287,14 @@
|
|||
</x-button>
|
||||
<x-button
|
||||
@click="setFieldActive('priority')"
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
icon="exclamation"
|
||||
>
|
||||
{{ $t('task.detail.actions.priority') }}
|
||||
</x-button>
|
||||
<x-button
|
||||
@click="setFieldActive('dueDate')"
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
icon="calendar"
|
||||
v-shortcut="'d'"
|
||||
>
|
||||
|
@ -302,42 +302,42 @@
|
|||
</x-button>
|
||||
<x-button
|
||||
@click="setFieldActive('startDate')"
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
icon="play"
|
||||
>
|
||||
{{ $t('task.detail.actions.startDate') }}
|
||||
</x-button>
|
||||
<x-button
|
||||
@click="setFieldActive('endDate')"
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
icon="stop"
|
||||
>
|
||||
{{ $t('task.detail.actions.endDate') }}
|
||||
</x-button>
|
||||
<x-button
|
||||
@click="setFieldActive('reminders')"
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
:icon="['far', 'clock']"
|
||||
>
|
||||
{{ $t('task.detail.actions.reminders') }}
|
||||
</x-button>
|
||||
<x-button
|
||||
@click="setFieldActive('repeatAfter')"
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
icon="history"
|
||||
>
|
||||
{{ $t('task.detail.actions.repeatAfter') }}
|
||||
</x-button>
|
||||
<x-button
|
||||
@click="setFieldActive('percentDone')"
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
icon="percent"
|
||||
>
|
||||
{{ $t('task.detail.actions.percentDone') }}
|
||||
</x-button>
|
||||
<x-button
|
||||
@click="setFieldActive('attachments')"
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
icon="paperclip"
|
||||
v-shortcut="'f'"
|
||||
>
|
||||
|
@ -345,7 +345,7 @@
|
|||
</x-button>
|
||||
<x-button
|
||||
@click="setFieldActive('relatedTasks')"
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
icon="sitemap"
|
||||
v-shortcut="'r'"
|
||||
>
|
||||
|
@ -353,21 +353,21 @@
|
|||
</x-button>
|
||||
<x-button
|
||||
@click="setFieldActive('moveList')"
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
icon="list"
|
||||
>
|
||||
{{ $t('task.detail.actions.moveList') }}
|
||||
</x-button>
|
||||
<x-button
|
||||
@click="setFieldActive('color')"
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
icon="fill-drip"
|
||||
>
|
||||
{{ $t('task.detail.actions.color') }}
|
||||
</x-button>
|
||||
<x-button
|
||||
@click="toggleFavorite"
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
:icon="task.isFavorite ? 'star' : ['far', 'star']"
|
||||
>
|
||||
{{
|
||||
|
|
|
@ -85,7 +85,7 @@
|
|||
@click="redirectToProvider(p)"
|
||||
v-for="(p, k) in openidConnect.providers"
|
||||
:key="k"
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
class="is-fullwidth mt-2"
|
||||
>
|
||||
{{ $t('user.auth.loginWith', {provider: p.name}) }}
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
>
|
||||
{{ $t('user.auth.resetPasswordAction') }}
|
||||
</x-button>
|
||||
<x-button :to="{ name: 'user.login' }" type="secondary">
|
||||
<x-button :to="{ name: 'user.login' }" variant="secondary">
|
||||
{{ $t('user.auth.login') }}
|
||||
</x-button>
|
||||
</div>
|
||||
|
|
|
@ -48,6 +48,7 @@
|
|||
<x-button
|
||||
:loading="avatarService.loading || loading"
|
||||
@click="uploadAvatar"
|
||||
v-cy="'uploadAvatar'"
|
||||
>
|
||||
{{ $t('user.settings.avatar.uploadAvatar') }}
|
||||
</x-button>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<card :title="$t('user.settings.general.title')" class="general-settings" :loading="userSettingsService.loading">
|
||||
<card :title="$t('user.settings.general.title')" class="general-settings" :loading="loading">
|
||||
<div class="field">
|
||||
<label class="label" :for="`newName${id}`">{{ $t('user.settings.general.name') }}</label>
|
||||
<div class="control">
|
||||
|
@ -67,8 +67,8 @@
|
|||
{{ $t('user.settings.general.language') }}
|
||||
</span>
|
||||
<div class="select ml-2">
|
||||
<select v-model="language">
|
||||
<option :value="lang.code" v-for="lang in availableLanguages" :key="lang.code">{{
|
||||
<select v-model="settings.language">
|
||||
<option :value="lang.code" v-for="lang in availableLanguageOptions" :key="lang.code">{{
|
||||
lang.title
|
||||
}}
|
||||
</option>
|
||||
|
@ -107,9 +107,10 @@
|
|||
</div>
|
||||
|
||||
<x-button
|
||||
:loading="userSettingsService.loading"
|
||||
:loading="loading"
|
||||
@click="updateSettings()"
|
||||
class="is-fullwidth mt-4"
|
||||
v-cy="'saveGeneralSettings'"
|
||||
>
|
||||
{{ $t('misc.save') }}
|
||||
</x-button>
|
||||
|
@ -120,14 +121,12 @@
|
|||
import {computed, watch} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
import {playSoundWhenDoneKey} from '@/helpers/playPop'
|
||||
import {availableLanguages, saveLanguage, getCurrentLanguage} from '@/i18n'
|
||||
import {playSoundWhenDoneKey, playPop} from '@/helpers/playPop'
|
||||
import {availableLanguages} from '@/i18n'
|
||||
import {getQuickAddMagicMode, setQuickAddMagicMode} from '@/helpers/quickAddMagicMode'
|
||||
import UserSettingsService from '@/services/userSettings'
|
||||
import {PrefixMode} from '@/modules/parseTaskText'
|
||||
import ListSearch from '@/components/tasks/partials/listSearch'
|
||||
import {createRandomID} from '@/helpers/randomId'
|
||||
import {playPop} from '@/helpers/playPop'
|
||||
import {useColorScheme} from '@/composables/useColorScheme'
|
||||
import {success} from '@/message'
|
||||
|
||||
|
@ -165,23 +164,19 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
playSoundWhenDone: getPlaySoundWhenDoneSetting(),
|
||||
language: getCurrentLanguage(),
|
||||
quickAddMagicMode: getQuickAddMagicMode(),
|
||||
quickAddMagicPrefixes: PrefixMode,
|
||||
userSettingsService: new UserSettingsService(),
|
||||
settings: {...this.$store.state.auth.settings},
|
||||
id: createRandomID(),
|
||||
availableLanguageOptions: Object.entries(availableLanguages)
|
||||
.map(l => ({code: l[0], title: l[1]}))
|
||||
.sort((a, b) => a.title.localeCompare(b.title)),
|
||||
}
|
||||
},
|
||||
components: {
|
||||
ListSearch,
|
||||
},
|
||||
computed: {
|
||||
availableLanguages() {
|
||||
return Object.entries(availableLanguages)
|
||||
.map(l => ({code: l[0], title: l[1]}))
|
||||
.sort((a, b) => a.title.localeCompare(b.title))
|
||||
},
|
||||
defaultList: {
|
||||
get() {
|
||||
return this.$store.getters['lists/getListById'](this.settings.defaultListId)
|
||||
|
@ -190,6 +185,9 @@ export default {
|
|||
this.settings.defaultListId = l ? l.id : DEFAULT_LIST_ID
|
||||
},
|
||||
},
|
||||
loading() {
|
||||
return this.$store.state.loading && this.$store.state.loadingModule === 'general-settings'
|
||||
},
|
||||
},
|
||||
|
||||
setup() {
|
||||
|
@ -211,16 +209,11 @@ export default {
|
|||
methods: {
|
||||
async updateSettings() {
|
||||
localStorage.setItem(playSoundWhenDoneKey, this.playSoundWhenDone)
|
||||
saveLanguage(this.language)
|
||||
setQuickAddMagicMode(this.quickAddMagicMode)
|
||||
|
||||
const settings = {
|
||||
...this.settings,
|
||||
}
|
||||
|
||||
await this.userSettingsService.update(settings)
|
||||
this.$store.commit('auth/setUserSettings', settings)
|
||||
this.$message.success({message: this.$t('user.settings.general.savedSuccess')})
|
||||
await this.$store.dispatch('auth/saveUserSettings', {
|
||||
settings: {...this.settings},
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
<x-button @click="totpDisable" class="is-danger">
|
||||
{{ $t('user.settings.totp.disable') }}
|
||||
</x-button>
|
||||
<x-button @click="totpDisableForm = false" type="tertary" class="ml-2">
|
||||
<x-button @click="totpDisableForm = false" variant="tertiary" class="ml-2">
|
||||
{{ $t('misc.cancel') }}
|
||||
</x-button>
|
||||
</div>
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
"baseUrl": ".",
|
||||
"isolatedModules": true,
|
||||
"types": [
|
||||
"jest",
|
||||
"vite/client"
|
||||
],
|
||||
"paths": {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/// <reference types="vitest" />
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import legacyFn from '@vitejs/plugin-legacy'
|
||||
|
@ -27,6 +28,10 @@ if (isModernBuild) {
|
|||
}
|
||||
|
||||
export default defineConfig({
|
||||
// https://vitest.dev/config/
|
||||
test: {
|
||||
environment: 'happy-dom',
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
Loading…
Reference in a new issue