forked from github/plane
chore: set up mobx store and configured all crud operations
This commit is contained in:
parent
2e2cace5de
commit
9b6efa2ed3
@ -0,0 +1,135 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
// headless ui
|
||||
import { Disclosure } from "@headlessui/react";
|
||||
// react-hook-form
|
||||
import { Control, Controller, UseFormWatch, useForm } from "react-hook-form";
|
||||
// components
|
||||
import {
|
||||
CheckboxAttributeForm,
|
||||
DateTimeAttributeForm,
|
||||
EmailAttributeForm,
|
||||
FileAttributeForm,
|
||||
NumberAttributeForm,
|
||||
RelationAttributeForm,
|
||||
SelectAttributeForm,
|
||||
TextAttributeForm,
|
||||
UrlAttributeForm,
|
||||
} from "components/custom-attributes";
|
||||
// ui
|
||||
import { PrimaryButton, 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 = {
|
||||
data: Partial<ICustomAttribute>;
|
||||
handleDeleteAttribute: () => void;
|
||||
handleUpdateAttribute: (data: Partial<ICustomAttribute>) => Promise<void>;
|
||||
type: TCustomAttributeTypes;
|
||||
};
|
||||
|
||||
export type FormComponentProps = {
|
||||
control: Control<Partial<ICustomAttribute>, any>;
|
||||
watch?: UseFormWatch<Partial<ICustomAttribute>>;
|
||||
};
|
||||
|
||||
export const AttributeForm: React.FC<Props> = ({
|
||||
data,
|
||||
handleDeleteAttribute,
|
||||
handleUpdateAttribute,
|
||||
type,
|
||||
}) => {
|
||||
const typeMetaData = CUSTOM_ATTRIBUTES_LIST[type];
|
||||
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
handleSubmit,
|
||||
reset,
|
||||
watch,
|
||||
} = useForm({ defaultValues: typeMetaData.defaultFormValues });
|
||||
|
||||
const handleFormSubmit = async (data: Partial<ICustomAttribute>) => {
|
||||
await handleUpdateAttribute(data);
|
||||
};
|
||||
|
||||
const renderForm = (type: TCustomAttributeTypes): JSX.Element => {
|
||||
let FormToRender = <></>;
|
||||
|
||||
if (type === "checkbox") FormToRender = <CheckboxAttributeForm control={control} />;
|
||||
else if (type === "datetime") FormToRender = <DateTimeAttributeForm control={control} />;
|
||||
else if (type === "email") FormToRender = <EmailAttributeForm control={control} />;
|
||||
else if (type === "files") FormToRender = <FileAttributeForm control={control} />;
|
||||
else if (type === "multi_select")
|
||||
FormToRender = <SelectAttributeForm control={control} multiple />;
|
||||
else if (type === "number")
|
||||
FormToRender = <NumberAttributeForm control={control} watch={watch} />;
|
||||
else if (type === "relation") FormToRender = <RelationAttributeForm control={control} />;
|
||||
else if (type === "select") FormToRender = <SelectAttributeForm control={control} />;
|
||||
else if (type === "text") FormToRender = <TextAttributeForm control={control} />;
|
||||
else if (type === "url") FormToRender = <UrlAttributeForm control={control} />;
|
||||
|
||||
return FormToRender;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
|
||||
reset({
|
||||
...typeMetaData.defaultFormValues,
|
||||
...data,
|
||||
});
|
||||
}, [data, 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">{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(handleFormSubmit)} className="p-3 pl-9 pt-0">
|
||||
{renderForm(type)}
|
||||
<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">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDeleteAttribute}
|
||||
className="text-xs font-medium px-3 py-2 rounded bg-custom-background-100 border border-custom-border-200"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
<PrimaryButton type="submit">{isSubmitting ? "Saving..." : "Save"}</PrimaryButton>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Disclosure.Panel>
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
);
|
||||
};
|
@ -1,23 +1,14 @@
|
||||
import Image from "next/image";
|
||||
|
||||
// react-hook-form
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// headless ui
|
||||
import { Disclosure } from "@headlessui/react";
|
||||
// ui
|
||||
import { ToggleSwitch } from "components/ui";
|
||||
import { Input } from "../input";
|
||||
import { Controller } from "react-hook-form";
|
||||
// components
|
||||
import { FormComponentProps, Input } from "components/custom-attributes";
|
||||
// icons
|
||||
import { CheckCircle2, ChevronDown } from "lucide-react";
|
||||
import { CheckCircle2 } 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, TCustomAttributeTypes } from "types";
|
||||
// constants
|
||||
import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
|
||||
|
||||
type Props = {};
|
||||
|
||||
const checkboxAttributeRepresentations = [
|
||||
{
|
||||
@ -32,129 +23,76 @@ const checkboxAttributeRepresentations = [
|
||||
},
|
||||
];
|
||||
|
||||
const defaultFormValues: Partial<ICustomAttribute> = {
|
||||
default_value: "checked",
|
||||
display_name: "",
|
||||
extra_settings: {
|
||||
representation: "check",
|
||||
},
|
||||
is_required: false,
|
||||
};
|
||||
export const CheckboxAttributeForm: React.FC<FormComponentProps> = ({ control }) => (
|
||||
<>
|
||||
<div className="space-y-3">
|
||||
<Input placeholder="Enter field title" />
|
||||
<div>
|
||||
<p className="text-xs">Default value</p>
|
||||
<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="checked"
|
||||
id="checked"
|
||||
className="scale-75"
|
||||
defaultChecked
|
||||
/>
|
||||
<label htmlFor="checked">Checked</label>
|
||||
</div>
|
||||
|
||||
export const CheckboxAttributeForm: React.FC<Props> = () => {
|
||||
const { control, watch } = useForm({ defaultValues: defaultFormValues });
|
||||
|
||||
const typeMetaData = CUSTOM_ATTRIBUTES_LIST.checkbox;
|
||||
|
||||
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">{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 className="p-3 pl-9 pt-0">
|
||||
<div className="space-y-3">
|
||||
<Input placeholder="Enter field title" />
|
||||
<div>
|
||||
<p className="text-xs">Default value</p>
|
||||
<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="checked"
|
||||
id="checked"
|
||||
className="scale-75"
|
||||
defaultChecked
|
||||
<div className="flex items-center gap-1 text-xs">
|
||||
<input
|
||||
type="radio"
|
||||
name="default_value"
|
||||
value="unchecked"
|
||||
id="unchecked"
|
||||
className="scale-75"
|
||||
/>
|
||||
<label htmlFor="unchecked">Unchecked</label>
|
||||
</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"
|
||||
/>
|
||||
<label htmlFor="checked">Checked</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 text-xs">
|
||||
<input
|
||||
type="radio"
|
||||
name="default_value"
|
||||
value="unchecked"
|
||||
id="unchecked"
|
||||
className="scale-75"
|
||||
/>
|
||||
<label htmlFor="unchecked">Unchecked</label>
|
||||
</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-between">
|
||||
<div className="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>
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs font-medium px-3 py-2 rounded bg-custom-background-100 border border-custom-border-200"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Disclosure.Panel>
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
);
|
||||
};
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -1,129 +1,64 @@
|
||||
// react-hook-form
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// headless ui
|
||||
import { Disclosure } from "@headlessui/react";
|
||||
import { Controller } from "react-hook-form";
|
||||
// components
|
||||
import { FormComponentProps, Input } from "components/custom-attributes";
|
||||
// ui
|
||||
import { CustomSelect, ToggleSwitch } from "components/ui";
|
||||
import { Input } from "../input";
|
||||
// icons
|
||||
import { ChevronDown } from "lucide-react";
|
||||
// types
|
||||
import { ICustomAttribute } from "types";
|
||||
import { CustomSelect } from "components/ui";
|
||||
// constants
|
||||
import { CUSTOM_ATTRIBUTES_LIST, DATE_FORMATS, TIME_FORMATS } from "constants/custom-attributes";
|
||||
import { DATE_FORMATS, TIME_FORMATS } from "constants/custom-attributes";
|
||||
|
||||
type Props = {};
|
||||
|
||||
const defaultFormValues: Partial<ICustomAttribute> = {
|
||||
default_value: "",
|
||||
display_name: "",
|
||||
extra_settings: {
|
||||
date_format: "DD-MM-YYYY",
|
||||
time_format: "12",
|
||||
},
|
||||
is_required: false,
|
||||
};
|
||||
|
||||
export const DateTimeAttributeForm: React.FC<Props> = () => {
|
||||
const { control } = useForm({ defaultValues: defaultFormValues });
|
||||
|
||||
const typeMetaData = CUSTOM_ATTRIBUTES_LIST.datetime;
|
||||
|
||||
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">{typeMetaData.label}</h6>
|
||||
</div>
|
||||
<div className={`${open ? "-rotate-180" : ""} transition-all`}>
|
||||
<ChevronDown size={16} strokeWidth={1.5} rotate="180deg" />
|
||||
</div>
|
||||
</Disclosure.Button>
|
||||
<Disclosure.Panel 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.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.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>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-8 flex items-center justify-between">
|
||||
<div className="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>
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs font-medium px-3 py-2 rounded bg-custom-background-100 border border-custom-border-200"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
</>
|
||||
export const DateTimeAttributeForm: 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} />
|
||||
)}
|
||||
</Disclosure>
|
||||
);
|
||||
};
|
||||
/>
|
||||
<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.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>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,92 +1,28 @@
|
||||
// react-hook-form
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// headless ui
|
||||
import { Disclosure } from "@headlessui/react";
|
||||
import { Controller } from "react-hook-form";
|
||||
// ui
|
||||
import { ToggleSwitch } from "components/ui";
|
||||
import { Input } from "../input";
|
||||
// icons
|
||||
import { ChevronDown } from "lucide-react";
|
||||
// types
|
||||
import { ICustomAttribute } from "types";
|
||||
// constants
|
||||
import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
|
||||
import { FormComponentProps, Input } from "components/custom-attributes";
|
||||
|
||||
type Props = {};
|
||||
|
||||
const defaultFormValues: Partial<ICustomAttribute> = {
|
||||
default_value: "",
|
||||
display_name: "",
|
||||
is_required: false,
|
||||
};
|
||||
|
||||
export const EmailAttributeForm: React.FC<Props> = () => {
|
||||
const { control } = useForm({ defaultValues: defaultFormValues });
|
||||
|
||||
const typeMetaData = CUSTOM_ATTRIBUTES_LIST.email;
|
||||
|
||||
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">{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 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}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="my-3 border-t-[0.5px] border-custom-border-200" />
|
||||
<div className="mt-8 flex items-center justify-between">
|
||||
<div className="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>
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs font-medium px-3 py-2 rounded bg-custom-background-100 border border-custom-border-200"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Disclosure.Panel>
|
||||
</>
|
||||
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} />
|
||||
)}
|
||||
</Disclosure>
|
||||
);
|
||||
};
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="default_value"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="Enter default email"
|
||||
value={value?.toString()}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -3,87 +3,23 @@ import { Controller, useForm } from "react-hook-form";
|
||||
// headless ui
|
||||
import { Disclosure } from "@headlessui/react";
|
||||
// components
|
||||
import { FileFormatsDropdown } from "components/custom-attributes";
|
||||
// ui
|
||||
import { ToggleSwitch } from "components/ui";
|
||||
import { Input } from "../input";
|
||||
// icons
|
||||
import { ChevronDown } from "lucide-react";
|
||||
// types
|
||||
import { ICustomAttribute } from "types";
|
||||
// constants
|
||||
import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
|
||||
import { FileFormatsDropdown, FormComponentProps, Input } from "components/custom-attributes";
|
||||
|
||||
type Props = {};
|
||||
|
||||
const defaultFormValues: Partial<ICustomAttribute> = {
|
||||
display_name: "",
|
||||
extra_settings: {
|
||||
file_formats: [],
|
||||
},
|
||||
is_multi: false,
|
||||
is_required: false,
|
||||
};
|
||||
|
||||
export const FileAttributeForm: React.FC<Props> = () => {
|
||||
const { control } = useForm({ defaultValues: defaultFormValues });
|
||||
|
||||
const typeMetaData = CUSTOM_ATTRIBUTES_LIST.files;
|
||||
|
||||
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">{typeMetaData.label}</h6>
|
||||
</div>
|
||||
<div className={`${open ? "-rotate-180" : ""} transition-all`}>
|
||||
<ChevronDown size={16} strokeWidth={1.5} rotate="180deg" />
|
||||
</div>
|
||||
</Disclosure.Button>
|
||||
<Disclosure.Panel 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-between">
|
||||
<div className="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>
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs font-medium px-3 py-2 rounded bg-custom-background-100 border border-custom-border-200"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
</>
|
||||
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} />
|
||||
)}
|
||||
</Disclosure>
|
||||
);
|
||||
};
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="extra_settings.file_formats"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<FileFormatsDropdown value={value} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,3 +1,4 @@
|
||||
export * from "./attribute-form";
|
||||
export * from "./checkbox-attribute-form";
|
||||
export * from "./date-time-attribute-form";
|
||||
export * from "./email-attribute-form";
|
||||
|
@ -1,23 +1,18 @@
|
||||
import Image from "next/image";
|
||||
|
||||
// headless ui
|
||||
import { Disclosure } from "@headlessui/react";
|
||||
// react-hook-form
|
||||
import { Controller } from "react-hook-form";
|
||||
// components
|
||||
import { FormComponentProps } from "components/custom-attributes";
|
||||
// ui
|
||||
import { ToggleSwitch } from "components/ui";
|
||||
import { Input } from "../input";
|
||||
// icons
|
||||
import { CheckCircle2, ChevronDown } from "lucide-react";
|
||||
import { CheckCircle2 } 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, TCustomAttributeTypes } from "types";
|
||||
// constants
|
||||
import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
type Props = {};
|
||||
|
||||
const numberAttributeRepresentations = [
|
||||
{
|
||||
@ -37,176 +32,116 @@ const numberAttributeRepresentations = [
|
||||
},
|
||||
];
|
||||
|
||||
const defaultFormValues: Partial<ICustomAttribute> = {
|
||||
default_value: "",
|
||||
display_name: "",
|
||||
extra_settings: {
|
||||
color: "Blue",
|
||||
divided_by: 100,
|
||||
representation: "numerical",
|
||||
show_number: true,
|
||||
},
|
||||
is_required: false,
|
||||
};
|
||||
|
||||
export const NumberAttributeForm: React.FC<Props> = () => {
|
||||
const { control, watch } = useForm({ defaultValues: defaultFormValues });
|
||||
|
||||
const typeMetaData = CUSTOM_ATTRIBUTES_LIST.number;
|
||||
|
||||
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">{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 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>
|
||||
{(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}
|
||||
step={1}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
<>
|
||||
<div className="text-xs">Color</div>
|
||||
<div className="col-span-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="extra_settings.color"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Accent color"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</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 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>
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs font-medium px-3 py-2 rounded bg-custom-background-100 border border-custom-border-200"
|
||||
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)}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Disclosure.Panel>
|
||||
</>
|
||||
<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>
|
||||
{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}
|
||||
step={1}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
<>
|
||||
<div className="text-xs">Color</div>
|
||||
<div className="col-span-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="extra_settings.color"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Input type="text" placeholder="Accent color" value={value} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
</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>
|
||||
)}
|
||||
</Disclosure>
|
||||
);
|
||||
};
|
||||
</>
|
||||
);
|
||||
|
@ -1,129 +1,65 @@
|
||||
// react-hook-form
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// headless ui
|
||||
import { Disclosure } from "@headlessui/react";
|
||||
import { Controller } from "react-hook-form";
|
||||
// components
|
||||
import { FormComponentProps, Input } from "components/custom-attributes";
|
||||
// ui
|
||||
import { CustomSelect, ToggleSwitch } from "components/ui";
|
||||
import { Input } from "../input";
|
||||
// icons
|
||||
import { ChevronDown } from "lucide-react";
|
||||
// types
|
||||
import { ICustomAttribute } from "types";
|
||||
import { CustomSelect } from "components/ui";
|
||||
// constants
|
||||
import { CUSTOM_ATTRIBUTES_LIST, CUSTOM_ATTRIBUTE_UNITS } from "constants/custom-attributes";
|
||||
import { CUSTOM_ATTRIBUTE_UNITS } from "constants/custom-attributes";
|
||||
|
||||
type Props = {};
|
||||
|
||||
const defaultFormValues: Partial<ICustomAttribute> = {
|
||||
display_name: "",
|
||||
is_multi: false,
|
||||
is_required: false,
|
||||
unit: "cycle",
|
||||
};
|
||||
|
||||
export const RelationAttributeForm: React.FC<Props> = () => {
|
||||
const { control } = useForm({ defaultValues: defaultFormValues });
|
||||
|
||||
const typeMetaData = CUSTOM_ATTRIBUTES_LIST.relation;
|
||||
|
||||
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">{typeMetaData.label}</h6>
|
||||
</div>
|
||||
<div className={`${open ? "-rotate-180" : ""} transition-all`}>
|
||||
<ChevronDown size={16} strokeWidth={1.5} rotate="180deg" />
|
||||
</div>
|
||||
</Disclosure.Button>
|
||||
<Disclosure.Panel 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) => {
|
||||
if (unit.value === "user") return null;
|
||||
|
||||
return (
|
||||
<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">
|
||||
<input
|
||||
type="radio"
|
||||
name="is_multi"
|
||||
value="false"
|
||||
id="singleSelect"
|
||||
className="scale-75"
|
||||
defaultChecked
|
||||
/>
|
||||
<label htmlFor="singleSelect">Single Select</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 text-xs">
|
||||
<input
|
||||
type="radio"
|
||||
name="is_multi"
|
||||
value="true"
|
||||
id="multiSelect"
|
||||
className="scale-75"
|
||||
/>
|
||||
<label htmlFor="multiSelect">Multi select</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 flex items-center justify-between">
|
||||
<div className="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>
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs font-medium px-3 py-2 rounded bg-custom-background-100 border border-custom-border-200"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
</>
|
||||
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} />
|
||||
)}
|
||||
</Disclosure>
|
||||
);
|
||||
};
|
||||
/>
|
||||
<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) => {
|
||||
if (unit.value === "user") return null;
|
||||
|
||||
return (
|
||||
<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">
|
||||
<input
|
||||
type="radio"
|
||||
name="is_multi"
|
||||
value="false"
|
||||
id="singleSelect"
|
||||
className="scale-75"
|
||||
defaultChecked
|
||||
/>
|
||||
<label htmlFor="singleSelect">Single Select</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 text-xs">
|
||||
<input type="radio" name="is_multi" value="true" id="multiSelect" className="scale-75" />
|
||||
<label htmlFor="multiSelect">Multi select</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,26 +1,15 @@
|
||||
// react-hook-form
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// headless ui
|
||||
import { Disclosure, Popover, Transition } from "@headlessui/react";
|
||||
// ui
|
||||
import { CustomSelect, ToggleSwitch } from "components/ui";
|
||||
import { Input } from "../input";
|
||||
// icons
|
||||
import { ChevronDown, GripVertical, MoreHorizontal } from "lucide-react";
|
||||
// types
|
||||
import { ICustomAttribute } from "types";
|
||||
// constants
|
||||
import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
|
||||
import { TwitterPicker } from "react-color";
|
||||
import React from "react";
|
||||
|
||||
type Props = {};
|
||||
|
||||
const defaultFormValues: Partial<ICustomAttribute> = {
|
||||
default_value: "",
|
||||
display_name: "",
|
||||
is_required: false,
|
||||
};
|
||||
// react-hook-form
|
||||
import { Controller } from "react-hook-form";
|
||||
// react-color
|
||||
import { TwitterPicker } from "react-color";
|
||||
// headless ui
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
// components
|
||||
import { FormComponentProps, Input } from "components/custom-attributes";
|
||||
// icons
|
||||
import { MoreHorizontal } from "lucide-react";
|
||||
|
||||
export const SelectOption: React.FC = () => (
|
||||
<div className="group w-3/5 flex items-center justify-between gap-1 hover:bg-custom-background-80 px-2 py-1 rounded">
|
||||
@ -46,112 +35,71 @@ export const SelectOption: React.FC = () => (
|
||||
</div>
|
||||
);
|
||||
|
||||
export const SelectAttributeForm: React.FC<Props> = () => {
|
||||
const { control } = useForm({ defaultValues: defaultFormValues });
|
||||
|
||||
const typeMetaData = CUSTOM_ATTRIBUTES_LIST.select;
|
||||
|
||||
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">{typeMetaData.label}</h6>
|
||||
</div>
|
||||
<div className={`${open ? "-rotate-180" : ""} transition-all`}>
|
||||
<ChevronDown size={16} strokeWidth={1.5} rotate="180deg" />
|
||||
</div>
|
||||
</Disclosure.Button>
|
||||
<Disclosure.Panel 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">
|
||||
{/* TODO: map over options */}
|
||||
<SelectOption />
|
||||
<SelectOption />
|
||||
<SelectOption />
|
||||
</div>
|
||||
<div className="mt-2 w-3/5">
|
||||
<div className="bg-custom-background-100 rounded border border-custom-border-200 flex items-center gap-2 px-3 py-2">
|
||||
<span className="flex-shrink-0 text-xs grid place-items-center">🚀</span>
|
||||
<input
|
||||
type="text"
|
||||
className="flex-grow border-none outline-none placeholder:text-custom-text-400 text-xs"
|
||||
placeholder="Enter new option"
|
||||
/>
|
||||
<Popover className="relative">
|
||||
{({ open, close }) => (
|
||||
<>
|
||||
<Popover.Button className="grid place-items-center h-3.5 w-3.5 rounded-sm focus:outline-none">
|
||||
<span className="h-full w-full rounded-sm bg-black" />
|
||||
</Popover.Button>
|
||||
|
||||
<Transition
|
||||
as={React.Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel className="absolute bottom-full right-0 z-10 mb-1 px-2 sm:px-0">
|
||||
<Controller
|
||||
name="color"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<TwitterPicker
|
||||
color={"#ff0000"}
|
||||
onChange={(value) => {
|
||||
onChange(value.hex);
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 flex items-center justify-between">
|
||||
<div className="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>
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs font-medium px-3 py-2 rounded bg-custom-background-100 border border-custom-border-200"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
</>
|
||||
export const SelectAttributeForm: React.FC<FormComponentProps & { multiple?: boolean }> = ({
|
||||
control,
|
||||
multiple = false,
|
||||
}) => (
|
||||
<div className="space-y-3">
|
||||
<Controller
|
||||
control={control}
|
||||
name="display_name"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Input placeholder="Enter field title" value={value} onChange={onChange} />
|
||||
)}
|
||||
</Disclosure>
|
||||
);
|
||||
};
|
||||
/>
|
||||
<div>
|
||||
<p className="text-xs">Options</p>
|
||||
<div className="mt-3 space-y-2">
|
||||
{/* TODO: map over options */}
|
||||
<SelectOption />
|
||||
<SelectOption />
|
||||
<SelectOption />
|
||||
</div>
|
||||
<div className="mt-2 w-3/5">
|
||||
<div className="bg-custom-background-100 rounded border border-custom-border-200 flex items-center gap-2 px-3 py-2">
|
||||
<span className="flex-shrink-0 text-xs grid place-items-center">🚀</span>
|
||||
<input
|
||||
type="text"
|
||||
className="flex-grow border-none outline-none placeholder:text-custom-text-400 text-xs"
|
||||
placeholder="Enter new option"
|
||||
/>
|
||||
<Popover className="relative">
|
||||
{({ open, close }) => (
|
||||
<>
|
||||
<Popover.Button className="grid place-items-center h-3.5 w-3.5 rounded-sm focus:outline-none">
|
||||
<span className="h-full w-full rounded-sm bg-black" />
|
||||
</Popover.Button>
|
||||
|
||||
<Transition
|
||||
as={React.Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel className="absolute bottom-full right-0 z-10 mb-1 px-2 sm:px-0">
|
||||
<Controller
|
||||
name="color"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<TwitterPicker
|
||||
color={"#ff0000"}
|
||||
onChange={(value) => {
|
||||
onChange(value.hex);
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,78 +1,23 @@
|
||||
// react-hook-form
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// headless ui
|
||||
import { Disclosure } from "@headlessui/react";
|
||||
import { Controller } from "react-hook-form";
|
||||
// ui
|
||||
import { ToggleSwitch } from "components/ui";
|
||||
import { Input } from "../input";
|
||||
// icons
|
||||
import { ChevronDown } from "lucide-react";
|
||||
// types
|
||||
import { ICustomAttribute } from "types";
|
||||
// constants
|
||||
import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
|
||||
import { FormComponentProps, Input } from "components/custom-attributes";
|
||||
|
||||
type Props = {};
|
||||
|
||||
const defaultFormValues: Partial<ICustomAttribute> = {
|
||||
default_value: "",
|
||||
display_name: "",
|
||||
is_required: false,
|
||||
};
|
||||
|
||||
export const TextAttributeForm: React.FC<Props> = () => {
|
||||
const { control } = useForm({ defaultValues: defaultFormValues });
|
||||
|
||||
const typeMetaData = CUSTOM_ATTRIBUTES_LIST.text;
|
||||
|
||||
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">{typeMetaData.label}</h6>
|
||||
</div>
|
||||
<div className={`${open ? "-rotate-180" : ""} transition-all`}>
|
||||
<ChevronDown size={16} strokeWidth={1.5} rotate="180deg" />
|
||||
</div>
|
||||
</Disclosure.Button>
|
||||
<Disclosure.Panel 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 items-center gap-2">
|
||||
<ToggleSwitch value={true} onChange={() => {}} />
|
||||
<span className="text-xs">Mandatory field</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs font-medium px-3 py-2 rounded bg-custom-background-100 border border-custom-border-200"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
</>
|
||||
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} />
|
||||
)}
|
||||
</Disclosure>
|
||||
);
|
||||
};
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="default_value"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Input placeholder="Enter default value" value={value ?? ""} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,92 +1,23 @@
|
||||
// react-hook-form
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// headless ui
|
||||
import { Disclosure } from "@headlessui/react";
|
||||
// ui
|
||||
import { ToggleSwitch } from "components/ui";
|
||||
import { Input } from "../input";
|
||||
// icons
|
||||
import { ChevronDown } from "lucide-react";
|
||||
// types
|
||||
import { ICustomAttribute } from "types";
|
||||
// constants
|
||||
import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
|
||||
import { Controller } from "react-hook-form";
|
||||
// components
|
||||
import { FormComponentProps, Input } from "components/custom-attributes";
|
||||
|
||||
type Props = {};
|
||||
|
||||
const defaultFormValues: Partial<ICustomAttribute> = {
|
||||
default_value: "",
|
||||
display_name: "",
|
||||
is_required: false,
|
||||
};
|
||||
|
||||
export const UrlAttributeForm: React.FC<Props> = () => {
|
||||
const { control } = useForm({ defaultValues: defaultFormValues });
|
||||
|
||||
const typeMetaData = CUSTOM_ATTRIBUTES_LIST.url;
|
||||
|
||||
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">{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 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="my-3 border-t-[0.5px] border-custom-border-200" />
|
||||
<div className="mt-8 flex items-center justify-between">
|
||||
<div className="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>
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs font-medium px-3 py-2 rounded bg-custom-background-100 border border-custom-border-200"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Disclosure.Panel>
|
||||
</>
|
||||
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} />
|
||||
)}
|
||||
</Disclosure>
|
||||
);
|
||||
};
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="default_value"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Input type="url" placeholder="Enter default URL" value={value ?? ""} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
111
web/components/custom-attributes/delete-object-modal.tsx
Normal file
111
web/components/custom-attributes/delete-object-modal.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// mobx
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// headless ui
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// ui
|
||||
import { DangerButton, SecondaryButton } from "components/ui";
|
||||
// icons
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
// types
|
||||
import { ICustomAttribute } from "types";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
objectToDelete: ICustomAttribute | null;
|
||||
onClose: () => void;
|
||||
onSubmit?: () => Promise<void>;
|
||||
};
|
||||
|
||||
export const DeleteObjectModal: React.FC<Props> = observer(
|
||||
({ isOpen, objectToDelete, onClose, onSubmit }) => {
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { customAttributes: customAttributesStore } = useMobxStore();
|
||||
const { deleteEntity } = customAttributesStore;
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleDeleteObject = async () => {
|
||||
if (!workspaceSlug || !objectToDelete) return;
|
||||
|
||||
setIsDeleting(true);
|
||||
|
||||
await deleteEntity(workspaceSlug.toString(), objectToDelete.id)
|
||||
.then(async () => {
|
||||
if (onSubmit) await onSubmit();
|
||||
handleClose();
|
||||
})
|
||||
.finally(() => setIsDeleting(false));
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<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="relative transform overflow-hidden rounded-lg border border-custom-border-200 bg-custom-background-100 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl">
|
||||
<div className="flex flex-col gap-6 p-6">
|
||||
<div className="flex w-full items-center justify-start gap-6">
|
||||
<span className="place-items-center rounded-full bg-red-500/20 p-4">
|
||||
<AlertTriangle className="text-red-500" size={24} strokeWidth={1.5} />
|
||||
</span>
|
||||
<span className="flex items-center justify-start">
|
||||
<h3 className="text-xl font-medium 2xl:text-2xl">Delete Object</h3>
|
||||
</span>
|
||||
</div>
|
||||
<span>
|
||||
<p className="text-sm text-custom-text-200">
|
||||
Are you sure you want to delete object{" "}
|
||||
<span className="break-words font-medium text-custom-text-100">
|
||||
{objectToDelete?.display_name}
|
||||
</span>
|
||||
? The object will be deleted permanently and cannot be recovered.
|
||||
</p>
|
||||
</span>
|
||||
<div className="flex justify-end gap-2">
|
||||
<SecondaryButton onClick={onClose}>Cancel</SecondaryButton>
|
||||
<DangerButton onClick={handleDeleteObject} loading={isDeleting}>
|
||||
{isDeleting ? "Deleting..." : "Delete Object"}
|
||||
</DangerButton>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
}
|
||||
);
|
@ -1,9 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// headless ui
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
// icons
|
||||
import { Plus } from "lucide-react";
|
||||
// types
|
||||
@ -11,57 +9,48 @@ import { TCustomAttributeTypes } from "types";
|
||||
// constants
|
||||
import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
|
||||
|
||||
type Props = {};
|
||||
|
||||
export const TypesDropdown: React.FC<Props> = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
return (
|
||||
<Listbox as="div" className="relative flex-shrink-0 text-left">
|
||||
{({ open }: { open: boolean }) => (
|
||||
<>
|
||||
<Listbox.Button className="flex items-center gap-1 text-xs font-medium text-custom-primary-100">
|
||||
<Plus size={14} strokeWidth={1.5} />
|
||||
Add Attribute
|
||||
</Listbox.Button>
|
||||
<Transition
|
||||
show={open}
|
||||
as={React.Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Listbox.Options className="absolute z-10 bottom-full mb-2 border-[0.5px] border-custom-border-300 p-1 min-w-[10rem] rounded bg-custom-background-100 text-xs shadow-custom-shadow-rg focus:outline-none">
|
||||
{Object.keys(CUSTOM_ATTRIBUTES_LIST).map((type) => {
|
||||
const typeMetaData = CUSTOM_ATTRIBUTES_LIST[type as TCustomAttributeTypes];
|
||||
|
||||
return (
|
||||
<Listbox.Option
|
||||
key={type}
|
||||
value={type}
|
||||
className={({ active, selected }) =>
|
||||
`flex items-center gap-1 cursor-pointer select-none truncate rounded px-1 py-1.5 ${
|
||||
active || selected ? "bg-custom-background-80" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
>
|
||||
{({ active, selected }) => (
|
||||
<>
|
||||
<typeMetaData.icon size={14} strokeWidth={1.5} />
|
||||
{typeMetaData.label}
|
||||
</>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
);
|
||||
})}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Listbox>
|
||||
);
|
||||
type Props = {
|
||||
onClick: (type: TCustomAttributeTypes) => void;
|
||||
};
|
||||
|
||||
export const TypesDropdown: React.FC<Props> = ({ onClick }) => (
|
||||
<Menu as="div" className="relative flex-shrink-0 text-left">
|
||||
{({ open }: { open: boolean }) => (
|
||||
<>
|
||||
<Menu.Button className="flex items-center gap-1 text-xs font-medium text-custom-primary-100">
|
||||
<Plus size={14} strokeWidth={1.5} />
|
||||
Add Attribute
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
show={open}
|
||||
as={React.Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Menu.Items className="fixed z-10 mb-2 border-[0.5px] border-custom-border-300 p-1 min-w-[10rem] max-h-60 rounded bg-custom-background-100 text-xs shadow-custom-shadow-rg focus:outline-none overflow-y-auto">
|
||||
{Object.keys(CUSTOM_ATTRIBUTES_LIST).map((type) => {
|
||||
const typeMetaData = CUSTOM_ATTRIBUTES_LIST[type as TCustomAttributeTypes];
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
key={type}
|
||||
as="button"
|
||||
type="button"
|
||||
onClick={() => onClick(type as TCustomAttributeTypes)}
|
||||
className="flex items-center gap-1 cursor-pointer select-none truncate rounded px-1 py-1.5 hover:bg-custom-background-80 w-full"
|
||||
>
|
||||
<typeMetaData.icon size={14} strokeWidth={1.5} />
|
||||
{typeMetaData.label}
|
||||
</Menu.Item>
|
||||
);
|
||||
})}
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
);
|
||||
|
@ -1,4 +1,6 @@
|
||||
export * from "./attribute-forms";
|
||||
export * from "./dropdowns";
|
||||
export * from "./delete-object-modal";
|
||||
export * from "./input";
|
||||
export * from "./object-modal";
|
||||
export * from "./objects-list";
|
||||
|
@ -1,7 +1,13 @@
|
||||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// mobx
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// headless ui
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// components
|
||||
import {
|
||||
TextAttributeForm,
|
||||
Input,
|
||||
@ -14,100 +20,230 @@ import {
|
||||
EmailAttributeForm,
|
||||
FileAttributeForm,
|
||||
SelectAttributeForm,
|
||||
AttributeForm,
|
||||
} from "components/custom-attributes";
|
||||
// ui
|
||||
import { Loader, PrimaryButton, SecondaryButton } from "components/ui";
|
||||
// types
|
||||
import { ICustomAttribute, TCustomAttributeTypes } from "types";
|
||||
// constants
|
||||
import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
|
||||
|
||||
type Props = {
|
||||
objectIdToEdit?: string | null;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit?: () => Promise<void>;
|
||||
};
|
||||
|
||||
export const ObjectModal: React.FC<Props> = ({ isOpen, onClose, onSubmit }) => {
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
};
|
||||
export const ObjectModal: React.FC<Props> = observer(
|
||||
({ objectIdToEdit, isOpen, onClose, onSubmit }) => {
|
||||
const [object, setObject] = useState<Partial<ICustomAttribute>>({
|
||||
display_name: "",
|
||||
description: "",
|
||||
});
|
||||
const [isCreatingObject, setIsCreatingObject] = useState(false);
|
||||
|
||||
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>
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
<div 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">
|
||||
<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="bg-custom-background-100 p-6 w-1/2 h-[85%] flex flex-col rounded-xl">
|
||||
<h3 className="text-2xl font-semibold">New Object</h3>
|
||||
<div className="mt-5 space-y-5 h-full overflow-y-auto">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-9 w-9 bg-custom-background-80 grid place-items-center rounded text-sm">
|
||||
🚀
|
||||
const { customAttributes: customAttributesStore } = useMobxStore();
|
||||
const {
|
||||
createEntity,
|
||||
createEntityAttribute,
|
||||
createEntityAttributeLoader,
|
||||
deleteEntityAttribute,
|
||||
entityAttributes,
|
||||
fetchEntityDetails,
|
||||
fetchEntityDetailsLoader,
|
||||
updateEntityAttribute,
|
||||
} = customAttributesStore;
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
|
||||
setTimeout(() => {
|
||||
setObject({ display_name: "", description: "" });
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const handleCreateEntity = async () => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
setIsCreatingObject(true);
|
||||
|
||||
const payload: Partial<ICustomAttribute> = {
|
||||
description: object.description ?? "",
|
||||
display_name: object.display_name ?? "",
|
||||
project: projectId.toString(),
|
||||
type: "entity",
|
||||
};
|
||||
|
||||
await createEntity(workspaceSlug.toString(), payload)
|
||||
.then((res) => {
|
||||
setObject((prevData) => ({ ...prevData, ...res }));
|
||||
if (onSubmit) onSubmit();
|
||||
})
|
||||
.finally(() => setIsCreatingObject(false));
|
||||
};
|
||||
|
||||
const handleCreateEntityAttribute = 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 createEntityAttribute(workspaceSlug.toString(), { ...payload, parent: object.id });
|
||||
};
|
||||
|
||||
const handleUpdateAttribute = async (attributeId: string, data: Partial<ICustomAttribute>) => {
|
||||
if (!workspaceSlug || !object || !object.id) return;
|
||||
|
||||
await updateEntityAttribute(workspaceSlug.toString(), object.id, attributeId, data);
|
||||
};
|
||||
|
||||
const handleDeleteAttribute = async (attributeId: string) => {
|
||||
if (!workspaceSlug || !object || !object.id) return;
|
||||
|
||||
await deleteEntityAttribute(workspaceSlug.toString(), object.id, attributeId);
|
||||
};
|
||||
|
||||
// fetch the object details if object state has id
|
||||
useEffect(() => {
|
||||
if (!object.id || object.id === "") return;
|
||||
|
||||
if (!entityAttributes[object.id]) {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
fetchEntityDetails(workspaceSlug.toString(), object.id).then((res) => {
|
||||
setObject({ ...res });
|
||||
});
|
||||
}
|
||||
}, [object.id, workspaceSlug, fetchEntityDetails, entityAttributes]);
|
||||
|
||||
// update the object state if objectIdToEdit is present
|
||||
useEffect(() => {
|
||||
if (!objectIdToEdit) return;
|
||||
|
||||
setObject((prevData) => ({
|
||||
...prevData,
|
||||
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>
|
||||
|
||||
<div 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">
|
||||
<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="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 text-sm">
|
||||
🚀
|
||||
</div>
|
||||
<Input
|
||||
placeholder="Enter Object Title"
|
||||
value={object.display_name}
|
||||
onChange={(e) =>
|
||||
setObject((prevData) => ({ ...prevData, display_name: e.target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Input placeholder="Enter Object Title" />
|
||||
<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 }))
|
||||
}
|
||||
/>
|
||||
</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"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium">Attributes</h4>
|
||||
<div className="mt-2 space-y-2">
|
||||
{/* TODO: Map over attributes */}
|
||||
<TextAttributeForm />
|
||||
<NumberAttributeForm />
|
||||
<CheckboxAttributeForm />
|
||||
<RelationAttributeForm />
|
||||
<DateTimeAttributeForm />
|
||||
<UrlAttributeForm />
|
||||
<EmailAttributeForm />
|
||||
<FileAttributeForm />
|
||||
<SelectAttributeForm />
|
||||
{object.id && (
|
||||
<div className="px-6 pb-5">
|
||||
<h4 className="font-medium">Attributes</h4>
|
||||
<div className="mt-2 space-y-2">
|
||||
{fetchEntityDetailsLoader ? (
|
||||
<Loader>
|
||||
<Loader.Item height="40px" />
|
||||
</Loader>
|
||||
) : (
|
||||
Object.keys(entityAttributes[object.id] ?? {})?.map((attributeId) => {
|
||||
const attribute = entityAttributes[object.id ?? ""][attributeId];
|
||||
|
||||
{/* <CUSTOM_ATTRIBUTES_LIST.text.component />
|
||||
<CUSTOM_ATTRIBUTES_LIST.number.component />
|
||||
<CUSTOM_ATTRIBUTES_LIST.checkbox.component />
|
||||
<CUSTOM_ATTRIBUTES_LIST.relation.component />
|
||||
<CUSTOM_ATTRIBUTES_LIST.datetime.component />
|
||||
<CUSTOM_ATTRIBUTES_LIST.url.component />
|
||||
<CUSTOM_ATTRIBUTES_LIST.email.component />
|
||||
<CUSTOM_ATTRIBUTES_LIST.files.component />
|
||||
<CUSTOM_ATTRIBUTES_LIST.select.component />
|
||||
<CUSTOM_ATTRIBUTES_LIST.multi_select.component /> */}
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<TypesDropdown />
|
||||
</div>
|
||||
return (
|
||||
<AttributeForm
|
||||
key={attributeId}
|
||||
data={attribute}
|
||||
handleDeleteAttribute={() => handleDeleteAttribute(attributeId)}
|
||||
handleUpdateAttribute={async (data) =>
|
||||
await handleUpdateAttribute(attributeId, data)
|
||||
}
|
||||
type={attribute.type}
|
||||
/>
|
||||
);
|
||||
})
|
||||
)}
|
||||
{createEntityAttributeLoader && (
|
||||
<Loader>
|
||||
<Loader.Item height="40px" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<TypesDropdown onClick={handleCreateEntityAttribute} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
<div className="flex items-center justify-end gap-3 px-6 py-5 border-t border-custom-border-200">
|
||||
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
|
||||
<PrimaryButton onClick={handleCreateEntity} loading={isCreatingObject}>
|
||||
{object.id
|
||||
? "Save changes"
|
||||
: isCreatingObject
|
||||
? "Creating..."
|
||||
: "Create Object"}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
93
web/components/custom-attributes/objects-list.tsx
Normal file
93
web/components/custom-attributes/objects-list.tsx
Normal file
@ -0,0 +1,93 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// mobx
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { DeleteObjectModal } from "components/custom-attributes";
|
||||
// ui
|
||||
import { CustomMenu, Loader } from "components/ui";
|
||||
// icons
|
||||
import { TableProperties } from "lucide-react";
|
||||
// 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);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { customAttributes: customAttributesStore } = useMobxStore();
|
||||
const { entities, fetchEntities } = customAttributesStore;
|
||||
|
||||
const handleDeleteObject = async (object: ICustomAttribute) => {
|
||||
setObjectToDelete(object);
|
||||
setDeleteObjectModal(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
if (!entities) fetchEntities(workspaceSlug.toString(), projectId);
|
||||
}, [entities, fetchEntities, projectId, workspaceSlug]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DeleteObjectModal
|
||||
isOpen={deleteObjectModal}
|
||||
objectToDelete={objectToDelete}
|
||||
onClose={() => {
|
||||
setDeleteObjectModal(false);
|
||||
|
||||
setTimeout(() => {
|
||||
setObjectToDelete(null);
|
||||
}, 300);
|
||||
}}
|
||||
/>
|
||||
<div className="space-y-4 divide-y divide-custom-border-100">
|
||||
{entities ? (
|
||||
entities.length > 0 ? (
|
||||
entities.map((entity) => (
|
||||
<div key={entity.id} className="p-4 flex items-center justify-between gap-4">
|
||||
<div className="flex gap-4">
|
||||
<div className="bg-custom-background-80 h-10 w-10 grid place-items-center rounded">
|
||||
<TableProperties size={20} strokeWidth={1.5} />
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="text-sm font-medium">{entity.display_name}</h5>
|
||||
<p className="text-custom-text-300 text-xs">{entity.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<CustomMenu ellipsis>
|
||||
<CustomMenu.MenuItem renderAs="button" onClick={() => handleEditObject(entity)}>
|
||||
Edit
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem renderAs="button" onClick={() => handleDeleteObject(entity)}>
|
||||
Delete
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="text-sm text-custom-text-200 text-center">No objects present</p>
|
||||
)
|
||||
) : (
|
||||
<Loader className="space-y-4">
|
||||
<Loader.Item height="60px" />
|
||||
<Loader.Item height="60px" />
|
||||
<Loader.Item height="60px" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
@ -1,15 +1,3 @@
|
||||
// components
|
||||
import {
|
||||
CheckboxAttributeForm,
|
||||
DateTimeAttributeForm,
|
||||
EmailAttributeForm,
|
||||
FileAttributeForm,
|
||||
NumberAttributeForm,
|
||||
RelationAttributeForm,
|
||||
SelectAttributeForm,
|
||||
TextAttributeForm,
|
||||
UrlAttributeForm,
|
||||
} from "components/custom-attributes";
|
||||
// icons
|
||||
import {
|
||||
AtSign,
|
||||
@ -25,66 +13,117 @@ import {
|
||||
LucideIcon,
|
||||
} from "lucide-react";
|
||||
// types
|
||||
import { TCustomAttributeTypes, TCustomAttributeUnits } from "types";
|
||||
import { ICustomAttribute, TCustomAttributeTypes, TCustomAttributeUnits } from "types";
|
||||
|
||||
export const CUSTOM_ATTRIBUTES_LIST: {
|
||||
[key in Partial<TCustomAttributeTypes>]: {
|
||||
component: React.FC<any>;
|
||||
defaultFormValues: Partial<ICustomAttribute>;
|
||||
icon: LucideIcon;
|
||||
initialPayload?: Partial<ICustomAttribute>;
|
||||
label: string;
|
||||
};
|
||||
} = {
|
||||
checkbox: {
|
||||
component: CheckboxAttributeForm,
|
||||
defaultFormValues: {
|
||||
default_value: "checked",
|
||||
display_name: "",
|
||||
extra_settings: {
|
||||
representation: "check",
|
||||
},
|
||||
is_required: false,
|
||||
},
|
||||
icon: CheckCircle,
|
||||
initialPayload: {
|
||||
extra_settings: {
|
||||
representation: "check",
|
||||
},
|
||||
},
|
||||
label: "Checkbox",
|
||||
},
|
||||
datetime: {
|
||||
component: DateTimeAttributeForm,
|
||||
defaultFormValues: {
|
||||
default_value: "",
|
||||
display_name: "",
|
||||
extra_settings: {
|
||||
date_format: "DD-MM-YYYY",
|
||||
time_format: "12",
|
||||
},
|
||||
is_required: false,
|
||||
},
|
||||
icon: Clock4,
|
||||
label: "Date Time",
|
||||
},
|
||||
email: {
|
||||
component: EmailAttributeForm,
|
||||
defaultFormValues: { default_value: "", display_name: "", is_required: false },
|
||||
icon: AtSign,
|
||||
label: "Email",
|
||||
},
|
||||
files: {
|
||||
component: FileAttributeForm,
|
||||
defaultFormValues: {
|
||||
display_name: "",
|
||||
extra_settings: {
|
||||
file_formats: [],
|
||||
},
|
||||
is_multi: false,
|
||||
is_required: false,
|
||||
},
|
||||
icon: FileMinus,
|
||||
label: "Files",
|
||||
initialPayload: {
|
||||
extra_settings: {
|
||||
file_formats: [".jpg", ".jpeg"],
|
||||
},
|
||||
},
|
||||
},
|
||||
multi_select: {
|
||||
component: SelectAttributeForm,
|
||||
defaultFormValues: { default_value: "", display_name: "", is_required: false },
|
||||
icon: Disc,
|
||||
label: "Multi Select",
|
||||
},
|
||||
number: {
|
||||
component: NumberAttributeForm,
|
||||
defaultFormValues: {
|
||||
default_value: "",
|
||||
display_name: "",
|
||||
extra_settings: {
|
||||
color: "Blue",
|
||||
divided_by: 100,
|
||||
representation: "numerical",
|
||||
show_number: true,
|
||||
},
|
||||
is_required: false,
|
||||
},
|
||||
icon: Hash,
|
||||
label: "Number",
|
||||
initialPayload: {
|
||||
extra_settings: {
|
||||
representation: "numerical",
|
||||
},
|
||||
},
|
||||
},
|
||||
option: {
|
||||
icon: Baseline,
|
||||
label: "Option",
|
||||
},
|
||||
relation: {
|
||||
component: RelationAttributeForm,
|
||||
defaultFormValues: { display_name: "", is_multi: false, is_required: false, unit: "cycle" },
|
||||
icon: Forward,
|
||||
label: "Relation",
|
||||
initialPayload: {
|
||||
unit: "cycle",
|
||||
},
|
||||
},
|
||||
select: {
|
||||
component: SelectAttributeForm,
|
||||
defaultFormValues: { default_value: "", display_name: "", is_required: false },
|
||||
icon: Disc,
|
||||
label: "Select",
|
||||
},
|
||||
text: {
|
||||
component: TextAttributeForm,
|
||||
defaultFormValues: { default_value: "", display_name: "", is_required: false },
|
||||
icon: CaseSensitive,
|
||||
label: "Text",
|
||||
},
|
||||
url: {
|
||||
component: UrlAttributeForm,
|
||||
defaultFormValues: { default_value: "", display_name: "", is_required: false },
|
||||
icon: Link2,
|
||||
label: "URL",
|
||||
},
|
||||
|
@ -9,19 +9,19 @@ import useToast from "hooks/use-toast";
|
||||
import useProjectDetails from "hooks/use-project-details";
|
||||
// components
|
||||
import { SettingsHeader } from "components/project";
|
||||
import { ObjectModal, ObjectsList } from "components/custom-attributes";
|
||||
// ui
|
||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
import { PrimaryButton } from "components/ui";
|
||||
// icons
|
||||
import { TableProperties } from "lucide-react";
|
||||
// types
|
||||
import type { NextPage } from "next";
|
||||
// helpers
|
||||
import { truncateText } from "helpers/string.helper";
|
||||
import { ObjectModal } from "components/custom-attributes";
|
||||
// types
|
||||
import type { NextPage } from "next";
|
||||
import { ICustomAttribute } from "types";
|
||||
|
||||
const ControlSettings: NextPage = () => {
|
||||
const [isCreateObjectModalOpen, setIsCreateObjectModalOpen] = useState(false);
|
||||
const [objectToEdit, setObjectToEdit] = useState<ICustomAttribute | null>(null);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
@ -30,6 +30,11 @@ const ControlSettings: NextPage = () => {
|
||||
|
||||
const { projectDetails } = useProjectDetails();
|
||||
|
||||
const handleEditObject = (object: ICustomAttribute) => {
|
||||
setObjectToEdit(object);
|
||||
setIsCreateObjectModalOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<ProjectAuthorizationWrapper
|
||||
breadcrumbs={
|
||||
@ -44,8 +49,12 @@ const ControlSettings: NextPage = () => {
|
||||
}
|
||||
>
|
||||
<ObjectModal
|
||||
objectIdToEdit={objectToEdit?.id}
|
||||
isOpen={isCreateObjectModalOpen}
|
||||
onClose={() => setIsCreateObjectModalOpen(false)}
|
||||
onClose={() => {
|
||||
setIsCreateObjectModalOpen(false);
|
||||
setObjectToEdit(null);
|
||||
}}
|
||||
/>
|
||||
<div className="p-8">
|
||||
<SettingsHeader />
|
||||
@ -57,20 +66,10 @@ const ControlSettings: NextPage = () => {
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
<div className="mt-4 border-y border-custom-border-100">
|
||||
<div className="space-y-4 divide-y divide-custom-border-100">
|
||||
{/* TODO: Map over objects */}
|
||||
<div className="p-4 flex items-center justify-between gap-4">
|
||||
<div className="flex gap-4">
|
||||
<div className="bg-custom-background-80 h-10 w-10 grid place-items-center rounded">
|
||||
<TableProperties size={20} strokeWidth={1.5} />
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="text-sm font-medium">Title</h5>
|
||||
<p className="text-custom-text-300 text-xs">Description of 100 chars</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ObjectsList
|
||||
handleEditObject={handleEditObject}
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
68
web/services/custom-attributes.service.ts
Normal file
68
web/services/custom-attributes.service.ts
Normal file
@ -0,0 +1,68 @@
|
||||
// services
|
||||
import APIService from "services/api.service";
|
||||
// types
|
||||
import { ICustomAttribute } from "types";
|
||||
|
||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
||||
|
||||
class CustomAttributesService extends APIService {
|
||||
constructor() {
|
||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
||||
}
|
||||
|
||||
async getEntitiesList(
|
||||
workspaceSlug: string,
|
||||
params: { project: string }
|
||||
): Promise<ICustomAttribute[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/entity-properties/`, {
|
||||
params,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getPropertyDetails(workspaceSlug: string, propertyId: string): Promise<ICustomAttribute> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/properties/${propertyId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async createProperty(
|
||||
workspaceSlug: string,
|
||||
data: Partial<ICustomAttribute>
|
||||
): Promise<ICustomAttribute> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/properties/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async patchProperty(
|
||||
workspaceSlug: string,
|
||||
propertyId: string,
|
||||
data: Partial<ICustomAttribute>
|
||||
): Promise<ICustomAttribute> {
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/properties/${propertyId}/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteProperty(workspaceSlug: string, propertyId: string): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/properties/${propertyId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const customAttributesService = new CustomAttributesService();
|
||||
|
||||
export default customAttributesService;
|
201
web/store/custom-attributes.ts
Normal file
201
web/store/custom-attributes.ts
Normal file
@ -0,0 +1,201 @@
|
||||
// mobx
|
||||
import { action, observable, runInAction, makeAutoObservable } from "mobx";
|
||||
// services
|
||||
import customAttributesService from "services/custom-attributes.service";
|
||||
// types
|
||||
import type { ICustomAttribute } from "types";
|
||||
|
||||
class CustomAttributesStore {
|
||||
entities: ICustomAttribute[] | null = null;
|
||||
entityAttributes: {
|
||||
[key: string]: { [key: string]: ICustomAttribute };
|
||||
} = {};
|
||||
// loaders
|
||||
fetchEntitiesLoader = false;
|
||||
fetchEntityDetailsLoader = false;
|
||||
createEntityAttributeLoader = false;
|
||||
// errors
|
||||
attributesFetchError: any | null = null;
|
||||
error: any | null = null;
|
||||
rootStore: any | null = null;
|
||||
|
||||
constructor(_rootStore: any | null = null) {
|
||||
makeAutoObservable(this, {
|
||||
entities: observable.ref,
|
||||
entityAttributes: observable.ref,
|
||||
fetchEntities: action,
|
||||
fetchEntityDetails: action,
|
||||
createEntity: action,
|
||||
deleteEntity: action,
|
||||
createEntityAttribute: action,
|
||||
updateEntityAttribute: action,
|
||||
deleteEntityAttribute: action,
|
||||
});
|
||||
|
||||
this.rootStore = _rootStore;
|
||||
}
|
||||
|
||||
fetchEntities = async (workspaceSlug: string, projectId: string) => {
|
||||
try {
|
||||
this.fetchEntitiesLoader = true;
|
||||
|
||||
const response = await customAttributesService.getEntitiesList(workspaceSlug, {
|
||||
project: projectId,
|
||||
});
|
||||
|
||||
if (response) {
|
||||
runInAction(() => {
|
||||
this.entities = response;
|
||||
this.fetchEntitiesLoader = false;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
this.fetchEntitiesLoader = false;
|
||||
this.attributesFetchError = error;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
fetchEntityDetails = async (workspaceSlug: string, propertyId: string) => {
|
||||
try {
|
||||
this.fetchEntityDetailsLoader = true;
|
||||
|
||||
const response = await customAttributesService.getPropertyDetails(workspaceSlug, propertyId);
|
||||
|
||||
const entityChildren: { [key: string]: ICustomAttribute } = response.children.reduce(
|
||||
(acc, child) => ({
|
||||
...acc,
|
||||
[child.id]: child,
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
runInAction(() => {
|
||||
this.entityAttributes = {
|
||||
...this.entityAttributes,
|
||||
[propertyId]: entityChildren,
|
||||
};
|
||||
this.fetchEntityDetailsLoader = false;
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
this.fetchEntityDetailsLoader = false;
|
||||
this.error = error;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
createEntity = async (workspaceSlug: string, data: Partial<ICustomAttribute>) => {
|
||||
try {
|
||||
const response = await customAttributesService.createProperty(workspaceSlug, data);
|
||||
|
||||
runInAction(() => {
|
||||
this.entities = [...(this.entities ?? []), response];
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
this.error = error;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
deleteEntity = async (workspaceSlug: string, propertyId: string) => {
|
||||
try {
|
||||
await customAttributesService.deleteProperty(workspaceSlug, propertyId);
|
||||
|
||||
const newEntities = this.entities?.filter((entity) => entity.id !== propertyId);
|
||||
|
||||
runInAction(() => {
|
||||
this.entities = [...(newEntities ?? [])];
|
||||
});
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
this.error = error;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
createEntityAttribute = async (
|
||||
workspaceSlug: string,
|
||||
data: Partial<ICustomAttribute> & { parent: string }
|
||||
) => {
|
||||
try {
|
||||
this.createEntityAttributeLoader = true;
|
||||
|
||||
const response = await customAttributesService.createProperty(workspaceSlug, data);
|
||||
|
||||
runInAction(() => {
|
||||
this.entityAttributes = {
|
||||
...this.entityAttributes,
|
||||
[data.parent]: {
|
||||
...this.entityAttributes[data.parent],
|
||||
[response.id]: response,
|
||||
},
|
||||
};
|
||||
this.createEntityAttributeLoader = false;
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
this.error = error;
|
||||
this.createEntityAttributeLoader = false;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
updateEntityAttribute = async (
|
||||
workspaceSlug: string,
|
||||
parentId: string,
|
||||
propertyId: string,
|
||||
data: Partial<ICustomAttribute>
|
||||
) => {
|
||||
try {
|
||||
await customAttributesService.patchProperty(workspaceSlug, propertyId, data);
|
||||
|
||||
const newEntities = this.entityAttributes[parentId];
|
||||
newEntities[propertyId] = {
|
||||
...newEntities[propertyId],
|
||||
...data,
|
||||
};
|
||||
|
||||
runInAction(() => {
|
||||
this.entityAttributes = {
|
||||
...this.entityAttributes,
|
||||
[parentId]: newEntities,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
this.error = error;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
deleteEntityAttribute = async (workspaceSlug: string, parentId: string, propertyId: string) => {
|
||||
try {
|
||||
await customAttributesService.deleteProperty(workspaceSlug, propertyId);
|
||||
|
||||
const newEntities = this.entityAttributes[parentId];
|
||||
delete newEntities[propertyId];
|
||||
|
||||
runInAction(() => {
|
||||
this.entityAttributes = {
|
||||
...this.entityAttributes,
|
||||
[parentId]: newEntities,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
this.error = error;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default CustomAttributesStore;
|
@ -6,6 +6,7 @@ import ThemeStore from "./theme";
|
||||
import ProjectStore, { IProjectStore } from "./project";
|
||||
import ProjectPublishStore, { IProjectPublishStore } from "./project-publish";
|
||||
import IssuesStore from "./issues";
|
||||
import CustomAttributesStore from "./custom-attributes";
|
||||
|
||||
enableStaticRendering(typeof window === "undefined");
|
||||
|
||||
@ -15,6 +16,7 @@ export class RootStore {
|
||||
project: IProjectStore;
|
||||
projectPublish: IProjectPublishStore;
|
||||
issues: IssuesStore;
|
||||
customAttributes: CustomAttributesStore;
|
||||
|
||||
constructor() {
|
||||
this.user = new UserStore(this);
|
||||
@ -22,5 +24,6 @@ export class RootStore {
|
||||
this.project = new ProjectStore(this);
|
||||
this.projectPublish = new ProjectPublishStore(this);
|
||||
this.issues = new IssuesStore(this);
|
||||
this.customAttributes = new CustomAttributesStore(this);
|
||||
}
|
||||
}
|
||||
|
12
web/types/custom-attributes.d.ts
vendored
12
web/types/custom-attributes.d.ts
vendored
@ -15,19 +15,21 @@ export type TCustomAttributeTypes =
|
||||
export type TCustomAttributeUnits = "cycle" | "issue" | "module" | "user";
|
||||
|
||||
export interface ICustomAttribute {
|
||||
children: ICustomAttribute[];
|
||||
color: string;
|
||||
default_value: string;
|
||||
default_value: string | null;
|
||||
description: string;
|
||||
display_name: string;
|
||||
extra_settings: any;
|
||||
icon: string;
|
||||
extra_settings: { [key: string]: any };
|
||||
icon: string | null;
|
||||
id: string;
|
||||
is_default: boolean;
|
||||
is_multi: boolean;
|
||||
is_required: boolean;
|
||||
is_shared: boolean;
|
||||
parent: string;
|
||||
project: string | null;
|
||||
sort_order: number;
|
||||
type: TCustomAttributeTypes;
|
||||
unit: TCustomAttributeUnits;
|
||||
unit: TCustomAttributeUnits | null;
|
||||
workspace: string;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user