Compare commits

..

No commits in common. "main" and "v0.20.3" have entirely different histories.

433 changed files with 27241 additions and 39323 deletions

View File

@ -15,7 +15,6 @@ trigger:
services:
- name: api
image: vikunja/api:unstable
pull: always
environment:
VIKUNJA_SERVICE_TESTINGTOKEN: averyLongSecretToSe33dtheDB
VIKUNJA_LOG_LEVEL: DEBUG
@ -42,12 +41,11 @@ steps:
# - .cache
- name: dependencies
image: node:20.11.0-alpine
image: node:18-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
CYPRESS_CACHE_FOLDER: .cache/cypress
PUPPETEER_SKIP_DOWNLOAD: true
commands:
- corepack enable && pnpm config set store-dir .cache/pnpm
- pnpm install --fetch-timeout 100000
@ -55,7 +53,7 @@ steps:
# - restore-cache
- name: lint
image: node:20.11.0-alpine
image: node:18-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -66,7 +64,7 @@ steps:
- dependencies
- name: build-prod
image: node:20.11.0-alpine
image: node:18-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -77,7 +75,7 @@ steps:
- dependencies
- name: test-unit
image: node:20.11.0-alpine
image: node:18-alpine
pull: always
commands:
- corepack enable && pnpm config set store-dir .cache/pnpm
@ -87,7 +85,7 @@ steps:
- name: typecheck
failure: ignore
image: node:20.11.0-alpine
image: node:18-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -137,9 +135,8 @@ steps:
# - dependencies
- name: deploy-preview
image: williamjackson/netlify-cli
image: node:18-alpine
pull: always
user: root # The rest runs as root and thus the permissions wouldn't work
environment:
NETLIFY_AUTH_TOKEN:
from_secret: netlify_auth_token
@ -202,15 +199,10 @@ steps:
# - .cache
- name: build
image: node:20.11.0-alpine
image: node:18-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
SENTRY_AUTH_TOKEN:
from_secret: sentry_auth_token
SENTRY_ORG: vikunja
SENTRY_PROJECT: frontend-oss
PUPPETEER_SKIP_DOWNLOAD: true
commands:
- apk add git
- corepack enable && pnpm config set store-dir .cache/pnpm
@ -226,7 +218,6 @@ steps:
image: kolaente/zip
pull: always
commands:
- cp src/version.json dist
- cd dist
- zip -r ../vikunja-frontend-unstable.zip *
- cd ..
@ -285,14 +276,10 @@ steps:
# - .cache
- name: build
image: node:20.11.0-alpine
image: node:18-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
SENTRY_AUTH_TOKEN:
from_secret: sentry_auth_token
SENTRY_ORG: vikunja
SENTRY_PROJECT: frontend-oss
commands:
- apk add git
- corepack enable && pnpm config set store-dir .cache/pnpm
@ -308,7 +295,6 @@ steps:
image: kolaente/zip
pull: always
commands:
- cp src/version.json dist
- cd dist
- zip -r ../vikunja-frontend-${DRONE_TAG##v}.zip *
- cd ..
@ -360,7 +346,8 @@ type: docker
name: docker-release
depends_on:
- build
- release-latest
- release-version
trigger:
ref:
@ -388,7 +375,8 @@ steps:
repo: vikunja/frontend
tags: unstable
build_args:
- USE_RELEASE=false
- USE_RELEASE=true
- RELEASE_VERSION=unstable
platforms:
- linux/386
- linux/amd64
@ -422,7 +410,8 @@ steps:
from_secret: docker_password
repo: vikunja/frontend
build_args:
- USE_RELEASE=false
- USE_RELEASE=true
- RELEASE_VERSION=${DRONE_TAG##v}
platforms:
- linux/386
- linux/amd64
@ -475,25 +464,24 @@ name: update-translations
trigger:
branch:
include:
- main
- main
event:
include:
- cron
- cron
cron:
- update_translations
steps:
- name: download
pull: always
image: ghcr.io/kolaente/kolaente/drone-crowdin-v2:latest
image: jonasfranz/crowdin
settings:
crowdin_key:
download: true
export_dir: src/i18n/lang/
ignore_branch: true
project_identifier: vikunja
environment:
CROWDIN_KEY:
from_secret: crowdin_key
project_id: 462614
target: download
download_to: src/i18n/lang/
download_export_approved_only: true
- name: move-files
pull: always
@ -513,25 +501,26 @@ steps:
author_name: Frederick [Bot]
branch: main
commit: true
commit_message: "chore(i18n): update translations via Crowdin"
commit_message: "[skip ci] Updated translations via Crowdin"
remote: "ssh://git@kolaente.dev:9022/vikunja/frontend.git"
ssh_key:
from_secret: git_push_ssh_key
from_secret: translation_git_push_ssh_key
- name: upload
pull: always
image: ghcr.io/kolaente/kolaente/drone-crowdin-v2:latest
image: jonasfranz/crowdin
depends_on:
- clone
settings:
crowdin_key:
from_secret: crowdin_key
project_id: 462614
target: upload
upload_files:
src/i18n/lang/en.json: en.json
files:
en.json: src/i18n/lang/en.json
ignore_branch: true
project_identifier: vikunja
environment:
CROWDIN_KEY:
from_secret: crowdin_key
---
kind: signature
hmac: a044c7c4db3c2a11299d4d118397e9d25be36db241723a1bbd0a2f9cc90ffdac
hmac: 971875b90c7bb1649d1b00d022d0b594ba9b68f927bf8f0dbe840190816d676b
...

View File

@ -1,12 +1,8 @@
# (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
# Duplicate this file and remove the '.example' suffix.
# Adjust the values as needed.
# (2) Comment in and adjust the values as needed.
# VITE_IS_ONLINE=true
# SENTRY_AUTH_TOKEN=YOUR_TOKEN
# SENTRY_ORG=vikunja
# SENTRY_PROJECT=frontend-oss
# VIKUNJA_FRONTEND_BASE=/custom-subpath
VITE_IS_ONLINE=true
VITE_WORKBOX_DEBUG=false
SENTRY_AUTH_TOKEN=YOUR_TOKEN
SENTRY_ORG=vikunja
SENTRY_PROJECT=frontend-oss

View File

@ -1,5 +1,5 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
require("@rushstack/eslint-patch/modern-module-resolution")
module.exports = {
'root': true,
@ -7,54 +7,52 @@ module.exports = {
'browser': true,
'es2022': true,
'node': true,
'vue/setup-compiler-macros': true,
},
'extends': [
'eslint:recommended',
'plugin:vue/vue3-recommended',
'plugin:vue/vue3-essential',
'@vue/eslint-config-typescript/recommended',
],
'rules': {
'quotes': ['error', 'single'],
'comma-dangle': ['error', 'always-multiline'],
'semi': ['error', 'never'],
'vue/v-on-event-hyphenation': ['warn', 'never', { 'autofix': true }],
'vue/multi-word-component-names': 'off',
// uncategorized rules:
'vue/component-api-style': ['error', ['script-setup']],
'vue/component-name-in-template-casing': ['warn', 'PascalCase'],
'vue/custom-event-name-casing': ['error', 'camelCase'],
'vue/define-macros-order': 'error',
'vue/match-component-file-name': ['error', {
'extensions': ['.js', '.jsx', '.ts', '.tsx', '.vue'],
'shouldMatchCase': true,
}],
'vue/no-boolean-default': ['warn', 'default-false'],
'vue/match-component-import-name': 'error',
'vue/prefer-separate-static-class': 'warn',
'vue/padding-line-between-blocks': 'error',
'vue/next-tick-style': ['error', 'promise'],
'vue/block-lang': [
'vue/html-quotes': [
'error',
{ 'script': { 'lang': 'ts' } },
'double',
],
'quotes': [
'error',
'single',
],
'comma-dangle': [
'error',
'always-multiline',
],
'semi': [
'error',
'never',
],
'vue/no-required-prop-with-default': ['error', { 'autofix': true }],
'vue/no-duplicate-attr-inheritance': 'error',
'vue/no-empty-component-block': 'error',
'vue/html-indent': ['error', 'tab'],
// vue3
'vue/no-ref-object-destructure': 'error',
// 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': 'latest',
'ecmaVersion': 2022,
'sourceType': 'module',
},
'ignorePatterns': [
'*.test.*',
'cypress/*',
],
'globals': {
'defineProps': 'readonly',
},
}

3
.github/FUNDING.yml vendored
View File

@ -1,3 +1,2 @@
github: kolaente
open_collective: vikunja
custom: ["https://vikunja.cloud", "https://www.buymeacoffee.com/kolaente"]
custom: https://www.buymeacoffee.com/kolaente

View File

@ -1,23 +0,0 @@
name: 'Repo Lockdown'
on:
pull_request_target:
types: opened
permissions:
issues: write
pull-requests: write
jobs:
action:
runs-on: ubuntu-latest
steps:
- uses: dessant/repo-lockdown@v4
with:
pr-comment: 'Hi! Thank you for your contribution.
This repo is only a mirror and unfortunately we can''t accept PRs made here. Please re-submit your changes to [our Gitea instance](https://kolaente.dev/vikunja/frontend/pulls).
Also check out the [contribution guidelines](https://vikunja.io/docs/development/#pull-requests).
Thank you for your understanding.'

14
.npmrc
View File

@ -1,14 +1,2 @@
fetch-timeout=100000
# pnpm settings
# The following settings prepare for the new default value of pnpm 8
# they can be removed directly after having moved to pnpm 8
auto-install-peers=true
dedupe-peer-dependents=true
resolve-peers-from-workspace-root=true
save-workspace-protocol=rolling
resolution-mode=lowest-direct
publishConfig.linkDirectory=true
# remove some time after having moved to pnpm 8
use-lockfile-v6=true
fetch-timeout=100000

2
.nvmrc
View File

@ -1 +1 @@
20.11.0
v18

View File

@ -8,7 +8,6 @@
"lokalise.i18n-ally",
"mgmcdermott.vscode-language-babel",
"mikestead.dotenv",
"Syler.sass-indented",
"zixuanchen.vitest-explorer"
"Syler.sass-indented"
]
}

View File

@ -18,12 +18,6 @@
"javascriptreact",
"vue"
],
"volar.completion.preferredTagNameCase": "pascal",
// disable vetur in case it is installed
"vetur.validation.template": false,
// i18n ally
"i18n-ally.localesPaths": [
"src/i18n/lang"

File diff suppressed because it is too large Load Diff

View File

@ -1,73 +1,49 @@
# syntax=docker/dockerfile:1
# ┬─┐┬ ┐o┬ ┬─┐
# │─││ │││ │ │
# ┘─┘┘─┘┘┘─┘┘─┘
FROM --platform=$BUILDPLATFORM node:20.11.0-alpine AS builder
# Stage 1: Build application
FROM --platform=$BUILDPLATFORM node:18-alpine AS compile-image
WORKDIR /build
ARG USE_RELEASE=false
ARG RELEASE_VERSION=unstable
ARG RELEASE_VERSION=main
ENV PNPM_CACHE_FOLDER .cache/pnpm/
ENV PUPPETEER_SKIP_DOWNLOAD true
ADD . ./
COPY package.json ./
COPY pnpm-lock.yaml ./
COPY patches ./patches/
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/ && \
exit 0; \
fi && \
# https://pnpm.io/installation#using-corepack
corepack enable && \
# we don't use corepack prepare here by intend since
# we have renovate to keep our dependencies up to date
# Build the frontend
pnpm install && \
apk add --no-cache git && \
echo '{"VERSION": "'$(git describe --tags --always --abbrev=10 | sed 's/-/+/' | sed 's/^v//' | sed 's/-g/-/')'"}' > src/version.json && \
pnpm run build
RUN if [ "$USE_RELEASE" != true ]; then \
# https://pnpm.io/installation#using-corepack
corepack enable && \
pnpm install; \
fi
# Stage 2: copy
FROM nginx:alpine
COPY . ./
COPY nginx.conf /etc/nginx/nginx.conf
COPY scripts/run.sh /run.sh
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
# copy compiled files from stage 1
COPY --from=compile-image /build/dist /usr/share/nginx/html
RUN if [ "$USE_RELEASE" = true ]; then \
wget "https://dl.vikunja.io/frontend/vikunja-frontend-${RELEASE_VERSION}.zip" -O frontend-release.zip && \
unzip frontend-release.zip -d dist/; \
else \
# we don't use corepack prepare here by intend since
# we have renovate to keep our dependencies up to date
# Build the frontend
pnpm run build; \
fi
# Unprivileged user
ENV PUID 1000
ENV PGID 1000
# ┌┐┐┌─┐o┌┐┐┐ │
# ││││ ┬││││┌┼┘
# ┘└┘┘─┘┘┘└┘┘ └
FROM nginx:stable-alpine AS runner
WORKDIR /usr/share/nginx/html
LABEL maintainer="maintainers@vikunja.io"
ENV VIKUNJA_HTTP_PORT 80
ENV VIKUNJA_HTTP2_PORT 81
ENV VIKUNJA_LOG_FORMAT main
ENV VIKUNJA_API_URL /api/v1
ENV VIKUNJA_SENTRY_ENABLED false
ENV VIKUNJA_SENTRY_DSN https://85694a2d757547cbbc90cd4b55c5a18d@o1047380.ingest.sentry.io/6024480
ENV VIKUNJA_PROJECT_INFINITE_NESTING_ENABLED false
ENV VIKUNJA_ALLOW_ICON_CHANGES true
ENV VIKUNJA_CUSTOM_LOGO_URL "''"
RUN apk add --no-cache \
# for sh file
bash \
# installs usermod and groupmod
shadow
COPY docker/injector.sh /docker-entrypoint.d/50-injector.sh
COPY docker/ipv6-disable.sh /docker-entrypoint.d/60-ipv6-disable.sh
COPY docker/nginx.conf /etc/nginx/nginx.conf
COPY docker/templates/. /etc/nginx/templates/
# copy compiled files from stage 1
COPY --from=builder /build/dist ./
# manage permissions
RUN chmod 0755 /docker-entrypoint.d/*.sh /etc/nginx/templates && \
chmod -R 0644 /etc/nginx/nginx.conf && \
chown -R nginx:nginx ./ /etc/nginx/conf.d /etc/nginx/templates && \
rm -f /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
CMD "/run.sh"

View File

@ -1,14 +1,10 @@
# This repository was merged with the api and is now archived
You can find the new (old) code over on [vikunja/vikunja](https://kolaente.dev/vikunja/vikunja).
# Web frontend for Vikunja
> The todo app to organize your life.
[![Build Status](https://drone.kolaente.de/api/badges/vikunja/frontend/status.svg)](https://drone.kolaente.de/vikunja/frontend)
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](LICENSE)
[![Download](https://img.shields.io/badge/download-v0.22.1-brightgreen.svg)](https://dl.vikunja.io)
[![Download](https://img.shields.io/badge/download-v0.20.3-brightgreen.svg)](https://dl.vikunja.io)
[![Translation](https://badges.crowdin.net/vikunja/localized.svg)](https://crowdin.com/project/vikunja)
This is the web frontend for Vikunja, written in Vue.js.
@ -22,14 +18,6 @@ If you find any security-related issues you don't want to disclose publicly, ple
## Docker
There is a [docker image available](https://hub.docker.com/r/vikunja/api) with support for http/2 and aggressive caching enabled.
In order to build it from sources run the command below. (Docker >= v19.03)
```shell
export DOCKER_BUILDKIT=1
docker build -t vikunja/frontend .
```
Refer to [multi-platform documentation](https://docs.docker.com/build/building/multi-platform/) in order to build for different platforms.
## Project setup
@ -53,4 +41,8 @@ pnpm run build
```shell
pnpm run lint
```
```
## Sponsors
[![Relm](https://vikunja.io/images/sponsors/relm.png)](https://relm.us)

0
crowdin.cli Normal file
View File

View File

@ -24,5 +24,4 @@ export default defineConfig({
},
viewportWidth: 1600,
viewportHeight: 900,
experimentalMemoryManagement: true,
})

View File

@ -0,0 +1,57 @@
import {createFakeUserAndLogin} from '../../support/authenticateUser'
import {ListFactory} from '../../factories/list'
import {prepareLists} from './prepareLists'
describe('List History', () => {
createFakeUserAndLogin()
prepareLists()
it('should show a list history on the home page', () => {
cy.intercept(Cypress.env('API_URL') + '/namespaces*').as('loadNamespaces')
cy.intercept(Cypress.env('API_URL') + '/lists/*').as('loadList')
const lists = ListFactory.create(6)
cy.visit('/')
cy.wait('@loadNamespaces')
cy.get('body')
.should('not.contain', 'Last viewed')
cy.visit(`/lists/${lists[0].id}`)
cy.wait('@loadNamespaces')
cy.wait('@loadList')
cy.visit(`/lists/${lists[1].id}`)
cy.wait('@loadNamespaces')
cy.wait('@loadList')
cy.visit(`/lists/${lists[2].id}`)
cy.wait('@loadNamespaces')
cy.wait('@loadList')
cy.visit(`/lists/${lists[3].id}`)
cy.wait('@loadNamespaces')
cy.wait('@loadList')
cy.visit(`/lists/${lists[4].id}`)
cy.wait('@loadNamespaces')
cy.wait('@loadList')
cy.visit(`/lists/${lists[5].id}`)
cy.wait('@loadNamespaces')
cy.wait('@loadList')
// cy.visit('/')
// cy.wait('@loadNamespaces')
// Not using cy.visit here to work around the redirect issue fixed in #1337
cy.get('nav.menu.top-menu a')
.contains('Overview')
.click()
cy.get('body')
.should('contain', 'Last viewed')
cy.get('[data-cy="listCardGrid"]')
.should('not.contain', lists[0].title)
.should('contain', lists[1].title)
.should('contain', lists[2].title)
.should('contain', lists[3].title)
.should('contain', lists[4].title)
.should('contain', lists[5].title)
})
})

View File

@ -3,15 +3,15 @@ import {formatISO, format} from 'date-fns'
import {createFakeUserAndLogin} from '../../support/authenticateUser'
import {TaskFactory} from '../../factories/task'
import {prepareProjects} from './prepareProjects'
import {prepareLists} from './prepareLists'
describe('Project View Gantt', () => {
describe('List View Gantt', () => {
createFakeUserAndLogin()
prepareProjects()
prepareLists()
it('Hides tasks with no dates', () => {
const tasks = TaskFactory.create(1)
cy.visit('/projects/1/gantt')
cy.visit('/lists/1/gantt')
cy.get('.g-gantt-rows-container')
.should('not.contain', tasks[0].title)
@ -25,7 +25,7 @@ describe('Project View Gantt', () => {
nextMonth.setDate(1)
nextMonth.setMonth(9)
cy.visit('/projects/1/gantt')
cy.visit('/lists/1/gantt')
cy.get('.g-timeunits-container')
.should('contain', format(now, 'MMMM'))
@ -38,7 +38,7 @@ describe('Project View Gantt', () => {
start_date: now.toISOString(),
end_date: new Date(new Date(now).setDate(now.getDate() + 4)).toISOString(),
})
cy.visit('/projects/1/gantt')
cy.visit('/lists/1/gantt')
cy.get('.g-gantt-rows-container')
.should('not.be.empty')
@ -50,7 +50,7 @@ describe('Project View Gantt', () => {
start_date: null,
end_date: null,
})
cy.visit('/projects/1/gantt')
cy.visit('/lists/1/gantt')
cy.get('.gantt-options .fancycheckbox')
.contains('Show tasks which don\'t have dates set')
@ -69,7 +69,7 @@ describe('Project View Gantt', () => {
start_date: now.toISOString(),
end_date: new Date(new Date(now).setDate(now.getDate() + 4)).toISOString(),
})
cy.visit('/projects/1/gantt')
cy.visit('/lists/1/gantt')
cy.get('.g-gantt-rows-container .g-gantt-row .g-gantt-row-bars-container div .g-gantt-bar')
.first()
@ -83,9 +83,9 @@ describe('Project View Gantt', () => {
const now = Date.UTC(2022, 10, 9)
cy.clock(now, ['Date'])
cy.visit('/projects/1/gantt')
cy.visit('/lists/1/gantt')
cy.get('.project-gantt .gantt-options .field .control input.input.form-control')
cy.get('.list-gantt .gantt-options .field .control input.input.form-control')
.click()
cy.get('.flatpickr-calendar .flatpickr-innerContainer .dayContainer .flatpickr-day')
.first()
@ -99,13 +99,13 @@ describe('Project View Gantt', () => {
})
it('Should change the date range based on date query parameters', () => {
cy.visit('/projects/1/gantt?dateFrom=2022-09-25&dateTo=2022-11-05')
cy.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('.project-gantt .gantt-options .field .control input.input.form-control')
cy.get('.list-gantt .gantt-options .field .control input.input.form-control')
.should('have.value', '25 Sep 2022 to 5 Nov 2022')
})
@ -115,7 +115,7 @@ describe('Project View Gantt', () => {
start_date: formatISO(now),
end_date: formatISO(now.setDate(now.getDate() + 4)),
})
cy.visit('/projects/1/gantt')
cy.visit('/lists/1/gantt')
cy.get('.gantt-container .g-gantt-chart .g-gantt-row-bars-container .g-gantt-bar')
.dblclick()

View File

@ -1,26 +1,13 @@
import {createFakeUserAndLogin} from '../../support/authenticateUser'
import {BucketFactory} from '../../factories/bucket'
import {ProjectFactory} from '../../factories/project'
import {ListFactory} from '../../factories/list'
import {TaskFactory} from '../../factories/task'
import {prepareProjects} from './prepareProjects'
import {prepareLists} from './prepareLists'
function createSingleTaskInBucket(count = 1, attrs = {}) {
const projects = ProjectFactory.create(1)
const buckets = BucketFactory.create(2, {
project_id: projects[0].id,
})
const tasks = TaskFactory.create(count, {
project_id: projects[0].id,
bucket_id: buckets[0].id,
...attrs,
})
return tasks[0]
}
describe('Project View Kanban', () => {
describe('List View Kanban', () => {
createFakeUserAndLogin()
prepareProjects()
prepareLists()
let buckets
beforeEach(() => {
@ -29,10 +16,10 @@ describe('Project View Kanban', () => {
it('Shows all buckets with their tasks', () => {
const data = TaskFactory.create(10, {
project_id: 1,
list_id: 1,
bucket_id: 1,
})
cy.visit('/projects/1/kanban')
cy.visit('/lists/1/kanban')
cy.get('.kanban .bucket .title')
.contains(buckets[0].title)
@ -47,10 +34,10 @@ describe('Project View Kanban', () => {
it('Can add a new task to a bucket', () => {
TaskFactory.create(2, {
project_id: 1,
list_id: 1,
bucket_id: 1,
})
cy.visit('/projects/1/kanban')
cy.visit('/lists/1/kanban')
cy.get('.kanban .bucket')
.contains(buckets[0].title)
@ -68,7 +55,7 @@ describe('Project View Kanban', () => {
})
it('Can create a new bucket', () => {
cy.visit('/projects/1/kanban')
cy.visit('/lists/1/kanban')
cy.get('.kanban .bucket.new-bucket .button')
.click()
@ -82,7 +69,7 @@ describe('Project View Kanban', () => {
})
it('Can set a bucket limit', () => {
cy.visit('/projects/1/kanban')
cy.visit('/lists/1/kanban')
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger')
.first()
@ -103,7 +90,7 @@ describe('Project View Kanban', () => {
})
it('Can rename a bucket', () => {
cy.visit('/projects/1/kanban')
cy.visit('/lists/1/kanban')
cy.get('.kanban .bucket .bucket-header .title')
.first()
@ -114,7 +101,7 @@ describe('Project View Kanban', () => {
})
it('Can delete a bucket', () => {
cy.visit('/projects/1/kanban')
cy.visit('/lists/1/kanban')
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger')
.first()
@ -138,10 +125,10 @@ describe('Project View Kanban', () => {
it('Can drag tasks around', () => {
const tasks = TaskFactory.create(2, {
project_id: 1,
list_id: 1,
bucket_id: 1,
})
cy.visit('/projects/1/kanban')
cy.visit('/lists/1/kanban')
cy.get('.kanban .bucket .tasks .task')
.contains(tasks[0].title)
@ -157,10 +144,10 @@ describe('Project View Kanban', () => {
it('Should navigate to the task when the task card is clicked', () => {
const tasks = TaskFactory.create(5, {
id: '{increment}',
project_id: 1,
list_id: 1,
bucket_id: 1,
})
cy.visit('/projects/1/kanban')
cy.visit('/lists/1/kanban')
cy.get('.kanban .bucket .tasks .task')
.contains(tasks[0].title)
@ -171,18 +158,18 @@ describe('Project View Kanban', () => {
.should('contain', `/tasks/${tasks[0].id}`, { timeout: 1000 })
})
it('Should remove a task from the kanban board when moving it to another project', () => {
const projects = ProjectFactory.create(2)
it('Should remove a task from the kanban board when moving it to another list', () => {
const lists = ListFactory.create(2)
BucketFactory.create(2, {
project_id: '{increment}',
list_id: '{increment}',
})
const tasks = TaskFactory.create(5, {
id: '{increment}',
project_id: 1,
list_id: 1,
bucket_id: 1,
})
const task = tasks[0]
cy.visit('/projects/1/kanban')
cy.visit('/lists/1/kanban')
cy.get('.kanban .bucket .tasks .task')
.contains(task.title)
@ -193,7 +180,7 @@ describe('Project View Kanban', () => {
.contains('Move')
.click()
cy.get('.task-view .content.details .field .multiselect.control .input-wrapper input')
.type(`${projects[1].title}{enter}`)
.type(`${lists[1].title}{enter}`)
// The requests happen with a 200ms timeout. Because of that, the results are not yet there when cypress
// presses enter and we can't simulate pressing on enter to select the item.
cy.get('.task-view .content.details .field .multiselect.control .search-results')
@ -210,18 +197,26 @@ describe('Project View Kanban', () => {
it('Shows a button to filter the kanban board', () => {
const data = TaskFactory.create(10, {
project_id: 1,
list_id: 1,
bucket_id: 1,
})
cy.visit('/projects/1/kanban')
cy.visit('/lists/1/kanban')
cy.get('.project-kanban .filter-container .base-button')
cy.get('.list-kanban .filter-container .base-button')
.should('exist')
})
it('Should remove a task from the board when deleting it', () => {
const task = createSingleTaskInBucket(5)
cy.visit('/projects/1/kanban')
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)
@ -243,43 +238,4 @@ describe('Project View Kanban', () => {
cy.get('.kanban .bucket .tasks')
.should('not.contain', task.title)
})
it('Should show a task description icon if the task has a description', () => {
cy.intercept(Cypress.env('API_URL') + '/projects/1/buckets**').as('loadTasks')
const task = createSingleTaskInBucket(1, {
description: 'Lorem Ipsum',
})
cy.visit(`/projects/${task.project_id}/kanban`)
cy.wait('@loadTasks')
cy.get('.bucket .tasks .task .footer .icon svg')
.should('exist')
})
it('Should not show a task description icon if the task has an empty description', () => {
cy.intercept(Cypress.env('API_URL') + '/projects/1/buckets**').as('loadTasks')
const task = createSingleTaskInBucket(1, {
description: '',
})
cy.visit(`/projects/${task.project_id}/kanban`)
cy.wait('@loadTasks')
cy.get('.bucket .tasks .task .footer .icon svg')
.should('not.exist')
})
it('Should not show a task description icon if the task has a description containing only an empty p tag', () => {
cy.intercept(Cypress.env('API_URL') + '/projects/1/buckets**').as('loadTasks')
const task = createSingleTaskInBucket(1, {
description: '<p></p>',
})
cy.visit(`/projects/${task.project_id}/kanban`)
cy.wait('@loadTasks')
cy.get('.bucket .tasks .task .footer .icon svg')
.should('not.exist')
})
})

View File

@ -1,32 +1,32 @@
import {createFakeUserAndLogin} from '../../support/authenticateUser'
import {UserProjectFactory} from '../../factories/users_project'
import {UserListFactory} from '../../factories/users_list'
import {TaskFactory} from '../../factories/task'
import {UserFactory} from '../../factories/user'
import {ProjectFactory} from '../../factories/project'
import {prepareProjects} from './prepareProjects'
import {ListFactory} from '../../factories/list'
import {prepareLists} from './prepareLists'
describe('Project View Project', () => {
describe('List View List', () => {
createFakeUserAndLogin()
prepareProjects()
prepareLists()
it('Should be an empty project', () => {
cy.visit('/projects/1')
it('Should be an empty list', () => {
cy.visit('/lists/1')
cy.url()
.should('contain', '/projects/1/list')
cy.get('.project-title')
.should('contain', 'First Project')
cy.get('.project-title-dropdown')
.should('contain', '/lists/1/list')
cy.get('.list-title h1')
.should('contain', 'First List')
cy.get('.list-title .dropdown')
.should('exist')
cy.get('p')
.contains('This project is currently empty.')
.contains('This list is currently empty.')
.should('exist')
})
it('Should create a new task', () => {
const newTaskTitle = 'New task'
cy.visit('/projects/1')
cy.visit('/lists/1')
cy.get('.task-add textarea')
.type(newTaskTitle+'{enter}')
cy.get('.tasks')
@ -36,9 +36,9 @@ describe('Project View Project', () => {
it('Should navigate to the task when the title is clicked', () => {
const tasks = TaskFactory.create(5, {
id: '{increment}',
project_id: 1,
list_id: 1,
})
cy.visit('/projects/1/list')
cy.visit('/lists/1/list')
cy.get('.tasks .task .tasktext')
.contains(tasks[0].title)
@ -49,32 +49,33 @@ describe('Project View Project', () => {
.should('contain', `/tasks/${tasks[0].id}`)
})
it('Should not see any elements for a project which is shared read only', () => {
it('Should not see any elements for a list which is shared read only', () => {
UserFactory.create(2)
UserProjectFactory.create(1, {
project_id: 2,
UserListFactory.create(1, {
list_id: 2,
user_id: 1,
right: 0,
})
const projects = ProjectFactory.create(2, {
const lists = ListFactory.create(2, {
owner_id: '{increment}',
namespace_id: '{increment}',
})
cy.visit(`/projects/${projects[1].id}/`)
cy.visit(`/lists/${lists[1].id}/`)
cy.get('.project-title-wrapper .icon')
cy.get('.list-title .icon')
.should('not.exist')
cy.get('input.input[placeholder="Add a new task..."')
.should('not.exist')
})
it('Should only show the color of a project in the navigation and not in the list view', () => {
const projects = ProjectFactory.create(1, {
it('Should only show the color of a list in the navigation and not in the list view', () => {
const lists = ListFactory.create(1, {
hex_color: '00db60',
})
TaskFactory.create(10, {
project_id: projects[0].id,
list_id: lists[0].id,
})
cy.visit(`/projects/${projects[0].id}/`)
cy.visit(`/lists/${lists[0].id}/`)
cy.get('.menu-list li .list-menu-link .color-bubble')
.should('have.css', 'background-color', 'rgb(0, 219, 96)')
@ -86,9 +87,9 @@ describe('Project View Project', () => {
const tasks = TaskFactory.create(100, {
id: '{increment}',
title: i => `task${i}`,
project_id: 1,
list_id: 1,
})
cy.visit('/projects/1/list')
cy.visit('/lists/1/list')
cy.get('.tasks')
.should('contain', tasks[1].title)

View File

@ -2,37 +2,37 @@ import {createFakeUserAndLogin} from '../../support/authenticateUser'
import {TaskFactory} from '../../factories/task'
describe('Project View Table', () => {
describe('List View Table', () => {
createFakeUserAndLogin()
it('Should show a table with tasks', () => {
const tasks = TaskFactory.create(1)
cy.visit('/projects/1/table')
cy.visit('/lists/1/table')
cy.get('.project-table table.table')
cy.get('.list-table table.table')
.should('exist')
cy.get('.project-table table.table')
cy.get('.list-table table.table')
.should('contain', tasks[0].title)
})
it('Should have working column switches', () => {
TaskFactory.create(1)
cy.visit('/projects/1/table')
cy.visit('/lists/1/table')
cy.get('.project-table .filter-container .items .button')
cy.get('.list-table .filter-container .items .button')
.contains('Columns')
.click()
cy.get('.project-table .filter-container .card.columns-filter .card-content .fancycheckbox')
cy.get('.list-table .filter-container .card.columns-filter .card-content .fancycheckbox .check')
.contains('Priority')
.click()
cy.get('.project-table .filter-container .card.columns-filter .card-content .fancycheckbox')
cy.get('.list-table .filter-container .card.columns-filter .card-content .fancycheckbox .check')
.contains('Done')
.click()
cy.get('.project-table table.table th')
cy.get('.list-table table.table th')
.contains('Priority')
.should('exist')
cy.get('.project-table table.table th')
cy.get('.list-table table.table th')
.contains('Done')
.should('not.exist')
})
@ -40,11 +40,11 @@ describe('Project View Table', () => {
it('Should navigate to the task when the title is clicked', () => {
const tasks = TaskFactory.create(5, {
id: '{increment}',
project_id: 1,
list_id: 1,
})
cy.visit('/projects/1/table')
cy.visit('/lists/1/table')
cy.get('.project-table table.table')
cy.get('.list-table table.table')
.contains(tasks[0].title)
.click()

View File

@ -0,0 +1,122 @@
import {createFakeUserAndLogin} from '../../support/authenticateUser'
import {TaskFactory} from '../../factories/task'
import {prepareLists} from './prepareLists'
describe('Lists', () => {
createFakeUserAndLogin()
let lists
prepareLists((newLists) => (lists = newLists))
it('Should create a new list', () => {
cy.visit('/')
cy.get('.namespace-title .dropdown-trigger')
.click()
cy.get('.namespace-title .dropdown .dropdown-item')
.contains('New list')
.click()
cy.url()
.should('contain', '/lists/new/1')
cy.get('.card-header-title')
.contains('New list')
cy.get('input.input')
.type('New List')
cy.get('.button')
.contains('Create')
.click()
cy.get('.global-notification', { timeout: 1000 }) // Waiting until the request to create the new list is done
.should('contain', 'Success')
cy.url()
.should('contain', '/lists/')
cy.get('.list-title h1')
.should('contain', 'New List')
})
it('Should redirect to a specific list view after visited', () => {
cy.visit('/lists/1/kanban')
cy.url()
.should('contain', '/lists/1/kanban')
cy.visit('/lists/1')
cy.url()
.should('contain', '/lists/1/kanban')
})
it('Should rename the list in all places', () => {
TaskFactory.create(5, {
id: '{increment}',
list_id: 1,
})
const newListName = 'New list name'
cy.visit('/lists/1')
cy.get('.list-title h1')
.should('contain', 'First List')
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .menu-list-dropdown-trigger')
.click()
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .dropdown-content')
.contains('Edit')
.click()
cy.get('#title')
.type(`{selectall}${newListName}`)
cy.get('footer.card-footer .button')
.contains('Save')
.click()
cy.get('.global-notification')
.should('contain', 'Success')
cy.get('.list-title h1')
.should('contain', newListName)
.should('not.contain', lists[0].title)
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child')
.should('contain', newListName)
.should('not.contain', lists[0].title)
cy.visit('/')
cy.get('.card-content')
.should('contain', newListName)
.should('not.contain', lists[0].title)
})
it('Should remove a list', () => {
cy.visit(`/lists/${lists[0].id}`)
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .menu-list-dropdown-trigger')
.click()
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .dropdown-content')
.contains('Delete')
.click()
cy.url()
.should('contain', '/settings/delete')
cy.get('[data-cy="modalPrimary"]')
.contains('Do it')
.click()
cy.get('.global-notification')
.should('contain', 'Success')
cy.get('.namespace-container .menu.namespaces-lists .menu-list')
.should('not.contain', lists[0].title)
cy.location('pathname')
.should('equal', '/')
})
it('Should archive a list', () => {
cy.visit(`/lists/${lists[0].id}`)
cy.get('.list-title .dropdown')
.click()
cy.get('.list-title .dropdown .dropdown-menu .dropdown-item')
.contains('Archive')
.click()
cy.get('.modal-content')
.should('contain.text', 'Archive this list')
cy.get('.modal-content [data-cy=modalPrimary]')
.click()
cy.get('.namespace-container .menu.namespaces-lists .menu-list')
.should('not.contain', lists[0].title)
cy.get('main.app-content')
.should('contain.text', 'This list is archived. It is not possible to create new or edit tasks for it.')
})
})

View File

@ -0,0 +1,145 @@
import {createFakeUserAndLogin} from '../../support/authenticateUser'
import {ListFactory} from '../../factories/list'
import {NamespaceFactory} from '../../factories/namespace'
describe('Namepaces', () => {
createFakeUserAndLogin()
let namespaces
beforeEach(() => {
namespaces = NamespaceFactory.create(1)
ListFactory.create(1)
})
it('Should be all there', () => {
cy.visit('/namespaces')
cy.get('[data-cy="namespace-title"]')
.should('contain', namespaces[0].title)
})
it('Should create a new Namespace', () => {
const newNamespaceTitle = 'New Namespace'
cy.visit('/namespaces')
cy.get('[data-cy="new-namespace"]')
.should('contain', 'New namespace')
.click()
cy.url()
.should('contain', '/namespaces/new')
cy.get('.card-header-title')
.should('contain', 'New namespace')
cy.get('input.input')
.type(newNamespaceTitle)
cy.get('.button')
.contains('Create')
.click()
cy.get('.global-notification')
.should('contain', 'Success')
cy.get('.namespace-container')
.should('contain', newNamespaceTitle)
cy.url()
.should('contain', '/namespaces')
})
it('Should rename the namespace all places', () => {
const newNamespaces = NamespaceFactory.create(5)
const newNamespaceName = 'New namespace name'
cy.visit('/namespaces')
cy.get(`.namespace-container .menu.namespaces-lists .namespace-title:contains(${newNamespaces[0].title}) .dropdown .dropdown-trigger`)