Skip to content

Commit

Permalink
Merge branch 'main' into IA-3656
Browse files Browse the repository at this point in the history
  • Loading branch information
beygorghor committed Nov 12, 2024
2 parents a98ebda + ea88b8d commit 24fdfca
Show file tree
Hide file tree
Showing 71 changed files with 2,163 additions and 987 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Grid } from '@mui/material';
import { FilesUpload, useSafeIntl } from 'bluesquare-components';
import React from 'react';
import { PdfPreview } from './PdfPreview';
import MESSAGES from './messages';
import { acceptPDF } from './utils';

type DocumentUploadWithPreviewProps = {
errors: string[] | undefined;
onFilesSelect: (files: File[]) => void;
document?: File[] | string;
};

const DocumentUploadWithPreview: React.FC<DocumentUploadWithPreviewProps> = ({
errors,
onFilesSelect,
document,
}) => {
const { formatMessage } = useSafeIntl();

let pdfUrl: string | null = null;
if (typeof document === 'string') {
pdfUrl = document;
} else if (
Array.isArray(document) &&
document.length > 0 &&
document[0] instanceof File
) {
pdfUrl = URL.createObjectURL(document[0]);
} else if (document instanceof File) {
pdfUrl = URL.createObjectURL(document);
}

return (
<Grid container spacing={2} alignItems="center">
<Grid item xs={document ? 10 : 12}>
<FilesUpload
accept={acceptPDF}
files={document ? [document as unknown as File] : []}
onFilesSelect={onFilesSelect}
multi={false}
errors={errors}
placeholder={formatMessage(MESSAGES.document)}
/>
</Grid>
{pdfUrl && (
<Grid item xs={2} sx={{ textAlign: 'right' }}>
<PdfPreview pdfUrl={pdfUrl} />
</Grid>
)}
</Grid>
);
};

export default DocumentUploadWithPreview;
93 changes: 93 additions & 0 deletions hat/assets/js/apps/Iaso/components/files/pdf/PdfPreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import {
Button,
Dialog,
DialogActions,
DialogContent,
IconButton,
} from '@mui/material';
import { useSafeIntl } from 'bluesquare-components';
import React, { FunctionComponent, useCallback, useState } from 'react';
import { Document, Page, pdfjs } from 'react-pdf';
import PdfSvgComponent from '../../svg/PdfSvgComponent';
import MESSAGES from './messages';

// Set the workerSrc for pdfjs to enable the use of Web Workers.
// Web Workers allow the PDF.js library to process PDF files in a separate thread,
// keeping the main thread responsive and ensuring smooth UI interactions.
// Note: The PDF file itself is not transferred to the worker; only the processing is offloaded.
// This is necessary for the react-pdf library to function correctly.
if (!pdfjs.GlobalWorkerOptions.workerSrc) {
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;
}

type PdfPreviewProps = {
pdfUrl?: string;
};

export const PdfPreview: FunctionComponent<PdfPreviewProps> = ({ pdfUrl }) => {
const [open, setOpen] = useState(false); // State to manage dialog open/close

const { formatMessage } = useSafeIntl();
const handleOpen = () => {
setOpen(true);
};

const handleClose = () => {
setOpen(false);
};

const handleDownload = useCallback(() => {
if (pdfUrl) {
const link = document.createElement('a');
link.href = pdfUrl;
const urlParts = pdfUrl.split('/');
const fileName = urlParts[urlParts.length - 1] || 'document.pdf';
link.download = fileName;
link.click();
}
}, [pdfUrl]);
return (
<>
<IconButton
onClick={handleOpen}
aria-label="preview document"
disabled={!pdfUrl}
>
<PdfSvgComponent />
</IconButton>
{open && (
<Dialog
fullWidth
maxWidth="md"
open={open}
onClose={handleClose}
>
<DialogContent
sx={{
px: 0,
display: 'flex',
justifyContent: 'center',
}}
>
<Document file={pdfUrl}>
<Page
pageNumber={1}
width={880}
renderTextLayer={false}
renderAnnotationLayer={false}
/>
</Document>
</DialogContent>
<DialogActions>
<Button onClick={handleDownload} color="primary">
{formatMessage(MESSAGES.download)}
</Button>
<Button onClick={handleClose} color="primary">
{formatMessage(MESSAGES.close)}
</Button>
</DialogActions>
</Dialog>
)}
</>
);
};
18 changes: 18 additions & 0 deletions hat/assets/js/apps/Iaso/components/files/pdf/messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { defineMessages } from 'react-intl';

const MESSAGES = defineMessages({
document: {
id: 'iaso.label.document',
defaultMessage: 'Document',
},
close: {
defaultMessage: 'Close',
id: 'blsq.buttons.label.close',
},
download: {
defaultMessage: 'Download',
id: 'iaso.label.download',
},
});

export default MESSAGES;
11 changes: 11 additions & 0 deletions hat/assets/js/apps/Iaso/components/files/pdf/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Accept } from 'react-dropzone';

export const acceptPDF: Accept = {
'application/pdf': ['.pdf'],
};

export const processErrorDocsBase = err_docs => {
if (!err_docs) return [];
if (!Array.isArray(err_docs)) return [err_docs];
return err_docs;
};
2 changes: 2 additions & 0 deletions hat/assets/js/apps/Iaso/domains/app/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,7 @@
"iaso.label.display": "Display",
"iaso.label.displayPassword": "Display the password",
"iaso.label.docs": "Docs",
"iaso.label.document": "Document",
"iaso.label.documents": "Documents",
"iaso.label.done": "Done",
"iaso.label.download": "Download",
Expand Down Expand Up @@ -1106,6 +1107,7 @@
"iaso.planning.title": "Planning",
"iaso.plannings.label.duplicatePlanning": "Duplicate planning",
"iaso.plannings.label.selectOrgUnit": "Please select org unit",
"iaso.polio.label.document": "Document CHECKME",
"iaso.projects.appId": "App ID",
"iaso.projects.create": "Create project",
"iaso.projects.false": "User doesn't need authentication",
Expand Down
2 changes: 2 additions & 0 deletions hat/assets/js/apps/Iaso/domains/app/translations/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,7 @@
"iaso.label.display": "Afficher",
"iaso.label.displayPassword": "Afficher le mot de passe",
"iaso.label.docs": "Docs",
"iaso.label.document": "Document",
"iaso.label.documents": "Documents",
"iaso.label.done": "Terminé",
"iaso.label.download": "Télécharger",
Expand Down Expand Up @@ -1106,6 +1107,7 @@
"iaso.planning.title": "Planning",
"iaso.plannings.label.duplicatePlanning": "Dupliquer planning",
"iaso.plannings.label.selectOrgUnit": "Sélectionner une unité d'org.",
"iaso.polio.label.document": "Document CHECKME",
"iaso.projects.appId": "Identifiant de l'App",
"iaso.projects.create": "Créer un projet",
"iaso.projects.false": "L'utilisateur n'a pas besoin d'authentification",
Expand Down
7 changes: 4 additions & 3 deletions hat/assets/js/test/helpers.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React from 'react';
import sinon from 'sinon';
import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
import { expect } from 'chai';
import { configure, mount, render, shallow } from 'enzyme';
import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
import nodeFetch from 'node-fetch';
import React from 'react';
import sinon from 'sinon';
import { mockMessages } from './utils/intl';
import './utils/pdf';
import { baseUrl as baseUrlConst } from './utils/requests';

configure({ adapter: new Adapter() });
Expand Down
32 changes: 32 additions & 0 deletions hat/assets/js/test/utils/pdf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// test/setup.js or in your test file
const mock = require('mock-require');

// Mock pdfjs-dist
mock('pdfjs-dist', {
GlobalWorkerOptions: {
workerSrc: '',
},
getDocument: () => ({
promise: Promise.resolve({
numPages: 1,
getPage: () => ({
promise: Promise.resolve({
getTextContent: () => ({
promise: Promise.resolve({ items: [] }),
}),
}),
}),
}),
}),
});

// Mock react-pdf
mock('react-pdf', {
Document: ({ children }) => <div>{children}</div>,
Page: () => <div>Page</div>,
pdfjs: {
GlobalWorkerOptions: {
workerSrc: '',
},
},
});
8 changes: 7 additions & 1 deletion hat/webpack.dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -264,13 +264,19 @@ module.exports = {
filename: 'videos/[name].[hash][ext]',
},
},
{
test: /\.mjs$/,
type: 'javascript/auto',
use: 'babel-loader',
},
],
noParse: [require.resolve('typescript/lib/typescript.js')], // remove warning: https://github.com/microsoft/TypeScript/issues/39436
},
externals: [{ './cptable': 'var cptable' }],

resolve: {
alias: {
'react/jsx-runtime': 'react/jsx-runtime.js',
// see LIVE_COMPONENTS feature in doc
...(process.env.LIVE_COMPONENTS === 'true' && {
'bluesquare-components': path.resolve(
Expand All @@ -292,7 +298,7 @@ module.exports = {
: /* assets/js/apps path allow using absolute import eg: from 'iaso/libs/Api' */
['node_modules', path.resolve(__dirname, 'assets/js/apps/')],

extensions: ['.js', '.tsx', '.ts'],
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
stats: {
errorDetails: true,
Expand Down
10 changes: 9 additions & 1 deletion hat/webpack.prod.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,11 @@ module.exports = {
filename: 'videos/[name].[hash][ext]',
},
},
{
test: /\.mjs$/,
type: 'javascript/auto',
use: 'babel-loader',
},
],
noParse: [require.resolve('typescript/lib/typescript.js')], // remove warning: https://github.com/microsoft/TypeScript/issues/39436
},
Expand All @@ -224,11 +229,14 @@ module.exports = {
externals: [{ './cptable': 'var cptable' }],

resolve: {
alias: {
'react/jsx-runtime': 'react/jsx-runtime.js',
},
fallback: {
fs: false,
},
/* assets/js/apps path allow using absolute import eg: from 'iaso/libs/Api' */
modules: ['node_modules', path.resolve(__dirname, 'assets/js/apps/')],
extensions: ['.js', '.tsx', '.ts'],
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
};
4 changes: 2 additions & 2 deletions iaso/api/fixtures/sample_bulk_user_creation.csv
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
username,password,email,first_name,last_name,orgunit,orgunit__source_ref,permissions,profile_language,dhis2_id,Organization,projects,user_roles,phone_number
user name should not contain whitespaces,"Min. 8 characters, should include 1 letter and 1 number",,,,Use Org Unit ID to avoid errors,Org Unit external ID,"Possible values: iaso_forms,iaso_mappings,iaso_completeness,iaso_org_units,iaso_links,iaso_users,iaso_pages,iaso_projects,iaso_sources,iaso_data_tasks,iaso_submissions,iaso_update_submission,iaso_planning,iaso_reports,iaso_teams,iaso_assignments,iaso_entities,iaso_storages,iaso_completeness_stats,iaso_workflows,iaso_registry","Possible values: EN, FR",Optional,Optional,projects,user roles,The phone number as a string (in single or double quote). It has to start with the country code like +1 for US
username,password,email,first_name,last_name,orgunit,orgunit__source_ref,permissions,profile_language,dhis2_id,organization,projects,user_roles,phone_number,editable_org_unit_types
user name should not contain whitespaces,"Min. 8 characters, should include 1 letter and 1 number",,,,Use Org Unit ID to avoid errors,Org Unit external ID,"Possible values: iaso_forms,iaso_mappings,iaso_completeness,iaso_org_units,iaso_links,iaso_users,iaso_pages,iaso_projects,iaso_sources,iaso_data_tasks,iaso_submissions,iaso_update_submission,iaso_planning,iaso_reports,iaso_teams,iaso_assignments,iaso_entities,iaso_storages,iaso_completeness_stats,iaso_workflows,iaso_registry","Possible values: EN, FR",Optional,Optional,projects,user roles,The phone number as a string (in single or double quote). It has to start with the country code like +1 for US,"Use comma separated Org Unit Type IDs to avoid errors: 1, 2"
6 changes: 5 additions & 1 deletion iaso/api/org_units.py
Original file line number Diff line number Diff line change
Expand Up @@ -763,9 +763,13 @@ def create(self, _, request):

def retrieve(self, request, pk=None):
org_unit: OrgUnit = get_object_or_404(
self.get_queryset().prefetch_related("reference_instances").annotate(instances_count=Count("instance")),
self.get_queryset().prefetch_related("reference_instances"),
pk=pk,
)
# Get instances count for the Org unit and its descendants
instances_count = org_unit.descendants().aggregate(Count("instance"))["instance__count"]
org_unit.instances_count = instances_count

self.check_object_permissions(request, org_unit)
res = org_unit.as_dict_with_parents(light=False, light_parents=False)
res["geo_json"] = None
Expand Down
Loading

0 comments on commit 24fdfca

Please sign in to comment.