@marimo-team/islands 0.23.7-dev55 → 0.23.7-dev57
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.
- package/dist/{chat-ui-DCyW3OUK.js → chat-ui-D3XBept8.js} +3 -3
- package/dist/{code-visibility-CJ7U5FE0.js → code-visibility-PjV7HUDZ.js} +10624 -1451
- package/dist/{formats-CpgZM9BM.js → formats-Dsy9kkZu.js} +1 -1
- package/dist/{html-to-image-40ZXSWP-.js → html-to-image-CpggM7u1.js} +1 -1
- package/dist/main.js +1353 -9989
- package/dist/{process-output-CCeeXIBd.js → process-output-X8TR20AK.js} +1 -1
- package/dist/{reveal-component-Bopa1DsA.js → reveal-component-Phd-LTXq.js} +3 -3
- package/dist/{toDate-CJWlVNGD.js → toDate-CIpC_34u.js} +30 -17
- package/dist/{vega-component-BtvQ-Kc4.js → vega-component-cSdqoAxe.js} +2 -2
- package/package.json +1 -1
- package/src/components/data-table/__tests__/column-header.test.tsx +106 -1
- package/src/components/data-table/__tests__/filter-pill-editor.test.tsx +88 -2
- package/src/components/data-table/__tests__/filters.test.ts +84 -13
- package/src/components/data-table/column-header.tsx +152 -26
- package/src/components/data-table/date-filter-inputs.tsx +325 -0
- package/src/components/data-table/filter-pill-editor.tsx +139 -30
- package/src/components/data-table/filter-pills.tsx +31 -57
- package/src/components/data-table/filters.ts +88 -66
- package/src/components/editor/chrome/wrapper/footer-items/backend-status.tsx +1 -1
- package/src/core/runtime/__tests__/runtime.test.ts +38 -17
- package/src/core/runtime/runtime.ts +57 -34
- package/src/core/websocket/__tests__/useMarimoKernelConnection.hook.test.tsx +5 -4
- package/src/core/websocket/__tests__/useMarimoKernelConnection.test.ts +18 -54
- package/src/core/websocket/transports/__tests__/ws.test.ts +125 -0
- package/src/core/websocket/transports/basic.ts +1 -3
- package/src/core/websocket/transports/transport.ts +0 -1
- package/src/core/websocket/transports/ws.ts +96 -0
- package/src/core/websocket/useMarimoKernelConnection.tsx +30 -26
- package/src/core/websocket/useWebSocket.tsx +3 -18
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { s as __toESM } from "./chunk-BNovOVIE.js";
|
|
2
2
|
import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
|
|
3
|
-
import { it as parseHtmlContent, rt as ansiToPlainText } from "./html-to-image-
|
|
3
|
+
import { it as parseHtmlContent, rt as ansiToPlainText } from "./html-to-image-CpggM7u1.js";
|
|
4
4
|
import { u as createLucideIcon } from "./dist-D3ZI9nhS.js";
|
|
5
5
|
import { t as Strings } from "./strings-BiIhGaI8.js";
|
|
6
6
|
import { t as require_jsx_runtime } from "./jsx-runtime-COBk7ree.js";
|
|
@@ -6,11 +6,11 @@ import { s as __toESM } from "./chunk-BNovOVIE.js";
|
|
|
6
6
|
import { _ as Logger, g as cn, h as Events, l as useEventListener, t as Button } from "./button-Dj4BTre0.js";
|
|
7
7
|
import { t as require_react } from "./react-DA-nE2FX.js";
|
|
8
8
|
import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
|
|
9
|
-
import "./html-to-image-
|
|
9
|
+
import "./html-to-image-CpggM7u1.js";
|
|
10
10
|
import "./chunk-5FQGJX7Z-BOg95xG5.js";
|
|
11
|
-
import { Ft as
|
|
11
|
+
import { Ft as EyeOff, It as Expand, a as DEFAULT_SLIDE_TYPE, c as Slide, i as DEFAULT_DECK_TRANSITION, s as SlideSidebar, t as useNotebookCodeAvailable, zt as Code } from "./code-visibility-PjV7HUDZ.js";
|
|
12
12
|
import "./input-D4kjoQUB.js";
|
|
13
|
-
import "./toDate-
|
|
13
|
+
import "./toDate-CIpC_34u.js";
|
|
14
14
|
import "./react-dom-BWRJ_g_k.js";
|
|
15
15
|
import { t as require_jsx_runtime } from "./jsx-runtime-COBk7ree.js";
|
|
16
16
|
import "./zod-BWkcDORu.js";
|
|
@@ -512,15 +512,21 @@ var RuntimeManager = class {
|
|
|
512
512
|
get isSameOrigin() {
|
|
513
513
|
return this.httpURL.origin === window.location.origin;
|
|
514
514
|
}
|
|
515
|
-
|
|
516
|
-
|
|
515
|
+
get isServerless() {
|
|
516
|
+
return isWasm() || isIslands() || isStaticNotebook();
|
|
517
|
+
}
|
|
518
|
+
formatHttpURL({ path: e = "", searchParams: b, restrictToKnownQueryParams: x = true }) {
|
|
517
519
|
let S = this.httpURL, w = new URLSearchParams(window.location.search);
|
|
518
520
|
if (b) for (let [e2, x2] of b.entries()) S.searchParams.set(e2, x2);
|
|
519
521
|
for (let [e2, b2] of w.entries()) x && !Object.values(KnownQueryParams).includes(e2) || S.searchParams.set(e2, b2);
|
|
520
522
|
return S.pathname = `${S.pathname.replace(/\/$/, "")}/${e.replace(/^\//, "")}`, S.hash = "", S;
|
|
521
523
|
}
|
|
522
524
|
formatWsURL(e, b) {
|
|
523
|
-
let x = this.formatHttpURL(
|
|
525
|
+
let x = this.formatHttpURL({
|
|
526
|
+
path: e,
|
|
527
|
+
searchParams: b,
|
|
528
|
+
restrictToKnownQueryParams: false
|
|
529
|
+
});
|
|
524
530
|
return !this.isSameOrigin && this.config.authToken && x.searchParams.set(KnownQueryParams.accessToken, this.config.authToken), asWsUrl(x.toString());
|
|
525
531
|
}
|
|
526
532
|
getWsURL(e) {
|
|
@@ -546,26 +552,33 @@ var RuntimeManager = class {
|
|
|
546
552
|
return this.formatWsURL(`/lsp/${e}`);
|
|
547
553
|
}
|
|
548
554
|
getAiURL(e) {
|
|
549
|
-
return this.formatHttpURL(`/api/ai/${e}`);
|
|
555
|
+
return this.formatHttpURL({ path: `/api/ai/${e}` });
|
|
550
556
|
}
|
|
551
557
|
healthURL() {
|
|
552
|
-
return this.formatHttpURL("/health");
|
|
558
|
+
return this.formatHttpURL({ path: "/health" });
|
|
553
559
|
}
|
|
554
|
-
async
|
|
555
|
-
if (isWasm() || isIslands() || isStaticNotebook()) return true;
|
|
560
|
+
async fetchHealth() {
|
|
556
561
|
try {
|
|
557
|
-
|
|
558
|
-
if (e.redirected) {
|
|
559
|
-
Logger.debug(`Runtime redirected to ${e.url}`);
|
|
560
|
-
let x2 = new URL(e.url);
|
|
561
|
-
x2.pathname = x2.pathname.replace(/\/health$/, ""), this.config.url = x2.toString();
|
|
562
|
-
}
|
|
563
|
-
let x = e.ok;
|
|
564
|
-
return x && this.setDOMBaseUri(this.config.url), x;
|
|
562
|
+
return await fetch(this.healthURL().toString());
|
|
565
563
|
} catch (e) {
|
|
566
|
-
return Logger.error(`Failed to check health: ${e instanceof Error ? e.message : "Unknown error"}`, { cause: e }),
|
|
564
|
+
return Logger.error(`Failed to check health: ${e instanceof Error ? e.message : "Unknown error"}`, { cause: e }), null;
|
|
567
565
|
}
|
|
568
566
|
}
|
|
567
|
+
async reconcileFromHealth() {
|
|
568
|
+
if (this.isServerless) return true;
|
|
569
|
+
let e = await this.fetchHealth();
|
|
570
|
+
if (!e) return false;
|
|
571
|
+
if (e.redirected) {
|
|
572
|
+
Logger.debug(`Runtime redirected to ${e.url}`);
|
|
573
|
+
let x = new URL(e.url);
|
|
574
|
+
x.pathname = x.pathname.replace(/\/health$/, ""), this.config.url = x.toString();
|
|
575
|
+
}
|
|
576
|
+
return e.ok && this.setDOMBaseUri(this.config.url), e.ok;
|
|
577
|
+
}
|
|
578
|
+
async probeHealth() {
|
|
579
|
+
var _a;
|
|
580
|
+
return this.isServerless ? true : ((_a = await this.fetchHealth()) == null ? void 0 : _a.ok) ?? false;
|
|
581
|
+
}
|
|
569
582
|
setDOMBaseUri(e) {
|
|
570
583
|
e = e.split("?", 1)[0], e.endsWith("/") || (e += "/");
|
|
571
584
|
let b = document.querySelector("base");
|
|
@@ -574,7 +587,7 @@ var RuntimeManager = class {
|
|
|
574
587
|
async init(e) {
|
|
575
588
|
Logger.debug("Initializing runtime...");
|
|
576
589
|
let x = 0;
|
|
577
|
-
for (; !await this.
|
|
590
|
+
for (; !await this.reconcileFromHealth(); ) {
|
|
578
591
|
if (x >= 25) {
|
|
579
592
|
Logger.error("Failed to connect after 25 retries"), this.initialHealthyCheck.reject(/* @__PURE__ */ Error("Failed to connect after 25 retries"));
|
|
580
593
|
return;
|
|
@@ -2,7 +2,7 @@ import { s as __toESM } from "./chunk-BNovOVIE.js";
|
|
|
2
2
|
import { _ as Logger, c as Objects, g as cn, h as Events } from "./button-Dj4BTre0.js";
|
|
3
3
|
import { t as require_react } from "./react-DA-nE2FX.js";
|
|
4
4
|
import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
|
|
5
|
-
import { c as asRemoteURL, v as CircleQuestionMark } from "./toDate-
|
|
5
|
+
import { c as asRemoteURL, v as CircleQuestionMark } from "./toDate-CIpC_34u.js";
|
|
6
6
|
import "./react-dom-BWRJ_g_k.js";
|
|
7
7
|
import { t as require_jsx_runtime } from "./jsx-runtime-COBk7ree.js";
|
|
8
8
|
import "./zod-BWkcDORu.js";
|
|
@@ -11,7 +11,7 @@ import { t as Tooltip } from "./tooltip-DRaMBu06.js";
|
|
|
11
11
|
import { i as debounce_default } from "./constants-D0gkYoE2.js";
|
|
12
12
|
import { n as useTheme, w as useEvent_default } from "./useTheme-DykuNHR2.js";
|
|
13
13
|
import { s as uniq } from "./arrays-CldYf7p7.js";
|
|
14
|
-
import { a as isValid, i as AlertTitle, n as Alert, t as arrow } from "./formats-
|
|
14
|
+
import { a as isValid, i as AlertTitle, n as Alert, t as arrow } from "./formats-Dsy9kkZu.js";
|
|
15
15
|
import { n as formats } from "./vega-loader.browser-3_z8GoFC.js";
|
|
16
16
|
import { a as getContainerWidth, n as vegaLoadData, s as tooltipHandler } from "./loader-Dr8Qem8p.js";
|
|
17
17
|
import { t as useAsyncData } from "./useAsyncData-C56Khv_R.js";
|
package/package.json
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
import type { Column } from "@tanstack/react-table";
|
|
3
3
|
import { fireEvent, render, screen, within } from "@testing-library/react";
|
|
4
4
|
import { beforeAll, describe, expect, it, vi } from "vitest";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
DateFilterMenu,
|
|
7
|
+
NumberFilterMenu,
|
|
8
|
+
TextFilterMenu,
|
|
9
|
+
} from "../column-header";
|
|
6
10
|
import { Filter } from "../filters";
|
|
7
11
|
|
|
8
12
|
beforeAll(() => {
|
|
@@ -201,3 +205,104 @@ describe("TextFilterMenu", () => {
|
|
|
201
205
|
expect(screen.getByRole("button", { name: /apply/i })).not.toBeDisabled();
|
|
202
206
|
});
|
|
203
207
|
});
|
|
208
|
+
|
|
209
|
+
type DateFilterValue = ReturnType<typeof Filter.date>;
|
|
210
|
+
|
|
211
|
+
function mockDateColumn(
|
|
212
|
+
filterType: "date" | "datetime" | "time" = "date",
|
|
213
|
+
initial?: DateFilterValue,
|
|
214
|
+
): Column<unknown, unknown> & {
|
|
215
|
+
setFilterValue: ReturnType<typeof vi.fn>;
|
|
216
|
+
} {
|
|
217
|
+
let filterValue = initial;
|
|
218
|
+
const setFilterValue = vi.fn((next) => {
|
|
219
|
+
filterValue = next;
|
|
220
|
+
});
|
|
221
|
+
return {
|
|
222
|
+
id: "when",
|
|
223
|
+
columnDef: { meta: { dataType: filterType, filterType } },
|
|
224
|
+
getFilterValue: () => filterValue,
|
|
225
|
+
setFilterValue,
|
|
226
|
+
} as unknown as Column<unknown, unknown> & {
|
|
227
|
+
setFilterValue: ReturnType<typeof vi.fn>;
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
describe("DateFilterMenu", () => {
|
|
232
|
+
it("shows all expected operators in the dropdown", () => {
|
|
233
|
+
const column = mockDateColumn("date");
|
|
234
|
+
render(<DateFilterMenu column={column} filterType="date" />);
|
|
235
|
+
fireEvent.click(screen.getByRole("combobox"));
|
|
236
|
+
const listbox = screen.getByRole("listbox");
|
|
237
|
+
const labels = within(listbox)
|
|
238
|
+
.getAllByRole("option")
|
|
239
|
+
.map((o) => o.textContent);
|
|
240
|
+
expect(labels).toEqual([
|
|
241
|
+
"Between",
|
|
242
|
+
"Equals",
|
|
243
|
+
"Doesn't equal",
|
|
244
|
+
"Greater than",
|
|
245
|
+
"Greater than or equal",
|
|
246
|
+
"Less than",
|
|
247
|
+
"Less than or equal",
|
|
248
|
+
"Is null",
|
|
249
|
+
"Is not null",
|
|
250
|
+
]);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("defaults to between mode and disables Apply until both bounds set", () => {
|
|
254
|
+
const column = mockDateColumn("date");
|
|
255
|
+
render(<DateFilterMenu column={column} filterType="date" />);
|
|
256
|
+
expect(screen.getByLabelText("range")).toBeInTheDocument();
|
|
257
|
+
expect(screen.getByRole("button", { name: /apply/i })).toBeDisabled();
|
|
258
|
+
expect(screen.queryByLabelText("value")).not.toBeInTheDocument();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("seeds between min/max from current filter", () => {
|
|
262
|
+
const column = mockDateColumn(
|
|
263
|
+
"date",
|
|
264
|
+
Filter.date({
|
|
265
|
+
operator: "between",
|
|
266
|
+
min: new Date("2024-01-01T00:00:00Z"),
|
|
267
|
+
max: new Date("2024-06-01T00:00:00Z"),
|
|
268
|
+
}),
|
|
269
|
+
);
|
|
270
|
+
render(<DateFilterMenu column={column} filterType="date" />);
|
|
271
|
+
expect(screen.getByLabelText("range")).toBeInTheDocument();
|
|
272
|
+
expect(screen.getByRole("button", { name: /apply/i })).not.toBeDisabled();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("comparison operator swaps range for a single value picker", () => {
|
|
276
|
+
const column = mockDateColumn(
|
|
277
|
+
"date",
|
|
278
|
+
Filter.date({
|
|
279
|
+
operator: ">",
|
|
280
|
+
value: new Date("2024-01-01T00:00:00Z"),
|
|
281
|
+
}),
|
|
282
|
+
);
|
|
283
|
+
render(<DateFilterMenu column={column} filterType="date" />);
|
|
284
|
+
expect(screen.getByLabelText("value")).toBeInTheDocument();
|
|
285
|
+
expect(screen.queryByLabelText("range")).not.toBeInTheDocument();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("time filter type renders two TimeFields for between", () => {
|
|
289
|
+
const column = mockDateColumn("time");
|
|
290
|
+
render(<DateFilterMenu column={column} filterType="time" />);
|
|
291
|
+
expect(screen.getByLabelText("min")).toBeInTheDocument();
|
|
292
|
+
expect(screen.getByLabelText("max")).toBeInTheDocument();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("selecting a nullish operator hides value inputs and commits on Apply", () => {
|
|
296
|
+
const column = mockDateColumn("date");
|
|
297
|
+
render(<DateFilterMenu column={column} filterType="date" />);
|
|
298
|
+
fireEvent.click(screen.getByRole("combobox"));
|
|
299
|
+
const listbox = screen.getByRole("listbox");
|
|
300
|
+
fireEvent.click(within(listbox).getByText("Is null"));
|
|
301
|
+
expect(screen.queryByLabelText("range")).not.toBeInTheDocument();
|
|
302
|
+
expect(screen.queryByLabelText("value")).not.toBeInTheDocument();
|
|
303
|
+
fireEvent.click(screen.getByRole("button", { name: /apply/i }));
|
|
304
|
+
expect(column.setFilterValue).toHaveBeenCalledWith(
|
|
305
|
+
Filter.date({ operator: "is_null" }),
|
|
306
|
+
);
|
|
307
|
+
});
|
|
308
|
+
});
|
|
@@ -26,7 +26,14 @@ beforeAll(() => {
|
|
|
26
26
|
|
|
27
27
|
function makeColumn(
|
|
28
28
|
id: string,
|
|
29
|
-
filterType:
|
|
29
|
+
filterType:
|
|
30
|
+
| "text"
|
|
31
|
+
| "number"
|
|
32
|
+
| "boolean"
|
|
33
|
+
| "select"
|
|
34
|
+
| "date"
|
|
35
|
+
| "datetime"
|
|
36
|
+
| "time",
|
|
30
37
|
): Column<unknown, unknown> {
|
|
31
38
|
return {
|
|
32
39
|
id,
|
|
@@ -35,7 +42,13 @@ function makeColumn(
|
|
|
35
42
|
}
|
|
36
43
|
|
|
37
44
|
function mockTable(): Table<unknown> {
|
|
38
|
-
const columns = [
|
|
45
|
+
const columns = [
|
|
46
|
+
makeColumn("name", "text"),
|
|
47
|
+
makeColumn("age", "number"),
|
|
48
|
+
makeColumn("when", "date"),
|
|
49
|
+
makeColumn("at", "datetime"),
|
|
50
|
+
makeColumn("clock", "time"),
|
|
51
|
+
];
|
|
39
52
|
return {
|
|
40
53
|
getAllColumns: () => columns,
|
|
41
54
|
getColumn: (id: string) => columns.find((c) => c.id === id),
|
|
@@ -144,6 +157,79 @@ describe("FilterPillEditor — snapshot rehydration", () => {
|
|
|
144
157
|
});
|
|
145
158
|
});
|
|
146
159
|
|
|
160
|
+
describe("FilterPillEditor — date/datetime/time", () => {
|
|
161
|
+
it("rehydrates a date between snapshot with the range picker", () => {
|
|
162
|
+
renderWithProviders(
|
|
163
|
+
<FilterPillEditor
|
|
164
|
+
snapshot={{
|
|
165
|
+
columnId: "when",
|
|
166
|
+
value: Filter.date({
|
|
167
|
+
operator: "between",
|
|
168
|
+
min: new Date("2024-01-01T00:00:00Z"),
|
|
169
|
+
max: new Date("2024-06-01T00:00:00Z"),
|
|
170
|
+
}),
|
|
171
|
+
}}
|
|
172
|
+
table={mockTable()}
|
|
173
|
+
onClose={vi.fn()}
|
|
174
|
+
/>,
|
|
175
|
+
);
|
|
176
|
+
expect(screen.getByLabelText("range")).toBeInTheDocument();
|
|
177
|
+
expect(screen.queryByLabelText("value")).not.toBeInTheDocument();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("rehydrates a datetime <= snapshot with a single value picker", () => {
|
|
181
|
+
renderWithProviders(
|
|
182
|
+
<FilterPillEditor
|
|
183
|
+
snapshot={{
|
|
184
|
+
columnId: "at",
|
|
185
|
+
value: Filter.datetime({
|
|
186
|
+
operator: "<=",
|
|
187
|
+
value: new Date("2024-06-01T12:00:00Z"),
|
|
188
|
+
}),
|
|
189
|
+
}}
|
|
190
|
+
table={mockTable()}
|
|
191
|
+
onClose={vi.fn()}
|
|
192
|
+
/>,
|
|
193
|
+
);
|
|
194
|
+
expect(screen.getByLabelText("value")).toBeInTheDocument();
|
|
195
|
+
expect(screen.queryByLabelText("range")).not.toBeInTheDocument();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("renders min/max TimeFields for time between", () => {
|
|
199
|
+
renderWithProviders(
|
|
200
|
+
<FilterPillEditor
|
|
201
|
+
snapshot={{
|
|
202
|
+
columnId: "clock",
|
|
203
|
+
value: Filter.time({
|
|
204
|
+
operator: "between",
|
|
205
|
+
min: new Date("2024-01-01T08:00:00Z"),
|
|
206
|
+
max: new Date("2024-01-01T17:00:00Z"),
|
|
207
|
+
}),
|
|
208
|
+
}}
|
|
209
|
+
table={mockTable()}
|
|
210
|
+
onClose={vi.fn()}
|
|
211
|
+
/>,
|
|
212
|
+
);
|
|
213
|
+
expect(screen.getByLabelText("min")).toBeInTheDocument();
|
|
214
|
+
expect(screen.getByLabelText("max")).toBeInTheDocument();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("hides the value slot for date is_null", () => {
|
|
218
|
+
renderWithProviders(
|
|
219
|
+
<FilterPillEditor
|
|
220
|
+
snapshot={{
|
|
221
|
+
columnId: "when",
|
|
222
|
+
value: Filter.date({ operator: "is_null" }),
|
|
223
|
+
}}
|
|
224
|
+
table={mockTable()}
|
|
225
|
+
onClose={vi.fn()}
|
|
226
|
+
/>,
|
|
227
|
+
);
|
|
228
|
+
expect(screen.queryByText("Value")).not.toBeInTheDocument();
|
|
229
|
+
expect(screen.queryByLabelText("range")).not.toBeInTheDocument();
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
147
233
|
describe("FilterPillEditor — apply", () => {
|
|
148
234
|
it("commits a number > filter via setColumnFilters", () => {
|
|
149
235
|
const table = mockTable();
|
|
@@ -215,22 +215,93 @@ describe("filterToFilterCondition", () => {
|
|
|
215
215
|
]);
|
|
216
216
|
});
|
|
217
217
|
|
|
218
|
-
it("handles date filter
|
|
219
|
-
const min = new Date(
|
|
220
|
-
const max = new Date(
|
|
218
|
+
it("handles date between filter", () => {
|
|
219
|
+
const min = new Date(2024, 0, 1);
|
|
220
|
+
const max = new Date(2024, 11, 31);
|
|
221
221
|
const result = filterToFilterCondition(
|
|
222
222
|
"created",
|
|
223
|
-
Filter.date({ min, max }),
|
|
223
|
+
Filter.date({ operator: "between", min, max }),
|
|
224
224
|
);
|
|
225
|
-
expect(result).
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
225
|
+
expect(result).toEqual([
|
|
226
|
+
{
|
|
227
|
+
column_id: "created",
|
|
228
|
+
operator: "between",
|
|
229
|
+
value: { min: "2024-01-01", max: "2024-12-31" },
|
|
230
|
+
type: "condition",
|
|
231
|
+
negate: false,
|
|
232
|
+
},
|
|
233
|
+
]);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("handles date comparison filter", () => {
|
|
237
|
+
const value = new Date(2024, 5, 15);
|
|
238
|
+
const result = filterToFilterCondition(
|
|
239
|
+
"created",
|
|
240
|
+
Filter.date({ operator: ">=", value }),
|
|
241
|
+
);
|
|
242
|
+
expect(result).toEqual([
|
|
243
|
+
{
|
|
244
|
+
column_id: "created",
|
|
245
|
+
operator: ">=",
|
|
246
|
+
value: "2024-06-15",
|
|
247
|
+
type: "condition",
|
|
248
|
+
negate: false,
|
|
249
|
+
},
|
|
250
|
+
]);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("handles datetime between filter as local ISO string without TZ", () => {
|
|
254
|
+
const min = new Date(2024, 0, 1, 0, 0, 0);
|
|
255
|
+
const max = new Date(2024, 11, 31, 23, 59, 59);
|
|
256
|
+
const result = filterToFilterCondition(
|
|
257
|
+
"created",
|
|
258
|
+
Filter.datetime({ operator: "between", min, max }),
|
|
259
|
+
);
|
|
260
|
+
expect(result).toEqual([
|
|
261
|
+
{
|
|
262
|
+
column_id: "created",
|
|
263
|
+
operator: "between",
|
|
264
|
+
value: {
|
|
265
|
+
min: "2024-01-01T00:00:00",
|
|
266
|
+
max: "2024-12-31T23:59:59",
|
|
267
|
+
},
|
|
268
|
+
type: "condition",
|
|
269
|
+
negate: false,
|
|
270
|
+
},
|
|
271
|
+
]);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("handles time between filter as HH:MM:SS", () => {
|
|
275
|
+
const min = new Date(2024, 0, 1, 9, 30, 0);
|
|
276
|
+
const max = new Date(2024, 0, 1, 17, 45, 15);
|
|
277
|
+
const result = filterToFilterCondition(
|
|
278
|
+
"start",
|
|
279
|
+
Filter.time({ operator: "between", min, max }),
|
|
280
|
+
);
|
|
281
|
+
expect(result).toEqual([
|
|
282
|
+
{
|
|
283
|
+
column_id: "start",
|
|
284
|
+
operator: "between",
|
|
285
|
+
value: { min: "09:30:00", max: "17:45:15" },
|
|
286
|
+
type: "condition",
|
|
287
|
+
negate: false,
|
|
288
|
+
},
|
|
289
|
+
]);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("handles date is_null filter", () => {
|
|
293
|
+
const result = filterToFilterCondition(
|
|
294
|
+
"created",
|
|
295
|
+
Filter.date({ operator: "is_null" }),
|
|
296
|
+
);
|
|
297
|
+
expect(result).toEqual([
|
|
298
|
+
{
|
|
299
|
+
column_id: "created",
|
|
300
|
+
operator: "is_null",
|
|
301
|
+
type: "condition",
|
|
302
|
+
negate: false,
|
|
303
|
+
},
|
|
304
|
+
]);
|
|
234
305
|
});
|
|
235
306
|
|
|
236
307
|
it("every condition has type and negate fields", () => {
|
|
@@ -44,14 +44,19 @@ import { OPERATOR_LABELS } from "./operator-labels";
|
|
|
44
44
|
import {
|
|
45
45
|
type ColumnFilterForType,
|
|
46
46
|
type ColumnFilterValue,
|
|
47
|
+
DATETIME_OPS,
|
|
47
48
|
Filter,
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
isDatetimeComparisonOp,
|
|
50
|
+
isNumberComparisonOp,
|
|
51
|
+
isTextScalarOp,
|
|
50
52
|
NUMBER_OPS,
|
|
51
53
|
TEXT_OPS,
|
|
52
|
-
TEXT_SCALAR_OPS,
|
|
53
|
-
type TextScalarOp,
|
|
54
54
|
} from "./filters";
|
|
55
|
+
import {
|
|
56
|
+
type DateLikeFilterType,
|
|
57
|
+
DateLikeInput,
|
|
58
|
+
DateLikeRangeInput,
|
|
59
|
+
} from "./date-filter-inputs";
|
|
55
60
|
import {
|
|
56
61
|
ClearFilterMenuItem,
|
|
57
62
|
FilterButtons,
|
|
@@ -283,19 +288,21 @@ export function renderMenuItemFilter<TData, TValue>(
|
|
|
283
288
|
return null;
|
|
284
289
|
}
|
|
285
290
|
|
|
286
|
-
if (
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
291
|
+
if (
|
|
292
|
+
filterType === "date" ||
|
|
293
|
+
filterType === "datetime" ||
|
|
294
|
+
filterType === "time"
|
|
295
|
+
) {
|
|
296
|
+
return (
|
|
297
|
+
<DropdownMenuSub>
|
|
298
|
+
{filterMenuItem}
|
|
299
|
+
<DropdownMenuPortal>
|
|
300
|
+
<DropdownMenuSubContent>
|
|
301
|
+
<DateFilterMenu column={column} filterType={filterType} />
|
|
302
|
+
</DropdownMenuSubContent>
|
|
303
|
+
</DropdownMenuPortal>
|
|
304
|
+
</DropdownMenuSub>
|
|
305
|
+
);
|
|
299
306
|
}
|
|
300
307
|
|
|
301
308
|
logNever(filterType);
|
|
@@ -369,12 +376,6 @@ const BooleanFilter = <TData, TValue>({
|
|
|
369
376
|
);
|
|
370
377
|
};
|
|
371
378
|
|
|
372
|
-
const NUMBER_COMPARISON_SET: ReadonlySet<OperatorType> = new Set(
|
|
373
|
-
NUMBER_COMPARISON_OPS,
|
|
374
|
-
);
|
|
375
|
-
const isNumberComparisonOp = (op: OperatorType): op is NumberComparisonOp =>
|
|
376
|
-
NUMBER_COMPARISON_SET.has(op);
|
|
377
|
-
|
|
378
379
|
type NumberComparisonFilter = Extract<
|
|
379
380
|
ColumnFilterForType<"number">,
|
|
380
381
|
{ value: number }
|
|
@@ -485,9 +486,134 @@ export const NumberFilterMenu = <TData, TValue>({
|
|
|
485
486
|
);
|
|
486
487
|
};
|
|
487
488
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
489
|
+
type DateComparisonFilter = Extract<
|
|
490
|
+
ColumnFilterForType<DateLikeFilterType>,
|
|
491
|
+
{ value: Date }
|
|
492
|
+
>;
|
|
493
|
+
const isDateComparisonFilter = (
|
|
494
|
+
filter: ColumnFilterForType<DateLikeFilterType>,
|
|
495
|
+
): filter is DateComparisonFilter => isDatetimeComparisonOp(filter.operator);
|
|
496
|
+
|
|
497
|
+
export const DateFilterMenu = <TData, TValue>({
|
|
498
|
+
column,
|
|
499
|
+
filterType,
|
|
500
|
+
}: {
|
|
501
|
+
column: Column<TData, TValue>;
|
|
502
|
+
filterType: DateLikeFilterType;
|
|
503
|
+
}) => {
|
|
504
|
+
const currentFilter = column.getFilterValue() as
|
|
505
|
+
| ColumnFilterForType<DateLikeFilterType>
|
|
506
|
+
| undefined;
|
|
507
|
+
const hasFilter = currentFilter !== undefined;
|
|
508
|
+
|
|
509
|
+
const [operator, setOperator] = useState<OperatorType>(
|
|
510
|
+
currentFilter?.operator ?? "between",
|
|
511
|
+
);
|
|
512
|
+
const [min, setMin] = useState<Date | undefined>(
|
|
513
|
+
currentFilter?.operator === "between" ? currentFilter.min : undefined,
|
|
514
|
+
);
|
|
515
|
+
const [max, setMax] = useState<Date | undefined>(
|
|
516
|
+
currentFilter?.operator === "between" ? currentFilter.max : undefined,
|
|
517
|
+
);
|
|
518
|
+
const [value, setValue] = useState<Date | undefined>(
|
|
519
|
+
currentFilter !== undefined && isDateComparisonFilter(currentFilter)
|
|
520
|
+
? currentFilter.value
|
|
521
|
+
: undefined,
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
const isComparison = isDatetimeComparisonOp(operator);
|
|
525
|
+
const isNullish = operator === "is_null" || operator === "is_not_null";
|
|
526
|
+
|
|
527
|
+
const applyDisabled =
|
|
528
|
+
(operator === "between" && (min === undefined || max === undefined)) ||
|
|
529
|
+
(isComparison && value === undefined);
|
|
530
|
+
|
|
531
|
+
const buildFilter = (
|
|
532
|
+
opts: Parameters<typeof Filter.date>[0],
|
|
533
|
+
): ColumnFilterForType<DateLikeFilterType> => {
|
|
534
|
+
switch (filterType) {
|
|
535
|
+
case "date":
|
|
536
|
+
return Filter.date(opts);
|
|
537
|
+
case "datetime":
|
|
538
|
+
return Filter.datetime(opts);
|
|
539
|
+
case "time":
|
|
540
|
+
return Filter.time(opts);
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
const handleApply = () => {
|
|
545
|
+
if (isNullish) {
|
|
546
|
+
column.setFilterValue(buildFilter({ operator }));
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
if (operator === "between" && min !== undefined && max !== undefined) {
|
|
550
|
+
column.setFilterValue(buildFilter({ operator: "between", min, max }));
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (isComparison && value !== undefined) {
|
|
554
|
+
column.setFilterValue(buildFilter({ operator, value }));
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
const [resetKey, setResetKey] = useState(0);
|
|
559
|
+
const handleClear = () => {
|
|
560
|
+
setMin(undefined);
|
|
561
|
+
setMax(undefined);
|
|
562
|
+
setValue(undefined);
|
|
563
|
+
setResetKey((k) => k + 1);
|
|
564
|
+
column.setFilterValue(undefined);
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
const handleOperatorChange = (next: OperatorType) => {
|
|
568
|
+
setOperator(next);
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
return (
|
|
572
|
+
<div
|
|
573
|
+
className="flex flex-col gap-1 pt-3 px-2"
|
|
574
|
+
onKeyDownCapture={(e) => {
|
|
575
|
+
if (e.key === "Tab") {
|
|
576
|
+
e.stopPropagation();
|
|
577
|
+
}
|
|
578
|
+
}}
|
|
579
|
+
>
|
|
580
|
+
<OperatorSelect
|
|
581
|
+
operator={operator}
|
|
582
|
+
options={DATETIME_OPS}
|
|
583
|
+
onChange={handleOperatorChange}
|
|
584
|
+
/>
|
|
585
|
+
{operator === "between" && (
|
|
586
|
+
<DateLikeRangeInput
|
|
587
|
+
key={`${filterType}-${resetKey}`}
|
|
588
|
+
filterType={filterType}
|
|
589
|
+
min={min}
|
|
590
|
+
max={max}
|
|
591
|
+
onRangeChange={(nextMin, nextMax) => {
|
|
592
|
+
setMin(nextMin);
|
|
593
|
+
setMax(nextMax);
|
|
594
|
+
}}
|
|
595
|
+
className="shadow-none! border-border hover:shadow-none!"
|
|
596
|
+
/>
|
|
597
|
+
)}
|
|
598
|
+
{isComparison && (
|
|
599
|
+
<DateLikeInput
|
|
600
|
+
key={`${filterType}-${resetKey}`}
|
|
601
|
+
filterType={filterType}
|
|
602
|
+
value={value}
|
|
603
|
+
onChange={setValue}
|
|
604
|
+
aria-label="value"
|
|
605
|
+
className="shadow-none! border-border hover:shadow-none!"
|
|
606
|
+
/>
|
|
607
|
+
)}
|
|
608
|
+
<FilterButtons
|
|
609
|
+
onApply={handleApply}
|
|
610
|
+
onClear={handleClear}
|
|
611
|
+
clearButtonDisabled={!hasFilter}
|
|
612
|
+
applyButtonDisabled={applyDisabled}
|
|
613
|
+
/>
|
|
614
|
+
</div>
|
|
615
|
+
);
|
|
616
|
+
};
|
|
491
617
|
|
|
492
618
|
export const TextFilterMenu = <TData, TValue>({
|
|
493
619
|
column,
|