From efb04217d9f4c717e3be6f3be4b8b4c14ee9144b Mon Sep 17 00:00:00 2001 From: AkosuaA Date: Fri, 4 Oct 2024 13:23:53 +0100 Subject: [PATCH 01/17] add compliance report model to cms --- ...04101557-create-compliance-report-model.js | 65 +++++++++++++++++++ packages/model/src/compliance-report.ts | 4 ++ packages/model/src/manuscript.ts | 2 + 3 files changed, 71 insertions(+) create mode 100644 packages/contentful/migrations/crn/complianceReports/20241004101557-create-compliance-report-model.js create mode 100644 packages/model/src/compliance-report.ts diff --git a/packages/contentful/migrations/crn/complianceReports/20241004101557-create-compliance-report-model.js b/packages/contentful/migrations/crn/complianceReports/20241004101557-create-compliance-report-model.js new file mode 100644 index 0000000000..d3a91d06f2 --- /dev/null +++ b/packages/contentful/migrations/crn/complianceReports/20241004101557-create-compliance-report-model.js @@ -0,0 +1,65 @@ +module.exports.description = 'Create Compliance Reports content model'; + +module.exports.up = (migration) => { + const complianceReports = migration + .createContentType('complianceReports') + .name('Compliance Reports') + .description(''); + + complianceReports + .createField('url') + .name('URL') + .type('Symbol') + .localized(false) + .required(true) + .validations([ + { + regexp: { + pattern: + '^(ftp|http|https):\\/\\/(\\w+:{0,1}\\w*@)?(\\S+)(:[0-9]+)?(\\/|\\/([\\w#!:.?+=&%@!\\-/]))?$', + flags: null, + }, + }, + ]) + .disabled(false) + .omitted(false); + + complianceReports + .createField('description') + .name('Description') + .type('Text') + .localized(false) + .required(true) + .validations([]) + .disabled(false) + .omitted(false); + + complianceReports + .createField('manuscriptVersion') + .name('Manuscript Version') + .type('Link') + .localized(false) + .required(true) + .validations([ + { + linkContentType: ['manuscriptVersions'], + }, + ]) + .disabled(false) + .omitted(false) + .linkType('Entry'); + + complianceReports.changeFieldControl( + 'manuscriptVersion', + 'builtin', + 'entryLinkEditor', + { + showLinkEntityAction: true, + showCreateEntityAction: false, + }, + ); +}; + +module.exports.down = (migration) => { + migration.deleteContentType('complianceReports'); +}; diff --git a/packages/model/src/compliance-report.ts b/packages/model/src/compliance-report.ts new file mode 100644 index 0000000000..1f2b57fb2e --- /dev/null +++ b/packages/model/src/compliance-report.ts @@ -0,0 +1,4 @@ +export type ComplianceReport = { + url: string; + description: string; +}; diff --git a/packages/model/src/manuscript.ts b/packages/model/src/manuscript.ts index 77d1cf2ea7..af7f63a27a 100644 --- a/packages/model/src/manuscript.ts +++ b/packages/model/src/manuscript.ts @@ -1,5 +1,6 @@ import { JSONSchemaType } from 'ajv'; import { AuthorAlgoliaResponse } from './authors'; +import { ComplianceReport } from './compliance-report'; import { UserResponse } from './user'; export const manuscriptTypes = [ @@ -109,6 +110,7 @@ export type ManuscriptVersion = { }; createdDate: string; publishedAt: string; + complianceReport?: ComplianceReport; }; export const manuscriptFormFieldsMapping: Record< From 52759942c00ba6a475dc37cc991efbb04ef78db8 Mon Sep 17 00:00:00 2001 From: AkosuaA Date: Fri, 4 Oct 2024 14:01:49 +0100 Subject: [PATCH 02/17] fix types --- packages/model/src/manuscript.ts | 5 ++++- packages/react-components/src/templates/ManuscriptForm.tsx | 7 ++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/model/src/manuscript.ts b/packages/model/src/manuscript.ts index af7f63a27a..28da982fa1 100644 --- a/packages/model/src/manuscript.ts +++ b/packages/model/src/manuscript.ts @@ -118,7 +118,10 @@ export const manuscriptFormFieldsMapping: Record< Record< ManuscriptLifecycle, Array< - keyof Omit + keyof Omit< + ManuscriptVersion, + 'complianceReport' | 'createdBy' | 'createdDate' | 'publishedAt' + > > > > = { diff --git a/packages/react-components/src/templates/ManuscriptForm.tsx b/packages/react-components/src/templates/ManuscriptForm.tsx index a002b4df97..965cb6f7a9 100644 --- a/packages/react-components/src/templates/ManuscriptForm.tsx +++ b/packages/react-components/src/templates/ManuscriptForm.tsx @@ -84,7 +84,12 @@ const apcCoverageLifecycles = [ type OptionalVersionFields = Array< keyof Omit< ManuscriptVersion, - 'type' | 'lifecycle' | 'createdBy' | 'createdDate' | 'publishedAt' + | 'type' + | 'lifecycle' + | 'complianceReport' + | 'createdBy' + | 'createdDate' + | 'publishedAt' > >; From e12149b977d1eeac6d2e278242ccd86745c9126b Mon Sep 17 00:00:00 2001 From: AkosuaA Date: Thu, 10 Oct 2024 12:46:47 +0100 Subject: [PATCH 03/17] backend updates --- apps/crn-server/src/app.ts | 17 + .../compliance-report.controller.ts | 15 + .../compliance-report.data-provider.ts | 47 +++ .../contentful/manuscript.data-provider.ts | 20 ++ .../compliance-report.data-provider.types.ts | 12 + .../src/data-providers/types/index.ts | 1 + .../src/routes/compliance-report.route.ts | 28 ++ .../compliance-report.validation.ts | 27 ++ ...04101557-create-compliance-report-model.js | 3 +- .../src/crn/autogenerated-gql/graphql.ts | 292 ++++++++++++++++++ .../src/crn/queries/manuscript.queries.ts | 8 + .../crn/schema/autogenerated-schema.graphql | 74 +++++ packages/model/src/compliance-report.ts | 8 +- packages/model/src/index.ts | 1 + packages/model/src/manuscript.ts | 7 +- 15 files changed, 555 insertions(+), 5 deletions(-) create mode 100644 apps/crn-server/src/controllers/compliance-report.controller.ts create mode 100644 apps/crn-server/src/data-providers/contentful/compliance-report.data-provider.ts create mode 100644 apps/crn-server/src/data-providers/types/compliance-report.data-provider.types.ts create mode 100644 apps/crn-server/src/routes/compliance-report.route.ts create mode 100644 apps/crn-server/src/validation/compliance-report.validation.ts diff --git a/apps/crn-server/src/app.ts b/apps/crn-server/src/app.ts index 99d59eec57..aa1376d0ad 100644 --- a/apps/crn-server/src/app.ts +++ b/apps/crn-server/src/app.ts @@ -33,6 +33,7 @@ import { } from './config'; import AnalyticsController from './controllers/analytics.controller'; import CalendarController from './controllers/calendar.controller'; +import ComplianceReportController from './controllers/compliance-report.controller'; import DashboardController from './controllers/dashboard.controller'; import DiscoverController from './controllers/discover.controller'; import EventController from './controllers/event.controller'; @@ -51,6 +52,7 @@ import UserController from './controllers/user.controller'; import WorkingGroupController from './controllers/working-group.controller'; import { AssetContentfulDataProvider } from './data-providers/contentful/asset.data-provider'; import { CalendarContentfulDataProvider } from './data-providers/contentful/calendar.data-provider'; +import { ComplianceReportContentfulDataProvider } from './data-providers/contentful/compliance-report.data-provider'; import { DashboardContentfulDataProvider } from './data-providers/contentful/dashboard.data-provider'; import { DiscoverContentfulDataProvider } from './data-providers/contentful/discover.data-provider'; import { EventContentfulDataProvider } from './data-providers/contentful/event.data-provider'; @@ -71,6 +73,7 @@ import { WorkingGroupContentfulDataProvider } from './data-providers/contentful/ import { GuideContentfulDataProvider } from './data-providers/contentful/guide.data-provider'; import { AssetDataProvider, + ComplianceReportDataProvider, DashboardDataProvider, DiscoverDataProvider, GuideDataProvider, @@ -113,6 +116,7 @@ import { ExternalAuthorDataProvider } from './data-providers/types/external-auth import { TeamDataProvider } from './data-providers/types/teams.data-provider.types'; import { AnalyticsContentfulDataProvider } from './data-providers/contentful/analytics.data-provider'; import { GenerativeContentDataProvider } from './data-providers/contentful/generative-content.data-provider'; +import { complianceReportRouteFactory } from './routes/compliance-report.route'; export const appFactory = (libs: Libs = {}): Express => { const app = express(); @@ -204,6 +208,10 @@ export const appFactory = (libs: Libs = {}): Express => { getContentfulRestClientFactory, ); + const complianceReportDataProvider = + libs.complianceReportDataProvider || + new ComplianceReportContentfulDataProvider(getContentfulRestClientFactory); + const workingGroupDataProvider = libs.workingGroupDataProvider || new WorkingGroupContentfulDataProvider( @@ -263,6 +271,9 @@ export const appFactory = (libs: Libs = {}): Express => { libs.analyticsController || new AnalyticsController(analyticsDataProvider); const calendarController = libs.calendarController || new CalendarController(calendarDataProvider); + const complianceReportController = + libs.complianceReportController || + new ComplianceReportController(complianceReportDataProvider); const dashboardController = libs.dashboardController || new DashboardController(dashboardDataProvider); const newsController = @@ -331,6 +342,9 @@ export const appFactory = (libs: Libs = {}): Express => { // Routes const analyticsRoutes = analyticsRouteFactory(analyticsController); const calendarRoutes = calendarRouteFactory(calendarController); + const complianceReportRoutes = complianceReportRouteFactory( + complianceReportController, + ); const dashboardRoutes = dashboardRouteFactory(dashboardController); const discoverRoutes = discoverRouteFactory(discoverController); const guideRoutes = guideRouteFactory(guideController); @@ -399,6 +413,7 @@ export const appFactory = (libs: Libs = {}): Express => { */ app.use(analyticsRoutes); app.use(calendarRoutes); + app.use(complianceReportRoutes); app.use(dashboardRoutes); app.use(discoverRoutes); app.use(guideRoutes); @@ -436,6 +451,7 @@ export type Libs = { analyticsDataProvider?: AnalyticsContentfulDataProvider; analyticsController?: AnalyticsController; calendarController?: CalendarController; + complianceReportController?: ComplianceReportController; dashboardController?: DashboardController; discoverController?: DiscoverController; guideController?: GuideController; @@ -454,6 +470,7 @@ export type Libs = { workingGroupController?: WorkingGroupController; assetDataProvider?: AssetDataProvider; calendarDataProvider?: CalendarDataProvider; + complianceReportDataProvider?: ComplianceReportDataProvider; dashboardDataProvider?: DashboardDataProvider; discoverDataProvider?: DiscoverDataProvider; guideDataProvider?: GuideDataProvider; diff --git a/apps/crn-server/src/controllers/compliance-report.controller.ts b/apps/crn-server/src/controllers/compliance-report.controller.ts new file mode 100644 index 0000000000..ff0ebf2cf3 --- /dev/null +++ b/apps/crn-server/src/controllers/compliance-report.controller.ts @@ -0,0 +1,15 @@ +import { ComplianceReportCreateDataObject } from '@asap-hub/model'; + +import { ComplianceReportDataProvider } from '../data-providers/types'; + +export default class ComplianceReportController { + constructor( + private complianceReportDataProvider: ComplianceReportDataProvider, + ) {} + + async create( + complianceReportCreateData: ComplianceReportCreateDataObject, + ): Promise { + return this.complianceReportDataProvider.create(complianceReportCreateData); + } +} diff --git a/apps/crn-server/src/data-providers/contentful/compliance-report.data-provider.ts b/apps/crn-server/src/data-providers/contentful/compliance-report.data-provider.ts new file mode 100644 index 0000000000..765659bfcc --- /dev/null +++ b/apps/crn-server/src/data-providers/contentful/compliance-report.data-provider.ts @@ -0,0 +1,47 @@ +import { + addLocaleToFields, + Environment, + getLinkEntity, +} from '@asap-hub/contentful'; +import { + ComplianceReportCreateDataObject, + ComplianceReportDataObject, + ListResponse, +} from '@asap-hub/model'; + +import { ComplianceReportDataProvider } from '../types'; + +export class ComplianceReportContentfulDataProvider + implements ComplianceReportDataProvider +{ + constructor(private getRestClient: () => Promise) {} + + async fetch(): Promise> { + throw new Error('Method not implemented.'); + } + + async fetchById(): Promise { + throw new Error('Method not implemented.'); + } + + async create(input: ComplianceReportCreateDataObject): Promise { + const environment = await this.getRestClient(); + + const { manuscriptVersionId, ...payload } = input; + + const complianceReport = await environment.createEntry( + 'complianceReports', + { + fields: { + ...addLocaleToFields({ + ...payload, + manuscriptVersion: getLinkEntity(manuscriptVersionId), + }), + }, + }, + ); + + await complianceReport.publish(); + return complianceReport.sys.id; + } +} diff --git a/apps/crn-server/src/data-providers/contentful/manuscript.data-provider.ts b/apps/crn-server/src/data-providers/contentful/manuscript.data-provider.ts index 105bc3eb4c..eb84b2d1a4 100644 --- a/apps/crn-server/src/data-providers/contentful/manuscript.data-provider.ts +++ b/apps/crn-server/src/data-providers/contentful/manuscript.data-provider.ts @@ -25,6 +25,14 @@ import { parseUserDisplayName } from '@asap-hub/server-common'; import { ManuscriptDataProvider } from '../types'; type ManuscriptItem = NonNullable; +// type ComplianceReport = NonNullable['items']>[number]>['linkedFrom']>['complianceReportsCollection']>['items'][number]>; +type ComplianceReport = NonNullable< + NonNullable< + NonNullable< + NonNullable['items'][number] + >['linkedFrom'] + >['complianceReportsCollection'] +>['items'][number]; export class ManuscriptContentfulDataProvider implements ManuscriptDataProvider @@ -144,6 +152,7 @@ export const parseGraphqlManuscriptVersion = ( ): ManuscriptVersion[] => versions .map((version) => ({ + id: version?.sys.id, type: version?.type, lifecycle: version?.lifecycle, manuscriptFile: { @@ -227,6 +236,9 @@ export const parseGraphqlManuscriptVersion = ( id: labItem?.sys.id, name: labItem?.name, })), + complianceReport: parseComplianceReport( + version?.linkedFrom?.complianceReportsCollection?.items[0], + ), })) .filter( (version) => @@ -239,3 +251,11 @@ export const parseGraphqlManuscriptVersion = ( )) || false, ) as ManuscriptVersion[]; + +const parseComplianceReport = ( + complianceReport: ComplianceReport | undefined, +) => + complianceReport && { + url: complianceReport.url, + description: complianceReport.description, + }; diff --git a/apps/crn-server/src/data-providers/types/compliance-report.data-provider.types.ts b/apps/crn-server/src/data-providers/types/compliance-report.data-provider.types.ts new file mode 100644 index 0000000000..fd00bedfe5 --- /dev/null +++ b/apps/crn-server/src/data-providers/types/compliance-report.data-provider.types.ts @@ -0,0 +1,12 @@ +import { + ComplianceReportCreateDataObject, + ComplianceReportDataObject, + DataProvider, +} from '@asap-hub/model'; + +export type ComplianceReportDataProvider = DataProvider< + ComplianceReportDataObject, + ComplianceReportDataObject, + null, + ComplianceReportCreateDataObject +>; diff --git a/apps/crn-server/src/data-providers/types/index.ts b/apps/crn-server/src/data-providers/types/index.ts index 4d4be3e8df..26f1bd1efc 100644 --- a/apps/crn-server/src/data-providers/types/index.ts +++ b/apps/crn-server/src/data-providers/types/index.ts @@ -1,5 +1,6 @@ export * from './analytics.data-provider.types'; export * from './assets.data-provider.types'; +export * from './compliance-report.data-provider.types'; export * from './dashboard.data-provider.types'; export * from './discover.data-provider.types'; export * from './guide.data-provider.types'; diff --git a/apps/crn-server/src/routes/compliance-report.route.ts b/apps/crn-server/src/routes/compliance-report.route.ts new file mode 100644 index 0000000000..babbbbf7da --- /dev/null +++ b/apps/crn-server/src/routes/compliance-report.route.ts @@ -0,0 +1,28 @@ +import { isCMSAdministrator } from '@asap-hub/validation'; +import Boom from '@hapi/boom'; +import { Router } from 'express'; +import ComplianceReportController from '../controllers/compliance-report.controller'; +import { validateComplianceReportPostRequestParameters } from '../validation/compliance-report.validation'; + +export const complianceReportRouteFactory = ( + complianceReportController: ComplianceReportController, +): Router => { + const complianceReportRoutes = Router(); + + complianceReportRoutes.post('/compliance-reports', async (req, res) => { + const { body, loggedInUser } = req; + const createRequest = validateComplianceReportPostRequestParameters(body); + + if (!loggedInUser || !isCMSAdministrator(loggedInUser.role)) { + throw Boom.forbidden(); + } + + const complianceReport = await complianceReportController.create({ + ...createRequest, + }); + + res.status(201).json(complianceReport); + }); + + return complianceReportRoutes; +}; diff --git a/apps/crn-server/src/validation/compliance-report.validation.ts b/apps/crn-server/src/validation/compliance-report.validation.ts new file mode 100644 index 0000000000..ac50b5c7a5 --- /dev/null +++ b/apps/crn-server/src/validation/compliance-report.validation.ts @@ -0,0 +1,27 @@ +import { ComplianceReportPostRequest } from '@asap-hub/model'; +import { validateInput } from '@asap-hub/server-common'; +import { urlExpression } from '@asap-hub/validation'; +import { JSONSchemaType } from 'ajv'; + +const complianceReportPostRequestValidationSchema: JSONSchemaType = + { + type: 'object', + properties: { + description: { type: 'string' }, + url: { + type: 'string', + pattern: urlExpression, + }, + manuscriptVersionId: { type: 'string' }, + }, + required: ['description', 'url', 'manuscriptVersionId'], + additionalProperties: false, + }; + +export const validateComplianceReportPostRequestParameters = validateInput( + complianceReportPostRequestValidationSchema, + { + skipNull: true, + coerce: true, + }, +); diff --git a/packages/contentful/migrations/crn/complianceReports/20241004101557-create-compliance-report-model.js b/packages/contentful/migrations/crn/complianceReports/20241004101557-create-compliance-report-model.js index d3a91d06f2..b07ad7ffe5 100644 --- a/packages/contentful/migrations/crn/complianceReports/20241004101557-create-compliance-report-model.js +++ b/packages/contentful/migrations/crn/complianceReports/20241004101557-create-compliance-report-model.js @@ -4,7 +4,8 @@ module.exports.up = (migration) => { const complianceReports = migration .createContentType('complianceReports') .name('Compliance Reports') - .description(''); + .description('') + .displayField('url'); complianceReports .createField('url') diff --git a/packages/contentful/src/crn/autogenerated-gql/graphql.ts b/packages/contentful/src/crn/autogenerated-gql/graphql.ts index ec00a0087a..bca8790d66 100644 --- a/packages/contentful/src/crn/autogenerated-gql/graphql.ts +++ b/packages/contentful/src/crn/autogenerated-gql/graphql.ts @@ -605,6 +605,94 @@ export enum CalendarsOrder { SysPublishedVersionDesc = 'sys_publishedVersion_DESC', } +/** [See type definition](https://app.contentful.com/spaces/5v6w5j61tndm/content_types/complianceReports) */ +export type ComplianceReports = Entry & + _Node & { + _id: Scalars['ID']; + contentfulMetadata: ContentfulMetadata; + description?: Maybe; + linkedFrom?: Maybe; + manuscriptVersion?: Maybe; + sys: Sys; + url?: Maybe; + }; + +/** [See type definition](https://app.contentful.com/spaces/5v6w5j61tndm/content_types/complianceReports) */ +export type ComplianceReportsDescriptionArgs = { + locale?: InputMaybe; +}; + +/** [See type definition](https://app.contentful.com/spaces/5v6w5j61tndm/content_types/complianceReports) */ +export type ComplianceReportsLinkedFromArgs = { + allowedLocales?: InputMaybe>>; +}; + +/** [See type definition](https://app.contentful.com/spaces/5v6w5j61tndm/content_types/complianceReports) */ +export type ComplianceReportsManuscriptVersionArgs = { + locale?: InputMaybe; + preview?: InputMaybe; + where?: InputMaybe; +}; + +/** [See type definition](https://app.contentful.com/spaces/5v6w5j61tndm/content_types/complianceReports) */ +export type ComplianceReportsUrlArgs = { + locale?: InputMaybe; +}; + +export type ComplianceReportsCollection = { + items: Array>; + limit: Scalars['Int']; + skip: Scalars['Int']; + total: Scalars['Int']; +}; + +export type ComplianceReportsFilter = { + AND?: InputMaybe>>; + OR?: InputMaybe>>; + contentfulMetadata?: InputMaybe; + description?: InputMaybe; + description_contains?: InputMaybe; + description_exists?: InputMaybe; + description_in?: InputMaybe>>; + description_not?: InputMaybe; + description_not_contains?: InputMaybe; + description_not_in?: InputMaybe>>; + manuscriptVersion?: InputMaybe; + manuscriptVersion_exists?: InputMaybe; + sys?: InputMaybe; + url?: InputMaybe; + url_contains?: InputMaybe; + url_exists?: InputMaybe; + url_in?: InputMaybe>>; + url_not?: InputMaybe; + url_not_contains?: InputMaybe; + url_not_in?: InputMaybe>>; +}; + +export type ComplianceReportsLinkingCollections = { + entryCollection?: Maybe; +}; + +export type ComplianceReportsLinkingCollectionsEntryCollectionArgs = { + limit?: InputMaybe; + locale?: InputMaybe; + preview?: InputMaybe; + skip?: InputMaybe; +}; + +export enum ComplianceReportsOrder { + SysFirstPublishedAtAsc = 'sys_firstPublishedAt_ASC', + SysFirstPublishedAtDesc = 'sys_firstPublishedAt_DESC', + SysIdAsc = 'sys_id_ASC', + SysIdDesc = 'sys_id_DESC', + SysPublishedAtAsc = 'sys_publishedAt_ASC', + SysPublishedAtDesc = 'sys_publishedAt_DESC', + SysPublishedVersionAsc = 'sys_publishedVersion_ASC', + SysPublishedVersionDesc = 'sys_publishedVersion_DESC', + UrlAsc = 'url_ASC', + UrlDesc = 'url_DESC', +} + export type ContentfulMetadata = { tags: Array>; }; @@ -4160,10 +4248,24 @@ export enum ManuscriptVersionsLabsCollectionOrder { } export type ManuscriptVersionsLinkingCollections = { + complianceReportsCollection?: Maybe; entryCollection?: Maybe; manuscriptsCollection?: Maybe; }; +export type ManuscriptVersionsLinkingCollectionsComplianceReportsCollectionArgs = + { + limit?: InputMaybe; + locale?: InputMaybe; + order?: InputMaybe< + Array< + InputMaybe + > + >; + preview?: InputMaybe; + skip?: InputMaybe; + }; + export type ManuscriptVersionsLinkingCollectionsEntryCollectionArgs = { limit?: InputMaybe; locale?: InputMaybe; @@ -4183,6 +4285,19 @@ export type ManuscriptVersionsLinkingCollectionsManuscriptsCollectionArgs = { skip?: InputMaybe; }; +export enum ManuscriptVersionsLinkingCollectionsComplianceReportsCollectionOrder { + SysFirstPublishedAtAsc = 'sys_firstPublishedAt_ASC', + SysFirstPublishedAtDesc = 'sys_firstPublishedAt_DESC', + SysIdAsc = 'sys_id_ASC', + SysIdDesc = 'sys_id_DESC', + SysPublishedAtAsc = 'sys_publishedAt_ASC', + SysPublishedAtDesc = 'sys_publishedAt_DESC', + SysPublishedVersionAsc = 'sys_publishedVersion_ASC', + SysPublishedVersionDesc = 'sys_publishedVersion_DESC', + UrlAsc = 'url_ASC', + UrlDesc = 'url_DESC', +} + export enum ManuscriptVersionsLinkingCollectionsManuscriptsCollectionOrder { SysFirstPublishedAtAsc = 'sys_firstPublishedAt_ASC', SysFirstPublishedAtDesc = 'sys_firstPublishedAt_DESC', @@ -5057,6 +5172,8 @@ export type Query = { assetCollection?: Maybe; calendars?: Maybe; calendarsCollection?: Maybe; + complianceReports?: Maybe; + complianceReportsCollection?: Maybe; dashboard?: Maybe; dashboardCollection?: Maybe; discover?: Maybe; @@ -5175,6 +5292,21 @@ export type QueryCalendarsCollectionArgs = { where?: InputMaybe; }; +export type QueryComplianceReportsArgs = { + id: Scalars['String']; + locale?: InputMaybe; + preview?: InputMaybe; +}; + +export type QueryComplianceReportsCollectionArgs = { + limit?: InputMaybe; + locale?: InputMaybe; + order?: InputMaybe>>; + preview?: InputMaybe; + skip?: InputMaybe; + where?: InputMaybe; +}; + export type QueryDashboardArgs = { id: Scalars['String']; locale?: InputMaybe; @@ -12903,6 +13035,9 @@ export type FetchDashboardQuery = { | ({ __typename: 'Calendars' } & { sys: Pick; }) + | ({ __typename: 'ComplianceReports' } & { + sys: Pick; + }) | ({ __typename: 'Dashboard' } & { sys: Pick; }) @@ -13046,6 +13181,9 @@ export type FetchDashboardQuery = { | ({ __typename: 'Calendars' } & { sys: Pick; }) + | ({ __typename: 'ComplianceReports' } & { + sys: Pick; + }) | ({ __typename: 'Dashboard' } & { sys: Pick; }) @@ -13213,6 +13351,9 @@ export type FetchDiscoverQuery = { sys: Pick; }) | ({ __typename: 'Calendars' } & { sys: Pick }) + | ({ __typename: 'ComplianceReports' } & { + sys: Pick; + }) | ({ __typename: 'Dashboard' } & { sys: Pick }) | ({ __typename: 'Discover' } & { sys: Pick }) | ({ __typename: 'EventSpeakers' } & { @@ -13381,6 +13522,7 @@ export type EventsContentFragment = Pick< Maybe< | ({ __typename: 'Announcements' } & { sys: Pick }) | ({ __typename: 'Calendars' } & { sys: Pick }) + | ({ __typename: 'ComplianceReports' } & { sys: Pick }) | ({ __typename: 'Dashboard' } & { sys: Pick }) | ({ __typename: 'Discover' } & { sys: Pick }) | ({ __typename: 'EventSpeakers' } & { sys: Pick }) @@ -13451,6 +13593,7 @@ export type EventsContentFragment = Pick< Maybe< | ({ __typename: 'Announcements' } & { sys: Pick }) | ({ __typename: 'Calendars' } & { sys: Pick }) + | ({ __typename: 'ComplianceReports' } & { sys: Pick }) | ({ __typename: 'Dashboard' } & { sys: Pick }) | ({ __typename: 'Discover' } & { sys: Pick }) | ({ __typename: 'EventSpeakers' } & { sys: Pick }) @@ -13521,6 +13664,7 @@ export type EventsContentFragment = Pick< Maybe< | ({ __typename: 'Announcements' } & { sys: Pick }) | ({ __typename: 'Calendars' } & { sys: Pick }) + | ({ __typename: 'ComplianceReports' } & { sys: Pick }) | ({ __typename: 'Dashboard' } & { sys: Pick }) | ({ __typename: 'Discover' } & { sys: Pick }) | ({ __typename: 'EventSpeakers' } & { sys: Pick }) @@ -13708,6 +13852,9 @@ export type FetchEventByIdQuery = { Maybe< | ({ __typename: 'Announcements' } & { sys: Pick }) | ({ __typename: 'Calendars' } & { sys: Pick }) + | ({ __typename: 'ComplianceReports' } & { + sys: Pick; + }) | ({ __typename: 'Dashboard' } & { sys: Pick }) | ({ __typename: 'Discover' } & { sys: Pick }) | ({ __typename: 'EventSpeakers' } & { sys: Pick }) @@ -13790,6 +13937,9 @@ export type FetchEventByIdQuery = { Maybe< | ({ __typename: 'Announcements' } & { sys: Pick }) | ({ __typename: 'Calendars' } & { sys: Pick }) + | ({ __typename: 'ComplianceReports' } & { + sys: Pick; + }) | ({ __typename: 'Dashboard' } & { sys: Pick }) | ({ __typename: 'Discover' } & { sys: Pick }) | ({ __typename: 'EventSpeakers' } & { sys: Pick }) @@ -13872,6 +14022,9 @@ export type FetchEventByIdQuery = { Maybe< | ({ __typename: 'Announcements' } & { sys: Pick }) | ({ __typename: 'Calendars' } & { sys: Pick }) + | ({ __typename: 'ComplianceReports' } & { + sys: Pick; + }) | ({ __typename: 'Dashboard' } & { sys: Pick }) | ({ __typename: 'Discover' } & { sys: Pick }) | ({ __typename: 'EventSpeakers' } & { sys: Pick }) @@ -14090,6 +14243,9 @@ export type FetchEventsQuery = { | ({ __typename: 'Calendars' } & { sys: Pick; }) + | ({ __typename: 'ComplianceReports' } & { + sys: Pick; + }) | ({ __typename: 'Dashboard' } & { sys: Pick; }) @@ -14202,6 +14358,9 @@ export type FetchEventsQuery = { | ({ __typename: 'Calendars' } & { sys: Pick; }) + | ({ __typename: 'ComplianceReports' } & { + sys: Pick; + }) | ({ __typename: 'Dashboard' } & { sys: Pick; }) @@ -14314,6 +14473,9 @@ export type FetchEventsQuery = { | ({ __typename: 'Calendars' } & { sys: Pick; }) + | ({ __typename: 'ComplianceReports' } & { + sys: Pick; + }) | ({ __typename: 'Dashboard' } & { sys: Pick; }) @@ -14577,6 +14739,9 @@ export type FetchEventsByUserIdQuery = { | ({ __typename: 'Calendars' } & { sys: Pick; }) + | ({ __typename: 'ComplianceReports' } & { + sys: Pick; + }) | ({ __typename: 'Dashboard' } & { sys: Pick; }) @@ -14706,6 +14871,9 @@ export type FetchEventsByUserIdQuery = { | ({ __typename: 'Calendars' } & { sys: Pick; }) + | ({ __typename: 'ComplianceReports' } & { + sys: Pick; + }) | ({ __typename: 'Dashboard' } & { sys: Pick; }) @@ -14835,6 +15003,9 @@ export type FetchEventsByUserIdQuery = { | ({ __typename: 'Calendars' } & { sys: Pick; }) + | ({ __typename: 'ComplianceReports' } & { + sys: Pick; + }) | ({ __typename: 'Dashboard' } & { sys: Pick; }) @@ -15140,6 +15311,9 @@ export type FetchEventsByExternalAuthorIdQuery = { | ({ __typename: 'Calendars' } & { sys: Pick; }) + | ({ __typename: 'ComplianceReports' } & { + sys: Pick; + }) | ({ __typename: 'Dashboard' } & { sys: Pick; }) @@ -15269,6 +15443,9 @@ export type FetchEventsByExternalAuthorIdQuery = { | ({ __typename: 'Calendars' } & { sys: Pick; }) + | ({ __typename: 'ComplianceReports' } & { + sys: Pick; + }) | ({ __typename: 'Dashboard' } & { sys: Pick; }) @@ -15398,6 +15575,9 @@ export type FetchEventsByExternalAuthorIdQuery = { | ({ __typename: 'Calendars' } & { sys: Pick; }) + | ({ __typename: 'ComplianceReports' } & { + sys: Pick; + }) | ({ __typename: 'Dashboard' } & { sys: Pick; }) @@ -15703,6 +15883,9 @@ export type FetchEventsByTeamIdQuery = { | ({ __typename: 'Calendars' } & { sys: Pick; }) + | ({ __typename: 'ComplianceReports' } & { + sys: Pick; + }) | ({ __typename: 'Dashboard' } & { sys: Pick; }) @@ -15832,6 +16015,9 @@ export type FetchEventsByTeamIdQuery = { | ({ __typename: 'Calendars' } & { sys: Pick; }) + | ({ __typename: 'ComplianceReports' } & { + sys: Pick; + }) | ({ __typename: 'Dashboard' } & { sys: Pick; }) @@ -15961,6 +16147,9 @@ export type FetchEventsByTeamIdQuery = { | ({ __typename: 'Calendars' } & { sys: Pick; }) + | ({ __typename: 'ComplianceReports' } & { + sys: Pick; + }) | ({ __typename: 'Dashboard' } & { sys: Pick; }) @@ -16756,6 +16945,13 @@ export type ManuscriptsContentFragment = Pick & { }>; } >; + linkedFrom?: Maybe<{ + complianceReportsCollection?: Maybe<{ + items: Array< + Maybe> + >; + }>; + }>; } > >; @@ -16847,6 +17043,13 @@ export type FetchManuscriptByIdQuery = { }>; } >; + linkedFrom?: Maybe<{ + complianceReportsCollection?: Maybe<{ + items: Array< + Maybe> + >; + }>; + }>; } > >; @@ -16869,6 +17072,7 @@ export type NewsContentFragment = Pick< Maybe< | ({ __typename: 'Announcements' } & { sys: Pick }) | ({ __typename: 'Calendars' } & { sys: Pick }) + | ({ __typename: 'ComplianceReports' } & { sys: Pick }) | ({ __typename: 'Dashboard' } & { sys: Pick }) | ({ __typename: 'Discover' } & { sys: Pick }) | ({ __typename: 'EventSpeakers' } & { sys: Pick }) @@ -16954,6 +17158,9 @@ export type FetchNewsByIdQuery = { Maybe< | ({ __typename: 'Announcements' } & { sys: Pick }) | ({ __typename: 'Calendars' } & { sys: Pick }) + | ({ __typename: 'ComplianceReports' } & { + sys: Pick; + }) | ({ __typename: 'Dashboard' } & { sys: Pick }) | ({ __typename: 'Discover' } & { sys: Pick }) | ({ __typename: 'EventSpeakers' } & { sys: Pick }) @@ -17070,6 +17277,9 @@ export type FetchNewsQuery = { | ({ __typename: 'Calendars' } & { sys: Pick; }) + | ({ __typename: 'ComplianceReports' } & { + sys: Pick; + }) | ({ __typename: 'Dashboard' } & { sys: Pick; }) @@ -17193,6 +17403,7 @@ export type PageContentFragment = Pick< Maybe< | ({ __typename: 'Announcements' } & { sys: Pick }) | ({ __typename: 'Calendars' } & { sys: Pick }) + | ({ __typename: 'ComplianceReports' } & { sys: Pick }) | ({ __typename: 'Dashboard' } & { sys: Pick }) | ({ __typename: 'Discover' } & { sys: Pick }) | ({ __typename: 'EventSpeakers' } & { sys: Pick }) @@ -17280,6 +17491,9 @@ export type FetchPagesQuery = { | ({ __typename: 'Calendars' } & { sys: Pick; }) + | ({ __typename: 'ComplianceReports' } & { + sys: Pick; + }) | ({ __typename: 'Dashboard' } & { sys: Pick; }) @@ -17595,6 +17809,7 @@ export type ResearchOutputsContentFragment = Pick< Maybe< | ({ __typename: 'Announcements' } & { sys: Pick }) | ({ __typename: 'Calendars' } & { sys: Pick }) + | ({ __typename: 'ComplianceReports' } & { sys: Pick }) | ({ __typename: 'Dashboard' } & { sys: Pick }) | ({ __typename: 'Discover' } & { sys: Pick }) | ({ __typename: 'EventSpeakers' } & { sys: Pick }) @@ -17822,6 +18037,9 @@ export type FetchResearchOutputByIdQuery = { Maybe< | ({ __typename: 'Announcements' } & { sys: Pick }) | ({ __typename: 'Calendars' } & { sys: Pick }) + | ({ __typename: 'ComplianceReports' } & { + sys: Pick; + }) | ({ __typename: 'Dashboard' } & { sys: Pick }) | ({ __typename: 'Discover' } & { sys: Pick }) | ({ __typename: 'EventSpeakers' } & { sys: Pick }) @@ -18079,6 +18297,9 @@ export type FetchResearchOutputsQuery = { | ({ __typename: 'Calendars' } & { sys: Pick; }) + | ({ __typename: 'ComplianceReports' } & { + sys: Pick; + }) | ({ __typename: 'Dashboard' } & { sys: Pick; }) @@ -18488,6 +18709,15 @@ export type FetchTeamByIdQuery = { }>; } >; + linkedFrom?: Maybe<{ + complianceReportsCollection?: Maybe<{ + items: Array< + Maybe< + Pick + > + >; + }>; + }>; } > >; @@ -18609,6 +18839,7 @@ export type TutorialsContentFragment = Pick< Maybe< | ({ __typename: 'Announcements' } & { sys: Pick }) | ({ __typename: 'Calendars' } & { sys: Pick }) + | ({ __typename: 'ComplianceReports' } & { sys: Pick }) | ({ __typename: 'Dashboard' } & { sys: Pick }) | ({ __typename: 'Discover' } & { sys: Pick }) | ({ __typename: 'EventSpeakers' } & { sys: Pick }) @@ -18743,6 +18974,9 @@ export type FetchTutorialByIdQuery = { Maybe< | ({ __typename: 'Announcements' } & { sys: Pick }) | ({ __typename: 'Calendars' } & { sys: Pick }) + | ({ __typename: 'ComplianceReports' } & { + sys: Pick; + }) | ({ __typename: 'Dashboard' } & { sys: Pick }) | ({ __typename: 'Discover' } & { sys: Pick }) | ({ __typename: 'EventSpeakers' } & { sys: Pick }) @@ -18908,6 +19142,9 @@ export type FetchTutorialsQuery = { | ({ __typename: 'Calendars' } & { sys: Pick; }) + | ({ __typename: 'ComplianceReports' } & { + sys: Pick; + }) | ({ __typename: 'Dashboard' } & { sys: Pick; }) @@ -19825,6 +20062,7 @@ export type WorkingGroupsContentFragment = Pick< Maybe< | ({ __typename: 'Announcements' } & { sys: Pick }) | ({ __typename: 'Calendars' } & { sys: Pick }) + | ({ __typename: 'ComplianceReports' } & { sys: Pick }) | ({ __typename: 'Dashboard' } & { sys: Pick }) | ({ __typename: 'Discover' } & { sys: Pick }) | ({ __typename: 'EventSpeakers' } & { sys: Pick }) @@ -19988,6 +20226,9 @@ export type FetchWorkingGroupByIdQuery = { Maybe< | ({ __typename: 'Announcements' } & { sys: Pick }) | ({ __typename: 'Calendars' } & { sys: Pick }) + | ({ __typename: 'ComplianceReports' } & { + sys: Pick; + }) | ({ __typename: 'Dashboard' } & { sys: Pick }) | ({ __typename: 'Discover' } & { sys: Pick }) | ({ __typename: 'EventSpeakers' } & { sys: Pick }) @@ -20179,6 +20420,9 @@ export type FetchWorkingGroupsQuery = { | ({ __typename: 'Calendars' } & { sys: Pick; }) + | ({ __typename: 'ComplianceReports' } & { + sys: Pick; + }) | ({ __typename: 'Dashboard' } & { sys: Pick; }) @@ -22557,6 +22801,54 @@ export const ManuscriptsContentFragmentDoc = { ], }, }, + { + kind: 'Field', + name: { kind: 'Name', value: 'linkedFrom' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { + kind: 'Name', + value: 'complianceReportsCollection', + }, + arguments: [ + { + kind: 'Argument', + name: { kind: 'Name', value: 'limit' }, + value: { kind: 'IntValue', value: '1' }, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'items' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'url' }, + }, + { + kind: 'Field', + name: { + kind: 'Name', + value: 'description', + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, ], }, }, diff --git a/packages/contentful/src/crn/queries/manuscript.queries.ts b/packages/contentful/src/crn/queries/manuscript.queries.ts index 7ab2ebf8d6..6ea71187d1 100644 --- a/packages/contentful/src/crn/queries/manuscript.queries.ts +++ b/packages/contentful/src/crn/queries/manuscript.queries.ts @@ -99,6 +99,14 @@ export const manuscriptContentQueryFragment = gql` } } } + linkedFrom { + complianceReportsCollection(limit: 1) { + items { + url + description + } + } + } } } } diff --git a/packages/contentful/src/crn/schema/autogenerated-schema.graphql b/packages/contentful/src/crn/schema/autogenerated-schema.graphql index 7a47e2efb9..458a2975a1 100644 --- a/packages/contentful/src/crn/schema/autogenerated-schema.graphql +++ b/packages/contentful/src/crn/schema/autogenerated-schema.graphql @@ -371,6 +371,64 @@ enum CalendarsOrder { sys_publishedVersion_DESC } +"""[See type definition](https://app.contentful.com/spaces/5v6w5j61tndm/content_types/complianceReports)""" +type ComplianceReports implements Entry & _Node { + _id: ID! + contentfulMetadata: ContentfulMetadata! + description(locale: String): String + linkedFrom(allowedLocales: [String]): ComplianceReportsLinkingCollections + manuscriptVersion(locale: String, preview: Boolean, where: ManuscriptVersionsFilter): ManuscriptVersions + sys: Sys! + url(locale: String): String +} + +type ComplianceReportsCollection { + items: [ComplianceReports]! + limit: Int! + skip: Int! + total: Int! +} + +input ComplianceReportsFilter { + AND: [ComplianceReportsFilter] + OR: [ComplianceReportsFilter] + contentfulMetadata: ContentfulMetadataFilter + description: String + description_contains: String + description_exists: Boolean + description_in: [String] + description_not: String + description_not_contains: String + description_not_in: [String] + manuscriptVersion: cfManuscriptVersionsNestedFilter + manuscriptVersion_exists: Boolean + sys: SysFilter + url: String + url_contains: String + url_exists: Boolean + url_in: [String] + url_not: String + url_not_contains: String + url_not_in: [String] +} + +type ComplianceReportsLinkingCollections { + entryCollection(limit: Int = 100, locale: String, preview: Boolean, skip: Int = 0): EntryCollection +} + +enum ComplianceReportsOrder { + sys_firstPublishedAt_ASC + sys_firstPublishedAt_DESC + sys_id_ASC + sys_id_DESC + sys_publishedAt_ASC + sys_publishedAt_DESC + sys_publishedVersion_ASC + sys_publishedVersion_DESC + url_ASC + url_DESC +} + type ContentfulMetadata { tags: [ContentfulTag]! } @@ -2945,10 +3003,24 @@ enum ManuscriptVersionsLabsCollectionOrder { } type ManuscriptVersionsLinkingCollections { + complianceReportsCollection(limit: Int = 100, locale: String, order: [ManuscriptVersionsLinkingCollectionsComplianceReportsCollectionOrder], preview: Boolean, skip: Int = 0): ComplianceReportsCollection entryCollection(limit: Int = 100, locale: String, preview: Boolean, skip: Int = 0): EntryCollection manuscriptsCollection(limit: Int = 100, locale: String, order: [ManuscriptVersionsLinkingCollectionsManuscriptsCollectionOrder], preview: Boolean, skip: Int = 0): ManuscriptsCollection } +enum ManuscriptVersionsLinkingCollectionsComplianceReportsCollectionOrder { + sys_firstPublishedAt_ASC + sys_firstPublishedAt_DESC + sys_id_ASC + sys_id_DESC + sys_publishedAt_ASC + sys_publishedAt_DESC + sys_publishedVersion_ASC + sys_publishedVersion_DESC + url_ASC + url_DESC +} + enum ManuscriptVersionsLinkingCollectionsManuscriptsCollectionOrder { sys_firstPublishedAt_ASC sys_firstPublishedAt_DESC @@ -3609,6 +3681,8 @@ type Query { assetCollection(limit: Int = 100, locale: String, order: [AssetOrder], preview: Boolean, skip: Int = 0, where: AssetFilter): AssetCollection calendars(id: String!, locale: String, preview: Boolean): Calendars calendarsCollection(limit: Int = 100, locale: String, order: [CalendarsOrder], preview: Boolean, skip: Int = 0, where: CalendarsFilter): CalendarsCollection + complianceReports(id: String!, locale: String, preview: Boolean): ComplianceReports + complianceReportsCollection(limit: Int = 100, locale: String, order: [ComplianceReportsOrder], preview: Boolean, skip: Int = 0, where: ComplianceReportsFilter): ComplianceReportsCollection dashboard(id: String!, locale: String, preview: Boolean): Dashboard dashboardCollection(limit: Int = 100, locale: String, order: [DashboardOrder], preview: Boolean, skip: Int = 0, where: DashboardFilter): DashboardCollection discover(id: String!, locale: String, preview: Boolean): Discover diff --git a/packages/model/src/compliance-report.ts b/packages/model/src/compliance-report.ts index 1f2b57fb2e..4e64b75af6 100644 --- a/packages/model/src/compliance-report.ts +++ b/packages/model/src/compliance-report.ts @@ -1,4 +1,10 @@ -export type ComplianceReport = { +export type ComplianceReportDataObject = { url: string; description: string; }; +export type ComplianceReportResponse = ComplianceReportDataObject; +export type ComplianceReportCreateDataObject = ComplianceReportDataObject & { + manuscriptVersionId: string; +}; +export type ComplianceReportPostRequest = ComplianceReportCreateDataObject; +export type ComplianceReportFormData = ComplianceReportDataObject; diff --git a/packages/model/src/index.ts b/packages/model/src/index.ts index bfde0fb8fc..83127b23cc 100644 --- a/packages/model/src/index.ts +++ b/packages/model/src/index.ts @@ -3,6 +3,7 @@ export * from './authors'; export * from './calendar'; export * from './calendar-common'; export * from './common'; +export * from './compliance-report'; export * from './controllers'; export * from './dashboard'; export * from './data-providers'; diff --git a/packages/model/src/manuscript.ts b/packages/model/src/manuscript.ts index 28da982fa1..c984ec18de 100644 --- a/packages/model/src/manuscript.ts +++ b/packages/model/src/manuscript.ts @@ -1,6 +1,6 @@ import { JSONSchemaType } from 'ajv'; import { AuthorAlgoliaResponse } from './authors'; -import { ComplianceReport } from './compliance-report'; +import { ComplianceReportDataObject } from './compliance-report'; import { UserResponse } from './user'; export const manuscriptTypes = [ @@ -66,6 +66,7 @@ const manuscriptFileTypes = [ export type ManuscriptFileType = (typeof manuscriptFileTypes)[number]; export type ManuscriptVersion = { + id: string; type: ManuscriptType; lifecycle: ManuscriptLifecycle; preprintDoi?: string; @@ -110,7 +111,7 @@ export type ManuscriptVersion = { }; createdDate: string; publishedAt: string; - complianceReport?: ComplianceReport; + complianceReport?: ComplianceReportDataObject; }; export const manuscriptFormFieldsMapping: Record< @@ -120,7 +121,7 @@ export const manuscriptFormFieldsMapping: Record< Array< keyof Omit< ManuscriptVersion, - 'complianceReport' | 'createdBy' | 'createdDate' | 'publishedAt' + 'complianceReport' | 'createdBy' | 'createdDate' | 'id' | 'publishedAt' > > > From eec739c027f60315bf1d5889304221004bfc32bc Mon Sep 17 00:00:00 2001 From: AkosuaA Date: Thu, 10 Oct 2024 12:50:26 +0100 Subject: [PATCH 04/17] frontend updates --- .../network/teams/ManuscriptToastProvider.tsx | 21 +- .../network/teams/TeamComplianceReport.tsx | 60 ++++++ .../src/network/teams/TeamManuscript.tsx | 3 +- .../src/network/teams/TeamProfile.tsx | 17 +- apps/crn-frontend/src/network/teams/api.ts | 26 +++ apps/crn-frontend/src/network/teams/state.ts | 39 +++- .../src/icons/compliance-report.tsx | 22 ++ packages/react-components/src/icons/index.ts | 1 + packages/react-components/src/index.ts | 2 + .../src/organisms/ComplianceReportCard.tsx | 80 ++++++++ .../src/organisms/ComplianceReportHeader.tsx | 41 ++++ .../src/organisms/ManuscriptCard.tsx | 52 ++++- .../src/organisms/ManuscriptVersionCard.tsx | 4 + .../react-components/src/organisms/index.ts | 1 + .../src/templates/ComplianceReportForm.tsx | 188 ++++++++++++++++++ .../src/templates/ManuscriptForm.tsx | 2 + .../src/templates/TeamProfileWorkspace.tsx | 2 +- .../react-components/src/templates/index.ts | 1 + packages/routing/src/network.ts | 11 +- 19 files changed, 562 insertions(+), 11 deletions(-) create mode 100644 apps/crn-frontend/src/network/teams/TeamComplianceReport.tsx create mode 100644 packages/react-components/src/icons/compliance-report.tsx create mode 100644 packages/react-components/src/organisms/ComplianceReportCard.tsx create mode 100644 packages/react-components/src/organisms/ComplianceReportHeader.tsx create mode 100644 packages/react-components/src/templates/ComplianceReportForm.tsx diff --git a/apps/crn-frontend/src/network/teams/ManuscriptToastProvider.tsx b/apps/crn-frontend/src/network/teams/ManuscriptToastProvider.tsx index babeb1bec6..fb3dded560 100644 --- a/apps/crn-frontend/src/network/teams/ManuscriptToastProvider.tsx +++ b/apps/crn-frontend/src/network/teams/ManuscriptToastProvider.tsx @@ -1,8 +1,13 @@ import { Toast } from '@asap-hub/react-components'; import React, { createContext, useState } from 'react'; +type ComplianceFormType = 'manuscript' | 'compliance-report' | ''; + type ManuscriptToastContextData = { setShowSuccessBanner: React.Dispatch>; + setComplianceFormType: React.Dispatch< + React.SetStateAction + >; }; export const ManuscriptToastContext = createContext( @@ -15,16 +20,26 @@ export const ManuscriptToastProvider = ({ children: React.ReactNode; }) => { const [showSuccessBanner, setShowSuccessBanner] = useState(false); + const [complianceFormType, setComplianceFormType] = + useState(''); + + const complianceFormTypeMapping = { + manuscript: 'Manuscript', + 'compliance-report': 'Compliance Report', + }; return ( - + <> - {showSuccessBanner && ( + {showSuccessBanner && !!complianceFormType && ( setShowSuccessBanner(false)} > - Manuscript submitted successfully. + {complianceFormTypeMapping[complianceFormType]} submitted + successfully. )} {children} diff --git a/apps/crn-frontend/src/network/teams/TeamComplianceReport.tsx b/apps/crn-frontend/src/network/teams/TeamComplianceReport.tsx new file mode 100644 index 0000000000..97237e6d46 --- /dev/null +++ b/apps/crn-frontend/src/network/teams/TeamComplianceReport.tsx @@ -0,0 +1,60 @@ +import { Frame } from '@asap-hub/frontend-utils'; +import { + ComplianceReportForm, + ComplianceReportHeader, + NotFoundPage, + usePushFromHere, +} from '@asap-hub/react-components'; +import { network } from '@asap-hub/routing'; +import { FormProvider, useForm } from 'react-hook-form'; +import { useParams } from 'react-router-dom'; +import { useSetRecoilState } from 'recoil'; +import { + refreshTeamState, + useManuscriptById, + usePostComplianceReport, +} from './state'; +import { useManuscriptToast } from './useManuscriptToast'; + +type TeamComplianceReportProps = { + teamId: string; +}; +const TeamComplianceReport: React.FC = ({ + teamId, +}) => { + const { manuscriptId } = useParams<{ manuscriptId: string }>(); + const manuscript = useManuscriptById(manuscriptId); + const { setShowSuccessBanner, setComplianceFormType } = useManuscriptToast(); + + const pushFromHere = usePushFromHere(); + + const setRefreshTeamState = useSetRecoilState(refreshTeamState(teamId)); + const form = useForm(); + const createComplianceReport = usePostComplianceReport(); + + if (manuscript && manuscript.versions[0]) { + const onSuccess = () => { + const path = network({}).teams({}).team({ teamId }).workspace({}).$; + setShowSuccessBanner(true); + setComplianceFormType('compliance-report'); + setRefreshTeamState((value) => value + 1); + pushFromHere(path); + }; + + return ( + + + + + + + ); + } + return ; +}; +export default TeamComplianceReport; diff --git a/apps/crn-frontend/src/network/teams/TeamManuscript.tsx b/apps/crn-frontend/src/network/teams/TeamManuscript.tsx index 8cdacbef4a..edbfdb2607 100644 --- a/apps/crn-frontend/src/network/teams/TeamManuscript.tsx +++ b/apps/crn-frontend/src/network/teams/TeamManuscript.tsx @@ -30,7 +30,7 @@ const TeamManuscript: React.FC = ({ teamId }) => { const team = useTeamById(teamId); const { eligibilityReasons } = useEligibilityReason(); - const { setShowSuccessBanner } = useManuscriptToast(); + const { setShowSuccessBanner, setComplianceFormType } = useManuscriptToast(); const form = useForm(); const createManuscript = usePostManuscript(); const handleFileUpload = useUploadManuscriptFile(); @@ -43,6 +43,7 @@ const TeamManuscript: React.FC = ({ teamId }) => { const onSuccess = () => { const path = network({}).teams({}).team({ teamId }).workspace({}).$; setShowSuccessBanner(true); + setComplianceFormType('manuscript'); setRefreshTeamState((value) => value + 1); pushFromHere(path); }; diff --git a/apps/crn-frontend/src/network/teams/TeamProfile.tsx b/apps/crn-frontend/src/network/teams/TeamProfile.tsx index 65acb9cd83..a8fe0f5251 100644 --- a/apps/crn-frontend/src/network/teams/TeamProfile.tsx +++ b/apps/crn-frontend/src/network/teams/TeamProfile.tsx @@ -19,9 +19,10 @@ import { useUpcomingAndPastEvents } from '../events'; import ProfileSwitch from '../ProfileSwitch'; import { ManuscriptToastProvider } from './ManuscriptToastProvider'; -import { useTeamById } from './state'; +import { useCanCreateComplianceReport, useTeamById } from './state'; import TeamManuscript from './TeamManuscript'; import { EligibilityReasonProvider } from './EligibilityReasonProvider'; +import TeamComplianceReport from './TeamComplianceReport'; const loadAbout = () => import(/* webpackChunkName: "network-team-about" */ './About'); @@ -89,6 +90,8 @@ const TeamProfile: FC = ({ currentTime }) => { const canDuplicateResearchOutput = useCanDuplicateResearchOutput('teams', [ teamId, ]); + + const canCreateComplianceReport = useCanCreateComplianceReport(); const [upcomingEvents, pastEvents] = useUpcomingAndPastEvents(currentTime, { teamId, }); @@ -147,6 +150,18 @@ const TeamProfile: FC = ({ currentTime }) => { + {canCreateComplianceReport && ( + + + + + + )} {canShareResearchOutput && ( diff --git a/apps/crn-frontend/src/network/teams/api.ts b/apps/crn-frontend/src/network/teams/api.ts index 695ab554ff..e3c33145bb 100644 --- a/apps/crn-frontend/src/network/teams/api.ts +++ b/apps/crn-frontend/src/network/teams/api.ts @@ -4,6 +4,8 @@ import { GetListOptions, } from '@asap-hub/frontend-utils'; import { + ComplianceReportPostRequest, + ComplianceReportResponse, ListLabsResponse, ListTeamResponse, ManuscriptFileResponse, @@ -235,3 +237,27 @@ export const uploadManuscriptFile = async ( return resp.json(); }; + +export const createComplianceReport = async ( + complianceReport: ComplianceReportPostRequest, + authorization: string, +): Promise => { + const resp = await fetch(`${API_BASE_URL}/compliance-reports`, { + method: 'POST', + headers: { + authorization, + 'content-type': 'application/json', + ...createSentryHeaders(), + }, + body: JSON.stringify(complianceReport), + }); + const response = await resp.json(); + if (!resp.ok) { + throw new BackendError( + `Failed to create compliance report. Expected status 201. Received status ${`${resp.status} ${resp.statusText}`.trim()}.`, + response, + resp.status, + ); + } + return response; +}; diff --git a/apps/crn-frontend/src/network/teams/state.ts b/apps/crn-frontend/src/network/teams/state.ts index 4671efb433..e67654b4fd 100644 --- a/apps/crn-frontend/src/network/teams/state.ts +++ b/apps/crn-frontend/src/network/teams/state.ts @@ -7,7 +7,9 @@ import { ManuscriptPostRequest, ManuscriptResponse, ManuscriptFileType, + ComplianceReportPostRequest, } from '@asap-hub/model'; +import { useCurrentUserCRN } from '@asap-hub/react-context'; import { atom, atomFamily, @@ -22,7 +24,9 @@ import useDeepCompareEffect from 'use-deep-compare-effect'; import { authorizationState } from '../../auth/state'; import { CARD_VIEW_PAGE_SIZE } from '../../hooks'; import { + createComplianceReport, createManuscript, + getManuscript, getTeam, getTeams, patchTeam, @@ -154,14 +158,31 @@ export const refreshManuscriptState = atomFamily({ default: 0, }); +const fetchManuscriptState = selectorFamily< + ManuscriptResponse | undefined, + string +>({ + key: 'fetchManuscript', + get: + (id) => + ({ get }) => { + get(refreshManuscriptState(id)); + const authorization = get(authorizationState); + return getManuscript(id, authorization); + }, +}); + export const manuscriptState = atomFamily< ManuscriptResponse | undefined, string >({ key: 'manuscript', - default: undefined, + default: fetchManuscriptState, }); +export const useManuscriptById = (id: string) => + useRecoilValue(manuscriptState(id)); + export const useSetManuscriptItem = () => { const [refresh, setRefresh] = useRecoilState(refreshManuscriptIndex); return useRecoilCallback(({ set }) => (manuscript: ManuscriptResponse) => { @@ -180,6 +201,22 @@ export const usePostManuscript = () => { }; }; +export const usePostComplianceReport = () => { + const authorization = useRecoilValue(authorizationState); + return async (payload: ComplianceReportPostRequest) => { + const complianceReport = await createComplianceReport( + payload, + authorization, + ); + return complianceReport; + }; +}; + +export const useCanCreateComplianceReport = (): boolean => { + const { role } = useCurrentUserCRN() ?? {}; + return role === 'Staff'; +}; + export const useUploadManuscriptFile = () => { const authorization = useRecoilValue(authorizationState); diff --git a/packages/react-components/src/icons/compliance-report.tsx b/packages/react-components/src/icons/compliance-report.tsx new file mode 100644 index 0000000000..df3dec0ee3 --- /dev/null +++ b/packages/react-components/src/icons/compliance-report.tsx @@ -0,0 +1,22 @@ +/* istanbul ignore file */ + +const complianceReportIcon = ( + + Submit Compliance Report Icon + + + +); + +export default complianceReportIcon; diff --git a/packages/react-components/src/icons/index.ts b/packages/react-components/src/icons/index.ts index 547585b1bd..7491e373d8 100644 --- a/packages/react-components/src/icons/index.ts +++ b/packages/react-components/src/icons/index.ts @@ -23,6 +23,7 @@ export { default as chevronLeftIcon } from './chevron-left'; export { default as chevronRightIcon } from './chevron-right'; export { default as chevronUpIcon } from './chevron-up'; export { default as clockIcon } from './clock'; +export { default as complianceReportIcon } from './compliance-report'; export { default as confidentialIcon } from './confidential'; export { default as copyIcon } from './copy-icon'; export { default as crnReportIcon } from './crnReport'; diff --git a/packages/react-components/src/index.ts b/packages/react-components/src/index.ts index acf7cf4e27..d84c21fb32 100644 --- a/packages/react-components/src/index.ts +++ b/packages/react-components/src/index.ts @@ -117,6 +117,7 @@ export { CaptionCard, CheckboxGroup, ComingSoon, + ComplianceReportHeader, ConfirmModal, DashboardRecommendedUsers, DashboardUpcomingEvents, @@ -216,6 +217,7 @@ export { AnalyticsPageHeader, BasicLayout, BiographyModal, + ComplianceReportForm, ContactInfoModal, ContentPage, DashboardPage, diff --git a/packages/react-components/src/organisms/ComplianceReportCard.tsx b/packages/react-components/src/organisms/ComplianceReportCard.tsx new file mode 100644 index 0000000000..0bb0bf3fe7 --- /dev/null +++ b/packages/react-components/src/organisms/ComplianceReportCard.tsx @@ -0,0 +1,80 @@ +import { ComplianceReportResponse } from '@asap-hub/model'; +import { css } from '@emotion/react'; +import { useState } from 'react'; +import { + Button, + minusRectIcon, + plusRectIcon, + Subtitle, + Link, + crnReportIcon, + colors, + externalLinkIcon, +} from '..'; +import { paddingStyles } from '../card'; +import { mobileScreen, perRem, rem } from '../pixels'; + +type ComplianceReportCardProps = ComplianceReportResponse; + +const toastStyles = css({ + padding: `${15 / perRem}em ${24 / perRem}em`, + borderRadius: `${rem(8)} ${rem(8)} 0 0`, +}); + +const iconStyles = css({ + display: 'inline-block', + width: `${24 / perRem}em`, + height: `${24 / perRem}em`, + paddingRight: `${12 / perRem}em`, +}); + +const toastHeaderStyles = css({ + display: 'flex', + alignItems: 'center', + + [`@media (max-width: ${mobileScreen.max}px)`]: { + alignItems: 'flex-start', + }, +}); + +const toastContentStyles = css({ + paddingLeft: `${60 / perRem}em`, + paddingTop: rem(15), +}); + +const ComplianceReportCard: React.FC = ({ + url, + description, +}) => { + const [expanded, setExpanded] = useState(false); + + return ( +
+
+ + + + + {crnReportIcon} + Compliance Report + +
+ {expanded && ( +
+
+
{description}
+
+ + {externalLinkIcon} View Report + +
+
+
+ )} +
+ ); +}; + +export default ComplianceReportCard; diff --git a/packages/react-components/src/organisms/ComplianceReportHeader.tsx b/packages/react-components/src/organisms/ComplianceReportHeader.tsx new file mode 100644 index 0000000000..a63323bac5 --- /dev/null +++ b/packages/react-components/src/organisms/ComplianceReportHeader.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { css } from '@emotion/react'; + +import { Display, Paragraph } from '../atoms'; +import { perRem } from '../pixels'; +import { paper, steel } from '../colors'; +import { contentSidePaddingWithNavigation } from '../layout'; + +const headerStyles = css({ + padding: `${36 / perRem}em ${contentSidePaddingWithNavigation(8)} ${ + 60 / perRem + }em `, + background: paper.rgb, + boxShadow: `0 2px 4px -2px ${steel.rgb}`, + marginBottom: `${30 / perRem}em`, + display: 'flex', + justifyContent: 'center', +}); + +const contentStyles = css({ + display: 'flex', + flexDirection: 'column', + maxWidth: `${800 / perRem}em`, + width: '100%', + justifyContent: 'center', +}); + +const ComplianceReportHeader: React.FC = () => ( +
+
+ Share a Compliance Report +
+ + Share the compliance report associated with this manuscript. + +
+
+
+); + +export default ComplianceReportHeader; diff --git a/packages/react-components/src/organisms/ManuscriptCard.tsx b/packages/react-components/src/organisms/ManuscriptCard.tsx index 2215d0ded0..d50fde39e6 100644 --- a/packages/react-components/src/organisms/ManuscriptCard.tsx +++ b/packages/react-components/src/organisms/ManuscriptCard.tsx @@ -1,11 +1,23 @@ import { TeamManuscript } from '@asap-hub/model'; +import { useCurrentUserCRN } from '@asap-hub/react-context'; +import { network } from '@asap-hub/routing'; import { css } from '@emotion/react'; import { useState } from 'react'; -import { Button, colors, minusRectIcon, plusRectIcon, Subtitle } from '..'; +import { useHistory } from 'react-router-dom'; +import { + Button, + colors, + complianceReportIcon, + minusRectIcon, + plusRectIcon, + Subtitle, +} from '..'; import { mobileScreen, perRem, rem } from '../pixels'; import ManuscriptVersionCard from './ManuscriptVersionCard'; -type ManuscriptCardProps = Pick; +type ManuscriptCardProps = Pick & { + teamId: string; +}; const manuscriptContainerStyles = css({ marginTop: rem(12), @@ -47,8 +59,27 @@ const toastHeaderStyles = css({ }, }); -const ManuscriptCard: React.FC = ({ title, versions }) => { +const ManuscriptCard: React.FC = ({ + id, + title, + versions, + teamId, +}) => { const [expanded, setExpanded] = useState(false); + const history = useHistory(); + + const complianceReportRoute = network({}) + .teams({}) + .team({ teamId }) + .workspace({}) + .createComplianceReport({ manuscriptId: id }).$; + + const handleShareComplianceReport = () => { + history.push(complianceReportRoute); + }; + + const { role } = useCurrentUserCRN() ?? {}; + const hasActiveComplianceReport = !!versions[0]?.complianceReport; return (
@@ -66,6 +97,21 @@ const ManuscriptCard: React.FC = ({ title, versions }) => { {title} + {role === 'Staff' && ( + + + + )}
{expanded && ( diff --git a/packages/react-components/src/organisms/ManuscriptVersionCard.tsx b/packages/react-components/src/organisms/ManuscriptVersionCard.tsx index 116123f906..22fb356bf6 100644 --- a/packages/react-components/src/organisms/ManuscriptVersionCard.tsx +++ b/packages/react-components/src/organisms/ManuscriptVersionCard.tsx @@ -22,6 +22,7 @@ import { UserCommentHeader } from '../molecules'; import ManuscriptFileSection from '../molecules/ManuscriptFileSection'; import UserTeamInfo from '../molecules/UserTeamInfo'; import { mobileScreen, perRem, rem } from '../pixels'; +import ComplianceReportCard from './ComplianceReportCard'; type ManuscriptVersionCardProps = ManuscriptVersion; @@ -124,6 +125,9 @@ const ManuscriptVersionCard: React.FC = ( return (
+ {version.complianceReport && ( + + )}
diff --git a/packages/react-components/src/organisms/index.ts b/packages/react-components/src/organisms/index.ts index 541bccb995..e011a11a9e 100644 --- a/packages/react-components/src/organisms/index.ts +++ b/packages/react-components/src/organisms/index.ts @@ -5,6 +5,7 @@ export { default as CalendarList } from './CalendarList'; export { default as CaptionCard } from './CaptionCard'; export { default as CheckboxGroup } from './CheckboxGroup'; export { default as ComingSoon } from './ComingSoon'; +export { default as ComplianceReportHeader } from './ComplianceReportHeader'; export { default as ConfirmModal } from './ConfirmModal'; export { default as DashboardRecommendedUsers } from './DashboardRecommendedUsers'; export { default as DashboardUpcomingEvents } from './DashboardUpcomingEvents'; diff --git a/packages/react-components/src/templates/ComplianceReportForm.tsx b/packages/react-components/src/templates/ComplianceReportForm.tsx new file mode 100644 index 0000000000..b7e0870da1 --- /dev/null +++ b/packages/react-components/src/templates/ComplianceReportForm.tsx @@ -0,0 +1,188 @@ +import { + ComplianceReportFormData, + ComplianceReportPostRequest, + ComplianceReportResponse, +} from '@asap-hub/model'; +import { urlExpression } from '@asap-hub/validation'; +import { css } from '@emotion/react'; +import React from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import { useHistory } from 'react-router-dom'; +import { GlobeIcon, LabeledTextArea, LabeledTextField } from '..'; +import { Button, Card, Paragraph } from '../atoms'; +import { defaultPageLayoutPaddingStyle } from '../layout'; +import { mobileScreen, rem } from '../pixels'; + +const mainStyles = css({ + display: 'flex', + justifyContent: 'center', + padding: defaultPageLayoutPaddingStyle, +}); + +const cardStyles = css({ + padding: `${rem(32)} ${rem(24)} ${rem(16)}`, +}); + +const contentStyles = css({ + display: 'grid', + gridTemplateColumns: '1fr', + width: '100%', + maxWidth: rem(800), + justifyContent: 'center', + gridAutoFlow: 'row', + rowGap: rem(36), +}); + +const buttonsOuterContainerStyles = css({ + display: 'flex', + justifyContent: 'end', + [`@media (max-width: ${mobileScreen.max}px)`]: { + width: '100%', + }, +}); + +const buttonsInnerContainerStyles = css({ + display: 'flex', + flexDirection: 'row', + gap: rem(24), + [`@media (max-width: ${mobileScreen.max}px)`]: { + flexDirection: 'column-reverse', + width: '100%', + }, +}); + +type ComplianceReportFormProps = { + manuscriptTitle: string; + manuscriptVersionId: string; + url?: string; + description?: string | ''; + onSave: ( + output: ComplianceReportPostRequest, + ) => Promise; + onSuccess: () => void; +}; + +const ComplianceReportForm: React.FC = ({ + onSave, + onSuccess, + manuscriptTitle, + manuscriptVersionId, + url, + description, +}) => { + const history = useHistory(); + + const methods = useForm({ + mode: 'onBlur', + defaultValues: { + url: url || '', + description: description || '', + }, + }); + + const { + handleSubmit, + control, + formState: { isSubmitting }, + } = methods; + + const onSubmit = async (data: ComplianceReportFormData) => { + await onSave({ + ...data, + manuscriptVersionId, + }); + + onSuccess(); + }; + + return ( +
+
+
+ + + Title of Manuscript + + {manuscriptTitle} + ( + } + placeholder="https://example.com" + /> + )} + /> + + ( + + Add a description to the compliance report. You can format + your text by using markup language. + + } + customValidationMessage={error?.message} + value={value || ''} + onChange={onChange} + enabled={!isSubmitting} + /> + )} + /> + +
+
+ + +
+
+
+
+
+ ); +}; + +export default ComplianceReportForm; diff --git a/packages/react-components/src/templates/ManuscriptForm.tsx b/packages/react-components/src/templates/ManuscriptForm.tsx index 965cb6f7a9..6d0337bd33 100644 --- a/packages/react-components/src/templates/ManuscriptForm.tsx +++ b/packages/react-components/src/templates/ManuscriptForm.tsx @@ -84,6 +84,7 @@ const apcCoverageLifecycles = [ type OptionalVersionFields = Array< keyof Omit< ManuscriptVersion, + | 'id' | 'type' | 'lifecycle' | 'complianceReport' @@ -208,6 +209,7 @@ const setDefaultFieldValues = ( type ManuscriptFormProps = Omit< ManuscriptVersion, + | 'id' | 'type' | 'lifecycle' | 'manuscriptFile' diff --git a/packages/react-components/src/templates/TeamProfileWorkspace.tsx b/packages/react-components/src/templates/TeamProfileWorkspace.tsx index f050b2a91a..2508c4a5e1 100644 --- a/packages/react-components/src/templates/TeamProfileWorkspace.tsx +++ b/packages/react-components/src/templates/TeamProfileWorkspace.tsx @@ -136,7 +136,7 @@ const TeamProfileWorkspace: React.FC = ({
{manuscripts.map((manuscript) => (
- +
))} diff --git a/packages/react-components/src/templates/index.ts b/packages/react-components/src/templates/index.ts index acdede996d..3c81a14112 100644 --- a/packages/react-components/src/templates/index.ts +++ b/packages/react-components/src/templates/index.ts @@ -9,6 +9,7 @@ export { default as AnalyticsEngagementPageBody } from './AnalyticsEngagementPag export { default as AnalyticsPageHeader } from './AnalyticsPageHeader'; export { default as BasicLayout } from './BasicLayout'; export { default as BiographyModal } from './BiographyModal'; +export { default as ComplianceReportForm } from './ComplianceReportForm'; export { default as ContactInfoModal } from './ContactInfoModal'; export { default as ContentPage } from './ContentPage'; export { default as DashboardPage } from './DashboardPage'; diff --git a/packages/routing/src/network.ts b/packages/routing/src/network.ts index 636b52dfc6..ead5b330e6 100644 --- a/packages/routing/src/network.ts +++ b/packages/routing/src/network.ts @@ -92,7 +92,16 @@ const team = (() => { const tool = route('/:toolIndex', { toolIndex: stringParser }, {}); const tools = route('/tools', {}, { tool }); const createManuscript = route('/create-manuscript', {}, {}); - const workspace = route('/workspace', {}, { tools, createManuscript }); + const createComplianceReport = route( + '/create-compliance-report/:manuscriptId', + { manuscriptId: stringParser }, + {}, + ); + const workspace = route( + '/workspace', + {}, + { tools, createManuscript, createComplianceReport }, + ); const createOutput = route( '/create-output/:outputDocumentType', { outputDocumentType: outputDocumentTypeParser }, From 663a8d3905448fd4126f2e7e4a9dc960c6728a53 Mon Sep 17 00:00:00 2001 From: AkosuaA Date: Thu, 10 Oct 2024 12:50:37 +0100 Subject: [PATCH 05/17] add tests --- .../__tests__/TeamComplianceReport.test.tsx | 129 ++++++++++++++++++ .../teams/__tests__/TeamProfile.test.tsx | 51 +++++++ .../src/network/teams/__tests__/api.test.ts | 31 +++++ .../compliance-report.controller.test.ts | 46 +++++++ .../compliance-reports.data-provider.test.ts | 66 +++++++++ .../fixtures/compliance-reports.fixtures.ts | 20 +++ .../test/fixtures/manuscript.fixtures.ts | 1 + .../test/fixtures/teams.fixtures.ts | 2 + .../compliance-report.controller.mock.ts | 5 + .../routes/compliance-report.route.test.ts | 129 ++++++++++++++++++ packages/fixtures/src/manuscripts.ts | 1 + .../__tests__/ComplianceReportCard.test.tsx | 27 ++++ .../__tests__/ComplianceReportHeader.test.tsx | 15 ++ .../__tests__/TeamProfileWorkspace.test.tsx | 2 + 14 files changed, 525 insertions(+) create mode 100644 apps/crn-frontend/src/network/teams/__tests__/TeamComplianceReport.test.tsx create mode 100644 apps/crn-server/test/controllers/compliance-report.controller.test.ts create mode 100644 apps/crn-server/test/data-providers/contentful/compliance-reports.data-provider.test.ts create mode 100644 apps/crn-server/test/fixtures/compliance-reports.fixtures.ts create mode 100644 apps/crn-server/test/mocks/compliance-report.controller.mock.ts create mode 100644 apps/crn-server/test/routes/compliance-report.route.test.ts create mode 100644 packages/react-components/src/organisms/__tests__/ComplianceReportCard.test.tsx create mode 100644 packages/react-components/src/organisms/__tests__/ComplianceReportHeader.test.tsx diff --git a/apps/crn-frontend/src/network/teams/__tests__/TeamComplianceReport.test.tsx b/apps/crn-frontend/src/network/teams/__tests__/TeamComplianceReport.test.tsx new file mode 100644 index 0000000000..89015eb581 --- /dev/null +++ b/apps/crn-frontend/src/network/teams/__tests__/TeamComplianceReport.test.tsx @@ -0,0 +1,129 @@ +import { + Auth0Provider, + WhenReady, +} from '@asap-hub/crn-frontend/src/auth/test-utils'; +import { network } from '@asap-hub/routing'; +import { + render, + screen, + waitFor, + waitForElementToBeRemoved, +} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { createMemoryHistory } from 'history'; +import { ComponentProps, Suspense } from 'react'; +import { Route, Router } from 'react-router-dom'; +import { RecoilRoot } from 'recoil'; + +import { createComplianceReport } from '../api'; +import { ManuscriptToastProvider } from '../ManuscriptToastProvider'; +import { refreshTeamState } from '../state'; +import TeamComplianceReport from '../TeamComplianceReport'; + +const manuscriptResponse = { + id: 'manuscript-1', + title: 'The Manuscript', + versions: [{ id: 'manuscript-version-1' }], +}; +const complianceReportResponse = { id: 'compliance-report-1' }; + +const teamId = '42'; +const history = createMemoryHistory({ + initialEntries: [ + network({}) + .teams({}) + .team({ teamId }) + .workspace({}) + .createComplianceReport({ manuscriptId: manuscriptResponse.id }).$, + ], +}); + +jest.mock('../../users/api'); + +jest.mock('../api', () => ({ + createComplianceReport: jest.fn().mockResolvedValue(complianceReportResponse), + getManuscript: jest.fn().mockResolvedValue(manuscriptResponse), +})); + +beforeEach(() => { + jest.resetModules(); +}); + +const renderPage = async ( + user: ComponentProps['user'] = {}, +) => { + const path = + network.template + + network({}).teams.template + + network({}).teams({}).team.template + + network({}).teams({}).team({ teamId }).workspace.template + + network({}).teams({}).team({ teamId }).workspace({}).createComplianceReport + .template; + + const { container } = render( + { + set(refreshTeamState(teamId), Math.random()); + }} + > + + + + + + + + + + + + + + , + ); + await waitForElementToBeRemoved(() => screen.queryByText(/loading/i)); + return { container }; +}; + +it('renders compliance report form page', async () => { + const { container } = await renderPage(); + + expect(container).toHaveTextContent( + 'Share the compliance report associated with this manuscript.', + ); + expect(container).toHaveTextContent('Title of Manuscript'); +}); + +it('can publish a form when the data is valid and navigates to team workspace', async () => { + const url = 'https://compliancereport.com'; + const description = 'compliance report description'; + + await renderPage(); + + userEvent.type(screen.getByRole('textbox', { name: /url/i }), url); + + userEvent.type( + screen.getByRole('textbox', { + name: /Compliance Report Description/i, + }), + description, + ); + + const shareButton = screen.getByRole('button', { name: /Share/i }); + + userEvent.click(shareButton); + + await waitFor(() => { + expect(createComplianceReport).toHaveBeenCalledWith( + { + url, + description, + manuscriptVersionId: manuscriptResponse.versions[0]!.id, + }, + expect.anything(), + ); + expect(history.location.pathname).toBe( + `/network/teams/${teamId}/workspace`, + ); + }); +}); diff --git a/apps/crn-frontend/src/network/teams/__tests__/TeamProfile.test.tsx b/apps/crn-frontend/src/network/teams/__tests__/TeamProfile.test.tsx index 843a7a20a1..8854d915f6 100644 --- a/apps/crn-frontend/src/network/teams/__tests__/TeamProfile.test.tsx +++ b/apps/crn-frontend/src/network/teams/__tests__/TeamProfile.test.tsx @@ -38,6 +38,12 @@ import { ManuscriptToastProvider } from '../ManuscriptToastProvider'; import { refreshTeamState } from '../state'; import TeamProfile from '../TeamProfile'; +const manuscriptResponse = { + id: 'manuscript-1', + title: 'The Manuscript', + versions: [{ id: 'manuscript-version-1' }], +}; + jest.mock('../api', () => ({ ...jest.requireActual('../api'), getTeam: jest.fn(), @@ -50,6 +56,7 @@ jest.mock('../api', () => ({ createManuscript: jest .fn() .mockResolvedValue({ title: 'A manuscript', id: '1' }), + getManuscript: jest.fn().mockResolvedValue(manuscriptResponse), })); jest.mock('../interest-groups/api'); @@ -287,6 +294,7 @@ it('does not allow navigating to the workspace tab when team tools are not avail screen.queryByText(/workspace/i, { selector: 'nav *' }), ).not.toBeInTheDocument(); }); + describe('Share Output', () => { it('shows share outputs button and page when the user has permissions user clicks an option', async () => { const teamResponse = createTeamResponse(); @@ -481,6 +489,49 @@ describe('Duplicate Output', () => { }); }); +describe('Create Compliance Report', () => { + it('allows a user who is an ASAP staff to create a compliance report', async () => { + const teamResponse = createTeamResponse(); + const userResponse = createUserResponse({}, 1); + userResponse.role = 'Staff'; + + const history = createMemoryHistory({ + initialEntries: [ + network({}) + .teams({}) + .team({ teamId: teamResponse.id }) + .workspace({}) + .createComplianceReport({ manuscriptId: manuscriptResponse.id }).$, + ], + }); + await renderPage( + teamResponse, + { teamId: teamResponse.id, currentTime: new Date() }, + { + ...userResponse, + teams: [ + { + ...userResponse.teams[0], + id: teamResponse.id, + role: 'Key Personnel', + }, + ], + }, + history, + ); + + expect( + screen.getByText( + /Share the compliance report associated with this manuscript./, + ), + ).toBeInTheDocument(); + + expect(history.location.pathname).toEqual( + `/network/teams/${teamResponse.id}/workspace/create-compliance-report/${manuscriptResponse.id}`, + ); + }); +}); + it('renders the 404 page for a missing team', async () => { await renderPage({ ...createTeamResponse(), id: '42' }, { teamId: '1337' }); expect(screen.getByText(/sorry.+page/i)).toBeVisible(); diff --git a/apps/crn-frontend/src/network/teams/__tests__/api.test.ts b/apps/crn-frontend/src/network/teams/__tests__/api.test.ts index 73d558d9a9..b61f8ba02a 100644 --- a/apps/crn-frontend/src/network/teams/__tests__/api.test.ts +++ b/apps/crn-frontend/src/network/teams/__tests__/api.test.ts @@ -6,6 +6,7 @@ import { } from '@asap-hub/fixtures'; import { GetListOptions } from '@asap-hub/frontend-utils'; import { + ComplianceReportPostRequest, ManuscriptFileResponse, ManuscriptPostRequest, ResearchOutputPostRequest, @@ -16,6 +17,7 @@ import nock from 'nock'; import { API_BASE_URL } from '../../../config'; import { CARD_VIEW_PAGE_SIZE } from '../../../hooks'; import { + createComplianceReport, createManuscript, createResearchOutput, getLabs, @@ -417,3 +419,32 @@ describe('Manuscript', () => { }); }); }); + +describe('Compliance Report', () => { + describe('POST', () => { + const payload: ComplianceReportPostRequest = { + url: 'https://compliancereport.com', + description: 'Compliance report description', + manuscriptVersionId: 'manuscript-version-1', + }; + + it('makes an authorized POST request to create a compliance report', async () => { + nock(API_BASE_URL, { reqheaders: { authorization: 'Bearer x' } }) + .post('/compliance-reports', payload) + .reply(201, { id: 123 }); + + await createComplianceReport(payload, 'Bearer x'); + expect(nock.isDone()).toBe(true); + }); + + it('errors for an error status', async () => { + nock(API_BASE_URL).post('/compliance-reports').reply(500, {}); + + await expect( + createComplianceReport(payload, 'Bearer x'), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Failed to create compliance report. Expected status 201. Received status 500."`, + ); + }); + }); +}); diff --git a/apps/crn-server/test/controllers/compliance-report.controller.test.ts b/apps/crn-server/test/controllers/compliance-report.controller.test.ts new file mode 100644 index 0000000000..d77f335fe5 --- /dev/null +++ b/apps/crn-server/test/controllers/compliance-report.controller.test.ts @@ -0,0 +1,46 @@ +import { GenericError } from '@asap-hub/errors'; +import ComplianceReportController from '../../src/controllers/compliance-report.controller'; +import { ComplianceReportDataProvider } from '../../src/data-providers/types'; +import { getComplianceReportCreateDataObject } from '../fixtures/compliance-reports.fixtures'; +import { getDataProviderMock } from '../mocks/data-provider.mock'; + +describe('Compliance Report controller', () => { + const complianceReportDataProviderMock: jest.Mocked = + getDataProviderMock(); + + const complianceReportController = new ComplianceReportController( + complianceReportDataProviderMock, + ); + + describe('Create method', () => { + beforeEach(jest.clearAllMocks); + + test('Should throw when fails to create the compliance report', async () => { + complianceReportDataProviderMock.create.mockRejectedValueOnce( + new GenericError(), + ); + + await expect( + complianceReportController.create( + getComplianceReportCreateDataObject(), + ), + ).rejects.toThrow(GenericError); + }); + + test('Should create the new compliance report and return its id', async () => { + const complianceReportId = 'compliance-report-id-1'; + complianceReportDataProviderMock.create.mockResolvedValueOnce( + complianceReportId, + ); + + const result = await complianceReportController.create( + getComplianceReportCreateDataObject(), + ); + + expect(result).toEqual(complianceReportId); + expect(complianceReportDataProviderMock.create).toHaveBeenCalledWith( + getComplianceReportCreateDataObject(), + ); + }); + }); +}); diff --git a/apps/crn-server/test/data-providers/contentful/compliance-reports.data-provider.test.ts b/apps/crn-server/test/data-providers/contentful/compliance-reports.data-provider.test.ts new file mode 100644 index 0000000000..65d7ad3b27 --- /dev/null +++ b/apps/crn-server/test/data-providers/contentful/compliance-reports.data-provider.test.ts @@ -0,0 +1,66 @@ +import { Entry, Environment } from '@asap-hub/contentful'; + +import { when } from 'jest-when'; +import { ComplianceReportContentfulDataProvider } from '../../../src/data-providers/contentful/compliance-report.data-provider'; + +import { getComplianceReportCreateDataObject } from '../../fixtures/compliance-reports.fixtures'; +import { getContentfulEnvironmentMock } from '../../mocks/contentful-rest-client.mock'; + +describe('Compliance Reports Contentful Data Provider', () => { + const environmentMock = getContentfulEnvironmentMock(); + const contentfulRestClientMock: () => Promise = () => + Promise.resolve(environmentMock); + + const complianceReportDataProvider = + new ComplianceReportContentfulDataProvider(contentfulRestClientMock); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('Create', () => { + test('can create a compliance report', async () => { + const complianceReportId = 'compliance-report-id-1'; + const complianceReportCreateDataObject = + getComplianceReportCreateDataObject(); + + const publish = jest.fn(); + + when(environmentMock.createEntry) + .calledWith('complianceReports', expect.anything()) + .mockResolvedValue({ + sys: { id: complianceReportId }, + publish, + } as unknown as Entry); + + const result = await complianceReportDataProvider.create({ + ...complianceReportCreateDataObject, + }); + + expect(environmentMock.createEntry).toHaveBeenCalledWith( + 'complianceReports', + { + fields: { + url: { + 'en-US': 'http://example.com', + }, + description: { + 'en-US': 'compliance report description', + }, + manuscriptVersion: { + 'en-US': { + sys: { + id: 'manuscript-version-1', + linkType: 'Entry', + type: 'Link', + }, + }, + }, + }, + }, + ); + expect(publish).toHaveBeenCalled(); + expect(result).toEqual(complianceReportId); + }); + }); +}); diff --git a/apps/crn-server/test/fixtures/compliance-reports.fixtures.ts b/apps/crn-server/test/fixtures/compliance-reports.fixtures.ts new file mode 100644 index 0000000000..2dae01c2b6 --- /dev/null +++ b/apps/crn-server/test/fixtures/compliance-reports.fixtures.ts @@ -0,0 +1,20 @@ +import { + ComplianceReportCreateDataObject, + ComplianceReportDataObject, +} from '@asap-hub/model'; + +export const getComplianceReportDataObject = + (): ComplianceReportDataObject => ({ + url: 'http://example.com', + description: 'compliance report description', + }); + +export const getComplianceReportCreateDataObject = + (): ComplianceReportCreateDataObject => { + const complianceReport = getComplianceReportDataObject(); + + return { + ...complianceReport, + manuscriptVersionId: 'manuscript-version-1', + }; + }; diff --git a/apps/crn-server/test/fixtures/manuscript.fixtures.ts b/apps/crn-server/test/fixtures/manuscript.fixtures.ts index d9ca2f9aa8..2d01297962 100644 --- a/apps/crn-server/test/fixtures/manuscript.fixtures.ts +++ b/apps/crn-server/test/fixtures/manuscript.fixtures.ts @@ -17,6 +17,7 @@ export const getManuscriptDataObject = ( teamId: 'team-1', versions: [ { + id: 'version-1', lifecycle: 'Preprint', type: 'Original Research', createdBy: manuscriptAuthor, diff --git a/apps/crn-server/test/fixtures/teams.fixtures.ts b/apps/crn-server/test/fixtures/teams.fixtures.ts index c6798eff29..e01dc4fc93 100644 --- a/apps/crn-server/test/fixtures/teams.fixtures.ts +++ b/apps/crn-server/test/fixtures/teams.fixtures.ts @@ -213,6 +213,7 @@ export const getTeamDataObject = (): TeamDataObject => ({ title: 'Manuscript 1', versions: [ { + id: 'version-1', lifecycle: 'Preprint', type: 'Original Research', createdBy: manuscriptAuthor, @@ -243,6 +244,7 @@ export const getTeamDataObject = (): TeamDataObject => ({ title: 'Manuscript 2', versions: [ { + id: 'version-1', lifecycle: 'Preprint', type: 'Original Research', createdBy: manuscriptAuthor, diff --git a/apps/crn-server/test/mocks/compliance-report.controller.mock.ts b/apps/crn-server/test/mocks/compliance-report.controller.mock.ts new file mode 100644 index 0000000000..550102e3a3 --- /dev/null +++ b/apps/crn-server/test/mocks/compliance-report.controller.mock.ts @@ -0,0 +1,5 @@ +import ComplianceReportController from '../../src/controllers/compliance-report.controller'; + +export const complianceReportControllerMock = { + create: jest.fn(), +} as unknown as jest.Mocked; diff --git a/apps/crn-server/test/routes/compliance-report.route.test.ts b/apps/crn-server/test/routes/compliance-report.route.test.ts new file mode 100644 index 0000000000..1031607d55 --- /dev/null +++ b/apps/crn-server/test/routes/compliance-report.route.test.ts @@ -0,0 +1,129 @@ +import { createUserResponse } from '@asap-hub/fixtures'; +import { UserResponse } from '@asap-hub/model'; +import { AuthHandler } from '@asap-hub/server-common'; +import supertest from 'supertest'; + +import { appFactory } from '../../src/app'; +import { getComplianceReportCreateDataObject } from '../fixtures/compliance-reports.fixtures'; +import { loggerMock } from '../mocks/logger.mock'; +import { complianceReportControllerMock } from '../mocks/compliance-report.controller.mock'; + +describe('/compliance-reports/ route', () => { + const userMockFactory = jest.fn(); + const authHandlerMock: AuthHandler = (req, _res, next) => { + req.loggedInUser = userMockFactory(); + next(); + }; + + const app = appFactory({ + complianceReportController: complianceReportControllerMock, + authHandler: authHandlerMock, + logger: loggerMock, + }); + + beforeEach(() => { + userMockFactory.mockReturnValue({ ...createUserResponse(), role: 'Staff' }); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('POST /compliance-reports/', () => { + const complianceReportId = 'compliance-report-id'; + const createComplianceReportRequest = getComplianceReportCreateDataObject(); + + test('Should return 403 when not allowed to create a compliance report because user is not onboarded', async () => { + userMockFactory.mockReturnValueOnce({ + ...createUserResponse(), + onboarded: false, + role: 'Staff', + }); + + const response = await supertest(app) + .post('/compliance-reports') + .send(createComplianceReportRequest) + .set('Accept', 'application/json'); + + expect(response.status).toEqual(403); + }); + + test('Should return 403 when not allowed to create a compliance report because user is not ASAP Staff', async () => { + userMockFactory.mockReturnValueOnce({ + ...createUserResponse(), + onboarded: true, + role: 'Grantee', + }); + + const response = await supertest(app) + .post('/compliance-reports') + .send(createComplianceReportRequest) + .set('Accept', 'application/json'); + + expect(response.status).toEqual(403); + }); + + test('Should return a 201 and pass input to the controller', async () => { + userMockFactory.mockReturnValueOnce({ + ...createUserResponse(), + role: 'Staff', + }); + + complianceReportControllerMock.create.mockResolvedValueOnce( + complianceReportId, + ); + + const response = await supertest(app) + .post('/compliance-reports') + .send(createComplianceReportRequest) + .set('Accept', 'application/json'); + + expect(response.status).toBe(201); + expect(complianceReportControllerMock.create).toHaveBeenCalledWith( + createComplianceReportRequest, + ); + + expect(response.body).toEqual(complianceReportId); + }); + + describe('Validation', () => { + test('Should return 400 when url is missing', async () => { + const { url: _url, ...createComplianceReportRequest } = + getComplianceReportCreateDataObject(); + + const response = await supertest(app) + .post('/compliance-reports') + .send(createComplianceReportRequest) + .set('Accept', 'application/json'); + + expect(response.status).toEqual(400); + }); + + test('Should return 400 when description is missing', async () => { + const { description: _description, ...createComplianceReportRequest } = + getComplianceReportCreateDataObject(); + + const response = await supertest(app) + .post('/compliance-reports') + .send(createComplianceReportRequest) + .set('Accept', 'application/json'); + + expect(response.status).toEqual(400); + }); + + test('Should return 400 when manuscriptVersionId is missing', async () => { + const { + manuscriptVersionId: _manuscriptVersionId, + ...createComplianceReportRequest + } = getComplianceReportCreateDataObject(); + + const response = await supertest(app) + .post('/compliance-reports') + .send(createComplianceReportRequest) + .set('Accept', 'application/json'); + + expect(response.status).toEqual(400); + }); + }); + }); +}); diff --git a/packages/fixtures/src/manuscripts.ts b/packages/fixtures/src/manuscripts.ts index 648e94ba36..1ae760f6ab 100644 --- a/packages/fixtures/src/manuscripts.ts +++ b/packages/fixtures/src/manuscripts.ts @@ -24,6 +24,7 @@ export const createManuscriptResponse = ( teamId: 'team-1', versions: [ { + id: 'version-1', lifecycle: 'Draft Manuscript (prior to Publication)', type: 'Original Research', createdBy: manuscriptAuthor, diff --git a/packages/react-components/src/organisms/__tests__/ComplianceReportCard.test.tsx b/packages/react-components/src/organisms/__tests__/ComplianceReportCard.test.tsx new file mode 100644 index 0000000000..dd144a6aa4 --- /dev/null +++ b/packages/react-components/src/organisms/__tests__/ComplianceReportCard.test.tsx @@ -0,0 +1,27 @@ +import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import ComplianceReportCard from '../ComplianceReportCard'; + +it('displays compliance report description and url when expanded', () => { + const props = { + url: 'http://example.com/', + description: 'compliance report description', + }; + const { getByText, queryByText, getByRole, rerender } = render( + , + ); + + expect(queryByText(/compliance report description/i)).not.toBeInTheDocument(); + expect(queryByText(/View Report/i)).not.toBeInTheDocument(); + expect(queryByText(/example.com/i)).not.toBeInTheDocument(); + + userEvent.click(getByRole('button')); + + rerender(); + + expect(getByText(/compliance report description/i)).toBeVisible(); + expect(getByText(/View Report/i)).toBeVisible(); + expect(getByText(/View Report/i).closest('a')?.href).toBe( + 'http://example.com/', + ); +}); diff --git a/packages/react-components/src/organisms/__tests__/ComplianceReportHeader.test.tsx b/packages/react-components/src/organisms/__tests__/ComplianceReportHeader.test.tsx new file mode 100644 index 0000000000..c1fd8181ce --- /dev/null +++ b/packages/react-components/src/organisms/__tests__/ComplianceReportHeader.test.tsx @@ -0,0 +1,15 @@ +import { render, screen } from '@testing-library/react'; + +import ComplianceReportHeader from '../ComplianceReportHeader'; + +it('renders the compliance report header content', () => { + render(); + expect( + screen.getByRole('heading', { name: /Share a Compliance Report/i }), + ).toBeInTheDocument(); + expect( + screen.getByText( + 'Share the compliance report associated with this manuscript.', + ), + ).toBeInTheDocument(); +}); diff --git a/packages/react-components/src/templates/__tests__/TeamProfileWorkspace.test.tsx b/packages/react-components/src/templates/__tests__/TeamProfileWorkspace.test.tsx index 7cd629daad..d2d64516d5 100644 --- a/packages/react-components/src/templates/__tests__/TeamProfileWorkspace.test.tsx +++ b/packages/react-components/src/templates/__tests__/TeamProfileWorkspace.test.tsx @@ -99,6 +99,7 @@ describe('compliance section', () => { title: 'Nice manuscript', versions: [ { + id: 'version-1', type: 'Original Research', lifecycle: 'Draft Manuscript (prior to Publication)', manuscriptFile: { @@ -138,6 +139,7 @@ describe('compliance section', () => { title: 'A Good Manuscript', versions: [ { + id: 'version-1', type: 'Review / Op-Ed / Letter / Hot Topic', lifecycle: 'Preprint', manuscriptFile: { From 2ecba973e7f023e8948f36eb4f8584eb5b26f487 Mon Sep 17 00:00:00 2001 From: AkosuaA Date: Thu, 10 Oct 2024 13:13:14 +0100 Subject: [PATCH 06/17] update fixture --- apps/crn-server/test/fixtures/manuscript.fixtures.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/crn-server/test/fixtures/manuscript.fixtures.ts b/apps/crn-server/test/fixtures/manuscript.fixtures.ts index 2d01297962..8fc077c1a2 100644 --- a/apps/crn-server/test/fixtures/manuscript.fixtures.ts +++ b/apps/crn-server/test/fixtures/manuscript.fixtures.ts @@ -141,8 +141,9 @@ export const getManuscriptPostBody = (): ManuscriptPostRequest => { const { createdBy: _, createdDate: __, - publishedAt: ___, - teams: ____, + id: ___, + publishedAt: ____, + teams: _____, ...version } = versions[0]!; return { From 55f847d323b0c7603b1e624adcf7b408cb7e4182 Mon Sep 17 00:00:00 2001 From: AkosuaA Date: Thu, 10 Oct 2024 13:40:07 +0100 Subject: [PATCH 07/17] update getManuscriptCreateDataObject --- apps/crn-server/test/fixtures/manuscript.fixtures.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/crn-server/test/fixtures/manuscript.fixtures.ts b/apps/crn-server/test/fixtures/manuscript.fixtures.ts index 8fc077c1a2..85c8bb36eb 100644 --- a/apps/crn-server/test/fixtures/manuscript.fixtures.ts +++ b/apps/crn-server/test/fixtures/manuscript.fixtures.ts @@ -191,6 +191,7 @@ export const getManuscriptCreateDataObject = (): ManuscriptCreateDataObject => { teams: _, publishedAt: __, createdDate: ___, + id: ____, ...version } = versions[0]!; From a08138d173c19a6e0c189478481792eab8f395e5 Mon Sep 17 00:00:00 2001 From: AkosuaA Date: Thu, 10 Oct 2024 18:35:35 +0100 Subject: [PATCH 08/17] update tests --- .../network/teams/TeamComplianceReport.tsx | 1 + .../__tests__/TeamComplianceReport.test.tsx | 46 ++++-- .../teams/__tests__/TeamProfile.test.tsx | 53 ++++++- .../compliance-reports.data-provider.test.ts | 16 +++ .../__tests__/ManuscriptVersionCard.test.tsx | 22 +++ .../src/templates/ComplianceReportForm.tsx | 4 +- .../__tests__/ComplianceReportForm.test.tsx | 132 ++++++++++++++++++ 7 files changed, 252 insertions(+), 22 deletions(-) create mode 100644 packages/react-components/src/templates/__tests__/ComplianceReportForm.test.tsx diff --git a/apps/crn-frontend/src/network/teams/TeamComplianceReport.tsx b/apps/crn-frontend/src/network/teams/TeamComplianceReport.tsx index 97237e6d46..36755c4ed4 100644 --- a/apps/crn-frontend/src/network/teams/TeamComplianceReport.tsx +++ b/apps/crn-frontend/src/network/teams/TeamComplianceReport.tsx @@ -55,6 +55,7 @@ const TeamComplianceReport: React.FC = ({ ); } + return ; }; export default TeamComplianceReport; diff --git a/apps/crn-frontend/src/network/teams/__tests__/TeamComplianceReport.test.tsx b/apps/crn-frontend/src/network/teams/__tests__/TeamComplianceReport.test.tsx index 89015eb581..ec583d9f24 100644 --- a/apps/crn-frontend/src/network/teams/__tests__/TeamComplianceReport.test.tsx +++ b/apps/crn-frontend/src/network/teams/__tests__/TeamComplianceReport.test.tsx @@ -15,7 +15,7 @@ import { ComponentProps, Suspense } from 'react'; import { Route, Router } from 'react-router-dom'; import { RecoilRoot } from 'recoil'; -import { createComplianceReport } from '../api'; +import { createComplianceReport, getManuscript } from '../api'; import { ManuscriptToastProvider } from '../ManuscriptToastProvider'; import { refreshTeamState } from '../state'; import TeamComplianceReport from '../TeamComplianceReport'; @@ -28,29 +28,31 @@ const manuscriptResponse = { const complianceReportResponse = { id: 'compliance-report-1' }; const teamId = '42'; -const history = createMemoryHistory({ - initialEntries: [ - network({}) - .teams({}) - .team({ teamId }) - .workspace({}) - .createComplianceReport({ manuscriptId: manuscriptResponse.id }).$, - ], -}); - -jest.mock('../../users/api'); jest.mock('../api', () => ({ createComplianceReport: jest.fn().mockResolvedValue(complianceReportResponse), getManuscript: jest.fn().mockResolvedValue(manuscriptResponse), })); +const mockGetManuscript = getManuscript as jest.MockedFunction< + typeof getManuscript +>; + beforeEach(() => { jest.resetModules(); }); const renderPage = async ( user: ComponentProps['user'] = {}, + history = createMemoryHistory({ + initialEntries: [ + network({}) + .teams({}) + .team({ teamId }) + .workspace({}) + .createComplianceReport({ manuscriptId: manuscriptResponse.id }).$, + ], + }), ) => { const path = network.template + @@ -97,8 +99,17 @@ it('renders compliance report form page', async () => { it('can publish a form when the data is valid and navigates to team workspace', async () => { const url = 'https://compliancereport.com'; const description = 'compliance report description'; + const history = createMemoryHistory({ + initialEntries: [ + network({}) + .teams({}) + .team({ teamId }) + .workspace({}) + .createComplianceReport({ manuscriptId: manuscriptResponse.id }).$, + ], + }); - await renderPage(); + await renderPage({}, history); userEvent.type(screen.getByRole('textbox', { name: /url/i }), url); @@ -127,3 +138,12 @@ it('can publish a form when the data is valid and navigates to team workspace', ); }); }); + +it('renders not found when the manuscript hook does not return a manuscript with a version', async () => { + mockGetManuscript.mockResolvedValue(undefined); + await renderPage(); + + expect(screen.getByRole('heading').textContent).toContain( + 'Sorry! We can’t seem to find that page.', + ); +}); diff --git a/apps/crn-frontend/src/network/teams/__tests__/TeamProfile.test.tsx b/apps/crn-frontend/src/network/teams/__tests__/TeamProfile.test.tsx index 8854d915f6..757e38b64a 100644 --- a/apps/crn-frontend/src/network/teams/__tests__/TeamProfile.test.tsx +++ b/apps/crn-frontend/src/network/teams/__tests__/TeamProfile.test.tsx @@ -5,6 +5,7 @@ import { import { createListEventResponse, createListResearchOutputResponse, + createManuscriptResponse, createResearchOutputResponse, createTeamResponse, createUserResponse, @@ -490,18 +491,51 @@ describe('Duplicate Output', () => { }); describe('Create Compliance Report', () => { + it('allows a user who is an ASAP staff to view Submit Compliance Report button', async () => { + enable('DISPLAY_MANUSCRIPTS'); + const teamResponse = createTeamResponse(); + const userResponse = createUserResponse({}, 1); + + teamResponse.manuscripts = [createManuscriptResponse()]; + userResponse.role = 'Staff'; + + const history = createMemoryHistory({ + initialEntries: [ + network({}).teams({}).team({ teamId: teamResponse.id }).workspace({}).$, + ], + }); + await renderPage( + teamResponse, + { teamId: teamResponse.id, currentTime: new Date() }, + { + ...userResponse, + teams: [ + { + ...userResponse.teams[0], + id: teamResponse.id, + role: 'Key Personnel', + }, + ], + }, + history, + ); + + expect( + screen.getByRole('button', { name: /Submit Compliance Report Icon/ }), + ).toBeInTheDocument(); + }); + it('allows a user who is an ASAP staff to create a compliance report', async () => { + enable('DISPLAY_MANUSCRIPTS'); const teamResponse = createTeamResponse(); const userResponse = createUserResponse({}, 1); + const teamManuscript = createManuscriptResponse(); + teamResponse.manuscripts = [teamManuscript]; userResponse.role = 'Staff'; const history = createMemoryHistory({ initialEntries: [ - network({}) - .teams({}) - .team({ teamId: teamResponse.id }) - .workspace({}) - .createComplianceReport({ manuscriptId: manuscriptResponse.id }).$, + network({}).teams({}).team({ teamId: teamResponse.id }).workspace({}).$, ], }); await renderPage( @@ -520,14 +554,19 @@ describe('Create Compliance Report', () => { history, ); + userEvent.click( + screen.getByRole('button', { name: /Submit Compliance Report Icon/ }), + ); + + // expect(await screen.findByText(/Output 1/i)).toBeVisible(); expect( - screen.getByText( + await screen.findByText( /Share the compliance report associated with this manuscript./, ), ).toBeInTheDocument(); expect(history.location.pathname).toEqual( - `/network/teams/${teamResponse.id}/workspace/create-compliance-report/${manuscriptResponse.id}`, + `/network/teams/${teamResponse.id}/workspace/create-compliance-report/${teamManuscript.id}`, ); }); }); diff --git a/apps/crn-server/test/data-providers/contentful/compliance-reports.data-provider.test.ts b/apps/crn-server/test/data-providers/contentful/compliance-reports.data-provider.test.ts index 65d7ad3b27..db68f638f4 100644 --- a/apps/crn-server/test/data-providers/contentful/compliance-reports.data-provider.test.ts +++ b/apps/crn-server/test/data-providers/contentful/compliance-reports.data-provider.test.ts @@ -63,4 +63,20 @@ describe('Compliance Reports Contentful Data Provider', () => { expect(result).toEqual(complianceReportId); }); }); + + describe('Fetch', () => { + test('should throw an error', async () => { + await expect(complianceReportDataProvider.fetch()).rejects.toThrow( + 'Method not implemented.', + ); + }); + }); + + describe('Fetch by ID', () => { + test('should throw an error', async () => { + await expect(complianceReportDataProvider.fetch()).rejects.toThrow( + 'Method not implemented.', + ); + }); + }); }); diff --git a/packages/react-components/src/organisms/__tests__/ManuscriptVersionCard.test.tsx b/packages/react-components/src/organisms/__tests__/ManuscriptVersionCard.test.tsx index 8f5e94439e..c66cd92988 100644 --- a/packages/react-components/src/organisms/__tests__/ManuscriptVersionCard.test.tsx +++ b/packages/react-components/src/organisms/__tests__/ManuscriptVersionCard.test.tsx @@ -277,3 +277,25 @@ it('renders additional files details and download link when provided', () => { 'https://example.com/additional-file.pdf', ); }); + +it('displays compliance report section when present', () => { + const { getByRole, queryByRole, rerender } = render( + , + ); + userEvent.click(getByRole('button')); + expect( + queryByRole('heading', { name: /Compliance Report/i }), + ).not.toBeInTheDocument(); + + rerender( + , + ); + + expect(getByRole('heading', { name: /Compliance Report/i })).toBeVisible(); +}); diff --git a/packages/react-components/src/templates/ComplianceReportForm.tsx b/packages/react-components/src/templates/ComplianceReportForm.tsx index b7e0870da1..bb0555d7f3 100644 --- a/packages/react-components/src/templates/ComplianceReportForm.tsx +++ b/packages/react-components/src/templates/ComplianceReportForm.tsx @@ -113,7 +113,7 @@ const ComplianceReportForm: React.FC = ({ message: 'Please enter a valid URL, starting with http:// or https://', }, - required: 'Please enter the url.', + required: 'Please enter a url.', }} render={({ field: { value, onChange }, @@ -136,7 +136,7 @@ const ComplianceReportForm: React.FC = ({ name="description" control={control} rules={{ - required: 'Please enter the description.', + required: 'Please enter a description.', }} render={({ field: { value, onChange }, diff --git a/packages/react-components/src/templates/__tests__/ComplianceReportForm.test.tsx b/packages/react-components/src/templates/__tests__/ComplianceReportForm.test.tsx new file mode 100644 index 0000000000..e9a56a5cf1 --- /dev/null +++ b/packages/react-components/src/templates/__tests__/ComplianceReportForm.test.tsx @@ -0,0 +1,132 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import { ComponentProps } from 'react'; +import { MemoryRouter, Route, Router, StaticRouter } from 'react-router-dom'; +import { createMemoryHistory, History } from 'history'; +import userEvent from '@testing-library/user-event'; +import ComplianceReportForm from '../ComplianceReportForm'; + +let history!: History; + +beforeEach(() => { + history = createMemoryHistory(); +}); + +const defaultProps: ComponentProps = { + onSave: jest.fn(() => Promise.resolve()), + onSuccess: jest.fn(), + manuscriptTitle: 'manuscript title', + manuscriptVersionId: 'manuscript-version-1', +}; + +it('renders the form', async () => { + render( + + + , + ); + expect(screen.getByText(/Title of Manuscript/i)).toBeVisible(); + expect(screen.getByRole('button', { name: /Share/i })).toBeVisible(); +}); + +it('data is sent on form submission', async () => { + const onSave = jest.fn(); + render( + + + , + ); + + userEvent.click(screen.getByRole('button', { name: /Share/i })); + await waitFor(() => { + expect(onSave).toHaveBeenCalledWith({ + url: 'http://example.com', + description: 'manuscript description', + manuscriptVersionId: defaultProps.manuscriptVersionId, + }); + }); +}); + +it('displays error message when url is missing', async () => { + render( + + + , + ); + + const input = screen.getByRole('textbox', { name: /url/i }); + const shareButton = screen.getByRole('button', { name: /Share/i }); + + userEvent.click(shareButton); + + await waitFor(() => { + expect(shareButton).toBeEnabled(); + }); + expect( + screen.getAllByText(/Please enter a url/i).length, + ).toBeGreaterThanOrEqual(1); + + userEvent.type(input, 'http://example.com'); + + userEvent.click(shareButton); + + await waitFor(() => { + expect(shareButton).toBeEnabled(); + }); + expect(screen.queryByText(/Please enter a url/i)).toBeNull(); +}); + +it('displays error message when description is missing', async () => { + render( + + + , + ); + + const input = screen.getByRole('textbox', { + name: /compliance report description/i, + }); + const shareButton = screen.getByRole('button', { name: /Share/i }); + + userEvent.click(shareButton); + + await waitFor(() => { + expect(shareButton).toBeEnabled(); + }); + expect( + screen.getAllByText(/Please enter a description/i).length, + ).toBeGreaterThanOrEqual(1); + + userEvent.type(input, 'manuscription description'); + + userEvent.click(shareButton); + + await waitFor(() => { + expect(shareButton).toBeEnabled(); + }); + expect(screen.queryByText(/Please enter a description/i)).toBeNull(); +}); + +it('should go back when cancel button is clicked', () => { + const { getByText } = render( + + + + + , + { wrapper: MemoryRouter }, + ); + + history.push('/another-url'); + history.push('/form'); + + const cancelButton = getByText(/cancel/i); + expect(cancelButton).toBeInTheDocument(); + userEvent.click(cancelButton); + + expect(history.location.pathname).toBe('/another-url'); +}); From 0278953e6c6e53ef85d43214833872a9178843d0 Mon Sep 17 00:00:00 2001 From: AkosuaA Date: Thu, 10 Oct 2024 18:38:45 +0100 Subject: [PATCH 09/17] update schema --- packages/contentful/src/crn/autogenerated-gql/gql.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/contentful/src/crn/autogenerated-gql/gql.ts b/packages/contentful/src/crn/autogenerated-gql/gql.ts index 19f44080f0..6e9abc79b4 100644 --- a/packages/contentful/src/crn/autogenerated-gql/gql.ts +++ b/packages/contentful/src/crn/autogenerated-gql/gql.ts @@ -73,7 +73,7 @@ const documents = { types.FetchInterestGroupsByUserIdDocument, '\n query FetchLabs($limit: Int, $skip: Int, $where: LabsFilter) {\n labsCollection(limit: $limit, skip: $skip, where: $where, order: name_ASC) {\n total\n items {\n sys {\n id\n }\n name\n }\n }\n }\n': types.FetchLabsDocument, - '\n fragment ManuscriptsContent on Manuscripts {\n sys {\n id\n }\n title\n versionsCollection(limit: 20, order: sys_publishedAt_DESC) {\n items {\n sys {\n id\n publishedAt\n firstPublishedAt\n }\n type\n lifecycle\n manuscriptFile {\n sys {\n id\n }\n fileName\n url\n }\n keyResourceTable {\n sys {\n id\n }\n fileName\n url\n }\n additionalFilesCollection(limit: 10) {\n items {\n sys {\n id\n }\n fileName\n url\n }\n }\n preprintDoi\n publicationDoi\n requestingApcCoverage\n submitterName\n submissionDate\n otherDetails\n acknowledgedGrantNumber\n acknowledgedGrantNumberDetails\n asapAffiliationIncluded\n asapAffiliationIncludedDetails\n manuscriptLicense\n manuscriptLicenseDetails\n datasetsDeposited\n datasetsDepositedDetails\n codeDeposited\n codeDepositedDetails\n protocolsDeposited\n protocolsDepositedDetails\n labMaterialsRegistered\n labMaterialsRegisteredDetails\n teamsCollection(limit: 10) {\n items {\n sys {\n id\n }\n displayName\n inactiveSince\n }\n }\n labsCollection(limit: 10) {\n items {\n sys {\n id\n }\n name\n }\n }\n createdBy {\n sys {\n id\n }\n firstName\n nickname\n lastName\n alumniSinceDate\n avatar {\n url\n }\n teamsCollection(limit: 3) {\n items {\n team {\n sys {\n id\n }\n displayName\n }\n }\n }\n }\n }\n }\n }\n': + '\n fragment ManuscriptsContent on Manuscripts {\n sys {\n id\n }\n title\n versionsCollection(limit: 20, order: sys_publishedAt_DESC) {\n items {\n sys {\n id\n publishedAt\n firstPublishedAt\n }\n type\n lifecycle\n manuscriptFile {\n sys {\n id\n }\n fileName\n url\n }\n keyResourceTable {\n sys {\n id\n }\n fileName\n url\n }\n additionalFilesCollection(limit: 10) {\n items {\n sys {\n id\n }\n fileName\n url\n }\n }\n preprintDoi\n publicationDoi\n requestingApcCoverage\n submitterName\n submissionDate\n otherDetails\n acknowledgedGrantNumber\n acknowledgedGrantNumberDetails\n asapAffiliationIncluded\n asapAffiliationIncludedDetails\n manuscriptLicense\n manuscriptLicenseDetails\n datasetsDeposited\n datasetsDepositedDetails\n codeDeposited\n codeDepositedDetails\n protocolsDeposited\n protocolsDepositedDetails\n labMaterialsRegistered\n labMaterialsRegisteredDetails\n teamsCollection(limit: 10) {\n items {\n sys {\n id\n }\n displayName\n inactiveSince\n }\n }\n labsCollection(limit: 10) {\n items {\n sys {\n id\n }\n name\n }\n }\n createdBy {\n sys {\n id\n }\n firstName\n nickname\n lastName\n alumniSinceDate\n avatar {\n url\n }\n teamsCollection(limit: 3) {\n items {\n team {\n sys {\n id\n }\n displayName\n }\n }\n }\n }\n linkedFrom {\n complianceReportsCollection(limit: 1) {\n items {\n url\n description\n }\n }\n }\n }\n }\n }\n': types.ManuscriptsContentFragmentDoc, '\n query FetchManuscriptById($id: String!) {\n manuscripts(id: $id) {\n ...ManuscriptsContent\n teamsCollection(limit: 10) {\n items {\n sys {\n id\n }\n }\n }\n }\n }\n \n': types.FetchManuscriptByIdDocument, @@ -333,8 +333,8 @@ export function gql( * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function gql( - source: '\n fragment ManuscriptsContent on Manuscripts {\n sys {\n id\n }\n title\n versionsCollection(limit: 20, order: sys_publishedAt_DESC) {\n items {\n sys {\n id\n publishedAt\n firstPublishedAt\n }\n type\n lifecycle\n manuscriptFile {\n sys {\n id\n }\n fileName\n url\n }\n keyResourceTable {\n sys {\n id\n }\n fileName\n url\n }\n additionalFilesCollection(limit: 10) {\n items {\n sys {\n id\n }\n fileName\n url\n }\n }\n preprintDoi\n publicationDoi\n requestingApcCoverage\n submitterName\n submissionDate\n otherDetails\n acknowledgedGrantNumber\n acknowledgedGrantNumberDetails\n asapAffiliationIncluded\n asapAffiliationIncludedDetails\n manuscriptLicense\n manuscriptLicenseDetails\n datasetsDeposited\n datasetsDepositedDetails\n codeDeposited\n codeDepositedDetails\n protocolsDeposited\n protocolsDepositedDetails\n labMaterialsRegistered\n labMaterialsRegisteredDetails\n teamsCollection(limit: 10) {\n items {\n sys {\n id\n }\n displayName\n inactiveSince\n }\n }\n labsCollection(limit: 10) {\n items {\n sys {\n id\n }\n name\n }\n }\n createdBy {\n sys {\n id\n }\n firstName\n nickname\n lastName\n alumniSinceDate\n avatar {\n url\n }\n teamsCollection(limit: 3) {\n items {\n team {\n sys {\n id\n }\n displayName\n }\n }\n }\n }\n }\n }\n }\n', -): (typeof documents)['\n fragment ManuscriptsContent on Manuscripts {\n sys {\n id\n }\n title\n versionsCollection(limit: 20, order: sys_publishedAt_DESC) {\n items {\n sys {\n id\n publishedAt\n firstPublishedAt\n }\n type\n lifecycle\n manuscriptFile {\n sys {\n id\n }\n fileName\n url\n }\n keyResourceTable {\n sys {\n id\n }\n fileName\n url\n }\n additionalFilesCollection(limit: 10) {\n items {\n sys {\n id\n }\n fileName\n url\n }\n }\n preprintDoi\n publicationDoi\n requestingApcCoverage\n submitterName\n submissionDate\n otherDetails\n acknowledgedGrantNumber\n acknowledgedGrantNumberDetails\n asapAffiliationIncluded\n asapAffiliationIncludedDetails\n manuscriptLicense\n manuscriptLicenseDetails\n datasetsDeposited\n datasetsDepositedDetails\n codeDeposited\n codeDepositedDetails\n protocolsDeposited\n protocolsDepositedDetails\n labMaterialsRegistered\n labMaterialsRegisteredDetails\n teamsCollection(limit: 10) {\n items {\n sys {\n id\n }\n displayName\n inactiveSince\n }\n }\n labsCollection(limit: 10) {\n items {\n sys {\n id\n }\n name\n }\n }\n createdBy {\n sys {\n id\n }\n firstName\n nickname\n lastName\n alumniSinceDate\n avatar {\n url\n }\n teamsCollection(limit: 3) {\n items {\n team {\n sys {\n id\n }\n displayName\n }\n }\n }\n }\n }\n }\n }\n']; + source: '\n fragment ManuscriptsContent on Manuscripts {\n sys {\n id\n }\n title\n versionsCollection(limit: 20, order: sys_publishedAt_DESC) {\n items {\n sys {\n id\n publishedAt\n firstPublishedAt\n }\n type\n lifecycle\n manuscriptFile {\n sys {\n id\n }\n fileName\n url\n }\n keyResourceTable {\n sys {\n id\n }\n fileName\n url\n }\n additionalFilesCollection(limit: 10) {\n items {\n sys {\n id\n }\n fileName\n url\n }\n }\n preprintDoi\n publicationDoi\n requestingApcCoverage\n submitterName\n submissionDate\n otherDetails\n acknowledgedGrantNumber\n acknowledgedGrantNumberDetails\n asapAffiliationIncluded\n asapAffiliationIncludedDetails\n manuscriptLicense\n manuscriptLicenseDetails\n datasetsDeposited\n datasetsDepositedDetails\n codeDeposited\n codeDepositedDetails\n protocolsDeposited\n protocolsDepositedDetails\n labMaterialsRegistered\n labMaterialsRegisteredDetails\n teamsCollection(limit: 10) {\n items {\n sys {\n id\n }\n displayName\n inactiveSince\n }\n }\n labsCollection(limit: 10) {\n items {\n sys {\n id\n }\n name\n }\n }\n createdBy {\n sys {\n id\n }\n firstName\n nickname\n lastName\n alumniSinceDate\n avatar {\n url\n }\n teamsCollection(limit: 3) {\n items {\n team {\n sys {\n id\n }\n displayName\n }\n }\n }\n }\n linkedFrom {\n complianceReportsCollection(limit: 1) {\n items {\n url\n description\n }\n }\n }\n }\n }\n }\n', +): (typeof documents)['\n fragment ManuscriptsContent on Manuscripts {\n sys {\n id\n }\n title\n versionsCollection(limit: 20, order: sys_publishedAt_DESC) {\n items {\n sys {\n id\n publishedAt\n firstPublishedAt\n }\n type\n lifecycle\n manuscriptFile {\n sys {\n id\n }\n fileName\n url\n }\n keyResourceTable {\n sys {\n id\n }\n fileName\n url\n }\n additionalFilesCollection(limit: 10) {\n items {\n sys {\n id\n }\n fileName\n url\n }\n }\n preprintDoi\n publicationDoi\n requestingApcCoverage\n submitterName\n submissionDate\n otherDetails\n acknowledgedGrantNumber\n acknowledgedGrantNumberDetails\n asapAffiliationIncluded\n asapAffiliationIncludedDetails\n manuscriptLicense\n manuscriptLicenseDetails\n datasetsDeposited\n datasetsDepositedDetails\n codeDeposited\n codeDepositedDetails\n protocolsDeposited\n protocolsDepositedDetails\n labMaterialsRegistered\n labMaterialsRegisteredDetails\n teamsCollection(limit: 10) {\n items {\n sys {\n id\n }\n displayName\n inactiveSince\n }\n }\n labsCollection(limit: 10) {\n items {\n sys {\n id\n }\n name\n }\n }\n createdBy {\n sys {\n id\n }\n firstName\n nickname\n lastName\n alumniSinceDate\n avatar {\n url\n }\n teamsCollection(limit: 3) {\n items {\n team {\n sys {\n id\n }\n displayName\n }\n }\n }\n }\n linkedFrom {\n complianceReportsCollection(limit: 1) {\n items {\n url\n description\n }\n }\n }\n }\n }\n }\n']; /** * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ From a1edb18afd95a136f6a4fb32d3b03cdc8f5452a7 Mon Sep 17 00:00:00 2001 From: AkosuaA Date: Thu, 10 Oct 2024 19:32:07 +0100 Subject: [PATCH 10/17] add more tests for code coverage --- apps/crn-frontend/src/network/teams/TeamProfile.tsx | 1 - .../src/data-providers/contentful/manuscript.data-provider.ts | 1 - .../contentful/compliance-reports.data-provider.test.ts | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/crn-frontend/src/network/teams/TeamProfile.tsx b/apps/crn-frontend/src/network/teams/TeamProfile.tsx index a8fe0f5251..e18a460599 100644 --- a/apps/crn-frontend/src/network/teams/TeamProfile.tsx +++ b/apps/crn-frontend/src/network/teams/TeamProfile.tsx @@ -69,7 +69,6 @@ const TeamProfile: FC = ({ currentTime }) => { const { path } = useRouteMatch(); const route = network({}).teams({}).team; const [teamListElementId] = useState(`team-list-${uuid()}`); - const { teamId } = useRouteParams(route); const team = useTeamById(teamId); diff --git a/apps/crn-server/src/data-providers/contentful/manuscript.data-provider.ts b/apps/crn-server/src/data-providers/contentful/manuscript.data-provider.ts index eb84b2d1a4..f5593e497f 100644 --- a/apps/crn-server/src/data-providers/contentful/manuscript.data-provider.ts +++ b/apps/crn-server/src/data-providers/contentful/manuscript.data-provider.ts @@ -25,7 +25,6 @@ import { parseUserDisplayName } from '@asap-hub/server-common'; import { ManuscriptDataProvider } from '../types'; type ManuscriptItem = NonNullable; -// type ComplianceReport = NonNullable['items']>[number]>['linkedFrom']>['complianceReportsCollection']>['items'][number]>; type ComplianceReport = NonNullable< NonNullable< NonNullable< diff --git a/apps/crn-server/test/data-providers/contentful/compliance-reports.data-provider.test.ts b/apps/crn-server/test/data-providers/contentful/compliance-reports.data-provider.test.ts index db68f638f4..8a31679bdd 100644 --- a/apps/crn-server/test/data-providers/contentful/compliance-reports.data-provider.test.ts +++ b/apps/crn-server/test/data-providers/contentful/compliance-reports.data-provider.test.ts @@ -74,7 +74,7 @@ describe('Compliance Reports Contentful Data Provider', () => { describe('Fetch by ID', () => { test('should throw an error', async () => { - await expect(complianceReportDataProvider.fetch()).rejects.toThrow( + await expect(complianceReportDataProvider.fetchById()).rejects.toThrow( 'Method not implemented.', ); }); From a266766e31583365024124bc40ff54e7b312527b Mon Sep 17 00:00:00 2001 From: AkosuaA Date: Thu, 10 Oct 2024 22:19:39 +0100 Subject: [PATCH 11/17] update tests --- .../src/network/teams/Workspace.tsx | 5 ++- .../teams/__tests__/TeamProfile.test.tsx | 7 ++-- .../src/TeamProfileWorkspace.stories.tsx | 1 + .../src/icons/compliance-report.tsx | 2 +- .../src/organisms/ManuscriptCard.tsx | 6 +-- .../__tests__/ManuscriptCard.test.tsx | 38 +++++++++++++++++++ .../src/templates/TeamProfileWorkspace.tsx | 8 +++- .../__tests__/TeamProfileWorkspace.test.tsx | 1 + 8 files changed, 58 insertions(+), 10 deletions(-) diff --git a/apps/crn-frontend/src/network/teams/Workspace.tsx b/apps/crn-frontend/src/network/teams/Workspace.tsx index 9d9d72ac5e..710518ee57 100644 --- a/apps/crn-frontend/src/network/teams/Workspace.tsx +++ b/apps/crn-frontend/src/network/teams/Workspace.tsx @@ -7,7 +7,7 @@ import { } from '@asap-hub/react-components'; import { TeamTool, TeamResponse } from '@asap-hub/model'; import { network, useRouteParams } from '@asap-hub/routing'; -import { ToastContext } from '@asap-hub/react-context'; +import { ToastContext, useCurrentUserCRN } from '@asap-hub/react-context'; import { usePatchTeamById } from './state'; import { useEligibilityReason } from './useEligibilityReason'; @@ -19,6 +19,8 @@ const Workspace: React.FC = ({ team }) => { const route = network({}).teams({}).team({ teamId: team.id }).workspace({}); const { path } = useRouteMatch(); const { setEligibilityReasons } = useEligibilityReason(); + const { role } = useCurrentUserCRN() ?? {}; + const canShareComplianceReport = role === 'Staff'; const [deleting, setDeleting] = useState(false); const patchTeam = usePatchTeamById(team.id); @@ -50,6 +52,7 @@ const Workspace: React.FC = ({ team }) => { setDeleting(false); } } + canShareComplianceReport={canShareComplianceReport} /> diff --git a/apps/crn-frontend/src/network/teams/__tests__/TeamProfile.test.tsx b/apps/crn-frontend/src/network/teams/__tests__/TeamProfile.test.tsx index 757e38b64a..4ccf9e31a0 100644 --- a/apps/crn-frontend/src/network/teams/__tests__/TeamProfile.test.tsx +++ b/apps/crn-frontend/src/network/teams/__tests__/TeamProfile.test.tsx @@ -491,7 +491,7 @@ describe('Duplicate Output', () => { }); describe('Create Compliance Report', () => { - it('allows a user who is an ASAP staff to view Submit Compliance Report button', async () => { + it('allows a user who is an ASAP staff to view Share Compliance Report button', async () => { enable('DISPLAY_MANUSCRIPTS'); const teamResponse = createTeamResponse(); const userResponse = createUserResponse({}, 1); @@ -521,7 +521,7 @@ describe('Create Compliance Report', () => { ); expect( - screen.getByRole('button', { name: /Submit Compliance Report Icon/ }), + screen.getByRole('button', { name: /Share Compliance Report Icon/ }), ).toBeInTheDocument(); }); @@ -555,10 +555,9 @@ describe('Create Compliance Report', () => { ); userEvent.click( - screen.getByRole('button', { name: /Submit Compliance Report Icon/ }), + screen.getByRole('button', { name: /Share Compliance Report Icon/ }), ); - // expect(await screen.findByText(/Output 1/i)).toBeVisible(); expect( await screen.findByText( /Share the compliance report associated with this manuscript./, diff --git a/apps/storybook/src/TeamProfileWorkspace.stories.tsx b/apps/storybook/src/TeamProfileWorkspace.stories.tsx index 8c62ed85a1..37e666b02f 100644 --- a/apps/storybook/src/TeamProfileWorkspace.stories.tsx +++ b/apps/storybook/src/TeamProfileWorkspace.stories.tsx @@ -17,5 +17,6 @@ export const Normal = () => ( description: 'Tool Description', }, ]} + canShareComplianceReport={false} /> ); diff --git a/packages/react-components/src/icons/compliance-report.tsx b/packages/react-components/src/icons/compliance-report.tsx index df3dec0ee3..eaaae9766f 100644 --- a/packages/react-components/src/icons/compliance-report.tsx +++ b/packages/react-components/src/icons/compliance-report.tsx @@ -2,7 +2,7 @@ const complianceReportIcon = ( - Submit Compliance Report Icon + Share Compliance Report Icon & { teamId: string; + canShareComplianceReport: boolean; }; const manuscriptContainerStyles = css({ @@ -64,6 +64,7 @@ const ManuscriptCard: React.FC = ({ title, versions, teamId, + canShareComplianceReport = false, }) => { const [expanded, setExpanded] = useState(false); const history = useHistory(); @@ -78,7 +79,6 @@ const ManuscriptCard: React.FC = ({ history.push(complianceReportRoute); }; - const { role } = useCurrentUserCRN() ?? {}; const hasActiveComplianceReport = !!versions[0]?.complianceReport; return ( @@ -97,7 +97,7 @@ const ManuscriptCard: React.FC = ({ {title} - {role === 'Staff' && ( + {canShareComplianceReport && (
{manuscripts.map((manuscript) => (
- +
))} diff --git a/packages/react-components/src/templates/__tests__/TeamProfileWorkspace.test.tsx b/packages/react-components/src/templates/__tests__/TeamProfileWorkspace.test.tsx index d2d64516d5..8e632ed2d5 100644 --- a/packages/react-components/src/templates/__tests__/TeamProfileWorkspace.test.tsx +++ b/packages/react-components/src/templates/__tests__/TeamProfileWorkspace.test.tsx @@ -20,6 +20,7 @@ const team: ComponentProps = { ...createTeamResponse({ teamMembers: 1, tools: 0 }), setEligibilityReasons: jest.fn(), tools: [], + canShareComplianceReport: false, }; it('renders the team workspace page', () => { const { getByRole } = render(); From 32303035901ec6298e24f9d02e90ab36f6fe07fa Mon Sep 17 00:00:00 2001 From: AkosuaA Date: Fri, 11 Oct 2024 09:42:56 +0100 Subject: [PATCH 12/17] update props for ManuscriptCard --- packages/react-components/src/organisms/ManuscriptCard.tsx | 2 +- .../src/templates/__tests__/TeamProfileWorkspace.test.tsx | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/react-components/src/organisms/ManuscriptCard.tsx b/packages/react-components/src/organisms/ManuscriptCard.tsx index 084f72b210..7fe237e1e2 100644 --- a/packages/react-components/src/organisms/ManuscriptCard.tsx +++ b/packages/react-components/src/organisms/ManuscriptCard.tsx @@ -64,7 +64,7 @@ const ManuscriptCard: React.FC = ({ title, versions, teamId, - canShareComplianceReport = false, + canShareComplianceReport, }) => { const [expanded, setExpanded] = useState(false); const history = useHistory(); diff --git a/packages/react-components/src/templates/__tests__/TeamProfileWorkspace.test.tsx b/packages/react-components/src/templates/__tests__/TeamProfileWorkspace.test.tsx index 8e632ed2d5..d2d64516d5 100644 --- a/packages/react-components/src/templates/__tests__/TeamProfileWorkspace.test.tsx +++ b/packages/react-components/src/templates/__tests__/TeamProfileWorkspace.test.tsx @@ -20,7 +20,6 @@ const team: ComponentProps = { ...createTeamResponse({ teamMembers: 1, tools: 0 }), setEligibilityReasons: jest.fn(), tools: [], - canShareComplianceReport: false, }; it('renders the team workspace page', () => { const { getByRole } = render(); From 8e5f4517f2c1704f21f3ec4b91c99cb7209b7ee5 Mon Sep 17 00:00:00 2001 From: AkosuaA Date: Fri, 11 Oct 2024 11:43:18 +0100 Subject: [PATCH 13/17] minor changes --- .../network/teams/ManuscriptToastProvider.tsx | 18 +++++++----------- .../src/network/teams/TeamComplianceReport.tsx | 4 ++-- .../src/network/teams/TeamManuscript.tsx | 4 ++-- .../src/network/teams/TeamProfile.tsx | 4 ++-- .../src/network/teams/Workspace.tsx | 7 +++---- apps/crn-frontend/src/network/teams/state.ts | 2 +- .../src/icons/compliance-report.tsx | 1 + .../src/organisms/ComplianceReportCard.tsx | 3 ++- 8 files changed, 20 insertions(+), 23 deletions(-) diff --git a/apps/crn-frontend/src/network/teams/ManuscriptToastProvider.tsx b/apps/crn-frontend/src/network/teams/ManuscriptToastProvider.tsx index fb3dded560..0c5e6145b5 100644 --- a/apps/crn-frontend/src/network/teams/ManuscriptToastProvider.tsx +++ b/apps/crn-frontend/src/network/teams/ManuscriptToastProvider.tsx @@ -1,13 +1,11 @@ import { Toast } from '@asap-hub/react-components'; import React, { createContext, useState } from 'react'; -type ComplianceFormType = 'manuscript' | 'compliance-report' | ''; +type FormType = 'manuscript' | 'compliance-report' | ''; type ManuscriptToastContextData = { setShowSuccessBanner: React.Dispatch>; - setComplianceFormType: React.Dispatch< - React.SetStateAction - >; + setFormType: React.Dispatch>; }; export const ManuscriptToastContext = createContext( @@ -20,26 +18,24 @@ export const ManuscriptToastProvider = ({ children: React.ReactNode; }) => { const [showSuccessBanner, setShowSuccessBanner] = useState(false); - const [complianceFormType, setComplianceFormType] = - useState(''); + const [formType, setFormType] = useState(''); - const complianceFormTypeMapping = { + const formTypeMapping = { manuscript: 'Manuscript', 'compliance-report': 'Compliance Report', }; return ( <> - {showSuccessBanner && !!complianceFormType && ( + {showSuccessBanner && !!formType && ( setShowSuccessBanner(false)} > - {complianceFormTypeMapping[complianceFormType]} submitted - successfully. + {formTypeMapping[formType]} submitted successfully. )} {children} diff --git a/apps/crn-frontend/src/network/teams/TeamComplianceReport.tsx b/apps/crn-frontend/src/network/teams/TeamComplianceReport.tsx index 36755c4ed4..9c778e54ac 100644 --- a/apps/crn-frontend/src/network/teams/TeamComplianceReport.tsx +++ b/apps/crn-frontend/src/network/teams/TeamComplianceReport.tsx @@ -24,7 +24,7 @@ const TeamComplianceReport: React.FC = ({ }) => { const { manuscriptId } = useParams<{ manuscriptId: string }>(); const manuscript = useManuscriptById(manuscriptId); - const { setShowSuccessBanner, setComplianceFormType } = useManuscriptToast(); + const { setShowSuccessBanner, setFormType } = useManuscriptToast(); const pushFromHere = usePushFromHere(); @@ -36,7 +36,7 @@ const TeamComplianceReport: React.FC = ({ const onSuccess = () => { const path = network({}).teams({}).team({ teamId }).workspace({}).$; setShowSuccessBanner(true); - setComplianceFormType('compliance-report'); + setFormType('compliance-report'); setRefreshTeamState((value) => value + 1); pushFromHere(path); }; diff --git a/apps/crn-frontend/src/network/teams/TeamManuscript.tsx b/apps/crn-frontend/src/network/teams/TeamManuscript.tsx index edbfdb2607..c9ab101585 100644 --- a/apps/crn-frontend/src/network/teams/TeamManuscript.tsx +++ b/apps/crn-frontend/src/network/teams/TeamManuscript.tsx @@ -30,7 +30,7 @@ const TeamManuscript: React.FC = ({ teamId }) => { const team = useTeamById(teamId); const { eligibilityReasons } = useEligibilityReason(); - const { setShowSuccessBanner, setComplianceFormType } = useManuscriptToast(); + const { setShowSuccessBanner, setFormType } = useManuscriptToast(); const form = useForm(); const createManuscript = usePostManuscript(); const handleFileUpload = useUploadManuscriptFile(); @@ -43,7 +43,7 @@ const TeamManuscript: React.FC = ({ teamId }) => { const onSuccess = () => { const path = network({}).teams({}).team({ teamId }).workspace({}).$; setShowSuccessBanner(true); - setComplianceFormType('manuscript'); + setFormType('manuscript'); setRefreshTeamState((value) => value + 1); pushFromHere(path); }; diff --git a/apps/crn-frontend/src/network/teams/TeamProfile.tsx b/apps/crn-frontend/src/network/teams/TeamProfile.tsx index e18a460599..bcf05a0591 100644 --- a/apps/crn-frontend/src/network/teams/TeamProfile.tsx +++ b/apps/crn-frontend/src/network/teams/TeamProfile.tsx @@ -19,7 +19,7 @@ import { useUpcomingAndPastEvents } from '../events'; import ProfileSwitch from '../ProfileSwitch'; import { ManuscriptToastProvider } from './ManuscriptToastProvider'; -import { useCanCreateComplianceReport, useTeamById } from './state'; +import { useCanShareComplianceReport, useTeamById } from './state'; import TeamManuscript from './TeamManuscript'; import { EligibilityReasonProvider } from './EligibilityReasonProvider'; import TeamComplianceReport from './TeamComplianceReport'; @@ -90,7 +90,7 @@ const TeamProfile: FC = ({ currentTime }) => { teamId, ]); - const canCreateComplianceReport = useCanCreateComplianceReport(); + const canCreateComplianceReport = useCanShareComplianceReport(); const [upcomingEvents, pastEvents] = useUpcomingAndPastEvents(currentTime, { teamId, }); diff --git a/apps/crn-frontend/src/network/teams/Workspace.tsx b/apps/crn-frontend/src/network/teams/Workspace.tsx index 710518ee57..ee7d6f654a 100644 --- a/apps/crn-frontend/src/network/teams/Workspace.tsx +++ b/apps/crn-frontend/src/network/teams/Workspace.tsx @@ -7,9 +7,9 @@ import { } from '@asap-hub/react-components'; import { TeamTool, TeamResponse } from '@asap-hub/model'; import { network, useRouteParams } from '@asap-hub/routing'; -import { ToastContext, useCurrentUserCRN } from '@asap-hub/react-context'; +import { ToastContext } from '@asap-hub/react-context'; -import { usePatchTeamById } from './state'; +import { useCanShareComplianceReport, usePatchTeamById } from './state'; import { useEligibilityReason } from './useEligibilityReason'; interface WorkspaceProps { @@ -19,8 +19,7 @@ const Workspace: React.FC = ({ team }) => { const route = network({}).teams({}).team({ teamId: team.id }).workspace({}); const { path } = useRouteMatch(); const { setEligibilityReasons } = useEligibilityReason(); - const { role } = useCurrentUserCRN() ?? {}; - const canShareComplianceReport = role === 'Staff'; + const canShareComplianceReport = useCanShareComplianceReport(); const [deleting, setDeleting] = useState(false); const patchTeam = usePatchTeamById(team.id); diff --git a/apps/crn-frontend/src/network/teams/state.ts b/apps/crn-frontend/src/network/teams/state.ts index e67654b4fd..9bdeacd1bb 100644 --- a/apps/crn-frontend/src/network/teams/state.ts +++ b/apps/crn-frontend/src/network/teams/state.ts @@ -212,7 +212,7 @@ export const usePostComplianceReport = () => { }; }; -export const useCanCreateComplianceReport = (): boolean => { +export const useCanShareComplianceReport = (): boolean => { const { role } = useCurrentUserCRN() ?? {}; return role === 'Staff'; }; diff --git a/packages/react-components/src/icons/compliance-report.tsx b/packages/react-components/src/icons/compliance-report.tsx index eaaae9766f..42ea4f3773 100644 --- a/packages/react-components/src/icons/compliance-report.tsx +++ b/packages/react-components/src/icons/compliance-report.tsx @@ -8,6 +8,7 @@ const complianceReportIcon = ( clipRule="evenodd" d="M3.8 21.5015C3.8 21.8881 4.1134 22.2015 4.5 22.2015H13.85C14.209 22.2015 14.5 22.4925 14.5 22.8515C14.5 23.2105 14.209 23.5015 13.85 23.5015H4.5C3.39543 23.5015 2.5 22.606 2.5 21.5015V2.5C2.5 1.39543 3.39543 0.5 4.5 0.5L18.5 0.501472C19.6046 0.501472 20.5 1.3969 20.5 2.50147V13.3515C20.5 13.7105 20.209 14.0015 19.85 14.0015C19.491 14.0015 19.2 13.7105 19.2 13.3515V2.50147C19.2 2.11487 18.8866 1.80147 18.5 1.80147L4.5 1.8C4.1134 1.8 3.8 2.1134 3.8 2.5V21.5015ZM7 4.00147C6.64102 4.00147 6.35 4.29249 6.35 4.65147C6.35 5.01046 6.64102 5.30147 7 5.30147H16.1C16.459 5.30147 16.75 5.01046 16.75 4.65147C16.75 4.29249 16.459 4.00147 16.1 4.00147H7ZM9.06042 7.56181C9.8125 7.19088 10.6392 6.99877 11.4768 7.00029C12.6879 6.98776 13.8688 7.38016 14.8342 8.11589C15.7996 8.85162 16.4948 9.88902 16.8107 11.0653C17.1267 12.2415 17.0454 13.49 16.5797 14.6147C16.114 15.7394 15.2902 16.6768 14.2377 17.2795H14.172C13.4442 17.6965 12.6308 17.9396 11.7947 17.9901C10.9586 18.0406 10.1222 17.8972 9.34991 17.5709C8.57767 17.2446 7.89027 16.7441 7.34079 16.1081C6.79131 15.4722 6.39444 14.7177 6.18081 13.9029C5.96718 13.0881 5.9425 12.2349 6.10868 11.409C6.27485 10.5831 6.62745 9.80665 7.13924 9.1396C7.65103 8.47256 8.30833 7.93274 9.06042 7.56181ZM14.5659 9.3925C13.8801 8.70255 12.9857 8.26151 12.0235 8.13876V11.9502H15.8121C15.6901 10.9822 15.2517 10.0825 14.5659 9.3925ZM8.50169 15.6971C9.30628 16.4625 10.3695 16.8924 11.4768 16.9C12.0538 16.8998 12.6248 16.7819 13.1552 16.5535L10.9684 12.6926C10.9452 12.631 10.9323 12.5658 10.9301 12.5L10.9301 8.13876C9.83246 8.28561 8.831 8.84589 8.12789 9.70653C7.42477 10.5672 7.07234 11.6641 7.14171 12.7759C7.21109 13.8877 7.69711 14.9317 8.50169 15.6971ZM12.4226 13.0501L14.1174 15.9981C14.5804 15.6446 14.9688 15.2019 15.2598 14.6957C15.5507 14.1896 15.7385 13.6301 15.8121 13.0501H12.4226Z" fill="currentColor" + strokeWidth="0" /> = ({ {expanded && (
-
{description}
+
{externalLinkIcon} View Report From b81da271fb063778732b0592f875458a5495274c Mon Sep 17 00:00:00 2001 From: AkosuaA Date: Mon, 14 Oct 2024 12:10:01 +0100 Subject: [PATCH 14/17] updates from code review --- .../network/teams/ManuscriptToastProvider.tsx | 13 +++-------- .../network/teams/TeamComplianceReport.tsx | 3 +-- .../src/network/teams/TeamManuscript.tsx | 3 +-- .../src/organisms/ComplianceReportCard.tsx | 23 ++++++++++++++++--- .../src/organisms/ManuscriptCard.tsx | 2 +- .../__tests__/ManuscriptCard.test.tsx | 2 +- .../src/templates/ComplianceReportForm.tsx | 4 ++-- 7 files changed, 29 insertions(+), 21 deletions(-) diff --git a/apps/crn-frontend/src/network/teams/ManuscriptToastProvider.tsx b/apps/crn-frontend/src/network/teams/ManuscriptToastProvider.tsx index 0c5e6145b5..695271d5e5 100644 --- a/apps/crn-frontend/src/network/teams/ManuscriptToastProvider.tsx +++ b/apps/crn-frontend/src/network/teams/ManuscriptToastProvider.tsx @@ -4,7 +4,6 @@ import React, { createContext, useState } from 'react'; type FormType = 'manuscript' | 'compliance-report' | ''; type ManuscriptToastContextData = { - setShowSuccessBanner: React.Dispatch>; setFormType: React.Dispatch>; }; @@ -17,7 +16,6 @@ export const ManuscriptToastProvider = ({ }: { children: React.ReactNode; }) => { - const [showSuccessBanner, setShowSuccessBanner] = useState(false); const [formType, setFormType] = useState(''); const formTypeMapping = { @@ -26,15 +24,10 @@ export const ManuscriptToastProvider = ({ }; return ( - + <> - {showSuccessBanner && !!formType && ( - setShowSuccessBanner(false)} - > + {!!formType && ( + setFormType('')}> {formTypeMapping[formType]} submitted successfully. )} diff --git a/apps/crn-frontend/src/network/teams/TeamComplianceReport.tsx b/apps/crn-frontend/src/network/teams/TeamComplianceReport.tsx index 9c778e54ac..b15c14a45f 100644 --- a/apps/crn-frontend/src/network/teams/TeamComplianceReport.tsx +++ b/apps/crn-frontend/src/network/teams/TeamComplianceReport.tsx @@ -24,7 +24,7 @@ const TeamComplianceReport: React.FC = ({ }) => { const { manuscriptId } = useParams<{ manuscriptId: string }>(); const manuscript = useManuscriptById(manuscriptId); - const { setShowSuccessBanner, setFormType } = useManuscriptToast(); + const { setFormType } = useManuscriptToast(); const pushFromHere = usePushFromHere(); @@ -35,7 +35,6 @@ const TeamComplianceReport: React.FC = ({ if (manuscript && manuscript.versions[0]) { const onSuccess = () => { const path = network({}).teams({}).team({ teamId }).workspace({}).$; - setShowSuccessBanner(true); setFormType('compliance-report'); setRefreshTeamState((value) => value + 1); pushFromHere(path); diff --git a/apps/crn-frontend/src/network/teams/TeamManuscript.tsx b/apps/crn-frontend/src/network/teams/TeamManuscript.tsx index c9ab101585..8c898f1383 100644 --- a/apps/crn-frontend/src/network/teams/TeamManuscript.tsx +++ b/apps/crn-frontend/src/network/teams/TeamManuscript.tsx @@ -30,7 +30,7 @@ const TeamManuscript: React.FC = ({ teamId }) => { const team = useTeamById(teamId); const { eligibilityReasons } = useEligibilityReason(); - const { setShowSuccessBanner, setFormType } = useManuscriptToast(); + const { setFormType } = useManuscriptToast(); const form = useForm(); const createManuscript = usePostManuscript(); const handleFileUpload = useUploadManuscriptFile(); @@ -42,7 +42,6 @@ const TeamManuscript: React.FC = ({ teamId }) => { const onSuccess = () => { const path = network({}).teams({}).team({ teamId }).workspace({}).$; - setShowSuccessBanner(true); setFormType('manuscript'); setRefreshTeamState((value) => value + 1); pushFromHere(path); diff --git a/packages/react-components/src/organisms/ComplianceReportCard.tsx b/packages/react-components/src/organisms/ComplianceReportCard.tsx index 6908c6a6a3..d825696e4f 100644 --- a/packages/react-components/src/organisms/ComplianceReportCard.tsx +++ b/packages/react-components/src/organisms/ComplianceReportCard.tsx @@ -43,6 +43,21 @@ const toastContentStyles = css({ paddingTop: rem(15), }); +const externalIconStyle = css({ + display: 'flex', + alignSelf: 'center', + gap: rem(8), + paddingRight: rem(8), + textWrap: 'nowrap', +}); + +const buttonStyles = css({ + width: rem(151), + '> a': { + height: rem(40), + }, +}); + const ComplianceReportCard: React.FC = ({ url, description, @@ -66,9 +81,11 @@ const ComplianceReportCard: React.FC = ({
-
- - {externalLinkIcon} View Report +
+ + + {externalLinkIcon} View Report +
diff --git a/packages/react-components/src/organisms/ManuscriptCard.tsx b/packages/react-components/src/organisms/ManuscriptCard.tsx index 7fe237e1e2..ce481e4609 100644 --- a/packages/react-components/src/organisms/ManuscriptCard.tsx +++ b/packages/react-components/src/organisms/ManuscriptCard.tsx @@ -106,7 +106,7 @@ const ManuscriptCard: React.FC = ({ onClick={handleShareComplianceReport} enabled={!hasActiveComplianceReport} > - svg': { stroke: 'none' } }}> + svg': { stroke: 'none' }, height: rem(24) }}> {complianceReportIcon} diff --git a/packages/react-components/src/organisms/__tests__/ManuscriptCard.test.tsx b/packages/react-components/src/organisms/__tests__/ManuscriptCard.test.tsx index c83f99775e..aa5e6d2d2c 100644 --- a/packages/react-components/src/organisms/__tests__/ManuscriptCard.test.tsx +++ b/packages/react-components/src/organisms/__tests__/ManuscriptCard.test.tsx @@ -55,7 +55,7 @@ it('displays share compliance report button if user has permission', () => { ).toBeVisible(); }); -it('redirects to manuscript form when user clicks on share compliance report button', () => { +it('redirects to compliance report form when user clicks on share compliance report button', () => { const history = createMemoryHistory({}); const { getByRole } = render( diff --git a/packages/react-components/src/templates/ComplianceReportForm.tsx b/packages/react-components/src/templates/ComplianceReportForm.tsx index bb0555d7f3..e85e4c0b84 100644 --- a/packages/react-components/src/templates/ComplianceReportForm.tsx +++ b/packages/react-components/src/templates/ComplianceReportForm.tsx @@ -83,7 +83,7 @@ const ComplianceReportForm: React.FC = ({ const { handleSubmit, control, - formState: { isSubmitting }, + formState: { isSubmitting, isValid }, } = methods; const onSubmit = async (data: ComplianceReportFormData) => { @@ -172,7 +172,7 @@ const ComplianceReportForm: React.FC = ({ primary noMargin submit - enabled={!isSubmitting} + enabled={!isSubmitting && isValid} preventDefault={false} > Share From 54606cf4ddb1ce971b905dd8b07af696de8909ba Mon Sep 17 00:00:00 2001 From: AkosuaA Date: Mon, 14 Oct 2024 12:58:12 +0100 Subject: [PATCH 15/17] update test to wait for enabled share button --- .../src/network/teams/__tests__/TeamComplianceReport.test.tsx | 1 + .../src/templates/__tests__/ComplianceReportForm.test.tsx | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/crn-frontend/src/network/teams/__tests__/TeamComplianceReport.test.tsx b/apps/crn-frontend/src/network/teams/__tests__/TeamComplianceReport.test.tsx index ec583d9f24..c1d991ab74 100644 --- a/apps/crn-frontend/src/network/teams/__tests__/TeamComplianceReport.test.tsx +++ b/apps/crn-frontend/src/network/teams/__tests__/TeamComplianceReport.test.tsx @@ -121,6 +121,7 @@ it('can publish a form when the data is valid and navigates to team workspace', ); const shareButton = screen.getByRole('button', { name: /Share/i }); + await waitFor(() => expect(shareButton).toBeEnabled()); userEvent.click(shareButton); diff --git a/packages/react-components/src/templates/__tests__/ComplianceReportForm.test.tsx b/packages/react-components/src/templates/__tests__/ComplianceReportForm.test.tsx index e9a56a5cf1..e4fab05681 100644 --- a/packages/react-components/src/templates/__tests__/ComplianceReportForm.test.tsx +++ b/packages/react-components/src/templates/__tests__/ComplianceReportForm.test.tsx @@ -41,7 +41,9 @@ it('data is sent on form submission', async () => { , ); - userEvent.click(screen.getByRole('button', { name: /Share/i })); + const shareButton = screen.getByRole('button', { name: /Share/i }); + await waitFor(() => expect(shareButton).toBeEnabled()); + userEvent.click(shareButton); await waitFor(() => { expect(onSave).toHaveBeenCalledWith({ url: 'http://example.com', From 1c1c90e500452978b4f03d82f9725e6eea5fe404 Mon Sep 17 00:00:00 2001 From: AkosuaA Date: Mon, 14 Oct 2024 15:11:53 +0100 Subject: [PATCH 16/17] allow validation onBlur --- .../react-components/src/atoms/TextArea.tsx | 3 ++ .../react-components/src/atoms/TextField.tsx | 5 ++- .../src/templates/ComplianceReportForm.tsx | 6 ++-- .../__tests__/ComplianceReportForm.test.tsx | 36 +++++++------------ 4 files changed, 24 insertions(+), 26 deletions(-) diff --git a/packages/react-components/src/atoms/TextArea.tsx b/packages/react-components/src/atoms/TextArea.tsx index c66f3ce0e3..66452b7e75 100644 --- a/packages/react-components/src/atoms/TextArea.tsx +++ b/packages/react-components/src/atoms/TextArea.tsx @@ -68,6 +68,7 @@ type TextAreaProps = { readonly value: string; readonly onChange?: (newValue: string) => void; + readonly onBlur?: (newValue: string) => void; readonly extras?: React.ReactNode; } & Pick< @@ -85,6 +86,7 @@ const TextArea: React.FC = ({ value, onChange = noop, + onBlur = noop, extras, @@ -111,6 +113,7 @@ const TextArea: React.FC = ({ onChange={({ currentTarget: { value: newValue } }) => onChange(newValue) } + onBlur={({ currentTarget: { value: newValue } }) => onBlur(newValue)} css={({ colors }) => [ styles, textareaStyles, diff --git a/packages/react-components/src/atoms/TextField.tsx b/packages/react-components/src/atoms/TextField.tsx index a7c08b7811..4b1f26ecb6 100644 --- a/packages/react-components/src/atoms/TextField.tsx +++ b/packages/react-components/src/atoms/TextField.tsx @@ -138,9 +138,10 @@ type TextFieldProps = { readonly value: string; readonly onChange?: (newValue: string) => void; + readonly onBlur?: (newValue: string) => void; } & Pick< InputHTMLAttributes, - 'id' | 'placeholder' | 'required' | 'maxLength' | 'pattern' | 'max' | 'onBlur' + 'id' | 'placeholder' | 'required' | 'maxLength' | 'pattern' | 'max' >; const TextField: React.FC = ({ type = 'text', @@ -162,6 +163,7 @@ const TextField: React.FC = ({ value, onChange = noop, + onBlur = noop, ...props }) => { @@ -186,6 +188,7 @@ const TextField: React.FC = ({ onChange={({ currentTarget: { value: newValue } }) => onChange(newValue) } + onBlur={({ currentTarget: { value: newValue } }) => onBlur(newValue)} css={({ colors }) => [ styles, textFieldStyles(colors), diff --git a/packages/react-components/src/templates/ComplianceReportForm.tsx b/packages/react-components/src/templates/ComplianceReportForm.tsx index e85e4c0b84..91a9e84af2 100644 --- a/packages/react-components/src/templates/ComplianceReportForm.tsx +++ b/packages/react-components/src/templates/ComplianceReportForm.tsx @@ -116,13 +116,14 @@ const ComplianceReportForm: React.FC = ({ required: 'Please enter a url.', }} render={({ - field: { value, onChange }, + field: { value, onBlur, onChange }, fieldState: { error }, }) => ( = ({ required: 'Please enter a description.', }} render={({ - field: { value, onChange }, + field: { value, onBlur, onChange }, fieldState: { error }, }) => ( = ({ customValidationMessage={error?.message} value={value || ''} onChange={onChange} + onBlur={onBlur} enabled={!isSubmitting} /> )} diff --git a/packages/react-components/src/templates/__tests__/ComplianceReportForm.test.tsx b/packages/react-components/src/templates/__tests__/ComplianceReportForm.test.tsx index e4fab05681..8c0df86cd2 100644 --- a/packages/react-components/src/templates/__tests__/ComplianceReportForm.test.tsx +++ b/packages/react-components/src/templates/__tests__/ComplianceReportForm.test.tsx @@ -1,4 +1,4 @@ -import { render, screen, waitFor } from '@testing-library/react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { ComponentProps } from 'react'; import { MemoryRouter, Route, Router, StaticRouter } from 'react-router-dom'; import { createMemoryHistory, History } from 'history'; @@ -61,25 +61,20 @@ it('displays error message when url is missing', async () => { ); const input = screen.getByRole('textbox', { name: /url/i }); - const shareButton = screen.getByRole('button', { name: /Share/i }); - - userEvent.click(shareButton); + fireEvent.blur(input); await waitFor(() => { - expect(shareButton).toBeEnabled(); + expect( + screen.getAllByText(/Please enter a url/i).length, + ).toBeGreaterThanOrEqual(1); }); - expect( - screen.getAllByText(/Please enter a url/i).length, - ).toBeGreaterThanOrEqual(1); userEvent.type(input, 'http://example.com'); - - userEvent.click(shareButton); + fireEvent.blur(input); await waitFor(() => { - expect(shareButton).toBeEnabled(); + expect(screen.queryByText(/Please enter a url/i)).toBeNull(); }); - expect(screen.queryByText(/Please enter a url/i)).toBeNull(); }); it('displays error message when description is missing', async () => { @@ -92,25 +87,20 @@ it('displays error message when description is missing', async () => { const input = screen.getByRole('textbox', { name: /compliance report description/i, }); - const shareButton = screen.getByRole('button', { name: /Share/i }); - - userEvent.click(shareButton); + fireEvent.blur(input); await waitFor(() => { - expect(shareButton).toBeEnabled(); + expect( + screen.getAllByText(/Please enter a description/i).length, + ).toBeGreaterThanOrEqual(1); }); - expect( - screen.getAllByText(/Please enter a description/i).length, - ).toBeGreaterThanOrEqual(1); userEvent.type(input, 'manuscription description'); - - userEvent.click(shareButton); + fireEvent.blur(input); await waitFor(() => { - expect(shareButton).toBeEnabled(); + expect(screen.queryByText(/Please enter a description/i)).toBeNull(); }); - expect(screen.queryByText(/Please enter a description/i)).toBeNull(); }); it('should go back when cancel button is clicked', () => { From 7b4605b77284a4998be6cdc777ea27afef929ca6 Mon Sep 17 00:00:00 2001 From: AkosuaA Date: Mon, 14 Oct 2024 16:30:04 +0100 Subject: [PATCH 17/17] conditional onBlur for older forms --- packages/react-components/src/atoms/TextArea.tsx | 7 +++---- packages/react-components/src/atoms/TextField.tsx | 7 +++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/react-components/src/atoms/TextArea.tsx b/packages/react-components/src/atoms/TextArea.tsx index 66452b7e75..be0ee932be 100644 --- a/packages/react-components/src/atoms/TextArea.tsx +++ b/packages/react-components/src/atoms/TextArea.tsx @@ -68,12 +68,11 @@ type TextAreaProps = { readonly value: string; readonly onChange?: (newValue: string) => void; - readonly onBlur?: (newValue: string) => void; readonly extras?: React.ReactNode; } & Pick< InputHTMLAttributes, - 'id' | 'placeholder' | 'required' | 'maxLength' + 'id' | 'placeholder' | 'required' | 'maxLength' | 'onBlur' >; const TextArea: React.FC = ({ enabled = true, @@ -86,7 +85,7 @@ const TextArea: React.FC = ({ value, onChange = noop, - onBlur = noop, + onBlur, extras, @@ -113,7 +112,6 @@ const TextArea: React.FC = ({ onChange={({ currentTarget: { value: newValue } }) => onChange(newValue) } - onBlur={({ currentTarget: { value: newValue } }) => onBlur(newValue)} css={({ colors }) => [ styles, textareaStyles, @@ -125,6 +123,7 @@ const TextArea: React.FC = ({ }, }, ]} + {...(onBlur ? { onBlur } : {})} />
diff --git a/packages/react-components/src/atoms/TextField.tsx b/packages/react-components/src/atoms/TextField.tsx index 4b1f26ecb6..f156e860ec 100644 --- a/packages/react-components/src/atoms/TextField.tsx +++ b/packages/react-components/src/atoms/TextField.tsx @@ -138,10 +138,9 @@ type TextFieldProps = { readonly value: string; readonly onChange?: (newValue: string) => void; - readonly onBlur?: (newValue: string) => void; } & Pick< InputHTMLAttributes, - 'id' | 'placeholder' | 'required' | 'maxLength' | 'pattern' | 'max' + 'id' | 'placeholder' | 'required' | 'maxLength' | 'pattern' | 'max' | 'onBlur' >; const TextField: React.FC = ({ type = 'text', @@ -163,7 +162,7 @@ const TextField: React.FC = ({ value, onChange = noop, - onBlur = noop, + onBlur, ...props }) => { @@ -188,7 +187,6 @@ const TextField: React.FC = ({ onChange={({ currentTarget: { value: newValue } }) => onChange(newValue) } - onBlur={({ currentTarget: { value: newValue } }) => onBlur(newValue)} css={({ colors }) => [ styles, textFieldStyles(colors), @@ -208,6 +206,7 @@ const TextField: React.FC = ({ ':focus': { borderColor: colors.primary500.rgba }, }, ]} + {...(onBlur ? { onBlur } : {})} /> {labelIndicator && (