@gradio/dataframe 0.22.0 → 0.23.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 (128) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/Dataframe.test.ts +697 -32
  3. package/Index.svelte +107 -106
  4. package/dist/Index.svelte +107 -106
  5. package/dist/Index.svelte.d.ts +0 -1
  6. package/dist/shared/BooleanCell.svelte +12 -6
  7. package/dist/shared/BooleanCell.svelte.d.ts +4 -18
  8. package/dist/shared/CellMenu.svelte +74 -49
  9. package/dist/shared/CellMenu.svelte.d.ts +5 -19
  10. package/dist/shared/CellMenuButton.svelte +3 -3
  11. package/dist/shared/CellMenuButton.svelte.d.ts +4 -18
  12. package/dist/shared/CellMenuIcons.svelte +1 -1
  13. package/dist/shared/CellMenuIcons.svelte.d.ts +4 -18
  14. package/dist/shared/DataCell.svelte +189 -0
  15. package/dist/shared/DataCell.svelte.d.ts +44 -0
  16. package/dist/shared/EditableCell.svelte +88 -64
  17. package/dist/shared/EditableCell.svelte.d.ts +8 -26
  18. package/dist/shared/EmptyRowButton.svelte +2 -2
  19. package/dist/shared/EmptyRowButton.svelte.d.ts +4 -18
  20. package/dist/shared/FilterMenu.svelte +26 -18
  21. package/dist/shared/FilterMenu.svelte.d.ts +5 -21
  22. package/dist/shared/HeaderCell.svelte +218 -0
  23. package/dist/shared/HeaderCell.svelte.d.ts +30 -0
  24. package/dist/shared/RowNumber.svelte +4 -2
  25. package/dist/shared/RowNumber.svelte.d.ts +4 -18
  26. package/dist/shared/Table.svelte +1037 -949
  27. package/dist/shared/Table.svelte.d.ts +12 -30
  28. package/dist/shared/Toolbar.svelte +32 -22
  29. package/dist/shared/Toolbar.svelte.d.ts +6 -21
  30. package/dist/shared/column_measurement.svelte.d.ts +20 -0
  31. package/dist/shared/column_measurement.svelte.js +79 -0
  32. package/dist/shared/icons/FilterIcon.svelte +14 -6
  33. package/dist/shared/icons/FilterIcon.svelte.d.ts +14 -6
  34. package/dist/shared/icons/Padlock.svelte +6 -2
  35. package/dist/shared/icons/Padlock.svelte.d.ts +4 -25
  36. package/dist/shared/icons/SelectionButtons.svelte +23 -8
  37. package/dist/shared/icons/SelectionButtons.svelte.d.ts +4 -18
  38. package/dist/shared/icons/SortButtonDown.svelte +15 -4
  39. package/dist/shared/icons/SortButtonDown.svelte.d.ts +4 -25
  40. package/dist/shared/icons/SortButtonUp.svelte +15 -5
  41. package/dist/shared/icons/SortButtonUp.svelte.d.ts +4 -25
  42. package/dist/shared/icons/SortIcon.svelte +13 -7
  43. package/dist/shared/icons/SortIcon.svelte.d.ts +7 -21
  44. package/dist/shared/tanstack/index.d.ts +3 -0
  45. package/dist/shared/tanstack/index.js +3 -0
  46. package/dist/shared/tanstack/table.svelte.d.ts +26 -0
  47. package/dist/shared/tanstack/table.svelte.js +102 -0
  48. package/dist/shared/tanstack/virtual.svelte.d.ts +14 -0
  49. package/dist/shared/tanstack/virtual.svelte.js +76 -0
  50. package/dist/shared/types.d.ts +2 -0
  51. package/dist/shared/utils/filter.d.ts +5 -0
  52. package/dist/shared/utils/filter.js +62 -0
  53. package/dist/shared/utils/selection_utils.js +1 -1
  54. package/dist/shared/utils/table_utils.d.ts +1 -14
  55. package/dist/shared/utils/table_utils.js +0 -61
  56. package/dist/standalone/Index.svelte +71 -46
  57. package/dist/standalone/Index.svelte.d.ts +4 -23
  58. package/dist/types.d.ts +32 -0
  59. package/dist/types.js +1 -0
  60. package/package.json +10 -9
  61. package/shared/BooleanCell.svelte +12 -6
  62. package/shared/CellMenu.svelte +74 -49
  63. package/shared/CellMenuButton.svelte +3 -3
  64. package/shared/CellMenuIcons.svelte +1 -1
  65. package/shared/DataCell.svelte +189 -0
  66. package/shared/EditableCell.svelte +88 -64
  67. package/shared/EmptyRowButton.svelte +2 -2
  68. package/shared/FilterMenu.svelte +26 -18
  69. package/shared/HeaderCell.svelte +218 -0
  70. package/shared/RowNumber.svelte +4 -2
  71. package/shared/Table.svelte +1037 -949
  72. package/shared/Toolbar.svelte +32 -22
  73. package/shared/column_measurement.svelte.ts +103 -0
  74. package/shared/icons/FilterIcon.svelte +14 -6
  75. package/shared/icons/Padlock.svelte +6 -2
  76. package/shared/icons/SelectionButtons.svelte +23 -8
  77. package/shared/icons/SortButtonDown.svelte +15 -4
  78. package/shared/icons/SortButtonUp.svelte +15 -5
  79. package/shared/icons/SortIcon.svelte +13 -7
  80. package/shared/tanstack/index.ts +23 -0
  81. package/shared/tanstack/table.svelte.ts +117 -0
  82. package/shared/tanstack/virtual.svelte.ts +105 -0
  83. package/shared/types.ts +3 -0
  84. package/shared/utils/filter.ts +66 -0
  85. package/shared/utils/selection_utils.ts +1 -1
  86. package/shared/utils/table_utils.ts +1 -100
  87. package/standalone/Index.svelte +71 -46
  88. package/test/filter.test.ts +350 -0
  89. package/test/selection_utils.test.ts +416 -0
  90. package/test/table_utils.test.ts +45 -112
  91. package/types.ts +34 -0
  92. package/dist/shared/TableCell.svelte +0 -330
  93. package/dist/shared/TableCell.svelte.d.ts +0 -66
  94. package/dist/shared/TableHeader.svelte +0 -344
  95. package/dist/shared/TableHeader.svelte.d.ts +0 -61
  96. package/dist/shared/VirtualTable.svelte +0 -379
  97. package/dist/shared/VirtualTable.svelte.d.ts +0 -42
  98. package/dist/shared/context/dataframe_context.d.ts +0 -180
  99. package/dist/shared/context/dataframe_context.js +0 -412
  100. package/dist/shared/icons/SortArrowDown.svelte +0 -25
  101. package/dist/shared/icons/SortArrowDown.svelte.d.ts +0 -20
  102. package/dist/shared/icons/SortArrowUp.svelte +0 -25
  103. package/dist/shared/icons/SortArrowUp.svelte.d.ts +0 -20
  104. package/dist/shared/utils/data_processing.d.ts +0 -14
  105. package/dist/shared/utils/data_processing.js +0 -47
  106. package/dist/shared/utils/drag_utils.d.ts +0 -15
  107. package/dist/shared/utils/drag_utils.js +0 -67
  108. package/dist/shared/utils/filter_utils.d.ts +0 -29
  109. package/dist/shared/utils/filter_utils.js +0 -123
  110. package/dist/shared/utils/keyboard_utils.d.ts +0 -3
  111. package/dist/shared/utils/keyboard_utils.js +0 -236
  112. package/dist/shared/utils/menu_utils.d.ts +0 -42
  113. package/dist/shared/utils/menu_utils.js +0 -58
  114. package/dist/shared/utils/sort_utils.d.ts +0 -26
  115. package/dist/shared/utils/sort_utils.js +0 -59
  116. package/shared/TableCell.svelte +0 -330
  117. package/shared/TableHeader.svelte +0 -344
  118. package/shared/VirtualTable.svelte +0 -379
  119. package/shared/context/dataframe_context.ts +0 -701
  120. package/shared/icons/SortArrowDown.svelte +0 -25
  121. package/shared/icons/SortArrowUp.svelte +0 -25
  122. package/shared/utils/data_processing.ts +0 -77
  123. package/shared/utils/drag_utils.ts +0 -105
  124. package/shared/utils/filter_utils.ts +0 -208
  125. package/shared/utils/keyboard_utils.ts +0 -309
  126. package/shared/utils/menu_utils.ts +0 -115
  127. package/shared/utils/sort_utils.ts +0 -91
  128. package/test/sort_utils.test.ts +0 -323
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Custom filter function for TanStack Table that handles Gradio's
3
+ * { dtype, filter, value } column filter format.
4
+ */
5
+ export function gradio_filter_fn(
6
+ row: any,
7
+ columnId: string,
8
+ filterValue: any
9
+ ): boolean {
10
+ const { dtype, filter, value: fval } = filterValue;
11
+ const cell_value = String(row.getValue(columnId) ?? "");
12
+ const compare_val = fval ?? "";
13
+
14
+ if (dtype === "number") {
15
+ const num = parseFloat(cell_value);
16
+ const target = parseFloat(compare_val);
17
+ if (isNaN(num) || isNaN(target)) {
18
+ if (filter === "Is empty") return cell_value.trim() === "";
19
+ if (filter === "Is not empty") return cell_value.trim() !== "";
20
+ return true;
21
+ }
22
+ switch (filter) {
23
+ case "=":
24
+ return num === target;
25
+ case "≠":
26
+ return num !== target;
27
+ case ">":
28
+ return num > target;
29
+ case "<":
30
+ return num < target;
31
+ case "≥":
32
+ return num >= target;
33
+ case "≤":
34
+ return num <= target;
35
+ case "Is empty":
36
+ return cell_value.trim() === "";
37
+ case "Is not empty":
38
+ return cell_value.trim() !== "";
39
+ default:
40
+ return true;
41
+ }
42
+ }
43
+
44
+ const lower = cell_value.toLowerCase();
45
+ const target_lower = compare_val.toLowerCase();
46
+ switch (filter) {
47
+ case "Contains":
48
+ return lower.includes(target_lower);
49
+ case "Does not contain":
50
+ return !lower.includes(target_lower);
51
+ case "Starts with":
52
+ return lower.startsWith(target_lower);
53
+ case "Ends with":
54
+ return lower.endsWith(target_lower);
55
+ case "Is":
56
+ return lower === target_lower;
57
+ case "Is not":
58
+ return lower !== target_lower;
59
+ case "Is empty":
60
+ return cell_value.trim() === "";
61
+ case "Is not empty":
62
+ return cell_value.trim() !== "";
63
+ default:
64
+ return true;
65
+ }
66
+ }
@@ -37,7 +37,7 @@ export function get_range_selection(
37
37
  const max_col = Math.max(start_col, end_col);
38
38
 
39
39
  const cells: CellCoordinate[] = [];
40
- // Add the start cell as the "anchor" cell so that when
40
+ // add the start cell as the "anchor" cell so that when
41
41
  // we press shift+arrow keys, the selection will always
42
42
  // include the anchor cell.
43
43
  cells.push(start);
@@ -1,14 +1,4 @@
1
- import type {
2
- CellValue,
3
- Headers,
4
- HeadersWithIDs,
5
- TableCell,
6
- TableData
7
- } from "../types";
8
- import { sort_data } from "./sort_utils";
9
- import { filter_data } from "./filter_utils";
10
- import type { SortDirection } from "./sort_utils";
11
- import type { FilterDatatype } from "./filter_utils";
1
+ import type { CellValue, Headers, HeadersWithIDs, TableData } from "../types";
12
2
  import { dsvFormat } from "d3-dsv";
13
3
 
14
4
  export function make_cell_id(row: number, col: number): string {
@@ -19,94 +9,6 @@ export function make_header_id(col: number): string {
19
9
  return `header-${col}`;
20
10
  }
21
11
 
22
- export function get_max(data: TableData): TableCell[] {
23
- if (!data || !data.length) return [];
24
- let max = data[0].slice();
25
- for (let i = 0; i < data.length; i++) {
26
- for (let j = 0; j < data[i].length; j++) {
27
- if (`${max[j].value}`.length < `${data[i][j].value}`.length) {
28
- max[j] = data[i][j];
29
- }
30
- }
31
- }
32
- return max;
33
- }
34
-
35
- export function sort_table_data(
36
- data: TableData,
37
- display_value: string[][] | null,
38
- styling: string[][] | null,
39
- sort_columns: { col: number; direction: SortDirection }[]
40
- ): void {
41
- if (!sort_columns.length) return;
42
- if (!data || !data.length) return;
43
-
44
- const indices = sort_data(data, sort_columns);
45
-
46
- const new_data = indices.map((i: number) => data[i]);
47
- data.splice(0, data.length, ...new_data);
48
-
49
- if (display_value) {
50
- const new_display = indices.map((i: number) => display_value[i]);
51
- display_value.splice(0, display_value.length, ...new_display);
52
- }
53
-
54
- if (styling) {
55
- const new_styling = indices.map((i: number) => styling[i]);
56
- styling.splice(0, styling.length, ...new_styling);
57
- }
58
- }
59
-
60
- export function filter_table_data(
61
- data: TableData,
62
- display_value: string[][] | null,
63
- styling: string[][] | null,
64
- filter_columns: {
65
- col: number;
66
- datatype: FilterDatatype;
67
- filter: string;
68
- value: string;
69
- }[],
70
- original_data?: TableData,
71
- original_display_value?: string[][] | null,
72
- original_styling?: string[][] | null
73
- ): void {
74
- const base_data = original_data ?? data;
75
- const base_display_value = original_display_value ?? display_value;
76
- const base_styling = original_styling ?? styling;
77
-
78
- if (!filter_columns.length) {
79
- data.splice(0, data.length, ...base_data.map((row) => [...row]));
80
- if (display_value && base_display_value) {
81
- display_value.splice(
82
- 0,
83
- display_value.length,
84
- ...base_display_value.map((row) => [...row])
85
- );
86
- }
87
- if (styling && base_styling) {
88
- styling.splice(0, styling.length, ...base_styling.map((row) => [...row]));
89
- }
90
- return;
91
- }
92
- if (!data || !data.length) return;
93
-
94
- const indices = filter_data(base_data, filter_columns);
95
-
96
- const new_data = indices.map((i: number) => base_data[i]);
97
- data.splice(0, data.length, ...new_data);
98
-
99
- if (display_value && base_display_value) {
100
- const new_display = indices.map((i: number) => base_display_value[i]);
101
- display_value.splice(0, display_value.length, ...new_display);
102
- }
103
-
104
- if (styling && base_styling) {
105
- const new_styling = indices.map((i: number) => base_styling[i]);
106
- styling.splice(0, styling.length, ...new_styling);
107
- }
108
- }
109
-
110
12
  export async function copy_table_data(
111
13
  data: TableData,
112
14
  selected_cells: [number, number][] | null
@@ -145,7 +47,6 @@ export async function copy_table_data(
145
47
  }
146
48
  }
147
49
 
148
- // File Import/Export
149
50
  export function guess_delimiter(
150
51
  text: string,
151
52
  possibleDelimiters: string[]
@@ -5,7 +5,52 @@
5
5
  import type { I18nFormatter } from "@gradio/utils";
6
6
  import { default_i18n } from "./default_i18n";
7
7
 
8
- export let i18n: I18nFormatter | undefined = undefined;
8
+ let {
9
+ i18n = undefined,
10
+ value = $bindable({
11
+ data: [["", "", ""]],
12
+ headers: ["1", "2", "3"],
13
+ metadata: null
14
+ }),
15
+ datatype = "str",
16
+ interactive = true,
17
+ show_row_numbers = false,
18
+ max_height = 500,
19
+ show_search = "none",
20
+ wrap = false,
21
+ line_breaks = true,
22
+ column_widths = [],
23
+ max_chars = undefined,
24
+ pinned_columns = 0,
25
+ static_columns = [],
26
+ fullscreen = $bindable(false),
27
+ label = null,
28
+ show_label = true,
29
+ latex_delimiters = [],
30
+ col_count = undefined,
31
+ row_count = undefined
32
+ }: {
33
+ i18n?: I18nFormatter | undefined;
34
+ value?: DataframeValue;
35
+ datatype?: Datatype | Datatype[];
36
+ interactive?: boolean;
37
+ show_row_numbers?: boolean;
38
+ max_height?: number;
39
+ show_search?: "none" | "search" | "filter";
40
+ wrap?: boolean;
41
+ line_breaks?: boolean;
42
+ column_widths?: string[];
43
+ max_chars?: number | undefined;
44
+ pinned_columns?: number;
45
+ static_columns?: (string | number)[];
46
+ fullscreen?: boolean;
47
+ label?: string | null;
48
+ show_label?: boolean;
49
+ latex_delimiters?: any[];
50
+ col_count?: [number, "fixed" | "dynamic"] | undefined;
51
+ row_count?: [number, "fixed" | "dynamic"] | undefined;
52
+ } = $props();
53
+
9
54
  const i18n_fn = (key: string | null | undefined): string => {
10
55
  if (!key) return "";
11
56
  if (typeof i18n === "function") return (i18n as any)(key);
@@ -14,31 +59,7 @@
14
59
  return default_i18n[key] ?? key;
15
60
  };
16
61
 
17
- export let value: DataframeValue = {
18
- data: [["", "", ""]],
19
- headers: ["1", "2", "3"],
20
- metadata: null
21
- };
22
- export let datatype: Datatype | Datatype[] = "str";
23
- export let interactive = true;
24
- export let show_row_numbers = false;
25
- export let max_height = 500;
26
- export let show_search: "none" | "search" | "filter" = "none";
27
- export let wrap = false;
28
- export let line_breaks = true;
29
- export let column_widths: string[] = [];
30
- export let max_chars: number | undefined = undefined;
31
- export let pinned_columns = 0;
32
- export let static_columns: (string | number)[] = [];
33
- export let fullscreen = false;
34
- export let label: string | null = null;
35
- export let show_label = true;
36
- export let latex_delimiters: any[] = [];
37
- export let col_count: [number, "fixed" | "dynamic"] | undefined = undefined;
38
- export let row_count: [number, "fixed" | "dynamic"] | undefined = undefined;
39
-
40
- // mirrors default row count and column count logic in dataframe.py
41
- $: resolved_row_count = (() => {
62
+ let resolved_row_count = $derived.by(() => {
42
63
  if (
43
64
  row_count &&
44
65
  Array.isArray(row_count) &&
@@ -49,8 +70,9 @@
49
70
  return row_count as [number, "fixed" | "dynamic"];
50
71
  }
51
72
  return [1, "dynamic"] as [number, "fixed" | "dynamic"];
52
- })();
53
- $: resolved_col_count = (() => {
73
+ });
74
+
75
+ let resolved_col_count_base = $derived.by(() => {
54
76
  if (
55
77
  col_count &&
56
78
  Array.isArray(col_count) &&
@@ -65,14 +87,21 @@
65
87
  ? value.headers.length
66
88
  : 3;
67
89
  return [headerLength, "dynamic"] as [number, "fixed" | "dynamic"];
68
- })();
69
- $: if (
70
- static_columns &&
71
- static_columns.length > 0 &&
72
- resolved_col_count[1] !== "fixed"
73
- ) {
74
- resolved_col_count = [resolved_col_count[0], "fixed"];
75
- }
90
+ });
91
+
92
+ let resolved_col_count = $derived.by(() => {
93
+ if (
94
+ static_columns &&
95
+ static_columns.length > 0 &&
96
+ resolved_col_count_base[1] !== "fixed"
97
+ ) {
98
+ return [resolved_col_count_base[0], "fixed"] as [
99
+ number,
100
+ "fixed" | "dynamic"
101
+ ];
102
+ }
103
+ return resolved_col_count_base;
104
+ });
76
105
 
77
106
  let root = "";
78
107
 
@@ -91,8 +120,8 @@
91
120
  <Table
92
121
  values={value.data}
93
122
  headers={value.headers}
94
- display_value={value?.metadata?.display_value}
95
- styling={value?.metadata?.styling}
123
+ display_value={value?.metadata?.display_value ?? null}
124
+ styling={value?.metadata?.styling ?? null}
96
125
  {datatype}
97
126
  editable={interactive}
98
127
  {show_row_numbers}
@@ -113,15 +142,11 @@
113
142
  row_count={resolved_row_count}
114
143
  {root}
115
144
  i18n={i18n_fn}
116
- on:change={(e) => {
117
- value.data = e.detail.data;
118
- value.headers = e.detail.headers;
145
+ onchange={(detail) => {
146
+ value.data = detail.data;
147
+ value.headers = detail.headers;
119
148
  }}
120
- on:blur
121
- on:keydown
122
- on:input
123
- on:select
124
- on:fullscreen={(e) => (fullscreen = e.detail)}
149
+ onfullscreen={() => (fullscreen = !fullscreen)}
125
150
  upload={async () => null}
126
151
  stream_handler={() => new EventSource("about:blank")}
127
152
  />
@@ -0,0 +1,350 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { gradio_filter_fn } from "../shared/utils/filter";
3
+
4
+ const make_row = (value: any) => ({
5
+ getValue: (_col: string) => value
6
+ });
7
+
8
+ describe("gradio_filter_fn", () => {
9
+ describe("number filters", () => {
10
+ test("= matches exact number", () => {
11
+ expect(
12
+ gradio_filter_fn(make_row(42), "col_0", {
13
+ dtype: "number",
14
+ filter: "=",
15
+ value: "42"
16
+ })
17
+ ).toBe(true);
18
+ expect(
19
+ gradio_filter_fn(make_row(42), "col_0", {
20
+ dtype: "number",
21
+ filter: "=",
22
+ value: "43"
23
+ })
24
+ ).toBe(false);
25
+ });
26
+
27
+ test("≠ excludes exact number", () => {
28
+ expect(
29
+ gradio_filter_fn(make_row(42), "col_0", {
30
+ dtype: "number",
31
+ filter: "≠",
32
+ value: "42"
33
+ })
34
+ ).toBe(false);
35
+ expect(
36
+ gradio_filter_fn(make_row(42), "col_0", {
37
+ dtype: "number",
38
+ filter: "≠",
39
+ value: "43"
40
+ })
41
+ ).toBe(true);
42
+ });
43
+
44
+ test("> greater than", () => {
45
+ expect(
46
+ gradio_filter_fn(make_row(10), "col_0", {
47
+ dtype: "number",
48
+ filter: ">",
49
+ value: "5"
50
+ })
51
+ ).toBe(true);
52
+ expect(
53
+ gradio_filter_fn(make_row(5), "col_0", {
54
+ dtype: "number",
55
+ filter: ">",
56
+ value: "5"
57
+ })
58
+ ).toBe(false);
59
+ });
60
+
61
+ test("< less than", () => {
62
+ expect(
63
+ gradio_filter_fn(make_row(3), "col_0", {
64
+ dtype: "number",
65
+ filter: "<",
66
+ value: "5"
67
+ })
68
+ ).toBe(true);
69
+ expect(
70
+ gradio_filter_fn(make_row(5), "col_0", {
71
+ dtype: "number",
72
+ filter: "<",
73
+ value: "5"
74
+ })
75
+ ).toBe(false);
76
+ });
77
+
78
+ test("≥ greater than or equal", () => {
79
+ expect(
80
+ gradio_filter_fn(make_row(5), "col_0", {
81
+ dtype: "number",
82
+ filter: "≥",
83
+ value: "5"
84
+ })
85
+ ).toBe(true);
86
+ expect(
87
+ gradio_filter_fn(make_row(4), "col_0", {
88
+ dtype: "number",
89
+ filter: "≥",
90
+ value: "5"
91
+ })
92
+ ).toBe(false);
93
+ });
94
+
95
+ test("≤ less than or equal", () => {
96
+ expect(
97
+ gradio_filter_fn(make_row(5), "col_0", {
98
+ dtype: "number",
99
+ filter: "≤",
100
+ value: "5"
101
+ })
102
+ ).toBe(true);
103
+ expect(
104
+ gradio_filter_fn(make_row(6), "col_0", {
105
+ dtype: "number",
106
+ filter: "≤",
107
+ value: "5"
108
+ })
109
+ ).toBe(false);
110
+ });
111
+
112
+ test("Is empty on empty string", () => {
113
+ expect(
114
+ gradio_filter_fn(make_row(""), "col_0", {
115
+ dtype: "number",
116
+ filter: "Is empty",
117
+ value: ""
118
+ })
119
+ ).toBe(true);
120
+ expect(
121
+ gradio_filter_fn(make_row("42"), "col_0", {
122
+ dtype: "number",
123
+ filter: "Is empty",
124
+ value: ""
125
+ })
126
+ ).toBe(false);
127
+ });
128
+
129
+ test("Is not empty on non-empty string", () => {
130
+ expect(
131
+ gradio_filter_fn(make_row("42"), "col_0", {
132
+ dtype: "number",
133
+ filter: "Is not empty",
134
+ value: ""
135
+ })
136
+ ).toBe(true);
137
+ expect(
138
+ gradio_filter_fn(make_row(""), "col_0", {
139
+ dtype: "number",
140
+ filter: "Is not empty",
141
+ value: ""
142
+ })
143
+ ).toBe(false);
144
+ });
145
+
146
+ test("NaN target falls through to true", () => {
147
+ expect(
148
+ gradio_filter_fn(make_row(42), "col_0", {
149
+ dtype: "number",
150
+ filter: "=",
151
+ value: "abc"
152
+ })
153
+ ).toBe(true);
154
+ });
155
+
156
+ test("NaN cell value falls through to true", () => {
157
+ expect(
158
+ gradio_filter_fn(make_row("abc"), "col_0", {
159
+ dtype: "number",
160
+ filter: "=",
161
+ value: "42"
162
+ })
163
+ ).toBe(true);
164
+ });
165
+ });
166
+
167
+ describe("string filters", () => {
168
+ test("Contains (case-insensitive)", () => {
169
+ expect(
170
+ gradio_filter_fn(make_row("Hello World"), "col_0", {
171
+ dtype: "string",
172
+ filter: "Contains",
173
+ value: "hello"
174
+ })
175
+ ).toBe(true);
176
+ expect(
177
+ gradio_filter_fn(make_row("Hello World"), "col_0", {
178
+ dtype: "string",
179
+ filter: "Contains",
180
+ value: "xyz"
181
+ })
182
+ ).toBe(false);
183
+ });
184
+
185
+ test("Does not contain", () => {
186
+ expect(
187
+ gradio_filter_fn(make_row("Hello World"), "col_0", {
188
+ dtype: "string",
189
+ filter: "Does not contain",
190
+ value: "xyz"
191
+ })
192
+ ).toBe(true);
193
+ expect(
194
+ gradio_filter_fn(make_row("Hello World"), "col_0", {
195
+ dtype: "string",
196
+ filter: "Does not contain",
197
+ value: "hello"
198
+ })
199
+ ).toBe(false);
200
+ });
201
+
202
+ test("Starts with", () => {
203
+ expect(
204
+ gradio_filter_fn(make_row("Hello World"), "col_0", {
205
+ dtype: "string",
206
+ filter: "Starts with",
207
+ value: "hello"
208
+ })
209
+ ).toBe(true);
210
+ expect(
211
+ gradio_filter_fn(make_row("Hello World"), "col_0", {
212
+ dtype: "string",
213
+ filter: "Starts with",
214
+ value: "world"
215
+ })
216
+ ).toBe(false);
217
+ });
218
+
219
+ test("Ends with", () => {
220
+ expect(
221
+ gradio_filter_fn(make_row("Hello World"), "col_0", {
222
+ dtype: "string",
223
+ filter: "Ends with",
224
+ value: "world"
225
+ })
226
+ ).toBe(true);
227
+ expect(
228
+ gradio_filter_fn(make_row("Hello World"), "col_0", {
229
+ dtype: "string",
230
+ filter: "Ends with",
231
+ value: "hello"
232
+ })
233
+ ).toBe(false);
234
+ });
235
+
236
+ test("Is (exact match, case-insensitive)", () => {
237
+ expect(
238
+ gradio_filter_fn(make_row("Hello"), "col_0", {
239
+ dtype: "string",
240
+ filter: "Is",
241
+ value: "hello"
242
+ })
243
+ ).toBe(true);
244
+ expect(
245
+ gradio_filter_fn(make_row("Hello World"), "col_0", {
246
+ dtype: "string",
247
+ filter: "Is",
248
+ value: "hello"
249
+ })
250
+ ).toBe(false);
251
+ });
252
+
253
+ test("Is not", () => {
254
+ expect(
255
+ gradio_filter_fn(make_row("Hello World"), "col_0", {
256
+ dtype: "string",
257
+ filter: "Is not",
258
+ value: "hello"
259
+ })
260
+ ).toBe(true);
261
+ expect(
262
+ gradio_filter_fn(make_row("Hello"), "col_0", {
263
+ dtype: "string",
264
+ filter: "Is not",
265
+ value: "hello"
266
+ })
267
+ ).toBe(false);
268
+ });
269
+
270
+ test("Is empty", () => {
271
+ expect(
272
+ gradio_filter_fn(make_row(""), "col_0", {
273
+ dtype: "string",
274
+ filter: "Is empty",
275
+ value: ""
276
+ })
277
+ ).toBe(true);
278
+ expect(
279
+ gradio_filter_fn(make_row(" "), "col_0", {
280
+ dtype: "string",
281
+ filter: "Is empty",
282
+ value: ""
283
+ })
284
+ ).toBe(true);
285
+ expect(
286
+ gradio_filter_fn(make_row("hello"), "col_0", {
287
+ dtype: "string",
288
+ filter: "Is empty",
289
+ value: ""
290
+ })
291
+ ).toBe(false);
292
+ });
293
+
294
+ test("Is not empty", () => {
295
+ expect(
296
+ gradio_filter_fn(make_row("hello"), "col_0", {
297
+ dtype: "string",
298
+ filter: "Is not empty",
299
+ value: ""
300
+ })
301
+ ).toBe(true);
302
+ expect(
303
+ gradio_filter_fn(make_row(""), "col_0", {
304
+ dtype: "string",
305
+ filter: "Is not empty",
306
+ value: ""
307
+ })
308
+ ).toBe(false);
309
+ });
310
+ });
311
+
312
+ describe("edge cases", () => {
313
+ test("null cell value coerced to empty string", () => {
314
+ expect(
315
+ gradio_filter_fn(make_row(null), "col_0", {
316
+ dtype: "string",
317
+ filter: "Is empty",
318
+ value: ""
319
+ })
320
+ ).toBe(true);
321
+ });
322
+
323
+ test("undefined filter value coerced to empty string", () => {
324
+ expect(
325
+ gradio_filter_fn(make_row("hello"), "col_0", {
326
+ dtype: "string",
327
+ filter: "Contains",
328
+ value: undefined
329
+ })
330
+ ).toBe(true);
331
+ });
332
+
333
+ test("unknown filter returns true (passthrough)", () => {
334
+ expect(
335
+ gradio_filter_fn(make_row("hello"), "col_0", {
336
+ dtype: "string",
337
+ filter: "UnknownFilter",
338
+ value: "x"
339
+ })
340
+ ).toBe(true);
341
+ expect(
342
+ gradio_filter_fn(make_row(42), "col_0", {
343
+ dtype: "number",
344
+ filter: "UnknownFilter",
345
+ value: "5"
346
+ })
347
+ ).toBe(true);
348
+ });
349
+ });
350
+ });