forked from vikunja/frontend
Compare commits
70 Commits
67e7b94f5d
...
a72cd31202
Author | SHA1 | Date | |
---|---|---|---|
a72cd31202 | |||
2141171529 | |||
9b050846a4 | |||
f6b8d2c4ca | |||
86119ff414 | |||
7c5622af11 | |||
6ac3ce65b4 | |||
07f9784e0d | |||
fb751236d1 | |||
396943b3a6 | |||
b8a19ac88a | |||
02fac73e07 | |||
21e52c3b1f | |||
213ef84586 | |||
25852ffac3 | |||
1255b50ed7 | |||
afe8198158 | |||
abe43c4ef8 | |||
8d5ddd695a | |||
a0087bc34a | |||
18a73c39e5 | |||
e0351495ff | |||
b97e045118 | |||
a052305adf | |||
237de35b25 | |||
85beebf0d8 | |||
e81b216f85 | |||
fc9e75e9b3 | |||
b576621569 | |||
08a031ca07 | |||
d306bb967c | |||
3f6a64d7f9 | |||
2d392c9973 | |||
9897bc9b43 | |||
92546f4b34 | |||
8d326aca03 | |||
4bf9284b38 | |||
0de9376b2b | |||
0602f6693b | |||
7d5cde53e3 | |||
8361640559 | |||
517a6cea1e | |||
ed4dd93bba | |||
d50de97490 | |||
aa719d3a68 | |||
463d22b07c | |||
33494cab6b | |||
8fa922a0ca | |||
e5815e21cb | |||
529b47e488 | |||
63c3e4ea58 | |||
d52e917357 | |||
b2da4fd126 | |||
83fb8c3ded | |||
|
b44d11cfc0 | ||
d4133b9e78 | |||
c478926038 | |||
00e40a0f53 | |||
0567ba2a47 | |||
|
3b95824f58 | ||
963f3bfb07 | |||
d1c05eb3fb | |||
2326e50d5d | |||
b7fa1a3ca1 | |||
a3e1e43ec7 | |||
39f163df4a | |||
f0e8ff93ff | |||
|
3ee0bc345d | ||
b4ffee8929 | |||
e3c3d3ee53 |
60
package.json
60
package.json
|
@ -24,21 +24,21 @@
|
|||
"@fortawesome/vue-fontawesome": "3.0.2",
|
||||
"@github/hotkey": "2.0.1",
|
||||
"@infectoone/vue-ganttastic": "2.1.3",
|
||||
"@kyvg/vue3-notification": "2.6.1",
|
||||
"@sentry/tracing": "7.21.1",
|
||||
"@sentry/vue": "7.21.1",
|
||||
"@intlify/unplugin-vue-i18n": "0.8.0",
|
||||
"@kyvg/vue3-notification": "2.7.0",
|
||||
"@sentry/tracing": "7.24.2",
|
||||
"@sentry/vue": "7.24.2",
|
||||
"@types/is-touch-device": "1.0.0",
|
||||
"@types/lodash.clonedeep": "4.5.7",
|
||||
"@types/sortablejs": "1.15.0",
|
||||
"@vueuse/core": "9.6.0",
|
||||
"@vueuse/router": "9.6.0",
|
||||
"axios": "0.27.2",
|
||||
"blurhash": "2.0.4",
|
||||
"bulma-css-variables": "0.9.33",
|
||||
"camel-case": "4.1.2",
|
||||
"codemirror": "5.65.10",
|
||||
"date-fns": "2.29.3",
|
||||
"dayjs": "1.11.6",
|
||||
"dayjs": "1.11.7",
|
||||
"dompurify": "2.4.1",
|
||||
"easymde": "2.18.0",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
|
@ -49,13 +49,13 @@
|
|||
"is-touch-device": "1.0.1",
|
||||
"lodash.clonedeep": "4.5.0",
|
||||
"lodash.debounce": "4.0.8",
|
||||
"marked": "4.2.3",
|
||||
"marked": "4.2.4",
|
||||
"minimist": "1.2.7",
|
||||
"pinia": "2.0.26",
|
||||
"pinia": "2.0.28",
|
||||
"register-service-worker": "1.7.2",
|
||||
"snake-case": "3.0.4",
|
||||
"sortablejs": "1.15.0",
|
||||
"ufo": "1.0.0",
|
||||
"ufo": "1.0.1",
|
||||
"vue": "3.2.45",
|
||||
"vue-advanced-cropper": "2.8.6",
|
||||
"vue-flatpickr-component": "11.0.1",
|
||||
|
@ -66,49 +66,49 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@4tw/cypress-drag-drop": "2.2.2",
|
||||
"@cypress/vite-dev-server": "4.0.1",
|
||||
"@cypress/vue": "5.0.2",
|
||||
"@cypress/vite-dev-server": "5.0.2",
|
||||
"@cypress/vue": "5.0.3",
|
||||
"@faker-js/faker": "7.6.0",
|
||||
"@rushstack/eslint-patch": "1.2.0",
|
||||
"@types/codemirror": "5.60.5",
|
||||
"@types/dompurify": "2.4.0",
|
||||
"@types/flexsearch": "0.7.3",
|
||||
"@types/lodash.debounce": "4.0.7",
|
||||
"@types/marked": "4.0.7",
|
||||
"@types/node": "18.11.9",
|
||||
"@types/marked": "4.0.8",
|
||||
"@types/node": "18.11.13",
|
||||
"@types/postcss-preset-env": "7.7.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.44.0",
|
||||
"@typescript-eslint/parser": "5.44.0",
|
||||
"@vitejs/plugin-legacy": "2.3.1",
|
||||
"@vitejs/plugin-vue": "3.2.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.46.0",
|
||||
"@typescript-eslint/parser": "5.46.0",
|
||||
"@vitejs/plugin-legacy": "3.0.1",
|
||||
"@vitejs/plugin-vue": "4.0.0",
|
||||
"@vue/eslint-config-typescript": "11.0.2",
|
||||
"@vue/test-utils": "2.2.4",
|
||||
"@vue/test-utils": "2.2.6",
|
||||
"@vue/tsconfig": "0.1.3",
|
||||
"autoprefixer": "10.4.13",
|
||||
"browserslist": "4.21.4",
|
||||
"caniuse-lite": "1.0.30001431",
|
||||
"caniuse-lite": "1.0.30001436",
|
||||
"csstype": "3.1.1",
|
||||
"cypress": "11.2.0",
|
||||
"esbuild": "0.15.16",
|
||||
"eslint": "8.28.0",
|
||||
"cypress": "12.0.2",
|
||||
"esbuild": "0.16.4",
|
||||
"eslint": "8.29.0",
|
||||
"eslint-plugin-vue": "9.8.0",
|
||||
"express": "4.18.2",
|
||||
"happy-dom": "7.7.0",
|
||||
"netlify-cli": "12.2.4",
|
||||
"happy-dom": "7.7.2",
|
||||
"netlify-cli": "12.2.8",
|
||||
"postcss": "8.4.19",
|
||||
"postcss-preset-env": "7.8.3",
|
||||
"rollup": "3.5.0",
|
||||
"rollup": "3.7.2",
|
||||
"rollup-plugin-visualizer": "5.8.3",
|
||||
"sass": "1.56.1",
|
||||
"typescript": "4.9.3",
|
||||
"vite": "3.2.4",
|
||||
"sass": "1.56.2",
|
||||
"typescript": "4.9.4",
|
||||
"vite": "3.2.5",
|
||||
"vite-plugin-pwa": "0.13.3",
|
||||
"vite-svg-loader": "3.6.0",
|
||||
"vitest": "0.25.3",
|
||||
"vue-tsc": "1.0.9",
|
||||
"vitest": "0.25.7",
|
||||
"vue-tsc": "1.0.12",
|
||||
"wait-on": "6.0.1",
|
||||
"workbox-cli": "6.5.4"
|
||||
},
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"packageManager": "pnpm@7.17.1"
|
||||
"packageManager": "pnpm@7.18.1"
|
||||
}
|
||||
|
|
1405
pnpm-lock.yaml
1405
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
18
src/App.vue
18
src/App.vue
|
@ -15,9 +15,8 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {computed, watch, type Ref} from 'vue'
|
||||
import {useRouter} from 'vue-router'
|
||||
import {useRouteQuery} from '@vueuse/router'
|
||||
import {computed, watch} from 'vue'
|
||||
import {useRoute, useRouter} from 'vue-router'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import isTouchDevice from 'is-touch-device'
|
||||
import {success} from '@/message'
|
||||
|
@ -41,6 +40,7 @@ import {useAuthStore} from './stores/auth'
|
|||
const baseStore = useBaseStore()
|
||||
const authStore = useAuthStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
useBodyClass('is-touch', isTouchDevice())
|
||||
const keyboardShortcutsActive = computed(() => baseStore.keyboardShortcutsActive)
|
||||
|
@ -51,9 +51,9 @@ const authLinkShare = computed(() => authStore.authLinkShare)
|
|||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
// setup account deletion verification
|
||||
const accountDeletionConfirm = useRouteQuery('accountDeletionConfirm') as Ref<null | string>
|
||||
const accountDeletionConfirm = computed(() => route.query?.accountDeletionConfirm as (string | undefined))
|
||||
watch(accountDeletionConfirm, async (accountDeletionConfirm) => {
|
||||
if (accountDeletionConfirm === null) {
|
||||
if (accountDeletionConfirm === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -64,9 +64,9 @@ watch(accountDeletionConfirm, async (accountDeletionConfirm) => {
|
|||
}, { immediate: true })
|
||||
|
||||
// setup password reset redirect
|
||||
const userPasswordReset = useRouteQuery('userPasswordReset') as Ref<null | string>
|
||||
const userPasswordReset = computed(() => route.query?.userPasswordReset as (string | undefined))
|
||||
watch(userPasswordReset, (userPasswordReset) => {
|
||||
if (userPasswordReset === null) {
|
||||
if (userPasswordReset === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -75,9 +75,9 @@ watch(userPasswordReset, (userPasswordReset) => {
|
|||
}, { immediate: true })
|
||||
|
||||
// setup email verification redirect
|
||||
const userEmailConfirm = useRouteQuery('userEmailConfirm') as Ref<null | string>
|
||||
const userEmailConfirm = computed(() => route.query?.userEmailConfirm as (string | undefined))
|
||||
watch(userEmailConfirm, (userEmailConfirm) => {
|
||||
if (userEmailConfirm === null) {
|
||||
if (userEmailConfirm === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
<script setup lang="ts">
|
||||
import {computed} from 'vue'
|
||||
import {useNow} from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
import { useNow } from '@vueuse/core'
|
||||
|
||||
import LogoFull from '@/assets/logo-full.svg?url'
|
||||
import LogoFullPride from '@/assets/logo-full-pride.svg?url'
|
||||
import LogoFull from '@/assets/logo-full.svg?component'
|
||||
import LogoFullPride from '@/assets/logo-full-pride.svg?component'
|
||||
|
||||
const now = useNow()
|
||||
const logoUrl = computed(() => now.value.getMonth() === 5 ? LogoFullPride : LogoFull)
|
||||
const Logo = computed(() => now.value.getMonth() === 5 ? LogoFullPride : LogoFull)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<img alt="Vikunja" :src="logoUrl" class="logo" />
|
||||
<Logo alt="Vikunja" class="logo" />
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<Loading
|
||||
v-if="props.isLoading && tasks.size || dayjsLanguageLoading"
|
||||
v-if="props.isLoading && !ganttBars.length || dayjsLanguageLoading"
|
||||
class="gantt-container"
|
||||
/>
|
||||
<div class="gantt-container" v-else>
|
||||
|
@ -16,14 +16,14 @@
|
|||
@dblclick-bar="openTask"
|
||||
:width="ganttChartWidth + 'px'"
|
||||
>
|
||||
<template #timeunit="{label, value}">
|
||||
<template #timeunit="{value, date}">
|
||||
<div
|
||||
class="timeunit-wrapper"
|
||||
:class="{'today': dayIsToday(label)}"
|
||||
:class="{'today': dateIsToday(date)}"
|
||||
>
|
||||
<span>{{ value }}</span>
|
||||
<span class="weekday">
|
||||
{{ weekdayFromTimeLabel(label) }}
|
||||
{{ weekDayFromDate(date) }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -40,9 +40,7 @@
|
|||
<script setup lang="ts">
|
||||
import {computed, ref, watch, toRefs} from 'vue'
|
||||
import {useRouter} from 'vue-router'
|
||||
import {format, parse} from 'date-fns'
|
||||
import dayjs from 'dayjs'
|
||||
import isToday from 'dayjs/plugin/isToday'
|
||||
import {useNow} from '@vueuse/core'
|
||||
|
||||
import {getHexColor} from '@/models/task'
|
||||
|
||||
|
@ -63,6 +61,7 @@ import {
|
|||
|
||||
import Loading from '@/components/misc/loading.vue'
|
||||
import {MILLISECONDS_A_DAY} from '@/constants/date'
|
||||
import {useWeekDayFromDate} from '@/helpers/time/formatDate'
|
||||
|
||||
export interface GanttChartProps {
|
||||
isLoading: boolean,
|
||||
|
@ -85,7 +84,6 @@ const {tasks, filters} = toRefs(props)
|
|||
// setup dayjs for vue-ganttastic
|
||||
const dayjsLanguageLoading = ref(false)
|
||||
// const dayjsLanguageLoading = useDayjsLanguageSync(dayjs)
|
||||
dayjs.extend(isToday)
|
||||
extendDayjs()
|
||||
|
||||
const router = useRouter()
|
||||
|
@ -157,23 +155,16 @@ function openTask(e: {
|
|||
})
|
||||
}
|
||||
|
||||
function parseTimeLabel(label: string) {
|
||||
return parse(label, 'dd.MMM', dateFromDate.value)
|
||||
}
|
||||
const weekDayFromDate = useWeekDayFromDate()
|
||||
|
||||
function weekdayFromTimeLabel(label: string): string {
|
||||
const parsed = parseTimeLabel(label)
|
||||
return format(parsed, 'E')
|
||||
}
|
||||
|
||||
function dayIsToday(label: string): boolean {
|
||||
const parsed = parseTimeLabel(label)
|
||||
|
||||
const today = new Date()
|
||||
return parsed.getDate() === today.getDate() &&
|
||||
parsed.getMonth() === today.getMonth() &&
|
||||
parsed.getFullYear() === today.getFullYear()
|
||||
}
|
||||
const today = useNow()
|
||||
const dateIsToday = computed(() => (date: Date) => {
|
||||
return (
|
||||
date.getDate() === today.value.getDate() &&
|
||||
date.getMonth() === today.value.getMonth() &&
|
||||
date.getFullYear() === today.value.getFullYear()
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
|
|
@ -53,6 +53,7 @@ import {RELATION_KIND} from '@/types/IRelationKind'
|
|||
import {useAuthStore} from '@/stores/auth'
|
||||
import {useTaskStore} from '@/stores/tasks'
|
||||
import {useAutoHeightTextarea} from '@/composables/useAutoHeightTextarea'
|
||||
import {getLabelsFromPrefix} from '@/modules/parseTaskText'
|
||||
|
||||
const props = defineProps({
|
||||
defaultPosition: {
|
||||
|
@ -82,6 +83,7 @@ function resetEmptyTitleError(e) {
|
|||
}
|
||||
|
||||
const loading = computed(() => taskStore.isLoading)
|
||||
|
||||
async function addTask() {
|
||||
if (newTaskTitle.value === '') {
|
||||
errorMessage.value = t('list.create.addTitleRequired')
|
||||
|
@ -98,14 +100,27 @@ async function addTask() {
|
|||
// by quick add magic.
|
||||
const createdTasks: { [key: ITask['title']]: ITask } = {}
|
||||
const tasksToCreate = parseSubtasksViaIndention(newTaskTitle.value)
|
||||
const newTasks = tasksToCreate.map(async ({title}) => {
|
||||
|
||||
// We ensure all labels exist prior to passing them down to the create task method
|
||||
// In the store it will only ever see one task at a time so there's no way to reliably
|
||||
// check if a new label was created before (because everything happens async).
|
||||
const allLabels = tasksToCreate.map(({title}) => getLabelsFromPrefix(title) ?? [])
|
||||
await taskStore.ensureLabelsExist(allLabels.flat())
|
||||
|
||||
const newTasks = tasksToCreate.map(async ({title, list}) => {
|
||||
if (title === '') {
|
||||
return
|
||||
}
|
||||
|
||||
// If the task has a list specified, make sure to use it
|
||||
let listId = null
|
||||
if (list !== null) {
|
||||
listId = await taskStore.findListId({list, listId: 0})
|
||||
}
|
||||
|
||||
const task = await taskStore.createNewTask({
|
||||
title,
|
||||
listId: authStore.settings.defaultListId,
|
||||
listId: listId || authStore.settings.defaultListId,
|
||||
position: props.defaultPosition,
|
||||
})
|
||||
createdTasks[title] = task
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
<template>
|
||||
<div class="heading">
|
||||
<BaseButton @click="copyUrl"><h1 class="title task-id">{{ textIdentifier }}</h1></BaseButton>
|
||||
<Done class="heading__done" :is-done="task.done"/>
|
||||
<ColorBubble
|
||||
v-if="task.hexColor !== ''"
|
||||
:color="getHexColor(task.hexColor)"
|
||||
class="mt-1 ml-2"
|
||||
/>
|
||||
<div class="flex is-align-items-center">
|
||||
<BaseButton @click="copyUrl"><h1 class="title task-id">{{ textIdentifier }}</h1></BaseButton>
|
||||
<Done class="heading__done" :is-done="task.done"/>
|
||||
<ColorBubble
|
||||
v-if="task.hexColor !== ''"
|
||||
:color="getHexColor(task.hexColor)"
|
||||
class="ml-2"
|
||||
/>
|
||||
</div>
|
||||
<h1
|
||||
class="title input"
|
||||
:class="{'disabled': !canWrite}"
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
<router-link
|
||||
:to="taskDetailRoute"
|
||||
:class="{ 'done': task.done}"
|
||||
:class="{ 'done': task.done, 'show-list': showList && taskList !== null}"
|
||||
class="tasktext"
|
||||
>
|
||||
<span>
|
||||
|
@ -391,6 +391,10 @@ function hideDeferDueDatePopup(e) {
|
|||
width: auto;
|
||||
}
|
||||
|
||||
.show-list .parent-tasks {
|
||||
padding-left: .25rem;
|
||||
}
|
||||
|
||||
.remove {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
|
|
@ -106,4 +106,15 @@ task two`)
|
|||
expect(tasks).to.have.length(1)
|
||||
expect(tasks[0].parent).toBeNull()
|
||||
})
|
||||
it('Should add the list of the parent task as list for all sub tasks', () => {
|
||||
const tasks = parseSubtasksViaIndention(
|
||||
`parent task +list
|
||||
sub task 1
|
||||
sub task 2`)
|
||||
|
||||
expect(tasks).to.have.length(3)
|
||||
expect(tasks[0].list).to.eq('list')
|
||||
expect(tasks[1].list).to.eq('list')
|
||||
expect(tasks[2].list).to.eq('list')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import {getListFromPrefix} from '@/modules/parseTaskText'
|
||||
|
||||
export interface TaskWithParent {
|
||||
title: string,
|
||||
parent: string | null,
|
||||
list: string | null,
|
||||
}
|
||||
|
||||
function cleanupTitle(title: string) {
|
||||
|
@ -20,8 +23,11 @@ export function parseSubtasksViaIndention(taskTitles: string): TaskWithParent[]
|
|||
const task: TaskWithParent = {
|
||||
title: cleanupTitle(title),
|
||||
parent: null,
|
||||
list: null,
|
||||
}
|
||||
|
||||
task.list = getListFromPrefix(task.title)
|
||||
|
||||
if (index === 0) {
|
||||
return task
|
||||
}
|
||||
|
@ -41,6 +47,10 @@ export function parseSubtasksViaIndention(taskTitles: string): TaskWithParent[]
|
|||
} while (parentSpaces >= matchedSpaces)
|
||||
task.title = cleanupTitle(title.replace(spaceRegex, ''))
|
||||
task.parent = task.parent.replace(spaceRegex, '')
|
||||
if (task.list === null) {
|
||||
// This allows to specify a list once for the parent task and inherit it to all subtasks
|
||||
task.list = getListFromPrefix(task.parent)
|
||||
}
|
||||
}
|
||||
|
||||
return task
|
||||
|
|
|
@ -5,6 +5,8 @@ import {format, formatDistanceToNow, formatISO as formatISOfns} from 'date-fns'
|
|||
import {enGB, de, fr, ru} from 'date-fns/locale'
|
||||
|
||||
import {i18n} from '@/i18n'
|
||||
import { createSharedComposable, type MaybeRef } from '@vueuse/core'
|
||||
import { computed, unref } from 'vue'
|
||||
|
||||
const locales = {en: enGB, de, ch: de, fr, ru}
|
||||
|
||||
|
@ -50,3 +52,17 @@ export const formatDateSince = (date) => {
|
|||
export function formatISO(date) {
|
||||
return date ? formatISOfns(date) : ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Because `Intl.DateTimeFormat` is expensive to instatiate we try to reuse it as often as possible,
|
||||
* by creating a shared composable.
|
||||
*/
|
||||
export const useDateTimeFormatter = createSharedComposable((options?: MaybeRef<Intl.DateTimeFormatOptions>) => {
|
||||
return computed(() => new Intl.DateTimeFormat(i18n.global.locale.value, unref(options)))
|
||||
})
|
||||
|
||||
export function useWeekDayFromDate() {
|
||||
const dateTimeFormatter = useDateTimeFormatter({ weekday: 'short' })
|
||||
|
||||
return computed(() => (date: Date) => dateTimeFormatter.value.format(date))
|
||||
}
|
|
@ -22,31 +22,22 @@ export const DEFAULT_LANGUAGE: SupportedLocale= 'en'
|
|||
|
||||
export type ISOLanguage = string
|
||||
|
||||
// we load all messsages async
|
||||
export const i18n = createI18n({
|
||||
locale: DEFAULT_LANGUAGE, // set locale
|
||||
fallbackLocale: DEFAULT_LANGUAGE,
|
||||
legacy: true,
|
||||
globalInjection: true,
|
||||
allowComposition: true,
|
||||
inheritLocale: true,
|
||||
legacy: false,
|
||||
messages: {
|
||||
en: langEN,
|
||||
} as Record<SupportedLocale, any>,
|
||||
})
|
||||
|
||||
function setI18nLanguage(lang: SupportedLocale): SupportedLocale {
|
||||
i18n.global.locale = lang
|
||||
document.documentElement.lang = lang
|
||||
return lang
|
||||
}
|
||||
|
||||
export async function loadLanguageAsync(lang: SupportedLocale) {
|
||||
export async function setLanguage(lang: SupportedLocale = getCurrentLanguage()): Promise<SupportedLocale | undefined> {
|
||||
if (!lang) {
|
||||
throw new Error()
|
||||
}
|
||||
|
||||
// do not change language to the current one
|
||||
if (i18n.global.locale === lang) {
|
||||
if (i18n.global.locale.value === lang) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -56,7 +47,9 @@ export async function loadLanguageAsync(lang: SupportedLocale) {
|
|||
i18n.global.setLocaleMessage(lang, messages.default)
|
||||
}
|
||||
|
||||
return setI18nLanguage(lang)
|
||||
i18n.global.locale.value = lang
|
||||
document.documentElement.lang = lang
|
||||
return lang
|
||||
}
|
||||
|
||||
export function getCurrentLanguage(): SupportedLocale {
|
||||
|
@ -74,11 +67,7 @@ export function getCurrentLanguage(): SupportedLocale {
|
|||
return language || DEFAULT_LANGUAGE
|
||||
}
|
||||
|
||||
export function saveLanguage(lang: SupportedLocale) {
|
||||
export async function saveLanguage(lang: SupportedLocale) {
|
||||
localStorage.setItem('language', lang)
|
||||
setLanguage()
|
||||
}
|
||||
|
||||
export function setLanguage() {
|
||||
return loadLanguageAsync(getCurrentLanguage())
|
||||
await setLanguage()
|
||||
}
|
1054
src/i18n/lang/no-NO.json
Normal file
1054
src/i18n/lang/no-NO.json
Normal file
File diff suppressed because it is too large
Load Diff
|
@ -554,41 +554,41 @@
|
|||
"lastWeek": "Last Week",
|
||||
"thisMonth": "This Month",
|
||||
"restOfThisMonth": "The Rest of This Month",
|
||||
"nextMonth": "Next Month",
|
||||
"next30Days": "Next 30 Days",
|
||||
"lastMonth": "Last Month",
|
||||
"thisYear": "This Year",
|
||||
"restOfThisYear": "The Rest of This Year"
|
||||
"nextMonth": "Próximo mês",
|
||||
"next30Days": "Próximos 30 dias",
|
||||
"lastMonth": "Último mês",
|
||||
"thisYear": "Este ano",
|
||||
"restOfThisYear": "O resto deste ano"
|
||||
}
|
||||
},
|
||||
"datemathHelp": {
|
||||
"canuse": "You can use date math to filter for relative dates.",
|
||||
"learnhow": "Check out how it works",
|
||||
"title": "Date Math",
|
||||
"intro": "Date Math allows you to specify relative dates which are resolved on the fly by Vikunja when applying the filter.",
|
||||
"canuse": "Você pode usar matemática de data para filtrar datas relativas.",
|
||||
"learnhow": "Veja como funciona",
|
||||
"title": "Matemática de Data",
|
||||
"intro": "A matemática de data permite que você especifique datas relativas que são resolvidas em tempo real pelo Vikunja ao aplicar o filtro.",
|
||||
"expression": "Each Date Math expression starts with an anchor date, which can either be {0}, or a date string ending with {1}. This anchor date can optionally be followed by one or more maths expressions.",
|
||||
"similar": "These expressions are similar to the ones provided by {0} and {1}.",
|
||||
"add1Day": "Add one day",
|
||||
"minus1Day": "Subtract one day",
|
||||
"add1Day": "Adicionar um dia",
|
||||
"minus1Day": "Subtrair um dia",
|
||||
"roundDay": "Round down to the nearest day",
|
||||
"supportedUnits": "Supported time units are:",
|
||||
"someExamples": "Some examples of time expressions:",
|
||||
"supportedUnits": "As unidades de tempo suportadas são:",
|
||||
"someExamples": "Alguns exemplos de expressões temporais:",
|
||||
"units": {
|
||||
"seconds": "Seconds",
|
||||
"minutes": "Minutes",
|
||||
"hours": "Hours",
|
||||
"days": "Days",
|
||||
"weeks": "Weeks",
|
||||
"months": "Months",
|
||||
"years": "Years"
|
||||
"seconds": "Segundos",
|
||||
"minutes": "Minutos",
|
||||
"hours": "Horas",
|
||||
"days": "Dias",
|
||||
"weeks": "Semanas",
|
||||
"months": "Meses",
|
||||
"years": "Anos"
|
||||
},
|
||||
"examples": {
|
||||
"now": "Right now",
|
||||
"in24h": "In 24h",
|
||||
"today": "Today at 00:00",
|
||||
"beginningOfThisWeek": "The beginning of this week at 00:00",
|
||||
"endOfThisWeek": "The end of this week",
|
||||
"in30Days": "In 30 days",
|
||||
"now": "Neste momento",
|
||||
"in24h": "Em 24h",
|
||||
"today": "Hoje às 00:00",
|
||||
"beginningOfThisWeek": "O começo desta semana às 00:00",
|
||||
"endOfThisWeek": "O fim desta semana",
|
||||
"in30Days": "Em 30 dias",
|
||||
"datePlusMonth": "{0} mais um mês às 00:00 desse dia"
|
||||
}
|
||||
}
|
||||
|
@ -663,13 +663,13 @@
|
|||
"endDate": "Data de término",
|
||||
"labels": "Etiquetas",
|
||||
"percentDone": "Progresso",
|
||||
"priority": "Priority",
|
||||
"relatedTasks": "Related Tasks",
|
||||
"priority": "Prioridade",
|
||||
"relatedTasks": "Tarefas relacionadas",
|
||||
"reminders": "Reminders",
|
||||
"repeat": "Repeat",
|
||||
"startDate": "Start Date",
|
||||
"title": "Title",
|
||||
"updated": "Updated"
|
||||
"startDate": "Data de ínicio",
|
||||
"title": "Título",
|
||||
"updated": "Atualizado"
|
||||
},
|
||||
"subscription": {
|
||||
"subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
|
||||
|
@ -681,8 +681,8 @@
|
|||
"notSubscribedList": "You are not subscribed to this list and won't receive notifications for changes.",
|
||||
"subscribedTask": "You are currently subscribed to this task and will receive notifications for changes.",
|
||||
"notSubscribedTask": "You are not subscribed to this task and won't receive notifications for changes.",
|
||||
"subscribe": "Subscribe",
|
||||
"unsubscribe": "Unsubscribe",
|
||||
"subscribe": "Inscrever-se",
|
||||
"unsubscribe": "Desinscrever-se",
|
||||
"subscribeSuccessNamespace": "You are now subscribed to this namespace",
|
||||
"unsubscribeSuccessNamespace": "You are now unsubscribed to this namespace",
|
||||
"subscribeSuccessList": "You are now subscribed to this list",
|
||||
|
@ -691,15 +691,15 @@
|
|||
"unsubscribeSuccessTask": "You are now unsubscribed to this task"
|
||||
},
|
||||
"attachment": {
|
||||
"title": "Attachments",
|
||||
"title": "Anexos",
|
||||
"createdBy": "created {0} by {1}",
|
||||
"downloadTooltip": "Download this attachment",
|
||||
"upload": "Upload attachment",
|
||||
"upload": "Enviar anexo",
|
||||
"drop": "Drop files here to upload",
|
||||
"delete": "Delete attachment",
|
||||
"deleteTooltip": "Delete this attachment",
|
||||
"deleteText1": "Are you sure you want to delete the attachment {filename}?",
|
||||
"copyUrl": "Copy URL",
|
||||
"copyUrl": "Copiar URL",
|
||||
"copyUrlTooltip": "Copy the url of this attachment for usage in text",
|
||||
"setAsCover": "Make cover",
|
||||
"unsetAsCover": "Remove cover",
|
||||
|
@ -710,7 +710,7 @@
|
|||
"title": "Comments",
|
||||
"loading": "Loading comments…",
|
||||
"edited": "edited {date}",
|
||||
"creating": "Creating comment…",
|
||||
"creating": "Criando comentário…",
|
||||
"placeholder": "Adicione seu comentário…",
|
||||
"comment": "Comentário",
|
||||
"delete": "Apagar este comentário",
|
||||
|
@ -720,27 +720,27 @@
|
|||
},
|
||||
"deferDueDate": {
|
||||
"title": "Defer due date",
|
||||
"1day": "1 day",
|
||||
"3days": "3 days",
|
||||
"1week": "1 week"
|
||||
"1day": "1 dia",
|
||||
"3days": "3 dias",
|
||||
"1week": "1 semana"
|
||||
},
|
||||
"description": {
|
||||
"placeholder": "Click here to enter a description…",
|
||||
"empty": "No description available yet."
|
||||
"placeholder": "Clique aqui para inserir uma descrição…",
|
||||
"empty": "Nenhuma descrição disponível ainda."
|
||||
},
|
||||
"assignee": {
|
||||
"placeholder": "Type to assign a user…",
|
||||
"placeholder": "Digite para atribuir um usuário…",
|
||||
"selectPlaceholder": "Assign this user",
|
||||
"assignSuccess": "The user has been assigned successfully.",
|
||||
"unassignSuccess": "The user has been unassigned successfully."
|
||||
"assignSuccess": "O usuário foi atribuído com sucesso.",
|
||||
"unassignSuccess": "O usuário foi desatribuído com sucesso."
|
||||
},
|
||||
"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.",
|
||||
"placeholder": "Digite para adicionar uma nova etiqueta…",
|
||||
"createPlaceholder": "Adicionar como nova etiqueta",
|
||||
"addSuccess": "A etiqueta foi adicionada com sucesso.",
|
||||
"createSuccess": "A etiqueta foi criada com sucesso.",
|
||||
"removeSuccess": "A etiqueta foi removida com sucesso.",
|
||||
"addCreateSuccess": "A etiqueta foi criada e adicionada com sucesso.",
|
||||
"delete": {
|
||||
"header": "Delete this label",
|
||||
"text1": "Are you sure you want to delete this label?",
|
||||
|
@ -748,12 +748,12 @@
|
|||
}
|
||||
},
|
||||
"priority": {
|
||||
"unset": "Unset",
|
||||
"low": "Low",
|
||||
"medium": "Medium",
|
||||
"unset": "Indefinida",
|
||||
"low": "Baixa",
|
||||
"medium": "Média",
|
||||
"high": "High",
|
||||
"urgent": "Urgent",
|
||||
"doNow": "DO NOW"
|
||||
"urgent": "Urgente",
|
||||
"doNow": "FAÇA AGORA"
|
||||
},
|
||||
"relation": {
|
||||
"add": "Add a New Task Relation",
|
||||
|
@ -766,7 +766,7 @@
|
|||
"delete": "Delete Task Relation",
|
||||
"deleteText1": "Are you sure you want to delete this task relation?",
|
||||
"select": "Select a relation kind",
|
||||
"taskRequired": "Please select a task or enter a new task title.",
|
||||
"taskRequired": "Por favor, selecione uma tarefa ou digite um novo título para a tarefa.",
|
||||
"kinds": {
|
||||
"subtask": "Subtask | Subtasks",
|
||||
"parenttask": "Parent Task | Parent Tasks",
|
||||
|
@ -782,24 +782,24 @@
|
|||
}
|
||||
},
|
||||
"repeat": {
|
||||
"everyDay": "Every Day",
|
||||
"everyWeek": "Every Week",
|
||||
"everyMonth": "Every Month",
|
||||
"mode": "Repeat mode",
|
||||
"everyDay": "Diariamente",
|
||||
"everyWeek": "Toda semana",
|
||||
"everyMonth": "Todo mês",
|
||||
"mode": "Modo repetição",
|
||||
"monthly": "Monthly",
|
||||
"fromCurrentDate": "From Current Date",
|
||||
"each": "Each",
|
||||
"specifyAmount": "Specify an amount…",
|
||||
"hours": "Hours",
|
||||
"days": "Days",
|
||||
"weeks": "Weeks",
|
||||
"months": "Months",
|
||||
"years": "Years",
|
||||
"hours": "Horas",
|
||||
"days": "Dias",
|
||||
"weeks": "Semanas",
|
||||
"months": "Meses",
|
||||
"years": "Anos",
|
||||
"invalidAmount": "Please enter more than 0."
|
||||
},
|
||||
"quickAddMagic": {
|
||||
"hint": "You can use Quick Add Magic",
|
||||
"what": "What?",
|
||||
"what": "O quê?",
|
||||
"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.",
|
||||
|
@ -807,14 +807,14 @@
|
|||
"label2": "Vikunja will first check if the label already exist and create it if not.",
|
||||
"label3": "To use spaces, simply add a \" or ' 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.",
|
||||
"priority1": "Para definir a prioridade de uma tarefa, adicione um número de 1 a 5, precedido de um {prefix}.",
|
||||
"priority2": "Quanto maior o número, maior a prioridade.",
|
||||
"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.",
|
||||
"list3": "To use spaces, simply add a \" or ' around the list name.",
|
||||
"list4": "For example: {prefix}\"List with spaces\".",
|
||||
"dateAndTime": "Date and time",
|
||||
"dateAndTime": "Data e hora",
|
||||
"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",
|
||||
|
@ -912,7 +912,7 @@
|
|||
},
|
||||
"update": {
|
||||
"available": "There is an update for Vikunja available!",
|
||||
"do": "Update Now"
|
||||
"do": "Atualizar agora"
|
||||
},
|
||||
"menu": {
|
||||
"edit": "Editar",
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import {computed, ref, watch} from 'vue'
|
||||
import type dayjs from 'dayjs'
|
||||
import type ILocale from 'dayjs/locale/*'
|
||||
|
||||
import {i18n, type SupportedLocale, type ISOLanguage} from '@/i18n'
|
||||
|
||||
|
@ -36,7 +35,7 @@ export function useDayjsLanguageSync(dayjsGlobal: typeof dayjs) {
|
|||
|
||||
const dayjsLanguageLoaded = ref(false)
|
||||
watch(
|
||||
() => i18n.global.locale,
|
||||
() => i18n.global.locale.value,
|
||||
async (currentLanguage: string) => {
|
||||
if (!dayjsGlobal) {
|
||||
return
|
||||
|
|
|
@ -15,7 +15,7 @@ import Notifications from '@kyvg/vue3-notification'
|
|||
import './registerServiceWorker'
|
||||
|
||||
// i18n
|
||||
import {i18n} from './i18n'
|
||||
import {i18n, setLanguage} from './i18n'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
@ -106,4 +106,6 @@ app.use(pinia)
|
|||
app.use(router)
|
||||
app.use(i18n)
|
||||
|
||||
app.mount('#app')
|
||||
setLanguage().then(() => {
|
||||
app.mount('#app')
|
||||
})
|
|
@ -1,6 +1,7 @@
|
|||
import {parseDate} from '../helpers/time/parseDate'
|
||||
import {PRIORITIES} from '@/constants/priorities'
|
||||
import {REPEAT_TYPES, type IRepeatAfter, type IRepeatType} from '@/types/IRepeatAfter'
|
||||
import {getQuickAddMagicMode} from '@/helpers/quickAddMagicMode'
|
||||
|
||||
const VIKUNJA_PREFIXES: Prefixes = {
|
||||
label: '*',
|
||||
|
@ -71,11 +72,10 @@ export const parseTaskText = (text: string, prefixesMode: PrefixMode = PrefixMod
|
|||
return result
|
||||
}
|
||||
|
||||
result.labels = getItemsFromPrefix(text, prefixes.label)
|
||||
result.labels = getLabelsFromPrefix(text, prefixes.label) ?? []
|
||||
result.text = cleanupItemText(result.text, result.labels, prefixes.label)
|
||||
|
||||
const lists: string[] = getItemsFromPrefix(result.text, prefixes.list)
|
||||
result.list = lists.length > 0 ? lists[0] : null
|
||||
result.list = getListFromPrefix(result.text, prefixes.list)
|
||||
result.text = result.list !== null ? cleanupItemText(result.text, [result.list], prefixes.list) : result.text
|
||||
|
||||
result.priority = getPriority(result.text, prefixes.priority)
|
||||
|
@ -130,6 +130,29 @@ const getItemsFromPrefix = (text: string, prefix: string): string[] => {
|
|||
return Array.from(new Set(items))
|
||||
}
|
||||
|
||||
export const getListFromPrefix = (text: string, listPrefix: string | null = null): string | null => {
|
||||
if (listPrefix === null) {
|
||||
const prefixes = PREFIXES[getQuickAddMagicMode()]
|
||||
if (prefixes === undefined) {
|
||||
return null
|
||||
}
|
||||
listPrefix = prefixes.list
|
||||
}
|
||||
const lists: string[] = getItemsFromPrefix(text, listPrefix)
|
||||
return lists.length > 0 ? lists[0] : null
|
||||
}
|
||||
|
||||
export const getLabelsFromPrefix = (text: string, listPrefix: string | null = null): string[] | null => {
|
||||
if (listPrefix === null) {
|
||||
const prefixes = PREFIXES[getQuickAddMagicMode()]
|
||||
if (prefixes === undefined) {
|
||||
return null
|
||||
}
|
||||
listPrefix = prefixes.label
|
||||
}
|
||||
return getItemsFromPrefix(text, listPrefix)
|
||||
}
|
||||
|
||||
const getPriority = (text: string, prefix: string): number | null => {
|
||||
const ps = getItemsFromPrefix(text, prefix)
|
||||
if (ps.length === 0) {
|
||||
|
|
|
@ -311,9 +311,11 @@ export const useAuthStore = defineStore('auth', () => {
|
|||
|
||||
const cancel = setModuleLoading(setIsLoadingGeneralSettings)
|
||||
try {
|
||||
saveLanguage(settings.language)
|
||||
await userSettingsService.update(settings)
|
||||
const updateSettingsPromise = userSettingsService.update(settings)
|
||||
const saveLanguagePromise = saveLanguage(settings.language)
|
||||
await updateSettingsPromise
|
||||
setUserSettings({...settings})
|
||||
await saveLanguagePromise
|
||||
if (showMessage) {
|
||||
success({message: i18n.global.t('user.settings.general.savedSuccess')})
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {computed, ref} from 'vue'
|
||||
import {defineStore, acceptHMRUpdate} from 'pinia'
|
||||
import {acceptHMRUpdate, defineStore} from 'pinia'
|
||||
import router from '@/router'
|
||||
import {formatISO} from 'date-fns'
|
||||
|
||||
|
@ -14,8 +14,8 @@ import {parseTaskText} from '@/modules/parseTaskText'
|
|||
|
||||
import TaskAssigneeModel from '@/models/taskAssignee'
|
||||
import LabelTaskModel from '@/models/labelTask'
|
||||
import TaskModel from '@/models/task'
|
||||
import LabelTask from '@/models/labelTask'
|
||||
import TaskModel from '@/models/task'
|
||||
import LabelModel from '@/models/label'
|
||||
|
||||
import type {ILabel} from '@/modelTypes/ILabel'
|
||||
|
@ -306,6 +306,20 @@ export const useTaskStore = defineStore('task', () => {
|
|||
|
||||
return response
|
||||
}
|
||||
|
||||
async function ensureLabelsExist(labels: string[]): Promise<LabelModel[]> {
|
||||
const all = [...new Set(labels)]
|
||||
const mustCreateLabel = all.map(async labelTitle => {
|
||||
let label = validateLabel(Object.values(labelStore.labels), labelTitle)
|
||||
if (typeof label === 'undefined') {
|
||||
// label not found, create it
|
||||
const labelModel = new LabelModel({title: labelTitle})
|
||||
label = await labelStore.createLabel(labelModel)
|
||||
}
|
||||
return label
|
||||
})
|
||||
return Promise.all(mustCreateLabel)
|
||||
}
|
||||
|
||||
// Do everything that is involved in finding, creating and adding the label to the task
|
||||
async function addLabelsToTask(
|
||||
|
@ -316,16 +330,8 @@ export const useTaskStore = defineStore('task', () => {
|
|||
return task
|
||||
}
|
||||
|
||||
const labelAddsToWaitFor = parsedLabels.map(async labelTitle => {
|
||||
let label = validateLabel(Object.values(labelStore.labels), labelTitle)
|
||||
if (typeof label === 'undefined') {
|
||||
// label not found, create it
|
||||
const labelModel = new LabelModel({title: labelTitle})
|
||||
label = await labelStore.createLabel(labelModel)
|
||||
}
|
||||
|
||||
return addLabelToTask(task, label)
|
||||
})
|
||||
const labels = await ensureLabelsExist(parsedLabels)
|
||||
const labelAddsToWaitFor = labels.map(async l => addLabelToTask(task, l))
|
||||
|
||||
// This waits until all labels are created and added to the task
|
||||
await Promise.all(labelAddsToWaitFor)
|
||||
|
@ -402,11 +408,10 @@ export const useTaskStore = defineStore('task', () => {
|
|||
const taskService = new TaskService()
|
||||
try {
|
||||
const createdTask = await taskService.create(task)
|
||||
const result = await addLabelsToTask({
|
||||
return await addLabelsToTask({
|
||||
task: createdTask,
|
||||
parsedLabels: parsedTask.labels,
|
||||
})
|
||||
return result
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
|
@ -437,6 +442,8 @@ export const useTaskStore = defineStore('task', () => {
|
|||
addLabelsToTask,
|
||||
createNewTask,
|
||||
setCoverImage,
|
||||
findListId,
|
||||
ensureLabelsExist,
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -2,13 +2,15 @@
|
|||
import {defineConfig} from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import legacyFn from '@vitejs/plugin-legacy'
|
||||
import { URL, fileURLToPath } from "url";
|
||||
import { dirname, resolve } from "path";
|
||||
|
||||
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'
|
||||
import {VitePWA} from 'vite-plugin-pwa'
|
||||
import {visualizer} from 'rollup-plugin-visualizer'
|
||||
import svgLoader from 'vite-svg-loader'
|
||||
import postcssPresetEnv from "postcss-preset-env";
|
||||
|
||||
import { fileURLToPath, URL } from 'url'
|
||||
|
||||
const pathSrc = fileURLToPath(new URL('./src', import.meta.url))
|
||||
|
||||
|
@ -58,6 +60,13 @@ export default defineConfig({
|
|||
// we don't need to optimize them again.
|
||||
svgo: false,
|
||||
}),
|
||||
VueI18nPlugin({
|
||||
// TODO: only install needed stuff
|
||||
// Whether to install the full set of APIs, components, etc. provided by Vue I18n.
|
||||
// By default, all of them will be installed.
|
||||
fullInstall: true,
|
||||
include: resolve(dirname(pathSrc), './src/i18n/lang/**'),
|
||||
}),
|
||||
VitePWA({
|
||||
srcDir: 'src',
|
||||
filename: 'sw.ts',
|
||||
|
|
Loading…
Reference in New Issue
Block a user