Merge remote-tracking branch 'upstream/main'

This commit is contained in:
Adrian Simmons 2021-12-09 10:32:46 +00:00
commit 7187cde253
46 changed files with 1316 additions and 1163 deletions

View File

@ -19,16 +19,16 @@
"dependencies": {
"@github/hotkey": "1.6.0",
"@kyvg/vue3-notification": "2.3.4",
"@sentry/tracing": "6.15.0",
"@sentry/vue": "6.15.0",
"@vue/compat": "3.2.23",
"@vueuse/core": "7.1.2",
"@sentry/tracing": "6.16.0",
"@sentry/vue": "6.16.0",
"@vue/compat": "3.2.24",
"@vueuse/core": "7.2.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.27.0",
"dompurify": "2.3.3",
"dompurify": "2.3.4",
"easymde": "2.15.0",
"flatpickr": "4.6.9",
"flexsearch": "0.7.21",
@ -36,20 +36,20 @@
"is-touch-device": "1.0.1",
"lodash.clonedeep": "4.5.0",
"lodash.debounce": "4.0.8",
"marked": "4.0.5",
"marked": "4.0.6",
"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.23",
"vue": "3.2.24",
"vue-advanced-cropper": "2.7.0",
"vue-drag-resize": "2.0.3",
"vue-flatpickr-component": "9.0.5",
"vue-i18n": "9.2.0-beta.22",
"vue-i18n": "9.2.0-beta.23",
"vue-router": "4.0.12",
"vuedraggable": "4.1.0",
"vuex": "4.0.2",
"workbox-precaching": "6.4.1"
"workbox-precaching": "6.4.2"
},
"devDependencies": {
"@4tw/cypress-drag-drop": "2.1.0",
@ -59,37 +59,37 @@
"@fortawesome/vue-fontawesome": "3.0.0-5",
"@types/flexsearch": "0.7.2",
"@types/jest": "27.0.3",
"@typescript-eslint/eslint-plugin": "5.5.0",
"@typescript-eslint/parser": "5.5.0",
"@vitejs/plugin-legacy": "1.6.3",
"@vitejs/plugin-vue": "1.10.1",
"@typescript-eslint/eslint-plugin": "5.6.0",
"@typescript-eslint/parser": "5.6.0",
"@vitejs/plugin-legacy": "1.6.4",
"@vitejs/plugin-vue": "1.10.2",
"@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.14.1",
"eslint": "8.3.0",
"eslint-plugin-vue": "8.1.1",
"esbuild": "0.14.2",
"eslint": "8.4.1",
"eslint-plugin-vue": "8.2.0",
"express": "4.17.1",
"faker": "5.5.3",
"jest": "27.4.2",
"netlify-cli": "8.0.6",
"jest": "27.4.3",
"netlify-cli": "8.0.16",
"postcss": "8.4.4",
"postcss-preset-env": "7.0.1",
"rollup": "2.60.2",
"rollup-plugin-visualizer": "5.5.2",
"sass": "1.44.0",
"slugify": "1.6.3",
"ts-jest": "27.0.7",
"ts-jest": "27.1.1",
"typescript": "4.5.2",
"vite": "2.6.14",
"vite-plugin-pwa": "0.11.9",
"vite": "2.7.1",
"vite-plugin-pwa": "0.11.10",
"vite-svg-loader": "3.1.0",
"vue-tsc": "0.29.8",
"wait-on": "6.0.0",
"workbox-cli": "6.4.1"
"workbox-cli": "6.4.2"
},
"eslintConfig": {
"root": true,

2
run.sh
View File

@ -4,7 +4,7 @@
VIKUNJA_API_URL="${VIKUNJA_API_URL:-"/api/v1"}"
VIKUNJA_SENTRY_ENABLED="${VIKUNJA_SENTRY_ENABLED:-"false"}"
VIKUNJA_SENTRY_DSN="${VIKUNJA_SENTRY_DSN:-"https://7e684483a06a4225b3e05cc47cae7a11@sentry.kolaente.de/2"}"
VIKUNJA_SENTRY_DSN="${VIKUNJA_SENTRY_DSN:-"https://85694a2d757547cbbc90cd4b55c5a18d@o1047380.ingest.sentry.io/6024480"}"
VIKUNJA_HTTP_PORT="${VIKUNJA_HTTP_PORT:-80}"
VIKUNJA_HTTPS_PORT="${VIKUNJA_HTTPS_PORT:-443}"

View File

@ -20,64 +20,59 @@
:class="{'is-favorite': list.isFavorite, 'is-archived': list.isArchived}"
@click.stop="toggleFavoriteList(list)"
class="favorite">
<icon icon="star" v-if="list.isFavorite"/>
<icon :icon="['far', 'star']" v-else/>
<icon :icon="list.isFavorite ? 'star' : ['far', 'star']" />
</span>
</div>
<div class="title">{{ list.title }}</div>
</router-link>
</template>
<script>
<script lang="ts" setup>
import {ref, watch} from 'vue'
import {useStore} from 'vuex'
import ListService from '@/services/list'
export default {
name: 'list-card',
data() {
return {
background: null,
backgroundLoading: false,
}
},
props: {
list: {
required: true,
},
showArchived: {
default: false,
type: Boolean,
},
},
watch: {
list: {
handler: 'loadBackground',
immediate: true,
},
},
methods: {
async loadBackground() {
if (this.list === null || !this.list.backgroundInformation || this.backgroundLoading) {
return
}
const background = ref(null)
const backgroundLoading = ref(false)
this.backgroundLoading = true
const listService = new ListService()
try {
this.background = await listService.background(this.list)
} finally {
this.backgroundLoading = false
}
},
toggleFavoriteList(list) {
// The favorites pseudo list is always favorite
// Archived lists cannot be marked favorite
if (list.id === -1 || list.isArchived) {
return
}
this.$store.dispatch('lists/toggleListFavorite', list)
},
const props = defineProps({
list: {
type: Object,
required: true,
},
showArchived: {
default: false,
type: Boolean,
},
})
watch(props.list, loadBackground, { immediate: true })
async function loadBackground() {
if (props.list === null || !props.list.backgroundInformation || backgroundLoading.value) {
return
}
backgroundLoading.value = true
const listService = new ListService()
try {
background.value = await listService.background(props.list)
} finally {
backgroundLoading.value = false
}
}
const store = useStore()
function toggleFavoriteList(list) {
// The favorites pseudo list is always favorite
// Archived lists cannot be marked favorite
if (list.id === -1 || list.isArchived) {
return
}
store.dispatch('lists/toggleListFavorite', list)
}
</script>

View File

@ -1,40 +1,27 @@
<template>
<div
v-if="isDone"
class="is-done"
:class="{ 'is-done--small': variant === variants.SMALL }"
>
{{ $t('task.attributes.done') }}
</div>
v-if="isDone"
class="is-done"
:class="{ 'is-done--small': variant === 'small' }"
>
{{ $t('task.attributes.done') }}
</div>
</template>
<script>
const VARIANTS = {
DEFAULT: 'default',
SMALL: 'small',
}
<script lang="ts" setup>
import {PropType} from 'vue'
type Variants = 'default' | 'small'
export default {
name: 'Done',
data() {
return {
variants: VARIANTS,
}
defineProps({
isDone: {
type: Boolean,
default: false,
},
props: {
isDone: {
type: Boolean,
default: false,
},
variant: {
type: String,
default: VARIANTS.DEFAULT,
validator: (variant) => Object.values(VARIANTS).includes(variant),
},
variant: {
type: String as PropType<Variants>,
default: 'default',
},
}
})
</script>

View File

@ -24,41 +24,39 @@
</div>
</template>
<script>
export default {
name: 'card',
props: {
title: {
type: String,
default: '',
},
padding: {
type: Boolean,
default: true,
},
hasClose: {
type: Boolean,
default: false,
},
closeIcon: {
type: String,
default: 'times',
},
shadow: {
type: Boolean,
default: true,
},
hasContent: {
type: Boolean,
default: true,
},
loading: {
type: Boolean,
default: false,
},
<script setup lang="ts">
defineProps({
title: {
type: String,
default: '',
},
emits: ['close'],
}
padding: {
type: Boolean,
default: true,
},
hasClose: {
type: Boolean,
default: false,
},
closeIcon: {
type: String,
default: 'times',
},
shadow: {
type: Boolean,
default: true,
},
hasContent: {
type: Boolean,
default: true,
},
loading: {
type: Boolean,
default: false,
},
})
defineEmits(['close'])
</script>
<style lang="scss" scoped>

View File

@ -11,18 +11,15 @@
</router-link>
</template>
<script>
export default {
name: 'dropdown-item',
props: {
to: {
required: true,
},
icon: {
type: String,
required: false,
default: '',
},
<script lang="ts" setup>
defineProps({
to: {
required: true,
},
}
icon: {
type: String,
required: false,
default: '',
},
})
</script>

View File

@ -7,16 +7,10 @@
</message>
</template>
<script>
import Message from '@/components/misc/message'
<script lang="ts" setup>
import Message from '@/components/misc/message.vue'
export default {
name: 'error',
components: {Message},
methods: {
reload() {
window.location.reload()
},
},
function reload() {
window.location.reload()
}
</script>

View File

@ -6,16 +6,14 @@
</div>
</template>
<script>
import {mapState} from 'vuex'
<script lang="ts" setup>
import {computed} from 'vue'
import {useStore} from 'vuex'
export default {
name: 'legal',
computed: mapState({
imprintUrl: state => state.config.legal.imprintUrl,
privacyPolicyUrl: state => state.config.legal.privacyPolicyUrl,
}),
}
const store = useStore()
const imprintUrl = computed(() => store.state.config.legal.imprintUrl)
const privacyPolicyUrl = computed(() => store.state.config.legal.privacyPolicyUrl)
</script>
<style lang="scss" scoped>

View File

@ -2,12 +2,6 @@
<div class="loader-container is-loading"></div>
</template>
<script>
export default {
name: 'loading',
}
</script>
<style scoped>
.loader-container {
height: 100%;

View File

@ -4,7 +4,7 @@
</div>
</template>
<script setup>
<script lang="ts" setup>
defineProps({
variant: {
type: String,

View File

@ -2,17 +2,17 @@
<div class="no-auth-wrapper">
<div class="noauth-container">
<Logo class="logo" width="400" height="117" />
<message v-if="motd !== ''" class="my-2">
<Message v-if="motd !== ''" class="my-2">
{{ motd }}
</message>
</Message>
<slot/>
</div>
</div>
</template>
<script setup>
<script lang="ts" setup>
import Logo from '@/components/home/Logo.vue'
import message from '@/components/misc/message'
import Message from '@/components/misc/message.vue'
import {useStore} from 'vuex'
import {computed} from 'vue'

View File

@ -34,8 +34,10 @@
</nav>
</template>
<script>
function createPagination(totalPages, currentPage) {
<script lang="ts" setup>
import {computed} from 'vue'
function createPagination(totalPages: number, currentPage: number) {
const pages = []
for (let i = 0; i < totalPages; i++) {
@ -79,30 +81,18 @@ function getRouteForPagination(page = 1, type = 'list') {
}
}
export default {
name: 'Pagination',
props: {
totalPages: {
type: Number,
required: true,
},
currentPage: {
type: Number,
default: 0,
},
const props = defineProps({
totalPages: {
type: Number,
required: true,
},
computed: {
pages() {
return createPagination(this.totalPages, this.currentPage)
},
currentPage: {
type: Number,
default: 0,
},
})
methods: {
getRouteForPagination,
},
}
const pages = computed(() => createPagination(props.totalPages, props.currentPage))
</script>
<style lang="scss" scoped>

View File

@ -13,7 +13,7 @@
<section v-else-if="error !== ''">
<no-auth-wrapper>
<card>
<p v-if="error === errorNoApiUrl">
<p v-if="error === ERROR_NO_API_URL">
{{ $t('ready.noApiUrlConfigured') }}
</p>
<message variant="danger" v-else>
@ -40,51 +40,34 @@
</transition>
</template>
<script>
<script lang="ts" setup>
import {ref, computed} from 'vue'
import {useStore} from 'vuex'
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 ApiConfig from '@/components/misc/api-config.vue'
import Message from '@/components/misc/message.vue'
import NoAuthWrapper from '@/components/misc/no-auth-wrapper.vue'
import {ERROR_NO_API_URL} from '@/helpers/checkAndSetApiUrl'
export default {
name: 'ready',
components: {
Message,
Logo,
NoAuthWrapper,
ApiConfig,
},
data() {
return {
error: '',
errorNoApiUrl: ERROR_NO_API_URL,
}
},
created() {
this.load()
},
computed: {
ready() {
return this.$store.state.vikunjaReady
},
showLoading() {
return !this.ready && this.error === ''
},
...mapState([
'online',
]),
},
methods: {
load() {
this.$store.dispatch('loadApp')
.catch(e => {
this.error = e
})
},
},
const store = useStore()
const ready = computed(() => store.state.vikunjaReady)
const online = computed(() => store.state.online)
const error = ref('')
const showLoading = computed(() => !ready.value && error.value === '')
async function load() {
try {
await store.dispatch('loadApp')
} catch(e: any) {
error.value = e
}
}
load()
</script>
<style lang="scss" scoped>

View File

@ -7,24 +7,21 @@
</component>
</template>
<script>
export default {
name: 'shortcut',
props: {
keys: {
type: Array,
required: true,
},
combination: {
type: String,
default: '+',
},
is: {
type: String,
default: 'div',
},
<script lang="ts" setup>
defineProps({
keys: {
type: Array,
required: true,
},
}
combination: {
type: String,
default: '+',
},
is: {
type: String,
default: 'div',
},
})
</script>
<style lang="scss" scoped>

View File

@ -22,91 +22,89 @@
</a>
</template>
<script>
<script lang="ts" setup>
import {computed, shallowRef} from 'vue'
import {useI18n} from 'vue-i18n'
import SubscriptionService from '@/services/subscription'
import SubscriptionModel from '@/models/subscription'
export default {
name: 'task-subscription',
data() {
return {
subscriptionService: new SubscriptionService(),
}
},
props: {
entity: {
required: true,
type: String,
},
subscription: {
required: true,
},
entityId: {
required: true,
},
isButton: {
type: Boolean,
default: true,
},
},
emits: ['change'],
computed: {
tooltipText() {
if (this.disabled) {
return this.$t('task.subscription.subscribedThroughParent', {
entity: this.entity,
parent: this.subscription.entity,
})
}
import {success} from '@/message'
return this.subscription !== null ?
this.$t('task.subscription.subscribed', {entity: this.entity}) :
this.$t('task.subscription.notSubscribed', {entity: this.entity})
},
buttonText() {
return this.subscription !== null ? this.$t('task.subscription.unsubscribe') : this.$t('task.subscription.subscribe')
},
icon() {
return this.subscription !== null ? ['far', 'bell-slash'] : 'bell'
},
disabled() {
if (this.subscription === null) {
return false
}
return this.subscription.entity !== this.entity
},
const props = defineProps({
entity: {
required: true,
type: String,
},
methods: {
changeSubscription() {
if (this.disabled) {
return
}
if (this.subscription === null) {
this.subscribe()
} else {
this.unsubscribe()
}
},
async subscribe() {
const subscription = new SubscriptionModel({
entity: this.entity,
entityId: this.entityId,
})
await this.subscriptionService.create(subscription)
this.$emit('change', subscription)
this.$message.success({message: this.$t('task.subscription.subscribeSuccess', {entity: this.entity})})
},
async unsubscribe() {
const subscription = new SubscriptionModel({
entity: this.entity,
entityId: this.entityId,
})
await this.subscriptionService.delete(subscription)
this.$emit('change', null)
this.$message.success({message: this.$t('task.subscription.unsubscribeSuccess', {entity: this.entity})})
},
subscription: {
required: true,
},
entityId: {
required: true,
},
isButton: {
type: Boolean,
default: true,
},
})
const emit = defineEmits(['change'])
const subscriptionService = shallowRef(new SubscriptionService())
const {t} = useI18n()
const tooltipText = computed(() => {
if (disabled.value) {
return t('task.subscription.subscribedThroughParent', {
entity: props.entity,
parent: props.subscription.entity,
})
}
return props.subscription !== null ?
t('task.subscription.subscribed', {entity: props.entity}) :
t('task.subscription.notSubscribed', {entity: props.entity})
})
const buttonText = computed(() => props.subscription !== null ? t('task.subscription.unsubscribe') : t('task.subscription.subscribe'))
const icon = computed(() => props.subscription !== null ? ['far', 'bell-slash'] : 'bell')
const disabled = computed(() => {
if (props.subscription === null) {
return false
}
return props.subscription.entity !== props.entity
})
function changeSubscription() {
if (disabled.value) {
return
}
if (props.subscription === null) {
subscribe()
} else {
unsubscribe()
}
}
async function subscribe() {
const subscription = new SubscriptionModel({
entity: props.entity,
entityId: props.entityId,
})
await subscriptionService.value.create(subscription)
emit('change', subscription)
success({message: t('task.subscription.subscribeSuccess', {entity: props.entity})})
}
async function unsubscribe() {
const subscription = new SubscriptionModel({
entity: props.entity,
entityId: props.entityId,
})
await subscriptionService.value.delete(subscription)
emit('change', null)
success({message: t('task.subscription.unsubscribeSuccess', {entity: props.entity})})
}
</script>

View File

@ -11,31 +11,28 @@
</div>
</template>
<script>
export default {
name: 'user',
props: {
user: {
required: true,
type: Object,
},
showUsername: {
required: false,
type: Boolean,
default: true,
},
avatarSize: {
required: false,
type: Number,
default: 50,
},
isInline: {
required: false,
type: Boolean,
default: false,
},
<script lang="ts" setup>
defineProps({
user: {
required: true,
type: Object,
},
}
showUsername: {
required: false,
type: Boolean,
default: true,
},
avatarSize: {
required: false,
type: Number,
default: 50,
},
isInline: {
required: false,
type: Boolean,
default: false,
},
})
</script>
<style lang="scss" scoped>

View File

@ -9,32 +9,23 @@
/>
</template>
<script>
<script lang="ts" setup>
import {ref, computed} from 'vue'
import {useStore} from 'vuex'
import Multiselect from '@/components/input/multiselect.vue'
export default {
name: 'namespace-search',
emits: ['selected'],
data() {
return {
query: '',
}
},
components: {
Multiselect,
},
computed: {
namespaces() {
return this.$store.getters['namespaces/searchNamespace'](this.query)
},
},
methods: {
findNamespaces(query) {
this.query = query
},
select(namespace) {
this.$emit('selected', namespace)
},
},
const emit = defineEmits(['selected'])
const query = ref('')
const store = useStore()
const namespaces = computed(() => store.getters['namespaces/searchNamespace'](query.value))
function findNamespaces(newQuery: string) {
query.value = newQuery
}
function select(namespace) {
emit('selected', namespace)
}
</script>

View File

@ -52,30 +52,22 @@
</dropdown>
</template>
<script>
<script setup lang="ts">
import {ref, onMounted} from 'vue'
import Dropdown from '@/components/misc/dropdown.vue'
import DropdownItem from '@/components/misc/dropdown-item.vue'
import TaskSubscription from '@/components/misc/subscription.vue'
export default {
name: 'namespace-settings-dropdown',
data() {
return {
subscription: null,
}
const props = defineProps({
namespace: {
type: Object, // NamespaceModel
required: true,
},
components: {
DropdownItem,
Dropdown,
TaskSubscription,
},
props: {
namespace: {
required: true,
},
},
mounted() {
this.subscription = this.namespace.subscription
},
}
})
const subscription = ref(null)
onMounted(() => {
subscription.value = props.namespace.subscription
})
</script>

View File

@ -3,7 +3,6 @@
<div class="filter-container">
<div class="items">
<filter-popup
:visible="showTaskFilter"
v-model="params"
@update:modelValue="loadTasks()"
/>

View File

@ -65,6 +65,21 @@
<li>17th ({{ $t('task.quickAddMagic.dateNth', {day: '17'}) }})</li>
</ul>
<p>{{ $t('task.quickAddMagic.dateTime', {time: 'at 17:00', timePM: '5pm'}) }}</p>
<h3>{{ $t('task.quickAddMagic.repeats') }}</h3>
<p>{{ $t('task.quickAddMagic.repeatsDescription', {suffix: 'every {amount} {type}'}) }}</p>
<p>{{ $t('misc.forExample') }}</p>
<ul>
<!-- Not localized because these only work in english -->
<li>Every day</li>
<li>Every 3 days</li>
<li>Every week</li>
<li>Every 2 weeks</li>
<li>Every month</li>
<li>Every 6 months</li>
<li>Every year</li>
<li>Every 2 years</li>
</ul>
</card>
</modal>
</div>

View File

@ -21,6 +21,7 @@ Here's some text in between
expect(checkboxes[0]).toBe(0)
expect(checkboxes[1]).toBe(18)
expect(checkboxes[2]).toBe(69)
expect(checkboxes[3]).toBe(90)
})
it('should find one checkbox with *', () => {
const text: string = '* [ ] Lorem Ipsum'

View File

@ -40,7 +40,7 @@ export const findCheckboxesInText = (text: string): number[] => {
return [
...checkboxes.checked,
...checkboxes.unchecked,
].sort()
].sort((a, b) => a < b ? -1 : 1)
}
export const getChecklistStatistics = (text: string): CheckboxStatistics => {

View File

@ -135,18 +135,18 @@ export const getDateFromText = (text: string, now: Date = new Date()) => {
if (result === null) {
// 3. Try parsing the date as "27/01" or "01/27"
const monthNumericRegex:RegExp = /([0-9][0-9]?\/[0-9][0-9]?)/ig
const monthNumericRegex: RegExp = /([0-9][0-9]?\/[0-9][0-9]?)/ig
results = monthNumericRegex.exec(text)
// Put the year before or after the date, depending on what works
result = results === null ? null : `${now.getFullYear()}/${results[0]}`
if(result === null) {
if (result === null) {
return {
foundText,
date: null,
}
}
foundText = results === null ? '' : results[0]
if (result === null || isNaN(new Date(result).getTime())) {
result = results === null ? null : `${results[0]}/${now.getFullYear()}`
@ -280,7 +280,7 @@ const getDateFromWeekday = (text: string): dateFoundResult => {
if (foundText.endsWith(' ')) {
foundText = foundText.substr(0, foundText.length - 1)
}
return {
foundText: foundText,
date: date,
@ -297,19 +297,20 @@ const getDayFromText = (text: string) => {
}
}
const date = new Date()
const now = new Date()
const date = new Date(now)
const day = parseInt(results[0])
date.setDate(day)
// If the parsed day is the 31st but the next month only has 30 days, setting the day to 31 will "overflow" the
// date to the next month, but the first.
// This would look like a very weired bug. Now, to prevent that, we check if the day is the same as parsed after
// setting it for the first time and set it again if it isn't - that would mean the month overflowed.
if(day === 31 && date.getDate() !== day) {
if (day === 31 && date.getDate() !== day) {
date.setDate(day)
}
if (date < new Date()) {
if (date < now) {
date.setMonth(date.getMonth() + 1)
}

View File

@ -102,6 +102,7 @@
"title": "Avatar",
"initials": "Iniciály",
"gravatar": "Gravatar",
"marble": "Mramor",
"upload": "Nahrát",
"uploadAvatar": "Nahrát avatara",
"statusUpdateSuccess": "Stav avatara byl úspěšně aktualizován!",
@ -114,12 +115,12 @@
"vikunja": "Vikunja"
},
"appearance": {
"title": "Color Scheme",
"setSuccess": "Saved change of color scheme to {colorScheme}",
"title": "Barevné schéma",
"setSuccess": "Změna barevného schématu na {colorScheme} byla uložena",
"colorScheme": {
"light": "Light",
"system": "System",
"dark": "Dark"
"light": "Světlý",
"system": "Systém",
"dark": "Tmavý"
}
}
},
@ -471,7 +472,8 @@
"close": "Zavřít",
"download": "Stáhnout",
"showMenu": "Zobrazit nabídku",
"hideMenu": "Skrýt nabídku"
"hideMenu": "Skrýt nabídku",
"forExample": "Například:"
},
"input": {
"resetColor": "Obnovit barvu",
@ -556,7 +558,7 @@
"text2": "Tímto také odstraníte všechny přílohy, připomenutí a vztahy spojené s tímto úkolem a nelze je vrátit zpět!"
},
"actions": {
"assign": "Assign to a user",
"assign": "Přiřadit uživateli",
"label": "Přidat štítky",
"priority": "Nastavit prioritu",
"dueDate": "Nastavit termín",
@ -720,7 +722,9 @@
"dateWeekday": "každý pracovní den použije další datum s tímto datem",
"dateCurrentYear": "použije aktuální rok",
"dateNth": "použije {day}. den aktuálního měsíce",
"dateTime": "Pro nastavení času zkombinujte libovolný formát data s \"{time}\" (nebo {timePM})."
"dateTime": "Pro nastavení času zkombinujte libovolný formát data s \"{time}\" (nebo {timePM}).",
"repeats": "Opakující se úkoly",
"repeatsDescription": "Chcete-li nastavit úkol jako opakující se v intervalu, stačí přidat '{suffix}' do textu úkolu. Množství musí být číslo a lze ji vynechat, aby byl použit pouze typ (viz příklady)."
}
},
"team": {
@ -775,7 +779,7 @@
"task": {
"title": "Stránka úkolů",
"done": "Označit úkol jako hotový",
"assign": "Assign to a user",
"assign": "Přiřadit uživateli",
"labels": "Přidat štítky k tomuto úkolu",
"dueDate": "Změnit termín tohoto úkolu",
"attachment": "Přidat přílohu k tomuto úkolu",
@ -901,7 +905,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": "The team name cannot be empty.",
"6001": "Název týmu nemůže být prázdný.",
"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

@ -102,6 +102,7 @@
"title": "Avatar",
"initials": "Initialen",
"gravatar": "Gravatar",
"marble": "Marble",
"upload": "Hochladen",
"uploadAvatar": "Avatar hochladen",
"statusUpdateSuccess": "Avatar-Status wurde erfolgreich aktualisiert.",
@ -471,7 +472,8 @@
"close": "Schließen",
"download": "Herunterladen",
"showMenu": "Menü anzeigen",
"hideMenu": "Menü ausblenden"
"hideMenu": "Menü ausblenden",
"forExample": "For example:"
},
"input": {
"resetColor": "Farbe zurücksetzen",
@ -720,7 +722,9 @@
"dateWeekday": "jeder Wochentag, wird das nächste Datum mit diesem Tag verwenden",
"dateCurrentYear": "wird das laufende Jahr nutzen",
"dateNth": "wird den {day}. des aktuellen Monats verwenden",
"dateTime": "Kombiniere eines der Datumsformate mit \"{time}\" (oder {timePM}), um eine Zeit festzulegen."
"dateTime": "Kombiniere eines der Datumsformate mit \"{time}\" (oder {timePM}), um eine Zeit festzulegen.",
"repeats": "Repeating tasks",
"repeatsDescription": "To set a task as repeating in an interval, simply add '{suffix}' to the task text. The amount needs to be a number and can be omitted to use just the type (see examples)."
}
},
"team": {

View File

@ -102,6 +102,7 @@
"title": "Herr Der Elemente",
"initials": "Initialä",
"gravatar": "Gravatar",
"marble": "Marble",
"upload": "Ufeladä",
"uploadAvatar": "Profiilbild ufeladä",
"statusUpdateSuccess": "Avatar Zuestand erfolgriich aktualisiert!",
@ -471,7 +472,8 @@
"close": "Schlüüse",
"download": "Herunterladen",
"showMenu": "Menü anzeigen",
"hideMenu": "Menü ausblenden"
"hideMenu": "Menü ausblenden",
"forExample": "For example:"
},
"input": {
"resetColor": "Farb zruggsetze",
@ -720,7 +722,9 @@
"dateWeekday": "jede Wuchetaag wird nimmt s'negste Datum mit dem Datum",
"dateCurrentYear": "nimmt das laufende Jahr",
"dateNth": "nimmt de {day}ti vom jetzige Monet",
"dateTime": "Kombiniere irgendeis vo dene Datumsformat mit \"{time}\" (oder {timePM}) um e Ziit z'setze."
"dateTime": "Kombiniere irgendeis vo dene Datumsformat mit \"{time}\" (oder {timePM}) um e Ziit z'setze.",
"repeats": "Repeating tasks",
"repeatsDescription": "To set a task as repeating in an interval, simply add '{suffix}' to the task text. The amount needs to be a number and can be omitted to use just the type (see examples)."
}
},
"team": {

View File

@ -102,6 +102,7 @@
"title": "Avatar",
"initials": "Initials",
"gravatar": "Gravatar",
"marble": "Marble",
"upload": "Upload",
"uploadAvatar": "Upload Avatar",
"statusUpdateSuccess": "Avatar status was updated successfully!",
@ -471,7 +472,8 @@
"close": "Close",
"download": "Download",
"showMenu": "Show the menu",
"hideMenu": "Hide the menu"
"hideMenu": "Hide the menu",
"forExample": "For example:"
},
"input": {
"resetColor": "Reset Color",
@ -720,7 +722,9 @@
"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."
"dateTime": "Combine any of the date formats with \"{time}\" (or {timePM}) to set a time.",
"repeats": "Repeating tasks",
"repeatsDescription": "To set a task as repeating in an interval, simply add '{suffix}' to the task text. The amount needs to be a number and can be omitted to use just the type (see examples)."
}
},
"team": {

View File

@ -102,6 +102,7 @@
"title": "Avatar",
"initials": "Initials",
"gravatar": "Gravatar",
"marble": "Marble",
"upload": "Upload",
"uploadAvatar": "Upload Avatar",
"statusUpdateSuccess": "Avatar status was updated successfully!",
@ -471,7 +472,8 @@
"close": "Close",
"download": "Download",
"showMenu": "Show the menu",
"hideMenu": "Hide the menu"
"hideMenu": "Hide the menu",
"forExample": "For example:"
},
"input": {
"resetColor": "Reset Color",
@ -720,7 +722,9 @@
"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."
"dateTime": "Combine any of the date formats with \"{time}\" (or {timePM}) to set a time.",
"repeats": "Repeating tasks",
"repeatsDescription": "To set a task as repeating in an interval, simply add '{suffix}' to the task text. The amount needs to be a number and can be omitted to use just the type (see examples)."
}
},
"team": {

View File

@ -102,6 +102,7 @@
"title": "Avatar",
"initials": "Initiales",
"gravatar": "Gravatar",
"marble": "Marble",
"upload": "Téléverser",
"uploadAvatar": "Téléverser lavatar",
"statusUpdateSuccess": "Statut de lavatar mis à jour.",
@ -471,7 +472,8 @@
"close": "Fermer",
"download": "Télécharger",
"showMenu": "Afficher le menu",
"hideMenu": "Masquer le menu"
"hideMenu": "Masquer le menu",
"forExample": "For example:"
},
"input": {
"resetColor": "Réinitialiser la couleur",
@ -720,7 +722,9 @@
"dateWeekday": "nimporte quel jour de la semaine, utilisera la date suivante avec cette date",
"dateCurrentYear": "utilisera lannée en cours",
"dateNth": "utilisera le {day}e du mois en cours",
"dateTime": "Combinez nimporte lequel des formats de date avec « {time} » (ou {timePM}) pour définir une heure."
"dateTime": "Combinez nimporte lequel des formats de date avec « {time} » (ou {timePM}) pour définir une heure.",
"repeats": "Repeating tasks",
"repeatsDescription": "To set a task as repeating in an interval, simply add '{suffix}' to the task text. The amount needs to be a number and can be omitted to use just the type (see examples)."
}
},
"team": {

View File

@ -102,6 +102,7 @@
"title": "Avatar",
"initials": "Iniziali",
"gravatar": "Gravatar",
"marble": "Marble",
"upload": "Carica",
"uploadAvatar": "Carica Avatar",
"statusUpdateSuccess": "Avatar status was updated successfully!",
@ -471,7 +472,8 @@
"close": "Chiudi",
"download": "Scarica",
"showMenu": "Show the menu",
"hideMenu": "Hide the menu"
"hideMenu": "Hide the menu",
"forExample": "For example:"
},
"input": {
"resetColor": "Ripristina Colore",
@ -720,7 +722,9 @@
"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."
"dateTime": "Combine any of the date formats with \"{time}\" (or {timePM}) to set a time.",
"repeats": "Repeating tasks",
"repeatsDescription": "To set a task as repeating in an interval, simply add '{suffix}' to the task text. The amount needs to be a number and can be omitted to use just the type (see examples)."
}
},
"team": {

View File

@ -102,6 +102,7 @@
"title": "Avatar",
"initials": "Initials",
"gravatar": "Gravatar",
"marble": "Marble",
"upload": "Upload",
"uploadAvatar": "Upload Avatar",
"statusUpdateSuccess": "Avatar status was updated successfully!",
@ -471,7 +472,8 @@
"close": "Close",
"download": "Download",
"showMenu": "Show the menu",
"hideMenu": "Hide the menu"
"hideMenu": "Hide the menu",
"forExample": "For example:"
},
"input": {
"resetColor": "Reset Color",
@ -720,7 +722,9 @@
"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."
"dateTime": "Combine any of the date formats with \"{time}\" (or {timePM}) to set a time.",
"repeats": "Repeating tasks",
"repeatsDescription": "To set a task as repeating in an interval, simply add '{suffix}' to the task text. The amount needs to be a number and can be omitted to use just the type (see examples)."
}
},
"team": {

View File

@ -102,6 +102,7 @@
"title": "Avatar",
"initials": "Initials",
"gravatar": "Gravatar",
"marble": "Marble",
"upload": "Upload",
"uploadAvatar": "Upload Avatar",
"statusUpdateSuccess": "Avatar status was updated successfully!",
@ -471,7 +472,8 @@
"close": "Close",
"download": "Download",
"showMenu": "Show the menu",
"hideMenu": "Hide the menu"
"hideMenu": "Hide the menu",
"forExample": "For example:"
},
"input": {
"resetColor": "Reset Color",
@ -720,7 +722,9 @@
"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."
"dateTime": "Combine any of the date formats with \"{time}\" (or {timePM}) to set a time.",
"repeats": "Repeating tasks",
"repeatsDescription": "To set a task as repeating in an interval, simply add '{suffix}' to the task text. The amount needs to be a number and can be omitted to use just the type (see examples)."
}
},
"team": {

View File

@ -102,6 +102,7 @@
"title": "Avatar",
"initials": "Initials",
"gravatar": "Gravatar",
"marble": "Marble",
"upload": "Upload",
"uploadAvatar": "Upload Avatar",
"statusUpdateSuccess": "Avatar status was updated successfully!",
@ -471,7 +472,8 @@
"close": "Close",
"download": "Download",
"showMenu": "Show the menu",
"hideMenu": "Hide the menu"
"hideMenu": "Hide the menu",
"forExample": "For example:"
},
"input": {
"resetColor": "Reset Color",
@ -720,7 +722,9 @@
"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."
"dateTime": "Combine any of the date formats with \"{time}\" (or {timePM}) to set a time.",
"repeats": "Repeating tasks",
"repeatsDescription": "To set a task as repeating in an interval, simply add '{suffix}' to the task text. The amount needs to be a number and can be omitted to use just the type (see examples)."
}
},
"team": {

View File

@ -102,6 +102,7 @@
"title": "Avatar",
"initials": "Initials",
"gravatar": "Gravatar",
"marble": "Marble",
"upload": "Upload",
"uploadAvatar": "Upload Avatar",
"statusUpdateSuccess": "Avatar status was updated successfully!",
@ -471,7 +472,8 @@
"close": "Close",
"download": "Download",
"showMenu": "Show the menu",
"hideMenu": "Hide the menu"
"hideMenu": "Hide the menu",
"forExample": "For example:"
},
"input": {
"resetColor": "Reset Color",
@ -720,7 +722,9 @@
"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."
"dateTime": "Combine any of the date formats with \"{time}\" (or {timePM}) to set a time.",
"repeats": "Repeating tasks",
"repeatsDescription": "To set a task as repeating in an interval, simply add '{suffix}' to the task text. The amount needs to be a number and can be omitted to use just the type (see examples)."
}
},
"team": {

View File

@ -102,6 +102,7 @@
"title": "Аватар",
"initials": "Инициалы",
"gravatar": "Gravatar",
"marble": "Marble",
"upload": "Загрузить файл",
"uploadAvatar": "Загрузить аватар",
"statusUpdateSuccess": "Статус аватара обновлён.",
@ -471,7 +472,8 @@
"close": "Закрыть",
"download": "Скачать",
"showMenu": "Show the menu",
"hideMenu": "Hide the menu"
"hideMenu": "Hide the menu",
"forExample": "For example:"
},
"input": {
"resetColor": "Сбросить цвет",
@ -720,7 +722,9 @@
"dateWeekday": "любой день недели, будет использовать следующую дату",
"dateCurrentYear": "текущий год",
"dateNth": "будет использовать {day}е текущего месяца",
"dateTime": "Комбинируй любой из этих форматов даты с «{time}» (или {timePM}), чтобы установить время."
"dateTime": "Комбинируй любой из этих форматов даты с «{time}» (или {timePM}), чтобы установить время.",
"repeats": "Repeating tasks",
"repeatsDescription": "To set a task as repeating in an interval, simply add '{suffix}' to the task text. The amount needs to be a number and can be omitted to use just the type (see examples)."
}
},
"team": {

View File

@ -102,6 +102,7 @@
"title": "Avatar",
"initials": "Initials",
"gravatar": "Gravatar",
"marble": "Marble",
"upload": "Upload",
"uploadAvatar": "Upload Avatar",
"statusUpdateSuccess": "Avatar status was updated successfully!",
@ -471,7 +472,8 @@
"close": "Close",
"download": "Download",
"showMenu": "Show the menu",
"hideMenu": "Hide the menu"
"hideMenu": "Hide the menu",
"forExample": "For example:"
},
"input": {
"resetColor": "Reset Color",
@ -720,7 +722,9 @@
"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."
"dateTime": "Combine any of the date formats with \"{time}\" (or {timePM}) to set a time.",
"repeats": "Repeating tasks",
"repeatsDescription": "To set a task as repeating in an interval, simply add '{suffix}' to the task text. The amount needs to be a number and can be omitted to use just the type (see examples)."
}
},
"team": {

View File

@ -102,6 +102,7 @@
"title": "Avatar",
"initials": "Initials",
"gravatar": "Gravatar",
"marble": "Marble",
"upload": "Upload",
"uploadAvatar": "Upload Avatar",
"statusUpdateSuccess": "Avatar status was updated successfully!",
@ -471,7 +472,8 @@
"close": "Close",
"download": "Download",
"showMenu": "Show the menu",
"hideMenu": "Hide the menu"
"hideMenu": "Hide the menu",
"forExample": "For example:"
},
"input": {
"resetColor": "Reset Color",
@ -720,7 +722,9 @@
"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."
"dateTime": "Combine any of the date formats with \"{time}\" (or {timePM}) to set a time.",
"repeats": "Repeating tasks",
"repeatsDescription": "To set a task as repeating in an interval, simply add '{suffix}' to the task text. The amount needs to be a number and can be omitted to use just the type (see examples)."
}
},
"team": {

View File

@ -102,6 +102,7 @@
"title": "Avatar",
"initials": "Chữ cái viết tắt",
"gravatar": "Gravatar",
"marble": "Marble",
"upload": "Tải lên",
"uploadAvatar": "Tải lên Avatar",
"statusUpdateSuccess": "Avatar đã được cập nhật!",
@ -471,7 +472,8 @@
"close": "Đóng",
"download": "Tải về",
"showMenu": "Hiển thị menu",
"hideMenu": "Ẩn menu"
"hideMenu": "Ẩn menu",
"forExample": "For example:"
},
"input": {
"resetColor": "Đặt lại màu",
@ -720,7 +722,9 @@
"dateWeekday": "bất kỳ ngày nào trong tuần, sẽ sử dụng ngày tiếp theo với ngày đó",
"dateCurrentYear": "sẽ sử dụng năm hiện tại",
"dateNth": "sẽ sử dụng ngày {day} của tháng hiện tại",
"dateTime": "Kết hợp bất kì đinh dạng ngày với \"{time}\" (hoặc {timePM}) để thiết lập thời gian."
"dateTime": "Kết hợp bất kì đinh dạng ngày với \"{time}\" (hoặc {timePM}) để thiết lập thời gian.",
"repeats": "Repeating tasks",
"repeatsDescription": "To set a task as repeating in an interval, simply add '{suffix}' to the task text. The amount needs to be a number and can be omitted to use just the type (see examples)."
}
},
"team": {

View File

@ -7,14 +7,14 @@ describe('Parse Task Text', () => {
it('should return text with no intents as is', () => {
expect(parseTaskText('Lorem Ipsum').text).toBe('Lorem Ipsum')
})
it('should not parse text when disabled', () => {
const text = 'Lorem Ipsum today *label +list !2 @user'
const result = parseTaskText(text, 'disabled')
expect(result.text).toBe(text)
})
it('should parse text in todoist mode when configured', () => {
const result = parseTaskText('Lorem Ipsum today @label #list !2 +user', 'todoist')
@ -208,7 +208,11 @@ describe('Parse Task Text', () => {
expect(result.text).toBe('Lorem Ipsum')
expect(result.date.getDate()).toBe(date.getDate())
expect(result.date.getMonth()).toBe(result.date.getDate() === 31 ? date.getMonth() + 2 : date.getMonth() + 1)
const nextMonthWithDate = result.date.getDate() === 31
? (date.getMonth() + 2) % 12
: (date.getMonth() + 1) % 12
expect(result.date.getMonth()).toBe(nextMonthWithDate)
})
it('should recognize dates of the month in the future', () => {
const nextDay = new Date(+new Date() + 60 * 60 * 24 * 1000)
@ -506,4 +510,54 @@ describe('Parse Task Text', () => {
expect(result.assignees[0]).toBe('user with long name')
})
})
describe('Recurring Dates', () => {
const cases = {
'every 1 hour': {type: 'hours', amount: 1},
'every hour': {type: 'hours', amount: 1},
'every 5 hours': {type: 'hours', amount: 5},
'every 12 hours': {type: 'hours', amount: 12},
'every day': {type: 'days', amount: 1},
'every 1 day': {type: 'days', amount: 1},
'every 2 days': {type: 'days', amount: 2},
'every week': {type: 'weeks', amount: 1},
'every 1 week': {type: 'weeks', amount: 1},
'every 3 weeks': {type: 'weeks', amount: 3},
'every month': {type: 'months', amount: 1},
'every 1 month': {type: 'months', amount: 1},
'every 2 months': {type: 'months', amount: 2},
'every year': {type: 'years', amount: 1},
'every 1 year': {type: 'years', amount: 1},
'every 4 years': {type: 'years', amount: 4},
'anually': {type: 'years', amount: 1},
'bianually': {type: 'months', amount: 6},
'semiannually': {type: 'months', amount: 6},
'biennially': {type: 'years', amount: 2},
'daily': {type: 'days', amount: 1},
'hourly': {type: 'hours', amount: 1},
'monthly': {type: 'months', amount: 1},
'weekly': {type: 'weeks', amount: 1},
'yearly': {type: 'years', amount: 1},
'every one hour': {type: 'hours', amount: 1}, // maybe unnesecary but better to include it for completeness sake
'every two hours': {type: 'hours', amount: 2},
'every three hours': {type: 'hours', amount: 3},
'every four hours': {type: 'hours', amount: 4},
'every five hours': {type: 'hours', amount: 5},
'every six hours': {type: 'hours', amount: 6},
'every seven hours': {type: 'hours', amount: 7},
'every eight hours': {type: 'hours', amount: 8},
'every nine hours': {type: 'hours', amount: 9},
'every ten hours': {type: 'hours', amount: 10},
}
for (const c in cases) {
it(`should parse ${c} as recurring date every ${cases[c].amount} ${cases[c].type}`, () => {
const result = parseTaskText(`Lorem Ipsum ${c}`)
expect(result.text).toBe('Lorem Ipsum')
expect(result.repeats.type).toBe(cases[c].type)
expect(result.repeats.amount).toBe(cases[c].amount)
})
}
})
})

View File

@ -38,6 +38,24 @@ interface Priorites {
DO_NOW: number,
}
enum RepeatType {
Hours = 'hours',
Days = 'days',
Weeks = 'weeks',
Months = 'months',
Years = 'years',
}
interface Repeats {
type: RepeatType,
amount: number,
}
interface repeatParsedResult {
textWithoutMatched: string,
repeats: Repeats | null,
}
interface ParsedTaskText {
text: string,
date: Date | null,
@ -45,6 +63,7 @@ interface ParsedTaskText {
list: string | null,
priority: number | null,
assignees: string[],
repeats: Repeats | null,
}
interface Prefixes {
@ -67,6 +86,7 @@ export const parseTaskText = (text: string, prefixesMode: PrefixMode = PrefixMod
list: null,
priority: null,
assignees: [],
repeats: null,
}
const prefixes = PREFIXES[prefixesMode]
@ -83,7 +103,11 @@ export const parseTaskText = (text: string, prefixesMode: PrefixMode = PrefixMod
result.assignees = getItemsFromPrefix(text, prefixes.assignee)
const {newText, date} = parseDate(text)
const {textWithoutMatched, repeats} = getRepeats(text)
result.text = textWithoutMatched
result.repeats = repeats
const {newText, date} = parseDate(result.text)
result.text = newText
result.date = date
@ -132,6 +156,113 @@ const getPriority = (text: string, prefix: string): number | null => {
return null
}
const getRepeats = (text: string): repeatParsedResult => {
const regex = /((every|each) (([0-9]+|one|two|three|four|five|six|seven|eight|nine|ten) )?(hours?|days?|weeks?|months?|years?))|anually|bianually|semiannually|biennially|daily|hourly|monthly|weekly|yearly/ig
const results = regex.exec(text)
if (results === null) {
return {
textWithoutMatched: text,
repeats: null,
}
}
let amount = 1
switch (results[3] ? results[3].trim() : undefined) {
case 'one':
amount = 1
break
case 'two':
amount = 2
break
case 'three':
amount = 3
break
case 'four':
amount = 4
break
case 'five':
amount = 5
break
case 'six':
amount = 6
break
case 'seven':
amount = 7
break
case 'eight':
amount = 8
break
case 'nine':
amount = 9
break
case 'ten':
amount = 10
break
default:
amount = results[3] ? parseInt(results[3]) : 1
}
let type: RepeatType = RepeatType.Hours
switch (results[0]) {
case 'biennially':
type = RepeatType.Years
amount = 2
break
case 'bianually':
case 'semiannually':
type = RepeatType.Months
amount = 6
break
case 'yearly':
case 'anually':
type = RepeatType.Years
break
case 'daily':
type = RepeatType.Days
break
case 'hourly':
type = RepeatType.Hours
break
case 'monthly':
type = RepeatType.Months
break
case 'weekly':
type = RepeatType.Weeks
break
default:
switch (results[5]) {
case 'hour':
case 'hours':
type = RepeatType.Hours
break
case 'day':
case 'days':
type = RepeatType.Days
break
case 'week':
case 'weeks':
type = RepeatType.Weeks
break
case 'month':
case 'months':
type = RepeatType.Months
break
case 'year':
case 'years':
type = RepeatType.Years
break
}
}
return {
textWithoutMatched: text.replace(results[0], ''),
repeats: {
amount,
type,
},
}
}
const cleanupItemText = (text: string, items: string[], prefix: string): string => {
items.forEach(l => {
text = text

View File

@ -69,7 +69,7 @@ export default class TaskService extends AbstractService {
// Make the repeating amount to seconds
let repeatAfterSeconds = 0
if (model.repeatAfter.amount !== null || model.repeatAfter.amount !== 0) {
if (model.repeatAfter !== null && (model.repeatAfter.amount !== null || model.repeatAfter.amount !== 0)) {
switch (model.repeatAfter.type) {
case 'hours':
repeatAfterSeconds = model.repeatAfter.amount * 60 * 60

View File

@ -292,6 +292,7 @@ export default {
bucketId: bucketId || 0,
position,
})
task.repeatAfter = parsedTask.repeats
const taskService = new TaskService()
const createdTask = await taskService.create(task)

View File

@ -1,7 +1,7 @@
/* eslint-disable no-console */
/* eslint-disable no-undef */
const workboxVersion = 'v6.4.1'
const workboxVersion = 'v6.4.2'
importScripts( `/workbox-${workboxVersion}/workbox-sw.js`)
workbox.setConfig({
modulePathPrefix: `/workbox-${workboxVersion}`,

View File

@ -47,7 +47,6 @@
v-for="(l, k) in listHistory"
:key="`l${k}`"
:list="l"
:background-resolver="() => null"
/>
</div>
</div>
@ -105,12 +104,8 @@ const migratorsEnabled = computed(() => store.state.config.availableMigrators?.l
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 defaultNamespaceId = computed(() => store.state.namespaces.namespaces?.[0]?.id || 0)
const hasLists = computed (() => 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))

View File

@ -13,6 +13,10 @@
<input name="avatarProvider" type="radio" v-model="avatarProvider" value="gravatar"/>
{{ $t('user.settings.avatar.gravatar') }}
</label>
<label class="radio">
<input name="avatarProvider" type="radio" v-model="avatarProvider" value="marble"/>
{{ $t('user.settings.avatar.marble') }}
</label>
<label class="radio">
<input name="avatarProvider" type="radio" v-model="avatarProvider" value="upload"/>
{{ $t('user.settings.avatar.upload') }}

1363
yarn.lock

File diff suppressed because it is too large Load Diff