Merge remote-tracking branch 'upstream/main' into bulma-css-variables
continuous-integration/drone/pr Build is failing
Details
continuous-integration/drone/pr Build is failing
Details
This commit is contained in:
commit
41ef1eea4a
|
@ -148,6 +148,7 @@ steps:
|
|||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
commands:
|
||||
- shasum -a 384 -c ./scripts/deploy-preview-netlify.js.sha384
|
||||
- node ./scripts/deploy-preview-netlify.js
|
||||
depends_on:
|
||||
- build-prod
|
||||
|
@ -655,6 +656,6 @@ steps:
|
|||
from_secret: crowdin_key
|
||||
---
|
||||
kind: signature
|
||||
hmac: 15df446c7e93a881249d46273485183386157229ee6a37b1ed0fcb2a0b32bbe2
|
||||
hmac: 188ee90100c5fc5922a445e531e7a47453121edddb2a64a182eb23ed2bf602de
|
||||
|
||||
...
|
||||
|
|
|
@ -24,8 +24,8 @@ context('Registration', () => {
|
|||
cy.visit('/register')
|
||||
cy.get('#username').type(fixture.username)
|
||||
cy.get('#email').type(fixture.email)
|
||||
cy.get('#password1').type(fixture.password)
|
||||
cy.get('#password2').type(fixture.password)
|
||||
cy.get('#password').type(fixture.password)
|
||||
cy.get('#passwordValidation').type(fixture.password)
|
||||
cy.get('#register-submit').click()
|
||||
cy.url().should('include', '/')
|
||||
cy.clock(1625656161057) // 13:00
|
||||
|
@ -42,8 +42,8 @@ context('Registration', () => {
|
|||
cy.visit('/register')
|
||||
cy.get('#username').type(fixture.username)
|
||||
cy.get('#email').type(fixture.email)
|
||||
cy.get('#password1').type(fixture.password)
|
||||
cy.get('#password2').type(fixture.password)
|
||||
cy.get('#password').type(fixture.password)
|
||||
cy.get('#passwordValidation').type(fixture.password)
|
||||
cy.get('#register-submit').click()
|
||||
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",
|
||||
"easymde": "2.15.0",
|
||||
"flatpickr": "4.6.9",
|
||||
"flexsearch": "0.7.21",
|
||||
"highlight.js": "11.3.1",
|
||||
"is-touch-device": "1.0.1",
|
||||
"lodash.clonedeep": "4.5.0",
|
||||
|
@ -39,8 +40,8 @@
|
|||
"register-service-worker": "1.7.2",
|
||||
"snake-case": "3.0.4",
|
||||
"ufo": "0.7.9",
|
||||
"vue": "3.2.21",
|
||||
"vue-advanced-cropper": "2.6.3",
|
||||
"vue": "3.2.22",
|
||||
"vue-advanced-cropper": "2.7.0",
|
||||
"vue-drag-resize": "2.0.3",
|
||||
"vue-flatpickr-component": "9.0.5",
|
||||
"vue-i18n": "9.2.0-beta.18",
|
||||
|
@ -55,6 +56,7 @@
|
|||
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
||||
"@fortawesome/vue-fontawesome": "3.0.0-5",
|
||||
"@types/flexsearch": "0.7.2",
|
||||
"@types/jest": "27.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "5.3.1",
|
||||
"@typescript-eslint/parser": "5.3.1",
|
||||
|
@ -63,7 +65,7 @@
|
|||
"@vue/eslint-config-typescript": "9.0.1",
|
||||
"autoprefixer": "10.4.0",
|
||||
"axios": "0.24.0",
|
||||
"browserslist": "4.17.6",
|
||||
"browserslist": "4.18.0",
|
||||
"cypress": "9.0.0",
|
||||
"cypress-file-upload": "5.0.8",
|
||||
"esbuild": "0.13.13",
|
||||
|
@ -81,9 +83,9 @@
|
|||
"ts-jest": "27.0.7",
|
||||
"typescript": "4.4.4",
|
||||
"vite": "2.6.14",
|
||||
"vite-plugin-pwa": "0.11.3",
|
||||
"vue-tsc": "0.29.4",
|
||||
"vite-plugin-pwa": "0.11.5",
|
||||
"vite-svg-loader": "3.1.0",
|
||||
"vue-tsc": "0.29.4",
|
||||
"wait-on": "6.0.0",
|
||||
"workbox-cli": "6.3.0"
|
||||
},
|
||||
|
|
|
@ -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="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)"/>
|
||||
|
|
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 EditLabels from '@/components/tasks/partials/editLabels.vue'
|
||||
|
||||
import {objectToSnakeCase} from '@/helpers/case'
|
||||
|
||||
// FIXME: merge with DEFAULT_PARAMS in taskList.js
|
||||
const DEFAULT_PARAMS = {
|
||||
sort_by: [],
|
||||
|
@ -261,7 +263,9 @@ export default {
|
|||
watch: {
|
||||
modelValue: {
|
||||
handler(value) {
|
||||
this.params = value
|
||||
// FIXME: filters should only be converted to snake case in
|
||||
// the last moment
|
||||
this.params = objectToSnakeCase(value)
|
||||
this.prepareFilters()
|
||||
},
|
||||
immediate: true,
|
||||
|
|
|
@ -67,7 +67,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
apiDomain() {
|
||||
return parseURL(this.apiUrl).host
|
||||
return parseURL(this.apiUrl).host || parseURL(window.location.href).host
|
||||
},
|
||||
},
|
||||
props: {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<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">
|
||||
<h3>{{ $t(s.title) }}</h3>
|
||||
|
||||
|
@ -12,10 +12,11 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<dl>
|
||||
<dl class="shortcut-list">
|
||||
<template v-for="(sc, si) in s.shortcuts" :key="si">
|
||||
<dt>{{ $t(sc.title) }}</dt>
|
||||
<dt class="shortcut-title">{{ $t(sc.title) }}</dt>
|
||||
<shortcut
|
||||
class="shortcut-keys"
|
||||
is="dd"
|
||||
:keys="sc.keys"
|
||||
:combination="typeof sc.combination !== 'undefined' ? $t(`keyboardShortcuts.${sc.combination}`) : null"/>
|
||||
|
@ -47,8 +48,30 @@ export default {
|
|||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
dt {
|
||||
font-weight: bold;
|
||||
<style scoped>
|
||||
.keyboard-shortcuts {
|
||||
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>
|
|
@ -89,7 +89,7 @@ export default {
|
|||
},
|
||||
currentPage: {
|
||||
type: Number,
|
||||
required: true,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
</section>
|
||||
<transition name="fade">
|
||||
<section class="vikunja-loading" v-if="showLoading">
|
||||
<img alt="Vikunja" :src="logoUrl" width="100" height="100"/>
|
||||
<Logo class="logo"/>
|
||||
<p>
|
||||
<span class="loader-container is-loading-small is-loading"></span>
|
||||
{{ $t('ready.loading') }}
|
||||
|
@ -41,7 +41,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import logoUrl from '@/assets/logo.svg'
|
||||
import Logo from '@/assets/logo.svg?component'
|
||||
import ApiConfig from '@/components/misc/api-config'
|
||||
import NoAuthWrapper from '@/components/misc/no-auth-wrapper'
|
||||
import {mapState} from 'vuex'
|
||||
|
@ -50,12 +50,12 @@ import {ERROR_NO_API_URL} from '@/helpers/checkAndSetApiUrl'
|
|||
export default {
|
||||
name: 'ready',
|
||||
components: {
|
||||
Logo,
|
||||
NoAuthWrapper,
|
||||
ApiConfig,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
logoUrl,
|
||||
error: '',
|
||||
errorNoApiUrl: ERROR_NO_API_URL,
|
||||
}
|
||||
|
@ -100,10 +100,12 @@ export default {
|
|||
right: 0;
|
||||
background: var(--grey-100);
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
img {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.logo {
|
||||
margin-bottom: 1rem;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.loader-container {
|
||||
|
|
|
@ -25,15 +25,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
namespaces() {
|
||||
if (this.query === '') {
|
||||
return []
|
||||
}
|
||||
|
||||
return this.$store.state.namespaces.namespaces.filter(n => {
|
||||
return !n.isArchived &&
|
||||
n.id > 0 &&
|
||||
n.title.toLowerCase().includes(this.query.toLowerCase())
|
||||
})
|
||||
return this.$store.getters['namespaces/searchNamespace'](this.query)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -110,40 +110,32 @@ export default {
|
|||
results() {
|
||||
let 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
|
||||
|
||||
if (list === null) {
|
||||
lists = []
|
||||
} 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 => {
|
||||
if (typeof l === 'undefined' || l === null) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (l.isArchived) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof ncache[l.namespaceId] === 'undefined') {
|
||||
ncache[l.namespaceId] = this.$store.getters['namespaces/getNamespaceById'](l.namespaceId)
|
||||
}
|
||||
|
||||
if (ncache[l.namespaceId].isArchived) {
|
||||
return false
|
||||
}
|
||||
|
||||
return l.title.toLowerCase().includes(list.toLowerCase())
|
||||
}) ?? []
|
||||
return !ncache[l.namespaceId].isArchived
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<template>
|
||||
<multiselect
|
||||
class="control is-expanded"
|
||||
:loading="listSerivce.loading"
|
||||
:placeholder="$t('list.search')"
|
||||
@search="findLists"
|
||||
:search-results="foundLists"
|
||||
|
@ -18,7 +17,6 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import ListService from '../../../services/list'
|
||||
import ListModel from '../../../models/list'
|
||||
import Multiselect from '@/components/input/multiselect.vue'
|
||||
|
||||
|
@ -26,7 +24,6 @@ export default {
|
|||
name: 'listSearch',
|
||||
data() {
|
||||
return {
|
||||
listSerivce: new ListService(),
|
||||
list: new ListModel(),
|
||||
foundLists: [],
|
||||
}
|
||||
|
@ -50,17 +47,8 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
async findLists(query) {
|
||||
if (query === '') {
|
||||
this.clearAll()
|
||||
return
|
||||
}
|
||||
|
||||
this.foundLists = await this.listSerivce.getAll({}, {s: query})
|
||||
},
|
||||
|
||||
clearAll() {
|
||||
this.foundLists = []
|
||||
findLists(query) {
|
||||
this.foundLists = this.$store.getters['lists/searchList'](query)
|
||||
},
|
||||
|
||||
select(list) {
|
||||
|
@ -82,6 +70,10 @@ export default {
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.list-namespace-title {
|
||||
<<<<<<< HEAD
|
||||
color: var(--grey-500);
|
||||
=======
|
||||
color: $grey-500;
|
||||
>>>>>>> upstream/main
|
||||
}
|
||||
</style>
|
|
@ -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 {createNewIndexer} from '../indexes'
|
||||
|
||||
const {add} = createNewIndexer('labels', ['title', 'description'])
|
||||
|
||||
describe('filter labels', () => {
|
||||
const state = {
|
||||
labels: [
|
||||
{id: 1, title: 'label1'},
|
||||
{id: 2, title: 'label2'},
|
||||
{id: 3, title: 'label3'},
|
||||
{id: 4, title: 'label4'},
|
||||
{id: 5, title: 'label5'},
|
||||
{id: 6, title: 'label6'},
|
||||
{id: 7, title: 'label7'},
|
||||
{id: 8, title: 'label8'},
|
||||
{id: 9, title: 'label9'},
|
||||
],
|
||||
labels: {
|
||||
1: {id: 1, title: 'label1'},
|
||||
2: {id: 2, title: 'label2'},
|
||||
3: {id: 3, title: 'label3'},
|
||||
4: {id: 4, title: 'label4'},
|
||||
5: {id: 5, title: 'label5'},
|
||||
6: {id: 6, title: 'label6'},
|
||||
7: {id: 7, title: 'label7'},
|
||||
8: {id: 8, title: 'label8'},
|
||||
9: {id: 9, title: 'label9'},
|
||||
},
|
||||
}
|
||||
|
||||
Object.values(state.labels).forEach(add)
|
||||
|
||||
it('should return an empty array for an empty query', () => {
|
||||
const labels = filterLabelsByQuery(state, [], '')
|
||||
|
||||
|
@ -31,7 +36,7 @@ describe('filter labels', () => {
|
|||
id: number,
|
||||
title: string,
|
||||
}
|
||||
|
||||
|
||||
const labelsToHide: label[] = [{id: 1, title: '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,
|
||||
title: string,
|
||||
}
|
||||
|
||||
interface labelState {
|
||||
labels: label[],
|
||||
labels: {
|
||||
[k: number]: label,
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -15,17 +21,12 @@ interface labelState {
|
|||
* @returns {Array}
|
||||
*/
|
||||
export function filterLabelsByQuery(state: labelState, labelsToHide: label[], query: string) {
|
||||
if (query === '') {
|
||||
return []
|
||||
}
|
||||
const labelIdsToHide: number[] = labelsToHide.map(({id}) => id)
|
||||
|
||||
const labelQuery = query.toLowerCase()
|
||||
const labelIds = labelsToHide.map(({id}) => id)
|
||||
return Object
|
||||
.values(state.labels)
|
||||
.filter(({id, title}) => {
|
||||
return !labelIds.includes(id) && title.toLowerCase().includes(labelQuery)
|
||||
})
|
||||
return search(query)
|
||||
?.filter(value => !labelIdsToHide.includes(value))
|
||||
.map(id => state.labels[id])
|
||||
|| []
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
export const setTitle = title => {
|
||||
if (typeof title === 'undefined' || title === '') {
|
||||
document.title = 'Vikunja'
|
||||
return
|
||||
}
|
||||
|
||||
document.title = `${title} | Vikunja`
|
||||
export function setTitle(title) {
|
||||
document.title = (typeof title === 'undefined' || title === '')
|
||||
? 'Vikunja'
|
||||
: `${title} | Vikunja`
|
||||
}
|
|
@ -17,14 +17,14 @@
|
|||
"text": "Požadovaná stránka neexistuje."
|
||||
},
|
||||
"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:"
|
||||
"loading": "Vikunja se načítá…",
|
||||
"errorOccured": "Došlo k chybě:",
|
||||
"checkApiUrl": "Zkontrolujte, zda je adresa URL API správná.",
|
||||
"noApiUrlConfigured": "Nebyla nakonfigurována žádná adresa API. Prosím nastavte jednu níže:"
|
||||
},
|
||||
"offline": {
|
||||
"title": "You are offline.",
|
||||
"text": "Please check your network connection and try again."
|
||||
"title": "Jste offline.",
|
||||
"text": "Zkontrolujte své internetové připojení a zkuste to znovu."
|
||||
},
|
||||
"user": {
|
||||
"auth": {
|
||||
|
@ -354,7 +354,7 @@
|
|||
},
|
||||
"filters": {
|
||||
"title": "Filtry",
|
||||
"clear": "Clear Filters",
|
||||
"clear": "Vymazat filtry",
|
||||
"attributes": {
|
||||
"title": "Název",
|
||||
"titlePlaceholder": "Název uloženého filtru přijde sem…",
|
||||
|
@ -461,8 +461,8 @@
|
|||
"default": "Výchozí",
|
||||
"close": "Zavřít",
|
||||
"download": "Stáhnout",
|
||||
"showMenu": "Show the menu",
|
||||
"hideMenu": "Hide the menu"
|
||||
"showMenu": "Zobrazit nabídku",
|
||||
"hideMenu": "Skrýt nabídku"
|
||||
},
|
||||
"input": {
|
||||
"resetColor": "Obnovit barvu",
|
||||
|
@ -656,7 +656,7 @@
|
|||
"searchPlaceholder": "Hledejte nový úkol, který chcete přidat jako související…",
|
||||
"createPlaceholder": "Přidat toto jako nový související úkol",
|
||||
"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.",
|
||||
"delete": "Odstranit vztah k úloze",
|
||||
"deleteText1": "Jste si jisti, že chcete odstranit tento vztah úkolu?",
|
||||
|
@ -757,12 +757,12 @@
|
|||
},
|
||||
"keyboardShortcuts": {
|
||||
"title": "Klávesové zkratky",
|
||||
"general": "General",
|
||||
"general": "Obecné",
|
||||
"allPages": "Tyto zkratky fungují na všech stránkách.",
|
||||
"currentPageOnly": "Tyto zkratky fungují pouze na aktuální stránce.",
|
||||
"toggleMenu": "Přepnout nabídku",
|
||||
"quickSearch": "Otevřít vyhledávání / panel rychlých akcí",
|
||||
"then": "then",
|
||||
"then": "potom",
|
||||
"task": {
|
||||
"title": "Stránka úkolů",
|
||||
"done": "Označit úkol jako hotový",
|
||||
|
@ -773,11 +773,11 @@
|
|||
"related": "Upravit související úkoly tohoto úkolu"
|
||||
},
|
||||
"list": {
|
||||
"title": "List Views",
|
||||
"switchToListView": "Switch to list view",
|
||||
"switchToGanttView": "Switch to gantt view",
|
||||
"switchToKanbanView": "Switch to kanban view",
|
||||
"switchToTableView": "Switch to table view"
|
||||
"title": "Zobrazení seznamů",
|
||||
"switchToListView": "Přepnout na zobrazení seznamu",
|
||||
"switchToGanttView": "Přepnout na zobrazení gantt",
|
||||
"switchToKanbanView": "Přepnout na zobrazení kanbanu",
|
||||
"switchToTableView": "Přepnout na zobrazení tabulky"
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
|
@ -799,9 +799,9 @@
|
|||
"urlPlaceholder": "např. https://localhost:3456",
|
||||
"change": "změnit",
|
||||
"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}\".",
|
||||
"urlRequired": "A url is required."
|
||||
"urlRequired": "Je vyžadována adresa URL."
|
||||
},
|
||||
"loadingError": {
|
||||
"failed": "Načítání selhalo, prosím {0}. Pokud chyba přetrvává, {1}.",
|
||||
|
@ -816,7 +816,7 @@
|
|||
"quickActions": {
|
||||
"commands": "Příkazy",
|
||||
"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",
|
||||
"lists": "Seznamy",
|
||||
"teams": "Týmy",
|
||||
|
|
|
@ -17,14 +17,14 @@
|
|||
"text": "Trang bạn yêu cầu không tồn tại."
|
||||
},
|
||||
"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:"
|
||||
"loading": "Vikunja đang tải…",
|
||||
"errorOccured": "Đã xảy ra lỗi:",
|
||||
"checkApiUrl": "Vui lòng kiểm tra lại url api.",
|
||||
"noApiUrlConfigured": "Không có url API nào được cấu hình. Hãy đặt một cái:"
|
||||
},
|
||||
"offline": {
|
||||
"title": "You are offline.",
|
||||
"text": "Please check your network connection and try again."
|
||||
"title": "Bạn đang offline.",
|
||||
"text": "Vui lòng kiểm tra kết nối mạng của bạn và thử lại."
|
||||
},
|
||||
"user": {
|
||||
"auth": {
|
||||
|
@ -354,7 +354,7 @@
|
|||
},
|
||||
"filters": {
|
||||
"title": "Bộ lọc",
|
||||
"clear": "Clear Filters",
|
||||
"clear": "Xoá các bộ lọc",
|
||||
"attributes": {
|
||||
"title": "Tiêu đề",
|
||||
"titlePlaceholder": "Tiêu đề bộ lọc đã lưu ở đây…",
|
||||
|
@ -461,8 +461,8 @@
|
|||
"default": "Mặc định",
|
||||
"close": "Đóng",
|
||||
"download": "Tải về",
|
||||
"showMenu": "Show the menu",
|
||||
"hideMenu": "Hide the menu"
|
||||
"showMenu": "Hiển thị menu",
|
||||
"hideMenu": "Ẩn menu"
|
||||
},
|
||||
"input": {
|
||||
"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…",
|
||||
"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.",
|
||||
"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.",
|
||||
"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?",
|
||||
|
@ -757,12 +757,12 @@
|
|||
},
|
||||
"keyboardShortcuts": {
|
||||
"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.",
|
||||
"currentPageOnly": "Các phím tắt này chỉ hoạt động ở trang hiện tại.",
|
||||
"toggleMenu": "Bật/tắt Menu",
|
||||
"quickSearch": "Mở thanh tìm kiếm/thao tác nhanh",
|
||||
"then": "then",
|
||||
"then": "sau đó",
|
||||
"task": {
|
||||
"title": "Trang công việc",
|
||||
"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"
|
||||
},
|
||||
"list": {
|
||||
"title": "List Views",
|
||||
"switchToListView": "Switch to list view",
|
||||
"switchToGanttView": "Switch to gantt view",
|
||||
"switchToKanbanView": "Switch to kanban view",
|
||||
"switchToTableView": "Switch to table view"
|
||||
"title": "Xem danh sách",
|
||||
"switchToListView": "Chuyển sang chế độ danh sách",
|
||||
"switchToGanttView": "Chuyển sang biểu đồ Gantt",
|
||||
"switchToKanbanView": "Chuyển sang Kanban",
|
||||
"switchToTableView": "Chuyển qua xem Bảng"
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
|
@ -799,9 +799,9 @@
|
|||
"urlPlaceholder": "ví dụ: https://localhost:3456",
|
||||
"change": "thay đổi",
|
||||
"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}\".",
|
||||
"urlRequired": "A url is required."
|
||||
"urlRequired": "Cần có một url."
|
||||
},
|
||||
"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}.",
|
||||
|
@ -816,7 +816,7 @@
|
|||
"quickActions": {
|
||||
"commands": "Các lệnh",
|
||||
"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ụ",
|
||||
"lists": "Danh sách",
|
||||
"teams": "Team",
|
||||
|
|
|
@ -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 ListSettingDelete from '../views/list/settings/delete'
|
||||
import ListSettingArchive from '../views/list/settings/archive'
|
||||
import FilterSettingEdit from '../views/filters/settings/edit'
|
||||
import FilterSettingDelete from '../views/filters/settings/delete'
|
||||
|
||||
// Namespace Settings
|
||||
import NamespaceSettingEdit from '../views/namespaces/settings/edit'
|
||||
import NamespaceSettingShare from '../views/namespaces/settings/share'
|
||||
import NamespaceSettingArchive from '../views/namespaces/settings/archive'
|
||||
import NamespaceSettingDelete from '../views/namespaces/settings/delete'
|
||||
|
||||
// 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 GetPasswordResetComponent = () => import('../views/user/RequestPasswordReset')
|
||||
|
@ -123,6 +125,7 @@ const router = createRouter({
|
|||
path: '/user/settings',
|
||||
name: 'user.settings',
|
||||
component: UserSettingsComponent,
|
||||
redirect: {name: 'user.settings.general'},
|
||||
children: [
|
||||
{
|
||||
path: '/user/settings/avatar',
|
||||
|
@ -279,14 +282,14 @@ const router = createRouter({
|
|||
path: '/lists/:listId/settings/edit',
|
||||
name: 'filter.settings.edit',
|
||||
components: {
|
||||
popup: FilterSettingEdit,
|
||||
popup: FilterEdit,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/delete',
|
||||
name: 'filter.settings.delete',
|
||||
components: {
|
||||
popup: FilterSettingDelete,
|
||||
popup: FilterDelete,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -337,12 +340,12 @@ const router = createRouter({
|
|||
{
|
||||
path: '/lists/:listId/settings/edit',
|
||||
name: 'filter.list.settings.edit',
|
||||
component: FilterSettingEdit,
|
||||
component: FilterEdit,
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/delete',
|
||||
name: 'filter.list.settings.delete',
|
||||
component: FilterSettingDelete,
|
||||
component: FilterDelete,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -389,12 +392,12 @@ const router = createRouter({
|
|||
{
|
||||
path: '/lists/:listId/settings/edit',
|
||||
name: 'filter.gantt.settings.edit',
|
||||
component: FilterSettingEdit,
|
||||
component: FilterEdit,
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/delete',
|
||||
name: 'filter.gantt.settings.delete',
|
||||
component: FilterSettingDelete,
|
||||
component: FilterDelete,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -436,12 +439,12 @@ const router = createRouter({
|
|||
{
|
||||
path: '/lists/:listId/settings/edit',
|
||||
name: 'filter.table.settings.edit',
|
||||
component: FilterSettingEdit,
|
||||
component: FilterEdit,
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/delete',
|
||||
name: 'filter.table.settings.delete',
|
||||
component: FilterSettingDelete,
|
||||
component: FilterDelete,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -488,12 +491,12 @@ const router = createRouter({
|
|||
{
|
||||
path: '/lists/:listId/settings/edit',
|
||||
name: 'filter.kanban.settings.edit',
|
||||
component: FilterSettingEdit,
|
||||
component: FilterEdit,
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/delete',
|
||||
name: 'filter.kanban.settings.delete',
|
||||
component: FilterSettingDelete,
|
||||
component: FilterDelete,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -542,7 +545,7 @@ const router = createRouter({
|
|||
path: '/filters/new',
|
||||
name: 'filters.create',
|
||||
components: {
|
||||
popup: CreateSavedFilter,
|
||||
popup: FilterNew,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -3,6 +3,9 @@ import {setLoading} from '@/store/helper'
|
|||
import {success} from '@/message'
|
||||
import {i18n} from '@/i18n'
|
||||
import {getLabelsByIds, filterLabelsByQuery} from '@/helpers/labels'
|
||||
import {createNewIndexer} from '@/indexes'
|
||||
|
||||
const {add, remove, update} = createNewIndexer('labels', ['title', 'description'])
|
||||
|
||||
async function getAllLabels(page = 1) {
|
||||
const labelService = new LabelService()
|
||||
|
@ -26,13 +29,16 @@ export default {
|
|||
setLabels(state, labels) {
|
||||
labels.forEach(l => {
|
||||
state.labels[l.id] = l
|
||||
add(l)
|
||||
})
|
||||
},
|
||||
setLabel(state, label) {
|
||||
state.labels[label.id] = label
|
||||
update(label)
|
||||
},
|
||||
removeLabelById(state, label) {
|
||||
delete state.labels[label.id]
|
||||
remove(label)
|
||||
},
|
||||
setLoaded(state, loaded) {
|
||||
state.loaded = loaded
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import ListService from '@/services/list'
|
||||
import {setLoading} from '@/store/helper'
|
||||
import {removeListFromHistory} from '@/modules/listHistory.ts'
|
||||
import {createNewIndexer} from '@/indexes'
|
||||
|
||||
const {add, remove, search, update} = createNewIndexer('lists', ['title', 'description'])
|
||||
|
||||
const FavoriteListsNamespace = -2
|
||||
|
||||
|
@ -11,14 +14,17 @@ export default {
|
|||
mutations: {
|
||||
setList(state, list) {
|
||||
state[list.id] = list
|
||||
update(list)
|
||||
},
|
||||
setLists(state, lists) {
|
||||
lists.forEach(l => {
|
||||
state[l.id] = l
|
||||
add(l)
|
||||
})
|
||||
},
|
||||
removeListById(state, list) {
|
||||
delete state[list.id]
|
||||
remove(list)
|
||||
},
|
||||
},
|
||||
getters: {
|
||||
|
@ -34,6 +40,13 @@ export default {
|
|||
})
|
||||
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: {
|
||||
toggleListFavorite(ctx, list) {
|
||||
|
@ -66,7 +79,7 @@ export default {
|
|||
await listService.update(list)
|
||||
ctx.commit('setList', list)
|
||||
ctx.commit('namespaces/setListInNamespaceById', list, {root: true})
|
||||
|
||||
|
||||
// the returned list from listService.update is the same!
|
||||
// in order to not validate vuex mutations we have to create a new copy
|
||||
const newList = {
|
||||
|
@ -81,7 +94,7 @@ export default {
|
|||
ctx.dispatch('namespaces/loadNamespacesIfFavoritesDontExist', null, {root: true})
|
||||
ctx.dispatch('namespaces/removeFavoritesNamespaceIfEmpty', null, {root: true})
|
||||
return newList
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
// Reset the list state to the initial one to avoid confusion for the user
|
||||
ctx.commit('setList', {
|
||||
...list,
|
||||
|
@ -97,13 +110,13 @@ export default {
|
|||
const cancel = setLoading(ctx, 'lists')
|
||||
const listService = new ListService()
|
||||
|
||||
try {
|
||||
try {
|
||||
const response = await listService.delete(list)
|
||||
ctx.commit('removeListById', list)
|
||||
ctx.commit('namespaces/removeListFromNamespaceById', list, {root: true})
|
||||
removeListFromHistory({id: list.id})
|
||||
return response
|
||||
} finally{
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import NamespaceService from '../../services/namespace'
|
||||
import {setLoading} from '@/store/helper'
|
||||
import {createNewIndexer} from '@/indexes'
|
||||
|
||||
const {add, remove, search, update} = createNewIndexer('namespaces', ['title', 'description'])
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
|
@ -9,6 +12,9 @@ export default {
|
|||
mutations: {
|
||||
namespaces(state, namespaces) {
|
||||
state.namespaces = namespaces
|
||||
namespaces.forEach(n => {
|
||||
add(n)
|
||||
})
|
||||
},
|
||||
setNamespaceById(state, namespace) {
|
||||
const namespaceIndex = state.namespaces.findIndex(n => n.id === namespace.id)
|
||||
|
@ -22,8 +28,9 @@ export default {
|
|||
if (!namespace.lists || namespace.lists.length === 0) {
|
||||
namespace.lists = state.namespaces[namespaceIndex].lists
|
||||
}
|
||||
|
||||
|
||||
state.namespaces[namespaceIndex] = namespace
|
||||
update(namespace)
|
||||
},
|
||||
setListInNamespaceById(state, list) {
|
||||
for (const n in state.namespaces) {
|
||||
|
@ -43,11 +50,13 @@ export default {
|
|||
},
|
||||
addNamespace(state, namespace) {
|
||||
state.namespaces.push(namespace)
|
||||
add(namespace)
|
||||
},
|
||||
removeNamespaceById(state, namespaceId) {
|
||||
for (const n in state.namespaces) {
|
||||
if (state.namespaces[n].id === namespaceId) {
|
||||
state.namespaces.splice(n, 1)
|
||||
remove(state.namespaces[n])
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -78,11 +87,11 @@ export default {
|
|||
getters: {
|
||||
getListAndNamespaceById: state => (listId, ignorePseudoNamespaces = false) => {
|
||||
for (const n in state.namespaces) {
|
||||
|
||||
if(ignorePseudoNamespaces && state.namespaces[n].id < 0) {
|
||||
|
||||
if (ignorePseudoNamespaces && state.namespaces[n].id < 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
for (const l in state.namespaces[n].lists) {
|
||||
if (state.namespaces[n].lists[l].id === listId) {
|
||||
return {
|
||||
|
@ -97,6 +106,13 @@ export default {
|
|||
getNamespaceById: state => namespaceId => {
|
||||
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: {
|
||||
async loadNamespaces(ctx) {
|
||||
|
@ -107,12 +123,12 @@ export default {
|
|||
// We always load all namespaces and filter them on the frontend
|
||||
const namespaces = await namespaceService.getAll({}, {is_archived: true})
|
||||
ctx.commit('namespaces', namespaces)
|
||||
|
||||
|
||||
// Put all lists in the list state
|
||||
const lists = namespaces.flatMap(({lists}) => lists)
|
||||
|
||||
|
||||
ctx.commit('lists/setLists', lists, {root: true})
|
||||
|
||||
|
||||
return namespaces
|
||||
} finally {
|
||||
cancel()
|
||||
|
|
|
@ -13,10 +13,10 @@
|
|||
>
|
||||
<div class="p-4">
|
||||
<p>
|
||||
{{ $t('about.frontendVersion', {version: this.frontendVersion}) }}
|
||||
{{ $t('about.frontendVersion', {version: frontendVersion}) }}
|
||||
</p>
|
||||
<p>
|
||||
{{ $t('about.apiVersion', {version: this.apiVersion}) }}
|
||||
{{ $t('about.apiVersion', {version: apiVersion}) }}
|
||||
</p>
|
||||
</div>
|
||||
<footer class="modal-card-foot is-flex is-justify-content-flex-end">
|
||||
|
@ -32,18 +32,11 @@
|
|||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {VERSION} from '../version.json'
|
||||
<script setup>
|
||||
import {computed} from 'vue'
|
||||
|
||||
export default {
|
||||
name: 'About',
|
||||
computed: {
|
||||
frontendVersion() {
|
||||
return VERSION
|
||||
},
|
||||
apiVersion() {
|
||||
return this.$store.state.config.version
|
||||
},
|
||||
},
|
||||
}
|
||||
import { store } from '@/store'
|
||||
import {VERSION as frontendVersion} from '@/version.json'
|
||||
|
||||
const apiVersion = computed(() => store.state.config.version)
|
||||
</script>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
<div class="control">
|
||||
<editor
|
||||
:key="savedFilter.id"
|
||||
v-model="savedFilter.description"
|
||||
:class="{ 'disabled': savedFilterService.loading}"
|
||||
:disabled="savedFilterService.loading"
|
||||
:preview-is-default="false"
|
||||
id="description"
|
||||
:placeholder="$t('filters.attributes.descriptionPlaceholder')"
|
||||
v-if="editorActive"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="filters">{{ $t('filters.title') }}</label>
|
||||
<div class="control">
|
||||
<filters
|
||||
<Filters
|
||||
:class="{ 'disabled': savedFilterService.loading}"
|
||||
:disabled="savedFilterService.loading"
|
||||
class="has-no-shadow has-no-border"
|
||||
|
@ -59,46 +59,30 @@
|
|||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AsyncEditor from '@/components/input/AsyncEditor'
|
||||
<script setup>
|
||||
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 SavedFilterService from '@/services/savedFilter'
|
||||
import SavedFilterModel from '@/models/savedFilter'
|
||||
|
||||
export default {
|
||||
name: 'CreateSavedFilter',
|
||||
data() {
|
||||
return {
|
||||
editorActive: false,
|
||||
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,
|
||||
},
|
||||
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()}})
|
||||
},
|
||||
},
|
||||
const savedFilterService = shallowRef(new SavedFilterService())
|
||||
|
||||
const savedFilter = ref(new SavedFilterModel())
|
||||
const filters = computed({
|
||||
get: () => savedFilter.value.filters,
|
||||
set: (value) => (savedFilter.value.filters = value),
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
async function create() {
|
||||
savedFilter.value = await savedFilterService.value.create(savedFilter.value)
|
||||
await store.dispatch('namespaces/loadNamespaces')
|
||||
router.push({name: 'list.index', params: {listId: savedFilter.value.getListId()}})
|
||||
}
|
||||
</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">
|
||||
<span v-for="i in progressDotsCount" :key="i" />
|
||||
</div>
|
||||
<Logo alt="Vikunja" />
|
||||
<Logo/>
|
||||
</div>
|
||||
<p>{{ $t('migrate.inProgress') }}</p>
|
||||
</div>
|
||||
|
@ -186,9 +186,10 @@ export default {
|
|||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
|
||||
img {
|
||||
img, svg {
|
||||
display: block;
|
||||
max-height: 100px;
|
||||
max-width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -35,36 +35,25 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DataExportService from '../../services/dataExport'
|
||||
<script setup>
|
||||
import {ref, computed, reactive} from 'vue'
|
||||
import DataExportService from '@/services/dataExport'
|
||||
import {store} from '@/store'
|
||||
|
||||
export default {
|
||||
name: 'data-export-download',
|
||||
data() {
|
||||
return {
|
||||
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
|
||||
}
|
||||
const dataExportService = reactive(new DataExportService())
|
||||
const password = ref('')
|
||||
const errPasswordRequired = ref(false)
|
||||
const passwordInput = ref(null)
|
||||
|
||||
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>
|
||||
|
|
|
@ -60,55 +60,49 @@
|
|||
{{ $t('user.auth.login') }}
|
||||
</x-button>
|
||||
</div>
|
||||
<legal/>
|
||||
<Legal />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PasswordResetModel from '../../models/passwordReset'
|
||||
import PasswordResetService from '../../services/passwordReset'
|
||||
import Legal from '../../components/misc/legal'
|
||||
<script setup>
|
||||
import {ref, reactive} from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Legal,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
passwordResetService: new PasswordResetService(),
|
||||
credentials: {
|
||||
password: '',
|
||||
password2: '',
|
||||
},
|
||||
errorMsg: '',
|
||||
successMessage: '',
|
||||
}
|
||||
},
|
||||
import Legal from '@/components/misc/legal'
|
||||
|
||||
mounted() {
|
||||
this.setTitle(this.$t('user.auth.resetPassword'))
|
||||
},
|
||||
import PasswordResetModel from '@/models/passwordReset'
|
||||
import PasswordResetService from '@/services/passwordReset'
|
||||
import { useTitle } from '@/composables/useTitle'
|
||||
|
||||
methods: {
|
||||
async submit() {
|
||||
this.errorMsg = ''
|
||||
const { t } = useI18n()
|
||||
useTitle(() => t('user.auth.resetPassword'))
|
||||
|
||||
if (this.credentials.password2 !== this.credentials.password) {
|
||||
this.errorMsg = this.$t('user.auth.passwordsDontMatch')
|
||||
return
|
||||
}
|
||||
const credentials = reactive({
|
||||
password: '',
|
||||
password2: '',
|
||||
})
|
||||
|
||||
let passwordReset = new PasswordResetModel({newPassword: this.credentials.password})
|
||||
try {
|
||||
const { message } = this.passwordResetService.resetPassword(passwordReset)
|
||||
this.successMessage = message
|
||||
localStorage.removeItem('passwordResetToken')
|
||||
} catch(e) {
|
||||
this.errorMsg = e.response.data.message
|
||||
}
|
||||
},
|
||||
},
|
||||
const passwordResetService = reactive(new PasswordResetService())
|
||||
const errorMsg = ref('')
|
||||
const successMessage = ref('')
|
||||
|
||||
async function submit() {
|
||||
errorMsg.value = ''
|
||||
|
||||
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>
|
||||
|
||||
|
|
|
@ -36,12 +36,12 @@
|
|||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<input
|
||||
class="input"
|
||||
id="password1"
|
||||
name="password1"
|
||||
id="password"
|
||||
name="password"
|
||||
:placeholder="$t('user.auth.passwordPlaceholder')"
|
||||
required
|
||||
type="password"
|
||||
|
@ -52,17 +52,17 @@
|
|||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<input
|
||||
class="input"
|
||||
id="password2"
|
||||
name="password2"
|
||||
id="passwordValidation"
|
||||
name="passwordValidation"
|
||||
:placeholder="$t('user.auth.passwordPlaceholder')"
|
||||
required
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
v-model="credentials.password2"
|
||||
v-model="passwordValidation"
|
||||
@keyup.enter="submit"
|
||||
/>
|
||||
</div>
|
||||
|
@ -95,61 +95,50 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import router from '../../router'
|
||||
import {mapState} from 'vuex'
|
||||
import {LOADING} from '@/store/mutation-types'
|
||||
import Legal from '../../components/misc/legal'
|
||||
<script setup>
|
||||
import {ref, reactive, toRaw, computed, onBeforeMount} from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Legal,
|
||||
},
|
||||
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 = ''
|
||||
import router from '@/router'
|
||||
import { store } from '@/store'
|
||||
import { useTitle } from '@/composables/useTitle'
|
||||
|
||||
if (this.credentials.password2 !== this.credentials.password) {
|
||||
this.errorMessage = this.$t('user.auth.passwordsDontMatch')
|
||||
return
|
||||
}
|
||||
import Legal from '@/components/misc/legal'
|
||||
|
||||
const credentials = {
|
||||
username: this.credentials.username,
|
||||
email: this.credentials.email,
|
||||
password: this.credentials.password,
|
||||
}
|
||||
// FIXME: use the `beforeEnter` hook of vue-router
|
||||
// Check if the user is already logged in, if so, redirect them to the homepage
|
||||
onBeforeMount(() => {
|
||||
if (store.state.auth.authenticated) {
|
||||
router.push({name: 'home'})
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
await this.$store.dispatch('auth/register', credentials)
|
||||
} catch(e) {
|
||||
this.errorMessage = e.message
|
||||
}
|
||||
},
|
||||
},
|
||||
const { t } = useI18n()
|
||||
useTitle(() => t('user.auth.register'))
|
||||
|
||||
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>
|
||||
|
|
|
@ -43,42 +43,38 @@
|
|||
{{ $t('user.auth.login') }}
|
||||
</x-button>
|
||||
</div>
|
||||
<legal/>
|
||||
<Legal />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PasswordResetModel from '../../models/passwordReset'
|
||||
import PasswordResetService from '../../services/passwordReset'
|
||||
import Legal from '../../components/misc/legal'
|
||||
<script setup>
|
||||
import {ref, reactive} from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Legal,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
passwordResetService: new PasswordResetService(),
|
||||
passwordReset: new PasswordResetModel(),
|
||||
errorMsg: '',
|
||||
isSuccess: false,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.setTitle(this.$t('user.auth.resetPassword'))
|
||||
},
|
||||
methods: {
|
||||
async submit() {
|
||||
this.errorMsg = ''
|
||||
try {
|
||||
await this.passwordResetService.requestResetPassword(this.passwordReset)
|
||||
this.isSuccess = true
|
||||
} catch(e) {
|
||||
this.errorMsg = e.response.data.message
|
||||
}
|
||||
},
|
||||
},
|
||||
import Legal from '@/components/misc/legal'
|
||||
|
||||
import PasswordResetModel from '@/models/passwordReset'
|
||||
import PasswordResetService from '@/services/passwordReset'
|
||||
import { useTitle } from '@/composables/useTitle'
|
||||
|
||||
const { t } = useI18n()
|
||||
useTitle(() => t('user.auth.resetPassword'))
|
||||
|
||||
// Not sure if this instance needs a shalloRef at all
|
||||
const passwordResetService = reactive(new PasswordResetService())
|
||||
const passwordReset = ref(new PasswordResetModel())
|
||||
const errorMsg = ref('')
|
||||
const isSuccess = ref(false)
|
||||
|
||||
async function submit() {
|
||||
errorMsg.value = ''
|
||||
try {
|
||||
await passwordResetService.requestResetPassword(passwordReset.value)
|
||||
isSuccess.value = true
|
||||
} catch(e) {
|
||||
errorMsg.value = e.response.data.message
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -57,24 +57,19 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapState} from 'vuex'
|
||||
<script setup>
|
||||
import {computed} from 'vue'
|
||||
import { store } from '@/store'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useTitle } from '@/composables/useTitle'
|
||||
|
||||
export default {
|
||||
name: 'Settings',
|
||||
mounted() {
|
||||
this.setTitle(this.$t('user.settings.title'))
|
||||
},
|
||||
computed: {
|
||||
...mapState('config', ['totpEnabled', 'caldavEnabled']),
|
||||
migratorsEnabled() {
|
||||
return this.$store.getters['config/migratorsEnabled']
|
||||
},
|
||||
isLocalUser() {
|
||||
return this.$store.state.auth.info?.isLocalUser
|
||||
},
|
||||
},
|
||||
}
|
||||
const { t } = useI18n()
|
||||
useTitle(() => t('user.settings.title'))
|
||||
|
||||
const totpEnabled = computed(() => store.state.config.totpEnabled)
|
||||
const caldavEnabled = computed(() => store.state.config.caldavEnabled)
|
||||
const migratorsEnabled = computed(() => store.getters['config/migratorsEnabled'])
|
||||
const isLocalUser = computed(() => store.state.auth.info?.isLocalUser)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
Reference in New Issue