diff --git a/apps/e2e/cypress/e2e/proposalPdfTemplates.cy.ts b/apps/e2e/cypress/e2e/proposalPdfTemplates.cy.ts
index aa2367c4da..4a9884bfd6 100644
--- a/apps/e2e/cypress/e2e/proposalPdfTemplates.cy.ts
+++ b/apps/e2e/cypress/e2e/proposalPdfTemplates.cy.ts
@@ -98,6 +98,538 @@ context('Proposal PDF template tests', () => {
createdTemplateName = result.createTemplate.name;
}
});
+
+ cy.updateProposalPdfTemplate({
+ proposalPdfTemplateId: 1,
+ dummyData: `{
+ "data": {
+ "proposal": {
+ "primaryKey": 4,
+ "title": "New Proposal",
+ "abstract": "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum",
+ "proposerId": 1,
+ "statusId": 1,
+ "created": "2023-10-18T11:38:15.429Z",
+ "updated": "2023-10-18T11:39:02.691Z",
+ "proposalId": "268490",
+ "callId": 1,
+ "questionaryId": 4,
+ "notified": false,
+ "submitted": true,
+ "referenceNumberSequence": 3,
+ "managementTimeAllocation": null,
+ "managementDecisionSubmitted": false
+ },
+ "principalInvestigator": {
+ "id": 1,
+ "firstname": "Carl",
+ "lastname": "Carlsson",
+ "preferredname": "Carl",
+ "institution": "Other",
+ "institutionId": 1,
+ "position": "Strategist",
+ "created": "2023-10-10T13:32:18.172Z",
+ "placeholder": false,
+ "email": "Javon4@hotmail.com"
+ },
+ "coProposers": [
+ {
+ "id": 4,
+ "firstname": "Benjamin",
+ "lastname": "Beckley",
+ "preferredname": "Benjamin",
+ "institution": "Other",
+ "institutionId": 1,
+ "position": "Management",
+ "created": "2023-10-18T11:38:15.429Z",
+ "placeholder": false,
+ "email": "ben@inbox.com"
+ }
+ ],
+ "questionarySteps": [
+ {
+ "questionaryId": 4,
+ "topic": {
+ "id": 41,
+ "title": "Samples & Info",
+ "templateId": 39,
+ "sortOrder": 1,
+ "isEnabled": true
+ },
+ "isCompleted": true,
+ "fields": [
+ {
+ "question": {
+ "categoryId": 1,
+ "id": "date_1661426719613",
+ "naturalKey": "date_1661426719613",
+ "dataType": "DATE",
+ "question": "When do you want to come?",
+ "config": {
+ "small_label": "",
+ "required": false,
+ "tooltip": "",
+ "includeTime": false,
+ "maxDate": null,
+ "minDate": null,
+ "defaultDate": null
+ }
+ },
+ "topicId": 41,
+ "sortOrder": 0,
+ "config": {
+ "small_label": "",
+ "required": false,
+ "tooltip": "",
+ "includeTime": false,
+ "maxDate": null,
+ "minDate": null,
+ "defaultDate": null
+ },
+ "dependencies": [],
+ "dependenciesOperator": "AND",
+ "answerId": 4,
+ "value": null
+ },
+ {
+ "question": {
+ "categoryId": 1,
+ "id": "sample_declaration_1666269436134",
+ "naturalKey": "sample_declaration_1666269436134",
+ "dataType": "SAMPLE_DECLARATION",
+ "question": "Samples",
+ "config": {
+ "addEntryButtonLabel": "Add",
+ "templateCategory": "SAMPLE_DECLARATION",
+ "templateId": 36,
+ "esiTemplateId": null,
+ "small_label": "",
+ "required": false,
+ "maxEntries": null,
+ "minEntries": null
+ }
+ },
+ "topicId": 41,
+ "sortOrder": 1,
+ "config": {
+ "addEntryButtonLabel": "Add",
+ "templateCategory": "SAMPLE_DECLARATION",
+ "templateId": 36,
+ "esiTemplateId": null,
+ "small_label": "",
+ "required": false,
+ "maxEntries": null,
+ "minEntries": null
+ },
+ "dependencies": [],
+ "dependenciesOperator": "AND",
+ "answerId": null,
+ "value": []
+ },
+ {
+ "question": {
+ "categoryId": 1,
+ "id": "selection_from_options_1605784613207",
+ "naturalKey": "selection_from_options_instrument",
+ "dataType": "SELECTION_FROM_OPTIONS",
+ "question": "Select an instrument",
+ "config": {
+ "small_label": "",
+ "required": false,
+ "tooltip": "",
+ "variant": "dropdown",
+ "options": ["LoKi", "MX"],
+ "isMultipleSelect": false
+ }
+ },
+ "topicId": 41,
+ "sortOrder": 2,
+ "config": {
+ "small_label": "",
+ "required": false,
+ "tooltip": "",
+ "variant": "dropdown",
+ "options": ["LoKi", "MX", "CRYO-EM"],
+ "isMultipleSelect": true
+ },
+ "dependencies": [],
+ "dependenciesOperator": "AND",
+ "answerId": 5,
+ "value": ["MX"]
+ },
+ {
+ "question": {
+ "categoryId": 1,
+ "id": "generic_template_1674814357714",
+ "naturalKey": "generic_template_1674814357714",
+ "dataType": "GENERIC_TEMPLATE",
+ "question": "MX",
+ "config": {
+ "addEntryButtonLabel": "Add",
+ "copyButtonLabel": "",
+ "canCopy": false,
+ "isMultipleCopySelect": false,
+ "isCompleteOnCopy": false,
+ "templateCategory": "GENERIC_TEMPLATE",
+ "templateId": 38,
+ "small_label": "",
+ "required": false,
+ "maxEntries": "1",
+ "minEntries": "1"
+ }
+ },
+ "topicId": 41,
+ "sortOrder": 5,
+ "config": {
+ "addEntryButtonLabel": "Add",
+ "copyButtonLabel": "",
+ "canCopy": false,
+ "isMultipleCopySelect": false,
+ "isCompleteOnCopy": false,
+ "templateCategory": "GENERIC_TEMPLATE",
+ "templateId": 38,
+ "small_label": "",
+ "required": false,
+ "maxEntries": 1,
+ "minEntries": 1
+ },
+ "dependencies": [
+ {
+ "questionId": "generic_template_1674814357714",
+ "dependencyId": "selection_from_options_1605784613207",
+ "dependencyNaturalKey": "selection_from_options_instrument",
+ "condition": {
+ "params": "MX",
+ "condition": "eq"
+ }
+ }
+ ],
+ "dependenciesOperator": "AND",
+ "answerId": null,
+ "value": [
+ {
+ "id": 1,
+ "title": "template 1",
+ "creatorId": 1,
+ "proposalPk": 4,
+ "questionaryId": 5,
+ "questionId": "generic_template_1674814357714",
+ "created": "2023-10-18T11:38:39.861Z"
+ }
+ ]
+ },
+ {
+ "question": {
+ "categoryId": 1,
+ "id": "boolean_1674812264143",
+ "naturalKey": "boolean_1674812264143",
+ "dataType": "BOOLEAN",
+ "question": "Do you want sample environment for MX",
+ "config": {
+ "small_label": "",
+ "required": false,
+ "tooltip": ""
+ }
+ },
+ "topicId": 41,
+ "sortOrder": 6,
+ "config": {
+ "small_label": "",
+ "required": false,
+ "tooltip": ""
+ },
+ "dependencies": [
+ {
+ "questionId": "boolean_1674812264143",
+ "dependencyId": "selection_from_options_1605784613207",
+ "dependencyNaturalKey": "selection_from_options_instrument",
+ "condition": {
+ "params": "MX",
+ "condition": "eq"
+ }
+ }
+ ],
+ "dependenciesOperator": "AND",
+ "answerId": 6,
+ "value": true
+ },
+ {
+ "question": {
+ "categoryId": 1,
+ "id": "embellishment_1636710963252",
+ "naturalKey": "embellishment_1636710963252",
+ "dataType": "EMBELLISHMENT",
+ "question": "New question",
+ "config": {
+ "html": "
Users can also submit Expression of Interest in the following Crystallisation services:
\\n\\n- Support for large protein crystal growth of either protiated or deuterated proteins (incl. sitting drop vapour diffusion, dialysis, batch, optimization, targeted screening, testing with X-rays)
\\n
",
+ "plain": "Crystallisation text",
+ "omitFromPdf": true
+ }
+ },
+ "topicId": 41,
+ "sortOrder": 7,
+ "config": {
+ "html": "Users can also submit Expression of Interest in the following Crystallisation services:
\\n\\n- Support for large protein crystal growth of either protiated or deuterated proteins (incl. sitting drop vapour diffusion, dialysis, batch, optimization, targeted screening, testing with X-rays)
\\n
",
+ "plain": "Crystallisation text",
+ "omitFromPdf": true
+ },
+ "dependencies": [
+ {
+ "questionId": "embellishment_1636710963252",
+ "dependencyId": "selection_from_options_1605784613207",
+ "dependencyNaturalKey": "selection_from_options_instrument",
+ "condition": {
+ "params": "MX",
+ "condition": "eq"
+ }
+ }
+ ],
+ "dependenciesOperator": "AND",
+ "answerId": null,
+ "value": null
+ }
+ ]
+ },
+ {
+ "questionaryId": 4,
+ "topic": {
+ "id": 42,
+ "title": "Sample environment",
+ "templateId": 39,
+ "sortOrder": 2,
+ "isEnabled": true
+ },
+ "isCompleted": true,
+ "fields": [
+ {
+ "question": {
+ "categoryId": 1,
+ "id": "boolean_1674821942563",
+ "naturalKey": "boolean_1674821942563",
+ "dataType": "BOOLEAN",
+ "question": "Do you need lab-support?",
+ "config": {
+ "small_label": "",
+ "required": false,
+ "tooltip": ""
+ }
+ },
+ "topicId": 42,
+ "sortOrder": 0,
+ "config": {
+ "small_label": "",
+ "required": false,
+ "tooltip": ""
+ },
+ "dependencies": [],
+ "dependenciesOperator": "AND",
+ "answerId": 7,
+ "value": true
+ },
+ {
+ "question": {
+ "categoryId": 1,
+ "id": "final_delivery_date_motivation",
+ "naturalKey": "final_delivery_date_motivation",
+ "dataType": "TEXT_INPUT",
+ "question": "Please give a brief explanation for the chosen date",
+ "config": {
+ "required": true,
+ "small_label": "",
+ "tooltip": "",
+ "htmlQuestion": "",
+ "isHtmlQuestion": false,
+ "min": "1",
+ "max": 500,
+ "multiline": true,
+ "placeholder": "(maximum 500 characters)",
+ "isCounterHidden": false
+ }
+ },
+ "topicId": 42,
+ "sortOrder": 1,
+ "config": {
+ "required": true,
+ "small_label": "",
+ "tooltip": "",
+ "htmlQuestion": "",
+ "isHtmlQuestion": false,
+ "min": 1,
+ "max": 500,
+ "multiline": true,
+ "placeholder": "(maximum 500 characters)",
+ "isCounterHidden": false
+ },
+ "dependencies": [
+ {
+ "questionId": "final_delivery_date_motivation",
+ "dependencyId": "boolean_1674821942563",
+ "dependencyNaturalKey": "boolean_1674821942563",
+ "condition": {
+ "params": true,
+ "condition": "eq"
+ }
+ }
+ ],
+ "dependenciesOperator": "AND",
+ "answerId": 8,
+ "value": "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, an"
+ }
+ ]
+ }
+ ],
+ "attachments": [],
+ "samples": [],
+ "genericTemplates": [
+ {
+ "genericTemplate": {
+ "id": 1,
+ "title": "template 1",
+ "creatorId": 1,
+ "proposalPk": 4,
+ "questionaryId": 5,
+ "questionId": "generic_template_1674814357714",
+ "created": "2023-10-18T11:38:39.861Z"
+ },
+ "genericTemplateQuestionaryFields": [
+ {
+ "question": {
+ "categoryId": 7,
+ "id": "generic_template_basis",
+ "naturalKey": "generic_template_basis",
+ "dataType": "GENERIC_TEMPLATE_BASIS",
+ "question": "Template basic information",
+ "config": {
+ "titlePlaceholder": "Title",
+ "questionLabel": "",
+ "tooltip": "",
+ "required": false,
+ "small_label": ""
+ }
+ },
+ "topicId": 39,
+ "sortOrder": 0,
+ "config": {
+ "titlePlaceholder": "Title",
+ "questionLabel": "",
+ "tooltip": "",
+ "required": false,
+ "small_label": ""
+ },
+ "dependencies": [],
+ "dependenciesOperator": "AND",
+ "answerId": null,
+ "value": null
+ },
+ {
+ "question": {
+ "categoryId": 7,
+ "id": "boolean_1674814267658",
+ "naturalKey": "boolean_1674814267658",
+ "dataType": "BOOLEAN",
+ "question": "New question",
+ "config": {
+ "small_label": "",
+ "required": false,
+ "tooltip": ""
+ }
+ },
+ "topicId": 39,
+ "sortOrder": 1,
+ "config": {
+ "small_label": "",
+ "required": false,
+ "tooltip": ""
+ },
+ "dependencies": [],
+ "dependenciesOperator": "AND",
+ "answerId": 1,
+ "value": true
+ },
+ {
+ "question": {
+ "categoryId": 7,
+ "id": "date_1674814304449",
+ "naturalKey": "date_1674814304449",
+ "dataType": "DATE",
+ "question": "New question",
+ "config": {
+ "small_label": "",
+ "required": false,
+ "tooltip": "",
+ "includeTime": false,
+ "maxDate": null,
+ "minDate": null,
+ "defaultDate": null
+ }
+ },
+ "topicId": 39,
+ "sortOrder": 2,
+ "config": {
+ "small_label": "",
+ "required": false,
+ "tooltip": "",
+ "includeTime": false,
+ "maxDate": null,
+ "minDate": null,
+ "defaultDate": null
+ },
+ "dependencies": [],
+ "dependenciesOperator": "AND",
+ "answerId": 2,
+ "value": "2023-10-19T22:00:00.000Z"
+ },
+ {
+ "question": {
+ "categoryId": 7,
+ "id": "text_input_1674814274952",
+ "naturalKey": "text_input_1674814274952",
+ "dataType": "TEXT_INPUT",
+ "question": "New question",
+ "config": {
+ "required": false,
+ "small_label": "",
+ "tooltip": "",
+ "htmlQuestion": "",
+ "isHtmlQuestion": false,
+ "min": null,
+ "max": null,
+ "multiline": false,
+ "placeholder": "",
+ "isCounterHidden": false
+ }
+ },
+ "topicId": 39,
+ "sortOrder": 3,
+ "config": {
+ "required": false,
+ "small_label": "",
+ "tooltip": "",
+ "htmlQuestion": "",
+ "isHtmlQuestion": false,
+ "min": null,
+ "max": null,
+ "multiline": false,
+ "placeholder": "",
+ "isCounterHidden": false
+ },
+ "dependencies": [],
+ "dependenciesOperator": "AND",
+ "answerId": 3,
+ "value": "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum"
+ }
+ ]
+ }
+ ]
+ },
+ "userRole": {
+ "id": 2,
+ "shortCode": "user_officer",
+ "title": "User Officer"
+ }
+}
+`,
+ });
});
it('User officer can modify PDF template', () => {
@@ -218,6 +750,26 @@ context('Proposal PDF template tests', () => {
cy.contains(newDescription).should('exist');
});
+ it.only('User officer opens a PDF template editor → PDF preview renders without error', () => {
+ cy.login('officer');
+ cy.visit('/');
+
+ cy.navigateToTemplatesSubmenu('PDF (Proposal)');
+
+ cy.contains(createdTemplateName)
+ .parent()
+ .find("[aria-label='Edit']")
+ .click();
+
+ cy.contains(createdTemplateName).should('exist');
+
+ cy.get('[data-cy="pdf-template-preview"] canvas', {
+ timeout: 60000,
+ }).should('be.visible');
+
+ cy.get('[data-cy="pdf-template-preview-error"]').should('not.exist');
+ });
+
it('User officer can clone PDF template', () => {
cy.login('officer');
cy.visit('/');
diff --git a/apps/e2e/cypress/support/template.ts b/apps/e2e/cypress/support/template.ts
index b36fe60fc0..324e909bc0 100644
--- a/apps/e2e/cypress/support/template.ts
+++ b/apps/e2e/cypress/support/template.ts
@@ -20,6 +20,8 @@ import {
UpdateQuestionMutationVariables,
UpdateQuestionTemplateRelationSettingsMutation,
UpdateQuestionTemplateRelationSettingsMutationVariables,
+ UpdateProposalPdfTemplateMutation,
+ UpdateProposalPdfTemplateMutationVariables,
UpdateTopicMutation,
UpdateTopicMutationVariables,
} from '@user-office-software-libs/shared-types';
@@ -73,6 +75,15 @@ const setActiveTemplate = (
return cy.wrap(request);
};
+const updateProposalPdfTemplate = (
+ updateProposalPdfTemplateInput: UpdateProposalPdfTemplateMutationVariables
+): Cypress.Chainable => {
+ const api = getE2EApi();
+ const request = api.updateProposalPdfTemplate(updateProposalPdfTemplateInput);
+
+ return cy.wrap(request);
+};
+
const typeToMenuTitle = new Map();
typeToMenuTitle.set('proposal', 'Proposal');
typeToMenuTitle.set('sample', 'Sample declaration');
@@ -810,3 +821,5 @@ Cypress.Commands.add(
);
Cypress.Commands.add('setActiveTemplate', setActiveTemplate);
+
+Cypress.Commands.add('updateProposalPdfTemplate', updateProposalPdfTemplate);
diff --git a/apps/e2e/cypress/types/template.d.ts b/apps/e2e/cypress/types/template.d.ts
index 5e16ceb86a..e1b461ec46 100644
--- a/apps/e2e/cypress/types/template.d.ts
+++ b/apps/e2e/cypress/types/template.d.ts
@@ -21,6 +21,8 @@ import {
UpdateQuestionTemplateRelationSettingsMutationVariables,
UpdateQuestionTemplateRelationSettingsMutation,
SetActiveTemplateMutationVariables,
+ UpdateProposalPdfTemplateMutationVariables,
+ UpdateProposalPdfTemplateMutation,
} from '@user-office-software-libs/shared-types';
declare global {
@@ -315,6 +317,7 @@ declare global {
createTemplate: (
createTemplateInput: CreateTemplateMutationVariables
) => Cypress.Chainable;
+
/**
* Clone template
*
@@ -397,6 +400,18 @@ declare global {
setActiveTemplate: (
setActiveTemplateInput: SetActiveTemplateMutationVariables
) => Cypress.Chainable;
+
+ /**
+ * Updates proposal PDF template
+ *
+ * @returns {typeof updateProposalPdfTemplate}
+ * @memberof Chainable
+ * @example
+ * cy.updateProposalPdfTemplate(updateProposalPdfTemplateInput: UpdateProposalPdfTemplateMutationVariables)
+ */
+ updateProposalPdfTemplate: (
+ updateProposalPdfTemplateInput: UpdateProposalPdfTemplateMutationVariables
+ ) => Cypress.Chainable;
}
}
}
diff --git a/apps/frontend/Dockerfile b/apps/frontend/Dockerfile
index 1490a35e44..351f043627 100644
--- a/apps/frontend/Dockerfile
+++ b/apps/frontend/Dockerfile
@@ -23,7 +23,7 @@ COPY apps/frontend/ /app/frontend
RUN npm run build
# Stage 1, based on Nginx, to have only the compiled app, ready for production with Nginx
-FROM nginxinc/nginx-unprivileged:1.25-alpine
+FROM nginxinc/nginx-unprivileged:alpine3.23
COPY --from=build-stage /app/frontend/build/ /usr/share/nginx/html
COPY --from=build-stage /app/frontend/build-version.txt /usr/share/nginx/html
diff --git a/apps/frontend/default.conf b/apps/frontend/default.conf
index d193267366..fefe0925cf 100644
--- a/apps/frontend/default.conf
+++ b/apps/frontend/default.conf
@@ -1,6 +1,7 @@
server {
listen 8080;
-
+ root /usr/share/nginx/html;
+
gzip on;
gzip_disable "msie6";
@@ -10,9 +11,17 @@ server {
gzip_http_version 1.1;
gzip_types text/plain application/javascript text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype;
+ # nginx's bundled mime.types has no mapping for .mjs, so ES module workers
+ # (e.g. pdf.worker.min.mjs) would be served as application/octet-stream and
+ # rejected by the browser's strict module MIME check. Set the type for .mjs
+ # only, without redefining the whole types map (a `types { }` block would
+ # REPLACE the inherited mime.types and break .html/.css serving).
+ location ~ \.mjs$ {
+ default_type text/javascript;
+ }
+
location / {
- root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html =200;
}
-}
\ No newline at end of file
+}
diff --git a/apps/frontend/src/components/template/pdf/proposal/PdfTemplateEditorViewer.tsx b/apps/frontend/src/components/template/pdf/proposal/PdfTemplateEditorViewer.tsx
index b1e6a7d06c..879c9cc878 100644
--- a/apps/frontend/src/components/template/pdf/proposal/PdfTemplateEditorViewer.tsx
+++ b/apps/frontend/src/components/template/pdf/proposal/PdfTemplateEditorViewer.tsx
@@ -42,8 +42,17 @@ const PDFViewer = ({ fileUrl }: { fileUrl: string }) => {
height: '1000px',
}}
ref={setContainerRef}
+ data-cy="pdf-template-preview"
>
-
+
+ Failed to load PDF file.
+
+ }
+ >
{Array.from(new Array(numPages), (_el, index) => (