chordia-ui 4.0.7 → 4.0.8

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.
@@ -18,7 +18,7 @@ import {
18
18
  horizontalListSortingStrategy,
19
19
  } from "@dnd-kit/sortable";
20
20
  import { CSS } from "@dnd-kit/utilities";
21
- import { GripVertical, ChevronUp, ChevronDown, ListFilter, ArrowUp, ArrowDown, Settings2, Search, Check, X, ArrowUpDown } from "lucide-react";
21
+ import { GripVertical, ChevronUp, ChevronDown, ListFilter, ArrowUp, ArrowDown, Settings2, Search, Check, X, ArrowUpDown, Minus } from "lucide-react";
22
22
  import { createPortal } from "react-dom";
23
23
  import Pagination from "../common/Pagination.jsx";
24
24
  // TODO: surface column limit errors via callback prop (onMaxColumnsError)
@@ -1189,6 +1189,16 @@ export default function DataTable2({
1189
1189
  // surrounding toolbar wrapper is also skipped if no other toolbar content
1190
1190
  // (e.g. `filtersConfig`) is configured.
1191
1191
  hideColumnPicker = false,
1192
+ // Row selection (opt-in). When `selectable` is true a leading checkbox
1193
+ // column is rendered outside the reorderable/persisted column system, so it
1194
+ // is always visible and unaffected by the column picker or saved prefs.
1195
+ // Selection is controlled by the host: pass `selectedRowIds` (array of ids)
1196
+ // and handle `onSelectionChange(nextIdsArray)`. `getRowId` resolves a row's
1197
+ // id (defaults to `row.id`).
1198
+ selectable = false,
1199
+ selectedRowIds = null,
1200
+ onSelectionChange = null,
1201
+ getRowId = null,
1192
1202
  }) {
1193
1203
  // Get user email for user-specific storage
1194
1204
  const { userData } = useUserContext();
@@ -1864,6 +1874,96 @@ export default function DataTable2({
1864
1874
  }
1865
1875
  }, [sortedData, page, pageSize, isServerSidePagination]);
1866
1876
 
1877
+ // ---- Row selection (opt-in) --------------------------------------------
1878
+ const resolveRowId = useCallback(
1879
+ (row, index) => {
1880
+ if (getRowId) return getRowId(row, index);
1881
+ return row?.id ?? index;
1882
+ },
1883
+ [getRowId]
1884
+ );
1885
+
1886
+ const selectedIdSet = useMemo(
1887
+ () => new Set(Array.isArray(selectedRowIds) ? selectedRowIds : []),
1888
+ [selectedRowIds]
1889
+ );
1890
+
1891
+ const pageRowIds = useMemo(
1892
+ () => paginatedData.map((row, index) => resolveRowId(row, index)),
1893
+ [paginatedData, resolveRowId]
1894
+ );
1895
+
1896
+ const selectedOnPageCount = useMemo(
1897
+ () => pageRowIds.reduce((count, id) => (selectedIdSet.has(id) ? count + 1 : count), 0),
1898
+ [pageRowIds, selectedIdSet]
1899
+ );
1900
+
1901
+ const allOnPageSelected = pageRowIds.length > 0 && selectedOnPageCount === pageRowIds.length;
1902
+ const someOnPageSelected = selectedOnPageCount > 0 && !allOnPageSelected;
1903
+
1904
+ const toggleRowSelection = useCallback(
1905
+ (rowId) => {
1906
+ if (!onSelectionChange) return;
1907
+ const next = new Set(selectedIdSet);
1908
+ if (next.has(rowId)) next.delete(rowId);
1909
+ else next.add(rowId);
1910
+ onSelectionChange(Array.from(next));
1911
+ },
1912
+ [onSelectionChange, selectedIdSet]
1913
+ );
1914
+
1915
+ const toggleSelectAllOnPage = useCallback(() => {
1916
+ if (!onSelectionChange) return;
1917
+ const next = new Set(selectedIdSet);
1918
+ if (allOnPageSelected) {
1919
+ pageRowIds.forEach((id) => next.delete(id));
1920
+ } else {
1921
+ pageRowIds.forEach((id) => next.add(id));
1922
+ }
1923
+ onSelectionChange(Array.from(next));
1924
+ }, [onSelectionChange, selectedIdSet, allOnPageSelected, pageRowIds]);
1925
+
1926
+ // Shared checkbox visual for row selection.
1927
+ const SelectionCheckbox = ({ checked, indeterminate = false, onToggle, ariaLabel }) => (
1928
+ <span
1929
+ role="checkbox"
1930
+ aria-checked={indeterminate ? "mixed" : checked}
1931
+ aria-label={ariaLabel}
1932
+ tabIndex={0}
1933
+ onClick={(e) => {
1934
+ e.stopPropagation();
1935
+ onToggle();
1936
+ }}
1937
+ onKeyDown={(e) => {
1938
+ if (e.key === " " || e.key === "Enter") {
1939
+ e.preventDefault();
1940
+ e.stopPropagation();
1941
+ onToggle();
1942
+ }
1943
+ }}
1944
+ style={{
1945
+ width: 16,
1946
+ height: 16,
1947
+ borderRadius: 5,
1948
+ display: "inline-flex",
1949
+ alignItems: "center",
1950
+ justifyContent: "center",
1951
+ background: checked || indeterminate ? "#0B0B0B" : "#FFFFFF",
1952
+ border: `1px solid ${checked || indeterminate ? "#0B0B0B" : "#676767"}`,
1953
+ cursor: "pointer",
1954
+ flexShrink: 0,
1955
+ }}
1956
+ >
1957
+ {indeterminate ? (
1958
+ <Minus size={11} color="#FFFFFF" strokeWidth={3} />
1959
+ ) : checked ? (
1960
+ <Check size={11} color="#FFFFFF" strokeWidth={3} />
1961
+ ) : null}
1962
+ </span>
1963
+ );
1964
+
1965
+ const SELECT_COLUMN_WIDTH = 44;
1966
+
1867
1967
  // Handle header click for sorting
1868
1968
  const handleSort = (field) => {
1869
1969
  if (isServerSideSorting && onSort) {
@@ -2804,6 +2904,14 @@ export default function DataTable2({
2804
2904
  </div>
2805
2905
  )}
2806
2906
 
2907
+ {/* Custom actions (e.g. bulk-update buttons) — kept left of the
2908
+ Columns picker so Columns stays the right-most control. */}
2909
+ {filtersConfig?.rightActions && (
2910
+ <div style={{ display: "flex", alignItems: "center", gap: 12, flexShrink: 0 }}>
2911
+ {filtersConfig.rightActions}
2912
+ </div>
2913
+ )}
2914
+
2807
2915
  {/* Columns button — compact */}
2808
2916
  {!hideColumnPicker && (
2809
2917
  <div style={{ position: "relative" }}>
@@ -3038,6 +3146,33 @@ export default function DataTable2({
3038
3146
  <thead>
3039
3147
  {/* Header row with column labels */}
3040
3148
  <tr>
3149
+ {selectable && (
3150
+ <th
3151
+ style={{
3152
+ width: SELECT_COLUMN_WIDTH,
3153
+ minWidth: SELECT_COLUMN_WIDTH,
3154
+ padding: '10px 8px',
3155
+ textAlign: 'center',
3156
+ verticalAlign: 'middle',
3157
+ background: '#f2f2f0',
3158
+ borderRight: '1px solid #e6e6e6',
3159
+ borderBottom: '1px solid #e6e6e6',
3160
+ position: 'sticky',
3161
+ top: 0,
3162
+ zIndex: 10,
3163
+ boxSizing: 'border-box',
3164
+ }}
3165
+ >
3166
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', lineHeight: 0 }}>
3167
+ <SelectionCheckbox
3168
+ checked={allOnPageSelected}
3169
+ indeterminate={someOnPageSelected}
3170
+ onToggle={toggleSelectAllOnPage}
3171
+ ariaLabel="Select all rows on this page"
3172
+ />
3173
+ </div>
3174
+ </th>
3175
+ )}
3041
3176
  <SortableContext
3042
3177
  items={visibleColumns}
3043
3178
  strategy={horizontalListSortingStrategy}
@@ -3052,7 +3187,7 @@ export default function DataTable2({
3052
3187
  {!isLoading && paginatedData.length === 0 ? (
3053
3188
  <tr>
3054
3189
  <td
3055
- colSpan={orderedColumns.length}
3190
+ colSpan={orderedColumns.length + (selectable ? 1 : 0)}
3056
3191
  style={{
3057
3192
  padding: '32px',
3058
3193
  textAlign: 'center',
@@ -3065,14 +3200,18 @@ export default function DataTable2({
3065
3200
  </td>
3066
3201
  </tr>
3067
3202
  ) : !isLoading && paginatedData.length > 0 ? (
3068
- paginatedData.map((row, rowIndex) => (
3203
+ paginatedData.map((row, rowIndex) => {
3204
+ const rowId = resolveRowId(row, rowIndex);
3205
+ const isRowSelected = selectable && selectedIdSet.has(rowId);
3206
+ return (
3069
3207
  <tr
3070
3208
  key={rowIndex}
3071
3209
  data-row-id={row.id || rowIndex}
3072
3210
  style={{
3073
3211
  borderBottom: '1px solid #e6e6e6',
3074
3212
  cursor: onRowClick ? 'pointer' : 'default',
3075
- transition: 'background 0.15s ease'
3213
+ transition: 'background 0.15s ease',
3214
+ background: isRowSelected ? '#F2F4F7' : 'transparent',
3076
3215
  }}
3077
3216
  onClick={onRowClick ? (e) => {
3078
3217
  e.stopPropagation();
@@ -3082,9 +3221,31 @@ export default function DataTable2({
3082
3221
  e.currentTarget.style.background = '#E6E6E6';
3083
3222
  }}
3084
3223
  onMouseLeave={(e) => {
3085
- e.currentTarget.style.background = 'transparent';
3224
+ e.currentTarget.style.background = isRowSelected ? '#F2F4F7' : 'transparent';
3086
3225
  }}
3087
3226
  >
3227
+ {selectable && (
3228
+ <td
3229
+ onClick={(e) => e.stopPropagation()}
3230
+ style={{
3231
+ width: SELECT_COLUMN_WIDTH,
3232
+ minWidth: SELECT_COLUMN_WIDTH,
3233
+ padding: '10px 8px',
3234
+ textAlign: 'center',
3235
+ verticalAlign: 'middle',
3236
+ borderRight: '1px solid #e6e6e6',
3237
+ boxSizing: 'border-box',
3238
+ }}
3239
+ >
3240
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', lineHeight: 0 }}>
3241
+ <SelectionCheckbox
3242
+ checked={isRowSelected}
3243
+ onToggle={() => toggleRowSelection(rowId)}
3244
+ ariaLabel="Select row"
3245
+ />
3246
+ </div>
3247
+ </td>
3248
+ )}
3088
3249
  {orderedColumns.map((column) => {
3089
3250
  const columnId = getColumnId(column);
3090
3251
  const resolvedWidth = getResolvedColumnWidth(columnId, column.width);
@@ -3123,7 +3284,8 @@ export default function DataTable2({
3123
3284
  );
3124
3285
  })}
3125
3286
  </tr>
3126
- ))
3287
+ );
3288
+ })
3127
3289
  ) : null}
3128
3290
  </tbody>
3129
3291
  </table>
@@ -327,7 +327,6 @@ export default function PoliciesPage({
327
327
  policy={policy}
328
328
  variant="browse"
329
329
  onViewDetails={() => openBrowseDetails(policy)}
330
- onApply={isAdmin ? () => openBrowseDetails(policy) : undefined}
331
330
  />
332
331
  ))}
333
332
  </CardGrid>