@xeplr/ui-table 1.0.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.
@@ -0,0 +1,75 @@
1
+ import React from 'react';
2
+ import FilterWrapper from './FilterWrapper.jsx';
3
+ import { resolveDate } from '../operators.js';
4
+
5
+ var OPERATORS = [
6
+ { value: 'eq', label: 'Equals' },
7
+ { value: 'neq', label: 'Not Equals' },
8
+ { value: 'gt', label: 'After' },
9
+ { value: 'gte', label: 'On or After' },
10
+ { value: 'lt', label: 'Before' },
11
+ { value: 'lte', label: 'On or Before' },
12
+ { value: 'between', label: 'Between' }
13
+ ];
14
+
15
+ /**
16
+ * Date filter: operator select + date input(s).
17
+ *
18
+ * Filter value shape: { operator, value, valueTo }
19
+ */
20
+ export default function DateFilter({ column }) {
21
+ var currentFilter = column.getFilterValue() || { operator: 'eq', value: '', valueTo: '' };
22
+ var isActive = currentFilter.value !== '';
23
+
24
+ function update(patch) {
25
+ column.setFilterValue(Object.assign({}, currentFilter, patch));
26
+ }
27
+
28
+ function handleClear() {
29
+ column.setFilterValue(undefined);
30
+ }
31
+
32
+ return (
33
+ <FilterWrapper isActive={isActive} onClear={handleClear}>
34
+ <div className="xeplr-table-date-filter">
35
+ <select
36
+ className="xeplr-table-filter-operator"
37
+ value={currentFilter.operator}
38
+ onChange={function(e) { update({ operator: e.target.value }); }}
39
+ >
40
+ {OPERATORS.map(function(op) {
41
+ return <option key={op.value} value={op.value}>{op.label}</option>;
42
+ })}
43
+ </select>
44
+
45
+ <input
46
+ className="xeplr-table-filter-input"
47
+ type="date"
48
+ value={currentFilter.value}
49
+ onChange={function(e) { update({ value: e.target.value }); }}
50
+ />
51
+
52
+ {currentFilter.operator === 'between' && (
53
+ <input
54
+ className="xeplr-table-filter-input"
55
+ type="date"
56
+ value={currentFilter.valueTo}
57
+ onChange={function(e) { update({ valueTo: e.target.value }); }}
58
+ />
59
+ )}
60
+ </div>
61
+ </FilterWrapper>
62
+ );
63
+ }
64
+
65
+ /**
66
+ * Custom filter function for date columns.
67
+ */
68
+ export function dateFilterFn(row, columnId, filterValue) {
69
+ if (!filterValue || !filterValue.value) return true;
70
+
71
+ var raw = row.getValue(columnId);
72
+ if (raw === null || raw === undefined || raw === '') return false;
73
+
74
+ return resolveDate(raw, filterValue.operator, filterValue.value, filterValue.valueTo);
75
+ }
@@ -0,0 +1,66 @@
1
+ import React, { useState, useRef, useEffect } from 'react';
2
+
3
+ /**
4
+ * Common filter popover shell.
5
+ * Renders a trigger icon and a dropdown panel that closes on outside click.
6
+ *
7
+ * @param {object} props
8
+ * @param {boolean} props.isActive - Whether filter is currently applied
9
+ * @param {React.ReactNode} props.children - Filter UI content
10
+ * @param {Function} props.onClear - Called when user clears the filter
11
+ */
12
+ export default function FilterWrapper({ isActive, children, onClear }) {
13
+ var [open, setOpen] = useState(false);
14
+ var wrapperRef = useRef(null);
15
+
16
+ // Close on outside click
17
+ useEffect(function() {
18
+ if (!open) return;
19
+
20
+ function handleClick(e) {
21
+ if (wrapperRef.current && !wrapperRef.current.contains(e.target)) {
22
+ setOpen(false);
23
+ }
24
+ }
25
+
26
+ document.addEventListener('mousedown', handleClick);
27
+ return function() { document.removeEventListener('mousedown', handleClick); };
28
+ }, [open]);
29
+
30
+ return (
31
+ <div className="xeplr-table-filter-wrapper" ref={wrapperRef}>
32
+ <button
33
+ className={'xeplr-table-filter-trigger' + (isActive ? ' xeplr-table-filter-active' : '')}
34
+ onClick={function() { setOpen(!open); }}
35
+ title="Filter"
36
+ type="button"
37
+ >
38
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
39
+ <polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" />
40
+ </svg>
41
+ </button>
42
+
43
+ {open && (
44
+ <div className="xeplr-table-filter-panel">
45
+ {children}
46
+ <div className="xeplr-table-filter-actions">
47
+ <button
48
+ type="button"
49
+ className="xeplr-table-filter-clear-btn"
50
+ onClick={function() { onClear(); setOpen(false); }}
51
+ >
52
+ Clear
53
+ </button>
54
+ <button
55
+ type="button"
56
+ className="xeplr-table-filter-close-btn"
57
+ onClick={function() { setOpen(false); }}
58
+ >
59
+ Done
60
+ </button>
61
+ </div>
62
+ </div>
63
+ )}
64
+ </div>
65
+ );
66
+ }
@@ -0,0 +1,105 @@
1
+ import React from 'react';
2
+ import FilterWrapper from './FilterWrapper.jsx';
3
+ import { resolveNumber, isNullish } from '../operators.js';
4
+
5
+ var OPERATORS = [
6
+ { value: 'eq', label: '= Equals' },
7
+ { value: 'neq', label: '!= Not Equals' },
8
+ { value: 'gt', label: '> Greater Than' },
9
+ { value: 'gte', label: '>= Greater or Equal' },
10
+ { value: 'lt', label: '< Less Than' },
11
+ { value: 'lte', label: '<= Less or Equal' },
12
+ { value: 'between', label: 'Between' }
13
+ ];
14
+
15
+ /**
16
+ * Number filter: operator select + input(s) + null checks.
17
+ *
18
+ * Filter value shape: { operator, value, valueTo, isNull: boolean | null }
19
+ */
20
+ export default function NumberFilter({ column }) {
21
+ var currentFilter = column.getFilterValue() || { operator: 'eq', value: '', valueTo: '', isNull: null };
22
+ var isActive = currentFilter.value !== '' || currentFilter.isNull !== null;
23
+
24
+ function update(patch) {
25
+ column.setFilterValue(Object.assign({}, currentFilter, patch));
26
+ }
27
+
28
+ function setNullMode(mode) {
29
+ var next = currentFilter.isNull === mode ? null : mode;
30
+ update({ isNull: next });
31
+ }
32
+
33
+ function handleClear() {
34
+ column.setFilterValue(undefined);
35
+ }
36
+
37
+ return (
38
+ <FilterWrapper isActive={isActive} onClear={handleClear}>
39
+ <div className="xeplr-table-number-filter">
40
+ <select
41
+ className="xeplr-table-filter-operator"
42
+ value={currentFilter.operator}
43
+ onChange={function(e) { update({ operator: e.target.value }); }}
44
+ >
45
+ {OPERATORS.map(function(op) {
46
+ return <option key={op.value} value={op.value}>{op.label}</option>;
47
+ })}
48
+ </select>
49
+
50
+ <input
51
+ className="xeplr-table-filter-input"
52
+ type="number"
53
+ placeholder={currentFilter.operator === 'between' ? 'Min' : 'Value'}
54
+ value={currentFilter.value}
55
+ onChange={function(e) { update({ value: e.target.value }); }}
56
+ />
57
+
58
+ {currentFilter.operator === 'between' && (
59
+ <input
60
+ className="xeplr-table-filter-input"
61
+ type="number"
62
+ placeholder="Max"
63
+ value={currentFilter.valueTo}
64
+ onChange={function(e) { update({ valueTo: e.target.value }); }}
65
+ />
66
+ )}
67
+
68
+ <div className="xeplr-table-filter-null-controls">
69
+ <button
70
+ type="button"
71
+ className={currentFilter.isNull === true ? 'active' : ''}
72
+ onClick={function() { setNullMode(true); }}
73
+ >
74
+ Is Null
75
+ </button>
76
+ <button
77
+ type="button"
78
+ className={currentFilter.isNull === false ? 'active' : ''}
79
+ onClick={function() { setNullMode(false); }}
80
+ >
81
+ Is Not Null
82
+ </button>
83
+ </div>
84
+ </div>
85
+ </FilterWrapper>
86
+ );
87
+ }
88
+
89
+ /**
90
+ * Custom filter function for number columns.
91
+ */
92
+ export function numberFilterFn(row, columnId, filterValue) {
93
+ if (!filterValue) return true;
94
+
95
+ var raw = row.getValue(columnId);
96
+
97
+ // Null mode
98
+ if (filterValue.isNull === true) return isNullish(raw);
99
+ if (filterValue.isNull === false && isNullish(raw)) return false;
100
+
101
+ // No value entered — pass through
102
+ if (filterValue.value === '' || filterValue.value === undefined) return true;
103
+
104
+ return resolveNumber(raw, filterValue.operator, filterValue.value, filterValue.valueTo);
105
+ }
@@ -0,0 +1,142 @@
1
+ import React, { useState, useMemo } from 'react';
2
+ import FilterWrapper from './FilterWrapper.jsx';
3
+
4
+ /**
5
+ * Excel-like string filter: searchable multi-select of unique values + null checks.
6
+ *
7
+ * Filter value shape: { selected: string[], isNull: boolean | null }
8
+ * - selected: array of checked values (empty = all selected = no filter)
9
+ * - isNull: true = show only nulls, false = exclude nulls, null = don't care
10
+ */
11
+ export default function StringFilter({ column }) {
12
+ var currentFilter = column.getFilterValue() || { selected: [], isNull: null };
13
+ var [search, setSearch] = useState('');
14
+
15
+ // Collect unique non-null values from the full (pre-filtered) data
16
+ var uniqueValues = useMemo(function() {
17
+ var vals = new Set();
18
+ var rows = column.getFacetedRowModel().rows;
19
+ for (var i = 0; i < rows.length; i++) {
20
+ var val = rows[i].getValue(column.id);
21
+ if (val !== null && val !== undefined && val !== '') {
22
+ vals.add(String(val));
23
+ }
24
+ }
25
+ var arr = Array.from(vals);
26
+ arr.sort();
27
+ return arr;
28
+ }, [column.getFacetedRowModel()]);
29
+
30
+ var filteredValues = useMemo(function() {
31
+ if (!search) return uniqueValues;
32
+ var lower = search.toLowerCase();
33
+ return uniqueValues.filter(function(v) { return v.toLowerCase().includes(lower); });
34
+ }, [uniqueValues, search]);
35
+
36
+ var selectedSet = new Set(currentFilter.selected);
37
+ var isActive = currentFilter.selected.length > 0 || currentFilter.isNull !== null;
38
+
39
+ function toggleValue(val) {
40
+ var next = new Set(selectedSet);
41
+ if (next.has(val)) {
42
+ next.delete(val);
43
+ } else {
44
+ next.add(val);
45
+ }
46
+ column.setFilterValue({ selected: Array.from(next), isNull: currentFilter.isNull });
47
+ }
48
+
49
+ function selectAll() {
50
+ column.setFilterValue({ selected: [], isNull: currentFilter.isNull });
51
+ }
52
+
53
+ function selectNone() {
54
+ // Selecting "none" means selecting all visible as the filter — i.e. show nothing
55
+ // Actually, set selected to a sentinel empty-but-active state
56
+ column.setFilterValue({ selected: ['__xeplr_none__'], isNull: currentFilter.isNull });
57
+ }
58
+
59
+ function setNullMode(mode) {
60
+ var next = currentFilter.isNull === mode ? null : mode;
61
+ column.setFilterValue({ selected: currentFilter.selected, isNull: next });
62
+ }
63
+
64
+ function handleClear() {
65
+ column.setFilterValue(undefined);
66
+ setSearch('');
67
+ }
68
+
69
+ return (
70
+ <FilterWrapper isActive={isActive} onClear={handleClear}>
71
+ <div className="xeplr-table-string-filter">
72
+ <input
73
+ className="xeplr-table-filter-search"
74
+ type="text"
75
+ placeholder="Search..."
76
+ value={search}
77
+ onChange={function(e) { setSearch(e.target.value); }}
78
+ />
79
+
80
+ <div className="xeplr-table-filter-bulk">
81
+ <button type="button" onClick={selectAll}>Select All</button>
82
+ <button type="button" onClick={selectNone}>Clear All</button>
83
+ </div>
84
+
85
+ <div className="xeplr-table-filter-list">
86
+ {filteredValues.map(function(val) {
87
+ var checked = selectedSet.size === 0 || selectedSet.has(val);
88
+ return (
89
+ <label key={val} className="xeplr-table-filter-option">
90
+ <input
91
+ type="checkbox"
92
+ checked={checked}
93
+ onChange={function() { toggleValue(val); }}
94
+ />
95
+ <span>{val}</span>
96
+ </label>
97
+ );
98
+ })}
99
+ {filteredValues.length === 0 && (
100
+ <div className="xeplr-table-filter-empty">No values</div>
101
+ )}
102
+ </div>
103
+
104
+ <div className="xeplr-table-filter-null-controls">
105
+ <button
106
+ type="button"
107
+ className={currentFilter.isNull === true ? 'active' : ''}
108
+ onClick={function() { setNullMode(true); }}
109
+ >
110
+ Is Null
111
+ </button>
112
+ <button
113
+ type="button"
114
+ className={currentFilter.isNull === false ? 'active' : ''}
115
+ onClick={function() { setNullMode(false); }}
116
+ >
117
+ Is Not Null
118
+ </button>
119
+ </div>
120
+ </div>
121
+ </FilterWrapper>
122
+ );
123
+ }
124
+
125
+ /**
126
+ * Custom filter function for string columns.
127
+ */
128
+ export function stringFilterFn(row, columnId, filterValue) {
129
+ if (!filterValue) return true;
130
+
131
+ var val = row.getValue(columnId);
132
+ var isNullish = val === null || val === undefined || val === '';
133
+
134
+ // Null mode check
135
+ if (filterValue.isNull === true) return isNullish;
136
+ if (filterValue.isNull === false && isNullish) return false;
137
+
138
+ // Selected values check
139
+ if (filterValue.selected.length === 0) return true; // no selection = show all
140
+ if (filterValue.selected[0] === '__xeplr_none__') return false;
141
+ return filterValue.selected.includes(String(val));
142
+ }
package/src/index.js ADDED
@@ -0,0 +1,32 @@
1
+ // Main component
2
+ export { default as XeplrTable } from './XeplrTable.jsx';
3
+
4
+ // Controller hooks
5
+ export { default as useTableController } from './useTableController.js';
6
+ export { default as useActionsController } from './useActionsController.js';
7
+
8
+ // Type detection
9
+ export { detectTypes, TYPES } from './detectTypes.js';
10
+
11
+ // Individual filter components (for custom layouts)
12
+ export { default as StringFilter, stringFilterFn } from './filters/StringFilter.jsx';
13
+ export { default as NumberFilter, numberFilterFn } from './filters/NumberFilter.jsx';
14
+ export { default as DateFilter, dateFilterFn } from './filters/DateFilter.jsx';
15
+ export { default as BooleanFilter, booleanFilterFn } from './filters/BooleanFilter.jsx';
16
+ export { default as FilterWrapper } from './filters/FilterWrapper.jsx';
17
+
18
+ // Action components (for custom layouts)
19
+ export { default as ActionsCell } from './actions/ActionsCell.jsx';
20
+ export { default as RecordModal } from './actions/RecordModal.jsx';
21
+ export { default as ChildTable } from './actions/ChildTable.jsx';
22
+ export { default as RecordDetail } from './actions/RecordDetail.jsx';
23
+ export { CHILD_DISPLAY } from './XeplrTable.jsx';
24
+
25
+ // Change set builder
26
+ export { buildChangeSet } from './buildChangeSet.js';
27
+
28
+ // Operator resolver (shared comparison logic)
29
+ export { resolve as resolveOperator, resolveString, resolveNumber, resolveDate, isNullish } from './operators.js';
30
+
31
+ // Conditional formatting
32
+ export { resolveCellStyle } from './resolveCellStyle.js';
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Shared operator resolver for comparing values.
3
+ * Used by filters, conditional formatting, and any future comparison logic.
4
+ *
5
+ * String operators: eq, neq, isnull, starts, ends, contains, notcontains
6
+ * Number operators: eq, neq, isnull, gt, gte, lt, lte, between
7
+ * Date operators: eq, neq, isnull, gt, gte, lt, lte, between
8
+ */
9
+
10
+ // ── Null check (works for all types) ──
11
+
12
+ function isNullish(value) {
13
+ return value === null || value === undefined || value === '';
14
+ }
15
+
16
+ // ── String ──
17
+
18
+ function resolveString(value, operator, target) {
19
+ if (operator === 'isnull') return isNullish(value);
20
+ if (isNullish(value)) return false;
21
+
22
+ var v = String(value).toLowerCase();
23
+ var t = (target != null ? String(target) : '').toLowerCase();
24
+
25
+ switch (operator) {
26
+ case 'eq': return v === t;
27
+ case 'neq': return v !== t;
28
+ case 'starts': return v.startsWith(t);
29
+ case 'ends': return v.endsWith(t);
30
+ case 'contains': return v.includes(t);
31
+ case 'notcontains': return !v.includes(t);
32
+ default: return false;
33
+ }
34
+ }
35
+
36
+ // ── Number ──
37
+
38
+ function resolveNumber(value, operator, target, targetTo) {
39
+ if (operator === 'isnull') return isNullish(value);
40
+ if (isNullish(value)) return false;
41
+
42
+ var v = Number(value);
43
+ var t = Number(target);
44
+
45
+ switch (operator) {
46
+ case 'eq': return v === t;
47
+ case 'neq': return v !== t;
48
+ case 'gt': return v > t;
49
+ case 'gte': return v >= t;
50
+ case 'lt': return v < t;
51
+ case 'lte': return v <= t;
52
+ case 'between': {
53
+ var to = Number(targetTo);
54
+ if (isNaN(to)) return v >= t;
55
+ return v >= t && v <= to;
56
+ }
57
+ default: return false;
58
+ }
59
+ }
60
+
61
+ // ── Date ──
62
+
63
+ function toDay(v) {
64
+ var d = new Date(v);
65
+ return Date.UTC(d.getFullYear(), d.getMonth(), d.getDate());
66
+ }
67
+
68
+ function resolveDate(value, operator, target, targetTo) {
69
+ if (operator === 'isnull') return isNullish(value);
70
+ if (isNullish(value)) return false;
71
+
72
+ var v = toDay(value);
73
+ var t = toDay(target);
74
+ if (isNaN(v) || isNaN(t)) return false;
75
+
76
+ switch (operator) {
77
+ case 'eq': return v === t;
78
+ case 'neq': return v !== t;
79
+ case 'gt': return v > t;
80
+ case 'gte': return v >= t;
81
+ case 'lt': return v < t;
82
+ case 'lte': return v <= t;
83
+ case 'between': {
84
+ if (!targetTo) return v >= t;
85
+ var to = toDay(targetTo);
86
+ return v >= t && v <= to;
87
+ }
88
+ default: return false;
89
+ }
90
+ }
91
+
92
+ // ── Unified entry point ──
93
+
94
+ /**
95
+ * Resolve an operator comparison.
96
+ *
97
+ * @param {string} dataType - 'string' | 'number' | 'date'
98
+ * @param {*} value - The value to test
99
+ * @param {string} operator - The operator key
100
+ * @param {*} target - The comparison target
101
+ * @param {*} [targetTo] - Second target for 'between'
102
+ * @returns {boolean}
103
+ */
104
+ function resolve(dataType, value, operator, target, targetTo) {
105
+ switch (dataType) {
106
+ case 'string': return resolveString(value, operator, target);
107
+ case 'number': return resolveNumber(value, operator, target, targetTo);
108
+ case 'date': return resolveDate(value, operator, target, targetTo);
109
+ default: return resolveString(value, operator, target);
110
+ }
111
+ }
112
+
113
+ export { resolve, resolveString, resolveNumber, resolveDate, isNullish };