@marimo-team/islands 0.23.7-dev56 → 0.23.7-dev58

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.
@@ -12,7 +12,11 @@ import { Badge } from "../ui/badge";
12
12
  import { Button } from "../ui/button";
13
13
  import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
14
14
  import { FilterPillEditor } from "./filter-pill-editor";
15
- import type { ColumnFilterValue } from "./filters";
15
+ import {
16
+ type ColumnFilterValue,
17
+ dateToISODate,
18
+ dateToISODateTime,
19
+ } from "./filters";
16
20
  import { OPERATOR_LABELS } from "./operator-labels";
17
21
  import { stringifyUnknownValue } from "./utils";
18
22
 
@@ -82,13 +86,6 @@ const FilterPill = <TData,>({
82
86
  return null;
83
87
  }
84
88
 
85
- // this is temporary, with more operator & datatype support this goes away
86
- const isReadOnly =
87
- "type" in value &&
88
- (value.type === "date" ||
89
- value.type === "datetime" ||
90
- value.type === "time");
91
-
92
89
  const twoSegment = formatted.value === undefined;
93
90
 
94
91
  const handleRemove = (e: React.MouseEvent) => {
@@ -130,20 +127,8 @@ const FilterPill = <TData,>({
130
127
  </Button>
131
128
  );
132
129
 
133
- if (isReadOnly) {
134
- return (
135
- <Badge
136
- variant="outline"
137
- className="bg-background border-border text-foreground"
138
- >
139
- {segments}
140
- {removeButton}
141
- </Badge>
142
- );
143
- }
144
-
145
130
  return (
146
- <Popover open={open} onOpenChange={setOpen} modal={true}>
131
+ <Popover open={open} onOpenChange={setOpen} modal={false}>
147
132
  <Badge
148
133
  variant="outline"
149
134
  className={cn(
@@ -249,23 +234,31 @@ function formatValue(
249
234
  };
250
235
  }
251
236
  }
252
- if (value.type === "date") {
253
- return formatMinMaxLegacy(
254
- value.min?.toISOString(),
255
- value.max?.toISOString(),
256
- );
257
- }
258
- if (value.type === "time") {
259
- return formatMinMaxLegacy(
260
- value.min ? timeFormatter.format(value.min) : undefined,
261
- value.max ? timeFormatter.format(value.max) : undefined,
262
- );
263
- }
264
- if (value.type === "datetime") {
265
- return formatMinMaxLegacy(
266
- value.min?.toISOString(),
267
- value.max?.toISOString(),
268
- );
237
+ if (
238
+ value.type === "date" ||
239
+ value.type === "datetime" ||
240
+ value.type === "time"
241
+ ) {
242
+ const format =
243
+ value.type === "time"
244
+ ? (d: Date) => timeFormatter.format(d)
245
+ : value.type === "date"
246
+ ? dateToISODate
247
+ : dateToISODateTime;
248
+ switch (value.operator) {
249
+ case "between":
250
+ return {
251
+ operator: OPERATOR_LABELS.between.toLowerCase(),
252
+ value: `${format(value.min)} - ${format(value.max)}`,
253
+ };
254
+ case "==":
255
+ case "!=":
256
+ case ">":
257
+ case ">=":
258
+ case "<":
259
+ case "<=":
260
+ return { operator: value.operator, value: format(value.value) };
261
+ }
269
262
  }
270
263
  if (value.type === "boolean") {
271
264
  return { operator: `is ${value.value ? "True" : "False"}` };
@@ -282,22 +275,3 @@ function formatValue(
282
275
  logNever(value);
283
276
  return undefined;
284
277
  }
285
-
286
- function formatMinMaxLegacy(
287
- min: string | number | undefined,
288
- max: string | number | undefined,
289
- ): FormattedFilter | undefined {
290
- if (min === undefined && max === undefined) {
291
- return;
292
- }
293
- if (min === max) {
294
- return { operator: "==", value: String(min) };
295
- }
296
- if (min === undefined) {
297
- return { operator: "<=", value: String(max) };
298
- }
299
- if (max === undefined) {
300
- return { operator: ">=", value: String(min) };
301
- }
302
- return { operator: "between", value: `${min} - ${max}` };
303
- }
@@ -51,6 +51,15 @@ export const TEXT_SCALAR_OPS = [
51
51
  "ends_with",
52
52
  ] as const;
53
53
 
54
+ export const DATETIME_COMPARISON_OPS = [
55
+ "==",
56
+ "!=",
57
+ ">",
58
+ ">=",
59
+ "<",
60
+ "<=",
61
+ ] as const;
62
+
54
63
  export const NUMBER_OPS = [
55
64
  "between",
56
65
  ...NUMBER_COMPARISON_OPS,
@@ -62,11 +71,26 @@ export const TEXT_OPS = [
62
71
  "is_empty",
63
72
  ...NULLISH_OPS,
64
73
  ] as const;
74
+ export const DATETIME_OPS = [
75
+ "between",
76
+ ...DATETIME_COMPARISON_OPS,
77
+ ...NULLISH_OPS,
78
+ ] as const;
65
79
 
66
80
  export type NullishOp = (typeof NULLISH_OPS)[number];
67
81
  export type MembershipOp = (typeof MEMBERSHIP_OPS)[number];
68
82
  export type NumberComparisonOp = (typeof NUMBER_COMPARISON_OPS)[number];
69
83
  export type TextScalarOp = (typeof TEXT_SCALAR_OPS)[number];
84
+ export type DatetimeComparisonOp = (typeof DATETIME_COMPARISON_OPS)[number];
85
+
86
+ const makeOpGuard = <T extends OperatorType>(ops: readonly T[]) => {
87
+ const set = new Set<OperatorType>(ops);
88
+ return (op: OperatorType): op is T => set.has(op);
89
+ };
90
+
91
+ export const isNumberComparisonOp = makeOpGuard(NUMBER_COMPARISON_OPS);
92
+ export const isTextScalarOp = makeOpGuard(TEXT_SCALAR_OPS);
93
+ export const isDatetimeComparisonOp = makeOpGuard(DATETIME_COMPARISON_OPS);
70
94
 
71
95
  interface NullishOpts {
72
96
  operator: NullishOp;
@@ -83,6 +107,11 @@ type TextFilterOpts =
83
107
  | { operator: "is_empty" }
84
108
  | NullishOpts;
85
109
 
110
+ type DateLikeFilterOpts =
111
+ | { operator: "between"; min: Date; max: Date }
112
+ | { operator: DatetimeComparisonOp; value: Date }
113
+ | NullishOpts;
114
+
86
115
  // Filter is a factory function that creates a filter object
87
116
  export const Filter = {
88
117
  number(opts: NumberFilterOpts) {
@@ -97,19 +126,19 @@ export const Filter = {
97
126
  ...opts,
98
127
  } as const;
99
128
  },
100
- date(opts: { min?: Date; max?: Date; operator?: OperatorType }) {
129
+ date(opts: DateLikeFilterOpts) {
101
130
  return {
102
131
  type: "date",
103
132
  ...opts,
104
133
  } as const;
105
134
  },
106
- datetime(opts: { min?: Date; max?: Date; operator?: OperatorType }) {
135
+ datetime(opts: DateLikeFilterOpts) {
107
136
  return {
108
137
  type: "datetime",
109
138
  ...opts,
110
139
  } as const;
111
140
  },
112
- time(opts: { min?: Date; max?: Date; operator?: OperatorType }) {
141
+ time(opts: DateLikeFilterOpts) {
113
142
  return {
114
143
  type: "time",
115
144
  ...opts,
@@ -135,6 +164,26 @@ export type ColumnFilterForType<T extends FilterType> = T extends FilterType
135
164
  ? Extract<ColumnFilterValue, { type: T }>
136
165
  : never;
137
166
 
167
+ function pad2(n: number): string {
168
+ return n.toString().padStart(2, "0");
169
+ }
170
+
171
+ function pad4(n: number): string {
172
+ return n.toString().padStart(4, "0");
173
+ }
174
+
175
+ export function dateToISODate(d: Date): string {
176
+ return `${pad4(d.getFullYear())}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
177
+ }
178
+
179
+ export function dateToISOTime(d: Date): string {
180
+ return `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
181
+ }
182
+
183
+ export function dateToISODateTime(d: Date): string {
184
+ return `${dateToISODate(d)}T${dateToISOTime(d)}`;
185
+ }
186
+
138
187
  function isNullishFilter(
139
188
  filter: ColumnFilterValue,
140
189
  ): filter is Extract<
@@ -235,71 +284,44 @@ export function filterToFilterCondition(
235
284
  default:
236
285
  assertNever(filter);
237
286
  }
238
- case "datetime": {
239
- const conditions: FilterConditionType[] = [];
240
- if (filter.min !== undefined) {
241
- conditions.push({
242
- column_id: columnId,
243
- operator: ">=",
244
- value: filter.min.toISOString(),
245
- type: "condition",
246
- negate: false,
247
- });
248
- }
249
- if (filter.max !== undefined) {
250
- conditions.push({
251
- column_id: columnId,
252
- operator: "<=",
253
- value: filter.max.toISOString(),
254
- type: "condition",
255
- negate: false,
256
- });
257
- }
258
- return conditions;
259
- }
260
- case "date": {
261
- const conditions: FilterConditionType[] = [];
262
- if (filter.min !== undefined) {
263
- conditions.push({
264
- column_id: columnId,
265
- operator: ">=",
266
- value: filter.min.toISOString(),
267
- type: "condition",
268
- negate: false,
269
- });
270
- }
271
- if (filter.max !== undefined) {
272
- conditions.push({
273
- column_id: columnId,
274
- operator: "<=",
275
- value: filter.max.toISOString(),
276
- type: "condition",
277
- negate: false,
278
- });
279
- }
280
- return conditions;
281
- }
287
+ case "date":
288
+ case "datetime":
282
289
  case "time": {
283
- const conditions: FilterConditionType[] = [];
284
- if (filter.min !== undefined) {
285
- conditions.push({
286
- column_id: columnId,
287
- operator: ">=",
288
- value: filter.min.toISOString(),
289
- type: "condition",
290
- negate: false,
291
- });
292
- }
293
- if (filter.max !== undefined) {
294
- conditions.push({
295
- column_id: columnId,
296
- operator: "<=",
297
- value: filter.max.toISOString(),
298
- type: "condition",
299
- negate: false,
300
- });
290
+ const encode =
291
+ filter.type === "date"
292
+ ? dateToISODate
293
+ : filter.type === "time"
294
+ ? dateToISOTime
295
+ : dateToISODateTime;
296
+ switch (filter.operator) {
297
+ case "between":
298
+ return [
299
+ {
300
+ column_id: columnId,
301
+ operator: "between",
302
+ value: { min: encode(filter.min), max: encode(filter.max) },
303
+ type: "condition",
304
+ negate: false,
305
+ },
306
+ ];
307
+ case "==":
308
+ case "!=":
309
+ case ">":
310
+ case ">=":
311
+ case "<":
312
+ case "<=":
313
+ return [
314
+ {
315
+ column_id: columnId,
316
+ operator: filter.operator,
317
+ value: encode(filter.value),
318
+ type: "condition",
319
+ negate: false,
320
+ },
321
+ ];
322
+ default:
323
+ assertNever(filter);
301
324
  }
302
- return conditions;
303
325
  }
304
326
  case "boolean":
305
327
  if (filter.value) {
@@ -55,7 +55,9 @@ export class WsTransport implements IConnectionTransport {
55
55
  // Match native EventTarget dedupe: a second addEventListener with the
56
56
  // same listener is a no-op. Without this, repeated adds leak wrappers
57
57
  // on the inner socket and double-fire on close.
58
- if (this.closeWrappers.has(userCb)) return;
58
+ if (this.closeWrappers.has(userCb)) {
59
+ return;
60
+ }
59
61
  const wrapper: ConnectionTransportCallback<"close"> = (e) => {
60
62
  if (this.inner.retryCount >= MAX_RETRIES) {
61
63
  userCb(