Merge remote-tracking branch 'upstream/main'

This commit is contained in:
Adrian Simmons 2021-12-01 09:35:09 +00:00
commit 8e4095e1bd
47 changed files with 2268 additions and 1158 deletions

View File

@ -99,7 +99,7 @@ steps:
- dependencies
- name: test-frontend
image: cypress/browsers:node14.17.0-chrome91-ff89
image: cypress/browsers:node16.5.0-chrome94-ff93
pull: true
environment:
CYPRESS_API_URL: http://api:3456/api/v1

View File

@ -8,7 +8,7 @@ const testAndAssertFailed = fixture => {
cy.wait(5000) // It can take waaaayy too long to log the user in
cy.url().should('include', '/')
cy.get('div.notification.is-danger').contains('Wrong username or password.')
cy.get('div.message.danger').contains('Wrong username or password.')
}
context('Login', () => {

View File

@ -32,7 +32,7 @@ context('Registration', () => {
cy.get('h2').should('contain', `Hi ${fixture.username}!`)
})
it('Should fail', () => {
it.only('Should fail', () => {
const fixture = {
username: 'test',
password: '123456',
@ -45,6 +45,6 @@ context('Registration', () => {
cy.get('#password').type(fixture.password)
cy.get('#passwordValidation').type(fixture.password)
cy.get('#register-submit').click()
cy.get('div.notification.is-danger').contains('A user with this username already exists.')
cy.get('div.message.danger').contains('A user with this username already exists.')
})
})

View File

@ -21,13 +21,13 @@
"@kyvg/vue3-notification": "2.3.4",
"@sentry/tracing": "6.15.0",
"@sentry/vue": "6.15.0",
"@vue/compat": "3.2.22",
"@vueuse/core": "7.0.3",
"@vue/compat": "3.2.23",
"@vueuse/core": "7.1.2",
"bulma-css-variables": "0.9.33",
"camel-case": "4.1.2",
"codemirror": "5.64.0",
"copy-to-clipboard": "3.3.1",
"date-fns": "2.26.0",
"date-fns": "2.27.0",
"dompurify": "2.3.3",
"easymde": "2.15.0",
"flatpickr": "4.6.9",
@ -36,57 +36,58 @@
"is-touch-device": "1.0.1",
"lodash.clonedeep": "4.5.0",
"lodash.debounce": "4.0.8",
"marked": "4.0.4",
"marked": "4.0.5",
"register-service-worker": "1.7.2",
"snake-case": "3.0.4",
"ufo": "0.7.9",
"v-tooltip": "4.0.0-beta.2",
"vue": "3.2.22",
"vue": "3.2.23",
"vue-advanced-cropper": "2.7.0",
"vue-drag-resize": "2.0.3",
"vue-flatpickr-component": "9.0.5",
"vue-i18n": "9.2.0-beta.20",
"vue-i18n": "9.2.0-beta.22",
"vue-router": "4.0.12",
"vuedraggable": "4.1.0",
"vuex": "4.0.2",
"workbox-precaching": "6.4.1"
},
"devDependencies": {
"@4tw/cypress-drag-drop": "2.0.0",
"@4tw/cypress-drag-drop": "2.1.0",
"@fortawesome/fontawesome-svg-core": "1.2.36",
"@fortawesome/free-regular-svg-icons": "5.15.4",
"@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.4.0",
"@typescript-eslint/parser": "5.4.0",
"@typescript-eslint/eslint-plugin": "5.5.0",
"@typescript-eslint/parser": "5.5.0",
"@vitejs/plugin-legacy": "1.6.3",
"@vitejs/plugin-vue": "1.10.0",
"@vitejs/plugin-vue": "1.10.1",
"@vue/eslint-config-typescript": "9.1.0",
"autoprefixer": "10.4.0",
"axios": "0.24.0",
"browserslist": "4.18.1",
"cypress": "8.7.0",
"cypress-file-upload": "5.0.8",
"esbuild": "0.13.15",
"esbuild": "0.14.1",
"eslint": "8.3.0",
"eslint-plugin-vue": "8.1.1",
"express": "4.17.1",
"faker": "5.5.3",
"jest": "27.3.1",
"netlify-cli": "7.1.0",
"postcss": "8.3.11",
"jest": "27.4.2",
"netlify-cli": "8.0.6",
"postcss": "8.4.4",
"postcss-preset-env": "7.0.1",
"rollup": "2.60.1",
"rollup": "2.60.2",
"rollup-plugin-visualizer": "5.5.2",
"sass": "1.43.4",
"sass": "1.44.0",
"slugify": "1.6.3",
"ts-jest": "27.0.7",
"typescript": "4.5.2",
"vite": "2.6.14",
"vite-plugin-pwa": "0.11.7",
"vite-plugin-pwa": "0.11.9",
"vite-svg-loader": "3.1.0",
"vue-tsc": "0.29.6",
"vue-tsc": "0.29.8",
"wait-on": "6.0.0",
"workbox-cli": "6.4.1"
},
@ -95,7 +96,8 @@
"env": {
"browser": true,
"es2021": true,
"node": true
"node": true,
"vue/setup-compiler-macros": true
},
"extends": [
"eslint:recommended",
@ -119,6 +121,7 @@
"error",
"never"
],
"vue/script-setup-uses-vars": "error",
"vue/multi-word-component-names": 0
},
"parser": "vue-eslint-parser",
@ -129,7 +132,10 @@
"ignorePatterns": [
"*.test.*",
"cypress/*"
]
],
"globals": {
"defineProps": "readonly"
}
},
"postcss": {
"plugins": {
@ -154,5 +160,6 @@
"json"
]
},
"license": "AGPL-3.0-or-later"
"license": "AGPL-3.0-or-later",
"packageManager": "yarn@1.22.17"
}

View File

@ -82,7 +82,7 @@ export default {
this.$route.name === 'labels.index' ||
this.$route.name === 'migrate.start' ||
this.$route.name === 'migrate.wunderlist' ||
this.$route.name === 'user.settings' ||
this.$route.name.startsWith('user.settings') ||
this.$route.name === 'namespaces.index'
) {
return this.$store.dispatch(CURRENT_LIST, null)

View File

@ -30,27 +30,25 @@
<a @click="() => (configureApi = true)">{{ $t('apiConfig.change') }}</a>
</div>
<div
class="notification is-success mt-2"
v-if="successMsg !== '' && errorMsg === ''"
>
<message variant="success" v-if="successMsg !== '' && errorMsg === ''" class="mt-2">
{{ successMsg }}
</div>
<div
class="notification is-danger mt-2"
v-if="errorMsg !== '' && successMsg === ''"
>
</message>
<message variant="danger" v-if="errorMsg !== '' && successMsg === ''" class="mt-2">
{{ errorMsg }}
</div>
</message>
</div>
</template>
<script>
import Message from '@/components/misc/message'
import {parseURL} from 'ufo'
import {checkAndSetApiUrl} from '@/helpers/checkAndSetApiUrl'
export default {
name: 'apiConfig',
components: {
Message,
},
data() {
return {
configureApi: false,

View File

@ -1,15 +1,18 @@
<template>
<div class="notification is-danger">
<message variant="danger">
<i18n-t keypath="loadingError.failed">
<a @click="reload">{{ $t('loadingError.tryAgain') }}</a>
<a href="https://vikunja.io/contact/" rel="noreferrer noopener nofollow" target="_blank">{{ $t('loadingError.contact') }}</a>
</i18n-t>
</div>
</message>
</template>
<script>
import Message from '@/components/misc/message'
export default {
name: 'error',
components: {Message},
methods: {
reload() {
window.location.reload()

View File

@ -4,13 +4,11 @@
<template v-for="(s, i) in shortcuts" :key="i">
<h3>{{ $t(s.title) }}</h3>
<div class="message is-primary">
<div class="message-body">
{{
s.available($route) ? $t('keyboardShortcuts.currentPageOnly') : $t('keyboardShortcuts.allPages')
}}
</div>
</div>
<message>
{{
s.available($route) ? $t('keyboardShortcuts.currentPageOnly') : $t('keyboardShortcuts.allPages')
}}
</message>
<dl class="shortcut-list">
<template v-for="(sc, si) in s.shortcuts" :key="si">
@ -30,11 +28,15 @@
<script>
import {KEYBOARD_SHORTCUTS_ACTIVE} from '@/store/mutation-types'
import Shortcut from '@/components/misc/shortcut.vue'
import Message from '@/components/misc/message'
import {KEYBOARD_SHORTCUTS} from './shortcuts'
export default {
name: 'keyboard-shortcuts',
components: {Shortcut},
components: {
Message,
Shortcut,
},
data() {
return {
shortcuts: KEYBOARD_SHORTCUTS,

View File

@ -0,0 +1,41 @@
<template>
<div class="message" :class="variant">
<slot/>
</div>
</template>
<script setup>
defineProps({
variant: {
type: String,
default: 'info',
},
})
</script>
<style lang="scss" scoped>
.message {
padding: .75rem 1rem;
border-radius: $radius;
}
.info {
border: 1px solid var(--primary);
background: hsla(var(--primary-hsl), .05);
}
.danger {
border: 1px solid var(--danger);
background: hsla(var(--danger-h), var(--danger-s), var(--danger-l), .05);
}
.warning {
border: 1px solid var(--warning);
background: hsla(var(--warning-h), var(--warning-s), var(--warning-l), .05);
}
.success {
border: 1px solid var(--success);
background: hsla(var(--success-h), var(--success-s), var(--success-l), .05);
}
</style>

View File

@ -2,14 +2,9 @@
<div class="no-auth-wrapper">
<div class="noauth-container">
<Logo class="logo" width="400" height="117" />
<div class="message is-info" v-if="motd !== ''">
<div class="message-header">
<p>{{ $t('misc.info') }}</p>
</div>
<div class="message-body">
{{ motd }}
</div>
</div>
<message v-if="motd !== ''" class="my-2">
{{ motd }}
</message>
<slot/>
</div>
</div>
@ -17,6 +12,7 @@
<script setup>
import Logo from '@/components/home/Logo.vue'
import message from '@/components/misc/message'
import {useStore} from 'vuex'
import {computed} from 'vue'
@ -26,7 +22,7 @@ const motd = computed(() => store.state.config.motd)
<style lang="scss" scoped>
.no-auth-wrapper {
background: url('@/assets/llama.svg') no-repeat bottom left fixed var(--site-background);
background: url('@/assets/llama.svg?url') no-repeat bottom left fixed var(--site-background);
min-height: 100vh;
}
@ -39,5 +35,6 @@ const motd = computed(() => store.state.config.motd)
.logo {
color: var(--logo-text-color);
max-width: 100%;
}
</style>

View File

@ -16,7 +16,7 @@
<p v-if="error === errorNoApiUrl">
{{ $t('ready.noApiUrlConfigured') }}
</p>
<div class="notification is-danger" v-else>
<message variant="danger" v-else>
<p>
{{ $t('ready.errorOccured') }}<br/>
{{ error }}
@ -24,7 +24,7 @@
<p>
{{ $t('ready.checkApiUrl') }}
</p>
</div>
</message>
<api-config :configure-open="true" @found-api="load"/>
</card>
</no-auth-wrapper>
@ -43,6 +43,7 @@
<script>
import Logo from '@/assets/logo.svg?component'
import ApiConfig from '@/components/misc/api-config'
import Message from '@/components/misc/message'
import NoAuthWrapper from '@/components/misc/no-auth-wrapper'
import {mapState} from 'vuex'
import {ERROR_NO_API_URL} from '@/helpers/checkAndSetApiUrl'
@ -50,6 +51,7 @@ import {ERROR_NO_API_URL} from '@/helpers/checkAndSetApiUrl'
export default {
name: 'ready',
components: {
Message,
Logo,
NoAuthWrapper,
ApiConfig,

View File

@ -193,10 +193,6 @@ export default {
align-items: center;
}
.message-body {
padding: .5rem .75rem;
}
}
}

View File

@ -3,17 +3,13 @@
<div class="field is-grouped">
<p class="control has-icons-left is-expanded">
<textarea
:disabled="taskService.loading || null"
class="input"
:disabled="taskService.loading || undefined"
class="add-task-textarea input"
:placeholder="$t('list.list.addPlaceholder')"
cols="1"
rows="1"
v-focus
v-model="newTaskTitle"
ref="newTaskInput"
:style="{
'minHeight': `${initialTextAreaHeight}px`,
'height': `calc(${textAreaHeight}px - 2px + 1rem)`
}"
@keyup="errorMessage = ''"
@keydown.enter="handleEnter"
/>
@ -23,7 +19,8 @@
</p>
<p class="control">
<x-button
:disabled="newTaskTitle === '' || taskService.loading || null"
class="add-task-button"
:disabled="newTaskTitle === '' || taskService.loading || undefined"
@click="addTask()"
icon="plus"
:loading="taskService.loading"
@ -35,121 +32,172 @@
<p class="help is-danger" v-if="errorMessage !== ''">
{{ errorMessage }}
</p>
<quick-add-magic v-if="errorMessage === ''"/>
<quick-add-magic v-else />
</div>
</template>
<script>
<script setup lang="ts">
import {ref, watch, unref, shallowReactive} from 'vue'
import {useI18n} from 'vue-i18n'
import {useStore} from 'vuex'
import { tryOnMounted, debouncedWatch, useWindowSize, MaybeRef } from '@vueuse/core'
import TaskService from '../../services/task'
import QuickAddMagic from '@/components/tasks/partials/quick-add-magic.vue'
const INPUT_BORDER_PX = 2
const LINE_HEIGHT = 1.5 // using getComputedStyles().lineHeight returns an (wrong) absolute pixel value, we need the factor to do calculations with it.
const cleanupTitle = title => {
function cleanupTitle(title: string) {
return title.replace(/^((\* |\+ |- )(\[ \] )?)/g, '')
}
export default {
name: 'add-task',
emits: ['taskAdded'],
data() {
return {
newTaskTitle: '',
taskService: new TaskService(),
errorMessage: '',
textAreaHeight: null,
initialTextAreaHeight: null,
function useAutoHeightTextarea(value: MaybeRef<string>) {
const textarea = ref<HTMLInputElement>()
const minHeight = ref(0)
// adapted from https://github.com/LeaVerou/stretchy/blob/47f5f065c733029acccb755cae793009645809e2/src/stretchy.js#L34
function resize(textareaEl: HTMLInputElement|undefined) {
if (!textareaEl) return
let empty
// the value here is the the attribute value
if (!textareaEl.value && textareaEl.placeholder) {
empty = true
textareaEl.value = textareaEl.placeholder
}
},
components: {
QuickAddMagic,
},
props: {
defaultPosition: {
type: Number,
required: false,
const cs = getComputedStyle(textareaEl)
textareaEl.style.minHeight = ''
textareaEl.style.height = '0'
const offset = textareaEl.offsetHeight - parseFloat(cs.paddingTop) - parseFloat(cs.paddingBottom)
const height = textareaEl.scrollHeight + offset + 'px'
textareaEl.style.height = height
// calculate min-height for the first time
if (!minHeight.value) {
minHeight.value = parseFloat(height)
}
textareaEl.style.minHeight = minHeight.value.toString()
if (empty) {
textareaEl.value = ''
}
}
tryOnMounted(() => {
if (textarea.value) {
// we don't want scrollbars
textarea.value.style.overflowY = 'hidden'
}
})
const { width: windowWidth } = useWindowSize()
debouncedWatch(
windowWidth,
() => resize(textarea.value),
{ debounce: 200 },
)
// It is not possible to get notified of a change of the value attribute of a textarea without workarounds (setTimeout)
// So instead we watch the value that we bound to it.
watch(
() => [textarea.value, unref(value)],
() => resize(textarea.value),
{
immediate: true, // calculate initial size
flush: 'post', // resize after value change is rendered to DOM
},
)
return textarea
}
const emit = defineEmits(['taskAdded'])
const props = defineProps({
defaultPosition: {
type: Number,
required: false,
},
watch: {
newTaskTitle(newVal) {
// Calculating the textarea height based on lines of input in it.
// That is more reliable when removing a line from the input.
const numberOfLines = newVal.split(/\r\n|\r|\n/).length
const fontSize = parseFloat(window.getComputedStyle(this.$refs.newTaskInput, null).getPropertyValue('font-size'))
})
this.textAreaHeight = numberOfLines * fontSize * LINE_HEIGHT + INPUT_BORDER_PX
},
},
mounted() {
this.initialTextAreaHeight = this.$refs.newTaskInput.scrollHeight + INPUT_BORDER_PX
},
methods: {
async addTask() {
if (this.newTaskTitle === '') {
this.errorMessage = this.$t('list.create.addTitleRequired')
return
}
this.errorMessage = ''
const taskService = shallowReactive(new TaskService())
const errorMessage = ref('')
if (this.taskService.loading) {
return
}
const newTaskTitle = ref('')
const newTaskInput = useAutoHeightTextarea(newTaskTitle)
const newTasks = this.newTaskTitle.split(/[\r\n]+/).map(async t => {
const title = cleanupTitle(t)
if (title === '') {
return
}
const { t } = useI18n()
const store = useStore()
const task = await this.$store.dispatch('tasks/createNewTask', {
title,
listId: this.$store.state.auth.settings.defaultListId,
position: this.defaultPosition,
})
this.$emit('taskAdded', task)
return task
})
async function addTask() {
if (newTaskTitle.value === '') {
errorMessage.value = t('list.create.addTitleRequired')
return
}
errorMessage.value = ''
try {
await Promise.all(newTasks)
this.newTaskTitle = ''
} catch (e) {
if (e.message === 'NO_LIST') {
this.errorMessage = this.$t('list.create.addListRequired')
return
}
throw e
}
},
handleEnter(e) {
// when pressing shift + enter we want to continue as we normally would. Otherwise, we want to create
// the new task(s). The vue event modifier don't allow this, hence this method.
if (e.shiftKey) {
return
}
if (taskService.loading) {
return
}
e.preventDefault()
this.addTask()
},
},
const taskTitleBackup = newTaskTitle.value
const newTasks = newTaskTitle.value.split(/[\r\n]+/).map(async uncleanedTitle => {
const title = cleanupTitle(uncleanedTitle)
if (title === '') {
return
}
const task = await store.dispatch('tasks/createNewTask', {
title,
listId: store.state.auth.settings.defaultListId,
position: props.defaultPosition,
})
emit('taskAdded', task)
return task
})
try {
newTaskTitle.value = ''
await Promise.all(newTasks)
} catch (e: any) {
newTaskTitle.value = taskTitleBackup
if (e?.message === 'NO_LIST') {
errorMessage.value = t('list.create.addListRequired')
return
}
throw e
}
}
function handleEnter(e: KeyboardEvent) {
// when pressing shift + enter we want to continue as we normally would. Otherwise, we want to create
// the new task(s). The vue event modifier don't allow this, hence this method.
if (e.shiftKey) {
return
}
e.preventDefault()
addTask()
}
</script>
<style lang="scss" scoped>
.task-add {
margin-bottom: 0;
.button {
height: 2.5rem;
}
}
.input, .textarea {
.add-task-button {
height: 2.5rem;
}
.add-task-textarea {
transition: border-color $transition;
}
.input {
resize: vertical;
resize: none;
}
</style>

View File

@ -2,13 +2,6 @@
<div class="gantt-chart">
<div class="filter-container">
<div class="items">
<x-button
@click.prevent.stop="showTaskFilter = !showTaskFilter"
type="secondary"
icon="filter"
>
{{ $t('filters.title') }}
</x-button>
<filter-popup
:visible="showTaskFilter"
v-model="params"
@ -237,7 +230,6 @@ export default {
newTaskFieldActive: false,
priorities: priorities,
taskCollectionService: new TaskCollectionService(),
showTaskFilter: false,
params: {
sort_by: ['done', 'id'],

View File

@ -1,9 +1,9 @@
import {computed, watch, readonly} from 'vue'
import {useStorage, createSharedComposable, ColorSchemes, usePreferredColorScheme, tryOnMounted} from '@vueuse/core'
import {useStorage, createSharedComposable, ColorSchema, usePreferredColorScheme, tryOnMounted} from '@vueuse/core'
const STORAGE_KEY = 'color-scheme'
const DEFAULT_COLOR_SCHEME_SETTING: ColorSchemes = 'light'
const DEFAULT_COLOR_SCHEME_SETTING: ColorSchema = 'light'
const CLASS_DARK = 'dark'
const CLASS_LIGHT = 'light'
@ -16,7 +16,7 @@ const CLASS_LIGHT = 'light'
// - value is synced via `createSharedComposable`
// https://github.com/vueuse/vueuse/blob/main/packages/core/useDark/index.ts
export const useColorScheme = createSharedComposable(() => {
const store = useStorage<ColorSchemes>(STORAGE_KEY, DEFAULT_COLOR_SCHEME_SETTING)
const store = useStorage<ColorSchema>(STORAGE_KEY, DEFAULT_COLOR_SCHEME_SETTING)
const preferredColorScheme = usePreferredColorScheme()

View File

@ -901,7 +901,7 @@
"5010": "Tento tým nemá k tomuto prostoru přístup.",
"5011": "Tento uživatel již má přístup k tomuto prostoru.",
"5012": "Prostor je archivován, a proto je přístupný pouze pro čtení.",
"6001": "Název týmu nemůže být prázdný.",
"6001": "The team name cannot be empty.",
"6002": "Tým neexistuje.",
"6004": "Tým již má přístup k tomuto prostoru nebo seznamu.",
"6005": "Uživatel je již členem tohoto týmu.",

View File

@ -114,12 +114,12 @@
"vikunja": "Vikunja"
},
"appearance": {
"title": "Color Scheme",
"setSuccess": "Saved change of color scheme to {colorScheme}",
"title": "Farbschema",
"setSuccess": "Änderung des Farbschemas auf {colorScheme} gespeichert",
"colorScheme": {
"light": "Light",
"light": "Hell",
"system": "System",
"dark": "Dark"
"dark": "Dunkel"
}
}
},
@ -556,7 +556,7 @@
"text2": "Dies wird auch alle Anhänge, Erinnerungen und Verknüpfungen, die zu dieser Aufgabe gehören löschen und kann nicht rückgängig gemacht werden!"
},
"actions": {
"assign": "Assign to a user",
"assign": "Benutzer:in zuweisen",
"label": "Label hinzufügen",
"priority": "Priorität setzen",
"dueDate": "Fälligkeitsdatum setzen",
@ -775,7 +775,7 @@
"task": {
"title": "Aufgabenseite",
"done": "Eine Aufgabe als erledigt markieren",
"assign": "Assign to a user",
"assign": "Benutzer:in zuweisen",
"labels": "Dieser Aufgabe ein Label hinzufügen",
"dueDate": "Ändere das Fälligkeitsdatum dieser Aufgabe",
"attachment": "Einen Anhang dieser Aufgabe hinzufügen",

View File

@ -114,12 +114,12 @@
"vikunja": "Vikunja"
},
"appearance": {
"title": "Color Scheme",
"setSuccess": "Saved change of color scheme to {colorScheme}",
"title": "Farbschema",
"setSuccess": "Änderung des Farbschemas auf {colorScheme} gespeichert",
"colorScheme": {
"light": "Light",
"light": "Hell",
"system": "System",
"dark": "Dark"
"dark": "Dunkel"
}
}
},
@ -556,7 +556,7 @@
"text2": "Das wird au alli Ahhäng, Errinnerige und Beziehige wo mit dere Uufgab verchnüpft sind chüble und cha nid rückgängig gmacht werde!"
},
"actions": {
"assign": "Assign to a user",
"assign": "Benutzer:in zuweisen",
"label": "Label hinzuefüege",
"priority": "Priorität setzä",
"dueDate": "Fälligkeitsdatum setze",
@ -775,7 +775,7 @@
"task": {
"title": "Uufgabesiite",
"done": "Uufgab als erledigt markiere",
"assign": "Assign to a user",
"assign": "Benutzer:in zuweisen",
"labels": "Labels ennere Uufgab hinzuefüege",
"dueDate": "S'Fälligkeitsdatum für die Uufgab ändere",
"attachment": "En Aahang dere Uufgab hinzuefüege",
@ -901,7 +901,7 @@
"5010": "Da Team hett kei zuegriff uf de Namensruum.",
"5011": "De Benutzer hett bereits zuegriff uf de Namensruum.",
"5012": "De Namensruum isch momentan schriibgschützt weil er archiviert isch.",
"6001": "Der TeamName kann nicht leer sein.",
"6001": "Der Teamname kann nicht leer sein.",
"6002": "Da Team giz nid.",
"6004": "Da Team het scho Zuegang zu dem Namensruum oder Liste.",
"6005": "De Benutzer isch scho bi dem Team.",

View File

@ -901,7 +901,7 @@
"5010": "This team does not have access to that namespace.",
"5011": "This user has already access to that namespace.",
"5012": "The namespace is archived and can therefore only be accessed read only.",
"6001": "The team name cannot be emtpy.",
"6001": "The team name cannot be empty.",
"6002": "The team does not exist.",
"6004": "The team already has access to that namespace or list.",
"6005": "The user is already a member of that team.",

View File

@ -901,7 +901,7 @@
"5010": "This team does not have access to that namespace.",
"5011": "This user has already access to that namespace.",
"5012": "The namespace is archived and can therefore only be accessed read only.",
"6001": "The team name cannot be emtpy.",
"6001": "The team name cannot be empty.",
"6002": "The team does not exist.",
"6004": "The team already has access to that namespace or list.",
"6005": "The user is already a member of that team.",

View File

@ -901,7 +901,7 @@
"5010": "Cette équipe na pas accès à cet espace de noms.",
"5011": "Cet·e utilisateur·rice a déjà accès à cet espace de noms.",
"5012": "Lespace de noms est archivé et ne peut donc être consulté quen lecture seule.",
"6001": "Le nom de léquipe ne peut pas être vide.",
"6001": "The team name cannot be empty.",
"6002": "Léquipe nexiste pas.",
"6004": "Léquipe a déjà accès à cet espace de noms ou à cette liste.",
"6005": "Lutilisateur·rice est déjà membre de cette équipe.",

View File

@ -901,7 +901,7 @@
"5010": "This team does not have access to that namespace.",
"5011": "This user has already access to that namespace.",
"5012": "The namespace is archived and can therefore only be accessed read only.",
"6001": "The team name cannot be emtpy.",
"6001": "The team name cannot be empty.",
"6002": "The team does not exist.",
"6004": "The team already has access to that namespace or list.",
"6005": "The user is already a member of that team.",

View File

@ -901,7 +901,7 @@
"5010": "This team does not have access to that namespace.",
"5011": "This user has already access to that namespace.",
"5012": "The namespace is archived and can therefore only be accessed read only.",
"6001": "The team name cannot be emtpy.",
"6001": "The team name cannot be empty.",
"6002": "The team does not exist.",
"6004": "The team already has access to that namespace or list.",
"6005": "The user is already a member of that team.",

View File

@ -901,7 +901,7 @@
"5010": "This team does not have access to that namespace.",
"5011": "This user has already access to that namespace.",
"5012": "The namespace is archived and can therefore only be accessed read only.",
"6001": "The team name cannot be emtpy.",
"6001": "The team name cannot be empty.",
"6002": "The team does not exist.",
"6004": "The team already has access to that namespace or list.",
"6005": "The user is already a member of that team.",

View File

@ -901,7 +901,7 @@
"5010": "This team does not have access to that namespace.",
"5011": "This user has already access to that namespace.",
"5012": "The namespace is archived and can therefore only be accessed read only.",
"6001": "The team name cannot be emtpy.",
"6001": "The team name cannot be empty.",
"6002": "The team does not exist.",
"6004": "The team already has access to that namespace or list.",
"6005": "The user is already a member of that team.",

View File

@ -901,7 +901,7 @@
"5010": "This team does not have access to that namespace.",
"5011": "This user has already access to that namespace.",
"5012": "The namespace is archived and can therefore only be accessed read only.",
"6001": "The team name cannot be emtpy.",
"6001": "The team name cannot be empty.",
"6002": "The team does not exist.",
"6004": "The team already has access to that namespace or list.",
"6005": "The user is already a member of that team.",

View File

@ -901,7 +901,7 @@
"5010": "У этой команды нет доступа к этому пространству имён.",
"5011": "Этот пользователь уже имеет доступ к этому пространству имён.",
"5012": "Это пространство имён архивировано и поэтому доступно только для чтения.",
"6001": "Имя команды не может быть пустым.",
"6001": "The team name cannot be empty.",
"6002": "Команда не существует.",
"6004": "Эта команда уже имеет доступ к этому пространству имён или списку.",
"6005": "Пользователь уже является участником этой команды.",

933
src/i18n/lang/sv-SE.json Normal file
View File

@ -0,0 +1,933 @@
{
"home": {
"welcomeNight": "Good Night {username}",
"welcomeMorning": "Good Morning {username}",
"welcomeDay": "Hi {username}",
"welcomeEvening": "Good Evening {username}",
"lastViewed": "Last viewed",
"list": {
"newText": "You can create a new list for your new tasks:",
"new": "Create a new list",
"importText": "Or import your lists and tasks from other services into Vikunja:",
"import": "Import your data into Vikunja"
}
},
"404": {
"title": "Not found",
"text": "The page you requested does not exist."
},
"ready": {
"loading": "Vikunja is loading…",
"errorOccured": "An error occured:",
"checkApiUrl": "Please check if the api url is correct.",
"noApiUrlConfigured": "No API url was configured. Please set one below:"
},
"offline": {
"title": "You are offline.",
"text": "Please check your network connection and try again."
},
"user": {
"auth": {
"username": "Username",
"usernameEmail": "Username Or Email Address",
"usernamePlaceholder": "e.g. frederick",
"email": "E-mail address",
"emailPlaceholder": "e.g. frederic{'@'}vikunja.io",
"password": "Password",
"passwordRepeat": "Retype your password",
"passwordPlaceholder": "e.g. •••••••••••",
"resetPassword": "Reset your password",
"resetPasswordAction": "Send me a password reset link",
"resetPasswordSuccess": "Check your inbox! You should have an e-mail with instructions on how to reset your password.",
"passwordsDontMatch": "Passwords don't match",
"confirmEmailSuccess": "You successfully confirmed your email! You can log in now.",
"totpTitle": "Two Factor Authentication Code",
"totpPlaceholder": "e.g. 123456",
"login": "Login",
"register": "Register",
"loginWith": "Log in with {provider}",
"authenticating": "Authenticating…",
"openIdStateError": "State does not match, refusing to continue!",
"openIdGeneralError": "An error occured while authenticating against the third party.",
"logout": "Logout"
},
"settings": {
"title": "Settings",
"newPasswordTitle": "Update Your Password",
"newPassword": "New Password",
"newPasswordConfirm": "New Password Confirmation",
"currentPassword": "Current Password",
"currentPasswordPlaceholder": "Your current password",
"passwordsDontMatch": "The new password and its confirmation don't match.",
"passwordUpdateSuccess": "The password was successfully updated.",
"updateEmailTitle": "Update Your E-Mail Address",
"updateEmailNew": "New Email Address",
"updateEmailSuccess": "Your email address was successfully updated. We've sent you a link to confirm it.",
"general": {
"title": "General Settings",
"name": "Name",
"newName": "The new Name",
"savedSuccess": "The settings were successfully updated.",
"emailReminders": "Send me reminders for tasks via Email",
"overdueReminders": "Send me reminders for overdue undone tasks via email each morning",
"discoverableByName": "Let other users find me when they search for my name",
"discoverableByEmail": "Let other users find me when they search for my full email",
"playSoundWhenDone": "Play a sound when marking tasks as done",
"weekStart": "Week starts on",
"weekStartSunday": "Sunday",
"weekStartMonday": "Monday",
"language": "Language",
"defaultList": "Default List"
},
"totp": {
"title": "Two Factor Authentication",
"enroll": "Enroll",
"finishSetupPart1": "To finish your setup, use this secret in your totp app (Google Authenticator or similar):",
"finishSetupPart2": "After that, enter a code from your app below.",
"scanQR": "Alternatively you can scan this QR code:",
"passcode": "Passcode",
"passcodePlaceholder": "A code generated by your totp application",
"setupSuccess": "You've sucessfully set up two factor authentication!",
"enterPassword": "Please Enter Your Password",
"disable": "Disable two factor authentication",
"confirmSuccess": "You've successfully confirmed your totp setup and can use it from now on!",
"disableSuccess": "Two factor authentication was sucessfully disabled."
},
"caldav": {
"title": "Caldav",
"howTo": "You can connect Vikunja to caldav clients to view and manage all tasks from different clients. Enter this url into your client:",
"more": "More information about caldav in Vikunja"
},
"avatar": {
"title": "Avatar",
"initials": "Initials",
"gravatar": "Gravatar",
"upload": "Upload",
"uploadAvatar": "Upload Avatar",
"statusUpdateSuccess": "Avatar status was updated successfully!",
"setSuccess": "The avatar has been set successfully!"
},
"quickAddMagic": {
"title": "Quick Add Magic Mode",
"disabled": "Disabled",
"todoist": "Todoist",
"vikunja": "Vikunja"
},
"appearance": {
"title": "Color Scheme",
"setSuccess": "Saved change of color scheme to {colorScheme}",
"colorScheme": {
"light": "Light",
"system": "System",
"dark": "Dark"
}
}
},
"deletion": {
"title": "Delete your Vikunja Account",
"text1": "The deletion of your account is permanent and cannot be undone. We will delete all your namespaces, lists, tasks and everything associated with it.",
"text2": "To proceed, please enter your password. You will receive an email with further instructions.",
"confirm": "Delete my account",
"requestSuccess": "The request was successful. You'll receive an email with further instructions.",
"passwordRequired": "Please enter your password.",
"confirmSuccess": "You've successfully confirmed the deletion of your account. We will delete your account in three days.",
"scheduled": "We will delete your Vikunja account at {date} ({dateSince}).",
"scheduledCancel": "To cancel the deletion of your account, click here.",
"scheduledCancelText": "To cancel the deletion of your account, please enter your password below:",
"scheduledCancelConfirm": "Cancel the deletion of my account",
"scheduledCancelSuccess": "We will not delete your account."
},
"export": {
"title": "Export your Vikunja data",
"description": "You can request a copy of all your Vikunja data. This include Namespaces, Lists, Tasks and everything associated to them. You can import this data in any Vikunja instance through the migration function.",
"descriptionPasswordRequired": "Please enter your password to proceed:",
"request": "Request a copy of my Vikunja Data",
"success": "You've successfully requested your Vikunja Data! We will send you an email once it's ready to download.",
"downloadTitle": "Download your exported Vikunja data"
}
},
"list": {
"archived": "This list is archived. It is not possible to create new or edit tasks for it.",
"title": "List Title",
"color": "Color",
"lists": "Lists",
"search": "Type to search for a list…",
"searchSelect": "Click or press enter to select this list",
"shared": "Shared Lists",
"create": {
"header": "Create a new list",
"titlePlaceholder": "The list's title goes here…",
"addTitleRequired": "Please specify a title.",
"createdSuccess": "The list was successfully created.",
"addListRequired": "Please specify a list or set a default list in the settings."
},
"archive": {
"title": "Archive \"{list}\"",
"archive": "Archive this list",
"unarchive": "Un-Archive this list",
"unarchiveText": "You will be able to create new tasks or edit it.",
"archiveText": "You won't be able to edit this list or create new tasks until you un-archive it.",
"success": "The list was successfully archived."
},
"background": {
"title": "Set list background",
"remove": "Remove Background",
"upload": "Choose a background from your pc",
"searchPlaceholder": "Search for a background…",
"poweredByUnsplash": "Powered by Unsplash",
"loadMore": "Load more photos",
"success": "The background has been set successfully!",
"removeSuccess": "The background has been removed successfully!"
},
"delete": {
"title": "Delete \"{list}\"",
"header": "Delete this list",
"text1": "Are you sure you want to delete this list and all of its contents?",
"text2": "This includes all tasks and CANNOT BE UNDONE!",
"success": "The list was successfully deleted."
},
"duplicate": {
"title": "Duplicate this list",
"label": "Duplicate",
"text": "Select a namespace which should hold the duplicated list:",
"success": "The list was successfully duplicated."
},
"edit": {
"header": "Edit This List",
"title": "Edit \"{list}\"",
"titlePlaceholder": "The list title goes here…",
"identifierTooltip": "The list identifier can be used to uniquely identify a task across lists. You can set it to empty to disable it.",
"identifier": "List Identifier",
"identifierPlaceholder": "The list identifier goes here…",
"description": "Description",
"descriptionPlaceholder": "The lists description goes here…",
"color": "Color",
"success": "The list was successfully updated."
},
"share": {
"header": "Share this list",
"title": "Share \"{list}\"",
"share": "Share",
"links": {
"title": "Share Links",
"what": "What is a share link?",
"explanation": "Share Links allow you to easily share a list with other users who don't have an account on Vikunja.",
"create": "Create a new link share",
"name": "Name (optional)",
"namePlaceholder": "e.g. Lorem Ipsum",
"nameExplanation": "All actions done by this link share will show up with the name.",
"password": "Password (optional)",
"passwordExplanation": "When authenticating, the user will be required to enter this password.",
"noName": "No name set",
"remove": "Remove a link share",
"removeText": "Are you sure you want to remove this link share? It will no longer be possible to access this list with this link share. This cannot be undone!",
"createSuccess": "The link share was successfully created.",
"deleteSuccess": "The link share was successfully deleted"
},
"userTeam": {
"typeUser": "user | users",
"typeTeam": "team | teams",
"shared": "Shared with these {type}",
"you": "You",
"notShared": "Not shared with any {type} yet.",
"removeHeader": "Remove a {type} from the {sharable}",
"removeText": "Are you sure you want to remove this {sharable} from the {type}? This cannot be undone!",
"removeSuccess": "The {sharable} was successfully removed from the {type}.",
"addedSuccess": "The {type} was successfully added.",
"updatedSuccess": "The {type} was successfully added."
},
"right": {
"title": "Right",
"read": "Read only",
"readWrite": "Read & write",
"admin": "Admin"
},
"attributes": {
"link": "Link",
"name": "Name",
"sharedBy": "Shared by",
"right": "Right",
"delete": "Delete"
}
},
"list": {
"title": "List",
"add": "Add",
"addPlaceholder": "Add a new task…",
"empty": "This list is currently empty.",
"newTaskCta": "Create a new task.",
"editTask": "Edit Task"
},
"gantt": {
"title": "Gantt",
"showTasksWithoutDates": "Show tasks which don't have dates set",
"size": "Size",
"default": "Default",
"month": "Month",
"day": "Day",
"from": "From",
"to": "To",
"noDates": "This task has no dates set."
},
"table": {
"title": "Table",
"columns": "Columns"
},
"kanban": {
"title": "Kanban",
"limit": "Limit: {limit}",
"noLimit": "Not Set",
"doneBucket": "Done bucket",
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
"doneBucketHintExtended": "All tasks moved into the done bucket will be marked as done automatically. All tasks marked as done from elsewhere will be moved as well.",
"doneBucketSavedSuccess": "The done bucket has been saved successfully.",
"deleteLast": "You cannot remove the last bucket.",
"addTaskPlaceholder": "Enter the new task title…",
"addTask": "Add a task",
"addAnotherTask": "Add another task",
"addBucket": "Create a new bucket",
"addBucketPlaceholder": "Enter the new bucket title…",
"deleteHeaderBucket": "Delete the bucket",
"deleteBucketText1": "Are you sure you want to delete this bucket?",
"deleteBucketText2": "This will not delete any tasks but move them into the default bucket.",
"deleteBucketSuccess": "The bucket has been deleted successfully.",
"bucketTitleSavedSuccess": "The bucket title has been saved successfully.",
"bucketLimitSavedSuccess": "The bucket limit been saved successfully.",
"collapse": "Collapse this bucket"
},
"pseudo": {
"favorites": {
"title": "Favorites"
}
}
},
"namespace": {
"title": "Namespaces & Lists",
"namespace": "Namespace",
"showArchived": "Show Archived",
"noneAvailable": "You don't have any namespaces right now.",
"unarchive": "Un-Archive",
"archived": "Archived",
"noLists": "This namespace does not contain any lists.",
"createList": "Create a new list in this namespace.",
"namespaces": "Namespaces",
"search": "Type to search for a namespace…",
"create": {
"title": "Create a 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?",
"success": "The namespace was successfully created."
},
"archive": {
"titleArchive": "Archive \"{namespace}\"",
"titleUnarchive": "Un-Archive \"{namespace}\"",
"archiveText": "You won't be able to edit this namespace or create new lists until you un-archive it. This will also archive all lists in this namespace.",
"unarchiveText": "You will be able to create new lists or edit it.",
"success": "The namespace was successfully archived.",
"description": "If a namespace is archived, you cannot create new lists or edit it."
},
"delete": {
"title": "Delete \"{namespace}\"",
"text1": "Are you sure you want to delete this namespace and all of its contents?",
"text2": "This includes all lists and tasks and CANNOT BE UNDONE!",
"success": "The namespace was successfully deleted."
},
"edit": {
"title": "Edit \"{namespace}\"",
"success": "The namespace was successfully updated."
},
"share": {
"title": "Share \"{namespace}\""
},
"attributes": {
"title": "Namespace Title",
"titlePlaceholder": "The namespace title goes here…",
"description": "Description",
"descriptionPlaceholder": "The namespaces description goes here…",
"color": "Color",
"archived": "Is Archived",
"isArchived": "This namespace is archived"
},
"pseudo": {
"sharedLists": {
"title": "Shared Lists"
},
"favorites": {
"title": "Favorites"
},
"savedFilters": {
"title": "Filters"
}
}
},
"filters": {
"title": "Filters",
"clear": "Clear Filters",
"attributes": {
"title": "Title",
"titlePlaceholder": "The saved filter title goes here…",
"description": "Description",
"descriptionPlaceholder": "The description goes here…",
"includeNulls": "Include Tasks which don't have a value set",
"requireAll": "Require all filters to be true for a task to show up",
"showDoneTasks": "Show Done Tasks",
"enablePriority": "Enable Filter By Priority",
"enablePercentDone": "Enable Filter By Percent Done",
"dueDateRange": "Due Date Range",
"startDateRange": "Start Date Range",
"endDateRange": "End Date Range",
"reminderRange": "Reminder Date Range"
},
"create": {
"title": "Create A 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"
},
"delete": {
"header": "Delete this saved filter",
"text": "Are you sure you want to delete this saved filter?",
"success": "The filter was deleted successfully."
},
"edit": {
"title": "Edit This Saved Filter",
"success": "The filter was saved successfully."
}
},
"migrate": {
"title": "Migrate from other services to Vikunja",
"titleService": "Import your data from {name} into Vikunja",
"import": "Import your data into Vikunja",
"description": "Click on the logo of one of the third-party services below to get started.",
"descriptionDo": "Vikunja will import all lists, tasks, notes, reminders and files you have access to.",
"authorize": "To authorize Vikunja to access your {name} Account, click the button below.",
"getStarted": "Get Started",
"inProgress": "Importing in progress…",
"alreadyMigrated1": "It looks like you've already imported your stuff from {name} at {date}.",
"alreadyMigrated2": "Importing again is possible, but might create duplicates. Are you sure?",
"confirm": "I am sure, please start migrating now!",
"importUpload": "To import data from {name} into Vikunja, click the button below to select a file.",
"upload": "Upload file"
},
"label": {
"title": "Labels",
"manage": "Manage labels",
"description": "Click on a label to edit it. You can edit all labels you created, you can use all labels which are associated with a task to whose list you have access.",
"newCTA": "You currently do not have any labels.",
"search": "Type to search for a label…",
"create": {
"header": "New label",
"title": "Create a new label",
"titleRequired": "Please specify a title.",
"success": "The label was successfully created."
},
"edit": {
"header": "Edit Label",
"forbidden": "You are not allowed to edit this label because you dont own it.",
"success": "The label was successfully updated."
},
"deleteSuccess": "The label was successfully deleted.",
"attributes": {
"title": "Title",
"titlePlaceholder": "The label title goes here…",
"description": "Description",
"descriptionPlaceholder": "Label description",
"color": "Color"
}
},
"sharing": {
"authenticating": "Authenticating…",
"passwordRequired": "This shared list requires a password. Please enter it below:",
"error": "An error occured.",
"invalidPassword": "The password is invalid."
},
"navigation": {
"overview": "Overview",
"upcoming": "Upcoming",
"settings": "Settings",
"imprint": "Imprint",
"privacy": "Privacy Policy"
},
"misc": {
"loading": "Loading…",
"save": "Save",
"delete": "Delete",
"confirm": "Confirm",
"cancel": "Cancel",
"refresh": "Refresh",
"disable": "Disable",
"copy": "Copy to clipboard",
"search": "Search",
"searchPlaceholder": "Type to search…",
"previous": "Previous",
"next": "Next",
"poweredBy": "Powered by Vikunja",
"info": "Info",
"create": "Create",
"doit": "Do it!",
"saving": "Saving…",
"saved": "Saved!",
"default": "Default",
"close": "Close",
"download": "Download",
"showMenu": "Show the menu",
"hideMenu": "Hide the menu"
},
"input": {
"resetColor": "Reset Color",
"datepicker": {
"today": "Today",
"tomorrow": "Tomorrow",
"nextMonday": "Next Monday",
"thisWeekend": "This Weekend",
"laterThisWeek": "Later This Week",
"nextWeek": "Next Week",
"chooseDate": "Choose a date"
},
"editor": {
"edit": "Edit",
"done": "Done",
"heading1": "Heading 1",
"heading2": "Heading 2",
"heading3": "Heading 3",
"headingSmaller": "Heading Smaller",
"headingBigger": "Heading Bigger",
"bold": "Bold",
"italic": "Italic",
"strikethrough": "Strikethrough",
"code": "Code",
"quote": "Quote",
"unorderedList": "Unordered List",
"orderedList": "Ordered List",
"cleanBlock": "Clean Block",
"link": "Link",
"image": "Image",
"table": "Table",
"horizontalRule": "Horizontal Rule",
"sideBySide": "Side By Side",
"guide": "Guide"
},
"multiselect": {
"createPlaceholder": "Create new",
"selectPlaceholder": "Click or press enter to select"
}
},
"task": {
"task": "Task",
"new": "Create a new task",
"delete": "Delete this task",
"createSuccess": "The task was successfully created.",
"addReminder": "Add a new reminder…",
"doneSuccess": "The task was successfully marked as done.",
"undoneSuccess": "The task was successfully un-marked as done.",
"openDetail": "Open task detail view",
"checklistTotal": "{checked} of {total} tasks",
"checklistAllDone": "{total} tasks",
"show": {
"titleCurrent": "Current Tasks",
"titleDates": "Tasks from {from} until {to}",
"noDates": "Show tasks without dates",
"current": "Current tasks",
"from": "Tasks from",
"until": "until",
"today": "Today",
"nextWeek": "Next Week",
"nextMonth": "Next Month",
"noTasks": "Nothing to do — Have a nice day!"
},
"detail": {
"chooseDueDate": "Click here to set a due date",
"chooseStartDate": "Click here to set a start date",
"chooseEndDate": "Click here to set an end date",
"move": "Move task to a different list",
"done": "Done!",
"undone": "Mark as undone",
"created": "Created {0} by {1}",
"updated": "Updated {0}",
"doneAt": "Done {0}",
"updateSuccess": "The task was saved successfully.",
"deleteSuccess": "The task has been deleted successfully.",
"belongsToList": "This task belongs to list '{list}'",
"due": "Due {at}",
"closePopup": "Close popup",
"delete": {
"header": "Delete this task",
"text1": "Are you sure you want to remove this task?",
"text2": "This will also remove all attachments, reminders and relations associated with this task and cannot be undone!"
},
"actions": {
"assign": "Assign to a user",
"label": "Add labels",
"priority": "Set Priority",
"dueDate": "Set Due Date",
"startDate": "Set a Start Date",
"endDate": "Set an End Date",
"reminders": "Set Reminders",
"repeatAfter": "Set a repeating interval",
"percentDone": "Set Percent Done",
"attachments": "Add attachments",
"relatedTasks": "Add task relations",
"moveList": "Move task",
"color": "Set task color",
"delete": "Delete task",
"favorite": "Save as favorite",
"unfavorite": "Remove from favorites"
}
},
"attributes": {
"assignees": "Assignees",
"color": "Color",
"created": "Created",
"createdBy": "Created By",
"description": "Description",
"done": "Done",
"dueDate": "Due Date",
"endDate": "End Date",
"labels": "Labels",
"percentDone": "% Done",
"priority": "Priority",
"relatedTasks": "Related Tasks",
"reminders": "Reminders",
"repeat": "Repeat",
"startDate": "Start Date",
"title": "Title",
"updated": "Updated"
},
"subscription": {
"subscribedThroughParent": "You can't unsubscribe here because you are subscribed to this {entity} through its {parent}.",
"subscribed": "You are currently subscribed to this {entity} and will receive notifications for changes.",
"notSubscribed": "You are not subscribed to this {entity} and won't receive notifications for changes.",
"subscribe": "Subscribe",
"unsubscribe": "Unsubscribe",
"subscribeSuccess": "You are now subscribed to this {entity}",
"unsubscribeSuccess": "You are now unsubscribed to this {entity}"
},
"attachment": {
"title": "Attachments",
"createdBy": "created {0} by {1}",
"downloadTooltip": "Download this attachment",
"upload": "Upload attachment",
"drop": "Drop files here to upload",
"delete": "Delete attachment",
"deleteTooltip": "Delete this attachment",
"deleteText1": "Are you sure you want to delete the attachment {filename}?",
"deleteText2": "This cannot be undone!",
"copyUrl": "Copy URL",
"copyUrlTooltip": "Copy the url of this attachment for usage in text"
},
"comment": {
"title": "Comments",
"loading": "Loading comments…",
"edited": "edited {date}",
"creating": "Creating comment…",
"placeholder": "Add your comment…",
"comment": "Comment",
"delete": "Delete this comment",
"deleteText1": "Are you sure you want to delete this comment?",
"deleteText2": "This cannot be undone!",
"addedSuccess": "The comment was added successfully."
},
"deferDueDate": {
"title": "Defer due date",
"1day": "1 day",
"3days": "3 days",
"1week": "1 week"
},
"description": {
"placeholder": "Click here to enter a description…",
"empty": "No description available yet."
},
"assignee": {
"placeholder": "Type to assign a user…",
"selectPlaceholder": "Assign this user",
"assignSuccess": "The user has been assigned successfully.",
"unassignSuccess": "The user has been unassigned successfully."
},
"label": {
"placeholder": "Type to add a new label…",
"createPlaceholder": "Add this as new label",
"addSuccess": "The label has been added successfully.",
"createSuccess": "The label has been created successfully.",
"removeSuccess": "The label has been removed successfully.",
"addCreateSuccess": "The label has been created and added successfully."
},
"priority": {
"unset": "Unset",
"low": "Low",
"medium": "Medium",
"high": "high",
"urgent": "Urgent",
"doNow": "DO NOW"
},
"relation": {
"add": "Add a New Task Relation",
"new": "New Task Relation",
"searchPlaceholder": "Type search for a new task to add as related…",
"createPlaceholder": "Add this as new related task",
"differentList": "This task belongs to a different list.",
"differentNamespace": "This task belongs to a different namespace.",
"noneYet": "No task relations yet.",
"delete": "Delete Task Relation",
"deleteText1": "Are you sure you want to delete this task relation?",
"deleteText2": "This cannot be undone!",
"select": "Select a relation kind",
"kinds": {
"subtask": "Subtask | Subtasks",
"parenttask": "Parent Task | Parent Tasks",
"related": "Related Task | Related Tasks",
"duplicateof": "Duplicate Of | Duplicates Of",
"duplicates": "Duplicates | Duplicates",
"blocking": "Blocking | Blocking",
"blocked": "Blocked By | Blocked By",
"precedes": "Precedes | Precedes",
"follows": "Follows | Follows",
"copiedfrom": "Copied From | Copied From",
"copiedto": "Copied To | Copied To"
}
},
"repeat": {
"everyDay": "Every Day",
"everyWeek": "Every Week",
"everyMonth": "Every Month",
"mode": "Repeat mode",
"monthly": "Monthly",
"fromCurrentDate": "From Current Date",
"each": "Each",
"specifyAmount": "Specify an amount…",
"hours": "Hours",
"days": "Days",
"weeks": "Weeks",
"months": "Months",
"years": "Years"
},
"quickAddMagic": {
"hint": "You can use Quick Add Magic",
"what": "What?",
"title": "Quick Add Magic",
"intro": "When creating a task, you can use special keywords to directly add attributes to the newly created task. This allows to add commonly used attributes to tasks much faster.",
"multiple": "You can use this multiple times.",
"label1": "To add a label, simply prefix the name of the label with {prefix}.",
"label2": "Vikunja will first check if the label already exist and create it if not.",
"label3": "To use spaces, simply add a \" around the label name.",
"label4": "For example: {prefix}\"Label with spaces\".",
"priority1": "To set a task's priority, add a number 1-5, prefixed with a {prefix}.",
"priority2": "The higher the number, the higher the priority.",
"assignees": "To directly assign the task to a user, add their username prefixed with {prefix} to the task.",
"list1": "To set a list for the task to appear in, enter its name prefixed with {prefix}.",
"list2": "This will return an error if the list does not exist.",
"dateAndTime": "Date and time",
"date": "Any date will be used as the due date of the new task. You can use dates in any of these formats:",
"dateWeekday": "any weekday, will use the next date with that date",
"dateCurrentYear": "will use the current year",
"dateNth": "will use the {day}th of the current month",
"dateTime": "Combine any of the date formats with \"{time}\" (or {timePM}) to set a time."
}
},
"team": {
"title": "Teams",
"noTeams": "You are currently not part of any teams.",
"create": {
"title": "Create a new team",
"success": "The team was successfully created."
},
"edit": {
"title": "Edit Team \"{team}\"",
"members": "Team Members",
"search": "Type to search a user…",
"addUser": "Add to team",
"makeMember": "Make Member",
"makeAdmin": "Make Admin",
"success": "The team was successfully updated.",
"userAddedSuccess": "The team member was successfully added.",
"madeMember": "The team member was successfully made member.",
"madeAdmin": "The team member was successfully made admin.",
"delete": {
"header": "Delete the team",
"text1": "Are you sure you want to delete this team and all of its members?",
"text2": "All team members will lose access to lists and namespaces shared with this team. This CANNOT BE UNDONE!",
"success": "The team was successfully deleted."
},
"deleteUser": {
"header": "Remove a user from the team",
"text1": "Are you sure you want to remove this user from the team?",
"text2": "They will lose access to all lists and namespaces this team has access to. This CANNOT BE UNDONE!",
"success": "The user was successfully deleted from the team."
}
},
"attributes": {
"name": "Team Name",
"namePlaceholder": "The team's name goes here…",
"nameRequired": "Please specify a name.",
"description": "Description",
"descriptionPlaceholder": "The teams description goes here…",
"admin": "Admin",
"member": "Member"
}
},
"keyboardShortcuts": {
"title": "Keyboard Shortcuts",
"general": "General",
"allPages": "These shortcuts work on all pages.",
"currentPageOnly": "These shortcuts work only on the current page.",
"toggleMenu": "Toggle The Menu",
"quickSearch": "Open the search/quick action bar",
"then": "then",
"task": {
"title": "Task Page",
"done": "Mark a task as done",
"assign": "Assign to a user",
"labels": "Add labels to this task",
"dueDate": "Change the due date of this task",
"attachment": "Add an attachment to this task",
"related": "Modify related tasks of this task"
},
"list": {
"title": "List Views",
"switchToListView": "Switch to list view",
"switchToGanttView": "Switch to gantt view",
"switchToKanbanView": "Switch to kanban view",
"switchToTableView": "Switch to table view"
}
},
"update": {
"available": "There is an update for Vikunja available!",
"do": "Update Now"
},
"menu": {
"edit": "Edit",
"archive": "Archive",
"duplicate": "Duplicate",
"delete": "Delete",
"unarchive": "Un-Archive",
"setBackground": "Set background",
"share": "Share",
"newList": "New list"
},
"apiConfig": {
"url": "Vikunja URL",
"urlPlaceholder": "eg. https://localhost:3456",
"change": "change",
"signInOn": "Sign in to your Vikunja account on {0}",
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
"success": "Using Vikunja installation at \"{domain}\".",
"urlRequired": "A url is required."
},
"loadingError": {
"failed": "Loading failed, please {0}. If the error persists, please {1}.",
"tryAgain": "try again",
"contact": "contact us"
},
"notification": {
"title": "Notifications",
"none": "You don't have any notifications. Have a nice day!",
"explainer": "Notifications will appear here when actions on namespaces, lists or tasks you subscribed to happen."
},
"quickActions": {
"commands": "Commands",
"placeholder": "Type a command or search…",
"hint": "You can use {list} to limit the search to a list. Combine {list} or {label} (labels) with a search query to search for a task with these labels or on that list. Use {assignee} to only search for teams.",
"tasks": "Tasks",
"lists": "Lists",
"teams": "Teams",
"newList": "Enter the title of the new list…",
"newTask": "Enter the title of the new task…",
"newNamespace": "Enter the title of the new namespace…",
"newTeam": "Enter the name of the new team…",
"createTask": "Create a task in the current list ({title})",
"createList": "Create a list in the current namespace ({title})",
"cmds": {
"newTask": "New task",
"newList": "New list",
"newNamespace": "New namespace",
"newTeam": "New team"
}
},
"date": {
"locale": "en",
"altFormatLong": "j M Y H:i",
"altFormatShort": "j M Y"
},
"error": {
"error": "Error",
"success": "Success",
"0001": "You're not allowed to do that.",
"1001": "A user with this username already exists.",
"1002": "A user with this email address already exists.",
"1004": "No username and password specified.",
"1005": "The user does not exist.",
"1006": "Could not get the user id.",
"1008": "No password reset token provided.",
"1009": "Invalid password reset token.",
"1010": "Invalid email confirm token.",
"1011": "Wrong username or password.",
"1012": "Email address of the user not confirmed.",
"1013": "New password is empty.",
"1014": "Old password is empty.",
"1015": "Totp is already enabled for this user.",
"1016": "Totp is not enabled for this user.",
"1017": "The totp passcode is invalid.",
"1018": "The user avatar type setting is invalid.",
"2001": "ID cannot be empty or 0.",
"2002": "Some of the request data was invalid.",
"3001": "The list does not exist.",
"3004": "You need to have read permissions on that list to perform that action.",
"3005": "The list title cannot be empty.",
"3006": "The list share does not exist.",
"3007": "A list with this identifier already exists.",
"3008": "The list is archived and can therefore only be accessed read only. This is also true for all tasks associated with this list.",
"4001": "The list task text cannot be empty.",
"4002": "The list task does not exist.",
"4003": "All bulk editing tasks must belong to the same list.",
"4004": "Need at least one task when bulk editing tasks.",
"4005": "You do not have the right to see the task.",
"4006": "You can't set a parent task as the task itself.",
"4007": "You can't create a task relation with an invalid kind of relation.",
"4008": "You can't create a task relation which already exists.",
"4009": "The task relation does not exist.",
"4010": "Cannot relate a task with itself.",
"4011": "The task attachment does not exist.",
"4012": "The task attachment is too large.",
"4013": "The task sort param is invalid.",
"4014": "The task sort order is invalid.",
"4015": "The task comment does not exist.",
"4016": "Invalid task field.",
"4017": "Invalid task filter comparator.",
"4018": "Invalid task filter concatinator.",
"4019": "Invalid task filter value.",
"5001": "The namespace does not exist.",
"5003": "You do not have access to the specified namespace.",
"5006": "The namespace name cannot be empty.",
"5009": "You need to have namespace read access to perform that action.",
"5010": "This team does not have access to that namespace.",
"5011": "This user has already access to that namespace.",
"5012": "The namespace is archived and can therefore only be accessed read only.",
"6001": "The team name cannot be empty.",
"6002": "The team does not exist.",
"6004": "The team already has access to that namespace or list.",
"6005": "The user is already a member of that team.",
"6006": "Cannot delete the last team member.",
"6007": "The team does not have access to the list to perform that action.",
"7002": "The user already has access to that list.",
"7003": "You do not have access to that list.",
"8001": "This label already exists on that task.",
"8002": "The label does not exist.",
"8003": "You do not have access to this label.",
"9001": "The right is invalid.",
"10001": "The bucket does not exist.",
"10002": "The bucket does not belong to that list.",
"10003": "You cannot remove the last bucket on a list.",
"10004": "You cannot add the task to this bucket as it already exceeded the limit of tasks it can hold.",
"10005": "There can be only one done bucket per list.",
"11001": "The saved filter does not exist.",
"11002": "Saved filters are not available for link shares.",
"12001": "The subscription entity type is invalid.",
"12002": "You are already subscribed to the entity itself or a parent entity.",
"13001": "This link share requires a password for authentication, but none was provided.",
"13002": "The provided link share password was invalid."
},
"about": {
"title": "About",
"frontendVersion": "Frontend Version: {version}",
"apiVersion": "API Version: {version}"
}
}

View File

@ -901,7 +901,7 @@
"5010": "This team does not have access to that namespace.",
"5011": "This user has already access to that namespace.",
"5012": "The namespace is archived and can therefore only be accessed read only.",
"6001": "The team name cannot be emtpy.",
"6001": "The team name cannot be empty.",
"6002": "The team does not exist.",
"6004": "The team already has access to that namespace or list.",
"6005": "The user is already a member of that team.",

View File

@ -901,7 +901,7 @@
"5010": "Team này không có quyền bước vào góc làm việc đó.",
"5011": "Người này đã có quyền bước vào góc làm việc đó.",
"5012": "Góc làm việc đã được lưu trữ nên chỉ có thể vào đó để đọc.",
"6001": "Team cũng cần có tên gọi mà.",
"6001": "The team name cannot be empty.",
"6002": "Team không tồn tại.",
"6004": "Team đã có quyền bước vào góc làm việc và xem danh sách.",
"6005": "Người này đã là thành viên của Team đó rồi.",

View File

@ -19,14 +19,12 @@ $mobile: math.div($tablet, 2);
$family-sans-serif: 'Open Sans', Helvetica, Arial, sans-serif;
$vikunja-font: 'Quicksand', sans-serif;
$thickness: 1px;
$pagination-current-border: var(--primary);
$navbar-item-active-color: var(--primary);
$dropdown-content-shadow: none;
$dropdown-item-hover-background-color: var(--grey-100);
$bulmaswatch-import-font: false !default;
$site-background: var(--grey-100);
$transition-duration: 150ms;

View File

@ -23,7 +23,7 @@
@import "bulma-css-variables/sass/elements/content";
@import "bulma-css-variables/sass/elements/icon";
@import "bulma-css-variables/sass/elements/image";
@import "bulma-css-variables/sass/elements/notification";
//@import "bulma-css-variables/sass/elements/notification"; // not used
@import "bulma-css-variables/sass/elements/progress";
@import "bulma-css-variables/sass/elements/table";
@import "bulma-css-variables/sass/elements/tag";
@ -48,7 +48,7 @@
// @import "bulma-css-variables/sass/components/level"; // not used
@import "bulma-css-variables/sass/components/media";
@import "bulma-css-variables/sass/components/menu";
@import "bulma-css-variables/sass/components/message";
//@import "bulma-css-variables/sass/components/message"; // not used
@import "bulma-css-variables/sass/components/modal";
@import "bulma-css-variables/sass/components/navbar";
@import "bulma-css-variables/sass/components/pagination";

View File

@ -7,5 +7,4 @@
@import "form";
@import "link-share";
@import "loading";
@import "notification";
@import "flatpickr";

View File

@ -40,6 +40,7 @@
}
.select select {
$thickness: 1px;
border-width: $thickness;
&:not([multiple]) {

View File

@ -1,12 +0,0 @@
.notification {
border: $thickness solid $border;
}
.notifications {
left: 0.5rem !important;
bottom: 1rem !important;
}
.message .message-body {
border: $thickness solid;
}

View File

@ -3,7 +3,7 @@
<h2 v-if="userInfo">
{{ $t(`home.welcome${welcome}`, {username: userInfo.name !== '' ? userInfo.name : userInfo.username}) }}!
</h2>
<div class="notification is-danger" v-if="deletionScheduledAt !== null">
<message variant="danger" v-if="deletionScheduledAt !== null">
{{
$t('user.deletion.scheduled', {
date: formatDateShort(deletionScheduledAt),
@ -13,7 +13,7 @@
<router-link :to="{name: 'user.settings', hash: '#deletion'}">
{{ $t('user.deletion.scheduledCancel') }}
</router-link>
</div>
</message>
<add-task
:listId="defaultListId"
@taskAdded="updateTaskList"
@ -55,90 +55,70 @@
</div>
</template>
<script>
import {mapState} from 'vuex'
import ShowTasks from './tasks/ShowTasks.vue'
import {getHistory} from '../modules/listHistory'
<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'
import ListCard from '@/components/list/partials/list-card.vue'
import AddTask from '../components/tasks/add-task.vue'
import {LOADING, LOADING_MODULE} from '../store/mutation-types'
import {parseDateOrNull} from '../helpers/parseDateOrNull'
import AddTask from '@/components/tasks/add-task.vue'
export default {
name: 'Home',
components: {
ListCard,
ShowTasks,
AddTask,
},
data() {
return {
currentDate: new Date(),
tasks: [],
showTasksKey: 0,
}
},
computed: {
welcome() {
const now = new Date()
import {getHistory} from '@/modules/listHistory'
import {parseDateOrNull} from '@/helpers/parseDateOrNull'
import {formatDateShort, formatDateSince} from '@/helpers/time/formatDate'
if (now.getHours() < 5) {
return 'Night'
}
const now = useNow()
const welcome = computed(() => {
const hours = new Date(now.value).getHours()
if (now.getHours() < 11) {
return 'Morning'
}
if (hours < 5) {
return 'Night'
}
if (now.getHours() < 18) {
return 'Day'
}
if (hours < 11) {
return 'Morning'
}
if (now.getHours() < 23) {
return 'Evening'
}
if (hours < 18) {
return 'Day'
}
return 'Night'
},
listHistory() {
const history = getHistory()
return history.map(l => {
return this.$store.getters['lists/getListById'](l.id)
}).filter(l => l !== null)
},
...mapState({
migratorsEnabled: state =>
state.config.availableMigrators !== null &&
state.config.availableMigrators.length > 0,
authenticated: state => state.auth.authenticated,
userInfo: state => state.auth.info,
hasTasks: state => state.hasTasks,
defaultListId: state => state.auth.defaultListId,
defaultNamespaceId: state => {
if (state.namespaces.namespaces.length === 0) {
return 0
}
if (hours < 23) {
return 'Evening'
}
return state.namespaces.namespaces[0].id
},
hasLists: state => {
if (state.namespaces.namespaces.length === 0) {
return false
}
return 'Night'
})
return state.namespaces.namespaces[0].lists.length > 0
},
loading: state => state[LOADING] && state[LOADING_MODULE] === 'tasks',
deletionScheduledAt: state => parseDateOrNull(state.auth.info?.deletionScheduledAt),
}),
},
methods: {
// This is to reload the tasks list after adding a new task through the global task add.
// FIXME: Should use vuex (somehow?)
updateTaskList() {
this.showTasksKey++
},
},
const store = useStore()
const listHistory = computed(() => {
const history = getHistory()
return history.map(l => {
return store.getters['lists/getListById'](l.id)
}).filter(l => l !== null)
})
const migratorsEnabled = computed(() => store.state.config.availableMigrators?.length > 0)
const userInfo = computed(() => store.state.auth.info)
const hasTasks = computed(() => store.state.hasTasks)
const defaultListId = computed(() => store.state.auth.defaultListId)
const defaultNamespaceId = computed(() => store.state.namespaces.namespaces?.[0].id || 0)
const hasLists = computed (() => {
return store.state.namespaces.namespaces.length === 0
? false
: store.state.namespaces.namespaces[0].lists.length > 0
})
const loading = computed(() => store.state.loading && store.state.loadingModule === 'tasks')
const deletionScheduledAt = computed(() => parseDateOrNull(store.state.auth.info?.deletionScheduledAt))
// This is to reload the tasks list after adding a new task through the global task add.
// FIXME: Should use vuex (somehow?)
const showTasksKey = ref(0)
function updateTaskList() {
showTasksKey.value++
}
</script>
@ -147,7 +127,7 @@ export default {
flex-wrap: wrap;
max-height: calc(#{$list-height * 2} + #{$list-spacing * 2} - 4px);
overflow: hidden;
@media screen and (max-width: $mobile) {
max-height: calc(#{$list-height * 4} + #{$list-spacing * 4} - 4px);
}

View File

@ -36,9 +36,9 @@
</div>
</div>
<transition name="fade">
<div class="notification is-warning" v-if="currentList.isArchived">
<message variant="warning" v-if="currentList.isArchived" class="mb-4">
{{ $t('list.archived') }}
</div>
</message>
</transition>
<router-view/>
@ -46,6 +46,7 @@
</template>
<script>
import Message from '@/components/misc/message'
import ListModel from '../../models/list'
import ListService from '../../services/list'
import {CURRENT_LIST} from '../../store/mutation-types'
@ -53,6 +54,7 @@ import {getListView} from '../../helpers/saveListView'
import {saveListToHistory} from '../../modules/listHistory'
export default {
components: {Message},
data() {
return {
listService: new ListService(),

View File

@ -39,7 +39,7 @@
<div class="migration-in-progress">
<img :alt="migrator.name" :src="migrator.icon" class="logo"/>
<div class="progress-dots">
<span v-for="i in progressDotsCount" :key="i" />
<span v-for="i in progressDotsCount" :key="i"/>
</div>
<Logo class="logo"/>
</div>
@ -57,11 +57,9 @@
</div>
</div>
<div v-else>
<div class="message is-primary">
<div class="message-body">
{{ message }}
</div>
</div>
<message class="mb-4">
{{ message }}
</message>
<x-button :to="{name: 'home'}">{{ $t('misc.refresh') }}</x-button>
</div>
</div>
@ -71,6 +69,7 @@
import AbstractMigrationService from '@/services/migrator/abstractMigration'
import AbstractMigrationFileService from '@/services/migrator/abstractMigrationFile'
import Logo from '@/assets/logo.svg?component'
import Message from '@/components/misc/message'
import {MIGRATORS} from './migrators'
@ -79,7 +78,10 @@ const PROGRESS_DOTS_COUNT = 8
export default {
name: 'MigrateService',
components: { Logo },
components: {
Logo,
Message,
},
data() {
return {
@ -101,7 +103,7 @@ export default {
beforeRouteEnter(to) {
if (MIGRATORS[to.params.service] === undefined) {
return { name: 'not-found' }
return {name: 'not-found'}
}
},
@ -122,7 +124,7 @@ export default {
if (this.migrator.isFileMigrator) {
return
}
this.authUrl = await this.migrationService.getAuthUrl().then(({url}) => url)
this.migratorAuthCode = location.hash.startsWith('#token=')
@ -150,7 +152,7 @@ export default {
this.lastMigrationDate = null
this.message = ''
let migrationConfig = { code: this.migratorAuthCode }
let migrationConfig = {code: this.migratorAuthCode}
if (this.migrator.isFileMigrator) {
if (this.$refs.uploadInput.files.length === 0) {
@ -160,7 +162,7 @@ export default {
}
try {
const { message } = await this.migrationService.migrate(migrationConfig)
const {message} = await this.migrationService.migrate(migrationConfig)
this.message = message
return this.$store.dispatch('namespaces/loadNamespaces')
} finally {
@ -173,24 +175,24 @@ export default {
<style lang="scss" scoped>
.migration-in-progress-container {
max-width: 400px;
margin: 4rem auto 0;
text-align: center;
max-width: 400px;
margin: 4rem auto 0;
text-align: center;
}
.migration-in-progress {
text-align: center;
display: flex;
max-width: 400px;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
text-align: center;
display: flex;
max-width: 400px;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.logo {
display: block;
max-height: 100px;
max-width: 100px;
display: block;
max-height: 100px;
max-width: 100px;
}
.progress-dots {

View File

@ -28,19 +28,20 @@
<div class="field">
<label class="label">{{ $t('namespace.attributes.color') }}</label>
<div class="control">
<color-picker v-model="namespace.hexColor" />
<color-picker v-model="namespace.hexColor"/>
</div>
</div>
<div class="notification is-info mt-4">
<message class="mt-4">
<h4 class="title">{{ $t('namespace.create.tooltip') }}</h4>
{{ $t('namespace.create.explanation') }}
</div>
</message>
</create-edit>
</template>
<script>
import Message from '@/components/misc/message'
import NamespaceModel from '../../models/namespace'
import NamespaceService from '../../services/namespace'
import CreateEdit from '@/components/misc/create-edit.vue'
@ -56,6 +57,7 @@ export default {
}
},
components: {
Message,
ColorPicker,
CreateEdit,
},
@ -72,7 +74,7 @@ export default {
const namespace = await this.namespaceService.create(this.namespace)
this.$store.commit('namespaces/addNamespace', namespace)
this.$message.success({message: this.$t('namespace.create.success') })
this.$message.success({message: this.$t('namespace.create.success')})
this.$router.back()
},
},

View File

@ -1,8 +1,8 @@
<template>
<div>
<div class="notification is-info is-light has-text-centered" v-if="loading">
<message v-if="loading">
{{ $t('sharing.authenticating') }}
</div>
</message>
<div v-if="authenticateWithPassword" class="box">
<p class="pb-2">
{{ $t('sharing.passwordRequired') }}
@ -25,18 +25,20 @@
{{ $t('user.auth.login') }}
</x-button>
<div class="notification is-danger mt-4" v-if="errorMessage !== ''">
<message variant="danger" class="mt-4" v-if="errorMessage !== ''">
{{ errorMessage }}
</div>
</message>
</div>
</div>
</template>
<script>
import {mapGetters} from 'vuex'
import Message from '@/components/misc/message'
export default {
name: 'LinkSharingAuth',
components: {Message},
data() {
return {
loading: true,
@ -72,7 +74,7 @@ export default {
password: this.password,
})
this.$router.push({name: 'list.list', params: {listId: r.list_id}})
} catch(e) {
} catch (e) {
if (typeof e.response.data.code !== 'undefined' && e.response.data.code === 13001) {
this.authenticateWithPassword = true
return

View File

@ -486,7 +486,6 @@ export default {
taskColor: '',
showDeleteModal: false,
descriptionChanged: false,
// Used to avoid flashing of empty elements if the task content is not yet loaded.
visible: false,
@ -679,13 +678,6 @@ export default {
this.saveTask(true, this.toggleTaskDone)
},
setDescriptionChanged(e) {
if (e.key === 'Enter' || e.key === 'Control') {
return
}
this.descriptionChanged = true
},
async changeList(list) {
this.$store.commit('kanban/removeTaskInBucket', this.task)
this.task.listId = list.id

View File

@ -2,9 +2,9 @@
<div>
<h2 class="title has-text-centered">Login</h2>
<div class="box">
<div class="notification is-success has-text-centered" v-if="confirmedEmailSuccess">
<message variant="success" class="has-text-centered" v-if="confirmedEmailSuccess">
{{ $t('user.auth.confirmEmailSuccess') }}
</div>
</message>
<api-config @foundApi="hasApiUrl = true"/>
<form @submit.prevent="submit" id="loginform" v-if="hasApiUrl && localAuthEnabled">
<div class="field">
@ -78,9 +78,9 @@
</router-link>
</div>
</div>
<div class="notification is-danger" v-if="errorMessage">
<message variant="danger" v-if="errorMessage">
{{ errorMessage }}
</div>
</message>
</form>
<div
@ -110,11 +110,13 @@ import {LOADING} from '@/store/mutation-types'
import legal from '../../components/misc/legal'
import ApiConfig from '@/components/misc/api-config.vue'
import {getErrorText} from '@/message'
import Message from '@/components/misc/message'
import {redirectToProvider} from '../../helpers/redirectToProvider'
import {getLastVisited, clearLastVisited} from '../../helpers/saveLastVisited'
export default {
components: {
Message,
ApiConfig,
legal,
},
@ -149,7 +151,7 @@ export default {
const last = getLastVisited()
if (last !== null) {
this.$router.push({
name: last.name,
name: last.name,
params: last.params,
})
clearLastVisited()
@ -206,7 +208,7 @@ export default {
try {
await this.$store.dispatch('auth/login', credentials)
this.$store.commit('auth/needsTotpPasscode', false)
} catch(e) {
} catch (e) {
if (e.response && e.response.data.code === 1017 && !credentials.totpPasscode) {
return
}

View File

@ -1,11 +1,11 @@
<template>
<div>
<div class="notification is-danger" v-if="errorMessage">
<message variant="danger" v-if="errorMessage">
{{ errorMessage }}
</div>
<div class="notification is-info" v-if="loading">
</message>
<message v-if="loading">
{{ $t('user.auth.authenticating') }}
</div>
</message>
</div>
</template>
@ -14,10 +14,12 @@ import {mapState} from 'vuex'
import {LOADING} from '@/store/mutation-types'
import {getErrorText} from '@/message'
import Message from '@/components/misc/message'
import {clearLastVisited, getLastVisited} from '../../helpers/saveLastVisited'
export default {
name: 'Auth',
components: {Message},
data() {
return {
errorMessage: '',

View File

@ -45,37 +45,37 @@
</x-button>
</div>
</div>
<div class="notification is-info" v-if="this.passwordResetService.loading">
<message v-if="this.passwordResetService.loading">
{{ $t('misc.loading') }}
</div>
<div class="notification is-danger" v-if="errorMsg">
</message>
<message v-if="errorMsg">
{{ errorMsg }}
</div>
</message>
</form>
<div class="has-text-centered" v-if="successMessage">
<div class="notification is-success">
<message variant="success">
{{ successMessage }}
</div>
</message>
<x-button :to="{ name: 'user.login' }">
{{ $t('user.auth.login') }}
</x-button>
</div>
<Legal />
<Legal/>
</div>
</div>
</template>
<script setup>
import {ref, reactive} from 'vue'
import { useI18n } from 'vue-i18n'
import {useI18n} from 'vue-i18n'
import Legal from '@/components/misc/legal'
import PasswordResetModel from '@/models/passwordReset'
import PasswordResetService from '@/services/passwordReset'
import { useTitle } from '@/composables/useTitle'
import {useTitle} from '@/composables/useTitle'
import Message from '@/components/misc/message'
const { t } = useI18n()
const {t} = useI18n()
useTitle(() => t('user.auth.resetPassword'))
const credentials = reactive({
@ -97,10 +97,10 @@ async function submit() {
const passwordReset = new PasswordResetModel({newPassword: credentials.password})
try {
const { message } = passwordResetService.resetPassword(passwordReset)
const {message} = passwordResetService.resetPassword(passwordReset)
successMessage.value = message
localStorage.removeItem('passwordResetToken')
} catch(e) {
} catch (e) {
errorMsg.value = e.response.data.message
}
}

View File

@ -83,12 +83,12 @@
</x-button>
</div>
</div>
<div class="notification is-info" v-if="loading">
<message v-if="loading">
{{ $t('misc.loading') }}
</div>
<div class="notification is-danger" v-if="errorMessage !== ''">
</message>
<message variant="danger" v-if="errorMessage !== ''">
{{ errorMessage }}
</div>
</message>
</form>
<legal/>
</div>
@ -97,13 +97,13 @@
<script setup>
import {ref, reactive, toRaw, computed, onBeforeMount} from 'vue'
import { useI18n } from 'vue-i18n'
import {useI18n} from 'vue-i18n'
import router from '@/router'
import { store } from '@/store'
import { useTitle } from '@/composables/useTitle'
import {store} from '@/store'
import {useTitle} from '@/composables/useTitle'
import Legal from '@/components/misc/legal'
import Message from '@/components/misc/message'
// FIXME: use the `beforeEnter` hook of vue-router
// Check if the user is already logged in, if so, redirect them to the homepage
@ -113,7 +113,7 @@ onBeforeMount(() => {
}
})
const { t } = useI18n()
const {t} = useI18n()
useTitle(() => t('user.auth.register'))
const credentials = reactive({
@ -137,7 +137,7 @@ async function submit() {
try {
await store.dispatch('auth/register', toRaw(credentials))
} catch(e) {
} catch (e) {
errorMessage.value = e.message
}
}

View File

@ -31,14 +31,14 @@
</x-button>
</div>
</div>
<div class="notification is-danger" v-if="errorMsg">
<message variant="danger" v-if="errorMsg">
{{ errorMsg }}
</div>
</message>
</form>
<div class="has-text-centered" v-if="isSuccess">
<div class="notification is-success">
<message variant="success">
{{ $t('user.auth.resetPasswordSuccess') }}
</div>
</message>
<x-button :to="{ name: 'user.login' }">
{{ $t('user.auth.login') }}
</x-button>
@ -57,6 +57,7 @@ import Legal from '@/components/misc/legal'
import PasswordResetModel from '@/models/passwordReset'
import PasswordResetService from '@/services/passwordReset'
import { useTitle } from '@/composables/useTitle'
import Message from '@/components/misc/message'
const { t } = useI18n()
useTitle(() => t('user.auth.resetPassword'))

1700
yarn.lock

File diff suppressed because it is too large Load Diff