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