tsense 0.0.17 → 0.2.0-next.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.
- package/README.md +99 -104
- package/dist/filters/filter-state.d.ts +22 -0
- package/dist/filters/filter-state.js +145 -0
- package/dist/filters/index.d.ts +40 -0
- package/dist/filters/index.js +128 -0
- package/dist/filters/url.d.ts +3 -0
- package/dist/filters/url.js +93 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/migrator.d.ts +1 -1
- package/dist/migrator.js +70 -88
- package/dist/rank.d.ts +7 -0
- package/dist/rank.js +12 -0
- package/dist/react/filter-builder.d.ts +60 -0
- package/dist/react/filter-builder.js +102 -0
- package/dist/react/index.d.ts +4 -0
- package/dist/react/index.js +2 -0
- package/dist/react/use-filter-builder.d.ts +23 -0
- package/dist/react/use-filter-builder.js +22 -0
- package/dist/tsense.d.ts +5 -4
- package/dist/tsense.js +388 -374
- package/dist/types.d.ts +37 -10
- package/package.json +69 -51
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const ARRAY_OPERATORS = new Set(["notIn"]);
|
|
2
|
+
function serializeValue(value) {
|
|
3
|
+
if (value instanceof Date) {
|
|
4
|
+
return value.toISOString();
|
|
5
|
+
}
|
|
6
|
+
return String(value);
|
|
7
|
+
}
|
|
8
|
+
function coerceValue(raw, columnType) {
|
|
9
|
+
if (columnType === "number") {
|
|
10
|
+
return Number(raw);
|
|
11
|
+
}
|
|
12
|
+
if (columnType === "boolean") {
|
|
13
|
+
return raw === "true";
|
|
14
|
+
}
|
|
15
|
+
if (columnType === "date") {
|
|
16
|
+
return new Date(raw);
|
|
17
|
+
}
|
|
18
|
+
return raw;
|
|
19
|
+
}
|
|
20
|
+
export function serializeFilter(filter, descriptor) {
|
|
21
|
+
const params = new URLSearchParams();
|
|
22
|
+
const columnMap = new Map(descriptor.columns.map((c) => [c.key, c]));
|
|
23
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
24
|
+
const column = columnMap.get(key);
|
|
25
|
+
if (!column || value == null) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (Array.isArray(value)) {
|
|
29
|
+
if (value.length === 1) {
|
|
30
|
+
params.set(key, serializeValue(value[0]));
|
|
31
|
+
}
|
|
32
|
+
else if (value.length > 1) {
|
|
33
|
+
params.set(key, value.map((v) => serializeValue(v)).join(","));
|
|
34
|
+
}
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (typeof value === "object" && !(value instanceof Date)) {
|
|
38
|
+
for (const [op, opValue] of Object.entries(value)) {
|
|
39
|
+
if (opValue == null) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (Array.isArray(opValue)) {
|
|
43
|
+
params.set(`${key}.${op}`, opValue.map((v) => serializeValue(v)).join(","));
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
params.set(`${key}.${op}`, serializeValue(opValue));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
params.set(key, serializeValue(value));
|
|
52
|
+
}
|
|
53
|
+
return params;
|
|
54
|
+
}
|
|
55
|
+
export function deserializeFilter(params, descriptor) {
|
|
56
|
+
const result = {};
|
|
57
|
+
const columnMap = new Map(descriptor.columns.map((c) => [c.key, c]));
|
|
58
|
+
for (const [paramKey, rawValue] of params.entries()) {
|
|
59
|
+
const dotIndex = paramKey.indexOf(".");
|
|
60
|
+
if (dotIndex !== -1) {
|
|
61
|
+
const field = paramKey.slice(0, dotIndex);
|
|
62
|
+
const operator = paramKey.slice(dotIndex + 1);
|
|
63
|
+
const column = columnMap.get(field);
|
|
64
|
+
if (!column) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const existing = (result[field] ?? {});
|
|
68
|
+
if (ARRAY_OPERATORS.has(operator)) {
|
|
69
|
+
existing[operator] = rawValue
|
|
70
|
+
.split(",")
|
|
71
|
+
.map((v) => coerceValue(v, column.type));
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
existing[operator] = coerceValue(rawValue, column.type);
|
|
75
|
+
}
|
|
76
|
+
result[field] = existing;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const column = columnMap.get(paramKey);
|
|
80
|
+
if (!column) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (rawValue.includes(",")) {
|
|
84
|
+
result[paramKey] = rawValue
|
|
85
|
+
.split(",")
|
|
86
|
+
.map((v) => coerceValue(v, column.type));
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
result[paramKey] = coerceValue(rawValue, column.type);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export type { TsenseFieldMeta, TsenseFieldType } from "./env.js";
|
|
2
|
+
export { rank } from "./rank.js";
|
|
2
3
|
export { DateTransformer } from "./transformers/date.js";
|
|
3
4
|
export { defaultTransformers } from "./transformers/defaults.js";
|
|
4
5
|
export type { FieldTransformer } from "./transformers/types.js";
|
|
5
6
|
export { TSense } from "./tsense.js";
|
|
6
|
-
export type { ConnectionConfig, DeleteResult, FilterFor, HighlightOptions, ProjectSearch, SearchListOptions, SearchListResult, SearchOptions, SearchOptionsPlain, SearchOptionsWithOmit, SearchOptionsWithPick, SearchResult, SyncConfig, SyncOptions, SyncResult, TsenseOptions, UpdateResult, UpsertResult, } from "./types.js";
|
|
7
|
+
export type { ConnectionConfig, DeleteResult, FilterFor, HighlightOptions, NumberFilter, ProjectSearch, SearchListOptions, SearchListResult, SearchInput, SearchOptions, ScopedCollection, SearchOptionsPlain, SearchOptionsWithOmit, SearchOptionsWithPick, SearchResult, StringFilter, SyncConfig, SyncOptions, SyncResult, TsenseOptions, UpdateResult, UpsertResult, } from "./types.js";
|
package/dist/index.js
CHANGED
package/dist/migrator.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export declare class TSenseMigrator {
|
|
|
5
5
|
private localFields;
|
|
6
6
|
private defaultSortingField;
|
|
7
7
|
private axios;
|
|
8
|
-
constructor(collectionName: string, localFields: FieldSchema[], defaultSortingField: string | undefined, axios: AxiosInstance);
|
|
8
|
+
constructor(collectionName: string, localFields: readonly FieldSchema[], defaultSortingField: string | undefined, axios: AxiosInstance);
|
|
9
9
|
sync(): Promise<void>;
|
|
10
10
|
private exists;
|
|
11
11
|
private getRemoteFields;
|
package/dist/migrator.js
CHANGED
|
@@ -1,61 +1,50 @@
|
|
|
1
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
-
});
|
|
9
|
-
};
|
|
10
1
|
const COMPARABLE_KEYS = ["type", "facet", "sort", "index", "optional"];
|
|
11
|
-
const NESTED_TYPES = ["object", "object[]"];
|
|
2
|
+
const NESTED_TYPES = new Set(["object", "object[]"]);
|
|
12
3
|
export class TSenseMigrator {
|
|
4
|
+
collectionName;
|
|
5
|
+
localFields;
|
|
6
|
+
defaultSortingField;
|
|
7
|
+
axios;
|
|
13
8
|
constructor(collectionName, localFields, defaultSortingField, axios) {
|
|
14
9
|
this.collectionName = collectionName;
|
|
15
10
|
this.localFields = localFields;
|
|
16
11
|
this.defaultSortingField = defaultSortingField;
|
|
17
12
|
this.axios = axios;
|
|
18
13
|
}
|
|
19
|
-
sync() {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
yield this.create();
|
|
35
|
-
});
|
|
14
|
+
async sync() {
|
|
15
|
+
const exists = await this.exists();
|
|
16
|
+
if (!exists) {
|
|
17
|
+
return await this.create();
|
|
18
|
+
}
|
|
19
|
+
const remoteFields = await this.getRemoteFields();
|
|
20
|
+
const diff = this.diff(remoteFields);
|
|
21
|
+
if (!diff.toAdd.length && !diff.toRemove.length && !diff.toModify.length) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const patched = await this.patch(diff);
|
|
25
|
+
if (patched)
|
|
26
|
+
return;
|
|
27
|
+
await this.drop();
|
|
28
|
+
await this.create();
|
|
36
29
|
}
|
|
37
|
-
exists() {
|
|
38
|
-
return
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
throw e;
|
|
48
|
-
});
|
|
30
|
+
async exists() {
|
|
31
|
+
return await this.axios({
|
|
32
|
+
method: "GET",
|
|
33
|
+
url: `/collections/${this.collectionName}`,
|
|
34
|
+
})
|
|
35
|
+
.then(() => true)
|
|
36
|
+
.catch((e) => {
|
|
37
|
+
if (e.status === 404)
|
|
38
|
+
return false;
|
|
39
|
+
throw e;
|
|
49
40
|
});
|
|
50
41
|
}
|
|
51
|
-
getRemoteFields() {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
url: `/collections/${this.collectionName}`,
|
|
56
|
-
});
|
|
57
|
-
return data.fields;
|
|
42
|
+
async getRemoteFields() {
|
|
43
|
+
const { data } = await this.axios({
|
|
44
|
+
method: "GET",
|
|
45
|
+
url: `/collections/${this.collectionName}`,
|
|
58
46
|
});
|
|
47
|
+
return data.fields;
|
|
59
48
|
}
|
|
60
49
|
diff(remote) {
|
|
61
50
|
const remoteByName = new Map(remote.map((f) => [f.name, f]));
|
|
@@ -85,57 +74,50 @@ export class TSenseMigrator {
|
|
|
85
74
|
return { toAdd, toRemove, toModify };
|
|
86
75
|
}
|
|
87
76
|
fieldsMatch(local, remote) {
|
|
88
|
-
var _a, _b;
|
|
89
77
|
for (const key of COMPARABLE_KEYS) {
|
|
90
|
-
if ((
|
|
78
|
+
if ((local[key] ?? undefined) !== (remote[key] ?? undefined)) {
|
|
91
79
|
return false;
|
|
92
80
|
}
|
|
93
81
|
}
|
|
94
82
|
return true;
|
|
95
83
|
}
|
|
96
|
-
patch(diff) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
.catch(() => false);
|
|
116
|
-
});
|
|
84
|
+
async patch(diff) {
|
|
85
|
+
const fields = [];
|
|
86
|
+
for (const field of diff.toRemove) {
|
|
87
|
+
fields.push({ name: field.name, drop: true });
|
|
88
|
+
}
|
|
89
|
+
for (const field of diff.toModify) {
|
|
90
|
+
fields.push({ name: field.name, drop: true });
|
|
91
|
+
fields.push(field);
|
|
92
|
+
}
|
|
93
|
+
for (const field of diff.toAdd) {
|
|
94
|
+
fields.push(field);
|
|
95
|
+
}
|
|
96
|
+
return await this.axios({
|
|
97
|
+
method: "PATCH",
|
|
98
|
+
url: `/collections/${this.collectionName}`,
|
|
99
|
+
data: { fields },
|
|
100
|
+
})
|
|
101
|
+
.then(() => true)
|
|
102
|
+
.catch(() => false);
|
|
117
103
|
}
|
|
118
|
-
create() {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
},
|
|
130
|
-
});
|
|
104
|
+
async create() {
|
|
105
|
+
const enable_nested_fields = this.localFields.some((f) => NESTED_TYPES.has(f.type));
|
|
106
|
+
await this.axios({
|
|
107
|
+
method: "POST",
|
|
108
|
+
url: "/collections",
|
|
109
|
+
data: {
|
|
110
|
+
name: this.collectionName,
|
|
111
|
+
fields: this.localFields,
|
|
112
|
+
default_sorting_field: this.defaultSortingField,
|
|
113
|
+
enable_nested_fields,
|
|
114
|
+
},
|
|
131
115
|
});
|
|
132
116
|
}
|
|
133
|
-
drop() {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
url: `/collections/${this.collectionName}`,
|
|
138
|
-
});
|
|
117
|
+
async drop() {
|
|
118
|
+
await this.axios({
|
|
119
|
+
method: "DELETE",
|
|
120
|
+
url: `/collections/${this.collectionName}`,
|
|
139
121
|
});
|
|
140
122
|
}
|
|
141
123
|
}
|
package/dist/rank.d.ts
ADDED
package/dist/rank.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function rank({ db, id_key, ts, }) {
|
|
2
|
+
const mapped = new Map(ts.map((t, i) => [t.id, i]));
|
|
3
|
+
const result = [];
|
|
4
|
+
for (const item of db) {
|
|
5
|
+
const position = mapped.get(String(item[id_key]));
|
|
6
|
+
if (position == null) {
|
|
7
|
+
continue;
|
|
8
|
+
}
|
|
9
|
+
result[position] = item;
|
|
10
|
+
}
|
|
11
|
+
return result.filter(Boolean);
|
|
12
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { FilterValue } from "../filters/filter-state.js";
|
|
3
|
+
import type { FilterDescriptor } from "../filters/index.js";
|
|
4
|
+
import type { FilterFor } from "../types.js";
|
|
5
|
+
export type FieldSelectSlotProps = {
|
|
6
|
+
columns: FilterDescriptor["columns"];
|
|
7
|
+
value: string | undefined;
|
|
8
|
+
onChange: (field: string) => void;
|
|
9
|
+
};
|
|
10
|
+
export type ConditionSelectSlotProps = {
|
|
11
|
+
conditions: FilterDescriptor["columns"][number]["conditions"];
|
|
12
|
+
value: string | undefined;
|
|
13
|
+
onChange: (condition: string) => void;
|
|
14
|
+
disabled: boolean;
|
|
15
|
+
};
|
|
16
|
+
export type ValueInputSlotProps = {
|
|
17
|
+
column: FilterDescriptor["columns"][number];
|
|
18
|
+
condition: string;
|
|
19
|
+
value: FilterValue;
|
|
20
|
+
onChange: (value: FilterValue) => void;
|
|
21
|
+
};
|
|
22
|
+
export type RemoveButtonSlotProps = {
|
|
23
|
+
onClick: () => void;
|
|
24
|
+
};
|
|
25
|
+
export type AddButtonSlotProps = {
|
|
26
|
+
onClick: () => void;
|
|
27
|
+
};
|
|
28
|
+
export type PresetButtonSlotProps = {
|
|
29
|
+
preset: {
|
|
30
|
+
field: string;
|
|
31
|
+
name: string;
|
|
32
|
+
};
|
|
33
|
+
onClick: () => void;
|
|
34
|
+
};
|
|
35
|
+
export type RowSlotProps = {
|
|
36
|
+
index: number;
|
|
37
|
+
fieldSelect: ReactNode;
|
|
38
|
+
conditionSelect: ReactNode;
|
|
39
|
+
valueInput: ReactNode | null;
|
|
40
|
+
removeButton: ReactNode;
|
|
41
|
+
};
|
|
42
|
+
export type RootSlotProps = {
|
|
43
|
+
rows: ReactNode;
|
|
44
|
+
addButton: ReactNode;
|
|
45
|
+
presets: ReactNode | null;
|
|
46
|
+
};
|
|
47
|
+
type FilterBuilderProps<T> = {
|
|
48
|
+
descriptor: FilterDescriptor<T>;
|
|
49
|
+
onChange?: (filter: FilterFor<T>) => void;
|
|
50
|
+
renderRoot?: (props: RootSlotProps) => ReactNode;
|
|
51
|
+
renderRow?: (props: RowSlotProps) => ReactNode;
|
|
52
|
+
renderFieldSelect?: (props: FieldSelectSlotProps) => ReactNode;
|
|
53
|
+
renderConditionSelect?: (props: ConditionSelectSlotProps) => ReactNode;
|
|
54
|
+
renderValueInput?: (props: ValueInputSlotProps) => ReactNode;
|
|
55
|
+
renderAddButton?: (props: AddButtonSlotProps) => ReactNode;
|
|
56
|
+
renderRemoveButton?: (props: RemoveButtonSlotProps) => ReactNode;
|
|
57
|
+
renderPresetButton?: (props: PresetButtonSlotProps) => ReactNode;
|
|
58
|
+
};
|
|
59
|
+
export declare function FilterBuilder<T>({ descriptor, onChange, renderRoot, renderRow, renderFieldSelect, renderConditionSelect, renderValueInput, renderAddButton, renderRemoveButton, renderPresetButton }: FilterBuilderProps<T>): ReactNode;
|
|
60
|
+
export {};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, useEffect, useRef } from "react";
|
|
3
|
+
import { useFilterBuilder } from "./use-filter-builder.js";
|
|
4
|
+
function defaultFieldSelect({ columns, value, onChange, }) {
|
|
5
|
+
return (_jsxs("select", { className: "rounded border px-2 py-1", value: value ?? "", onChange: (e) => onChange(e.target.value), children: [_jsx("option", { value: "", children: "Column" }), columns.map((col) => (_jsx("option", { value: col.key, children: col.label }, col.key)))] }));
|
|
6
|
+
}
|
|
7
|
+
function defaultConditionSelect({ conditions, value, onChange, disabled, }) {
|
|
8
|
+
return (_jsxs("select", { className: "rounded border px-2 py-1", value: value ?? "", onChange: (e) => onChange(e.target.value), disabled: disabled, children: [_jsx("option", { value: "", children: "Condition" }), conditions.map((c) => (_jsx("option", { value: c.key, children: c.label }, c.key)))] }));
|
|
9
|
+
}
|
|
10
|
+
function formatDateForInput(date) {
|
|
11
|
+
if (!(date instanceof Date))
|
|
12
|
+
return "";
|
|
13
|
+
return date.toISOString().slice(0, 10);
|
|
14
|
+
}
|
|
15
|
+
function parseDateInput(str) {
|
|
16
|
+
if (!str)
|
|
17
|
+
return undefined;
|
|
18
|
+
return new Date(str + "T00:00:00Z");
|
|
19
|
+
}
|
|
20
|
+
function asTuple(value) {
|
|
21
|
+
if (Array.isArray(value)) {
|
|
22
|
+
return [value[0], value[1]];
|
|
23
|
+
}
|
|
24
|
+
return [undefined, undefined];
|
|
25
|
+
}
|
|
26
|
+
function defaultValueInput({ column, condition, value, onChange, }) {
|
|
27
|
+
if (condition === "between" && column.type === "date") {
|
|
28
|
+
const tuple = asTuple(value);
|
|
29
|
+
return (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx("input", { className: "rounded border px-2 py-1", type: "date", value: formatDateForInput(tuple[0]), onChange: (e) => onChange([parseDateInput(e.target.value), tuple[1]]) }), _jsx("span", { className: "text-sm text-gray-500", children: "and" }), _jsx("input", { className: "rounded border px-2 py-1", type: "date", value: formatDateForInput(tuple[1]), onChange: (e) => onChange([tuple[0], parseDateInput(e.target.value)]) })] }));
|
|
30
|
+
}
|
|
31
|
+
if (condition === "between") {
|
|
32
|
+
const tuple = asTuple(value);
|
|
33
|
+
return (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx("input", { className: "w-24 rounded border px-2 py-1", type: "number", value: String(tuple[0] ?? ""), onChange: (e) => onChange([Number(e.target.value), tuple[1]]) }), _jsx("span", { className: "text-sm text-gray-500", children: "and" }), _jsx("input", { className: "w-24 rounded border px-2 py-1", type: "number", value: String(tuple[1] ?? ""), onChange: (e) => onChange([tuple[0], Number(e.target.value)]) })] }));
|
|
34
|
+
}
|
|
35
|
+
if (column.values) {
|
|
36
|
+
const selected = Array.isArray(value) ? value : [];
|
|
37
|
+
return (_jsx("div", { className: "flex flex-wrap gap-2", children: column.values.map((v) => {
|
|
38
|
+
const checked = selected.includes(v.value);
|
|
39
|
+
return (_jsxs("label", { className: "flex items-center gap-1 text-sm", children: [_jsx("input", { type: "checkbox", checked: checked, onChange: () => onChange(checked
|
|
40
|
+
? selected.filter((s) => s !== v.value)
|
|
41
|
+
: [...selected, v.value]) }), v.label] }, v.value));
|
|
42
|
+
}) }));
|
|
43
|
+
}
|
|
44
|
+
if (column.type === "date") {
|
|
45
|
+
return (_jsx("input", { className: "rounded border px-2 py-1", type: "date", value: formatDateForInput(value), onChange: (e) => onChange(parseDateInput(e.target.value)) }));
|
|
46
|
+
}
|
|
47
|
+
if (column.type === "number") {
|
|
48
|
+
return (_jsx("input", { className: "rounded border px-2 py-1", type: "number", value: String(value ?? ""), onChange: (e) => onChange(Number(e.target.value)) }));
|
|
49
|
+
}
|
|
50
|
+
if (column.type === "boolean") {
|
|
51
|
+
return (_jsxs("select", { className: "rounded border px-2 py-1", value: value == null ? "" : String(value), onChange: (e) => onChange(e.target.value === "true"), children: [_jsx("option", { value: "", children: "Value" }), _jsx("option", { value: "true", children: "true" }), _jsx("option", { value: "false", children: "false" })] }));
|
|
52
|
+
}
|
|
53
|
+
return (_jsx("input", { className: "rounded border px-2 py-1", type: "text", value: typeof value === "string" ? value : "", onChange: (e) => onChange(e.target.value) }));
|
|
54
|
+
}
|
|
55
|
+
function defaultRemoveButton({ onClick }) {
|
|
56
|
+
return (_jsx("button", { className: "text-red-500 hover:text-red-700", onClick: onClick, children: "\u00D7" }));
|
|
57
|
+
}
|
|
58
|
+
function defaultAddButton({ onClick }) {
|
|
59
|
+
return (_jsx("button", { className: "text-blue-500 hover:text-blue-700", onClick: onClick, children: "+ Add filter" }));
|
|
60
|
+
}
|
|
61
|
+
function defaultPresetButton({ preset, onClick }) {
|
|
62
|
+
return (_jsx("button", { className: "rounded bg-gray-100 px-2 py-1 text-sm hover:bg-gray-200", onClick: onClick, children: preset.name }));
|
|
63
|
+
}
|
|
64
|
+
export function FilterBuilder({ descriptor, onChange, renderRoot, renderRow, renderFieldSelect, renderConditionSelect, renderValueInput, renderAddButton, renderRemoveButton, renderPresetButton, }) {
|
|
65
|
+
const filters = useFilterBuilder(descriptor);
|
|
66
|
+
const onChangeRef = useRef(onChange);
|
|
67
|
+
onChangeRef.current = onChange;
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
onChangeRef.current?.(filters.result);
|
|
70
|
+
}, [filters.result]);
|
|
71
|
+
const FieldSelect = renderFieldSelect ?? defaultFieldSelect;
|
|
72
|
+
const ConditionSelect = renderConditionSelect ?? defaultConditionSelect;
|
|
73
|
+
const ValueInput = renderValueInput ?? defaultValueInput;
|
|
74
|
+
const RemoveButton = renderRemoveButton ?? defaultRemoveButton;
|
|
75
|
+
const AddButton = renderAddButton ?? defaultAddButton;
|
|
76
|
+
const PresetButton = renderPresetButton ?? defaultPresetButton;
|
|
77
|
+
const rows = filters.rows.map((row, i) => {
|
|
78
|
+
const fieldSelect = (_jsx(FieldSelect, { columns: filters.columns, value: row.field, onChange: (field) => filters.setField(i, field) }));
|
|
79
|
+
const conditionSelect = (_jsx(ConditionSelect, { conditions: row.field ? filters.conditionsFor(row.field) : [], value: row.condition, onChange: (condition) => filters.setCondition(i, condition), disabled: !row.field }));
|
|
80
|
+
const column = row.field
|
|
81
|
+
? filters.columns.find((c) => c.key === row.field)
|
|
82
|
+
: undefined;
|
|
83
|
+
const valueInput = row.field && row.condition && column ? (_jsx(ValueInput, { column: column, condition: row.condition, value: row.value, onChange: (v) => filters.setValue(i, v) })) : null;
|
|
84
|
+
const removeButton = _jsx(RemoveButton, { onClick: () => filters.remove(i) });
|
|
85
|
+
if (renderRow) {
|
|
86
|
+
return (_jsx(Fragment, { children: renderRow({
|
|
87
|
+
index: i,
|
|
88
|
+
fieldSelect,
|
|
89
|
+
conditionSelect,
|
|
90
|
+
valueInput,
|
|
91
|
+
removeButton,
|
|
92
|
+
}) }, row.id));
|
|
93
|
+
}
|
|
94
|
+
return (_jsxs("div", { className: "flex items-center gap-2", children: [fieldSelect, conditionSelect, valueInput, removeButton] }, row.id));
|
|
95
|
+
});
|
|
96
|
+
const addButton = _jsx(AddButton, { onClick: filters.add });
|
|
97
|
+
const presets = filters.presets.length > 0 ? (_jsx("div", { className: "flex flex-wrap gap-1 border-t pt-2", children: filters.presets.map((preset) => (_jsx(Fragment, { children: _jsx(PresetButton, { preset: preset, onClick: () => filters.applyPreset(preset.field, preset.name) }) }, `${preset.field}-${preset.name}`))) })) : null;
|
|
98
|
+
if (renderRoot) {
|
|
99
|
+
return _jsx(_Fragment, { children: renderRoot({ rows: _jsx(_Fragment, { children: rows }), addButton, presets }) });
|
|
100
|
+
}
|
|
101
|
+
return (_jsxs("div", { className: "flex flex-col gap-2", children: [rows, addButton, presets] }));
|
|
102
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { FilterBuilder } from "./filter-builder.js";
|
|
2
|
+
export type { AddButtonSlotProps, ConditionSelectSlotProps, FieldSelectSlotProps, PresetButtonSlotProps, RemoveButtonSlotProps, RootSlotProps, RowSlotProps, ValueInputSlotProps, } from "./filter-builder.js";
|
|
3
|
+
export type { FilterValue } from "../filters/filter-state.js";
|
|
4
|
+
export { useFilterBuilder } from "./use-filter-builder.js";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type FilterRow, type FilterValue } from "../filters/filter-state.js";
|
|
2
|
+
import type { FilterDescriptor } from "../filters/index.js";
|
|
3
|
+
type Preset = {
|
|
4
|
+
field: string;
|
|
5
|
+
name: string;
|
|
6
|
+
};
|
|
7
|
+
type UseFilterBuilderReturn = {
|
|
8
|
+
columns: FilterDescriptor["columns"];
|
|
9
|
+
rows: FilterRow[];
|
|
10
|
+
add: () => void;
|
|
11
|
+
remove: (index: number) => void;
|
|
12
|
+
setField: (index: number, field: string) => void;
|
|
13
|
+
setCondition: (index: number, condition: string) => void;
|
|
14
|
+
setValue: (index: number, value: FilterValue) => void;
|
|
15
|
+
clear: () => void;
|
|
16
|
+
conditionsFor: (field: string) => FilterDescriptor["columns"][number]["conditions"];
|
|
17
|
+
columnFor: (field: string) => FilterDescriptor["columns"][number];
|
|
18
|
+
presets: Preset[];
|
|
19
|
+
applyPreset: (field: string, name: string) => void;
|
|
20
|
+
result: Record<string, unknown>;
|
|
21
|
+
};
|
|
22
|
+
export declare function useFilterBuilder<T>(descriptor: FilterDescriptor<T>): UseFilterBuilderReturn;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useMemo, useState } from "react";
|
|
2
|
+
import { addRow, applyPreset as applyPresetState, buildResult, clearState, columnFor, conditionsFor, createInitialState, removeRow, setRowCondition, setRowField, setRowValue, } from "../filters/filter-state.js";
|
|
3
|
+
export function useFilterBuilder(descriptor) {
|
|
4
|
+
const [state, setState] = useState(createInitialState);
|
|
5
|
+
const presets = useMemo(() => descriptor.columns.flatMap((col) => (col.presets ?? []).map((p) => ({ field: col.key, name: p.name }))), [descriptor]);
|
|
6
|
+
const result = useMemo(() => buildResult(state), [state]);
|
|
7
|
+
return {
|
|
8
|
+
columns: descriptor.columns,
|
|
9
|
+
rows: state.rows,
|
|
10
|
+
add: () => setState(addRow),
|
|
11
|
+
remove: (index) => setState((s) => removeRow(s, index)),
|
|
12
|
+
setField: (index, field) => setState((s) => setRowField(s, index, field)),
|
|
13
|
+
setCondition: (index, condition) => setState((s) => setRowCondition(s, index, condition)),
|
|
14
|
+
setValue: (index, value) => setState((s) => setRowValue(s, index, value)),
|
|
15
|
+
clear: () => setState(clearState),
|
|
16
|
+
conditionsFor: (field) => conditionsFor(descriptor, field),
|
|
17
|
+
columnFor: (field) => columnFor(descriptor, field),
|
|
18
|
+
presets,
|
|
19
|
+
applyPreset: (field, name) => setState((s) => applyPresetState(s, descriptor, field, name)),
|
|
20
|
+
result,
|
|
21
|
+
};
|
|
22
|
+
}
|
package/dist/tsense.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Type } from "arktype";
|
|
2
2
|
import redaxios from "redaxios";
|
|
3
|
-
import type { DeleteResult, FieldSchema, FilterFor, ProjectSearch, SearchListOptions, SearchListResult, SearchOptions, SearchOptionsPlain, SearchResult, SyncOptions, SyncResult, TsenseOptions, UpdateResult, UpsertResult } from "./types.js";
|
|
3
|
+
import type { DeleteResult, FieldSchema, FilterFor, ProjectSearch, ScopedCollection, SearchListOptions, SearchListResult, SearchOptions, SearchOptionsPlain, SearchResult, SyncOptions, SyncResult, TsenseOptions, UpdateResult, UpsertResult } from "./types.js";
|
|
4
4
|
declare const redaxiosInstance: {
|
|
5
5
|
<T>(urlOrConfig: string | redaxios.Options, config?: redaxios.Options | undefined, _method?: any, data?: any, _undefined?: undefined): Promise<redaxios.Response<T>>;
|
|
6
6
|
request: (<T_1 = any>(config?: redaxios.Options | undefined) => Promise<redaxios.Response<T_1>>) | (<T_2 = any>(url: string, config?: redaxios.Options | undefined) => Promise<redaxios.Response<T_2>>);
|
|
@@ -53,19 +53,18 @@ declare const redaxiosInstance: {
|
|
|
53
53
|
spread<Args, R>(fn: (...args: Args[]) => R): (array: Args[]) => R;
|
|
54
54
|
CancelToken: AbortController;
|
|
55
55
|
defaults: redaxios.Options;
|
|
56
|
-
create: any;
|
|
56
|
+
create: /*elided*/ any;
|
|
57
57
|
};
|
|
58
58
|
};
|
|
59
59
|
export type AxiosInstance = ReturnType<typeof redaxiosInstance.create>;
|
|
60
60
|
export declare class TSense<T extends Type> {
|
|
61
61
|
private options;
|
|
62
|
-
fields: FieldSchema[];
|
|
62
|
+
readonly fields: readonly FieldSchema[];
|
|
63
63
|
private axios;
|
|
64
64
|
private synced;
|
|
65
65
|
private fieldTransformers;
|
|
66
66
|
private dataSyncConfig?;
|
|
67
67
|
infer: T["infer"];
|
|
68
|
-
getFields(): FieldSchema[];
|
|
69
68
|
constructor(options: TsenseOptions<T>);
|
|
70
69
|
private getBaseType;
|
|
71
70
|
private inferType;
|
|
@@ -77,6 +76,7 @@ export declare class TSense<T extends Type> {
|
|
|
77
76
|
syncSchema(): Promise<void>;
|
|
78
77
|
private buildObjectFilter;
|
|
79
78
|
private buildFilter;
|
|
79
|
+
private validateFields;
|
|
80
80
|
private buildSort;
|
|
81
81
|
create(): Promise<this>;
|
|
82
82
|
drop(): Promise<void>;
|
|
@@ -92,5 +92,6 @@ export declare class TSense<T extends Type> {
|
|
|
92
92
|
syncData(options?: SyncOptions): Promise<SyncResult>;
|
|
93
93
|
private purgeOrphans;
|
|
94
94
|
exportIds(): Promise<string[]>;
|
|
95
|
+
scoped(baseFilter: FilterFor<T["infer"]>): ScopedCollection<T["infer"]>;
|
|
95
96
|
}
|
|
96
97
|
export {};
|