Compare commits

..

8 Commits

Author SHA1 Message Date
kolaente f4f324576c
fix: rename component imports
continuous-integration/drone/pr Build is failing Details
2022-11-13 23:06:43 +01:00
kolaente 70ec5fb8b3
fix: don't wait until background is loaded for list to show
continuous-integration/drone/pr Build is failing Details
2022-11-13 22:57:38 +01:00
kolaente bc24605711
Merge branch 'main' into feature/rename-lists-to-projects
# Conflicts:
#	src/components/list/ListWrapper.vue
#	src/router/index.ts
2022-11-13 22:56:23 +01:00
kolaente 88d64def93
fix: list view route
continuous-integration/drone/pr Build was killed Details
2022-11-13 22:44:01 +01:00
kolaente 184aaaa955
fix: typo in readme 2022-11-13 22:41:32 +01:00
kolaente e4a5b9abf0
fix: e2e tests 2022-11-13 22:35:41 +01:00
kolaente cf26b61f9f
fix: project table view 2022-11-13 22:24:29 +01:00
kolaente b8829dfaeb
feat: rename list to project everywhere 2022-11-13 22:04:57 +01:00
175 changed files with 4047 additions and 7232 deletions

View File

@ -96,7 +96,7 @@ steps:
- dependencies
- name: test-frontend
image: cypress/browsers:node18.12.0-chrome107
image: cypress/browsers:node18.12.0-chrome106-ff106
pull: always
environment:
CYPRESS_API_URL: http://api:3456/api/v1
@ -110,7 +110,8 @@ steps:
- sed -i 's/localhost/api/g' dist/index.html
- corepack enable && pnpm config set store-dir .cache/pnpm
- pnpm cypress install
- pnpm run test:e2e-record
- pnpm run serve:dist & npx wait-on http://localhost:4173
- pnpm run test:frontend --browser chrome --record
depends_on:
- build-prod
@ -203,7 +204,7 @@ steps:
PNPM_CACHE_FOLDER: .cache/pnpm
commands:
- 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 run lint
- "echo '{\"VERSION\": \"'$(git describe --tags --always --abbrev=10 | sed 's/-/+/' | sed 's/^v//' | sed 's/-g/-/')'\"}' > src/version.json"
@ -364,7 +365,7 @@ steps:
- name: docker-unstable
image: thegeeklab/drone-docker-buildx
privileged: true
pull: always
pull: true
settings:
username:
from_secret: docker_username
@ -386,20 +387,10 @@ steps:
ref:
- 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
image: thegeeklab/drone-docker-buildx
privileged: true
pull: always
pull: true
settings:
username:
from_secret: docker_username
@ -416,7 +407,7 @@ steps:
- linux/arm/v6
- linux/arm/v7
- linux/arm64/v8
depends_on: [ generate-tags ]
depends_on: [ fetch-tags ]
when:
ref:
- "refs/tags/**"
@ -460,6 +451,9 @@ kind: pipeline
type: docker
name: update-translations
depends_on:
- build
trigger:
branch:
- main
@ -519,6 +513,6 @@ steps:
from_secret: crowdin_key
---
kind: signature
hmac: 0a65a032008f2d1911866dcb0d9536b26c1d6b105943ed5328ac13abc99c423f
hmac: 9f26b5af73e3464e9ee1b5fbcb96854ca8a7e5f8d6ee2d85fd8376aad951b446
...

View File

@ -1,7 +1,6 @@
name: Bug Report
description: Found something you weren't expecting? Report it here!
labels:
- kind/bug
labels: kind/bug
body:
- type: markdown
attributes:

35
.gitignore vendored
View File

@ -1,31 +1,26 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
stats.html
node_modules
.DS_Store
node_modules
/dist*
coverage
*.zip
.direnv/
# Test files
cypress/screenshots
cypress/videos
# local env files
.env.local
.env.*.local
# Log files
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
stats.html
pnpm-debug.log*
lerna-debug.log*
# Editor directories and files
.vscode
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
@ -33,9 +28,9 @@ cypress/videos
*.sw*
!rollup.sw.js
# Test files
cypress/screenshots
cypress/videos
# Local Netlify folder
.netlify
# histoire
.histoire

View File

@ -9,211 +9,6 @@ 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.
## [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
### Bug Fixes

View File

@ -4,7 +4,7 @@
[![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)
[![Download](https://img.shields.io/badge/download-v0.20.2-brightgreen.svg)](https://dl.vikunja.io)
[![Download](https://img.shields.io/badge/download-v0.20.1-brightgreen.svg)](https://dl.vikunja.io)
[![Translation](https://badges.crowdin.net/vikunja/localized.svg)](https://crowdin.com/project/vikunja)
This is the web frontend for Vikunja, written in Vue.js.

View File

@ -11,10 +11,8 @@ export default defineConfig({
},
projectId: '181c7x',
e2e: {
specPattern: 'cypress/e2e/**/*.{cy,spec}.{js,jsx,ts,tsx}',
baseUrl: 'http://127.0.0.1:4173',
experimentalRunAllSpecs: true,
// testIsolation: false,
baseUrl: 'http://localhost:4173',
specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}',
},
component: {
devServer: {

View File

@ -36,7 +36,7 @@ to get a shell inside the cypress container.
In that shell you can then execute the tests with
```shell
pnpm run test:e2e
pnpm run test:frontend
```
### Using The Cypress Dashboard
@ -44,5 +44,5 @@ pnpm run test:e2e
To open the Cypress Dashboard and run tests from there, run
```shell
pnpm run test:e2e:dev
pnpm run cypress:open
```

View File

@ -9,7 +9,7 @@ services:
ports:
- 3456:3456
cypress:
image: cypress/browsers:node18.12.0-chrome107
image: cypress/browsers:node16.14.0-chrome99-ff97
volumes:
- ..:/project
- $HOME/.cache:/home/node/.cache/

View File

@ -1,13 +1,11 @@
import {createFakeUserAndLogin} from '../../support/authenticateUser'
import {TaskFactory} from '../../factories/task'
import {ProjectFactory} from '../../factories/project'
import {NamespaceFactory} from '../../factories/namespace'
import {UserProjectFactory} from '../../factories/users_project'
import '../../support/authenticateUser'
describe('Editor', () => {
createFakeUserAndLogin()
beforeEach(() => {
NamespaceFactory.create(1)
const projects = ProjectFactory.create(1)

View File

@ -1,12 +1,6 @@
import {createFakeUserAndLogin} from '../../support/authenticateUser'
import '../../support/authenticateUser'
describe('The Menu', () => {
createFakeUserAndLogin()
beforeEach(() => {
cy.visit('/')
})
it('Is visible by default on desktop', () => {
cy.get('.namespace-container')
.should('have.class', 'is-active')

View File

@ -1,14 +1,14 @@
import {createFakeUserAndLogin} from '../../support/authenticateUser'
import {UserFactory} from '../../factories/user'
import '../../support/authenticateUser'
import {ProjectFactory} from '../../factories/project'
import {NamespaceFactory} from '../../factories/namespace'
describe('Namepaces', () => {
createFakeUserAndLogin()
let namespaces
beforeEach(() => {
UserFactory.create(1)
namespaces = NamespaceFactory.create(1)
ProjectFactory.create(1)
})

View File

@ -1,8 +1,10 @@
import {ProjectFactory} from '../../factories/project'
import {UserFactory} from '../../factories/user'
import {NamespaceFactory} from '../../factories/namespace'
import {TaskFactory} from '../../factories/task'
export function createProjects() {
UserFactory.create(1)
NamespaceFactory.create(1)
const projects = ProjectFactory.create(1, {
title: 'First Project'

View File

@ -1,11 +1,10 @@
import {createFakeUserAndLogin} from '../../support/authenticateUser'
import {ProjectFactory} from '../../factories/project'
import '../../support/authenticateUser'
import {prepareProjects} from './prepareProjects'
describe('Project History', () => {
createFakeUserAndLogin()
prepareLists()
prepareProjects()
it('should show a project history on the home page', () => {
cy.intercept(Cypress.env('API_URL') + '/namespaces*').as('loadNamespaces')
@ -46,7 +45,7 @@ describe('Project History', () => {
cy.get('body')
.should('contain', 'Last viewed')
cy.get('[data-cy="listCardGrid"]')
cy.get('.project-cards-wrapper-2-rows')
.should('not.contain', projects[0].title)
.should('contain', projects[1].title)
.should('contain', projects[2].title)

View File

@ -1,12 +1,10 @@
import {formatISO, format} from 'date-fns'
import {createFakeUserAndLogin} from '../../support/authenticateUser'
import {TaskFactory} from '../../factories/task'
import {prepareProjects} from './prepareProjects'
import '../../support/authenticateUser'
describe('Project View Gantt', () => {
createFakeUserAndLogin()
prepareProjects()
it('Hides tasks with no dates', () => {
@ -35,8 +33,8 @@ describe('Project View Gantt', () => {
it('Shows tasks with dates', () => {
const now = new Date()
const tasks = TaskFactory.create(1, {
start_date: now.toISOString(),
end_date: new Date(new Date(now).setDate(now.getDate() + 4)).toISOString(),
start_date: formatISO(now),
end_date: formatISO(now.setDate(now.getDate() + 4)),
})
cy.visit('/projects/1/gantt')
@ -62,12 +60,13 @@ describe('Project View Gantt', () => {
})
it('Drags a task around', () => {
cy.intercept(Cypress.env('API_URL') + '/tasks/*').as('taskUpdate')
cy.intercept('**/api/v1/tasks/*')
.as('taskUpdate')
const now = new Date()
TaskFactory.create(1, {
start_date: now.toISOString(),
end_date: new Date(new Date(now).setDate(now.getDate() + 4)).toISOString(),
start_date: formatISO(now),
end_date: formatISO(now.setDate(now.getDate() + 4)),
})
cy.visit('/projects/1/gantt')

View File

@ -1,15 +1,14 @@
import {createFakeUserAndLogin} from '../../support/authenticateUser'
import {BucketFactory} from '../../factories/bucket'
import {ProjectFactory} from '../../factories/project'
import {TaskFactory} from '../../factories/task'
import {prepareProjects} from './prepareProjects'
import '../../support/authenticateUser'
describe('Project View Kanban', () => {
createFakeUserAndLogin()
prepareProjects()
let buckets
prepareProjects()
beforeEach(() => {
buckets = BucketFactory.create(2)
})
@ -39,7 +38,7 @@ describe('Project View Kanban', () => {
})
cy.visit('/projects/1/kanban')
cy.get('.kanban .bucket')
cy.getSettled('.kanban .bucket')
.contains(buckets[0].title)
.get('.bucket-footer .button')
.contains('Add another task')
@ -71,7 +70,7 @@ describe('Project View Kanban', () => {
it('Can set a bucket limit', () => {
cy.visit('/projects/1/kanban')
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger')
cy.getSettled('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger')
.first()
.click()
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item')
@ -92,7 +91,7 @@ describe('Project View Kanban', () => {
it('Can rename a bucket', () => {
cy.visit('/projects/1/kanban')
cy.get('.kanban .bucket .bucket-header .title')
cy.getSettled('.kanban .bucket .bucket-header .title')
.first()
.type('{selectall}New Bucket Title{enter}')
cy.get('.kanban .bucket .bucket-header .title')
@ -103,7 +102,7 @@ describe('Project View Kanban', () => {
it('Can delete a bucket', () => {
cy.visit('/projects/1/kanban')
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger')
cy.getSettled('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger')
.first()
.click()
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item')
@ -130,7 +129,7 @@ describe('Project View Kanban', () => {
})
cy.visit('/projects/1/kanban')
cy.get('.kanban .bucket .tasks .task')
cy.getSettled('.kanban .bucket .tasks .task')
.contains(tasks[0].title)
.first()
.drag('.kanban .bucket:nth-child(2) .tasks')
@ -149,7 +148,7 @@ describe('Project View Kanban', () => {
})
cy.visit('/projects/1/kanban')
cy.get('.kanban .bucket .tasks .task')
cy.getSettled('.kanban .bucket .tasks .task')
.contains(tasks[0].title)
.should('be.visible')
.click()
@ -171,7 +170,7 @@ describe('Project View Kanban', () => {
const task = tasks[0]
cy.visit('/projects/1/kanban')
cy.get('.kanban .bucket .tasks .task')
cy.getSettled('.kanban .bucket .tasks .task')
.contains(task.title)
.should('be.visible')
.click()
@ -218,7 +217,7 @@ describe('Project View Kanban', () => {
const task = tasks[0]
cy.visit('/projects/1/kanban')
cy.get('.kanban .bucket .tasks .task')
cy.getSettled('.kanban .bucket .tasks .task')
.contains(task.title)
.should('be.visible')
.click()
@ -235,7 +234,7 @@ describe('Project View Kanban', () => {
cy.get('.global-notification')
.should('contain', 'Success')
cy.get('.kanban .bucket .tasks')
cy.getSettled('.kanban .bucket .tasks')
.should('not.contain', task.title)
})
})

View File

@ -1,13 +1,12 @@
import {createFakeUserAndLogin} from '../../support/authenticateUser'
import {UserProjectFactory} from '../../factories/users_project'
import {TaskFactory} from '../../factories/task'
import {UserFactory} from '../../factories/user'
import {ProjectFactory} from '../../factories/project'
import {prepareProjects} from './prepareProjects'
import '../../support/authenticateUser'
describe('Project View Project', () => {
createFakeUserAndLogin()
prepareProjects()
it('Should be an empty project', () => {
@ -79,7 +78,7 @@ describe('Project View Project', () => {
cy.get('.menu-project li .project-menu-link .color-bubble')
.should('have.css', 'background-color', 'rgb(0, 219, 96)')
cy.get('.tasks .color-bubble')
cy.get('.tasks-container .tasks .color-bubble')
.should('not.exist')
})
@ -91,9 +90,9 @@ describe('Project View Project', () => {
})
cy.visit('/projects/1/project')
cy.get('.tasks')
cy.get('.tasks-container .tasks')
.should('contain', tasks[1].title)
cy.get('.tasks')
cy.get('.tasks-container .tasks')
.should('not.contain', tasks[99].title)
cy.get('.card-content .pagination .pagination-link')
@ -102,9 +101,9 @@ describe('Project View Project', () => {
cy.url()
.should('contain', '?page=2')
cy.get('.tasks')
cy.get('.tasks-container .tasks')
.should('contain', tasks[99].title)
cy.get('.tasks')
cy.get('.tasks-container .tasks')
.should('not.contain', tasks[1].title)
})
})

View File

@ -1,10 +1,8 @@
import {createFakeUserAndLogin} from '../../support/authenticateUser'
import {TaskFactory} from '../../factories/task'
describe('Project View Table', () => {
createFakeUserAndLogin()
import '../../support/authenticateUser'
describe('Project View Table', () => {
it('Should show a table with tasks', () => {
const tasks = TaskFactory.create(1)
cy.visit('/projects/1/table')

View File

@ -1,11 +1,9 @@
import {createFakeUserAndLogin} from '../../support/authenticateUser'
import {TaskFactory} from '../../factories/task'
import {prepareProjects} from './prepareProjects'
describe('Projects', () => {
createFakeUserAndLogin()
import '../../support/authenticateUser'
describe('Projects', () => {
let projects
prepareProjects((newProjects) => (projects = newProjects))
@ -54,7 +52,7 @@ describe('Projects', () => {
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')
cy.get('.namespace-container .menu.namespaces-projects .menu-project li:first-child .dropdown .dropdown-trigger')
.click()
cy.get('.namespace-container .menu.namespaces-projects .menu-project li:first-child .dropdown .dropdown-content')
.contains('Edit')
@ -82,7 +80,7 @@ describe('Projects', () => {
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')
cy.get('.namespace-container .menu.namespaces-projects .menu-project li:first-child .dropdown .dropdown-trigger')
.click()
cy.get('.namespace-container .menu.namespaces-projects .menu-project li:first-child .dropdown .dropdown-content')
.contains('Delete')

View File

@ -1,12 +1,9 @@
import {createFakeUserAndLogin} from '../../support/authenticateUser'
import {TeamFactory} from '../../factories/team'
import {TeamMemberFactory} from '../../factories/team_member'
import {UserFactory} from '../../factories/user'
import '../../support/authenticateUser'
describe('Team', () => {
createFakeUserAndLogin()
it('Creates a new team', () => {
TeamFactory.truncate()
cy.visit('/teams')

View File

@ -1,13 +1,16 @@
import {createFakeUserAndLogin} from '../../support/authenticateUser'
import {ProjectFactory} from '../../factories/project'
import {seed} from '../../support/seed'
import {TaskFactory} from '../../factories/task'
import {formatISO} from 'date-fns'
import {UserFactory} from '../../factories/user'
import {NamespaceFactory} from '../../factories/namespace'
import {BucketFactory} from '../../factories/bucket'
import {updateUserSettings} from '../../support/updateUserSettings'
function seedTasks(numberOfTasks = 50, startDueDate = new Date()) {
import '../../support/authenticateUser'
function seedTasks(numberOfTasks = 100, startDueDate = new Date()) {
UserFactory.create(1)
NamespaceFactory.create(1)
const project = ProjectFactory.create()[0]
BucketFactory.create(1, {
@ -17,7 +20,7 @@ function seedTasks(numberOfTasks = 50, startDueDate = new Date()) {
let dueDate = startDueDate
for (let i = 0; i < numberOfTasks; i++) {
const now = new Date()
dueDate = new Date(new Date(dueDate).setDate(dueDate.getDate() + 2))
dueDate = (new Date(dueDate.valueOf())).setDate((new Date(dueDate.valueOf())).getDate() + 2)
tasks.push({
id: i + 1,
project_id: project.id,
@ -25,9 +28,9 @@ function seedTasks(numberOfTasks = 50, startDueDate = new Date()) {
created_by_id: 1,
title: 'Test Task ' + i,
index: i + 1,
due_date: dueDate.toISOString(),
created: now.toISOString(),
updated: now.toISOString(),
due_date: formatISO(dueDate),
created: formatISO(now),
updated: formatISO(now),
})
}
seed(TaskFactory.table, tasks)
@ -35,11 +38,8 @@ function seedTasks(numberOfTasks = 50, startDueDate = new Date()) {
}
describe('Home Page Task Overview', () => {
createFakeUserAndLogin()
it('Should show tasks with a near due date first on the home page overview', () => {
const taskCount = 50
const {tasks} = seedTasks(taskCount)
const {tasks} = seedTasks()
cy.visit('/')
cy.get('[data-cy="showTasks"] .card .task')
@ -49,10 +49,8 @@ describe('Home Page Task Overview', () => {
})
it('Should show overdue tasks first, then show other tasks', () => {
const now = new Date()
const oldDate = new Date(new Date(now).setDate(now.getDate() - 14))
const taskCount = 50
const {tasks} = seedTasks(taskCount, oldDate)
const oldDate = (new Date()).setDate((new Date()).getDate() - 14)
const {tasks} = seedTasks(100, oldDate)
cy.visit('/')
cy.get('[data-cy="showTasks"] .card .task')
@ -70,7 +68,7 @@ describe('Home Page Task Overview', () => {
TaskFactory.create(1, {
id: 999,
title: newTaskTitle,
due_date: new Date().toISOString(),
due_date: formatISO(new Date()),
}, false)
cy.visit(`/projects/${tasks[0].project_id}/project`)
@ -85,7 +83,7 @@ describe('Home Page Task Overview', () => {
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
const {tasks} = seedTasks(100)
const {tasks} = seedTasks()
const newTaskTitle = 'New Task'
cy.visit('/')

View File

@ -1,4 +1,4 @@
import {createFakeUserAndLogin} from '../../support/authenticateUser'
import {formatISO} from 'date-fns'
import {TaskFactory} from '../../factories/task'
import {ProjectFactory} from '../../factories/project'
@ -11,6 +11,7 @@ import {LabelFactory} from '../../factories/labels'
import {LabelTaskFactory} from '../../factories/label_task'
import {BucketFactory} from '../../factories/bucket'
import '../../support/authenticateUser'
import {TaskAttachmentFactory} from '../../factories/task_attachments'
function addLabelToTaskAndVerify(labelTitle: string) {
@ -45,14 +46,12 @@ function uploadAttachmentAndVerify(taskId: number) {
}
describe('Task', () => {
createFakeUserAndLogin()
let namespaces
let projects
let buckets
beforeEach(() => {
// UserFactory.create(1)
UserFactory.create(1)
namespaces = NamespaceFactory.create(1)
projects = ProjectFactory.create(1)
buckets = BucketFactory.create(1, {
@ -146,7 +145,7 @@ describe('Task', () => {
id: 1,
index: 1,
done: true,
done_at: new Date().toISOString()
done_at: formatISO(new Date())
})
cy.visit(`/tasks/${tasks[0].id}`)
@ -422,10 +421,10 @@ describe('Task', () => {
cy.visit(`/tasks/${tasks[0].id}`)
cy.get('.task-view .details.labels-list .multiselect .input-wrapper')
cy.getSettled('.task-view .details.labels-list .multiselect .input-wrapper')
.should('be.visible')
.should('contain', labels[0].title)
cy.get('.task-view .details.labels-list .multiselect .input-wrapper')
cy.getSettled('.task-view .details.labels-list .multiselect .input-wrapper')
.children()
.first()
.get('[data-cy="taskDetail.removeLabel"]')

View File

@ -1,11 +0,0 @@
{
"extends": "@vue/tsconfig/tsconfig.web.json",
"include": ["./**/*", "../support/**/*", "../factories/**/*"],
"compilerOptions": {
"baseUrl": ".",
"isolatedModules": false,
"target": "ES2015",
"lib": ["ESNext", "dom"],
"types": ["cypress"]
}
}

View File

@ -11,11 +11,16 @@ const testAndAssertFailed = fixture => {
cy.get('div.message.danger').contains('Wrong username or password.')
}
const username = 'test'
context('Login', () => {
beforeEach(() => {
UserFactory.create(1, {username})
UserFactory.create(1, {
username: 'test',
})
cy.visit('/', {
onBeforeLoad(win) {
win.localStorage.removeItem('token')
},
})
})
it('Should log in with the right credentials', () => {

View File

@ -1,5 +1,5 @@
import {createFakeUserAndLogin} from '../../support/authenticateUser'
import {createLists} from '../list/prepareLists'
import '../../support/authenticateUser'
import {createProjects} from '../project/prepareProjects'
function logout() {
cy.get('.navbar .user .username')
@ -10,8 +10,6 @@ function logout() {
}
describe('Log out', () => {
createFakeUserAndLogin()
it('Logs the user out', () => {
cy.visit('/')

View File

@ -1,7 +1,11 @@
import {createFakeUserAndLogin} from '../../support/authenticateUser'
import {UserFactory} from '../../factories/user'
import '../../support/authenticateUser'
describe('User Settings', () => {
createFakeUserAndLogin()
beforeEach(() => {
UserFactory.create(1)
})
it('Changes the user avatar', () => {
cy.intercept(`${Cypress.env('API_URL')}/user/settings/avatar/upload`).as('uploadAvatar')

View File

@ -1,5 +1,6 @@
import {faker} from '@faker-js/faker'
import {Factory} from '../support/factory'
import {formatISO} from 'date-fns'
export class BucketFactory extends Factory {
static table = 'buckets'
@ -12,8 +13,8 @@ export class BucketFactory extends Factory {
title: faker.lorem.words(3),
project_id: 1,
created_by_id: 1,
created: now.toISOString(),
updated: now.toISOString(),
created: formatISO(now),
updated: formatISO(now)
}
}
}

View File

@ -1,4 +1,5 @@
import {Factory} from '../support/factory'
import {formatISO} from 'date-fns'
export class LabelTaskFactory extends Factory {
static table = 'label_tasks'
@ -10,7 +11,7 @@ export class LabelTaskFactory extends Factory {
id: '{increment}',
task_id: 1,
label_id: 1,
created: now.toISOString(),
created: formatISO(now),
}
}
}

View File

@ -1,6 +1,7 @@
import {faker} from '@faker-js/faker'
import {Factory} from '../support/factory'
import {formatISO} from 'date-fns'
export class LabelFactory extends Factory {
static table = 'labels'
@ -14,8 +15,8 @@ export class LabelFactory extends Factory {
description: faker.lorem.text(10),
hex_color: (Math.random()*0xFFFFFF<<0).toString(16), // random 6-digit hex number
created_by_id: 1,
created: now.toISOString(),
updated: now.toISOString(),
created: formatISO(now),
updated: formatISO(now),
}
}
}

View File

@ -1,4 +1,5 @@
import {Factory} from '../support/factory'
import {formatISO} from "date-fns"
import {faker} from '@faker-js/faker'
export class LinkShareFactory extends Factory {
@ -14,8 +15,8 @@ export class LinkShareFactory extends Factory {
right: 0,
sharing_type: 0,
shared_by_id: 1,
created: now.toISOString(),
updated: now.toISOString(),
created: formatISO(now),
updated: formatISO(now)
}
}
}

View File

@ -1,5 +1,6 @@
import {faker} from '@faker-js/faker'
import {Factory} from '../support/factory'
import {formatISO} from 'date-fns'
export class NamespaceFactory extends Factory {
static table = 'namespaces'
@ -11,8 +12,8 @@ export class NamespaceFactory extends Factory {
id: '{increment}',
title: faker.lorem.words(3),
owner_id: 1,
created: now.toISOString(),
updated: now.toISOString(),
created: formatISO(now),
updated: formatISO(now)
}
}
}

View File

@ -1,4 +1,5 @@
import {Factory} from '../support/factory'
import {formatISO} from "date-fns"
import {faker} from '@faker-js/faker'
export class ProjectFactory extends Factory {
@ -12,8 +13,8 @@ export class ProjectFactory extends Factory {
title: faker.lorem.words(3),
owner_id: 1,
namespace_id: 1,
created: now.toISOString(),
updated: now.toISOString(),
created: formatISO(now),
updated: formatISO(now)
}
}
}

View File

@ -1,5 +1,6 @@
import {faker} from '@faker-js/faker'
import {Factory} from '../support/factory'
import {formatISO} from 'date-fns'
export class TaskFactory extends Factory {
static table = 'tasks'
@ -15,8 +16,8 @@ export class TaskFactory extends Factory {
created_by_id: 1,
index: '{increment}',
position: '{increment}',
created: now.toISOString(),
updated: now.toISOString()
created: formatISO(now),
updated: formatISO(now)
}
}
}

View File

@ -1,4 +1,5 @@
import {Factory} from '../support/factory'
import {formatISO} from 'date-fns'
export class TaskAssigneeFactory extends Factory {
static table = 'task_assignees'
@ -10,7 +11,7 @@ export class TaskAssigneeFactory extends Factory {
id: '{increment}',
task_id: 1,
user_id: 1,
created: now.toISOString(),
created: formatISO(now),
}
}
}

View File

@ -1,4 +1,5 @@
import {Factory} from '../support/factory'
import {formatISO} from 'date-fns'
export class TaskAttachmentFactory extends Factory {
static table = 'task_attachments'
@ -10,7 +11,7 @@ export class TaskAttachmentFactory extends Factory {
id: '{increment}',
task_id: 1,
file_id: 1,
created: now.toISOString(),
created: formatISO(now),
}
}
}

View File

@ -1,6 +1,7 @@
import {faker} from '@faker-js/faker'
import {Factory} from '../support/factory'
import {formatISO} from "date-fns"
export class TaskCommentFactory extends Factory {
static table = 'task_comments'
@ -13,8 +14,8 @@ export class TaskCommentFactory extends Factory {
comment: faker.lorem.text(3),
author_id: 1,
task_id: 1,
created: now.toISOString(),
updated: now.toISOString()
created: formatISO(now),
updated: formatISO(now)
}
}
}

View File

@ -1,5 +1,6 @@
import {faker} from '@faker-js/faker'
import {Factory} from '../support/factory'
import {formatISO} from 'date-fns'
export class TeamFactory extends Factory {
static table = 'teams'
@ -10,8 +11,8 @@ export class TeamFactory extends Factory {
return {
name: faker.lorem.words(3),
created_by_id: 1,
created: now.toISOString(),
updated: now.toISOString(),
created: formatISO(now),
updated: formatISO(now)
}
}
}

View File

@ -1,4 +1,5 @@
import {Factory} from '../support/factory'
import {formatISO} from 'date-fns'
export class TeamMemberFactory extends Factory {
static table = 'team_members'
@ -8,7 +9,7 @@ export class TeamMemberFactory extends Factory {
team_id: 1,
user_id: 1,
admin: false,
created: new Date().toISOString(),
created: formatISO(new Date()),
}
}
}

View File

@ -1,6 +1,7 @@
import {faker} from '@faker-js/faker'
import {Factory} from '../support/factory'
import {formatISO} from "date-fns"
export class UserFactory extends Factory {
static table = 'users'
@ -14,8 +15,8 @@ export class UserFactory extends Factory {
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.', // 1234
status: 0,
issuer: 'local',
created: now.toISOString(),
updated: now.toISOString(),
created: formatISO(now),
updated: formatISO(now)
}
}
}

View File

@ -1,4 +1,5 @@
import {Factory} from '../support/factory'
import {formatISO} from "date-fns"
export class UserProjectFactory extends Factory {
static table = 'users_projects'
@ -11,8 +12,8 @@ export class UserProjectFactory extends Factory {
project_id: 1,
user_id: 1,
right: 0,
created: now.toISOString(),
updated: now.toISOString(),
created: formatISO(now),
updated: formatISO(now)
}
}
}

View File

@ -4,32 +4,26 @@
import {UserFactory} from '../factories/user'
export function login(user, cacheAcrossSpecs = false) {
if (!user) {
throw new Error('Needs user')
}
// Caching session when logging in via page visit
cy.session(`user__${user.username}`, () => {
cy.request('POST', `${Cypress.env('API_URL')}/login`, {
username: user.username,
password: '1234',
}).then(({ body }) => {
window.localStorage.setItem('token', body.token)
let token
before(() => {
const users = UserFactory.create(1)
cy.request('POST', `${Cypress.env('API_URL')}/login`, {
username: users[0].username,
password: '1234',
})
.its('body')
.then(r => {
token = r.token
})
}, {
cacheAcrossSpecs,
})
}
})
export function createFakeUserAndLogin() {
let user
before(() => {
user = UserFactory.create(1)[0]
beforeEach(() => {
cy.log(`Using token ${token} to make authenticated requests`)
cy.visit('/', {
onBeforeLoad(win) {
win.localStorage.setItem('token', token)
},
})
beforeEach(() => {
login(user, true)
})
return user
}
})

View File

@ -34,4 +34,38 @@
// 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)
})
})
})

10
cypress/tsconfig.json Normal file
View File

@ -0,0 +1,10 @@
{
"extends": "@vue/tsconfig/tsconfig.web.json",
"include": ["./integration/**/*", "./support/**/*"],
"compilerOptions": {
"isolatedModules": false,
"target": "es5",
"lib": ["es5", "dom"],
"types": ["cypress"]
}
}

9
env.config.d.ts vendored
View File

@ -1,9 +0,0 @@
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
View File

@ -1,12 +1,3 @@
/// <reference types="vite/client" />
/// <reference types="vite-svg-loader" />
/// <reference types="cypress" />
/// <reference types="@histoire/plugin-vue/components" />
interface ImportMetaEnv {
readonly VITE_IS_ONLINE: boolean
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
/// <reference types="cypress" />

View File

@ -1,34 +0,0 @@
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',
},
})

View File

@ -9,9 +9,13 @@
<link rel="icon" href="/favicon.ico">
<link rel="apple-touch-icon" href="/images/icons/apple-touch-icon-180x180.png"/>
<link rel="preload" crossorigin="anonymous" href="/src/assets/fonts/OpenSans[wght].woff2" as="font">
<link rel="preload" crossorigin="anonymous" href="/src/assets/fonts/OpenSans-Italic[wght].woff2" as="font">
<link rel="preload" crossorigin="anonymous" href="/src/assets/fonts/Quicksand[wght].woff2" as="font">
<link rel="preload" crossorigin="anonymous" href="/fonts/open-sans-v15-latin-700italic.woff2" as="font">
<link rel="preload" crossorigin="anonymous" href="/fonts/open-sans-v15-latin-italic.woff2" as="font">
<link rel="preload" crossorigin="anonymous" href="/fonts/quicksand-v7-latin-500.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>
<body>
<noscript>

View File

@ -1,91 +1,63 @@
{
"name": "vikunja-frontend",
"description": "The todo app to organize your life.",
"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"
],
"private": true,
"scripts": {
"serve": "vite",
"preview": "vite preview --port 4173",
"preview:dev": "vite preview --outDir dist-dev --mode development --port 4173",
"serve:dist-dev": "node scripts/serve-dist.js",
"serve:dist": "vite preview --port 4173",
"build": "vite build && workbox copyLibraries dist/",
"build:modern-only": "BUILD_MODERN_ONLY=true vite build && workbox copyLibraries dist/",
"build:dev": "vite build --mode development --outDir dist-dev/",
"build:dev": "vite build -m development --outDir dist-dev/",
"lint": "eslint --ignore-pattern '*.test.*' ./src --ext .vue,.js,.ts",
"test:e2e": "start-server-and-test preview http://127.0.0.1:4173 'cypress run --e2e --browser chrome'",
"test:e2e-record": "start-server-and-test preview http://127.0.0.1:4173 'cypress run --e2e --browser chrome --record'",
"test:e2e-dev-dev": "start-server-and-test preview:dev http://127.0.0.1:4173 'cypress open --e2e'",
"test:e2e-dev": "start-server-and-test preview http://127.0.0.1:4173 'cypress open --e2e'",
"test:unit": "vitest",
"cypress:open": "cypress open",
"test:unit": "vitest --run",
"test:unit-watch": "vitest watch",
"test:frontend": "cypress run",
"typecheck": "vue-tsc --noEmit && vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
"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"
"browserslist:update": "npx browserslist@latest --update-db"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "6.2.1",
"@fortawesome/free-regular-svg-icons": "6.2.1",
"@fortawesome/free-solid-svg-icons": "6.2.1",
"@fortawesome/fontawesome-svg-core": "6.2.0",
"@fortawesome/free-regular-svg-icons": "6.2.0",
"@fortawesome/free-solid-svg-icons": "6.2.0",
"@fortawesome/vue-fontawesome": "3.0.2",
"@github/hotkey": "2.0.1",
"@infectoone/vue-ganttastic": "2.1.3",
"@intlify/unplugin-vue-i18n": "0.8.1",
"@kyvg/vue3-notification": "2.8.0",
"@sentry/tracing": "7.31.1",
"@sentry/vue": "7.31.1",
"@infectoone/vue-ganttastic": "2.1.2",
"@kyvg/vue3-notification": "2.6.1",
"@sentry/tracing": "7.19.0",
"@sentry/vue": "7.19.0",
"@types/is-touch-device": "1.0.0",
"@types/lodash.clonedeep": "4.5.7",
"@types/sortablejs": "1.15.0",
"@vueuse/core": "9.11.0",
"axios": "1.2.2",
"@vueuse/core": "9.5.0",
"@vueuse/router": "9.5.0",
"axios": "0.27.2",
"blurhash": "2.0.4",
"bulma-css-variables": "0.9.33",
"camel-case": "4.1.2",
"codemirror": "5.65.11",
"codemirror": "5.65.9",
"date-fns": "2.29.3",
"dayjs": "1.11.7",
"dompurify": "2.4.3",
"dayjs": "1.11.6",
"dompurify": "2.4.1",
"easymde": "2.18.0",
"fast-deep-equal": "3.1.3",
"flatpickr": "4.6.13",
"flexsearch": "0.7.21",
"floating-vue": "2.0.0-beta.20",
"focus-within": "3.0.2",
"highlight.js": "11.7.0",
"highlight.js": "11.6.0",
"is-touch-device": "1.0.1",
"lodash.clonedeep": "4.5.0",
"lodash.debounce": "4.0.8",
"marked": "4.2.12",
"marked": "4.2.2",
"minimist": "1.2.7",
"pinia": "2.0.29",
"pinia": "2.0.23",
"register-service-worker": "1.7.2",
"snake-case": "3.0.4",
"sortablejs": "1.15.0",
"ufo": "1.0.1",
"ufo": "0.8.6",
"vue": "3.2.45",
"vue-advanced-cropper": "2.8.8",
"vue-advanced-cropper": "2.8.6",
"vue-flatpickr-component": "11.0.1",
"vue-i18n": "9.2.2",
"vue-router": "4.1.6",
@ -93,54 +65,50 @@
"zhyswan-vuedraggable": "4.1.3"
},
"devDependencies": {
"@4tw/cypress-drag-drop": "2.2.3",
"@cypress/vite-dev-server": "5.0.2",
"@cypress/vue": "5.0.3",
"@4tw/cypress-drag-drop": "2.2.1",
"@cypress/vite-dev-server": "4.0.1",
"@cypress/vue": "5.0.1",
"@faker-js/faker": "7.6.0",
"@histoire/plugin-screenshot": "0.12.4",
"@histoire/plugin-vue": "0.12.4",
"@rushstack/eslint-patch": "1.2.0",
"@types/codemirror": "5.60.6",
"@types/codemirror": "5.60.5",
"@types/dompurify": "2.4.0",
"@types/flexsearch": "0.7.3",
"@types/focus-within": "1.0.1",
"@types/lodash.debounce": "4.0.7",
"@types/marked": "4.0.8",
"@types/node": "18.11.18",
"@types/marked": "4.0.7",
"@types/node": "18.11.9",
"@types/postcss-preset-env": "7.7.0",
"@typescript-eslint/eslint-plugin": "5.48.2",
"@typescript-eslint/parser": "5.48.2",
"@vitejs/plugin-legacy": "3.0.1",
"@vitejs/plugin-vue": "4.0.0",
"@typescript-eslint/eslint-plugin": "5.42.1",
"@typescript-eslint/parser": "5.42.1",
"@vitejs/plugin-legacy": "2.3.1",
"@vitejs/plugin-vue": "3.2.0",
"@vue/eslint-config-typescript": "11.0.2",
"@vue/test-utils": "2.2.7",
"@vue/test-utils": "2.2.2",
"@vue/tsconfig": "0.1.3",
"autoprefixer": "10.4.13",
"browserslist": "4.21.4",
"caniuse-lite": "1.0.30001445",
"caniuse-lite": "1.0.30001430",
"csstype": "3.1.1",
"cypress": "12.3.0",
"esbuild": "0.17.2",
"eslint": "8.32.0",
"eslint-plugin-vue": "9.9.0",
"happy-dom": "8.1.4",
"histoire": "0.12.4",
"netlify-cli": "12.7.2",
"postcss": "8.4.21",
"postcss-easing-gradients": "3.0.1",
"postcss-easings": "3.0.1",
"postcss-preset-env": "7.8.3",
"rollup": "3.10.0",
"rollup-plugin-visualizer": "5.9.0",
"sass": "1.57.1",
"start-server-and-test": "1.15.2",
"typescript": "4.9.4",
"vite": "4.0.4",
"vite-plugin-pwa": "0.14.1",
"vite-svg-loader": "4.0.0",
"vitest": "0.27.2",
"vue-tsc": "1.0.24",
"wait-on": "7.0.1",
"cypress": "11.0.1",
"esbuild": "0.15.13",
"eslint": "8.27.0",
"eslint-plugin-vue": "9.7.0",
"express": "4.18.2",
"happy-dom": "7.6.6",
"netlify-cli": "12.1.0",
"postcss": "8.4.19",
"postcss-preset-env": "7.8.2",
"rollup": "3.3.0",
"rollup-plugin-visualizer": "5.8.3",
"sass": "1.56.1",
"typescript": "4.8.4",
"vite": "3.2.3",
"vite-plugin-pwa": "0.13.3",
"vite-svg-loader": "3.6.0",
"vitest": "0.25.1",
"vue-tsc": "1.0.9",
"wait-on": "6.0.1",
"workbox-cli": "6.5.4"
}
},
"license": "AGPL-3.0-or-later",
"packageManager": "pnpm@7.15.0"
}

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.

View File

@ -1,18 +1,15 @@
const { exec } = require('child_process')
function createSlug(string) {
return String(string)
.trim()
.normalize('NFKD')
.toLowerCase()
.replace(/[.\s/]/g, '-')
.replace(/[^A-Za-z\d-]/g, '')
}
const {exec} = require('child_process')
const axios = require('axios')
const BOT_USER_ID = 513
const giteaToken = process.env.GITEA_TOKEN
const siteId = process.env.NETLIFY_SITE_ID
const branchSlug = createSlug(process.env.DRONE_SOURCE_BRANCH)
const branchSlug = String(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 prIssueCommentsUrl = `https://kolaente.dev/api/v1/repos/vikunja/frontend/issues/${prNumber}/comments`
@ -38,7 +35,7 @@ const promiseExec = cmd => {
stdout = await promiseExec(`./node_modules/.bin/netlify deploy --alias ${alias}`)
console.log(stdout)
const data = await fetch(prIssueCommentsUrl).then(response => response.json())
const {data} = await axios.get(prIssueCommentsUrl)
const hasComment = data.some(c => c.user.id === BOT_USER_ID)
if (hasComment) {
@ -46,7 +43,8 @@ const promiseExec = cmd => {
return
}
const message = `
await axios.post(prIssueCommentsUrl, {
body: `
Hi ${process.env.DRONE_COMMIT_AUTHOR}!
Thank you for creating a PR!
@ -59,25 +57,14 @@ You will need to manually connect this to an api running somehwere. The easiest
Have a nice day!
> Beep boop, I'm a bot.
`
`,
}, {
headers: {
'Content-Type': 'application/json',
'accept': 'application/json',
'Authorization': `token ${giteaToken}`,
},
})
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}`)
}
console.log(`Preview comment sent successfully to PR #${prNumber}!`)
})()

View File

@ -1 +1 @@
24df869e7a9282c76c9e1883071a39c0b11a53a57da68b37f2b918df25b1ae0f1b403e38a29c9cb694575bb9a7b52b6e ./scripts/deploy-preview-netlify.js
bb46342a0a08105b340ba7976cff9d80ef89901120ec0639669caa70bb7d2dbc43e78b1f635a7654ab2456e8358c98a4 ./scripts/deploy-preview-netlify.js

View File

@ -1,56 +0,0 @@
#!/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"

View File

@ -1,161 +0,0 @@
#!/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

16
scripts/serve-dist.js Normal file
View File

@ -0,0 +1,16 @@
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.projecten(port, '127.0.0.1', () => {
console.log(`Serving files from ${p}`)
console.log(`Server started on port ${port}`)
})

View File

@ -15,8 +15,9 @@
</template>
<script lang="ts" setup>
import {computed, watch} from 'vue'
import {useRoute, useRouter} from 'vue-router'
import {computed, watch, type Ref} from 'vue'
import {useRouter} from 'vue-router'
import {useRouteQuery} from '@vueuse/router'
import {useI18n} from 'vue-i18n'
import isTouchDevice from 'is-touch-device'
import {success} from '@/message'
@ -40,7 +41,6 @@ import {useAuthStore} from './stores/auth'
const baseStore = useBaseStore()
const authStore = useAuthStore()
const router = useRouter()
const route = useRoute()
useBodyClass('is-touch', isTouchDevice())
const keyboardShortcutsActive = computed(() => baseStore.keyboardShortcutsActive)
@ -51,9 +51,9 @@ const authLinkShare = computed(() => authStore.authLinkShare)
const {t} = useI18n({useScope: 'global'})
// setup account deletion verification
const accountDeletionConfirm = computed(() => route.query?.accountDeletionConfirm as (string | undefined))
const accountDeletionConfirm = useRouteQuery('accountDeletionConfirm') as Ref<null | string>
watch(accountDeletionConfirm, async (accountDeletionConfirm) => {
if (accountDeletionConfirm === undefined) {
if (accountDeletionConfirm === null) {
return
}
@ -64,9 +64,9 @@ watch(accountDeletionConfirm, async (accountDeletionConfirm) => {
}, { immediate: true })
// setup password reset redirect
const userPasswordReset = computed(() => route.query?.userPasswordReset as (string | undefined))
const userPasswordReset = useRouteQuery('userPasswordReset') as Ref<null | string>
watch(userPasswordReset, (userPasswordReset) => {
if (userPasswordReset === undefined) {
if (userPasswordReset === null) {
return
}
@ -75,9 +75,9 @@ watch(userPasswordReset, (userPasswordReset) => {
}, { immediate: true })
// setup email verification redirect
const userEmailConfirm = computed(() => route.query?.userEmailConfirm as (string | undefined))
const userEmailConfirm = useRouteQuery('userEmailConfirm') as Ref<null | string>
watch(userEmailConfirm, (userEmailConfirm) => {
if (userEmailConfirm === undefined) {
if (userEmailConfirm === null) {
return
}

View File

@ -1,58 +0,0 @@
<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>

View File

@ -61,12 +61,12 @@ export type BaseButtonTypes = typeof BASE_BUTTON_TYPES_MAP[keyof typeof BASE_BUT
import {unrefElement} from '@vueuse/core'
import {ref, type HTMLAttributes} from 'vue'
import type {RouteLocationRaw} from 'vue-router'
import type {RouteLocationNamedRaw} from 'vue-router'
export interface BaseButtonProps extends HTMLAttributes {
type?: BaseButtonTypes
disabled?: boolean
to?: RouteLocationRaw
to?: RouteLocationNamedRaw
href?: string
}

View File

@ -1,179 +0,0 @@
<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>

View File

@ -54,8 +54,8 @@
</p>
<modal
:enabled="showHowItWorks"
@close="() => showHowItWorks = false"
:enabled="showHowItWorks"
transition-name="fade"
:overflow="true"
variant="hint-modal"

View File

@ -1,16 +1,16 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useNow } from '@vueuse/core'
import {computed} from 'vue'
import {useNow} from '@vueuse/core'
import LogoFull from '@/assets/logo-full.svg?component'
import LogoFullPride from '@/assets/logo-full-pride.svg?component'
import LogoFull from '@/assets/logo-full.svg?url'
import LogoFullPride from '@/assets/logo-full-pride.svg?url'
const now = useNow()
const Logo = computed(() => now.value.getMonth() === 5 ? LogoFullPride : LogoFull)
const logoUrl = computed(() => now.value.getMonth() === 5 ? LogoFullPride : LogoFull)
</script>
<template>
<Logo alt="Vikunja" class="logo" />
<img alt="Vikunja" :src="logoUrl" class="logo" />
</template>
<style lang="scss" scoped>

View File

@ -39,7 +39,7 @@
</router-view>
<modal
:enabled="Boolean(currentModal)"
v-if="currentModal"
@close="closeModal()"
variant="scrolling"
class="task-detail-view-modal"
@ -159,20 +159,27 @@ labelStore.loadAllLabels()
.app-content {
z-index: 10;
position: relative;
padding: 1.5rem 0.5rem 1rem;
padding-top: 1rem;
@media screen and (max-width: $tablet) {
margin-left: 0;
min-height: calc(100vh - 4rem);
}
@media screen and (min-width: $tablet) {
@media screen {
padding: $navbar-height + 1.5rem 1.5rem 1rem 1.5rem;
}
&.is-menu-enabled {
@media screen and (min-width: $tablet) {
@media screen and (max-width: $tablet) {
margin-left: 0;
padding-top: 1.5rem;
min-height: calc(100vh - 4rem);
}
@media screen {
&.is-menu-enabled {
margin-left: $navbar-width;
@media screen and (max-width: $tablet) {
min-width: 100%;
margin-left: 0;
}
}
}
@ -230,4 +237,6 @@ labelStore.loadAllLabels()
.content-auth.z-unset {
z-index: unset;
}
@include modal-transition();
</style>

View File

@ -7,7 +7,7 @@
<ul class="menu-project">
<li>
<router-link :to="{ name: 'home'}" v-shortcut="'g o'">
<span class="menu-item-icon icon">
<span class="icon">
<icon icon="calendar"/>
</span>
{{ $t('navigation.overview') }}
@ -15,7 +15,7 @@
</li>
<li>
<router-link :to="{ name: 'tasks.range'}" v-shortcut="'g u'">
<span class="menu-item-icon icon">
<span class="icon">
<icon :icon="['far', 'calendar-alt']"/>
</span>
{{ $t('navigation.upcoming') }}
@ -23,7 +23,7 @@
</li>
<li>
<router-link :to="{ name: 'namespaces.index'}" v-shortcut="'g n'">
<span class="menu-item-icon icon">
<span class="icon">
<icon icon="layer-group"/>
</span>
{{ $t('namespace.title') }}
@ -31,7 +31,7 @@
</li>
<li>
<router-link :to="{ name: 'labels.index'}" v-shortcut="'g a'">
<span class="menu-item-icon icon">
<span class="icon">
<icon icon="tags"/>
</span>
{{ $t('label.title') }}
@ -39,7 +39,7 @@
</li>
<li>
<router-link :to="{ name: 'teams.index'}" v-shortcut="'g m'">
<span class="menu-item-icon icon">
<span class="icon">
<icon icon="users"/>
</span>
{{ $t('team.title') }}
@ -63,7 +63,7 @@
/>
<span class="name">{{ namespaceTitles[nk] }}</span>
<div
class="icon menu-item-icon is-small toggle-projects-icon pl-2"
class="icon is-small toggle-projects-icon pl-2"
:class="{'active': typeof projectsVisible[n.id] !== 'undefined' ? projectsVisible[n.id] : true}"
>
<icon icon="chevron-down"/>
@ -72,7 +72,7 @@
({{ namespaceProjectsCount[nk] }})
</span>
</BaseButton>
<namespace-settings-dropdown class="menu-list-dropdown" :namespace="n" v-if="n.id > 0"/>
<namespace-settings-dropdown :namespace="n" v-if="n.id > 0"/>
</div>
<!--
NOTE: a v-model / computed setter is not possible, since the updateActiveProjects function
@ -111,11 +111,11 @@
class="project-menu-link"
:class="{'router-link-exact-active': currentProject.id === l.id}"
>
<span class="icon menu-item-icon handle">
<span class="icon handle">
<icon icon="grip-lines"/>
</span>
<ColorBubble
v-if="l.hexColor !== ''"
v-if="l.hexColor !== ''"
:color="l.hexColor"
class="mr-1"
/>
@ -128,13 +128,7 @@
>
<icon :icon="l.isFavorite ? 'star' : ['far', 'star']"/>
</BaseButton>
<project-settings-dropdown class="menu-list-dropdown" :project="l" v-if="l.id > 0">
<template #trigger="{toggleOpen}">
<BaseButton class="menu-list-dropdown-trigger" @click="toggleOpen">
<icon icon="ellipsis-h" class="icon"/>
</BaseButton>
</template>
</list-settings-dropdown>
<project-settings-dropdown :project="l" v-if="l.id > 0"/>
<span class="project-setting-spacer" v-else></span>
</li>
</template>
@ -286,18 +280,6 @@ $vikunja-nav-background: var(--site-background);
$vikunja-nav-color: var(--grey-700);
$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 {
background: $vikunja-nav-background;
color: $vikunja-nav-color;
@ -321,228 +303,248 @@ $vikunja-nav-selected-width: 0.4rem;
transform: translateX(0);
transition: transform $transition-duration ease-out;
}
}
// these are general menu styles
// should be in own components
.menu {
.menu-label,
.menu-list .list-menu-link,
.menu-list a {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
.menu {
.menu-label {
font-size: 1rem;
font-weight: 700;
font-weight: bold;
font-family: $vikunja-font;
color: $vikunja-nav-color;
font-weight: 500;
min-height: 2.5rem;
padding-top: 0;
padding-left: $navbar-padding;
.color-bubble {
height: 12px;
flex: 0 0 12px;
overflow: hidden;
}
}
.menu-list {
li {
height: 44px;
.menu-label,
.menu-project .project-menu-link,
.menu-project a {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
&:hover {
background: var(--white);
.project-menu-title {
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
.menu-list-dropdown {
opacity: 0;
transition: $transition;
.color-bubble {
height: 12px;
flex: 0 0 12px;
}
&:hover .menu-list-dropdown {
opacity: 1;
}
.favorite {
margin-left: .25rem;
transition: opacity $transition, color $transition;
opacity: 0;
&:hover,
&.is-favorite {
color: var(--warning);
}
}
.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;
}
}
}
}
.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);
.favorite.is-favorite,
.project-menu:hover .favorite {
opacity: 1;
}
&.active svg {
transform: rotate(0deg);
opacity: 0;
.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-projects-icon {
svg {
transition: all $transition;
transform: rotate(90deg);
opacity: 1;
}
&.active svg {
transform: rotate(0deg);
opacity: 0;
}
}
&:hover .toggle-projects-icon svg {
opacity: 1;
}
&:not(.has-menu) .toggle-projects-icon {
padding-right: 1rem;
}
}
.menu-label,
.nsettings,
.menu-project .project-menu-link,
.menu-project a {
color: $vikunja-nav-color;
}
.menu-project {
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-project-move {
transition: transform $transition-duration;
}
.ghost {
background: var(--grey-200);
* {
opacity: 0;
}
}
a:hover {
background: transparent;
}
.project-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-projects {
padding-top: math.div($navbar-padding, 2);
}
.icon {
color: var(--grey-400) !important;
}
}
&:hover .toggle-lists-icon svg {
opacity: 1;
}
.top-menu {
margin-top: math.div($navbar-padding, 2);
&:not(.has-menu) .toggle-lists-icon {
padding-right: 1rem;
.menu-project {
li {
font-weight: 500;
font-family: $vikunja-font;
}
.project-menu-link, li > a {
padding-left: 2rem;
display: inline-block;
.icon {
padding-bottom: .25rem;
}
}
}
}
}

View File

@ -1,26 +0,0 @@
<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>

View File

@ -1,14 +0,0 @@
<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>

View File

@ -37,7 +37,6 @@
<script setup lang="ts">
import {computed, ref, toRef, watch} from 'vue'
import {createRandomID} from '@/helpers/randomId'
import XButton from '@/components/input/button.vue'
const DEFAULT_COLORS = [
'#1973ff',

View File

@ -4,7 +4,7 @@
{{ date === null ? chooseDateLabel : formatDateShort(date) }}
</BaseButton>
<CustomTransition name="fade">
<transition name="fade">
<div v-if="show" class="datepicker-popup" ref="datepickerPopup">
<BaseButton
@ -84,7 +84,7 @@
{{ $t('misc.confirm') }}
</x-button>
</div>
</CustomTransition>
</transition>
</div>
</template>
@ -94,7 +94,6 @@ import flatPickr from 'vue-flatpickr-component'
import 'flatpickr/dist/flatpickr.css'
import BaseButton from '@/components/base/BaseButton.vue'
import CustomTransition from '@/components/misc/CustomTransition.vue'
import {formatDate, formatDateShort} from '@/helpers/time/formatDate'
import {calculateDayInterval} from '@/helpers/time/calculateDayInterval'

View File

@ -9,7 +9,6 @@ export function createEasyMDEConfig({ placeholder, uploadImage, imageUploadFunct
uploadImage,
imageUploadFunction,
minHeight: '150px',
sideBySideFullscreen: false,
toolbar: [
{
name: 'heading-1',

View File

@ -36,7 +36,7 @@
</div>
</div>
<CustomTransition name="fade">
<transition name="fade">
<div class="search-results" :class="{'search-results-inline': inline}" v-if="searchResultsVisible">
<BaseButton
class="search-result-button is-fullwidth"
@ -78,7 +78,8 @@
</span>
</BaseButton>
</div>
</CustomTransition>
</transition>
</div>
</template>
@ -89,7 +90,6 @@ import {useI18n} from 'vue-i18n'
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
import BaseButton from '@/components/base/BaseButton.vue'
import CustomTransition from '@/components/misc/CustomTransition.vue'
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.

View File

@ -1,11 +0,0 @@
<script lang="ts" setup>
import Card from './card.vue'
</script>
<template>
<Story :layout="{ type: 'grid', width: '200px' }">
<Card>
Card content
</Card>
</Story>
</template>

View File

@ -1,67 +0,0 @@
<template>
<transition :name="name">
<slot />
</transition>
</template>
<script setup lang="ts">
defineProps<{
name: 'flash-background' | 'fade' | 'width' | 'modal'
}>()
</script>
<style scoped lang="scss">
$flash-background-duration: 750ms;
.flash-background-enter-from,
.flash-background-enter-active {
animation: flash-background $flash-background-duration ease 1;
}
@keyframes flash-background {
0% {
background: var(--primary-light);
}
100% {
background: transparent;
}
}
@media (prefers-reduced-motion: reduce) {
@keyframes flash-background {
0% {
background: transparent;
}
}
}
.fade-enter-active,
.fade-leave-active {
transition: opacity $transition-duration;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.width-enter-active,
.width-leave-active {
transition: width $transition-duration;
}
.width-enter-from,
.width-leave-to {
width: 0;
}
.modal-enter,
.modal-leave-active {
opacity: 0;
}
.modal-enter .modal-container,
.modal-leave-active .modal-container {
transform: scale(0.9);
}
</style>

View File

@ -6,13 +6,13 @@
</BaseButton>
</slot>
<CustomTransition name="fade">
<transition name="fade">
<div class="dropdown-menu" v-if="open">
<div class="dropdown-content">
<slot :close="close"></slot>
</div>
</div>
</CustomTransition>
</transition>
</div>
</template>
@ -21,7 +21,6 @@ import {ref, type PropType} from 'vue'
import {onClickOutside} from '@vueuse/core'
import type {IconProp} from '@fortawesome/fontawesome-svg-core'
import CustomTransition from '@/components/misc/CustomTransition.vue'
import BaseButton from '@/components/base/BaseButton.vue'
defineProps({

View File

@ -1,7 +1,7 @@
<template>
<Teleport to="body">
<!-- FIXME: transition should not be included in the modal -->
<CustomTransition :name="transitionName" appear>
<transition :name="transitionName">
<section
v-if="enabled"
class="modal-mask"
@ -59,7 +59,7 @@
</div>
</div>
</section>
</CustomTransition>
</transition>
</Teleport>
</template>
@ -70,7 +70,6 @@ export default {
</script>
<script lang="ts" setup>
import CustomTransition from '@/components/misc/CustomTransition.vue'
import BaseButton from '@/components/base/BaseButton.vue'
import {ref, useAttrs, watchEffect} from 'vue'
import {useScrollLock} from '@vueuse/core'

View File

@ -29,7 +29,7 @@
</card>
</no-auth-wrapper>
</section>
<CustomTransition name="fade">
<transition name="fade">
<section class="vikunja-loading" v-if="showLoading">
<Logo class="logo"/>
<p>
@ -37,7 +37,7 @@
{{ $t('ready.loading') }}
</p>
</section>
</CustomTransition>
</transition>
</template>
<script lang="ts" setup>
@ -47,7 +47,6 @@ import {useRouter, useRoute} from 'vue-router'
import Logo from '@/assets/logo.svg?component'
import ApiConfig from '@/components/misc/api-config.vue'
import Message from '@/components/misc/message.vue'
import CustomTransition from '@/components/misc/CustomTransition.vue'
import NoAuthWrapper from '@/components/misc/no-auth-wrapper.vue'
import {ERROR_NO_API_URL} from '@/helpers/checkAndSetApiUrl'

View File

@ -1,13 +1,5 @@
<template>
<dropdown>
<template #trigger="triggerProps">
<slot name="trigger" v-bind="triggerProps">
<BaseButton class="dropdown-trigger" @click="triggerProps.toggleOpen">
<icon icon="ellipsis-h" class="icon"/>
</BaseButton>
</slot>
</template>
<template v-if="namespace.isArchived">
<dropdown-item
:to="{ name: 'namespace.settings.archive', params: { id: namespace.id } }"
@ -64,7 +56,6 @@
<script setup lang="ts">
import {ref, onMounted, type PropType} from 'vue'
import BaseButton from '@/components/base/BaseButton.vue'
import Dropdown from '@/components/misc/dropdown.vue'
import DropdownItem from '@/components/misc/dropdown-item.vue'
import Subscription from '@/components/misc/subscription.vue'
@ -94,10 +85,3 @@ function setSubscriptionInStore(sub: ISubscription) {
})
}
</script>
<style scoped lang="scss">
.dropdown-trigger {
padding: 0.5rem;
display: flex;
}
</style>

View File

@ -7,7 +7,7 @@
</BaseButton>
</div>
<CustomTransition name="fade">
<transition name="fade">
<div class="notifications-project" v-if="showNotifications" ref="popup">
<span class="head">{{ $t('notification.title') }}</span>
<div
@ -42,7 +42,7 @@
</span>
</p>
</div>
</CustomTransition>
</transition>
</div>
</template>
@ -52,7 +52,6 @@ import {useRouter} from 'vue-router'
import NotificationService from '@/services/notification'
import BaseButton from '@/components/base/BaseButton.vue'
import CustomTransition from '@/components/misc/CustomTransition.vue'
import User from '@/components/misc/user.vue'
import { NOTIFICATION_NAMES as names, type INotification} from '@/modelTypes/INotification'
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'

View File

@ -14,11 +14,11 @@
{{ $t('filters.title') }}
</x-button>
<modal
@close="() => modalOpen = false"
:enabled="modalOpen"
transition-name="fade"
:overflow="true"
variant="hint-modal"
@close="() => modalOpen = false"
>
<filters
:has-title="true"

View File

@ -0,0 +1,222 @@
<template>
<router-link
:class="{
'has-light-text': !colorIsDark(project.hexColor) || background !== null,
'has-background': blurHashUrl !== '' || background !== null,
}"
:style="{
'background-color': project.hexColor,
'background-image': blurHashUrl !== null ? `url(${blurHashUrl})` : false,
}"
:to="{ name: 'project.index', params: { projectId: project.id} }"
class="project-card"
v-if="project !== null && (showArchived ? true : !project.isArchived)"
>
<div
class="project-background background-fade-in"
:class="{'is-visible': background}"
:style="{'background-image': background !== null ? `url(${background})` : undefined}"
/>
<div class="project-content">
<span class="is-archived" v-if="project.isArchived">
{{ $t('namespace.archived') }}
</span>
<BaseButton
v-else
:class="{'is-favorite': project.isFavorite}"
@click.stop="projectStore.toggleProjectFavorite(project)"
class="favorite"
>
<icon :icon="project.isFavorite ? 'star' : ['far', 'star']"/>
</BaseButton>
<div class="title">{{ project.title }}</div>
</div>
</router-link>
</template>
<script lang="ts" setup>
import {type PropType, ref, watch} from 'vue'
import ProjectService from '@/services/project'
import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash'
import {colorIsDark} from '@/helpers/color/colorIsDark'
import BaseButton from '@/components/base/BaseButton.vue'
import type {IProject} from '@/modelTypes/IProject'
import {useProjectStore} from '@/stores/projects'
const background = ref<string | null>(null)
const backgroundLoading = ref(false)
const blurHashUrl = ref('')
const props = defineProps({
project: {
type: Object as PropType<IProject>,
required: true,
},
showArchived: {
default: false,
type: Boolean,
},
})
watch(props.project, loadBackground, {immediate: true})
async function loadBackground() {
if (props.project === null || !props.project.backgroundInformation || backgroundLoading.value) {
return
}
const blurHash = await getBlobFromBlurHash(props.project.backgroundBlurHash)
if (blurHash) {
blurHashUrl.value = window.URL.createObjectURL(blurHash)
}
backgroundLoading.value = true
const projectService = new ProjectService()
try {
background.value = await projectService.background(props.project)
} finally {
backgroundLoading.value = false
}
}
const projectStore = useProjectStore()
</script>
<style lang="scss" scoped>
.project-card {
cursor: pointer;
width: calc((100% - #{($projects-per-row - 1) * 1rem}) / #{$projects-per-row});
height: $project-height;
background: var(--white);
margin: 0 $project-spacing $project-spacing 0;
border-radius: $radius;
box-shadow: var(--shadow-sm);
transition: box-shadow $transition;
position: relative;
overflow: hidden;
&.has-light-text .title {
color: var(--grey-100) !important;
}
&.has-background,
.project-background {
background-size: cover;
background-repeat: no-repeat;
background-position: center;
}
&.has-background .title {
text-shadow: 0 0 10px var(--black), 1px 1px 5px var(--grey-700), -1px -1px 5px var(--grey-700);
color: var(--white);
}
.project-background {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
&:hover {
box-shadow: var(--shadow-md);
}
&:active,
&:focus,
&:focus:not(:active) {
box-shadow: var(--shadow-xs) !important;
}
@media screen and (min-width: $widescreen) {
&:nth-child(#{$projects-per-row}n) {
margin-right: 0;
}
}
@media screen and (max-width: $widescreen) and (min-width: $tablet) {
$projects-per-row: 3;
& {
width: calc((100% - #{($projects-per-row - 1) * 1rem}) / #{$projects-per-row});
}
&:nth-child(#{$projects-per-row}n) {
margin-right: 0;
}
}
@media screen and (max-width: $tablet) {
$projects-per-row: 2;
& {
width: calc((100% - #{($projects-per-row - 1) * 1rem}) / #{$projects-per-row});
}
&:nth-child(#{$projects-per-row}n) {
margin-right: 0;
}
}
@media screen and (max-width: $mobile) {
$projects-per-row: 1;
& {
width: 100%;
margin-right: 0;
}
}
.project-content {
display: flex;
align-content: space-between;
flex-wrap: wrap;
padding: 1rem;
position: absolute;
height: 100%;
width: 100%;
.is-archived {
font-size: .75rem;
}
.favorite {
margin-left: auto;
transition: opacity $transition, color $transition;
opacity: 0;
display: block;
&:hover,
&.is-favorite {
color: var(--warning);
}
}
.favorite.is-favorite,
&:hover .favorite {
opacity: 1;
}
.title {
align-self: flex-end;
font-family: $vikunja-font;
font-weight: 400;
font-size: 1.5rem;
color: var(--text);
width: 100%;
margin-bottom: 0;
max-height: calc(100% - 2rem); // 1rem padding, 1rem height of the "is archived" badge
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
}
}
</style>

View File

@ -1,13 +1,5 @@
<template>
<dropdown>
<template #trigger="triggerProps">
<slot name="trigger" v-bind="triggerProps">
<BaseButton class="dropdown-trigger" @click="triggerProps.toggleOpen">
<icon icon="ellipsis-h" class="icon"/>
</BaseButton>
</slot>
</template>
<template v-if="isSavedFilter(project)">
<dropdown-item
:to="{ name: 'filter.settings.edit', params: { projectId: project.id } }"
@ -86,7 +78,6 @@
<script setup lang="ts">
import {ref, computed, watchEffect, type PropType} from 'vue'
import BaseButton from '@/components/base/BaseButton.vue'
import Dropdown from '@/components/misc/dropdown.vue'
import DropdownItem from '@/components/misc/dropdown-item.vue'
import Subscription from '@/components/misc/subscription.vue'
@ -124,4 +115,4 @@ function setSubscriptionInStore(sub: ISubscription) {
projectStore.setProject(updatedProject)
namespaceStore.setProjectInNamespaceById(updatedProject)
}
</script>
</script>

View File

@ -1,5 +1,5 @@
<template>
<modal :enabled="active" @close="closeQuickActions" :overflow="isNewTaskCommand">
<modal v-if="active" @close="closeQuickActions" :overflow="isNewTaskCommand">
<div class="card quick-actions">
<div class="action-input" :class="{'has-active-cmd': selectedCmd !== null}">
<div class="active-cmd tag" v-if="selectedCmd !== null">

View File

@ -169,19 +169,21 @@
</table>
</div>
<modal
:enabled="showDeleteModal"
@close="showDeleteModal = false"
@submit="remove(projectId)"
>
<template #header>
<span>{{ $t('project.share.links.remove') }}</span>
</template>
<transition name="modal">
<modal
@close="showDeleteModal = false"
@submit="remove(projectId)"
v-if="showDeleteModal"
>
<template #header>
<span>{{ $t('project.share.links.remove') }}</span>
</template>
<template #text>
<p>{{ $t('project.share.links.removeText') }}</p>
</template>
</modal>
<template #text>
<p>{{ $t('project.share.links.removeText') }}</p>
</template>
</modal>
</transition>
</div>
</template>
@ -295,4 +297,6 @@ function getShareLink(hash: string, view: ProjectView = PROJECT_VIEWS.PROJECT) {
.sharables-project:not(.card-content) {
overflow-y: auto
}
@include modal-transition();
</style>

Some files were not shown because too many files have changed in this diff Show More