infotable 1.4.1 → 1.6.0

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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +179 -11
  3. data/app/assets/javascripts/infotable/controllers/drawer_controller.js +74 -378
  4. data/app/assets/javascripts/infotable/controllers/table_controller.js +112 -120
  5. data/app/assets/javascripts/infotable/vendor/sortable.esm.js +3363 -0
  6. data/app/controllers/infotable/tables_controller.rb +16 -5
  7. data/app/helpers/infotable/tables_helper.rb +10 -48
  8. data/app/views/infotable/tables/_column_visibility.html.erb +4 -4
  9. data/app/views/infotable/tables/_end_indicator.html.erb +8 -0
  10. data/app/views/infotable/tables/_filter_drawer.html.erb +6 -6
  11. data/app/views/infotable/tables/_next_page_frame.html.erb +1 -9
  12. data/app/views/infotable/tables/_pagination.html.erb +4 -7
  13. data/app/views/infotable/tables/_table.html.erb +1 -1
  14. data/app/views/infotable/tables/_table_rows.html.erb +9 -2
  15. data/app/views/infotable/tables/filters/_boolean.html.erb +1 -1
  16. data/app/views/infotable/tables/filters/_range.html.erb +1 -1
  17. data/app/views/infotable/tables/filters/_select.html.erb +2 -2
  18. data/app/views/infotable/tables/filters/_text.html.erb +2 -2
  19. data/app/views/infotable/tables/show.html.erb +2 -2
  20. data/config/importmap.rb +2 -2
  21. data/config/locales/de.yml +126 -0
  22. data/config/locales/el.yml +126 -0
  23. data/config/locales/en.yml +18 -4
  24. data/config/locales/es.yml +126 -0
  25. data/config/locales/fr.yml +126 -0
  26. data/config/locales/it.yml +126 -0
  27. data/config/locales/pl.yml +18 -4
  28. data/exe/infotable-mcp +13 -0
  29. data/lib/generators/infotable/install/templates/initializer.rb +5 -3
  30. data/lib/generators/infotable/table/table_generator.rb +23 -1
  31. data/lib/generators/infotable/table/templates/table.rb.tt +8 -1
  32. data/lib/infotable/base.rb +62 -4
  33. data/lib/infotable/configuration.rb +1 -1
  34. data/lib/infotable/engine.rb +1 -8
  35. data/lib/infotable/filter_processor.rb +10 -1
  36. data/lib/infotable/formatters/boolean_formatter.rb +4 -4
  37. data/lib/infotable/formatters/currency_formatter.rb +4 -12
  38. data/lib/infotable/formatters/date_formatter.rb +3 -9
  39. data/lib/infotable/formatters/duration_formatter.rb +51 -0
  40. data/lib/infotable/formatters/filesize_formatter.rb +14 -0
  41. data/lib/infotable/formatters/image_formatter.rb +31 -0
  42. data/lib/infotable/formatters/number_formatter.rb +46 -0
  43. data/lib/infotable/formatters/tags_formatter.rb +44 -0
  44. data/lib/infotable/formatters/truncate_formatter.rb +19 -0
  45. data/lib/infotable/mcp/introspection.rb +94 -0
  46. data/lib/infotable/mcp/server.rb +26 -0
  47. data/lib/infotable/mcp/table_checker.rb +80 -0
  48. data/lib/infotable/mcp/table_writer.rb +66 -0
  49. data/lib/infotable/mcp/tools.rb +122 -0
  50. data/lib/infotable/mcp.rb +30 -0
  51. data/lib/infotable/table_resolver.rb +9 -0
  52. data/lib/infotable/version.rb +1 -1
  53. data/lib/infotable.rb +22 -0
  54. metadata +53 -4
@@ -267,7 +267,7 @@ export default class extends Controller {
267
267
  // Keep search and sort from the current frame URL
268
268
  if (tableFrame?.src) {
269
269
  const current = new URL(tableFrame.src, window.location.href).searchParams
270
- for (const key of ["search", "sort", "sort_column", "sort_direction"]) {
270
+ for (const key of ["search", "per_page", "sort", "sort_column", "sort_direction"]) {
271
271
  if (current.has(key) && !params.has(key)) params.set(key, current.get(key))
272
272
  }
273
273
  }
@@ -568,7 +568,7 @@ export default class extends Controller {
568
568
  this.saveColumnVisibility(tableName, columnName, isVisible)
569
569
 
570
570
  // Apply visibility to table
571
- this.applyColumnVisibility(tableName)
571
+ this.applyToTable(tableName)
572
572
 
573
573
  // Update badge
574
574
  this.updateColumnBadge(tableName)
@@ -591,7 +591,7 @@ export default class extends Controller {
591
591
  const columnName = checkbox.dataset.columnName
592
592
  this.saveColumnVisibility(tableName, columnName, true)
593
593
  })
594
- this.applyColumnVisibility(tableName)
594
+ this.applyToTable(tableName)
595
595
  this.updateColumnBadge(tableName)
596
596
 
597
597
  // Dispatch event to update export link
@@ -613,7 +613,7 @@ export default class extends Controller {
613
613
  const columnName = checkbox.dataset.columnName
614
614
  this.saveColumnVisibility(tableName, columnName, false)
615
615
  })
616
- this.applyColumnVisibility(tableName)
616
+ this.applyToTable(tableName)
617
617
  this.updateColumnBadge(tableName)
618
618
 
619
619
  // Dispatch event to update export link
@@ -638,7 +638,7 @@ export default class extends Controller {
638
638
  }
639
639
 
640
640
  visibility[columnName] = isVisible
641
- localStorage.setItem(key, JSON.stringify(visibility))
641
+ this.storeSet(key, JSON.stringify(visibility))
642
642
  }
643
643
 
644
644
  // Sync checkboxes with localStorage values or defaults
@@ -722,67 +722,12 @@ export default class extends Controller {
722
722
  }
723
723
  }
724
724
 
725
- // Apply column visibility to the table
726
- applyColumnVisibility(tableName) {
727
- const tableWrapper = document.querySelector(`[data-infotable--table-name-value="${tableName}"]`)
728
- if (!tableWrapper) {
729
- console.warn(`Table wrapper not found for: ${tableName}`)
730
- return
731
- }
732
-
733
- // Try to find table in the wrapper or in turbo-frame
734
- let table = tableWrapper.querySelector("table")
735
- if (!table) {
736
- // Table might be inside turbo-frame
737
- const turboFrame = document.getElementById(`${tableName}_content`)
738
- if (turboFrame) {
739
- table = turboFrame.querySelector("table")
740
- }
741
- }
742
-
743
- if (!table) {
744
- console.warn(`Table not found for: ${tableName}`)
745
- return
746
- }
747
-
748
- // Find all table headers and cells
749
- const headers = table.querySelectorAll("thead th[data-column-name]")
750
- const rows = table.querySelectorAll("tbody tr")
751
-
752
- if (headers.length === 0) {
753
- console.warn(`No column headers found for table: ${tableName}`)
754
- return
755
- }
756
-
757
- // Get saved preferences from localStorage
758
- const visibility = this.getColumnVisibility(tableName)
759
-
760
- headers.forEach((header) => {
761
- const columnName = header.dataset.columnName
762
- const defaultVisible = header.dataset.defaultVisible === "true"
763
-
764
- // Determine if column should be visible
765
- let shouldShow
766
- if (visibility && visibility.hasOwnProperty(columnName)) {
767
- // Use saved preference
768
- shouldShow = visibility[columnName]
769
- } else {
770
- // Use default visibility
771
- shouldShow = defaultVisible
772
- }
773
-
774
- // Apply visibility
775
- header.style.display = shouldShow ? "" : "none"
776
-
777
- // Hide/show corresponding cells in all rows
778
- rows.forEach(row => {
779
- const cells = row.querySelectorAll("td[data-column-name]")
780
- const cell = Array.from(cells).find(c => c.dataset.columnName === columnName)
781
- if (cell) {
782
- cell.style.display = shouldShow ? "" : "none"
783
- }
784
- })
785
- })
725
+ // The table controller applies order, visibility and pinning (it owns the table DOM); an explicit
726
+ // `order` replaces the stored one (used right after a drag, a pin or a reset)
727
+ applyToTable(tableName, order = null) {
728
+ document.dispatchEvent(new CustomEvent("infotable:apply-columns", {
729
+ detail: { tableName, order }
730
+ }))
786
731
  }
787
732
 
788
733
  // Update column badge count for a specific table
@@ -876,13 +821,10 @@ export default class extends Controller {
876
821
  const columnItems = list.querySelectorAll('.column-item')
877
822
  const newOrder = Array.from(columnItems).map(item => item.dataset.columnName)
878
823
 
879
- // Apply the new order to the table (reorders DOM)
880
- this.applyColumnOrder(tableName, newOrder)
824
+ // Order and sticky positioning, applied to the table immediately
825
+ this.applyToTable(tableName, newOrder)
881
826
  }
882
827
 
883
- // Apply sticky positioning to table immediately
884
- this.applyColumnPinningToTable(tableName)
885
-
886
828
  // Dispatch event for export link update
887
829
  document.dispatchEvent(new CustomEvent("infotable:columns-changed", {
888
830
  detail: { tableName }
@@ -903,40 +845,53 @@ export default class extends Controller {
903
845
  console.warn("Failed to parse column pinned from localStorage", e)
904
846
  }
905
847
 
906
- if (pinned) {
907
- pinnedData[columnName] = pinned
908
- } else {
909
- delete pinnedData[columnName]
910
- }
848
+ // An unpin is stored as null (not removed): a column pinned by default must stay unpinned
849
+ pinnedData[columnName] = pinned || null
911
850
 
912
- localStorage.setItem(key, JSON.stringify(pinnedData))
851
+ this.storeSet(key, JSON.stringify(pinnedData))
913
852
  }
914
853
 
915
- // Get pinned state from localStorage
916
- getColumnPinned(tableName, columnName) {
917
- const key = `infotable_column_pinned_${tableName}`
854
+ // localStorage is missing or full in some browsers (private mode): the drawer then works for this page only
855
+ storeSet(key, value) {
918
856
  try {
919
- const stored = localStorage.getItem(key)
920
- if (stored) {
921
- const pinnedData = JSON.parse(stored)
922
- return pinnedData[columnName] || null
923
- }
857
+ localStorage.setItem(key, value)
924
858
  } catch (e) {
925
- console.warn("Failed to parse column pinned from localStorage", e)
859
+ console.warn("Failed to write to localStorage", e)
860
+ }
861
+ }
862
+
863
+ storeRemove(key) {
864
+ try {
865
+ localStorage.removeItem(key)
866
+ } catch (e) {
867
+ console.warn("Failed to write to localStorage", e)
926
868
  }
927
- return null
928
869
  }
929
870
 
930
- // Get all pinned columns
871
+ // Get pinned state from localStorage
872
+ getColumnPinned(tableName, columnName) {
873
+ return this.getAllColumnsPinned(tableName)[columnName] || null
874
+ }
875
+
876
+ // Pinned columns: what the user chose (localStorage) over the table's `pinned:` defaults.
877
+ // A column the user unpinned is stored as null and stays unpinned.
931
878
  getAllColumnsPinned(tableName) {
932
879
  const key = `infotable_column_pinned_${tableName}`
880
+ let stored = {}
933
881
  try {
934
- const stored = localStorage.getItem(key)
935
- return stored ? JSON.parse(stored) : {}
882
+ const raw = localStorage.getItem(key)
883
+ if (raw) stored = JSON.parse(raw)
936
884
  } catch (e) {
937
885
  console.warn("Failed to parse column pinned from localStorage", e)
938
- return {}
939
886
  }
887
+
888
+ const pinned = {}
889
+ const form = document.getElementById(`column-visibility-form-${tableName}`)
890
+ form?.querySelectorAll('.column-item').forEach(item => {
891
+ const defaultPinned = item.dataset.defaultPinned
892
+ if (defaultPinned) pinned[item.dataset.columnName] = defaultPinned
893
+ })
894
+ return { ...pinned, ...stored }
940
895
  }
941
896
 
942
897
  // Update pin button styling
@@ -1001,202 +956,6 @@ export default class extends Controller {
1001
956
  this.saveColumnOrder(tableName, newOrder)
1002
957
  }
1003
958
 
1004
- // Apply pinning to table (add sticky positioning)
1005
- applyColumnPinningToTable(tableName) {
1006
- const tableWrapper = document.querySelector(`[data-infotable--table-name-value="${tableName}"]`)
1007
- if (!tableWrapper) return
1008
-
1009
- let table = tableWrapper.querySelector("table")
1010
- if (!table) {
1011
- const turboFrame = document.getElementById(`${tableName}_content`)
1012
- if (turboFrame) {
1013
- table = turboFrame.querySelector("table")
1014
- }
1015
- }
1016
-
1017
- if (!table) return
1018
-
1019
- const pinnedData = this.getAllColumnsPinned(tableName)
1020
- const headers = Array.from(table.querySelectorAll("thead th[data-column-name]"))
1021
- const rows = table.querySelectorAll("tbody tr")
1022
-
1023
- // Calculate positions for pinned columns
1024
- let leftOffset = 0
1025
- let rightOffset = 0
1026
-
1027
- // First pass: reset all columns
1028
- headers.forEach(header => {
1029
- header.style.position = ''
1030
- header.style.left = ''
1031
- header.style.right = ''
1032
- header.style.zIndex = ''
1033
- header.style.removeProperty('background-color')
1034
- header.style.removeProperty('background-image')
1035
- header.style.removeProperty('opacity')
1036
- header.classList.remove('infotable-pinned-left', 'infotable-pinned-right', '!bg-gray-50', 'dark:!bg-gray-800', "after:content-['']", "after:absolute", "after:top-0", "after:right-0", "after:bottom-0", "after:w-5", "after:pointer-events-none", "after:bg-gradient-to-r", "after:from-transparent", "after:to-black/[0.04]", "after:z-[1]", "before:content-['']", "before:absolute", "before:top-0", "before:left-0", "before:bottom-0", "before:w-5", "before:pointer-events-none", "before:bg-gradient-to-l", "before:from-transparent", "before:to-black/[0.04]", "before:z-[1]")
1037
- })
1038
-
1039
- rows.forEach(row => {
1040
- const cells = row.querySelectorAll("td[data-column-name]")
1041
- cells.forEach(cell => {
1042
- cell.style.position = ''
1043
- cell.style.left = ''
1044
- cell.style.right = ''
1045
- cell.style.zIndex = ''
1046
- cell.style.removeProperty('background-color')
1047
- cell.style.removeProperty('background-image')
1048
- cell.style.removeProperty('opacity')
1049
- cell.classList.remove('infotable-pinned-left', 'infotable-pinned-right', '!bg-white', 'dark:!bg-gray-900', "after:content-['']", "after:absolute", "after:top-0", "after:right-0", "after:bottom-0", "after:w-5", "after:pointer-events-none", "after:bg-gradient-to-r", "after:from-transparent", "after:to-black/[0.04]", "after:z-[1]", "before:content-['']", "before:absolute", "before:top-0", "before:left-0", "before:bottom-0", "before:w-5", "before:pointer-events-none", "before:bg-gradient-to-l", "before:from-transparent", "before:to-black/[0.04]", "before:z-[1]")
1050
- })
1051
- })
1052
-
1053
- // Find last left-pinned and first right-pinned columns
1054
- let lastLeftPinned = null
1055
- let firstRightPinned = null
1056
-
1057
- headers.forEach(header => {
1058
- const columnName = header.dataset.columnName
1059
- const pinned = pinnedData[columnName]
1060
- if (pinned === 'left') {
1061
- lastLeftPinned = columnName
1062
- }
1063
- if (pinned === 'right' && !firstRightPinned) {
1064
- firstRightPinned = columnName
1065
- }
1066
- })
1067
-
1068
- // Apply sticky positioning for pinned columns
1069
- headers.forEach(header => {
1070
- const columnName = header.dataset.columnName
1071
- const pinned = pinnedData[columnName]
1072
-
1073
- if (pinned === 'left') {
1074
- // Get existing inline styles
1075
- const existingStyles = header.getAttribute('style') || ''
1076
-
1077
- // Build comprehensive style string with all !important flags
1078
- const newStyles = `
1079
- position: sticky !important;
1080
- left: ${leftOffset}px !important;
1081
- z-index: 30 !important;
1082
- background: #f9fafb !important;
1083
- background-color: #f9fafb !important;
1084
- background-image: none !important;
1085
- background-position: initial !important;
1086
- background-size: initial !important;
1087
- background-repeat: initial !important;
1088
- background-attachment: initial !important;
1089
- opacity: 1 !important;
1090
- ${existingStyles}
1091
- `.replace(/\s+/g, ' ').trim()
1092
-
1093
- // Set all styles at once via setAttribute
1094
- header.setAttribute('style', newStyles)
1095
- header.classList.add('infotable-pinned-left', '!bg-gray-50', 'dark:!bg-gray-800')
1096
-
1097
- // Mark last left-pinned column
1098
- if (columnName === lastLeftPinned) {
1099
- header.classList.add("after:content-['']", "after:absolute", "after:top-0", "after:right-0", "after:bottom-0", "after:w-5", "after:pointer-events-none", "after:bg-gradient-to-r", "after:from-transparent", "after:to-black/[0.04]", "after:z-[1]")
1100
- }
1101
-
1102
- // Apply to cells in all rows
1103
- rows.forEach(row => {
1104
- const cell = row.querySelector(`td[data-column-name="${columnName}"]`)
1105
- if (cell) {
1106
- // Get existing inline styles
1107
- const existingStyles = cell.getAttribute('style') || ''
1108
-
1109
- // Build comprehensive style string
1110
- const newStyles = `
1111
- position: sticky !important;
1112
- left: ${leftOffset}px !important;
1113
- z-index: 15 !important;
1114
- background: #ffffff !important;
1115
- background-color: #ffffff !important;
1116
- background-image: none !important;
1117
- background-position: initial !important;
1118
- background-size: initial !important;
1119
- background-repeat: initial !important;
1120
- background-attachment: initial !important;
1121
- opacity: 1 !important;
1122
- ${existingStyles}
1123
- `.replace(/\s+/g, ' ').trim()
1124
-
1125
- cell.setAttribute('style', newStyles)
1126
- cell.classList.add('infotable-pinned-left', '!bg-white', 'dark:!bg-gray-900')
1127
-
1128
- if (columnName === lastLeftPinned) {
1129
- cell.classList.add("after:content-['']", "after:absolute", "after:top-0", "after:right-0", "after:bottom-0", "after:w-5", "after:pointer-events-none", "after:bg-gradient-to-r", "after:from-transparent", "after:to-black/[0.04]", "after:z-[1]")
1130
- }
1131
- }
1132
- })
1133
-
1134
- leftOffset += header.offsetWidth
1135
- } else if (pinned === 'right') {
1136
- // Get existing inline styles
1137
- const existingStyles = header.getAttribute('style') || ''
1138
-
1139
- // Build comprehensive style string
1140
- const newStyles = `
1141
- position: sticky !important;
1142
- right: ${rightOffset}px !important;
1143
- z-index: 30 !important;
1144
- background: #f9fafb !important;
1145
- background-color: #f9fafb !important;
1146
- background-image: none !important;
1147
- background-position: initial !important;
1148
- background-size: initial !important;
1149
- background-repeat: initial !important;
1150
- background-attachment: initial !important;
1151
- opacity: 1 !important;
1152
- ${existingStyles}
1153
- `.replace(/\s+/g, ' ').trim()
1154
-
1155
- header.setAttribute('style', newStyles)
1156
- header.classList.add('infotable-pinned-right', '!bg-gray-50', 'dark:!bg-gray-800')
1157
-
1158
- // Mark first right-pinned column
1159
- if (columnName === firstRightPinned) {
1160
- header.classList.add("before:content-['']", "before:absolute", "before:top-0", "before:left-0", "before:bottom-0", "before:w-5", "before:pointer-events-none", "before:bg-gradient-to-l", "before:from-transparent", "before:to-black/[0.04]", "before:z-[1]")
1161
- }
1162
-
1163
- // Apply to cells in all rows
1164
- rows.forEach(row => {
1165
- const cell = row.querySelector(`td[data-column-name="${columnName}"]`)
1166
- if (cell) {
1167
- // Get existing inline styles
1168
- const existingStyles = cell.getAttribute('style') || ''
1169
-
1170
- // Build comprehensive style string
1171
- const newStyles = `
1172
- position: sticky !important;
1173
- right: ${rightOffset}px !important;
1174
- z-index: 15 !important;
1175
- background: #ffffff !important;
1176
- background-color: #ffffff !important;
1177
- background-image: none !important;
1178
- background-position: initial !important;
1179
- background-size: initial !important;
1180
- background-repeat: initial !important;
1181
- background-attachment: initial !important;
1182
- opacity: 1 !important;
1183
- ${existingStyles}
1184
- `.replace(/\s+/g, ' ').trim()
1185
-
1186
- cell.setAttribute('style', newStyles)
1187
- cell.classList.add('infotable-pinned-right', '!bg-white', 'dark:!bg-gray-900')
1188
-
1189
- if (columnName === firstRightPinned) {
1190
- cell.classList.add("before:content-['']", "before:absolute", "before:top-0", "before:left-0", "before:bottom-0", "before:w-5", "before:pointer-events-none", "before:bg-gradient-to-l", "before:from-transparent", "before:to-black/[0.04]", "before:z-[1]")
1191
- }
1192
- }
1193
- })
1194
-
1195
- rightOffset += header.offsetWidth
1196
- }
1197
- })
1198
- }
1199
-
1200
959
  // ========== Column Ordering with Drag and Drop ==========
1201
960
 
1202
961
  // Initialize Sortable for drag and drop column ordering
@@ -1217,7 +976,7 @@ export default class extends Controller {
1217
976
  ghostClass: 'opacity-50',
1218
977
  dragClass: 'shadow-lg',
1219
978
  onEnd: (evt) => {
1220
- this.handleColumnReorder(tableName, list)
979
+ this.handleColumnReorder(tableName, list, evt.item)
1221
980
  }
1222
981
  })
1223
982
 
@@ -1229,29 +988,29 @@ export default class extends Controller {
1229
988
  })
1230
989
  }
1231
990
 
1232
- // Handle column reorder after drag and drop
1233
- handleColumnReorder(tableName, list) {
1234
- // Get new order from DOM
1235
- const columnItems = list.querySelectorAll('.column-item')
1236
- const newOrder = Array.from(columnItems).map(item => item.dataset.columnName)
991
+ // Handle column reorder after drag and drop.
992
+ // Pins survive a reorder; only the dragged column can change: dropped among the left pins it pins
993
+ // left, among the right pins it pins right, anywhere else it is unpinned.
994
+ handleColumnReorder(tableName, list, dragged) {
995
+ const items = Array.from(list.querySelectorAll('.column-item'))
996
+ const pinnedData = this.getAllColumnsPinned(tableName)
1237
997
 
1238
- // Unpin all columns when manually reordering
1239
- const pinnedKey = `infotable_column_pinned_${tableName}`
1240
- localStorage.removeItem(pinnedKey)
998
+ if (dragged) {
999
+ const at = items.indexOf(dragged)
1000
+ const nextPin = pinnedData[items[at + 1]?.dataset.columnName]
1001
+ const prevPin = pinnedData[items[at - 1]?.dataset.columnName]
1002
+ const pinned = nextPin === 'left' ? 'left' : (prevPin === 'right' ? 'right' : null)
1241
1003
 
1242
- // Update pin button styling for all items
1243
- columnItems.forEach(item => {
1244
- this.updatePinButtons(item, null)
1245
- })
1246
-
1247
- // Save to localStorage
1248
- this.saveColumnOrder(tableName, newOrder)
1004
+ this.saveColumnPinned(tableName, dragged.dataset.columnName, pinned)
1005
+ this.updatePinButtons(dragged, pinned)
1006
+ }
1249
1007
 
1250
- // Apply order to table
1251
- this.applyColumnOrder(tableName, newOrder)
1008
+ // Keep the list as left pins, the rest, right pins (this also saves the order)
1009
+ this.reorderColumnsWithPinning(tableName)
1010
+ const newOrder = Array.from(list.querySelectorAll('.column-item')).map(item => item.dataset.columnName)
1252
1011
 
1253
- // Remove sticky positioning since all are unpinned
1254
- this.applyColumnPinningToTable(tableName)
1012
+ // Apply order and pins to the table
1013
+ this.applyToTable(tableName, newOrder)
1255
1014
 
1256
1015
  // Dispatch event for export link update
1257
1016
  document.dispatchEvent(new CustomEvent("infotable:columns-changed", {
@@ -1262,7 +1021,7 @@ export default class extends Controller {
1262
1021
  // Save column order to localStorage
1263
1022
  saveColumnOrder(tableName, order) {
1264
1023
  const key = `infotable_column_order_${tableName}`
1265
- localStorage.setItem(key, JSON.stringify(order))
1024
+ this.storeSet(key, JSON.stringify(order))
1266
1025
  }
1267
1026
 
1268
1027
  // Load column order from localStorage
@@ -1298,7 +1057,7 @@ export default class extends Controller {
1298
1057
 
1299
1058
  // Valid schema, apply the order
1300
1059
  this.applyColumnOrderToList(tableName, storedOrder)
1301
- this.applyColumnOrder(tableName, storedOrder)
1060
+ this.applyToTable(tableName, storedOrder)
1302
1061
  }
1303
1062
 
1304
1063
  // Check if stored order is valid against default schema
@@ -1352,64 +1111,10 @@ export default class extends Controller {
1352
1111
  })
1353
1112
  }
1354
1113
 
1355
- // Apply column order to the table
1356
- applyColumnOrder(tableName, order) {
1357
- const tableWrapper = document.querySelector(`[data-infotable--table-name-value="${tableName}"]`)
1358
- if (!tableWrapper) return
1359
-
1360
- // Try to find table in the wrapper or in turbo-frame
1361
- let table = tableWrapper.querySelector("table")
1362
- if (!table) {
1363
- const turboFrame = document.getElementById(`${tableName}_content`)
1364
- if (turboFrame) {
1365
- table = turboFrame.querySelector("table")
1366
- }
1367
- }
1368
-
1369
- if (!table) return
1370
-
1371
- // Get headers and create a map
1372
- const headerRow = table.querySelector("thead tr")
1373
- if (!headerRow) return
1374
-
1375
- const headers = Array.from(headerRow.querySelectorAll("th[data-column-name]"))
1376
- const headerMap = new Map()
1377
-
1378
- headers.forEach((header, index) => {
1379
- headerMap.set(header.dataset.columnName, { element: header, originalIndex: index })
1380
- })
1381
-
1382
- // Reorder headers according to order array
1383
- order.forEach(columnName => {
1384
- const headerInfo = headerMap.get(columnName)
1385
- if (headerInfo) {
1386
- headerRow.appendChild(headerInfo.element)
1387
- }
1388
- })
1389
-
1390
- // Reorder cells in each row
1391
- const rows = table.querySelectorAll("tbody tr")
1392
- rows.forEach(row => {
1393
- const cells = Array.from(row.querySelectorAll("td[data-column-name]"))
1394
- const cellMap = new Map()
1395
-
1396
- cells.forEach(cell => {
1397
- cellMap.set(cell.dataset.columnName, cell)
1398
- })
1399
-
1400
- order.forEach(columnName => {
1401
- const cell = cellMap.get(columnName)
1402
- if (cell) {
1403
- row.appendChild(cell)
1404
- }
1405
- })
1406
- })
1407
- }
1408
-
1409
1114
  // Clear column order from localStorage
1410
1115
  clearColumnOrder(tableName) {
1411
1116
  const key = `infotable_column_order_${tableName}`
1412
- localStorage.removeItem(key)
1117
+ this.storeRemove(key)
1413
1118
  }
1414
1119
 
1415
1120
  // Reset to defaults - clear all custom settings
@@ -1421,14 +1126,14 @@ export default class extends Controller {
1421
1126
 
1422
1127
  // Clear column visibility
1423
1128
  const visibilityKey = `infotable_columns_${tableName}`
1424
- localStorage.removeItem(visibilityKey)
1129
+ this.storeRemove(visibilityKey)
1425
1130
 
1426
1131
  // Clear column order
1427
1132
  this.clearColumnOrder(tableName)
1428
1133
 
1429
1134
  // Clear column pinning
1430
1135
  const pinnedKey = `infotable_column_pinned_${tableName}`
1431
- localStorage.removeItem(pinnedKey)
1136
+ this.storeRemove(pinnedKey)
1432
1137
 
1433
1138
  // Reset checkboxes to defaults
1434
1139
  const form = document.getElementById(`column-visibility-form-${tableName}`)
@@ -1452,19 +1157,10 @@ export default class extends Controller {
1452
1157
  this.applyColumnOrderToList(tableName, schemaNames)
1453
1158
  }
1454
1159
 
1455
- // Apply defaults to table
1456
- this.applyColumnVisibility(tableName)
1457
-
1458
- // Apply default column order to table
1160
+ // Apply the defaults (visibility, default order, default pinning) to the table
1459
1161
  const list = Array.from(this.columnListTargets).find(l => l.dataset.tableName === tableName)
1460
- if (list) {
1461
- const defaultSchema = JSON.parse(list.dataset.columnsSchema || '[]')
1462
- const schemaNames = defaultSchema.map(item => typeof item === 'string' ? item : item.name)
1463
- this.applyColumnOrder(tableName, schemaNames)
1464
- }
1465
-
1466
- // Remove sticky positioning
1467
- this.applyColumnPinningToTable(tableName)
1162
+ const defaultSchema = JSON.parse(list?.dataset.columnsSchema || '[]')
1163
+ this.applyToTable(tableName, list ? defaultSchema.map(item => typeof item === 'string' ? item : item.name) : null)
1468
1164
 
1469
1165
  // Update badge
1470
1166
  this.updateColumnBadge(tableName)