From a8f08456b671e223b2313cc9bde38f867d08c56f Mon Sep 17 00:00:00 2001 From: Devin Holderness Date: Thu, 21 Aug 2025 12:31:30 -0600 Subject: [PATCH] add option to save node coords separately on journey export --- src/cli/journey/journey-export.ts | 86 +++++++++++++++++------ src/ops/JourneyOps.ts | 113 ++++++++++++++++++++++++++++++ 2 files changed, 178 insertions(+), 21 deletions(-) diff --git a/src/cli/journey/journey-export.ts b/src/cli/journey/journey-export.ts index 04ec0095c..af3ce73d5 100644 --- a/src/cli/journey/journey-export.ts +++ b/src/cli/journey/journey-export.ts @@ -2,6 +2,7 @@ import { Option } from 'commander'; import { getTokens } from '../../ops/AuthenticateOps'; import { + exportJourneyCoords, exportJourneysToFile, exportJourneysToFiles, exportJourneyToFile, @@ -62,6 +63,12 @@ export default function setup() { 'Do not include the x and y coordinate positions of the journey/tree nodes.' ) ) + .addOption( + new Option( + '--sep-coords', + 'Export x and y coordinate positions of the journey/tree nodes to separate files.' + ).default(false) + ) // .addOption( // new Option( // '-O, --organize ', @@ -82,44 +89,80 @@ export default function setup() { options, command ); + if (!(await getTokens())) { + printMessage('Authentication failed.', 'error'); + process.exitCode = 1; + return; + } + let outcome = false; // export - if (options.journeyId && (await getTokens())) { + if (options.journeyId) { verboseMessage('Exporting journey...'); - const outcome = await exportJourneyToFile( + outcome = await exportJourneyToFile( options.journeyId, options.file, options.metadata, { useStringArrays: options.useStringArrays, deps: options.deps, - coords: options.coords, + coords: !options.sepCoords, } ); - if (!outcome) process.exitCode = 1; + if (outcome && options.sepCoords) { + verboseMessage('Exporting journey coordinates separately...'); + await exportJourneyCoords([options.journeyId], { + deps: options.journeyId, + useStringArrays: options.useStringArrays, + coords: true, + sepCoords: true, + }); + } } // --all -a - else if (options.all && (await getTokens())) { + else if (options.all) { verboseMessage('Exporting all journeys to a single file...'); - const outcome = await exportJourneysToFile( - options.file, - options.metadata, - { - useStringArrays: options.useStringArrays, - deps: options.deps, - coords: options.coords, - } - ); - if (!outcome) process.exitCode = 1; + outcome = await exportJourneysToFile(options.file, options.metadata, { + useStringArrays: options.useStringArrays, + deps: options.deps, + coords: !options.sepCoords, + }); + if (outcome && options.sepCoords) { + verboseMessage( + 'Exporting coordinates of all journeys to a separate file...' + ); + await exportJourneyCoords( + 'all', + { + deps: options.deps, + useStringArrays: options.useStringArrays, + coords: true, + }, + 'all' + ); + } } // --all-separate -A - else if (options.allSeparate && (await getTokens())) { + else if (options.allSeparate) { verboseMessage('Exporting all journeys to separate files...'); - const outcome = await exportJourneysToFiles(options.metadata, { + outcome = await exportJourneysToFiles(options.metadata, { useStringArrays: options.useStringArrays, deps: options.deps, - coords: options.coords, + coords: !options.sepCoords, }); - if (!outcome) process.exitCode = 1; + if (outcome && options.sepCoords) { + verboseMessage( + 'Exporting coordinates of all journeys to separate files...' + ); + await exportJourneyCoords( + 'all', + { + deps: options.deps, + useStringArrays: options.useStringArrays, + coords: true, + }, + 'allSeparate' + ); + } } // unrecognized combination of options or no options else { @@ -129,10 +172,11 @@ export default function setup() { ); program.help(); process.exitCode = 1; + return; } + + if (!outcome) process.exitCode = 1; } - // end command logic inside action handler ); - return program; } diff --git a/src/ops/JourneyOps.ts b/src/ops/JourneyOps.ts index d416a2c04..fb3691f3a 100644 --- a/src/ops/JourneyOps.ts +++ b/src/ops/JourneyOps.ts @@ -291,6 +291,119 @@ export async function exportJourneysToFiles( return false; } +export async function exportJourneyCoords( + journeyIds: string[] | 'all', + options: TreeExportOptions = { + deps: false, + useStringArrays: false, + coords: true, + }, + mode: 'single' | 'all' | 'allSeparate' = 'single' +): Promise { + try { + let idsToExport: string[] = []; + if (journeyIds === 'all') { + const journeysExport = await exportJourneys(options, errorHandler); + idsToExport = Object.keys(journeysExport.trees); + } else { + idsToExport = journeyIds; + } + if (mode === 'all') { + const allCoords: Record< + string, + { + nodeCoordinates: Record; + staticNodeCoordinates: Record; + } + > = {}; + for (const treeId of idsToExport) { + const treeExport = await exportJourney(treeId, options); + const nodeCoordinates: Record = {}; + const staticNodeCoordinates: Record = + {}; + const justNodes: typeof treeExport.tree.nodes = {}; + for (const [nodeId, nodeData] of Object.entries( + treeExport.tree.nodes + )) { + const { x, y, ...etc } = nodeData as any; + if (typeof x === 'number' && typeof y === 'number') + nodeCoordinates[nodeId] = { x, y }; + justNodes[nodeId] = etc; + } + const justStaticNodes: typeof treeExport.tree.staticNodes = {}; + for (const [nodeId, nodeData] of Object.entries( + treeExport.tree.staticNodes ?? {} + )) { + const { x, y, ...etc } = nodeData as any; + if (typeof x === 'number' && typeof y === 'number') + staticNodeCoordinates[nodeId] = { x, y }; + justStaticNodes[nodeId] = etc; + } + allCoords[treeId] = { nodeCoordinates, staticNodeCoordinates }; + } + const coordsFile = getFilePath( + getTypedFilename(`allJourneys`, 'coords'), + true + ); + saveJsonToFile({ coordinates: allCoords }, coordsFile); + return true; + } + for (const treeId of idsToExport) { + try { + const treeExport = await exportJourney(treeId, options); + const nodeCoordinates: Record = {}; + const staticNodeCoordinates: Record = + {}; + const justNodes: typeof treeExport.tree.nodes = {}; + for (const [nodeId, nodeData] of Object.entries( + treeExport.tree.nodes + )) { + const { x, y, ...etc } = nodeData as any; + if (typeof x === 'number' && typeof y === 'number') + nodeCoordinates[nodeId] = { x, y }; + justNodes[nodeId] = etc; + } + const justStaticNodes: typeof treeExport.tree.staticNodes = {}; + for (const [nodeId, nodeData] of Object.entries( + treeExport.tree.staticNodes ?? {} + )) { + const { x, y, ...etc } = nodeData as any; + if (typeof x === 'number' && typeof y === 'number') + staticNodeCoordinates[nodeId] = { x, y }; + justStaticNodes[nodeId] = etc; + } + const justTree = { + ...treeExport, + tree: { + ...treeExport.tree, + nodes: justNodes, + staticNodes: justStaticNodes, + }, + }; + if (mode === 'single') { + const journeyFile = getFilePath( + getTypedFilename(treeId, 'journey'), + true + ); + saveJsonToFile({ trees: { [treeId]: justTree } }, journeyFile); + } + const coordsFile = getFilePath( + getTypedFilename(treeId, 'coords'), + true + ); + saveJsonToFile({ nodeCoordinates, staticNodeCoordinates }, coordsFile); + printMessage(`${treeId} node coordinates saved.`, 'info'); + } catch (error) { + throw new FrodoError(`Error saving ${treeId}`); + } + } + return true; + } catch (error) { + printError(error); + } + return false; +} + /** * Import a journey from file * @param {string} journeyId journey id/name