Compare commits
No commits in common. "main" and "main" have entirely different histories.
514
.drone.yml
514
.drone.yml
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: build
|
||||
|
||||
# TODO: update translations only nightly
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
include:
|
||||
|
@ -15,7 +15,6 @@ trigger:
|
|||
services:
|
||||
- name: api
|
||||
image: vikunja/api:unstable
|
||||
pull: always
|
||||
environment:
|
||||
VIKUNJA_SERVICE_TESTINGTOKEN: averyLongSecretToSe33dtheDB
|
||||
VIKUNJA_LOG_LEVEL: DEBUG
|
||||
|
@ -24,101 +23,36 @@ steps:
|
|||
# Disabled until we figure out why it is so slow
|
||||
# - name: restore-cache
|
||||
# image: meltwater/drone-cache:dev
|
||||
# pull: always
|
||||
# pull: true
|
||||
# environment:
|
||||
# AWS_ACCESS_KEY_ID:
|
||||
# from_secret: cache_aws_access_key_id
|
||||
# AWS_SECRET_ACCESS_KEY:
|
||||
# from_secret: cache_aws_secret_access_key
|
||||
# settings:
|
||||
# debug: true
|
||||
# restore: true
|
||||
# bucket: kolaente.dev-drone-dependency-cache
|
||||
# endpoint: https://s3.fr-par.scw.cloud
|
||||
# region: fr-par
|
||||
# path_style: true
|
||||
# cache_key: '{{ .Repo.Name }}_{{ checksum "pnpm-lock.yaml" }}_{{ arch }}_{{ os }}'
|
||||
# cache_key: '{{ .Repo.Name }}_{{ checksum "yarn.lock" }}_{{ arch }}_{{ os }}'
|
||||
# mount:
|
||||
# - .cache
|
||||
# - '.cache'
|
||||
|
||||
- name: dependencies
|
||||
image: node:20.11.0-alpine
|
||||
pull: always
|
||||
image: node:16
|
||||
pull: true
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
CYPRESS_CACHE_FOLDER: .cache/cypress
|
||||
PUPPETEER_SKIP_DOWNLOAD: true
|
||||
YARN_CACHE_FOLDER: .cache/yarn/
|
||||
CYPRESS_CACHE_FOLDER: .cache/cypress/
|
||||
commands:
|
||||
- corepack enable && pnpm config set store-dir .cache/pnpm
|
||||
- pnpm install --fetch-timeout 100000
|
||||
- yarn --frozen-lockfile --network-timeout 100000
|
||||
# depends_on:
|
||||
# - 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
|
||||
# image: meltwater/drone-cache:dev
|
||||
# pull: always
|
||||
# pull: true
|
||||
# environment:
|
||||
# AWS_ACCESS_KEY_ID:
|
||||
# from_secret: cache_aws_access_key_id
|
||||
|
@ -130,42 +64,82 @@ steps:
|
|||
# endpoint: https://s3.fr-par.scw.cloud
|
||||
# region: fr-par
|
||||
# path_style: true
|
||||
# cache_key: '{{ .Repo.Name }}_{{ checksum "pnpm-lock.yaml" }}_{{ arch }}_{{ os }}'
|
||||
# cache_key: '{{ .Repo.Name }}_{{ checksum "yarn.lock" }}_{{ arch }}_{{ os }}'
|
||||
# mount:
|
||||
# - .cache
|
||||
# - '.cache'
|
||||
# depends_on:
|
||||
# - dependencies
|
||||
|
||||
- name: deploy-preview
|
||||
image: williamjackson/netlify-cli
|
||||
pull: always
|
||||
user: root # The rest runs as root and thus the permissions wouldn't work
|
||||
- name: lint
|
||||
image: node:16
|
||||
pull: true
|
||||
environment:
|
||||
NETLIFY_AUTH_TOKEN:
|
||||
from_secret: netlify_auth_token
|
||||
NETLIFY_SITE_ID:
|
||||
from_secret: netlify_site_id
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
YARN_CACHE_FOLDER: .cache/yarn/
|
||||
CYPRESS_CACHE_FOLDER: .cache/cypress/
|
||||
commands:
|
||||
- cp -r dist dist-preview
|
||||
# Override the default api url used for preview
|
||||
- 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
|
||||
- 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: test-frontend
|
||||
image: cypress/browsers:node14.17.0-chrome91-ff89
|
||||
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:
|
||||
event:
|
||||
include:
|
||||
- pull_request
|
||||
status:
|
||||
- failure
|
||||
- success
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: release-latest
|
||||
|
||||
depends_on:
|
||||
|
@ -185,7 +159,7 @@ steps:
|
|||
|
||||
# - name: restore-cache
|
||||
# image: meltwater/drone-cache:dev
|
||||
# pull: always
|
||||
# pull: true
|
||||
# environment:
|
||||
# AWS_ACCESS_KEY_ID:
|
||||
# from_secret: cache_aws_access_key_id
|
||||
|
@ -197,36 +171,29 @@ steps:
|
|||
# endpoint: https://s3.fr-par.scw.cloud
|
||||
# region: fr-par
|
||||
# path_style: true
|
||||
# cache_key: '{{ .Repo.Name }}_{{ checksum "pnpm-lock.yaml" }}_{{ arch }}_{{ os }}'
|
||||
# cache_key: '{{ .Repo.Name }}_{{ checksum "yarn.lock" }}_{{ arch }}_{{ os }}'
|
||||
# mount:
|
||||
# - .cache
|
||||
# - '.cache'
|
||||
|
||||
- name: build
|
||||
image: node:20.11.0-alpine
|
||||
pull: always
|
||||
image: node:16
|
||||
pull: true
|
||||
group: build-static
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
SENTRY_AUTH_TOKEN:
|
||||
from_secret: sentry_auth_token
|
||||
SENTRY_ORG: vikunja
|
||||
SENTRY_PROJECT: frontend-oss
|
||||
PUPPETEER_SKIP_DOWNLOAD: true
|
||||
YARN_CACHE_FOLDER: .cache/yarn/
|
||||
commands:
|
||||
- apk add git
|
||||
- corepack enable && pnpm config set store-dir .cache/pnpm
|
||||
- pnpm install --fetch-timeout 100000 --frozen-lockfile
|
||||
- pnpm run lint
|
||||
- yarn --frozen-lockfile --network-timeout 100000
|
||||
- yarn run lint
|
||||
- "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
|
||||
# depends_on:
|
||||
# - restore-cache
|
||||
|
||||
- name: static
|
||||
image: kolaente/zip
|
||||
pull: always
|
||||
pull: true
|
||||
commands:
|
||||
- cp src/version.json dist
|
||||
- cd dist
|
||||
- zip -r ../vikunja-frontend-unstable.zip *
|
||||
- cd ..
|
||||
|
@ -234,7 +201,7 @@ steps:
|
|||
|
||||
- name: release
|
||||
image: plugins/s3
|
||||
pull: always
|
||||
pull: true
|
||||
settings:
|
||||
bucket: vikunja-releases
|
||||
access_key:
|
||||
|
@ -250,7 +217,6 @@ steps:
|
|||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: release-version
|
||||
|
||||
depends_on:
|
||||
|
@ -268,7 +234,7 @@ steps:
|
|||
|
||||
# - name: restore-cache
|
||||
# image: meltwater/drone-cache:dev
|
||||
# pull: always
|
||||
# pull: true
|
||||
# environment:
|
||||
# AWS_ACCESS_KEY_ID:
|
||||
# from_secret: cache_aws_access_key_id
|
||||
|
@ -280,35 +246,29 @@ steps:
|
|||
# endpoint: https://s3.fr-par.scw.cloud
|
||||
# region: fr-par
|
||||
# path_style: true
|
||||
# cache_key: '{{ .Repo.Name }}_{{ checksum "pnpm-lock.yaml" }}_{{ arch }}_{{ os }}'
|
||||
# cache_key: '{{ .Repo.Name }}_{{ checksum "yarn.lock" }}_{{ arch }}_{{ os }}'
|
||||
# mount:
|
||||
# - .cache
|
||||
# - '.cache'
|
||||
|
||||
- name: build
|
||||
image: node:20.11.0-alpine
|
||||
pull: always
|
||||
image: node:16
|
||||
pull: true
|
||||
group: build-static
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
SENTRY_AUTH_TOKEN:
|
||||
from_secret: sentry_auth_token
|
||||
SENTRY_ORG: vikunja
|
||||
SENTRY_PROJECT: frontend-oss
|
||||
YARN_CACHE_FOLDER: .cache/yarn/
|
||||
commands:
|
||||
- apk add git
|
||||
- corepack enable && pnpm config set store-dir .cache/pnpm
|
||||
- pnpm install --fetch-timeout 100000 --frozen-lockfile
|
||||
- pnpm run lint
|
||||
- yarn --frozen-lockfile --network-timeout 100000
|
||||
- yarn run lint
|
||||
- "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
|
||||
# depends_on:
|
||||
# - restore-cache
|
||||
|
||||
- name: static
|
||||
image: kolaente/zip
|
||||
pull: always
|
||||
pull: true
|
||||
commands:
|
||||
- cp src/version.json dist
|
||||
- cd dist
|
||||
- zip -r ../vikunja-frontend-${DRONE_TAG##v}.zip *
|
||||
- cd ..
|
||||
|
@ -316,7 +276,7 @@ steps:
|
|||
|
||||
- name: release
|
||||
image: plugins/s3
|
||||
pull: always
|
||||
pull: true
|
||||
settings:
|
||||
bucket: vikunja-releases
|
||||
access_key:
|
||||
|
@ -332,7 +292,6 @@ steps:
|
|||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: trigger-desktop-update
|
||||
|
||||
trigger:
|
||||
|
@ -357,10 +316,15 @@ steps:
|
|||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: docker-release
|
||||
name: docker-arm-release
|
||||
|
||||
depends_on:
|
||||
- build
|
||||
- release-latest
|
||||
- release-version
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: arm64
|
||||
|
||||
trigger:
|
||||
ref:
|
||||
|
@ -371,65 +335,201 @@ trigger:
|
|||
- cron
|
||||
|
||||
steps:
|
||||
- name: fetch-tags
|
||||
image: docker:git
|
||||
commands:
|
||||
- git fetch --tags
|
||||
|
||||
- name: docker-unstable
|
||||
image: thegeeklab/drone-docker-buildx
|
||||
privileged: true
|
||||
pull: always
|
||||
image: plugins/docker:linux-arm
|
||||
pull: true
|
||||
settings:
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
repo: vikunja/frontend
|
||||
tags: unstable
|
||||
tags: unstable-linux-arm
|
||||
build_args:
|
||||
- USE_RELEASE=false
|
||||
platforms:
|
||||
- linux/386
|
||||
- linux/amd64
|
||||
- linux/arm/v6
|
||||
- linux/arm/v7
|
||||
- linux/arm64/v8
|
||||
depends_on: [ fetch-tags ]
|
||||
- USE_RELEASE=true
|
||||
- RELEASE_VERSION=unstable
|
||||
when:
|
||||
ref:
|
||||
- refs/heads/main
|
||||
depends_on:
|
||||
- clone
|
||||
|
||||
- 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:
|
||||
ref:
|
||||
- refs/heads/main
|
||||
|
||||
- name: generate-tags
|
||||
image: thegeeklab/docker-autotag
|
||||
environment:
|
||||
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
|
||||
- name: docker-version
|
||||
image: plugins/docker:linux-amd64
|
||||
pull: true
|
||||
settings:
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
repo: vikunja/frontend
|
||||
auto_tag: true
|
||||
auto_tag_suffix: linux-amd64
|
||||
build_args:
|
||||
- USE_RELEASE=false
|
||||
platforms:
|
||||
- linux/386
|
||||
- linux/amd64
|
||||
- linux/arm/v6
|
||||
- linux/arm/v7
|
||||
- linux/arm64/v8
|
||||
depends_on: [ generate-tags ]
|
||||
- USE_RELEASE=true
|
||||
- RELEASE_VERSION=${DRONE_TAG##v}
|
||||
when:
|
||||
ref:
|
||||
- "refs/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:
|
||||
ref:
|
||||
- "refs/tags/**"
|
||||
|
@ -452,7 +552,9 @@ depends_on:
|
|||
- release-version
|
||||
- release-latest
|
||||
- trigger-desktop-update
|
||||
- docker-release
|
||||
- docker-arm-release
|
||||
- docker-amd64-release
|
||||
- docker-manifest
|
||||
|
||||
steps:
|
||||
- name: notify
|
||||
|
@ -473,27 +575,29 @@ kind: pipeline
|
|||
type: docker
|
||||
name: update-translations
|
||||
|
||||
depends_on:
|
||||
- build
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
include:
|
||||
- main
|
||||
- main
|
||||
event:
|
||||
include:
|
||||
- cron
|
||||
- cron
|
||||
cron:
|
||||
- update_translations
|
||||
|
||||
steps:
|
||||
- name: download
|
||||
pull: always
|
||||
image: ghcr.io/kolaente/kolaente/drone-crowdin-v2:latest
|
||||
image: jonasfranz/crowdin
|
||||
settings:
|
||||
crowdin_key:
|
||||
download: true
|
||||
export_dir: src/i18n/lang/
|
||||
ignore_branch: true
|
||||
project_identifier: vikunja
|
||||
environment:
|
||||
CROWDIN_KEY:
|
||||
from_secret: crowdin_key
|
||||
project_id: 462614
|
||||
target: download
|
||||
download_to: src/i18n/lang/
|
||||
download_export_approved_only: true
|
||||
|
||||
- name: move-files
|
||||
pull: always
|
||||
|
@ -513,25 +617,21 @@ steps:
|
|||
author_name: Frederick [Bot]
|
||||
branch: main
|
||||
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"
|
||||
ssh_key:
|
||||
from_secret: git_push_ssh_key
|
||||
from_secret: translation_git_push_ssh_key
|
||||
|
||||
- name: upload
|
||||
pull: always
|
||||
image: ghcr.io/kolaente/kolaente/drone-crowdin-v2:latest
|
||||
image: jonasfranz/crowdin
|
||||
depends_on:
|
||||
- clone
|
||||
settings:
|
||||
crowdin_key:
|
||||
from_secret: crowdin_key
|
||||
project_id: 462614
|
||||
target: upload
|
||||
upload_files:
|
||||
src/i18n/lang/en.json: en.json
|
||||
---
|
||||
kind: signature
|
||||
hmac: a044c7c4db3c2a11299d4d118397e9d25be36db241723a1bbd0a2f9cc90ffdac
|
||||
|
||||
...
|
||||
files:
|
||||
en.json: src/i18n/lang/en.json
|
||||
ignore_branch: true
|
||||
project_identifier: vikunja
|
||||
environment:
|
||||
CROWDIN_KEY:
|
||||
from_secret: crowdin_key
|
||||
|
|
|
@ -21,9 +21,5 @@ indent_size = 2
|
|||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.{scss,css}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[.nvmrc]
|
||||
insert_final_newline = false
|
|
@ -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
|
||||
open_collective: vikunja
|
||||
custom: ["https://vikunja.cloud", "https://www.buymeacoffee.com/kolaente"]
|
||||
custom: 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
|
||||
node_modules
|
||||
/dist*
|
||||
coverage
|
||||
*.zip
|
||||
.direnv/
|
||||
|
||||
# Test files
|
||||
cypress/screenshots
|
||||
cypress/videos
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
stats.html
|
||||
|
||||
# Editor directories and files
|
||||
.vscode
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
|
@ -33,9 +23,6 @@ cypress/videos
|
|||
*.sw*
|
||||
!rollup.sw.js
|
||||
|
||||
|
||||
# Local Netlify folder
|
||||
.netlify
|
||||
|
||||
# histoire
|
||||
.histoire
|
||||
# Test files
|
||||
cypress/screenshots
|
||||
cypress/videos
|
||||
|
|
14
.npmrc
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",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"editorconfig.editorconfig",
|
||||
"vue.volar",
|
||||
"vue.vscode-typescript-vue-plugin",
|
||||
"johnsoncodehk.volar",
|
||||
"lokalise.i18n-ally",
|
||||
"mgmcdermott.vscode-language-babel",
|
||||
"mikestead.dotenv",
|
||||
"Syler.sass-indented",
|
||||
"zixuanchen.vitest-explorer"
|
||||
"Syler.sass-indented"
|
||||
]
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"eslint.packageManager": "pnpm",
|
||||
"eslint.packageManager": "yarn",
|
||||
"editor.formatOnSave": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": true
|
||||
|
@ -18,10 +18,6 @@
|
|||
"javascriptreact",
|
||||
"vue"
|
||||
],
|
||||
|
||||
"volar.completion.preferredTagNameCase": "pascal",
|
||||
|
||||
// disable vetur in case it is installed
|
||||
"vetur.validation.template": false,
|
||||
|
||||
// i18n ally
|
||||
|
@ -30,5 +26,5 @@
|
|||
],
|
||||
"i18n-ally.sortKeys": true,
|
||||
"i18n-ally.keepFulfilled": true,
|
||||
"i18n-ally.keystyle": "nested"
|
||||
"i18n-ally.keystyle": "nested",
|
||||
}
|
4629
CHANGELOG.md
4629
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
92
Dockerfile
92
Dockerfile
|
@ -1,73 +1,45 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
# ┬─┐┬ ┐o┬ ┬─┐
|
||||
# │─││ │││ │ │
|
||||
# ┘─┘┘─┘┘┘─┘┘─┘
|
||||
|
||||
FROM --platform=$BUILDPLATFORM node:20.11.0-alpine AS builder
|
||||
# Stage 1: Build application
|
||||
FROM node:16 AS compile-image
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
ARG USE_RELEASE=false
|
||||
ARG RELEASE_VERSION=unstable
|
||||
ENV PNPM_CACHE_FOLDER .cache/pnpm/
|
||||
ENV PUPPETEER_SKIP_DOWNLOAD true
|
||||
ARG RELEASE_VERSION=main
|
||||
|
||||
COPY package.json ./
|
||||
COPY pnpm-lock.yaml ./
|
||||
COPY patches ./patches/
|
||||
ENV YARN_CACHE_FOLDER .cache/yarn/
|
||||
COPY . ./
|
||||
|
||||
RUN if [ "$USE_RELEASE" != true ]; then \
|
||||
# https://pnpm.io/installation#using-corepack
|
||||
corepack enable && \
|
||||
pnpm install; \
|
||||
fi
|
||||
RUN \
|
||||
if [ $USE_RELEASE = true ]; then \
|
||||
rm -rf dist/ && \
|
||||
wget https://dl.vikunja.io/frontend/vikunja-frontend-$RELEASE_VERSION.zip -O frontend-release.zip && \
|
||||
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 \
|
||||
apk add --no-cache --virtual .build-deps git jq && \
|
||||
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 apt-get update && apt-get install -y apt-utils openssl && \
|
||||
mkdir -p /etc/nginx/ssl && \
|
||||
openssl genrsa -out /etc/nginx/ssl/dummy.key 2048 && \
|
||||
openssl req -new -key /etc/nginx/ssl/dummy.key -out /etc/nginx/ssl/dummy.csr -subj "/C=DE/L=Berlin/O=Vikunja/CN=Vikunja Snakeoil" && \
|
||||
openssl x509 -req -days 3650 -in /etc/nginx/ssl/dummy.csr -signkey /etc/nginx/ssl/dummy.key -out /etc/nginx/ssl/dummy.crt
|
||||
|
||||
RUN if [ "$USE_RELEASE" = true ]; then \
|
||||
wget "https://dl.vikunja.io/frontend/vikunja-frontend-${RELEASE_VERSION}.zip" -O frontend-release.zip && \
|
||||
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
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
COPY run.sh /run.sh
|
||||
|
||||
# ┌┐┐┌─┐o┌┐┐┐ │
|
||||
# ││││ ┬││││┌┼┘
|
||||
# ┘└┘┘─┘┘┘└┘┘ └
|
||||
# copy compiled files from stage 1
|
||||
COPY --from=compile-image /build/dist /usr/share/nginx/html
|
||||
|
||||
# Unprivileged user
|
||||
ENV PUID 1000
|
||||
ENV PGID 1000
|
||||
|
||||
FROM nginx:stable-alpine AS runner
|
||||
WORKDIR /usr/share/nginx/html
|
||||
LABEL maintainer="maintainers@vikunja.io"
|
||||
|
||||
ENV VIKUNJA_HTTP_PORT 80
|
||||
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
|
||||
CMD "/run.sh"
|
||||
|
|
24
README.md
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
|
||||
|
||||
> 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)
|
||||
[![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.1-brightgreen.svg)](https://dl.vikunja.io)
|
||||
[![Translation](https://badges.crowdin.net/vikunja/localized.svg)](https://crowdin.com/project/vikunja)
|
||||
|
||||
This is the web frontend for Vikunja, written in Vue.js.
|
||||
|
@ -22,35 +18,27 @@ If you find any security-related issues you don't want to disclose publicly, ple
|
|||
## Docker
|
||||
|
||||
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
|
||||
|
||||
```shell
|
||||
pnpm install
|
||||
yarn install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
|
||||
```shell
|
||||
pnpm run serve
|
||||
yarn run serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
|
||||
```shell
|
||||
pnpm run build
|
||||
yarn run build
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
|
||||
```shell
|
||||
pnpm run lint
|
||||
```
|
||||
yarn run lint
|
||||
```
|
||||
|
|
59
cliff.toml
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
|
||||
|
||||
```shell
|
||||
pnpm run test:e2e
|
||||
yarn test:frontend
|
||||
```
|
||||
|
||||
### Using The Cypress Dashboard
|
||||
|
@ -44,5 +44,5 @@ pnpm run test:e2e
|
|||
To open the Cypress Dashboard and run tests from there, run
|
||||
|
||||
```shell
|
||||
pnpm run test:e2e:dev
|
||||
yarn cypress:open
|
||||
```
|
||||
|
|
|
@ -9,7 +9,7 @@ services:
|
|||
ports:
|
||||
- 3456:3456
|
||||
cypress:
|
||||
image: cypress/browsers:node18.12.0-chrome107
|
||||
image: cypress/browsers:node12.18.3-chrome87-ff82
|
||||
volumes:
|
||||
- ..:/project
|
||||
- $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 {formatISO} from 'date-fns'
|
||||
|
||||
export class BucketFactory extends Factory {
|
||||
static table = 'buckets'
|
||||
|
@ -10,10 +11,10 @@ export class BucketFactory extends Factory {
|
|||
return {
|
||||
id: '{increment}',
|
||||
title: faker.lorem.words(3),
|
||||
project_id: 1,
|
||||
list_id: 1,
|
||||
created_by_id: 1,
|
||||
created: now.toISOString(),
|
||||
updated: now.toISOString(),
|
||||
created: formatISO(now),
|
||||
updated: formatISO(now)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import {Factory} from '../support/factory'
|
||||
import {formatISO} from 'date-fns'
|
||||
|
||||
export class LabelTaskFactory extends Factory {
|
||||
static table = 'label_tasks'
|
||||
|
@ -10,7 +11,7 @@ export class LabelTaskFactory extends Factory {
|
|||
id: '{increment}',
|
||||
task_id: 1,
|
||||
label_id: 1,
|
||||
created: now.toISOString(),
|
||||
created: formatISO(now),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import {faker} from '@faker-js/faker'
|
||||
import faker from 'faker'
|
||||
|
||||
import {Factory} from '../support/factory'
|
||||
import {formatISO} from 'date-fns'
|
||||
|
||||
export class LabelFactory extends Factory {
|
||||
static table = 'labels'
|
||||
|
@ -14,8 +15,8 @@ export class LabelFactory extends Factory {
|
|||
description: faker.lorem.text(10),
|
||||
hex_color: (Math.random()*0xFFFFFF<<0).toString(16), // random 6-digit hex number
|
||||
created_by_id: 1,
|
||||
created: now.toISOString(),
|
||||
updated: now.toISOString(),
|
||||
created: formatISO(now),
|
||||
updated: formatISO(now),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
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 {
|
||||
static table = 'link_shares'
|
||||
|
@ -10,12 +11,12 @@ export class LinkShareFactory extends Factory {
|
|||
return {
|
||||
id: '{increment}',
|
||||
hash: faker.random.word(32),
|
||||
project_id: 1,
|
||||
list_id: 1,
|
||||
right: 0,
|
||||
sharing_type: 0,
|
||||
shared_by_id: 1,
|
||||
created: now.toISOString(),
|
||||
updated: now.toISOString(),
|
||||
created: formatISO(now),
|
||||
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 {formatISO} from 'date-fns'
|
||||
|
||||
export class TaskFactory extends Factory {
|
||||
static table = 'tasks'
|
||||
|
@ -11,12 +12,11 @@ export class TaskFactory extends Factory {
|
|||
id: '{increment}',
|
||||
title: faker.lorem.words(3),
|
||||
done: false,
|
||||
project_id: 1,
|
||||
list_id: 1,
|
||||
created_by_id: 1,
|
||||
index: '{increment}',
|
||||
position: '{increment}',
|
||||
created: now.toISOString(),
|
||||
updated: now.toISOString()
|
||||
created: formatISO(now),
|
||||
updated: formatISO(now)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import {Factory} from '../support/factory'
|
||||
import {formatISO} from 'date-fns'
|
||||
|
||||
export class TaskAssigneeFactory extends Factory {
|
||||
static table = 'task_assignees'
|
||||
|
@ -10,7 +11,7 @@ export class TaskAssigneeFactory extends Factory {
|
|||
id: '{increment}',
|
||||
task_id: 1,
|
||||
user_id: 1,
|
||||
created: now.toISOString(),
|
||||
created: formatISO(now),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {formatISO} from "date-fns"
|
||||
|
||||
export class TaskCommentFactory extends Factory {
|
||||
static table = 'task_comments'
|
||||
|
@ -13,8 +14,8 @@ export class TaskCommentFactory extends Factory {
|
|||
comment: faker.lorem.text(3),
|
||||
author_id: 1,
|
||||
task_id: 1,
|
||||
created: now.toISOString(),
|
||||
updated: now.toISOString()
|
||||
created: formatISO(now),
|
||||
updated: formatISO(now)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {formatISO} from 'date-fns'
|
||||
|
||||
export class TeamFactory extends Factory {
|
||||
static table = 'teams'
|
||||
|
@ -10,8 +11,8 @@ export class TeamFactory extends Factory {
|
|||
return {
|
||||
name: faker.lorem.words(3),
|
||||
created_by_id: 1,
|
||||
created: now.toISOString(),
|
||||
updated: now.toISOString(),
|
||||
created: formatISO(now),
|
||||
updated: formatISO(now)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import {Factory} from '../support/factory'
|
||||
import {formatISO} from 'date-fns'
|
||||
|
||||
export class TeamMemberFactory extends Factory {
|
||||
static table = 'team_members'
|
||||
|
@ -8,7 +9,7 @@ export class TeamMemberFactory extends Factory {
|
|||
team_id: 1,
|
||||
user_id: 1,
|
||||
admin: false,
|
||||
created: new Date().toISOString(),
|
||||
created: formatISO(new Date()),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import {faker} from '@faker-js/faker'
|
||||
import faker from 'faker'
|
||||
|
||||
import {Factory} from '../support/factory'
|
||||
import {formatISO} from "date-fns"
|
||||
|
||||
export class UserFactory extends Factory {
|
||||
static table = 'users'
|
||||
|
@ -13,9 +14,8 @@ export class UserFactory extends Factory {
|
|||
username: faker.lorem.word(10) + faker.datatype.uuid(),
|
||||
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.', // 1234
|
||||
status: 0,
|
||||
issuer: 'local',
|
||||
created: now.toISOString(),
|
||||
updated: now.toISOString(),
|
||||
created: formatISO(now),
|
||||
updated: formatISO(now)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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('Create a 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('.modal-mask .modal-container .modal-content .actions a.button')
|
||||
.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 .card-content .fancycheckbox .check')
|
||||
.contains('Priority')
|
||||
.click()
|
||||
cy.get('.table-view .filter-container .card .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('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item .field a.button.is-primary')
|
||||
.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('.namespace h1 span')
|
||||
.should('contain', namespaces[0].title)
|
||||
})
|
||||
|
||||
it('Should create a new Namespace', () => {
|
||||
const newNamespaceTitle = 'New Namespace'
|
||||
|
||||
cy.visit('/namespaces')
|
||||
cy.get('a.button')
|
||||
.contains('Create a new namespace')
|
||||
.click()
|
||||
|
||||
cy.url()
|
||||
.should('contain', '/namespaces/new')
|
||||
cy.get('.card-header-title')
|
||||
.should('contain', 'Create a 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('.content.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('.modal-mask .modal-container .modal-content .actions a.button')
|
||||
.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('.namespaces-list .namespace')
|
||||
.should('not.contain', 'Archived')
|
||||
|
||||
// Show archived
|
||||
cy.get('.namespaces-list .fancycheckbox.show-archived-check label.check span')
|
||||
.should('be.visible')
|
||||
.click()
|
||||
cy.get('.namespaces-list .fancycheckbox.show-archived-check input')
|
||||
.should('be.checked')
|
||||
cy.get('.namespaces-list .namespace')
|
||||
.should('contain', 'Archived')
|
||||
|
||||
// Don't show archived
|
||||
cy.get('.namespaces-list .fancycheckbox.show-archived-check label.check span')
|
||||
.should('be.visible')
|
||||
.click()
|
||||
cy.get('.namespaces-list .fancycheckbox.show-archived-check input')
|
||||
.should('not.be.checked')
|
||||
|
||||
// Second time visiting after unchecking
|
||||
cy.visit('/namespaces')
|
||||
cy.get('.namespaces-list .fancycheckbox.show-archived-check input')
|
||||
.should('not.be.checked')
|
||||
cy.get('.namespaces-list .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')
|
||||
})
|
||||
})
|
|
@ -0,0 +1,35 @@
|
|||
import '../../support/authenticateUser'
|
||||
|
||||
const setHours = hours => {
|
||||
const date = new Date()
|
||||
date.setHours(hours)
|
||||
cy.clock(+date)
|
||||
}
|
||||
|
||||
describe('Home Page', () => {
|
||||
it('shows the right salutation in the night', () => {
|
||||
setHours(4)
|
||||
cy.visit('/')
|
||||
cy.get('h2').should('contain', 'Good Night')
|
||||
})
|
||||
it('shows the right salutation in the morning', () => {
|
||||
setHours(8)
|
||||
cy.visit('/')
|
||||
cy.get('h2').should('contain', 'Good Morning')
|
||||
})
|
||||
it('shows the right salutation in the day', () => {
|
||||
setHours(13)
|
||||
cy.visit('/')
|
||||
cy.get('h2').should('contain', 'Hi')
|
||||
})
|
||||
it('shows the right salutation in the night', () => {
|
||||
setHours(20)
|
||||
cy.visit('/')
|
||||
cy.get('h2').should('contain', 'Good Evening')
|
||||
})
|
||||
it('shows the right salutation in the night again', () => {
|
||||
setHours(23)
|
||||
cy.visit('/')
|
||||
cy.get('h2').should('contain', 'Good Night')
|
||||
})
|
||||
})
|
|
@ -1,35 +1,29 @@
|
|||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||
import '../../support/authenticateUser'
|
||||
|
||||
describe('The Menu', () => {
|
||||
createFakeUserAndLogin()
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit('/')
|
||||
})
|
||||
|
||||
it('Is visible by default on desktop', () => {
|
||||
cy.get('.menu-container')
|
||||
cy.get('.namespace-container')
|
||||
.should('have.class', 'is-active')
|
||||
})
|
||||
|
||||
it('Can be hidden on desktop', () => {
|
||||
cy.get('button.menu-show-button:visible')
|
||||
cy.get('a.menu-show-button:visible')
|
||||
.click()
|
||||
cy.get('.menu-container')
|
||||
cy.get('.namespace-container')
|
||||
.should('not.have.class', 'is-active')
|
||||
})
|
||||
|
||||
it('Is hidden by default on mobile', () => {
|
||||
cy.viewport('iphone-8')
|
||||
cy.get('.menu-container')
|
||||
cy.get('.namespace-container')
|
||||
.should('not.have.class', 'is-active')
|
||||
})
|
||||
|
||||
it('Is can be shown on mobile', () => {
|
||||
cy.viewport('iphone-8')
|
||||
cy.get('button.menu-show-button:visible')
|
||||
cy.get('a.menu-show-button:visible')
|
||||
.click()
|
||||
cy.get('.menu-container')
|
||||
cy.get('.namespace-container')
|
||||
.should('have.class', 'is-active')
|
||||
})
|
||||
})
|
|
@ -0,0 +1,25 @@
|
|||
import {LinkShareFactory} from '../../factories/link_sharing'
|
||||
import {ListFactory} from '../../factories/list'
|
||||
import {TaskFactory} from '../../factories/task'
|
||||
|
||||
describe('Link shares', () => {
|
||||
it('Can view a link share', () => {
|
||||
const lists = ListFactory.create(1)
|
||||
const tasks = TaskFactory.create(10, {
|
||||
list_id: lists[0].id
|
||||
})
|
||||
const linkShares = LinkShareFactory.create(1, {
|
||||
list_id: lists[0].id,
|
||||
right: 0,
|
||||
})
|
||||
|
||||
cy.visit(`/share/${linkShares[0].hash}/auth`)
|
||||
|
||||
cy.get('h1.title')
|
||||
.should('contain', lists[0].title)
|
||||
cy.get('input.input[placeholder="Add a new task..."')
|
||||
.should('not.exist')
|
||||
cy.get('.tasks')
|
||||
.should('contain', tasks[0].title)
|
||||
})
|
||||
})
|
|
@ -1,12 +1,9 @@
|
|||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||
|
||||
import {TeamFactory} from '../../factories/team'
|
||||
import {TeamMemberFactory} from '../../factories/team_member'
|
||||
import {UserFactory} from '../../factories/user'
|
||||
import '../../support/authenticateUser'
|
||||
|
||||
describe('Team', () => {
|
||||
createFakeUserAndLogin()
|
||||
|
||||
it('Creates a new team', () => {
|
||||
TeamFactory.truncate()
|
||||
cy.visit('/teams')
|
|
@ -0,0 +1,420 @@
|
|||
import {formatISO} from 'date-fns'
|
||||
|
||||
import {TaskFactory} from '../../factories/task'
|
||||
import {ListFactory} from '../../factories/list'
|
||||
import {TaskCommentFactory} from '../../factories/task_comment'
|
||||
import {UserFactory} from '../../factories/user'
|
||||
import {NamespaceFactory} from '../../factories/namespace'
|
||||
import {UserListFactory} from '../../factories/users_list'
|
||||
|
||||
import '../../support/authenticateUser'
|
||||
import {TaskAssigneeFactory} from '../../factories/task_assignee'
|
||||
import {LabelFactory} from '../../factories/labels'
|
||||
import {LabelTaskFactory} from '../../factories/label_task'
|
||||
import {BucketFactory} from '../../factories/bucket'
|
||||
|
||||
describe('Task', () => {
|
||||
let namespaces
|
||||
let lists
|
||||
|
||||
beforeEach(() => {
|
||||
UserFactory.create(1)
|
||||
namespaces = NamespaceFactory.create(1)
|
||||
lists = ListFactory.create(1)
|
||||
TaskFactory.truncate()
|
||||
UserListFactory.truncate()
|
||||
})
|
||||
|
||||
it('Should be created new', () => {
|
||||
cy.visit('/lists/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 list', () => {
|
||||
TaskFactory.create(1)
|
||||
|
||||
cy.visit('/lists/1/list')
|
||||
cy.get('.list-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('/lists/1/list')
|
||||
cy.get('.tasks .task .fancycheckbox label.check')
|
||||
.first()
|
||||
.click()
|
||||
cy.get('.global-notification')
|
||||
.should('contain', 'Success')
|
||||
})
|
||||
|
||||
it('Can add a task to favorites', () => {
|
||||
TaskFactory.create(1)
|
||||
|
||||
cy.visit('/lists/1/list')
|
||||
cy.get('.tasks .task .favorite')
|
||||
.first()
|
||||
.click()
|
||||
cy.get('.menu.namespaces-lists')
|
||||
.should('contain', 'Favorites')
|
||||
})
|
||||
|
||||
describe('Task Detail View', () => {
|
||||
beforeEach(() => {
|
||||
TaskCommentFactory.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', namespaces[0].title)
|
||||
.should('contain', lists[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: formatISO(new Date())
|
||||
})
|
||||
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')
|
||||
.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('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 list has one', () => {
|
||||
const lists = ListFactory.create(1, {
|
||||
id: 1,
|
||||
identifier: 'TEST',
|
||||
})
|
||||
const tasks = TaskFactory.create(1, {
|
||||
id: 1,
|
||||
list_id: lists[0].id,
|
||||
index: 1,
|
||||
})
|
||||
|
||||
cy.visit(`/tasks/${tasks[0].id}`)
|
||||
|
||||
cy.get('.task-view h1.title.task-id')
|
||||
.should('contain', `${lists[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 .editor a')
|
||||
.click()
|
||||
cy.get('.task-view .details.content.description .editor .vue-easymde .EasyMDEContainer .CodeMirror-scroll')
|
||||
.type('{selectall}New Description')
|
||||
cy.get('.task-view .details.content.description .editor a')
|
||||
.contains('Save')
|
||||
.click()
|
||||
|
||||
cy.get('.task-view .details.content.description h3 span.is-small.has-text-success')
|
||||
.contains('Saved!')
|
||||
.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 .editor .vue-easymde .EasyMDEContainer .CodeMirror-scroll')
|
||||
.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 .editor')
|
||||
.should('contain', 'New Comment')
|
||||
cy.get('.global-notification')
|
||||
.should('contain', 'Success')
|
||||
})
|
||||
|
||||
it('Can move a task to another list', () => {
|
||||
const lists = ListFactory.create(2)
|
||||
BucketFactory.create(2, {
|
||||
list_id: '{increment}'
|
||||
})
|
||||
const tasks = TaskFactory.create(1, {
|
||||
id: 1,
|
||||
list_id: lists[0].id,
|
||||
})
|
||||
cy.visit(`/tasks/${tasks[0].id}`)
|
||||
|
||||
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('.task-view h6.subtitle')
|
||||
.should('contain', namespaces[0].title)
|
||||
.should('contain', lists[1].title)
|
||||
cy.get('.global-notification')
|
||||
.should('contain', 'Success')
|
||||
})
|
||||
|
||||
it('Can delete a task', () => {
|
||||
const tasks = TaskFactory.create(1, {
|
||||
id: 1,
|
||||
list_id: 1,
|
||||
})
|
||||
cy.visit(`/tasks/${tasks[0].id}`)
|
||||
|
||||
cy.get('.task-view .action-buttons .button')
|
||||
.should('be.visible')
|
||||
.contains('Delete task')
|
||||
.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', `/lists/${tasks[0].list_id}/`)
|
||||
})
|
||||
|
||||
it('Can add an assignee to a task', () => {
|
||||
const users = UserFactory.create(5)
|
||||
const tasks = TaskFactory.create(1, {
|
||||
id: 1,
|
||||
list_id: 1,
|
||||
})
|
||||
UserListFactory.create(5, {
|
||||
list_id: 1,
|
||||
user_id: '{increment}',
|
||||
})
|
||||
|
||||
cy.visit(`/tasks/${tasks[0].id}`)
|
||||
|
||||
cy.get('.task-view .action-buttons .button')
|
||||
.contains('Assign this task to a user')
|
||||
.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,
|
||||
list_id: 1,
|
||||
})
|
||||
UserListFactory.create(5, {
|
||||
list_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('a.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,
|
||||
list_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,
|
||||
list_id: 1,
|
||||
})
|
||||
const labels = LabelFactory.create(1)
|
||||
|
||||
cy.visit(`/tasks/${tasks[0].id}`)
|
||||
|
||||
cy.get('.task-view .action-buttons .button')
|
||||
.contains('Add labels')
|
||||
.click()
|
||||
cy.get('.task-view .details.labels-list .multiselect input')
|
||||
.type(labels[0].title)
|
||||
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', labels[0].title)
|
||||
})
|
||||
|
||||
it('Can remove a label from a task', () => {
|
||||
const tasks = TaskFactory.create(1, {
|
||||
id: 1,
|
||||
list_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('a.delete')
|
||||
.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 a')
|
||||
.contains('Tomorrow')
|
||||
.click()
|
||||
cy.get('.datepicker .datepicker-popup a.button')
|
||||
.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')
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,58 @@
|
|||
import {UserFactory} from '../../factories/user'
|
||||
|
||||
const testAndAssertFailed = fixture => {
|
||||
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(5000) // It can take waaaayy too long to log the user in
|
||||
cy.url().should('include', '/')
|
||||
cy.get('div.notification.is-danger').contains('Wrong username or password.')
|
||||
}
|
||||
|
||||
context('Login', () => {
|
||||
beforeEach(() => {
|
||||
UserFactory.create(1, {
|
||||
username: 'test',
|
||||
})
|
||||
cy.visit('/', {
|
||||
onBeforeLoad(win) {
|
||||
win.localStorage.removeItem('token')
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('Should log in with the right credentials', () => {
|
||||
const fixture = {
|
||||
username: 'test',
|
||||
password: '1234',
|
||||
}
|
||||
|
||||
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.url().should('include', '/')
|
||||
cy.clock(1625656161057) // 13:00
|
||||
cy.get('h2').should('contain', `Hi ${fixture.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)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,16 @@
|
|||
import '../../support/authenticateUser'
|
||||
|
||||
describe('Log out', () => {
|
||||
it('Logs the user out', () => {
|
||||
cy.visit('/')
|
||||
|
||||
cy.get('.navbar .user .username')
|
||||
.click()
|
||||
cy.get('.navbar .user .dropdown-menu a.dropdown-item')
|
||||
.contains('Logout')
|
||||
.click()
|
||||
|
||||
cy.url()
|
||||
.should('contain', '/login')
|
||||
})
|
||||
})
|
|
@ -17,14 +17,15 @@ context('Registration', () => {
|
|||
it('Should work without issues', () => {
|
||||
const fixture = {
|
||||
username: 'testuser',
|
||||
password: '12345678',
|
||||
password: '123456',
|
||||
email: 'testuser@example.com',
|
||||
}
|
||||
|
||||
cy.visit('/register')
|
||||
cy.get('#username').type(fixture.username)
|
||||
cy.get('#email').type(fixture.email)
|
||||
cy.get('#password').type(fixture.password)
|
||||
cy.get('#password1').type(fixture.password)
|
||||
cy.get('#password2').type(fixture.password)
|
||||
cy.get('#register-submit').click()
|
||||
cy.url().should('include', '/')
|
||||
cy.clock(1625656161057) // 13:00
|
||||
|
@ -34,15 +35,16 @@ context('Registration', () => {
|
|||
it('Should fail', () => {
|
||||
const fixture = {
|
||||
username: 'test',
|
||||
password: '12345678',
|
||||
password: '123456',
|
||||
email: 'testuser@example.com',
|
||||
}
|
||||
|
||||
cy.visit('/register')
|
||||
cy.get('#username').type(fixture.username)
|
||||
cy.get('#email').type(fixture.email)
|
||||
cy.get('#password').type(fixture.password)
|
||||
cy.get('#password1').type(fixture.password)
|
||||
cy.get('#password2').type(fixture.password)
|
||||
cy.get('#register-submit').click()
|
||||
cy.get('div.message.danger').contains('A user with this username already exists.')
|
||||
cy.get('div.notification.is-danger').contains('A user with this username already exists.')
|
||||
})
|
||||
})
|
|
@ -1,26 +1,28 @@
|
|||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||
import {UserFactory} from '../../factories/user'
|
||||
|
||||
import '../../support/authenticateUser'
|
||||
|
||||
describe('User Settings', () => {
|
||||
createFakeUserAndLogin()
|
||||
beforeEach(() => {
|
||||
UserFactory.create(1)
|
||||
})
|
||||
|
||||
it('Changes the user avatar', () => {
|
||||
cy.intercept(`${Cypress.env('API_URL')}/user/settings/avatar/upload`).as('uploadAvatar')
|
||||
|
||||
cy.visit('/user/settings/avatar')
|
||||
|
||||
cy.get('input[name=avatarProvider][value=upload]')
|
||||
.click()
|
||||
cy.get('input[type=file]', {timeout: 1000})
|
||||
.selectFile('cypress/fixtures/image.jpg', {force: true}) // The input is not visible, but on purpose
|
||||
cy.get('input[type=file]', { timeout: 1000 })
|
||||
.attachFile('image.jpg')
|
||||
cy.get('.vue-handler-wrapper.vue-handler-wrapper--south .vue-simple-handler.vue-simple-handler--south')
|
||||
.trigger('mousedown', {which: 1})
|
||||
.trigger('mousemove', {clientY: 100})
|
||||
.trigger('mouseup')
|
||||
cy.get('[data-cy="uploadAvatar"]')
|
||||
cy.get('a.button.is-primary')
|
||||
.contains('Upload Avatar')
|
||||
.click()
|
||||
|
||||
cy.wait('@uploadAvatar')
|
||||
cy.wait(3000) // Wait for the request to finish
|
||||
cy.get('.global-notification')
|
||||
.should('contain', 'Success')
|
||||
})
|
||||
|
@ -31,13 +33,13 @@ describe('User Settings', () => {
|
|||
cy.get('.general-settings .control input.input')
|
||||
.first()
|
||||
.type('Lorem Ipsum')
|
||||
cy.get('[data-cy="saveGeneralSettings"]')
|
||||
cy.get('.card.general-settings .button.is-primary')
|
||||
.contains('Save')
|
||||
.click()
|
||||
|
||||
cy.get('.global-notification')
|
||||
.should('contain', 'Success')
|
||||
cy.get('.navbar .username-dropdown-trigger .username')
|
||||
cy.get('.navbar .user .username')
|
||||
.should('contain', 'Lorem Ipsum')
|
||||
})
|
||||
})
|
|
@ -0,0 +1,21 @@
|
|||
/// <reference types="cypress" />
|
||||
// ***********************************************************
|
||||
// This example plugins/index.js can be used to load plugins
|
||||
//
|
||||
// You can change the location of this file or turn off loading
|
||||
// the plugins file with the 'pluginsFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/plugins-guide
|
||||
// ***********************************************************
|
||||
|
||||
// This function is called when a project is opened or re-opened (e.g. due to
|
||||
// the project's config changing)
|
||||
|
||||
/**
|
||||
* @type {Cypress.PluginConfig}
|
||||
*/
|
||||
module.exports = (on, config) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
|
||||
// This authenticates a user and puts the token in local storage which allows us to perform authenticated requests.
|
||||
// Built after https://github.com/cypress-io/cypress-example-recipes/tree/bd2d6ffb33214884cab343d38e7f9e6ebffb323f/examples/logging-in__jwt
|
||||
|
||||
import {UserFactory} from '../factories/user'
|
||||
|
||||
let token
|
||||
|
||||
before(() => {
|
||||
const users = UserFactory.create(1)
|
||||
|
||||
cy.request('POST', `${Cypress.env('API_URL')}/login`, {
|
||||
username: users[0].username,
|
||||
password: '1234',
|
||||
})
|
||||
.its('body')
|
||||
.then(r => {
|
||||
token = r.token
|
||||
})
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.log(`Using token ${token} to make authenticated requests`)
|
||||
cy.visit('/', {
|
||||
onBeforeLoad(win) {
|
||||
win.localStorage.setItem('token', token)
|
||||
},
|
||||
})
|
||||
})
|
|
@ -1,35 +0,0 @@
|
|||
|
||||
// This authenticates a user and puts the token in local storage which allows us to perform authenticated requests.
|
||||
// Built after https://github.com/cypress-io/cypress-example-recipes/tree/bd2d6ffb33214884cab343d38e7f9e6ebffb323f/examples/logging-in__jwt
|
||||
|
||||
import {UserFactory} from '../factories/user'
|
||||
|
||||
export function login(user, cacheAcrossSpecs = false) {
|
||||
if (!user) {
|
||||
throw new Error('Needs user')
|
||||
}
|
||||
// Caching session when logging in via page visit
|
||||
cy.session(`user__${user.username}`, () => {
|
||||
cy.request('POST', `${Cypress.env('API_URL')}/login`, {
|
||||
username: user.username,
|
||||
password: '1234',
|
||||
}).then(({ body }) => {
|
||||
window.localStorage.setItem('token', body.token)
|
||||
})
|
||||
}, {
|
||||
cacheAcrossSpecs,
|
||||
})
|
||||
}
|
||||
|
||||
export function createFakeUserAndLogin() {
|
||||
let user
|
||||
before(() => {
|
||||
user = UserFactory.create(1)[0]
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
login(user, true)
|
||||
})
|
||||
|
||||
return user
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/**
|
||||
* Recursively gets an element, returning only after it's determined to be attached to the DOM for good.
|
||||
*
|
||||
* Source: https://github.com/cypress-io/cypress/issues/7306#issuecomment-850621378
|
||||
*/
|
||||
Cypress.Commands.add('getSettled', (selector, opts = {}) => {
|
||||
const retries = opts.retries || 3
|
||||
const delay = opts.delay || 100
|
||||
|
||||
const isAttached = (resolve, count = 0) => {
|
||||
const el = Cypress.$(selector)
|
||||
|
||||
// is element attached to the DOM?
|
||||
count = Cypress.dom.isAttached(el) ? count + 1 : 0
|
||||
|
||||
// hit our base case, return the element
|
||||
if (count >= retries) {
|
||||
return resolve(el)
|
||||
}
|
||||
|
||||
// retry after a bit of a delay
|
||||
setTimeout(() => isAttached(resolve, count), delay)
|
||||
}
|
||||
|
||||
// wrap, so we can chain cypress commands off the result
|
||||
return cy.wrap(null).then(() => {
|
||||
return new Cypress.Promise((resolve) => {
|
||||
return isAttached(resolve, 0)
|
||||
}).then((el) => {
|
||||
return cy.wrap(el)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,37 +0,0 @@
|
|||
/// <reference types="cypress" />
|
||||
// ***********************************************
|
||||
// This example commands.ts shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add('login', (email, password) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||
//
|
||||
// declare global {
|
||||
// namespace Cypress {
|
||||
// interface Chainable {
|
||||
// login(email: string, password: string): Chainable<void>
|
||||
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
|
||||
// }
|
||||
// }
|
||||
// }
|
|
@ -1,12 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>Components App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div data-cy-root></div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,29 +0,0 @@
|
|||
// ***********************************************************
|
||||
// This example support/component.ts is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands'
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
|
||||
import { mount } from 'cypress/vue'
|
||||
// Ensure global styles are loaded
|
||||
import '../../src/styles/global.scss';
|
||||
|
||||
Cypress.Commands.add('mount', mount)
|
||||
|
||||
// Example use:
|
||||
// cy.mount(MyComponent)
|
|
@ -4,7 +4,7 @@ import {seed} from './seed'
|
|||
* A factory makes it easy to seed the database with data.
|
||||
*/
|
||||
export class Factory {
|
||||
static table: string | null = null
|
||||
static table = null
|
||||
|
||||
static factory() {
|
||||
return {}
|
|
@ -1,5 +1,6 @@
|
|||
|
||||
import './commands'
|
||||
import 'cypress-file-upload'
|
||||
import '@4tw/cypress-drag-drop'
|
||||
|
||||
// see https://github.com/cypress-io/cypress/issues/702#issuecomment-587127275
|
|
@ -1,26 +0,0 @@
|
|||
|
||||
export function updateUserSettings(settings) {
|
||||
const token = `Bearer ${window.localStorage.getItem('token')}`
|
||||
|
||||
return cy.request({
|
||||
method: 'GET',
|
||||
url: `${Cypress.env('API_URL')}/user`,
|
||||
headers: {
|
||||
'Authorization': token,
|
||||
},
|
||||
})
|
||||
.its('body')
|
||||
.then(oldSettings => {
|
||||
return cy.request({
|
||||
method: 'POST',
|
||||
url: `${Cypress.env('API_URL')}/user/settings/general`,
|
||||
headers: {
|
||||
'Authorization': token,
|
||||
},
|
||||
body: {
|
||||
...oldSettings,
|
||||
...settings,
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
image: vikunja/frontend:unstable
|
||||
manifests:
|
||||
-
|
||||
image: vikunja/frontend:unstable-linux-amd64
|
||||
platform:
|
||||
architecture: amd64
|
||||
os: linux
|
||||
-
|
||||
image: vikunja/frontend:unstable-linux-arm64
|
||||
platform:
|
||||
architecture: arm64
|
||||
os: linux
|
||||
-
|
||||
image: vikunja/frontend:unstable-linux-arm
|
||||
platform:
|
||||
architecture: arm
|
||||
os: linux
|
|
@ -0,0 +1,23 @@
|
|||
image: vikunja/frontend:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
|
||||
{{#if build.tags}}
|
||||
tags:
|
||||
{{#each build.tags}}
|
||||
- {{this}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
manifests:
|
||||
-
|
||||
image: vikunja/frontend:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
|
||||
platform:
|
||||
architecture: amd64
|
||||
os: linux
|
||||
-
|
||||
image: vikunja/frontend:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
|
||||
platform:
|
||||
architecture: arm64
|
||||
os: linux
|
||||
-
|
||||
image: vikunja/frontend:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
|
||||
platform:
|
||||
architecture: arm
|
||||
os: linux
|
|
@ -1,18 +0,0 @@
|
|||
#!/usr/bin/env sh
|
||||
set -e
|
||||
|
||||
echo "info: API URL is $VIKUNJA_API_URL"
|
||||
echo "info: Sentry enabled: $VIKUNJA_SENTRY_ENABLED"
|
||||
|
||||
# Escape the variable to prevent sed from complaining
|
||||
VIKUNJA_API_URL="$(echo "$VIKUNJA_API_URL" | sed -r 's/([:;])/\\\1/g')"
|
||||
VIKUNJA_SENTRY_DSN="$(echo "$VIKUNJA_SENTRY_DSN" | sed -r 's/([:;])/\\\1/g')"
|
||||
|
||||
sed -ri "s:^(\s*window.API_URL\s*=)\s*.+:\1 '${VIKUNJA_API_URL}':g" /usr/share/nginx/html/index.html
|
||||
sed -ri "s:^(\s*window.SENTRY_ENABLED\s*=)\s*.+:\1 ${VIKUNJA_SENTRY_ENABLED}:g" /usr/share/nginx/html/index.html
|
||||
sed -ri "s:^(\s*window.SENTRY_DSN\s*=)\s*.+:\1 '${VIKUNJA_SENTRY_DSN}':g" /usr/share/nginx/html/index.html
|
||||
sed -ri "s:^(\s*window.PROJECT_INFINITE_NESTING_ENABLED\s*=)\s*.+:\1 '${VIKUNJA_PROJECT_INFINITE_NESTING_ENABLED}':g" /usr/share/nginx/html/index.html
|
||||
sed -ri "s:^(\s*window.ALLOW_ICON_CHANGES\s*=)\s*.+:\1 ${VIKUNJA_ALLOW_ICON_CHANGES}:g" /usr/share/nginx/html/index.html
|
||||
sed -ri "s:^(\s*window.CUSTOM_LOGO_URL\s*=)\s*.+:\1 ${VIKUNJA_CUSTOM_LOGO_URL}:g" /usr/share/nginx/html/index.html
|
||||
|
||||
date -uIseconds | xargs echo 'info: started at'
|
|
@ -1,19 +0,0 @@
|
|||
#!/usr/bin/env sh
|
||||
set -e
|
||||
|
||||
if [ ! -f "/proc/net/if_inet6" ]; then
|
||||
echo "info: IPv6 is not available! Removing IPv6 listen configuration"
|
||||
find /etc/nginx/conf.d -name '*.conf' -type f | \
|
||||
while IFS= read -r CONFIG; do
|
||||
sed -r '/^\s*listen\s*\[::\]:.+$/d' "$CONFIG" > "$CONFIG.temp"
|
||||
if ! diff -U 5 "$CONFIG" "$CONFIG.temp" > "$CONFIG.diff"; then
|
||||
echo "info: Removing IPv6 lines from $CONFIG" | \
|
||||
cat - "$CONFIG.diff"
|
||||
echo "# IPv6 is disabled because /proc/net/if_inet6 was not found" | \
|
||||
cat - "$CONFIG.temp" > "$CONFIG"
|
||||
else
|
||||
echo "info: Skipping $CONFIG because it does not have IPv6 listen"
|
||||
fi
|
||||
rm -f "$CONFIG.temp" "$CONFIG.diff"
|
||||
done
|
||||
fi
|
|
@ -1,111 +0,0 @@
|
|||
# Generated by nginxconfig.io
|
||||
# https://www.digitalocean.com/community/tools/nginx?domains.0.server.domain=localhost&domains.0.server.documentRoot=%2Fusr%2Fshare%2Fnginx%2Fhtml&domains.0.server.cdnSubdomain=true&domains.0.https.https=false&domains.0.php.php=false&domains.0.routing.index=index.html&domains.0.routing.fallbackHtml=true&domains.0.routing.fallbackPhp=false&global.performance.assetsExpiration=1d&global.performance.mediaExpiration=1d&global.performance.svgExpiration=1d&global.performance.fontsExpiration=1d&global.logging.accessLog=%2Fdev%2Fstdout&global.logging.errorLog=%2Fdev%2Fstderr%20warn&global.logging.logNotFound=true&global.nginx.user=nginx&global.nginx.pid=%2Fvar%2Frun%2Fnginx.pid&global.nginx.clientMaxBodySize=50&global.docker.dockerfile=true&global.tools.modularizedStructure=false&global.tools.symlinkVhost=false
|
||||
# and then edited manually ;)
|
||||
|
||||
pid /tmp/nginx.pid;
|
||||
worker_processes auto;
|
||||
|
||||
events {
|
||||
multi_accept on;
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
charset utf-8;
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
server_tokens off;
|
||||
types_hash_max_size 2048;
|
||||
types_hash_bucket_size 64;
|
||||
|
||||
# rootless
|
||||
client_body_temp_path /tmp/client_temp;
|
||||
proxy_temp_path /tmp/proxy_temp_path;
|
||||
fastcgi_temp_path /tmp/fastcgi_temp;
|
||||
uwsgi_temp_path /tmp/uwsgi_temp;
|
||||
scgi_temp_path /tmp/scgi_temp;
|
||||
|
||||
# MIME
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
types {
|
||||
application/manifest+json webmanifest;
|
||||
}
|
||||
|
||||
# Logging
|
||||
log_format json escape=json
|
||||
'{'
|
||||
'"bytes_sent": "$bytes_sent",'
|
||||
'"http_user_agent": "$http_user_agent",'
|
||||
'"nginx_version": "$nginx_version",'
|
||||
'"query_string": "$query_string",'
|
||||
'"realip_remote_addr": "$realip_remote_addr",'
|
||||
'"remote_addr": "$remote_addr",'
|
||||
'"remote_user": "$remote_user",'
|
||||
'"request_length": "$request_length",'
|
||||
'"request_method": "$request_method",'
|
||||
'"request_time": "$request_time",'
|
||||
'"server_addr": "$server_addr",'
|
||||
'"server_port": "$server_port",'
|
||||
'"server_protocol": "$server_protocol",'
|
||||
'"status": "$status",'
|
||||
'"time_local": "$time_local",'
|
||||
'"uri": "$uri"'
|
||||
'}';
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /dev/stdout main;
|
||||
error_log /dev/stderr warn;
|
||||
|
||||
keepalive_timeout 65;
|
||||
|
||||
# compression
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_buffers 16 8k;
|
||||
gzip_http_version 1.1;
|
||||
gzip_min_length 256;
|
||||
gzip_types
|
||||
text/plain
|
||||
text/css
|
||||
application/json
|
||||
application/x-javascript
|
||||
application/javascript
|
||||
text/xml
|
||||
application/xml
|
||||
application/xml+rss
|
||||
text/javascript
|
||||
application/vnd.ms-fontobject
|
||||
application/x-font-ttf
|
||||
font/opentype
|
||||
image/svg+xml
|
||||
image/x-icon
|
||||
audio/wav;
|
||||
|
||||
map_hash_max_size 128;
|
||||
map_hash_bucket_size 128;
|
||||
|
||||
map $sent_http_content_type $expires {
|
||||
default off;
|
||||
text/css max;
|
||||
application/javascript max;
|
||||
text/javascript max;
|
||||
application/vnd.ms-fontobject max;
|
||||
application/x-font-ttf max;
|
||||
font/opentype max;
|
||||
font/woff2 max;
|
||||
image/svg+xml max;
|
||||
image/x-icon max;
|
||||
audio/wav max;
|
||||
~images/ max;
|
||||
~font/ max;
|
||||
}
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
server {
|
||||
listen ${VIKUNJA_HTTP_PORT};
|
||||
listen [::]:${VIKUNJA_HTTP_PORT};
|
||||
## Needed when behind HAProxy with SSL termination + HTTP/2 support
|
||||
listen ${VIKUNJA_HTTP2_PORT} default_server http2 proxy_protocol;
|
||||
listen [::]:${VIKUNJA_HTTP2_PORT} default_server http2 proxy_protocol;
|
||||
|
||||
server_name _;
|
||||
expires $expires;
|
||||
root /usr/share/nginx/html;
|
||||
access_log /dev/stdout ${VIKUNJA_LOG_FORMAT};
|
||||
# security headers
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||
add_header Content-Security-Policy "default-src 'self' http: https: ws: wss: data: blob: 'unsafe-inline'; frame-ancestors 'self';" always;
|
||||
add_header Permissions-Policy "interest-cohort=()" always;
|
||||
|
||||
# . files
|
||||
location ~ /\.(?!well-known) {
|
||||
deny all;
|
||||
}
|
||||
|
||||
# assume that everything else is handled by the application router, by injecting the index.html.
|
||||
location / {
|
||||
autoindex off;
|
||||
expires off;
|
||||
add_header Cache-Control "public, max-age=0, s-maxage=0, must-revalidate" always;
|
||||
try_files $uri /index.html =404;
|
||||
}
|
||||
|
||||
# Disable caching for sw
|
||||
location = /sw.js {
|
||||
autoindex off;
|
||||
expires off;
|
||||
add_header Cache-Control "public, max-age=0, s-maxage=0, must-revalidate" always;
|
||||
}
|
||||
|
||||
# Disable caching for webmanifest
|
||||
location = /manifest.webmanifest {
|
||||
autoindex off;
|
||||
expires off;
|
||||
add_header Cache-Control "public, max-age=0, s-maxage=0, must-revalidate" always;
|
||||
}
|
||||
|
||||
# favicon.ico
|
||||
location = /favicon.ico {
|
||||
log_not_found off;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# robots.txt
|
||||
location = /robots.txt {
|
||||
log_not_found off;
|
||||
access_log off;
|
||||
expires -1; # no-cache
|
||||
}
|
||||
|
||||
location = /ready {
|
||||
return 200 "";
|
||||
access_log off;
|
||||
expires -1; # no-cache
|
||||
}
|
||||
|
||||
# all assets contain hash in filename, cache forever
|
||||
location ^~ /assets/ {
|
||||
add_header Cache-Control "public, max-age=31536000, s-maxage=31536000, immutable";
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# all workbox scripts are compiled with hash in filename, cache forever3
|
||||
location ^~ /workbox- {
|
||||
add_header Cache-Control "public, max-age=31536000, s-maxage=31536000, immutable";
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# assets, media
|
||||
location ~* .(txt|webmanifest|css|js|mjs|map|svg|jpg|jpeg|png|ico|ttf|woff|woff2|wav)$ {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html { }
|
||||
|
||||
}
|
|
@ -30,21 +30,21 @@ A basic service can look like this:
|
|||
|
||||
```javascript
|
||||
import AbstractService from './abstractService'
|
||||
import ProjectModel from '../models/project'
|
||||
import ListModel from '../models/list'
|
||||
|
||||
export default class ProjectService extends AbstractService {
|
||||
export default class ListService extends AbstractService {
|
||||
constructor() {
|
||||
super({
|
||||
getAll: '/projects',
|
||||
get: '/projects/{id}',
|
||||
create: '/namespaces/{namespaceID}/projects',
|
||||
update: '/projects/{id}',
|
||||
delete: '/projects/{id}',
|
||||
getAll: '/lists',
|
||||
get: '/lists/{id}',
|
||||
create: '/namespaces/{namespaceID}/lists',
|
||||
update: '/lists/{id}',
|
||||
delete: '/lists/{id}',
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new ProjectModel(data)
|
||||
return new ListModel(data)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
@ -132,7 +132,7 @@ import AbstractModel from './abstractModel'
|
|||
import TaskModel from './task'
|
||||
import UserModel from './user'
|
||||
|
||||
export default class ProjectModel extends AbstractModel {
|
||||
export default class ListModel extends AbstractModel {
|
||||
|
||||
constructor(data) {
|
||||
// The constructor of AbstractModel handles all the default parsing.
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
declare module 'postcss-easings' {
|
||||
import postcssEasings from 'postcss-easings'
|
||||
export default postcssEasings
|
||||
}
|
||||
|
||||
declare module 'postcss-easing-gradients' {
|
||||
import postcssEasingGradients from 'postcss-easing-gradients'
|
||||
export default postcssEasingGradients
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
/// <reference types="vite/client" />
|
||||
/// <reference types="vite-svg-loader" />
|
||||
/// <reference types="cypress" />
|
||||
/// <reference types="@histoire/plugin-vue/components" />
|
||||
|
||||
declare module 'postcss-focus-within/browser' {
|
||||
import focusWithinInit from 'postcss-focus-within/browser'
|
||||
export default focusWithinInit
|
||||
}
|
||||
|
||||
declare module 'css-has-pseudo/browser' {
|
||||
import cssHasPseudo from 'css-has-pseudo/browser'
|
||||
export default cssHasPseudo
|
||||
}
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VIKUNJA_API_URL?: string
|
||||
readonly VIKUNJA_HTTP_PORT?: number
|
||||
readonly VIKUNJA_HTTPS_PORT?: number
|
||||
|
||||
readonly VIKUNJA_SENTRY_ENABLED?: boolean
|
||||
readonly VIKUNJA_SENTRY_DSN?: string
|
||||
|
||||
readonly SENTRY_AUTH_TOKEN?: string
|
||||
readonly SENTRY_ORG?: string
|
||||
readonly SENTRY_PROJECT?: string
|
||||
|
||||
readonly VITE_IS_ONLINE: boolean
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
25
flake.lock
25
flake.lock
|
@ -1,25 +0,0 @@
|
|||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1701336116,
|
||||
"narHash": "sha256-kEmpezCR/FpITc6yMbAh4WrOCiT2zg5pSjnKrq51h5Y=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "f5c27c6136db4d76c30e533c20517df6864c46ee",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
10
flake.nix
10
flake.nix
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
description = "Vikunja frontend dev environment";
|
||||
|
||||
outputs = { self, nixpkgs }:
|
||||
let pkgs = nixpkgs.legacyPackages.x86_64-linux;
|
||||
in {
|
||||
defaultPackage.x86_64-linux =
|
||||
pkgs.mkShell { buildInputs = [ pkgs.nodePackages.pnpm pkgs.cypress pkgs.git-cliff ]; };
|
||||
};
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
import {defineConfig, defaultColors} from 'histoire'
|
||||
import {HstVue} from '@histoire/plugin-vue'
|
||||
import {HstScreenshot} from '@histoire/plugin-screenshot'
|
||||
|
||||
export default defineConfig({
|
||||
setupFile: './src/histoire.setup.ts',
|
||||
storyIgnored: [
|
||||
'**/node_modules/**',
|
||||
'**/dist/**',
|
||||
// see https://kolaente.dev/vikunja/frontend/pulls/2724#issuecomment-42012
|
||||
'**/.direnv/**',
|
||||
],
|
||||
plugins: [
|
||||
HstVue(),
|
||||
HstScreenshot({
|
||||
// Options here
|
||||
}),
|
||||
],
|
||||
theme: {
|
||||
title: 'Vikunja',
|
||||
colors: {
|
||||
// https://histoire.dev/guide/config.html#builtin-colors
|
||||
gray: defaultColors.zinc,
|
||||
primary: defaultColors.cyan,
|
||||
},
|
||||
// logo: {
|
||||
// square: './img/square.png',
|
||||
// light: './img/light.png',
|
||||
// dark: './img/dark.png',
|
||||
// },
|
||||
logoHref: 'https://vikunja.io',
|
||||
// favicon: './favicon.ico',
|
||||
},
|
||||
})
|
24
index.html
24
index.html
|
@ -1,15 +1,22 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>Vikunja</title>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<meta name="description" content="Vikunja (/vɪˈkuːnjə/) - The to-do app to organize your life.">
|
||||
<meta name="theme-color" content="#1973ff"/>
|
||||
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<link rel="apple-touch-icon" href="/images/icons/apple-touch-icon-180x180.png"/>
|
||||
<!--__vite-plugin-inject-preload__-->
|
||||
<link rel="preload" crossorigin="anonymous" href="/fonts/open-sans-v15-latin-700italic.woff2" as="font">
|
||||
<link rel="preload" crossorigin="anonymous" href="/fonts/open-sans-v15-latin-italic.woff2" as="font">
|
||||
<link rel="preload" crossorigin="anonymous" href="/fonts/quicksand-v7-latin-500.woff2" as="font">
|
||||
<link rel="preload" crossorigin="anonymous" href="/fonts/quicksand-v7-latin-700.woff2" as="font">
|
||||
<link rel="preload" crossorigin="anonymous" href="/fonts/open-sans-v15-latin-regular.woff2" as="font">
|
||||
<link rel="preload" crossorigin="anonymous" href="/fonts/open-sans-v15-latin-700.woff2" as="font">
|
||||
<link rel="preload" crossorigin="anonymous" href="/fonts/quicksand-v7-latin-regular.woff2" as="font">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
|
@ -18,22 +25,15 @@
|
|||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
<script>
|
||||
//
|
||||
//
|
||||
// This variable points the frontend to the api.
|
||||
// It has to be the full url, including the last /api/v1 part and port.
|
||||
// You can change this if your api is not reachable on the same port as the frontend.
|
||||
window.API_URL = 'http://localhost:3456/api/v1'
|
||||
// Enable error tracking with sentry. If this is set to true, will send anonymized data to
|
||||
// Enable error tracking with sentry. If this is set to true, will send anonymized data to
|
||||
// our sentry instance to notify us of potential problems.
|
||||
window.SENTRY_ENABLED = false
|
||||
window.SENTRY_DSN = 'https://85694a2d757547cbbc90cd4b55c5a18d@o1047380.ingest.sentry.io/6024480'
|
||||
// If enabled, allows the user to nest projects infinitely, instead of the default 2 levels.
|
||||
// This setting might change in the future or be removed completely.
|
||||
window.PROJECT_INFINITE_NESTING_ENABLED = false
|
||||
// Allow changing the logo and other icons based on various occasions throughout the year.
|
||||
window.ALLOW_ICON_CHANGES = true
|
||||
// Allow using a custom logo via external URL.
|
||||
window.CUSTOM_LOGO_URL = ''
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
15
netlify.toml
15
netlify.toml
|
@ -1,15 +0,0 @@
|
|||
[build]
|
||||
command = "pnpm run build"
|
||||
publish = "dist-preview"
|
||||
|
||||
[[redirects]]
|
||||
from = "/*"
|
||||
to = "/index.html"
|
||||
status = 200
|
||||
|
||||
[[headers]]
|
||||
for = "/*"
|
||||
[headers.values]
|
||||
X-Frame-Options = "DENY"
|
||||
X-XSS-Protection = "1; mode=block"
|
||||
X-Robots-Tag = "noindex"
|
|
@ -0,0 +1,83 @@
|
|||
user nginx;
|
||||
worker_processes 1;
|
||||
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
sendfile on;
|
||||
#tcp_nopush on;
|
||||
|
||||
keepalive_timeout 65;
|
||||
|
||||
gzip on;
|
||||
gzip_disable "msie6";
|
||||
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_buffers 16 8k;
|
||||
gzip_http_version 1.1;
|
||||
gzip_min_length 256;
|
||||
gzip_types text/plain text/css application/json application/x-javascript application/javascript text/xml application/xml application/xml+rss text/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype image/svg+xml image/x-icon audio/wav;
|
||||
|
||||
map_hash_max_size 128;
|
||||
map_hash_bucket_size 128;
|
||||
|
||||
# Expires map
|
||||
map $sent_http_content_type $expires {
|
||||
default off;
|
||||
text/html max;
|
||||
text/css max;
|
||||
application/javascript max;
|
||||
text/javascript max;
|
||||
application/vnd.ms-fontobject max;
|
||||
application/x-font-ttf max;
|
||||
font/opentype max;
|
||||
font/woff2 max;
|
||||
image/svg+xml max;
|
||||
image/x-icon max;
|
||||
audio/wav max;
|
||||
~image/ max;
|
||||
~font/ max;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen 81 default_server http2 proxy_protocol; ## Needed when behind HAProxy with SSL termination + HTTP/2 support
|
||||
listen 443 default_server ssl http2;
|
||||
|
||||
server_name _;
|
||||
|
||||
expires $expires;
|
||||
|
||||
ssl_certificate /etc/nginx/ssl/dummy.crt;
|
||||
ssl_certificate_key /etc/nginx/ssl/dummy.key;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
try_files $uri $uri/ /;
|
||||
index index.html index.htm;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -49,7 +49,7 @@
|
|||
inkscape:label="ink_ext_XXXXXX 1"
|
||||
style="display:inline"
|
||||
transform="translate(-92.67749,-674.48297)"><circle
|
||||
style="fill:#196aff;fill-opacity:1;stroke:none;stroke-width:2.88757133;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
style="fill:#5974d9;fill-opacity:1;stroke:none;stroke-width:2.88757133;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
id="path920"
|
||||
cx="242.67749"
|
||||
cy="828.77881"
|
||||
|
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
299
package.json
299
package.json
|
@ -1,187 +1,150 @@
|
|||
{
|
||||
"name": "vikunja-frontend",
|
||||
"description": "The todo app to organize your life.",
|
||||
"private": true,
|
||||
"version": "0.10.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://kolaente.dev/vikunja/frontend"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://kolaente.dev/vikunja/frontend/issues"
|
||||
},
|
||||
"homepage": "https://vikunja.io/",
|
||||
"funding": "https://opencollective.com/vikunja",
|
||||
"packageManager": "pnpm@8.15.1",
|
||||
"keywords": [
|
||||
"todo",
|
||||
"productivity",
|
||||
"task management",
|
||||
"organisation",
|
||||
"gantt",
|
||||
"kanban"
|
||||
],
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vite",
|
||||
"preview": "vite preview --port 4173",
|
||||
"preview:dev": "vite preview --outDir dist-dev --mode development --port 4173",
|
||||
"serve:dist-dev": "node scripts/serve-dist.js",
|
||||
"serve:dist": "vite preview",
|
||||
"build": "vite build && workbox copyLibraries dist/",
|
||||
"build:modern-only": "BUILD_MODERN_ONLY=true vite build && workbox copyLibraries dist/",
|
||||
"build:dev": "vite build --mode development --outDir dist-dev/",
|
||||
"lint": "eslint 'src/**/*.{js,ts,vue}'",
|
||||
"lint:fix": "pnpm run lint --fix",
|
||||
"test:e2e": "start-server-and-test preview http://127.0.0.1:4173 'cypress run --e2e --browser chrome'",
|
||||
"test:e2e-record": "start-server-and-test preview http://127.0.0.1:4173 'cypress run --e2e --browser chrome --record'",
|
||||
"test:e2e-dev-dev": "start-server-and-test preview:dev http://127.0.0.1:4173 'cypress open --e2e'",
|
||||
"test:e2e-dev": "start-server-and-test preview http://127.0.0.1:4173 'cypress open --e2e'",
|
||||
"test:unit": "vitest --dir ./src",
|
||||
"typecheck": "vue-tsc --noEmit && vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
|
||||
"browserslist:update": "pnpm dlx browserslist@latest --update-db",
|
||||
"fonts:update": "pnpm fonts:download && pnpm fonts:subset",
|
||||
"fonts:download": "./scripts/fonts-download.sh",
|
||||
"fonts:subset": "./scripts/fonts-subset.sh",
|
||||
"story:dev": "histoire dev",
|
||||
"story:build": "histoire build",
|
||||
"story:preview": "histoire preview"
|
||||
"build:dev": "vite build -m development --outDir dist-dev/",
|
||||
"lint": "eslint --ignore-pattern '*.test.*' ./src --ext .vue,.js,.ts",
|
||||
"lint:markup": "vue-tsc --noEmit",
|
||||
"cypress:open": "cypress open",
|
||||
"test:unit": "jest",
|
||||
"test:frontend": "cypress run",
|
||||
"browserslist:update": "npx browserslist@latest --update-db"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "6.5.1",
|
||||
"@fortawesome/free-regular-svg-icons": "6.5.1",
|
||||
"@fortawesome/free-solid-svg-icons": "6.5.1",
|
||||
"@fortawesome/vue-fontawesome": "3.0.6",
|
||||
"@github/hotkey": "3.1.0",
|
||||
"@infectoone/vue-ganttastic": "2.2.0",
|
||||
"@intlify/unplugin-vue-i18n": "2.0.0",
|
||||
"@kyvg/vue3-notification": "3.1.4",
|
||||
"@sentry/tracing": "7.100.1",
|
||||
"@sentry/vue": "7.100.1",
|
||||
"@tiptap/core": "2.2.1",
|
||||
"@tiptap/extension-blockquote": "2.2.1",
|
||||
"@tiptap/extension-bold": "2.2.1",
|
||||
"@tiptap/extension-bullet-list": "2.2.1",
|
||||
"@tiptap/extension-code": "2.2.1",
|
||||
"@tiptap/extension-code-block-lowlight": "2.2.1",
|
||||
"@tiptap/extension-document": "2.2.1",
|
||||
"@tiptap/extension-dropcursor": "2.2.1",
|
||||
"@tiptap/extension-gapcursor": "2.2.1",
|
||||
"@tiptap/extension-hard-break": "2.2.1",
|
||||
"@tiptap/extension-heading": "2.2.1",
|
||||
"@tiptap/extension-history": "2.2.1",
|
||||
"@tiptap/extension-horizontal-rule": "2.2.1",
|
||||
"@tiptap/extension-image": "2.2.1",
|
||||
"@tiptap/extension-italic": "2.2.1",
|
||||
"@tiptap/extension-link": "2.2.1",
|
||||
"@tiptap/extension-list-item": "2.2.1",
|
||||
"@tiptap/extension-ordered-list": "2.2.1",
|
||||
"@tiptap/extension-paragraph": "2.2.1",
|
||||
"@tiptap/extension-placeholder": "2.2.1",
|
||||
"@tiptap/extension-strike": "2.2.1",
|
||||
"@tiptap/extension-table": "2.2.1",
|
||||
"@tiptap/extension-table-cell": "2.2.1",
|
||||
"@tiptap/extension-table-header": "2.2.1",
|
||||
"@tiptap/extension-table-row": "2.2.1",
|
||||
"@tiptap/extension-task-item": "2.2.1",
|
||||
"@tiptap/extension-task-list": "2.2.1",
|
||||
"@tiptap/extension-text": "2.2.1",
|
||||
"@tiptap/extension-typography": "2.2.1",
|
||||
"@tiptap/extension-underline": "2.2.1",
|
||||
"@tiptap/pm": "2.2.1",
|
||||
"@tiptap/suggestion": "2.2.1",
|
||||
"@tiptap/vue-3": "2.2.1",
|
||||
"@types/is-touch-device": "1.0.2",
|
||||
"@types/lodash.clonedeep": "4.5.9",
|
||||
"@vueuse/core": "10.7.2",
|
||||
"@vueuse/router": "10.7.2",
|
||||
"axios": "1.6.7",
|
||||
"blurhash": "2.0.5",
|
||||
"bulma-css-variables": "0.9.33",
|
||||
"@kyvg/vue3-notification": "2.3.4",
|
||||
"@sentry/tracing": "6.14.1",
|
||||
"@sentry/vue": "6.14.1",
|
||||
"@vue/compat": "3.2.21",
|
||||
"bulma": "0.9.3",
|
||||
"camel-case": "4.1.2",
|
||||
"date-fns": "3.3.1",
|
||||
"dayjs": "1.11.10",
|
||||
"dompurify": "3.0.8",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"flatpickr": "4.6.13",
|
||||
"flexsearch": "0.7.31",
|
||||
"floating-vue": "5.2.2",
|
||||
"codemirror": "5.63.3",
|
||||
"copy-to-clipboard": "3.3.1",
|
||||
"date-fns": "2.25.0",
|
||||
"dompurify": "2.3.3",
|
||||
"easymde": "2.15.0",
|
||||
"flatpickr": "4.6.9",
|
||||
"highlight.js": "11.3.1",
|
||||
"is-touch-device": "1.0.1",
|
||||
"klona": "2.0.6",
|
||||
"lodash.clonedeep": "4.5.0",
|
||||
"lodash.debounce": "4.0.8",
|
||||
"lowlight": "2.9.0",
|
||||
"pinia": "2.1.7",
|
||||
"marked": "4.0.0",
|
||||
"register-service-worker": "1.7.2",
|
||||
"snake-case": "3.0.4",
|
||||
"sortablejs": "1.15.2",
|
||||
"tippy.js": "6.3.7",
|
||||
"ufo": "1.4.0",
|
||||
"vue": "3.4.15",
|
||||
"vue-advanced-cropper": "2.8.8",
|
||||
"vue-flatpickr-component": "11.0.3",
|
||||
"vue-i18n": "9.9.1",
|
||||
"vue-router": "4.2.5",
|
||||
"workbox-precaching": "7.0.0",
|
||||
"zhyswan-vuedraggable": "4.1.3"
|
||||
"ufo": "0.7.9",
|
||||
"vue": "3.2.21",
|
||||
"vue-advanced-cropper": "2.6.3",
|
||||
"vue-drag-resize": "2.0.3",
|
||||
"vue-flatpickr-component": "9.0.5",
|
||||
"vue-i18n": "9.2.0-beta.17",
|
||||
"vue-router": "4.0.12",
|
||||
"vuedraggable": "4.1.0",
|
||||
"vuex": "4.0.2",
|
||||
"workbox-precaching": "6.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@4tw/cypress-drag-drop": "2.2.5",
|
||||
"@cypress/vite-dev-server": "5.0.7",
|
||||
"@cypress/vue": "6.0.0",
|
||||
"@faker-js/faker": "8.4.0",
|
||||
"@histoire/plugin-screenshot": "0.17.8",
|
||||
"@histoire/plugin-vue": "0.17.9",
|
||||
"@rushstack/eslint-patch": "1.7.2",
|
||||
"@tsconfig/node18": "18.2.2",
|
||||
"@types/codemirror": "5.60.15",
|
||||
"@types/dompurify": "3.0.5",
|
||||
"@types/flexsearch": "0.7.6",
|
||||
"@types/is-touch-device": "1.0.2",
|
||||
"@types/lodash.debounce": "4.0.9",
|
||||
"@types/marked": "5.0.2",
|
||||
"@types/node": "20.11.10",
|
||||
"@types/postcss-preset-env": "7.7.0",
|
||||
"@types/sortablejs": "1.15.7",
|
||||
"@typescript-eslint/eslint-plugin": "6.20.0",
|
||||
"@typescript-eslint/parser": "6.20.0",
|
||||
"@vitejs/plugin-legacy": "5.3.0",
|
||||
"@vitejs/plugin-vue": "5.0.3",
|
||||
"@vue/eslint-config-typescript": "12.0.0",
|
||||
"@vue/test-utils": "2.4.4",
|
||||
"@vue/tsconfig": "0.5.1",
|
||||
"autoprefixer": "10.4.17",
|
||||
"browserslist": "4.22.3",
|
||||
"caniuse-lite": "1.0.30001581",
|
||||
"css-has-pseudo": "6.0.1",
|
||||
"csstype": "3.1.3",
|
||||
"cypress": "13.6.3",
|
||||
"esbuild": "0.20.0",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-plugin-vue": "9.20.1",
|
||||
"happy-dom": "13.3.5",
|
||||
"histoire": "0.17.9",
|
||||
"postcss": "8.4.33",
|
||||
"postcss-easing-gradients": "3.0.1",
|
||||
"postcss-easings": "4.0.0",
|
||||
"postcss-focus-within": "8.0.1",
|
||||
"postcss-preset-env": "9.3.0",
|
||||
"rollup": "4.9.6",
|
||||
"rollup-plugin-visualizer": "5.12.0",
|
||||
"sass": "1.70.0",
|
||||
"start-server-and-test": "2.0.3",
|
||||
"typescript": "5.3.3",
|
||||
"vite": "5.0.12",
|
||||
"vite-plugin-inject-preload": "1.3.3",
|
||||
"vite-plugin-pwa": "0.17.5",
|
||||
"vite-plugin-sentry": "1.3.0",
|
||||
"vite-svg-loader": "5.1.0",
|
||||
"vitest": "1.2.2",
|
||||
"vue-tsc": "1.8.27",
|
||||
"wait-on": "7.2.0",
|
||||
"workbox-cli": "7.0.0"
|
||||
"@4tw/cypress-drag-drop": "2.0.0",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
||||
"@fortawesome/vue-fontawesome": "3.0.0-5",
|
||||
"@types/jest": "27.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "5.3.1",
|
||||
"@typescript-eslint/parser": "5.3.1",
|
||||
"@vitejs/plugin-legacy": "1.6.2",
|
||||
"@vitejs/plugin-vue": "1.9.4",
|
||||
"@vue/eslint-config-typescript": "9.0.1",
|
||||
"autoprefixer": "10.4.0",
|
||||
"axios": "0.24.0",
|
||||
"browserslist": "4.17.6",
|
||||
"cypress": "8.7.0",
|
||||
"cypress-file-upload": "5.0.8",
|
||||
"esbuild": "0.13.12",
|
||||
"eslint": "8.2.0",
|
||||
"eslint-plugin-vue": "8.0.3",
|
||||
"express": "4.17.1",
|
||||
"faker": "5.5.3",
|
||||
"jest": "27.3.1",
|
||||
"postcss": "8.3.11",
|
||||
"rollup": "2.59.0",
|
||||
"rollup-plugin-visualizer": "5.5.2",
|
||||
"sass": "1.43.4",
|
||||
"ts-jest": "27.0.7",
|
||||
"typescript": "4.4.4",
|
||||
"vite": "2.6.13",
|
||||
"vite-plugin-pwa": "0.11.3",
|
||||
"vue-tsc": "0.29.2",
|
||||
"wait-on": "6.0.0",
|
||||
"workbox-cli": "6.3.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"flexsearch@0.7.31": "patches/flexsearch@0.7.31.patch"
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:vue/vue3-essential",
|
||||
"@vue/typescript"
|
||||
],
|
||||
"rules": {
|
||||
"vue/html-quotes": [
|
||||
"error",
|
||||
"double"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
],
|
||||
"comma-dangle": [
|
||||
"error",
|
||||
"always-multiline"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"vue/multi-word-component-names": 0
|
||||
},
|
||||
"parser": "vue-eslint-parser",
|
||||
"parserOptions": {
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"ecmaVersion": 2021
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"*.test.*",
|
||||
"cypress/*"
|
||||
]
|
||||
},
|
||||
"postcss": {
|
||||
"plugins": {
|
||||
"autoprefixer": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"jest": {
|
||||
"testPathIgnorePatterns": [
|
||||
"cypress"
|
||||
],
|
||||
"testEnvironment": "jsdom",
|
||||
"preset": "ts-jest",
|
||||
"roots": [
|
||||
"<rootDir>/src"
|
||||
],
|
||||
"transform": {
|
||||
"^.+\\.(js|tsx?)$": "ts-jest"
|
||||
},
|
||||
"moduleFileExtensions": [
|
||||
"ts",
|
||||
"js",
|
||||
"json"
|
||||
]
|
||||
},
|
||||
"license": "AGPL-3.0-or-later"
|
||||
}
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
diff --git a/index.d.ts b/index.d.ts
|
||||
deleted file mode 100644
|
||||
index 9f39f41073864b83968bdaa242ac4e3c3149685a..0000000000000000000000000000000000000000
|
||||
diff --git a/package.json b/package.json
|
||||
index 8968f5bf8010ff194240591c8b83299f7328e79d..6d84b6f590a841b129ed8b3860cb786df5a185c0 100644
|
||||
--- a/package.json
|
||||
+++ b/package.json
|
||||
@@ -22,8 +22,6 @@
|
||||
},
|
||||
"main": "dist/flexsearch.bundle.js",
|
||||
"browser": "dist/flexsearch.bundle.js",
|
||||
- "module": "dist/module/index.js",
|
||||
- "types": "./index.d.ts",
|
||||
"preferGlobal": false,
|
||||
"repository": {
|
||||
"type": "git",
|
11789
pnpm-lock.yaml
11789
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue