datool 0.0.1

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 (45) hide show
  1. package/README.md +218 -0
  2. package/client-dist/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
  3. package/client-dist/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
  4. package/client-dist/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
  5. package/client-dist/assets/index-BeRNeRUq.css +1 -0
  6. package/client-dist/assets/index-uoZ4c_I8.js +164 -0
  7. package/client-dist/index.html +13 -0
  8. package/index.html +12 -0
  9. package/package.json +55 -0
  10. package/src/client/App.tsx +885 -0
  11. package/src/client/components/connection-status.tsx +43 -0
  12. package/src/client/components/data-table-cell.tsx +235 -0
  13. package/src/client/components/data-table-col-icon.tsx +73 -0
  14. package/src/client/components/data-table-header-col.tsx +225 -0
  15. package/src/client/components/data-table-search-input.tsx +729 -0
  16. package/src/client/components/data-table.tsx +2014 -0
  17. package/src/client/components/stream-controls.tsx +157 -0
  18. package/src/client/components/theme-provider.tsx +230 -0
  19. package/src/client/components/ui/button.tsx +68 -0
  20. package/src/client/components/ui/combobox.tsx +308 -0
  21. package/src/client/components/ui/context-menu.tsx +261 -0
  22. package/src/client/components/ui/dropdown-menu.tsx +267 -0
  23. package/src/client/components/ui/input-group.tsx +153 -0
  24. package/src/client/components/ui/input.tsx +19 -0
  25. package/src/client/components/ui/textarea.tsx +18 -0
  26. package/src/client/components/viewer-settings.tsx +185 -0
  27. package/src/client/index.css +192 -0
  28. package/src/client/lib/data-table-search.ts +750 -0
  29. package/src/client/lib/datool-icons.ts +37 -0
  30. package/src/client/lib/datool-url-state.ts +159 -0
  31. package/src/client/lib/filterable-table.ts +146 -0
  32. package/src/client/lib/table-search-persistence.ts +94 -0
  33. package/src/client/lib/utils.ts +6 -0
  34. package/src/client/main.tsx +14 -0
  35. package/src/index.ts +19 -0
  36. package/src/node/cli.ts +54 -0
  37. package/src/node/config.ts +231 -0
  38. package/src/node/lines.ts +82 -0
  39. package/src/node/runtime.ts +102 -0
  40. package/src/node/server.ts +403 -0
  41. package/src/node/sources/command.ts +82 -0
  42. package/src/node/sources/file.ts +116 -0
  43. package/src/node/sources/ssh.ts +59 -0
  44. package/src/shared/columns.ts +41 -0
  45. package/src/shared/types.ts +188 -0
@@ -0,0 +1,750 @@
1
+ export type DataTableSearchFieldKind =
2
+ | "date"
3
+ | "enum"
4
+ | "json"
5
+ | "number"
6
+ | "text"
7
+
8
+ export type DataTableSearchField<Row> = {
9
+ getValue: (row: Row) => unknown
10
+ id: string
11
+ kind: DataTableSearchFieldKind
12
+ options?: string[]
13
+ sample?: string
14
+ }
15
+
16
+ export type DataTableSearchSuggestion = {
17
+ group: "filters" | "input" | "values"
18
+ id: string
19
+ insertText: string
20
+ keepOpen?: boolean
21
+ label: string
22
+ mode: "append" | "replace-token" | "replace-whole"
23
+ }
24
+
25
+ export type DataTableSearchFilterClause = {
26
+ operator: ":" | "<" | ">"
27
+ value: string
28
+ }
29
+
30
+ export type DataTableSearchTokenOperator = ":" | "." | "<" | ">"
31
+
32
+ export type DataTableSearchSelectorRange = {
33
+ end: number
34
+ fieldId: string
35
+ operator: DataTableSearchTokenOperator
36
+ start: number
37
+ token: string
38
+ }
39
+
40
+ type DataTableSearchTokenRange = {
41
+ end: number
42
+ start: number
43
+ token: string
44
+ }
45
+
46
+ function withSuggestionMode(
47
+ suggestion: DataTableSearchSuggestion,
48
+ mode: DataTableSearchSuggestion["mode"]
49
+ ): DataTableSearchSuggestion {
50
+ return {
51
+ ...suggestion,
52
+ mode,
53
+ }
54
+ }
55
+
56
+ type TokenContext<Row> = {
57
+ end: number
58
+ field?: DataTableSearchField<Row>
59
+ fragment: string
60
+ operator?: DataTableSearchTokenOperator
61
+ start: number
62
+ token: string
63
+ }
64
+
65
+ type MatchedSearchToken<Row> = {
66
+ field?: DataTableSearchField<Row>
67
+ fieldId: string
68
+ fragment: string
69
+ operator: DataTableSearchTokenOperator
70
+ }
71
+
72
+ function matchSearchToken<Row>(
73
+ token: string,
74
+ fields: DataTableSearchField<Row>[]
75
+ ): MatchedSearchToken<Row> | null {
76
+ const match = token.match(/^([a-zA-Z0-9_.-]+)(:|>|<|\.)(.*)$/)
77
+
78
+ if (!match) {
79
+ return null
80
+ }
81
+
82
+ const [, fieldId, operator, fragment] = match
83
+
84
+ return {
85
+ field: fields.find((item) => item.id === fieldId),
86
+ fieldId,
87
+ fragment,
88
+ operator: operator as DataTableSearchTokenOperator,
89
+ }
90
+ }
91
+
92
+ function isSupportedOperator<Row>(
93
+ field: DataTableSearchField<Row>,
94
+ operator: DataTableSearchTokenOperator
95
+ ) {
96
+ switch (field.kind) {
97
+ case "date":
98
+ case "number":
99
+ return operator === ":" || operator === ">" || operator === "<"
100
+ case "json":
101
+ return operator === ":" || operator === "."
102
+ default:
103
+ return operator === ":"
104
+ }
105
+ }
106
+
107
+ function stringifyValue(value: unknown): string {
108
+ if (value === null || value === undefined) {
109
+ return ""
110
+ }
111
+
112
+ if (value instanceof Date) {
113
+ return value.toISOString()
114
+ }
115
+
116
+ if (typeof value === "object") {
117
+ return JSON.stringify(value)
118
+ }
119
+
120
+ return String(value)
121
+ }
122
+
123
+ function getSearchTokenRanges(value: string) {
124
+ const ranges: DataTableSearchTokenRange[] = []
125
+ let tokenStart = -1
126
+ let isQuoted = false
127
+ let isEscaped = false
128
+
129
+ for (let index = 0; index < value.length; index += 1) {
130
+ const character = value[index]
131
+
132
+ if (tokenStart === -1) {
133
+ if (/\s/.test(character)) {
134
+ continue
135
+ }
136
+
137
+ tokenStart = index
138
+ }
139
+
140
+ if (isEscaped) {
141
+ isEscaped = false
142
+ continue
143
+ }
144
+
145
+ if (character === "\\") {
146
+ isEscaped = true
147
+ continue
148
+ }
149
+
150
+ if (character === '"') {
151
+ isQuoted = !isQuoted
152
+ continue
153
+ }
154
+
155
+ if (!isQuoted && /\s/.test(character)) {
156
+ ranges.push({
157
+ end: index,
158
+ start: tokenStart,
159
+ token: value.slice(tokenStart, index),
160
+ })
161
+ tokenStart = -1
162
+ }
163
+ }
164
+
165
+ if (tokenStart !== -1) {
166
+ ranges.push({
167
+ end: value.length,
168
+ start: tokenStart,
169
+ token: value.slice(tokenStart),
170
+ })
171
+ }
172
+
173
+ return ranges
174
+ }
175
+
176
+ export function splitSearchQuery(value: string) {
177
+ return getSearchTokenRanges(value).map((range) => range.token)
178
+ }
179
+
180
+ function unescapeSearchTokenValue(value: string) {
181
+ return value.replace(/\\(["\\])/g, "$1")
182
+ }
183
+
184
+ function unquoteSearchTokenValue(value: string) {
185
+ const trimmedValue = value.trim()
186
+
187
+ if (
188
+ trimmedValue.length >= 2 &&
189
+ trimmedValue.startsWith('"') &&
190
+ trimmedValue.endsWith('"')
191
+ ) {
192
+ return unescapeSearchTokenValue(trimmedValue.slice(1, -1))
193
+ }
194
+
195
+ return unescapeSearchTokenValue(trimmedValue)
196
+ }
197
+
198
+ export function quoteSearchTokenValue(value: string) {
199
+ if (!value || /[\s"]/u.test(value)) {
200
+ return `"${value.replace(/["\\]/g, "\\$&")}"`
201
+ }
202
+
203
+ return value
204
+ }
205
+
206
+ function parseDateOperand(value: string) {
207
+ const normalized = value.trim().toLowerCase()
208
+
209
+ if (!normalized) {
210
+ return null
211
+ }
212
+
213
+ if (normalized === "today") {
214
+ const now = new Date()
215
+
216
+ return new Date(now.getFullYear(), now.getMonth(), now.getDate())
217
+ }
218
+
219
+ const relativeMatch = normalized.match(/^-(\d+)d$/)
220
+
221
+ if (relativeMatch) {
222
+ const days = Number(relativeMatch[1])
223
+
224
+ return new Date(Date.now() - days * 24 * 60 * 60 * 1000)
225
+ }
226
+
227
+ const timestamp = Date.parse(value)
228
+
229
+ if (Number.isNaN(timestamp)) {
230
+ return null
231
+ }
232
+
233
+ return new Date(timestamp)
234
+ }
235
+
236
+ function sameCalendarDay(left: Date, right: Date) {
237
+ return (
238
+ left.getFullYear() === right.getFullYear() &&
239
+ left.getMonth() === right.getMonth() &&
240
+ left.getDate() === right.getDate()
241
+ )
242
+ }
243
+
244
+ function matchFieldToken<Row>(
245
+ row: Row,
246
+ field: DataTableSearchField<Row>,
247
+ operator: ":" | "<" | ">",
248
+ rawValue: string
249
+ ) {
250
+ const fieldValue = field.getValue(row)
251
+ const stringValue = stringifyValue(fieldValue)
252
+
253
+ if (!rawValue.trim()) {
254
+ return true
255
+ }
256
+
257
+ if (field.kind === "enum") {
258
+ return stringValue.toLowerCase() === rawValue.trim().toLowerCase()
259
+ }
260
+
261
+ if (field.kind === "number") {
262
+ const left = Number(fieldValue)
263
+ const right = Number(rawValue)
264
+
265
+ if (Number.isNaN(left) || Number.isNaN(right)) {
266
+ return stringValue.toLowerCase().includes(rawValue.trim().toLowerCase())
267
+ }
268
+
269
+ if (operator === ">") {
270
+ return left > right
271
+ }
272
+
273
+ if (operator === "<") {
274
+ return left < right
275
+ }
276
+
277
+ return left === right
278
+ }
279
+
280
+ if (field.kind === "date") {
281
+ const nestedOperatorMatch =
282
+ operator === ":" ? rawValue.match(/^([<>])(.*)$/) : null
283
+ const effectiveOperator = nestedOperatorMatch
284
+ ? (nestedOperatorMatch[1] as "<" | ">")
285
+ : operator
286
+ const effectiveValue = nestedOperatorMatch
287
+ ? nestedOperatorMatch[2]
288
+ : rawValue
289
+ const left = parseDateOperand(stringValue)
290
+ const right = parseDateOperand(effectiveValue)
291
+
292
+ if (!left || !right) {
293
+ return stringValue.toLowerCase().includes(rawValue.trim().toLowerCase())
294
+ }
295
+
296
+ if (effectiveOperator === ">") {
297
+ return left > right
298
+ }
299
+
300
+ if (effectiveOperator === "<") {
301
+ return left < right
302
+ }
303
+
304
+ return sameCalendarDay(left, right)
305
+ }
306
+
307
+ return stringValue.toLowerCase().includes(rawValue.trim().toLowerCase())
308
+ }
309
+
310
+ export function matchesFieldClauses<Row>(
311
+ row: Row,
312
+ field: DataTableSearchField<Row>,
313
+ clauses: DataTableSearchFilterClause[]
314
+ ) {
315
+ return clauses.every((clause) =>
316
+ matchFieldToken(row, field, clause.operator, clause.value)
317
+ )
318
+ }
319
+
320
+ function buildBaseSuggestions<Row>(fields: DataTableSearchField<Row>[]) {
321
+ return fields.flatMap<DataTableSearchSuggestion>((field) => {
322
+ switch (field.kind) {
323
+ case "date":
324
+ return [
325
+ {
326
+ id: `${field.id}-eq`,
327
+ group: "filters",
328
+ insertText: `${field.id}:`,
329
+ keepOpen: true,
330
+ label: `${field.id}:`,
331
+ mode: "append",
332
+ },
333
+ {
334
+ id: `${field.id}-gt`,
335
+ group: "filters",
336
+ insertText: `${field.id}>`,
337
+ keepOpen: true,
338
+ label: `${field.id}>`,
339
+ mode: "append",
340
+ },
341
+ {
342
+ id: `${field.id}-lt`,
343
+ group: "filters",
344
+ insertText: `${field.id}<`,
345
+ keepOpen: true,
346
+ label: `${field.id}<`,
347
+ mode: "append",
348
+ },
349
+ ]
350
+ case "number":
351
+ return [
352
+ {
353
+ id: `${field.id}-eq`,
354
+ group: "filters",
355
+ insertText: `${field.id}:`,
356
+ keepOpen: true,
357
+ label: `${field.id}:`,
358
+ mode: "append",
359
+ },
360
+ {
361
+ id: `${field.id}-gt`,
362
+ group: "filters",
363
+ insertText: `${field.id}>`,
364
+ keepOpen: true,
365
+ label: `${field.id}>`,
366
+ mode: "append",
367
+ },
368
+ {
369
+ id: `${field.id}-lt`,
370
+ group: "filters",
371
+ insertText: `${field.id}<`,
372
+ keepOpen: true,
373
+ label: `${field.id}<`,
374
+ mode: "append",
375
+ },
376
+ ]
377
+ case "json":
378
+ return [
379
+ {
380
+ id: `${field.id}-dot`,
381
+ group: "filters",
382
+ insertText: `${field.id}.`,
383
+ keepOpen: true,
384
+ label: `${field.id}.`,
385
+ mode: "append",
386
+ },
387
+ {
388
+ id: `${field.id}-eq`,
389
+ group: "filters",
390
+ insertText: `${field.id}:`,
391
+ keepOpen: true,
392
+ label: `${field.id}:`,
393
+ mode: "append",
394
+ },
395
+ ]
396
+ default:
397
+ return [
398
+ {
399
+ id: `${field.id}-eq`,
400
+ group: "filters",
401
+ insertText: `${field.id}:`,
402
+ keepOpen: true,
403
+ label: `${field.id}:`,
404
+ mode: "append",
405
+ },
406
+ ]
407
+ }
408
+ })
409
+ }
410
+
411
+ export function getTokenContext<Row>(
412
+ value: string,
413
+ cursor: number,
414
+ fields: DataTableSearchField<Row>[]
415
+ ): TokenContext<Row> {
416
+ const tokenRange = getSearchTokenRanges(value).find(
417
+ (range) => cursor >= range.start && cursor <= range.end
418
+ )
419
+ const start = tokenRange?.start ?? cursor
420
+ const end = tokenRange?.end ?? cursor
421
+ const token = tokenRange?.token ?? ""
422
+ const match = matchSearchToken(token, fields)
423
+
424
+ if (!match) {
425
+ return {
426
+ end,
427
+ fragment: token,
428
+ start,
429
+ token,
430
+ }
431
+ }
432
+
433
+ return {
434
+ end,
435
+ field: match.field,
436
+ fragment: match.fragment,
437
+ operator: match.operator,
438
+ start,
439
+ token,
440
+ }
441
+ }
442
+
443
+ export function getSelectorHighlightRanges<Row>(
444
+ value: string,
445
+ fields: DataTableSearchField<Row>[]
446
+ ) {
447
+ const ranges: DataTableSearchSelectorRange[] = []
448
+
449
+ for (const { end, start, token } of getSearchTokenRanges(value)) {
450
+ const parsedToken = matchSearchToken(token, fields)
451
+
452
+ if (!parsedToken?.field) {
453
+ continue
454
+ }
455
+
456
+ if (!isSupportedOperator(parsedToken.field, parsedToken.operator)) {
457
+ continue
458
+ }
459
+
460
+ ranges.push({
461
+ end,
462
+ fieldId: parsedToken.field.id,
463
+ operator: parsedToken.operator,
464
+ start,
465
+ token,
466
+ })
467
+ }
468
+
469
+ return ranges
470
+ }
471
+
472
+ export function getSearchSuggestions<Row>(
473
+ value: string,
474
+ cursor: number,
475
+ fields: DataTableSearchField<Row>[]
476
+ ) {
477
+ const token = getTokenContext(value, cursor, fields)
478
+ const hasTokenFragment = token.fragment.trim().length > 0
479
+ const inputSuggestion = value
480
+ ? [
481
+ {
482
+ group: "input" as const,
483
+ id: "input-value",
484
+ insertText: value,
485
+ label: value,
486
+ mode: "replace-whole" as const,
487
+ },
488
+ ]
489
+ : []
490
+
491
+ if (!token.field || !token.operator) {
492
+ const baseSuggestions = buildBaseSuggestions(fields)
493
+ const filteredSuggestions = !token.fragment.trim()
494
+ ? baseSuggestions
495
+ : baseSuggestions.filter((suggestion) =>
496
+ suggestion.label
497
+ .toLowerCase()
498
+ .includes(token.fragment.trim().toLowerCase())
499
+ )
500
+
501
+ if (hasTokenFragment) {
502
+ return [
503
+ ...inputSuggestion,
504
+ ...filteredSuggestions.map((suggestion) =>
505
+ withSuggestionMode(suggestion, "replace-token")
506
+ ),
507
+ ]
508
+ }
509
+
510
+ return [...inputSuggestion, ...filteredSuggestions]
511
+ }
512
+
513
+ if (token.field.kind === "enum" && token.operator === ":") {
514
+ const selectedValue = unquoteSearchTokenValue(token.fragment).toLowerCase()
515
+
516
+ if (
517
+ selectedValue &&
518
+ (token.field.options ?? []).some(
519
+ (option) => option.toLowerCase() === selectedValue
520
+ )
521
+ ) {
522
+ return inputSuggestion
523
+ }
524
+
525
+ const valueSuggestions = (token.field.options ?? [])
526
+ .filter((option) => option.toLowerCase().includes(selectedValue))
527
+ .map<DataTableSearchSuggestion>((option) => ({
528
+ group: "values",
529
+ id: `${token.field?.id}-${option}`,
530
+ insertText: `${token.field?.id}:${quoteSearchTokenValue(option)}`,
531
+ label: option,
532
+ mode: "replace-token",
533
+ }))
534
+
535
+ return [...inputSuggestion, ...valueSuggestions]
536
+ }
537
+
538
+ if (token.field.kind === "date") {
539
+ const sample = token.field.sample ?? new Date().toISOString()
540
+ const suggestions = [
541
+ {
542
+ group: "values" as const,
543
+ id: `${token.field.id}-today`,
544
+ insertText: `${token.field.id}${token.operator}today`,
545
+ label: "today",
546
+ mode: "replace-token" as const,
547
+ },
548
+ {
549
+ group: "values" as const,
550
+ id: `${token.field.id}-7d`,
551
+ insertText: `${token.field.id}${token.operator}-7d`,
552
+ label: "-7d",
553
+ mode: "replace-token" as const,
554
+ },
555
+ {
556
+ group: "values" as const,
557
+ id: `${token.field.id}-sample`,
558
+ insertText: `${token.field.id}${token.operator}${sample}`,
559
+ label: sample,
560
+ mode: "replace-token" as const,
561
+ },
562
+ ]
563
+
564
+ if (token.operator === ":") {
565
+ suggestions[1] = {
566
+ group: "values",
567
+ id: `${token.field.id}-7d`,
568
+ insertText: `${token.field.id}:>-7d`,
569
+ label: ">-7d",
570
+ mode: "replace-token",
571
+ }
572
+ suggestions[2] = {
573
+ group: "values",
574
+ id: `${token.field.id}-sample`,
575
+ insertText: `${token.field.id}:>${sample}`,
576
+ label: `>${sample}`,
577
+ mode: "replace-token",
578
+ }
579
+ }
580
+
581
+ const filteredSuggestions = !token.fragment.trim()
582
+ ? suggestions
583
+ : suggestions.filter((suggestion) =>
584
+ suggestion.label
585
+ .toLowerCase()
586
+ .includes(token.fragment.trim().toLowerCase())
587
+ )
588
+
589
+ return [...inputSuggestion, ...filteredSuggestions]
590
+ }
591
+
592
+ const filteredSuggestions = buildBaseSuggestions([token.field]).filter(
593
+ (suggestion) =>
594
+ suggestion.label.toLowerCase().includes(token.token.trim().toLowerCase())
595
+ )
596
+
597
+ return [...inputSuggestion, ...filteredSuggestions]
598
+ }
599
+
600
+ export function applySuggestionToValue<Row>(
601
+ value: string,
602
+ cursor: number,
603
+ fields: DataTableSearchField<Row>[],
604
+ suggestion: DataTableSearchSuggestion
605
+ ) {
606
+ const token = getTokenContext(value, cursor, fields)
607
+ const nextValue =
608
+ suggestion.mode === "replace-whole"
609
+ ? suggestion.insertText
610
+ : suggestion.mode === "append"
611
+ ? `${value.trimEnd()}${value.trim() ? " " : ""}${suggestion.insertText}`
612
+ : `${value.slice(0, token.start)}${suggestion.insertText}${value.slice(token.end)}`
613
+ const nextCursor =
614
+ suggestion.mode === "append"
615
+ ? nextValue.length
616
+ : suggestion.mode === "replace-whole"
617
+ ? suggestion.insertText.length
618
+ : token.start + suggestion.insertText.length
619
+
620
+ return {
621
+ keepOpen: Boolean(suggestion.keepOpen),
622
+ value: nextValue,
623
+ selectionStart: nextCursor,
624
+ }
625
+ }
626
+
627
+ export function parseSearchQuery<Row>(
628
+ query: string,
629
+ fields: DataTableSearchField<Row>[]
630
+ ) {
631
+ const trimmedQuery = query.trim()
632
+
633
+ if (!trimmedQuery) {
634
+ return {
635
+ columnFilters: [] as Array<{
636
+ id: string
637
+ value: DataTableSearchFilterClause[]
638
+ }>,
639
+ globalFilter: "",
640
+ }
641
+ }
642
+
643
+ const tokens = splitSearchQuery(trimmedQuery)
644
+ const columnFiltersMap = new Map<string, DataTableSearchFilterClause[]>()
645
+ const globalTokens: string[] = []
646
+
647
+ tokens.forEach((token) => {
648
+ const match = token.match(/^([a-zA-Z0-9_.-]+)(:|>|<)(.*)$/)
649
+
650
+ if (!match) {
651
+ globalTokens.push(token)
652
+ return
653
+ }
654
+
655
+ const [, fieldId, operator, rawValue] = match
656
+ const field = fields.find((item) => item.id === fieldId)
657
+
658
+ if (!field) {
659
+ globalTokens.push(token)
660
+ return
661
+ }
662
+
663
+ const existingClauses = columnFiltersMap.get(field.id) ?? []
664
+
665
+ existingClauses.push({
666
+ operator: operator as ":" | "<" | ">",
667
+ value: unquoteSearchTokenValue(rawValue),
668
+ })
669
+
670
+ columnFiltersMap.set(field.id, existingClauses)
671
+ })
672
+
673
+ return {
674
+ columnFilters: Array.from(columnFiltersMap.entries()).map(
675
+ ([id, value]) => ({
676
+ id,
677
+ value,
678
+ })
679
+ ),
680
+ globalFilter: globalTokens.join(" "),
681
+ }
682
+ }
683
+
684
+ export function matchesSearch<Row>(
685
+ row: Row,
686
+ query: string,
687
+ fields: DataTableSearchField<Row>[]
688
+ ) {
689
+ const parsedQuery = parseSearchQuery(query, fields)
690
+ const fallbackValue = fields
691
+ .map((field) => stringifyValue(field.getValue(row)).toLowerCase())
692
+ .join("\n")
693
+
694
+ const matchesGlobalFilter = parsedQuery.globalFilter
695
+ ? parsedQuery.globalFilter
696
+ .split(/\s+/)
697
+ .filter(Boolean)
698
+ .every((token) => fallbackValue.includes(token.toLowerCase()))
699
+ : true
700
+
701
+ if (!matchesGlobalFilter) {
702
+ return false
703
+ }
704
+
705
+ return parsedQuery.columnFilters.every((columnFilter) => {
706
+ const field = fields.find((item) => item.id === columnFilter.id)
707
+
708
+ if (!field) {
709
+ return true
710
+ }
711
+
712
+ return matchesFieldClauses(row, field, columnFilter.value)
713
+ })
714
+ }
715
+
716
+ export function getColumnHighlightTerms<Row>(
717
+ query: string,
718
+ columnId: string,
719
+ fields: DataTableSearchField<Row>[]
720
+ ) {
721
+ const parsedQuery = parseSearchQuery(query, fields)
722
+ const field = fields.find((item) => item.id === columnId)
723
+ const globalTerms = parsedQuery.globalFilter.split(/\s+/).filter(Boolean)
724
+
725
+ if (!field || field.kind !== "text") {
726
+ return globalTerms
727
+ }
728
+
729
+ const columnTerms =
730
+ parsedQuery.columnFilters
731
+ .find((columnFilter) => columnFilter.id === columnId)
732
+ ?.value.filter((clause) => clause.operator === ":" && clause.value.trim())
733
+ .map((clause) => clause.value.trim()) ?? []
734
+
735
+ return [...globalTerms, ...columnTerms]
736
+ }
737
+
738
+ export function buildEnumOptions<Row>(
739
+ rows: Row[],
740
+ getValue: (row: Row) => unknown
741
+ ) {
742
+ return Array.from(
743
+ new Set(
744
+ rows
745
+ .map((row) => stringifyValue(getValue(row)).trim())
746
+ .filter(Boolean)
747
+ .map((value) => value.toUpperCase())
748
+ )
749
+ )
750
+ }