Make add task a component #520
|
@ -95,7 +95,7 @@ steps:
|
|||
CYPRESS_TEST_SECRET: averyLongSecretToSe33dtheDB
|
||||
YARN_CACHE_FOLDER: .cache/yarn/
|
||||
CYPRESS_CACHE_FOLDER: .cache/cypress/
|
||||
CYPRESS_DEFAULT_COMMAND_TIMEOUT: 20000
|
||||
CYPRESS_DEFAULT_COMMAND_TIMEOUT: 60000
|
||||
commands:
|
||||
- sed -i 's/localhost/api/g' public/index.html
|
||||
- yarn serve & npx wait-on http://localhost:8080
|
||||
|
|
22
.editorconfig
Normal file
|
@ -0,0 +1,22 @@
|
|||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = tab
|
||||
end_of_line = lf
|
||||
sytone marked this conversation as resolved
Outdated
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = false
|
||||
insert_final_newline = false
|
||||
|
||||
[*.vue]
|
||||
indent_style = tab
|
||||
|
||||
[*.{yaml,yml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.json]
|
||||
indent_style = space
|
||||
indent_size = 2
|
12
README.md
|
@ -20,21 +20,25 @@ If you find any security-related issues you don't want to disclose publicly, ple
|
|||
There is a [docker image available](https://hub.docker.com/r/vikunja/api) with support for http/2 and aggressive caching enabled.
|
||||
|
||||
## Project setup
|
||||
```
|
||||
|
||||
```shell
|
||||
yarn install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
|
||||
```shell
|
||||
yarn run serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
|
||||
```shell
|
||||
yarn run build
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
|
||||
```shell
|
||||
yarn run lint
|
||||
```
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
module.exports = {
|
||||
presets: [
|
||||
'@vue/app'
|
||||
]
|
||||
'@vue/app',
|
||||
],
|
||||
}
|
||||
|
|
|
@ -36,7 +36,6 @@ describe('User Settings', () => {
|
|||
.contains('Save')
|
||||
.click()
|
||||
|
||||
cy.wait(3000) // Wait for the request to finish
|
||||
cy.get('.global-notification')
|
||||
.should('contain', 'Success')
|
||||
cy.get('.navbar .user .username')
|
||||
|
|
21
package.json
|
@ -69,7 +69,24 @@
|
|||
"plugin:vue/essential",
|
||||
"eslint:recommended"
|
||||
],
|
||||
"rules": {},
|
||||
"rules": {
|
||||
"vue/html-quotes": [
|
||||
"error",
|
||||
"double"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
],
|
||||
"comma-dangle": [
|
||||
"error",
|
||||
"always-multiline"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"never"
|
||||
]
|
||||
},
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint"
|
||||
},
|
||||
|
@ -95,4 +112,4 @@
|
|||
],
|
||||
"testEnvironment": "jsdom"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -55,7 +55,7 @@ export default {
|
|||
computed: {
|
||||
showIconOnly() {
|
||||
return this.icon !== '' && typeof this.$slots.default === 'undefined'
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
click(e) {
|
||||
|
|
|
@ -137,18 +137,18 @@ export default {
|
|||
},
|
||||
props: {
|
||||
value: {
|
||||
validator: prop => prop instanceof Date || prop === null || typeof prop === 'string'
|
||||
validator: prop => prop instanceof Date || prop === null || typeof prop === 'string',
|
||||
},
|
||||
chooseDateLabel: {
|
||||
type: String,
|
||||
default() {
|
||||
return this.$t('input.datepicker.chooseDate')
|
||||
}
|
||||
},
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setDateValue(this.value)
|
||||
|
|
|
@ -366,7 +366,7 @@ export default {
|
|||
link: (href, title, text) => {
|
||||
const isLocal = href.startsWith(`${location.protocol}//${location.hostname}`)
|
||||
const html = linkRenderer.call(renderer, href, title, text)
|
||||
return isLocal ? html : html.replace(/^<a /, `<a target="_blank" rel="noreferrer noopener nofollow" `)
|
||||
return isLocal ? html : html.replace(/^<a /, '<a target="_blank" rel="noreferrer noopener nofollow" ')
|
||||
},
|
||||
},
|
||||
highlight: function (code, language) {
|
||||
|
|
|
@ -108,21 +108,21 @@ export default {
|
|||
type: Boolean,
|
||||
default() {
|
||||
return false
|
||||
}
|
||||
},
|
||||
},
|
||||
// The placeholder of the search input
|
||||
placeholder: {
|
||||
type: String,
|
||||
default() {
|
||||
return ''
|
||||
}
|
||||
},
|
||||
},
|
||||
// The search results where the @search listener needs to put the results into
|
||||
searchResults: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
}
|
||||
},
|
||||
},
|
||||
// The name of the property of the searched object to show the user.
|
||||
// If empty the component will show all raw data of an entry.
|
||||
|
@ -130,13 +130,13 @@ export default {
|
|||
type: String,
|
||||
default() {
|
||||
return ''
|
||||
}
|
||||
},
|
||||
},
|
||||
// The object with the value, updated every time an entry is selected.
|
||||
value: {
|
||||
default() {
|
||||
return null
|
||||
}
|
||||
},
|
||||
},
|
||||
// If true, will provide an "add this as a new value" entry which fires an @create event when clicking on it.
|
||||
creatable: {
|
||||
|
@ -150,14 +150,14 @@ export default {
|
|||
type: String,
|
||||
default() {
|
||||
return this.$t('input.multiselect.createPlaceholder')
|
||||
}
|
||||
},
|
||||
},
|
||||
// The text shown next to an option.
|
||||
selectPlaceholder: {
|
||||
type: String,
|
||||
default() {
|
||||
return this.$t('input.multiselect.selectPlaceholder')
|
||||
}
|
||||
},
|
||||
},
|
||||
// If true, allows for selecting multiple items. v-model will be an array with all selected values in that case.
|
||||
multiple: {
|
||||
|
|
|
@ -22,7 +22,7 @@ export default {
|
|||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -6,6 +6,6 @@
|
|||
|
||||
<script>
|
||||
export default {
|
||||
name: 'nothing'
|
||||
name: 'nothing',
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -14,7 +14,7 @@ export default {
|
|||
keys: {
|
||||
type: Array,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -57,7 +57,7 @@ export default {
|
|||
if (this.disabled) {
|
||||
return this.$t('task.subscription.subscribedThroughParent', {
|
||||
entity: this.entity,
|
||||
parent: this.subscription.entity
|
||||
parent: this.subscription.entity,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -118,7 +118,7 @@ export default {
|
|||
.catch(e => {
|
||||
this.error(e)
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -481,7 +481,7 @@ export default {
|
|||
reset() {
|
||||
this.query = ''
|
||||
this.selectedCmd = null
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -235,11 +235,11 @@ export default {
|
|||
this.searchLabel = 'username'
|
||||
|
||||
if (this.type === 'list') {
|
||||
this.typeString = `list`
|
||||
this.typeString = 'list'
|
||||
this.stuffService = new UserListService()
|
||||
this.stuffModel = new UserListModel({listId: this.id})
|
||||
} else if (this.type === 'namespace') {
|
||||
this.typeString = `namespace`
|
||||
this.typeString = 'namespace'
|
||||
this.stuffService = new UserNamespaceService()
|
||||
this.stuffModel = new UserNamespaceModel({
|
||||
namespaceId: this.id,
|
||||
|
@ -253,11 +253,11 @@ export default {
|
|||
this.searchLabel = 'name'
|
||||
|
||||
if (this.type === 'list') {
|
||||
this.typeString = `list`
|
||||
this.typeString = 'list'
|
||||
this.stuffService = new TeamListService()
|
||||
this.stuffModel = new TeamListModel({listId: this.id})
|
||||
} else if (this.type === 'namespace') {
|
||||
this.typeString = `namespace`
|
||||
this.typeString = 'namespace'
|
||||
this.stuffService = new TeamNamespaceService()
|
||||
this.stuffModel = new TeamNamespaceModel({
|
||||
namespaceId: this.id,
|
||||
|
@ -278,7 +278,7 @@ export default {
|
|||
.then((r) => {
|
||||
this.$set(this, 'sharables', r)
|
||||
r.forEach((s) =>
|
||||
this.$set(this.selectedRight, s.id, s.right)
|
||||
this.$set(this.selectedRight, s.id, s.right),
|
||||
)
|
||||
})
|
||||
.catch((e) => {
|
||||
|
|
102
src/components/tasks/add-task.vue
Normal file
|
@ -0,0 +1,102 @@
|
|||
<template>
|
||||
<div class="task-add">
|
||||
sytone marked this conversation as resolved
Outdated
konrad
commented
The validation error should be in this component as well: https://kolaente.dev/vikunja/frontend/src/branch/main/src/views/list/views/List.vue#L82-L84 The validation error should be in this component as well: https://kolaente.dev/vikunja/frontend/src/branch/main/src/views/list/views/List.vue#L82-L84
sytone
commented
Added and will verify. Added and will verify.
|
||||
<div class="field is-grouped">
|
||||
<p :class="{ 'is-loading': taskService.loading}" class="control has-icons-left is-expanded">
|
||||
<input
|
||||
:class="{ 'disabled': taskService.loading}"
|
||||
@keyup.enter="addTask()"
|
||||
class="input"
|
||||
:placeholder="$t('list.list.addPlaceholder')"
|
||||
type="text"
|
||||
v-focus
|
||||
v-model="newTaskTitle"
|
||||
ref="newTaskInput"
|
||||
@keyup="errorMessage = ''"
|
||||
/>
|
||||
<span class="icon is-small is-left">
|
||||
<icon icon="tasks"/>
|
||||
</span>
|
||||
</p>
|
||||
<p class="control">
|
||||
<x-button
|
||||
:disabled="newTaskTitle.length === 0"
|
||||
@click="addTask()"
|
||||
icon="plus"
|
||||
>
|
||||
{{ $t('list.list.add') }}
|
||||
</x-button>
|
||||
</p>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errorMessage !== ''">
|
||||
sytone marked this conversation as resolved
Outdated
konrad
commented
This will never be shown because the whole component only appears if This will never be shown because the whole component only appears if `validListIdAvailable` is true. And if that's the case, this `v-if` can never evaluate to true.
|
||||
{{ errorMessage }}
|
||||
</p>
|
||||
sytone marked this conversation as resolved
Outdated
konrad
commented
Validation error for an empty title still missing: https://kolaente.dev/vikunja/frontend/src/branch/main/src/views/list/views/List.vue#L82-L84 Validation error for an empty title still missing: https://kolaente.dev/vikunja/frontend/src/branch/main/src/views/list/views/List.vue#L82-L84
sytone
commented
Not sure what you mean by title. I have updated the component to have a div control the input vs warning message. Look in current commit. Not sure what you mean by title. I have updated the component to have a div control the input vs warning message. Look in current commit.
konrad
commented
The user has to enter a title for a new task. If none is provided they should see an error message as they currently do. Please correct me if I'm wrong, but I did not find that here in this new component. The user has to enter a title for a new task. If none is provided they should see an error message as they currently do. Please correct me if I'm wrong, but I did not find that here in this new component.
|
||||
<quick-add-magic v-if="errorMessage === ''"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListService from '../../services/list'
|
||||
sytone marked this conversation as resolved
Outdated
konrad
commented
If the component is not used, please remove the import. If the component is not used, please remove the import.
|
||||
import TaskService from '../../services/task'
|
||||
import LabelService from '../../services/label'
|
||||
import LabelTaskService from '../../services/labelTask'
|
||||
sytone marked this conversation as resolved
Outdated
konrad
commented
Please remove unused imports. Please remove unused imports.
sytone
commented
Done Done
|
||||
import createTask from '@/components/tasks/mixins/createTask'
|
||||
import QuickAddMagic from '@/components/tasks/partials/quick-add-magic'
|
||||
|
||||
export default {
|
||||
name: 'add-task',
|
||||
data() {
|
||||
return {
|
||||
newTaskTitle: '',
|
||||
listService: ListService,
|
||||
taskService: TaskService,
|
||||
labelService: LabelService,
|
||||
labelTaskService: LabelTaskService,
|
||||
errorMessage: '',
|
||||
}
|
||||
},
|
||||
mixins: [
|
||||
createTask,
|
||||
],
|
||||
components: {
|
||||
QuickAddMagic,
|
||||
},
|
||||
sytone marked this conversation as resolved
Outdated
konrad
commented
Please remove the empty block. Please remove the empty block.
|
||||
created() {
|
||||
this.listService = new ListService()
|
||||
this.taskService = new TaskService()
|
||||
this.labelService = new LabelService()
|
||||
this.labelTaskService = new LabelTaskService()
|
||||
},
|
||||
methods: {
|
||||
addTask() {
|
||||
if (this.newTaskTitle === '') {
|
||||
this.errorMessage = this.$t('list.create.addTitleRequired')
|
||||
return
|
||||
}
|
||||
this.errorMessage = ''
|
||||
sytone marked this conversation as resolved
Outdated
konrad
commented
Could you move these variables declared inside this block into computed properties? Could you move these variables declared inside this block into computed properties?
sytone
commented
I was following the convention where the services were set in the created function. Can you point me to an example of how to do it in another component so I Can understand more. I was following the convention where the services were set in the created function. Can you point me to an example of how to do it in another component so I Can understand more.
konrad
commented
Oh I didn't mean the services, I mean the Check out the Vue docs about computed properties and an example of using a computed property. You'd define one computed property for each variable and then place the code inside them, something like this:
Oh I didn't mean the services, I mean the `listIdForNewTask` and `validListIdAvailable` variables declared in `beforeMount()`.
Check out the [Vue docs about computed properties](https://vuejs.org/v2/guide/computed.html#Computed-Properties) and [an example of using a computed property](https://kolaente.dev/vikunja/frontend/src/branch/main/src/components/quick-actions/quick-actions.vue#L100-L106).
You'd define one computed property for each variable and then place the code inside them, something like this:
```js
listIdForNewTask() {
if (this.listId !== undefined) {
return this.listId
}
if(this.$store.state.auth.settings.defaultListId !== undefined) {
return this.$store.state.auth.settings.defaultListId
}
return null // or undefined
},
validListIdAvailable() {
return this.listIdForNewTask !== undefined
}
```
|
||||
|
||||
this.createNewTask(this.newTaskTitle, 0, this.$store.state.auth.settings.defaultListId)
|
||||
.then(task => {
|
||||
sytone marked this conversation as resolved
Outdated
konrad
commented
This should be This should be `const` instead of `let`.
|
||||
this.newTaskTitle = ''
|
||||
this.$emit('taskAdded', task)
|
||||
sytone marked this conversation as resolved
Outdated
konrad
commented
Needs a Needs a `,` at the end.
|
||||
})
|
||||
.catch(e => {
|
||||
if (e === 'NO_LIST') {
|
||||
this.errorMessage = this.$t('list.create.addListRequired')
|
||||
sytone marked this conversation as resolved
Outdated
konrad
commented
That's something that would be required to get this merged. I'd like to make that a setting at the api level though. Another approach would be to only merge the refactoring of the component and add the rest (=adding a task from the home page) later, once the default list is implemented. That's something that would be required to get this merged. I'd like to make that a setting at the api level though.
Another approach would be to only merge the refactoring of the component and add the rest (=adding a task from the home page) later, once the default list is implemented.
sytone
commented
The API is completed now and I update updated the code to use the settings. The API is completed now and I update updated the code to use the settings.
|
||||
return
|
||||
}
|
||||
this.error(e)
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
sytone marked this conversation as resolved
Outdated
konrad
commented
If it's not used, please remove it. If it's not used, please remove it.
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.task-add {
|
||||
margin-bottom: 0;
|
||||
|
||||
.button {
|
||||
height: 2.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -388,7 +388,7 @@ export default {
|
|||
|
||||
let startDate = new Date(this.startDate)
|
||||
startDate.setDate(
|
||||
startDate.getDate() + newRect.left / this.dayWidth
|
||||
startDate.getDate() + newRect.left / this.dayWidth,
|
||||
)
|
||||
startDate.setUTCHours(0)
|
||||
startDate.setUTCMinutes(0)
|
||||
|
@ -397,7 +397,7 @@ export default {
|
|||
this.taskDragged.startDate = startDate
|
||||
let endDate = new Date(startDate)
|
||||
endDate.setDate(
|
||||
startDate.getDate() + newRect.width / this.dayWidth
|
||||
startDate.getDate() + newRect.width / this.dayWidth,
|
||||
)
|
||||
this.taskDragged.startDate = startDate
|
||||
this.taskDragged.endDate = endDate
|
||||
|
@ -440,7 +440,7 @@ export default {
|
|||
this.$set(
|
||||
this.theTasks,
|
||||
tt,
|
||||
this.addGantAttributes(r)
|
||||
this.addGantAttributes(r),
|
||||
)
|
||||
break
|
||||
}
|
||||
|
|
|
@ -26,6 +26,11 @@ export default {
|
|||
const parsedTask = parseTaskText(newTaskTitle)
|
||||
const assignees = []
|
||||
|
||||
// Uses the following ways to get the list id of the new task:
|
||||
// 1. If specified in quick add magic, look in store if it exists and use it if it does
|
||||
// 2. Else check if a list was passed as parameter
|
||||
// 3. Otherwise use the id from the route parameter
|
||||
// 4. If none of the above worked, reject the promise with an error.
|
||||
let listId = null
|
||||
if (parsedTask.list !== null) {
|
||||
const list = this.$store.getters['lists/findListByExactname'](parsedTask.list)
|
||||
|
@ -34,7 +39,11 @@ export default {
|
|||
if (listId === null) {
|
||||
listId = lId !== 0 ? lId : this.$route.params.listId
|
||||
}
|
||||
|
||||
|
||||
if (typeof listId === 'undefined' || listId === 0) {
|
||||
return Promise.reject('NO_LIST')
|
||||
}
|
||||
|
||||
// Separate closure because we need to wait for the results of the user search if users were entered in the
|
||||
// task create request. Because _that_ happens in a promise, we'll need something to call when it resolves.
|
||||
const createTask = () => {
|
||||
|
@ -83,7 +92,7 @@ export default {
|
|||
.then(res => {
|
||||
return addLabelToTask(res)
|
||||
})
|
||||
.catch(e => Promise.reject(e))
|
||||
.catch(e => Promise.reject(e)),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
@ -110,7 +119,7 @@ export default {
|
|||
assignees.push(user)
|
||||
}
|
||||
return Promise.resolve(users)
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
@ -229,7 +229,7 @@ export default {
|
|||
.then((r) => {
|
||||
this.$store.commit(
|
||||
'attachments/removeById',
|
||||
this.attachmentToDelete.id
|
||||
this.attachmentToDelete.id,
|
||||
)
|
||||
this.success(r)
|
||||
})
|
||||
|
|
|
@ -91,7 +91,7 @@ export default {
|
|||
.finally(() => {
|
||||
this.saving = false
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -95,7 +95,7 @@ export default {
|
|||
.finally(() => {
|
||||
this.saving = false
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -32,6 +32,11 @@ export default {
|
|||
foundLists: [],
|
||||
}
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
konrad marked this conversation as resolved
Outdated
konrad
commented
I think this should either expose the internal list or (better) use the v-model prop to populate the selected list. That one is bidirectional. I think this should either expose the internal list or (better) [use the v-model](https://www.digitalocean.com/community/tutorials/how-to-add-v-model-support-to-custom-vue-js-components) prop to populate the selected list. That one is bidirectional.
|
||||
components: {
|
||||
Multiselect,
|
||||
},
|
||||
|
@ -39,6 +44,14 @@ export default {
|
|||
this.listSerivce = new ListService()
|
||||
this.list = new ListModel()
|
||||
sytone marked this conversation as resolved
Outdated
konrad
commented
Good catch! Good catch!
|
||||
},
|
||||
watch: {
|
||||
value(newVal) {
|
||||
this.list = newVal
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.list = this.value
|
||||
},
|
||||
methods: {
|
||||
findLists(query) {
|
||||
if (query === '') {
|
||||
|
@ -58,7 +71,9 @@ export default {
|
|||
this.$set(this, 'foundLists', [])
|
||||
},
|
||||
select(list) {
|
||||
this.list = list
|
||||
this.$emit('selected', list)
|
||||
this.$emit('input', list)
|
||||
},
|
||||
namespace(namespaceId) {
|
||||
const namespace = this.$store.getters['namespaces/getNamespaceById'](namespaceId)
|
||||
|
|
|
@ -135,7 +135,7 @@ export default {
|
|||
showListColor: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
theTask(newVal) {
|
||||
|
@ -178,13 +178,13 @@ export default {
|
|||
this.success({
|
||||
message: this.task.done ?
|
||||
this.$t('task.doneSuccess') :
|
||||
this.$t('task.undoneSuccess')
|
||||
this.$t('task.undoneSuccess'),
|
||||
}, [{
|
||||
title: 'Undo',
|
||||
callback: () => {
|
||||
this.task.done = !this.task.done
|
||||
this.markAsDone(!checked)
|
||||
}
|
||||
},
|
||||
}])
|
||||
})
|
||||
.catch(e => {
|
||||
|
|
|
@ -12,7 +12,7 @@ export const createDateFromString = dateString => {
|
|||
}
|
||||
|
||||
if (dateString.includes('-')) {
|
||||
dateString = dateString.replace(/-/g, "/")
|
||||
dateString = dateString.replace(/-/g, '/')
|
||||
}
|
||||
|
||||
return new Date(dateString)
|
||||
|
|
|
@ -65,7 +65,8 @@
|
|||
"weekStart": "Week starts on",
|
||||
"weekStartSunday": "Sunday",
|
||||
"weekStartMonday": "Monday",
|
||||
"language": "Language"
|
||||
"language": "Language",
|
||||
"defaultList": "Default List"
|
||||
},
|
||||
"totp": {
|
||||
"title": "Two Factor Authentication",
|
||||
|
@ -109,7 +110,8 @@
|
|||
"header": "Create a new list",
|
||||
"titlePlaceholder": "The list's title goes here…",
|
||||
"addTitleRequired": "Please specify a title.",
|
||||
"createdSuccess": "The list was successfully created."
|
||||
"createdSuccess": "The list was successfully created.",
|
||||
"addListRequired": "Please specify a list or set a default list in the settings."
|
||||
},
|
||||
"archive": {
|
||||
"title": "Archive \"{list}\"",
|
||||
|
@ -204,7 +206,6 @@
|
|||
"title": "List",
|
||||
"add": "Add",
|
||||
"addPlaceholder": "Add a new task…",
|
||||
"addTitleRequired": "Please specify a title.",
|
||||
"empty": "This list is currently empty.",
|
||||
"newTaskCta": "Create a new task.",
|
||||
"editTask": "Edit Task"
|
||||
|
|
|
@ -9,6 +9,7 @@ export default class UserSettingsModel extends AbstractModel {
|
|||
discoverableByName: false,
|
||||
discoverableByEmail: false,
|
||||
overdueTasksRemindersEnabled: true,
|
||||
defaultListId: undefined,
|
||||
sytone marked this conversation as resolved
Outdated
konrad
commented
That should be That should be `0` - you don't know if the current user has access to that list.
|
||||
weekStart: 0,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -171,7 +171,7 @@ export default new Router({
|
|||
name: 'list.create',
|
||||
components: {
|
||||
popup: NewListComponent,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/namespaces/:id/settings/edit',
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {HTTPFactory} from '@/http-common'
|
||||
import {ERROR_MESSAGE, LOADING} from '../mutation-types'
|
||||
import { HTTPFactory } from '@/http-common'
|
||||
import { ERROR_MESSAGE, LOADING } from '../mutation-types'
|
||||
import UserModel from '../../models/user'
|
||||
import {getToken, refreshToken, removeToken, saveToken} from '@/helpers/auth'
|
||||
|
||||
|
@ -58,7 +58,7 @@ export default {
|
|||
// Logs a user in with a set of credentials.
|
||||
login(ctx, credentials) {
|
||||
const HTTP = HTTPFactory()
|
||||
ctx.commit(LOADING, true, {root: true})
|
||||
ctx.commit(LOADING, true, { root: true })
|
||||
|
||||
// Delete an eventually preexisting old token
|
||||
removeToken()
|
||||
|
@ -93,7 +93,7 @@ export default {
|
|||
return Promise.reject(e)
|
||||
})
|
||||
.finally(() => {
|
||||
ctx.commit(LOADING, false, {root: true})
|
||||
ctx.commit(LOADING, false, { root: true })
|
||||
})
|
||||
},
|
||||
// Registers a new user and logs them in.
|
||||
|
@ -110,18 +110,18 @@ export default {
|
|||
})
|
||||
.catch(e => {
|
||||
if (e.response && e.response.data && e.response.data.message) {
|
||||
ctx.commit(ERROR_MESSAGE, e.response.data.message, {root: true})
|
||||
ctx.commit(ERROR_MESSAGE, e.response.data.message, { root: true })
|
||||
}
|
||||
|
||||
return Promise.reject(e)
|
||||
})
|
||||
.finally(() => {
|
||||
ctx.commit(LOADING, false, {root: true})
|
||||
ctx.commit(LOADING, false, { root: true })
|
||||
})
|
||||
},
|
||||
openIdAuth(ctx, {provider, code}) {
|
||||
openIdAuth(ctx, { provider, code }) {
|
||||
const HTTP = HTTPFactory()
|
||||
ctx.commit(LOADING, true, {root: true})
|
||||
ctx.commit(LOADING, true, { root: true })
|
||||
|
||||
const data = {
|
||||
code: code,
|
||||
|
@ -143,10 +143,10 @@ export default {
|
|||
return Promise.reject(e)
|
||||
})
|
||||
.finally(() => {
|
||||
ctx.commit(LOADING, false, {root: true})
|
||||
ctx.commit(LOADING, false, { root: true })
|
||||
})
|
||||
},
|
||||
linkShareAuth(ctx, {hash, password}) {
|
||||
linkShareAuth(ctx, { hash, password }) {
|
||||
const HTTP = HTTPFactory()
|
||||
return HTTP.post('/shares/' + hash + '/auth', {
|
||||
password: password,
|
||||
|
|
|
@ -10,13 +10,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
.task-add {
|
||||
padding: 1rem 1rem 0;
|
||||
margin-bottom: 0;
|
||||
|
||||
.button {
|
||||
height: 40px;
|
||||
}
|
||||
.list-view .task-add {
|
||||
padding: 1rem 1rem 0;
|
||||
}
|
||||
|
||||
.list-title {
|
||||
|
|
|
@ -3,10 +3,15 @@
|
|||
<h2>
|
||||
{{ $t(`home.welcome${welcome}`, {username: userInfo.name !== '' ? userInfo.name : userInfo.username}) }}!
|
||||
sytone marked this conversation as resolved
Outdated
konrad
commented
Would you mind moving this to a separate PR? Would you mind moving this to a separate PR?
sytone
commented
np, reverted that change for this PR. np, reverted that change for this PR.
|
||||
</h2>
|
||||
<add-task
|
||||
konrad marked this conversation as resolved
Outdated
konrad
commented
I suppose that's just left from debugging? Maybe check out the vue devtools extension, it is great to look into the values of props and data in a component. I suppose that's just left from debugging?
Maybe check out the vue devtools extension, it is great to look into the values of props and data in a component.
sytone
commented
yea, was from merge and should not be there. I am using the tools now. yea, was from merge and should not be there. I am using the tools now.
|
||||
:listId="defaultListId"
|
||||
@taskAdded="updateTaskList"
|
||||
class="is-max-width-desktop"
|
||||
/>
|
||||
<template v-if="!hasTasks">
|
||||
<p>{{ $t('home.list.newText') }}</p>
|
||||
<x-button
|
||||
:to="{name: 'list.create', params: { id: defaultNamespaceId }}"
|
||||
:to="{ name: 'list.create', params: { id: defaultNamespaceId } }"
|
||||
:shadow="false"
|
||||
class="ml-2"
|
||||
v-if="defaultNamespaceId > 0"
|
||||
|
@ -34,7 +39,7 @@
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ShowTasks :show-all="true" v-if="hasLists"/>
|
||||
<ShowTasks :show-all="true" v-if="hasLists" :key="showTasksKey"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -43,18 +48,21 @@ import {mapState} from 'vuex'
|
|||
import ShowTasks from './tasks/ShowTasks'
|
||||
import {getHistory} from '@/modules/listHistory'
|
||||
import ListCard from '@/components/list/partials/list-card'
|
||||
import AddTask from '../components/tasks/add-task'
|
||||
|
||||
sytone marked this conversation as resolved
Outdated
konrad
commented
That could be a computed property, mapped from state (see below in this component) That could be a computed property, mapped from state (see below in this component)
|
||||
export default {
|
||||
name: 'Home',
|
||||
components: {
|
||||
ListCard,
|
||||
ShowTasks,
|
||||
AddTask,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
currentDate: new Date(),
|
||||
tasks: [],
|
||||
showTasksKey: 0,
|
||||
}
|
||||
},
|
||||
sytone marked this conversation as resolved
Outdated
konrad
commented
I'd set that statically and to a much higher value - or maybe even remove the timeout alltogether. I don't think people will let the start page of Vikunja sit still all day without using it and then complain about the welcome text not updating. Should be enough to only render it when the component is rendered. And then this whole thing could be refactored into a computed property. I'd set that statically and to a much higher value - or maybe even remove the timeout alltogether. I don't think people will let the start page of Vikunja sit still all day without using it and then complain about the welcome text not updating. Should be enough to only render it when the component is rendered. And then this whole thing could be refactored into a computed property.
sytone
commented
NA, will move to seperate PR. NA, will move to seperate PR.
|
||||
computed: {
|
||||
|
@ -86,10 +94,13 @@ export default {
|
|||
})
|
||||
},
|
||||
sytone marked this conversation as resolved
Outdated
konrad
commented
What is that good for? AFAIU the only place this is used is the What is that good for? AFAIU the only place this is used is the `key` on the task list component? Why does that component need a key?
sytone
commented
I looked up the best way to refresh a vue component when there was a change, this was the recommended way outside of manually invalidating and causing a render which was a sledgehammer. I looked up the best way to refresh a vue component when there was a change, this was the recommended way outside of manually invalidating and causing a render which was a sledgehammer.
|
||||
...mapState({
|
||||
migratorsEnabled: state => state.config.availableMigrators !== null && state.config.availableMigrators.length > 0,
|
||||
migratorsEnabled: state =>
|
||||
state.config.availableMigrators !== null &&
|
||||
state.config.availableMigrators.length > 0,
|
||||
authenticated: state => state.auth.authenticated,
|
||||
userInfo: state => state.auth.info,
|
||||
hasTasks: state => state.hasTasks,
|
||||
defaultListId: state => state.auth.defaultListId,
|
||||
defaultNamespaceId: state => {
|
||||
if (state.namespaces.namespaces.length === 0) {
|
||||
return 0
|
||||
sytone marked this conversation as resolved
Outdated
konrad
commented
Maybe an interval would be better suited here? Maybe an interval would be better suited here?
sytone
commented
NA moving to seperate PR. NA moving to seperate PR.
|
||||
|
@ -105,6 +116,13 @@ export default {
|
|||
return state.namespaces.namespaces[0].lists.length > 0
|
||||
},
|
||||
}),
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// This is to reload the tasks list after adding a new task through the global task add.
|
||||
// FIXME: Should use vuex (somehow?)
|
||||
updateTaskList() {
|
||||
this.showTasksKey++
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
<template>
|
||||
<div
|
||||
:class="{ 'is-loading': taskCollectionService.loading}"
|
||||
class="loader-container is-max-width-desktop list-view">
|
||||
<div class="filter-container" v-if="list.isSavedFilter && !list.isSavedFilter()">
|
||||
:class="{ 'is-loading': taskCollectionService.loading }"
|
||||
class="loader-container is-max-width-desktop list-view"
|
||||
>
|
||||
<div
|
||||
class="filter-container"
|
||||
v-if="list.isSavedFilter && !list.isSavedFilter()"
|
||||
>
|
||||
<div class="items">
|
||||
<div class="search">
|
||||
<div :class="{ 'hidden': !showTaskSearch }" class="field has-addons">
|
||||
<div :class="{ hidden: !showTaskSearch }" class="field has-addons">
|
||||
<div class="control has-icons-left has-icons-right">
|
||||
<input
|
||||
@blur="hideSearchBar()"
|
||||
|
@ -14,9 +18,10 @@
|
|||
:placeholder="$t('misc.search')"
|
||||
type="text"
|
||||
v-focus
|
||||
v-model="searchTerm"/>
|
||||
v-model="searchTerm"
|
||||
/>
|
||||
<span class="icon is-left">
|
||||
<icon icon="search"/>
|
||||
<icon icon="search" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="control">
|
||||
|
@ -52,48 +57,30 @@
|
|||
</div>
|
||||
|
||||
<card :padding="false" :has-content="false" class="has-overflow">
|
||||
<div class="field task-add" v-if="!list.isArchived && canWrite && list.id > 0">
|
||||
<div class="field is-grouped">
|
||||
<p :class="{ 'is-loading': taskService.loading}" class="control has-icons-left is-expanded">
|
||||
<input
|
||||
:class="{ 'disabled': taskService.loading}"
|
||||
@keyup.enter="addTask()"
|
||||
class="input"
|
||||
:placeholder="$t('list.list.addPlaceholder')"
|
||||
type="text"
|
||||
v-focus
|
||||
v-model="newTaskText"
|
||||
ref="newTaskInput"
|
||||
/>
|
||||
<span class="icon is-small is-left">
|
||||
<icon icon="tasks"/>
|
||||
</span>
|
||||
</p>
|
||||
<p class="control">
|
||||
<x-button
|
||||
:disabled="newTaskText.length === 0"
|
||||
@click="addTask()"
|
||||
icon="plus"
|
||||
>
|
||||
{{ $t('list.list.add') }}
|
||||
</x-button>
|
||||
</p>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="showError && newTaskText === ''">
|
||||
{{ $t('list.list.addTitleRequired') }}
|
||||
</p>
|
||||
<quick-add-magic v-if="!showError"/>
|
||||
</div>
|
||||
<template
|
||||
v-if="!list.isArchived && canWrite && list.id > 0"
|
||||
>
|
||||
<add-task
|
||||
@taskAdded="updateTaskList"
|
||||
ref="newTaskInput"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<nothing v-if="ctaVisible && tasks.length === 0 && !taskCollectionService.loading">
|
||||
|
||||
{{ $t('list.list.empty') }}
|
||||
<a @click="$refs.newTaskInput.focus()">
|
||||
<a @click="focusNewTaskInput()">
|
||||
{{ $t('list.list.newTaskCta') }}
|
||||
</a>
|
||||
|
||||
</nothing>
|
||||
|
||||
<div class="tasks-container">
|
||||
<div :class="{'short': isTaskEdit}" class="tasks mt-0" v-if="tasks && tasks.length > 0">
|
||||
<div
|
||||
sytone marked this conversation as resolved
Outdated
konrad
commented
Does that still work when the add task input is in a new component? Does that still work when the add task input is in a new component?
sytone
commented
yes it does, I validated in a few different combinations and looked this up on how to do it with Vue yes it does, I validated in a few different combinations and looked this up on how to do it with Vue
|
||||
:class="{ short: isTaskEdit }"
|
||||
class="tasks mt-0"
|
||||
v-if="tasks && tasks.length > 0"
|
||||
>
|
||||
<single-task-in-list
|
||||
:show-list-color="false"
|
||||
:disabled="!canWrite"
|
||||
|
@ -103,8 +90,12 @@
|
|||
task-detail-route="task.detail"
|
||||
v-for="t in tasks"
|
||||
>
|
||||
<div @click="editTask(t.id)" class="icon settings" v-if="!list.isArchived && canWrite">
|
||||
<icon icon="pencil-alt"/>
|
||||
<div
|
||||
@click="editTask(t.id)"
|
||||
class="icon settings"
|
||||
v-if="!list.isArchived && canWrite"
|
||||
>
|
||||
<icon icon="pencil-alt" />
|
||||
</div>
|
||||
</single-task-in-list>
|
||||
</div>
|
||||
|
@ -121,7 +112,8 @@
|
|||
aria-label="pagination"
|
||||
class="pagination is-centered p-4"
|
||||
role="navigation"
|
||||
v-if="taskCollectionService.totalPages > 1">
|
||||
v-if="taskCollectionService.totalPages > 1"
|
||||
>
|
||||
<router-link
|
||||
:disabled="currentPage === 1"
|
||||
:to="getRouteForPagination(currentPage - 1)"
|
||||
|
@ -138,13 +130,16 @@
|
|||
</router-link>
|
||||
<ul class="pagination-list">
|
||||
<template v-for="(p, i) in pages">
|
||||
<li :key="'page'+i" v-if="p.isEllipsis"><span class="pagination-ellipsis">…</span></li>
|
||||
<li :key="'page'+i" v-else>
|
||||
<li :key="'page' + i" v-if="p.isEllipsis">
|
||||
<span class="pagination-ellipsis">…</span>
|
||||
</li>
|
||||
<li :key="'page' + i" v-else>
|
||||
<router-link
|
||||
:aria-label="'Goto page ' + p.number"
|
||||
:class="{'is-current': p.number === currentPage}"
|
||||
:class="{ 'is-current': p.number === currentPage }"
|
||||
:to="getRouteForPagination(p.number)"
|
||||
class="pagination-link">
|
||||
class="pagination-link"
|
||||
>
|
||||
{{ p.number }}
|
||||
</router-link>
|
||||
</li>
|
||||
|
@ -155,10 +150,8 @@
|
|||
|
||||
<!-- This router view is used to show the task popup while keeping the kanban board itself -->
|
||||
<transition name="modal">
|
||||
<router-view/>
|
||||
<router-view />
|
||||
</transition>
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -167,15 +160,16 @@ import TaskService from '../../../services/task'
|
|||
import TaskModel from '../../../models/task'
|
||||
|
||||
import EditTask from '../../../components/tasks/edit-task'
|
||||
import AddTask from '../../../components/tasks/add-task'
|
||||
import SingleTaskInList from '../../../components/tasks/partials/singleTaskInList'
|
||||
import taskList from '../../../components/tasks/mixins/taskList'
|
||||
import {saveListView} from '@/helpers/saveListView'
|
||||
import { saveListView } from '@/helpers/saveListView'
|
||||
import Rights from '../../../models/rights.json'
|
||||
import {mapState} from 'vuex'
|
||||
import { mapState } from 'vuex'
|
||||
import FilterPopup from '@/components/list/partials/filter-popup'
|
||||
import { HAS_TASKS } from '@/store/mutation-types'
|
||||
import Nothing from '@/components/misc/nothing'
|
||||
import createTask from '@/components/tasks/mixins/createTask'
|
||||
import QuickAddMagic from '@/components/tasks/partials/quick-add-magic'
|
||||
|
||||
export default {
|
||||
name: 'List',
|
||||
|
@ -184,8 +178,6 @@ export default {
|
|||
taskService: TaskService,
|
||||
isTaskEdit: false,
|
||||
taskEditTask: TaskModel,
|
||||
newTaskText: '',
|
||||
showError: false,
|
||||
ctaVisible: false,
|
||||
}
|
||||
},
|
||||
|
@ -194,11 +186,11 @@ export default {
|
|||
createTask,
|
||||
],
|
||||
components: {
|
||||
QuickAddMagic,
|
||||
Nothing,
|
||||
FilterPopup,
|
||||
SingleTaskInList,
|
||||
EditTask,
|
||||
AddTask,
|
||||
},
|
||||
created() {
|
||||
this.taskService = new TaskService()
|
||||
|
@ -212,7 +204,7 @@ export default {
|
|||
list: state => state.currentList,
|
||||
}),
|
||||
mounted() {
|
||||
sytone marked this conversation as resolved
Outdated
konrad
commented
Please add a semicolon at the end. Please add a semicolon at the end.
|
||||
this.$nextTick(() => this.ctaVisible = true)
|
||||
this.$nextTick(() => (this.ctaVisible = true))
|
||||
},
|
||||
methods: {
|
||||
// This function initializes the tasks page and loads the first page of tasks
|
||||
|
@ -221,22 +213,13 @@ export default {
|
|||
this.isTaskEdit = false
|
||||
this.loadTasks(page, search)
|
||||
},
|
||||
addTask() {
|
||||
if (this.newTaskText === '') {
|
||||
this.showError = true
|
||||
return
|
||||
}
|
||||
this.showError = false
|
||||
|
||||
this.createNewTask(this.newTaskText)
|
||||
.then(task => {
|
||||
this.tasks.push(task)
|
||||
this.sortTasks()
|
||||
this.newTaskText = ''
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e)
|
||||
})
|
||||
focusNewTaskInput() {
|
||||
this.$refs.newTaskInput.$refs.newTaskInput.focus()
|
||||
},
|
||||
updateTaskList(task) {
|
||||
this.tasks.push(task)
|
||||
this.sortTasks()
|
||||
this.$store.commit(HAS_TASKS, true)
|
||||
},
|
||||
editTask(id) {
|
||||
// Find the selected task and set it to the current object
|
||||
|
@ -263,4 +246,4 @@ export default {
|
|||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
@ -645,7 +645,7 @@ export default {
|
|||
this.$refs[fieldName].$el.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
inline: 'nearest'
|
||||
inline: 'nearest',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
|
@ -312,7 +312,7 @@ export default {
|
|||
this.success({
|
||||
message: member.admin ?
|
||||
this.$t('team.edit.madeAdmin') :
|
||||
this.$t('team.edit.madeMember')
|
||||
this.$t('team.edit.madeMember'),
|
||||
})
|
||||
})
|
||||
.catch((e) => {
|
||||
|
|
|
@ -129,7 +129,7 @@ export default {
|
|||
let emailVerifyToken = localStorage.getItem('emailConfirmToken')
|
||||
if (emailVerifyToken) {
|
||||
const cancel = this.setLoading()
|
||||
HTTP.post(`user/confirm`, {token: emailVerifyToken})
|
||||
HTTP.post('user/confirm', {token: emailVerifyToken})
|
||||
.then(() => {
|
||||
localStorage.removeItem('emailConfirmToken')
|
||||
this.confirmedEmailSuccess = true
|
||||
|
|
|
@ -16,6 +16,12 @@
|
|||
v-model="settings.name"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">
|
||||
{{ $t('user.settings.general.defaultList') }}
|
||||
</label>
|
||||
<list-search v-model="defaultList"/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" v-model="settings.emailRemindersEnabled"/>
|
||||
|
@ -282,6 +288,7 @@ import {mapState} from 'vuex'
|
|||
|
||||
import AvatarSettings from '../../components/user/avatar-settings'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import ListSearch from '@/components/tasks/partials/listSearch'
|
||||
|
||||
export default {
|
||||
name: 'Settings',
|
||||
|
@ -306,9 +313,12 @@ export default {
|
|||
|
||||
settings: UserSettingsModel,
|
||||
userSettingsService: UserSettingsService,
|
||||
|
||||
defaultList: null,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
ListSearch,
|
||||
AvatarSettings,
|
||||
},
|
||||
created() {
|
||||
|
@ -326,6 +336,8 @@ export default {
|
|||
|
||||
this.playSoundWhenDone = localStorage.getItem(playSoundWhenDoneKey) === 'true' || localStorage.getItem(playSoundWhenDoneKey) === null
|
||||
|
||||
this.defaultList = this.$store.getters['lists/getListById'](this.settings.defaultListId)
|
||||
|
||||
this.totpStatus()
|
||||
},
|
||||
mounted() {
|
||||
|
@ -351,7 +363,7 @@ export default {
|
|||
migratorsEnabled: state => state.config.availableMigrators !== null && state.config.availableMigrators.length > 0,
|
||||
caldavEnabled: state => state.config.caldavEnabled,
|
||||
userInfo: state => state.auth.info,
|
||||
})
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
updatePassword() {
|
||||
|
@ -428,6 +440,7 @@ export default {
|
|||
updateSettings() {
|
||||
localStorage.setItem(playSoundWhenDoneKey, this.playSoundWhenDone)
|
||||
saveLanguage(this.language)
|
||||
this.settings.defaultListId = this.defaultList ? this.defaultList.id : 0
|
||||
|
||||
this.userSettingsService.update(this.settings)
|
||||
.then(() => {
|
||||
|
|
|
@ -20,22 +20,22 @@ module.exports = {
|
|||
msTileImage: 'images/icons/msapplication-icon-144x144.png',
|
||||
},
|
||||
manifestOptions: {
|
||||
"icons": [
|
||||
'icons': [
|
||||
{
|
||||
"src": "./images/icons/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
'src': './images/icons/android-chrome-192x192.png',
|
||||
'sizes': '192x192',
|
||||
'type': 'image/png',
|
||||
},
|
||||
{
|
||||
"src": "./images/icons/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
'src': './images/icons/android-chrome-512x512.png',
|
||||
'sizes': '512x512',
|
||||
'type': 'image/png',
|
||||
},
|
||||
{
|
||||
"src": "./images/icons/icon-maskable.png",
|
||||
"sizes": "1024x1024",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
'src': './images/icons/icon-maskable.png',
|
||||
'sizes': '1024x1024',
|
||||
'type': 'image/png',
|
||||
'purpose': 'maskable',
|
||||
},
|
||||
],
|
||||
shortcuts: [
|
||||
|
@ -62,8 +62,8 @@ module.exports = {
|
|||
name: 'Teams Overview',
|
||||
short_name: 'Teams',
|
||||
url: '/teams',
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
That should be
lf