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";
// mobx
import { observer } from "mobx-react-lite";
// mobx store
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
import {
CheckboxAttributeForm,
@ -21,81 +15,23 @@ import {
TextAttributeForm,
UrlAttributeForm,
} from "components/custom-attributes";
// ui
import { PrimaryButton, SecondaryButton, ToggleSwitch } from "components/ui";
// icons
import { ChevronDown } from "lucide-react";
// types
import { ICustomAttribute, TCustomAttributeTypes } from "types";
// constants
import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
type Props = {
attributeDetails: Partial<ICustomAttribute>;
attributeDetails: ICustomAttribute;
objectId: string;
type: TCustomAttributeTypes;
};
export type FormComponentProps = {
control: Control<Partial<ICustomAttribute>, any>;
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);
export const AttributeForm: React.FC<Props> = observer((props) => {
const { attributeDetails, objectId, type } = props;
const router = useRouter();
const { workspaceSlug } = router.query;
const typeMetaData = CUSTOM_ATTRIBUTES_LIST[type];
const { customAttributes } = useMobxStore();
const {
control,
formState: { isSubmitting },
handleSubmit,
reset,
watch,
} = useForm({ defaultValues: typeMetaData.defaultFormValues });
const handleUpdateAttribute = async (data: Partial<ICustomAttribute>) => {
if (!workspaceSlug || !attributeDetails.id || !objectId) return;
@ -110,80 +46,96 @@ export const AttributeForm: React.FC<Props> = observer(({ attributeDetails, obje
const handleDeleteAttribute = async () => {
if (!workspaceSlug || !attributeDetails.id || !objectId) return;
setIsRemoving(true);
await customAttributes
.deleteObjectAttribute(workspaceSlug.toString(), objectId, attributeDetails.id)
.finally(() => setIsRemoving(false));
await customAttributes.deleteObjectAttribute(
workspaceSlug.toString(),
objectId,
attributeDetails.id
);
};
useEffect(() => {
if (!attributeDetails) return;
reset({
...typeMetaData.defaultFormValues,
...attributeDetails,
});
}, [attributeDetails, reset, typeMetaData.defaultFormValues]);
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">
{attributeDetails.type && (
<RenderForm
type={attributeDetails.type}
control={control}
objectId={objectId}
watch={watch}
/>
)}
<div className="mt-8 flex items-center justify-between">
<div className="flex-shrink-0 flex items-center gap-2">
{!OPTIONAL_FIELDS.includes(attributeDetails.type ?? "") && (
<>
<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={handleDeleteAttribute}
loading={isRemoving}
>
{isRemoving ? "Removing..." : "Remove"}
</SecondaryButton>
<PrimaryButton type="submit" loading={isSubmitting}>
{isSubmitting ? "Saving..." : "Save"}
</PrimaryButton>
</div>
</div>
</form>
</Disclosure.Panel>
</>
)}
</Disclosure>
);
switch (type) {
case "checkbox":
return (
<CheckboxAttributeForm
attributeDetails={attributeDetails}
handleDeleteAttribute={handleDeleteAttribute}
handleUpdateAttribute={handleUpdateAttribute}
/>
);
case "datetime":
return (
<DateTimeAttributeForm
attributeDetails={attributeDetails}
handleDeleteAttribute={handleDeleteAttribute}
handleUpdateAttribute={handleUpdateAttribute}
/>
);
case "email":
return (
<EmailAttributeForm
attributeDetails={attributeDetails}
handleDeleteAttribute={handleDeleteAttribute}
handleUpdateAttribute={handleUpdateAttribute}
/>
);
case "file":
return (
<FileAttributeForm
attributeDetails={attributeDetails}
handleDeleteAttribute={handleDeleteAttribute}
handleUpdateAttribute={handleUpdateAttribute}
/>
);
case "multi_select":
return (
<SelectAttributeForm
attributeDetails={attributeDetails}
handleDeleteAttribute={handleDeleteAttribute}
handleUpdateAttribute={handleUpdateAttribute}
multiple
/>
);
case "number":
return (
<NumberAttributeForm
attributeDetails={attributeDetails}
handleDeleteAttribute={handleDeleteAttribute}
handleUpdateAttribute={handleUpdateAttribute}
/>
);
case "relation":
return (
<RelationAttributeForm
attributeDetails={attributeDetails}
handleDeleteAttribute={handleDeleteAttribute}
handleUpdateAttribute={handleUpdateAttribute}
/>
);
case "select":
return (
<SelectAttributeForm
attributeDetails={attributeDetails}
handleDeleteAttribute={handleDeleteAttribute}
handleUpdateAttribute={handleUpdateAttribute}
/>
);
case "text":
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 { Controller, useForm } from "react-hook-form";
import { Disclosure } from "@headlessui/react";
// react-hook-form
import { Controller } from "react-hook-form";
// components
import { FormComponentProps, Input } from "components/custom-attributes";
import { Input } from "components/custom-attributes";
// ui
import { PrimaryButton, SecondaryButton } from "components/ui";
// icons
import { CheckCircle2 } from "lucide-react";
import { CheckCircle2, ChevronDown } from "lucide-react";
// assets
import CheckRepresentation from "public/custom-attributes/checkbox/check.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 = [
{
@ -23,91 +36,141 @@ const checkboxAttributeRepresentations = [
},
];
export const CheckboxAttributeForm: React.FC<FormComponentProps> = ({ control }) => (
<>
<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>
const typeMetaData = CUSTOM_ATTRIBUTES_LIST.checkbox;
<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>
export const CheckboxAttributeForm: 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>
</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 } }) => (
<>
{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 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">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">
<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>
<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 } }) => (
<>
{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 { Controller } from "react-hook-form";
// components
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";
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { Disclosure } from "@headlessui/react";
export const DateTimeAttributeForm: React.FC<FormComponentProps> = ({ control, watch }) => (
<div className="space-y-3">
<Controller
control={control}
name="display_name"
render={({ field: { onChange, value } }) => (
<Input placeholder="Enter field title" value={value} onChange={onChange} />
// components
import { Input } from "components/custom-attributes";
// ui
import { CustomSelect, PrimaryButton, SecondaryButton, ToggleSwitch, Tooltip } from "components/ui";
// icons
import { ChevronDown } from "lucide-react";
// 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>
</>
)}
/>
<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>
);
</Disclosure>
);
};

View File

@ -1,28 +1,106 @@
// react-hook-form
import { Controller } from "react-hook-form";
// ui
import { FormComponentProps, Input } from "components/custom-attributes";
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { Disclosure } from "@headlessui/react";
export const EmailAttributeForm: React.FC<FormComponentProps> = ({ control }) => (
<div className="space-y-3">
<Controller
control={control}
name="display_name"
render={({ field: { onChange, value } }) => (
<Input placeholder="Enter field title" value={value} onChange={onChange} />
// components
import { Input } from "components/custom-attributes";
// ui
import { PrimaryButton, SecondaryButton, ToggleSwitch } from "components/ui";
// icons
import { ChevronDown } from "lucide-react";
// 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>
</>
)}
/>
<Controller
control={control}
name="default_value"
render={({ field: { onChange, value } }) => (
<Input
type="email"
placeholder="Enter default email"
value={value?.toString()}
onChange={onChange}
/>
)}
/>
</div>
);
</Disclosure>
);
};

View File

@ -1,23 +1,91 @@
// react-hook-form
import { Controller } from "react-hook-form";
// components
import { FileFormatsDropdown, FormComponentProps, Input } from "components/custom-attributes";
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { Disclosure } from "@headlessui/react";
export const FileAttributeForm: React.FC<FormComponentProps> = ({ control }) => (
<div className="space-y-3">
<Controller
control={control}
name="display_name"
render={({ field: { onChange, value } }) => (
<Input placeholder="Enter field title" value={value} onChange={onChange} />
// components
import { FileFormatsDropdown, Input } from "components/custom-attributes";
// ui
import { PrimaryButton, SecondaryButton } from "components/ui";
// icons
import { ChevronDown } from "lucide-react";
// 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>
</>
)}
/>
<Controller
control={control}
name="extra_settings.file_formats"
render={({ field: { onChange, value } }) => (
<FileFormatsDropdown value={value} onChange={onChange} />
)}
/>
</div>
);
</Disclosure>
);
};

View File

@ -1,18 +1,28 @@
import { useState } from "react";
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
import { ColorPicker, FormComponentProps } from "components/custom-attributes";
import { ColorPicker, Input } from "components/custom-attributes";
// ui
import { ToggleSwitch } from "components/ui";
import { Input } from "../input";
import { PrimaryButton, SecondaryButton, ToggleSwitch } from "components/ui";
// icons
import { CheckCircle2 } from "lucide-react";
import { CheckCircle2, ChevronDown } from "lucide-react";
// assets
import NumericalRepresentation from "public/custom-attributes/number/numerical.svg";
import BarRepresentation from "public/custom-attributes/number/bar.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 = [
{
@ -32,120 +42,185 @@ const numberAttributeRepresentations = [
},
];
export const NumberAttributeForm: React.FC<FormComponentProps> = ({ control, watch }) => (
<>
<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="number"
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"
/>
const typeMetaData = CUSTOM_ATTRIBUTES_LIST.number;
export const NumberAttributeForm: 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} />
)}
/>
<Controller
control={control}
name="default_value"
render={({ field: { onChange, value } }) => (
<Input
type="number"
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>
</>
<>
<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 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,42 +1,90 @@
// react-hook-form
import { Controller } from "react-hook-form";
// components
import { FormComponentProps, Input } from "components/custom-attributes";
// ui
import { CustomSelect } from "components/ui";
// constants
import { CUSTOM_ATTRIBUTE_UNITS } from "constants/custom-attributes";
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { Disclosure } from "@headlessui/react";
export const RelationAttributeForm: React.FC<FormComponentProps> = ({ control }) => (
<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>
// components
import { Input } from "components/custom-attributes";
// ui
import { CustomSelect, PrimaryButton, SecondaryButton, ToggleSwitch } from "components/ui";
// icons
import { ChevronDown } from "lucide-react";
// types
import { ICustomAttribute } from "types";
// constants
import { CUSTOM_ATTRIBUTES_LIST, CUSTOM_ATTRIBUTE_UNITS } from "constants/custom-attributes";
type Props = {
attributeDetails: ICustomAttribute;
handleDeleteAttribute: () => Promise<void>;
handleUpdateAttribute: (data: Partial<ICustomAttribute>) => Promise<void>;
};
const typeMetaData = CUSTOM_ATTRIBUTES_LIST.relation;
export const RelationAttributeForm: 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="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>
<div className="mt-2 flex items-center gap-6 accent-custom-primary-100">
<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 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";
// mobx
import React, { useEffect, useState } from "react";
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";
// react-hook-form
import { Controller } from "react-hook-form";
// 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
import { ICustomAttribute } from "types";
// constants
import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
export const SelectAttributeForm: React.FC<FormComponentProps & { multiple?: boolean }> = observer(
({ control, multiple = false, objectId = "", watch }) => {
const [optionToEdit, setOptionToEdit] = useState<ICustomAttribute | null>(null);
type Props = {
attributeDetails: ICustomAttribute;
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 (
<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={objectId}
option={option}
/>
))}
</div>
<div className="mt-2 w-3/5">
<OptionForm
data={optionToEdit}
objectId={objectId}
onSubmit={() => setOptionToEdit(null)}
parentId={watch("id") ?? ""}
/>
</div>
</div>
</div>
);
}
);
const [optionToEdit, setOptionToEdit] = useState<ICustomAttribute | null>(null);
const [isRemoving, setIsRemoving] = useState(false);
const { customAttributes } = useMobxStore();
const {
control,
formState: { isSubmitting },
handleSubmit,
reset,
watch,
} = useForm({ defaultValues: typeMetaData.defaultFormValues });
const options =
customAttributes.objectAttributes?.[attributeDetails.parent ?? ""]?.[watch("id") ?? ""]
?.children;
const handleDelete = async () => {
setIsRemoving(true);
await handleDeleteAttribute().finally(() => setIsRemoving(false));
};
useEffect(() => {
if (!attributeDetails) return;
reset({
...typeMetaData.defaultFormValues,
...attributeDetails,
});
}, [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 { Controller } from "react-hook-form";
// ui
import { FormComponentProps, Input } from "components/custom-attributes";
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { Disclosure } from "@headlessui/react";
export const TextAttributeForm: React.FC<FormComponentProps> = ({ control }) => (
<div className="space-y-3">
<Controller
control={control}
name="display_name"
render={({ field: { onChange, value } }) => (
<Input placeholder="Enter field title" value={value} onChange={onChange} />
// components
import { Input } from "components/custom-attributes";
// ui
import { PrimaryButton, SecondaryButton, ToggleSwitch } from "components/ui";
// icons
import { ChevronDown } from "lucide-react";
// 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>
</>
)}
/>
<Controller
control={control}
name="default_value"
render={({ field: { onChange, value } }) => (
<Input placeholder="Enter default value" value={value ?? ""} onChange={onChange} />
)}
/>
</div>
);
</Disclosure>
);
};

View File

@ -1,23 +1,106 @@
// react-hook-form
import { Controller } from "react-hook-form";
// components
import { FormComponentProps, Input } from "components/custom-attributes";
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { Disclosure } from "@headlessui/react";
export const UrlAttributeForm: React.FC<FormComponentProps> = ({ control }) => (
<div className="space-y-3">
<Controller
control={control}
name="display_name"
render={({ field: { onChange, value } }) => (
<Input placeholder="Enter field title" value={value} onChange={onChange} />
// components
import { Input } from "components/custom-attributes";
// ui
import { PrimaryButton, SecondaryButton, ToggleSwitch } from "components/ui";
// icons
import { ChevronDown } from "lucide-react";
// 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>
</>
)}
/>
<Controller
control={control}
name="default_value"
render={({ field: { onChange, value } }) => (
<Input type="url" placeholder="Enter default URL" value={value ?? ""} onChange={onChange} />
)}
/>
</div>
);
</Disclosure>
);
};

View File

@ -23,6 +23,8 @@ export const CustomAttributesCheckboxes: React.FC<Props> = observer((props) => {
const checkboxFields = Object.values(attributes).filter((a) => a.type === "checkbox");
if (checkboxFields.length === 0) return null;
return (
<div className="space-y-4">
{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)
);
if (descriptionFields.length === 0) return null;
return (
<Disclosure as="div" defaultOpen>
{({ 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");
if (fileUploadFields.length === 0) return null;
return (
<>
{customAttributes.fetchObjectDetailsLoader ? (

View File

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

View File

@ -1,38 +1,26 @@
import { useEffect, useState } from "react";
import { useEffect } from "react";
import { useRouter } from "next/router";
// mobx
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { DeleteObjectModal, SingleObject } from "components/custom-attributes";
import { SingleObject } from "components/custom-attributes";
// ui
import { EmptyState, Loader } from "components/ui";
// assets
import emptyCustomObjects from "public/empty-state/custom-objects.svg";
// types
import { ICustomAttribute } from "types";
type Props = {
handleEditObject: (object: ICustomAttribute) => void;
projectId: string;
};
export const ObjectsList: React.FC<Props> = observer(({ handleEditObject, projectId }) => {
const [deleteObjectModal, setDeleteObjectModal] = useState(false);
const [objectToDelete, setObjectToDelete] = useState<ICustomAttribute | null>(null);
export const ObjectsList: React.FC<Props> = observer(({ projectId }) => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { customAttributes } = useMobxStore();
const handleDeleteObject = async (object: ICustomAttribute) => {
setObjectToDelete(object);
setDeleteObjectModal(true);
};
useEffect(() => {
if (!workspaceSlug) return;
@ -41,47 +29,27 @@ export const ObjectsList: React.FC<Props> = observer(({ handleEditObject, projec
}, [customAttributes, projectId, workspaceSlug]);
return (
<>
<DeleteObjectModal
isOpen={deleteObjectModal}
objectToDelete={objectToDelete}
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>
)
<div className="divide-y divide-custom-border-100">
{customAttributes.objects ? (
customAttributes.objects.length > 0 ? (
customAttributes.objects.map((object) => <SingleObject key={object.id} object={object} />)
) : (
<Loader className="space-y-4">
<Loader.Item height="60px" />
<Loader.Item height="60px" />
<Loader.Item height="60px" />
</Loader>
)}
</div>
</>
<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">
<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
import { CustomMenu } from "components/ui";
// icons
@ -9,32 +13,49 @@ import { ICustomAttribute } from "types";
type Props = {
object: ICustomAttribute;
handleDeleteObject: () => void;
handleEditObject: () => void;
};
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 (
<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>
<>
<ObjectModal
data={object}
isOpen={isEditObjectModalOpen}
onClose={() => setIsEditObjectModalOpen(false)}
/>
<DeleteObjectModal
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>
<CustomMenu ellipsis>
<CustomMenu.MenuItem renderAs="button" onClick={() => setIsEditObjectModalOpen(true)}>
Edit
</CustomMenu.MenuItem>
<CustomMenu.MenuItem renderAs="button" onClick={() => setIsDeleteObjectModalOpen(true)}>
Delete
</CustomMenu.MenuItem>
</CustomMenu>
</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 { useRouter } from "next/router";
// layouts
@ -16,22 +15,15 @@ import { PrimaryButton } from "components/ui";
import { truncateText } from "helpers/string.helper";
// types
import type { NextPage } from "next";
import { ICustomAttribute } from "types";
const CustomObjectSettings: NextPage = () => {
const [isCreateObjectModalOpen, setIsCreateObjectModalOpen] = useState(false);
const [objectToEdit, setObjectToEdit] = useState<ICustomAttribute | null>(null);
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { projectDetails } = useProjectDetails();
const handleEditObject = (object: ICustomAttribute) => {
setObjectToEdit(object);
setIsCreateObjectModalOpen(true);
};
return (
<ProjectAuthorizationWrapper
breadcrumbs={
@ -46,12 +38,8 @@ const CustomObjectSettings: NextPage = () => {
}
>
<ObjectModal
objectIdToEdit={objectToEdit?.id}
isOpen={isCreateObjectModalOpen}
onClose={() => {
setIsCreateObjectModalOpen(false);
setObjectToEdit(null);
}}
onClose={() => setIsCreateObjectModalOpen(false)}
/>
<div className="flex flex-row gap-2">
<div className="w-80 py-8">
@ -66,10 +54,7 @@ const CustomObjectSettings: NextPage = () => {
</div>
<div>
<div className="mt-4">
<ObjectsList
handleEditObject={handleEditObject}
projectId={projectId?.toString() ?? ""}
/>
<ObjectsList projectId={projectId?.toString() ?? ""} />
</div>
</div>
</section>