chore: set up mobx store and configured all crud operations

This commit is contained in:
Aaryan Khandelwal 2023-09-13 22:25:47 +05:30
parent 2e2cace5de
commit 9b6efa2ed3
22 changed files with 1432 additions and 1213 deletions

View File

@ -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>
);
};

View File

@ -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>
</>
);

View File

@ -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>
);

View File

@ -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>
);

View File

@ -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>
);

View File

@ -1,3 +1,4 @@
export * from "./attribute-form";
export * from "./checkbox-attribute-form";
export * from "./date-time-attribute-form";
export * from "./email-attribute-form";

View File

@ -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>
);
};
</>
);

View File

@ -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>
);

View File

@ -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>
);

View File

@ -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>
);

View File

@ -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>
);

View 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>
);
}
);

View File

@ -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>
);

View File

@ -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";

View File

@ -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>
);
}
);

View 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>
</>
);
});

View File

@ -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",
},

View File

@ -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>

View 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;

View 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;

View File

@ -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);
}
}

View File

@ -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;
}