infotable 1.4.1 → 1.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f00cd7591b4bc03f8f7443771dc1be2abc2e022602a2a2b14a61090d6eea76aa
4
- data.tar.gz: 1c48ace8adb272674a1b0138b7aa1744dfe5154695b87ea5e1773cc918d912fc
3
+ metadata.gz: a1a84d197b721ed57ad46be7d91a870d0f7d0d3c6a7bc471e4e1c306aefa9e1f
4
+ data.tar.gz: 19c3fd871c6e5bee6908266fe877774786b8b2e7d7fa289117b05e65684016e2
5
5
  SHA512:
6
- metadata.gz: 9ed45ecaf4c41b844dd6a7df34e35bcecefe593fa146fbe352ec9c6e4b20501f7661464a383f6a6ae386190e13d6a4cb395765c6ea4450f1d2637f442256c148
7
- data.tar.gz: 79fae3ddc36f3c98c826eb18fcb9856f810e5d8fd2681eb9244ba3577efa7a397b384eb8bb170c791cd1668c5c5f5e444f546553d55cffb0cd099813555366bd
6
+ metadata.gz: b8bcbd636497943daa6fb58cec7a8550f4258c14a7a65a771d10c1f4e2776bed51fe6b8ad2137126131b353fedd91076e6d017697b0f405b3694d257bd436098
7
+ data.tar.gz: c6a4f20ebe7642c1d8d686be6e8fee8226f1076ee93b2daa5de79f0b7a630f17d264eb64cc067d164fb1de5033f1c08c059ca1e46a7b9d5b7666298f7d5b5a47
data/README.md CHANGED
@@ -129,6 +129,15 @@ Infotable.configure do |config|
129
129
  end
130
130
  ```
131
131
 
132
+ ### Security
133
+
134
+ Tables are served by the engine at `/infotable/tables/:name/{data,export,ids}`. A table without an
135
+ `authorize { |controller| ... }` block is readable (and exportable as CSV) by **everyone who can reach the
136
+ route**, unless the engine controllers inherit your app's authentication. The installer therefore sets
137
+ `config.parent_controller = "ApplicationController"`; keep it (or point it at a controller with your
138
+ `before_action :authenticate_user!`) and add `authorize` to tables with restricted data. The engine's own
139
+ default (`ActionController::Base`) applies no authentication at all.
140
+
132
141
  ### Available Global Options
133
142
 
134
143
  | Option | Type | Default | Description |
@@ -137,7 +146,8 @@ end
137
146
  | `per_page_options` | Array | `[10, 25, 50, 100]` | Available rows per page options |
138
147
  | `drawer_position` | Symbol | `:right` | Position of filter/column drawer (`:left` or `:right`) |
139
148
  | `drawer_width` | String | `"26rem"` | Width of the drawer (`px`, `rem`, `em`, `vw` or `%`; anything else falls back to the default) |
140
- | `parent_controller` | String | `"ActionController::Base"` | Controller the engine's controllers inherit from. Set it to `"ApplicationController"` to reuse your authentication |
149
+ | `parent_controller` | String | `"ActionController::Base"` (the installer sets `"ApplicationController"`) | Controller the engine's controllers inherit from. Set it to `"ApplicationController"` to reuse your authentication |
150
+ | `max_select_all` | Integer, nil | `4000` | "Select all N matching" collects at most this many ids (each is sent as `ids[]`; Rack accepts 4096 parameters per request) |
141
151
  | `table_min_height` | String | `"400px"` | Minimum table height |
142
152
  | `table_max_height` | String | `"600px"` | Maximum table height (enables scrolling) |
143
153
  | `autofilter` | Boolean | `true` | Auto-apply filters on change (debounced) vs manual apply |
@@ -448,6 +458,30 @@ Features:
448
458
  - Cmd/Ctrl+Click to open in new tab
449
459
  - Ignores clicks on buttons, links, and inputs
450
460
 
461
+ #### Row click, classes and data attributes
462
+
463
+ ```ruby
464
+ # Style rows by their data (write the Tailwind classes out in full so they are generated)
465
+ row_class ->(order) { "bg-red-50" if order.overdue? }
466
+
467
+ # data-* attributes on the <tr>, e.g. for your own Stimulus controller
468
+ row_data ->(post) { { post_id: post.id, status: post.status } } # data-post-id, data-status
469
+
470
+ # Rows fire a cancelable `infotable:row-click` event (needs no row_link)
471
+ row_click
472
+ ```
473
+
474
+ Every row also gets `id="<dom_id>"` (`order_42`), so Turbo Streams can target it (`turbo_stream.replace dom_id(order)`, `broadcasts_to`). Rows without a record id are left without one.
475
+
476
+ Clicking a row (not a link, button or input inside it) first dispatches `infotable:row-click` on the `<tr>`, with `detail: { id, url, data, originalEvent }`. If nothing calls `preventDefault()` and the table has a `row_link`, the click then falls back to following it, so the two compose:
477
+
478
+ ```js
479
+ document.addEventListener("infotable:row-click", event => {
480
+ if (event.detail.data.status === "locked") event.preventDefault() // no navigation for locked rows
481
+ else openPreview(event.detail.data.postId) // ...or handle it yourself
482
+ })
483
+ ```
484
+
451
485
  #### Row Size
452
486
 
453
487
  Control table row padding and text size with three options:
@@ -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
@@ -1237,7 +996,9 @@ export default class extends Controller {
1237
996
 
1238
997
  // Unpin all columns when manually reordering
1239
998
  const pinnedKey = `infotable_column_pinned_${tableName}`
1240
- localStorage.removeItem(pinnedKey)
999
+ const unpinned = {}
1000
+ columnItems.forEach(item => { unpinned[item.dataset.columnName] = null })
1001
+ this.storeSet(pinnedKey, JSON.stringify(unpinned))
1241
1002
 
1242
1003
  // Update pin button styling for all items
1243
1004
  columnItems.forEach(item => {
@@ -1247,11 +1008,8 @@ export default class extends Controller {
1247
1008
  // Save to localStorage
1248
1009
  this.saveColumnOrder(tableName, newOrder)
1249
1010
 
1250
- // Apply order to table
1251
- this.applyColumnOrder(tableName, newOrder)
1252
-
1253
- // Remove sticky positioning since all are unpinned
1254
- this.applyColumnPinningToTable(tableName)
1011
+ // Apply order to the table; nothing is pinned any more
1012
+ this.applyToTable(tableName, newOrder)
1255
1013
 
1256
1014
  // Dispatch event for export link update
1257
1015
  document.dispatchEvent(new CustomEvent("infotable:columns-changed", {
@@ -1262,7 +1020,7 @@ export default class extends Controller {
1262
1020
  // Save column order to localStorage
1263
1021
  saveColumnOrder(tableName, order) {
1264
1022
  const key = `infotable_column_order_${tableName}`
1265
- localStorage.setItem(key, JSON.stringify(order))
1023
+ this.storeSet(key, JSON.stringify(order))
1266
1024
  }
1267
1025
 
1268
1026
  // Load column order from localStorage
@@ -1298,7 +1056,7 @@ export default class extends Controller {
1298
1056
 
1299
1057
  // Valid schema, apply the order
1300
1058
  this.applyColumnOrderToList(tableName, storedOrder)
1301
- this.applyColumnOrder(tableName, storedOrder)
1059
+ this.applyToTable(tableName, storedOrder)
1302
1060
  }
1303
1061
 
1304
1062
  // Check if stored order is valid against default schema
@@ -1352,64 +1110,10 @@ export default class extends Controller {
1352
1110
  })
1353
1111
  }
1354
1112
 
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
1113
  // Clear column order from localStorage
1410
1114
  clearColumnOrder(tableName) {
1411
1115
  const key = `infotable_column_order_${tableName}`
1412
- localStorage.removeItem(key)
1116
+ this.storeRemove(key)
1413
1117
  }
1414
1118
 
1415
1119
  // Reset to defaults - clear all custom settings
@@ -1421,14 +1125,14 @@ export default class extends Controller {
1421
1125
 
1422
1126
  // Clear column visibility
1423
1127
  const visibilityKey = `infotable_columns_${tableName}`
1424
- localStorage.removeItem(visibilityKey)
1128
+ this.storeRemove(visibilityKey)
1425
1129
 
1426
1130
  // Clear column order
1427
1131
  this.clearColumnOrder(tableName)
1428
1132
 
1429
1133
  // Clear column pinning
1430
1134
  const pinnedKey = `infotable_column_pinned_${tableName}`
1431
- localStorage.removeItem(pinnedKey)
1135
+ this.storeRemove(pinnedKey)
1432
1136
 
1433
1137
  // Reset checkboxes to defaults
1434
1138
  const form = document.getElementById(`column-visibility-form-${tableName}`)
@@ -1452,19 +1156,10 @@ export default class extends Controller {
1452
1156
  this.applyColumnOrderToList(tableName, schemaNames)
1453
1157
  }
1454
1158
 
1455
- // Apply defaults to table
1456
- this.applyColumnVisibility(tableName)
1457
-
1458
- // Apply default column order to table
1159
+ // Apply the defaults (visibility, default order, default pinning) to the table
1459
1160
  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)
1161
+ const defaultSchema = JSON.parse(list?.dataset.columnsSchema || '[]')
1162
+ this.applyToTable(tableName, list ? defaultSchema.map(item => typeof item === 'string' ? item : item.name) : null)
1468
1163
 
1469
1164
  // Update badge
1470
1165
  this.updateColumnBadge(tableName)