diff --git a/.drone.yml b/.drone.yml index 27e99bade..76da86f6d 100644 --- a/.drone.yml +++ b/.drone.yml @@ -39,7 +39,7 @@ steps: # - '.cache' - name: dependencies - image: node:16 + image: node:18 pull: true environment: YARN_CACHE_FOLDER: .cache/yarn/ @@ -70,7 +70,7 @@ steps: # - dependencies - name: lint - image: node:16 + image: node:18 pull: true environment: YARN_CACHE_FOLDER: .cache/yarn/ @@ -81,7 +81,7 @@ steps: - dependencies - name: build-prod - image: node:16 + image: node:18 pull: true environment: YARN_CACHE_FOLDER: .cache/yarn/ @@ -91,13 +91,22 @@ steps: - dependencies - name: test-unit - image: node:16 + 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 @@ -107,38 +116,17 @@ steps: 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:5000 - - yarn test:frontend --browser chrome + - yarn serve:dist & npx wait-on http://localhost:4173 + - yarn test:frontend --browser chrome --record depends_on: - - dependencies - build-prod - - name: upload-test-results - image: plugins/s3 - pull: true - settings: - bucket: drone-test-results - access_key: - from_secret: test_results_aws_access_key_id - secret_key: - from_secret: test_results_aws_secret_access_key - endpoint: https://s3.fr-par.scw.cloud - region: fr-par - path_style: true - source: cypress/screenshots/**/**/* - strip_prefix: cypress/screenshots/ - target: /${DRONE_REPO}/${DRONE_PULL_REQUEST}_${DRONE_BRANCH}/${DRONE_BUILD_NUMBER}/ - depends_on: - - test-frontend - when: - status: - - failure - - success - - name: deploy-preview - image: node:16 + image: node:18 pull: true environment: NETLIFY_AUTH_TOKEN: @@ -148,6 +136,9 @@ steps: GITEA_TOKEN: from_secret: gitea_token commands: + - cp -r dist dist-preview + # Override the default api url used for preview + - 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: @@ -195,14 +186,13 @@ steps: # - '.cache' - name: build - image: node:16 + image: node:18 pull: true group: build-static environment: YARN_CACHE_FOLDER: .cache/yarn/ commands: - yarn --frozen-lockfile --network-timeout 100000 - - npx browserslist@latest --update-db - yarn run lint - "echo '{\"VERSION\": \"'$(git describe --tags --always --abbrev=10 | sed 's/-/+/' | sed 's/^v//' | sed 's/-g/-/')'\"}' > src/version.json" - yarn run build @@ -271,7 +261,7 @@ steps: # - '.cache' - name: build - image: node:16 + image: node:18 pull: true group: build-static environment: @@ -657,6 +647,6 @@ steps: from_secret: crowdin_key --- kind: signature -hmac: 188ee90100c5fc5922a445e531e7a47453121edddb2a64a182eb23ed2bf602de +hmac: 997e1badebe484ac29557c4af356e63db4d3d57f3d32e92d482f117f8cec64da ... diff --git a/.nvmrc b/.nvmrc index 5edcff036..0828ab794 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v16 \ No newline at end of file +v18 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 159930f6a..b4d851e19 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Stage 1: Build application -FROM node:16 AS compile-image +FROM node:18 AS compile-image WORKDIR /build diff --git a/cypress.json b/cypress.json index 27f12495c..48eb6ac59 100644 --- a/cypress.json +++ b/cypress.json @@ -1,5 +1,5 @@ { - "baseUrl": "http://localhost:5000", + "baseUrl": "http://localhost:4173", "env": { "API_URL": "http://localhost:3456/api/v1", "TEST_SECRET": "averyLongSecretToSe33dtheDB" @@ -7,5 +7,6 @@ "video": false, "retries": { "runMode": 2 - } + }, + "projectId": "181c7x" } diff --git a/cypress/factories/bucket.js b/cypress/factories/bucket.js index be90cca99..8001899b4 100644 --- a/cypress/factories/bucket.js +++ b/cypress/factories/bucket.js @@ -1,4 +1,4 @@ -import faker from 'faker' +import faker from '@faker-js/faker' import {Factory} from '../support/factory' import {formatISO} from 'date-fns' diff --git a/cypress/factories/labels.js b/cypress/factories/labels.js index b3f9ab30f..7aac5eb09 100644 --- a/cypress/factories/labels.js +++ b/cypress/factories/labels.js @@ -1,4 +1,4 @@ -import faker from 'faker' +import faker from '@faker-js/faker' import {Factory} from '../support/factory' import {formatISO} from 'date-fns' diff --git a/cypress/factories/link_sharing.js b/cypress/factories/link_sharing.js index e2c01dd07..3a406ea22 100644 --- a/cypress/factories/link_sharing.js +++ b/cypress/factories/link_sharing.js @@ -1,6 +1,6 @@ import {Factory} from '../support/factory' import {formatISO} from "date-fns" -import faker from 'faker' +import faker from '@faker-js/faker' export class LinkShareFactory extends Factory { static table = 'link_shares' diff --git a/cypress/factories/list.js b/cypress/factories/list.js index f93cdba4c..2ffc31256 100644 --- a/cypress/factories/list.js +++ b/cypress/factories/list.js @@ -1,6 +1,6 @@ import {Factory} from '../support/factory' import {formatISO} from "date-fns" -import faker from 'faker' +import faker from '@faker-js/faker' export class ListFactory extends Factory { static table = 'lists' diff --git a/cypress/factories/namespace.js b/cypress/factories/namespace.js index 89096d2dd..203f7159d 100644 --- a/cypress/factories/namespace.js +++ b/cypress/factories/namespace.js @@ -1,4 +1,4 @@ -import faker from 'faker' +import faker from '@faker-js/faker' import {Factory} from '../support/factory' import {formatISO} from 'date-fns' diff --git a/cypress/factories/task.js b/cypress/factories/task.js index 6fa8d5b67..5410a25eb 100644 --- a/cypress/factories/task.js +++ b/cypress/factories/task.js @@ -1,4 +1,4 @@ -import faker from 'faker' +import faker from '@faker-js/faker' import {Factory} from '../support/factory' import {formatISO} from 'date-fns' diff --git a/cypress/factories/task_comment.js b/cypress/factories/task_comment.js index 74e043f92..7800c0093 100644 --- a/cypress/factories/task_comment.js +++ b/cypress/factories/task_comment.js @@ -1,4 +1,4 @@ -import faker from 'faker' +import faker from '@faker-js/faker' import {Factory} from '../support/factory' import {formatISO} from "date-fns" diff --git a/cypress/factories/team.js b/cypress/factories/team.js index 928b8ce42..33cc37947 100644 --- a/cypress/factories/team.js +++ b/cypress/factories/team.js @@ -1,4 +1,4 @@ -import faker from 'faker' +import faker from '@faker-js/faker' import {Factory} from '../support/factory' import {formatISO} from 'date-fns' diff --git a/cypress/factories/user.js b/cypress/factories/user.js index 9e133b552..93971efeb 100644 --- a/cypress/factories/user.js +++ b/cypress/factories/user.js @@ -1,4 +1,4 @@ -import faker from 'faker' +import faker from '@faker-js/faker' import {Factory} from '../support/factory' import {formatISO} from "date-fns" diff --git a/cypress/integration/list/list-history.spec.js b/cypress/integration/list/list-history.spec.js new file mode 100644 index 000000000..b7633cbda --- /dev/null +++ b/cypress/integration/list/list-history.spec.js @@ -0,0 +1,56 @@ +import {ListFactory} from '../../factories/list' + +import '../../support/authenticateUser' +import {prepareLists} from './prepareLists' + +describe('List History', () => { + 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('.list-cards-wrapper-2-rows') + .should('not.contain', lists[0].title) + .should('contain', lists[1].title) + .should('contain', lists[2].title) + .should('contain', lists[3].title) + .should('contain', lists[4].title) + .should('contain', lists[5].title) + }) +}) \ No newline at end of file diff --git a/cypress/integration/list/list-view-gantt.spec.js b/cypress/integration/list/list-view-gantt.spec.js new file mode 100644 index 000000000..69805a30d --- /dev/null +++ b/cypress/integration/list/list-view-gantt.spec.js @@ -0,0 +1,76 @@ +import {formatISO, format} from 'date-fns' +import {TaskFactory} from '../../factories/task' +import {prepareLists} from './prepareLists' + +import '../../support/authenticateUser' + +describe('List View Gantt', () => { + prepareLists() + + it('Hides tasks with no dates', () => { + const tasks = TaskFactory.create(1) + cy.visit('/lists/1/gantt') + + cy.get('.gantt-chart .tasks') + .should('not.contain', tasks[0].title) + }) + + it('Shows tasks from the current and next month', () => { + const now = new Date() + const nextMonth = now + nextMonth.setDate(1) + nextMonth.setMonth(now.getMonth() + 1) + + cy.visit('/lists/1/gantt') + + cy.get('.gantt-chart .months') + .should('contain', format(now, 'MMMM')) + .should('contain', format(nextMonth, 'MMMM')) + }) + + it('Shows tasks with dates', () => { + const now = new Date() + const tasks = TaskFactory.create(1, { + start_date: formatISO(now), + end_date: formatISO(now.setDate(now.getDate() + 4)) + }) + cy.visit('/lists/1/gantt') + + cy.get('.gantt-chart .tasks') + .should('not.be.empty') + cy.get('.gantt-chart .tasks') + .should('contain', tasks[0].title) + }) + + it('Shows tasks with no dates after enabling them', () => { + TaskFactory.create(1, { + start_date: null, + end_date: null, + }) + cy.visit('/lists/1/gantt') + + cy.get('.gantt-options .fancycheckbox') + .contains('Show tasks which don\'t have dates set') + .click() + + cy.get('.gantt-chart .tasks') + .should('not.be.empty') + cy.get('.gantt-chart .tasks .task.nodate') + .should('exist') + }) + + it('Drags a task around', () => { + const now = new Date() + TaskFactory.create(1, { + start_date: formatISO(now), + end_date: formatISO(now.setDate(now.getDate() + 4)) + }) + cy.visit('/lists/1/gantt') + + cy.get('.gantt-chart .tasks .task') + .first() + .trigger('mousedown', {which: 1}) + .trigger('mousemove', {clientX: 500, clientY: 0}) + .trigger('mouseup', {force: true}) + }) +}) \ No newline at end of file diff --git a/cypress/integration/list/list-view-kanban.spec.js b/cypress/integration/list/list-view-kanban.spec.js new file mode 100644 index 000000000..68268304d --- /dev/null +++ b/cypress/integration/list/list-view-kanban.spec.js @@ -0,0 +1,196 @@ +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', () => { + let buckets + prepareLists() + + beforeEach(() => { + buckets = BucketFactory.create(2) + }) + + it('Shows all buckets with their tasks', () => { + const data = TaskFactory.create(10, { + list_id: 1, + bucket_id: 1, + }) + cy.visit('/lists/1/kanban') + + cy.get('.kanban .bucket .title') + .contains(buckets[0].title) + .should('exist') + cy.get('.kanban .bucket .title') + .contains(buckets[1].title) + .should('exist') + cy.get('.kanban .bucket') + .first() + .should('contain', data[0].title) + }) + + it('Can add a new task to a bucket', () => { + TaskFactory.create(2, { + list_id: 1, + bucket_id: 1, + }) + cy.visit('/lists/1/kanban') + + cy.getSettled('.kanban .bucket') + .contains(buckets[0].title) + .get('.bucket-footer .button') + .contains('Add another task') + .click() + cy.get('.kanban .bucket') + .contains(buckets[0].title) + .get('.bucket-footer .field .control input.input') + .type('New Task{enter}') + + cy.get('.kanban .bucket') + .first() + .should('contain', 'New Task') + }) + + it('Can create a new bucket', () => { + cy.visit('/lists/1/kanban') + + cy.get('.kanban .bucket.new-bucket .button') + .click() + cy.get('.kanban .bucket.new-bucket input.input') + .type('New Bucket{enter}') + + cy.wait(1000) // Wait for the request to finish + cy.get('.kanban .bucket .title') + .contains('New Bucket') + .should('exist') + }) + + it('Can set a bucket limit', () => { + cy.visit('/lists/1/kanban') + + cy.getSettled('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger') + .first() + .click() + cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item') + .contains('Limit: Not Set') + .click() + cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item .field input.input') + .first() + .type(3) + cy.get('[data-cy="setBucketLimit"]') + .first() + .click() + + cy.get('.kanban .bucket .bucket-header span.limit') + .contains('0/3') + .should('exist') + }) + + it('Can rename a bucket', () => { + cy.visit('/lists/1/kanban') + + cy.getSettled('.kanban .bucket .bucket-header .title') + .first() + .type('{selectall}New Bucket Title{enter}') + cy.get('.kanban .bucket .bucket-header .title') + .first() + .should('contain', 'New Bucket Title') + }) + + it('Can delete a bucket', () => { + cy.visit('/lists/1/kanban') + + cy.getSettled('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger') + .first() + .click() + cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item') + .contains('Delete') + .click() + cy.get('.modal-mask .modal-container .modal-content .header') + .should('contain', 'Delete the bucket') + cy.get('.modal-mask .modal-container .modal-content .actions .button') + .contains('Do it!') + .click() + + cy.get('.kanban .bucket .title') + .contains(buckets[0].title) + .should('not.exist') + cy.get('.kanban .bucket .title') + .contains(buckets[1].title) + .should('exist') + }) + + it('Can drag tasks around', () => { + const tasks = TaskFactory.create(2, { + list_id: 1, + bucket_id: 1, + }) + cy.visit('/lists/1/kanban') + + cy.getSettled('.kanban .bucket .tasks .task') + .contains(tasks[0].title) + .first() + .drag('.kanban .bucket:nth-child(2) .tasks') + + cy.get('.kanban .bucket:nth-child(2) .tasks') + .should('contain', tasks[0].title) + cy.get('.kanban .bucket:nth-child(1) .tasks') + .should('not.contain', tasks[0].title) + }) + + it('Should navigate to the task when the task card is clicked', () => { + const tasks = TaskFactory.create(5, { + id: '{increment}', + list_id: 1, + bucket_id: 1, + }) + cy.visit('/lists/1/kanban') + + cy.getSettled('.kanban .bucket .tasks .task') + .contains(tasks[0].title) + .should('be.visible') + .click() + + cy.url() + .should('contain', `/tasks/${tasks[0].id}`, { timeout: 1000 }) + }) + + it('Should remove a task from the kanban board when moving it to another list', () => { + const lists = ListFactory.create(2) + BucketFactory.create(2, { + list_id: '{increment}', + }) + const tasks = TaskFactory.create(5, { + id: '{increment}', + list_id: 1, + bucket_id: 1, + }) + const task = tasks[0] + cy.visit('/lists/1/kanban') + + cy.getSettled('.kanban .bucket .tasks .task') + .contains(task.title) + .should('be.visible') + .click() + + cy.get('.task-view .action-buttons .button', { timeout: 3000 }) + .contains('Move') + .click() + cy.get('.task-view .content.details .field .multiselect.control .input-wrapper input') + .type(`${lists[1].title}{enter}`) + // The requests happen with a 200ms timeout. Because of that, the results are not yet there when cypress + // presses enter and we can't simulate pressing on enter to select the item. + cy.get('.task-view .content.details .field .multiselect.control .search-results') + .children() + .first() + .click() + + cy.get('.global-notification', { timeout: 1000 }) + .should('contain', 'Success') + cy.go('back') + cy.get('.kanban .bucket') + .should('not.contain', task.title) + }) +}) \ No newline at end of file diff --git a/cypress/integration/list/list-view-list.spec.js b/cypress/integration/list/list-view-list.spec.js new file mode 100644 index 000000000..e1a4a0f69 --- /dev/null +++ b/cypress/integration/list/list-view-list.spec.js @@ -0,0 +1,97 @@ +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', () => { + prepareLists() + + it('Should be an empty list', () => { + cy.visit('/lists/1') + cy.url() + .should('contain', '/lists/1/list') + cy.get('.list-title h1') + .should('contain', 'First List') + cy.get('.list-title .dropdown') + .should('exist') + cy.get('p') + .contains('This list is currently empty.') + .should('exist') + }) + + it('Should navigate to the task when the title is clicked', () => { + const tasks = TaskFactory.create(5, { + id: '{increment}', + list_id: 1, + }) + cy.visit('/lists/1/list') + + cy.get('.tasks .task .tasktext') + .contains(tasks[0].title) + .first() + .click() + + cy.url() + .should('contain', `/tasks/${tasks[0].id}`) + }) + + it('Should not see any elements for a list which is shared read only', () => { + UserFactory.create(2) + UserListFactory.create(1, { + list_id: 2, + user_id: 1, + right: 0, + }) + const lists = ListFactory.create(2, { + owner_id: '{increment}', + namespace_id: '{increment}', + }) + cy.visit(`/lists/${lists[1].id}/`) + + cy.get('.list-title a.icon') + .should('not.exist') + cy.get('input.input[placeholder="Add a new task..."') + .should('not.exist') + }) + + it('Should only show the color of a list in the navigation and not in the list view', () => { + const lists = ListFactory.create(1, { + hex_color: '00db60', + }) + TaskFactory.create(10, { + list_id: lists[0].id, + }) + cy.visit(`/lists/${lists[0].id}/`) + + cy.get('.menu-list li .list-menu-link .color-bubble') + .should('have.css', 'background-color', 'rgb(0, 219, 96)') + cy.get('.tasks-container .tasks .color-bubble') + .should('not.exist') + }) + + it('Should paginate for > 50 tasks', () => { + const tasks = TaskFactory.create(100, { + id: '{increment}', + title: i => `task${i}`, + list_id: 1, + }) + cy.visit('/lists/1/list') + + cy.get('.tasks-container .tasks') + .should('contain', tasks[99].title) + + cy.get('.card-content .pagination .pagination-link') + .contains('2') + .click() + + cy.url() + .should('contain', '?page=2') + cy.get('.tasks-container .tasks') + .should('contain', tasks[1].title) + cy.get('.tasks-container .tasks') + .should('not.contain', tasks[99].title) + }) +}) \ No newline at end of file diff --git a/cypress/integration/list/list-view-table.spec.js b/cypress/integration/list/list-view-table.spec.js new file mode 100644 index 000000000..e0336efc5 --- /dev/null +++ b/cypress/integration/list/list-view-table.spec.js @@ -0,0 +1,52 @@ +import {TaskFactory} from '../../factories/task' + +import '../../support/authenticateUser' + +describe('List View Table', () => { + it('Should show a table with tasks', () => { + const tasks = TaskFactory.create(1) + cy.visit('/lists/1/table') + + cy.get('.list-table table.table') + .should('exist') + cy.get('.list-table table.table') + .should('contain', tasks[0].title) + }) + + it('Should have working column switches', () => { + TaskFactory.create(1) + cy.visit('/lists/1/table') + + cy.get('.list-table .filter-container .items .button') + .contains('Columns') + .click() + cy.get('.list-table .filter-container .card.columns-filter .card-content .fancycheckbox .check') + .contains('Priority') + .click() + cy.get('.list-table .filter-container .card.columns-filter .card-content .fancycheckbox .check') + .contains('Done') + .click() + + cy.get('.list-table table.table th') + .contains('Priority') + .should('exist') + cy.get('.list-table table.table th') + .contains('Done') + .should('not.exist') + }) + + it('Should navigate to the task when the title is clicked', () => { + const tasks = TaskFactory.create(5, { + id: '{increment}', + list_id: 1, + }) + cy.visit('/lists/1/table') + + cy.get('.list-table table.table') + .contains(tasks[0].title) + .click() + + cy.url() + .should('contain', `/tasks/${tasks[0].id}`) + }) +}) \ No newline at end of file diff --git a/cypress/integration/list/list.spec.js b/cypress/integration/list/list.spec.js index 864b533b3..00f5b4f5e 100644 --- a/cypress/integration/list/list.spec.js +++ b/cypress/integration/list/list.spec.js @@ -1,25 +1,11 @@ -import {formatISO, format} from 'date-fns' - import {TaskFactory} from '../../factories/task' -import {ListFactory} from '../../factories/list' -import {UserListFactory} from '../../factories/users_list' -import {UserFactory} from '../../factories/user' -import {NamespaceFactory} from '../../factories/namespace' -import {BucketFactory} from '../../factories/bucket' +import {prepareLists} from './prepareLists' import '../../support/authenticateUser' describe('Lists', () => { let lists - - beforeEach(() => { - UserFactory.create(1) - NamespaceFactory.create(1) - lists = ListFactory.create(1, { - title: 'First List' - }) - TaskFactory.truncate() - }) + prepareLists((newLists) => (lists = newLists)) it('Should create a new list', () => { cy.visit('/') @@ -29,9 +15,9 @@ describe('Lists', () => { .contains('New list') .click() cy.url() - .should('contain', '/namespaces/1/list') + .should('contain', '/lists/new/1') cy.get('.card-header-title') - .contains('Create a new list') + .contains('New list') cy.get('input.input') .type('New List') cy.get('.button') @@ -56,7 +42,7 @@ describe('Lists', () => { }) it('Should rename the list in all places', () => { - const tasks = TaskFactory.create(5, { + TaskFactory.create(5, { id: '{increment}', list_id: 1, }) @@ -86,7 +72,7 @@ describe('Lists', () => { .should('contain', newListName) .should('not.contain', lists[0].title) cy.visit('/') - cy.get('.card-content .tasks') + cy.get('.card-content') .should('contain', newListName) .should('not.contain', lists[0].title) }) @@ -101,7 +87,7 @@ describe('Lists', () => { .click() cy.url() .should('contain', '/settings/delete') - cy.get('.modal-mask .modal-container .modal-content .actions a.button') + cy.get('[data-cy="modalPrimary"]') .contains('Do it') .click() @@ -112,429 +98,4 @@ describe('Lists', () => { cy.location('pathname') .should('equal', '/') }) - - describe('List View', () => { - it('Should be an empty list', () => { - cy.visit('/lists/1') - cy.url() - .should('contain', '/lists/1/list') - cy.get('.list-title h1') - .should('contain', 'First List') - cy.get('.list-title .dropdown') - .should('exist') - cy.get('p') - .contains('This list is currently empty.') - .should('exist') - }) - - it('Should navigate to the task when the title is clicked', () => { - const tasks = TaskFactory.create(5, { - id: '{increment}', - list_id: 1, - }) - cy.visit('/lists/1/list') - - cy.get('.tasks .task .tasktext') - .contains(tasks[0].title) - .first() - .click() - - cy.url() - .should('contain', `/tasks/${tasks[0].id}`) - }) - - it('Should not see any elements for a list which is shared read only', () => { - UserFactory.create(2) - UserListFactory.create(1, { - list_id: 2, - user_id: 1, - right: 0, - }) - const lists = ListFactory.create(2, { - owner_id: '{increment}', - namespace_id: '{increment}', - }) - cy.visit(`/lists/${lists[1].id}/`) - - cy.get('.list-title a.icon') - .should('not.exist') - cy.get('input.input[placeholder="Add a new task..."') - .should('not.exist') - }) - - it('Should only show the color of a list in the navigation and not in the list view', () => { - const lists = ListFactory.create(1, { - hex_color: '00db60', - }) - TaskFactory.create(10, { - list_id: lists[0].id, - }) - cy.visit(`/lists/${lists[0].id}/`) - - cy.get('.menu-list li .list-menu-link .color-bubble') - .should('have.css', 'background-color', 'rgb(0, 219, 96)') - cy.get('.tasks-container .tasks .color-bubble') - .should('not.exist') - }) - - it('Should paginate for > 50 tasks', () => { - const tasks = TaskFactory.create(100, { - id: '{increment}', - title: i => `task${i}`, - list_id: 1, - }) - cy.visit('/lists/1/list') - - cy.get('.tasks-container .tasks') - .should('contain', tasks[99].title) - - cy.get('.card-content .pagination .pagination-link') - .contains('2') - .click() - - cy.url() - .should('contain', '?page=2') - cy.get('.tasks-container .tasks') - .should('contain', tasks[1].title) - cy.get('.tasks-container .tasks') - .should('not.contain', tasks[99].title) - }) - }) - - describe('Table View', () => { - it('Should show a table with tasks', () => { - const tasks = TaskFactory.create(1) - cy.visit('/lists/1/table') - - cy.get('.table-view table.table') - .should('exist') - cy.get('.table-view table.table') - .should('contain', tasks[0].title) - }) - - it('Should have working column switches', () => { - TaskFactory.create(1) - cy.visit('/lists/1/table') - - cy.get('.table-view .filter-container .items .button') - .contains('Columns') - .click() - cy.get('.table-view .filter-container .card.columns-filter .card-content .fancycheckbox .check') - .contains('Priority') - .click() - cy.get('.table-view .filter-container .card.columns-filter .card-content .fancycheckbox .check') - .contains('Done') - .click() - - cy.get('.table-view table.table th') - .contains('Priority') - .should('exist') - cy.get('.table-view table.table th') - .contains('Done') - .should('not.exist') - }) - - it('Should navigate to the task when the title is clicked', () => { - const tasks = TaskFactory.create(5, { - id: '{increment}', - list_id: 1, - }) - cy.visit('/lists/1/table') - - cy.get('.table-view table.table') - .contains(tasks[0].title) - .click() - - cy.url() - .should('contain', `/tasks/${tasks[0].id}`) - }) - }) - - describe('Gantt View', () => { - it('Hides tasks with no dates', () => { - const tasks = TaskFactory.create(1) - cy.visit('/lists/1/gantt') - - cy.get('.gantt-chart-container .gantt-chart .tasks') - .should('not.contain', tasks[0].title) - }) - - it('Shows tasks from the current and next month', () => { - const now = new Date() - const nextMonth = now - nextMonth.setDate(1) - nextMonth.setMonth(now.getMonth() + 1) - - cy.visit('/lists/1/gantt') - - cy.get('.gantt-chart-container .gantt-chart .months') - .should('contain', format(now, 'MMMM')) - .should('contain', format(nextMonth, 'MMMM')) - }) - - it('Shows tasks with dates', () => { - const now = new Date() - const tasks = TaskFactory.create(1, { - start_date: formatISO(now), - end_date: formatISO(now.setDate(now.getDate() + 4)) - }) - cy.visit('/lists/1/gantt') - - cy.get('.gantt-chart-container .gantt-chart .tasks') - .should('not.be.empty') - cy.get('.gantt-chart-container .gantt-chart .tasks') - .should('contain', tasks[0].title) - }) - - it('Shows tasks with no dates after enabling them', () => { - TaskFactory.create(1, { - start_date: null, - end_date: null, - }) - cy.visit('/lists/1/gantt') - - cy.get('.gantt-chart-container .gantt-options .fancycheckbox') - .contains('Show tasks which don\'t have dates set') - .click() - - cy.get('.gantt-chart-container .gantt-chart .tasks') - .should('not.be.empty') - cy.get('.gantt-chart-container .gantt-chart .tasks .task.nodate') - .should('exist') - }) - - it('Drags a task around', () => { - const now = new Date() - TaskFactory.create(1, { - start_date: formatISO(now), - end_date: formatISO(now.setDate(now.getDate() + 4)) - }) - cy.visit('/lists/1/gantt') - - cy.get('.gantt-chart-container .gantt-chart .tasks .task') - .first() - .trigger('mousedown', {which: 1}) - .trigger('mousemove', {clientX: 500, clientY: 0}) - .trigger('mouseup', {force: true}) - }) - }) - - describe('Kanban', () => { - let buckets - - beforeEach(() => { - buckets = BucketFactory.create(2) - }) - - it('Shows all buckets with their tasks', () => { - const data = TaskFactory.create(10, { - list_id: 1, - bucket_id: 1, - }) - cy.visit('/lists/1/kanban') - - cy.get('.kanban .bucket .title') - .contains(buckets[0].title) - .should('exist') - cy.get('.kanban .bucket .title') - .contains(buckets[1].title) - .should('exist') - cy.get('.kanban .bucket') - .first() - .should('contain', data[0].title) - }) - - it('Can add a new task to a bucket', () => { - const data = TaskFactory.create(2, { - list_id: 1, - bucket_id: 1, - }) - cy.visit('/lists/1/kanban') - - cy.get('.kanban .bucket') - .contains(buckets[0].title) - .get('.bucket-footer .button') - .contains('Add another task') - .click() - cy.get('.kanban .bucket') - .contains(buckets[0].title) - .get('.bucket-footer .field .control input.input') - .type('New Task{enter}') - - cy.get('.kanban .bucket') - .first() - .should('contain', 'New Task') - }) - - it('Can create a new bucket', () => { - cy.visit('/lists/1/kanban') - - cy.get('.kanban .bucket.new-bucket .button') - .click() - cy.get('.kanban .bucket.new-bucket input.input') - .type('New Bucket{enter}') - - cy.wait(1000) // Wait for the request to finish - cy.get('.kanban .bucket .title') - .contains('New Bucket') - .should('exist') - }) - - it('Can set a bucket limit', () => { - cy.visit('/lists/1/kanban') - - cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger') - .first() - .click() - cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item') - .contains('Limit: Not Set') - .click() - cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item .field input.input') - .first() - .type(3) - cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item .field a.button.is-primary') - .first() - .click() - - cy.get('.kanban .bucket .bucket-header span.limit') - .contains('0/3') - .should('exist') - }) - - it('Can rename a bucket', () => { - cy.visit('/lists/1/kanban') - - cy.get('.kanban .bucket .bucket-header .title') - .first() - .type('{selectall}New Bucket Title{enter}') - cy.get('.kanban .bucket .bucket-header .title') - .first() - .should('contain', 'New Bucket Title') - }) - - it('Can delete a bucket', () => { - cy.visit('/lists/1/kanban') - - cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger') - .first() - .click() - cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item') - .contains('Delete') - .click() - cy.get('.modal-mask .modal-container .modal-content .header') - .should('contain', 'Delete the bucket') - cy.get('.modal-mask .modal-container .modal-content .actions .button') - .contains('Do it!') - .click() - - cy.get('.kanban .bucket .title') - .contains(buckets[0].title) - .should('not.exist') - cy.get('.kanban .bucket .title') - .contains(buckets[1].title) - .should('exist') - }) - - it('Can drag tasks around', () => { - const tasks = TaskFactory.create(2, { - list_id: 1, - bucket_id: 1, - }) - cy.visit('/lists/1/kanban') - - cy.get('.kanban .bucket .tasks .task') - .contains(tasks[0].title) - .first() - .drag('.kanban .bucket:nth-child(2) .tasks .dropper') - - cy.get('.kanban .bucket:nth-child(2) .tasks') - .should('contain', tasks[0].title) - cy.get('.kanban .bucket:nth-child(1) .tasks') - .should('not.contain', tasks[0].title) - }) - - it('Should navigate to the task when the task card is clicked', () => { - const tasks = TaskFactory.create(5, { - id: '{increment}', - list_id: 1, - bucket_id: 1, - }) - cy.visit('/lists/1/kanban') - - cy.getSettled('.kanban .bucket .tasks .task') - .contains(tasks[0].title) - .should('be.visible') - .click() - - cy.url() - .should('contain', `/tasks/${tasks[0].id}`) - }) - - it('Should remove a task from the kanban board when moving it to another list', () => { - const lists = ListFactory.create(2) - BucketFactory.create(2, { - list_id: '{increment}', - }) - const tasks = TaskFactory.create(5, { - id: '{increment}', - list_id: 1, - bucket_id: 1, - }) - const task = tasks[0] - cy.visit('/lists/1/kanban') - - cy.getSettled('.kanban .bucket .tasks .task') - .contains(task.title) - .should('be.visible') - .click() - - cy.get('.task-view .action-buttons .button') - .contains('Move task') - .click() - cy.get('.task-view .content.details .field .multiselect.control .input-wrapper input') - .type(`${lists[1].title}{enter}`) - // The requests happen with a 200ms timeout. Because of that, the results are not yet there when cypress - // presses enter and we can't simulate pressing on enter to select the item. - cy.get('.task-view .content.details .field .multiselect.control .search-results') - .children() - .first() - .click() - - cy.get('.global-notification', { timeout: 1000 }) - .should('contain', 'Success') - cy.go('back') - cy.get('.kanban .bucket') - .should('not.contain', task.title) - }) - }) - - describe('List history', () => { - it('should show a list history on the home page', () => { - const lists = ListFactory.create(6) - - cy.visit('/') - cy.get('h3') - .contains('Last viewed') - .should('not.exist') - - cy.visit(`/lists/${lists[0].id}`) - cy.visit(`/lists/${lists[1].id}`) - cy.visit(`/lists/${lists[2].id}`) - cy.visit(`/lists/${lists[3].id}`) - cy.visit(`/lists/${lists[4].id}`) - cy.visit(`/lists/${lists[5].id}`) - - cy.visit('/') - cy.get('h3') - .contains('Last viewed') - .should('exist') - cy.get('.list-cards-wrapper-2-rows') - .should('not.contain', lists[0].title) - .should('contain', lists[1].title) - .should('contain', lists[2].title) - .should('contain', lists[3].title) - .should('contain', lists[4].title) - .should('contain', lists[5].title) - }) - }) }) diff --git a/cypress/integration/list/namespaces.spec.js b/cypress/integration/list/namespaces.spec.js index 7d092113a..2ede493f5 100644 --- a/cypress/integration/list/namespaces.spec.js +++ b/cypress/integration/list/namespaces.spec.js @@ -15,7 +15,7 @@ describe('Namepaces', () => { it('Should be all there', () => { cy.visit('/namespaces') - cy.get('.namespace h1 span') + cy.get('[data-cy="namespace-title"]') .should('contain', namespaces[0].title) }) @@ -23,14 +23,14 @@ describe('Namepaces', () => { const newNamespaceTitle = 'New Namespace' cy.visit('/namespaces') - cy.get('a.button') - .contains('Create a new namespace') + cy.get('[data-cy="new-namespace"]') + .should('contain', 'New namespace') .click() cy.url() .should('contain', '/namespaces/new') cy.get('.card-header-title') - .should('contain', 'Create a new namespace') + .should('contain', 'New namespace') cy.get('input.input') .type(newNamespaceTitle) cy.get('.button') @@ -72,7 +72,7 @@ describe('Namepaces', () => { cy.get('.namespace-container .menu.namespaces-lists') .should('contain', newNamespaceName) .should('not.contain', newNamespaces[0].title) - cy.get('.content.namespaces-list') + cy.get('[data-cy="namespaces-list"]') .should('contain', newNamespaceName) .should('not.contain', newNamespaces[0].title) }) @@ -89,7 +89,7 @@ describe('Namepaces', () => { .click() cy.url() .should('contain', '/settings/delete') - cy.get('.modal-mask .modal-container .modal-content .actions a.button') + cy.get('[data-cy="modalPrimary"]') .contains('Do it') .click() @@ -116,30 +116,30 @@ describe('Namepaces', () => { // Initial cy.visit('/namespaces') - cy.get('.namespaces-list .namespace') + cy.get('.namespace') .should('not.contain', 'Archived') // Show archived - cy.get('.namespaces-list .fancycheckbox.show-archived-check label.check span') + cy.get('[data-cy="show-archived-check"] label.check span') .should('be.visible') .click() - cy.get('.namespaces-list .fancycheckbox.show-archived-check input') + cy.get('[data-cy="show-archived-check"] input') .should('be.checked') - cy.get('.namespaces-list .namespace') + cy.get('.namespace') .should('contain', 'Archived') // Don't show archived - cy.get('.namespaces-list .fancycheckbox.show-archived-check label.check span') + cy.get('[data-cy="show-archived-check"] label.check span') .should('be.visible') .click() - cy.get('.namespaces-list .fancycheckbox.show-archived-check input') + cy.get('[data-cy="show-archived-check"] input') .should('not.be.checked') // Second time visiting after unchecking cy.visit('/namespaces') - cy.get('.namespaces-list .fancycheckbox.show-archived-check input') + cy.get('[data-cy="show-archived-check"] input') .should('not.be.checked') - cy.get('.namespaces-list .namespace') + cy.get('.namespace') .should('not.contain', 'Archived') }) }) diff --git a/cypress/integration/list/prepareLists.js b/cypress/integration/list/prepareLists.js new file mode 100644 index 000000000..afef6ba4f --- /dev/null +++ b/cypress/integration/list/prepareLists.js @@ -0,0 +1,16 @@ +import {ListFactory} from '../../factories/list' +import {UserFactory} from '../../factories/user' +import {NamespaceFactory} from '../../factories/namespace' +import {TaskFactory} from '../../factories/task' + +export function prepareLists(setLists = () => {}) { + beforeEach(() => { + UserFactory.create(1) + NamespaceFactory.create(1) + const lists = ListFactory.create(1, { + title: 'First List' + }) + setLists(lists) + TaskFactory.truncate() + }) +} \ No newline at end of file diff --git a/cypress/integration/misc/home.spec.js b/cypress/integration/misc/home.spec.js deleted file mode 100644 index 82cbeed22..000000000 --- a/cypress/integration/misc/home.spec.js +++ /dev/null @@ -1,35 +0,0 @@ -import '../../support/authenticateUser' - -const setHours = hours => { - const date = new Date() - date.setHours(hours) - cy.clock(+date) -} - -describe('Home Page', () => { - it('shows the right salutation in the night', () => { - setHours(4) - cy.visit('/') - cy.get('h2').should('contain', 'Good Night') - }) - it('shows the right salutation in the morning', () => { - setHours(8) - cy.visit('/') - cy.get('h2').should('contain', 'Good Morning') - }) - it('shows the right salutation in the day', () => { - setHours(13) - cy.visit('/') - cy.get('h2').should('contain', 'Hi') - }) - it('shows the right salutation in the night', () => { - setHours(20) - cy.visit('/') - cy.get('h2').should('contain', 'Good Evening') - }) - it('shows the right salutation in the night again', () => { - setHours(23) - cy.visit('/') - cy.get('h2').should('contain', 'Good Night') - }) -}) \ No newline at end of file diff --git a/cypress/integration/task/task.spec.js b/cypress/integration/task/task.spec.js index 68027e3d8..29ade1d24 100644 --- a/cypress/integration/task/task.spec.js +++ b/cypress/integration/task/task.spec.js @@ -116,6 +116,7 @@ describe('Task', () => { .should('be.visible') .should('contain', 'Done') cy.get('.task-view .action-buttons p.created') + .scrollIntoView() .should('be.visible') .should('contain', 'Done') }) @@ -128,7 +129,7 @@ describe('Task', () => { cy.visit(`/tasks/${tasks[0].id}`) cy.get('.task-view .action-buttons .button') - .contains('Done!') + .contains('Mark task done!') .click() cy.get('.task-view .heading .is-done') @@ -168,7 +169,7 @@ describe('Task', () => { .click() cy.get('.task-view .details.content.description .editor .vue-easymde .EasyMDEContainer .CodeMirror-scroll') .type('{selectall}New Description') - cy.get('.task-view .details.content.description .editor a') + cy.get('[data-cy="saveEditor"]') .contains('Save') .click() @@ -209,7 +210,7 @@ describe('Task', () => { cy.visit(`/tasks/${tasks[0].id}`) cy.get('.task-view .action-buttons .button') - .contains('Move task') + .contains('Move') .click() cy.get('.task-view .content.details .field .multiselect.control .input-wrapper input') .type(`${lists[1].title}{enter}`) @@ -236,7 +237,7 @@ describe('Task', () => { cy.get('.task-view .action-buttons .button') .should('be.visible') - .contains('Delete task') + .contains('Delete') .click() cy.get('.modal-mask .modal-container .modal-content .header') .should('contain', 'Delete this task') @@ -316,7 +317,7 @@ describe('Task', () => { cy.visit(`/tasks/${tasks[0].id}`) cy.get('.task-view .action-buttons .button') - .contains('Add labels') + .contains('Add Labels') .should('be.visible') .click() cy.get('.task-view .details.labels-list .multiselect input') @@ -343,7 +344,7 @@ describe('Task', () => { cy.visit(`/tasks/${tasks[0].id}`) cy.get('.task-view .action-buttons .button') - .contains('Add labels') + .contains('Add Labels') .click() cy.get('.task-view .details.labels-list .multiselect input') .type(labels[0].title) @@ -372,13 +373,13 @@ 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('a.delete') + .get('[data-cy="taskDetail.removeLabel"]') .click() cy.get('.global-notification') @@ -404,7 +405,7 @@ describe('Task', () => { cy.get('.datepicker .datepicker-popup a') .contains('Tomorrow') .click() - cy.get('.datepicker .datepicker-popup a.button') + cy.get('[data-cy="closeDatepicker"]') .contains('Confirm') .click() diff --git a/cypress/integration/user/logout.spec.js b/cypress/integration/user/logout.spec.js index fbbc7088c..1a22e21ef 100644 --- a/cypress/integration/user/logout.spec.js +++ b/cypress/integration/user/logout.spec.js @@ -6,7 +6,7 @@ describe('Log out', () => { cy.get('.navbar .user .username') .click() - cy.get('.navbar .user .dropdown-menu a.dropdown-item') + cy.get('.navbar .user .dropdown-menu .dropdown-item') .contains('Logout') .click() diff --git a/cypress/integration/user/registration.spec.js b/cypress/integration/user/registration.spec.js index fd940aa7e..16e959d7b 100644 --- a/cypress/integration/user/registration.spec.js +++ b/cypress/integration/user/registration.spec.js @@ -25,7 +25,6 @@ context('Registration', () => { cy.get('#username').type(fixture.username) cy.get('#email').type(fixture.email) cy.get('#password').type(fixture.password) - cy.get('#passwordValidation').type(fixture.password) cy.get('#register-submit').click() cy.url().should('include', '/') cy.clock(1625656161057) // 13:00 @@ -43,7 +42,6 @@ context('Registration', () => { cy.get('#username').type(fixture.username) cy.get('#email').type(fixture.email) cy.get('#password').type(fixture.password) - cy.get('#passwordValidation').type(fixture.password) cy.get('#register-submit').click() cy.get('div.message.danger').contains('A user with this username already exists.') }) diff --git a/cypress/integration/user/settings.spec.js b/cypress/integration/user/settings.spec.js index 29cb1ad03..21bd9c1d9 100644 --- a/cypress/integration/user/settings.spec.js +++ b/cypress/integration/user/settings.spec.js @@ -8,21 +8,23 @@ describe('User Settings', () => { }) it('Changes the user avatar', () => { + cy.intercept(`${Cypress.env('API_URL')}/user/settings/avatar/upload`).as('uploadAvatar') + cy.visit('/user/settings/avatar') cy.get('input[name=avatarProvider][value=upload]') .click() - cy.get('input[type=file]', { timeout: 1000 }) - .attachFile('image.jpg') + cy.get('input[type=file]', {timeout: 1000}) + .selectFile('cypress/fixtures/image.jpg', {force: true}) // The input is not visible, but on purpose cy.get('.vue-handler-wrapper.vue-handler-wrapper--south .vue-simple-handler.vue-simple-handler--south') .trigger('mousedown', {which: 1}) .trigger('mousemove', {clientY: 100}) .trigger('mouseup') - cy.get('a.button.is-primary') + cy.get('[data-cy="uploadAvatar"]') .contains('Upload Avatar') .click() - cy.wait(3000) // Wait for the request to finish + cy.wait('@uploadAvatar') cy.get('.global-notification') .should('contain', 'Success') }) @@ -33,7 +35,7 @@ describe('User Settings', () => { cy.get('.general-settings .control input.input') .first() .type('Lorem Ipsum') - cy.get('.card.general-settings .button.is-primary') + cy.get('[data-cy="saveGeneralSettings"]') .contains('Save') .click() diff --git a/cypress/support/index.js b/cypress/support/index.js index 0c885c654..7b0c56d18 100644 --- a/cypress/support/index.js +++ b/cypress/support/index.js @@ -1,6 +1,5 @@ import './commands' -import 'cypress-file-upload' import '@4tw/cypress-drag-drop' // see https://github.com/cypress-io/cypress/issues/702#issuecomment-587127275 diff --git a/netlify.toml b/netlify.toml index 24ee45e7f..a0bfdfabc 100644 --- a/netlify.toml +++ b/netlify.toml @@ -1,6 +1,6 @@ [build] command = "yarn build" - publish = "dist" + publish = "dist-preview" [[redirects]] from = "/*" diff --git a/nginx.conf b/nginx.conf index 9b0674b72..56e34e81b 100644 --- a/nginx.conf +++ b/nginx.conf @@ -6,79 +6,110 @@ pid /var/run/nginx.pid; events { - worker_connections 1024; + worker_connections 1024; } http { - include /etc/nginx/mime.types; - default_type application/octet-stream; + include /etc/nginx/mime.types; + default_type application/octet-stream; - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; + types { + application/manifest+json webmanifest; + } - access_log /var/log/nginx/access.log main; + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; - sendfile on; - #tcp_nopush on; + access_log /var/log/nginx/access.log main; - keepalive_timeout 65; + sendfile on; + #tcp_nopush on; - gzip on; - gzip_disable "msie6"; + keepalive_timeout 65; - 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; + gzip on; - map_hash_max_size 128; - map_hash_bucket_size 128; + 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; - # Expires map - map $sent_http_content_type $expires { - default off; - text/html max; - text/css max; - application/javascript max; - text/javascript max; - application/vnd.ms-fontobject max; - application/x-font-ttf max; - font/opentype max; - font/woff2 max; - image/svg+xml max; - image/x-icon max; - audio/wav max; - ~image/ max; - ~font/ max; - } + map_hash_max_size 128; + map_hash_bucket_size 128; - server { - listen 80; - listen 81 default_server http2 proxy_protocol; ## Needed when behind HAProxy with SSL termination + HTTP/2 support + # 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_name _; + server { + listen 80; + listen 81 default_server http2 proxy_protocol; ## Needed when behind HAProxy with SSL termination + HTTP/2 support - expires $expires; + server_name _; - location ~* .(txt|webmanifest|css|js|mjs|map|svg|jpg|jpeg|png|ico|ttf|woff|woff2|wav)$ { - root /usr/share/nginx/html; - try_files $uri $uri/ =404; - } + expires $expires; - location / { - root /usr/share/nginx/html; - try_files $uri $uri/ /index.html; - index index.html; - } + root /usr/share/nginx/html; - error_page 500 502 503 504 /50x.html; - location = /50x.html { - 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 { + } + } } diff --git a/originalMedia/images/cool.svg b/originalMedia/images/cool.svg index 859852978..72063dbc2 100644 --- a/originalMedia/images/cool.svg +++ b/originalMedia/images/cool.svg @@ -49,7 +49,7 @@ inkscape:label="ink_ext_XXXXXX 1" style="display:inline" transform="translate(-92.67749,-674.48297)">/src" - ], - "transform": { - "^.+\\.(js|tsx?)$": "ts-jest" - }, - "moduleFileExtensions": [ - "ts", - "js", - "json" - ] - }, "license": "AGPL-3.0-or-later", - "packageManager": "yarn@1.22.17" + "packageManager": "yarn@1.22.18" } diff --git a/renovate.json b/renovate.json index 2019a5e48..753b1e715 100644 --- a/renovate.json +++ b/renovate.json @@ -3,5 +3,17 @@ "labels": ["dependencies"], "extends": [ "config:base" + ], + "packageRules": [ + { + "matchPackageNames": ["netlify-cli", "caniuse-lite"], + "extends": ["schedule:weekly"] + }, + { + "groupName": "vueuse", + "matchPackagePrefixes": [ + "@vueuse/" + ] + } ] } diff --git a/scripts/deploy-preview-netlify.js b/scripts/deploy-preview-netlify.js index b2dd23364..11eac4e3a 100644 --- a/scripts/deploy-preview-netlify.js +++ b/scripts/deploy-preview-netlify.js @@ -1,20 +1,24 @@ -const slugify = require('slugify') const {exec} = require('child_process') const axios = require('axios') const BOT_USER_ID = 513 const giteaToken = process.env.GITEA_TOKEN const siteId = process.env.NETLIFY_SITE_ID -const branchSlug = slugify(process.env.DRONE_SOURCE_BRANCH) +const branchSlug = String(process.env.DRONE_SOURCE_BRANCH) + .trim() + .normalize('NFKD') + .toLowerCase() + .replace(/[.\s/]/g, '-') + .replace(/[^A-Za-z\d-]/g, '') const prNumber = process.env.DRONE_PULL_REQUEST const prIssueCommentsUrl = `https://kolaente.dev/api/v1/repos/vikunja/frontend/issues/${prNumber}/comments` -const alias = `${prNumber}-${branchSlug}` +const alias = `${prNumber}-${branchSlug}`.substring(0,37) const fullPreviewUrl = `https://${alias}--vikunja-frontend-preview.netlify.app` const promiseExec = cmd => { return new Promise((resolve, reject) => { - exec(cmd, (error, stdout, stderr) => { + exec(cmd, (error, stdout) => { if (error) { reject(error) return diff --git a/scripts/deploy-preview-netlify.js.sha384 b/scripts/deploy-preview-netlify.js.sha384 index fe5f72f1d..03ac06468 100644 --- a/scripts/deploy-preview-netlify.js.sha384 +++ b/scripts/deploy-preview-netlify.js.sha384 @@ -1 +1 @@ -55ce0faaa2c1919341617ccfaeccbb6029ac12107964ff488985cff13dd952f1a991df3ab0d4b0705deb761e508e6434 ./scripts/deploy-preview-netlify.js +bb46342a0a08105b340ba7976cff9d80ef89901120ec0639669caa70bb7d2dbc43e78b1f635a7654ab2456e8358c98a4 ./scripts/deploy-preview-netlify.js diff --git a/scripts/serve-dist.js b/scripts/serve-dist.js index e0303dd7f..f6e092e5f 100644 --- a/scripts/serve-dist.js +++ b/scripts/serve-dist.js @@ -3,7 +3,7 @@ const express = require('express') const app = express() const p = path.join(__dirname, '..', 'dist-dev') -const port = 5000 +const port = 4173 app.use(express.static(p)) // Handle urls set by the frontend diff --git a/src/App.vue b/src/App.vue index 5eacb532c..ec62fd9a9 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,116 +1,92 @@ - diff --git a/src/components/date/dateRanges.ts b/src/components/date/dateRanges.ts new file mode 100644 index 000000000..648f001b4 --- /dev/null +++ b/src/components/date/dateRanges.ts @@ -0,0 +1,21 @@ +export const DATE_RANGES = { + // Format: + // Key is the title, as a translation string, the first entry of the value array + // is the "from" date, the second one is the "to" date. + 'today': ['now/d', 'now/d+1d'], + + 'lastWeek': ['now/w-1w', 'now/w-2w'], + 'thisWeek': ['now/w', 'now/w+1w'], + 'restOfThisWeek': ['now', 'now/w+1w'], + 'nextWeek': ['now/w+1w', 'now/w+2w'], + 'next7Days': ['now', 'now+7d'], + + 'lastMonth': ['now/M-1M', 'now/M-2M'], + 'thisMonth': ['now/M', 'now/M+1M'], + 'restOfThisMonth': ['now', 'now/M+1M'], + 'nextMonth': ['now/M+1M', 'now/M+2M'], + 'next30Days': ['now', 'now+30d'], + + 'thisYear': ['now/y', 'now/y+1y'], + 'restOfThisYear': ['now', 'now/y+1y'], +} diff --git a/src/components/date/datemathHelp.vue b/src/components/date/datemathHelp.vue new file mode 100644 index 000000000..f645fdb3e --- /dev/null +++ b/src/components/date/datemathHelp.vue @@ -0,0 +1,131 @@ + + + + + diff --git a/src/components/date/datepickerWithRange.vue b/src/components/date/datepickerWithRange.vue new file mode 100644 index 000000000..61c21ef53 --- /dev/null +++ b/src/components/date/datepickerWithRange.vue @@ -0,0 +1,254 @@ + + + + + diff --git a/src/components/home/Logo.vue b/src/components/home/Logo.vue index 5a5612b46..6b9f12f8f 100644 --- a/src/components/home/Logo.vue +++ b/src/components/home/Logo.vue @@ -1,9 +1,12 @@ - @@ -54,7 +47,7 @@ export default { display: flex; align-items: center; background: $warning; - padding: 0 0 0 .5rem; + padding: .25rem .5rem; border-radius: $radius; font-size: .9rem; color: var(--grey-900); @@ -85,4 +78,8 @@ export default { margin-left: .5rem; } } + +.dark .update-notification { + color: var(--grey-200); +} \ No newline at end of file diff --git a/src/components/input/AsyncEditor.js b/src/components/input/AsyncEditor.ts similarity index 100% rename from src/components/input/AsyncEditor.js rename to src/components/input/AsyncEditor.ts diff --git a/src/components/input/button.vue b/src/components/input/button.vue index 831ef11a7..07e0ad2f2 100644 --- a/src/components/input/button.vue +++ b/src/components/input/button.vue @@ -1,77 +1,65 @@ - - if (this.to !== false) { - this.$router.push(this.to) - } + \ No newline at end of file diff --git a/src/components/input/vue-easymde/vue-easymde.vue b/src/components/input/vue-easymde/vue-easymde.vue index 5e68f079f..434c2dd75 100644 --- a/src/components/input/vue-easymde/vue-easymde.vue +++ b/src/components/input/vue-easymde/vue-easymde.vue @@ -9,11 +9,13 @@ - diff --git a/src/components/list/partials/list-card.vue b/src/components/list/partials/list-card.vue index f2ccfb718..cfa34fc85 100644 --- a/src/components/list/partials/list-card.vue +++ b/src/components/list/partials/list-card.vue @@ -2,43 +2,54 @@ -
+
+
+
{{ $t('namespace.archived') }} - - + + +
+
{{ list.title }}
-
{{ list.title }}
diff --git a/src/components/misc/create-edit.vue b/src/components/misc/create-edit.vue index 4e802fd20..12a292b4e 100644 --- a/src/components/misc/create-edit.vue +++ b/src/components/misc/create-edit.vue @@ -14,25 +14,25 @@