diff --git a/.drone.yml b/.drone.yml index 4090e6a8..a7522020 100644 --- a/.drone.yml +++ b/.drone.yml @@ -116,36 +116,16 @@ 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 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 pull: true @@ -665,6 +645,6 @@ steps: from_secret: crowdin_key --- kind: signature -hmac: 188ee90100c5fc5922a445e531e7a47453121edddb2a64a182eb23ed2bf602de +hmac: 997e1badebe484ac29557c4af356e63db4d3d57f3d32e92d482f117f8cec64da ... diff --git a/cypress.json b/cypress.json index 27f12495..28fd022c 100644 --- a/cypress.json +++ b/cypress.json @@ -7,5 +7,6 @@ "video": false, "retries": { "runMode": 2 - } + }, + "projectId": "181c7x" } diff --git a/cypress/factories/bucket.js b/cypress/factories/bucket.js index be90cca9..8001899b 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 b3f9ab30..7aac5eb0 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 e2c01dd0..3a406ea2 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 f93cdba4..2ffc3125 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 89096d2d..203f7159 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 6fa8d5b6..5410a25e 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 74e043f9..7800c009 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 928b8ce4..33cc3794 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 9e133b55..93971efe 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 00000000..b7633cbd --- /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 00000000..69805a30 --- /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 00000000..52d67282 --- /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 .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}`, { 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 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) + }) +}) \ 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 00000000..e1a4a0f6 --- /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 00000000..e0336efc --- /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 55afcc7b..00f5b4f5 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,7 +15,7 @@ describe('Lists', () => { .contains('New list') .click() cy.url() - .should('contain', '/namespaces/1/list') + .should('contain', '/lists/new/1') cy.get('.card-header-title') .contains('New list') cy.get('input.input') @@ -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, }) @@ -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('[data-cy="setBucketLimit"]') - .first() - .click() - - cy.get('.kanban .bucket .bucket-header span.limit') - .contains('0/3') - .should('exist') - }) - - it('Can rename a bucket', () => { - cy.visit('/lists/1/kanban') - - cy.get('.kanban .bucket .bucket-header .title') - .first() - .type('{selectall}New Bucket Title{enter}') - cy.get('.kanban .bucket .bucket-header .title') - .first() - .should('contain', 'New Bucket Title') - }) - - it('Can delete a bucket', () => { - cy.visit('/lists/1/kanban') - - cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger') - .first() - .click() - cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item') - .contains('Delete') - .click() - cy.get('.modal-mask .modal-container .modal-content .header') - .should('contain', 'Delete the bucket') - cy.get('.modal-mask .modal-container .modal-content .actions .button') - .contains('Do it!') - .click() - - cy.get('.kanban .bucket .title') - .contains(buckets[0].title) - .should('not.exist') - cy.get('.kanban .bucket .title') - .contains(buckets[1].title) - .should('exist') - }) - - it('Can drag tasks around', () => { - const tasks = TaskFactory.create(2, { - list_id: 1, - bucket_id: 1, - }) - cy.visit('/lists/1/kanban') - - cy.get('.kanban .bucket .tasks .task') - .contains(tasks[0].title) - .first() - .drag('.kanban .bucket:nth-child(2) .tasks .dropper') - - cy.get('.kanban .bucket:nth-child(2) .tasks') - .should('contain', tasks[0].title) - cy.get('.kanban .bucket:nth-child(1) .tasks') - .should('not.contain', tasks[0].title) - }) - - it('Should navigate to the task when the task card is clicked', () => { - const tasks = TaskFactory.create(5, { - id: '{increment}', - list_id: 1, - bucket_id: 1, - }) - cy.visit('/lists/1/kanban') - - cy.getSettled('.kanban .bucket .tasks .task') - .contains(tasks[0].title) - .should('be.visible') - .click() - - cy.url() - .should('contain', `/tasks/${tasks[0].id}`) - }) - - it('Should remove a task from the kanban board when moving it to another list', () => { - const lists = ListFactory.create(2) - BucketFactory.create(2, { - list_id: '{increment}', - }) - const tasks = TaskFactory.create(5, { - id: '{increment}', - list_id: 1, - bucket_id: 1, - }) - const task = tasks[0] - cy.visit('/lists/1/kanban') - - cy.getSettled('.kanban .bucket .tasks .task') - .contains(task.title) - .should('be.visible') - .click() - - cy.get('.task-view .action-buttons .button') - .contains('Move task') - .click() - cy.get('.task-view .content.details .field .multiselect.control .input-wrapper input') - .type(`${lists[1].title}{enter}`) - // The requests happen with a 200ms timeout. Because of that, the results are not yet there when cypress - // presses enter and we can't simulate pressing on enter to select the item. - cy.get('.task-view .content.details .field .multiselect.control .search-results') - .children() - .first() - .click() - - cy.get('.global-notification', { timeout: 1000 }) - .should('contain', 'Success') - cy.go('back') - cy.get('.kanban .bucket') - .should('not.contain', task.title) - }) - }) - - describe('List history', () => { - it('should show a list history on the home page', () => { - const lists = ListFactory.create(6) - - cy.visit('/') - cy.get('h3') - .contains('Last viewed') - .should('not.exist') - - cy.visit(`/lists/${lists[0].id}`) - cy.visit(`/lists/${lists[1].id}`) - cy.visit(`/lists/${lists[2].id}`) - cy.visit(`/lists/${lists[3].id}`) - cy.visit(`/lists/${lists[4].id}`) - cy.visit(`/lists/${lists[5].id}`) - - cy.visit('/') - cy.get('h3') - .contains('Last viewed') - .should('exist') - cy.get('.list-cards-wrapper-2-rows') - .should('not.contain', lists[0].title) - .should('contain', lists[1].title) - .should('contain', lists[2].title) - .should('contain', lists[3].title) - .should('contain', lists[4].title) - .should('contain', lists[5].title) - }) - }) }) diff --git a/cypress/integration/list/prepareLists.js b/cypress/integration/list/prepareLists.js new file mode 100644 index 00000000..afef6ba4 --- /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/task/task.spec.js b/cypress/integration/task/task.spec.js index 1b85e992..62343c91 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') }) @@ -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') diff --git a/cypress/integration/user/registration.spec.js b/cypress/integration/user/registration.spec.js index fd940aa7..16e959d7 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 c6a645d5..21bd9c1d 100644 --- a/cypress/integration/user/settings.spec.js +++ b/cypress/integration/user/settings.spec.js @@ -8,12 +8,14 @@ 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}) @@ -22,7 +24,7 @@ describe('User Settings', () => { .contains('Upload Avatar') .click() - cy.wait(3000) // Wait for the request to finish + cy.wait('@uploadAvatar') cy.get('.global-notification') .should('contain', 'Success') }) diff --git a/cypress/support/index.js b/cypress/support/index.js index 0c885c65..7b0c56d1 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/package.json b/package.json index 9ab35adb..9adfbbbe 100644 --- a/package.json +++ b/package.json @@ -18,37 +18,37 @@ "browserslist:update": "npx browserslist@latest --update-db" }, "dependencies": { - "@github/hotkey": "1.6.1", + "@github/hotkey": "2.0.0", "@kyvg/vue3-notification": "2.3.4", - "@sentry/tracing": "6.16.1", - "@sentry/vue": "6.16.1", + "@sentry/tracing": "6.17.4", + "@sentry/vue": "6.17.4", "@types/is-touch-device": "1.0.0", - "@vue/compat": "3.2.26", - "@vueuse/core": "7.5.2", - "@vueuse/router": "7.5.3", + "@vue/compat": "3.2.29", + "@vueuse/core": "7.5.5", + "@vueuse/router": "7.5.5", "bulma-css-variables": "0.9.33", "camel-case": "4.1.2", - "codemirror": "5.65.0", + "codemirror": "5.65.1", "copy-to-clipboard": "3.3.1", "date-fns": "2.28.0", - "dompurify": "2.3.4", - "easymde": "2.15.0", + "dompurify": "2.3.5", + "easymde": "2.16.1", "flatpickr": "4.6.9", "flexsearch": "0.7.21", "highlight.js": "11.4.0", "is-touch-device": "1.0.1", "lodash.clonedeep": "4.5.0", "lodash.debounce": "4.0.8", - "marked": "4.0.9", + "marked": "4.0.12", "register-service-worker": "1.7.2", "snake-case": "3.0.4", - "ufo": "0.7.9", - "v-tooltip": "4.0.0-beta.13", - "vue": "3.2.26", - "vue-advanced-cropper": "2.7.1", + "ufo": "0.7.10", + "v-tooltip": "4.0.0-beta.17", + "vue": "3.2.29", + "vue-advanced-cropper": "2.8.0", "vue-drag-resize": "2.0.3", "vue-flatpickr-component": "9.0.5", - "vue-i18n": "9.2.0-beta.26", + "vue-i18n": "9.2.0-beta.30", "vue-router": "4.0.12", "vuedraggable": "4.1.0", "vuex": "4.0.2", @@ -56,41 +56,40 @@ }, "devDependencies": { "@4tw/cypress-drag-drop": "2.1.0", + "@faker-js/faker": "6.0.0-alpha.5", "@fortawesome/fontawesome-svg-core": "1.2.36", "@fortawesome/free-regular-svg-icons": "5.15.4", "@fortawesome/free-solid-svg-icons": "5.15.4", "@fortawesome/vue-fontawesome": "3.0.0-5", "@types/flexsearch": "0.7.2", - "@typescript-eslint/eslint-plugin": "5.9.0", - "@typescript-eslint/parser": "5.9.0", + "@typescript-eslint/eslint-plugin": "5.10.2", + "@typescript-eslint/parser": "5.10.2", "@vitejs/plugin-legacy": "1.6.4", - "@vitejs/plugin-vue": "2.0.1", + "@vitejs/plugin-vue": "2.1.0", "@vue/eslint-config-typescript": "10.0.0", "autoprefixer": "10.4.2", - "axios": "0.24.0", + "axios": "0.25.0", "browserslist": "4.19.1", - "caniuse-lite": "1.0.30001298", - "cypress": "9.2.0", - "cypress-file-upload": "5.0.8", - "esbuild": "0.14.11", - "eslint": "8.6.0", - "eslint-plugin-vue": "8.2.0", + "caniuse-lite": "1.0.30001307", + "cypress": "9.4.1", + "esbuild": "0.14.18", + "eslint": "8.8.0", + "eslint-plugin-vue": "8.4.1", "express": "4.17.2", - "faker": "5.5.3", - "netlify-cli": "8.6.15", - "happy-dom": "2.25.1", - "postcss": "8.4.5", - "postcss-preset-env": "7.2.0", - "rollup": "2.63.0", - "rollup-plugin-visualizer": "5.5.2", - "sass": "1.47.0", + "happy-dom": "2.31.1", + "netlify-cli": "8.15.0", + "postcss": "8.4.6", + "postcss-preset-env": "7.3.1", + "rollup": "2.67.0", + "rollup-plugin-visualizer": "5.5.4", + "sass": "1.49.7", "slugify": "1.6.5", - "typescript": "4.5.4", - "vite": "2.7.10", - "vite-plugin-pwa": "0.11.12", - "vite-svg-loader": "3.1.1", - "vitest": "0.0.139", - "vue-tsc": "0.30.2", + "typescript": "4.5.5", + "vite": "2.7.13", + "vite-plugin-pwa": "0.11.13", + "vite-svg-loader": "3.1.2", + "vitest": "0.2.7", + "vue-tsc": "0.31.1", "wait-on": "6.0.0", "workbox-cli": "6.4.2" }, @@ -130,7 +129,7 @@ "parser": "vue-eslint-parser", "parserOptions": { "parser": "@typescript-eslint/parser", - "ecmaVersion": 2021 + "ecmaVersion": 2022 }, "ignorePatterns": [ "*.test.*", diff --git a/renovate.json b/renovate.json index 2019a5e4..440fcf3f 100644 --- a/renovate.json +++ b/renovate.json @@ -3,5 +3,11 @@ "labels": ["dependencies"], "extends": [ "config:base" + ], + "packageRules": [ + { + "matchPackageNames": ["netlify-cli"], + "extends": ["schedule:weekly"] + } ] } diff --git a/src/App.vue b/src/App.vue index 06a81850..5ab3635f 100644 --- a/src/App.vue +++ b/src/App.vue @@ -42,7 +42,7 @@ import {useBodyClass} from '@/composables/useBodyClass' const store = useStore() const router = useRouter() -useBodyClass('is-touch', isTouchDevice) +useBodyClass('is-touch', isTouchDevice()) const keyboardShortcutsActive = computed(() => store.state.keyboardShortcutsActive) const authUser = computed(() => store.getters['auth/authUser']) diff --git a/src/assets/logo-full-pride.svg b/src/assets/logo-full-pride.svg index 1ecacb3e..bff6f8bb 100644 --- a/src/assets/logo-full-pride.svg +++ b/src/assets/logo-full-pride.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/logo-full.svg b/src/assets/logo-full.svg index 20b6ae13..db656b85 100644 --- a/src/assets/logo-full.svg +++ b/src/assets/logo-full.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/components/home/contentAuth.vue b/src/components/home/contentAuth.vue index d3f0dc8a..25fd01e7 100644 --- a/src/components/home/contentAuth.vue +++ b/src/components/home/contentAuth.vue @@ -1,33 +1,51 @@ + + \ No newline at end of file diff --git a/src/components/list/list-settings-dropdown.vue b/src/components/list/list-settings-dropdown.vue index 35fc6356..b8a9082b 100644 --- a/src/components/list/list-settings-dropdown.vue +++ b/src/components/list/list-settings-dropdown.vue @@ -2,21 +2,22 @@ + - diff --git a/src/components/list/partials/filter-popup.vue b/src/components/list/partials/filter-popup.vue index efbfcbe3..d4caf9b4 100644 --- a/src/components/list/partials/filter-popup.vue +++ b/src/components/list/partials/filter-popup.vue @@ -29,9 +29,10 @@ diff --git a/src/components/misc/keyboard-shortcuts/shortcuts.js b/src/components/misc/keyboard-shortcuts/shortcuts.ts similarity index 78% rename from src/components/misc/keyboard-shortcuts/shortcuts.js rename to src/components/misc/keyboard-shortcuts/shortcuts.ts index bcc5014b..f68b7c35 100644 --- a/src/components/misc/keyboard-shortcuts/shortcuts.js +++ b/src/components/misc/keyboard-shortcuts/shortcuts.ts @@ -1,11 +1,24 @@ +import {RouteLocation} from 'vue-router' + import {isAppleDevice} from '@/helpers/isAppleDevice' const ctrl = isAppleDevice() ? '⌘' : 'ctrl' -export const KEYBOARD_SHORTCUTS = [ +interface Shortcut { + title: string + keys: string[] + combination?: 'then' +} + +interface ShortcutGroup { + title: string + available?: (route: RouteLocation) => boolean + shortcuts: Shortcut[] +} + +export const KEYBOARD_SHORTCUTS : ShortcutGroup[] = [ { title: 'keyboardShortcuts.general', - available: () => null, shortcuts: [ { title: 'keyboardShortcuts.toggleMenu', @@ -29,7 +42,7 @@ export const KEYBOARD_SHORTCUTS = [ }, { title: 'keyboardShortcuts.list.title', - available: (route) => route.name.startsWith('list.'), + available: (route) => (route.name as string)?.startsWith('list.'), shortcuts: [ { title: 'keyboardShortcuts.list.switchToListView', @@ -55,13 +68,7 @@ export const KEYBOARD_SHORTCUTS = [ }, { title: 'keyboardShortcuts.task.title', - available: (route) => [ - 'task.detail', - 'task.list.detail', - 'task.gantt.detail', - 'task.kanban.detail', - 'task.detail', - ].includes(route.name), + available: (route) => route.name === 'task.detail', shortcuts: [ { title: 'keyboardShortcuts.task.assign', diff --git a/src/components/misc/message.vue b/src/components/misc/message.vue index df60cc38..7ff84f9f 100644 --- a/src/components/misc/message.vue +++ b/src/components/misc/message.vue @@ -1,18 +1,35 @@ -.modal-enter .modal-container, -.modal-leave-active .modal-container { - transform: scale(0.9); + \ No newline at end of file diff --git a/src/components/namespace/namespace-search.vue b/src/components/namespace/namespace-search.vue index 19a5c3b2..1dd1354e 100644 --- a/src/components/namespace/namespace-search.vue +++ b/src/components/namespace/namespace-search.vue @@ -13,6 +13,7 @@ import {ref, computed} from 'vue' import {useStore} from 'vuex' import Multiselect from '@/components/input/multiselect.vue' +import NamespaceModel from '@/models/namespace' const emit = defineEmits(['selected']) @@ -25,7 +26,7 @@ function findNamespaces(newQuery: string) { query.value = newQuery } -function select(namespace) { +function select(namespace: NamespaceModel) { emit('selected', namespace) } diff --git a/src/components/namespace/namespace-settings-dropdown.vue b/src/components/namespace/namespace-settings-dropdown.vue index c2752578..3359fed9 100644 --- a/src/components/namespace/namespace-settings-dropdown.vue +++ b/src/components/namespace/namespace-settings-dropdown.vue @@ -16,13 +16,13 @@ {{ $t('menu.edit') }} {{ $t('menu.share') }} {{ $t('menu.newList') }} @@ -34,6 +34,7 @@ {{ $t('menu.archive') }} \ No newline at end of file diff --git a/src/components/sharing/userTeam.vue b/src/components/sharing/userTeam.vue index 007cfaa6..7b7debdd 100644 --- a/src/components/sharing/userTeam.vue +++ b/src/components/sharing/userTeam.vue @@ -365,3 +365,7 @@ export default { }, } + + \ No newline at end of file diff --git a/src/components/tasks/edit-task.vue b/src/components/tasks/edit-task.vue index e4ada125..718304c6 100644 --- a/src/components/tasks/edit-task.vue +++ b/src/components/tasks/edit-task.vue @@ -67,7 +67,7 @@ {{ $t('task.openDetail') }} @@ -97,6 +97,15 @@ export default { taskEditTask: TaskModel, } }, + computed: { + taskDetailRoute() { + return { + name: 'task.detail', + params: { id: this.taskEditTask.id }, + state: { backdropView: this.$router.currentRoute.value.fullPath }, + } + }, + }, components: { ColorPicker, Reminders, diff --git a/src/components/tasks/mixins/taskList.js b/src/components/tasks/mixins/taskList.js deleted file mode 100644 index a9b2e587..00000000 --- a/src/components/tasks/mixins/taskList.js +++ /dev/null @@ -1,101 +0,0 @@ -import TaskCollectionService from '@/services/taskCollection' - -// FIXME: merge with DEFAULT_PARAMS in filters.vue -export const getDefaultParams = () => ({ - sort_by: ['position', 'id'], - order_by: ['asc', 'desc'], - filter_by: ['done'], - filter_value: ['false'], - filter_comparator: ['equals'], - filter_concat: 'and', -}) - -/** - * This mixin provides a base set of methods and properties to get tasks on a list. - */ -export default { - data() { - return { - taskCollectionService: new TaskCollectionService(), - tasks: [], - - currentPage: 0, - - loadedList: null, - - searchTerm: '', - - showTaskFilter: false, - params: {...getDefaultParams()}, - } - }, - watch: { - // Only listen for query path changes - '$route.query': { - handler: 'loadTasksForPage', - immediate: true, - }, - '$route.path': 'loadTasksOnSavedFilter', - }, - methods: { - async loadTasks( - page, - search = '', - params = null, - forceLoading = false, - ) { - // Because this function is triggered every time on topNavigation, we're putting a condition here to only load it when we actually want to show tasks - // FIXME: This is a bit hacky -> Cleanup. - if ( - this.$route.name !== 'list.list' && - this.$route.name !== 'list.table' && - !forceLoading - ) { - return - } - - if (params === null) { - params = this.params - } - - if (search !== '') { - params.s = search - } - - const list = {listId: parseInt(this.$route.params.listId)} - - const currentList = { - id: list.listId, - params, - search, - page, - } - if (JSON.stringify(currentList) === JSON.stringify(this.loadedList) && !forceLoading) { - return - } - - this.tasks = [] - this.tasks = await this.taskCollectionService.getAll(list, params, page) - this.currentPage = page - this.loadedList = JSON.parse(JSON.stringify(currentList)) - }, - - loadTasksForPage(e) { - // The page parameter can be undefined, in the case where the user loads a new list from the side bar menu - let page = Number(e.page) - if (typeof e.page === 'undefined') { - page = 1 - } - let search = e.search - if (typeof e.search === 'undefined') { - search = '' - } - this.initTasks(page, search) - }, - loadTasksOnSavedFilter() { - if (typeof this.$route.params.listId !== 'undefined' && parseInt(this.$route.params.listId) < 0) { - this.loadTasks(1, '', null, true) - } - }, - }, -} \ No newline at end of file diff --git a/src/components/tasks/partials/attachments.vue b/src/components/tasks/partials/attachments.vue index a01a6201..7d507e5e 100644 --- a/src/components/tasks/partials/attachments.vue +++ b/src/components/tasks/partials/attachments.vue @@ -34,7 +34,7 @@ >
{{ a.file.name }}
-

+

{{ formatDateSince(a.created) }} @@ -289,21 +289,6 @@ export default { content: '·'; padding: 0 .25rem; } - - @media screen and (max-width: $mobile) { - &.collapses { - flex-direction: column; - - > span:not(:last-child):after, - > a:not(:last-child):after { - display: none; - } - - .user .username { - display: none; - } - } - } } } } @@ -341,6 +326,10 @@ export default { height: auto; text-shadow: var(--shadow-md); animation: bounce 2s infinite; + + @media (prefers-reduced-motion: reduce) { + animation: none; + } } .hint { @@ -357,6 +346,35 @@ export default { } } +.attachment-info-meta { + display: flex; + align-items: center; + + :deep(.user) { + display: flex !important; + align-items: center; + margin: 0 .5rem; + } + + @media screen and (max-width: $mobile) { + flex-direction: column; + align-items: flex-start; + + :deep(.user) { + margin: .5rem 0; + } + + > span:not(:last-child):after, + > a:not(:last-child):after { + display: none; + } + + .user .username { + display: none; + } + } +} + @keyframes bounce { from, 20%, @@ -382,4 +400,6 @@ export default { transform: translate3d(0, -4px, 0); } } + +@include modal-transition(); \ No newline at end of file diff --git a/src/components/tasks/partials/comments.vue b/src/components/tasks/partials/comments.vue index d5db45c3..23105771 100644 --- a/src/components/tasks/partials/comments.vue +++ b/src/components/tasks/partials/comments.vue @@ -162,7 +162,7 @@ import {mapState} from 'vuex' export default { name: 'comments', components: { - editor: AsyncEditor, + Editor: AsyncEditor, }, props: { taskId: { @@ -339,4 +339,6 @@ export default { .media-content { width: calc(100% - 48px - 2rem); } + +@include modal-transition(); \ No newline at end of file diff --git a/src/components/tasks/partials/date-table-cell.vue b/src/components/tasks/partials/date-table-cell.vue index e405c0b3..8df948c7 100644 --- a/src/components/tasks/partials/date-table-cell.vue +++ b/src/components/tasks/partials/date-table-cell.vue @@ -1,6 +1,8 @@ diff --git a/src/components/tasks/partials/description.vue b/src/components/tasks/partials/description.vue index 28fd5a88..07d136fd 100644 --- a/src/components/tasks/partials/description.vue +++ b/src/components/tasks/partials/description.vue @@ -38,7 +38,7 @@ import {mapState} from 'vuex' export default { name: 'description', components: { - editor: AsyncEditor, + Editor: AsyncEditor, }, data() { return { diff --git a/src/components/tasks/partials/editLabels.vue b/src/components/tasks/partials/editLabels.vue index ddc10dc3..f2d01934 100644 --- a/src/components/tasks/partials/editLabels.vue +++ b/src/components/tasks/partials/editLabels.vue @@ -19,19 +19,19 @@ :style="{'background': props.item.hexColor, 'color': props.item.textColor}" class="tag"> {{ props.item.title }} - +

@@ -453,8 +460,10 @@ import {CURRENT_LIST} from '@/store/mutation-types' import {uploadFile} from '@/helpers/attachments' import ChecklistSummary from '../../components/tasks/partials/checklist-summary' + export default { name: 'TaskDetailView', + compatConfig: { ATTR_FALSE_VALUE: false }, components: { ChecklistSummary, TaskSubscription, @@ -473,6 +482,14 @@ export default { description, heading, }, + + props: { + taskId: { + type: Number, + required: true, + }, + }, + data() { return { taskService: new TaskService(), @@ -523,10 +540,6 @@ export default { }, }, computed: { - taskId() { - const {id} = this.$route.params - return id === undefined ? id : Number(id) - }, currentList() { return this.$store.state[CURRENT_LIST] }, @@ -589,6 +602,9 @@ export default { } }, scrollToHeading() { + if(!this.$refs?.heading?.$el) { + return + } this.$refs.heading.$el.scrollIntoView({block: 'center'}) }, setActiveFields() { @@ -931,4 +947,14 @@ $flash-background-duration: 750ms; background: transparent; } } + +@media (prefers-reduced-motion: reduce) { + @keyframes flash-background { + 0% { + background: transparent; + } + } +} + +@include modal-transition(); \ No newline at end of file diff --git a/src/views/tasks/TaskDetailViewModal.vue b/src/views/tasks/TaskDetailViewModal.vue deleted file mode 100644 index 9b493246..00000000 --- a/src/views/tasks/TaskDetailViewModal.vue +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/src/views/teams/EditTeam.vue b/src/views/teams/EditTeam.vue index 059530e8..31c690f9 100644 --- a/src/views/teams/EditTeam.vue +++ b/src/views/teams/EditTeam.vue @@ -308,4 +308,6 @@ export default { padding: 0; } } + +@include modal-transition(); \ No newline at end of file diff --git a/src/views/user/Login.vue b/src/views/user/Login.vue index d04b9736..b6e5be03 100644 --- a/src/views/user/Login.vue +++ b/src/views/user/Login.vue @@ -1,9 +1,9 @@