Compare commits

..

30 Commits

Author SHA1 Message Date
renovate 1653de7198 fix(deps): update dependency @intlify/unplugin-vue-i18n to v4
continuous-integration/drone/pr Build is passing Details
2024-04-08 08:09:44 +00:00
Raymi306 1adaa73141 docs: fix build-from-sources docs mistake (#2251)
continuous-integration/drone/push Build is passing Details
While attempting to build on OpenBSD without having built the frontend, I ran into the following error:

`frontend/embed.go:21:12: pattern dist: no matching files found`

I saw in the docs to create a directory and touch a file, this resulted in a second error:

`frontend/embed.go:21:12: pattern dist: cannot embed directory dist: contains no embeddable files`

Creating the index.html file inside the new directory allowed me to build Vikunja

Reviewed-on: #2251
Co-authored-by: Raymi306 <raymi306@gmail.com>
Co-committed-by: Raymi306 <raymi306@gmail.com>
2024-04-08 07:48:12 +00:00
renovate 3e77e3043e chore(deps): update dev-dependencies
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is failing Details
2024-04-08 07:09:20 +00:00
kolaente d082c0399d
fix(test): visit one more project in project history test
continuous-integration/drone/push Build is passing Details
2024-04-07 22:36:09 +02:00
kolaente 0b9ef27d04
fix(migration): show correct message after starting a migration
continuous-integration/drone/push Build is failing Details
Related to https://github.com/go-vikunja/vikunja/issues/238
2024-04-07 15:11:59 +02:00
kolaente 7acd1a7e51
fix(project): remove child projects from state when deleting a project
continuous-integration/drone/push Build is failing Details
2024-04-07 15:03:18 +02:00
kolaente 8bee5aa806
fix(project): return the full project when setting a background
continuous-integration/drone/push Build is failing Details
Related to #2246
2024-04-07 14:53:57 +02:00
kolaente 6641cbebc2
fix(project): save the last 6 projects in history, show only 5 on desktop
continuous-integration/drone/push Build is failing Details
The project grid on the home page with the recently visited projects now contains an even number of projects which makes for a much nicer grid (because it's now uniform).
2024-04-07 14:34:18 +02:00
kolaente 5892622676
fix(notifications): rendering of plaintext mails
continuous-integration/drone/push Build is passing Details
2024-04-07 14:12:44 +02:00
kolaente 191a476823
fix(notifications): only sanitze html content in notifications, do not convert it to markdown
continuous-integration/drone/push Build is failing Details
Resolves https://community.vikunja.io/t/trello-import-html-mails/2197
2024-04-07 13:34:53 +02:00
renovate c146b72d64 chore(deps): update golangci/golangci-lint docker tag to v1.57.2 (#2225)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #2225
Reviewed-by: konrad <k@knt.li>
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2024-04-07 11:09:14 +00:00
kolaente ca33c0b2bc
fix: drop bucket index before recreating it
continuous-integration/drone/push Build is failing Details
Resolves #2243
2024-04-07 12:50:42 +02:00
kolaente 4d78ae7fa8
chore(dev): move nix flake to top level, add api tooling
continuous-integration/drone/push Build is passing Details
2024-04-07 12:16:13 +02:00
kolaente c1d06c5e5a
fix(projects): do not return parent project id of parents where the user does not have access
continuous-integration/drone/push Build is failing Details
This caused the frontend to not show such projects, throwing errors in the process and sometimes made it hang.
2024-04-07 12:10:20 +02:00
kolaente f1c3ce5eeb
fix(projects): allow arbitrary nesting of new projects 2024-04-07 12:00:39 +02:00
kolaente 2f6b395334
feat(kanban): set task position to 0 (top) when it is moved into the done bucket automatically after marking it done
continuous-integration/drone/push Build is passing Details
2024-04-06 14:35:05 +02:00
kolaente 1cd5dd2b2f
fix: lint
continuous-integration/drone/push Build is passing Details
2024-04-06 14:12:08 +02:00
kolaente 521300613f
fix: update task in typesense when adding a label or assignee to them
continuous-integration/drone/push Build is failing Details
Resolves https://community.vikunja.io/t/typesense-only-works-if-i-re-index/2212
2024-04-06 14:04:04 +02:00
kolaente 037022e857
fix: do not try to fetch nonexistant bucket
continuous-integration/drone/push Build is failing Details
2024-04-06 13:55:11 +02:00
kolaente ec1ff80791
fix(kanban): save done and default bucket on the view and not on the project
The frontend was still trying to update the two in the project which won't work since they are now saved at the view level, not the project.
2024-04-06 13:32:54 +02:00
kolaente 7b8fab33a5
fix(kanban): Make sure all saved taskBucket positions are saved with their project view id
continuous-integration/drone/push Build is passing Details
When the tasks were migrated from belonging directly to a bucket to only belonging to a view, I forgot to add the view in that migration, resulting in task buckets where the view was 0. These entries were not deleted when a task was moved between buckets, but the new task bucket relation nevertheless inserted. This resulted in tasks showing up multiple times on the kanban board.

This change adds a new migration which adds the correct project view id (as derived from the bucket) and fixes the old migration as well.

Resolves https://community.vikunja.io/t/no-longer-able-to-properly-move-tasks-between-kanban-columns/2175
2024-04-06 13:04:36 +02:00
kolaente e0417c8bda
docs: add Korganizer to supported caldav clients
continuous-integration/drone/push Build is passing Details
2024-04-06 12:15:08 +02:00
kolaente 6fbd24d5f6
fix(filter): move spaces out of button to after the matched filter value to prevent removal of spaces
continuous-integration/drone/push Build is failing Details
2024-04-06 12:08:58 +02:00
kolaente e534a6a5bf
fix(modal): do not set p in modal card as flex
This fixes a bug where the description of a project or filter would be aligned right.
2024-04-06 12:08:58 +02:00
kolaente bf85cb0505
fix(filters): always show filter values in a readable color 2024-04-06 12:08:57 +02:00
kolaente 20e2314128
fix(filters): enclose values with a slash in them as strings so that date math values work
Previously, in a filter like "due_date = now/d", the / was parsed as the beginning of a comment, but as it did not contain the full value, this is an invalid comment, resulting in an error message.

Resolves https://community.vikunja.io/t/filter-setting-s/1791/12
2024-04-06 12:08:57 +02:00
kolaente 1ebb551864
fix(filters): make sure the same filter attribute is transformed in all instances
Resolves https://community.vikunja.io/t/filter-setting-s/1791/13
2024-04-06 12:08:57 +02:00
renovate 30c1a46ed4 fix(deps): update src.techknowlogick.com/xgo digest to e01c4fb
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is failing Details
2024-04-06 09:07:13 +00:00
kolaente 1910f69392
fix(test): correctly mock localstorage in unit tests
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-04-06 10:34:41 +02:00
renovate fe4a093825 chore(deps): update dev-dependencies
continuous-integration/drone/pr Build is failing Details
2024-04-06 00:07:33 +00:00
45 changed files with 541 additions and 279 deletions

View File

@ -139,7 +139,7 @@ steps:
event: [ push, tag, pull_request ]
- name: api-lint
image: golangci/golangci-lint:v1.56.2
image: golangci/golangci-lint:v1.57.2
pull: always
environment:
GOPROXY: 'https://goproxy.kolaente.de'
@ -1400,6 +1400,6 @@ steps:
- failure
---
kind: signature
hmac: c312afe632177a2d45f47c429bf6c7528af3c51a097430956558532ccdcc42b9
hmac: ad1d7014fb230dd4ced032ea8995b7a7dc18fcc7cf0805ee38dcbcd6413325af
...

1
.gitignore vendored
View File

@ -28,3 +28,4 @@ vendor/
os-packages/
mage_output_file.go
mage-static
.direnv/

View File

@ -18,6 +18,7 @@ linters:
- scopelint # Obsolete, using exportloopref instead
- durationcheck
- goconst
- musttag
presets:
- bugs
- unused

View File

@ -35,7 +35,7 @@ That means compiling it boils down to these steps:
1. Make sure [Go](https://golang.org/doc/install) is properly installed on your system. You'll need at least Go `1.21`.
2. Make sure [Mage](https://magefile.org) is properly installed on your system.
3. If you did not build the frontend in the steps before, you need to either do that or create a dummy index file with `mkdir -p frontend/dist && touch index.html`.
3. If you did not build the frontend in the steps before, you need to either do that or create a dummy index file with `mkdir -p frontend/dist && touch frontend/dist/index.html`.
4. Run `mage build` in the source of the main repo. This will build a binary in the root of the repo which will be able to run on your system.
### Build for different architectures

View File

@ -72,6 +72,7 @@ Vikunja **currently does not** support these properties:
* [Evolution](https://wiki.gnome.org/Apps/Evolution/)
* [OpenTasks](https://opentasks.app/) & [DAVx⁵](https://www.davx5.com/)
* [Tasks (Android)](https://tasks.org/)
* [Korganizer](https://apps.kde.org/korganizer/)
### Not working

View File

@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1701336116,
"narHash": "sha256-kEmpezCR/FpITc6yMbAh4WrOCiT2zg5pSjnKrq51h5Y=",
"lastModified": 1712449641,
"narHash": "sha256-U9DDWMexN6o5Td2DznEgguh8TRIUnIl9levmit43GcI=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "f5c27c6136db4d76c30e533c20517df6864c46ee",
"rev": "600b15aea1b36eeb43833a50b0e96579147099ff",
"type": "github"
},
"original": {

18
flake.nix Normal file
View File

@ -0,0 +1,18 @@
{
description = "Vikunja dev environment";
outputs = { self, nixpkgs }:
let pkgs = nixpkgs.legacyPackages.x86_64-linux;
in {
defaultPackage.x86_64-linux =
pkgs.mkShell { buildInputs = with pkgs; [
# General tools
git-cliff
# Frontend tools
nodePackages.pnpm cypress
# API tools
go golangci-lint mage
];
};
};
}

1
frontend/.gitignore vendored
View File

@ -13,7 +13,6 @@ node_modules
/dist*
coverage
*.zip
.direnv/
# Test files
cypress/screenshots

View File

@ -12,7 +12,7 @@ describe('Project History', () => {
cy.intercept(Cypress.env('API_URL') + '/projects*').as('loadProjectArray')
cy.intercept(Cypress.env('API_URL') + '/projects/*').as('loadProject')
const projects = ProjectFactory.create(6)
const projects = ProjectFactory.create(7)
ProjectViewFactory.truncate()
projects.forEach(p => ProjectViewFactory.create(1, {
id: p.id,
@ -36,6 +36,8 @@ describe('Project History', () => {
cy.wait('@loadProject')
cy.visit(`/projects/${projects[5].id}/${projects[5].id}`)
cy.wait('@loadProject')
cy.visit(`/projects/${projects[6].id}/${projects[6].id}`)
cy.wait('@loadProject')
// cy.visit('/')
// Not using cy.visit here to work around the redirect issue fixed in #1337
@ -52,5 +54,6 @@ describe('Project History', () => {
.should('contain', projects[3].title)
.should('contain', projects[4].title)
.should('contain', projects[5].title)
.should('contain', projects[6].title)
})
})

View File

@ -1,10 +0,0 @@
{
description = "Vikunja frontend dev environment";
outputs = { self, nixpkgs }:
let pkgs = nixpkgs.legacyPackages.x86_64-linux;
in {
defaultPackage.x86_64-linux =
pkgs.mkShell { buildInputs = [ pkgs.nodePackages.pnpm pkgs.cypress pkgs.git-cliff ]; };
};
}

View File

@ -142,7 +142,7 @@
"@types/is-touch-device": "1.0.2",
"@types/lodash.debounce": "4.0.9",
"@types/marked": "5.0.2",
"@types/node": "20.12.4",
"@types/node": "20.12.5",
"@types/postcss-preset-env": "7.7.0",
"@types/sortablejs": "1.15.8",
"@typescript-eslint/eslint-plugin": "7.5.0",
@ -154,21 +154,21 @@
"@vue/tsconfig": "0.5.1",
"autoprefixer": "10.4.19",
"browserslist": "4.23.0",
"caniuse-lite": "1.0.30001606",
"caniuse-lite": "1.0.30001607",
"css-has-pseudo": "6.0.3",
"csstype": "3.1.3",
"cypress": "13.7.2",
"esbuild": "0.20.2",
"eslint": "8.57.0",
"eslint-plugin-vue": "9.24.0",
"happy-dom": "14.3.2",
"happy-dom": "14.7.1",
"histoire": "0.17.15",
"postcss": "8.4.38",
"postcss-easing-gradients": "3.0.1",
"postcss-easings": "4.0.0",
"postcss-focus-within": "8.0.1",
"postcss-preset-env": "9.5.4",
"rollup": "4.14.0",
"rollup": "4.14.1",
"rollup-plugin-visualizer": "5.12.0",
"sass": "1.74.1",
"start-server-and-test": "2.0.3",
@ -179,7 +179,7 @@
"vite-plugin-sentry": "1.4.0",
"vite-svg-loader": "5.1.0",
"vitest": "1.4.0",
"vue-tsc": "2.0.10",
"vue-tsc": "2.0.11",
"wait-on": "7.2.0",
"workbox-cli": "7.0.0"
},

View File

@ -33,7 +33,7 @@ dependencies:
version: 2.3.1(dayjs@1.11.10)(vue@3.4.21)
'@intlify/unplugin-vue-i18n':
specifier: 4.0.0
version: 4.0.0(rollup@4.14.0)(vue-i18n@9.10.2)
version: 4.0.0(rollup@4.14.1)(vue-i18n@9.10.2)
'@kyvg/vue3-notification':
specifier: 3.2.1
version: 3.2.1(vue@3.4.21)
@ -283,8 +283,8 @@ devDependencies:
specifier: 5.0.2
version: 5.0.2
'@types/node':
specifier: 20.12.4
version: 20.12.4
specifier: 20.12.5
version: 20.12.5
'@types/postcss-preset-env':
specifier: 7.7.0
version: 7.7.0
@ -319,8 +319,8 @@ devDependencies:
specifier: 4.23.0
version: 4.23.0
caniuse-lite:
specifier: 1.0.30001606
version: 1.0.30001606
specifier: 1.0.30001607
version: 1.0.30001607
css-has-pseudo:
specifier: 6.0.3
version: 6.0.3(postcss@8.4.38)
@ -340,11 +340,11 @@ devDependencies:
specifier: 9.24.0
version: 9.24.0(eslint@8.57.0)
happy-dom:
specifier: 14.3.2
version: 14.3.2
specifier: 14.7.1
version: 14.7.1
histoire:
specifier: 0.17.15
version: 0.17.15(@types/node@20.12.4)(sass@1.74.1)(terser@5.24.0)(vite@5.2.8)
version: 0.17.15(@types/node@20.12.5)(sass@1.74.1)(terser@5.24.0)(vite@5.2.8)
postcss:
specifier: 8.4.38
version: 8.4.38
@ -361,11 +361,11 @@ devDependencies:
specifier: 9.5.4
version: 9.5.4(postcss@8.4.38)
rollup:
specifier: 4.14.0
version: 4.14.0
specifier: 4.14.1
version: 4.14.1
rollup-plugin-visualizer:
specifier: 5.12.0
version: 5.12.0(rollup@4.14.0)
version: 5.12.0(rollup@4.14.1)
sass:
specifier: 1.74.1
version: 1.74.1
@ -377,7 +377,7 @@ devDependencies:
version: 5.4.4
vite:
specifier: 5.2.8
version: 5.2.8(@types/node@20.12.4)(sass@1.74.1)(terser@5.24.0)
version: 5.2.8(@types/node@20.12.5)(sass@1.74.1)(terser@5.24.0)
vite-plugin-inject-preload:
specifier: 1.3.3
version: 1.3.3(vite@5.2.8)
@ -392,10 +392,10 @@ devDependencies:
version: 5.1.0(vue@3.4.21)
vitest:
specifier: 1.4.0
version: 1.4.0(@types/node@20.12.4)(happy-dom@14.3.2)(sass@1.74.1)(terser@5.24.0)
version: 1.4.0(@types/node@20.12.5)(happy-dom@14.7.1)(sass@1.74.1)(terser@5.24.0)
vue-tsc:
specifier: 2.0.10
version: 2.0.10(typescript@5.4.4)
specifier: 2.0.11
version: 2.0.11(typescript@5.4.4)
wait-on:
specifier: 7.2.0
version: 7.2.0(debug@4.3.4)
@ -2529,7 +2529,7 @@ packages:
capture-website: 2.4.1
defu: 6.1.3
fs-extra: 10.1.0
histoire: 0.17.15(@types/node@20.12.4)(sass@1.74.1)(terser@5.24.0)(vite@5.2.8)
histoire: 0.17.15(@types/node@20.12.5)(sass@1.74.1)(terser@5.24.0)(vite@5.2.8)
pathe: 1.1.1
transitivePeerDependencies:
- bufferutil
@ -2549,7 +2549,7 @@ packages:
'@histoire/vendors': 0.17.15
change-case: 4.1.2
globby: 13.2.2
histoire: 0.17.15(@types/node@20.12.4)(sass@1.74.1)(terser@5.24.0)(vite@5.2.8)
histoire: 0.17.15(@types/node@20.12.5)(sass@1.74.1)(terser@5.24.0)(vite@5.2.8)
launch-editor: 2.6.1
pathe: 1.1.1
vue: 3.4.21(typescript@5.4.4)
@ -2568,7 +2568,7 @@ packages:
chokidar: 3.5.3
pathe: 1.1.1
picocolors: 1.0.0
vite: 5.2.8(@types/node@20.12.4)(sass@1.74.1)(terser@5.24.0)
vite: 5.2.8(@types/node@20.12.5)(sass@1.74.1)(terser@5.24.0)
dev: true
/@histoire/vendors@0.17.15:
@ -2653,7 +2653,7 @@ packages:
engines: {node: '>= 16'}
dev: false
/@intlify/unplugin-vue-i18n@4.0.0(rollup@4.14.0)(vue-i18n@9.10.2):
/@intlify/unplugin-vue-i18n@4.0.0(rollup@4.14.1)(vue-i18n@9.10.2):
resolution: {integrity: sha512-q2Mhqa/mLi0tulfLFO4fMXXvEbkSZpI5yGhNNsLTNJJ41icEGUuyDe+j5zRZIKSkOJRgX6YbCyibTDJdRsukmw==}
engines: {node: '>= 14.16'}
peerDependencies:
@ -2670,7 +2670,7 @@ packages:
dependencies:
'@intlify/bundle-utils': 8.0.0(vue-i18n@9.10.2)
'@intlify/shared': 9.10.2
'@rollup/pluginutils': 5.1.0(rollup@4.14.0)
'@rollup/pluginutils': 5.1.0(rollup@4.14.1)
'@vue/compiler-sfc': 3.4.21
debug: 4.3.4(supports-color@8.1.1)
fast-glob: 3.3.2
@ -2909,7 +2909,7 @@ packages:
rollup: 2.79.1
dev: true
/@rollup/pluginutils@5.1.0(rollup@4.14.0):
/@rollup/pluginutils@5.1.0(rollup@4.14.1):
resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==}
engines: {node: '>=14.0.0'}
peerDependencies:
@ -2921,109 +2921,109 @@ packages:
'@types/estree': 1.0.5
estree-walker: 2.0.2
picomatch: 2.3.1
rollup: 4.14.0
rollup: 4.14.1
dev: false
/@rollup/rollup-android-arm-eabi@4.14.0:
resolution: {integrity: sha512-jwXtxYbRt1V+CdQSy6Z+uZti7JF5irRKF8hlKfEnF/xJpcNGuuiZMBvuoYM+x9sr9iWGnzrlM0+9hvQ1kgkf1w==}
/@rollup/rollup-android-arm-eabi@4.14.1:
resolution: {integrity: sha512-fH8/o8nSUek8ceQnT7K4EQbSiV7jgkHq81m9lWZFIXjJ7lJzpWXbQFpT/Zh6OZYnpFykvzC3fbEvEAFZu03dPA==}
cpu: [arm]
os: [android]
requiresBuild: true
optional: true
/@rollup/rollup-android-arm64@4.14.0:
resolution: {integrity: sha512-fI9nduZhCccjzlsA/OuAwtFGWocxA4gqXGTLvOyiF8d+8o0fZUeSztixkYjcGq1fGZY3Tkq4yRvHPFxU+jdZ9Q==}
/@rollup/rollup-android-arm64@4.14.1:
resolution: {integrity: sha512-Y/9OHLjzkunF+KGEoJr3heiD5X9OLa8sbT1lm0NYeKyaM3oMhhQFvPB0bNZYJwlq93j8Z6wSxh9+cyKQaxS7PQ==}
cpu: [arm64]
os: [android]
requiresBuild: true
optional: true
/@rollup/rollup-darwin-arm64@4.14.0:
resolution: {integrity: sha512-BcnSPRM76/cD2gQC+rQNGBN6GStBs2pl/FpweW8JYuz5J/IEa0Fr4AtrPv766DB/6b2MZ/AfSIOSGw3nEIP8SA==}
/@rollup/rollup-darwin-arm64@4.14.1:
resolution: {integrity: sha512-+kecg3FY84WadgcuSVm6llrABOdQAEbNdnpi5X3UwWiFVhZIZvKgGrF7kmLguvxHNQy+UuRV66cLVl3S+Rkt+Q==}
cpu: [arm64]
os: [darwin]
requiresBuild: true
optional: true
/@rollup/rollup-darwin-x64@4.14.0:
resolution: {integrity: sha512-LDyFB9GRolGN7XI6955aFeI3wCdCUszFWumWU0deHA8VpR3nWRrjG6GtGjBrQxQKFevnUTHKCfPR4IvrW3kCgQ==}
/@rollup/rollup-darwin-x64@4.14.1:
resolution: {integrity: sha512-2pYRzEjVqq2TB/UNv47BV/8vQiXkFGVmPFwJb+1E0IFFZbIX8/jo1olxqqMbo6xCXf8kabANhp5bzCij2tFLUA==}
cpu: [x64]
os: [darwin]
requiresBuild: true
optional: true
/@rollup/rollup-linux-arm-gnueabihf@4.14.0:
resolution: {integrity: sha512-ygrGVhQP47mRh0AAD0zl6QqCbNsf0eTo+vgwkY6LunBcg0f2Jv365GXlDUECIyoXp1kKwL5WW6rsO429DBY/bA==}
/@rollup/rollup-linux-arm-gnueabihf@4.14.1:
resolution: {integrity: sha512-mS6wQ6Do6/wmrF9aTFVpIJ3/IDXhg1EZcQFYHZLHqw6AzMBjTHWnCG35HxSqUNphh0EHqSM6wRTT8HsL1C0x5g==}
cpu: [arm]
os: [linux]
requiresBuild: true
optional: true
/@rollup/rollup-linux-arm64-gnu@4.14.0:
resolution: {integrity: sha512-x+uJ6MAYRlHGe9wi4HQjxpaKHPM3d3JjqqCkeC5gpnnI6OWovLdXTpfa8trjxPLnWKyBsSi5kne+146GAxFt4A==}
/@rollup/rollup-linux-arm64-gnu@4.14.1:
resolution: {integrity: sha512-p9rGKYkHdFMzhckOTFubfxgyIO1vw//7IIjBBRVzyZebWlzRLeNhqxuSaZ7kCEKVkm/kuC9fVRW9HkC/zNRG2w==}
cpu: [arm64]
os: [linux]
requiresBuild: true
optional: true
/@rollup/rollup-linux-arm64-musl@4.14.0:
resolution: {integrity: sha512-nrRw8ZTQKg6+Lttwqo6a2VxR9tOroa2m91XbdQ2sUUzHoedXlsyvY1fN4xWdqz8PKmf4orDwejxXHjh7YBGUCA==}
/@rollup/rollup-linux-arm64-musl@4.14.1:
resolution: {integrity: sha512-nDY6Yz5xS/Y4M2i9JLQd3Rofh5OR8Bn8qe3Mv/qCVpHFlwtZSBYSPaU4mrGazWkXrdQ98GB//H0BirGR/SKFSw==}
cpu: [arm64]
os: [linux]
requiresBuild: true
optional: true
/@rollup/rollup-linux-powerpc64le-gnu@4.14.0:
resolution: {integrity: sha512-xV0d5jDb4aFu84XKr+lcUJ9y3qpIWhttO3Qev97z8DKLXR62LC3cXT/bMZXrjLF9X+P5oSmJTzAhqwUbY96PnA==}
/@rollup/rollup-linux-powerpc64le-gnu@4.14.1:
resolution: {integrity: sha512-im7HE4VBL+aDswvcmfx88Mp1soqL9OBsdDBU8NqDEYtkri0qV0THhQsvZtZeNNlLeCUQ16PZyv7cqutjDF35qw==}
cpu: [ppc64le]
os: [linux]
requiresBuild: true
optional: true
/@rollup/rollup-linux-riscv64-gnu@4.14.0:
resolution: {integrity: sha512-SDDhBQwZX6LPRoPYjAZWyL27LbcBo7WdBFWJi5PI9RPCzU8ijzkQn7tt8NXiXRiFMJCVpkuMkBf4OxSxVMizAw==}
/@rollup/rollup-linux-riscv64-gnu@4.14.1:
resolution: {integrity: sha512-RWdiHuAxWmzPJgaHJdpvUUlDz8sdQz4P2uv367T2JocdDa98iRw2UjIJ4QxSyt077mXZT2X6pKfT2iYtVEvOFw==}
cpu: [riscv64]
os: [linux]
requiresBuild: true
optional: true
/@rollup/rollup-linux-s390x-gnu@4.14.0:
resolution: {integrity: sha512-RxB/qez8zIDshNJDufYlTT0ZTVut5eCpAZ3bdXDU9yTxBzui3KhbGjROK2OYTTor7alM7XBhssgoO3CZ0XD3qA==}
/@rollup/rollup-linux-s390x-gnu@4.14.1:
resolution: {integrity: sha512-VMgaGQ5zRX6ZqV/fas65/sUGc9cPmsntq2FiGmayW9KMNfWVG/j0BAqImvU4KTeOOgYSf1F+k6at1UfNONuNjA==}
cpu: [s390x]
os: [linux]
requiresBuild: true
optional: true
/@rollup/rollup-linux-x64-gnu@4.14.0:
resolution: {integrity: sha512-C6y6z2eCNCfhZxT9u+jAM2Fup89ZjiG5pIzZIDycs1IwESviLxwkQcFRGLjnDrP+PT+v5i4YFvlcfAs+LnreXg==}
/@rollup/rollup-linux-x64-gnu@4.14.1:
resolution: {integrity: sha512-9Q7DGjZN+hTdJomaQ3Iub4m6VPu1r94bmK2z3UeWP3dGUecRC54tmVu9vKHTm1bOt3ASoYtEz6JSRLFzrysKlA==}
cpu: [x64]
os: [linux]
requiresBuild: true
optional: true
/@rollup/rollup-linux-x64-musl@4.14.0:
resolution: {integrity: sha512-i0QwbHYfnOMYsBEyjxcwGu5SMIi9sImDVjDg087hpzXqhBSosxkE7gyIYFHgfFl4mr7RrXksIBZ4DoLoP4FhJg==}
/@rollup/rollup-linux-x64-musl@4.14.1:
resolution: {integrity: sha512-JNEG/Ti55413SsreTguSx0LOVKX902OfXIKVg+TCXO6Gjans/k9O6ww9q3oLGjNDaTLxM+IHFMeXy/0RXL5R/g==}
cpu: [x64]
os: [linux]
requiresBuild: true
optional: true
/@rollup/rollup-win32-arm64-msvc@4.14.0:
resolution: {integrity: sha512-Fq52EYb0riNHLBTAcL0cun+rRwyZ10S9vKzhGKKgeD+XbwunszSY0rVMco5KbOsTlwovP2rTOkiII/fQ4ih/zQ==}
/@rollup/rollup-win32-arm64-msvc@4.14.1:
resolution: {integrity: sha512-ryS22I9y0mumlLNwDFYZRDFLwWh3aKaC72CWjFcFvxK0U6v/mOkM5Up1bTbCRAhv3kEIwW2ajROegCIQViUCeA==}
cpu: [arm64]
os: [win32]
requiresBuild: true
optional: true
/@rollup/rollup-win32-ia32-msvc@4.14.0:
resolution: {integrity: sha512-e/PBHxPdJ00O9p5Ui43+vixSgVf4NlLsmV6QneGERJ3lnjIua/kim6PRFe3iDueT1rQcgSkYP8ZBBXa/h4iPvw==}
/@rollup/rollup-win32-ia32-msvc@4.14.1:
resolution: {integrity: sha512-TdloItiGk+T0mTxKx7Hp279xy30LspMso+GzQvV2maYePMAWdmrzqSNZhUpPj3CGw12aGj57I026PgLCTu8CGg==}
cpu: [ia32]
os: [win32]
requiresBuild: true
optional: true
/@rollup/rollup-win32-x64-msvc@4.14.0:
resolution: {integrity: sha512-aGg7iToJjdklmxlUlJh/PaPNa4PmqHfyRMLunbL3eaMO0gp656+q1zOKkpJ/CVe9CryJv6tAN1HDoR8cNGzkag==}
/@rollup/rollup-win32-x64-msvc@4.14.1:
resolution: {integrity: sha512-wQGI+LY/Py20zdUPq+XCem7JcPOyzIJBm3dli+56DJsQOHbnXZFEwgmnC6el1TPAfC8lBT3m+z69RmLykNUbew==}
cpu: [x64]
os: [win32]
requiresBuild: true
@ -3556,10 +3556,6 @@ packages:
resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==}
dev: true
/@types/estree@1.0.0:
resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==}
dev: true
/@types/estree@1.0.5:
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
@ -3584,7 +3580,7 @@ packages:
/@types/fs-extra@9.0.13:
resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==}
dependencies:
'@types/node': 20.12.4
'@types/node': 20.12.5
dev: true
/@types/har-format@1.2.10:
@ -3608,7 +3604,7 @@ packages:
/@types/keyv@3.1.3:
resolution: {integrity: sha512-FXCJgyyN3ivVgRoml4h94G/p3kY+u/B86La+QptcqJaWtBWtmc6TtkNfS40n9bIvyLteHh7zXOtgbobORKPbDg==}
dependencies:
'@types/node': 20.12.4
'@types/node': 20.12.5
dev: true
/@types/linkify-it@3.0.2:
@ -3649,8 +3645,8 @@ packages:
resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==}
dev: true
/@types/node@20.12.4:
resolution: {integrity: sha512-E+Fa9z3wSQpzgYQdYmme5X3OTuejnnTx88A6p6vkkJosR3KBz+HpE3kqNm98VE6cfLFcISx7zW7MsJkH6KwbTw==}
/@types/node@20.12.5:
resolution: {integrity: sha512-BD+BjQ9LS/D8ST9p5uqBxghlN+S42iuNxjsUGjeZobe/ciXzk2qb1B6IXc6AnRLS+yFJRpN2IPEHMzwspfDJNw==}
dependencies:
undici-types: 5.26.5
dev: true
@ -3677,13 +3673,13 @@ packages:
/@types/resolve@1.17.1:
resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==}
dependencies:
'@types/node': 20.12.4
'@types/node': 20.12.5
dev: true
/@types/responselike@1.0.0:
resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==}
dependencies:
'@types/node': 20.12.4
'@types/node': 20.12.5
dev: true
/@types/semver@7.5.0:
@ -3705,7 +3701,7 @@ packages:
/@types/tern@0.23.4:
resolution: {integrity: sha512-JAUw1iXGO1qaWwEOzxTKJZ/5JxVeON9kvGZ/osgZaJImBnyjyn0cjovPsf6FNLmyGY8Vw9DoXZCMlfMkMwHRWg==}
dependencies:
'@types/estree': 1.0.0
'@types/estree': 1.0.5
dev: true
/@types/throttle-debounce@2.1.0:
@ -3732,7 +3728,7 @@ packages:
resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==}
requiresBuild: true
dependencies:
'@types/node': 20.12.4
'@types/node': 20.12.5
dev: true
optional: true
@ -3888,7 +3884,7 @@ packages:
regenerator-runtime: 0.14.1
systemjs: 6.14.3
terser: 5.24.0
vite: 5.2.8(@types/node@20.12.4)(sass@1.74.1)(terser@5.24.0)
vite: 5.2.8(@types/node@20.12.5)(sass@1.74.1)(terser@5.24.0)
transitivePeerDependencies:
- supports-color
dev: true
@ -3900,7 +3896,7 @@ packages:
vite: ^5.0.0
vue: ^3.2.25
dependencies:
vite: 5.2.8(@types/node@20.12.4)(sass@1.74.1)(terser@5.24.0)
vite: 5.2.8(@types/node@20.12.5)(sass@1.74.1)(terser@5.24.0)
vue: 3.4.21(typescript@5.4.4)
dev: true
@ -3943,22 +3939,22 @@ packages:
pretty-format: 29.7.0
dev: true
/@volar/language-core@2.2.0-alpha.5:
resolution: {integrity: sha512-RqERQ8HXxKC/HAGpDg7oG/Yg8n3rC3KEnYE3D7lcKIblU59JEZX73IWD/L3fdjzyeSglDWjL91iOblU8MuKEoA==}
/@volar/language-core@2.2.0-alpha.6:
resolution: {integrity: sha512-GmT28LX2w4x82uuQqNN/P94VOCsZRHBbGcGe+5bFtA2hbIbH6f8tFdMfgXFtyhbft/pj6f3xl37xe+t+nomLIA==}
dependencies:
'@volar/source-map': 2.2.0-alpha.5
'@volar/source-map': 2.2.0-alpha.6
dev: true
/@volar/source-map@2.2.0-alpha.5:
resolution: {integrity: sha512-Lw1LOPgt1QGaQX9HstRTlBz5x6d5mGq9ZTFMeyWVr8/5YOv3hCU0ehtMTwmCiAX/ZyNSINFI01ODePy2hwy06A==}
/@volar/source-map@2.2.0-alpha.6:
resolution: {integrity: sha512-EztD2zoUopETY+ZCUZAGUHKgj4gOkY/2WnaOS+RSTc56xm85miSA4qOBS8Lt1Ruu5vV52WIZKHW/R9PbjkZWFA==}
dependencies:
muggle-string: 0.4.1
dev: true
/@volar/typescript@2.2.0-alpha.5:
resolution: {integrity: sha512-9UKZSDTcgvKMXz9TiU1kHmu3uMuH8+M7oZ6/CzBt8LvFda+ec/ZDcvBjQg2rU5EVn4d+YPYcqenkeHre3tO7Og==}
/@volar/typescript@2.2.0-alpha.6:
resolution: {integrity: sha512-wTr0jO3wVXQ9FjBbWE2iX8GgDoiHp1Nttsb+tKk5IeUUb6f1uOjyeIXuS4KfeMBpCufthRO2st2O2uatAs/UXQ==}
dependencies:
'@volar/language-core': 2.2.0-alpha.5
'@volar/language-core': 2.2.0-alpha.6
path-browserify: 1.0.1
dev: true
@ -4025,15 +4021,15 @@ packages:
- supports-color
dev: true
/@vue/language-core@2.0.10(typescript@5.4.4):
resolution: {integrity: sha512-3ULtX6hSPJNdNChi6aJ4FfdJNs5EShBLxnwLFTqrk2N1385WOwGVlbHeS2R6W9s9lXZ0+mC2bv4VlFSyeNPNGA==}
/@vue/language-core@2.0.11(typescript@5.4.4):
resolution: {integrity: sha512-5ivg8Vem/yckzXI3L3n0mdKBPRcHSlsGt6/dpbEx42PcH3MIHAjSAJBYvENXeWJxv2ClQc8BS2mH1Ho2U7jZig==}
peerDependencies:
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
dependencies:
'@volar/language-core': 2.2.0-alpha.5
'@volar/language-core': 2.2.0-alpha.6
'@vue/compiler-dom': 3.4.21
'@vue/shared': 3.4.21
computeds: 0.0.1
@ -4331,7 +4327,7 @@ packages:
postcss: ^8.1.0
dependencies:
browserslist: 4.23.0
caniuse-lite: 1.0.30001606
caniuse-lite: 1.0.30001607
fraction.js: 4.3.7
normalize-range: 0.1.2
picocolors: 1.0.0
@ -4487,7 +4483,7 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
dependencies:
caniuse-lite: 1.0.30001606
caniuse-lite: 1.0.30001607
electron-to-chromium: 1.4.685
node-releases: 2.0.14
update-browserslist-db: 1.0.13(browserslist@4.23.0)
@ -4573,8 +4569,8 @@ packages:
engines: {node: '>=6'}
dev: true
/caniuse-lite@1.0.30001606:
resolution: {integrity: sha512-LPbwnW4vfpJId225pwjZJOgX1m9sGfbw/RKJvw/t0QhYOOaTXHvkjVGFGPpvwEzufrjvTlsULnVTxdy4/6cqkg==}
/caniuse-lite@1.0.30001607:
resolution: {integrity: sha512-WcvhVRjXLKFB/kmOFVwELtMxyhq3iM/MvmXcyCe2PNf166c39mptscOc/45TTS96n2gpNV2z7+NakArTWZCQ3w==}
dev: true
/capital-case@1.0.4:
@ -6228,8 +6224,8 @@ packages:
strip-bom-string: 1.0.0
dev: true
/happy-dom@14.3.2:
resolution: {integrity: sha512-3+e7CNot85v+Sv8LBHmZbWKo6KaHapTVHt/sOg+0PBDPYpGo/yF8qEZN1bjSVqrq/T6kU//RS3703mcIhidWHw==}
/happy-dom@14.7.1:
resolution: {integrity: sha512-v60Q0evZ4clvMcrAh5/F8EdxDdfHdFrtffz/CNe10jKD+nFweZVxM91tW+UyY2L4AtpgIaXdZ7TQmiO1pfcwbg==}
engines: {node: '>=16.0.0'}
dependencies:
entities: 4.5.0
@ -6296,7 +6292,7 @@ packages:
engines: {node: '>=12.0.0'}
dev: false
/histoire@0.17.15(@types/node@20.12.4)(sass@1.74.1)(terser@5.24.0)(vite@5.2.8):
/histoire@0.17.15(@types/node@20.12.5)(sass@1.74.1)(terser@5.24.0)(vite@5.2.8):
resolution: {integrity: sha512-DiRMSIgj340z+zikqf0f3Pj0CTv2/xtdBMBIAO1EARat+QXxMwumbfK41Gi7f9IIBr+UVmomNcwFxVY2EM/vrw==}
hasBin: true
peerDependencies:
@ -6332,8 +6328,8 @@ packages:
sade: 1.8.1
shiki-es: 0.2.0
sirv: 2.0.3
vite: 5.2.8(@types/node@20.12.4)(sass@1.74.1)(terser@5.24.0)
vite-node: 0.34.6(@types/node@20.12.4)(sass@1.74.1)(terser@5.24.0)
vite: 5.2.8(@types/node@20.12.5)(sass@1.74.1)(terser@5.24.0)
vite-node: 0.34.6(@types/node@20.12.5)(sass@1.74.1)(terser@5.24.0)
transitivePeerDependencies:
- '@types/node'
- bufferutil
@ -6779,7 +6775,7 @@ packages:
resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==}
engines: {node: '>= 10.13.0'}
dependencies:
'@types/node': 20.12.4
'@types/node': 20.12.5
merge-stream: 2.0.0
supports-color: 7.2.0
dev: true
@ -8749,7 +8745,7 @@ packages:
- acorn
dev: true
/rollup-plugin-visualizer@5.12.0(rollup@4.14.0):
/rollup-plugin-visualizer@5.12.0(rollup@4.14.1):
resolution: {integrity: sha512-8/NU9jXcHRs7Nnj07PF2o4gjxmm9lXIrZ8r175bT9dK8qoLlvKTwRMArRCMgpMGlq8CTLugRvEmyMeMXIU2pNQ==}
engines: {node: '>=14'}
hasBin: true
@ -8761,7 +8757,7 @@ packages:
dependencies:
open: 8.4.0
picomatch: 2.3.1
rollup: 4.14.0
rollup: 4.14.1
source-map: 0.7.4
yargs: 17.6.0
dev: true
@ -8774,28 +8770,28 @@ packages:
fsevents: 2.3.3
dev: true
/rollup@4.14.0:
resolution: {integrity: sha512-Qe7w62TyawbDzB4yt32R0+AbIo6m1/sqO7UPzFS8Z/ksL5mrfhA0v4CavfdmFav3D+ub4QeAgsGEe84DoWe/nQ==}
/rollup@4.14.1:
resolution: {integrity: sha512-4LnHSdd3QK2pa1J6dFbfm1HN0D7vSK/ZuZTsdyUAlA6Rr1yTouUTL13HaDOGJVgby461AhrNGBS7sCGXXtT+SA==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
dependencies:
'@types/estree': 1.0.5
optionalDependencies:
'@rollup/rollup-android-arm-eabi': 4.14.0
'@rollup/rollup-android-arm64': 4.14.0
'@rollup/rollup-darwin-arm64': 4.14.0
'@rollup/rollup-darwin-x64': 4.14.0
'@rollup/rollup-linux-arm-gnueabihf': 4.14.0
'@rollup/rollup-linux-arm64-gnu': 4.14.0
'@rollup/rollup-linux-arm64-musl': 4.14.0
'@rollup/rollup-linux-powerpc64le-gnu': 4.14.0
'@rollup/rollup-linux-riscv64-gnu': 4.14.0
'@rollup/rollup-linux-s390x-gnu': 4.14.0
'@rollup/rollup-linux-x64-gnu': 4.14.0
'@rollup/rollup-linux-x64-musl': 4.14.0
'@rollup/rollup-win32-arm64-msvc': 4.14.0
'@rollup/rollup-win32-ia32-msvc': 4.14.0
'@rollup/rollup-win32-x64-msvc': 4.14.0
'@rollup/rollup-android-arm-eabi': 4.14.1
'@rollup/rollup-android-arm64': 4.14.1
'@rollup/rollup-darwin-arm64': 4.14.1
'@rollup/rollup-darwin-x64': 4.14.1
'@rollup/rollup-linux-arm-gnueabihf': 4.14.1
'@rollup/rollup-linux-arm64-gnu': 4.14.1
'@rollup/rollup-linux-arm64-musl': 4.14.1
'@rollup/rollup-linux-powerpc64le-gnu': 4.14.1
'@rollup/rollup-linux-riscv64-gnu': 4.14.1
'@rollup/rollup-linux-s390x-gnu': 4.14.1
'@rollup/rollup-linux-x64-gnu': 4.14.1
'@rollup/rollup-linux-x64-musl': 4.14.1
'@rollup/rollup-win32-arm64-msvc': 4.14.1
'@rollup/rollup-win32-ia32-msvc': 4.14.1
'@rollup/rollup-win32-x64-msvc': 4.14.1
fsevents: 2.3.3
/rope-sequence@1.3.4:
@ -9733,7 +9729,7 @@ packages:
extsprintf: 1.3.0
dev: true
/vite-node@0.34.6(@types/node@20.12.4)(sass@1.74.1)(terser@5.24.0):
/vite-node@0.34.6(@types/node@20.12.5)(sass@1.74.1)(terser@5.24.0):
resolution: {integrity: sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==}
engines: {node: '>=v14.18.0'}
hasBin: true
@ -9743,7 +9739,7 @@ packages:
mlly: 1.4.2
pathe: 1.1.1
picocolors: 1.0.0
vite: 5.2.8(@types/node@20.12.4)(sass@1.74.1)(terser@5.24.0)
vite: 5.2.8(@types/node@20.12.5)(sass@1.74.1)(terser@5.24.0)
transitivePeerDependencies:
- '@types/node'
- less
@ -9755,7 +9751,7 @@ packages:
- terser
dev: true
/vite-node@1.4.0(@types/node@20.12.4)(sass@1.74.1)(terser@5.24.0):
/vite-node@1.4.0(@types/node@20.12.5)(sass@1.74.1)(terser@5.24.0):
resolution: {integrity: sha512-VZDAseqjrHgNd4Kh8icYHWzTKSCZMhia7GyHfhtzLW33fZlG9SwsB6CEhgyVOWkJfJ2pFLrp/Gj1FSfAiqH9Lw==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
@ -9764,7 +9760,7 @@ packages:
debug: 4.3.4(supports-color@8.1.1)
pathe: 1.1.1
picocolors: 1.0.0
vite: 5.2.8(@types/node@20.12.4)(sass@1.74.1)(terser@5.24.0)
vite: 5.2.8(@types/node@20.12.5)(sass@1.74.1)(terser@5.24.0)
transitivePeerDependencies:
- '@types/node'
- less
@ -9783,7 +9779,7 @@ packages:
vite: ^3.0.0 || ^4.0.0
dependencies:
mime-types: 2.1.35
vite: 5.2.8(@types/node@20.12.4)(sass@1.74.1)(terser@5.24.0)
vite: 5.2.8(@types/node@20.12.5)(sass@1.74.1)(terser@5.24.0)
dev: true
/vite-plugin-pwa@0.19.8(vite@5.2.8)(workbox-build@7.0.0)(workbox-window@7.0.0):
@ -9801,7 +9797,7 @@ packages:
debug: 4.3.4(supports-color@8.1.1)
fast-glob: 3.3.2
pretty-bytes: 6.1.1
vite: 5.2.8(@types/node@20.12.4)(sass@1.74.1)(terser@5.24.0)
vite: 5.2.8(@types/node@20.12.5)(sass@1.74.1)(terser@5.24.0)
workbox-build: 7.0.0(acorn@8.11.2)
workbox-window: 7.0.0
transitivePeerDependencies:
@ -9815,7 +9811,7 @@ packages:
vite: ^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0
dependencies:
'@sentry/cli': 2.19.1
vite: 5.2.8(@types/node@20.12.4)(sass@1.74.1)(terser@5.24.0)
vite: 5.2.8(@types/node@20.12.5)(sass@1.74.1)(terser@5.24.0)
transitivePeerDependencies:
- encoding
- supports-color
@ -9830,7 +9826,7 @@ packages:
vue: 3.4.21(typescript@5.4.4)
dev: true
/vite@5.2.8(@types/node@20.12.4)(sass@1.74.1)(terser@5.24.0):
/vite@5.2.8(@types/node@20.12.5)(sass@1.74.1)(terser@5.24.0):
resolution: {integrity: sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
@ -9858,17 +9854,17 @@ packages:
terser:
optional: true
dependencies:
'@types/node': 20.12.4
'@types/node': 20.12.5
esbuild: 0.20.2
postcss: 8.4.38
rollup: 4.14.0
rollup: 4.14.1
sass: 1.74.1
terser: 5.24.0
optionalDependencies:
fsevents: 2.3.3
dev: true
/vitest@1.4.0(@types/node@20.12.4)(happy-dom@14.3.2)(sass@1.74.1)(terser@5.24.0):
/vitest@1.4.0(@types/node@20.12.5)(happy-dom@14.7.1)(sass@1.74.1)(terser@5.24.0):
resolution: {integrity: sha512-gujzn0g7fmwf83/WzrDTnncZt2UiXP41mHuFYFrdwaLRVQ6JYQEiME2IfEjU3vcFL3VKa75XhI3lFgn+hfVsQw==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
@ -9893,7 +9889,7 @@ packages:
jsdom:
optional: true
dependencies:
'@types/node': 20.12.4
'@types/node': 20.12.5
'@vitest/expect': 1.4.0
'@vitest/runner': 1.4.0
'@vitest/snapshot': 1.4.0
@ -9903,7 +9899,7 @@ packages:
chai: 4.3.10
debug: 4.3.4(supports-color@8.1.1)
execa: 8.0.1
happy-dom: 14.3.2
happy-dom: 14.7.1
local-pkg: 0.5.0
magic-string: 0.30.7
pathe: 1.1.1
@ -9912,8 +9908,8 @@ packages:
strip-literal: 2.0.0
tinybench: 2.5.1
tinypool: 0.8.2
vite: 5.2.8(@types/node@20.12.4)(sass@1.74.1)(terser@5.24.0)
vite-node: 1.4.0(@types/node@20.12.4)(sass@1.74.1)(terser@5.24.0)
vite: 5.2.8(@types/node@20.12.5)(sass@1.74.1)(terser@5.24.0)
vite-node: 1.4.0(@types/node@20.12.5)(sass@1.74.1)(terser@5.24.0)
why-is-node-running: 2.2.2
transitivePeerDependencies:
- less
@ -10035,14 +10031,14 @@ packages:
he: 1.2.0
dev: true
/vue-tsc@2.0.10(typescript@5.4.4):
resolution: {integrity: sha512-XD9GuUuc40fdL6VrfbFS5PehxK6exhKGEkzCbMjT01HcJVNuJxXaPFIhMEfxn581eryX7LBygAH6YYqnXQGElA==}
/vue-tsc@2.0.11(typescript@5.4.4):
resolution: {integrity: sha512-dl5MEU4VGZdQFGBnKfPpAfV3SQmBDWs9o4YhUPvDmwk+zmb/RprzFJK2sagR6EWazogZhXENvykd3wBXWS9kng==}
hasBin: true
peerDependencies:
typescript: '*'
dependencies:
'@volar/typescript': 2.2.0-alpha.5
'@vue/language-core': 2.0.10(typescript@5.4.4)
'@volar/typescript': 2.2.0-alpha.6
'@vue/language-core': 2.0.11(typescript@5.4.4)
semver: 7.6.0
typescript: 5.4.4
dev: true

View File

@ -26,7 +26,6 @@
:project="project"
:is-loading="projectUpdating[project.id]"
:can-collapse="canCollapse"
:level="level"
:data-project-id="project.id"
/>
</template>
@ -49,7 +48,6 @@ const props = defineProps<{
modelValue?: IProject[],
canEditOrder: boolean,
canCollapse?: boolean,
level?: number,
}>()
const emit = defineEmits<{
(e: 'update:modelValue', projects: IProject[]): void

View File

@ -58,7 +58,6 @@
<ProjectSettingsDropdown
class="menu-list-dropdown"
:project="project"
:level="level"
>
<template #trigger="{toggleOpen}">
<BaseButton
@ -78,7 +77,6 @@
:model-value="childProjects"
:can-edit-order="true"
:can-collapse="canCollapse"
:level="level + 1"
/>
</li>
</template>
@ -101,12 +99,10 @@ const {
project,
isLoading,
canCollapse,
level = 0,
} = defineProps<{
project: IProject,
isLoading?: boolean,
canCollapse?: boolean,
level?: number,
}>()
const projectStore = useProjectStore()

View File

@ -188,12 +188,6 @@ $modal-width: 1024px;
.info {
font-style: italic;
}
p {
display: flex;
justify-content: space-between;
align-items: center;
}
}
}

View File

@ -91,7 +91,15 @@ const highlightedFilterQuery = computed(() => {
value = ''
}
return `${o}${spacesBefore}${token}${spacesAfter}<button class="is-primary filter-query__date_value" data-position="${position}">${value}</button><span class="filter-query__date_value_placeholder">${value}</span>`
let endPadding = ''
if(value.endsWith(' ')) {
const fullLength = value.length
value = value.trimEnd()
const numberOfRemovedSpaces = fullLength - value.length
endPadding = endPadding.padEnd(numberOfRemovedSpaces, ' ')
}
return `${o}${spacesBefore}${token}${spacesAfter}<button class="is-primary filter-query__date_value" data-position="${position}">${value}</button><span class="filter-query__date_value_placeholder">${value}</span>${endPadding}`
})
})
ASSIGNEE_FIELDS
@ -317,6 +325,11 @@ const blurDebounced = useDebounceFn(() => emit('blur'), 500)
<style lang="scss">
.filter-input-highlight {
&, button.filter-query__date_value {
color: var(--card-color);
}
span {
&.filter-query__field {
color: var(--code-literal);

View File

@ -63,6 +63,10 @@ const filteredProjects = computed(() => {
@media screen and (min-width: $widescreen) {
--project-grid-columns: 5;
.project-grid-item:nth-child(6) {
display: none;
}
}
}

View File

@ -96,7 +96,6 @@
{{ $t('project.webhooks.title') }}
</DropdownItem>
<DropdownItem
v-if="level < 2"
:to="{ name: 'project.createFromParent', params: { parentProjectId: project.id } }"
icon="layer-group"
>
@ -135,9 +134,6 @@ const props = defineProps({
type: Object as PropType<IProject>,
required: true,
},
level: {
type: Number,
},
})
const projectStore = useProjectStore()

View File

@ -41,7 +41,7 @@
@click="() => unCollapseBucket(bucket)"
>
<span
v-if="project?.doneBucketId === bucket.id"
v-if="view?.doneBucketId === bucket.id"
v-tooltip="$t('project.kanban.doneBucketHint')"
class="icon is-small has-text-success mr-2"
>
@ -109,7 +109,7 @@
</DropdownItem>
<DropdownItem
v-tooltip="$t('project.kanban.doneBucketHintExtended')"
:icon-class="{'has-text-success': bucket.id === project?.doneBucketId}"
:icon-class="{'has-text-success': bucket.id === view?.doneBucketId}"
icon="check-double"
@click.stop="toggleDoneBucket(bucket)"
>
@ -117,7 +117,7 @@
</DropdownItem>
<DropdownItem
v-tooltip="$t('project.kanban.defaultBucketHint')"
:icon-class="{'has-text-primary': bucket.id === project.defaultBucketId}"
:icon-class="{'has-text-primary': bucket.id === view?.defaultBucketId}"
icon="th"
@click.stop="toggleDefaultBucket(bucket)"
>
@ -304,6 +304,8 @@ import type {IProjectView} from '@/modelTypes/IProjectView'
import TaskPositionService from '@/services/taskPosition'
import TaskPositionModel from '@/models/taskPosition'
import {i18n} from '@/i18n'
import ProjectViewService from '@/services/projectViews'
import ProjectViewModel from '@/models/projectView'
const {
projectId,
@ -393,6 +395,8 @@ const project = computed(() => projectId ? projectStore.projects[projectId] : nu
const buckets = computed(() => kanbanStore.buckets)
const loading = computed(() => kanbanStore.isLoading)
const view = computed<IProjectView | null>(() => project.value?.views.find(v => v.id === viewId) || null)
const taskLoading = computed(() => taskStore.isLoading || taskPositionService.value.loading)
watch(
@ -701,26 +705,46 @@ function dragstart(bucket: IBucket) {
}
async function toggleDefaultBucket(bucket: IBucket) {
const defaultBucketId = project.value.defaultBucketId === bucket.id
const defaultBucketId = view.value?.defaultBucketId === bucket.id
? 0
: bucket.id
await projectStore.updateProject({
...project.value,
const projectViewService = new ProjectViewService()
const updatedView = await projectViewService.update(new ProjectViewModel({
...view.value,
defaultBucketId,
})
}))
const views = project.value.views.map(v => v.id === view.value?.id ? updatedView : v)
const updatedProject = {
...project.value,
views,
}
projectStore.setProject(updatedProject)
success({message: t('project.kanban.defaultBucketSavedSuccess')})
}
async function toggleDoneBucket(bucket: IBucket) {
const doneBucketId = project.value?.doneBucketId === bucket.id
const doneBucketId = view.value?.doneBucketId === bucket.id
? 0
: bucket.id
await projectStore.updateProject({
...project.value,
const projectViewService = new ProjectViewService()
const updatedView = await projectViewService.update(new ProjectViewModel({
...view.value,
doneBucketId,
})
}))
const views = project.value.views.map(v => v.id === view.value?.id ? updatedView : v)
const updatedProject = {
...project.value,
views,
}
projectStore.setProject(updatedProject)
success({message: t('project.kanban.doneBucketSavedSuccess')})
}

View File

@ -107,7 +107,7 @@ describe('Filter Transformation', () => {
expect(transformed).toBe('project = 1')
})
it('should resolve project and labels independently', () => {
const transformed = transformFilterStringForApi(
'project = lorem && labels = ipsum',
@ -117,6 +117,16 @@ describe('Filter Transformation', () => {
expect(transformed).toBe('project = 1 && labels = 2')
})
it('should transform the same attribute multiple times', () => {
const transformed = transformFilterStringForApi(
'dueDate = now/d || dueDate > now/w+1w',
nullTitleToIdResolver,
nullTitleToIdResolver,
)
expect(transformed).toBe('due_date = now/d || due_date > now/w+1w')
})
})
describe('To API', () => {
@ -198,5 +208,15 @@ describe('Filter Transformation', () => {
expect(transformed).toBe('project in lorem, ipsum')
})
it('should transform the same attribute multiple times', () => {
const transformed = transformFilterStringFromApi(
'due_date = now/d || due_date > now/w+1w',
nullIdToTitleResolver,
nullIdToTitleResolver,
)
expect(transformed).toBe('dueDate = now/d || dueDate > now/w+1w')
})
})
})

View File

@ -127,7 +127,7 @@ export function transformFilterStringForApi(
// Transform all attributes to snake case
AVAILABLE_FILTER_FIELDS.forEach(f => {
filter = filter.replace(f, snakeCase(f))
filter = filter.replaceAll(f, snakeCase(f))
})
return filter
@ -145,7 +145,7 @@ export function transformFilterStringFromApi(
// Transform all attributes from snake case
AVAILABLE_FILTER_FIELDS.forEach(f => {
filter = filter.replace(snakeCase(f), f)
filter = filter.replaceAll(snakeCase(f), f)
})
// Transform labels to their titles

View File

@ -20,8 +20,6 @@ export interface IProject extends IAbstract {
position: number
backgroundBlurHash: string
parentProjectId: number
doneBucketId: number
defaultBucketId: number
views: IProjectView[]
created: Date

View File

@ -24,8 +24,6 @@ export default class ProjectModel extends AbstractModel<IProject> implements IPr
position = 0
backgroundBlurHash = ''
parentProjectId = 0
doneBucketId = 0
defaultBucketId = 0
views = []
created: Date = null

View File

@ -2,14 +2,14 @@ import {test, expect, vi} from 'vitest'
import {getHistory, removeProjectFromHistory, saveProjectToHistory} from './projectHistory'
test('return an empty history when none was saved', () => {
Storage.prototype.getItem = vi.fn(() => null)
vi.spyOn(localStorage, 'getItem').mockImplementation(() => null)
const h = getHistory()
expect(h).toStrictEqual([])
})
test('return a saved history', () => {
const saved = [{id: 1}, {id: 2}]
Storage.prototype.getItem = vi.fn(() => JSON.stringify(saved))
vi.spyOn(localStorage, 'getItem').mockImplementation(() => JSON.stringify(saved))
const h = getHistory()
expect(h).toStrictEqual(saved)
@ -17,8 +17,8 @@ test('return a saved history', () => {
test('store project in history', () => {
let saved = {}
Storage.prototype.getItem = vi.fn(() => null)
Storage.prototype.setItem = vi.fn((key, projects) => {
vi.spyOn(localStorage, 'getItem').mockImplementation(() => null)
vi.spyOn(localStorage, 'setItem').mockImplementation((key: string, projects: string) => {
saved = projects
})
@ -26,10 +26,10 @@ test('store project in history', () => {
expect(saved).toBe('[{"id":1}]')
})
test('store only the last 5 projects in history', () => {
test('store only the last 6 projects in history', () => {
let saved: string | null = null
Storage.prototype.getItem = vi.fn(() => saved)
Storage.prototype.setItem = vi.fn((key: string, projects: string) => {
vi.spyOn(localStorage, 'getItem').mockImplementation(() => saved)
vi.spyOn(localStorage, 'setItem').mockImplementation((key: string, projects: string) => {
saved = projects
})
@ -39,13 +39,14 @@ test('store only the last 5 projects in history', () => {
saveProjectToHistory({id: 4})
saveProjectToHistory({id: 5})
saveProjectToHistory({id: 6})
expect(saved).toBe('[{"id":6},{"id":5},{"id":4},{"id":3},{"id":2}]')
saveProjectToHistory({id: 7})
expect(saved).toBe('[{"id":7},{"id":6},{"id":5},{"id":4},{"id":3},{"id":2}]')
})
test('don\'t store the same project twice', () => {
let saved: string | null = null
Storage.prototype.getItem = vi.fn(() => saved)
Storage.prototype.setItem = vi.fn((key: string, projects: string) => {
vi.spyOn(localStorage, 'getItem').mockImplementation(() => saved)
vi.spyOn(localStorage, 'setItem').mockImplementation((key: string, projects: string) => {
saved = projects
})
@ -56,8 +57,8 @@ test('don\'t store the same project twice', () => {
test('move a project to the beginning when storing it multiple times', () => {
let saved: string | null = null
Storage.prototype.getItem = vi.fn(() => saved)
Storage.prototype.setItem = vi.fn((key: string, projects: string) => {
vi.spyOn(localStorage, 'getItem').mockImplementation(() => saved)
vi.spyOn(localStorage, 'setItem').mockImplementation((key: string, projects: string) => {
saved = projects
})
@ -69,11 +70,11 @@ test('move a project to the beginning when storing it multiple times', () => {
test('remove project from history', () => {
let saved: string | null = '[{"id": 1}]'
Storage.prototype.getItem = vi.fn(() => null)
Storage.prototype.setItem = vi.fn((key: string, projects: string) => {
vi.spyOn(localStorage, 'getItem').mockImplementation(() => saved)
vi.spyOn(localStorage, 'setItem').mockImplementation((key: string, projects: string) => {
saved = projects
})
Storage.prototype.removeItem = vi.fn((key: string) => {
vi.spyOn(localStorage, 'removeItem').mockImplementation((key: string) => {
saved = null
})

View File

@ -20,6 +20,8 @@ function saveHistory(history: ProjectHistory[]) {
localStorage.setItem('projectHistory', JSON.stringify(history))
}
const MAX_SAVED_PROJECTS = 6
export function saveProjectToHistory(project: ProjectHistory) {
const history: ProjectHistory[] = getHistory()
@ -33,7 +35,7 @@ export function saveProjectToHistory(project: ProjectHistory) {
// Add the new project to the beginning of the project
history.unshift(project)
if (history.length > 5) {
if (history.length > MAX_SAVED_PROJECTS) {
history.pop()
}
saveHistory(history)

View File

@ -106,6 +106,12 @@ export const useProjectStore = defineStore('project', () => {
}
function removeProjectById(project: IProject) {
// Remove child projects from state as well
projectsArray.value
.filter(p => p.parentProjectId === project.id)
.forEach(p => removeProjectById(p))
remove(project)
delete projects.value[project.id]
}

View File

@ -3,7 +3,7 @@
<h1>{{ $t('migrate.titleService', {name: migrator.name}) }}</h1>
<p>{{ $t('migrate.descriptionDo') }}</p>
<template v-if="message === '' && lastMigrationStartedAt === null">
<template v-if="message === '' && lastMigrationStartedAt === null && !migrationJustStarted">
<template v-if="isMigrating === false">
<template v-if="migrator.isFileMigrator">
<p>{{ $t('migrate.importUpload', {name: migrator.name}) }}</p>

13
go.mod
View File

@ -67,17 +67,17 @@ require (
github.com/ulule/limiter/v3 v3.11.2
github.com/wneessen/go-mail v0.4.0
github.com/yuin/goldmark v1.7.0
golang.org/x/crypto v0.21.0
golang.org/x/crypto v0.22.0
golang.org/x/image v0.15.0
golang.org/x/oauth2 v0.18.0
golang.org/x/sync v0.6.0
golang.org/x/sys v0.18.0
golang.org/x/term v0.18.0
golang.org/x/sys v0.19.0
golang.org/x/term v0.19.0
golang.org/x/text v0.14.0
gopkg.in/d4l3k/messagediff.v1 v1.2.1
gopkg.in/yaml.v3 v3.0.1
mvdan.cc/xurls/v2 v2.5.0
src.techknowlogick.com/xgo v1.7.1-0.20240305180710-770b8eae9cec
src.techknowlogick.com/xgo v1.7.1-0.20240403232151-e01c4fbef884
src.techknowlogick.com/xormigrate v1.7.1
xorm.io/builder v0.3.13
xorm.io/xorm v1.3.9
@ -90,6 +90,7 @@ require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/beevik/etree v1.1.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
@ -121,6 +122,7 @@ require (
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
@ -137,6 +139,7 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/microcosm-cc/bluemonday v1.0.26 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
@ -176,7 +179,7 @@ require (
golang.org/x/arch v0.4.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/net v0.22.0 // indirect
golang.org/x/net v0.24.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.13.0 // indirect
google.golang.org/appengine v1.6.8 // indirect

16
go.sum
View File

@ -35,6 +35,8 @@ github.com/arran4/golang-ical v0.2.7 h1:VO7YlVaGupZE15aj6NhUhte/MIfZuoIzkoI71VsG
github.com/arran4/golang-ical v0.2.7/go.mod h1:RqMuPGmwRRwjkb07hmm+JBqcWa1vF1LvVmPtSZN2OhQ=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/bbrks/go-blurhash v1.1.1 h1:uoXOxRPDca9zHYabUTwvS4KnY++KKUbwFo+Yxb8ME4M=
github.com/bbrks/go-blurhash v1.1.1/go.mod h1:lkAsdyXp+EhARcUo85yS2G1o+Sh43I2ebF5togC4bAY=
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
@ -215,6 +217,8 @@ github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@ -372,6 +376,8 @@ github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -575,6 +581,8 @@ golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
@ -611,6 +619,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -654,6 +664,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@ -662,6 +674,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@ -788,6 +802,8 @@ sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
src.techknowlogick.com/xgo v1.7.1-0.20240305180710-770b8eae9cec h1:ICDp83UjJvLcOFWHAxr7vmziKIHJkE4jsIF1mbT9Bwk=
src.techknowlogick.com/xgo v1.7.1-0.20240305180710-770b8eae9cec/go.mod h1:31CE1YKtDOrKTk9PSnjTpe6YbO6W/0LTYZ1VskL09oU=
src.techknowlogick.com/xgo v1.7.1-0.20240403232151-e01c4fbef884 h1:Ttvt8FCpUXfC8r3+LgSPrBUIr/JkHmYQtvmOwEET8qE=
src.techknowlogick.com/xgo v1.7.1-0.20240403232151-e01c4fbef884/go.mod h1:31CE1YKtDOrKTk9PSnjTpe6YbO6W/0LTYZ1VskL09oU=
src.techknowlogick.com/xormigrate v1.7.1 h1:RKGLLUAqJ+zO8iZ7eOc7oLH7f0cs2gfXSZSvBRBHnlY=
src.techknowlogick.com/xormigrate v1.7.1/go.mod h1:YGNBdj8prENlySwIKmfoEXp7ILGjAltyKFXD0qLgD7U=
xorm.io/builder v0.3.7/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=

View File

@ -82,6 +82,7 @@ func init() {
if config.DatabaseType.GetString() == "sqlite" {
_, err = tx.Exec(`
create table buckets_dg_tmp
(
id INTEGER not null
@ -99,6 +100,8 @@ insert into buckets_dg_tmp(id, title, "limit", position, created, updated, creat
select id, title, "limit", position, created, updated, created_by_id, project_view_id
from buckets;
drop index if exists buckets.UQE_buckets_id;
drop table buckets;
alter table buckets_dg_tmp

View File

@ -78,8 +78,9 @@ func init() {
if view.ViewKind == 3 { // Kanban view
pos := taskBuckets20240315110428{
TaskID: task.ID,
BucketID: task.BucketID,
TaskID: task.ID,
BucketID: task.BucketID,
ProjectViewID: view.ID,
}
_, err = tx.Insert(pos)

View File

@ -0,0 +1,89 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-present Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package migration
import (
"code.vikunja.io/api/pkg/log"
"src.techknowlogick.com/xormigrate"
"xorm.io/xorm"
)
type taskBucket20240406125227 struct {
BucketID int64 `xorm:"bigint not null index"`
TaskID int64 `xorm:"bigint not null index"`
ProjectViewID int64 `xorm:"bigint not null index"`
}
func (taskBucket20240406125227) TableName() string {
return "task_buckets"
}
type bucket20240406125227 struct {
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"bucket"`
ProjectViewID int64 `xorm:"bigint not null" json:"project_view_id" param:"view"`
}
func (bucket20240406125227) TableName() string {
return "buckets"
}
func init() {
migrations = append(migrations, &xormigrate.Migration{
ID: "20240406125227",
Description: "Add correct project_view_id to task_buckets",
Migrate: func(tx *xorm.Engine) error {
buckets := make(map[int64]*bucket20240406125227)
err := tx.Find(&buckets)
if err != nil {
return err
}
tbs := []*taskBucket20240406125227{}
err = tx.Where("project_view_id = 0").Find(&tbs)
if err != nil {
return err
}
if len(tbs) == 0 {
return nil
}
for _, tb := range tbs {
bucket, exists := buckets[tb.BucketID]
if !exists {
log.Debugf("Bucket %d does not exist but has task_buckets relation", tb.BucketID)
continue
}
tb.ProjectViewID = bucket.ProjectViewID
_, err = tx.
Where("task_id = ? AND bucket_id = ?", tb.TaskID, tb.BucketID).
Cols("project_view_id").
Update(tb)
if err != nil {
return err
}
}
return nil
},
Rollback: func(tx *xorm.Engine) error {
return nil
},
})
}

View File

@ -77,7 +77,7 @@ func (l *Label) hasAccessToLabel(s *xorm.Session, a web.Auth) (has bool, maxRigh
builder.
Select("id").
From("tasks").
Where(builder.In("project_id", getUserProjectsStatement(nil, u.ID, "", false).Select("l.id"))),
Where(builder.In("project_id", getUserProjectsStatement(u.ID, "", false).Select("l.id"))),
)
ll := &LabelTask{}

View File

@ -22,7 +22,7 @@ import (
"time"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/events"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/web"
@ -46,7 +46,7 @@ type LabelTask struct {
}
// TableName makes a pretty table name
func (LabelTask) TableName() string {
func (*LabelTask) TableName() string {
return "label_tasks"
}
@ -84,7 +84,7 @@ func (lt *LabelTask) Delete(s *xorm.Session, _ web.Auth) (err error) {
// @Failure 404 {object} web.HTTPError "The label does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{task}/labels [put]
func (lt *LabelTask) Create(s *xorm.Session, _ web.Auth) (err error) {
func (lt *LabelTask) Create(s *xorm.Session, auth web.Auth) (err error) {
// Check if the label is already added
exists, err := s.Exist(&LabelTask{LabelID: lt.LabelID, TaskID: lt.TaskID})
if err != nil {
@ -100,6 +100,20 @@ func (lt *LabelTask) Create(s *xorm.Session, _ web.Auth) (err error) {
return err
}
t, err := GetTaskByIDSimple(s, lt.TaskID)
if err != nil {
return err
}
doer, _ := user.GetFromAuth(auth)
err = events.Dispatch(&TaskUpdatedEvent{
Task: &t,
Doer: doer,
})
if err != nil {
return err
}
err = updateProjectByTaskID(s, lt.TaskID)
return
}

View File

@ -17,10 +17,8 @@
package models
import (
"bufio"
"sort"
"strconv"
"strings"
"time"
"code.vikunja.io/api/pkg/utils"
@ -77,20 +75,16 @@ func (n *TaskCommentNotification) SubjectID() int64 {
func (n *TaskCommentNotification) ToMail() *notifications.Mail {
mail := notifications.NewMail().
From(n.Doer.GetNameAndFromEmail())
From(n.Doer.GetNameAndFromEmail()).
Subject("Re: " + n.Task.Title)
subject := "Re: " + n.Task.Title
if n.Mentioned {
subject = n.Doer.GetName() + ` mentioned you in a comment in "` + n.Task.Title + `"`
mail.Line("**" + n.Doer.GetName() + "** mentioned you in a comment:")
mail.
Line("**" + n.Doer.GetName() + "** mentioned you in a comment:").
Subject(n.Doer.GetName() + ` mentioned you in a comment in "` + n.Task.Title + `"`)
}
mail.Subject(subject)
lines := bufio.NewScanner(strings.NewReader(n.Comment.Comment))
for lines.Scan() {
mail.Line(lines.Text())
}
mail.HTML(n.Comment.Comment)
return mail.
Action("View Task", n.Task.GetFrontendURL())
@ -306,12 +300,8 @@ func (n *UserMentionedInTaskNotification) ToMail() *notifications.Mail {
mail := notifications.NewMail().
From(n.Doer.GetNameAndFromEmail()).
Subject(subject).
Line("**" + n.Doer.GetName() + "** mentioned you in a task:")
lines := bufio.NewScanner(strings.NewReader(n.Task.Description))
for lines.Scan() {
mail.Line(lines.Text())
}
Line("**" + n.Doer.GetName() + "** mentioned you in a task:").
HTML(n.Task.Description)
return mail.
Action("View Task", n.Task.GetFrontendURL())

View File

@ -368,7 +368,7 @@ type projectOptions struct {
getArchived bool
}
func getUserProjectsStatement(parentProjectIDs []int64, userID int64, search string, getArchived bool) *builder.Builder {
func getUserProjectsStatement(userID int64, search string, getArchived bool) *builder.Builder {
dialect := db.GetDialect()
// Adding a 1=1 condition by default here because xorm always needs a condition and cannot handle nil conditions
@ -413,18 +413,13 @@ func getUserProjectsStatement(parentProjectIDs []int64, userID int64, search str
),
)
}
projectCol := "id"
if len(parentProjectIDs) > 0 {
parentCondition = builder.In("l.parent_project_id", parentProjectIDs)
projectCol = "parent_project_id"
}
return builder.Dialect(dialect).
Select("l.*").
From("projects", "l").
Join("LEFT", "team_projects tl", "tl.project_id = l."+projectCol).
Join("LEFT", "team_projects tl", "tl.project_id = l.id").
Join("LEFT", "team_members tm2", "tm2.team_id = tl.team_id").
Join("LEFT", "users_projects ul", "ul.project_id = l."+projectCol).
Join("LEFT", "users_projects ul", "ul.project_id = l.id").
Where(builder.And(
builder.Or(
builder.Eq{"tm2.user_id": userID},
@ -434,7 +429,6 @@ func getUserProjectsStatement(parentProjectIDs []int64, userID int64, search str
filterCond,
getArchivedCond,
parentCondition,
builder.NotIn("l.id", parentProjectIDs),
)).
GroupBy("l.id")
}
@ -442,7 +436,7 @@ func getUserProjectsStatement(parentProjectIDs []int64, userID int64, search str
func getAllProjectsForUser(s *xorm.Session, userID int64, opts *projectOptions) (projects []*Project, totalCount int64, err error) {
limit, start := getLimitFromPageIndex(opts.page, opts.perPage)
query := getUserProjectsStatement(nil, userID, opts.search, opts.getArchived)
query := getUserProjectsStatement(userID, opts.search, opts.getArchived)
querySQLString, args, err := query.ToSQL()
if err != nil {
@ -459,9 +453,26 @@ UNION ALL
SELECT p.* FROM projects p
INNER JOIN all_projects ap ON p.parent_project_id = ap.id`
columnStr := strings.Join([]string{
"all_projects.id",
"all_projects.title",
"all_projects.description",
"all_projects.identifier",
"all_projects.hex_color",
"all_projects.owner_id",
"CASE WHEN np.id IS NULL THEN 0 ELSE all_projects.parent_project_id END AS parent_project_id",
"all_projects.is_archived",
"all_projects.background_file_id",
"all_projects.background_blur_hash",
"all_projects.position",
"all_projects.created",
"all_projects.updated",
}, ", ")
currentProjects := []*Project{}
err = s.SQL(`WITH RECURSIVE all_projects as (`+baseQuery+`)
SELECT DISTINCT * FROM all_projects ORDER BY position `+limitSQL, args...).Find(&currentProjects)
SELECT DISTINCT `+columnStr+` FROM all_projects
LEFT JOIN all_projects np on all_projects.parent_project_id = np.id
ORDER BY all_projects.position `+limitSQL, args...).Find(&currentProjects)
if err != nil {
return
}

View File

@ -280,6 +280,13 @@ func (t *Task) addNewAssigneeByID(s *xorm.Session, newAssigneeID int64, project
if err != nil {
return err
}
err = events.Dispatch(&TaskUpdatedEvent{
Task: t,
Doer: doer,
})
if err != nil {
return err
}
err = updateProjectLastUpdated(s, &Project{ID: t.ProjectID})
return

View File

@ -24,10 +24,9 @@ import (
"strings"
"time"
"github.com/ganigeorgiev/fexpr"
"code.vikunja.io/api/pkg/config"
"github.com/ganigeorgiev/fexpr"
"github.com/iancoleman/strcase"
"github.com/jszwedko/go-datemath"
"xorm.io/xorm/schemas"
@ -155,15 +154,27 @@ func getTaskFiltersFromFilterString(filter string, filterTimezone string) (filte
filter = strings.ReplaceAll(filter, " in ", " ?= ")
// Replaces all occurrences with in with a string so that it passes the filter
pattern := `\?=\s+([^&|']+)`
pattern := `(\?=\s+([^&|']+))|(([<>]?=|[<>])[^&|')]+\/[^&|')]+([&|')]+))`
re := regexp.MustCompile(pattern)
filter = re.ReplaceAllStringFunc(filter, func(match string) string {
value := strings.TrimSpace(strings.TrimPrefix(match, "?="))
comparator := match[:2]
value := strings.TrimSpace(match[2:])
if match[1] == ' ' {
comparator = match[:1]
}
var end string
if value[len(value)-1:] == ")" {
end = ")"
value = value[0 : len(value)-1]
}
value = strings.ReplaceAll(value, "'", `\'`)
enclosedValue := "'" + value + "'"
return "?= " + enclosedValue
return comparator + " " + enclosedValue + end
})
parsedFilter, err := fexpr.Parse(filter)

View File

@ -672,9 +672,21 @@ func setTaskBucket(s *xorm.Session, task *Task, originalTask *Task, view *Projec
return
}
// If the task was marked as done and the view has a done bucket, move the task to the done bucket
if task.Done && originalTask != nil &&
(!originalTask.Done || task.ProjectID != originalTask.ProjectID) {
targetBucket.BucketID = view.DoneBucketID
// …and also reset the position so that it shows up at the top
// Note: this might result in an "off-looking" position when there is already a task with position 0.
// This is done by design, because recalculating all positions is really costly and will happen
// later anyway.
_, err = s.
Where("task_id = ? AND project_view_id = ?", task.ID, view.ID).
Cols("position").
Update(&TaskPosition{Position: 0})
if err != nil {
return
}
}
if targetBucket.BucketID == 0 && oldTaskBucket.BucketID != 0 {

View File

@ -143,6 +143,13 @@ func (bp *BackgroundProvider) SetBackground(c echo.Context) error {
_ = s.Rollback()
return handler.HandleHTTPError(err, c)
}
err = project.ReadOne(s, auth)
if err != nil {
_ = s.Rollback()
return handler.HandleHTTPError(err, c)
}
return c.JSON(http.StatusOK, project)
}

View File

@ -51,7 +51,7 @@ func (s *MigrationListener) Handle(msg *message.Message) (err error) {
mstr := registeredMigrators[event.MigratorKind]
event.Migrator = mstr.MigrationStruct()
// unmarshaling again to make sure the migrator has the correct type now
// unmarshalling again to make sure the migrator has the correct type now
err = json.Unmarshal(msg.Payload, event)
if err != nil {
return

View File

@ -26,8 +26,13 @@ type Mail struct {
actionText string
actionURL string
greeting string
introLines []string
outroLines []string
introLines []*mailLine
outroLines []*mailLine
}
type mailLine struct {
Text string
isHTML bool
}
// NewMail creates a new mail object with a default greeting
@ -66,14 +71,28 @@ func (m *Mail) Action(text, url string) *Mail {
return m
}
// Line adds a line of text to the mail
// Line adds a line of Text to the mail
func (m *Mail) Line(line string) *Mail {
return m.appendLine(line, false)
}
func (m *Mail) HTML(line string) *Mail {
return m.appendLine(line, true)
}
func (m *Mail) appendLine(line string, isHTML bool) *Mail {
if m.actionURL == "" {
m.introLines = append(m.introLines, line)
m.introLines = append(m.introLines, &mailLine{
Text: line,
isHTML: isHTML,
})
return m
}
m.outroLines = append(m.outroLines, line)
m.outroLines = append(m.outroLines, &mailLine{
Text: line,
isHTML: isHTML,
})
return m
}

View File

@ -23,6 +23,8 @@ import (
templatehtml "html/template"
templatetext "text/template"
"github.com/microcosm-cc/bluemonday"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/mail"
"code.vikunja.io/api/pkg/utils"
@ -33,12 +35,12 @@ import (
const mailTemplatePlain = `
{{ .Greeting }}
{{ range $line := .IntroLines}}
{{ $line }}
{{ $line.Text }}
{{ end }}
{{ if .ActionURL }}{{ .ActionText }}:
{{ .ActionURL }}{{end}}
{{ range $line := .OutroLines}}
{{ $line }}
{{ $line.Text }}
{{ end }}`
const mailTemplateHTML = `
@ -48,9 +50,9 @@ const mailTemplateHTML = `
<meta name="viewport" content="width: display-width;">
</head>
<body style="width: 100%; padding: 0; margin: 0; background: #f3f4f6">
<div style="width: 100%; font-family: 'Open Sans', sans-serif; text-rendering: optimizeLegibility">
<div style="width: 600px; margin: 0 auto; text-align: justify;">
<h1 style="font-size: 30px; text-align: center;">
<div style="width: 100%; font-family: 'Open Sans', sans-serif; Text-rendering: optimizeLegibility">
<div style="width: 600px; margin: 0 auto; Text-align: justify;">
<h1 style="font-size: 30px; Text-align: center;">
<img src="cid:logo.png" style="height: 75px;" alt="Vikunja"/>
</h1>
<div style="border: 1px solid #dbdbdb; -webkit-box-shadow: 0.3em 0.3em 0.8em #e6e6e6; box-shadow: 0.3em 0.3em 0.8em #e6e6e6; color: #4a4a4a; padding: 5px 25px; border-radius: 3px; background: #fff;">
@ -64,7 +66,7 @@ const mailTemplateHTML = `
{{ if .ActionURL }}
<a href="{{ .ActionURL }}" title="{{ .ActionText }}"
style="position: relative;text-decoration:none;display: block;border-radius: 4px;cursor: pointer;padding-bottom: 8px;padding-left: 14px;padding-right: 14px;padding-top: 8px;width:280px;margin:10px auto;text-align: center;white-space: nowrap;border: 0;text-transform: uppercase;font-size: 14px;font-weight: 700;-webkit-box-shadow: 0 3px 6px rgba(107,114,128,.12),0 2px 4px rgba(107,114,128,.1);box-shadow: 0 3px 6px rgba(107,114,128,.12),0 2px 4px rgba(107,114,128,.1);background-color: #1973ff;border-color: transparent;color: #fff;">
style="position: relative;Text-decoration:none;display: block;border-radius: 4px;cursor: pointer;padding-bottom: 8px;padding-left: 14px;padding-right: 14px;padding-top: 8px;width:280px;margin:10px auto;Text-align: center;white-space: nowrap;border: 0;Text-transform: uppercase;font-size: 14px;font-weight: 700;-webkit-box-shadow: 0 3px 6px rgba(107,114,128,.12),0 2px 4px rgba(107,114,128,.1);box-shadow: 0 3px 6px rgba(107,114,128,.12),0 2px 4px rgba(107,114,128,.1);background-color: #1973ff;border-color: transparent;color: #fff;">
{{ .ActionText }}
</a>
{{end}}
@ -117,29 +119,43 @@ func RenderMail(m *Mail) (mailOpts *mail.Opts, err error) {
data["Boundary"] = boundary
data["FrontendURL"] = config.ServicePublicURL.GetString()
p := bluemonday.UGCPolicy()
var introLinesHTML []templatehtml.HTML
for _, line := range m.introLines {
md := []byte(templatehtml.HTMLEscapeString(line))
if line.isHTML {
// #nosec G203 -- the html is sanitized
introLinesHTML = append(introLinesHTML, templatehtml.HTML(p.Sanitize(line.Text)))
continue
}
md := []byte(templatehtml.HTMLEscapeString(line.Text))
var buf bytes.Buffer
err = goldmark.Convert(md, &buf)
if err != nil {
return nil, err
}
//#nosec - the html is escaped few lines before
introLinesHTML = append(introLinesHTML, templatehtml.HTML(buf.String()))
// #nosec G203 -- the html is sanitized
introLinesHTML = append(introLinesHTML, templatehtml.HTML(p.Sanitize(buf.String())))
}
data["IntroLinesHTML"] = introLinesHTML
var outroLinesHTML []templatehtml.HTML
for _, line := range m.outroLines {
md := []byte(templatehtml.HTMLEscapeString(line))
if line.isHTML {
// #nosec G203 -- the html is sanitized
outroLinesHTML = append(outroLinesHTML, templatehtml.HTML(p.Sanitize(line.Text)))
continue
}
md := []byte(templatehtml.HTMLEscapeString(line.Text))
var buf bytes.Buffer
err = goldmark.Convert(md, &buf)
if err != nil {
return nil, err
}
//#nosec - the html is escaped few lines before
outroLinesHTML = append(outroLinesHTML, templatehtml.HTML(buf.String()))
// #nosec G203 -- the html is sanitized
outroLinesHTML = append(outroLinesHTML, templatehtml.HTML(p.Sanitize(buf.String())))
}
data["OutroLinesHTML"] = outroLinesHTML

View File

@ -41,11 +41,15 @@ func TestNewMail(t *testing.T) {
assert.Equal(t, "Testmail", mail.subject)
assert.Equal(t, "Hi there,", mail.greeting)
assert.Len(t, mail.introLines, 2)
assert.Equal(t, "This is a line", mail.introLines[0])
assert.Equal(t, "And another one", mail.introLines[1])
assert.Equal(t, "This is a line", mail.introLines[0].Text)
assert.False(t, mail.introLines[0].isHTML)
assert.Equal(t, "And another one", mail.introLines[1].Text)
assert.False(t, mail.introLines[1].isHTML)
assert.Len(t, mail.outroLines, 2)
assert.Equal(t, "This should be an outro line", mail.outroLines[0])
assert.Equal(t, "And one more, because why not?", mail.outroLines[1])
assert.Equal(t, "This should be an outro line", mail.outroLines[0].Text)
assert.False(t, mail.outroLines[0].isHTML)
assert.Equal(t, "And one more, because why not?", mail.outroLines[1].Text)
assert.False(t, mail.outroLines[1].isHTML)
})
t.Run("No greeting", func(t *testing.T) {
mail := NewMail().
@ -60,8 +64,8 @@ func TestNewMail(t *testing.T) {
assert.Equal(t, "Testmail", mail.subject)
assert.Equal(t, "", mail.greeting)
assert.Len(t, mail.introLines, 2)
assert.Equal(t, "This is a line", mail.introLines[0])
assert.Equal(t, "And another one", mail.introLines[1])
assert.Equal(t, "This is a line", mail.introLines[0].Text)
assert.Equal(t, "And another one", mail.introLines[1].Text)
})
t.Run("No action", func(t *testing.T) {
mail := NewMail().
@ -77,10 +81,10 @@ func TestNewMail(t *testing.T) {
assert.Equal(t, "test@otherdomain.com", mail.to)
assert.Equal(t, "Testmail", mail.subject)
assert.Len(t, mail.introLines, 4)
assert.Equal(t, "This is a line", mail.introLines[0])
assert.Equal(t, "And another one", mail.introLines[1])
assert.Equal(t, "This should be an outro line", mail.introLines[2])
assert.Equal(t, "And one more, because why not?", mail.introLines[3])
assert.Equal(t, "This is a line", mail.introLines[0].Text)
assert.Equal(t, "And another one", mail.introLines[1].Text)
assert.Equal(t, "This should be an outro line", mail.introLines[2].Text)
assert.Equal(t, "And one more, because why not?", mail.introLines[3].Text)
})
}
@ -125,9 +129,9 @@ And one more, because why not?
<meta name="viewport" content="width: display-width;">
</head>
<body style="width: 100%; padding: 0; margin: 0; background: #f3f4f6">
<div style="width: 100%; font-family: 'Open Sans', sans-serif; text-rendering: optimizeLegibility">
<div style="width: 600px; margin: 0 auto; text-align: justify;">
<h1 style="font-size: 30px; text-align: center;">
<div style="width: 100%; font-family: 'Open Sans', sans-serif; Text-rendering: optimizeLegibility">
<div style="width: 600px; margin: 0 auto; Text-align: justify;">
<h1 style="font-size: 30px; Text-align: center;">
<img src="cid:logo.png" style="height: 75px;" alt="Vikunja"/>
</h1>
<div style="border: 1px solid #dbdbdb; -webkit-box-shadow: 0.3em 0.3em 0.8em #e6e6e6; box-shadow: 0.3em 0.3em 0.8em #e6e6e6; color: #4a4a4a; padding: 5px 25px; border-radius: 3px; background: #fff;">
@ -139,7 +143,7 @@ And one more, because why not?
<p>This is a line</p>
<p>This <strong>line</strong> contains <a href="https://vikunja.io">a link</a></p>
<p>This <strong>line</strong> contains <a href="https://vikunja.io" rel="nofollow">a link</a></p>
<p>And another one</p>
@ -148,7 +152,7 @@ And one more, because why not?
<a href="https://example.com" title="The action"
style="position: relative;text-decoration:none;display: block;border-radius: 4px;cursor: pointer;padding-bottom: 8px;padding-left: 14px;padding-right: 14px;padding-top: 8px;width:280px;margin:10px auto;text-align: center;white-space: nowrap;border: 0;text-transform: uppercase;font-size: 14px;font-weight: 700;-webkit-box-shadow: 0 3px 6px rgba(107,114,128,.12),0 2px 4px rgba(107,114,128,.1);box-shadow: 0 3px 6px rgba(107,114,128,.12),0 2px 4px rgba(107,114,128,.1);background-color: #1973ff;border-color: transparent;color: #fff;">
style="position: relative;Text-decoration:none;display: block;border-radius: 4px;cursor: pointer;padding-bottom: 8px;padding-left: 14px;padding-right: 14px;padding-top: 8px;width:280px;margin:10px auto;Text-align: center;white-space: nowrap;border: 0;Text-transform: uppercase;font-size: 14px;font-weight: 700;-webkit-box-shadow: 0 3px 6px rgba(107,114,128,.12),0 2px 4px rgba(107,114,128,.1);box-shadow: 0 3px 6px rgba(107,114,128,.12),0 2px 4px rgba(107,114,128,.1);background-color: #1973ff;border-color: transparent;color: #fff;">
The action
</a>