From 86e4f6ef3e9d8ffae9ecac46b33a4dd234abd067 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Sat, 21 Dec 2024 12:48:01 -0300 Subject: [PATCH 1/7] refactor SafeActionFormModal --- .../[slug]/members/AddUserToGroupAction.tsx | 61 ++++++----- .../[owner]/[slug]/ModelSettingsButton.tsx | 4 +- .../models/[owner]/[slug]/MoveModelAction.tsx | 71 ++++++------ .../[owner]/[slug]/UpdateModelSlugAction.tsx | 66 ++++++------ .../src/components/ui/SafeActionFormModal.tsx | 62 +++++++++++ .../components/ui/SafeActionModalAction.tsx | 102 ------------------ .../Dropdown/DropdownMenuModalActionItem.tsx | 8 +- 7 files changed, 174 insertions(+), 200 deletions(-) create mode 100644 apps/hub/src/components/ui/SafeActionFormModal.tsx delete mode 100644 apps/hub/src/components/ui/SafeActionModalAction.tsx diff --git a/apps/hub/src/app/groups/[slug]/members/AddUserToGroupAction.tsx b/apps/hub/src/app/groups/[slug]/members/AddUserToGroupAction.tsx index c218c1842b..1497b39e82 100644 --- a/apps/hub/src/app/groups/[slug]/members/AddUserToGroupAction.tsx +++ b/apps/hub/src/app/groups/[slug]/members/AddUserToGroupAction.tsx @@ -1,10 +1,14 @@ import { MembershipRole } from "@prisma/client"; import { FC } from "react"; -import { PlusIcon, SelectStringFormField } from "@quri/ui"; +import { + DropdownMenuModalActionItem, + PlusIcon, + SelectStringFormField, +} from "@quri/ui"; import { SelectUser, SelectUserOption } from "@/components/SelectUser"; -import { SafeActionModalAction } from "@/components/ui/SafeActionModalAction"; +import { SafeActionFormModal } from "@/components/ui/SafeActionFormModal"; import { addUserToGroupAction } from "@/groups/actions/addUserToGroupAction"; import { GroupMemberDTO } from "@/groups/data/members"; @@ -17,33 +21,36 @@ type FormShape = { user: SelectUserOption; role: MembershipRole }; export const AddUserToGroupAction: FC = ({ groupSlug, append }) => { return ( - + { - append(membership); - }} - defaultValues={{ role: "Member" }} - formDataToInput={(data) => ({ - group: groupSlug, - username: data.user.slug, - role: data.role, - })} - submitText="Add" - modalTitle={`Add to group ${groupSlug}`} - > - {() => ( -
- label="User" name="user" /> - - name="role" - label="Role" - options={["Member", "Admin"]} - required - /> -
+ render={({ close }) => ( + + close={close} + action={addUserToGroupAction} + onSuccess={(membership) => { + append(membership); + }} + defaultValues={{ role: "Member" }} + formDataToInput={(data) => ({ + group: groupSlug, + username: data.user.slug, + role: data.role, + })} + submitText="Add" + title={`Add to group ${groupSlug}`} + > +
+ label="User" name="user" /> + + name="role" + label="Role" + options={["Member", "Admin"]} + required + /> +
+ )} - + /> ); }; diff --git a/apps/hub/src/app/models/[owner]/[slug]/ModelSettingsButton.tsx b/apps/hub/src/app/models/[owner]/[slug]/ModelSettingsButton.tsx index f92f25e845..7102c6f0bc 100644 --- a/apps/hub/src/app/models/[owner]/[slug]/ModelSettingsButton.tsx +++ b/apps/hub/src/app/models/[owner]/[slug]/ModelSettingsButton.tsx @@ -15,9 +15,9 @@ export const ModelSettingsButton: FC<{ }> = ({ model }) => { return ( ( + render={() => ( - + diff --git a/apps/hub/src/app/models/[owner]/[slug]/MoveModelAction.tsx b/apps/hub/src/app/models/[owner]/[slug]/MoveModelAction.tsx index 25470b4401..245b3041eb 100644 --- a/apps/hub/src/app/models/[owner]/[slug]/MoveModelAction.tsx +++ b/apps/hub/src/app/models/[owner]/[slug]/MoveModelAction.tsx @@ -1,10 +1,10 @@ import { useRouter } from "next/navigation"; import { FC } from "react"; -import { RightArrowIcon } from "@quri/ui"; +import { DropdownMenuModalActionItem, RightArrowIcon } from "@quri/ui"; import { SelectOwner, SelectOwnerOption } from "@/components/SelectOwner"; -import { SafeActionModalAction } from "@/components/ui/SafeActionModalAction"; +import { SafeActionFormModal } from "@/components/ui/SafeActionFormModal"; import { modelRoute } from "@/lib/routes"; import { moveModelAction } from "@/models/actions/moveModelAction"; import { ModelCardDTO } from "@/models/data/cards"; @@ -21,42 +21,45 @@ export const MoveModelAction: FC = ({ model }) => { const router = useRouter(); return ( - + ({ - oldOwner: model.owner.slug, - owner: { slug: data.owner.slug }, - slug: model.slug, - })} - onSuccess={({ model: newModel }) => { - draftUtils.rename( - modelToDraftLocator(model), - modelToDraftLocator(newModel) - ); - router.push( - modelRoute({ owner: newModel.owner.slug, slug: newModel.slug }) - ); - }} icon={RightArrowIcon} - initialFocus="owner" - blockOnSuccess - > - {() => ( -
+ render={({ close }) => ( + + close={close} + title={`Change owner for ${model.owner.slug}/${model.slug}`} + submitText="Save" + defaultValues={{ + // __typename from fragment is string, while SelectOwner requires 'User' | 'Group' union, + // so we have to explicitly recast + owner: model.owner as SelectOwnerOption, + }} + action={moveModelAction} + formDataToInput={(data) => ({ + oldOwner: model.owner.slug, + owner: { slug: data.owner.slug }, + slug: model.slug, + })} + onSuccess={({ model: newModel }) => { + draftUtils.rename( + modelToDraftLocator(model), + modelToDraftLocator(newModel) + ); + router.push( + modelRoute({ owner: newModel.owner.slug, slug: newModel.slug }) + ); + }} + initialFocus="owner" + blockOnSuccess + >
- Are you sure? All existing links to the model will break. +
+ Are you sure? All existing links to the model will break. +
+ name="owner" label="New owner" myOnly />
- name="owner" label="New owner" myOnly /> -
+ )} - + /> ); }; diff --git a/apps/hub/src/app/models/[owner]/[slug]/UpdateModelSlugAction.tsx b/apps/hub/src/app/models/[owner]/[slug]/UpdateModelSlugAction.tsx index 5cd77d72b1..9b6456124a 100644 --- a/apps/hub/src/app/models/[owner]/[slug]/UpdateModelSlugAction.tsx +++ b/apps/hub/src/app/models/[owner]/[slug]/UpdateModelSlugAction.tsx @@ -1,9 +1,9 @@ import { useRouter } from "next/navigation"; import { FC } from "react"; -import { EditIcon } from "@quri/ui"; +import { DropdownMenuModalActionItem, EditIcon } from "@quri/ui"; -import { SafeActionModalAction } from "@/components/ui/SafeActionModalAction"; +import { SafeActionFormModal } from "@/components/ui/SafeActionFormModal"; import { SlugFormField } from "@/components/ui/SlugFormField"; import { modelRoute } from "@/lib/routes"; import { updateModelSlugAction } from "@/models/actions/updateModelSlugAction"; @@ -13,46 +13,48 @@ import { draftUtils, modelToDraftLocator } from "./SquiggleSnippetDraftDialog"; type Props = { model: ModelCardDTO; - close(): void; }; type FormShape = { slug: string }; -export const UpdateModelSlugAction: FC = ({ model, close }) => { +export const UpdateModelSlugAction: FC = ({ model }) => { const router = useRouter(); return ( - + ({ - owner: model.owner.slug, - oldSlug: model.slug, - slug: data.slug, - })} - onSuccess={({ model: newModel }) => { - draftUtils.rename( - modelToDraftLocator(model), - modelToDraftLocator(newModel) - ); - router.push( - modelRoute({ owner: newModel.owner.slug, slug: newModel.slug }) - ); - }} - submitText="Save" - modalTitle={`Rename ${model.owner.slug}/${model.slug}`} - initialFocus="slug" - > - {() => ( -
-
- Are you sure? All existing links to the model will break. + render={({ close }) => ( + + close={close} + action={updateModelSlugAction} + defaultValues={{ slug: model.slug }} + formDataToInput={(data) => ({ + owner: model.owner.slug, + oldSlug: model.slug, + slug: data.slug, + })} + onSuccess={({ model: newModel }) => { + draftUtils.rename( + modelToDraftLocator(model), + modelToDraftLocator(newModel) + ); + router.push( + modelRoute({ owner: newModel.owner.slug, slug: newModel.slug }) + ); + }} + submitText="Save" + title={`Rename ${model.owner.slug}/${model.slug}`} + initialFocus="slug" + > +
+
+ Are you sure? All existing links to the model will break. +
+ name="slug" label="New slug" />
- name="slug" label="New slug" /> -
+ )} - + /> ); }; diff --git a/apps/hub/src/components/ui/SafeActionFormModal.tsx b/apps/hub/src/components/ui/SafeActionFormModal.tsx new file mode 100644 index 0000000000..fa7082fceb --- /dev/null +++ b/apps/hub/src/components/ui/SafeActionFormModal.tsx @@ -0,0 +1,62 @@ +import { HookSafeActionFn } from "next-safe-action/hooks"; +import { PropsWithChildren, ReactNode } from "react"; +import { FieldPath, FieldValues } from "react-hook-form"; + +import { FormModal } from "@/components/ui/FormModal"; +import { useSafeActionForm } from "@/lib/hooks/useSafeActionForm"; + +export function SafeActionFormModal< + TFormShape extends FieldValues, + const Action extends HookSafeActionFn, +>({ + // action hook props + formDataToInput, + defaultValues, + action, + onSuccess, + // modal props + title, + initialFocus, + submitText, + children, +}: Pick< + Parameters>[0], + | "formDataToInput" + | "defaultValues" + | "action" + | "onSuccess" + | "blockOnSuccess" +> & + PropsWithChildren<{ + title: string; + initialFocus?: FieldPath; + submitText: string; + // Intentionally explicit. In case of forms activated by dropdowns we could obtain this with `useCloseDropdown`, but it's not always the case. + // The common pattern is to use this component in `DropdownMenuModalActionItem` and pass the close function from `render({ close })`. + close: () => void; + }>): ReactNode { + const { form, onSubmit, inFlight } = useSafeActionForm({ + mode: "onChange", + defaultValues, + action, + formDataToInput, + async onSuccess(data) { + await onSuccess?.(data); + close(); + }, + }); + + return ( + + {children} + + ); +} diff --git a/apps/hub/src/components/ui/SafeActionModalAction.tsx b/apps/hub/src/components/ui/SafeActionModalAction.tsx deleted file mode 100644 index e4fbc726f9..0000000000 --- a/apps/hub/src/components/ui/SafeActionModalAction.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { HookSafeActionFn } from "next-safe-action/hooks"; -import { FC, PropsWithChildren, ReactNode } from "react"; -import { FieldPath, FieldValues } from "react-hook-form"; - -import { - DropdownMenuModalActionItem, - IconProps, - useCloseDropdown, -} from "@quri/ui"; - -import { FormModal } from "@/components/ui/FormModal"; -import { useSafeActionForm } from "@/lib/hooks/useSafeActionForm"; - -type CommonProps< - TFormShape extends FieldValues, - Action extends HookSafeActionFn, -> = Pick< - Parameters>[0], - | "formDataToInput" - | "defaultValues" - | "action" - | "onSuccess" - | "blockOnSuccess" -> & { - initialFocus?: FieldPath; - submitText: string; -}; - -function SafeActionFormModal< - TFormShape extends FieldValues, - const Action extends HookSafeActionFn, ->({ - formDataToInput, - initialFocus, - defaultValues, - submitText, - action, - onSuccess, - title, - children, -}: PropsWithChildren> & { - title: string; -}): ReactNode { - // Note that we use the same `close` that's responsible for closing the dropdown. - const closeDropdown = useCloseDropdown(); - - const { form, onSubmit, inFlight } = useSafeActionForm({ - mode: "onChange", - defaultValues, - action, - formDataToInput, - async onSuccess(data) { - onSuccess?.(data); - closeDropdown(); - }, - }); - - return ( - - {children} - - ); -} - -export function SafeActionModalAction< - TFormShape extends FieldValues, - const Action extends HookSafeActionFn, ->({ - modalTitle, - title, - icon, - children, - ...modalProps -}: CommonProps & { - modalTitle: string; - title: string; - icon?: FC; - children: () => ReactNode; -}): ReactNode { - return ( - ( - - {...modalProps} - title={modalTitle} - > - {children()} - - )} - /> - ); -} diff --git a/packages/ui/src/components/Dropdown/DropdownMenuModalActionItem.tsx b/packages/ui/src/components/Dropdown/DropdownMenuModalActionItem.tsx index 1af6cccef7..ec7c1fcd7b 100644 --- a/packages/ui/src/components/Dropdown/DropdownMenuModalActionItem.tsx +++ b/packages/ui/src/components/Dropdown/DropdownMenuModalActionItem.tsx @@ -1,16 +1,17 @@ "use client"; import { FC, ReactNode, useState } from "react"; +import { useCloseDropdown } from "./DropdownContext.js"; import { DropdownMenuActionItem } from "./DropdownMenuActionItem.js"; import { ItemLayoutProps } from "./DropdownMenuItemLayout.js"; type Props = ItemLayoutProps & { - render(): ReactNode; + render: ({ close }: { close: () => void }) => ReactNode; }; /* * This component doesn't close the dropdown when modal is displayed. - * Instead, you should close the dropdown manually by passing `useCloseDropdown()` result to Modal's `close` prop. + * Instead, you should close the dropdown manually by passing `useCloseDropdown()` result to Modal's `close` prop, or by using `close` prop from `render` function. */ export const DropdownMenuModalActionItem: FC = ({ title, @@ -18,6 +19,7 @@ export const DropdownMenuModalActionItem: FC = ({ render, }) => { const [isOpen, setIsOpen] = useState(false); + const closeDropdown = useCloseDropdown(); return ( <> @@ -26,7 +28,7 @@ export const DropdownMenuModalActionItem: FC = ({ onClick={() => setIsOpen(true)} icon={icon} /> - {isOpen && render()} + {isOpen && render({ close: closeDropdown })} ); }; From 5ed7cc6b1fb53220d3b4d6fb5efe5d5a29353724 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Sat, 21 Dec 2024 13:38:29 -0300 Subject: [PATCH 2/7] various form cleanups --- apps/hub/src/app/new/group/NewGroup.tsx | 4 ++-- apps/hub/src/app/new/model/NewModel.tsx | 4 ++-- .../choose-username/ChooseUsername.tsx | 2 +- apps/hub/src/components/ui/FormModal.tsx | 2 +- apps/hub/src/lib/hooks/useSafeActionForm.ts | 19 +++++++++---------- .../RelativeValuesDefinitionForm/index.tsx | 4 ++-- 6 files changed, 17 insertions(+), 18 deletions(-) diff --git a/apps/hub/src/app/new/group/NewGroup.tsx b/apps/hub/src/app/new/group/NewGroup.tsx index 592d5b3bbe..dc0f52e192 100644 --- a/apps/hub/src/app/new/group/NewGroup.tsx +++ b/apps/hub/src/app/new/group/NewGroup.tsx @@ -47,9 +47,9 @@ export const NewGroup: FC = () => { />
diff --git a/apps/hub/src/app/new/model/NewModel.tsx b/apps/hub/src/app/new/model/NewModel.tsx index f181e9f9ae..85635c12d2 100644 --- a/apps/hub/src/app/new/model/NewModel.tsx +++ b/apps/hub/src/app/new/model/NewModel.tsx @@ -88,9 +88,9 @@ export const NewModel: FC<{ initialGroup: SelectGroupOption | null }> = ({ label="Private" name="isPrivate" /> diff --git a/apps/hub/src/app/settings/choose-username/ChooseUsername.tsx b/apps/hub/src/app/settings/choose-username/ChooseUsername.tsx index fa6b349293..4aec7172ba 100644 --- a/apps/hub/src/app/settings/choose-username/ChooseUsername.tsx +++ b/apps/hub/src/app/settings/choose-username/ChooseUsername.tsx @@ -41,7 +41,7 @@ export const ChooseUsername: FC = () => { label="Pick a username" size="small" /> - diff --git a/apps/hub/src/components/ui/FormModal.tsx b/apps/hub/src/components/ui/FormModal.tsx index 7d84eba4eb..b880d2121b 100644 --- a/apps/hub/src/components/ui/FormModal.tsx +++ b/apps/hub/src/components/ui/FormModal.tsx @@ -43,7 +43,7 @@ export function FormModal({ {children} From b784dafc50824a6438d0143badff807f04d0b35c Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Sat, 21 Dec 2024 13:43:58 -0300 Subject: [PATCH 3/7] NewModel form uses useSafeActionForm --- apps/hub/src/app/new/model/NewModel.tsx | 55 ++++++++----------------- 1 file changed, 18 insertions(+), 37 deletions(-) diff --git a/apps/hub/src/app/new/model/NewModel.tsx b/apps/hub/src/app/new/model/NewModel.tsx index 85635c12d2..86ffbf9b5f 100644 --- a/apps/hub/src/app/new/model/NewModel.tsx +++ b/apps/hub/src/app/new/model/NewModel.tsx @@ -1,14 +1,14 @@ "use client"; -import { useAction } from "next-safe-action/hooks"; import { useRouter } from "next/navigation"; import { FC, useState } from "react"; -import { FormProvider, useForm } from "react-hook-form"; +import { FormProvider } from "react-hook-form"; -import { Button, CheckboxFormField, useToast } from "@quri/ui"; +import { Button, CheckboxFormField } from "@quri/ui"; import { SelectGroup, SelectGroupOption } from "@/components/SelectGroup"; import { H1 } from "@/components/ui/Headers"; import { SlugFormField } from "@/components/ui/SlugFormField"; +import { useSafeActionForm } from "@/lib/hooks/useSafeActionForm"; import { createModelAction } from "@/models/actions/createModelAction"; type FormShape = { @@ -22,49 +22,30 @@ export const NewModel: FC<{ initialGroup: SelectGroupOption | null }> = ({ }) => { const [group] = useState(initialGroup); - const toast = useToast(); const router = useRouter(); - const { executeAsync, isPending } = useAction(createModelAction, { - onSuccess: ({ data }) => { - if (data) { - // redirect in action is incompatible with https://github.com/TheEdoRan/next-safe-action/issues/303 - // (and might a bad idea anyway, returning an url is more verbose but more flexible for reuse) - router.push(data.url); - } - }, - onError: ({ error }) => { - if (error.serverError) { - toast(error.serverError, "error"); - return; - } - - const slugError = error.validationErrors?.slug?._errors?.[0]; - if (slugError) { - form.setError("slug", { - message: slugError, - }); - } else { - toast("Internal error", "error"); - } - }, - }); - - const form = useForm({ + const { form, onSubmit, inFlight } = useSafeActionForm< + FormShape, + typeof createModelAction + >({ mode: "onChange", defaultValues: { // don't pass `slug: ""` here, it will lead to form reset if a user started to type in a value before JS finished loading group, isPrivate: false, }, - }); - - const onSubmit = form.handleSubmit(async (data) => { - await executeAsync({ + formDataToInput: (data) => ({ slug: data.slug ?? "", // shouldn't happen but satisfies Typescript groupSlug: data.group?.slug, isPrivate: data.isPrivate, - }); + }), + action: createModelAction, + blockOnSuccess: true, + onSuccess: (data) => { + // Note: redirect in server action would be incompatible with https://github.com/TheEdoRan/next-safe-action/issues/303 + // (and might a bad idea anyway, returning a url is more verbose but more flexible for reuse) + router.push(data.url); + }, }); return ( @@ -83,14 +64,14 @@ export const NewModel: FC<{ initialGroup: SelectGroupOption | null }> = ({ description="Optional. Models owned by a group are editable by all members of the group." name="group" required={false} - myOnly={true} + myOnly /> label="Private" name="isPrivate" /> From 83c8ff1823954be4b36f556fb3d4ac8fa8f4c7ae Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Sat, 21 Dec 2024 13:59:00 -0300 Subject: [PATCH 4/7] publish workflow button --- .../WorkflowViewer/PublishWorkflowButton.tsx | 65 +++++++++++++++++++ apps/hub/src/app/ai/WorkflowViewer/index.tsx | 4 +- apps/hub/src/app/new/model/NewModel.tsx | 45 +++++++------ .../src/components/ui/SafeActionFormModal.tsx | 1 + .../src/models/actions/createModelAction.ts | 3 +- 5 files changed, 97 insertions(+), 21 deletions(-) create mode 100644 apps/hub/src/app/ai/WorkflowViewer/PublishWorkflowButton.tsx diff --git a/apps/hub/src/app/ai/WorkflowViewer/PublishWorkflowButton.tsx b/apps/hub/src/app/ai/WorkflowViewer/PublishWorkflowButton.tsx new file mode 100644 index 0000000000..87352beb28 --- /dev/null +++ b/apps/hub/src/app/ai/WorkflowViewer/PublishWorkflowButton.tsx @@ -0,0 +1,65 @@ +import { useRouter } from "next/navigation"; +import { FC, useState } from "react"; + +import { ClientWorkflow } from "@quri/squiggle-ai"; +import { Button } from "@quri/ui"; + +import { NewModelFormBody, NewModelFormShape } from "@/app/new/model/NewModel"; +import { SafeActionFormModal } from "@/components/ui/SafeActionFormModal"; +import { createModelAction } from "@/models/actions/createModelAction"; + +type Props = { + workflow: Extract; +}; + +const PublishWorkflowModal: FC void }> = ({ + workflow, + close, +}) => { + const router = useRouter(); + + return ( + + title="Publish Workflow" + action={createModelAction} + close={close} + defaultValues={{ + // TODO - LLM-generated slug + isPrivate: false, + }} + formDataToInput={(data) => ({ + code: `// Generated by Squiggle AI +${workflow.result.code}`, + slug: data.slug ?? "", + groupSlug: data.group?.slug, + isPrivate: data.isPrivate, + })} + submitText="Publish" + onSuccess={(data) => { + // Note: redirect in server action would be incompatible with https://github.com/TheEdoRan/next-safe-action/issues/303 + // (and might a bad idea anyway, returning a url is more verbose but more flexible for reuse) + router.push(data.url); + }} + > + + + ); +}; + +export const PublishWorkflowButton: FC = ({ workflow }) => { + const [isOpen, setIsOpen] = useState(false); + + return ( + <> + + {isOpen && ( + setIsOpen(false)} + /> + )} + + ); +}; diff --git a/apps/hub/src/app/ai/WorkflowViewer/index.tsx b/apps/hub/src/app/ai/WorkflowViewer/index.tsx index 2b5e60c035..a60e8a7791 100644 --- a/apps/hub/src/app/ai/WorkflowViewer/index.tsx +++ b/apps/hub/src/app/ai/WorkflowViewer/index.tsx @@ -9,6 +9,7 @@ import { useAvailableHeight } from "@/lib/hooks/useAvailableHeight"; import { LogsView } from "../LogsView"; import { SquigglePlaygroundForWorkflow } from "../SquigglePlaygroundForWorkflow"; import { Header } from "./Header"; +import { PublishWorkflowButton } from "./PublishWorkflowButton"; import { WorkflowSteps } from "./WorkflowSteps"; type WorkflowViewerProps< @@ -56,7 +57,8 @@ const FinishedWorkflowViewer: FC> = ({ /> )} renderRight={() => ( -
+
+ diff --git a/apps/hub/src/app/new/model/NewModel.tsx b/apps/hub/src/app/new/model/NewModel.tsx index 86ffbf9b5f..5bc7793206 100644 --- a/apps/hub/src/app/new/model/NewModel.tsx +++ b/apps/hub/src/app/new/model/NewModel.tsx @@ -11,12 +11,34 @@ import { SlugFormField } from "@/components/ui/SlugFormField"; import { useSafeActionForm } from "@/lib/hooks/useSafeActionForm"; import { createModelAction } from "@/models/actions/createModelAction"; -type FormShape = { +export type NewModelFormShape = { slug: string | undefined; group: SelectGroupOption | null; isPrivate: boolean; }; +export const NewModelFormBody: FC = () => { + return ( + // reused in AI workflows +
+ + name="slug" + example="my-long-model" + label="Model Name" + placeholder="my-model" + /> + + label="Group" + description="Optional. Models owned by a group are editable by all members of the group." + name="group" + required={false} + myOnly + /> + label="Private" name="isPrivate" /> +
+ ); +}; + export const NewModel: FC<{ initialGroup: SelectGroupOption | null }> = ({ initialGroup, }) => { @@ -25,12 +47,12 @@ export const NewModel: FC<{ initialGroup: SelectGroupOption | null }> = ({ const router = useRouter(); const { form, onSubmit, inFlight } = useSafeActionForm< - FormShape, + NewModelFormShape, typeof createModelAction >({ mode: "onChange", defaultValues: { - // don't pass `slug: ""` here, it will lead to form reset if a user started to type in a value before JS finished loading + // don't pass `slug: ""` here, it will lead to form reset if the user starts to type in a value before JS loading finishes group, isPrivate: false, }, @@ -52,22 +74,7 @@ export const NewModel: FC<{ initialGroup: SelectGroupOption | null }> = ({

New Model

-
- - name="slug" - example="my-long-model" - label="Model Name" - placeholder="my-model" - /> - - label="Group" - description="Optional. Models owned by a group are editable by all members of the group." - name="group" - required={false} - myOnly - /> - label="Private" name="isPrivate" /> -
+ {isOpen && (