From de774beacd4a5a5bc91209939ed3f847c08b72c2 Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 10 Feb 2021 18:17:20 +0100 Subject: [PATCH 001/454] Change main branch to main --- .drone.yml | 32 ++++++++++++++++---------------- Dockerfile | 2 +- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.drone.yml b/.drone.yml index 30c4b76fe..5d18822cb 100644 --- a/.drone.yml +++ b/.drone.yml @@ -4,7 +4,7 @@ name: build trigger: branch: include: - - master + - main event: include: - push @@ -134,7 +134,7 @@ depends_on: trigger: branch: - - master + - main event: - push @@ -180,7 +180,7 @@ steps: pull: true commands: - cd dist - - zip -r ../vikunja-frontend-master.zip * + - zip -r ../vikunja-frontend-unstable.zip * - cd .. depends_on: [ build ] @@ -196,7 +196,7 @@ steps: endpoint: https://s3.fr-par.scw.cloud region: fr-par path_style: true - source: vikunja-frontend-master.zip + source: vikunja-frontend-unstable.zip target: /frontend/ depends_on: [ static ] @@ -279,7 +279,7 @@ name: trigger-desktop-update trigger: branch: - - master + - main event: - push @@ -294,7 +294,7 @@ steps: token: from_secret: drone_token repositories: - - vikunja/desktop@master + - vikunja/desktop@main --- kind: pipeline @@ -311,7 +311,7 @@ platform: trigger: ref: - - refs/heads/master + - refs/heads/main - "refs/tags/**" steps: @@ -328,10 +328,10 @@ steps: auto_tag_suffix: linux-arm build_args: - USE_RELEASE=true - - RELEASE_VERSION=master + - RELEASE_VERSION=unstable when: ref: - - refs/heads/master + - refs/heads/main depends_on: - clone @@ -368,10 +368,10 @@ steps: auto_tag_suffix: linux-arm64 build_args: - USE_RELEASE=true - - RELEASE_VERSION=master + - RELEASE_VERSION=unstable when: ref: - - refs/heads/master + - refs/heads/main depends_on: - clone @@ -410,7 +410,7 @@ depends_on: trigger: ref: - - refs/heads/master + - refs/heads/main - "refs/tags/**" steps: @@ -427,10 +427,10 @@ steps: auto_tag_suffix: linux-amd64 build_args: - USE_RELEASE=true - - RELEASE_VERSION=master + - RELEASE_VERSION=unstable when: ref: - - refs/heads/master + - refs/heads/main - name: docker-version image: plugins/docker:linux-amd64 @@ -457,7 +457,7 @@ name: docker-manifest trigger: ref: - - refs/heads/master + - refs/heads/main - "refs/tags/**" depends_on: @@ -484,7 +484,7 @@ name: notify trigger: ref: - - refs/heads/master + - refs/heads/main - "refs/tags/**" depends_on: diff --git a/Dockerfile b/Dockerfile index 1d4b3850c..3553f2827 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ FROM node:13.14.0 AS compile-image WORKDIR /build ARG USE_RELEASE=false -ARG RELEASE_VERSION=master +ARG RELEASE_VERSION=main ENV YARN_CACHE_FOLDER .cache/yarn/ COPY . ./ From 26d9f8d365ab3d325cbfb94e5870678c0220bd9f Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 10 Feb 2021 18:25:39 +0100 Subject: [PATCH 002/454] Fix waiting for dependency step when building --- .drone.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.drone.yml b/.drone.yml index 5d18822cb..7e77f558d 100644 --- a/.drone.yml +++ b/.drone.yml @@ -174,6 +174,8 @@ steps: - "echo '{\"VERSION\": \"'$(git describe --tags --always --abbrev=10 | sed 's/-/+/' | sed 's/^v//' | sed 's/-g/-/')'\"}' > src/version.json" - yarn run build - sed -i 's/http\:\\/\\/localhost\\:3456\\/api\\/v1/\\/api\\/v1/g' dist/index.html # Override the default api url used for developing + depends_on: + - restore-cache - name: static image: kolaente/zip @@ -247,6 +249,8 @@ steps: - "echo '{\"VERSION\": \"'$(git describe --tags --always --abbrev=10 | sed 's/-/+/' | sed 's/^v//' | sed 's/-g/-/')'\"}' > src/version.json" - yarn run build - sed -i 's/http\:\\/\\/localhost\\:3456\\/api\\/v1/\\/api\\/v1/g' dist/index.html # Override the default api url used for developing + depends_on: + - restore-cache - name: static image: kolaente/zip From e8d7ea85861903918a9ee894838feb20076091f9 Mon Sep 17 00:00:00 2001 From: kolaente Date: Fri, 12 Feb 2021 20:02:51 +0100 Subject: [PATCH 003/454] Work around auto tag for main branch --- .drone.yml | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/.drone.yml b/.drone.yml index 7e77f558d..94aae6080 100644 --- a/.drone.yml +++ b/.drone.yml @@ -118,7 +118,7 @@ steps: source: cypress/screenshots/**/**/* strip_prefix: cypress/screenshots/ target: /${DRONE_REPO}/${DRONE_PULL_REQUEST}_${DRONE_BRANCH}/${DRONE_BUILD_NUMBER}/ - depends_on: + depends_on: - test-frontend when: status: @@ -328,8 +328,7 @@ steps: password: from_secret: docker_password repo: vikunja/frontend - auto_tag: true - auto_tag_suffix: linux-arm + tags: latest-linux-arm build_args: - USE_RELEASE=true - RELEASE_VERSION=unstable @@ -368,8 +367,7 @@ steps: password: from_secret: docker_password repo: vikunja/frontend - auto_tag: true - auto_tag_suffix: linux-arm64 + tags: latest-linux-arm64 build_args: - USE_RELEASE=true - RELEASE_VERSION=unstable @@ -427,8 +425,7 @@ steps: password: from_secret: docker_password repo: vikunja/frontend - auto_tag: true - auto_tag_suffix: linux-amd64 + tags: latest-linux-amd64 build_args: - USE_RELEASE=true - RELEASE_VERSION=unstable @@ -469,6 +466,20 @@ depends_on: - docker-arm-release steps: + - name: manifest + pull: always + image: plugins/manifest + settings: + tags: latest + spec: docker-manifest.tmpl + password: + from_secret: docker_password + username: + from_secret: docker_username + when: + ref: + - refs/heads/main + - name: manifest pull: always image: plugins/manifest @@ -480,6 +491,9 @@ steps: from_secret: docker_password username: from_secret: docker_username + when: + ref: + - "refs/tags/**" --- kind: pipeline From 09680312c1f528a44f228f6b67a34eec7a076ac1 Mon Sep 17 00:00:00 2001 From: kolaente Date: Fri, 12 Feb 2021 20:06:50 +0100 Subject: [PATCH 004/454] Fix release pipeline steps --- .drone.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.drone.yml b/.drone.yml index 94aae6080..af3569ba5 100644 --- a/.drone.yml +++ b/.drone.yml @@ -466,7 +466,7 @@ depends_on: - docker-arm-release steps: - - name: manifest + - name: manifest-latest pull: always image: plugins/manifest settings: @@ -480,7 +480,7 @@ steps: ref: - refs/heads/main - - name: manifest + - name: manifest-release pull: always image: plugins/manifest settings: From fea369f5946b7b94f6d8eab8f348fb97d6ec42fb Mon Sep 17 00:00:00 2001 From: renovate Date: Fri, 12 Feb 2021 22:52:45 +0000 Subject: [PATCH 005/454] Update dependency faker to v5.4.0 (#408) Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/408 Co-authored-by: renovate Co-committed-by: renovate --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index c71fa63c6..c56c0a8a4 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "cypress-file-upload": "5.0.2", "eslint": "7.19.0", "eslint-plugin-vue": "7.5.0", - "faker": "5.3.1", + "faker": "5.4.0", "jest": "26.6.3", "node-sass": "5.0.0", "sass-loader": "10.1.1", diff --git a/yarn.lock b/yarn.lock index a80e7f386..1097c500c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7266,10 +7266,10 @@ extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= -faker@5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/faker/-/faker-5.3.1.tgz#67f8f5c170b97a76b875389e0e8b9155da7b4853" - integrity sha512-sVdoApX/awJHO9DZHZsHVaJBNFiJW0n3lPs0q/nFxp/Mtya1dr2sCMktST3mdxNMHjkvKTTMAW488E+jH1eSbg== +faker@5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/faker/-/faker-5.4.0.tgz#f18e55993c6887918182b003d163df14daeb3011" + integrity sha512-Y9n/Ky/xZx/Bj8DePvXspUYRtHl/rGQytoIT5LaxmNwSe3wWyOeOXb3lT6Dpipq240PVpeFaGKzScz/5fvff2g== fast-deep-equal@^2.0.1: version "2.0.1" From b384c8ecde280ce9330f963cea71da0b226c290e Mon Sep 17 00:00:00 2001 From: renovate Date: Sat, 13 Feb 2021 08:39:27 +0000 Subject: [PATCH 006/454] Update dependency eslint to v7.20.0 (#409) Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/409 Co-authored-by: renovate Co-committed-by: renovate --- package.json | 2 +- yarn.lock | 27 +++++++++++++++++---------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index c56c0a8a4..8b50d2aac 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "babel-eslint": "10.1.0", "cypress": "6.4.0", "cypress-file-upload": "5.0.2", - "eslint": "7.19.0", + "eslint": "7.20.0", "eslint-plugin-vue": "7.5.0", "faker": "5.4.0", "jest": "26.6.3", diff --git a/yarn.lock b/yarn.lock index 1097c500c..4954b629b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -78,6 +78,13 @@ resolved "https://registry.yarnpkg.com/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.24.tgz#3ce939cb127fb8aaa3ffc1e90dff9b8af9f2e3dc" integrity sha512-8GqG48m1XqyXh4mIZrtB5xOhUwSsh1WsrrsaZQOEYYql3YN9DEu9OOSg0ILzXHZo/h2Q74777YE4YzlArQzQEQ== +"@babel/code-frame@7.12.11": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" + integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== + dependencies: + "@babel/highlight" "^7.10.4" + "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e" @@ -6815,12 +6822,12 @@ eslint-visitor-keys@^2.0.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8" integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== -eslint@7.19.0: - version "7.19.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.19.0.tgz#6719621b196b5fad72e43387981314e5d0dc3f41" - integrity sha512-CGlMgJY56JZ9ZSYhJuhow61lMPPjUzWmChFya71Z/jilVos7mR/jPgaEfVGgMBY5DshbKdG8Ezb8FDCHcoMEMg== +eslint@7.20.0: + version "7.20.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.20.0.tgz#db07c4ca4eda2e2316e7aa57ac7fc91ec550bdc7" + integrity sha512-qGi0CTcOGP2OtCQBgWZlQjcTuP0XkIpYFj25XtRTQSHC+umNnp7UMshr2G8SLsRFYDdAPFeHOsiteadmMH02Yw== dependencies: - "@babel/code-frame" "^7.0.0" + "@babel/code-frame" "7.12.11" "@eslint/eslintrc" "^0.3.0" ajv "^6.10.0" chalk "^4.0.0" @@ -6832,7 +6839,7 @@ eslint@7.19.0: eslint-utils "^2.1.0" eslint-visitor-keys "^2.0.0" espree "^7.3.1" - esquery "^1.2.0" + esquery "^1.4.0" esutils "^2.0.2" file-entry-cache "^6.0.0" functional-red-black-tree "^1.0.1" @@ -6902,10 +6909,10 @@ esquery@^1.0.1: dependencies: estraverse "^4.0.0" -esquery@^1.2.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.3.1.tgz#b78b5828aa8e214e29fb74c4d5b752e1c033da57" - integrity sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ== +esquery@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" + integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== dependencies: estraverse "^5.1.0" From 3f20ae89a8838dfdb53ec90ac3e39436b5d2e771 Mon Sep 17 00:00:00 2001 From: konrad Date: Sun, 14 Feb 2021 19:18:51 +0000 Subject: [PATCH 007/454] Subscriptions and notifications for namespaces, tasks and lists (#410) Co-authored-by: kolaente Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/410 Co-authored-by: konrad Co-committed-by: konrad --- .../list/list-settings-dropdown.vue | 18 +++ src/components/misc/subscription.vue | 121 ++++++++++++++++++ .../namespace/namespace-settings-dropdown.vue | 18 +++ src/main.js | 5 +- src/models/list.js | 6 + src/models/namespace.js | 7 + src/models/subscription.js | 20 +++ src/models/task.js | 6 + src/services/subscription.js | 15 +++ src/views/tasks/TaskDetailView.vue | 8 ++ 10 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 src/components/misc/subscription.vue create mode 100644 src/models/subscription.js create mode 100644 src/services/subscription.js diff --git a/src/components/list/list-settings-dropdown.vue b/src/components/list/list-settings-dropdown.vue index c560efef1..72af3814d 100644 --- a/src/components/list/list-settings-dropdown.vue +++ b/src/components/list/list-settings-dropdown.vue @@ -54,6 +54,14 @@ > Archive + 0 diff --git a/src/components/misc/subscription.vue b/src/components/misc/subscription.vue new file mode 100644 index 000000000..d59a491c6 --- /dev/null +++ b/src/components/misc/subscription.vue @@ -0,0 +1,121 @@ + + + diff --git a/src/components/namespace/namespace-settings-dropdown.vue b/src/components/namespace/namespace-settings-dropdown.vue index 70125cec8..a647ed711 100644 --- a/src/components/namespace/namespace-settings-dropdown.vue +++ b/src/components/namespace/namespace-settings-dropdown.vue @@ -33,6 +33,14 @@ > Archive + import Dropdown from '@/components/misc/dropdown' import DropdownItem from '@/components/misc/dropdown-item' +import TaskSubscription from '@/components/misc/subscription' export default { name: 'namespace-settings-dropdown', + data() { + return { + subscription: null, + } + }, components: { DropdownItem, Dropdown, + TaskSubscription, }, props: { namespace: { required: true, }, }, + mounted() { + this.subscription = this.namespace.subscription + }, } diff --git a/src/main.js b/src/main.js index 75829e37e..9f8a92bc7 100644 --- a/src/main.js +++ b/src/main.js @@ -61,8 +61,9 @@ import { faArchive, faShareAlt, faImage, + faBell, } from '@fortawesome/free-solid-svg-icons' -import {faCalendarAlt, faClock, faComments, faSave, faStar, faTimesCircle, faSun} from '@fortawesome/free-regular-svg-icons' +import {faCalendarAlt, faClock, faComments, faSave, faStar, faTimesCircle, faSun, faBellSlash} from '@fortawesome/free-regular-svg-icons' import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome' // PWA import './registerServiceWorker' @@ -152,6 +153,8 @@ library.add(faEllipsisH) library.add(faArchive) library.add(faShareAlt) library.add(faImage) +library.add(faBell) +library.add(faBellSlash) Vue.component('icon', FontAwesomeIcon) diff --git a/src/models/list.js b/src/models/list.js index cbae65cff..e098a600c 100644 --- a/src/models/list.js +++ b/src/models/list.js @@ -2,6 +2,7 @@ import AbstractModel from './abstractModel' import TaskModel from './task' import UserModel from './user' import {getSavedFilterIdFromListId} from '@/helpers/savedFilter' +import SubscriptionModel from '@/models/subscription' export default class ListModel extends AbstractModel { @@ -19,6 +20,10 @@ export default class ListModel extends AbstractModel { this.owner = new UserModel(this.owner) + if(typeof this.subscription !== 'undefined' && this.subscription !== null) { + this.subscription = new SubscriptionModel(this.subscription) + } + this.created = new Date(this.created) this.updated = new Date(this.updated) } @@ -37,6 +42,7 @@ export default class ListModel extends AbstractModel { identifier: '', backgroundInformation: null, isFavorite: false, + subscription: null, created: null, updated: null, diff --git a/src/models/namespace.js b/src/models/namespace.js index 928692029..88c049aaf 100644 --- a/src/models/namespace.js +++ b/src/models/namespace.js @@ -1,6 +1,7 @@ import AbstractModel from './abstractModel' import ListModel from './list' import UserModel from './user' +import SubscriptionModel from '@/models/subscription' export default class NamespaceModel extends AbstractModel { constructor(data) { @@ -13,8 +14,13 @@ export default class NamespaceModel extends AbstractModel { this.lists = this.lists.map(l => { return new ListModel(l) }) + this.owner = new UserModel(this.owner) + if(typeof this.subscription !== 'undefined' && this.subscription !== null) { + this.subscription = new SubscriptionModel(this.subscription) + } + this.created = new Date(this.created) this.updated = new Date(this.updated) } @@ -29,6 +35,7 @@ export default class NamespaceModel extends AbstractModel { lists: [], isArchived: false, hexColor: '', + subscription: null, created: null, updated: null, diff --git a/src/models/subscription.js b/src/models/subscription.js new file mode 100644 index 000000000..68d0be855 --- /dev/null +++ b/src/models/subscription.js @@ -0,0 +1,20 @@ +import AbstractModel from '@/models/abstractModel' +import UserModel from '@/models/user' + +export default class SubscriptionModel extends AbstractModel { + constructor(data) { + super(data) + this.user = new UserModel(this.user) + this.created = new Date(this.created) + } + + defaults() { + return { + id: 0, + entity: '', + entityId: 0, + created: null, + user: {}, + } + } +} diff --git a/src/models/task.js b/src/models/task.js index 091f58b79..d6385d506 100644 --- a/src/models/task.js +++ b/src/models/task.js @@ -2,6 +2,7 @@ import AbstractModel from './abstractModel' import UserModel from './user' import LabelModel from './label' import AttachmentModel from './attachment' +import SubscriptionModel from '@/models/subscription' const parseDate = date => { if (date && !date.startsWith('0001')) { @@ -75,6 +76,10 @@ export default class TaskModel extends AbstractModel { this.identifier = '' } + if(typeof this.subscription !== 'undefined' && this.subscription !== null) { + this.subscription = new SubscriptionModel(this.subscription) + } + this.created = new Date(this.created) this.updated = new Date(this.updated) } @@ -104,6 +109,7 @@ export default class TaskModel extends AbstractModel { identifier: '', index: 0, isFavorite: false, + subscription: null, createdBy: UserModel, created: null, diff --git a/src/services/subscription.js b/src/services/subscription.js new file mode 100644 index 000000000..20c79c097 --- /dev/null +++ b/src/services/subscription.js @@ -0,0 +1,15 @@ +import AbstractService from '@/services/abstractService' +import SubscriptionModel from '@/models/subscription' + +export default class SubscriptionService extends AbstractService { + constructor() { + super({ + create: '/subscriptions/{entity}/{entityId}', + delete: '/subscriptions/{entity}/{entityId}', + }) + } + + modelFactory(data) { + return new SubscriptionModel(data) + } +} \ No newline at end of file diff --git a/src/views/tasks/TaskDetailView.vue b/src/views/tasks/TaskDetailView.vue index ca300e1ae..cb838f0a4 100644 --- a/src/views/tasks/TaskDetailView.vue +++ b/src/views/tasks/TaskDetailView.vue @@ -255,6 +255,12 @@ > {{ task.done ? 'Mark as undone' : 'Done!' }} + Date: Sun, 14 Feb 2021 23:41:29 +0100 Subject: [PATCH 008/454] Add separate manifest template for latest --- .drone.yml | 2 +- docker-manifest-latest.tmpl | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 docker-manifest-latest.tmpl diff --git a/.drone.yml b/.drone.yml index af3569ba5..a70e95aef 100644 --- a/.drone.yml +++ b/.drone.yml @@ -471,7 +471,7 @@ steps: image: plugins/manifest settings: tags: latest - spec: docker-manifest.tmpl + spec: docker-manifest-latest.tmpl password: from_secret: docker_password username: diff --git a/docker-manifest-latest.tmpl b/docker-manifest-latest.tmpl new file mode 100644 index 000000000..e9b19eaca --- /dev/null +++ b/docker-manifest-latest.tmpl @@ -0,0 +1,17 @@ +image: vikunja/frontend:latest +manifests: + - + image: vikunja/frontend:latest-linux-amd64 + platform: + architecture: amd64 + os: linux + - + image: vikunja/frontend:latest-linux-arm64 + platform: + architecture: arm64 + os: linux + - + image: vikunja/frontend:latest-linux-arm + platform: + architecture: arm + os: linux From e4ec70663f2186e35a641692d224e3e73caf8b40 Mon Sep 17 00:00:00 2001 From: renovate Date: Mon, 15 Feb 2021 07:26:56 +0000 Subject: [PATCH 009/454] Update dependency eslint-plugin-vue to v7.6.0 (#411) Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/411 Co-authored-by: renovate Co-committed-by: renovate --- package.json | 2 +- yarn.lock | 29 +++++++++++------------------ 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 8b50d2aac..95f9446c3 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "cypress": "6.4.0", "cypress-file-upload": "5.0.2", "eslint": "7.20.0", - "eslint-plugin-vue": "7.5.0", + "eslint-plugin-vue": "7.6.0", "faker": "5.4.0", "jest": "26.6.3", "node-sass": "5.0.0", diff --git a/yarn.lock b/yarn.lock index 4954b629b..cdeeb2d55 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6766,15 +6766,15 @@ eslint-loader@^2.2.1: object-hash "^1.1.4" rimraf "^2.6.1" -eslint-plugin-vue@7.5.0: - version "7.5.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-7.5.0.tgz#cc6d983eb22781fa2440a7573cf39af439bb5725" - integrity sha512-QnMMTcyV8PLxBz7QQNAwISSEs6LYk2LJvGlxalXvpCtfKnqo7qcY0aZTIxPe8QOnHd7WCwiMZLOJzg6A03T0Gw== +eslint-plugin-vue@7.6.0: + version "7.6.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-7.6.0.tgz#ea616e6dfd45d545adb16cba628c5a992cc31f0b" + integrity sha512-qYpKwAvpcQXyUXVcG8Zd+fxHDx9iSgTQuO7dql7Ug/2BCvNNDr6s3I9p8MoUo23JJdO7ZAjW3vSwY/EBf4uBcw== dependencies: eslint-utils "^2.1.0" natural-compare "^1.4.0" semver "^7.3.2" - vue-eslint-parser "^7.4.1" + vue-eslint-parser "^7.5.0" eslint-scope@^4.0.3: version "4.0.3" @@ -6902,13 +6902,6 @@ esprima@^4.0.0, esprima@^4.0.1, esprima@~4.0.0: resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -esquery@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708" - integrity sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA== - dependencies: - estraverse "^4.0.0" - esquery@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" @@ -6930,7 +6923,7 @@ esrecurse@^4.3.0: dependencies: estraverse "^5.2.0" -estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: +estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: version "4.3.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== @@ -15158,16 +15151,16 @@ vue-easymde@1.3.2: easymde "^2.13.0" marked "^1.2.7" -vue-eslint-parser@^7.4.1: - version "7.4.1" - resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-7.4.1.tgz#e4adcf7876a7379758d9056a72235af18a587f92" - integrity sha512-AFvhdxpFvliYq1xt/biNBslTHE/zbEvSnr1qfHA/KxRIpErmEDrQZlQnvEexednRHmLfDNOMuDYwZL5xkLzIXQ== +vue-eslint-parser@^7.5.0: + version "7.5.0" + resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-7.5.0.tgz#b68221c55fee061899afcfb4441ec74c1495285e" + integrity sha512-6EHzl00hIpy4yWZo3qSbtvtVw1A1cTKOv1w95QSuAqGgk4113XtRjvNIiEGo49r0YWOPYsrmI4Dl64axL5Agrw== dependencies: debug "^4.1.1" eslint-scope "^5.0.0" eslint-visitor-keys "^1.1.0" espree "^6.2.1" - esquery "^1.0.1" + esquery "^1.4.0" lodash "^4.17.15" vue-flatpickr-component@8.1.6: From 1fb52fb7504917e05ea04e4936a99fbb07e25fea Mon Sep 17 00:00:00 2001 From: renovate Date: Tue, 16 Feb 2021 06:54:51 +0000 Subject: [PATCH 010/454] Update dependency cypress to v6.5.0 (#412) Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/412 Co-authored-by: renovate Co-committed-by: renovate --- package.json | 2 +- yarn.lock | 25 +++++++++++++++++++------ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 95f9446c3..7bd349f9f 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "@vue/cli-service": "4.5.11", "axios": "0.21.1", "babel-eslint": "10.1.0", - "cypress": "6.4.0", + "cypress": "6.5.0", "cypress-file-upload": "5.0.2", "eslint": "7.20.0", "eslint-plugin-vue": "7.6.0", diff --git a/yarn.lock b/yarn.lock index cdeeb2d55..37911c648 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2417,6 +2417,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-13.11.1.tgz#49a2a83df9d26daacead30d0ccc8762b128d53c7" integrity sha512-eWQGP3qtxwL8FGneRrC5DwrJLGN4/dH1clNTuLfN81HCrxVtxRjygDTUoZJ5ASlDEeo0ppYFQjQIlXhtXpOn6g== +"@types/node@12.12.50": + version "12.12.50" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.50.tgz#e9b2e85fafc15f2a8aa8fdd41091b983da5fd6ee" + integrity sha512-5ImO01Fb8YsEOYpV+aeyGYztcYcjGsBvN4D7G5r1ef2cuQOpymjWNQi5V0rKHE6PC2ru3HkoUr/Br2/8GUA84w== + "@types/node@^10.1.0": version "10.17.19" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.19.tgz#1d31ddd5503dba2af7a901aafef3392e4955620e" @@ -5988,14 +5993,15 @@ cypress-file-upload@5.0.2: dependencies: mime "^2.5.0" -cypress@6.4.0: - version "6.4.0" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-6.4.0.tgz#432c516bf4f1a0f042a6aa1f2c3a4278fa35a8b2" - integrity sha512-SrsPsZ4IBterudkoFYBvkQmXOVxclh1/+ytbzpV8AH/D2FA+s2Qy5ISsaRzOFsbQa4KZWoi3AKwREmF1HucYkg== +cypress@6.5.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-6.5.0.tgz#d853d7a8f915f894249a8788294bfba077278c17" + integrity sha512-ol/yTAqHrQQpYBjxLlRSvZf4DOb9AhaQNVlwdOZgJcBHZOOa52/p/6/p3PPcvzjWGOMG6Yq0z4G+jrbWyk/9Dg== dependencies: "@cypress/listr-verbose-renderer" "^0.4.1" "@cypress/request" "^2.88.5" "@cypress/xvfb" "^1.2.4" + "@types/node" "12.12.50" "@types/sinonjs__fake-timers" "^6.0.1" "@types/sizzle" "^2.3.2" arch "^2.1.2" @@ -6008,7 +6014,7 @@ cypress@6.4.0: commander "^5.1.0" common-tags "^1.8.0" dayjs "^1.9.3" - debug "^4.1.1" + debug "4.3.2" eventemitter2 "^6.4.2" execa "^4.0.2" executable "^4.1.1" @@ -6081,6 +6087,13 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: dependencies: ms "2.0.0" +debug@4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" + integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== + dependencies: + ms "2.1.2" + debug@^3.1.0, debug@^3.1.1, debug@^3.2.5, debug@^3.2.6: version "3.2.6" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" @@ -10816,7 +10829,7 @@ ms@2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== -ms@^2.1.1: +ms@2.1.2, ms@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== From 08dcc7722874c2e0bfc4d44d077cc27d9e357b20 Mon Sep 17 00:00:00 2001 From: kolaente Date: Thu, 18 Feb 2021 23:36:06 +0100 Subject: [PATCH 011/454] Fix deleting task relations See vikunja/api/commit/eb3a94567817b78a3325a937cedce237837e3785 --- src/components/tasks/partials/relatedTasks.vue | 2 +- src/services/taskRelation.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/tasks/partials/relatedTasks.vue b/src/components/tasks/partials/relatedTasks.vue index 4b91ff570..d2a0c5443 100644 --- a/src/components/tasks/partials/relatedTasks.vue +++ b/src/components/tasks/partials/relatedTasks.vue @@ -219,7 +219,7 @@ export default { }) }, removeTaskRelation() { - let rel = new TaskRelationModel({ + const rel = new TaskRelationModel({ relationKind: this.relationToDelete.relationKind, taskId: this.taskId, otherTaskId: this.relationToDelete.otherTaskId, diff --git a/src/services/taskRelation.js b/src/services/taskRelation.js index ae80da005..54459f0e8 100644 --- a/src/services/taskRelation.js +++ b/src/services/taskRelation.js @@ -6,7 +6,7 @@ export default class TaskRelationService extends AbstractService { constructor() { super({ create: '/tasks/{taskId}/relations', - delete: '/tasks/{taskId}/relations', + delete: '/tasks/{taskId}/relations/{relationKind}/{otherTaskId}', }) } From 8e2dfcffd5dc8d778ee6b6d3c781464cfd49a6b3 Mon Sep 17 00:00:00 2001 From: kolaente Date: Sat, 20 Feb 2021 15:35:30 +0100 Subject: [PATCH 012/454] Fix tasks not disappearing from the kanban board when moving them between lists --- cypress/integration/list/list.spec.js | 34 +++++++++++++++++++++++++++ src/views/tasks/TaskDetailView.vue | 1 + 2 files changed, 35 insertions(+) diff --git a/cypress/integration/list/list.spec.js b/cypress/integration/list/list.spec.js index 28b6b5091..c4c3ba572 100644 --- a/cypress/integration/list/list.spec.js +++ b/cypress/integration/list/list.spec.js @@ -371,5 +371,39 @@ describe('Lists', () => { cy.url() .should('contain', `/tasks/${tasks[0].id}`) }) + + it('Should remove a task from the kanban board when moving it to another list', () => { + const lists = ListFactory.create(2) + const tasks = TaskFactory.create(5, { + id: '{increment}', + list_id: 1, + bucket_id: 1, + }) + const task = tasks[0] + cy.visit('/lists/1/kanban') + + cy.getAttached('.kanban .bucket .tasks .task') + .contains(task.title) + .should('be.visible') + .click() + + cy.get('.task-view .action-buttons .button') + .contains('Move task') + .click() + cy.get('.task-view .content.details .field .multiselect.control .input-wrapper input') + .type(`${lists[1].title}{enter}`) + // The requests happen with a 200ms timeout. Because of that, the results are not yet there when cypress + // presses enter and we can't simulate pressing on enter to select the item. + cy.get('.task-view .content.details .field .multiselect.control .search-results') + .children() + .first() + .click() + + cy.get('.global-notification') + .should('contain', 'Success') + cy.go('back') + cy.get('.kanban .bucket') + .should('not.contain', task.title) + }) }) }) diff --git a/src/views/tasks/TaskDetailView.vue b/src/views/tasks/TaskDetailView.vue index cb838f0a4..7943a6fb0 100644 --- a/src/views/tasks/TaskDetailView.vue +++ b/src/views/tasks/TaskDetailView.vue @@ -679,6 +679,7 @@ export default { changeList(list) { this.task.listId = list.id this.saveTask() + this.$store.commit('kanban/removeTaskInBucket', this.task) }, }, } From 074b7e53f5e509999fcab29f88f75110cc65b976 Mon Sep 17 00:00:00 2001 From: kolaente Date: Sat, 20 Feb 2021 15:54:12 +0100 Subject: [PATCH 013/454] Don't show the list color in the list view --- cypress/integration/list/list.spec.js | 15 +++++++++++++++ .../tasks/partials/singleTaskInList.vue | 7 ++++++- src/views/list/views/List.vue | 1 + 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/cypress/integration/list/list.spec.js b/cypress/integration/list/list.spec.js index c4c3ba572..815eb2ac8 100644 --- a/cypress/integration/list/list.spec.js +++ b/cypress/integration/list/list.spec.js @@ -102,6 +102,21 @@ describe('Lists', () => { cy.get('input.input[placeholder="Add a new task..."') .should('not.exist') }) + + it('Should only show the color of a list in the navigation and not in the list view', () => { + const lists = ListFactory.create(1, { + hex_color: '00db60', + }) + TaskFactory.create(10, { + list_id: lists[0].id, + }) + cy.visit(`/lists/${lists[0].id}/`) + + cy.get('.menu-list li .list-menu-link .color-bubble') + .should('have.css', 'background-color', 'rgb(0, 219, 96)') + cy.get('.tasks-container .tasks .color-bubble') + .should('not.exist') + }) }) describe('Table View', () => { diff --git a/src/components/tasks/partials/singleTaskInList.vue b/src/components/tasks/partials/singleTaskInList.vue index aa02baf8a..a0b987f3c 100644 --- a/src/components/tasks/partials/singleTaskInList.vue +++ b/src/components/tasks/partials/singleTaskInList.vue @@ -2,9 +2,10 @@
+ >
Date: Sat, 20 Feb 2021 16:28:45 +0100 Subject: [PATCH 014/454] Fix showing and hiding lists in the menu --- src/components/home/navigation.vue | 37 +++++++++++++++++++----------- src/store/modules/namespaces.js | 2 +- src/styles/theme/navigation.scss | 27 +--------------------- 3 files changed, 26 insertions(+), 40 deletions(-) diff --git a/src/components/home/navigation.vue b/src/components/home/navigation.vue index 2dc5ff7c3..29f9c3fa8 100644 --- a/src/components/home/navigation.vue +++ b/src/components/home/navigation.vue @@ -51,8 +51,8 @@
@@ -118,7 +118,6 @@ import {calculateDayInterval} from '@/helpers/time/calculateDayInterval' import {calculateNearestHours} from '@/helpers/time/calculateNearestHours' import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside' import {createDateFromString} from '@/helpers/time/createDateFromString' -import {mapState} from 'vuex' export default { name: 'datepicker', @@ -142,7 +141,9 @@ export default { }, chooseDateLabel: { type: String, - default: 'Choose a date' + default() { + return this.$t('input.datepicker.chooseDate') + } }, disabled: { type: Boolean, @@ -165,19 +166,21 @@ export default { this.updateData() }, }, - computed: mapState({ - flatPickerConfig: state => ({ - altFormat: 'j M Y H:i', - altInput: true, - dateFormat: 'Y-m-d H:i', - enableTime: true, - time_24hr: true, - inline: true, - locale: { - firstDayOfWeek: state.auth.settings.weekStart, - }, - }) - }), + computed: { + flatPickerConfig() { + return { + altFormat: this.$t('date.altFormatLong'), + altInput: true, + dateFormat: 'Y-m-d H:i', + enableTime: true, + time_24hr: true, + inline: true, + locale: { + firstDayOfWeek: this.$store.state.auth.settings.weekStart, + }, + } + }, + }, methods: { setDateValue(newVal) { if(newVal === null) { diff --git a/src/components/input/editor.vue b/src/components/input/editor.vue index 0334bac6b..068799901 100644 --- a/src/components/input/editor.vue +++ b/src/components/input/editor.vue @@ -15,7 +15,7 @@ :shadow="false" type="secondary" > - Done + {{ $t('input.editor.done') }}
@@ -129,112 +129,112 @@ export default { { name: 'heading-1', action: EasyMDE.toggleHeading1, - title: 'Heading 1', + title: this.$t('input.editor.heading1'), icon: '', }, { name: 'heading-2', action: EasyMDE.toggleHeading2, - title: 'Heading 2', + title: this.$t('input.editor.heading2'), icon: '', }, { name: 'heading-3', action: EasyMDE.toggleHeading3, - title: 'Heading 3', + title: this.$t('input.editor.heading3'), icon: '', }, { name: 'heading-smaller', action: EasyMDE.toggleHeadingSmaller, - title: 'Heading Smaller', + title: this.$t('input.editor.headingSmaller'), icon: '', }, { name: 'heading-bigger', action: EasyMDE.toggleHeadingBigger, - title: 'Heading Bigger', + title: this.$t('input.editor.headingBigger'), icon: '', }, '|', { name: 'bold', action: EasyMDE.toggleBold, - title: 'Bold', + title: this.$t('input.editor.bold'), icon: '', }, { name: 'italic', action: EasyMDE.toggleItalic, - title: 'Italic', + title: this.$t('input.editor.italic'), icon: '', }, { name: 'strikethrough', action: EasyMDE.toggleStrikethrough, - title: 'Strikethrough', + title: this.$t('input.editor.strikethrough'), icon: '', }, { name: 'code', action: EasyMDE.toggleCodeBlock, - title: 'Code', + title: this.$t('input.editor.code'), icon: '', }, { name: 'quote', action: EasyMDE.toggleBlockquote, - title: 'Quote', + title: this.$t('input.editor.quote'), icon: '', }, { name: 'unordered-list', action: EasyMDE.toggleUnorderedList, - title: 'Unordered List', + title: this.$t('input.editor.unorderedList'), icon: '', }, { name: 'ordered-list', action: EasyMDE.toggleOrderedList, - title: 'Ordered List', + title: this.$t('input.editor.orderedList'), icon: '', }, '|', { name: 'clean-block', action: EasyMDE.cleanBlock, - title: 'Clean Block', + title: this.$t('input.editor.cleanBlock'), icon: '', }, { name: 'link', action: EasyMDE.drawLink, - title: 'Link', + title: this.$t('input.editor.link'), icon: '', }, { name: 'image', action: EasyMDE.drawImage, - title: 'Image', + title: this.$t('input.editor.image'), icon: '', }, { name: 'table', action: EasyMDE.drawTable, - title: 'Table', + title: this.$t('input.editor.table'), icon: '', }, { name: 'horizontal-rule', action: EasyMDE.drawHorizontalRule, - title: 'Horizontal Rule', + title: this.$t('input.editor.horizontalRule'), icon: '', }, '|', { name: 'side-by-side', action: EasyMDE.toggleSideBySide, - title: 'Side By Side', + title: this.$t('input.editor.sideBySide'), icon: '', }, { @@ -242,7 +242,7 @@ export default { action: () => { window.open('https://www.markdownguide.org/basic-syntax/', '_blank') }, - title: 'Guide', + title: this.$t('input.editor.guide'), icon: '', }, ], diff --git a/src/components/input/multiselect.vue b/src/components/input/multiselect.vue index f0db907c5..34a81fad2 100644 --- a/src/components/input/multiselect.vue +++ b/src/components/input/multiselect.vue @@ -149,15 +149,15 @@ export default { createPlaceholder: { type: String, default() { - return 'Create new' - }, + return this.$t('input.multiselect.createPlaceholder') + } }, // The text shown next to an option. selectPlaceholder: { type: String, default() { - return 'Click or press enter to select' - }, + 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: { diff --git a/src/components/list/list-settings-dropdown.vue b/src/components/list/list-settings-dropdown.vue index 090defb1f..e44bec3a1 100644 --- a/src/components/list/list-settings-dropdown.vue +++ b/src/components/list/list-settings-dropdown.vue @@ -5,13 +5,13 @@ :to="{ name: `${listRoutePrefix}.settings.edit`, params: { listId: list.id } }" icon="pen" > - Edit + {{ $t('menu.edit') }}
- Delete + {{ $t('misc.delete') }} diff --git a/src/components/list/partials/filters.vue b/src/components/list/partials/filters.vue index 448222d0f..1bb586593 100644 --- a/src/components/list/partials/filters.vue +++ b/src/components/list/partials/filters.vue @@ -1,28 +1,30 @@ @@ -76,19 +78,19 @@ :selected="s.right === rights.READ" :value="rights.READ" > - Read only + {{ $t('list.share.right.read') }} @@ -108,7 +110,7 @@ - Not shared with any {{ shareType }} yet. + {{ $t('list.share.userTeam.notShared', {type: shareTypeNames}) }} @@ -117,13 +119,11 @@ @submit="deleteSharable()" v-if="showDeleteModal" > - Remove a {{ shareType }} from the {{ typeString }} + + {{ $t('list.share.userTeam.removeHeader', {type: shareTypeName, sharable: sharableName}) }} +

- Are you sure you want to remove this {{ shareType }} from the - {{ typeString }}?
- This CANNOT BE UNDONE! + {{ $t('list.share.userTeam.removeText', {type: shareTypeName, sharable: sharableName}) }}

@@ -131,8 +131,6 @@ diff --git a/src/helpers/parseDateOrNull.js b/src/helpers/parseDateOrNull.js index 08022e8a8..7fd1cf280 100644 --- a/src/helpers/parseDateOrNull.js +++ b/src/helpers/parseDateOrNull.js @@ -1,4 +1,8 @@ export const parseDateOrNull = date => { + if (date instanceof Date) { + return date + } + if (date && !date.startsWith('0001')) { return new Date(date) } diff --git a/src/helpers/parseTaskText.js b/src/helpers/parseTaskText.js new file mode 100644 index 000000000..0039a6d80 --- /dev/null +++ b/src/helpers/parseTaskText.js @@ -0,0 +1,103 @@ +import {parseDate} from './time/parseDate' +import priorities from '../models/priorities.json' + +const LABEL_PREFIX = '~' +const LIST_PREFIX = '*' +const PRIORITY_PREFIX = '!' +const ASSIGNEE_PREFIX = '@' + +/** + * Parses task text for dates, assignees, labels, lists, priorities and returns an object with all found intents. + * + * @param text + */ +export const parseTaskText = text => { + const result = { + text: text, + date: null, + labels: [], + list: null, + priority: null, + assignees: [], + } + + result.labels = getItemsFromPrefix(text, LABEL_PREFIX) + + const lists = getItemsFromPrefix(text, LIST_PREFIX) + result.list = lists.length > 0 ? lists[0] : null + + result.priority = getPriority(text) + + result.assignees = getItemsFromPrefix(text, ASSIGNEE_PREFIX) + + const {newText, date} = parseDate(text) + result.text = newText + result.date = date + + return cleanupResult(result) +} + +const getItemsFromPrefix = (text, prefix) => { + const items = [] + + const itemParts = text.split(prefix) + itemParts.forEach((p, index) => { + // First part contains the rest + if (index < 1) { + return + } + + let labelText + if (p.charAt(0) === `'`) { + labelText = p.split(`'`)[1] + } else if (p.charAt(0) === `"`) { + labelText = p.split(`"`)[1] + } else { + // Only until the next space + labelText = p.split(' ')[0] + } + items.push(labelText) + }) + + return Array.from(new Set(items)) +} + +const getPriority = text => { + const ps = getItemsFromPrefix(text, PRIORITY_PREFIX) + if (ps.length === 0) { + return null + } + + for (const p of ps) { + for (const pi in priorities) { + if (priorities[pi] === parseInt(p)) { + return parseInt(p) + } + } + } + + return null +} + +const cleanupItemText = (text, items, prefix) => { + items.forEach(l => { + text = text + .replace(`${prefix}'${l}' `, '') + .replace(`${prefix}'${l}'`, '') + .replace(`${prefix}"${l}" `, '') + .replace(`${prefix}"${l}"`, '') + .replace(`${prefix}${l} `, '') + .replace(`${prefix}${l}`, '') + }) + return text +} + +const cleanupResult = result => { + result.text = cleanupItemText(result.text, result.labels, LABEL_PREFIX) + result.text = cleanupItemText(result.text, [result.list], LIST_PREFIX) + result.text = cleanupItemText(result.text, [result.priority], PRIORITY_PREFIX) + result.text = cleanupItemText(result.text, result.assignees, ASSIGNEE_PREFIX) + result.text = result.text.trim() + + return result +} diff --git a/src/helpers/parseTaskText.test.js b/src/helpers/parseTaskText.test.js new file mode 100644 index 000000000..618e97223 --- /dev/null +++ b/src/helpers/parseTaskText.test.js @@ -0,0 +1,409 @@ +import {parseTaskText} from './parseTaskText' +import {getDateFromText, getDateFromTextIn} from './time/parseDate' +import {calculateDayInterval} from './time/calculateDayInterval' +import priorities from '../models/priorities.json' + +describe('Parse Task Text', () => { + it('should return text with no intents as is', () => { + expect(parseTaskText('Lorem Ipsum').text).toBe('Lorem Ipsum') + }) + + describe('Date Parsing', () => { + it('should not return any date if none was provided', () => { + const result = parseTaskText('Lorem Ipsum') + + expect(result.text).toBe('Lorem Ipsum') + expect(result.date).toBeNull() + }) + it('should ignore casing', () => { + const result = parseTaskText('Lorem Ipsum ToDay') + + expect(result.text).toBe('Lorem Ipsum') + const now = new Date() + expect(result.date.getFullYear()).toBe(now.getFullYear()) + expect(result.date.getMonth()).toBe(now.getMonth()) + expect(result.date.getDate()).toBe(now.getDate()) + }) + it('should recognize today', () => { + const result = parseTaskText('Lorem Ipsum today') + + expect(result.text).toBe('Lorem Ipsum') + const now = new Date() + expect(result.date.getFullYear()).toBe(now.getFullYear()) + expect(result.date.getMonth()).toBe(now.getMonth()) + expect(result.date.getDate()).toBe(now.getDate()) + }) + describe('should recognize today with a time', () => { + const cases = { + 'at 15:00': '15:0', + '@ 15:00': '15:0', + 'at 15:30': '15:30', + '@ 3pm': '15:0', + 'at 3pm': '15:0', + 'at 3 pm': '15:0', + 'at 3am': '3:0', + 'at 3:12 am': '3:12', + 'at 3:12 pm': '15:12', + } + + for (const c in cases) { + it('should recognize today with a time ' + c, () => { + const result = parseTaskText('Lorem Ipsum today ' + c) + + expect(result.text).toBe('Lorem Ipsum') + const now = new Date() + expect(result.date.getFullYear()).toBe(now.getFullYear()) + expect(result.date.getMonth()).toBe(now.getMonth()) + expect(result.date.getDate()).toBe(now.getDate()) + expect(`${result.date.getHours()}:${result.date.getMinutes()}`).toBe(cases[c]) + expect(result.date.getSeconds()).toBe(0) + }) + } + }) + it('should recognize tomorrow', () => { + const result = parseTaskText('Lorem Ipsum tomorrow') + + expect(result.text).toBe('Lorem Ipsum') + const tomorrow = new Date() + tomorrow.setDate(tomorrow.getDate() + 1) + expect(result.date.getFullYear()).toBe(tomorrow.getFullYear()) + expect(result.date.getMonth()).toBe(tomorrow.getMonth()) + expect(result.date.getDate()).toBe(tomorrow.getDate()) + }) + it('should recognize next monday', () => { + const result = parseTaskText('Lorem Ipsum next monday') + + const untilNextMonday = calculateDayInterval('nextMonday') + + expect(result.text).toBe('Lorem Ipsum') + const nextMonday = new Date() + nextMonday.setDate(nextMonday.getDate() + untilNextMonday) + expect(result.date.getFullYear()).toBe(nextMonday.getFullYear()) + expect(result.date.getMonth()).toBe(nextMonday.getMonth()) + expect(result.date.getDate()).toBe(nextMonday.getDate()) + }) + it('should recognize this weekend', () => { + const result = parseTaskText('Lorem Ipsum this weekend') + + const untilThisWeekend = calculateDayInterval('thisWeekend') + + expect(result.text).toBe('Lorem Ipsum') + const thisWeekend = new Date() + thisWeekend.setDate(thisWeekend.getDate() + untilThisWeekend) + expect(result.date.getFullYear()).toBe(thisWeekend.getFullYear()) + expect(result.date.getMonth()).toBe(thisWeekend.getMonth()) + expect(result.date.getDate()).toBe(thisWeekend.getDate()) + }) + it('should recognize later this week', () => { + const result = parseTaskText('Lorem Ipsum later this week') + + const untilLaterThisWeek = calculateDayInterval('laterThisWeek') + + expect(result.text).toBe('Lorem Ipsum') + const laterThisWeek = new Date() + laterThisWeek.setDate(laterThisWeek.getDate() + untilLaterThisWeek) + expect(result.date.getFullYear()).toBe(laterThisWeek.getFullYear()) + expect(result.date.getMonth()).toBe(laterThisWeek.getMonth()) + expect(result.date.getDate()).toBe(laterThisWeek.getDate()) + }) + it('should recognize later next week', () => { + const result = parseTaskText('Lorem Ipsum later next week') + + const untilLaterNextWeek = calculateDayInterval('laterNextWeek') + + expect(result.text).toBe('Lorem Ipsum') + const laterNextWeek = new Date() + laterNextWeek.setDate(laterNextWeek.getDate() + untilLaterNextWeek) + expect(result.date.getFullYear()).toBe(laterNextWeek.getFullYear()) + expect(result.date.getMonth()).toBe(laterNextWeek.getMonth()) + expect(result.date.getDate()).toBe(laterNextWeek.getDate()) + }) + it('should recognize next week', () => { + const result = parseTaskText('Lorem Ipsum next week') + + const untilNextWeek = calculateDayInterval('nextWeek') + + expect(result.text).toBe('Lorem Ipsum') + const nextWeek = new Date() + nextWeek.setDate(nextWeek.getDate() + untilNextWeek) + expect(result.date.getFullYear()).toBe(nextWeek.getFullYear()) + expect(result.date.getMonth()).toBe(nextWeek.getMonth()) + expect(result.date.getDate()).toBe(nextWeek.getDate()) + }) + it('should recognize next month', () => { + const result = parseTaskText('Lorem Ipsum next month') + + expect(result.text).toBe('Lorem Ipsum') + const nextMonth = new Date() + nextMonth.setDate(1) + nextMonth.setMonth(nextMonth.getMonth() + 1) + expect(result.date.getFullYear()).toBe(nextMonth.getFullYear()) + expect(result.date.getMonth()).toBe(nextMonth.getMonth()) + expect(result.date.getDate()).toBe(nextMonth.getDate()) + }) + it('should recognize a date', () => { + const result = parseTaskText('Lorem Ipsum 06/26/2021') + + expect(result.text).toBe('Lorem Ipsum') + const date = new Date() + date.setFullYear(2021, 5, 26) + expect(result.date.getFullYear()).toBe(date.getFullYear()) + expect(result.date.getMonth()).toBe(date.getMonth()) + expect(result.date.getDate()).toBe(date.getDate()) + }) + it('should recognize end of month', () => { + const result = parseTaskText('Lorem Ipsum end of month') + + expect(result.text).toBe('Lorem Ipsum') + const curDate = new Date() + const date = new Date(curDate.getFullYear(), curDate.getMonth() + 1, 0) + expect(result.date.getFullYear()).toBe(date.getFullYear()) + expect(result.date.getMonth()).toBe(date.getMonth()) + expect(result.date.getDate()).toBe(date.getDate()) + }) + it('should recognize weekdays', () => { + const result = parseTaskText('Lorem Ipsum thu') + + expect(result.text).toBe('Lorem Ipsum') + const nextThursday = new Date() + nextThursday.setDate(nextThursday.getDate() + ((4 + 7 - nextThursday.getDay()) % 7)) + expect(`${result.date.getFullYear()}-${result.date.getMonth()}-${result.date.getDate()}`).toBe(`${nextThursday.getFullYear()}-${nextThursday.getMonth()}-${nextThursday.getDate()}`) + expect(+new Date(result.date)).toBeGreaterThan(+new Date() - 10) // In on thursdays, this may be different by one second and thus fails the test + }) + it('should recognize weekdays with time', () => { + const result = parseTaskText('Lorem Ipsum thu at 14:00') + + expect(result.text).toBe('Lorem Ipsum') + const nextThursday = new Date() + nextThursday.setDate(nextThursday.getDate() + ((4 + 7 - nextThursday.getDay()) % 7)) + expect(`${result.date.getFullYear()}-${result.date.getMonth()}-${result.date.getDate()}`).toBe(`${nextThursday.getFullYear()}-${nextThursday.getMonth()}-${nextThursday.getDate()}`) + expect(`${result.date.getHours()}:${result.date.getMinutes()}`).toBe('14:0') + expect(+new Date(result.date)).toBeGreaterThan(+new Date() - 10) // In on thursdays, this may be different by one second and thus fails the test + }) + it('should recognize dates of the month in the past but next month', () => { + const date = new Date() + date.setDate(date.getDate() - 1) + const result = parseTaskText(`Lorem Ipsum ${date.getDate()}nd`) + + expect(result.text).toBe('Lorem Ipsum') + expect(result.date.getDate()).toBe(date.getDate()) + expect(result.date.getMonth()).toBe(date.getMonth() + 1) + }) + it('should recognize dates of the month in the future', () => { + const date = new Date() + const result = parseTaskText(`Lorem Ipsum ${date.getDate() + 1}nd`) + + expect(result.text).toBe('Lorem Ipsum') + expect(result.date.getDate()).toBe(date.getDate() + 1) + }) + + describe('Parse date from text', () => { + const now = new Date() + now.setFullYear(2021, 5, 24) + + const cases = { + 'Lorem Ipsum 06/08/2021 ad': '2021-6-8', + 'Lorem Ipsum 6/7/21 ad': '2021-6-7', + '27/07/2021,': null, + '2021/07/06,': '2021-7-6', + '2021-07-06': '2021-7-6', + '27 jan': '2022-1-27', + '27/1': '2022-1-27', + '27/01': '2022-1-27', + '16/12': '2021-12-16', + '01/27': '2022-1-27', + '1/27': '2022-1-27', + 'Jan 27': '2022-1-27', + 'jan 27': '2022-1-27', + 'feb 21': '2022-2-21', + 'mar 21': '2022-3-21', + 'apr 21': '2022-4-21', + 'may 21': '2022-5-21', + 'jun 21': '2022-6-21', + 'jul 21': '2021-7-21', + 'aug 21': '2021-8-21', + 'sep 21': '2021-9-21', + 'oct 21': '2021-10-21', + 'nov 21': '2021-11-21', + 'dec 21': '2021-12-21', + } + + for (const c in cases) { + it(`should parse '${c}' as '${cases[c]}'`, () => { + const {date} = getDateFromText(c, now) + if (date === null && cases[c] === null) { + expect(date).toBeNull() + return + } + + expect(`${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`).toBe(cases[c]) + }) + } + }) + + describe('Parse date from text in', () => { + const now = new Date() + now.setFullYear(2021, 5, 24) + now.setHours(12) + now.setMinutes(0) + now.setSeconds(0) + + const cases = { + 'Lorem Ipsum in 1 hour': '2021-6-24 13:0', + 'in 2 hours': '2021-6-24 14:0', + 'in 1 day': '2021-6-25 12:0', + 'in 2 days': '2021-6-26 12:0', + 'in 1 week': '2021-7-1 12:0', + 'in 2 weeks': '2021-7-8 12:0', + 'in 4 weeks': '2021-7-22 12:0', + 'in 1 month': '2021-7-24 12:0', + 'in 3 months': '2021-9-24 12:0', + } + + for (const c in cases) { + it(`should parse '${c}' as '${cases[c]}'`, () => { + const {date} = getDateFromTextIn(c, now) + if (date === null && cases[c] === null) { + expect(date).toBeNull() + return + } + + expect(`${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()} ${date.getHours()}:${date.getMinutes()}`).toBe(cases[c]) + }) + } + }) + + }) + + describe('Labels', () => { + it('should parse labels', () => { + const result = parseTaskText('Lorem Ipsum ~label1 ~label2') + + expect(result.text).toBe('Lorem Ipsum') + expect(result.labels).toHaveLength(2) + expect(result.labels[0]).toBe('label1') + expect(result.labels[1]).toBe('label2') + }) + it('should parse labels from the start', () => { + const result = parseTaskText('~label1 Lorem Ipsum ~label2') + + expect(result.text).toBe('Lorem Ipsum') + expect(result.labels).toHaveLength(2) + expect(result.labels[0]).toBe('label1') + expect(result.labels[1]).toBe('label2') + }) + it('should resolve duplicate labels', () => { + const result = parseTaskText('Lorem Ipsum ~label1 ~label1 ~label2') + + expect(result.text).toBe('Lorem Ipsum') + expect(result.labels).toHaveLength(2) + expect(result.labels[0]).toBe('label1') + expect(result.labels[1]).toBe('label2') + }) + it('should correctly parse labels with spaces in them', () => { + const result = parseTaskText(`Lorem ~'label with space' Ipsum`) + + expect(result.text).toBe('Lorem Ipsum') + expect(result.labels).toHaveLength(1) + expect(result.labels[0]).toBe('label with space') + }) + it('should correctly parse labels with spaces in them and "', () => { + const result = parseTaskText('Lorem ~"label with space" Ipsum') + + expect(result.text).toBe('Lorem Ipsum') + expect(result.labels).toHaveLength(1) + expect(result.labels[0]).toBe('label with space') + }) + }) + + describe('List', () => { + it('should parse a list', () => { + const result = parseTaskText('Lorem Ipsum *list') + + expect(result.text).toBe('Lorem Ipsum') + expect(result.list).toBe('list') + }) + it('should parse a list with a space in it', () => { + const result = parseTaskText(`Lorem Ipsum *'list with long name'`) + + expect(result.text).toBe('Lorem Ipsum') + expect(result.list).toBe('list with long name') + }) + it('should parse a list with a space in it and "', () => { + const result = parseTaskText(`Lorem Ipsum *"list with long name"`) + + expect(result.text).toBe('Lorem Ipsum') + expect(result.list).toBe('list with long name') + }) + it('should parse only the first list', () => { + const result = parseTaskText(`Lorem Ipsum *list1 *list2 *list3`) + + expect(result.text).toBe('Lorem Ipsum *list2 *list3') + expect(result.list).toBe('list1') + }) + }) + + describe('Priority', () => { + for (const p in priorities) { + it(`should parse priority ${p}`, () => { + const result = parseTaskText(`Lorem Ipsum !${priorities[p]}`) + + expect(result.text).toBe('Lorem Ipsum') + expect(result.priority).toBe(priorities[p]) + }) + } + it(`should not parse an invalid priority`, () => { + const result = parseTaskText(`Lorem Ipsum !9999`) + + expect(result.text).toBe('Lorem Ipsum !9999') + expect(result.priority).toBe(null) + }) + it(`should not parse an invalid priority but use the first valid one it finds`, () => { + const result = parseTaskText(`Lorem Ipsum !9999 !1`) + + expect(result.text).toBe('Lorem Ipsum !9999') + expect(result.priority).toBe(1) + }) + }) + + describe('Assignee', () => { + it('should parse an assignee', () => { + const result = parseTaskText('Lorem Ipsum @user') + + expect(result.text).toBe('Lorem Ipsum') + expect(result.assignees).toHaveLength(1) + expect(result.assignees[0]).toBe('user') + }) + it('should parse multiple assignees', () => { + const result = parseTaskText('Lorem Ipsum @user1 @user2 @user3') + + expect(result.text).toBe('Lorem Ipsum') + expect(result.assignees).toHaveLength(3) + expect(result.assignees[0]).toBe('user1') + expect(result.assignees[1]).toBe('user2') + expect(result.assignees[2]).toBe('user3') + }) + it('should parse avoid duplicate assignees', () => { + const result = parseTaskText('Lorem Ipsum @user1 @user1 @user2') + + expect(result.text).toBe('Lorem Ipsum') + expect(result.assignees).toHaveLength(2) + expect(result.assignees[0]).toBe('user1') + expect(result.assignees[1]).toBe('user2') + }) + it('should parse an assignee with a space in it', () => { + const result = parseTaskText(`Lorem Ipsum @'user with long name'`) + + expect(result.text).toBe('Lorem Ipsum') + expect(result.assignees).toHaveLength(1) + expect(result.assignees[0]).toBe('user with long name') + }) + it('should parse an assignee with a space in it and "', () => { + const result = parseTaskText(`Lorem Ipsum @"user with long name"`) + + expect(result.text).toBe('Lorem Ipsum') + expect(result.assignees).toHaveLength(1) + expect(result.assignees[0]).toBe('user with long name') + }) + }) +}) diff --git a/src/helpers/replaceAll.js b/src/helpers/replaceAll.js new file mode 100644 index 000000000..db6834b17 --- /dev/null +++ b/src/helpers/replaceAll.js @@ -0,0 +1,15 @@ +/** + * This function replaces all text, no matter the case. + * + * See https://stackoverflow.com/a/7313467/10924593 + * + * @parma str + * @param search + * @param replace + * @returns {*} + */ +export const replaceAll = (str, search, replace) => { + const esc = search.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); + const reg = new RegExp(esc, 'ig'); + return str.replace(reg, replace); +} \ No newline at end of file diff --git a/src/helpers/time/parseDate.js b/src/helpers/time/parseDate.js new file mode 100644 index 000000000..77e7fb9ed --- /dev/null +++ b/src/helpers/time/parseDate.js @@ -0,0 +1,290 @@ +import {calculateDayInterval} from './calculateDayInterval' +import {calculateNearestHours} from './calculateNearestHours' +import {replaceAll} from '../replaceAll' + +export const parseDate = text => { + const lowerText = text.toLowerCase() + + if (lowerText.includes('today')) { + return addTimeToDate(text, getDateFromInterval(calculateDayInterval('today')), 'today') + } + if (lowerText.includes('tomorrow')) { + return addTimeToDate(text, getDateFromInterval(calculateDayInterval('tomorrow')), 'tomorrow') + } + if (lowerText.includes('next monday')) { + return addTimeToDate(text, getDateFromInterval(calculateDayInterval('nextMonday')), 'next monday') + } + if (lowerText.includes('this weekend')) { + return addTimeToDate(text, getDateFromInterval(calculateDayInterval('thisWeekend')), 'this weekend') + } + if (lowerText.includes('later this week')) { + return addTimeToDate(text, getDateFromInterval(calculateDayInterval('laterThisWeek')), 'later this week') + } + if (lowerText.includes('later next week')) { + return addTimeToDate(text, getDateFromInterval(calculateDayInterval('laterNextWeek')), 'later next week') + } + if (lowerText.includes('next week')) { + return addTimeToDate(text, getDateFromInterval(calculateDayInterval('nextWeek')), 'next week') + } + if (lowerText.includes('next month')) { + const date = new Date() + date.setDate(1) + date.setMonth(date.getMonth() + 1) + date.setHours(calculateNearestHours(date)) + date.setMinutes(0) + date.setSeconds(0) + + return addTimeToDate(text, date, 'next month') + } + if (lowerText.includes('end of month')) { + const curDate = new Date() + const date = new Date(curDate.getFullYear(), curDate.getMonth() + 1, 0) + date.setHours(calculateNearestHours(date)) + date.setMinutes(0) + date.setSeconds(0) + + return addTimeToDate(text, date, 'end of month') + } + + let parsed = getDateFromWeekday(text) + if (parsed.date !== null) { + return addTimeToDate(text, parsed.date, parsed.foundText) + } + + parsed = getDayFromText(text) + if (parsed.date !== null) { + return addTimeToDate(text, parsed.date, parsed.foundText) + } + + parsed = getDateFromTextIn(text) + if (parsed.date !== null) { + return { + newText: replaceAll(text, parsed.foundText, ''), + date: parsed.date, + } + } + + parsed = getDateFromText(text) + + return { + newText: replaceAll(text, parsed.foundText, ''), + date: parsed.date, + } +} + +const addTimeToDate = (text, date, match) => { + const matcher = new RegExp(`(${match} (at|@) )([0-9][0-9]?(:[0-9][0-9]?)?( ?(a|p)m)?)`, 'ig') + const results = matcher.exec(text) + + if (results !== null) { + const time = results[3] + const parts = time.split(':') + let hours = parseInt(parts[0]) + let minutes = 0 + if (time.endsWith('pm')) { + hours += 12 + } + if (parts.length > 1) { + minutes = parseInt(parts[1]) + } + + date.setHours(hours) + date.setMinutes(minutes) + date.setSeconds(0) + } + + const replace = results !== null ? results[0] : match + return { + newText: replaceAll(text, replace, ''), + date: date, + } +} + +export const getDateFromText = (text, now = new Date()) => { + const fullDateRegex = /([0-9][0-9]?\/[0-9][0-9]?\/[0-9][0-9]([0-9][0-9])?|[0-9][0-9][0-9][0-9]\/[0-9][0-9]?\/[0-9][0-9]?|[0-9][0-9][0-9][0-9]-[0-9][0-9]?-[0-9][0-9]?)/ig + + // 1. Try parsing the text as a "usual" date, like 2021-06-24 or 06/24/2021 + let results = fullDateRegex.exec(text) + let result = results === null ? null : results[0] + let foundText = result + let containsYear = true + if (result === null) { + // 2. Try parsing the date as something like "jan 21" or "21 jan" + const monthRegex = /((jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec) [0-9][0-9]?|[0-9][0-9]? (jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec))/ig + results = monthRegex.exec(text) + result = results === null ? null : `${results[0]} ${now.getFullYear()}` + foundText = results === null ? '' : results[0] + containsYear = false + + if (result === null) { + // 3. Try parsing the date as "27/01" or "01/27" + const monthNumericRegex = /([0-9][0-9]?\/[0-9][0-9]?)/ig + results = monthNumericRegex.exec(text) + + // Put the year before or after the date, depending on what works + result = results === null ? null : `${now.getFullYear()}/${results[0]}` + foundText = results === null ? '' : results[0] + if (isNaN(new Date(result))) { + result = results === null ? null : `${results[0]}/${now.getFullYear()}` + } + if (isNaN(new Date(result)) && results[0] !== null) { + const parts = results[0].split('/') + result = `${parts[1]}/${parts[0]}/${now.getFullYear()}` + } + } + } + + if (result === null) { + return { + foundText, + date: null, + } + } + + const date = new Date(result) + if (isNaN(date)) { + return { + foundText, + date: null, + } + } + + if (!containsYear && date < now) { + date.setFullYear(date.getFullYear() + 1) + } + + return { + foundText, + date, + } +} + +export const getDateFromTextIn = (text, now = new Date()) => { + const regex = /(in [0-9]+ (hours?|days?|weeks?|months?))/ig + const results = regex.exec(text) + if (results === null) { + return { + foundText: '', + date: null, + } + } + + let foundText = results[0] + const date = new Date(now) + const parts = foundText.split(' ') + switch (parts[2]) { + case 'hours': + case 'hour': + date.setHours(date.getHours() + parseInt(parts[1])) + break + case 'days': + case 'day': + date.setDate(date.getDate() + parseInt(parts[1])) + break + case 'weeks': + case 'week': + date.setDate(date.getDate() + parseInt(parts[1]) * 7) + break + case 'months': + case 'month': + date.setMonth(date.getMonth() + parseInt(parts[1])) + break + } + + return { + foundText, + date, + } +} + +const getDateFromWeekday = text => { + const matcher = /(mon|monday|tue|tuesday|wed|wednesday|thu|thursday|fri|friday|sat|saturday|sun|sunday)/ig + const results = matcher.exec(text) + if (results === null) { + return { + foundText: null, + date: null, + } + } + + const date = new Date() + const currentDay = date.getDay() + let day = 0 + + switch (results[0]) { + case 'mon': + case 'monday': + day = 1 + break + case 'tue': + case 'tuesday': + day = 2 + break + case 'wed': + case 'wednesday': + day = 3 + break + case 'thu': + case 'thursday': + day = 4 + break + case 'fri': + case 'friday': + day = 5 + break + case 'sat': + case 'saturday': + day = 6 + break + case 'sun': + case 'sunday': + day = 0 + break + default: + return { + foundText: null, + date: null, + } + } + + const distance = (day + 7 - currentDay) % 7 + date.setDate(date.getDate() + distance) + + return { + foundText: results[0], + date: date, + } +} + +const getDayFromText = text => { + const matcher = /(([1-2][0-9])|(3[01])|(0?[1-9]))(st|nd|rd|th|\.)/ig + const results = matcher.exec(text) + if (results === null) { + return { + foundText: null, + date: null, + } + } + + const date = new Date() + date.setDate(parseInt(results[0])) + + if (date < new Date()) { + date.setMonth(date.getMonth() + 1) + } + + return { + foundText: results[0], + date: date, + } +} + +const getDateFromInterval = interval => { + const newDate = new Date() + newDate.setDate(newDate.getDate() + interval) + newDate.setHours(calculateNearestHours(newDate)) + newDate.setMinutes(0) + newDate.setSeconds(0) + + return newDate +} diff --git a/src/i18n/lang/en.json b/src/i18n/lang/en.json index 87d1091f9..f7ba7f71f 100644 --- a/src/i18n/lang/en.json +++ b/src/i18n/lang/en.json @@ -597,6 +597,28 @@ "weeks": "Weeks", "months": "Months", "years": "Years" + }, + "quickAddMagic": { + "hint": "You can use Quick Add Magic", + "what": "What?", + "title": "Quick Add Magic", + "intro": "When creating a task, you can use special keywords to directly add attributes to the newly created task. This allows to add commonly used attributes to tasks much faster.", + "multiple": "You can use this multiple times.", + "label1": "To add a label, simply prefix the name of the label with {prefix}.", + "label2": "Vikunja will first check if the label already exist and create it if not.", + "label3": "To use spaces, simply add a \" around the label name.", + "label4": "For example: {prefix}\"Label with spaces\".", + "priority1": "To set a task's priority, add a number 1-5, prefixed with a {prefix}.", + "priority2": "The higher the number, the higher the priority.", + "assignees": "To directly assign the task to a user, add their username prefixed with @ to the task.", + "list1": "To set a list for the task to appear in, enter its name prefixed with {prefix}.", + "list2": "This will return an error if the list does not exist.", + "dateAndTime": "Date and time", + "date": "Any date will be used as the due date of the new task. You can use dates in any of these formats:", + "dateWeekday": "any weekday, will use the next date with that date", + "dateCurrentYear": "will use the current year", + "dateNth": "will use the {day}th of the current month", + "dateTime": "Combine any of the date formats with \"{time}\" (or {timePM}) to set a time." } }, "team": { diff --git a/src/store/modules/lists.js b/src/store/modules/lists.js index ace651f69..cd088b86a 100644 --- a/src/store/modules/lists.js +++ b/src/store/modules/lists.js @@ -25,6 +25,12 @@ export default { } return null }, + findListByExactname: state => name => { + const list = Object.values(state).find(l => { + return l.title.toLowerCase() === name.toLowerCase() + }) + return typeof list === 'undefined' ? null : list + }, }, actions: { toggleListFavorite(ctx, list) { diff --git a/src/styles/components/_all.scss b/src/styles/components/_all.scss index b870f9648..5895a11fe 100644 --- a/src/styles/components/_all.scss +++ b/src/styles/components/_all.scss @@ -24,3 +24,4 @@ @import 'datepicker'; @import 'notifications'; @import 'quick-actions'; +@import 'hint-modal'; diff --git a/src/styles/components/hint-modal.scss b/src/styles/components/hint-modal.scss new file mode 100644 index 000000000..e82b32d28 --- /dev/null +++ b/src/styles/components/hint-modal.scss @@ -0,0 +1,43 @@ +.hint-modal { + z-index: 4600; + + .card-content { + text-align: left; + + .info { + font-style: italic; + } + + p { + display: flex; + justify-content: space-between; + align-items: center; + + .shortcuts { + display: flex; + align-items: center; + + i { + padding: 0 .25rem; + } + + span { + padding: .1rem .35rem; + border: 1px solid $grey-300; + background: $grey-100; + border-radius: 3px; + font-size: .75rem; + } + } + } + + .message-body { + padding: .5rem .75rem; + } + } +} + +.modal-container-smaller .hint-modal .modal-container { + height: calc(100vh - 5rem); +} + diff --git a/src/styles/components/keyboard-shortcuts.scss b/src/styles/components/keyboard-shortcuts.scss index 892fb07c9..93b3f9e4c 100644 --- a/src/styles/components/keyboard-shortcuts.scss +++ b/src/styles/components/keyboard-shortcuts.scss @@ -7,42 +7,3 @@ color: $grey-500; transition: color $transition; } - -.keyboard-shortcuts-modal { - z-index: 4600; - - .card-content { - text-align: left; - - .info { - font-style: italic; - } - - p { - display: flex; - justify-content: space-between; - align-items: center; - - .shortcuts { - display: flex; - align-items: center; - - i { - padding: 0 .25rem; - } - - span { - padding: .1rem .35rem; - border: 1px solid $grey-300; - background: $grey-100; - border-radius: 3px; - font-size: .75rem; - } - } - } - - .message-body { - padding: .5rem .75rem; - } - } -} diff --git a/src/views/filters/CreateSavedFilter.vue b/src/views/filters/CreateSavedFilter.vue index 331e923db..6bbd76522 100644 --- a/src/views/filters/CreateSavedFilter.vue +++ b/src/views/filters/CreateSavedFilter.vue @@ -1,5 +1,5 @@ From c7c9b5ee47493f3b0d7f80917fcb2f9f4c02efce Mon Sep 17 00:00:00 2001 From: kolaente Date: Tue, 6 Jul 2021 17:13:13 +0200 Subject: [PATCH 219/454] Show salutation based on the time of day --- src/i18n/lang/en.json | 5 +++- src/views/Home.vue | 55 ++++++++++++++++++++++++++++++------------- 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/src/i18n/lang/en.json b/src/i18n/lang/en.json index b1aa20d3f..e183ce401 100644 --- a/src/i18n/lang/en.json +++ b/src/i18n/lang/en.json @@ -1,6 +1,9 @@ { "home": { - "welcome": "Hi {username}", + "welcomeNight": "Good Night {username}", + "welcomeMorning": "Good Morning {username}", + "welcomeDay": "Hi {username}", + "welcomeEvening": "Good Evening {username}", "list": { "newText": "You can create a new list for your new tasks:", "new": "Create a new list", diff --git a/src/views/Home.vue b/src/views/Home.vue index 487d7aaec..325570b90 100644 --- a/src/views/Home.vue +++ b/src/views/Home.vue @@ -1,7 +1,7 @@ @@ -30,10 +41,13 @@ From b6bc410346e05c0e002d5fb7e19438518ba85b17 Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 7 Jul 2021 22:13:21 +0200 Subject: [PATCH 229/454] Add a button to copy an attachment url from the attachment overview --- src/components/tasks/mixins/attachmentUpload.js | 3 ++- src/components/tasks/partials/attachments.vue | 17 ++++++++++++++--- src/helpers/generateAttachmentUrl.js | 3 +++ src/i18n/lang/en.json | 6 +++++- 4 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 src/helpers/generateAttachmentUrl.js diff --git a/src/components/tasks/mixins/attachmentUpload.js b/src/components/tasks/mixins/attachmentUpload.js index 1e08e0fc4..4ba368822 100644 --- a/src/components/tasks/mixins/attachmentUpload.js +++ b/src/components/tasks/mixins/attachmentUpload.js @@ -1,5 +1,6 @@ import AttachmentModel from '../../../models/attachment' import AttachmentService from '../../../services/attachment' +import {generateAttachmentUrl} from '@/helpers/generateAttachmentUrl' export default { methods: { @@ -21,7 +22,7 @@ export default { taskId: this.taskId, attachment: a, }) - onSuccess(`${window.API_URL}/tasks/${this.taskId}/attachments/${a.id}`) + onSuccess(generateAttachmentUrl(this.taskId, a.id)) }) } if (r.errors !== null) { diff --git a/src/components/tasks/partials/attachments.vue b/src/components/tasks/partials/attachments.vue index 2f76ae3c7..52f49fa6f 100644 --- a/src/components/tasks/partials/attachments.vue +++ b/src/components/tasks/partials/attachments.vue @@ -55,14 +55,20 @@

{{ $t('task.attachment.download') }} + + {{ $t('task.attachment.copyUrl') }} + {{ $t('misc.delete') }} @@ -106,7 +112,7 @@ > {{ $t('task.attachment.delete') }}

- {{ $t('task.attachment.deleteText1', {filename: attachmentUpload.file.name}) }}
+ {{ $t('task.attachment.deleteText1', {filename: attachmentToDelete.file.name}) }}
{{ $t('task.attachment.deleteText2') }}

@@ -133,7 +139,9 @@ import AttachmentService from '../../../services/attachment' import AttachmentModel from '../../../models/attachment' import User from '../../misc/user' import attachmentUpload from '@/components/tasks/mixins/attachmentUpload' +import {generateAttachmentUrl} from '@/helpers/generateAttachmentUrl' import {mapState} from 'vuex' +import copy from 'copy-to-clipboard' export default { name: 'attachments', @@ -247,6 +255,9 @@ export default { this.downloadAttachment(attachment) } }, + copyUrl(attachment) { + copy(generateAttachmentUrl(this.taskId, attachment.id)) + }, }, } diff --git a/src/helpers/generateAttachmentUrl.js b/src/helpers/generateAttachmentUrl.js new file mode 100644 index 000000000..40c1e1efc --- /dev/null +++ b/src/helpers/generateAttachmentUrl.js @@ -0,0 +1,3 @@ +export const generateAttachmentUrl = (taskId, attachmentId) => { + return `${window.API_URL}/tasks/${taskId}/attachments/${attachmentId}` +} \ No newline at end of file diff --git a/src/i18n/lang/en.json b/src/i18n/lang/en.json index fdebc1893..fe007a663 100644 --- a/src/i18n/lang/en.json +++ b/src/i18n/lang/en.json @@ -529,11 +529,15 @@ "title": "Attachments", "createdBy": "created {0} by {1}", "download": "Download", + "downloadTooltip": "Download this attachment", "upload": "Upload attachment", "drop": "Drop files here to upload", "delete": "Delete attachment", + "deleteTooltip": "Delete this attachment", "deleteText1": "Are you sure you want to delete the attachment {filename}?", - "deleteText2": "This cannot be undone!" + "deleteText2": "This cannot be undone!", + "copyUrl": "Copy URL", + "copyUrlTooltip": "Copy the url of this attachment for usage in text" }, "comment": { "title": "Comments", From 9f9016e17b55003a7341e4e3b9bd3760b690e8af Mon Sep 17 00:00:00 2001 From: renovate Date: Wed, 7 Jul 2021 20:38:08 +0000 Subject: [PATCH 230/454] Update dependency sass to v1.35.2 (#579) Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/579 Co-authored-by: renovate Co-committed-by: renovate --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 249cd7dd3..38208a235 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "lodash": "4.17.21", "marked": "2.1.3", "register-service-worker": "1.7.2", - "sass": "1.35.1", + "sass": "1.35.2", "snake-case": "3.0.4", "verte": "0.0.12", "vue": "2.6.14", diff --git a/yarn.lock b/yarn.lock index 468d4f350..027630515 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11870,10 +11870,10 @@ sass-loader@10.2.0: schema-utils "^3.0.0" semver "^7.3.2" -sass@1.35.1: - version "1.35.1" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.35.1.tgz#90ecf774dfe68f07b6193077e3b42fb154b9e1cd" - integrity sha512-oCisuQJstxMcacOPmxLNiLlj4cUyN2+8xJnG7VanRoh2GOLr9RqkvI4AxA4a6LHVg/rsu+PmxXeGhrdSF9jCiQ== +sass@1.35.2: + version "1.35.2" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.35.2.tgz#b732314fcdaf7ef8d0f1698698adc378043cb821" + integrity sha512-jhO5KAR+AMxCEwIH3v+4zbB2WB0z67V1X0jbapfVwQQdjHZUGUyukpnoM6+iCMfsIUC016w9OPKQ5jrNOS9uXw== dependencies: chokidar ">=3.0.0 <4.0.0" From 21e96a08c9220ac42d4875c7f16d1b6e2f23e70d Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 7 Jul 2021 22:39:35 +0200 Subject: [PATCH 231/454] Change the docker builder image to a working one on arm --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index f83b1adae..d73a4fe80 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Stage 1: Build application -FROM node:16.4.2 AS compile-image +FROM node:16 AS compile-image WORKDIR /build From 096fc0bbc84ff14ca459cd0044baf971f8e45d40 Mon Sep 17 00:00:00 2001 From: kolaente Date: Fri, 9 Jul 2021 09:48:33 +0200 Subject: [PATCH 232/454] Fix tests failing on thursdays --- src/helpers/parseTaskText.test.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/helpers/parseTaskText.test.js b/src/helpers/parseTaskText.test.js index 459ad628d..94382558f 100644 --- a/src/helpers/parseTaskText.test.js +++ b/src/helpers/parseTaskText.test.js @@ -168,7 +168,6 @@ describe('Parse Task Text', () => { const nextThursday = new Date() nextThursday.setDate(nextThursday.getDate() + ((4 + 7 - nextThursday.getDay()) % 7)) expect(`${result.date.getFullYear()}-${result.date.getMonth()}-${result.date.getDate()}`).toBe(`${nextThursday.getFullYear()}-${nextThursday.getMonth()}-${nextThursday.getDate()}`) - expect(+new Date(result.date)).toBeGreaterThan(+new Date() - 10) // In on thursdays, this may be different by one second and thus fails the test }) it('should recognize weekdays with time', () => { const result = parseTaskText('Lorem Ipsum thu at 14:00') @@ -178,7 +177,6 @@ describe('Parse Task Text', () => { nextThursday.setDate(nextThursday.getDate() + ((4 + 7 - nextThursday.getDay()) % 7)) expect(`${result.date.getFullYear()}-${result.date.getMonth()}-${result.date.getDate()}`).toBe(`${nextThursday.getFullYear()}-${nextThursday.getMonth()}-${nextThursday.getDate()}`) expect(`${result.date.getHours()}:${result.date.getMinutes()}`).toBe('14:0') - expect(+new Date(result.date)).toBeGreaterThan(+new Date() - 10) // In on thursdays, this may be different by one second and thus fails the test }) it('should recognize dates of the month in the past but next month', () => { const date = new Date() From 7355204d2f7367a52d27b5079087b231c6caf5ae Mon Sep 17 00:00:00 2001 From: andreymal Date: Fri, 9 Jul 2021 08:22:20 +0000 Subject: [PATCH 233/454] Improve some translations (#581) Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/581 Reviewed-by: konrad Co-authored-by: andreymal Co-committed-by: andreymal --- src/components/home/navigation.vue | 6 +++--- src/components/home/topNavigation.vue | 2 +- src/components/input/editor.vue | 6 +++--- src/components/list/partials/filters.vue | 2 +- src/components/notifications/notifications.vue | 2 +- src/components/sharing/linkSharing.vue | 2 +- src/helpers/getListTitle.js | 6 ++++++ src/helpers/getNamespaceTitle.js | 12 ++++++++++++ src/i18n/lang/en.json | 18 ++++++++++++++++++ src/main.js | 8 ++++++++ src/store/index.js | 5 +---- src/views/list/ShowList.vue | 5 +++-- src/views/namespaces/ListNamespaces.vue | 2 +- src/views/tasks/TaskDetailView.vue | 4 ++-- 14 files changed, 61 insertions(+), 19 deletions(-) create mode 100644 src/helpers/getListTitle.js create mode 100644 src/helpers/getNamespaceTitle.js diff --git a/src/components/home/navigation.vue b/src/components/home/navigation.vue index 65b045088..ed91c1771 100644 --- a/src/components/home/navigation.vue +++ b/src/components/home/navigation.vue @@ -54,14 +54,14 @@ + v-tooltip="getNamespaceTitle(n) + ' (' + n.lists.filter(l => !l.isArchived).length + ')'"> - {{ n.title }} ({{ n.lists.filter(l => !l.isArchived).length }}) + {{ getNamespaceTitle(n) }} ({{ n.lists.filter(l => !l.isArchived).length }}) - {{ l.title }} + {{ getListTitle(l) }} - {{ currentList.title === '' ? $t('misc.loading') : currentList.title }} + {{ currentList.title === '' ? $t('misc.loading') : getListTitle(currentList) }} diff --git a/src/components/input/editor.vue b/src/components/input/editor.vue index 068799901..4b9ced964 100644 --- a/src/components/input/editor.vue +++ b/src/components/input/editor.vue @@ -35,15 +35,15 @@

{{ emptyText }}

  • diff --git a/src/components/list/partials/filters.vue b/src/components/list/partials/filters.vue index 1bb586593..b2f54f30f 100644 --- a/src/components/list/partials/filters.vue +++ b/src/components/list/partials/filters.vue @@ -130,7 +130,7 @@
    - +
    - Notifications + {{ $t('notification.title') }}
    diff --git a/src/helpers/getListTitle.js b/src/helpers/getListTitle.js new file mode 100644 index 000000000..35c943e32 --- /dev/null +++ b/src/helpers/getListTitle.js @@ -0,0 +1,6 @@ +export const getListTitle = (l, $t) => { + if (l.id === -1) { + return $t('list.pseudo.favorites.title'); + } + return l.title; +} diff --git a/src/helpers/getNamespaceTitle.js b/src/helpers/getNamespaceTitle.js new file mode 100644 index 000000000..21fe7b7e4 --- /dev/null +++ b/src/helpers/getNamespaceTitle.js @@ -0,0 +1,12 @@ +export const getNamespaceTitle = (n, $t) => { + if (n.id === -1) { + return $t('namespace.pseudo.sharedLists.title'); + } + if (n.id === -2) { + return $t('namespace.pseudo.favorites.title'); + } + if (n.id === -3) { + return $t('namespace.pseudo.savedFilters.title'); + } + return n.title; +} diff --git a/src/i18n/lang/en.json b/src/i18n/lang/en.json index fe007a663..889ed5e9a 100644 --- a/src/i18n/lang/en.json +++ b/src/i18n/lang/en.json @@ -245,6 +245,11 @@ "bucketTitleSavedSuccess": "The bucket title has been saved successfully.", "bucketLimitSavedSuccess": "The bucket limit been saved successfully.", "collapse": "Collapse this bucket" + }, + "pseudo": { + "favorites": { + "title": "Favorites" + } } }, "namespace": { @@ -294,6 +299,17 @@ "color": "Color", "archived": "Is Archived", "isArchived": "This namespace is archived" + }, + "pseudo": { + "sharedLists": { + "title": "Shared Lists" + }, + "favorites": { + "title": "Favorites" + }, + "savedFilters": { + "title": "Filters" + } } }, "filters": { @@ -414,6 +430,7 @@ "chooseDate": "Choose a date" }, "editor": { + "edit": "Edit", "done": "Done", "heading1": "Heading 1", "heading2": "Heading 2", @@ -716,6 +733,7 @@ "contact": "contact us" }, "notification": { + "title": "Notifications", "none": "You don't have any notifications. Have a nice day!", "explainer": "Notifications will appear here when actions on namespaces, lists or tasks you subscribed to happen." }, diff --git a/src/main.js b/src/main.js index ce389dcf0..7475e9575 100644 --- a/src/main.js +++ b/src/main.js @@ -85,6 +85,8 @@ import vueShortkey from 'vue-shortkey' import message from './message' import {colorIsDark} from './helpers/color/colorIsDark' import {setTitle} from './helpers/setTitle' +import {getNamespaceTitle} from './helpers/getNamespaceTitle' +import {getListTitle} from './helpers/getListTitle' // Vuex import {store} from './store' // i18n @@ -199,6 +201,12 @@ Vue.mixin({ formatDateShort(date) { return formatDate(date, 'PPpp', this.$t('date.locale')) }, + getNamespaceTitle(n) { + return getNamespaceTitle(n, p => this.$t(p)) + }, + getListTitle(l) { + return getListTitle(l, p => this.$t(p)) + }, error(e, actions = []) { return message.error(e, this, p => this.$t(p), actions) }, diff --git a/src/store/index.js b/src/store/index.js index dbe860673..ef235f649 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -20,7 +20,6 @@ import attachments from './modules/attachments' import labels from './modules/labels' import ListService from '../services/list' -import {setTitle} from '@/helpers/setTitle' Vue.use(Vuex) @@ -69,8 +68,6 @@ export const store = new Vuex.Store({ return } - setTitle(currentList.title) - // Not sure if this is the right way to do it but hey, it works if ( // List changed @@ -140,4 +137,4 @@ export const store = new Vuex.Store({ state.quickActionsActive = active }, }, -}) \ No newline at end of file +}) diff --git a/src/views/list/ShowList.vue b/src/views/list/ShowList.vue index e2ec94c3d..086e0bcdd 100644 --- a/src/views/list/ShowList.vue +++ b/src/views/list/ShowList.vue @@ -97,7 +97,7 @@ export default { saveListToHistory(listData) - this.setTitle(this.currentList.title) + this.setTitle(this.currentList.id ? this.getListTitle(this.currentList) : '') // This invalidates the loaded list at the kanban board which lets it reload its content when // switched to it. This ensures updates done to tasks in the gantt or list views are consistently @@ -144,6 +144,7 @@ export default { .then(r => { this.$set(this, 'list', r) this.$store.commit(CURRENT_LIST, r) + this.setTitle(this.getListTitle(r)) }) .catch(e => { this.error(e) @@ -154,4 +155,4 @@ export default { }, }, } - \ No newline at end of file + diff --git a/src/views/namespaces/ListNamespaces.vue b/src/views/namespaces/ListNamespaces.vue index d8fe0fd59..1f520816c 100644 --- a/src/views/namespaces/ListNamespaces.vue +++ b/src/views/namespaces/ListNamespaces.vue @@ -39,7 +39,7 @@

    - {{ n.title }} + {{ getNamespaceTitle(n) }} {{ $t('namespace.archived') }} diff --git a/src/views/tasks/TaskDetailView.vue b/src/views/tasks/TaskDetailView.vue index 6330e25b6..7092b38e8 100644 --- a/src/views/tasks/TaskDetailView.vue +++ b/src/views/tasks/TaskDetailView.vue @@ -3,9 +3,9 @@
    - {{ parent.namespace.title }} > + {{ getNamespaceTitle(parent.namespace) }} > - {{ parent.list.title }} + {{ getListTitle(parent.list) }}
    From e3d7ebf1b30c5f4af078fdcfc0da59019d15dd32 Mon Sep 17 00:00:00 2001 From: renovate Date: Fri, 9 Jul 2021 08:44:34 +0000 Subject: [PATCH 234/454] Update dependency highlight.js to v11.1.0 (#582) Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/582 Co-authored-by: renovate Co-committed-by: renovate --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 38208a235..d197039a4 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "copy-to-clipboard": "3.3.1", "date-fns": "2.22.1", "dompurify": "2.3.0", - "highlight.js": "11.0.1", + "highlight.js": "11.1.0", "lodash": "4.17.21", "marked": "2.1.3", "register-service-worker": "1.7.2", diff --git a/yarn.lock b/yarn.lock index 027630515..dce3dab4f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7571,10 +7571,10 @@ hex-color-regex@^1.1.0: resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== -highlight.js@11.0.1: - version "11.0.1" - resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.0.1.tgz#a78bafccd9aa297978799fe5eed9beb7ee1ef887" - integrity sha512-EqYpWyTF2s8nMfttfBA2yLKPNoZCO33pLS4MnbXQ4hECf1TKujCt1Kq7QAdrio7roL4+CqsfjqwYj4tYgq0pJQ== +highlight.js@11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.1.0.tgz#0198f7326e64ddfbea5f76b00e84ab542cf24ae8" + integrity sha512-X9VVhYKHQPPuwffO8jk4bP/FVj+ibNCy3HxZZNDXFtJrq4O5FdcdCDRIkDis5MiMnjh7UwEdHgRZJcHFYdzDdA== highlight.js@^9.6.0: version "9.17.1" From 2854d3a0c2b45b02303d030c608ebc0cce9b6021 Mon Sep 17 00:00:00 2001 From: kolaente Date: Fri, 9 Jul 2021 17:43:49 +0200 Subject: [PATCH 235/454] Fix table text alignment in task detail page Fixes #585 --- src/styles/components/task.scss | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/styles/components/task.scss b/src/styles/components/task.scss index a403c37c9..d5d214987 100644 --- a/src/styles/components/task.scss +++ b/src/styles/components/task.scss @@ -83,15 +83,6 @@ } } - table { - table-layout: fixed; - - td { - overflow: hidden; - word-break: break-all; - } - } - .details { padding-bottom: 0.75rem; flex-flow: row wrap; From eac4f88a65b24b8f79b9af3d34ba3b2adf6d7b8e Mon Sep 17 00:00:00 2001 From: kolaente Date: Fri, 9 Jul 2021 18:04:49 +0200 Subject: [PATCH 236/454] Don't load already loaded task attachments again when saving an edited task description --- src/components/input/editor.vue | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/components/input/editor.vue b/src/components/input/editor.vue index 4b9ced964..fe857caad 100644 --- a/src/components/input/editor.vue +++ b/src/components/input/editor.vue @@ -1,7 +1,7 @@