Skip to content

Commit

Permalink
Merge pull request #236 from pnnl/186-downloading-and-uploading-project
Browse files Browse the repository at this point in the history
186 Project level document upload and download functionality
  • Loading branch information
sudhacheran authored Nov 12, 2024
2 parents aea178e + 5636c7d commit 55eb194
Show file tree
Hide file tree
Showing 9 changed files with 463 additions and 8 deletions.
8 changes: 6 additions & 2 deletions public/print.css
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,9 @@ h1 + h3 {
}

.button-container-right {
float: right;
padding-left: 0.75rem;
padding-right: 0.75rem;
padding-right: 0.75rem;
float: right;
}
.button-container-center {
float: center;
Expand Down Expand Up @@ -401,4 +401,8 @@ btn-light:hover {
position: relative; /* Position for absolute child */
height: auto; /* Allow height to adjust based on content */
max-height: 500px; /* Maximum height */
}

.align-right{
text-align: right;
}
8 changes: 6 additions & 2 deletions src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,9 @@ h1 + h3 {
}

.button-container-right {
float: right;
padding-left: 0.75rem;
padding-right: 0.75rem;
padding-right: 0.75rem;
float: right;
}
.button-container-center {
float: center;
Expand Down Expand Up @@ -401,4 +401,8 @@ btn-light:hover {
position: relative; /* Position for absolute child */
height: auto; /* Allow height to adjust based on content */
max-height: 500px; /* Maximum height */
}

.align-right{
text-align: right;
}
60 changes: 60 additions & 0 deletions src/components/export_document.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React from 'react'
import JSONValue from '../types/json_value.type'
import { Button } from 'react-bootstrap'
import { TfiImport } from 'react-icons/tfi'
import { EXPORT_FILE_TYPE } from '../utilities/paths_utils'

interface ExportDocProps {
sendData: JSONValue // The data to be downloaded
fileName: string // The name of the file
}

/**
* A component that triggers the download of a document as a file.
* @param sendData - The data to be exported, json data from the DB.
* @param fileName - The name of the file to be downloaded. (extension as *.qit)
* @returns A React element that renders a button for downloading the document.
*/
const ExportDoc: React.FC<ExportDocProps> = ({
sendData,
fileName,
}: any): JSX.Element => {
const handleDownload = () => {
try {
//Create a Blob from the sendData
const blob = new Blob([sendData as BlobPart], {
type: 'application/json',
})
const url = URL.createObjectURL(blob)

// Create a temporary anchor element to trigger the download
const link = document.createElement('a')
link.href = url
link.setAttribute('download', `${fileName + EXPORT_FILE_TYPE}`)

// Append to the body, click and remove it
document.body.appendChild(link)
link.click()
document.body.removeChild(link)

// Clean up the URL object
URL.revokeObjectURL(url)
} catch (error) {
console.log('Error in exporting the document', error)
}
}
return (
<Button
variant="light"
onClick={event => {
event.stopPropagation()
event.preventDefault()
handleDownload()
}}
>
<TfiImport size={20} />
</Button>
)
}

export default ExportDoc
40 changes: 40 additions & 0 deletions src/components/export_document_wrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { FC, useEffect, useState } from 'react'
import ExportDoc from './export_document'
import { exportDocumentAsJSONObject, useDB } from '../utilities/database_utils'
import JSONValue from '../types/json_value.type'

// Define the props interface for ExportDocWrapper
interface ExportDocWrapperProps {
docId: string
docName: string
includeChild: boolean
}

/**
* A wrapper component for `ExportDoc` that exports a document from a PouchDB database as a JSON object.
* @param {string} props.docId - The ID of the document to be exported.
* @param {string} props.docName - The name of the document, used for naming the export file.
* @param {boolean} props.includeChild - A flag indicating whether to include child documents in the export.
* @returns {JSX.Element} A React element that renders the `ExportDoc` component with the exported data.
*/
const ExportDocWrapper: FC<ExportDocWrapperProps> = ({
docId,
docName,
includeChild,
}: ExportDocWrapperProps): JSX.Element => {
const db = useDB()
const [sendData, setSendData] = useState<JSONValue>({})

useEffect(() => {
exportDocumentAsJSONObject(db, docId, includeChild).then(data =>
setSendData(data),
)
}, [])

const timestamp = new Date(Date.now()).toUTCString()
return (
<ExportDoc sendData={sendData} fileName={docName + ' ' + timestamp} />
)
}

export default ExportDocWrapper
22 changes: 19 additions & 3 deletions src/components/home.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import React, { useState, type FC, useEffect, SetStateAction } from 'react'
import { ListGroup, Button, Modal } from 'react-bootstrap'
import { LinkContainer } from 'react-router-bootstrap'
import { TfiTrash, TfiPencil } from 'react-icons/tfi'
import { TfiTrash, TfiPencil, TfiArrowDown } from 'react-icons/tfi'
import { useNavigate } from 'react-router-dom'
import { deleteEmptyProjects, useDB } from '../utilities/database_utils'
import ImportDoc from './import_document_wrapper'
import ExportDoc from './export_document_wrapper'

/**
* Home: Renders the Home page for the APP
Expand Down Expand Up @@ -101,7 +103,7 @@ const Home: FC = () => {
}

const sortByEditTime = (jobsList: any[]) => {
const sortedJobsByEditTime = jobsList.sort((a, b) => {
jobsList.sort((a, b) => {
if (
a.metadata_.last_modified_at.toString() <
b.metadata_.last_modified_at.toString()
Expand All @@ -126,8 +128,9 @@ const Home: FC = () => {
const editAddressDetails = (projectID: string) => {
navigate('app/' + projectID, { replace: true })
}

const projects_display =
Object.keys(projectList).length == 0
Object.keys(projectList).length === 0
? []
: projectList.map((key, value) => (
<div key={key._id}>
Expand Down Expand Up @@ -158,6 +161,11 @@ const Home: FC = () => {
>
<TfiTrash size={22} />
</Button>
<ExportDoc
docId={key._id}
docName={key.metadata_?.doc_name}
includeChild={true}
/>
</span>
<b>{key.metadata_?.doc_name}</b>
{key.data_?.location?.street_address && (
Expand Down Expand Up @@ -213,6 +221,10 @@ const Home: FC = () => {
>
Add a New Project
</Button>
<ImportDoc
id="project_json"
label="Import a Project"
/>
</div>
</center>
)}
Expand All @@ -225,6 +237,10 @@ const Home: FC = () => {
>
Add a New Project
</Button>
<ImportDoc
id="project_json"
label="Import Project"
/>
</div>
{projects_display}
</div>
Expand Down
127 changes: 127 additions & 0 deletions src/components/import_document.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { useEffect, useRef, useState } from 'react'
import type { ChangeEvent, FC, MouseEvent } from 'react'
import { Button } from 'react-bootstrap'
import { ImportDocumentIntoDB, useDB } from '../utilities/database_utils'
import { EXPORT_FILE_TYPE } from '../utilities/paths_utils'

interface ImportDocProps {
id: string
label: string
}

/**
* ImportDoc component : Imports JSON documents into a PouchDB database.
*
* This component handles the importation of a project document from a JSON file into a PouchDB database.
* It renders a button that, when clicked, triggers a hidden file input to open. When a file is selected,
* the file's content is read, processed, and imported into the database.
*
* @param label - The label for the import operation.
* @param file - The file to be imported.
*
* @returns The rendered button and hidden file input elements for importing a project.
*/
const ImportDoc: FC<ImportDocProps> = ({ id, label }) => {
// Create references to the hidden file inputs
const hiddenFileUploadInputRef = useRef<HTMLInputElement>(null)
const [projectNames, setProjectNames] = useState<string[]>([])
const [isFileProcessed, setIsFileProcessed] = useState<boolean>(false)
const [error, setError] = useState<String>('')
const db = useDB()

const handleFileInputButtonClick = (
event: MouseEvent<HTMLButtonElement>,
) => {
hiddenFileUploadInputRef.current?.click()
}

/**
* Fetches project names from the database and updates the state.
*
* @returns {Promise<void>} A promise that resolves when the fetch operation is complete.
*/
const fetchProjectNames = async (): Promise<void> => {
const result = await db.allDocs({
include_docs: true,
})
const projectDocs = result.rows
.map((row: any) => row.doc)
.filter((doc: any) => doc.type === 'project')

const projectNames = projectDocs.map(
(doc: any) => doc.metadata_.doc_name,
)
setProjectNames(projectNames)
}

useEffect(() => {
// Retrieve all project names from the database when each upload is processed
fetchProjectNames()
}, [projectNames, isFileProcessed])

const handleFileInputChange = async (
event: ChangeEvent<HTMLInputElement>,
) => {
if (event.target.files) {
const file = event.target.files[0]
if (file) {
const isValid = file.name.endsWith(EXPORT_FILE_TYPE)
if (!isValid) {
setError(
"Please select file with extension '" +
EXPORT_FILE_TYPE +
"'",
)
} else {
setError('')
processJsonData(file) // Processes JSON data from a file
}
}
event.target.value = ''
}
}

/**
* Processes JSON data from a file and imports it into the database.
*
* @param {File} file - The JSON file containing documents to be imported.
* @returns {Promise<void>} A promise that resolves when the processing is complete.
*/
const processJsonData = async (file: File): Promise<void> => {
// Reset state and local variables for every import
setIsFileProcessed(false)

const reader = new FileReader()
reader.readAsText(file)
reader.onload = async event => {
const dataFromFile = (event.target as FileReader).result
if (typeof dataFromFile !== 'string') {
console.error('File content is not a string.')
return
}
try {
const jsonData = JSON.parse(dataFromFile)
await ImportDocumentIntoDB(db, jsonData, projectNames)
} catch (error) {
console.error('Error parsing JSON from file:', error)
}
}
}
return (
<>
{' '}
&nbsp;
<Button onClick={handleFileInputButtonClick}>{label}</Button>
<input
accept={'*' + EXPORT_FILE_TYPE}
onChange={handleFileInputChange}
ref={hiddenFileUploadInputRef}
className="photo-upload-input"
type="file"
/>
{error && <div className="error">{error}</div>}
</>
)
}

export default ImportDoc
27 changes: 27 additions & 0 deletions src/components/import_document_wrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { FC } from 'react'
import ImportDoc from './import_document'

// Define the props interface for ImportDocWrapper
interface ImportDocWrapperProps {
id: string
label: string
}

/**
* ImportDocWrapper component.
*
* This component wraps the `ImportDoc` component, retrieves attachments and job ID from the context, constructs
* a reference ID, and passes the appropriate file blob and label to `ImportDoc`.
*
* @param {string} label - The label to be displayed for the import operation.
*
* @returns {JSX.Element} The rendered ImportDoc component with context-provided props.
*/
const ImportDocWrapper: FC<ImportDocWrapperProps> = ({
id,
label,
}: ImportDocWrapperProps): JSX.Element => {
return <ImportDoc id={id} label={label} />
}

export default ImportDocWrapper
Loading

0 comments on commit 55eb194

Please sign in to comment.