diff --git a/cypress/integration/start_page.spec.js b/cypress/integration/start_page.spec.js index 9521a7248..233dfeb5c 100644 --- a/cypress/integration/start_page.spec.js +++ b/cypress/integration/start_page.spec.js @@ -131,7 +131,11 @@ describe('NeoDash E2E Tests', () => { it('creates a single value report', () => { createReportOfType('Single Value', barChartCypherQuery); - cy.get('main .react-grid-item:eq(2) .MuiCardContent-root > div > div:nth-child(2) > span').contains('1,999'); + cy.get('main .react-grid-item:eq(2) .MuiCardContent-root > div > div:nth-child(2) > span') + .invoke('text') + .then((text) => { + expect(text).to.be.oneOf(['1999', '1,999']); + }); }); it('creates a gauge chart report', () => { diff --git a/src/card/settings/custom/CardSettingsContentPropertySelect.tsx b/src/card/settings/custom/CardSettingsContentPropertySelect.tsx index 1843511de..43ec6d006 100644 --- a/src/card/settings/custom/CardSettingsContentPropertySelect.tsx +++ b/src/card/settings/custom/CardSettingsContentPropertySelect.tsx @@ -30,9 +30,17 @@ const NeoCardSettingsContentPropertySelect = ({ const [labelInputText, setLabelInputText] = React.useState(settings.entityType); const [labelRecords, setLabelRecords] = React.useState([]); const [propertyInputText, setPropertyInputText] = React.useState(settings.propertyType); + const [propertyInputDisplayText, setPropertyInputDisplayText] = React.useState( + settings.propertyDisplay || settings.propertyType + ); const [propertyRecords, setPropertyRecords] = React.useState([]); let { parameterName } = settings; + // When certain settings are updated, a re-generated search query is needed. + useEffect(() => { + updateReportQuery(settings.entityType, settings.propertyType, settings.propertyDisplay || settings.propertyDisplay); + }, [settings.suggestionLimit, settings.deduplicateSuggestions, settings.searchType, settings.caseSensitive]); + const cleanParameter = (parameter: string) => parameter.replaceAll(' ', '_').replaceAll('-', '_').toLowerCase(); const formatParameterId = (id: string | undefined | null) => { const cleanedId = id || ''; @@ -40,11 +48,6 @@ const NeoCardSettingsContentPropertySelect = ({ return formattedId; }; - // When certain settings are updated, a re-generated search query is needed. - useEffect(() => { - updateReportQuery(settings.entityType, settings.propertyType); - }, [settings.suggestionLimit, settings.deduplicateSuggestions, settings.searchType, settings.caseSensitive]); - if (settings.type == undefined) { onReportSettingUpdate('type', 'Node Property'); } @@ -76,20 +79,23 @@ const NeoCardSettingsContentPropertySelect = ({ onReportSettingUpdate('propertyType', undefined); onReportSettingUpdate('id', undefined); onReportSettingUpdate('parameterName', undefined); + onReportSettingUpdate('propertyDisplay', undefined); onReportSettingUpdate('type', newValue); } function handleNodeLabelSelectionUpdate(newValue) { setPropertyInputText(''); + setPropertyInputDisplayText(''); onReportSettingUpdate('entityType', newValue); onReportSettingUpdate('propertyType', undefined); onReportSettingUpdate('parameterName', undefined); + onReportSettingUpdate('propertyDisplay', undefined); } function handleFreeTextNameSelectionUpdate(newValue) { if (newValue) { const new_parameter_name = cleanParameter(`neodash_${newValue}`); - handleReportQueryUpdate(new_parameter_name, newValue, undefined); + handleReportQueryUpdate(new_parameter_name, newValue, undefined, undefined); } else { onReportSettingUpdate('parameterName', undefined); } @@ -97,12 +103,22 @@ const NeoCardSettingsContentPropertySelect = ({ function handlePropertyNameSelectionUpdate(newValue) { onReportSettingUpdate('propertyType', newValue); + onReportSettingUpdate('propertyDisplay', newValue); if (newValue && settings.entityType) { const newParameterName = `neodash_${settings.entityType}_${newValue}`; const formattedParameterId = formatParameterId(settings.id); const cleanedParameter = cleanParameter(newParameterName + formattedParameterId); - handleReportQueryUpdate(cleanedParameter, settings.entityType, newValue); + handleReportQueryUpdate(cleanedParameter, settings.entityType, newValue, newValue); + } else { + onReportSettingUpdate('parameterName', undefined); + } + } + + function handlePropertyDisplayNameSelectionUpdate(newValue) { + onReportSettingUpdate('propertyDisplay', newValue); + if (newValue && settings.entityType) { + updateReportQuery(settings.entityType, settings.propertyType, newValue); } else { onReportSettingUpdate('parameterName', undefined); } @@ -115,16 +131,18 @@ const NeoCardSettingsContentPropertySelect = ({ const newParameterName = `neodash_${settings.entityType}_${settings.propertyType}`; const formattedParameterId = formatParameterId(`${newValue}`); const cleanedParameter = cleanParameter(newParameterName + formattedParameterId); - handleReportQueryUpdate(cleanedParameter, settings.entityType, settings.propertyType); + + handleReportQueryUpdate(cleanedParameter, settings.entityType, settings.propertyType, settings.propertyDisplay); } } - function handleReportQueryUpdate(new_parameter_name, entityType, propertyType) { + function handleReportQueryUpdate(new_parameter_name, entityType, propertyType, propertyDisplay) { onReportSettingUpdate('parameterName', new_parameter_name); - updateReportQuery(entityType, propertyType); + updateReportQuery(entityType, propertyType, propertyDisplay); } - function updateReportQuery(entityType, propertyType) { + function updateReportQuery(entityType, propertyType, propertyDisplay) { + const propertyDisplaySanitized = propertyDisplay || propertyType; const limit = settings.suggestionLimit ? settings.suggestionLimit : 5; const deduplicate = settings.deduplicateSuggestions !== undefined ? settings.deduplicateSuggestions : true; const searchType = settings.searchType ? settings.searchType : 'CONTAINS'; @@ -132,19 +150,21 @@ const NeoCardSettingsContentPropertySelect = ({ if (settings.type == 'Node Property') { const newQuery = `MATCH (n:\`${entityType}\`) \n` + - `WHERE ${caseSensitive ? '' : 'toLower'}(toString(n.\`${propertyType}\`)) ${searchType} ${ + `WHERE ${caseSensitive ? '' : 'toLower'}(toString(n.\`${propertyDisplaySanitized}\`)) ${searchType} ${ caseSensitive ? '' : 'toLower' }($input) \n` + - `RETURN ${deduplicate ? 'DISTINCT' : ''} n.\`${propertyType}\` as value ` + + `RETURN ${deduplicate ? 'DISTINCT' : ''} n.\`${propertyType}\` as value, ` + + ` n.\`${propertyDisplaySanitized}\` as display ` + `ORDER BY size(toString(value)) ASC LIMIT ${limit}`; onQueryUpdate(newQuery); } else if (settings.type == 'Relationship Property') { const newQuery = `MATCH ()-[n:\`${entityType}\`]->() \n` + - `WHERE ${caseSensitive ? '' : 'toLower'}(toString(n.\`${propertyType}\`)) ${searchType} ${ + `WHERE ${caseSensitive ? '' : 'toLower'}(toString(n.\`${propertyDisplaySanitized}\`)) ${searchType} ${ caseSensitive ? '' : 'toLower' }($input) \n` + - `RETURN ${deduplicate ? 'DISTINCT' : ''} n.\`${propertyType}\` as value ` + + `RETURN ${deduplicate ? 'DISTINCT' : ''} n.\`${propertyType}\` as value, ` + + ` n.\`${propertyDisplaySanitized}\` as display ` + `ORDER BY size(toString(value)) ASC LIMIT ${limit}`; onQueryUpdate(newQuery); } else { @@ -156,6 +176,8 @@ const NeoCardSettingsContentPropertySelect = ({ // TODO: since this component is only rendered for parameter select, this is technically not needed const parameterSelectTypes = ['Node Property', 'Relationship Property', 'Free Text']; const reportTypes = getReportTypes(extensions); + const overridePropertyDisplayName = + settings.overridePropertyDisplayName !== undefined ? settings.overridePropertyDisplayName : false; return (
@@ -173,7 +195,7 @@ const NeoCardSettingsContentPropertySelect = ({ style={{ width: '25%' }} label='Selection Type' type='text' - style={{ width: 335, marginLeft: '5px', marginTop: '0px' }} + style={{ width: 350, marginLeft: '5px', marginTop: '0px' }} > {parameterSelectTypes.map((option) => ( @@ -205,8 +227,8 @@ const NeoCardSettingsContentPropertySelect = ({ ? [settings.entityType] : labelRecords.map((r) => (r._fields ? r._fields[0] : '(no data)')) } - getOptionLabel={(option) => (option ? option : '')} - style={{ width: 335, marginLeft: '5px', marginTop: '5px' }} + getOptionLabel={(option) => option || ''} + style={{ width: 350, marginLeft: '5px', marginTop: '5px' }} inputValue={labelInputText} onInputChange={(event, value) => { setLabelInputText(value); @@ -248,10 +270,11 @@ const NeoCardSettingsContentPropertySelect = ({ : propertyRecords.map((r) => (r._fields ? r._fields[0] : '(no data)')) } getOptionLabel={(option) => (option ? option : '')} - style={{ display: 'inline-block', width: 185, marginLeft: '5px', marginTop: '5px' }} + style={{ display: 'inline-block', width: 170, marginLeft: '5px', marginTop: '5px' }} inputValue={propertyInputText} onInputChange={(event, value) => { setPropertyInputText(value); + setPropertyInputDisplayText(value); if (manualPropertyNameSpecification) { handlePropertyNameSelectionUpdate(value); } else { @@ -273,12 +296,49 @@ const NeoCardSettingsContentPropertySelect = ({ /> )} /> + {overridePropertyDisplayName ? ( + (r._fields ? r._fields[0] : '(no data)')) + } + getOptionLabel={(option) => (option ? option : '')} + style={{ display: 'inline-block', width: 170, marginLeft: '10px', marginTop: '5px' }} + inputValue={propertyInputDisplayText} + onInputChange={(event, value) => { + setPropertyInputDisplayText(value); + if (manualPropertyNameSpecification) { + handlePropertyDisplayNameSelectionUpdate(value); + } else { + queryCallback( + 'CALL db.propertyKeys() YIELD propertyKey as propertyName WITH propertyName WHERE toLower(propertyName) CONTAINS toLower($input) RETURN DISTINCT propertyName LIMIT 5', + { input: value }, + setPropertyRecords + ); + } + }} + value={settings.propertyDisplay || settings.propertyType} + onChange={(event, newValue) => handlePropertyDisplayNameSelectionUpdate(newValue)} + renderInput={(params) => ( + + )} + /> + ) : ( + <> + )} { handleIdSelectionUpdate(value); }} diff --git a/src/chart/parameter/ParameterSelectionChart.tsx b/src/chart/parameter/ParameterSelectionChart.tsx index ebc722920..3c38443e1 100644 --- a/src/chart/parameter/ParameterSelectionChart.tsx +++ b/src/chart/parameter/ParameterSelectionChart.tsx @@ -11,6 +11,7 @@ const NeoParameterSelectionChart = (props: ChartProps) => { const { records } = props; const query = records[0].input ? records[0].input : undefined; const parameter = props.settings && props.settings.parameterName ? props.settings.parameterName : undefined; + const parameterDisplay = `${parameter}_display`; const type = props.settings && props.settings.type ? props.settings.type : undefined; const suggestionsUpdateTimeout = props.settings && props.settings.suggestionsUpdateTimeout ? props.settings.suggestionsUpdateTimeout : 250; @@ -22,15 +23,22 @@ const NeoParameterSelectionChart = (props: ChartProps) => { : ''; const currentValue = props.getGlobalParameter && props.getGlobalParameter(parameter) ? props.getGlobalParameter(parameter) : ''; + + const currentDisplayValue = + props.getGlobalParameter && props.getGlobalParameter(parameterDisplay) + ? props.getGlobalParameter(parameterDisplay) + : ''; + const [extraRecords, setExtraRecords] = React.useState([]); - const [inputText, setInputText] = React.useState(currentValue); + const [inputText, setInputText] = React.useState(currentDisplayValue); const queryCallback = props.queryCallback ? props.queryCallback : () => {}; const setGlobalParameter = props.setGlobalParameter ? props.setGlobalParameter : () => {}; const [value, setValue] = React.useState(currentValue); const debouncedQueryCallback = useCallback(debounce(queryCallback, suggestionsUpdateTimeout), []); const debouncedSetGlobalParameter = useCallback(debounce(setGlobalParameter, setParameterTimeout), []); - + const compatibilityMode = !query.includes('as display'); + const indexCompatibility = compatibilityMode ? 0 : 1; const queryTimeOut = setTimeout(() => { debouncedQueryCallback && debouncedQueryCallback(query, { input: inputText, ...props.parameters }, setExtraRecords); }, 150); @@ -39,6 +47,7 @@ const NeoParameterSelectionChart = (props: ChartProps) => { return () => clearTimeout(queryTimeOut); }, [inputText, query, props.parameters]); + // In case the components gets (re)loaded with a different/non-existing selected parameter, set the text to the current global parameter value. useEffect(() => { const timeOutId = setTimeout(() => { if (value == '' && clearParameterOnFieldClear) { @@ -51,12 +60,19 @@ const NeoParameterSelectionChart = (props: ChartProps) => { }, [value]); // In case the components gets (re)loaded with a different/non-existing selected parameter, set the text to the current global parameter value. - if (query && value != currentValue && currentValue != inputText) { + if (query && value != currentValue && currentDisplayValue != inputText) { setValue(currentValue); - setInputText(value == defaultValue ? '' : currentValue); + setInputText(value == defaultValue ? '' : currentDisplayValue); setExtraRecords([]); } + const label = props.settings && props.settings.entityType ? props.settings.entityType : ''; + const property = props.settings && props.settings.propertyType ? props.settings.propertyType : ''; + const propertyDisplay = props?.settings?.propertyDisplay || property; + const settings = props.settings ? props.settings : {}; + const { helperText } = settings; + const { clearParameterOnFieldClear } = settings; + if (!query || query.trim().length == 0) { return (

@@ -65,18 +81,13 @@ const NeoParameterSelectionChart = (props: ChartProps) => { ); } - const label = props.settings && props.settings.entityType ? props.settings.entityType : ''; - const property = props.settings && props.settings.propertyType ? props.settings.propertyType : ''; - const settings = props.settings ? props.settings : {}; - const { helperText, clearParameterOnFieldClear } = settings; - return (

{type == 'Free Text' ? (
{ ) : ( (r._fields && r._fields[0] !== null ? r._fields[0] : '(no data)')).sort()} + options={extraRecords.map((r) => (r._fields ? r._fields[indexCompatibility] : '(no data)')).sort()} getOptionLabel={(option) => (option ? option.toString() : '')} style={{ maxWidth: 'calc(100% - 30px)', marginLeft: '15px', marginTop: '5px' }} inputValue={inputText !== null ? inputText.toString() : ''} - onInputChange={(event, value) => { - setInputText(`${value}`); - debouncedQueryCallback(query, { input: `${value}` }, setExtraRecords); + onInputChange={(event, val) => { + setInputText(`${val}`); + debouncedQueryCallback(query, { input: `${val}` }, setExtraRecords); }} - getOptionSelected={(option, value) => { - return (option && option.toString()) === (value && value.toString()); - }} - value={value !== null ? value.toString() : `${currentValue}`} - onChange={(event, newValue) => { + getOptionSelected={(option, val) => (option && option.toString()) === (val && val.toString())} + value={inputText !== null ? inputText.toString() : `${currentValue}`} + onChange={(event, newVal: string) => { + let newValue = extraRecords.filter((r) => r._fields[indexCompatibility].toString() == newVal)[0]._fields[0]; + if (newValue && newValue.low) { + newValue = newValue.low; + } setValue(newValue); - setInputText(`${newValue}`); + setInputText(`${newVal}`); if (newValue && newValue.low) { newValue = newValue.low; } if (newValue == null && clearParameterOnFieldClear) { props.setGlobalParameter(parameter, undefined); + props.setGlobalParameter(parameterDisplay, undefined); } else if (newValue == null) { props.setGlobalParameter(parameter, defaultValue); + props.setGlobalParameter(parameterDisplay, defaultValue); } else { props.setGlobalParameter(parameter, newValue); + props.setGlobalParameter(parameterDisplay, newVal); } }} renderInput={(params) => ( @@ -131,7 +147,7 @@ const NeoParameterSelectionChart = (props: ChartProps) => { {...params} InputLabelProps={{ shrink: true }} placeholder='Start typing...' - label={helperText ? helperText : `${label} ${property}`} + label={helperText ? helperText : `${label} ${propertyDisplay}`} variant='outlined' /> )} diff --git a/src/config/ReportConfig.tsx b/src/config/ReportConfig.tsx index af989724d..4e91f712f 100644 --- a/src/config/ReportConfig.tsx +++ b/src/config/ReportConfig.tsx @@ -1062,6 +1062,12 @@ export const REPORT_TYPES = { type: SELECTION_TYPES.COLOR, default: '#fafafa', }, + overridePropertyDisplayName: { + label: 'Property Display Name Override', + type: SELECTION_TYPES.LIST, + values: [true, false], + default: false, + }, suggestionLimit: { label: 'Value Suggestion Limit', type: SELECTION_TYPES.NUMBER,