diff --git a/.drone.yml b/.drone.yml index 4090e6a8f..dc82fa780 100644 --- a/.drone.yml +++ b/.drone.yml @@ -116,36 +116,15 @@ 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 pull: true @@ -157,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: @@ -665,6 +647,6 @@ steps: from_secret: crowdin_key --- kind: signature -hmac: 188ee90100c5fc5922a445e531e7a47453121edddb2a64a182eb23ed2bf602de +hmac: 997e1badebe484ac29557c4af356e63db4d3d57f3d32e92d482f117f8cec64da ... 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 8b8630fdb..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,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, }) @@ -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) }) @@ -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 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/task/task.spec.js b/cypress/integration/task/task.spec.js index 1b85e9921..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') }) @@ -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') 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 c6a645d59..21bd9c1d9 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 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/package.json b/package.json index eb3ff293f..f543b0f54 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "scripts": { "serve": "vite", "serve:dist-dev": "node scripts/serve-dist.js", - "serve:dist": "vite preview", + "serve:dist": "vite preview --port 4173", "build": "vite build && workbox copyLibraries dist/", "build:modern-only": "BUILD_MODERN_ONLY=true vite build && workbox copyLibraries dist/", "build:dev": "vite build -m development --outDir dist-dev/", @@ -18,82 +18,80 @@ "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.19.3", + "@sentry/vue": "6.19.3", "@types/is-touch-device": "1.0.0", - "@vue/compat": "3.2.27", - "@vueuse/core": "7.5.2", - "@vueuse/router": "7.5.3", + "@vue/compat": "3.2.31", + "@vueuse/core": "8.2.3", + "@vueuse/router": "8.2.3", "blurhash": "^1.1.4", "bulma-css-variables": "0.9.33", "camel-case": "4.1.2", - "codemirror": "5.65.0", + "codemirror": "5.65.2", "copy-to-clipboard": "3.3.1", "date-fns": "2.28.0", - "dompurify": "2.3.4", + "dompurify": "2.3.6", "easymde": "2.16.1", - "flatpickr": "4.6.9", + "flatpickr": "4.6.11", "flexsearch": "0.7.21", - "highlight.js": "11.4.0", + "highlight.js": "11.5.0", "is-touch-device": "1.0.1", "lodash.clonedeep": "4.5.0", "lodash.debounce": "4.0.8", - "marked": "4.0.10", + "marked": "4.0.12", "register-service-worker": "1.7.2", "snake-case": "3.0.4", - "ufo": "0.7.9", + "ufo": "0.8.3", "v-tooltip": "4.0.0-beta.17", - "vue": "3.2.27", - "vue-advanced-cropper": "2.7.1", + "vue": "3.2.31", + "vue-advanced-cropper": "2.8.1", "vue-drag-resize": "2.0.3", "vue-flatpickr-component": "9.0.5", - "vue-i18n": "9.2.0-beta.28", - "vue-router": "4.0.12", + "vue-i18n": "9.2.0-beta.30", + "vue-router": "4.0.14", "vuedraggable": "4.1.0", "vuex": "4.0.2", - "workbox-precaching": "6.4.2" + "workbox-precaching": "6.5.2" }, "devDependencies": { "@4tw/cypress-drag-drop": "2.1.0", - "@fortawesome/fontawesome-svg-core": "1.2.36", - "@fortawesome/free-regular-svg-icons": "5.15.4", - "@fortawesome/free-solid-svg-icons": "5.15.4", + "@faker-js/faker": "6.1.1", + "@fortawesome/fontawesome-svg-core": "6.1.1", + "@fortawesome/free-regular-svg-icons": "6.1.1", + "@fortawesome/free-solid-svg-icons": "6.1.1", "@fortawesome/vue-fontawesome": "3.0.0-5", - "@types/flexsearch": "0.7.2", - "@typescript-eslint/eslint-plugin": "5.9.1", - "@typescript-eslint/parser": "5.9.1", - "@vitejs/plugin-legacy": "1.6.4", - "@vitejs/plugin-vue": "2.0.1", + "@types/flexsearch": "0.7.3", + "@typescript-eslint/eslint-plugin": "5.17.0", + "@typescript-eslint/parser": "5.17.0", + "@vitejs/plugin-legacy": "1.8.0", + "@vitejs/plugin-vue": "2.3.1", "@vue/eslint-config-typescript": "10.0.0", - "autoprefixer": "10.4.2", - "axios": "0.24.0", - "browserslist": "4.19.1", - "caniuse-lite": "1.0.30001299", - "cypress": "9.2.1", - "cypress-file-upload": "5.0.8", - "esbuild": "0.14.11", - "eslint": "8.7.0", - "eslint-plugin-vue": "8.3.0", - "express": "4.17.2", - "faker": "5.5.3", - "netlify-cli": "8.8.2", - "happy-dom": "2.25.2", - "postcss": "8.4.5", - "postcss-preset-env": "7.2.3", - "rollup": "2.64.0", - "rollup-plugin-visualizer": "5.5.4", - "sass": "1.48.0", - "slugify": "1.6.5", - "typescript": "4.5.4", - "vite": "2.7.12", + "autoprefixer": "10.4.4", + "axios": "0.26.1", + "browserslist": "4.20.2", + "caniuse-lite": "1.0.30001324", + "cypress": "9.5.3", + "esbuild": "0.14.30", + "eslint": "8.12.0", + "eslint-plugin-vue": "8.5.0", + "express": "4.17.3", + "happy-dom": "2.55.0", + "netlify-cli": "9.13.5", + "postcss": "8.4.12", + "postcss-preset-env": "7.4.3", + "rollup": "2.70.1", + "rollup-plugin-visualizer": "5.6.0", + "sass": "1.49.11", + "typescript": "4.6.3", + "vite": "2.9.1", "vite-plugin-pwa": "0.11.13", - "vite-svg-loader": "3.1.2", - "vitest": "0.1.17", - "vue-tsc": "0.30.4", - "wait-on": "6.0.0", - "workbox-cli": "6.4.2" + "vite-svg-loader": "3.2.0", + "vitest": "0.8.2", + "vue-tsc": "0.33.9", + "wait-on": "6.0.1", + "workbox-cli": "6.5.2" }, "eslintConfig": { "root": true, @@ -131,7 +129,7 @@ "parser": "vue-eslint-parser", "parserOptions": { "parser": "@typescript-eslint/parser", - "ecmaVersion": 2021 + "ecmaVersion": 2022 }, "ignorePatterns": [ "*.test.*", @@ -147,5 +145,5 @@ } }, "license": "AGPL-3.0-or-later", - "packageManager": "yarn@1.22.17" + "packageManager": "yarn@1.22.18" } 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 06a818506..4080a6cc9 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,7 +1,7 @@ - diff --git a/src/components/misc/keyboard-shortcuts/shortcuts.js b/src/components/misc/keyboard-shortcuts/shortcuts.ts similarity index 54% rename from src/components/misc/keyboard-shortcuts/shortcuts.js rename to src/components/misc/keyboard-shortcuts/shortcuts.ts index bcc5014b4..e69722f92 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', @@ -17,6 +30,36 @@ export const KEYBOARD_SHORTCUTS = [ }, ], }, + { + title: 'keyboardShortcuts.navigation.title', + shortcuts: [ + { + title: 'keyboardShortcuts.navigation.overview', + keys: ['g', 'o'], + combination: 'then', + }, + { + title: 'keyboardShortcuts.navigation.upcoming', + keys: ['g', 'u'], + combination: 'then', + }, + { + title: 'keyboardShortcuts.navigation.namespaces', + keys: ['g', 'n'], + combination: 'then', + }, + { + title: 'keyboardShortcuts.navigation.labels', + keys: ['g', 'a'], + combination: 'then', + }, + { + title: 'keyboardShortcuts.navigation.teams', + keys: ['g', 'm'], + combination: 'then', + }, + ], + }, { title: 'list.kanban.title', available: (route) => route.name === 'list.kanban', @@ -29,7 +72,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,14 +98,12 @@ 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.done', + keys: ['t'], + }, { title: 'keyboardShortcuts.task.assign', keys: ['a'], @@ -83,6 +124,14 @@ export const KEYBOARD_SHORTCUTS = [ title: 'keyboardShortcuts.task.related', keys: ['r'], }, + { + title: 'keyboardShortcuts.task.move', + keys: ['m'], + }, + { + title: 'keyboardShortcuts.task.color', + keys: ['c'], + }, ], }, ] diff --git a/src/components/misc/message.vue b/src/components/misc/message.vue index df60cc384..7ff84f9fe 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 19a5c3b2b..1dd1354eb 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 c27525789..3359fed95 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') }} n.readAt === null).length }, notifications() { - return this.allNotifications.filter(n => n.name !== '') + return this.allNotifications ? this.allNotifications.filter(n => n.name !== '') : [] }, ...mapState({ userInfo: state => state.auth.info, diff --git a/src/components/sharing/linkSharing.vue b/src/components/sharing/linkSharing.vue index d07577ded..99da29b98 100644 --- a/src/components/sharing/linkSharing.vue +++ b/src/components/sharing/linkSharing.vue @@ -264,4 +264,6 @@ export default { .sharables-list:not(.card-content) { overflow-y: auto } + +@include modal-transition(); \ No newline at end of file diff --git a/src/components/sharing/userTeam.vue b/src/components/sharing/userTeam.vue index 007cfaa68..7b7debddc 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/add-task.vue b/src/components/tasks/add-task.vue index 0ddc61555..c5d00b12f 100644 --- a/src/components/tasks/add-task.vue +++ b/src/components/tasks/add-task.vue @@ -5,12 +5,13 @@