From c6efa6c5c86411f78c61e844b941ac24c03c28a5 Mon Sep 17 00:00:00 2001 From: Jack Date: Mon, 4 Dec 2023 19:09:40 +0800 Subject: [PATCH] Make details pane title editable (#44) * Make details pane title editable * Add E2E test * Version bump * Self-review * Self-review --- .../integration/node-inspection-panel.spec.ts | 28 ++++++++++++++ .../PropertiesPanelContent/DetailsPane.tsx | 23 +++++++++++- .../VisualizationView/VisualizationView.tsx | 37 ++++++++++++++++++- .../DefaultDetailsPane.tsx | 26 +++++++++++-- .../Graph/GraphEventHandlerModel.ts | 2 + src/neo4j-arc/graph-visualization/index.ts | 3 +- src/neo4j-arc/package.json | 2 +- 7 files changed, 113 insertions(+), 8 deletions(-) diff --git a/e2e_tests/integration/node-inspection-panel.spec.ts b/e2e_tests/integration/node-inspection-panel.spec.ts index 90b8af4dff7..b8e4186f3b7 100644 --- a/e2e_tests/integration/node-inspection-panel.spec.ts +++ b/e2e_tests/integration/node-inspection-panel.spec.ts @@ -26,6 +26,10 @@ describe('Node Inspection Panel rendering', () => { cy.ensureConnection() }) + afterEach(() => { + cy.executeCommand('MATCH (n) DETACH DELETE n') + }) + it('should display node/rel caption as panel title', () => { cy.executeCommand(':clear') cy.executeCommand(`CREATE (s:SourceNode {name: 'My Node'}) RETURN s`, { @@ -38,4 +42,28 @@ describe('Node Inspection Panel rendering', () => { .get('[data-testid="viz-details-pane-title"]') .contains('My Node') }) + + it('details pane title should be editable', () => { + cy.executeCommand(':clear') + cy.executeCommand(`CREATE (s:SourceNode {name: 'My Node'}) RETURN s`, { + parseSpecialCharSequences: false + }) + + cy.get(`[aria-label^="graph-node"]`) + .trigger('mouseover', { force: true }) + .trigger('mouseenter', { force: true }) + .get('[data-testid="viz-details-pane-title"]') + .find('[contenteditable]') + .clear() + .type('New Title{enter}', { force: true }) + + cy.wait(1500) + + cy.get(`[aria-label^="graph-node"]`) + .first() + .trigger('mouseover', { force: true }) + .trigger('mouseenter', { force: true }) + .get('[data-testid="viz-details-pane-title"]') + .contains('New Title') + }) }) diff --git a/src/browser/modules/Stream/CypherFrame/VisualizationView/PropertiesPanelContent/DetailsPane.tsx b/src/browser/modules/Stream/CypherFrame/VisualizationView/PropertiesPanelContent/DetailsPane.tsx index 97473caaf82..c21a7df8583 100644 --- a/src/browser/modules/Stream/CypherFrame/VisualizationView/PropertiesPanelContent/DetailsPane.tsx +++ b/src/browser/modules/Stream/CypherFrame/VisualizationView/PropertiesPanelContent/DetailsPane.tsx @@ -26,12 +26,14 @@ import { StyleableRelType } from './StyleableRelType' import { PaneBody, PaneHeader, PaneTitle, PaneWrapper } from './styled' import { DetailsPaneProps } from 'neo4j-arc' +import { DETAILS_PANE_TITLE_UPDATE } from 'neo4j-arc/graph-visualization' + export const DETAILS_PANE_STEP_SIZE = 1000 export function DetailsPane({ vizItem, graphStyle, nodeInspectorWidth, - onGraphInteraction + onGraphInteraction = () => undefined }: DetailsPaneProps): JSX.Element { const [maxPropertiesCount, setMaxPropertiesCount] = useState( DETAILS_PANE_STEP_SIZE @@ -62,6 +64,7 @@ export function DetailsPane({ const item = vizItem.item const captionPropertyKey = graphStyle .pickupCaptionPropertyKey(item) + // strip off sorounding "{}" because pickupCaptionPropertyKey(item) returns something like "{title}" .replace(/[{}]/g, '') for (let i = 0; i < item.propertyList.length; i++) { if (item.propertyList[i].key == captionPropertyKey) { @@ -73,7 +76,23 @@ export function DetailsPane({ - {`${paneTitle}`} +
{ + if (event.keyCode === 13) { + event.preventDefault() + onGraphInteraction(DETAILS_PANE_TITLE_UPDATE, { + isNode: vizItem.type === 'node', + nodeOrRelId: vizItem.item.id, + titlePropertyKey: captionPropertyKey, + newTitle: event.currentTarget.textContent + }) + } + }} + > + {`${paneTitle}`} +
`${prop.key}: ${prop.value}`) diff --git a/src/browser/modules/Stream/CypherFrame/VisualizationView/VisualizationView.tsx b/src/browser/modules/Stream/CypherFrame/VisualizationView/VisualizationView.tsx index 1dab9aea83b..3b6608e4229 100644 --- a/src/browser/modules/Stream/CypherFrame/VisualizationView/VisualizationView.tsx +++ b/src/browser/modules/Stream/CypherFrame/VisualizationView/VisualizationView.tsx @@ -32,7 +32,8 @@ import { NODE_ON_CANVAS_CREATE, NODE_PROP_UPDATE, NODE_LABEL_UPDATE, - REL_TYPE_UPDATE + REL_TYPE_UPDATE, + DETAILS_PANE_TITLE_UPDATE } from 'neo4j-arc/graph-visualization' import { StyledVisContainer } from './VisualizationView.styled' @@ -437,6 +438,40 @@ LIMIT ${maxNewNeighbours}` const action = executeCommand(cmd, { source: commandSources.rerunFrame }) this.props.bus.send(action.type, action) } + + if (event == DETAILS_PANE_TITLE_UPDATE) { + if (properties == null) { + throw new Error('DETAILS_PANE_TITLE_UPDATE: properties map is required') + } + + const nodeOrRelId = properties['nodeOrRelId'] + const titlePropertyKey = properties['titlePropertyKey'] + const newTitle = properties['newTitle'] + + const isNode = properties['isNode'] + const query = + isNode == 'true' + ? `MATCH (n) WHERE ID(n) = ${nodeOrRelId} SET n.${titlePropertyKey} = '${newTitle}'` + : `MATCH ()-[r]-() WHERE ID(r) = ${nodeOrRelId} SET r.${titlePropertyKey} = '${newTitle}'` + + this.props.bus.self( + CYPHER_REQUEST, + { + query, + params: { nodeOrRelId, titlePropertyKey, newTitle }, + queryType: NEO4J_BROWSER_USER_ACTION_QUERY + }, + (response: any) => { + if (!response.success) { + throw new Error(response.error) + } + } + ) + + const cmd = 'MATCH (n) RETURN n;' + const action = executeCommand(cmd, { source: commandSources.rerunFrame }) + this.props.bus.send(action.type, action) + } } render(): React.ReactNode { diff --git a/src/neo4j-arc/graph-visualization/GraphVisualizer/DefaultPanelContent/DefaultDetailsPane.tsx b/src/neo4j-arc/graph-visualization/GraphVisualizer/DefaultPanelContent/DefaultDetailsPane.tsx index cc86d931790..2db8a53eb16 100644 --- a/src/neo4j-arc/graph-visualization/GraphVisualizer/DefaultPanelContent/DefaultDetailsPane.tsx +++ b/src/neo4j-arc/graph-visualization/GraphVisualizer/DefaultPanelContent/DefaultDetailsPane.tsx @@ -26,7 +26,10 @@ import { PaneBody, PaneHeader, PaneTitle, PaneWrapper } from './styled' import { NodeLabel } from './NodeLabel' import { RelType } from './RelType' import { GraphStyleModel } from '../../models/GraphStyle' -import { GraphInteractionCallBack } from '../Graph/GraphEventHandlerModel' +import { + DETAILS_PANE_TITLE_UPDATE, + GraphInteractionCallBack +} from '../Graph/GraphEventHandlerModel' export const DETAILS_PANE_STEP_SIZE = 1000 export type DetailsPaneProps = { @@ -39,7 +42,7 @@ export function DefaultDetailsPane({ vizItem, graphStyle, nodeInspectorWidth, - onGraphInteraction + onGraphInteraction = () => undefined }: DetailsPaneProps): JSX.Element { const [maxPropertiesCount, setMaxPropertiesCount] = useState( DETAILS_PANE_STEP_SIZE @@ -70,6 +73,7 @@ export function DefaultDetailsPane({ const item = vizItem.item const captionPropertyKey = graphStyle .pickupCaptionPropertyKey(item) + // strip off sorounding "{}" because pickupCaptionPropertyKey(item) returns something like "{title}" .replace(/[{}]/g, '') for (let i = 0; i < item.propertyList.length; i++) { if (item.propertyList[i].key == captionPropertyKey) { @@ -81,7 +85,23 @@ export function DefaultDetailsPane({ - {`${paneTitle}`} +
{ + if (event.keyCode === 13) { + event.preventDefault() + onGraphInteraction(DETAILS_PANE_TITLE_UPDATE, { + isNode: vizItem.type === 'node', + nodeOrRelId: vizItem.item.id, + titlePropertyKey: captionPropertyKey, + newTitle: event.currentTarget.textContent + }) + } + }} + > + {`${paneTitle}`} +
`${prop.key}: ${prop.value}`) diff --git a/src/neo4j-arc/graph-visualization/GraphVisualizer/Graph/GraphEventHandlerModel.ts b/src/neo4j-arc/graph-visualization/GraphVisualizer/Graph/GraphEventHandlerModel.ts index b26aef46262..6fefcb06eed 100644 --- a/src/neo4j-arc/graph-visualization/GraphVisualizer/Graph/GraphEventHandlerModel.ts +++ b/src/neo4j-arc/graph-visualization/GraphVisualizer/Graph/GraphEventHandlerModel.ts @@ -35,6 +35,7 @@ export const NODE_PROP_UPDATE = 'NODE_PROP_UPDATE' export const NODE_LABEL_UPDATE = 'NODE_LABEL_UPDATE' export const REL_ON_CANVAS_CREATE = 'REL_ON_CANVAS_CREATE' export const REL_TYPE_UPDATE = 'REL_TYPE_UPDATE' +export const DETAILS_PANE_TITLE_UPDATE = 'DETAILS_PANE_TITLE_UPDATE' export type GraphInteraction = | 'NODE_EXPAND' @@ -45,6 +46,7 @@ export type GraphInteraction = | typeof NODE_LABEL_UPDATE | typeof REL_ON_CANVAS_CREATE | typeof REL_TYPE_UPDATE + | typeof DETAILS_PANE_TITLE_UPDATE export type GraphInteractionCallBack = ( event: GraphInteraction, diff --git a/src/neo4j-arc/graph-visualization/index.ts b/src/neo4j-arc/graph-visualization/index.ts index e439395d8cc..eeb831d2630 100644 --- a/src/neo4j-arc/graph-visualization/index.ts +++ b/src/neo4j-arc/graph-visualization/index.ts @@ -40,7 +40,8 @@ export { NODE_ON_CANVAS_CREATE, NODE_PROP_UPDATE, NODE_LABEL_UPDATE, - REL_TYPE_UPDATE + REL_TYPE_UPDATE, + DETAILS_PANE_TITLE_UPDATE } from './GraphVisualizer/Graph/GraphEventHandlerModel' export type { GraphInteractionCallBack } from './GraphVisualizer/Graph/GraphEventHandlerModel' diff --git a/src/neo4j-arc/package.json b/src/neo4j-arc/package.json index bb48e93ff0c..584ab4c6983 100644 --- a/src/neo4j-arc/package.json +++ b/src/neo4j-arc/package.json @@ -1,6 +1,6 @@ { "name": "neo4j-devtools-arc", - "version": "0.0.73", + "version": "0.0.74", "main": "dist/neo4j-arc.js", "author": "Neo4j Inc.", "license": "GPL-3.0",