@bygd/nc-report-ui 0.1.43 → 0.1.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/esm/index.js +111 -14
- package/dist/default/cjs/index.cjs +111 -14
- package/dist/default/esm/index.js +111 -14
- package/package.json +1 -1
package/dist/app/esm/index.js
CHANGED
|
@@ -77759,6 +77759,16 @@ const SortableComputedChip = ({
|
|
|
77759
77759
|
handleCancel();
|
|
77760
77760
|
}
|
|
77761
77761
|
};
|
|
77762
|
+
|
|
77763
|
+
// The value field is multiline, so Enter inserts a newline instead of
|
|
77764
|
+
// saving; only Escape (cancel) and Cmd/Ctrl+Enter (save) are handled here.
|
|
77765
|
+
const handleValueKeyDown = e => {
|
|
77766
|
+
if (e.key === "Escape") {
|
|
77767
|
+
handleCancel();
|
|
77768
|
+
} else if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
|
77769
|
+
handleSave();
|
|
77770
|
+
}
|
|
77771
|
+
};
|
|
77762
77772
|
return /*#__PURE__*/gn.createElement("div", _extends({
|
|
77763
77773
|
ref: setNodeRef,
|
|
77764
77774
|
style: style
|
|
@@ -77862,10 +77872,13 @@ const SortableComputedChip = ({
|
|
|
77862
77872
|
}), /*#__PURE__*/gn.createElement(TextField, {
|
|
77863
77873
|
value: editValue,
|
|
77864
77874
|
onChange: e => setEditValue(e.target.value),
|
|
77865
|
-
onKeyDown:
|
|
77875
|
+
onKeyDown: handleValueKeyDown,
|
|
77866
77876
|
size: "small",
|
|
77867
77877
|
label: "Value",
|
|
77868
|
-
helperText: " ",
|
|
77878
|
+
helperText: "Cmd/Ctrl+Enter to save",
|
|
77879
|
+
multiline: true,
|
|
77880
|
+
minRows: 2,
|
|
77881
|
+
maxRows: 10,
|
|
77869
77882
|
sx: {
|
|
77870
77883
|
flex: 1,
|
|
77871
77884
|
minWidth: "200px",
|
|
@@ -78275,6 +78288,9 @@ const Dimensions = ({
|
|
|
78275
78288
|
value: computedValue,
|
|
78276
78289
|
onChange: e => setComputedValue(e.target.value),
|
|
78277
78290
|
size: "small",
|
|
78291
|
+
multiline: true,
|
|
78292
|
+
minRows: 2,
|
|
78293
|
+
maxRows: 10,
|
|
78278
78294
|
sx: {
|
|
78279
78295
|
width: "400px",
|
|
78280
78296
|
"& .MuiInputBase-input": {
|
|
@@ -90517,6 +90533,50 @@ class AdapterDayjs {
|
|
|
90517
90533
|
};
|
|
90518
90534
|
}
|
|
90519
90535
|
|
|
90536
|
+
// Small text input + button for adding a filter value that isn't present in
|
|
90537
|
+
// the "Select Filter Values" dropdown (e.g. because no existing row has that
|
|
90538
|
+
// value). Kept as an uncontrolled local field so it clears itself after add.
|
|
90539
|
+
const CustomFilterValueInput = ({
|
|
90540
|
+
onAdd
|
|
90541
|
+
}) => {
|
|
90542
|
+
const [value, setValue] = d('');
|
|
90543
|
+
const handleAdd = () => {
|
|
90544
|
+
const trimmed = value.trim();
|
|
90545
|
+
if (!trimmed) return;
|
|
90546
|
+
onAdd(trimmed);
|
|
90547
|
+
setValue('');
|
|
90548
|
+
};
|
|
90549
|
+
return /*#__PURE__*/gn.createElement(Box, {
|
|
90550
|
+
sx: {
|
|
90551
|
+
display: 'flex',
|
|
90552
|
+
gap: 1,
|
|
90553
|
+
mt: 1
|
|
90554
|
+
}
|
|
90555
|
+
}, /*#__PURE__*/gn.createElement(TextField, {
|
|
90556
|
+
size: "small",
|
|
90557
|
+
fullWidth: true,
|
|
90558
|
+
placeholder: "Add a value not in the list above",
|
|
90559
|
+
value: value,
|
|
90560
|
+
onChange: e => setValue(e.target.value),
|
|
90561
|
+
onKeyDown: e => {
|
|
90562
|
+
if (e.key === 'Enter') {
|
|
90563
|
+
e.preventDefault();
|
|
90564
|
+
handleAdd();
|
|
90565
|
+
}
|
|
90566
|
+
}
|
|
90567
|
+
}), /*#__PURE__*/gn.createElement(Button, {
|
|
90568
|
+
variant: "outlined",
|
|
90569
|
+
size: "small",
|
|
90570
|
+
onClick: handleAdd,
|
|
90571
|
+
disabled: !value.trim(),
|
|
90572
|
+
sx: {
|
|
90573
|
+
textTransform: 'none',
|
|
90574
|
+
whiteSpace: 'nowrap',
|
|
90575
|
+
borderColor: "rgb(70, 134, 128)",
|
|
90576
|
+
color: "rgb(70, 134, 128)"
|
|
90577
|
+
}
|
|
90578
|
+
}, "Add"));
|
|
90579
|
+
};
|
|
90520
90580
|
const Filters = ({
|
|
90521
90581
|
providersData,
|
|
90522
90582
|
rootProvider,
|
|
@@ -90656,8 +90716,9 @@ const Filters = ({
|
|
|
90656
90716
|
}
|
|
90657
90717
|
} else {
|
|
90658
90718
|
// For regular filters, set selected values and fetch available values
|
|
90659
|
-
|
|
90660
|
-
|
|
90719
|
+
const currentValues = filter.values || [];
|
|
90720
|
+
setSelectedFilterValues(currentValues);
|
|
90721
|
+
await fetchFilterValues(fullPath, '', currentValues);
|
|
90661
90722
|
}
|
|
90662
90723
|
};
|
|
90663
90724
|
const handleSaveEditedFilter = fullPath => {
|
|
@@ -90752,7 +90813,11 @@ const Filters = ({
|
|
|
90752
90813
|
};
|
|
90753
90814
|
|
|
90754
90815
|
// Fetch distinct values for the selected dimension
|
|
90755
|
-
|
|
90816
|
+
// preserveValues lets a caller pass the values that must survive the merge
|
|
90817
|
+
// explicitly, instead of relying on the selectedFilterValues closure - a
|
|
90818
|
+
// setSelectedFilterValues() call right before fetchFilterValues() hasn't
|
|
90819
|
+
// committed yet by the time this function reads that state.
|
|
90820
|
+
const fetchFilterValues = async (fullPath, searchText = '', preserveValues = null) => {
|
|
90756
90821
|
setLoadingFilterValues(true);
|
|
90757
90822
|
try {
|
|
90758
90823
|
// Get parameters from context if available, otherwise use default
|
|
@@ -90825,7 +90890,8 @@ const Filters = ({
|
|
|
90825
90890
|
// Merge with currently selected values to ensure they're always available
|
|
90826
90891
|
// This is important when editing filters with search text - we don't want to lose
|
|
90827
90892
|
// previously selected values that don't match the current search
|
|
90828
|
-
const
|
|
90893
|
+
const valuesToPreserve = preserveValues !== null ? preserveValues : selectedFilterValues;
|
|
90894
|
+
const selectedValueItems = valuesToPreserve.map(key => ({
|
|
90829
90895
|
key: String(key),
|
|
90830
90896
|
value: String(key)
|
|
90831
90897
|
}));
|
|
@@ -91054,6 +91120,21 @@ const Filters = ({
|
|
|
91054
91120
|
}
|
|
91055
91121
|
};
|
|
91056
91122
|
|
|
91123
|
+
// Add a value typed in the "custom value" box directly into the selected
|
|
91124
|
+
// filter values, and inject it into availableFilterValues too, since
|
|
91125
|
+
// CheckboxMultiAutocomplete only renders a chip for keys it can resolve
|
|
91126
|
+
// against its items list.
|
|
91127
|
+
const handleAddCustomFilterValue = rawValue => {
|
|
91128
|
+
const value = rawValue.trim();
|
|
91129
|
+
if (!value) return;
|
|
91130
|
+
if (selectedFilterValues.includes(value)) return;
|
|
91131
|
+
setAvailableFilterValues(prev => prev.some(item => item.key === value) ? prev : [{
|
|
91132
|
+
key: value,
|
|
91133
|
+
value
|
|
91134
|
+
}, ...prev]);
|
|
91135
|
+
setSelectedFilterValues(prev => [...prev, value]);
|
|
91136
|
+
};
|
|
91137
|
+
|
|
91057
91138
|
// Handler for search text input in filter values dropdown (debounced - triggers API call)
|
|
91058
91139
|
const handleFilterSearchChange = searchText => {
|
|
91059
91140
|
// Determine the fullPath based on current mode
|
|
@@ -91273,6 +91354,8 @@ const Filters = ({
|
|
|
91273
91354
|
loading: loadingFilterValues,
|
|
91274
91355
|
helperText: selectedFilterValues.length > 0 ? `${selectedFilterValues.length} value(s) selected` : 'Select at least one value',
|
|
91275
91356
|
debounceMs: 1000
|
|
91357
|
+
}), /*#__PURE__*/gn.createElement(CustomFilterValueInput, {
|
|
91358
|
+
onAdd: handleAddCustomFilterValue
|
|
91276
91359
|
})));
|
|
91277
91360
|
})(), /*#__PURE__*/gn.createElement(Box, {
|
|
91278
91361
|
sx: {
|
|
@@ -91424,6 +91507,8 @@ const Filters = ({
|
|
|
91424
91507
|
loading: loadingFilterValues,
|
|
91425
91508
|
helperText: selectedFilterValues.length > 0 ? `${selectedFilterValues.length} value(s) selected` : 'Select at least one value',
|
|
91426
91509
|
debounceMs: 1000
|
|
91510
|
+
}), /*#__PURE__*/gn.createElement(CustomFilterValueInput, {
|
|
91511
|
+
onAdd: handleAddCustomFilterValue
|
|
91427
91512
|
})));
|
|
91428
91513
|
})(), /*#__PURE__*/gn.createElement(Box, {
|
|
91429
91514
|
sx: {
|
|
@@ -91713,6 +91798,8 @@ const Filters = ({
|
|
|
91713
91798
|
loading: loadingFilterValues,
|
|
91714
91799
|
helperText: selectedFilterValues.length > 0 ? `${selectedFilterValues.length} value(s) selected` : 'Select at least one value',
|
|
91715
91800
|
debounceMs: 1000
|
|
91801
|
+
}), /*#__PURE__*/gn.createElement(CustomFilterValueInput, {
|
|
91802
|
+
onAdd: handleAddCustomFilterValue
|
|
91716
91803
|
})), /*#__PURE__*/gn.createElement(Box, {
|
|
91717
91804
|
sx: {
|
|
91718
91805
|
display: 'flex',
|
|
@@ -93104,14 +93191,24 @@ const ReportBuilder = ({
|
|
|
93104
93191
|
queryObj.ordered_metrics = report.metrics.map(metric => metric.id);
|
|
93105
93192
|
}
|
|
93106
93193
|
|
|
93107
|
-
// Persist the
|
|
93108
|
-
//
|
|
93109
|
-
//
|
|
93110
|
-
//
|
|
93111
|
-
|
|
93112
|
-
|
|
93113
|
-
|
|
93114
|
-
|
|
93194
|
+
// Persist the full resolved column order - the explicit Column Order tab
|
|
93195
|
+
// arrangement when defined, otherwise dimensions/computed (in their own
|
|
93196
|
+
// order) followed by metrics (in their own order), same as what the
|
|
93197
|
+
// results grid renders. Always emitted (not just when a custom order
|
|
93198
|
+
// exists) so the API/results grid get an explicit order even for
|
|
93199
|
+
// untouched reports, rather than having to fall back to guessing it.
|
|
93200
|
+
const orderedDimensionItems = report.dimensionOrder.map(entry => entry.type === "dimension" ? {
|
|
93201
|
+
kind: "dimension",
|
|
93202
|
+
data: dimensionById[entry.key]
|
|
93203
|
+
} : {
|
|
93204
|
+
kind: "computed",
|
|
93205
|
+
data: computedByName[entry.key]
|
|
93206
|
+
}).filter(item => item.data);
|
|
93207
|
+
const resolvedColumnItems = resolveColumnOrder(report.columnOrder, orderedDimensionItems, report.metrics);
|
|
93208
|
+
if (resolvedColumnItems.length > 0) {
|
|
93209
|
+
queryObj.ordered_columns = resolvedColumnItems.map(item => ({
|
|
93210
|
+
type: item.kind,
|
|
93211
|
+
ref: item.key
|
|
93115
93212
|
}));
|
|
93116
93213
|
}
|
|
93117
93214
|
|
|
@@ -3172,6 +3172,16 @@ const SortableComputedChip = ({
|
|
|
3172
3172
|
handleCancel();
|
|
3173
3173
|
}
|
|
3174
3174
|
};
|
|
3175
|
+
|
|
3176
|
+
// The value field is multiline, so Enter inserts a newline instead of
|
|
3177
|
+
// saving; only Escape (cancel) and Cmd/Ctrl+Enter (save) are handled here.
|
|
3178
|
+
const handleValueKeyDown = e => {
|
|
3179
|
+
if (e.key === "Escape") {
|
|
3180
|
+
handleCancel();
|
|
3181
|
+
} else if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
|
3182
|
+
handleSave();
|
|
3183
|
+
}
|
|
3184
|
+
};
|
|
3175
3185
|
return /*#__PURE__*/React__namespace.default.createElement("div", _extends({
|
|
3176
3186
|
ref: setNodeRef,
|
|
3177
3187
|
style: style
|
|
@@ -3275,10 +3285,13 @@ const SortableComputedChip = ({
|
|
|
3275
3285
|
}), /*#__PURE__*/React__namespace.default.createElement(material.TextField, {
|
|
3276
3286
|
value: editValue,
|
|
3277
3287
|
onChange: e => setEditValue(e.target.value),
|
|
3278
|
-
onKeyDown:
|
|
3288
|
+
onKeyDown: handleValueKeyDown,
|
|
3279
3289
|
size: "small",
|
|
3280
3290
|
label: "Value",
|
|
3281
|
-
helperText: " ",
|
|
3291
|
+
helperText: "Cmd/Ctrl+Enter to save",
|
|
3292
|
+
multiline: true,
|
|
3293
|
+
minRows: 2,
|
|
3294
|
+
maxRows: 10,
|
|
3282
3295
|
sx: {
|
|
3283
3296
|
flex: 1,
|
|
3284
3297
|
minWidth: "200px",
|
|
@@ -3688,6 +3701,9 @@ const Dimensions = ({
|
|
|
3688
3701
|
value: computedValue,
|
|
3689
3702
|
onChange: e => setComputedValue(e.target.value),
|
|
3690
3703
|
size: "small",
|
|
3704
|
+
multiline: true,
|
|
3705
|
+
minRows: 2,
|
|
3706
|
+
maxRows: 10,
|
|
3691
3707
|
sx: {
|
|
3692
3708
|
width: "400px",
|
|
3693
3709
|
"& .MuiInputBase-input": {
|
|
@@ -4582,6 +4598,50 @@ const Metrics = ({
|
|
|
4582
4598
|
})))));
|
|
4583
4599
|
};
|
|
4584
4600
|
|
|
4601
|
+
// Small text input + button for adding a filter value that isn't present in
|
|
4602
|
+
// the "Select Filter Values" dropdown (e.g. because no existing row has that
|
|
4603
|
+
// value). Kept as an uncontrolled local field so it clears itself after add.
|
|
4604
|
+
const CustomFilterValueInput = ({
|
|
4605
|
+
onAdd
|
|
4606
|
+
}) => {
|
|
4607
|
+
const [value, setValue] = React.useState('');
|
|
4608
|
+
const handleAdd = () => {
|
|
4609
|
+
const trimmed = value.trim();
|
|
4610
|
+
if (!trimmed) return;
|
|
4611
|
+
onAdd(trimmed);
|
|
4612
|
+
setValue('');
|
|
4613
|
+
};
|
|
4614
|
+
return /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
4615
|
+
sx: {
|
|
4616
|
+
display: 'flex',
|
|
4617
|
+
gap: 1,
|
|
4618
|
+
mt: 1
|
|
4619
|
+
}
|
|
4620
|
+
}, /*#__PURE__*/React__namespace.default.createElement(material.TextField, {
|
|
4621
|
+
size: "small",
|
|
4622
|
+
fullWidth: true,
|
|
4623
|
+
placeholder: "Add a value not in the list above",
|
|
4624
|
+
value: value,
|
|
4625
|
+
onChange: e => setValue(e.target.value),
|
|
4626
|
+
onKeyDown: e => {
|
|
4627
|
+
if (e.key === 'Enter') {
|
|
4628
|
+
e.preventDefault();
|
|
4629
|
+
handleAdd();
|
|
4630
|
+
}
|
|
4631
|
+
}
|
|
4632
|
+
}), /*#__PURE__*/React__namespace.default.createElement(material.Button, {
|
|
4633
|
+
variant: "outlined",
|
|
4634
|
+
size: "small",
|
|
4635
|
+
onClick: handleAdd,
|
|
4636
|
+
disabled: !value.trim(),
|
|
4637
|
+
sx: {
|
|
4638
|
+
textTransform: 'none',
|
|
4639
|
+
whiteSpace: 'nowrap',
|
|
4640
|
+
borderColor: "rgb(70, 134, 128)",
|
|
4641
|
+
color: "rgb(70, 134, 128)"
|
|
4642
|
+
}
|
|
4643
|
+
}, "Add"));
|
|
4644
|
+
};
|
|
4585
4645
|
const Filters = ({
|
|
4586
4646
|
providersData,
|
|
4587
4647
|
rootProvider,
|
|
@@ -4721,8 +4781,9 @@ const Filters = ({
|
|
|
4721
4781
|
}
|
|
4722
4782
|
} else {
|
|
4723
4783
|
// For regular filters, set selected values and fetch available values
|
|
4724
|
-
|
|
4725
|
-
|
|
4784
|
+
const currentValues = filter.values || [];
|
|
4785
|
+
setSelectedFilterValues(currentValues);
|
|
4786
|
+
await fetchFilterValues(fullPath, '', currentValues);
|
|
4726
4787
|
}
|
|
4727
4788
|
};
|
|
4728
4789
|
const handleSaveEditedFilter = fullPath => {
|
|
@@ -4817,7 +4878,11 @@ const Filters = ({
|
|
|
4817
4878
|
};
|
|
4818
4879
|
|
|
4819
4880
|
// Fetch distinct values for the selected dimension
|
|
4820
|
-
|
|
4881
|
+
// preserveValues lets a caller pass the values that must survive the merge
|
|
4882
|
+
// explicitly, instead of relying on the selectedFilterValues closure - a
|
|
4883
|
+
// setSelectedFilterValues() call right before fetchFilterValues() hasn't
|
|
4884
|
+
// committed yet by the time this function reads that state.
|
|
4885
|
+
const fetchFilterValues = async (fullPath, searchText = '', preserveValues = null) => {
|
|
4821
4886
|
setLoadingFilterValues(true);
|
|
4822
4887
|
try {
|
|
4823
4888
|
// Get parameters from context if available, otherwise use default
|
|
@@ -4890,7 +4955,8 @@ const Filters = ({
|
|
|
4890
4955
|
// Merge with currently selected values to ensure they're always available
|
|
4891
4956
|
// This is important when editing filters with search text - we don't want to lose
|
|
4892
4957
|
// previously selected values that don't match the current search
|
|
4893
|
-
const
|
|
4958
|
+
const valuesToPreserve = preserveValues !== null ? preserveValues : selectedFilterValues;
|
|
4959
|
+
const selectedValueItems = valuesToPreserve.map(key => ({
|
|
4894
4960
|
key: String(key),
|
|
4895
4961
|
value: String(key)
|
|
4896
4962
|
}));
|
|
@@ -5119,6 +5185,21 @@ const Filters = ({
|
|
|
5119
5185
|
}
|
|
5120
5186
|
};
|
|
5121
5187
|
|
|
5188
|
+
// Add a value typed in the "custom value" box directly into the selected
|
|
5189
|
+
// filter values, and inject it into availableFilterValues too, since
|
|
5190
|
+
// CheckboxMultiAutocomplete only renders a chip for keys it can resolve
|
|
5191
|
+
// against its items list.
|
|
5192
|
+
const handleAddCustomFilterValue = rawValue => {
|
|
5193
|
+
const value = rawValue.trim();
|
|
5194
|
+
if (!value) return;
|
|
5195
|
+
if (selectedFilterValues.includes(value)) return;
|
|
5196
|
+
setAvailableFilterValues(prev => prev.some(item => item.key === value) ? prev : [{
|
|
5197
|
+
key: value,
|
|
5198
|
+
value
|
|
5199
|
+
}, ...prev]);
|
|
5200
|
+
setSelectedFilterValues(prev => [...prev, value]);
|
|
5201
|
+
};
|
|
5202
|
+
|
|
5122
5203
|
// Handler for search text input in filter values dropdown (debounced - triggers API call)
|
|
5123
5204
|
const handleFilterSearchChange = searchText => {
|
|
5124
5205
|
// Determine the fullPath based on current mode
|
|
@@ -5338,6 +5419,8 @@ const Filters = ({
|
|
|
5338
5419
|
loading: loadingFilterValues,
|
|
5339
5420
|
helperText: selectedFilterValues.length > 0 ? `${selectedFilterValues.length} value(s) selected` : 'Select at least one value',
|
|
5340
5421
|
debounceMs: 1000
|
|
5422
|
+
}), /*#__PURE__*/React__namespace.default.createElement(CustomFilterValueInput, {
|
|
5423
|
+
onAdd: handleAddCustomFilterValue
|
|
5341
5424
|
})));
|
|
5342
5425
|
})(), /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
5343
5426
|
sx: {
|
|
@@ -5489,6 +5572,8 @@ const Filters = ({
|
|
|
5489
5572
|
loading: loadingFilterValues,
|
|
5490
5573
|
helperText: selectedFilterValues.length > 0 ? `${selectedFilterValues.length} value(s) selected` : 'Select at least one value',
|
|
5491
5574
|
debounceMs: 1000
|
|
5575
|
+
}), /*#__PURE__*/React__namespace.default.createElement(CustomFilterValueInput, {
|
|
5576
|
+
onAdd: handleAddCustomFilterValue
|
|
5492
5577
|
})));
|
|
5493
5578
|
})(), /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
5494
5579
|
sx: {
|
|
@@ -5778,6 +5863,8 @@ const Filters = ({
|
|
|
5778
5863
|
loading: loadingFilterValues,
|
|
5779
5864
|
helperText: selectedFilterValues.length > 0 ? `${selectedFilterValues.length} value(s) selected` : 'Select at least one value',
|
|
5780
5865
|
debounceMs: 1000
|
|
5866
|
+
}), /*#__PURE__*/React__namespace.default.createElement(CustomFilterValueInput, {
|
|
5867
|
+
onAdd: handleAddCustomFilterValue
|
|
5781
5868
|
})), /*#__PURE__*/React__namespace.default.createElement(material.Box, {
|
|
5782
5869
|
sx: {
|
|
5783
5870
|
display: 'flex',
|
|
@@ -7165,14 +7252,24 @@ const ReportBuilder = ({
|
|
|
7165
7252
|
queryObj.ordered_metrics = report.metrics.map(metric => metric.id);
|
|
7166
7253
|
}
|
|
7167
7254
|
|
|
7168
|
-
// Persist the
|
|
7169
|
-
//
|
|
7170
|
-
//
|
|
7171
|
-
//
|
|
7172
|
-
|
|
7173
|
-
|
|
7174
|
-
|
|
7175
|
-
|
|
7255
|
+
// Persist the full resolved column order - the explicit Column Order tab
|
|
7256
|
+
// arrangement when defined, otherwise dimensions/computed (in their own
|
|
7257
|
+
// order) followed by metrics (in their own order), same as what the
|
|
7258
|
+
// results grid renders. Always emitted (not just when a custom order
|
|
7259
|
+
// exists) so the API/results grid get an explicit order even for
|
|
7260
|
+
// untouched reports, rather than having to fall back to guessing it.
|
|
7261
|
+
const orderedDimensionItems = report.dimensionOrder.map(entry => entry.type === "dimension" ? {
|
|
7262
|
+
kind: "dimension",
|
|
7263
|
+
data: dimensionById[entry.key]
|
|
7264
|
+
} : {
|
|
7265
|
+
kind: "computed",
|
|
7266
|
+
data: computedByName[entry.key]
|
|
7267
|
+
}).filter(item => item.data);
|
|
7268
|
+
const resolvedColumnItems = resolveColumnOrder(report.columnOrder, orderedDimensionItems, report.metrics);
|
|
7269
|
+
if (resolvedColumnItems.length > 0) {
|
|
7270
|
+
queryObj.ordered_columns = resolvedColumnItems.map(item => ({
|
|
7271
|
+
type: item.kind,
|
|
7272
|
+
ref: item.key
|
|
7176
7273
|
}));
|
|
7177
7274
|
}
|
|
7178
7275
|
|
|
@@ -3091,6 +3091,16 @@ const SortableComputedChip = ({
|
|
|
3091
3091
|
handleCancel();
|
|
3092
3092
|
}
|
|
3093
3093
|
};
|
|
3094
|
+
|
|
3095
|
+
// The value field is multiline, so Enter inserts a newline instead of
|
|
3096
|
+
// saving; only Escape (cancel) and Cmd/Ctrl+Enter (save) are handled here.
|
|
3097
|
+
const handleValueKeyDown = e => {
|
|
3098
|
+
if (e.key === "Escape") {
|
|
3099
|
+
handleCancel();
|
|
3100
|
+
} else if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
|
3101
|
+
handleSave();
|
|
3102
|
+
}
|
|
3103
|
+
};
|
|
3094
3104
|
return /*#__PURE__*/React__default.createElement("div", _extends({
|
|
3095
3105
|
ref: setNodeRef,
|
|
3096
3106
|
style: style
|
|
@@ -3194,10 +3204,13 @@ const SortableComputedChip = ({
|
|
|
3194
3204
|
}), /*#__PURE__*/React__default.createElement(TextField, {
|
|
3195
3205
|
value: editValue,
|
|
3196
3206
|
onChange: e => setEditValue(e.target.value),
|
|
3197
|
-
onKeyDown:
|
|
3207
|
+
onKeyDown: handleValueKeyDown,
|
|
3198
3208
|
size: "small",
|
|
3199
3209
|
label: "Value",
|
|
3200
|
-
helperText: " ",
|
|
3210
|
+
helperText: "Cmd/Ctrl+Enter to save",
|
|
3211
|
+
multiline: true,
|
|
3212
|
+
minRows: 2,
|
|
3213
|
+
maxRows: 10,
|
|
3201
3214
|
sx: {
|
|
3202
3215
|
flex: 1,
|
|
3203
3216
|
minWidth: "200px",
|
|
@@ -3607,6 +3620,9 @@ const Dimensions = ({
|
|
|
3607
3620
|
value: computedValue,
|
|
3608
3621
|
onChange: e => setComputedValue(e.target.value),
|
|
3609
3622
|
size: "small",
|
|
3623
|
+
multiline: true,
|
|
3624
|
+
minRows: 2,
|
|
3625
|
+
maxRows: 10,
|
|
3610
3626
|
sx: {
|
|
3611
3627
|
width: "400px",
|
|
3612
3628
|
"& .MuiInputBase-input": {
|
|
@@ -4501,6 +4517,50 @@ const Metrics = ({
|
|
|
4501
4517
|
})))));
|
|
4502
4518
|
};
|
|
4503
4519
|
|
|
4520
|
+
// Small text input + button for adding a filter value that isn't present in
|
|
4521
|
+
// the "Select Filter Values" dropdown (e.g. because no existing row has that
|
|
4522
|
+
// value). Kept as an uncontrolled local field so it clears itself after add.
|
|
4523
|
+
const CustomFilterValueInput = ({
|
|
4524
|
+
onAdd
|
|
4525
|
+
}) => {
|
|
4526
|
+
const [value, setValue] = useState('');
|
|
4527
|
+
const handleAdd = () => {
|
|
4528
|
+
const trimmed = value.trim();
|
|
4529
|
+
if (!trimmed) return;
|
|
4530
|
+
onAdd(trimmed);
|
|
4531
|
+
setValue('');
|
|
4532
|
+
};
|
|
4533
|
+
return /*#__PURE__*/React__default.createElement(Box$1, {
|
|
4534
|
+
sx: {
|
|
4535
|
+
display: 'flex',
|
|
4536
|
+
gap: 1,
|
|
4537
|
+
mt: 1
|
|
4538
|
+
}
|
|
4539
|
+
}, /*#__PURE__*/React__default.createElement(TextField, {
|
|
4540
|
+
size: "small",
|
|
4541
|
+
fullWidth: true,
|
|
4542
|
+
placeholder: "Add a value not in the list above",
|
|
4543
|
+
value: value,
|
|
4544
|
+
onChange: e => setValue(e.target.value),
|
|
4545
|
+
onKeyDown: e => {
|
|
4546
|
+
if (e.key === 'Enter') {
|
|
4547
|
+
e.preventDefault();
|
|
4548
|
+
handleAdd();
|
|
4549
|
+
}
|
|
4550
|
+
}
|
|
4551
|
+
}), /*#__PURE__*/React__default.createElement(Button, {
|
|
4552
|
+
variant: "outlined",
|
|
4553
|
+
size: "small",
|
|
4554
|
+
onClick: handleAdd,
|
|
4555
|
+
disabled: !value.trim(),
|
|
4556
|
+
sx: {
|
|
4557
|
+
textTransform: 'none',
|
|
4558
|
+
whiteSpace: 'nowrap',
|
|
4559
|
+
borderColor: "rgb(70, 134, 128)",
|
|
4560
|
+
color: "rgb(70, 134, 128)"
|
|
4561
|
+
}
|
|
4562
|
+
}, "Add"));
|
|
4563
|
+
};
|
|
4504
4564
|
const Filters = ({
|
|
4505
4565
|
providersData,
|
|
4506
4566
|
rootProvider,
|
|
@@ -4640,8 +4700,9 @@ const Filters = ({
|
|
|
4640
4700
|
}
|
|
4641
4701
|
} else {
|
|
4642
4702
|
// For regular filters, set selected values and fetch available values
|
|
4643
|
-
|
|
4644
|
-
|
|
4703
|
+
const currentValues = filter.values || [];
|
|
4704
|
+
setSelectedFilterValues(currentValues);
|
|
4705
|
+
await fetchFilterValues(fullPath, '', currentValues);
|
|
4645
4706
|
}
|
|
4646
4707
|
};
|
|
4647
4708
|
const handleSaveEditedFilter = fullPath => {
|
|
@@ -4736,7 +4797,11 @@ const Filters = ({
|
|
|
4736
4797
|
};
|
|
4737
4798
|
|
|
4738
4799
|
// Fetch distinct values for the selected dimension
|
|
4739
|
-
|
|
4800
|
+
// preserveValues lets a caller pass the values that must survive the merge
|
|
4801
|
+
// explicitly, instead of relying on the selectedFilterValues closure - a
|
|
4802
|
+
// setSelectedFilterValues() call right before fetchFilterValues() hasn't
|
|
4803
|
+
// committed yet by the time this function reads that state.
|
|
4804
|
+
const fetchFilterValues = async (fullPath, searchText = '', preserveValues = null) => {
|
|
4740
4805
|
setLoadingFilterValues(true);
|
|
4741
4806
|
try {
|
|
4742
4807
|
// Get parameters from context if available, otherwise use default
|
|
@@ -4809,7 +4874,8 @@ const Filters = ({
|
|
|
4809
4874
|
// Merge with currently selected values to ensure they're always available
|
|
4810
4875
|
// This is important when editing filters with search text - we don't want to lose
|
|
4811
4876
|
// previously selected values that don't match the current search
|
|
4812
|
-
const
|
|
4877
|
+
const valuesToPreserve = preserveValues !== null ? preserveValues : selectedFilterValues;
|
|
4878
|
+
const selectedValueItems = valuesToPreserve.map(key => ({
|
|
4813
4879
|
key: String(key),
|
|
4814
4880
|
value: String(key)
|
|
4815
4881
|
}));
|
|
@@ -5038,6 +5104,21 @@ const Filters = ({
|
|
|
5038
5104
|
}
|
|
5039
5105
|
};
|
|
5040
5106
|
|
|
5107
|
+
// Add a value typed in the "custom value" box directly into the selected
|
|
5108
|
+
// filter values, and inject it into availableFilterValues too, since
|
|
5109
|
+
// CheckboxMultiAutocomplete only renders a chip for keys it can resolve
|
|
5110
|
+
// against its items list.
|
|
5111
|
+
const handleAddCustomFilterValue = rawValue => {
|
|
5112
|
+
const value = rawValue.trim();
|
|
5113
|
+
if (!value) return;
|
|
5114
|
+
if (selectedFilterValues.includes(value)) return;
|
|
5115
|
+
setAvailableFilterValues(prev => prev.some(item => item.key === value) ? prev : [{
|
|
5116
|
+
key: value,
|
|
5117
|
+
value
|
|
5118
|
+
}, ...prev]);
|
|
5119
|
+
setSelectedFilterValues(prev => [...prev, value]);
|
|
5120
|
+
};
|
|
5121
|
+
|
|
5041
5122
|
// Handler for search text input in filter values dropdown (debounced - triggers API call)
|
|
5042
5123
|
const handleFilterSearchChange = searchText => {
|
|
5043
5124
|
// Determine the fullPath based on current mode
|
|
@@ -5257,6 +5338,8 @@ const Filters = ({
|
|
|
5257
5338
|
loading: loadingFilterValues,
|
|
5258
5339
|
helperText: selectedFilterValues.length > 0 ? `${selectedFilterValues.length} value(s) selected` : 'Select at least one value',
|
|
5259
5340
|
debounceMs: 1000
|
|
5341
|
+
}), /*#__PURE__*/React__default.createElement(CustomFilterValueInput, {
|
|
5342
|
+
onAdd: handleAddCustomFilterValue
|
|
5260
5343
|
})));
|
|
5261
5344
|
})(), /*#__PURE__*/React__default.createElement(Box$1, {
|
|
5262
5345
|
sx: {
|
|
@@ -5408,6 +5491,8 @@ const Filters = ({
|
|
|
5408
5491
|
loading: loadingFilterValues,
|
|
5409
5492
|
helperText: selectedFilterValues.length > 0 ? `${selectedFilterValues.length} value(s) selected` : 'Select at least one value',
|
|
5410
5493
|
debounceMs: 1000
|
|
5494
|
+
}), /*#__PURE__*/React__default.createElement(CustomFilterValueInput, {
|
|
5495
|
+
onAdd: handleAddCustomFilterValue
|
|
5411
5496
|
})));
|
|
5412
5497
|
})(), /*#__PURE__*/React__default.createElement(Box$1, {
|
|
5413
5498
|
sx: {
|
|
@@ -5697,6 +5782,8 @@ const Filters = ({
|
|
|
5697
5782
|
loading: loadingFilterValues,
|
|
5698
5783
|
helperText: selectedFilterValues.length > 0 ? `${selectedFilterValues.length} value(s) selected` : 'Select at least one value',
|
|
5699
5784
|
debounceMs: 1000
|
|
5785
|
+
}), /*#__PURE__*/React__default.createElement(CustomFilterValueInput, {
|
|
5786
|
+
onAdd: handleAddCustomFilterValue
|
|
5700
5787
|
})), /*#__PURE__*/React__default.createElement(Box$1, {
|
|
5701
5788
|
sx: {
|
|
5702
5789
|
display: 'flex',
|
|
@@ -7084,14 +7171,24 @@ const ReportBuilder = ({
|
|
|
7084
7171
|
queryObj.ordered_metrics = report.metrics.map(metric => metric.id);
|
|
7085
7172
|
}
|
|
7086
7173
|
|
|
7087
|
-
// Persist the
|
|
7088
|
-
//
|
|
7089
|
-
//
|
|
7090
|
-
//
|
|
7091
|
-
|
|
7092
|
-
|
|
7093
|
-
|
|
7094
|
-
|
|
7174
|
+
// Persist the full resolved column order - the explicit Column Order tab
|
|
7175
|
+
// arrangement when defined, otherwise dimensions/computed (in their own
|
|
7176
|
+
// order) followed by metrics (in their own order), same as what the
|
|
7177
|
+
// results grid renders. Always emitted (not just when a custom order
|
|
7178
|
+
// exists) so the API/results grid get an explicit order even for
|
|
7179
|
+
// untouched reports, rather than having to fall back to guessing it.
|
|
7180
|
+
const orderedDimensionItems = report.dimensionOrder.map(entry => entry.type === "dimension" ? {
|
|
7181
|
+
kind: "dimension",
|
|
7182
|
+
data: dimensionById[entry.key]
|
|
7183
|
+
} : {
|
|
7184
|
+
kind: "computed",
|
|
7185
|
+
data: computedByName[entry.key]
|
|
7186
|
+
}).filter(item => item.data);
|
|
7187
|
+
const resolvedColumnItems = resolveColumnOrder(report.columnOrder, orderedDimensionItems, report.metrics);
|
|
7188
|
+
if (resolvedColumnItems.length > 0) {
|
|
7189
|
+
queryObj.ordered_columns = resolvedColumnItems.map(item => ({
|
|
7190
|
+
type: item.kind,
|
|
7191
|
+
ref: item.key
|
|
7095
7192
|
}));
|
|
7096
7193
|
}
|
|
7097
7194
|
|