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":
reset({
...typeMetaData.defaultFormValues,
...attributeDetails,
});
}, [attributeDetails, reset, typeMetaData.defaultFormValues]);
return ( return (
<Disclosure <CheckboxAttributeForm
as="div" attributeDetails={attributeDetails}
className="bg-custom-background-90 border border-custom-border-200 rounded" handleDeleteAttribute={handleDeleteAttribute}
> handleUpdateAttribute={handleUpdateAttribute}
{({ 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>
); );
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 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,8 +36,43 @@ const checkboxAttributeRepresentations = [
}, },
]; ];
export const CheckboxAttributeForm: React.FC<FormComponentProps> = ({ control }) => ( const typeMetaData = CUSTOM_ATTRIBUTES_LIST.checkbox;
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 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"> <div className="space-y-3">
<Controller <Controller
control={control} control={control}
@ -109,5 +157,20 @@ export const CheckboxAttributeForm: React.FC<FormComponentProps> = ({ control })
/> />
</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,13 +1,62 @@
// 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
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"> <div className="space-y-3">
<Controller <Controller
control={control} control={control}
@ -23,7 +72,9 @@ export const DateTimeAttributeForm: React.FC<FormComponentProps> = ({ control, w
render={({ field: { onChange, value } }) => ( render={({ field: { onChange, value } }) => (
<CustomSelect <CustomSelect
label={ label={
<span className="text-xs">{DATE_FORMATS.find((f) => f.value === value)?.label}</span> <span className="text-xs">
{DATE_FORMATS.find((f) => f.value === value)?.label}
</span>
} }
value={value} value={value}
onChange={onChange} onChange={onChange}
@ -69,7 +120,9 @@ export const DateTimeAttributeForm: React.FC<FormComponentProps> = ({ control, w
render={({ field: { onChange, value } }) => ( render={({ field: { onChange, value } }) => (
<CustomSelect <CustomSelect
label={ label={
<span className="text-xs">{TIME_FORMATS.find((f) => f.value === value)?.label}</span> <span className="text-xs">
{TIME_FORMATS.find((f) => f.value === value)?.label}
</span>
} }
value={value} value={value}
onChange={onChange} onChange={onChange}
@ -109,4 +162,30 @@ export const DateTimeAttributeForm: React.FC<FormComponentProps> = ({ control, w
/> />
</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,9 +1,61 @@
// 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
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"> <div className="space-y-3">
<Controller <Controller
control={control} control={control}
@ -25,4 +77,30 @@ export const EmailAttributeForm: React.FC<FormComponentProps> = ({ control }) =>
)} )}
/> />
</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,9 +1,61 @@
// 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
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"> <div className="space-y-3">
<Controller <Controller
control={control} control={control}
@ -20,4 +72,20 @@ export const FileAttributeForm: React.FC<FormComponentProps> = ({ control }) =>
)} )}
/> />
</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,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,8 +42,44 @@ const numberAttributeRepresentations = [
}, },
]; ];
export const NumberAttributeForm: React.FC<FormComponentProps> = ({ control, watch }) => ( 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"> <div className="space-y-3">
<Controller <Controller
control={control} control={control}
@ -128,7 +174,11 @@ export const NumberAttributeForm: React.FC<FormComponentProps> = ({ control, wat
control={control} control={control}
name="color" name="color"
render={({ field: { onChange, value } }) => ( render={({ field: { onChange, value } }) => (
<ColorPicker onChange={onChange} selectedColor={value ?? "#000000"} size={18} /> <ColorPicker
onChange={onChange}
selectedColor={value ?? "#000000"}
size={18}
/>
)} )}
/> />
</div> </div>
@ -147,5 +197,30 @@ export const NumberAttributeForm: React.FC<FormComponentProps> = ({ control, wat
</> </>
</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,13 +1,61 @@
// 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
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"> <div className="space-y-3">
<Controller <Controller
control={control} control={control}
@ -58,4 +106,30 @@ export const RelationAttributeForm: React.FC<FormComponentProps> = ({ control })
</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,24 +1,87 @@
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";
type Props = {
attributeDetails: ICustomAttribute;
handleDeleteAttribute: () => Promise<void>;
handleUpdateAttribute: (data: Partial<ICustomAttribute>) => Promise<void>;
};
const typeMetaData = CUSTOM_ATTRIBUTES_LIST.select;
export const SelectAttributeForm: React.FC<Props & { multiple?: boolean }> = observer((props) => {
const {
attributeDetails,
handleDeleteAttribute,
handleUpdateAttribute,
multiple = false,
} = props;
export const SelectAttributeForm: React.FC<FormComponentProps & { multiple?: boolean }> = observer(
({ control, multiple = false, objectId = "", watch }) => {
const [optionToEdit, setOptionToEdit] = useState<ICustomAttribute | null>(null); const [optionToEdit, setOptionToEdit] = useState<ICustomAttribute | null>(null);
const [isRemoving, setIsRemoving] = useState(false);
const { customAttributes } = useMobxStore(); const { customAttributes } = useMobxStore();
const options = customAttributes.objectAttributes?.[objectId]?.[watch("id") ?? ""]?.children; 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 ( 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"> <div className="space-y-3">
<Controller <Controller
control={control} control={control}
@ -34,7 +97,7 @@ export const SelectAttributeForm: React.FC<FormComponentProps & { multiple?: boo
<SelectOption <SelectOption
key={option.id} key={option.id}
handleEditOption={() => setOptionToEdit(option)} handleEditOption={() => setOptionToEdit(option)}
objectId={objectId} objectId={attributeDetails.parent ?? ""}
option={option} option={option}
/> />
))} ))}
@ -42,13 +105,38 @@ export const SelectAttributeForm: React.FC<FormComponentProps & { multiple?: boo
<div className="mt-2 w-3/5"> <div className="mt-2 w-3/5">
<OptionForm <OptionForm
data={optionToEdit} data={optionToEdit}
objectId={objectId} objectId={attributeDetails.parent ?? ""}
onSubmit={() => setOptionToEdit(null)} onSubmit={() => setOptionToEdit(null)}
parentId={watch("id") ?? ""} parentId={watch("id") ?? ""}
/> />
</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,9 +1,61 @@
// 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
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"> <div className="space-y-3">
<Controller <Controller
control={control} control={control}
@ -16,8 +68,38 @@ export const TextAttributeForm: React.FC<FormComponentProps> = ({ control }) =>
control={control} control={control}
name="default_value" name="default_value"
render={({ field: { onChange, value } }) => ( render={({ field: { onChange, value } }) => (
<Input placeholder="Enter default value" value={value ?? ""} onChange={onChange} /> <Input
placeholder="Enter default value"
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,9 +1,61 @@
// 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
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"> <div className="space-y-3">
<Controller <Controller
control={control} control={control}
@ -16,8 +68,39 @@ export const UrlAttributeForm: React.FC<FormComponentProps> = ({ control }) => (
control={control} control={control}
name="default_value" name="default_value"
render={({ field: { onChange, value } }) => ( render={({ field: { onChange, value } }) => (
<Input type="url" placeholder="Enter default URL" value={value ?? ""} onChange={onChange} /> <Input
type="url"
placeholder="Enter default URL"
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

@ -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,92 +1,100 @@
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 }) => {
const [object, setObject] = useState<Partial<ICustomAttribute>>({
display_name: "", display_name: "",
description: "", description: "",
}); icon: "",
const [isCreatingObject, setIsCreatingObject] = useState(false); };
const [isUpdatingObject, setIsUpdatingObject] = useState(false);
export const ObjectModal: React.FC<Props> = observer((props) => {
const { data, isOpen, onClose, onSubmit } = props;
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; 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 { customAttributes } = useMobxStore();
const handleClose = () => { const handleClose = () => {
onClose(); onClose();
setTimeout(() => { setTimeout(() => {
setObject({ display_name: "", description: "" }); reset({ ...defaultValues });
}, 300); }, 300);
}; };
const handleCreateObject = async () => { const handleCreateObject = async (formData: Partial<ICustomAttribute>) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
setIsCreatingObject(true);
const payload: Partial<ICustomAttribute> = { const payload: Partial<ICustomAttribute> = {
description: object.description ?? "", description: formData.description ?? "",
display_name: object.display_name ?? "", display_name: formData.display_name ?? "",
icon: object.icon ?? "", icon: formData.icon ?? "",
project: projectId.toString(), project: projectId.toString(),
type: "entity", type: "entity",
}; };
await customAttributes await customAttributes
.createObject(workspaceSlug.toString(), payload) .createObject(workspaceSlug.toString(), payload)
.then((res) => { .then((res) => setValue("id", res?.id ?? ""));
setObject((prevData) => ({ ...prevData, ...res }));
if (onSubmit) onSubmit();
})
.finally(() => setIsCreatingObject(false));
}; };
const handleUpdateObject = async () => { const handleUpdateObject = async (formData: Partial<ICustomAttribute>) => {
if (!workspaceSlug || !object || !object.id) return; if (!workspaceSlug || !data || !data.id) return;
setIsUpdatingObject(true);
const payload: Partial<ICustomAttribute> = { const payload: Partial<ICustomAttribute> = {
description: object.description ?? "", description: formData.description ?? "",
display_name: object.display_name ?? "", display_name: formData.display_name ?? "",
icon: object.icon ?? "", icon: formData.icon ?? "",
}; };
await customAttributes await customAttributes.updateObject(workspaceSlug.toString(), data.id, payload);
.updateObject(workspaceSlug.toString(), object.id, payload) };
.finally(() => setIsUpdatingObject(false));
const handleObjectFormSubmit = async (formData: Partial<ICustomAttribute>) => {
if (data) await handleUpdateObject(formData);
else await handleCreateObject(formData);
if (onSubmit) onSubmit();
}; };
const handleCreateObjectAttribute = async (type: TCustomAttributeTypes) => { const handleCreateObjectAttribute = async (type: TCustomAttributeTypes) => {
if (!workspaceSlug || !object || !object.id) return; if (!workspaceSlug || !objectId) return;
const typeMetaData = CUSTOM_ATTRIBUTES_LIST[type]; const typeMetaData = CUSTOM_ATTRIBUTES_LIST[type];
@ -98,37 +106,36 @@ export const ObjectModal: React.FC<Props> = observer(
await customAttributes.createObjectAttribute(workspaceSlug.toString(), { await customAttributes.createObjectAttribute(workspaceSlug.toString(), {
...payload, ...payload,
parent: object.id, parent: objectId,
}); });
}; };
// fetch the object details if object state has id // fetch the object details if object state has id
useEffect(() => { useEffect(() => {
if (!object.id || object.id === "") return; if (!objectId) return;
if (!customAttributes.objectAttributes[object.id]) { if (!customAttributes.objectAttributes[objectId]) {
if (!workspaceSlug) return; if (!workspaceSlug) return;
customAttributes.fetchObjectDetails(workspaceSlug.toString(), object.id).then((res) => { customAttributes.fetchObjectDetails(workspaceSlug.toString(), objectId).then((res) => {
setObject((prev) => ({ ...prev, ...res })); reset({ ...res });
}); });
} else { } else {
setObject((prev) => ({ reset({
...prev, ...customAttributes.objects?.find((e) => e.id === objectId),
...customAttributes.objects?.find((e) => e.id === object.id), });
}));
} }
}, [customAttributes, object.id, workspaceSlug]); }, [customAttributes, objectId, reset, workspaceSlug]);
// update the object state if objectIdToEdit is present // update the form if data is present
useEffect(() => { useEffect(() => {
if (!objectIdToEdit) return; if (!data) return;
setObject((prev) => ({ reset({
...prev, ...defaultValues,
id: objectIdToEdit, ...data,
})); });
}, [objectIdToEdit]); }, [data, reset]);
return ( return (
<Transition.Root show={isOpen} as={React.Fragment}> <Transition.Root show={isOpen} as={React.Fragment}>
@ -159,27 +166,43 @@ export const ObjectModal: React.FC<Props> = observer(
<div className="bg-custom-background-100 w-1/2 max-h-[85%] flex flex-col rounded-xl"> <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> <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="mt-5 space-y-5 h-full overflow-y-auto">
<div className="space-y-4 px-6"> <form
onSubmit={handleSubmit(handleObjectFormSubmit)}
className="space-y-4 px-6 pb-5"
>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="h-9 w-9 bg-custom-background-80 grid place-items-center rounded"> <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 <EmojiIconPicker
label={object.icon ? renderEmoji(object.icon) : "Icon"} label={value ? renderEmoji(value) : "Icon"}
onChange={(icon) => { onChange={(icon) => {
if (typeof icon === "string") if (typeof icon === "string") onChange(icon);
setObject((prevData) => ({ ...prevData, icon }));
}} }}
value={object.icon} value={value}
showIconPicker={false} showIconPicker={false}
/> />
</div> )}
<Input
placeholder="Enter Object Title"
value={object.display_name}
onChange={(e) =>
setObject((prevData) => ({ ...prevData, display_name: e.target.value }))
}
/> />
</div> </div>
<Controller
control={control}
name="display_name"
render={({ field: { onChange, value } }) => (
<Input
placeholder="Enter Object Title"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
)}
/>
</div>
<Controller
control={control}
name="description"
render={({ field: { onChange, value } }) => (
<textarea <textarea
name="objectDescription" name="objectDescription"
id="objectDescription" id="objectDescription"
@ -187,20 +210,30 @@ export const ObjectModal: React.FC<Props> = observer(
cols={30} cols={30}
rows={5} rows={5}
placeholder="Enter Object Description" placeholder="Enter Object Description"
value={object.description} value={value}
onChange={(e) => onChange={(e) => onChange(e.target.value)}
setObject((prevData) => ({ ...prevData, description: e.target.value }))
}
/> />
{object.id && ( )}
<div className="text-right"> />
<PrimaryButton onClick={handleUpdateObject} loading={isUpdatingObject}> <div className="flex items-center justify-end gap-3">
{isUpdatingObject ? "Saving..." : "Save changes"} <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> </PrimaryButton>
</div> </div>
)}
</div> </div>
{object.id && ( </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,26 +265,14 @@ 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 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"> <div className="flex-shrink-0">
<TypesDropdown onClick={handleCreateObjectAttribute} /> <TypesDropdown onClick={handleCreateObjectAttribute} />
</div> </div>
)}
<div className="flex items-center gap-3">
<SecondaryButton onClick={handleClose}>Close</SecondaryButton> <SecondaryButton onClick={handleClose}>Close</SecondaryButton>
{!object.id && (
<PrimaryButton onClick={handleCreateObject} loading={isCreatingObject}>
{isCreatingObject ? "Creating..." : "Create Object"}
</PrimaryButton>
)}
</div> </div>
</>
)}
</div> </div>
</div> </div>
</div> </div>
@ -260,5 +281,4 @@ export const ObjectModal: React.FC<Props> = observer(
</Dialog> </Dialog>
</Transition.Root> </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,29 +29,10 @@ export const ObjectsList: React.FC<Props> = observer(({ handleEditObject, projec
}, [customAttributes, projectId, workspaceSlug]); }, [customAttributes, projectId, workspaceSlug]);
return ( return (
<>
<DeleteObjectModal
isOpen={deleteObjectModal}
objectToDelete={objectToDelete}
onClose={() => {
setDeleteObjectModal(false);
setTimeout(() => {
setObjectToDelete(null);
}, 300);
}}
/>
<div className="divide-y divide-custom-border-100"> <div className="divide-y divide-custom-border-100">
{customAttributes.objects ? ( {customAttributes.objects ? (
customAttributes.objects.length > 0 ? ( customAttributes.objects.length > 0 ? (
customAttributes.objects.map((object) => ( customAttributes.objects.map((object) => <SingleObject key={object.id} object={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"> <div className="bg-custom-background-90 border border-custom-border-100 rounded max-w-3xl mt-10 mx-auto">
<EmptyState <EmptyState
@ -82,6 +51,5 @@ export const ObjectsList: React.FC<Props> = observer(({ handleEditObject, projec
</Loader> </Loader>
)} )}
</div> </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,18 +13,34 @@ 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 (
<>
<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 items-center justify-between gap-4 py-4">
<div className={`flex gap-4 ${object.description === "" ? "items-center" : "items-start"}`}> <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"> <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} />} {object.icon ? (
renderEmoji(object.icon)
) : (
<TableProperties size={20} strokeWidth={1.5} />
)}
</div> </div>
<div> <div>
<h5 className="text-sm font-medium">{object.display_name}</h5> <h5 className="text-sm font-medium">{object.display_name}</h5>
@ -28,13 +48,14 @@ export const SingleObject: React.FC<Props> = (props) => {
</div> </div>
</div> </div>
<CustomMenu ellipsis> <CustomMenu ellipsis>
<CustomMenu.MenuItem renderAs="button" onClick={handleEditObject}> <CustomMenu.MenuItem renderAs="button" onClick={() => setIsEditObjectModalOpen(true)}>
Edit Edit
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
<CustomMenu.MenuItem renderAs="button" onClick={handleDeleteObject}> <CustomMenu.MenuItem renderAs="button" onClick={() => setIsDeleteObjectModalOpen(true)}>
Delete Delete
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
</CustomMenu> </CustomMenu>
</div> </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>