Skip to content

Commit

Permalink
Merge pull request #103 from premieroctet/fix/search-select
Browse files Browse the repository at this point in the history
Store formData in context
  • Loading branch information
cregourd authored Jan 15, 2024
2 parents 8e0dfb8 + ce1c30c commit 622e0d5
Show file tree
Hide file tree
Showing 20 changed files with 315 additions and 185 deletions.
5 changes: 5 additions & 0 deletions .changeset/nasty-toys-learn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@premieroctet/next-admin": patch
---

Fix search for relationship fields and enum fields
5 changes: 5 additions & 0 deletions .changeset/shiny-phones-attend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@premieroctet/next-admin": patch
---

Only allow the relationship field in the configuration, not the field that carries the relationship at all - this allows several fields to be used in the Prisma @relation options
5 changes: 5 additions & 0 deletions .changeset/twenty-years-kneel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@premieroctet/next-admin": patch
---

Form submitted with error will keep the state with user modification
2 changes: 1 addition & 1 deletion apps/example/components/DatePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const DatePicker = ({ value, name, onChange }: Props) => {
wrapperClassName="w-full"
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6 px-2 disabled:opacity-50 disabled:bg-gray-200 disabled:cursor-not-allowed [&>div]:border-none"
/>
<input type="hidden" name={name} value={value} />
<input type="hidden" name={name} value={value ?? ""} />
</>
);
};
Expand Down
2 changes: 1 addition & 1 deletion apps/example/components/JsonEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const JsonEditor = ({ value, onChange, name }: Props) => {

return (
<>
<input type="hidden" name={name} value={value} />
<input type="hidden" name={name} value={value ?? ""} />
<Editor
height="20vh"
defaultLanguage="json"
Expand Down
12 changes: 6 additions & 6 deletions apps/example/e2e/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const dataTest: DataTest = {
},
Post: {
title: "MY_POST",
authorId: "User 0 ([email protected])",
author: "User 0 ([email protected])",
},
Category: {
name: "MY_CATEGORY",
Expand All @@ -32,7 +32,7 @@ const dataTestUpdate: DataTest = {
},
Post: {
title: "UPDATE_MY_POST",
authorId: "User 1 ([email protected])",
author: "User 1 ([email protected])",
},
Category: {
name: "UPDATE_MY_CATEGORY",
Expand Down Expand Up @@ -98,8 +98,8 @@ export const fillForm = async (
break;
case "Post":
await page.fill('input[id="title"]', dataTest.Post.title);
await page.getByLabel("authorId*").click();
await page.getByText(dataTest.Post.authorId).click();
await page.getByLabel("author*").click();
await page.getByText(dataTest.Post.author).click();
break;
case "Category":
await page.fill('input[id="name"]', dataTest.Category.name);
Expand Down Expand Up @@ -132,8 +132,8 @@ export const readForm = async (
expect(await page.inputValue('input[id="title"]')).toBe(
dataTest.Post.title
);
expect(await page.inputValue('input[id="authorId"]')).toBe(
dataTest.Post.authorId
expect(await page.inputValue('input[id="author"]')).toBe(
dataTest.Post.author
);
break;
case "Category":
Expand Down
11 changes: 9 additions & 2 deletions apps/example/options.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { NextAdminOptions } from "@premieroctet/next-admin";
import React from "react";
import DatePicker from "./components/DatePicker";
import JsonEditor from "./components/JsonEditor";

Expand Down Expand Up @@ -36,7 +35,7 @@ export const options: NextAdminOptions = {
"role",
"birthDate",
"avatar",
"metadata"
"metadata",
],
fields: {
email: {
Expand Down Expand Up @@ -114,6 +113,14 @@ export const options: NextAdminOptions = {
optionFormatter: (category) => `${category.name} Cat.${category.id}`,
},
},
display: [
"id",
"title",
"content",
"published",
"categories",
"author",
],
},
},
Category: {
Expand Down
2 changes: 1 addition & 1 deletion apps/example/pageRouterOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export const options: NextAdminOptions = {
"title",
"content",
"published",
"authorId",
"author",
"categories",
],
},
Expand Down
2 changes: 1 addition & 1 deletion apps/example/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ model Post {
title String
content String?
published Boolean @default(false)
author User @relation("author", fields: [authorId], references: [id])
author User @relation("author", fields: [authorId], references: [id]) // Many-to-one relation
authorId Int
categories Category[] @relation("category") // implicit Many-to-many relation
comments post_comment[] @relation("comments") // One-to-many relation
Expand Down
49 changes: 28 additions & 21 deletions packages/next-admin/src/components/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import validator from "@rjsf/validator-ajv8";
import clsx from "clsx";
import { ChangeEvent, cloneElement, useMemo, useState } from "react";
import { useConfig } from "../context/ConfigContext";
import { FormContext, FormProvider } from "../context/FormContext";
import { PropertyValidationError } from "../exceptions/ValidationError";
import { useRouterInternal } from "../hooks/useRouterInternal";
import { Field, ModelAction, ModelName, SubmitFormResult } from "../types";
Expand Down Expand Up @@ -237,7 +238,7 @@ const Form = ({
<input
onChange={onChangeOverride || onTextChange}
{...props}
value={props.value ?? undefined}
value={props.value ?? ""}
className={clsx(
"block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6 px-2 disabled:opacity-50 disabled:bg-gray-200 disabled:cursor-not-allowed",
{ "ring-red-600": rawErrors }
Expand Down Expand Up @@ -272,26 +273,32 @@ const Form = ({
/>
)}
</div>
<CustomForm
// @ts-expect-error
action={action ? onSubmit : ""}
{...(!action ? { method: "post" } : {})}
idPrefix=""
idSeparator=""
enctype={!action ? "multipart/form-data" : undefined}
{...schemas}
formData={data}
validator={validator}
extraErrors={extraErrors}
fields={fields}
templates={{
...templates,
ButtonTemplates: { SubmitButton: submitButton },
}}
widgets={widgets}
onSubmit={(e) => console.log("onSubmit", e)}
onError={(e) => console.log("onError", e)}
/>
<FormProvider initialValue={data}>
<FormContext.Consumer>
{({formData, setFormData}) =>
<CustomForm
// @ts-expect-error
action={action ? onSubmit : ""}
{...(!action ? { method: "post" } : {})}
onChange={(e) => {setFormData(e.formData)}}
idPrefix=""
idSeparator=""
enctype={!action ? "multipart/form-data" : undefined}
{...schemas}
formData={formData}
validator={validator}
extraErrors={extraErrors}
fields={fields}
templates={{
...templates,
ButtonTemplates: { SubmitButton: submitButton },
}}
widgets={widgets}
onSubmit={(e) => console.log("onSubmit", e)}
onError={(e) => console.log("onError", e)}
/>}
</FormContext.Consumer>
</FormProvider>
</div>
);
};
Expand Down
56 changes: 31 additions & 25 deletions packages/next-admin/src/components/inputs/MultiSelectWidget.tsx
Original file line number Diff line number Diff line change
@@ -1,57 +1,63 @@
import { useRef, useState } from "react";
import { useEffect, useRef } from "react";
import DoubleArrow from "../../assets/icons/DoubleArrow";
import { useForm } from "../../context/FormContext";
import useCloseOnOutsideClick from "../../hooks/useCloseOnOutsideClick";
import { Enumeration } from "../../types";
import MultiSelectItem from "./MultiSelectItem";
import { Selector } from "./Selector";

const MultiSelectWidget = (props: any) => {
const formContext = useForm();
const { formData, onChange, options, name } = props;
const [open, setOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
useCloseOnOutsideClick(containerRef, () => setOpen(false));
useCloseOnOutsideClick(containerRef, () => formContext.setOpen(false, name));

const onRemoveClick = (value: any) => {
onChange(formData?.filter((item: any) => item !== value));
onChange(formData?.filter((item: Enumeration) => item.value !== value));
};

const values = formData?.map((item: any) =>
options.find((option: any) => option.value === item)
);
const selectValue = values?.map((item: any) => item?.value) ?? [];
const selectedValues = formData?.map((item: any) => item?.value) ?? [];

const optionsLeft = options?.filter(
(option: any) => !formData?.find((item: any) => item === option.value)
(option: Enumeration) => !formData?.find((item: Enumeration) => item.value === option.value)
);

useEffect(() => {
if (formContext.relationState?.[name]?.open) {
// @ts-expect-error
containerRef.current?.querySelector(`#${name}-search`)?.focus();
}
}, []);

return (
<div className="relative" ref={containerRef}>
<div className="relative">
<input type="hidden" name={name} value={JSON.stringify(selectValue)} />
<input type="hidden" name={name} value={JSON.stringify(selectedValues)} />
<div
className="w-full px-3 py-2 pr-10 text-base placeholder-gray-500 border border-gray-300 rounded-md shadow-sm appearance-none focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm cursor-default flex min-h-[38px] flex-wrap gap-x-1 gap-y-1"
onClick={() => setOpen(!open)}
onClick={() => formContext.toggleOpen(name)}
>
{values &&
values.map(
(value: any, index: number) =>
value && (
<MultiSelectItem
key={index}
label={value.label}
onRemoveClick={() => onRemoveClick(value.value)}
/>
)
)}
{formData?.map(
(value: any, index: number) =>
value && (
<MultiSelectItem
key={index}
label={value.label}
onRemoveClick={() => onRemoveClick(value.value)}
/>
)
)}
</div>
<div className="absolute inset-y-0 right-0 flex items-center px-3 pointer-events-none">
<DoubleArrow />
</div>
</div>
<Selector
open={open}
open={formContext.relationState?.[name]?.open!}
options={optionsLeft}
name={name}
onChange={(value: string) => {
onChange([...(formData || []), value]);
onChange={(option: Enumeration) => {
onChange([...(formData || []), option]);
}}
/>
</div>
Expand Down
47 changes: 21 additions & 26 deletions packages/next-admin/src/components/inputs/SelectWidget.tsx
Original file line number Diff line number Diff line change
@@ -1,61 +1,56 @@
import { XMarkIcon } from "@heroicons/react/24/outline";
import { WidgetProps } from "@rjsf/utils";
import { useEffect, useRef, useState } from "react";
import { useEffect, useRef } from "react";
import DoubleArrow from "../../assets/icons/DoubleArrow";
import { XMarkIcon } from "@heroicons/react/24/outline";
import { useForm } from "../../context/FormContext";
import useCloseOnOutsideClick from "../../hooks/useCloseOnOutsideClick";
import { Enumeration } from "../../types";
import { Selector } from "./Selector";
import useCloseOnOutsideClick from "../../hooks/useCloseOnOutsideClick";

const SelectWidget = ({ options, onChange, value, ...props }: WidgetProps) => {
const formContext = useForm();
const name = props.name;
options as { enumOptions: Enumeration[] };
const enumOptions = options.enumOptions?.map(
(option: any) => option.value as Enumeration
);
const [open, setOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
useCloseOnOutsideClick(containerRef, () => setOpen(false));
useCloseOnOutsideClick(containerRef, () => formContext.setOpen(false, name));

function onWindowClick() {}
const handleChange = (option: Enumeration) => {
onChange(option);
formContext.setOpen(false, name);
}

useEffect(() => {
window.addEventListener("click", onWindowClick);
return () => {
window.removeEventListener("click", onWindowClick);
};
if (formContext.relationState?.[name]?.open) {
// @ts-expect-error
containerRef.current?.querySelector(`#${name}-search`)?.focus();
}
}, []);

return (
<div
className="relative"
ref={containerRef}
onBlur={(e) => {
if (e.relatedTarget?.id !== `${props.id}-search`) {
setOpen(false);
}
}}
>
<div className="relative flex justify-between w-full px-3 py-2 text-base placeholder-gray-500 border border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm cursor-default">
<input
type="hidden"
value={value}
value={value?.value || ""}
name={props.name}
className="absolute -z-10 inset-0 w-full h-full opacity-0"
/>
<input
id={props.id}
readOnly
className="w-full h-full flex-1 appearance-none focus:outline-none cursor-default"
value={
value
? enumOptions?.find((option: any) => option.value === value)
?.label
: ""
}
onMouseDown={() => setOpen(!open)}
value={value?.label || ""}
onMouseDown={() => formContext.toggleOpen(name)}
/>
<div className="flex space-x-3">
{value && (
<div className="flex items-center" onClick={() => onChange("")}>
<div className="flex items-center" onClick={() => onChange({})}>
<XMarkIcon className="w-5 h-5 text-gray-400" />
</div>
)}
Expand All @@ -65,10 +60,10 @@ const SelectWidget = ({ options, onChange, value, ...props }: WidgetProps) => {
</div>
</div>
<Selector
open={open}
open={formContext.relationState?.[name]?.open!}
options={enumOptions}
name={props.name}
onChange={onChange}
onChange={handleChange}
/>
</div>
);
Expand Down
Loading

0 comments on commit 622e0d5

Please sign in to comment.