Compare commits
6 Commits
main
...
feature/ca
Author | SHA1 | Date |
---|---|---|
kolaente | 118efb6234 | |
kolaente | 5a503a31bb | |
kolaente | f52dd5e772 | |
kolaente | 829c95dd2e | |
kolaente | ab4a4a4a2a | |
kolaente | 6fc47dd529 |
517
.drone.yml
|
@ -1,6 +1,5 @@
|
||||||
---
|
---
|
||||||
kind: pipeline
|
kind: pipeline
|
||||||
type: docker
|
|
||||||
name: build
|
name: build
|
||||||
|
|
||||||
trigger:
|
trigger:
|
||||||
|
@ -15,7 +14,6 @@ trigger:
|
||||||
services:
|
services:
|
||||||
- name: api
|
- name: api
|
||||||
image: vikunja/api:unstable
|
image: vikunja/api:unstable
|
||||||
pull: always
|
|
||||||
environment:
|
environment:
|
||||||
VIKUNJA_SERVICE_TESTINGTOKEN: averyLongSecretToSe33dtheDB
|
VIKUNJA_SERVICE_TESTINGTOKEN: averyLongSecretToSe33dtheDB
|
||||||
VIKUNJA_LOG_LEVEL: DEBUG
|
VIKUNJA_LOG_LEVEL: DEBUG
|
||||||
|
@ -24,101 +22,36 @@ steps:
|
||||||
# Disabled until we figure out why it is so slow
|
# Disabled until we figure out why it is so slow
|
||||||
# - name: restore-cache
|
# - name: restore-cache
|
||||||
# image: meltwater/drone-cache:dev
|
# image: meltwater/drone-cache:dev
|
||||||
# pull: always
|
# pull: true
|
||||||
# environment:
|
# environment:
|
||||||
# AWS_ACCESS_KEY_ID:
|
# AWS_ACCESS_KEY_ID:
|
||||||
# from_secret: cache_aws_access_key_id
|
# from_secret: cache_aws_access_key_id
|
||||||
# AWS_SECRET_ACCESS_KEY:
|
# AWS_SECRET_ACCESS_KEY:
|
||||||
# from_secret: cache_aws_secret_access_key
|
# from_secret: cache_aws_secret_access_key
|
||||||
# settings:
|
# settings:
|
||||||
# debug: true
|
|
||||||
# restore: true
|
# restore: true
|
||||||
# bucket: kolaente.dev-drone-dependency-cache
|
# bucket: kolaente.dev-drone-dependency-cache
|
||||||
# endpoint: https://s3.fr-par.scw.cloud
|
# endpoint: https://s3.fr-par.scw.cloud
|
||||||
# region: fr-par
|
# region: fr-par
|
||||||
# path_style: true
|
# path_style: true
|
||||||
# cache_key: '{{ .Repo.Name }}_{{ checksum "pnpm-lock.yaml" }}_{{ arch }}_{{ os }}'
|
# cache_key: '{{ .Repo.Name }}_{{ checksum "yarn.lock" }}_{{ arch }}_{{ os }}'
|
||||||
# mount:
|
# mount:
|
||||||
# - .cache
|
# - '.cache'
|
||||||
|
|
||||||
- name: dependencies
|
- name: dependencies
|
||||||
image: node:20.11.0-alpine
|
image: node:16
|
||||||
pull: always
|
pull: true
|
||||||
environment:
|
environment:
|
||||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
YARN_CACHE_FOLDER: .cache/yarn/
|
||||||
CYPRESS_CACHE_FOLDER: .cache/cypress
|
CYPRESS_CACHE_FOLDER: .cache/cypress/
|
||||||
PUPPETEER_SKIP_DOWNLOAD: true
|
|
||||||
commands:
|
commands:
|
||||||
- corepack enable && pnpm config set store-dir .cache/pnpm
|
- yarn --frozen-lockfile --network-timeout 100000
|
||||||
- pnpm install --fetch-timeout 100000
|
|
||||||
# depends_on:
|
# depends_on:
|
||||||
# - restore-cache
|
# - restore-cache
|
||||||
|
|
||||||
- name: lint
|
|
||||||
image: node:20.11.0-alpine
|
|
||||||
pull: always
|
|
||||||
environment:
|
|
||||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
|
||||||
commands:
|
|
||||||
- corepack enable && pnpm config set store-dir .cache/pnpm
|
|
||||||
- pnpm run lint
|
|
||||||
depends_on:
|
|
||||||
- dependencies
|
|
||||||
|
|
||||||
- name: build-prod
|
|
||||||
image: node:20.11.0-alpine
|
|
||||||
pull: always
|
|
||||||
environment:
|
|
||||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
|
||||||
commands:
|
|
||||||
- corepack enable && pnpm config set store-dir .cache/pnpm
|
|
||||||
- pnpm run build
|
|
||||||
depends_on:
|
|
||||||
- dependencies
|
|
||||||
|
|
||||||
- name: test-unit
|
|
||||||
image: node:20.11.0-alpine
|
|
||||||
pull: always
|
|
||||||
commands:
|
|
||||||
- corepack enable && pnpm config set store-dir .cache/pnpm
|
|
||||||
- pnpm run test:unit
|
|
||||||
depends_on:
|
|
||||||
- dependencies
|
|
||||||
|
|
||||||
- name: typecheck
|
|
||||||
failure: ignore
|
|
||||||
image: node:20.11.0-alpine
|
|
||||||
pull: always
|
|
||||||
environment:
|
|
||||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
|
||||||
commands:
|
|
||||||
- corepack enable && pnpm config set store-dir .cache/pnpm
|
|
||||||
- pnpm run typecheck
|
|
||||||
depends_on:
|
|
||||||
- dependencies
|
|
||||||
|
|
||||||
- name: test-frontend
|
|
||||||
image: cypress/browsers:node18.12.0-chrome107
|
|
||||||
pull: always
|
|
||||||
environment:
|
|
||||||
CYPRESS_API_URL: http://api:3456/api/v1
|
|
||||||
CYPRESS_TEST_SECRET: averyLongSecretToSe33dtheDB
|
|
||||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
|
||||||
CYPRESS_CACHE_FOLDER: .cache/cypress
|
|
||||||
CYPRESS_DEFAULT_COMMAND_TIMEOUT: 60000
|
|
||||||
CYPRESS_RECORD_KEY:
|
|
||||||
from_secret: cypress_project_key
|
|
||||||
commands:
|
|
||||||
- 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
|
|
||||||
depends_on:
|
|
||||||
- build-prod
|
|
||||||
|
|
||||||
# - name: rebuild-cache
|
# - name: rebuild-cache
|
||||||
# image: meltwater/drone-cache:dev
|
# image: meltwater/drone-cache:dev
|
||||||
# pull: always
|
# pull: true
|
||||||
# environment:
|
# environment:
|
||||||
# AWS_ACCESS_KEY_ID:
|
# AWS_ACCESS_KEY_ID:
|
||||||
# from_secret: cache_aws_access_key_id
|
# from_secret: cache_aws_access_key_id
|
||||||
|
@ -130,16 +63,92 @@ steps:
|
||||||
# endpoint: https://s3.fr-par.scw.cloud
|
# endpoint: https://s3.fr-par.scw.cloud
|
||||||
# region: fr-par
|
# region: fr-par
|
||||||
# path_style: true
|
# path_style: true
|
||||||
# cache_key: '{{ .Repo.Name }}_{{ checksum "pnpm-lock.yaml" }}_{{ arch }}_{{ os }}'
|
# cache_key: '{{ .Repo.Name }}_{{ checksum "yarn.lock" }}_{{ arch }}_{{ os }}'
|
||||||
# mount:
|
# mount:
|
||||||
# - .cache
|
# - '.cache'
|
||||||
# depends_on:
|
# depends_on:
|
||||||
# - dependencies
|
# - dependencies
|
||||||
|
|
||||||
|
- name: lint
|
||||||
|
image: node:16
|
||||||
|
pull: true
|
||||||
|
environment:
|
||||||
|
YARN_CACHE_FOLDER: .cache/yarn/
|
||||||
|
CYPRESS_CACHE_FOLDER: .cache/cypress/
|
||||||
|
commands:
|
||||||
|
- yarn run lint
|
||||||
|
depends_on:
|
||||||
|
- dependencies
|
||||||
|
|
||||||
|
- name: build-prod
|
||||||
|
image: node:16
|
||||||
|
pull: true
|
||||||
|
environment:
|
||||||
|
YARN_CACHE_FOLDER: .cache/yarn/
|
||||||
|
commands:
|
||||||
|
- yarn build
|
||||||
|
depends_on:
|
||||||
|
- dependencies
|
||||||
|
|
||||||
|
- name: test-unit
|
||||||
|
image: node:16
|
||||||
|
pull: true
|
||||||
|
commands:
|
||||||
|
- yarn test:unit
|
||||||
|
depends_on:
|
||||||
|
- dependencies
|
||||||
|
|
||||||
|
- name: typecheck
|
||||||
|
failure: ignore
|
||||||
|
image: node:16
|
||||||
|
pull: true
|
||||||
|
commands:
|
||||||
|
- yarn typecheck
|
||||||
|
depends_on:
|
||||||
|
- dependencies
|
||||||
|
|
||||||
|
- name: test-frontend
|
||||||
|
image: cypress/browsers:node16.5.0-chrome94-ff93
|
||||||
|
pull: true
|
||||||
|
environment:
|
||||||
|
CYPRESS_API_URL: http://api:3456/api/v1
|
||||||
|
CYPRESS_TEST_SECRET: averyLongSecretToSe33dtheDB
|
||||||
|
YARN_CACHE_FOLDER: .cache/yarn/
|
||||||
|
CYPRESS_CACHE_FOLDER: .cache/cypress/
|
||||||
|
CYPRESS_DEFAULT_COMMAND_TIMEOUT: 60000
|
||||||
|
commands:
|
||||||
|
- sed -i 's/localhost/api/g' dist/index.html
|
||||||
|
- yarn serve:dist & npx wait-on http://localhost:5000
|
||||||
|
- yarn test:frontend --browser chrome
|
||||||
|
depends_on:
|
||||||
|
- dependencies
|
||||||
|
- build-prod
|
||||||
|
|
||||||
|
- name: upload-test-results
|
||||||
|
image: plugins/s3
|
||||||
|
pull: true
|
||||||
|
settings:
|
||||||
|
bucket: drone-test-results
|
||||||
|
access_key:
|
||||||
|
from_secret: test_results_aws_access_key_id
|
||||||
|
secret_key:
|
||||||
|
from_secret: test_results_aws_secret_access_key
|
||||||
|
endpoint: https://s3.fr-par.scw.cloud
|
||||||
|
region: fr-par
|
||||||
|
path_style: true
|
||||||
|
source: cypress/screenshots/**/**/*
|
||||||
|
strip_prefix: cypress/screenshots/
|
||||||
|
target: /${DRONE_REPO}/${DRONE_PULL_REQUEST}_${DRONE_BRANCH}/${DRONE_BUILD_NUMBER}/
|
||||||
|
depends_on:
|
||||||
|
- test-frontend
|
||||||
|
when:
|
||||||
|
status:
|
||||||
|
- failure
|
||||||
|
- success
|
||||||
|
|
||||||
- name: deploy-preview
|
- name: deploy-preview
|
||||||
image: williamjackson/netlify-cli
|
image: node:16
|
||||||
pull: always
|
pull: true
|
||||||
user: root # The rest runs as root and thus the permissions wouldn't work
|
|
||||||
environment:
|
environment:
|
||||||
NETLIFY_AUTH_TOKEN:
|
NETLIFY_AUTH_TOKEN:
|
||||||
from_secret: netlify_auth_token
|
from_secret: netlify_auth_token
|
||||||
|
@ -148,14 +157,8 @@ steps:
|
||||||
GITEA_TOKEN:
|
GITEA_TOKEN:
|
||||||
from_secret: gitea_token
|
from_secret: gitea_token
|
||||||
commands:
|
commands:
|
||||||
- cp -r dist dist-preview
|
- shasum -a 384 -c ./scripts/deploy-preview-netlify.js.sha384
|
||||||
# Override the default api url used for preview
|
- node ./scripts/deploy-preview-netlify.js
|
||||||
- sed -i 's|http://localhost:3456|https://try.vikunja.io|g' dist-preview/index.html
|
|
||||||
- apk add --no-cache perl-utils
|
|
||||||
# create via:
|
|
||||||
# `shasum -a 384 ./scripts/deploy-preview-netlify.mjs > ./scripts/deploy-preview-netlify.mjs.sha384`
|
|
||||||
- shasum -a 384 -c ./scripts/deploy-preview-netlify.mjs.sha384
|
|
||||||
- node ./scripts/deploy-preview-netlify.mjs
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- build-prod
|
- build-prod
|
||||||
when:
|
when:
|
||||||
|
@ -165,7 +168,6 @@ steps:
|
||||||
|
|
||||||
---
|
---
|
||||||
kind: pipeline
|
kind: pipeline
|
||||||
type: docker
|
|
||||||
name: release-latest
|
name: release-latest
|
||||||
|
|
||||||
depends_on:
|
depends_on:
|
||||||
|
@ -185,7 +187,7 @@ steps:
|
||||||
|
|
||||||
# - name: restore-cache
|
# - name: restore-cache
|
||||||
# image: meltwater/drone-cache:dev
|
# image: meltwater/drone-cache:dev
|
||||||
# pull: always
|
# pull: true
|
||||||
# environment:
|
# environment:
|
||||||
# AWS_ACCESS_KEY_ID:
|
# AWS_ACCESS_KEY_ID:
|
||||||
# from_secret: cache_aws_access_key_id
|
# from_secret: cache_aws_access_key_id
|
||||||
|
@ -197,36 +199,29 @@ steps:
|
||||||
# endpoint: https://s3.fr-par.scw.cloud
|
# endpoint: https://s3.fr-par.scw.cloud
|
||||||
# region: fr-par
|
# region: fr-par
|
||||||
# path_style: true
|
# path_style: true
|
||||||
# cache_key: '{{ .Repo.Name }}_{{ checksum "pnpm-lock.yaml" }}_{{ arch }}_{{ os }}'
|
# cache_key: '{{ .Repo.Name }}_{{ checksum "yarn.lock" }}_{{ arch }}_{{ os }}'
|
||||||
# mount:
|
# mount:
|
||||||
# - .cache
|
# - '.cache'
|
||||||
|
|
||||||
- name: build
|
- name: build
|
||||||
image: node:20.11.0-alpine
|
image: node:16
|
||||||
pull: always
|
pull: true
|
||||||
|
group: build-static
|
||||||
environment:
|
environment:
|
||||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
YARN_CACHE_FOLDER: .cache/yarn/
|
||||||
SENTRY_AUTH_TOKEN:
|
|
||||||
from_secret: sentry_auth_token
|
|
||||||
SENTRY_ORG: vikunja
|
|
||||||
SENTRY_PROJECT: frontend-oss
|
|
||||||
PUPPETEER_SKIP_DOWNLOAD: true
|
|
||||||
commands:
|
commands:
|
||||||
- apk add git
|
- yarn --frozen-lockfile --network-timeout 100000
|
||||||
- corepack enable && pnpm config set store-dir .cache/pnpm
|
- yarn run lint
|
||||||
- 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"
|
- "echo '{\"VERSION\": \"'$(git describe --tags --always --abbrev=10 | sed 's/-/+/' | sed 's/^v//' | sed 's/-g/-/')'\"}' > src/version.json"
|
||||||
- pnpm run build
|
- yarn run build
|
||||||
- sed -i 's/http\:\\/\\/localhost\\:3456\\/api\\/v1/\\/api\\/v1/g' dist/index.html # Override the default api url used for developing
|
- sed -i 's/http\:\\/\\/localhost\\:3456\\/api\\/v1/\\/api\\/v1/g' dist/index.html # Override the default api url used for developing
|
||||||
# depends_on:
|
# depends_on:
|
||||||
# - restore-cache
|
# - restore-cache
|
||||||
|
|
||||||
- name: static
|
- name: static
|
||||||
image: kolaente/zip
|
image: kolaente/zip
|
||||||
pull: always
|
pull: true
|
||||||
commands:
|
commands:
|
||||||
- cp src/version.json dist
|
|
||||||
- cd dist
|
- cd dist
|
||||||
- zip -r ../vikunja-frontend-unstable.zip *
|
- zip -r ../vikunja-frontend-unstable.zip *
|
||||||
- cd ..
|
- cd ..
|
||||||
|
@ -234,7 +229,7 @@ steps:
|
||||||
|
|
||||||
- name: release
|
- name: release
|
||||||
image: plugins/s3
|
image: plugins/s3
|
||||||
pull: always
|
pull: true
|
||||||
settings:
|
settings:
|
||||||
bucket: vikunja-releases
|
bucket: vikunja-releases
|
||||||
access_key:
|
access_key:
|
||||||
|
@ -250,7 +245,6 @@ steps:
|
||||||
|
|
||||||
---
|
---
|
||||||
kind: pipeline
|
kind: pipeline
|
||||||
type: docker
|
|
||||||
name: release-version
|
name: release-version
|
||||||
|
|
||||||
depends_on:
|
depends_on:
|
||||||
|
@ -268,7 +262,7 @@ steps:
|
||||||
|
|
||||||
# - name: restore-cache
|
# - name: restore-cache
|
||||||
# image: meltwater/drone-cache:dev
|
# image: meltwater/drone-cache:dev
|
||||||
# pull: always
|
# pull: true
|
||||||
# environment:
|
# environment:
|
||||||
# AWS_ACCESS_KEY_ID:
|
# AWS_ACCESS_KEY_ID:
|
||||||
# from_secret: cache_aws_access_key_id
|
# from_secret: cache_aws_access_key_id
|
||||||
|
@ -280,35 +274,29 @@ steps:
|
||||||
# endpoint: https://s3.fr-par.scw.cloud
|
# endpoint: https://s3.fr-par.scw.cloud
|
||||||
# region: fr-par
|
# region: fr-par
|
||||||
# path_style: true
|
# path_style: true
|
||||||
# cache_key: '{{ .Repo.Name }}_{{ checksum "pnpm-lock.yaml" }}_{{ arch }}_{{ os }}'
|
# cache_key: '{{ .Repo.Name }}_{{ checksum "yarn.lock" }}_{{ arch }}_{{ os }}'
|
||||||
# mount:
|
# mount:
|
||||||
# - .cache
|
# - '.cache'
|
||||||
|
|
||||||
- name: build
|
- name: build
|
||||||
image: node:20.11.0-alpine
|
image: node:16
|
||||||
pull: always
|
pull: true
|
||||||
|
group: build-static
|
||||||
environment:
|
environment:
|
||||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
YARN_CACHE_FOLDER: .cache/yarn/
|
||||||
SENTRY_AUTH_TOKEN:
|
|
||||||
from_secret: sentry_auth_token
|
|
||||||
SENTRY_ORG: vikunja
|
|
||||||
SENTRY_PROJECT: frontend-oss
|
|
||||||
commands:
|
commands:
|
||||||
- apk add git
|
- yarn --frozen-lockfile --network-timeout 100000
|
||||||
- corepack enable && pnpm config set store-dir .cache/pnpm
|
- yarn run lint
|
||||||
- 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"
|
- "echo '{\"VERSION\": \"'$(git describe --tags --always --abbrev=10 | sed 's/-/+/' | sed 's/^v//' | sed 's/-g/-/')'\"}' > src/version.json"
|
||||||
- pnpm run build
|
- yarn run build
|
||||||
- sed -i 's/http\:\\/\\/localhost\\:3456\\/api\\/v1/\\/api\\/v1/g' dist/index.html # Override the default api url used for developing
|
- sed -i 's/http\:\\/\\/localhost\\:3456\\/api\\/v1/\\/api\\/v1/g' dist/index.html # Override the default api url used for developing
|
||||||
# depends_on:
|
# depends_on:
|
||||||
# - restore-cache
|
# - restore-cache
|
||||||
|
|
||||||
- name: static
|
- name: static
|
||||||
image: kolaente/zip
|
image: kolaente/zip
|
||||||
pull: always
|
pull: true
|
||||||
commands:
|
commands:
|
||||||
- cp src/version.json dist
|
|
||||||
- cd dist
|
- cd dist
|
||||||
- zip -r ../vikunja-frontend-${DRONE_TAG##v}.zip *
|
- zip -r ../vikunja-frontend-${DRONE_TAG##v}.zip *
|
||||||
- cd ..
|
- cd ..
|
||||||
|
@ -316,7 +304,7 @@ steps:
|
||||||
|
|
||||||
- name: release
|
- name: release
|
||||||
image: plugins/s3
|
image: plugins/s3
|
||||||
pull: always
|
pull: true
|
||||||
settings:
|
settings:
|
||||||
bucket: vikunja-releases
|
bucket: vikunja-releases
|
||||||
access_key:
|
access_key:
|
||||||
|
@ -332,7 +320,6 @@ steps:
|
||||||
|
|
||||||
---
|
---
|
||||||
kind: pipeline
|
kind: pipeline
|
||||||
type: docker
|
|
||||||
name: trigger-desktop-update
|
name: trigger-desktop-update
|
||||||
|
|
||||||
trigger:
|
trigger:
|
||||||
|
@ -357,10 +344,15 @@ steps:
|
||||||
---
|
---
|
||||||
kind: pipeline
|
kind: pipeline
|
||||||
type: docker
|
type: docker
|
||||||
name: docker-release
|
name: docker-arm-release
|
||||||
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- build
|
- release-latest
|
||||||
|
- release-version
|
||||||
|
|
||||||
|
platform:
|
||||||
|
os: linux
|
||||||
|
arch: arm64
|
||||||
|
|
||||||
trigger:
|
trigger:
|
||||||
ref:
|
ref:
|
||||||
|
@ -371,65 +363,201 @@ trigger:
|
||||||
- cron
|
- cron
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: fetch-tags
|
|
||||||
image: docker:git
|
|
||||||
commands:
|
|
||||||
- git fetch --tags
|
|
||||||
|
|
||||||
- name: docker-unstable
|
- name: docker-unstable
|
||||||
image: thegeeklab/drone-docker-buildx
|
image: plugins/docker:linux-arm
|
||||||
privileged: true
|
pull: true
|
||||||
pull: always
|
|
||||||
settings:
|
settings:
|
||||||
username:
|
username:
|
||||||
from_secret: docker_username
|
from_secret: docker_username
|
||||||
password:
|
password:
|
||||||
from_secret: docker_password
|
from_secret: docker_password
|
||||||
repo: vikunja/frontend
|
repo: vikunja/frontend
|
||||||
tags: unstable
|
tags: unstable-linux-arm
|
||||||
build_args:
|
build_args:
|
||||||
- USE_RELEASE=false
|
- USE_RELEASE=true
|
||||||
platforms:
|
- RELEASE_VERSION=unstable
|
||||||
- linux/386
|
when:
|
||||||
- linux/amd64
|
ref:
|
||||||
- linux/arm/v6
|
- refs/heads/main
|
||||||
- linux/arm/v7
|
depends_on:
|
||||||
- linux/arm64/v8
|
- clone
|
||||||
depends_on: [ fetch-tags ]
|
|
||||||
|
- name: docker-version
|
||||||
|
image: plugins/docker:linux-arm
|
||||||
|
pull: true
|
||||||
|
settings:
|
||||||
|
username:
|
||||||
|
from_secret: docker_username
|
||||||
|
password:
|
||||||
|
from_secret: docker_password
|
||||||
|
repo: vikunja/frontend
|
||||||
|
auto_tag: true
|
||||||
|
auto_tag_suffix: linux-arm
|
||||||
|
build_args:
|
||||||
|
- USE_RELEASE=true
|
||||||
|
- RELEASE_VERSION=${DRONE_TAG##v}
|
||||||
|
when:
|
||||||
|
ref:
|
||||||
|
- "refs/tags/**"
|
||||||
|
depends_on:
|
||||||
|
- clone
|
||||||
|
|
||||||
|
- name: docker-unstable-arm64
|
||||||
|
image: plugins/docker:linux-arm64
|
||||||
|
pull: true
|
||||||
|
settings:
|
||||||
|
username:
|
||||||
|
from_secret: docker_username
|
||||||
|
password:
|
||||||
|
from_secret: docker_password
|
||||||
|
repo: vikunja/frontend
|
||||||
|
tags: unstable-linux-arm64
|
||||||
|
build_args:
|
||||||
|
- USE_RELEASE=true
|
||||||
|
- RELEASE_VERSION=unstable
|
||||||
|
when:
|
||||||
|
ref:
|
||||||
|
- refs/heads/main
|
||||||
|
depends_on:
|
||||||
|
- clone
|
||||||
|
|
||||||
|
- name: docker-version-arm64
|
||||||
|
image: plugins/docker:linux-arm64
|
||||||
|
pull: true
|
||||||
|
settings:
|
||||||
|
username:
|
||||||
|
from_secret: docker_username
|
||||||
|
password:
|
||||||
|
from_secret: docker_password
|
||||||
|
repo: vikunja/frontend
|
||||||
|
auto_tag: true
|
||||||
|
auto_tag_suffix: linux-arm64
|
||||||
|
build_args:
|
||||||
|
- USE_RELEASE=true
|
||||||
|
- RELEASE_VERSION=${DRONE_TAG##v}
|
||||||
|
when:
|
||||||
|
ref:
|
||||||
|
- "refs/tags/**"
|
||||||
|
depends_on:
|
||||||
|
- clone
|
||||||
|
|
||||||
|
---
|
||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: docker-amd64-release
|
||||||
|
|
||||||
|
platform:
|
||||||
|
os: linux
|
||||||
|
arch: amd64
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
- release-latest
|
||||||
|
- release-version
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
ref:
|
||||||
|
- refs/heads/main
|
||||||
|
- "refs/tags/**"
|
||||||
|
event:
|
||||||
|
exclude:
|
||||||
|
- cron
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: docker-unstable
|
||||||
|
image: plugins/docker:linux-amd64
|
||||||
|
pull: true
|
||||||
|
settings:
|
||||||
|
username:
|
||||||
|
from_secret: docker_username
|
||||||
|
password:
|
||||||
|
from_secret: docker_password
|
||||||
|
repo: vikunja/frontend
|
||||||
|
tags: unstable-linux-amd64
|
||||||
|
build_args:
|
||||||
|
- USE_RELEASE=true
|
||||||
|
- RELEASE_VERSION=unstable
|
||||||
when:
|
when:
|
||||||
ref:
|
ref:
|
||||||
- refs/heads/main
|
- refs/heads/main
|
||||||
|
|
||||||
- name: generate-tags
|
- name: docker-version
|
||||||
image: thegeeklab/docker-autotag
|
image: plugins/docker:linux-amd64
|
||||||
environment:
|
pull: true
|
||||||
DOCKER_AUTOTAG_VERSION: ${DRONE_TAG}
|
|
||||||
DOCKER_AUTOTAG_EXTRA_TAGS: latest
|
|
||||||
DOCKER_AUTOTAG_OUTPUT_FILE: .tags
|
|
||||||
depends_on: [ fetch-tags ]
|
|
||||||
when:
|
|
||||||
ref:
|
|
||||||
- "refs/tags/**"
|
|
||||||
|
|
||||||
- name: docker-release
|
|
||||||
image: thegeeklab/drone-docker-buildx
|
|
||||||
privileged: true
|
|
||||||
pull: always
|
|
||||||
settings:
|
settings:
|
||||||
username:
|
username:
|
||||||
from_secret: docker_username
|
from_secret: docker_username
|
||||||
password:
|
password:
|
||||||
from_secret: docker_password
|
from_secret: docker_password
|
||||||
repo: vikunja/frontend
|
repo: vikunja/frontend
|
||||||
|
auto_tag: true
|
||||||
|
auto_tag_suffix: linux-amd64
|
||||||
build_args:
|
build_args:
|
||||||
- USE_RELEASE=false
|
- USE_RELEASE=true
|
||||||
platforms:
|
- RELEASE_VERSION=${DRONE_TAG##v}
|
||||||
- linux/386
|
when:
|
||||||
- linux/amd64
|
ref:
|
||||||
- linux/arm/v6
|
- "refs/tags/**"
|
||||||
- linux/arm/v7
|
|
||||||
- linux/arm64/v8
|
---
|
||||||
depends_on: [ generate-tags ]
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: docker-manifest
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
ref:
|
||||||
|
- refs/heads/main
|
||||||
|
- "refs/tags/**"
|
||||||
|
event:
|
||||||
|
exclude:
|
||||||
|
- cron
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
- docker-amd64-release
|
||||||
|
- docker-arm-release
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: manifest-unstable
|
||||||
|
pull: always
|
||||||
|
image: plugins/manifest
|
||||||
|
settings:
|
||||||
|
tags: unstable
|
||||||
|
spec: docker-manifest-unstable.tmpl
|
||||||
|
password:
|
||||||
|
from_secret: docker_password
|
||||||
|
username:
|
||||||
|
from_secret: docker_username
|
||||||
|
when:
|
||||||
|
ref:
|
||||||
|
- refs/heads/main
|
||||||
|
|
||||||
|
- name: manifest-release
|
||||||
|
pull: always
|
||||||
|
image: plugins/manifest
|
||||||
|
settings:
|
||||||
|
auto_tag: true
|
||||||
|
ignore_missing: true
|
||||||
|
spec: docker-manifest.tmpl
|
||||||
|
password:
|
||||||
|
from_secret: docker_password
|
||||||
|
username:
|
||||||
|
from_secret: docker_username
|
||||||
|
when:
|
||||||
|
ref:
|
||||||
|
- "refs/tags/**"
|
||||||
|
|
||||||
|
- name: manifest-release-latest
|
||||||
|
pull: always
|
||||||
|
image: plugins/manifest
|
||||||
|
depends_on:
|
||||||
|
- clone
|
||||||
|
settings:
|
||||||
|
tags: latest
|
||||||
|
ignore_missing: true
|
||||||
|
spec: docker-manifest.tmpl
|
||||||
|
password:
|
||||||
|
from_secret: docker_password
|
||||||
|
username:
|
||||||
|
from_secret: docker_username
|
||||||
when:
|
when:
|
||||||
ref:
|
ref:
|
||||||
- "refs/tags/**"
|
- "refs/tags/**"
|
||||||
|
@ -452,7 +580,9 @@ depends_on:
|
||||||
- release-version
|
- release-version
|
||||||
- release-latest
|
- release-latest
|
||||||
- trigger-desktop-update
|
- trigger-desktop-update
|
||||||
- docker-release
|
- docker-arm-release
|
||||||
|
- docker-amd64-release
|
||||||
|
- docker-manifest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: notify
|
- name: notify
|
||||||
|
@ -473,27 +603,29 @@ kind: pipeline
|
||||||
type: docker
|
type: docker
|
||||||
name: update-translations
|
name: update-translations
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
- build
|
||||||
|
|
||||||
trigger:
|
trigger:
|
||||||
branch:
|
branch:
|
||||||
include:
|
- main
|
||||||
- main
|
|
||||||
event:
|
event:
|
||||||
include:
|
- cron
|
||||||
- cron
|
|
||||||
cron:
|
cron:
|
||||||
- update_translations
|
- update_translations
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: download
|
- name: download
|
||||||
pull: always
|
pull: always
|
||||||
image: ghcr.io/kolaente/kolaente/drone-crowdin-v2:latest
|
image: jonasfranz/crowdin
|
||||||
settings:
|
settings:
|
||||||
crowdin_key:
|
download: true
|
||||||
|
export_dir: src/i18n/lang/
|
||||||
|
ignore_branch: true
|
||||||
|
project_identifier: vikunja
|
||||||
|
environment:
|
||||||
|
CROWDIN_KEY:
|
||||||
from_secret: crowdin_key
|
from_secret: crowdin_key
|
||||||
project_id: 462614
|
|
||||||
target: download
|
|
||||||
download_to: src/i18n/lang/
|
|
||||||
download_export_approved_only: true
|
|
||||||
|
|
||||||
- name: move-files
|
- name: move-files
|
||||||
pull: always
|
pull: always
|
||||||
|
@ -513,25 +645,26 @@ steps:
|
||||||
author_name: Frederick [Bot]
|
author_name: Frederick [Bot]
|
||||||
branch: main
|
branch: main
|
||||||
commit: true
|
commit: true
|
||||||
commit_message: "chore(i18n): update translations via Crowdin"
|
commit_message: "[skip ci] Updated translations via Crowdin"
|
||||||
remote: "ssh://git@kolaente.dev:9022/vikunja/frontend.git"
|
remote: "ssh://git@kolaente.dev:9022/vikunja/frontend.git"
|
||||||
ssh_key:
|
ssh_key:
|
||||||
from_secret: git_push_ssh_key
|
from_secret: translation_git_push_ssh_key
|
||||||
|
|
||||||
- name: upload
|
- name: upload
|
||||||
pull: always
|
pull: always
|
||||||
image: ghcr.io/kolaente/kolaente/drone-crowdin-v2:latest
|
image: jonasfranz/crowdin
|
||||||
depends_on:
|
depends_on:
|
||||||
- clone
|
- clone
|
||||||
settings:
|
settings:
|
||||||
crowdin_key:
|
files:
|
||||||
from_secret: crowdin_key
|
en.json: src/i18n/lang/en.json
|
||||||
project_id: 462614
|
ignore_branch: true
|
||||||
target: upload
|
project_identifier: vikunja
|
||||||
upload_files:
|
environment:
|
||||||
src/i18n/lang/en.json: en.json
|
CROWDIN_KEY:
|
||||||
|
from_secret: crowdin_key
|
||||||
---
|
---
|
||||||
kind: signature
|
kind: signature
|
||||||
hmac: a044c7c4db3c2a11299d4d118397e9d25be36db241723a1bbd0a2f9cc90ffdac
|
hmac: 188ee90100c5fc5922a445e531e7a47453121edddb2a64a182eb23ed2bf602de
|
||||||
|
|
||||||
...
|
...
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
# (1) Duplicate this file and remove the '.example' suffix.
|
|
||||||
# Naming this file '.env.local' is a Vite convention to prevent accidentally
|
|
||||||
# submitting to git.
|
|
||||||
# For more info see: https://vitejs.dev/guide/env-and-mode.html#env-files
|
|
||||||
|
|
||||||
# (2) Comment in and adjust the values as needed.
|
|
||||||
|
|
||||||
# VITE_IS_ONLINE=true
|
|
||||||
# SENTRY_AUTH_TOKEN=YOUR_TOKEN
|
|
||||||
# SENTRY_ORG=vikunja
|
|
||||||
# SENTRY_PROJECT=frontend-oss
|
|
||||||
# VIKUNJA_FRONTEND_BASE=/custom-subpath
|
|
|
@ -1,60 +0,0 @@
|
||||||
/* eslint-env node */
|
|
||||||
require('@rushstack/eslint-patch/modern-module-resolution')
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
'root': true,
|
|
||||||
'env': {
|
|
||||||
'browser': true,
|
|
||||||
'es2022': true,
|
|
||||||
'node': true,
|
|
||||||
},
|
|
||||||
'extends': [
|
|
||||||
'eslint:recommended',
|
|
||||||
'plugin:vue/vue3-recommended',
|
|
||||||
'@vue/eslint-config-typescript/recommended',
|
|
||||||
],
|
|
||||||
'rules': {
|
|
||||||
'quotes': ['error', 'single'],
|
|
||||||
'comma-dangle': ['error', 'always-multiline'],
|
|
||||||
'semi': ['error', 'never'],
|
|
||||||
|
|
||||||
'vue/v-on-event-hyphenation': ['warn', 'never', { 'autofix': true }],
|
|
||||||
'vue/multi-word-component-names': 'off',
|
|
||||||
|
|
||||||
// uncategorized rules:
|
|
||||||
'vue/component-api-style': ['error', ['script-setup']],
|
|
||||||
'vue/component-name-in-template-casing': ['warn', 'PascalCase'],
|
|
||||||
'vue/custom-event-name-casing': ['error', 'camelCase'],
|
|
||||||
'vue/define-macros-order': 'error',
|
|
||||||
'vue/match-component-file-name': ['error', {
|
|
||||||
'extensions': ['.js', '.jsx', '.ts', '.tsx', '.vue'],
|
|
||||||
'shouldMatchCase': true,
|
|
||||||
}],
|
|
||||||
'vue/no-boolean-default': ['warn', 'default-false'],
|
|
||||||
'vue/match-component-import-name': 'error',
|
|
||||||
'vue/prefer-separate-static-class': 'warn',
|
|
||||||
|
|
||||||
'vue/padding-line-between-blocks': 'error',
|
|
||||||
'vue/next-tick-style': ['error', 'promise'],
|
|
||||||
'vue/block-lang': [
|
|
||||||
'error',
|
|
||||||
{ 'script': { 'lang': 'ts' } },
|
|
||||||
],
|
|
||||||
'vue/no-required-prop-with-default': ['error', { 'autofix': true }],
|
|
||||||
'vue/no-duplicate-attr-inheritance': 'error',
|
|
||||||
'vue/no-empty-component-block': 'error',
|
|
||||||
'vue/html-indent': ['error', 'tab'],
|
|
||||||
|
|
||||||
// vue3
|
|
||||||
'vue/no-ref-object-destructure': 'error',
|
|
||||||
},
|
|
||||||
'parser': 'vue-eslint-parser',
|
|
||||||
'parserOptions': {
|
|
||||||
'parser': '@typescript-eslint/parser',
|
|
||||||
'ecmaVersion': 'latest',
|
|
||||||
},
|
|
||||||
'ignorePatterns': [
|
|
||||||
'*.test.*',
|
|
||||||
'cypress/*',
|
|
||||||
],
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
<!--
|
|
||||||
|
|
||||||
Please fill out this issue template to report a bug.
|
|
||||||
If you want to propose a new feature, please open a discussion thread in the forum: https://community.vikunja.io
|
|
||||||
|
|
||||||
-->
|
|
||||||
|
|
||||||
**Version information:**
|
|
||||||
|
|
||||||
Frontend Version:
|
|
||||||
API Version:
|
|
||||||
Browser and OS Version:
|
|
||||||
|
|
||||||
**Steps to reproduce:**
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Add clear steps to reproduce the bug. Provide screenshots where applicable.
|
|
||||||
-->
|
|
||||||
|
|
||||||
1.
|
|
||||||
2.
|
|
||||||
...
|
|
||||||
|
|
||||||
**Expected behavior:**
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Describe what happened.
|
|
||||||
-->
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
**Actual behavior:**
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Describe what happened instead.
|
|
||||||
-->
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
**Checklist:**
|
|
||||||
|
|
||||||
* [ ] I have provided all required information
|
|
||||||
* [ ] I am using the latest release or the latest unstable build
|
|
||||||
* [ ] I was able to reproduce the bug on [try](https://try.vikunja.io)
|
|
|
@ -1,3 +1,2 @@
|
||||||
github: kolaente
|
github: kolaente
|
||||||
open_collective: vikunja
|
custom: https://www.buymeacoffee.com/kolaente
|
||||||
custom: ["https://vikunja.cloud", "https://www.buymeacoffee.com/kolaente"]
|
|
||||||
|
|
|
@ -1,59 +0,0 @@
|
||||||
name: Bug Report
|
|
||||||
description: Found something you weren't expecting? Report it here!
|
|
||||||
labels:
|
|
||||||
- kind/bug
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
NOTE: If your issue is a security concern, please send an email to security@vikunja.io instead of opening a public issue.
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
Please fill out this issue template to report a bug.
|
|
||||||
|
|
||||||
1. If you want to propose a new feature, please open a discussion thread in the forum: https://community.vikunja.io
|
|
||||||
2. Please ask questions or configuration/deploy problems on our [Matrix Room](https://matrix.to/#/#vikunja:matrix.org) or forum (https://community.vikunja.io).
|
|
||||||
3. Make sure you are using the latest release and
|
|
||||||
take a moment to check that your issue hasn't been reported before.
|
|
||||||
4. Please give all relevant information below for bug reports, because
|
|
||||||
incomplete details will be handled as an invalid report and closed.
|
|
||||||
- type: textarea
|
|
||||||
id: description
|
|
||||||
attributes:
|
|
||||||
label: Description
|
|
||||||
description: |
|
|
||||||
Please provide a description of your issue here, with a URL if you were able to reproduce the issue (see below).
|
|
||||||
- type: input
|
|
||||||
id: frontend-version
|
|
||||||
attributes:
|
|
||||||
label: Vikunja Frontend Version
|
|
||||||
description: Vikunja frontend version (or commit reference) of your instance
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: input
|
|
||||||
id: api-version
|
|
||||||
attributes:
|
|
||||||
label: Vikunja API Version
|
|
||||||
description: Vikunja API version (or commit reference) of your instance
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: input
|
|
||||||
id: browser-version
|
|
||||||
attributes:
|
|
||||||
label: Browser and version
|
|
||||||
description: If your issue is related to a frontend problem, please provide the browser and version you used to reproduce it.
|
|
||||||
- type: dropdown
|
|
||||||
id: can-reproduce
|
|
||||||
attributes:
|
|
||||||
label: Can you reproduce the bug on the Vikunja demo site?
|
|
||||||
options:
|
|
||||||
- "Yes"
|
|
||||||
- "No"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: screenshots
|
|
||||||
attributes:
|
|
||||||
label: Screenshots
|
|
||||||
description: If this issue involves the Web Interface, please provide one or more screenshots
|
|
|
@ -1,17 +0,0 @@
|
||||||
blank_issues_enabled: false
|
|
||||||
contact_links:
|
|
||||||
- name: API issues
|
|
||||||
url: https://code.vikunja.io/api/issues
|
|
||||||
about: This is the frontend repo. Please open api-related bug reports and discussions in the api 0repo. Not sure if your issue is frontend or api? Ask in Matrix or the forum first.
|
|
||||||
- name: Forum
|
|
||||||
url: https://community.vikunja.io/
|
|
||||||
about: Feature Requests, Questions, configuration or deployment problems should be discussed in the forum.
|
|
||||||
- name: Security-related issues
|
|
||||||
url: https://vikunja.io/contact/#security
|
|
||||||
about: For security concerns, please send a mail to security@vikunja.io instead of opening a public issue.
|
|
||||||
- name: Chat on Matrix
|
|
||||||
url: https://matrix.to/#/#vikunja:matrix.org
|
|
||||||
about: Please ask any quick questions here.
|
|
||||||
- name: Translations
|
|
||||||
url: https://crowdin.com/project/vikunja
|
|
||||||
about: Any problems or requests for new languages about translations should be handled in crowdin.
|
|
|
@ -1,23 +0,0 @@
|
||||||
name: 'Repo Lockdown'
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
types: opened
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
issues: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
action:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: dessant/repo-lockdown@v4
|
|
||||||
with:
|
|
||||||
pr-comment: 'Hi! Thank you for your contribution.
|
|
||||||
|
|
||||||
This repo is only a mirror and unfortunately we can''t accept PRs made here. Please re-submit your changes to [our Gitea instance](https://kolaente.dev/vikunja/frontend/pulls).
|
|
||||||
|
|
||||||
Also check out the [contribution guidelines](https://vikunja.io/docs/development/#pull-requests).
|
|
||||||
|
|
||||||
Thank you for your understanding.'
|
|
|
@ -1,31 +1,21 @@
|
||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
stats.html
|
|
||||||
|
|
||||||
node_modules
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
node_modules
|
||||||
/dist*
|
/dist*
|
||||||
coverage
|
|
||||||
*.zip
|
*.zip
|
||||||
.direnv/
|
|
||||||
|
|
||||||
# Test files
|
|
||||||
cypress/screenshots
|
|
||||||
cypress/videos
|
|
||||||
|
|
||||||
# local env files
|
# local env files
|
||||||
.env.local
|
.env.local
|
||||||
.env.*.local
|
.env.*.local
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
stats.html
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode
|
|
||||||
.idea
|
.idea
|
||||||
|
.vscode
|
||||||
*.suo
|
*.suo
|
||||||
*.ntvs*
|
*.ntvs*
|
||||||
*.njsproj
|
*.njsproj
|
||||||
|
@ -33,9 +23,9 @@ cypress/videos
|
||||||
*.sw*
|
*.sw*
|
||||||
!rollup.sw.js
|
!rollup.sw.js
|
||||||
|
|
||||||
|
# Test files
|
||||||
|
cypress/screenshots
|
||||||
|
cypress/videos
|
||||||
|
|
||||||
# Local Netlify folder
|
# Local Netlify folder
|
||||||
.netlify
|
.netlify
|
||||||
|
|
||||||
# histoire
|
|
||||||
.histoire
|
|
14
.npmrc
|
@ -1,14 +0,0 @@
|
||||||
fetch-timeout=100000
|
|
||||||
|
|
||||||
# pnpm settings
|
|
||||||
# The following settings prepare for the new default value of pnpm 8
|
|
||||||
# they can be removed directly after having moved to pnpm 8
|
|
||||||
auto-install-peers=true
|
|
||||||
dedupe-peer-dependents=true
|
|
||||||
resolve-peers-from-workspace-root=true
|
|
||||||
save-workspace-protocol=rolling
|
|
||||||
resolution-mode=lowest-direct
|
|
||||||
publishConfig.linkDirectory=true
|
|
||||||
|
|
||||||
# remove some time after having moved to pnpm 8
|
|
||||||
use-lockfile-v6=true
|
|
|
@ -3,12 +3,10 @@
|
||||||
"codezombiech.gitignore",
|
"codezombiech.gitignore",
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
"editorconfig.editorconfig",
|
"editorconfig.editorconfig",
|
||||||
"vue.volar",
|
"johnsoncodehk.volar",
|
||||||
"vue.vscode-typescript-vue-plugin",
|
|
||||||
"lokalise.i18n-ally",
|
"lokalise.i18n-ally",
|
||||||
"mgmcdermott.vscode-language-babel",
|
"mgmcdermott.vscode-language-babel",
|
||||||
"mikestead.dotenv",
|
"mikestead.dotenv",
|
||||||
"Syler.sass-indented",
|
"Syler.sass-indented"
|
||||||
"zixuanchen.vitest-explorer"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"eslint.packageManager": "pnpm",
|
"eslint.packageManager": "yarn",
|
||||||
"editor.formatOnSave": false,
|
"editor.formatOnSave": false,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll": true
|
"source.fixAll": true
|
||||||
|
@ -18,10 +18,6 @@
|
||||||
"javascriptreact",
|
"javascriptreact",
|
||||||
"vue"
|
"vue"
|
||||||
],
|
],
|
||||||
|
|
||||||
"volar.completion.preferredTagNameCase": "pascal",
|
|
||||||
|
|
||||||
// disable vetur in case it is installed
|
|
||||||
"vetur.validation.template": false,
|
"vetur.validation.template": false,
|
||||||
|
|
||||||
// i18n ally
|
// i18n ally
|
||||||
|
@ -30,5 +26,5 @@
|
||||||
],
|
],
|
||||||
"i18n-ally.sortKeys": true,
|
"i18n-ally.sortKeys": true,
|
||||||
"i18n-ally.keepFulfilled": true,
|
"i18n-ally.keepFulfilled": true,
|
||||||
"i18n-ally.keystyle": "nested"
|
"i18n-ally.keystyle": "nested",
|
||||||
}
|
}
|
4622
CHANGELOG.md
86
Dockerfile
|
@ -1,73 +1,39 @@
|
||||||
# syntax=docker/dockerfile:1
|
# Stage 1: Build application
|
||||||
# ┬─┐┬ ┐o┬ ┬─┐
|
FROM node:16 AS compile-image
|
||||||
# │─││ │││ │ │
|
|
||||||
# ┘─┘┘─┘┘┘─┘┘─┘
|
|
||||||
|
|
||||||
FROM --platform=$BUILDPLATFORM node:20.11.0-alpine AS builder
|
|
||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
ARG USE_RELEASE=false
|
ARG USE_RELEASE=false
|
||||||
ARG RELEASE_VERSION=unstable
|
ARG RELEASE_VERSION=main
|
||||||
ENV PNPM_CACHE_FOLDER .cache/pnpm/
|
|
||||||
ENV PUPPETEER_SKIP_DOWNLOAD true
|
|
||||||
|
|
||||||
COPY package.json ./
|
ENV YARN_CACHE_FOLDER .cache/yarn/
|
||||||
COPY pnpm-lock.yaml ./
|
COPY . ./
|
||||||
COPY patches ./patches/
|
|
||||||
|
|
||||||
RUN if [ "$USE_RELEASE" != true ]; then \
|
RUN \
|
||||||
# https://pnpm.io/installation#using-corepack
|
if [ $USE_RELEASE = true ]; then \
|
||||||
corepack enable && \
|
rm -rf dist/ && \
|
||||||
pnpm install; \
|
wget https://dl.vikunja.io/frontend/vikunja-frontend-$RELEASE_VERSION.zip -O frontend-release.zip && \
|
||||||
fi
|
unzip frontend-release.zip -d dist/ && \
|
||||||
|
exit 0; \
|
||||||
|
fi && \
|
||||||
|
# Build the frontend
|
||||||
|
yarn install --frozen-lockfile --network-timeout 100000 && \
|
||||||
|
echo '{"VERSION": "'$(git describe --tags --always --abbrev=10 | sed 's/-/+/' | sed 's/^v//' | sed 's/-g/-/')'"}' > src/version.json && \
|
||||||
|
yarn run build
|
||||||
|
|
||||||
COPY . ./
|
# Stage 2: copy
|
||||||
|
FROM nginx
|
||||||
|
|
||||||
RUN if [ "$USE_RELEASE" != true ]; then \
|
COPY nginx.conf /etc/nginx/nginx.conf
|
||||||
apk add --no-cache --virtual .build-deps git jq && \
|
COPY run.sh /run.sh
|
||||||
git describe --tags --always --abbrev=10 | sed 's/-/+/; s/^v//; s/-g/-/' | \
|
|
||||||
xargs -0 -I{} jq -Mcnr --arg version {} '{VERSION:$version}' | \
|
|
||||||
tee src/version.json && \
|
|
||||||
apk del .build-deps; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
RUN if [ "$USE_RELEASE" = true ]; then \
|
# copy compiled files from stage 1
|
||||||
wget "https://dl.vikunja.io/frontend/vikunja-frontend-${RELEASE_VERSION}.zip" -O frontend-release.zip && \
|
COPY --from=compile-image /build/dist /usr/share/nginx/html
|
||||||
unzip frontend-release.zip -d dist/; \
|
|
||||||
else \
|
|
||||||
# we don't use corepack prepare here by intend since
|
|
||||||
# we have renovate to keep our dependencies up to date
|
|
||||||
# Build the frontend
|
|
||||||
pnpm run build; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ┌┐┐┌─┐o┌┐┐┐ │
|
# Unprivileged user
|
||||||
# ││││ ┬││││┌┼┘
|
ENV PUID 1000
|
||||||
# ┘└┘┘─┘┘┘└┘┘ └
|
ENV PGID 1000
|
||||||
|
|
||||||
FROM nginx:stable-alpine AS runner
|
|
||||||
WORKDIR /usr/share/nginx/html
|
|
||||||
LABEL maintainer="maintainers@vikunja.io"
|
LABEL maintainer="maintainers@vikunja.io"
|
||||||
|
|
||||||
ENV VIKUNJA_HTTP_PORT 80
|
CMD "/run.sh"
|
||||||
ENV VIKUNJA_HTTP2_PORT 81
|
|
||||||
ENV VIKUNJA_LOG_FORMAT main
|
|
||||||
ENV VIKUNJA_API_URL /api/v1
|
|
||||||
ENV VIKUNJA_SENTRY_ENABLED false
|
|
||||||
ENV VIKUNJA_SENTRY_DSN https://85694a2d757547cbbc90cd4b55c5a18d@o1047380.ingest.sentry.io/6024480
|
|
||||||
ENV VIKUNJA_PROJECT_INFINITE_NESTING_ENABLED false
|
|
||||||
ENV VIKUNJA_ALLOW_ICON_CHANGES true
|
|
||||||
ENV VIKUNJA_CUSTOM_LOGO_URL "''"
|
|
||||||
|
|
||||||
COPY docker/injector.sh /docker-entrypoint.d/50-injector.sh
|
|
||||||
COPY docker/ipv6-disable.sh /docker-entrypoint.d/60-ipv6-disable.sh
|
|
||||||
COPY docker/nginx.conf /etc/nginx/nginx.conf
|
|
||||||
COPY docker/templates/. /etc/nginx/templates/
|
|
||||||
# copy compiled files from stage 1
|
|
||||||
COPY --from=builder /build/dist ./
|
|
||||||
# manage permissions
|
|
||||||
RUN chmod 0755 /docker-entrypoint.d/*.sh /etc/nginx/templates && \
|
|
||||||
chmod -R 0644 /etc/nginx/nginx.conf && \
|
|
||||||
chown -R nginx:nginx ./ /etc/nginx/conf.d /etc/nginx/templates && \
|
|
||||||
rm -f /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
|
|
||||||
|
|
24
README.md
|
@ -1,14 +1,10 @@
|
||||||
# This repository was merged with the api and is now archived
|
|
||||||
|
|
||||||
You can find the new (old) code over on [vikunja/vikunja](https://kolaente.dev/vikunja/vikunja).
|
|
||||||
|
|
||||||
# Web frontend for Vikunja
|
# Web frontend for Vikunja
|
||||||
|
|
||||||
> The todo app to organize your life.
|
> The todo app to organize your life.
|
||||||
|
|
||||||
[![Build Status](https://drone.kolaente.de/api/badges/vikunja/frontend/status.svg)](https://drone.kolaente.de/vikunja/frontend)
|
[![Build Status](https://drone.kolaente.de/api/badges/vikunja/frontend/status.svg)](https://drone.kolaente.de/vikunja/frontend)
|
||||||
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](LICENSE)
|
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](LICENSE)
|
||||||
[![Download](https://img.shields.io/badge/download-v0.22.1-brightgreen.svg)](https://dl.vikunja.io)
|
[![Download](https://img.shields.io/badge/download-v0.18.2-brightgreen.svg)](https://dl.vikunja.io)
|
||||||
[![Translation](https://badges.crowdin.net/vikunja/localized.svg)](https://crowdin.com/project/vikunja)
|
[![Translation](https://badges.crowdin.net/vikunja/localized.svg)](https://crowdin.com/project/vikunja)
|
||||||
|
|
||||||
This is the web frontend for Vikunja, written in Vue.js.
|
This is the web frontend for Vikunja, written in Vue.js.
|
||||||
|
@ -22,35 +18,27 @@ If you find any security-related issues you don't want to disclose publicly, ple
|
||||||
## Docker
|
## Docker
|
||||||
|
|
||||||
There is a [docker image available](https://hub.docker.com/r/vikunja/api) with support for http/2 and aggressive caching enabled.
|
There is a [docker image available](https://hub.docker.com/r/vikunja/api) with support for http/2 and aggressive caching enabled.
|
||||||
In order to build it from sources run the command below. (Docker >= v19.03)
|
|
||||||
|
|
||||||
```shell
|
|
||||||
export DOCKER_BUILDKIT=1
|
|
||||||
docker build -t vikunja/frontend .
|
|
||||||
```
|
|
||||||
|
|
||||||
Refer to [multi-platform documentation](https://docs.docker.com/build/building/multi-platform/) in order to build for different platforms.
|
|
||||||
|
|
||||||
## Project setup
|
## Project setup
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
pnpm install
|
yarn install
|
||||||
```
|
```
|
||||||
|
|
||||||
### Compiles and hot-reloads for development
|
### Compiles and hot-reloads for development
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
pnpm run serve
|
yarn run serve
|
||||||
```
|
```
|
||||||
|
|
||||||
### Compiles and minifies for production
|
### Compiles and minifies for production
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
pnpm run build
|
yarn run build
|
||||||
```
|
```
|
||||||
|
|
||||||
### Lints and fixes files
|
### Lints and fixes files
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
pnpm run lint
|
yarn run lint
|
||||||
```
|
```
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
# NPM renames .gitignore to .npmignore
|
||||||
|
# In order to prevent that, we remove the initial "."
|
||||||
|
# And the CLI then renames it
|
||||||
|
|
||||||
|
# Using Android gitignore template: https://github.com/github/gitignore/blob/master/Android.gitignore
|
||||||
|
|
||||||
|
# Built application files
|
||||||
|
*.apk
|
||||||
|
*.ap_
|
||||||
|
*.aab
|
||||||
|
|
||||||
|
# Files for the ART/Dalvik VM
|
||||||
|
*.dex
|
||||||
|
|
||||||
|
# Java class files
|
||||||
|
*.class
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
bin/
|
||||||
|
gen/
|
||||||
|
out/
|
||||||
|
release/
|
||||||
|
|
||||||
|
# Gradle files
|
||||||
|
.gradle/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Local configuration file (sdk path, etc)
|
||||||
|
local.properties
|
||||||
|
|
||||||
|
# Proguard folder generated by Eclipse
|
||||||
|
proguard/
|
||||||
|
|
||||||
|
# Log Files
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Android Studio Navigation editor temp files
|
||||||
|
.navigation/
|
||||||
|
|
||||||
|
# Android Studio captures folder
|
||||||
|
captures/
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
*.iml
|
||||||
|
.idea/workspace.xml
|
||||||
|
.idea/tasks.xml
|
||||||
|
.idea/gradle.xml
|
||||||
|
.idea/assetWizardSettings.xml
|
||||||
|
.idea/dictionaries
|
||||||
|
.idea/libraries
|
||||||
|
# Android Studio 3 in .gitignore file.
|
||||||
|
.idea/caches
|
||||||
|
.idea/modules.xml
|
||||||
|
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
|
||||||
|
.idea/navEditor.xml
|
||||||
|
|
||||||
|
# Keystore files
|
||||||
|
# Uncomment the following lines if you do not want to check your keystore files in.
|
||||||
|
#*.jks
|
||||||
|
#*.keystore
|
||||||
|
|
||||||
|
# External native build folder generated in Android Studio 2.2 and later
|
||||||
|
.externalNativeBuild
|
||||||
|
|
||||||
|
# Freeline
|
||||||
|
freeline.py
|
||||||
|
freeline/
|
||||||
|
freeline_project_description.json
|
||||||
|
|
||||||
|
# fastlane
|
||||||
|
fastlane/report.xml
|
||||||
|
fastlane/Preview.html
|
||||||
|
fastlane/screenshots
|
||||||
|
fastlane/test_output
|
||||||
|
fastlane/readme.md
|
||||||
|
|
||||||
|
# Version control
|
||||||
|
vcs.xml
|
||||||
|
|
||||||
|
# lint
|
||||||
|
lint/intermediates/
|
||||||
|
lint/generated/
|
||||||
|
lint/outputs/
|
||||||
|
lint/tmp/
|
||||||
|
# lint/reports/
|
||||||
|
|
||||||
|
# Cordova plugins for Capacitor
|
||||||
|
capacitor-cordova-android-plugins
|
||||||
|
|
||||||
|
# Copied web assets
|
||||||
|
app/src/main/assets/public
|
|
@ -0,0 +1,2 @@
|
||||||
|
/build/*
|
||||||
|
!/build/.npmkeep
|
|
@ -0,0 +1,46 @@
|
||||||
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "io.vikunja.app"
|
||||||
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
|
versionCode 1
|
||||||
|
versionName "1.0"
|
||||||
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
}
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
flatDir{
|
||||||
|
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||||
|
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
||||||
|
implementation project(':capacitor-android')
|
||||||
|
testImplementation "junit:junit:$junitVersion"
|
||||||
|
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
||||||
|
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
||||||
|
implementation project(':capacitor-cordova-android-plugins')
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: 'capacitor.build.gradle'
|
||||||
|
|
||||||
|
try {
|
||||||
|
def servicesJSON = file('google-services.json')
|
||||||
|
if (servicesJSON.text) {
|
||||||
|
apply plugin: 'com.google.gms.google-services'
|
||||||
|
}
|
||||||
|
} catch(Exception e) {
|
||||||
|
logger.warn("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||||
|
dependencies {
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (hasProperty('postBuildExtras')) {
|
||||||
|
postBuildExtras()
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
|
@ -0,0 +1,63 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="io.vikunja.app">
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/AppTheme">
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||||
|
android:name="io.vikunja.app.MainActivity"
|
||||||
|
android:label="@string/title_activity_main"
|
||||||
|
android:theme="@style/AppTheme.NoActionBarLaunch"
|
||||||
|
android:launchMode="singleTask">
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<data android:scheme="@string/custom_url_scheme" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.fileprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/file_paths"></meta-data>
|
||||||
|
</provider>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
<!-- Permissions -->
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<!-- Camera, Photos, input file -->
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
<!-- Geolocation API -->
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
<uses-feature android:name="android.hardware.location.gps" />
|
||||||
|
<!-- Network API -->
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<!-- Navigator.getUserMedia -->
|
||||||
|
<!-- Video -->
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<!-- Audio -->
|
||||||
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
|
||||||
|
</manifest>
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"appId": "io.vikunja.app",
|
||||||
|
"appName": "Vikunja",
|
||||||
|
"webDir": "dist",
|
||||||
|
"bundledWebRuntime": false
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
[]
|
After Width: | Height: | Size: 26 KiB |
|
@ -0,0 +1,21 @@
|
||||||
|
package io.vikunja.app;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import com.getcapacitor.BridgeActivity;
|
||||||
|
import com.getcapacitor.Plugin;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
public class MainActivity extends BridgeActivity {
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
// Initializes the Bridge
|
||||||
|
this.init(savedInstanceState, new ArrayList<Class<? extends Plugin>>() {{
|
||||||
|
// Additional plugins you've installed go here
|
||||||
|
// Ex: add(TotallyAwesomePlugin.class);
|
||||||
|
}});
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 8.4 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 9.8 KiB |
After Width: | Height: | Size: 6.2 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 26 KiB |
|
@ -0,0 +1,101 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<group android:scaleX="0.29109365"
|
||||||
|
android:scaleY="0.29109365"
|
||||||
|
android:translateX="16.740015"
|
||||||
|
android:translateY="16.74">
|
||||||
|
<path
|
||||||
|
android:pathData="m167.62,161.652c4.758,-13.899 12.617,-18.154 13.594,-39.601 0.343,-7.537 -1.955,-31.477 -1.955,-31.477 0,-0 3.735,-9.599 -3.443,-20.292 -11.13,-16.58 -34.053,-17.707 -46.092,-17.707 -12.039,-0 -34.694,1.127 -45.825,17.707 -7.179,10.693 -3.444,20.292 -3.444,20.292 0,-0 -1.271,12.313 -2.234,23.172 -0.227,2.548 0.021,12.629 0.327,15.064 0.55,4.364 1.931,8.652 3.441,12.826 1.912,5.285 4.909,10.648 7.198,16.07 6.907,16.371 1.127,36.647 1.48,54.3 0.298,12.531 -2.668,25.242 -3.831,37.496 13.084,4.434 26.945,6.747 41.162,6.747 15.978,-0 31.507,-2.913 45.987,-8.484 -2.719,-15.342 -5.879,-29.894 -6.366,-35.446 -1.419,-16.785 -5.507,-34.587 -0.001,-50.667"
|
||||||
|
android:strokeWidth="0.048874177"
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:fillType="nonZero"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m79.507,63.921c0.153,2.497 0.4,5.057 1.103,7.488 0.221,0.758 0.547,2.624 1.522,2.902 1.212,0.345 3.455,-1.101 4.305,-1.728 1.648,-1.213 3.19,-2.461 4.838,-3.687 1.051,-0.782 2.199,-1.396 3.228,-2.199 1.12,-0.873 1.963,-1.93 2.895,-2.957 1.131,-1.248 1.98,-2.54 2.85,-3.933 0.68,-1.087 4.943,-1.795 5.625,-2.804 0.745,-1.103 0.122,-2.468 0.122,-3.826 0,-9.889 0.594,-19.675 0.825,-29.372 0.238,-6.652 2.851,-14.55 -7.125,-11.432 -21.375,6.652 -21.137,36.168 -20.187,51.549"
|
||||||
|
android:strokeWidth="0.04884417"
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:fillType="nonZero"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m173.67,77.809c-0.215,-0.233 -0.559,-0.432 -0.835,-0.636 -1.197,-0.887 -2.326,-1.814 -3.354,-2.864 -1.255,-1.282 -2.559,-2.528 -3.755,-3.857 -1.052,-1.168 -2.308,-2.214 -3.334,-3.41 -0.684,-0.795 -1.344,-1.641 -1.948,-2.475 -0.308,-0.423 -0.469,-0.933 -0.712,-1.386 -0.361,-0.668 -5.318,-1.349 -4.825,-2.001 0.84,-1.111 2.71,-2.053 3.16,-3.325 2.299,-6.489 3.303,-32.603 3.58,-39.268 0.714,-17.044 15.676,0.832 18.289,7.691 7.601,19.954 3.086,34.504 -6.175,51.549"
|
||||||
|
android:strokeWidth="0.04884417"
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:fillType="nonZero"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m153.319,143.388c-0.011,-9.841 -1.234,-13.822 -6.721,-18.634 -3.432,-3.009 -7.445,-4.157 -13.832,-3.999 -6.387,-0.158 -10.99,1.784 -14.309,5.23 -5.007,5.199 -6.325,8.274 -6.339,18.114 -0.008,5.319 5.727,17.089 7.434,19.805 2.832,4.791 8.075,6.513 13.214,6.154 5.139,0.359 10.382,-1.363 13.213,-6.154 1.708,-2.716 7.348,-14.261 7.34,-20.517"
|
||||||
|
android:strokeWidth="0.04884417"
|
||||||
|
android:fillColor="#f1e6d3"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:fillType="nonZero"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m88.003,72.959c-1.282,-11.478 -5.144,-31.997 8.198,-44.165 1.472,-1.343 1.867,3.227 2.113,5.075 0.338,2.538 -0.007,9.789 0.045,12.344 0.095,4.812 1.103,7.329 0.634,9.528 -1.284,6.012 -2.314,5.624 -3.541,9.28 -2.434,7.25 -7.098,11.075 -7.449,7.937"
|
||||||
|
android:strokeWidth="0.04884417"
|
||||||
|
android:fillColor="#f1d7d4"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:fillType="nonZero"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m171.355,70.691c1.366,2.444 4.933,-10.701 6.051,-16.41 1.053,-5.374 2.39,-12.516 0.541,-17.855 -0.992,-2.864 -7.077,-11.276 -7.795,-8.386 -0.28,1.122 -0.424,9.48 -1.113,13.479 -0.852,4.948 -2.388,9.841 -2.397,14.873 -0.02,11.276 3.378,11.741 4.713,14.299"
|
||||||
|
android:strokeWidth="0.04884417"
|
||||||
|
android:fillColor="#f1d7d4"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:fillType="nonZero"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m127.391,205.803c-0.289,0.878 -2.479,1.521 -3.302,1.786 -1.988,0.642 -3.753,-0.925 -4.863,-2.246 -1.544,-1.831 -1.503,-4.751 -2.15,-6.886 -0.391,-1.291 -2.604,-1.466 -3.269,-0.272 -2.217,3.987 -10.821,-3.515 -8.822,-6.013 1.318,-1.647 -1.838,-3.102 -3.139,-1.477 -3.86,4.821 6.523,13.799 12.477,11.366 0.265,1.205 0.543,2.408 1.055,3.551 1.003,2.247 3.197,3.698 5.468,4.919 3.093,1.658 9.101,-1.254 10.019,-4.034 0.639,-1.934 -2.837,-2.618 -3.474,-0.695"
|
||||||
|
android:strokeWidth="0.04884417"
|
||||||
|
android:fillColor="#faeee0"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:fillType="nonZero"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m154.806,176.672c0.319,1.339 0.37,5.876 -2.125,3.394 -1.012,-1.007 -3.022,-0.413 -3.104,0.934 -0.193,3.118 -3.32,3.187 -6.018,2.244 -2.141,-0.747 -3.271,2.212 -1.147,2.953 3.962,1.382 8.261,0.616 9.97,-2.378 1.344,0.633 2.865,0.805 4.084,-0.342 2.129,-2.006 2.45,-4.83 1.831,-7.428 -0.47,-1.968 -3.96,-1.346 -3.49,0.623"
|
||||||
|
android:strokeWidth="0.04884417"
|
||||||
|
android:fillColor="#faeee0"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:fillType="nonZero"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m161.128,241.87c-0.934,1.909 -2.647,1.79 -4.485,0.988 -1.348,-0.587 -3.014,0.383 -2.608,1.748 0.818,2.736 -2.638,3.535 -4.975,2.559 -2.057,-0.859 -3.845,1.839 -1.766,2.708 4.211,1.757 9.564,0.22 10.34,-3.485 2.708,0.429 5.295,-0.331 6.586,-2.975 0.894,-1.824 -2.187,-3.396 -3.093,-1.544"
|
||||||
|
android:strokeWidth="0.04884417"
|
||||||
|
android:fillColor="#faeee0"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:fillType="nonZero"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m133.758,94.668c-0.434,2.691 -5.479,-0.443 -6.732,-1.292 -1.103,-0.746 -2.53,-0.293 -2.664,0.965 -0.18,1.695 -2.464,1.203 -3.508,0.99 -1.974,-0.401 -3.649,-1.632 -3.696,-3.481 -0.046,-1.796 1.094,-3.683 1.689,-5.369 0.38,-1.077 -1.163,-2.262 -2.291,-1.542 -2.032,1.297 -11.829,2.464 -7.927,-1.806 1.243,-1.36 -0.951,-3.306 -2.207,-1.931 -1.802,1.973 -3.021,6.493 0.878,7.45 2.058,0.505 5.037,0.479 7.575,-0.1 -1.06,3.397 -1.282,6.507 3.087,8.689 2.953,1.476 7.008,1.465 8.726,-0.814 3.615,1.944 9.295,3.831 10.08,-1.035 0.278,-1.721 -2.728,-2.464 -3.009,-0.725"
|
||||||
|
android:strokeWidth="0.04884417"
|
||||||
|
android:fillColor="#faeee0"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:fillType="nonZero"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m116.859,157.027c5.249,0.444 10.5,1.636 15.797,1.445 3.386,-0.123 6.892,-0.597 10.233,-1.093 2.427,-0.359 5.403,-0.784 7.026,-2.583 0.152,6.002 -12.335,7.881 -17.178,8.003 -5.619,0.142 -11.935,-2.296 -15.878,-5.772"
|
||||||
|
android:strokeWidth="0.04884417"
|
||||||
|
android:fillColor="#494949"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:fillType="nonZero"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m131.049,144.504c-0.6,3.853 -0.724,7.76 -0.363,11.636 0.096,1.013 -0.174,3.428 1.173,3.936 1.092,0.413 2.442,-0.363 2.62,-1.348 0.301,-1.661 -0.405,-3.755 -0.465,-5.456 -0.071,-1.992 0,-3.987 0.211,-5.969 0.126,-1.175 0.785,-2.761 -0.56,-3.589 -0.987,-0.607 -2.434,-0.383 -2.616,0.79"
|
||||||
|
android:strokeWidth="0.04884417"
|
||||||
|
android:fillColor="#494949"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:fillType="nonZero"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m141.253,135.138c-3.155,0.136 -6.084,1.902 -8.453,3.603 -2.144,-1.423 -5.391,-2.731 -8.092,-1.97 -1.61,0.452 -0.148,0.946 0.524,1.375 1.241,0.825 2.253,1.886 3.166,2.982 1.385,1.68 2.617,3.444 4.095,5.064 1.023,-0.737 2,-1.48 2.89,-2.341 0.413,-0.399 3.791,-4.923 4.31,-5.689 0.331,-0.487 0.665,-0.991 1.045,-1.45 0.385,-0.464 1.476,-1.202 0.517,-1.575"
|
||||||
|
android:strokeWidth="0.04884417"
|
||||||
|
android:fillColor="#494949"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:fillType="nonZero"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m101.325,113.765c1.558,-4.04 3.232,-9.118 7.502,-8.31 3.233,0.693 7.56,11.482 4.79,10.417 -1.5,-0.577 -2.694,-5.481 -5.511,-6.117 -1.79,-0.404 -4.271,5.8 -6.666,6.435 -1.161,0.307 -0.577,-1.385 -0.115,-2.424"
|
||||||
|
android:strokeWidth="0.04884417"
|
||||||
|
android:fillColor="#2c3844"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:fillType="nonZero"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m147.202,113.765c1.558,-4.04 3.233,-9.118 7.502,-8.31 3.232,0.693 7.559,11.482 4.79,10.417 -1.5,-0.577 -2.694,-5.481 -5.511,-6.117 -1.79,-0.404 -4.271,5.8 -6.665,6.435 -1.161,0.307 -0.577,-1.385 -0.116,-2.424"
|
||||||
|
android:strokeWidth="0.04884417"
|
||||||
|
android:fillColor="#2c3844"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:fillType="nonZero"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
After Width: | Height: | Size: 6.0 KiB |
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".MainActivity">
|
||||||
|
|
||||||
|
<WebView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
After Width: | Height: | Size: 5.3 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 7.5 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 17 KiB |
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#1973FF</color>
|
||||||
|
</resources>
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">Vikunja</string>
|
||||||
|
<string name="title_activity_main">Vikunja</string>
|
||||||
|
<string name="package_name">io.vikunja.app</string>
|
||||||
|
<string name="custom_url_scheme">io.vikunja.app</string>
|
||||||
|
</resources>
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
|
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||||
|
<item name="colorAccent">@color/colorAccent</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.NoActionBar">
|
||||||
|
<item name="windowActionBar">false</item>
|
||||||
|
<item name="windowNoTitle">true</item>
|
||||||
|
<item name="android:background">@null</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
<style name="AppTheme.NoActionBarLaunch" parent="AppTheme.NoActionBar">
|
||||||
|
<item name="android:background">@drawable/splash</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
|
<widget version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
|
||||||
|
<access origin="*" />
|
||||||
|
|
||||||
|
|
||||||
|
</widget>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<external-path name="my_images" path="." />
|
||||||
|
<cache-path name="my_cache_images" path="." />
|
||||||
|
</paths>
|
|
@ -0,0 +1,29 @@
|
||||||
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
jcenter()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath 'com.android.tools.build:gradle:3.6.1'
|
||||||
|
classpath 'com.google.gms:google-services:4.3.3'
|
||||||
|
|
||||||
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
|
// in the individual module build.gradle files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "variables.gradle"
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
jcenter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task clean(type: Delete) {
|
||||||
|
delete rootProject.buildDir
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
||||||
|
include ':capacitor-android'
|
||||||
|
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Project-wide Gradle settings.
|
||||||
|
|
||||||
|
# IDE (e.g. Android Studio) users:
|
||||||
|
# Gradle settings configured through the IDE *will override*
|
||||||
|
# any settings specified in this file.
|
||||||
|
|
||||||
|
# For more details on how to configure your build environment visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
|
|
||||||
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
|
org.gradle.jvmargs=-Xmx1536m
|
||||||
|
|
||||||
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
|
# org.gradle.parallel=true
|
||||||
|
|
||||||
|
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||||
|
# Android operating system, and which are packaged with your app's APK
|
||||||
|
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||||
|
android.useAndroidX=true
|
||||||
|
# Automatically convert third-party libraries to use AndroidX
|
||||||
|
android.enableJetifier=true
|
|
@ -0,0 +1,5 @@
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
|
@ -0,0 +1,188 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright 2015 the original author or authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
##
|
||||||
|
## Gradle start up script for UN*X
|
||||||
|
##
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
PRG="$0"
|
||||||
|
# Need this for relative symlinks.
|
||||||
|
while [ -h "$PRG" ] ; do
|
||||||
|
ls=`ls -ld "$PRG"`
|
||||||
|
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||||
|
if expr "$link" : '/.*' > /dev/null; then
|
||||||
|
PRG="$link"
|
||||||
|
else
|
||||||
|
PRG=`dirname "$PRG"`"/$link"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
SAVED="`pwd`"
|
||||||
|
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||||
|
APP_HOME="`pwd -P`"
|
||||||
|
cd "$SAVED" >/dev/null
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=`basename "$0"`
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD="maximum"
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "`uname`" in
|
||||||
|
CYGWIN* )
|
||||||
|
cygwin=true
|
||||||
|
;;
|
||||||
|
Darwin* )
|
||||||
|
darwin=true
|
||||||
|
;;
|
||||||
|
MINGW* )
|
||||||
|
msys=true
|
||||||
|
;;
|
||||||
|
NONSTOP* )
|
||||||
|
nonstop=true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
|
else
|
||||||
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD="java"
|
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||||
|
MAX_FD_LIMIT=`ulimit -H -n`
|
||||||
|
if [ $? -eq 0 ] ; then
|
||||||
|
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||||
|
MAX_FD="$MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
ulimit -n $MAX_FD
|
||||||
|
if [ $? -ne 0 ] ; then
|
||||||
|
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Darwin, add options to specify how the application appears in the dock
|
||||||
|
if $darwin; then
|
||||||
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||||
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
|
|
||||||
|
# We build the pattern for arguments to be converted via cygpath
|
||||||
|
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||||
|
SEP=""
|
||||||
|
for dir in $ROOTDIRSRAW ; do
|
||||||
|
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||||
|
SEP="|"
|
||||||
|
done
|
||||||
|
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||||
|
# Add a user-defined pattern to the cygpath arguments
|
||||||
|
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||||
|
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||||
|
fi
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
i=0
|
||||||
|
for arg in "$@" ; do
|
||||||
|
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||||
|
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||||
|
|
||||||
|
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||||
|
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||||
|
else
|
||||||
|
eval `echo args$i`="\"$arg\""
|
||||||
|
fi
|
||||||
|
i=$((i+1))
|
||||||
|
done
|
||||||
|
case $i in
|
||||||
|
(0) set -- ;;
|
||||||
|
(1) set -- "$args0" ;;
|
||||||
|
(2) set -- "$args0" "$args1" ;;
|
||||||
|
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||||
|
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||||
|
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||||
|
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||||
|
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||||
|
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||||
|
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Escape application args
|
||||||
|
save () {
|
||||||
|
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||||
|
echo " "
|
||||||
|
}
|
||||||
|
APP_ARGS=$(save "$@")
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||||
|
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||||
|
|
||||||
|
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||||
|
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
|
@ -0,0 +1,100 @@
|
||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%" == "" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if "%ERRORLEVEL%" == "0" goto init
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto init
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:init
|
||||||
|
@rem Get command-line arguments, handling Windows variants
|
||||||
|
|
||||||
|
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||||
|
|
||||||
|
:win9xME_args
|
||||||
|
@rem Slurp the command line arguments.
|
||||||
|
set CMD_LINE_ARGS=
|
||||||
|
set _SKIP=2
|
||||||
|
|
||||||
|
:win9xME_args_slurp
|
||||||
|
if "x%~1" == "x" goto execute
|
||||||
|
|
||||||
|
set CMD_LINE_ARGS=%*
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
|
@ -0,0 +1,5 @@
|
||||||
|
include ':app'
|
||||||
|
include ':capacitor-cordova-android-plugins'
|
||||||
|
project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
|
||||||
|
|
||||||
|
apply from: 'capacitor.settings.gradle'
|
|
@ -0,0 +1,17 @@
|
||||||
|
ext {
|
||||||
|
minSdkVersion = 21
|
||||||
|
compileSdkVersion = 29
|
||||||
|
targetSdkVersion = 29
|
||||||
|
androidxAppCompatVersion = '1.1.0'
|
||||||
|
androidxCoreVersion = '1.2.0'
|
||||||
|
androidxMaterialVersion = '1.1.0-rc02'
|
||||||
|
androidxBrowserVersion = '1.2.0'
|
||||||
|
androidxLocalbroadcastmanagerVersion = '1.0.0'
|
||||||
|
androidxExifInterfaceVersion = '1.2.0'
|
||||||
|
firebaseMessagingVersion = '20.1.2'
|
||||||
|
playServicesLocationVersion = '17.0.0'
|
||||||
|
junitVersion = '4.12'
|
||||||
|
androidxJunitVersion = '1.1.1'
|
||||||
|
androidxEspressoCoreVersion = '3.2.0'
|
||||||
|
cordovaAndroidVersion = '7.0.0'
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { CapacitorConfig } from '@capacitor/cli'
|
||||||
|
|
||||||
|
const config: CapacitorConfig = {
|
||||||
|
appId: 'io.vikunja.app',
|
||||||
|
appName: 'Vikunja',
|
||||||
|
webDir: 'dist',
|
||||||
|
bundledWebRuntime: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
59
cliff.toml
|
@ -1,59 +0,0 @@
|
||||||
[changelog]
|
|
||||||
body = """
|
|
||||||
{% if version %}\
|
|
||||||
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
|
||||||
{% else %}\
|
|
||||||
## [unreleased]
|
|
||||||
{% endif %}\
|
|
||||||
|
|
||||||
|
|
||||||
{% for group, commits in commits | group_by(attribute="group") %}
|
|
||||||
### {{ group | upper_first }}
|
|
||||||
{% for commit in commits
|
|
||||||
| filter(attribute="scope")
|
|
||||||
| sort(attribute="scope") %}
|
|
||||||
* *({{commit.scope}})* {{ commit.message | upper_first }}
|
|
||||||
{%- if commit.breaking %}
|
|
||||||
{% raw %} {% endraw %}- **BREAKING**: {{commit.breaking_description}}
|
|
||||||
{%- endif -%}
|
|
||||||
{%- endfor -%}
|
|
||||||
{%- for commit in commits %}
|
|
||||||
{%- if commit.scope -%}
|
|
||||||
{% else -%}
|
|
||||||
* {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ commit.id }}))
|
|
||||||
{% if commit.breaking -%}
|
|
||||||
{% raw %} {% endraw %}- **BREAKING**: {{commit.breaking_description}}
|
|
||||||
{% endif -%}
|
|
||||||
{% endif -%}
|
|
||||||
{% endfor -%}
|
|
||||||
{% raw %}\n{% endraw %}\
|
|
||||||
{% endfor %}\n
|
|
||||||
|
|
||||||
"""
|
|
||||||
#{% for group, commits in commits | group_by(attribute="group") %}
|
|
||||||
# ### {{ group | upper_first }}
|
|
||||||
# {% for commit in commits %}\
|
|
||||||
# - {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ commit.id }}))
|
|
||||||
# {% endfor %}\
|
|
||||||
#{% endfor %}\n
|
|
||||||
# remove the leading and trailing whitespace from the template
|
|
||||||
trim = true
|
|
||||||
|
|
||||||
[git]
|
|
||||||
conventional_commits = true
|
|
||||||
filter_unconventional = false
|
|
||||||
commit_parsers = [
|
|
||||||
{ message = ".*(deps).*", group = "Dependencies"},
|
|
||||||
{ message = "^feat", group = "Features"},
|
|
||||||
{ message = "^fix", group = "Bug Fixes"},
|
|
||||||
{ message = "^doc", group = "Documentation"},
|
|
||||||
{ message = "^perf", group = "Performance"},
|
|
||||||
{ message = "^refactor", group = "Refactor"},
|
|
||||||
{ message = "^style", group = "Styling"},
|
|
||||||
{ message = "^test", group = "Testing"},
|
|
||||||
{ message = "^chore\\(release\\): prepare for", skip = true},
|
|
||||||
{ message = "^chore", group = "Miscellaneous Tasks"},
|
|
||||||
{ body = ".*security", group = "Security"},
|
|
||||||
{ message = ".*", group = "Other", default_scope = "other"}, # Everything that's not a conventional commit goes into the "Other" category
|
|
||||||
]
|
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
import {defineConfig} from 'cypress'
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
env: {
|
|
||||||
API_URL: 'http://localhost:3456/api/v1',
|
|
||||||
TEST_SECRET: 'averyLongSecretToSe33dtheDB',
|
|
||||||
},
|
|
||||||
video: false,
|
|
||||||
retries: {
|
|
||||||
runMode: 2,
|
|
||||||
},
|
|
||||||
projectId: '181c7x',
|
|
||||||
e2e: {
|
|
||||||
specPattern: 'cypress/e2e/**/*.{cy,spec}.{js,jsx,ts,tsx}',
|
|
||||||
baseUrl: 'http://127.0.0.1:4173',
|
|
||||||
experimentalRunAllSpecs: true,
|
|
||||||
// testIsolation: false,
|
|
||||||
},
|
|
||||||
component: {
|
|
||||||
devServer: {
|
|
||||||
framework: 'vue',
|
|
||||||
bundler: 'vite',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
viewportWidth: 1600,
|
|
||||||
viewportHeight: 900,
|
|
||||||
experimentalMemoryManagement: true,
|
|
||||||
})
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"baseUrl": "http://localhost:5000",
|
||||||
|
"env": {
|
||||||
|
"API_URL": "http://localhost:3456/api/v1",
|
||||||
|
"TEST_SECRET": "averyLongSecretToSe33dtheDB"
|
||||||
|
},
|
||||||
|
"video": false,
|
||||||
|
"retries": {
|
||||||
|
"runMode": 2
|
||||||
|
}
|
||||||
|
}
|
|
@ -36,7 +36,7 @@ to get a shell inside the cypress container.
|
||||||
In that shell you can then execute the tests with
|
In that shell you can then execute the tests with
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
pnpm run test:e2e
|
yarn test:frontend
|
||||||
```
|
```
|
||||||
|
|
||||||
### Using The Cypress Dashboard
|
### Using The Cypress Dashboard
|
||||||
|
@ -44,5 +44,5 @@ pnpm run test:e2e
|
||||||
To open the Cypress Dashboard and run tests from there, run
|
To open the Cypress Dashboard and run tests from there, run
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
pnpm run test:e2e:dev
|
yarn cypress:open
|
||||||
```
|
```
|
||||||
|
|
|
@ -9,7 +9,7 @@ services:
|
||||||
ports:
|
ports:
|
||||||
- 3456:3456
|
- 3456:3456
|
||||||
cypress:
|
cypress:
|
||||||
image: cypress/browsers:node18.12.0-chrome107
|
image: cypress/browsers:node12.18.3-chrome87-ff82
|
||||||
volumes:
|
volumes:
|
||||||
- ..:/project
|
- ..:/project
|
||||||
- $HOME/.cache:/home/node/.cache/
|
- $HOME/.cache:/home/node/.cache/
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
import {ProjectFactory} from '../../factories/project'
|
|
||||||
import {TaskFactory} from '../../factories/task'
|
|
||||||
|
|
||||||
export function createProjects() {
|
|
||||||
const projects = ProjectFactory.create(1, {
|
|
||||||
title: 'First Project'
|
|
||||||
})
|
|
||||||
TaskFactory.truncate()
|
|
||||||
return projects
|
|
||||||
}
|
|
||||||
|
|
||||||
export function prepareProjects(setProjects = (...args: any[]) => {}) {
|
|
||||||
beforeEach(() => {
|
|
||||||
const projects = createProjects()
|
|
||||||
setProjects(projects)
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,50 +0,0 @@
|
||||||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
|
||||||
|
|
||||||
import {ProjectFactory} from '../../factories/project'
|
|
||||||
import {prepareProjects} from './prepareProjects'
|
|
||||||
|
|
||||||
describe('Project History', () => {
|
|
||||||
createFakeUserAndLogin()
|
|
||||||
prepareProjects()
|
|
||||||
|
|
||||||
it('should show a project history on the home page', () => {
|
|
||||||
cy.intercept(Cypress.env('API_URL') + '/projects*').as('loadProjectArray')
|
|
||||||
cy.intercept(Cypress.env('API_URL') + '/projects/*').as('loadProject')
|
|
||||||
|
|
||||||
const projects = ProjectFactory.create(6)
|
|
||||||
|
|
||||||
cy.visit('/')
|
|
||||||
cy.wait('@loadProjectArray')
|
|
||||||
cy.get('body')
|
|
||||||
.should('not.contain', 'Last viewed')
|
|
||||||
|
|
||||||
cy.visit(`/projects/${projects[0].id}`)
|
|
||||||
cy.wait('@loadProject')
|
|
||||||
cy.visit(`/projects/${projects[1].id}`)
|
|
||||||
cy.wait('@loadProject')
|
|
||||||
cy.visit(`/projects/${projects[2].id}`)
|
|
||||||
cy.wait('@loadProject')
|
|
||||||
cy.visit(`/projects/${projects[3].id}`)
|
|
||||||
cy.wait('@loadProject')
|
|
||||||
cy.visit(`/projects/${projects[4].id}`)
|
|
||||||
cy.wait('@loadProject')
|
|
||||||
cy.visit(`/projects/${projects[5].id}`)
|
|
||||||
cy.wait('@loadProject')
|
|
||||||
|
|
||||||
// cy.visit('/')
|
|
||||||
// Not using cy.visit here to work around the redirect issue fixed in #1337
|
|
||||||
cy.get('nav.menu.top-menu a')
|
|
||||||
.contains('Overview')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('body')
|
|
||||||
.should('contain', 'Last viewed')
|
|
||||||
cy.get('[data-cy="projectCardGrid"]')
|
|
||||||
.should('not.contain', projects[0].title)
|
|
||||||
.should('contain', projects[1].title)
|
|
||||||
.should('contain', projects[2].title)
|
|
||||||
.should('contain', projects[3].title)
|
|
||||||
.should('contain', projects[4].title)
|
|
||||||
.should('contain', projects[5].title)
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,126 +0,0 @@
|
||||||
import {formatISO, format} from 'date-fns'
|
|
||||||
|
|
||||||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
|
||||||
|
|
||||||
import {TaskFactory} from '../../factories/task'
|
|
||||||
import {prepareProjects} from './prepareProjects'
|
|
||||||
|
|
||||||
describe('Project View Gantt', () => {
|
|
||||||
createFakeUserAndLogin()
|
|
||||||
prepareProjects()
|
|
||||||
|
|
||||||
it('Hides tasks with no dates', () => {
|
|
||||||
const tasks = TaskFactory.create(1)
|
|
||||||
cy.visit('/projects/1/gantt')
|
|
||||||
|
|
||||||
cy.get('.g-gantt-rows-container')
|
|
||||||
.should('not.contain', tasks[0].title)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Shows tasks from the current and next month', () => {
|
|
||||||
const now = Date.UTC(2022, 8, 25)
|
|
||||||
cy.clock(now, ['Date'])
|
|
||||||
|
|
||||||
const nextMonth = new Date(now)
|
|
||||||
nextMonth.setDate(1)
|
|
||||||
nextMonth.setMonth(9)
|
|
||||||
|
|
||||||
cy.visit('/projects/1/gantt')
|
|
||||||
|
|
||||||
cy.get('.g-timeunits-container')
|
|
||||||
.should('contain', format(now, 'MMMM'))
|
|
||||||
.should('contain', format(nextMonth, 'MMMM'))
|
|
||||||
})
|
|
||||||
|
|
||||||
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(),
|
|
||||||
})
|
|
||||||
cy.visit('/projects/1/gantt')
|
|
||||||
|
|
||||||
cy.get('.g-gantt-rows-container')
|
|
||||||
.should('not.be.empty')
|
|
||||||
.should('contain', tasks[0].title)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Shows tasks with no dates after enabling them', () => {
|
|
||||||
const tasks = TaskFactory.create(1, {
|
|
||||||
start_date: null,
|
|
||||||
end_date: null,
|
|
||||||
})
|
|
||||||
cy.visit('/projects/1/gantt')
|
|
||||||
|
|
||||||
cy.get('.gantt-options .fancycheckbox')
|
|
||||||
.contains('Show tasks which don\'t have dates set')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.g-gantt-rows-container')
|
|
||||||
.should('not.be.empty')
|
|
||||||
.should('contain', tasks[0].title)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Drags a task around', () => {
|
|
||||||
cy.intercept(Cypress.env('API_URL') + '/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(),
|
|
||||||
})
|
|
||||||
cy.visit('/projects/1/gantt')
|
|
||||||
|
|
||||||
cy.get('.g-gantt-rows-container .g-gantt-row .g-gantt-row-bars-container div .g-gantt-bar')
|
|
||||||
.first()
|
|
||||||
.trigger('mousedown', {which: 1})
|
|
||||||
.trigger('mousemove', {clientX: 500, clientY: 0})
|
|
||||||
.trigger('mouseup', {force: true})
|
|
||||||
cy.wait('@taskUpdate')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should change the query parameters when selecting a date range', () => {
|
|
||||||
const now = Date.UTC(2022, 10, 9)
|
|
||||||
cy.clock(now, ['Date'])
|
|
||||||
|
|
||||||
cy.visit('/projects/1/gantt')
|
|
||||||
|
|
||||||
cy.get('.project-gantt .gantt-options .field .control input.input.form-control')
|
|
||||||
.click()
|
|
||||||
cy.get('.flatpickr-calendar .flatpickr-innerContainer .dayContainer .flatpickr-day')
|
|
||||||
.first()
|
|
||||||
.click()
|
|
||||||
cy.get('.flatpickr-calendar .flatpickr-innerContainer .dayContainer .flatpickr-day')
|
|
||||||
.last()
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.url().should('contain', 'dateFrom=2022-09-25')
|
|
||||||
cy.url().should('contain', 'dateTo=2022-11-05')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should change the date range based on date query parameters', () => {
|
|
||||||
cy.visit('/projects/1/gantt?dateFrom=2022-09-25&dateTo=2022-11-05')
|
|
||||||
|
|
||||||
cy.get('.g-timeunits-container')
|
|
||||||
.should('contain', 'September 2022')
|
|
||||||
.should('contain', 'October 2022')
|
|
||||||
.should('contain', 'November 2022')
|
|
||||||
cy.get('.project-gantt .gantt-options .field .control input.input.form-control')
|
|
||||||
.should('have.value', '25 Sep 2022 to 5 Nov 2022')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should open a task when double clicked on it', () => {
|
|
||||||
const now = new Date()
|
|
||||||
const tasks = TaskFactory.create(1, {
|
|
||||||
start_date: formatISO(now),
|
|
||||||
end_date: formatISO(now.setDate(now.getDate() + 4)),
|
|
||||||
})
|
|
||||||
cy.visit('/projects/1/gantt')
|
|
||||||
|
|
||||||
cy.get('.gantt-container .g-gantt-chart .g-gantt-row-bars-container .g-gantt-bar')
|
|
||||||
.dblclick()
|
|
||||||
|
|
||||||
cy.url()
|
|
||||||
.should('contain', `/tasks/${tasks[0].id}`)
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,285 +0,0 @@
|
||||||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
|
||||||
|
|
||||||
import {BucketFactory} from '../../factories/bucket'
|
|
||||||
import {ProjectFactory} from '../../factories/project'
|
|
||||||
import {TaskFactory} from '../../factories/task'
|
|
||||||
import {prepareProjects} from './prepareProjects'
|
|
||||||
|
|
||||||
function createSingleTaskInBucket(count = 1, attrs = {}) {
|
|
||||||
const projects = ProjectFactory.create(1)
|
|
||||||
const buckets = BucketFactory.create(2, {
|
|
||||||
project_id: projects[0].id,
|
|
||||||
})
|
|
||||||
const tasks = TaskFactory.create(count, {
|
|
||||||
project_id: projects[0].id,
|
|
||||||
bucket_id: buckets[0].id,
|
|
||||||
...attrs,
|
|
||||||
})
|
|
||||||
return tasks[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Project View Kanban', () => {
|
|
||||||
createFakeUserAndLogin()
|
|
||||||
prepareProjects()
|
|
||||||
|
|
||||||
let buckets
|
|
||||||
beforeEach(() => {
|
|
||||||
buckets = BucketFactory.create(2)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Shows all buckets with their tasks', () => {
|
|
||||||
const data = TaskFactory.create(10, {
|
|
||||||
project_id: 1,
|
|
||||||
bucket_id: 1,
|
|
||||||
})
|
|
||||||
cy.visit('/projects/1/kanban')
|
|
||||||
|
|
||||||
cy.get('.kanban .bucket .title')
|
|
||||||
.contains(buckets[0].title)
|
|
||||||
.should('exist')
|
|
||||||
cy.get('.kanban .bucket .title')
|
|
||||||
.contains(buckets[1].title)
|
|
||||||
.should('exist')
|
|
||||||
cy.get('.kanban .bucket')
|
|
||||||
.first()
|
|
||||||
.should('contain', data[0].title)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Can add a new task to a bucket', () => {
|
|
||||||
TaskFactory.create(2, {
|
|
||||||
project_id: 1,
|
|
||||||
bucket_id: 1,
|
|
||||||
})
|
|
||||||
cy.visit('/projects/1/kanban')
|
|
||||||
|
|
||||||
cy.get('.kanban .bucket')
|
|
||||||
.contains(buckets[0].title)
|
|
||||||
.get('.bucket-footer .button')
|
|
||||||
.contains('Add another task')
|
|
||||||
.click()
|
|
||||||
cy.get('.kanban .bucket')
|
|
||||||
.contains(buckets[0].title)
|
|
||||||
.get('.bucket-footer .field .control input.input')
|
|
||||||
.type('New Task{enter}')
|
|
||||||
|
|
||||||
cy.get('.kanban .bucket')
|
|
||||||
.first()
|
|
||||||
.should('contain', 'New Task')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Can create a new bucket', () => {
|
|
||||||
cy.visit('/projects/1/kanban')
|
|
||||||
|
|
||||||
cy.get('.kanban .bucket.new-bucket .button')
|
|
||||||
.click()
|
|
||||||
cy.get('.kanban .bucket.new-bucket input.input')
|
|
||||||
.type('New Bucket{enter}')
|
|
||||||
|
|
||||||
cy.wait(1000) // Wait for the request to finish
|
|
||||||
cy.get('.kanban .bucket .title')
|
|
||||||
.contains('New Bucket')
|
|
||||||
.should('exist')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Can set a bucket limit', () => {
|
|
||||||
cy.visit('/projects/1/kanban')
|
|
||||||
|
|
||||||
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger')
|
|
||||||
.first()
|
|
||||||
.click()
|
|
||||||
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item')
|
|
||||||
.contains('Limit: Not Set')
|
|
||||||
.click()
|
|
||||||
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item .field input.input')
|
|
||||||
.first()
|
|
||||||
.type(3)
|
|
||||||
cy.get('[data-cy="setBucketLimit"]')
|
|
||||||
.first()
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.kanban .bucket .bucket-header span.limit')
|
|
||||||
.contains('0/3')
|
|
||||||
.should('exist')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Can rename a bucket', () => {
|
|
||||||
cy.visit('/projects/1/kanban')
|
|
||||||
|
|
||||||
cy.get('.kanban .bucket .bucket-header .title')
|
|
||||||
.first()
|
|
||||||
.type('{selectall}New Bucket Title{enter}')
|
|
||||||
cy.get('.kanban .bucket .bucket-header .title')
|
|
||||||
.first()
|
|
||||||
.should('contain', 'New Bucket Title')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Can delete a bucket', () => {
|
|
||||||
cy.visit('/projects/1/kanban')
|
|
||||||
|
|
||||||
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger')
|
|
||||||
.first()
|
|
||||||
.click()
|
|
||||||
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item')
|
|
||||||
.contains('Delete')
|
|
||||||
.click()
|
|
||||||
cy.get('.modal-mask .modal-container .modal-content .header')
|
|
||||||
.should('contain', 'Delete the bucket')
|
|
||||||
cy.get('.modal-mask .modal-container .modal-content .actions .button')
|
|
||||||
.contains('Do it!')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.kanban .bucket .title')
|
|
||||||
.contains(buckets[0].title)
|
|
||||||
.should('not.exist')
|
|
||||||
cy.get('.kanban .bucket .title')
|
|
||||||
.contains(buckets[1].title)
|
|
||||||
.should('exist')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Can drag tasks around', () => {
|
|
||||||
const tasks = TaskFactory.create(2, {
|
|
||||||
project_id: 1,
|
|
||||||
bucket_id: 1,
|
|
||||||
})
|
|
||||||
cy.visit('/projects/1/kanban')
|
|
||||||
|
|
||||||
cy.get('.kanban .bucket .tasks .task')
|
|
||||||
.contains(tasks[0].title)
|
|
||||||
.first()
|
|
||||||
.drag('.kanban .bucket:nth-child(2) .tasks')
|
|
||||||
|
|
||||||
cy.get('.kanban .bucket:nth-child(2) .tasks')
|
|
||||||
.should('contain', tasks[0].title)
|
|
||||||
cy.get('.kanban .bucket:nth-child(1) .tasks')
|
|
||||||
.should('not.contain', tasks[0].title)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should navigate to the task when the task card is clicked', () => {
|
|
||||||
const tasks = TaskFactory.create(5, {
|
|
||||||
id: '{increment}',
|
|
||||||
project_id: 1,
|
|
||||||
bucket_id: 1,
|
|
||||||
})
|
|
||||||
cy.visit('/projects/1/kanban')
|
|
||||||
|
|
||||||
cy.get('.kanban .bucket .tasks .task')
|
|
||||||
.contains(tasks[0].title)
|
|
||||||
.should('be.visible')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.url()
|
|
||||||
.should('contain', `/tasks/${tasks[0].id}`, { timeout: 1000 })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should remove a task from the kanban board when moving it to another project', () => {
|
|
||||||
const projects = ProjectFactory.create(2)
|
|
||||||
BucketFactory.create(2, {
|
|
||||||
project_id: '{increment}',
|
|
||||||
})
|
|
||||||
const tasks = TaskFactory.create(5, {
|
|
||||||
id: '{increment}',
|
|
||||||
project_id: 1,
|
|
||||||
bucket_id: 1,
|
|
||||||
})
|
|
||||||
const task = tasks[0]
|
|
||||||
cy.visit('/projects/1/kanban')
|
|
||||||
|
|
||||||
cy.get('.kanban .bucket .tasks .task')
|
|
||||||
.contains(task.title)
|
|
||||||
.should('be.visible')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.task-view .action-buttons .button', { timeout: 3000 })
|
|
||||||
.contains('Move')
|
|
||||||
.click()
|
|
||||||
cy.get('.task-view .content.details .field .multiselect.control .input-wrapper input')
|
|
||||||
.type(`${projects[1].title}{enter}`)
|
|
||||||
// The requests happen with a 200ms timeout. Because of that, the results are not yet there when cypress
|
|
||||||
// presses enter and we can't simulate pressing on enter to select the item.
|
|
||||||
cy.get('.task-view .content.details .field .multiselect.control .search-results')
|
|
||||||
.children()
|
|
||||||
.first()
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.global-notification', { timeout: 1000 })
|
|
||||||
.should('contain', 'Success')
|
|
||||||
cy.go('back')
|
|
||||||
cy.get('.kanban .bucket')
|
|
||||||
.should('not.contain', task.title)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Shows a button to filter the kanban board', () => {
|
|
||||||
const data = TaskFactory.create(10, {
|
|
||||||
project_id: 1,
|
|
||||||
bucket_id: 1,
|
|
||||||
})
|
|
||||||
cy.visit('/projects/1/kanban')
|
|
||||||
|
|
||||||
cy.get('.project-kanban .filter-container .base-button')
|
|
||||||
.should('exist')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should remove a task from the board when deleting it', () => {
|
|
||||||
const task = createSingleTaskInBucket(5)
|
|
||||||
cy.visit('/projects/1/kanban')
|
|
||||||
|
|
||||||
cy.get('.kanban .bucket .tasks .task')
|
|
||||||
.contains(task.title)
|
|
||||||
.should('be.visible')
|
|
||||||
.click()
|
|
||||||
cy.get('.task-view .action-buttons .button')
|
|
||||||
.should('be.visible')
|
|
||||||
.contains('Delete')
|
|
||||||
.click()
|
|
||||||
cy.get('.modal-mask .modal-container .modal-content .header')
|
|
||||||
.should('contain', 'Delete this task')
|
|
||||||
cy.get('.modal-mask .modal-container .modal-content .actions .button')
|
|
||||||
.contains('Do it!')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.global-notification')
|
|
||||||
.should('contain', 'Success')
|
|
||||||
|
|
||||||
cy.get('.kanban .bucket .tasks')
|
|
||||||
.should('not.contain', task.title)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should show a task description icon if the task has a description', () => {
|
|
||||||
cy.intercept(Cypress.env('API_URL') + '/projects/1/buckets**').as('loadTasks')
|
|
||||||
const task = createSingleTaskInBucket(1, {
|
|
||||||
description: 'Lorem Ipsum',
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.visit(`/projects/${task.project_id}/kanban`)
|
|
||||||
cy.wait('@loadTasks')
|
|
||||||
|
|
||||||
cy.get('.bucket .tasks .task .footer .icon svg')
|
|
||||||
.should('exist')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should not show a task description icon if the task has an empty description', () => {
|
|
||||||
cy.intercept(Cypress.env('API_URL') + '/projects/1/buckets**').as('loadTasks')
|
|
||||||
const task = createSingleTaskInBucket(1, {
|
|
||||||
description: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.visit(`/projects/${task.project_id}/kanban`)
|
|
||||||
cy.wait('@loadTasks')
|
|
||||||
|
|
||||||
cy.get('.bucket .tasks .task .footer .icon svg')
|
|
||||||
.should('not.exist')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should not show a task description icon if the task has a description containing only an empty p tag', () => {
|
|
||||||
cy.intercept(Cypress.env('API_URL') + '/projects/1/buckets**').as('loadTasks')
|
|
||||||
const task = createSingleTaskInBucket(1, {
|
|
||||||
description: '<p></p>',
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.visit(`/projects/${task.project_id}/kanban`)
|
|
||||||
cy.wait('@loadTasks')
|
|
||||||
|
|
||||||
cy.get('.bucket .tasks .task .footer .icon svg')
|
|
||||||
.should('not.exist')
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,109 +0,0 @@
|
||||||
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'
|
|
||||||
|
|
||||||
describe('Project View Project', () => {
|
|
||||||
createFakeUserAndLogin()
|
|
||||||
prepareProjects()
|
|
||||||
|
|
||||||
it('Should be an empty project', () => {
|
|
||||||
cy.visit('/projects/1')
|
|
||||||
cy.url()
|
|
||||||
.should('contain', '/projects/1/list')
|
|
||||||
cy.get('.project-title')
|
|
||||||
.should('contain', 'First Project')
|
|
||||||
cy.get('.project-title-dropdown')
|
|
||||||
.should('exist')
|
|
||||||
cy.get('p')
|
|
||||||
.contains('This project is currently empty.')
|
|
||||||
.should('exist')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should create a new task', () => {
|
|
||||||
const newTaskTitle = 'New task'
|
|
||||||
|
|
||||||
cy.visit('/projects/1')
|
|
||||||
cy.get('.task-add textarea')
|
|
||||||
.type(newTaskTitle+'{enter}')
|
|
||||||
cy.get('.tasks')
|
|
||||||
.should('contain.text', newTaskTitle)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should navigate to the task when the title is clicked', () => {
|
|
||||||
const tasks = TaskFactory.create(5, {
|
|
||||||
id: '{increment}',
|
|
||||||
project_id: 1,
|
|
||||||
})
|
|
||||||
cy.visit('/projects/1/list')
|
|
||||||
|
|
||||||
cy.get('.tasks .task .tasktext')
|
|
||||||
.contains(tasks[0].title)
|
|
||||||
.first()
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.url()
|
|
||||||
.should('contain', `/tasks/${tasks[0].id}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should not see any elements for a project which is shared read only', () => {
|
|
||||||
UserFactory.create(2)
|
|
||||||
UserProjectFactory.create(1, {
|
|
||||||
project_id: 2,
|
|
||||||
user_id: 1,
|
|
||||||
right: 0,
|
|
||||||
})
|
|
||||||
const projects = ProjectFactory.create(2, {
|
|
||||||
owner_id: '{increment}',
|
|
||||||
})
|
|
||||||
cy.visit(`/projects/${projects[1].id}/`)
|
|
||||||
|
|
||||||
cy.get('.project-title-wrapper .icon')
|
|
||||||
.should('not.exist')
|
|
||||||
cy.get('input.input[placeholder="Add a new task..."')
|
|
||||||
.should('not.exist')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should only show the color of a project in the navigation and not in the list view', () => {
|
|
||||||
const projects = ProjectFactory.create(1, {
|
|
||||||
hex_color: '00db60',
|
|
||||||
})
|
|
||||||
TaskFactory.create(10, {
|
|
||||||
project_id: projects[0].id,
|
|
||||||
})
|
|
||||||
cy.visit(`/projects/${projects[0].id}/`)
|
|
||||||
|
|
||||||
cy.get('.menu-list li .list-menu-link .color-bubble')
|
|
||||||
.should('have.css', 'background-color', 'rgb(0, 219, 96)')
|
|
||||||
cy.get('.tasks .color-bubble')
|
|
||||||
.should('not.exist')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should paginate for > 50 tasks', () => {
|
|
||||||
const tasks = TaskFactory.create(100, {
|
|
||||||
id: '{increment}',
|
|
||||||
title: i => `task${i}`,
|
|
||||||
project_id: 1,
|
|
||||||
})
|
|
||||||
cy.visit('/projects/1/list')
|
|
||||||
|
|
||||||
cy.get('.tasks')
|
|
||||||
.should('contain', tasks[1].title)
|
|
||||||
cy.get('.tasks')
|
|
||||||
.should('not.contain', tasks[99].title)
|
|
||||||
|
|
||||||
cy.get('.card-content .pagination .pagination-link')
|
|
||||||
.contains('2')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.url()
|
|
||||||
.should('contain', '?page=2')
|
|
||||||
cy.get('.tasks')
|
|
||||||
.should('contain', tasks[99].title)
|
|
||||||
cy.get('.tasks')
|
|
||||||
.should('not.contain', tasks[1].title)
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,54 +0,0 @@
|
||||||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
|
||||||
|
|
||||||
import {TaskFactory} from '../../factories/task'
|
|
||||||
|
|
||||||
describe('Project View Table', () => {
|
|
||||||
createFakeUserAndLogin()
|
|
||||||
|
|
||||||
it('Should show a table with tasks', () => {
|
|
||||||
const tasks = TaskFactory.create(1)
|
|
||||||
cy.visit('/projects/1/table')
|
|
||||||
|
|
||||||
cy.get('.project-table table.table')
|
|
||||||
.should('exist')
|
|
||||||
cy.get('.project-table table.table')
|
|
||||||
.should('contain', tasks[0].title)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should have working column switches', () => {
|
|
||||||
TaskFactory.create(1)
|
|
||||||
cy.visit('/projects/1/table')
|
|
||||||
|
|
||||||
cy.get('.project-table .filter-container .items .button')
|
|
||||||
.contains('Columns')
|
|
||||||
.click()
|
|
||||||
cy.get('.project-table .filter-container .card.columns-filter .card-content .fancycheckbox')
|
|
||||||
.contains('Priority')
|
|
||||||
.click()
|
|
||||||
cy.get('.project-table .filter-container .card.columns-filter .card-content .fancycheckbox')
|
|
||||||
.contains('Done')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.project-table table.table th')
|
|
||||||
.contains('Priority')
|
|
||||||
.should('exist')
|
|
||||||
cy.get('.project-table table.table th')
|
|
||||||
.contains('Done')
|
|
||||||
.should('not.exist')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should navigate to the task when the title is clicked', () => {
|
|
||||||
const tasks = TaskFactory.create(5, {
|
|
||||||
id: '{increment}',
|
|
||||||
project_id: 1,
|
|
||||||
})
|
|
||||||
cy.visit('/projects/1/table')
|
|
||||||
|
|
||||||
cy.get('.project-table table.table')
|
|
||||||
.contains(tasks[0].title)
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.url()
|
|
||||||
.should('contain', `/tasks/${tasks[0].id}`)
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,171 +0,0 @@
|
||||||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
|
||||||
|
|
||||||
import {TaskFactory} from '../../factories/task'
|
|
||||||
import {ProjectFactory} from '../../factories/project'
|
|
||||||
import {prepareProjects} from './prepareProjects'
|
|
||||||
|
|
||||||
describe('Projects', () => {
|
|
||||||
createFakeUserAndLogin()
|
|
||||||
|
|
||||||
let projects
|
|
||||||
prepareProjects((newProjects) => (projects = newProjects))
|
|
||||||
|
|
||||||
it('Should create a new project', () => {
|
|
||||||
cy.visit('/projects')
|
|
||||||
cy.get('.project-header [data-cy=new-project]')
|
|
||||||
.click()
|
|
||||||
cy.url()
|
|
||||||
.should('contain', '/projects/new')
|
|
||||||
cy.get('.card-header-title')
|
|
||||||
.contains('New project')
|
|
||||||
cy.get('input[name=projectTitle]')
|
|
||||||
.type('New Project')
|
|
||||||
cy.get('.button')
|
|
||||||
.contains('Create')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.global-notification', {timeout: 1000}) // Waiting until the request to create the new project is done
|
|
||||||
.should('contain', 'Success')
|
|
||||||
cy.url()
|
|
||||||
.should('contain', '/projects/')
|
|
||||||
cy.get('.project-title')
|
|
||||||
.should('contain', 'New Project')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should redirect to a specific project view after visited', () => {
|
|
||||||
cy.intercept(Cypress.env('API_URL') + '/projects/*/buckets*').as('loadBuckets')
|
|
||||||
cy.visit('/projects/1/kanban')
|
|
||||||
cy.url()
|
|
||||||
.should('contain', '/projects/1/kanban')
|
|
||||||
cy.wait('@loadBuckets')
|
|
||||||
cy.visit('/projects/1')
|
|
||||||
cy.url()
|
|
||||||
.should('contain', '/projects/1/kanban')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should rename the project in all places', () => {
|
|
||||||
TaskFactory.create(5, {
|
|
||||||
id: '{increment}',
|
|
||||||
project_id: 1,
|
|
||||||
})
|
|
||||||
const newProjectName = 'New project name'
|
|
||||||
|
|
||||||
cy.visit('/projects/1')
|
|
||||||
cy.get('.project-title')
|
|
||||||
.should('contain', 'First Project')
|
|
||||||
|
|
||||||
cy.get('.menu-container .menu-list li:first-child .dropdown .menu-list-dropdown-trigger')
|
|
||||||
.click()
|
|
||||||
cy.get('.menu-container .menu-list li:first-child .dropdown .dropdown-content')
|
|
||||||
.contains('Edit')
|
|
||||||
.click()
|
|
||||||
cy.get('#title')
|
|
||||||
.type(`{selectall}${newProjectName}`)
|
|
||||||
cy.get('footer.card-footer .button')
|
|
||||||
.contains('Save')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.global-notification')
|
|
||||||
.should('contain', 'Success')
|
|
||||||
cy.get('.project-title')
|
|
||||||
.should('contain', newProjectName)
|
|
||||||
.should('not.contain', projects[0].title)
|
|
||||||
cy.get('.menu-container .menu-list li:first-child')
|
|
||||||
.should('contain', newProjectName)
|
|
||||||
.should('not.contain', projects[0].title)
|
|
||||||
cy.visit('/')
|
|
||||||
cy.get('.project-grid')
|
|
||||||
.should('contain', newProjectName)
|
|
||||||
.should('not.contain', projects[0].title)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should remove a project when deleting it', () => {
|
|
||||||
cy.visit(`/projects/${projects[0].id}`)
|
|
||||||
|
|
||||||
cy.get('.menu-container .menu-list li:first-child .dropdown .menu-list-dropdown-trigger')
|
|
||||||
.click()
|
|
||||||
cy.get('.menu-container .menu-list li:first-child .dropdown .dropdown-content')
|
|
||||||
.contains('Delete')
|
|
||||||
.click()
|
|
||||||
cy.url()
|
|
||||||
.should('contain', '/settings/delete')
|
|
||||||
cy.get('[data-cy="modalPrimary"]')
|
|
||||||
.contains('Do it')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.global-notification')
|
|
||||||
.should('contain', 'Success')
|
|
||||||
cy.get('.menu-container .menu-list')
|
|
||||||
.should('not.contain', projects[0].title)
|
|
||||||
cy.location('pathname')
|
|
||||||
.should('equal', '/')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should archive a project', () => {
|
|
||||||
cy.visit(`/projects/${projects[0].id}`)
|
|
||||||
|
|
||||||
cy.get('.project-title-dropdown')
|
|
||||||
.click()
|
|
||||||
cy.get('.project-title-dropdown .dropdown-menu .dropdown-item')
|
|
||||||
.contains('Archive')
|
|
||||||
.click()
|
|
||||||
cy.get('.modal-content')
|
|
||||||
.should('contain.text', 'Archive this project')
|
|
||||||
cy.get('.modal-content [data-cy=modalPrimary]')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.menu-container .menu-list')
|
|
||||||
.should('not.contain', projects[0].title)
|
|
||||||
cy.get('main.app-content')
|
|
||||||
.should('contain.text', 'This project is archived. It is not possible to create new or edit tasks for it.')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should show all projects on the projects page', () => {
|
|
||||||
const projects = ProjectFactory.create(10)
|
|
||||||
|
|
||||||
cy.visit('/projects')
|
|
||||||
|
|
||||||
projects.forEach(p => {
|
|
||||||
cy.get('[data-cy="projects-list"]')
|
|
||||||
.should('contain', p.title)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should not show archived projects if the filter is not checked', () => {
|
|
||||||
ProjectFactory.create(1, {
|
|
||||||
id: 2,
|
|
||||||
}, false)
|
|
||||||
ProjectFactory.create(1, {
|
|
||||||
id: 3,
|
|
||||||
is_archived: true,
|
|
||||||
}, false)
|
|
||||||
|
|
||||||
// Initial
|
|
||||||
cy.visit('/projects')
|
|
||||||
cy.get('.project-grid')
|
|
||||||
.should('not.contain', 'Archived')
|
|
||||||
|
|
||||||
// Show archived
|
|
||||||
cy.get('[data-cy="show-archived-check"] label span')
|
|
||||||
.should('be.visible')
|
|
||||||
.click()
|
|
||||||
cy.get('[data-cy="show-archived-check"] input')
|
|
||||||
.should('be.checked')
|
|
||||||
cy.get('.project-grid')
|
|
||||||
.should('contain', 'Archived')
|
|
||||||
|
|
||||||
// Don't show archived
|
|
||||||
cy.get('[data-cy="show-archived-check"] label span')
|
|
||||||
.should('be.visible')
|
|
||||||
.click()
|
|
||||||
cy.get('[data-cy="show-archived-check"] input')
|
|
||||||
.should('not.be.checked')
|
|
||||||
|
|
||||||
// Second time visiting after unchecking
|
|
||||||
cy.visit('/projects')
|
|
||||||
cy.get('[data-cy="show-archived-check"] input')
|
|
||||||
.should('not.be.checked')
|
|
||||||
cy.get('.project-grid')
|
|
||||||
.should('not.contain', 'Archived')
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,59 +0,0 @@
|
||||||
import {LinkShareFactory} from '../../factories/link_sharing'
|
|
||||||
import {ProjectFactory} from '../../factories/project'
|
|
||||||
import {TaskFactory} from '../../factories/task'
|
|
||||||
|
|
||||||
function prepareLinkShare() {
|
|
||||||
const projects = ProjectFactory.create(1)
|
|
||||||
const tasks = TaskFactory.create(10, {
|
|
||||||
project_id: projects[0].id
|
|
||||||
})
|
|
||||||
const linkShares = LinkShareFactory.create(1, {
|
|
||||||
project_id: projects[0].id,
|
|
||||||
right: 0,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
share: linkShares[0],
|
|
||||||
project: projects[0],
|
|
||||||
tasks,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Link shares', () => {
|
|
||||||
it('Can view a link share', () => {
|
|
||||||
const {share, project, tasks} = prepareLinkShare()
|
|
||||||
|
|
||||||
cy.visit(`/share/${share.hash}/auth`)
|
|
||||||
|
|
||||||
cy.get('h1.title')
|
|
||||||
.should('contain', project.title)
|
|
||||||
cy.get('input.input[placeholder="Add a new task..."')
|
|
||||||
.should('not.exist')
|
|
||||||
cy.get('.tasks')
|
|
||||||
.should('contain', tasks[0].title)
|
|
||||||
|
|
||||||
cy.url().should('contain', `/projects/${project.id}/list#share-auth-token=${share.hash}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should work when directly viewing a project with share hash present', () => {
|
|
||||||
const {share, project, tasks} = prepareLinkShare()
|
|
||||||
|
|
||||||
cy.visit(`/projects/${project.id}/list#share-auth-token=${share.hash}`)
|
|
||||||
|
|
||||||
cy.get('h1.title')
|
|
||||||
.should('contain', project.title)
|
|
||||||
cy.get('input.input[placeholder="Add a new task..."')
|
|
||||||
.should('not.exist')
|
|
||||||
cy.get('.tasks')
|
|
||||||
.should('contain', tasks[0].title)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should work when directly viewing a task with share hash present', () => {
|
|
||||||
const {share, project, tasks} = prepareLinkShare()
|
|
||||||
|
|
||||||
cy.visit(`/tasks/${tasks[0].id}#share-auth-token=${share.hash}`)
|
|
||||||
|
|
||||||
cy.get('h1.title')
|
|
||||||
.should('contain', tasks[0].title)
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,150 +0,0 @@
|
||||||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
|
||||||
|
|
||||||
import {ProjectFactory} from '../../factories/project'
|
|
||||||
import {seed} from '../../support/seed'
|
|
||||||
import {TaskFactory} from '../../factories/task'
|
|
||||||
import {BucketFactory} from '../../factories/bucket'
|
|
||||||
import {updateUserSettings} from '../../support/updateUserSettings'
|
|
||||||
|
|
||||||
function seedTasks(numberOfTasks = 50, startDueDate = new Date()) {
|
|
||||||
const project = ProjectFactory.create()[0]
|
|
||||||
BucketFactory.create(1, {
|
|
||||||
project_id: project.id,
|
|
||||||
})
|
|
||||||
const tasks = []
|
|
||||||
let dueDate = startDueDate
|
|
||||||
for (let i = 0; i < numberOfTasks; i++) {
|
|
||||||
const now = new Date()
|
|
||||||
dueDate = new Date(new Date(dueDate).setDate(dueDate.getDate() + 2))
|
|
||||||
tasks.push({
|
|
||||||
id: i + 1,
|
|
||||||
project_id: project.id,
|
|
||||||
done: false,
|
|
||||||
created_by_id: 1,
|
|
||||||
title: 'Test Task ' + i,
|
|
||||||
index: i + 1,
|
|
||||||
due_date: dueDate.toISOString(),
|
|
||||||
created: now.toISOString(),
|
|
||||||
updated: now.toISOString(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
seed(TaskFactory.table, tasks)
|
|
||||||
return {tasks, project}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
cy.visit('/')
|
|
||||||
cy.get('[data-cy="showTasks"] .card .task')
|
|
||||||
.each(([task], index) => {
|
|
||||||
expect(task.innerText).to.contain(tasks[index].title)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
cy.visit('/')
|
|
||||||
cy.get('[data-cy="showTasks"] .card .task')
|
|
||||||
.each(([task], index) => {
|
|
||||||
expect(task.innerText).to.contain(tasks[index].title)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should show a new task with a very soon due date at the top', () => {
|
|
||||||
const {tasks} = seedTasks()
|
|
||||||
const newTaskTitle = 'New Task'
|
|
||||||
|
|
||||||
cy.visit('/')
|
|
||||||
|
|
||||||
TaskFactory.create(1, {
|
|
||||||
id: 999,
|
|
||||||
title: newTaskTitle,
|
|
||||||
due_date: new Date().toISOString(),
|
|
||||||
}, false)
|
|
||||||
|
|
||||||
cy.visit(`/projects/${tasks[0].project_id}/list`)
|
|
||||||
cy.get('.tasks .task')
|
|
||||||
.first()
|
|
||||||
.should('contain.text', newTaskTitle)
|
|
||||||
cy.visit('/')
|
|
||||||
cy.get('[data-cy="showTasks"] .card .task')
|
|
||||||
.first()
|
|
||||||
.should('contain.text', newTaskTitle)
|
|
||||||
})
|
|
||||||
|
|
||||||
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 newTaskTitle = 'New Task'
|
|
||||||
|
|
||||||
cy.visit('/')
|
|
||||||
|
|
||||||
cy.visit(`/projects/${tasks[0].project_id}/list`)
|
|
||||||
cy.get('.task-add textarea')
|
|
||||||
.type(newTaskTitle+'{enter}')
|
|
||||||
cy.visit('/')
|
|
||||||
cy.get('[data-cy="showTasks"] .card .task')
|
|
||||||
.last()
|
|
||||||
.should('not.contain.text', newTaskTitle)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should show a new task without a date at the bottom when there are < 50 tasks', () => {
|
|
||||||
seedTasks(40)
|
|
||||||
const newTaskTitle = 'New Task'
|
|
||||||
TaskFactory.create(1, {
|
|
||||||
id: 999,
|
|
||||||
title: newTaskTitle,
|
|
||||||
}, false)
|
|
||||||
|
|
||||||
cy.visit('/')
|
|
||||||
cy.get('[data-cy="showTasks"] .card .task')
|
|
||||||
.last()
|
|
||||||
.should('contain.text', newTaskTitle)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should show a task without a due date added via default project at the bottom', () => {
|
|
||||||
const {project} = seedTasks(40)
|
|
||||||
updateUserSettings({
|
|
||||||
default_project_id: project.id,
|
|
||||||
overdue_tasks_reminders_time: '9:00',
|
|
||||||
})
|
|
||||||
|
|
||||||
const newTaskTitle = 'New Task'
|
|
||||||
cy.visit('/')
|
|
||||||
|
|
||||||
cy.get('.add-task-textarea')
|
|
||||||
.type(`${newTaskTitle}{enter}`)
|
|
||||||
|
|
||||||
cy.get('[data-cy="showTasks"] .card .task')
|
|
||||||
.last()
|
|
||||||
.should('contain.text', newTaskTitle)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should show the cta buttons for new project when there are no tasks', () => {
|
|
||||||
TaskFactory.truncate()
|
|
||||||
|
|
||||||
cy.visit('/')
|
|
||||||
|
|
||||||
cy.get('.home.app-content .content')
|
|
||||||
.should('contain.text', 'Import your projects and tasks from other services into Vikunja:')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should not show the cta buttons for new project when there are tasks', () => {
|
|
||||||
seedTasks()
|
|
||||||
|
|
||||||
cy.visit('/')
|
|
||||||
|
|
||||||
cy.get('.home.app-content .content')
|
|
||||||
.should('not.contain.text', 'You can create a new project for your new tasks:')
|
|
||||||
.should('not.contain.text', 'Or import your projects and tasks from other services into Vikunja:')
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,968 +0,0 @@
|
||||||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
|
||||||
|
|
||||||
import {TaskFactory} from '../../factories/task'
|
|
||||||
import {ProjectFactory} from '../../factories/project'
|
|
||||||
import {TaskCommentFactory} from '../../factories/task_comment'
|
|
||||||
import {UserFactory} from '../../factories/user'
|
|
||||||
import {UserProjectFactory} from '../../factories/users_project'
|
|
||||||
import {TaskAssigneeFactory} from '../../factories/task_assignee'
|
|
||||||
import {LabelFactory} from '../../factories/labels'
|
|
||||||
import {LabelTaskFactory} from '../../factories/label_task'
|
|
||||||
import {BucketFactory} from '../../factories/bucket'
|
|
||||||
|
|
||||||
import {TaskAttachmentFactory} from '../../factories/task_attachments'
|
|
||||||
import {TaskReminderFactory} from '../../factories/task_reminders'
|
|
||||||
|
|
||||||
function addLabelToTaskAndVerify(labelTitle: string) {
|
|
||||||
cy.get('.task-view .action-buttons .button')
|
|
||||||
.contains('Add Labels')
|
|
||||||
.click()
|
|
||||||
cy.get('.task-view .details.labels-list .multiselect input')
|
|
||||||
.type(labelTitle)
|
|
||||||
cy.get('.task-view .details.labels-list .multiselect .search-results')
|
|
||||||
.children()
|
|
||||||
.first()
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.global-notification', {timeout: 4000})
|
|
||||||
.should('contain', 'Success')
|
|
||||||
cy.get('.task-view .details.labels-list .multiselect .input-wrapper span.tag')
|
|
||||||
.should('exist')
|
|
||||||
.should('contain', labelTitle)
|
|
||||||
}
|
|
||||||
|
|
||||||
function uploadAttachmentAndVerify(taskId: number) {
|
|
||||||
cy.intercept(`${Cypress.env('API_URL')}/tasks/${taskId}/attachments`).as('uploadAttachment')
|
|
||||||
cy.get('.task-view .action-buttons .button')
|
|
||||||
.contains('Add Attachments')
|
|
||||||
.click()
|
|
||||||
cy.get('input[type=file]#files', {timeout: 1000})
|
|
||||||
.selectFile('cypress/fixtures/image.jpg', {force: true}) // The input is not visible, but on purpose
|
|
||||||
cy.wait('@uploadAttachment')
|
|
||||||
|
|
||||||
cy.get('.attachments .attachments .files a.attachment')
|
|
||||||
.should('exist')
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Task', () => {
|
|
||||||
createFakeUserAndLogin()
|
|
||||||
|
|
||||||
let projects
|
|
||||||
let buckets
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
// UserFactory.create(1)
|
|
||||||
projects = ProjectFactory.create(1)
|
|
||||||
buckets = BucketFactory.create(1, {
|
|
||||||
project_id: projects[0].id,
|
|
||||||
})
|
|
||||||
TaskFactory.truncate()
|
|
||||||
UserProjectFactory.truncate()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should be created new', () => {
|
|
||||||
cy.visit('/projects/1/list')
|
|
||||||
cy.get('.input[placeholder="Add a new task…"')
|
|
||||||
.type('New Task')
|
|
||||||
cy.get('.button')
|
|
||||||
.contains('Add')
|
|
||||||
.click()
|
|
||||||
cy.get('.tasks .task .tasktext')
|
|
||||||
.first()
|
|
||||||
.should('contain', 'New Task')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Inserts new tasks at the top of the project', () => {
|
|
||||||
TaskFactory.create(1)
|
|
||||||
|
|
||||||
cy.visit('/projects/1/list')
|
|
||||||
cy.get('.project-is-empty-notice')
|
|
||||||
.should('not.exist')
|
|
||||||
cy.get('.input[placeholder="Add a new task…"')
|
|
||||||
.type('New Task')
|
|
||||||
cy.get('.button')
|
|
||||||
.contains('Add')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.wait(1000) // Wait for the request
|
|
||||||
cy.get('.tasks .task .tasktext')
|
|
||||||
.first()
|
|
||||||
.should('contain', 'New Task')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Marks a task as done', () => {
|
|
||||||
TaskFactory.create(1)
|
|
||||||
|
|
||||||
cy.visit('/projects/1/list')
|
|
||||||
cy.get('.tasks .task .fancycheckbox')
|
|
||||||
.first()
|
|
||||||
.click()
|
|
||||||
cy.get('.global-notification')
|
|
||||||
.should('contain', 'Success')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Can add a task to favorites', () => {
|
|
||||||
TaskFactory.create(1)
|
|
||||||
|
|
||||||
cy.visit('/projects/1/list')
|
|
||||||
cy.get('.tasks .task .favorite')
|
|
||||||
.first()
|
|
||||||
.click()
|
|
||||||
cy.get('.menu-container')
|
|
||||||
.should('contain', 'Favorites')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should show a task description icon if the task has a description', () => {
|
|
||||||
cy.intercept(Cypress.env('API_URL') + '/projects/1/tasks**').as('loadTasks')
|
|
||||||
TaskFactory.create(1, {
|
|
||||||
description: 'Lorem Ipsum',
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.visit('/projects/1/list')
|
|
||||||
cy.wait('@loadTasks')
|
|
||||||
|
|
||||||
cy.get('.tasks .task .project-task-icon')
|
|
||||||
.should('exist')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should not show a task description icon if the task has an empty description', () => {
|
|
||||||
cy.intercept(Cypress.env('API_URL') + '/projects/1/tasks**').as('loadTasks')
|
|
||||||
TaskFactory.create(1, {
|
|
||||||
description: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.visit('/projects/1/list')
|
|
||||||
cy.wait('@loadTasks')
|
|
||||||
|
|
||||||
cy.get('.tasks .task .project-task-icon')
|
|
||||||
.should('not.exist')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should not show a task description icon if the task has a description containing only an empty p tag', () => {
|
|
||||||
cy.intercept(Cypress.env('API_URL') + '/projects/1/tasks**').as('loadTasks')
|
|
||||||
TaskFactory.create(1, {
|
|
||||||
description: '<p></p>',
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.visit('/projects/1/list')
|
|
||||||
cy.wait('@loadTasks')
|
|
||||||
|
|
||||||
cy.get('.tasks .task .project-task-icon')
|
|
||||||
.should('not.exist')
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Task Detail View', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
TaskCommentFactory.truncate()
|
|
||||||
LabelTaskFactory.truncate()
|
|
||||||
TaskAttachmentFactory.truncate()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Shows all task details', () => {
|
|
||||||
const tasks = TaskFactory.create(1, {
|
|
||||||
id: 1,
|
|
||||||
index: 1,
|
|
||||||
description: 'Lorem ipsum dolor sit amet.',
|
|
||||||
})
|
|
||||||
cy.visit(`/tasks/${tasks[0].id}`)
|
|
||||||
|
|
||||||
cy.get('.task-view h1.title.input')
|
|
||||||
.should('contain', tasks[0].title)
|
|
||||||
cy.get('.task-view h1.title.task-id')
|
|
||||||
.should('contain', '#1')
|
|
||||||
cy.get('.task-view h6.subtitle')
|
|
||||||
.should('contain', projects[0].title)
|
|
||||||
cy.get('.task-view .details.content.description')
|
|
||||||
.should('contain', tasks[0].description)
|
|
||||||
cy.get('.task-view .action-buttons p.created')
|
|
||||||
.should('contain', 'Created')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Shows a done label for done tasks', () => {
|
|
||||||
const tasks = TaskFactory.create(1, {
|
|
||||||
id: 1,
|
|
||||||
index: 1,
|
|
||||||
done: true,
|
|
||||||
done_at: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
cy.visit(`/tasks/${tasks[0].id}`)
|
|
||||||
|
|
||||||
cy.get('.task-view .heading .is-done')
|
|
||||||
.should('be.visible')
|
|
||||||
.should('contain', 'Done')
|
|
||||||
cy.get('.task-view .action-buttons p.created')
|
|
||||||
.scrollIntoView()
|
|
||||||
.should('be.visible')
|
|
||||||
.should('contain', 'Done')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Can mark a task as done', () => {
|
|
||||||
const tasks = TaskFactory.create(1, {
|
|
||||||
id: 1,
|
|
||||||
done: false,
|
|
||||||
})
|
|
||||||
cy.visit(`/tasks/${tasks[0].id}`)
|
|
||||||
|
|
||||||
cy.get('.task-view .action-buttons .button')
|
|
||||||
.contains('Mark task done!')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.task-view .heading .is-done')
|
|
||||||
.should('exist')
|
|
||||||
.should('contain', 'Done')
|
|
||||||
cy.get('.global-notification')
|
|
||||||
.should('contain', 'Success')
|
|
||||||
cy.get('.task-view .action-buttons .button')
|
|
||||||
.should('contain', 'Mark as undone')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Shows a task identifier since the project has one', () => {
|
|
||||||
const projects = ProjectFactory.create(1, {
|
|
||||||
id: 1,
|
|
||||||
identifier: 'TEST',
|
|
||||||
})
|
|
||||||
const tasks = TaskFactory.create(1, {
|
|
||||||
id: 1,
|
|
||||||
project_id: projects[0].id,
|
|
||||||
index: 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.visit(`/tasks/${tasks[0].id}`)
|
|
||||||
|
|
||||||
cy.get('.task-view h1.title.task-id')
|
|
||||||
.should('contain', `${projects[0].identifier}-${tasks[0].index}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Can edit the description', () => {
|
|
||||||
const tasks = TaskFactory.create(1, {
|
|
||||||
id: 1,
|
|
||||||
description: 'Lorem ipsum dolor sit amet.',
|
|
||||||
})
|
|
||||||
cy.visit(`/tasks/${tasks[0].id}`)
|
|
||||||
|
|
||||||
cy.get('.task-view .details.content.description .tiptap button.done-edit')
|
|
||||||
.click()
|
|
||||||
cy.get('.task-view .details.content.description .tiptap__editor .tiptap.ProseMirror')
|
|
||||||
.type('{selectall}New Description')
|
|
||||||
cy.get('[data-cy="saveEditor"]')
|
|
||||||
.contains('Save')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.task-view .details.content.description h3 span.is-small.has-text-success')
|
|
||||||
.contains('Saved!')
|
|
||||||
.should('exist')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Shows an empty editor when the description of a task is empty', () => {
|
|
||||||
const tasks = TaskFactory.create(1, {
|
|
||||||
id: 1,
|
|
||||||
description: '',
|
|
||||||
})
|
|
||||||
cy.visit(`/tasks/${tasks[0].id}`)
|
|
||||||
|
|
||||||
cy.get('.task-view .details.content.description .tiptap.ProseMirror p')
|
|
||||||
.should('have.attr', 'data-placeholder')
|
|
||||||
cy.get('.task-view .details.content.description .tiptap button.done-edit')
|
|
||||||
.should('not.exist')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Shows a preview editor when the description of a task is not empty', () => {
|
|
||||||
const tasks = TaskFactory.create(1, {
|
|
||||||
id: 1,
|
|
||||||
description: 'Lorem Ipsum dolor sit amet',
|
|
||||||
})
|
|
||||||
cy.visit(`/tasks/${tasks[0].id}`)
|
|
||||||
|
|
||||||
cy.get('.task-view .details.content.description .tiptap.ProseMirror p')
|
|
||||||
.should('not.have.attr', 'data-placeholder')
|
|
||||||
cy.get('.task-view .details.content.description .tiptap button.done-edit')
|
|
||||||
.should('exist')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Shows a preview editor when the description of a task contains html', () => {
|
|
||||||
const tasks = TaskFactory.create(1, {
|
|
||||||
id: 1,
|
|
||||||
description: '<p>Lorem Ipsum dolor sit amet</p>',
|
|
||||||
})
|
|
||||||
cy.visit(`/tasks/${tasks[0].id}`)
|
|
||||||
|
|
||||||
cy.get('.task-view .details.content.description .tiptap.ProseMirror p')
|
|
||||||
.should('not.have.attr', 'data-placeholder')
|
|
||||||
cy.get('.task-view .details.content.description .tiptap button.done-edit')
|
|
||||||
.should('exist')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Can add a new comment', () => {
|
|
||||||
const tasks = TaskFactory.create(1, {
|
|
||||||
id: 1,
|
|
||||||
})
|
|
||||||
cy.visit(`/tasks/${tasks[0].id}`)
|
|
||||||
|
|
||||||
cy.get('.task-view .comments .media.comment .tiptap__editor .tiptap.ProseMirror')
|
|
||||||
.should('be.visible')
|
|
||||||
.type('{selectall}New Comment')
|
|
||||||
cy.get('.task-view .comments .media.comment .button:not([disabled])')
|
|
||||||
.contains('Comment')
|
|
||||||
.should('be.visible')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.task-view .comments .media.comment .tiptap__editor')
|
|
||||||
.should('contain', 'New Comment')
|
|
||||||
cy.get('.global-notification')
|
|
||||||
.should('contain', 'Success')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Can move a task to another project', () => {
|
|
||||||
const projects = ProjectFactory.create(2)
|
|
||||||
BucketFactory.create(2, {
|
|
||||||
project_id: '{increment}',
|
|
||||||
})
|
|
||||||
const tasks = TaskFactory.create(1, {
|
|
||||||
id: 1,
|
|
||||||
project_id: projects[0].id,
|
|
||||||
})
|
|
||||||
cy.visit(`/tasks/${tasks[0].id}`)
|
|
||||||
|
|
||||||
cy.get('.task-view .action-buttons .button')
|
|
||||||
.contains('Move')
|
|
||||||
.click()
|
|
||||||
cy.get('.task-view .content.details .field .multiselect.control .input-wrapper input')
|
|
||||||
.type(`${projects[1].title}{enter}`)
|
|
||||||
// The requests happen with a 200ms timeout. Because of that, the results are not yet there when cypress
|
|
||||||
// presses enter and we can't simulate pressing on enter to select the item.
|
|
||||||
cy.get('.task-view .content.details .field .multiselect.control .search-results')
|
|
||||||
.children()
|
|
||||||
.first()
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.task-view h6.subtitle')
|
|
||||||
.should('contain', projects[1].title)
|
|
||||||
cy.get('.global-notification')
|
|
||||||
.should('contain', 'Success')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Can delete a task', () => {
|
|
||||||
const tasks = TaskFactory.create(1, {
|
|
||||||
id: 1,
|
|
||||||
project_id: 1,
|
|
||||||
})
|
|
||||||
cy.visit(`/tasks/${tasks[0].id}`)
|
|
||||||
|
|
||||||
cy.get('.task-view .action-buttons .button')
|
|
||||||
.should('be.visible')
|
|
||||||
.contains('Delete')
|
|
||||||
.click()
|
|
||||||
cy.get('.modal-mask .modal-container .modal-content .header')
|
|
||||||
.should('contain', 'Delete this task')
|
|
||||||
cy.get('.modal-mask .modal-container .modal-content .actions .button')
|
|
||||||
.contains('Do it!')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.global-notification')
|
|
||||||
.should('contain', 'Success')
|
|
||||||
cy.url()
|
|
||||||
.should('contain', `/projects/${tasks[0].project_id}/`)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Can add an assignee to a task', () => {
|
|
||||||
const users = UserFactory.create(5)
|
|
||||||
const tasks = TaskFactory.create(1, {
|
|
||||||
id: 1,
|
|
||||||
project_id: 1,
|
|
||||||
})
|
|
||||||
UserProjectFactory.create(5, {
|
|
||||||
project_id: 1,
|
|
||||||
user_id: '{increment}',
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.visit(`/tasks/${tasks[0].id}`)
|
|
||||||
|
|
||||||
cy.get('[data-cy="taskDetail.assign"]')
|
|
||||||
.click()
|
|
||||||
cy.get('.task-view .column.assignees .multiselect input')
|
|
||||||
.type(users[1].username)
|
|
||||||
cy.get('.task-view .column.assignees .multiselect .search-results')
|
|
||||||
.children()
|
|
||||||
.first()
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.global-notification')
|
|
||||||
.should('contain', 'Success')
|
|
||||||
cy.get('.task-view .column.assignees .multiselect .input-wrapper span.assignee')
|
|
||||||
.should('exist')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Can remove an assignee from a task', () => {
|
|
||||||
const users = UserFactory.create(2)
|
|
||||||
const tasks = TaskFactory.create(1, {
|
|
||||||
id: 1,
|
|
||||||
project_id: 1,
|
|
||||||
})
|
|
||||||
UserProjectFactory.create(5, {
|
|
||||||
project_id: 1,
|
|
||||||
user_id: '{increment}',
|
|
||||||
})
|
|
||||||
TaskAssigneeFactory.create(1, {
|
|
||||||
task_id: tasks[0].id,
|
|
||||||
user_id: users[1].id,
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.visit(`/tasks/${tasks[0].id}`)
|
|
||||||
|
|
||||||
cy.get('.task-view .column.assignees .multiselect .input-wrapper span.assignee')
|
|
||||||
.get('.remove-assignee')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.global-notification')
|
|
||||||
.should('contain', 'Success')
|
|
||||||
cy.get('.task-view .column.assignees .multiselect .input-wrapper span.assignee')
|
|
||||||
.should('not.exist')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Can add a new label to a task', () => {
|
|
||||||
const tasks = TaskFactory.create(1, {
|
|
||||||
id: 1,
|
|
||||||
project_id: 1,
|
|
||||||
})
|
|
||||||
LabelFactory.truncate()
|
|
||||||
const newLabelText = 'some new label'
|
|
||||||
|
|
||||||
cy.visit(`/tasks/${tasks[0].id}`)
|
|
||||||
|
|
||||||
cy.get('.task-view .action-buttons .button')
|
|
||||||
.contains('Add Labels')
|
|
||||||
.should('be.visible')
|
|
||||||
.click()
|
|
||||||
cy.get('.task-view .details.labels-list .multiselect input')
|
|
||||||
.type(newLabelText)
|
|
||||||
cy.get('.task-view .details.labels-list .multiselect .search-results')
|
|
||||||
.children()
|
|
||||||
.first()
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.global-notification')
|
|
||||||
.should('contain', 'Success')
|
|
||||||
cy.get('.task-view .details.labels-list .multiselect .input-wrapper span.tag')
|
|
||||||
.should('exist')
|
|
||||||
.should('contain', newLabelText)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Can add an existing label to a task', () => {
|
|
||||||
const tasks = TaskFactory.create(1, {
|
|
||||||
id: 1,
|
|
||||||
project_id: 1,
|
|
||||||
})
|
|
||||||
const labels = LabelFactory.create(1)
|
|
||||||
LabelTaskFactory.truncate()
|
|
||||||
|
|
||||||
cy.visit(`/tasks/${tasks[0].id}`)
|
|
||||||
|
|
||||||
addLabelToTaskAndVerify(labels[0].title)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Can add a label to a task and it shows up on the kanban board afterwards', () => {
|
|
||||||
const tasks = TaskFactory.create(1, {
|
|
||||||
id: 1,
|
|
||||||
project_id: projects[0].id,
|
|
||||||
bucket_id: buckets[0].id,
|
|
||||||
})
|
|
||||||
const labels = LabelFactory.create(1)
|
|
||||||
LabelTaskFactory.truncate()
|
|
||||||
|
|
||||||
cy.visit(`/projects/${projects[0].id}/kanban`)
|
|
||||||
|
|
||||||
cy.get('.bucket .task')
|
|
||||||
.contains(tasks[0].title)
|
|
||||||
.click()
|
|
||||||
|
|
||||||
addLabelToTaskAndVerify(labels[0].title)
|
|
||||||
|
|
||||||
cy.get('.modal-content .close')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.bucket .task')
|
|
||||||
.should('contain.text', labels[0].title)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Can remove a label from a task', () => {
|
|
||||||
const tasks = TaskFactory.create(1, {
|
|
||||||
id: 1,
|
|
||||||
project_id: 1,
|
|
||||||
})
|
|
||||||
const labels = LabelFactory.create(1)
|
|
||||||
LabelTaskFactory.create(1, {
|
|
||||||
task_id: tasks[0].id,
|
|
||||||
label_id: labels[0].id,
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.visit(`/tasks/${tasks[0].id}`)
|
|
||||||
|
|
||||||
cy.get('.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')
|
|
||||||
.children()
|
|
||||||
.first()
|
|
||||||
.get('[data-cy="taskDetail.removeLabel"]')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.global-notification')
|
|
||||||
.should('contain', 'Success')
|
|
||||||
cy.get('.task-view .details.labels-list .multiselect .input-wrapper')
|
|
||||||
.should('not.contain', labels[0].title)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Can set a due date for a task', () => {
|
|
||||||
const tasks = TaskFactory.create(1, {
|
|
||||||
id: 1,
|
|
||||||
done: false,
|
|
||||||
})
|
|
||||||
cy.visit(`/tasks/${tasks[0].id}`)
|
|
||||||
|
|
||||||
cy.get('.task-view .action-buttons .button')
|
|
||||||
.contains('Set Due Date')
|
|
||||||
.click()
|
|
||||||
cy.get('.task-view .columns.details .column')
|
|
||||||
.contains('Due Date')
|
|
||||||
.get('.date-input .datepicker .show')
|
|
||||||
.click()
|
|
||||||
cy.get('.datepicker .datepicker-popup button')
|
|
||||||
.contains('Tomorrow')
|
|
||||||
.click()
|
|
||||||
cy.get('[data-cy="closeDatepicker"]')
|
|
||||||
.contains('Confirm')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.task-view .columns.details .column')
|
|
||||||
.contains('Due Date')
|
|
||||||
.get('.date-input .datepicker-popup')
|
|
||||||
.should('not.exist')
|
|
||||||
cy.get('.global-notification')
|
|
||||||
.should('contain', 'Success')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Can set a due date to a specific date for a task', () => {
|
|
||||||
const tasks = TaskFactory.create(1, {
|
|
||||||
id: 1,
|
|
||||||
done: false,
|
|
||||||
})
|
|
||||||
cy.visit(`/tasks/${tasks[0].id}`)
|
|
||||||
|
|
||||||
cy.get('.task-view .action-buttons .button')
|
|
||||||
.contains('Set Due Date')
|
|
||||||
.click()
|
|
||||||
cy.get('.task-view .columns.details .column')
|
|
||||||
.contains('Due Date')
|
|
||||||
.get('.date-input .datepicker .show')
|
|
||||||
.click()
|
|
||||||
cy.get('.datepicker-popup .flatpickr-innerContainer .flatpickr-days .flatpickr-day.today')
|
|
||||||
.click()
|
|
||||||
cy.get('[data-cy="closeDatepicker"]')
|
|
||||||
.contains('Confirm')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
const today = new Date()
|
|
||||||
const day = today.toLocaleString('default', {day: 'numeric'})
|
|
||||||
const month = today.toLocaleString('default', {month: 'short'})
|
|
||||||
const year = today.toLocaleString('default', {year: 'numeric'})
|
|
||||||
const date = `${day} ${month} ${year}, 12:00:00`
|
|
||||||
cy.get('.task-view .columns.details .column')
|
|
||||||
.contains('Due Date')
|
|
||||||
.get('.date-input .datepicker-popup')
|
|
||||||
.should('not.exist')
|
|
||||||
cy.get('.task-view .columns.details .column')
|
|
||||||
.contains('Due Date')
|
|
||||||
.get('.date-input')
|
|
||||||
.should('contain.text', date)
|
|
||||||
cy.get('.global-notification')
|
|
||||||
.should('contain', 'Success')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Can change a due date to a specific date for a task', () => {
|
|
||||||
const dueDate = new Date()
|
|
||||||
dueDate.setHours(12)
|
|
||||||
dueDate.setMinutes(0)
|
|
||||||
dueDate.setSeconds(0)
|
|
||||||
dueDate.setDate(1)
|
|
||||||
const tasks = TaskFactory.create(1, {
|
|
||||||
id: 1,
|
|
||||||
done: false,
|
|
||||||
due_date: dueDate.toISOString(),
|
|
||||||
})
|
|
||||||
cy.visit(`/tasks/${tasks[0].id}`)
|
|
||||||
|
|
||||||
cy.get('.task-view .action-buttons .button')
|
|
||||||
.contains('Set Due Date')
|
|
||||||
.click()
|
|
||||||
cy.get('.task-view .columns.details .column')
|
|
||||||
.contains('Due Date')
|
|
||||||
.get('.date-input .datepicker .show')
|
|
||||||
.click()
|
|
||||||
cy.get('.datepicker-popup .flatpickr-innerContainer .flatpickr-days .flatpickr-day.today')
|
|
||||||
.click()
|
|
||||||
cy.get('[data-cy="closeDatepicker"]')
|
|
||||||
.contains('Confirm')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
const today = new Date()
|
|
||||||
const day = today.toLocaleString('default', {day: 'numeric'})
|
|
||||||
const month = today.toLocaleString('default', {month: 'short'})
|
|
||||||
const year = today.toLocaleString('default', {year: 'numeric'})
|
|
||||||
const date = `${day} ${month} ${year}, 12:00:00`
|
|
||||||
cy.get('.task-view .columns.details .column')
|
|
||||||
.contains('Due Date')
|
|
||||||
.get('.date-input .datepicker-popup')
|
|
||||||
.should('not.exist')
|
|
||||||
cy.get('.task-view .columns.details .column')
|
|
||||||
.contains('Due Date')
|
|
||||||
.get('.date-input')
|
|
||||||
.should('contain.text', date)
|
|
||||||
cy.get('.global-notification')
|
|
||||||
.should('contain', 'Success')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Can set a reminder', () => {
|
|
||||||
TaskReminderFactory.truncate()
|
|
||||||
const tasks = TaskFactory.create(1, {
|
|
||||||
id: 1,
|
|
||||||
done: false,
|
|
||||||
})
|
|
||||||
cy.visit(`/tasks/${tasks[0].id}`)
|
|
||||||
|
|
||||||
cy.get('.task-view .action-buttons .button')
|
|
||||||
.contains('Set Reminders')
|
|
||||||
.click()
|
|
||||||
cy.get('.task-view .columns.details .column button')
|
|
||||||
.contains('Add a new reminder')
|
|
||||||
.click()
|
|
||||||
cy.get('.datepicker__quick-select-date')
|
|
||||||
.contains('Tomorrow')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.reminder-options-popup')
|
|
||||||
.should('not.be.visible')
|
|
||||||
cy.get('.global-notification')
|
|
||||||
.should('contain', 'Success')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Allows to set a relative reminder when the task already has a due date', () => {
|
|
||||||
TaskReminderFactory.truncate()
|
|
||||||
const tasks = TaskFactory.create(1, {
|
|
||||||
id: 1,
|
|
||||||
done: false,
|
|
||||||
due_date: (new Date()).toISOString(),
|
|
||||||
})
|
|
||||||
cy.visit(`/tasks/${tasks[0].id}`)
|
|
||||||
|
|
||||||
cy.get('.task-view .action-buttons .button')
|
|
||||||
.contains('Set Reminders')
|
|
||||||
.click()
|
|
||||||
cy.get('.task-view .columns.details .column button')
|
|
||||||
.contains('Add a new reminder')
|
|
||||||
.click()
|
|
||||||
cy.get('.datepicker__quick-select-date')
|
|
||||||
.should('not.exist')
|
|
||||||
cy.get('.reminder-options-popup .card-content')
|
|
||||||
.should('contain', '1 day before Due Date')
|
|
||||||
cy.get('.reminder-options-popup .card-content')
|
|
||||||
.contains('1 day before Due Date')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.reminder-options-popup')
|
|
||||||
.should('not.be.visible')
|
|
||||||
cy.get('.global-notification')
|
|
||||||
.should('contain', 'Success')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Allows to set a relative reminder when the task already has a start date', () => {
|
|
||||||
TaskReminderFactory.truncate()
|
|
||||||
const tasks = TaskFactory.create(1, {
|
|
||||||
id: 1,
|
|
||||||
done: false,
|
|
||||||
start_date: (new Date()).toISOString(),
|
|
||||||
})
|
|
||||||
cy.visit(`/tasks/${tasks[0].id}`)
|
|
||||||
|
|
||||||
cy.get('.task-view .action-buttons .button')
|
|
||||||
.contains('Set Reminders')
|
|
||||||
.click()
|
|
||||||
cy.get('.task-view .columns.details .column button')
|
|
||||||
.contains('Add a new reminder')
|
|
||||||
.click()
|
|
||||||
cy.get('.datepicker__quick-select-date')
|
|
||||||
.should('not.exist')
|
|
||||||
cy.get('.reminder-options-popup .card-content')
|
|
||||||
.should('contain', '1 day before Start Date')
|
|
||||||
cy.get('.reminder-options-popup .card-content')
|
|
||||||
.contains('1 day before Start Date')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.reminder-options-popup')
|
|
||||||
.should('not.be.visible')
|
|
||||||
cy.get('.global-notification')
|
|
||||||
.should('contain', 'Success')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Allows to set a custom relative reminder when the task already has a due date', () => {
|
|
||||||
TaskReminderFactory.truncate()
|
|
||||||
const tasks = TaskFactory.create(1, {
|
|
||||||
id: 1,
|
|
||||||
done: false,
|
|
||||||
due_date: (new Date()).toISOString(),
|
|
||||||
})
|
|
||||||
cy.visit(`/tasks/${tasks[0].id}`)
|
|
||||||
|
|
||||||
cy.get('.task-view .action-buttons .button')
|
|
||||||
.contains('Set Reminders')
|
|
||||||
.click()
|
|
||||||
cy.get('.task-view .columns.details .column button')
|
|
||||||
.contains('Add a new reminder')
|
|
||||||
.click()
|
|
||||||
cy.get('.datepicker__quick-select-date')
|
|
||||||
.should('not.exist')
|
|
||||||
cy.get('.reminder-options-popup .card-content')
|
|
||||||
.contains('Custom')
|
|
||||||
.click()
|
|
||||||
cy.get('.reminder-options-popup .card-content .reminder-period input')
|
|
||||||
.first()
|
|
||||||
.type('{selectall}10')
|
|
||||||
cy.get('.reminder-options-popup .card-content .reminder-period select')
|
|
||||||
.first()
|
|
||||||
.select('days')
|
|
||||||
cy.get('.reminder-options-popup .card-content button')
|
|
||||||
.contains('Confirm')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.reminder-options-popup')
|
|
||||||
.should('not.be.visible')
|
|
||||||
cy.get('.global-notification')
|
|
||||||
.should('contain', 'Success')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Allows to set a fixed reminder when the task already has a due date', () => {
|
|
||||||
TaskReminderFactory.truncate()
|
|
||||||
const tasks = TaskFactory.create(1, {
|
|
||||||
id: 1,
|
|
||||||
done: false,
|
|
||||||
due_date: (new Date()).toISOString(),
|
|
||||||
})
|
|
||||||
cy.visit(`/tasks/${tasks[0].id}`)
|
|
||||||
|
|
||||||
cy.get('.task-view .action-buttons .button')
|
|
||||||
.contains('Set Reminders')
|
|
||||||
.click()
|
|
||||||
cy.get('.task-view .columns.details .column button')
|
|
||||||
.contains('Add a new reminder')
|
|
||||||
.click()
|
|
||||||
cy.get('.datepicker__quick-select-date')
|
|
||||||
.should('not.exist')
|
|
||||||
cy.get('.reminder-options-popup .card-content')
|
|
||||||
.contains('Date and time')
|
|
||||||
.click()
|
|
||||||
cy.get('.datepicker__quick-select-date')
|
|
||||||
.contains('Tomorrow')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.reminder-options-popup')
|
|
||||||
.should('not.be.visible')
|
|
||||||
cy.get('.global-notification')
|
|
||||||
.should('contain', 'Success')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Can set a priority for a task', () => {
|
|
||||||
const tasks = TaskFactory.create(1, {
|
|
||||||
id: 1,
|
|
||||||
})
|
|
||||||
cy.visit(`/tasks/${tasks[0].id}`)
|
|
||||||
|
|
||||||
cy.get('.task-view .action-buttons .button')
|
|
||||||
.contains('Set Priority')
|
|
||||||
.click()
|
|
||||||
cy.get('.task-view .columns.details .column')
|
|
||||||
.contains('Priority')
|
|
||||||
.get('.select select')
|
|
||||||
.select('Urgent')
|
|
||||||
cy.get('.global-notification')
|
|
||||||
.should('contain', 'Success')
|
|
||||||
|
|
||||||
cy.get('.task-view .columns.details .column')
|
|
||||||
.contains('Priority')
|
|
||||||
.get('.select select')
|
|
||||||
.should('have.value', '4')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Can set the progress for a task', () => {
|
|
||||||
const tasks = TaskFactory.create(1, {
|
|
||||||
id: 1,
|
|
||||||
})
|
|
||||||
cy.visit(`/tasks/${tasks[0].id}`)
|
|
||||||
|
|
||||||
cy.get('.task-view .action-buttons .button')
|
|
||||||
.contains('Set Progress')
|
|
||||||
.click()
|
|
||||||
cy.get('.task-view .columns.details .column')
|
|
||||||
.contains('Progress')
|
|
||||||
.get('.select select')
|
|
||||||
.select('50%')
|
|
||||||
cy.get('.global-notification')
|
|
||||||
.should('contain', 'Success')
|
|
||||||
|
|
||||||
cy.wait(200)
|
|
||||||
|
|
||||||
cy.get('.task-view .columns.details .column')
|
|
||||||
.contains('Progress')
|
|
||||||
.get('.select select')
|
|
||||||
.should('be.visible')
|
|
||||||
.should('have.value', '0.5')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Can add an attachment to a task', () => {
|
|
||||||
TaskAttachmentFactory.truncate()
|
|
||||||
const tasks = TaskFactory.create(1, {
|
|
||||||
id: 1,
|
|
||||||
})
|
|
||||||
cy.visit(`/tasks/${tasks[0].id}`)
|
|
||||||
|
|
||||||
uploadAttachmentAndVerify(tasks[0].id)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Can add an attachment to a task and see it appearing on kanban', () => {
|
|
||||||
TaskAttachmentFactory.truncate()
|
|
||||||
const tasks = TaskFactory.create(1, {
|
|
||||||
id: 1,
|
|
||||||
project_id: projects[0].id,
|
|
||||||
bucket_id: buckets[0].id,
|
|
||||||
})
|
|
||||||
const labels = LabelFactory.create(1)
|
|
||||||
LabelTaskFactory.truncate()
|
|
||||||
|
|
||||||
cy.visit(`/projects/${projects[0].id}/kanban`)
|
|
||||||
|
|
||||||
cy.get('.bucket .task')
|
|
||||||
.contains(tasks[0].title)
|
|
||||||
.click()
|
|
||||||
|
|
||||||
uploadAttachmentAndVerify(tasks[0].id)
|
|
||||||
|
|
||||||
cy.get('.modal-content .close')
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.bucket .task .footer .icon svg.fa-paperclip')
|
|
||||||
.should('exist')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Can check items off a checklist', () => {
|
|
||||||
const tasks = TaskFactory.create(1, {
|
|
||||||
id: 1,
|
|
||||||
description: `
|
|
||||||
<ul data-type="taskList">
|
|
||||||
<li data-checked="false" data-type="taskItem"><label><input type="checkbox"><span></span></label>
|
|
||||||
<div><p>First Item</p></div>
|
|
||||||
</li>
|
|
||||||
<li data-checked="false" data-type="taskItem"><label><input type="checkbox"><span></span></label>
|
|
||||||
<div><p>Second Item</p></div>
|
|
||||||
</li>
|
|
||||||
<li data-checked="false" data-type="taskItem"><label><input type="checkbox"><span></span></label>
|
|
||||||
<div><p>Third Item</p></div>
|
|
||||||
</li>
|
|
||||||
<li data-checked="false" data-type="taskItem"><label><input type="checkbox"><span></span></label>
|
|
||||||
<div><p>Fourth Item</p></div>
|
|
||||||
</li>
|
|
||||||
<li data-checked="true" data-type="taskItem"><label><input type="checkbox"><span></span></label>
|
|
||||||
<div><p>Fifth Item</p></div>
|
|
||||||
</li>
|
|
||||||
</ul>`,
|
|
||||||
})
|
|
||||||
cy.visit(`/tasks/${tasks[0].id}`)
|
|
||||||
|
|
||||||
cy.get('.task-view .checklist-summary')
|
|
||||||
.should('contain.text', '1 of 5 tasks')
|
|
||||||
cy.get('.tiptap__editor ul > li input[type=checkbox]')
|
|
||||||
.eq(2)
|
|
||||||
.click()
|
|
||||||
|
|
||||||
cy.get('.task-view .details.content.description h3 span.is-small.has-text-success')
|
|
||||||
.contains('Saved!')
|
|
||||||
.should('exist')
|
|
||||||
cy.get('.tiptap__editor ul > li input[type=checkbox]')
|
|
||||||
.eq(2)
|
|
||||||
.should('be.checked')
|
|
||||||
cy.get('.tiptap__editor input[type=checkbox]')
|
|
||||||
.should('have.length', 5)
|
|
||||||
cy.get('.task-view .checklist-summary')
|
|
||||||
.should('contain.text', '2 of 5 tasks')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should use the editor to render description', () => {
|
|
||||||
const tasks = TaskFactory.create(1, {
|
|
||||||
id: 1,
|
|
||||||
description: `
|
|
||||||
<h1>Lorem Ipsum</h1>
|
|
||||||
<p>Dolor sit amet</p>
|
|
||||||
<ul data-type="taskList">
|
|
||||||
<li data-checked="false" data-type="taskItem"><label><input type="checkbox"><span></span></label>
|
|
||||||
<div><p>First Item</p></div>
|
|
||||||
</li>
|
|
||||||
<li data-checked="false" data-type="taskItem"><label><input type="checkbox"><span></span></label>
|
|
||||||
<div><p>Second Item</p></div>
|
|
||||||
</li>
|
|
||||||
</ul>`,
|
|
||||||
})
|
|
||||||
cy.visit(`/tasks/${tasks[0].id}`)
|
|
||||||
|
|
||||||
cy.get('.tiptap__editor ul > li input[type=checkbox]')
|
|
||||||
.should('exist')
|
|
||||||
cy.get('.tiptap__editor h1')
|
|
||||||
.contains('Lorem Ipsum')
|
|
||||||
.should('exist')
|
|
||||||
cy.get('.tiptap__editor p')
|
|
||||||
.contains('Dolor sit amet')
|
|
||||||
.should('exist')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should render an image from attachment', async () => {
|
|
||||||
|
|
||||||
TaskAttachmentFactory.truncate()
|
|
||||||
|
|
||||||
const tasks = TaskFactory.create(1, {
|
|
||||||
id: 1,
|
|
||||||
description: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.readFile('cypress/fixtures/image.jpg', null).then(file => {
|
|
||||||
|
|
||||||
const formData = new FormData()
|
|
||||||
formData.append('files', new Blob([file]), 'image.jpg')
|
|
||||||
|
|
||||||
cy.request({
|
|
||||||
method: 'PUT',
|
|
||||||
url: `${Cypress.env('API_URL')}/tasks/${tasks[0].id}/attachments`,
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${window.localStorage.getItem('token')}`,
|
|
||||||
'Content-Type': 'multipart/form-data',
|
|
||||||
},
|
|
||||||
body: formData,
|
|
||||||
})
|
|
||||||
.then(({body}) => {
|
|
||||||
const dec = new TextDecoder('utf-8')
|
|
||||||
const {success} = JSON.parse(dec.decode(body))
|
|
||||||
|
|
||||||
TaskFactory.create(1, {
|
|
||||||
id: 1,
|
|
||||||
description: `<img src="${Cypress.env('API_URL')}/tasks/${tasks[0].id}/attachments/${success[0].id}" alt="test image">`,
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.visit(`/tasks/${tasks[0].id}`)
|
|
||||||
|
|
||||||
cy.get('.tiptap__editor img')
|
|
||||||
.should('be.visible')
|
|
||||||
.and(($img) => {
|
|
||||||
// "naturalWidth" and "naturalHeight" are set when the image loads
|
|
||||||
expect($img[0].naturalWidth).to.be.greaterThan(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,12 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
|
||||||
"include": ["./**/*", "../support/**/*", "../factories/**/*"],
|
|
||||||
"compilerOptions": {
|
|
||||||
"baseUrl": ".",
|
|
||||||
"isolatedModules": false,
|
|
||||||
"target": "ES2015",
|
|
||||||
"lib": ["ESNext", "dom"],
|
|
||||||
"types": ["cypress"],
|
|
||||||
"ignoreDeprecations": "5.0"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,74 +0,0 @@
|
||||||
import {UserFactory} from '../../factories/user'
|
|
||||||
import {ProjectFactory} from '../../factories/project'
|
|
||||||
|
|
||||||
const testAndAssertFailed = fixture => {
|
|
||||||
cy.intercept(Cypress.env('API_URL') + '/login*').as('login')
|
|
||||||
|
|
||||||
cy.visit('/login')
|
|
||||||
cy.get('input[id=username]').type(fixture.username)
|
|
||||||
cy.get('input[id=password]').type(fixture.password)
|
|
||||||
cy.get('.button').contains('Login').click()
|
|
||||||
|
|
||||||
cy.wait('@login')
|
|
||||||
cy.url().should('include', '/')
|
|
||||||
cy.get('div.message.danger').contains('Wrong username or password.')
|
|
||||||
}
|
|
||||||
|
|
||||||
const credentials = {
|
|
||||||
username: 'test',
|
|
||||||
password: '1234',
|
|
||||||
}
|
|
||||||
|
|
||||||
function login() {
|
|
||||||
cy.get('input[id=username]').type(credentials.username)
|
|
||||||
cy.get('input[id=password]').type(credentials.password)
|
|
||||||
cy.get('.button').contains('Login').click()
|
|
||||||
cy.url().should('include', '/')
|
|
||||||
}
|
|
||||||
|
|
||||||
context('Login', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
UserFactory.create(1, {username: credentials.username})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should log in with the right credentials', () => {
|
|
||||||
cy.visit('/login')
|
|
||||||
login()
|
|
||||||
cy.clock(1625656161057) // 13:00
|
|
||||||
cy.get('h2').should('contain', `Hi ${credentials.username}!`)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should fail with a bad password', () => {
|
|
||||||
const fixture = {
|
|
||||||
username: 'test',
|
|
||||||
password: '123456',
|
|
||||||
}
|
|
||||||
|
|
||||||
testAndAssertFailed(fixture)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should fail with a bad username', () => {
|
|
||||||
const fixture = {
|
|
||||||
username: 'loremipsum',
|
|
||||||
password: '1234',
|
|
||||||
}
|
|
||||||
|
|
||||||
testAndAssertFailed(fixture)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should redirect to /login when no user is logged in', () => {
|
|
||||||
cy.visit('/')
|
|
||||||
cy.url().should('include', '/login')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should redirect to the previous route after logging in', () => {
|
|
||||||
const projects = ProjectFactory.create(1)
|
|
||||||
cy.visit(`/projects/${projects[0].id}/list`)
|
|
||||||
|
|
||||||
cy.url().should('include', '/login')
|
|
||||||
|
|
||||||
login()
|
|
||||||
|
|
||||||
cy.url().should('include', `/projects/${projects[0].id}/list`)
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,46 +0,0 @@
|
||||||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
|
||||||
import {createProjects} from '../project/prepareProjects'
|
|
||||||
|
|
||||||
function logout() {
|
|
||||||
cy.get('.navbar .username-dropdown-trigger')
|
|
||||||
.click()
|
|
||||||
cy.get('.navbar .dropdown-item')
|
|
||||||
.contains('Logout')
|
|
||||||
.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Log out', () => {
|
|
||||||
createFakeUserAndLogin()
|
|
||||||
|
|
||||||
it('Logs the user out', () => {
|
|
||||||
cy.visit('/')
|
|
||||||
|
|
||||||
expect(localStorage.getItem('token')).to.not.eq(null)
|
|
||||||
|
|
||||||
logout()
|
|
||||||
|
|
||||||
cy.url()
|
|
||||||
.should('contain', '/login')
|
|
||||||
.then(() => {
|
|
||||||
expect(localStorage.getItem('token')).to.eq(null)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it.skip('Should clear the project history after logging the user out', () => {
|
|
||||||
const projects = createProjects()
|
|
||||||
cy.visit(`/projects/${projects[0].id}`)
|
|
||||||
.then(() => {
|
|
||||||
expect(localStorage.getItem('projectHistory')).to.not.eq(null)
|
|
||||||
})
|
|
||||||
|
|
||||||
logout()
|
|
||||||
|
|
||||||
cy.wait(1000) // This makes re-loading of the project and associated entities (and the resulting error) visible
|
|
||||||
|
|
||||||
cy.url()
|
|
||||||
.should('contain', '/login')
|
|
||||||
.then(() => {
|
|
||||||
expect(localStorage.getItem('projectHistory')).to.eq(null)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,5 +1,6 @@
|
||||||
import {faker} from '@faker-js/faker'
|
import faker from 'faker'
|
||||||
import {Factory} from '../support/factory'
|
import {Factory} from '../support/factory'
|
||||||
|
import {formatISO} from 'date-fns'
|
||||||
|
|
||||||
export class BucketFactory extends Factory {
|
export class BucketFactory extends Factory {
|
||||||
static table = 'buckets'
|
static table = 'buckets'
|
||||||
|
@ -10,10 +11,10 @@ export class BucketFactory extends Factory {
|
||||||
return {
|
return {
|
||||||
id: '{increment}',
|
id: '{increment}',
|
||||||
title: faker.lorem.words(3),
|
title: faker.lorem.words(3),
|
||||||
project_id: 1,
|
list_id: 1,
|
||||||
created_by_id: 1,
|
created_by_id: 1,
|
||||||
created: now.toISOString(),
|
created: formatISO(now),
|
||||||
updated: now.toISOString(),
|
updated: formatISO(now)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
import {Factory} from '../support/factory'
|
import {Factory} from '../support/factory'
|
||||||
|
import {formatISO} from 'date-fns'
|
||||||
|
|
||||||
export class LabelTaskFactory extends Factory {
|
export class LabelTaskFactory extends Factory {
|
||||||
static table = 'label_tasks'
|
static table = 'label_tasks'
|
||||||
|
@ -10,7 +11,7 @@ export class LabelTaskFactory extends Factory {
|
||||||
id: '{increment}',
|
id: '{increment}',
|
||||||
task_id: 1,
|
task_id: 1,
|
||||||
label_id: 1,
|
label_id: 1,
|
||||||
created: now.toISOString(),
|
created: formatISO(now),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import {faker} from '@faker-js/faker'
|
import faker from 'faker'
|
||||||
|
|
||||||
import {Factory} from '../support/factory'
|
import {Factory} from '../support/factory'
|
||||||
|
import {formatISO} from 'date-fns'
|
||||||
|
|
||||||
export class LabelFactory extends Factory {
|
export class LabelFactory extends Factory {
|
||||||
static table = 'labels'
|
static table = 'labels'
|
||||||
|
@ -14,8 +15,8 @@ export class LabelFactory extends Factory {
|
||||||
description: faker.lorem.text(10),
|
description: faker.lorem.text(10),
|
||||||
hex_color: (Math.random()*0xFFFFFF<<0).toString(16), // random 6-digit hex number
|
hex_color: (Math.random()*0xFFFFFF<<0).toString(16), // random 6-digit hex number
|
||||||
created_by_id: 1,
|
created_by_id: 1,
|
||||||
created: now.toISOString(),
|
created: formatISO(now),
|
||||||
updated: now.toISOString(),
|
updated: formatISO(now),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
import {Factory} from '../support/factory'
|
import {Factory} from '../support/factory'
|
||||||
import {faker} from '@faker-js/faker'
|
import {formatISO} from "date-fns"
|
||||||
|
import faker from 'faker'
|
||||||
|
|
||||||
export class LinkShareFactory extends Factory {
|
export class LinkShareFactory extends Factory {
|
||||||
static table = 'link_shares'
|
static table = 'link_shares'
|
||||||
|
@ -10,12 +11,12 @@ export class LinkShareFactory extends Factory {
|
||||||
return {
|
return {
|
||||||
id: '{increment}',
|
id: '{increment}',
|
||||||
hash: faker.random.word(32),
|
hash: faker.random.word(32),
|
||||||
project_id: 1,
|
list_id: 1,
|
||||||
right: 0,
|
right: 0,
|
||||||
sharing_type: 0,
|
sharing_type: 0,
|
||||||
shared_by_id: 1,
|
shared_by_id: 1,
|
||||||
created: now.toISOString(),
|
created: formatISO(now),
|
||||||
updated: now.toISOString(),
|
updated: formatISO(now)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
import {Factory} from '../support/factory'
|
||||||
|
import {formatISO} from "date-fns"
|
||||||
|
import faker from 'faker'
|
||||||
|
|
||||||
|
export class ListFactory extends Factory {
|
||||||
|
static table = 'lists'
|
||||||
|
|
||||||
|
static factory() {
|
||||||
|
const now = new Date()
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: '{increment}',
|
||||||
|
title: faker.lorem.words(3),
|
||||||
|
owner_id: 1,
|
||||||
|
namespace_id: 1,
|
||||||
|
created: formatISO(now),
|
||||||
|
updated: formatISO(now)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
import faker from 'faker'
|
||||||
|
import {Factory} from '../support/factory'
|
||||||
|
import {formatISO} from 'date-fns'
|
||||||
|
|
||||||
|
export class NamespaceFactory extends Factory {
|
||||||
|
static table = 'namespaces'
|
||||||
|
|
||||||
|
static factory() {
|
||||||
|
const now = new Date()
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: '{increment}',
|
||||||
|
title: faker.lorem.words(3),
|
||||||
|
owner_id: 1,
|
||||||
|
created: formatISO(now),
|
||||||
|
updated: formatISO(now)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,18 +0,0 @@
|
||||||
import {Factory} from '../support/factory'
|
|
||||||
import {faker} from '@faker-js/faker'
|
|
||||||
|
|
||||||
export class ProjectFactory extends Factory {
|
|
||||||
static table = 'projects'
|
|
||||||
|
|
||||||
static factory() {
|
|
||||||
const now = new Date()
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: '{increment}',
|
|
||||||
title: faker.lorem.words(3),
|
|
||||||
owner_id: 1,
|
|
||||||
created: now.toISOString(),
|
|
||||||
updated: now.toISOString(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +1,6 @@
|
||||||
import {faker} from '@faker-js/faker'
|
import faker from 'faker'
|
||||||
import {Factory} from '../support/factory'
|
import {Factory} from '../support/factory'
|
||||||
|
import {formatISO} from 'date-fns'
|
||||||
|
|
||||||
export class TaskFactory extends Factory {
|
export class TaskFactory extends Factory {
|
||||||
static table = 'tasks'
|
static table = 'tasks'
|
||||||
|
@ -11,12 +12,11 @@ export class TaskFactory extends Factory {
|
||||||
id: '{increment}',
|
id: '{increment}',
|
||||||
title: faker.lorem.words(3),
|
title: faker.lorem.words(3),
|
||||||
done: false,
|
done: false,
|
||||||
project_id: 1,
|
list_id: 1,
|
||||||
created_by_id: 1,
|
created_by_id: 1,
|
||||||
index: '{increment}',
|
index: '{increment}',
|
||||||
position: '{increment}',
|
created: formatISO(now),
|
||||||
created: now.toISOString(),
|
updated: formatISO(now)
|
||||||
updated: now.toISOString()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
import {Factory} from '../support/factory'
|
import {Factory} from '../support/factory'
|
||||||
|
import {formatISO} from 'date-fns'
|
||||||
|
|
||||||
export class TaskAssigneeFactory extends Factory {
|
export class TaskAssigneeFactory extends Factory {
|
||||||
static table = 'task_assignees'
|
static table = 'task_assignees'
|
||||||
|
@ -10,7 +11,7 @@ export class TaskAssigneeFactory extends Factory {
|
||||||
id: '{increment}',
|
id: '{increment}',
|
||||||
task_id: 1,
|
task_id: 1,
|
||||||
user_id: 1,
|
user_id: 1,
|
||||||
created: now.toISOString(),
|
created: formatISO(now),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,16 +0,0 @@
|
||||||
import {Factory} from '../support/factory'
|
|
||||||
|
|
||||||
export class TaskAttachmentFactory extends Factory {
|
|
||||||
static table = 'task_attachments'
|
|
||||||
|
|
||||||
static factory() {
|
|
||||||
const now = new Date()
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: '{increment}',
|
|
||||||
task_id: 1,
|
|
||||||
file_id: 1,
|
|
||||||
created: now.toISOString(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {faker} from '@faker-js/faker'
|
import faker from 'faker'
|
||||||
|
|
||||||
import {Factory} from '../support/factory'
|
import {Factory} from '../support/factory'
|
||||||
|
import {formatISO} from "date-fns"
|
||||||
|
|
||||||
export class TaskCommentFactory extends Factory {
|
export class TaskCommentFactory extends Factory {
|
||||||
static table = 'task_comments'
|
static table = 'task_comments'
|
||||||
|
@ -13,8 +14,8 @@ export class TaskCommentFactory extends Factory {
|
||||||
comment: faker.lorem.text(3),
|
comment: faker.lorem.text(3),
|
||||||
author_id: 1,
|
author_id: 1,
|
||||||
task_id: 1,
|
task_id: 1,
|
||||||
created: now.toISOString(),
|
created: formatISO(now),
|
||||||
updated: now.toISOString()
|
updated: formatISO(now)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,18 +0,0 @@
|
||||||
import {Factory} from '../support/factory'
|
|
||||||
|
|
||||||
export class TaskReminderFactory extends Factory {
|
|
||||||
static table = 'task_reminders'
|
|
||||||
|
|
||||||
static factory() {
|
|
||||||
const now = new Date()
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: '{increment}',
|
|
||||||
task_id: 1,
|
|
||||||
reminder: now.toISOString(),
|
|
||||||
created: now.toISOString(),
|
|
||||||
relative_to: '',
|
|
||||||
relative_period: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +1,6 @@
|
||||||
import {faker} from '@faker-js/faker'
|
import faker from 'faker'
|
||||||
import {Factory} from '../support/factory'
|
import {Factory} from '../support/factory'
|
||||||
|
import {formatISO} from 'date-fns'
|
||||||
|
|
||||||
export class TeamFactory extends Factory {
|
export class TeamFactory extends Factory {
|
||||||
static table = 'teams'
|
static table = 'teams'
|
||||||
|
@ -10,8 +11,8 @@ export class TeamFactory extends Factory {
|
||||||
return {
|
return {
|
||||||
name: faker.lorem.words(3),
|
name: faker.lorem.words(3),
|
||||||
created_by_id: 1,
|
created_by_id: 1,
|
||||||
created: now.toISOString(),
|
created: formatISO(now),
|
||||||
updated: now.toISOString(),
|
updated: formatISO(now)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
import {Factory} from '../support/factory'
|
import {Factory} from '../support/factory'
|
||||||
|
import {formatISO} from 'date-fns'
|
||||||
|
|
||||||
export class TeamMemberFactory extends Factory {
|
export class TeamMemberFactory extends Factory {
|
||||||
static table = 'team_members'
|
static table = 'team_members'
|
||||||
|
@ -8,7 +9,7 @@ export class TeamMemberFactory extends Factory {
|
||||||
team_id: 1,
|
team_id: 1,
|
||||||
user_id: 1,
|
user_id: 1,
|
||||||
admin: false,
|
admin: false,
|
||||||
created: new Date().toISOString(),
|
created: formatISO(new Date()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import {faker} from '@faker-js/faker'
|
import faker from 'faker'
|
||||||
|
|
||||||
import {Factory} from '../support/factory'
|
import {Factory} from '../support/factory'
|
||||||
|
import {formatISO} from "date-fns"
|
||||||
|
|
||||||
export class UserFactory extends Factory {
|
export class UserFactory extends Factory {
|
||||||
static table = 'users'
|
static table = 'users'
|
||||||
|
@ -13,9 +14,8 @@ export class UserFactory extends Factory {
|
||||||
username: faker.lorem.word(10) + faker.datatype.uuid(),
|
username: faker.lorem.word(10) + faker.datatype.uuid(),
|
||||||
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.', // 1234
|
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.', // 1234
|
||||||
status: 0,
|
status: 0,
|
||||||
issuer: 'local',
|
created: formatISO(now),
|
||||||
created: now.toISOString(),
|
updated: formatISO(now)
|
||||||
updated: now.toISOString(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
import {Factory} from '../support/factory'
|
||||||
|
import {formatISO} from "date-fns"
|
||||||
|
|
||||||
|
export class UserListFactory extends Factory {
|
||||||
|
static table = 'users_lists'
|
||||||
|
|
||||||
|
static factory() {
|
||||||
|
const now = new Date()
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: '{increment}',
|
||||||
|
list_id: 1,
|
||||||
|
user_id: 1,
|
||||||
|
right: 0,
|
||||||
|
created: formatISO(now),
|
||||||
|
updated: formatISO(now)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,18 +0,0 @@
|
||||||
import {Factory} from '../support/factory'
|
|
||||||
|
|
||||||
export class UserProjectFactory extends Factory {
|
|
||||||
static table = 'users_projects'
|
|
||||||
|
|
||||||
static factory() {
|
|
||||||
const now = new Date()
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: '{increment}',
|
|
||||||
project_id: 1,
|
|
||||||
user_id: 1,
|
|
||||||
right: 0,
|
|
||||||
created: now.toISOString(),
|
|
||||||
updated: now.toISOString(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,540 @@
|
||||||
|
import {formatISO, format} from 'date-fns'
|
||||||
|
|
||||||
|
import {TaskFactory} from '../../factories/task'
|
||||||
|
import {ListFactory} from '../../factories/list'
|
||||||
|
import {UserListFactory} from '../../factories/users_list'
|
||||||
|
import {UserFactory} from '../../factories/user'
|
||||||
|
import {NamespaceFactory} from '../../factories/namespace'
|
||||||
|
import {BucketFactory} from '../../factories/bucket'
|
||||||
|
|
||||||
|
import '../../support/authenticateUser'
|
||||||
|
|
||||||
|
describe('Lists', () => {
|
||||||
|
let lists
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
UserFactory.create(1)
|
||||||
|
NamespaceFactory.create(1)
|
||||||
|
lists = ListFactory.create(1, {
|
||||||
|
title: 'First List'
|
||||||
|
})
|
||||||
|
TaskFactory.truncate()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should create a new list', () => {
|
||||||
|
cy.visit('/')
|
||||||
|
cy.get('.namespace-title .dropdown-trigger')
|
||||||
|
.click()
|
||||||
|
cy.get('.namespace-title .dropdown .dropdown-item')
|
||||||
|
.contains('New list')
|
||||||
|
.click()
|
||||||
|
cy.url()
|
||||||
|
.should('contain', '/namespaces/1/list')
|
||||||
|
cy.get('.card-header-title')
|
||||||
|
.contains('New list')
|
||||||
|
cy.get('input.input')
|
||||||
|
.type('New List')
|
||||||
|
cy.get('.button')
|
||||||
|
.contains('Create')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.get('.global-notification', { timeout: 1000 }) // Waiting until the request to create the new list is done
|
||||||
|
.should('contain', 'Success')
|
||||||
|
cy.url()
|
||||||
|
.should('contain', '/lists/')
|
||||||
|
cy.get('.list-title h1')
|
||||||
|
.should('contain', 'New List')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should redirect to a specific list view after visited', () => {
|
||||||
|
cy.visit('/lists/1/kanban')
|
||||||
|
cy.url()
|
||||||
|
.should('contain', '/lists/1/kanban')
|
||||||
|
cy.visit('/lists/1')
|
||||||
|
cy.url()
|
||||||
|
.should('contain', '/lists/1/kanban')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should rename the list in all places', () => {
|
||||||
|
const tasks = TaskFactory.create(5, {
|
||||||
|
id: '{increment}',
|
||||||
|
list_id: 1,
|
||||||
|
})
|
||||||
|
const newListName = 'New list name'
|
||||||
|
|
||||||
|
cy.visit('/lists/1')
|
||||||
|
cy.get('.list-title h1')
|
||||||
|
.should('contain', 'First List')
|
||||||
|
|
||||||
|
cy.get('.namespace-container .menu.namespaces-lists .more-container .menu-list li:first-child .dropdown .dropdown-trigger')
|
||||||
|
.click()
|
||||||
|
cy.get('.namespace-container .menu.namespaces-lists .more-container .menu-list li:first-child .dropdown .dropdown-content')
|
||||||
|
.contains('Edit')
|
||||||
|
.click()
|
||||||
|
cy.get('#title')
|
||||||
|
.type(`{selectall}${newListName}`)
|
||||||
|
cy.get('footer.modal-card-foot .button')
|
||||||
|
.contains('Save')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.get('.global-notification')
|
||||||
|
.should('contain', 'Success')
|
||||||
|
cy.get('.list-title h1')
|
||||||
|
.should('contain', newListName)
|
||||||
|
.should('not.contain', lists[0].title)
|
||||||
|
cy.get('.namespace-container .menu.namespaces-lists .more-container .menu-list li:first-child')
|
||||||
|
.should('contain', newListName)
|
||||||
|
.should('not.contain', lists[0].title)
|
||||||
|
cy.visit('/')
|
||||||
|
cy.get('.card-content .tasks')
|
||||||
|
.should('contain', newListName)
|
||||||
|
.should('not.contain', lists[0].title)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should remove a list', () => {
|
||||||
|
cy.visit(`/lists/${lists[0].id}`)
|
||||||
|
|
||||||
|
cy.get('.namespace-container .menu.namespaces-lists .more-container .menu-list li:first-child .dropdown .dropdown-trigger')
|
||||||
|
.click()
|
||||||
|
cy.get('.namespace-container .menu.namespaces-lists .more-container .menu-list li:first-child .dropdown .dropdown-content')
|
||||||
|
.contains('Delete')
|
||||||
|
.click()
|
||||||
|
cy.url()
|
||||||
|
.should('contain', '/settings/delete')
|
||||||
|
cy.get('[data-cy="modalPrimary"]')
|
||||||
|
.contains('Do it')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.get('.global-notification')
|
||||||
|
.should('contain', 'Success')
|
||||||
|
cy.get('.namespace-container .menu.namespaces-lists .more-container .menu-list')
|
||||||
|
.should('not.contain', lists[0].title)
|
||||||
|
cy.location('pathname')
|
||||||
|
.should('equal', '/')
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('List View', () => {
|
||||||
|
it('Should be an empty list', () => {
|
||||||
|
cy.visit('/lists/1')
|
||||||
|
cy.url()
|
||||||
|
.should('contain', '/lists/1/list')
|
||||||
|
cy.get('.list-title h1')
|
||||||
|
.should('contain', 'First List')
|
||||||
|
cy.get('.list-title .dropdown')
|
||||||
|
.should('exist')
|
||||||
|
cy.get('p')
|
||||||
|
.contains('This list is currently empty.')
|
||||||
|
.should('exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should navigate to the task when the title is clicked', () => {
|
||||||
|
const tasks = TaskFactory.create(5, {
|
||||||
|
id: '{increment}',
|
||||||
|
list_id: 1,
|
||||||
|
})
|
||||||
|
cy.visit('/lists/1/list')
|
||||||
|
|
||||||
|
cy.get('.tasks .task .tasktext')
|
||||||
|
.contains(tasks[0].title)
|
||||||
|
.first()
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.url()
|
||||||
|
.should('contain', `/tasks/${tasks[0].id}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should not see any elements for a list which is shared read only', () => {
|
||||||
|
UserFactory.create(2)
|
||||||
|
UserListFactory.create(1, {
|
||||||
|
list_id: 2,
|
||||||
|
user_id: 1,
|
||||||
|
right: 0,
|
||||||
|
})
|
||||||
|
const lists = ListFactory.create(2, {
|
||||||
|
owner_id: '{increment}',
|
||||||
|
namespace_id: '{increment}',
|
||||||
|
})
|
||||||
|
cy.visit(`/lists/${lists[1].id}/`)
|
||||||
|
|
||||||
|
cy.get('.list-title a.icon')
|
||||||
|
.should('not.exist')
|
||||||
|
cy.get('input.input[placeholder="Add a new task..."')
|
||||||
|
.should('not.exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should only show the color of a list in the navigation and not in the list view', () => {
|
||||||
|
const lists = ListFactory.create(1, {
|
||||||
|
hex_color: '00db60',
|
||||||
|
})
|
||||||
|
TaskFactory.create(10, {
|
||||||
|
list_id: lists[0].id,
|
||||||
|
})
|
||||||
|
cy.visit(`/lists/${lists[0].id}/`)
|
||||||
|
|
||||||
|
cy.get('.menu-list li .list-menu-link .color-bubble')
|
||||||
|
.should('have.css', 'background-color', 'rgb(0, 219, 96)')
|
||||||
|
cy.get('.tasks-container .tasks .color-bubble')
|
||||||
|
.should('not.exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should paginate for > 50 tasks', () => {
|
||||||
|
const tasks = TaskFactory.create(100, {
|
||||||
|
id: '{increment}',
|
||||||
|
title: i => `task${i}`,
|
||||||
|
list_id: 1,
|
||||||
|
})
|
||||||
|
cy.visit('/lists/1/list')
|
||||||
|
|
||||||
|
cy.get('.tasks-container .tasks')
|
||||||
|
.should('contain', tasks[99].title)
|
||||||
|
|
||||||
|
cy.get('.card-content .pagination .pagination-link')
|
||||||
|
.contains('2')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.url()
|
||||||
|
.should('contain', '?page=2')
|
||||||
|
cy.get('.tasks-container .tasks')
|
||||||
|
.should('contain', tasks[1].title)
|
||||||
|
cy.get('.tasks-container .tasks')
|
||||||
|
.should('not.contain', tasks[99].title)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Table View', () => {
|
||||||
|
it('Should show a table with tasks', () => {
|
||||||
|
const tasks = TaskFactory.create(1)
|
||||||
|
cy.visit('/lists/1/table')
|
||||||
|
|
||||||
|
cy.get('.table-view table.table')
|
||||||
|
.should('exist')
|
||||||
|
cy.get('.table-view table.table')
|
||||||
|
.should('contain', tasks[0].title)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should have working column switches', () => {
|
||||||
|
TaskFactory.create(1)
|
||||||
|
cy.visit('/lists/1/table')
|
||||||
|
|
||||||
|
cy.get('.table-view .filter-container .items .button')
|
||||||
|
.contains('Columns')
|
||||||
|
.click()
|
||||||
|
cy.get('.table-view .filter-container .card.columns-filter .card-content .fancycheckbox .check')
|
||||||
|
.contains('Priority')
|
||||||
|
.click()
|
||||||
|
cy.get('.table-view .filter-container .card.columns-filter .card-content .fancycheckbox .check')
|
||||||
|
.contains('Done')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.get('.table-view table.table th')
|
||||||
|
.contains('Priority')
|
||||||
|
.should('exist')
|
||||||
|
cy.get('.table-view table.table th')
|
||||||
|
.contains('Done')
|
||||||
|
.should('not.exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should navigate to the task when the title is clicked', () => {
|
||||||
|
const tasks = TaskFactory.create(5, {
|
||||||
|
id: '{increment}',
|
||||||
|
list_id: 1,
|
||||||
|
})
|
||||||
|
cy.visit('/lists/1/table')
|
||||||
|
|
||||||
|
cy.get('.table-view table.table')
|
||||||
|
.contains(tasks[0].title)
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.url()
|
||||||
|
.should('contain', `/tasks/${tasks[0].id}`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Gantt View', () => {
|
||||||
|
it('Hides tasks with no dates', () => {
|
||||||
|
const tasks = TaskFactory.create(1)
|
||||||
|
cy.visit('/lists/1/gantt')
|
||||||
|
|
||||||
|
cy.get('.gantt-chart-container .gantt-chart .tasks')
|
||||||
|
.should('not.contain', tasks[0].title)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Shows tasks from the current and next month', () => {
|
||||||
|
const now = new Date()
|
||||||
|
const nextMonth = now
|
||||||
|
nextMonth.setDate(1)
|
||||||
|
nextMonth.setMonth(now.getMonth() + 1)
|
||||||
|
|
||||||
|
cy.visit('/lists/1/gantt')
|
||||||
|
|
||||||
|
cy.get('.gantt-chart-container .gantt-chart .months')
|
||||||
|
.should('contain', format(now, 'MMMM'))
|
||||||
|
.should('contain', format(nextMonth, 'MMMM'))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Shows tasks with dates', () => {
|
||||||
|
const now = new Date()
|
||||||
|
const tasks = TaskFactory.create(1, {
|
||||||
|
start_date: formatISO(now),
|
||||||
|
end_date: formatISO(now.setDate(now.getDate() + 4))
|
||||||
|
})
|
||||||
|
cy.visit('/lists/1/gantt')
|
||||||
|
|
||||||
|
cy.get('.gantt-chart-container .gantt-chart .tasks')
|
||||||
|
.should('not.be.empty')
|
||||||
|
cy.get('.gantt-chart-container .gantt-chart .tasks')
|
||||||
|
.should('contain', tasks[0].title)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Shows tasks with no dates after enabling them', () => {
|
||||||
|
TaskFactory.create(1, {
|
||||||
|
start_date: null,
|
||||||
|
end_date: null,
|
||||||
|
})
|
||||||
|
cy.visit('/lists/1/gantt')
|
||||||
|
|
||||||
|
cy.get('.gantt-chart-container .gantt-options .fancycheckbox')
|
||||||
|
.contains('Show tasks which don\'t have dates set')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.get('.gantt-chart-container .gantt-chart .tasks')
|
||||||
|
.should('not.be.empty')
|
||||||
|
cy.get('.gantt-chart-container .gantt-chart .tasks .task.nodate')
|
||||||
|
.should('exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Drags a task around', () => {
|
||||||
|
const now = new Date()
|
||||||
|
TaskFactory.create(1, {
|
||||||
|
start_date: formatISO(now),
|
||||||
|
end_date: formatISO(now.setDate(now.getDate() + 4))
|
||||||
|
})
|
||||||
|
cy.visit('/lists/1/gantt')
|
||||||
|
|
||||||
|
cy.get('.gantt-chart-container .gantt-chart .tasks .task')
|
||||||
|
.first()
|
||||||
|
.trigger('mousedown', {which: 1})
|
||||||
|
.trigger('mousemove', {clientX: 500, clientY: 0})
|
||||||
|
.trigger('mouseup', {force: true})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Kanban', () => {
|
||||||
|
let buckets
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
buckets = BucketFactory.create(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Shows all buckets with their tasks', () => {
|
||||||
|
const data = TaskFactory.create(10, {
|
||||||
|
list_id: 1,
|
||||||
|
bucket_id: 1,
|
||||||
|
})
|
||||||
|
cy.visit('/lists/1/kanban')
|
||||||
|
|
||||||
|
cy.get('.kanban .bucket .title')
|
||||||
|
.contains(buckets[0].title)
|
||||||
|
.should('exist')
|
||||||
|
cy.get('.kanban .bucket .title')
|
||||||
|
.contains(buckets[1].title)
|
||||||
|
.should('exist')
|
||||||
|
cy.get('.kanban .bucket')
|
||||||
|
.first()
|
||||||
|
.should('contain', data[0].title)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Can add a new task to a bucket', () => {
|
||||||
|
const data = TaskFactory.create(2, {
|
||||||
|
list_id: 1,
|
||||||
|
bucket_id: 1,
|
||||||
|
})
|
||||||
|
cy.visit('/lists/1/kanban')
|
||||||
|
|
||||||
|
cy.get('.kanban .bucket')
|
||||||
|
.contains(buckets[0].title)
|
||||||
|
.get('.bucket-footer .button')
|
||||||
|
.contains('Add another task')
|
||||||
|
.click()
|
||||||
|
cy.get('.kanban .bucket')
|
||||||
|
.contains(buckets[0].title)
|
||||||
|
.get('.bucket-footer .field .control input.input')
|
||||||
|
.type('New Task{enter}')
|
||||||
|
|
||||||
|
cy.get('.kanban .bucket')
|
||||||
|
.first()
|
||||||
|
.should('contain', 'New Task')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Can create a new bucket', () => {
|
||||||
|
cy.visit('/lists/1/kanban')
|
||||||
|
|
||||||
|
cy.get('.kanban .bucket.new-bucket .button')
|
||||||
|
.click()
|
||||||
|
cy.get('.kanban .bucket.new-bucket input.input')
|
||||||
|
.type('New Bucket{enter}')
|
||||||
|
|
||||||
|
cy.wait(1000) // Wait for the request to finish
|
||||||
|
cy.get('.kanban .bucket .title')
|
||||||
|
.contains('New Bucket')
|
||||||
|
.should('exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Can set a bucket limit', () => {
|
||||||
|
cy.visit('/lists/1/kanban')
|
||||||
|
|
||||||
|
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger')
|
||||||
|
.first()
|
||||||
|
.click()
|
||||||
|
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item')
|
||||||
|
.contains('Limit: Not Set')
|
||||||
|
.click()
|
||||||
|
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item .field input.input')
|
||||||
|
.first()
|
||||||
|
.type(3)
|
||||||
|
cy.get('[data-cy="setBucketLimit"]')
|
||||||
|
.first()
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.get('.kanban .bucket .bucket-header span.limit')
|
||||||
|
.contains('0/3')
|
||||||
|
.should('exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Can rename a bucket', () => {
|
||||||
|
cy.visit('/lists/1/kanban')
|
||||||
|
|
||||||
|
cy.get('.kanban .bucket .bucket-header .title')
|
||||||
|
.first()
|
||||||
|
.type('{selectall}New Bucket Title{enter}')
|
||||||
|
cy.get('.kanban .bucket .bucket-header .title')
|
||||||
|
.first()
|
||||||
|
.should('contain', 'New Bucket Title')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Can delete a bucket', () => {
|
||||||
|
cy.visit('/lists/1/kanban')
|
||||||
|
|
||||||
|
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger')
|
||||||
|
.first()
|
||||||
|
.click()
|
||||||
|
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item')
|
||||||
|
.contains('Delete')
|
||||||
|
.click()
|
||||||
|
cy.get('.modal-mask .modal-container .modal-content .header')
|
||||||
|
.should('contain', 'Delete the bucket')
|
||||||
|
cy.get('.modal-mask .modal-container .modal-content .actions .button')
|
||||||
|
.contains('Do it!')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.get('.kanban .bucket .title')
|
||||||
|
.contains(buckets[0].title)
|
||||||
|
.should('not.exist')
|
||||||
|
cy.get('.kanban .bucket .title')
|
||||||
|
.contains(buckets[1].title)
|
||||||
|
.should('exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Can drag tasks around', () => {
|
||||||
|
const tasks = TaskFactory.create(2, {
|
||||||
|
list_id: 1,
|
||||||
|
bucket_id: 1,
|
||||||
|
})
|
||||||
|
cy.visit('/lists/1/kanban')
|
||||||
|
|
||||||
|
cy.get('.kanban .bucket .tasks .task')
|
||||||
|
.contains(tasks[0].title)
|
||||||
|
.first()
|
||||||
|
.drag('.kanban .bucket:nth-child(2) .tasks .dropper')
|
||||||
|
|
||||||
|
cy.get('.kanban .bucket:nth-child(2) .tasks')
|
||||||
|
.should('contain', tasks[0].title)
|
||||||
|
cy.get('.kanban .bucket:nth-child(1) .tasks')
|
||||||
|
.should('not.contain', tasks[0].title)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should navigate to the task when the task card is clicked', () => {
|
||||||
|
const tasks = TaskFactory.create(5, {
|
||||||
|
id: '{increment}',
|
||||||
|
list_id: 1,
|
||||||
|
bucket_id: 1,
|
||||||
|
})
|
||||||
|
cy.visit('/lists/1/kanban')
|
||||||
|
|
||||||
|
cy.getSettled('.kanban .bucket .tasks .task')
|
||||||
|
.contains(tasks[0].title)
|
||||||
|
.should('be.visible')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.url()
|
||||||
|
.should('contain', `/tasks/${tasks[0].id}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should remove a task from the kanban board when moving it to another list', () => {
|
||||||
|
const lists = ListFactory.create(2)
|
||||||
|
BucketFactory.create(2, {
|
||||||
|
list_id: '{increment}',
|
||||||
|
})
|
||||||
|
const tasks = TaskFactory.create(5, {
|
||||||
|
id: '{increment}',
|
||||||
|
list_id: 1,
|
||||||
|
bucket_id: 1,
|
||||||
|
})
|
||||||
|
const task = tasks[0]
|
||||||
|
cy.visit('/lists/1/kanban')
|
||||||
|
|
||||||
|
cy.getSettled('.kanban .bucket .tasks .task')
|
||||||
|
.contains(task.title)
|
||||||
|
.should('be.visible')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.get('.task-view .action-buttons .button')
|
||||||
|
.contains('Move task')
|
||||||
|
.click()
|
||||||
|
cy.get('.task-view .content.details .field .multiselect.control .input-wrapper input')
|
||||||
|
.type(`${lists[1].title}{enter}`)
|
||||||
|
// The requests happen with a 200ms timeout. Because of that, the results are not yet there when cypress
|
||||||
|
// presses enter and we can't simulate pressing on enter to select the item.
|
||||||
|
cy.get('.task-view .content.details .field .multiselect.control .search-results')
|
||||||
|
.children()
|
||||||
|
.first()
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.get('.global-notification', { timeout: 1000 })
|
||||||
|
.should('contain', 'Success')
|
||||||
|
cy.go('back')
|
||||||
|
cy.get('.kanban .bucket')
|
||||||
|
.should('not.contain', task.title)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('List history', () => {
|
||||||
|
it('should show a list history on the home page', () => {
|
||||||
|
const lists = ListFactory.create(6)
|
||||||
|
|
||||||
|
cy.visit('/')
|
||||||
|
cy.get('h3')
|
||||||
|
.contains('Last viewed')
|
||||||
|
.should('not.exist')
|
||||||
|
|
||||||
|
cy.visit(`/lists/${lists[0].id}`)
|
||||||
|
cy.visit(`/lists/${lists[1].id}`)
|
||||||
|
cy.visit(`/lists/${lists[2].id}`)
|
||||||
|
cy.visit(`/lists/${lists[3].id}`)
|
||||||
|
cy.visit(`/lists/${lists[4].id}`)
|
||||||
|
cy.visit(`/lists/${lists[5].id}`)
|
||||||
|
|
||||||
|
cy.visit('/')
|
||||||
|
cy.get('h3')
|
||||||
|
.contains('Last viewed')
|
||||||
|
.should('exist')
|
||||||
|
cy.get('.list-cards-wrapper-2-rows')
|
||||||
|
.should('not.contain', lists[0].title)
|
||||||
|
.should('contain', lists[1].title)
|
||||||
|
.should('contain', lists[2].title)
|
||||||
|
.should('contain', lists[3].title)
|
||||||
|
.should('contain', lists[4].title)
|
||||||
|
.should('contain', lists[5].title)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,145 @@
|
||||||
|
import {UserFactory} from '../../factories/user'
|
||||||
|
|
||||||
|
import '../../support/authenticateUser'
|
||||||
|
import {ListFactory} from '../../factories/list'
|
||||||
|
import {NamespaceFactory} from '../../factories/namespace'
|
||||||
|
|
||||||
|
describe('Namepaces', () => {
|
||||||
|
let namespaces
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
UserFactory.create(1)
|
||||||
|
namespaces = NamespaceFactory.create(1)
|
||||||
|
ListFactory.create(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should be all there', () => {
|
||||||
|
cy.visit('/namespaces')
|
||||||
|
cy.get('[data-cy="namespace-title"]')
|
||||||
|
.should('contain', namespaces[0].title)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should create a new Namespace', () => {
|
||||||
|
const newNamespaceTitle = 'New Namespace'
|
||||||
|
|
||||||
|
cy.visit('/namespaces')
|
||||||
|
cy.get('[data-cy="new-namespace"]')
|
||||||
|
.should('contain', 'New namespace')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.url()
|
||||||
|
.should('contain', '/namespaces/new')
|
||||||
|
cy.get('.card-header-title')
|
||||||
|
.should('contain', 'New namespace')
|
||||||
|
cy.get('input.input')
|
||||||
|
.type(newNamespaceTitle)
|
||||||
|
cy.get('.button')
|
||||||
|
.contains('Create')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.get('.global-notification')
|
||||||
|
.should('contain', 'Success')
|
||||||
|
cy.get('.namespace-container')
|
||||||
|
.should('contain', newNamespaceTitle)
|
||||||
|
cy.url()
|
||||||
|
.should('contain', '/namespaces')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should rename the namespace all places', () => {
|
||||||
|
const newNamespaces = NamespaceFactory.create(5)
|
||||||
|
const newNamespaceName = 'New namespace name'
|
||||||
|
|
||||||
|
cy.visit('/namespaces')
|
||||||
|
|
||||||
|
cy.get(`.namespace-container .menu.namespaces-lists .namespace-title:contains(${newNamespaces[0].title}) .dropdown .dropdown-trigger`)
|
||||||
|
.click()
|
||||||
|
cy.get('.namespace-container .menu.namespaces-lists .namespace-title .dropdown .dropdown-content')
|
||||||
|
.contains('Edit')
|
||||||
|
.click()
|
||||||
|
cy.url()
|
||||||
|
.should('contain', '/settings/edit')
|
||||||
|
cy.get('#namespacetext')
|
||||||
|
.invoke('val')
|
||||||
|
.should('equal', newNamespaces[0].title) // wait until the namespace data is loaded
|
||||||
|
cy.get('#namespacetext')
|
||||||
|
.type(`{selectall}${newNamespaceName}`)
|
||||||
|
cy.get('footer.modal-card-foot .button')
|
||||||
|
.contains('Save')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.get('.global-notification', { timeout: 1000 })
|
||||||
|
.should('contain', 'Success')
|
||||||
|
cy.get('.namespace-container .menu.namespaces-lists')
|
||||||
|
.should('contain', newNamespaceName)
|
||||||
|
.should('not.contain', newNamespaces[0].title)
|
||||||
|
cy.get('[data-cy="namespaces-list"]')
|
||||||
|
.should('contain', newNamespaceName)
|
||||||
|
.should('not.contain', newNamespaces[0].title)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should remove a namespace when deleting it', () => {
|
||||||
|
const newNamespaces = NamespaceFactory.create(5)
|
||||||
|
|
||||||
|
cy.visit('/')
|
||||||
|
|
||||||
|
cy.get(`.namespace-container .menu.namespaces-lists .namespace-title:contains(${newNamespaces[0].title}) .dropdown .dropdown-trigger`)
|
||||||
|
.click()
|
||||||
|
cy.get('.namespace-container .menu.namespaces-lists .namespace-title .dropdown .dropdown-content')
|
||||||
|
.contains('Delete')
|
||||||
|
.click()
|
||||||
|
cy.url()
|
||||||
|
.should('contain', '/settings/delete')
|
||||||
|
cy.get('[data-cy="modalPrimary"]')
|
||||||
|
.contains('Do it')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.get('.global-notification')
|
||||||
|
.should('contain', 'Success')
|
||||||
|
cy.get('.namespace-container .menu.namespaces-lists')
|
||||||
|
.should('not.contain', newNamespaces[0].title)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should not show archived lists & namespaces if the filter is not checked', () => {
|
||||||
|
const n = NamespaceFactory.create(1, {
|
||||||
|
id: 2,
|
||||||
|
is_archived: true,
|
||||||
|
}, false)
|
||||||
|
ListFactory.create(1, {
|
||||||
|
id: 2,
|
||||||
|
namespace_id: n[0].id,
|
||||||
|
}, false)
|
||||||
|
|
||||||
|
ListFactory.create(1, {
|
||||||
|
id: 3,
|
||||||
|
is_archived: true,
|
||||||
|
}, false)
|
||||||
|
|
||||||
|
// Initial
|
||||||
|
cy.visit('/namespaces')
|
||||||
|
cy.get('.namespace')
|
||||||
|
.should('not.contain', 'Archived')
|
||||||
|
|
||||||
|
// Show archived
|
||||||
|
cy.get('[data-cy="show-archived-check"] label.check span')
|
||||||
|
.should('be.visible')
|
||||||
|
.click()
|
||||||
|
cy.get('[data-cy="show-archived-check"] input')
|
||||||
|
.should('be.checked')
|
||||||
|
cy.get('.namespace')
|
||||||
|
.should('contain', 'Archived')
|
||||||
|
|
||||||
|
// Don't show archived
|
||||||
|
cy.get('[data-cy="show-archived-check"] label.check span')
|
||||||
|
.should('be.visible')
|
||||||
|
.click()
|
||||||
|
cy.get('[data-cy="show-archived-check"] input')
|
||||||
|
.should('not.be.checked')
|
||||||
|
|
||||||
|
// Second time visiting after unchecking
|
||||||
|
cy.visit('/namespaces')
|
||||||
|
cy.get('[data-cy="show-archived-check"] input')
|
||||||
|
.should('not.be.checked')
|
||||||
|
cy.get('.namespace')
|
||||||
|
.should('not.contain', 'Archived')
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,37 @@
|
||||||
|
import {TaskFactory} from '../../factories/task'
|
||||||
|
import {ListFactory} from '../../factories/list'
|
||||||
|
import {NamespaceFactory} from '../../factories/namespace'
|
||||||
|
import {UserListFactory} from '../../factories/users_list'
|
||||||
|
|
||||||
|
import '../../support/authenticateUser'
|
||||||
|
|
||||||
|
describe('Editor', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
NamespaceFactory.create(1)
|
||||||
|
const lists = ListFactory.create(1)
|
||||||
|
TaskFactory.truncate()
|
||||||
|
UserListFactory.truncate()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Has a preview with checkable checkboxes', () => {
|
||||||
|
const tasks = TaskFactory.create(1, {
|
||||||
|
description: `# Test Heading
|
||||||
|
* Bullet 1
|
||||||
|
* Bullet 2
|
||||||
|
|
||||||
|
* [ ] Checklist
|
||||||
|
* [x] Checklist checked
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.visit(`/tasks/${tasks[0].id}`)
|
||||||
|
cy.get('input[type=checkbox][data-checkbox-num=0]')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.get('.task-view .details.content.description h3 span.is-small.has-text-success')
|
||||||
|
.contains('Saved!')
|
||||||
|
.should('exist')
|
||||||
|
cy.get('.preview.content')
|
||||||
|
.should('contain', 'Test Heading')
|
||||||
|
})
|
||||||
|
})
|