From 780084a36ca840c35b3dbe2fa0b44ab0fa2cd247 Mon Sep 17 00:00:00 2001 From: Gareth George Date: Wed, 27 Nov 2024 17:53:28 -0800 Subject: [PATCH 1/6] typesystem enforces paths exist in protos --- webui/src/components/form/TypedForm.tsx | 154 ++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 webui/src/components/form/TypedForm.tsx diff --git a/webui/src/components/form/TypedForm.tsx b/webui/src/components/form/TypedForm.tsx new file mode 100644 index 00000000..613aa73c --- /dev/null +++ b/webui/src/components/form/TypedForm.tsx @@ -0,0 +1,154 @@ +import React, { useContext, useEffect, useMemo } from "react"; +import { Form } from "antd"; +import { clone, create, DescMessage, Message, MessageShape } from "@bufbuild/protobuf"; +import { RepoSchema } from "../../../gen/ts/v1/config_pb"; + +type Paths = T extends object + ? { + [K in keyof T]: `${Exclude}${"" | `.${Paths}`}`; + }[keyof T] + : never; + +type DeepIndex = T extends object + ? K extends `${infer F}.${infer R}` + ? DeepIndex, R> + : Idx + : never; + +type Idx = K extends keyof T ? T[K] : never; + +const getValue = >( + obj: T, + path: K +): DeepIndex => { + let curr: any = obj; + const parts = path.split("."); + for (let i = 0; i < parts.length; i++) { + curr = curr[parts[i]]; + } + return curr as DeepIndex; +}; + +const setValue = >( + obj: T, + path: K, + value: DeepIndex +): void => { + let curr: any = obj; + const parts = path.split("."); + for (let i = 0; i < parts.length - 1; i++) { + curr = curr[parts[i]]; + } + curr[parts[parts.length - 1]] = value; +}; + +interface typedFormCtx { + formData: any; + setFormData: (value: any) => any; +} + +const TypedFormCtx = React.createContext({ + formData: {}, + setFormData: () => {}, +}); + +export const TypedForm = ({ + schema, + initialValue, + props, + children, + onChange, +}: { + props: any; + schema: Desc; + initialValue: MessageShape; + children: React.ReactNode; + onChange?: (value: MessageShape) => void; +}) => { + const [form] = Form.useForm(); + const [formData, setFormData] = + React.useState>(initialValue); + + return ( + +
{ + setFormData((prev) => { + const next = clone(schema, prev); + changedFields.forEach((field) => { + setValue(next, field.name, field.value); + }); + onChange?.(next); + return next; + }); + }} + {...props} + > + {children} +
+
+ ); +}; + +export const TypedFormItem = ({ + name, + children, + ...props, +}: { + name: Paths; + children: React.ReactNode; + props: any; +}): React.ReactElement => { + const { formData } = useContext(TypedFormCtx); + + return ( + + {children} + + ); +}; + +export const TypedFormOneof = ({ + name, + children, + ...props, +}: { + name: Paths; + children: React.ReactNode; + props: any; +}): React.ReactElement => { + return <>; +}; + +const const TypedFormOneofCase = ({ + name, + children, + ...props, +}: { + name: Paths; + children: React.ReactNode; + props: any; +}): React.ReactElement => { + const { formData, setFormData } = useContext(TypedFormCtx); + + return <> +} + +const TestElement = () => { + return ( + + + + + + + + + ); +} \ No newline at end of file From 526b26e17ceeb43982e177991622513b97452bfc Mon Sep 17 00:00:00 2001 From: Gareth George Date: Wed, 27 Nov 2024 23:31:49 -0800 Subject: [PATCH 2/6] basically working type constraints --- webui/src/components/form/TypedForm.tsx | 113 ++++++++++++------------ 1 file changed, 58 insertions(+), 55 deletions(-) diff --git a/webui/src/components/form/TypedForm.tsx b/webui/src/components/form/TypedForm.tsx index 613aa73c..9b1899b0 100644 --- a/webui/src/components/form/TypedForm.tsx +++ b/webui/src/components/form/TypedForm.tsx @@ -1,15 +1,28 @@ import React, { useContext, useEffect, useMemo } from "react"; import { Form } from "antd"; -import { clone, create, DescMessage, Message, MessageShape } from "@bufbuild/protobuf"; -import { RepoSchema } from "../../../gen/ts/v1/config_pb"; +import { + clone, + create, + DescMessage, + Message, + MessageShape, +} from "@bufbuild/protobuf"; +import { + Plan, + PlanSchema, + Repo, + RepoSchema, +} from "../../../gen/ts/v1/config_pb"; -type Paths = T extends object +export type Paths = T extends object ? { - [K in keyof T]: `${Exclude}${"" | `.${Paths}`}`; - }[keyof T] + [K in Exclude]: `${K}${Paths extends never + ? "" + : `.${Paths}`}`; + }[Exclude] : never; -type DeepIndex = T extends object +export type DeepIndex = T extends object ? K extends `${infer F}.${infer R}` ? DeepIndex, R> : Idx @@ -17,7 +30,7 @@ type DeepIndex = T extends object type Idx = K extends keyof T ? T[K] : never; -const getValue = >( +const getValue = ( obj: T, path: K ): DeepIndex => { @@ -55,15 +68,15 @@ const TypedFormCtx = React.createContext({ export const TypedForm = ({ schema, initialValue, - props, children, onChange, + ...props }: { - props: any; schema: Desc; initialValue: MessageShape; - children: React.ReactNode; + children?: React.ReactNode; onChange?: (value: MessageShape) => void; + props?: any; }) => { const [form] = Form.useForm(); const [formData, setFormData] = @@ -92,63 +105,53 @@ export const TypedForm = ({ ); }; -export const TypedFormItem = ({ - name, +export const TypedFormItem = ({ + field, children, - ...props, }: { - name: Paths; - children: React.ReactNode; - props: any; + field: Paths; + children?: React.ReactNode; }): React.ReactElement => { const { formData } = useContext(TypedFormCtx); return ( - + {children} ); }; -export const TypedFormOneof = ({ - name, - children, - ...props, +export const TypedFormOneof = >({ + field, + items, }: { - name: Paths; - children: React.ReactNode; - props: any; -}): React.ReactElement => { - return <>; + field: K; + items: { [K2 in DeepIndex]: React.ReactNode }; +}): React.ReactNode => { + const { formData } = useContext(TypedFormCtx); + const c = getValue(formData as T, field); + const val = items[c]; + if (!val) { + return <>; + } + return val; }; -const const TypedFormOneofCase = ({ - name, - children, - ...props, -}: { - name: Paths; - children: React.ReactNode; - props: any; -}): React.ReactElement => { - const { formData, setFormData } = useContext(TypedFormCtx); - - return <> -} +// const TestElement = () => { +// const a = create(PlanSchema, {}); +// const b: Paths = "retention.policy.case"; -const TestElement = () => { - return ( - - - - - - - - - ); -} \ No newline at end of file +// return ( +// +// field={"id"}> +// +// field="retention.policy.case" +// items={{ +// policyKeepAll: <>, +// policyKeepLastN: <>, +// policyTimeBucketed: <>, +// }} +// /> +// +// ); +// }; From f36a7994107c9ca12f458dd4a65aa6a5cf04dc1a Mon Sep 17 00:00:00 2001 From: Gareth George Date: Thu, 28 Nov 2024 00:00:10 -0800 Subject: [PATCH 3/6] start migrating forms --- webui/src/components/form/TypedForm.tsx | 30 +++++++------------------ webui/src/views/AddRepoModal.tsx | 10 ++++----- 2 files changed, 13 insertions(+), 27 deletions(-) diff --git a/webui/src/components/form/TypedForm.tsx b/webui/src/components/form/TypedForm.tsx index 9b1899b0..1c045a5f 100644 --- a/webui/src/components/form/TypedForm.tsx +++ b/webui/src/components/form/TypedForm.tsx @@ -68,15 +68,14 @@ const TypedFormCtx = React.createContext({ export const TypedForm = ({ schema, initialValue, - children, onChange, + children, ...props }: { schema: Desc; initialValue: MessageShape; children?: React.ReactNode; onChange?: (value: MessageShape) => void; - props?: any; }) => { const [form] = Form.useForm(); const [formData, setFormData] = @@ -108,14 +107,20 @@ export const TypedForm = ({ export const TypedFormItem = ({ field, children, + ...props }: { field: Paths; children?: React.ReactNode; + props?: any; }): React.ReactElement => { const { formData } = useContext(TypedFormCtx); return ( - + {children} ); @@ -136,22 +141,3 @@ export const TypedFormOneof = >({ } return val; }; - -// const TestElement = () => { -// const a = create(PlanSchema, {}); -// const b: Paths = "retention.policy.case"; - -// return ( -// -// field={"id"}> -// -// field="retention.policy.case" -// items={{ -// policyKeepAll: <>, -// policyKeepLastN: <>, -// policyTimeBucketed: <>, -// }} -// /> -// -// ); -// }; diff --git a/webui/src/views/AddRepoModal.tsx b/webui/src/views/AddRepoModal.tsx index 1cfb7198..83b1d3af 100644 --- a/webui/src/views/AddRepoModal.tsx +++ b/webui/src/views/AddRepoModal.tsx @@ -37,13 +37,13 @@ import { } from "../components/HooksFormList"; import { ConfirmButton } from "../components/SpinButton"; import { useConfig } from "../components/ConfigProvider"; -import Cron from "react-js-cron"; import { ScheduleDefaultsInfrequent, ScheduleFormItem, } from "../components/ScheduleFormItem"; import { isWindows } from "../state/buildcfg"; import { create, fromJson, toJson } from "@bufbuild/protobuf"; +import { TypedForm, TypedFormItem } from "../components/form/TypedForm"; const repoDefaults = create(RepoSchema, { prunePolicy: { @@ -221,9 +221,10 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => { for more details about repositories.


-
{ "Unique ID that identifies this repo in the backrest UI (e.g. s3-mybucket). This cannot be changed after creation." } > - + hasFeedback - name="id" label="Repo Name" validateTrigger={["onChange", "onBlur"]} rules={[ From 8f7fae85f46cd9f42c79425f4287c3caedabb13c Mon Sep 17 00:00:00 2001 From: Gareth George Date: Fri, 29 Nov 2024 15:54:57 -0800 Subject: [PATCH 4/6] more work on typed forms --- webui/src/components/form/TypedForm.tsx | 86 +++++++++++++++---------- webui/src/views/AddRepoModal.tsx | 1 + 2 files changed, 53 insertions(+), 34 deletions(-) diff --git a/webui/src/components/form/TypedForm.tsx b/webui/src/components/form/TypedForm.tsx index 1c045a5f..46122855 100644 --- a/webui/src/components/form/TypedForm.tsx +++ b/webui/src/components/form/TypedForm.tsx @@ -7,12 +7,7 @@ import { Message, MessageShape, } from "@bufbuild/protobuf"; -import { - Plan, - PlanSchema, - Repo, - RepoSchema, -} from "../../../gen/ts/v1/config_pb"; +import { useWatch } from "antd/es/form/Form"; export type Paths = T extends object ? { @@ -56,14 +51,12 @@ const setValue = >( }; interface typedFormCtx { - formData: any; + formData: Message; + schema: DescMessage; setFormData: (value: any) => any; } -const TypedFormCtx = React.createContext({ - formData: {}, - setFormData: () => {}, -}); +const TypedFormCtx = React.createContext(null); export const TypedForm = ({ schema, @@ -76,26 +69,18 @@ export const TypedForm = ({ initialValue: MessageShape; children?: React.ReactNode; onChange?: (value: MessageShape) => void; +} & { + [key: string]: any; }) => { const [form] = Form.useForm(); const [formData, setFormData] = React.useState>(initialValue); return ( - + { - setFormData((prev) => { - const next = clone(schema, prev); - changedFields.forEach((field) => { - setValue(next, field.name, field.value); - }); - onChange?.(next); - return next; - }); - }} {...props} > {children} @@ -104,16 +89,26 @@ export const TypedForm = ({ ); }; -export const TypedFormItem = ({ +export const TypedFormItem = ({ field, children, ...props }: { field: Paths; children?: React.ReactNode; - props?: any; +} & { + [key: string]: any; }): React.ReactElement => { - const { formData } = useContext(TypedFormCtx); + const { formData, setFormData, schema } = useContext(TypedFormCtx)!; + const value = useWatch({ name: field as string }); + + useEffect(() => { + setFormData((prev: any) => { + const next = clone(schema, prev) as any as T; + setValue(next, field, value); + return next; + }); + }, [value]); return ( ({ ); }; -export const TypedFormOneof = >({ +interface oneofCase> { + case: DeepIndex; + create: () => DeepIndex; + view: React.ReactNode; +} + +export const TypedFormOneof = >({ field, items, }: { - field: K; - items: { [K2 in DeepIndex]: React.ReactNode }; + field: F; + items: oneofCase[]; }): React.ReactNode => { - const { formData } = useContext(TypedFormCtx); - const c = getValue(formData as T, field); - const val = items[c]; - if (!val) { - return <>; + const { formData, setFormData, schema } = useContext(TypedFormCtx)!; + const v = getValue(formData as T, `${field}.value`); + const c = getValue(formData as T, `${field}.case`); + useEffect(() => { + for (const item of items) { + if (item.case === c) { + setFormData((prev) => { + const next = clone(schema, prev) as any as T; + setValue(next, `${field}.value` as Paths, v); + return next; + } + + const nv = item.create(); + setValue(formData, field + ".value", nv); + } + } + }, [c]); + + for (const item of items) { + if (item.case === c) { + return item.view; + } } - return val; + return null; }; diff --git a/webui/src/views/AddRepoModal.tsx b/webui/src/views/AddRepoModal.tsx index 83b1d3af..7afb967b 100644 --- a/webui/src/views/AddRepoModal.tsx +++ b/webui/src/views/AddRepoModal.tsx @@ -237,6 +237,7 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => { > hasFeedback + field={"id"} label="Repo Name" validateTrigger={["onChange", "onBlur"]} rules={[ From 44e2aee49a99fb1b3f1d4aa6e902be076f35d70e Mon Sep 17 00:00:00 2001 From: Gareth George Date: Fri, 29 Nov 2024 16:40:27 -0800 Subject: [PATCH 5/6] more progress --- webui/src/components/form/TypedForm.tsx | 59 ++++++++++++++ webui/src/views/AddRepoModal.tsx | 102 +++++++++++++----------- 2 files changed, 115 insertions(+), 46 deletions(-) diff --git a/webui/src/components/form/TypedForm.tsx b/webui/src/components/form/TypedForm.tsx index 46122855..295f810e 100644 --- a/webui/src/components/form/TypedForm.tsx +++ b/webui/src/components/form/TypedForm.tsx @@ -53,11 +53,16 @@ const setValue = >( interface typedFormCtx { formData: Message; schema: DescMessage; +<<<<<<< Updated upstream setFormData: (value: any) => any; +======= + setFormData: (fn: (prev: any) => any) => void; +>>>>>>> Stashed changes } const TypedFormCtx = React.createContext(null); +<<<<<<< Updated upstream export const TypedForm = ({ schema, initialValue, @@ -83,6 +88,27 @@ export const TypedForm = ({ form={form} {...props} > +======= +export const TypedForm = ({ + schema, + formData, + setFormData, + children, + ...props +}: { + schema: DescMessage; + formData: T; + setFormData: (fn: (prev: T) => T) => void; + children?: React.ReactNode; +} & { + [key: string]: any; +}) => { + const [form] = Form.useForm(); + + return ( + + +>>>>>>> Stashed changes {children} @@ -100,9 +126,16 @@ export const TypedFormItem = ({ [key: string]: any; }): React.ReactElement => { const { formData, setFormData, schema } = useContext(TypedFormCtx)!; +<<<<<<< Updated upstream const value = useWatch({ name: field as string }); useEffect(() => { +======= + const value = useWatch(field as string); + + useEffect(() => { + console.log("set", field, value); +>>>>>>> Stashed changes setFormData((prev: any) => { const next = clone(schema, prev) as any as T; setValue(next, field, value); @@ -127,7 +160,11 @@ interface oneofCase> { view: React.ReactNode; } +<<<<<<< Updated upstream export const TypedFormOneof = >({ +======= +export const TypedFormOneof = >({ +>>>>>>> Stashed changes field, items, }: { @@ -135,6 +172,7 @@ export const TypedFormOneof = >({ items: oneofCase[]; }): React.ReactNode => { const { formData, setFormData, schema } = useContext(TypedFormCtx)!; +<<<<<<< Updated upstream const v = getValue(formData as T, `${field}.value`); const c = getValue(formData as T, `${field}.case`); useEffect(() => { @@ -150,6 +188,23 @@ export const TypedFormOneof = >({ setValue(formData, field + ".value", nv); } } +======= + const c = useWatch(`${field}.case`); + useEffect(() => { + for (const item of items) { + if (item.case === c) { + setFormData((prev: any) => { + const next = clone(schema, prev as T) as T; + setValue(next, field, { + case: c, + value: item.create(), + } as any); + return next; + }); + } + } + throw new Error("case " + c + " not found"); +>>>>>>> Stashed changes }, [c]); for (const item of items) { @@ -157,5 +212,9 @@ export const TypedFormOneof = >({ return item.view; } } +<<<<<<< Updated upstream +======= + +>>>>>>> Stashed changes return null; }; diff --git a/webui/src/views/AddRepoModal.tsx b/webui/src/views/AddRepoModal.tsx index 7afb967b..0b340bb7 100644 --- a/webui/src/views/AddRepoModal.tsx +++ b/webui/src/views/AddRepoModal.tsx @@ -15,6 +15,7 @@ import { Checkbox, Select, Space, + Alert, } from "antd"; import React, { useEffect, useState } from "react"; import { useShowModal } from "../components/ModalManager"; @@ -42,7 +43,11 @@ import { ScheduleFormItem, } from "../components/ScheduleFormItem"; import { isWindows } from "../state/buildcfg"; +<<<<<<< Updated upstream import { create, fromJson, toJson } from "@bufbuild/protobuf"; +======= +import { create, fromJson, toJson, toJsonString } from "@bufbuild/protobuf"; +>>>>>>> Stashed changes import { TypedForm, TypedFormItem } from "../components/form/TypedForm"; const repoDefaults = create(RepoSchema, { @@ -73,15 +78,7 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => { const alertsApi = useAlertApi()!; const [config, setConfig] = useConfig(); const [form] = Form.useForm(); - useEffect(() => { - form.setFieldsValue( - template - ? toJson(RepoSchema, template, { - alwaysEmitImplicit: true, - }) - : toJson(RepoSchema, repoDefaults, { alwaysEmitImplicit: true }) - ); - }, [template]); + const [repo, setRepo] = useState(template || repoDefaults); if (!config) { return null; @@ -206,28 +203,41 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => { ]} maskClosable={false} > -

- See{" "} - - backrest getting started guide - {" "} - for repository configuration instructions or check the{" "} - - restic documentation - {" "} - for more details about repositories. -

+ + See{" "} + + backrest getting started guide + {" "} + for repository configuration instructions or check the{" "} + + restic documentation + {" "} + for more details about repositories. + + } + type="info" + showIcon + />
+<<<<<<< Updated upstream + schema={RepoSchema} +>>>>>>> Stashed changes autoComplete="off" labelCol={{ span: 4 }} wrapperCol={{ span: 18 }} disabled={confirmLoading} + formData={repo} + setFormData={setRepo} > {/* Repo.id */} { message: "Please input repo name", }, { - validator: async (_, value) => { + validator: async (_: any, value: any) => { if (template) return; if (config?.repos?.find((r) => r.id === value)) { throw new Error(); @@ -265,7 +275,7 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => { disabled={!!template} placeholder={"repo" + ((config?.repos?.length || 0) + 1)} /> -
+ {/* Repo.uri */} @@ -292,9 +302,9 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => { } > - + hasFeedback - name="uri" + field="uri" label="Repository URI" validateTrigger={["onChange", "onBlur"]} rules={[ @@ -305,7 +315,7 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => { ]} > -
+ {/* Repo.password */} @@ -334,13 +344,13 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => { - + hasFeedback - name="password" + field="password" validateTrigger={["onChange", "onBlur"]} > - + { {/* Repo.env */} - { )}
- + */} {/* Repo.flags */} - + {/* {(fields, { add, remove }, { errors }) => ( <> @@ -472,10 +482,10 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => { )} - + */} {/* Repo.prunePolicy */} - { name={["prunePolicy", "schedule"]} defaults={ScheduleDefaultsInfrequent} /> - + */} {/* Repo.checkPolicy */} - { name={["checkPolicy", "schedule"]} defaults={ScheduleDefaultsInfrequent} /> - + */} {/* Repo.commandPrefix */} - {!isWindows && ( + {/* {!isWindows && ( { - )} + )} */} - label={ { Auto Unlock } - name="autoUnlock" + field="autoUnlock" valuePropName="checked" > - + Hooks} @@ -688,7 +698,7 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => { children: (
-                          {JSON.stringify(form.getFieldsValue(), undefined, 2)}
+                          {toJsonString(RepoSchema, repo, { prettySpaces: 2 })}
                         
), @@ -697,7 +707,7 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => { /> )}
- + ); From b026aaaeb51997bc7904d3bd86e81ddb4ec88fae Mon Sep 17 00:00:00 2001 From: Gareth George Date: Thu, 5 Dec 2024 23:22:14 -0800 Subject: [PATCH 6/6] stash progress --- webui/src/components/form/TypedForm.tsx | 156 +++++++++++------------- webui/src/views/AddRepoModal.tsx | 25 ++-- 2 files changed, 83 insertions(+), 98 deletions(-) diff --git a/webui/src/components/form/TypedForm.tsx b/webui/src/components/form/TypedForm.tsx index 295f810e..2e505e9c 100644 --- a/webui/src/components/form/TypedForm.tsx +++ b/webui/src/components/form/TypedForm.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useEffect, useMemo } from "react"; +import React, { useContext, useEffect, useMemo, useState } from "react"; import { Form } from "antd"; import { clone, @@ -8,6 +8,7 @@ import { MessageShape, } from "@bufbuild/protobuf"; import { useWatch } from "antd/es/form/Form"; +import useFormInstance from "antd/es/form/hooks/useFormInstance"; export type Paths = T extends object ? { @@ -25,27 +26,28 @@ export type DeepIndex = T extends object type Idx = K extends keyof T ? T[K] : never; -const getValue = ( - obj: T, - path: K -): DeepIndex => { +const getValue = (obj: any, path: string): any => { let curr: any = obj; const parts = path.split("."); for (let i = 0; i < parts.length; i++) { - curr = curr[parts[i]]; + if (Array.isArray(curr)) { + curr = curr[parseInt(parts[i])]; + } else { + curr = curr[parts[i]]; + } } - return curr as DeepIndex; + return curr as any; }; -const setValue = >( - obj: T, - path: K, - value: DeepIndex -): void => { +const setValue = (obj: any, path: string, value: any): void => { let curr: any = obj; const parts = path.split("."); for (let i = 0; i < parts.length - 1; i++) { - curr = curr[parts[i]]; + if (Array.isArray(curr)) { + curr = curr[parseInt(parts[i])]; + } else { + curr = curr[parts[i]]; + } } curr[parts[parts.length - 1]] = value; }; @@ -53,42 +55,12 @@ const setValue = >( interface typedFormCtx { formData: Message; schema: DescMessage; -<<<<<<< Updated upstream - setFormData: (value: any) => any; -======= setFormData: (fn: (prev: any) => any) => void; ->>>>>>> Stashed changes } const TypedFormCtx = React.createContext(null); +const FieldPrefixCtx = React.createContext(""); -<<<<<<< Updated upstream -export const TypedForm = ({ - schema, - initialValue, - onChange, - children, - ...props -}: { - schema: Desc; - initialValue: MessageShape; - children?: React.ReactNode; - onChange?: (value: MessageShape) => void; -} & { - [key: string]: any; -}) => { - const [form] = Form.useForm(); - const [formData, setFormData] = - React.useState>(initialValue); - - return ( - -
-======= export const TypedForm = ({ schema, formData, @@ -108,7 +80,6 @@ export const TypedForm = ({ return ( ->>>>>>> Stashed changes {children} @@ -125,27 +96,32 @@ export const TypedFormItem = ({ } & { [key: string]: any; }): React.ReactElement => { + const prefix = useContext(FieldPrefixCtx); const { formData, setFormData, schema } = useContext(TypedFormCtx)!; -<<<<<<< Updated upstream - const value = useWatch({ name: field as string }); + const form = useFormInstance(); + const resolvedField = prefix + field; - useEffect(() => { -======= - const value = useWatch(field as string); + const [lastSeen, setLastSeen] = useState(null); + const formValue = useWatch(resolvedField); + const formDataStateValue = getValue(formData, resolvedField); useEffect(() => { - console.log("set", field, value); ->>>>>>> Stashed changes - setFormData((prev: any) => { - const next = clone(schema, prev) as any as T; - setValue(next, field, value); - return next; - }); - }, [value]); + if (lastSeen !== formValue) { + setFormData((prev: any) => { + const next = clone(schema, prev) as any as T; + setValue(next, resolvedField, formValue); + return next; + }); + setLastSeen(formValue); + } else if (lastSeen !== formDataStateValue) { + form.setFieldsValue({ [resolvedField]: formDataStateValue } as any); + setLastSeen(formDataStateValue); + } + }, [lastSeen, formValue, formDataStateValue]); return ( @@ -154,17 +130,47 @@ export const TypedFormItem = ({ ); }; +// This is a helper component that sets a prefix for all of its children. +const WithFieldPrefix = ({ + prefix, + children, +}: { + prefix: string; + children?: React.ReactNode; +}): React.ReactNode => { + const prev = useContext(FieldPrefixCtx); + return ( + + {children} + + ); +}; + +export const TypedFormList = ({ + field, + children, + ...props +}: { + field: Paths; + children?: React.ReactNode; +} & { + [key: string]: any; +}): React.ReactElement => { + return ( + + + + ); +}; + interface oneofCase> { case: DeepIndex; create: () => DeepIndex; view: React.ReactNode; } -<<<<<<< Updated upstream -export const TypedFormOneof = >({ -======= +/* export const TypedFormOneof = >({ ->>>>>>> Stashed changes field, items, }: { @@ -172,23 +178,6 @@ export const TypedFormOneof = >({ items: oneofCase[]; }): React.ReactNode => { const { formData, setFormData, schema } = useContext(TypedFormCtx)!; -<<<<<<< Updated upstream - const v = getValue(formData as T, `${field}.value`); - const c = getValue(formData as T, `${field}.case`); - useEffect(() => { - for (const item of items) { - if (item.case === c) { - setFormData((prev) => { - const next = clone(schema, prev) as any as T; - setValue(next, `${field}.value` as Paths, v); - return next; - } - - const nv = item.create(); - setValue(formData, field + ".value", nv); - } - } -======= const c = useWatch(`${field}.case`); useEffect(() => { for (const item of items) { @@ -204,7 +193,6 @@ export const TypedFormOneof = >({ } } throw new Error("case " + c + " not found"); ->>>>>>> Stashed changes }, [c]); for (const item of items) { @@ -212,9 +200,7 @@ export const TypedFormOneof = >({ return item.view; } } -<<<<<<< Updated upstream -======= - ->>>>>>> Stashed changes return null; }; + +*/ diff --git a/webui/src/views/AddRepoModal.tsx b/webui/src/views/AddRepoModal.tsx index 0b340bb7..c1be4b7e 100644 --- a/webui/src/views/AddRepoModal.tsx +++ b/webui/src/views/AddRepoModal.tsx @@ -43,11 +43,13 @@ import { ScheduleFormItem, } from "../components/ScheduleFormItem"; import { isWindows } from "../state/buildcfg"; -<<<<<<< Updated upstream -import { create, fromJson, toJson } from "@bufbuild/protobuf"; -======= -import { create, fromJson, toJson, toJsonString } from "@bufbuild/protobuf"; ->>>>>>> Stashed changes +import { + clone, + create, + fromJson, + toJson, + toJsonString, +} from "@bufbuild/protobuf"; import { TypedForm, TypedFormItem } from "../components/form/TypedForm"; const repoDefaults = create(RepoSchema, { @@ -224,14 +226,8 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => { showIcon />
-<<<<<<< Updated upstream - schema={RepoSchema} ->>>>>>> Stashed changes autoComplete="off" labelCol={{ span: 4 }} wrapperCol={{ span: 18 }} @@ -361,8 +357,11 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => { type="text" onClick={() => { if (template) return; - form.setFieldsValue({ - password: cryptoRandomPassword(), + + setRepo((prev) => { + const copy = clone(RepoSchema, prev); + copy.password = cryptoRandomPassword(); + return copy; }); }} >