Skip to content

Commit 8d9f30d

Browse files
committed
fix: correct path parsing in entity refresh, topic selection, and fallback fetch
- refreshSelectedEntity: use selectedEntity.type/id instead of path parsing - handleTopicSelection: find parent entity from tree, fetch via getEntityDataItem - fetchEntityFromApi: use depth heuristic instead of treating path[0] as entityType - downloadBulkData: add encodeURIComponent for path segments - Replace all dynamic imports with static imports
1 parent 61490e9 commit 8d9f30d

1 file changed

Lines changed: 89 additions & 44 deletions

File tree

src/lib/store.ts

Lines changed: 89 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -22,23 +22,28 @@ import type { SovdResourceEntityType } from './types';
2222
import {
2323
transformFaultsResponse,
2424
transformOperationsResponse,
25+
transformDataResponse,
2526
transformConfigurationsResponse,
2627
transformFault,
2728
unwrapItems,
2829
} from './transforms';
2930
import {
31+
getEntityDetail,
3032
getEntityConfigurations,
3133
getEntityOperations,
3234
getEntityData,
35+
getEntityDataItem,
3336
getEntityFaults,
3437
getEntityFaultDetail,
3538
getEntityExecution,
3639
postEntityExecution,
3740
deleteEntityExecution,
3841
deleteEntityFault,
3942
putEntityConfiguration,
43+
putEntityDataItem,
4044
deleteEntityConfiguration,
4145
deleteEntityConfigurations,
46+
getEntityBulkData,
4247
} from './api-dispatch';
4348

4449
const STORAGE_KEY = 'ros2_medkit_web_ui_server_url';
@@ -303,26 +308,53 @@ async function handleTopicSelection(ctx: SelectionContext, client: MedkitClient)
303308
const isTopicNodeData = 'isPublisher' in data && 'isSubscriber' in data && !('type' in data);
304309

305310
if (isTopicNodeData) {
306-
// TopicNodeData - need to fetch full details
311+
// TopicNodeData - need to fetch full topic details from the parent entity
307312
const { isPublisher, isSubscriber } = data as TopicNodeData;
308-
const apiPath = path.replace(/^\/server/, '');
309-
const pathSegments = apiPath.split('/').filter(Boolean);
310-
const entityType = (pathSegments[0] || 'apps') as SovdResourceEntityType;
311-
const entityId = pathSegments[1] || '';
312-
const { getEntityDetail } = await import('./api-dispatch');
313-
const { data: detailData } = await getEntityDetail(client, entityType, entityId);
314-
const details = (detailData || { id: entityId, name: entityId, type: entityType, href: apiPath }) as SovdEntityDetails;
315-
316-
// Update tree with full data merged with direction info
317-
const updatedTree = updateNodeInTree(rootEntities, path, (n) => ({
318-
...n,
319-
data: { ...((details as unknown as Record<string, unknown>)?.topicData as Record<string, unknown>), isPublisher, isSubscriber },
320-
}));
313+
const topicName = node.id;
314+
315+
// Find parent entity by walking up the tree path
316+
const parentPath = path.split('/').slice(0, -1).join('/');
317+
const parentNode = findNode(rootEntities, parentPath);
318+
const parentType = parentNode?.type || 'component';
319+
const entityType = `${parentType}s` as SovdResourceEntityType;
320+
const entityId = parentNode?.id || '';
321+
322+
// Fetch the specific data item for this topic
323+
const { data: topicDetail } = await getEntityDataItem(client, entityType, entityId, topicName);
324+
const topicData = topicDetail as unknown as ComponentTopic | null;
325+
326+
if (topicData) {
327+
// Update tree with full data merged with direction info
328+
const updatedTree = updateNodeInTree(rootEntities, path, (n) => ({
329+
...n,
330+
data: { ...topicData, isPublisher, isSubscriber },
331+
}));
332+
333+
return {
334+
selectedPath: path,
335+
selectedEntity: {
336+
id: node.id,
337+
name: node.name,
338+
href: node.href,
339+
topicData: { ...topicData, isPublisher, isSubscriber },
340+
rosType: topicData.type,
341+
type: 'topic',
342+
},
343+
rootEntities: updatedTree,
344+
isLoadingDetails: false,
345+
};
346+
}
321347

348+
// Fallback if topic fetch fails
322349
return {
323350
selectedPath: path,
324-
selectedEntity: details,
325-
rootEntities: updatedTree,
351+
selectedEntity: {
352+
id: node.id,
353+
name: node.name,
354+
type: 'topic',
355+
href: node.href,
356+
error: 'Failed to load topic details',
357+
},
326358
isLoadingDetails: false,
327359
};
328360
}
@@ -520,6 +552,17 @@ function handleOperationSelection(ctx: SelectionContext): SelectionResult | null
520552
};
521553
}
522554

555+
/**
556+
* Infer entity type from tree path depth.
557+
* Tree paths: /server/<areaId> (depth 1), /server/<areaId>/<componentId> (depth 2),
558+
* /server/<areaId>/<componentId>/<appId> (depth 3)
559+
*/
560+
function inferEntityTypeFromDepth(depth: number): SovdResourceEntityType {
561+
if (depth <= 1) return 'areas';
562+
if (depth === 2) return 'components';
563+
return 'apps';
564+
}
565+
523566
/** Fallback: fetch entity details from API when not in tree */
524567
async function fetchEntityFromApi(
525568
path: string,
@@ -530,24 +573,23 @@ async function fetchEntityFromApi(
530573

531574
try {
532575
const apiPath = path.replace(/^\/server/, '');
533-
// Parse entity type and id from path: /areas/foo -> areas, foo
534576
const pathSegments = apiPath.split('/').filter(Boolean);
535-
const entityType = (pathSegments[0] || 'areas') as SovdResourceEntityType;
536-
const entityId = pathSegments[1] || '';
537-
const { data } = await (await import('./api-dispatch')).getEntityDetail(client, entityType, entityId);
538-
const details = (data || { id: entityId, name: entityId, type: entityType, href: apiPath }) as SovdEntityDetails;
577+
const entityId = pathSegments[pathSegments.length - 1] || '';
578+
const entityType = inferEntityTypeFromDepth(pathSegments.length);
579+
580+
const { data } = await getEntityDetail(client, entityType, entityId);
581+
const details = (data || { id: entityId, name: entityId, type: entityType.slice(0, -1), href: apiPath }) as SovdEntityDetails;
539582
set({ selectedEntity: details, isLoadingDetails: false });
540583
} catch (error) {
541584
const message = error instanceof Error ? error.message : 'Unknown error';
542585
toast.error(`Failed to load entity details for ${path}: ${message}`);
543586

544-
// Infer entity type from path structure
545587
const segments = path.split('/').filter(Boolean);
546588
const id = segments[segments.length - 1] || path;
547-
const inferredType = segments.length === 1 ? 'area' : segments.length === 2 ? 'component' : 'unknown';
589+
const inferredType = inferEntityTypeFromDepth(segments.length);
548590

549591
set({
550-
selectedEntity: { id, name: id, type: inferredType, href: path, error: 'Failed to load details' },
592+
selectedEntity: { id, name: id, type: inferredType.slice(0, -1), href: path, error: 'Failed to load details' },
551593
isLoadingDetails: false,
552594
});
553595
}
@@ -1037,20 +1079,26 @@ export const useAppStore = create<AppState>()(
10371079

10381080
// Refresh the currently selected entity (re-fetch from server)
10391081
refreshSelectedEntity: async () => {
1040-
const { selectedPath, client } = get();
1041-
if (!selectedPath || !client) {
1082+
const { selectedPath, selectedEntity, client } = get();
1083+
if (!selectedPath || !client || !selectedEntity) {
10421084
return;
10431085
}
10441086

10451087
set({ isRefreshing: true });
10461088

10471089
try {
1048-
// Convert tree path to API path (remove /server prefix)
1049-
const apiPath = selectedPath.replace(/^\/server/, '');
1050-
const pathSegments = apiPath.split('/').filter(Boolean);
1051-
const entityType = (pathSegments[0] || 'areas') as SovdResourceEntityType;
1052-
const entityId = pathSegments[1] || '';
1053-
const { data } = await (await import('./api-dispatch')).getEntityDetail(client, entityType, entityId);
1090+
const entityType = `${selectedEntity.type}s` as SovdResourceEntityType;
1091+
const entityId = selectedEntity.id;
1092+
1093+
// Only refresh actual entities (area, component, app, function)
1094+
const validTypes: SovdResourceEntityType[] = ['areas', 'components', 'apps', 'functions'];
1095+
if (!validTypes.includes(entityType)) {
1096+
// For non-entity nodes (topic, fault, parameter), just clear refreshing
1097+
set({ isRefreshing: false });
1098+
return;
1099+
}
1100+
1101+
const { data } = await getEntityDetail(client, entityType, entityId);
10541102
if (data) {
10551103
set({ selectedEntity: data as unknown as SovdEntityDetails, isRefreshing: false });
10561104
} else {
@@ -1589,7 +1637,7 @@ export const useAppStore = create<AppState>()(
15891637
if (!client) return [];
15901638
const { data, error: fetchError } = await getEntityData(client, entityType, entityId);
15911639
if (fetchError) return [];
1592-
return (await import('./transforms')).transformDataResponse(data);
1640+
return transformDataResponse(data);
15931641
},
15941642

15951643
fetchEntityOperations: async (entityType: SovdResourceEntityType, entityId: string) => {
@@ -1628,7 +1676,6 @@ export const useAppStore = create<AppState>()(
16281676
) => {
16291677
const { client } = get();
16301678
if (!client) return;
1631-
const { putEntityDataItem } = await import('./api-dispatch');
16321679
await putEntityDataItem(client, entityType, entityId, dataId, request);
16331680
},
16341681

@@ -1652,21 +1699,19 @@ export const useAppStore = create<AppState>()(
16521699
category: string,
16531700
fileId: string
16541701
) => {
1655-
const { client } = get();
1656-
if (!client) return null;
1657-
const { getEntityBulkData } = await import('./api-dispatch');
1658-
// For binary download, we need the URL to fetch directly with progress
1659-
// Build URL from client base and use fetch directly
1702+
const { client, serverUrl } = get();
1703+
if (!client || !serverUrl) return null;
1704+
1705+
// Fetch file list to get filename
16601706
const { data } = await getEntityBulkData(client, entityType, entityId, category);
16611707
if (!data) return null;
1662-
// Find the file descriptor to get download info
16631708
const items = (data as unknown as { items?: Array<{ id: string; name?: string }> })?.items || [];
16641709
const fileDesc = items.find((item) => item.id === fileId);
16651710
const filename = fileDesc?.name || fileId;
1666-
// Construct the download URL manually since openapi-fetch doesn't support blob responses well
1667-
const { serverUrl: storedUrl } = get();
1668-
const baseUrl = storedUrl ? storedUrl.replace(/\/$/, '') : '';
1669-
const downloadUrl = `${baseUrl}/${entityType}/${entityId}/bulk-data/${category}/${fileId}`;
1711+
1712+
// Download binary via fetch (openapi-fetch doesn't support blob responses)
1713+
const baseUrl = serverUrl.replace(/\/+$/, '');
1714+
const downloadUrl = `${baseUrl}/${entityType}/${encodeURIComponent(entityId)}/bulk-data/${encodeURIComponent(category)}/${encodeURIComponent(fileId)}`;
16701715
const response = await fetch(downloadUrl);
16711716
if (!response.ok) return null;
16721717
const blob = await response.blob();

0 commit comments

Comments
 (0)