Skip to content

Commit

Permalink
Issue
Browse files Browse the repository at this point in the history
  • Loading branch information
cregourd committed Feb 28, 2025
1 parent 5a69cbd commit a3feb95
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 138 deletions.
169 changes: 77 additions & 92 deletions packages/next-admin/src/components/inputs/SelectWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,9 @@ import * as Select from "@radix-ui/react-select";
import { WidgetProps } from "@rjsf/utils";
import clsx from "clsx";
import Link from "next/link";
import { useCallback, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import DoubleArrow from "../../assets/icons/DoubleArrow";
import { useConfig } from "../../context/ConfigContext";
import useClickOutside from "../../hooks/useCloseOnOutsideClick";
import { useDisclosure } from "../../hooks/useDisclosure";
import { Enumeration } from "../../types";
import { slugify } from "../../utils/tools";
import { Selector } from "./Selector";
Expand All @@ -32,112 +30,99 @@ const SelectWidget = ({
placeholder,
schema
}: SelectWidgetProps) => {
const { isOpen, onToggle, onClose } = useDisclosure();
const containerRef = useClickOutside<HTMLDivElement>(() => {
onClose();
});
const [enumOptions, setEnumOptions] = useState<Enumeration[]>(
options?.enumOptions || []
);

const [selectedOption, setSelectedOption] = useState<Enumeration | null>(() => {
const addEnumOptions = useCallback((options: Enumeration[]) => {
setEnumOptions((prev) => [
...prev,
...options.filter((option) => !prev.some((p) => p.value === option.value))
]);
}, []);

const [selectedOption, setSelectedOption] = useState<Enumeration | null>();


useEffect(() => {
if (typeof value === "string") {
return enumOptions.find((option) => option.value === value) || null;
setSelectedOption(enumOptions.find((option) => String(option.value) === value) || null);
} else {
setSelectedOption(value || null);
}
return value || null;
});
}, []);

const { basePath } = useConfig();
useEffect(() => {
console.log("selectedOption", name, selectedOption);
}, [selectedOption]);

const handleChange = useCallback(
(value: Enumeration | null) => {
if (!enumOptions.some((option) => option.value === value?.value)) {
value && setEnumOptions((prev) => [...(prev ?? []), value]);
}
setSelectedOption(value);
onChange(value?.value);
onClose();
},
[onChange, onClose]
);
const { basePath } = useConfig();

const hasValue = useMemo(() => {
return Object.keys(selectedOption || {}).length > 0;
}, [selectedOption]);

return (
<div className="relative">
<Select.Root
name={name}
disabled={disabled}
required={required && !hasValue}
onValueChange={(value) => {
const selectedEnum = enumOptions.find(option => option.value === value);
if (selectedEnum) {
handleChange(selectedEnum);
}
}}
open={isOpen}
onOpenChange={(open) => {
if (open) {
onToggle();
} else {
onClose();
}
}}
value={selectedOption?.value}
<Select.Root
name={name}
disabled={disabled}
required={required && !hasValue}
onValueChange={(value) => {
console.log("value", value);
const option = enumOptions.find((option) => String(option.value) === value);
console.log("option", option);
setSelectedOption(option || null);
}}
>
<Select.Trigger
className={clsx(
"ring-nextadmin-border-default dark:ring-dark-nextadmin-border-strong dark:bg-dark-nextadmin-background-subtle flex w-full cursor-default justify-between rounded-md px-3 py-2 text-sm placeholder-gray-500 shadow-sm ring-1 focus-within:ring-nextadmin-brand-default dark:focus-within:ring-dark-nextadmin-brand-default outline-none",
disabled && "cursor-not-allowed opacity-50"
)}
>
<Select.Trigger
className={clsx(
"ring-nextadmin-border-default dark:ring-dark-nextadmin-border-strong dark:bg-dark-nextadmin-background-subtle flex w-full cursor-default justify-between rounded-md px-3 py-2 text-sm placeholder-gray-500 shadow-sm ring-1 focus-within:ring-nextadmin-brand-default dark:focus-within:ring-dark-nextadmin-brand-default",
disabled && "cursor-not-allowed opacity-50"
)}
>
<Select.Value placeholder={placeholder} className="text-nextadmin-content-inverted dark:text-dark-nextadmin-content-inverted h-full w-full flex-1 appearance-none bg-transparent focus:outline-none">
{selectedOption?.label}
</Select.Value>
<div className="relative z-10 flex cursor-pointer space-x-3">
{hasValue && schema.relation && (
<Select.Icon asChild>
<Link
href={`${basePath}/${slugify(
schema.relation
)}/${selectedOption?.value}`}
className="flex items-center"
>
<ArrowTopRightOnSquareIcon className="h-5 w-5 cursor-pointer text-gray-400" />
</Link>
</Select.Icon>
)}
{hasValue && !disabled && (
<Select.Icon
className="flex items-center"
onClick={(e) => {
e.preventDefault();
handleChange(null);
}}
>
<XMarkIcon className="h-5 w-5 cursor-pointer text-gray-400" />
</Select.Icon>
)}
{!disabled && (
<Select.Icon
<Select.Value placeholder={placeholder} className="text-nextadmin-content-inverted dark:text-dark-nextadmin-content-inverted h-full w-full flex-1 appearance-none bg-transparent focus:outline-none">
{selectedOption?.label}
</Select.Value>
<div className="relative z-10 flex cursor-pointer space-x-3">
{hasValue && schema.relation && (
<Select.Icon asChild>
<Link
href={`${basePath}/${slugify(
schema.relation
)}/${selectedOption?.value}`}
className="flex items-center"
>
<DoubleArrow />
</Select.Icon>
)}
</div>
</Select.Trigger>
<Selector
ref={containerRef}
name={name}
onChange={handleChange}
options={enumOptions?.length && !schema.relation ? enumOptions : undefined}
selectedOptions={hasValue && selectedOption ? [selectedOption] : []}
/>
</Select.Root>
</div>
<ArrowTopRightOnSquareIcon className="h-5 w-5 cursor-pointer text-gray-400" />
</Link>
</Select.Icon>
)}
{hasValue && !disabled && (
<Select.Icon
className="flex items-center"
onMouseDown={(e) => {
e.preventDefault();
onChange(null);
}}
>
<XMarkIcon className="h-5 w-5 cursor-pointer text-gray-400" />
</Select.Icon>
)}
{!disabled && (
<Select.Icon
className="flex items-center"
>
<DoubleArrow />
</Select.Icon>
)}
</div>
</Select.Trigger>
<Selector
name={name}
options={enumOptions?.length && !schema.relation ? enumOptions : undefined}
selectedOptions={hasValue && selectedOption ? [selectedOption] : []}
setEnumOptions={addEnumOptions}
/>
</Select.Root>
);
};

Expand Down
96 changes: 51 additions & 45 deletions packages/next-admin/src/components/inputs/Selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ import LoaderRow from "../LoaderRow";
export type SelectorProps = {
open?: boolean;
name: string;
onChange: (value: Enumeration | null) => void;
options?: Enumeration[];
selectedOptions?: Enumeration[];
setEnumOptions: (options: Enumeration[]) => void;
};

export const Selector = forwardRef<HTMLDivElement, SelectorProps>(
({ open, name, onChange, options, selectedOptions }, ref) => {
({ open, name, options, selectedOptions, setEnumOptions }, ref) => {
const currentQuery = useRef("");
const searchInput = useRef<HTMLInputElement>(null);
const { t } = useI18n();
Expand All @@ -36,6 +36,11 @@ export const Selector = forwardRef<HTMLDivElement, SelectorProps>(
fieldName: name,
initialOptions: options,
});

useEffect(() => {
setEnumOptions(allOptions);
}, [allOptions]);

const [optionsLeft, setOptionsLeft] = useState<Enumeration[]>(() => {
return allOptions.filter(
(item) => !selectedOptions?.some((option) => option.value === item.value)
Expand Down Expand Up @@ -108,51 +113,52 @@ export const Selector = forwardRef<HTMLDivElement, SelectorProps>(
};

return (
<SelectPrimitive.Content
onScroll={onScroll}
className="bg-nextadmin-background-default dark:bg-dark-nextadmin-background-emphasis ring-nextadmin-border-default dark:ring-dark-nextadmin-border-strong z-20 mt-2 overflow-hidden w-full rounded-md shadow-2xl ring-1"
ref={ref}
>
<SelectPrimitive.Viewport className="max-h-60 w-full overflow-y-auto">
<div className="relative flex flex-col">
<div className="dark:bg-dark-nextadmin-background-subtle dark:border-dark-nextadmin-border-strong sticky top-0 block items-center justify-between border-b border-gray-200 bg-gray-50 px-3 py-2">
<div className="relative flex items-center">
<input
autoFocus
id={`${name}-search`}
ref={searchInput}
defaultValue={currentQuery.current}
type="text"
className="dark:bg-dark-nextadmin-background-subtle text-nextadmin-content-inverted dark:text-dark-nextadmin-content-inverted ring-nextadmin-border-default focus:ring-nextadmin-brand-default dark:focus:ring-dark-nextadmin-brand-default dark:ring-dark-nextadmin-border-strong block w-full rounded-md border-0 px-2 py-1.5 text-sm shadow-sm ring-1 ring-inset transition-all duration-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 sm:leading-6"
placeholder={`${t("list.header.search.placeholder")}...`}
onChange={onSearchChange}
/>
<SelectPrimitive.Portal>
<SelectPrimitive.Content
onScroll={onScroll}
className="bg-nextadmin-background-default dark:bg-dark-nextadmin-background-emphasis ring-nextadmin-border-default dark:ring-dark-nextadmin-border-strong z-20 my-2 overflow-hidden rounded-md shadow-2xl ring-1"
ref={ref}
position="popper"
side="bottom"
style={{ width: 'var(--radix-select-trigger-width)' }}
>
<SelectPrimitive.Viewport className="max-h-60 w-full overflow-y-auto">
<div className="flex flex-col">
<div className="dark:bg-dark-nextadmin-background-subtle dark:border-dark-nextadmin-border-strong sticky top-0 block items-center justify-between border-b border-gray-200 bg-gray-50 px-3 py-2">
<div className="relative flex items-center">
<input
autoFocus
id={`${name}-search`}
ref={searchInput}
defaultValue={currentQuery.current}
type="text"
className="dark:bg-dark-nextadmin-background-subtle text-nextadmin-content-inverted dark:text-dark-nextadmin-content-inverted ring-nextadmin-border-default focus:ring-nextadmin-brand-default dark:focus:ring-dark-nextadmin-brand-default dark:ring-dark-nextadmin-border-strong block w-full rounded-md border-0 px-2 py-1.5 text-sm shadow-sm ring-1 ring-inset transition-all duration-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 sm:leading-6"
placeholder={`${t("list.header.search.placeholder")}...`}
onChange={onSearchChange}
/>
</div>
</div>
{optionsLeft &&
optionsLeft.length > 0 &&
optionsLeft?.map((option, index: number) => (
<SelectPrimitive.Item
key={index}
value={String(option.value)}
className="dark:bg-dark-nextadmin-background-subtle dark:text-dark-nextadmin-content-inverted dark:hover:bg-dark-nextadmin-brand-default cursor-pointer px-3 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900 outline-none"
>
{option.label}
</SelectPrimitive.Item>
))}
{isPending && <LoaderRow />}
{optionsLeft && optionsLeft.length === 0 && !isPending && (
<div className="dark:bg-dark-nextadmin-background-subtle dark:text-dark-nextadmin-content-inverted px-3 py-2 text-sm text-gray-700">
No results found
</div>
)}
</div>
{optionsLeft &&
optionsLeft.length > 0 &&
optionsLeft?.map((option, index: number) => (
<SelectPrimitive.Item
key={index}
value={option.value}
className="dark:bg-dark-nextadmin-background-subtle dark:text-dark-nextadmin-content-inverted dark:hover:bg-dark-nextadmin-brand-default cursor-pointer px-3 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900"
onSelect={(event) => {
event.preventDefault();
onChange(option);
}}
>
{option.label}
</SelectPrimitive.Item>
))}
{isPending && <LoaderRow />}
{optionsLeft && optionsLeft.length === 0 && !isPending && (
<div className="dark:bg-dark-nextadmin-background-subtle dark:text-dark-nextadmin-content-inverted px-3 py-2 text-sm text-gray-700">
No results found
</div>
)}
</div>
</SelectPrimitive.Viewport>
</SelectPrimitive.Content>
</SelectPrimitive.Viewport>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
);
}
);
2 changes: 1 addition & 1 deletion packages/next-admin/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -813,7 +813,7 @@ export type Select<M extends ModelName> = {

export type Enumeration = {
label: string;
value: string;
value: string | number;
data?: any;
};

Expand Down

0 comments on commit a3feb95

Please sign in to comment.