diff --git a/.drone.yml b/.drone.yml index 325d66533..cdd477476 100644 --- a/.drone.yml +++ b/.drone.yml @@ -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 diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..95bef4270 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,22 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = tab +end_of_line = lf +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 \ No newline at end of file diff --git a/README.md b/README.md index 852c4b4db..0e1a8615c 100644 --- a/README.md +++ b/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 ``` diff --git a/babel.config.js b/babel.config.js index ba179669a..d39e57b42 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,5 +1,5 @@ module.exports = { presets: [ - '@vue/app' - ] + '@vue/app', + ], } diff --git a/cypress/integration/user/settings.spec.js b/cypress/integration/user/settings.spec.js index c2d20d087..36467bae7 100644 --- a/cypress/integration/user/settings.spec.js +++ b/cypress/integration/user/settings.spec.js @@ -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') diff --git a/package.json b/package.json index c0deccfca..2e0dbb667 100644 --- a/package.json +++ b/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" } -} +} \ No newline at end of file diff --git a/src/components/input/button.vue b/src/components/input/button.vue index c0f1f75ac..0957fe295 100644 --- a/src/components/input/button.vue +++ b/src/components/input/button.vue @@ -55,7 +55,7 @@ export default { computed: { showIconOnly() { return this.icon !== '' && typeof this.$slots.default === 'undefined' - } + }, }, methods: { click(e) { diff --git a/src/components/input/datepicker.vue b/src/components/input/datepicker.vue index 029ccac17..5501287c2 100644 --- a/src/components/input/datepicker.vue +++ b/src/components/input/datepicker.vue @@ -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) diff --git a/src/components/input/editor.vue b/src/components/input/editor.vue index fe857caad..16702ce6a 100644 --- a/src/components/input/editor.vue +++ b/src/components/input/editor.vue @@ -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(/^ diff --git a/src/components/misc/nothing.vue b/src/components/misc/nothing.vue index 923d924b8..1f301353a 100644 --- a/src/components/misc/nothing.vue +++ b/src/components/misc/nothing.vue @@ -6,6 +6,6 @@ diff --git a/src/components/misc/shortcut.vue b/src/components/misc/shortcut.vue index b1294db56..8ec894f27 100644 --- a/src/components/misc/shortcut.vue +++ b/src/components/misc/shortcut.vue @@ -14,7 +14,7 @@ export default { keys: { type: Array, required: true, - } + }, }, } diff --git a/src/components/misc/subscription.vue b/src/components/misc/subscription.vue index 3ffc4de3d..a6d92e566 100644 --- a/src/components/misc/subscription.vue +++ b/src/components/misc/subscription.vue @@ -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) }) - } + }, }, } diff --git a/src/components/quick-actions/quick-actions.vue b/src/components/quick-actions/quick-actions.vue index 185a050a1..bd7e80213 100644 --- a/src/components/quick-actions/quick-actions.vue +++ b/src/components/quick-actions/quick-actions.vue @@ -481,7 +481,7 @@ export default { reset() { this.query = '' this.selectedCmd = null - } + }, }, } diff --git a/src/components/sharing/userTeam.vue b/src/components/sharing/userTeam.vue index 6f4612b66..32a9727d1 100644 --- a/src/components/sharing/userTeam.vue +++ b/src/components/sharing/userTeam.vue @@ -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) => { diff --git a/src/components/tasks/add-task.vue b/src/components/tasks/add-task.vue new file mode 100644 index 000000000..b82e97ebf --- /dev/null +++ b/src/components/tasks/add-task.vue @@ -0,0 +1,102 @@ + + + + + diff --git a/src/components/tasks/gantt-component.vue b/src/components/tasks/gantt-component.vue index 53527eef6..a5ba8fbfc 100644 --- a/src/components/tasks/gantt-component.vue +++ b/src/components/tasks/gantt-component.vue @@ -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 } diff --git a/src/components/tasks/mixins/createTask.js b/src/components/tasks/mixins/createTask.js index 2b7b8c8d3..6bf139d18 100644 --- a/src/components/tasks/mixins/createTask.js +++ b/src/components/tasks/mixins/createTask.js @@ -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) - }) + }), ) }) diff --git a/src/components/tasks/partials/attachments.vue b/src/components/tasks/partials/attachments.vue index 52f49fa6f..d49409f21 100644 --- a/src/components/tasks/partials/attachments.vue +++ b/src/components/tasks/partials/attachments.vue @@ -229,7 +229,7 @@ export default { .then((r) => { this.$store.commit( 'attachments/removeById', - this.attachmentToDelete.id + this.attachmentToDelete.id, ) this.success(r) }) diff --git a/src/components/tasks/partials/description.vue b/src/components/tasks/partials/description.vue index 77f9bc910..0c4025074 100644 --- a/src/components/tasks/partials/description.vue +++ b/src/components/tasks/partials/description.vue @@ -91,7 +91,7 @@ export default { .finally(() => { this.saving = false }) - } + }, }, } diff --git a/src/components/tasks/partials/heading.vue b/src/components/tasks/partials/heading.vue index 58a283a66..841751ee6 100644 --- a/src/components/tasks/partials/heading.vue +++ b/src/components/tasks/partials/heading.vue @@ -95,7 +95,7 @@ export default { .finally(() => { this.saving = false }) - } + }, }, } diff --git a/src/components/tasks/partials/listSearch.vue b/src/components/tasks/partials/listSearch.vue index 3a74be228..bb1a645f4 100644 --- a/src/components/tasks/partials/listSearch.vue +++ b/src/components/tasks/partials/listSearch.vue @@ -32,6 +32,11 @@ export default { foundLists: [], } }, + props: { + value: { + required: false, + }, + }, components: { Multiselect, }, @@ -39,6 +44,14 @@ export default { this.listSerivce = new ListService() this.list = new ListModel() }, + 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) diff --git a/src/components/tasks/partials/singleTaskInList.vue b/src/components/tasks/partials/singleTaskInList.vue index f73ee4450..145ac00bd 100644 --- a/src/components/tasks/partials/singleTaskInList.vue +++ b/src/components/tasks/partials/singleTaskInList.vue @@ -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 => { diff --git a/src/helpers/time/createDateFromString.js b/src/helpers/time/createDateFromString.js index 607d69c92..5e9271aa7 100644 --- a/src/helpers/time/createDateFromString.js +++ b/src/helpers/time/createDateFromString.js @@ -12,7 +12,7 @@ export const createDateFromString = dateString => { } if (dateString.includes('-')) { - dateString = dateString.replace(/-/g, "/") + dateString = dateString.replace(/-/g, '/') } return new Date(dateString) diff --git a/src/i18n/lang/en.json b/src/i18n/lang/en.json index 889ed5e9a..05eb95155 100644 --- a/src/i18n/lang/en.json +++ b/src/i18n/lang/en.json @@ -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" diff --git a/src/models/userSettings.js b/src/models/userSettings.js index 71166534c..6ed0ffe58 100644 --- a/src/models/userSettings.js +++ b/src/models/userSettings.js @@ -9,6 +9,7 @@ export default class UserSettingsModel extends AbstractModel { discoverableByName: false, discoverableByEmail: false, overdueTasksRemindersEnabled: true, + defaultListId: undefined, weekStart: 0, } } diff --git a/src/router/index.js b/src/router/index.js index 5f14afdc5..d56ef05b6 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -171,7 +171,7 @@ export default new Router({ name: 'list.create', components: { popup: NewListComponent, - } + }, }, { path: '/namespaces/:id/settings/edit', diff --git a/src/store/modules/auth.js b/src/store/modules/auth.js index 65369a3e0..8ea064189 100644 --- a/src/store/modules/auth.js +++ b/src/store/modules/auth.js @@ -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, diff --git a/src/styles/components/list.scss b/src/styles/components/list.scss index cfd512c5a..825a4cac2 100644 --- a/src/styles/components/list.scss +++ b/src/styles/components/list.scss @@ -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 { diff --git a/src/views/Home.vue b/src/views/Home.vue index d2cbb2661..ea8973a67 100644 --- a/src/views/Home.vue +++ b/src/views/Home.vue @@ -3,10 +3,15 @@

{{ $t(`home.welcome${welcome}`, {username: userInfo.name !== '' ? userInfo.name : userInfo.username}) }}!

+ @@ -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' export default { name: 'Home', components: { ListCard, ShowTasks, + AddTask, }, data() { return { loading: false, currentDate: new Date(), tasks: [], + showTasksKey: 0, } }, computed: { @@ -86,10 +94,13 @@ export default { }) }, ...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 @@ -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++ + }, + }, } diff --git a/src/views/list/views/List.vue b/src/views/list/views/List.vue index f93da3451..532d67615 100644 --- a/src/views/list/views/List.vue +++ b/src/views/list/views/List.vue @@ -1,11 +1,15 @@