diff --git a/.commitlintrc.js b/.commitlintrc.js deleted file mode 100644 index 98ee7dfc2..000000000 --- a/.commitlintrc.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - extends: ['@commitlint/config-conventional'], -} diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index e505490a9..000000000 --- a/.eslintignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules/ -packages/devdocs/.docusaurus -packages/devdocs/build -packages/server/scripts/local diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 1d5f9099d..000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,13 +0,0 @@ -const { eslint } = require('@coko/lint') - -// Added parserOptions to remove the @decorators issues -eslint.parserOptions = { - ecmaVersion: 6, - ecmaFeatures: { - legacyDecorators: true, - experimentalObjectRestSpread: true, - }, -} -eslint.rules['react/jsx-props-no-spreading'] = 0 - -module.exports = eslint diff --git a/.gitignore b/.gitignore index 07e5608b6..19c8ba147 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,5 @@ packages/server/scripts/local !**/.yarn/releases !**/.yarn/sdks !**/.yarn/versions + +.claude diff --git a/.husky/commit-msg b/.husky/commit-msg deleted file mode 100755 index 04bce2461..000000000 --- a/.husky/commit-msg +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -./node_modules/.bin/commitlint --edit diff --git a/.lintstagedrc.js b/.lintstagedrc.js deleted file mode 100644 index f067f2663..000000000 --- a/.lintstagedrc.js +++ /dev/null @@ -1,3 +0,0 @@ -const { lintstaged } = require('@coko/lint') - -module.exports = lintstaged diff --git a/.prettierrc.js b/.prettierrc.js deleted file mode 100644 index ef9d33916..000000000 --- a/.prettierrc.js +++ /dev/null @@ -1,3 +0,0 @@ -const { prettier } = require('@coko/lint') - -module.exports = prettier diff --git a/.prettierrc.mjs b/.prettierrc.mjs new file mode 100644 index 000000000..365a6aa0a --- /dev/null +++ b/.prettierrc.mjs @@ -0,0 +1,3 @@ +import { prettier } from '@coko/lint' + +export default prettier diff --git a/.renovaterc b/.renovaterc deleted file mode 100644 index 40eb80873..000000000 --- a/.renovaterc +++ /dev/null @@ -1,16 +0,0 @@ -{ - "enabled": false, - "assignees": ["benwh"], - "labels": ["renovate"], - "rebaseWhen": "auto", - "automerge": true, - "semanticCommits": true, - "prConcurrentLimit": 10, - "major": { - "automerge": false - }, - "extends": [ - "config:base" - ] -} - diff --git a/.stylelintignore b/.stylelintignore deleted file mode 100644 index b7ea6fa9b..000000000 --- a/.stylelintignore +++ /dev/null @@ -1 +0,0 @@ -packages/client/app/components/wax-collab/src/CustomWaxToolGroups/ListsService/BlockQuoteService/BlockQuoteToolGroupService/BlockQuoteTool.js diff --git a/.stylelintrc.js b/.stylelintrc.js deleted file mode 100644 index e1444b3fa..000000000 --- a/.stylelintrc.js +++ /dev/null @@ -1,17 +0,0 @@ -const { stylelint } = require('@coko/lint') - -stylelint.ignoreFiles = [ - 'packages/client/app/components/wax-collab/src/CustomWaxToolGroups/JatsSideMenuToolGroupService/JatsSideMenu.js', - 'packages/client/app/components/wax-collab/src/CustomWaxToolGroups/KotahiBlockDropDownToolGroupService/KotahiBlockDropDown.js', - 'packages/client/app/components/wax-collab/src/CustomWaxToolGroups/AnystyleService/AnyStyleToolGroupService/AnyStyle.js', - 'packages/client/app/components/wax-collab/src/CustomWaxToolGroups/JatsSideMenuToolGroupService/menugroups/AcknowledgementsGroup.js', - 'packages/client/app/components/wax-collab/src/CustomWaxToolGroups/JatsSideMenuToolGroupService/menugroups/AppendixGroup.js', - 'packages/client/app/components/wax-collab/src/CustomWaxToolGroups/JatsSideMenuToolGroupService/menugroups/CitationGroup.js', - 'packages/client/app/components/wax-collab/src/CustomWaxToolGroups/JatsSideMenuToolGroupService/menugroups/FrontMatterGroup.js', - 'packages/client/app/components/wax-collab/src/CustomWaxToolGroups/JatsSideMenuToolGroupService/menugroups/FundingGroup.js', - 'packages/client/app/components/wax-collab/src/CustomWaxToolGroups/JatsSideMenuToolGroupService/menugroups/GlossaryGroup.js', - 'packages/client/app/components/wax-collab/src/CustomWaxToolGroups/JatsSideMenuToolGroupService/menugroups/KeywordGroup.js', - 'packages/client/app/components/wax-collab/src/CustomWaxToolGroups/ListsService/ListToolGroupService/Lists.js', -] - -module.exports = stylelint diff --git a/.stylelintrc.mjs b/.stylelintrc.mjs new file mode 100644 index 000000000..c36237462 --- /dev/null +++ b/.stylelintrc.mjs @@ -0,0 +1,17 @@ +import { stylelint } from '@coko/lint' + +// stylelint.ignoreFiles = [ +// 'packages/client/app/components/wax-collab/src/CustomWaxToolGroups/JatsSideMenuToolGroupService/JatsSideMenu.js', +// 'packages/client/app/components/wax-collab/src/CustomWaxToolGroups/KotahiBlockDropDownToolGroupService/KotahiBlockDropDown.js', +// 'packages/client/app/components/wax-collab/src/CustomWaxToolGroups/AnystyleService/AnyStyleToolGroupService/AnyStyle.js', +// 'packages/client/app/components/wax-collab/src/CustomWaxToolGroups/JatsSideMenuToolGroupService/menugroups/AcknowledgementsGroup.js', +// 'packages/client/app/components/wax-collab/src/CustomWaxToolGroups/JatsSideMenuToolGroupService/menugroups/AppendixGroup.js', +// 'packages/client/app/components/wax-collab/src/CustomWaxToolGroups/JatsSideMenuToolGroupService/menugroups/CitationGroup.js', +// 'packages/client/app/components/wax-collab/src/CustomWaxToolGroups/JatsSideMenuToolGroupService/menugroups/FrontMatterGroup.js', +// 'packages/client/app/components/wax-collab/src/CustomWaxToolGroups/JatsSideMenuToolGroupService/menugroups/FundingGroup.js', +// 'packages/client/app/components/wax-collab/src/CustomWaxToolGroups/JatsSideMenuToolGroupService/menugroups/GlossaryGroup.js', +// 'packages/client/app/components/wax-collab/src/CustomWaxToolGroups/JatsSideMenuToolGroupService/menugroups/KeywordGroup.js', +// 'packages/client/app/components/wax-collab/src/CustomWaxToolGroups/ListsService/ListToolGroupService/Lists.js', +// ] + +export default stylelint diff --git a/babel.config.js b/babel.config.js deleted file mode 100644 index 0a73c8131..000000000 --- a/babel.config.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = { - presets: ['@babel/preset-env', '@babel/preset-react'], - plugins: [ - ['@babel/plugin-proposal-decorators', { legacy: true }], - 'babel-plugin-parameter-decorator', - 'babel-plugin-styled-components', - ['@babel/plugin-proposal-class-properties', { loose: true }], - ], -} diff --git a/cypress.config.js b/cypress.config.js index 84240f407..fb71d3c2c 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -1,9 +1,6 @@ const { defineConfig } = require('cypress') -const path = require('path') -require('dotenv').config({ path: path.join(__dirname, './.env') }) - -const serverUrl = process.env.SERVER_URL +const serverUrl = process.env.SERVER_URL || 'http://localhost:3000' const e2eApiUrl = `${serverUrl}/api/e2e` const restoreUrl = `${e2eApiUrl}/restore` const seedUrl = `${e2eApiUrl}/seed` @@ -11,27 +8,9 @@ const createTokenUrl = `${e2eApiUrl}/createToken` const seedFormsUrl = `${e2eApiUrl}/seedForms` module.exports = defineConfig({ - defaultCommandTimeout: 20000, viewportWidth: 1200, e2e: { baseUrl: 'http://localhost:4000', - - // 🛠 Correct place for on() and task handlers - // setupNodeEvents(on, config) { - // on('task', { - // restore(name) { - // if (name === 'email_notification') { - // console.log('Restoring email_notification test data...') - // // Your custom logic here - // return null // or a promise, or true/false - // } - - // return null - // }, - // }) - - // return config - // }, }, screenshotOnRunFailure: false, video: false, diff --git a/cypress/.eslintrc.js b/cypress/.eslintrc.js deleted file mode 100644 index 57feec7ef..000000000 --- a/cypress/.eslintrc.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - rules: { - 'jest/no-disabled-tests': 0, - }, -} diff --git a/cypress/e2e/a-journal/000-login_spec.cy.js b/cypress/e2e/a-journal/000-login_spec.cy.js index 798e5a3c8..026187254 100644 --- a/cypress/e2e/a-journal/000-login_spec.cy.js +++ b/cypress/e2e/a-journal/000-login_spec.cy.js @@ -1,4 +1,5 @@ -/* eslint-disable jest/expect-expect */ +/* eslint-disable promise/always-return */ + import { DashboardPage } from '../../page-object/dashboard-page' import { Menu } from '../../page-object/page-component/menu' import { dashboard } from '../../support/routes' @@ -12,7 +13,7 @@ describe('Login test', () => { cy.request('POST', `${seedUrl}/new_user`) // login as admin and validate admin is logged in - // eslint-disable-next-line jest/valid-expect-in-promise + cy.fixture('role_names').then(name => { cy.login(name.role.admin, dashboard) // enter email diff --git a/cypress/e2e/a-journal/001-docx_submission_spec.cy.js b/cypress/e2e/a-journal/001-docx_submission_spec.cy.js index 0ab5f0d6d..ef2ec0825 100644 --- a/cypress/e2e/a-journal/001-docx_submission_spec.cy.js +++ b/cypress/e2e/a-journal/001-docx_submission_spec.cy.js @@ -1,12 +1,10 @@ -/* eslint-disable jest/no-commented-out-tests */ -/* eslint-disable jest/expect-expect */ +/* eslint-disable promise/always-return */ import { DashboardPage } from '../../page-object/dashboard-page' import { SubmissionFormPage } from '../../page-object/submission-form-page' import { dashboard } from '../../support/routes' describe('Create a new submission', () => { - // eslint-disable-next-line jest/no-disabled-tests before(() => { const restoreUrl = Cypress.config('restoreUrl') cy.request('POST', `${restoreUrl}/commons.bootstrap`) @@ -19,6 +17,7 @@ describe('Create a new submission', () => { }) DashboardPage.clickSubmissionButton() // Click on new submission + cy.url().should('include', 'newSubmission') }) it('can upload a manuscript and add a title', () => { diff --git a/cypress/e2e/a-journal/002-assign_reviewers_spec.cy.js b/cypress/e2e/a-journal/002-assign_reviewers_spec.cy.js index b996c8b35..cba556e33 100644 --- a/cypress/e2e/a-journal/002-assign_reviewers_spec.cy.js +++ b/cypress/e2e/a-journal/002-assign_reviewers_spec.cy.js @@ -1,6 +1,5 @@ -/* eslint-disable jest/expect-expect */ +/* eslint-disable promise/always-return */ -import { seniorEditor } from '../../fixtures/role_names' import { ControlPage } from '../../page-object/control-page' import { DashboardPage } from '../../page-object/dashboard-page' import { Menu } from '../../page-object/page-component/menu' @@ -16,15 +15,7 @@ describe('Editor assigning reviewers', () => { }) it('can assign 6 reviewers', () => { - // login as seniorEditor - cy.login(seniorEditor, dashboard) - cy.url().should('eq', `${Cypress.config().baseUrl}/journal/dashboard`) - cy.reload() - cy.awaitDisappearSpinner() - - // eslint-disable-next-line jest/valid-expect-in-promise cy.fixture('role_names').then(name => { - // login as seniorEditor cy.login(name.role.seniorEditor, dashboard) cy.reload() DashboardPage.clickDashboardTab(2) @@ -33,7 +24,7 @@ describe('Editor assigning reviewers', () => { // Invite all the reviewers cy.reload() - name.role.reviewers.forEach((reviewer, index) => { + name.role.reviewers.forEach(reviewer => { ControlPage.clickInviteReviewerDropdown() ControlPage.inviteReviewer(reviewer) // Ensure modal closes before continuing @@ -49,6 +40,6 @@ describe('Editor assigning reviewers', () => { // Go to dashboard and verify number of invited reviewer Menu.clickDashboard() - cy.get('.ReviewStatusDonut__CenterLabel-sc-76zxfe-1').contains('6') + cy.getByDataTestId('donut-center-label').contains('6') }) }) diff --git a/cypress/e2e/a-journal/003-review_spec.cy.js b/cypress/e2e/a-journal/003-review_spec.cy.js index f4883e66d..5d92f887c 100644 --- a/cypress/e2e/a-journal/003-review_spec.cy.js +++ b/cypress/e2e/a-journal/003-review_spec.cy.js @@ -1,5 +1,5 @@ -/* eslint-disable jest/valid-expect-in-promise */ -/* eslint-disable jest/expect-expect */ +/* eslint-disable promise/always-return */ + import { DashboardPage } from '../../page-object/dashboard-page' import { ReviewPage } from '../../page-object/review-page' import { dashboard } from '../../support/routes' @@ -63,14 +63,14 @@ describe('Completing reviews', () => { cy.get('[data-testid="control-panel-team"]').click() cy.awaitDisappearSpinner() cy.get( - '[class*=KanbanBoard__Kanban] > :nth-child(1) > [class*=KanbanBoard__CardsWrapper] > [class*=KanbanCard]', + '[data-testid=kanban] > :nth-child(1) > [data-testid=kanban-cards-wrapper] > [data-testid=kanban-card]', ).should('contain', name.role.reviewers[5]) // ControlPage.getInvitedReviewer cy.get( - '[class*=KanbanBoard__Kanban] > :nth-child(2) > [class*=KanbanBoard__CardsWrapper] > [class*=KanbanCard]', + '[data-testid=kanban] > :nth-child(2) > [data-testid=kanban-cards-wrapper] > [data-testid=kanban-card]', ).should('contain', name.role.reviewers[3]) cy.get( - '[class*=KanbanBoard__Kanban] > :nth-child(4) > [class*=KanbanBoard__CardsWrapper] > [class*=KanbanCard]', + '[data-testid=kanban] > :nth-child(4) > [data-testid=kanban-cards-wrapper] > [data-testid=kanban-card]', ).should('contain', name.role.reviewers[1]) cy.contains('See Declined (1)').should('exist') @@ -96,7 +96,7 @@ const doReview = (name, reviewData) => { DashboardPage.clickDoReview() cy.awaitDisappearSpinner() cy.contains('Type of Research Object').should('exist') - cy.get('[class*=TabsContainer]').contains('Review').click() + cy.get('[data-testid=tab-container]').contains('Review').click() ReviewPage.getReviewCommentField().focus().type('comment', { delay: 200 }) ReviewPage.getReviewCommentField().fillInput(reviewData.comment) if (reviewData.radioButton === 'accept') @@ -106,9 +106,6 @@ const doReview = (name, reviewData) => { if (reviewData.radioButton === 'revise') ReviewPage.clickReviseRadioButton() - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.wait(1000) - // Submit the review ReviewPage.clickSubmitButton() cy.contains('Confirm your review').should('exist') diff --git a/cypress/e2e/a-journal/004-assigning_editor_spec.cy.js b/cypress/e2e/a-journal/004-assigning_editor_spec.cy.js index c458592d8..d64ee30dd 100644 --- a/cypress/e2e/a-journal/004-assigning_editor_spec.cy.js +++ b/cypress/e2e/a-journal/004-assigning_editor_spec.cy.js @@ -1,6 +1,5 @@ -/* eslint-disable no-unused-vars */ -/* eslint-disable jest/valid-expect-in-promise */ -/* eslint-disable jest/expect-expect */ +/* eslint-disable promise/always-return */ + import { Menu } from '../../page-object/page-component/menu' import { ManuscriptsPage } from '../../page-object/manuscripts-page' import { ControlPage } from '../../page-object/control-page' @@ -16,17 +15,12 @@ describe('Assigning editors and decision reject', () => { cy.fixture('submission_form_data').then(data => { cy.fixture('role_names').then(name => { - // login as admin cy.login(name.role.admin, dashboard) - // select Control on the Manuscripts page Menu.clickManuscripts() ManuscriptsPage.selectOptionWithText('Control') - cy.reload() - // added a reload here because tests were failing on an unhandled promise. - // assign seniorEditor ControlPage.clickAssignSeniorEditorDropdown() ControlPage.selectDropdownOptionByName(name.role.seniorEditor) @@ -37,15 +31,23 @@ describe('Assigning editors and decision reject', () => { ControlPage.selectDropdownOptionByName(name.role.admin) // reject submission - cy.log('Admin rejects a submission.') - ControlPage.clickDecisionTab(1) + ControlPage.clickDecisionTab() + + cy.intercept('POST', '/graphql').as('autoSaveEditor') ControlPage.fillInDecision(data.rejectedDecision) + cy.wait('@autoSaveEditor') + + cy.intercept('POST', '/graphql').as('autoSaveReject') ControlPage.clickReject() + cy.wait('@autoSaveReject') + + /* eslint-disable-next-line cypress/no-unnecessary-waiting */ + cy.wait(1000) ControlPage.clickSubmit() ControlPage.checkSvgExists() }) }) - cy.contains('Dashboard').click() + // cy.contains('Dashboard').click() }) }) diff --git a/cypress/e2e/a-journal/005-decision_spec.cy.js b/cypress/e2e/a-journal/005-decision_spec.cy.js index 2b5f68856..47d73cd19 100644 --- a/cypress/e2e/a-journal/005-decision_spec.cy.js +++ b/cypress/e2e/a-journal/005-decision_spec.cy.js @@ -1,4 +1,5 @@ -/* eslint-disable jest/expect-expect */ +/* eslint-disable promise/always-return */ + import { DashboardPage } from '../../page-object/dashboard-page' import { ControlPage } from '../../page-object/control-page' import { ManuscriptsPage } from '../../page-object/manuscripts-page' @@ -7,7 +8,7 @@ import { dashboard } from '../../support/routes' const decisionTextContent = 'Please fix Foo in the Paper!' const decisionFileName = 'test-pdf.pdf' -const decisinFilePath = 'cypress/fixtures/test-pdf.pdf' +const decisionFilePath = 'cypress/fixtures/test-pdf.pdf' describe('Completing a decision', () => { before(() => { @@ -17,11 +18,10 @@ describe('Completing a decision', () => { cy.request('POST', `${restoreUrl}/commons.bootstrap`) cy.request('POST', `${seedUrl}/three_reviews_completed`) - // eslint-disable-next-line jest/valid-expect-in-promise cy.fixture('role_names').then(name => { /* Group Manager assigns Editor to manuscript */ cy.login(name.role.admin, dashboard) - // eslint-disable-next-line jest/valid-expect-in-promise + DashboardPage.clickManuscriptNavButton() ManuscriptsPage.selectOptionWithText('Control') ControlPage.getAssignSeniorEditorDropdown().click({ force: true }) @@ -32,29 +32,34 @@ describe('Completing a decision', () => { beforeEach(() => { cy.fixture('role_names').then(name => { cy.login(name.role.seniorEditor, dashboard) - DashboardPage.clickDashboardTab(2) + DashboardPage.clickEditingQueueTab() DashboardPage.clickControl() // Navigate to Control Page - ControlPage.clickDecisionTab(1) + ControlPage.clickDecisionTab() ControlPage.getPublishButton().should('be.disabled') // Verify publish button is disabled }) }) it('editor decided "revise" and then the author submits a new version', () => { - // eslint-disable-next-line jest/valid-expect-in-promise cy.fixture('role_names').then(name => { ControlPage.clickDecisionTextInput() ControlPage.getDecisionTextInput().type(decisionTextContent) + cy.get('[data-testid="dropzone"]:first > input').selectFile( - decisinFilePath, + decisionFilePath, { force: true, }, ) + + cy.intercept('POST', '/graphql').as('autoSave') ControlPage.clickRevise() + cy.wait('@autoSave') + + /* eslint-disable-next-line cypress/no-unnecessary-waiting */ + cy.wait(1000) ControlPage.clickSubmitDecisionButton() // Submit the decision ControlPage.checkSvgExists() // Check appears in front of button - cy.log('Author revises and submits new version.') /* View Decision as an Author */ cy.login(name.role.author, dashboard) // Login as an Author DashboardPage.getSubmittedManuscript().click() // Click on first MySubmission @@ -82,11 +87,22 @@ describe('Completing a decision', () => { it('editor accepts the new version', () => { /* Editor Workflow: Approve the new Manuscript version */ - ControlPage.getDecisionTextInput().type('Great Paper!') - ControlPage.getDecisionFileInput().eq(0).selectFile(decisinFilePath, { + ControlPage.clickDecisionTextInput() + ControlPage.getDecisionTextInput() + .scrollIntoView() + .click({ force: true }) + .type('Great Paper!') + + ControlPage.getDecisionFileInput().eq(0).selectFile(decisionFilePath, { force: true, }) + + cy.intercept('POST', '/graphql').as('autoSaveDecision') ControlPage.clickAccept() + cy.wait('@autoSaveDecision') + + /* eslint-disable-next-line cypress/no-unnecessary-waiting */ + cy.wait(1000) ControlPage.clickSubmitDecisionButton() // Submit the decision ControlPage.checkSvgExists() // The below should be fixed by #1872 !!! diff --git a/cypress/e2e/a-journal/006-report_spec.cy.js b/cypress/e2e/a-journal/006-report_spec.cy.js index 31a808261..3dfc52500 100644 --- a/cypress/e2e/a-journal/006-report_spec.cy.js +++ b/cypress/e2e/a-journal/006-report_spec.cy.js @@ -1,4 +1,5 @@ -/* eslint-disable jest/expect-expect */ +/* eslint-disable promise/always-return, promise/catch-or-return, promise/no-nesting */ + import { dashboard } from '../../support/routes' import { Menu } from '../../page-object/page-component/menu' import { ReportPage, REVIEWER_COLUMNS } from '../../page-object/reports-page' @@ -11,7 +12,6 @@ describe('Report Page', () => { cy.request('POST', `${restoreUrl}/commons.bootstrap`) cy.request('POST', `${seedUrl}/three_reviews_completed`) - // eslint-disable-next-line jest/valid-expect-in-promise cy.fixture('role_names').then(name => { cy.login(name.role.admin, dashboard) }) @@ -43,7 +43,6 @@ describe('Report Page', () => { // TO-DO: The features: recommendedToAccept, recommendedToRevise, // and recommendedToReject are not included since htey do not work in the app - // eslint-disable-next-line jest/valid-expect-in-promise cy.fixture('report_data').then(({ reviewersData }) => { reviewersData.forEach(reviewer => { const { diff --git a/cypress/e2e/a-journal/007-formbuilder_spec.cy.js b/cypress/e2e/a-journal/007-formbuilder_spec.cy.js index 47080b413..d102dcbde 100644 --- a/cypress/e2e/a-journal/007-formbuilder_spec.cy.js +++ b/cypress/e2e/a-journal/007-formbuilder_spec.cy.js @@ -1,4 +1,5 @@ -/* eslint-disable jest/expect-expect, cypress/unsafe-to-chain-command */ +/* eslint-disable promise/always-return */ +/* eslint-disable cypress/unsafe-to-chain-command */ import { FormsPage } from '../../page-object/forms-page' import { Menu } from '../../page-object/page-component/menu' @@ -14,8 +15,6 @@ describe('Form builder', () => { }) it('viewing and adding fields in Submission, Review and Decision forms', () => { - // login as admin - // eslint-disable-next-line jest/valid-expect-in-promise cy.fixture('role_names').then(name => { cy.login(name.role.admin, dashboard) }) @@ -34,6 +33,7 @@ describe('Form builder', () => { FormsPage.getFieldValidate().scrollIntoView().click() cy.get('[class*="react-select__option"]').eq(0).click() cy.contains('Save').click() + // adding a field in submission form cy.get('[title="Add a field..."]').click() cy.getByDataTestId('fieldType').click() @@ -41,7 +41,6 @@ describe('Form builder', () => { cy.contains('Save').click() // for review field - // Menu.clickSettings() cy.contains('Review').click() FormsPage.getFormTitleTab(0).should('contain', 'Review') FormsPage.clickFormOption(1) @@ -60,7 +59,6 @@ describe('Form builder', () => { cy.contains('Save').click() // for decision field - // Menu.clickSettings() cy.contains('Decision').click() FormsPage.getFormTitleTab(0).should('contain', 'Decision') FormsPage.clickFormOption(1) @@ -71,7 +69,7 @@ describe('Form builder', () => { // adding a field in decision form cy.get('[title="Add a field..."]').click({ force: true }) cy.get('[data-testid="fieldType"]').click() - // cy.get('button') + cy.get('[class*="react-select__option"]') .contains('Rich text') .scrollIntoView() @@ -82,7 +80,7 @@ describe('Form builder', () => { it('cannot submit manuscript without filling in the required fields', () => { // login as author and attempt to submit an incomplete submission form - // eslint-disable-next-line jest/valid-expect-in-promise + cy.fixture('role_names').then(name => { cy.login(name.role.author, dashboard) @@ -100,7 +98,6 @@ describe('Form builder', () => { }) // Change the title so that we can look for it - // eslint-disable-next-line jest/valid-expect-in-promise cy.fixture('submission_form_data').then(data => { SubmissionFormPage.fillInField('submission.$title', data.newTitle) SubmissionFormPage.clickSubmitResearch() diff --git a/cypress/e2e/b-colab-prc/100-login_page_spec.cy.js b/cypress/e2e/b-colab-prc/100-login_page_spec.cy.js index e2f743bee..86fc6c7e6 100644 --- a/cypress/e2e/b-colab-prc/100-login_page_spec.cy.js +++ b/cypress/e2e/b-colab-prc/100-login_page_spec.cy.js @@ -1,4 +1,5 @@ -/* eslint-disable jest/expect-expect */ +/* eslint-disable promise/always-return */ + import Color from 'color' import { Menu } from '../../page-object/page-component/menu' import { LoginPage } from '../../page-object/login-page' @@ -11,7 +12,6 @@ describe('Login page tests', () => { }) it('page should display prc branding settings', () => { - // eslint-disable-next-line jest/valid-expect-in-promise cy.fixture('branding_settings').then(settings => { const primaryAsRgb = Color(settings.prc.primaryColor).string() @@ -45,7 +45,6 @@ describe('Login page tests', () => { }) it('branding settings should be visible after login', () => { - // eslint-disable-next-line jest/valid-expect-in-promise cy.fixture('branding_settings').then(settings => { Menu.getBackground() .should('have.css', 'background') diff --git a/cypress/e2e/b-colab-prc/101-profile_page_spec.cy.js b/cypress/e2e/b-colab-prc/101-profile_page_spec.cy.js index 22fd14a4c..615d8338b 100644 --- a/cypress/e2e/b-colab-prc/101-profile_page_spec.cy.js +++ b/cypress/e2e/b-colab-prc/101-profile_page_spec.cy.js @@ -1,4 +1,5 @@ -/* eslint-disable jest/expect-expect,jest/valid-expect-in-promise */ +/* eslint-disable promise/always-return */ + import { dashboard, profile, diff --git a/cypress/e2e/b-colab-prc/102-manuscript_page_label_and_tooltip_spec.cy.js b/cypress/e2e/b-colab-prc/102-manuscript_page_label_and_tooltip_spec.cy.js index 29167ff89..5c6eeb1ae 100644 --- a/cypress/e2e/b-colab-prc/102-manuscript_page_label_and_tooltip_spec.cy.js +++ b/cypress/e2e/b-colab-prc/102-manuscript_page_label_and_tooltip_spec.cy.js @@ -1,4 +1,5 @@ -/* eslint-disable jest/expect-expect */ +/* eslint-disable promise/always-return */ + import { dashboard } from '../../support/routes1' import { ManuscriptsPage } from '../../page-object/manuscripts-page' import { NewSubmissionPage } from '../../page-object/new-submission-page' @@ -11,7 +12,6 @@ describe('Checking manuscripts page: label selection and tooltip', () => { const restoreUrl = Cypress.config('restoreUrl') cy.request('POST', `${restoreUrl}/commons.colab_bootstrap`) - // eslint-disable-next-line jest/valid-expect-in-promise cy.fixture('role_names').then(name => { cy.login(name.role.admin, dashboard) }) @@ -69,7 +69,6 @@ describe('Checking manuscripts page: label selection and tooltip', () => { it('check tooltip text', () => { cy.contains('Continue Submission').click() - // eslint-disable-next-line jest/valid-expect-in-promise cy.fixture('submission_form_data').then(data => { SubmissionFormPage.fillInAbstractColab(data.abstract) Menu.clickManuscriptsAndAssertPageLoad() @@ -85,7 +84,6 @@ describe('Checking manuscripts page: label selection and tooltip', () => { it('check length for the tooltip text, to be less than 1000', () => { cy.contains('Continue Submission').click() - // eslint-disable-next-line jest/valid-expect-in-promise cy.fixture('submission_form_data').then(data => { SubmissionFormPage.fillInAbstractColab( data.abstractWithMoreThan1000Characters, diff --git a/cypress/e2e/b-colab-prc/103-manuscripts_page_filter_sort_archive_spec.cy.js b/cypress/e2e/b-colab-prc/103-manuscripts_page_filter_sort_archive_spec.cy.js index e0f2d0f9c..289296796 100644 --- a/cypress/e2e/b-colab-prc/103-manuscripts_page_filter_sort_archive_spec.cy.js +++ b/cypress/e2e/b-colab-prc/103-manuscripts_page_filter_sort_archive_spec.cy.js @@ -1,5 +1,5 @@ -/* eslint-disable jest/valid-expect-in-promise */ -/* eslint-disable jest/expect-expect */ +/* eslint-disable promise/always-return */ + import { dashboard } from '../../support/routes1' import { ManuscriptsPage } from '../../page-object/manuscripts-page' import { NewSubmissionPage } from '../../page-object/new-submission-page' @@ -14,7 +14,7 @@ describe('manuscripts page tests - Filter, sort, bulk select, archive', () => { // cy.task('seedForms') // login as admin - // eslint-disable-next-line jest/valid-expect-in-promise + cy.fixture('role_names').then(name => { cy.login(name.role.admin, dashboard) }) diff --git a/cypress/e2e/b-colab-prc/104-control_page_spec.cy.js b/cypress/e2e/b-colab-prc/104-control_page_spec.cy.js index 4fdb3192c..f96d2ae27 100644 --- a/cypress/e2e/b-colab-prc/104-control_page_spec.cy.js +++ b/cypress/e2e/b-colab-prc/104-control_page_spec.cy.js @@ -1,6 +1,4 @@ -/* eslint-disable jest/no-commented-out-tests */ -/* eslint-disable jest/valid-expect-in-promise */ -/* eslint-disable jest/expect-expect */ +/* eslint-disable promise/always-return, promise/no-nesting */ import { dashboard, manuscripts } from '../../support/routes1' import { ManuscriptsPage } from '../../page-object/manuscripts-page' @@ -10,7 +8,6 @@ import { DashboardPage } from '../../page-object/dashboard-page' import { ControlPage } from '../../page-object/control-page' import { ReviewPage } from '../../page-object/review-page' -// eslint-disable-next-line jest/no-disabled-tests describe('control page tests', () => { // UPDATE 0.05.2025 // SHARED checkbox can be clicked only on completed reviews @@ -220,8 +217,8 @@ describe('control page tests', () => { ControlPage.getHideReviewerNameCheckbox('should', 'be.checked') cy.fixture('role_names').then(name => { cy.login(name.role.reviewers[1], dashboard) - cy.get('[name="submission.$title"]:last').click() - cy.get('[class*=TabsContainer]').contains('Review').click() + cy.get('[data-testid="submission.$title"]:last').click() + cy.get('[data-testid=tab-container]').contains('Review').click() ControlPage.getReviewerName().should( 'not.contain', name.role.reviewers[1], @@ -236,8 +233,8 @@ describe('control page tests', () => { ControlPage.getHideReviewerNameCheckbox('should', 'not.be.checked') cy.fixture('role_names').then(name => { cy.login(name.role.reviewers[1], dashboard) - cy.get('[name="submission.$title"]:last').click() - cy.get('[class*=TabsContainer]').contains('Review').click() + cy.get('[data-testid="submission.$title"]:last').click() + cy.get('[data-testid=tab-container]').contains('Review').click() ControlPage.getReviewerName().should('contain', name.role.reviewers[1]) }) }) diff --git a/cypress/e2e/b-colab-prc/105-review_and_decision_page_spec.cy.js b/cypress/e2e/b-colab-prc/105-review_and_decision_page_spec.cy.js index 8ea1a0390..ae3fdff42 100644 --- a/cypress/e2e/b-colab-prc/105-review_and_decision_page_spec.cy.js +++ b/cypress/e2e/b-colab-prc/105-review_and_decision_page_spec.cy.js @@ -1,6 +1,4 @@ -/* eslint-disable jest/valid-expect-in-promise */ -/* eslint-disable jest/expect-expect */ -/* eslint-disable jest/valid-expect */ +/* eslint-disable promise/always-return */ // import { beforeEach } from 'mocha' import { dashboard, manuscripts } from '../../support/routes1' @@ -70,7 +68,7 @@ Cypress.Commands.add('addReviewer', reviewerIndex => { cy.fixture('role_names').then(name => { const reviewer = name.role.reviewers[`${reviewerIndex}`] // login as seniorEditor - // eslint-disable-next-line no-undef + cy.login(name.role.seniorEditor, dashboard) cy.url().should('include', '/dashboard') diff --git a/cypress/e2e/b-colab-prc/106-form_builder_and_submission_page_spec.cy.js b/cypress/e2e/b-colab-prc/106-form_builder_and_submission_page_spec.cy.js index 67a6a0343..e265550c0 100644 --- a/cypress/e2e/b-colab-prc/106-form_builder_and_submission_page_spec.cy.js +++ b/cypress/e2e/b-colab-prc/106-form_builder_and_submission_page_spec.cy.js @@ -1,4 +1,6 @@ -/* eslint-disable jest/expect-expect, cypress/unsafe-to-chain-command */ +/* eslint-disable promise/always-return */ +/* eslint-disable cypress/unsafe-to-chain-command */ + import { FormsPage } from '../../page-object/forms-page' import { Menu } from '../../page-object/page-component/menu' import { dashboard } from '../../support/routes1' @@ -13,7 +15,7 @@ describe('Form builder and Submission pages', () => { it('viewing and adding fields in Submission, Review and Decision forms', () => { // login as admin - // eslint-disable-next-line jest/valid-expect-in-promise + cy.fixture('role_names').then(name => { cy.login(name.role.admin, dashboard) }) @@ -100,7 +102,6 @@ describe('Form builder and Submission pages', () => { it('word count button should be visible & display info', () => { SubmissionFormPage.getWordCountInfo().its('length').should('eq', 5) - // eslint-disable-next-line no-plusplus for (let i = 0; i < 5; i++) { SubmissionFormPage.getWordCountInfo() .eq(i) @@ -129,7 +130,7 @@ describe('Form builder and Submission pages', () => { it('author uploads manuscript after filling in all the required fields', () => { // Change the title so that we can look for it - // eslint-disable-next-line jest/valid-expect-in-promise + cy.fixture('submission_form_data').then(data => { SubmissionFormPage.fillInTitle(data.newTitle) SubmissionFormPage.fillInDoi(data.doi) @@ -147,7 +148,7 @@ describe('Form builder and Submission pages', () => { // Submit the form SubmissionFormPage.clickSubmitYourManuscript() - cy.get('[data-test-id="tab-container"]:nth(0)') + cy.get('[data-testid="tab-container"]:nth(0)') .should('contain', 'My Submissions') .should('exist') // Contains new title diff --git a/cypress/e2e/b-colab-prc/107-new_manuscript_version_spec.cy.js b/cypress/e2e/b-colab-prc/107-new_manuscript_version_spec.cy.js index 9bff6de67..0ffa39c5b 100644 --- a/cypress/e2e/b-colab-prc/107-new_manuscript_version_spec.cy.js +++ b/cypress/e2e/b-colab-prc/107-new_manuscript_version_spec.cy.js @@ -1,4 +1,5 @@ -/* eslint-disable jest/expect-expect */ +/* eslint-disable promise/always-return, promise/no-nesting */ + import { DashboardPage } from '../../page-object/dashboard-page' import { ControlPage } from '../../page-object/control-page' import { SubmissionFormPage } from '../../page-object/submission-form-page' @@ -18,7 +19,6 @@ describe('checking manuscript version', () => { }) it('editor checks for new manuscript version', () => { - // eslint-disable-next-line jest/valid-expect-in-promise cy.fixture('role_names').then(name => { /* Editor Submits a decision */ cy.login(name.role.seniorEditor, dashboard) @@ -36,7 +36,7 @@ describe('checking manuscript version', () => { cy.contains('test-pdf.pdf').should('exist') cy.contains('Decision Status').scrollIntoView() // ReviewPage.clickRevise() - cy.get('[class*=FormTemplate__SafeRadioGroup]').eq(1).click() + cy.get('[data-testid=safe-radio-group] input[value=revise]').click() /* Submit the decision */ ControlPage.clickSubmitDecisionButton() /* Check appears in front of button */ @@ -53,7 +53,7 @@ describe('checking manuscript version', () => { /* Create new manuscript version */ DashboardPage.clickCreateNewVersionButton() cy.contains('Edit submission info').should('exist') - // eslint-disable-next-line jest/valid-expect-in-promise + cy.fixture('submission_form_data').then(data => { SubmissionFormPage.fillInAbstractColab(data.abstract) SubmissionFormPage.getWaxInputBox(0).fillInput(data.abstract) diff --git a/cypress/e2e/c-elife-single_form/200-login_page_spec.cy.js b/cypress/e2e/c-elife-single_form/200-login_page_spec.cy.js index abc7be549..2467e4fc5 100644 --- a/cypress/e2e/c-elife-single_form/200-login_page_spec.cy.js +++ b/cypress/e2e/c-elife-single_form/200-login_page_spec.cy.js @@ -1,4 +1,5 @@ -/* eslint-disable jest/expect-expect */ +/* eslint-disable promise/always-return */ + import { Menu } from '../../page-object/page-component/menu' import { LoginPage } from '../../page-object/login-page' import { manuscripts, login } from '../../support/routes2' @@ -6,14 +7,13 @@ import { ManuscriptsPage } from '../../page-object/manuscripts-page' describe('Login page tests', () => { before(() => { - cy.fixture('branding_settings').then(settings => { + cy.fixture('branding_settings').then(() => { const restoreUrl = Cypress.config('restoreUrl') cy.request('POST', `${restoreUrl}/commons.elife_bootstrap`) }) }) it('page should display eLife branding settings', () => { - // eslint-disable-next-line jest/valid-expect-in-promise cy.fixture('branding_settings').then(settings => { cy.visit(login) @@ -48,7 +48,6 @@ describe('Login page tests', () => { beforeEach(() => { // login as admin - // eslint-disable-next-line jest/valid-expect-in-promise cy.fixture('role_names').then(name => { cy.login(name.role.admin, manuscripts) }) diff --git a/cypress/e2e/c-elife-single_form/201-form_builer_page_spec.cy.js b/cypress/e2e/c-elife-single_form/201-form_builer_page_spec.cy.js index a8268d752..bdb29bf5e 100644 --- a/cypress/e2e/c-elife-single_form/201-form_builer_page_spec.cy.js +++ b/cypress/e2e/c-elife-single_form/201-form_builer_page_spec.cy.js @@ -1,4 +1,6 @@ -/* eslint-disable jest/expect-expect, cypress/unsafe-to-chain-command */ +/* eslint-disable promise/always-return */ +/* eslint-disable cypress/unsafe-to-chain-command */ + import { FormsPage } from '../../page-object/forms-page' // import { Menu } from '../../page-object/page-component/menu' import { submissionForm } from '../../support/routes2' @@ -11,7 +13,6 @@ describe('Form builder', () => { beforeEach(() => { // login as admin - // eslint-disable-next-line jest/valid-expect-in-promise cy.fixture('role_names').then(name => { cy.login(name.role.admin, submissionForm) }) @@ -39,7 +40,6 @@ describe('Form builder', () => { data.preprint1.summaryDate, ] - // eslint-disable-next-line no-plusplus for (let i = 0; i < 16; i++) { FormsPage.getFormBuilderElementName(i).should( 'contain', @@ -73,7 +73,6 @@ describe('Form builder', () => { 'Text', ] - // eslint-disable-next-line no-plusplus for (let i = 0; i < 16; i++) { FormsPage.clickFormOption(i) cy.getByDataTestId('fieldType').should('contain', typeField[i]) diff --git a/cypress/e2e/c-elife-single_form/202-modify_submission_form_spec.cy.js b/cypress/e2e/c-elife-single_form/202-modify_submission_form_spec.cy.js index 7b93e0df5..44cd4edb4 100644 --- a/cypress/e2e/c-elife-single_form/202-modify_submission_form_spec.cy.js +++ b/cypress/e2e/c-elife-single_form/202-modify_submission_form_spec.cy.js @@ -1,4 +1,5 @@ -/* eslint-disable jest/expect-expect */ +/* eslint-disable promise/always-return */ + import { Menu } from '../../page-object/page-component/menu' import { ManuscriptsPage } from '../../page-object/manuscripts-page' import { NewSubmissionPage } from '../../page-object/new-submission-page' @@ -32,7 +33,6 @@ describe('validating required field and doi values in submission form', () => { context('check the Submission form based on form builder', () => { it('check if the form contain all the fields', () => { - // eslint-disable-next-line jest/valid-expect-in-promise cy.fixture('form_option').then(data => { const formElements = [ data.preprint1.articleId, @@ -65,9 +65,8 @@ describe('validating required field and doi values in submission form', () => { // check if it is displayed the required message it('check required message', () => { SubmissionFormPage.clickElifeSubmitResearch() - // eslint-disable-next-line jest/valid-expect-in-promise + cy.fixture('form_option').then(data => { - // eslint-disable-next-line no-plusplus for (let i = 0; i < 4; i++) { SubmissionFormPage.getFormOptionList(i) .get('[class*="MessageWrapper"]') @@ -79,7 +78,6 @@ describe('validating required field and doi values in submission form', () => { context('DOI validations', () => { it('check doi link is available in submission form', () => { - // eslint-disable-next-line jest/valid-expect-in-promise cy.fixture('submission_form_data').then(data => { SubmissionFormPage.fillInArticleld(data.articleId) SubmissionFormPage.fillInDoi(data.doi) diff --git a/cypress/e2e/c-elife-single_form/203-manuscripts_page_spec.cy.js b/cypress/e2e/c-elife-single_form/203-manuscripts_page_spec.cy.js index e3ecc59af..22dcb4ebf 100644 --- a/cypress/e2e/c-elife-single_form/203-manuscripts_page_spec.cy.js +++ b/cypress/e2e/c-elife-single_form/203-manuscripts_page_spec.cy.js @@ -1,4 +1,4 @@ -/* eslint-disable jest/expect-expect */ +/* eslint-disable promise/always-return, promise/no-nesting */ import { ManuscriptsPage } from '../../page-object/manuscripts-page' import { NewSubmissionPage } from '../../page-object/new-submission-page' @@ -64,7 +64,6 @@ describe('Manuscripts page tests', () => { ManuscriptsPage.clickEvaluationAndVerifyUrl() SubmissionFormPage.getWordCountInfo().its('length').should('eq', 4) - // eslint-disable-next-line no-plusplus for (let i = 0; i < 4; i++) { SubmissionFormPage.getWordCountInfo() .eq(i) diff --git a/cypress/e2e/c-elife-single_form/204-publish_submission_form_field.cy.js b/cypress/e2e/c-elife-single_form/204-publish_submission_form_field.cy.js index 5580dbec2..d46ed921f 100644 --- a/cypress/e2e/c-elife-single_form/204-publish_submission_form_field.cy.js +++ b/cypress/e2e/c-elife-single_form/204-publish_submission_form_field.cy.js @@ -1,4 +1,5 @@ -/* eslint-disable jest/expect-expect */ +/* eslint-disable promise/always-return */ + import { FormsPage } from '../../page-object/forms-page' import { Menu } from '../../page-object/page-component/menu' import { ManuscriptsPage } from '../../page-object/manuscripts-page' @@ -14,7 +15,7 @@ describe('Update the submission form field', () => { cy.request('POST', `${restoreUrl}/commons.elife_bootstrap`) // login as admin - // eslint-disable-next-line jest/valid-expect-in-promise + cy.fixture('role_names').then(name => { cy.login(name.role.admin, dashboard) }) @@ -36,9 +37,9 @@ describe('Update the submission form field', () => { Menu.clickManuscripts() ManuscriptsPage.clickSubmit() // Upload manuscript - // eslint-disable-next-line jest/valid-expect-in-promise + cy.get('button').contains('Submit a URL instead').click() - // eslint-disable-next-line jest/valid-expect-in-promise + cy.fixture('submission_form_data').then(data => { SubmissionFormPage.fillInArticleld(data.articleId) SubmissionFormPage.fillInDoi(data.doi) diff --git a/cypress/e2e/ncrc/03-form_builder_page_spec.cy.js b/cypress/e2e/ncrc/03-form_builder_page_spec.cy.js index 18398be01..f836fdbb6 100644 --- a/cypress/e2e/ncrc/03-form_builder_page_spec.cy.js +++ b/cypress/e2e/ncrc/03-form_builder_page_spec.cy.js @@ -1,5 +1,3 @@ -/* eslint-disable jest/no-commented-out-tests */ - // /* eslint-disable jest/expect-expect,no-plusplus,jest/valid-expect-in-promise */ // // import { manuscripts, submissionForm } from '../../support/routes3' // import { FormsPage } from '../../page-object/forms-page' diff --git a/cypress/e2e/ncrc/04-users_page_spec.cy.js b/cypress/e2e/ncrc/04-users_page_spec.cy.js index 4e3090168..946805fde 100644 --- a/cypress/e2e/ncrc/04-users_page_spec.cy.js +++ b/cypress/e2e/ncrc/04-users_page_spec.cy.js @@ -1,4 +1,5 @@ -/* eslint-disable jest/valid-expect-in-promise,jest/expect-expect */ +/* eslint-disable promise/always-return, promise/catch-or-return */ + /// import { users, @@ -31,10 +32,8 @@ describe.skip('users page tests', () => { UsersPage.getRemoveGroupManagerRoleButton() .its('length') .then(length => { - // eslint-disable-next-line jest/valid-expect expect(length).to.be.eq(7) - // eslint-disable-next-line no-plusplus for (let i = 0; i < 7; i++) { UsersPage.getRemoveGroupManagerRoleButton() .eq(i) diff --git a/cypress/e2e/ncrc/05-manuscripts_page_spec.cy.js b/cypress/e2e/ncrc/05-manuscripts_page_spec.cy.js index 73744377c..9efe9cabb 100644 --- a/cypress/e2e/ncrc/05-manuscripts_page_spec.cy.js +++ b/cypress/e2e/ncrc/05-manuscripts_page_spec.cy.js @@ -1,5 +1,6 @@ -/* eslint-disable jest/valid-expect-in-promise,cypress/no-unnecessary-waiting */ -/* eslint-disable jest/expect-expect */ +/* eslint-disable promise/always-return, promise/catch-or-return */ +/* eslint-disable cypress/no-unnecessary-waiting */ + import { manuscripts } from '../../support/routes' import { ManuscriptsPage } from '../../page-object/manuscripts-page' import { NewSubmissionPage } from '../../page-object/new-submission-page' @@ -95,7 +96,6 @@ describe.skip('manuscripts page tests', () => { it('word count button should be visible & display info', () => { SubmissionFormPage.getWordCountInfo().its('length').should('eq', 7) - // eslint-disable-next-line no-plusplus for (let i = 0; i < 7; i++) { SubmissionFormPage.getWordCountInfo() .eq(i) @@ -199,7 +199,7 @@ describe.skip('manuscripts page tests', () => { SubmissionFormPage.fillInReviewer(data.creator) SubmissionFormPage.checkEditDateIsUpdated() SubmissionFormPage.fillInReviewCreator(data.creator) - // eslint-disable-next-line + SubmissionFormPage.waitThreeSec() SubmissionFormPage.clickSubmitResearchAndWaitPageLoad() }) @@ -246,7 +246,7 @@ describe.skip('manuscripts page tests', () => { ManuscriptsPage.clickEvaluation() SubmissionFormPage.fillInValueAdded('Evaluated') SubmissionFormPage.clickTopicsCheckboxContainsText('Vaccine') - // eslint-disable-next-line + SubmissionFormPage.waitThreeSec() SubmissionFormPage.clickSubmitResearchAndWaitPageLoad() ManuscriptsPage.clickEvaluation() @@ -392,7 +392,6 @@ describe.skip('manuscripts page tests', () => { }) it('filter article after label and url contain that label', () => { - // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(3000) ManuscriptsPage.clickArticleLabel(-1) ManuscriptsPage.getTableRowsCount().should('eq', 2) @@ -455,11 +454,11 @@ describe.skip('manuscripts page tests', () => { SubmissionFormPage.fillInJournal(data.journal) SubmissionFormPage.fillInReviewer(data.creator) SubmissionFormPage.fillInReviewCreator(data.creator) - // eslint-disable-next-line + SubmissionFormPage.waitThreeSec() SubmissionFormPage.clickSubmitResearchAndWaitPageLoad() }) - // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(3000) ManuscriptsPage.clickStatus(-1) ManuscriptsPage.getTableRowsCount().should('eq', 3) @@ -494,11 +493,11 @@ describe.skip('manuscripts page tests', () => { SubmissionFormPage.fillInJournal(data.journal) SubmissionFormPage.fillInReviewer(data.creator) SubmissionFormPage.fillInReviewCreator(data.creator) - // eslint-disable-next-line + SubmissionFormPage.waitThreeSec() SubmissionFormPage.clickSubmitResearchAndWaitPageLoad() }) - // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(3000) ManuscriptsPage.getTableRowsCount().should('eq', 4) ManuscriptsPage.clickTableHead(5) @@ -530,11 +529,11 @@ describe.skip('manuscripts page tests', () => { SubmissionFormPage.fillInJournal(data.journal) SubmissionFormPage.fillInReviewer(data.creator) SubmissionFormPage.fillInReviewCreator(data.creator) - // eslint-disable-next-line + SubmissionFormPage.waitThreeSec() SubmissionFormPage.clickSubmitResearchAndWaitPageLoad() }) - // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(3000) ManuscriptsPage.clickStatus(-1) ManuscriptsPage.getTableRowsCount().should('eq', 3) @@ -573,11 +572,11 @@ describe.skip('manuscripts page tests', () => { SubmissionFormPage.fillInJournal(data.journal) SubmissionFormPage.fillInReviewer(data.creator) SubmissionFormPage.fillInReviewCreator(data.creator) - // eslint-disable-next-line + SubmissionFormPage.waitThreeSec() SubmissionFormPage.clickSubmitResearchAndWaitPageLoad() }) - // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(3000) ManuscriptsPage.getTableRowsCount().should('eq', 4) ManuscriptsPage.clickTableHead(5) diff --git a/cypress/e2e/ncrc/06-manuscripts_page_assign_editors_spec.cy.js b/cypress/e2e/ncrc/06-manuscripts_page_assign_editors_spec.cy.js index dc85e2dba..d54d07e85 100644 --- a/cypress/e2e/ncrc/06-manuscripts_page_assign_editors_spec.cy.js +++ b/cypress/e2e/ncrc/06-manuscripts_page_assign_editors_spec.cy.js @@ -1,4 +1,4 @@ -/* eslint-disable jest/expect-expect */ +/* eslint-disable promise/always-return */ import { dashboard, manuscripts } from '../../support/routes' import { ManuscriptsPage } from '../../page-object/manuscripts-page' @@ -16,7 +16,7 @@ describe.skip('manuscripts page assign editors tests', () => { cy.request('POST', seedFormsUrl) // login as admin - // eslint-disable-next-line jest/valid-expect-in-promise + cy.fixture('role_names').then(name => { cy.login(name.role.admin, manuscripts) }) @@ -36,7 +36,7 @@ describe.skip('manuscripts page assign editors tests', () => { 'contain', 'Assign Senior Editor…', ) - // eslint-disable-next-line jest/valid-expect-in-promise + cy.fixture('role_names').then(name => { SubmissionFormPage.getAssignEditor(0).click() SubmissionFormPage.selectDropdownOption(0) @@ -60,7 +60,6 @@ describe.skip('manuscripts page assign editors tests', () => { }) it('check all three editor names appears in the table', () => { - // eslint-disable-next-line jest/valid-expect-in-promise cy.fixture('role_names').then(name => { ManuscriptsPage.clickEvaluationAndVerifyUrl() cy.awaitDisappearSpinner() @@ -87,7 +86,6 @@ describe.skip('manuscripts page assign editors tests', () => { }) }) it('assign senior editor only', () => { - // eslint-disable-next-line jest/valid-expect-in-promise cy.fixture('role_names').then(name => { ManuscriptsPage.clickEvaluationAndVerifyUrl() cy.awaitDisappearSpinner() @@ -111,7 +109,6 @@ describe.skip('manuscripts page assign editors tests', () => { }) }) it('assign first handling editor only', () => { - // eslint-disable-next-line jest/valid-expect-in-promise cy.fixture('role_names').then(name => { ManuscriptsPage.clickEvaluationAndVerifyUrl() cy.awaitDisappearSpinner() @@ -135,7 +132,6 @@ describe.skip('manuscripts page assign editors tests', () => { }) }) it('assign second handling editor only', () => { - // eslint-disable-next-line jest/valid-expect-in-promise cy.fixture('role_names').then(name => { ManuscriptsPage.clickEvaluationAndVerifyUrl() cy.awaitDisappearSpinner() @@ -162,12 +158,10 @@ describe.skip('manuscripts page assign editors tests', () => { }) }) // to be implemented in #489 - // eslint-disable-next-line jest/no-commented-out-tests it('editor should see article title in dashboard', () => { ManuscriptsPage.clickEvaluation() cy.url().should('contain', 'evaluation') cy.awaitDisappearSpinner() - // eslint-disable-next-line jest/valid-expect-in-promise cy.fixture('role_names').then(name => { SubmissionFormPage.getAssignEditor(0).click() SubmissionFormPage.selectDropdownOption(0) diff --git a/cypress/e2e/ncrc/07-manuscripts_page_checkboxes_spec.cy.js b/cypress/e2e/ncrc/07-manuscripts_page_checkboxes_spec.cy.js index 64b12c59e..117f8710c 100644 --- a/cypress/e2e/ncrc/07-manuscripts_page_checkboxes_spec.cy.js +++ b/cypress/e2e/ncrc/07-manuscripts_page_checkboxes_spec.cy.js @@ -1,4 +1,4 @@ -/* eslint-disable jest/expect-expect */ +/* eslint-disable promise/always-return */ import { manuscripts } from '../../support/routes' import { ManuscriptsPage } from '../../page-object/manuscripts-page' @@ -16,7 +16,7 @@ describe.skip('manuscripts page checkboxes tests', () => { cy.request('POST', seedFormsUrl) // login as admin - // eslint-disable-next-line jest/valid-expect-in-promise + cy.fixture('role_names').then(name => { cy.login(name.role.admin, manuscripts) }) @@ -36,7 +36,7 @@ describe.skip('manuscripts page checkboxes tests', () => { }) beforeEach(() => { // login as admin - // eslint-disable-next-line jest/valid-expect-in-promise + cy.fixture('role_names').then(name => { cy.login(name.role.admin, manuscripts) }) @@ -86,14 +86,12 @@ describe.skip('manuscripts page checkboxes tests', () => { cy.request('POST', seedFormsUrl) // login as admin - // eslint-disable-next-line jest/valid-expect-in-promise cy.fixture('role_names').then(name => { cy.login(name.role.admin, manuscripts) }) cy.awaitDisappearSpinner() ManuscriptsPage.clickSubmit() NewSubmissionPage.clickSubmitUrlAndWaitPageLoad() - // eslint-disable-next-line jest/valid-expect-in-promise cy.fixture('submission_form_data').then(data => { SubmissionFormPage.fillInDoi(data.doi) SubmissionFormPage.fillInTitle(data.articleId) @@ -112,7 +110,7 @@ describe.skip('manuscripts page checkboxes tests', () => { SubmissionFormPage.fillInJournal(data.journal) SubmissionFormPage.fillInReviewer(data.creator) SubmissionFormPage.fillInReviewCreator(data.creator) - // eslint-disable-next-line + SubmissionFormPage.waitThreeSec() SubmissionFormPage.clickSubmitResearchAndWaitPageLoad() }) diff --git a/cypress/e2e/ncrc/08-manuscripts_page_tooltip_spec.cy.js b/cypress/e2e/ncrc/08-manuscripts_page_tooltip_spec.cy.js index 9bf53c99d..c2b06bd20 100644 --- a/cypress/e2e/ncrc/08-manuscripts_page_tooltip_spec.cy.js +++ b/cypress/e2e/ncrc/08-manuscripts_page_tooltip_spec.cy.js @@ -1,4 +1,4 @@ -/* eslint-disable jest/expect-expect */ +/* eslint-disable promise/always-return */ import { manuscripts } from '../../support/routes' import { ManuscriptsPage } from '../../page-object/manuscripts-page' @@ -15,7 +15,7 @@ describe.skip('tooltip tests', () => { cy.request('POST', seedFormsUrl) // login as admin - // eslint-disable-next-line jest/valid-expect-in-promise + cy.fixture('role_names').then(name => { cy.login(name.role.admin, manuscripts) }) @@ -30,7 +30,6 @@ describe.skip('tooltip tests', () => { ManuscriptsPage.getTooltipText().invoke('text').should('eq', '') }) it('check tooltip text', () => { - // eslint-disable-next-line jest/valid-expect-in-promise cy.fixture('submission_form_data').then(data => { SubmissionFormPage.fillInAbstract(data.abstract) Menu.clickManuscriptsAndAssertPageLoad() @@ -43,7 +42,6 @@ describe.skip('tooltip tests', () => { }) it('check length for the tooltip text, to be less than 1000', () => { - // eslint-disable-next-line jest/valid-expect-in-promise cy.fixture('submission_form_data').then(data => { SubmissionFormPage.fillInAbstract(data.abstractWithMoreThan1000Characters) Menu.clickManuscriptsAndAssertPageLoad() diff --git a/cypress/e2e/ncrc/09-manuscripts_page_refresh_button_spec.cy.js b/cypress/e2e/ncrc/09-manuscripts_page_refresh_button_spec.cy.js index e2c8d6a36..fadf88d89 100644 --- a/cypress/e2e/ncrc/09-manuscripts_page_refresh_button_spec.cy.js +++ b/cypress/e2e/ncrc/09-manuscripts_page_refresh_button_spec.cy.js @@ -1,5 +1,5 @@ -/* eslint-disable jest/expect-expect */ -/* eslint-disable jest/valid-expect-in-promise,jest/valid-expect */ +/* eslint-disable promise/always-return, promise/catch-or-return, promise/no-nesting */ + import { manuscripts, manuscriptStatus, @@ -78,7 +78,6 @@ describe.skip('refresh button tests', () => { it('at least one topic should exist per imported article', () => { ManuscriptsPage.getTableRowsCount().then(length => { - // eslint-disable-next-line no-plusplus for (let i = 0; i < length; i++) { ManuscriptsPage.getStatus(i).should('be.eq', 'Unsubmitted') ManuscriptsPage.getArticleTopicByRow(i) diff --git a/cypress/e2e/ncrc/300-login_page_spec.cy.js b/cypress/e2e/ncrc/300-login_page_spec.cy.js index d7340b5af..7e6efcb7f 100644 --- a/cypress/e2e/ncrc/300-login_page_spec.cy.js +++ b/cypress/e2e/ncrc/300-login_page_spec.cy.js @@ -1,4 +1,5 @@ -/* eslint-disable jest/expect-expect */ +/* eslint-disable promise/always-return */ + import { Menu } from '../../page-object/page-component/menu' import { dashboard, manuscripts, login } from '../../support/routes3' import { ManuscriptsPage } from '../../page-object/manuscripts-page' @@ -12,7 +13,6 @@ describe('Login page tests', () => { }) it('checking Kotahi branding in the login page', () => { - // eslint-disable-next-line jest/valid-expect-in-promise cy.fixture('branding_settings').then(settings => { cy.visit(login) @@ -37,7 +37,6 @@ describe('Login page tests', () => { beforeEach(() => { cy.fixture('role_names').then(name => { // login as admin - // eslint-disable-next-line jest/valid-expect-in-promise cy.login(name.role.admin, dashboard) }) cy.awaitDisappearSpinner() @@ -64,7 +63,6 @@ describe('Login page tests', () => { context('other users can only access the Dashboard page', () => { beforeEach(() => { // login as a reviewer - // eslint-disable-next-line jest/valid-expect-in-promise cy.fixture('role_names').then(name => { cy.login(name.role.reviewers[0], dashboard) }) diff --git a/cypress/fixtures/role_names.js b/cypress/fixtures/role_names.js index c325c5e03..83c901dd3 100644 --- a/cypress/fixtures/role_names.js +++ b/cypress/fixtures/role_names.js @@ -13,4 +13,4 @@ const role = { ], } -module.exports = role +export default role diff --git a/cypress/page-object/control-page.js b/cypress/page-object/control-page.js index d1416e9bd..b6cabe6a9 100644 --- a/cypress/page-object/control-page.js +++ b/cypress/page-object/control-page.js @@ -1,4 +1,3 @@ -/* eslint-disable padding-line-between-statements */ /// /** * Page object representing the Control page, @@ -8,85 +7,83 @@ */ // Emai Notifications -const NOTIFY_BUTTON = '[class*=emailNotifications__RowGridStyled] > button' -const EMAIL_NOTIFICATION_LOG_MESSAGE = 'style__InnerMessageContainer' -const EMAIL_NOTIFICATION_SECTION = 'emailNotifications__RowGridStyled' -const NEW_USER_CHECKBOX = '[class*=emailNotifications__RowGridStyled] > label' +const NOTIFY_BUTTON = '[data-testid="email-notification-row"] > button' +const EMAIL_NOTIFICATION_LOG_MESSAGE = 'email-log-message' +const EMAIL_NOTIFICATION_SECTION = 'email-notification-row' +const NEW_USER_CHECKBOX = '[data-testid="email-notification-row"] > label' const NEW_USER_EMAIL_FIELD = '[placeholder="Email"]' const NEW_USER_NAME_FIELD = '[placeholder="Name"]' const EMAIL_NOTIFICATION_DROPDOWNS = - '[class*=emailNotifications__RowGridStyled] > div' + '[data-testid="email-notification-row"] > div' -const ASSIGN_SENIOR_EDITOR_DROPDOWN = 'Assign seniorEditor' -const ASSIGN_HANDLING_EDITOR_DROPDOWN = 'Assign handlingEditor' -const ASSIGN_EDITOR_DROPDOWN = 'Assign editor' +const ASSIGN_SENIOR_EDITOR = 'assign-seniorEditor' +const ASSIGN_HANDLING_EDITOR = 'assign-handlingEditor' +const ASSIGN_MANUSCRIPT_EDITOR = 'assign-editor' // Reviews + Invitations -const INVITE_REVIEWER_DROPDOWN = 'Invite reviewers' -const INVITE_REVIEWER_OPTION_LIST = 'react-select' +const INVITE_REVIEWER_SELECT = 'invite-reviewer-select' const INVITE_REVIEWER_SUBMIT_BUTTON = 'invite-reviewer' -const INVITED_REVIEWERS = '[class*=KanbanCard__Card]' +const INVITED_REVIEWERS = '[data-testid="kanban-card"]' const INVITE_REVIEWER_SUBMIT_MODAL_BUTTON = 'submit-modal' const REVIEWER_MODAL_SHARED_CHECKBOX = 'input[type="checkbox"]:nth(10)' // Decision + Publishing -const PUBLISH_BUTTON = - '.General__SectionAction-sc-1chiust-11 > .Publish__ActionButtonsWrapper-sc-1pyb416-0 > .Button__StyledButton-sc-qdm33h-0' -// '[class*=General__SectionAction-sc-1chiust-11] > .sc-bkzZxe' +const PUBLISH_BUTTON = '[data-testid="publish-button"]' const PUBLISH_INFO_MESSAGE = 'General__SectionActionInfo-sc-1chiust-12' const DECISION_FIELD = '[contenteditable="true"]' // Review -const REVIEW_MESSAGE = '[class*=EditorStyles__ReadOnlySimpleEditorDiv]' +const REVIEW_MESSAGE = '[data-testid="readonly-editor"]' const REVIEW_OPTION_CHECKBOX = - '[class*=ReviewHeading__StyledCheckbox] > [type=checkbox]' -const REVIEWER_NAME = '[class*=Review__Heading]' -const NO_REVIEWS_MESSAGE = '[class*=General__SectionRow]' -const ACCEPTED_TO_PUBLISH_REVIEW_ICON = '[class*=DecisionReview__Name] img' + '[data-testid="review-option-checkbox"] > [type=checkbox]' +const REVIEWER_NAME = '[data-testid="reviewer-name"]' +const NO_REVIEWS_MESSAGE = '[data-testid="section-row"]' +const ACCEPTED_TO_PUBLISH_REVIEW_ICON = + '[data-testid="decision-review-name"] img' // Chat -const MESSAGE_CONTAINER = '.General__Chat-sc-1chiust-18' -const CHAT_TAB = '[class*=General__Chat] [data-test-id=tab-container]' -const EXPAND_CHAT_BUTTON = '[class*=style__ChatButton]' +const MESSAGE_CONTAINER = '[data-testid="chat-panel"]' +const CHAT_TAB = '[data-testid="chat-panel"] [data-test-id=tab-container]' +const EXPAND_CHAT_BUTTON = '[data-testid="expand-chat"]' // Multiple Elements const SUBMIT_BUTTON = 'decision-action-btn' // Also Matches Notify Button -const DROPDOWN_OPTION = '[class*=react-select__option]' -const METADATA_TAB = 'HiddenTabs__TabContainer-sc-11z25w4-2' -const METADATA_CELL = 'VersionSwitcher__Title' -const ERROR_TEXT = 'style__ErrorText-' -const FORM_STATUS = 'style__FormStatus-' -const SHOW_BUTTON = '[class*=DecisionReview__Controls]>[type*=button]' +const DROPDOWN_OPTION = '[data-testid="select-option"]' +const METADATA_TAB = 'tab-container' +const METADATA_CELL = 'version-title' +const ERROR_TEXT = 'form-error-text' +const FORM_STATUS = 'form-status' +const SHOW_BUTTON = '[data-testid="decision-review-controls"]>[type*=button]' // Decision Form const DECISION_TEXT_INPUT = 'comment' // ':nth-child(1) > :nth-child(2) > :nth-child(1) > :nth-child(1) > .EditorStyles__SimpleGrid-k4rcxo-9 > .EditorStyles__SimpleEditorDiv-k4rcxo-11' -const DECISION_RADIO_BUTTON = '[class*=FormTemplate__SafeRadioGroup]' +const DECISION_RADIO_BUTTON = '[data-testid="safe-radio-group"]' const DECISION_SUBMIT_BUTTON = 'decision-action-btn' const DECISION_FILE_INPUT = 'input[type=file]' const CHECK_SVG = 'check-svg' -// eslint-disable-next-line import/prefer-default-export export const ControlPage = { getInviteReviewerDropdown() { - return cy.getByContainsAriaLabel(INVITE_REVIEWER_DROPDOWN) + return cy.getByDataTestId(INVITE_REVIEWER_SELECT) + }, + openInviteReviewerDropdown() { + this.getInviteReviewerDropdown() + .closest('.react-select__control') + .click({ force: true }) + this.getInviteReviewerDropdown().find('input').should('exist') }, clickInviteReviewerDropdown() { - this.getInviteReviewerDropdown().click({ force: true }) + this.openInviteReviewerDropdown() }, inviteReviewer(name) { - this.clickInviteReviewerDropdown() + this.openInviteReviewerDropdown() this.selectReviewerNamed(name) this.clickInviteReviewerSubmit() this.clickReviewerSubmitModalButton() - // eslint-disable-next-line cypress/no-unnecessary-waiting - // cy.wait(1000) - }, - getInviteReviewerOptionList() { - return cy.getByContainsId(INVITE_REVIEWER_OPTION_LIST) }, getInviteReviewerSubmitButton() { return cy.getByDataTestId(INVITE_REVIEWER_SUBMIT_BUTTON) @@ -94,8 +91,14 @@ export const ControlPage = { clickInviteReviewerSubmit() { this.getInviteReviewerSubmitButton().click() }, - selectReviewerNamed(uuid) { - this.getInviteReviewerOptionList().contains(uuid).click() + selectReviewerNamed(name) { + this.getInviteReviewerDropdown() + .find('input') + .type(`{selectall}${name}`, { delay: 50, force: true }) + cy.get('.react-select__menu', { timeout: 10000 }) + .should('be.visible') + .contains('.react-select__option', name) + .click({ force: true }) }, getInvitedReviewersList() { return cy.get(INVITED_REVIEWERS) @@ -107,7 +110,7 @@ export const ControlPage = { return cy.get(DECISION_FIELD).eq(nth) }, fillInDecision(decision) { - this.getDecisionField(0).fillInput(decision) + this.getDecisionTextInput().click().type(decision, { force: true }) }, getPublishButton() { return cy.get(PUBLISH_BUTTON) @@ -119,52 +122,80 @@ export const ControlPage = { return cy.getByContainsClass(PUBLISH_INFO_MESSAGE).invoke('text') }, getAssignSeniorEditorDropdown() { - return cy.getByContainsAriaLabel(ASSIGN_SENIOR_EDITOR_DROPDOWN) + return cy.getByDataTestId(ASSIGN_SENIOR_EDITOR) }, clickAssignSeniorEditorDropdown() { this.getAssignSeniorEditorDropdown().click({ force: true }) }, getAssignHandlingEditorDropdown() { - return cy.getByContainsAriaLabel(ASSIGN_HANDLING_EDITOR_DROPDOWN) + return cy.getByDataTestId(ASSIGN_HANDLING_EDITOR) }, clickAssignHandlingEditorDropdown() { this.getAssignHandlingEditorDropdown().click({ force: true }) }, getAssignEditorDropdown() { - return cy.getByContainsAriaLabel(ASSIGN_EDITOR_DROPDOWN) + return cy.getByDataTestId(ASSIGN_MANUSCRIPT_EDITOR) }, clickAssignEditorDropdown() { this.getAssignEditorDropdown().click({ force: true }) }, selectDropdownOptionByName(name) { - cy.get(DROPDOWN_OPTION).contains(name).click({ force: true }) + cy.get(DROPDOWN_OPTION, { timeout: 10000 }) + .should('be.visible') + .contains(name) + .click({ force: true }) + }, + waitForAssignEditorValue(testId, name) { + cy.get(`[data-testid="${testId}"]`, { timeout: 20000 }).should( + 'contain', + name, + ) + cy.awaitDisappearSpinner() + }, + assignSeniorEditor(name) { + this.clickAssignSeniorEditorDropdown() + this.selectDropdownOptionByName(name) + this.waitForAssignEditorValue(ASSIGN_SENIOR_EDITOR, name) + }, + assignHandlingEditor(name) { + this.clickAssignHandlingEditorDropdown() + this.selectDropdownOptionByName(name) + this.waitForAssignEditorValue(ASSIGN_HANDLING_EDITOR, name) + }, + assignManuscriptEditor(name) { + this.clickAssignEditorDropdown() + this.selectDropdownOptionByName(name) + this.waitForAssignEditorValue(ASSIGN_MANUSCRIPT_EDITOR, name) }, getMetadataCell() { - return cy.getByContainsClass(METADATA_CELL) + return cy.get(`[data-testid="${METADATA_CELL}"]`) }, getErrorText() { - return cy.getByContainsClass(ERROR_TEXT) + return cy.get(`[data-testid="${ERROR_TEXT}"]`) }, + getDecisionRadioGroup() { + return cy.get(DECISION_RADIO_BUTTON).should('be.visible') + }, getAcceptRadioButton() { - return cy.get(DECISION_RADIO_BUTTON).eq(0) + return this.getDecisionRadioGroup().find('input[value="accept"]') }, clickAccept() { - this.getAcceptRadioButton().click() + this.getAcceptRadioButton().click({ force: true }) }, getReviseRadioButton() { - return cy.get(DECISION_RADIO_BUTTON).eq(1) + return this.getDecisionRadioGroup().find('input[value="revise"]') }, clickRevise() { - this.getReviseRadioButton().click() + this.getReviseRadioButton().click({ force: true }) }, getRejectRadioButton() { - return cy.get(DECISION_RADIO_BUTTON).eq(2) + return this.getDecisionRadioGroup().find('input[value="reject"]') }, clickReject() { - this.getRejectRadioButton().click() + this.getRejectRadioButton().click({ force: true }) }, getSubmitButton() { return cy.getByDataTestId(SUBMIT_BUTTON) @@ -173,7 +204,7 @@ export const ControlPage = { this.getSubmitButton().click() }, getFormStatus() { - return cy.getByContainsClass(FORM_STATUS) + return cy.get(`[data-testid="${FORM_STATUS}"]`) }, getShowButton() { return cy.get(SHOW_BUTTON) @@ -189,7 +220,7 @@ export const ControlPage = { }, clickHideReviewToAuthor() { this.getHideReviewToAuthorCheckbox().click() - // eslint-disable-next-line cypress/no-unnecessary-waiting + // cy.wait(2000) // cy.getHideReviewToAuthorCheckbox('should', 'not.be.checked') }, @@ -198,7 +229,7 @@ export const ControlPage = { }, clickHideReviewerNameToAuthor() { this.getHideReviewerNameCheckbox().click() - // eslint-disable-next-line cypress/no-unnecessary-waiting + // cy.wait(2000) }, getReviewerSubmitModalButton() { @@ -251,7 +282,7 @@ export const ControlPage = { this.getMetadataCell(13).should('contain', getTodayDate()) }, getEmailNotificationSection() { - return cy.getByContainsClass(EMAIL_NOTIFICATION_SECTION) + return cy.get(`[data-testid="${EMAIL_NOTIFICATION_SECTION}"]`) }, getNewUserCheckbox() { return cy.get(NEW_USER_CHECKBOX) @@ -299,26 +330,29 @@ export const ControlPage = { this.getChatTab().eq(nth).click() }, getLogMessage() { - return cy.getByContainsClass(EMAIL_NOTIFICATION_LOG_MESSAGE) + return cy.get(`[data-testid="${EMAIL_NOTIFICATION_LOG_MESSAGE}"]`) }, getMetadataTab(nth) { - return cy.getByContainsClass(METADATA_TAB).eq(nth) + return cy.getByDataTestId(METADATA_TAB).eq(nth) }, getWorkflowTab() { - return cy.get('[data-test-id=tab-container]').contains('Workflow') + return cy.getByDataTestId('tab-container').contains('Workflow') }, getControlPageTabs() { - return cy.get('[data-test-id=tab-container]') + return cy.getByDataTestId('tab-container') }, clickDecisionTab() { - this.getControlPageTabs().contains('Decision').click() + cy.getByDataTestId('tab-decision').click({ force: true }) }, clickReviewsTab() { - this.getControlPageTabs().contains('Reviews').click() + cy.getByDataTestId('tab-reviews').click({ force: true }) }, // Decision Form getDecisionTextInput() { - return cy.getByDataTestId(DECISION_TEXT_INPUT) + return cy + .getByDataTestId(DECISION_TEXT_INPUT) + .find('[contenteditable]') + .first() }, clickDecisionTextInput() { return this.getDecisionTextInput().click() @@ -338,8 +372,6 @@ export const ControlPage = { return cy.getByDataTestId(CHECK_SVG) }, checkSvgExists() { - this.getCheckSvg().should('exist') - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.wait(2000) + cy.get('[data-testid="check-svg"]', { timeout: 10000 }).should('exist') }, } diff --git a/cypress/page-object/dashboard-page.js b/cypress/page-object/dashboard-page.js index dda66b9a7..eebda837a 100644 --- a/cypress/page-object/dashboard-page.js +++ b/cypress/page-object/dashboard-page.js @@ -9,22 +9,20 @@ const BUTTON = 'button' const HEADER = 'General__Heading' -const SECTION_TITLE = 'General__Title' -const SECTION_PLACEHOLDER = 'style__Placeholder' -const SUBMISSION_TITLE = '[name="submission.$title"] > div' +const SUBMISSION_TITLE = '[data-testid="submission.$title"] > div' const SUBMISSION_BUTTON = '+ New submission' const SUBMISSION_FILE_UPLOAD_INPUT = 'input[type=file]' const SUBMISSION_CREATED = 'Submission created' /* My Submissions */ -const SUBMITTED_MANUSCRIPTS = '[class*=style__ClickableManuscriptsRow]' +const SUBMITTED_MANUSCRIPTS = '[data-testid=clickable-manuscripts-row]' const CREATE_NEW_VERSION_BUTTON = 'create-new-manuscript-version-button' /* Submitted Info */ -const DECISION_FIELDS = ':nth-child(1) > .General__Section-sc-1chiust-0 > div' +const DECISION_FIELDS = ':nth-child(1) > [data-testid=section] > div' // 'To Review section' -const DO_REVIEW_BUTTON = '[name="reviewerLinks"] button' +const DO_REVIEW_BUTTON = '[data-testid="reviewerLinks"] button' const ACCEPT_REVIEW_BUTTON = 'accept-review' const REJECT_REVIEW_BUTTON = 'reject-review' @@ -36,10 +34,8 @@ const COMPLETED_REVIEWS_STATUS = 'completed' const REJECTED_REVIEWS_STATUS = 'rejected' const ACCEPTED_REVIEWS_STATUS = 'accepted' const VERSION_TITLE = 'VersionTitle__Root-sc' -const ARTICLE_LINK = '[name="reviewerLinks"] button' -const DASHBOARD_TAB = '[data-test-id=tab-container]' +const ARTICLE_LINK = '[data-testid="reviewerLinks"] button' -// eslint-disable-next-line import/prefer-default-export export const DashboardPage = { getSubmittedManuscript() { return cy.get(SUBMITTED_MANUSCRIPTS) @@ -69,7 +65,7 @@ export const DashboardPage = { this.getSubmitButton().click() }, getSectionTitleWithText(title) { - return cy.getByContainsClass(SECTION_TITLE).contains(title) + return cy.getByDataTestId('section-title').contains(title) }, getSubmissionTitle() { return cy.get(SUBMISSION_TITLE) @@ -81,7 +77,7 @@ export const DashboardPage = { return this.getSubmitButton().click() }, getSectionPlaceholder(nth) { - return cy.getByContainsClass(SECTION_PLACEHOLDER).eq(nth) + return cy.getByDataTestId('placeholder').eq(nth) }, getCreateNewVersionButton() { return cy.getByDataTestId(CREATE_NEW_VERSION_BUTTON) @@ -157,9 +153,11 @@ export const DashboardPage = { this.getCompletedReviewButton().click({ force: true }) }, getDashboardTab() { - return cy.get(DASHBOARD_TAB) + return cy + .getByDataTestId('tab-container') + .filter(':has(a[href*="/dashboard/"])') }, clickDashboardTab(nth) { - this.getDashboardTab().eq(nth).click() + this.getDashboardTab().eq(nth).find('a').click() }, } diff --git a/cypress/page-object/forms-page.js b/cypress/page-object/forms-page.js index f2d134c3c..6c8a519a0 100644 --- a/cypress/page-object/forms-page.js +++ b/cypress/page-object/forms-page.js @@ -3,11 +3,11 @@ * Page object representing the second option from the left side menu, * which contains a number of options & fields to fill in / dropdowns. */ -const FORM_TITLE_TAB = '[data-test-id="tab-container"] > div' -const FORM_OPTION_LIST = '[class*=FormBuilder__Element] > button:nth-child(1)' +const FORM_TITLE_TAB = '[data-testid="tab-container"] > div' +const FORM_OPTION_LIST = + '[data-testid=formbuilder-element] > button:nth-child(1)' const NAME_FIELD = '[data-testid="name"]' const COMPONENT_TYPE = '[role=listbox]' -const FIELD_VALIDATE = '[data-testid="validate"]' const FIELD_TYPE = '[data-testid="fieldType"]' const DOI_VALIDATION = '[class*=RadioBox__RadioGroup]' const UPDATE_FORM_BUTTON = '[type=submit]' @@ -45,7 +45,7 @@ export const FormsPage = { return cy.get(FIELD_TYPE) }, getFieldValidate() { - return cy.get(FIELD_VALIDATE) + return cy.getByDataTestId('validate') }, getDoiValidation(nth) { return cy.get(DOI_VALIDATION).eq(nth).scrollIntoView() diff --git a/cypress/page-object/manuscripts-page.js b/cypress/page-object/manuscripts-page.js index 8c9bd1b03..1e1469625 100644 --- a/cypress/page-object/manuscripts-page.js +++ b/cypress/page-object/manuscripts-page.js @@ -2,15 +2,12 @@ import { evaluate } from '../support/routes' // import { ControlPage } from './control-page' -/* eslint-disable cypress/unsafe-to-chain-command */ - /** * Page component representing the fourth option in the left side menu, * where users can see the list of submitted manuscripts & select Control, * View or Delete. */ -const MANUSCRIPTS_OPTIONS_LIST = '[class*=Action__ActionLink]' -const MANUSCRIPTS_OPTIONS_E = '[class*=Action__ActionLink]' + const BUTTON = 'button' const ACTIONBUTTON = 'Manuscripts__DropdownContainer' const LIVE_CHAT_BUTTON = '[class*=VideoChat__FloatRightButton]' @@ -49,11 +46,11 @@ const CONFIRMATION_MESSAGE = const IMPORT_CONFIRMATION_POPUP = '[class*=Toastify] > [role=alert]' // const CONTROL = '[href*=decision]' -const DROPDOWN_OPTION = '[class*=react-select__option]' +const DROPDOWN_OPTION = '[data-testid="select-option"]' export const ManuscriptsPage = { getManuscriptsOptionsList() { - return cy.get(MANUSCRIPTS_OPTIONS_LIST) + return cy.getByDataTestId('action-link') }, selectOptionWithText(text) { this.getManuscriptsOptionsList().contains(text).click() @@ -62,10 +59,10 @@ export const ManuscriptsPage = { return this.getManuscriptsOptionsList().contains(text) }, getOptionsElife() { - return cy.get(MANUSCRIPTS_OPTIONS_E) + return cy.getByDataTestId('action-link') }, getOptionsElifeText(text) { - return cy.get(MANUSCRIPTS_OPTIONS_E).contains(text) + return cy.getByDataTestId('action-link').contains(text) }, getSubmitButton() { return cy.get(BUTTON).contains('New submission') @@ -178,7 +175,7 @@ export const ManuscriptsPage = { ) // .scrollIntoView() .click() - cy.get('[class*="react-select__option"]').contains(statusLabel).click() + cy.get('[data-testid="select-option"]').contains(statusLabel).click() }, getArticleLabel() { return cy.get(ARTICLE_LABEL) diff --git a/cypress/page-object/new-submission-page.js b/cypress/page-object/new-submission-page.js index e5639e2a5..e5039de02 100644 --- a/cypress/page-object/new-submission-page.js +++ b/cypress/page-object/new-submission-page.js @@ -10,7 +10,6 @@ const UPLOAD_MANUSCRIPT_BUTTON = 'UploadManuscript__Info' const SUBMIT_URL_BUTTON = 'submitUrl' const SUBMISSION_MESSAGE = 'body' -// eslint-disable-next-line import/prefer-default-export export const NewSubmissionPage = { getUploadManuscriptButton() { return cy.getByContainsClass(UPLOAD_MANUSCRIPT_BUTTON) diff --git a/cypress/page-object/page-component/menu.js b/cypress/page-object/page-component/menu.js index 05804ae35..17daca23c 100644 --- a/cypress/page-object/page-component/menu.js +++ b/cypress/page-object/page-component/menu.js @@ -9,7 +9,6 @@ import { UsersPage } from '../users-page' * These options are available on all other pages. */ const MENU_CONTAINER = 'menu-container' -const USER_BUTTON = 'menuStyleds__UserItem' const BACKGROUND = 'menuStyleds__MainNavWrapper' const MESSAGE_NOT_AUTHORISED = 'AdminPage__Root' @@ -81,7 +80,7 @@ export const Menu = { this.getMyProfileButton().click() }, getLoggedUserButton() { - return cy.getByContainsClass(USER_BUTTON) + return cy.getByDataTestId('user-item') }, clickLoggedUser() { this.getLoggedUserButton().click() diff --git a/cypress/page-object/profile-page.js b/cypress/page-object/profile-page.js index c126cacac..9005456b4 100644 --- a/cypress/page-object/profile-page.js +++ b/cypress/page-object/profile-page.js @@ -12,7 +12,6 @@ const POPUP_NEXT_BUTTON = '[class*=EnterEmail__ButtonContainer] > button' const POPUP_UPDATE_EMAIL_ERROR = 'EnterEmail__UpdateEmailError' const EMAIL_CHANGE_ERROR_MESSAGE = 'ChangeEmail__UpdateEmailError' -// eslint-disable-next-line import/prefer-default-export export const ProfilePage = { getButtonByIndex(nth) { return cy.get(BUTTON).eq(nth) diff --git a/cypress/page-object/publication-page.js b/cypress/page-object/publication-page.js index 577e0ca30..54c7c2bbb 100644 --- a/cypress/page-object/publication-page.js +++ b/cypress/page-object/publication-page.js @@ -4,7 +4,6 @@ */ const PUBLICATION_TITLE = 'General__Title' -// eslint-disable-next-line import/prefer-default-export export const PublicationPage = { getPublicationTitle() { return cy.getByContainsClass(PUBLICATION_TITLE).invoke('text') diff --git a/cypress/page-object/reports-page.js b/cypress/page-object/reports-page.js index bc56d77fb..ae3a6190f 100644 --- a/cypress/page-object/reports-page.js +++ b/cypress/page-object/reports-page.js @@ -1,5 +1,3 @@ -/* eslint-disable padding-line-between-statements */ - const REPORT_OPTIONS = 'report-options' const DATE_INPUT = ' [data-testid="report-options"] input' @@ -20,7 +18,6 @@ export const REVIEWER_COLUMNS = { DURATION: 4, } -// eslint-disable-next-line import/prefer-default-export export const ReportPage = { getReportActions() { return cy.getByDataTestId(REPORT_OPTIONS) diff --git a/cypress/page-object/review-page.js b/cypress/page-object/review-page.js index 9cb2bb8da..d297d0907 100644 --- a/cypress/page-object/review-page.js +++ b/cypress/page-object/review-page.js @@ -27,13 +27,12 @@ const DECISION_RECOMMENDATION = 'style__Legend-sc-1jvxmxn-2 bTVTjY' const DECISION_BUTTON_SELECTED = '[class*=component-decision-viewer__RadioGroup] [checked]' -// eslint-disable-next-line import/prefer-default-export export const ReviewPage = { getReviewMetadataCell(nth) { return cy.getByContainsClass(REVIEW_METADATA_CELL).eq(nth) }, getReviewCommentField() { - return cy.get('[contenteditable="true"]:nth(0)') + return cy.get('[data-testid="comment"] [contenteditable="true"]').first() }, fillInReviewComment(reviewComment) { this.getReviewCommentField() @@ -50,19 +49,19 @@ export const ReviewPage = { return cy.get(ACCEPT_RADIO_BUTTON) }, clickAcceptRadioButton() { - this.getAcceptRadioButton().eq(0).click({ force: true }) + this.getAcceptRadioButton().eq(0).check({ force: true }) }, getReviseRadioButton() { return cy.get(REVISE_RADIO_BUTTON) }, clickReviseRadioButton() { - this.getReviseRadioButton().eq(0).click({ force: true }) + this.getReviseRadioButton().eq(0).check({ force: true }) }, getRejectRadioButton() { return cy.get(REJECT_RADIO_BUTTON) }, clickRejectRadioButton() { - this.getRejectRadioButton().eq(0).click({ force: true }) + this.getRejectRadioButton().eq(0).check({ force: true }) }, getReviseButton() { return cy.get(REVISE_RADIO_BUTTON) diff --git a/cypress/page-object/users-page.js b/cypress/page-object/users-page.js index 9df89fed1..d6acb7b0d 100644 --- a/cypress/page-object/users-page.js +++ b/cypress/page-object/users-page.js @@ -10,7 +10,6 @@ const DELETE_USER_BUTTON = 'button:nth-child(1)' const ADD_GROUP_MANAGER_ROLE_BUTTON = 'button:nth-child(3)' const REMOVE_GROUP_MANAGER_ROLE_BUTTON = 'button:nth-child(3)' -// eslint-disable-next-line import/prefer-default-export export const UsersPage = { getTitle() { return cy.getByContainsClass(TITLE) diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js index cb9f53980..519998d35 100644 --- a/cypress/plugins/index.js +++ b/cypress/plugins/index.js @@ -1,4 +1,4 @@ -module.exports = async (on, config) => { +export default async (on, config) => { on('task', {}) // important: return the changed config diff --git a/cypress/support/commands.js b/cypress/support/commands.js index a23c5e6c6..827647bb3 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -1,6 +1,5 @@ -/* eslint-disable consistent-return */ -/* eslint-disable no-console */ -/* eslint-disable no-undef */ +/* eslint-disable promise/always-return */ + // *********************************************** // This example commands.js shows you how to // create various custom commands and overwrite @@ -99,7 +98,6 @@ Cypress.Commands.add('awaitDisappearSpinner', () => { ) }) -/* eslint-disable-next-line node/handle-callback-err */ Cypress.on('uncaught:exception', (err, runnable, promise) => { if (promise) { return false diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js index d68db96df..185d654ec 100644 --- a/cypress/support/e2e.js +++ b/cypress/support/e2e.js @@ -15,6 +15,3 @@ // Import commands.js using ES2015 syntax: import './commands' - -// Alternatively you can use CommonJS syntax: -// require('./commands') diff --git a/docker-compose.production.yml b/docker-compose.production.yml index 171a08947..6adf0c26c 100644 --- a/docker-compose.production.yml +++ b/docker-compose.production.yml @@ -19,7 +19,7 @@ services: - db - fileHosting ports: - - ${SERVER_PORT:-3000}:${SERVER_PORT:-3000} + - ${SERVER_PORT:-3000}:3000 - ${WS_YJS_SERVER_PORT:-5010}:${WS_YJS_SERVER_PORT:-5010} environment: - NODE_ENV=production diff --git a/docker-compose.yml b/docker-compose.yml index 3a55fba1e..7846aaf6b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,13 +5,14 @@ services: build: context: ./packages/client dockerfile: ./Dockerfile-development - command: ['node_modules/.bin/coko-client-dev-js'] + command: ['node_modules/.bin/coko-client-dev'] ports: - ${CLIENT_PORT:-4000}:${CLIENT_PORT:-4000} environment: - NODE_ENV=development - SERVER_URL=http://localhost:3000 - CLIENT_PORT=${CLIENT_PORT:-4000} + - CLIENT_ENTRY_FILE_PATH=/start.js - CLIENT_STATIC_FOLDER_PATH=../public - CLIENT_FAST_REFRESH=1 - CLIENT_PAGE_TITLE=Kotahi - Open Journals @@ -33,14 +34,17 @@ services: depends_on: - db - fileHosting + - createbucket + - pagedjs + - xsweet + - anystyle command: ['yarn', 'coko-server', 'start-dev'] ports: - ${SERVER_PORT:-3000}:3000 - ${WS_YJS_SERVER_PORT:-5010}:${WS_YJS_SERVER_PORT:-5010} - - ${INSPECTOR_PORT:-9229}:${INSPECTOR_PORT:-9229} + - ${INSPECTOR_PORT:-9229}:9229 environment: - NODE_ENV=development - - INSPECTOR_PORT=${INSPECTOR_PORT:-9229} - POSTGRES_USER=${POSTGRES_USER:-dev} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-password} - POSTGRES_DB=${POSTGRES_DB:-kotahi_dev} @@ -79,6 +83,7 @@ services: - PUBLISHING_WEBHOOK_TOKEN=${PUBLISHING_WEBHOOK_TOKEN:-} - PUBLISHING_WEBHOOK_REF=${PUBLISHING_WEBHOOK_REF:-} - S3_URL=${S3_URL:-http://fileHosting:9000} + - S3_PUBLIC_URL=${S3_PUBLIC_URL:-http://localhost:9000} - S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID:-nonRootUser} - S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY:-nonRootPassword} - S3_BUCKET=${S3_BUCKET:-uploads} @@ -126,11 +131,18 @@ services: volumes: - ./packages/server/api:/home/node/app/api - ./packages/server/config:/home/node/app/config + - ./packages/server/config.ts:/home/node/app/config.ts + - ./packages/server/configSchema.ts:/home/node/app/configSchema.ts - ./packages/server/controllers:/home/node/app/controllers + - ./packages/server/jobQueues.ts:/home/node/app/jobQueues.ts - ./packages/server/models:/home/node/app/models + - ./packages/server/permissions.js:/home/node/app/permissions.js - ./packages/server/scripts:/home/node/app/scripts - ./packages/server/services:/home/node/app/services + - ./packages/server/startup.js:/home/node/app/startup.js - ./packages/server/utils:/home/node/app/utils + - ./packages/server/profiles:/home/node/app/profiles + # - ./packages/server/node_modules/@coko/server:/home/node/app/node_modules/@coko/server healthcheck: test: ['CMD-SHELL', 'curl --fail http://localhost:3000/healthcheck || exit 1'] @@ -158,7 +170,7 @@ services: - seccomp:unconfined db-pagedjs: - image: postgres:16-alpine + image: postgres:18 environment: - POSTGRES_USER=pagedjs_user_dev - POSTGRES_DB=pagedjs_dev @@ -181,7 +193,7 @@ services: - POSTGRES_PASSWORD=xsweet_user_password db-xsweet: - image: postgres:16-alpine + image: postgres:18 environment: - POSTGRES_DB=xsweet_dev - POSTGRES_USER=xsweet_user_dev @@ -247,7 +259,7 @@ services: - POSTGRES_PASSWORD=anystyle_user_password db-anystyle: - image: postgres:16-alpine + image: postgres:18 environment: - POSTGRES_USER=anystyle_user_dev - POSTGRES_DB=anystyle_dev diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 000000000..f3f04c796 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,10 @@ +import { defineEslintConfig, rootEslintConfig } from '@coko/lint' + +const config = defineEslintConfig(rootEslintConfig) + +const extraIgnores = ['./packages/devdocs/'] + +const globalIgnoresObject = config.find(o => o.name?.includes('globalIgnores')) +globalIgnoresObject.ignores = [...globalIgnoresObject.ignores, ...extraIgnores] + +export default config diff --git a/jest.config.js b/jest.config.js index f51fdb100..582d6664b 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,19 +1,19 @@ -module.exports = { - projects: [ - { - displayName: 'client', - testEnvironment: 'jsdom', - // verbose: true, - testURL: 'http://localhost/', - testMatch: ['**/packages/client/**/__tests__/**.test.js'], - setupFilesAfterEnv: [require.resolve('regenerator-runtime/runtime')], - }, - { - displayName: 'server', - testEnvironment: 'node', - // verbose: true, - testMatch: ['**/packages/server/**/__tests__/**.test.js'], - setupFilesAfterEnv: [require.resolve('regenerator-runtime/runtime')], - }, - ], -} +// module.exports = { +// projects: [ +// { +// displayName: 'client', +// testEnvironment: 'jsdom', +// // verbose: true, +// testURL: 'http://localhost/', +// testMatch: ['**/packages/client/**/__tests__/**.test.js'], +// setupFilesAfterEnv: [require.resolve('regenerator-runtime/runtime')], +// }, +// { +// displayName: 'server', +// testEnvironment: 'node', +// // verbose: true, +// testMatch: ['**/packages/server/**/__tests__/**.test.js'], +// setupFilesAfterEnv: [require.resolve('regenerator-runtime/runtime')], +// }, +// ], +// } diff --git a/package.json b/package.json index 9ab36cdca..33b526c68 100644 --- a/package.json +++ b/package.json @@ -8,14 +8,11 @@ "url": "https://github.com/eLifePathways/Kotahi" }, "license": "MIT", - "scripts": { - "cz": "./node_modules/.bin/lint-staged && ./node_modules/.bin/git-cz", - "storybook": "coko-storybook" + "engines": { + "node": ">=24.0.0" }, - "husky": { - "hooks": { - "commit-msg": "commitlint -E HUSKY_GIT_PARAMS && lint-staged" - } + "scripts": { + "commit": "yarn coko-lint commit" }, "config": { "commitizen": { @@ -23,20 +20,12 @@ } }, "resolutions": { - "glob": "7", - "postcss": "8", - "prettier": "2.8.8" + "glob": "7" }, "dependencies": { - "@babel/eslint-parser": "^7.24.1", - "@coko/lint": "^2.1.0", - "@coko/storybook": "^1.2.2", - "babel-plugin-parameter-decorator": "^1.0.16", + "@coko/lint": "^3.0.0-alpha.37", "color": "^4.2.3", - "cypress": "^13.8.0", - "dotenv": "^16.4.1", - "dotenv-cli": "^4.0.0", - "eslint-plugin-prettier": "^3.1.2", + "cypress": "^15.15.0", "jest": "^27.1.0" }, "packageManager": "yarn@4.9.2" diff --git a/packages/client/Dockerfile-development b/packages/client/Dockerfile-development index 9c895ef8e..f4c6b6c10 100644 --- a/packages/client/Dockerfile-development +++ b/packages/client/Dockerfile-development @@ -1,5 +1,6 @@ -FROM cokoapps/base-dev:20 +FROM cokoapps/base-dev:24 +RUN corepack enable WORKDIR /home/node/app COPY .yarnrc.yml . diff --git a/packages/client/Dockerfile-production b/packages/client/Dockerfile-production index 53987dab9..df08c0f2f 100644 --- a/packages/client/Dockerfile-production +++ b/packages/client/Dockerfile-production @@ -1,7 +1,9 @@ ############################################################## # IMAGE FOR BUILDING -FROM cokoapps/base:20 +FROM cokoapps/base:24 + +RUN corepack enable ENV NODE_ENV='production' ENV CLIENT_STATIC_FOLDER_PATH=../public @@ -22,7 +24,7 @@ RUN yarn coko-client-build-js ############################################################## # IMAGE FOR RUNNING -FROM cokoapps/base:20 +FROM cokoapps/base:24 WORKDIR /home/node/app diff --git a/packages/client/app/Pages.js b/packages/client/app/Pages.js deleted file mode 100644 index 2a7be7d95..000000000 --- a/packages/client/app/Pages.js +++ /dev/null @@ -1,209 +0,0 @@ -import React, { useEffect } from 'react' -import { Route, Switch, useLocation, Redirect } from 'react-router-dom' -import styled from 'styled-components' -import { useQuery } from '@apollo/client' - -import { grid } from '@coko/client' - -import { ConfigProvider } from './components/config/src' -import { DynamicThemeProvider } from './components/theme/src' -import { YjsProvider } from './components/provider-yjs/YjsProvider' -import theme, { setBrandColors } from './theme' -import GlobalStyle from './theme/elements/GlobalStyle' -import { Spinner, CommsErrorBanner } from './components/shared' -import DynamicFavicon from './dynamicFavicon' - -import { GET_GROUPS } from './queries' - -import Login from './components/component-login/src' -import ArticleArtifactPage from './components/component-published-artifact/components/ArticleArtifactPage' -import DeclineArticleOwnershipPage from './components/component-dashboard/src/components/DeclineArticleOwnershipPage' -import AcceptArticleOwnershipPage from './components/component-dashboard/src/components/AcceptArticleOwnershipPage' -import InvitationAcceptedPage from './components/component-dashboard/src/components/InvitationAcceptedPage' -import AssetManager from './components/asset-manager/src/AssetManagerPage' -import AdminPage from './components/AdminPage' -import GroupPage from './components/component-frontpage/src/GroupPage' -import { JournalProvider } from './components/xpub-journal/src' -import * as journal from '../config/journal' -import ModalProvider from './components/asset-manager/src/ui/Modal/ModalProvider' -import { XpubProvider } from './components/xpub-with-context/src' -import { reloadTranslationsForGroup } from './i18n' - -const Container = styled.div` - display: grid; - height: 10vh; - place-items: center; -` - -const Content = styled.div` - margin-bottom: 1rem; - max-width: 40em; - padding: ${grid(4)}; - text-align: center; - - h1 { - margin-bottom: ${grid(2)}; - } -` - -const Centered = styled.div` - text-align: center; -` - -const modals = { - assetManagerEditor: AssetManager, -} - -const Pages = () => { - const location = useLocation() - - const { loading, error, data } = useQuery(GET_GROUPS) - - const groups = data?.groups ? data.groups : [] - - const hasMultipleGroups = groups && groups.length > 1 - let onlyGroupName = '' - let currentGroup = null - - if (!hasMultipleGroups) { - onlyGroupName = groups[0]?.name - } - - const name = location.pathname.split('/')[1] - - if (name) { - currentGroup = groups.find(group => group.name === name) - } - - const activeConfig = currentGroup?.configs?.find(config => config?.active) - - useEffect(() => { - reloadTranslationsForGroup( - currentGroup?.name, - JSON.parse(activeConfig?.translationOverrides || '{}'), - ) - }, [currentGroup?.id, activeConfig?.id]) - - if (loading && !data) return - - if (error) - return ( - - - - - - - - ) - - window.localStorage.setItem('groupId', currentGroup?.id) - - // TODO: Remove old config once refactor of config is completed - const oldConfig = JSON.parse(currentGroup?.oldConfig || '{}') - - const config = { - id: activeConfig?.id, - groupId: currentGroup?.id, - groupName: currentGroup?.name, - formData: activeConfig?.formData, - urlFrag: `/${currentGroup?.name}`, - logo: activeConfig?.logo, - icon: activeConfig?.icon, - flaxSiteUrl: activeConfig?.flaxSiteUrl, - ...oldConfig, - ...JSON.parse(activeConfig?.formData || '{}'), - } - - // Overwrites config theme colors - setBrandColors( - config?.groupIdentity?.primaryColor, - config?.groupIdentity?.secondaryColor, - ) - - const { urlFrag } = config - - return ( - - - - - - - - - - {hasMultipleGroups ? ( - - ) : ( - } - /> - )} - - ( - - )} - /> - - ( - - )} - /> - - {/* */} - {/* */} - - - - - - - - {/* AdminPage has nested routes within */} - - - - - - - - - - - ) -} - -export default diff --git a/packages/client/app/Pages.jsx b/packages/client/app/Pages.jsx new file mode 100644 index 000000000..b0a8d72d8 --- /dev/null +++ b/packages/client/app/Pages.jsx @@ -0,0 +1,206 @@ +/* eslint-disable react-hooks/exhaustive-deps */ + +import { useEffect } from 'react' +import { Route, Routes, useLocation, Navigate } from 'react-router-dom' +import styled from 'styled-components' +import { useQuery } from '@apollo/client/react' + +import { grid } from '@coko/client' + +import { ConfigProvider } from './components/config/src' +import { DynamicThemeProvider } from './components/theme/src' +import { YjsProvider } from './components/provider-yjs/YjsProvider' +import theme, { setBrandColors } from './theme' +import GlobalStyle from './theme/elements/GlobalStyle' +import { Spinner, CommsErrorBanner } from './components/shared' +import DynamicFavicon from './dynamicFavicon' + +import { GET_GROUPS } from './queries' + +import Login from './components/component-login/src' +import ArticleArtifactPage from './components/component-published-artifact/components/ArticleArtifactPage' +import DeclineArticleOwnershipPage from './components/component-dashboard/src/components/DeclineArticleOwnershipPage' +import AcceptArticleOwnershipPage from './components/component-dashboard/src/components/AcceptArticleOwnershipPage' +import InvitationAcceptedPage from './components/component-dashboard/src/components/InvitationAcceptedPage' +import AssetManager from './components/asset-manager/src/AssetManagerPage' +import AdminPage from './components/AdminPage' +import GroupPage from './components/component-frontpage/src/GroupPage' +import { JournalProvider } from './components/xpub-journal' +import journal from '../config/journal' +import ModalProvider from './components/asset-manager/src/ui/Modal/ModalProvider' +import { XpubProvider } from './components/xpub-with-context/src' +import { reloadTranslationsForGroup } from './i18n' + +const Container = styled.div` + display: grid; + height: 10vh; + place-items: center; +` + +const Content = styled.div` + margin-bottom: 1rem; + max-width: 40em; + padding: ${grid(4)}; + text-align: center; + + h1 { + margin-bottom: ${grid(2)}; + } +` + +const Centered = styled.div` + text-align: center; +` + +const modals = { + assetManagerEditor: AssetManager, +} + +const Pages = () => { + const location = useLocation() + + const { loading, error, data } = useQuery(GET_GROUPS) + + const groups = data?.groups ? data.groups : [] + + const hasMultipleGroups = groups && groups.length > 1 + let onlyGroupName = '' + let currentGroup = null + + if (!hasMultipleGroups) { + onlyGroupName = groups[0]?.name + } + + const name = location.pathname.split('/')[1] + + if (name) { + currentGroup = groups.find(group => group.name === name) + } + + const activeConfig = currentGroup?.configs?.find(config => config?.active) + + useEffect(() => { + reloadTranslationsForGroup( + currentGroup?.name, + JSON.parse(activeConfig?.translationOverrides || '{}'), + ) + }, [currentGroup?.id, activeConfig?.id]) + + if (loading && !data) return + + if (error) + return ( + + + + + + + + ) + + window.localStorage.setItem('groupId', currentGroup?.id) + + // TODO: Remove old config once refactor of config is completed + const oldConfig = JSON.parse(currentGroup?.oldConfig || '{}') + + const config = { + id: activeConfig?.id, + groupId: currentGroup?.id, + groupName: currentGroup?.name, + formData: activeConfig?.formData, + urlFrag: `/${currentGroup?.name}`, + logo: activeConfig?.logo, + icon: activeConfig?.icon, + flaxSiteUrl: activeConfig?.flaxSiteUrl, + ...oldConfig, + ...JSON.parse(activeConfig?.formData || '{}'), + } + + // Overwrites config theme colors + setBrandColors( + config?.groupIdentity?.primaryColor, + config?.groupIdentity?.secondaryColor, + ) + + const { urlFrag } = config + + return ( + + + + + + + + + + {hasMultipleGroups ? ( + } path="/" /> + ) : ( + + } + path="/" + /> + )} + + + } + path="/login" + /> + + + } + path={urlFrag} + /> + + {/* } path={`${urlFrag}`} /> */} + {/* } path="/" /> */} + + } path={`${urlFrag}/login`} /> + + } + path={`${urlFrag}/versions/:version/artifacts/:artifactId`} + /> + } + path={`${urlFrag}/decline/:invitationId`} + /> + } + path={`${urlFrag}/acceptarticle/:invitationId`} + /> + } + path={`${urlFrag}/invitation/accepted`} + /> + {/* AdminPage has nested routes within */} + } path={`${urlFrag}/*`} /> + + + + + + + + ) +} + +export default diff --git a/packages/client/app/components/.eslintrc b/packages/client/app/components/.eslintrc deleted file mode 100644 index 8df796991..000000000 --- a/packages/client/app/components/.eslintrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "rules": { - "react/prop-types": 0 - } -} diff --git a/packages/client/app/components/AdminPage.js b/packages/client/app/components/AdminPage.js deleted file mode 100644 index 26beec12a..000000000 --- a/packages/client/app/components/AdminPage.js +++ /dev/null @@ -1,637 +0,0 @@ -import { useApolloClient, useMutation, useQuery } from '@apollo/client' -import { throttle } from 'lodash' -import PropTypes from 'prop-types' -import React, { useContext, useEffect, useRef } from 'react' -import Modal from 'react-modal' -import { - matchPath, - Redirect, - Route, - Switch, - useHistory, - useLocation, -} from 'react-router-dom' -import styled from 'styled-components' -import i18next from 'i18next' -import { useTranslation } from 'react-i18next' -import { JournalContext } from './xpub-journal/src' -import { XpubContext } from './xpub-with-context/src' -import { ConfigContext } from './config/src' -import { getLanguages } from '../i18n' - -import FormBuilderPage from './component-formbuilder/src/components/FormBuilderPage' -import ManuscriptPage from './component-manuscript/src/components/ManuscriptPage' -import ManuscriptsPage from './component-manuscripts/src/ManuscriptsPage' -import ProductionPage from './component-production/src/components/ProductionPage' -import ProfilePage, { - UPDATE_LANGUAGE, -} from './component-profile/src/ProfilePage' -import ReportPage from './component-reporting/src/ReportPage' -import DecisionPage from './component-review/src/components/DecisionPage' -import ReviewPage from './component-review/src/components/ReviewPage' -import ReviewPreviewPage from './component-review/src/components/ReviewPreviewPage' -import NewSubmissionPage from './component-submit/src/components/NewSubmissionPage' -import SubmitPage from './component-submit/src/components/SubmitPage' -import TasksTemplatePage from './component-task-manager/src/TasksTemplatePage' -import UsersPage from './component-users-manager/src/UsersPage' -import ConfigManagerPage from './component-config-manager/src/ConfigManagerPage' - -import CMSPagesPage from './component-cms-manager/src/CMSPagesPage' -import CMSLayoutPage from './component-cms-manager/src/CMSLayoutPage' -import CMSArticlePage from './component-cms-manager/src/CMSArticlePage' -import CMSMetadataPage from './component-cms-manager/src/CMSMetadataPage' -import CMSPublishingCollectionPage from './component-cms-manager/src/CMSPublishingCollectionPage' -import CMSFileBrowserPage from './component-cms-manager/src/CMSFileBrowserPage' -import QUERY from './adminPageQueries' - -import Menu from './Menu' -import { Spinner, PageError } from './shared' - -import DashboardEditsPage from './component-dashboard/src/components/DashboardEditsPage' -import DashboardLayout from './component-dashboard/src/components/DashboardLayout' -import DashboardReviewsPage from './component-dashboard/src/components/DashboardReviewsPage' -import DashboardSubmissionsPage from './component-dashboard/src/components/DashboardSubmissionsPage' - -const getParams = ({ routerPath, path }) => { - return matchPath(routerPath, path).params -} - -const Root = styled.div` - display: flex; - height: 100vh; - max-height: 100vh; - ${({ converting }) => - converting && - ` - button, - a { - pointer-events: none; - } - `}; - overflow: hidden; - position: relative; - width: 100vw; - z-index: 0; -` - -// TODO: Redirect if token expires -const PrivateRoute = ({ component: Component, redirectLink, ...rest }) => { - const config = useContext(ConfigContext) - const { urlFrag, instanceName } = config - - if ( - ['journal', 'prc'].includes(instanceName) && - rest.currentUser && - !rest.currentUser.email && - rest.path !== `${urlFrag}/profile` // TODO configure this url via config manager - ) { - return - } - - return ( - { - return localStorage.getItem('token') ? ( - - ) : ( - - ) - }} - /> - ) -} - -PrivateRoute.propTypes = { - component: PropTypes.func.isRequired, - redirectLink: PropTypes.string.isRequired, -} - -const AdminPage = () => { - Modal.setAppElement('#root') - const history = useHistory() - const journal = useContext(JournalContext) - const [conversion] = useContext(XpubContext) - const config = useContext(ConfigContext) - const { urlFrag, instanceName } = config - const location = useLocation() - const { t } = useTranslation() - const client = useApolloClient() - - const { loading, error, data, refetch } = useQuery(QUERY, { - fetchPolicy: 'network-only', - }) - - const previousDataRef = useRef(null) - - const [updateLanguage] = useMutation(UPDATE_LANGUAGE) - useEffect(() => { - if (!data?.currentUser) return - - if (!data.currentUser.preferredLanguage) { - updateLanguage({ - variables: { id: currentUser.id, preferredLanguage: i18next.language }, - }) - } else { - const languageValues = getLanguages().map(l => l.value) - i18next.changeLanguage( - languageValues.includes(currentUser.preferredLanguage) - ? currentUser.preferredLanguage - : 'en', - ) - } - }, [data?.currentUser]) - - // Do this to prevent polling-related flicker - if (loading && !previousDataRef.current) { - return - } - - let notice = '' - - if (error || !data.currentUser) { - if (error?.networkError) { - notice = 'You are offline.' - } else { - if (localStorage.getItem('token') !== null) { - localStorage.removeItem('token') - } - - client.cache.reset() - - localStorage.setItem('intendedPage', location.pathname + location.search) - const redirectlocation = `${urlFrag}/login` - return - } - } - - const currentUser = data?.currentUser - journal.textStyles = data?.builtCss.css - const hasAlert = data?.userHasTaskAlerts - - previousDataRef.current = data - - const { pathname } = history.location - const showLinks = pathname.match(/^\/(submit|manuscript)/g) - let links = [] - const submissionFormBuilderLink = `${urlFrag}/admin/submission-form-builder` - const reviewFormBuilderLink = `${urlFrag}/admin/review-form-builder` - const decisionFormBuilderLink = `${urlFrag}/admin/decision-form-builder` - const configurationLink = `${urlFrag}/admin/configuration` - const homeLink = `${urlFrag}/dashboard` - const dashboardSubmissionsLink = `${urlFrag}/dashboard/submissions` - const dashboardReviewsLink = `${urlFrag}/dashboard/reviews` - const dashboardEditsLink = `${urlFrag}/dashboard/edits` - const profileLink = `${urlFrag}/profile` - const manuscriptsLink = `${urlFrag}/admin/manuscripts` - const reportsLink = `${urlFrag}/admin/reports` - const userAdminLink = `${urlFrag}/admin/users` - const tasksTemplateLink = `${urlFrag}/admin/tasks` - const CMSPagesPageLink = `${urlFrag}/admin/cms/pages` - const CMSLayoutPageLink = `${urlFrag}/admin/cms/layout` - const CMSArticlePageLink = `${urlFrag}/admin/cms/article` - const CMSFileBrowserLink = `${urlFrag}/admin/cms/filebrowser` - const CMSMetadataPageLink = `${urlFrag}/admin/cms/metadata` - const CMSPublishingCollectionPageLink = `${urlFrag}/admin/cms/collections` - const loginLink = `${urlFrag}/login?next=${homeLink}` - const path = `${urlFrag}/versions/:version` - const redirectLink = `${urlFrag}/login?next=${homeLink}` - - if (showLinks) { - const params = getParams(pathname, path) - const baseLink = `${urlFrag}/versions/${params.version}` - const submitLink = `${baseLink}/submit` - const manuscriptLink = `${baseLink}/manuscript` - - links = showLinks - ? [ - { link: submitLink, name: t('leftMenu.Summary Info') }, - { - link: manuscriptLink, - name: t('leftMenu.Manuscript'), - }, - ] - : null - } - - const isUser = currentUser?.groupRoles?.includes('user') - const isGroupManager = currentUser?.groupRoles?.includes('groupManager') - const isGroupAdmin = currentUser?.groupRoles?.includes('groupAdmin') - const isAdmin = currentUser?.globalRoles?.includes('admin') - - if ( - currentUser && - (isUser || isGroupManager || isGroupAdmin || isAdmin) && - ['journal', 'prc', 'preprint2'].includes(instanceName) // TODO: remove instance based logic and refactor it to be enabled and disabled from config manager - ) { - links.push({ - link: homeLink, - name: t('leftMenu.Dashboard'), - icon: 'home', - hasAlert, - }) - } - - if (isGroupManager || isGroupAdmin) { - links.push({ - link: manuscriptsLink, - name: t('leftMenu.Manuscripts'), - icon: 'file-text', - }) - if (config?.report?.showInMenu && isGroupAdmin) - links.push({ - link: reportsLink, - name: t('leftMenu.Reports'), - icon: 'activity', - }) - } - - if (isGroupAdmin || isAdmin) { - links.push({ - menu: 'Settings', - name: t('leftMenu.Settings'), - icon: 'settings', - links: [ - { - menu: 'Forms', - name: t('leftMenu.Forms'), - icon: 'check-square', - links: [ - { - link: submissionFormBuilderLink, - name: t('leftMenu.Submission'), - }, - { - link: reviewFormBuilderLink, - name: t('leftMenu.Review'), - }, - { - link: decisionFormBuilderLink, - name: t('leftMenu.Decision'), - }, - ], - }, - { - link: tasksTemplateLink, - name: t('leftMenu.Tasks'), - icon: 'list', - }, - { - link: userAdminLink, - name: t('leftMenu.Users'), - icon: 'users', - }, - { - link: configurationLink, - name: t('leftMenu.Configuration'), - icon: 'sliders', - }, - { - menu: 'CMS', - name: t('leftMenu.CMS'), - icon: 'layout', - links: [ - { - link: CMSPagesPageLink, - name: t('leftMenu.Pages'), - icon: '', - }, - { - link: CMSLayoutPageLink, - name: t('leftMenu.Layout'), - icon: '', - }, - { - link: CMSArticlePageLink, - name: t('leftMenu.Article'), - icon: '', - }, - { - link: CMSMetadataPageLink, - name: t('leftMenu.Metadata'), - icon: '', - }, - { - link: CMSFileBrowserLink, - name: t('leftMenu.FileBrowser'), - - icon: '', - }, - { - link: CMSPublishingCollectionPageLink, - name: t('leftMenu.Collections'), - icon: '', - }, - ], - }, - ], - }) - } - - const invitationId = window.localStorage.getItem('invitationId') - ? window.localStorage.getItem('invitationId') - : '' - - const inviteAction = window.localStorage.getItem('inviteAction') - ? window.localStorage.getItem('inviteAction') - : '' - - const dashboardRedirectUrl = currentUser?.recentTab - ? `${urlFrag}/dashboard/${currentUser.recentTab}` - : dashboardSubmissionsLink - - const dashboardRedirect = () => - invitationId ? ( - - ) : ( - - ) - - // Throttled refetch query `currentUser` once every 2mins - const throttledRefetch = throttle(refetch, 120000, { trailing: false }) - - // Triggered by captured events onClick and onKeyDown - const handleEvent = e => { - throttledRefetch() - } - - return ( - - - - - {(isUser || isGroupManager || isGroupAdmin || isAdmin) && [ - , - , - , - , - , - , - - - - - {config?.dashboard?.showSections?.includes('submission') && ( - - )} - {config?.dashboard?.showSections?.includes('review') && ( - - )} - {config?.dashboard?.showSections?.includes('editor') && ( - - )} - - - , - , - ]} - {(isGroupAdmin || isAdmin) && [ - // We use array instead of <> because of https://stackoverflow.com/a/68637108/6505513 - , - , - , - , - , - , - - , - - , - - , - - , - - , - - , - - , - ]} - {(isGroupManager || isGroupAdmin) && [ - , - , - ]} - {isGroupAdmin && [ - , - ]} - - {isUser || isGroupManager || isGroupAdmin || isAdmin ? ( - - - - ) : ( - - - - )} - - - ) -} - -export default AdminPage diff --git a/packages/client/app/components/AdminPage.jsx b/packages/client/app/components/AdminPage.jsx new file mode 100644 index 000000000..034e3fbc3 --- /dev/null +++ b/packages/client/app/components/AdminPage.jsx @@ -0,0 +1,748 @@ +/* eslint-disable react/prop-types, react-hooks/exhaustive-deps, react-hooks/immutability, react-hooks/refs */ + +import { useApolloClient, useMutation, useQuery } from '@apollo/client/react' +import PropTypes from 'prop-types' +import { useContext, useEffect, useRef } from 'react' +import Modal from 'react-modal' +import { + matchPath, + Navigate, + Route, + Routes, + useLocation, +} from 'react-router-dom' +import styled from 'styled-components' +import i18next from 'i18next' +import { useTranslation } from 'react-i18next' +import { JournalContext } from './xpub-journal' +import { XpubContext } from './xpub-with-context/src' +import { ConfigContext } from './config/src' +import { getLanguages } from '../i18n' + +import FormBuilderPage from './component-formbuilder/src/components/FormBuilderPage' +import ManuscriptPage from './component-manuscript/src/components/ManuscriptPage' +import ManuscriptsPage from './component-manuscripts/src/ManuscriptsPage' +import ProductionPage from './component-production/src/components/ProductionPage' +import ProfilePage, { + UPDATE_LANGUAGE, +} from './component-profile/src/ProfilePage' +import ReportPage from './component-reporting/src/ReportPage' +import DecisionPage from './component-review/src/components/DecisionPage' +import ReviewPage from './component-review/src/components/ReviewPage' +import ReviewPreviewPage from './component-review/src/components/ReviewPreviewPage' +import NewSubmissionPage from './component-submit/src/components/NewSubmissionPage' +import SubmitPage from './component-submit/src/components/SubmitPage' +import TasksTemplatePage from './component-task-manager/src/TasksTemplatePage' +import UsersPage from './component-users-manager/src/UsersPage' +import ConfigManagerPage from './component-config-manager/src/ConfigManagerPage' + +import CMSPagesPage from './component-cms-manager/src/CMSPagesPage' +import CMSLayoutPage from './component-cms-manager/src/CMSLayoutPage' +import CMSArticlePage from './component-cms-manager/src/CMSArticlePage' +import CMSMetadataPage from './component-cms-manager/src/CMSMetadataPage' +import CMSPublishingCollectionPage from './component-cms-manager/src/CMSPublishingCollectionPage' +import CMSFileBrowserPage from './component-cms-manager/src/CMSFileBrowserPage' +import QUERY from './adminPageQueries' + +import Menu from './Menu' +import { Spinner, PageError } from './shared' + +import DashboardEditsPage from './component-dashboard/src/components/DashboardEditsPage' +import DashboardLayout from './component-dashboard/src/components/DashboardLayout' +import DashboardReviewsPage from './component-dashboard/src/components/DashboardReviewsPage' +import DashboardSubmissionsPage from './component-dashboard/src/components/DashboardSubmissionsPage' + +const getParams = ({ routerPath, path }) => { + return matchPath(routerPath, path).params +} + +const Root = styled.div` + display: flex; + height: 100vh; + max-height: 100vh; + ${props => + props.$converting && + ` + button, + a { + pointer-events: none; + } + `}; + overflow: hidden; + position: relative; + width: 100vw; + z-index: 0; +` + +// TODO: Redirect if token expires +const PrivateRoute = ({ + component: Component, + redirectLink, + path, + currentUser, + ...rest +}) => { + const config = useContext(ConfigContext) + const { urlFrag, instanceName } = config + + if ( + ['journal', 'prc'].includes(instanceName) && + currentUser && + !currentUser.email && + path !== `${urlFrag}/profile` // TODO configure this url via config manager + ) { + return + } + + return localStorage.getItem('token') ? ( + + ) : ( + + ) +} + +PrivateRoute.propTypes = { + component: PropTypes.func.isRequired, + redirectLink: PropTypes.string.isRequired, +} + +const AdminPage = () => { + Modal.setAppElement('#root') + const journal = useContext(JournalContext) + const [conversion] = useContext(XpubContext) + const config = useContext(ConfigContext) + const { urlFrag, instanceName } = config + const location = useLocation() + const { t } = useTranslation() + const client = useApolloClient() + + const { loading, error, data } = useQuery(QUERY, { + fetchPolicy: 'network-only', + pollInterval: 120000, + }) + + const previousDataRef = useRef(null) + + const [updateLanguage] = useMutation(UPDATE_LANGUAGE) + useEffect(() => { + if (!data?.currentUser) return + + if (!data.currentUser.preferredLanguage) { + updateLanguage({ + variables: { id: currentUser.id, preferredLanguage: i18next.language }, + }) + } else { + const languageValues = getLanguages().map(l => l.value) + i18next.changeLanguage( + languageValues.includes(currentUser.preferredLanguage) + ? currentUser.preferredLanguage + : 'en', + ) + } + }, [data?.currentUser]) + + // Do this to prevent polling-related flicker + if (loading && !previousDataRef.current) { + return + } + + let notice = '' + + if (error || !data?.currentUser) { + if (error?.networkError) { + notice = 'You are offline.' + } else { + if (localStorage.getItem('token') !== null) { + localStorage.removeItem('token') + } + + client.cache.reset() + + localStorage.setItem('intendedPage', location.pathname + location.search) + const redirectlocation = `${urlFrag}/login` + return + } + } + + const currentUser = data?.currentUser + journal.textStyles = data?.builtCss?.css + const hasAlert = data?.userHasTaskAlerts + + previousDataRef.current = data + + const showLinks = location.pathname.match(/^\/(submit|manuscript)/g) + let links = [] + const submissionFormBuilderLink = `${urlFrag}/admin/submission-form-builder` + const reviewFormBuilderLink = `${urlFrag}/admin/review-form-builder` + const decisionFormBuilderLink = `${urlFrag}/admin/decision-form-builder` + const configurationLink = `${urlFrag}/admin/configuration` + const homeLink = `${urlFrag}/dashboard` + const dashboardSubmissionsLink = `${urlFrag}/dashboard/submissions` + const dashboardReviewsLink = `${urlFrag}/dashboard/reviews` + const dashboardEditsLink = `${urlFrag}/dashboard/edits` + const profileLink = `${urlFrag}/profile` + const manuscriptsLink = `${urlFrag}/admin/manuscripts` + const reportsLink = `${urlFrag}/admin/reports` + const userAdminLink = `${urlFrag}/admin/users` + const tasksTemplateLink = `${urlFrag}/admin/tasks` + const CMSPagesPageLink = `${urlFrag}/admin/cms/pages` + const CMSLayoutPageLink = `${urlFrag}/admin/cms/layout` + const CMSArticlePageLink = `${urlFrag}/admin/cms/article` + const CMSFileBrowserLink = `${urlFrag}/admin/cms/filebrowser` + const CMSMetadataPageLink = `${urlFrag}/admin/cms/metadata` + const CMSPublishingCollectionPageLink = `${urlFrag}/admin/cms/collections` + const loginLink = `${urlFrag}/login?next=${homeLink}` + const path = `${urlFrag}/versions/:version` + const redirectLink = `${urlFrag}/login?next=${homeLink}` + + if (showLinks) { + const params = getParams(location.pathname, path) + const baseLink = `${urlFrag}/versions/${params.version}` + const submitLink = `${baseLink}/submit` + const manuscriptLink = `${baseLink}/manuscript` + + links = showLinks + ? [ + { link: submitLink, name: t('leftMenu.Summary Info') }, + { + link: manuscriptLink, + name: t('leftMenu.Manuscript'), + }, + ] + : null + } + + const isUser = currentUser?.groupRoles?.includes('user') + const isGroupManager = currentUser?.groupRoles?.includes('groupManager') + const isGroupAdmin = currentUser?.groupRoles?.includes('groupAdmin') + const isAdmin = currentUser?.globalRoles?.includes('admin') + + if ( + currentUser && + (isUser || isGroupManager || isGroupAdmin || isAdmin) && + ['journal', 'prc', 'preprint2'].includes(instanceName) // TODO: remove instance based logic and refactor it to be enabled and disabled from config manager + ) { + links.push({ + link: homeLink, + name: t('leftMenu.Dashboard'), + icon: 'home', + hasAlert, + }) + } + + if (isGroupManager || isGroupAdmin) { + links.push({ + link: manuscriptsLink, + name: t('leftMenu.Manuscripts'), + icon: 'file-text', + }) + if (config?.report?.showInMenu && isGroupAdmin) + links.push({ + link: reportsLink, + name: t('leftMenu.Reports'), + icon: 'activity', + }) + } + + if (isGroupAdmin || isAdmin) { + links.push({ + menu: 'Settings', + name: t('leftMenu.Settings'), + icon: 'settings', + links: [ + { + menu: 'Forms', + name: t('leftMenu.Forms'), + icon: 'check-square', + links: [ + { + link: submissionFormBuilderLink, + name: t('leftMenu.Submission'), + }, + { + link: reviewFormBuilderLink, + name: t('leftMenu.Review'), + }, + { + link: decisionFormBuilderLink, + name: t('leftMenu.Decision'), + }, + ], + }, + { + link: tasksTemplateLink, + name: t('leftMenu.Tasks'), + icon: 'list', + }, + { + link: userAdminLink, + name: t('leftMenu.Users'), + icon: 'users', + }, + { + link: configurationLink, + name: t('leftMenu.Configuration'), + icon: 'sliders', + }, + { + menu: 'CMS', + name: t('leftMenu.CMS'), + icon: 'layout', + links: [ + { + link: CMSPagesPageLink, + name: t('leftMenu.Pages'), + icon: '', + }, + { + link: CMSLayoutPageLink, + name: t('leftMenu.Layout'), + icon: '', + }, + { + link: CMSArticlePageLink, + name: t('leftMenu.Article'), + icon: '', + }, + { + link: CMSMetadataPageLink, + name: t('leftMenu.Metadata'), + icon: '', + }, + { + link: CMSFileBrowserLink, + name: t('leftMenu.FileBrowser'), + + icon: '', + }, + { + link: CMSPublishingCollectionPageLink, + name: t('leftMenu.Collections'), + icon: '', + }, + ], + }, + ], + }) + } + + const invitationId = window.localStorage.getItem('invitationId') + ? window.localStorage.getItem('invitationId') + : '' + + const inviteAction = window.localStorage.getItem('inviteAction') + ? window.localStorage.getItem('inviteAction') + : '' + + const dashboardRedirectUrl = currentUser?.recentTab + ? `${urlFrag}/dashboard/${currentUser.recentTab}` + : dashboardSubmissionsLink + + const dashboardRedirect = () => + invitationId ? ( + + ) : ( + + ) + + return ( + + + + + } + path="profile" + /> + + {(isUser || isGroupManager || isGroupAdmin || isAdmin) && [ + + } + key="profile" + path="profile/:id" + />, + + } + key="new-submission" + path="newSubmission" + />, + + } + key="review" + path="versions/:version/review" + />, + + } + key="review-preview" + path="versions/:version/reviewPreview" + />, + + } + key="decision" + path="versions/:version/decision" + />, + + } + key="submit" + path={`versions/:version/${ + ['preprint1', 'preprint2'].includes(config.instanceName) + ? 'evaluation' + : 'submit' + }`} // TODO: Remove instance based custom submit page and refactor it use config manager flag in future + />, + + + + } + path="" + /> + {config?.dashboard?.showSections?.includes('submission') && ( + + } + key="submission" + path="submissions" + /> + )} + {config?.dashboard?.showSections?.includes('review') && ( + + } + key="review" + path="reviews" + /> + )} + {config?.dashboard?.showSections?.includes('editor') && ( + + } + key="editor" + path="edits" + /> + )} + + + } + key="dashboard" + path={`dashboard/*`} + />, + + } + key="production" + path="versions/:version/production" + />, + ]} + {(isGroupAdmin || isAdmin) && [ + // We use array instead of <> because of https://stackoverflow.com/a/68637108/6505513 + + } + key="form-builder" + path={`/admin/form-builder`} + />, + + } + key="submission-form-builder" + path={`admin/submission-form-builder`} + />, + + } + key="review-form-builder" + path={`admin/review-form-builder`} + />, + + } + key="decision-form-builder" + path={`admin/decision-form-builder`} + />, + + } + key="users" + path={`admin/users`} + />, + + } + key="tasks" + path={`admin/tasks`} + />, + + } + key="CMSPagesPage" + path={`admin/cms/pages/:pageId?`} + />, + + } + key="CMSLayoutPage" + path="admin/cms/layout" + />, + + } + key="CMSArticlePage" + path="admin/cms/article" + />, + + } + key="CMSFileBrowserPage" + path="admin/cms/filebrowser" + />, + + } + key="CMSMetadataPage" + path="admin/cms/metadata" + />, + + } + key="CMSPublishingCollectionPage" + path="admin/cms/collections" + />, + + } + key="configuration" + path={`admin/configuration`} + />, + ]} + {(isGroupManager || isGroupAdmin) && [ + + } + key="manuscript" + path={`versions/:version/manuscript`} + />, + + } + key="manuscripts" + path={`admin/manuscripts`} + />, + ]} + {isGroupAdmin && [ + + } + key="reports" + path="admin/reports" + />, + ]} + + {isUser || isGroupManager || isGroupAdmin || isAdmin ? ( + } path="*" /> + ) : ( + } path="*" /> + )} + + + ) +} + +export default AdminPage diff --git a/packages/client/app/components/Menu.js b/packages/client/app/components/Menu.js deleted file mode 100644 index 7dba4b0f8..000000000 --- a/packages/client/app/components/Menu.js +++ /dev/null @@ -1,125 +0,0 @@ -/* eslint-disable react/jsx-handler-names */ -import React, { useState, useMemo } from 'react' -import PropTypes from 'prop-types' -import { useTranslation } from 'react-i18next' -import { useMutation } from '@apollo/client' -import { - NavItem, - NavLinks, - MainNavWrapper, - SectionNavLayoutSettings, - Root, - ScrollWrapper, - SubMenu, - UserComponent, - StyledPinButton, -} from './component-menu' -import mutations from './component-dashboard/src/graphql/mutations' - -const Menu = ({ - className, - loginLink = '/login', - navLinkComponents, - user, - notice, - profileLink, -}) => { - const { t } = useTranslation() - const [menuIsMinimal, setMenuIsMinimal] = useState(!user.menuPinned) - const [menuPinned, setMenuPinned] = useState(!!user.menuPinned) - const [updateMenuState] = useMutation(mutations.updateMenu) - - const renderLinks = useMemo(() => { - return ( - navLinkComponents && - navLinkComponents.map(navInfo => - navInfo.menu ? ( - - ) : ( - - ), - ) - ) - }, [menuIsMinimal, navLinkComponents]) - - const handlers = { - pinNavbar: e => { - updateMenuState({ variables: { expanded: !menuPinned } }) - setMenuPinned(!menuPinned) - }, - mouseoverNav: () => { - if (menuIsMinimal) { - setMenuIsMinimal(false) - } - }, - mouseleaveNav: () => { - if (!menuPinned) { - setMenuIsMinimal(true) - } - }, - expandCollapse: () => { - setMenuIsMinimal(!menuIsMinimal) - }, - } - - return ( - - - - - {/* TODO: Place this notice (used for offline notification) better */} - {notice} - - - - {renderLinks} - - - - ) -} - -Menu.propTypes = { - className: PropTypes.string.isRequired, - loginLink: PropTypes.string.isRequired, - navLinkComponents: PropTypes.arrayOf(PropTypes.object).isRequired, // eslint-disable-line react/forbid-prop-types - user: PropTypes.oneOfType([PropTypes.object]), - notice: PropTypes.node.isRequired, - profileLink: PropTypes.string.isRequired, -} - -Menu.defaultProps = { - user: undefined, -} - -export default Menu diff --git a/packages/client/app/components/Menu.jsx b/packages/client/app/components/Menu.jsx new file mode 100644 index 000000000..f1e301e38 --- /dev/null +++ b/packages/client/app/components/Menu.jsx @@ -0,0 +1,120 @@ +import { useState, useMemo } from 'react' +import PropTypes from 'prop-types' +import { useTranslation } from 'react-i18next' +import { useMutation } from '@apollo/client/react' +import { + NavItem, + NavLinks, + MainNavWrapper, + SectionNavLayoutSettings, + Root, + ScrollWrapper, + SubMenu, + UserComponent, + StyledPinButton, +} from './component-menu' +import mutations from './component-dashboard/src/graphql/mutations' + +const Menu = ({ + className, + loginLink = '/login', + navLinkComponents, + user, + notice, + profileLink, +}) => { + const { t } = useTranslation() + const [menuIsMinimal, setMenuIsMinimal] = useState(!user.menuPinned) + const [menuPinned, setMenuPinned] = useState(!!user.menuPinned) + const [updateMenuState] = useMutation(mutations.updateMenu) + + const renderLinks = useMemo(() => { + return ( + navLinkComponents && + navLinkComponents.map(navInfo => + navInfo.menu ? ( + + ) : ( + + ), + ) + ) + }, [menuIsMinimal, navLinkComponents]) + + const handlers = { + pinNavbar: () => { + updateMenuState({ variables: { expanded: !menuPinned } }) + setMenuPinned(!menuPinned) + }, + mouseoverNav: () => { + if (menuIsMinimal) { + setMenuIsMinimal(false) + } + }, + mouseleaveNav: () => { + if (!menuPinned) { + setMenuIsMinimal(true) + } + }, + expandCollapse: () => { + setMenuIsMinimal(!menuIsMinimal) + }, + } + + return ( + + + + + {/* TODO: Place this notice (used for offline notification) better */} + {notice} + + + + {renderLinks} + + + + ) +} + +Menu.propTypes = { + className: PropTypes.string.isRequired, + loginLink: PropTypes.string.isRequired, + navLinkComponents: PropTypes.arrayOf(PropTypes.object).isRequired, + user: PropTypes.oneOfType([PropTypes.object]), + notice: PropTypes.node.isRequired, + profileLink: PropTypes.string.isRequired, +} + +export default Menu diff --git a/packages/client/app/components/NextPageButton/index.js b/packages/client/app/components/NextPageButton/index.js deleted file mode 100644 index 141c4e43e..000000000 --- a/packages/client/app/components/NextPageButton/index.js +++ /dev/null @@ -1,78 +0,0 @@ -import VisibilitySensor from 'react-visibility-sensor' -import { Link } from 'react-router-dom' -import React from 'react' -import PropTypes from 'prop-types' -import { Spinner } from '../shared' -import { HasNextPage, NextPageButton } from './style' - -const NextPageButtonWrapper = props => { - const { - isFetchingMore, - fetchMore, - href, - children, - automatic, - topOffset, - bottomOffset, - } = props - - const onChange = isVisible => { - if (isFetchingMore || !isVisible) return undefined - return fetchMore() - } - - return ( - { - evt.preventDefault() - onChange(true) - }} - to={href} - > - - - {isFetchingMore ? ( - - ) : ( - children || 'Load more' - )} - - - - ) -} - -// TODO: Set default props -NextPageButtonWrapper.propTypes = { - isFetchingMore: PropTypes.bool, - href: PropTypes.string, - fetchMore: PropTypes.func.isRequired, - children: PropTypes.string, - automatic: PropTypes.bool, - topOffset: PropTypes.number, - bottomOffset: PropTypes.number, -} - -NextPageButtonWrapper.defaultProps = { - isFetchingMore: false, - href: undefined, - children: undefined, - automatic: true, - topOffset: -250, - bottomOffset: -250, -} - -export default NextPageButtonWrapper diff --git a/packages/client/app/components/NextPageButton/index.jsx b/packages/client/app/components/NextPageButton/index.jsx new file mode 100644 index 000000000..4124ccab4 --- /dev/null +++ b/packages/client/app/components/NextPageButton/index.jsx @@ -0,0 +1,68 @@ +import { useEffect, useRef } from 'react' +import { Link } from 'react-router-dom' +import PropTypes from 'prop-types' +import { Spinner } from '../shared' +import { HasNextPage, NextPageButton } from './style' + +const NextPageButtonWrapper = props => { + const { + isFetchingMore = false, + fetchMore, + href, + children, + automatic = true, + topOffset = -250, + bottomOffset = -250, + } = props + + const sentinelRef = useRef(null) + + useEffect(() => { + if (automatic === false || isFetchingMore) return undefined + + const rootMargin = `${topOffset ?? -250}px 0px ${bottomOffset ?? -250}px 0px` + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) fetchMore() + }, + { rootMargin }, + ) + + const el = sentinelRef.current + if (el) observer.observe(el) + return () => observer.disconnect() + }, [automatic, isFetchingMore, fetchMore, topOffset, bottomOffset]) + + return ( + { + evt.preventDefault() + fetchMore() + }} + to={href} + > + + {isFetchingMore ? ( + + ) : ( + children || 'Load more' + )} + + + ) +} + +// TODO: Set default props +NextPageButtonWrapper.propTypes = { + isFetchingMore: PropTypes.bool, + href: PropTypes.string, + fetchMore: PropTypes.func.isRequired, + children: PropTypes.string, + automatic: PropTypes.bool, + topOffset: PropTypes.number, + bottomOffset: PropTypes.number, +} + +export default NextPageButtonWrapper diff --git a/packages/client/app/components/NextPageButton/style.js b/packages/client/app/components/NextPageButton/style.jsx similarity index 100% rename from packages/client/app/components/NextPageButton/style.js rename to packages/client/app/components/NextPageButton/style.jsx diff --git a/packages/client/app/components/adminPageQueries.js b/packages/client/app/components/adminPageQueries.js index 3c1f9607a..97c15c204 100644 --- a/packages/client/app/components/adminPageQueries.js +++ b/packages/client/app/components/adminPageQueries.js @@ -1,7 +1,7 @@ -import gql from 'graphql-tag' +import { gql } from '@apollo/client' const QUERY = gql` - query { + query CurrentUser { currentUser { id profilePicture diff --git a/packages/client/app/components/asset-manager/src/AssetManagerPage.js b/packages/client/app/components/asset-manager/src/AssetManagerPage.js deleted file mode 100644 index ce39d29ce..000000000 --- a/packages/client/app/components/asset-manager/src/AssetManagerPage.js +++ /dev/null @@ -1,123 +0,0 @@ -/* eslint-disable no-console */ -import React from 'react' -import { get } from 'lodash' -import { adopt } from 'react-adopt' -import { AssetManager } from './ui' -import { - getEntityFilesQuery, - getSpecificFilesQuery, - uploadFilesMutation, - deleteFilesMutation, - updateFileMutation, - filesUploadedSubscription, - filesDeletedSubscription, - fileUpdatedSubscription, -} from './queries' - -const mapper = { - getEntityFilesQuery, - getSpecificFilesQuery, - filesUploadedSubscription, - filesDeletedSubscription, - fileUpdatedSubscription, - uploadFilesMutation, - deleteFilesMutation, - updateFileMutation, -} - -const mapProps = args => ({ - files: get(args.getEntityFilesQuery, 'data.getEntityFiles'), - uploadFiles: (manuscriptId, files) => { - const { - uploadFilesMutation: { uploadFiles }, - } = args - - return uploadFiles({ - variables: { - files, - fileType: 'manuscriptImage', - entityId: manuscriptId, - }, - }) - }, - deleteFiles: ids => { - const { - deleteFilesMutation: { deleteFiles }, - } = args - - return deleteFiles({ - variables: { - ids, - }, - }) - }, - refetch: (manuscriptId, sortingParams) => { - const { - getEntityFilesQuery: { refetch }, - } = args - - refetch({ - input: { - entityId: manuscriptId, - sortingParams, - includeInUse: true, - }, - }) - }, - updateFile: (fileId, data) => { - const { - updateFileMutation: { updateFile }, - } = args - - return updateFile({ - variables: { - input: { - id: fileId, - ...data, - }, - }, - }) - }, - refetching: - args.getEntityFilesQuery.networkStatus === 4 || - args.getEntityFilesQuery.networkStatus === 2, // possible apollo bug - loading: args.getEntityFilesQuery.networkStatus === 1, -}) - -const Composed = adopt(mapper, mapProps) - -const Connected = props => { - const { data, isOpen, hideModal } = props - const { manuscriptId, withImport, handleImport } = data - - return ( - - {({ - deleteFiles, - files, - loading, - uploadFiles, - updateFile, - refetching, - refetch, - }) => ( - - )} - - ) -} - -export default Connected diff --git a/packages/client/app/components/asset-manager/src/AssetManagerPage.jsx b/packages/client/app/components/asset-manager/src/AssetManagerPage.jsx new file mode 100644 index 000000000..dcc37d319 --- /dev/null +++ b/packages/client/app/components/asset-manager/src/AssetManagerPage.jsx @@ -0,0 +1,58 @@ +/* eslint-disable react/prop-types */ + +import { AssetManager } from './ui' +import { + useGetEntityFiles, + useUploadFiles, + useDeleteFiles, + useUpdateFile, + useFilesUploadedSubscription, + useFilesDeletedSubscription, + useFileUpdatedSubscription, +} from './queries' + +const Connected = props => { + const { data, isOpen, hideModal } = props + const { manuscriptId, withImport, handleImport } = data + + const { + data: filesData, + networkStatus, + refetch, + } = useGetEntityFiles(manuscriptId) + + const [uploadFiles] = useUploadFiles() + const [deleteFiles] = useDeleteFiles() + const [updateFile] = useUpdateFile() + + useFilesUploadedSubscription(refetch) + useFilesDeletedSubscription(refetch) + useFileUpdatedSubscription(refetch) + + return ( + deleteFiles({ variables: { ids } })} + files={filesData?.getEntityFiles} + handleImport={handleImport} + hideModal={hideModal} + isOpen={isOpen} + loading={networkStatus === 1} + manuscriptId={manuscriptId} + refetch={(id, sortingParams) => + refetch({ input: { entityId: id, sortingParams, includeInUse: true } }) + } + refetching={networkStatus === 4 || networkStatus === 2} + updateFile={(fileId, fileData) => + updateFile({ variables: { input: { id: fileId, ...fileData } } }) + } + uploadFiles={(id, files) => + uploadFiles({ + variables: { files, fileType: 'manuscriptImage', entityId: id }, + }) + } + withImport={withImport} + /> + ) +} + +export default Connected diff --git a/packages/client/app/components/asset-manager/src/queries/assetManagerSubscriptions.js b/packages/client/app/components/asset-manager/src/queries/assetManagerSubscriptions.js index f7edb147f..84717d924 100644 --- a/packages/client/app/components/asset-manager/src/queries/assetManagerSubscriptions.js +++ b/packages/client/app/components/asset-manager/src/queries/assetManagerSubscriptions.js @@ -1,5 +1,4 @@ -import React from 'react' -import { Subscription } from '@apollo/client/react/components' +import { useSubscription } from '@apollo/client/react' import { gql } from '@apollo/client' const FILES_UPLOADED_SUBSCRIPTION = gql` @@ -22,59 +21,20 @@ const FILE_UPDATED_SUBSCRIPTION = gql` } ` -const filesUploadedSubscription = props => { - const { render, getEntityFilesQuery } = props - - const triggerRefetch = () => { - getEntityFilesQuery.refetch() - } - - return ( - - {render} - - ) +const useFilesUploadedSubscription = refetch => { + useSubscription(FILES_UPLOADED_SUBSCRIPTION, { onData: () => refetch() }) } -const filesDeletedSubscription = props => { - const { render, getEntityFilesQuery } = props - - const triggerRefetch = () => { - getEntityFilesQuery.refetch() - } - - return ( - - {render} - - ) +const useFilesDeletedSubscription = refetch => { + useSubscription(FILES_DELETED_SUBSCRIPTION, { onData: () => refetch() }) } -const fileUpdatedSubscription = props => { - const { render, getEntityFilesQuery } = props - - const triggerRefetch = () => { - getEntityFilesQuery.refetch() - } - - return ( - - {render} - - ) +const useFileUpdatedSubscription = refetch => { + useSubscription(FILE_UPDATED_SUBSCRIPTION, { onData: () => refetch() }) } export { - filesUploadedSubscription, - filesDeletedSubscription, - fileUpdatedSubscription, + useFilesUploadedSubscription, + useFilesDeletedSubscription, + useFileUpdatedSubscription, } diff --git a/packages/client/app/components/asset-manager/src/queries/deleteFiles.js b/packages/client/app/components/asset-manager/src/queries/deleteFiles.js index 5aa33a59b..55d21c2cb 100644 --- a/packages/client/app/components/asset-manager/src/queries/deleteFiles.js +++ b/packages/client/app/components/asset-manager/src/queries/deleteFiles.js @@ -1,5 +1,4 @@ -import React from 'react' -import { Mutation } from '@apollo/client/react/components' +import { useMutation } from '@apollo/client/react' import { gql } from '@apollo/client' const DELETE_FILES = gql` @@ -8,16 +7,6 @@ const DELETE_FILES = gql` } ` -const deleteFilesMutation = props => { - const { render } = props +const useDeleteFiles = () => useMutation(DELETE_FILES) - return ( - - {(deleteFiles, deleteFilesResult) => - render({ deleteFiles, deleteFilesResult }) - } - - ) -} - -export default deleteFilesMutation +export default useDeleteFiles diff --git a/packages/client/app/components/asset-manager/src/queries/getEntityAssets.js b/packages/client/app/components/asset-manager/src/queries/getEntityAssets.js index 1b0ee2f2b..cf0ca1f84 100644 --- a/packages/client/app/components/asset-manager/src/queries/getEntityAssets.js +++ b/packages/client/app/components/asset-manager/src/queries/getEntityAssets.js @@ -1,6 +1,5 @@ -import React from 'react' -import { Query } from '@apollo/client/react/components' import { gql } from '@apollo/client' +import { useQuery } from '@apollo/client/react' const GET_ENTITY_FILES = gql` query GetEntityFilesQuery($input: EntityFilesInput) { @@ -28,28 +27,20 @@ const GET_ENTITY_FILES = gql` } ` -const getEntityFilesQuery = props => { - const { entityId, render } = props - - return ( - - {render} - - ) -} +const useGetEntityFiles = entityId => + useQuery(GET_ENTITY_FILES, { + fetchPolicy: 'cache-and-network', + variables: { + input: { + entityId, + sortingParams: [ + { key: 'name', order: 'asc' }, + { key: 'updated', order: 'asc' }, + ], + includeInUse: true, + }, + }, + }) export { GET_ENTITY_FILES } -export default getEntityFilesQuery +export default useGetEntityFiles diff --git a/packages/client/app/components/asset-manager/src/queries/getSpecificFiles.js b/packages/client/app/components/asset-manager/src/queries/getSpecificFiles.js index fc389d9de..ba0296906 100644 --- a/packages/client/app/components/asset-manager/src/queries/getSpecificFiles.js +++ b/packages/client/app/components/asset-manager/src/queries/getSpecificFiles.js @@ -1,5 +1,5 @@ -import React from 'react' -import { ApolloConsumer, gql } from '@apollo/client' +import { useApolloClient } from '@apollo/client/react' +import { gql } from '@apollo/client' const GET_SPECIFIC_FILES = gql` query GetSpecificFilesQuery($ids: [ID!]!) { @@ -26,14 +26,10 @@ const GET_SPECIFIC_FILES = gql` } ` -const getSpecificFilesQuery = props => { - const { render } = props - return ( - - {client => render({ client, query: GET_SPECIFIC_FILES })} - - ) +const useGetSpecificFiles = () => { + const client = useApolloClient() + return { client, query: GET_SPECIFIC_FILES } } export { GET_SPECIFIC_FILES } -export default getSpecificFilesQuery +export default useGetSpecificFiles diff --git a/packages/client/app/components/asset-manager/src/queries/index.js b/packages/client/app/components/asset-manager/src/queries/index.js index a110a9c7e..7ecd43385 100644 --- a/packages/client/app/components/asset-manager/src/queries/index.js +++ b/packages/client/app/components/asset-manager/src/queries/index.js @@ -1,11 +1,14 @@ -export { default as getEntityFilesQuery } from './getEntityAssets' -export { default as getSpecificFilesQuery } from './getSpecificFiles' -export { default as uploadFilesMutation } from './uploadFiles' -export { default as deleteFilesMutation } from './deleteFiles' -export { default as updateFileMutation } from './updateFile' +export { default as useGetEntityFiles } from './getEntityAssets' +export { + default as useGetSpecificFiles, + GET_SPECIFIC_FILES, +} from './getSpecificFiles' +export { default as useUploadFiles } from './uploadFiles' +export { default as useDeleteFiles } from './deleteFiles' +export { default as useUpdateFile } from './updateFile' export { - filesUploadedSubscription, - filesDeletedSubscription, - fileUpdatedSubscription, + useFilesUploadedSubscription, + useFilesDeletedSubscription, + useFileUpdatedSubscription, } from './assetManagerSubscriptions' diff --git a/packages/client/app/components/asset-manager/src/queries/updateFile.js b/packages/client/app/components/asset-manager/src/queries/updateFile.js index acd39d26c..90367e9ea 100644 --- a/packages/client/app/components/asset-manager/src/queries/updateFile.js +++ b/packages/client/app/components/asset-manager/src/queries/updateFile.js @@ -1,5 +1,4 @@ -import React from 'react' -import { Mutation } from '@apollo/client/react/components' +import { useMutation } from '@apollo/client/react' import { gql } from '@apollo/client' const UPDATE_FILE = gql` @@ -10,19 +9,6 @@ const UPDATE_FILE = gql` } ` -const updateFileMutation = props => { - const { render } = props +const useUpdateFile = () => useMutation(UPDATE_FILE) - return ( - - {(updateFile, updateFileResult) => - render({ - updateFile, - updateFileResult, - }) - } - - ) -} - -export default updateFileMutation +export default useUpdateFile diff --git a/packages/client/app/components/asset-manager/src/queries/uploadFiles.js b/packages/client/app/components/asset-manager/src/queries/uploadFiles.js index ca6b148b0..cb4656571 100644 --- a/packages/client/app/components/asset-manager/src/queries/uploadFiles.js +++ b/packages/client/app/components/asset-manager/src/queries/uploadFiles.js @@ -1,5 +1,4 @@ -import React from 'react' -import { Mutation } from '@apollo/client/react/components' +import { useMutation } from '@apollo/client/react' import { gql } from '@apollo/client' const UPLOAD_FILES = gql` @@ -10,20 +9,7 @@ const UPLOAD_FILES = gql` } ` -const uploadFilesMutation = props => { - const { render } = props - - return ( - - {(uploadFiles, uploadFilesResult) => - render({ - uploadFiles, - uploadFilesResult, - }) - } - - ) -} +const useUploadFiles = () => useMutation(UPLOAD_FILES) export { UPLOAD_FILES } -export default uploadFilesMutation +export default useUploadFiles diff --git a/packages/client/app/components/asset-manager/src/ui/ActionButton.js b/packages/client/app/components/asset-manager/src/ui/ActionButton.js deleted file mode 100644 index 70b1d09ca..000000000 --- a/packages/client/app/components/asset-manager/src/ui/ActionButton.js +++ /dev/null @@ -1,69 +0,0 @@ -import React from 'react' -import styled from 'styled-components' -import { th, darken } from '@coko/client' -import { color } from '../../../../theme' - -const Button = styled.button` - align-items: center; - background: ${({ type }) => { - if (type === 'primary') { - return color.brand1.base - } - - if (type === 'delete') { - return darken('colorError', 30) - } - - return 'none' - }}; - border: none; - cursor: pointer; - display: flex; - flex-basis: fit-content; - justify-content: center; - margin-right: calc(2 * ${th('gridUnit')}); - padding: calc(${th('gridUnit')} / 2) ${th('gridUnit')}; - - &:focus { - outline: 0; - } - - &:disabled { - background: ${th('colorFurniture')}; - cursor: not-allowed; - } - - &:not(:disabled):hover { - background: ${({ type }) => { - if (type === 'primary') { - return color.brand1.tint25 - } - - if (type === 'delete') { - return th('colorError') - } - - return 'none' - }}; - } -` - -const Label = styled.span` - color: ${th('colorTextReverse')}; - font-family: ${th('fontHeading')}; - font-size: ${th('fontSizeBase')}; - line-height: ${th('lineHeightBase')}; -` - -const ActionButton = ({ onClick, label, disabled, className, type }) => ( - -) - -export default ActionButton diff --git a/packages/client/app/components/asset-manager/src/ui/ActionButton.jsx b/packages/client/app/components/asset-manager/src/ui/ActionButton.jsx new file mode 100644 index 000000000..ef314b338 --- /dev/null +++ b/packages/client/app/components/asset-manager/src/ui/ActionButton.jsx @@ -0,0 +1,70 @@ +/* eslint-disable react/prop-types */ + +import styled from 'styled-components' +import { th, darken } from '@coko/client' +import { color } from '../../../../theme' + +const Button = styled.button` + align-items: center; + background: ${({ type }) => { + if (type === 'primary') { + return color.brand1.base + } + + if (type === 'delete') { + return darken('colorError', 30) + } + + return 'none' + }}; + border: none; + cursor: pointer; + display: flex; + flex-basis: fit-content; + justify-content: center; + margin-right: calc(2 * ${th('gridUnit')}); + padding: calc(${th('gridUnit')} / 2) ${th('gridUnit')}; + + &:focus { + outline: 0; + } + + &:disabled { + background: ${th('colorFurniture')}; + cursor: not-allowed; + } + + &:not(:disabled):hover { + background: ${({ type }) => { + if (type === 'primary') { + return color.brand1.tint25 + } + + if (type === 'delete') { + return th('colorError') + } + + return 'none' + }}; + } +` + +const Label = styled.span` + color: ${th('colorTextReverse')}; + font-family: ${th('fontHeading')}; + font-size: ${th('fontSizeBase')}; + line-height: ${th('lineHeightBase')}; +` + +const ActionButton = ({ onClick, label, disabled, className, type }) => ( + +) + +export default ActionButton diff --git a/packages/client/app/components/asset-manager/src/ui/ActionSection.js b/packages/client/app/components/asset-manager/src/ui/ActionSection.js deleted file mode 100644 index a51a671d7..000000000 --- a/packages/client/app/components/asset-manager/src/ui/ActionSection.js +++ /dev/null @@ -1,109 +0,0 @@ -import React, { Component } from 'react' -import styled from 'styled-components' -import { th, darken, grid } from '@coko/client' - -import UploadFilesButton from './UploadFilesButton' -import { Button } from './Modal' - -const Wrapper = styled.div` - align-items: center; - display: flex; - height: 10%; - justify-content: flex-start; - width: 100%; - - button:not(:last-child) { - margin-right: ${grid(1)}; - } -` - -const WarningAlert = styled.div` - background: ${darken('colorError', 30)}; - color: ${th('colorTextReverse')}; - font-family: ${th('fontHeading')}; - font-size: ${th('fontSizeBase')}; - line-height: ${th('lineHeightBase')}; - width: 100%; -` - -const SecondaryAction = styled.span` - cursor: pointer; - - &:hover { - text-decoration: underline; - } -` - -class ActionSection extends Component { - constructor(props) { - super(props) - this.state = { - shouldWarn: false, - } - - this.handleShouldWarn = this.handleShouldWarn.bind(this) - this.handleDelete = this.handleDelete.bind(this) - } - - handleShouldWarn() { - const { shouldWarn } = this.state - this.setState({ shouldWarn: !shouldWarn }) - } - - handleDelete() { - const { deleteHandler } = this.props - deleteHandler() - this.handleShouldWarn() - } - - render() { - const { shouldWarn } = this.state - - const { - shouldShowDelete, - shouldShowImport, - uploadHandler, - importHandler, - deleteDisabled, - importDisabled, - } = this.props - - return ( - - {shouldWarn ? ( - - Are you sure you want to proceed with this action?{' '} - Yes | - {' '} - - No - - - ) : ( - <> - - {shouldShowImport && ( - - ) -} - -const DefaultButton = ({ onClick, label, disabled, className, title }) => { - return ( - - ) -} - -const ButtonWithoutLabel = ({ onClick, icon, disabled, className, title }) => { - return ( - - ) -} - -export { ButtonWithIcon, DefaultButton, ButtonWithoutLabel } diff --git a/packages/client/app/components/asset-manager/src/ui/Button.jsx b/packages/client/app/components/asset-manager/src/ui/Button.jsx new file mode 100644 index 000000000..c012de945 --- /dev/null +++ b/packages/client/app/components/asset-manager/src/ui/Button.jsx @@ -0,0 +1,139 @@ +/* eslint-disable react/prop-types */ + +import styled from 'styled-components' +import { th } from '@coko/client' +import { color } from '../../../../theme' + +const Button = styled.button` + /* stylelint-disable declaration-no-important */ + align-items: center; + background: none; + border: none; + color: ${color.gray50}; + display: flex; + font-family: 'Fira Sans Condensed', sans-serif !important; + padding: 0; + /* padding: calc(${th('gridUnit')} / 2); */ + svg { + svg { + path { + fill: ${color.gray50}; + } + } + height: 28px; + width: 28px; + } + + &:disabled { + color: ${color.gray90}; + + svg { + path { + fill: ${color.gray90}; + } + } + + cursor: not-allowed !important; + font-size: ${th('fontSizeBase')} !important; + font-style: normal !important; + font-weight: 200 !important; + line-height: ${th('lineHeightBase')} !important; + } + + &:focus { + outline: 0; + } + + &:not(:disabled):hover { + /* background-color: ${color.backgroundC}; */ + color: ${color.brand1.base}; + + svg { + path { + fill: ${color.brand1.base}; + } + } + } + + &:not(:disabled):active { + /* background-color: ${color.gray90}; */ + border: none; + color: ${color.brand1.base}; + outline: none; + + svg { + path { + fill: ${color.brand1.base}; + } + } + } +` +/* stylelint-enable declaration-no-important */ + +const Icon = styled.span` + height: calc(3.5 * ${th('gridUnit')}); + /* margin: 0 ${th('gridUnit')} 0 0; */ + padding: 0; + width: calc(3.5 * ${th('gridUnit')}); +` + +const OnlyIcon = styled.span` + height: calc(3.5 * ${th('gridUnit')}); + padding: 0; + width: calc(3.5 * ${th('gridUnit')}); +` + +const Label = styled.div` + font-size: ${th('fontSizeBase')}; + line-height: ${th('lineHeightBase')}; + /* padding-right: 4px; */ +` + +const ButtonWithIcon = ({ + onClick, + icon, + label, + disabled, + title, + className, +}) => { + return ( + + ) +} + +const DefaultButton = ({ onClick, label, disabled, className, title }) => { + return ( + + ) +} + +const ButtonWithoutLabel = ({ onClick, icon, disabled, className, title }) => { + return ( + + ) +} + +export { ButtonWithIcon, DefaultButton, ButtonWithoutLabel } diff --git a/packages/client/app/components/asset-manager/src/ui/FileDetails.js b/packages/client/app/components/asset-manager/src/ui/FileDetails.js deleted file mode 100644 index 6116686bc..000000000 --- a/packages/client/app/components/asset-manager/src/ui/FileDetails.js +++ /dev/null @@ -1,183 +0,0 @@ -import React from 'react' -import styled from 'styled-components' -import { th } from '@coko/client' - -import { dateTimeFormatter, fileSizeFormatter } from './helpers' -import InfoItem from './InfoItem' -import { Button, Icons } from './Modal' - -const { exitIcon } = Icons - -const PreviewWrapper = styled.div` - align-items: center; - display: flex; - flex-direction: column; - height: 100%; - justify-content: flex-start; - padding-left: 16px; - width: 35%; -` - -const ClosePreview = styled.div` - display: flex; - height: 4.96%; - justify-content: flex-end; - width: 100%; -` - -const ImagePreviewer = styled.div` - align-items: center; - align-self: center; - display: flex; - height: 53.79%; - justify-content: center; - width: 100%; -` - -const InfoSection = styled.div` - display: flex; - flex-direction: column; - height: 41.25%; - overflow-y: auto; - width: 100%; -` - -const InfoHeaderWrapper = styled.div` - background: white; - border-bottom: 1px solid black; - display: flex; - margin-bottom: 4px; - position: sticky; - top: 0; - width: 100%; -` - -const InfoHeader = styled.h2` - font-family: ${th('fontHeading')}; - font-size: ${th('fontSizeHeading4')}; - line-height: ${th('lineHeightHeading4')}; - margin: 0; - padding: 0; -` - -const ItemWrapper = styled.div` - align-items: flex-start; - display: flex; - flex-direction: column; - margin-bottom: 4px; - width: 100%; -` - -const ItemHeader = styled.h5` - font-family: ${th('fontHeading')}; - font-size: ${th('fontSizeHeading6')}; - line-height: ${th('lineHeightHeading6')}; - margin: 0; - padding: 0; -` - -const FileDetails = ({ file, updateFile, closeHandler }) => { - const { alt, id, name, updated, storedObjects } = file - - const storedObjectOriginal = storedObjects.find( - storedObject => storedObject.type === 'original', - ) - - const storedObjectSmall = storedObjects.find( - storedObject => storedObject.type === 'small', - ) - - return ( - - - -) - -export default IconButton diff --git a/packages/client/app/components/asset-manager/src/ui/IconButton.jsx b/packages/client/app/components/asset-manager/src/ui/IconButton.jsx new file mode 100644 index 000000000..6ef44298a --- /dev/null +++ b/packages/client/app/components/asset-manager/src/ui/IconButton.jsx @@ -0,0 +1,51 @@ +/* eslint-disable react/prop-types */ + +import styled from 'styled-components' +import { color } from '../../../../theme' + +const Button = styled.button` + align-items: center; + background: none; + border: none; + cursor: pointer; + display: flex; + flex-basis: fit-content; + height: 24px; + justify-content: center; + padding: 0; + width: 24px; + + &:focus { + outline: 0; + } + + svg { + height: 24px; + width: 24px; + } + + &:disabled { + svg { + fill: ${color.gray90}; + } + } + + &:not(:disabled):hover { + svg { + fill: ${color.brand1.base}; + } + } +` + +const IconButton = ({ onClick, icon, disabled, className, type }) => ( + +) + +export default IconButton diff --git a/packages/client/app/components/asset-manager/src/ui/InfoItem.js b/packages/client/app/components/asset-manager/src/ui/InfoItem.js deleted file mode 100644 index 5fe287b70..000000000 --- a/packages/client/app/components/asset-manager/src/ui/InfoItem.js +++ /dev/null @@ -1,135 +0,0 @@ -import React, { Component } from 'react' -import styled from 'styled-components' -import { th } from '@coko/client' - -import { Button, Icons } from './Modal' -import { color } from '../../../../theme' - -const { saveIcon, editIcon, exitIcon } = Icons - -const Input = styled.input` - border: 0; - font-family: ${th('fontHeading')}; - font-size: ${th('fontSizeBase')}; - line-height: ${th('lineHeightBase')}; - outline: 0; - padding: 0; - width: 78.2%; - - &:focus { - border-bottom: 1px dashed ${color.brand1.base}; - outline: 0; - } - - &:placeholder-shown { - font-size: ${th('fontSizeBase')}; - line-height: ${th('lineHeightBase')}; - } -` - -const PlainItem = styled.div` - font-family: ${th('fontHeading')}; - font-size: ${th('fontSizeBase')}; - line-height: ${th('lineHeightBase')}; - text-align: left; - width: 100%; -` - -const Wrapper = styled.div` - align-items: center; - display: flex; - justify-content: flex-start; - width: 100%; -` - -const Actions = styled.div` - display: flex; - width: 15.8%; -` - -class InfoItem extends Component { - constructor(props) { - super(props) - - this.state = { - initialValue: props.value, - newValue: props.value, - focus: false, - editMode: false, - } - - this.handleKeyPress = this.handleKeyPress.bind(this) - this.handleEditMode = this.handleEditMode.bind(this) - this.handleCancel = this.handleCancel.bind(this) - this.handleSave = this.handleSave.bind(this) - this.renderItem = this.renderItem.bind(this) - } - - handleKeyPress(e) { - this.setState({ newValue: e.target.value }) - } - - handleEditMode(e) { - this.setState({ editMode: true, focus: true }) - } - - handleSave(e) { - const { updateFile, type } = this.props - const { newValue } = this.state - const self = this - updateFile({ [type]: newValue }).then(() => - self.setState({ editMode: false, initialValue: newValue, focus: false }), - ) - } - - handleCancel(e) { - const { initialValue } = this.state - this.setState({ editMode: false, newValue: initialValue, focus: false }) - } - - renderItem() { - const { editable, value } = this.props - const { newValue, editMode, focus } = this.state - - if (editable) { - return !editMode ? ( - <> - {value} - -