@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,213 @@
1
+ import React, { useState } from 'react';
2
+ import ChildTable from './ChildTable.jsx';
3
+
4
+ /**
5
+ * Detail popup — shows parent record fields at top, child tables below.
6
+ * Opened on double-click (or view icon) when childDisplay === 'popup'.
7
+ *
8
+ * @param {object} props
9
+ * @param {object} props.record - The parent row (with child arrays)
10
+ * @param {string} props.rootId - Row id (or _tempId)
11
+ * @param {object} props.schema - Full schema
12
+ * @param {Array} props.schemaColumns - Level 0 columns for display hints
13
+ * @param {boolean} props.hasSave - Whether save is available
14
+ * @param {object} props.actions - Actions controller
15
+ * @param {Function} props.onClose - Close handler
16
+ */
17
+ export default function RecordDetail(props) {
18
+ var record = props.record;
19
+ var rootId = props.rootId;
20
+ var schema = props.schema;
21
+ var schemaColumns = props.schemaColumns || [];
22
+ var hasSave = props.hasSave;
23
+ var actions = props.actions;
24
+ var onClose = props.onClose;
25
+
26
+ var [editing, setEditing] = useState(false);
27
+ var [editRecord, setEditRecord] = useState(null);
28
+
29
+ // Build column lookup for display hints
30
+ var columnMap = {};
31
+ for (var i = 0; i < schemaColumns.length; i++) {
32
+ columnMap[schemaColumns[i].accessor] = schemaColumns[i];
33
+ }
34
+
35
+ // Collect child keys from schema
36
+ var childKeys = [];
37
+ var level = 1;
38
+ while (schema[level]) {
39
+ if (level === 1) childKeys.push(schema[level].key);
40
+ level++;
41
+ }
42
+
43
+ // Derive display fields from record (exclude arrays and internal fields)
44
+ var fields = [];
45
+ if (record) {
46
+ var keys = Object.keys(record);
47
+ for (var i = 0; i < keys.length; i++) {
48
+ var k = keys[i];
49
+ if (k === '_tempId') continue;
50
+ if (Array.isArray(record[k])) continue;
51
+ var col = columnMap[k];
52
+ fields.push({
53
+ accessor: k,
54
+ header: col ? (col.header || k) : k,
55
+ dataType: col ? (col.dataType || 'string') : guessType(record[k])
56
+ });
57
+ }
58
+ }
59
+
60
+ function guessType(value) {
61
+ if (typeof value === 'number') return 'number';
62
+ if (typeof value === 'boolean') return 'boolean';
63
+ return 'string';
64
+ }
65
+
66
+ function getInputType(dataType) {
67
+ switch (dataType) {
68
+ case 'number': return 'number';
69
+ case 'date': return 'date';
70
+ case 'boolean': return 'checkbox';
71
+ default: return 'text';
72
+ }
73
+ }
74
+
75
+ function handleStartEdit() {
76
+ setEditing(true);
77
+ setEditRecord(Object.assign({}, record));
78
+ }
79
+
80
+ function handleFieldChange(field, value) {
81
+ setEditRecord(function(prev) {
82
+ var updated = Object.assign({}, prev);
83
+ updated[field] = value;
84
+ return updated;
85
+ });
86
+ }
87
+
88
+ function handleSaveParent() {
89
+ if (!editRecord) return;
90
+ // Queue the parent edit
91
+ var clean = Object.assign({}, editRecord);
92
+ // Remove child arrays — parent save is own fields only
93
+ for (var i = 0; i < childKeys.length; i++) {
94
+ delete clean[childKeys[i]];
95
+ }
96
+
97
+ actions.modal = { mode: 'edit', record: clean, context: { level: 'parent' } };
98
+ // Directly queue via handleSave logic
99
+ if (!clean.id && !clean._tempId) {
100
+ clean._tempId = '__inline_';
101
+ }
102
+ // Push to queue manually
103
+ actions.handleSave.__queueDirect
104
+ ? actions.handleSave.__queueDirect(clean)
105
+ : null;
106
+
107
+ // Simpler: open the edit modal which handles queueing
108
+ setEditing(false);
109
+ setEditRecord(null);
110
+ }
111
+
112
+ // Actually, simpler approach: use the existing modal flow for parent edits
113
+ function handleEditViaModal() {
114
+ actions.openEdit(record);
115
+ }
116
+
117
+ function handleCancelEdit() {
118
+ setEditing(false);
119
+ setEditRecord(null);
120
+ }
121
+
122
+ var displayRecord = editing ? editRecord : record;
123
+
124
+ return (
125
+ <div className="xeplr-table-modal-overlay" onClick={onClose}>
126
+ <div className="xeplr-table-detail" onClick={function(e) { e.stopPropagation(); }}>
127
+ <div className="xeplr-table-modal-header">
128
+ <span className="xeplr-table-modal-title">Record Detail</span>
129
+ <button type="button" className="xeplr-table-modal-close" onClick={onClose}>
130
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
131
+ <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
132
+ </svg>
133
+ </button>
134
+ </div>
135
+
136
+ <div className="xeplr-table-detail-body">
137
+ {/* ── Parent fields ── */}
138
+ <div className="xeplr-table-detail-fields">
139
+ {fields.map(function(field) {
140
+ var value = displayRecord[field.accessor];
141
+ var inputType = getInputType(field.dataType);
142
+ var isId = field.accessor === 'id';
143
+ var disabled = !editing || isId;
144
+
145
+ if (inputType === 'checkbox') {
146
+ return (
147
+ <div key={field.accessor} className="xeplr-table-detail-field">
148
+ <span className="xeplr-table-detail-label">{field.header}</span>
149
+ <input
150
+ type="checkbox"
151
+ className="xeplr-table-modal-checkbox"
152
+ checked={!!value}
153
+ disabled={disabled}
154
+ onChange={function(e) { handleFieldChange(field.accessor, e.target.checked); }}
155
+ />
156
+ </div>
157
+ );
158
+ }
159
+
160
+ return (
161
+ <div key={field.accessor} className="xeplr-table-detail-field">
162
+ <span className="xeplr-table-detail-label">
163
+ {field.header}
164
+ {isId && <span className="xeplr-table-modal-id-badge">ID</span>}
165
+ </span>
166
+ {editing ? (
167
+ <input
168
+ type={inputType}
169
+ className="xeplr-table-modal-input"
170
+ value={value != null ? value : ''}
171
+ disabled={disabled}
172
+ onChange={function(e) { handleFieldChange(field.accessor, e.target.value); }}
173
+ />
174
+ ) : (
175
+ <span className="xeplr-table-detail-value">{value != null ? String(value) : '—'}</span>
176
+ )}
177
+ </div>
178
+ );
179
+ })}
180
+ </div>
181
+
182
+ {/* ── Parent actions ── */}
183
+ <div className="xeplr-table-detail-actions">
184
+ {!editing && hasSave && (
185
+ <button type="button" className="xeplr-table-modal-btn xeplr-table-modal-btn-primary" onClick={handleEditViaModal}>
186
+ Edit
187
+ </button>
188
+ )}
189
+ </div>
190
+
191
+ {/* ── Child tables ── */}
192
+ {childKeys.map(function(ck) {
193
+ var nextLevel = schema[1];
194
+ if (!nextLevel || nextLevel.key !== ck) return null;
195
+
196
+ return (
197
+ <div key={ck} className="xeplr-table-detail-children">
198
+ <ChildTable
199
+ rootId={rootId}
200
+ path={[ck]}
201
+ schemaLevel={1}
202
+ schema={schema}
203
+ rows={record[ck] || []}
204
+ actions={actions}
205
+ />
206
+ </div>
207
+ );
208
+ })}
209
+ </div>
210
+ </div>
211
+ </div>
212
+ );
213
+ }
@@ -0,0 +1,159 @@
1
+ import React from 'react';
2
+
3
+ /**
4
+ * Modal overlay for viewing, editing, or adding a record.
5
+ *
6
+ * Form fields are auto-generated from ALL keys in the record.
7
+ * Schema columns provide display hints (header, dataType) where available.
8
+ * Fields not in schema columns still appear — with the key name as header.
9
+ *
10
+ * @param {object} props
11
+ * @param {string} props.mode - 'view' | 'edit' | 'add'
12
+ * @param {object} props.record - Current form data
13
+ * @param {Array} [props.schemaColumns] - Schema column configs for display hints
14
+ * @param {boolean} props.hasSave - Whether save action is available
15
+ * @param {Function} props.onFieldChange - (accessor, value) => void
16
+ * @param {Function} props.onSave - Save handler
17
+ * @param {Function} props.onEdit - Switch to edit mode
18
+ * @param {Function} props.onClose - Close modal
19
+ */
20
+ export default function RecordModal(props) {
21
+ var mode = props.mode;
22
+ var record = props.record;
23
+ var schemaColumns = props.schemaColumns || [];
24
+ var hasSave = props.hasSave;
25
+ var onFieldChange = props.onFieldChange;
26
+ var onSave = props.onSave;
27
+ var onEdit = props.onEdit;
28
+ var onClose = props.onClose;
29
+
30
+ var isReadOnly = mode === 'view';
31
+ var title = mode === 'view' ? 'View Record' : mode === 'edit' ? 'Edit Record' : 'Add Record';
32
+
33
+ // Build column lookup for display hints
34
+ var columnMap = {};
35
+ for (var i = 0; i < schemaColumns.length; i++) {
36
+ columnMap[schemaColumns[i].accessor] = schemaColumns[i];
37
+ }
38
+
39
+ // Derive form fields from ALL keys in the record
40
+ var fields = [];
41
+ if (record) {
42
+ var keys = Object.keys(record);
43
+ for (var i = 0; i < keys.length; i++) {
44
+ var k = keys[i];
45
+ if (k === '_tempId') continue;
46
+ // Skip array fields (those are child tables, not form fields)
47
+ if (Array.isArray(record[k])) continue;
48
+
49
+ var col = columnMap[k];
50
+ fields.push({
51
+ accessor: k,
52
+ header: col ? (col.header || k) : k,
53
+ dataType: col ? (col.dataType || 'string') : guessType(record[k])
54
+ });
55
+ }
56
+ }
57
+
58
+ function guessType(value) {
59
+ if (typeof value === 'number') return 'number';
60
+ if (typeof value === 'boolean') return 'boolean';
61
+ return 'string';
62
+ }
63
+
64
+ function getInputType(dataType) {
65
+ switch (dataType) {
66
+ case 'number': return 'number';
67
+ case 'date': return 'date';
68
+ case 'boolean': return 'checkbox';
69
+ default: return 'text';
70
+ }
71
+ }
72
+
73
+ function handleSubmit(e) {
74
+ e.preventDefault();
75
+ onSave();
76
+ }
77
+
78
+ return (
79
+ <div className="xeplr-table-modal-overlay" onClick={onClose}>
80
+ <div className="xeplr-table-modal" onClick={function(e) { e.stopPropagation(); }}>
81
+ <div className="xeplr-table-modal-header">
82
+ <span className="xeplr-table-modal-title">{title}</span>
83
+ <button type="button" className="xeplr-table-modal-close" onClick={onClose}>
84
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
85
+ <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
86
+ </svg>
87
+ </button>
88
+ </div>
89
+
90
+ <form className="xeplr-table-modal-body" onSubmit={handleSubmit}>
91
+ {fields.map(function(field) {
92
+ var value = record[field.accessor];
93
+ var inputType = getInputType(field.dataType);
94
+ var isId = field.accessor === 'id';
95
+ var disabled = isReadOnly || isId;
96
+
97
+ if (inputType === 'checkbox') {
98
+ return (
99
+ <div key={field.accessor} className="xeplr-table-modal-field">
100
+ <label className="xeplr-table-modal-label">{field.header}</label>
101
+ <input
102
+ type="checkbox"
103
+ className="xeplr-table-modal-checkbox"
104
+ checked={!!value}
105
+ disabled={disabled}
106
+ onChange={function(e) { onFieldChange(field.accessor, e.target.checked); }}
107
+ />
108
+ </div>
109
+ );
110
+ }
111
+
112
+ return (
113
+ <div key={field.accessor} className="xeplr-table-modal-field">
114
+ <label className="xeplr-table-modal-label">
115
+ {field.header}
116
+ {isId && <span className="xeplr-table-modal-id-badge">ID</span>}
117
+ </label>
118
+ <input
119
+ type={inputType}
120
+ className="xeplr-table-modal-input"
121
+ value={value != null ? value : ''}
122
+ disabled={disabled}
123
+ onChange={function(e) { onFieldChange(field.accessor, e.target.value); }}
124
+ />
125
+ </div>
126
+ );
127
+ })}
128
+
129
+ <div className="xeplr-table-modal-footer">
130
+ {isReadOnly && hasSave && (
131
+ <button
132
+ type="button"
133
+ className="xeplr-table-modal-btn xeplr-table-modal-btn-primary"
134
+ onClick={function() { onEdit(record); }}
135
+ >
136
+ Edit
137
+ </button>
138
+ )}
139
+ {!isReadOnly && (
140
+ <button
141
+ type="submit"
142
+ className="xeplr-table-modal-btn xeplr-table-modal-btn-primary"
143
+ >
144
+ Save
145
+ </button>
146
+ )}
147
+ <button
148
+ type="button"
149
+ className="xeplr-table-modal-btn"
150
+ onClick={onClose}
151
+ >
152
+ {isReadOnly ? 'Close' : 'Cancel'}
153
+ </button>
154
+ </div>
155
+ </form>
156
+ </div>
157
+ </div>
158
+ );
159
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Build a sparse deep diff between original and staged data.
3
+ * Recursive — supports N levels of nesting via schema.
4
+ *
5
+ * Rules:
6
+ * - No id → new record (include all fields)
7
+ * - Has id, changed → include id + only changed fields
8
+ * - Has id, deleted → { id, deleted: true }
9
+ * - Unchanged → omitted entirely
10
+ *
11
+ * @param {Array} originalData - Original data
12
+ * @param {Array} stagedData - Staged data after queue applied
13
+ * @param {object} schema - { 0: { key, columns }, 1: { key, columns }, ... }
14
+ * @param {number} [level] - Current nesting level (default: 0)
15
+ * @returns {Array} Sparse changeset
16
+ */
17
+ export function buildChangeSet(originalData, stagedData, schema, level) {
18
+ level = level || 0;
19
+
20
+ // Determine child key from next level in schema
21
+ var nextLevel = schema[level + 1];
22
+ var childKey = nextLevel ? nextLevel.key : null;
23
+
24
+ // Collect all child keys at deeper levels rooted under this level
25
+ var childKeys = [];
26
+ var l = level + 1;
27
+ while (schema[l]) {
28
+ if (l === level + 1) childKeys.push(schema[l].key);
29
+ l++;
30
+ }
31
+
32
+ var originalMap = new Map();
33
+ for (var i = 0; i < originalData.length; i++) {
34
+ originalMap.set(originalData[i].id, originalData[i]);
35
+ }
36
+
37
+ var stagedMap = new Map();
38
+ for (var i = 0; i < stagedData.length; i++) {
39
+ var row = stagedData[i];
40
+ var key = row._tempId || row.id;
41
+ stagedMap.set(key, row);
42
+ }
43
+
44
+ var changes = [];
45
+
46
+ // ── New and modified records ──
47
+ stagedMap.forEach(function(staged) {
48
+ var isNew = !!staged._tempId || !staged.id;
49
+
50
+ if (isNew) {
51
+ changes.push(buildNewRecord(staged, childKey));
52
+ } else {
53
+ var original = originalMap.get(staged.id);
54
+ if (!original) return;
55
+
56
+ var fieldDiff = diffFields(original, staged, childKeys);
57
+ var childDiffResult = null;
58
+
59
+ if (childKey) {
60
+ var origChildren = original[childKey] || [];
61
+ var stagedChildren = staged[childKey] || [];
62
+ var childChanges = buildChangeSet(origChildren, stagedChildren, schema, level + 1);
63
+ if (childChanges.length > 0) {
64
+ childDiffResult = childChanges;
65
+ }
66
+ }
67
+
68
+ if (fieldDiff || childDiffResult) {
69
+ var entry = fieldDiff ? Object.assign({ id: staged.id }, fieldDiff) : { id: staged.id };
70
+ if (childDiffResult) {
71
+ entry[childKey] = childDiffResult;
72
+ }
73
+ changes.push(entry);
74
+ }
75
+ }
76
+ });
77
+
78
+ // ── Deleted records ──
79
+ originalMap.forEach(function(original, id) {
80
+ if (!stagedMap.has(id)) {
81
+ changes.push({ id: id, deleted: true });
82
+ }
83
+ });
84
+
85
+ return changes;
86
+ }
87
+
88
+ /**
89
+ * Build a new record for the changeset (strip internal fields).
90
+ * Recursively cleans child arrays.
91
+ */
92
+ function buildNewRecord(staged, childKey) {
93
+ var rec = {};
94
+ var keys = Object.keys(staged);
95
+ for (var i = 0; i < keys.length; i++) {
96
+ var k = keys[i];
97
+ if (k === '_tempId') continue;
98
+
99
+ if (k === childKey && Array.isArray(staged[k])) {
100
+ rec[k] = staged[k].map(function(child) {
101
+ return buildNewRecord(child, null);
102
+ });
103
+ } else {
104
+ rec[k] = staged[k];
105
+ }
106
+ }
107
+ return rec;
108
+ }
109
+
110
+ /**
111
+ * Diff own fields (excluding id, _tempId, and child array keys).
112
+ */
113
+ function diffFields(original, staged, childKeys) {
114
+ var diff = null;
115
+ var keys = Object.keys(staged);
116
+ for (var i = 0; i < keys.length; i++) {
117
+ var k = keys[i];
118
+ if (k === 'id' || k === '_tempId') continue;
119
+ if (childKeys.indexOf(k) !== -1) continue;
120
+ if (staged[k] !== original[k]) {
121
+ if (!diff) diff = {};
122
+ diff[k] = staged[k];
123
+ }
124
+ }
125
+ return diff;
126
+ }
@@ -0,0 +1,102 @@
1
+ const TYPES = {
2
+ STRING: 'string',
3
+ NUMBER: 'number',
4
+ DATE: 'date',
5
+ BOOLEAN: 'boolean'
6
+ };
7
+
8
+ const BOOLEAN_VALUES = new Set([true, false, 0, 1, '0', '1', 'true', 'false', 'True', 'False', 'TRUE', 'FALSE']);
9
+
10
+ const MAX_SAMPLE_ROWS = 100;
11
+
12
+ /**
13
+ * Detect column data types from row data.
14
+ *
15
+ * @param {Array<object>} data - Row array
16
+ * @param {Array<object>} columns - Column defs: [{ accessor, dataType? }]
17
+ * @param {number} [minRows] - Minimum non-null samples needed for a confident detection
18
+ * @returns {{ types: Map<string, string>, confident: boolean }}
19
+ * - types: Map of accessor → 'string' | 'number' | 'date' | 'boolean'
20
+ * - confident: true if every column had enough samples (≥ minRows)
21
+ */
22
+ export function detectTypes(data, columns, minRows = 10) {
23
+ var types = new Map();
24
+ var confident = true;
25
+ var sampleRows = data.slice(0, MAX_SAMPLE_ROWS);
26
+
27
+ for (var i = 0; i < columns.length; i++) {
28
+ var col = columns[i];
29
+ var accessor = col.accessor;
30
+
31
+ // Explicit dataType — always trust it
32
+ if (col.dataType) {
33
+ types.set(accessor, col.dataType);
34
+ continue;
35
+ }
36
+
37
+ // Collect non-null, non-undefined, non-empty-string values
38
+ var values = [];
39
+ for (var r = 0; r < sampleRows.length; r++) {
40
+ var val = sampleRows[r][accessor];
41
+ if (val !== null && val !== undefined && val !== '') {
42
+ values.push(val);
43
+ }
44
+ }
45
+
46
+ // Not enough data — mark as inconclusive, fallback to string
47
+ if (values.length < minRows) {
48
+ confident = false;
49
+ types.set(accessor, values.length === 0 ? TYPES.STRING : detectSingle(values));
50
+ continue;
51
+ }
52
+
53
+ types.set(accessor, detectSingle(values));
54
+ }
55
+
56
+ return { types, confident };
57
+ }
58
+
59
+ /**
60
+ * Detect the type of a single column from its sampled values.
61
+ * Order matters: boolean → number → date → string
62
+ */
63
+ function detectSingle(values) {
64
+ // Boolean check — all values must be boolean-ish
65
+ var allBoolean = true;
66
+ for (var i = 0; i < values.length; i++) {
67
+ if (!BOOLEAN_VALUES.has(values[i])) {
68
+ allBoolean = false;
69
+ break;
70
+ }
71
+ }
72
+ if (allBoolean) return TYPES.BOOLEAN;
73
+
74
+ // Number check — all values must be numeric
75
+ var allNumber = true;
76
+ for (var i = 0; i < values.length; i++) {
77
+ if (typeof values[i] === 'boolean') { allNumber = false; break; }
78
+ var num = Number(values[i]);
79
+ if (isNaN(num)) { allNumber = false; break; }
80
+ }
81
+ if (allNumber) return TYPES.NUMBER;
82
+
83
+ // Date check — all values must parse as dates AND not be plain numbers
84
+ var allDate = true;
85
+ for (var i = 0; i < values.length; i++) {
86
+ var v = values[i];
87
+ // Skip plain numbers — they parse as dates but aren't
88
+ if (typeof v === 'number' || (typeof v === 'string' && v.trim() !== '' && !isNaN(Number(v)))) {
89
+ allDate = false;
90
+ break;
91
+ }
92
+ if (isNaN(Date.parse(v))) {
93
+ allDate = false;
94
+ break;
95
+ }
96
+ }
97
+ if (allDate) return TYPES.DATE;
98
+
99
+ return TYPES.STRING;
100
+ }
101
+
102
+ export { TYPES };
@@ -0,0 +1,66 @@
1
+ import React from 'react';
2
+ import FilterWrapper from './FilterWrapper.jsx';
3
+
4
+ /**
5
+ * Boolean filter: True / False / All toggle.
6
+ *
7
+ * Filter value shape: { value: true | false | null }
8
+ */
9
+ export default function BooleanFilter({ column }) {
10
+ var currentFilter = column.getFilterValue() || { value: null };
11
+ var isActive = currentFilter.value !== null;
12
+
13
+ function setValue(val) {
14
+ column.setFilterValue(currentFilter.value === val ? { value: null } : { value: val });
15
+ }
16
+
17
+ function handleClear() {
18
+ column.setFilterValue(undefined);
19
+ }
20
+
21
+ return (
22
+ <FilterWrapper isActive={isActive} onClear={handleClear}>
23
+ <div className="xeplr-table-boolean-filter">
24
+ <button
25
+ type="button"
26
+ className={'xeplr-table-bool-btn' + (currentFilter.value === null ? ' active' : '')}
27
+ onClick={function() { column.setFilterValue(undefined); }}
28
+ >
29
+ All
30
+ </button>
31
+ <button
32
+ type="button"
33
+ className={'xeplr-table-bool-btn' + (currentFilter.value === true ? ' active' : '')}
34
+ onClick={function() { setValue(true); }}
35
+ >
36
+ True
37
+ </button>
38
+ <button
39
+ type="button"
40
+ className={'xeplr-table-bool-btn' + (currentFilter.value === false ? ' active' : '')}
41
+ onClick={function() { setValue(false); }}
42
+ >
43
+ False
44
+ </button>
45
+ </div>
46
+ </FilterWrapper>
47
+ );
48
+ }
49
+
50
+ function toBool(val) {
51
+ if (val === true || val === 1 || val === '1' || val === 'true' || val === 'True' || val === 'TRUE') return true;
52
+ if (val === false || val === 0 || val === '0' || val === 'false' || val === 'False' || val === 'FALSE') return false;
53
+ return null;
54
+ }
55
+
56
+ /**
57
+ * Custom filter function for boolean columns.
58
+ */
59
+ export function booleanFilterFn(row, columnId, filterValue) {
60
+ if (!filterValue || filterValue.value === null || filterValue.value === undefined) return true;
61
+
62
+ var raw = row.getValue(columnId);
63
+ var bool = toBool(raw);
64
+
65
+ return bool === filterValue.value;
66
+ }