From c00c0c230ed66efcc44e5e527f11071638a7e9e7 Mon Sep 17 00:00:00 2001 From: Henry Ventura Date: Tue, 11 Mar 2025 10:40:08 -0700 Subject: [PATCH] feat(ui): display array properties with brackets in sidebar --- frontend/src/spanner-store.js | 28 ++++++----- frontend/src/visualization/spanner-sidebar.js | 10 +++- frontend/tests/unit/spanner-store.test.ts | 49 ++++++++++++++++++- 3 files changed, 71 insertions(+), 16 deletions(-) diff --git a/frontend/src/spanner-store.js b/frontend/src/spanner-store.js index b348e50..c1be5b2 100644 --- a/frontend/src/spanner-store.js +++ b/frontend/src/spanner-store.js @@ -638,31 +638,35 @@ class GraphStore { } /** - * Gets the type of a specific property for a node. - * @param {GraphNode} node - The node to get the property type from + * Gets the type of a specific property for a node or edge. + * @param {GraphNode|GraphEdge} graphObject - The object to get the property type from * @param {string} propertyName - The name of the property to get the type for * @returns {PropertyDeclarationType|null} The type of the property, or null if not found */ - getPropertyType(node, propertyName) { - if (!this.config.schema || !this.config.schema.rawSchema || !node) { + getPropertyType(graphObject, propertyName) { + if (!this.config.schema || !this.config.schema.rawSchema) { return null; } + if (!(graphObject instanceof Node) && !(graphObject instanceof Edge)) { + return; + } + const schema = this.config.schema.rawSchema; - // Find matching node tables for this node's labels - const matchingNodeTables = schema.nodeTables.filter(nodeTable => - node.labels.some(label => nodeTable.labelNames.includes(label)) + // Find matching node/edge tables for the labels + const matchingTables = schema[graphObject instanceof Node ? 'nodeTables' : 'edgeTables'].filter(table => + graphObject.labels.some(label => table.labelNames.includes(label)) ); - if (matchingNodeTables.length === 0) { - console.error(`No matching node table found for labels: ${node.labels.join(', ')}`); + if (matchingTables.length === 0) { + console.error(`No matching table found for labels: ${graphObject.labels.join(', ')}`); return null; } // Look through all matching node tables for the property - for (const nodeTable of matchingNodeTables) { - const propertyDef = nodeTable.propertyDefinitions.find( + for (const table of matchingTables) { + const propertyDef = table.propertyDefinitions.find( prop => prop.propertyDeclarationName === propertyName ); @@ -678,7 +682,7 @@ class GraphStore { } } - console.error(`Property ${propertyName} not found in any matching node tables for labels: ${node.labels.join(', ')}`); + console.error(`Property ${propertyName} not found in any matching tables for labels: ${graphObject.labels.join(', ')}`); return null; } } diff --git a/frontend/src/visualization/spanner-sidebar.js b/frontend/src/visualization/spanner-sidebar.js index d1e1ece..bdf0780 100644 --- a/frontend/src/visualization/spanner-sidebar.js +++ b/frontend/src/visualization/spanner-sidebar.js @@ -831,14 +831,20 @@ class SidebarConstructor { } const createPropertyRow = (key, value) => { - const property = document.createElement('div'); + /** @type PropertyDeclarationType */ + const arrayPropertyType = 'ARRAY'; + if (this.store.getPropertyType(selectedObject, key) === arrayPropertyType) { + value = `[${value}]`; + } + + const property = document.createElement('div'); property.className = 'property'; property.innerHTML = `
${key}
${value}
`; return property; - } + }; const properties = Object .entries(selectedObject.properties) diff --git a/frontend/tests/unit/spanner-store.test.ts b/frontend/tests/unit/spanner-store.test.ts index 0c7b3cf..8eac844 100644 --- a/frontend/tests/unit/spanner-store.test.ts +++ b/frontend/tests/unit/spanner-store.test.ts @@ -799,8 +799,8 @@ describe('GraphStore', () => { }); it('should handle null/undefined inputs', () => { - expect(store.getPropertyType(null, 'name')).toBeNull(); - expect(store.getPropertyType(undefined, 'name')).toBeNull(); + expect(store.getPropertyType(null, 'name')).toBeUndefined(); + expect(store.getPropertyType(undefined, 'name')).toBeUndefined(); expect(store.getPropertyType(userNode, null)).toBeNull(); expect(store.getPropertyType(userNode, undefined)).toBeNull(); }); @@ -830,6 +830,51 @@ describe('GraphStore', () => { expect(storeWithoutDeclarations.getPropertyType(userNode, 'age')).toBeNull(); }); + + it('should return correct property type for an edge', () => { + // Create a test edge with the FOLLOWS label + const testEdge = new Edge({ + identifier: 'edge1', + source_node_identifier: 'user1', + destination_node_identifier: 'user2', + labels: ['FOLLOWS'] + }); + + // Set source and destination nodes (required for proper edge handling) + testEdge.sourceNode = userNode; + testEdge.destinationNode = userNode; + + // Test getting property type for the edge + const activeType = store.getPropertyType(testEdge, 'active'); + + // The 'active' property should be of type 'BOOL' as defined in the mock data + expect(activeType).toBe('BOOL'); + + // Test with a non-existent property + const nonExistentType = store.getPropertyType(testEdge, 'nonexistent'); + expect(nonExistentType).toBeNull(); + + // Test with an edge that has a non-existent label + const invalidEdge = new Edge({ + identifier: 'invalid-edge', + source_node_identifier: 'user1', + destination_node_identifier: 'user2', + labels: ['NON_EXISTENT_LABEL'] + }); + + invalidEdge.sourceNode = userNode; + invalidEdge.destinationNode = userNode; + + const invalidEdgePropertyType = store.getPropertyType(invalidEdge, 'active'); + expect(invalidEdgePropertyType).toBeNull(); + }); + + it('should handle invalid graph objects', () => { + // Test with an object that is neither a Node nor an Edge + const invalidObject = { labels: ['User'] }; + const propertyType = store.getPropertyType(invalidObject as any, 'name'); + expect(propertyType).toBeUndefined(); + }); }); describe('getNodeCount', () => {