forked from github/plane
refactor: attribute form components and object modal
This commit is contained in:
parent
4f33c4bea3
commit
4d158a6d8f
@ -1,14 +1,8 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
// mobx
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// headless ui
|
|
||||||
import { Disclosure } from "@headlessui/react";
|
|
||||||
// react-hook-form
|
|
||||||
import { Control, Controller, UseFormWatch, useForm } from "react-hook-form";
|
|
||||||
// components
|
// components
|
||||||
import {
|
import {
|
||||||
CheckboxAttributeForm,
|
CheckboxAttributeForm,
|
||||||
@ -21,81 +15,23 @@ import {
|
|||||||
TextAttributeForm,
|
TextAttributeForm,
|
||||||
UrlAttributeForm,
|
UrlAttributeForm,
|
||||||
} from "components/custom-attributes";
|
} from "components/custom-attributes";
|
||||||
// ui
|
|
||||||
import { PrimaryButton, SecondaryButton, ToggleSwitch } from "components/ui";
|
|
||||||
// icons
|
|
||||||
import { ChevronDown } from "lucide-react";
|
|
||||||
// types
|
// types
|
||||||
import { ICustomAttribute, TCustomAttributeTypes } from "types";
|
import { ICustomAttribute, TCustomAttributeTypes } from "types";
|
||||||
// constants
|
|
||||||
import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
attributeDetails: Partial<ICustomAttribute>;
|
attributeDetails: ICustomAttribute;
|
||||||
objectId: string;
|
objectId: string;
|
||||||
type: TCustomAttributeTypes;
|
type: TCustomAttributeTypes;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FormComponentProps = {
|
export const AttributeForm: React.FC<Props> = observer((props) => {
|
||||||
control: Control<Partial<ICustomAttribute>, any>;
|
const { attributeDetails, objectId, type } = props;
|
||||||
objectId: string;
|
|
||||||
watch: UseFormWatch<Partial<ICustomAttribute>>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const RenderForm: React.FC<{ type: TCustomAttributeTypes } & FormComponentProps> = ({
|
|
||||||
control,
|
|
||||||
objectId,
|
|
||||||
type,
|
|
||||||
watch,
|
|
||||||
}) => {
|
|
||||||
let FormToRender: any = <></>;
|
|
||||||
|
|
||||||
if (type === "checkbox")
|
|
||||||
FormToRender = <CheckboxAttributeForm control={control} objectId={objectId} watch={watch} />;
|
|
||||||
else if (type === "datetime")
|
|
||||||
FormToRender = <DateTimeAttributeForm control={control} objectId={objectId} watch={watch} />;
|
|
||||||
else if (type === "email")
|
|
||||||
FormToRender = <EmailAttributeForm control={control} objectId={objectId} watch={watch} />;
|
|
||||||
else if (type === "file")
|
|
||||||
FormToRender = <FileAttributeForm control={control} objectId={objectId} watch={watch} />;
|
|
||||||
else if (type === "multi_select")
|
|
||||||
FormToRender = (
|
|
||||||
<SelectAttributeForm control={control} objectId={objectId} watch={watch} multiple />
|
|
||||||
);
|
|
||||||
else if (type === "number")
|
|
||||||
FormToRender = <NumberAttributeForm control={control} objectId={objectId} watch={watch} />;
|
|
||||||
else if (type === "relation")
|
|
||||||
FormToRender = <RelationAttributeForm control={control} objectId={objectId} watch={watch} />;
|
|
||||||
else if (type === "select")
|
|
||||||
FormToRender = <SelectAttributeForm control={control} objectId={objectId} watch={watch} />;
|
|
||||||
else if (type === "text")
|
|
||||||
FormToRender = <TextAttributeForm control={control} objectId={objectId} watch={watch} />;
|
|
||||||
else if (type === "url")
|
|
||||||
FormToRender = <UrlAttributeForm control={control} objectId={objectId} watch={watch} />;
|
|
||||||
|
|
||||||
return FormToRender;
|
|
||||||
};
|
|
||||||
|
|
||||||
const OPTIONAL_FIELDS = ["checkbox", "file"];
|
|
||||||
|
|
||||||
export const AttributeForm: React.FC<Props> = observer(({ attributeDetails, objectId, type }) => {
|
|
||||||
const [isRemoving, setIsRemoving] = useState(false);
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
const typeMetaData = CUSTOM_ATTRIBUTES_LIST[type];
|
|
||||||
|
|
||||||
const { customAttributes } = useMobxStore();
|
const { customAttributes } = useMobxStore();
|
||||||
|
|
||||||
const {
|
|
||||||
control,
|
|
||||||
formState: { isSubmitting },
|
|
||||||
handleSubmit,
|
|
||||||
reset,
|
|
||||||
watch,
|
|
||||||
} = useForm({ defaultValues: typeMetaData.defaultFormValues });
|
|
||||||
|
|
||||||
const handleUpdateAttribute = async (data: Partial<ICustomAttribute>) => {
|
const handleUpdateAttribute = async (data: Partial<ICustomAttribute>) => {
|
||||||
if (!workspaceSlug || !attributeDetails.id || !objectId) return;
|
if (!workspaceSlug || !attributeDetails.id || !objectId) return;
|
||||||
|
|
||||||
@ -110,80 +46,96 @@ export const AttributeForm: React.FC<Props> = observer(({ attributeDetails, obje
|
|||||||
const handleDeleteAttribute = async () => {
|
const handleDeleteAttribute = async () => {
|
||||||
if (!workspaceSlug || !attributeDetails.id || !objectId) return;
|
if (!workspaceSlug || !attributeDetails.id || !objectId) return;
|
||||||
|
|
||||||
setIsRemoving(true);
|
await customAttributes.deleteObjectAttribute(
|
||||||
|
workspaceSlug.toString(),
|
||||||
await customAttributes
|
objectId,
|
||||||
.deleteObjectAttribute(workspaceSlug.toString(), objectId, attributeDetails.id)
|
attributeDetails.id
|
||||||
.finally(() => setIsRemoving(false));
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
switch (type) {
|
||||||
if (!attributeDetails) return;
|
case "checkbox":
|
||||||
|
return (
|
||||||
reset({
|
<CheckboxAttributeForm
|
||||||
...typeMetaData.defaultFormValues,
|
attributeDetails={attributeDetails}
|
||||||
...attributeDetails,
|
handleDeleteAttribute={handleDeleteAttribute}
|
||||||
});
|
handleUpdateAttribute={handleUpdateAttribute}
|
||||||
}, [attributeDetails, reset, typeMetaData.defaultFormValues]);
|
/>
|
||||||
|
);
|
||||||
return (
|
case "datetime":
|
||||||
<Disclosure
|
return (
|
||||||
as="div"
|
<DateTimeAttributeForm
|
||||||
className="bg-custom-background-90 border border-custom-border-200 rounded"
|
attributeDetails={attributeDetails}
|
||||||
>
|
handleDeleteAttribute={handleDeleteAttribute}
|
||||||
{({ open }) => (
|
handleUpdateAttribute={handleUpdateAttribute}
|
||||||
<>
|
/>
|
||||||
<Disclosure.Button className="p-3 flex items-center justify-between gap-1 w-full">
|
);
|
||||||
<div className="flex items-center gap-2.5">
|
case "email":
|
||||||
<typeMetaData.icon size={14} strokeWidth={1.5} />
|
return (
|
||||||
<h6 className="text-sm">{attributeDetails.display_name ?? typeMetaData.label}</h6>
|
<EmailAttributeForm
|
||||||
</div>
|
attributeDetails={attributeDetails}
|
||||||
<div className={`${open ? "-rotate-180" : ""} transition-all`}>
|
handleDeleteAttribute={handleDeleteAttribute}
|
||||||
<ChevronDown size={16} strokeWidth={1.5} rotate="180deg" />
|
handleUpdateAttribute={handleUpdateAttribute}
|
||||||
</div>
|
/>
|
||||||
</Disclosure.Button>
|
);
|
||||||
<Disclosure.Panel>
|
case "file":
|
||||||
<form onSubmit={handleSubmit(handleUpdateAttribute)} className="p-3 pl-9 pt-0">
|
return (
|
||||||
{attributeDetails.type && (
|
<FileAttributeForm
|
||||||
<RenderForm
|
attributeDetails={attributeDetails}
|
||||||
type={attributeDetails.type}
|
handleDeleteAttribute={handleDeleteAttribute}
|
||||||
control={control}
|
handleUpdateAttribute={handleUpdateAttribute}
|
||||||
objectId={objectId}
|
/>
|
||||||
watch={watch}
|
);
|
||||||
/>
|
case "multi_select":
|
||||||
)}
|
return (
|
||||||
<div className="mt-8 flex items-center justify-between">
|
<SelectAttributeForm
|
||||||
<div className="flex-shrink-0 flex items-center gap-2">
|
attributeDetails={attributeDetails}
|
||||||
{!OPTIONAL_FIELDS.includes(attributeDetails.type ?? "") && (
|
handleDeleteAttribute={handleDeleteAttribute}
|
||||||
<>
|
handleUpdateAttribute={handleUpdateAttribute}
|
||||||
<Controller
|
multiple
|
||||||
control={control}
|
/>
|
||||||
name="is_required"
|
);
|
||||||
render={({ field: { onChange, value } }) => (
|
case "number":
|
||||||
<ToggleSwitch value={value ?? false} onChange={onChange} />
|
return (
|
||||||
)}
|
<NumberAttributeForm
|
||||||
/>
|
attributeDetails={attributeDetails}
|
||||||
<span className="text-xs">Mandatory field</span>
|
handleDeleteAttribute={handleDeleteAttribute}
|
||||||
</>
|
handleUpdateAttribute={handleUpdateAttribute}
|
||||||
)}
|
/>
|
||||||
</div>
|
);
|
||||||
<div className="flex items-center gap-2">
|
case "relation":
|
||||||
<SecondaryButton
|
return (
|
||||||
type="button"
|
<RelationAttributeForm
|
||||||
onClick={handleDeleteAttribute}
|
attributeDetails={attributeDetails}
|
||||||
loading={isRemoving}
|
handleDeleteAttribute={handleDeleteAttribute}
|
||||||
>
|
handleUpdateAttribute={handleUpdateAttribute}
|
||||||
{isRemoving ? "Removing..." : "Remove"}
|
/>
|
||||||
</SecondaryButton>
|
);
|
||||||
<PrimaryButton type="submit" loading={isSubmitting}>
|
case "select":
|
||||||
{isSubmitting ? "Saving..." : "Save"}
|
return (
|
||||||
</PrimaryButton>
|
<SelectAttributeForm
|
||||||
</div>
|
attributeDetails={attributeDetails}
|
||||||
</div>
|
handleDeleteAttribute={handleDeleteAttribute}
|
||||||
</form>
|
handleUpdateAttribute={handleUpdateAttribute}
|
||||||
</Disclosure.Panel>
|
/>
|
||||||
</>
|
);
|
||||||
)}
|
case "text":
|
||||||
</Disclosure>
|
return (
|
||||||
);
|
<TextAttributeForm
|
||||||
|
attributeDetails={attributeDetails}
|
||||||
|
handleDeleteAttribute={handleDeleteAttribute}
|
||||||
|
handleUpdateAttribute={handleUpdateAttribute}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "url":
|
||||||
|
return (
|
||||||
|
<UrlAttributeForm
|
||||||
|
attributeDetails={attributeDetails}
|
||||||
|
handleDeleteAttribute={handleDeleteAttribute}
|
||||||
|
handleUpdateAttribute={handleUpdateAttribute}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,14 +1,27 @@
|
|||||||
|
import { useState } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { Disclosure } from "@headlessui/react";
|
||||||
|
|
||||||
// react-hook-form
|
|
||||||
import { Controller } from "react-hook-form";
|
|
||||||
// components
|
// components
|
||||||
import { FormComponentProps, Input } from "components/custom-attributes";
|
import { Input } from "components/custom-attributes";
|
||||||
|
// ui
|
||||||
|
import { PrimaryButton, SecondaryButton } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { CheckCircle2 } from "lucide-react";
|
import { CheckCircle2, ChevronDown } from "lucide-react";
|
||||||
// assets
|
// assets
|
||||||
import CheckRepresentation from "public/custom-attributes/checkbox/check.svg";
|
import CheckRepresentation from "public/custom-attributes/checkbox/check.svg";
|
||||||
import ToggleSwitchRepresentation from "public/custom-attributes/checkbox/toggle-switch.svg";
|
import ToggleSwitchRepresentation from "public/custom-attributes/checkbox/toggle-switch.svg";
|
||||||
|
// types
|
||||||
|
import { ICustomAttribute } from "types";
|
||||||
|
// constants
|
||||||
|
import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
attributeDetails: ICustomAttribute;
|
||||||
|
handleDeleteAttribute: () => Promise<void>;
|
||||||
|
handleUpdateAttribute: (data: Partial<ICustomAttribute>) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
const checkboxAttributeRepresentations = [
|
const checkboxAttributeRepresentations = [
|
||||||
{
|
{
|
||||||
@ -23,91 +36,141 @@ const checkboxAttributeRepresentations = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const CheckboxAttributeForm: React.FC<FormComponentProps> = ({ control }) => (
|
const typeMetaData = CUSTOM_ATTRIBUTES_LIST.checkbox;
|
||||||
<>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="display_name"
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<Input placeholder="Enter field title" value={value} onChange={onChange} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<p className="text-xs">Default value</p>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="default_value"
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<div className="mt-2 flex items-center gap-6 accent-custom-primary-100">
|
|
||||||
<div className="flex items-center gap-1 text-xs">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="default_value"
|
|
||||||
value="true"
|
|
||||||
id="checked"
|
|
||||||
className="scale-75"
|
|
||||||
defaultChecked={value === "true"}
|
|
||||||
onChange={(e) => onChange(e.target.value)}
|
|
||||||
/>
|
|
||||||
<label htmlFor="checked">Checked</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-1 text-xs">
|
export const CheckboxAttributeForm: React.FC<Props> = (props) => {
|
||||||
<input
|
const { attributeDetails, handleDeleteAttribute, handleUpdateAttribute } = props;
|
||||||
type="radio"
|
|
||||||
name="default_value"
|
const [isRemoving, setIsRemoving] = useState(false);
|
||||||
value="false"
|
|
||||||
id="unchecked"
|
const {
|
||||||
className="scale-75"
|
control,
|
||||||
defaultChecked={value === "false"}
|
formState: { isSubmitting },
|
||||||
onChange={(e) => onChange(e.target.value)}
|
handleSubmit,
|
||||||
/>
|
} = useForm({ defaultValues: typeMetaData.defaultFormValues });
|
||||||
<label htmlFor="unchecked">Unchecked</label>
|
|
||||||
</div>
|
const handleDelete = async () => {
|
||||||
|
setIsRemoving(true);
|
||||||
|
|
||||||
|
await handleDeleteAttribute().finally(() => setIsRemoving(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Disclosure
|
||||||
|
as="div"
|
||||||
|
className="bg-custom-background-90 border border-custom-border-200 rounded"
|
||||||
|
>
|
||||||
|
{({ open }) => (
|
||||||
|
<>
|
||||||
|
<Disclosure.Button className="p-3 flex items-center justify-between gap-1 w-full">
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<typeMetaData.icon size={14} strokeWidth={1.5} />
|
||||||
|
<h6 className="text-sm">{attributeDetails.display_name ?? typeMetaData.label}</h6>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className={`${open ? "-rotate-180" : ""} transition-all`}>
|
||||||
/>
|
<ChevronDown size={16} strokeWidth={1.5} rotate="180deg" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Disclosure.Button>
|
||||||
<div className="my-3 border-t-[0.5px] border-custom-border-200" />
|
<Disclosure.Panel>
|
||||||
<div>
|
<form onSubmit={handleSubmit(handleUpdateAttribute)} className="p-3 pl-9 pt-0">
|
||||||
<h6 className="text-xs">Show as</h6>
|
<div className="space-y-3">
|
||||||
<div className="mt-2 flex items-center gap-4 flex-wrap">
|
<Controller
|
||||||
<Controller
|
control={control}
|
||||||
control={control}
|
name="display_name"
|
||||||
name="extra_settings.representation"
|
render={({ field: { onChange, value } }) => (
|
||||||
render={({ field: { onChange, value } }) => (
|
<Input placeholder="Enter field title" value={value} onChange={onChange} />
|
||||||
<>
|
)}
|
||||||
{checkboxAttributeRepresentations.map((representation) => (
|
/>
|
||||||
<div
|
<div>
|
||||||
key={representation.key}
|
<p className="text-xs">Default value</p>
|
||||||
className={`rounded divide-y w-32 cursor-pointer border ${
|
<Controller
|
||||||
value === representation.key
|
control={control}
|
||||||
? "border-custom-primary-100 divide-custom-primary-100"
|
name="default_value"
|
||||||
: "border-custom-border-200 divide-custom-border-200"
|
render={({ field: { onChange, value } }) => (
|
||||||
}`}
|
<div className="mt-2 flex items-center gap-6 accent-custom-primary-100">
|
||||||
onClick={() => onChange(representation.key)}
|
<div className="flex items-center gap-1 text-xs">
|
||||||
>
|
<input
|
||||||
<div className="h-24 p-2.5 grid place-items-center">
|
type="radio"
|
||||||
<Image src={representation.image} alt={representation.label} />
|
name="default_value"
|
||||||
</div>
|
value="true"
|
||||||
<div className="h-9 text-xs font-medium p-2.5 bg-custom-background-100 rounded-b flex items-center justify-between gap-2">
|
id="checked"
|
||||||
{representation.label}
|
className="scale-75"
|
||||||
{value === representation.key && (
|
defaultChecked={value === "true"}
|
||||||
<CheckCircle2
|
onChange={(e) => onChange(e.target.value)}
|
||||||
size={14}
|
/>
|
||||||
strokeWidth={1.5}
|
<label htmlFor="checked">Checked</label>
|
||||||
className="text-custom-primary-100"
|
</div>
|
||||||
/>
|
|
||||||
|
<div className="flex items-center gap-1 text-xs">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="default_value"
|
||||||
|
value="false"
|
||||||
|
id="unchecked"
|
||||||
|
className="scale-75"
|
||||||
|
defaultChecked={value === "false"}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="unchecked">Unchecked</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
</>
|
<div className="my-3 border-t-[0.5px] border-custom-border-200" />
|
||||||
)}
|
<div>
|
||||||
/>
|
<h6 className="text-xs">Show as</h6>
|
||||||
</div>
|
<div className="mt-2 flex items-center gap-4 flex-wrap">
|
||||||
</div>
|
<Controller
|
||||||
</>
|
control={control}
|
||||||
);
|
name="extra_settings.representation"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<>
|
||||||
|
{checkboxAttributeRepresentations.map((representation) => (
|
||||||
|
<div
|
||||||
|
key={representation.key}
|
||||||
|
className={`rounded divide-y w-32 cursor-pointer border ${
|
||||||
|
value === representation.key
|
||||||
|
? "border-custom-primary-100 divide-custom-primary-100"
|
||||||
|
: "border-custom-border-200 divide-custom-border-200"
|
||||||
|
}`}
|
||||||
|
onClick={() => onChange(representation.key)}
|
||||||
|
>
|
||||||
|
<div className="h-24 p-2.5 grid place-items-center">
|
||||||
|
<Image src={representation.image} alt={representation.label} />
|
||||||
|
</div>
|
||||||
|
<div className="h-9 text-xs font-medium p-2.5 bg-custom-background-100 rounded-b flex items-center justify-between gap-2">
|
||||||
|
{representation.label}
|
||||||
|
{value === representation.key && (
|
||||||
|
<CheckCircle2
|
||||||
|
size={14}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
className="text-custom-primary-100"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-8 flex items-center justify-end">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<SecondaryButton type="button" onClick={handleDelete} loading={isRemoving}>
|
||||||
|
{isRemoving ? "Removing..." : "Remove"}
|
||||||
|
</SecondaryButton>
|
||||||
|
<PrimaryButton type="submit" loading={isSubmitting}>
|
||||||
|
{isSubmitting ? "Saving..." : "Save"}
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Disclosure.Panel>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Disclosure>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -1,112 +1,191 @@
|
|||||||
// react-hook-form
|
import { useState } from "react";
|
||||||
import { Controller } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
// components
|
import { Disclosure } from "@headlessui/react";
|
||||||
import { FormComponentProps, Input } from "components/custom-attributes";
|
|
||||||
// ui
|
|
||||||
import { CustomSelect, ToggleSwitch, Tooltip } from "components/ui";
|
|
||||||
// constants
|
|
||||||
import { DATE_FORMATS, TIME_FORMATS } from "constants/custom-attributes";
|
|
||||||
|
|
||||||
export const DateTimeAttributeForm: React.FC<FormComponentProps> = ({ control, watch }) => (
|
// components
|
||||||
<div className="space-y-3">
|
import { Input } from "components/custom-attributes";
|
||||||
<Controller
|
// ui
|
||||||
control={control}
|
import { CustomSelect, PrimaryButton, SecondaryButton, ToggleSwitch, Tooltip } from "components/ui";
|
||||||
name="display_name"
|
// icons
|
||||||
render={({ field: { onChange, value } }) => (
|
import { ChevronDown } from "lucide-react";
|
||||||
<Input placeholder="Enter field title" value={value} onChange={onChange} />
|
// types
|
||||||
|
import { ICustomAttribute } from "types";
|
||||||
|
// constants
|
||||||
|
import { CUSTOM_ATTRIBUTES_LIST, DATE_FORMATS, TIME_FORMATS } from "constants/custom-attributes";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
attributeDetails: ICustomAttribute;
|
||||||
|
handleDeleteAttribute: () => Promise<void>;
|
||||||
|
handleUpdateAttribute: (data: Partial<ICustomAttribute>) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const typeMetaData = CUSTOM_ATTRIBUTES_LIST.datetime;
|
||||||
|
|
||||||
|
export const DateTimeAttributeForm: React.FC<Props> = (props) => {
|
||||||
|
const { attributeDetails, handleDeleteAttribute, handleUpdateAttribute } = props;
|
||||||
|
|
||||||
|
const [isRemoving, setIsRemoving] = useState(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
formState: { isSubmitting },
|
||||||
|
handleSubmit,
|
||||||
|
watch,
|
||||||
|
} = useForm({ defaultValues: typeMetaData.defaultFormValues });
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
setIsRemoving(true);
|
||||||
|
|
||||||
|
await handleDeleteAttribute().finally(() => setIsRemoving(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Disclosure
|
||||||
|
as="div"
|
||||||
|
className="bg-custom-background-90 border border-custom-border-200 rounded"
|
||||||
|
>
|
||||||
|
{({ open }) => (
|
||||||
|
<>
|
||||||
|
<Disclosure.Button className="p-3 flex items-center justify-between gap-1 w-full">
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<typeMetaData.icon size={14} strokeWidth={1.5} />
|
||||||
|
<h6 className="text-sm">{attributeDetails.display_name ?? typeMetaData.label}</h6>
|
||||||
|
</div>
|
||||||
|
<div className={`${open ? "-rotate-180" : ""} transition-all`}>
|
||||||
|
<ChevronDown size={16} strokeWidth={1.5} rotate="180deg" />
|
||||||
|
</div>
|
||||||
|
</Disclosure.Button>
|
||||||
|
<Disclosure.Panel>
|
||||||
|
<form onSubmit={handleSubmit(handleUpdateAttribute)} className="p-3 pl-9 pt-0">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="display_name"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<Input placeholder="Enter field title" value={value} onChange={onChange} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="extra_settings.date_format"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<CustomSelect
|
||||||
|
label={
|
||||||
|
<span className="text-xs">
|
||||||
|
{DATE_FORMATS.find((f) => f.value === value)?.label}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
buttonClassName="bg-custom-background-100 !px-3 !py-2 !border-custom-border-200 !rounded"
|
||||||
|
optionsClassName="w-full"
|
||||||
|
input
|
||||||
|
>
|
||||||
|
{DATE_FORMATS.map((format) => (
|
||||||
|
<CustomSelect.Option key={format.value} value={format.value}>
|
||||||
|
{format.label}
|
||||||
|
</CustomSelect.Option>
|
||||||
|
))}
|
||||||
|
</CustomSelect>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="extra_settings.hide_date"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<div className="flex items-center justify-end gap-1 mt-2">
|
||||||
|
<Tooltip
|
||||||
|
tooltipContent="Cannot disable both, date and time"
|
||||||
|
disabled={!watch("extra_settings.hide_time")}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<ToggleSwitch
|
||||||
|
value={value ?? false}
|
||||||
|
onChange={onChange}
|
||||||
|
size="sm"
|
||||||
|
disabled={watch("extra_settings.hide_time")}
|
||||||
|
/>
|
||||||
|
<span className="text-xs">Don{"'"}t show date</span>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="extra_settings.time_format"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<CustomSelect
|
||||||
|
label={
|
||||||
|
<span className="text-xs">
|
||||||
|
{TIME_FORMATS.find((f) => f.value === value)?.label}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
buttonClassName="bg-custom-background-100 !px-3 !py-2 !border-custom-border-200 !rounded"
|
||||||
|
optionsClassName="w-full"
|
||||||
|
input
|
||||||
|
>
|
||||||
|
{TIME_FORMATS.map((format) => (
|
||||||
|
<CustomSelect.Option key={format.value} value={format.value}>
|
||||||
|
{format.label}
|
||||||
|
</CustomSelect.Option>
|
||||||
|
))}
|
||||||
|
</CustomSelect>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="extra_settings.hide_time"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<div className="flex items-center justify-end gap-1 mt-2">
|
||||||
|
<Tooltip
|
||||||
|
tooltipContent="Cannot disable both, date and time"
|
||||||
|
disabled={!watch("extra_settings.hide_date")}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<ToggleSwitch
|
||||||
|
value={value ?? false}
|
||||||
|
onChange={onChange}
|
||||||
|
size="sm"
|
||||||
|
disabled={watch("extra_settings.hide_date")}
|
||||||
|
/>
|
||||||
|
<span className="text-xs">Don{"'"}t show time</span>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-8 flex items-center justify-between">
|
||||||
|
<div className="flex-shrink-0 flex items-center gap-2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="is_required"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<ToggleSwitch value={value ?? false} onChange={onChange} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span className="text-xs">Mandatory field</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<SecondaryButton type="button" onClick={handleDelete} loading={isRemoving}>
|
||||||
|
{isRemoving ? "Removing..." : "Remove"}
|
||||||
|
</SecondaryButton>
|
||||||
|
<PrimaryButton type="submit" loading={isSubmitting}>
|
||||||
|
{isSubmitting ? "Saving..." : "Save"}
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Disclosure.Panel>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
</Disclosure>
|
||||||
<div>
|
);
|
||||||
<Controller
|
};
|
||||||
control={control}
|
|
||||||
name="extra_settings.date_format"
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<CustomSelect
|
|
||||||
label={
|
|
||||||
<span className="text-xs">{DATE_FORMATS.find((f) => f.value === value)?.label}</span>
|
|
||||||
}
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
buttonClassName="bg-custom-background-100 !px-3 !py-2 !border-custom-border-200 !rounded"
|
|
||||||
optionsClassName="w-full"
|
|
||||||
input
|
|
||||||
>
|
|
||||||
{DATE_FORMATS.map((format) => (
|
|
||||||
<CustomSelect.Option key={format.value} value={format.value}>
|
|
||||||
{format.label}
|
|
||||||
</CustomSelect.Option>
|
|
||||||
))}
|
|
||||||
</CustomSelect>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="extra_settings.hide_date"
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<div className="flex items-center justify-end gap-1 mt-2">
|
|
||||||
<Tooltip
|
|
||||||
tooltipContent="Cannot disable both, date and time"
|
|
||||||
disabled={!watch("extra_settings.hide_time")}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<ToggleSwitch
|
|
||||||
value={value ?? false}
|
|
||||||
onChange={onChange}
|
|
||||||
size="sm"
|
|
||||||
disabled={watch("extra_settings.hide_time")}
|
|
||||||
/>
|
|
||||||
<span className="text-xs">Don{"'"}t show date</span>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="extra_settings.time_format"
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<CustomSelect
|
|
||||||
label={
|
|
||||||
<span className="text-xs">{TIME_FORMATS.find((f) => f.value === value)?.label}</span>
|
|
||||||
}
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
buttonClassName="bg-custom-background-100 !px-3 !py-2 !border-custom-border-200 !rounded"
|
|
||||||
optionsClassName="w-full"
|
|
||||||
input
|
|
||||||
>
|
|
||||||
{TIME_FORMATS.map((format) => (
|
|
||||||
<CustomSelect.Option key={format.value} value={format.value}>
|
|
||||||
{format.label}
|
|
||||||
</CustomSelect.Option>
|
|
||||||
))}
|
|
||||||
</CustomSelect>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="extra_settings.hide_time"
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<div className="flex items-center justify-end gap-1 mt-2">
|
|
||||||
<Tooltip
|
|
||||||
tooltipContent="Cannot disable both, date and time"
|
|
||||||
disabled={!watch("extra_settings.hide_date")}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<ToggleSwitch
|
|
||||||
value={value ?? false}
|
|
||||||
onChange={onChange}
|
|
||||||
size="sm"
|
|
||||||
disabled={watch("extra_settings.hide_date")}
|
|
||||||
/>
|
|
||||||
<span className="text-xs">Don{"'"}t show time</span>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
@ -1,28 +1,106 @@
|
|||||||
// react-hook-form
|
import { useState } from "react";
|
||||||
import { Controller } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
// ui
|
import { Disclosure } from "@headlessui/react";
|
||||||
import { FormComponentProps, Input } from "components/custom-attributes";
|
|
||||||
|
|
||||||
export const EmailAttributeForm: React.FC<FormComponentProps> = ({ control }) => (
|
// components
|
||||||
<div className="space-y-3">
|
import { Input } from "components/custom-attributes";
|
||||||
<Controller
|
// ui
|
||||||
control={control}
|
import { PrimaryButton, SecondaryButton, ToggleSwitch } from "components/ui";
|
||||||
name="display_name"
|
// icons
|
||||||
render={({ field: { onChange, value } }) => (
|
import { ChevronDown } from "lucide-react";
|
||||||
<Input placeholder="Enter field title" value={value} onChange={onChange} />
|
// types
|
||||||
|
import { ICustomAttribute } from "types";
|
||||||
|
// constants
|
||||||
|
import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
attributeDetails: ICustomAttribute;
|
||||||
|
handleDeleteAttribute: () => Promise<void>;
|
||||||
|
handleUpdateAttribute: (data: Partial<ICustomAttribute>) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const typeMetaData = CUSTOM_ATTRIBUTES_LIST.email;
|
||||||
|
|
||||||
|
export const EmailAttributeForm: React.FC<Props> = (props) => {
|
||||||
|
const { attributeDetails, handleDeleteAttribute, handleUpdateAttribute } = props;
|
||||||
|
|
||||||
|
const [isRemoving, setIsRemoving] = useState(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
formState: { isSubmitting },
|
||||||
|
handleSubmit,
|
||||||
|
} = useForm({ defaultValues: typeMetaData.defaultFormValues });
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
setIsRemoving(true);
|
||||||
|
|
||||||
|
await handleDeleteAttribute().finally(() => setIsRemoving(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Disclosure
|
||||||
|
as="div"
|
||||||
|
className="bg-custom-background-90 border border-custom-border-200 rounded"
|
||||||
|
>
|
||||||
|
{({ open }) => (
|
||||||
|
<>
|
||||||
|
<Disclosure.Button className="p-3 flex items-center justify-between gap-1 w-full">
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<typeMetaData.icon size={14} strokeWidth={1.5} />
|
||||||
|
<h6 className="text-sm">{attributeDetails.display_name ?? typeMetaData.label}</h6>
|
||||||
|
</div>
|
||||||
|
<div className={`${open ? "-rotate-180" : ""} transition-all`}>
|
||||||
|
<ChevronDown size={16} strokeWidth={1.5} rotate="180deg" />
|
||||||
|
</div>
|
||||||
|
</Disclosure.Button>
|
||||||
|
<Disclosure.Panel>
|
||||||
|
<form onSubmit={handleSubmit(handleUpdateAttribute)} className="p-3 pl-9 pt-0">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="display_name"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<Input placeholder="Enter field title" value={value} onChange={onChange} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="default_value"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
placeholder="Enter default email"
|
||||||
|
value={value?.toString()}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-8 flex items-center justify-between">
|
||||||
|
<div className="flex-shrink-0 flex items-center gap-2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="is_required"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<ToggleSwitch value={value ?? false} onChange={onChange} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span className="text-xs">Mandatory field</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<SecondaryButton type="button" onClick={handleDelete} loading={isRemoving}>
|
||||||
|
{isRemoving ? "Removing..." : "Remove"}
|
||||||
|
</SecondaryButton>
|
||||||
|
<PrimaryButton type="submit" loading={isSubmitting}>
|
||||||
|
{isSubmitting ? "Saving..." : "Save"}
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Disclosure.Panel>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
</Disclosure>
|
||||||
<Controller
|
);
|
||||||
control={control}
|
};
|
||||||
name="default_value"
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<Input
|
|
||||||
type="email"
|
|
||||||
placeholder="Enter default email"
|
|
||||||
value={value?.toString()}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
@ -1,23 +1,91 @@
|
|||||||
// react-hook-form
|
import { useState } from "react";
|
||||||
import { Controller } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
// components
|
import { Disclosure } from "@headlessui/react";
|
||||||
import { FileFormatsDropdown, FormComponentProps, Input } from "components/custom-attributes";
|
|
||||||
|
|
||||||
export const FileAttributeForm: React.FC<FormComponentProps> = ({ control }) => (
|
// components
|
||||||
<div className="space-y-3">
|
import { FileFormatsDropdown, Input } from "components/custom-attributes";
|
||||||
<Controller
|
// ui
|
||||||
control={control}
|
import { PrimaryButton, SecondaryButton } from "components/ui";
|
||||||
name="display_name"
|
// icons
|
||||||
render={({ field: { onChange, value } }) => (
|
import { ChevronDown } from "lucide-react";
|
||||||
<Input placeholder="Enter field title" value={value} onChange={onChange} />
|
// types
|
||||||
|
import { ICustomAttribute } from "types";
|
||||||
|
// constants
|
||||||
|
import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
attributeDetails: ICustomAttribute;
|
||||||
|
handleDeleteAttribute: () => Promise<void>;
|
||||||
|
handleUpdateAttribute: (data: Partial<ICustomAttribute>) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const typeMetaData = CUSTOM_ATTRIBUTES_LIST.file;
|
||||||
|
|
||||||
|
export const FileAttributeForm: React.FC<Props> = (props) => {
|
||||||
|
const { attributeDetails, handleDeleteAttribute, handleUpdateAttribute } = props;
|
||||||
|
|
||||||
|
const [isRemoving, setIsRemoving] = useState(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
formState: { isSubmitting },
|
||||||
|
handleSubmit,
|
||||||
|
} = useForm({ defaultValues: typeMetaData.defaultFormValues });
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
setIsRemoving(true);
|
||||||
|
|
||||||
|
await handleDeleteAttribute().finally(() => setIsRemoving(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Disclosure
|
||||||
|
as="div"
|
||||||
|
className="bg-custom-background-90 border border-custom-border-200 rounded"
|
||||||
|
>
|
||||||
|
{({ open }) => (
|
||||||
|
<>
|
||||||
|
<Disclosure.Button className="p-3 flex items-center justify-between gap-1 w-full">
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<typeMetaData.icon size={14} strokeWidth={1.5} />
|
||||||
|
<h6 className="text-sm">{attributeDetails.display_name ?? typeMetaData.label}</h6>
|
||||||
|
</div>
|
||||||
|
<div className={`${open ? "-rotate-180" : ""} transition-all`}>
|
||||||
|
<ChevronDown size={16} strokeWidth={1.5} rotate="180deg" />
|
||||||
|
</div>
|
||||||
|
</Disclosure.Button>
|
||||||
|
<Disclosure.Panel>
|
||||||
|
<form onSubmit={handleSubmit(handleUpdateAttribute)} className="p-3 pl-9 pt-0">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="display_name"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<Input placeholder="Enter field title" value={value} onChange={onChange} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="extra_settings.file_formats"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<FileFormatsDropdown value={value} onChange={onChange} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-8 flex items-center justify-end">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<SecondaryButton type="button" onClick={handleDelete} loading={isRemoving}>
|
||||||
|
{isRemoving ? "Removing..." : "Remove"}
|
||||||
|
</SecondaryButton>
|
||||||
|
<PrimaryButton type="submit" loading={isSubmitting}>
|
||||||
|
{isSubmitting ? "Saving..." : "Save"}
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Disclosure.Panel>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
</Disclosure>
|
||||||
<Controller
|
);
|
||||||
control={control}
|
};
|
||||||
name="extra_settings.file_formats"
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<FileFormatsDropdown value={value} onChange={onChange} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
@ -1,18 +1,28 @@
|
|||||||
|
import { useState } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { Disclosure } from "@headlessui/react";
|
||||||
|
|
||||||
// react-hook-form
|
|
||||||
import { Controller } from "react-hook-form";
|
|
||||||
// components
|
// components
|
||||||
import { ColorPicker, FormComponentProps } from "components/custom-attributes";
|
import { ColorPicker, Input } from "components/custom-attributes";
|
||||||
// ui
|
// ui
|
||||||
import { ToggleSwitch } from "components/ui";
|
import { PrimaryButton, SecondaryButton, ToggleSwitch } from "components/ui";
|
||||||
import { Input } from "../input";
|
|
||||||
// icons
|
// icons
|
||||||
import { CheckCircle2 } from "lucide-react";
|
import { CheckCircle2, ChevronDown } from "lucide-react";
|
||||||
// assets
|
// assets
|
||||||
import NumericalRepresentation from "public/custom-attributes/number/numerical.svg";
|
import NumericalRepresentation from "public/custom-attributes/number/numerical.svg";
|
||||||
import BarRepresentation from "public/custom-attributes/number/bar.svg";
|
import BarRepresentation from "public/custom-attributes/number/bar.svg";
|
||||||
import RingRepresentation from "public/custom-attributes/number/ring.svg";
|
import RingRepresentation from "public/custom-attributes/number/ring.svg";
|
||||||
|
// types
|
||||||
|
import { ICustomAttribute } from "types";
|
||||||
|
// constants
|
||||||
|
import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
attributeDetails: ICustomAttribute;
|
||||||
|
handleDeleteAttribute: () => Promise<void>;
|
||||||
|
handleUpdateAttribute: (data: Partial<ICustomAttribute>) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
const numberAttributeRepresentations = [
|
const numberAttributeRepresentations = [
|
||||||
{
|
{
|
||||||
@ -32,120 +42,185 @@ const numberAttributeRepresentations = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const NumberAttributeForm: React.FC<FormComponentProps> = ({ control, watch }) => (
|
const typeMetaData = CUSTOM_ATTRIBUTES_LIST.number;
|
||||||
<>
|
|
||||||
<div className="space-y-3">
|
export const NumberAttributeForm: React.FC<Props> = (props) => {
|
||||||
<Controller
|
const { attributeDetails, handleDeleteAttribute, handleUpdateAttribute } = props;
|
||||||
control={control}
|
|
||||||
name="display_name"
|
const [isRemoving, setIsRemoving] = useState(false);
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<Input placeholder="Enter field title" value={value} onChange={onChange} />
|
const {
|
||||||
)}
|
control,
|
||||||
/>
|
formState: { isSubmitting },
|
||||||
<Controller
|
handleSubmit,
|
||||||
control={control}
|
watch,
|
||||||
name="default_value"
|
} = useForm({ defaultValues: typeMetaData.defaultFormValues });
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<Input
|
const handleDelete = async () => {
|
||||||
type="number"
|
setIsRemoving(true);
|
||||||
placeholder="Enter default value"
|
|
||||||
value={value ?? ""}
|
await handleDeleteAttribute().finally(() => setIsRemoving(false));
|
||||||
onChange={onChange}
|
};
|
||||||
step={1}
|
|
||||||
/>
|
return (
|
||||||
)}
|
<Disclosure
|
||||||
/>
|
as="div"
|
||||||
</div>
|
className="bg-custom-background-90 border border-custom-border-200 rounded"
|
||||||
<div className="my-3 border-t-[0.5px] border-custom-border-200" />
|
>
|
||||||
<div>
|
{({ open }) => (
|
||||||
<h6 className="text-xs">Show as</h6>
|
<>
|
||||||
<div className="mt-2 flex items-center gap-4 flex-wrap">
|
<Disclosure.Button className="p-3 flex items-center justify-between gap-1 w-full">
|
||||||
<Controller
|
<div className="flex items-center gap-2.5">
|
||||||
control={control}
|
<typeMetaData.icon size={14} strokeWidth={1.5} />
|
||||||
name="extra_settings.representation"
|
<h6 className="text-sm">{attributeDetails.display_name ?? typeMetaData.label}</h6>
|
||||||
render={({ field: { onChange, value } }) => (
|
</div>
|
||||||
<>
|
<div className={`${open ? "-rotate-180" : ""} transition-all`}>
|
||||||
{numberAttributeRepresentations.map((representation) => (
|
<ChevronDown size={16} strokeWidth={1.5} rotate="180deg" />
|
||||||
<div
|
</div>
|
||||||
key={representation.key}
|
</Disclosure.Button>
|
||||||
className={`rounded divide-y w-32 cursor-pointer border ${
|
<Disclosure.Panel>
|
||||||
value === representation.key
|
<form onSubmit={handleSubmit(handleUpdateAttribute)} className="p-3 pl-9 pt-0">
|
||||||
? "border-custom-primary-100 divide-custom-primary-100"
|
<div className="space-y-3">
|
||||||
: "border-custom-border-200 divide-custom-border-200"
|
<Controller
|
||||||
}`}
|
control={control}
|
||||||
onClick={() => onChange(representation.key)}
|
name="display_name"
|
||||||
>
|
render={({ field: { onChange, value } }) => (
|
||||||
<div className="h-24 p-2.5 grid place-items-center">
|
<Input placeholder="Enter field title" value={value} onChange={onChange} />
|
||||||
<Image src={representation.image} alt={representation.label} />
|
)}
|
||||||
</div>
|
/>
|
||||||
<div className="h-9 text-xs font-medium p-2.5 bg-custom-background-100 rounded-b flex items-center justify-between gap-2">
|
<Controller
|
||||||
{representation.label}
|
control={control}
|
||||||
{value === representation.key && (
|
name="default_value"
|
||||||
<CheckCircle2
|
render={({ field: { onChange, value } }) => (
|
||||||
size={14}
|
<Input
|
||||||
strokeWidth={1.5}
|
type="number"
|
||||||
className="text-custom-primary-100"
|
placeholder="Enter default value"
|
||||||
/>
|
value={value ?? ""}
|
||||||
|
onChange={onChange}
|
||||||
|
step={1}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="my-3 border-t-[0.5px] border-custom-border-200" />
|
||||||
|
<div>
|
||||||
|
<h6 className="text-xs">Show as</h6>
|
||||||
|
<div className="mt-2 flex items-center gap-4 flex-wrap">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="extra_settings.representation"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<>
|
||||||
|
{numberAttributeRepresentations.map((representation) => (
|
||||||
|
<div
|
||||||
|
key={representation.key}
|
||||||
|
className={`rounded divide-y w-32 cursor-pointer border ${
|
||||||
|
value === representation.key
|
||||||
|
? "border-custom-primary-100 divide-custom-primary-100"
|
||||||
|
: "border-custom-border-200 divide-custom-border-200"
|
||||||
|
}`}
|
||||||
|
onClick={() => onChange(representation.key)}
|
||||||
|
>
|
||||||
|
<div className="h-24 p-2.5 grid place-items-center">
|
||||||
|
<Image src={representation.image} alt={representation.label} />
|
||||||
|
</div>
|
||||||
|
<div className="h-9 text-xs font-medium p-2.5 bg-custom-background-100 rounded-b flex items-center justify-between gap-2">
|
||||||
|
{representation.label}
|
||||||
|
{value === representation.key && (
|
||||||
|
<CheckCircle2
|
||||||
|
size={14}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
className="text-custom-primary-100"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{watch &&
|
|
||||||
(watch("extra_settings.representation") === "bar" ||
|
|
||||||
watch("extra_settings.representation") === "ring") && (
|
|
||||||
<div className="mt-6 grid grid-cols-3 gap-x-2 gap-y-3 items-center">
|
|
||||||
<>
|
|
||||||
<div className="text-xs">Divided by</div>
|
|
||||||
<div className="col-span-2">
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="extra_settings.divided_by"
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
placeholder="Maximum value"
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
className="hide-arrows"
|
|
||||||
min={0}
|
|
||||||
step={1}
|
|
||||||
defaultValue={100}
|
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{watch &&
|
||||||
|
(watch("extra_settings.representation") === "bar" ||
|
||||||
|
watch("extra_settings.representation") === "ring") && (
|
||||||
|
<div className="mt-6 grid grid-cols-3 gap-x-2 gap-y-3 items-center">
|
||||||
|
<>
|
||||||
|
<div className="text-xs">Divided by</div>
|
||||||
|
<div className="col-span-2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="extra_settings.divided_by"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="Maximum value"
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
className="hide-arrows"
|
||||||
|
min={0}
|
||||||
|
step={1}
|
||||||
|
defaultValue={100}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
<>
|
||||||
|
<div className="text-xs">Color</div>
|
||||||
|
<div className="col-span-2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="color"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<ColorPicker
|
||||||
|
onChange={onChange}
|
||||||
|
selectedColor={value ?? "#000000"}
|
||||||
|
size={18}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
<>
|
||||||
|
<div className="text-xs">Show number</div>
|
||||||
|
<div className="col-span-2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="extra_settings.show_number"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<ToggleSwitch value={value} onChange={onChange} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
<div className="mt-8 flex items-center justify-between">
|
||||||
</div>
|
<div className="flex-shrink-0 flex items-center gap-2">
|
||||||
</>
|
<Controller
|
||||||
<>
|
control={control}
|
||||||
<div className="text-xs">Color</div>
|
name="is_required"
|
||||||
<div className="col-span-2">
|
render={({ field: { onChange, value } }) => (
|
||||||
<Controller
|
<ToggleSwitch value={value ?? false} onChange={onChange} />
|
||||||
control={control}
|
)}
|
||||||
name="color"
|
/>
|
||||||
render={({ field: { onChange, value } }) => (
|
<span className="text-xs">Mandatory field</span>
|
||||||
<ColorPicker onChange={onChange} selectedColor={value ?? "#000000"} size={18} />
|
</div>
|
||||||
)}
|
<div className="flex items-center gap-2">
|
||||||
/>
|
<SecondaryButton type="button" onClick={handleDelete} loading={isRemoving}>
|
||||||
</div>
|
{isRemoving ? "Removing..." : "Remove"}
|
||||||
</>
|
</SecondaryButton>
|
||||||
<>
|
<PrimaryButton type="submit" loading={isSubmitting}>
|
||||||
<div className="text-xs">Show number</div>
|
{isSubmitting ? "Saving..." : "Save"}
|
||||||
<div className="col-span-2">
|
</PrimaryButton>
|
||||||
<Controller
|
</div>
|
||||||
control={control}
|
</div>
|
||||||
name="extra_settings.show_number"
|
</form>
|
||||||
render={({ field: { onChange, value } }) => (
|
</Disclosure.Panel>
|
||||||
<ToggleSwitch value={value} onChange={onChange} />
|
</>
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</Disclosure>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
@ -1,42 +1,90 @@
|
|||||||
// react-hook-form
|
import { useState } from "react";
|
||||||
import { Controller } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
// components
|
import { Disclosure } from "@headlessui/react";
|
||||||
import { FormComponentProps, Input } from "components/custom-attributes";
|
|
||||||
// ui
|
|
||||||
import { CustomSelect } from "components/ui";
|
|
||||||
// constants
|
|
||||||
import { CUSTOM_ATTRIBUTE_UNITS } from "constants/custom-attributes";
|
|
||||||
|
|
||||||
export const RelationAttributeForm: React.FC<FormComponentProps> = ({ control }) => (
|
// components
|
||||||
<div className="space-y-3">
|
import { Input } from "components/custom-attributes";
|
||||||
<Controller
|
// ui
|
||||||
control={control}
|
import { CustomSelect, PrimaryButton, SecondaryButton, ToggleSwitch } from "components/ui";
|
||||||
name="display_name"
|
// icons
|
||||||
render={({ field: { onChange, value } }) => (
|
import { ChevronDown } from "lucide-react";
|
||||||
<Input placeholder="Enter field title" value={value} onChange={onChange} />
|
// types
|
||||||
)}
|
import { ICustomAttribute } from "types";
|
||||||
/>
|
// constants
|
||||||
<Controller
|
import { CUSTOM_ATTRIBUTES_LIST, CUSTOM_ATTRIBUTE_UNITS } from "constants/custom-attributes";
|
||||||
control={control}
|
|
||||||
name="unit"
|
type Props = {
|
||||||
render={({ field: { onChange, value } }) => (
|
attributeDetails: ICustomAttribute;
|
||||||
<CustomSelect
|
handleDeleteAttribute: () => Promise<void>;
|
||||||
label={<span className="capitalize text-xs">{value}</span>}
|
handleUpdateAttribute: (data: Partial<ICustomAttribute>) => Promise<void>;
|
||||||
value={value}
|
};
|
||||||
onChange={onChange}
|
|
||||||
buttonClassName="bg-custom-background-100 !px-3 !py-2 !border-custom-border-200 !rounded"
|
const typeMetaData = CUSTOM_ATTRIBUTES_LIST.relation;
|
||||||
optionsClassName="w-full"
|
|
||||||
input
|
export const RelationAttributeForm: React.FC<Props> = (props) => {
|
||||||
>
|
const { attributeDetails, handleDeleteAttribute, handleUpdateAttribute } = props;
|
||||||
{CUSTOM_ATTRIBUTE_UNITS.map((unit) => (
|
|
||||||
<CustomSelect.Option key={unit.value} value={unit.value}>
|
const [isRemoving, setIsRemoving] = useState(false);
|
||||||
{unit.label}
|
|
||||||
</CustomSelect.Option>
|
const {
|
||||||
))}
|
control,
|
||||||
</CustomSelect>
|
formState: { isSubmitting },
|
||||||
)}
|
handleSubmit,
|
||||||
/>
|
} = useForm({ defaultValues: typeMetaData.defaultFormValues });
|
||||||
{/* <div>
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
setIsRemoving(true);
|
||||||
|
|
||||||
|
await handleDeleteAttribute().finally(() => setIsRemoving(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Disclosure
|
||||||
|
as="div"
|
||||||
|
className="bg-custom-background-90 border border-custom-border-200 rounded"
|
||||||
|
>
|
||||||
|
{({ open }) => (
|
||||||
|
<>
|
||||||
|
<Disclosure.Button className="p-3 flex items-center justify-between gap-1 w-full">
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<typeMetaData.icon size={14} strokeWidth={1.5} />
|
||||||
|
<h6 className="text-sm">{attributeDetails.display_name ?? typeMetaData.label}</h6>
|
||||||
|
</div>
|
||||||
|
<div className={`${open ? "-rotate-180" : ""} transition-all`}>
|
||||||
|
<ChevronDown size={16} strokeWidth={1.5} rotate="180deg" />
|
||||||
|
</div>
|
||||||
|
</Disclosure.Button>
|
||||||
|
<Disclosure.Panel>
|
||||||
|
<form onSubmit={handleSubmit(handleUpdateAttribute)} className="p-3 pl-9 pt-0">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="display_name"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<Input placeholder="Enter field title" value={value} onChange={onChange} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="unit"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<CustomSelect
|
||||||
|
label={<span className="capitalize text-xs">{value}</span>}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
buttonClassName="bg-custom-background-100 !px-3 !py-2 !border-custom-border-200 !rounded"
|
||||||
|
optionsClassName="w-full"
|
||||||
|
input
|
||||||
|
>
|
||||||
|
{CUSTOM_ATTRIBUTE_UNITS.map((unit) => (
|
||||||
|
<CustomSelect.Option key={unit.value} value={unit.value}>
|
||||||
|
{unit.label}
|
||||||
|
</CustomSelect.Option>
|
||||||
|
))}
|
||||||
|
</CustomSelect>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{/* <div>
|
||||||
<p className="text-xs">Selection type</p>
|
<p className="text-xs">Selection type</p>
|
||||||
<div className="mt-2 flex items-center gap-6 accent-custom-primary-100">
|
<div className="mt-2 flex items-center gap-6 accent-custom-primary-100">
|
||||||
<div className="flex items-center gap-1 text-xs">
|
<div className="flex items-center gap-1 text-xs">
|
||||||
@ -57,5 +105,31 @@ export const RelationAttributeForm: React.FC<FormComponentProps> = ({ control })
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div> */}
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
);
|
<div className="mt-8 flex items-center justify-between">
|
||||||
|
<div className="flex-shrink-0 flex items-center gap-2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="is_required"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<ToggleSwitch value={value ?? false} onChange={onChange} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span className="text-xs">Mandatory field</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<SecondaryButton type="button" onClick={handleDelete} loading={isRemoving}>
|
||||||
|
{isRemoving ? "Removing..." : "Remove"}
|
||||||
|
</SecondaryButton>
|
||||||
|
<PrimaryButton type="submit" loading={isSubmitting}>
|
||||||
|
{isSubmitting ? "Saving..." : "Save"}
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Disclosure.Panel>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Disclosure>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -1,54 +1,142 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
// mobx
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { Disclosure } from "@headlessui/react";
|
||||||
|
|
||||||
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// react-hook-form
|
|
||||||
import { Controller } from "react-hook-form";
|
|
||||||
// components
|
// components
|
||||||
import { FormComponentProps, Input, OptionForm, SelectOption } from "components/custom-attributes";
|
import { Input, OptionForm, SelectOption } from "components/custom-attributes";
|
||||||
|
// ui
|
||||||
|
import { PrimaryButton, SecondaryButton, ToggleSwitch } from "components/ui";
|
||||||
|
// icons
|
||||||
|
import { ChevronDown } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import { ICustomAttribute } from "types";
|
import { ICustomAttribute } from "types";
|
||||||
|
// constants
|
||||||
|
import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
|
||||||
|
|
||||||
export const SelectAttributeForm: React.FC<FormComponentProps & { multiple?: boolean }> = observer(
|
type Props = {
|
||||||
({ control, multiple = false, objectId = "", watch }) => {
|
attributeDetails: ICustomAttribute;
|
||||||
const [optionToEdit, setOptionToEdit] = useState<ICustomAttribute | null>(null);
|
handleDeleteAttribute: () => Promise<void>;
|
||||||
|
handleUpdateAttribute: (data: Partial<ICustomAttribute>) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
const { customAttributes } = useMobxStore();
|
const typeMetaData = CUSTOM_ATTRIBUTES_LIST.select;
|
||||||
|
|
||||||
const options = customAttributes.objectAttributes?.[objectId]?.[watch("id") ?? ""]?.children;
|
export const SelectAttributeForm: React.FC<Props & { multiple?: boolean }> = observer((props) => {
|
||||||
|
const {
|
||||||
|
attributeDetails,
|
||||||
|
handleDeleteAttribute,
|
||||||
|
handleUpdateAttribute,
|
||||||
|
multiple = false,
|
||||||
|
} = props;
|
||||||
|
|
||||||
return (
|
const [optionToEdit, setOptionToEdit] = useState<ICustomAttribute | null>(null);
|
||||||
<div className="space-y-3">
|
const [isRemoving, setIsRemoving] = useState(false);
|
||||||
<Controller
|
|
||||||
control={control}
|
const { customAttributes } = useMobxStore();
|
||||||
name="display_name"
|
|
||||||
render={({ field: { onChange, value } }) => (
|
const {
|
||||||
<Input placeholder="Enter field title" value={value} onChange={onChange} />
|
control,
|
||||||
)}
|
formState: { isSubmitting },
|
||||||
/>
|
handleSubmit,
|
||||||
<div>
|
reset,
|
||||||
<p className="text-xs">Options</p>
|
watch,
|
||||||
<div className="mt-3 space-y-2 w-3/5">
|
} = useForm({ defaultValues: typeMetaData.defaultFormValues });
|
||||||
{options?.map((option) => (
|
|
||||||
<SelectOption
|
const options =
|
||||||
key={option.id}
|
customAttributes.objectAttributes?.[attributeDetails.parent ?? ""]?.[watch("id") ?? ""]
|
||||||
handleEditOption={() => setOptionToEdit(option)}
|
?.children;
|
||||||
objectId={objectId}
|
|
||||||
option={option}
|
const handleDelete = async () => {
|
||||||
/>
|
setIsRemoving(true);
|
||||||
))}
|
|
||||||
</div>
|
await handleDeleteAttribute().finally(() => setIsRemoving(false));
|
||||||
<div className="mt-2 w-3/5">
|
};
|
||||||
<OptionForm
|
|
||||||
data={optionToEdit}
|
useEffect(() => {
|
||||||
objectId={objectId}
|
if (!attributeDetails) return;
|
||||||
onSubmit={() => setOptionToEdit(null)}
|
|
||||||
parentId={watch("id") ?? ""}
|
reset({
|
||||||
/>
|
...typeMetaData.defaultFormValues,
|
||||||
</div>
|
...attributeDetails,
|
||||||
</div>
|
});
|
||||||
</div>
|
}, [attributeDetails, reset]);
|
||||||
);
|
|
||||||
}
|
return (
|
||||||
);
|
<Disclosure
|
||||||
|
as="div"
|
||||||
|
className="bg-custom-background-90 border border-custom-border-200 rounded"
|
||||||
|
>
|
||||||
|
{({ open }) => (
|
||||||
|
<>
|
||||||
|
<Disclosure.Button className="p-3 flex items-center justify-between gap-1 w-full">
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<typeMetaData.icon size={14} strokeWidth={1.5} />
|
||||||
|
<h6 className="text-sm">{attributeDetails.display_name ?? typeMetaData.label}</h6>
|
||||||
|
</div>
|
||||||
|
<div className={`${open ? "-rotate-180" : ""} transition-all`}>
|
||||||
|
<ChevronDown size={16} strokeWidth={1.5} rotate="180deg" />
|
||||||
|
</div>
|
||||||
|
</Disclosure.Button>
|
||||||
|
<Disclosure.Panel>
|
||||||
|
<form onSubmit={handleSubmit(handleUpdateAttribute)} className="p-3 pl-9 pt-0">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="display_name"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<Input placeholder="Enter field title" value={value} onChange={onChange} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs">Options</p>
|
||||||
|
<div className="mt-3 space-y-2 w-3/5">
|
||||||
|
{options?.map((option) => (
|
||||||
|
<SelectOption
|
||||||
|
key={option.id}
|
||||||
|
handleEditOption={() => setOptionToEdit(option)}
|
||||||
|
objectId={attributeDetails.parent ?? ""}
|
||||||
|
option={option}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 w-3/5">
|
||||||
|
<OptionForm
|
||||||
|
data={optionToEdit}
|
||||||
|
objectId={attributeDetails.parent ?? ""}
|
||||||
|
onSubmit={() => setOptionToEdit(null)}
|
||||||
|
parentId={watch("id") ?? ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 flex items-center justify-between">
|
||||||
|
<div className="flex-shrink-0 flex items-center gap-2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="is_required"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<ToggleSwitch value={value ?? false} onChange={onChange} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span className="text-xs">Mandatory field</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<SecondaryButton type="button" onClick={handleDelete} loading={isRemoving}>
|
||||||
|
{isRemoving ? "Removing..." : "Remove"}
|
||||||
|
</SecondaryButton>
|
||||||
|
<PrimaryButton type="submit" loading={isSubmitting}>
|
||||||
|
{isSubmitting ? "Saving..." : "Save"}
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Disclosure.Panel>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Disclosure>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
@ -1,23 +1,105 @@
|
|||||||
// react-hook-form
|
import { useState } from "react";
|
||||||
import { Controller } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
// ui
|
import { Disclosure } from "@headlessui/react";
|
||||||
import { FormComponentProps, Input } from "components/custom-attributes";
|
|
||||||
|
|
||||||
export const TextAttributeForm: React.FC<FormComponentProps> = ({ control }) => (
|
// components
|
||||||
<div className="space-y-3">
|
import { Input } from "components/custom-attributes";
|
||||||
<Controller
|
// ui
|
||||||
control={control}
|
import { PrimaryButton, SecondaryButton, ToggleSwitch } from "components/ui";
|
||||||
name="display_name"
|
// icons
|
||||||
render={({ field: { onChange, value } }) => (
|
import { ChevronDown } from "lucide-react";
|
||||||
<Input placeholder="Enter field title" value={value} onChange={onChange} />
|
// types
|
||||||
|
import { ICustomAttribute } from "types";
|
||||||
|
// constants
|
||||||
|
import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
attributeDetails: ICustomAttribute;
|
||||||
|
handleDeleteAttribute: () => Promise<void>;
|
||||||
|
handleUpdateAttribute: (data: Partial<ICustomAttribute>) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const typeMetaData = CUSTOM_ATTRIBUTES_LIST.text;
|
||||||
|
|
||||||
|
export const TextAttributeForm: React.FC<Props> = (props) => {
|
||||||
|
const { attributeDetails, handleDeleteAttribute, handleUpdateAttribute } = props;
|
||||||
|
|
||||||
|
const [isRemoving, setIsRemoving] = useState(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
formState: { isSubmitting },
|
||||||
|
handleSubmit,
|
||||||
|
} = useForm({ defaultValues: typeMetaData.defaultFormValues });
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
setIsRemoving(true);
|
||||||
|
|
||||||
|
await handleDeleteAttribute().finally(() => setIsRemoving(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Disclosure
|
||||||
|
as="div"
|
||||||
|
className="bg-custom-background-90 border border-custom-border-200 rounded"
|
||||||
|
>
|
||||||
|
{({ open }) => (
|
||||||
|
<>
|
||||||
|
<Disclosure.Button className="p-3 flex items-center justify-between gap-1 w-full">
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<typeMetaData.icon size={14} strokeWidth={1.5} />
|
||||||
|
<h6 className="text-sm">{attributeDetails.display_name ?? typeMetaData.label}</h6>
|
||||||
|
</div>
|
||||||
|
<div className={`${open ? "-rotate-180" : ""} transition-all`}>
|
||||||
|
<ChevronDown size={16} strokeWidth={1.5} rotate="180deg" />
|
||||||
|
</div>
|
||||||
|
</Disclosure.Button>
|
||||||
|
<Disclosure.Panel>
|
||||||
|
<form onSubmit={handleSubmit(handleUpdateAttribute)} className="p-3 pl-9 pt-0">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="display_name"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<Input placeholder="Enter field title" value={value} onChange={onChange} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="default_value"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<Input
|
||||||
|
placeholder="Enter default value"
|
||||||
|
value={value ?? ""}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-8 flex items-center justify-between">
|
||||||
|
<div className="flex-shrink-0 flex items-center gap-2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="is_required"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<ToggleSwitch value={value ?? false} onChange={onChange} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span className="text-xs">Mandatory field</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<SecondaryButton type="button" onClick={handleDelete} loading={isRemoving}>
|
||||||
|
{isRemoving ? "Removing..." : "Remove"}
|
||||||
|
</SecondaryButton>
|
||||||
|
<PrimaryButton type="submit" loading={isSubmitting}>
|
||||||
|
{isSubmitting ? "Saving..." : "Save"}
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Disclosure.Panel>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
</Disclosure>
|
||||||
<Controller
|
);
|
||||||
control={control}
|
};
|
||||||
name="default_value"
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<Input placeholder="Enter default value" value={value ?? ""} onChange={onChange} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
@ -1,23 +1,106 @@
|
|||||||
// react-hook-form
|
import { useState } from "react";
|
||||||
import { Controller } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
// components
|
import { Disclosure } from "@headlessui/react";
|
||||||
import { FormComponentProps, Input } from "components/custom-attributes";
|
|
||||||
|
|
||||||
export const UrlAttributeForm: React.FC<FormComponentProps> = ({ control }) => (
|
// components
|
||||||
<div className="space-y-3">
|
import { Input } from "components/custom-attributes";
|
||||||
<Controller
|
// ui
|
||||||
control={control}
|
import { PrimaryButton, SecondaryButton, ToggleSwitch } from "components/ui";
|
||||||
name="display_name"
|
// icons
|
||||||
render={({ field: { onChange, value } }) => (
|
import { ChevronDown } from "lucide-react";
|
||||||
<Input placeholder="Enter field title" value={value} onChange={onChange} />
|
// types
|
||||||
|
import { ICustomAttribute } from "types";
|
||||||
|
// constants
|
||||||
|
import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
attributeDetails: ICustomAttribute;
|
||||||
|
handleDeleteAttribute: () => Promise<void>;
|
||||||
|
handleUpdateAttribute: (data: Partial<ICustomAttribute>) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const typeMetaData = CUSTOM_ATTRIBUTES_LIST.url;
|
||||||
|
|
||||||
|
export const UrlAttributeForm: React.FC<Props> = (props) => {
|
||||||
|
const { attributeDetails, handleDeleteAttribute, handleUpdateAttribute } = props;
|
||||||
|
|
||||||
|
const [isRemoving, setIsRemoving] = useState(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
formState: { isSubmitting },
|
||||||
|
handleSubmit,
|
||||||
|
} = useForm({ defaultValues: typeMetaData.defaultFormValues });
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
setIsRemoving(true);
|
||||||
|
|
||||||
|
await handleDeleteAttribute().finally(() => setIsRemoving(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Disclosure
|
||||||
|
as="div"
|
||||||
|
className="bg-custom-background-90 border border-custom-border-200 rounded"
|
||||||
|
>
|
||||||
|
{({ open }) => (
|
||||||
|
<>
|
||||||
|
<Disclosure.Button className="p-3 flex items-center justify-between gap-1 w-full">
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<typeMetaData.icon size={14} strokeWidth={1.5} />
|
||||||
|
<h6 className="text-sm">{attributeDetails.display_name ?? typeMetaData.label}</h6>
|
||||||
|
</div>
|
||||||
|
<div className={`${open ? "-rotate-180" : ""} transition-all`}>
|
||||||
|
<ChevronDown size={16} strokeWidth={1.5} rotate="180deg" />
|
||||||
|
</div>
|
||||||
|
</Disclosure.Button>
|
||||||
|
<Disclosure.Panel>
|
||||||
|
<form onSubmit={handleSubmit(handleUpdateAttribute)} className="p-3 pl-9 pt-0">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="display_name"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<Input placeholder="Enter field title" value={value} onChange={onChange} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="default_value"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<Input
|
||||||
|
type="url"
|
||||||
|
placeholder="Enter default URL"
|
||||||
|
value={value ?? ""}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-8 flex items-center justify-between">
|
||||||
|
<div className="flex-shrink-0 flex items-center gap-2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="is_required"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<ToggleSwitch value={value ?? false} onChange={onChange} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span className="text-xs">Mandatory field</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<SecondaryButton type="button" onClick={handleDelete} loading={isRemoving}>
|
||||||
|
{isRemoving ? "Removing..." : "Remove"}
|
||||||
|
</SecondaryButton>
|
||||||
|
<PrimaryButton type="submit" loading={isSubmitting}>
|
||||||
|
{isSubmitting ? "Saving..." : "Save"}
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Disclosure.Panel>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
</Disclosure>
|
||||||
<Controller
|
);
|
||||||
control={control}
|
};
|
||||||
name="default_value"
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<Input type="url" placeholder="Enter default URL" value={value ?? ""} onChange={onChange} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
@ -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]) => (
|
||||||
|
@ -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 }) => (
|
||||||
|
@ -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 ? (
|
||||||
|
@ -1,206 +1,239 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect } from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
// mobx
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
// headless ui
|
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// components
|
// components
|
||||||
import { Input, TypesDropdown, AttributeForm } from "components/custom-attributes";
|
import { Input, TypesDropdown, AttributeForm } from "components/custom-attributes";
|
||||||
|
import EmojiIconPicker from "components/emoji-icon-picker";
|
||||||
// ui
|
// ui
|
||||||
import { Loader, PrimaryButton, SecondaryButton } from "components/ui";
|
import { Loader, PrimaryButton, SecondaryButton } from "components/ui";
|
||||||
|
// helpers
|
||||||
|
import { renderEmoji } from "helpers/emoji.helper";
|
||||||
// types
|
// types
|
||||||
import { ICustomAttribute, TCustomAttributeTypes } from "types";
|
import { ICustomAttribute, TCustomAttributeTypes } from "types";
|
||||||
// constants
|
// constants
|
||||||
import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
|
import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
|
||||||
import { renderEmoji } from "helpers/emoji.helper";
|
|
||||||
import EmojiIconPicker from "components/emoji-icon-picker";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
objectIdToEdit?: string | null;
|
data?: ICustomAttribute;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSubmit?: () => Promise<void>;
|
onSubmit?: () => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ObjectModal: React.FC<Props> = observer(
|
const defaultValues: Partial<ICustomAttribute> = {
|
||||||
({ objectIdToEdit, isOpen, onClose, onSubmit }) => {
|
display_name: "",
|
||||||
const [object, setObject] = useState<Partial<ICustomAttribute>>({
|
description: "",
|
||||||
display_name: "",
|
icon: "",
|
||||||
description: "",
|
};
|
||||||
|
|
||||||
|
export const ObjectModal: React.FC<Props> = observer((props) => {
|
||||||
|
const { data, isOpen, onClose, onSubmit } = props;
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
formState: { isSubmitting },
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
setValue,
|
||||||
|
watch,
|
||||||
|
} = useForm<ICustomAttribute>({ defaultValues });
|
||||||
|
|
||||||
|
const objectId = watch("id") && watch("id") !== "" ? watch("id") : null;
|
||||||
|
|
||||||
|
const { customAttributes } = useMobxStore();
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
onClose();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
reset({ ...defaultValues });
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateObject = async (formData: Partial<ICustomAttribute>) => {
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
const payload: Partial<ICustomAttribute> = {
|
||||||
|
description: formData.description ?? "",
|
||||||
|
display_name: formData.display_name ?? "",
|
||||||
|
icon: formData.icon ?? "",
|
||||||
|
project: projectId.toString(),
|
||||||
|
type: "entity",
|
||||||
|
};
|
||||||
|
|
||||||
|
await customAttributes
|
||||||
|
.createObject(workspaceSlug.toString(), payload)
|
||||||
|
.then((res) => setValue("id", res?.id ?? ""));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateObject = async (formData: Partial<ICustomAttribute>) => {
|
||||||
|
if (!workspaceSlug || !data || !data.id) return;
|
||||||
|
|
||||||
|
const payload: Partial<ICustomAttribute> = {
|
||||||
|
description: formData.description ?? "",
|
||||||
|
display_name: formData.display_name ?? "",
|
||||||
|
icon: formData.icon ?? "",
|
||||||
|
};
|
||||||
|
|
||||||
|
await customAttributes.updateObject(workspaceSlug.toString(), data.id, payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleObjectFormSubmit = async (formData: Partial<ICustomAttribute>) => {
|
||||||
|
if (data) await handleUpdateObject(formData);
|
||||||
|
else await handleCreateObject(formData);
|
||||||
|
|
||||||
|
if (onSubmit) onSubmit();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateObjectAttribute = async (type: TCustomAttributeTypes) => {
|
||||||
|
if (!workspaceSlug || !objectId) return;
|
||||||
|
|
||||||
|
const typeMetaData = CUSTOM_ATTRIBUTES_LIST[type];
|
||||||
|
|
||||||
|
const payload: Partial<ICustomAttribute> = {
|
||||||
|
display_name: typeMetaData.label,
|
||||||
|
type,
|
||||||
|
...typeMetaData.initialPayload,
|
||||||
|
};
|
||||||
|
|
||||||
|
await customAttributes.createObjectAttribute(workspaceSlug.toString(), {
|
||||||
|
...payload,
|
||||||
|
parent: objectId,
|
||||||
});
|
});
|
||||||
const [isCreatingObject, setIsCreatingObject] = useState(false);
|
};
|
||||||
const [isUpdatingObject, setIsUpdatingObject] = useState(false);
|
|
||||||
|
|
||||||
const router = useRouter();
|
// fetch the object details if object state has id
|
||||||
const { workspaceSlug, projectId } = router.query;
|
useEffect(() => {
|
||||||
|
if (!objectId) return;
|
||||||
|
|
||||||
const { customAttributes } = useMobxStore();
|
if (!customAttributes.objectAttributes[objectId]) {
|
||||||
|
if (!workspaceSlug) return;
|
||||||
|
|
||||||
const handleClose = () => {
|
customAttributes.fetchObjectDetails(workspaceSlug.toString(), objectId).then((res) => {
|
||||||
onClose();
|
reset({ ...res });
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
setObject({ display_name: "", description: "" });
|
|
||||||
}, 300);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCreateObject = async () => {
|
|
||||||
if (!workspaceSlug || !projectId) return;
|
|
||||||
|
|
||||||
setIsCreatingObject(true);
|
|
||||||
|
|
||||||
const payload: Partial<ICustomAttribute> = {
|
|
||||||
description: object.description ?? "",
|
|
||||||
display_name: object.display_name ?? "",
|
|
||||||
icon: object.icon ?? "",
|
|
||||||
project: projectId.toString(),
|
|
||||||
type: "entity",
|
|
||||||
};
|
|
||||||
|
|
||||||
await customAttributes
|
|
||||||
.createObject(workspaceSlug.toString(), payload)
|
|
||||||
.then((res) => {
|
|
||||||
setObject((prevData) => ({ ...prevData, ...res }));
|
|
||||||
if (onSubmit) onSubmit();
|
|
||||||
})
|
|
||||||
.finally(() => setIsCreatingObject(false));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpdateObject = async () => {
|
|
||||||
if (!workspaceSlug || !object || !object.id) return;
|
|
||||||
|
|
||||||
setIsUpdatingObject(true);
|
|
||||||
|
|
||||||
const payload: Partial<ICustomAttribute> = {
|
|
||||||
description: object.description ?? "",
|
|
||||||
display_name: object.display_name ?? "",
|
|
||||||
icon: object.icon ?? "",
|
|
||||||
};
|
|
||||||
|
|
||||||
await customAttributes
|
|
||||||
.updateObject(workspaceSlug.toString(), object.id, payload)
|
|
||||||
.finally(() => setIsUpdatingObject(false));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCreateObjectAttribute = async (type: TCustomAttributeTypes) => {
|
|
||||||
if (!workspaceSlug || !object || !object.id) return;
|
|
||||||
|
|
||||||
const typeMetaData = CUSTOM_ATTRIBUTES_LIST[type];
|
|
||||||
|
|
||||||
const payload: Partial<ICustomAttribute> = {
|
|
||||||
display_name: typeMetaData.label,
|
|
||||||
type,
|
|
||||||
...typeMetaData.initialPayload,
|
|
||||||
};
|
|
||||||
|
|
||||||
await customAttributes.createObjectAttribute(workspaceSlug.toString(), {
|
|
||||||
...payload,
|
|
||||||
parent: object.id,
|
|
||||||
});
|
});
|
||||||
};
|
} else {
|
||||||
|
reset({
|
||||||
|
...customAttributes.objects?.find((e) => e.id === objectId),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [customAttributes, objectId, reset, workspaceSlug]);
|
||||||
|
|
||||||
// fetch the object details if object state has id
|
// update the form if data is present
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!object.id || object.id === "") return;
|
if (!data) return;
|
||||||
|
|
||||||
if (!customAttributes.objectAttributes[object.id]) {
|
reset({
|
||||||
if (!workspaceSlug) return;
|
...defaultValues,
|
||||||
|
...data,
|
||||||
|
});
|
||||||
|
}, [data, reset]);
|
||||||
|
|
||||||
customAttributes.fetchObjectDetails(workspaceSlug.toString(), object.id).then((res) => {
|
return (
|
||||||
setObject((prev) => ({ ...prev, ...res }));
|
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||||
});
|
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||||
} else {
|
<Transition.Child
|
||||||
setObject((prev) => ({
|
as={React.Fragment}
|
||||||
...prev,
|
enter="ease-out duration-300"
|
||||||
...customAttributes.objects?.find((e) => e.id === object.id),
|
enterFrom="opacity-0"
|
||||||
}));
|
enterTo="opacity-100"
|
||||||
}
|
leave="ease-in duration-200"
|
||||||
}, [customAttributes, object.id, workspaceSlug]);
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
|
||||||
|
</Transition.Child>
|
||||||
|
|
||||||
// update the object state if objectIdToEdit is present
|
<Transition.Child
|
||||||
useEffect(() => {
|
as={React.Fragment}
|
||||||
if (!objectIdToEdit) return;
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
setObject((prev) => ({
|
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||||
...prev,
|
leave="ease-in duration-200"
|
||||||
id: objectIdToEdit,
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
}));
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
}, [objectIdToEdit]);
|
>
|
||||||
|
<Dialog.Panel className="fixed inset-0 h-full w-full z-20">
|
||||||
return (
|
<div className="flex items-center justify-center h-full w-full p-4 sm:p-0 scale-90">
|
||||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
<div className="bg-custom-background-100 w-1/2 max-h-[85%] flex flex-col rounded-xl">
|
||||||
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
<h3 className="text-2xl font-semibold px-6 pt-5">New Object</h3>
|
||||||
<Transition.Child
|
<div className="mt-5 space-y-5 h-full overflow-y-auto">
|
||||||
as={React.Fragment}
|
<form
|
||||||
enter="ease-out duration-300"
|
onSubmit={handleSubmit(handleObjectFormSubmit)}
|
||||||
enterFrom="opacity-0"
|
className="space-y-4 px-6 pb-5"
|
||||||
enterTo="opacity-100"
|
>
|
||||||
leave="ease-in duration-200"
|
<div className="flex items-center gap-2">
|
||||||
leaveFrom="opacity-100"
|
<div className="h-9 w-9 bg-custom-background-80 grid place-items-center rounded">
|
||||||
leaveTo="opacity-0"
|
<Controller
|
||||||
>
|
control={control}
|
||||||
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
|
name="icon"
|
||||||
</Transition.Child>
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<EmojiIconPicker
|
||||||
<Transition.Child
|
label={value ? renderEmoji(value) : "Icon"}
|
||||||
as={React.Fragment}
|
onChange={(icon) => {
|
||||||
enter="ease-out duration-300"
|
if (typeof icon === "string") onChange(icon);
|
||||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
}}
|
||||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
value={value}
|
||||||
leave="ease-in duration-200"
|
showIconPicker={false}
|
||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
/>
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
)}
|
||||||
>
|
|
||||||
<Dialog.Panel className="fixed inset-0 h-full w-full z-20">
|
|
||||||
<div className="flex items-center justify-center h-full w-full p-4 sm:p-0 scale-90">
|
|
||||||
<div className="bg-custom-background-100 w-1/2 max-h-[85%] flex flex-col rounded-xl">
|
|
||||||
<h3 className="text-2xl font-semibold px-6 pt-5">New Object</h3>
|
|
||||||
<div className="mt-5 space-y-5 h-full overflow-y-auto">
|
|
||||||
<div className="space-y-4 px-6">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="h-9 w-9 bg-custom-background-80 grid place-items-center rounded">
|
|
||||||
<EmojiIconPicker
|
|
||||||
label={object.icon ? renderEmoji(object.icon) : "Icon"}
|
|
||||||
onChange={(icon) => {
|
|
||||||
if (typeof icon === "string")
|
|
||||||
setObject((prevData) => ({ ...prevData, icon }));
|
|
||||||
}}
|
|
||||||
value={object.icon}
|
|
||||||
showIconPicker={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Input
|
|
||||||
placeholder="Enter Object Title"
|
|
||||||
value={object.display_name}
|
|
||||||
onChange={(e) =>
|
|
||||||
setObject((prevData) => ({ ...prevData, display_name: e.target.value }))
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<Controller
|
||||||
name="objectDescription"
|
control={control}
|
||||||
id="objectDescription"
|
name="display_name"
|
||||||
className="placeholder:text-custom-text-400 text-xs px-3 py-2 rounded bg-custom-background-100 border border-custom-border-200 w-full focus:outline-none"
|
render={({ field: { onChange, value } }) => (
|
||||||
cols={30}
|
<Input
|
||||||
rows={5}
|
placeholder="Enter Object Title"
|
||||||
placeholder="Enter Object Description"
|
value={value}
|
||||||
value={object.description}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
onChange={(e) =>
|
/>
|
||||||
setObject((prevData) => ({ ...prevData, description: e.target.value }))
|
)}
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
{object.id && (
|
|
||||||
<div className="text-right">
|
|
||||||
<PrimaryButton onClick={handleUpdateObject} loading={isUpdatingObject}>
|
|
||||||
{isUpdatingObject ? "Saving..." : "Save changes"}
|
|
||||||
</PrimaryButton>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{object.id && (
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="description"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<textarea
|
||||||
|
name="objectDescription"
|
||||||
|
id="objectDescription"
|
||||||
|
className="placeholder:text-custom-text-400 text-xs px-3 py-2 rounded bg-custom-background-100 border border-custom-border-200 w-full focus:outline-none"
|
||||||
|
cols={30}
|
||||||
|
rows={5}
|
||||||
|
placeholder="Enter Object Description"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center justify-end gap-3">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{!objectId && (
|
||||||
|
<SecondaryButton onClick={handleClose}>Close</SecondaryButton>
|
||||||
|
)}
|
||||||
|
<PrimaryButton type="submit" loading={isSubmitting}>
|
||||||
|
{data
|
||||||
|
? isSubmitting
|
||||||
|
? "Saving..."
|
||||||
|
: "Save changes"
|
||||||
|
: isSubmitting
|
||||||
|
? "Creating..."
|
||||||
|
: "Create Object"}
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{objectId && (
|
||||||
|
<>
|
||||||
<div className="px-6 pb-5">
|
<div className="px-6 pb-5">
|
||||||
<h4 className="font-medium">Attributes</h4>
|
<h4 className="font-medium">Attributes</h4>
|
||||||
<div className="mt-2 space-y-2">
|
<div className="mt-2 space-y-2">
|
||||||
@ -209,16 +242,16 @@ export const ObjectModal: React.FC<Props> = observer(
|
|||||||
<Loader.Item height="40px" />
|
<Loader.Item height="40px" />
|
||||||
</Loader>
|
</Loader>
|
||||||
) : (
|
) : (
|
||||||
Object.keys(customAttributes.objectAttributes[object.id] ?? {})?.map(
|
Object.keys(customAttributes.objectAttributes[objectId] ?? {})?.map(
|
||||||
(attributeId) => {
|
(attributeId) => {
|
||||||
const attribute =
|
const attribute =
|
||||||
customAttributes.objectAttributes[object.id ?? ""][attributeId];
|
customAttributes.objectAttributes[objectId][attributeId];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AttributeForm
|
<AttributeForm
|
||||||
key={attributeId}
|
key={attributeId}
|
||||||
attributeDetails={attribute}
|
attributeDetails={attribute}
|
||||||
objectId={object.id ?? ""}
|
objectId={objectId}
|
||||||
type={attribute.type}
|
type={attribute.type}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -232,33 +265,20 @@ export const ObjectModal: React.FC<Props> = observer(
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="flex items-center justify-between gap-3 px-6 py-5 border-t border-custom-border-200">
|
||||||
</div>
|
<div className="flex-shrink-0">
|
||||||
<div
|
<TypesDropdown onClick={handleCreateObjectAttribute} />
|
||||||
className={`flex items-center gap-3 px-6 py-5 border-t border-custom-border-200 ${
|
</div>
|
||||||
object.id ? "justify-between" : "justify-end"
|
<SecondaryButton onClick={handleClose}>Close</SecondaryButton>
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{object.id && (
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<TypesDropdown onClick={handleCreateObjectAttribute} />
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</>
|
||||||
<div className="flex items-center gap-3">
|
)}
|
||||||
<SecondaryButton onClick={handleClose}>Close</SecondaryButton>
|
|
||||||
{!object.id && (
|
|
||||||
<PrimaryButton onClick={handleCreateObject} loading={isCreatingObject}>
|
|
||||||
{isCreatingObject ? "Creating..." : "Create Object"}
|
|
||||||
</PrimaryButton>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Panel>
|
</div>
|
||||||
</Transition.Child>
|
</Dialog.Panel>
|
||||||
</Dialog>
|
</Transition.Child>
|
||||||
</Transition.Root>
|
</Dialog>
|
||||||
);
|
</Transition.Root>
|
||||||
}
|
);
|
||||||
);
|
});
|
||||||
|
@ -1,38 +1,26 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
// mobx
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// components
|
// components
|
||||||
import { DeleteObjectModal, SingleObject } from "components/custom-attributes";
|
import { SingleObject } from "components/custom-attributes";
|
||||||
// ui
|
// ui
|
||||||
import { EmptyState, Loader } from "components/ui";
|
import { EmptyState, Loader } from "components/ui";
|
||||||
// assets
|
// assets
|
||||||
import emptyCustomObjects from "public/empty-state/custom-objects.svg";
|
import emptyCustomObjects from "public/empty-state/custom-objects.svg";
|
||||||
// types
|
|
||||||
import { ICustomAttribute } from "types";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
handleEditObject: (object: ICustomAttribute) => void;
|
|
||||||
projectId: string;
|
projectId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ObjectsList: React.FC<Props> = observer(({ handleEditObject, projectId }) => {
|
export const ObjectsList: React.FC<Props> = observer(({ projectId }) => {
|
||||||
const [deleteObjectModal, setDeleteObjectModal] = useState(false);
|
|
||||||
const [objectToDelete, setObjectToDelete] = useState<ICustomAttribute | null>(null);
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
const { customAttributes } = useMobxStore();
|
const { customAttributes } = useMobxStore();
|
||||||
|
|
||||||
const handleDeleteObject = async (object: ICustomAttribute) => {
|
|
||||||
setObjectToDelete(object);
|
|
||||||
setDeleteObjectModal(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!workspaceSlug) return;
|
if (!workspaceSlug) return;
|
||||||
|
|
||||||
@ -41,47 +29,27 @@ export const ObjectsList: React.FC<Props> = observer(({ handleEditObject, projec
|
|||||||
}, [customAttributes, projectId, workspaceSlug]);
|
}, [customAttributes, projectId, workspaceSlug]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="divide-y divide-custom-border-100">
|
||||||
<DeleteObjectModal
|
{customAttributes.objects ? (
|
||||||
isOpen={deleteObjectModal}
|
customAttributes.objects.length > 0 ? (
|
||||||
objectToDelete={objectToDelete}
|
customAttributes.objects.map((object) => <SingleObject key={object.id} object={object} />)
|
||||||
onClose={() => {
|
|
||||||
setDeleteObjectModal(false);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
setObjectToDelete(null);
|
|
||||||
}, 300);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="divide-y divide-custom-border-100">
|
|
||||||
{customAttributes.objects ? (
|
|
||||||
customAttributes.objects.length > 0 ? (
|
|
||||||
customAttributes.objects.map((object) => (
|
|
||||||
<SingleObject
|
|
||||||
key={object.id}
|
|
||||||
object={object}
|
|
||||||
handleDeleteObject={() => handleDeleteObject(object)}
|
|
||||||
handleEditObject={() => handleEditObject(object)}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className="bg-custom-background-90 border border-custom-border-100 rounded max-w-3xl mt-10 mx-auto">
|
|
||||||
<EmptyState
|
|
||||||
title="No custom objects yet"
|
|
||||||
description="You can think of Pages as an AI-powered notepad."
|
|
||||||
image={emptyCustomObjects}
|
|
||||||
isFullScreen={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
) : (
|
) : (
|
||||||
<Loader className="space-y-4">
|
<div className="bg-custom-background-90 border border-custom-border-100 rounded max-w-3xl mt-10 mx-auto">
|
||||||
<Loader.Item height="60px" />
|
<EmptyState
|
||||||
<Loader.Item height="60px" />
|
title="No custom objects yet"
|
||||||
<Loader.Item height="60px" />
|
description="You can think of Pages as an AI-powered notepad."
|
||||||
</Loader>
|
image={emptyCustomObjects}
|
||||||
)}
|
isFullScreen={false}
|
||||||
</div>
|
/>
|
||||||
</>
|
</div>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<Loader className="space-y-4">
|
||||||
|
<Loader.Item height="60px" />
|
||||||
|
<Loader.Item height="60px" />
|
||||||
|
<Loader.Item height="60px" />
|
||||||
|
</Loader>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { DeleteObjectModal, ObjectModal } from "components/custom-attributes";
|
||||||
// ui
|
// ui
|
||||||
import { CustomMenu } from "components/ui";
|
import { CustomMenu } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
@ -9,32 +13,49 @@ import { ICustomAttribute } from "types";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
object: ICustomAttribute;
|
object: ICustomAttribute;
|
||||||
handleDeleteObject: () => void;
|
|
||||||
handleEditObject: () => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SingleObject: React.FC<Props> = (props) => {
|
export const SingleObject: React.FC<Props> = (props) => {
|
||||||
const { object, handleDeleteObject, handleEditObject } = props;
|
const { object } = props;
|
||||||
|
|
||||||
|
const [isEditObjectModalOpen, setIsEditObjectModalOpen] = useState(false);
|
||||||
|
const [isDeleteObjectModalOpen, setIsDeleteObjectModalOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between gap-4 py-4">
|
<>
|
||||||
<div className={`flex gap-4 ${object.description === "" ? "items-center" : "items-start"}`}>
|
<ObjectModal
|
||||||
<div className="bg-custom-background-80 h-10 w-10 grid place-items-center rounded">
|
data={object}
|
||||||
{object.icon ? renderEmoji(object.icon) : <TableProperties size={20} strokeWidth={1.5} />}
|
isOpen={isEditObjectModalOpen}
|
||||||
</div>
|
onClose={() => setIsEditObjectModalOpen(false)}
|
||||||
<div>
|
/>
|
||||||
<h5 className="text-sm font-medium">{object.display_name}</h5>
|
<DeleteObjectModal
|
||||||
<p className="text-custom-text-300 text-xs">{object.description}</p>
|
isOpen={isDeleteObjectModalOpen}
|
||||||
|
objectToDelete={object}
|
||||||
|
onClose={() => setIsDeleteObjectModalOpen(false)}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center justify-between gap-4 py-4">
|
||||||
|
<div className={`flex gap-4 ${object.description === "" ? "items-center" : "items-start"}`}>
|
||||||
|
<div className="bg-custom-background-80 h-10 w-10 grid place-items-center rounded">
|
||||||
|
{object.icon ? (
|
||||||
|
renderEmoji(object.icon)
|
||||||
|
) : (
|
||||||
|
<TableProperties size={20} strokeWidth={1.5} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5 className="text-sm font-medium">{object.display_name}</h5>
|
||||||
|
<p className="text-custom-text-300 text-xs">{object.description}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<CustomMenu ellipsis>
|
||||||
|
<CustomMenu.MenuItem renderAs="button" onClick={() => setIsEditObjectModalOpen(true)}>
|
||||||
|
Edit
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
<CustomMenu.MenuItem renderAs="button" onClick={() => setIsDeleteObjectModalOpen(true)}>
|
||||||
|
Delete
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
</CustomMenu>
|
||||||
</div>
|
</div>
|
||||||
<CustomMenu ellipsis>
|
</>
|
||||||
<CustomMenu.MenuItem renderAs="button" onClick={handleEditObject}>
|
|
||||||
Edit
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
<CustomMenu.MenuItem renderAs="button" onClick={handleDeleteObject}>
|
|
||||||
Delete
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
</CustomMenu>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user