@bygd/nc-report-ui 0.1.43 → 0.1.45

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.
@@ -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: handleKeyDown,
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
- setSelectedFilterValues(filter.values || []);
90660
- await fetchFilterValues(fullPath);
90719
+ const currentValues = filter.values || [];
90720
+ setSelectedFilterValues(currentValues);
90721
+ await fetchFilterValues(fullPath, '', currentValues);
90661
90722
  }
90662
90723
  };
90663
90724
  const handleSaveEditedFilter = fullPath => {
@@ -90666,11 +90727,6 @@ const Filters = ({
90666
90727
 
90667
90728
  // Check if this is a date filter
90668
90729
  if (isDateDimension(existingFilter.dimension)) {
90669
- // For date filters, validate that at least one date is provided
90670
- if (!dateRangeFrom && !dateRangeTo) {
90671
- return; // Don't save if no dates provided
90672
- }
90673
-
90674
90730
  // Update with new date range values (only include provided dates)
90675
90731
  const dateValues = {};
90676
90732
  if (dateRangeFrom) {
@@ -90687,11 +90743,6 @@ const Filters = ({
90687
90743
  // Save the updated filter
90688
90744
  onSaveFilter(fullPath, updatedFilter);
90689
90745
  } else {
90690
- // For regular filters, validate selected values
90691
- if (selectedFilterValues.length === 0) {
90692
- return; // Don't save if no values selected
90693
- }
90694
-
90695
90746
  // Update with new values
90696
90747
  const updatedFilter = {
90697
90748
  ...existingFilter,
@@ -90752,7 +90803,11 @@ const Filters = ({
90752
90803
  };
90753
90804
 
90754
90805
  // Fetch distinct values for the selected dimension
90755
- const fetchFilterValues = async (fullPath, searchText = '') => {
90806
+ // preserveValues lets a caller pass the values that must survive the merge
90807
+ // explicitly, instead of relying on the selectedFilterValues closure - a
90808
+ // setSelectedFilterValues() call right before fetchFilterValues() hasn't
90809
+ // committed yet by the time this function reads that state.
90810
+ const fetchFilterValues = async (fullPath, searchText = '', preserveValues = null) => {
90756
90811
  setLoadingFilterValues(true);
90757
90812
  try {
90758
90813
  // Get parameters from context if available, otherwise use default
@@ -90825,7 +90880,8 @@ const Filters = ({
90825
90880
  // Merge with currently selected values to ensure they're always available
90826
90881
  // This is important when editing filters with search text - we don't want to lose
90827
90882
  // previously selected values that don't match the current search
90828
- const selectedValueItems = selectedFilterValues.map(key => ({
90883
+ const valuesToPreserve = preserveValues !== null ? preserveValues : selectedFilterValues;
90884
+ const selectedValueItems = valuesToPreserve.map(key => ({
90829
90885
  key: String(key),
90830
90886
  value: String(key)
90831
90887
  }));
@@ -90860,13 +90916,6 @@ const Filters = ({
90860
90916
 
90861
90917
  // Check if this is a date filter
90862
90918
  const isDate = isDateDimension(selectedItem.dimension);
90863
-
90864
- // Validate based on filter type
90865
- if (isDate) {
90866
- if (!dateRangeFrom && !dateRangeTo) return; // At least one date must be provided
90867
- } else {
90868
- if (selectedFilterValues.length === 0) return; // Regular filters must have values
90869
- }
90870
90919
  if (!selectedDimension) return;
90871
90920
 
90872
90921
  // Build the complete relation objects array
@@ -90941,13 +90990,6 @@ const Filters = ({
90941
90990
  // Check if this is a date filter
90942
90991
  const isDate = isDateDimension(dimensionData.dimension);
90943
90992
 
90944
- // Validate based on filter type
90945
- if (isDate) {
90946
- if (!dateRangeFrom && !dateRangeTo) return; // At least one date must be provided
90947
- } else {
90948
- if (selectedFilterValues.length === 0) return; // Regular filters must have values
90949
- }
90950
-
90951
90993
  // Prepare filter values based on type
90952
90994
  let filterValues;
90953
90995
  if (isDate) {
@@ -91054,6 +91096,21 @@ const Filters = ({
91054
91096
  }
91055
91097
  };
91056
91098
 
91099
+ // Add a value typed in the "custom value" box directly into the selected
91100
+ // filter values, and inject it into availableFilterValues too, since
91101
+ // CheckboxMultiAutocomplete only renders a chip for keys it can resolve
91102
+ // against its items list.
91103
+ const handleAddCustomFilterValue = rawValue => {
91104
+ const value = rawValue.trim();
91105
+ if (!value) return;
91106
+ if (selectedFilterValues.includes(value)) return;
91107
+ setAvailableFilterValues(prev => prev.some(item => item.key === value) ? prev : [{
91108
+ key: value,
91109
+ value
91110
+ }, ...prev]);
91111
+ setSelectedFilterValues(prev => [...prev, value]);
91112
+ };
91113
+
91057
91114
  // Handler for search text input in filter values dropdown (debounced - triggers API call)
91058
91115
  const handleFilterSearchChange = searchText => {
91059
91116
  // Determine the fullPath based on current mode
@@ -91273,6 +91330,8 @@ const Filters = ({
91273
91330
  loading: loadingFilterValues,
91274
91331
  helperText: selectedFilterValues.length > 0 ? `${selectedFilterValues.length} value(s) selected` : 'Select at least one value',
91275
91332
  debounceMs: 1000
91333
+ }), /*#__PURE__*/gn.createElement(CustomFilterValueInput, {
91334
+ onAdd: handleAddCustomFilterValue
91276
91335
  })));
91277
91336
  })(), /*#__PURE__*/gn.createElement(Box, {
91278
91337
  sx: {
@@ -91302,14 +91361,7 @@ const Filters = ({
91302
91361
  }, "Cancel"), /*#__PURE__*/gn.createElement(Button, {
91303
91362
  variant: "contained",
91304
91363
  onClick: handleSave,
91305
- disabled: (() => {
91306
- if (!selectedDimension) return true;
91307
- const dimensionItems = getDimensionItems();
91308
- const selectedItem = dimensionItems.find(item => item.key === selectedDimension);
91309
- if (!selectedItem) return true;
91310
- const isDate = isDateDimension(selectedItem.dimension);
91311
- return isDate ? !dateRangeFrom && !dateRangeTo : selectedFilterValues.length === 0;
91312
- })(),
91364
+ disabled: !selectedDimension,
91313
91365
  sx: {
91314
91366
  height: "40px",
91315
91367
  fontFamily: "system-ui",
@@ -91424,6 +91476,8 @@ const Filters = ({
91424
91476
  loading: loadingFilterValues,
91425
91477
  helperText: selectedFilterValues.length > 0 ? `${selectedFilterValues.length} value(s) selected` : 'Select at least one value',
91426
91478
  debounceMs: 1000
91479
+ }), /*#__PURE__*/gn.createElement(CustomFilterValueInput, {
91480
+ onAdd: handleAddCustomFilterValue
91427
91481
  })));
91428
91482
  })(), /*#__PURE__*/gn.createElement(Box, {
91429
91483
  sx: {
@@ -91453,13 +91507,7 @@ const Filters = ({
91453
91507
  }, "Cancel"), /*#__PURE__*/gn.createElement(Button, {
91454
91508
  variant: "contained",
91455
91509
  onClick: handleSaveFromDimension,
91456
- disabled: (() => {
91457
- if (!selectedExistingDimension) return true;
91458
- const dimensionData = existingDimensions.find(dim => dim.fullPath === selectedExistingDimension);
91459
- if (!dimensionData) return true;
91460
- const isDate = isDateDimension(dimensionData.dimension);
91461
- return isDate ? !dateRangeFrom && !dateRangeTo : selectedFilterValues.length === 0;
91462
- })(),
91510
+ disabled: !selectedExistingDimension,
91463
91511
  sx: {
91464
91512
  height: "40px",
91465
91513
  fontFamily: "system-ui",
@@ -91713,6 +91761,8 @@ const Filters = ({
91713
91761
  loading: loadingFilterValues,
91714
91762
  helperText: selectedFilterValues.length > 0 ? `${selectedFilterValues.length} value(s) selected` : 'Select at least one value',
91715
91763
  debounceMs: 1000
91764
+ }), /*#__PURE__*/gn.createElement(CustomFilterValueInput, {
91765
+ onAdd: handleAddCustomFilterValue
91716
91766
  })), /*#__PURE__*/gn.createElement(Box, {
91717
91767
  sx: {
91718
91768
  display: 'flex',
@@ -91731,7 +91781,6 @@ const Filters = ({
91731
91781
  variant: "contained",
91732
91782
  size: "small",
91733
91783
  onClick: () => handleSaveEditedFilter(fullPath),
91734
- disabled: isDate ? !dateRangeFrom && !dateRangeTo : selectedFilterValues.length === 0,
91735
91784
  sx: {
91736
91785
  textTransform: 'none',
91737
91786
  borderRadius: '6px',
@@ -93027,20 +93076,12 @@ const ReportBuilder = ({
93027
93076
  if (report.filters && Object.keys(report.filters).length > 0) {
93028
93077
  Object.entries(report.filters).forEach(([fullPath, filterData]) => {
93029
93078
  if (filterData.values) {
93030
- // Check if values is an object (date range) or array (regular filter)
93031
- if (Array.isArray(filterData.values)) {
93032
- // Regular filter - only add if array has values
93033
- if (filterData.values.length > 0) {
93034
- conditions.push({
93035
- [fullPath]: filterData.values
93036
- });
93037
- }
93038
- } else if (typeof filterData.values === "object") {
93039
- // Date range filter - add the object as-is
93040
- conditions.push({
93041
- [fullPath]: filterData.values
93042
- });
93043
- }
93079
+ // Include the condition as-is regardless of whether the array/object
93080
+ // has any entries - an explicitly saved (but empty) filter should
93081
+ // still appear in the query.
93082
+ conditions.push({
93083
+ [fullPath]: filterData.values
93084
+ });
93044
93085
  }
93045
93086
  });
93046
93087
  }
@@ -93104,14 +93145,24 @@ const ReportBuilder = ({
93104
93145
  queryObj.ordered_metrics = report.metrics.map(metric => metric.id);
93105
93146
  }
93106
93147
 
93107
- // Persist the optional unified column order (Column Order tab) so the
93108
- // results grid can reproduce it on reload. Absent when no custom order
93109
- // has been defined, so old/untouched reports keep their current
93110
- // dimensions-then-metrics column order unchanged.
93111
- if (report.columnOrder.length > 0) {
93112
- queryObj.ordered_columns = report.columnOrder.map(entry => ({
93113
- type: entry.type,
93114
- ref: entry.key
93148
+ // Persist the full resolved column order - the explicit Column Order tab
93149
+ // arrangement when defined, otherwise dimensions/computed (in their own
93150
+ // order) followed by metrics (in their own order), same as what the
93151
+ // results grid renders. Always emitted (not just when a custom order
93152
+ // exists) so the API/results grid get an explicit order even for
93153
+ // untouched reports, rather than having to fall back to guessing it.
93154
+ const orderedDimensionItems = report.dimensionOrder.map(entry => entry.type === "dimension" ? {
93155
+ kind: "dimension",
93156
+ data: dimensionById[entry.key]
93157
+ } : {
93158
+ kind: "computed",
93159
+ data: computedByName[entry.key]
93160
+ }).filter(item => item.data);
93161
+ const resolvedColumnItems = resolveColumnOrder(report.columnOrder, orderedDimensionItems, report.metrics);
93162
+ if (resolvedColumnItems.length > 0) {
93163
+ queryObj.ordered_columns = resolvedColumnItems.map(item => ({
93164
+ type: item.kind,
93165
+ ref: item.key
93115
93166
  }));
93116
93167
  }
93117
93168
 
@@ -93834,6 +93885,7 @@ const Root = () => {
93834
93885
  const config = useConfig();
93835
93886
  const client = config.client;
93836
93887
  const merchants = config.merchants;
93888
+ const organizations = config.organizations;
93837
93889
 
93838
93890
  //const test_token = config.test_token;
93839
93891
  const token = config.tokens[config.active_user];
@@ -93851,8 +93903,10 @@ const Root = () => {
93851
93903
  // client:"7774bcad-05dc-48cf-a52b-9a8a56168818",
93852
93904
  // client:"7774bcad-05dc-48cf-a52b-9a8a56168829" // this is some wrong client id for testing
93853
93905
  // client: undefined
93854
- client: client || "7774bcad-05dc-48cf-a52b-9a8a56168818",
93855
- merchants: merchants || []
93906
+ //client: client || "7774bcad-05dc-48cf-a52b-9a8a56168818",
93907
+ client: client || [],
93908
+ merchants: merchants || [],
93909
+ organizations: organizations || []
93856
93910
  };
93857
93911
 
93858
93912
  // Api.setBaseUrl(apiDefaults.base_url);
@@ -93891,8 +93945,9 @@ var tokens = {
93891
93945
  "serdar.kizilkurt@gmail.com": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJXeFB6alpqaXJUcm5ienYwZW5iWHBhUjhoWklNQWRxb29FdEhaY0Nmbl9zIn0.eyJleHAiOjE3NjU5ODIzMjcsImlhdCI6MTc2NTk4MTQyNywianRpIjoiYjA4ZWMyMmYtN2IxMC00YzVjLWEwODgtNzA5MDhmNzJlNGIxIiwiaXNzIjoiaHR0cHM6Ly9pZHAubmV0Y2FwaXRhbC5wcm8vcmVhbG1zL25ldGNhcGl0YWwiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiNjAzNGI5NzktNzkyNi00NjJlLTljYjQtYjJmZWMzYjNjYzhjIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiYXBwIiwic2lkIjoiMjczYzA0OTItYjQwZS00ZmIwLWEwYWItMTRmMDU5NzUzYTM2IiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyJodHRwOi8vbG9jYWxob3N0OjMxMDIiLCJodHRwczovL2FwcC5uZXRjYXBpdGFsLnBybyIsImh0dHBzOi8vZGV2Lm5ldGNhcGl0YWwucHJvIl0sInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJkZWZhdWx0LXJvbGVzLW5ldGNhcGl0YWwiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJvcGVuaWQgZW1haWwgcHJvZmlsZSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJuYW1lIjoiU2VyZGFyIFJlZFdvbGYiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJzZXJkYXIua2l6aWxrdXJ0QGdtYWlsLmNvbSIsImdpdmVuX25hbWUiOiJTZXJkYXIiLCJmYW1pbHlfbmFtZSI6IlJlZFdvbGYiLCJlbWFpbCI6InNlcmRhci5raXppbGt1cnRAZ21haWwuY29tIn0.mIrffBy6n4HmCnFJAX7Njl8fR-m-QQtsn2iGbyefXXK17lCP8b_lUlAvHHFaan7Te8OT8P8rHu9FgfSyPQkBMqZ0AVVf6VrhUmEO12wZx71Di59QNHjcZKfljNykPKLXXWEIWlulvScrLUsxuR5emufTJaxD9zU4fJzRh00vDCIuzIyl9o51asLmhNAtNnW8QUtT-rWOM6Fgh3pskP4yrGUe5dI17DpAYafcWgUNx86Qi9E09nzUE5IToKgXfQvVpPs7_ZwlcfOE95VNcO_pwRQQ85sJmFiUCg5_TWzNFJKoTui5barb74oBCP7OCJTb5yj5KeriL2ozg0e2IyVrHA"
93892
93946
  };
93893
93947
  var active_user = "s.kizilkurt@azerion.com";
93894
- var client = "7774bcad-05dc-48cf-a52b-9a8a56168818";
93948
+ var client = [];
93895
93949
  var merchants = [];
93950
+ var organizations = [];
93896
93951
  var base_url_local = "http://localhost:8081";
93897
93952
  var base_url_prod = "https://dev-report-api.netcapital.pro";
93898
93953
  var base_url = "http://localhost:8081";
@@ -93904,6 +93959,7 @@ var config = {
93904
93959
  active_user: active_user,
93905
93960
  client: client,
93906
93961
  merchants: merchants,
93962
+ organizations: organizations,
93907
93963
  base_url_local: base_url_local,
93908
93964
  base_url_prod: base_url_prod,
93909
93965
  base_url: base_url,
@@ -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: handleKeyDown,
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
- setSelectedFilterValues(filter.values || []);
4725
- await fetchFilterValues(fullPath);
4784
+ const currentValues = filter.values || [];
4785
+ setSelectedFilterValues(currentValues);
4786
+ await fetchFilterValues(fullPath, '', currentValues);
4726
4787
  }
4727
4788
  };
4728
4789
  const handleSaveEditedFilter = fullPath => {
@@ -4731,11 +4792,6 @@ const Filters = ({
4731
4792
 
4732
4793
  // Check if this is a date filter
4733
4794
  if (isDateDimension(existingFilter.dimension)) {
4734
- // For date filters, validate that at least one date is provided
4735
- if (!dateRangeFrom && !dateRangeTo) {
4736
- return; // Don't save if no dates provided
4737
- }
4738
-
4739
4795
  // Update with new date range values (only include provided dates)
4740
4796
  const dateValues = {};
4741
4797
  if (dateRangeFrom) {
@@ -4752,11 +4808,6 @@ const Filters = ({
4752
4808
  // Save the updated filter
4753
4809
  onSaveFilter(fullPath, updatedFilter);
4754
4810
  } else {
4755
- // For regular filters, validate selected values
4756
- if (selectedFilterValues.length === 0) {
4757
- return; // Don't save if no values selected
4758
- }
4759
-
4760
4811
  // Update with new values
4761
4812
  const updatedFilter = {
4762
4813
  ...existingFilter,
@@ -4817,7 +4868,11 @@ const Filters = ({
4817
4868
  };
4818
4869
 
4819
4870
  // Fetch distinct values for the selected dimension
4820
- const fetchFilterValues = async (fullPath, searchText = '') => {
4871
+ // preserveValues lets a caller pass the values that must survive the merge
4872
+ // explicitly, instead of relying on the selectedFilterValues closure - a
4873
+ // setSelectedFilterValues() call right before fetchFilterValues() hasn't
4874
+ // committed yet by the time this function reads that state.
4875
+ const fetchFilterValues = async (fullPath, searchText = '', preserveValues = null) => {
4821
4876
  setLoadingFilterValues(true);
4822
4877
  try {
4823
4878
  // Get parameters from context if available, otherwise use default
@@ -4890,7 +4945,8 @@ const Filters = ({
4890
4945
  // Merge with currently selected values to ensure they're always available
4891
4946
  // This is important when editing filters with search text - we don't want to lose
4892
4947
  // previously selected values that don't match the current search
4893
- const selectedValueItems = selectedFilterValues.map(key => ({
4948
+ const valuesToPreserve = preserveValues !== null ? preserveValues : selectedFilterValues;
4949
+ const selectedValueItems = valuesToPreserve.map(key => ({
4894
4950
  key: String(key),
4895
4951
  value: String(key)
4896
4952
  }));
@@ -4925,13 +4981,6 @@ const Filters = ({
4925
4981
 
4926
4982
  // Check if this is a date filter
4927
4983
  const isDate = isDateDimension(selectedItem.dimension);
4928
-
4929
- // Validate based on filter type
4930
- if (isDate) {
4931
- if (!dateRangeFrom && !dateRangeTo) return; // At least one date must be provided
4932
- } else {
4933
- if (selectedFilterValues.length === 0) return; // Regular filters must have values
4934
- }
4935
4984
  if (!selectedDimension) return;
4936
4985
 
4937
4986
  // Build the complete relation objects array
@@ -5006,13 +5055,6 @@ const Filters = ({
5006
5055
  // Check if this is a date filter
5007
5056
  const isDate = isDateDimension(dimensionData.dimension);
5008
5057
 
5009
- // Validate based on filter type
5010
- if (isDate) {
5011
- if (!dateRangeFrom && !dateRangeTo) return; // At least one date must be provided
5012
- } else {
5013
- if (selectedFilterValues.length === 0) return; // Regular filters must have values
5014
- }
5015
-
5016
5058
  // Prepare filter values based on type
5017
5059
  let filterValues;
5018
5060
  if (isDate) {
@@ -5119,6 +5161,21 @@ const Filters = ({
5119
5161
  }
5120
5162
  };
5121
5163
 
5164
+ // Add a value typed in the "custom value" box directly into the selected
5165
+ // filter values, and inject it into availableFilterValues too, since
5166
+ // CheckboxMultiAutocomplete only renders a chip for keys it can resolve
5167
+ // against its items list.
5168
+ const handleAddCustomFilterValue = rawValue => {
5169
+ const value = rawValue.trim();
5170
+ if (!value) return;
5171
+ if (selectedFilterValues.includes(value)) return;
5172
+ setAvailableFilterValues(prev => prev.some(item => item.key === value) ? prev : [{
5173
+ key: value,
5174
+ value
5175
+ }, ...prev]);
5176
+ setSelectedFilterValues(prev => [...prev, value]);
5177
+ };
5178
+
5122
5179
  // Handler for search text input in filter values dropdown (debounced - triggers API call)
5123
5180
  const handleFilterSearchChange = searchText => {
5124
5181
  // Determine the fullPath based on current mode
@@ -5338,6 +5395,8 @@ const Filters = ({
5338
5395
  loading: loadingFilterValues,
5339
5396
  helperText: selectedFilterValues.length > 0 ? `${selectedFilterValues.length} value(s) selected` : 'Select at least one value',
5340
5397
  debounceMs: 1000
5398
+ }), /*#__PURE__*/React__namespace.default.createElement(CustomFilterValueInput, {
5399
+ onAdd: handleAddCustomFilterValue
5341
5400
  })));
5342
5401
  })(), /*#__PURE__*/React__namespace.default.createElement(material.Box, {
5343
5402
  sx: {
@@ -5367,14 +5426,7 @@ const Filters = ({
5367
5426
  }, "Cancel"), /*#__PURE__*/React__namespace.default.createElement(material.Button, {
5368
5427
  variant: "contained",
5369
5428
  onClick: handleSave,
5370
- disabled: (() => {
5371
- if (!selectedDimension) return true;
5372
- const dimensionItems = getDimensionItems();
5373
- const selectedItem = dimensionItems.find(item => item.key === selectedDimension);
5374
- if (!selectedItem) return true;
5375
- const isDate = isDateDimension(selectedItem.dimension);
5376
- return isDate ? !dateRangeFrom && !dateRangeTo : selectedFilterValues.length === 0;
5377
- })(),
5429
+ disabled: !selectedDimension,
5378
5430
  sx: {
5379
5431
  height: "40px",
5380
5432
  fontFamily: "system-ui",
@@ -5489,6 +5541,8 @@ const Filters = ({
5489
5541
  loading: loadingFilterValues,
5490
5542
  helperText: selectedFilterValues.length > 0 ? `${selectedFilterValues.length} value(s) selected` : 'Select at least one value',
5491
5543
  debounceMs: 1000
5544
+ }), /*#__PURE__*/React__namespace.default.createElement(CustomFilterValueInput, {
5545
+ onAdd: handleAddCustomFilterValue
5492
5546
  })));
5493
5547
  })(), /*#__PURE__*/React__namespace.default.createElement(material.Box, {
5494
5548
  sx: {
@@ -5518,13 +5572,7 @@ const Filters = ({
5518
5572
  }, "Cancel"), /*#__PURE__*/React__namespace.default.createElement(material.Button, {
5519
5573
  variant: "contained",
5520
5574
  onClick: handleSaveFromDimension,
5521
- disabled: (() => {
5522
- if (!selectedExistingDimension) return true;
5523
- const dimensionData = existingDimensions.find(dim => dim.fullPath === selectedExistingDimension);
5524
- if (!dimensionData) return true;
5525
- const isDate = isDateDimension(dimensionData.dimension);
5526
- return isDate ? !dateRangeFrom && !dateRangeTo : selectedFilterValues.length === 0;
5527
- })(),
5575
+ disabled: !selectedExistingDimension,
5528
5576
  sx: {
5529
5577
  height: "40px",
5530
5578
  fontFamily: "system-ui",
@@ -5778,6 +5826,8 @@ const Filters = ({
5778
5826
  loading: loadingFilterValues,
5779
5827
  helperText: selectedFilterValues.length > 0 ? `${selectedFilterValues.length} value(s) selected` : 'Select at least one value',
5780
5828
  debounceMs: 1000
5829
+ }), /*#__PURE__*/React__namespace.default.createElement(CustomFilterValueInput, {
5830
+ onAdd: handleAddCustomFilterValue
5781
5831
  })), /*#__PURE__*/React__namespace.default.createElement(material.Box, {
5782
5832
  sx: {
5783
5833
  display: 'flex',
@@ -5796,7 +5846,6 @@ const Filters = ({
5796
5846
  variant: "contained",
5797
5847
  size: "small",
5798
5848
  onClick: () => handleSaveEditedFilter(fullPath),
5799
- disabled: isDate ? !dateRangeFrom && !dateRangeTo : selectedFilterValues.length === 0,
5800
5849
  sx: {
5801
5850
  textTransform: 'none',
5802
5851
  borderRadius: '6px',
@@ -7088,20 +7137,12 @@ const ReportBuilder = ({
7088
7137
  if (report.filters && Object.keys(report.filters).length > 0) {
7089
7138
  Object.entries(report.filters).forEach(([fullPath, filterData]) => {
7090
7139
  if (filterData.values) {
7091
- // Check if values is an object (date range) or array (regular filter)
7092
- if (Array.isArray(filterData.values)) {
7093
- // Regular filter - only add if array has values
7094
- if (filterData.values.length > 0) {
7095
- conditions.push({
7096
- [fullPath]: filterData.values
7097
- });
7098
- }
7099
- } else if (typeof filterData.values === "object") {
7100
- // Date range filter - add the object as-is
7101
- conditions.push({
7102
- [fullPath]: filterData.values
7103
- });
7104
- }
7140
+ // Include the condition as-is regardless of whether the array/object
7141
+ // has any entries - an explicitly saved (but empty) filter should
7142
+ // still appear in the query.
7143
+ conditions.push({
7144
+ [fullPath]: filterData.values
7145
+ });
7105
7146
  }
7106
7147
  });
7107
7148
  }
@@ -7165,14 +7206,24 @@ const ReportBuilder = ({
7165
7206
  queryObj.ordered_metrics = report.metrics.map(metric => metric.id);
7166
7207
  }
7167
7208
 
7168
- // Persist the optional unified column order (Column Order tab) so the
7169
- // results grid can reproduce it on reload. Absent when no custom order
7170
- // has been defined, so old/untouched reports keep their current
7171
- // dimensions-then-metrics column order unchanged.
7172
- if (report.columnOrder.length > 0) {
7173
- queryObj.ordered_columns = report.columnOrder.map(entry => ({
7174
- type: entry.type,
7175
- ref: entry.key
7209
+ // Persist the full resolved column order - the explicit Column Order tab
7210
+ // arrangement when defined, otherwise dimensions/computed (in their own
7211
+ // order) followed by metrics (in their own order), same as what the
7212
+ // results grid renders. Always emitted (not just when a custom order
7213
+ // exists) so the API/results grid get an explicit order even for
7214
+ // untouched reports, rather than having to fall back to guessing it.
7215
+ const orderedDimensionItems = report.dimensionOrder.map(entry => entry.type === "dimension" ? {
7216
+ kind: "dimension",
7217
+ data: dimensionById[entry.key]
7218
+ } : {
7219
+ kind: "computed",
7220
+ data: computedByName[entry.key]
7221
+ }).filter(item => item.data);
7222
+ const resolvedColumnItems = resolveColumnOrder(report.columnOrder, orderedDimensionItems, report.metrics);
7223
+ if (resolvedColumnItems.length > 0) {
7224
+ queryObj.ordered_columns = resolvedColumnItems.map(item => ({
7225
+ type: item.kind,
7226
+ ref: item.key
7176
7227
  }));
7177
7228
  }
7178
7229
 
@@ -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: handleKeyDown,
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
- setSelectedFilterValues(filter.values || []);
4644
- await fetchFilterValues(fullPath);
4703
+ const currentValues = filter.values || [];
4704
+ setSelectedFilterValues(currentValues);
4705
+ await fetchFilterValues(fullPath, '', currentValues);
4645
4706
  }
4646
4707
  };
4647
4708
  const handleSaveEditedFilter = fullPath => {
@@ -4650,11 +4711,6 @@ const Filters = ({
4650
4711
 
4651
4712
  // Check if this is a date filter
4652
4713
  if (isDateDimension(existingFilter.dimension)) {
4653
- // For date filters, validate that at least one date is provided
4654
- if (!dateRangeFrom && !dateRangeTo) {
4655
- return; // Don't save if no dates provided
4656
- }
4657
-
4658
4714
  // Update with new date range values (only include provided dates)
4659
4715
  const dateValues = {};
4660
4716
  if (dateRangeFrom) {
@@ -4671,11 +4727,6 @@ const Filters = ({
4671
4727
  // Save the updated filter
4672
4728
  onSaveFilter(fullPath, updatedFilter);
4673
4729
  } else {
4674
- // For regular filters, validate selected values
4675
- if (selectedFilterValues.length === 0) {
4676
- return; // Don't save if no values selected
4677
- }
4678
-
4679
4730
  // Update with new values
4680
4731
  const updatedFilter = {
4681
4732
  ...existingFilter,
@@ -4736,7 +4787,11 @@ const Filters = ({
4736
4787
  };
4737
4788
 
4738
4789
  // Fetch distinct values for the selected dimension
4739
- const fetchFilterValues = async (fullPath, searchText = '') => {
4790
+ // preserveValues lets a caller pass the values that must survive the merge
4791
+ // explicitly, instead of relying on the selectedFilterValues closure - a
4792
+ // setSelectedFilterValues() call right before fetchFilterValues() hasn't
4793
+ // committed yet by the time this function reads that state.
4794
+ const fetchFilterValues = async (fullPath, searchText = '', preserveValues = null) => {
4740
4795
  setLoadingFilterValues(true);
4741
4796
  try {
4742
4797
  // Get parameters from context if available, otherwise use default
@@ -4809,7 +4864,8 @@ const Filters = ({
4809
4864
  // Merge with currently selected values to ensure they're always available
4810
4865
  // This is important when editing filters with search text - we don't want to lose
4811
4866
  // previously selected values that don't match the current search
4812
- const selectedValueItems = selectedFilterValues.map(key => ({
4867
+ const valuesToPreserve = preserveValues !== null ? preserveValues : selectedFilterValues;
4868
+ const selectedValueItems = valuesToPreserve.map(key => ({
4813
4869
  key: String(key),
4814
4870
  value: String(key)
4815
4871
  }));
@@ -4844,13 +4900,6 @@ const Filters = ({
4844
4900
 
4845
4901
  // Check if this is a date filter
4846
4902
  const isDate = isDateDimension(selectedItem.dimension);
4847
-
4848
- // Validate based on filter type
4849
- if (isDate) {
4850
- if (!dateRangeFrom && !dateRangeTo) return; // At least one date must be provided
4851
- } else {
4852
- if (selectedFilterValues.length === 0) return; // Regular filters must have values
4853
- }
4854
4903
  if (!selectedDimension) return;
4855
4904
 
4856
4905
  // Build the complete relation objects array
@@ -4925,13 +4974,6 @@ const Filters = ({
4925
4974
  // Check if this is a date filter
4926
4975
  const isDate = isDateDimension(dimensionData.dimension);
4927
4976
 
4928
- // Validate based on filter type
4929
- if (isDate) {
4930
- if (!dateRangeFrom && !dateRangeTo) return; // At least one date must be provided
4931
- } else {
4932
- if (selectedFilterValues.length === 0) return; // Regular filters must have values
4933
- }
4934
-
4935
4977
  // Prepare filter values based on type
4936
4978
  let filterValues;
4937
4979
  if (isDate) {
@@ -5038,6 +5080,21 @@ const Filters = ({
5038
5080
  }
5039
5081
  };
5040
5082
 
5083
+ // Add a value typed in the "custom value" box directly into the selected
5084
+ // filter values, and inject it into availableFilterValues too, since
5085
+ // CheckboxMultiAutocomplete only renders a chip for keys it can resolve
5086
+ // against its items list.
5087
+ const handleAddCustomFilterValue = rawValue => {
5088
+ const value = rawValue.trim();
5089
+ if (!value) return;
5090
+ if (selectedFilterValues.includes(value)) return;
5091
+ setAvailableFilterValues(prev => prev.some(item => item.key === value) ? prev : [{
5092
+ key: value,
5093
+ value
5094
+ }, ...prev]);
5095
+ setSelectedFilterValues(prev => [...prev, value]);
5096
+ };
5097
+
5041
5098
  // Handler for search text input in filter values dropdown (debounced - triggers API call)
5042
5099
  const handleFilterSearchChange = searchText => {
5043
5100
  // Determine the fullPath based on current mode
@@ -5257,6 +5314,8 @@ const Filters = ({
5257
5314
  loading: loadingFilterValues,
5258
5315
  helperText: selectedFilterValues.length > 0 ? `${selectedFilterValues.length} value(s) selected` : 'Select at least one value',
5259
5316
  debounceMs: 1000
5317
+ }), /*#__PURE__*/React__default.createElement(CustomFilterValueInput, {
5318
+ onAdd: handleAddCustomFilterValue
5260
5319
  })));
5261
5320
  })(), /*#__PURE__*/React__default.createElement(Box$1, {
5262
5321
  sx: {
@@ -5286,14 +5345,7 @@ const Filters = ({
5286
5345
  }, "Cancel"), /*#__PURE__*/React__default.createElement(Button, {
5287
5346
  variant: "contained",
5288
5347
  onClick: handleSave,
5289
- disabled: (() => {
5290
- if (!selectedDimension) return true;
5291
- const dimensionItems = getDimensionItems();
5292
- const selectedItem = dimensionItems.find(item => item.key === selectedDimension);
5293
- if (!selectedItem) return true;
5294
- const isDate = isDateDimension(selectedItem.dimension);
5295
- return isDate ? !dateRangeFrom && !dateRangeTo : selectedFilterValues.length === 0;
5296
- })(),
5348
+ disabled: !selectedDimension,
5297
5349
  sx: {
5298
5350
  height: "40px",
5299
5351
  fontFamily: "system-ui",
@@ -5408,6 +5460,8 @@ const Filters = ({
5408
5460
  loading: loadingFilterValues,
5409
5461
  helperText: selectedFilterValues.length > 0 ? `${selectedFilterValues.length} value(s) selected` : 'Select at least one value',
5410
5462
  debounceMs: 1000
5463
+ }), /*#__PURE__*/React__default.createElement(CustomFilterValueInput, {
5464
+ onAdd: handleAddCustomFilterValue
5411
5465
  })));
5412
5466
  })(), /*#__PURE__*/React__default.createElement(Box$1, {
5413
5467
  sx: {
@@ -5437,13 +5491,7 @@ const Filters = ({
5437
5491
  }, "Cancel"), /*#__PURE__*/React__default.createElement(Button, {
5438
5492
  variant: "contained",
5439
5493
  onClick: handleSaveFromDimension,
5440
- disabled: (() => {
5441
- if (!selectedExistingDimension) return true;
5442
- const dimensionData = existingDimensions.find(dim => dim.fullPath === selectedExistingDimension);
5443
- if (!dimensionData) return true;
5444
- const isDate = isDateDimension(dimensionData.dimension);
5445
- return isDate ? !dateRangeFrom && !dateRangeTo : selectedFilterValues.length === 0;
5446
- })(),
5494
+ disabled: !selectedExistingDimension,
5447
5495
  sx: {
5448
5496
  height: "40px",
5449
5497
  fontFamily: "system-ui",
@@ -5697,6 +5745,8 @@ const Filters = ({
5697
5745
  loading: loadingFilterValues,
5698
5746
  helperText: selectedFilterValues.length > 0 ? `${selectedFilterValues.length} value(s) selected` : 'Select at least one value',
5699
5747
  debounceMs: 1000
5748
+ }), /*#__PURE__*/React__default.createElement(CustomFilterValueInput, {
5749
+ onAdd: handleAddCustomFilterValue
5700
5750
  })), /*#__PURE__*/React__default.createElement(Box$1, {
5701
5751
  sx: {
5702
5752
  display: 'flex',
@@ -5715,7 +5765,6 @@ const Filters = ({
5715
5765
  variant: "contained",
5716
5766
  size: "small",
5717
5767
  onClick: () => handleSaveEditedFilter(fullPath),
5718
- disabled: isDate ? !dateRangeFrom && !dateRangeTo : selectedFilterValues.length === 0,
5719
5768
  sx: {
5720
5769
  textTransform: 'none',
5721
5770
  borderRadius: '6px',
@@ -7007,20 +7056,12 @@ const ReportBuilder = ({
7007
7056
  if (report.filters && Object.keys(report.filters).length > 0) {
7008
7057
  Object.entries(report.filters).forEach(([fullPath, filterData]) => {
7009
7058
  if (filterData.values) {
7010
- // Check if values is an object (date range) or array (regular filter)
7011
- if (Array.isArray(filterData.values)) {
7012
- // Regular filter - only add if array has values
7013
- if (filterData.values.length > 0) {
7014
- conditions.push({
7015
- [fullPath]: filterData.values
7016
- });
7017
- }
7018
- } else if (typeof filterData.values === "object") {
7019
- // Date range filter - add the object as-is
7020
- conditions.push({
7021
- [fullPath]: filterData.values
7022
- });
7023
- }
7059
+ // Include the condition as-is regardless of whether the array/object
7060
+ // has any entries - an explicitly saved (but empty) filter should
7061
+ // still appear in the query.
7062
+ conditions.push({
7063
+ [fullPath]: filterData.values
7064
+ });
7024
7065
  }
7025
7066
  });
7026
7067
  }
@@ -7084,14 +7125,24 @@ const ReportBuilder = ({
7084
7125
  queryObj.ordered_metrics = report.metrics.map(metric => metric.id);
7085
7126
  }
7086
7127
 
7087
- // Persist the optional unified column order (Column Order tab) so the
7088
- // results grid can reproduce it on reload. Absent when no custom order
7089
- // has been defined, so old/untouched reports keep their current
7090
- // dimensions-then-metrics column order unchanged.
7091
- if (report.columnOrder.length > 0) {
7092
- queryObj.ordered_columns = report.columnOrder.map(entry => ({
7093
- type: entry.type,
7094
- ref: entry.key
7128
+ // Persist the full resolved column order - the explicit Column Order tab
7129
+ // arrangement when defined, otherwise dimensions/computed (in their own
7130
+ // order) followed by metrics (in their own order), same as what the
7131
+ // results grid renders. Always emitted (not just when a custom order
7132
+ // exists) so the API/results grid get an explicit order even for
7133
+ // untouched reports, rather than having to fall back to guessing it.
7134
+ const orderedDimensionItems = report.dimensionOrder.map(entry => entry.type === "dimension" ? {
7135
+ kind: "dimension",
7136
+ data: dimensionById[entry.key]
7137
+ } : {
7138
+ kind: "computed",
7139
+ data: computedByName[entry.key]
7140
+ }).filter(item => item.data);
7141
+ const resolvedColumnItems = resolveColumnOrder(report.columnOrder, orderedDimensionItems, report.metrics);
7142
+ if (resolvedColumnItems.length > 0) {
7143
+ queryObj.ordered_columns = resolvedColumnItems.map(item => ({
7144
+ type: item.kind,
7145
+ ref: item.key
7095
7146
  }));
7096
7147
  }
7097
7148
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bygd/nc-report-ui",
3
- "version": "0.1.43",
3
+ "version": "0.1.45",
4
4
  "files": [
5
5
  "dist/*",
6
6
  "fnet/input.yaml",