From e2275d28fd711a2af29df49e82fbcfd33ebaeae9 Mon Sep 17 00:00:00 2001 From: AdiAkhileshSingh15 Date: Sun, 27 Oct 2024 19:16:39 +0530 Subject: [PATCH 1/4] adds scan-result for 1st four checklist statements, some refactoring as well, testing and comments added Signed-off-by: AdiAkhileshSingh15 --- .../ProjectEntryPoint/ProjectEntryPoint.js | 16 +- .../ChecklistItem/ChecklistItem.js | 521 ++++++++++++++++++ .../ReproChecklist/ReproChecklist.js | 380 +++++++++++++ app/utils/asset.js | 24 + app/utils/checklist.js | 173 ++++++ app/utils/workflow.js | 47 +- test/utils/checklist.spec.js | 275 +++++++++ 7 files changed, 1419 insertions(+), 17 deletions(-) create mode 100644 app/components/ReproChecklist/ChecklistItem/ChecklistItem.js create mode 100644 app/components/ReproChecklist/ReproChecklist.js create mode 100644 app/utils/checklist.js create mode 100644 test/utils/checklist.spec.js diff --git a/app/components/ProjectEntryPoint/ProjectEntryPoint.js b/app/components/ProjectEntryPoint/ProjectEntryPoint.js index 548136e..29c736b 100644 --- a/app/components/ProjectEntryPoint/ProjectEntryPoint.js +++ b/app/components/ProjectEntryPoint/ProjectEntryPoint.js @@ -7,17 +7,6 @@ import { faFileImport, faShareFromSquare } from '@fortawesome/free-solid-svg-ico import styles from './ProjectEntryPoint.css'; import AssetUtil from '../../utils/asset'; -function findEntryPoints(asset, entryPointsList) { - if (asset.attributes && asset.attributes.entrypoint === true) { - entryPointsList.push(asset); - } - if (asset.children) { - asset.children.forEach((child) => { - findEntryPoints(child, entryPointsList); - }); - } -} - const projectEntryPoint = (props) => { const { assets, rootUri, onSelect } = props; @@ -31,10 +20,7 @@ const projectEntryPoint = (props) => { } }; - const entryPointsList = []; - if (assets) { - findEntryPoints(assets, entryPointsList); - } + const entryPointsList = AssetUtil.findEntryPointAssets(assets); if (entryPointsList && entryPointsList.length > 0) { return ( diff --git a/app/components/ReproChecklist/ChecklistItem/ChecklistItem.js b/app/components/ReproChecklist/ChecklistItem/ChecklistItem.js new file mode 100644 index 0000000..ca73324 --- /dev/null +++ b/app/components/ReproChecklist/ChecklistItem/ChecklistItem.js @@ -0,0 +1,521 @@ +import React, { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import styles from './ChecklistItem.css'; +import NoteEditor from '../../NoteEditor/NoteEditor'; +import { ContentCopy, Done, Delete } from '@mui/icons-material'; +import { + IconButton, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, +} from '@mui/material'; +import { FaFolderOpen, FaFolderMinus, FaChevronUp, FaChevronDown } from 'react-icons/fa'; +import AssetTree from '../../AssetTree/AssetTree'; +import AssetUtil from '../../../utils/asset'; +import Constants from '../../../constants/constants'; + +const { v4: uuidv4 } = require('uuid'); + +function ChecklistItem(props) { + const { + item, + project, + onUpdatedNote, + onDeletedNote, + onAddedNote, + onItemUpdate, + onSelectedAsset, + } = props; + + const treeRef = React.useRef(null); + + const [expanded, setExpanded] = useState(false); + + const [copiedUrlId, setCopiedUrlId] = useState(null); + + const [showImages, setShowImages] = useState(false); + const [showURLs, setShowURLs] = useState(false); + const [showSubChecks, setShowSubChecks] = useState(false); + + const [selectedAsset, setSelectedAsset] = useState(null); + const [addAsset, setAddAsset] = useState(false); + const [assetTitle, setAssetTitle] = useState(''); + const [assetDescription, setAssetDescription] = useState(''); + + const resetAssetDialog = () => { + setAddAsset(false); + setAssetTitle(''); + setAssetDescription(''); + setSelectedAsset(null); + }; + + const handleSelectAsset = (selAsset) => { + let asset = selAsset; + if (asset && (asset.contentTypes === null || asset.contentTypes === undefined)) { + if (project && project.assets) { + asset = AssetUtil.findDescendantAssetByUri(project.assets, asset.uri); + } + } + + if (asset && asset.uri) { + if (asset.type === Constants.AssetType.FILE) { + const fileName = AssetUtil.getAssetNameFromUri(asset.uri); + setAssetTitle(fileName); + // Handles case for URL assets + // TODO: Title is currently the same as URI, some way to get a better title? + } else if (asset.type === Constants.AssetType.URL) { + setAssetTitle(asset.uri); + } else { + setAssetTitle(''); + } + } + + setSelectedAsset(asset); + if (onSelectedAsset) { + onSelectedAsset(asset); + } + }; + + const handleAddAsset = () => { + if (!selectedAsset) { + console.log('No asset selected - cannot add asset reference'); + return; + } + if (!assetTitle) { + console.log('No title provided - cannot add asset reference'); + return; + } + + if (selectedAsset.type === Constants.AssetType.FILE) { + // Adds image files to checklist images + if (selectedAsset.contentTypes.includes(Constants.AssetContentType.IMAGE)) { + const updatedItem = { + ...item, + images: [ + ...item.images, + { + id: uuidv4(), + uri: selectedAsset.uri, + title: assetTitle, + description: assetDescription, + }, + ], + }; + onItemUpdate(updatedItem); + setShowImages(true); + // Adds file as URL + } else { + const updatedItem = { + ...item, + urls: [ + ...item.urls, + { + id: uuidv4(), + hyperlink: AssetUtil.absoluteToRelativePath(project.path, selectedAsset), + title: assetTitle, + description: assetDescription, + }, + ], + }; + onItemUpdate(updatedItem); + setShowURLs(true); + } + } else if (selectedAsset.type === Constants.AssetType.URL) { + const updatedItem = { + ...item, + urls: [ + ...item.urls, + { + id: uuidv4(), + hyperlink: selectedAsset.uri, + title: assetTitle, + description: assetDescription, + }, + ], + }; + onItemUpdate(updatedItem); + setShowURLs(true); + } + + resetAssetDialog(); + }; + + const handleCopy = (urlId, hyperlink) => { + // Copy the URL to the clipboard + navigator.clipboard.writeText(hyperlink).then(() => { + setCopiedUrlId(urlId); + setTimeout(() => { + setCopiedUrlId(null); + }, 3000); + }); + }; + + const handleDeleteUrl = (urlId) => { + const updatedItem = { ...item, urls: item.urls.filter((url) => url.id !== urlId) }; + onItemUpdate(updatedItem); + }; + + const handleDeleteImage = (imageId) => { + const updatedItem = { ...item, images: item.images.filter((image) => image.id !== imageId) }; + onItemUpdate(updatedItem); + }; + + const handleNoteUpdate = (note, text) => { + if (note) { + if (onUpdatedNote) { + onUpdatedNote(item, text, note); + } + } else { + if (onAddedNote) { + onAddedNote(item, text); + } + } + }; + + const handleNoteDelete = (note) => { + if (onDeletedNote) { + onDeletedNote(item, note); + } + }; + + return ( +
+ {item && ( +
+
+ + + {item.id}. {item.statement} + +
+ + +
+
+ {expanded && ( +
+
+ {item.scanResult && + Object.keys(item.scanResult).map((key) => { + return ( +
+ {key} +
    + {item.scanResult[key].length ? ( + item.scanResult[key].map((answer, index) => ( +
  • {answer}
  • + )) + ) : ( +
  • No answers available
  • + )} +
+
+ ); + })} +
+ +
+ +
+ setAddAsset(false)}> + Add Asset Reference + +
+
+ treeRef.current.setExpandAll(true)} + className={styles.toolbarButton} + aria-label="expand all tree items" + fontSize="small" + > +  Expand + + treeRef.current.setExpandAll(false)} + > +  Collapse + +
+ +
+
+
+ + setAssetTitle(e.target.value)} + required + /> +
+
+ + setAssetDescription(e.target.value)} + /> +
+
+
+ + + + +
+ {item.urls.length > 0 && ( +
+
+

Attached URLs:

+ +
+
+
    + {item.urls.map((url) => ( +
    +
    +
  • +
    + {url.title} +
    + {copiedUrlId === url.id ? ( + + ) : ( + handleCopy(url.id, url.hyperlink)} + /> + )} + handleDeleteUrl(url.id)} + /> +
    +
    + + {url.hyperlink} + +

    {url.description}

    +
  • +
    +
    +
    + ))} +
+
+
+ )} + {item.images.length > 0 && ( +
+
+

Attached Images:

+ +
+
+
    + {item.images.map((image) => ( +
  • +
    + {image.title} + handleDeleteImage(image.id)} + /> +
    + attached +

    {image.description}

    +
  • + ))} +
+
+
+ )} + {item.subChecklist.length > 0 && ( +
+
+

Sub-Checklist:

+ +
+
+
    + {item.subChecklist.map((subCheck) => ( +
  • +
    + {subCheck.statement} +
    + + +
    +
    +
  • + ))} +
+
+
+ )} +
+ )} +
+ )} +
+ ); +} + +ChecklistItem.propTypes = { + item: PropTypes.shape({ + id: PropTypes.number.isRequired, + name: PropTypes.string.isRequired, + statement: PropTypes.string.isRequired, + answer: PropTypes.bool.isRequired, + scanResult: PropTypes.objectOf(PropTypes.arrayOf(PropTypes.string)), + notes: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + author: PropTypes.string.isRequired, + updated: PropTypes.string.isRequired, + content: PropTypes.string.isRequired, + }), + ), + images: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + uri: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, + }), + ), + urls: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + hyperlink: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, + }), + ), + subChecklist: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + statement: PropTypes.string.isRequired, + answer: PropTypes.bool.isRequired, + }), + ), + }).isRequired, + project: PropTypes.object.isRequired, + onUpdatedNote: PropTypes.func.isRequired, + onDeletedNote: PropTypes.func.isRequired, + onAddedNote: PropTypes.func.isRequired, + onItemUpdate: PropTypes.func.isRequired, + onSelectedAsset: PropTypes.func.isRequired, +}; + +export default ChecklistItem; diff --git a/app/components/ReproChecklist/ReproChecklist.js b/app/components/ReproChecklist/ReproChecklist.js new file mode 100644 index 0000000..390a8d8 --- /dev/null +++ b/app/components/ReproChecklist/ReproChecklist.js @@ -0,0 +1,380 @@ +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import styles from './ReproChecklist.css'; +import ChecklistItem from './ChecklistItem/ChecklistItem'; +import Error from '../Error/Error'; +import { + Typography, + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, +} from '@mui/material'; +import { SaveAlt } from '@mui/icons-material'; +import pdfMake from 'pdfmake/build/pdfmake'; +import pdfFonts from 'pdfmake/build/vfs_fonts'; +import GeneralUtil from '../../utils/general'; +import ChecklistUtil from '../../utils/checklist'; +import AssetUtil from '../../utils/asset'; + +pdfMake.vfs = pdfFonts.pdfMake.vfs; + +const path = require('path'); + +function ReproChecklist(props) { + const { + project, + checklist, + error, + onUpdated, + onAddedNote, + onUpdatedNote, + onDeletedNote, + onSelectedAsset, + } = props; + const [openExportDialog, setOpenExportDialog] = useState(false); + + // this useEffect hook is here to load the scan results for all the checklist statements + useEffect(() => { + if (project && checklist && !error) { + if (project.assets) { + // scan result for the first checklist statement + const scanResult1 = ChecklistUtil.findAssetLanguagesAndDependencies(project.assets); + const scanResult2 = ChecklistUtil.findDataFiles(project.assets); + const scanResult3 = ChecklistUtil.findEntryPointFiles( + AssetUtil.findEntryPointAssets(project.assets), + ); + const scanResult4 = ChecklistUtil.findDocumentationFiles(project.assets); + checklist[0].scanResult = scanResult1; + checklist[1].scanResult = scanResult2; + checklist[2].scanResult = scanResult3; + checklist[3].scanResult = scanResult4; + } + } + }, [project]); + + // Handles the update of checklist for changes in the checklist items + const handleItemUpdate = (updatedItem) => { + const updatedChecklist = checklist.map((item) => + item.id === updatedItem.id ? updatedItem : item, + ); + onUpdated(project, updatedChecklist); + }; + + // Handles the generation of the reproducibility checklist report in PDF format + const handleReportGeneration = (exportNotes) => { + // pdfMake requires base64 encoded images + const checkedIcon = GeneralUtil.convertImageToBase64(path.join(__dirname, 'images/yes.png')); + const statWrapLogo = GeneralUtil.convertImageToBase64( + path.join(__dirname, 'images/banner.png'), + ); + + const documentDefinition = { + content: [ + { + image: statWrapLogo, + width: 150, + alignment: 'center', + }, + { + text: 'Reproducibility Checklist', + style: 'mainHeader', + alignment: 'center', + margin: [0, 20], + }, + { + text: 'Project Overview', + style: 'sectionHeader', + margin: [0, 10], + }, + { + text: `Project Name: ${project.name}`, + margin: [0, 5], + }, + { + text: `Date: ${new Date().toLocaleDateString()}`, + margin: [0, 5], + }, + { + columns: [ + { + text: `Checklist Summary`, + style: 'sectionHeader', + margin: [0, 10], + }, + { + text: 'Yes', + margin: [0, 12, 5, 0], + width: 30, + alignment: 'right', + fontSize: 16, + bold: true, + noWrap: true, + }, + { + text: 'No', + margin: [0, 12], + width: 40, + alignment: 'right', + fontSize: 16, + bold: true, + noWrap: true, + }, + ], + }, + ...checklist + .map((item, index) => { + const maxWidth = 450; + let subChecklist = []; + if (item.subChecklist && item.subChecklist.length > 0) { + subChecklist = item.subChecklist.map((subItem, subIndex) => ({ + columns: [ + { + text: `${index + 1}.${subIndex + 1} ${subItem.statement}`, + margin: [15, 5], + width: '*', + alignment: 'left', + }, + subItem.answer + ? { + image: checkedIcon, + width: 15, + alignment: 'right', + margin: [0, 5, 25, 0], + } + : { + image: checkedIcon, + width: 15, + alignment: 'right', + margin: [0, 5, 1, 0], + }, + ], + columnGap: 0, + })); + } + + let notes = []; + if (exportNotes && item.notes && item.notes.length > 0) { + notes = item.notes.map((note, noteIndex) => ({ + text: `${noteIndex + 1}. ${note.content}`, + margin: [20, 2], + width: maxWidth, + })); + } + + let images = []; + const imageWidth = 135; + if (item.images && item.images.length > 0) { + images = item.images.map((image) => { + const base64Image = GeneralUtil.convertImageToBase64(image.uri); + if (base64Image) { + return { + image: base64Image, + width: imageWidth, + margin: [0, 5], + alignment: 'center', + }; + } + return { text: `Failed to load image: ${image.uri}`, color: 'red' }; + }); + } + + // Rendering images by rows, as rendering in columns overflows the page and we can't wrap columns under each other, + // math for 3 images per row is as follows: + // imageWidth*n + calumnGap*(n-1) <= maxWidth - leftMargin - rightMargin + // 135*n + 10*(n-1) <= 450 - 20 - 0; + // n <= 440/145 --> n = 3 + const imagesPerRow = Math.floor((maxWidth - 20 + 10) / (imageWidth + 10)); + + let imageRows = []; + for (let i = 0; i < images.length; i += imagesPerRow) { + imageRows.push({ + columns: images.slice(i, i + imagesPerRow), + columnGap: 10, + margin: [20, 5], + }); + } + + let urls = []; + if (item.urls && item.urls.length > 0) { + urls = item.urls.map((url, urlIndex) => { + return { + unbreakable: true, + columns: [ + { + text: `${urlIndex + 1}. `, + width: 25, + margin: [20, 1, 0, 0], + alignment: 'left', + noWrap: true, + }, + { + stack: [ + { + text: url.title, + margin: [7, 1], + alignment: 'left', + style: 'hyperlink', + link: url.hyperlink, + }, + { + text: url.description, + margin: [7, 3], + alignment: 'left', + }, + ], + width: maxWidth, + }, + ], + }; + }); + } + + return [ + { + columns: [ + { + text: `${index + 1}. `, + width: 10, + margin: [0, 10], + alignment: 'left', + }, + { + text: `${item.statement}`, + margin: [0, 10, 25, 0], + width: maxWidth, + alignment: 'left', + bold: true, + }, + item.answer + ? { + image: checkedIcon, + width: 15, + alignment: 'right', + marginRight: 10, + marginTop: 12, + } + : { + image: checkedIcon, + width: 15, + marginLeft: 28, + marginTop: 12, + }, + ], + columnGap: 5, + }, + ...subChecklist, + notes.length > 0 ? { text: 'Notes:', margin: [15, 5] } : '', + ...notes, + images.length > 0 ? { text: 'Related Images:', margin: [15, 10] } : '', + ...imageRows, + urls.length > 0 ? { text: 'Related URLs:', margin: [15, 10] } : '', + ...urls, + ]; + }) + .flat(), + ], + styles: { + mainHeader: { fontSize: 22, bold: true, color: '#663399' }, + sectionHeader: { fontSize: 18, bold: true, color: '#8b6fb3', margin: [0, 20] }, + hyperlink: { color: '#0000EE' }, + }, + defaultStyle: { + fontSize: 12, + }, + pageMargins: [40, 25, 40, 60], + footer: function (currentPage, pageCount) { + return { + text: `Page ${currentPage} of ${pageCount}`, + alignment: 'center', + margin: [0, 30], + }; + }, + }; + + pdfMake.createPdf(documentDefinition).download('Reproducibility_Checklist.pdf'); + setOpenExportDialog(false); + }; + + let content =
Checklist not configured.
; + + if (checklist && checklist.length > 0) { + content = ( +
+ + Reproducibility Checklist + +
+ {checklist.map((item) => ( + + ))} +
+
+ +
+ + setOpenExportDialog(false)}> + Export Report + + + Do you want to include the checklist notes in the exported reproducibility checklist? + + + + {/* selective export of notes */} + + + + +
+ ); + } else if (error) { + content = There was an error loading the project checklist: {error}; + } + + return
{content}
; +} + +ReproChecklist.propTypes = { + project: PropTypes.object.isRequired, + checklist: PropTypes.arrayOf(PropTypes.object), + error: PropTypes.string, + onUpdated: PropTypes.func.isRequired, + onAddedNote: PropTypes.func.isRequired, + onUpdatedNote: PropTypes.func.isRequired, + onDeletedNote: PropTypes.func.isRequired, + onSelectedAsset: PropTypes.func.isRequired, +}; + +ReproChecklist.defaultProps = { + project: null, + checklist: null, + error: null, + onUpdated: null, + onAddedNote: null, + onUpdatedNote: null, + onDeletedNote: null, + onSelectedAsset: null, +}; + +export default ReproChecklist; diff --git a/app/utils/asset.js b/app/utils/asset.js index afa3256..755c501 100644 --- a/app/utils/asset.js +++ b/app/utils/asset.js @@ -140,6 +140,30 @@ export default class AssetUtil { return descendantsList; } + /** + * Finds all the assets marked as entry points in the asset tree + * @param {object} asset The asset to search for entry points + * @param {array} entryPointList The list of entry points found so far + * @returns {array} The list of entry points found + */ + static findEntryPointAssets(asset, entryPointList = []) { + if (!asset) { + return entryPointList; + } + + if (asset.attributes && asset.attributes.entrypoint) { + entryPointList.push(asset); + } + + if (asset.children) { + asset.children.forEach((child) => { + AssetUtil.findEntryPointAssets(child, entryPointList); + }); + } + + return entryPointList; + } + /** * Recursively collect and flatten all asset notes into an array * @param {object} asset The asset to find all notes for diff --git a/app/utils/checklist.js b/app/utils/checklist.js new file mode 100644 index 0000000..afe115f --- /dev/null +++ b/app/utils/checklist.js @@ -0,0 +1,173 @@ +import Constants from '../constants/constants'; +import AssetsConfig from '../constants/assets-config'; +import AssetUtil from './asset'; +import WorkflowUtil from './workflow'; +const path = require('path'); + +export default class ChecklistUtil { + /** + * This function initializes the checklist with the statements and seeds other properties + * @returns {object} The initialized checklist + */ + static initializeChecklist() { + const checklist = []; + Constants.CHECKLIST.forEach((statement, index) => { + checklist.push({ + id: index + 1, + name: statement[0], + statement: statement[1], + answer: false, + scanResult: {}, + notes: [], + images: [], + urls: [], + subChecklist: [], + }); + }); + return checklist; + } + + /** + * This function returns the languages and dependencies of the asset + * @param {object} asset The asset to find the languages and dependencies of + * @returns {object} An object containing the languages and dependencies found as arrays + */ + static findAssetLanguagesAndDependencies(asset) { + if (!asset) { + return { + languages: [], + dependencies: [], + }; + } + + return { + languages: ChecklistUtil.findAssetLanguages(asset), + dependencies: ChecklistUtil.findAssetDependencies(asset), + }; + } + + /** + * This function returns the languages of an asset and its children recursively + * @param {object} asset The asset to find the languages of + * @param {object} languages Empty object that acts like a map to store the languages found as keys + * @returns {array} An array containing the languages found + */ + static findAssetLanguages(asset, languages = {}) { + if ( + asset.type === Constants.AssetType.FILE && + asset.contentTypes.includes(Constants.AssetContentType.CODE) + ) { + const lastSep = asset.uri.lastIndexOf(path.sep); + const fileName = asset.uri.substring(lastSep + 1); + const ext = fileName.split('.').pop(); + + if (ext) { + AssetsConfig.contentTypes.forEach((contentType) => { + // Ensures both the extension and content type are for code files + if ( + contentType.categories.includes(Constants.AssetContentType.CODE) && + contentType.extensions.includes(ext) + ) { + languages[contentType.name] = true; + } + }); + } + } + + if (asset.children) { + asset.children.forEach((child) => { + ChecklistUtil.findAssetLanguages(child, languages); + }); + } + + return Object.keys(languages); + } + + /** + * This function returns the dependencies of an asset and its children recursively + * @param {object} asset The asset to find the dependencies of + * @param {object} dependencies Empty object that acts like a map to store the dependencies found as keys + * @returns {array} An array containing the dependencies found + */ + static findAssetDependencies(asset) { + const dependencies = []; + const assetDepedencies = WorkflowUtil.getAllLibraryDependencies(asset); + assetDepedencies.forEach((x) => { + if (x.assetType && x.assetType !== Constants.AssetType.GENERIC) { + x.dependencies.forEach((dep) => { + if (dependencies.findIndex((i) => i === dep.id) === -1) { + dependencies.push(WorkflowUtil.getShortDependencyName(dep.id)); + } + }); + } + }); + return dependencies; + } + + /** This function finds the data files in the asset and its children recursively + * @param {object} asset The asset to find the data files within + * @param {array} dataFiles An array to store the data files found + * @returns {object} An object containing the data files found + */ + static findDataFiles(asset, dataFiles = []) { + if (!asset) { + return { datafiles: dataFiles }; + } + if ( + asset.type === Constants.AssetType.FILE && + asset.contentTypes.includes(Constants.AssetContentType.DATA) + ) { + const fileName = AssetUtil.getAssetNameFromUri(asset.uri); + dataFiles.push(fileName); + } + + if (asset.children) { + asset.children.forEach((child) => { + ChecklistUtil.findDataFiles(child, dataFiles); + }); + } + + return { datafiles: dataFiles }; + } + + /** + * This function gets the entry point file names from the entryPoints assets array + * @param {array} entryPoints Array containing the entry point assets + * @returns {object} An object containing the entry point file names found + */ + static findEntryPointFiles(entryPoints) { + const entryPointFiles = []; + entryPoints?.forEach((entryPoint) => { + const fileName = AssetUtil.getAssetNameFromUri(entryPoint.uri); + entryPointFiles.push(fileName); + }); + return { entrypoints: entryPointFiles }; + } + + /** + * This function finds the documentation files in the asset + * @param {object} asset The asset to find the documentation files within + * @param {array} documentationFiles An array to store the documentation files found + * @returns {object} An object containing the documentation files found + */ + static findDocumentationFiles(asset, documentationFiles = []) { + if (!asset) { + return { documentationfiles: documentationFiles }; + } + if ( + asset.type === Constants.AssetType.FILE && + asset.contentTypes.includes(Constants.AssetContentType.DOCUMENTATION) + ) { + const fileName = AssetUtil.getAssetNameFromUri(asset.uri); + documentationFiles.push(fileName); + } + + if (asset.children) { + asset.children.forEach((child) => { + ChecklistUtil.findDocumentationFiles(child, documentationFiles); + }); + } + + return { documentationfiles: documentationFiles }; + } +} diff --git a/app/utils/workflow.js b/app/utils/workflow.js index aff630b..b997a18 100644 --- a/app/utils/workflow.js +++ b/app/utils/workflow.js @@ -46,9 +46,9 @@ export default class WorkflowUtil { // we don't count the 3 periods towards the max length (meaning, our resulting label will // technically go over the max length we detect). // eslint-disable-next-line prettier/prettier - const beginningPart = name.substring(0, (Constants.MAX_GRAPH_LABEL_LENGTH / 2)); + const beginningPart = name.substring(0, Constants.MAX_GRAPH_LABEL_LENGTH / 2); // eslint-disable-next-line prettier/prettier - const endPart = name.substring(name.length - (Constants.MAX_GRAPH_LABEL_LENGTH / 2) + 1); + const endPart = name.substring(name.length - Constants.MAX_GRAPH_LABEL_LENGTH / 2 + 1); return `${beginningPart}...${endPart}`; } @@ -310,4 +310,47 @@ export default class WorkflowUtil { } return dependencies.flat(); } + + /** + * Given an asset, get the list of library dependencies + * @param {object} asset The asset to find library dependencies for + * @returns Array of library dependencies + */ + static getLibraryDependencies(asset) { + if (!asset) { + return []; + } + + const libraries = []; + WorkflowUtil._getMetadataDependencies(asset, PythonHandler.id, libraries, [], []); + WorkflowUtil._getMetadataDependencies(asset, RHandler.id, libraries, [], []); + WorkflowUtil._getMetadataDependencies(asset, SASHandler.id, libraries, [], []); + WorkflowUtil._getMetadataDependencies(asset, StataHandler.id, libraries, [], []); + + return libraries; + } + + /** + * This function finds all library dependencies of an asset and its children recursively, that can be used to find overall project dependencies with `project.assets` as input + * @param {object} asset The asset to find the library dependencies of + * @returns {array} An array containing the library dependencies found + */ + static getAllLibraryDependencies(asset) { + const libraries = asset + ? [ + { + asset: asset.uri, + assetType: WorkflowUtil.getAssetType(asset), + dependencies: WorkflowUtil.getLibraryDependencies(asset), + }, + ] + : []; + if (!asset || !asset.children) { + return libraries.flat(); + } + for (let index = 0; index < asset.children.length; index++) { + libraries.push(WorkflowUtil.getAllLibraryDependencies(asset.children[index])); + } + return libraries.flat(); + } } diff --git a/test/utils/checklist.spec.js b/test/utils/checklist.spec.js new file mode 100644 index 0000000..8de55f7 --- /dev/null +++ b/test/utils/checklist.spec.js @@ -0,0 +1,275 @@ +import ChecklistUtil from '../../app/utils/checklist'; +import Constants from '../../app/constants/constants'; + +describe('utils', () => { + describe('ChecklistUtil', () => { + describe('findAssetLanguagesAndDependencies', () => { + it('should return empty result when asset is null or undefined', () => { + expect(ChecklistUtil.findAssetLanguagesAndDependencies(null)).toEqual({ + languages: [], + dependencies: [], + }); + expect(ChecklistUtil.findAssetLanguagesAndDependencies(undefined)).toEqual({ + languages: [], + dependencies: [], + }); + }); + + it('should return correct languages and dependencies for valid code assets', () => { + const languages = { + R: ['r', 'rmd', 'rnw', 'snw'], + Python: ['py', 'py3', 'pyi'], + SAS: ['sas'], + Stata: ['do', 'ado', 'mata'], + HTML: ['htm', 'html'], + }; + Object.keys(languages).forEach((lang) => { + languages[lang].forEach((ext) => { + expect( + ChecklistUtil.findAssetLanguagesAndDependencies({ + type: Constants.AssetType.FILE, + contentTypes: [Constants.AssetContentType.CODE], + uri: `path/to/file.${ext}`, + }), + ).toEqual({ + languages: [lang], + dependencies: [], + }); + }); + }); + }); + + it('should return empty result for non-code assets (data, documentation)', () => { + expect( + ChecklistUtil.findAssetLanguagesAndDependencies({ + type: Constants.AssetType.FILE, + contentTypes: [Constants.AssetContentType.DATA], + uri: 'path/to/file.csv', + }), + ).toEqual({ + languages: [], + dependencies: [], + }); + }); + + it('should return empty result for unmatching content type and extension', () => { + expect( + ChecklistUtil.findAssetLanguagesAndDependencies({ + type: Constants.AssetType.FILE, + contentTypes: [Constants.AssetContentType.DATA], + uri: 'path/to/file.py', + }), + ).toEqual({ + languages: [], + dependencies: [], + }); + + expect( + ChecklistUtil.findAssetLanguagesAndDependencies({ + type: Constants.AssetType.FILE, + contentTypes: [Constants.AssetContentType.CODE], + uri: 'path/to/file.csv', + }), + ).toEqual({ + languages: [], + dependencies: [], + }); + }); + + it('should return empty result for directory/folder type assets', () => { + expect( + ChecklistUtil.findAssetLanguagesAndDependencies({ + type: Constants.AssetType.DIRECTORY, + contentTypes: [Constants.AssetContentType.CODE], + uri: 'path/to/directory/', + }), + ).toEqual({ + languages: [], + dependencies: [], + }); + }); + + it('should not identify malformed URIs', () => { + expect( + ChecklistUtil.findAssetLanguagesAndDependencies({ + type: Constants.AssetType.FILE, + contentTypes: [Constants.AssetContentType.CODE], + uri: 'path/to/malformed-uri.', + }), + ).toEqual({ + languages: [], + dependencies: [], + }); + }); + + it('should ignore random/unknown extensions that are not in the content types', () => { + expect( + ChecklistUtil.findAssetLanguagesAndDependencies({ + type: Constants.AssetType.FILE, + contentTypes: [Constants.AssetContentType.CODE], + uri: 'path/to/file.cmd', + }), + ).toEqual({ + languages: [], + dependencies: [], + }); + }); + + it('should handle nested assets and recurse properly', () => { + expect( + ChecklistUtil.findAssetLanguagesAndDependencies({ + type: Constants.AssetType.FOLDER, + contentTypes: [Constants.AssetContentType.CODE], + uri: 'path/to/folder', + children: [ + { + type: Constants.AssetType.FILE, + contentTypes: [Constants.AssetContentType.CODE], + uri: 'path/to/folder/file1.py', + }, + { + type: Constants.AssetType.FILE, + contentTypes: [Constants.AssetContentType.CODE], + uri: 'path/to/folder/file2.r', + }, + ], + }), + ).toEqual({ + languages: ['Python', 'R'], // don't change the languages ordering in this array + dependencies: [], + }); + }); + + it('should not crash when asset has no extension in its URI', () => { + expect( + ChecklistUtil.findAssetLanguagesAndDependencies({ + type: Constants.AssetType.FILE, + contentTypes: [Constants.AssetContentType.CODE], + uri: 'path/to/file', + }), + ).toEqual({ + languages: [], + dependencies: [], + }); + }); + }); + + describe('findDataFiles', () => { + it('should return empty result when asset is null or undefined', () => { + expect(ChecklistUtil.findDataFiles(null)).toEqual({ datafiles: [] }); + expect(ChecklistUtil.findDataFiles(undefined)).toEqual({ datafiles: [] }); + }); + + it('should return empty result when asset is not a data file', () => { + expect( + ChecklistUtil.findDataFiles({ + type: Constants.AssetType.FILE, + contentTypes: [Constants.AssetContentType.CODE], + uri: 'path/to/file.py', + }), + ).toEqual({ datafiles: [] }); + }); + + it('should return data file name when asset is a data file', () => { + expect( + ChecklistUtil.findDataFiles({ + type: Constants.AssetType.FILE, + contentTypes: [Constants.AssetContentType.DATA], + uri: 'path/to/file.csv', + }), + ).toEqual({ datafiles: ['file.csv'] }); + }); + + it('should return data file names for nested data files', () => { + expect( + ChecklistUtil.findDataFiles({ + type: Constants.AssetType.FOLDER, + contentTypes: [Constants.AssetContentType.DATA], + uri: 'path/to/folder', + children: [ + { + type: Constants.AssetType.FILE, + contentTypes: [Constants.AssetContentType.DATA], + uri: 'path/to/folder/file1.csv', + }, + { + type: Constants.AssetType.FILE, + contentTypes: [Constants.AssetContentType.DATA], + uri: 'path/to/folder/file2.csv', + }, + ], + }), + ).toEqual({ datafiles: ['file1.csv', 'file2.csv'] }); + }); + }); + + describe('findEntryPointFiles', () => { + it('should return empty result when entryPoints is null or undefined', () => { + expect(ChecklistUtil.findEntryPointFiles(null)).toEqual({ entrypoints: [] }); + expect(ChecklistUtil.findEntryPointFiles(undefined)).toEqual({ entrypoints: [] }); + }); + + it('should return entry point file names for given entrypoint assets', () => { + expect( + ChecklistUtil.findEntryPointFiles([ + { + uri: 'path/to/file1.py', + }, + { + uri: 'path/to/file2.py', + }, + ]), + ).toEqual({ entrypoints: ['file1.py', 'file2.py'] }); + }); + }); + + describe('findDocumentationFiles', () => { + it('should return empty result when asset is null or undefined', () => { + expect(ChecklistUtil.findDocumentationFiles(null)).toEqual({ documentationfiles: [] }); + expect(ChecklistUtil.findDocumentationFiles(undefined)).toEqual({ documentationfiles: [] }); + }); + + it('should return empty result when asset is not a documentation file', () => { + expect( + ChecklistUtil.findDocumentationFiles({ + type: Constants.AssetType.FILE, + contentTypes: [Constants.AssetContentType.CODE], + uri: 'path/to/file.py', + }), + ).toEqual({ documentationfiles: [] }); + }); + + it('should return documentation file name when asset is a documentation file', () => { + expect( + ChecklistUtil.findDocumentationFiles({ + type: Constants.AssetType.FILE, + contentTypes: [Constants.AssetContentType.DOCUMENTATION], + uri: 'path/to/file.md', + }), + ).toEqual({ documentationfiles: ['file.md'] }); + }); + + it('should return documentation file names for nested documentation files', () => { + expect( + ChecklistUtil.findDocumentationFiles({ + type: Constants.AssetType.FOLDER, + contentTypes: [Constants.AssetContentType.DOCUMENTATION], + uri: 'path/to/folder', + children: [ + { + type: Constants.AssetType.FILE, + contentTypes: [Constants.AssetContentType.DOCUMENTATION], + uri: 'path/to/folder/file1.md', + }, + { + type: Constants.AssetType.FILE, + contentTypes: [Constants.AssetContentType.DOCUMENTATION], + uri: 'path/to/folder/file2.md', + }, + ], + }), + ).toEqual({ documentationfiles: ['file1.md', 'file2.md'] }); + }); + }); + }); +}); From 216396fc6deafe6d22df4d07b4c0bc5883351ae7 Mon Sep 17 00:00:00 2001 From: AdiAkhileshSingh15 Date: Sun, 27 Oct 2024 21:34:04 +0530 Subject: [PATCH 2/4] minor refactor Signed-off-by: AdiAkhileshSingh15 --- .../ReproChecklist/ReproChecklist.js | 23 ++++++++++--------- app/utils/checklist.js | 5 ++-- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/app/components/ReproChecklist/ReproChecklist.js b/app/components/ReproChecklist/ReproChecklist.js index 390a8d8..a3ab4f5 100644 --- a/app/components/ReproChecklist/ReproChecklist.js +++ b/app/components/ReproChecklist/ReproChecklist.js @@ -23,6 +23,13 @@ pdfMake.vfs = pdfFonts.pdfMake.vfs; const path = require('path'); +const scanFunctions = [ + ChecklistUtil.findAssetLanguagesAndDependencies, + ChecklistUtil.findDataFiles, + ChecklistUtil.findEntryPointFiles, + ChecklistUtil.findDocumentationFiles, +]; + function ReproChecklist(props) { const { project, @@ -40,17 +47,11 @@ function ReproChecklist(props) { useEffect(() => { if (project && checklist && !error) { if (project.assets) { - // scan result for the first checklist statement - const scanResult1 = ChecklistUtil.findAssetLanguagesAndDependencies(project.assets); - const scanResult2 = ChecklistUtil.findDataFiles(project.assets); - const scanResult3 = ChecklistUtil.findEntryPointFiles( - AssetUtil.findEntryPointAssets(project.assets), - ); - const scanResult4 = ChecklistUtil.findDocumentationFiles(project.assets); - checklist[0].scanResult = scanResult1; - checklist[1].scanResult = scanResult2; - checklist[2].scanResult = scanResult3; - checklist[3].scanResult = scanResult4; + // scan the project assets for each checklist statement + scanFunctions.forEach((scanFunction, index) => { + const scanResult = scanFunction(project.assets); + checklist[index].scanResult = scanResult; + }); } } }, [project]); diff --git a/app/utils/checklist.js b/app/utils/checklist.js index afe115f..deefc72 100644 --- a/app/utils/checklist.js +++ b/app/utils/checklist.js @@ -132,10 +132,11 @@ export default class ChecklistUtil { /** * This function gets the entry point file names from the entryPoints assets array - * @param {array} entryPoints Array containing the entry point assets + * @param {object} asset The asset to find the entry point files within * @returns {object} An object containing the entry point file names found */ - static findEntryPointFiles(entryPoints) { + static findEntryPointFiles(asset) { + const entryPoints = AssetUtil.findEntryPointAssets(asset); const entryPointFiles = []; entryPoints?.forEach((entryPoint) => { const fileName = AssetUtil.getAssetNameFromUri(entryPoint.uri); From c37c44a562d98918543f510b6838e4437d8caf22 Mon Sep 17 00:00:00 2001 From: AdiAkhileshSingh15 Date: Tue, 29 Oct 2024 13:48:42 +0530 Subject: [PATCH 3/4] review fixes Signed-off-by: AdiAkhileshSingh15 --- .../ReproChecklist/ReproChecklist.js | 23 ++++--- app/constants/constants.js | 2 +- app/utils/checklist.js | 10 +-- app/utils/workflow.js | 2 - test/utils/checklist.spec.js | 61 ++++++++++++------- 5 files changed, 60 insertions(+), 38 deletions(-) diff --git a/app/components/ReproChecklist/ReproChecklist.js b/app/components/ReproChecklist/ReproChecklist.js index a3ab4f5..3cb40ea 100644 --- a/app/components/ReproChecklist/ReproChecklist.js +++ b/app/components/ReproChecklist/ReproChecklist.js @@ -18,17 +18,19 @@ import pdfFonts from 'pdfmake/build/vfs_fonts'; import GeneralUtil from '../../utils/general'; import ChecklistUtil from '../../utils/checklist'; import AssetUtil from '../../utils/asset'; +import Constants from '../../constants/constants'; pdfMake.vfs = pdfFonts.pdfMake.vfs; const path = require('path'); -const scanFunctions = [ - ChecklistUtil.findAssetLanguagesAndDependencies, - ChecklistUtil.findDataFiles, - ChecklistUtil.findEntryPointFiles, - ChecklistUtil.findDocumentationFiles, -]; +// These functions are mapped using the statement type to the corresponding scan function +const scanFunctions = { + Dependency: ChecklistUtil.findAssetLanguagesAndDependencies, + Data: ChecklistUtil.findDataFiles, + Entrypoint: ChecklistUtil.findEntryPointFiles, + Documentation: ChecklistUtil.findDocumentationFiles, +}; function ReproChecklist(props) { const { @@ -48,9 +50,12 @@ function ReproChecklist(props) { if (project && checklist && !error) { if (project.assets) { // scan the project assets for each checklist statement - scanFunctions.forEach((scanFunction, index) => { - const scanResult = scanFunction(project.assets); - checklist[index].scanResult = scanResult; + Constants.CHECKLIST.forEach((statement, index) => { + // if used here as there might be a statement that doesn't have a corresponding scan function + if (scanFunctions[statement[0]]) { + const scanResult = scanFunctions[statement[0]](project.assets); + checklist[index].scanResult = scanResult; + } }); } } diff --git a/app/constants/constants.js b/app/constants/constants.js index fb4e9f0..76737af 100644 --- a/app/constants/constants.js +++ b/app/constants/constants.js @@ -72,7 +72,7 @@ module.exports = { PROJECT: 'project', PERSON: 'person', ASSET: 'asset', - EXTERNAL_ASSET: 'external asset', // Slightly different from 'asset' in that it lives outside the project folder + EXTERNAL_ASSET: 'external asset', // Slightly different from 'asset' in that it lives outside the project folder CHECKLIST: 'checklist', }, diff --git a/app/utils/checklist.js b/app/utils/checklist.js index deefc72..759cba1 100644 --- a/app/utils/checklist.js +++ b/app/utils/checklist.js @@ -111,7 +111,7 @@ export default class ChecklistUtil { */ static findDataFiles(asset, dataFiles = []) { if (!asset) { - return { datafiles: dataFiles }; + return { dataFiles: dataFiles }; } if ( asset.type === Constants.AssetType.FILE && @@ -127,7 +127,7 @@ export default class ChecklistUtil { }); } - return { datafiles: dataFiles }; + return { dataFiles: dataFiles }; } /** @@ -142,7 +142,7 @@ export default class ChecklistUtil { const fileName = AssetUtil.getAssetNameFromUri(entryPoint.uri); entryPointFiles.push(fileName); }); - return { entrypoints: entryPointFiles }; + return { entryPoints: entryPointFiles }; } /** @@ -153,7 +153,7 @@ export default class ChecklistUtil { */ static findDocumentationFiles(asset, documentationFiles = []) { if (!asset) { - return { documentationfiles: documentationFiles }; + return { documentationFiles: documentationFiles }; } if ( asset.type === Constants.AssetType.FILE && @@ -169,6 +169,6 @@ export default class ChecklistUtil { }); } - return { documentationfiles: documentationFiles }; + return { documentationFiles: documentationFiles }; } } diff --git a/app/utils/workflow.js b/app/utils/workflow.js index b997a18..f24bff0 100644 --- a/app/utils/workflow.js +++ b/app/utils/workflow.js @@ -45,9 +45,7 @@ export default class WorkflowUtil { // approximating it. So we roughly cut the max length in half, but it may not be exact, and // we don't count the 3 periods towards the max length (meaning, our resulting label will // technically go over the max length we detect). - // eslint-disable-next-line prettier/prettier const beginningPart = name.substring(0, Constants.MAX_GRAPH_LABEL_LENGTH / 2); - // eslint-disable-next-line prettier/prettier const endPart = name.substring(name.length - Constants.MAX_GRAPH_LABEL_LENGTH / 2 + 1); return `${beginningPart}...${endPart}`; diff --git a/test/utils/checklist.spec.js b/test/utils/checklist.spec.js index 8de55f7..93439f6 100644 --- a/test/utils/checklist.spec.js +++ b/test/utils/checklist.spec.js @@ -156,8 +156,8 @@ describe('utils', () => { describe('findDataFiles', () => { it('should return empty result when asset is null or undefined', () => { - expect(ChecklistUtil.findDataFiles(null)).toEqual({ datafiles: [] }); - expect(ChecklistUtil.findDataFiles(undefined)).toEqual({ datafiles: [] }); + expect(ChecklistUtil.findDataFiles(null)).toEqual({ dataFiles: [] }); + expect(ChecklistUtil.findDataFiles(undefined)).toEqual({ dataFiles: [] }); }); it('should return empty result when asset is not a data file', () => { @@ -167,7 +167,7 @@ describe('utils', () => { contentTypes: [Constants.AssetContentType.CODE], uri: 'path/to/file.py', }), - ).toEqual({ datafiles: [] }); + ).toEqual({ dataFiles: [] }); }); it('should return data file name when asset is a data file', () => { @@ -177,7 +177,7 @@ describe('utils', () => { contentTypes: [Constants.AssetContentType.DATA], uri: 'path/to/file.csv', }), - ).toEqual({ datafiles: ['file.csv'] }); + ).toEqual({ dataFiles: ['file.csv'] }); }); it('should return data file names for nested data files', () => { @@ -199,34 +199,48 @@ describe('utils', () => { }, ], }), - ).toEqual({ datafiles: ['file1.csv', 'file2.csv'] }); + ).toEqual({ dataFiles: ['file1.csv', 'file2.csv'] }); }); }); describe('findEntryPointFiles', () => { it('should return empty result when entryPoints is null or undefined', () => { - expect(ChecklistUtil.findEntryPointFiles(null)).toEqual({ entrypoints: [] }); - expect(ChecklistUtil.findEntryPointFiles(undefined)).toEqual({ entrypoints: [] }); + expect(ChecklistUtil.findEntryPointFiles(null)).toEqual({ entryPoints: [] }); + expect(ChecklistUtil.findEntryPointFiles(undefined)).toEqual({ entryPoints: [] }); }); it('should return entry point file names for given entrypoint assets', () => { expect( - ChecklistUtil.findEntryPointFiles([ - { - uri: 'path/to/file1.py', - }, - { - uri: 'path/to/file2.py', - }, - ]), - ).toEqual({ entrypoints: ['file1.py', 'file2.py'] }); + ChecklistUtil.findEntryPointFiles({ + type: Constants.AssetType.FOLDER, + uri: 'path/to/folder', + children: [ + { + type: Constants.AssetType.FILE, + contentTypes: [Constants.AssetContentType.CODE], + uri: 'path/to/folder/file1.py', + attributes: { + entrypoint: true, + }, + }, + { + type: Constants.AssetType.FILE, + contentTypes: [Constants.AssetContentType.CODE], + uri: 'path/to/folder/file2.py', + attributes: { + entrypoint: false, + }, + }, + ], + }), + ).toEqual({ entryPoints: ['file1.py'] }); }); }); describe('findDocumentationFiles', () => { it('should return empty result when asset is null or undefined', () => { - expect(ChecklistUtil.findDocumentationFiles(null)).toEqual({ documentationfiles: [] }); - expect(ChecklistUtil.findDocumentationFiles(undefined)).toEqual({ documentationfiles: [] }); + expect(ChecklistUtil.findDocumentationFiles(null)).toEqual({ documentationFiles: [] }); + expect(ChecklistUtil.findDocumentationFiles(undefined)).toEqual({ documentationFiles: [] }); }); it('should return empty result when asset is not a documentation file', () => { @@ -236,7 +250,7 @@ describe('utils', () => { contentTypes: [Constants.AssetContentType.CODE], uri: 'path/to/file.py', }), - ).toEqual({ documentationfiles: [] }); + ).toEqual({ documentationFiles: [] }); }); it('should return documentation file name when asset is a documentation file', () => { @@ -246,7 +260,7 @@ describe('utils', () => { contentTypes: [Constants.AssetContentType.DOCUMENTATION], uri: 'path/to/file.md', }), - ).toEqual({ documentationfiles: ['file.md'] }); + ).toEqual({ documentationFiles: ['file.md'] }); }); it('should return documentation file names for nested documentation files', () => { @@ -266,9 +280,14 @@ describe('utils', () => { contentTypes: [Constants.AssetContentType.DOCUMENTATION], uri: 'path/to/folder/file2.md', }, + { + type: Constants.AssetType.FILE, + contentTypes: [Constants.AssetContentType.CODE], + uri: 'path/to/folder/file3.py', + }, ], }), - ).toEqual({ documentationfiles: ['file1.md', 'file2.md'] }); + ).toEqual({ documentationFiles: ['file1.md', 'file2.md'] }); }); }); }); From 3ca302daff3bff495faa7025bdd575a3c83853a6 Mon Sep 17 00:00:00 2001 From: AdiAkhileshSingh15 Date: Tue, 29 Oct 2024 21:38:35 +0530 Subject: [PATCH 4/4] integrates the newly added url assets in the checklist attachments Signed-off-by: AdiAkhileshSingh15 --- .../ChecklistItem/ChecklistItem.js | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/app/components/ReproChecklist/ChecklistItem/ChecklistItem.js b/app/components/ReproChecklist/ChecklistItem/ChecklistItem.js index ca73324..3d617f7 100644 --- a/app/components/ReproChecklist/ChecklistItem/ChecklistItem.js +++ b/app/components/ReproChecklist/ChecklistItem/ChecklistItem.js @@ -30,6 +30,14 @@ function ChecklistItem(props) { } = props; const treeRef = React.useRef(null); + const externalTreeRef = React.useRef(null); + + const [assets, setAssets] = useState(project && project.assets); + const [externalAssets, setExternalAssets] = useState( + project && project.externalAssets + ? project.externalAssets + : AssetUtil.createEmptyExternalAssets(), + ); const [expanded, setExpanded] = useState(false); @@ -54,7 +62,7 @@ function ChecklistItem(props) { const handleSelectAsset = (selAsset) => { let asset = selAsset; if (asset && (asset.contentTypes === null || asset.contentTypes === undefined)) { - if (project && project.assets) { + if (project && project.assets && asset.type === Constants.AssetType.FILE) { asset = AssetUtil.findDescendantAssetByUri(project.assets, asset.uri); } } @@ -66,7 +74,7 @@ function ChecklistItem(props) { // Handles case for URL assets // TODO: Title is currently the same as URI, some way to get a better title? } else if (asset.type === Constants.AssetType.URL) { - setAssetTitle(asset.uri); + setAssetTitle(asset.name); } else { setAssetTitle(''); } @@ -276,11 +284,17 @@ function ChecklistItem(props) { +