Skip to content

Commit 6608417

Browse files
authored
Merge pull request #545 from neo4j-labs/feature/selectorConfirmation
Manual Parameter Selector Refresh
2 parents fb0397e + 090051a commit 6608417

File tree

8 files changed

+160
-58
lines changed

8 files changed

+160
-58
lines changed

docs/modules/ROOT/pages/user-guide/reports/parameter-select.adoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ global parameter completely when the field is cleared. This may break
5555
some visualizations. If disabled, sets the parameter value to “” (empty
5656
string) when the input field is cleared.
5757

58+
|Multiple Selection |on/off |off |If enabled, allows user to select multiple choices. Parameter will be then an array of selections.
59+
60+
|Manual Parameter Save |on/off |off |If enabled, adds a confirmation button in order to propagate the selection into the dashboard parameter.
61+
5862
|Enable Manual Label/Property Name Specification |on/off |off |If
5963
enabled, does not enforce you to select a node label/property using an
6064
auto-complete field, instead, you can enter any value. This is useful

src/chart/parameter/ParameterSelectionChart.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export const NeoParameterSelectionChart = (props: ChartProps) => {
2828
const setParameterDisplayValue = (value) => setGlobalParameter(parameterDisplayName, value);
2929
const allParameters = props.parameters;
3030
const multiSelector = props?.settings?.multiSelector;
31-
31+
const manualParameterSave = props?.settings?.manualParameterSave;
3232
// in NeoDash 2.2.1 or earlier, there was no means to have a different display value in the selector. This condition handles that.
3333
const compatibilityMode = !query?.toLowerCase().includes('as display') || false;
3434

@@ -50,6 +50,7 @@ export const NeoParameterSelectionChart = (props: ChartProps) => {
5050
settings={props.settings}
5151
allParameters={allParameters}
5252
compatibilityMode={compatibilityMode}
53+
manualParameterSave={manualParameterSave}
5354
/>
5455
);
5556
} else if (type == 'Node Property') {
@@ -67,6 +68,7 @@ export const NeoParameterSelectionChart = (props: ChartProps) => {
6768
allParameters={allParameters}
6869
compatibilityMode={compatibilityMode}
6970
multiSelector={multiSelector}
71+
manualParameterSave={manualParameterSave}
7072
/>
7173
);
7274
} else if (type == 'Relationship Property') {
@@ -84,6 +86,7 @@ export const NeoParameterSelectionChart = (props: ChartProps) => {
8486
allParameters={allParameters}
8587
compatibilityMode={compatibilityMode}
8688
multiSelector={multiSelector}
89+
manualParameterSave={manualParameterSave}
8790
/>
8891
);
8992
} else if (type == 'Date Picker') {
@@ -100,6 +103,7 @@ export const NeoParameterSelectionChart = (props: ChartProps) => {
100103
settings={props.settings}
101104
allParameters={allParameters}
102105
compatibilityMode={compatibilityMode}
106+
manualParameterSave={manualParameterSave}
103107
/>
104108
);
105109
} else if (type == 'Custom Query') {
@@ -117,6 +121,7 @@ export const NeoParameterSelectionChart = (props: ChartProps) => {
117121
allParameters={allParameters}
118122
compatibilityMode={compatibilityMode}
119123
multiSelector={multiSelector}
124+
manualParameterSave={manualParameterSave}
120125
/>
121126
);
122127
}

src/chart/parameter/component/FreeTextParameterSelect.tsx

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import { debounce, CircularProgress } from '@mui/material';
22
import React, { useCallback, useEffect } from 'react';
33
import { ParameterSelectProps } from './ParameterSelect';
44
import NeoField from '../../../component/field/Field';
5+
import { SelectionConfirmationButton } from './SelectionConfirmationButton';
56

67
const FreeTextParameterSelectComponent = (props: ParameterSelectProps) => {
8+
const { manualParameterSave } = props;
79
const setParameterTimeout =
810
props.settings && props.settings.setParameterTimeout ? props.settings.setParameterTimeout : 1000;
911
const defaultValue =
@@ -17,38 +19,66 @@ const FreeTextParameterSelectComponent = (props: ParameterSelectProps) => {
1719
const clearParameterOnFieldClear =
1820
props.settings && props.settings.clearParameterOnFieldClear ? props.settings.clearParameterOnFieldClear : false;
1921
const [running, setRunning] = React.useState(false);
22+
const [paramValueLocal, setParamValueLocal] = React.useState(null);
23+
2024
const setParameterValue = (value) => {
2125
setRunning(false);
2226
props.setParameterValue(value);
2327
};
2428
const debouncedSetParameterValue = useCallback(debounce(setParameterValue, setParameterTimeout), []);
2529

30+
const manualHandleParametersUpdate = () => {
31+
handleParametersUpdate(paramValueLocal, false);
32+
};
33+
34+
const handleParametersUpdate = (value, manual = false) => {
35+
setParamValueLocal(value);
36+
37+
if (manual) {
38+
// setRunning(false);
39+
return;
40+
}
41+
42+
if (value == null && clearParameterOnFieldClear) {
43+
debouncedSetParameterValue(defaultValue);
44+
} else {
45+
debouncedSetParameterValue(value);
46+
}
47+
};
48+
2649
// If the user hasn't typed, and the parameter value mismatches the input value --> it was changed externally --> refresh the input value.
2750
if (running == false && inputText !== props.parameterValue) {
2851
setInputText(props.parameterValue);
2952
}
3053

3154
return (
32-
<div style={{ width: '100%', marginTop: '5px' }}>
55+
<div className={'n-flex n-flex-row n-flex-wrap n-items-center'} style={{ width: '100%', marginTop: '5px' }}>
3356
<NeoField
3457
key={'freetext'}
3558
label={helperText ? helperText : `${label} ${property}`}
3659
defaultValue={defaultValue}
3760
value={inputText}
3861
variant='outlined'
3962
placeholder={'Enter text here...'}
40-
style={{ marginBottom: '10px', marginRight: '10px', marginLeft: '15px', width: 'calc(100% - 80px)' }}
63+
style={{
64+
marginBottom: '20px',
65+
marginRight: '10px',
66+
marginLeft: '15px',
67+
minWidth: `calc(100% - ${manualParameterSave ? '80' : '30'}px)`,
68+
maxWidth: 'calc(100% - 30px)',
69+
}}
4170
onChange={(newValue) => {
4271
setRunning(true);
4372
setInputText(newValue);
4473

45-
if (newValue == null && clearParameterOnFieldClear) {
46-
debouncedSetParameterValue(defaultValue);
47-
} else {
48-
debouncedSetParameterValue(newValue);
49-
}
74+
handleParametersUpdate(newValue, manualParameterSave);
5075
}}
5176
/>
77+
{manualParameterSave ? (
78+
<SelectionConfirmationButton onClick={() => manualHandleParametersUpdate()} key={`selectionConfirmation`} />
79+
) : (
80+
<></>
81+
)}
5282
{running ? <CircularProgress size={26} style={{ marginTop: '20px', marginLeft: '5px' }} /> : <></>}
5383
</div>
5484
);

src/chart/parameter/component/NodePropertyParameterSelect.tsx

Lines changed: 79 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { debounce, TextField } from '@mui/material';
33
import Autocomplete from '@mui/material/Autocomplete';
44
import { ParameterSelectProps } from './ParameterSelect';
55
import { RenderSubValue } from '../../../report/ReportRecordProcessing';
6+
import { SelectionConfirmationButton } from './SelectionConfirmationButton';
67

78
const NodePropertyParameterSelectComponent = (props: ParameterSelectProps) => {
89
const suggestionsUpdateTimeout =
@@ -20,14 +21,17 @@ const NodePropertyParameterSelectComponent = (props: ParameterSelectProps) => {
2021
}
2122
return multi ? [] : value;
2223
};
23-
const { multiSelector } = props;
24+
const { multiSelector, manualParameterSave } = props;
2425
const allParameters = props.allParameters ? props.allParameters : {};
2526
const [extraRecords, setExtraRecords] = React.useState([]);
2627
const [inputDisplayText, setInputDisplayText] = React.useState(
2728
props.parameterDisplayValue && multiSelector ? '' : props.parameterDisplayValue
2829
);
2930
const [inputValue, setInputValue] = React.useState(getInitialValue(props.parameterDisplayValue, multiSelector));
3031

32+
const [paramValueLocal, setParamValueLocal] = React.useState(props.parameterValue);
33+
const [paramValueDisplayLocal, setParamValueDisplayLocal] = React.useState(props.parameterDisplayValue);
34+
3135
const debouncedQueryCallback = useCallback(debounce(props.queryCallback, suggestionsUpdateTimeout), []);
3236
const label = props.settings && props.settings.entityType ? props.settings.entityType : '';
3337
const propertyType = props.settings && props.settings.propertyType ? props.settings.propertyType : '';
@@ -42,94 +46,119 @@ const NodePropertyParameterSelectComponent = (props: ParameterSelectProps) => {
4246

4347
const realValueRowIndex = props.compatibilityMode ? 0 : 1 - displayValueRowIndex;
4448

49+
const manualHandleParametersUpdate = () => {
50+
handleParametersUpdate(paramValueLocal, paramValueDisplayLocal, false);
51+
};
52+
const handleParametersUpdate = (value, displayValue, manual = false) => {
53+
setParamValueLocal(value);
54+
setParamValueDisplayLocal(displayValue);
55+
56+
if (manual) {
57+
return;
58+
}
59+
60+
props.setParameterValue(value);
61+
props.setParameterDisplayValue(displayValue);
62+
};
4563
const handleCrossClick = (isMulti, value) => {
4664
if (isMulti) {
4765
if (value.length == 0 && clearParameterOnFieldClear) {
4866
setInputValue([]);
49-
props.setParameterValue(undefined);
50-
props.setParameterDisplayValue(undefined);
51-
return;
67+
handleParametersUpdate(undefined, undefined, manualParameterSave);
68+
return true;
5269
}
5370
if (value.length == 0) {
5471
setInputValue([]);
55-
props.setParameterValue([]);
56-
props.setParameterDisplayValue([]);
57-
72+
handleParametersUpdate([], [], manualParameterSave);
73+
return true;
5874
}
5975
} else {
6076
if (value && clearParameterOnFieldClear) {
6177
setInputValue(null);
62-
props.setParameterValue(undefined);
63-
props.setParameterDisplayValue(undefined);
64-
return;
78+
handleParametersUpdate(undefined, undefined, manualParameterSave);
79+
return true;
6580
}
6681
if (value == null) {
6782
setInputValue(null);
68-
props.setParameterValue(defaultValue);
69-
props.setParameterDisplayValue(defaultValue);
70-
83+
handleParametersUpdate(defaultValue, defaultValue, manualParameterSave);
84+
return true;
7185
}
86+
return false;
7287
}
7388
};
7489
const propagateSelection = (event, newDisplay) => {
7590
const isMulti = Array.isArray(newDisplay);
76-
handleCrossClick(isMulti, newDisplay);
91+
if (handleCrossClick(isMulti, newDisplay)) {
92+
return;
93+
}
7794
let newValue;
95+
let valReference = manualParameterSave ? paramValueLocal : props.parameterValue;
96+
let valDisplayReference = manualParameterSave ? paramValueDisplayLocal : props.parameterDisplayValue;
7897
// Multiple and new entry
7998
if (isMulti && inputValue.length < newDisplay.length) {
80-
newValue = Array.isArray(props.parameterValue) ? [...props.parameterValue] : [props.parameterValue];
99+
newValue = Array.isArray(valReference) ? [...valReference] : [valReference];
81100
const newDisplayValue = [...newDisplay].slice(-1)[0];
82101

83102
let val = extraRecords.filter((r) => r._fields[displayValueRowIndex].toString() == newDisplayValue)[0]._fields[
84103
realValueRowIndex
85104
];
86105

87-
newValue.push(val?.low ?? val);
106+
newValue.push(RenderSubValue(val));
88107
} else if (!isMulti) {
89-
newValue = extraRecords.filter((r) => r._fields[displayValueRowIndex].toString() == newDisplay)[0]._fields[
90-
realValueRowIndex
91-
];
108+
newValue = extraRecords.filter((r) => (r?._fields?.[displayValueRowIndex]?.toString() || null) == newDisplay)[0]
109+
._fields[realValueRowIndex];
92110

93-
newValue = newValue?.low || newValue;
111+
newValue = RenderSubValue(newValue);
94112
} else {
95-
let ele = props.parameterDisplayValue.filter((x) => !newDisplay.includes(x))[0];
96-
newValue = [...props.parameterValue];
97-
newValue.splice(props.parameterDisplayValue.indexOf(ele), 1);
113+
let ele = valDisplayReference.filter((x) => !newDisplay.includes(x))[0];
114+
newValue = [...valReference];
115+
newValue.splice(valDisplayReference.indexOf(ele), 1);
98116
}
99117

100118
setInputDisplayText(isMulti ? '' : newDisplay);
101119
setInputValue(newDisplay);
102120

103-
props.setParameterValue(newValue);
104-
props.setParameterDisplayValue(newDisplay);
121+
handleParametersUpdate(newValue, newDisplay, manualParameterSave);
105122
};
106123
return (
107-
<Autocomplete
108-
id='autocomplete'
109-
multiple={multiSelector}
110-
options={extraRecords.map((r) => r?._fields?.[displayValueRowIndex] || '(no data)').sort()}
111-
style={{ maxWidth: 'calc(100% - 30px)', marginLeft: '15px', marginTop: '5px' }}
112-
inputValue={inputDisplayText}
113-
onInputChange={(event, value) => {
114-
setInputDisplayText(value);
115-
debouncedQueryCallback(props.query, { input: `${value}`, ...allParameters }, setExtraRecords);
116-
}}
117-
isOptionEqualToValue={(option, value) => {
118-
return (option && option.toString()) === (value && value.toString());
119-
}}
120-
value={inputValue}
121-
onChange={propagateSelection}
122-
renderInput={(params) => (
123-
<TextField
124-
{...params}
125-
InputLabelProps={{ shrink: true }}
126-
placeholder='Start typing...'
127-
label={helperText ? helperText : `${label} ${propertyType}`}
128-
variant='outlined'
129-
/>
124+
<div className={'n-flex n-flex-row n-flex-wrap n-items-center'}>
125+
<Autocomplete
126+
id='autocomplete'
127+
multiple={multiSelector}
128+
options={extraRecords.map((r) => r?._fields?.[displayValueRowIndex] || '(no data)').sort()}
129+
style={{
130+
maxWidth: 'calc(100% - 30px)',
131+
minWidth: `calc(100% - ${manualParameterSave ? '80' : '30'}px)`,
132+
marginLeft: '15px',
133+
marginTop: '5px',
134+
}}
135+
inputValue={inputDisplayText || ''}
136+
onInputChange={(event, value) => {
137+
setInputDisplayText(value);
138+
debouncedQueryCallback(props.query, { input: `${value}`, ...allParameters }, setExtraRecords);
139+
}}
140+
isOptionEqualToValue={(option, value) => {
141+
return (option && option.toString()) === (value && value.toString());
142+
}}
143+
value={inputValue}
144+
onChange={propagateSelection}
145+
renderInput={(params) => (
146+
<TextField
147+
{...params}
148+
InputLabelProps={{ shrink: true }}
149+
placeholder='Start typing...'
150+
label={helperText ? helperText : `${label} ${propertyType}`}
151+
variant='outlined'
152+
/>
153+
)}
154+
getOptionLabel={(option) => RenderSubValue(option)}
155+
/>
156+
{manualParameterSave ? (
157+
<SelectionConfirmationButton onClick={() => manualHandleParametersUpdate()} key={`selectionConfirmation`} />
158+
) : (
159+
<></>
130160
)}
131-
getOptionLabel={(option) => RenderSubValue(option)}
132-
/>
161+
</div>
133162
);
134163
};
135164

src/chart/parameter/component/ParameterSelect.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,8 @@ export interface ParameterSelectProps {
5050
* Add the possibility for multiple selections
5151
*/
5252
multiSelector?: boolean;
53+
/**
54+
* Add the possibility for manual selection confirmation
55+
*/
56+
manualParameterSave?: boolean;
5357
}

src/chart/parameter/component/RelationshipPropertyParameterSelect.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const RelationshipPropertyParameterSelectComponent = (props: ParameterSelectProp
2121
allParameters={props.allParameters}
2222
compatibilityMode={props.compatibilityMode}
2323
multiSelector={props.multiSelector}
24+
manualParameterSave={props.manualParameterSave}
2425
/>
2526
);
2627
};
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import React from 'react';
2+
import { Tooltip } from '@mui/material';
3+
import { PlayIconOutline } from '@neo4j-ndl/react/icons';
4+
import { IconButton } from '@neo4j-ndl/react';
5+
/**
6+
* Returns a button to confirm a selection entry from the parameter selector.
7+
*/
8+
export const SelectionConfirmationButton = ({ onClick, key }) => {
9+
return (
10+
<Tooltip title={'Confirm'} disableInteractive key={key}>
11+
<IconButton
12+
key={`btn${key}`}
13+
className='logo-btn n-p-1'
14+
aria-label={'btb-confirmation'}
15+
size='large'
16+
onClick={onClick}
17+
clean
18+
>
19+
<PlayIconOutline className='header-icon' type='outline' />
20+
</IconButton>
21+
</Tooltip>
22+
);
23+
};

src/config/ReportConfig.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1296,6 +1296,12 @@ export const REPORT_TYPES = {
12961296
values: [true, false],
12971297
default: false,
12981298
},
1299+
manualParameterSave: {
1300+
label: 'Manual Parameter Save',
1301+
type: SELECTION_TYPES.LIST,
1302+
values: [true, false],
1303+
default: false,
1304+
},
12991305
overridePropertyDisplayName: {
13001306
label: 'Property Display Name Override',
13011307
type: SELECTION_TYPES.LIST,

0 commit comments

Comments
 (0)