Skip to content

Commit fec0bb0

Browse files
committed
feat: support filtering
1 parent bfc38a2 commit fec0bb0

23 files changed

+1008
-40
lines changed

components/DialogAddUpdateLocal.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export function DialogAddUpdateLocal(
9090
name="path"
9191
id="path"
9292
value={store?.path}
93-
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500 w-72 md:w-96"
93+
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500 md:w-96"
9494
placeholder="Path (on server) to local store"
9595
required
9696
>
@@ -103,8 +103,9 @@ export function DialogAddUpdateLocal(
103103
type="text"
104104
name="name"
105105
id="name"
106+
autoComplete="off"
106107
value={store?.name}
107-
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500 w-72 md:w-96"
108+
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500 md:w-96"
108109
placeholder="Friendly name (optional)"
109110
>
110111
</input>

components/DialogAddUpdateRemote.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ export function DialogAddUpdateRemote(
122122
<input
123123
type="text"
124124
name="name"
125+
autoComplete="off"
125126
id="name"
126127
value={store?.name}
127128
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"

components/DialogQuery.tsx

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import type { KvFilterJSON } from "@kitsonk/kv-toolbox/query";
2+
import { useRef } from "preact/hooks";
3+
import { type Signal, useComputed, useSignal } from "@preact/signals";
4+
import { addNotification } from "$utils/state.ts";
5+
6+
import { CloseButton } from "./CloseButton.tsx";
7+
import { Dialog } from "./Dialog.tsx";
8+
import { type KvFilterIndeterminateJSON, QueryFilter } from "./QueryFilter.tsx";
9+
import IconPlus from "./icons/Plus.tsx";
10+
11+
export function DialogQuery(
12+
{ open, filters, active }: {
13+
open: Signal<boolean>;
14+
filters: Signal<KvFilterJSON[]>;
15+
active: Signal<boolean>;
16+
},
17+
) {
18+
const form = useRef<HTMLFormElement>(null);
19+
const localFilters = filters.peek().length
20+
? useSignal<(KvFilterJSON | KvFilterIndeterminateJSON)[]>(
21+
[...filters.value],
22+
)
23+
: useSignal<(KvFilterJSON | KvFilterIndeterminateJSON)[]>([{ kind: "" }]);
24+
25+
const handleFilterOnChange = (index: number, value: KvFilterJSON) =>
26+
localFilters.value = localFilters.value.map((filter, i) =>
27+
i === index ? value : filter
28+
);
29+
const handleFilterOnRemove = (index: number) =>
30+
localFilters.value = localFilters.value.filter((_, i) => i !== index);
31+
const handleAddOnClick = () => {
32+
localFilters.value = [...localFilters.value, { kind: "" }];
33+
};
34+
const filterComponents = useComputed(() =>
35+
localFilters.value.map((filter, index) => (
36+
<QueryFilter
37+
key={index}
38+
index={index}
39+
id={String(index)}
40+
filter={filter}
41+
onChange={handleFilterOnChange}
42+
onRemove={handleFilterOnRemove}
43+
/>
44+
))
45+
);
46+
return (
47+
<Dialog
48+
class="p-4 bg-white rounded-lg shadow dark:bg-gray-800 sm:p-5"
49+
open={open}
50+
>
51+
<div class="flex justify-between items-center pb-4 mb-4 rounded-t border-b sm:mb-5 dark:border-gray-600">
52+
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
53+
Filter
54+
</h3>
55+
<CloseButton
56+
onClick={() => {
57+
form.current?.reset();
58+
localFilters.value = filters.value.length
59+
? [...filters.value]
60+
: [{ kind: "" }];
61+
open.value = false;
62+
}}
63+
/>
64+
</div>
65+
<form
66+
method="dialog"
67+
ref={form}
68+
class="z-10 w-full max-w-screen-md space-y-3"
69+
onSubmit={(_) => {
70+
filters.value = localFilters.value.filter(
71+
(filter) => filter.kind !== "",
72+
);
73+
if (filters.value.length > 0) {
74+
active.value = true;
75+
addNotification("Filter applied", "success", true, 5);
76+
}
77+
open.value = false;
78+
}}
79+
>
80+
{filterComponents}
81+
<a
82+
href="#"
83+
class="flex items-center pb-2 text-sm font-medium border-b dark:border-gray-600 text-primary-600 dark:text-primary-500 hover:underline"
84+
onClick={handleAddOnClick}
85+
>
86+
<IconPlus />
87+
Add Condition
88+
</a>
89+
<div class="flex items-center justify-between">
90+
<button
91+
type="submit"
92+
class="text-white bg-primary-600 hover:bg-primary-800 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:focus:ring-primary-800"
93+
>
94+
Apply
95+
</button>
96+
<button
97+
type="reset"
98+
class="py-2.5 px-5 flex items-center hover:bg-gray-100 dark:hover:bg-gray-600 text-sm font-medium text-gray-900 focus:outline-none rounded-lg hover:text-black focus:z-10 focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 dark:text-gray-400 dark:hover:text-white"
99+
onClick={() => {
100+
form.current?.reset();
101+
localFilters.value = [{ kind: "" }];
102+
active.value = false;
103+
}}
104+
>
105+
<svg
106+
xmlns="http://www.w3.org/2000/svg"
107+
class="w-4 h-4 mr-1"
108+
viewBox="0 0 20 20"
109+
fill="currentColor"
110+
>
111+
<path
112+
fill-rule="evenodd"
113+
clip-rule="evenodd"
114+
aria-hidden="true"
115+
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
116+
/>
117+
</svg>
118+
Clear all
119+
</button>
120+
</div>
121+
</form>
122+
</Dialog>
123+
);
124+
}

components/KvKey.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1-
import { type KvKeyJSON } from "@deno/kv-utils/json";
1+
import { type KvKeyJSON, KvKeyPartJSON } from "@deno/kv-utils/json";
22
import { type ComponentChildren } from "preact";
33
import { type Signal } from "@preact/signals";
44

55
import IconHome from "./icons/Home.tsx";
66
import { KvKeyPart } from "./KvKeyPart.tsx";
77

8+
function isKvKeyJSON(value: unknown): value is KvKeyJSON {
9+
return Array.isArray(value);
10+
}
11+
812
export function KvKey(
913
{ value, entry, showRoot, noLink }: {
1014
value: Signal<KvKeyJSON | undefined> | KvKeyJSON;
@@ -16,7 +20,7 @@ export function KvKey(
1620
let key: KvKeyJSON;
1721
let isSignal;
1822
let onClick;
19-
if (Array.isArray(value)) {
23+
if (isKvKeyJSON(value)) {
2024
isSignal = false;
2125
key = value;
2226
} else {
@@ -38,7 +42,7 @@ export function KvKey(
3842
key = [...key];
3943
const children: ComponentChildren[] = [];
4044
let part;
41-
while ((part = key.pop())) {
45+
while ((part = (key as KvKeyPartJSON[]).pop())) {
4246
children.unshift(
4347
<KvKeyPart
4448
part={part}

components/KvSimpleValueEditor.tsx

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import type { Kinds } from "@kitsonk/kv-toolbox/query";
2+
3+
const TYPE_OPTIONS: [Kinds, string][] = [
4+
["string", "String"],
5+
["number", "Number"],
6+
["bigint", "BigInt"],
7+
["boolean", "Boolean"],
8+
["undefined", "Undefined"],
9+
["null", "Null"],
10+
["Array", "Array"],
11+
["Map", "Map"],
12+
["Set", "Set"],
13+
["object", "JSON"],
14+
["RegExp", "RegExp"],
15+
["Date", "Date"],
16+
["KvU64", "KvU64"],
17+
["ArrayBuffer", "ArrayBuffer"],
18+
["DataView", "DataView"],
19+
["Int8Array", "Int8Array"],
20+
["Uint8Array", "Uint8Array"],
21+
["Uint8ClampedArray", "Uint8ClampedArray"],
22+
["Int16Array", "Int16Array"],
23+
["Uint16Array", "Uint16Array"],
24+
["Int32Array", "Int32Array"],
25+
["Uint32Array", "Uint32Array"],
26+
["Float32Array", "Float32Array"],
27+
["Float64Array", "Float64Array"],
28+
["BigInt64Array", "BigInt64Array"],
29+
["BigUint64Array", "BigUint64Array"],
30+
];
31+
32+
function Editor(
33+
{ id, type, value, onChange }: {
34+
id: string;
35+
type: string;
36+
value: string;
37+
onChange: (event: Event) => void;
38+
},
39+
) {
40+
switch (type) {
41+
case "number":
42+
return (
43+
<input
44+
id={`value-${id}`}
45+
name={`value-${id}`}
46+
type="text"
47+
class="bg-white border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500 invalid:border-red-700"
48+
pattern="-?\d+(\.\d+)?|-?Infinity|NaN"
49+
placeholder="Number"
50+
required
51+
onChange={onChange}
52+
value={value}
53+
/>
54+
);
55+
case "bigint":
56+
case "KvU64":
57+
return (
58+
<input
59+
id={`value-${id}`}
60+
name={`value-${id}`}
61+
type="number"
62+
class="bg-white border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500 invalid:border-red-700"
63+
placeholder="Number"
64+
required
65+
onChange={onChange}
66+
value={value}
67+
/>
68+
);
69+
case "boolean":
70+
return (
71+
<select
72+
id={`value-${id}`}
73+
name={`value-${id}`}
74+
class="bg-white border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
75+
onChange={onChange}
76+
value={value}
77+
>
78+
<option>true</option>
79+
<option>false</option>
80+
</select>
81+
);
82+
case "null":
83+
case "undefined":
84+
return (
85+
<input
86+
id={`value-${id}`}
87+
name={`value-${id}`}
88+
type="text"
89+
class="bg-white border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
90+
value={type === "null" ? "null" : "undefined"}
91+
readOnly
92+
/>
93+
);
94+
case "RegExp":
95+
return (
96+
<input
97+
id={`value-${id}`}
98+
name={`value-${id}`}
99+
type="text"
100+
class="bg-white border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500 invalid:border-red-700"
101+
pattern="\/(?![*+?])([^\r\n\[\/\\]|\\.|\[([^\r\n\]\\]|\\.)*\])+/(g(im?|mi?)?|i(gm?|mg?)?|m(gi?|ig?)?)?"
102+
placeholder="RegExp"
103+
required
104+
onChange={onChange}
105+
value={value}
106+
/>
107+
);
108+
case "Date":
109+
return (
110+
<input
111+
id={`value-${id}`}
112+
name={`value-${id}`}
113+
type="text"
114+
class="bg-white border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500 invalid:border-red-700"
115+
pattern="[0-9]{4}-((0[13578]|1[02])-(0[1-9]|[12][0-9]|3[01])|(0[469]|11)-(0[1-9]|[12][0-9]|30)|(02)-(0[1-9]|[12][0-9]))T(0[0-9]|1[0-9]|2[0-3]):(0[0-9]|[1-5][0-9]):(0[0-9]|[1-5][0-9])\.[0-9]{3}Z"
116+
placeholder="Date"
117+
required
118+
onChange={onChange}
119+
value={value}
120+
/>
121+
);
122+
case "ArrayBuffer":
123+
case "DataView":
124+
case "Int8Array":
125+
case "Uint8Array":
126+
case "Uint8ClampedArray":
127+
case "Int16Array":
128+
case "Uint16Array":
129+
case "Int32Array":
130+
case "Uint32Array":
131+
case "Float32Array":
132+
case "Float64Array":
133+
case "BigInt64Array":
134+
case "BigUint64Array":
135+
return (
136+
<input
137+
id={`value-${id}`}
138+
name={`value-${id}`}
139+
type="text"
140+
class="bg-white border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500 invalid:border-red-700"
141+
pattern="[-A-Za-z0-9+/]*={0,3}"
142+
placeholder="Base64"
143+
required
144+
onChange={onChange}
145+
value={value}
146+
/>
147+
);
148+
default:
149+
return (
150+
<input
151+
id={`value-${id}`}
152+
name={`value-${id}`}
153+
type="text"
154+
class="bg-white border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
155+
placeholder="Value"
156+
required
157+
onChange={onChange}
158+
value={value}
159+
/>
160+
);
161+
}
162+
}
163+
164+
export function KvSimpleValueEditor(
165+
{ type, id, value, only, onChange }: {
166+
type: Kinds;
167+
id: string;
168+
value: string;
169+
only?: Kinds[] | undefined;
170+
onChange: (type: Kinds, value: string) => void;
171+
},
172+
) {
173+
const handleValueOnChange = (event: Event) => {
174+
onChange(type, (event.currentTarget as HTMLInputElement).value);
175+
};
176+
177+
return (
178+
<>
179+
<select
180+
id={`value_type-${id}`}
181+
name={`value_type-${id}`}
182+
value={type}
183+
class="bg-white border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
184+
onChange={(evt) => onChange(evt.currentTarget.value as Kinds, value)}
185+
>
186+
{TYPE_OPTIONS
187+
.filter(([type]) => only ? only.includes(type) : true)
188+
.map(([type, label]) => <option value={type}>{label}</option>)}
189+
</select>
190+
<Editor
191+
id={id}
192+
type={type}
193+
value={value}
194+
onChange={handleValueOnChange}
195+
/>
196+
</>
197+
);
198+
}

0 commit comments

Comments
 (0)