refactor: attribute form components and object modal

This commit is contained in:
Aaryan Khandelwal 2023-10-02 17:29:48 +05:30
parent 4f33c4bea3
commit 4d158a6d8f
17 changed files with 1573 additions and 931 deletions

View File

@ -1,14 +1,8 @@
import { useEffect, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// mobx
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// headless ui
import { Disclosure } from "@headlessui/react";
// react-hook-form
import { Control, Controller, UseFormWatch, useForm } from "react-hook-form";
// components // components
import { import {
CheckboxAttributeForm, CheckboxAttributeForm,
@ -21,81 +15,23 @@ import {
TextAttributeForm, TextAttributeForm,
UrlAttributeForm, UrlAttributeForm,
} from "components/custom-attributes"; } from "components/custom-attributes";
// ui
import { PrimaryButton, SecondaryButton, ToggleSwitch } from "components/ui";
// icons
import { ChevronDown } from "lucide-react";
// types // types
import { ICustomAttribute, TCustomAttributeTypes } from "types"; import { ICustomAttribute, TCustomAttributeTypes } from "types";
// constants
import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
type Props = { type Props = {
attributeDetails: Partial<ICustomAttribute>; attributeDetails: ICustomAttribute;
objectId: string; objectId: string;
type: TCustomAttributeTypes; type: TCustomAttributeTypes;
}; };
export type FormComponentProps = { export const AttributeForm: React.FC<Props> = observer((props) => {
control: Control<Partial<ICustomAttribute>, any>; const { attributeDetails, objectId, type } = props;
objectId: string;
watch: UseFormWatch<Partial<ICustomAttribute>>;
};
const RenderForm: React.FC<{ type: TCustomAttributeTypes } & FormComponentProps> = ({
control,
objectId,
type,
watch,
}) => {
let FormToRender: any = <></>;
if (type === "checkbox")
FormToRender = <CheckboxAttributeForm control={control} objectId={objectId} watch={watch} />;
else if (type === "datetime")
FormToRender = <DateTimeAttributeForm control={control} objectId={objectId} watch={watch} />;
else if (type === "email")
FormToRender = <EmailAttributeForm control={control} objectId={objectId} watch={watch} />;
else if (type === "file")
FormToRender = <FileAttributeForm control={control} objectId={objectId} watch={watch} />;
else if (type === "multi_select")
FormToRender = (
<SelectAttributeForm control={control} objectId={objectId} watch={watch} multiple />
);
else if (type === "number")
FormToRender = <NumberAttributeForm control={control} objectId={objectId} watch={watch} />;
else if (type === "relation")
FormToRender = <RelationAttributeForm control={control} objectId={objectId} watch={watch} />;
else if (type === "select")
FormToRender = <SelectAttributeForm control={control} objectId={objectId} watch={watch} />;
else if (type === "text")
FormToRender = <TextAttributeForm control={control} objectId={objectId} watch={watch} />;
else if (type === "url")
FormToRender = <UrlAttributeForm control={control} objectId={objectId} watch={watch} />;
return FormToRender;
};
const OPTIONAL_FIELDS = ["checkbox", "file"];
export const AttributeForm: React.FC<Props> = observer(({ attributeDetails, objectId, type }) => {
const [isRemoving, setIsRemoving] = useState(false);
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
const typeMetaData = CUSTOM_ATTRIBUTES_LIST[type];
const { customAttributes } = useMobxStore(); const { customAttributes } = useMobxStore();
const {
control,
formState: { isSubmitting },
handleSubmit,
reset,
watch,
} = useForm({ defaultValues: typeMetaData.defaultFormValues });
const handleUpdateAttribute = async (data: Partial<ICustomAttribute>) => { const handleUpdateAttribute = async (data: Partial<ICustomAttribute>) => {
if (!workspaceSlug || !attributeDetails.id || !objectId) return; if (!workspaceSlug || !attributeDetails.id || !objectId) return;
@ -110,80 +46,96 @@ export const AttributeForm: React.FC<Props> = observer(({ attributeDetails, obje
const handleDeleteAttribute = async () => { const handleDeleteAttribute = async () => {
if (!workspaceSlug || !attributeDetails.id || !objectId) return; if (!workspaceSlug || !attributeDetails.id || !objectId) return;
setIsRemoving(true); await customAttributes.deleteObjectAttribute(
workspaceSlug.toString(),
await customAttributes objectId,
.deleteObjectAttribute(workspaceSlug.toString(), objectId, attributeDetails.id) attributeDetails.id
.finally(() => setIsRemoving(false)); );
}; };
useEffect(() => { switch (type) {
if (!attributeDetails) return; case "checkbox":
return (
reset({ <CheckboxAttributeForm
...typeMetaData.defaultFormValues, attributeDetails={attributeDetails}
...attributeDetails, handleDeleteAttribute={handleDeleteAttribute}
}); handleUpdateAttribute={handleUpdateAttribute}
}, [attributeDetails, reset, typeMetaData.defaultFormValues]); />
);
return ( case "datetime":
<Disclosure return (
as="div" <DateTimeAttributeForm
className="bg-custom-background-90 border border-custom-border-200 rounded" attributeDetails={attributeDetails}
> handleDeleteAttribute={handleDeleteAttribute}
{({ open }) => ( handleUpdateAttribute={handleUpdateAttribute}
<> />
<Disclosure.Button className="p-3 flex items-center justify-between gap-1 w-full"> );
<div className="flex items-center gap-2.5"> case "email":
<typeMetaData.icon size={14} strokeWidth={1.5} /> return (
<h6 className="text-sm">{attributeDetails.display_name ?? typeMetaData.label}</h6> <EmailAttributeForm
</div> attributeDetails={attributeDetails}
<div className={`${open ? "-rotate-180" : ""} transition-all`}> handleDeleteAttribute={handleDeleteAttribute}
<ChevronDown size={16} strokeWidth={1.5} rotate="180deg" /> handleUpdateAttribute={handleUpdateAttribute}
</div> />
</Disclosure.Button> );
<Disclosure.Panel> case "file":
<form onSubmit={handleSubmit(handleUpdateAttribute)} className="p-3 pl-9 pt-0"> return (
{attributeDetails.type && ( <FileAttributeForm
<RenderForm attributeDetails={attributeDetails}
type={attributeDetails.type} handleDeleteAttribute={handleDeleteAttribute}
control={control} handleUpdateAttribute={handleUpdateAttribute}
objectId={objectId} />
watch={watch} );
/> case "multi_select":
)} return (
<div className="mt-8 flex items-center justify-between"> <SelectAttributeForm
<div className="flex-shrink-0 flex items-center gap-2"> attributeDetails={attributeDetails}
{!OPTIONAL_FIELDS.includes(attributeDetails.type ?? "") && ( handleDeleteAttribute={handleDeleteAttribute}
<> handleUpdateAttribute={handleUpdateAttribute}
<Controller multiple
control={control} />
name="is_required" );
render={({ field: { onChange, value } }) => ( case "number":
<ToggleSwitch value={value ?? false} onChange={onChange} /> return (
)} <NumberAttributeForm
/> attributeDetails={attributeDetails}
<span className="text-xs">Mandatory field</span> handleDeleteAttribute={handleDeleteAttribute}
</> handleUpdateAttribute={handleUpdateAttribute}
)} />
</div> );
<div className="flex items-center gap-2"> case "relation":
<SecondaryButton return (
type="button" <RelationAttributeForm
onClick={handleDeleteAttribute} attributeDetails={attributeDetails}
loading={isRemoving} handleDeleteAttribute={handleDeleteAttribute}
> handleUpdateAttribute={handleUpdateAttribute}
{isRemoving ? "Removing..." : "Remove"} />
</SecondaryButton> );
<PrimaryButton type="submit" loading={isSubmitting}> case "select":
{isSubmitting ? "Saving..." : "Save"} return (
</PrimaryButton> <SelectAttributeForm
</div> attributeDetails={attributeDetails}
</div> handleDeleteAttribute={handleDeleteAttribute}
</form> handleUpdateAttribute={handleUpdateAttribute}
</Disclosure.Panel> />
</> );
)} case "text":
</Disclosure> return (
); <TextAttributeForm
attributeDetails={attributeDetails}
handleDeleteAttribute={handleDeleteAttribute}
handleUpdateAttribute={handleUpdateAttribute}
/>
);
case "url":
return (
<UrlAttributeForm
attributeDetails={attributeDetails}
handleDeleteAttribute={handleDeleteAttribute}
handleUpdateAttribute={handleUpdateAttribute}
/>
);
default:
return null;
}
}); });

View File

@ -1,14 +1,27 @@
import { useState } from "react";
import Image from "next/image"; import Image from "next/image";
import { Controller, useForm } from "react-hook-form";
import { Disclosure } from "@headlessui/react";
// react-hook-form
import { Controller } from "react-hook-form";
// components // components
import { FormComponentProps, Input } from "components/custom-attributes"; import { Input } from "components/custom-attributes";
// ui
import { PrimaryButton, SecondaryButton } from "components/ui";
// icons // icons
import { CheckCircle2 } from "lucide-react"; import { CheckCircle2, ChevronDown } from "lucide-react";
// assets // assets
import CheckRepresentation from "public/custom-attributes/checkbox/check.svg"; import CheckRepresentation from "public/custom-attributes/checkbox/check.svg";
import ToggleSwitchRepresentation from "public/custom-attributes/checkbox/toggle-switch.svg"; import ToggleSwitchRepresentation from "public/custom-attributes/checkbox/toggle-switch.svg";
// types
import { ICustomAttribute } from "types";
// constants
import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
type Props = {
attributeDetails: ICustomAttribute;
handleDeleteAttribute: () => Promise<void>;
handleUpdateAttribute: (data: Partial<ICustomAttribute>) => Promise<void>;
};
const checkboxAttributeRepresentations = [ const checkboxAttributeRepresentations = [
{ {
@ -23,91 +36,141 @@ const checkboxAttributeRepresentations = [
}, },
]; ];
export const CheckboxAttributeForm: React.FC<FormComponentProps> = ({ control }) => ( const typeMetaData = CUSTOM_ATTRIBUTES_LIST.checkbox;
<>
<div className="space-y-3">
<Controller
control={control}
name="display_name"
render={({ field: { onChange, value } }) => (
<Input placeholder="Enter field title" value={value} onChange={onChange} />
)}
/>
<div>
<p className="text-xs">Default value</p>
<Controller
control={control}
name="default_value"
render={({ field: { onChange, value } }) => (
<div className="mt-2 flex items-center gap-6 accent-custom-primary-100">
<div className="flex items-center gap-1 text-xs">
<input
type="radio"
name="default_value"
value="true"
id="checked"
className="scale-75"
defaultChecked={value === "true"}
onChange={(e) => onChange(e.target.value)}
/>
<label htmlFor="checked">Checked</label>
</div>
<div className="flex items-center gap-1 text-xs"> export const CheckboxAttributeForm: React.FC<Props> = (props) => {
<input const { attributeDetails, handleDeleteAttribute, handleUpdateAttribute } = props;
type="radio"
name="default_value" const [isRemoving, setIsRemoving] = useState(false);
value="false"
id="unchecked" const {
className="scale-75" control,
defaultChecked={value === "false"} formState: { isSubmitting },
onChange={(e) => onChange(e.target.value)} handleSubmit,
/> } = useForm({ defaultValues: typeMetaData.defaultFormValues });
<label htmlFor="unchecked">Unchecked</label>
</div> const handleDelete = async () => {
setIsRemoving(true);
await handleDeleteAttribute().finally(() => setIsRemoving(false));
};
return (
<Disclosure
as="div"
className="bg-custom-background-90 border border-custom-border-200 rounded"
>
{({ open }) => (
<>
<Disclosure.Button className="p-3 flex items-center justify-between gap-1 w-full">
<div className="flex items-center gap-2.5">
<typeMetaData.icon size={14} strokeWidth={1.5} />
<h6 className="text-sm">{attributeDetails.display_name ?? typeMetaData.label}</h6>
</div> </div>
)} <div className={`${open ? "-rotate-180" : ""} transition-all`}>
/> <ChevronDown size={16} strokeWidth={1.5} rotate="180deg" />
</div> </div>
</div> </Disclosure.Button>
<div className="my-3 border-t-[0.5px] border-custom-border-200" /> <Disclosure.Panel>
<div> <form onSubmit={handleSubmit(handleUpdateAttribute)} className="p-3 pl-9 pt-0">
<h6 className="text-xs">Show as</h6> <div className="space-y-3">
<div className="mt-2 flex items-center gap-4 flex-wrap"> <Controller
<Controller control={control}
control={control} name="display_name"
name="extra_settings.representation" render={({ field: { onChange, value } }) => (
render={({ field: { onChange, value } }) => ( <Input placeholder="Enter field title" value={value} onChange={onChange} />
<> )}
{checkboxAttributeRepresentations.map((representation) => ( />
<div <div>
key={representation.key} <p className="text-xs">Default value</p>
className={`rounded divide-y w-32 cursor-pointer border ${ <Controller
value === representation.key control={control}
? "border-custom-primary-100 divide-custom-primary-100" name="default_value"
: "border-custom-border-200 divide-custom-border-200" render={({ field: { onChange, value } }) => (
}`} <div className="mt-2 flex items-center gap-6 accent-custom-primary-100">
onClick={() => onChange(representation.key)} <div className="flex items-center gap-1 text-xs">
> <input
<div className="h-24 p-2.5 grid place-items-center"> type="radio"
<Image src={representation.image} alt={representation.label} /> name="default_value"
</div> value="true"
<div className="h-9 text-xs font-medium p-2.5 bg-custom-background-100 rounded-b flex items-center justify-between gap-2"> id="checked"
{representation.label} className="scale-75"
{value === representation.key && ( defaultChecked={value === "true"}
<CheckCircle2 onChange={(e) => onChange(e.target.value)}
size={14} />
strokeWidth={1.5} <label htmlFor="checked">Checked</label>
className="text-custom-primary-100" </div>
/>
<div className="flex items-center gap-1 text-xs">
<input
type="radio"
name="default_value"
value="false"
id="unchecked"
className="scale-75"
defaultChecked={value === "false"}
onChange={(e) => onChange(e.target.value)}
/>
<label htmlFor="unchecked">Unchecked</label>
</div>
</div>
)} )}
</div> />
</div> </div>
))} </div>
</> <div className="my-3 border-t-[0.5px] border-custom-border-200" />
)} <div>
/> <h6 className="text-xs">Show as</h6>
</div> <div className="mt-2 flex items-center gap-4 flex-wrap">
</div> <Controller
</> control={control}
); name="extra_settings.representation"
render={({ field: { onChange, value } }) => (
<>
{checkboxAttributeRepresentations.map((representation) => (
<div
key={representation.key}
className={`rounded divide-y w-32 cursor-pointer border ${
value === representation.key
? "border-custom-primary-100 divide-custom-primary-100"
: "border-custom-border-200 divide-custom-border-200"
}`}
onClick={() => onChange(representation.key)}
>
<div className="h-24 p-2.5 grid place-items-center">
<Image src={representation.image} alt={representation.label} />
</div>
<div className="h-9 text-xs font-medium p-2.5 bg-custom-background-100 rounded-b flex items-center justify-between gap-2">
{representation.label}
{value === representation.key && (
<CheckCircle2
size={14}
strokeWidth={1.5}
className="text-custom-primary-100"
/>
)}
</div>
</div>
))}
</>
)}
/>
</div>
</div>
<div className="mt-8 flex items-center justify-end">
<div className="flex items-center gap-2">
<SecondaryButton type="button" onClick={handleDelete} loading={isRemoving}>
{isRemoving ? "Removing..." : "Remove"}
</SecondaryButton>
<PrimaryButton type="submit" loading={isSubmitting}>
{isSubmitting ? "Saving..." : "Save"}
</PrimaryButton>
</div>
</div>
</form>
</Disclosure.Panel>
</>
)}
</Disclosure>
);
};

View File

@ -1,112 +1,191 @@
// react-hook-form import { useState } from "react";
import { Controller } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
// components import { Disclosure } from "@headlessui/react";
import { FormComponentProps, Input } from "components/custom-attributes";
// ui
import { CustomSelect, ToggleSwitch, Tooltip } from "components/ui";
// constants
import { DATE_FORMATS, TIME_FORMATS } from "constants/custom-attributes";
export const DateTimeAttributeForm: React.FC<FormComponentProps> = ({ control, watch }) => ( // components
<div className="space-y-3"> import { Input } from "components/custom-attributes";
<Controller // ui
control={control} import { CustomSelect, PrimaryButton, SecondaryButton, ToggleSwitch, Tooltip } from "components/ui";
name="display_name" // icons
render={({ field: { onChange, value } }) => ( import { ChevronDown } from "lucide-react";
<Input placeholder="Enter field title" value={value} onChange={onChange} /> // types
import { ICustomAttribute } from "types";
// constants
import { CUSTOM_ATTRIBUTES_LIST, DATE_FORMATS, TIME_FORMATS } from "constants/custom-attributes";
type Props = {
attributeDetails: ICustomAttribute;
handleDeleteAttribute: () => Promise<void>;
handleUpdateAttribute: (data: Partial<ICustomAttribute>) => Promise<void>;
};
const typeMetaData = CUSTOM_ATTRIBUTES_LIST.datetime;
export const DateTimeAttributeForm: React.FC<Props> = (props) => {
const { attributeDetails, handleDeleteAttribute, handleUpdateAttribute } = props;
const [isRemoving, setIsRemoving] = useState(false);
const {
control,
formState: { isSubmitting },
handleSubmit,
watch,
} = useForm({ defaultValues: typeMetaData.defaultFormValues });
const handleDelete = async () => {
setIsRemoving(true);
await handleDeleteAttribute().finally(() => setIsRemoving(false));
};
return (
<Disclosure
as="div"
className="bg-custom-background-90 border border-custom-border-200 rounded"
>
{({ open }) => (
<>
<Disclosure.Button className="p-3 flex items-center justify-between gap-1 w-full">
<div className="flex items-center gap-2.5">
<typeMetaData.icon size={14} strokeWidth={1.5} />
<h6 className="text-sm">{attributeDetails.display_name ?? typeMetaData.label}</h6>
</div>
<div className={`${open ? "-rotate-180" : ""} transition-all`}>
<ChevronDown size={16} strokeWidth={1.5} rotate="180deg" />
</div>
</Disclosure.Button>
<Disclosure.Panel>
<form onSubmit={handleSubmit(handleUpdateAttribute)} className="p-3 pl-9 pt-0">
<div className="space-y-3">
<Controller
control={control}
name="display_name"
render={({ field: { onChange, value } }) => (
<Input placeholder="Enter field title" value={value} onChange={onChange} />
)}
/>
<div>
<Controller
control={control}
name="extra_settings.date_format"
render={({ field: { onChange, value } }) => (
<CustomSelect
label={
<span className="text-xs">
{DATE_FORMATS.find((f) => f.value === value)?.label}
</span>
}
value={value}
onChange={onChange}
buttonClassName="bg-custom-background-100 !px-3 !py-2 !border-custom-border-200 !rounded"
optionsClassName="w-full"
input
>
{DATE_FORMATS.map((format) => (
<CustomSelect.Option key={format.value} value={format.value}>
{format.label}
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
<Controller
control={control}
name="extra_settings.hide_date"
render={({ field: { onChange, value } }) => (
<div className="flex items-center justify-end gap-1 mt-2">
<Tooltip
tooltipContent="Cannot disable both, date and time"
disabled={!watch("extra_settings.hide_time")}
>
<div className="flex items-center gap-1">
<ToggleSwitch
value={value ?? false}
onChange={onChange}
size="sm"
disabled={watch("extra_settings.hide_time")}
/>
<span className="text-xs">Don{"'"}t show date</span>
</div>
</Tooltip>
</div>
)}
/>
</div>
<div>
<Controller
control={control}
name="extra_settings.time_format"
render={({ field: { onChange, value } }) => (
<CustomSelect
label={
<span className="text-xs">
{TIME_FORMATS.find((f) => f.value === value)?.label}
</span>
}
value={value}
onChange={onChange}
buttonClassName="bg-custom-background-100 !px-3 !py-2 !border-custom-border-200 !rounded"
optionsClassName="w-full"
input
>
{TIME_FORMATS.map((format) => (
<CustomSelect.Option key={format.value} value={format.value}>
{format.label}
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
<Controller
control={control}
name="extra_settings.hide_time"
render={({ field: { onChange, value } }) => (
<div className="flex items-center justify-end gap-1 mt-2">
<Tooltip
tooltipContent="Cannot disable both, date and time"
disabled={!watch("extra_settings.hide_date")}
>
<div className="flex items-center gap-1">
<ToggleSwitch
value={value ?? false}
onChange={onChange}
size="sm"
disabled={watch("extra_settings.hide_date")}
/>
<span className="text-xs">Don{"'"}t show time</span>
</div>
</Tooltip>
</div>
)}
/>
</div>
</div>
<div className="mt-8 flex items-center justify-between">
<div className="flex-shrink-0 flex items-center gap-2">
<Controller
control={control}
name="is_required"
render={({ field: { onChange, value } }) => (
<ToggleSwitch value={value ?? false} onChange={onChange} />
)}
/>
<span className="text-xs">Mandatory field</span>
</div>
<div className="flex items-center gap-2">
<SecondaryButton type="button" onClick={handleDelete} loading={isRemoving}>
{isRemoving ? "Removing..." : "Remove"}
</SecondaryButton>
<PrimaryButton type="submit" loading={isSubmitting}>
{isSubmitting ? "Saving..." : "Save"}
</PrimaryButton>
</div>
</div>
</form>
</Disclosure.Panel>
</>
)} )}
/> </Disclosure>
<div> );
<Controller };
control={control}
name="extra_settings.date_format"
render={({ field: { onChange, value } }) => (
<CustomSelect
label={
<span className="text-xs">{DATE_FORMATS.find((f) => f.value === value)?.label}</span>
}
value={value}
onChange={onChange}
buttonClassName="bg-custom-background-100 !px-3 !py-2 !border-custom-border-200 !rounded"
optionsClassName="w-full"
input
>
{DATE_FORMATS.map((format) => (
<CustomSelect.Option key={format.value} value={format.value}>
{format.label}
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
<Controller
control={control}
name="extra_settings.hide_date"
render={({ field: { onChange, value } }) => (
<div className="flex items-center justify-end gap-1 mt-2">
<Tooltip
tooltipContent="Cannot disable both, date and time"
disabled={!watch("extra_settings.hide_time")}
>
<div className="flex items-center gap-1">
<ToggleSwitch
value={value ?? false}
onChange={onChange}
size="sm"
disabled={watch("extra_settings.hide_time")}
/>
<span className="text-xs">Don{"'"}t show date</span>
</div>
</Tooltip>
</div>
)}
/>
</div>
<div>
<Controller
control={control}
name="extra_settings.time_format"
render={({ field: { onChange, value } }) => (
<CustomSelect
label={
<span className="text-xs">{TIME_FORMATS.find((f) => f.value === value)?.label}</span>
}
value={value}
onChange={onChange}
buttonClassName="bg-custom-background-100 !px-3 !py-2 !border-custom-border-200 !rounded"
optionsClassName="w-full"
input
>
{TIME_FORMATS.map((format) => (
<CustomSelect.Option key={format.value} value={format.value}>
{format.label}
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
<Controller
control={control}
name="extra_settings.hide_time"
render={({ field: { onChange, value } }) => (
<div className="flex items-center justify-end gap-1 mt-2">
<Tooltip
tooltipContent="Cannot disable both, date and time"
disabled={!watch("extra_settings.hide_date")}
>
<div className="flex items-center gap-1">
<ToggleSwitch
value={value ?? false}
onChange={onChange}
size="sm"
disabled={watch("extra_settings.hide_date")}
/>
<span className="text-xs">Don{"'"}t show time</span>
</div>
</Tooltip>
</div>
)}
/>
</div>
</div>
);

View File

@ -1,28 +1,106 @@
// react-hook-form import { useState } from "react";
import { Controller } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
// ui import { Disclosure } from "@headlessui/react";
import { FormComponentProps, Input } from "components/custom-attributes";
export const EmailAttributeForm: React.FC<FormComponentProps> = ({ control }) => ( // components
<div className="space-y-3"> import { Input } from "components/custom-attributes";
<Controller // ui
control={control} import { PrimaryButton, SecondaryButton, ToggleSwitch } from "components/ui";
name="display_name" // icons
render={({ field: { onChange, value } }) => ( import { ChevronDown } from "lucide-react";
<Input placeholder="Enter field title" value={value} onChange={onChange} /> // types
import { ICustomAttribute } from "types";
// constants
import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
type Props = {
attributeDetails: ICustomAttribute;
handleDeleteAttribute: () => Promise<void>;
handleUpdateAttribute: (data: Partial<ICustomAttribute>) => Promise<void>;
};
const typeMetaData = CUSTOM_ATTRIBUTES_LIST.email;
export const EmailAttributeForm: React.FC<Props> = (props) => {
const { attributeDetails, handleDeleteAttribute, handleUpdateAttribute } = props;
const [isRemoving, setIsRemoving] = useState(false);
const {
control,
formState: { isSubmitting },
handleSubmit,
} = useForm({ defaultValues: typeMetaData.defaultFormValues });
const handleDelete = async () => {
setIsRemoving(true);
await handleDeleteAttribute().finally(() => setIsRemoving(false));
};
return (
<Disclosure
as="div"
className="bg-custom-background-90 border border-custom-border-200 rounded"
>
{({ open }) => (
<>
<Disclosure.Button className="p-3 flex items-center justify-between gap-1 w-full">
<div className="flex items-center gap-2.5">
<typeMetaData.icon size={14} strokeWidth={1.5} />
<h6 className="text-sm">{attributeDetails.display_name ?? typeMetaData.label}</h6>
</div>
<div className={`${open ? "-rotate-180" : ""} transition-all`}>
<ChevronDown size={16} strokeWidth={1.5} rotate="180deg" />
</div>
</Disclosure.Button>
<Disclosure.Panel>
<form onSubmit={handleSubmit(handleUpdateAttribute)} className="p-3 pl-9 pt-0">
<div className="space-y-3">
<Controller
control={control}
name="display_name"
render={({ field: { onChange, value } }) => (
<Input placeholder="Enter field title" value={value} onChange={onChange} />
)}
/>
<Controller
control={control}
name="default_value"
render={({ field: { onChange, value } }) => (
<Input
type="email"
placeholder="Enter default email"
value={value?.toString()}
onChange={onChange}
/>
)}
/>
</div>
<div className="mt-8 flex items-center justify-between">
<div className="flex-shrink-0 flex items-center gap-2">
<Controller
control={control}
name="is_required"
render={({ field: { onChange, value } }) => (
<ToggleSwitch value={value ?? false} onChange={onChange} />
)}
/>
<span className="text-xs">Mandatory field</span>
</div>
<div className="flex items-center gap-2">
<SecondaryButton type="button" onClick={handleDelete} loading={isRemoving}>
{isRemoving ? "Removing..." : "Remove"}
</SecondaryButton>
<PrimaryButton type="submit" loading={isSubmitting}>
{isSubmitting ? "Saving..." : "Save"}
</PrimaryButton>
</div>
</div>
</form>
</Disclosure.Panel>
</>
)} )}
/> </Disclosure>
<Controller );
control={control} };
name="default_value"
render={({ field: { onChange, value } }) => (
<Input
type="email"
placeholder="Enter default email"
value={value?.toString()}
onChange={onChange}
/>
)}
/>
</div>
);

View File

@ -1,23 +1,91 @@
// react-hook-form import { useState } from "react";
import { Controller } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
// components import { Disclosure } from "@headlessui/react";
import { FileFormatsDropdown, FormComponentProps, Input } from "components/custom-attributes";
export const FileAttributeForm: React.FC<FormComponentProps> = ({ control }) => ( // components
<div className="space-y-3"> import { FileFormatsDropdown, Input } from "components/custom-attributes";
<Controller // ui
control={control} import { PrimaryButton, SecondaryButton } from "components/ui";
name="display_name" // icons
render={({ field: { onChange, value } }) => ( import { ChevronDown } from "lucide-react";
<Input placeholder="Enter field title" value={value} onChange={onChange} /> // types
import { ICustomAttribute } from "types";
// constants
import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
type Props = {
attributeDetails: ICustomAttribute;
handleDeleteAttribute: () => Promise<void>;
handleUpdateAttribute: (data: Partial<ICustomAttribute>) => Promise<void>;
};
const typeMetaData = CUSTOM_ATTRIBUTES_LIST.file;
export const FileAttributeForm: React.FC<Props> = (props) => {
const { attributeDetails, handleDeleteAttribute, handleUpdateAttribute } = props;
const [isRemoving, setIsRemoving] = useState(false);
const {
control,
formState: { isSubmitting },
handleSubmit,
} = useForm({ defaultValues: typeMetaData.defaultFormValues });
const handleDelete = async () => {
setIsRemoving(true);
await handleDeleteAttribute().finally(() => setIsRemoving(false));
};
return (
<Disclosure
as="div"
className="bg-custom-background-90 border border-custom-border-200 rounded"
>
{({ open }) => (
<>
<Disclosure.Button className="p-3 flex items-center justify-between gap-1 w-full">
<div className="flex items-center gap-2.5">
<typeMetaData.icon size={14} strokeWidth={1.5} />
<h6 className="text-sm">{attributeDetails.display_name ?? typeMetaData.label}</h6>
</div>
<div className={`${open ? "-rotate-180" : ""} transition-all`}>
<ChevronDown size={16} strokeWidth={1.5} rotate="180deg" />
</div>
</Disclosure.Button>
<Disclosure.Panel>
<form onSubmit={handleSubmit(handleUpdateAttribute)} className="p-3 pl-9 pt-0">
<div className="space-y-3">
<Controller
control={control}
name="display_name"
render={({ field: { onChange, value } }) => (
<Input placeholder="Enter field title" value={value} onChange={onChange} />
)}
/>
<Controller
control={control}
name="extra_settings.file_formats"
render={({ field: { onChange, value } }) => (
<FileFormatsDropdown value={value} onChange={onChange} />
)}
/>
</div>
<div className="mt-8 flex items-center justify-end">
<div className="flex items-center gap-2">
<SecondaryButton type="button" onClick={handleDelete} loading={isRemoving}>
{isRemoving ? "Removing..." : "Remove"}
</SecondaryButton>
<PrimaryButton type="submit" loading={isSubmitting}>
{isSubmitting ? "Saving..." : "Save"}
</PrimaryButton>
</div>
</div>
</form>
</Disclosure.Panel>
</>
)} )}
/> </Disclosure>
<Controller );
control={control} };
name="extra_settings.file_formats"
render={({ field: { onChange, value } }) => (
<FileFormatsDropdown value={value} onChange={onChange} />
)}
/>
</div>
);

View File

@ -1,18 +1,28 @@
import { useState } from "react";
import Image from "next/image"; import Image from "next/image";
import { Controller, useForm } from "react-hook-form";
import { Disclosure } from "@headlessui/react";
// react-hook-form
import { Controller } from "react-hook-form";
// components // components
import { ColorPicker, FormComponentProps } from "components/custom-attributes"; import { ColorPicker, Input } from "components/custom-attributes";
// ui // ui
import { ToggleSwitch } from "components/ui"; import { PrimaryButton, SecondaryButton, ToggleSwitch } from "components/ui";
import { Input } from "../input";
// icons // icons
import { CheckCircle2 } from "lucide-react"; import { CheckCircle2, ChevronDown } from "lucide-react";
// assets // assets
import NumericalRepresentation from "public/custom-attributes/number/numerical.svg"; import NumericalRepresentation from "public/custom-attributes/number/numerical.svg";
import BarRepresentation from "public/custom-attributes/number/bar.svg"; import BarRepresentation from "public/custom-attributes/number/bar.svg";
import RingRepresentation from "public/custom-attributes/number/ring.svg"; import RingRepresentation from "public/custom-attributes/number/ring.svg";
// types
import { ICustomAttribute } from "types";
// constants
import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
type Props = {
attributeDetails: ICustomAttribute;
handleDeleteAttribute: () => Promise<void>;
handleUpdateAttribute: (data: Partial<ICustomAttribute>) => Promise<void>;
};
const numberAttributeRepresentations = [ const numberAttributeRepresentations = [
{ {
@ -32,120 +42,185 @@ const numberAttributeRepresentations = [
}, },
]; ];
export const NumberAttributeForm: React.FC<FormComponentProps> = ({ control, watch }) => ( const typeMetaData = CUSTOM_ATTRIBUTES_LIST.number;
<>
<div className="space-y-3"> export const NumberAttributeForm: React.FC<Props> = (props) => {
<Controller const { attributeDetails, handleDeleteAttribute, handleUpdateAttribute } = props;
control={control}
name="display_name" const [isRemoving, setIsRemoving] = useState(false);
render={({ field: { onChange, value } }) => (
<Input placeholder="Enter field title" value={value} onChange={onChange} /> const {
)} control,
/> formState: { isSubmitting },
<Controller handleSubmit,
control={control} watch,
name="default_value" } = useForm({ defaultValues: typeMetaData.defaultFormValues });
render={({ field: { onChange, value } }) => (
<Input const handleDelete = async () => {
type="number" setIsRemoving(true);
placeholder="Enter default value"
value={value ?? ""} await handleDeleteAttribute().finally(() => setIsRemoving(false));
onChange={onChange} };
step={1}
/> return (
)} <Disclosure
/> as="div"
</div> className="bg-custom-background-90 border border-custom-border-200 rounded"
<div className="my-3 border-t-[0.5px] border-custom-border-200" /> >
<div> {({ open }) => (
<h6 className="text-xs">Show as</h6> <>
<div className="mt-2 flex items-center gap-4 flex-wrap"> <Disclosure.Button className="p-3 flex items-center justify-between gap-1 w-full">
<Controller <div className="flex items-center gap-2.5">
control={control} <typeMetaData.icon size={14} strokeWidth={1.5} />
name="extra_settings.representation" <h6 className="text-sm">{attributeDetails.display_name ?? typeMetaData.label}</h6>
render={({ field: { onChange, value } }) => ( </div>
<> <div className={`${open ? "-rotate-180" : ""} transition-all`}>
{numberAttributeRepresentations.map((representation) => ( <ChevronDown size={16} strokeWidth={1.5} rotate="180deg" />
<div </div>
key={representation.key} </Disclosure.Button>
className={`rounded divide-y w-32 cursor-pointer border ${ <Disclosure.Panel>
value === representation.key <form onSubmit={handleSubmit(handleUpdateAttribute)} className="p-3 pl-9 pt-0">
? "border-custom-primary-100 divide-custom-primary-100" <div className="space-y-3">
: "border-custom-border-200 divide-custom-border-200" <Controller
}`} control={control}
onClick={() => onChange(representation.key)} name="display_name"
> render={({ field: { onChange, value } }) => (
<div className="h-24 p-2.5 grid place-items-center"> <Input placeholder="Enter field title" value={value} onChange={onChange} />
<Image src={representation.image} alt={representation.label} /> )}
</div> />
<div className="h-9 text-xs font-medium p-2.5 bg-custom-background-100 rounded-b flex items-center justify-between gap-2"> <Controller
{representation.label} control={control}
{value === representation.key && ( name="default_value"
<CheckCircle2 render={({ field: { onChange, value } }) => (
size={14} <Input
strokeWidth={1.5} type="number"
className="text-custom-primary-100" placeholder="Enter default value"
/> value={value ?? ""}
onChange={onChange}
step={1}
/>
)}
/>
</div>
<div className="my-3 border-t-[0.5px] border-custom-border-200" />
<div>
<h6 className="text-xs">Show as</h6>
<div className="mt-2 flex items-center gap-4 flex-wrap">
<Controller
control={control}
name="extra_settings.representation"
render={({ field: { onChange, value } }) => (
<>
{numberAttributeRepresentations.map((representation) => (
<div
key={representation.key}
className={`rounded divide-y w-32 cursor-pointer border ${
value === representation.key
? "border-custom-primary-100 divide-custom-primary-100"
: "border-custom-border-200 divide-custom-border-200"
}`}
onClick={() => onChange(representation.key)}
>
<div className="h-24 p-2.5 grid place-items-center">
<Image src={representation.image} alt={representation.label} />
</div>
<div className="h-9 text-xs font-medium p-2.5 bg-custom-background-100 rounded-b flex items-center justify-between gap-2">
{representation.label}
{value === representation.key && (
<CheckCircle2
size={14}
strokeWidth={1.5}
className="text-custom-primary-100"
/>
)}
</div>
</div>
))}
</>
)} )}
</div>
</div>
))}
</>
)}
/>
</div>
</div>
{watch &&
(watch("extra_settings.representation") === "bar" ||
watch("extra_settings.representation") === "ring") && (
<div className="mt-6 grid grid-cols-3 gap-x-2 gap-y-3 items-center">
<>
<div className="text-xs">Divided by</div>
<div className="col-span-2">
<Controller
control={control}
name="extra_settings.divided_by"
render={({ field: { onChange, value } }) => (
<Input
type="number"
placeholder="Maximum value"
value={value}
onChange={onChange}
className="hide-arrows"
min={0}
step={1}
defaultValue={100}
required
/> />
</div>
</div>
{watch &&
(watch("extra_settings.representation") === "bar" ||
watch("extra_settings.representation") === "ring") && (
<div className="mt-6 grid grid-cols-3 gap-x-2 gap-y-3 items-center">
<>
<div className="text-xs">Divided by</div>
<div className="col-span-2">
<Controller
control={control}
name="extra_settings.divided_by"
render={({ field: { onChange, value } }) => (
<Input
type="number"
placeholder="Maximum value"
value={value}
onChange={onChange}
className="hide-arrows"
min={0}
step={1}
defaultValue={100}
required
/>
)}
/>
</div>
</>
<>
<div className="text-xs">Color</div>
<div className="col-span-2">
<Controller
control={control}
name="color"
render={({ field: { onChange, value } }) => (
<ColorPicker
onChange={onChange}
selectedColor={value ?? "#000000"}
size={18}
/>
)}
/>
</div>
</>
<>
<div className="text-xs">Show number</div>
<div className="col-span-2">
<Controller
control={control}
name="extra_settings.show_number"
render={({ field: { onChange, value } }) => (
<ToggleSwitch value={value} onChange={onChange} />
)}
/>
</div>
</>
</div>
)} )}
/> <div className="mt-8 flex items-center justify-between">
</div> <div className="flex-shrink-0 flex items-center gap-2">
</> <Controller
<> control={control}
<div className="text-xs">Color</div> name="is_required"
<div className="col-span-2"> render={({ field: { onChange, value } }) => (
<Controller <ToggleSwitch value={value ?? false} onChange={onChange} />
control={control} )}
name="color" />
render={({ field: { onChange, value } }) => ( <span className="text-xs">Mandatory field</span>
<ColorPicker onChange={onChange} selectedColor={value ?? "#000000"} size={18} /> </div>
)} <div className="flex items-center gap-2">
/> <SecondaryButton type="button" onClick={handleDelete} loading={isRemoving}>
</div> {isRemoving ? "Removing..." : "Remove"}
</> </SecondaryButton>
<> <PrimaryButton type="submit" loading={isSubmitting}>
<div className="text-xs">Show number</div> {isSubmitting ? "Saving..." : "Save"}
<div className="col-span-2"> </PrimaryButton>
<Controller </div>
control={control} </div>
name="extra_settings.show_number" </form>
render={({ field: { onChange, value } }) => ( </Disclosure.Panel>
<ToggleSwitch value={value} onChange={onChange} /> </>
)}
/>
</div>
</>
</div>
)} )}
</> </Disclosure>
); );
};

View File

@ -1,42 +1,90 @@
// react-hook-form import { useState } from "react";
import { Controller } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
// components import { Disclosure } from "@headlessui/react";
import { FormComponentProps, Input } from "components/custom-attributes";
// ui
import { CustomSelect } from "components/ui";
// constants
import { CUSTOM_ATTRIBUTE_UNITS } from "constants/custom-attributes";
export const RelationAttributeForm: React.FC<FormComponentProps> = ({ control }) => ( // components
<div className="space-y-3"> import { Input } from "components/custom-attributes";
<Controller // ui
control={control} import { CustomSelect, PrimaryButton, SecondaryButton, ToggleSwitch } from "components/ui";
name="display_name" // icons
render={({ field: { onChange, value } }) => ( import { ChevronDown } from "lucide-react";
<Input placeholder="Enter field title" value={value} onChange={onChange} /> // types
)} import { ICustomAttribute } from "types";
/> // constants
<Controller import { CUSTOM_ATTRIBUTES_LIST, CUSTOM_ATTRIBUTE_UNITS } from "constants/custom-attributes";
control={control}
name="unit" type Props = {
render={({ field: { onChange, value } }) => ( attributeDetails: ICustomAttribute;
<CustomSelect handleDeleteAttribute: () => Promise<void>;
label={<span className="capitalize text-xs">{value}</span>} handleUpdateAttribute: (data: Partial<ICustomAttribute>) => Promise<void>;
value={value} };
onChange={onChange}
buttonClassName="bg-custom-background-100 !px-3 !py-2 !border-custom-border-200 !rounded" const typeMetaData = CUSTOM_ATTRIBUTES_LIST.relation;
optionsClassName="w-full"
input export const RelationAttributeForm: React.FC<Props> = (props) => {
> const { attributeDetails, handleDeleteAttribute, handleUpdateAttribute } = props;
{CUSTOM_ATTRIBUTE_UNITS.map((unit) => (
<CustomSelect.Option key={unit.value} value={unit.value}> const [isRemoving, setIsRemoving] = useState(false);
{unit.label}
</CustomSelect.Option> const {
))} control,
</CustomSelect> formState: { isSubmitting },
)} handleSubmit,
/> } = useForm({ defaultValues: typeMetaData.defaultFormValues });
{/* <div>
const handleDelete = async () => {
setIsRemoving(true);
await handleDeleteAttribute().finally(() => setIsRemoving(false));
};
return (
<Disclosure
as="div"
className="bg-custom-background-90 border border-custom-border-200 rounded"
>
{({ open }) => (
<>
<Disclosure.Button className="p-3 flex items-center justify-between gap-1 w-full">
<div className="flex items-center gap-2.5">
<typeMetaData.icon size={14} strokeWidth={1.5} />
<h6 className="text-sm">{attributeDetails.display_name ?? typeMetaData.label}</h6>
</div>
<div className={`${open ? "-rotate-180" : ""} transition-all`}>
<ChevronDown size={16} strokeWidth={1.5} rotate="180deg" />
</div>
</Disclosure.Button>
<Disclosure.Panel>
<form onSubmit={handleSubmit(handleUpdateAttribute)} className="p-3 pl-9 pt-0">
<div className="space-y-3">
<Controller
control={control}
name="display_name"
render={({ field: { onChange, value } }) => (
<Input placeholder="Enter field title" value={value} onChange={onChange} />
)}
/>
<Controller
control={control}
name="unit"
render={({ field: { onChange, value } }) => (
<CustomSelect
label={<span className="capitalize text-xs">{value}</span>}
value={value}
onChange={onChange}
buttonClassName="bg-custom-background-100 !px-3 !py-2 !border-custom-border-200 !rounded"
optionsClassName="w-full"
input
>
{CUSTOM_ATTRIBUTE_UNITS.map((unit) => (
<CustomSelect.Option key={unit.value} value={unit.value}>
{unit.label}
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
{/* <div>
<p className="text-xs">Selection type</p> <p className="text-xs">Selection type</p>
<div className="mt-2 flex items-center gap-6 accent-custom-primary-100"> <div className="mt-2 flex items-center gap-6 accent-custom-primary-100">
<div className="flex items-center gap-1 text-xs"> <div className="flex items-center gap-1 text-xs">
@ -57,5 +105,31 @@ export const RelationAttributeForm: React.FC<FormComponentProps> = ({ control })
</div> </div>
</div> </div>
</div> */} </div> */}
</div> </div>
); <div className="mt-8 flex items-center justify-between">
<div className="flex-shrink-0 flex items-center gap-2">
<Controller
control={control}
name="is_required"
render={({ field: { onChange, value } }) => (
<ToggleSwitch value={value ?? false} onChange={onChange} />
)}
/>
<span className="text-xs">Mandatory field</span>
</div>
<div className="flex items-center gap-2">
<SecondaryButton type="button" onClick={handleDelete} loading={isRemoving}>
{isRemoving ? "Removing..." : "Remove"}
</SecondaryButton>
<PrimaryButton type="submit" loading={isSubmitting}>
{isSubmitting ? "Saving..." : "Save"}
</PrimaryButton>
</div>
</div>
</form>
</Disclosure.Panel>
</>
)}
</Disclosure>
);
};

View File

@ -1,54 +1,142 @@
import React, { useState } from "react"; import React, { useEffect, useState } from "react";
// mobx
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Controller, useForm } from "react-hook-form";
import { Disclosure } from "@headlessui/react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// react-hook-form
import { Controller } from "react-hook-form";
// components // components
import { FormComponentProps, Input, OptionForm, SelectOption } from "components/custom-attributes"; import { Input, OptionForm, SelectOption } from "components/custom-attributes";
// ui
import { PrimaryButton, SecondaryButton, ToggleSwitch } from "components/ui";
// icons
import { ChevronDown } from "lucide-react";
// types // types
import { ICustomAttribute } from "types"; import { ICustomAttribute } from "types";
// constants
import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
export const SelectAttributeForm: React.FC<FormComponentProps & { multiple?: boolean }> = observer( type Props = {
({ control, multiple = false, objectId = "", watch }) => { attributeDetails: ICustomAttribute;
const [optionToEdit, setOptionToEdit] = useState<ICustomAttribute | null>(null); handleDeleteAttribute: () => Promise<void>;
handleUpdateAttribute: (data: Partial<ICustomAttribute>) => Promise<void>;
};
const { customAttributes } = useMobxStore(); const typeMetaData = CUSTOM_ATTRIBUTES_LIST.select;
const options = customAttributes.objectAttributes?.[objectId]?.[watch("id") ?? ""]?.children; export const SelectAttributeForm: React.FC<Props & { multiple?: boolean }> = observer((props) => {
const {
attributeDetails,
handleDeleteAttribute,
handleUpdateAttribute,
multiple = false,
} = props;
return ( const [optionToEdit, setOptionToEdit] = useState<ICustomAttribute | null>(null);
<div className="space-y-3"> const [isRemoving, setIsRemoving] = useState(false);
<Controller
control={control} const { customAttributes } = useMobxStore();
name="display_name"
render={({ field: { onChange, value } }) => ( const {
<Input placeholder="Enter field title" value={value} onChange={onChange} /> control,
)} formState: { isSubmitting },
/> handleSubmit,
<div> reset,
<p className="text-xs">Options</p> watch,
<div className="mt-3 space-y-2 w-3/5"> } = useForm({ defaultValues: typeMetaData.defaultFormValues });
{options?.map((option) => (
<SelectOption const options =
key={option.id} customAttributes.objectAttributes?.[attributeDetails.parent ?? ""]?.[watch("id") ?? ""]
handleEditOption={() => setOptionToEdit(option)} ?.children;
objectId={objectId}
option={option} const handleDelete = async () => {
/> setIsRemoving(true);
))}
</div> await handleDeleteAttribute().finally(() => setIsRemoving(false));
<div className="mt-2 w-3/5"> };
<OptionForm
data={optionToEdit} useEffect(() => {
objectId={objectId} if (!attributeDetails) return;
onSubmit={() => setOptionToEdit(null)}
parentId={watch("id") ?? ""} reset({
/> ...typeMetaData.defaultFormValues,
</div> ...attributeDetails,
</div> });
</div> }, [attributeDetails, reset]);
);
} return (
); <Disclosure
as="div"
className="bg-custom-background-90 border border-custom-border-200 rounded"
>
{({ open }) => (
<>
<Disclosure.Button className="p-3 flex items-center justify-between gap-1 w-full">
<div className="flex items-center gap-2.5">
<typeMetaData.icon size={14} strokeWidth={1.5} />
<h6 className="text-sm">{attributeDetails.display_name ?? typeMetaData.label}</h6>
</div>
<div className={`${open ? "-rotate-180" : ""} transition-all`}>
<ChevronDown size={16} strokeWidth={1.5} rotate="180deg" />
</div>
</Disclosure.Button>
<Disclosure.Panel>
<form onSubmit={handleSubmit(handleUpdateAttribute)} className="p-3 pl-9 pt-0">
<div className="space-y-3">
<Controller
control={control}
name="display_name"
render={({ field: { onChange, value } }) => (
<Input placeholder="Enter field title" value={value} onChange={onChange} />
)}
/>
<div>
<p className="text-xs">Options</p>
<div className="mt-3 space-y-2 w-3/5">
{options?.map((option) => (
<SelectOption
key={option.id}
handleEditOption={() => setOptionToEdit(option)}
objectId={attributeDetails.parent ?? ""}
option={option}
/>
))}
</div>
<div className="mt-2 w-3/5">
<OptionForm
data={optionToEdit}
objectId={attributeDetails.parent ?? ""}
onSubmit={() => setOptionToEdit(null)}
parentId={watch("id") ?? ""}
/>
</div>
</div>
</div>
<div className="mt-8 flex items-center justify-between">
<div className="flex-shrink-0 flex items-center gap-2">
<Controller
control={control}
name="is_required"
render={({ field: { onChange, value } }) => (
<ToggleSwitch value={value ?? false} onChange={onChange} />
)}
/>
<span className="text-xs">Mandatory field</span>
</div>
<div className="flex items-center gap-2">
<SecondaryButton type="button" onClick={handleDelete} loading={isRemoving}>
{isRemoving ? "Removing..." : "Remove"}
</SecondaryButton>
<PrimaryButton type="submit" loading={isSubmitting}>
{isSubmitting ? "Saving..." : "Save"}
</PrimaryButton>
</div>
</div>
</form>
</Disclosure.Panel>
</>
)}
</Disclosure>
);
});

View File

@ -1,23 +1,105 @@
// react-hook-form import { useState } from "react";
import { Controller } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
// ui import { Disclosure } from "@headlessui/react";
import { FormComponentProps, Input } from "components/custom-attributes";
export const TextAttributeForm: React.FC<FormComponentProps> = ({ control }) => ( // components
<div className="space-y-3"> import { Input } from "components/custom-attributes";
<Controller // ui
control={control} import { PrimaryButton, SecondaryButton, ToggleSwitch } from "components/ui";
name="display_name" // icons
render={({ field: { onChange, value } }) => ( import { ChevronDown } from "lucide-react";
<Input placeholder="Enter field title" value={value} onChange={onChange} /> // types
import { ICustomAttribute } from "types";
// constants
import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
type Props = {
attributeDetails: ICustomAttribute;
handleDeleteAttribute: () => Promise<void>;
handleUpdateAttribute: (data: Partial<ICustomAttribute>) => Promise<void>;
};
const typeMetaData = CUSTOM_ATTRIBUTES_LIST.text;
export const TextAttributeForm: React.FC<Props> = (props) => {
const { attributeDetails, handleDeleteAttribute, handleUpdateAttribute } = props;
const [isRemoving, setIsRemoving] = useState(false);
const {
control,
formState: { isSubmitting },
handleSubmit,
} = useForm({ defaultValues: typeMetaData.defaultFormValues });
const handleDelete = async () => {
setIsRemoving(true);
await handleDeleteAttribute().finally(() => setIsRemoving(false));
};
return (
<Disclosure
as="div"
className="bg-custom-background-90 border border-custom-border-200 rounded"
>
{({ open }) => (
<>
<Disclosure.Button className="p-3 flex items-center justify-between gap-1 w-full">
<div className="flex items-center gap-2.5">
<typeMetaData.icon size={14} strokeWidth={1.5} />
<h6 className="text-sm">{attributeDetails.display_name ?? typeMetaData.label}</h6>
</div>
<div className={`${open ? "-rotate-180" : ""} transition-all`}>
<ChevronDown size={16} strokeWidth={1.5} rotate="180deg" />
</div>
</Disclosure.Button>
<Disclosure.Panel>
<form onSubmit={handleSubmit(handleUpdateAttribute)} className="p-3 pl-9 pt-0">
<div className="space-y-3">
<Controller
control={control}
name="display_name"
render={({ field: { onChange, value } }) => (
<Input placeholder="Enter field title" value={value} onChange={onChange} />
)}
/>
<Controller
control={control}
name="default_value"
render={({ field: { onChange, value } }) => (
<Input
placeholder="Enter default value"
value={value ?? ""}
onChange={onChange}
/>
)}
/>
</div>
<div className="mt-8 flex items-center justify-between">
<div className="flex-shrink-0 flex items-center gap-2">
<Controller
control={control}
name="is_required"
render={({ field: { onChange, value } }) => (
<ToggleSwitch value={value ?? false} onChange={onChange} />
)}
/>
<span className="text-xs">Mandatory field</span>
</div>
<div className="flex items-center gap-2">
<SecondaryButton type="button" onClick={handleDelete} loading={isRemoving}>
{isRemoving ? "Removing..." : "Remove"}
</SecondaryButton>
<PrimaryButton type="submit" loading={isSubmitting}>
{isSubmitting ? "Saving..." : "Save"}
</PrimaryButton>
</div>
</div>
</form>
</Disclosure.Panel>
</>
)} )}
/> </Disclosure>
<Controller );
control={control} };
name="default_value"
render={({ field: { onChange, value } }) => (
<Input placeholder="Enter default value" value={value ?? ""} onChange={onChange} />
)}
/>
</div>
);

View File

@ -1,23 +1,106 @@
// react-hook-form import { useState } from "react";
import { Controller } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
// components import { Disclosure } from "@headlessui/react";
import { FormComponentProps, Input } from "components/custom-attributes";
export const UrlAttributeForm: React.FC<FormComponentProps> = ({ control }) => ( // components
<div className="space-y-3"> import { Input } from "components/custom-attributes";
<Controller // ui
control={control} import { PrimaryButton, SecondaryButton, ToggleSwitch } from "components/ui";
name="display_name" // icons
render={({ field: { onChange, value } }) => ( import { ChevronDown } from "lucide-react";
<Input placeholder="Enter field title" value={value} onChange={onChange} /> // types
import { ICustomAttribute } from "types";
// constants
import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
type Props = {
attributeDetails: ICustomAttribute;
handleDeleteAttribute: () => Promise<void>;
handleUpdateAttribute: (data: Partial<ICustomAttribute>) => Promise<void>;
};
const typeMetaData = CUSTOM_ATTRIBUTES_LIST.url;
export const UrlAttributeForm: React.FC<Props> = (props) => {
const { attributeDetails, handleDeleteAttribute, handleUpdateAttribute } = props;
const [isRemoving, setIsRemoving] = useState(false);
const {
control,
formState: { isSubmitting },
handleSubmit,
} = useForm({ defaultValues: typeMetaData.defaultFormValues });
const handleDelete = async () => {
setIsRemoving(true);
await handleDeleteAttribute().finally(() => setIsRemoving(false));
};
return (
<Disclosure
as="div"
className="bg-custom-background-90 border border-custom-border-200 rounded"
>
{({ open }) => (
<>
<Disclosure.Button className="p-3 flex items-center justify-between gap-1 w-full">
<div className="flex items-center gap-2.5">
<typeMetaData.icon size={14} strokeWidth={1.5} />
<h6 className="text-sm">{attributeDetails.display_name ?? typeMetaData.label}</h6>
</div>
<div className={`${open ? "-rotate-180" : ""} transition-all`}>
<ChevronDown size={16} strokeWidth={1.5} rotate="180deg" />
</div>
</Disclosure.Button>
<Disclosure.Panel>
<form onSubmit={handleSubmit(handleUpdateAttribute)} className="p-3 pl-9 pt-0">
<div className="space-y-3">
<Controller
control={control}
name="display_name"
render={({ field: { onChange, value } }) => (
<Input placeholder="Enter field title" value={value} onChange={onChange} />
)}
/>
<Controller
control={control}
name="default_value"
render={({ field: { onChange, value } }) => (
<Input
type="url"
placeholder="Enter default URL"
value={value ?? ""}
onChange={onChange}
/>
)}
/>
</div>
<div className="mt-8 flex items-center justify-between">
<div className="flex-shrink-0 flex items-center gap-2">
<Controller
control={control}
name="is_required"
render={({ field: { onChange, value } }) => (
<ToggleSwitch value={value ?? false} onChange={onChange} />
)}
/>
<span className="text-xs">Mandatory field</span>
</div>
<div className="flex items-center gap-2">
<SecondaryButton type="button" onClick={handleDelete} loading={isRemoving}>
{isRemoving ? "Removing..." : "Remove"}
</SecondaryButton>
<PrimaryButton type="submit" loading={isSubmitting}>
{isSubmitting ? "Saving..." : "Save"}
</PrimaryButton>
</div>
</div>
</form>
</Disclosure.Panel>
</>
)} )}
/> </Disclosure>
<Controller );
control={control} };
name="default_value"
render={({ field: { onChange, value } }) => (
<Input type="url" placeholder="Enter default URL" value={value ?? ""} onChange={onChange} />
)}
/>
</div>
);

View File

@ -23,6 +23,8 @@ export const CustomAttributesCheckboxes: React.FC<Props> = observer((props) => {
const checkboxFields = Object.values(attributes).filter((a) => a.type === "checkbox"); const checkboxFields = Object.values(attributes).filter((a) => a.type === "checkbox");
if (checkboxFields.length === 0) return null;
return ( return (
<div className="space-y-4"> <div className="space-y-4">
{Object.entries(checkboxFields).map(([attributeId, attribute]) => ( {Object.entries(checkboxFields).map(([attributeId, attribute]) => (

View File

@ -35,6 +35,8 @@ export const CustomAttributesDescriptionFields: React.FC<Props> = observer((prop
DESCRIPTION_FIELDS.includes(a.type) DESCRIPTION_FIELDS.includes(a.type)
); );
if (descriptionFields.length === 0) return null;
return ( return (
<Disclosure as="div" defaultOpen> <Disclosure as="div" defaultOpen>
{({ open }) => ( {({ open }) => (

View File

@ -191,6 +191,8 @@ export const CustomAttributesFileUploads: React.FC<Props> = observer((props) =>
const fileUploadFields = Object.values(attributes).filter((a) => a.type === "file"); const fileUploadFields = Object.values(attributes).filter((a) => a.type === "file");
if (fileUploadFields.length === 0) return null;
return ( return (
<> <>
{customAttributes.fetchObjectDetailsLoader ? ( {customAttributes.fetchObjectDetailsLoader ? (

View File

@ -1,206 +1,239 @@
import React, { useEffect, useState } from "react"; import React, { useEffect } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// mobx
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider"; import { Controller, useForm } from "react-hook-form";
// headless ui
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { Input, TypesDropdown, AttributeForm } from "components/custom-attributes"; import { Input, TypesDropdown, AttributeForm } from "components/custom-attributes";
import EmojiIconPicker from "components/emoji-icon-picker";
// ui // ui
import { Loader, PrimaryButton, SecondaryButton } from "components/ui"; import { Loader, PrimaryButton, SecondaryButton } from "components/ui";
// helpers
import { renderEmoji } from "helpers/emoji.helper";
// types // types
import { ICustomAttribute, TCustomAttributeTypes } from "types"; import { ICustomAttribute, TCustomAttributeTypes } from "types";
// constants // constants
import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes"; import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
import { renderEmoji } from "helpers/emoji.helper";
import EmojiIconPicker from "components/emoji-icon-picker";
type Props = { type Props = {
objectIdToEdit?: string | null; data?: ICustomAttribute;
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
onSubmit?: () => Promise<void>; onSubmit?: () => Promise<void>;
}; };
export const ObjectModal: React.FC<Props> = observer( const defaultValues: Partial<ICustomAttribute> = {
({ objectIdToEdit, isOpen, onClose, onSubmit }) => { display_name: "",
const [object, setObject] = useState<Partial<ICustomAttribute>>({ description: "",
display_name: "", icon: "",
description: "", };
export const ObjectModal: React.FC<Props> = observer((props) => {
const { data, isOpen, onClose, onSubmit } = props;
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const {
control,
formState: { isSubmitting },
handleSubmit,
reset,
setValue,
watch,
} = useForm<ICustomAttribute>({ defaultValues });
const objectId = watch("id") && watch("id") !== "" ? watch("id") : null;
const { customAttributes } = useMobxStore();
const handleClose = () => {
onClose();
setTimeout(() => {
reset({ ...defaultValues });
}, 300);
};
const handleCreateObject = async (formData: Partial<ICustomAttribute>) => {
if (!workspaceSlug || !projectId) return;
const payload: Partial<ICustomAttribute> = {
description: formData.description ?? "",
display_name: formData.display_name ?? "",
icon: formData.icon ?? "",
project: projectId.toString(),
type: "entity",
};
await customAttributes
.createObject(workspaceSlug.toString(), payload)
.then((res) => setValue("id", res?.id ?? ""));
};
const handleUpdateObject = async (formData: Partial<ICustomAttribute>) => {
if (!workspaceSlug || !data || !data.id) return;
const payload: Partial<ICustomAttribute> = {
description: formData.description ?? "",
display_name: formData.display_name ?? "",
icon: formData.icon ?? "",
};
await customAttributes.updateObject(workspaceSlug.toString(), data.id, payload);
};
const handleObjectFormSubmit = async (formData: Partial<ICustomAttribute>) => {
if (data) await handleUpdateObject(formData);
else await handleCreateObject(formData);
if (onSubmit) onSubmit();
};
const handleCreateObjectAttribute = async (type: TCustomAttributeTypes) => {
if (!workspaceSlug || !objectId) return;
const typeMetaData = CUSTOM_ATTRIBUTES_LIST[type];
const payload: Partial<ICustomAttribute> = {
display_name: typeMetaData.label,
type,
...typeMetaData.initialPayload,
};
await customAttributes.createObjectAttribute(workspaceSlug.toString(), {
...payload,
parent: objectId,
}); });
const [isCreatingObject, setIsCreatingObject] = useState(false); };
const [isUpdatingObject, setIsUpdatingObject] = useState(false);
const router = useRouter(); // fetch the object details if object state has id
const { workspaceSlug, projectId } = router.query; useEffect(() => {
if (!objectId) return;
const { customAttributes } = useMobxStore(); if (!customAttributes.objectAttributes[objectId]) {
if (!workspaceSlug) return;
const handleClose = () => { customAttributes.fetchObjectDetails(workspaceSlug.toString(), objectId).then((res) => {
onClose(); reset({ ...res });
setTimeout(() => {
setObject({ display_name: "", description: "" });
}, 300);
};
const handleCreateObject = async () => {
if (!workspaceSlug || !projectId) return;
setIsCreatingObject(true);
const payload: Partial<ICustomAttribute> = {
description: object.description ?? "",
display_name: object.display_name ?? "",
icon: object.icon ?? "",
project: projectId.toString(),
type: "entity",
};
await customAttributes
.createObject(workspaceSlug.toString(), payload)
.then((res) => {
setObject((prevData) => ({ ...prevData, ...res }));
if (onSubmit) onSubmit();
})
.finally(() => setIsCreatingObject(false));
};
const handleUpdateObject = async () => {
if (!workspaceSlug || !object || !object.id) return;
setIsUpdatingObject(true);
const payload: Partial<ICustomAttribute> = {
description: object.description ?? "",
display_name: object.display_name ?? "",
icon: object.icon ?? "",
};
await customAttributes
.updateObject(workspaceSlug.toString(), object.id, payload)
.finally(() => setIsUpdatingObject(false));
};
const handleCreateObjectAttribute = async (type: TCustomAttributeTypes) => {
if (!workspaceSlug || !object || !object.id) return;
const typeMetaData = CUSTOM_ATTRIBUTES_LIST[type];
const payload: Partial<ICustomAttribute> = {
display_name: typeMetaData.label,
type,
...typeMetaData.initialPayload,
};
await customAttributes.createObjectAttribute(workspaceSlug.toString(), {
...payload,
parent: object.id,
}); });
}; } else {
reset({
...customAttributes.objects?.find((e) => e.id === objectId),
});
}
}, [customAttributes, objectId, reset, workspaceSlug]);
// fetch the object details if object state has id // update the form if data is present
useEffect(() => { useEffect(() => {
if (!object.id || object.id === "") return; if (!data) return;
if (!customAttributes.objectAttributes[object.id]) { reset({
if (!workspaceSlug) return; ...defaultValues,
...data,
});
}, [data, reset]);
customAttributes.fetchObjectDetails(workspaceSlug.toString(), object.id).then((res) => { return (
setObject((prev) => ({ ...prev, ...res })); <Transition.Root show={isOpen} as={React.Fragment}>
}); <Dialog as="div" className="relative z-20" onClose={handleClose}>
} else { <Transition.Child
setObject((prev) => ({ as={React.Fragment}
...prev, enter="ease-out duration-300"
...customAttributes.objects?.find((e) => e.id === object.id), enterFrom="opacity-0"
})); enterTo="opacity-100"
} leave="ease-in duration-200"
}, [customAttributes, object.id, workspaceSlug]); leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child>
// update the object state if objectIdToEdit is present <Transition.Child
useEffect(() => { as={React.Fragment}
if (!objectIdToEdit) return; enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
setObject((prev) => ({ enterTo="opacity-100 translate-y-0 sm:scale-100"
...prev, leave="ease-in duration-200"
id: objectIdToEdit, leaveFrom="opacity-100 translate-y-0 sm:scale-100"
})); leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
}, [objectIdToEdit]); >
<Dialog.Panel className="fixed inset-0 h-full w-full z-20">
return ( <div className="flex items-center justify-center h-full w-full p-4 sm:p-0 scale-90">
<Transition.Root show={isOpen} as={React.Fragment}> <div className="bg-custom-background-100 w-1/2 max-h-[85%] flex flex-col rounded-xl">
<Dialog as="div" className="relative z-20" onClose={handleClose}> <h3 className="text-2xl font-semibold px-6 pt-5">New Object</h3>
<Transition.Child <div className="mt-5 space-y-5 h-full overflow-y-auto">
as={React.Fragment} <form
enter="ease-out duration-300" onSubmit={handleSubmit(handleObjectFormSubmit)}
enterFrom="opacity-0" className="space-y-4 px-6 pb-5"
enterTo="opacity-100" >
leave="ease-in duration-200" <div className="flex items-center gap-2">
leaveFrom="opacity-100" <div className="h-9 w-9 bg-custom-background-80 grid place-items-center rounded">
leaveTo="opacity-0" <Controller
> control={control}
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" /> name="icon"
</Transition.Child> render={({ field: { onChange, value } }) => (
<EmojiIconPicker
<Transition.Child label={value ? renderEmoji(value) : "Icon"}
as={React.Fragment} onChange={(icon) => {
enter="ease-out duration-300" if (typeof icon === "string") onChange(icon);
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" }}
enterTo="opacity-100 translate-y-0 sm:scale-100" value={value}
leave="ease-in duration-200" showIconPicker={false}
leaveFrom="opacity-100 translate-y-0 sm:scale-100" />
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" )}
>
<Dialog.Panel className="fixed inset-0 h-full w-full z-20">
<div className="flex items-center justify-center h-full w-full p-4 sm:p-0 scale-90">
<div className="bg-custom-background-100 w-1/2 max-h-[85%] flex flex-col rounded-xl">
<h3 className="text-2xl font-semibold px-6 pt-5">New Object</h3>
<div className="mt-5 space-y-5 h-full overflow-y-auto">
<div className="space-y-4 px-6">
<div className="flex items-center gap-2">
<div className="h-9 w-9 bg-custom-background-80 grid place-items-center rounded">
<EmojiIconPicker
label={object.icon ? renderEmoji(object.icon) : "Icon"}
onChange={(icon) => {
if (typeof icon === "string")
setObject((prevData) => ({ ...prevData, icon }));
}}
value={object.icon}
showIconPicker={false}
/>
</div>
<Input
placeholder="Enter Object Title"
value={object.display_name}
onChange={(e) =>
setObject((prevData) => ({ ...prevData, display_name: e.target.value }))
}
/> />
</div> </div>
<textarea <Controller
name="objectDescription" control={control}
id="objectDescription" name="display_name"
className="placeholder:text-custom-text-400 text-xs px-3 py-2 rounded bg-custom-background-100 border border-custom-border-200 w-full focus:outline-none" render={({ field: { onChange, value } }) => (
cols={30} <Input
rows={5} placeholder="Enter Object Title"
placeholder="Enter Object Description" value={value}
value={object.description} onChange={(e) => onChange(e.target.value)}
onChange={(e) => />
setObject((prevData) => ({ ...prevData, description: e.target.value })) )}
}
/> />
{object.id && (
<div className="text-right">
<PrimaryButton onClick={handleUpdateObject} loading={isUpdatingObject}>
{isUpdatingObject ? "Saving..." : "Save changes"}
</PrimaryButton>
</div>
)}
</div> </div>
{object.id && ( <Controller
control={control}
name="description"
render={({ field: { onChange, value } }) => (
<textarea
name="objectDescription"
id="objectDescription"
className="placeholder:text-custom-text-400 text-xs px-3 py-2 rounded bg-custom-background-100 border border-custom-border-200 w-full focus:outline-none"
cols={30}
rows={5}
placeholder="Enter Object Description"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
)}
/>
<div className="flex items-center justify-end gap-3">
<div className="flex items-center gap-3">
{!objectId && (
<SecondaryButton onClick={handleClose}>Close</SecondaryButton>
)}
<PrimaryButton type="submit" loading={isSubmitting}>
{data
? isSubmitting
? "Saving..."
: "Save changes"
: isSubmitting
? "Creating..."
: "Create Object"}
</PrimaryButton>
</div>
</div>
</form>
{objectId && (
<>
<div className="px-6 pb-5"> <div className="px-6 pb-5">
<h4 className="font-medium">Attributes</h4> <h4 className="font-medium">Attributes</h4>
<div className="mt-2 space-y-2"> <div className="mt-2 space-y-2">
@ -209,16 +242,16 @@ export const ObjectModal: React.FC<Props> = observer(
<Loader.Item height="40px" /> <Loader.Item height="40px" />
</Loader> </Loader>
) : ( ) : (
Object.keys(customAttributes.objectAttributes[object.id] ?? {})?.map( Object.keys(customAttributes.objectAttributes[objectId] ?? {})?.map(
(attributeId) => { (attributeId) => {
const attribute = const attribute =
customAttributes.objectAttributes[object.id ?? ""][attributeId]; customAttributes.objectAttributes[objectId][attributeId];
return ( return (
<AttributeForm <AttributeForm
key={attributeId} key={attributeId}
attributeDetails={attribute} attributeDetails={attribute}
objectId={object.id ?? ""} objectId={objectId}
type={attribute.type} type={attribute.type}
/> />
); );
@ -232,33 +265,20 @@ export const ObjectModal: React.FC<Props> = observer(
)} )}
</div> </div>
</div> </div>
)} <div className="flex items-center justify-between gap-3 px-6 py-5 border-t border-custom-border-200">
</div> <div className="flex-shrink-0">
<div <TypesDropdown onClick={handleCreateObjectAttribute} />
className={`flex items-center gap-3 px-6 py-5 border-t border-custom-border-200 ${ </div>
object.id ? "justify-between" : "justify-end" <SecondaryButton onClick={handleClose}>Close</SecondaryButton>
}`}
>
{object.id && (
<div className="flex-shrink-0">
<TypesDropdown onClick={handleCreateObjectAttribute} />
</div> </div>
)} </>
<div className="flex items-center gap-3"> )}
<SecondaryButton onClick={handleClose}>Close</SecondaryButton>
{!object.id && (
<PrimaryButton onClick={handleCreateObject} loading={isCreatingObject}>
{isCreatingObject ? "Creating..." : "Create Object"}
</PrimaryButton>
)}
</div>
</div>
</div> </div>
</div> </div>
</Dialog.Panel> </div>
</Transition.Child> </Dialog.Panel>
</Dialog> </Transition.Child>
</Transition.Root> </Dialog>
); </Transition.Root>
} );
); });

View File

@ -1,38 +1,26 @@
import { useEffect, useState } from "react"; import { useEffect } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// mobx
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { DeleteObjectModal, SingleObject } from "components/custom-attributes"; import { SingleObject } from "components/custom-attributes";
// ui // ui
import { EmptyState, Loader } from "components/ui"; import { EmptyState, Loader } from "components/ui";
// assets // assets
import emptyCustomObjects from "public/empty-state/custom-objects.svg"; import emptyCustomObjects from "public/empty-state/custom-objects.svg";
// types
import { ICustomAttribute } from "types";
type Props = { type Props = {
handleEditObject: (object: ICustomAttribute) => void;
projectId: string; projectId: string;
}; };
export const ObjectsList: React.FC<Props> = observer(({ handleEditObject, projectId }) => { export const ObjectsList: React.FC<Props> = observer(({ projectId }) => {
const [deleteObjectModal, setDeleteObjectModal] = useState(false);
const [objectToDelete, setObjectToDelete] = useState<ICustomAttribute | null>(null);
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
const { customAttributes } = useMobxStore(); const { customAttributes } = useMobxStore();
const handleDeleteObject = async (object: ICustomAttribute) => {
setObjectToDelete(object);
setDeleteObjectModal(true);
};
useEffect(() => { useEffect(() => {
if (!workspaceSlug) return; if (!workspaceSlug) return;
@ -41,47 +29,27 @@ export const ObjectsList: React.FC<Props> = observer(({ handleEditObject, projec
}, [customAttributes, projectId, workspaceSlug]); }, [customAttributes, projectId, workspaceSlug]);
return ( return (
<> <div className="divide-y divide-custom-border-100">
<DeleteObjectModal {customAttributes.objects ? (
isOpen={deleteObjectModal} customAttributes.objects.length > 0 ? (
objectToDelete={objectToDelete} customAttributes.objects.map((object) => <SingleObject key={object.id} object={object} />)
onClose={() => {
setDeleteObjectModal(false);
setTimeout(() => {
setObjectToDelete(null);
}, 300);
}}
/>
<div className="divide-y divide-custom-border-100">
{customAttributes.objects ? (
customAttributes.objects.length > 0 ? (
customAttributes.objects.map((object) => (
<SingleObject
key={object.id}
object={object}
handleDeleteObject={() => handleDeleteObject(object)}
handleEditObject={() => handleEditObject(object)}
/>
))
) : (
<div className="bg-custom-background-90 border border-custom-border-100 rounded max-w-3xl mt-10 mx-auto">
<EmptyState
title="No custom objects yet"
description="You can think of Pages as an AI-powered notepad."
image={emptyCustomObjects}
isFullScreen={false}
/>
</div>
)
) : ( ) : (
<Loader className="space-y-4"> <div className="bg-custom-background-90 border border-custom-border-100 rounded max-w-3xl mt-10 mx-auto">
<Loader.Item height="60px" /> <EmptyState
<Loader.Item height="60px" /> title="No custom objects yet"
<Loader.Item height="60px" /> description="You can think of Pages as an AI-powered notepad."
</Loader> image={emptyCustomObjects}
)} isFullScreen={false}
</div> />
</> </div>
)
) : (
<Loader className="space-y-4">
<Loader.Item height="60px" />
<Loader.Item height="60px" />
<Loader.Item height="60px" />
</Loader>
)}
</div>
); );
}); });

View File

@ -1,3 +1,7 @@
import { useState } from "react";
// components
import { DeleteObjectModal, ObjectModal } from "components/custom-attributes";
// ui // ui
import { CustomMenu } from "components/ui"; import { CustomMenu } from "components/ui";
// icons // icons
@ -9,32 +13,49 @@ import { ICustomAttribute } from "types";
type Props = { type Props = {
object: ICustomAttribute; object: ICustomAttribute;
handleDeleteObject: () => void;
handleEditObject: () => void;
}; };
export const SingleObject: React.FC<Props> = (props) => { export const SingleObject: React.FC<Props> = (props) => {
const { object, handleDeleteObject, handleEditObject } = props; const { object } = props;
const [isEditObjectModalOpen, setIsEditObjectModalOpen] = useState(false);
const [isDeleteObjectModalOpen, setIsDeleteObjectModalOpen] = useState(false);
return ( return (
<div className="flex items-center justify-between gap-4 py-4"> <>
<div className={`flex gap-4 ${object.description === "" ? "items-center" : "items-start"}`}> <ObjectModal
<div className="bg-custom-background-80 h-10 w-10 grid place-items-center rounded"> data={object}
{object.icon ? renderEmoji(object.icon) : <TableProperties size={20} strokeWidth={1.5} />} isOpen={isEditObjectModalOpen}
</div> onClose={() => setIsEditObjectModalOpen(false)}
<div> />
<h5 className="text-sm font-medium">{object.display_name}</h5> <DeleteObjectModal
<p className="text-custom-text-300 text-xs">{object.description}</p> isOpen={isDeleteObjectModalOpen}
objectToDelete={object}
onClose={() => setIsDeleteObjectModalOpen(false)}
/>
<div className="flex items-center justify-between gap-4 py-4">
<div className={`flex gap-4 ${object.description === "" ? "items-center" : "items-start"}`}>
<div className="bg-custom-background-80 h-10 w-10 grid place-items-center rounded">
{object.icon ? (
renderEmoji(object.icon)
) : (
<TableProperties size={20} strokeWidth={1.5} />
)}
</div>
<div>
<h5 className="text-sm font-medium">{object.display_name}</h5>
<p className="text-custom-text-300 text-xs">{object.description}</p>
</div>
</div> </div>
<CustomMenu ellipsis>
<CustomMenu.MenuItem renderAs="button" onClick={() => setIsEditObjectModalOpen(true)}>
Edit
</CustomMenu.MenuItem>
<CustomMenu.MenuItem renderAs="button" onClick={() => setIsDeleteObjectModalOpen(true)}>
Delete
</CustomMenu.MenuItem>
</CustomMenu>
</div> </div>
<CustomMenu ellipsis> </>
<CustomMenu.MenuItem renderAs="button" onClick={handleEditObject}>
Edit
</CustomMenu.MenuItem>
<CustomMenu.MenuItem renderAs="button" onClick={handleDeleteObject}>
Delete
</CustomMenu.MenuItem>
</CustomMenu>
</div>
); );
}; };

View File

@ -1,5 +1,4 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// layouts // layouts
@ -16,22 +15,15 @@ import { PrimaryButton } from "components/ui";
import { truncateText } from "helpers/string.helper"; import { truncateText } from "helpers/string.helper";
// types // types
import type { NextPage } from "next"; import type { NextPage } from "next";
import { ICustomAttribute } from "types";
const CustomObjectSettings: NextPage = () => { const CustomObjectSettings: NextPage = () => {
const [isCreateObjectModalOpen, setIsCreateObjectModalOpen] = useState(false); const [isCreateObjectModalOpen, setIsCreateObjectModalOpen] = useState(false);
const [objectToEdit, setObjectToEdit] = useState<ICustomAttribute | null>(null);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
const { projectDetails } = useProjectDetails(); const { projectDetails } = useProjectDetails();
const handleEditObject = (object: ICustomAttribute) => {
setObjectToEdit(object);
setIsCreateObjectModalOpen(true);
};
return ( return (
<ProjectAuthorizationWrapper <ProjectAuthorizationWrapper
breadcrumbs={ breadcrumbs={
@ -46,12 +38,8 @@ const CustomObjectSettings: NextPage = () => {
} }
> >
<ObjectModal <ObjectModal
objectIdToEdit={objectToEdit?.id}
isOpen={isCreateObjectModalOpen} isOpen={isCreateObjectModalOpen}
onClose={() => { onClose={() => setIsCreateObjectModalOpen(false)}
setIsCreateObjectModalOpen(false);
setObjectToEdit(null);
}}
/> />
<div className="flex flex-row gap-2"> <div className="flex flex-row gap-2">
<div className="w-80 py-8"> <div className="w-80 py-8">
@ -66,10 +54,7 @@ const CustomObjectSettings: NextPage = () => {
</div> </div>
<div> <div>
<div className="mt-4"> <div className="mt-4">
<ObjectsList <ObjectsList projectId={projectId?.toString() ?? ""} />
handleEditObject={handleEditObject}
projectId={projectId?.toString() ?? ""}
/>
</div> </div>
</div> </div>
</section> </section>