Skip to content

Commit 10adab6

Browse files
drew-harriscdxker
authored andcommitted
feat: input validation
1 parent b4d5d5e commit 10adab6

3 files changed

Lines changed: 150 additions & 7 deletions

File tree

frontends/dashboard/src/pages/dataset/CrawlingSettings.tsx

Lines changed: 78 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { DatasetContext } from "../../contexts/DatasetContext";
44
import { useTrieve } from "../../hooks/useTrieve";
55
import { CrawlInterval, CrawlOptions } from "trieve-ts-sdk";
66
import { createStore } from "solid-js/store";
7-
import { Select } from "shared/ui";
7+
import { MultiStringInput, Select } from "shared/ui";
88
import { toTitleCase } from "../../analytics/utils/titleCase";
99

1010
const defaultCrawlOptions: CrawlOptions = {
@@ -52,11 +52,56 @@ interface RealCrawlingSettingsProps {
5252
mode: "edit" | "create";
5353
}
5454

55+
const Error = (props: { error: string | null | undefined }) => {
56+
return (
57+
<Show when={props.error}>
58+
<div class="text-sm text-red-500">{props.error}</div>
59+
</Show>
60+
);
61+
};
62+
63+
type ValidateFn<T extends Record<string, any>> = (value: T) => {
64+
errors: {
65+
[key in keyof T]: string | undefined;
66+
};
67+
valid: boolean;
68+
};
69+
5570
const RealCrawlingSettings = (props: RealCrawlingSettingsProps) => {
5671
const [options, setOptions] = createStore(props.initialCrawlingSettings);
72+
const [errors, setErrors] = createStore<
73+
ReturnType<ValidateFn<CrawlOptions>>["errors"]
74+
>({});
75+
76+
const validate: ValidateFn<CrawlOptions> = (value) => {
77+
const errors: Record<string, string> = {};
78+
if (!value.site_url) {
79+
errors.site_url = "Site URL is required";
80+
}
81+
82+
return {
83+
errors,
84+
valid: Object.values(errors).filter((v) => !!v).length === 0,
85+
};
86+
};
87+
88+
const submit = () => {
89+
const validateResult = validate(options);
90+
if (validateResult.valid) {
91+
console.log("submit");
92+
} else {
93+
setErrors(validateResult.errors);
94+
}
95+
};
5796

5897
return (
59-
<div class="rounded border border-neutral-300 bg-white p-4 shadow">
98+
<form
99+
onSubmit={(e) => {
100+
e.preventDefault();
101+
submit();
102+
}}
103+
class="rounded border border-neutral-300 bg-white p-4 shadow"
104+
>
60105
<div class="text-lg">Crawl Options</div>
61106

62107
<div class="flex w-full items-stretch justify-between gap-4 pt-2">
@@ -70,8 +115,9 @@ const RealCrawlingSettings = (props: RealCrawlingSettingsProps) => {
70115
onInput={(e) => {
71116
setOptions("site_url", e.currentTarget.value);
72117
}}
73-
class="block w-full rounded-md border border-neutral-300 px-3 py-1.5 shadow-sm placeholder:text-neutral-400 focus:outline-magenta-500 sm:text-sm sm:leading-6"
118+
class="block w-full rounded border border-neutral-300 px-3 py-1.5 shadow-sm placeholder:text-neutral-400 focus:outline-magenta-500 sm:text-sm sm:leading-6"
74119
/>
120+
<Error error={errors.site_url} />
75121
</div>
76122
<div>
77123
<Select
@@ -90,7 +136,7 @@ const RealCrawlingSettings = (props: RealCrawlingSettingsProps) => {
90136
<div class="flex items-center gap-2 pt-2">
91137
<label class="block">Boost Titles</label>
92138
<input
93-
class="h-4 w-4 rounded-md border border-neutral-300 bg-neutral-100 p-1 accent-magenta-400 dark:border-neutral-900 dark:bg-neutral-800"
139+
class="h-4 w-4 rounded border border-neutral-300 bg-neutral-100 p-1 accent-magenta-400 dark:border-neutral-900 dark:bg-neutral-800"
94140
type="checkbox"
95141
/>
96142
</div>
@@ -105,7 +151,7 @@ const RealCrawlingSettings = (props: RealCrawlingSettingsProps) => {
105151
onInput={(e) => {
106152
setOptions("limit", parseInt(e.currentTarget.value));
107153
}}
108-
class="block rounded-md border border-neutral-300 px-3 py-1.5 shadow-sm placeholder:text-neutral-400 focus:outline-magenta-500 sm:text-sm sm:leading-6"
154+
class="block rounded border border-neutral-300 px-3 py-1.5 shadow-sm placeholder:text-neutral-400 focus:outline-magenta-500 sm:text-sm sm:leading-6"
109155
type="number"
110156
/>
111157
</div>
@@ -118,11 +164,36 @@ const RealCrawlingSettings = (props: RealCrawlingSettingsProps) => {
118164
onInput={(e) => {
119165
setOptions("max_depth", parseInt(e.currentTarget.value));
120166
}}
121-
class="block rounded-md border border-neutral-300 px-3 py-1.5 shadow-sm placeholder:text-neutral-400 focus:outline-magenta-500 sm:text-sm sm:leading-6"
167+
class="block rounded border border-neutral-300 px-3 py-1.5 shadow-sm placeholder:text-neutral-400 focus:outline-magenta-500 sm:text-sm sm:leading-6"
122168
type="number"
123169
/>
124170
</div>
125171
</div>
126-
</div>
172+
<div class="flex gap-2">
173+
<div class="pt-4">
174+
<div>Include Paths</div>
175+
<MultiStringInput
176+
addClass="bg-magenta-100/40 px-2 rounded border border-magenta-300/40"
177+
addLabel="Add Path"
178+
onChange={(value) => {
179+
setOptions("include_paths", value);
180+
}}
181+
value={options.include_paths || []}
182+
/>
183+
</div>
184+
<div class="pt-4">
185+
<div>Include Tags</div>
186+
<MultiStringInput
187+
addClass="bg-magenta-100/40 px-2 rounded border border-magenta-300/40"
188+
addLabel="Add Tag"
189+
onChange={(value) => {
190+
setOptions("include_tags", value);
191+
}}
192+
value={options.include_tags || []}
193+
/>
194+
</div>
195+
</div>
196+
<button>Submit</button>
197+
</form>
127198
);
128199
};
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { createEffect, For, Show } from "solid-js";
2+
import { createStore } from "solid-js/store";
3+
import { cn } from "../utils";
4+
import { FiPlus, FiX } from "solid-icons/fi";
5+
6+
interface MultiStringInputProps {
7+
value: string[];
8+
onChange: (value: string[]) => void;
9+
inputClass?: string;
10+
addClass?: string;
11+
addLabel?: string;
12+
}
13+
14+
export const MultiStringInput = (props: MultiStringInputProps) => {
15+
const [proxyStore, setProxyStore] = createStore(
16+
// eslint-disable-next-line solid/reactivity
17+
props.value.map((value) => ({
18+
value,
19+
id: Math.random().toString(36).slice(2),
20+
})),
21+
);
22+
23+
createEffect(() => {
24+
props.onChange(proxyStore.map((item) => item.value));
25+
});
26+
27+
const updateValue = (id: string, value: string) => {
28+
setProxyStore((v) => v.id == id, "value", value);
29+
};
30+
31+
return (
32+
<div class="flex min-w-[206px] flex-col gap-2 items-start">
33+
<For each={proxyStore}>
34+
{(entry) => (
35+
<div class="flex items-center gap-2">
36+
<input
37+
value={entry.value}
38+
onInput={(e) => {
39+
updateValue(entry.id, e.currentTarget.value);
40+
}}
41+
class={cn(
42+
"block rounded-md border border-neutral-300 px-3 py-1.5 shadow-sm placeholder:text-neutral-400 focus:outline-magenta-500 sm:text-sm sm:leading-6",
43+
props.inputClass,
44+
)}
45+
/>
46+
<button
47+
class="text-neutral-400 hover:text-neutral-500 dark:text-neutral-300 dark:hover:text-neutral-400"
48+
onClick={() => {
49+
setProxyStore((v) => v.filter((item) => item.id != entry.id));
50+
}}
51+
>
52+
<FiX />
53+
</button>
54+
</div>
55+
)}
56+
</For>
57+
<button
58+
class={cn("flex gap-2 items-center justify-center", props.addClass)}
59+
onClick={() => {
60+
setProxyStore((v) => [
61+
...v,
62+
{ value: "", id: Math.random().toString(36).slice(2) },
63+
]);
64+
}}
65+
>
66+
<Show when={props.addLabel}>{props.addLabel}</Show>
67+
<FiPlus />
68+
</button>
69+
</div>
70+
);
71+
};

frontends/shared/ui/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ export * from "./JsonInput";
77
export * from "./JSONMetadata";
88
export * from "./Pagination";
99
export * from "./TanStackTable";
10+
export * from "./MultiStringInput";

0 commit comments

Comments
 (0)