From 4d158a6d8f73fac8755707eae0fe159632dc4c72 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Mon, 2 Oct 2023 17:29:48 +0530 Subject: [PATCH] refactor: attribute form components and object modal --- .../attribute-forms/attribute-form.tsx | 238 ++++------ .../checkbox-attribute-form.tsx | 239 ++++++---- .../date-time-attribute-form.tsx | 299 +++++++----- .../attribute-forms/email-attribute-form.tsx | 130 ++++-- .../attribute-forms/file-attribute-form.tsx | 110 ++++- .../attribute-forms/number-attribute-form.tsx | 313 ++++++++----- .../relation-attribute-form.tsx | 154 +++++-- .../select-attribute-form.tsx | 178 ++++++-- .../attribute-forms/text-attribute-form.tsx | 124 ++++- .../attribute-forms/url-attribute-form.tsx | 125 +++++- .../issue-modal/checkboxes.tsx | 2 + .../issue-modal/description-fields.tsx | 2 + .../issue-modal/file-uploads.tsx | 2 + .../custom-attributes/object-modal.tsx | 424 +++++++++--------- .../custom-attributes/objects-list.tsx | 84 ++-- .../custom-attributes/single-object.tsx | 61 ++- .../[projectId]/settings/custom-objects.tsx | 19 +- 17 files changed, 1573 insertions(+), 931 deletions(-) diff --git a/web/components/custom-attributes/attribute-forms/attribute-form.tsx b/web/components/custom-attributes/attribute-forms/attribute-form.tsx index 5d5b4aa48..f6d199641 100644 --- a/web/components/custom-attributes/attribute-forms/attribute-form.tsx +++ b/web/components/custom-attributes/attribute-forms/attribute-form.tsx @@ -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; + attributeDetails: ICustomAttribute; objectId: string; type: TCustomAttributeTypes; }; -export type FormComponentProps = { - control: Control, any>; - objectId: string; - watch: UseFormWatch>; -}; - -const RenderForm: React.FC<{ type: TCustomAttributeTypes } & FormComponentProps> = ({ - control, - objectId, - type, - watch, -}) => { - let FormToRender: any = <>; - - if (type === "checkbox") - FormToRender = ; - else if (type === "datetime") - FormToRender = ; - else if (type === "email") - FormToRender = ; - else if (type === "file") - FormToRender = ; - else if (type === "multi_select") - FormToRender = ( - - ); - else if (type === "number") - FormToRender = ; - else if (type === "relation") - FormToRender = ; - else if (type === "select") - FormToRender = ; - else if (type === "text") - FormToRender = ; - else if (type === "url") - FormToRender = ; - - return FormToRender; -}; - -const OPTIONAL_FIELDS = ["checkbox", "file"]; - -export const AttributeForm: React.FC = observer(({ attributeDetails, objectId, type }) => { - const [isRemoving, setIsRemoving] = useState(false); +export const AttributeForm: React.FC = 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) => { if (!workspaceSlug || !attributeDetails.id || !objectId) return; @@ -110,80 +46,96 @@ export const AttributeForm: React.FC = 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 ( - - {({ open }) => ( - <> - -
- -
{attributeDetails.display_name ?? typeMetaData.label}
-
-
- -
-
- -
- {attributeDetails.type && ( - - )} -
-
- {!OPTIONAL_FIELDS.includes(attributeDetails.type ?? "") && ( - <> - ( - - )} - /> - Mandatory field - - )} -
-
- - {isRemoving ? "Removing..." : "Remove"} - - - {isSubmitting ? "Saving..." : "Save"} - -
-
- -
- - )} -
- ); + switch (type) { + case "checkbox": + return ( + + ); + case "datetime": + return ( + + ); + case "email": + return ( + + ); + case "file": + return ( + + ); + case "multi_select": + return ( + + ); + case "number": + return ( + + ); + case "relation": + return ( + + ); + case "select": + return ( + + ); + case "text": + return ( + + ); + case "url": + return ( + + ); + default: + return null; + } }); diff --git a/web/components/custom-attributes/attribute-forms/checkbox-attribute-form.tsx b/web/components/custom-attributes/attribute-forms/checkbox-attribute-form.tsx index aa1bba3f5..45d8d9aef 100644 --- a/web/components/custom-attributes/attribute-forms/checkbox-attribute-form.tsx +++ b/web/components/custom-attributes/attribute-forms/checkbox-attribute-form.tsx @@ -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; + handleUpdateAttribute: (data: Partial) => Promise; +}; const checkboxAttributeRepresentations = [ { @@ -23,91 +36,141 @@ const checkboxAttributeRepresentations = [ }, ]; -export const CheckboxAttributeForm: React.FC = ({ control }) => ( - <> -
- ( - - )} - /> -
-

Default value

- ( -
-
- onChange(e.target.value)} - /> - -
+const typeMetaData = CUSTOM_ATTRIBUTES_LIST.checkbox; -
- onChange(e.target.value)} - /> - -
+export const CheckboxAttributeForm: React.FC = (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 ( + + {({ open }) => ( + <> + +
+ +
{attributeDetails.display_name ?? typeMetaData.label}
- )} - /> -
-
-
-
-
Show as
-
- ( - <> - {checkboxAttributeRepresentations.map((representation) => ( -
onChange(representation.key)} - > -
- {representation.label} -
-
- {representation.label} - {value === representation.key && ( - +
+ +
+ + +
+
+ ( + + )} + /> +
+

Default value

+ ( +
+
+ onChange(e.target.value)} + /> + +
+ +
+ onChange(e.target.value)} + /> + +
+
)} -
+ />
- ))} - - )} - /> -
-
- -); +
+
+
+
Show as
+
+ ( + <> + {checkboxAttributeRepresentations.map((representation) => ( +
onChange(representation.key)} + > +
+ {representation.label} +
+
+ {representation.label} + {value === representation.key && ( + + )} +
+
+ ))} + + )} + /> +
+
+
+
+ + {isRemoving ? "Removing..." : "Remove"} + + + {isSubmitting ? "Saving..." : "Save"} + +
+
+ + + + )} + + ); +}; diff --git a/web/components/custom-attributes/attribute-forms/date-time-attribute-form.tsx b/web/components/custom-attributes/attribute-forms/date-time-attribute-form.tsx index 72dfd5f62..ae177c6a5 100644 --- a/web/components/custom-attributes/attribute-forms/date-time-attribute-form.tsx +++ b/web/components/custom-attributes/attribute-forms/date-time-attribute-form.tsx @@ -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 = ({ 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; + handleUpdateAttribute: (data: Partial) => Promise; +}; + +const typeMetaData = CUSTOM_ATTRIBUTES_LIST.datetime; + +export const DateTimeAttributeForm: React.FC = (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 ( + + {({ open }) => ( + <> + +
+ +
{attributeDetails.display_name ?? typeMetaData.label}
+
+
+ +
+
+ +
+
+ ( + + )} + /> +
+ ( + + {DATE_FORMATS.find((f) => f.value === value)?.label} + + } + 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) => ( + + {format.label} + + ))} + + )} + /> + ( +
+ +
+ + Don{"'"}t show date +
+
+
+ )} + /> +
+
+ ( + + {TIME_FORMATS.find((f) => f.value === value)?.label} + + } + 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) => ( + + {format.label} + + ))} + + )} + /> + ( +
+ +
+ + Don{"'"}t show time +
+
+
+ )} + /> +
+
+
+
+ ( + + )} + /> + Mandatory field +
+
+ + {isRemoving ? "Removing..." : "Remove"} + + + {isSubmitting ? "Saving..." : "Save"} + +
+
+
+
+ )} - /> -
- ( - {DATE_FORMATS.find((f) => f.value === value)?.label} - } - 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) => ( - - {format.label} - - ))} - - )} - /> - ( -
- -
- - Don{"'"}t show date -
-
-
- )} - /> -
-
- ( - {TIME_FORMATS.find((f) => f.value === value)?.label} - } - 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) => ( - - {format.label} - - ))} - - )} - /> - ( -
- -
- - Don{"'"}t show time -
-
-
- )} - /> -
-
-); + + ); +}; diff --git a/web/components/custom-attributes/attribute-forms/email-attribute-form.tsx b/web/components/custom-attributes/attribute-forms/email-attribute-form.tsx index 17db51f41..efe975689 100644 --- a/web/components/custom-attributes/attribute-forms/email-attribute-form.tsx +++ b/web/components/custom-attributes/attribute-forms/email-attribute-form.tsx @@ -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 = ({ 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; + handleUpdateAttribute: (data: Partial) => Promise; +}; + +const typeMetaData = CUSTOM_ATTRIBUTES_LIST.email; + +export const EmailAttributeForm: React.FC = (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 ( + + {({ open }) => ( + <> + +
+ +
{attributeDetails.display_name ?? typeMetaData.label}
+
+
+ +
+
+ +
+
+ ( + + )} + /> + ( + + )} + /> +
+
+
+ ( + + )} + /> + Mandatory field +
+
+ + {isRemoving ? "Removing..." : "Remove"} + + + {isSubmitting ? "Saving..." : "Save"} + +
+
+
+
+ )} - /> - ( - - )} - /> -
-); + + ); +}; diff --git a/web/components/custom-attributes/attribute-forms/file-attribute-form.tsx b/web/components/custom-attributes/attribute-forms/file-attribute-form.tsx index e2d60cbcf..9e27e7e0c 100644 --- a/web/components/custom-attributes/attribute-forms/file-attribute-form.tsx +++ b/web/components/custom-attributes/attribute-forms/file-attribute-form.tsx @@ -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 = ({ 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; + handleUpdateAttribute: (data: Partial) => Promise; +}; + +const typeMetaData = CUSTOM_ATTRIBUTES_LIST.file; + +export const FileAttributeForm: React.FC = (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 ( + + {({ open }) => ( + <> + +
+ +
{attributeDetails.display_name ?? typeMetaData.label}
+
+
+ +
+
+ +
+
+ ( + + )} + /> + ( + + )} + /> +
+
+
+ + {isRemoving ? "Removing..." : "Remove"} + + + {isSubmitting ? "Saving..." : "Save"} + +
+
+
+
+ )} - /> - ( - - )} - /> -
-); + + ); +}; diff --git a/web/components/custom-attributes/attribute-forms/number-attribute-form.tsx b/web/components/custom-attributes/attribute-forms/number-attribute-form.tsx index 49d93961f..9deef8d82 100644 --- a/web/components/custom-attributes/attribute-forms/number-attribute-form.tsx +++ b/web/components/custom-attributes/attribute-forms/number-attribute-form.tsx @@ -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; + handleUpdateAttribute: (data: Partial) => Promise; +}; const numberAttributeRepresentations = [ { @@ -32,120 +42,185 @@ const numberAttributeRepresentations = [ }, ]; -export const NumberAttributeForm: React.FC = ({ control, watch }) => ( - <> -
- ( - - )} - /> - ( - - )} - /> -
-
-
-
Show as
-
- ( - <> - {numberAttributeRepresentations.map((representation) => ( -
onChange(representation.key)} - > -
- {representation.label} -
-
- {representation.label} - {value === representation.key && ( - +const typeMetaData = CUSTOM_ATTRIBUTES_LIST.number; + +export const NumberAttributeForm: React.FC = (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 ( + + {({ open }) => ( + <> + +
+ +
{attributeDetails.display_name ?? typeMetaData.label}
+
+
+ +
+
+ +
+
+ ( + + )} + /> + ( + + )} + /> +
+
+
+
Show as
+
+ ( + <> + {numberAttributeRepresentations.map((representation) => ( +
onChange(representation.key)} + > +
+ {representation.label} +
+
+ {representation.label} + {value === representation.key && ( + + )} +
+
+ ))} + )} -
-
- ))} - - )} - /> -
-
- {watch && - (watch("extra_settings.representation") === "bar" || - watch("extra_settings.representation") === "ring") && ( -
- <> -
Divided by
-
- ( - +
+
+ {watch && + (watch("extra_settings.representation") === "bar" || + watch("extra_settings.representation") === "ring") && ( +
+ <> +
Divided by
+
+ ( + + )} + /> +
+ + <> +
Color
+
+ ( + + )} + /> +
+ + <> +
Show number
+
+ ( + + )} + /> +
+ +
)} - /> -
- - <> -
Color
-
- ( - - )} - /> -
- - <> -
Show number
-
- ( - - )} - /> -
- -
+
+
+ ( + + )} + /> + Mandatory field +
+
+ + {isRemoving ? "Removing..." : "Remove"} + + + {isSubmitting ? "Saving..." : "Save"} + +
+
+ + + )} - -); + + ); +}; diff --git a/web/components/custom-attributes/attribute-forms/relation-attribute-form.tsx b/web/components/custom-attributes/attribute-forms/relation-attribute-form.tsx index 629abdff1..35c327413 100644 --- a/web/components/custom-attributes/attribute-forms/relation-attribute-form.tsx +++ b/web/components/custom-attributes/attribute-forms/relation-attribute-form.tsx @@ -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 = ({ control }) => ( -
- ( - - )} - /> - ( - {value}} - 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) => ( - - {unit.label} - - ))} - - )} - /> - {/*
+// 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; + handleUpdateAttribute: (data: Partial) => Promise; +}; + +const typeMetaData = CUSTOM_ATTRIBUTES_LIST.relation; + +export const RelationAttributeForm: React.FC = (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 ( + + {({ open }) => ( + <> + +
+ +
{attributeDetails.display_name ?? typeMetaData.label}
+
+
+ +
+
+ +
+
+ ( + + )} + /> + ( + {value}} + 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) => ( + + {unit.label} + + ))} + + )} + /> + {/*

Selection type

@@ -57,5 +105,31 @@ export const RelationAttributeForm: React.FC = ({ control })
*/} -
-); +
+
+
+ ( + + )} + /> + Mandatory field +
+
+ + {isRemoving ? "Removing..." : "Remove"} + + + {isSubmitting ? "Saving..." : "Save"} + +
+
+ + + + )} + + ); +}; diff --git a/web/components/custom-attributes/attribute-forms/select-attribute/select-attribute-form.tsx b/web/components/custom-attributes/attribute-forms/select-attribute/select-attribute-form.tsx index aa4cc9dcc..dac05a3c4 100644 --- a/web/components/custom-attributes/attribute-forms/select-attribute/select-attribute-form.tsx +++ b/web/components/custom-attributes/attribute-forms/select-attribute/select-attribute-form.tsx @@ -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 = observer( - ({ control, multiple = false, objectId = "", watch }) => { - const [optionToEdit, setOptionToEdit] = useState(null); +type Props = { + attributeDetails: ICustomAttribute; + handleDeleteAttribute: () => Promise; + handleUpdateAttribute: (data: Partial) => Promise; +}; - const { customAttributes } = useMobxStore(); +const typeMetaData = CUSTOM_ATTRIBUTES_LIST.select; - const options = customAttributes.objectAttributes?.[objectId]?.[watch("id") ?? ""]?.children; +export const SelectAttributeForm: React.FC = observer((props) => { + const { + attributeDetails, + handleDeleteAttribute, + handleUpdateAttribute, + multiple = false, + } = props; - return ( -
- ( - - )} - /> -
-

Options

-
- {options?.map((option) => ( - setOptionToEdit(option)} - objectId={objectId} - option={option} - /> - ))} -
-
- setOptionToEdit(null)} - parentId={watch("id") ?? ""} - /> -
-
-
- ); - } -); + const [optionToEdit, setOptionToEdit] = useState(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 ( + + {({ open }) => ( + <> + +
+ +
{attributeDetails.display_name ?? typeMetaData.label}
+
+
+ +
+
+ +
+
+ ( + + )} + /> +
+

Options

+
+ {options?.map((option) => ( + setOptionToEdit(option)} + objectId={attributeDetails.parent ?? ""} + option={option} + /> + ))} +
+
+ setOptionToEdit(null)} + parentId={watch("id") ?? ""} + /> +
+
+
+ +
+
+ ( + + )} + /> + Mandatory field +
+
+ + {isRemoving ? "Removing..." : "Remove"} + + + {isSubmitting ? "Saving..." : "Save"} + +
+
+
+
+ + )} +
+ ); +}); diff --git a/web/components/custom-attributes/attribute-forms/text-attribute-form.tsx b/web/components/custom-attributes/attribute-forms/text-attribute-form.tsx index 3748006d3..b1738b10d 100644 --- a/web/components/custom-attributes/attribute-forms/text-attribute-form.tsx +++ b/web/components/custom-attributes/attribute-forms/text-attribute-form.tsx @@ -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 = ({ 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; + handleUpdateAttribute: (data: Partial) => Promise; +}; + +const typeMetaData = CUSTOM_ATTRIBUTES_LIST.text; + +export const TextAttributeForm: React.FC = (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 ( + + {({ open }) => ( + <> + +
+ +
{attributeDetails.display_name ?? typeMetaData.label}
+
+
+ +
+
+ +
+
+ ( + + )} + /> + ( + + )} + /> +
+
+
+ ( + + )} + /> + Mandatory field +
+
+ + {isRemoving ? "Removing..." : "Remove"} + + + {isSubmitting ? "Saving..." : "Save"} + +
+
+
+
+ )} - /> - ( - - )} - /> -
-); + + ); +}; diff --git a/web/components/custom-attributes/attribute-forms/url-attribute-form.tsx b/web/components/custom-attributes/attribute-forms/url-attribute-form.tsx index 775889527..9d80eeb85 100644 --- a/web/components/custom-attributes/attribute-forms/url-attribute-form.tsx +++ b/web/components/custom-attributes/attribute-forms/url-attribute-form.tsx @@ -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 = ({ 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; + handleUpdateAttribute: (data: Partial) => Promise; +}; + +const typeMetaData = CUSTOM_ATTRIBUTES_LIST.url; + +export const UrlAttributeForm: React.FC = (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 ( + + {({ open }) => ( + <> + +
+ +
{attributeDetails.display_name ?? typeMetaData.label}
+
+
+ +
+
+ +
+
+ ( + + )} + /> + ( + + )} + /> +
+
+
+ ( + + )} + /> + Mandatory field +
+
+ + {isRemoving ? "Removing..." : "Remove"} + + + {isSubmitting ? "Saving..." : "Save"} + +
+
+
+
+ )} - /> - ( - - )} - /> -
-); + + ); +}; diff --git a/web/components/custom-attributes/attributes-list/issue-modal/checkboxes.tsx b/web/components/custom-attributes/attributes-list/issue-modal/checkboxes.tsx index 40642672a..f9079a157 100644 --- a/web/components/custom-attributes/attributes-list/issue-modal/checkboxes.tsx +++ b/web/components/custom-attributes/attributes-list/issue-modal/checkboxes.tsx @@ -23,6 +23,8 @@ export const CustomAttributesCheckboxes: React.FC = observer((props) => { const checkboxFields = Object.values(attributes).filter((a) => a.type === "checkbox"); + if (checkboxFields.length === 0) return null; + return (
{Object.entries(checkboxFields).map(([attributeId, attribute]) => ( diff --git a/web/components/custom-attributes/attributes-list/issue-modal/description-fields.tsx b/web/components/custom-attributes/attributes-list/issue-modal/description-fields.tsx index aa9f34462..2a325147c 100644 --- a/web/components/custom-attributes/attributes-list/issue-modal/description-fields.tsx +++ b/web/components/custom-attributes/attributes-list/issue-modal/description-fields.tsx @@ -35,6 +35,8 @@ export const CustomAttributesDescriptionFields: React.FC = observer((prop DESCRIPTION_FIELDS.includes(a.type) ); + if (descriptionFields.length === 0) return null; + return ( {({ open }) => ( diff --git a/web/components/custom-attributes/attributes-list/issue-modal/file-uploads.tsx b/web/components/custom-attributes/attributes-list/issue-modal/file-uploads.tsx index 9da9012ad..dd030d15b 100644 --- a/web/components/custom-attributes/attributes-list/issue-modal/file-uploads.tsx +++ b/web/components/custom-attributes/attributes-list/issue-modal/file-uploads.tsx @@ -191,6 +191,8 @@ export const CustomAttributesFileUploads: React.FC = observer((props) => const fileUploadFields = Object.values(attributes).filter((a) => a.type === "file"); + if (fileUploadFields.length === 0) return null; + return ( <> {customAttributes.fetchObjectDetailsLoader ? ( diff --git a/web/components/custom-attributes/object-modal.tsx b/web/components/custom-attributes/object-modal.tsx index ffc8fbc77..ad8e4833d 100644 --- a/web/components/custom-attributes/object-modal.tsx +++ b/web/components/custom-attributes/object-modal.tsx @@ -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; }; -export const ObjectModal: React.FC = observer( - ({ objectIdToEdit, isOpen, onClose, onSubmit }) => { - const [object, setObject] = useState>({ - display_name: "", - description: "", +const defaultValues: Partial = { + display_name: "", + description: "", + icon: "", +}; + +export const ObjectModal: React.FC = 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({ 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) => { + if (!workspaceSlug || !projectId) return; + + const payload: Partial = { + 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) => { + if (!workspaceSlug || !data || !data.id) return; + + const payload: Partial = { + description: formData.description ?? "", + display_name: formData.display_name ?? "", + icon: formData.icon ?? "", + }; + + await customAttributes.updateObject(workspaceSlug.toString(), data.id, payload); + }; + + const handleObjectFormSubmit = async (formData: Partial) => { + 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 = { + 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 = { - 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 = { - 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 = { - 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 ( + + + +
+ - // update the object state if objectIdToEdit is present - useEffect(() => { - if (!objectIdToEdit) return; - - setObject((prev) => ({ - ...prev, - id: objectIdToEdit, - })); - }, [objectIdToEdit]); - - return ( - - - -
- - - - -
-
-

New Object

-
-
-
-
- { - if (typeof icon === "string") - setObject((prevData) => ({ ...prevData, icon })); - }} - value={object.icon} - showIconPicker={false} - /> -
- - setObject((prevData) => ({ ...prevData, display_name: e.target.value })) - } + + +
+
+

New Object

+
+
+
+
+ ( + { + if (typeof icon === "string") onChange(icon); + }} + value={value} + showIconPicker={false} + /> + )} />
-