forked from vikunja/frontend
Compare commits
2 Commits
main
...
renovate/w
Author | SHA1 | Date | |
---|---|---|---|
26cdf94740 | |||
43d9a67725 |
434
.drone.yml
434
.drone.yml
|
@ -1,6 +1,5 @@
|
|||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: build
|
||||
|
||||
trigger:
|
||||
|
@ -23,100 +22,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:18-alpine
|
||||
pull: always
|
||||
image: node:18
|
||||
pull: true
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
CYPRESS_CACHE_FOLDER: .cache/cypress
|
||||
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:18-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:18-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:18-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:18-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
|
||||
|
@ -128,15 +63,71 @@ 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: lint
|
||||
image: node:18
|
||||
pull: true
|
||||
environment:
|
||||
YARN_CACHE_FOLDER: .cache/yarn/
|
||||
CYPRESS_CACHE_FOLDER: .cache/cypress/
|
||||
commands:
|
||||
- yarn run lint
|
||||
depends_on:
|
||||
- dependencies
|
||||
|
||||
- name: build-prod
|
||||
image: node:18
|
||||
pull: true
|
||||
environment:
|
||||
YARN_CACHE_FOLDER: .cache/yarn/
|
||||
commands:
|
||||
- yarn build
|
||||
depends_on:
|
||||
- dependencies
|
||||
|
||||
- name: test-unit
|
||||
image: node:18
|
||||
pull: true
|
||||
commands:
|
||||
- yarn test:unit
|
||||
depends_on:
|
||||
- dependencies
|
||||
|
||||
- name: typecheck
|
||||
failure: ignore
|
||||
image: node:18
|
||||
pull: true
|
||||
commands:
|
||||
- yarn typecheck
|
||||
depends_on:
|
||||
- dependencies
|
||||
|
||||
- name: test-frontend
|
||||
image: cypress/browsers:node16.5.0-chrome94-ff93
|
||||
pull: true
|
||||
environment:
|
||||
CYPRESS_API_URL: http://api:3456/api/v1
|
||||
CYPRESS_TEST_SECRET: averyLongSecretToSe33dtheDB
|
||||
YARN_CACHE_FOLDER: .cache/yarn/
|
||||
CYPRESS_CACHE_FOLDER: .cache/cypress/
|
||||
CYPRESS_DEFAULT_COMMAND_TIMEOUT: 60000
|
||||
CYPRESS_RECORD_KEY:
|
||||
from_secret: cypress_project_key
|
||||
commands:
|
||||
- sed -i 's/localhost/api/g' dist/index.html
|
||||
- yarn serve:dist & npx wait-on http://localhost:4173
|
||||
- yarn test:frontend --browser chrome --record
|
||||
depends_on:
|
||||
- build-prod
|
||||
|
||||
- name: deploy-preview
|
||||
image: node:18-alpine
|
||||
pull: always
|
||||
image: node:18
|
||||
pull: true
|
||||
environment:
|
||||
NETLIFY_AUTH_TOKEN:
|
||||
from_secret: netlify_auth_token
|
||||
|
@ -147,12 +138,9 @@ steps:
|
|||
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
|
||||
- sed -i 's|localhost:3456|try.vikunja.io|g' dist-preview/index.html
|
||||
- shasum -a 384 -c ./scripts/deploy-preview-netlify.js.sha384
|
||||
- node ./scripts/deploy-preview-netlify.js
|
||||
depends_on:
|
||||
- build-prod
|
||||
when:
|
||||
|
@ -162,7 +150,6 @@ steps:
|
|||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: release-latest
|
||||
|
||||
depends_on:
|
||||
|
@ -182,7 +169,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
|
||||
|
@ -194,29 +181,28 @@ 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:18-alpine
|
||||
pull: always
|
||||
image: node:18
|
||||
pull: true
|
||||
group: build-static
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
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:
|
||||
- cd dist
|
||||
- zip -r ../vikunja-frontend-unstable.zip *
|
||||
|
@ -225,7 +211,7 @@ steps:
|
|||
|
||||
- name: release
|
||||
image: plugins/s3
|
||||
pull: always
|
||||
pull: true
|
||||
settings:
|
||||
bucket: vikunja-releases
|
||||
access_key:
|
||||
|
@ -241,7 +227,6 @@ steps:
|
|||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: release-version
|
||||
|
||||
depends_on:
|
||||
|
@ -259,7 +244,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
|
||||
|
@ -271,29 +256,28 @@ 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:18-alpine
|
||||
pull: always
|
||||
image: node:18
|
||||
pull: true
|
||||
group: build-static
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
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:
|
||||
- cd dist
|
||||
- zip -r ../vikunja-frontend-${DRONE_TAG##v}.zip *
|
||||
|
@ -302,7 +286,7 @@ steps:
|
|||
|
||||
- name: release
|
||||
image: plugins/s3
|
||||
pull: always
|
||||
pull: true
|
||||
settings:
|
||||
bucket: vikunja-releases
|
||||
access_key:
|
||||
|
@ -318,7 +302,6 @@ steps:
|
|||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: trigger-desktop-update
|
||||
|
||||
trigger:
|
||||
|
@ -343,7 +326,111 @@ steps:
|
|||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: docker-release
|
||||
name: docker-arm-release
|
||||
|
||||
depends_on:
|
||||
- release-latest
|
||||
- release-version
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: arm64
|
||||
|
||||
trigger:
|
||||
ref:
|
||||
- refs/heads/main
|
||||
- "refs/tags/**"
|
||||
event:
|
||||
exclude:
|
||||
- cron
|
||||
|
||||
steps:
|
||||
- name: docker-unstable
|
||||
image: plugins/docker:linux-arm
|
||||
pull: true
|
||||
settings:
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
repo: vikunja/frontend
|
||||
tags: unstable-linux-arm
|
||||
build_args:
|
||||
- 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
|
||||
|
@ -358,67 +445,101 @@ 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-amd64
|
||||
pull: true
|
||||
settings:
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
repo: vikunja/frontend
|
||||
tags: unstable
|
||||
tags: unstable-linux-amd64
|
||||
build_args:
|
||||
- USE_RELEASE=true
|
||||
- RELEASE_VERSION=unstable
|
||||
platforms:
|
||||
- linux/386
|
||||
- linux/amd64
|
||||
- linux/arm/v6
|
||||
- linux/arm/v7
|
||||
- linux/arm64/v8
|
||||
depends_on: [ fetch-tags ]
|
||||
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=true
|
||||
- RELEASE_VERSION=${DRONE_TAG##v}
|
||||
platforms:
|
||||
- linux/386
|
||||
- linux/amd64
|
||||
- linux/arm/v6
|
||||
- linux/arm/v7
|
||||
- linux/arm64/v8
|
||||
depends_on: [ generate-tags ]
|
||||
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/**"
|
||||
|
@ -441,7 +562,9 @@ depends_on:
|
|||
- release-version
|
||||
- release-latest
|
||||
- trigger-desktop-update
|
||||
- docker-release
|
||||
- docker-arm-release
|
||||
- docker-amd64-release
|
||||
- docker-manifest
|
||||
|
||||
steps:
|
||||
- name: notify
|
||||
|
@ -462,6 +585,9 @@ kind: pipeline
|
|||
type: docker
|
||||
name: update-translations
|
||||
|
||||
depends_on:
|
||||
- build
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
- main
|
||||
|
@ -521,6 +647,6 @@ steps:
|
|||
from_secret: crowdin_key
|
||||
---
|
||||
kind: signature
|
||||
hmac: 971875b90c7bb1649d1b00d022d0b594ba9b68f927bf8f0dbe840190816d676b
|
||||
hmac: 997e1badebe484ac29557c4af356e63db4d3d57f3d32e92d482f117f8cec64da
|
||||
|
||||
...
|
||||
|
|
|
@ -1,13 +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
|
||||
# VITE_WORKBOX_DEBUG=false
|
||||
# SENTRY_AUTH_TOKEN=YOUR_TOKEN
|
||||
# SENTRY_ORG=vikunja
|
||||
# SENTRY_PROJECT=frontend-oss
|
||||
# VIKUNJA_FRONTEND_BASE=/custom-subpath
|
|
@ -1,18 +1,15 @@
|
|||
/* eslint-env node */
|
||||
require("@rushstack/eslint-patch/modern-module-resolution")
|
||||
|
||||
module.exports = {
|
||||
'root': true,
|
||||
'env': {
|
||||
'browser': true,
|
||||
'es2022': true,
|
||||
'es2021': true,
|
||||
'node': true,
|
||||
'vue/setup-compiler-macros': true,
|
||||
},
|
||||
'extends': [
|
||||
'eslint:recommended',
|
||||
'plugin:vue/vue3-essential',
|
||||
'@vue/eslint-config-typescript/recommended',
|
||||
'@vue/typescript',
|
||||
],
|
||||
'rules': {
|
||||
'vue/html-quotes': [
|
||||
|
@ -31,22 +28,18 @@ module.exports = {
|
|||
'error',
|
||||
'never',
|
||||
],
|
||||
'vue/script-setup-uses-vars': 'error',
|
||||
|
||||
// see https://segmentfault.com/q/1010000040813116/a-1020000041134455 (original in chinese)
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': ['error', { vars: 'all', args: 'after-used', ignoreRestSiblings: true }],
|
||||
|
||||
'vue/multi-word-component-names': 0,
|
||||
// disabled until we have support for reactivityTransform
|
||||
// See https://github.com/vuejs/eslint-plugin-vue/issues/1948
|
||||
// see also setting in `vite.config`
|
||||
'vue/no-setup-props-destructure': 0,
|
||||
},
|
||||
'parser': 'vue-eslint-parser',
|
||||
'parserOptions': {
|
||||
'parser': '@typescript-eslint/parser',
|
||||
'ecmaVersion': 2022,
|
||||
'sourceType': 'module',
|
||||
},
|
||||
'ignorePatterns': [
|
||||
'*.test.*',
|
3
.github/FUNDING.yml
vendored
3
.github/FUNDING.yml
vendored
|
@ -1,3 +1,2 @@
|
|||
github: kolaente
|
||||
open_collective: vikunja
|
||||
custom: ["https://vikunja.cloud", "https://www.buymeacoffee.com/kolaente"]
|
||||
custom: https://www.buymeacoffee.com/kolaente
|
||||
|
|
59
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
59
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
|
@ -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
|
17
.github/ISSUE_TEMPLATE/config.yml
vendored
17
.github/ISSUE_TEMPLATE/config.yml
vendored
|
@ -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.
|
32
.gitignore
vendored
32
.gitignore
vendored
|
@ -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,9 @@ cypress/videos
|
|||
*.sw*
|
||||
!rollup.sw.js
|
||||
|
||||
# Test files
|
||||
cypress/screenshots
|
||||
cypress/videos
|
||||
|
||||
# Local Netlify folder
|
||||
.netlify
|
||||
|
||||
# histoire
|
||||
.histoire
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"eslint.packageManager": "pnpm",
|
||||
"eslint.packageManager": "yarn",
|
||||
"editor.formatOnSave": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": true
|
||||
|
|
3115
CHANGELOG.md
3115
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
83
Dockerfile
83
Dockerfile
|
@ -1,70 +1,39 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
# ┬─┐┬ ┐o┬ ┬─┐
|
||||
# │─││ │││ │ │
|
||||
# ┘─┘┘─┘┘┘─┘┘─┘
|
||||
|
||||
FROM --platform=$BUILDPLATFORM node:18-alpine AS builder
|
||||
# Stage 1: Build application
|
||||
FROM node:18 AS compile-image
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
ARG USE_RELEASE=false
|
||||
ARG RELEASE_VERSION=main
|
||||
ENV PNPM_CACHE_FOLDER .cache/pnpm/
|
||||
|
||||
COPY package.json ./
|
||||
COPY pnpm-lock.yaml ./
|
||||
|
||||
RUN if [ "$USE_RELEASE" != true ]; then \
|
||||
# https://pnpm.io/installation#using-corepack
|
||||
corepack enable && \
|
||||
pnpm install; \
|
||||
fi
|
||||
|
||||
ENV YARN_CACHE_FOLDER .cache/yarn/
|
||||
COPY . ./
|
||||
|
||||
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 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
|
||||
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
|
||||
pnpm run build; \
|
||||
fi
|
||||
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
|
||||
|
||||
# ┌┐┐┌─┐o┌┐┐┐ │
|
||||
# ││││ ┬││││┌┼┘
|
||||
# ┘└┘┘─┘┘┘└┘┘ └
|
||||
# Stage 2: copy
|
||||
FROM nginx
|
||||
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
COPY run.sh /run.sh
|
||||
|
||||
# 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
|
||||
|
||||
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
|
||||
# unprivileged user
|
||||
USER nginx
|
||||
CMD "/run.sh"
|
||||
|
|
19
README.md
19
README.md
|
@ -4,7 +4,7 @@
|
|||
|
||||
[![Build Status](https://drone.kolaente.de/api/badges/vikunja/frontend/status.svg)](https://drone.kolaente.de/vikunja/frontend)
|
||||
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](LICENSE)
|
||||
[![Download](https://img.shields.io/badge/download-v0.20.3-brightgreen.svg)](https://dl.vikunja.io)
|
||||
[![Download](https://img.shields.io/badge/download-v0.18.2-brightgreen.svg)](https://dl.vikunja.io)
|
||||
[![Translation](https://badges.crowdin.net/vikunja/localized.svg)](https://crowdin.com/project/vikunja)
|
||||
|
||||
This is the web frontend for Vikunja, written in Vue.js.
|
||||
|
@ -18,36 +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 Refer [to multi-platform documentation](https://docs.docker.com/build/building/multi-platform/) in order to build for the different platform.
|
||||
|
||||
## 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
|
||||
]
|
||||
|
|
@ -11,10 +11,8 @@ export default defineConfig({
|
|||
},
|
||||
projectId: '181c7x',
|
||||
e2e: {
|
||||
specPattern: 'cypress/e2e/**/*.{cy,spec}.{js,jsx,ts,tsx}',
|
||||
baseUrl: 'http://127.0.0.1:4173',
|
||||
experimentalRunAllSpecs: true,
|
||||
// testIsolation: false,
|
||||
baseUrl: 'http://localhost:4173',
|
||||
specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}',
|
||||
},
|
||||
component: {
|
||||
devServer: {
|
||||
|
|
|
@ -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,10 +1,9 @@
|
|||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||
|
||||
import {ListFactory} from '../../factories/list'
|
||||
|
||||
import '../../support/authenticateUser'
|
||||
import {prepareLists} from './prepareLists'
|
||||
|
||||
describe('List History', () => {
|
||||
createFakeUserAndLogin()
|
||||
prepareLists()
|
||||
|
||||
it('should show a list history on the home page', () => {
|
||||
|
@ -46,7 +45,7 @@ describe('List History', () => {
|
|||
|
||||
cy.get('body')
|
||||
.should('contain', 'Last viewed')
|
||||
cy.get('[data-cy="listCardGrid"]')
|
||||
cy.get('.list-cards-wrapper-2-rows')
|
||||
.should('not.contain', lists[0].title)
|
||||
.should('contain', lists[1].title)
|
||||
.should('contain', lists[2].title)
|
||||
|
|
|
@ -1,33 +1,29 @@
|
|||
import {formatISO, format} from 'date-fns'
|
||||
|
||||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||
|
||||
import {TaskFactory} from '../../factories/task'
|
||||
import {prepareLists} from './prepareLists'
|
||||
|
||||
import '../../support/authenticateUser'
|
||||
|
||||
describe('List View Gantt', () => {
|
||||
createFakeUserAndLogin()
|
||||
prepareLists()
|
||||
|
||||
it('Hides tasks with no dates', () => {
|
||||
const tasks = TaskFactory.create(1)
|
||||
cy.visit('/lists/1/gantt')
|
||||
|
||||
cy.get('.g-gantt-rows-container')
|
||||
cy.get('.gantt-chart .tasks')
|
||||
.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)
|
||||
const now = new Date()
|
||||
const nextMonth = now
|
||||
nextMonth.setDate(1)
|
||||
nextMonth.setMonth(9)
|
||||
nextMonth.setMonth(now.getMonth() + 1)
|
||||
|
||||
cy.visit('/lists/1/gantt')
|
||||
|
||||
cy.get('.g-timeunits-container')
|
||||
cy.get('.gantt-chart .months')
|
||||
.should('contain', format(now, 'MMMM'))
|
||||
.should('contain', format(nextMonth, 'MMMM'))
|
||||
})
|
||||
|
@ -35,18 +31,19 @@ describe('List View Gantt', () => {
|
|||
it('Shows tasks with dates', () => {
|
||||
const now = new Date()
|
||||
const tasks = TaskFactory.create(1, {
|
||||
start_date: now.toISOString(),
|
||||
end_date: new Date(new Date(now).setDate(now.getDate() + 4)).toISOString(),
|
||||
start_date: formatISO(now),
|
||||
end_date: formatISO(now.setDate(now.getDate() + 4))
|
||||
})
|
||||
cy.visit('/lists/1/gantt')
|
||||
|
||||
cy.get('.g-gantt-rows-container')
|
||||
cy.get('.gantt-chart .tasks')
|
||||
.should('not.be.empty')
|
||||
cy.get('.gantt-chart .tasks')
|
||||
.should('contain', tasks[0].title)
|
||||
})
|
||||
|
||||
it('Shows tasks with no dates after enabling them', () => {
|
||||
const tasks = TaskFactory.create(1, {
|
||||
TaskFactory.create(1, {
|
||||
start_date: null,
|
||||
end_date: null,
|
||||
})
|
||||
|
@ -56,71 +53,24 @@ describe('List View Gantt', () => {
|
|||
.contains('Show tasks which don\'t have dates set')
|
||||
.click()
|
||||
|
||||
cy.get('.g-gantt-rows-container')
|
||||
cy.get('.gantt-chart .tasks')
|
||||
.should('not.be.empty')
|
||||
.should('contain', tasks[0].title)
|
||||
cy.get('.gantt-chart .tasks .task.nodate')
|
||||
.should('exist')
|
||||
})
|
||||
|
||||
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(),
|
||||
start_date: formatISO(now),
|
||||
end_date: formatISO(now.setDate(now.getDate() + 4))
|
||||
})
|
||||
cy.visit('/lists/1/gantt')
|
||||
|
||||
cy.get('.g-gantt-rows-container .g-gantt-row .g-gantt-row-bars-container div .g-gantt-bar')
|
||||
cy.get('.gantt-chart .tasks .task')
|
||||
.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('/lists/1/gantt')
|
||||
|
||||
cy.get('.list-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('/lists/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('.list-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('/lists/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,15 +1,14 @@
|
|||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||
|
||||
import {BucketFactory} from '../../factories/bucket'
|
||||
import {ListFactory} from '../../factories/list'
|
||||
import {TaskFactory} from '../../factories/task'
|
||||
import {prepareLists} from './prepareLists'
|
||||
|
||||
import '../../support/authenticateUser'
|
||||
|
||||
describe('List View Kanban', () => {
|
||||
createFakeUserAndLogin()
|
||||
let buckets
|
||||
prepareLists()
|
||||
|
||||
let buckets
|
||||
beforeEach(() => {
|
||||
buckets = BucketFactory.create(2)
|
||||
})
|
||||
|
@ -39,7 +38,7 @@ describe('List View Kanban', () => {
|
|||
})
|
||||
cy.visit('/lists/1/kanban')
|
||||
|
||||
cy.get('.kanban .bucket')
|
||||
cy.getSettled('.kanban .bucket')
|
||||
.contains(buckets[0].title)
|
||||
.get('.bucket-footer .button')
|
||||
.contains('Add another task')
|
||||
|
@ -71,7 +70,7 @@ describe('List View Kanban', () => {
|
|||
it('Can set a bucket limit', () => {
|
||||
cy.visit('/lists/1/kanban')
|
||||
|
||||
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger')
|
||||
cy.getSettled('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger')
|
||||
.first()
|
||||
.click()
|
||||
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item')
|
||||
|
@ -92,7 +91,7 @@ describe('List View Kanban', () => {
|
|||
it('Can rename a bucket', () => {
|
||||
cy.visit('/lists/1/kanban')
|
||||
|
||||
cy.get('.kanban .bucket .bucket-header .title')
|
||||
cy.getSettled('.kanban .bucket .bucket-header .title')
|
||||
.first()
|
||||
.type('{selectall}New Bucket Title{enter}')
|
||||
cy.get('.kanban .bucket .bucket-header .title')
|
||||
|
@ -103,7 +102,7 @@ describe('List View Kanban', () => {
|
|||
it('Can delete a bucket', () => {
|
||||
cy.visit('/lists/1/kanban')
|
||||
|
||||
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger')
|
||||
cy.getSettled('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger')
|
||||
.first()
|
||||
.click()
|
||||
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item')
|
||||
|
@ -130,7 +129,7 @@ describe('List View Kanban', () => {
|
|||
})
|
||||
cy.visit('/lists/1/kanban')
|
||||
|
||||
cy.get('.kanban .bucket .tasks .task')
|
||||
cy.getSettled('.kanban .bucket .tasks .task')
|
||||
.contains(tasks[0].title)
|
||||
.first()
|
||||
.drag('.kanban .bucket:nth-child(2) .tasks')
|
||||
|
@ -149,7 +148,7 @@ describe('List View Kanban', () => {
|
|||
})
|
||||
cy.visit('/lists/1/kanban')
|
||||
|
||||
cy.get('.kanban .bucket .tasks .task')
|
||||
cy.getSettled('.kanban .bucket .tasks .task')
|
||||
.contains(tasks[0].title)
|
||||
.should('be.visible')
|
||||
.click()
|
||||
|
@ -171,7 +170,7 @@ describe('List View Kanban', () => {
|
|||
const task = tasks[0]
|
||||
cy.visit('/lists/1/kanban')
|
||||
|
||||
cy.get('.kanban .bucket .tasks .task')
|
||||
cy.getSettled('.kanban .bucket .tasks .task')
|
||||
.contains(task.title)
|
||||
.should('be.visible')
|
||||
.click()
|
||||
|
@ -194,48 +193,4 @@ describe('List View Kanban', () => {
|
|||
cy.get('.kanban .bucket')
|
||||
.should('not.contain', task.title)
|
||||
})
|
||||
|
||||
it('Shows a button to filter the kanban board', () => {
|
||||
const data = TaskFactory.create(10, {
|
||||
list_id: 1,
|
||||
bucket_id: 1,
|
||||
})
|
||||
cy.visit('/lists/1/kanban')
|
||||
|
||||
cy.get('.list-kanban .filter-container .base-button')
|
||||
.should('exist')
|
||||
})
|
||||
|
||||
it('Should remove a task from the board when deleting it', () => {
|
||||
const lists = ListFactory.create(1)
|
||||
const buckets = BucketFactory.create(2, {
|
||||
list_id: lists[0].id,
|
||||
})
|
||||
const tasks = TaskFactory.create(5, {
|
||||
list_id: 1,
|
||||
bucket_id: buckets[0].id,
|
||||
})
|
||||
const task = tasks[0]
|
||||
cy.visit('/lists/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)
|
||||
})
|
||||
})
|
|
@ -1,22 +1,21 @@
|
|||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||
|
||||
import {UserListFactory} from '../../factories/users_list'
|
||||
import {TaskFactory} from '../../factories/task'
|
||||
import {UserFactory} from '../../factories/user'
|
||||
import {ListFactory} from '../../factories/list'
|
||||
import {prepareLists} from './prepareLists'
|
||||
|
||||
import '../../support/authenticateUser'
|
||||
|
||||
describe('List View List', () => {
|
||||
createFakeUserAndLogin()
|
||||
prepareLists()
|
||||
|
||||
it('Should be an empty list', () => {
|
||||
cy.visit('/lists/1')
|
||||
cy.url()
|
||||
.should('contain', '/lists/1/list')
|
||||
cy.get('.list-title')
|
||||
cy.get('.list-title h1')
|
||||
.should('contain', 'First List')
|
||||
cy.get('.list-title-dropdown')
|
||||
cy.get('.list-title .dropdown')
|
||||
.should('exist')
|
||||
cy.get('p')
|
||||
.contains('This list is currently empty.')
|
||||
|
@ -62,7 +61,7 @@ describe('List View List', () => {
|
|||
})
|
||||
cy.visit(`/lists/${lists[1].id}/`)
|
||||
|
||||
cy.get('.list-title-wrapper .icon')
|
||||
cy.get('.list-title .icon')
|
||||
.should('not.exist')
|
||||
cy.get('input.input[placeholder="Add a new task..."')
|
||||
.should('not.exist')
|
||||
|
@ -79,7 +78,7 @@ describe('List View List', () => {
|
|||
|
||||
cy.get('.menu-list li .list-menu-link .color-bubble')
|
||||
.should('have.css', 'background-color', 'rgb(0, 219, 96)')
|
||||
cy.get('.tasks .color-bubble')
|
||||
cy.get('.tasks-container .tasks .color-bubble')
|
||||
.should('not.exist')
|
||||
})
|
||||
|
||||
|
@ -91,9 +90,9 @@ describe('List View List', () => {
|
|||
})
|
||||
cy.visit('/lists/1/list')
|
||||
|
||||
cy.get('.tasks')
|
||||
cy.get('.tasks-container .tasks')
|
||||
.should('contain', tasks[1].title)
|
||||
cy.get('.tasks')
|
||||
cy.get('.tasks-container .tasks')
|
||||
.should('not.contain', tasks[99].title)
|
||||
|
||||
cy.get('.card-content .pagination .pagination-link')
|
||||
|
@ -102,9 +101,9 @@ describe('List View List', () => {
|
|||
|
||||
cy.url()
|
||||
.should('contain', '?page=2')
|
||||
cy.get('.tasks')
|
||||
cy.get('.tasks-container .tasks')
|
||||
.should('contain', tasks[99].title)
|
||||
cy.get('.tasks')
|
||||
cy.get('.tasks-container .tasks')
|
||||
.should('not.contain', tasks[1].title)
|
||||
})
|
||||
})
|
|
@ -1,10 +1,8 @@
|
|||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||
|
||||
import {TaskFactory} from '../../factories/task'
|
||||
|
||||
describe('List View Table', () => {
|
||||
createFakeUserAndLogin()
|
||||
import '../../support/authenticateUser'
|
||||
|
||||
describe('List View Table', () => {
|
||||
it('Should show a table with tasks', () => {
|
||||
const tasks = TaskFactory.create(1)
|
||||
cy.visit('/lists/1/table')
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||
|
||||
import {TaskFactory} from '../../factories/task'
|
||||
import {prepareLists} from './prepareLists'
|
||||
|
||||
describe('Lists', () => {
|
||||
createFakeUserAndLogin()
|
||||
import '../../support/authenticateUser'
|
||||
|
||||
describe('Lists', () => {
|
||||
let lists
|
||||
prepareLists((newLists) => (lists = newLists))
|
||||
|
||||
|
@ -30,7 +28,7 @@ describe('Lists', () => {
|
|||
.should('contain', 'Success')
|
||||
cy.url()
|
||||
.should('contain', '/lists/')
|
||||
cy.get('.list-title')
|
||||
cy.get('.list-title h1')
|
||||
.should('contain', 'New List')
|
||||
})
|
||||
|
||||
|
@ -51,23 +49,23 @@ describe('Lists', () => {
|
|||
const newListName = 'New list name'
|
||||
|
||||
cy.visit('/lists/1')
|
||||
cy.get('.list-title')
|
||||
cy.get('.list-title h1')
|
||||
.should('contain', 'First List')
|
||||
|
||||
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .menu-list-dropdown-trigger')
|
||||
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .dropdown-trigger')
|
||||
.click()
|
||||
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .dropdown-content')
|
||||
.contains('Edit')
|
||||
.click()
|
||||
cy.get('#title')
|
||||
.type(`{selectall}${newListName}`)
|
||||
cy.get('footer.card-footer .button')
|
||||
cy.get('footer.modal-card-foot .button')
|
||||
.contains('Save')
|
||||
.click()
|
||||
|
||||
cy.get('.global-notification')
|
||||
.should('contain', 'Success')
|
||||
cy.get('.list-title')
|
||||
cy.get('.list-title h1')
|
||||
.should('contain', newListName)
|
||||
.should('not.contain', lists[0].title)
|
||||
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child')
|
||||
|
@ -82,7 +80,7 @@ describe('Lists', () => {
|
|||
it('Should remove a list', () => {
|
||||
cy.visit(`/lists/${lists[0].id}`)
|
||||
|
||||
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .menu-list-dropdown-trigger')
|
||||
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .dropdown-trigger')
|
||||
.click()
|
||||
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .dropdown-content')
|
||||
.contains('Delete')
|
||||
|
@ -104,9 +102,9 @@ describe('Lists', () => {
|
|||
it('Should archive a list', () => {
|
||||
cy.visit(`/lists/${lists[0].id}`)
|
||||
|
||||
cy.get('.list-title-dropdown')
|
||||
cy.get('.list-title .dropdown')
|
||||
.click()
|
||||
cy.get('.list-title-dropdown .dropdown-menu .dropdown-item')
|
||||
cy.get('.list-title .dropdown .dropdown-menu .dropdown-item')
|
||||
.contains('Archive')
|
||||
.click()
|
||||
cy.get('.modal-content')
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||
import {UserFactory} from '../../factories/user'
|
||||
|
||||
import '../../support/authenticateUser'
|
||||
import {ListFactory} from '../../factories/list'
|
||||
import {NamespaceFactory} from '../../factories/namespace'
|
||||
|
||||
describe('Namepaces', () => {
|
||||
createFakeUserAndLogin()
|
||||
|
||||
let namespaces
|
||||
|
||||
beforeEach(() => {
|
||||
UserFactory.create(1)
|
||||
namespaces = NamespaceFactory.create(1)
|
||||
ListFactory.create(1)
|
||||
})
|
||||
|
@ -63,7 +63,7 @@ describe('Namepaces', () => {
|
|||
.should('equal', newNamespaces[0].title) // wait until the namespace data is loaded
|
||||
cy.get('#namespacetext')
|
||||
.type(`{selectall}${newNamespaceName}`)
|
||||
cy.get('footer.card-footer .button')
|
||||
cy.get('footer.modal-card-foot .button')
|
||||
.contains('Save')
|
||||
.click()
|
||||
|
||||
|
|
|
@ -1,19 +1,16 @@
|
|||
import {ListFactory} from '../../factories/list'
|
||||
import {UserFactory} from '../../factories/user'
|
||||
import {NamespaceFactory} from '../../factories/namespace'
|
||||
import {TaskFactory} from '../../factories/task'
|
||||
|
||||
export function createLists() {
|
||||
export function prepareLists(setLists = () => {}) {
|
||||
beforeEach(() => {
|
||||
UserFactory.create(1)
|
||||
NamespaceFactory.create(1)
|
||||
const lists = ListFactory.create(1, {
|
||||
title: 'First List'
|
||||
})
|
||||
TaskFactory.truncate()
|
||||
return lists
|
||||
}
|
||||
|
||||
export function prepareLists(setLists = (...args: any[]) => {}) {
|
||||
beforeEach(() => {
|
||||
const lists = createLists()
|
||||
setLists(lists)
|
||||
TaskFactory.truncate()
|
||||
})
|
||||
}
|
|
@ -1,16 +1,14 @@
|
|||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||
|
||||
import {TaskFactory} from '../../factories/task'
|
||||
import {ListFactory} from '../../factories/list'
|
||||
import {NamespaceFactory} from '../../factories/namespace'
|
||||
import {UserListFactory} from '../../factories/users_list'
|
||||
|
||||
describe('Editor', () => {
|
||||
createFakeUserAndLogin()
|
||||
import '../../support/authenticateUser'
|
||||
|
||||
describe('Editor', () => {
|
||||
beforeEach(() => {
|
||||
NamespaceFactory.create(1)
|
||||
ListFactory.create(1)
|
||||
const lists = ListFactory.create(1)
|
||||
TaskFactory.truncate()
|
||||
UserListFactory.truncate()
|
||||
})
|
||||
|
|
|
@ -1,12 +1,6 @@
|
|||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||
import '../../support/authenticateUser'
|
||||
|
||||
describe('The Menu', () => {
|
||||
createFakeUserAndLogin()
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit('/')
|
||||
})
|
||||
|
||||
it('Is visible by default on desktop', () => {
|
||||
cy.get('.namespace-container')
|
||||
.should('have.class', 'is-active')
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||
|
||||
import {ListFactory} from '../../factories/list'
|
||||
import {seed} from '../../support/seed'
|
||||
import {TaskFactory} from '../../factories/task'
|
||||
import {formatISO} from 'date-fns'
|
||||
import {UserFactory} from '../../factories/user'
|
||||
import {NamespaceFactory} from '../../factories/namespace'
|
||||
import {BucketFactory} from '../../factories/bucket'
|
||||
import {updateUserSettings} from '../../support/updateUserSettings'
|
||||
|
||||
function seedTasks(numberOfTasks = 50, startDueDate = new Date()) {
|
||||
import '../../support/authenticateUser'
|
||||
|
||||
function seedTasks(numberOfTasks = 100, startDueDate = new Date()) {
|
||||
UserFactory.create(1)
|
||||
NamespaceFactory.create(1)
|
||||
const list = ListFactory.create()[0]
|
||||
BucketFactory.create(1, {
|
||||
|
@ -17,7 +20,7 @@ function seedTasks(numberOfTasks = 50, startDueDate = new Date()) {
|
|||
let dueDate = startDueDate
|
||||
for (let i = 0; i < numberOfTasks; i++) {
|
||||
const now = new Date()
|
||||
dueDate = new Date(new Date(dueDate).setDate(dueDate.getDate() + 2))
|
||||
dueDate = (new Date(dueDate.valueOf())).setDate((new Date(dueDate.valueOf())).getDate() + 2)
|
||||
tasks.push({
|
||||
id: i + 1,
|
||||
list_id: list.id,
|
||||
|
@ -25,9 +28,9 @@ function seedTasks(numberOfTasks = 50, startDueDate = new Date()) {
|
|||
created_by_id: 1,
|
||||
title: 'Test Task ' + i,
|
||||
index: i + 1,
|
||||
due_date: dueDate.toISOString(),
|
||||
created: now.toISOString(),
|
||||
updated: now.toISOString(),
|
||||
due_date: formatISO(dueDate),
|
||||
created: formatISO(now),
|
||||
updated: formatISO(now),
|
||||
})
|
||||
}
|
||||
seed(TaskFactory.table, tasks)
|
||||
|
@ -35,11 +38,8 @@ function seedTasks(numberOfTasks = 50, startDueDate = new Date()) {
|
|||
}
|
||||
|
||||
describe('Home Page Task Overview', () => {
|
||||
createFakeUserAndLogin()
|
||||
|
||||
it('Should show tasks with a near due date first on the home page overview', () => {
|
||||
const taskCount = 50
|
||||
const {tasks} = seedTasks(taskCount)
|
||||
const {tasks} = seedTasks()
|
||||
|
||||
cy.visit('/')
|
||||
cy.get('[data-cy="showTasks"] .card .task')
|
||||
|
@ -49,10 +49,8 @@ describe('Home Page Task Overview', () => {
|
|||
})
|
||||
|
||||
it('Should show overdue tasks first, then show other tasks', () => {
|
||||
const now = new Date()
|
||||
const oldDate = new Date(new Date(now).setDate(now.getDate() - 14))
|
||||
const taskCount = 50
|
||||
const {tasks} = seedTasks(taskCount, oldDate)
|
||||
const oldDate = (new Date()).setDate((new Date()).getDate() - 14)
|
||||
const {tasks} = seedTasks(100, oldDate)
|
||||
|
||||
cy.visit('/')
|
||||
cy.get('[data-cy="showTasks"] .card .task')
|
||||
|
@ -70,7 +68,7 @@ describe('Home Page Task Overview', () => {
|
|||
TaskFactory.create(1, {
|
||||
id: 999,
|
||||
title: newTaskTitle,
|
||||
due_date: new Date().toISOString(),
|
||||
due_date: formatISO(new Date()),
|
||||
}, false)
|
||||
|
||||
cy.visit(`/lists/${tasks[0].list_id}/list`)
|
||||
|
@ -85,7 +83,7 @@ describe('Home Page Task Overview', () => {
|
|||
|
||||
it('Should not show a new task without a date at the bottom when there are > 50 tasks', () => {
|
||||
// We're not using the api here to create the task in order to verify the flow
|
||||
const {tasks} = seedTasks(100)
|
||||
const {tasks} = seedTasks()
|
||||
const newTaskTitle = 'New Task'
|
||||
|
||||
cy.visit('/')
|
||||
|
@ -130,24 +128,4 @@ describe('Home Page Task Overview', () => {
|
|||
.last()
|
||||
.should('contain.text', newTaskTitle)
|
||||
})
|
||||
|
||||
it('Should show the cta buttons for new list when there are no tasks', () => {
|
||||
TaskFactory.truncate()
|
||||
|
||||
cy.visit('/')
|
||||
|
||||
cy.get('.home.app-content .content')
|
||||
.should('contain.text', 'You can create a new list for your new tasks:')
|
||||
.should('contain.text', 'Or import your lists and tasks from other services into Vikunja:')
|
||||
})
|
||||
|
||||
it('Should not show the cta buttons for new list when there are tasks', () => {
|
||||
seedTasks()
|
||||
|
||||
cy.visit('/')
|
||||
|
||||
cy.get('.home.app-content .content')
|
||||
.should('not.contain.text', 'You can create a new list for your new tasks:')
|
||||
.should('not.contain.text', 'Or import your lists and tasks from other services into Vikunja:')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||
import {formatISO} from 'date-fns'
|
||||
|
||||
import {TaskFactory} from '../../factories/task'
|
||||
import {ListFactory} from '../../factories/list'
|
||||
|
@ -11,53 +11,16 @@ import {LabelFactory} from '../../factories/labels'
|
|||
import {LabelTaskFactory} from '../../factories/label_task'
|
||||
import {BucketFactory} from '../../factories/bucket'
|
||||
|
||||
import {TaskAttachmentFactory} from '../../factories/task_attachments'
|
||||
|
||||
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]', {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')
|
||||
}
|
||||
import '../../support/authenticateUser'
|
||||
|
||||
describe('Task', () => {
|
||||
createFakeUserAndLogin()
|
||||
|
||||
let namespaces
|
||||
let lists
|
||||
let buckets
|
||||
|
||||
beforeEach(() => {
|
||||
// UserFactory.create(1)
|
||||
UserFactory.create(1)
|
||||
namespaces = NamespaceFactory.create(1)
|
||||
lists = ListFactory.create(1)
|
||||
buckets = BucketFactory.create(1, {
|
||||
list_id: lists[0].id,
|
||||
})
|
||||
TaskFactory.truncate()
|
||||
UserListFactory.truncate()
|
||||
})
|
||||
|
@ -117,7 +80,6 @@ describe('Task', () => {
|
|||
describe('Task Detail View', () => {
|
||||
beforeEach(() => {
|
||||
TaskCommentFactory.truncate()
|
||||
LabelTaskFactory.truncate()
|
||||
})
|
||||
|
||||
it('Shows all task details', () => {
|
||||
|
@ -146,7 +108,7 @@ describe('Task', () => {
|
|||
id: 1,
|
||||
index: 1,
|
||||
done: true,
|
||||
done_at: new Date().toISOString()
|
||||
done_at: formatISO(new Date())
|
||||
})
|
||||
cy.visit(`/tasks/${tasks[0].id}`)
|
||||
|
||||
|
@ -382,31 +344,21 @@ describe('Task', () => {
|
|||
|
||||
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,
|
||||
list_id: lists[0].id,
|
||||
bucket_id: buckets[0].id,
|
||||
})
|
||||
const labels = LabelFactory.create(1)
|
||||
LabelTaskFactory.truncate()
|
||||
|
||||
cy.visit(`/lists/${lists[0].id}/kanban`)
|
||||
|
||||
cy.get('.bucket .task')
|
||||
.contains(tasks[0].title)
|
||||
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()
|
||||
|
||||
addLabelToTaskAndVerify(labels[0].title)
|
||||
|
||||
cy.get('.modal-content .close')
|
||||
.click()
|
||||
|
||||
cy.get('.bucket .task')
|
||||
.should('contain.text', labels[0].title)
|
||||
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', () => {
|
||||
|
@ -422,10 +374,10 @@ describe('Task', () => {
|
|||
|
||||
cy.visit(`/tasks/${tasks[0].id}`)
|
||||
|
||||
cy.get('.task-view .details.labels-list .multiselect .input-wrapper')
|
||||
cy.getSettled('.task-view .details.labels-list .multiselect .input-wrapper')
|
||||
.should('be.visible')
|
||||
.should('contain', labels[0].title)
|
||||
cy.get('.task-view .details.labels-list .multiselect .input-wrapper')
|
||||
cy.getSettled('.task-view .details.labels-list .multiselect .input-wrapper')
|
||||
.children()
|
||||
.first()
|
||||
.get('[data-cy="taskDetail.removeLabel"]')
|
||||
|
@ -465,117 +417,5 @@ describe('Task', () => {
|
|||
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,
|
||||
list_id: lists[0].id,
|
||||
bucket_id: buckets[0].id,
|
||||
})
|
||||
const labels = LabelFactory.create(1)
|
||||
LabelTaskFactory.truncate()
|
||||
|
||||
cy.visit(`/lists/${lists[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: `
|
||||
This is a checklist:
|
||||
|
||||
* [ ] one item
|
||||
* [ ] another item
|
||||
* [ ] third item
|
||||
* [ ] fourth item
|
||||
* [x] and this one is already done
|
||||
`,
|
||||
})
|
||||
cy.visit(`/tasks/${tasks[0].id}`)
|
||||
|
||||
cy.get('.task-view .checklist-summary')
|
||||
.should('contain.text', '1 of 5 tasks')
|
||||
cy.get('.editor .content ul > li input[type=checkbox]')
|
||||
.eq(2)
|
||||
.click()
|
||||
|
||||
cy.get('.editor .content ul > li input[type=checkbox]')
|
||||
.eq(2)
|
||||
.should('be.checked')
|
||||
cy.get('.editor .content input[type=checkbox]')
|
||||
.should('have.length', 5)
|
||||
cy.get('.task-view .checklist-summary')
|
||||
.should('contain.text', '2 of 5 tasks')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.web.json",
|
||||
"include": ["./**/*", "../support/**/*", "../factories/**/*"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"isolatedModules": false,
|
||||
"target": "ES2015",
|
||||
"lib": ["ESNext", "dom"],
|
||||
"types": ["cypress"]
|
||||
}
|
||||
}
|
|
@ -11,11 +11,16 @@ const testAndAssertFailed = fixture => {
|
|||
cy.get('div.message.danger').contains('Wrong username or password.')
|
||||
}
|
||||
|
||||
const username = 'test'
|
||||
|
||||
context('Login', () => {
|
||||
beforeEach(() => {
|
||||
UserFactory.create(1, {username})
|
||||
UserFactory.create(1, {
|
||||
username: 'test',
|
||||
})
|
||||
cy.visit('/', {
|
||||
onBeforeLoad(win) {
|
||||
win.localStorage.removeItem('token')
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('Should log in with the right credentials', () => {
|
||||
|
@ -50,9 +55,4 @@ context('Login', () => {
|
|||
|
||||
testAndAssertFailed(fixture)
|
||||
})
|
||||
|
||||
it('Should redirect to /login when no user is logged in', () => {
|
||||
cy.visit('/')
|
||||
cy.url().should('include', '/login')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,46 +1,16 @@
|
|||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||
import {createLists} from '../list/prepareLists'
|
||||
|
||||
function logout() {
|
||||
cy.get('.navbar .username-dropdown-trigger')
|
||||
.click()
|
||||
cy.get('.navbar .dropdown-item')
|
||||
.contains('Logout')
|
||||
.click()
|
||||
}
|
||||
import '../../support/authenticateUser'
|
||||
|
||||
describe('Log out', () => {
|
||||
createFakeUserAndLogin()
|
||||
|
||||
it('Logs the user out', () => {
|
||||
cy.visit('/')
|
||||
|
||||
expect(localStorage.getItem('token')).to.not.eq(null)
|
||||
|
||||
logout()
|
||||
cy.get('.navbar .user .username')
|
||||
.click()
|
||||
cy.get('.navbar .user .dropdown-menu .dropdown-item')
|
||||
.contains('Logout')
|
||||
.click()
|
||||
|
||||
cy.url()
|
||||
.should('contain', '/login')
|
||||
.then(() => {
|
||||
expect(localStorage.getItem('token')).to.eq(null)
|
||||
})
|
||||
})
|
||||
|
||||
it.skip('Should clear the list history after logging the user out', () => {
|
||||
const lists = createLists()
|
||||
cy.visit(`/lists/${lists[0].id}`)
|
||||
.then(() => {
|
||||
expect(localStorage.getItem('listHistory')).to.not.eq(null)
|
||||
})
|
||||
|
||||
logout()
|
||||
|
||||
cy.wait(1000) // This makes re-loading of the list and associated entities (and the resulting error) visible
|
||||
|
||||
cy.url()
|
||||
.should('contain', '/login')
|
||||
.then(() => {
|
||||
expect(localStorage.getItem('listHistory')).to.eq(null)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||
import {UserFactory} from '../../factories/user'
|
||||
|
||||
import '../../support/authenticateUser'
|
||||
|
||||
describe('User Settings', () => {
|
||||
createFakeUserAndLogin()
|
||||
beforeEach(() => {
|
||||
UserFactory.create(1)
|
||||
})
|
||||
|
||||
it('Changes the user avatar', () => {
|
||||
cy.intercept(`${Cypress.env('API_URL')}/user/settings/avatar/upload`).as('uploadAvatar')
|
||||
|
@ -37,7 +41,7 @@ describe('User Settings', () => {
|
|||
|
||||
cy.get('.global-notification')
|
||||
.should('contain', 'Success')
|
||||
cy.get('.navbar .username-dropdown-trigger .username')
|
||||
cy.get('.navbar .user .username')
|
||||
.should('contain', 'Lorem Ipsum')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import {faker} from '@faker-js/faker'
|
||||
import {Factory} from '../support/factory'
|
||||
import {formatISO} from 'date-fns'
|
||||
|
||||
export class BucketFactory extends Factory {
|
||||
static table = 'buckets'
|
||||
|
@ -12,8 +13,8 @@ export class BucketFactory extends Factory {
|
|||
title: faker.lorem.words(3),
|
||||
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 {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,4 +1,5 @@
|
|||
import {Factory} from '../support/factory'
|
||||
import {formatISO} from "date-fns"
|
||||
import {faker} from '@faker-js/faker'
|
||||
|
||||
export class LinkShareFactory extends Factory {
|
||||
|
@ -14,8 +15,8 @@ export class LinkShareFactory extends Factory {
|
|||
right: 0,
|
||||
sharing_type: 0,
|
||||
shared_by_id: 1,
|
||||
created: now.toISOString(),
|
||||
updated: now.toISOString(),
|
||||
created: formatISO(now),
|
||||
updated: formatISO(now)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {Factory} from '../support/factory'
|
||||
import {formatISO} from "date-fns"
|
||||
import {faker} from '@faker-js/faker'
|
||||
|
||||
export class ListFactory extends Factory {
|
||||
|
@ -12,8 +13,8 @@ export class ListFactory extends Factory {
|
|||
title: faker.lorem.words(3),
|
||||
owner_id: 1,
|
||||
namespace_id: 1,
|
||||
created: now.toISOString(),
|
||||
updated: now.toISOString(),
|
||||
created: formatISO(now),
|
||||
updated: formatISO(now)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import {faker} from '@faker-js/faker'
|
||||
import {Factory} from '../support/factory'
|
||||
import {formatISO} from 'date-fns'
|
||||
|
||||
export class NamespaceFactory extends Factory {
|
||||
static table = 'namespaces'
|
||||
|
@ -11,8 +12,8 @@ export class NamespaceFactory extends Factory {
|
|||
id: '{increment}',
|
||||
title: faker.lorem.words(3),
|
||||
owner_id: 1,
|
||||
created: now.toISOString(),
|
||||
updated: now.toISOString(),
|
||||
created: formatISO(now),
|
||||
updated: formatISO(now)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import {faker} from '@faker-js/faker'
|
||||
import {Factory} from '../support/factory'
|
||||
import {formatISO} from 'date-fns'
|
||||
|
||||
export class TaskFactory extends Factory {
|
||||
static table = 'tasks'
|
||||
|
@ -15,8 +16,8 @@ export class TaskFactory extends Factory {
|
|||
created_by_id: 1,
|
||||
index: '{increment}',
|
||||
position: '{increment}',
|
||||
created: now.toISOString(),
|
||||
updated: now.toISOString()
|
||||
created: formatISO(now),
|
||||
updated: formatISO(now)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {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,5 +1,6 @@
|
|||
import {faker} from '@faker-js/faker'
|
||||
import {Factory} from '../support/factory'
|
||||
import {formatISO} from 'date-fns'
|
||||
|
||||
export class TeamFactory extends Factory {
|
||||
static table = 'teams'
|
||||
|
@ -10,8 +11,8 @@ export class TeamFactory extends Factory {
|
|||
return {
|
||||
name: faker.lorem.words(3),
|
||||
created_by_id: 1,
|
||||
created: now.toISOString(),
|
||||
updated: now.toISOString(),
|
||||
created: formatISO(now),
|
||||
updated: formatISO(now)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {Factory} from '../support/factory'
|
||||
import {formatISO} from "date-fns"
|
||||
|
||||
export class UserFactory extends Factory {
|
||||
static table = 'users'
|
||||
|
@ -14,8 +15,8 @@ export class UserFactory extends Factory {
|
|||
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.', // 1234
|
||||
status: 0,
|
||||
issuer: 'local',
|
||||
created: now.toISOString(),
|
||||
updated: now.toISOString(),
|
||||
created: formatISO(now),
|
||||
updated: formatISO(now)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import {Factory} from '../support/factory'
|
||||
import {formatISO} from "date-fns"
|
||||
|
||||
export class UserListFactory extends Factory {
|
||||
static table = 'users_lists'
|
||||
|
@ -11,8 +12,8 @@ export class UserListFactory extends Factory {
|
|||
list_id: 1,
|
||||
user_id: 1,
|
||||
right: 0,
|
||||
created: now.toISOString(),
|
||||
updated: now.toISOString(),
|
||||
created: formatISO(now),
|
||||
updated: formatISO(now)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,32 +4,26 @@
|
|||
|
||||
import {UserFactory} from '../factories/user'
|
||||
|
||||
export function login(user, cacheAcrossSpecs = false) {
|
||||
if (!user) {
|
||||
throw new Error('Needs user')
|
||||
}
|
||||
// Caching session when logging in via page visit
|
||||
cy.session(`user__${user.username}`, () => {
|
||||
cy.request('POST', `${Cypress.env('API_URL')}/login`, {
|
||||
username: user.username,
|
||||
password: '1234',
|
||||
}).then(({ body }) => {
|
||||
window.localStorage.setItem('token', body.token)
|
||||
})
|
||||
}, {
|
||||
cacheAcrossSpecs,
|
||||
})
|
||||
}
|
||||
let token
|
||||
|
||||
export function createFakeUserAndLogin() {
|
||||
let user
|
||||
before(() => {
|
||||
user = UserFactory.create(1)[0]
|
||||
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(() => {
|
||||
login(user, true)
|
||||
cy.log(`Using token ${token} to make authenticated requests`)
|
||||
cy.visit('/', {
|
||||
onBeforeLoad(win) {
|
||||
win.localStorage.setItem('token', token)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
return user
|
||||
}
|
|
@ -35,3 +35,37 @@
|
|||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* Recursively gets an element, returning only after it's determined to be attached to the DOM for good.
|
||||
*
|
||||
* Source: https://github.com/cypress-io/cypress/issues/7306#issuecomment-850621378
|
||||
*/
|
||||
Cypress.Commands.add('getSettled', (selector, opts = {}) => {
|
||||
const retries = opts.retries || 3
|
||||
const delay = opts.delay || 100
|
||||
|
||||
const isAttached = (resolve, count = 0) => {
|
||||
const el = Cypress.$(selector)
|
||||
|
||||
// is element attached to the DOM?
|
||||
count = Cypress.dom.isAttached(el) ? count + 1 : 0
|
||||
|
||||
// hit our base case, return the element
|
||||
if (count >= retries) {
|
||||
return resolve(el)
|
||||
}
|
||||
|
||||
// retry after a bit of a delay
|
||||
setTimeout(() => isAttached(resolve, count), delay)
|
||||
}
|
||||
|
||||
// wrap, so we can chain cypress commands off the result
|
||||
return cy.wrap(null).then(() => {
|
||||
return new Cypress.Promise((resolve) => {
|
||||
return isAttached(resolve, 0)
|
||||
}).then((el) => {
|
||||
return cy.wrap(el)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
10
cypress/tsconfig.json
Normal file
10
cypress/tsconfig.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.web.json",
|
||||
"include": ["./integration/**/*", "./support/**/*"],
|
||||
"compilerOptions": {
|
||||
"isolatedModules": false,
|
||||
"target": "es5",
|
||||
"lib": ["es5", "dom"],
|
||||
"types": ["cypress"]
|
||||
}
|
||||
}
|
17
docker-manifest-unstable.tmpl
Normal file
17
docker-manifest-unstable.tmpl
Normal file
|
@ -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
|
23
docker-manifest.tmpl
Normal file
23
docker-manifest.tmpl
Normal file
|
@ -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,15 +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
|
||||
|
||||
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,112 +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;
|
||||
worker_rlimit_nofile 65535;
|
||||
|
||||
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,71 +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;
|
||||
}
|
||||
|
||||
# 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 { }
|
||||
|
||||
}
|
9
env.config.d.ts
vendored
9
env.config.d.ts
vendored
|
@ -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
|
||||
}
|
9
env.d.ts
vendored
9
env.d.ts
vendored
|
@ -1,12 +1,3 @@
|
|||
/// <reference types="vite/client" />
|
||||
/// <reference types="vite-svg-loader" />
|
||||
/// <reference types="cypress" />
|
||||
/// <reference types="@histoire/plugin-vue/components" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_IS_ONLINE: boolean
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
25
flake.lock
25
flake.lock
|
@ -1,25 +0,0 @@
|
|||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1664753041,
|
||||
"narHash": "sha256-0ogaD8PaGHluARFeupofvk1Nq9gpVeZdlFM0Kcwguys=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "a62844b302507c7531ad68a86cb7aa54704c9cb4",
|
||||
"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://acme.com',
|
||||
// favicon: './favicon.ico',
|
||||
},
|
||||
})
|
13
index.html
13
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>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
[build]
|
||||
command = "pnpm run build"
|
||||
command = "yarn build"
|
||||
publish = "dist-preview"
|
||||
|
||||
[[redirects]]
|
||||
|
|
115
nginx.conf
Normal file
115
nginx.conf
Normal file
|
@ -0,0 +1,115 @@
|
|||
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;
|
||||
|
||||
types {
|
||||
application/manifest+json webmanifest;
|
||||
}
|
||||
|
||||
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_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/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;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen 81 default_server http2 proxy_protocol; ## Needed when behind HAProxy with SSL termination + HTTP/2 support
|
||||
|
||||
server_name _;
|
||||
|
||||
expires $expires;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
# 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;
|
||||
}
|
||||
|
||||
# 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;
|
||||
}
|
||||
|
||||
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 {
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
192
package.json
192
package.json
|
@ -1,146 +1,106 @@
|
|||
{
|
||||
"name": "vikunja-frontend",
|
||||
"description": "The todo app to organize your life.",
|
||||
"private": true,
|
||||
"version": "0.10.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://kolaente.dev/vikunja/frontend"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://kolaente.dev/vikunja/frontend/issues"
|
||||
},
|
||||
"homepage": "https://vikunja.io/",
|
||||
"funding": "https://opencollective.com/vikunja",
|
||||
"packageManager": "pnpm@7.27.1",
|
||||
"keywords": [
|
||||
"todo",
|
||||
"productivity",
|
||||
"task management",
|
||||
"organisation",
|
||||
"gantt",
|
||||
"kanban"
|
||||
],
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vite",
|
||||
"preview": "vite preview --port 4173",
|
||||
"preview:dev": "vite preview --outDir dist-dev --mode development --port 4173",
|
||||
"serve:dist-dev": "node scripts/serve-dist.js",
|
||||
"serve:dist": "vite preview --port 4173",
|
||||
"build": "vite build && workbox copyLibraries dist/",
|
||||
"build:modern-only": "BUILD_MODERN_ONLY=true vite build && workbox copyLibraries dist/",
|
||||
"build:dev": "vite build --mode development --outDir dist-dev/",
|
||||
"build:dev": "vite build -m development --outDir dist-dev/",
|
||||
"lint": "eslint --ignore-pattern '*.test.*' ./src --ext .vue,.js,.ts",
|
||||
"test:e2e": "start-server-and-test preview http://127.0.0.1:4173 'cypress run --e2e --browser chrome'",
|
||||
"test:e2e-record": "start-server-and-test preview http://127.0.0.1:4173 'cypress run --e2e --browser chrome --record'",
|
||||
"test:e2e-dev-dev": "start-server-and-test preview:dev http://127.0.0.1:4173 'cypress open --e2e'",
|
||||
"test:e2e-dev": "start-server-and-test preview http://127.0.0.1:4173 'cypress open --e2e'",
|
||||
"test:unit": "vitest --dir ./src",
|
||||
"cypress:open": "cypress open",
|
||||
"test:unit": "vitest",
|
||||
"test:unit-watch": "vitest watch",
|
||||
"test:frontend": "cypress run",
|
||||
"typecheck": "vue-tsc --noEmit && vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
|
||||
"browserslist:update": "pnpm dlx browserslist@latest --update-db",
|
||||
"fonts:update": "pnpm fonts:download && pnpm fonts:subset",
|
||||
"fonts:download": "./scripts/fonts-download.sh",
|
||||
"fonts:subset": "./scripts/fonts-subset.sh",
|
||||
"story:dev": "histoire dev",
|
||||
"story:build": "histoire build",
|
||||
"story:preview": "histoire preview"
|
||||
"browserslist:update": "npx browserslist@latest --update-db"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "6.3.0",
|
||||
"@fortawesome/free-regular-svg-icons": "6.3.0",
|
||||
"@fortawesome/free-solid-svg-icons": "6.3.0",
|
||||
"@fortawesome/vue-fontawesome": "3.0.3",
|
||||
"@github/hotkey": "2.0.1",
|
||||
"@infectoone/vue-ganttastic": "2.1.4",
|
||||
"@intlify/unplugin-vue-i18n": "0.8.2",
|
||||
"@kyvg/vue3-notification": "2.9.0",
|
||||
"@sentry/tracing": "7.38.0",
|
||||
"@sentry/vue": "7.38.0",
|
||||
"@kyvg/vue3-notification": "2.3.5",
|
||||
"@sentry/tracing": "7.7.0",
|
||||
"@sentry/vue": "7.7.0",
|
||||
"@types/is-touch-device": "1.0.0",
|
||||
"@types/lodash.clonedeep": "4.5.7",
|
||||
"@types/sortablejs": "1.15.0",
|
||||
"@vueuse/core": "9.13.0",
|
||||
"axios": "1.3.3",
|
||||
"blurhash": "2.0.5",
|
||||
"@types/sortablejs": "1.13.0",
|
||||
"@vueuse/core": "8.9.4",
|
||||
"@vueuse/router": "8.9.4",
|
||||
"blurhash": "1.1.5",
|
||||
"bulma-css-variables": "0.9.33",
|
||||
"camel-case": "4.1.2",
|
||||
"codemirror": "5.65.12",
|
||||
"date-fns": "2.29.3",
|
||||
"dayjs": "1.11.7",
|
||||
"dompurify": "3.0.0",
|
||||
"easymde": "2.18.0",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"date-fns": "2.29.1",
|
||||
"dompurify": "2.3.10",
|
||||
"easymde": "2.16.1",
|
||||
"flatpickr": "4.6.13",
|
||||
"flexsearch": "0.7.21",
|
||||
"floating-vue": "2.0.0-beta.20",
|
||||
"focus-within": "3.0.2",
|
||||
"highlight.js": "11.7.0",
|
||||
"highlight.js": "11.6.0",
|
||||
"is-touch-device": "1.0.1",
|
||||
"klona": "2.0.6",
|
||||
"lodash.clonedeep": "4.5.0",
|
||||
"lodash.debounce": "4.0.8",
|
||||
"marked": "4.2.12",
|
||||
"pinia": "2.0.32",
|
||||
"marked": "4.0.18",
|
||||
"minimist": "1.2.6",
|
||||
"register-service-worker": "1.7.2",
|
||||
"snake-case": "3.0.4",
|
||||
"sortablejs": "1.15.0",
|
||||
"ufo": "1.1.0",
|
||||
"vue": "3.2.47",
|
||||
"vue-advanced-cropper": "2.8.8",
|
||||
"vue-flatpickr-component": "11.0.2",
|
||||
"vue-i18n": "9.2.2",
|
||||
"vue-router": "4.1.6",
|
||||
"ufo": "0.8.5",
|
||||
"v-tooltip": "4.0.0-beta.17",
|
||||
"vue": "3.2.37",
|
||||
"vue-advanced-cropper": "2.8.3",
|
||||
"vue-drag-resize": "2.0.3",
|
||||
"vue-flatpickr-component": "9.0.6",
|
||||
"vue-i18n": "9.2.0-beta.40",
|
||||
"vue-router": "4.1.2",
|
||||
"vuex": "4.0.2",
|
||||
"workbox-precaching": "6.5.4",
|
||||
"zhyswan-vuedraggable": "4.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@4tw/cypress-drag-drop": "2.2.3",
|
||||
"@cypress/vite-dev-server": "5.0.3",
|
||||
"@cypress/vue": "5.0.4",
|
||||
"@faker-js/faker": "7.6.0",
|
||||
"@histoire/plugin-screenshot": "0.15.8",
|
||||
"@histoire/plugin-vue": "0.15.8",
|
||||
"@rushstack/eslint-patch": "1.2.0",
|
||||
"@types/codemirror": "5.60.7",
|
||||
"@types/dompurify": "2.4.0",
|
||||
"@4tw/cypress-drag-drop": "2.2.1",
|
||||
"@cypress/vite-dev-server": "3.0.0",
|
||||
"@cypress/vue": "4.0.0",
|
||||
"@faker-js/faker": "7.3.0",
|
||||
"@fortawesome/fontawesome-svg-core": "6.1.2",
|
||||
"@fortawesome/free-regular-svg-icons": "6.1.2",
|
||||
"@fortawesome/free-solid-svg-icons": "6.1.2",
|
||||
"@fortawesome/vue-fontawesome": "3.0.1",
|
||||
"@types/flexsearch": "0.7.3",
|
||||
"@types/focus-within": "1.0.1",
|
||||
"@types/lodash.debounce": "4.0.7",
|
||||
"@types/marked": "4.0.8",
|
||||
"@types/node": "18.14.0",
|
||||
"@types/postcss-preset-env": "7.7.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.53.0",
|
||||
"@typescript-eslint/parser": "5.53.0",
|
||||
"@vitejs/plugin-legacy": "4.0.1",
|
||||
"@vitejs/plugin-vue": "4.0.0",
|
||||
"@vue/eslint-config-typescript": "11.0.2",
|
||||
"@vue/test-utils": "2.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.30.7",
|
||||
"@typescript-eslint/parser": "5.30.7",
|
||||
"@vitejs/plugin-legacy": "2.0.0",
|
||||
"@vitejs/plugin-vue": "3.0.1",
|
||||
"@vue/eslint-config-typescript": "11.0.0",
|
||||
"@vue/test-utils": "2.0.2",
|
||||
"@vue/tsconfig": "0.1.3",
|
||||
"autoprefixer": "10.4.13",
|
||||
"browserslist": "4.21.5",
|
||||
"caniuse-lite": "1.0.30001457",
|
||||
"csstype": "3.1.1",
|
||||
"cypress": "12.6.0",
|
||||
"esbuild": "0.17.10",
|
||||
"eslint": "8.34.0",
|
||||
"eslint-plugin-vue": "9.9.0",
|
||||
"happy-dom": "8.6.0",
|
||||
"histoire": "0.15.8",
|
||||
"netlify-cli": "12.13.2",
|
||||
"postcss": "8.4.21",
|
||||
"postcss-easing-gradients": "3.0.1",
|
||||
"postcss-easings": "3.0.1",
|
||||
"postcss-preset-env": "8.0.1",
|
||||
"rollup": "3.17.2",
|
||||
"rollup-plugin-visualizer": "5.9.0",
|
||||
"sass": "1.58.3",
|
||||
"start-server-and-test": "1.15.4",
|
||||
"typescript": "4.9.5",
|
||||
"vite": "4.1.3",
|
||||
"vite-plugin-inject-preload": "1.3.0",
|
||||
"vite-plugin-pwa": "0.14.4",
|
||||
"vite-svg-loader": "4.0.0",
|
||||
"vitest": "0.28.5",
|
||||
"vue-tsc": "1.1.5",
|
||||
"wait-on": "7.0.1",
|
||||
"autoprefixer": "10.4.7",
|
||||
"axios": "0.27.2",
|
||||
"browserslist": "4.21.3",
|
||||
"caniuse-lite": "1.0.30001373",
|
||||
"cypress": "10.3.1",
|
||||
"esbuild": "0.14.51",
|
||||
"eslint": "8.20.0",
|
||||
"eslint-plugin-vue": "9.3.0",
|
||||
"express": "4.18.1",
|
||||
"happy-dom": "6.0.4",
|
||||
"netlify-cli": "10.13.0",
|
||||
"postcss": "8.4.14",
|
||||
"postcss-preset-env": "7.7.2",
|
||||
"rollup": "2.77.0",
|
||||
"rollup-plugin-visualizer": "5.7.1",
|
||||
"sass": "1.54.0",
|
||||
"typescript": "4.7.4",
|
||||
"vite": "3.0.4",
|
||||
"vite-plugin-pwa": "0.12.3",
|
||||
"vite-svg-loader": "3.4.0",
|
||||
"vitest": "0.20.2",
|
||||
"vue-tsc": "0.38.9",
|
||||
"wait-on": "6.0.1",
|
||||
"workbox-cli": "6.5.4"
|
||||
},
|
||||
"postcss": {
|
||||
"plugins": {
|
||||
"autoprefixer": {}
|
||||
}
|
||||
},
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"packageManager": "yarn@1.22.19"
|
||||
}
|
||||
|
|
15532
pnpm-lock.yaml
15532
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
BIN
public/fonts/open-sans-v15-latin-700.woff2
Normal file
BIN
public/fonts/open-sans-v15-latin-700.woff2
Normal file
Binary file not shown.
BIN
public/fonts/open-sans-v15-latin-700italic.woff2
Normal file
BIN
public/fonts/open-sans-v15-latin-700italic.woff2
Normal file
Binary file not shown.
BIN
public/fonts/open-sans-v15-latin-italic.woff2
Normal file
BIN
public/fonts/open-sans-v15-latin-italic.woff2
Normal file
Binary file not shown.
BIN
public/fonts/open-sans-v15-latin-regular.woff2
Normal file
BIN
public/fonts/open-sans-v15-latin-regular.woff2
Normal file
Binary file not shown.
BIN
public/fonts/quicksand-v7-latin-500.woff2
Normal file
BIN
public/fonts/quicksand-v7-latin-500.woff2
Normal file
Binary file not shown.
BIN
public/fonts/quicksand-v7-latin-700.woff2
Normal file
BIN
public/fonts/quicksand-v7-latin-700.woff2
Normal file
Binary file not shown.
BIN
public/fonts/quicksand-v7-latin-regular.woff2
Normal file
BIN
public/fonts/quicksand-v7-latin-regular.woff2
Normal file
Binary file not shown.
|
@ -2,11 +2,11 @@
|
|||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"labels": ["dependencies"],
|
||||
"extends": [
|
||||
"config:js-app"
|
||||
"config:base"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchPackageNames": ["netlify-cli", "happy-dom"],
|
||||
"matchPackageNames": ["netlify-cli"],
|
||||
"extends": ["schedule:weekly"]
|
||||
},
|
||||
{
|
||||
|
@ -19,19 +19,6 @@
|
|||
"matchPackagePrefixes": [
|
||||
"@vueuse/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"groupName": "histoire",
|
||||
"matchPackagePrefixes": [
|
||||
"@histoire/",
|
||||
"histoire"
|
||||
]
|
||||
},
|
||||
{
|
||||
"matchDepTypes": ["devDependencies"],
|
||||
"automerge": true,
|
||||
"automergeStrategy": "squash",
|
||||
"automergeType": "pr"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
28
run.sh
Executable file
28
run.sh
Executable file
|
@ -0,0 +1,28 @@
|
|||
#!/bin/bash
|
||||
|
||||
# This shell script sets the api url based on an environment variable and starts nginx in foreground.
|
||||
|
||||
VIKUNJA_API_URL="${VIKUNJA_API_URL:-"/api/v1"}"
|
||||
VIKUNJA_SENTRY_ENABLED="${VIKUNJA_SENTRY_ENABLED:-"false"}"
|
||||
VIKUNJA_SENTRY_DSN="${VIKUNJA_SENTRY_DSN:-"https://85694a2d757547cbbc90cd4b55c5a18d@o1047380.ingest.sentry.io/6024480"}"
|
||||
VIKUNJA_HTTP_PORT="${VIKUNJA_HTTP_PORT:-80}"
|
||||
VIKUNJA_HTTPS_PORT="${VIKUNJA_HTTPS_PORT:-443}"
|
||||
|
||||
echo "Using $VIKUNJA_API_URL as default api url"
|
||||
|
||||
# Escape the variable to prevent sed from complaining
|
||||
VIKUNJA_API_URL=$(echo $VIKUNJA_API_URL |sed 's/\//\\\//g')
|
||||
|
||||
sed -i "s/http\:\/\/localhost\:3456//g" /usr/share/nginx/html/index.html # replacing in two steps to make sure api urls from releases are properly replaced as well
|
||||
sed -i "s/'\/api\/v1/'$VIKUNJA_API_URL/g" /usr/share/nginx/html/index.html
|
||||
sed -i "s/\.SENTRY_ENABLED = false/\.SENTRY_ENABLED = $VIKUNJA_SENTRY_ENABLED/g" /usr/share/nginx/html/index.html
|
||||
sed -i "s|\.SENTRY_DSN = '.*'|\.SENTRY_DSN = '$VIKUNJA_SENTRY_DSN'|g" /usr/share/nginx/html/index.html
|
||||
|
||||
sed -i "s/listen 80/listen $VIKUNJA_HTTP_PORT/g" /etc/nginx/nginx.conf
|
||||
sed -i "s/listen 443/listen $VIKUNJA_HTTPS_PORT/g" /etc/nginx/nginx.conf
|
||||
|
||||
# Set the uid and gid of the nginx run user
|
||||
usermod --non-unique --uid ${PUID} nginx
|
||||
groupmod --non-unique --gid ${PGID} nginx
|
||||
|
||||
nginx -g "daemon off;"
|
|
@ -1,18 +1,15 @@
|
|||
import { exec } from 'node:child_process'
|
||||
const {exec} = require('child_process')
|
||||
const axios = require('axios')
|
||||
|
||||
function createSlug(string) {
|
||||
return String(string)
|
||||
const BOT_USER_ID = 513
|
||||
const giteaToken = process.env.GITEA_TOKEN
|
||||
const siteId = process.env.NETLIFY_SITE_ID
|
||||
const branchSlug = String(process.env.DRONE_SOURCE_BRANCH)
|
||||
.trim()
|
||||
.normalize('NFKD')
|
||||
.toLowerCase()
|
||||
.replace(/[.\s/]/g, '-')
|
||||
.replace(/[^A-Za-z\d-]/g, '')
|
||||
}
|
||||
|
||||
const BOT_USER_ID = 513
|
||||
const giteaToken = process.env.GITEA_TOKEN
|
||||
const siteId = process.env.NETLIFY_SITE_ID
|
||||
const branchSlug = createSlug(process.env.DRONE_SOURCE_BRANCH)
|
||||
const prNumber = process.env.DRONE_PULL_REQUEST
|
||||
|
||||
const prIssueCommentsUrl = `https://kolaente.dev/api/v1/repos/vikunja/frontend/issues/${prNumber}/comments`
|
||||
|
@ -38,7 +35,7 @@ const promiseExec = cmd => {
|
|||
stdout = await promiseExec(`./node_modules/.bin/netlify deploy --alias ${alias}`)
|
||||
console.log(stdout)
|
||||
|
||||
const data = await fetch(prIssueCommentsUrl).then(response => response.json())
|
||||
const {data} = await axios.get(prIssueCommentsUrl)
|
||||
const hasComment = data.some(c => c.user.id === BOT_USER_ID)
|
||||
|
||||
if (hasComment) {
|
||||
|
@ -46,7 +43,8 @@ const promiseExec = cmd => {
|
|||
return
|
||||
}
|
||||
|
||||
const message = `
|
||||
await axios.post(prIssueCommentsUrl, {
|
||||
body: `
|
||||
Hi ${process.env.DRONE_COMMIT_AUTHOR}!
|
||||
|
||||
Thank you for creating a PR!
|
||||
|
@ -59,25 +57,14 @@ You will need to manually connect this to an api running somehwere. The easiest
|
|||
Have a nice day!
|
||||
|
||||
> Beep boop, I'm a bot.
|
||||
`
|
||||
|
||||
try {
|
||||
const response = await fetch(prIssueCommentsUrl, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
body: message,
|
||||
}),
|
||||
`,
|
||||
}, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'accept': 'application/json',
|
||||
'Authorization': `token ${giteaToken}`,
|
||||
},
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error, status = ${response.status}`)
|
||||
}
|
||||
|
||||
console.log(`Preview comment sent successfully to PR #${prNumber}!`)
|
||||
} catch (e) {
|
||||
console.log(`Could not send preview comment to PR #${prNumber}! ${e.message}`)
|
||||
}
|
||||
})()
|
1
scripts/deploy-preview-netlify.js.sha384
Normal file
1
scripts/deploy-preview-netlify.js.sha384
Normal file
|
@ -0,0 +1 @@
|
|||
bb46342a0a08105b340ba7976cff9d80ef89901120ec0639669caa70bb7d2dbc43e78b1f635a7654ab2456e8358c98a4 ./scripts/deploy-preview-netlify.js
|
|
@ -1 +0,0 @@
|
|||
57af69409e66bc87f4f2fc5822dd8d3c2eb47c601f81af1ac4a56f3e2d80837b1a2de06f4ff57695ec379b7c15b881e3 ./scripts/deploy-preview-netlify.mjs
|
|
@ -1,56 +0,0 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
#
|
||||
# This script downloads our original font files from their source repos
|
||||
# and puts them in our originalMedia folder.
|
||||
#
|
||||
|
||||
err_report() {
|
||||
echo "Error on line $(caller)" >&2
|
||||
}
|
||||
|
||||
trap err_report ERR
|
||||
|
||||
ORIGINAL_FONTS_DIR="./originalMedia/fonts"
|
||||
|
||||
# update these if there is a new version
|
||||
FONT_URLS=(
|
||||
"https://github.com/googlefonts/opensans/blob/27d060e1aad6886daeda67629ee28189f795f534/fonts/variable/OpenSans%5Bwdth%2Cwght%5D.ttf?raw=true"
|
||||
"https://github.com/googlefonts/opensans/blob/27d060e1aad6886daeda67629ee28189f795f534/fonts/variable/OpenSans-Italic%5Bwdth%2Cwght%5D.ttf?raw=true"
|
||||
"https://github.com/andrew-paglinawan/QuicksandFamily/blob/db6de44878582966f45a0debaef10d57108d93a7/fonts/Quicksand%5Bwght%5D.ttf?raw=true"
|
||||
)
|
||||
|
||||
|
||||
echo ""
|
||||
echo "###################################################"
|
||||
echo "# Download font files"
|
||||
echo "###################################################"
|
||||
echo ""
|
||||
|
||||
mkdir -p $ORIGINAL_FONTS_DIR
|
||||
|
||||
for URL in ${FONT_URLS[@]}; do
|
||||
wget -L $URL \
|
||||
--directory-prefix=$ORIGINAL_FONTS_DIR \
|
||||
--quiet \
|
||||
--timestamping \
|
||||
--show-progress
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "###################################################"
|
||||
echo "# Remove '?raw=true' filename suffix"
|
||||
echo "###################################################"
|
||||
echo ""
|
||||
|
||||
# Iterate over all files in directory with filetype ending in "?raw=true"
|
||||
for file in $ORIGINAL_FONTS_DIR/*?raw=true; do
|
||||
# Remove "?raw=true" from file name and store in variable
|
||||
new_name=$(echo $file | sed 's/?raw=true//')
|
||||
|
||||
# Overwrite existing file with new name
|
||||
mv -v $file $new_name
|
||||
done
|
||||
|
||||
echo "Renaming files complete"
|
|
@ -1,161 +0,0 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
#
|
||||
# This script subsets our variable fonts,
|
||||
# converts them to woff2 files and puts them in the
|
||||
# fonts folder.
|
||||
#
|
||||
# We do have to update the font paths in the @font-face
|
||||
# definitions manually since we use a checksum to make
|
||||
#
|
||||
# We use fonttools to create a partial instance of the
|
||||
# variable font where we keep only our needed features.
|
||||
# See more at:
|
||||
# https://fonttools.readthedocs.io/en/latest/varLib/instancer.html
|
||||
#
|
||||
# fonttools requires python > 3.7. For up-to-date
|
||||
# instructions see https://github.com/fonttools/fonttools#installation
|
||||
#
|
||||
# Lot's of info was gathered from:
|
||||
# https://markoskon.com/creating-font-subsets/
|
||||
# https://barrd.dev/article/create-a-variable-font-subset-for-smaller-file-size/
|
||||
#
|
||||
|
||||
ORIGINAL_FONTS="./originalMedia/fonts"
|
||||
TEMP_FOLDER="./.subset-fonts-temp"
|
||||
FONT_FOLDER="./src/assets/fonts"
|
||||
|
||||
err_report() {
|
||||
echo "Error on line $(caller)" >&2
|
||||
}
|
||||
|
||||
trap err_report ERR
|
||||
|
||||
mkdir -p $TEMP_FOLDER
|
||||
|
||||
# the latin subset that google uses on GoogleFonts
|
||||
# this is the same as the latin subset range that google uses on GoogleFonts
|
||||
# see for examle the unicode-range definition here:
|
||||
# https://fonts.googleapis.com/css2?family=Open+Sans
|
||||
UNICODE_LATIN_SUBSET="U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,\
|
||||
U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,\
|
||||
U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD"
|
||||
|
||||
get_filename_without_type() {
|
||||
filename=$1
|
||||
dirname=$(dirname $filename)
|
||||
# Extract the file type using parameter expansion
|
||||
filetype=${filename##*.}
|
||||
basename=$(basename $filename .$filetype)
|
||||
echo $basename
|
||||
}
|
||||
|
||||
# This function takes a font file and creates a subset of it using a specified set of unicode characters.
|
||||
instance_and_subset () {
|
||||
# Define default arguments for the subsetter.
|
||||
DEFAULT_SUBSETTER_ARGS="--layout-features=* --unicodes=${UNICODE_LATIN_SUBSET}"
|
||||
|
||||
# Assign function arguments to variables with more descriptive names.
|
||||
INPUT_FONT_FILE=$1
|
||||
INSTANCER_ARGS=$2
|
||||
OUTPUT_FONT_BASENAME=$3
|
||||
OUTPUT_FOLDER=$FONT_FOLDER
|
||||
|
||||
# If the output font basename is not provided, use the input font file's basename as the output font basename.
|
||||
if [ -z "$OUTPUT_FONT_BASENAME" ]; then
|
||||
INPUT_FONT_BASENAME=$(get_filename_without_type $INPUT_FONT_FILE)
|
||||
OUTPUT_FONT_BASENAME=$INPUT_FONT_BASENAME
|
||||
fi
|
||||
|
||||
# Use the default subsetter arguments if no custom arguments are provided.
|
||||
SUBSETTER_ARGS="${4:-$DEFAULT_SUBSETTER_ARGS}"
|
||||
|
||||
CHECKSUM=$(
|
||||
# Concatenate the contents of the input font file, the instancer arguments, and the subsetter arguments
|
||||
printf "%s%s" "$(cat $INPUT_FONT_FILE)" "$INSTANCER_ARGS" "$SUBSETTER_ARGS" |
|
||||
# Calculate the Blake2b checksum of the concatenated string
|
||||
b2sum |
|
||||
# Extract the checksum from the output of b2sum (it's the first field)
|
||||
awk '{print $1}'
|
||||
)
|
||||
|
||||
# Limit the checksum to 8 characters.
|
||||
CHECKSUM=$(echo "${CHECKSUM:0:8}")
|
||||
|
||||
# Construct the output font's filename
|
||||
OUTPUT_FONT_BASENAME="${OUTPUT_FONT_BASENAME}_${CHECKSUM}"
|
||||
OUTPUT_FONT_FILE="${OUTPUT_FOLDER}/${OUTPUT_FONT_BASENAME}.woff2"
|
||||
|
||||
# Check if the output font file already exists
|
||||
if test -f $OUTPUT_FONT_FILE; then
|
||||
echo "${OUTPUT_FONT_FILE} exists"
|
||||
return 0
|
||||
fi
|
||||
|
||||
FONT_INSTANCE="${TEMP_FOLDER}/${OUTPUT_FONT_BASENAME}.ttf"
|
||||
|
||||
if [ -n "$INSTANCER_ARGS" ]; then
|
||||
# If the INSTANCER_ARGS variable is set, use fonttools to create a font instance
|
||||
fonttools varLib.instancer --output $FONT_INSTANCE $INPUT_FONT_FILE $INSTANCER_ARGS
|
||||
else
|
||||
# Otherwise, just copy the input font file to the font instance file
|
||||
cp $INPUT_FONT_FILE $FONT_INSTANCE
|
||||
fi
|
||||
|
||||
# Use pyftsubset to create a subset of the font instance and save it to the output font file
|
||||
pyftsubset $FONT_INSTANCE --output-file=$OUTPUT_FONT_FILE --flavor=woff2 $SUBSETTER_ARGS
|
||||
|
||||
echo "${OUTPUT_FONT_BASENAME} subsetted."
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "###################################################"
|
||||
echo "# Install required libs"
|
||||
echo "###################################################"
|
||||
echo ""
|
||||
|
||||
pip install fonttools brotli
|
||||
|
||||
echo ""
|
||||
echo "###################################################"
|
||||
echo "# Create a partial instance of the variable font"
|
||||
echo "# where we keep only our needed features and then"
|
||||
echo "# subset fonts with latin unicode range and export"
|
||||
echo "# as woff2 file"
|
||||
echo "###################################################"
|
||||
echo ""
|
||||
|
||||
mkdir -p $TEMP_FOLDER
|
||||
|
||||
echo "\nOpen Sans"
|
||||
# we drop the wdth axis for all
|
||||
|
||||
instance_and_subset "${ORIGINAL_FONTS}/OpenSans[wdth,wght].ttf" "wdth=drop wght=400:700" "OpenSans[wght]"
|
||||
|
||||
# we restrict the wght range
|
||||
instance_and_subset "${ORIGINAL_FONTS}/OpenSans[wdth,wght].ttf" "wdth=drop wght=400" "OpenSans-Regular"
|
||||
instance_and_subset "${ORIGINAL_FONTS}/OpenSans[wdth,wght].ttf" "wdth=drop wght=700" "OpenSans-Bold"
|
||||
|
||||
echo "\nOpen Sans Italic"
|
||||
# we drop the wdth axis for all
|
||||
|
||||
instance_and_subset "${ORIGINAL_FONTS}/OpenSans-Italic[wdth,wght].ttf" "wdth=drop wght=400:700" "OpenSans-Italic[wght]"
|
||||
|
||||
# we restrict the wght range
|
||||
instance_and_subset "${ORIGINAL_FONTS}/OpenSans-Italic[wdth,wght].ttf" "wdth=drop wght=400" "OpenSans-RegularItalic"
|
||||
instance_and_subset "${ORIGINAL_FONTS}/OpenSans-Italic[wdth,wght].ttf" "wdth=drop wght=700" "OpenSans-BoldItalic"
|
||||
|
||||
echo "\nQuicksand"
|
||||
|
||||
instance_and_subset "${ORIGINAL_FONTS}/Quicksand[wght].ttf" "wght=400:700"
|
||||
|
||||
# we restrict the wght range
|
||||
instance_and_subset "${ORIGINAL_FONTS}/Quicksand[wght].ttf" "wght=400" "Quicksand-Regular"
|
||||
instance_and_subset "${ORIGINAL_FONTS}/Quicksand[wght].ttf" "wght=600" "Quicksand-SemiBold"
|
||||
instance_and_subset "${ORIGINAL_FONTS}/Quicksand[wght].ttf" "wght=700" "Quicksand-Bold"
|
||||
|
||||
echo "\nSubsetting files complete"
|
||||
|
||||
# remove temp folder
|
||||
rm -r $TEMP_FOLDER
|
16
scripts/serve-dist.js
Normal file
16
scripts/serve-dist.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
const path = require('path')
|
||||
const express = require('express')
|
||||
const app = express()
|
||||
|
||||
const p = path.join(__dirname, '..', 'dist-dev')
|
||||
const port = 4173
|
||||
|
||||
app.use(express.static(p))
|
||||
// Handle urls set by the frontend
|
||||
app.get('*', (request, response, next) => {
|
||||
response.sendFile(`${p}/index.html`)
|
||||
})
|
||||
app.listen(port, '127.0.0.1', () => {
|
||||
console.log(`Serving files from ${p}`)
|
||||
console.log(`Server started on port ${port}`)
|
||||
})
|
51
src/App.vue
51
src/App.vue
|
@ -8,72 +8,63 @@
|
|||
<no-auth-wrapper v-else>
|
||||
<router-view/>
|
||||
</no-auth-wrapper>
|
||||
<Notification/>
|
||||
|
||||
<keyboard-shortcuts v-if="keyboardShortcutsActive"/>
|
||||
|
||||
<Teleport to="body">
|
||||
<UpdateNotification/>
|
||||
<Notification/>
|
||||
</Teleport>
|
||||
</ready>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {computed, watch} from 'vue'
|
||||
import {useRoute, useRouter} from 'vue-router'
|
||||
import {computed, watch, Ref} from 'vue'
|
||||
import {useRouter} from 'vue-router'
|
||||
import {useRouteQuery} from '@vueuse/router'
|
||||
import {useStore} from 'vuex'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import isTouchDevice from 'is-touch-device'
|
||||
import {success} from '@/message'
|
||||
|
||||
import Notification from '@/components/misc/notification.vue'
|
||||
import UpdateNotification from '@/components/home/UpdateNotification.vue'
|
||||
import KeyboardShortcuts from '@/components/misc/keyboard-shortcuts/index.vue'
|
||||
|
||||
import KeyboardShortcuts from './components/misc/keyboard-shortcuts/index.vue'
|
||||
import TheNavigation from '@/components/home/TheNavigation.vue'
|
||||
import ContentAuth from '@/components/home/contentAuth.vue'
|
||||
import ContentLinkShare from '@/components/home/contentLinkShare.vue'
|
||||
import ContentAuth from './components/home/contentAuth.vue'
|
||||
import ContentLinkShare from './components/home/contentLinkShare.vue'
|
||||
import NoAuthWrapper from '@/components/misc/no-auth-wrapper.vue'
|
||||
import Ready from '@/components/misc/ready.vue'
|
||||
|
||||
import {setLanguage} from '@/i18n'
|
||||
import {setLanguage} from './i18n'
|
||||
import AccountDeleteService from '@/services/accountDelete'
|
||||
import {success} from '@/message'
|
||||
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
|
||||
import {useColorScheme} from '@/composables/useColorScheme'
|
||||
import {useBodyClass} from '@/composables/useBodyClass'
|
||||
|
||||
const baseStore = useBaseStore()
|
||||
const authStore = useAuthStore()
|
||||
const store = useStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
useBodyClass('is-touch', isTouchDevice())
|
||||
const keyboardShortcutsActive = computed(() => baseStore.keyboardShortcutsActive)
|
||||
const keyboardShortcutsActive = computed(() => store.state.keyboardShortcutsActive)
|
||||
|
||||
const authUser = computed(() => authStore.authUser)
|
||||
const authLinkShare = computed(() => authStore.authLinkShare)
|
||||
const authUser = computed(() => store.getters['auth/authUser'])
|
||||
const authLinkShare = computed(() => store.getters['auth/authLinkShare'])
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
// setup account deletion verification
|
||||
const accountDeletionConfirm = computed(() => route.query?.accountDeletionConfirm as (string | undefined))
|
||||
const accountDeletionConfirm = useRouteQuery('accountDeletionConfirm') as Ref<null | string>
|
||||
watch(accountDeletionConfirm, async (accountDeletionConfirm) => {
|
||||
if (accountDeletionConfirm === undefined) {
|
||||
if (accountDeletionConfirm === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const accountDeletionService = new AccountDeleteService()
|
||||
await accountDeletionService.confirm(accountDeletionConfirm)
|
||||
success({message: t('user.deletion.confirmSuccess')})
|
||||
authStore.refreshUserInfo()
|
||||
store.dispatch('auth/refreshUserInfo')
|
||||
}, { immediate: true })
|
||||
|
||||
// setup password reset redirect
|
||||
const userPasswordReset = computed(() => route.query?.userPasswordReset as (string | undefined))
|
||||
const userPasswordReset = useRouteQuery('userPasswordReset') as Ref<null | string>
|
||||
watch(userPasswordReset, (userPasswordReset) => {
|
||||
if (userPasswordReset === undefined) {
|
||||
if (userPasswordReset === null) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -82,9 +73,9 @@ watch(userPasswordReset, (userPasswordReset) => {
|
|||
}, { immediate: true })
|
||||
|
||||
// setup email verification redirect
|
||||
const userEmailConfirm = computed(() => route.query?.userEmailConfirm as (string | undefined))
|
||||
const userEmailConfirm = useRouteQuery('userEmailConfirm') as Ref<null | string>
|
||||
watch(userEmailConfirm, (userEmailConfirm) => {
|
||||
if (userEmailConfirm === undefined) {
|
||||
if (userEmailConfirm === null) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,58 +0,0 @@
|
|||
<script lang="ts" setup>
|
||||
import {logEvent} from 'histoire/client'
|
||||
import {reactive} from 'vue'
|
||||
import {createRouter, createMemoryHistory} from 'vue-router'
|
||||
import BaseButton from './BaseButton.vue'
|
||||
|
||||
function setupApp({ app }) {
|
||||
// Router mock
|
||||
app.use(createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{ path: '/', name: 'home', component: { render: () => null } },
|
||||
],
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
const state = reactive({
|
||||
disabled: false,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story :setup-app="setupApp" :layout="{ type: 'grid', width: '200px' }">
|
||||
<Variant title="custom">
|
||||
<template #controls>
|
||||
<HstCheckbox v-model="state.disabled" title="Disabled" />
|
||||
</template>
|
||||
<BaseButton :disabled="state.disabled">
|
||||
Hello!
|
||||
</BaseButton>
|
||||
</Variant>
|
||||
|
||||
<Variant title="disabled">
|
||||
<BaseButton disabled>
|
||||
Hello!
|
||||
</BaseButton>
|
||||
</Variant>
|
||||
|
||||
<Variant title="router link">
|
||||
<BaseButton :to="'home'">
|
||||
Hello!
|
||||
</BaseButton>
|
||||
</Variant>
|
||||
|
||||
<Variant title="external link">
|
||||
<BaseButton href="https://vikunja.io">
|
||||
Hello!
|
||||
</BaseButton>
|
||||
</Variant>
|
||||
|
||||
<Variant title="button">
|
||||
<BaseButton @click="logEvent('Click', $event)">
|
||||
Hello!
|
||||
</BaseButton>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
|
@ -1,53 +1,23 @@
|
|||
<!-- a disabled link of any kind is not a link -->
|
||||
<!-- we have a router link -->
|
||||
<!-- just a normal link -->
|
||||
<!-- a button it shall be -->
|
||||
<!-- note that we only pass the click listener here -->
|
||||
<template>
|
||||
<div
|
||||
v-if="disabled === true && (to !== undefined || href !== undefined)"
|
||||
<component
|
||||
:is="componentNodeName"
|
||||
class="base-button"
|
||||
:aria-disabled="disabled || undefined"
|
||||
ref="button"
|
||||
>
|
||||
<slot/>
|
||||
</div>
|
||||
<router-link
|
||||
v-else-if="to !== undefined"
|
||||
:to="to"
|
||||
class="base-button"
|
||||
ref="button"
|
||||
>
|
||||
<slot/>
|
||||
</router-link>
|
||||
<a v-else-if="href !== undefined"
|
||||
class="base-button"
|
||||
:href="href"
|
||||
rel="noreferrer noopener nofollow"
|
||||
target="_blank"
|
||||
ref="button"
|
||||
>
|
||||
<slot/>
|
||||
</a>
|
||||
<button
|
||||
v-else
|
||||
:type="type"
|
||||
class="base-button base-button--type-button"
|
||||
:class="{ 'base-button--type-button': isButton }"
|
||||
v-bind="elementBindings"
|
||||
:disabled="disabled || undefined"
|
||||
ref="button"
|
||||
@click="(event: MouseEvent) => emit('click', event)"
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
const BASE_BUTTON_TYPES_MAP = {
|
||||
BUTTON: 'button',
|
||||
SUBMIT: 'submit',
|
||||
} as const
|
||||
import {defineComponent} from 'vue'
|
||||
|
||||
export type BaseButtonTypes = typeof BASE_BUTTON_TYPES_MAP[keyof typeof BASE_BUTTON_TYPES_MAP] | undefined
|
||||
// see https://v3.vuejs.org/api/sfc-script-setup.html#usage-alongside-normal-script
|
||||
export default defineComponent({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
@ -55,36 +25,75 @@ export type BaseButtonTypes = typeof BASE_BUTTON_TYPES_MAP[keyof typeof BASE_BUT
|
|||
// by doing so we make it easy abstract the functionality from style and enable easier and semantic
|
||||
// correct button and link usage. Also see: https://css-tricks.com/a-complete-guide-to-links-and-buttons/#accessibility-considerations
|
||||
|
||||
// the component tries to heuristically determine what it should be checking the props
|
||||
// the component tries to heuristically determine what it should be checking the props (see the
|
||||
// componentNodeName and elementBindings ref for this).
|
||||
|
||||
// NOTE: Do NOT use buttons with @click to push routes. => Use router-links instead!
|
||||
|
||||
import {unrefElement} from '@vueuse/core'
|
||||
import {ref, type HTMLAttributes} from 'vue'
|
||||
import type {RouteLocationRaw} from 'vue-router'
|
||||
import { ref, watchEffect, computed, useAttrs, PropType } from 'vue'
|
||||
|
||||
export interface BaseButtonProps extends HTMLAttributes {
|
||||
type?: BaseButtonTypes
|
||||
disabled?: boolean
|
||||
to?: RouteLocationRaw
|
||||
href?: string
|
||||
const BASE_BUTTON_TYPES_MAP = Object.freeze({
|
||||
button: 'button',
|
||||
submit: 'submit',
|
||||
})
|
||||
|
||||
type BaseButtonTypes = keyof typeof BASE_BUTTON_TYPES_MAP
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String as PropType<BaseButtonTypes>,
|
||||
default: 'button',
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
const componentNodeName = ref<Node['nodeName']>('button')
|
||||
interface ElementBindings {
|
||||
type?: string;
|
||||
rel?: string;
|
||||
target?: string;
|
||||
}
|
||||
|
||||
export interface BaseButtonEmits {
|
||||
(e: 'click', payload: MouseEvent): void
|
||||
const elementBindings = ref({})
|
||||
|
||||
const attrs = useAttrs()
|
||||
watchEffect(() => {
|
||||
// by default this component is a button element with the attribute of the type "button" (default prop value)
|
||||
let nodeName = 'button'
|
||||
let bindings: ElementBindings = {type: props.type}
|
||||
|
||||
// if we find a "to" prop we set it as router-link
|
||||
if ('to' in attrs) {
|
||||
nodeName = 'router-link'
|
||||
bindings = {}
|
||||
}
|
||||
|
||||
const {
|
||||
type = BASE_BUTTON_TYPES_MAP.BUTTON,
|
||||
disabled = false,
|
||||
} = defineProps<BaseButtonProps>()
|
||||
// if there is a href we assume the user wants an external link via a link element
|
||||
// we also set a predefined value for the attribute rel, but make it possible to overwrite this by the user.
|
||||
if ('href' in attrs) {
|
||||
nodeName = 'a'
|
||||
bindings = {
|
||||
rel: 'noreferrer noopener nofollow',
|
||||
target: '_blank',
|
||||
}
|
||||
}
|
||||
|
||||
const emit = defineEmits<BaseButtonEmits>()
|
||||
componentNodeName.value = nodeName
|
||||
elementBindings.value = {
|
||||
...bindings,
|
||||
...attrs,
|
||||
}
|
||||
})
|
||||
|
||||
const button = ref<HTMLElement | null>(null)
|
||||
const isButton = computed(() => componentNodeName.value === 'button')
|
||||
|
||||
const button = ref()
|
||||
function focus() {
|
||||
unrefElement(button)?.focus()
|
||||
button.value.focus()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
|
@ -114,7 +123,7 @@ defineExpose({
|
|||
user-select: none;
|
||||
pointer-events: auto; // disable possible resets
|
||||
|
||||
&:focus, &.is-focused {
|
||||
&:focus {
|
||||
outline: transparent;
|
||||
}
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user