@prairielearn/ui 1.1.1 → 1.2.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 (40) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/components/CategoricalColumnFilter.js +1 -1
  3. package/dist/components/CategoricalColumnFilter.js.map +1 -1
  4. package/dist/components/ColumnManager.d.ts.map +1 -1
  5. package/dist/components/ColumnManager.js +4 -2
  6. package/dist/components/ColumnManager.js.map +1 -1
  7. package/dist/components/MultiSelectColumnFilter.d.ts +25 -0
  8. package/dist/components/MultiSelectColumnFilter.d.ts.map +1 -0
  9. package/dist/components/MultiSelectColumnFilter.js +41 -0
  10. package/dist/components/MultiSelectColumnFilter.js.map +1 -0
  11. package/dist/components/NumericInputColumnFilter.d.ts +42 -0
  12. package/dist/components/NumericInputColumnFilter.d.ts.map +1 -0
  13. package/dist/components/NumericInputColumnFilter.js +79 -0
  14. package/dist/components/NumericInputColumnFilter.js.map +1 -0
  15. package/dist/components/TanstackTable.d.ts +3 -1
  16. package/dist/components/TanstackTable.d.ts.map +1 -1
  17. package/dist/components/TanstackTable.js +63 -20
  18. package/dist/components/TanstackTable.js.map +1 -1
  19. package/dist/components/TanstackTableDownloadButton.d.ts.map +1 -1
  20. package/dist/components/TanstackTableDownloadButton.js +3 -1
  21. package/dist/components/TanstackTableDownloadButton.js.map +1 -1
  22. package/dist/components/useShiftClickCheckbox.d.ts +26 -0
  23. package/dist/components/useShiftClickCheckbox.d.ts.map +1 -0
  24. package/dist/components/useShiftClickCheckbox.js +59 -0
  25. package/dist/components/useShiftClickCheckbox.js.map +1 -0
  26. package/dist/index.d.ts +3 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +3 -0
  29. package/dist/index.js.map +1 -1
  30. package/package.json +7 -5
  31. package/src/components/CategoricalColumnFilter.tsx +1 -1
  32. package/src/components/ColumnManager.tsx +5 -2
  33. package/src/components/MultiSelectColumnFilter.tsx +103 -0
  34. package/src/components/NumericInputColumnFilter.test.ts +102 -0
  35. package/src/components/NumericInputColumnFilter.tsx +153 -0
  36. package/src/components/TanstackTable.tsx +123 -41
  37. package/src/components/TanstackTableDownloadButton.tsx +27 -1
  38. package/src/components/useShiftClickCheckbox.tsx +67 -0
  39. package/src/index.ts +7 -0
  40. package/vitest.config.ts +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @prairielearn/ui
2
2
 
3
+ ## 1.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 50dbe96: - Add optional header labels / max height for column manager
8
+ - Add new option for buttons in next to the View column manager button
9
+ - add MultiSelectColumnFilter
10
+ - NumericInputColumnFilter
11
+ - add useShiftClickCheckbox hook + update table/buttons to support row selection
12
+
13
+ ## 1.1.2
14
+
15
+ ### Patch Changes
16
+
17
+ - 0425922: Upgrade all JavaScript dependencies
18
+ - Updated dependencies [0425922]
19
+ - @prairielearn/preact-cjs@1.1.6
20
+
3
21
  ## 1.1.1
4
22
 
5
23
  ### Patch Changes
@@ -41,7 +41,7 @@ export function CategoricalColumnFilter({ columnId, columnLabel, allColumnValues
41
41
  }
42
42
  apply(mode, set);
43
43
  };
44
- return (_jsxs(Dropdown, { align: "end", children: [_jsx(Dropdown.Toggle, { variant: "link", class: "text-muted p-0 ms-2", id: `filter-${columnId}`, "aria-label": `Filter ${columnLabel.toLowerCase()}`, title: `Filter ${columnLabel.toLowerCase()}`, children: _jsx("i", { class: clsx('bi', selected.size > 0 ? ['bi-funnel-fill', 'text-primary'] : 'bi-funnel'), "aria-hidden": "true" }) }), _jsx(Dropdown.Menu, { class: "p-0", children: _jsxs("div", { class: "p-3", children: [_jsxs("div", { class: "d-flex align-items-center justify-content-between mb-2", children: [_jsx("div", { class: "fw-semibold", children: columnLabel }), _jsx("button", { type: "button", class: "btn btn-link btn-sm text-decoration-none", onClick: () => apply(mode, new Set()), children: "Clear" })] }), _jsxs("div", { class: "btn-group w-100 mb-2", role: "group", "aria-label": "Include or exclude values", children: [_jsx("button", { type: "button", class: clsx('btn', mode === 'include' ? 'btn-primary' : 'btn-outline-secondary'), onClick: () => apply('include', selected), children: "Include" }), _jsx("button", { type: "button", class: clsx('btn', mode === 'exclude' ? 'btn-primary' : 'btn-outline-secondary'), onClick: () => apply('exclude', selected), children: "Exclude" })] }), _jsx("div", { class: "list-group list-group-flush", children: allColumnValues.map((value) => {
44
+ return (_jsxs(Dropdown, { align: "end", children: [_jsx(Dropdown.Toggle, { variant: "link", class: "text-muted p-0", id: `filter-${columnId}`, "aria-label": `Filter ${columnLabel.toLowerCase()}`, title: `Filter ${columnLabel.toLowerCase()}`, children: _jsx("i", { class: clsx('bi', selected.size > 0 ? ['bi-funnel-fill', 'text-primary'] : 'bi-funnel'), "aria-hidden": "true" }) }), _jsx(Dropdown.Menu, { class: "p-0", children: _jsxs("div", { class: "p-3", children: [_jsxs("div", { class: "d-flex align-items-center justify-content-between mb-2", children: [_jsx("div", { class: "fw-semibold", children: columnLabel }), _jsx("button", { type: "button", class: "btn btn-link btn-sm text-decoration-none", onClick: () => apply(mode, new Set()), children: "Clear" })] }), _jsxs("div", { class: "btn-group w-100 mb-2", role: "group", "aria-label": "Include or exclude values", children: [_jsx("button", { type: "button", class: clsx('btn', mode === 'include' ? 'btn-primary' : 'btn-outline-secondary'), onClick: () => apply('include', selected), children: "Include" }), _jsx("button", { type: "button", class: clsx('btn', mode === 'exclude' ? 'btn-primary' : 'btn-outline-secondary'), onClick: () => apply('exclude', selected), children: "Exclude" })] }), _jsx("div", { class: "list-group list-group-flush", children: allColumnValues.map((value) => {
45
45
  const isSelected = selected.has(value);
46
46
  return (_jsxs("div", { class: "list-group-item d-flex align-items-center gap-3", children: [_jsx("input", { class: "form-check-input", type: "checkbox", checked: isSelected, id: `${columnId}-${value}`, onChange: () => toggleSelected(value) }), _jsx("label", { class: "form-check-label", for: `${columnId}-${value}`, children: renderValueLabel({
47
47
  value,
@@ -1 +1 @@
1
- {"version":3,"file":"CategoricalColumnFilter.js","sourceRoot":"","sources":["../../src/components/CategoricalColumnFilter.tsx"],"names":[],"mappings":"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","sourcesContent":["import clsx from 'clsx';\nimport { type JSX, useMemo, useState } from 'preact/compat';\nimport Dropdown from 'react-bootstrap/Dropdown';\n\nfunction computeSelected<T extends readonly any[]>(\n allStatusValues: T,\n mode: 'include' | 'exclude',\n selected: Set<T[number]>,\n) {\n if (mode === 'include') {\n return selected;\n }\n return new Set(allStatusValues.filter((s) => !selected.has(s)));\n}\n\nfunction defaultRenderValueLabel<T>({ value }: { value: T }) {\n return <span>{String(value)}</span>;\n}\n/**\n * A component that allows the user to filter a categorical column. State is managed by the parent component.\n * The filter mode always defaults to \"include\".\n *\n * @param params\n * @param params.columnId - The ID of the column\n * @param params.columnLabel - The label of the column, e.g. \"Status\"\n * @param params.allColumnValues - The values to filter by\n * @param params.renderValueLabel - A function that renders the label for a value\n * @param params.columnValuesFilter - The current state of the column filter\n * @param params.setColumnValuesFilter - A function that sets the state of the column filter\n */\nexport function CategoricalColumnFilter<T extends readonly any[]>({\n columnId,\n columnLabel,\n allColumnValues,\n renderValueLabel = defaultRenderValueLabel,\n columnValuesFilter,\n setColumnValuesFilter,\n}: {\n columnId: string;\n columnLabel: string;\n allColumnValues: T;\n renderValueLabel?: (props: { value: T[number]; isSelected: boolean }) => JSX.Element;\n columnValuesFilter: T[number][];\n setColumnValuesFilter: (value: T[number][]) => void;\n}) {\n const [mode, setMode] = useState<'include' | 'exclude'>('include');\n\n const selected = useMemo(\n () => computeSelected(allColumnValues, mode, new Set(columnValuesFilter)),\n [mode, columnValuesFilter, allColumnValues],\n );\n\n const apply = (newMode: 'include' | 'exclude', newSelected: Set<T[number]>) => {\n const selected = computeSelected(allColumnValues, newMode, newSelected);\n setMode(newMode);\n setColumnValuesFilter(Array.from(selected));\n };\n\n const toggleSelected = (value: T[number]) => {\n const set = new Set(selected);\n if (set.has(value)) {\n set.delete(value);\n } else {\n set.add(value);\n }\n apply(mode, set);\n };\n\n return (\n <Dropdown align=\"end\">\n <Dropdown.Toggle\n variant=\"link\"\n class=\"text-muted p-0 ms-2\"\n id={`filter-${columnId}`}\n aria-label={`Filter ${columnLabel.toLowerCase()}`}\n title={`Filter ${columnLabel.toLowerCase()}`}\n >\n <i\n class={clsx('bi', selected.size > 0 ? ['bi-funnel-fill', 'text-primary'] : 'bi-funnel')}\n aria-hidden=\"true\"\n />\n </Dropdown.Toggle>\n <Dropdown.Menu class=\"p-0\">\n <div class=\"p-3\">\n <div class=\"d-flex align-items-center justify-content-between mb-2\">\n <div class=\"fw-semibold\">{columnLabel}</div>\n <button\n type=\"button\"\n class=\"btn btn-link btn-sm text-decoration-none\"\n onClick={() => apply(mode, new Set())}\n >\n Clear\n </button>\n </div>\n\n <div class=\"btn-group w-100 mb-2\" role=\"group\" aria-label=\"Include or exclude values\">\n <button\n type=\"button\"\n class={clsx('btn', mode === 'include' ? 'btn-primary' : 'btn-outline-secondary')}\n onClick={() => apply('include', selected)}\n >\n Include\n </button>\n <button\n type=\"button\"\n class={clsx('btn', mode === 'exclude' ? 'btn-primary' : 'btn-outline-secondary')}\n onClick={() => apply('exclude', selected)}\n >\n Exclude\n </button>\n </div>\n\n <div class=\"list-group list-group-flush\">\n {allColumnValues.map((value) => {\n const isSelected = selected.has(value);\n return (\n <div key={value} class=\"list-group-item d-flex align-items-center gap-3\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n checked={isSelected}\n id={`${columnId}-${value}`}\n onChange={() => toggleSelected(value)}\n />\n <label class=\"form-check-label\" for={`${columnId}-${value}`}>\n {renderValueLabel({\n value,\n isSelected,\n })}\n </label>\n </div>\n );\n })}\n </div>\n </div>\n </Dropdown.Menu>\n </Dropdown>\n );\n}\n"]}
1
+ {"version":3,"file":"CategoricalColumnFilter.js","sourceRoot":"","sources":["../../src/components/CategoricalColumnFilter.tsx"],"names":[],"mappings":"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","sourcesContent":["import clsx from 'clsx';\nimport { type JSX, useMemo, useState } from 'preact/compat';\nimport Dropdown from 'react-bootstrap/Dropdown';\n\nfunction computeSelected<T extends readonly any[]>(\n allStatusValues: T,\n mode: 'include' | 'exclude',\n selected: Set<T[number]>,\n) {\n if (mode === 'include') {\n return selected;\n }\n return new Set(allStatusValues.filter((s) => !selected.has(s)));\n}\n\nfunction defaultRenderValueLabel<T>({ value }: { value: T }) {\n return <span>{String(value)}</span>;\n}\n/**\n * A component that allows the user to filter a categorical column. State is managed by the parent component.\n * The filter mode always defaults to \"include\".\n *\n * @param params\n * @param params.columnId - The ID of the column\n * @param params.columnLabel - The label of the column, e.g. \"Status\"\n * @param params.allColumnValues - The values to filter by\n * @param params.renderValueLabel - A function that renders the label for a value\n * @param params.columnValuesFilter - The current state of the column filter\n * @param params.setColumnValuesFilter - A function that sets the state of the column filter\n */\nexport function CategoricalColumnFilter<T extends readonly any[]>({\n columnId,\n columnLabel,\n allColumnValues,\n renderValueLabel = defaultRenderValueLabel,\n columnValuesFilter,\n setColumnValuesFilter,\n}: {\n columnId: string;\n columnLabel: string;\n allColumnValues: T;\n renderValueLabel?: (props: { value: T[number]; isSelected: boolean }) => JSX.Element;\n columnValuesFilter: T[number][];\n setColumnValuesFilter: (value: T[number][]) => void;\n}) {\n const [mode, setMode] = useState<'include' | 'exclude'>('include');\n\n const selected = useMemo(\n () => computeSelected(allColumnValues, mode, new Set(columnValuesFilter)),\n [mode, columnValuesFilter, allColumnValues],\n );\n\n const apply = (newMode: 'include' | 'exclude', newSelected: Set<T[number]>) => {\n const selected = computeSelected(allColumnValues, newMode, newSelected);\n setMode(newMode);\n setColumnValuesFilter(Array.from(selected));\n };\n\n const toggleSelected = (value: T[number]) => {\n const set = new Set(selected);\n if (set.has(value)) {\n set.delete(value);\n } else {\n set.add(value);\n }\n apply(mode, set);\n };\n\n return (\n <Dropdown align=\"end\">\n <Dropdown.Toggle\n variant=\"link\"\n class=\"text-muted p-0\"\n id={`filter-${columnId}`}\n aria-label={`Filter ${columnLabel.toLowerCase()}`}\n title={`Filter ${columnLabel.toLowerCase()}`}\n >\n <i\n class={clsx('bi', selected.size > 0 ? ['bi-funnel-fill', 'text-primary'] : 'bi-funnel')}\n aria-hidden=\"true\"\n />\n </Dropdown.Toggle>\n <Dropdown.Menu class=\"p-0\">\n <div class=\"p-3\">\n <div class=\"d-flex align-items-center justify-content-between mb-2\">\n <div class=\"fw-semibold\">{columnLabel}</div>\n <button\n type=\"button\"\n class=\"btn btn-link btn-sm text-decoration-none\"\n onClick={() => apply(mode, new Set())}\n >\n Clear\n </button>\n </div>\n\n <div class=\"btn-group w-100 mb-2\" role=\"group\" aria-label=\"Include or exclude values\">\n <button\n type=\"button\"\n class={clsx('btn', mode === 'include' ? 'btn-primary' : 'btn-outline-secondary')}\n onClick={() => apply('include', selected)}\n >\n Include\n </button>\n <button\n type=\"button\"\n class={clsx('btn', mode === 'exclude' ? 'btn-primary' : 'btn-outline-secondary')}\n onClick={() => apply('exclude', selected)}\n >\n Exclude\n </button>\n </div>\n\n <div class=\"list-group list-group-flush\">\n {allColumnValues.map((value) => {\n const isSelected = selected.has(value);\n return (\n <div key={value} class=\"list-group-item d-flex align-items-center gap-3\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n checked={isSelected}\n id={`${columnId}-${value}`}\n onChange={() => toggleSelected(value)}\n />\n <label class=\"form-check-label\" for={`${columnId}-${value}`}>\n {renderValueLabel({\n value,\n isSelected,\n })}\n </label>\n </div>\n );\n })}\n </div>\n </div>\n </Dropdown.Menu>\n </Dropdown>\n );\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"ColumnManager.d.ts","sourceRoot":"","sources":["../../src/components/ColumnManager.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,KAAK,EAAE,MAAM,uBAAuB,CAAC;AA6FhE,wBAAgB,aAAa,CAAC,YAAY,EAAE,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,KAAK,CAAC,YAAY,CAAC,CAAA;CAAE,uCA2HpF"}
1
+ {"version":3,"file":"ColumnManager.d.ts","sourceRoot":"","sources":["../../src/components/ColumnManager.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAgGhE,wBAAgB,aAAa,CAAC,YAAY,EAAE,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,KAAK,CAAC,YAAY,CAAC,CAAA;CAAE,uCA2HpF"}
@@ -9,7 +9,9 @@ function ColumnMenuItem({ column, hidePinButton = false, onTogglePin, onClearEle
9
9
  const pinButtonRef = useRef(null);
10
10
  if (!column.getCanHide() && !column.getCanPin())
11
11
  return null;
12
- const header = typeof column.columnDef.header === 'string' ? column.columnDef.header : column.id;
12
+ // Use meta.label if available, otherwise fall back to header or column.id
13
+ const header = column.columnDef.meta?.label ??
14
+ (typeof column.columnDef.header === 'string' ? column.columnDef.header : column.id);
13
15
  return (_jsxs(Dropdown.Item, { as: "div", class: "px-2 py-1 d-flex align-items-center justify-content-between", onKeyDown: onClearElementFocus, children: [_jsxs("label", { class: "form-check me-auto text-nowrap d-flex align-items-stretch", children: [_jsx(OverlayTrigger, { placement: "top", overlay: _jsx(Tooltip, { children: column.getIsVisible() ? 'Hide column' : 'Show column' }), children: _jsx("input", { type: "checkbox", class: "form-check-input", checked: column.getIsVisible(), disabled: !column.getCanHide(), "aria-label": column.getIsVisible() ? `Hide '${header}' column` : `Show '${header}' column`, "aria-describedby": `${column.id}-label`, onChange: column.getToggleVisibilityHandler() }) }), _jsx("span", { class: "form-check-label ms-2", id: `${column.id}-label`, children: header })] }), column.getCanPin() && !hidePinButton && (_jsx("button", { ref: pinButtonRef, type: "button",
14
16
  // Since the HTML changes, but we want to refocus the pin button, we track
15
17
  // the active pin button and refocuses it when the column manager is rerendered.
@@ -73,7 +75,7 @@ export function ColumnManager({ table }) {
73
75
  if (menuRef.current && !menuRef.current.contains(e.target)) {
74
76
  setDropdownOpen(false);
75
77
  }
76
- }, children: [_jsxs(Dropdown.Toggle, { variant: "outline-secondary", id: "column-manager-button", children: [_jsx("i", { class: "bi bi-view-list me-2", "aria-hidden": "true" }), "View"] }), _jsxs(Dropdown.Menu, { children: [pinnedColumns.length > 0 && (_jsxs(_Fragment, { children: [_jsx("div", { class: "px-2 py-1 text-muted small", role: "presentation", children: "Frozen columns" }), _jsx("div", { role: "group", children: pinnedColumns.map((column, index) => {
78
+ }, children: [_jsxs(Dropdown.Toggle, { variant: "outline-secondary", id: "column-manager-button", children: [_jsx("i", { class: "bi bi-view-list me-2", "aria-hidden": "true" }), "View"] }), _jsxs(Dropdown.Menu, { style: { maxHeight: '60vh', overflowY: 'auto' }, children: [pinnedColumns.length > 0 && (_jsxs(_Fragment, { children: [_jsx("div", { class: "px-2 py-1 text-muted small", role: "presentation", children: "Frozen columns" }), _jsx("div", { role: "group", children: pinnedColumns.map((column, index) => {
77
79
  return (_jsx(ColumnMenuItem, { column: column, hidePinButton: index !== pinnedColumns.length - 1, onTogglePin: handleTogglePin, onClearElementFocus: () => setActiveElementId(null) }, column.id));
78
80
  }) }), _jsx(Dropdown.Divider, {})] })), unpinnedColumns.length > 0 && (_jsxs(_Fragment, { children: [_jsx("div", { role: "group", children: unpinnedColumns.map((column, index) => {
79
81
  return (_jsx(ColumnMenuItem, { column: column, hidePinButton: index !== 0, onTogglePin: handleTogglePin, onClearElementFocus: () => setActiveElementId(null) }, column.id));
@@ -1 +1 @@
1
- {"version":3,"file":"ColumnManager.js","sourceRoot":"","sources":["../../src/components/ColumnManager.tsx"],"names":[],"mappings":"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","sourcesContent":["import { type Column, type Table } from '@tanstack/react-table';\nimport { useEffect, useRef, useState } from 'preact/compat';\nimport Button from 'react-bootstrap/Button';\nimport Dropdown from 'react-bootstrap/Dropdown';\nimport OverlayTrigger from 'react-bootstrap/OverlayTrigger';\nimport Tooltip from 'react-bootstrap/Tooltip';\n\ninterface ColumnMenuItemProps<RowDataModel> {\n column: Column<RowDataModel>;\n hidePinButton: boolean;\n onTogglePin: (columnId: string) => void;\n onClearElementFocus: () => void;\n}\n\nfunction ColumnMenuItem<RowDataModel>({\n column,\n hidePinButton = false,\n onTogglePin,\n onClearElementFocus,\n}: ColumnMenuItemProps<RowDataModel>) {\n const pinButtonRef = useRef<HTMLButtonElement>(null);\n\n if (!column.getCanHide() && !column.getCanPin()) return null;\n\n const header = typeof column.columnDef.header === 'string' ? column.columnDef.header : column.id;\n\n return (\n <Dropdown.Item\n key={column.id}\n as=\"div\"\n class=\"px-2 py-1 d-flex align-items-center justify-content-between\"\n onKeyDown={onClearElementFocus}\n >\n <label class=\"form-check me-auto text-nowrap d-flex align-items-stretch\">\n <OverlayTrigger\n placement=\"top\"\n overlay={<Tooltip>{column.getIsVisible() ? 'Hide column' : 'Show column'}</Tooltip>}\n >\n <input\n type=\"checkbox\"\n class=\"form-check-input\"\n checked={column.getIsVisible()}\n disabled={!column.getCanHide()}\n aria-label={\n column.getIsVisible() ? `Hide '${header}' column` : `Show '${header}' column`\n }\n aria-describedby={`${column.id}-label`}\n onChange={column.getToggleVisibilityHandler()}\n />\n </OverlayTrigger>\n <span class=\"form-check-label ms-2\" id={`${column.id}-label`}>\n {header}\n </span>\n </label>\n {column.getCanPin() && !hidePinButton && (\n <button\n ref={pinButtonRef}\n type=\"button\"\n // Since the HTML changes, but we want to refocus the pin button, we track\n // the active pin button and refocuses it when the column manager is rerendered.\n id={`${column.id}-pin`}\n class=\"btn btn-sm btn-ghost ms-2\"\n aria-label={\n column.getIsPinned() ? `Unfreeze '${header}' column` : `Freeze '${header}' column`\n }\n title={column.getIsPinned() ? 'Unfreeze column' : 'Freeze column'}\n data-bs-toggle=\"tooltip\"\n tabIndex={0}\n onKeyDown={(e) => {\n if (!pinButtonRef.current) {\n throw new Error('pinButtonRef.current is null');\n }\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onTogglePin(column.id);\n return;\n }\n }}\n // Instead, use the arrow keys to move between interactive elements in each menu item.\n onClick={() => {\n if (!pinButtonRef.current) {\n throw new Error('pinButtonRef.current is null');\n }\n onTogglePin(column.id);\n }}\n >\n <i class={`bi ${column.getIsPinned() ? 'bi-x' : 'bi-snow'}`} aria-hidden=\"true\" />\n </button>\n )}\n </Dropdown.Item>\n );\n}\n\nexport function ColumnManager<RowDataModel>({ table }: { table: Table<RowDataModel> }) {\n const [activeElementId, setActiveElementId] = useState<string | null>(null);\n const [dropdownOpen, setDropdownOpen] = useState(false);\n const menuRef = useRef<HTMLDivElement>(null);\n const handleTogglePin = (columnId: string) => {\n const currentLeft = table.getState().columnPinning.left ?? [];\n const isPinned = currentLeft.includes(columnId);\n let newLeft: string[];\n if (isPinned) {\n newLeft = currentLeft.filter((id) => id !== columnId);\n } else {\n const columnOrder = table.getAllLeafColumns().map((c) => c.id);\n const newPinned = new Set([...currentLeft, columnId]);\n newLeft = columnOrder.filter((id) => newPinned.has(id));\n }\n table.setColumnPinning({ left: newLeft, right: [] });\n setActiveElementId(`${columnId}-pin`);\n };\n\n const isVisibilityChanged = Object.entries(table.getState().columnVisibility).some(\n ([key, value]) => {\n return value !== table.initialState.columnVisibility[key];\n },\n );\n\n const initialPinning = table.initialState.columnPinning.left ?? [];\n const currentPinning = table.getState().columnPinning.left ?? [];\n const isPinningChanged =\n initialPinning.length !== currentPinning.length ||\n initialPinning.some((id) => !currentPinning.includes(id));\n const showResetButton = isVisibilityChanged || isPinningChanged;\n\n const pinnedColumns = table.getAllLeafColumns().filter((c) => c.getIsPinned() === 'left');\n const unpinnedColumns = table.getAllLeafColumns().filter((c) => c.getIsPinned() !== 'left');\n\n useEffect(() => {\n // When we use the pin or reset button, we want to refocus to another element.\n // We want this in a useEffect so that this code runs after the component re-renders.\n\n // eslint-disable-next-line react-you-might-not-need-an-effect/no-event-handler\n if (activeElementId) {\n document.getElementById(activeElementId)?.focus();\n }\n }, [activeElementId]);\n\n return (\n <Dropdown\n ref={menuRef}\n autoClose=\"outside\"\n show={dropdownOpen}\n onToggle={(isOpen, _meta) => setDropdownOpen(isOpen)}\n onFocusOut={(e: FocusEvent) => {\n // Since we aren't using role=\"menu\", we need to manually close the dropdown when focus leaves.\n if (menuRef.current && !menuRef.current.contains(e.target as Node)) {\n setDropdownOpen(false);\n }\n }}\n >\n <Dropdown.Toggle variant=\"outline-secondary\" id=\"column-manager-button\">\n <i class=\"bi bi-view-list me-2\" aria-hidden=\"true\" />\n View\n </Dropdown.Toggle>\n <Dropdown.Menu>\n {pinnedColumns.length > 0 && (\n <>\n <div class=\"px-2 py-1 text-muted small\" role=\"presentation\">\n Frozen columns\n </div>\n <div role=\"group\">\n {pinnedColumns.map((column, index) => {\n return (\n <ColumnMenuItem\n key={column.id}\n column={column}\n hidePinButton={index !== pinnedColumns.length - 1}\n onTogglePin={handleTogglePin}\n onClearElementFocus={() => setActiveElementId(null)}\n />\n );\n })}\n </div>\n <Dropdown.Divider />\n </>\n )}\n {unpinnedColumns.length > 0 && (\n <>\n <div role=\"group\">\n {unpinnedColumns.map((column, index) => {\n return (\n <ColumnMenuItem\n key={column.id}\n column={column}\n hidePinButton={index !== 0}\n onTogglePin={handleTogglePin}\n onClearElementFocus={() => setActiveElementId(null)}\n />\n );\n })}\n </div>\n {showResetButton && <Dropdown.Divider />}\n </>\n )}\n {showResetButton && (\n <div class=\"px-2 py-1\">\n <Button\n variant=\"secondary\"\n size=\"sm\"\n class=\"w-100\"\n aria-label=\"Reset all columns to default visibility and pinning\"\n onClick={() => {\n table.resetColumnVisibility();\n table.resetColumnPinning();\n setActiveElementId('column-manager-button');\n }}\n >\n <i class=\"bi bi-arrow-counterclockwise me-2\" aria-hidden=\"true\" />\n Reset view\n </Button>\n </div>\n )}\n </Dropdown.Menu>\n </Dropdown>\n );\n}\n"]}
1
+ {"version":3,"file":"ColumnManager.js","sourceRoot":"","sources":["../../src/components/ColumnManager.tsx"],"names":[],"mappings":";AAAA,OAAO,EAA2B,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC5D,OAAO,MAAM,MAAM,wBAAwB,CAAC;AAC5C,OAAO,QAAQ,MAAM,0BAA0B,CAAC;AAChD,OAAO,cAAc,MAAM,gCAAgC,CAAC;AAC5D,OAAO,OAAO,MAAM,yBAAyB,CAAC;AAS9C,SAAS,cAAc,CAAe,EACpC,MAAM,EACN,aAAa,GAAG,KAAK,EACrB,WAAW,EACX,mBAAmB,GACe;IAClC,MAAM,YAAY,GAAG,MAAM,CAAoB,IAAI,CAAC,CAAC;IAErD,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE;QAAE,OAAO,IAAI,CAAC;IAE7D,0EAA0E;IAC1E,MAAM,MAAM,GACT,MAAM,CAAC,SAAS,CAAC,IAAY,EAAE,KAAK;QACrC,CAAC,OAAO,MAAM,CAAC,SAAS,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAEtF,OAAO,CACL,MAAC,QAAQ,CAAC,IAAI,IAEZ,EAAE,EAAC,KAAK,EACR,KAAK,EAAC,6DAA6D,EACnE,SAAS,EAAE,mBAAmB,aAE9B,iBAAO,KAAK,EAAC,2DAA2D,aACtE,KAAC,cAAc,IACb,SAAS,EAAC,KAAK,EACf,OAAO,EAAE,KAAC,OAAO,cAAE,MAAM,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,aAAa,GAAW,YAEnF,gBACE,IAAI,EAAC,UAAU,EACf,KAAK,EAAC,kBAAkB,EACxB,OAAO,EAAE,MAAM,CAAC,YAAY,EAAE,EAC9B,QAAQ,EAAE,CAAC,MAAM,CAAC,UAAU,EAAE,gBAE5B,MAAM,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,SAAS,MAAM,UAAU,CAAC,CAAC,CAAC,SAAS,MAAM,UAAU,sBAE7D,GAAG,MAAM,CAAC,EAAE,QAAQ,EACtC,QAAQ,EAAE,MAAM,CAAC,0BAA0B,EAAE,GAC7C,GACa,EACjB,eAAM,KAAK,EAAC,uBAAuB,EAAC,EAAE,EAAE,GAAG,MAAM,CAAC,EAAE,QAAQ,YACzD,MAAM,GACF,IACD,EACP,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,aAAa,IAAI,CACvC,iBACE,GAAG,EAAE,YAAY,EACjB,IAAI,EAAC,QAAQ;gBACb,0EAA0E;gBAC1E,gFAAgF;gBAChF,EAAE,EAAE,GAAG,MAAM,CAAC,EAAE,MAAM,EACtB,KAAK,EAAC,2BAA2B,gBAE/B,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,aAAa,MAAM,UAAU,CAAC,CAAC,CAAC,WAAW,MAAM,WAAW,EAErF,KAAK,EAAE,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,eAAe,oBAClD,SAAS,EACxB,QAAQ,EAAE,CAAC,EACX,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE;oBACf,IAAI,CAAC,YAAY,CAAC,OAAO,EAAE,CAAC;wBAC1B,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;oBAClD,CAAC;oBACD,IAAI,CAAC,CAAC,GAAG,KAAK,OAAO,IAAI,CAAC,CAAC,GAAG,KAAK,GAAG,EAAE,CAAC;wBACvC,CAAC,CAAC,cAAc,EAAE,CAAC;wBACnB,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;wBACvB,OAAO;oBACT,CAAC;gBACH,CAAC;gBACD,sFAAsF;gBACtF,OAAO,EAAE,GAAG,EAAE;oBACZ,IAAI,CAAC,YAAY,CAAC,OAAO,EAAE,CAAC;wBAC1B,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;oBAClD,CAAC;oBACD,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;gBACzB,CAAC,YAED,YAAG,KAAK,EAAE,MAAM,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,EAAE,iBAAc,MAAM,GAAG,GAC3E,CACV,KA5DI,MAAM,CAAC,EAAE,CA6DA,CACjB,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,aAAa,CAAe,EAAE,KAAK,EAAkC;IACnF,MAAM,CAAC,eAAe,EAAE,kBAAkB,CAAC,GAAG,QAAQ,CAAgB,IAAI,CAAC,CAAC;IAC5E,MAAM,CAAC,YAAY,EAAE,eAAe,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IACxD,MAAM,OAAO,GAAG,MAAM,CAAiB,IAAI,CAAC,CAAC;IAC7C,MAAM,eAAe,GAAG,CAAC,QAAgB,EAAE,EAAE;QAC3C,MAAM,WAAW,GAAG,KAAK,CAAC,QAAQ,EAAE,CAAC,aAAa,CAAC,IAAI,IAAI,EAAE,CAAC;QAC9D,MAAM,QAAQ,GAAG,WAAW,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAChD,IAAI,OAAiB,CAAC;QACtB,IAAI,QAAQ,EAAE,CAAC;YACb,OAAO,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,KAAK,QAAQ,CAAC,CAAC;QACxD,CAAC;aAAM,CAAC;YACN,MAAM,WAAW,GAAG,KAAK,CAAC,iBAAiB,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YAC/D,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,WAAW,EAAE,QAAQ,CAAC,CAAC,CAAC;YACtD,OAAO,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;QAC1D,CAAC;QACD,KAAK,CAAC,gBAAgB,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;QACrD,kBAAkB,CAAC,GAAG,QAAQ,MAAM,CAAC,CAAC;IACxC,CAAC,CAAC;IAEF,MAAM,mBAAmB,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,gBAAgB,CAAC,CAAC,IAAI,CAChF,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE;QACf,OAAO,KAAK,KAAK,KAAK,CAAC,YAAY,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC;IAC5D,CAAC,CACF,CAAC;IAEF,MAAM,cAAc,GAAG,KAAK,CAAC,YAAY,CAAC,aAAa,CAAC,IAAI,IAAI,EAAE,CAAC;IACnE,MAAM,cAAc,GAAG,KAAK,CAAC,QAAQ,EAAE,CAAC,aAAa,CAAC,IAAI,IAAI,EAAE,CAAC;IACjE,MAAM,gBAAgB,GACpB,cAAc,CAAC,MAAM,KAAK,cAAc,CAAC,MAAM;QAC/C,cAAc,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,cAAc,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC;IAC5D,MAAM,eAAe,GAAG,mBAAmB,IAAI,gBAAgB,CAAC;IAEhE,MAAM,aAAa,GAAG,KAAK,CAAC,iBAAiB,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,KAAK,MAAM,CAAC,CAAC;IAC1F,MAAM,eAAe,GAAG,KAAK,CAAC,iBAAiB,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,KAAK,MAAM,CAAC,CAAC;IAE5F,SAAS,CAAC,GAAG,EAAE;QACb,8EAA8E;QAC9E,qFAAqF;QAErF,+EAA+E;QAC/E,IAAI,eAAe,EAAE,CAAC;YACpB,QAAQ,CAAC,cAAc,CAAC,eAAe,CAAC,EAAE,KAAK,EAAE,CAAC;QACpD,CAAC;IACH,CAAC,EAAE,CAAC,eAAe,CAAC,CAAC,CAAC;IAEtB,OAAO,CACL,MAAC,QAAQ,IACP,GAAG,EAAE,OAAO,EACZ,SAAS,EAAC,SAAS,EACnB,IAAI,EAAE,YAAY,EAClB,QAAQ,EAAE,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,eAAe,CAAC,MAAM,CAAC,EACpD,UAAU,EAAE,CAAC,CAAa,EAAE,EAAE;YAC5B,+FAA+F;YAC/F,IAAI,OAAO,CAAC,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAc,CAAC,EAAE,CAAC;gBACnE,eAAe,CAAC,KAAK,CAAC,CAAC;YACzB,CAAC;QACH,CAAC,aAED,MAAC,QAAQ,CAAC,MAAM,IAAC,OAAO,EAAC,mBAAmB,EAAC,EAAE,EAAC,uBAAuB,aACrE,YAAG,KAAK,EAAC,sBAAsB,iBAAa,MAAM,GAAG,YAErC,EAClB,MAAC,QAAQ,CAAC,IAAI,IAAC,KAAK,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,aAC3D,aAAa,CAAC,MAAM,GAAG,CAAC,IAAI,CAC3B,8BACE,cAAK,KAAK,EAAC,4BAA4B,EAAC,IAAI,EAAC,cAAc,+BAErD,EACN,cAAK,IAAI,EAAC,OAAO,YACd,aAAa,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE;oCACnC,OAAO,CACL,KAAC,cAAc,IAEb,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,KAAK,KAAK,aAAa,CAAC,MAAM,GAAG,CAAC,EACjD,WAAW,EAAE,eAAe,EAC5B,mBAAmB,EAAE,GAAG,EAAE,CAAC,kBAAkB,CAAC,IAAI,CAAC,IAJ9C,MAAM,CAAC,EAAE,CAKd,CACH,CAAC;gCACJ,CAAC,CAAC,GACE,EACN,KAAC,QAAQ,CAAC,OAAO,KAAG,IACnB,CACJ,EACA,eAAe,CAAC,MAAM,GAAG,CAAC,IAAI,CAC7B,8BACE,cAAK,IAAI,EAAC,OAAO,YACd,eAAe,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE;oCACrC,OAAO,CACL,KAAC,cAAc,IAEb,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,KAAK,KAAK,CAAC,EAC1B,WAAW,EAAE,eAAe,EAC5B,mBAAmB,EAAE,GAAG,EAAE,CAAC,kBAAkB,CAAC,IAAI,CAAC,IAJ9C,MAAM,CAAC,EAAE,CAKd,CACH,CAAC;gCACJ,CAAC,CAAC,GACE,EACL,eAAe,IAAI,KAAC,QAAQ,CAAC,OAAO,KAAG,IACvC,CACJ,EACA,eAAe,IAAI,CAClB,cAAK,KAAK,EAAC,WAAW,YACpB,MAAC,MAAM,IACL,OAAO,EAAC,WAAW,EACnB,IAAI,EAAC,IAAI,EACT,KAAK,EAAC,OAAO,gBACF,qDAAqD,EAChE,OAAO,EAAE,GAAG,EAAE;gCACZ,KAAK,CAAC,qBAAqB,EAAE,CAAC;gCAC9B,KAAK,CAAC,kBAAkB,EAAE,CAAC;gCAC3B,kBAAkB,CAAC,uBAAuB,CAAC,CAAC;4BAC9C,CAAC,aAED,YAAG,KAAK,EAAC,mCAAmC,iBAAa,MAAM,GAAG,kBAE3D,GACL,CACP,IACa,IACP,CACZ,CAAC;AACJ,CAAC","sourcesContent":["import { type Column, type Table } from '@tanstack/react-table';\nimport { useEffect, useRef, useState } from 'preact/compat';\nimport Button from 'react-bootstrap/Button';\nimport Dropdown from 'react-bootstrap/Dropdown';\nimport OverlayTrigger from 'react-bootstrap/OverlayTrigger';\nimport Tooltip from 'react-bootstrap/Tooltip';\n\ninterface ColumnMenuItemProps<RowDataModel> {\n column: Column<RowDataModel>;\n hidePinButton: boolean;\n onTogglePin: (columnId: string) => void;\n onClearElementFocus: () => void;\n}\n\nfunction ColumnMenuItem<RowDataModel>({\n column,\n hidePinButton = false,\n onTogglePin,\n onClearElementFocus,\n}: ColumnMenuItemProps<RowDataModel>) {\n const pinButtonRef = useRef<HTMLButtonElement>(null);\n\n if (!column.getCanHide() && !column.getCanPin()) return null;\n\n // Use meta.label if available, otherwise fall back to header or column.id\n const header =\n (column.columnDef.meta as any)?.label ??\n (typeof column.columnDef.header === 'string' ? column.columnDef.header : column.id);\n\n return (\n <Dropdown.Item\n key={column.id}\n as=\"div\"\n class=\"px-2 py-1 d-flex align-items-center justify-content-between\"\n onKeyDown={onClearElementFocus}\n >\n <label class=\"form-check me-auto text-nowrap d-flex align-items-stretch\">\n <OverlayTrigger\n placement=\"top\"\n overlay={<Tooltip>{column.getIsVisible() ? 'Hide column' : 'Show column'}</Tooltip>}\n >\n <input\n type=\"checkbox\"\n class=\"form-check-input\"\n checked={column.getIsVisible()}\n disabled={!column.getCanHide()}\n aria-label={\n column.getIsVisible() ? `Hide '${header}' column` : `Show '${header}' column`\n }\n aria-describedby={`${column.id}-label`}\n onChange={column.getToggleVisibilityHandler()}\n />\n </OverlayTrigger>\n <span class=\"form-check-label ms-2\" id={`${column.id}-label`}>\n {header}\n </span>\n </label>\n {column.getCanPin() && !hidePinButton && (\n <button\n ref={pinButtonRef}\n type=\"button\"\n // Since the HTML changes, but we want to refocus the pin button, we track\n // the active pin button and refocuses it when the column manager is rerendered.\n id={`${column.id}-pin`}\n class=\"btn btn-sm btn-ghost ms-2\"\n aria-label={\n column.getIsPinned() ? `Unfreeze '${header}' column` : `Freeze '${header}' column`\n }\n title={column.getIsPinned() ? 'Unfreeze column' : 'Freeze column'}\n data-bs-toggle=\"tooltip\"\n tabIndex={0}\n onKeyDown={(e) => {\n if (!pinButtonRef.current) {\n throw new Error('pinButtonRef.current is null');\n }\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onTogglePin(column.id);\n return;\n }\n }}\n // Instead, use the arrow keys to move between interactive elements in each menu item.\n onClick={() => {\n if (!pinButtonRef.current) {\n throw new Error('pinButtonRef.current is null');\n }\n onTogglePin(column.id);\n }}\n >\n <i class={`bi ${column.getIsPinned() ? 'bi-x' : 'bi-snow'}`} aria-hidden=\"true\" />\n </button>\n )}\n </Dropdown.Item>\n );\n}\n\nexport function ColumnManager<RowDataModel>({ table }: { table: Table<RowDataModel> }) {\n const [activeElementId, setActiveElementId] = useState<string | null>(null);\n const [dropdownOpen, setDropdownOpen] = useState(false);\n const menuRef = useRef<HTMLDivElement>(null);\n const handleTogglePin = (columnId: string) => {\n const currentLeft = table.getState().columnPinning.left ?? [];\n const isPinned = currentLeft.includes(columnId);\n let newLeft: string[];\n if (isPinned) {\n newLeft = currentLeft.filter((id) => id !== columnId);\n } else {\n const columnOrder = table.getAllLeafColumns().map((c) => c.id);\n const newPinned = new Set([...currentLeft, columnId]);\n newLeft = columnOrder.filter((id) => newPinned.has(id));\n }\n table.setColumnPinning({ left: newLeft, right: [] });\n setActiveElementId(`${columnId}-pin`);\n };\n\n const isVisibilityChanged = Object.entries(table.getState().columnVisibility).some(\n ([key, value]) => {\n return value !== table.initialState.columnVisibility[key];\n },\n );\n\n const initialPinning = table.initialState.columnPinning.left ?? [];\n const currentPinning = table.getState().columnPinning.left ?? [];\n const isPinningChanged =\n initialPinning.length !== currentPinning.length ||\n initialPinning.some((id) => !currentPinning.includes(id));\n const showResetButton = isVisibilityChanged || isPinningChanged;\n\n const pinnedColumns = table.getAllLeafColumns().filter((c) => c.getIsPinned() === 'left');\n const unpinnedColumns = table.getAllLeafColumns().filter((c) => c.getIsPinned() !== 'left');\n\n useEffect(() => {\n // When we use the pin or reset button, we want to refocus to another element.\n // We want this in a useEffect so that this code runs after the component re-renders.\n\n // eslint-disable-next-line react-you-might-not-need-an-effect/no-event-handler\n if (activeElementId) {\n document.getElementById(activeElementId)?.focus();\n }\n }, [activeElementId]);\n\n return (\n <Dropdown\n ref={menuRef}\n autoClose=\"outside\"\n show={dropdownOpen}\n onToggle={(isOpen, _meta) => setDropdownOpen(isOpen)}\n onFocusOut={(e: FocusEvent) => {\n // Since we aren't using role=\"menu\", we need to manually close the dropdown when focus leaves.\n if (menuRef.current && !menuRef.current.contains(e.target as Node)) {\n setDropdownOpen(false);\n }\n }}\n >\n <Dropdown.Toggle variant=\"outline-secondary\" id=\"column-manager-button\">\n <i class=\"bi bi-view-list me-2\" aria-hidden=\"true\" />\n View\n </Dropdown.Toggle>\n <Dropdown.Menu style={{ maxHeight: '60vh', overflowY: 'auto' }}>\n {pinnedColumns.length > 0 && (\n <>\n <div class=\"px-2 py-1 text-muted small\" role=\"presentation\">\n Frozen columns\n </div>\n <div role=\"group\">\n {pinnedColumns.map((column, index) => {\n return (\n <ColumnMenuItem\n key={column.id}\n column={column}\n hidePinButton={index !== pinnedColumns.length - 1}\n onTogglePin={handleTogglePin}\n onClearElementFocus={() => setActiveElementId(null)}\n />\n );\n })}\n </div>\n <Dropdown.Divider />\n </>\n )}\n {unpinnedColumns.length > 0 && (\n <>\n <div role=\"group\">\n {unpinnedColumns.map((column, index) => {\n return (\n <ColumnMenuItem\n key={column.id}\n column={column}\n hidePinButton={index !== 0}\n onTogglePin={handleTogglePin}\n onClearElementFocus={() => setActiveElementId(null)}\n />\n );\n })}\n </div>\n {showResetButton && <Dropdown.Divider />}\n </>\n )}\n {showResetButton && (\n <div class=\"px-2 py-1\">\n <Button\n variant=\"secondary\"\n size=\"sm\"\n class=\"w-100\"\n aria-label=\"Reset all columns to default visibility and pinning\"\n onClick={() => {\n table.resetColumnVisibility();\n table.resetColumnPinning();\n setActiveElementId('column-manager-button');\n }}\n >\n <i class=\"bi bi-arrow-counterclockwise me-2\" aria-hidden=\"true\" />\n Reset view\n </Button>\n </div>\n )}\n </Dropdown.Menu>\n </Dropdown>\n );\n}\n"]}
@@ -0,0 +1,25 @@
1
+ import { type JSX } from 'preact/compat';
2
+ /**
3
+ * A component that allows the user to filter a column containing arrays of values.
4
+ * Uses AND logic: rows must contain ALL selected values to match.
5
+ *
6
+ * @param params
7
+ * @param params.columnId - The ID of the column
8
+ * @param params.columnLabel - The label of the column, e.g. "Rubric Items"
9
+ * @param params.allColumnValues - All possible values that can appear in the column
10
+ * @param params.renderValueLabel - A function that renders the label for a value
11
+ * @param params.columnValuesFilter - The current state of the column filter
12
+ * @param params.setColumnValuesFilter - A function that sets the state of the column filter
13
+ */
14
+ export declare function MultiSelectColumnFilter<T extends readonly any[]>({ columnId, columnLabel, allColumnValues, renderValueLabel, columnValuesFilter, setColumnValuesFilter, }: {
15
+ columnId: string;
16
+ columnLabel: string;
17
+ allColumnValues: T;
18
+ renderValueLabel?: (props: {
19
+ value: T[number];
20
+ isSelected: boolean;
21
+ }) => JSX.Element;
22
+ columnValuesFilter: T[number][];
23
+ setColumnValuesFilter: (value: T[number][]) => void;
24
+ }): JSX.Element;
25
+ //# sourceMappingURL=MultiSelectColumnFilter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MultiSelectColumnFilter.d.ts","sourceRoot":"","sources":["../../src/components/MultiSelectColumnFilter.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,GAAG,EAAW,MAAM,eAAe,CAAC;AAOlD;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,SAAS,SAAS,GAAG,EAAE,EAAE,EAChE,QAAQ,EACR,WAAW,EACX,eAAe,EACf,gBAA0C,EAC1C,kBAAkB,EAClB,qBAAqB,GACtB,EAAE;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,CAAC,CAAC;IACnB,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC;QAAC,UAAU,EAAE,OAAO,CAAA;KAAE,KAAK,GAAG,CAAC,OAAO,CAAC;IACrF,kBAAkB,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC;IAChC,qBAAqB,EAAE,CAAC,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,KAAK,IAAI,CAAC;CACrD,eAoEA"}
@@ -0,0 +1,41 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@prairielearn/preact-cjs/jsx-runtime";
2
+ import clsx from 'clsx';
3
+ import { useMemo } from 'preact/compat';
4
+ import Dropdown from 'react-bootstrap/Dropdown';
5
+ function defaultRenderValueLabel({ value }) {
6
+ return _jsx("span", { children: String(value) });
7
+ }
8
+ /**
9
+ * A component that allows the user to filter a column containing arrays of values.
10
+ * Uses AND logic: rows must contain ALL selected values to match.
11
+ *
12
+ * @param params
13
+ * @param params.columnId - The ID of the column
14
+ * @param params.columnLabel - The label of the column, e.g. "Rubric Items"
15
+ * @param params.allColumnValues - All possible values that can appear in the column
16
+ * @param params.renderValueLabel - A function that renders the label for a value
17
+ * @param params.columnValuesFilter - The current state of the column filter
18
+ * @param params.setColumnValuesFilter - A function that sets the state of the column filter
19
+ */
20
+ export function MultiSelectColumnFilter({ columnId, columnLabel, allColumnValues, renderValueLabel = defaultRenderValueLabel, columnValuesFilter, setColumnValuesFilter, }) {
21
+ const selected = useMemo(() => new Set(columnValuesFilter), [columnValuesFilter]);
22
+ const toggleSelected = (value) => {
23
+ const set = new Set(selected);
24
+ if (set.has(value)) {
25
+ set.delete(value);
26
+ }
27
+ else {
28
+ set.add(value);
29
+ }
30
+ setColumnValuesFilter(Array.from(set));
31
+ };
32
+ const hasActiveFilter = selected.size > 0;
33
+ return (_jsxs(Dropdown, { align: "end", children: [_jsx(Dropdown.Toggle, { variant: "link", class: "text-muted p-0", id: `filter-${columnId}`, "aria-label": `Filter ${columnLabel.toLowerCase()}`, title: `Filter ${columnLabel.toLowerCase()}`, children: _jsx("i", { class: clsx('bi', hasActiveFilter ? ['bi-funnel-fill', 'text-primary'] : 'bi-funnel'), "aria-hidden": "true" }) }), _jsx(Dropdown.Menu, { class: "p-0", children: _jsxs("div", { class: "p-3", style: { minWidth: '250px' }, children: [_jsxs("div", { class: "d-flex align-items-center justify-content-between mb-2", children: [_jsx("div", { class: "fw-semibold", children: columnLabel }), _jsx("button", { type: "button", class: "btn btn-link btn-sm text-decoration-none p-0", onClick: () => setColumnValuesFilter([]), children: "Clear" })] }), _jsx("div", { class: "list-group list-group-flush", children: allColumnValues.map((value) => {
34
+ const isSelected = selected.has(value);
35
+ return (_jsxs("div", { class: "list-group-item d-flex align-items-center gap-3 px-0", children: [_jsx("input", { class: "form-check-input", type: "checkbox", checked: isSelected, id: `${columnId}-${value}`, onChange: () => toggleSelected(value) }), _jsx("label", { class: "form-check-label fw-normal", for: `${columnId}-${value}`, children: renderValueLabel({
36
+ value,
37
+ isSelected,
38
+ }) })] }, value));
39
+ }) })] }) })] }));
40
+ }
41
+ //# sourceMappingURL=MultiSelectColumnFilter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MultiSelectColumnFilter.js","sourceRoot":"","sources":["../../src/components/MultiSelectColumnFilter.tsx"],"names":[],"mappings":"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","sourcesContent":["import clsx from 'clsx';\nimport { type JSX, useMemo } from 'preact/compat';\nimport Dropdown from 'react-bootstrap/Dropdown';\n\nfunction defaultRenderValueLabel<T>({ value }: { value: T }) {\n return <span>{String(value)}</span>;\n}\n\n/**\n * A component that allows the user to filter a column containing arrays of values.\n * Uses AND logic: rows must contain ALL selected values to match.\n *\n * @param params\n * @param params.columnId - The ID of the column\n * @param params.columnLabel - The label of the column, e.g. \"Rubric Items\"\n * @param params.allColumnValues - All possible values that can appear in the column\n * @param params.renderValueLabel - A function that renders the label for a value\n * @param params.columnValuesFilter - The current state of the column filter\n * @param params.setColumnValuesFilter - A function that sets the state of the column filter\n */\nexport function MultiSelectColumnFilter<T extends readonly any[]>({\n columnId,\n columnLabel,\n allColumnValues,\n renderValueLabel = defaultRenderValueLabel,\n columnValuesFilter,\n setColumnValuesFilter,\n}: {\n columnId: string;\n columnLabel: string;\n allColumnValues: T;\n renderValueLabel?: (props: { value: T[number]; isSelected: boolean }) => JSX.Element;\n columnValuesFilter: T[number][];\n setColumnValuesFilter: (value: T[number][]) => void;\n}) {\n const selected = useMemo(() => new Set(columnValuesFilter), [columnValuesFilter]);\n\n const toggleSelected = (value: T[number]) => {\n const set = new Set(selected);\n if (set.has(value)) {\n set.delete(value);\n } else {\n set.add(value);\n }\n setColumnValuesFilter(Array.from(set));\n };\n\n const hasActiveFilter = selected.size > 0;\n\n return (\n <Dropdown align=\"end\">\n <Dropdown.Toggle\n variant=\"link\"\n class=\"text-muted p-0\"\n id={`filter-${columnId}`}\n aria-label={`Filter ${columnLabel.toLowerCase()}`}\n title={`Filter ${columnLabel.toLowerCase()}`}\n >\n <i\n class={clsx('bi', hasActiveFilter ? ['bi-funnel-fill', 'text-primary'] : 'bi-funnel')}\n aria-hidden=\"true\"\n />\n </Dropdown.Toggle>\n <Dropdown.Menu class=\"p-0\">\n <div class=\"p-3\" style={{ minWidth: '250px' }}>\n <div class=\"d-flex align-items-center justify-content-between mb-2\">\n <div class=\"fw-semibold\">{columnLabel}</div>\n <button\n type=\"button\"\n class=\"btn btn-link btn-sm text-decoration-none p-0\"\n onClick={() => setColumnValuesFilter([])}\n >\n Clear\n </button>\n </div>\n\n <div class=\"list-group list-group-flush\">\n {allColumnValues.map((value) => {\n const isSelected = selected.has(value);\n return (\n <div key={value} class=\"list-group-item d-flex align-items-center gap-3 px-0\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n checked={isSelected}\n id={`${columnId}-${value}`}\n onChange={() => toggleSelected(value)}\n />\n <label class=\"form-check-label fw-normal\" for={`${columnId}-${value}`}>\n {renderValueLabel({\n value,\n isSelected,\n })}\n </label>\n </div>\n );\n })}\n </div>\n </div>\n </Dropdown.Menu>\n </Dropdown>\n );\n}\n"]}
@@ -0,0 +1,42 @@
1
+ interface NumericInputColumnFilterProps {
2
+ columnId: string;
3
+ columnLabel: string;
4
+ value: string;
5
+ onChange: (value: string) => void;
6
+ }
7
+ /**
8
+ * A component that allows the user to filter a numeric column using comparison operators.
9
+ * Supports syntax like: <1, >0, <=5, >=10, =5, or just 5 (implicit equals)
10
+ *
11
+ * @param params
12
+ * @param params.columnId - The ID of the column
13
+ * @param params.columnLabel - The label of the column, e.g. "Manual Points"
14
+ * @param params.value - The current filter value (e.g., ">5" or "10")
15
+ * @param params.onChange - Callback when the filter value changes
16
+ */
17
+ export declare function NumericInputColumnFilter({ columnId, columnLabel, value, onChange, }: NumericInputColumnFilterProps): import("original-preact").JSX.Element;
18
+ /**
19
+ * Helper function to parse a numeric filter value.
20
+ * Returns null if the filter is invalid or empty.
21
+ *
22
+ * @param filterValue - The filter string (e.g., ">5", "<=10", "3")
23
+ * @returns Parsed operator and value, or null if invalid
24
+ */
25
+ export declare function parseNumericFilter(filterValue: string): {
26
+ operator: '<' | '>' | '<=' | '>=' | '=';
27
+ value: number;
28
+ } | null;
29
+ /**
30
+ * TanStack Table filter function for numeric columns.
31
+ * Use this as the `filterFn` for numeric columns.
32
+ *
33
+ * @example
34
+ * {
35
+ * id: 'manual_points',
36
+ * accessorKey: 'manual_points',
37
+ * filterFn: numericColumnFilterFn,
38
+ * }
39
+ */
40
+ export declare function numericColumnFilterFn(row: any, columnId: string, filterValue: string): boolean;
41
+ export {};
42
+ //# sourceMappingURL=NumericInputColumnFilter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NumericInputColumnFilter.d.ts","sourceRoot":"","sources":["../../src/components/NumericInputColumnFilter.tsx"],"names":[],"mappings":"AAGA,UAAU,6BAA6B;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC;AAED;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CAAC,EACvC,QAAQ,EACR,WAAW,EACX,KAAK,EACL,QAAQ,GACT,EAAE,6BAA6B,yCAqE/B;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG;IACvD,QAAQ,EAAE,GAAG,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,GAAG,GAAG,CAAC;IACxC,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,IAAI,CAYP;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAqB9F"}
@@ -0,0 +1,79 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@prairielearn/preact-cjs/jsx-runtime";
2
+ import clsx from 'clsx';
3
+ import Dropdown from 'react-bootstrap/Dropdown';
4
+ /**
5
+ * A component that allows the user to filter a numeric column using comparison operators.
6
+ * Supports syntax like: <1, >0, <=5, >=10, =5, or just 5 (implicit equals)
7
+ *
8
+ * @param params
9
+ * @param params.columnId - The ID of the column
10
+ * @param params.columnLabel - The label of the column, e.g. "Manual Points"
11
+ * @param params.value - The current filter value (e.g., ">5" or "10")
12
+ * @param params.onChange - Callback when the filter value changes
13
+ */
14
+ export function NumericInputColumnFilter({ columnId, columnLabel, value, onChange, }) {
15
+ const hasActiveFilter = value.trim().length > 0;
16
+ const isInvalid = hasActiveFilter && parseNumericFilter(value) === null;
17
+ return (_jsxs(Dropdown, { align: "end", children: [_jsx(Dropdown.Toggle, { variant: "link", class: clsx('text-muted p-0', hasActiveFilter && (isInvalid ? 'text-warning' : 'text-primary')), id: `filter-${columnId}`, "aria-label": `Filter ${columnLabel.toLowerCase()}`, title: `Filter ${columnLabel.toLowerCase()}`, children: _jsx("i", { class: clsx('bi', isInvalid
18
+ ? 'bi-exclamation-triangle'
19
+ : hasActiveFilter
20
+ ? 'bi-funnel-fill'
21
+ : 'bi-funnel'), "aria-hidden": "true" }) }), _jsx(Dropdown.Menu, { children: _jsxs("div", { class: "p-3", style: { minWidth: '240px' }, children: [_jsx("label", { class: "form-label small fw-semibold mb-2", children: columnLabel }), _jsx("input", { type: "text", class: clsx('form-control form-control-sm', isInvalid && 'is-invalid'), placeholder: "e.g., >0, <5, =10", value: value, onInput: (e) => {
22
+ if (e.target instanceof HTMLInputElement) {
23
+ onChange(e.target.value);
24
+ }
25
+ }, onClick: (e) => e.stopPropagation() }), isInvalid && (_jsxs("div", { class: "invalid-feedback d-block", children: ["Invalid filter format. Use operators like ", _jsx("code", { children: ">5" }), " or ", _jsx("code", { children: "<=10" })] })), !isInvalid && (_jsxs("div", { class: "form-text small mt-2", children: ["Use operators: ", _jsx("code", { children: "<" }), ", ", _jsx("code", { children: ">" }), ", ", _jsx("code", { children: "<=" }), ",", ' ', _jsx("code", { children: ">=" }), ", ", _jsx("code", { children: "=" }), _jsx("br", {}), "Example: ", _jsx("code", { children: ">5" }), " or ", _jsx("code", { children: "<=10" })] })), hasActiveFilter && (_jsx("button", { type: "button", class: "btn btn-sm btn-link text-decoration-none mt-2 p-0", onClick: () => onChange(''), children: "Clear filter" }))] }) })] }));
26
+ }
27
+ /**
28
+ * Helper function to parse a numeric filter value.
29
+ * Returns null if the filter is invalid or empty.
30
+ *
31
+ * @param filterValue - The filter string (e.g., ">5", "<=10", "3")
32
+ * @returns Parsed operator and value, or null if invalid
33
+ */
34
+ export function parseNumericFilter(filterValue) {
35
+ if (!filterValue.trim())
36
+ return null;
37
+ const match = filterValue.trim().match(/^(<=?|>=?|=)?\s*(-?\d+\.?\d*)$/);
38
+ if (!match)
39
+ return null;
40
+ const operator = (match[1] || '=');
41
+ const value = Number.parseFloat(match[2]);
42
+ if (Number.isNaN(value))
43
+ return null;
44
+ return { operator, value };
45
+ }
46
+ /**
47
+ * TanStack Table filter function for numeric columns.
48
+ * Use this as the `filterFn` for numeric columns.
49
+ *
50
+ * @example
51
+ * {
52
+ * id: 'manual_points',
53
+ * accessorKey: 'manual_points',
54
+ * filterFn: numericColumnFilterFn,
55
+ * }
56
+ */
57
+ export function numericColumnFilterFn(row, columnId, filterValue) {
58
+ const parsed = parseNumericFilter(filterValue);
59
+ if (!parsed)
60
+ return true; // Invalid or empty filter = show all
61
+ const cellValue = row.getValue(columnId);
62
+ if (cellValue === null || cellValue === undefined)
63
+ return false;
64
+ switch (parsed.operator) {
65
+ case '<':
66
+ return cellValue < parsed.value;
67
+ case '>':
68
+ return cellValue > parsed.value;
69
+ case '<=':
70
+ return cellValue <= parsed.value;
71
+ case '>=':
72
+ return cellValue >= parsed.value;
73
+ case '=':
74
+ return cellValue === parsed.value;
75
+ default:
76
+ return true;
77
+ }
78
+ }
79
+ //# sourceMappingURL=NumericInputColumnFilter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NumericInputColumnFilter.js","sourceRoot":"","sources":["../../src/components/NumericInputColumnFilter.tsx"],"names":[],"mappings":";AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,QAAQ,MAAM,0BAA0B,CAAC;AAShD;;;;;;;;;GASG;AACH,MAAM,UAAU,wBAAwB,CAAC,EACvC,QAAQ,EACR,WAAW,EACX,KAAK,EACL,QAAQ,GACsB;IAC9B,MAAM,eAAe,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;IAChD,MAAM,SAAS,GAAG,eAAe,IAAI,kBAAkB,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC;IAExE,OAAO,CACL,MAAC,QAAQ,IAAC,KAAK,EAAC,KAAK,aACnB,KAAC,QAAQ,CAAC,MAAM,IACd,OAAO,EAAC,MAAM,EACd,KAAK,EAAE,IAAI,CACT,gBAAgB,EAChB,eAAe,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,cAAc,CAAC,CACjE,EACD,EAAE,EAAE,UAAU,QAAQ,EAAE,gBACZ,UAAU,WAAW,CAAC,WAAW,EAAE,EAAE,EACjD,KAAK,EAAE,UAAU,WAAW,CAAC,WAAW,EAAE,EAAE,YAE5C,YACE,KAAK,EAAE,IAAI,CACT,IAAI,EACJ,SAAS;wBACP,CAAC,CAAC,yBAAyB;wBAC3B,CAAC,CAAC,eAAe;4BACf,CAAC,CAAC,gBAAgB;4BAClB,CAAC,CAAC,WAAW,CAClB,iBACW,MAAM,GAClB,GACc,EAClB,KAAC,QAAQ,CAAC,IAAI,cACZ,eAAK,KAAK,EAAC,KAAK,EAAC,KAAK,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,aAC3C,gBAAO,KAAK,EAAC,mCAAmC,YAAE,WAAW,GAAS,EACtE,gBACE,IAAI,EAAC,MAAM,EACX,KAAK,EAAE,IAAI,CAAC,8BAA8B,EAAE,SAAS,IAAI,YAAY,CAAC,EACtE,WAAW,EAAC,mBAAmB,EAC/B,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE;gCACb,IAAI,CAAC,CAAC,MAAM,YAAY,gBAAgB,EAAE,CAAC;oCACzC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gCAC3B,CAAC;4BACH,CAAC,EACD,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,eAAe,EAAE,GACnC,EACD,SAAS,IAAI,CACZ,eAAK,KAAK,EAAC,0BAA0B,2DACO,gCAAkB,UAAI,kCAAoB,IAChF,CACP,EACA,CAAC,SAAS,IAAI,CACb,eAAK,KAAK,EAAC,sBAAsB,gCAChB,+BAAiB,QAAE,+BAAiB,QAAE,gCAAkB,OAAE,GAAG,EAC5E,gCAAkB,QAAE,+BAAc,EAClC,cAAM,eACG,gCAAkB,UAAI,kCAAoB,IAC/C,CACP,EACA,eAAe,IAAI,CAClB,iBACE,IAAI,EAAC,QAAQ,EACb,KAAK,EAAC,mDAAmD,EACzD,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,6BAGpB,CACV,IACG,GACQ,IACP,CACZ,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,WAAmB;IAIpD,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE;QAAE,OAAO,IAAI,CAAC;IAErC,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,gCAAgC,CAAC,CAAC;IACzE,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IAExB,MAAM,QAAQ,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,GAAG,CAAkC,CAAC;IACpE,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAE1C,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAErC,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;AAC7B,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,qBAAqB,CAAC,GAAQ,EAAE,QAAgB,EAAE,WAAmB;IACnF,MAAM,MAAM,GAAG,kBAAkB,CAAC,WAAW,CAAC,CAAC;IAC/C,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC,CAAC,qCAAqC;IAE/D,MAAM,SAAS,GAAG,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAkB,CAAC;IAC1D,IAAI,SAAS,KAAK,IAAI,IAAI,SAAS,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IAEhE,QAAQ,MAAM,CAAC,QAAQ,EAAE,CAAC;QACxB,KAAK,GAAG;YACN,OAAO,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC;QAClC,KAAK,GAAG;YACN,OAAO,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC;QAClC,KAAK,IAAI;YACP,OAAO,SAAS,IAAI,MAAM,CAAC,KAAK,CAAC;QACnC,KAAK,IAAI;YACP,OAAO,SAAS,IAAI,MAAM,CAAC,KAAK,CAAC;QACnC,KAAK,GAAG;YACN,OAAO,SAAS,KAAK,MAAM,CAAC,KAAK,CAAC;QACpC;YACE,OAAO,IAAI,CAAC;IAChB,CAAC;AACH,CAAC","sourcesContent":["import clsx from 'clsx';\nimport Dropdown from 'react-bootstrap/Dropdown';\n\ninterface NumericInputColumnFilterProps {\n columnId: string;\n columnLabel: string;\n value: string;\n onChange: (value: string) => void;\n}\n\n/**\n * A component that allows the user to filter a numeric column using comparison operators.\n * Supports syntax like: <1, >0, <=5, >=10, =5, or just 5 (implicit equals)\n *\n * @param params\n * @param params.columnId - The ID of the column\n * @param params.columnLabel - The label of the column, e.g. \"Manual Points\"\n * @param params.value - The current filter value (e.g., \">5\" or \"10\")\n * @param params.onChange - Callback when the filter value changes\n */\nexport function NumericInputColumnFilter({\n columnId,\n columnLabel,\n value,\n onChange,\n}: NumericInputColumnFilterProps) {\n const hasActiveFilter = value.trim().length > 0;\n const isInvalid = hasActiveFilter && parseNumericFilter(value) === null;\n\n return (\n <Dropdown align=\"end\">\n <Dropdown.Toggle\n variant=\"link\"\n class={clsx(\n 'text-muted p-0',\n hasActiveFilter && (isInvalid ? 'text-warning' : 'text-primary'),\n )}\n id={`filter-${columnId}`}\n aria-label={`Filter ${columnLabel.toLowerCase()}`}\n title={`Filter ${columnLabel.toLowerCase()}`}\n >\n <i\n class={clsx(\n 'bi',\n isInvalid\n ? 'bi-exclamation-triangle'\n : hasActiveFilter\n ? 'bi-funnel-fill'\n : 'bi-funnel',\n )}\n aria-hidden=\"true\"\n />\n </Dropdown.Toggle>\n <Dropdown.Menu>\n <div class=\"p-3\" style={{ minWidth: '240px' }}>\n <label class=\"form-label small fw-semibold mb-2\">{columnLabel}</label>\n <input\n type=\"text\"\n class={clsx('form-control form-control-sm', isInvalid && 'is-invalid')}\n placeholder=\"e.g., >0, <5, =10\"\n value={value}\n onInput={(e) => {\n if (e.target instanceof HTMLInputElement) {\n onChange(e.target.value);\n }\n }}\n onClick={(e) => e.stopPropagation()}\n />\n {isInvalid && (\n <div class=\"invalid-feedback d-block\">\n Invalid filter format. Use operators like <code>&gt;5</code> or <code>&lt;=10</code>\n </div>\n )}\n {!isInvalid && (\n <div class=\"form-text small mt-2\">\n Use operators: <code>&lt;</code>, <code>&gt;</code>, <code>&lt;=</code>,{' '}\n <code>&gt;=</code>, <code>=</code>\n <br />\n Example: <code>&gt;5</code> or <code>&lt;=10</code>\n </div>\n )}\n {hasActiveFilter && (\n <button\n type=\"button\"\n class=\"btn btn-sm btn-link text-decoration-none mt-2 p-0\"\n onClick={() => onChange('')}\n >\n Clear filter\n </button>\n )}\n </div>\n </Dropdown.Menu>\n </Dropdown>\n );\n}\n\n/**\n * Helper function to parse a numeric filter value.\n * Returns null if the filter is invalid or empty.\n *\n * @param filterValue - The filter string (e.g., \">5\", \"<=10\", \"3\")\n * @returns Parsed operator and value, or null if invalid\n */\nexport function parseNumericFilter(filterValue: string): {\n operator: '<' | '>' | '<=' | '>=' | '=';\n value: number;\n} | null {\n if (!filterValue.trim()) return null;\n\n const match = filterValue.trim().match(/^(<=?|>=?|=)?\\s*(-?\\d+\\.?\\d*)$/);\n if (!match) return null;\n\n const operator = (match[1] || '=') as '<' | '>' | '<=' | '>=' | '=';\n const value = Number.parseFloat(match[2]);\n\n if (Number.isNaN(value)) return null;\n\n return { operator, value };\n}\n\n/**\n * TanStack Table filter function for numeric columns.\n * Use this as the `filterFn` for numeric columns.\n *\n * @example\n * {\n * id: 'manual_points',\n * accessorKey: 'manual_points',\n * filterFn: numericColumnFilterFn,\n * }\n */\nexport function numericColumnFilterFn(row: any, columnId: string, filterValue: string): boolean {\n const parsed = parseNumericFilter(filterValue);\n if (!parsed) return true; // Invalid or empty filter = show all\n\n const cellValue = row.getValue(columnId) as number | null;\n if (cellValue === null || cellValue === undefined) return false;\n\n switch (parsed.operator) {\n case '<':\n return cellValue < parsed.value;\n case '>':\n return cellValue > parsed.value;\n case '<=':\n return cellValue <= parsed.value;\n case '>=':\n return cellValue >= parsed.value;\n case '=':\n return cellValue === parsed.value;\n default:\n return true;\n }\n}\n"]}
@@ -28,6 +28,7 @@ export declare function TanstackTable<RowDataModel>({ table, title, filters, row
28
28
  * @param params.table - The table model
29
29
  * @param params.title - The title of the card
30
30
  * @param params.headerButtons - The buttons to display in the header
31
+ * @param params.columnManagerButtons - The buttons to display next to the column manager (View button)
31
32
  * @param params.globalFilter - State management for the global filter
32
33
  * @param params.globalFilter.value
33
34
  * @param params.globalFilter.setValue
@@ -35,10 +36,11 @@ export declare function TanstackTable<RowDataModel>({ table, title, filters, row
35
36
  * @param params.tableOptions - Specific options for the table. See {@link TanstackTableProps} for more details.
36
37
  * @param params.downloadButtonOptions - Specific options for the download button. See {@link TanstackTableDownloadButtonProps} for more details.
37
38
  */
38
- export declare function TanstackTableCard<RowDataModel>({ table, title, headerButtons, globalFilter, tableOptions, downloadButtonOptions, }: {
39
+ export declare function TanstackTableCard<RowDataModel>({ table, title, headerButtons, columnManagerButtons, globalFilter, tableOptions, downloadButtonOptions, }: {
39
40
  table: Table<RowDataModel>;
40
41
  title: string;
41
42
  headerButtons: JSX.Element;
43
+ columnManagerButtons?: JSX.Element;
42
44
  globalFilter: {
43
45
  value: string;
44
46
  setValue: (value: string) => void;
@@ -1 +1 @@
1
- {"version":3,"file":"TanstackTable.d.ts","sourceRoot":"","sources":["../../src/components/TanstackTable.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAsB,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAG9E,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,oBAAoB,CAAC;AAG9C,OAAO,EAEL,KAAK,gCAAgC,EACtC,MAAM,kCAAkC,CAAC;AA0F1C,UAAU,kBAAkB,CAAC,YAAY;IACvC,KAAK,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC,YAAY,EAAE,OAAO,CAAC,CAAA;KAAE,KAAK,GAAG,CAAC,OAAO,CAAC,CAAC;IAC5F,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC;IAC7B,UAAU,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC;CAC1B;AAID;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,YAAY,EAAE,EAC1C,KAAK,EACL,KAAK,EACL,OAA4B,EAC5B,SAAc,EACd,cAAsC,EACtC,UAA8B,GAC/B,EAAE,kBAAkB,CAAC,YAAY,CAAC,eAkUlC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,EAC9C,KAAK,EACL,KAAK,EACL,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,qBAA4B,GAC7B,EAAE;IACD,KAAK,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,GAAG,CAAC,OAAO,CAAC;IAC3B,YAAY,EAAE;QACZ,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;QAClC,WAAW,EAAE,MAAM,CAAC;KACrB,CAAC;IACF,YAAY,EAAE,OAAO,CAAC,IAAI,CAAC,kBAAkB,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;IACvE,qBAAqB,CAAC,EAAE,IAAI,CAAC,gCAAgC,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC9F,eAgGA"}
1
+ {"version":3,"file":"TanstackTable.d.ts","sourceRoot":"","sources":["../../src/components/TanstackTable.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAsB,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAG9E,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,oBAAoB,CAAC;AAG9C,OAAO,EAEL,KAAK,gCAAgC,EACtC,MAAM,kCAAkC,CAAC;AA0F1C,UAAU,kBAAkB,CAAC,YAAY;IACvC,KAAK,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC,YAAY,EAAE,OAAO,CAAC,CAAA;KAAE,KAAK,GAAG,CAAC,OAAO,CAAC,CAAC;IAC5F,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC;IAC7B,UAAU,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC;CAC1B;AAID;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,YAAY,EAAE,EAC1C,KAAK,EACL,KAAK,EACL,OAA4B,EAC5B,SAAc,EACd,cAAsC,EACtC,UAA8B,GAC/B,EAAE,kBAAkB,CAAC,YAAY,CAAC,eAuYlC;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,EAC9C,KAAK,EACL,KAAK,EACL,aAAa,EACb,oBAAoB,EACpB,YAAY,EACZ,YAAY,EACZ,qBAA4B,GAC7B,EAAE;IACD,KAAK,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,GAAG,CAAC,OAAO,CAAC;IAC3B,oBAAoB,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC;IACnC,YAAY,EAAE;QACZ,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;QAClC,WAAW,EAAE,MAAM,CAAC;KACrB,CAAC;IACF,YAAY,EAAE,OAAO,CAAC,IAAI,CAAC,kBAAkB,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;IACvE,qBAAqB,CAAC,EAAE,IAAI,CAAC,gCAAgC,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC9F,eA0GA"}