From beafdd86eb6a01fd1c323317b39a8fa8e5812f6c Mon Sep 17 00:00:00 2001 From: dibyanshu-pal-kushwaha Date: Sat, 28 Feb 2026 19:26:10 +0530 Subject: [PATCH] Allow adding additional folders to asset list --- .../AssetAttributes/AssetAttributes.js | 12 +-- app/components/AssetDetails/AssetDetails.js | 42 +++++----- .../AssetTree/AssetNode/AssetNode.js | 7 +- app/components/AssetTree/AssetTree.js | 2 +- app/components/Project/Assets/Assets.js | 26 +++++-- app/components/Project/Project.js | 25 ++++-- .../ChecklistItem/ChecklistItem.js | 62 +++++++-------- .../ExternalAssetDialog.js | 76 ++++++++++++++++--- app/containers/ProjectPage/ProjectPage.js | 7 +- app/main.dev.js | 8 +- app/preload.js | 37 ++++++++- app/utils/asset.js | 8 +- app/utils/project.js | 2 + test/utils/project.spec.js | 5 +- 14 files changed, 227 insertions(+), 92 deletions(-) diff --git a/app/components/AssetAttributes/AssetAttributes.js b/app/components/AssetAttributes/AssetAttributes.js index 79556786..c2da4d9a 100644 --- a/app/components/AssetAttributes/AssetAttributes.js +++ b/app/components/AssetAttributes/AssetAttributes.js @@ -12,11 +12,13 @@ const assetAttributes = (props) => { const applicableAttributes = configuration .map((a) => { - // If the asset doesn't have a content type (needed for attribute detection), - // or the attribute doesn't apply to this asset, skip it - if (asset.contentTypes == null || - (!a.appliesTo.includes('*') && !a.appliesTo.some((x) => asset.contentTypes.includes(x)))) { - return null; + const appliesToAll = a.appliesTo.includes('*'); + // If the attribute doesn't apply to everything, we need to check the content type. + // If the asset has no content type, or its content types don't match our allowed ones, skip it. + if (!appliesToAll) { + if (asset.contentTypes == null || !a.appliesTo.some((x) => asset.contentTypes.includes(x))) { + return null; + } } return a; }) diff --git a/app/components/AssetDetails/AssetDetails.js b/app/components/AssetDetails/AssetDetails.js index c116c55c..cb8c637b 100644 --- a/app/components/AssetDetails/AssetDetails.js +++ b/app/components/AssetDetails/AssetDetails.js @@ -11,6 +11,7 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import AssetAttributes from '../AssetAttributes/AssetAttributes'; import NoteEditor from '../NoteEditor/NoteEditor'; import Loading from '../Loading/Loading'; +import AssetUtil from '../../utils/asset'; import SourceControlHistory from '../SourceControlHistory/SourceControlHistory'; import styles from './AssetDetails.css'; import { styled } from '@mui/material/styles'; @@ -93,7 +94,7 @@ const assetDetails = (props) => { } }; - const isExternalAsset = asset && asset.type === Constants.AssetType.URL; + const isExternalAsset = AssetUtil.isExternalAsset(asset); let sourceControlAccordion = null; if (!isExternalAsset && sourceControlEnabled) { @@ -115,27 +116,24 @@ const assetDetails = (props) => { ); } - let attributesAccordion = null; - if (!isExternalAsset) { - attributesAccordion = ( - - } - aria-controls="attributes-content" - id="attributes-header" - > - Attributes - - - - - - ); - } + let attributesAccordion = ( + + } + aria-controls="attributes-content" + id="attributes-header" + > + Attributes + + + + + + ); let actions = null; if (isExternalAsset) { diff --git a/app/components/AssetTree/AssetNode/AssetNode.js b/app/components/AssetTree/AssetNode/AssetNode.js index a290d7db..ff4ae419 100644 --- a/app/components/AssetTree/AssetNode/AssetNode.js +++ b/app/components/AssetTree/AssetNode/AssetNode.js @@ -124,15 +124,15 @@ function AssetNode(props) { }} > onToggle(node)}> - {node.type === Constants.AssetType.DIRECTORY && + {(node.type === Constants.AssetType.DIRECTORY || node.type === Constants.AssetType.FOLDER) && (isOpen ? : )} {checkbox} {node.type === Constants.AssetType.FILE && (!node.attributes || !node.attributes.entrypoint) && } {node.type === Constants.AssetType.FILE && node.attributes && node.attributes.entrypoint && } - {node.type === Constants.AssetType.DIRECTORY && isOpen === true && } - {node.type === Constants.AssetType.DIRECTORY && !isOpen && } + {(node.type === Constants.AssetType.DIRECTORY || node.type === Constants.AssetType.FOLDER) && isOpen === true && } + {(node.type === Constants.AssetType.DIRECTORY || node.type === Constants.AssetType.FOLDER) && !isOpen && } {node.type === Constants.AssetType.ASSET_GROUP && } {node.type === Constants.AssetType.FILTER && } {node.type === Constants.AssetType.URL && } @@ -141,6 +141,7 @@ function AssetNode(props) { {AssetUtil.getAssetNameForTree(node)} + {isOpen && (!node.children ? null diff --git a/app/components/AssetTree/AssetTree.js b/app/components/AssetTree/AssetTree.js index e8fc5135..a024b6d6 100644 --- a/app/components/AssetTree/AssetTree.js +++ b/app/components/AssetTree/AssetTree.js @@ -51,7 +51,7 @@ class AssetTree extends Component { // node - the node to try and add (will only be added if a directory) // expandedNodes - the collection of URIs of expanded nodes setNodeExpanded = (node, expandedNodes) => { - if (node.type === Constants.AssetType.DIRECTORY) { + if (node.type === Constants.AssetType.DIRECTORY || node.type === Constants.AssetType.FOLDER) { expandedNodes.push(node.uri); if (node.children) { node.children.forEach((x) => this.setNodeExpanded(x, expandedNodes)); diff --git a/app/components/Project/Assets/Assets.js b/app/components/Project/Assets/Assets.js index dc2d52af..53d2de57 100644 --- a/app/components/Project/Assets/Assets.js +++ b/app/components/Project/Assets/Assets.js @@ -90,8 +90,14 @@ const assetsComponent = (props) => { // in that case. useEffect(() => { // Get a more recent copy of the asset from the updated project - if (project && project.assets && selectedAsset) { - const updatedAsset = AssetUtil.findDescendantAssetByUri(project.assets, selectedAsset.uri); + if (project && selectedAsset) { + let updatedAsset = null; + if (project.assets) { + updatedAsset = AssetUtil.findDescendantAssetByUri(project.assets, selectedAsset.uri); + } + if (!updatedAsset && project.externalAssets) { + updatedAsset = AssetUtil.findDescendantAssetByUri(project.externalAssets, selectedAsset.uri); + } setSelectedAsset(updatedAsset); if (onSelectedAsset) { onSelectedAsset(updatedAsset); @@ -112,7 +118,7 @@ const assetsComponent = (props) => { setCurrentAssetGroup(null); setGroupedAssets(null); setFilterEnabled(true); - setExternalAssets(project.externalAssets ? project.externalAssets : AssetUtil.createEmptyExternalAssets()); + setExternalAssets(project.externalAssets ? ProjectUtil._filterAssets(project.externalAssets, null) : AssetUtil.createEmptyExternalAssets()); }, [project]); // Whenever the filter changes, update the list of assets to include only @@ -120,11 +126,13 @@ const assetsComponent = (props) => { const handleFilterChanged = (updatedFilter) => { setFilter(updatedFilter); setAssets(ProjectUtil.filterProjectAssets(project, updatedFilter)); + setExternalAssets(project.externalAssets ? ProjectUtil._filterAssets(project.externalAssets, updatedFilter) : AssetUtil.createEmptyExternalAssets()); }; const handleFilterReset = () => { setFilter(resetFilter(project)); setAssets(ProjectUtil.filterProjectAssets(project, null)); + setExternalAssets(project.externalAssets ? ProjectUtil._filterAssets(project.externalAssets, null) : AssetUtil.createEmptyExternalAssets()); }; // When the user triggers saving an Asset Group, update the UI so the dialog @@ -292,8 +300,14 @@ const assetsComponent = (props) => { // The extra logic we use here is to enrich the selected asset, if it's missing some key // information. The key thing we have right now is if it's missing the 'contentTypes' attribute. if (asset && (asset.contentTypes === null || asset.contentTypes === undefined)) { - if (project && project.assets) { - const foundAsset = AssetUtil.findDescendantAssetByUri(project.assets, asset.uri); + if (project) { + let foundAsset = null; + if (project.assets) { + foundAsset = AssetUtil.findDescendantAssetByUri(project.assets, asset.uri); + } + if (!foundAsset && project.externalAssets) { + foundAsset = AssetUtil.findDescendantAssetByUri(project.externalAssets, asset.uri); + } if (foundAsset !== null) { asset = foundAsset; } @@ -474,6 +488,8 @@ const assetsComponent = (props) => { onSave={handleSavedExternalAsset} uri={editableExternalAsset ? editableExternalAsset.uri : ''} name={editableExternalAsset ? editableExternalAsset.name : ''} + type={editableExternalAsset ? editableExternalAsset.type : Constants.AssetType.URL} + isNew={!editableExternalAsset} /> ); diff --git a/app/components/Project/Project.js b/app/components/Project/Project.js index 0cde8ca4..7a3913db 100644 --- a/app/components/Project/Project.js +++ b/app/components/Project/Project.js @@ -241,7 +241,7 @@ class Project extends Component { let assetsCopy = null; // Depending on if this is a core asset or an external asset, we need to select the correct container. The rest of // the code will work the same. - const isExternalAsset = (asset.type === AssetType.URL); + const isExternalAsset = AssetUtil.isExternalAsset(asset); if (isExternalAsset) { assetsCopy = { ...project.externalAssets }; } else { @@ -367,7 +367,16 @@ class Project extends Component { */ assetUpdateAttributeHandler = (asset, name, value) => { const project = { ...this.props.project }; - const assetsCopy = { ...project.assets }; + + // Depending on if this is an external asset or a main asset, determine the right collection + // of assets to use. + let assetsCopy = null; + const isExternalAsset = AssetUtil.isExternalAsset(asset); + if (isExternalAsset) { + assetsCopy = { ...project.externalAssets }; + } else { + assetsCopy = { ...project.assets }; + } const existingAsset = AssetUtil.findDescendantAssetByUri(assetsCopy, asset.uri); let actionDescription = ''; @@ -382,12 +391,18 @@ class Project extends Component { } existingAsset.attributes[name] = value; - project.assets = assetsCopy; + + if (isExternalAsset) { + project.externalAssets = assetsCopy; + } else { + project.assets = assetsCopy; + } + if (this.props.onUpdated) { this.props.onUpdated( project, ActionType.ATTRIBUTE_UPDATED, - EntityType.ASSET, + isExternalAsset ? EntityType.EXTERNAL_ASSET : EntityType.ASSET, asset.uri, `Asset ${ActionType.ATTRIBUTE_UPDATED}`, actionDescription, @@ -409,7 +424,7 @@ class Project extends Component { // Depending on if this is an external asset or a main asset, determine the right collection // of assets to use. let assetsCopy = null; - const isExternalAsset = (asset.type === AssetType.URL); + const isExternalAsset = AssetUtil.isExternalAsset(asset); if (isExternalAsset) { assetsCopy = { ...project.externalAssets }; } else { diff --git a/app/components/ReproChecklist/ChecklistItem/ChecklistItem.js b/app/components/ReproChecklist/ChecklistItem/ChecklistItem.js index 9cc31896..33f0c5cb 100644 --- a/app/components/ReproChecklist/ChecklistItem/ChecklistItem.js +++ b/app/components/ReproChecklist/ChecklistItem/ChecklistItem.js @@ -109,7 +109,7 @@ function ChecklistItem(props) { Constants.ActionType.CHECKLIST_UPDATED, Constants.ActionType.CHECKLIST_UPDATED, `Set "${updatedItem.statement}" to "${formatYesNo(newValue)}"`, - {oldValue: formatYesNo(item.Value), newValue: formatYesNo(newValue)} + { oldValue: formatYesNo(item.Value), newValue: formatYesNo(newValue) } ); }; @@ -141,7 +141,7 @@ function ChecklistItem(props) { Constants.ActionType.CHECKLIST_UPDATED, Constants.ActionType.CHECKLIST_UPDATED, `Set sub-item ${subCheck.id} of ${updatedItem.statement} to "Yes"`, - {oldValue: 'No', newValue: 'Yes'} + { oldValue: 'No', newValue: 'Yes' } ); }; @@ -154,8 +154,8 @@ function ChecklistItem(props) { } if (asset && asset.uri) { - if (asset.type === Constants.AssetType.URL) { - setAssetTitle(asset.name); + if (AssetUtil.isExternalAsset(asset)) { + setAssetTitle(asset.name ? asset.name : asset.uri); } else { const fileName = AssetUtil.getAssetNameFromUri(asset.uri); setAssetTitle(fileName); @@ -207,10 +207,10 @@ function ChecklistItem(props) { const formatDisplayLink = (asset) => { if (asset.isExternalAsset) { - return({asset.uri}); + return ({asset.uri}); } //return (
{AssetUtil.absoluteToRelativePath(project.path, asset)}
); - return({AssetUtil.absoluteToRelativePath(project.path, asset)}); + return ({AssetUtil.absoluteToRelativePath(project.path, asset)}); }; const handleCopy = (uri) => { @@ -321,33 +321,33 @@ function ChecklistItem(props) {
- {item.assets.map((asset) => ( - - - + + - - ))} + + + ))}
-
{asset.name}
-
{formatDisplayLink(asset)}
-
{asset.description}
-
- {copiedAsset === asset.uri ? ( - - ) : ( - - handleCopy(asset.uri)} + {item.assets.map((asset) => ( +
+
{asset.name}
+
{formatDisplayLink(asset)}
+
{asset.description}
+
+ {copiedAsset === asset.uri ? ( + + ) : ( + + handleCopy(asset.uri)} + /> + + )} + + handleDeleteAsset(asset.uri)} /> - )} - - handleDeleteAsset(asset.uri)} - /> - -
diff --git a/app/containers/ExternalAssetDialog/ExternalAssetDialog.js b/app/containers/ExternalAssetDialog/ExternalAssetDialog.js index 90d6b576..91e9a33f 100644 --- a/app/containers/ExternalAssetDialog/ExternalAssetDialog.js +++ b/app/containers/ExternalAssetDialog/ExternalAssetDialog.js @@ -1,7 +1,9 @@ import React, { Component, useRef } from 'react'; import PropTypes from 'prop-types'; import Draggable from 'react-draggable'; -import { Dialog, DialogActions, DialogTitle, Button, Paper } from '@mui/material'; +import { Dialog, DialogActions, DialogTitle, Button, Paper, Select, MenuItem } from '@mui/material'; +import { dialog } from '@electron/remote'; +const fs = require('fs'); import Error from '../../components/Error/Error'; import Constants from '../../constants/constants'; import GeneralUtil from '../../utils/general'; @@ -25,14 +27,16 @@ class ExternalAssetDialog extends Component { errorMessage: null, uri: props.uri ? props.uri : '', name: props.name ? props.name : '', - type: Constants.AssetType.URL, // For now, always URL - isNew: props.isNew ? props.isNew : true, + type: props.type ? props.type : Constants.AssetType.URL, + isNew: props.isNew !== undefined ? props.isNew : true, validPath: true }; this.handleInputChange = this.handleInputChange.bind(this); this.handleSave = this.handleSave.bind(this); this.handleValidatePath = this.handleValidatePath.bind(this); + this.handleTypeChange = this.handleTypeChange.bind(this); + this.handleBrowse = this.handleBrowse.bind(this); } handleSave() { @@ -43,7 +47,7 @@ class ExternalAssetDialog extends Component { }; if (asset.name.trim() === '' || asset.uri.trim() === '') { - this.setState({errorMessage: 'You must enter a resource name and URL'}); + this.setState({ errorMessage: 'You must enter a resource name and URL' }); return; } @@ -63,7 +67,37 @@ class ExternalAssetDialog extends Component { handleValidatePath(event) { const { target } = event; const value = target.value; - this.setState({validPath: GeneralUtil.isValidResourceUrl(value)}); + if (this.state.type === Constants.AssetType.URL) { + this.setState({ validPath: GeneralUtil.isValidResourceUrl(value) }); + } else { + let isValid = false; + try { + isValid = fs.existsSync(value); + } catch (e) { } + this.setState({ validPath: isValid || value.trim() === '' }); + } + } + + handleTypeChange(event) { + this.setState({ type: event.target.value, uri: '', validPath: true }); + } + + handleBrowse() { + const properties = this.state.type === Constants.AssetType.FOLDER ? ['openDirectory'] : ['openFile']; + dialog + .showOpenDialog({ + title: `Select the ${this.state.type === Constants.AssetType.FOLDER ? 'folder' : 'file'}`, + properties: properties, + }) + .then((result) => { + if (!result.canceled && result.filePaths !== null && result.filePaths.length > 0) { + this.setState({ uri: result.filePaths[0], validPath: true }); + } + return result; + }) + .catch((err) => { + this.setState({ errorMessage: `There was an error accessing the path: ${err}` }); + }); } render() { @@ -74,11 +108,10 @@ class ExternalAssetDialog extends Component { let pathError = null; if (!this.state.validPath) { - pathError =
Please check that your URL is valid
+ pathError =
Please check that your {this.state.type === Constants.AssetType.URL ? 'URL' : 'path'} is valid
} let dialogAction = this.state.isNew ? 'Add' : 'Edit'; - return (
+ {this.state.isNew ? ( +
+ + +
+ ) : ( +
+ +
+ {this.state.type === Constants.AssetType.URL ? 'URL' : this.state.type === Constants.AssetType.FOLDER ? 'Folder' : 'File'} +
+
+ )}
- + + {this.state.type !== Constants.AssetType.URL && ( + + )} {pathError}
diff --git a/app/containers/ProjectPage/ProjectPage.js b/app/containers/ProjectPage/ProjectPage.js index e9ba6876..d8f3c8f4 100644 --- a/app/containers/ProjectPage/ProjectPage.js +++ b/app/containers/ProjectPage/ProjectPage.js @@ -423,6 +423,7 @@ class ProjectPage extends Component { const projectWithAssets = { ...selectedProject, sourceControlEnabled: response.project.sourceControlEnabled, + externalAssets: response.project.externalAssets, assets: response.error ? { error: response.error, errorMessage: response.errorMessage } : response.assets, @@ -692,8 +693,8 @@ class ProjectPage extends Component { this.state.projectListMenuAnchor ? this.state.projectListMenuAnchor.element : null } project={ - this.state.projectListMenuAnchor ? this.state.projectListMenuAnchor.project : null - } + this.state.projectListMenuAnchor ? this.state.projectListMenuAnchor.project : null + } onClose={this.handleCloseProjectListMenu} onMenuClick={this.handleClickProjectListMenu} /> @@ -701,7 +702,7 @@ class ProjectPage extends Component { open={this.state.showDirtyConfirmation} onClose={this.handleCancelSwitch} > - Discard Changes + Discard Changes You have unsaved changes in the current project. Switching to another project will discard these changes. Are you sure you want to proceed? diff --git a/app/main.dev.js b/app/main.dev.js index da28369b..021b156a 100644 --- a/app/main.dev.js +++ b/app/main.dev.js @@ -128,7 +128,7 @@ const createWindow = async () => { // Shared handler to take a URL and determine if we should open it in the user's // browser (returns true), or if we let electron handle it normally (false). - async function handleUrl(url: string) { + async function handleUrl(url) { try { const parsedUrl = new URL(url); const { protocol } = parsedUrl; @@ -794,6 +794,12 @@ ipcMain.on( response = ProjectUtil.saveProject(updatedProject, projectService); if (response && !response.error) { logService.writeLog(projectPath, actionType, title, description, details, level, user); + + if (actionType === Constants.ActionType.EXTERNAL_ASSET_ADDED || + actionType === Constants.ActionType.EXTERNAL_ASSET_UPDATED || + actionType === Constants.ActionType.EXTERNAL_ASSET_DELETED) { + workerWindow.webContents.send(Messages.SCAN_PROJECT_WORKER_REQUEST, response.project, app.getPath('userData')); + } } } } catch (e) { diff --git a/app/preload.js b/app/preload.js index eb22154b..ab7c4afd 100644 --- a/app/preload.js +++ b/app/preload.js @@ -95,7 +95,42 @@ contextBridge.exposeInMainWorld('workerElectronBridge', { projectConfig.assetGroups, ); - response.project.externalAssets = projectConfig.externalAssets; + if (projectConfig.externalAssets && projectConfig.externalAssets.children) { + const newExternalAssets = AssetUtil.createEmptyExternalAssets(); + for (let i = 0; i < projectConfig.externalAssets.children.length; i++) { + const configAsset = projectConfig.externalAssets.children[i]; + if (configAsset.type === 'directory' || configAsset.type === 'folder' || configAsset.type === 'asset-group' || configAsset.type === 'URL') { + // Note: Only scan if it's actually a directory + } + if (configAsset.type === 'directory' || configAsset.type === 'folder') { + try { + const scanned = service.scan(configAsset.uri); + scanned.name = configAsset.name; // Keep its custom name + scanned.type = configAsset.type; // Preserve type + + const setIsExternalRecursively = (a) => { + if (a) { + a.isExternal = true; + if (a.children) a.children.forEach(setIsExternalRecursively); + } + }; + setIsExternalRecursively(scanned); + + newExternalAssets.children.push(scanned); + } catch (e) { + console.log('Error scanning external asset:', e); + newExternalAssets.children.push(Object.assign({}, configAsset, { error: 'Unable to scan directory' })); + } + } else { + configAsset.isExternal = true; // Ensure basic properties are enforced + newExternalAssets.children.push(cloneDeep(configAsset)); + } + } + projectService.addNotesAndAttributesToAssets(newExternalAssets, projectConfig.externalAssets); + response.project.externalAssets = newExternalAssets; + } else { + response.project.externalAssets = projectConfig.externalAssets; + } const saveResponse = ProjectUtil.saveProject(response.project, projectService); response.project = saveResponse.project; // Pick up any enrichment from saveProject diff --git a/app/utils/asset.js b/app/utils/asset.js index dad475d5..bcce879b 100644 --- a/app/utils/asset.js +++ b/app/utils/asset.js @@ -207,8 +207,8 @@ export default class AssetUtil { const notes = asset && asset.notes ? asset.notes.map((n) => { - return { ...n, uri: asset.uri }; - }) + return { ...n, uri: asset.uri }; + }) : []; if (!asset || !asset.children) { return notes.flat(); @@ -471,11 +471,11 @@ export default class AssetUtil { * @returns true if the object is confirmed as an external asset, and false otherwise */ static isExternalAsset(asset) { - if (!asset || asset === undefined || !asset.type || asset.type === undefined) { + if (!asset || asset === undefined || (!asset.type && !asset.isExternal)) { return false; } - return asset.type === Constants.AssetType.URL; + return asset.type === Constants.AssetType.URL || asset.isExternal === true; } /** diff --git a/app/utils/project.js b/app/utils/project.js index b3a1339e..c56908e4 100644 --- a/app/utils/project.js +++ b/app/utils/project.js @@ -540,12 +540,14 @@ export default class ProjectUtil { // If we have a URI for the asset, we need to see if it already exists. Note that our // earlier check confirms the URI is set, so we proceed without any other checks. let addAssetToArray = false; + asset.isExternal = true; // explicitly flag as external const existingAsset = project.externalAssets.children.find((p) => p.uri === asset.uri); if (existingAsset) { // Copy over only what attributes need to be saved in the directory. existingAsset.uri = asset.uri; existingAsset.name = asset.name; existingAsset.type = asset.type; + existingAsset.isExternal = true; } else { addAssetToArray = true; } diff --git a/test/utils/project.spec.js b/test/utils/project.spec.js index db4cba71..e6fc068d 100644 --- a/test/utils/project.spec.js +++ b/test/utils/project.spec.js @@ -1465,6 +1465,7 @@ describe('utils', () => { expect(project.externalAssets.children[0]).toEqual({ uri: '1-2-3', type: 'Test', + isExternal: true }); }); it('should update an existing external asset', () => { @@ -1530,7 +1531,7 @@ describe('utils', () => { expect(project.externalAssets).toBeNull(); }); it('should remove an existing external asset', () => { - const project = { externalAssets: { children: [{ uri: '1-2-3', name: 'Test' }]} }; + const project = { externalAssets: { children: [{ uri: '1-2-3', name: 'Test' }] } }; const asset = { uri: '1-2-3', name: 'Test', @@ -1539,7 +1540,7 @@ describe('utils', () => { expect(project.externalAssets.children.length).toEqual(0); }); it('should not remove anything if there is no matching external asset URI', () => { - const project = { externalAssets: { children: [{ uri: '1-2-3', name: 'Test' }]} }; + const project = { externalAssets: { children: [{ uri: '1-2-3', name: 'Test' }] } }; const asset = { uri: '1-2-4', name: 'Test',