forked from vikunja/frontend
Compare commits
37 Commits
8e4095e1bd
...
7187cde253
Author | SHA1 | Date |
---|---|---|
Adrian Simmons | 7187cde253 | |
drone | 306d562f65 | |
renovate | 4b8a7e1556 | |
renovate | d0c6576efa | |
renovate | e684e9a90b | |
drone | 805e1bc554 | |
drone | 8ee793c054 | |
konrad | 1a119f97c5 | |
kolaente | 10fe38cef6 | |
renovate | b4cbe1e1fd | |
konrad | 8b8e413af0 | |
renovate | c8029ec3c4 | |
renovate | 6225c54447 | |
renovate | 809e876091 | |
renovate | 470022899f | |
renovate | 8d1d60ba80 | |
renovate | 028ad3dc14 | |
renovate | f4df628e47 | |
renovate | 150b847638 | |
renovate | 684acc01bd | |
renovate | 3218cf60f0 | |
kolaente | bba9a8e008 | |
renovate | 852d71e8b7 | |
renovate | c65bb4e93b | |
renovate | 1c3f655323 | |
renovate | bd19234041 | |
Dominik Pschenitschni | ac630ac775 | |
renovate | f758eefa88 | |
Dominik Pschenitschni | 4137bab7fc | |
renovate | d253d2e743 | |
renovate | fe5770082a | |
renovate | 2041722b8a | |
renovate | 648b947a05 | |
renovate | f58e114947 | |
renovate | 144e7bd10c | |
Dominik Pschenitschni | b96e89ca8c | |
Dominik Pschenitschni | 20f0496fa5 |
44
package.json
44
package.json
|
@ -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
2
run.sh
|
@ -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}"
|
||||
|
||||
|
|
|
@ -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: {
|
||||
const background = ref(null)
|
||||
const backgroundLoading = ref(false)
|
||||
|
||||
const props = defineProps({
|
||||
list: {
|
||||
type: Object,
|
||||
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) {
|
||||
})
|
||||
|
||||
watch(props.list, loadBackground, { immediate: true })
|
||||
|
||||
async function loadBackground() {
|
||||
if (props.list === null || !props.list.backgroundInformation || backgroundLoading.value) {
|
||||
return
|
||||
}
|
||||
|
||||
this.backgroundLoading = true
|
||||
backgroundLoading.value = true
|
||||
|
||||
const listService = new ListService()
|
||||
try {
|
||||
this.background = await listService.background(this.list)
|
||||
background.value = await listService.background(props.list)
|
||||
} finally {
|
||||
this.backgroundLoading = false
|
||||
backgroundLoading.value = false
|
||||
}
|
||||
},
|
||||
toggleFavoriteList(list) {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
this.$store.dispatch('lists/toggleListFavorite', list)
|
||||
},
|
||||
},
|
||||
store.dispatch('lists/toggleListFavorite', list)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -2,39 +2,26 @@
|
|||
<div
|
||||
v-if="isDone"
|
||||
class="is-done"
|
||||
:class="{ 'is-done--small': variant === variants.SMALL }"
|
||||
: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,
|
||||
}
|
||||
},
|
||||
|
||||
props: {
|
||||
defineProps({
|
||||
isDone: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: VARIANTS.DEFAULT,
|
||||
validator: (variant) => Object.values(VARIANTS).includes(variant),
|
||||
type: String as PropType<Variants>,
|
||||
default: 'default',
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
|
|
|
@ -24,10 +24,8 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'card',
|
||||
props: {
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
|
@ -56,9 +54,9 @@ export default {
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['close'],
|
||||
}
|
||||
})
|
||||
|
||||
defineEmits(['close'])
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -11,10 +11,8 @@
|
|||
</router-link>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'dropdown-item',
|
||||
props: {
|
||||
<script lang="ts" setup>
|
||||
defineProps({
|
||||
to: {
|
||||
required: true,
|
||||
},
|
||||
|
@ -23,6 +21,5 @@ export default {
|
|||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
|
@ -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() {
|
||||
function reload() {
|
||||
window.location.reload()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -2,12 +2,6 @@
|
|||
<div class="loader-container is-loading"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'loading',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.loader-container {
|
||||
height: 100%;
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
<script lang="ts" setup>
|
||||
defineProps({
|
||||
variant: {
|
||||
type: String,
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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,10 +81,7 @@ function getRouteForPagination(page = 1, type = 'list') {
|
|||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'Pagination',
|
||||
|
||||
props: {
|
||||
const props = defineProps({
|
||||
totalPages: {
|
||||
type: Number,
|
||||
required: true,
|
||||
|
@ -91,18 +90,9 @@ export default {
|
|||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
computed: {
|
||||
pages() {
|
||||
return createPagination(this.totalPages, this.currentPage)
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
getRouteForPagination,
|
||||
},
|
||||
}
|
||||
const pages = computed(() => createPagination(props.totalPages, props.currentPage))
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -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,
|
||||
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
|
||||
}
|
||||
},
|
||||
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
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
load()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -7,10 +7,8 @@
|
|||
</component>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'shortcut',
|
||||
props: {
|
||||
<script lang="ts" setup>
|
||||
defineProps({
|
||||
keys: {
|
||||
type: Array,
|
||||
required: true,
|
||||
|
@ -23,8 +21,7 @@ export default {
|
|||
type: String,
|
||||
default: 'div',
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -22,18 +22,16 @@
|
|||
</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: {
|
||||
import {success} from '@/message'
|
||||
|
||||
const props = defineProps({
|
||||
entity: {
|
||||
required: true,
|
||||
type: String,
|
||||
|
@ -48,65 +46,65 @@ export default {
|
|||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
emits: ['change'],
|
||||
computed: {
|
||||
tooltipText() {
|
||||
if (this.disabled) {
|
||||
return this.$t('task.subscription.subscribedThroughParent', {
|
||||
entity: this.entity,
|
||||
parent: this.subscription.entity,
|
||||
})
|
||||
|
||||
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 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 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 this.subscription.entity !== this.entity
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
changeSubscription() {
|
||||
if (this.disabled) {
|
||||
return props.subscription.entity !== props.entity
|
||||
})
|
||||
|
||||
function changeSubscription() {
|
||||
if (disabled.value) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.subscription === null) {
|
||||
this.subscribe()
|
||||
if (props.subscription === null) {
|
||||
subscribe()
|
||||
} else {
|
||||
this.unsubscribe()
|
||||
unsubscribe()
|
||||
}
|
||||
},
|
||||
async subscribe() {
|
||||
}
|
||||
|
||||
async function subscribe() {
|
||||
const subscription = new SubscriptionModel({
|
||||
entity: this.entity,
|
||||
entityId: this.entityId,
|
||||
entity: props.entity,
|
||||
entityId: props.entityId,
|
||||
})
|
||||
await this.subscriptionService.create(subscription)
|
||||
this.$emit('change', subscription)
|
||||
this.$message.success({message: this.$t('task.subscription.subscribeSuccess', {entity: this.entity})})
|
||||
},
|
||||
async unsubscribe() {
|
||||
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: this.entity,
|
||||
entityId: this.entityId,
|
||||
entity: props.entity,
|
||||
entityId: props.entityId,
|
||||
})
|
||||
await this.subscriptionService.delete(subscription)
|
||||
this.$emit('change', null)
|
||||
this.$message.success({message: this.$t('task.subscription.unsubscribeSuccess', {entity: this.entity})})
|
||||
},
|
||||
},
|
||||
await subscriptionService.value.delete(subscription)
|
||||
emit('change', null)
|
||||
success({message: t('task.subscription.unsubscribeSuccess', {entity: props.entity})})
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -11,10 +11,8 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'user',
|
||||
props: {
|
||||
<script lang="ts" setup>
|
||||
defineProps({
|
||||
user: {
|
||||
required: true,
|
||||
type: Object,
|
||||
|
@ -34,8 +32,7 @@ export default {
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -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: '',
|
||||
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
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Multiselect,
|
||||
},
|
||||
computed: {
|
||||
namespaces() {
|
||||
return this.$store.getters['namespaces/searchNamespace'](this.query)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
findNamespaces(query) {
|
||||
this.query = query
|
||||
},
|
||||
select(namespace) {
|
||||
this.$emit('selected', namespace)
|
||||
},
|
||||
},
|
||||
|
||||
function select(namespace) {
|
||||
emit('selected', namespace)
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
DropdownItem,
|
||||
Dropdown,
|
||||
TaskSubscription,
|
||||
},
|
||||
props: {
|
||||
const props = defineProps({
|
||||
namespace: {
|
||||
type: Object, // NamespaceModel
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.subscription = this.namespace.subscription
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const subscription = ref(null)
|
||||
onMounted(() => {
|
||||
subscription.value = props.namespace.subscription
|
||||
})
|
||||
</script>
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
<div class="filter-container">
|
||||
<div class="items">
|
||||
<filter-popup
|
||||
:visible="showTaskFilter"
|
||||
v-model="params"
|
||||
@update:modelValue="loadTasks()"
|
||||
/>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -297,7 +297,8 @@ 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)
|
||||
|
||||
|
@ -309,7 +310,7 @@ const getDayFromText = (text: string) => {
|
|||
date.setDate(day)
|
||||
}
|
||||
|
||||
if (date < new Date()) {
|
||||
if (date < now) {
|
||||
date.setMonth(date.getMonth() + 1)
|
||||
}
|
||||
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -102,6 +102,7 @@
|
|||
"title": "Avatar",
|
||||
"initials": "Initiales",
|
||||
"gravatar": "Gravatar",
|
||||
"marble": "Marble",
|
||||
"upload": "Téléverser",
|
||||
"uploadAvatar": "Téléverser l’avatar",
|
||||
"statusUpdateSuccess": "Statut de l’avatar 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": "n’importe quel jour de la semaine, utilisera la date suivante avec cette date",
|
||||
"dateCurrentYear": "utilisera l’année en cours",
|
||||
"dateNth": "utilisera le {day}e du mois en cours",
|
||||
"dateTime": "Combinez n’importe lequel des formats de date avec « {time} » (ou {timePM}) pour définir une heure."
|
||||
"dateTime": "Combinez n’importe 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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -292,6 +292,7 @@ export default {
|
|||
bucketId: bucketId || 0,
|
||||
position,
|
||||
})
|
||||
task.repeatAfter = parsedTask.repeats
|
||||
|
||||
const taskService = new TaskService()
|
||||
const createdTask = await taskService.create(task)
|
||||
|
|
|
@ -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}`,
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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') }}
|
||||
|
|
Loading…
Reference in New Issue