diff --git a/.eslintrc.json b/.eslintrc.json index a067703569..4bc0d8de2e 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -21,6 +21,7 @@ } }, "rules": { + "no-alert": "error", "sort-class-members/sort-class-members": [ "error", { @@ -41,7 +42,7 @@ "import/no-named-as-default": "off", "max-classes-per-file": "error", "no-useless-escape": "off", - "react/display-name": "warn", + "react/display-name": "error", "react/jsx-no-target-blank": "warn", // https://github.com/standard/eslint-config-standard-with-typescript/issues/248 "react/no-deprecated": "warn", @@ -84,29 +85,7 @@ "camelCase": true, "pascalCase": true }, - // TODO - most can be renamed although holding off until after #1501 and #1533 merged - "ignore": [ - "docusaurus.config.gh-pages.js", - "install-clean.ts", - "SWUpdateNotification.tsx", - "react-app-env.d.ts", - "service-worker.ts", - "AbstractDBClient.ts", - "react-firebase-file-uploader.d.ts", - "reset-staging-site.ts", - "post-cra-build.ts", - "user_pp.mock.tsx", - "user_pp.models.tsx", - "admin-subheader.tsx", - "admin-approvals.tsx", - "admin-howtos.tsx", - "admin-mappins.tsx", - "admin-research-detail.tsx", - "admin-research.tsx", - "admin-tags.tsx", - "admin-user-detail.tsx", - "admin-users.tsx" - ] + "ignore": ["react-app-env.d.ts", "service-worker.ts"] } ], "@typescript-eslint/no-empty-function": "off", diff --git a/.github/workflows/reset-staging-site.yml b/.github/workflows/reset-staging-site.yml index 86abdb2a59..863fbf606d 100644 --- a/.github/workflows/reset-staging-site.yml +++ b/.github/workflows/reset-staging-site.yml @@ -1,5 +1,5 @@ # Action to allow trigger of the firebase project migration script, used to reset the staging site -# For more info see the script file (/scripts/maintenance/reset-staging-site) +# For more info see the script file (/scripts/maintenance/resetStagingSite.ts) name: Reset Staging Site on: # Run weekly on a Sunday at 02:00 @@ -42,4 +42,4 @@ jobs: env: ONEARMY_MIGRATOR_SERVICE_ACCOUNT_JSON: ${{secrets.ONEARMY_MIGRATOR_SERVICE_ACCOUNT_JSON}} - name: Run db migration script - run: ts-node --project scripts/tsconfig.json scripts/maintenance/reset-staging-site.ts + run: ts-node --project scripts/tsconfig.json scripts/maintenance/resetStagingSite.ts.ts diff --git a/package.json b/package.json index 63e5605456..9428feda44 100644 --- a/package.json +++ b/package.json @@ -183,6 +183,7 @@ "eslint-plugin-import": "^2.26.0", "eslint-plugin-jest": "^27.2.1", "eslint-plugin-mocha": "^10.1.0", + "eslint-plugin-prefer-arrow-functions": "^3.1.4", "eslint-plugin-prettier": "^3.4.0", "eslint-plugin-sort-class-members": "^1.15.2", "eslint-plugin-unicorn": "^36.0.0", diff --git a/packages/components/.eslintrc.json b/packages/components/.eslintrc.json index e2b2a7cd91..a5a55a57d5 100644 --- a/packages/components/.eslintrc.json +++ b/packages/components/.eslintrc.json @@ -14,7 +14,8 @@ { "files": ["**/*.stories.tsx"], "rules": { - "import/no-default-export": "off" + "import/no-default-export": "off", + "no-alert": "off" } } ] diff --git a/packages/components/src/ArticleCallToAction/ArticleCallToAction.tsx b/packages/components/src/ArticleCallToAction/ArticleCallToAction.tsx index d8ef7f0f96..bba47fb228 100644 --- a/packages/components/src/ArticleCallToAction/ArticleCallToAction.tsx +++ b/packages/components/src/ArticleCallToAction/ArticleCallToAction.tsx @@ -27,10 +27,11 @@ export const ArticleCallToAction = (props: Props) => { sx={{ ml: 1 }} /> - {props.contributors && ( + {props.contributors && props?.contributors.length ? ( With contributions from:{' '} {props.contributors.map((contributor, key) => ( @@ -39,14 +40,12 @@ export const ArticleCallToAction = (props: Props) => { user={contributor} isVerified={contributor.isVerified} sx={{ - fontSize: '16px', - color: 'grey', mr: 1, }} /> ))} - )} + ) : null} Like what you see? 👇 ( { cy.get(`[data-cy="comments-form"]`).should('not.exist') }) + + it('[Views only visible for beta-testers]', () => { + cy.visit(specificHowtoUrl) + cy.step(`ViewsCounter should not be visible`) + cy.get('[data-cy="ViewsCounter"]').should('not.exist') + }) }) describe('[By Authenticated]', () => { @@ -200,6 +206,23 @@ describe('[How To]', () => { }) }) + describe('[By Beta-Tester]', () => { + it('[Views show on multiple howtos]', () => { + cy.login('demo_beta_tester@example.com', 'demo_beta_tester') + + cy.step('Views show on first howto') + cy.visit(specificHowtoUrl) + cy.get('[data-cy="ViewsCounter"]').should('exist') + + cy.step('Go back') + cy.get('[data-cy="go-back"]:eq(0)').as('topBackButton').click() + + cy.step('Views show on second howto') + cy.visit('/how-to/make-glass-like-beams') + cy.get('[data-cy="ViewsCounter"]').should('exist') + }) + }) + it('[By Owner]', () => { cy.step('Edit button is available to the owner') cy.visit(specificHowtoUrl) diff --git a/packages/cypress/src/integration/research/read.spec.ts b/packages/cypress/src/integration/research/read.spec.ts index f4eb3bd140..1de2a8d494 100644 --- a/packages/cypress/src/integration/research/read.spec.ts +++ b/packages/cypress/src/integration/research/read.spec.ts @@ -1,9 +1,8 @@ describe('[Research]', () => { const SKIP_TIMEOUT = { timeout: 300 } - const totalResearchCount = 1 + const totalResearchCount = 2 describe('[List research articles]', () => { - const researchArticleUrl = '/research/qwerty' beforeEach(() => { cy.visit('/research') }) @@ -14,24 +13,58 @@ describe('[Research]', () => { cy.get('[data-cy="ResearchListItem"]') .its('length') .should('be.eq', totalResearchCount) + }) + }) + + describe('[Read a research article]', () => { + const researchArticleUrl = '/research/qwerty' + beforeEach(() => { + cy.visit('/research') + }) - cy.step('Research cards has basic info') - cy.get( - `[data-cy="ResearchListItem"] a[href="${researchArticleUrl}"]`, - ).within(() => { - cy.contains('qwerty').should('be.exist') - cy.contains('event_reader').should('be.exist') - cy.get('[data-cy="ItemUpdateText"]').contains('1').should('be.exist') + describe('[By Everyone]', () => { + it('[See all info]', () => { + cy.step('Research cards has basic info') + cy.get( + `[data-cy="ResearchListItem"] a[href="${researchArticleUrl}"]`, + ).within(() => { + cy.contains('qwerty').should('be.exist') + cy.contains('event_reader').should('be.exist') + cy.get('[data-cy="ItemUpdateText"]').contains('1').should('be.exist') + }) + + cy.step( + `Open Research details when click on a Research ${researchArticleUrl}`, + ) + cy.get( + `[data-cy="ResearchListItem"] a[href="${researchArticleUrl}"]`, + SKIP_TIMEOUT, + ).click() + cy.url().should('include', researchArticleUrl) + }) + + it('[Views only visible for beta-testers]', () => { + cy.step(`ViewsCounter should not be visible`) + cy.visit(researchArticleUrl) + cy.get('[data-cy="ViewsCounter"]').should('not.exist') }) + }) - cy.step( - `Open Research details when click on a Research ${researchArticleUrl}`, - ) - cy.get( - `[data-cy="ResearchListItem"] a[href="${researchArticleUrl}"]`, - SKIP_TIMEOUT, - ).click() - cy.url().should('include', researchArticleUrl) + describe('[Beta-tester]', () => { + it('[Views show on multiple research articles]', () => { + cy.login('demo_beta_tester@example.com', 'demo_beta_tester') + + cy.step('Views show on first research article') + cy.visit(researchArticleUrl) + cy.get('[data-cy="ViewsCounter"]').should('exist') + + cy.step('Go back') + cy.get('[data-cy="go-back"]:eq(0)').as('topBackButton').click() + + cy.step('Views show on second research article') + cy.visit('/research/A%20test%20research') + cy.get('[data-cy="ViewsCounter"]').should('exist') + }) }) }) }) diff --git a/packages/cypress/src/integration/settings.spec.ts b/packages/cypress/src/integration/settings.spec.ts index 0ad5d35152..653b03438d 100644 --- a/packages/cypress/src/integration/settings.spec.ts +++ b/packages/cypress/src/integration/settings.spec.ts @@ -53,7 +53,7 @@ describe('[Settings]', () => { .click() } - const addContactLink = (link: ILink) => { + const addContactLink = (link: Omit) => { if (link.index > 0) { // click the button to add another set of input fields cy.get('[data-cy=add-link]').click() @@ -359,6 +359,50 @@ describe('[Settings]', () => { .should('eqSettings', expected) }) }) + + it('[Edit Contact and Links]', () => { + cy.login('settings_member_new@test.com', 'test1234') + cy.step('Go to User Settings') + cy.clickMenuItem(UserMenuItem.Settings) + + addContactLink({ + index: 1, + label: 'social', + url: 'https://social.network', + }) + + // Remove first item + cy.get('[data-cy="delete-link-0"]').last().trigger('click') + + cy.get('[data-cy="Link.field: Modal"]').should('be.visible') + + cy.get('[data-cy="Link.field: Delete"]').trigger('click') + + cy.get('[data-cy=save]').click() + cy.get('[data-cy=save]').should('not.be.disabled') + + // Assert + cy.queryDocuments( + DbCollectionName.users, + 'userName', + '==', + expected.userName, + ).then((docs) => { + cy.log('queryDocs', docs) + expect(docs.length).to.equal(1) + cy.wrap(null) + .then(() => docs[0]) + .should('eqSettings', { + ...expected, + links: [ + { + label: 'social', + url: 'https://social.network', + }, + ], + }) + }) + }) }) describe('[Focus Machine Builder]', () => { diff --git a/packages/documentation/.eslintrc.json b/packages/documentation/.eslintrc.json new file mode 100644 index 0000000000..375f86419f --- /dev/null +++ b/packages/documentation/.eslintrc.json @@ -0,0 +1,14 @@ +{ + "rules": { + "unicorn/filename-case": [ + "error", + { + "cases": { + "camelCase": true, + "pascalCase": true + }, + "ignore": ["docusaurus.config.gh-pages.js"] + } + ] + } +} diff --git a/packages/documentation/docs/Server Maintenance/data-migration.md b/packages/documentation/docs/Server Maintenance/data-migration.md index 8bf166dea2..8b056f7d51 100644 --- a/packages/documentation/docs/Server Maintenance/data-migration.md +++ b/packages/documentation/docs/Server Maintenance/data-migration.md @@ -11,13 +11,13 @@ Currently this can be done in a semi-automated way using a script in the scripts ``` cd scripts -ts-node ./maintenance/reset-staging-site.ts +ts-node ./maintenance/resetStagingSite.ts ``` :::note This script requires access to a service worker with specific permissions for source and target projects, and intermediate storage buckets. ::: -For more information about the script and known limitations see the source code at [scripts/maintenance/reset-staging-site.ts](https://github.com/ONEARMY/community-platform/blob/master/scripts/maintenance/reset-staging-site.ts). +For more information about the script and known limitations see the source code at [scripts/maintenance/resetStagingSite.ts](https://github.com/ONEARMY/community-platform/blob/master/scripts/maintenance/resetStagingSite.ts). The script is currently run weekly via the github action, see the source code at [.github/workflows/reset-staging-site.yml](https://github.com/ONEARMY/community-platform/blob/master/.github/workflows/reset-staging-site.yml)) diff --git a/scripts/install-clean.ts b/scripts/installClean.ts similarity index 100% rename from scripts/install-clean.ts rename to scripts/installClean.ts diff --git a/scripts/maintenance/reset-staging-site.ts b/scripts/maintenance/resetStagingSite.ts similarity index 99% rename from scripts/maintenance/reset-staging-site.ts rename to scripts/maintenance/resetStagingSite.ts index accea5044b..033d4944bb 100644 --- a/scripts/maintenance/reset-staging-site.ts +++ b/scripts/maintenance/resetStagingSite.ts @@ -52,7 +52,7 @@ const COLLECTION_IDS = Object.values(DB_ENDPOINTS) * * Example execution * ``` - ts-node --project scripts/tsconfig.json scripts/maintenance/reset-staging-site.ts + ts-node --project scripts/tsconfig.json scripts/maintenance/resetStagingSite.ts.ts ``` Also available as a github action in the /.github/reset-staging-site.yml */ diff --git a/scripts/package.json b/scripts/package.json index be5d43c3e3..b1ca51e07c 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -2,8 +2,8 @@ "name": "oa-scripts", "version": "1.0.0", "scripts": { - "install:clean": "ts-node ./install-clean.ts", - "post-cra-build": "ts-node ./post-cra-build.ts" + "install:clean": "ts-node ./installClean.ts", + "post-cra-build": "ts-node ./postCraBuild.ts" }, "dependencies": { "cheerio": "^1.0.0-rc.10", diff --git a/scripts/post-cra-build.ts b/scripts/postCraBuild.ts similarity index 100% rename from scripts/post-cra-build.ts rename to scripts/postCraBuild.ts diff --git a/shared/mocks/data/research.ts b/shared/mocks/data/research.ts index 3c53d5de9d..31d16eabf0 100644 --- a/shared/mocks/data/research.ts +++ b/shared/mocks/data/research.ts @@ -388,4 +388,19 @@ export const research = { }, ], }, + '0up6oJCTT3M9bDYx34Et': { + _created: '2023-02-27T22:08:25.999Z', + _createdBy: 'test user', + _deleted: false, + _id: '0up6oJCTT3M9bDYx34Et', + _modified: '2023-03-01T19:12:11.271Z', + creatorCountry: 'it', + description: 'A test!', + moderation: 'accepted', + slug: 'A test research', + tags: { + h1wCs0o9j60lkw3AYPB1: true, + }, + title: 'A test research', + }, } diff --git a/src/.eslintrc.json b/src/.eslintrc.json new file mode 100644 index 0000000000..31813b0715 --- /dev/null +++ b/src/.eslintrc.json @@ -0,0 +1,15 @@ +{ + "rules": { + "no-console": "error", + "prefer-arrow-functions/prefer-arrow-functions": [ + "error", + { + "classPropertiesAllowed": false, + "disallowPrototype": false, + "returnStyle": "unchanged", + "singleReturnOnly": false + } + ] + }, + "plugins": ["prefer-arrow-functions"] +} diff --git a/src/common/Error/handler.ts b/src/common/Error/handler.ts index 30abdde6d7..d8d405fa0e 100644 --- a/src/common/Error/handler.ts +++ b/src/common/Error/handler.ts @@ -1,4 +1,5 @@ import * as Sentry from '@sentry/react' +import { logger } from '../../logger' import { SENTRY_CONFIG } from '../../config/config' export const initErrorHandler = () => { @@ -7,7 +8,7 @@ export const initErrorHandler = () => { location.search.indexOf('noSentry=true') !== -1 || location.hostname === 'localhost' ) { - console.log('No error handler for this environment') + logger.info('No error handler for this environment') return } diff --git a/src/common/Error/handlers/Reloader.tsx b/src/common/Error/handlers/Reloader.tsx index defc829008..b03298bb44 100644 --- a/src/common/Error/handlers/Reloader.tsx +++ b/src/common/Error/handlers/Reloader.tsx @@ -16,7 +16,7 @@ export const attemptReload = () => { * Set a sessionStorage value with a future expiry date that will prompt self-deletion * even if page session not closed (i.e. tab left open) **/ -function setWithExpiry(key: string, value: string, ttl: number) { +const setWithExpiry = (key: string, value: string, ttl: number) => { const item = { value: value, expiry: new Date().getTime() + ttl, @@ -25,7 +25,7 @@ function setWithExpiry(key: string, value: string, ttl: number) { } /** Get a sessionStorage value with an expiry date, returning value only if not expired */ -function getWithExpiry(key: string): string | null { +const getWithExpiry = (key: string): string | null => { const itemString = window.sessionStorage.getItem(key) if (!itemString) return null diff --git a/src/common/Form/Select.field.tsx b/src/common/Form/Select.field.tsx index 296eb18145..8b522559b6 100644 --- a/src/common/Form/Select.field.tsx +++ b/src/common/Form/Select.field.tsx @@ -20,20 +20,16 @@ interface ISelectFieldProps extends FieldProps { // therefore the following two functions are used for converting to-from string values and field options // depending on select type (e.g. multi) and option selected get value -function getValueFromSelect( +const getValueFromSelect = ( v: ISelectOption | ISelectOption[] | null | undefined, -) { - return v ? (Array.isArray(v) ? v.map((el) => el.value) : v.value) : v -} +) => (v ? (Array.isArray(v) ? v.map((el) => el.value) : v.value) : v) // given current values find the relevant select options -function getValueForSelect( +const getValueForSelect = ( opts: ISelectOption[] = [], v: string | string[] | null | undefined, -) { - function findVal(optVal: string) { - return opts.find((o) => o.value === optVal) - } +) => { + const findVal = (optVal: string) => opts.find((o) => o.value === optVal) return v ? Array.isArray(v) ? v.map((optVal) => findVal(optVal) as ISelectOption) diff --git a/src/common/Form/UnsavedChangesDialog.tsx b/src/common/Form/UnsavedChangesDialog.tsx index 4eb86ce408..629ef4a228 100644 --- a/src/common/Form/UnsavedChangesDialog.tsx +++ b/src/common/Form/UnsavedChangesDialog.tsx @@ -10,7 +10,7 @@ interface IProps { const CONFIRM_DIALOG_MSG = 'You have unsaved changes. Are you sure you want to leave this page?' -const beforeUnload = function (e) { +const beforeUnload = (e) => { e.preventDefault() e.returnValue = CONFIRM_DIALOG_MSG } diff --git a/src/common/isUserVerified.ts b/src/common/isUserVerified.ts index 2b9e80a395..313bdc4e25 100644 --- a/src/common/isUserVerified.ts +++ b/src/common/isUserVerified.ts @@ -1,7 +1,7 @@ import type { AggregationsStore } from 'src/stores/Aggregations/aggregations.store' import { useCommonStores } from '../' -export const isUserVerified = function (userId: string) { +export const isUserVerified = (userId: string) => { const { aggregationsStore } = useCommonStores().stores return isUserVerifiedWithStore(userId, aggregationsStore) } @@ -11,9 +11,7 @@ export const isUserVerified = function (userId: string) { * are not compatible with hooks. * https://reactjs.org/docs/hooks-intro.html */ -export const isUserVerifiedWithStore = function ( +export const isUserVerifiedWithStore = ( userId: string, store: AggregationsStore, -) { - return store.aggregations.users_verified?.[userId] -} +) => store.aggregations.users_verified?.[userId] diff --git a/src/config/config.ts b/src/config/config.ts index a4f42205e3..6aa33e0df2 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -25,7 +25,7 @@ import type { UserRole } from '../models' * @param fallbackValue - optional fallback value * @returns string */ -function _c(property: ConfigurationOption, fallbackValue?: string): string { +const _c = (property: ConfigurationOption, fallbackValue?: string): string => { const configurationSource = ['development', 'test'].includes( process.env.NODE_ENV, ) @@ -43,7 +43,7 @@ export const getConfigurationOption = _c // On dev sites user can override default role const devSiteRole: UserRole = localStorage.getItem('devSiteRole') as UserRole -function getSiteVariant(): siteVariants { +const getSiteVariant = (): siteVariants => { const devSiteVariant: siteVariants = localStorage.getItem( 'devSiteVariant', ) as any diff --git a/src/logger/index.ts b/src/logger/index.ts index bde445c9c8..b5e1d120e8 100644 --- a/src/logger/index.ts +++ b/src/logger/index.ts @@ -1,6 +1,6 @@ import Logger from 'pino' import { createPinoBrowserSend, createWriteStream } from 'pino-logflare' -import { getConfigurationOption } from 'src/config/config' +import { getConfigurationOption } from '../config/config' const logLevel = getConfigurationOption('REACT_APP_LOG_LEVEL', 'info') diff --git a/src/models/index.ts b/src/models/index.ts index d104e6a0e7..4c91736cd1 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -11,7 +11,7 @@ export * from './project.models' export * from './selectorList.models' export * from './tags.model' export * from './user.models' -export * from './user_pp.models' +export * from './userPreciousPlastic.models' export interface UserComment extends IComment { isEditable: boolean diff --git a/src/models/maps.models.tsx b/src/models/maps.models.tsx index 7c7664c286..42d587fcf1 100644 --- a/src/models/maps.models.tsx +++ b/src/models/maps.models.tsx @@ -1,5 +1,5 @@ import type { ISODateString, IModerable } from './common.models' -import type { WorkspaceType } from './user_pp.models' +import type { WorkspaceType } from './userPreciousPlastic.models' import type { ProfileTypeLabel } from '../modules/profile/types' /** diff --git a/src/models/research.models.tsx b/src/models/research.models.tsx index 331c95856c..4330f30852 100644 --- a/src/models/research.models.tsx +++ b/src/models/research.models.tsx @@ -6,7 +6,7 @@ import type { IResearchCategory } from './researchCategories.model' /** * Research retrieved from the database also include metadata such as _id, _created and _modified */ -export type IResearchDB = IResearch.ItemDB & DBDoc +export type IResearchDB = DBDoc & IResearch.ItemDB export type IResearchStats = { votedUsefulCount: number @@ -15,12 +15,13 @@ export type IResearchStats = { /** All typings related to the Research Module can be found here */ export namespace IResearch { /** The main research item, as created by a user */ - export interface Item extends FormInput { + export type Item = { updates: Update[] mentions?: UserMention[] _createdBy: string total_views?: number - } + collaborators: string[] + } & Omit /** A research item update */ export interface Update { @@ -39,11 +40,14 @@ export namespace IResearch { slug: string tags: ISelectedTags creatorCountry?: string + collaborators: string } /** Research items synced from the database will contain additional metadata */ // Use of Omit to override the 'updates' type to UpdateDB - export type ItemDB = Omit & { updates: UpdateDB[] } & DBDoc + export type ItemDB = Omit & { + updates: UpdateDB[] + } & DBDoc export type UpdateDB = Update & DBDoc } diff --git a/src/models/user.models.tsx b/src/models/user.models.tsx index c7140da94c..acd0e4091b 100644 --- a/src/models/user.models.tsx +++ b/src/models/user.models.tsx @@ -56,6 +56,7 @@ export interface IUserBadges { } interface IExternalLink { + key: string url: string label: | 'email' diff --git a/src/models/user_pp.models.tsx b/src/models/userPreciousPlastic.models.tsx similarity index 100% rename from src/models/user_pp.models.tsx rename to src/models/userPreciousPlastic.models.tsx diff --git a/src/modules/admin/components/admin-subheader.tsx b/src/modules/admin/components/AdminSubheader.tsx similarity index 100% rename from src/modules/admin/components/admin-subheader.tsx rename to src/modules/admin/components/AdminSubheader.tsx diff --git a/src/modules/admin/components/Table/HeadFilter.tsx b/src/modules/admin/components/Table/HeadFilter.tsx index ad945b7cd1..2d4e189e63 100644 --- a/src/modules/admin/components/Table/HeadFilter.tsx +++ b/src/modules/admin/components/Table/HeadFilter.tsx @@ -25,7 +25,7 @@ const Top = styled(Box)` border-bottom: 15px solid white; ` -function HeadFilter(props: Props) { +const HeadFilter = (props: Props) => { const { field, filterOptions, diff --git a/src/modules/admin/components/Table/Table.tsx b/src/modules/admin/components/Table/Table.tsx index 9aced96fad..a253432d1a 100644 --- a/src/modules/admin/components/Table/Table.tsx +++ b/src/modules/admin/components/Table/Table.tsx @@ -58,98 +58,96 @@ const getTHeadThProps: ComponentPropsGetterC = ( } } -function Table(props: ITableProps) { - return ( - <> - {/* Override styles applied in global css */} - - { - const isSortAsc = row.className?.includes('-sort-asc') - const isSortDesc = row.className?.includes('-sort-desc') - const { header, className, toggleSort } = row - return ( - - toggleSort?.(e)} - p={2} - sx={{ - backgroundColor: '#E2EDF7', - borderRadius: '4px', - }} - className={className} - > - {header} - - {isSortAsc && ⬆️} - {isSortDesc && ⬇️} - {props.filterComponent && props.filterComponent({ ...row })} - - ) - }} - TdComponent={(col) => ( - - - - )} - getTdProps={getTdProps} - getTheadThProps={getTHeadThProps} - getTrProps={() => { - return { - style: { - marginTop: '10px', - marginBottom: '10px', - border: '1px solid', - borderRadius: '10px', - display: 'flex', - alignItems: 'center', - padding: '10px', - height: '4rem', - backgroundColor: 'white', +const Table = (props: ITableProps) => ( + <> + {/* Override styles applied in global css */} + + { + const isSortAsc = row.className?.includes('-sort-asc') + const isSortDesc = row.className?.includes('-sort-desc') + const { header, className, toggleSort } = row + return ( + + toggleSort?.(e)} + p={2} + sx={{ + backgroundColor: '#E2EDF7', + borderRadius: '4px', + }} + className={className} + > + {header} + + {isSortAsc && ⬆️} + {isSortDesc && ⬇️} + {props.filterComponent && props.filterComponent({ ...row })} + + ) + }} + TdComponent={(col) => ( + { - return { - style: { - marginTop: '10px', + 'a:hover': { + textDecoration: 'none', }, - } - }} - showPagination={true} - data={props.data} - columns={props.columns} - defaultPageSize={10} - minRows={props.data.length ? 3 : 1} - showPageSizeOptions={true} - sortable - /> - - ) -} + }} + > + + + )} + getTdProps={getTdProps} + getTheadThProps={getTHeadThProps} + getTrProps={() => { + return { + style: { + marginTop: '10px', + marginBottom: '10px', + border: '1px solid', + borderRadius: '10px', + display: 'flex', + alignItems: 'center', + padding: '10px', + height: '4rem', + backgroundColor: 'white', + }, + } + }} + getPaginationProps={() => { + return { + style: { + marginTop: '10px', + }, + } + }} + showPagination={true} + data={props.data} + columns={props.columns} + defaultPageSize={10} + minRows={props.data.length ? 3 : 1} + showPageSizeOptions={true} + sortable + /> + +) export default Table diff --git a/src/modules/admin/components/Table/TableHead.tsx b/src/modules/admin/components/Table/TableHead.tsx index cb40d289bb..a89b554c32 100644 --- a/src/modules/admin/components/Table/TableHead.tsx +++ b/src/modules/admin/components/Table/TableHead.tsx @@ -6,22 +6,20 @@ interface Props { row: any } -function TableHead({ children, row }: Props) { - return ( - - {children} - - ) -} +const TableHead = ({ children, row }: Props) => ( + + {children} + +) export default TableHead diff --git a/src/modules/admin/components/adminUserSearch.tsx b/src/modules/admin/components/adminUserSearch.tsx index 972fc81813..d5d5e4173f 100644 --- a/src/modules/admin/components/adminUserSearch.tsx +++ b/src/modules/admin/components/adminUserSearch.tsx @@ -7,7 +7,7 @@ type Props = { onSearchChange: (text: string) => void } -function AdminUserSearch({ total, onSearchChange }: Props) { +const AdminUserSearch = ({ total, onSearchChange }: Props) => { const theme = useTheme() return ( diff --git a/src/modules/admin/index.tsx b/src/modules/admin/index.tsx index e1596cca8a..de54a836e2 100644 --- a/src/modules/admin/index.tsx +++ b/src/modules/admin/index.tsx @@ -2,11 +2,21 @@ import type { IPageMeta } from 'src/pages/PageList' import { AdminStoreV2Context, AdminStoreV2 } from './admin.storeV2' import { MODULE } from '..' import adminRoutes from './admin.routes' -import AdminSubheader from './components/admin-subheader' +import AdminSubheader from './components/AdminSubheader' import { AuthRoute } from 'src/pages/common/AuthRoute' const moduleName = MODULE.ADMIN +/** + * Wraps the research module routing elements with the research module provider + */ +const AdminModuleContainer = () => ( + + + + +) + export const AdminModule: IPageMeta = { moduleName, path: `/${moduleName}`, @@ -15,15 +25,3 @@ export const AdminModule: IPageMeta = { description: 'Admin Home Page', requiredRole: 'admin', } - -/** - * Wraps the research module routing elements with the research module provider - */ -function AdminModuleContainer() { - return ( - - - - - ) -} diff --git a/src/modules/admin/pages/adminUsers.tsx b/src/modules/admin/pages/adminUsers.tsx index 14a91b85d2..80b37a5fd0 100644 --- a/src/modules/admin/pages/adminUsers.tsx +++ b/src/modules/admin/pages/adminUsers.tsx @@ -1,6 +1,7 @@ import { Box, Text } from 'theme-ui' import { useEffect, useState } from 'react' import { format } from 'date-fns' +import { logger } from '../../../logger' import Table from '../components/Table/Table' import type { ITableProps, @@ -91,7 +92,7 @@ const AdminUsers = observer(() => { const index = Fuse.createIndex(fuseOptions.keys, usersdata) fuse.setCollection(usersdata, index) } catch (error) { - console.log({ error }) + logger.info({ error }) } setLoading(false) } diff --git a/src/modules/index.ts b/src/modules/index.ts index b942d86e27..463a0bf2bd 100644 --- a/src/modules/index.ts +++ b/src/modules/index.ts @@ -11,7 +11,7 @@ export enum MODULE { ADMIN = 'admin_v2', } -export function getSupportedModules(): MODULE[] { +export const getSupportedModules = (): MODULE[] => { const envModules: string[] = getConfigurationOption( 'REACT_APP_SUPPORTED_MODULES', @@ -24,6 +24,5 @@ export function getSupportedModules(): MODULE[] { ) } -export function isModuleSupported(MODULE): boolean { - return getSupportedModules().includes(MODULE) -} +export const isModuleSupported = (MODULE): boolean => + getSupportedModules().includes(MODULE) diff --git a/src/modules/profile/SupportedProfileTypesFactory.ts b/src/modules/profile/SupportedProfileTypesFactory.ts index e84200a940..988877d225 100644 --- a/src/modules/profile/SupportedProfileTypesFactory.ts +++ b/src/modules/profile/SupportedProfileTypesFactory.ts @@ -46,7 +46,7 @@ const MemberAndSpace = { }, } -function getProfileTypes(currentTheme?: PlatformTheme) { +const getProfileTypes = (currentTheme?: PlatformTheme) => { const PROFILE_TYPES: IProfileType[] = [ { label: ProfileType.MEMBER, @@ -107,10 +107,10 @@ function getProfileTypes(currentTheme?: PlatformTheme) { return PROFILE_TYPES } -export function SupportedProfileTypesFactory( +export const SupportedProfileTypesFactory = ( configurationString: string, currentTheme?: PlatformTheme, -) { +) => { const supportedProfileTypes = (configurationString || DEFAULT_PROFILE_TYPES) .split(',') .map((s) => s.trim()) diff --git a/src/modules/profile/index.ts b/src/modules/profile/index.ts index 6d751be330..99c3543d97 100644 --- a/src/modules/profile/index.ts +++ b/src/modules/profile/index.ts @@ -2,7 +2,7 @@ import { getConfigurationOption } from '../../config/config' import { SupportedProfileTypesFactory } from './SupportedProfileTypesFactory' import type { PlatformTheme } from '../../themes/types' -export function getSupportedProfileTypes(currentTheme?: PlatformTheme) { +export const getSupportedProfileTypes = (currentTheme?: PlatformTheme) => { const supportedProfileTypes = SupportedProfileTypesFactory( getConfigurationOption('REACT_APP_PLATFORM_PROFILES', ''), currentTheme, diff --git a/src/pages/Academy/Academy.tsx b/src/pages/Academy/Academy.tsx index a5085cdc9e..9773dccd0f 100644 --- a/src/pages/Academy/Academy.tsx +++ b/src/pages/Academy/Academy.tsx @@ -2,14 +2,13 @@ import { Route } from 'react-router' import { useCommonStores } from 'src/index' import ExternalEmbed from 'src/pages/Academy/ExternalEmbed/ExternalEmbed' -export function getFrameSrc(base, path): string { - return `${base}${path +export const getFrameSrc = (base, path): string => + `${base}${path .split('/') .filter((str) => str !== 'academy' && Boolean(str)) .join('/')}` -} -export default function Academy() { +const Academy = () => { const { stores } = useCommonStores() const src = stores.themeStore.currentTheme.academyResource @@ -25,3 +24,5 @@ export default function Academy() { /> ) } + +export default Academy diff --git a/src/pages/Howto/Content/Common/Howto.form.test.skip.tsx b/src/pages/Howto/Content/Common/Howto.form.test.skip.tsx index b3a18522ad..23ef198c37 100644 --- a/src/pages/Howto/Content/Common/Howto.form.test.skip.tsx +++ b/src/pages/Howto/Content/Common/Howto.form.test.skip.tsx @@ -5,17 +5,17 @@ import { Provider } from 'mobx-react' import { HowtoForm } from './Howto.form' declare const window: any -describe('Howto form', function () { +describe('Howto form', () => { let howtoStore let tagsStore let formValues let parentType - beforeAll(function () { + beforeAll(() => { window.confirm = jest.fn(() => true) }) - beforeEach(function () { + beforeEach(() => { howtoStore = { uploadStatus: { Start: false, @@ -46,7 +46,7 @@ describe('Howto form', function () { window.confirm.mockReset() }) - it.skip('should not show the confirm dialog', async function () { + it.skip('should not show the confirm dialog', async () => { let renderResult const navProps: any = {} await waitFor(() => { @@ -76,7 +76,7 @@ describe('Howto form', function () { expect(window.confirm).not.toBeCalled() }) - it.skip('should show the confirm dialog, title change', async function () { + it.skip('should show the confirm dialog, title change', async () => { let renderResult const navProps: any = {} await waitFor(() => { diff --git a/src/pages/Howto/Content/Howto/HowToComments/HowToComments.tsx b/src/pages/Howto/Content/Howto/HowToComments/HowToComments.tsx index 1456d1d3b1..16e4343d5d 100644 --- a/src/pages/Howto/Content/Howto/HowToComments/HowToComments.tsx +++ b/src/pages/Howto/Content/Howto/HowToComments/HowToComments.tsx @@ -16,7 +16,7 @@ export const HowToComments = ({ comments }: IProps) => { const [comment, setComment] = useState('') const { stores } = useCommonStores() - async function onSubmit(comment: string) { + const onSubmit = async (comment: string) => { try { const howto = stores.howtoStore.activeHowto await stores.howtoStore.addComment(comment) @@ -49,7 +49,7 @@ export const HowToComments = ({ comments }: IProps) => { } } - async function handleEditRequest() { + const handleEditRequest = async () => { ReactGA.event({ category: 'Comments', action: 'Edit existing comment', @@ -57,7 +57,8 @@ export const HowToComments = ({ comments }: IProps) => { }) } - async function handleDelete(_id: string) { + const handleDelete = async (_id: string) => { + // eslint-disable-next-line no-alert const confirmation = window.confirm( 'Are you sure you want to delete this comment?', ) @@ -79,7 +80,7 @@ export const HowToComments = ({ comments }: IProps) => { } } - async function handleEdit(_id: string, comment: string) { + const handleEdit = async (_id: string, comment: string) => { ReactGA.event({ category: 'Comments', action: 'Update', diff --git a/src/pages/Howto/Content/Howto/HowtoDescription/HowtoDescription.tsx b/src/pages/Howto/Content/Howto/HowtoDescription/HowtoDescription.tsx index 364bfb762f..c7347ac917 100644 --- a/src/pages/Howto/Content/Howto/HowtoDescription/HowtoDescription.tsx +++ b/src/pages/Howto/Content/Howto/HowtoDescription/HowtoDescription.tsx @@ -53,12 +53,11 @@ interface IProps { onUsefulClick: () => void } -let didInit = false - const HowtoDescription = ({ howto, loggedInUser, ...props }: IProps) => { const [fileDownloadCount, setFileDownloadCount] = useState( howto.total_downloads, ) + let didInit = false const [viewCount, setViewCount] = useState() const { stores } = useCommonStores() diff --git a/src/pages/Maps/Content/Controls/transformAvailableFiltersToGroups.tsx b/src/pages/Maps/Content/Controls/transformAvailableFiltersToGroups.tsx index fd25645238..6e6aa806ae 100644 --- a/src/pages/Maps/Content/Controls/transformAvailableFiltersToGroups.tsx +++ b/src/pages/Maps/Content/Controls/transformAvailableFiltersToGroups.tsx @@ -8,8 +8,8 @@ import { transformSpecialistWorkspaceTypeToWorkspace } from './transformSpeciali const ICON_SIZE = 30 -function asOptions(mapStore, items: Array): FilterGroupOption[] { - return (items || []) +const asOptions = (mapStore, items: Array): FilterGroupOption[] => + (items || []) .filter((item) => { return !item.hidden }) @@ -39,7 +39,6 @@ function asOptions(mapStore, items: Array): FilterGroupOption[] { } }) .filter(({ number }) => !!number) -} type FilterGroupOption = { label: string diff --git a/src/pages/Maps/Content/Controls/transformSpecialistWorkspaceTypeToWorkspace.tsx b/src/pages/Maps/Content/Controls/transformSpecialistWorkspaceTypeToWorkspace.tsx index d0847ebb66..3d5dab39d1 100644 --- a/src/pages/Maps/Content/Controls/transformSpecialistWorkspaceTypeToWorkspace.tsx +++ b/src/pages/Maps/Content/Controls/transformSpecialistWorkspaceTypeToWorkspace.tsx @@ -1,9 +1,6 @@ -export function transformSpecialistWorkspaceTypeToWorkspace( +export const transformSpecialistWorkspaceTypeToWorkspace = ( type: string, -): string { - return ['extrusion', 'injection', 'shredder', 'sheetpress', 'mix'].includes( - type, - ) +): string => + ['extrusion', 'injection', 'shredder', 'sheetpress', 'mix'].includes(type) ? 'workspace' : type -} diff --git a/src/pages/Maps/Content/View/Sprites.tsx b/src/pages/Maps/Content/View/Sprites.tsx index b38b3a7275..98ee81ad91 100644 --- a/src/pages/Maps/Content/View/Sprites.tsx +++ b/src/pages/Maps/Content/View/Sprites.tsx @@ -54,7 +54,7 @@ export const createMarkerIcon = (pin: IMapPin, currentTheme: PlatformTheme) => { * to scale cluster depending on value and ensure fits in icon * @param cluster - MarkerCluster passed from creation function */ -function getClusterSizes(cluster: MarkerCluster) { +const getClusterSizes = (cluster: MarkerCluster) => { const count = cluster.getChildCount() const order = Math.round(count).toString().length switch (order) { diff --git a/src/pages/Maps/Maps.tsx b/src/pages/Maps/Maps.tsx index 86cd3e61fb..3505172c5e 100644 --- a/src/pages/Maps/Maps.tsx +++ b/src/pages/Maps/Maps.tsx @@ -9,6 +9,7 @@ import { Box } from 'theme-ui' import './styles.css' +import { logger } from '../../logger' import type { ILatLng } from 'src/models/maps.models' import { GetLocation } from 'src/utils/geolocation' import type { Map } from 'react-leaflet' @@ -68,7 +69,7 @@ class MapsPage extends React.Component { lng: position.coords.longitude, }) } catch (error) { - console.error(error) + logger.error(error) // do nothing if location cannot be retrieved } } diff --git a/src/pages/PageList.tsx b/src/pages/PageList.tsx index 022eb79dea..e0af252279 100644 --- a/src/pages/PageList.tsx +++ b/src/pages/PageList.tsx @@ -65,11 +65,10 @@ const TermsPolicy = lazy( () => import(/* webpackChunkName: "terms" */ './policy/terms'), ) -export function getAvailablePageList(supportedModules: MODULE[]): IPageMeta[] { - return COMMUNITY_PAGES.filter((pageItem) => +export const getAvailablePageList = (supportedModules: MODULE[]): IPageMeta[] => + COMMUNITY_PAGES.filter((pageItem) => supportedModules.includes(pageItem.moduleName), ) -} export interface IPageMeta { moduleName: MODULE diff --git a/src/pages/Research/Content/Common/Research.form.tsx b/src/pages/Research/Content/Common/Research.form.tsx index 8bcce6109c..664fc8765c 100644 --- a/src/pages/Research/Content/Common/Research.form.tsx +++ b/src/pages/Research/Content/Common/Research.form.tsx @@ -48,7 +48,7 @@ const Label = styled.label` display: block; ` -const beforeUnload = function (e) { +const beforeUnload = (e) => { e.preventDefault() e.returnValue = CONFIRM_DIALOG_MSG } @@ -249,6 +249,17 @@ const ResearchForm = observer((props: IProps) => { isEqual={COMPARISONS.tags} /> + + + + diff --git a/src/pages/Research/Content/Common/Update.form.tsx b/src/pages/Research/Content/Common/Update.form.tsx index 891257d9a8..fd1c2a9573 100644 --- a/src/pages/Research/Content/Common/Update.form.tsx +++ b/src/pages/Research/Content/Common/Update.form.tsx @@ -46,7 +46,7 @@ const Label = styled.label` display: block; ` -const beforeUnload = function (e) { +const beforeUnload = (e) => { e.preventDefault() e.returnValue = CONFIRM_DIALOG_MSG } diff --git a/src/pages/Research/Content/ResearchArticle.test.tsx b/src/pages/Research/Content/ResearchArticle.test.tsx new file mode 100644 index 0000000000..8590fa97f6 --- /dev/null +++ b/src/pages/Research/Content/ResearchArticle.test.tsx @@ -0,0 +1,100 @@ +import { render } from '@testing-library/react' +import { ThemeProvider } from '@theme-ui/core' +import { Provider } from 'mobx-react' +import { MemoryRouter } from 'react-router' +import { Route } from 'react-router-dom' +import { useResearchStore } from 'src/stores/Research/research.store' +import { FactoryResearchItem } from 'src/test/factories/ResearchItem' +import Theme from 'src/themes/styled.theme' + +jest.mock('src/index', () => ({ + // eslint-disable-next-line @typescript-eslint/naming-convention + __esModule: true, + useCommonStores: () => ({ + stores: { + userStore: {}, + aggregationsStore: { + aggregations: { + users_votedUsefulResearch: {}, + }, + }, + }, + }), +})) + +jest.mock('src/stores/Research/research.store') + +import ResearchArticle from './ResearchArticle' + +describe('Research Article', () => { + const mockResearchStore = { + activeResearchItem: FactoryResearchItem(), + setActiveResearchItem: jest.fn().mockResolvedValue(true), + needsModeration: jest.fn(), + getActiveResearchUpdateComments: jest.fn(), + incrementViewCount: jest.fn(), + } + + it('does not display contributors when undefined', () => { + // Arrange + ;(useResearchStore as jest.Mock).mockReturnValue({ + ...mockResearchStore, + activeResearchItem: FactoryResearchItem({ + collaborators: undefined, + }), + }) + + // Act + const wrapper = render( + + + + + + + , + , + ) + + // Assert + expect(() => { + wrapper.getAllByTestId('ArticleCallToAction: contributors') + }).toThrow() + }) + + it('displays contributors', async () => { + // Arrange + ;(useResearchStore as jest.Mock).mockReturnValue({ + ...mockResearchStore, + activeResearchItem: FactoryResearchItem({ + collaborators: ['example-username', 'another-example-username'], + }), + }) + + // Act + const wrapper = render( + + + + + + + , + ) + + // Assert + expect(wrapper.getAllByText('With contributions from:')).toHaveLength(1) + expect(wrapper.getAllByText('example-username')).toHaveLength(1) + expect(wrapper.getAllByText('another-example-username')).toHaveLength(1) + }) +}) diff --git a/src/pages/Research/Content/ResearchArticle.tsx b/src/pages/Research/Content/ResearchArticle.tsx index 934b4a1f32..8e7ad40e9c 100644 --- a/src/pages/Research/Content/ResearchArticle.tsx +++ b/src/pages/Research/Content/ResearchArticle.tsx @@ -26,7 +26,7 @@ type IProps = RouteComponentProps<{ slug: string }> const researchCommentUrlRegex = new RegExp(researchCommentUrlPattern) -function areCommentVisible(updateIndex) { +const areCommentVisible = (updateIndex) => { let showComments = false if (researchCommentUrlRegex.test(window.location.hash)) { @@ -132,6 +132,9 @@ const ResearchArticle = observer((props: IProps) => { isVerified: isUserVerified(item._createdBy), } + const collaborators = Array.isArray(item.collaborators) + ? item.collaborators + : ((item.collaborators as string) || '').split(',').filter(Boolean) return ( { mb: 16, }} > - + ({ + userName: c, + isVerified: false, + }))} + > { } }) -function transformToUserComment( +const transformToUserComment = ( comments: IComment[], loggedInUsername, -): UserComment[] { +): UserComment[] => { + if (!comments) return [] return comments.map((c) => ({ ...c, isEditable: c.creatorName === loggedInUsername, diff --git a/src/pages/Research/Content/ResearchComments/ResearchComments.tsx b/src/pages/Research/Content/ResearchComments/ResearchComments.tsx index ad399b08d4..6a2a33a051 100644 --- a/src/pages/Research/Content/ResearchComments/ResearchComments.tsx +++ b/src/pages/Research/Content/ResearchComments/ResearchComments.tsx @@ -31,7 +31,6 @@ export const getResearchCommentId = (s: string) => export const ResearchComments = ({ comments, update, - updateIndex, showComments, }: IProps) => { const [comment, setComment] = useState('') @@ -40,20 +39,12 @@ export const ResearchComments = ({ const [viewComments, setViewComments] = useState(!!showComments) const { stores } = useCommonStores() - async function onSubmit(comment: string) { + const onSubmit = async (comment: string) => { try { setLoading(true) await researchStore.addComment(comment, update as IResearch.Update) setLoading(false) setComment('') - const currResearchItem = researchStore.activeResearchItem - if (currResearchItem) { - await stores.userNotificationsStore.triggerNotification( - 'new_comment_research', - currResearchItem._createdBy, - '/research/' + currResearchItem.slug + '#update_' + updateIndex, - ) - } ReactGA.event({ category: 'Comments', @@ -74,7 +65,7 @@ export const ResearchComments = ({ } } - async function handleEditRequest() { + const handleEditRequest = async () => { ReactGA.event({ category: 'Comments', action: 'Edit existing comment', @@ -82,7 +73,8 @@ export const ResearchComments = ({ }) } - async function handleDelete(_id: string) { + const handleDelete = async (_id: string) => { + // eslint-disable-next-line no-alert const confirmation = window.confirm( 'Are you sure you want to delete this comment?', ) @@ -104,7 +96,7 @@ export const ResearchComments = ({ } } - async function handleEdit(_id: string, comment: string) { + const handleEdit = async (_id: string, comment: string) => { ReactGA.event({ category: 'Comments', action: 'Update', diff --git a/src/pages/Research/Content/ResearchDescription.tsx b/src/pages/Research/Content/ResearchDescription.tsx index 00d90047f0..d6c160d1a7 100644 --- a/src/pages/Research/Content/ResearchDescription.tsx +++ b/src/pages/Research/Content/ResearchDescription.tsx @@ -33,8 +33,6 @@ interface IProps { onUsefulClick: () => void } -let didInit = false - const ResearchDescription = ({ research, isEditable, ...props }: IProps) => { const dateLastUpdateText = (research: IResearch.ItemDB): string => { const lastModifiedDate = format(new Date(research._modified), 'DD-MM-YYYY') @@ -45,8 +43,8 @@ const ResearchDescription = ({ research, isEditable, ...props }: IProps) => { return '' } } + let didInit = false const store = useResearchStore() - const [viewCount, setViewCount] = useState() const incrementViewCount = async () => { diff --git a/src/pages/Research/Content/ResearchListItem.tsx b/src/pages/Research/Content/ResearchListItem.tsx index f24ac4dc0c..4e3e578c41 100644 --- a/src/pages/Research/Content/ResearchListItem.tsx +++ b/src/pages/Research/Content/ResearchListItem.tsx @@ -36,6 +36,7 @@ const MobileItemInfo = styled.div` ` const ResearchListItem: React.FC = ({ item }) => { + const collaborators = item['collaborators'] || [] return ( @@ -80,6 +81,22 @@ const ResearchListItem: React.FC = ({ item }) => { }} isVerified={isUserVerified(item._createdBy)} /> + {Boolean(collaborators.length) && ( + + {collaborators.length + + (collaborators.length === 1 + ? ' contributor' + : ' contributors')} + + )} {/* Hide this on mobile, show on tablet & above. */} , - title: 'Research', - description: 'Welcome to research', - // requiredRole: 'beta-tester', -} - /** * Wraps the research module routing elements with the research module provider */ -function ResearchModuleContainer() { +const ResearchModuleContainer = () => { const { aggregationsStore } = useCommonStores().stores // Ensure aggregations up-to-date when using any child pages and unsubscribe when leaving @@ -44,3 +30,17 @@ function ResearchModuleContainer() { ) } + +/** + * Default export format used for integrating with the platform + * @description The research module enables users to share ongoing updates for + * experimental projects + */ +export const ResearchModule: IPageMeta = { + moduleName: MODULE.RESEARCH, + path: '/research', + component: , + title: 'Research', + description: 'Welcome to research', + // requiredRole: 'beta-tester', +} diff --git a/src/pages/Research/research.routes.test.tsx b/src/pages/Research/research.routes.test.tsx index 3e0f4cea43..106c7d19c6 100644 --- a/src/pages/Research/research.routes.test.tsx +++ b/src/pages/Research/research.routes.test.tsx @@ -13,21 +13,19 @@ import type { ResearchStore } from 'src/stores/Research/research.store' jest.mock('src/index', () => ({ // eslint-disable-next-line @typescript-eslint/naming-convention __esModule: true, - useCommonStores() { - return { - stores: { - userStore: { - fetchAllVerifiedUsers: jest.fn(), - }, - aggregationsStore: { - aggregations: {}, - }, - researchCategoriesStore: { - allResearchCategories: [], - }, + useCommonStores: () => ({ + stores: { + userStore: { + fetchAllVerifiedUsers: jest.fn(), + }, + aggregationsStore: { + aggregations: {}, }, - } - }, + researchCategoriesStore: { + allResearchCategories: [], + }, + }, + }), })) /** When mocking research routes replace default store methods with below */ @@ -59,9 +57,7 @@ const mockResearchStore = new mockResearchStoreClass() jest.mock('src/stores/Research/research.store', () => ({ // eslint-disable-next-line @typescript-eslint/naming-convention __esModule: true, - useResearchStore() { - return mockResearchStore - }, + useResearchStore: () => mockResearchStore, })) describe('research.routes', () => { @@ -211,9 +207,7 @@ describe('research.routes', () => { jest.doMock('src/pages/Research/Content/Common/Research.form', () => ({ // eslint-disable-next-line @typescript-eslint/naming-convention __esModule: true, - default(props) { - return
{props.parentType} your research
- }, + default: (props) =>
{props.parentType} your research
, })) const wrapper = render( @@ -263,9 +257,7 @@ describe('research.routes', () => { jest.doMock('src/pages/Research/Content/Common/Research.form', () => ({ // eslint-disable-next-line @typescript-eslint/naming-convention __esModule: true, - default(props) { - return
{props.parentType} your research
- }, + default: (props) =>
{props.parentType} your research
, })) const wrapper = render( @@ -319,9 +311,7 @@ describe('research.routes', () => { jest.doMock('src/pages/Research/Content/EditUpdate/index.tsx', () => ({ // eslint-disable-next-line @typescript-eslint/naming-convention __esModule: true, - default() { - return
Edit update within research
- }, + default: () =>
Edit update within research
, })) const wrapper = render( diff --git a/src/pages/Settings/SettingsPage.tsx b/src/pages/Settings/SettingsPage.tsx index 66ea5c5495..7392fe8239 100644 --- a/src/pages/Settings/SettingsPage.tsx +++ b/src/pages/Settings/SettingsPage.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { Card, Flex, Heading, Box, Text } from 'theme-ui' -import type { IUserPP } from 'src/models/user_pp.models' +import type { IUserPP } from 'src/models/userPreciousPlastic.models' import type { ThemeStore } from 'src/stores/Theme/theme.store' import type { UserStore } from 'src/stores/User/user.store' import { observer, inject } from 'mobx-react' @@ -26,6 +26,7 @@ import { logger } from 'src/logger' import { ProfileType } from 'src/modules/profile/types' import { AuthWrapper } from 'src/common/AuthWrapper' import { UnsavedChangesDialog } from 'src/common/Form/UnsavedChangesDialog' +import { v4 as uuid } from 'uuid' interface IProps { /** user ID for lookup when editing another user as admin */ @@ -86,7 +87,10 @@ export class SettingsPage extends React.Component { coverImages: new Array(4) .fill(null) .map((v, i) => (coverImages[i] ? coverImages[i] : v)), - links: links.length > 0 ? links : [{} as any], + links: (links.length > 0 ? links : [{} as any]).map((i) => ({ + ...i, + key: uuid(), + })), openingHours: openingHours!.length > 0 ? openingHours : [{} as any], } this.setState({ diff --git a/src/pages/Settings/Template.tsx b/src/pages/Settings/Template.tsx index 1548e84f6a..72f001ffcd 100644 --- a/src/pages/Settings/Template.tsx +++ b/src/pages/Settings/Template.tsx @@ -1,4 +1,4 @@ -import type { IUserPP } from 'src/models/user_pp.models' +import type { IUserPP } from 'src/models/userPreciousPlastic.models' import type { IUser } from 'src/models/user.models' import { ProfileType } from 'src/modules/profile/types' diff --git a/src/pages/Settings/UserBadgeSettings.tsx b/src/pages/Settings/UserBadgeSettings.tsx index 05128c6ca2..615dfcade0 100644 --- a/src/pages/Settings/UserBadgeSettings.tsx +++ b/src/pages/Settings/UserBadgeSettings.tsx @@ -17,7 +17,7 @@ export const UserBadgeSettings = observer((props: { userId: string }) => { const [isLoading, setLoading] = useState(true) const [isSaving, setSaving] = useState(false) - async function fetchUser() { + const fetchUser = async () => { const user = await userStore.getUserProfile(props.userId) if (user && user.badges) { setBadges(user.badges) diff --git a/src/pages/Settings/content/formSections/Collection.section.tsx b/src/pages/Settings/content/formSections/Collection.section.tsx index ecf193132f..96b955f29f 100644 --- a/src/pages/Settings/content/formSections/Collection.section.tsx +++ b/src/pages/Settings/content/formSections/Collection.section.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import type { IUserPP } from 'src/models/user_pp.models' +import type { IUserPP } from 'src/models/userPreciousPlastic.models' import type { IPlasticType } from 'src/models' import { Flex, Heading, Box, Text, Grid } from 'theme-ui' diff --git a/src/pages/Settings/content/formSections/Fields/Link.field.tsx b/src/pages/Settings/content/formSections/Fields/ProfileLink.field.tsx similarity index 66% rename from src/pages/Settings/content/formSections/Fields/Link.field.tsx rename to src/pages/Settings/content/formSections/Fields/ProfileLink.field.tsx index 3393219fab..6327216abc 100644 --- a/src/pages/Settings/content/formSections/Fields/Link.field.tsx +++ b/src/pages/Settings/content/formSections/Fields/ProfileLink.field.tsx @@ -1,7 +1,7 @@ import { Component } from 'react' import { Field } from 'react-final-form' import { Button, FieldInput, Modal } from 'oa-components' -import { Text, Flex, Grid } from 'theme-ui' +import { Text, Flex, Grid, Box } from 'theme-ui' import { SelectField } from 'src/common/Form/Select.field' import { validateUrl, validateEmail, required } from 'src/utils/validators' import { formatLink } from 'src/utils/formatters' @@ -31,7 +31,9 @@ interface IProps { initialType?: string onDelete: () => void 'data-cy'?: string + isDeleteEnabled: boolean } + interface IState { showDeleteModal: boolean _toDocsList: boolean @@ -74,7 +76,7 @@ export class ProfileLinkField extends Component { } render() { - const { index, name } = this.props + const { index, name, isDeleteEnabled } = this.props const DeleteButton = (props) => ( ) return ( - - -
+ + + { placeholder="type" validate={required} validateFields={[]} - style={{ width: '100%', height: '40px', marginRight: '8px' }} + style={{ width: '100%', height: '40px' }} /> -
- +
+ {isDeleteEnabled ? ( + + ) : null} { style={{ width: '100%', height: '40px', marginBottom: '0px' }} /> - + {isDeleteEnabled ? ( + + ) : null} { this.toggleDeleteModal()} isOpen={!!this.state.showDeleteModal} > - Are you sure you want to delete this link? - - - - - - + + Are you sure you want to delete this link? + + + + + + + - + } diff --git a/src/pages/Settings/content/formSections/Focus.section.tsx b/src/pages/Settings/content/formSections/Focus.section.tsx index 40e3b2e6c6..452a2d7d40 100644 --- a/src/pages/Settings/content/formSections/Focus.section.tsx +++ b/src/pages/Settings/content/formSections/Focus.section.tsx @@ -7,7 +7,7 @@ import { CustomRadioField } from './Fields/CustomRadio.field' import { Field } from 'react-final-form' import { useTheme } from '@emotion/react' -function ProfileTypes() { +const ProfileTypes = () => { const theme = useTheme() const profileTypes = getSupportedProfileTypes().filter(({ label }) => Object.keys(theme.badges).includes(label), @@ -62,6 +62,6 @@ function ProfileTypes() { ) } -export function FocusSection() { - return -} +export const FocusSection = () => ( + +) diff --git a/src/pages/Settings/content/formSections/UserInfos.section.tsx b/src/pages/Settings/content/formSections/UserInfos.section.tsx index 26ca034e15..527321cd05 100644 --- a/src/pages/Settings/content/formSections/UserInfos.section.tsx +++ b/src/pages/Settings/content/formSections/UserInfos.section.tsx @@ -5,10 +5,10 @@ import { countries } from 'countries-list' import { Button, FieldInput, FieldTextarea } from 'oa-components' import theme from 'src/themes/styled.theme' import { FieldArray } from 'react-final-form-arrays' -import { ProfileLinkField } from './Fields/Link.field' +import { ProfileLinkField } from './Fields/ProfileLink.field' import { FlexSectionContainer } from './elements' import { required } from 'src/utils/validators' -import type { IUserPP } from 'src/models/user_pp.models' +import type { IUserPP } from 'src/models/userPreciousPlastic.models' import { ImageInputField } from 'src/common/Form/ImageInput.field' import type { IUser } from 'src/models' import type { IUploadedFileMeta } from 'src/stores/storage' @@ -190,7 +190,7 @@ export class UserInfosSection extends React.Component { coverImages={coverImages} /> - <> + Contacts & links * @@ -199,16 +199,19 @@ export class UserInfosSection extends React.Component { {({ fields }) => ( <> - {fields.map((name, i: number) => ( - { - fields.remove(i) - }} - index={i} - /> - ))} + {fields + ? fields.map((name, i: number) => ( + { + fields.remove(i) + }} + index={i} + isDeleteEnabled={i > 0 || (fields as any).length > 1} + /> + )) + : null}