@visns-studio/visns-components 5.11.10 → 5.11.12

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/README.md CHANGED
@@ -321,6 +321,7 @@ The DataGrid component utilizes a modular column renderer system with 28 special
321
321
  - **Extensibility**: Easy to add new column types or modify existing ones
322
322
  - **Maintainability**: Centralized location for all column rendering logic
323
323
  - **Performance**: Optimized rendering for each specific column type
324
+ - **Intelligent Sorting**: Automatic detection of sortable relationship and JSON fields
324
325
 
325
326
  **Supported Column Types:**
326
327
  - `boolean` - Yes/No display with tooltips
@@ -354,6 +355,133 @@ The DataGrid component utilizes a modular column renderer system with 28 special
354
355
 
355
356
  All column renderers are exported from `@visns-studio/visns-components` and can be used individually or as part of the DataGrid component.
356
357
 
358
+ #### Intelligent Relationship Sorting
359
+
360
+ The DataGrid component includes intelligent sorting capabilities that automatically detect and enable sorting for relationship and JSON fields. This feature works seamlessly with the backend `HasRelationshipSorting` trait.
361
+
362
+ **Automatic Sortable Detection:**
363
+
364
+ ```jsx
365
+ // The DataGrid automatically detects sortable fields based on naming patterns
366
+ <DataGrid
367
+ columns={[
368
+ {
369
+ name: 'user.profile.name', // Relationship field - automatically sortable
370
+ headerName: 'User Name',
371
+ nameFrom: ['user', 'profile', 'name']
372
+ },
373
+ {
374
+ name: 'settings.theme', // JSON field - automatically sortable
375
+ headerName: 'Theme Preference',
376
+ nameFrom: ['settings', 'theme']
377
+ },
378
+ {
379
+ name: 'client.company', // BelongsTo relationship - automatically sortable
380
+ headerName: 'Company',
381
+ nameFrom: ['client', 'company']
382
+ }
383
+ ]}
384
+ settings={{
385
+ intelligentSorting: true // Enable intelligent sorting (default: true)
386
+ }}
387
+ />
388
+ ```
389
+
390
+ **Manual Sortable Control:**
391
+
392
+ ```jsx
393
+ // Override automatic detection with explicit sortable configuration
394
+ <DataGrid
395
+ columns={[
396
+ {
397
+ name: 'user.profile.name',
398
+ headerName: 'User Name',
399
+ sortable: true, // Explicitly enable sorting
400
+ nameFrom: ['user', 'profile', 'name']
401
+ },
402
+ {
403
+ name: 'complex.calculation',
404
+ headerName: 'Complex Field',
405
+ sortable: false, // Explicitly disable sorting
406
+ nameFrom: ['complex', 'calculation']
407
+ }
408
+ ]}
409
+ />
410
+ ```
411
+
412
+ **Supported Sorting Patterns:**
413
+
414
+ The intelligent sorting system recognizes these field patterns as sortable:
415
+
416
+ - **Relationship Fields**: `user.profile.name`, `order.customer.company`
417
+ - **JSON Fields**: `settings.theme`, `metadata.tags`, `data.preferences.language`
418
+ - **Standard Fields**: Any direct model attribute
419
+ - **Nested Relationships**: `post.author.profile.display_name`
420
+
421
+ **Field Name Detection:**
422
+
423
+ The sorting system intelligently determines sortable fields using:
424
+
425
+ 1. **nameFrom Array**: Primary method for complex field paths
426
+ ```jsx
427
+ nameFrom: ['user', 'profile', 'full_name'] // → user.profile.full_name
428
+ ```
429
+
430
+ 2. **Dot Notation**: Direct string paths
431
+ ```jsx
432
+ name: 'client.address.city' // → client.address.city
433
+ ```
434
+
435
+ 3. **Pattern Recognition**: Automatic detection of common relationship patterns
436
+ - Fields ending with `_id` that have corresponding relationships
437
+ - JSON field patterns like `data.*`, `settings.*`, `metadata.*`
438
+ - Nested object notation
439
+
440
+ **Configuration Options:**
441
+
442
+ ```jsx
443
+ <DataGrid
444
+ settings={{
445
+ // Global intelligent sorting toggle
446
+ intelligentSorting: true, // default: true
447
+
448
+ // Additional sorting configuration
449
+ sortingConfig: {
450
+ // Custom sortable field patterns
451
+ additionalPatterns: [
452
+ /^custom_data\./, // Match custom_data.* fields
453
+ /^config\./ // Match config.* fields
454
+ ],
455
+
456
+ // Fields to exclude from automatic sorting
457
+ excludePatterns: [
458
+ /^temp\./, // Exclude temp.* fields
459
+ /^cache\./ // Exclude cache.* fields
460
+ ]
461
+ }
462
+ }}
463
+ />
464
+ ```
465
+
466
+ **Backend Integration:**
467
+
468
+ The frontend intelligent sorting works seamlessly with the backend `HasRelationshipSorting` trait:
469
+
470
+ ```jsx
471
+ // Frontend automatically generates these API calls:
472
+ // GET /ajax/users/table?orderBy=profile.name&order=asc
473
+ // GET /ajax/orders/table?orderBy=customer.company&order=desc
474
+ // GET /ajax/products/table?orderBy=metadata.category&order=asc
475
+ ```
476
+
477
+ **Benefits:**
478
+
479
+ - **Zero Configuration**: Most relationship and JSON fields work automatically
480
+ - **Performance Optimized**: Uses backend subqueries instead of joins
481
+ - **Type Safety**: Automatic validation of sortable field patterns
482
+ - **Fallback Support**: Graceful handling of unsupported sort fields
483
+ - **Developer Friendly**: Clear visual indicators for sortable columns
484
+
357
485
  #### DataGrid Props
358
486
 
359
487
  | Prop | Description | Type | Default |
package/package.json CHANGED
@@ -24,13 +24,13 @@
24
24
  "dayjs": "^1.11.13",
25
25
  "fabric": "^6.7.0",
26
26
  "file-saver": "^2.0.5",
27
- "framer-motion": "^12.18.1",
27
+ "framer-motion": "^12.19.1",
28
28
  "html-react-parser": "^5.2.5",
29
29
  "lodash": "^4.17.21",
30
30
  "lodash.debounce": "^4.0.8",
31
31
  "lucide-react": "^0.518.0",
32
32
  "moment": "^2.30.1",
33
- "motion": "^12.18.1",
33
+ "motion": "^12.19.1",
34
34
  "numeral": "^2.0.6",
35
35
  "pluralize": "^8.0.0",
36
36
  "qrcode.react": "^4.2.0",
@@ -58,7 +58,7 @@
58
58
  "reactjs-popup": "^2.0.6",
59
59
  "style-loader": "^4.0.0",
60
60
  "swapy": "^1.0.5",
61
- "sweetalert2": "^11.22.0",
61
+ "sweetalert2": "^11.22.1",
62
62
  "tesseract.js": "^6.0.1",
63
63
  "truncate": "^3.0.0",
64
64
  "uuid": "^11.1.0",
@@ -87,7 +87,7 @@
87
87
  "react-dom": "^17.0.0 || ^18.0.0"
88
88
  },
89
89
  "name": "@visns-studio/visns-components",
90
- "version": "5.11.10",
90
+ "version": "5.11.12",
91
91
  "description": "Various packages to assist in the development of our Custom Applications.",
92
92
  "main": "src/index.js",
93
93
  "files": [
@@ -36,6 +36,7 @@ import 'react-toggle/style.css';
36
36
 
37
37
  import CustomFetch from '../crm/Fetch';
38
38
  import Form from './Form';
39
+ import { extractColumnsMetadata, isColumnSortable } from '../../utils/columnsMetadataUtils';
39
40
 
40
41
  const loadData = async (
41
42
  { skip, limit, sortInfo, currentData, filterValue, groupBy },
@@ -83,10 +84,14 @@ const loadData = async (
83
84
  try {
84
85
  const response = await fetchUtil.post(url, params);
85
86
  const { data, total } = response.data;
86
- return { data, count: total };
87
+
88
+ // Extract columns metadata if available
89
+ const metadata = extractColumnsMetadata(response);
90
+
91
+ return { data, count: total, metadata };
87
92
  } catch (error) {
88
93
  console.error('Error fetching data:', error);
89
- return { data: [], count: 0 };
94
+ return { data: [], count: 0, metadata: {} };
90
95
  }
91
96
  };
92
97
 
@@ -146,9 +151,16 @@ const DataGrid = ({
146
151
 
147
152
  sqlResult.then((res) => {
148
153
  setDataCount(res.count);
154
+ // Update metadata when data is loaded
155
+ if (res.metadata) {
156
+ setColumnsMetadata(res.metadata);
157
+ }
149
158
  });
150
159
 
151
- return sqlResult;
160
+ return sqlResult.then(res => ({
161
+ data: res.data,
162
+ count: res.count
163
+ }));
152
164
  },
153
165
  [ajaxSetting, dataReload, dSearch]
154
166
  );
@@ -157,6 +169,7 @@ const DataGrid = ({
157
169
  const [gridColumns, setGridColumns] = useState([]);
158
170
  const [limit, setLimit] = useState(ajaxSetting.take || 10);
159
171
  const [search, setSearch] = useState('');
172
+ const [columnsMetadata, setColumnsMetadata] = useState({});
160
173
 
161
174
  /** Table Functions */
162
175
  const handleChangeSearch = (e) => {
@@ -455,9 +468,14 @@ const DataGrid = ({
455
468
  let style;
456
469
  let value;
457
470
 
471
+ // Check if column is sortable based on metadata
472
+ const columnKey = column.id;
473
+ const sortable = isColumnSortable(columnKey, columnsMetadata);
474
+
458
475
  const commonProps = {
459
476
  header: column.label,
460
477
  defaultFlex: 1,
478
+ sortable: sortable,
461
479
  link: column.link ? column.link : {},
462
480
  minWidth:
463
481
  column.minWidth && column.minWidth > 0
@@ -1384,7 +1402,7 @@ const DataGrid = ({
1384
1402
  });
1385
1403
 
1386
1404
  setFilterValue(newFilterValue);
1387
- }, [columns, filterDataSource]);
1405
+ }, [columns, filterDataSource, columnsMetadata]);
1388
1406
 
1389
1407
  useEffect(() => {
1390
1408
  if (setFilterData) {
@@ -178,11 +178,11 @@ const VisnsAutocomplete = memo((props) => {
178
178
 
179
179
  // Effect to handle overwrite toggle
180
180
  useEffect(() => {
181
- if (!overwrite && search) {
181
+ if (!overwrite && search && search.length > 1 && isFocused) {
182
182
  setIsLoading(true);
183
183
  performSearch(1, search);
184
184
  }
185
- }, [overwrite, search, performSearch]);
185
+ }, [overwrite, search, performSearch, isFocused]);
186
186
 
187
187
  // Effect to sync with inputValue prop
188
188
  useEffect(() => {
@@ -274,7 +274,7 @@ const VisnsAutocomplete = memo((props) => {
274
274
  )}
275
275
  </div>
276
276
 
277
- {isFocused && (search.length > 1 || isLoading) && (
277
+ {isFocused && !overwrite && (search.length > 1 || isLoading) && (
278
278
  <ul
279
279
  ref={resultsRef}
280
280
  className={styles['AutocompletePlace-results']}
@@ -41,6 +41,8 @@ import ReactDataGrid from '@visns-studio/visns-datagrid-enterprise';
41
41
  import { toast } from 'react-toastify';
42
42
  import fetchUtil from '../../utils/fetchUtil';
43
43
  import { confirmDialog } from '../utils/ConfirmDialog';
44
+ import { DEFAULT_INTELLIGENT_SORTING_CONFIG } from '../../utils/relationshipSortingUtils';
45
+ import { extractColumnsMetadata, isColumnSortable } from '../../utils/columnsMetadataUtils';
44
46
  import {
45
47
  AlarmClock,
46
48
  RotateCcw,
@@ -204,10 +206,14 @@ const loadData = async (
204
206
  try {
205
207
  const response = await fetchUtil.post(url, params);
206
208
  const { data, total } = response.data;
207
- return { data, count: total };
209
+
210
+ // Extract columns metadata if available
211
+ const metadata = extractColumnsMetadata(response);
212
+
213
+ return { data, count: total, metadata };
208
214
  } catch (error) {
209
215
  console.error('Error fetching data:', error);
210
- return { data: [], count: 0 };
216
+ return { data: [], count: 0, metadata: {} };
211
217
  }
212
218
  };
213
219
 
@@ -376,6 +382,11 @@ const DataGrid = forwardRef(
376
382
  }
377
383
 
378
384
  sqlResult.then((res) => {
385
+ // Update metadata when data is loaded
386
+ if (res.metadata) {
387
+ setColumnsMetadata(res.metadata);
388
+ }
389
+
379
390
  // Sort data by grouping field if grouping is enabled
380
391
  if (ajaxSetting?.groupBy?.length > 0) {
381
392
  const groupField = ajaxSetting.groupBy[0]; // groupBy is array of strings
@@ -433,13 +444,17 @@ const DataGrid = forwardRef(
433
444
  }, 10);
434
445
  });
435
446
 
436
- return sqlResult;
447
+ return sqlResult.then(res => ({
448
+ data: res.data,
449
+ count: res.count
450
+ }));
437
451
  },
438
452
  [ajaxSetting, dSearch, collapsedGroups]
439
453
  );
440
454
  const [filterDataSource, setFilterDataSource] = useState([]);
441
455
  const [filterValue, setFilterValue] = useState([]);
442
456
  const [gridColumns, setGridColumns] = useState([]);
457
+ const [columnsMetadata, setColumnsMetadata] = useState({});
443
458
  const gridRef = useRef(null);
444
459
  const [limit, setLimit] = useState(0);
445
460
  const [search, setSearch] = useState('');
@@ -2353,9 +2368,14 @@ const DataGrid = forwardRef(
2353
2368
  let columnStyle;
2354
2369
  let value;
2355
2370
 
2371
+ // Check if column is sortable based on metadata
2372
+ const columnKey = column.id;
2373
+ const sortable = isColumnSortable(columnKey, columnsMetadata);
2374
+
2356
2375
  const commonProps = {
2357
2376
  header: column.label,
2358
2377
  defaultFlex: 1,
2378
+ sortable: sortable,
2359
2379
  link: column.link ?? {},
2360
2380
  minWidth: column.minWidth ?? undefined,
2361
2381
  maxWidth: column.maxWidth ?? undefined,
@@ -2592,6 +2612,7 @@ const DataGrid = forwardRef(
2592
2612
  filterEditor,
2593
2613
  filterEditorProps,
2594
2614
  relationName,
2615
+ intelligentSortingConfig: tableSetting?.intelligentSorting || DEFAULT_INTELLIGENT_SORTING_CONFIG,
2595
2616
  });
2596
2617
  case 'relationArray':
2597
2618
  return renderRelationArrayColumn({
@@ -2600,6 +2621,7 @@ const DataGrid = forwardRef(
2600
2621
  filterEditor,
2601
2622
  filterEditorProps,
2602
2623
  relationName,
2624
+ intelligentSortingConfig: tableSetting?.intelligentSorting || DEFAULT_INTELLIGENT_SORTING_CONFIG,
2603
2625
  });
2604
2626
  case 'richtext':
2605
2627
  return renderRichTextColumn({
@@ -2833,7 +2855,7 @@ const DataGrid = forwardRef(
2833
2855
  .catch((error) => {
2834
2856
  console.error('Error in fetching dropdown data: ', error);
2835
2857
  });
2836
- }, [columns, filterDataSource]);
2858
+ }, [columns, filterDataSource, columnsMetadata]);
2837
2859
 
2838
2860
  useEffect(() => {
2839
2861
  if (setFilterData) {
@@ -1,6 +1,6 @@
1
1
  import '../styles/global.css';
2
2
 
3
- import React, { useEffect, useRef, useState } from 'react';
3
+ import React, { useEffect, useRef, useState, useMemo } from 'react';
4
4
  import DatePicker from 'react-datepicker';
5
5
  import _ from 'lodash';
6
6
  import Toggle from 'react-toggle';
@@ -189,22 +189,6 @@ function Field({
189
189
  const fetchOptions = () => {
190
190
  let filter = {};
191
191
 
192
- if (settings.where?.length > 0) {
193
- const processedWhere = settings.where.map((whereItem) => {
194
- if (whereItem.getKey && formData[whereItem.getKey]) {
195
- return {
196
- ...whereItem,
197
- value: formData[whereItem.getKey],
198
- };
199
- }
200
- return whereItem;
201
- });
202
-
203
- filter = {
204
- where: processedWhere,
205
- };
206
- }
207
-
208
192
  if (settings.fields?.length > 0) {
209
193
  filter.fields = settings.fields;
210
194
  }
@@ -261,7 +245,10 @@ function Field({
261
245
  case 'dropdown-ajax':
262
246
  case 'multi-dropdown-ajax':
263
247
  case 'radio-ajax':
264
- fetchOptions();
248
+ // Only fetch if there are no dependent fields (no 'where' conditions)
249
+ if (!settings.where || settings.where.length === 0) {
250
+ fetchOptions();
251
+ }
265
252
  break;
266
253
  default:
267
254
  break;
@@ -285,7 +272,66 @@ function Field({
285
272
  );
286
273
  setCanvasUrl(canvasType.url);
287
274
  }
288
- }, [settings, counter, formData]);
275
+ }, [settings, counter]);
276
+
277
+ // Get dependency field keys once
278
+ const dependencyKeys = useMemo(() => {
279
+ return settings.where?.map(whereItem => whereItem.getKey).filter(Boolean) || [];
280
+ }, [settings.where]);
281
+
282
+ // Separate useEffect for handling dependent form data changes for dropdown refetch
283
+ useEffect(() => {
284
+ if (['dropdown-ajax', 'multi-dropdown-ajax', 'radio-ajax'].includes(settings.type) && settings.where?.length > 0) {
285
+ let filter = {};
286
+
287
+ const processedWhere = settings.where.map((whereItem) => {
288
+ if (whereItem.getKey && formData[whereItem.getKey]) {
289
+ return {
290
+ ...whereItem,
291
+ value: formData[whereItem.getKey],
292
+ };
293
+ }
294
+ return whereItem;
295
+ });
296
+
297
+ filter = { where: processedWhere };
298
+
299
+ if (settings.fields?.length > 0) {
300
+ filter.fields = settings.fields;
301
+ }
302
+
303
+ if (settings.order && settings.order.sortBy && settings.order.sort) {
304
+ filter.sortBy = settings.order.sortBy;
305
+ filter.sort = settings.order.sort;
306
+ }
307
+
308
+ if (settings?.customParams) {
309
+ filter = { ...filter, ...settings.customParams };
310
+ }
311
+
312
+ CustomFetch(settings.url, 'POST', filter, function (result) {
313
+ setOptions(result.data);
314
+
315
+ let dataArray = [];
316
+ result.data.forEach((a) => {
317
+ let _tempObject = a;
318
+ if (!_.isEmpty(_tempObject)) {
319
+ let modifiedObject = {};
320
+ Object.keys(_tempObject).forEach(function (c) {
321
+ if (c !== 'id' && c !== 'label') {
322
+ modifiedObject['data-' + c] = _tempObject[c];
323
+ }
324
+ });
325
+ dataArray.push(modifiedObject);
326
+ }
327
+ });
328
+
329
+ if (dataArray.length > 0) {
330
+ setDataOptions(dataArray);
331
+ }
332
+ });
333
+ }
334
+ }, [...dependencyKeys.map(key => formData[key])]);
289
335
 
290
336
  const fetchChildDropdownData = (s, v) => {
291
337
  let filter = {};
@@ -2126,7 +2172,6 @@ function Field({
2126
2172
  </span>
2127
2173
  );
2128
2174
  }
2129
- break;
2130
2175
  case 'plaintextheading':
2131
2176
  return <h2>{settings.label ? settings.label : ''}</h2>;
2132
2177
  case 'section':
@@ -2143,7 +2188,7 @@ function Field({
2143
2188
  onEditorChange={(value, e) => {
2144
2189
  onChangeRicheditor(e, value, settings.id);
2145
2190
  }}
2146
- onInit={(evt, editor) => (editorRef.current = editor)}
2191
+ onInit={(_, editor) => (editorRef.current = editor)}
2147
2192
  value={inputValue || ''}
2148
2193
  init={{
2149
2194
  branding: false,
@@ -4,6 +4,11 @@ import parse from 'html-react-parser';
4
4
  import { Download, AlarmClock } from 'lucide-react';
5
5
  import { toast } from 'react-toastify';
6
6
  import CellWithTooltip from '../cells/CellWithTooltip';
7
+ import {
8
+ isRelationshipColumnSortable,
9
+ getRelationshipPath,
10
+ DEFAULT_INTELLIGENT_SORTING_CONFIG,
11
+ } from '../../../utils/relationshipSortingUtils';
7
12
 
8
13
  // Boolean Column Renderer
9
14
  export const renderBooleanColumn = ({
@@ -17,7 +22,7 @@ export const renderBooleanColumn = ({
17
22
  filterEditor: filterEditor,
18
23
  filterEditorProps: filterEditorProps,
19
24
  name: `${column.type}.${column.id}`,
20
- sortable: false,
25
+ sortable: true,
21
26
  render: ({ data }) => {
22
27
  const value = data[column.id] ? 'Yes' : 'No';
23
28
  return (
@@ -146,7 +151,7 @@ export const renderArrayCountColumn = ({ column, commonProps }) => {
146
151
  return {
147
152
  ...commonProps,
148
153
  name: `${column.type}.${column.id}`,
149
- sortable: false,
154
+ sortable: true,
150
155
  render: ({ data }) => {
151
156
  if (data && data[column.id]) {
152
157
  return data[column.id].length;
@@ -713,11 +718,39 @@ export const renderRelationColumn = ({
713
718
  filterEditor,
714
719
  filterEditorProps,
715
720
  relationName,
721
+ intelligentSortingConfig = DEFAULT_INTELLIGENT_SORTING_CONFIG,
716
722
  }) => {
723
+ // Intelligent sorting detection
724
+ const isSortable = isRelationshipColumnSortable(
725
+ column,
726
+ intelligentSortingConfig
727
+ );
728
+
729
+ // Log sorting analysis in development
730
+ if (
731
+ intelligentSortingConfig.logAnalysis &&
732
+ process.env.NODE_ENV === 'development'
733
+ ) {
734
+ const relationshipPath = getRelationshipPath(column);
735
+ console.log(
736
+ `Relationship sorting analysis for "${relationshipPath}":`,
737
+ {
738
+ sortable: isSortable,
739
+ fieldName: column.nameFrom,
740
+ relationshipDepth: Array.isArray(column.id)
741
+ ? column.id.length
742
+ : 1,
743
+ explicitSortable: column.hasOwnProperty('sortable')
744
+ ? column.sortable
745
+ : 'not specified',
746
+ }
747
+ );
748
+ }
749
+
717
750
  return {
718
751
  ...commonProps,
719
752
  name: relationName,
720
- sortable: false,
753
+ sortable: isSortable,
721
754
  filterEditor: filterEditor,
722
755
  filterEditorProps: filterEditorProps,
723
756
  render: ({ data }) => {
@@ -770,18 +803,22 @@ export const renderRelationColumn = ({
770
803
  // Check if the value is a hex color and render with color preview
771
804
  const isHexColor = (str) => {
772
805
  if (!str || typeof str !== 'string') return false;
773
- return /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(str);
806
+ return /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(
807
+ str
808
+ );
774
809
  };
775
810
 
776
811
  if (isHexColor(value)) {
777
812
  return (
778
813
  <div className="relation-color-container">
779
- <div
814
+ <div
780
815
  className="relation-color-preview"
781
816
  style={{ backgroundColor: value }}
782
817
  title={`Color: ${value}`}
783
818
  ></div>
784
- <span className="relation-color-text">{value}</span>
819
+ <span className="relation-color-text">
820
+ {value}
821
+ </span>
785
822
  </div>
786
823
  );
787
824
  }
@@ -812,11 +849,39 @@ export const renderRelationArrayColumn = ({
812
849
  filterEditor,
813
850
  filterEditorProps,
814
851
  relationName,
852
+ intelligentSortingConfig = DEFAULT_INTELLIGENT_SORTING_CONFIG,
815
853
  }) => {
854
+ // Intelligent sorting detection for relation arrays
855
+ const isSortable = isRelationshipColumnSortable(
856
+ column,
857
+ intelligentSortingConfig
858
+ );
859
+
860
+ // Log sorting analysis in development
861
+ if (
862
+ intelligentSortingConfig.logAnalysis &&
863
+ process.env.NODE_ENV === 'development'
864
+ ) {
865
+ const relationshipPath = getRelationshipPath(column);
866
+ console.log(
867
+ `Relationship array sorting analysis for "${relationshipPath}":`,
868
+ {
869
+ sortable: isSortable,
870
+ fieldName: column.nameFrom,
871
+ relationshipDepth: Array.isArray(column.id)
872
+ ? column.id.length
873
+ : 1,
874
+ explicitSortable: column.hasOwnProperty('sortable')
875
+ ? column.sortable
876
+ : 'not specified',
877
+ }
878
+ );
879
+ }
880
+
816
881
  return {
817
882
  ...commonProps,
818
883
  name: relationName,
819
- sortable: false,
884
+ sortable: isSortable,
820
885
  filterEditor: filterEditor,
821
886
  filterEditorProps: filterEditorProps,
822
887
  render: ({ data }) => {
@@ -1284,7 +1349,7 @@ export const renderCreatedByColumn = ({ column, commonProps }) => {
1284
1349
  return {
1285
1350
  ...commonProps,
1286
1351
  name: `${column.type}.${column.id}`,
1287
- sortable: false,
1352
+ sortable: true,
1288
1353
  render: ({ data }) => {
1289
1354
  if (
1290
1355
  data.audits[0] &&