Compare commits
314 Commits
f4f324576c
...
f54aeb8e0b
Author | SHA1 | Date | |
---|---|---|---|
f54aeb8e0b | |||
e9aa784739 | |||
d759741b20 | |||
03040cbc12 | |||
2f30ae2efa | |||
dd98a3d972 | |||
6db913b205 | |||
da26ec7f1c | |||
|
14466bf9b7 | ||
|
903e9a9904 | ||
|
56fd25e888 | ||
c815830700 | |||
69f398f789 | |||
8280bd6bf5 | |||
89559557b8 | |||
84b1b61af4 | |||
7f84441e78 | |||
ec6d6018da | |||
bcec4b97d2 | |||
9e9e7eaecb | |||
665f1a7a18 | |||
25a56a89ae | |||
9b21d23245 | |||
5c500c711e | |||
af65447920 | |||
60fee6da7f | |||
d1dbc7f983 | |||
28ace38ebb | |||
2af42f8fbe | |||
5999def569 | |||
3b99facbfe | |||
c980729b0e | |||
b719766062 | |||
61592a3c33 | |||
e8877174d4 | |||
2edf3aebef | |||
63b409e3bd | |||
de45568a17 | |||
c3224a72c1 | |||
6a151a8cf3 | |||
a61ce6f1d6 | |||
0124f60bac | |||
1a14b1dac6 | |||
9662c79b95 | |||
43b67a9d33 | |||
37368a64df | |||
098113b3a4 | |||
5c13945393 | |||
|
86d957be4f | ||
9fe2b4ad73 | |||
|
72e80f637d | ||
a4424e089c | |||
|
945128c3cd | ||
35cfb2f3ca | |||
ccc85b9a82 | |||
9523f60763 | |||
7be8e892e2 | |||
2a6aff6ffa | |||
|
6a03972f16 | ||
|
4023ebcdd1 | ||
|
6049427322 | ||
|
f658d3bbba | ||
|
a029887102 | ||
|
0f7b7f72d0 | ||
|
a29131e7d4 | ||
|
b71d41c5ec | ||
81594e234a | |||
0846d1dc5e | |||
5dfaa48ea5 | |||
63671efbe2 | |||
|
4be53b098c | ||
5a89bc0183 | |||
97b1149a90 | |||
2ca33671aa | |||
d82f377f94 | |||
a2cd08a7af | |||
830d6d0a38 | |||
6cc23ff7aa | |||
bb4ed3d223 | |||
f56302a99f | |||
d850d5b98f | |||
4908469d49 | |||
|
1f25386f54 | ||
c97ed67f50 | |||
be53474eeb | |||
c6cb2343ae | |||
76bb081db5 | |||
8e9468228e | |||
61ba2facbc | |||
1bec289021 | |||
82b108a79d | |||
54c49391d1 | |||
6ddfba4f1f | |||
b2bf39fffa | |||
|
09d13520b0 | ||
0d91d2845f | |||
1b69b1b527 | |||
e14b34fca2 | |||
ce5e4aad6f | |||
12e85909b2 | |||
5c245d8921 | |||
ee89aa3b46 | |||
efe22c339a | |||
0aa4d1cb65 | |||
5b6ad786ee | |||
53b4352e04 | |||
ac5b849d06 | |||
3a8a45375c | |||
93f2ccf2e6 | |||
a910b263cb | |||
a6e4bbebec | |||
96dd0aab34 | |||
a6a0c3b121 | |||
367f55c04a | |||
c2fd41b80a | |||
373b04bd58 | |||
60890e4bb7 | |||
aaa8a3859a | |||
7f886bc6ac | |||
cacb59d6e3 | |||
017dad9b4b | |||
7123bbc440 | |||
4eb63452cb | |||
4ee201e7bd | |||
7eb971890c | |||
872197414d | |||
59b99407cc | |||
f5d30ccd44 | |||
46f89bd5ed | |||
65167c5989 | |||
e8b46829dd | |||
865172951a | |||
f152a84847 | |||
c09fbe9abe | |||
a6bafe1a9a | |||
3dfb8b858e | |||
ebccd6f411 | |||
f465576baa | |||
25b082c45d | |||
3e42eeba2c | |||
263265157a | |||
fc95c8e2be | |||
f58013fc32 | |||
563ff7b20e | |||
44821b4f24 | |||
344b38bf93 | |||
f6171935ca | |||
5f96407dc0 | |||
|
b6a89a0cde | ||
a3978bb359 | |||
43eb7c4abf | |||
6dd566ba46 | |||
|
318e8c83a6 | ||
|
f6c6f52abe | ||
|
c449925826 | ||
c6eb72bdeb | |||
aecfe7e15f | |||
f3a7093ed8 | |||
b37ae37116 | |||
a3a482769d | |||
7cc6cf233f | |||
ce529579c3 | |||
7680c82ce1 | |||
1bb6bedd6e | |||
4b29e3d9dd | |||
00b869c727 | |||
fe2db90c4a | |||
5b566178b3 | |||
c887e15421 | |||
99813772ef | |||
4d280a26d3 | |||
6d6922c90b | |||
ab65276e6b | |||
3b6dfcec78 | |||
a72cd31202 | |||
2141171529 | |||
9b050846a4 | |||
f6b8d2c4ca | |||
86119ff414 | |||
7c5622af11 | |||
6ac3ce65b4 | |||
07f9784e0d | |||
fb751236d1 | |||
396943b3a6 | |||
b8a19ac88a | |||
02fac73e07 | |||
21e52c3b1f | |||
213ef84586 | |||
25852ffac3 | |||
1255b50ed7 | |||
afe8198158 | |||
abe43c4ef8 | |||
8d5ddd695a | |||
a0087bc34a | |||
18a73c39e5 | |||
e0351495ff | |||
b97e045118 | |||
a052305adf | |||
237de35b25 | |||
85beebf0d8 | |||
e81b216f85 | |||
fc9e75e9b3 | |||
b576621569 | |||
08a031ca07 | |||
d306bb967c | |||
3f6a64d7f9 | |||
2d392c9973 | |||
9897bc9b43 | |||
92546f4b34 | |||
8d326aca03 | |||
4bf9284b38 | |||
0de9376b2b | |||
0602f6693b | |||
7d5cde53e3 | |||
8361640559 | |||
517a6cea1e | |||
ed4dd93bba | |||
d50de97490 | |||
aa719d3a68 | |||
463d22b07c | |||
33494cab6b | |||
8fa922a0ca | |||
e5815e21cb | |||
529b47e488 | |||
63c3e4ea58 | |||
d52e917357 | |||
b2da4fd126 | |||
83fb8c3ded | |||
|
b44d11cfc0 | ||
d4133b9e78 | |||
c478926038 | |||
00e40a0f53 | |||
0567ba2a47 | |||
|
3b95824f58 | ||
963f3bfb07 | |||
d1c05eb3fb | |||
2326e50d5d | |||
b7fa1a3ca1 | |||
a3e1e43ec7 | |||
39f163df4a | |||
f0e8ff93ff | |||
|
3ee0bc345d | ||
b4ffee8929 | |||
e3c3d3ee53 | |||
67e7b94f5d | |||
6bbddeae8c | |||
94a0e1e25f | |||
4df9bc33df | |||
5c64e8a2d7 | |||
e10791f28c | |||
44b58ff34b | |||
da17f78d30 | |||
61cdb7a91f | |||
1b8ed9417a | |||
4657da8c90 | |||
6cd2908040 | |||
4dd99ae6fc | |||
0d5fa1326d | |||
dd692de7c4 | |||
|
93d95b0821 | ||
|
422e731fe0 | ||
|
7db79ff04e | ||
59cc241226 | |||
2ac2e95cf2 | |||
f8ce3d6ed6 | |||
93f33d9647 | |||
6d32b22da3 | |||
b333898595 | |||
ccc633f3d9 | |||
d39b0675d3 | |||
274092bfc4 | |||
cb2c032e60 | |||
fdf294bcb3 | |||
58baa5960c | |||
e948678e42 | |||
|
5ccedc6f67 | ||
74ad98de68 | |||
3282f55c34 | |||
d9984b28f7 | |||
4fc7b9c67e | |||
ff9efe7889 | |||
66be0e6ac4 | |||
|
da8df8b667 | ||
42e9f306e8 | |||
|
4b47478440 | ||
|
b42e4cca59 | ||
|
33d4efecc4 | ||
|
45ec1623d5 | ||
|
8ef309243d | ||
|
3aaacf4533 | ||
0350e37fbb | |||
244c436202 | |||
18d0c8ba2c | |||
3891d5b876 | |||
|
98b38af43c | ||
77ff0aa256 | |||
2ab26ee7c5 | |||
58f38bcfc3 | |||
bcb5190365 | |||
c99d09c83e | |||
f49ea9752d | |||
a56683cdc2 | |||
7f4af63003 | |||
8c44ed83e6 | |||
b388677eaf | |||
bd7430b405 | |||
4baed8fe79 | |||
fdbe4e8314 | |||
4a7f839449 | |||
c359f4d4dd | |||
79d6212e48 | |||
e541213872 | |||
631a19fa92 | |||
fba402fcd0 |
28
.drone.yml
28
.drone.yml
|
@ -96,7 +96,7 @@ steps:
|
||||||
- dependencies
|
- dependencies
|
||||||
|
|
||||||
- name: test-frontend
|
- name: test-frontend
|
||||||
image: cypress/browsers:node18.12.0-chrome106-ff106
|
image: cypress/browsers:node18.12.0-chrome107
|
||||||
pull: always
|
pull: always
|
||||||
environment:
|
environment:
|
||||||
CYPRESS_API_URL: http://api:3456/api/v1
|
CYPRESS_API_URL: http://api:3456/api/v1
|
||||||
|
@ -110,8 +110,7 @@ steps:
|
||||||
- sed -i 's/localhost/api/g' dist/index.html
|
- sed -i 's/localhost/api/g' dist/index.html
|
||||||
- corepack enable && pnpm config set store-dir .cache/pnpm
|
- corepack enable && pnpm config set store-dir .cache/pnpm
|
||||||
- pnpm cypress install
|
- pnpm cypress install
|
||||||
- pnpm run serve:dist & npx wait-on http://localhost:4173
|
- pnpm run test:e2e-record
|
||||||
- pnpm run test:frontend --browser chrome --record
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- build-prod
|
- build-prod
|
||||||
|
|
||||||
|
@ -204,7 +203,7 @@ steps:
|
||||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||||
commands:
|
commands:
|
||||||
- apk add git
|
- apk add git
|
||||||
- corepack enable && pnpm config set store-dir .cache/.pnpm
|
- corepack enable && pnpm config set store-dir .cache/pnpm
|
||||||
- pnpm install --fetch-timeout 100000 --frozen-lockfile
|
- pnpm install --fetch-timeout 100000 --frozen-lockfile
|
||||||
- pnpm run lint
|
- pnpm run lint
|
||||||
- "echo '{\"VERSION\": \"'$(git describe --tags --always --abbrev=10 | sed 's/-/+/' | sed 's/^v//' | sed 's/-g/-/')'\"}' > src/version.json"
|
- "echo '{\"VERSION\": \"'$(git describe --tags --always --abbrev=10 | sed 's/-/+/' | sed 's/^v//' | sed 's/-g/-/')'\"}' > src/version.json"
|
||||||
|
@ -365,7 +364,7 @@ steps:
|
||||||
- name: docker-unstable
|
- name: docker-unstable
|
||||||
image: thegeeklab/drone-docker-buildx
|
image: thegeeklab/drone-docker-buildx
|
||||||
privileged: true
|
privileged: true
|
||||||
pull: true
|
pull: always
|
||||||
settings:
|
settings:
|
||||||
username:
|
username:
|
||||||
from_secret: docker_username
|
from_secret: docker_username
|
||||||
|
@ -387,10 +386,20 @@ steps:
|
||||||
ref:
|
ref:
|
||||||
- refs/heads/main
|
- refs/heads/main
|
||||||
|
|
||||||
|
- name: generate-tags
|
||||||
|
image: thegeeklab/docker-autotag
|
||||||
|
environment:
|
||||||
|
DOCKER_AUTOTAG_VERSION: ${DRONE_TAG}
|
||||||
|
DOCKER_AUTOTAG_EXTRA_TAGS: latest
|
||||||
|
depends_on: [ fetch-tags ]
|
||||||
|
when:
|
||||||
|
ref:
|
||||||
|
- "refs/tags/**"
|
||||||
|
|
||||||
- name: docker-release
|
- name: docker-release
|
||||||
image: thegeeklab/drone-docker-buildx
|
image: thegeeklab/drone-docker-buildx
|
||||||
privileged: true
|
privileged: true
|
||||||
pull: true
|
pull: always
|
||||||
settings:
|
settings:
|
||||||
username:
|
username:
|
||||||
from_secret: docker_username
|
from_secret: docker_username
|
||||||
|
@ -407,7 +416,7 @@ steps:
|
||||||
- linux/arm/v6
|
- linux/arm/v6
|
||||||
- linux/arm/v7
|
- linux/arm/v7
|
||||||
- linux/arm64/v8
|
- linux/arm64/v8
|
||||||
depends_on: [ fetch-tags ]
|
depends_on: [ generate-tags ]
|
||||||
when:
|
when:
|
||||||
ref:
|
ref:
|
||||||
- "refs/tags/**"
|
- "refs/tags/**"
|
||||||
|
@ -451,9 +460,6 @@ kind: pipeline
|
||||||
type: docker
|
type: docker
|
||||||
name: update-translations
|
name: update-translations
|
||||||
|
|
||||||
depends_on:
|
|
||||||
- build
|
|
||||||
|
|
||||||
trigger:
|
trigger:
|
||||||
branch:
|
branch:
|
||||||
- main
|
- main
|
||||||
|
@ -513,6 +519,6 @@ steps:
|
||||||
from_secret: crowdin_key
|
from_secret: crowdin_key
|
||||||
---
|
---
|
||||||
kind: signature
|
kind: signature
|
||||||
hmac: 9f26b5af73e3464e9ee1b5fbcb96854ca8a7e5f8d6ee2d85fd8376aad951b446
|
hmac: 0a65a032008f2d1911866dcb0d9536b26c1d6b105943ed5328ac13abc99c423f
|
||||||
|
|
||||||
...
|
...
|
||||||
|
|
3
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
3
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
|
@ -1,6 +1,7 @@
|
||||||
name: Bug Report
|
name: Bug Report
|
||||||
description: Found something you weren't expecting? Report it here!
|
description: Found something you weren't expecting? Report it here!
|
||||||
labels: kind/bug
|
labels:
|
||||||
|
- kind/bug
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
|
|
37
.gitignore
vendored
37
.gitignore
vendored
|
@ -1,26 +1,31 @@
|
||||||
.DS_Store
|
# Logs
|
||||||
node_modules
|
|
||||||
/dist*
|
|
||||||
*.zip
|
|
||||||
.direnv/
|
|
||||||
|
|
||||||
# local env files
|
|
||||||
.env.local
|
|
||||||
.env.*.local
|
|
||||||
|
|
||||||
# Log files
|
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
stats.html
|
|
||||||
pnpm-debug.log*
|
pnpm-debug.log*
|
||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
|
stats.html
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
/dist*
|
||||||
|
coverage
|
||||||
|
*.zip
|
||||||
|
.direnv/
|
||||||
|
|
||||||
|
# Test files
|
||||||
|
cypress/screenshots
|
||||||
|
cypress/videos
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.idea
|
|
||||||
.vscode
|
.vscode
|
||||||
|
.idea
|
||||||
*.suo
|
*.suo
|
||||||
*.ntvs*
|
*.ntvs*
|
||||||
*.njsproj
|
*.njsproj
|
||||||
|
@ -28,9 +33,9 @@ lerna-debug.log*
|
||||||
*.sw*
|
*.sw*
|
||||||
!rollup.sw.js
|
!rollup.sw.js
|
||||||
|
|
||||||
# Test files
|
|
||||||
cypress/screenshots
|
|
||||||
cypress/videos
|
|
||||||
|
|
||||||
# Local Netlify folder
|
# Local Netlify folder
|
||||||
.netlify
|
.netlify
|
||||||
|
|
||||||
|
# histoire
|
||||||
|
.histoire
|
205
CHANGELOG.md
205
CHANGELOG.md
|
@ -9,6 +9,211 @@ All releases can be found on https://code.vikunja.io/frontend/releases.
|
||||||
|
|
||||||
The releases aim at the api versions which is why there are missing versions.
|
The releases aim at the api versions which is why there are missing versions.
|
||||||
|
|
||||||
|
## [0.20.2] - 2022-12-18
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* *(bug-report.yml)* List (#2845)
|
||||||
|
* *(quick add magic)* Don't create a new label multiple times if it is used in multiple tasks
|
||||||
|
* *(task)* Pass a list specified via quick add magic down to all subtasks created via indention
|
||||||
|
* *(task)* Move task color bubble next to task index and done badge on mobile
|
||||||
|
* *(tasks)* Remove a task from its bucket when it is in the first kanban bucket
|
||||||
|
* *(tasks)* Missing space when showing parent tasks and list title
|
||||||
|
* *(tasks)* Translation for multiple related tasks now works
|
||||||
|
* Move createdUpdated styles to component (#2685) ([4c458a1](4c458a1ad0761920868e3863982d5175664b3e6e))
|
||||||
|
* Move heading styles to component (#2686) ([293402b](293402b6fdfc699661c7f287ff1759a9ce5bea17))
|
||||||
|
* Use scss for datemathHelp (#2690) ([06775cf](06775cf4c72cf81a125b91d49c8d81e8649af661))
|
||||||
|
* Reactive const assignment (#2692) ([4c4adfd](4c4adfdf4e79eff3e101d9f0bd68bc3e5bb76495))
|
||||||
|
* Remove vuex leftover from setModuleLoading (#2716) ([3aaacf4](3aaacf4533c761864d3081edb92c9380df43f8b1))
|
||||||
|
* Icon offset and color ([74ad98d](74ad98de680f8b56e42886cd1e33874bd05772fa))
|
||||||
|
* Only load buckets if listId set (#2741) ([7db79ff](7db79ff04e4ce87d62cae7f93b67570bbc5c13be))
|
||||||
|
* Add all json files in src (#2737) ([422e731](422e731fe0d44c2e3be603b549538a05a695b95c))
|
||||||
|
* Vite.config imports (#2843) ([318e8c8](318e8c83a68bcb2f7953553c036f677a97b01c21))
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
* *(deps)* Update dependency rollup to v3.3.0 (#2689)
|
||||||
|
* *(deps)* Update dependency @types/dompurify to v2.4.0 (#2688)
|
||||||
|
* *(deps)* Update dependency @vue/test-utils to v2.2.2 (#2696)
|
||||||
|
* *(deps)* Update dependency caniuse-lite to v1.0.30001431
|
||||||
|
* *(deps)* Update dependency happy-dom to v7.7.0
|
||||||
|
* *(deps)* Update dependency netlify-cli to v12.1.1 (#2699)
|
||||||
|
* *(deps)* Update dependency postcss-preset-env to v7.8.3 (#2701)
|
||||||
|
* *(deps)* Update dependency vitest to v0.25.2 (#2702)
|
||||||
|
* *(deps)* Update pnpm to v7.16.0 (#2703)
|
||||||
|
* *(deps)* Update typescript-eslint monorepo to v5.43.0
|
||||||
|
* *(deps)* Update dependency ufo to v1
|
||||||
|
* *(deps)* Update dependency esbuild to v0.15.14 (#2706)
|
||||||
|
* *(deps)* Update dependency @vue/test-utils to v2.2.3 (#2707)
|
||||||
|
* *(deps)* Update dependency vite to v3.2.4
|
||||||
|
* *(deps)* Update dependency typescript to v4.9.3
|
||||||
|
* *(deps)* Update dependency cypress to v11.1.0
|
||||||
|
* *(deps)* Update font awesome to v6.2.1 (#2712)
|
||||||
|
* *(deps)* Update pnpm to v7.16.1 (#2717)
|
||||||
|
* *(deps)* Update dependency pinia to v2.0.24
|
||||||
|
* *(deps)* Update sentry-javascript monorepo to v7.20.0 (#2720)
|
||||||
|
* *(deps)* Update dependency eslint to v8.28.0
|
||||||
|
* *(deps)* Update dependency esbuild to v0.15.15
|
||||||
|
* *(deps)* Update dependency netlify-cli to v12.2.4
|
||||||
|
* *(deps)* Update dependency @vue/test-utils to v2.2.4
|
||||||
|
* *(deps)* Update pnpm to v7.17.0
|
||||||
|
* *(deps)* Update dependency marked to v4.2.3
|
||||||
|
* *(deps)* Update dependency codemirror to v5.65.10
|
||||||
|
* *(deps)* Update sentry-javascript monorepo to v7.20.1
|
||||||
|
* *(deps)* Update dependency pinia to v2.0.25
|
||||||
|
* *(deps)* Update dependency rollup to v3.4.0
|
||||||
|
* *(deps)* Update typescript-eslint monorepo to v5.44.0
|
||||||
|
* *(deps)* Update vueuse to v9.6.0 (#2742)
|
||||||
|
* *(deps)* Update dependency vitest to v0.25.3 (#2743)
|
||||||
|
* *(deps)* Update dependency cypress to v11.2.0
|
||||||
|
* *(deps)* Update sentry-javascript monorepo to v7.21.0
|
||||||
|
* *(deps)* Update dependency @4tw/cypress-drag-drop to v2.2.2
|
||||||
|
* *(deps)* Update sentry-javascript monorepo to v7.21.1 (#2747)
|
||||||
|
* *(deps)* Update dependency pinia to v2.0.26
|
||||||
|
* *(deps)* Update dependency @cypress/vue to v5.0.2
|
||||||
|
* *(deps)* Update dependency highlight.js to v11.7.0 (#2752)
|
||||||
|
* *(deps)* Update dependency eslint-plugin-vue to v9.8.0 (#2753)
|
||||||
|
* *(deps)* Update dependency @infectoone/vue-ganttastic to v2.1.3
|
||||||
|
* *(deps)* Update dependency rollup to v3.5.0 (#2756)
|
||||||
|
* *(deps)* Update pnpm to v7.17.1 (#2755)
|
||||||
|
* *(deps)* Update dependency esbuild to v0.15.16
|
||||||
|
* *(deps)* Update dependency pinia to v2.0.27 (#2757)
|
||||||
|
* *(deps)* Update dependency caniuse-lite to v1.0.30001434 (#2759)
|
||||||
|
* *(deps)* Update dependency netlify-cli to v12.2.7 (#2760)
|
||||||
|
* *(deps)* Update dependency @kyvg/vue3-notification to v2.7.0 (#2761)
|
||||||
|
* *(deps)* Update typescript-eslint monorepo to v5.45.0 (#2762)
|
||||||
|
* *(deps)* Update dependency ufo to v1.0.1 (#2763)
|
||||||
|
* *(deps)* Update dependency vue-tsc to v1.0.10 (#2764)
|
||||||
|
* *(deps)* Update sentry-javascript monorepo to v7.22.0 (#2765)
|
||||||
|
* *(deps)* Update dependency @types/node to v18.11.10 (#2768)
|
||||||
|
* *(deps)* Update dependency rollup to v3.5.1 (#2769)
|
||||||
|
* *(deps)* Update sentry-javascript monorepo to v7.23.0
|
||||||
|
* *(deps)* Update dependency @vue/test-utils to v2.2.5 (#2773)
|
||||||
|
* *(deps)* Update dependency eslint to v8.29.0 (#2774)
|
||||||
|
* *(deps)* Update dependency @cypress/vue to v5.0.3 (#2775)
|
||||||
|
* *(deps)* Update dependency vue-tsc to v1.0.11 (#2777)
|
||||||
|
* *(deps)* Update dependency @cypress/vite-dev-server to v5 (#2776)
|
||||||
|
* *(deps)* Update pnpm to v7.18.0 (#2778)
|
||||||
|
* *(deps)* Update dependency esbuild to v0.15.17 (#2779)
|
||||||
|
* *(deps)* Update dependency caniuse-lite to v1.0.30001436 (#2780)
|
||||||
|
* *(deps)* Update dependency @vue/test-utils to v2.2.6 (#2784)
|
||||||
|
* *(deps)* Update dependency esbuild to v0.15.18 (#2783)
|
||||||
|
* *(deps)* Update dependency netlify-cli to v12.2.8 (#2782)
|
||||||
|
* *(deps)* Update dependency happy-dom to v7.7.2 (#2781)
|
||||||
|
* *(deps)* Update dependency vite to v3.2.5 (#2785)
|
||||||
|
* *(deps)* Update dependency rollup to v3.6.0 (#2786)
|
||||||
|
* *(deps)* Update typescript-eslint monorepo to v5.45.1 (#2787)
|
||||||
|
* *(deps)* Update dependency vitest to v0.25.4 (#2788)
|
||||||
|
* *(deps)* Update dependency @types/node to v18.11.11 (#2789)
|
||||||
|
* *(deps)* Update pnpm to v7.18.1 (#2790)
|
||||||
|
* *(deps)* Update dependency dayjs to v1.11.7 (#2791)
|
||||||
|
* *(deps)* Update dependency cypress to v12 (#2792)
|
||||||
|
* *(deps)* Update dependency vitest to v0.25.5 (#2793)
|
||||||
|
* *(deps)* Update dependency marked to v4.2.4 (#2796)
|
||||||
|
* *(deps)* Update dependency esbuild to v0.16.1 (#2795)
|
||||||
|
* *(deps)* Update dependency cypress to v12.0.1 (#2794)
|
||||||
|
* *(deps)* Update sentry-javascript monorepo to v7.24.0 (#2797)
|
||||||
|
* *(deps)* Update sentry-javascript monorepo to v7.24.1 (#2798)
|
||||||
|
* *(deps)* Update sentry-javascript monorepo to v7.24.2 (#2799)
|
||||||
|
* *(deps)* Update dependency typescript to v4.9.4 (#2800)
|
||||||
|
* *(deps)* Update dependency rollup to v3.7.0 (#2801)
|
||||||
|
* *(deps)* Update dependency esbuild to v0.16.2 (#2802)
|
||||||
|
* *(deps)* Update typescript-eslint monorepo to v5.46.0 (#2803)
|
||||||
|
* *(deps)* Update dependency vitest to v0.25.6 (#2804)
|
||||||
|
* *(deps)* Update dependency @cypress/vite-dev-server to v5.0.1 (#2806)
|
||||||
|
* *(deps)* Update dependency esbuild to v0.16.3 (#2809)
|
||||||
|
* *(deps)* Update dependency sass to v1.56.2 (#2810)
|
||||||
|
* *(deps)* Update dependency @types/marked to v4.0.8 (#2812)
|
||||||
|
* *(deps)* Update dependency vue-tsc to v1.0.12 (#2811)
|
||||||
|
* *(deps)* Update dependency @types/node to v18.11.12 (#2808)
|
||||||
|
* *(deps)* Update dependency cypress to v12.0.2 (#2807)
|
||||||
|
* *(deps)* Update dependency @vitejs/plugin-vue to v4 (#2814)
|
||||||
|
* *(deps)* Update dependency @vitejs/plugin-legacy to v3 (#2813)
|
||||||
|
* *(deps)* Update dependency pinia to v2.0.28 (#2815)
|
||||||
|
* *(deps)* Update dependency @vitejs/plugin-legacy to v3.0.1 (#2818)
|
||||||
|
* *(deps)* Update dependency @cypress/vite-dev-server to v5.0.2 (#2819)
|
||||||
|
* *(deps)* Update dependency rollup to v3.7.1 (#2820)
|
||||||
|
* *(deps)* Update dependency rollup to v3.7.2 (#2822)
|
||||||
|
* *(deps)* Update dependency esbuild to v0.16.4 (#2821)
|
||||||
|
* *(deps)* Update dependency vitest to v0.25.7 (#2824)
|
||||||
|
* *(deps)* Update dependency @types/node to v18.11.13 (#2823)
|
||||||
|
* *(deps)* Update dependency happy-dom to v8 (#2831)
|
||||||
|
* *(deps)* Update dependency postcss to v8.4.20 (#2827)
|
||||||
|
* *(deps)* Update dependency caniuse-lite to v1.0.30001439 (#2828)
|
||||||
|
* *(deps)* Update dependency @intlify/unplugin-vue-i18n to v0.8.1 (#2826)
|
||||||
|
* *(deps)* Update dependency netlify-cli to v12.2.10 (#2829)
|
||||||
|
* *(deps)* Update dependency vite-plugin-pwa to v0.14.0 (#2833)
|
||||||
|
* *(deps)* Update dependency rollup to v3.7.3 (#2825)
|
||||||
|
* *(deps)* Update dependency vue-tsc to v1.0.13 (#2832)
|
||||||
|
* *(deps)* Update sentry-javascript monorepo to v7.25.0
|
||||||
|
* *(deps)* Update dependency vite to v4 (#2816)
|
||||||
|
* *(deps)* Update pnpm to v7.18.2 (#2834)
|
||||||
|
* *(deps)* Update typescript-eslint monorepo to v5.46.1 (#2837)
|
||||||
|
* *(deps)* Update dependency @4tw/cypress-drag-drop to v2.2.3 (#2836)
|
||||||
|
* *(deps)* Update dependency @types/node to v18.11.14 (#2839)
|
||||||
|
* *(deps)* Update dependency cypress to v12.1.0 (#2838)
|
||||||
|
* *(deps)* Update dependency rollup to v3.7.4 (#2840)
|
||||||
|
* *(deps)* Update dependency vitest to v0.25.8
|
||||||
|
* *(deps)* Update sentry-javascript monorepo to v7.26.0
|
||||||
|
* *(deps)* Update dependency esbuild to v0.16.5 (#2846)
|
||||||
|
* *(deps)* Update dependency @types/node to v18.11.15
|
||||||
|
* *(deps)* Update dependency esbuild to v0.16.6 (#2848)
|
||||||
|
* *(deps)* Update dependency esbuild to v0.16.7
|
||||||
|
* *(deps)* Update sentry-javascript monorepo to v7.27.0 (#2850)
|
||||||
|
* *(deps)* Update dependency @vueuse/core to v9.7.0 (#2851)
|
||||||
|
* *(deps)* Update dependency wait-on to v7 (#2852)
|
||||||
|
* *(deps)* Update dependency @types/node to v18.11.16 (#2853)
|
||||||
|
* *(deps)* Update dependency eslint to v8.30.0
|
||||||
|
* *(deps)* Update dependency rollup to v3.7.5 (#2857)
|
||||||
|
* *(deps)* Update dependency esbuild to v0.16.8 (#2854)
|
||||||
|
* *(deps)* Update dependency sass to v1.57.0 (#2856)
|
||||||
|
* *(deps)* Update dependency vue-tsc to v1.0.14 (#2860)
|
||||||
|
* *(deps)* Update dependency esbuild to v0.16.9 (#2859)
|
||||||
|
* *(deps)* Update dependency @types/node to v18.11.17 (#2858)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* *(ci)* Use docker buildx for multiarch builds* Filters script setup ([4bad685](4bad685f39388d59fdd8ff79a1766c55f75262c2))
|
||||||
|
* Move select filters to dedicated components ([bb58dba](bb58dba8e07d683c75637ec88a378e873711eb29))
|
||||||
|
* Add vite build target esnext (#2674) ([163d936](163d9366d3061c40b5db7f3aad5c2cea01948403))
|
||||||
|
* Filters script setup (#2671) ([4a550da](4a550da6a69a50126b9d4a555b6713687347c2d3))
|
||||||
|
* Reduce multiselect selector specificity (#2678) ([9f0f0b3](9f0f0b39f8eea399b7b03003afa5893d0b8016f8))
|
||||||
|
* Reduce contentAuth selector specifity (#2677) ([12a8f7e](12a8f7ebe9fc556a7b0bc6e2d74e81d424ccfcf8))
|
||||||
|
* Reduce ListWrapper selector specificity (#2679) ([599c1ba](599c1ba4b5b0861d89755addf016e8f797b49dfe))
|
||||||
|
* Reduce dropdown-item selector specificity (#2680) ([eb4c2a4](eb4c2a4b9df93ee35404cd7143cc88b3d44f9d59))
|
||||||
|
* Reduce attachments selector specificity (#2682) ([0f1f131](0f1f131f7a2a38ee57175edfd5ed1c932225af16))
|
||||||
|
* Reduce ready selector specificity (#2683) ([9d604f7](9d604f7a3bc057bbe27ac19e73ac59736154d9b7))
|
||||||
|
* Use img for logo so that it's not part of the main bundle (#2684) ([02de481](02de481297502ad4b0b2eb2fa3e06366cce6d630))
|
||||||
|
* Improve user component (#2687) ([708ef2d](708ef2d72efbdfe6261322937b0a8f76ee19b9e4))
|
||||||
|
* Reduce TaskDetailView selector specificity ([fba402f](fba402fcd056ee397ce54f97ed4fec98845c7933))
|
||||||
|
* Move transition in own component ([631a19f](631a19fa923dba2759603e6a8b224cb4d3e1a038))
|
||||||
|
* Feature/load-views-async (#2672)
|
||||||
|
* Use transition component everywhere ([8c44ed8](8c44ed83e6530f67cc923a5e6d1a26c14575884a))
|
||||||
|
* Move transition in component (#2694) ([77ff0aa](77ff0aa256fbf388210af09d88673475386b3553))
|
||||||
|
* Disable fullscreen for EasyMDE side-by-side mode (#2710) ([98b38af](98b38af43c3acc9822f167ebca295f5aecb4908d))
|
||||||
|
* Only automatically redirect to provider if the url contains ?redirectToProvider=true and it's the only one ([3891d5b](3891d5b87634c890265477680fafaa04ff06cc3e))
|
||||||
|
* Improve loadTask logic (#2715) ([8ef3092](8ef309243db4e37d306167455987572006858cad))
|
||||||
|
* Remove edit-task from list view (#2721) ([45ec162](45ec1623d525ed31a49b6be6d609802c341fad27))
|
||||||
|
* Move useAutoHeightTextarea to composable (#2723) ([33d4efe](33d4efecc45ef8da5360fb878b7d365d1901b56c))
|
||||||
|
* More horizontal space on mobile (#2722) ([b42e4cc](b42e4cca59e338278261bc3ec613eefedde6fcce))
|
||||||
|
* Change list-content style (#91) ([4b47478](4b47478440d0af1bf24c44ea614c0f62f20723f7))
|
||||||
|
* Grid for list cards ([42e9f30](42e9f306e84120ba51d9b527c7868148730bf892))
|
||||||
|
* Move avatar class to where it is used (#2725) ([da8df8b](da8df8b667fc57798c1de7d78c1a7f88b0419d38))
|
||||||
|
* Undent and order navigation css ([66be0e6](66be0e6ac4bcf48124b33267224187b56ac9320a))
|
||||||
|
* Outdent navigation logo styles ([ff9efe7](ff9efe7889256706ac86bb1face842cd2de6f935))
|
||||||
|
* Group navigation styles further ([4fc7b9c](4fc7b9c67e2088e82760005cd530ea97cf796a4c))
|
||||||
|
* Move link color location together ([d9984b2](d9984b28f7d01da0f9d8f0afd5b6f0edf35823c2))
|
||||||
|
* Use fetch instead of axios for deploy preview (#2719) ([93d95b0](93d95b0821f39719c4a28c144ebb583c2eac754e))
|
||||||
|
* Remove useRouteQuery (#2751) ([3ee0bc3](3ee0bc345d6cd65769789ec029c50e652d80e1ca))
|
||||||
|
* Use Intl.DateTimeFormat for gantt weekdays (#2766) ([3b95824](3b95824f5834d7de50210414c56b07889db895c7))
|
||||||
|
* Add @intlify/unplugin-vue-i18n (#2772) ([b44d11c](b44d11cfc04712b9f9ec9479ba3a77a26c453532))
|
||||||
|
* Use vite preview for serve:dist:dev (#2842) ([f6c6f52](f6c6f52abe71674fa5f3951cc0ba61798758bd03))
|
||||||
|
* Use variable fonts with subsetting (#2817) ([b6a89a0](b6a89a0cde3c769e38146b05c33ff4ca4e97bca2))
|
||||||
|
|
||||||
|
### Other
|
||||||
|
|
||||||
|
* *(other)* [skip ci] Updated translations via Crowdin
|
||||||
|
|
||||||
## [0.20.1] - 2022-11-11
|
## [0.20.1] - 2022-11-11
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
[![Build Status](https://drone.kolaente.de/api/badges/vikunja/frontend/status.svg)](https://drone.kolaente.de/vikunja/frontend)
|
[![Build Status](https://drone.kolaente.de/api/badges/vikunja/frontend/status.svg)](https://drone.kolaente.de/vikunja/frontend)
|
||||||
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](LICENSE)
|
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](LICENSE)
|
||||||
[![Download](https://img.shields.io/badge/download-v0.20.1-brightgreen.svg)](https://dl.vikunja.io)
|
[![Download](https://img.shields.io/badge/download-v0.20.2-brightgreen.svg)](https://dl.vikunja.io)
|
||||||
[![Translation](https://badges.crowdin.net/vikunja/localized.svg)](https://crowdin.com/project/vikunja)
|
[![Translation](https://badges.crowdin.net/vikunja/localized.svg)](https://crowdin.com/project/vikunja)
|
||||||
|
|
||||||
This is the web frontend for Vikunja, written in Vue.js.
|
This is the web frontend for Vikunja, written in Vue.js.
|
||||||
|
|
|
@ -11,8 +11,10 @@ export default defineConfig({
|
||||||
},
|
},
|
||||||
projectId: '181c7x',
|
projectId: '181c7x',
|
||||||
e2e: {
|
e2e: {
|
||||||
baseUrl: 'http://localhost:4173',
|
specPattern: 'cypress/e2e/**/*.{cy,spec}.{js,jsx,ts,tsx}',
|
||||||
specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}',
|
baseUrl: 'http://127.0.0.1:4173',
|
||||||
|
experimentalRunAllSpecs: true,
|
||||||
|
// testIsolation: false,
|
||||||
},
|
},
|
||||||
component: {
|
component: {
|
||||||
devServer: {
|
devServer: {
|
||||||
|
|
|
@ -36,7 +36,7 @@ to get a shell inside the cypress container.
|
||||||
In that shell you can then execute the tests with
|
In that shell you can then execute the tests with
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
pnpm run test:frontend
|
pnpm run test:e2e
|
||||||
```
|
```
|
||||||
|
|
||||||
### Using The Cypress Dashboard
|
### Using The Cypress Dashboard
|
||||||
|
@ -44,5 +44,5 @@ pnpm run test:frontend
|
||||||
To open the Cypress Dashboard and run tests from there, run
|
To open the Cypress Dashboard and run tests from there, run
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
pnpm run cypress:open
|
pnpm run test:e2e:dev
|
||||||
```
|
```
|
||||||
|
|
|
@ -9,7 +9,7 @@ services:
|
||||||
ports:
|
ports:
|
||||||
- 3456:3456
|
- 3456:3456
|
||||||
cypress:
|
cypress:
|
||||||
image: cypress/browsers:node16.14.0-chrome99-ff97
|
image: cypress/browsers:node18.12.0-chrome107
|
||||||
volumes:
|
volumes:
|
||||||
- ..:/project
|
- ..:/project
|
||||||
- $HOME/.cache:/home/node/.cache/
|
- $HOME/.cache:/home/node/.cache/
|
||||||
|
|
|
@ -1,56 +0,0 @@
|
||||||
import {ListFactory} from '../../factories/list'
|
|
||||||
|
|
||||||
import '../../support/authenticateUser'
|
|
||||||
import {prepareLists} from './prepareLists'
|
|
||||||
|
|
||||||
describe('List History', () => {
|
|
||||||
prepareLists()
|
|
||||||
|
|
||||||
it('should show a list history on the home page', () => {
|
|
||||||
cy.intercept(Cypress.env('API_URL') + '/namespaces*').as('loadNamespaces')
|
|
||||||
cy.intercept(Cypress.env('API_URL') + '/lists/*').as('loadList')
|
|
||||||
|
|
||||||
const lists = ListFactory.create(6)
|
|
||||||
|
|
||||||
cy.visit('/')
|
|
||||||
cy.wait('@loadNamespaces')
|
|
||||||
cy.get('body')
|
|
||||||
.should('not.contain', 'Last viewed')
|
|
||||||
|
|
||||||
cy.visit(`/lists/${lists[0].id}`)
|
|
||||||
cy.wait('@loadNamespaces')
|
|
||||||
cy.wait('@loadList')
|
|
||||||
cy.visit(`/lists/${lists[1].id}`)
|
|
||||||
cy.wait('@loadNamespaces')
|
|
||||||
cy.wait('@loadList')
|
|
||||||
cy.visit(`/lists/${lists[2].id}`)
|
|
||||||
cy.wait('@loadNamespaces')
|
|
||||||
cy.wait('@loadList')
|
|
||||||
cy.visit(`/lists/${lists[3].id}`)
|
|
||||||
cy.wait('@loadNamespaces')
|
|
||||||
cy.wait('@loadList')
|
|
||||||
cy.visit(`/lists/${lists[4].id}`)
|
|
||||||
cy.wait('@loadNamespaces')
|
|
||||||
cy.wait('@loadList')
|
|
||||||
cy.visit(`/lists/${lists[5].id}`)
|
|
||||||
cy.wait('@loadNamespaces')
|
|
||||||
cy.wait('@loadList')
|
|
||||||
|
|
||||||
// cy.visit('/')
|
|
||||||
// cy.wait('@loadNamespaces')
|
|
||||||
// Not using cy.visit here to work around the redirect issue fixed in #1337
|
|
||||||
cy.get('nav.menu.top-menu a')
|
|
||||||
.contains('Overview')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('body')
|
|
||||||
.should('contain', 'Last viewed')
|
|
||||||
cy.get('.list-cards-wrapper-2-rows')
|
|
||||||
.should('not.contain', lists[0].title)
|
|
||||||
.should('contain', lists[1].title)
|
|
||||||
.should('contain', lists[2].title)
|
|
||||||
.should('contain', lists[3].title)
|
|
||||||
.should('contain', lists[4].title)
|
|
||||||
.should('contain', lists[5].title)
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,120 +0,0 @@
|
||||||
import {TaskFactory} from '../../factories/task'
|
|
||||||
import {prepareLists} from './prepareLists'
|
|
||||||
|
|
||||||
import '../../support/authenticateUser'
|
|
||||||
|
|
||||||
describe('Lists', () => {
|
|
||||||
let lists
|
|
||||||
prepareLists((newLists) => (lists = newLists))
|
|
||||||
|
|
||||||
it('Should create a new list', () => {
|
|
||||||
cy.visit('/')
|
|
||||||
cy.get('.namespace-title .dropdown-trigger')
|
|
||||||
.click()
|
|
||||||
cy.get('.namespace-title .dropdown .dropdown-item')
|
|
||||||
.contains('New list')
|
|
||||||
.click()
|
|
||||||
cy.url()
|
|
||||||
.should('contain', '/lists/new/1')
|
|
||||||
cy.get('.card-header-title')
|
|
||||||
.contains('New list')
|
|
||||||
cy.get('input.input')
|
|
||||||
.type('New List')
|
|
||||||
cy.get('.button')
|
|
||||||
.contains('Create')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.global-notification', { timeout: 1000 }) // Waiting until the request to create the new list is done
|
|
||||||
.should('contain', 'Success')
|
|
||||||
cy.url()
|
|
||||||
.should('contain', '/lists/')
|
|
||||||
cy.get('.list-title h1')
|
|
||||||
.should('contain', 'New List')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should redirect to a specific list view after visited', () => {
|
|
||||||
cy.visit('/lists/1/kanban')
|
|
||||||
cy.url()
|
|
||||||
.should('contain', '/lists/1/kanban')
|
|
||||||
cy.visit('/lists/1')
|
|
||||||
cy.url()
|
|
||||||
.should('contain', '/lists/1/kanban')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should rename the list in all places', () => {
|
|
||||||
TaskFactory.create(5, {
|
|
||||||
id: '{increment}',
|
|
||||||
list_id: 1,
|
|
||||||
})
|
|
||||||
const newListName = 'New list name'
|
|
||||||
|
|
||||||
cy.visit('/lists/1')
|
|
||||||
cy.get('.list-title h1')
|
|
||||||
.should('contain', 'First List')
|
|
||||||
|
|
||||||
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .dropdown-trigger')
|
|
||||||
.click()
|
|
||||||
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .dropdown-content')
|
|
||||||
.contains('Edit')
|
|
||||||
.click()
|
|
||||||
cy.get('#title')
|
|
||||||
.type(`{selectall}${newListName}`)
|
|
||||||
cy.get('footer.card-footer .button')
|
|
||||||
.contains('Save')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.global-notification')
|
|
||||||
.should('contain', 'Success')
|
|
||||||
cy.get('.list-title h1')
|
|
||||||
.should('contain', newListName)
|
|
||||||
.should('not.contain', lists[0].title)
|
|
||||||
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child')
|
|
||||||
.should('contain', newListName)
|
|
||||||
.should('not.contain', lists[0].title)
|
|
||||||
cy.visit('/')
|
|
||||||
cy.get('.card-content')
|
|
||||||
.should('contain', newListName)
|
|
||||||
.should('not.contain', lists[0].title)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should remove a list', () => {
|
|
||||||
cy.visit(`/lists/${lists[0].id}`)
|
|
||||||
|
|
||||||
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .dropdown-trigger')
|
|
||||||
.click()
|
|
||||||
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .dropdown-content')
|
|
||||||
.contains('Delete')
|
|
||||||
.click()
|
|
||||||
cy.url()
|
|
||||||
.should('contain', '/settings/delete')
|
|
||||||
cy.get('[data-cy="modalPrimary"]')
|
|
||||||
.contains('Do it')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.global-notification')
|
|
||||||
.should('contain', 'Success')
|
|
||||||
cy.get('.namespace-container .menu.namespaces-lists .menu-list')
|
|
||||||
.should('not.contain', lists[0].title)
|
|
||||||
cy.location('pathname')
|
|
||||||
.should('equal', '/')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should archive a list', () => {
|
|
||||||
cy.visit(`/lists/${lists[0].id}`)
|
|
||||||
|
|
||||||
cy.get('.list-title .dropdown')
|
|
||||||
.click()
|
|
||||||
cy.get('.list-title .dropdown .dropdown-menu .dropdown-item')
|
|
||||||
.contains('Archive')
|
|
||||||
.click()
|
|
||||||
cy.get('.modal-content')
|
|
||||||
.should('contain.text', 'Archive this list')
|
|
||||||
cy.get('.modal-content [data-cy=modalPrimary]')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.namespace-container .menu.namespaces-lists .menu-list')
|
|
||||||
.should('not.contain', lists[0].title)
|
|
||||||
cy.get('main.app-content')
|
|
||||||
.should('contain.text', 'This list is archived. It is not possible to create new or edit tasks for it.')
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,21 +0,0 @@
|
||||||
import {ListFactory} from '../../factories/list'
|
|
||||||
import {UserFactory} from '../../factories/user'
|
|
||||||
import {NamespaceFactory} from '../../factories/namespace'
|
|
||||||
import {TaskFactory} from '../../factories/task'
|
|
||||||
|
|
||||||
export function createLists() {
|
|
||||||
UserFactory.create(1)
|
|
||||||
NamespaceFactory.create(1)
|
|
||||||
const lists = ListFactory.create(1, {
|
|
||||||
title: 'First List'
|
|
||||||
})
|
|
||||||
TaskFactory.truncate()
|
|
||||||
return lists
|
|
||||||
}
|
|
||||||
|
|
||||||
export function prepareLists(setLists = () => {}) {
|
|
||||||
beforeEach(() => {
|
|
||||||
const lists = createLists()
|
|
||||||
setLists(lists)
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,16 +1,18 @@
|
||||||
import {TaskFactory} from '../../factories/task'
|
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||||
import {ListFactory} from '../../factories/list'
|
|
||||||
import {NamespaceFactory} from '../../factories/namespace'
|
|
||||||
import {UserListFactory} from '../../factories/users_list'
|
|
||||||
|
|
||||||
import '../../support/authenticateUser'
|
import {TaskFactory} from '../../factories/task'
|
||||||
|
import {ProjectFactory} from '../../factories/project'
|
||||||
|
import {NamespaceFactory} from '../../factories/namespace'
|
||||||
|
import {UserProjectFactory} from '../../factories/users_project'
|
||||||
|
|
||||||
describe('Editor', () => {
|
describe('Editor', () => {
|
||||||
|
createFakeUserAndLogin()
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
NamespaceFactory.create(1)
|
NamespaceFactory.create(1)
|
||||||
const lists = ListFactory.create(1)
|
const projects = ProjectFactory.create(1)
|
||||||
TaskFactory.truncate()
|
TaskFactory.truncate()
|
||||||
UserListFactory.truncate()
|
UserProjectFactory.truncate()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Has a preview with checkable checkboxes', () => {
|
it('Has a preview with checkable checkboxes', () => {
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
import '../../support/authenticateUser'
|
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||||
|
|
||||||
describe('The Menu', () => {
|
describe('The Menu', () => {
|
||||||
|
createFakeUserAndLogin()
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('/')
|
||||||
|
})
|
||||||
|
|
||||||
it('Is visible by default on desktop', () => {
|
it('Is visible by default on desktop', () => {
|
||||||
cy.get('.namespace-container')
|
cy.get('.namespace-container')
|
||||||
.should('have.class', 'is-active')
|
.should('have.class', 'is-active')
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import {UserFactory} from '../../factories/user'
|
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||||
|
|
||||||
import '../../support/authenticateUser'
|
import {ProjectFactory} from '../../factories/project'
|
||||||
import {ListFactory} from '../../factories/list'
|
|
||||||
import {NamespaceFactory} from '../../factories/namespace'
|
import {NamespaceFactory} from '../../factories/namespace'
|
||||||
|
|
||||||
describe('Namepaces', () => {
|
describe('Namepaces', () => {
|
||||||
|
createFakeUserAndLogin()
|
||||||
|
|
||||||
let namespaces
|
let namespaces
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
UserFactory.create(1)
|
|
||||||
namespaces = NamespaceFactory.create(1)
|
namespaces = NamespaceFactory.create(1)
|
||||||
ListFactory.create(1)
|
ProjectFactory.create(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should be all there', () => {
|
it('Should be all there', () => {
|
||||||
|
@ -51,9 +51,9 @@ describe('Namepaces', () => {
|
||||||
|
|
||||||
cy.visit('/namespaces')
|
cy.visit('/namespaces')
|
||||||
|
|
||||||
cy.get(`.namespace-container .menu.namespaces-lists .namespace-title:contains(${newNamespaces[0].title}) .dropdown .dropdown-trigger`)
|
cy.get(`.namespace-container .menu.namespaces-projects .namespace-title:contains(${newNamespaces[0].title}) .dropdown .dropdown-trigger`)
|
||||||
.click()
|
.click()
|
||||||
cy.get('.namespace-container .menu.namespaces-lists .namespace-title .dropdown .dropdown-content')
|
cy.get('.namespace-container .menu.namespaces-projects .namespace-title .dropdown .dropdown-content')
|
||||||
.contains('Edit')
|
.contains('Edit')
|
||||||
.click()
|
.click()
|
||||||
cy.url()
|
cy.url()
|
||||||
|
@ -69,7 +69,7 @@ describe('Namepaces', () => {
|
||||||
|
|
||||||
cy.get('.global-notification', { timeout: 1000 })
|
cy.get('.global-notification', { timeout: 1000 })
|
||||||
.should('contain', 'Success')
|
.should('contain', 'Success')
|
||||||
cy.get('.namespace-container .menu.namespaces-lists')
|
cy.get('.namespace-container .menu.namespaces-projects')
|
||||||
.should('contain', newNamespaceName)
|
.should('contain', newNamespaceName)
|
||||||
.should('not.contain', newNamespaces[0].title)
|
.should('not.contain', newNamespaces[0].title)
|
||||||
cy.get('[data-cy="namespaces-list"]')
|
cy.get('[data-cy="namespaces-list"]')
|
||||||
|
@ -82,9 +82,9 @@ describe('Namepaces', () => {
|
||||||
|
|
||||||
cy.visit('/')
|
cy.visit('/')
|
||||||
|
|
||||||
cy.get(`.namespace-container .menu.namespaces-lists .namespace-title:contains(${newNamespaces[0].title}) .dropdown .dropdown-trigger`)
|
cy.get(`.namespace-container .menu.namespaces-projects .namespace-title:contains(${newNamespaces[0].title}) .dropdown .dropdown-trigger`)
|
||||||
.click()
|
.click()
|
||||||
cy.get('.namespace-container .menu.namespaces-lists .namespace-title .dropdown .dropdown-content')
|
cy.get('.namespace-container .menu.namespaces-projects .namespace-title .dropdown .dropdown-content')
|
||||||
.contains('Delete')
|
.contains('Delete')
|
||||||
.click()
|
.click()
|
||||||
cy.url()
|
cy.url()
|
||||||
|
@ -95,21 +95,21 @@ describe('Namepaces', () => {
|
||||||
|
|
||||||
cy.get('.global-notification')
|
cy.get('.global-notification')
|
||||||
.should('contain', 'Success')
|
.should('contain', 'Success')
|
||||||
cy.get('.namespace-container .menu.namespaces-lists')
|
cy.get('.namespace-container .menu.namespaces-projects')
|
||||||
.should('not.contain', newNamespaces[0].title)
|
.should('not.contain', newNamespaces[0].title)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should not show archived lists & namespaces if the filter is not checked', () => {
|
it('Should not show archived projects & namespaces if the filter is not checked', () => {
|
||||||
const n = NamespaceFactory.create(1, {
|
const n = NamespaceFactory.create(1, {
|
||||||
id: 2,
|
id: 2,
|
||||||
is_archived: true,
|
is_archived: true,
|
||||||
}, false)
|
}, false)
|
||||||
ListFactory.create(1, {
|
ProjectFactory.create(1, {
|
||||||
id: 2,
|
id: 2,
|
||||||
namespace_id: n[0].id,
|
namespace_id: n[0].id,
|
||||||
}, false)
|
}, false)
|
||||||
|
|
||||||
ListFactory.create(1, {
|
ProjectFactory.create(1, {
|
||||||
id: 3,
|
id: 3,
|
||||||
is_archived: true,
|
is_archived: true,
|
||||||
}, false)
|
}, false)
|
19
cypress/e2e/project/prepareProjects.ts
Normal file
19
cypress/e2e/project/prepareProjects.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import {ProjectFactory} from '../../factories/project'
|
||||||
|
import {NamespaceFactory} from '../../factories/namespace'
|
||||||
|
import {TaskFactory} from '../../factories/task'
|
||||||
|
|
||||||
|
export function createProjects() {
|
||||||
|
NamespaceFactory.create(1)
|
||||||
|
const projects = ProjectFactory.create(1, {
|
||||||
|
title: 'First Project'
|
||||||
|
})
|
||||||
|
TaskFactory.truncate()
|
||||||
|
return projects
|
||||||
|
}
|
||||||
|
|
||||||
|
export function prepareProjects(setProjects = () => {}) {
|
||||||
|
beforeEach(() => {
|
||||||
|
const projects = createProjects()
|
||||||
|
setProjects(projects)
|
||||||
|
})
|
||||||
|
}
|
57
cypress/e2e/project/project-history.spec.ts
Normal file
57
cypress/e2e/project/project-history.spec.ts
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||||
|
|
||||||
|
import {ProjectFactory} from '../../factories/project'
|
||||||
|
import {prepareProjects} from './prepareProjects'
|
||||||
|
|
||||||
|
describe('Project History', () => {
|
||||||
|
createFakeUserAndLogin()
|
||||||
|
prepareLists()
|
||||||
|
|
||||||
|
it('should show a project history on the home page', () => {
|
||||||
|
cy.intercept(Cypress.env('API_URL') + '/namespaces*').as('loadNamespaces')
|
||||||
|
cy.intercept(Cypress.env('API_URL') + '/projects/*').as('loadProject')
|
||||||
|
|
||||||
|
const projects = ProjectFactory.create(6)
|
||||||
|
|
||||||
|
cy.visit('/')
|
||||||
|
cy.wait('@loadNamespaces')
|
||||||
|
cy.get('body')
|
||||||
|
.should('not.contain', 'Last viewed')
|
||||||
|
|
||||||
|
cy.visit(`/projects/${projects[0].id}`)
|
||||||
|
cy.wait('@loadNamespaces')
|
||||||
|
cy.wait('@loadProject')
|
||||||
|
cy.visit(`/projects/${projects[1].id}`)
|
||||||
|
cy.wait('@loadNamespaces')
|
||||||
|
cy.wait('@loadProject')
|
||||||
|
cy.visit(`/projects/${projects[2].id}`)
|
||||||
|
cy.wait('@loadNamespaces')
|
||||||
|
cy.wait('@loadProject')
|
||||||
|
cy.visit(`/projects/${projects[3].id}`)
|
||||||
|
cy.wait('@loadNamespaces')
|
||||||
|
cy.wait('@loadProject')
|
||||||
|
cy.visit(`/projects/${projects[4].id}`)
|
||||||
|
cy.wait('@loadNamespaces')
|
||||||
|
cy.wait('@loadProject')
|
||||||
|
cy.visit(`/projects/${projects[5].id}`)
|
||||||
|
cy.wait('@loadNamespaces')
|
||||||
|
cy.wait('@loadProject')
|
||||||
|
|
||||||
|
// cy.visit('/')
|
||||||
|
// cy.wait('@loadNamespaces')
|
||||||
|
// Not using cy.visit here to work around the redirect issue fixed in #1337
|
||||||
|
cy.get('nav.menu.top-menu a')
|
||||||
|
.contains('Overview')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.get('body')
|
||||||
|
.should('contain', 'Last viewed')
|
||||||
|
cy.get('[data-cy="listCardGrid"]')
|
||||||
|
.should('not.contain', projects[0].title)
|
||||||
|
.should('contain', projects[1].title)
|
||||||
|
.should('contain', projects[2].title)
|
||||||
|
.should('contain', projects[3].title)
|
||||||
|
.should('contain', projects[4].title)
|
||||||
|
.should('contain', projects[5].title)
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,15 +1,17 @@
|
||||||
import {formatISO, format} from 'date-fns'
|
import {formatISO, format} from 'date-fns'
|
||||||
|
|
||||||
|
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||||
|
|
||||||
import {TaskFactory} from '../../factories/task'
|
import {TaskFactory} from '../../factories/task'
|
||||||
import {prepareLists} from './prepareLists'
|
import {prepareProjects} from './prepareProjects'
|
||||||
|
|
||||||
import '../../support/authenticateUser'
|
describe('Project View Gantt', () => {
|
||||||
|
createFakeUserAndLogin()
|
||||||
describe('List View Gantt', () => {
|
prepareProjects()
|
||||||
prepareLists()
|
|
||||||
|
|
||||||
it('Hides tasks with no dates', () => {
|
it('Hides tasks with no dates', () => {
|
||||||
const tasks = TaskFactory.create(1)
|
const tasks = TaskFactory.create(1)
|
||||||
cy.visit('/lists/1/gantt')
|
cy.visit('/projects/1/gantt')
|
||||||
|
|
||||||
cy.get('.g-gantt-rows-container')
|
cy.get('.g-gantt-rows-container')
|
||||||
.should('not.contain', tasks[0].title)
|
.should('not.contain', tasks[0].title)
|
||||||
|
@ -23,7 +25,7 @@ describe('List View Gantt', () => {
|
||||||
nextMonth.setDate(1)
|
nextMonth.setDate(1)
|
||||||
nextMonth.setMonth(9)
|
nextMonth.setMonth(9)
|
||||||
|
|
||||||
cy.visit('/lists/1/gantt')
|
cy.visit('/projects/1/gantt')
|
||||||
|
|
||||||
cy.get('.g-timeunits-container')
|
cy.get('.g-timeunits-container')
|
||||||
.should('contain', format(now, 'MMMM'))
|
.should('contain', format(now, 'MMMM'))
|
||||||
|
@ -33,10 +35,10 @@ describe('List View Gantt', () => {
|
||||||
it('Shows tasks with dates', () => {
|
it('Shows tasks with dates', () => {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const tasks = TaskFactory.create(1, {
|
const tasks = TaskFactory.create(1, {
|
||||||
start_date: formatISO(now),
|
start_date: now.toISOString(),
|
||||||
end_date: formatISO(now.setDate(now.getDate() + 4)),
|
end_date: new Date(new Date(now).setDate(now.getDate() + 4)).toISOString(),
|
||||||
})
|
})
|
||||||
cy.visit('/lists/1/gantt')
|
cy.visit('/projects/1/gantt')
|
||||||
|
|
||||||
cy.get('.g-gantt-rows-container')
|
cy.get('.g-gantt-rows-container')
|
||||||
.should('not.be.empty')
|
.should('not.be.empty')
|
||||||
|
@ -48,7 +50,7 @@ describe('List View Gantt', () => {
|
||||||
start_date: null,
|
start_date: null,
|
||||||
end_date: null,
|
end_date: null,
|
||||||
})
|
})
|
||||||
cy.visit('/lists/1/gantt')
|
cy.visit('/projects/1/gantt')
|
||||||
|
|
||||||
cy.get('.gantt-options .fancycheckbox')
|
cy.get('.gantt-options .fancycheckbox')
|
||||||
.contains('Show tasks which don\'t have dates set')
|
.contains('Show tasks which don\'t have dates set')
|
||||||
|
@ -60,15 +62,14 @@ describe('List View Gantt', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Drags a task around', () => {
|
it('Drags a task around', () => {
|
||||||
cy.intercept('**/api/v1/tasks/*')
|
cy.intercept(Cypress.env('API_URL') + '/tasks/*').as('taskUpdate')
|
||||||
.as('taskUpdate')
|
|
||||||
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
TaskFactory.create(1, {
|
TaskFactory.create(1, {
|
||||||
start_date: formatISO(now),
|
start_date: now.toISOString(),
|
||||||
end_date: formatISO(now.setDate(now.getDate() + 4)),
|
end_date: new Date(new Date(now).setDate(now.getDate() + 4)).toISOString(),
|
||||||
})
|
})
|
||||||
cy.visit('/lists/1/gantt')
|
cy.visit('/projects/1/gantt')
|
||||||
|
|
||||||
cy.get('.g-gantt-rows-container .g-gantt-row .g-gantt-row-bars-container div .g-gantt-bar')
|
cy.get('.g-gantt-rows-container .g-gantt-row .g-gantt-row-bars-container div .g-gantt-bar')
|
||||||
.first()
|
.first()
|
||||||
|
@ -82,9 +83,9 @@ describe('List View Gantt', () => {
|
||||||
const now = Date.UTC(2022, 10, 9)
|
const now = Date.UTC(2022, 10, 9)
|
||||||
cy.clock(now, ['Date'])
|
cy.clock(now, ['Date'])
|
||||||
|
|
||||||
cy.visit('/lists/1/gantt')
|
cy.visit('/projects/1/gantt')
|
||||||
|
|
||||||
cy.get('.list-gantt .gantt-options .field .control input.input.form-control')
|
cy.get('.project-gantt .gantt-options .field .control input.input.form-control')
|
||||||
.click()
|
.click()
|
||||||
cy.get('.flatpickr-calendar .flatpickr-innerContainer .dayContainer .flatpickr-day')
|
cy.get('.flatpickr-calendar .flatpickr-innerContainer .dayContainer .flatpickr-day')
|
||||||
.first()
|
.first()
|
||||||
|
@ -98,13 +99,13 @@ describe('List View Gantt', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should change the date range based on date query parameters', () => {
|
it('Should change the date range based on date query parameters', () => {
|
||||||
cy.visit('/lists/1/gantt?dateFrom=2022-09-25&dateTo=2022-11-05')
|
cy.visit('/projects/1/gantt?dateFrom=2022-09-25&dateTo=2022-11-05')
|
||||||
|
|
||||||
cy.get('.g-timeunits-container')
|
cy.get('.g-timeunits-container')
|
||||||
.should('contain', 'September 2022')
|
.should('contain', 'September 2022')
|
||||||
.should('contain', 'October 2022')
|
.should('contain', 'October 2022')
|
||||||
.should('contain', 'November 2022')
|
.should('contain', 'November 2022')
|
||||||
cy.get('.list-gantt .gantt-options .field .control input.input.form-control')
|
cy.get('.project-gantt .gantt-options .field .control input.input.form-control')
|
||||||
.should('have.value', '25 Sep 2022 to 5 Nov 2022')
|
.should('have.value', '25 Sep 2022 to 5 Nov 2022')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -114,7 +115,7 @@ describe('List View Gantt', () => {
|
||||||
start_date: formatISO(now),
|
start_date: formatISO(now),
|
||||||
end_date: formatISO(now.setDate(now.getDate() + 4)),
|
end_date: formatISO(now.setDate(now.getDate() + 4)),
|
||||||
})
|
})
|
||||||
cy.visit('/lists/1/gantt')
|
cy.visit('/projects/1/gantt')
|
||||||
|
|
||||||
cy.get('.gantt-container .g-gantt-chart .g-gantt-row-bars-container .g-gantt-bar')
|
cy.get('.gantt-container .g-gantt-chart .g-gantt-row-bars-container .g-gantt-bar')
|
||||||
.dblclick()
|
.dblclick()
|
|
@ -1,24 +1,25 @@
|
||||||
|
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||||
|
|
||||||
import {BucketFactory} from '../../factories/bucket'
|
import {BucketFactory} from '../../factories/bucket'
|
||||||
import {ListFactory} from '../../factories/list'
|
import {ProjectFactory} from '../../factories/project'
|
||||||
import {TaskFactory} from '../../factories/task'
|
import {TaskFactory} from '../../factories/task'
|
||||||
import {prepareLists} from './prepareLists'
|
import {prepareProjects} from './prepareProjects'
|
||||||
|
|
||||||
import '../../support/authenticateUser'
|
describe('Project View Kanban', () => {
|
||||||
|
createFakeUserAndLogin()
|
||||||
describe('List View Kanban', () => {
|
prepareProjects()
|
||||||
|
|
||||||
let buckets
|
let buckets
|
||||||
prepareLists()
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
buckets = BucketFactory.create(2)
|
buckets = BucketFactory.create(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Shows all buckets with their tasks', () => {
|
it('Shows all buckets with their tasks', () => {
|
||||||
const data = TaskFactory.create(10, {
|
const data = TaskFactory.create(10, {
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
bucket_id: 1,
|
bucket_id: 1,
|
||||||
})
|
})
|
||||||
cy.visit('/lists/1/kanban')
|
cy.visit('/projects/1/kanban')
|
||||||
|
|
||||||
cy.get('.kanban .bucket .title')
|
cy.get('.kanban .bucket .title')
|
||||||
.contains(buckets[0].title)
|
.contains(buckets[0].title)
|
||||||
|
@ -33,12 +34,12 @@ describe('List View Kanban', () => {
|
||||||
|
|
||||||
it('Can add a new task to a bucket', () => {
|
it('Can add a new task to a bucket', () => {
|
||||||
TaskFactory.create(2, {
|
TaskFactory.create(2, {
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
bucket_id: 1,
|
bucket_id: 1,
|
||||||
})
|
})
|
||||||
cy.visit('/lists/1/kanban')
|
cy.visit('/projects/1/kanban')
|
||||||
|
|
||||||
cy.getSettled('.kanban .bucket')
|
cy.get('.kanban .bucket')
|
||||||
.contains(buckets[0].title)
|
.contains(buckets[0].title)
|
||||||
.get('.bucket-footer .button')
|
.get('.bucket-footer .button')
|
||||||
.contains('Add another task')
|
.contains('Add another task')
|
||||||
|
@ -54,7 +55,7 @@ describe('List View Kanban', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Can create a new bucket', () => {
|
it('Can create a new bucket', () => {
|
||||||
cy.visit('/lists/1/kanban')
|
cy.visit('/projects/1/kanban')
|
||||||
|
|
||||||
cy.get('.kanban .bucket.new-bucket .button')
|
cy.get('.kanban .bucket.new-bucket .button')
|
||||||
.click()
|
.click()
|
||||||
|
@ -68,9 +69,9 @@ describe('List View Kanban', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Can set a bucket limit', () => {
|
it('Can set a bucket limit', () => {
|
||||||
cy.visit('/lists/1/kanban')
|
cy.visit('/projects/1/kanban')
|
||||||
|
|
||||||
cy.getSettled('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger')
|
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger')
|
||||||
.first()
|
.first()
|
||||||
.click()
|
.click()
|
||||||
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item')
|
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item')
|
||||||
|
@ -89,9 +90,9 @@ describe('List View Kanban', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Can rename a bucket', () => {
|
it('Can rename a bucket', () => {
|
||||||
cy.visit('/lists/1/kanban')
|
cy.visit('/projects/1/kanban')
|
||||||
|
|
||||||
cy.getSettled('.kanban .bucket .bucket-header .title')
|
cy.get('.kanban .bucket .bucket-header .title')
|
||||||
.first()
|
.first()
|
||||||
.type('{selectall}New Bucket Title{enter}')
|
.type('{selectall}New Bucket Title{enter}')
|
||||||
cy.get('.kanban .bucket .bucket-header .title')
|
cy.get('.kanban .bucket .bucket-header .title')
|
||||||
|
@ -100,9 +101,9 @@ describe('List View Kanban', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Can delete a bucket', () => {
|
it('Can delete a bucket', () => {
|
||||||
cy.visit('/lists/1/kanban')
|
cy.visit('/projects/1/kanban')
|
||||||
|
|
||||||
cy.getSettled('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger')
|
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger')
|
||||||
.first()
|
.first()
|
||||||
.click()
|
.click()
|
||||||
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item')
|
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item')
|
||||||
|
@ -124,12 +125,12 @@ describe('List View Kanban', () => {
|
||||||
|
|
||||||
it('Can drag tasks around', () => {
|
it('Can drag tasks around', () => {
|
||||||
const tasks = TaskFactory.create(2, {
|
const tasks = TaskFactory.create(2, {
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
bucket_id: 1,
|
bucket_id: 1,
|
||||||
})
|
})
|
||||||
cy.visit('/lists/1/kanban')
|
cy.visit('/projects/1/kanban')
|
||||||
|
|
||||||
cy.getSettled('.kanban .bucket .tasks .task')
|
cy.get('.kanban .bucket .tasks .task')
|
||||||
.contains(tasks[0].title)
|
.contains(tasks[0].title)
|
||||||
.first()
|
.first()
|
||||||
.drag('.kanban .bucket:nth-child(2) .tasks')
|
.drag('.kanban .bucket:nth-child(2) .tasks')
|
||||||
|
@ -143,12 +144,12 @@ describe('List View Kanban', () => {
|
||||||
it('Should navigate to the task when the task card is clicked', () => {
|
it('Should navigate to the task when the task card is clicked', () => {
|
||||||
const tasks = TaskFactory.create(5, {
|
const tasks = TaskFactory.create(5, {
|
||||||
id: '{increment}',
|
id: '{increment}',
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
bucket_id: 1,
|
bucket_id: 1,
|
||||||
})
|
})
|
||||||
cy.visit('/lists/1/kanban')
|
cy.visit('/projects/1/kanban')
|
||||||
|
|
||||||
cy.getSettled('.kanban .bucket .tasks .task')
|
cy.get('.kanban .bucket .tasks .task')
|
||||||
.contains(tasks[0].title)
|
.contains(tasks[0].title)
|
||||||
.should('be.visible')
|
.should('be.visible')
|
||||||
.click()
|
.click()
|
||||||
|
@ -157,20 +158,20 @@ describe('List View Kanban', () => {
|
||||||
.should('contain', `/tasks/${tasks[0].id}`, { timeout: 1000 })
|
.should('contain', `/tasks/${tasks[0].id}`, { timeout: 1000 })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should remove a task from the kanban board when moving it to another list', () => {
|
it('Should remove a task from the kanban board when moving it to another project', () => {
|
||||||
const lists = ListFactory.create(2)
|
const projects = ProjectFactory.create(2)
|
||||||
BucketFactory.create(2, {
|
BucketFactory.create(2, {
|
||||||
list_id: '{increment}',
|
project_id: '{increment}',
|
||||||
})
|
})
|
||||||
const tasks = TaskFactory.create(5, {
|
const tasks = TaskFactory.create(5, {
|
||||||
id: '{increment}',
|
id: '{increment}',
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
bucket_id: 1,
|
bucket_id: 1,
|
||||||
})
|
})
|
||||||
const task = tasks[0]
|
const task = tasks[0]
|
||||||
cy.visit('/lists/1/kanban')
|
cy.visit('/projects/1/kanban')
|
||||||
|
|
||||||
cy.getSettled('.kanban .bucket .tasks .task')
|
cy.get('.kanban .bucket .tasks .task')
|
||||||
.contains(task.title)
|
.contains(task.title)
|
||||||
.should('be.visible')
|
.should('be.visible')
|
||||||
.click()
|
.click()
|
||||||
|
@ -179,7 +180,7 @@ describe('List View Kanban', () => {
|
||||||
.contains('Move')
|
.contains('Move')
|
||||||
.click()
|
.click()
|
||||||
cy.get('.task-view .content.details .field .multiselect.control .input-wrapper input')
|
cy.get('.task-view .content.details .field .multiselect.control .input-wrapper input')
|
||||||
.type(`${lists[1].title}{enter}`)
|
.type(`${projects[1].title}{enter}`)
|
||||||
// The requests happen with a 200ms timeout. Because of that, the results are not yet there when cypress
|
// 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.
|
// 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')
|
cy.get('.task-view .content.details .field .multiselect.control .search-results')
|
||||||
|
@ -196,28 +197,28 @@ describe('List View Kanban', () => {
|
||||||
|
|
||||||
it('Shows a button to filter the kanban board', () => {
|
it('Shows a button to filter the kanban board', () => {
|
||||||
const data = TaskFactory.create(10, {
|
const data = TaskFactory.create(10, {
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
bucket_id: 1,
|
bucket_id: 1,
|
||||||
})
|
})
|
||||||
cy.visit('/lists/1/kanban')
|
cy.visit('/projects/1/kanban')
|
||||||
|
|
||||||
cy.get('.list-kanban .filter-container .base-button')
|
cy.get('.project-kanban .filter-container .base-button')
|
||||||
.should('exist')
|
.should('exist')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should remove a task from the board when deleting it', () => {
|
it('Should remove a task from the board when deleting it', () => {
|
||||||
const lists = ListFactory.create(1)
|
const projects = ProjectFactory.create(1)
|
||||||
const buckets = BucketFactory.create(2, {
|
const buckets = BucketFactory.create(2, {
|
||||||
list_id: lists[0].id,
|
project_id: projects[0].id,
|
||||||
})
|
})
|
||||||
const tasks = TaskFactory.create(5, {
|
const tasks = TaskFactory.create(5, {
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
bucket_id: buckets[0].id,
|
bucket_id: buckets[0].id,
|
||||||
})
|
})
|
||||||
const task = tasks[0]
|
const task = tasks[0]
|
||||||
cy.visit('/lists/1/kanban')
|
cy.visit('/projects/1/kanban')
|
||||||
|
|
||||||
cy.getSettled('.kanban .bucket .tasks .task')
|
cy.get('.kanban .bucket .tasks .task')
|
||||||
.contains(task.title)
|
.contains(task.title)
|
||||||
.should('be.visible')
|
.should('be.visible')
|
||||||
.click()
|
.click()
|
||||||
|
@ -234,7 +235,7 @@ describe('List View Kanban', () => {
|
||||||
cy.get('.global-notification')
|
cy.get('.global-notification')
|
||||||
.should('contain', 'Success')
|
.should('contain', 'Success')
|
||||||
|
|
||||||
cy.getSettled('.kanban .bucket .tasks')
|
cy.get('.kanban .bucket .tasks')
|
||||||
.should('not.contain', task.title)
|
.should('not.contain', task.title)
|
||||||
})
|
})
|
||||||
})
|
})
|
|
@ -1,31 +1,32 @@
|
||||||
import {UserListFactory} from '../../factories/users_list'
|
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||||
|
|
||||||
|
import {UserProjectFactory} from '../../factories/users_project'
|
||||||
import {TaskFactory} from '../../factories/task'
|
import {TaskFactory} from '../../factories/task'
|
||||||
import {UserFactory} from '../../factories/user'
|
import {UserFactory} from '../../factories/user'
|
||||||
import {ListFactory} from '../../factories/list'
|
import {ProjectFactory} from '../../factories/project'
|
||||||
import {prepareLists} from './prepareLists'
|
import {prepareProjects} from './prepareProjects'
|
||||||
|
|
||||||
import '../../support/authenticateUser'
|
describe('Project View Project', () => {
|
||||||
|
createFakeUserAndLogin()
|
||||||
|
prepareProjects()
|
||||||
|
|
||||||
describe('List View List', () => {
|
it('Should be an empty project', () => {
|
||||||
prepareLists()
|
cy.visit('/projects/1')
|
||||||
|
|
||||||
it('Should be an empty list', () => {
|
|
||||||
cy.visit('/lists/1')
|
|
||||||
cy.url()
|
cy.url()
|
||||||
.should('contain', '/lists/1/list')
|
.should('contain', '/projects/1/project')
|
||||||
cy.get('.list-title h1')
|
cy.get('.project-title h1')
|
||||||
.should('contain', 'First List')
|
.should('contain', 'First Project')
|
||||||
cy.get('.list-title .dropdown')
|
cy.get('.project-title .dropdown')
|
||||||
.should('exist')
|
.should('exist')
|
||||||
cy.get('p')
|
cy.get('p')
|
||||||
.contains('This list is currently empty.')
|
.contains('This project is currently empty.')
|
||||||
.should('exist')
|
.should('exist')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should create a new task', () => {
|
it('Should create a new task', () => {
|
||||||
const newTaskTitle = 'New task'
|
const newTaskTitle = 'New task'
|
||||||
|
|
||||||
cy.visit('/lists/1')
|
cy.visit('/projects/1')
|
||||||
cy.get('.task-add textarea')
|
cy.get('.task-add textarea')
|
||||||
.type(newTaskTitle+'{enter}')
|
.type(newTaskTitle+'{enter}')
|
||||||
cy.get('.tasks')
|
cy.get('.tasks')
|
||||||
|
@ -35,9 +36,9 @@ describe('List View List', () => {
|
||||||
it('Should navigate to the task when the title is clicked', () => {
|
it('Should navigate to the task when the title is clicked', () => {
|
||||||
const tasks = TaskFactory.create(5, {
|
const tasks = TaskFactory.create(5, {
|
||||||
id: '{increment}',
|
id: '{increment}',
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
})
|
})
|
||||||
cy.visit('/lists/1/list')
|
cy.visit('/projects/1/project')
|
||||||
|
|
||||||
cy.get('.tasks .task .tasktext')
|
cy.get('.tasks .task .tasktext')
|
||||||
.contains(tasks[0].title)
|
.contains(tasks[0].title)
|
||||||
|
@ -48,37 +49,37 @@ describe('List View List', () => {
|
||||||
.should('contain', `/tasks/${tasks[0].id}`)
|
.should('contain', `/tasks/${tasks[0].id}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should not see any elements for a list which is shared read only', () => {
|
it('Should not see any elements for a project which is shared read only', () => {
|
||||||
UserFactory.create(2)
|
UserFactory.create(2)
|
||||||
UserListFactory.create(1, {
|
UserProjectFactory.create(1, {
|
||||||
list_id: 2,
|
project_id: 2,
|
||||||
user_id: 1,
|
user_id: 1,
|
||||||
right: 0,
|
right: 0,
|
||||||
})
|
})
|
||||||
const lists = ListFactory.create(2, {
|
const projects = ProjectFactory.create(2, {
|
||||||
owner_id: '{increment}',
|
owner_id: '{increment}',
|
||||||
namespace_id: '{increment}',
|
namespace_id: '{increment}',
|
||||||
})
|
})
|
||||||
cy.visit(`/lists/${lists[1].id}/`)
|
cy.visit(`/projects/${projects[1].id}/`)
|
||||||
|
|
||||||
cy.get('.list-title .icon')
|
cy.get('.project-title .icon')
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
cy.get('input.input[placeholder="Add a new task..."')
|
cy.get('input.input[placeholder="Add a new task..."')
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should only show the color of a list in the navigation and not in the list view', () => {
|
it('Should only show the color of a project in the navigation and not in the project view', () => {
|
||||||
const lists = ListFactory.create(1, {
|
const projects = ProjectFactory.create(1, {
|
||||||
hex_color: '00db60',
|
hex_color: '00db60',
|
||||||
})
|
})
|
||||||
TaskFactory.create(10, {
|
TaskFactory.create(10, {
|
||||||
list_id: lists[0].id,
|
project_id: projects[0].id,
|
||||||
})
|
})
|
||||||
cy.visit(`/lists/${lists[0].id}/`)
|
cy.visit(`/projects/${projects[0].id}/`)
|
||||||
|
|
||||||
cy.get('.menu-list li .list-menu-link .color-bubble')
|
cy.get('.menu-project li .project-menu-link .color-bubble')
|
||||||
.should('have.css', 'background-color', 'rgb(0, 219, 96)')
|
.should('have.css', 'background-color', 'rgb(0, 219, 96)')
|
||||||
cy.get('.tasks-container .tasks .color-bubble')
|
cy.get('.tasks .color-bubble')
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -86,13 +87,13 @@ describe('List View List', () => {
|
||||||
const tasks = TaskFactory.create(100, {
|
const tasks = TaskFactory.create(100, {
|
||||||
id: '{increment}',
|
id: '{increment}',
|
||||||
title: i => `task${i}`,
|
title: i => `task${i}`,
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
})
|
})
|
||||||
cy.visit('/lists/1/list')
|
cy.visit('/projects/1/project')
|
||||||
|
|
||||||
cy.get('.tasks-container .tasks')
|
cy.get('.tasks')
|
||||||
.should('contain', tasks[1].title)
|
.should('contain', tasks[1].title)
|
||||||
cy.get('.tasks-container .tasks')
|
cy.get('.tasks')
|
||||||
.should('not.contain', tasks[99].title)
|
.should('not.contain', tasks[99].title)
|
||||||
|
|
||||||
cy.get('.card-content .pagination .pagination-link')
|
cy.get('.card-content .pagination .pagination-link')
|
||||||
|
@ -101,9 +102,9 @@ describe('List View List', () => {
|
||||||
|
|
||||||
cy.url()
|
cy.url()
|
||||||
.should('contain', '?page=2')
|
.should('contain', '?page=2')
|
||||||
cy.get('.tasks-container .tasks')
|
cy.get('.tasks')
|
||||||
.should('contain', tasks[99].title)
|
.should('contain', tasks[99].title)
|
||||||
cy.get('.tasks-container .tasks')
|
cy.get('.tasks')
|
||||||
.should('not.contain', tasks[1].title)
|
.should('not.contain', tasks[1].title)
|
||||||
})
|
})
|
||||||
})
|
})
|
|
@ -1,36 +1,38 @@
|
||||||
|
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||||
|
|
||||||
import {TaskFactory} from '../../factories/task'
|
import {TaskFactory} from '../../factories/task'
|
||||||
|
|
||||||
import '../../support/authenticateUser'
|
describe('Project View Table', () => {
|
||||||
|
createFakeUserAndLogin()
|
||||||
|
|
||||||
describe('List View Table', () => {
|
|
||||||
it('Should show a table with tasks', () => {
|
it('Should show a table with tasks', () => {
|
||||||
const tasks = TaskFactory.create(1)
|
const tasks = TaskFactory.create(1)
|
||||||
cy.visit('/lists/1/table')
|
cy.visit('/projects/1/table')
|
||||||
|
|
||||||
cy.get('.list-table table.table')
|
cy.get('.project-table table.table')
|
||||||
.should('exist')
|
.should('exist')
|
||||||
cy.get('.list-table table.table')
|
cy.get('.project-table table.table')
|
||||||
.should('contain', tasks[0].title)
|
.should('contain', tasks[0].title)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should have working column switches', () => {
|
it('Should have working column switches', () => {
|
||||||
TaskFactory.create(1)
|
TaskFactory.create(1)
|
||||||
cy.visit('/lists/1/table')
|
cy.visit('/projects/1/table')
|
||||||
|
|
||||||
cy.get('.list-table .filter-container .items .button')
|
cy.get('.project-table .filter-container .items .button')
|
||||||
.contains('Columns')
|
.contains('Columns')
|
||||||
.click()
|
.click()
|
||||||
cy.get('.list-table .filter-container .card.columns-filter .card-content .fancycheckbox .check')
|
cy.get('.project-table .filter-container .card.columns-filter .card-content .fancycheckbox .check')
|
||||||
.contains('Priority')
|
.contains('Priority')
|
||||||
.click()
|
.click()
|
||||||
cy.get('.list-table .filter-container .card.columns-filter .card-content .fancycheckbox .check')
|
cy.get('.project-table .filter-container .card.columns-filter .card-content .fancycheckbox .check')
|
||||||
.contains('Done')
|
.contains('Done')
|
||||||
.click()
|
.click()
|
||||||
|
|
||||||
cy.get('.list-table table.table th')
|
cy.get('.project-table table.table th')
|
||||||
.contains('Priority')
|
.contains('Priority')
|
||||||
.should('exist')
|
.should('exist')
|
||||||
cy.get('.list-table table.table th')
|
cy.get('.project-table table.table th')
|
||||||
.contains('Done')
|
.contains('Done')
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
})
|
})
|
||||||
|
@ -38,11 +40,11 @@ describe('List View Table', () => {
|
||||||
it('Should navigate to the task when the title is clicked', () => {
|
it('Should navigate to the task when the title is clicked', () => {
|
||||||
const tasks = TaskFactory.create(5, {
|
const tasks = TaskFactory.create(5, {
|
||||||
id: '{increment}',
|
id: '{increment}',
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
})
|
})
|
||||||
cy.visit('/lists/1/table')
|
cy.visit('/projects/1/table')
|
||||||
|
|
||||||
cy.get('.list-table table.table')
|
cy.get('.project-table table.table')
|
||||||
.contains(tasks[0].title)
|
.contains(tasks[0].title)
|
||||||
.click()
|
.click()
|
||||||
|
|
122
cypress/e2e/project/project.spec.ts
Normal file
122
cypress/e2e/project/project.spec.ts
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||||
|
|
||||||
|
import {TaskFactory} from '../../factories/task'
|
||||||
|
import {prepareProjects} from './prepareProjects'
|
||||||
|
|
||||||
|
describe('Projects', () => {
|
||||||
|
createFakeUserAndLogin()
|
||||||
|
|
||||||
|
let projects
|
||||||
|
prepareProjects((newProjects) => (projects = newProjects))
|
||||||
|
|
||||||
|
it('Should create a new project', () => {
|
||||||
|
cy.visit('/')
|
||||||
|
cy.get('.namespace-title .dropdown-trigger')
|
||||||
|
.click()
|
||||||
|
cy.get('.namespace-title .dropdown .dropdown-item')
|
||||||
|
.contains('New project')
|
||||||
|
.click()
|
||||||
|
cy.url()
|
||||||
|
.should('contain', '/projects/new/1')
|
||||||
|
cy.get('.card-header-title')
|
||||||
|
.contains('New project')
|
||||||
|
cy.get('input.input')
|
||||||
|
.type('New Project')
|
||||||
|
cy.get('.button')
|
||||||
|
.contains('Create')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.get('.global-notification', { timeout: 1000 }) // Waiting until the request to create the new project is done
|
||||||
|
.should('contain', 'Success')
|
||||||
|
cy.url()
|
||||||
|
.should('contain', '/projects/')
|
||||||
|
cy.get('.project-title h1')
|
||||||
|
.should('contain', 'New Project')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should redirect to a specific project view after visited', () => {
|
||||||
|
cy.visit('/projects/1/kanban')
|
||||||
|
cy.url()
|
||||||
|
.should('contain', '/projects/1/kanban')
|
||||||
|
cy.visit('/projects/1')
|
||||||
|
cy.url()
|
||||||
|
.should('contain', '/projects/1/kanban')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should rename the project in all places', () => {
|
||||||
|
TaskFactory.create(5, {
|
||||||
|
id: '{increment}',
|
||||||
|
project_id: 1,
|
||||||
|
})
|
||||||
|
const newProjectName = 'New project name'
|
||||||
|
|
||||||
|
cy.visit('/projects/1')
|
||||||
|
cy.get('.project-title h1')
|
||||||
|
.should('contain', 'First Project')
|
||||||
|
|
||||||
|
cy.get('.namespace-container .menu.namespaces-projects .menu-project li:first-child .dropdown .menu-list-dropdown-trigger')
|
||||||
|
.click()
|
||||||
|
cy.get('.namespace-container .menu.namespaces-projects .menu-project li:first-child .dropdown .dropdown-content')
|
||||||
|
.contains('Edit')
|
||||||
|
.click()
|
||||||
|
cy.get('#title')
|
||||||
|
.type(`{selectall}${newProjectName}`)
|
||||||
|
cy.get('footer.card-footer .button')
|
||||||
|
.contains('Save')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.get('.global-notification')
|
||||||
|
.should('contain', 'Success')
|
||||||
|
cy.get('.project-title h1')
|
||||||
|
.should('contain', newProjectName)
|
||||||
|
.should('not.contain', projects[0].title)
|
||||||
|
cy.get('.namespace-container .menu.namespaces-projects .menu-project li:first-child')
|
||||||
|
.should('contain', newProjectName)
|
||||||
|
.should('not.contain', projects[0].title)
|
||||||
|
cy.visit('/')
|
||||||
|
cy.get('.card-content')
|
||||||
|
.should('contain', newProjectName)
|
||||||
|
.should('not.contain', projects[0].title)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should remove a project', () => {
|
||||||
|
cy.visit(`/projects/${projects[0].id}`)
|
||||||
|
|
||||||
|
cy.get('.namespace-container .menu.namespaces-projects .menu-project li:first-child .dropdown .menu-list-dropdown-trigger')
|
||||||
|
.click()
|
||||||
|
cy.get('.namespace-container .menu.namespaces-projects .menu-project li:first-child .dropdown .dropdown-content')
|
||||||
|
.contains('Delete')
|
||||||
|
.click()
|
||||||
|
cy.url()
|
||||||
|
.should('contain', '/settings/delete')
|
||||||
|
cy.get('[data-cy="modalPrimary"]')
|
||||||
|
.contains('Do it')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.get('.global-notification')
|
||||||
|
.should('contain', 'Success')
|
||||||
|
cy.get('.namespace-container .menu.namespaces-projects .menu-project')
|
||||||
|
.should('not.contain', projects[0].title)
|
||||||
|
cy.location('pathname')
|
||||||
|
.should('equal', '/')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should archive a project', () => {
|
||||||
|
cy.visit(`/projects/${projects[0].id}`)
|
||||||
|
|
||||||
|
cy.get('.project-title .dropdown')
|
||||||
|
.click()
|
||||||
|
cy.get('.project-title .dropdown .dropdown-menu .dropdown-item')
|
||||||
|
.contains('Archive')
|
||||||
|
.click()
|
||||||
|
cy.get('.modal-content')
|
||||||
|
.should('contain.text', 'Archive this project')
|
||||||
|
cy.get('.modal-content [data-cy=modalPrimary]')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.get('.namespace-container .menu.namespaces-projects .menu-project')
|
||||||
|
.should('not.contain', projects[0].title)
|
||||||
|
cy.get('main.app-content')
|
||||||
|
.should('contain.text', 'This project is archived. It is not possible to create new or edit tasks for it.')
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,22 +1,22 @@
|
||||||
import {LinkShareFactory} from '../../factories/link_sharing'
|
import {LinkShareFactory} from '../../factories/link_sharing'
|
||||||
import {ListFactory} from '../../factories/list'
|
import {ProjectFactory} from '../../factories/project'
|
||||||
import {TaskFactory} from '../../factories/task'
|
import {TaskFactory} from '../../factories/task'
|
||||||
|
|
||||||
describe('Link shares', () => {
|
describe('Link shares', () => {
|
||||||
it('Can view a link share', () => {
|
it('Can view a link share', () => {
|
||||||
const lists = ListFactory.create(1)
|
const projects = ProjectFactory.create(1)
|
||||||
const tasks = TaskFactory.create(10, {
|
const tasks = TaskFactory.create(10, {
|
||||||
list_id: lists[0].id
|
project_id: projects[0].id
|
||||||
})
|
})
|
||||||
const linkShares = LinkShareFactory.create(1, {
|
const linkShares = LinkShareFactory.create(1, {
|
||||||
list_id: lists[0].id,
|
project_id: projects[0].id,
|
||||||
right: 0,
|
right: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.visit(`/share/${linkShares[0].hash}/auth`)
|
cy.visit(`/share/${linkShares[0].hash}/auth`)
|
||||||
|
|
||||||
cy.get('h1.title')
|
cy.get('h1.title')
|
||||||
.should('contain', lists[0].title)
|
.should('contain', projects[0].title)
|
||||||
cy.get('input.input[placeholder="Add a new task..."')
|
cy.get('input.input[placeholder="Add a new task..."')
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
cy.get('.tasks')
|
cy.get('.tasks')
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
|
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||||
|
|
||||||
import {TeamFactory} from '../../factories/team'
|
import {TeamFactory} from '../../factories/team'
|
||||||
import {TeamMemberFactory} from '../../factories/team_member'
|
import {TeamMemberFactory} from '../../factories/team_member'
|
||||||
import {UserFactory} from '../../factories/user'
|
import {UserFactory} from '../../factories/user'
|
||||||
import '../../support/authenticateUser'
|
|
||||||
|
|
||||||
describe('Team', () => {
|
describe('Team', () => {
|
||||||
|
createFakeUserAndLogin()
|
||||||
|
|
||||||
it('Creates a new team', () => {
|
it('Creates a new team', () => {
|
||||||
TeamFactory.truncate()
|
TeamFactory.truncate()
|
||||||
cy.visit('/teams')
|
cy.visit('/teams')
|
||||||
|
|
|
@ -1,45 +1,45 @@
|
||||||
import {ListFactory} from '../../factories/list'
|
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||||
|
|
||||||
|
import {ProjectFactory} from '../../factories/project'
|
||||||
import {seed} from '../../support/seed'
|
import {seed} from '../../support/seed'
|
||||||
import {TaskFactory} from '../../factories/task'
|
import {TaskFactory} from '../../factories/task'
|
||||||
import {formatISO} from 'date-fns'
|
|
||||||
import {UserFactory} from '../../factories/user'
|
|
||||||
import {NamespaceFactory} from '../../factories/namespace'
|
import {NamespaceFactory} from '../../factories/namespace'
|
||||||
import {BucketFactory} from '../../factories/bucket'
|
import {BucketFactory} from '../../factories/bucket'
|
||||||
import {updateUserSettings} from '../../support/updateUserSettings'
|
import {updateUserSettings} from '../../support/updateUserSettings'
|
||||||
|
|
||||||
import '../../support/authenticateUser'
|
function seedTasks(numberOfTasks = 50, startDueDate = new Date()) {
|
||||||
|
|
||||||
function seedTasks(numberOfTasks = 100, startDueDate = new Date()) {
|
|
||||||
UserFactory.create(1)
|
|
||||||
NamespaceFactory.create(1)
|
NamespaceFactory.create(1)
|
||||||
const list = ListFactory.create()[0]
|
const project = ProjectFactory.create()[0]
|
||||||
BucketFactory.create(1, {
|
BucketFactory.create(1, {
|
||||||
list_id: list.id,
|
project_id: project.id,
|
||||||
})
|
})
|
||||||
const tasks = []
|
const tasks = []
|
||||||
let dueDate = startDueDate
|
let dueDate = startDueDate
|
||||||
for (let i = 0; i < numberOfTasks; i++) {
|
for (let i = 0; i < numberOfTasks; i++) {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
dueDate = (new Date(dueDate.valueOf())).setDate((new Date(dueDate.valueOf())).getDate() + 2)
|
dueDate = new Date(new Date(dueDate).setDate(dueDate.getDate() + 2))
|
||||||
tasks.push({
|
tasks.push({
|
||||||
id: i + 1,
|
id: i + 1,
|
||||||
list_id: list.id,
|
project_id: project.id,
|
||||||
done: false,
|
done: false,
|
||||||
created_by_id: 1,
|
created_by_id: 1,
|
||||||
title: 'Test Task ' + i,
|
title: 'Test Task ' + i,
|
||||||
index: i + 1,
|
index: i + 1,
|
||||||
due_date: formatISO(dueDate),
|
due_date: dueDate.toISOString(),
|
||||||
created: formatISO(now),
|
created: now.toISOString(),
|
||||||
updated: formatISO(now),
|
updated: now.toISOString(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
seed(TaskFactory.table, tasks)
|
seed(TaskFactory.table, tasks)
|
||||||
return {tasks, list}
|
return {tasks, project}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('Home Page Task Overview', () => {
|
describe('Home Page Task Overview', () => {
|
||||||
|
createFakeUserAndLogin()
|
||||||
|
|
||||||
it('Should show tasks with a near due date first on the home page overview', () => {
|
it('Should show tasks with a near due date first on the home page overview', () => {
|
||||||
const {tasks} = seedTasks()
|
const taskCount = 50
|
||||||
|
const {tasks} = seedTasks(taskCount)
|
||||||
|
|
||||||
cy.visit('/')
|
cy.visit('/')
|
||||||
cy.get('[data-cy="showTasks"] .card .task')
|
cy.get('[data-cy="showTasks"] .card .task')
|
||||||
|
@ -49,8 +49,10 @@ describe('Home Page Task Overview', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should show overdue tasks first, then show other tasks', () => {
|
it('Should show overdue tasks first, then show other tasks', () => {
|
||||||
const oldDate = (new Date()).setDate((new Date()).getDate() - 14)
|
const now = new Date()
|
||||||
const {tasks} = seedTasks(100, oldDate)
|
const oldDate = new Date(new Date(now).setDate(now.getDate() - 14))
|
||||||
|
const taskCount = 50
|
||||||
|
const {tasks} = seedTasks(taskCount, oldDate)
|
||||||
|
|
||||||
cy.visit('/')
|
cy.visit('/')
|
||||||
cy.get('[data-cy="showTasks"] .card .task')
|
cy.get('[data-cy="showTasks"] .card .task')
|
||||||
|
@ -68,10 +70,10 @@ describe('Home Page Task Overview', () => {
|
||||||
TaskFactory.create(1, {
|
TaskFactory.create(1, {
|
||||||
id: 999,
|
id: 999,
|
||||||
title: newTaskTitle,
|
title: newTaskTitle,
|
||||||
due_date: formatISO(new Date()),
|
due_date: new Date().toISOString(),
|
||||||
}, false)
|
}, false)
|
||||||
|
|
||||||
cy.visit(`/lists/${tasks[0].list_id}/list`)
|
cy.visit(`/projects/${tasks[0].project_id}/project`)
|
||||||
cy.get('.tasks .task')
|
cy.get('.tasks .task')
|
||||||
.first()
|
.first()
|
||||||
.should('contain.text', newTaskTitle)
|
.should('contain.text', newTaskTitle)
|
||||||
|
@ -83,12 +85,12 @@ describe('Home Page Task Overview', () => {
|
||||||
|
|
||||||
it('Should not show a new task without a date at the bottom when there are > 50 tasks', () => {
|
it('Should not show a new task without a date at the bottom when there are > 50 tasks', () => {
|
||||||
// We're not using the api here to create the task in order to verify the flow
|
// We're not using the api here to create the task in order to verify the flow
|
||||||
const {tasks} = seedTasks()
|
const {tasks} = seedTasks(100)
|
||||||
const newTaskTitle = 'New Task'
|
const newTaskTitle = 'New Task'
|
||||||
|
|
||||||
cy.visit('/')
|
cy.visit('/')
|
||||||
|
|
||||||
cy.visit(`/lists/${tasks[0].list_id}/list`)
|
cy.visit(`/projects/${tasks[0].project_id}/project`)
|
||||||
cy.get('.task-add textarea')
|
cy.get('.task-add textarea')
|
||||||
.type(newTaskTitle+'{enter}')
|
.type(newTaskTitle+'{enter}')
|
||||||
cy.visit('/')
|
cy.visit('/')
|
||||||
|
@ -111,10 +113,10 @@ describe('Home Page Task Overview', () => {
|
||||||
.should('contain.text', newTaskTitle)
|
.should('contain.text', newTaskTitle)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should show a task without a due date added via default list at the bottom', () => {
|
it('Should show a task without a due date added via default project at the bottom', () => {
|
||||||
const {list} = seedTasks(40)
|
const {project} = seedTasks(40)
|
||||||
updateUserSettings({
|
updateUserSettings({
|
||||||
default_list_id: list.id,
|
default_project_id: project.id,
|
||||||
overdue_tasks_reminders_time: '9:00',
|
overdue_tasks_reminders_time: '9:00',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -129,23 +131,23 @@ describe('Home Page Task Overview', () => {
|
||||||
.should('contain.text', newTaskTitle)
|
.should('contain.text', newTaskTitle)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should show the cta buttons for new list when there are no tasks', () => {
|
it('Should show the cta buttons for new project when there are no tasks', () => {
|
||||||
TaskFactory.truncate()
|
TaskFactory.truncate()
|
||||||
|
|
||||||
cy.visit('/')
|
cy.visit('/')
|
||||||
|
|
||||||
cy.get('.home.app-content .content')
|
cy.get('.home.app-content .content')
|
||||||
.should('contain.text', 'You can create a new list for your new tasks:')
|
.should('contain.text', 'You can create a new project for your new tasks:')
|
||||||
.should('contain.text', 'Or import your lists and tasks from other services into Vikunja:')
|
.should('contain.text', 'Or import your projects and tasks from other services into Vikunja:')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should not show the cta buttons for new list when there are tasks', () => {
|
it('Should not show the cta buttons for new project when there are tasks', () => {
|
||||||
seedTasks()
|
seedTasks()
|
||||||
|
|
||||||
cy.visit('/')
|
cy.visit('/')
|
||||||
|
|
||||||
cy.get('.home.app-content .content')
|
cy.get('.home.app-content .content')
|
||||||
.should('not.contain.text', 'You can create a new list for your new tasks:')
|
.should('not.contain.text', 'You can create a new project for your new tasks:')
|
||||||
.should('not.contain.text', 'Or import your lists and tasks from other services into Vikunja:')
|
.should('not.contain.text', 'Or import your projects and tasks from other services into Vikunja:')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,17 +1,16 @@
|
||||||
import {formatISO} from 'date-fns'
|
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||||
|
|
||||||
import {TaskFactory} from '../../factories/task'
|
import {TaskFactory} from '../../factories/task'
|
||||||
import {ListFactory} from '../../factories/list'
|
import {ProjectFactory} from '../../factories/project'
|
||||||
import {TaskCommentFactory} from '../../factories/task_comment'
|
import {TaskCommentFactory} from '../../factories/task_comment'
|
||||||
import {UserFactory} from '../../factories/user'
|
import {UserFactory} from '../../factories/user'
|
||||||
import {NamespaceFactory} from '../../factories/namespace'
|
import {NamespaceFactory} from '../../factories/namespace'
|
||||||
import {UserListFactory} from '../../factories/users_list'
|
import {UserProjectFactory} from '../../factories/users_project'
|
||||||
import {TaskAssigneeFactory} from '../../factories/task_assignee'
|
import {TaskAssigneeFactory} from '../../factories/task_assignee'
|
||||||
import {LabelFactory} from '../../factories/labels'
|
import {LabelFactory} from '../../factories/labels'
|
||||||
import {LabelTaskFactory} from '../../factories/label_task'
|
import {LabelTaskFactory} from '../../factories/label_task'
|
||||||
import {BucketFactory} from '../../factories/bucket'
|
import {BucketFactory} from '../../factories/bucket'
|
||||||
|
|
||||||
import '../../support/authenticateUser'
|
|
||||||
import {TaskAttachmentFactory} from '../../factories/task_attachments'
|
import {TaskAttachmentFactory} from '../../factories/task_attachments'
|
||||||
|
|
||||||
function addLabelToTaskAndVerify(labelTitle: string) {
|
function addLabelToTaskAndVerify(labelTitle: string) {
|
||||||
|
@ -46,23 +45,25 @@ function uploadAttachmentAndVerify(taskId: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('Task', () => {
|
describe('Task', () => {
|
||||||
|
createFakeUserAndLogin()
|
||||||
|
|
||||||
let namespaces
|
let namespaces
|
||||||
let lists
|
let projects
|
||||||
let buckets
|
let buckets
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
UserFactory.create(1)
|
// UserFactory.create(1)
|
||||||
namespaces = NamespaceFactory.create(1)
|
namespaces = NamespaceFactory.create(1)
|
||||||
lists = ListFactory.create(1)
|
projects = ProjectFactory.create(1)
|
||||||
buckets = BucketFactory.create(1, {
|
buckets = BucketFactory.create(1, {
|
||||||
list_id: lists[0].id,
|
project_id: projects[0].id,
|
||||||
})
|
})
|
||||||
TaskFactory.truncate()
|
TaskFactory.truncate()
|
||||||
UserListFactory.truncate()
|
UserProjectFactory.truncate()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should be created new', () => {
|
it('Should be created new', () => {
|
||||||
cy.visit('/lists/1/list')
|
cy.visit('/projects/1/project')
|
||||||
cy.get('.input[placeholder="Add a new task…"')
|
cy.get('.input[placeholder="Add a new task…"')
|
||||||
.type('New Task')
|
.type('New Task')
|
||||||
cy.get('.button')
|
cy.get('.button')
|
||||||
|
@ -73,11 +74,11 @@ describe('Task', () => {
|
||||||
.should('contain', 'New Task')
|
.should('contain', 'New Task')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Inserts new tasks at the top of the list', () => {
|
it('Inserts new tasks at the top of the project', () => {
|
||||||
TaskFactory.create(1)
|
TaskFactory.create(1)
|
||||||
|
|
||||||
cy.visit('/lists/1/list')
|
cy.visit('/projects/1/project')
|
||||||
cy.get('.list-is-empty-notice')
|
cy.get('.project-is-empty-notice')
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
cy.get('.input[placeholder="Add a new task…"')
|
cy.get('.input[placeholder="Add a new task…"')
|
||||||
.type('New Task')
|
.type('New Task')
|
||||||
|
@ -94,7 +95,7 @@ describe('Task', () => {
|
||||||
it('Marks a task as done', () => {
|
it('Marks a task as done', () => {
|
||||||
TaskFactory.create(1)
|
TaskFactory.create(1)
|
||||||
|
|
||||||
cy.visit('/lists/1/list')
|
cy.visit('/projects/1/project')
|
||||||
cy.get('.tasks .task .fancycheckbox label.check')
|
cy.get('.tasks .task .fancycheckbox label.check')
|
||||||
.first()
|
.first()
|
||||||
.click()
|
.click()
|
||||||
|
@ -105,11 +106,11 @@ describe('Task', () => {
|
||||||
it('Can add a task to favorites', () => {
|
it('Can add a task to favorites', () => {
|
||||||
TaskFactory.create(1)
|
TaskFactory.create(1)
|
||||||
|
|
||||||
cy.visit('/lists/1/list')
|
cy.visit('/projects/1/project')
|
||||||
cy.get('.tasks .task .favorite')
|
cy.get('.tasks .task .favorite')
|
||||||
.first()
|
.first()
|
||||||
.click()
|
.click()
|
||||||
cy.get('.menu.namespaces-lists')
|
cy.get('.menu.namespaces-projects')
|
||||||
.should('contain', 'Favorites')
|
.should('contain', 'Favorites')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -133,7 +134,7 @@ describe('Task', () => {
|
||||||
.should('contain', '#1')
|
.should('contain', '#1')
|
||||||
cy.get('.task-view h6.subtitle')
|
cy.get('.task-view h6.subtitle')
|
||||||
.should('contain', namespaces[0].title)
|
.should('contain', namespaces[0].title)
|
||||||
.should('contain', lists[0].title)
|
.should('contain', projects[0].title)
|
||||||
cy.get('.task-view .details.content.description')
|
cy.get('.task-view .details.content.description')
|
||||||
.should('contain', tasks[0].description)
|
.should('contain', tasks[0].description)
|
||||||
cy.get('.task-view .action-buttons p.created')
|
cy.get('.task-view .action-buttons p.created')
|
||||||
|
@ -145,7 +146,7 @@ describe('Task', () => {
|
||||||
id: 1,
|
id: 1,
|
||||||
index: 1,
|
index: 1,
|
||||||
done: true,
|
done: true,
|
||||||
done_at: formatISO(new Date())
|
done_at: new Date().toISOString()
|
||||||
})
|
})
|
||||||
cy.visit(`/tasks/${tasks[0].id}`)
|
cy.visit(`/tasks/${tasks[0].id}`)
|
||||||
|
|
||||||
|
@ -178,21 +179,21 @@ describe('Task', () => {
|
||||||
.should('contain', 'Mark as undone')
|
.should('contain', 'Mark as undone')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Shows a task identifier since the list has one', () => {
|
it('Shows a task identifier since the project has one', () => {
|
||||||
const lists = ListFactory.create(1, {
|
const projects = ProjectFactory.create(1, {
|
||||||
id: 1,
|
id: 1,
|
||||||
identifier: 'TEST',
|
identifier: 'TEST',
|
||||||
})
|
})
|
||||||
const tasks = TaskFactory.create(1, {
|
const tasks = TaskFactory.create(1, {
|
||||||
id: 1,
|
id: 1,
|
||||||
list_id: lists[0].id,
|
project_id: projects[0].id,
|
||||||
index: 1,
|
index: 1,
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.visit(`/tasks/${tasks[0].id}`)
|
cy.visit(`/tasks/${tasks[0].id}`)
|
||||||
|
|
||||||
cy.get('.task-view h1.title.task-id')
|
cy.get('.task-view h1.title.task-id')
|
||||||
.should('contain', `${lists[0].identifier}-${tasks[0].index}`)
|
.should('contain', `${projects[0].identifier}-${tasks[0].index}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Can edit the description', () => {
|
it('Can edit the description', () => {
|
||||||
|
@ -235,14 +236,14 @@ describe('Task', () => {
|
||||||
.should('contain', 'Success')
|
.should('contain', 'Success')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Can move a task to another list', () => {
|
it('Can move a task to another project', () => {
|
||||||
const lists = ListFactory.create(2)
|
const projects = ProjectFactory.create(2)
|
||||||
BucketFactory.create(2, {
|
BucketFactory.create(2, {
|
||||||
list_id: '{increment}'
|
project_id: '{increment}'
|
||||||
})
|
})
|
||||||
const tasks = TaskFactory.create(1, {
|
const tasks = TaskFactory.create(1, {
|
||||||
id: 1,
|
id: 1,
|
||||||
list_id: lists[0].id,
|
project_id: projects[0].id,
|
||||||
})
|
})
|
||||||
cy.visit(`/tasks/${tasks[0].id}`)
|
cy.visit(`/tasks/${tasks[0].id}`)
|
||||||
|
|
||||||
|
@ -250,7 +251,7 @@ describe('Task', () => {
|
||||||
.contains('Move')
|
.contains('Move')
|
||||||
.click()
|
.click()
|
||||||
cy.get('.task-view .content.details .field .multiselect.control .input-wrapper input')
|
cy.get('.task-view .content.details .field .multiselect.control .input-wrapper input')
|
||||||
.type(`${lists[1].title}{enter}`)
|
.type(`${projects[1].title}{enter}`)
|
||||||
// The requests happen with a 200ms timeout. Because of that, the results are not yet there when cypress
|
// 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.
|
// 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')
|
cy.get('.task-view .content.details .field .multiselect.control .search-results')
|
||||||
|
@ -260,7 +261,7 @@ describe('Task', () => {
|
||||||
|
|
||||||
cy.get('.task-view h6.subtitle')
|
cy.get('.task-view h6.subtitle')
|
||||||
.should('contain', namespaces[0].title)
|
.should('contain', namespaces[0].title)
|
||||||
.should('contain', lists[1].title)
|
.should('contain', projects[1].title)
|
||||||
cy.get('.global-notification')
|
cy.get('.global-notification')
|
||||||
.should('contain', 'Success')
|
.should('contain', 'Success')
|
||||||
})
|
})
|
||||||
|
@ -268,7 +269,7 @@ describe('Task', () => {
|
||||||
it('Can delete a task', () => {
|
it('Can delete a task', () => {
|
||||||
const tasks = TaskFactory.create(1, {
|
const tasks = TaskFactory.create(1, {
|
||||||
id: 1,
|
id: 1,
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
})
|
})
|
||||||
cy.visit(`/tasks/${tasks[0].id}`)
|
cy.visit(`/tasks/${tasks[0].id}`)
|
||||||
|
|
||||||
|
@ -285,17 +286,17 @@ describe('Task', () => {
|
||||||
cy.get('.global-notification')
|
cy.get('.global-notification')
|
||||||
.should('contain', 'Success')
|
.should('contain', 'Success')
|
||||||
cy.url()
|
cy.url()
|
||||||
.should('contain', `/lists/${tasks[0].list_id}/`)
|
.should('contain', `/projects/${tasks[0].project_id}/`)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Can add an assignee to a task', () => {
|
it('Can add an assignee to a task', () => {
|
||||||
const users = UserFactory.create(5)
|
const users = UserFactory.create(5)
|
||||||
const tasks = TaskFactory.create(1, {
|
const tasks = TaskFactory.create(1, {
|
||||||
id: 1,
|
id: 1,
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
})
|
})
|
||||||
UserListFactory.create(5, {
|
UserProjectFactory.create(5, {
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
user_id: '{increment}',
|
user_id: '{increment}',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -320,10 +321,10 @@ describe('Task', () => {
|
||||||
const users = UserFactory.create(2)
|
const users = UserFactory.create(2)
|
||||||
const tasks = TaskFactory.create(1, {
|
const tasks = TaskFactory.create(1, {
|
||||||
id: 1,
|
id: 1,
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
})
|
})
|
||||||
UserListFactory.create(5, {
|
UserProjectFactory.create(5, {
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
user_id: '{increment}',
|
user_id: '{increment}',
|
||||||
})
|
})
|
||||||
TaskAssigneeFactory.create(1, {
|
TaskAssigneeFactory.create(1, {
|
||||||
|
@ -346,7 +347,7 @@ describe('Task', () => {
|
||||||
it('Can add a new label to a task', () => {
|
it('Can add a new label to a task', () => {
|
||||||
const tasks = TaskFactory.create(1, {
|
const tasks = TaskFactory.create(1, {
|
||||||
id: 1,
|
id: 1,
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
})
|
})
|
||||||
LabelFactory.truncate()
|
LabelFactory.truncate()
|
||||||
const newLabelText = 'some new label'
|
const newLabelText = 'some new label'
|
||||||
|
@ -374,7 +375,7 @@ describe('Task', () => {
|
||||||
it('Can add an existing label to a task', () => {
|
it('Can add an existing label to a task', () => {
|
||||||
const tasks = TaskFactory.create(1, {
|
const tasks = TaskFactory.create(1, {
|
||||||
id: 1,
|
id: 1,
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
})
|
})
|
||||||
const labels = LabelFactory.create(1)
|
const labels = LabelFactory.create(1)
|
||||||
LabelTaskFactory.truncate()
|
LabelTaskFactory.truncate()
|
||||||
|
@ -387,13 +388,13 @@ describe('Task', () => {
|
||||||
it('Can add a label to a task and it shows up on the kanban board afterwards', () => {
|
it('Can add a label to a task and it shows up on the kanban board afterwards', () => {
|
||||||
const tasks = TaskFactory.create(1, {
|
const tasks = TaskFactory.create(1, {
|
||||||
id: 1,
|
id: 1,
|
||||||
list_id: lists[0].id,
|
project_id: projects[0].id,
|
||||||
bucket_id: buckets[0].id,
|
bucket_id: buckets[0].id,
|
||||||
})
|
})
|
||||||
const labels = LabelFactory.create(1)
|
const labels = LabelFactory.create(1)
|
||||||
LabelTaskFactory.truncate()
|
LabelTaskFactory.truncate()
|
||||||
|
|
||||||
cy.visit(`/lists/${lists[0].id}/kanban`)
|
cy.visit(`/projects/${projects[0].id}/kanban`)
|
||||||
|
|
||||||
cy.get('.bucket .task')
|
cy.get('.bucket .task')
|
||||||
.contains(tasks[0].title)
|
.contains(tasks[0].title)
|
||||||
|
@ -411,7 +412,7 @@ describe('Task', () => {
|
||||||
it('Can remove a label from a task', () => {
|
it('Can remove a label from a task', () => {
|
||||||
const tasks = TaskFactory.create(1, {
|
const tasks = TaskFactory.create(1, {
|
||||||
id: 1,
|
id: 1,
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
})
|
})
|
||||||
const labels = LabelFactory.create(1)
|
const labels = LabelFactory.create(1)
|
||||||
LabelTaskFactory.create(1, {
|
LabelTaskFactory.create(1, {
|
||||||
|
@ -421,10 +422,10 @@ describe('Task', () => {
|
||||||
|
|
||||||
cy.visit(`/tasks/${tasks[0].id}`)
|
cy.visit(`/tasks/${tasks[0].id}`)
|
||||||
|
|
||||||
cy.getSettled('.task-view .details.labels-list .multiselect .input-wrapper')
|
cy.get('.task-view .details.labels-list .multiselect .input-wrapper')
|
||||||
.should('be.visible')
|
.should('be.visible')
|
||||||
.should('contain', labels[0].title)
|
.should('contain', labels[0].title)
|
||||||
cy.getSettled('.task-view .details.labels-list .multiselect .input-wrapper')
|
cy.get('.task-view .details.labels-list .multiselect .input-wrapper')
|
||||||
.children()
|
.children()
|
||||||
.first()
|
.first()
|
||||||
.get('[data-cy="taskDetail.removeLabel"]')
|
.get('[data-cy="taskDetail.removeLabel"]')
|
||||||
|
@ -526,13 +527,13 @@ describe('Task', () => {
|
||||||
TaskAttachmentFactory.truncate()
|
TaskAttachmentFactory.truncate()
|
||||||
const tasks = TaskFactory.create(1, {
|
const tasks = TaskFactory.create(1, {
|
||||||
id: 1,
|
id: 1,
|
||||||
list_id: lists[0].id,
|
project_id: projects[0].id,
|
||||||
bucket_id: buckets[0].id,
|
bucket_id: buckets[0].id,
|
||||||
})
|
})
|
||||||
const labels = LabelFactory.create(1)
|
const labels = LabelFactory.create(1)
|
||||||
LabelTaskFactory.truncate()
|
LabelTaskFactory.truncate()
|
||||||
|
|
||||||
cy.visit(`/lists/${lists[0].id}/kanban`)
|
cy.visit(`/projects/${projects[0].id}/kanban`)
|
||||||
|
|
||||||
cy.get('.bucket .task')
|
cy.get('.bucket .task')
|
||||||
.contains(tasks[0].title)
|
.contains(tasks[0].title)
|
||||||
|
|
11
cypress/e2e/tsconfig.json
Normal file
11
cypress/e2e/tsconfig.json
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"extends": "@vue/tsconfig/tsconfig.web.json",
|
||||||
|
"include": ["./**/*", "../support/**/*", "../factories/**/*"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"isolatedModules": false,
|
||||||
|
"target": "ES2015",
|
||||||
|
"lib": ["ESNext", "dom"],
|
||||||
|
"types": ["cypress"]
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,16 +11,11 @@ const testAndAssertFailed = fixture => {
|
||||||
cy.get('div.message.danger').contains('Wrong username or password.')
|
cy.get('div.message.danger').contains('Wrong username or password.')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const username = 'test'
|
||||||
|
|
||||||
context('Login', () => {
|
context('Login', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
UserFactory.create(1, {
|
UserFactory.create(1, {username})
|
||||||
username: 'test',
|
|
||||||
})
|
|
||||||
cy.visit('/', {
|
|
||||||
onBeforeLoad(win) {
|
|
||||||
win.localStorage.removeItem('token')
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should log in with the right credentials', () => {
|
it('Should log in with the right credentials', () => {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import '../../support/authenticateUser'
|
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||||
import {createLists} from '../list/prepareLists'
|
import {createLists} from '../list/prepareLists'
|
||||||
|
|
||||||
function logout() {
|
function logout() {
|
||||||
|
@ -10,6 +10,8 @@ function logout() {
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('Log out', () => {
|
describe('Log out', () => {
|
||||||
|
createFakeUserAndLogin()
|
||||||
|
|
||||||
it('Logs the user out', () => {
|
it('Logs the user out', () => {
|
||||||
cy.visit('/')
|
cy.visit('/')
|
||||||
|
|
||||||
|
@ -24,21 +26,21 @@ describe('Log out', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it.skip('Should clear the list history after logging the user out', () => {
|
it.skip('Should clear the project history after logging the user out', () => {
|
||||||
const lists = createLists()
|
const projects = createProjects()
|
||||||
cy.visit(`/lists/${lists[0].id}`)
|
cy.visit(`/projects/${projects[0].id}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
expect(localStorage.getItem('listHistory')).to.not.eq(null)
|
expect(localStorage.getItem('projectHistory')).to.not.eq(null)
|
||||||
})
|
})
|
||||||
|
|
||||||
logout()
|
logout()
|
||||||
|
|
||||||
cy.wait(1000) // This makes re-loading of the list and associated entities (and the resulting error) visible
|
cy.wait(1000) // This makes re-loading of the project and associated entities (and the resulting error) visible
|
||||||
|
|
||||||
cy.url()
|
cy.url()
|
||||||
.should('contain', '/login')
|
.should('contain', '/login')
|
||||||
.then(() => {
|
.then(() => {
|
||||||
expect(localStorage.getItem('listHistory')).to.eq(null)
|
expect(localStorage.getItem('projectHistory')).to.eq(null)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
import {UserFactory} from '../../factories/user'
|
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||||
|
|
||||||
import '../../support/authenticateUser'
|
|
||||||
|
|
||||||
describe('User Settings', () => {
|
describe('User Settings', () => {
|
||||||
beforeEach(() => {
|
createFakeUserAndLogin()
|
||||||
UserFactory.create(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Changes the user avatar', () => {
|
it('Changes the user avatar', () => {
|
||||||
cy.intercept(`${Cypress.env('API_URL')}/user/settings/avatar/upload`).as('uploadAvatar')
|
cy.intercept(`${Cypress.env('API_URL')}/user/settings/avatar/upload`).as('uploadAvatar')
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import {faker} from '@faker-js/faker'
|
import {faker} from '@faker-js/faker'
|
||||||
import {Factory} from '../support/factory'
|
import {Factory} from '../support/factory'
|
||||||
import {formatISO} from 'date-fns'
|
|
||||||
|
|
||||||
export class BucketFactory extends Factory {
|
export class BucketFactory extends Factory {
|
||||||
static table = 'buckets'
|
static table = 'buckets'
|
||||||
|
@ -11,10 +10,10 @@ export class BucketFactory extends Factory {
|
||||||
return {
|
return {
|
||||||
id: '{increment}',
|
id: '{increment}',
|
||||||
title: faker.lorem.words(3),
|
title: faker.lorem.words(3),
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
created_by_id: 1,
|
created_by_id: 1,
|
||||||
created: formatISO(now),
|
created: now.toISOString(),
|
||||||
updated: formatISO(now)
|
updated: now.toISOString(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import {Factory} from '../support/factory'
|
import {Factory} from '../support/factory'
|
||||||
import {formatISO} from 'date-fns'
|
|
||||||
|
|
||||||
export class LabelTaskFactory extends Factory {
|
export class LabelTaskFactory extends Factory {
|
||||||
static table = 'label_tasks'
|
static table = 'label_tasks'
|
||||||
|
@ -11,7 +10,7 @@ export class LabelTaskFactory extends Factory {
|
||||||
id: '{increment}',
|
id: '{increment}',
|
||||||
task_id: 1,
|
task_id: 1,
|
||||||
label_id: 1,
|
label_id: 1,
|
||||||
created: formatISO(now),
|
created: now.toISOString(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
import {faker} from '@faker-js/faker'
|
import {faker} from '@faker-js/faker'
|
||||||
|
|
||||||
import {Factory} from '../support/factory'
|
import {Factory} from '../support/factory'
|
||||||
import {formatISO} from 'date-fns'
|
|
||||||
|
|
||||||
export class LabelFactory extends Factory {
|
export class LabelFactory extends Factory {
|
||||||
static table = 'labels'
|
static table = 'labels'
|
||||||
|
@ -15,8 +14,8 @@ export class LabelFactory extends Factory {
|
||||||
description: faker.lorem.text(10),
|
description: faker.lorem.text(10),
|
||||||
hex_color: (Math.random()*0xFFFFFF<<0).toString(16), // random 6-digit hex number
|
hex_color: (Math.random()*0xFFFFFF<<0).toString(16), // random 6-digit hex number
|
||||||
created_by_id: 1,
|
created_by_id: 1,
|
||||||
created: formatISO(now),
|
created: now.toISOString(),
|
||||||
updated: formatISO(now),
|
updated: now.toISOString(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
import {Factory} from '../support/factory'
|
import {Factory} from '../support/factory'
|
||||||
import {formatISO} from "date-fns"
|
|
||||||
import {faker} from '@faker-js/faker'
|
import {faker} from '@faker-js/faker'
|
||||||
|
|
||||||
export class LinkShareFactory extends Factory {
|
export class LinkShareFactory extends Factory {
|
||||||
|
@ -11,12 +10,12 @@ export class LinkShareFactory extends Factory {
|
||||||
return {
|
return {
|
||||||
id: '{increment}',
|
id: '{increment}',
|
||||||
hash: faker.random.word(32),
|
hash: faker.random.word(32),
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
right: 0,
|
right: 0,
|
||||||
sharing_type: 0,
|
sharing_type: 0,
|
||||||
shared_by_id: 1,
|
shared_by_id: 1,
|
||||||
created: formatISO(now),
|
created: now.toISOString(),
|
||||||
updated: formatISO(now)
|
updated: now.toISOString(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import {faker} from '@faker-js/faker'
|
import {faker} from '@faker-js/faker'
|
||||||
import {Factory} from '../support/factory'
|
import {Factory} from '../support/factory'
|
||||||
import {formatISO} from 'date-fns'
|
|
||||||
|
|
||||||
export class NamespaceFactory extends Factory {
|
export class NamespaceFactory extends Factory {
|
||||||
static table = 'namespaces'
|
static table = 'namespaces'
|
||||||
|
@ -12,8 +11,8 @@ export class NamespaceFactory extends Factory {
|
||||||
id: '{increment}',
|
id: '{increment}',
|
||||||
title: faker.lorem.words(3),
|
title: faker.lorem.words(3),
|
||||||
owner_id: 1,
|
owner_id: 1,
|
||||||
created: formatISO(now),
|
created: now.toISOString(),
|
||||||
updated: formatISO(now)
|
updated: now.toISOString(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import {Factory} from '../support/factory'
|
import {Factory} from '../support/factory'
|
||||||
import {formatISO} from "date-fns"
|
|
||||||
import {faker} from '@faker-js/faker'
|
import {faker} from '@faker-js/faker'
|
||||||
|
|
||||||
export class ListFactory extends Factory {
|
export class ProjectFactory extends Factory {
|
||||||
static table = 'lists'
|
static table = 'projects'
|
||||||
|
|
||||||
static factory() {
|
static factory() {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
|
@ -13,8 +12,8 @@ export class ListFactory extends Factory {
|
||||||
title: faker.lorem.words(3),
|
title: faker.lorem.words(3),
|
||||||
owner_id: 1,
|
owner_id: 1,
|
||||||
namespace_id: 1,
|
namespace_id: 1,
|
||||||
created: formatISO(now),
|
created: now.toISOString(),
|
||||||
updated: formatISO(now)
|
updated: now.toISOString(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
import {faker} from '@faker-js/faker'
|
import {faker} from '@faker-js/faker'
|
||||||
import {Factory} from '../support/factory'
|
import {Factory} from '../support/factory'
|
||||||
import {formatISO} from 'date-fns'
|
|
||||||
|
|
||||||
export class TaskFactory extends Factory {
|
export class TaskFactory extends Factory {
|
||||||
static table = 'tasks'
|
static table = 'tasks'
|
||||||
|
@ -12,12 +11,12 @@ export class TaskFactory extends Factory {
|
||||||
id: '{increment}',
|
id: '{increment}',
|
||||||
title: faker.lorem.words(3),
|
title: faker.lorem.words(3),
|
||||||
done: false,
|
done: false,
|
||||||
list_id: 1,
|
project_id: 1,
|
||||||
created_by_id: 1,
|
created_by_id: 1,
|
||||||
index: '{increment}',
|
index: '{increment}',
|
||||||
position: '{increment}',
|
position: '{increment}',
|
||||||
created: formatISO(now),
|
created: now.toISOString(),
|
||||||
updated: formatISO(now)
|
updated: now.toISOString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import {Factory} from '../support/factory'
|
import {Factory} from '../support/factory'
|
||||||
import {formatISO} from 'date-fns'
|
|
||||||
|
|
||||||
export class TaskAssigneeFactory extends Factory {
|
export class TaskAssigneeFactory extends Factory {
|
||||||
static table = 'task_assignees'
|
static table = 'task_assignees'
|
||||||
|
@ -11,7 +10,7 @@ export class TaskAssigneeFactory extends Factory {
|
||||||
id: '{increment}',
|
id: '{increment}',
|
||||||
task_id: 1,
|
task_id: 1,
|
||||||
user_id: 1,
|
user_id: 1,
|
||||||
created: formatISO(now),
|
created: now.toISOString(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
import {Factory} from '../support/factory'
|
import {Factory} from '../support/factory'
|
||||||
import {formatISO} from 'date-fns'
|
|
||||||
|
|
||||||
export class TaskAttachmentFactory extends Factory {
|
export class TaskAttachmentFactory extends Factory {
|
||||||
static table = 'task_attachments'
|
static table = 'task_attachments'
|
||||||
|
@ -11,7 +10,7 @@ export class TaskAttachmentFactory extends Factory {
|
||||||
id: '{increment}',
|
id: '{increment}',
|
||||||
task_id: 1,
|
task_id: 1,
|
||||||
file_id: 1,
|
file_id: 1,
|
||||||
created: formatISO(now),
|
created: now.toISOString(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
import {faker} from '@faker-js/faker'
|
import {faker} from '@faker-js/faker'
|
||||||
|
|
||||||
import {Factory} from '../support/factory'
|
import {Factory} from '../support/factory'
|
||||||
import {formatISO} from "date-fns"
|
|
||||||
|
|
||||||
export class TaskCommentFactory extends Factory {
|
export class TaskCommentFactory extends Factory {
|
||||||
static table = 'task_comments'
|
static table = 'task_comments'
|
||||||
|
@ -14,8 +13,8 @@ export class TaskCommentFactory extends Factory {
|
||||||
comment: faker.lorem.text(3),
|
comment: faker.lorem.text(3),
|
||||||
author_id: 1,
|
author_id: 1,
|
||||||
task_id: 1,
|
task_id: 1,
|
||||||
created: formatISO(now),
|
created: now.toISOString(),
|
||||||
updated: formatISO(now)
|
updated: now.toISOString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import {faker} from '@faker-js/faker'
|
import {faker} from '@faker-js/faker'
|
||||||
import {Factory} from '../support/factory'
|
import {Factory} from '../support/factory'
|
||||||
import {formatISO} from 'date-fns'
|
|
||||||
|
|
||||||
export class TeamFactory extends Factory {
|
export class TeamFactory extends Factory {
|
||||||
static table = 'teams'
|
static table = 'teams'
|
||||||
|
@ -11,8 +10,8 @@ export class TeamFactory extends Factory {
|
||||||
return {
|
return {
|
||||||
name: faker.lorem.words(3),
|
name: faker.lorem.words(3),
|
||||||
created_by_id: 1,
|
created_by_id: 1,
|
||||||
created: formatISO(now),
|
created: now.toISOString(),
|
||||||
updated: formatISO(now)
|
updated: now.toISOString(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import {Factory} from '../support/factory'
|
import {Factory} from '../support/factory'
|
||||||
import {formatISO} from 'date-fns'
|
|
||||||
|
|
||||||
export class TeamMemberFactory extends Factory {
|
export class TeamMemberFactory extends Factory {
|
||||||
static table = 'team_members'
|
static table = 'team_members'
|
||||||
|
@ -9,7 +8,7 @@ export class TeamMemberFactory extends Factory {
|
||||||
team_id: 1,
|
team_id: 1,
|
||||||
user_id: 1,
|
user_id: 1,
|
||||||
admin: false,
|
admin: false,
|
||||||
created: formatISO(new Date()),
|
created: new Date().toISOString(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
import {faker} from '@faker-js/faker'
|
import {faker} from '@faker-js/faker'
|
||||||
|
|
||||||
import {Factory} from '../support/factory'
|
import {Factory} from '../support/factory'
|
||||||
import {formatISO} from "date-fns"
|
|
||||||
|
|
||||||
export class UserFactory extends Factory {
|
export class UserFactory extends Factory {
|
||||||
static table = 'users'
|
static table = 'users'
|
||||||
|
@ -15,8 +14,8 @@ export class UserFactory extends Factory {
|
||||||
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.', // 1234
|
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.', // 1234
|
||||||
status: 0,
|
status: 0,
|
||||||
issuer: 'local',
|
issuer: 'local',
|
||||||
created: formatISO(now),
|
created: now.toISOString(),
|
||||||
updated: formatISO(now)
|
updated: now.toISOString(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,19 +0,0 @@
|
||||||
import {Factory} from '../support/factory'
|
|
||||||
import {formatISO} from "date-fns"
|
|
||||||
|
|
||||||
export class UserListFactory extends Factory {
|
|
||||||
static table = 'users_lists'
|
|
||||||
|
|
||||||
static factory() {
|
|
||||||
const now = new Date()
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: '{increment}',
|
|
||||||
list_id: 1,
|
|
||||||
user_id: 1,
|
|
||||||
right: 0,
|
|
||||||
created: formatISO(now),
|
|
||||||
updated: formatISO(now)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
18
cypress/factories/users_project.ts
Normal file
18
cypress/factories/users_project.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import {Factory} from '../support/factory'
|
||||||
|
|
||||||
|
export class UserProjectFactory extends Factory {
|
||||||
|
static table = 'users_projects'
|
||||||
|
|
||||||
|
static factory() {
|
||||||
|
const now = new Date()
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: '{increment}',
|
||||||
|
project_id: 1,
|
||||||
|
user_id: 1,
|
||||||
|
right: 0,
|
||||||
|
created: now.toISOString(),
|
||||||
|
updated: now.toISOString(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,26 +4,32 @@
|
||||||
|
|
||||||
import {UserFactory} from '../factories/user'
|
import {UserFactory} from '../factories/user'
|
||||||
|
|
||||||
let token
|
export function login(user, cacheAcrossSpecs = false) {
|
||||||
|
if (!user) {
|
||||||
before(() => {
|
throw new Error('Needs user')
|
||||||
const users = UserFactory.create(1)
|
}
|
||||||
|
// Caching session when logging in via page visit
|
||||||
cy.request('POST', `${Cypress.env('API_URL')}/login`, {
|
cy.session(`user__${user.username}`, () => {
|
||||||
username: users[0].username,
|
cy.request('POST', `${Cypress.env('API_URL')}/login`, {
|
||||||
password: '1234',
|
username: user.username,
|
||||||
})
|
password: '1234',
|
||||||
.its('body')
|
}).then(({ body }) => {
|
||||||
.then(r => {
|
window.localStorage.setItem('token', body.token)
|
||||||
token = r.token
|
|
||||||
})
|
})
|
||||||
})
|
}, {
|
||||||
|
cacheAcrossSpecs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
export function createFakeUserAndLogin() {
|
||||||
cy.log(`Using token ${token} to make authenticated requests`)
|
let user
|
||||||
cy.visit('/', {
|
before(() => {
|
||||||
onBeforeLoad(win) {
|
user = UserFactory.create(1)[0]
|
||||||
win.localStorage.setItem('token', token)
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
beforeEach(() => {
|
||||||
|
login(user, true)
|
||||||
|
})
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
|
@ -34,38 +34,4 @@
|
||||||
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
|
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively gets an element, returning only after it's determined to be attached to the DOM for good.
|
|
||||||
*
|
|
||||||
* Source: https://github.com/cypress-io/cypress/issues/7306#issuecomment-850621378
|
|
||||||
*/
|
|
||||||
Cypress.Commands.add('getSettled', (selector, opts = {}) => {
|
|
||||||
const retries = opts.retries || 3
|
|
||||||
const delay = opts.delay || 100
|
|
||||||
|
|
||||||
const isAttached = (resolve, count = 0) => {
|
|
||||||
const el = Cypress.$(selector)
|
|
||||||
|
|
||||||
// is element attached to the DOM?
|
|
||||||
count = Cypress.dom.isAttached(el) ? count + 1 : 0
|
|
||||||
|
|
||||||
// hit our base case, return the element
|
|
||||||
if (count >= retries) {
|
|
||||||
return resolve(el)
|
|
||||||
}
|
|
||||||
|
|
||||||
// retry after a bit of a delay
|
|
||||||
setTimeout(() => isAttached(resolve, count), delay)
|
|
||||||
}
|
|
||||||
|
|
||||||
// wrap, so we can chain cypress commands off the result
|
|
||||||
return cy.wrap(null).then(() => {
|
|
||||||
return new Cypress.Promise((resolve) => {
|
|
||||||
return isAttached(resolve, 0)
|
|
||||||
}).then((el) => {
|
|
||||||
return cy.wrap(el)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,10 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "@vue/tsconfig/tsconfig.web.json",
|
|
||||||
"include": ["./integration/**/*", "./support/**/*"],
|
|
||||||
"compilerOptions": {
|
|
||||||
"isolatedModules": false,
|
|
||||||
"target": "es5",
|
|
||||||
"lib": ["es5", "dom"],
|
|
||||||
"types": ["cypress"]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -30,21 +30,21 @@ A basic service can look like this:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
import AbstractService from './abstractService'
|
import AbstractService from './abstractService'
|
||||||
import ListModel from '../models/list'
|
import ProjectModel from '../models/project'
|
||||||
|
|
||||||
export default class ListService extends AbstractService {
|
export default class ProjectService extends AbstractService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super({
|
super({
|
||||||
getAll: '/lists',
|
getAll: '/projects',
|
||||||
get: '/lists/{id}',
|
get: '/projects/{id}',
|
||||||
create: '/namespaces/{namespaceID}/lists',
|
create: '/namespaces/{namespaceID}/projects',
|
||||||
update: '/lists/{id}',
|
update: '/projects/{id}',
|
||||||
delete: '/lists/{id}',
|
delete: '/projects/{id}',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
modelFactory(data) {
|
modelFactory(data) {
|
||||||
return new ListModel(data)
|
return new ProjectModel(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -132,7 +132,7 @@ import AbstractModel from './abstractModel'
|
||||||
import TaskModel from './task'
|
import TaskModel from './task'
|
||||||
import UserModel from './user'
|
import UserModel from './user'
|
||||||
|
|
||||||
export default class ListModel extends AbstractModel {
|
export default class ProjectModel extends AbstractModel {
|
||||||
|
|
||||||
constructor(data) {
|
constructor(data) {
|
||||||
// The constructor of AbstractModel handles all the default parsing.
|
// The constructor of AbstractModel handles all the default parsing.
|
||||||
|
|
9
env.config.d.ts
vendored
Normal file
9
env.config.d.ts
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
declare module 'postcss-easings' {
|
||||||
|
import postcssEasings from 'postcss-easings'
|
||||||
|
export default postcssEasings
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'postcss-easing-gradients' {
|
||||||
|
import postcssEasingGradients from 'postcss-easing-gradients'
|
||||||
|
export default postcssEasingGradients
|
||||||
|
}
|
11
env.d.ts
vendored
11
env.d.ts
vendored
|
@ -1,3 +1,12 @@
|
||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
/// <reference types="vite-svg-loader" />
|
/// <reference types="vite-svg-loader" />
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
/// <reference types="@histoire/plugin-vue/components" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_IS_ONLINE: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv
|
||||||
|
}
|
34
histoire.config.ts
Normal file
34
histoire.config.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import {defineConfig, defaultColors} from 'histoire'
|
||||||
|
import {HstVue} from '@histoire/plugin-vue'
|
||||||
|
import {HstScreenshot} from '@histoire/plugin-screenshot'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
setupFile: './src/histoire.setup.ts',
|
||||||
|
storyIgnored: [
|
||||||
|
'**/node_modules/**',
|
||||||
|
'**/dist/**',
|
||||||
|
// see https://kolaente.dev/vikunja/frontend/pulls/2724#issuecomment-42012
|
||||||
|
'**/.direnv/**',
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
HstVue(),
|
||||||
|
HstScreenshot({
|
||||||
|
// Options here
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
title: 'Vikunja',
|
||||||
|
colors: {
|
||||||
|
// https://histoire.dev/guide/config.html#builtin-colors
|
||||||
|
gray: defaultColors.zinc,
|
||||||
|
primary: defaultColors.cyan,
|
||||||
|
},
|
||||||
|
// logo: {
|
||||||
|
// square: './img/square.png',
|
||||||
|
// light: './img/light.png',
|
||||||
|
// dark: './img/dark.png',
|
||||||
|
// },
|
||||||
|
// logoHref: 'https://acme.com',
|
||||||
|
// favicon: './favicon.ico',
|
||||||
|
},
|
||||||
|
})
|
10
index.html
10
index.html
|
@ -9,13 +9,9 @@
|
||||||
|
|
||||||
<link rel="icon" href="/favicon.ico">
|
<link rel="icon" href="/favicon.ico">
|
||||||
<link rel="apple-touch-icon" href="/images/icons/apple-touch-icon-180x180.png"/>
|
<link rel="apple-touch-icon" href="/images/icons/apple-touch-icon-180x180.png"/>
|
||||||
<link rel="preload" crossorigin="anonymous" href="/fonts/open-sans-v15-latin-700italic.woff2" as="font">
|
<link rel="preload" crossorigin="anonymous" href="/src/assets/fonts/OpenSans[wght].woff2" as="font">
|
||||||
<link rel="preload" crossorigin="anonymous" href="/fonts/open-sans-v15-latin-italic.woff2" as="font">
|
<link rel="preload" crossorigin="anonymous" href="/src/assets/fonts/OpenSans-Italic[wght].woff2" as="font">
|
||||||
<link rel="preload" crossorigin="anonymous" href="/fonts/quicksand-v7-latin-500.woff2" as="font">
|
<link rel="preload" crossorigin="anonymous" href="/src/assets/fonts/Quicksand[wght].woff2" as="font">
|
||||||
<link rel="preload" crossorigin="anonymous" href="/fonts/quicksand-v7-latin-700.woff2" as="font">
|
|
||||||
<link rel="preload" crossorigin="anonymous" href="/fonts/open-sans-v15-latin-regular.woff2" as="font">
|
|
||||||
<link rel="preload" crossorigin="anonymous" href="/fonts/open-sans-v15-latin-700.woff2" as="font">
|
|
||||||
<link rel="preload" crossorigin="anonymous" href="/fonts/quicksand-v7-latin-regular.woff2" as="font">
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>
|
<noscript>
|
||||||
|
|
BIN
originalMedia/fonts/OpenSans-Italic[wdth,wght].ttf
Normal file
BIN
originalMedia/fonts/OpenSans-Italic[wdth,wght].ttf
Normal file
Binary file not shown.
BIN
originalMedia/fonts/OpenSans[wdth,wght].ttf
Normal file
BIN
originalMedia/fonts/OpenSans[wdth,wght].ttf
Normal file
Binary file not shown.
BIN
originalMedia/fonts/Quicksand[wght].ttf
Normal file
BIN
originalMedia/fonts/Quicksand[wght].ttf
Normal file
Binary file not shown.
154
package.json
154
package.json
|
@ -1,63 +1,91 @@
|
||||||
{
|
{
|
||||||
"name": "vikunja-frontend",
|
"name": "vikunja-frontend",
|
||||||
"version": "0.10.0",
|
"description": "The todo app to organize your life.",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"version": "0.10.0",
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://kolaente.dev/vikunja/frontend"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://kolaente.dev/vikunja/frontend/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://vikunja.io/",
|
||||||
|
"funding": "https://opencollective.com/vikunja",
|
||||||
|
"packageManager": "pnpm@7.25.0",
|
||||||
|
"keywords": [
|
||||||
|
"todo",
|
||||||
|
"productivity",
|
||||||
|
"task management",
|
||||||
|
"organisation",
|
||||||
|
"gantt",
|
||||||
|
"kanban"
|
||||||
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vite",
|
"serve": "vite",
|
||||||
"serve:dist-dev": "node scripts/serve-dist.js",
|
"preview": "vite preview --port 4173",
|
||||||
"serve:dist": "vite preview --port 4173",
|
"preview:dev": "vite preview --outDir dist-dev --mode development --port 4173",
|
||||||
"build": "vite build && workbox copyLibraries dist/",
|
"build": "vite build && workbox copyLibraries dist/",
|
||||||
"build:modern-only": "BUILD_MODERN_ONLY=true vite build && workbox copyLibraries dist/",
|
"build:modern-only": "BUILD_MODERN_ONLY=true vite build && workbox copyLibraries dist/",
|
||||||
"build:dev": "vite build -m development --outDir dist-dev/",
|
"build:dev": "vite build --mode development --outDir dist-dev/",
|
||||||
"lint": "eslint --ignore-pattern '*.test.*' ./src --ext .vue,.js,.ts",
|
"lint": "eslint --ignore-pattern '*.test.*' ./src --ext .vue,.js,.ts",
|
||||||
"cypress:open": "cypress open",
|
"test:e2e": "start-server-and-test preview http://127.0.0.1:4173 'cypress run --e2e --browser chrome'",
|
||||||
"test:unit": "vitest --run",
|
"test:e2e-record": "start-server-and-test preview http://127.0.0.1:4173 'cypress run --e2e --browser chrome --record'",
|
||||||
"test:unit-watch": "vitest watch",
|
"test:e2e-dev-dev": "start-server-and-test preview:dev http://127.0.0.1:4173 'cypress open --e2e'",
|
||||||
"test:frontend": "cypress run",
|
"test:e2e-dev": "start-server-and-test preview http://127.0.0.1:4173 'cypress open --e2e'",
|
||||||
|
"test:unit": "vitest",
|
||||||
"typecheck": "vue-tsc --noEmit && vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
|
"typecheck": "vue-tsc --noEmit && vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
|
||||||
"browserslist:update": "npx browserslist@latest --update-db"
|
"browserslist:update": "pnpm dlx browserslist@latest --update-db",
|
||||||
|
"fonts:update": "pnpm fonts:download && pnpm fonts:subset",
|
||||||
|
"fonts:download": "./scripts/fonts-download.sh",
|
||||||
|
"fonts:subset": "./scripts/fonts-subset.sh",
|
||||||
|
"story:dev": "histoire dev",
|
||||||
|
"story:build": "histoire build",
|
||||||
|
"story:preview": "histoire preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "6.2.0",
|
"@fortawesome/fontawesome-svg-core": "6.2.1",
|
||||||
"@fortawesome/free-regular-svg-icons": "6.2.0",
|
"@fortawesome/free-regular-svg-icons": "6.2.1",
|
||||||
"@fortawesome/free-solid-svg-icons": "6.2.0",
|
"@fortawesome/free-solid-svg-icons": "6.2.1",
|
||||||
"@fortawesome/vue-fontawesome": "3.0.2",
|
"@fortawesome/vue-fontawesome": "3.0.2",
|
||||||
"@github/hotkey": "2.0.1",
|
"@github/hotkey": "2.0.1",
|
||||||
"@infectoone/vue-ganttastic": "2.1.2",
|
"@infectoone/vue-ganttastic": "2.1.3",
|
||||||
"@kyvg/vue3-notification": "2.6.1",
|
"@intlify/unplugin-vue-i18n": "0.8.1",
|
||||||
"@sentry/tracing": "7.19.0",
|
"@kyvg/vue3-notification": "2.8.0",
|
||||||
"@sentry/vue": "7.19.0",
|
"@sentry/tracing": "7.31.1",
|
||||||
|
"@sentry/vue": "7.31.1",
|
||||||
"@types/is-touch-device": "1.0.0",
|
"@types/is-touch-device": "1.0.0",
|
||||||
"@types/lodash.clonedeep": "4.5.7",
|
"@types/lodash.clonedeep": "4.5.7",
|
||||||
"@types/sortablejs": "1.15.0",
|
"@types/sortablejs": "1.15.0",
|
||||||
"@vueuse/core": "9.5.0",
|
"@vueuse/core": "9.11.0",
|
||||||
"@vueuse/router": "9.5.0",
|
"axios": "1.2.2",
|
||||||
"axios": "0.27.2",
|
|
||||||
"blurhash": "2.0.4",
|
"blurhash": "2.0.4",
|
||||||
"bulma-css-variables": "0.9.33",
|
"bulma-css-variables": "0.9.33",
|
||||||
"camel-case": "4.1.2",
|
"camel-case": "4.1.2",
|
||||||
"codemirror": "5.65.9",
|
"codemirror": "5.65.11",
|
||||||
"date-fns": "2.29.3",
|
"date-fns": "2.29.3",
|
||||||
"dayjs": "1.11.6",
|
"dayjs": "1.11.7",
|
||||||
"dompurify": "2.4.1",
|
"dompurify": "2.4.3",
|
||||||
"easymde": "2.18.0",
|
"easymde": "2.18.0",
|
||||||
"fast-deep-equal": "3.1.3",
|
"fast-deep-equal": "3.1.3",
|
||||||
"flatpickr": "4.6.13",
|
"flatpickr": "4.6.13",
|
||||||
"flexsearch": "0.7.21",
|
"flexsearch": "0.7.21",
|
||||||
"floating-vue": "2.0.0-beta.20",
|
"floating-vue": "2.0.0-beta.20",
|
||||||
"highlight.js": "11.6.0",
|
"focus-within": "3.0.2",
|
||||||
|
"highlight.js": "11.7.0",
|
||||||
"is-touch-device": "1.0.1",
|
"is-touch-device": "1.0.1",
|
||||||
"lodash.clonedeep": "4.5.0",
|
"lodash.clonedeep": "4.5.0",
|
||||||
"lodash.debounce": "4.0.8",
|
"lodash.debounce": "4.0.8",
|
||||||
"marked": "4.2.2",
|
"marked": "4.2.12",
|
||||||
"minimist": "1.2.7",
|
"minimist": "1.2.7",
|
||||||
"pinia": "2.0.23",
|
"pinia": "2.0.29",
|
||||||
"register-service-worker": "1.7.2",
|
"register-service-worker": "1.7.2",
|
||||||
"snake-case": "3.0.4",
|
"snake-case": "3.0.4",
|
||||||
"sortablejs": "1.15.0",
|
"sortablejs": "1.15.0",
|
||||||
"ufo": "0.8.6",
|
"ufo": "1.0.1",
|
||||||
"vue": "3.2.45",
|
"vue": "3.2.45",
|
||||||
"vue-advanced-cropper": "2.8.6",
|
"vue-advanced-cropper": "2.8.8",
|
||||||
"vue-flatpickr-component": "11.0.1",
|
"vue-flatpickr-component": "11.0.1",
|
||||||
"vue-i18n": "9.2.2",
|
"vue-i18n": "9.2.2",
|
||||||
"vue-router": "4.1.6",
|
"vue-router": "4.1.6",
|
||||||
|
@ -65,50 +93,54 @@
|
||||||
"zhyswan-vuedraggable": "4.1.3"
|
"zhyswan-vuedraggable": "4.1.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@4tw/cypress-drag-drop": "2.2.1",
|
"@4tw/cypress-drag-drop": "2.2.3",
|
||||||
"@cypress/vite-dev-server": "4.0.1",
|
"@cypress/vite-dev-server": "5.0.2",
|
||||||
"@cypress/vue": "5.0.1",
|
"@cypress/vue": "5.0.3",
|
||||||
"@faker-js/faker": "7.6.0",
|
"@faker-js/faker": "7.6.0",
|
||||||
|
"@histoire/plugin-screenshot": "0.12.4",
|
||||||
|
"@histoire/plugin-vue": "0.12.4",
|
||||||
"@rushstack/eslint-patch": "1.2.0",
|
"@rushstack/eslint-patch": "1.2.0",
|
||||||
"@types/codemirror": "5.60.5",
|
"@types/codemirror": "5.60.6",
|
||||||
"@types/dompurify": "2.4.0",
|
"@types/dompurify": "2.4.0",
|
||||||
"@types/flexsearch": "0.7.3",
|
"@types/flexsearch": "0.7.3",
|
||||||
|
"@types/focus-within": "1.0.1",
|
||||||
"@types/lodash.debounce": "4.0.7",
|
"@types/lodash.debounce": "4.0.7",
|
||||||
"@types/marked": "4.0.7",
|
"@types/marked": "4.0.8",
|
||||||
"@types/node": "18.11.9",
|
"@types/node": "18.11.18",
|
||||||
"@types/postcss-preset-env": "7.7.0",
|
"@types/postcss-preset-env": "7.7.0",
|
||||||
"@typescript-eslint/eslint-plugin": "5.42.1",
|
"@typescript-eslint/eslint-plugin": "5.48.2",
|
||||||
"@typescript-eslint/parser": "5.42.1",
|
"@typescript-eslint/parser": "5.48.2",
|
||||||
"@vitejs/plugin-legacy": "2.3.1",
|
"@vitejs/plugin-legacy": "3.0.1",
|
||||||
"@vitejs/plugin-vue": "3.2.0",
|
"@vitejs/plugin-vue": "4.0.0",
|
||||||
"@vue/eslint-config-typescript": "11.0.2",
|
"@vue/eslint-config-typescript": "11.0.2",
|
||||||
"@vue/test-utils": "2.2.2",
|
"@vue/test-utils": "2.2.7",
|
||||||
"@vue/tsconfig": "0.1.3",
|
"@vue/tsconfig": "0.1.3",
|
||||||
"autoprefixer": "10.4.13",
|
"autoprefixer": "10.4.13",
|
||||||
"browserslist": "4.21.4",
|
"browserslist": "4.21.4",
|
||||||
"caniuse-lite": "1.0.30001430",
|
"caniuse-lite": "1.0.30001445",
|
||||||
"csstype": "3.1.1",
|
"csstype": "3.1.1",
|
||||||
"cypress": "11.0.1",
|
"cypress": "12.3.0",
|
||||||
"esbuild": "0.15.13",
|
"esbuild": "0.17.2",
|
||||||
"eslint": "8.27.0",
|
"eslint": "8.32.0",
|
||||||
"eslint-plugin-vue": "9.7.0",
|
"eslint-plugin-vue": "9.9.0",
|
||||||
"express": "4.18.2",
|
"happy-dom": "8.1.4",
|
||||||
"happy-dom": "7.6.6",
|
"histoire": "0.12.4",
|
||||||
"netlify-cli": "12.1.0",
|
"netlify-cli": "12.7.2",
|
||||||
"postcss": "8.4.19",
|
"postcss": "8.4.21",
|
||||||
"postcss-preset-env": "7.8.2",
|
"postcss-easing-gradients": "3.0.1",
|
||||||
"rollup": "3.3.0",
|
"postcss-easings": "3.0.1",
|
||||||
"rollup-plugin-visualizer": "5.8.3",
|
"postcss-preset-env": "7.8.3",
|
||||||
"sass": "1.56.1",
|
"rollup": "3.10.0",
|
||||||
"typescript": "4.8.4",
|
"rollup-plugin-visualizer": "5.9.0",
|
||||||
"vite": "3.2.3",
|
"sass": "1.57.1",
|
||||||
"vite-plugin-pwa": "0.13.3",
|
"start-server-and-test": "1.15.2",
|
||||||
"vite-svg-loader": "3.6.0",
|
"typescript": "4.9.4",
|
||||||
"vitest": "0.25.1",
|
"vite": "4.0.4",
|
||||||
"vue-tsc": "1.0.9",
|
"vite-plugin-pwa": "0.14.1",
|
||||||
"wait-on": "6.0.1",
|
"vite-svg-loader": "4.0.0",
|
||||||
|
"vitest": "0.27.2",
|
||||||
|
"vue-tsc": "1.0.24",
|
||||||
|
"wait-on": "7.0.1",
|
||||||
"workbox-cli": "6.5.4"
|
"workbox-cli": "6.5.4"
|
||||||
},
|
}
|
||||||
"license": "AGPL-3.0-or-later",
|
|
||||||
"packageManager": "pnpm@7.15.0"
|
|
||||||
}
|
}
|
||||||
|
|
4441
pnpm-lock.yaml
4441
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,15 +1,18 @@
|
||||||
const {exec} = require('child_process')
|
const { exec } = require('child_process')
|
||||||
const axios = require('axios')
|
|
||||||
|
function createSlug(string) {
|
||||||
|
return String(string)
|
||||||
|
.trim()
|
||||||
|
.normalize('NFKD')
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[.\s/]/g, '-')
|
||||||
|
.replace(/[^A-Za-z\d-]/g, '')
|
||||||
|
}
|
||||||
|
|
||||||
const BOT_USER_ID = 513
|
const BOT_USER_ID = 513
|
||||||
const giteaToken = process.env.GITEA_TOKEN
|
const giteaToken = process.env.GITEA_TOKEN
|
||||||
const siteId = process.env.NETLIFY_SITE_ID
|
const siteId = process.env.NETLIFY_SITE_ID
|
||||||
const branchSlug = String(process.env.DRONE_SOURCE_BRANCH)
|
const branchSlug = createSlug(process.env.DRONE_SOURCE_BRANCH)
|
||||||
.trim()
|
|
||||||
.normalize('NFKD')
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/[.\s/]/g, '-')
|
|
||||||
.replace(/[^A-Za-z\d-]/g, '')
|
|
||||||
const prNumber = process.env.DRONE_PULL_REQUEST
|
const prNumber = process.env.DRONE_PULL_REQUEST
|
||||||
|
|
||||||
const prIssueCommentsUrl = `https://kolaente.dev/api/v1/repos/vikunja/frontend/issues/${prNumber}/comments`
|
const prIssueCommentsUrl = `https://kolaente.dev/api/v1/repos/vikunja/frontend/issues/${prNumber}/comments`
|
||||||
|
@ -35,7 +38,7 @@ const promiseExec = cmd => {
|
||||||
stdout = await promiseExec(`./node_modules/.bin/netlify deploy --alias ${alias}`)
|
stdout = await promiseExec(`./node_modules/.bin/netlify deploy --alias ${alias}`)
|
||||||
console.log(stdout)
|
console.log(stdout)
|
||||||
|
|
||||||
const {data} = await axios.get(prIssueCommentsUrl)
|
const data = await fetch(prIssueCommentsUrl).then(response => response.json())
|
||||||
const hasComment = data.some(c => c.user.id === BOT_USER_ID)
|
const hasComment = data.some(c => c.user.id === BOT_USER_ID)
|
||||||
|
|
||||||
if (hasComment) {
|
if (hasComment) {
|
||||||
|
@ -43,8 +46,7 @@ const promiseExec = cmd => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await axios.post(prIssueCommentsUrl, {
|
const message = `
|
||||||
body: `
|
|
||||||
Hi ${process.env.DRONE_COMMIT_AUTHOR}!
|
Hi ${process.env.DRONE_COMMIT_AUTHOR}!
|
||||||
|
|
||||||
Thank you for creating a PR!
|
Thank you for creating a PR!
|
||||||
|
@ -57,14 +59,25 @@ You will need to manually connect this to an api running somehwere. The easiest
|
||||||
Have a nice day!
|
Have a nice day!
|
||||||
|
|
||||||
> Beep boop, I'm a bot.
|
> Beep boop, I'm a bot.
|
||||||
`,
|
`
|
||||||
}, {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'accept': 'application/json',
|
|
||||||
'Authorization': `token ${giteaToken}`,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log(`Preview comment sent successfully to PR #${prNumber}!`)
|
try {
|
||||||
|
const response = await fetch(prIssueCommentsUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
body: message,
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'accept': 'application/json',
|
||||||
|
'Authorization': `token ${giteaToken}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error, status = ${response.status}`)
|
||||||
|
}
|
||||||
|
console.log(`Preview comment sent successfully to PR #${prNumber}!`)
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`Could not send preview comment to PR #${prNumber}! ${e.message}`)
|
||||||
|
}
|
||||||
})()
|
})()
|
|
@ -1 +1 @@
|
||||||
bb46342a0a08105b340ba7976cff9d80ef89901120ec0639669caa70bb7d2dbc43e78b1f635a7654ab2456e8358c98a4 ./scripts/deploy-preview-netlify.js
|
24df869e7a9282c76c9e1883071a39c0b11a53a57da68b37f2b918df25b1ae0f1b403e38a29c9cb694575bb9a7b52b6e ./scripts/deploy-preview-netlify.js
|
||||||
|
|
56
scripts/fonts-download.sh
Executable file
56
scripts/fonts-download.sh
Executable file
|
@ -0,0 +1,56 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
#
|
||||||
|
# This script downloads our original font files from their source repos
|
||||||
|
# and puts them in our originalMedia folder.
|
||||||
|
#
|
||||||
|
|
||||||
|
err_report() {
|
||||||
|
echo "Error on line $(caller)" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
trap err_report ERR
|
||||||
|
|
||||||
|
ORIGINAL_FONTS_DIR="./originalMedia/fonts"
|
||||||
|
|
||||||
|
# update these if there is a new version
|
||||||
|
FONT_URLS=(
|
||||||
|
"https://github.com/googlefonts/opensans/blob/27d060e1aad6886daeda67629ee28189f795f534/fonts/variable/OpenSans%5Bwdth%2Cwght%5D.ttf?raw=true"
|
||||||
|
"https://github.com/googlefonts/opensans/blob/27d060e1aad6886daeda67629ee28189f795f534/fonts/variable/OpenSans-Italic%5Bwdth%2Cwght%5D.ttf?raw=true"
|
||||||
|
"https://github.com/andrew-paglinawan/QuicksandFamily/blob/db6de44878582966f45a0debaef10d57108d93a7/fonts/Quicksand%5Bwght%5D.ttf?raw=true"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "###################################################"
|
||||||
|
echo "# Download font files"
|
||||||
|
echo "###################################################"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
mkdir -p $ORIGINAL_FONTS_DIR
|
||||||
|
|
||||||
|
for URL in ${FONT_URLS[@]}; do
|
||||||
|
wget -L $URL \
|
||||||
|
--directory-prefix=$ORIGINAL_FONTS_DIR \
|
||||||
|
--quiet \
|
||||||
|
--timestamping \
|
||||||
|
--show-progress
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "###################################################"
|
||||||
|
echo "# Remove '?raw=true' filename suffix"
|
||||||
|
echo "###################################################"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Iterate over all files in directory with filetype ending in "?raw=true"
|
||||||
|
for file in $ORIGINAL_FONTS_DIR/*?raw=true; do
|
||||||
|
# Remove "?raw=true" from file name and store in variable
|
||||||
|
new_name=$(echo $file | sed 's/?raw=true//')
|
||||||
|
|
||||||
|
# Overwrite existing file with new name
|
||||||
|
mv -v $file $new_name
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Renaming files complete"
|
161
scripts/fonts-subset.sh
Executable file
161
scripts/fonts-subset.sh
Executable file
|
@ -0,0 +1,161 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
#
|
||||||
|
# This script subsets our variable fonts,
|
||||||
|
# converts them to woff2 files and puts them in the
|
||||||
|
# fonts folder.
|
||||||
|
#
|
||||||
|
# We do have to update the font paths in the @font-face
|
||||||
|
# definitions manually since we use a checksum to make
|
||||||
|
#
|
||||||
|
# We use fonttools to create a partial instance of the
|
||||||
|
# variable font where we keep only our needed features.
|
||||||
|
# See more at:
|
||||||
|
# https://fonttools.readthedocs.io/en/latest/varLib/instancer.html
|
||||||
|
#
|
||||||
|
# fonttools requires python > 3.7. For up-to-date
|
||||||
|
# instructions see https://github.com/fonttools/fonttools#installation
|
||||||
|
#
|
||||||
|
# Lot's of info was gathered from:
|
||||||
|
# https://markoskon.com/creating-font-subsets/
|
||||||
|
# https://barrd.dev/article/create-a-variable-font-subset-for-smaller-file-size/
|
||||||
|
#
|
||||||
|
|
||||||
|
ORIGINAL_FONTS="./originalMedia/fonts"
|
||||||
|
TEMP_FOLDER="./.subset-fonts-temp"
|
||||||
|
FONT_FOLDER="./src/assets/fonts"
|
||||||
|
|
||||||
|
err_report() {
|
||||||
|
echo "Error on line $(caller)" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
trap err_report ERR
|
||||||
|
|
||||||
|
mkdir -p $TEMP_FOLDER
|
||||||
|
|
||||||
|
# the latin subset that google uses on GoogleFonts
|
||||||
|
# this is the same as the latin subset range that google uses on GoogleFonts
|
||||||
|
# see for examle the unicode-range definition here:
|
||||||
|
# https://fonts.googleapis.com/css2?family=Open+Sans
|
||||||
|
UNICODE_LATIN_SUBSET="U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,\
|
||||||
|
U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,\
|
||||||
|
U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD"
|
||||||
|
|
||||||
|
get_filename_without_type() {
|
||||||
|
filename=$1
|
||||||
|
dirname=$(dirname $filename)
|
||||||
|
# Extract the file type using parameter expansion
|
||||||
|
filetype=${filename##*.}
|
||||||
|
basename=$(basename $filename .$filetype)
|
||||||
|
echo $basename
|
||||||
|
}
|
||||||
|
|
||||||
|
# This function takes a font file and creates a subset of it using a specified set of unicode characters.
|
||||||
|
instance_and_subset () {
|
||||||
|
# Define default arguments for the subsetter.
|
||||||
|
DEFAULT_SUBSETTER_ARGS="--layout-features=* --unicodes=${UNICODE_LATIN_SUBSET}"
|
||||||
|
|
||||||
|
# Assign function arguments to variables with more descriptive names.
|
||||||
|
INPUT_FONT_FILE=$1
|
||||||
|
INSTANCER_ARGS=$2
|
||||||
|
OUTPUT_FONT_BASENAME=$3
|
||||||
|
OUTPUT_FOLDER=$FONT_FOLDER
|
||||||
|
|
||||||
|
# If the output font basename is not provided, use the input font file's basename as the output font basename.
|
||||||
|
if [ -z "$OUTPUT_FONT_BASENAME" ]; then
|
||||||
|
INPUT_FONT_BASENAME=$(get_filename_without_type $INPUT_FONT_FILE)
|
||||||
|
OUTPUT_FONT_BASENAME=$INPUT_FONT_BASENAME
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use the default subsetter arguments if no custom arguments are provided.
|
||||||
|
SUBSETTER_ARGS="${4:-$DEFAULT_SUBSETTER_ARGS}"
|
||||||
|
|
||||||
|
CHECKSUM=$(
|
||||||
|
# Concatenate the contents of the input font file, the instancer arguments, and the subsetter arguments
|
||||||
|
printf "%s%s" "$(cat $INPUT_FONT_FILE)" "$INSTANCER_ARGS" "$SUBSETTER_ARGS" |
|
||||||
|
# Calculate the Blake2b checksum of the concatenated string
|
||||||
|
b2sum |
|
||||||
|
# Extract the checksum from the output of b2sum (it's the first field)
|
||||||
|
awk '{print $1}'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Limit the checksum to 8 characters.
|
||||||
|
CHECKSUM=$(echo "${CHECKSUM:0:8}")
|
||||||
|
|
||||||
|
# Construct the output font's filename
|
||||||
|
OUTPUT_FONT_BASENAME="${OUTPUT_FONT_BASENAME}_${CHECKSUM}"
|
||||||
|
OUTPUT_FONT_FILE="${OUTPUT_FOLDER}/${OUTPUT_FONT_BASENAME}.woff2"
|
||||||
|
|
||||||
|
# Check if the output font file already exists
|
||||||
|
if test -f $OUTPUT_FONT_FILE; then
|
||||||
|
echo "${OUTPUT_FONT_FILE} exists"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
FONT_INSTANCE="${TEMP_FOLDER}/${OUTPUT_FONT_BASENAME}.ttf"
|
||||||
|
|
||||||
|
if [ -n "$INSTANCER_ARGS" ]; then
|
||||||
|
# If the INSTANCER_ARGS variable is set, use fonttools to create a font instance
|
||||||
|
fonttools varLib.instancer --output $FONT_INSTANCE $INPUT_FONT_FILE $INSTANCER_ARGS
|
||||||
|
else
|
||||||
|
# Otherwise, just copy the input font file to the font instance file
|
||||||
|
cp $INPUT_FONT_FILE $FONT_INSTANCE
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use pyftsubset to create a subset of the font instance and save it to the output font file
|
||||||
|
pyftsubset $FONT_INSTANCE --output-file=$OUTPUT_FONT_FILE --flavor=woff2 $SUBSETTER_ARGS
|
||||||
|
|
||||||
|
echo "${OUTPUT_FONT_BASENAME} subsetted."
|
||||||
|
}
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "###################################################"
|
||||||
|
echo "# Install required libs"
|
||||||
|
echo "###################################################"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
pip install fonttools brotli
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "###################################################"
|
||||||
|
echo "# Create a partial instance of the variable font"
|
||||||
|
echo "# where we keep only our needed features and then"
|
||||||
|
echo "# subset fonts with latin unicode range and export"
|
||||||
|
echo "# as woff2 file"
|
||||||
|
echo "###################################################"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
mkdir -p $TEMP_FOLDER
|
||||||
|
|
||||||
|
echo "\nOpen Sans"
|
||||||
|
# we drop the wdth axis for all
|
||||||
|
|
||||||
|
instance_and_subset "${ORIGINAL_FONTS}/OpenSans[wdth,wght].ttf" "wdth=drop wght=400:700" "OpenSans[wght]"
|
||||||
|
|
||||||
|
# we restrict the wght range
|
||||||
|
instance_and_subset "${ORIGINAL_FONTS}/OpenSans[wdth,wght].ttf" "wdth=drop wght=400" "OpenSans-Regular"
|
||||||
|
instance_and_subset "${ORIGINAL_FONTS}/OpenSans[wdth,wght].ttf" "wdth=drop wght=700" "OpenSans-Bold"
|
||||||
|
|
||||||
|
echo "\nOpen Sans Italic"
|
||||||
|
# we drop the wdth axis for all
|
||||||
|
|
||||||
|
instance_and_subset "${ORIGINAL_FONTS}/OpenSans-Italic[wdth,wght].ttf" "wdth=drop wght=400:700" "OpenSans-Italic[wght]"
|
||||||
|
|
||||||
|
# we restrict the wght range
|
||||||
|
instance_and_subset "${ORIGINAL_FONTS}/OpenSans-Italic[wdth,wght].ttf" "wdth=drop wght=400" "OpenSans-RegularItalic"
|
||||||
|
instance_and_subset "${ORIGINAL_FONTS}/OpenSans-Italic[wdth,wght].ttf" "wdth=drop wght=700" "OpenSans-BoldItalic"
|
||||||
|
|
||||||
|
echo "\nQuicksand"
|
||||||
|
|
||||||
|
instance_and_subset "${ORIGINAL_FONTS}/Quicksand[wght].ttf" "wght=400:700"
|
||||||
|
|
||||||
|
# we restrict the wght range
|
||||||
|
instance_and_subset "${ORIGINAL_FONTS}/Quicksand[wght].ttf" "wght=400" "Quicksand-Regular"
|
||||||
|
instance_and_subset "${ORIGINAL_FONTS}/Quicksand[wght].ttf" "wght=600" "Quicksand-SemiBold"
|
||||||
|
instance_and_subset "${ORIGINAL_FONTS}/Quicksand[wght].ttf" "wght=700" "Quicksand-Bold"
|
||||||
|
|
||||||
|
echo "\nSubsetting files complete"
|
||||||
|
|
||||||
|
# remove temp folder
|
||||||
|
rm -r $TEMP_FOLDER
|
|
@ -18,8 +18,8 @@ sed -i "s/'\/api\/v1/'$VIKUNJA_API_URL/g" /usr/share/nginx/html/index.html
|
||||||
sed -i "s/\.SENTRY_ENABLED = false/\.SENTRY_ENABLED = $VIKUNJA_SENTRY_ENABLED/g" /usr/share/nginx/html/index.html
|
sed -i "s/\.SENTRY_ENABLED = false/\.SENTRY_ENABLED = $VIKUNJA_SENTRY_ENABLED/g" /usr/share/nginx/html/index.html
|
||||||
sed -i "s|\.SENTRY_DSN = '.*'|\.SENTRY_DSN = '$VIKUNJA_SENTRY_DSN'|g" /usr/share/nginx/html/index.html
|
sed -i "s|\.SENTRY_DSN = '.*'|\.SENTRY_DSN = '$VIKUNJA_SENTRY_DSN'|g" /usr/share/nginx/html/index.html
|
||||||
|
|
||||||
sed -i "s/listen 80/listen $VIKUNJA_HTTP_PORT/g" /etc/nginx/nginx.conf
|
sed -i "s/projecten 80/projecten $VIKUNJA_HTTP_PORT/g" /etc/nginx/nginx.conf
|
||||||
sed -i "s/listen 443/listen $VIKUNJA_HTTPS_PORT/g" /etc/nginx/nginx.conf
|
sed -i "s/projecten 443/projecten $VIKUNJA_HTTPS_PORT/g" /etc/nginx/nginx.conf
|
||||||
|
|
||||||
# Set the uid and gid of the nginx run user
|
# Set the uid and gid of the nginx run user
|
||||||
usermod --non-unique --uid ${PUID} nginx
|
usermod --non-unique --uid ${PUID} nginx
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
const path = require('path')
|
|
||||||
const express = require('express')
|
|
||||||
const app = express()
|
|
||||||
|
|
||||||
const p = path.join(__dirname, '..', 'dist-dev')
|
|
||||||
const port = 4173
|
|
||||||
|
|
||||||
app.use(express.static(p))
|
|
||||||
// Handle urls set by the frontend
|
|
||||||
app.get('*', (request, response, next) => {
|
|
||||||
response.sendFile(`${p}/index.html`)
|
|
||||||
})
|
|
||||||
app.listen(port, '127.0.0.1', () => {
|
|
||||||
console.log(`Serving files from ${p}`)
|
|
||||||
console.log(`Server started on port ${port}`)
|
|
||||||
})
|
|
18
src/App.vue
18
src/App.vue
|
@ -15,9 +15,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {computed, watch, type Ref} from 'vue'
|
import {computed, watch} from 'vue'
|
||||||
import {useRouter} from 'vue-router'
|
import {useRoute, useRouter} from 'vue-router'
|
||||||
import {useRouteQuery} from '@vueuse/router'
|
|
||||||
import {useI18n} from 'vue-i18n'
|
import {useI18n} from 'vue-i18n'
|
||||||
import isTouchDevice from 'is-touch-device'
|
import isTouchDevice from 'is-touch-device'
|
||||||
import {success} from '@/message'
|
import {success} from '@/message'
|
||||||
|
@ -41,6 +40,7 @@ import {useAuthStore} from './stores/auth'
|
||||||
const baseStore = useBaseStore()
|
const baseStore = useBaseStore()
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
useBodyClass('is-touch', isTouchDevice())
|
useBodyClass('is-touch', isTouchDevice())
|
||||||
const keyboardShortcutsActive = computed(() => baseStore.keyboardShortcutsActive)
|
const keyboardShortcutsActive = computed(() => baseStore.keyboardShortcutsActive)
|
||||||
|
@ -51,9 +51,9 @@ const authLinkShare = computed(() => authStore.authLinkShare)
|
||||||
const {t} = useI18n({useScope: 'global'})
|
const {t} = useI18n({useScope: 'global'})
|
||||||
|
|
||||||
// setup account deletion verification
|
// setup account deletion verification
|
||||||
const accountDeletionConfirm = useRouteQuery('accountDeletionConfirm') as Ref<null | string>
|
const accountDeletionConfirm = computed(() => route.query?.accountDeletionConfirm as (string | undefined))
|
||||||
watch(accountDeletionConfirm, async (accountDeletionConfirm) => {
|
watch(accountDeletionConfirm, async (accountDeletionConfirm) => {
|
||||||
if (accountDeletionConfirm === null) {
|
if (accountDeletionConfirm === undefined) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,9 +64,9 @@ watch(accountDeletionConfirm, async (accountDeletionConfirm) => {
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
// setup password reset redirect
|
// setup password reset redirect
|
||||||
const userPasswordReset = useRouteQuery('userPasswordReset') as Ref<null | string>
|
const userPasswordReset = computed(() => route.query?.userPasswordReset as (string | undefined))
|
||||||
watch(userPasswordReset, (userPasswordReset) => {
|
watch(userPasswordReset, (userPasswordReset) => {
|
||||||
if (userPasswordReset === null) {
|
if (userPasswordReset === undefined) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,9 +75,9 @@ watch(userPasswordReset, (userPasswordReset) => {
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
// setup email verification redirect
|
// setup email verification redirect
|
||||||
const userEmailConfirm = useRouteQuery('userEmailConfirm') as Ref<null | string>
|
const userEmailConfirm = computed(() => route.query?.userEmailConfirm as (string | undefined))
|
||||||
watch(userEmailConfirm, (userEmailConfirm) => {
|
watch(userEmailConfirm, (userEmailConfirm) => {
|
||||||
if (userEmailConfirm === null) {
|
if (userEmailConfirm === undefined) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
BIN
src/assets/fonts/OpenSans-BoldItalic_3ff98862.woff2
Normal file
BIN
src/assets/fonts/OpenSans-BoldItalic_3ff98862.woff2
Normal file
Binary file not shown.
BIN
src/assets/fonts/OpenSans-Bold_eb52363b.woff2
Normal file
BIN
src/assets/fonts/OpenSans-Bold_eb52363b.woff2
Normal file
Binary file not shown.
BIN
src/assets/fonts/OpenSans-Italic[wght]_c9a8fe68.woff2
Normal file
BIN
src/assets/fonts/OpenSans-Italic[wght]_c9a8fe68.woff2
Normal file
Binary file not shown.
BIN
src/assets/fonts/OpenSans-RegularItalic_48244a7a.woff2
Normal file
BIN
src/assets/fonts/OpenSans-RegularItalic_48244a7a.woff2
Normal file
Binary file not shown.
BIN
src/assets/fonts/OpenSans-Regular_d0acb717.woff2
Normal file
BIN
src/assets/fonts/OpenSans-Regular_d0acb717.woff2
Normal file
Binary file not shown.
BIN
src/assets/fonts/OpenSans[wght]_54a65da5.woff2
Normal file
BIN
src/assets/fonts/OpenSans[wght]_54a65da5.woff2
Normal file
Binary file not shown.
BIN
src/assets/fonts/Quicksand-Bold_20b26f76.woff2
Normal file
BIN
src/assets/fonts/Quicksand-Bold_20b26f76.woff2
Normal file
Binary file not shown.
BIN
src/assets/fonts/Quicksand-Regular_3e913e7e.woff2
Normal file
BIN
src/assets/fonts/Quicksand-Regular_3e913e7e.woff2
Normal file
Binary file not shown.
BIN
src/assets/fonts/Quicksand-SemiBold_be48a442.woff2
Normal file
BIN
src/assets/fonts/Quicksand-SemiBold_be48a442.woff2
Normal file
Binary file not shown.
BIN
src/assets/fonts/Quicksand[wght]_87bdcc7f.woff2
Normal file
BIN
src/assets/fonts/Quicksand[wght]_87bdcc7f.woff2
Normal file
Binary file not shown.
58
src/components/base/BaseButton.story.vue
Normal file
58
src/components/base/BaseButton.story.vue
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {logEvent} from 'histoire/client'
|
||||||
|
import {reactive} from 'vue'
|
||||||
|
import {createRouter, createMemoryHistory} from 'vue-router'
|
||||||
|
import BaseButton from './BaseButton.vue'
|
||||||
|
|
||||||
|
function setupApp({ app }) {
|
||||||
|
// Router mock
|
||||||
|
app.use(createRouter({
|
||||||
|
history: createMemoryHistory(),
|
||||||
|
routes: [
|
||||||
|
{ path: '/', name: 'home', component: { render: () => null } },
|
||||||
|
],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
disabled: false,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Story :setup-app="setupApp" :layout="{ type: 'grid', width: '200px' }">
|
||||||
|
<Variant title="custom">
|
||||||
|
<template #controls>
|
||||||
|
<HstCheckbox v-model="state.disabled" title="Disabled" />
|
||||||
|
</template>
|
||||||
|
<BaseButton :disabled="state.disabled">
|
||||||
|
Hello!
|
||||||
|
</BaseButton>
|
||||||
|
</Variant>
|
||||||
|
|
||||||
|
<Variant title="disabled">
|
||||||
|
<BaseButton disabled>
|
||||||
|
Hello!
|
||||||
|
</BaseButton>
|
||||||
|
</Variant>
|
||||||
|
|
||||||
|
<Variant title="router link">
|
||||||
|
<BaseButton :to="'home'">
|
||||||
|
Hello!
|
||||||
|
</BaseButton>
|
||||||
|
</Variant>
|
||||||
|
|
||||||
|
<Variant title="external link">
|
||||||
|
<BaseButton href="https://vikunja.io">
|
||||||
|
Hello!
|
||||||
|
</BaseButton>
|
||||||
|
</Variant>
|
||||||
|
|
||||||
|
<Variant title="button">
|
||||||
|
<BaseButton @click="logEvent('Click', $event)">
|
||||||
|
Hello!
|
||||||
|
</BaseButton>
|
||||||
|
</Variant>
|
||||||
|
</Story>
|
||||||
|
</template>
|
|
@ -61,12 +61,12 @@ export type BaseButtonTypes = typeof BASE_BUTTON_TYPES_MAP[keyof typeof BASE_BUT
|
||||||
|
|
||||||
import {unrefElement} from '@vueuse/core'
|
import {unrefElement} from '@vueuse/core'
|
||||||
import {ref, type HTMLAttributes} from 'vue'
|
import {ref, type HTMLAttributes} from 'vue'
|
||||||
import type {RouteLocationNamedRaw} from 'vue-router'
|
import type {RouteLocationRaw} from 'vue-router'
|
||||||
|
|
||||||
export interface BaseButtonProps extends HTMLAttributes {
|
export interface BaseButtonProps extends HTMLAttributes {
|
||||||
type?: BaseButtonTypes
|
type?: BaseButtonTypes
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
to?: RouteLocationNamedRaw
|
to?: RouteLocationRaw
|
||||||
href?: string
|
href?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
179
src/components/base/Expandable.vue
Normal file
179
src/components/base/Expandable.vue
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
<template>
|
||||||
|
<transition
|
||||||
|
name="expandable-slide"
|
||||||
|
@before-enter="beforeEnter"
|
||||||
|
@enter="enter"
|
||||||
|
@after-enter="afterEnter"
|
||||||
|
@enter-cancelled="enterCancelled"
|
||||||
|
@before-leave="beforeLeave"
|
||||||
|
@leave="leave"
|
||||||
|
@after-leave="afterLeave"
|
||||||
|
@leave-cancelled="leaveCancelled"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="initialHeight"
|
||||||
|
class="expandable-initial-height"
|
||||||
|
:style="{ maxHeight: `${initialHeight}px` }"
|
||||||
|
:class="{ 'expandable-initial-height--expanded': open }"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
<div v-else-if="open" class="expandable">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// the logic of this component is loosly based on this article
|
||||||
|
// https://gomakethings.com/how-to-add-transition-animations-to-vanilla-javascript-show-and-hide-methods/#putting-it-all-together
|
||||||
|
|
||||||
|
import {computed, ref} from 'vue'
|
||||||
|
import {getInheritedBackgroundColor} from '@/helpers/getInheritedBackgroundColor'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
/** Wheather the Expandable is open or not */
|
||||||
|
open: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
/** If there is too much content, content will be cut of here. */
|
||||||
|
initialHeight: {
|
||||||
|
type: Number,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
/** The hidden content is indicated by a gradient. This is the color that the gradient fades to.
|
||||||
|
* Makes only sense if `initialHeight` is set. */
|
||||||
|
backgroundColor: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const wrapper = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
const computedBackgroundColor = computed(() => {
|
||||||
|
if (wrapper.value === null) {
|
||||||
|
return props.backgroundColor || '#fff'
|
||||||
|
}
|
||||||
|
return props.backgroundColor || getInheritedBackgroundColor(wrapper.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the natural height of the element
|
||||||
|
*/
|
||||||
|
function getHeight(el: HTMLElement) {
|
||||||
|
const { display } = el.style // save display property
|
||||||
|
el.style.display = 'block' // Make it visible
|
||||||
|
const height = `${el.scrollHeight}px` // Get its height
|
||||||
|
el.style.display = display // revert to original display property
|
||||||
|
return height
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* force layout of element changes
|
||||||
|
* https://gist.github.com/paulirish/5d52fb081b3570c81e3a
|
||||||
|
*/
|
||||||
|
function forceLayout(el: HTMLElement) {
|
||||||
|
el.offsetTop
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ######################################################################
|
||||||
|
# The following functions are called by the js hooks of the transitions.
|
||||||
|
# They follow the orignal hook order of the vue transition component
|
||||||
|
# see: https://vuejs.org/guide/built-ins/transition.html#javascript-hooks
|
||||||
|
###################################################################### */
|
||||||
|
|
||||||
|
function beforeEnter(el: HTMLElement) {
|
||||||
|
el.style.height = '0'
|
||||||
|
el.style.willChange = 'height'
|
||||||
|
el.style.backfaceVisibility = 'hidden'
|
||||||
|
forceLayout(el)
|
||||||
|
}
|
||||||
|
|
||||||
|
// the done callback is optional when
|
||||||
|
// used in combination with CSS
|
||||||
|
function enter(el: HTMLElement) {
|
||||||
|
const height = getHeight(el) // Get the natural height
|
||||||
|
el.style.height = height // Update the height
|
||||||
|
}
|
||||||
|
|
||||||
|
function afterEnter(el: HTMLElement) {
|
||||||
|
removeHeight(el)
|
||||||
|
}
|
||||||
|
|
||||||
|
function enterCancelled(el: HTMLElement) {
|
||||||
|
removeHeight(el)
|
||||||
|
}
|
||||||
|
|
||||||
|
function beforeLeave(el: HTMLElement) {
|
||||||
|
// Give the element a height to change from
|
||||||
|
el.style.height = `${el.scrollHeight}px`
|
||||||
|
forceLayout(el)
|
||||||
|
}
|
||||||
|
|
||||||
|
function leave(el: HTMLElement) {
|
||||||
|
// Set the height back to 0
|
||||||
|
el.style.height = '0'
|
||||||
|
el.style.willChange = ''
|
||||||
|
el.style.backfaceVisibility = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function afterLeave(el: HTMLElement) {
|
||||||
|
removeHeight(el)
|
||||||
|
}
|
||||||
|
|
||||||
|
function leaveCancelled(el: HTMLElement) {
|
||||||
|
removeHeight(el)
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeHeight(el: HTMLElement) {
|
||||||
|
el.style.height = ''
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
$transition-time: 300ms;
|
||||||
|
|
||||||
|
.expandable-slide-enter-active,
|
||||||
|
.expandable-slide-leave-active {
|
||||||
|
transition:
|
||||||
|
opacity $transition-time ease-in-quint,
|
||||||
|
height $transition-time ease-in-out-quint;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandable-slide-enter,
|
||||||
|
.expandable-slide-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandable-initial-height {
|
||||||
|
padding: 5px;
|
||||||
|
margin: -5px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
background-image: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
transparent,
|
||||||
|
ease-in-out
|
||||||
|
v-bind(computedBackgroundColor)
|
||||||
|
);
|
||||||
|
position: absolute;
|
||||||
|
height: 40px;
|
||||||
|
width: 100%;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandable-initial-height--expanded {
|
||||||
|
height: 100% !important;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -54,8 +54,8 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<modal
|
<modal
|
||||||
@close="() => showHowItWorks = false"
|
|
||||||
:enabled="showHowItWorks"
|
:enabled="showHowItWorks"
|
||||||
|
@close="() => showHowItWorks = false"
|
||||||
transition-name="fade"
|
transition-name="fade"
|
||||||
:overflow="true"
|
:overflow="true"
|
||||||
variant="hint-modal"
|
variant="hint-modal"
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {computed} from 'vue'
|
import { computed } from 'vue'
|
||||||
import {useNow} from '@vueuse/core'
|
import { useNow } from '@vueuse/core'
|
||||||
|
|
||||||
import LogoFull from '@/assets/logo-full.svg?url'
|
import LogoFull from '@/assets/logo-full.svg?component'
|
||||||
import LogoFullPride from '@/assets/logo-full-pride.svg?url'
|
import LogoFullPride from '@/assets/logo-full-pride.svg?component'
|
||||||
|
|
||||||
const now = useNow()
|
const now = useNow()
|
||||||
const logoUrl = computed(() => now.value.getMonth() === 5 ? LogoFullPride : LogoFull)
|
const Logo = computed(() => now.value.getMonth() === 5 ? LogoFullPride : LogoFull)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<img alt="Vikunja" :src="logoUrl" class="logo" />
|
<Logo alt="Vikunja" class="logo" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -8,19 +8,19 @@
|
||||||
<Logo width="164" height="48"/>
|
<Logo width="164" height="48"/>
|
||||||
</router-link>
|
</router-link>
|
||||||
<MenuButton class="menu-button"/>
|
<MenuButton class="menu-button"/>
|
||||||
<div class="list-title" ref="listTitle" v-show="currentList.id">
|
<div class="project-title" ref="projectTitle" v-show="currentProject.id">
|
||||||
<template v-if="currentList.id">
|
<template v-if="currentProject.id">
|
||||||
<h1
|
<h1
|
||||||
:style="{ 'opacity': currentList.title === '' ? '0': '1' }"
|
:style="{ 'opacity': currentProject.title === '' ? '0': '1' }"
|
||||||
class="title">
|
class="title">
|
||||||
{{ currentList.title === '' ? $t('misc.loading') : getListTitle(currentList) }}
|
{{ currentProject.title === '' ? $t('misc.loading') : getProjectTitle(currentProject) }}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<BaseButton :to="{name: 'list.info', params: {listId: currentList.id}}" class="info-button">
|
<BaseButton :to="{name: 'project.info', params: {projectId: currentProject.id}}" class="info-button">
|
||||||
<icon icon="circle-info"/>
|
<icon icon="circle-info"/>
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
|
|
||||||
<list-settings-dropdown v-if="canWriteCurrentList && currentList.id !== -1" :list="currentList"/>
|
<project-settings-dropdown v-if="canWriteCurrentProject && currentProject.id !== -1" :project="currentProject"/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -96,7 +96,7 @@ import {ref, computed, onMounted, nextTick} from 'vue'
|
||||||
import {RIGHTS as Rights} from '@/constants/rights'
|
import {RIGHTS as Rights} from '@/constants/rights'
|
||||||
|
|
||||||
import Update from '@/components/home/update.vue'
|
import Update from '@/components/home/update.vue'
|
||||||
import ListSettingsDropdown from '@/components/list/list-settings-dropdown.vue'
|
import ProjectSettingsDropdown from '@/components/project/project-settings-dropdown.vue'
|
||||||
import Dropdown from '@/components/misc/dropdown.vue'
|
import Dropdown from '@/components/misc/dropdown.vue'
|
||||||
import DropdownItem from '@/components/misc/dropdown-item.vue'
|
import DropdownItem from '@/components/misc/dropdown-item.vue'
|
||||||
import Notifications from '@/components/notifications/notifications.vue'
|
import Notifications from '@/components/notifications/notifications.vue'
|
||||||
|
@ -104,16 +104,16 @@ import Logo from '@/components/home/Logo.vue'
|
||||||
import BaseButton from '@/components/base/BaseButton.vue'
|
import BaseButton from '@/components/base/BaseButton.vue'
|
||||||
import MenuButton from '@/components/home/MenuButton.vue'
|
import MenuButton from '@/components/home/MenuButton.vue'
|
||||||
|
|
||||||
import {getListTitle} from '@/helpers/getListTitle'
|
import {getProjectTitle} from '@/helpers/getProjectTitle'
|
||||||
|
|
||||||
import {useBaseStore} from '@/stores/base'
|
import {useBaseStore} from '@/stores/base'
|
||||||
import {useConfigStore} from '@/stores/config'
|
import {useConfigStore} from '@/stores/config'
|
||||||
import {useAuthStore} from '@/stores/auth'
|
import {useAuthStore} from '@/stores/auth'
|
||||||
|
|
||||||
const baseStore = useBaseStore()
|
const baseStore = useBaseStore()
|
||||||
const currentList = computed(() => baseStore.currentList)
|
const currentProject = computed(() => baseStore.currentProject)
|
||||||
const background = computed(() => baseStore.background)
|
const background = computed(() => baseStore.background)
|
||||||
const canWriteCurrentList = computed(() => baseStore.currentList.maxRight > Rights.READ)
|
const canWriteCurrentProject = computed(() => baseStore.currentProject.maxRight > Rights.READ)
|
||||||
const menuActive = computed(() => baseStore.menuActive)
|
const menuActive = computed(() => baseStore.menuActive)
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
@ -123,15 +123,15 @@ const imprintUrl = computed(() => configStore.legal.imprintUrl)
|
||||||
const privacyPolicyUrl = computed(() => configStore.legal.privacyPolicyUrl)
|
const privacyPolicyUrl = computed(() => configStore.legal.privacyPolicyUrl)
|
||||||
|
|
||||||
const usernameDropdown = ref()
|
const usernameDropdown = ref()
|
||||||
const listTitle = ref()
|
const projectTitle = ref()
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
if (typeof usernameDropdown.value === 'undefined' || typeof listTitle.value === 'undefined') {
|
if (typeof usernameDropdown.value === 'undefined' || typeof projectTitle.value === 'undefined') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const usernameWidth = usernameDropdown.value.$el.clientWidth
|
const usernameWidth = usernameDropdown.value.$el.clientWidth
|
||||||
listTitle.value.style.setProperty('--nav-username-width', `${usernameWidth}px`)
|
projectTitle.value.style.setProperty('--nav-username-width', `${usernameWidth}px`)
|
||||||
})
|
})
|
||||||
|
|
||||||
function openQuickActions() {
|
function openQuickActions() {
|
||||||
|
@ -259,7 +259,7 @@ $hamburger-menu-icon-width: 28px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-title {
|
.project-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
|
@ -33,13 +33,13 @@
|
||||||
<quick-actions/>
|
<quick-actions/>
|
||||||
|
|
||||||
<router-view :route="routeWithModal" v-slot="{ Component }">
|
<router-view :route="routeWithModal" v-slot="{ Component }">
|
||||||
<keep-alive :include="['list.list', 'list.gantt', 'list.table', 'list.kanban']">
|
<keep-alive :include="['project.list', 'project.gantt', 'project.table', 'project.kanban']">
|
||||||
<component :is="Component"/>
|
<component :is="Component"/>
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
</router-view>
|
</router-view>
|
||||||
|
|
||||||
<modal
|
<modal
|
||||||
v-if="currentModal"
|
:enabled="Boolean(currentModal)"
|
||||||
@close="closeModal()"
|
@close="closeModal()"
|
||||||
variant="scrolling"
|
variant="scrolling"
|
||||||
class="task-detail-view-modal"
|
class="task-detail-view-modal"
|
||||||
|
@ -90,7 +90,7 @@ const route = useRoute()
|
||||||
watch(() => route.fullPath, () => window.innerWidth < 769 && baseStore.setMenuActive(false))
|
watch(() => route.fullPath, () => window.innerWidth < 769 && baseStore.setMenuActive(false))
|
||||||
|
|
||||||
// FIXME: this is really error prone
|
// FIXME: this is really error prone
|
||||||
// Reset the current list highlight in menu if the current route is not list related.
|
// Reset the current project highlight in menu if the current route is not project related.
|
||||||
watch(() => route.name as string, (routeName) => {
|
watch(() => route.name as string, (routeName) => {
|
||||||
if (
|
if (
|
||||||
routeName &&
|
routeName &&
|
||||||
|
@ -109,7 +109,7 @@ watch(() => route.name as string, (routeName) => {
|
||||||
routeName.startsWith('user.settings')
|
routeName.startsWith('user.settings')
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
baseStore.handleSetCurrentList({list: null})
|
baseStore.handleSetCurrentProject({project: null})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -159,27 +159,20 @@ labelStore.loadAllLabels()
|
||||||
.app-content {
|
.app-content {
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-top: 1rem;
|
padding: 1.5rem 0.5rem 1rem;
|
||||||
|
|
||||||
@media screen {
|
|
||||||
padding: $navbar-height + 1.5rem 1.5rem 1rem 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: $tablet) {
|
@media screen and (max-width: $tablet) {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
padding-top: 1.5rem;
|
|
||||||
min-height: calc(100vh - 4rem);
|
min-height: calc(100vh - 4rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: $tablet) {
|
||||||
|
padding: $navbar-height + 1.5rem 1.5rem 1rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen {
|
&.is-menu-enabled {
|
||||||
&.is-menu-enabled {
|
@media screen and (min-width: $tablet) {
|
||||||
margin-left: $navbar-width;
|
margin-left: $navbar-width;
|
||||||
|
|
||||||
@media screen and (max-width: $tablet) {
|
|
||||||
min-width: 100%;
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -237,6 +230,4 @@ labelStore.loadAllLabels()
|
||||||
.content-auth.z-unset {
|
.content-auth.z-unset {
|
||||||
z-index: unset;
|
z-index: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include modal-transition();
|
|
||||||
</style>
|
</style>
|
|
@ -9,9 +9,9 @@
|
||||||
<Logo class="logo" v-if="logoVisible"/>
|
<Logo class="logo" v-if="logoVisible"/>
|
||||||
<h1
|
<h1
|
||||||
:class="{'m-0': !logoVisible}"
|
:class="{'m-0': !logoVisible}"
|
||||||
:style="{ 'opacity': currentList.title === '' ? '0': '1' }"
|
:style="{ 'opacity': currentProject.title === '' ? '0': '1' }"
|
||||||
class="title">
|
class="title">
|
||||||
{{ currentList.title === '' ? $t('misc.loading') : currentList.title }}
|
{{ currentProject.title === '' ? $t('misc.loading') : currentProject.title }}
|
||||||
</h1>
|
</h1>
|
||||||
<div class="box has-text-left view">
|
<div class="box has-text-left view">
|
||||||
<router-view/>
|
<router-view/>
|
||||||
|
@ -31,7 +31,7 @@ import Logo from '@/components/home/Logo.vue'
|
||||||
import PoweredByLink from './PoweredByLink.vue'
|
import PoweredByLink from './PoweredByLink.vue'
|
||||||
|
|
||||||
const baseStore = useBaseStore()
|
const baseStore = useBaseStore()
|
||||||
const currentList = computed(() => baseStore.currentList)
|
const currentProject = computed(() => baseStore.currentProject)
|
||||||
const background = computed(() => baseStore.background)
|
const background = computed(() => baseStore.background)
|
||||||
const logoVisible = computed(() => baseStore.logoVisible)
|
const logoVisible = computed(() => baseStore.logoVisible)
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -4,10 +4,10 @@
|
||||||
<router-link :to="{name: 'home'}" class="logo">
|
<router-link :to="{name: 'home'}" class="logo">
|
||||||
<Logo width="164" height="48"/>
|
<Logo width="164" height="48"/>
|
||||||
</router-link>
|
</router-link>
|
||||||
<ul class="menu-list">
|
<ul class="menu-project">
|
||||||
<li>
|
<li>
|
||||||
<router-link :to="{ name: 'home'}" v-shortcut="'g o'">
|
<router-link :to="{ name: 'home'}" v-shortcut="'g o'">
|
||||||
<span class="icon">
|
<span class="menu-item-icon icon">
|
||||||
<icon icon="calendar"/>
|
<icon icon="calendar"/>
|
||||||
</span>
|
</span>
|
||||||
{{ $t('navigation.overview') }}
|
{{ $t('navigation.overview') }}
|
||||||
|
@ -15,7 +15,7 @@
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<router-link :to="{ name: 'tasks.range'}" v-shortcut="'g u'">
|
<router-link :to="{ name: 'tasks.range'}" v-shortcut="'g u'">
|
||||||
<span class="icon">
|
<span class="menu-item-icon icon">
|
||||||
<icon :icon="['far', 'calendar-alt']"/>
|
<icon :icon="['far', 'calendar-alt']"/>
|
||||||
</span>
|
</span>
|
||||||
{{ $t('navigation.upcoming') }}
|
{{ $t('navigation.upcoming') }}
|
||||||
|
@ -23,7 +23,7 @@
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<router-link :to="{ name: 'namespaces.index'}" v-shortcut="'g n'">
|
<router-link :to="{ name: 'namespaces.index'}" v-shortcut="'g n'">
|
||||||
<span class="icon">
|
<span class="menu-item-icon icon">
|
||||||
<icon icon="layer-group"/>
|
<icon icon="layer-group"/>
|
||||||
</span>
|
</span>
|
||||||
{{ $t('namespace.title') }}
|
{{ $t('namespace.title') }}
|
||||||
|
@ -31,7 +31,7 @@
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<router-link :to="{ name: 'labels.index'}" v-shortcut="'g a'">
|
<router-link :to="{ name: 'labels.index'}" v-shortcut="'g a'">
|
||||||
<span class="icon">
|
<span class="menu-item-icon icon">
|
||||||
<icon icon="tags"/>
|
<icon icon="tags"/>
|
||||||
</span>
|
</span>
|
||||||
{{ $t('label.title') }}
|
{{ $t('label.title') }}
|
||||||
|
@ -39,7 +39,7 @@
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<router-link :to="{ name: 'teams.index'}" v-shortcut="'g m'">
|
<router-link :to="{ name: 'teams.index'}" v-shortcut="'g m'">
|
||||||
<span class="icon">
|
<span class="menu-item-icon icon">
|
||||||
<icon icon="users"/>
|
<icon icon="users"/>
|
||||||
</span>
|
</span>
|
||||||
{{ $t('team.title') }}
|
{{ $t('team.title') }}
|
||||||
|
@ -48,11 +48,11 @@
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<nav class="menu namespaces-lists loader-container is-loading-small" :class="{'is-loading': loading}">
|
<nav class="menu namespaces-projects loader-container is-loading-small" :class="{'is-loading': loading}">
|
||||||
<template v-for="(n, nk) in namespaces" :key="n.id">
|
<template v-for="(n, nk) in namespaces" :key="n.id">
|
||||||
<div class="namespace-title" :class="{'has-menu': n.id > 0}">
|
<div class="namespace-title" :class="{'has-menu': n.id > 0}">
|
||||||
<BaseButton
|
<BaseButton
|
||||||
@click="toggleLists(n.id)"
|
@click="toggleProjects(n.id)"
|
||||||
class="menu-label"
|
class="menu-label"
|
||||||
v-tooltip="namespaceTitles[nk]"
|
v-tooltip="namespaceTitles[nk]"
|
||||||
>
|
>
|
||||||
|
@ -63,29 +63,29 @@
|
||||||
/>
|
/>
|
||||||
<span class="name">{{ namespaceTitles[nk] }}</span>
|
<span class="name">{{ namespaceTitles[nk] }}</span>
|
||||||
<div
|
<div
|
||||||
class="icon is-small toggle-lists-icon pl-2"
|
class="icon menu-item-icon is-small toggle-projects-icon pl-2"
|
||||||
:class="{'active': typeof listsVisible[n.id] !== 'undefined' ? listsVisible[n.id] : true}"
|
:class="{'active': typeof projectsVisible[n.id] !== 'undefined' ? projectsVisible[n.id] : true}"
|
||||||
>
|
>
|
||||||
<icon icon="chevron-down"/>
|
<icon icon="chevron-down"/>
|
||||||
</div>
|
</div>
|
||||||
<span class="count" :class="{'ml-2 mr-0': n.id > 0}">
|
<span class="count" :class="{'ml-2 mr-0': n.id > 0}">
|
||||||
({{ namespaceListsCount[nk] }})
|
({{ namespaceProjectsCount[nk] }})
|
||||||
</span>
|
</span>
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
<namespace-settings-dropdown :namespace="n" v-if="n.id > 0"/>
|
<namespace-settings-dropdown class="menu-list-dropdown" :namespace="n" v-if="n.id > 0"/>
|
||||||
</div>
|
</div>
|
||||||
<!--
|
<!--
|
||||||
NOTE: a v-model / computed setter is not possible, since the updateActiveLists function
|
NOTE: a v-model / computed setter is not possible, since the updateActiveProjects function
|
||||||
triggered by the change needs to have access to the current namespace
|
triggered by the change needs to have access to the current namespace
|
||||||
-->
|
-->
|
||||||
<draggable
|
<draggable
|
||||||
v-if="listsVisible[n.id] ?? true"
|
v-if="projectsVisible[n.id] ?? true"
|
||||||
v-bind="dragOptions"
|
v-bind="dragOptions"
|
||||||
:modelValue="activeLists[nk]"
|
:modelValue="activeProjects[nk]"
|
||||||
@update:modelValue="(lists) => updateActiveLists(n, lists)"
|
@update:modelValue="(projects) => updateActiveProjects(n, projects)"
|
||||||
group="namespace-lists"
|
group="namespace-projects"
|
||||||
@start="() => drag = true"
|
@start="() => drag = true"
|
||||||
@end="saveListPosition"
|
@end="saveProjectPosition"
|
||||||
handle=".handle"
|
handle=".handle"
|
||||||
:disabled="n.id < 0 || undefined"
|
:disabled="n.id < 0 || undefined"
|
||||||
tag="ul"
|
tag="ul"
|
||||||
|
@ -94,42 +94,48 @@
|
||||||
:data-namespace-index="nk"
|
:data-namespace-index="nk"
|
||||||
:component-data="{
|
:component-data="{
|
||||||
type: 'transition-group',
|
type: 'transition-group',
|
||||||
name: !drag ? 'flip-list' : null,
|
name: !drag ? 'flip-project' : null,
|
||||||
class: [
|
class: [
|
||||||
'menu-list can-be-hidden',
|
'menu-project can-be-hidden',
|
||||||
{ 'dragging-disabled': n.id < 0 }
|
{ 'dragging-disabled': n.id < 0 }
|
||||||
]
|
]
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<template #item="{element: l}">
|
<template #item="{element: l}">
|
||||||
<li
|
<li
|
||||||
class="list-menu loader-container is-loading-small"
|
class="project-menu loader-container is-loading-small"
|
||||||
:class="{'is-loading': listUpdating[l.id]}"
|
:class="{'is-loading': projectUpdating[l.id]}"
|
||||||
>
|
>
|
||||||
<BaseButton
|
<BaseButton
|
||||||
:to="{ name: 'list.index', params: { listId: l.id} }"
|
:to="{ name: 'project.index', params: { projectId: l.id} }"
|
||||||
class="list-menu-link"
|
class="project-menu-link"
|
||||||
:class="{'router-link-exact-active': currentList.id === l.id}"
|
:class="{'router-link-exact-active': currentProject.id === l.id}"
|
||||||
>
|
>
|
||||||
<span class="icon handle">
|
<span class="icon menu-item-icon handle">
|
||||||
<icon icon="grip-lines"/>
|
<icon icon="grip-lines"/>
|
||||||
</span>
|
</span>
|
||||||
<ColorBubble
|
<ColorBubble
|
||||||
v-if="l.hexColor !== ''"
|
v-if="l.hexColor !== ''"
|
||||||
:color="l.hexColor"
|
:color="l.hexColor"
|
||||||
class="mr-1"
|
class="mr-1"
|
||||||
/>
|
/>
|
||||||
<span class="list-menu-title">{{ getListTitle(l) }}</span>
|
<span class="project-menu-title">{{ getProjectTitle(l) }}</span>
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
<BaseButton
|
<BaseButton
|
||||||
class="favorite"
|
class="favorite"
|
||||||
:class="{'is-favorite': l.isFavorite}"
|
:class="{'is-favorite': l.isFavorite}"
|
||||||
@click="listStore.toggleListFavorite(l)"
|
@click="projectStore.toggleProjectFavorite(l)"
|
||||||
>
|
>
|
||||||
<icon :icon="l.isFavorite ? 'star' : ['far', 'star']"/>
|
<icon :icon="l.isFavorite ? 'star' : ['far', 'star']"/>
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
<list-settings-dropdown :list="l" v-if="l.id > 0"/>
|
<project-settings-dropdown class="menu-list-dropdown" :project="l" v-if="l.id > 0">
|
||||||
<span class="list-setting-spacer" v-else></span>
|
<template #trigger="{toggleOpen}">
|
||||||
|
<BaseButton class="menu-list-dropdown-trigger" @click="toggleOpen">
|
||||||
|
<icon icon="ellipsis-h" class="icon"/>
|
||||||
|
</BaseButton>
|
||||||
|
</template>
|
||||||
|
</list-settings-dropdown>
|
||||||
|
<span class="project-setting-spacer" v-else></span>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
</draggable>
|
</draggable>
|
||||||
|
@ -145,21 +151,21 @@ import draggable from 'zhyswan-vuedraggable'
|
||||||
import type {SortableEvent} from 'sortablejs'
|
import type {SortableEvent} from 'sortablejs'
|
||||||
|
|
||||||
import BaseButton from '@/components/base/BaseButton.vue'
|
import BaseButton from '@/components/base/BaseButton.vue'
|
||||||
import ListSettingsDropdown from '@/components/list/list-settings-dropdown.vue'
|
import ProjectSettingsDropdown from '@/components/project/project-settings-dropdown.vue'
|
||||||
import NamespaceSettingsDropdown from '@/components/namespace/namespace-settings-dropdown.vue'
|
import NamespaceSettingsDropdown from '@/components/namespace/namespace-settings-dropdown.vue'
|
||||||
import PoweredByLink from '@/components/home/PoweredByLink.vue'
|
import PoweredByLink from '@/components/home/PoweredByLink.vue'
|
||||||
import Logo from '@/components/home/Logo.vue'
|
import Logo from '@/components/home/Logo.vue'
|
||||||
|
|
||||||
import {calculateItemPosition} from '@/helpers/calculateItemPosition'
|
import {calculateItemPosition} from '@/helpers/calculateItemPosition'
|
||||||
import {getNamespaceTitle} from '@/helpers/getNamespaceTitle'
|
import {getNamespaceTitle} from '@/helpers/getNamespaceTitle'
|
||||||
import {getListTitle} from '@/helpers/getListTitle'
|
import {getProjectTitle} from '@/helpers/getProjectTitle'
|
||||||
import {useEventListener} from '@vueuse/core'
|
import {useEventListener} from '@vueuse/core'
|
||||||
import type {IList} from '@/modelTypes/IList'
|
import type {IProject} from '@/modelTypes/IProject'
|
||||||
import type {INamespace} from '@/modelTypes/INamespace'
|
import type {INamespace} from '@/modelTypes/INamespace'
|
||||||
import ColorBubble from '@/components/misc/colorBubble.vue'
|
import ColorBubble from '@/components/misc/colorBubble.vue'
|
||||||
|
|
||||||
import {useBaseStore} from '@/stores/base'
|
import {useBaseStore} from '@/stores/base'
|
||||||
import {useListStore} from '@/stores/lists'
|
import {useProjectStore} from '@/stores/projects'
|
||||||
import {useNamespaceStore} from '@/stores/namespaces'
|
import {useNamespaceStore} from '@/stores/namespaces'
|
||||||
|
|
||||||
const drag = ref(false)
|
const drag = ref(false)
|
||||||
|
@ -170,7 +176,7 @@ const dragOptions = {
|
||||||
|
|
||||||
const baseStore = useBaseStore()
|
const baseStore = useBaseStore()
|
||||||
const namespaceStore = useNamespaceStore()
|
const namespaceStore = useNamespaceStore()
|
||||||
const currentList = computed(() => baseStore.currentList)
|
const currentProject = computed(() => baseStore.currentProject)
|
||||||
const menuActive = computed(() => baseStore.menuActive)
|
const menuActive = computed(() => baseStore.menuActive)
|
||||||
const loading = computed(() => namespaceStore.isLoading)
|
const loading = computed(() => namespaceStore.isLoading)
|
||||||
|
|
||||||
|
@ -178,9 +184,9 @@ const loading = computed(() => namespaceStore.isLoading)
|
||||||
const namespaces = computed(() => {
|
const namespaces = computed(() => {
|
||||||
return namespaceStore.namespaces.filter(n => !n.isArchived)
|
return namespaceStore.namespaces.filter(n => !n.isArchived)
|
||||||
})
|
})
|
||||||
const activeLists = computed(() => {
|
const activeProjects = computed(() => {
|
||||||
return namespaces.value.map(({lists}) => {
|
return namespaces.value.map(({projects}) => {
|
||||||
return lists?.filter(item => {
|
return projects?.filter(item => {
|
||||||
return typeof item !== 'undefined' && !item.isArchived
|
return typeof item !== 'undefined' && !item.isArchived
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -190,86 +196,86 @@ const namespaceTitles = computed(() => {
|
||||||
return namespaces.value.map((namespace) => getNamespaceTitle(namespace))
|
return namespaces.value.map((namespace) => getNamespaceTitle(namespace))
|
||||||
})
|
})
|
||||||
|
|
||||||
const namespaceListsCount = computed(() => {
|
const namespaceProjectsCount = computed(() => {
|
||||||
return namespaces.value.map((_, index) => activeLists.value[index]?.length ?? 0)
|
return namespaces.value.map((_, index) => activeProjects.value[index]?.length ?? 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
useEventListener('resize', resize)
|
useEventListener('resize', resize)
|
||||||
onMounted(() => resize())
|
onMounted(() => resize())
|
||||||
|
|
||||||
const listStore = useListStore()
|
const projectStore = useProjectStore()
|
||||||
|
|
||||||
function resize() {
|
function resize() {
|
||||||
// Hide the menu by default on mobile
|
// Hide the menu by default on mobile
|
||||||
baseStore.setMenuActive(window.innerWidth >= 770)
|
baseStore.setMenuActive(window.innerWidth >= 770)
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleLists(namespaceId: INamespace['id']) {
|
function toggleProjects(namespaceId: INamespace['id']) {
|
||||||
listsVisible.value[namespaceId] = !listsVisible.value[namespaceId]
|
projectsVisible.value[namespaceId] = !projectsVisible.value[namespaceId]
|
||||||
}
|
}
|
||||||
|
|
||||||
const listsVisible = ref<{ [id: INamespace['id']]: boolean }>({})
|
const projectsVisible = ref<{ [id: INamespace['id']]: boolean }>({})
|
||||||
// FIXME: async action will be unfinished when component mounts
|
// FIXME: async action will be unfinished when component mounts
|
||||||
onBeforeMount(async () => {
|
onBeforeMount(async () => {
|
||||||
const namespaces = await namespaceStore.loadNamespaces()
|
const namespaces = await namespaceStore.loadNamespaces()
|
||||||
namespaces.forEach(n => {
|
namespaces.forEach(n => {
|
||||||
if (typeof listsVisible.value[n.id] === 'undefined') {
|
if (typeof projectsVisible.value[n.id] === 'undefined') {
|
||||||
listsVisible.value[n.id] = true
|
projectsVisible.value[n.id] = true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
function updateActiveLists(namespace: INamespace, activeLists: IList[]) {
|
function updateActiveProjects(namespace: INamespace, activeProjects: IProject[]) {
|
||||||
// This is a bit hacky: since we do have to filter out the archived items from the list
|
// This is a bit hacky: since we do have to filter out the archived items from the project
|
||||||
// for vue draggable updating it is not as simple as replacing it.
|
// for vue draggable updating it is not as simple as replacing it.
|
||||||
// To work around this, we merge the active lists with the archived ones. Doing so breaks the order
|
// To work around this, we merge the active projects with the archived ones. Doing so breaks the order
|
||||||
// because now all archived lists are sorted after the active ones. This is fine because they are sorted
|
// because now all archived projects are sorted after the active ones. This is fine because they are sorted
|
||||||
// later when showing them anyway, and it makes the merging happening here a lot easier.
|
// later when showing them anyway, and it makes the merging happening here a lot easier.
|
||||||
const lists = [
|
const projects = [
|
||||||
...activeLists,
|
...activeProjects,
|
||||||
...namespace.lists.filter(l => l.isArchived),
|
...namespace.projects.filter(l => l.isArchived),
|
||||||
]
|
]
|
||||||
|
|
||||||
namespaceStore.setNamespaceById({
|
namespaceStore.setNamespaceById({
|
||||||
...namespace,
|
...namespace,
|
||||||
lists,
|
projects,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const listUpdating = ref<{ [id: INamespace['id']]: boolean }>({})
|
const projectUpdating = ref<{ [id: INamespace['id']]: boolean }>({})
|
||||||
|
|
||||||
async function saveListPosition(e: SortableEvent) {
|
async function saveProjectPosition(e: SortableEvent) {
|
||||||
if (!e.newIndex && e.newIndex !== 0) return
|
if (!e.newIndex && e.newIndex !== 0) return
|
||||||
|
|
||||||
const namespaceId = parseInt(e.to.dataset.namespaceId as string)
|
const namespaceId = parseInt(e.to.dataset.namespaceId as string)
|
||||||
const newNamespaceIndex = parseInt(e.to.dataset.namespaceIndex as string)
|
const newNamespaceIndex = parseInt(e.to.dataset.namespaceIndex as string)
|
||||||
|
|
||||||
const listsActive = activeLists.value[newNamespaceIndex]
|
const projectsActive = activeProjects.value[newNamespaceIndex]
|
||||||
// If the list was dragged to the last position, Safari will report e.newIndex as the size of the listsActive
|
// If the project was dragged to the last position, Safari will report e.newIndex as the size of the projectsActive
|
||||||
// array instead of using the position. Because the index is wrong in that case, dragging the list will fail.
|
// array instead of using the position. Because the index is wrong in that case, dragging the project will fail.
|
||||||
// To work around that we're explicitly checking that case here and decrease the index.
|
// To work around that we're explicitly checking that case here and decrease the index.
|
||||||
const newIndex = e.newIndex === listsActive.length ? e.newIndex - 1 : e.newIndex
|
const newIndex = e.newIndex === projectsActive.length ? e.newIndex - 1 : e.newIndex
|
||||||
|
|
||||||
const list = listsActive[newIndex]
|
const project = projectsActive[newIndex]
|
||||||
const listBefore = listsActive[newIndex - 1] ?? null
|
const projectBefore = projectsActive[newIndex - 1] ?? null
|
||||||
const listAfter = listsActive[newIndex + 1] ?? null
|
const projectAfter = projectsActive[newIndex + 1] ?? null
|
||||||
listUpdating.value[list.id] = true
|
projectUpdating.value[project.id] = true
|
||||||
|
|
||||||
const position = calculateItemPosition(
|
const position = calculateItemPosition(
|
||||||
listBefore !== null ? listBefore.position : null,
|
projectBefore !== null ? projectBefore.position : null,
|
||||||
listAfter !== null ? listAfter.position : null,
|
projectAfter !== null ? projectAfter.position : null,
|
||||||
)
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// create a copy of the list in order to not violate pinia manipulation
|
// create a copy of the project in order to not violate pinia manipulation
|
||||||
await listStore.updateList({
|
await projectStore.updateProject({
|
||||||
...list,
|
...project,
|
||||||
position,
|
position,
|
||||||
namespaceId,
|
namespaceId,
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
listUpdating.value[list.id] = false
|
projectUpdating.value[project.id] = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -280,6 +286,18 @@ $vikunja-nav-background: var(--site-background);
|
||||||
$vikunja-nav-color: var(--grey-700);
|
$vikunja-nav-color: var(--grey-700);
|
||||||
$vikunja-nav-selected-width: 0.4rem;
|
$vikunja-nav-selected-width: 0.4rem;
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
padding-left: 1rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
|
@media screen and (min-width: $tablet) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.namespace-container {
|
.namespace-container {
|
||||||
background: $vikunja-nav-background;
|
background: $vikunja-nav-background;
|
||||||
color: $vikunja-nav-color;
|
color: $vikunja-nav-color;
|
||||||
|
@ -303,257 +321,237 @@ $vikunja-nav-selected-width: 0.4rem;
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
transition: transform $transition-duration ease-out;
|
transition: transform $transition-duration ease-out;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.menu {
|
// these are general menu styles
|
||||||
.menu-label {
|
// should be in own components
|
||||||
font-size: 1rem;
|
.menu {
|
||||||
font-weight: 700;
|
.menu-label,
|
||||||
font-weight: bold;
|
.menu-list .list-menu-link,
|
||||||
font-family: $vikunja-font;
|
.menu-list a {
|
||||||
color: $vikunja-nav-color;
|
display: flex;
|
||||||
font-weight: 500;
|
align-items: center;
|
||||||
min-height: 2.5rem;
|
justify-content: space-between;
|
||||||
padding-top: 0;
|
cursor: pointer;
|
||||||
padding-left: $navbar-padding;
|
|
||||||
|
|
||||||
overflow: hidden;
|
.color-bubble {
|
||||||
}
|
height: 12px;
|
||||||
|
flex: 0 0 12px;
|
||||||
.menu-label,
|
|
||||||
.menu-list .list-menu-link,
|
|
||||||
.menu-list a {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
.list-menu-title {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.color-bubble {
|
|
||||||
height: 12px;
|
|
||||||
flex: 0 0 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
.favorite {
|
|
||||||
margin-left: .25rem;
|
|
||||||
transition: opacity $transition, color $transition;
|
|
||||||
opacity: 0;
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&.is-favorite {
|
|
||||||
color: var(--warning);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.favorite.is-favorite,
|
|
||||||
.list-menu:hover .favorite {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-label {
|
|
||||||
.color-bubble {
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
flex-basis: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.is-archived {
|
|
||||||
min-width: 85px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.namespace-title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
color: $vikunja-nav-color;
|
|
||||||
padding: 0 .25rem;
|
|
||||||
|
|
||||||
.menu-label {
|
|
||||||
margin-bottom: 0;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
|
|
||||||
.name {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.count {
|
|
||||||
color: var(--grey-500);
|
|
||||||
margin-right: .5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.dropdown-trigger) {
|
|
||||||
padding: .5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle-lists-icon {
|
|
||||||
svg {
|
|
||||||
transition: all $transition;
|
|
||||||
transform: rotate(90deg);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active svg {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover .toggle-lists-icon svg {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(.has-menu) .toggle-lists-icon {
|
|
||||||
padding-right: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-label,
|
|
||||||
.nsettings,
|
|
||||||
.menu-list .list-menu-link,
|
|
||||||
.menu-list a {
|
|
||||||
color: $vikunja-nav-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-list {
|
|
||||||
li {
|
|
||||||
height: 44px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--white);
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.dropdown-trigger) {
|
|
||||||
opacity: 0;
|
|
||||||
padding: .5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: $transition;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover :deep(.dropdown-trigger) {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.flip-list-move {
|
|
||||||
transition: transform $transition-duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ghost {
|
|
||||||
background: var(--grey-200);
|
|
||||||
|
|
||||||
* {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-menu-link, li > a {
|
|
||||||
padding: 0.75rem .5rem 0.75rem ($navbar-padding * 1.5 - 1.75rem);
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
|
|
||||||
border-radius: 0;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
width: 100%;
|
|
||||||
border-left: $vikunja-nav-selected-width solid transparent;
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
height: 1rem;
|
|
||||||
vertical-align: middle;
|
|
||||||
padding-right: 0.5rem;
|
|
||||||
|
|
||||||
&.handle {
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity $transition;
|
|
||||||
margin-right: .25rem;
|
|
||||||
cursor: grab;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover .icon.handle {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.router-link-exact-active {
|
|
||||||
color: var(--primary);
|
|
||||||
border-left: $vikunja-nav-selected-width solid var(--primary);
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
color: var(--primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-left: $vikunja-nav-selected-width solid var(--primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
padding-left: 1rem;
|
|
||||||
margin-right: 1rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
|
|
||||||
@media screen and (min-width: $tablet) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.namespaces-lists {
|
|
||||||
padding-top: math.div($navbar-padding, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
color: var(--grey-400) !important;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-menu {
|
.menu-list {
|
||||||
margin-top: math.div($navbar-padding, 2);
|
li {
|
||||||
|
height: 44px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
.menu-list {
|
&:hover {
|
||||||
li {
|
background: var(--white);
|
||||||
font-weight: 500;
|
|
||||||
font-family: $vikunja-font;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-menu-link, li > a {
|
.menu-list-dropdown {
|
||||||
padding-left: 2rem;
|
opacity: 0;
|
||||||
display: inline-block;
|
transition: $transition;
|
||||||
|
}
|
||||||
|
|
||||||
.icon {
|
&:hover .menu-list-dropdown {
|
||||||
padding-bottom: .25rem;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item-icon {
|
||||||
|
color: var(--grey-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-list-dropdown-trigger {
|
||||||
|
display: flex;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flip-list-move {
|
||||||
|
transition: transform $transition-duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghost {
|
||||||
|
background: var(--grey-200);
|
||||||
|
|
||||||
|
* {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-menu-link,
|
||||||
|
li > a {
|
||||||
|
color: $vikunja-nav-color;
|
||||||
|
padding: 0.75rem .5rem 0.75rem ($navbar-padding * 1.5 - 1.75rem);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
border-radius: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
border-left: $vikunja-nav-selected-width solid transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-left: $vikunja-nav-selected-width solid var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.router-link-exact-active {
|
||||||
|
color: var(--primary);
|
||||||
|
border-left: $vikunja-nav-selected-width solid var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
height: 1rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.router-link-exact-active .icon:not(.handle) {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity $transition;
|
||||||
|
margin-right: .25rem;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
&:hover .handle {
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-setting-spacer {
|
.top-menu {
|
||||||
|
margin-top: math.div($navbar-padding, 2);
|
||||||
|
|
||||||
|
.menu-list {
|
||||||
|
li {
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: $vikunja-font;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-menu-link,
|
||||||
|
li > a {
|
||||||
|
padding-left: 2rem;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
padding-bottom: .25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.namespaces-lists {
|
||||||
|
padding-top: math.div($navbar-padding, 2);
|
||||||
|
|
||||||
|
.menu-label {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
font-weight: bold;
|
||||||
|
font-family: $vikunja-font;
|
||||||
|
color: $vikunja-nav-color;
|
||||||
|
font-weight: 600;
|
||||||
|
min-height: 2.5rem;
|
||||||
|
padding-top: 0;
|
||||||
|
padding-left: $navbar-padding;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
|
||||||
|
.name {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count {
|
||||||
|
color: var(--grey-500);
|
||||||
|
margin-right: .5rem;
|
||||||
|
// align brackets with number
|
||||||
|
font-feature-settings: "case";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite {
|
||||||
|
margin-left: .25rem;
|
||||||
|
transition: opacity $transition, color $transition;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&.is-favorite {
|
||||||
|
color: var(--warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite.is-favorite,
|
||||||
|
.list-menu:hover .favorite {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-menu-title {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-bubble {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
flex-basis: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-archived {
|
||||||
|
min-width: 85px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.namespace-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
color: $vikunja-nav-color;
|
||||||
|
padding: 0 .25rem;
|
||||||
|
|
||||||
|
.toggle-lists-icon {
|
||||||
|
svg {
|
||||||
|
transition: all $transition;
|
||||||
|
transform: rotate(90deg);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active svg {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .toggle-lists-icon svg {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.has-menu) .toggle-lists-icon {
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-setting-spacer {
|
||||||
width: 2.5rem;
|
width: 2.5rem;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.namespaces-list.loader-container.is-loading {
|
.namespaces-project.loader-container.is-loading {
|
||||||
min-height: calc(100vh - #{$navbar-height + 1.5rem + 1rem + 1.5rem});
|
min-height: calc(100vh - #{$navbar-height + 1.5rem + 1rem + 1.5rem});
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
26
src/components/input/Button.story.vue
Normal file
26
src/components/input/Button.story.vue
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {logEvent} from 'histoire/client'
|
||||||
|
import XButton from './button.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Story :layout="{ type: 'grid', width: '200px' }">
|
||||||
|
<Variant title="primary">
|
||||||
|
<XButton @click="logEvent('Click', $event)" variant="primary">
|
||||||
|
Order pizza!
|
||||||
|
</XButton>
|
||||||
|
</Variant>
|
||||||
|
|
||||||
|
<Variant title="secondary">
|
||||||
|
<XButton @click="logEvent('Click', $event)" variant="secondary">
|
||||||
|
Order spaghetti!
|
||||||
|
</XButton>
|
||||||
|
</Variant>
|
||||||
|
|
||||||
|
<Variant title="tertiary">
|
||||||
|
<XButton @click="logEvent('Click', $event)" variant="tertiary">
|
||||||
|
Order tortellini!
|
||||||
|
</XButton>
|
||||||
|
</Variant>
|
||||||
|
</Story>
|
||||||
|
</template>
|
14
src/components/input/ColorPicker.story.vue
Normal file
14
src/components/input/ColorPicker.story.vue
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {reactive} from 'vue'
|
||||||
|
import ColorPicker from './ColorPicker.vue'
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
color: '#f2f2f2',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Story :layout="{ type: 'grid', width: '200px' }">
|
||||||
|
<ColorPicker v-model="state.color" />
|
||||||
|
</Story>
|
||||||
|
</template>
|
|
@ -37,6 +37,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {computed, ref, toRef, watch} from 'vue'
|
import {computed, ref, toRef, watch} from 'vue'
|
||||||
import {createRandomID} from '@/helpers/randomId'
|
import {createRandomID} from '@/helpers/randomId'
|
||||||
|
import XButton from '@/components/input/button.vue'
|
||||||
|
|
||||||
const DEFAULT_COLORS = [
|
const DEFAULT_COLORS = [
|
||||||
'#1973ff',
|
'#1973ff',
|
|
@ -1,63 +0,0 @@
|
||||||
<template>
|
|
||||||
<multiselect
|
|
||||||
v-model="selectedLists"
|
|
||||||
:search-results="foundLists"
|
|
||||||
:loading="listService.loading"
|
|
||||||
:multiple="true"
|
|
||||||
:placeholder="$t('list.search')"
|
|
||||||
label="title"
|
|
||||||
@search="findLists"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import {computed, ref, shallowReactive, watchEffect, type PropType} from 'vue'
|
|
||||||
|
|
||||||
import Multiselect from '@/components/input/multiselect.vue'
|
|
||||||
|
|
||||||
import type {IList} from '@/modelTypes/IList'
|
|
||||||
|
|
||||||
import ListService from '@/services/list'
|
|
||||||
import {includesById} from '@/helpers/utils'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
modelValue: {
|
|
||||||
type: Array as PropType<IList[]>,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:modelValue', value: IList[]): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const lists = ref<IList[]>([])
|
|
||||||
|
|
||||||
watchEffect(() => {
|
|
||||||
lists.value = props.modelValue
|
|
||||||
})
|
|
||||||
|
|
||||||
const selectedLists = computed({
|
|
||||||
get() {
|
|
||||||
return lists.value
|
|
||||||
},
|
|
||||||
set: (value) => {
|
|
||||||
lists.value = value
|
|
||||||
emit('update:modelValue', value)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const listService = shallowReactive(new ListService())
|
|
||||||
const foundLists = ref<IList[]>([])
|
|
||||||
|
|
||||||
async function findLists(query: string) {
|
|
||||||
if (query === '') {
|
|
||||||
foundLists.value = []
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await listService.getAll({}, {s: query}) as IList[]
|
|
||||||
|
|
||||||
// Filter selected items from the results
|
|
||||||
foundLists.value = response.filter(({id}) => !includesById(lists.value, id))
|
|
||||||
}
|
|
||||||
</script>
|
|
63
src/components/input/SelectProject.vue
Normal file
63
src/components/input/SelectProject.vue
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
<template>
|
||||||
|
<multiselect
|
||||||
|
v-model="selectedProjects"
|
||||||
|
:search-results="foundProjects"
|
||||||
|
:loading="projectService.loading"
|
||||||
|
:multiple="true"
|
||||||
|
:placeholder="$t('project.search')"
|
||||||
|
label="title"
|
||||||
|
@search="findProjects"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, ref, shallowReactive, watchEffect, type PropType} from 'vue'
|
||||||
|
|
||||||
|
import Multiselect from '@/components/input/multiselect.vue'
|
||||||
|
|
||||||
|
import type {IProject} from '@/modelTypes/IProject'
|
||||||
|
|
||||||
|
import ProjectService from '@/services/project'
|
||||||
|
import {includesById} from '@/helpers/utils'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Array as PropType<IProject[]>,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: IProject[]): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const projects = ref<IProject[]>([])
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
projects.value = props.modelValue
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedProjects = computed({
|
||||||
|
get() {
|
||||||
|
return projects.value
|
||||||
|
},
|
||||||
|
set: (value) => {
|
||||||
|
projects.value = value
|
||||||
|
emit('update:modelValue', value)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const projectService = shallowReactive(new ProjectService())
|
||||||
|
const foundProjects = ref<IProject[]>([])
|
||||||
|
|
||||||
|
async function findProjects(query: string) {
|
||||||
|
if (query === '') {
|
||||||
|
foundProjects.value = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await projectService.getAll({}, {s: query}) as IProject[]
|
||||||
|
|
||||||
|
// Filter selected items from the results
|
||||||
|
foundProjects.value = response.filter(({id}) => !includesById(projects.value, id))
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -4,7 +4,7 @@
|
||||||
{{ date === null ? chooseDateLabel : formatDateShort(date) }}
|
{{ date === null ? chooseDateLabel : formatDateShort(date) }}
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
|
|
||||||
<transition name="fade">
|
<CustomTransition name="fade">
|
||||||
<div v-if="show" class="datepicker-popup" ref="datepickerPopup">
|
<div v-if="show" class="datepicker-popup" ref="datepickerPopup">
|
||||||
|
|
||||||
<BaseButton
|
<BaseButton
|
||||||
|
@ -84,7 +84,7 @@
|
||||||
{{ $t('misc.confirm') }}
|
{{ $t('misc.confirm') }}
|
||||||
</x-button>
|
</x-button>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</CustomTransition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -94,6 +94,7 @@ import flatPickr from 'vue-flatpickr-component'
|
||||||
import 'flatpickr/dist/flatpickr.css'
|
import 'flatpickr/dist/flatpickr.css'
|
||||||
|
|
||||||
import BaseButton from '@/components/base/BaseButton.vue'
|
import BaseButton from '@/components/base/BaseButton.vue'
|
||||||
|
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||||
|
|
||||||
import {formatDate, formatDateShort} from '@/helpers/time/formatDate'
|
import {formatDate, formatDateShort} from '@/helpers/time/formatDate'
|
||||||
import {calculateDayInterval} from '@/helpers/time/calculateDayInterval'
|
import {calculateDayInterval} from '@/helpers/time/calculateDayInterval'
|
||||||
|
|
|
@ -286,11 +286,11 @@ function handleCheckboxClick(e: Event) {
|
||||||
console.debug('no index found')
|
console.debug('no index found')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const listPrefix = text.value.substring(index, index + 1)
|
const projectPrefix = text.value.substring(index, index + 1)
|
||||||
|
|
||||||
console.debug({index, listPrefix, checked, text: text.value})
|
console.debug({index, projectPrefix, checked, text: text.value})
|
||||||
|
|
||||||
text.value = replaceAt(text.value, index, `${listPrefix} ${checked ? '[x]' : '[ ]'} `)
|
text.value = replaceAt(text.value, index, `${projectPrefix} ${checked ? '[x]' : '[ ]'} `)
|
||||||
bubble()
|
bubble()
|
||||||
renderPreview()
|
renderPreview()
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ export function createEasyMDEConfig({ placeholder, uploadImage, imageUploadFunct
|
||||||
uploadImage,
|
uploadImage,
|
||||||
imageUploadFunction,
|
imageUploadFunction,
|
||||||
minHeight: '150px',
|
minHeight: '150px',
|
||||||
|
sideBySideFullscreen: false,
|
||||||
toolbar: [
|
toolbar: [
|
||||||
{
|
{
|
||||||
name: 'heading-1',
|
name: 'heading-1',
|
||||||
|
@ -72,15 +73,15 @@ export function createEasyMDEConfig({ placeholder, uploadImage, imageUploadFunct
|
||||||
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M19.373 5.16357H5.62695C4.79102 5.16357 4.11133 5.84326 4.11133 6.6792V16.2095C4.11133 17.0464 4.79102 17.7261 5.62695 17.7261H6.8877V21.1245C6.8877 21.3667 7.0332 21.5854 7.25684 21.6782C7.33203 21.7095 7.41016 21.7241 7.4873 21.7241C7.64258 21.7241 7.7959 21.6636 7.91113 21.5493L11.748 17.7261H19.373C20.209 17.7261 20.8887 17.0464 20.8887 16.2095V6.6792C20.8887 5.84326 20.209 5.16357 19.373 5.16357ZM19.6895 16.2095C19.6895 16.3843 19.5469 16.5269 19.373 16.5269H11.5C11.3408 16.5269 11.1895 16.5894 11.0762 16.7017L8.08691 19.6802V17.1265C8.08691 16.7954 7.81836 16.5269 7.4873 16.5269H5.62695C5.45312 16.5269 5.31055 16.3843 5.31055 16.2095V6.6792C5.31055 6.50537 5.45312 6.36279 5.62695 6.36279H19.373C19.5469 6.36279 19.6895 6.50537 19.6895 6.6792V16.2095ZM10.3431 8.45264C9.46326 8.45264 8.75 9.16589 8.75 10.0458C8.75 10.9257 9.46326 11.639 10.3431 11.639C10.4775 11.639 10.6058 11.6173 10.7305 11.5861V11.6195C10.7305 12.1322 10.3135 12.5492 9.75586 12.5492C9.4248 12.5492 9.17871 12.8177 9.17871 13.1488C9.17871 13.4799 9.46973 13.7484 9.80078 13.7484C10.9746 13.7484 11.9297 12.7933 11.9297 11.6195V10.1176L11.9294 10.1165L11.9292 10.1155C11.9297 10.1049 11.9312 10.0946 11.9326 10.0843L11.9326 10.0843C11.9345 10.0716 11.9363 10.059 11.9363 10.0458C11.9363 9.16589 11.223 8.45264 10.3431 8.45264ZM13.0637 10.0458C13.0637 9.16589 13.7771 8.45264 14.657 8.45264C15.5369 8.45264 16.2501 9.16589 16.2501 10.0458C16.2501 10.0584 16.2484 10.0706 16.2466 10.0828C16.2452 10.0929 16.2437 10.103 16.2433 10.1134C16.2433 10.1149 16.2441 10.1161 16.2441 10.1176V11.6195C16.2441 12.7933 15.2891 13.7484 14.1152 13.7484C13.7842 13.7484 13.4922 13.4799 13.4922 13.1488C13.4922 12.8177 13.7383 12.5492 14.0693 12.5492C14.6279 12.5492 15.0449 12.1322 15.0449 11.6195V11.5858C14.9202 11.6173 14.7915 11.639 14.657 11.639C13.7771 11.639 13.0637 10.9257 13.0637 10.0458Z"/></svg>',
|
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M19.373 5.16357H5.62695C4.79102 5.16357 4.11133 5.84326 4.11133 6.6792V16.2095C4.11133 17.0464 4.79102 17.7261 5.62695 17.7261H6.8877V21.1245C6.8877 21.3667 7.0332 21.5854 7.25684 21.6782C7.33203 21.7095 7.41016 21.7241 7.4873 21.7241C7.64258 21.7241 7.7959 21.6636 7.91113 21.5493L11.748 17.7261H19.373C20.209 17.7261 20.8887 17.0464 20.8887 16.2095V6.6792C20.8887 5.84326 20.209 5.16357 19.373 5.16357ZM19.6895 16.2095C19.6895 16.3843 19.5469 16.5269 19.373 16.5269H11.5C11.3408 16.5269 11.1895 16.5894 11.0762 16.7017L8.08691 19.6802V17.1265C8.08691 16.7954 7.81836 16.5269 7.4873 16.5269H5.62695C5.45312 16.5269 5.31055 16.3843 5.31055 16.2095V6.6792C5.31055 6.50537 5.45312 6.36279 5.62695 6.36279H19.373C19.5469 6.36279 19.6895 6.50537 19.6895 6.6792V16.2095ZM10.3431 8.45264C9.46326 8.45264 8.75 9.16589 8.75 10.0458C8.75 10.9257 9.46326 11.639 10.3431 11.639C10.4775 11.639 10.6058 11.6173 10.7305 11.5861V11.6195C10.7305 12.1322 10.3135 12.5492 9.75586 12.5492C9.4248 12.5492 9.17871 12.8177 9.17871 13.1488C9.17871 13.4799 9.46973 13.7484 9.80078 13.7484C10.9746 13.7484 11.9297 12.7933 11.9297 11.6195V10.1176L11.9294 10.1165L11.9292 10.1155C11.9297 10.1049 11.9312 10.0946 11.9326 10.0843L11.9326 10.0843C11.9345 10.0716 11.9363 10.059 11.9363 10.0458C11.9363 9.16589 11.223 8.45264 10.3431 8.45264ZM13.0637 10.0458C13.0637 9.16589 13.7771 8.45264 14.657 8.45264C15.5369 8.45264 16.2501 9.16589 16.2501 10.0458C16.2501 10.0584 16.2484 10.0706 16.2466 10.0828C16.2452 10.0929 16.2437 10.103 16.2433 10.1134C16.2433 10.1149 16.2441 10.1161 16.2441 10.1176V11.6195C16.2441 12.7933 15.2891 13.7484 14.1152 13.7484C13.7842 13.7484 13.4922 13.4799 13.4922 13.1488C13.4922 12.8177 13.7383 12.5492 14.0693 12.5492C14.6279 12.5492 15.0449 12.1322 15.0449 11.6195V11.5858C14.9202 11.6173 14.7915 11.639 14.657 11.639C13.7771 11.639 13.0637 10.9257 13.0637 10.0458Z"/></svg>',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'unordered-list',
|
name: 'unordered-project',
|
||||||
action: EasyMDE.toggleUnorderedList,
|
action: EasyMDE.toggleUnorderedProject,
|
||||||
title: i18n.global.t('input.editor.unorderedList'),
|
title: i18n.global.t('input.editor.unorderedProject'),
|
||||||
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M6.5281 3.7002H3.5281C3.1981 3.7002 2.9281 3.9702 2.9281 4.3002V7.3002C2.9281 7.6302 3.1981 7.9002 3.5281 7.9002H6.5281C6.8581 7.9002 7.1281 7.6302 7.1281 7.3002V4.3002C7.1281 3.9702 6.8581 3.7002 6.5281 3.7002ZM5.9281 6.7002H4.1281V4.9002H5.9281V6.7002ZM3.5281 9.90015H6.5281C6.8581 9.90015 7.1281 10.1701 7.1281 10.5001V13.5001C7.1281 13.8301 6.8581 14.1001 6.5281 14.1001H3.5281C3.1981 14.1001 2.9281 13.8301 2.9281 13.5001V10.5001C2.9281 10.1701 3.1981 9.90015 3.5281 9.90015ZM4.1281 12.9001H5.9281V11.1001H4.1281V12.9001ZM3.5281 16.1001H6.5281C6.8581 16.1001 7.1281 16.3701 7.1281 16.7001V19.7001C7.1281 20.0301 6.8581 20.3001 6.5281 20.3001H3.5281C3.1981 20.3001 2.9281 20.0301 2.9281 19.7001V16.7001C2.9281 16.3701 3.1981 16.1001 3.5281 16.1001ZM4.1281 19.1001H5.9281V17.3001H4.1281V19.1001ZM9.72817 6.4002H20.7282C21.0582 6.4002 21.3282 6.1302 21.3282 5.8002C21.3282 5.4702 21.0582 5.2002 20.7282 5.2002H9.72817C9.39817 5.2002 9.12817 5.4702 9.12817 5.8002C9.12817 6.1302 9.39817 6.4002 9.72817 6.4002ZM9.72817 11.4001H20.7282C21.0582 11.4001 21.3282 11.6701 21.3282 12.0001C21.3282 12.3301 21.0582 12.6001 20.7282 12.6001H9.72817C9.39817 12.6001 9.12817 12.3301 9.12817 12.0001C9.12817 11.6701 9.39817 11.4001 9.72817 11.4001ZM9.72817 17.6001H20.7282C21.0582 17.6001 21.3282 17.8701 21.3282 18.2001C21.3282 18.5301 21.0582 18.8001 20.7282 18.8001H9.72817C9.39817 18.8001 9.12817 18.5301 9.12817 18.2001C9.12817 17.8701 9.39817 17.6001 9.72817 17.6001Z"/></svg>',
|
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M6.5281 3.7002H3.5281C3.1981 3.7002 2.9281 3.9702 2.9281 4.3002V7.3002C2.9281 7.6302 3.1981 7.9002 3.5281 7.9002H6.5281C6.8581 7.9002 7.1281 7.6302 7.1281 7.3002V4.3002C7.1281 3.9702 6.8581 3.7002 6.5281 3.7002ZM5.9281 6.7002H4.1281V4.9002H5.9281V6.7002ZM3.5281 9.90015H6.5281C6.8581 9.90015 7.1281 10.1701 7.1281 10.5001V13.5001C7.1281 13.8301 6.8581 14.1001 6.5281 14.1001H3.5281C3.1981 14.1001 2.9281 13.8301 2.9281 13.5001V10.5001C2.9281 10.1701 3.1981 9.90015 3.5281 9.90015ZM4.1281 12.9001H5.9281V11.1001H4.1281V12.9001ZM3.5281 16.1001H6.5281C6.8581 16.1001 7.1281 16.3701 7.1281 16.7001V19.7001C7.1281 20.0301 6.8581 20.3001 6.5281 20.3001H3.5281C3.1981 20.3001 2.9281 20.0301 2.9281 19.7001V16.7001C2.9281 16.3701 3.1981 16.1001 3.5281 16.1001ZM4.1281 19.1001H5.9281V17.3001H4.1281V19.1001ZM9.72817 6.4002H20.7282C21.0582 6.4002 21.3282 6.1302 21.3282 5.8002C21.3282 5.4702 21.0582 5.2002 20.7282 5.2002H9.72817C9.39817 5.2002 9.12817 5.4702 9.12817 5.8002C9.12817 6.1302 9.39817 6.4002 9.72817 6.4002ZM9.72817 11.4001H20.7282C21.0582 11.4001 21.3282 11.6701 21.3282 12.0001C21.3282 12.3301 21.0582 12.6001 20.7282 12.6001H9.72817C9.39817 12.6001 9.12817 12.3301 9.12817 12.0001C9.12817 11.6701 9.39817 11.4001 9.72817 11.4001ZM9.72817 17.6001H20.7282C21.0582 17.6001 21.3282 17.8701 21.3282 18.2001C21.3282 18.5301 21.0582 18.8001 20.7282 18.8001H9.72817C9.39817 18.8001 9.12817 18.5301 9.12817 18.2001C9.12817 17.8701 9.39817 17.6001 9.72817 17.6001Z"/></svg>',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'ordered-list',
|
name: 'ordered-project',
|
||||||
action: EasyMDE.toggleOrderedList,
|
action: EasyMDE.toggleOrderedProject,
|
||||||
title: i18n.global.t('input.editor.orderedList'),
|
title: i18n.global.t('input.editor.orderedProject'),
|
||||||
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M4.19494 8.29994H5.99494C6.26494 8.29994 6.49494 8.07995 6.49494 7.79994C6.49494 7.51995 6.27494 7.29994 5.99494 7.29994H5.59494V3.79994C5.59494 3.62994 5.50494 3.46994 5.36494 3.37994C5.22494 3.28994 5.04494 3.26994 4.89494 3.33994L3.89494 3.76994C3.64494 3.87994 3.52494 4.17994 3.63494 4.42994C3.74494 4.67994 4.03494 4.79994 4.29494 4.68994L4.59494 4.55994V7.29994H4.19494C3.91494 7.29994 3.69494 7.51995 3.69494 7.79994C3.69494 8.07995 3.91494 8.29994 4.19494 8.29994ZM20.195 6.39995H9.19497C8.86497 6.39995 8.59497 6.12995 8.59497 5.79995C8.59497 5.46995 8.86497 5.19995 9.19497 5.19995H20.195C20.525 5.19995 20.795 5.46995 20.795 5.79995C20.795 6.12995 20.525 6.39995 20.195 6.39995ZM3.78486 14.36H6.37486C6.65486 14.36 6.87486 14.14 6.87486 13.86C6.87486 13.58 6.65486 13.36 6.37486 13.36H4.88486C5.00486 13.23 5.12486 13.09 5.23486 12.95C5.26626 12.9151 5.29645 12.8802 5.32626 12.8458L5.32629 12.8457C5.38192 12.7814 5.43627 12.7186 5.49486 12.66C5.73486 12.4 5.98486 12.12 6.17486 11.79C6.47486 11.25 6.41486 10.63 6.01486 10.17C5.57486 9.66 4.86486 9.5 4.24486 9.74C3.74486 9.95 3.39486 10.35 3.22486 10.91C3.14486 11.18 3.29486 11.46 3.56486 11.54C3.82486 11.61 4.10486 11.46 4.18486 11.2C4.29486 10.85 4.48486 10.73 4.62486 10.67C4.88486 10.57 5.13486 10.68 5.26486 10.82C5.38486 10.96 5.40486 11.12 5.30486 11.29C5.17595 11.5202 4.99618 11.7165 4.80458 11.9257L4.75486 11.98C4.67298 12.0801 4.58283 12.1801 4.49946 12.2727L4.49945 12.2727L4.47486 12.3C4.12486 12.72 3.76486 13.13 3.40486 13.53C3.27486 13.68 3.23486 13.9 3.32486 14.07C3.41486 14.24 3.58486 14.36 3.78486 14.36ZM3.68486 20.3699C4.04486 20.5899 4.46486 20.6999 4.87486 20.6999C5.13486 20.6999 5.38486 20.6499 5.61486 20.5499C6.31486 20.2799 6.73486 19.5599 6.60486 18.8799C6.53486 18.5499 6.35486 18.2899 6.12486 18.0899C6.32486 17.8999 6.45486 17.6499 6.50486 17.3799C6.57486 17.0099 6.49486 16.6299 6.27486 16.3099C5.85486 15.6899 5.07486 15.5199 4.10486 15.8299C3.83486 15.9199 3.69486 16.1999 3.77486 16.4599C3.86486 16.7299 4.14486 16.8699 4.40486 16.7899C4.70486 16.6999 5.24486 16.5799 5.45486 16.8899C5.51486 16.9899 5.54486 17.0999 5.52486 17.1999C5.51486 17.2699 5.47486 17.3599 5.36486 17.4299C5.26486 17.4999 5.12486 17.5399 4.95486 17.5799L4.77486 17.6299C4.54486 17.6999 4.40486 17.9099 4.41486 18.1499C4.42486 18.3899 4.61486 18.5799 4.84486 18.6099C5.20486 18.6599 5.58486 18.8299 5.63486 19.0799C5.67486 19.2999 5.46486 19.5499 5.25486 19.6299C4.94486 19.7599 4.52486 19.7099 4.21486 19.5199C3.97486 19.3699 3.67486 19.4399 3.52486 19.6799C3.37486 19.9199 3.44486 20.2299 3.68486 20.3699ZM20.195 18.7999H9.19497C8.86497 18.7999 8.59497 18.5299 8.59497 18.1999C8.59497 17.8699 8.86497 17.5999 9.19497 17.5999H20.195C20.525 17.5999 20.795 17.8699 20.795 18.1999C20.795 18.5299 20.525 18.7999 20.195 18.7999ZM9.19497 12.5999H20.195C20.525 12.5999 20.795 12.3299 20.795 11.9999C20.795 11.6699 20.525 11.3999 20.195 11.3999H9.19497C8.86497 11.3999 8.59497 11.6699 8.59497 11.9999C8.59497 12.3299 8.86497 12.5999 9.19497 12.5999Z"/></svg>',
|
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M4.19494 8.29994H5.99494C6.26494 8.29994 6.49494 8.07995 6.49494 7.79994C6.49494 7.51995 6.27494 7.29994 5.99494 7.29994H5.59494V3.79994C5.59494 3.62994 5.50494 3.46994 5.36494 3.37994C5.22494 3.28994 5.04494 3.26994 4.89494 3.33994L3.89494 3.76994C3.64494 3.87994 3.52494 4.17994 3.63494 4.42994C3.74494 4.67994 4.03494 4.79994 4.29494 4.68994L4.59494 4.55994V7.29994H4.19494C3.91494 7.29994 3.69494 7.51995 3.69494 7.79994C3.69494 8.07995 3.91494 8.29994 4.19494 8.29994ZM20.195 6.39995H9.19497C8.86497 6.39995 8.59497 6.12995 8.59497 5.79995C8.59497 5.46995 8.86497 5.19995 9.19497 5.19995H20.195C20.525 5.19995 20.795 5.46995 20.795 5.79995C20.795 6.12995 20.525 6.39995 20.195 6.39995ZM3.78486 14.36H6.37486C6.65486 14.36 6.87486 14.14 6.87486 13.86C6.87486 13.58 6.65486 13.36 6.37486 13.36H4.88486C5.00486 13.23 5.12486 13.09 5.23486 12.95C5.26626 12.9151 5.29645 12.8802 5.32626 12.8458L5.32629 12.8457C5.38192 12.7814 5.43627 12.7186 5.49486 12.66C5.73486 12.4 5.98486 12.12 6.17486 11.79C6.47486 11.25 6.41486 10.63 6.01486 10.17C5.57486 9.66 4.86486 9.5 4.24486 9.74C3.74486 9.95 3.39486 10.35 3.22486 10.91C3.14486 11.18 3.29486 11.46 3.56486 11.54C3.82486 11.61 4.10486 11.46 4.18486 11.2C4.29486 10.85 4.48486 10.73 4.62486 10.67C4.88486 10.57 5.13486 10.68 5.26486 10.82C5.38486 10.96 5.40486 11.12 5.30486 11.29C5.17595 11.5202 4.99618 11.7165 4.80458 11.9257L4.75486 11.98C4.67298 12.0801 4.58283 12.1801 4.49946 12.2727L4.49945 12.2727L4.47486 12.3C4.12486 12.72 3.76486 13.13 3.40486 13.53C3.27486 13.68 3.23486 13.9 3.32486 14.07C3.41486 14.24 3.58486 14.36 3.78486 14.36ZM3.68486 20.3699C4.04486 20.5899 4.46486 20.6999 4.87486 20.6999C5.13486 20.6999 5.38486 20.6499 5.61486 20.5499C6.31486 20.2799 6.73486 19.5599 6.60486 18.8799C6.53486 18.5499 6.35486 18.2899 6.12486 18.0899C6.32486 17.8999 6.45486 17.6499 6.50486 17.3799C6.57486 17.0099 6.49486 16.6299 6.27486 16.3099C5.85486 15.6899 5.07486 15.5199 4.10486 15.8299C3.83486 15.9199 3.69486 16.1999 3.77486 16.4599C3.86486 16.7299 4.14486 16.8699 4.40486 16.7899C4.70486 16.6999 5.24486 16.5799 5.45486 16.8899C5.51486 16.9899 5.54486 17.0999 5.52486 17.1999C5.51486 17.2699 5.47486 17.3599 5.36486 17.4299C5.26486 17.4999 5.12486 17.5399 4.95486 17.5799L4.77486 17.6299C4.54486 17.6999 4.40486 17.9099 4.41486 18.1499C4.42486 18.3899 4.61486 18.5799 4.84486 18.6099C5.20486 18.6599 5.58486 18.8299 5.63486 19.0799C5.67486 19.2999 5.46486 19.5499 5.25486 19.6299C4.94486 19.7599 4.52486 19.7099 4.21486 19.5199C3.97486 19.3699 3.67486 19.4399 3.52486 19.6799C3.37486 19.9199 3.44486 20.2299 3.68486 20.3699ZM20.195 18.7999H9.19497C8.86497 18.7999 8.59497 18.5299 8.59497 18.1999C8.59497 17.8699 8.86497 17.5999 9.19497 17.5999H20.195C20.525 17.5999 20.795 17.8699 20.795 18.1999C20.795 18.5299 20.525 18.7999 20.195 18.7999ZM9.19497 12.5999H20.195C20.525 12.5999 20.795 12.3299 20.795 11.9999C20.795 11.6699 20.525 11.3999 20.195 11.3999H9.19497C8.86497 11.3999 8.59497 11.6699 8.59497 11.9999C8.59497 12.3299 8.86497 12.5999 9.19497 12.5999Z"/></svg>',
|
||||||
},
|
},
|
||||||
'|',
|
'|',
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<transition name="fade">
|
<CustomTransition name="fade">
|
||||||
<div class="search-results" :class="{'search-results-inline': inline}" v-if="searchResultsVisible">
|
<div class="search-results" :class="{'search-results-inline': inline}" v-if="searchResultsVisible">
|
||||||
<BaseButton
|
<BaseButton
|
||||||
class="search-result-button is-fullwidth"
|
class="search-result-button is-fullwidth"
|
||||||
|
@ -78,8 +78,7 @@
|
||||||
</span>
|
</span>
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</CustomTransition>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -90,6 +89,7 @@ import {useI18n} from 'vue-i18n'
|
||||||
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
|
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
|
||||||
|
|
||||||
import BaseButton from '@/components/base/BaseButton.vue'
|
import BaseButton from '@/components/base/BaseButton.vue'
|
||||||
|
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||||
|
|
||||||
function elementInResults(elem: string | any, label: string, query: string): boolean {
|
function elementInResults(elem: string | any, label: string, query: string): boolean {
|
||||||
// Don't make create available if we have an exact match in our search results.
|
// Don't make create available if we have an exact match in our search results.
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user