Merge remote-tracking branch 'upstream/main' into bulma-css-variables
Some checks failed
continuous-integration/drone/pr Build is failing
Some checks failed
continuous-integration/drone/pr Build is failing
This commit is contained in:
commit
41ef1eea4a
|
@ -148,6 +148,7 @@ steps:
|
||||||
GITEA_TOKEN:
|
GITEA_TOKEN:
|
||||||
from_secret: gitea_token
|
from_secret: gitea_token
|
||||||
commands:
|
commands:
|
||||||
|
- shasum -a 384 -c ./scripts/deploy-preview-netlify.js.sha384
|
||||||
- node ./scripts/deploy-preview-netlify.js
|
- node ./scripts/deploy-preview-netlify.js
|
||||||
depends_on:
|
depends_on:
|
||||||
- build-prod
|
- build-prod
|
||||||
|
@ -655,6 +656,6 @@ steps:
|
||||||
from_secret: crowdin_key
|
from_secret: crowdin_key
|
||||||
---
|
---
|
||||||
kind: signature
|
kind: signature
|
||||||
hmac: 15df446c7e93a881249d46273485183386157229ee6a37b1ed0fcb2a0b32bbe2
|
hmac: 188ee90100c5fc5922a445e531e7a47453121edddb2a64a182eb23ed2bf602de
|
||||||
|
|
||||||
...
|
...
|
||||||
|
|
|
@ -24,8 +24,8 @@ context('Registration', () => {
|
||||||
cy.visit('/register')
|
cy.visit('/register')
|
||||||
cy.get('#username').type(fixture.username)
|
cy.get('#username').type(fixture.username)
|
||||||
cy.get('#email').type(fixture.email)
|
cy.get('#email').type(fixture.email)
|
||||||
cy.get('#password1').type(fixture.password)
|
cy.get('#password').type(fixture.password)
|
||||||
cy.get('#password2').type(fixture.password)
|
cy.get('#passwordValidation').type(fixture.password)
|
||||||
cy.get('#register-submit').click()
|
cy.get('#register-submit').click()
|
||||||
cy.url().should('include', '/')
|
cy.url().should('include', '/')
|
||||||
cy.clock(1625656161057) // 13:00
|
cy.clock(1625656161057) // 13:00
|
||||||
|
@ -42,8 +42,8 @@ context('Registration', () => {
|
||||||
cy.visit('/register')
|
cy.visit('/register')
|
||||||
cy.get('#username').type(fixture.username)
|
cy.get('#username').type(fixture.username)
|
||||||
cy.get('#email').type(fixture.email)
|
cy.get('#email').type(fixture.email)
|
||||||
cy.get('#password1').type(fixture.password)
|
cy.get('#password').type(fixture.password)
|
||||||
cy.get('#password2').type(fixture.password)
|
cy.get('#passwordValidation').type(fixture.password)
|
||||||
cy.get('#register-submit').click()
|
cy.get('#register-submit').click()
|
||||||
cy.get('div.notification.is-danger').contains('A user with this username already exists.')
|
cy.get('div.notification.is-danger').contains('A user with this username already exists.')
|
||||||
})
|
})
|
||||||
|
|
12
package.json
12
package.json
|
@ -31,6 +31,7 @@
|
||||||
"dompurify": "2.3.3",
|
"dompurify": "2.3.3",
|
||||||
"easymde": "2.15.0",
|
"easymde": "2.15.0",
|
||||||
"flatpickr": "4.6.9",
|
"flatpickr": "4.6.9",
|
||||||
|
"flexsearch": "0.7.21",
|
||||||
"highlight.js": "11.3.1",
|
"highlight.js": "11.3.1",
|
||||||
"is-touch-device": "1.0.1",
|
"is-touch-device": "1.0.1",
|
||||||
"lodash.clonedeep": "4.5.0",
|
"lodash.clonedeep": "4.5.0",
|
||||||
|
@ -39,8 +40,8 @@
|
||||||
"register-service-worker": "1.7.2",
|
"register-service-worker": "1.7.2",
|
||||||
"snake-case": "3.0.4",
|
"snake-case": "3.0.4",
|
||||||
"ufo": "0.7.9",
|
"ufo": "0.7.9",
|
||||||
"vue": "3.2.21",
|
"vue": "3.2.22",
|
||||||
"vue-advanced-cropper": "2.6.3",
|
"vue-advanced-cropper": "2.7.0",
|
||||||
"vue-drag-resize": "2.0.3",
|
"vue-drag-resize": "2.0.3",
|
||||||
"vue-flatpickr-component": "9.0.5",
|
"vue-flatpickr-component": "9.0.5",
|
||||||
"vue-i18n": "9.2.0-beta.18",
|
"vue-i18n": "9.2.0-beta.18",
|
||||||
|
@ -55,6 +56,7 @@
|
||||||
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
||||||
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
||||||
"@fortawesome/vue-fontawesome": "3.0.0-5",
|
"@fortawesome/vue-fontawesome": "3.0.0-5",
|
||||||
|
"@types/flexsearch": "0.7.2",
|
||||||
"@types/jest": "27.0.2",
|
"@types/jest": "27.0.2",
|
||||||
"@typescript-eslint/eslint-plugin": "5.3.1",
|
"@typescript-eslint/eslint-plugin": "5.3.1",
|
||||||
"@typescript-eslint/parser": "5.3.1",
|
"@typescript-eslint/parser": "5.3.1",
|
||||||
|
@ -63,7 +65,7 @@
|
||||||
"@vue/eslint-config-typescript": "9.0.1",
|
"@vue/eslint-config-typescript": "9.0.1",
|
||||||
"autoprefixer": "10.4.0",
|
"autoprefixer": "10.4.0",
|
||||||
"axios": "0.24.0",
|
"axios": "0.24.0",
|
||||||
"browserslist": "4.17.6",
|
"browserslist": "4.18.0",
|
||||||
"cypress": "9.0.0",
|
"cypress": "9.0.0",
|
||||||
"cypress-file-upload": "5.0.8",
|
"cypress-file-upload": "5.0.8",
|
||||||
"esbuild": "0.13.13",
|
"esbuild": "0.13.13",
|
||||||
|
@ -81,9 +83,9 @@
|
||||||
"ts-jest": "27.0.7",
|
"ts-jest": "27.0.7",
|
||||||
"typescript": "4.4.4",
|
"typescript": "4.4.4",
|
||||||
"vite": "2.6.14",
|
"vite": "2.6.14",
|
||||||
"vite-plugin-pwa": "0.11.3",
|
"vite-plugin-pwa": "0.11.5",
|
||||||
"vue-tsc": "0.29.4",
|
|
||||||
"vite-svg-loader": "3.1.0",
|
"vite-svg-loader": "3.1.0",
|
||||||
|
"vue-tsc": "0.29.4",
|
||||||
"wait-on": "6.0.0",
|
"wait-on": "6.0.0",
|
||||||
"workbox-cli": "6.3.0"
|
"workbox-cli": "6.3.0"
|
||||||
},
|
},
|
||||||
|
|
1
scripts/deploy-preview-netlify.js.sha384
Normal file
1
scripts/deploy-preview-netlify.js.sha384
Normal file
|
@ -0,0 +1 @@
|
||||||
|
55ce0faaa2c1919341617ccfaeccbb6029ac12107964ff488985cff13dd952f1a991df3ab0d4b0705deb761e508e6434 ./scripts/deploy-preview-netlify.js
|
|
@ -1,4 +1,4 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="256" height="256">
|
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 256 256" width="256" height="256">
|
||||||
<path d="M2268.2 2512.3a953.7 953.7 0 0 1-50 57c-180.5 189.5-426.2 294-691.6 294A953.7 953.7 0 0 1 847.8 2582a952.7 952.7 0 0 1-281.2-678.8 953.8 953.8 0 0 1 281.2-678.9 953.7 953.7 0 0 1 678.8-281.1 953.7 953.7 0 0 1 678.8 281.1 953.7 953.7 0 0 1 281.2 678.9c0 219.2-78.9 437.2-218.4 609" style="fill:#196aff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.36633128" transform="matrix(.13333 0 0 -.13333 -75.5 381.8)"/>
|
<path d="M2268.2 2512.3a953.7 953.7 0 0 1-50 57c-180.5 189.5-426.2 294-691.6 294A953.7 953.7 0 0 1 847.8 2582a952.7 952.7 0 0 1-281.2-678.8 953.8 953.8 0 0 1 281.2-678.9 953.7 953.7 0 0 1 678.8-281.1 953.7 953.7 0 0 1 678.8 281.1 953.7 953.7 0 0 1 281.2 678.9c0 219.2-78.9 437.2-218.4 609" style="fill:#196aff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.36633128" transform="matrix(.13333 0 0 -.13333 -75.5 381.8)"/>
|
||||||
<path d="M1823.7 1650.9c35.7 104.2 94.7 136.1 102 297 2.6 56.5-14.7 236-14.7 236s28 72-25.8 152.3c-83.5 124.3-255.4 132.8-345.7 132.8-90.3 0-260.2-8.5-343.7-132.8C1142 2256 1170 2184 1170 2184s-9.5-92.4-16.7-173.8c-1.7-19.1.1-94.7 2.4-113a453 453 0 0 1 25.8-96.2c14.4-39.6 36.8-79.9 54-120.5 51.8-122.8 8.4-274.9 11.1-407.3 2.2-94-20-189.3-28.7-281.2a960.4 960.4 0 0 1 308.7-50.6 958.6 958.6 0 0 1 344.9 63.6c-20.4 115-44.1 224.2-47.8 265.9-10.6 125.9-41.3 259.4 0 380" style="fill:#fff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.36655635" transform="matrix(.13333 0 0 -.13333 -75.5 381.8)"/>
|
<path d="M1823.7 1650.9c35.7 104.2 94.7 136.1 102 297 2.6 56.5-14.7 236-14.7 236s28 72-25.8 152.3c-83.5 124.3-255.4 132.8-345.7 132.8-90.3 0-260.2-8.5-343.7-132.8C1142 2256 1170 2184 1170 2184s-9.5-92.4-16.7-173.8c-1.7-19.1.1-94.7 2.4-113a453 453 0 0 1 25.8-96.2c14.4-39.6 36.8-79.9 54-120.5 51.8-122.8 8.4-274.9 11.1-407.3 2.2-94-20-189.3-28.7-281.2a960.4 960.4 0 0 1 308.7-50.6 958.6 958.6 0 0 1 344.9 63.6c-20.4 115-44.1 224.2-47.8 265.9-10.6 125.9-41.3 259.4 0 380" style="fill:#fff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.36655635" transform="matrix(.13333 0 0 -.13333 -75.5 381.8)"/>
|
||||||
<path d="M1162.9 2383.9c1.1-18.8 3-38 8.3-56.2 1.6-5.7 4-19.7 11.4-21.8 9-2.6 25.9 8.3 32.3 13 12.3 9 23.9 18.5 36.2 27.6 8 6 16.5 10.5 24.3 16.5 8.4 6.6 14.7 14.5 21.7 22.2 8.4 9.4 14.8 19 21.3 29.5 5.1 8.2 37.1 13.5 42.2 21 5.6 8.3 1 18.6 1 28.7 0 74.2 4.4 147.6 6.1 220.3 1.8 50 21.4 109.2-53.4 85.8-160.3-50-158.5-271.3-151.4-386.6M1869.1 2279.7c-1.6 1.8-4.2 3.2-6.3 4.8a208 208 0 0 0-25.1 21.5c-9.4 9.6-19.2 19-28.2 28.9-7.9 8.7-17.3 16.6-25 25.6-5.1 6-10 12.3-14.6 18.5-2.3 3.2-3.5 7-5.3 10.4-2.7 5-40 10.1-36.2 15 6.3 8.3 20.3 15.4 23.7 25 17.2 48.6 24.8 244.5 26.8 294.5 5.4 127.8 117.6-6.3 137.2-57.7 57-149.7 23.2-258.8-46.3-386.6" style="fill:#fff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.36633128" transform="matrix(.13333 0 0 -.13333 -75.5 381.8)"/>
|
<path d="M1162.9 2383.9c1.1-18.8 3-38 8.3-56.2 1.6-5.7 4-19.7 11.4-21.8 9-2.6 25.9 8.3 32.3 13 12.3 9 23.9 18.5 36.2 27.6 8 6 16.5 10.5 24.3 16.5 8.4 6.6 14.7 14.5 21.7 22.2 8.4 9.4 14.8 19 21.3 29.5 5.1 8.2 37.1 13.5 42.2 21 5.6 8.3 1 18.6 1 28.7 0 74.2 4.4 147.6 6.1 220.3 1.8 50 21.4 109.2-53.4 85.8-160.3-50-158.5-271.3-151.4-386.6M1869.1 2279.7c-1.6 1.8-4.2 3.2-6.3 4.8a208 208 0 0 0-25.1 21.5c-9.4 9.6-19.2 19-28.2 28.9-7.9 8.7-17.3 16.6-25 25.6-5.1 6-10 12.3-14.6 18.5-2.3 3.2-3.5 7-5.3 10.4-2.7 5-40 10.1-36.2 15 6.3 8.3 20.3 15.4 23.7 25 17.2 48.6 24.8 244.5 26.8 294.5 5.4 127.8 117.6-6.3 137.2-57.7 57-149.7 23.2-258.8-46.3-386.6" style="fill:#fff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.36633128" transform="matrix(.13333 0 0 -.13333 -75.5 381.8)"/>
|
||||||
|
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
|
@ -189,6 +189,8 @@ import ListService from '@/services/list'
|
||||||
import NamespaceService from '@/services/namespace'
|
import NamespaceService from '@/services/namespace'
|
||||||
import EditLabels from '@/components/tasks/partials/editLabels.vue'
|
import EditLabels from '@/components/tasks/partials/editLabels.vue'
|
||||||
|
|
||||||
|
import {objectToSnakeCase} from '@/helpers/case'
|
||||||
|
|
||||||
// FIXME: merge with DEFAULT_PARAMS in taskList.js
|
// FIXME: merge with DEFAULT_PARAMS in taskList.js
|
||||||
const DEFAULT_PARAMS = {
|
const DEFAULT_PARAMS = {
|
||||||
sort_by: [],
|
sort_by: [],
|
||||||
|
@ -261,7 +263,9 @@ export default {
|
||||||
watch: {
|
watch: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
handler(value) {
|
handler(value) {
|
||||||
this.params = value
|
// FIXME: filters should only be converted to snake case in
|
||||||
|
// the last moment
|
||||||
|
this.params = objectToSnakeCase(value)
|
||||||
this.prepareFilters()
|
this.prepareFilters()
|
||||||
},
|
},
|
||||||
immediate: true,
|
immediate: true,
|
||||||
|
|
|
@ -67,7 +67,7 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
apiDomain() {
|
apiDomain() {
|
||||||
return parseURL(this.apiUrl).host
|
return parseURL(this.apiUrl).host || parseURL(window.location.href).host
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<modal @close="close()">
|
<modal @close="close()">
|
||||||
<card class="has-background-white has-no-shadow" :title="$t('keyboardShortcuts.title')">
|
<card class="has-background-white has-no-shadow keyboard-shortcuts" :title="$t('keyboardShortcuts.title')">
|
||||||
<template v-for="(s, i) in shortcuts" :key="i">
|
<template v-for="(s, i) in shortcuts" :key="i">
|
||||||
<h3>{{ $t(s.title) }}</h3>
|
<h3>{{ $t(s.title) }}</h3>
|
||||||
|
|
||||||
|
@ -12,10 +12,11 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<dl>
|
<dl class="shortcut-list">
|
||||||
<template v-for="(sc, si) in s.shortcuts" :key="si">
|
<template v-for="(sc, si) in s.shortcuts" :key="si">
|
||||||
<dt>{{ $t(sc.title) }}</dt>
|
<dt class="shortcut-title">{{ $t(sc.title) }}</dt>
|
||||||
<shortcut
|
<shortcut
|
||||||
|
class="shortcut-keys"
|
||||||
is="dd"
|
is="dd"
|
||||||
:keys="sc.keys"
|
:keys="sc.keys"
|
||||||
:combination="typeof sc.combination !== 'undefined' ? $t(`keyboardShortcuts.${sc.combination}`) : null"/>
|
:combination="typeof sc.combination !== 'undefined' ? $t(`keyboardShortcuts.${sc.combination}`) : null"/>
|
||||||
|
@ -47,8 +48,30 @@ export default {
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style scoped>
|
||||||
dt {
|
.keyboard-shortcuts {
|
||||||
font-weight: bold;
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message:not(:last-child) {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-body {
|
||||||
|
padding: .75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcut-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcut-title {
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcut-keys {
|
||||||
|
justify-content: end;
|
||||||
|
margin-bottom: .5rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
|
@ -89,7 +89,7 @@ export default {
|
||||||
},
|
},
|
||||||
currentPage: {
|
currentPage: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true,
|
default: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
</section>
|
</section>
|
||||||
<transition name="fade">
|
<transition name="fade">
|
||||||
<section class="vikunja-loading" v-if="showLoading">
|
<section class="vikunja-loading" v-if="showLoading">
|
||||||
<img alt="Vikunja" :src="logoUrl" width="100" height="100"/>
|
<Logo class="logo"/>
|
||||||
<p>
|
<p>
|
||||||
<span class="loader-container is-loading-small is-loading"></span>
|
<span class="loader-container is-loading-small is-loading"></span>
|
||||||
{{ $t('ready.loading') }}
|
{{ $t('ready.loading') }}
|
||||||
|
@ -41,7 +41,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import logoUrl from '@/assets/logo.svg'
|
import Logo from '@/assets/logo.svg?component'
|
||||||
import ApiConfig from '@/components/misc/api-config'
|
import ApiConfig from '@/components/misc/api-config'
|
||||||
import NoAuthWrapper from '@/components/misc/no-auth-wrapper'
|
import NoAuthWrapper from '@/components/misc/no-auth-wrapper'
|
||||||
import {mapState} from 'vuex'
|
import {mapState} from 'vuex'
|
||||||
|
@ -50,12 +50,12 @@ import {ERROR_NO_API_URL} from '@/helpers/checkAndSetApiUrl'
|
||||||
export default {
|
export default {
|
||||||
name: 'ready',
|
name: 'ready',
|
||||||
components: {
|
components: {
|
||||||
|
Logo,
|
||||||
NoAuthWrapper,
|
NoAuthWrapper,
|
||||||
ApiConfig,
|
ApiConfig,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
logoUrl,
|
|
||||||
error: '',
|
error: '',
|
||||||
errorNoApiUrl: ERROR_NO_API_URL,
|
errorNoApiUrl: ERROR_NO_API_URL,
|
||||||
}
|
}
|
||||||
|
@ -100,10 +100,12 @@ export default {
|
||||||
right: 0;
|
right: 0;
|
||||||
background: var(--grey-100);
|
background: var(--grey-100);
|
||||||
z-index: 99;
|
z-index: 99;
|
||||||
|
}
|
||||||
|
|
||||||
img {
|
.logo {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loader-container {
|
.loader-container {
|
||||||
|
|
|
@ -25,15 +25,7 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
namespaces() {
|
namespaces() {
|
||||||
if (this.query === '') {
|
return this.$store.getters['namespaces/searchNamespace'](this.query)
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.$store.state.namespaces.namespaces.filter(n => {
|
|
||||||
return !n.isArchived &&
|
|
||||||
n.id > 0 &&
|
|
||||||
n.title.toLowerCase().includes(this.query.toLowerCase())
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
@ -110,40 +110,32 @@ export default {
|
||||||
results() {
|
results() {
|
||||||
let lists = []
|
let lists = []
|
||||||
if (this.searchMode === SEARCH_MODE_ALL || this.searchMode === SEARCH_MODE_LISTS) {
|
if (this.searchMode === SEARCH_MODE_ALL || this.searchMode === SEARCH_MODE_LISTS) {
|
||||||
const ncache = {}
|
|
||||||
|
|
||||||
const history = getHistory()
|
|
||||||
// Puts recently visited lists at the top
|
|
||||||
const allLists = [...new Set([
|
|
||||||
...history.map(l => {
|
|
||||||
return this.$store.getters['lists/getListById'](l.id)
|
|
||||||
}),
|
|
||||||
...Object.values(this.$store.state.lists)])]
|
|
||||||
|
|
||||||
const {list} = this.parsedQuery
|
const {list} = this.parsedQuery
|
||||||
|
|
||||||
if (list === null) {
|
if (list === null) {
|
||||||
lists = []
|
lists = []
|
||||||
} else {
|
} else {
|
||||||
|
const ncache = {}
|
||||||
|
const history = getHistory()
|
||||||
|
// Puts recently visited lists at the top
|
||||||
|
const allLists = [...new Set([
|
||||||
|
...history.map(l => {
|
||||||
|
return this.$store.getters['lists/getListById'](l.id)
|
||||||
|
}),
|
||||||
|
...this.$store.getters['lists/searchList'](list),
|
||||||
|
])]
|
||||||
|
|
||||||
lists = allLists.filter(l => {
|
lists = allLists.filter(l => {
|
||||||
if (typeof l === 'undefined' || l === null) {
|
if (typeof l === 'undefined' || l === null) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (l.isArchived) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof ncache[l.namespaceId] === 'undefined') {
|
if (typeof ncache[l.namespaceId] === 'undefined') {
|
||||||
ncache[l.namespaceId] = this.$store.getters['namespaces/getNamespaceById'](l.namespaceId)
|
ncache[l.namespaceId] = this.$store.getters['namespaces/getNamespaceById'](l.namespaceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ncache[l.namespaceId].isArchived) {
|
return !ncache[l.namespaceId].isArchived
|
||||||
return false
|
})
|
||||||
}
|
|
||||||
|
|
||||||
return l.title.toLowerCase().includes(list.toLowerCase())
|
|
||||||
}) ?? []
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<multiselect
|
<multiselect
|
||||||
class="control is-expanded"
|
class="control is-expanded"
|
||||||
:loading="listSerivce.loading"
|
|
||||||
:placeholder="$t('list.search')"
|
:placeholder="$t('list.search')"
|
||||||
@search="findLists"
|
@search="findLists"
|
||||||
:search-results="foundLists"
|
:search-results="foundLists"
|
||||||
|
@ -18,7 +17,6 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ListService from '../../../services/list'
|
|
||||||
import ListModel from '../../../models/list'
|
import ListModel from '../../../models/list'
|
||||||
import Multiselect from '@/components/input/multiselect.vue'
|
import Multiselect from '@/components/input/multiselect.vue'
|
||||||
|
|
||||||
|
@ -26,7 +24,6 @@ export default {
|
||||||
name: 'listSearch',
|
name: 'listSearch',
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
listSerivce: new ListService(),
|
|
||||||
list: new ListModel(),
|
list: new ListModel(),
|
||||||
foundLists: [],
|
foundLists: [],
|
||||||
}
|
}
|
||||||
|
@ -50,17 +47,8 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async findLists(query) {
|
findLists(query) {
|
||||||
if (query === '') {
|
this.foundLists = this.$store.getters['lists/searchList'](query)
|
||||||
this.clearAll()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.foundLists = await this.listSerivce.getAll({}, {s: query})
|
|
||||||
},
|
|
||||||
|
|
||||||
clearAll() {
|
|
||||||
this.foundLists = []
|
|
||||||
},
|
},
|
||||||
|
|
||||||
select(list) {
|
select(list) {
|
||||||
|
@ -82,6 +70,10 @@ export default {
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.list-namespace-title {
|
.list-namespace-title {
|
||||||
|
<<<<<<< HEAD
|
||||||
color: var(--grey-500);
|
color: var(--grey-500);
|
||||||
|
=======
|
||||||
|
color: $grey-500;
|
||||||
|
>>>>>>> upstream/main
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
12
src/composables/useTitle.ts
Normal file
12
src/composables/useTitle.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { computed, watchEffect } from 'vue'
|
||||||
|
import { setTitle } from '@/helpers/setTitle'
|
||||||
|
|
||||||
|
import { ComputedGetter, ComputedRef } from '@vue/reactivity'
|
||||||
|
|
||||||
|
export function useTitle<T>(titleGetter: ComputedGetter<T>) : ComputedRef<T> {
|
||||||
|
const titleRef = computed(titleGetter)
|
||||||
|
|
||||||
|
watchEffect(() => setTitle(titleRef.value))
|
||||||
|
|
||||||
|
return titleRef
|
||||||
|
}
|
|
@ -1,20 +1,25 @@
|
||||||
import {filterLabelsByQuery} from './labels'
|
import {filterLabelsByQuery} from './labels'
|
||||||
|
import {createNewIndexer} from '../indexes'
|
||||||
|
|
||||||
|
const {add} = createNewIndexer('labels', ['title', 'description'])
|
||||||
|
|
||||||
describe('filter labels', () => {
|
describe('filter labels', () => {
|
||||||
const state = {
|
const state = {
|
||||||
labels: [
|
labels: {
|
||||||
{id: 1, title: 'label1'},
|
1: {id: 1, title: 'label1'},
|
||||||
{id: 2, title: 'label2'},
|
2: {id: 2, title: 'label2'},
|
||||||
{id: 3, title: 'label3'},
|
3: {id: 3, title: 'label3'},
|
||||||
{id: 4, title: 'label4'},
|
4: {id: 4, title: 'label4'},
|
||||||
{id: 5, title: 'label5'},
|
5: {id: 5, title: 'label5'},
|
||||||
{id: 6, title: 'label6'},
|
6: {id: 6, title: 'label6'},
|
||||||
{id: 7, title: 'label7'},
|
7: {id: 7, title: 'label7'},
|
||||||
{id: 8, title: 'label8'},
|
8: {id: 8, title: 'label8'},
|
||||||
{id: 9, title: 'label9'},
|
9: {id: 9, title: 'label9'},
|
||||||
],
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Object.values(state.labels).forEach(add)
|
||||||
|
|
||||||
it('should return an empty array for an empty query', () => {
|
it('should return an empty array for an empty query', () => {
|
||||||
const labels = filterLabelsByQuery(state, [], '')
|
const labels = filterLabelsByQuery(state, [], '')
|
||||||
|
|
||||||
|
@ -31,7 +36,7 @@ describe('filter labels', () => {
|
||||||
id: number,
|
id: number,
|
||||||
title: string,
|
title: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
const labelsToHide: label[] = [{id: 1, title: 'label1'}]
|
const labelsToHide: label[] = [{id: 1, title: 'label1'}]
|
||||||
const labels = filterLabelsByQuery(state, labelsToHide, 'label1')
|
const labels = filterLabelsByQuery(state, labelsToHide, 'label1')
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,16 @@
|
||||||
interface label {
|
import {createNewIndexer} from '../indexes'
|
||||||
|
|
||||||
|
const {search} = createNewIndexer('labels', ['title', 'description'])
|
||||||
|
|
||||||
|
export interface label {
|
||||||
id: number,
|
id: number,
|
||||||
title: string,
|
title: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface labelState {
|
interface labelState {
|
||||||
labels: label[],
|
labels: {
|
||||||
|
[k: number]: label,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -15,17 +21,12 @@ interface labelState {
|
||||||
* @returns {Array}
|
* @returns {Array}
|
||||||
*/
|
*/
|
||||||
export function filterLabelsByQuery(state: labelState, labelsToHide: label[], query: string) {
|
export function filterLabelsByQuery(state: labelState, labelsToHide: label[], query: string) {
|
||||||
if (query === '') {
|
const labelIdsToHide: number[] = labelsToHide.map(({id}) => id)
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const labelQuery = query.toLowerCase()
|
return search(query)
|
||||||
const labelIds = labelsToHide.map(({id}) => id)
|
?.filter(value => !labelIdsToHide.includes(value))
|
||||||
return Object
|
.map(id => state.labels[id])
|
||||||
.values(state.labels)
|
|| []
|
||||||
.filter(({id, title}) => {
|
|
||||||
return !labelIds.includes(id) && title.toLowerCase().includes(labelQuery)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
export const setTitle = title => {
|
export function setTitle(title) {
|
||||||
if (typeof title === 'undefined' || title === '') {
|
document.title = (typeof title === 'undefined' || title === '')
|
||||||
document.title = 'Vikunja'
|
? 'Vikunja'
|
||||||
return
|
: `${title} | Vikunja`
|
||||||
}
|
|
||||||
|
|
||||||
document.title = `${title} | Vikunja`
|
|
||||||
}
|
}
|
|
@ -17,14 +17,14 @@
|
||||||
"text": "Požadovaná stránka neexistuje."
|
"text": "Požadovaná stránka neexistuje."
|
||||||
},
|
},
|
||||||
"ready": {
|
"ready": {
|
||||||
"loading": "Vikunja is loading…",
|
"loading": "Vikunja se načítá…",
|
||||||
"errorOccured": "An error occured:",
|
"errorOccured": "Došlo k chybě:",
|
||||||
"checkApiUrl": "Please check if the api url is correct.",
|
"checkApiUrl": "Zkontrolujte, zda je adresa URL API správná.",
|
||||||
"noApiUrlConfigured": "No API url was configured. Please set one below:"
|
"noApiUrlConfigured": "Nebyla nakonfigurována žádná adresa API. Prosím nastavte jednu níže:"
|
||||||
},
|
},
|
||||||
"offline": {
|
"offline": {
|
||||||
"title": "You are offline.",
|
"title": "Jste offline.",
|
||||||
"text": "Please check your network connection and try again."
|
"text": "Zkontrolujte své internetové připojení a zkuste to znovu."
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"auth": {
|
"auth": {
|
||||||
|
@ -354,7 +354,7 @@
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"title": "Filtry",
|
"title": "Filtry",
|
||||||
"clear": "Clear Filters",
|
"clear": "Vymazat filtry",
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"title": "Název",
|
"title": "Název",
|
||||||
"titlePlaceholder": "Název uloženého filtru přijde sem…",
|
"titlePlaceholder": "Název uloženého filtru přijde sem…",
|
||||||
|
@ -461,8 +461,8 @@
|
||||||
"default": "Výchozí",
|
"default": "Výchozí",
|
||||||
"close": "Zavřít",
|
"close": "Zavřít",
|
||||||
"download": "Stáhnout",
|
"download": "Stáhnout",
|
||||||
"showMenu": "Show the menu",
|
"showMenu": "Zobrazit nabídku",
|
||||||
"hideMenu": "Hide the menu"
|
"hideMenu": "Skrýt nabídku"
|
||||||
},
|
},
|
||||||
"input": {
|
"input": {
|
||||||
"resetColor": "Obnovit barvu",
|
"resetColor": "Obnovit barvu",
|
||||||
|
@ -656,7 +656,7 @@
|
||||||
"searchPlaceholder": "Hledejte nový úkol, který chcete přidat jako související…",
|
"searchPlaceholder": "Hledejte nový úkol, který chcete přidat jako související…",
|
||||||
"createPlaceholder": "Přidat toto jako nový související úkol",
|
"createPlaceholder": "Přidat toto jako nový související úkol",
|
||||||
"differentList": "Tento úkol patří do jiného seznamu.",
|
"differentList": "Tento úkol patří do jiného seznamu.",
|
||||||
"differentNamespace": "This task belongs to a different namespace.",
|
"differentNamespace": "Tento úkol patří do jiného prostoru.",
|
||||||
"noneYet": "Zatím žádné vztahy mezi úkoly.",
|
"noneYet": "Zatím žádné vztahy mezi úkoly.",
|
||||||
"delete": "Odstranit vztah k úloze",
|
"delete": "Odstranit vztah k úloze",
|
||||||
"deleteText1": "Jste si jisti, že chcete odstranit tento vztah úkolu?",
|
"deleteText1": "Jste si jisti, že chcete odstranit tento vztah úkolu?",
|
||||||
|
@ -757,12 +757,12 @@
|
||||||
},
|
},
|
||||||
"keyboardShortcuts": {
|
"keyboardShortcuts": {
|
||||||
"title": "Klávesové zkratky",
|
"title": "Klávesové zkratky",
|
||||||
"general": "General",
|
"general": "Obecné",
|
||||||
"allPages": "Tyto zkratky fungují na všech stránkách.",
|
"allPages": "Tyto zkratky fungují na všech stránkách.",
|
||||||
"currentPageOnly": "Tyto zkratky fungují pouze na aktuální stránce.",
|
"currentPageOnly": "Tyto zkratky fungují pouze na aktuální stránce.",
|
||||||
"toggleMenu": "Přepnout nabídku",
|
"toggleMenu": "Přepnout nabídku",
|
||||||
"quickSearch": "Otevřít vyhledávání / panel rychlých akcí",
|
"quickSearch": "Otevřít vyhledávání / panel rychlých akcí",
|
||||||
"then": "then",
|
"then": "potom",
|
||||||
"task": {
|
"task": {
|
||||||
"title": "Stránka úkolů",
|
"title": "Stránka úkolů",
|
||||||
"done": "Označit úkol jako hotový",
|
"done": "Označit úkol jako hotový",
|
||||||
|
@ -773,11 +773,11 @@
|
||||||
"related": "Upravit související úkoly tohoto úkolu"
|
"related": "Upravit související úkoly tohoto úkolu"
|
||||||
},
|
},
|
||||||
"list": {
|
"list": {
|
||||||
"title": "List Views",
|
"title": "Zobrazení seznamů",
|
||||||
"switchToListView": "Switch to list view",
|
"switchToListView": "Přepnout na zobrazení seznamu",
|
||||||
"switchToGanttView": "Switch to gantt view",
|
"switchToGanttView": "Přepnout na zobrazení gantt",
|
||||||
"switchToKanbanView": "Switch to kanban view",
|
"switchToKanbanView": "Přepnout na zobrazení kanbanu",
|
||||||
"switchToTableView": "Switch to table view"
|
"switchToTableView": "Přepnout na zobrazení tabulky"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"update": {
|
"update": {
|
||||||
|
@ -799,9 +799,9 @@
|
||||||
"urlPlaceholder": "např. https://localhost:3456",
|
"urlPlaceholder": "např. https://localhost:3456",
|
||||||
"change": "změnit",
|
"change": "změnit",
|
||||||
"signInOn": "Přihlaste se ke svému účtu Vikunja na {0}",
|
"signInOn": "Přihlaste se ke svému účtu Vikunja na {0}",
|
||||||
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
|
"error": "Nelze najít nebo použít instalaci Vikunja na \"{domain}\". Zkuste prosím jinou url.",
|
||||||
"success": "Pomocí instalace Vikunja na \"{domain}\".",
|
"success": "Pomocí instalace Vikunja na \"{domain}\".",
|
||||||
"urlRequired": "A url is required."
|
"urlRequired": "Je vyžadována adresa URL."
|
||||||
},
|
},
|
||||||
"loadingError": {
|
"loadingError": {
|
||||||
"failed": "Načítání selhalo, prosím {0}. Pokud chyba přetrvává, {1}.",
|
"failed": "Načítání selhalo, prosím {0}. Pokud chyba přetrvává, {1}.",
|
||||||
|
@ -816,7 +816,7 @@
|
||||||
"quickActions": {
|
"quickActions": {
|
||||||
"commands": "Příkazy",
|
"commands": "Příkazy",
|
||||||
"placeholder": "Napište příkaz nebo vyhledávání…",
|
"placeholder": "Napište příkaz nebo vyhledávání…",
|
||||||
"hint": "You can use {list} to limit the search to a list. Combine {list} or {label} (labels) with a search query to search for a task with these labels or on that list. Use {assignee} to only search for teams.",
|
"hint": "Můžete použít {list} k omezení hledání na seznam. Kombinujte {list} nebo {label} (štítky) s vyhledávacím dotazem pro hledání úkolu s těmito štítky nebo na tomto seznamu. Použijte {assignee} pouze pro hledání týmů.",
|
||||||
"tasks": "Úkoly",
|
"tasks": "Úkoly",
|
||||||
"lists": "Seznamy",
|
"lists": "Seznamy",
|
||||||
"teams": "Týmy",
|
"teams": "Týmy",
|
||||||
|
|
|
@ -17,14 +17,14 @@
|
||||||
"text": "Trang bạn yêu cầu không tồn tại."
|
"text": "Trang bạn yêu cầu không tồn tại."
|
||||||
},
|
},
|
||||||
"ready": {
|
"ready": {
|
||||||
"loading": "Vikunja is loading…",
|
"loading": "Vikunja đang tải…",
|
||||||
"errorOccured": "An error occured:",
|
"errorOccured": "Đã xảy ra lỗi:",
|
||||||
"checkApiUrl": "Please check if the api url is correct.",
|
"checkApiUrl": "Vui lòng kiểm tra lại url api.",
|
||||||
"noApiUrlConfigured": "No API url was configured. Please set one below:"
|
"noApiUrlConfigured": "Không có url API nào được cấu hình. Hãy đặt một cái:"
|
||||||
},
|
},
|
||||||
"offline": {
|
"offline": {
|
||||||
"title": "You are offline.",
|
"title": "Bạn đang offline.",
|
||||||
"text": "Please check your network connection and try again."
|
"text": "Vui lòng kiểm tra kết nối mạng của bạn và thử lại."
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"auth": {
|
"auth": {
|
||||||
|
@ -354,7 +354,7 @@
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"title": "Bộ lọc",
|
"title": "Bộ lọc",
|
||||||
"clear": "Clear Filters",
|
"clear": "Xoá các bộ lọc",
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"title": "Tiêu đề",
|
"title": "Tiêu đề",
|
||||||
"titlePlaceholder": "Tiêu đề bộ lọc đã lưu ở đây…",
|
"titlePlaceholder": "Tiêu đề bộ lọc đã lưu ở đây…",
|
||||||
|
@ -461,8 +461,8 @@
|
||||||
"default": "Mặc định",
|
"default": "Mặc định",
|
||||||
"close": "Đóng",
|
"close": "Đóng",
|
||||||
"download": "Tải về",
|
"download": "Tải về",
|
||||||
"showMenu": "Show the menu",
|
"showMenu": "Hiển thị menu",
|
||||||
"hideMenu": "Hide the menu"
|
"hideMenu": "Ẩn menu"
|
||||||
},
|
},
|
||||||
"input": {
|
"input": {
|
||||||
"resetColor": "Đặt lại màu",
|
"resetColor": "Đặt lại màu",
|
||||||
|
@ -656,7 +656,7 @@
|
||||||
"searchPlaceholder": "Gõ tìm kiếm một công việc để thêm dưới dạng liên quan…",
|
"searchPlaceholder": "Gõ tìm kiếm một công việc để thêm dưới dạng liên quan…",
|
||||||
"createPlaceholder": "Thêm điều này làm công việc liên quan mới",
|
"createPlaceholder": "Thêm điều này làm công việc liên quan mới",
|
||||||
"differentList": "Công việc này thuộc về một danh sách khác.",
|
"differentList": "Công việc này thuộc về một danh sách khác.",
|
||||||
"differentNamespace": "This task belongs to a different namespace.",
|
"differentNamespace": "Công việc này thuộc về một Góc làm việc khác.",
|
||||||
"noneYet": "Không có công việc liên quan nào.",
|
"noneYet": "Không có công việc liên quan nào.",
|
||||||
"delete": "Xóa công việc liên quan",
|
"delete": "Xóa công việc liên quan",
|
||||||
"deleteText1": "Bạn có chắc muốn xóa công việc liên quan này không?",
|
"deleteText1": "Bạn có chắc muốn xóa công việc liên quan này không?",
|
||||||
|
@ -757,12 +757,12 @@
|
||||||
},
|
},
|
||||||
"keyboardShortcuts": {
|
"keyboardShortcuts": {
|
||||||
"title": "Phím tắt",
|
"title": "Phím tắt",
|
||||||
"general": "General",
|
"general": "Tổng quan",
|
||||||
"allPages": "Các phím tắt này hoạt động ở mọi nơi.",
|
"allPages": "Các phím tắt này hoạt động ở mọi nơi.",
|
||||||
"currentPageOnly": "Các phím tắt này chỉ hoạt động ở trang hiện tại.",
|
"currentPageOnly": "Các phím tắt này chỉ hoạt động ở trang hiện tại.",
|
||||||
"toggleMenu": "Bật/tắt Menu",
|
"toggleMenu": "Bật/tắt Menu",
|
||||||
"quickSearch": "Mở thanh tìm kiếm/thao tác nhanh",
|
"quickSearch": "Mở thanh tìm kiếm/thao tác nhanh",
|
||||||
"then": "then",
|
"then": "sau đó",
|
||||||
"task": {
|
"task": {
|
||||||
"title": "Trang công việc",
|
"title": "Trang công việc",
|
||||||
"done": "Đánh dấu hoàn thành",
|
"done": "Đánh dấu hoàn thành",
|
||||||
|
@ -773,11 +773,11 @@
|
||||||
"related": "Sửa đổi các công việc liên kết"
|
"related": "Sửa đổi các công việc liên kết"
|
||||||
},
|
},
|
||||||
"list": {
|
"list": {
|
||||||
"title": "List Views",
|
"title": "Xem danh sách",
|
||||||
"switchToListView": "Switch to list view",
|
"switchToListView": "Chuyển sang chế độ danh sách",
|
||||||
"switchToGanttView": "Switch to gantt view",
|
"switchToGanttView": "Chuyển sang biểu đồ Gantt",
|
||||||
"switchToKanbanView": "Switch to kanban view",
|
"switchToKanbanView": "Chuyển sang Kanban",
|
||||||
"switchToTableView": "Switch to table view"
|
"switchToTableView": "Chuyển qua xem Bảng"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"update": {
|
"update": {
|
||||||
|
@ -799,9 +799,9 @@
|
||||||
"urlPlaceholder": "ví dụ: https://localhost:3456",
|
"urlPlaceholder": "ví dụ: https://localhost:3456",
|
||||||
"change": "thay đổi",
|
"change": "thay đổi",
|
||||||
"signInOn": "Đăng nhập vào tài khoản Vikunia tại {0}",
|
"signInOn": "Đăng nhập vào tài khoản Vikunia tại {0}",
|
||||||
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
|
"error": "Không thể tìm thấy hoặc sử dụng cài đặt Vikunja tại \"{domain}\". Vui lòng thử một url khác.",
|
||||||
"success": "Sử dụng cài đặt Vikunja tại \"{domain}\".",
|
"success": "Sử dụng cài đặt Vikunja tại \"{domain}\".",
|
||||||
"urlRequired": "A url is required."
|
"urlRequired": "Cần có một url."
|
||||||
},
|
},
|
||||||
"loadingError": {
|
"loadingError": {
|
||||||
"failed": "Tải thất bại, vui lòng {0}. Nếu lỗi vẫn xảy ra, vui lòng {1}.",
|
"failed": "Tải thất bại, vui lòng {0}. Nếu lỗi vẫn xảy ra, vui lòng {1}.",
|
||||||
|
@ -816,7 +816,7 @@
|
||||||
"quickActions": {
|
"quickActions": {
|
||||||
"commands": "Các lệnh",
|
"commands": "Các lệnh",
|
||||||
"placeholder": "Gõ một lệnh hoặc tìm kiếm…",
|
"placeholder": "Gõ một lệnh hoặc tìm kiếm…",
|
||||||
"hint": "You can use {list} to limit the search to a list. Combine {list} or {label} (labels) with a search query to search for a task with these labels or on that list. Use {assignee} to only search for teams.",
|
"hint": "Bạn có thể dùng {list} để giới hạn tìm kiếm trong một danh sách. Kết hợp {list} hoặc {label} (các nhãn) với một truy vấn để tìm kiếm một công việc được gắn các nhãn này hoặc trên danh sách đó. Sử dụng {assignee} để chỉ tìm kiếm các Team.",
|
||||||
"tasks": "Tác vụ",
|
"tasks": "Tác vụ",
|
||||||
"lists": "Danh sách",
|
"lists": "Danh sách",
|
||||||
"teams": "Team",
|
"teams": "Team",
|
||||||
|
|
52
src/indexes/index.ts
Normal file
52
src/indexes/index.ts
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import {Document, SimpleDocumentSearchResultSetUnit} from 'flexsearch'
|
||||||
|
|
||||||
|
export interface withId {
|
||||||
|
id: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
const indexes: { [k: string]: Document<withId> } = {}
|
||||||
|
|
||||||
|
export const createNewIndexer = (name: string, fieldsToIndex: string[]) => {
|
||||||
|
if (typeof indexes[name] === 'undefined') {
|
||||||
|
indexes[name] = new Document<withId>({
|
||||||
|
tokenize: 'full',
|
||||||
|
document: {
|
||||||
|
id: 'id',
|
||||||
|
index: fieldsToIndex,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = indexes[name]
|
||||||
|
|
||||||
|
function add(item: withId) {
|
||||||
|
return index.add(item.id, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove(item: withId) {
|
||||||
|
return index.remove(item.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(item: withId) {
|
||||||
|
return index.update(item.id, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
function search(query: string | null): number[] | null {
|
||||||
|
if (query === '' || query === null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
return index.search(query)
|
||||||
|
?.flatMap(r => r.result)
|
||||||
|
.filter((value, index, self) => self.indexOf(value) === index)
|
||||||
|
|| null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
add,
|
||||||
|
remove,
|
||||||
|
update,
|
||||||
|
search,
|
||||||
|
}
|
||||||
|
}
|
|
@ -35,15 +35,17 @@ import ListSettingDuplicate from '../views/list/settings/duplicate'
|
||||||
import ListSettingShare from '../views/list/settings/share'
|
import ListSettingShare from '../views/list/settings/share'
|
||||||
import ListSettingDelete from '../views/list/settings/delete'
|
import ListSettingDelete from '../views/list/settings/delete'
|
||||||
import ListSettingArchive from '../views/list/settings/archive'
|
import ListSettingArchive from '../views/list/settings/archive'
|
||||||
import FilterSettingEdit from '../views/filters/settings/edit'
|
|
||||||
import FilterSettingDelete from '../views/filters/settings/delete'
|
|
||||||
// Namespace Settings
|
// Namespace Settings
|
||||||
import NamespaceSettingEdit from '../views/namespaces/settings/edit'
|
import NamespaceSettingEdit from '../views/namespaces/settings/edit'
|
||||||
import NamespaceSettingShare from '../views/namespaces/settings/share'
|
import NamespaceSettingShare from '../views/namespaces/settings/share'
|
||||||
import NamespaceSettingArchive from '../views/namespaces/settings/archive'
|
import NamespaceSettingArchive from '../views/namespaces/settings/archive'
|
||||||
import NamespaceSettingDelete from '../views/namespaces/settings/delete'
|
import NamespaceSettingDelete from '../views/namespaces/settings/delete'
|
||||||
|
|
||||||
// Saved Filters
|
// Saved Filters
|
||||||
import CreateSavedFilter from '../views/filters/CreateSavedFilter'
|
import FilterNew from '@/views/filters/FilterNew'
|
||||||
|
import FilterEdit from '@/views/filters/FilterEdit'
|
||||||
|
import FilterDelete from '@/views/filters/FilterDelete'
|
||||||
|
|
||||||
const PasswordResetComponent = () => import('../views/user/PasswordReset')
|
const PasswordResetComponent = () => import('../views/user/PasswordReset')
|
||||||
const GetPasswordResetComponent = () => import('../views/user/RequestPasswordReset')
|
const GetPasswordResetComponent = () => import('../views/user/RequestPasswordReset')
|
||||||
|
@ -123,6 +125,7 @@ const router = createRouter({
|
||||||
path: '/user/settings',
|
path: '/user/settings',
|
||||||
name: 'user.settings',
|
name: 'user.settings',
|
||||||
component: UserSettingsComponent,
|
component: UserSettingsComponent,
|
||||||
|
redirect: {name: 'user.settings.general'},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '/user/settings/avatar',
|
path: '/user/settings/avatar',
|
||||||
|
@ -279,14 +282,14 @@ const router = createRouter({
|
||||||
path: '/lists/:listId/settings/edit',
|
path: '/lists/:listId/settings/edit',
|
||||||
name: 'filter.settings.edit',
|
name: 'filter.settings.edit',
|
||||||
components: {
|
components: {
|
||||||
popup: FilterSettingEdit,
|
popup: FilterEdit,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/lists/:listId/settings/delete',
|
path: '/lists/:listId/settings/delete',
|
||||||
name: 'filter.settings.delete',
|
name: 'filter.settings.delete',
|
||||||
components: {
|
components: {
|
||||||
popup: FilterSettingDelete,
|
popup: FilterDelete,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -337,12 +340,12 @@ const router = createRouter({
|
||||||
{
|
{
|
||||||
path: '/lists/:listId/settings/edit',
|
path: '/lists/:listId/settings/edit',
|
||||||
name: 'filter.list.settings.edit',
|
name: 'filter.list.settings.edit',
|
||||||
component: FilterSettingEdit,
|
component: FilterEdit,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/lists/:listId/settings/delete',
|
path: '/lists/:listId/settings/delete',
|
||||||
name: 'filter.list.settings.delete',
|
name: 'filter.list.settings.delete',
|
||||||
component: FilterSettingDelete,
|
component: FilterDelete,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -389,12 +392,12 @@ const router = createRouter({
|
||||||
{
|
{
|
||||||
path: '/lists/:listId/settings/edit',
|
path: '/lists/:listId/settings/edit',
|
||||||
name: 'filter.gantt.settings.edit',
|
name: 'filter.gantt.settings.edit',
|
||||||
component: FilterSettingEdit,
|
component: FilterEdit,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/lists/:listId/settings/delete',
|
path: '/lists/:listId/settings/delete',
|
||||||
name: 'filter.gantt.settings.delete',
|
name: 'filter.gantt.settings.delete',
|
||||||
component: FilterSettingDelete,
|
component: FilterDelete,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -436,12 +439,12 @@ const router = createRouter({
|
||||||
{
|
{
|
||||||
path: '/lists/:listId/settings/edit',
|
path: '/lists/:listId/settings/edit',
|
||||||
name: 'filter.table.settings.edit',
|
name: 'filter.table.settings.edit',
|
||||||
component: FilterSettingEdit,
|
component: FilterEdit,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/lists/:listId/settings/delete',
|
path: '/lists/:listId/settings/delete',
|
||||||
name: 'filter.table.settings.delete',
|
name: 'filter.table.settings.delete',
|
||||||
component: FilterSettingDelete,
|
component: FilterDelete,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -488,12 +491,12 @@ const router = createRouter({
|
||||||
{
|
{
|
||||||
path: '/lists/:listId/settings/edit',
|
path: '/lists/:listId/settings/edit',
|
||||||
name: 'filter.kanban.settings.edit',
|
name: 'filter.kanban.settings.edit',
|
||||||
component: FilterSettingEdit,
|
component: FilterEdit,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/lists/:listId/settings/delete',
|
path: '/lists/:listId/settings/delete',
|
||||||
name: 'filter.kanban.settings.delete',
|
name: 'filter.kanban.settings.delete',
|
||||||
component: FilterSettingDelete,
|
component: FilterDelete,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -542,7 +545,7 @@ const router = createRouter({
|
||||||
path: '/filters/new',
|
path: '/filters/new',
|
||||||
name: 'filters.create',
|
name: 'filters.create',
|
||||||
components: {
|
components: {
|
||||||
popup: CreateSavedFilter,
|
popup: FilterNew,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -3,6 +3,9 @@ import {setLoading} from '@/store/helper'
|
||||||
import {success} from '@/message'
|
import {success} from '@/message'
|
||||||
import {i18n} from '@/i18n'
|
import {i18n} from '@/i18n'
|
||||||
import {getLabelsByIds, filterLabelsByQuery} from '@/helpers/labels'
|
import {getLabelsByIds, filterLabelsByQuery} from '@/helpers/labels'
|
||||||
|
import {createNewIndexer} from '@/indexes'
|
||||||
|
|
||||||
|
const {add, remove, update} = createNewIndexer('labels', ['title', 'description'])
|
||||||
|
|
||||||
async function getAllLabels(page = 1) {
|
async function getAllLabels(page = 1) {
|
||||||
const labelService = new LabelService()
|
const labelService = new LabelService()
|
||||||
|
@ -26,13 +29,16 @@ export default {
|
||||||
setLabels(state, labels) {
|
setLabels(state, labels) {
|
||||||
labels.forEach(l => {
|
labels.forEach(l => {
|
||||||
state.labels[l.id] = l
|
state.labels[l.id] = l
|
||||||
|
add(l)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
setLabel(state, label) {
|
setLabel(state, label) {
|
||||||
state.labels[label.id] = label
|
state.labels[label.id] = label
|
||||||
|
update(label)
|
||||||
},
|
},
|
||||||
removeLabelById(state, label) {
|
removeLabelById(state, label) {
|
||||||
delete state.labels[label.id]
|
delete state.labels[label.id]
|
||||||
|
remove(label)
|
||||||
},
|
},
|
||||||
setLoaded(state, loaded) {
|
setLoaded(state, loaded) {
|
||||||
state.loaded = loaded
|
state.loaded = loaded
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
import ListService from '@/services/list'
|
import ListService from '@/services/list'
|
||||||
import {setLoading} from '@/store/helper'
|
import {setLoading} from '@/store/helper'
|
||||||
import {removeListFromHistory} from '@/modules/listHistory.ts'
|
import {removeListFromHistory} from '@/modules/listHistory.ts'
|
||||||
|
import {createNewIndexer} from '@/indexes'
|
||||||
|
|
||||||
|
const {add, remove, search, update} = createNewIndexer('lists', ['title', 'description'])
|
||||||
|
|
||||||
const FavoriteListsNamespace = -2
|
const FavoriteListsNamespace = -2
|
||||||
|
|
||||||
|
@ -11,14 +14,17 @@ export default {
|
||||||
mutations: {
|
mutations: {
|
||||||
setList(state, list) {
|
setList(state, list) {
|
||||||
state[list.id] = list
|
state[list.id] = list
|
||||||
|
update(list)
|
||||||
},
|
},
|
||||||
setLists(state, lists) {
|
setLists(state, lists) {
|
||||||
lists.forEach(l => {
|
lists.forEach(l => {
|
||||||
state[l.id] = l
|
state[l.id] = l
|
||||||
|
add(l)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
removeListById(state, list) {
|
removeListById(state, list) {
|
||||||
delete state[list.id]
|
delete state[list.id]
|
||||||
|
remove(list)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
|
@ -34,6 +40,13 @@ export default {
|
||||||
})
|
})
|
||||||
return typeof list === 'undefined' ? null : list
|
return typeof list === 'undefined' ? null : list
|
||||||
},
|
},
|
||||||
|
searchList: state => (query, includeArchived = false) => {
|
||||||
|
return search(query)
|
||||||
|
?.filter(value => value > 0)
|
||||||
|
.map(id => state[id])
|
||||||
|
.filter(list => list.isArchived === includeArchived)
|
||||||
|
|| []
|
||||||
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
toggleListFavorite(ctx, list) {
|
toggleListFavorite(ctx, list) {
|
||||||
|
@ -66,7 +79,7 @@ export default {
|
||||||
await listService.update(list)
|
await listService.update(list)
|
||||||
ctx.commit('setList', list)
|
ctx.commit('setList', list)
|
||||||
ctx.commit('namespaces/setListInNamespaceById', list, {root: true})
|
ctx.commit('namespaces/setListInNamespaceById', list, {root: true})
|
||||||
|
|
||||||
// the returned list from listService.update is the same!
|
// the returned list from listService.update is the same!
|
||||||
// in order to not validate vuex mutations we have to create a new copy
|
// in order to not validate vuex mutations we have to create a new copy
|
||||||
const newList = {
|
const newList = {
|
||||||
|
@ -81,7 +94,7 @@ export default {
|
||||||
ctx.dispatch('namespaces/loadNamespacesIfFavoritesDontExist', null, {root: true})
|
ctx.dispatch('namespaces/loadNamespacesIfFavoritesDontExist', null, {root: true})
|
||||||
ctx.dispatch('namespaces/removeFavoritesNamespaceIfEmpty', null, {root: true})
|
ctx.dispatch('namespaces/removeFavoritesNamespaceIfEmpty', null, {root: true})
|
||||||
return newList
|
return newList
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
// Reset the list state to the initial one to avoid confusion for the user
|
// Reset the list state to the initial one to avoid confusion for the user
|
||||||
ctx.commit('setList', {
|
ctx.commit('setList', {
|
||||||
...list,
|
...list,
|
||||||
|
@ -97,13 +110,13 @@ export default {
|
||||||
const cancel = setLoading(ctx, 'lists')
|
const cancel = setLoading(ctx, 'lists')
|
||||||
const listService = new ListService()
|
const listService = new ListService()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await listService.delete(list)
|
const response = await listService.delete(list)
|
||||||
ctx.commit('removeListById', list)
|
ctx.commit('removeListById', list)
|
||||||
ctx.commit('namespaces/removeListFromNamespaceById', list, {root: true})
|
ctx.commit('namespaces/removeListFromNamespaceById', list, {root: true})
|
||||||
removeListFromHistory({id: list.id})
|
removeListFromHistory({id: list.id})
|
||||||
return response
|
return response
|
||||||
} finally{
|
} finally {
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
import NamespaceService from '../../services/namespace'
|
import NamespaceService from '../../services/namespace'
|
||||||
import {setLoading} from '@/store/helper'
|
import {setLoading} from '@/store/helper'
|
||||||
|
import {createNewIndexer} from '@/indexes'
|
||||||
|
|
||||||
|
const {add, remove, search, update} = createNewIndexer('namespaces', ['title', 'description'])
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
|
@ -9,6 +12,9 @@ export default {
|
||||||
mutations: {
|
mutations: {
|
||||||
namespaces(state, namespaces) {
|
namespaces(state, namespaces) {
|
||||||
state.namespaces = namespaces
|
state.namespaces = namespaces
|
||||||
|
namespaces.forEach(n => {
|
||||||
|
add(n)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
setNamespaceById(state, namespace) {
|
setNamespaceById(state, namespace) {
|
||||||
const namespaceIndex = state.namespaces.findIndex(n => n.id === namespace.id)
|
const namespaceIndex = state.namespaces.findIndex(n => n.id === namespace.id)
|
||||||
|
@ -22,8 +28,9 @@ export default {
|
||||||
if (!namespace.lists || namespace.lists.length === 0) {
|
if (!namespace.lists || namespace.lists.length === 0) {
|
||||||
namespace.lists = state.namespaces[namespaceIndex].lists
|
namespace.lists = state.namespaces[namespaceIndex].lists
|
||||||
}
|
}
|
||||||
|
|
||||||
state.namespaces[namespaceIndex] = namespace
|
state.namespaces[namespaceIndex] = namespace
|
||||||
|
update(namespace)
|
||||||
},
|
},
|
||||||
setListInNamespaceById(state, list) {
|
setListInNamespaceById(state, list) {
|
||||||
for (const n in state.namespaces) {
|
for (const n in state.namespaces) {
|
||||||
|
@ -43,11 +50,13 @@ export default {
|
||||||
},
|
},
|
||||||
addNamespace(state, namespace) {
|
addNamespace(state, namespace) {
|
||||||
state.namespaces.push(namespace)
|
state.namespaces.push(namespace)
|
||||||
|
add(namespace)
|
||||||
},
|
},
|
||||||
removeNamespaceById(state, namespaceId) {
|
removeNamespaceById(state, namespaceId) {
|
||||||
for (const n in state.namespaces) {
|
for (const n in state.namespaces) {
|
||||||
if (state.namespaces[n].id === namespaceId) {
|
if (state.namespaces[n].id === namespaceId) {
|
||||||
state.namespaces.splice(n, 1)
|
state.namespaces.splice(n, 1)
|
||||||
|
remove(state.namespaces[n])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -78,11 +87,11 @@ export default {
|
||||||
getters: {
|
getters: {
|
||||||
getListAndNamespaceById: state => (listId, ignorePseudoNamespaces = false) => {
|
getListAndNamespaceById: state => (listId, ignorePseudoNamespaces = false) => {
|
||||||
for (const n in state.namespaces) {
|
for (const n in state.namespaces) {
|
||||||
|
|
||||||
if(ignorePseudoNamespaces && state.namespaces[n].id < 0) {
|
if (ignorePseudoNamespaces && state.namespaces[n].id < 0) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const l in state.namespaces[n].lists) {
|
for (const l in state.namespaces[n].lists) {
|
||||||
if (state.namespaces[n].lists[l].id === listId) {
|
if (state.namespaces[n].lists[l].id === listId) {
|
||||||
return {
|
return {
|
||||||
|
@ -97,6 +106,13 @@ export default {
|
||||||
getNamespaceById: state => namespaceId => {
|
getNamespaceById: state => namespaceId => {
|
||||||
return state.namespaces.find(({id}) => id == namespaceId) || null
|
return state.namespaces.find(({id}) => id == namespaceId) || null
|
||||||
},
|
},
|
||||||
|
searchNamespace: (state, getters) => query => {
|
||||||
|
return search(query)
|
||||||
|
?.filter(value => value > 0)
|
||||||
|
.map(getters.getNamespaceById)
|
||||||
|
.filter(n => n !== null)
|
||||||
|
|| []
|
||||||
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
async loadNamespaces(ctx) {
|
async loadNamespaces(ctx) {
|
||||||
|
@ -107,12 +123,12 @@ export default {
|
||||||
// We always load all namespaces and filter them on the frontend
|
// We always load all namespaces and filter them on the frontend
|
||||||
const namespaces = await namespaceService.getAll({}, {is_archived: true})
|
const namespaces = await namespaceService.getAll({}, {is_archived: true})
|
||||||
ctx.commit('namespaces', namespaces)
|
ctx.commit('namespaces', namespaces)
|
||||||
|
|
||||||
// Put all lists in the list state
|
// Put all lists in the list state
|
||||||
const lists = namespaces.flatMap(({lists}) => lists)
|
const lists = namespaces.flatMap(({lists}) => lists)
|
||||||
|
|
||||||
ctx.commit('lists/setLists', lists, {root: true})
|
ctx.commit('lists/setLists', lists, {root: true})
|
||||||
|
|
||||||
return namespaces
|
return namespaces
|
||||||
} finally {
|
} finally {
|
||||||
cancel()
|
cancel()
|
||||||
|
|
|
@ -13,10 +13,10 @@
|
||||||
>
|
>
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<p>
|
<p>
|
||||||
{{ $t('about.frontendVersion', {version: this.frontendVersion}) }}
|
{{ $t('about.frontendVersion', {version: frontendVersion}) }}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{{ $t('about.apiVersion', {version: this.apiVersion}) }}
|
{{ $t('about.apiVersion', {version: apiVersion}) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<footer class="modal-card-foot is-flex is-justify-content-flex-end">
|
<footer class="modal-card-foot is-flex is-justify-content-flex-end">
|
||||||
|
@ -32,18 +32,11 @@
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import {VERSION} from '../version.json'
|
import {computed} from 'vue'
|
||||||
|
|
||||||
export default {
|
import { store } from '@/store'
|
||||||
name: 'About',
|
import {VERSION as frontendVersion} from '@/version.json'
|
||||||
computed: {
|
|
||||||
frontendVersion() {
|
const apiVersion = computed(() => store.state.config.version)
|
||||||
return VERSION
|
|
||||||
},
|
|
||||||
apiVersion() {
|
|
||||||
return this.$store.state.config.version
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
40
src/views/filters/FilterDelete.vue
Normal file
40
src/views/filters/FilterDelete.vue
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
<template>
|
||||||
|
<modal
|
||||||
|
@close="$router.back()"
|
||||||
|
@submit="deleteSavedFilter()"
|
||||||
|
>
|
||||||
|
<template #header><span>{{ $t('filters.delete.header') }}</span></template>
|
||||||
|
|
||||||
|
<template #text>
|
||||||
|
<p>{{ $t('filters.delete.text') }}</p>
|
||||||
|
</template>
|
||||||
|
</modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { store } from '@/store'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import {success} from '@/message'
|
||||||
|
|
||||||
|
import SavedFilterModel from '@/models/savedFilter'
|
||||||
|
import SavedFilterService from '@/services/savedFilter'
|
||||||
|
import {getSavedFilterIdFromListId} from '@/helpers/savedFilter'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
async function deleteSavedFilter() {
|
||||||
|
// We assume the listId in the route is the pseudolist
|
||||||
|
const savedFilterId = getSavedFilterIdFromListId(route.params.listId)
|
||||||
|
|
||||||
|
const filterService = new SavedFilterService()
|
||||||
|
const filter = new SavedFilterModel({id: savedFilterId})
|
||||||
|
|
||||||
|
await filterService.delete(filter)
|
||||||
|
await store.dispatch('namespaces/loadNamespaces')
|
||||||
|
success({message: t('filters.delete.success')})
|
||||||
|
router.push({name: 'namespaces.index'})
|
||||||
|
}
|
||||||
|
</script>
|
127
src/views/filters/FilterEdit.vue
Normal file
127
src/views/filters/FilterEdit.vue
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
<template>
|
||||||
|
<create-edit
|
||||||
|
:title="$t('filters.edit.title')"
|
||||||
|
primary-icon=""
|
||||||
|
:primary-label="$t('misc.save')"
|
||||||
|
@primary="saveSavedFilter"
|
||||||
|
:tertary="$t('misc.delete')"
|
||||||
|
@tertary="$router.push({ name: 'filter.settings.delete', params: { id: $route.params.listId } })"
|
||||||
|
>
|
||||||
|
<form @submit.prevent="saveSavedFilter()">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="title">{{ $t('filters.attributes.title') }}</label>
|
||||||
|
<div class="control">
|
||||||
|
<input
|
||||||
|
:class="{ 'disabled': filterService.loading}"
|
||||||
|
:disabled="filterService.loading || null"
|
||||||
|
@keyup.enter="saveSavedFilter"
|
||||||
|
class="input"
|
||||||
|
id="title"
|
||||||
|
:placeholder="$t('filters.attributes.titlePlaceholder')"
|
||||||
|
type="text"
|
||||||
|
v-focus
|
||||||
|
v-model="filter.title"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="description">{{ $t('filters.attributes.description') }}</label>
|
||||||
|
<div class="control">
|
||||||
|
<editor
|
||||||
|
:class="{ 'disabled': filterService.loading}"
|
||||||
|
:disabled="filterService.loading"
|
||||||
|
:preview-is-default="false"
|
||||||
|
id="description"
|
||||||
|
:placeholder="$t('filters.attributes.descriptionPlaceholder')"
|
||||||
|
v-model="filter.description"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="filters">{{ $t('filters.title') }}</label>
|
||||||
|
<div class="control">
|
||||||
|
<Filters
|
||||||
|
:class="{ 'disabled': filterService.loading}"
|
||||||
|
:disabled="filterService.loading"
|
||||||
|
class="has-no-shadow has-no-border"
|
||||||
|
v-model="filters"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</create-edit>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, shallowRef, computed, watch } from 'vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { store } from '@/store'
|
||||||
|
import { success } from '@/message'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import {default as Editor} from '@/components/input/AsyncEditor'
|
||||||
|
import CreateEdit from '@/components/misc/create-edit.vue'
|
||||||
|
import Filters from '@/components/list/partials/filters.vue'
|
||||||
|
|
||||||
|
import SavedFilterModel from '@/models/savedFilter'
|
||||||
|
import SavedFilterService from '@/services/savedFilter'
|
||||||
|
|
||||||
|
import {objectToSnakeCase} from '@/helpers/case'
|
||||||
|
import {getSavedFilterIdFromListId} from '@/helpers/savedFilter'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
function useSavedFilter(listId) {
|
||||||
|
const filterService = shallowRef(new SavedFilterService())
|
||||||
|
|
||||||
|
const filter = ref(new SavedFilterModel())
|
||||||
|
const filters = computed({
|
||||||
|
get: () => filter.value.filters,
|
||||||
|
set(value) {
|
||||||
|
filter.value.filters = value
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// loadSavedFilter
|
||||||
|
watch(listId, async () => {
|
||||||
|
// We assume the listId in the route is the pseudolist
|
||||||
|
const savedFilterId = getSavedFilterIdFromListId(route.params.listId)
|
||||||
|
|
||||||
|
filter.value = new SavedFilterModel({id: savedFilterId })
|
||||||
|
const response = await filterService.value.get(filter.value)
|
||||||
|
response.filters = objectToSnakeCase(filter.value.filters)
|
||||||
|
filter.value = response
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
filter.value.filters = filters.value
|
||||||
|
const response = await filterService.value.update(filter.value)
|
||||||
|
await store.dispatch('namespaces/loadNamespaces')
|
||||||
|
success({message: t('filters.edit.success')})
|
||||||
|
response.filters = objectToSnakeCase(filter.value.filters)
|
||||||
|
filter.value = response
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
save,
|
||||||
|
filter,
|
||||||
|
filters,
|
||||||
|
filterService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const listId = computed(() => route.params.listId)
|
||||||
|
|
||||||
|
const {
|
||||||
|
save,
|
||||||
|
filter,
|
||||||
|
filters,
|
||||||
|
filterService,
|
||||||
|
} = useSavedFilter(listId)
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
async function saveSavedFilter() {
|
||||||
|
await save()
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -26,20 +26,20 @@
|
||||||
<label class="label" for="description">{{ $t('filters.attributes.description') }}</label>
|
<label class="label" for="description">{{ $t('filters.attributes.description') }}</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<editor
|
<editor
|
||||||
|
:key="savedFilter.id"
|
||||||
v-model="savedFilter.description"
|
v-model="savedFilter.description"
|
||||||
:class="{ 'disabled': savedFilterService.loading}"
|
:class="{ 'disabled': savedFilterService.loading}"
|
||||||
:disabled="savedFilterService.loading"
|
:disabled="savedFilterService.loading"
|
||||||
:preview-is-default="false"
|
:preview-is-default="false"
|
||||||
id="description"
|
id="description"
|
||||||
:placeholder="$t('filters.attributes.descriptionPlaceholder')"
|
:placeholder="$t('filters.attributes.descriptionPlaceholder')"
|
||||||
v-if="editorActive"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="filters">{{ $t('filters.title') }}</label>
|
<label class="label" for="filters">{{ $t('filters.title') }}</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<filters
|
<Filters
|
||||||
:class="{ 'disabled': savedFilterService.loading}"
|
:class="{ 'disabled': savedFilterService.loading}"
|
||||||
:disabled="savedFilterService.loading"
|
:disabled="savedFilterService.loading"
|
||||||
class="has-no-shadow has-no-border"
|
class="has-no-shadow has-no-border"
|
||||||
|
@ -59,46 +59,30 @@
|
||||||
</modal>
|
</modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import AsyncEditor from '@/components/input/AsyncEditor'
|
import { ref, shallowRef, computed } from 'vue'
|
||||||
|
|
||||||
|
import { store } from '@/store'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import {default as Editor} from '@/components/input/AsyncEditor'
|
||||||
import Filters from '@/components/list/partials/filters.vue'
|
import Filters from '@/components/list/partials/filters.vue'
|
||||||
|
|
||||||
import SavedFilterService from '@/services/savedFilter'
|
import SavedFilterService from '@/services/savedFilter'
|
||||||
import SavedFilterModel from '@/models/savedFilter'
|
import SavedFilterModel from '@/models/savedFilter'
|
||||||
|
|
||||||
export default {
|
const savedFilterService = shallowRef(new SavedFilterService())
|
||||||
name: 'CreateSavedFilter',
|
|
||||||
data() {
|
const savedFilter = ref(new SavedFilterModel())
|
||||||
return {
|
const filters = computed({
|
||||||
editorActive: false,
|
get: () => savedFilter.value.filters,
|
||||||
filters: {
|
set: (value) => (savedFilter.value.filters = value),
|
||||||
sort_by: ['done', 'id'],
|
})
|
||||||
order_by: ['asc', 'desc'],
|
|
||||||
filter_by: ['done'],
|
const router = useRouter()
|
||||||
filter_value: ['false'],
|
async function create() {
|
||||||
filter_comparator: ['equals'],
|
savedFilter.value = await savedFilterService.value.create(savedFilter.value)
|
||||||
filter_concat: 'and',
|
await store.dispatch('namespaces/loadNamespaces')
|
||||||
filter_include_nulls: true,
|
router.push({name: 'list.index', params: {listId: savedFilter.value.getListId()}})
|
||||||
},
|
|
||||||
savedFilterService: new SavedFilterService(),
|
|
||||||
savedFilter: new SavedFilterModel(),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
Filters,
|
|
||||||
editor: AsyncEditor,
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
this.editorActive = false
|
|
||||||
this.$nextTick(() => this.editorActive = true)
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async create() {
|
|
||||||
this.savedFilter.filters = this.filters
|
|
||||||
const savedFilter = await this.savedFilterService.create(this.savedFilter)
|
|
||||||
await this.$store.dispatch('namespaces/loadNamespaces')
|
|
||||||
this.$router.push({name: 'list.index', params: {listId: savedFilter.getListId()}})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
|
@ -1,39 +0,0 @@
|
||||||
<template>
|
|
||||||
<modal
|
|
||||||
@close="$router.back()"
|
|
||||||
@submit="deleteSavedFilter()"
|
|
||||||
>
|
|
||||||
<template #header><span>{{ $t('filters.delete.header') }}</span></template>
|
|
||||||
|
|
||||||
<template #text>
|
|
||||||
<p>{{ $t('filters.delete.text') }}</p>
|
|
||||||
</template>
|
|
||||||
</modal>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import SavedFilterModel from '@/models/savedFilter'
|
|
||||||
import SavedFilterService from '@/services/savedFilter'
|
|
||||||
import ListModel from '@/models/list'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'filter-settings-delete',
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
filterService: new SavedFilterService(),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async deleteSavedFilter() {
|
|
||||||
// We assume the listId in the route is the pseudolist
|
|
||||||
const list = new ListModel({id: this.$route.params.listId})
|
|
||||||
const filter = new SavedFilterModel({id: list.getSavedFilterId()})
|
|
||||||
|
|
||||||
await this.filterService.delete(filter)
|
|
||||||
await this.$store.dispatch('namespaces/loadNamespaces')
|
|
||||||
this.$message.success({message: this.$t('filters.delete.success')})
|
|
||||||
this.$router.push({name: 'namespaces.index'})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -1,117 +0,0 @@
|
||||||
<template>
|
|
||||||
<create-edit
|
|
||||||
:title="$t('filters.edit.title')"
|
|
||||||
primary-icon=""
|
|
||||||
:primary-label="$t('misc.save')"
|
|
||||||
@primary="save"
|
|
||||||
:tertary="$t('misc.delete')"
|
|
||||||
@tertary="$router.push({ name: 'filter.list.settings.delete', params: { id: $route.params.listId } })"
|
|
||||||
>
|
|
||||||
<form @submit.prevent="save()">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label" for="title">{{ $t('filters.attributes.title') }}</label>
|
|
||||||
<div class="control">
|
|
||||||
<input
|
|
||||||
:class="{ 'disabled': filterService.loading}"
|
|
||||||
:disabled="filterService.loading || null"
|
|
||||||
@keyup.enter="save"
|
|
||||||
class="input"
|
|
||||||
id="title"
|
|
||||||
:placeholder="$t('filters.attributes.titlePlaceholder')"
|
|
||||||
type="text"
|
|
||||||
v-focus
|
|
||||||
v-model="filter.title"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label" for="description">{{ $t('filters.attributes.description') }}</label>
|
|
||||||
<div class="control">
|
|
||||||
<editor
|
|
||||||
:class="{ 'disabled': filterService.loading}"
|
|
||||||
:disabled="filterService.loading"
|
|
||||||
:preview-is-default="false"
|
|
||||||
id="description"
|
|
||||||
:placeholder="$t('filters.attributes.descriptionPlaceholder')"
|
|
||||||
v-model="filter.description"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label" for="filters">{{ $t('filters.title') }}</label>
|
|
||||||
<div class="control">
|
|
||||||
<filters
|
|
||||||
:class="{ 'disabled': filterService.loading}"
|
|
||||||
:disabled="filterService.loading"
|
|
||||||
class="has-no-shadow has-no-border"
|
|
||||||
v-model="filters"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</create-edit>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import AsyncEditor from '@/components/input/AsyncEditor'
|
|
||||||
|
|
||||||
import CreateEdit from '@/components/misc/create-edit.vue'
|
|
||||||
|
|
||||||
import SavedFilterModel from '@/models/savedFilter'
|
|
||||||
import SavedFilterService from '@/services/savedFilter'
|
|
||||||
import ListModel from '@/models/list'
|
|
||||||
import Filters from '@/components/list/partials/filters.vue'
|
|
||||||
import {objectToSnakeCase} from '@/helpers/case'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'filter-settings-edit',
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
filter: SavedFilterModel,
|
|
||||||
filterService: new SavedFilterService(),
|
|
||||||
filters: {
|
|
||||||
sort_by: ['done', 'id'],
|
|
||||||
order_by: ['asc', 'desc'],
|
|
||||||
filter_by: ['done'],
|
|
||||||
filter_value: ['false'],
|
|
||||||
filter_comparator: ['equals'],
|
|
||||||
filter_concat: 'and',
|
|
||||||
filter_include_nulls: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
showDeleteModal: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
CreateEdit,
|
|
||||||
Filters,
|
|
||||||
editor: AsyncEditor,
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
// call again the method if the route changes
|
|
||||||
'$route': {
|
|
||||||
handler: 'loadSavedFilter',
|
|
||||||
deep: true,
|
|
||||||
immediate: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async loadSavedFilter() {
|
|
||||||
// We assume the listId in the route is the pseudolist
|
|
||||||
const list = new ListModel({id: this.$route.params.listId})
|
|
||||||
|
|
||||||
this.filter = new SavedFilterModel({id: list.getSavedFilterId()})
|
|
||||||
this.filter = await this.filterService.get(this.filter)
|
|
||||||
this.filters = objectToSnakeCase(this.filter.filters)
|
|
||||||
},
|
|
||||||
async save() {
|
|
||||||
this.filter.filters = this.filters
|
|
||||||
const filter = await this.filterService.update(this.filter)
|
|
||||||
await this.$store.dispatch('namespaces/loadNamespaces')
|
|
||||||
this.$message.success({message: this.$t('filters.edit.success')})
|
|
||||||
this.filter = filter
|
|
||||||
this.filters = objectToSnakeCase(this.filter.filters)
|
|
||||||
this.$router.back()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -41,7 +41,7 @@
|
||||||
<div class="progress-dots">
|
<div class="progress-dots">
|
||||||
<span v-for="i in progressDotsCount" :key="i" />
|
<span v-for="i in progressDotsCount" :key="i" />
|
||||||
</div>
|
</div>
|
||||||
<Logo alt="Vikunja" />
|
<Logo/>
|
||||||
</div>
|
</div>
|
||||||
<p>{{ $t('migrate.inProgress') }}</p>
|
<p>{{ $t('migrate.inProgress') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -186,9 +186,10 @@ export default {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
|
|
||||||
img {
|
img, svg {
|
||||||
display: block;
|
display: block;
|
||||||
max-height: 100px;
|
max-height: 100px;
|
||||||
|
max-width: 100px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,36 +35,25 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import DataExportService from '../../services/dataExport'
|
import {ref, computed, reactive} from 'vue'
|
||||||
|
import DataExportService from '@/services/dataExport'
|
||||||
|
import {store} from '@/store'
|
||||||
|
|
||||||
export default {
|
const dataExportService = reactive(new DataExportService())
|
||||||
name: 'data-export-download',
|
const password = ref('')
|
||||||
data() {
|
const errPasswordRequired = ref(false)
|
||||||
return {
|
const passwordInput = ref(null)
|
||||||
dataExportService: DataExportService,
|
|
||||||
password: '',
|
|
||||||
errPasswordRequired: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
this.dataExportService = new DataExportService()
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
isLocalUser() {
|
|
||||||
return this.$store.state.auth.info?.isLocalUser
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
download() {
|
|
||||||
if (this.password === '' && this.isLocalUser) {
|
|
||||||
this.errPasswordRequired = true
|
|
||||||
this.$refs.passwordInput.focus()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dataExportService.download(this.password)
|
const isLocalUser = computed(() => store.state.auth.info?.isLocalUser)
|
||||||
},
|
|
||||||
},
|
function download() {
|
||||||
|
if (password.value === '' && isLocalUser.value) {
|
||||||
|
errPasswordRequired.value = true
|
||||||
|
passwordInput.value.focus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dataExportService.download(password.value)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -60,55 +60,49 @@
|
||||||
{{ $t('user.auth.login') }}
|
{{ $t('user.auth.login') }}
|
||||||
</x-button>
|
</x-button>
|
||||||
</div>
|
</div>
|
||||||
<legal/>
|
<Legal />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import PasswordResetModel from '../../models/passwordReset'
|
import {ref, reactive} from 'vue'
|
||||||
import PasswordResetService from '../../services/passwordReset'
|
import { useI18n } from 'vue-i18n'
|
||||||
import Legal from '../../components/misc/legal'
|
|
||||||
|
|
||||||
export default {
|
import Legal from '@/components/misc/legal'
|
||||||
components: {
|
|
||||||
Legal,
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
passwordResetService: new PasswordResetService(),
|
|
||||||
credentials: {
|
|
||||||
password: '',
|
|
||||||
password2: '',
|
|
||||||
},
|
|
||||||
errorMsg: '',
|
|
||||||
successMessage: '',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
import PasswordResetModel from '@/models/passwordReset'
|
||||||
this.setTitle(this.$t('user.auth.resetPassword'))
|
import PasswordResetService from '@/services/passwordReset'
|
||||||
},
|
import { useTitle } from '@/composables/useTitle'
|
||||||
|
|
||||||
methods: {
|
const { t } = useI18n()
|
||||||
async submit() {
|
useTitle(() => t('user.auth.resetPassword'))
|
||||||
this.errorMsg = ''
|
|
||||||
|
|
||||||
if (this.credentials.password2 !== this.credentials.password) {
|
const credentials = reactive({
|
||||||
this.errorMsg = this.$t('user.auth.passwordsDontMatch')
|
password: '',
|
||||||
return
|
password2: '',
|
||||||
}
|
})
|
||||||
|
|
||||||
let passwordReset = new PasswordResetModel({newPassword: this.credentials.password})
|
const passwordResetService = reactive(new PasswordResetService())
|
||||||
try {
|
const errorMsg = ref('')
|
||||||
const { message } = this.passwordResetService.resetPassword(passwordReset)
|
const successMessage = ref('')
|
||||||
this.successMessage = message
|
|
||||||
localStorage.removeItem('passwordResetToken')
|
async function submit() {
|
||||||
} catch(e) {
|
errorMsg.value = ''
|
||||||
this.errorMsg = e.response.data.message
|
|
||||||
}
|
if (credentials.password2 !== credentials.password) {
|
||||||
},
|
errorMsg.value = t('user.auth.passwordsDontMatch')
|
||||||
},
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwordReset = new PasswordResetModel({newPassword: credentials.password})
|
||||||
|
try {
|
||||||
|
const { message } = passwordResetService.resetPassword(passwordReset)
|
||||||
|
successMessage.value = message
|
||||||
|
localStorage.removeItem('passwordResetToken')
|
||||||
|
} catch(e) {
|
||||||
|
errorMsg.value = e.response.data.message
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -36,12 +36,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="password1">{{ $t('user.auth.password') }}</label>
|
<label class="label" for="password">{{ $t('user.auth.password') }}</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input
|
<input
|
||||||
class="input"
|
class="input"
|
||||||
id="password1"
|
id="password"
|
||||||
name="password1"
|
name="password"
|
||||||
:placeholder="$t('user.auth.passwordPlaceholder')"
|
:placeholder="$t('user.auth.passwordPlaceholder')"
|
||||||
required
|
required
|
||||||
type="password"
|
type="password"
|
||||||
|
@ -52,17 +52,17 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="password2">{{ $t('user.auth.passwordRepeat') }}</label>
|
<label class="label" for="passwordValidation">{{ $t('user.auth.passwordRepeat') }}</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input
|
<input
|
||||||
class="input"
|
class="input"
|
||||||
id="password2"
|
id="passwordValidation"
|
||||||
name="password2"
|
name="passwordValidation"
|
||||||
:placeholder="$t('user.auth.passwordPlaceholder')"
|
:placeholder="$t('user.auth.passwordPlaceholder')"
|
||||||
required
|
required
|
||||||
type="password"
|
type="password"
|
||||||
autocomplete="new-password"
|
autocomplete="new-password"
|
||||||
v-model="credentials.password2"
|
v-model="passwordValidation"
|
||||||
@keyup.enter="submit"
|
@keyup.enter="submit"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -95,61 +95,50 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import router from '../../router'
|
import {ref, reactive, toRaw, computed, onBeforeMount} from 'vue'
|
||||||
import {mapState} from 'vuex'
|
import { useI18n } from 'vue-i18n'
|
||||||
import {LOADING} from '@/store/mutation-types'
|
|
||||||
import Legal from '../../components/misc/legal'
|
|
||||||
|
|
||||||
export default {
|
import router from '@/router'
|
||||||
components: {
|
import { store } from '@/store'
|
||||||
Legal,
|
import { useTitle } from '@/composables/useTitle'
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
credentials: {
|
|
||||||
username: '',
|
|
||||||
email: '',
|
|
||||||
password: '',
|
|
||||||
password2: '',
|
|
||||||
},
|
|
||||||
errorMessage: '',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
beforeMount() {
|
|
||||||
// Check if the user is already logged in, if so, redirect them to the homepage
|
|
||||||
if (this.authenticated) {
|
|
||||||
router.push({name: 'home'})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.setTitle(this.$t('user.auth.register'))
|
|
||||||
},
|
|
||||||
computed: mapState({
|
|
||||||
authenticated: state => state.auth.authenticated,
|
|
||||||
loading: LOADING,
|
|
||||||
}),
|
|
||||||
methods: {
|
|
||||||
async submit() {
|
|
||||||
this.errorMessage = ''
|
|
||||||
|
|
||||||
if (this.credentials.password2 !== this.credentials.password) {
|
import Legal from '@/components/misc/legal'
|
||||||
this.errorMessage = this.$t('user.auth.passwordsDontMatch')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const credentials = {
|
// FIXME: use the `beforeEnter` hook of vue-router
|
||||||
username: this.credentials.username,
|
// Check if the user is already logged in, if so, redirect them to the homepage
|
||||||
email: this.credentials.email,
|
onBeforeMount(() => {
|
||||||
password: this.credentials.password,
|
if (store.state.auth.authenticated) {
|
||||||
}
|
router.push({name: 'home'})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
try {
|
const { t } = useI18n()
|
||||||
await this.$store.dispatch('auth/register', credentials)
|
useTitle(() => t('user.auth.register'))
|
||||||
} catch(e) {
|
|
||||||
this.errorMessage = e.message
|
const credentials = reactive({
|
||||||
}
|
username: '',
|
||||||
},
|
email: '',
|
||||||
},
|
password: '',
|
||||||
|
})
|
||||||
|
const passwordValidation = ref('')
|
||||||
|
|
||||||
|
const loading = computed(() => store.state.loading)
|
||||||
|
const errorMessage = ref('')
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
errorMessage.value = ''
|
||||||
|
|
||||||
|
if (credentials.password !== passwordValidation.value) {
|
||||||
|
errorMessage.value = t('user.auth.passwordsDontMatch')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
await store.dispatch('auth/register', toRaw(credentials))
|
||||||
|
} catch(e) {
|
||||||
|
errorMessage.value = e.message
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -43,42 +43,38 @@
|
||||||
{{ $t('user.auth.login') }}
|
{{ $t('user.auth.login') }}
|
||||||
</x-button>
|
</x-button>
|
||||||
</div>
|
</div>
|
||||||
<legal/>
|
<Legal />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import PasswordResetModel from '../../models/passwordReset'
|
import {ref, reactive} from 'vue'
|
||||||
import PasswordResetService from '../../services/passwordReset'
|
import { useI18n } from 'vue-i18n'
|
||||||
import Legal from '../../components/misc/legal'
|
|
||||||
|
|
||||||
export default {
|
import Legal from '@/components/misc/legal'
|
||||||
components: {
|
|
||||||
Legal,
|
import PasswordResetModel from '@/models/passwordReset'
|
||||||
},
|
import PasswordResetService from '@/services/passwordReset'
|
||||||
data() {
|
import { useTitle } from '@/composables/useTitle'
|
||||||
return {
|
|
||||||
passwordResetService: new PasswordResetService(),
|
const { t } = useI18n()
|
||||||
passwordReset: new PasswordResetModel(),
|
useTitle(() => t('user.auth.resetPassword'))
|
||||||
errorMsg: '',
|
|
||||||
isSuccess: false,
|
// Not sure if this instance needs a shalloRef at all
|
||||||
}
|
const passwordResetService = reactive(new PasswordResetService())
|
||||||
},
|
const passwordReset = ref(new PasswordResetModel())
|
||||||
mounted() {
|
const errorMsg = ref('')
|
||||||
this.setTitle(this.$t('user.auth.resetPassword'))
|
const isSuccess = ref(false)
|
||||||
},
|
|
||||||
methods: {
|
async function submit() {
|
||||||
async submit() {
|
errorMsg.value = ''
|
||||||
this.errorMsg = ''
|
try {
|
||||||
try {
|
await passwordResetService.requestResetPassword(passwordReset.value)
|
||||||
await this.passwordResetService.requestResetPassword(this.passwordReset)
|
isSuccess.value = true
|
||||||
this.isSuccess = true
|
} catch(e) {
|
||||||
} catch(e) {
|
errorMsg.value = e.response.data.message
|
||||||
this.errorMsg = e.response.data.message
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -57,24 +57,19 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import {mapState} from 'vuex'
|
import {computed} from 'vue'
|
||||||
|
import { store } from '@/store'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useTitle } from '@/composables/useTitle'
|
||||||
|
|
||||||
export default {
|
const { t } = useI18n()
|
||||||
name: 'Settings',
|
useTitle(() => t('user.settings.title'))
|
||||||
mounted() {
|
|
||||||
this.setTitle(this.$t('user.settings.title'))
|
const totpEnabled = computed(() => store.state.config.totpEnabled)
|
||||||
},
|
const caldavEnabled = computed(() => store.state.config.caldavEnabled)
|
||||||
computed: {
|
const migratorsEnabled = computed(() => store.getters['config/migratorsEnabled'])
|
||||||
...mapState('config', ['totpEnabled', 'caldavEnabled']),
|
const isLocalUser = computed(() => store.state.auth.info?.isLocalUser)
|
||||||
migratorsEnabled() {
|
|
||||||
return this.$store.getters['config/migratorsEnabled']
|
|
||||||
},
|
|
||||||
isLocalUser() {
|
|
||||||
return this.$store.state.auth.info?.isLocalUser
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
Reference in New Issue
Block a user