Merge branch 'main' into feature/search-in-quick-actions
This commit is contained in:
commit
2e345c7931
|
@ -24,12 +24,6 @@ RUN \
|
|||
# Stage 2: copy
|
||||
FROM nginx
|
||||
|
||||
RUN apt-get update && apt-get install -y apt-utils openssl && \
|
||||
mkdir -p /etc/nginx/ssl && \
|
||||
openssl genrsa -out /etc/nginx/ssl/dummy.key 2048 && \
|
||||
openssl req -new -key /etc/nginx/ssl/dummy.key -out /etc/nginx/ssl/dummy.csr -subj "/C=DE/L=Berlin/O=Vikunja/CN=Vikunja Snakeoil" && \
|
||||
openssl x509 -req -days 3650 -in /etc/nginx/ssl/dummy.csr -signkey /etc/nginx/ssl/dummy.key -out /etc/nginx/ssl/dummy.crt
|
||||
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
COPY run.sh /run.sh
|
||||
|
||||
|
|
|
@ -219,10 +219,10 @@ describe('Lists', () => {
|
|||
cy.get('.table-view .filter-container .items .button')
|
||||
.contains('Columns')
|
||||
.click()
|
||||
cy.get('.table-view .filter-container .card .card-content .fancycheckbox .check')
|
||||
cy.get('.table-view .filter-container .card.columns-filter .card-content .fancycheckbox .check')
|
||||
.contains('Priority')
|
||||
.click()
|
||||
cy.get('.table-view .filter-container .card .card-content .fancycheckbox .check')
|
||||
cy.get('.table-view .filter-container .card.columns-filter .card-content .fancycheckbox .check')
|
||||
.contains('Done')
|
||||
.click()
|
||||
|
||||
|
|
|
@ -60,15 +60,11 @@ http {
|
|||
server {
|
||||
listen 80;
|
||||
listen 81 default_server http2 proxy_protocol; ## Needed when behind HAProxy with SSL termination + HTTP/2 support
|
||||
listen 443 default_server ssl http2;
|
||||
|
||||
server_name _;
|
||||
|
||||
expires $expires;
|
||||
|
||||
ssl_certificate /etc/nginx/ssl/dummy.crt;
|
||||
ssl_certificate_key /etc/nginx/ssl/dummy.key;
|
||||
|
||||
location ~* .(txt|webmanifest|css|js|mjs|map|svg|jpg|jpeg|png|ico|ttf|woff|woff2|wav)$ {
|
||||
root /usr/share/nginx/html;
|
||||
try_files $uri $uri/ =404;
|
||||
|
|
69
src/App.vue
69
src/App.vue
|
@ -1,25 +1,21 @@
|
|||
<template>
|
||||
<div :class="{'is-touch': isTouch}">
|
||||
<div :class="{'is-hidden': !online}">
|
||||
<!-- This is a workaround to get the sw to "see" the to-be-cached version of the offline background image -->
|
||||
<div class="offline" style="height: 0;width: 0;"></div>
|
||||
<top-navigation v-if="authUser"/>
|
||||
<content-auth v-if="authUser"/>
|
||||
<content-link-share v-else-if="authLinkShare"/>
|
||||
<content-no-auth v-else/>
|
||||
<notification/>
|
||||
</div>
|
||||
<div class="app offline" v-if="!online">
|
||||
<div class="offline-message">
|
||||
<h1>You are offline.</h1>
|
||||
<p>Please check your network connection and try again.</p>
|
||||
<ready>
|
||||
<div :class="{'is-touch': isTouch}">
|
||||
<div :class="{'is-hidden': !online}">
|
||||
<template v-if="authUser">
|
||||
<top-navigation/>
|
||||
<content-auth/>
|
||||
</template>
|
||||
<content-link-share v-else-if="authLinkShare"/>
|
||||
<content-no-auth v-else/>
|
||||
<notification/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<transition name="fade">
|
||||
<keyboard-shortcuts v-if="keyboardShortcutsActive"/>
|
||||
</transition>
|
||||
</div>
|
||||
<transition name="fade">
|
||||
<keyboard-shortcuts v-if="keyboardShortcutsActive"/>
|
||||
</transition>
|
||||
</div>
|
||||
</ready>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -36,6 +32,7 @@ import ContentLinkShare from './components/home/contentLinkShare'
|
|||
import ContentNoAuth from './components/home/contentNoAuth'
|
||||
import {setLanguage} from './i18n'
|
||||
import AccountDeleteService from '@/services/accountDelete'
|
||||
import Ready from '@/components/misc/ready'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'app',
|
||||
|
@ -46,6 +43,7 @@ export default defineComponent({
|
|||
TopNavigation,
|
||||
KeyboardShortcuts,
|
||||
Notification,
|
||||
Ready,
|
||||
},
|
||||
beforeMount() {
|
||||
this.setupOnlineStatus()
|
||||
|
@ -54,13 +52,6 @@ export default defineComponent({
|
|||
this.setupAccountDeletionVerification()
|
||||
},
|
||||
beforeCreate() {
|
||||
// FIXME: async action in beforeCreate, might be not finished when component mounts
|
||||
this.$store.dispatch('config/update')
|
||||
.then(() => {
|
||||
this.$store.dispatch('auth/checkAuth')
|
||||
})
|
||||
this.$store.dispatch('auth/checkAuth')
|
||||
|
||||
setLanguage()
|
||||
},
|
||||
created() {
|
||||
|
@ -121,29 +112,3 @@ export default defineComponent({
|
|||
<style lang="scss">
|
||||
@import '@/styles/global.scss';
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.offline {
|
||||
background: url('@/assets/llama-nightscape.jpg') no-repeat center;
|
||||
background-size: cover;
|
||||
height: 100vh;
|
||||
|
||||
.offline-message {
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
width: 100vw;
|
||||
bottom: 5vh;
|
||||
color: $white;
|
||||
padding: 0 1rem;
|
||||
|
||||
h1 {
|
||||
font-weight: bold;
|
||||
font-size: 1.5rem;
|
||||
text-align: center;
|
||||
color: $white;
|
||||
font-weight: 700 !important;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,37 +1,20 @@
|
|||
<template>
|
||||
<div class="no-auth-wrapper">
|
||||
<div class="noauth-container">
|
||||
<Logo width="400" height="117" />
|
||||
<div class="message is-info" v-if="motd !== ''">
|
||||
<div class="message-header">
|
||||
<p>{{ $t('misc.info') }}</p>
|
||||
</div>
|
||||
<div class="message-body">
|
||||
{{ motd }}
|
||||
</div>
|
||||
</div>
|
||||
<router-view/>
|
||||
</div>
|
||||
</div>
|
||||
<no-auth-wrapper>
|
||||
<router-view/>
|
||||
</no-auth-wrapper>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapState} from 'vuex'
|
||||
|
||||
import Logo from '@/components/home/Logo.vue'
|
||||
|
||||
import { saveLastVisited } from '@/helpers/saveLastVisited'
|
||||
import {saveLastVisited} from '@/helpers/saveLastVisited'
|
||||
import NoAuthWrapper from '@/components/misc/no-auth-wrapper'
|
||||
|
||||
export default {
|
||||
name: 'contentNoAuth',
|
||||
components: { Logo },
|
||||
components: {NoAuthWrapper},
|
||||
computed: {
|
||||
routeName() {
|
||||
return this.$route.name
|
||||
},
|
||||
...mapState({
|
||||
motd: state => state.config.motd,
|
||||
}),
|
||||
},
|
||||
watch: {
|
||||
routeName: {
|
||||
|
@ -62,17 +45,3 @@ export default {
|
|||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.no-auth-wrapper {
|
||||
background: url('@/assets/llama.svg?url') no-repeat bottom left fixed $light-background;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.noauth-container {
|
||||
max-width: 450px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
</style>
|
|
@ -48,7 +48,7 @@
|
|||
</ul>
|
||||
</div>
|
||||
|
||||
<aside class="menu namespaces-lists loader-container" :class="{'is-loading': loading}">
|
||||
<aside class="menu namespaces-lists loader-container is-loading-small" :class="{'is-loading': loading}">
|
||||
<template v-for="(n, nk) in namespaces" :key="n.id" >
|
||||
<div class="namespace-title" :class="{'has-menu': n.id > 0}">
|
||||
<span
|
||||
|
@ -105,7 +105,7 @@
|
|||
>
|
||||
<template #item="{element: l}">
|
||||
<li
|
||||
class="loader-container"
|
||||
class="loader-container is-loading-small"
|
||||
:class="{'is-loading': listUpdating[l.id]}"
|
||||
>
|
||||
<router-link
|
||||
|
@ -449,14 +449,6 @@ $vikunja-nav-selected-width: 0.4rem;
|
|||
&:hover :deep(.dropdown-trigger) {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.loader-container.is-loading:after {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
top: calc(50% - .75rem);
|
||||
left: calc(50% - .75rem);
|
||||
border-width: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.flip-list-move {
|
||||
|
@ -533,14 +525,6 @@ $vikunja-nav-selected-width: 0.4rem;
|
|||
padding-top: math.div($navbar-padding, 2);
|
||||
}
|
||||
|
||||
&.loader-container.is-loading:after {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
top: calc(50% - .75rem);
|
||||
left: calc(50% - .75rem);
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: $grey-400 !important;
|
||||
}
|
||||
|
|
|
@ -1,37 +1,49 @@
|
|||
<template>
|
||||
<transition name="fade">
|
||||
<filters
|
||||
v-if="visibleInternal"
|
||||
v-model="value"
|
||||
ref="filters"
|
||||
/>
|
||||
</transition>
|
||||
<x-button
|
||||
v-if="hasFilters"
|
||||
type="secondary"
|
||||
@click="clearFilters"
|
||||
>
|
||||
{{ $t('filters.clear') }}
|
||||
</x-button>
|
||||
<popup>
|
||||
<template #trigger="{toggle}">
|
||||
<x-button
|
||||
@click.prevent.stop="toggle()"
|
||||
type="secondary"
|
||||
icon="filter"
|
||||
>
|
||||
{{ $t('filters.title') }}
|
||||
</x-button>
|
||||
</template>
|
||||
<template #content="{isOpen}">
|
||||
<filters
|
||||
v-model="value"
|
||||
ref="filters"
|
||||
class="filter-popup"
|
||||
:class="{'is-open': isOpen}"
|
||||
/>
|
||||
</template>
|
||||
</popup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
|
||||
import Filters from '../../../components/list/partials/filters'
|
||||
import Filters from '@/components/list/partials/filters'
|
||||
import {getDefaultParams} from '@/components/tasks/mixins/taskList'
|
||||
import Popup from '@/components/misc/popup'
|
||||
|
||||
export default {
|
||||
name: 'filter-popup',
|
||||
components: {
|
||||
Popup,
|
||||
Filters,
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
required: true,
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
data() {
|
||||
return {
|
||||
visibleInternal: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
value: {
|
||||
get() {
|
||||
|
@ -41,34 +53,46 @@ export default {
|
|||
this.$emit('update:modelValue', value)
|
||||
},
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener('click', this.hidePopup)
|
||||
},
|
||||
beforeUnmount() {
|
||||
document.removeEventListener('click', this.hidePopup)
|
||||
hasFilters() {
|
||||
// this.value also contains the page parameter which we don't want to include in filters
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const {filter_by, filter_value, filter_comparator, filter_concat, s} = this.value
|
||||
const def = {...getDefaultParams()}
|
||||
|
||||
const params = {filter_by, filter_value, filter_comparator, filter_concat, s}
|
||||
const defaultParams = {
|
||||
filter_by: def.filter_by,
|
||||
filter_value: def.filter_value,
|
||||
filter_comparator: def.filter_comparator,
|
||||
filter_concat: def.filter_concat,
|
||||
s: s ? def.s : undefined,
|
||||
}
|
||||
|
||||
return JSON.stringify(params) !== JSON.stringify(defaultParams)
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
modelValue: {
|
||||
handler(value) {
|
||||
this.params = value
|
||||
this.value = value
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
visible() {
|
||||
this.visibleInternal = !this.visibleInternal
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
hidePopup(e) {
|
||||
if (!this.visibleInternal) {
|
||||
return
|
||||
}
|
||||
|
||||
closeWhenClickedOutside(e, this.$refs.filters.$el, () => {
|
||||
this.visibleInternal = false
|
||||
})
|
||||
clearFilters() {
|
||||
this.value = {...getDefaultParams()}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.filter-popup {
|
||||
margin: 0;
|
||||
|
||||
&.is-open {
|
||||
margin: 2rem 0 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -458,15 +458,7 @@ export default {
|
|||
return
|
||||
}
|
||||
|
||||
let foundDone = false
|
||||
this.params.filter_by.forEach((f, i) => {
|
||||
if (f === 'done') {
|
||||
foundDone = i
|
||||
}
|
||||
})
|
||||
if (foundDone === false) {
|
||||
this.filters.done = true
|
||||
}
|
||||
this.filters.done = this.params.filter_by.some((f) => f === 'done') === false
|
||||
},
|
||||
async prepareRelatedObjectFilter(kind, filterName = null, servicePrefix = null) {
|
||||
if (filterName === null) {
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
<i18n-t keypath="apiConfig.signInOn">
|
||||
<span class="url" v-tooltip="apiUrl"> {{ apiDomain }} </span>
|
||||
</i18n-t>
|
||||
<br />
|
||||
<br/>
|
||||
<a @click="() => (configureApi = true)">{{ $t('apiConfig.change') }}</a>
|
||||
</div>
|
||||
|
||||
|
@ -46,9 +46,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { parseURL } from 'ufo'
|
||||
|
||||
const API_DEFAULT_PORT = 3456
|
||||
import {parseURL} from 'ufo'
|
||||
import {checkAndSetApiUrl} from '@/helpers/checkAndSetApiUrl'
|
||||
|
||||
export default {
|
||||
name: 'apiConfig',
|
||||
|
@ -71,128 +70,48 @@ export default {
|
|||
return parseURL(this.apiUrl).host
|
||||
},
|
||||
},
|
||||
props: {
|
||||
configureOpen: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
configureOpen: {
|
||||
handler(value) {
|
||||
this.configureApi = value
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setApiUrl() {
|
||||
async setApiUrl() {
|
||||
if (this.apiUrl === '') {
|
||||
// Don't try to check and set an empty url
|
||||
this.errorMsg = this.$t('apiConfig.urlRequired')
|
||||
return
|
||||
}
|
||||
|
||||
let urlToCheck = this.apiUrl
|
||||
try {
|
||||
const url = await checkAndSetApiUrl(this.apiUrl)
|
||||
|
||||
// Check if the url has an http prefix
|
||||
if (
|
||||
!urlToCheck.startsWith('http://') &&
|
||||
!urlToCheck.startsWith('https://')
|
||||
) {
|
||||
urlToCheck = `http://${urlToCheck}`
|
||||
if (url === '') {
|
||||
// If the config setter function could not figure out a url
|
||||
throw new Error('URL cannot be empty.')
|
||||
}
|
||||
|
||||
// Set it + save it to local storage to save us the hoops
|
||||
this.errorMsg = ''
|
||||
this.successMsg = this.$t('apiConfig.success', {domain: this.apiDomain})
|
||||
this.configureApi = false
|
||||
this.apiUrl = url
|
||||
this.$emit('foundApi', this.apiUrl)
|
||||
} catch (e) {
|
||||
// Still not found, url is still invalid
|
||||
this.successMsg = ''
|
||||
this.errorMsg = this.$t('apiConfig.error', {domain: this.apiDomain})
|
||||
}
|
||||
|
||||
urlToCheck = new URL(urlToCheck)
|
||||
const origUrlToCheck = urlToCheck
|
||||
|
||||
const oldUrl = window.API_URL
|
||||
window.API_URL = urlToCheck.toString()
|
||||
|
||||
// Check if the api is reachable at the provided url
|
||||
this.$store
|
||||
.dispatch('config/update')
|
||||
.catch((e) => {
|
||||
// Check if it is reachable at /api/v1 and http
|
||||
if (
|
||||
!urlToCheck.pathname.endsWith('/api/v1') &&
|
||||
!urlToCheck.pathname.endsWith('/api/v1/')
|
||||
) {
|
||||
urlToCheck.pathname = `${urlToCheck.pathname}api/v1`
|
||||
window.API_URL = urlToCheck.toString()
|
||||
return this.$store.dispatch('config/update')
|
||||
}
|
||||
throw e
|
||||
})
|
||||
.catch((e) => {
|
||||
// Check if it has a port and if not check if it is reachable at https
|
||||
if (urlToCheck.protocol === 'http:') {
|
||||
urlToCheck.protocol = 'https:'
|
||||
window.API_URL = urlToCheck.toString()
|
||||
return this.$store.dispatch('config/update')
|
||||
}
|
||||
throw e
|
||||
})
|
||||
.catch((e) => {
|
||||
// Check if it is reachable at /api/v1 and https
|
||||
urlToCheck.pathname = origUrlToCheck.pathname
|
||||
if (
|
||||
!urlToCheck.pathname.endsWith('/api/v1') &&
|
||||
!urlToCheck.pathname.endsWith('/api/v1/')
|
||||
) {
|
||||
urlToCheck.pathname = `${urlToCheck.pathname}api/v1`
|
||||
window.API_URL = urlToCheck.toString()
|
||||
return this.$store.dispatch('config/update')
|
||||
}
|
||||
throw e
|
||||
})
|
||||
.catch((e) => {
|
||||
// Check if it is reachable at port API_DEFAULT_PORT and https
|
||||
if (urlToCheck.port !== API_DEFAULT_PORT) {
|
||||
urlToCheck.protocol = 'https:'
|
||||
urlToCheck.port = API_DEFAULT_PORT
|
||||
window.API_URL = urlToCheck.toString()
|
||||
return this.$store.dispatch('config/update')
|
||||
}
|
||||
throw e
|
||||
})
|
||||
.catch((e) => {
|
||||
// Check if it is reachable at :API_DEFAULT_PORT and /api/v1 and https
|
||||
urlToCheck.pathname = origUrlToCheck.pathname
|
||||
if (
|
||||
!urlToCheck.pathname.endsWith('/api/v1') &&
|
||||
!urlToCheck.pathname.endsWith('/api/v1/')
|
||||
) {
|
||||
urlToCheck.pathname = `${urlToCheck.pathname}api/v1`
|
||||
window.API_URL = urlToCheck.toString()
|
||||
return this.$store.dispatch('config/update')
|
||||
}
|
||||
throw e
|
||||
})
|
||||
.catch((e) => {
|
||||
// Check if it is reachable at port API_DEFAULT_PORT and http
|
||||
if (urlToCheck.port !== API_DEFAULT_PORT) {
|
||||
urlToCheck.protocol = 'http:'
|
||||
urlToCheck.port = API_DEFAULT_PORT
|
||||
window.API_URL = urlToCheck.toString()
|
||||
return this.$store.dispatch('config/update')
|
||||
}
|
||||
throw e
|
||||
})
|
||||
.catch((e) => {
|
||||
// Check if it is reachable at :API_DEFAULT_PORT and /api/v1 and http
|
||||
urlToCheck.pathname = origUrlToCheck.pathname
|
||||
if (
|
||||
!urlToCheck.pathname.endsWith('/api/v1') &&
|
||||
!urlToCheck.pathname.endsWith('/api/v1/')
|
||||
) {
|
||||
urlToCheck.pathname = `${urlToCheck.pathname}api/v1`
|
||||
window.API_URL = urlToCheck.toString()
|
||||
return this.$store.dispatch('config/update')
|
||||
}
|
||||
throw e
|
||||
})
|
||||
.catch(() => {
|
||||
// Still not found, url is still invalid
|
||||
this.successMsg = ''
|
||||
this.errorMsg = this.$t('apiConfig.error', {domain: this.apiDomain})
|
||||
window.API_URL = oldUrl
|
||||
})
|
||||
.then((r) => {
|
||||
if (typeof r !== 'undefined') {
|
||||
// Set it + save it to local storage to save us the hoops
|
||||
this.errorMsg = ''
|
||||
this.successMsg = this.$t('apiConfig.success', {domain: this.apiDomain})
|
||||
localStorage.setItem('API_URL', window.API_URL)
|
||||
this.configureApi = false
|
||||
this.apiUrl = window.API_URL
|
||||
this.$emit('foundApi', this.apiUrl)
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -200,15 +119,15 @@ export default {
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.api-config {
|
||||
margin-bottom: .75rem;
|
||||
margin-bottom: .75rem;
|
||||
}
|
||||
|
||||
.api-url-info {
|
||||
font-size: .9rem;
|
||||
text-align: right;
|
||||
font-size: .9rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
span.url {
|
||||
border-bottom: 1px dashed $primary;
|
||||
}
|
||||
.url {
|
||||
border-bottom: 1px dashed $primary;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,39 @@
|
|||
<template>
|
||||
<div class="no-auth-wrapper">
|
||||
<div class="noauth-container">
|
||||
<Logo width="400" height="117" />
|
||||
<div class="message is-info" v-if="motd !== ''">
|
||||
<div class="message-header">
|
||||
<p>{{ $t('misc.info') }}</p>
|
||||
</div>
|
||||
<div class="message-body">
|
||||
{{ motd }}
|
||||
</div>
|
||||
</div>
|
||||
<slot/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Logo from '@/components/home/Logo.vue'
|
||||
import {useStore} from 'vuex'
|
||||
import {computed} from 'vue'
|
||||
|
||||
const store = useStore()
|
||||
const motd = computed(() => store.state.config.motd)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.no-auth-wrapper {
|
||||
background: url('@/assets/llama.svg') no-repeat bottom left fixed $light-background;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.noauth-container {
|
||||
max-width: 450px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,54 @@
|
|||
<template>
|
||||
<slot name="trigger" :isOpen="open" :toggle="toggle"></slot>
|
||||
<div class="popup" :class="{'is-open': open}" ref="popup">
|
||||
<slot name="content" :isOpen="open"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
|
||||
import {onBeforeUnmount, onMounted, ref} from 'vue'
|
||||
|
||||
const open = ref(false)
|
||||
const popup = ref(null)
|
||||
|
||||
const toggle = () => {
|
||||
open.value = !open.value
|
||||
}
|
||||
|
||||
function hidePopup(e) {
|
||||
if (!open.value) {
|
||||
return
|
||||
}
|
||||
|
||||
// we actually want to use popup.$el, not its value.
|
||||
// eslint-disable-next-line vue/no-ref-as-operand
|
||||
closeWhenClickedOutside(e, popup.value, () => {
|
||||
open.value = false
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', hidePopup)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', hidePopup)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.popup {
|
||||
transition: opacity $transition;
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
|
||||
&.is-open {
|
||||
opacity: 1;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,141 @@
|
|||
<template>
|
||||
<!-- This is a workaround to get the sw to "see" the to-be-cached version of the offline background image -->
|
||||
<div class="offline" style="height: 0;width: 0;"></div>
|
||||
<div class="app offline" v-if="!online">
|
||||
<div class="offline-message">
|
||||
<h1>{{ $t('offline.title') }}</h1>
|
||||
<p>{{ $t('offline.text') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<template v-else-if="ready">
|
||||
<slot/>
|
||||
</template>
|
||||
<section v-else-if="error !== ''">
|
||||
<no-auth-wrapper>
|
||||
<card>
|
||||
<p v-if="error === errorNoApiUrl">
|
||||
{{ $t('ready.noApiUrlConfigured') }}
|
||||
</p>
|
||||
<div class="notification is-danger" v-else>
|
||||
<p>
|
||||
{{ $t('ready.errorOccured') }}<br/>
|
||||
{{ error }}
|
||||
</p>
|
||||
<p>
|
||||
{{ $t('ready.checkApiUrl') }}
|
||||
</p>
|
||||
</div>
|
||||
<api-config :configure-open="true" @found-api="load"/>
|
||||
</card>
|
||||
</no-auth-wrapper>
|
||||
</section>
|
||||
<transition name="fade">
|
||||
<section class="vikunja-loading" v-if="showLoading">
|
||||
<img alt="Vikunja" :src="logoUrl" width="100" height="100"/>
|
||||
<p>
|
||||
<span class="loader-container is-loading-small is-loading"></span>
|
||||
{{ $t('ready.loading') }}
|
||||
</p>
|
||||
</section>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import logoUrl from '@/assets/logo.svg'
|
||||
import ApiConfig from '@/components/misc/api-config'
|
||||
import NoAuthWrapper from '@/components/misc/no-auth-wrapper'
|
||||
import {mapState} from 'vuex'
|
||||
import {ERROR_NO_API_URL} from '@/helpers/checkAndSetApiUrl'
|
||||
|
||||
export default {
|
||||
name: 'ready',
|
||||
components: {
|
||||
NoAuthWrapper,
|
||||
ApiConfig,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
logoUrl,
|
||||
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
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vikunja-loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background: $grey-100;
|
||||
z-index: 99;
|
||||
|
||||
img {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.loader-container {
|
||||
margin-right: 1rem;
|
||||
|
||||
&.is-loading::after {
|
||||
border-left-color: $grey-400;
|
||||
border-bottom-color: $grey-400;
|
||||
}
|
||||
}
|
||||
|
||||
.offline {
|
||||
background: url('@/assets/llama-nightscape.jpg') no-repeat center;
|
||||
background-size: cover;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.offline-message {
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
width: 100vw;
|
||||
bottom: 5vh;
|
||||
color: $white;
|
||||
padding: 0 1rem;
|
||||
|
||||
h1 {
|
||||
font-weight: bold;
|
||||
font-size: 1.5rem;
|
||||
text-align: center;
|
||||
color: $white;
|
||||
font-weight: 700 !important;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -9,12 +9,12 @@
|
|||
>
|
||||
{{ $t('filters.title') }}
|
||||
</x-button>
|
||||
<filter-popup
|
||||
:visible="showTaskFilter"
|
||||
v-model="params"
|
||||
@update:modelValue="loadTasks()"
|
||||
/>
|
||||
</div>
|
||||
<filter-popup
|
||||
:visible="showTaskFilter"
|
||||
v-model="params"
|
||||
@update:modelValue="loadTasks()"
|
||||
/>
|
||||
</div>
|
||||
<div class="dates">
|
||||
<template v-for="(y, yk) in days" :key="yk + 'year'">
|
||||
|
@ -347,7 +347,7 @@ export default {
|
|||
return
|
||||
}
|
||||
|
||||
let newTask = { ...taskDragged }
|
||||
let newTask = {...taskDragged}
|
||||
|
||||
const didntHaveDates = newTask.startDate === null ? true : false
|
||||
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import TaskCollectionService from '@/services/taskCollection'
|
||||
|
||||
// FIXME: merge with DEFAULT_PARAMS in filters.vue
|
||||
const DEFAULT_PARAMS = {
|
||||
export const getDefaultParams = () => ({
|
||||
sort_by: ['position', 'id'],
|
||||
order_by: ['asc', 'desc'],
|
||||
filter_by: ['done'],
|
||||
filter_value: ['false'],
|
||||
filter_comparator: ['equals'],
|
||||
filter_concat: 'and',
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* This mixin provides a base set of methods and properties to get tasks on a list.
|
||||
|
@ -26,7 +26,7 @@ export default {
|
|||
searchTerm: '',
|
||||
|
||||
showTaskFilter: false,
|
||||
params: DEFAULT_PARAMS,
|
||||
params: {...getDefaultParams()},
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
@ -94,7 +94,7 @@ export default {
|
|||
this.initTasks(page, search)
|
||||
},
|
||||
loadTasksOnSavedFilter() {
|
||||
if(typeof this.$route.params.listId !== 'undefined' && parseInt(this.$route.params.listId) < 0) {
|
||||
if (typeof this.$route.params.listId !== 'undefined' && parseInt(this.$route.params.listId) < 0) {
|
||||
this.loadTasks(1, '', null, true)
|
||||
}
|
||||
},
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
import {store} from '@/store'
|
||||
|
||||
const API_DEFAULT_PORT = '3456'
|
||||
|
||||
export const ERROR_NO_API_URL = 'noApiUrlProvided'
|
||||
|
||||
const updateConfig = () => store.dispatch('config/update')
|
||||
|
||||
export const checkAndSetApiUrl = (url: string): Promise<string> => {
|
||||
// Check if the url has an http prefix
|
||||
if (
|
||||
!url.startsWith('http://') &&
|
||||
!url.startsWith('https://')
|
||||
) {
|
||||
url = `http://${url}`
|
||||
}
|
||||
|
||||
const urlToCheck: URL = new URL(url)
|
||||
const origUrlToCheck = urlToCheck
|
||||
|
||||
const oldUrl = window.API_URL
|
||||
window.API_URL = urlToCheck.toString()
|
||||
|
||||
// Check if the api is reachable at the provided url
|
||||
return updateConfig()
|
||||
.catch(e => {
|
||||
// Check if it is reachable at /api/v1 and http
|
||||
if (
|
||||
!urlToCheck.pathname.endsWith('/api/v1') &&
|
||||
!urlToCheck.pathname.endsWith('/api/v1/')
|
||||
) {
|
||||
urlToCheck.pathname = `${urlToCheck.pathname}api/v1`
|
||||
window.API_URL = urlToCheck.toString()
|
||||
return updateConfig()
|
||||
}
|
||||
throw e
|
||||
})
|
||||
.catch(e => {
|
||||
// Check if it has a port and if not check if it is reachable at https
|
||||
if (urlToCheck.protocol === 'http:') {
|
||||
urlToCheck.protocol = 'https:'
|
||||
window.API_URL = urlToCheck.toString()
|
||||
return updateConfig()
|
||||
}
|
||||
throw e
|
||||
})
|
||||
.catch(e => {
|
||||
// Check if it is reachable at /api/v1 and https
|
||||
urlToCheck.pathname = origUrlToCheck.pathname
|
||||
if (
|
||||
!urlToCheck.pathname.endsWith('/api/v1') &&
|
||||
!urlToCheck.pathname.endsWith('/api/v1/')
|
||||
) {
|
||||
urlToCheck.pathname = `${urlToCheck.pathname}api/v1`
|
||||
window.API_URL = urlToCheck.toString()
|
||||
return updateConfig()
|
||||
}
|
||||
throw e
|
||||
})
|
||||
.catch(e => {
|
||||
// Check if it is reachable at port API_DEFAULT_PORT and https
|
||||
if (urlToCheck.port !== API_DEFAULT_PORT) {
|
||||
urlToCheck.protocol = 'https:'
|
||||
urlToCheck.port = API_DEFAULT_PORT
|
||||
window.API_URL = urlToCheck.toString()
|
||||
return updateConfig()
|
||||
}
|
||||
throw e
|
||||
})
|
||||
.catch(e => {
|
||||
// Check if it is reachable at :API_DEFAULT_PORT and /api/v1 and https
|
||||
urlToCheck.pathname = origUrlToCheck.pathname
|
||||
if (
|
||||
!urlToCheck.pathname.endsWith('/api/v1') &&
|
||||
!urlToCheck.pathname.endsWith('/api/v1/')
|
||||
) {
|
||||
urlToCheck.pathname = `${urlToCheck.pathname}api/v1`
|
||||
window.API_URL = urlToCheck.toString()
|
||||
return updateConfig()
|
||||
}
|
||||
throw e
|
||||
})
|
||||
.catch(e => {
|
||||
// Check if it is reachable at port API_DEFAULT_PORT and http
|
||||
if (urlToCheck.port !== API_DEFAULT_PORT) {
|
||||
urlToCheck.protocol = 'http:'
|
||||
urlToCheck.port = API_DEFAULT_PORT
|
||||
window.API_URL = urlToCheck.toString()
|
||||
return updateConfig()
|
||||
}
|
||||
throw e
|
||||
})
|
||||
.catch(e => {
|
||||
// Check if it is reachable at :API_DEFAULT_PORT and /api/v1 and http
|
||||
urlToCheck.pathname = origUrlToCheck.pathname
|
||||
if (
|
||||
!urlToCheck.pathname.endsWith('/api/v1') &&
|
||||
!urlToCheck.pathname.endsWith('/api/v1/')
|
||||
) {
|
||||
urlToCheck.pathname = `${urlToCheck.pathname}api/v1`
|
||||
window.API_URL = urlToCheck.toString()
|
||||
return updateConfig()
|
||||
}
|
||||
throw e
|
||||
})
|
||||
.catch(e => {
|
||||
window.API_URL = oldUrl
|
||||
throw e
|
||||
})
|
||||
.then(r => {
|
||||
if (typeof r !== 'undefined') {
|
||||
localStorage.setItem('API_URL', window.API_URL)
|
||||
return window.API_URL
|
||||
}
|
||||
|
||||
throw new Error(ERROR_NO_API_URL)
|
||||
})
|
||||
}
|
|
@ -16,6 +16,16 @@
|
|||
"title": "Not found",
|
||||
"text": "The page you requested does not exist."
|
||||
},
|
||||
"ready": {
|
||||
"loading": "Vikunja is loading…",
|
||||
"errorOccured": "An error occured:",
|
||||
"checkApiUrl": "Please check if the api url is correct.",
|
||||
"noApiUrlConfigured": "No API url was configured. Please set one below:"
|
||||
},
|
||||
"offline": {
|
||||
"title": "You are offline.",
|
||||
"text": "Please check your network connection and try again."
|
||||
},
|
||||
"user": {
|
||||
"auth": {
|
||||
"username": "Username",
|
||||
|
@ -344,6 +354,7 @@
|
|||
},
|
||||
"filters": {
|
||||
"title": "Filters",
|
||||
"clear": "Clear Filters",
|
||||
"attributes": {
|
||||
"title": "Title",
|
||||
"titlePlaceholder": "The saved filter title goes here…",
|
||||
|
@ -778,8 +789,9 @@
|
|||
"urlPlaceholder": "eg. https://localhost:3456",
|
||||
"change": "change",
|
||||
"signInOn": "Sign in to your Vikunja account on {0}",
|
||||
"error": "Could not find or use Vikunja installation at \"{domain}\".",
|
||||
"success": "Using Vikunja installation at \"{domain}\"."
|
||||
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
|
||||
"success": "Using Vikunja installation at \"{domain}\".",
|
||||
"urlRequired": "A url is required."
|
||||
},
|
||||
"loadingError": {
|
||||
"failed": "Loading failed, please {0}. If the error persists, please {1}.",
|
||||
|
|
|
@ -19,6 +19,7 @@ import attachments from './modules/attachments'
|
|||
import labels from './modules/labels'
|
||||
|
||||
import ListService from '../services/list'
|
||||
import {checkAndSetApiUrl} from '@/helpers/checkAndSetApiUrl'
|
||||
|
||||
export const store = createStore({
|
||||
strict: import.meta.env.DEV,
|
||||
|
@ -43,6 +44,7 @@ export const store = createStore({
|
|||
menuActive: true,
|
||||
keyboardShortcutsActive: false,
|
||||
quickActionsActive: false,
|
||||
vikunjaReady: false,
|
||||
},
|
||||
mutations: {
|
||||
[LOADING](state, loading) {
|
||||
|
@ -84,6 +86,9 @@ export const store = createStore({
|
|||
[BACKGROUND](state, background) {
|
||||
state.background = background
|
||||
},
|
||||
vikunjaReady(state, ready) {
|
||||
state.vikunjaReady = ready
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async [CURRENT_LIST]({state, commit}, currentList) {
|
||||
|
@ -138,5 +143,10 @@ export const store = createStore({
|
|||
|
||||
commit(CURRENT_LIST, currentList)
|
||||
},
|
||||
async loadApp({commit, dispatch}) {
|
||||
await checkAndSetApiUrl(window.API_URL)
|
||||
await dispatch('auth/checkAuth')
|
||||
commit('vikunjaReady', true)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
@ -34,7 +34,6 @@ $filter-container-top-link-share-list: -47px;
|
|||
|
||||
.card {
|
||||
text-align: left;
|
||||
margin-top: calc(1rem - 1px);
|
||||
}
|
||||
|
||||
.fancycheckbox {
|
||||
|
@ -47,10 +46,6 @@ $filter-container-top-link-share-list: -47px;
|
|||
justify-content: space-between;
|
||||
margin-right: .5rem;
|
||||
|
||||
.button, .input {
|
||||
height: $switch-view-height;
|
||||
}
|
||||
|
||||
.field {
|
||||
transition: width $transition;
|
||||
width: 100%;
|
||||
|
|
|
@ -1,30 +1,38 @@
|
|||
// FIXME: move to loading.vue
|
||||
.loader-container.is-loading {
|
||||
position: relative;
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
position: relative;
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
|
||||
&::after {
|
||||
@include loader;
|
||||
position: absolute;
|
||||
top: calc(50% - 2.5rem);
|
||||
left: calc(50% - 2.5rem);
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
border-width: 0.25rem;
|
||||
}
|
||||
&::after {
|
||||
@include loader;
|
||||
position: absolute;
|
||||
top: calc(50% - 2.5rem);
|
||||
left: calc(50% - 2.5rem);
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
border-width: 0.25rem;
|
||||
}
|
||||
|
||||
&.is-loading-small::after {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
top: calc(50% - .75rem);
|
||||
left: calc(50% - .75rem);
|
||||
border-width: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: move to ShowTasks.vue
|
||||
.spinner.is-loading {
|
||||
pointer-events: none;
|
||||
pointer-events: none;
|
||||
|
||||
&::after {
|
||||
@include loader;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
margin-left: calc(50% - 1rem);
|
||||
margin-top: 1rem;
|
||||
border-width: 0.25rem;
|
||||
}
|
||||
&::after {
|
||||
@include loader;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
margin-left: calc(50% - 1rem);
|
||||
margin-top: 1rem;
|
||||
border-width: 0.25rem;
|
||||
}
|
||||
}
|
|
@ -2,18 +2,11 @@
|
|||
<div class="kanban-view">
|
||||
<div class="filter-container" v-if="isSavedFilter">
|
||||
<div class="items">
|
||||
<x-button
|
||||
@click.prevent.stop="toggleFilterPopup"
|
||||
icon="filter"
|
||||
type="secondary"
|
||||
>
|
||||
{{ $t('filters.title') }}
|
||||
</x-button>
|
||||
<filter-popup
|
||||
v-model="params"
|
||||
@update:modelValue="loadBuckets"
|
||||
/>
|
||||
</div>
|
||||
<filter-popup
|
||||
:visible="showFilters"
|
||||
v-model="params"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
:class="{ 'is-loading': loading && !oneTaskUpdating}"
|
||||
|
@ -143,7 +136,7 @@
|
|||
:component-data="taskDraggableTaskComponentData"
|
||||
>
|
||||
<template #item="{element: task}">
|
||||
<kanban-card :task="task" />
|
||||
<kanban-card :task="task"/>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
|
@ -213,7 +206,7 @@
|
|||
<!-- This router view is used to show the task popup while keeping the kanban board itself -->
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="modal">
|
||||
<component :is="Component" />
|
||||
<component :is="Component"/>
|
||||
</transition>
|
||||
</router-view>
|
||||
|
||||
|
@ -224,10 +217,10 @@
|
|||
v-if="showBucketDeleteModal"
|
||||
>
|
||||
<template #header><span>{{ $t('list.kanban.deleteHeaderBucket') }}</span></template>
|
||||
|
||||
|
||||
<template #text>
|
||||
<p>{{ $t('list.kanban.deleteBucketText1') }}<br/>
|
||||
{{ $t('list.kanban.deleteBucketText2') }}</p>
|
||||
{{ $t('list.kanban.deleteBucketText2') }}</p>
|
||||
</template>
|
||||
</modal>
|
||||
</transition>
|
||||
|
@ -300,7 +293,6 @@ export default {
|
|||
filter_comparator: [],
|
||||
filter_concat: 'and',
|
||||
},
|
||||
showFilters: false,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
|
@ -328,10 +320,10 @@ export default {
|
|||
return {
|
||||
type: 'transition',
|
||||
tag: 'div',
|
||||
name: !this.dragBucket ? 'move-bucket': null,
|
||||
name: !this.dragBucket ? 'move-bucket' : null,
|
||||
class: [
|
||||
'kanban-bucket-container',
|
||||
{ 'dragging-disabled': !this.canWrite },
|
||||
{'dragging-disabled': !this.canWrite},
|
||||
],
|
||||
}
|
||||
},
|
||||
|
@ -339,10 +331,10 @@ export default {
|
|||
return {
|
||||
type: 'transition',
|
||||
tag: 'div',
|
||||
name: !this.drag ? 'move-card': null,
|
||||
name: !this.drag ? 'move-card' : null,
|
||||
class: [
|
||||
'dropper',
|
||||
{ 'dragging-disabled': !this.canWrite },
|
||||
{'dragging-disabled': !this.canWrite},
|
||||
],
|
||||
}
|
||||
},
|
||||
|
@ -357,19 +349,15 @@ export default {
|
|||
list: state => state.currentList,
|
||||
}),
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleFilterPopup() {
|
||||
this.showFilters = !this.showFilters
|
||||
},
|
||||
|
||||
methods: {
|
||||
loadBuckets() {
|
||||
// Prevent trying to load buckets if the task popup view is active
|
||||
if (this.$route.name !== 'list.kanban') {
|
||||
return
|
||||
}
|
||||
|
||||
const { listId, params } = this.loadBucketParameter
|
||||
const {listId, params} = this.loadBucketParameter
|
||||
|
||||
this.collapsedBuckets = getCollapsedBucketState(listId)
|
||||
|
||||
|
@ -424,7 +412,7 @@ export default {
|
|||
|
||||
const newTask = cloneDeep(task) // cloning the task to avoid vuex store mutations
|
||||
newTask.bucketId = newBucket.id,
|
||||
newTask.kanbanPosition = calculateItemPosition(taskBefore !== null ? taskBefore.kanbanPosition : null, taskAfter !== null ? taskAfter.kanbanPosition : null)
|
||||
newTask.kanbanPosition = calculateItemPosition(taskBefore !== null ? taskBefore.kanbanPosition : null, taskAfter !== null ? taskAfter.kanbanPosition : null)
|
||||
|
||||
try {
|
||||
await this.$store.dispatch('tasks/update', newTask)
|
||||
|
|
|
@ -41,19 +41,11 @@
|
|||
v-if="!showTaskSearch"
|
||||
/>
|
||||
</div>
|
||||
<x-button
|
||||
@click.prevent.stop="showTaskFilter = !showTaskFilter"
|
||||
type="secondary"
|
||||
icon="filter"
|
||||
>
|
||||
{{ $t('filters.title') }}
|
||||
</x-button>
|
||||
<filter-popup
|
||||
v-model="params"
|
||||
@update:modelValue="loadTasks()"
|
||||
/>
|
||||
</div>
|
||||
<filter-popup
|
||||
:visible="showTaskFilter"
|
||||
v-model="params"
|
||||
@update:modelValue="loadTasks()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<card :padding="false" :has-content="false" class="has-overflow">
|
||||
|
@ -126,7 +118,7 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<Pagination
|
||||
<Pagination
|
||||
:total-pages="taskCollectionService.totalPages"
|
||||
:current-page="currentPage"
|
||||
/>
|
||||
|
@ -135,7 +127,7 @@
|
|||
<!-- This router view is used to show the task popup while keeping the kanban board itself -->
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="modal">
|
||||
<component :is="Component" />
|
||||
<component :is="Component"/>
|
||||
</transition>
|
||||
</router-view>
|
||||
</div>
|
||||
|
@ -155,6 +147,7 @@ import FilterPopup from '@/components/list/partials/filter-popup.vue'
|
|||
import {HAS_TASKS} from '@/store/mutation-types'
|
||||
import Nothing from '@/components/misc/nothing.vue'
|
||||
import Pagination from '@/components/misc/pagination.vue'
|
||||
import Popup from '@/components/misc/popup'
|
||||
|
||||
import draggable from 'vuedraggable'
|
||||
import {calculateItemPosition} from '../../../helpers/calculateItemPosition'
|
||||
|
@ -198,6 +191,7 @@ export default {
|
|||
taskList,
|
||||
],
|
||||
components: {
|
||||
Popup,
|
||||
Nothing,
|
||||
FilterPopup,
|
||||
SingleTaskInList,
|
||||
|
@ -294,11 +288,11 @@ export default {
|
|||
|
||||
async saveTaskPosition(e) {
|
||||
this.drag = false
|
||||
|
||||
|
||||
const task = this.tasks[e.newIndex]
|
||||
const taskBefore = this.tasks[e.newIndex - 1] ?? null
|
||||
const taskAfter = this.tasks[e.newIndex + 1] ?? null
|
||||
|
||||
const taskAfter = this.tasks[e.newIndex + 1] ?? null
|
||||
|
||||
const newTask = {
|
||||
...task,
|
||||
position: calculateItemPosition(taskBefore !== null ? taskBefore.position : null, taskAfter !== null ? taskAfter.position : null),
|
||||
|
|
|
@ -2,67 +2,63 @@
|
|||
<div :class="{'is-loading': taskCollectionService.loading}" class="table-view loader-container">
|
||||
<div class="filter-container">
|
||||
<div class="items">
|
||||
<x-button
|
||||
@click.prevent.stop="() => {showActiveColumnsFilter = !showActiveColumnsFilter; showTaskFilter = false}"
|
||||
icon="th"
|
||||
type="secondary"
|
||||
>
|
||||
{{ $t('list.table.columns') }}
|
||||
</x-button>
|
||||
<x-button
|
||||
@click.prevent.stop="() => {showTaskFilter = !showTaskFilter; showActiveColumnsFilter = false}"
|
||||
icon="filter"
|
||||
type="secondary"
|
||||
>
|
||||
{{ $t('filters.title') }}
|
||||
</x-button>
|
||||
<popup>
|
||||
<template #trigger="{toggle}">
|
||||
<x-button
|
||||
@click.prevent.stop="toggle()"
|
||||
icon="th"
|
||||
type="secondary"
|
||||
>
|
||||
{{ $t('list.table.columns') }}
|
||||
</x-button>
|
||||
</template>
|
||||
<template #content="{isOpen}">
|
||||
<card class="columns-filter" :class="{'is-open': isOpen}">
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.id">#</fancycheckbox>
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.done">
|
||||
{{ $t('task.attributes.done') }}
|
||||
</fancycheckbox>
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.title">
|
||||
{{ $t('task.attributes.title') }}
|
||||
</fancycheckbox>
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.priority">
|
||||
{{ $t('task.attributes.priority') }}
|
||||
</fancycheckbox>
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.labels">
|
||||
{{ $t('task.attributes.labels') }}
|
||||
</fancycheckbox>
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.assignees">
|
||||
{{ $t('task.attributes.assignees') }}
|
||||
</fancycheckbox>
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.dueDate">
|
||||
{{ $t('task.attributes.dueDate') }}
|
||||
</fancycheckbox>
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.startDate">
|
||||
{{ $t('task.attributes.startDate') }}
|
||||
</fancycheckbox>
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.endDate">
|
||||
{{ $t('task.attributes.endDate') }}
|
||||
</fancycheckbox>
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.percentDone">
|
||||
{{ $t('task.attributes.percentDone') }}
|
||||
</fancycheckbox>
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.created">
|
||||
{{ $t('task.attributes.created') }}
|
||||
</fancycheckbox>
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.updated">
|
||||
{{ $t('task.attributes.updated') }}
|
||||
</fancycheckbox>
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.createdBy">
|
||||
{{ $t('task.attributes.createdBy') }}
|
||||
</fancycheckbox>
|
||||
</card>
|
||||
</template>
|
||||
</popup>
|
||||
<filter-popup
|
||||
v-model="params"
|
||||
@update:modelValue="loadTasks()"
|
||||
/>
|
||||
</div>
|
||||
<transition name="fade">
|
||||
<card v-if="showActiveColumnsFilter">
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.id">#</fancycheckbox>
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.done">
|
||||
{{ $t('task.attributes.done') }}
|
||||
</fancycheckbox>
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.title">
|
||||
{{ $t('task.attributes.title') }}
|
||||
</fancycheckbox>
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.priority">
|
||||
{{ $t('task.attributes.priority') }}
|
||||
</fancycheckbox>
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.labels">
|
||||
{{ $t('task.attributes.labels') }}
|
||||
</fancycheckbox>
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.assignees">
|
||||
{{ $t('task.attributes.assignees') }}
|
||||
</fancycheckbox>
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.dueDate">
|
||||
{{ $t('task.attributes.dueDate') }}
|
||||
</fancycheckbox>
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.startDate">
|
||||
{{ $t('task.attributes.startDate') }}
|
||||
</fancycheckbox>
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.endDate">
|
||||
{{ $t('task.attributes.endDate') }}
|
||||
</fancycheckbox>
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.percentDone">
|
||||
{{ $t('task.attributes.percentDone') }}
|
||||
</fancycheckbox>
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.created">
|
||||
{{ $t('task.attributes.created') }}
|
||||
</fancycheckbox>
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.updated">
|
||||
{{ $t('task.attributes.updated') }}
|
||||
</fancycheckbox>
|
||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.createdBy">
|
||||
{{ $t('task.attributes.createdBy') }}
|
||||
</fancycheckbox>
|
||||
</card>
|
||||
</transition>
|
||||
<filter-popup
|
||||
:visible="showTaskFilter"
|
||||
v-model="params"
|
||||
@update:modelValue="loadTasks()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<card :padding="false" :has-content="false">
|
||||
|
@ -189,21 +185,23 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import taskList from '../../../components/tasks/mixins/taskList'
|
||||
import taskList from '@/components/tasks/mixins/taskList'
|
||||
import Done from '@/components/misc/Done.vue'
|
||||
import User from '../../../components/misc/user'
|
||||
import PriorityLabel from '../../../components/tasks/partials/priorityLabel'
|
||||
import Labels from '../../../components/tasks/partials/labels'
|
||||
import DateTableCell from '../../../components/tasks/partials/date-table-cell'
|
||||
import Fancycheckbox from '../../../components/input/fancycheckbox'
|
||||
import Sort from '../../../components/tasks/partials/sort'
|
||||
import User from '@/components/misc/user'
|
||||
import PriorityLabel from '@/components/tasks/partials/priorityLabel'
|
||||
import Labels from '@/components/tasks/partials/labels'
|
||||
import DateTableCell from '@/components/tasks/partials/date-table-cell'
|
||||
import Fancycheckbox from '@/components/input/fancycheckbox'
|
||||
import Sort from '@/components/tasks/partials/sort'
|
||||
import {saveListView} from '@/helpers/saveListView'
|
||||
import FilterPopup from '@/components/list/partials/filter-popup.vue'
|
||||
import Pagination from '@/components/misc/pagination.vue'
|
||||
import Popup from '@/components/misc/popup'
|
||||
|
||||
export default {
|
||||
name: 'Table',
|
||||
components: {
|
||||
Popup,
|
||||
Done,
|
||||
FilterPopup,
|
||||
Sort,
|
||||
|
@ -219,7 +217,6 @@ export default {
|
|||
],
|
||||
data() {
|
||||
return {
|
||||
showActiveColumnsFilter: false,
|
||||
activeColumns: {
|
||||
id: true,
|
||||
done: true,
|
||||
|
@ -323,4 +320,12 @@ export default {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.columns-filter {
|
||||
margin: 0;
|
||||
|
||||
&.is-open {
|
||||
margin: 2rem 0 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
Reference in New Issue