chore: single select options

This commit is contained in:
Aaryan Khandelwal 2023-09-14 13:09:21 +05:30
parent 02d18e9edd
commit cf384d3a4d
17 changed files with 503 additions and 163 deletions

View File

@ -29,18 +29,53 @@ type Props = {
data: Partial<ICustomAttribute>; data: Partial<ICustomAttribute>;
handleDeleteAttribute: () => void; handleDeleteAttribute: () => void;
handleUpdateAttribute: (data: Partial<ICustomAttribute>) => Promise<void>; handleUpdateAttribute: (data: Partial<ICustomAttribute>) => Promise<void>;
objectId: string;
type: TCustomAttributeTypes; type: TCustomAttributeTypes;
}; };
export type FormComponentProps = { export type FormComponentProps = {
control: Control<Partial<ICustomAttribute>, any>; control: Control<Partial<ICustomAttribute>, any>;
watch?: UseFormWatch<Partial<ICustomAttribute>>; objectId: string;
watch: UseFormWatch<Partial<ICustomAttribute>>;
};
const RenderForm: React.FC<{ type: TCustomAttributeTypes } & FormComponentProps> = ({
control,
objectId,
type,
watch,
}) => {
let FormToRender: any = <></>;
if (type === "checkbox")
FormToRender = <CheckboxAttributeForm control={control} objectId={objectId} watch={watch} />;
else if (type === "datetime")
FormToRender = <DateTimeAttributeForm control={control} objectId={objectId} watch={watch} />;
else if (type === "email")
FormToRender = <EmailAttributeForm control={control} objectId={objectId} watch={watch} />;
else if (type === "files")
FormToRender = <FileAttributeForm control={control} objectId={objectId} watch={watch} />;
else if (type === "multi_select")
FormToRender = <SelectAttributeForm control={control} objectId={objectId} watch={watch} />;
else if (type === "number")
FormToRender = <NumberAttributeForm control={control} objectId={objectId} watch={watch} />;
else if (type === "relation")
FormToRender = <RelationAttributeForm control={control} objectId={objectId} watch={watch} />;
else if (type === "select")
FormToRender = <SelectAttributeForm control={control} objectId={objectId} watch={watch} />;
else if (type === "text")
FormToRender = <TextAttributeForm control={control} objectId={objectId} watch={watch} />;
else if (type === "url")
FormToRender = <UrlAttributeForm control={control} objectId={objectId} watch={watch} />;
return FormToRender;
}; };
export const AttributeForm: React.FC<Props> = ({ export const AttributeForm: React.FC<Props> = ({
data, data,
handleDeleteAttribute, handleDeleteAttribute,
handleUpdateAttribute, handleUpdateAttribute,
objectId,
type, type,
}) => { }) => {
const typeMetaData = CUSTOM_ATTRIBUTES_LIST[type]; const typeMetaData = CUSTOM_ATTRIBUTES_LIST[type];
@ -57,25 +92,6 @@ export const AttributeForm: React.FC<Props> = ({
await handleUpdateAttribute(data); 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(() => { useEffect(() => {
if (!data) return; if (!data) return;
@ -95,7 +111,7 @@ export const AttributeForm: React.FC<Props> = ({
<Disclosure.Button className="p-3 flex items-center justify-between gap-1 w-full"> <Disclosure.Button className="p-3 flex items-center justify-between gap-1 w-full">
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
<typeMetaData.icon size={14} strokeWidth={1.5} /> <typeMetaData.icon size={14} strokeWidth={1.5} />
<h6 className="text-sm">{typeMetaData.label}</h6> <h6 className="text-sm">{data.display_name ?? typeMetaData.label}</h6>
</div> </div>
<div className={`${open ? "-rotate-180" : ""} transition-all`}> <div className={`${open ? "-rotate-180" : ""} transition-all`}>
<ChevronDown size={16} strokeWidth={1.5} rotate="180deg" /> <ChevronDown size={16} strokeWidth={1.5} rotate="180deg" />
@ -103,7 +119,9 @@ export const AttributeForm: React.FC<Props> = ({
</Disclosure.Button> </Disclosure.Button>
<Disclosure.Panel> <Disclosure.Panel>
<form onSubmit={handleSubmit(handleFormSubmit)} className="p-3 pl-9 pt-0"> <form onSubmit={handleSubmit(handleFormSubmit)} className="p-3 pl-9 pt-0">
{renderForm(type)} {data.type && (
<RenderForm type={data.type} control={control} objectId={objectId} watch={watch} />
)}
<div className="mt-8 flex items-center justify-between"> <div className="mt-8 flex items-center justify-between">
<div className="flex-shrink-0 flex items-center gap-2"> <div className="flex-shrink-0 flex items-center gap-2">
<Controller <Controller
@ -123,7 +141,9 @@ export const AttributeForm: React.FC<Props> = ({
> >
Remove Remove
</button> </button>
<PrimaryButton type="submit">{isSubmitting ? "Saving..." : "Save"}</PrimaryButton> <PrimaryButton type="submit" loading={isSubmitting}>
{isSubmitting ? "Saving..." : "Save"}
</PrimaryButton>
</div> </div>
</div> </div>
</form> </form>

View File

@ -1,3 +1,4 @@
export * from "./select-attribute";
export * from "./attribute-form"; export * from "./attribute-form";
export * from "./checkbox-attribute-form"; export * from "./checkbox-attribute-form";
export * from "./date-time-attribute-form"; export * from "./date-time-attribute-form";
@ -5,6 +6,5 @@ export * from "./email-attribute-form";
export * from "./file-attribute-form"; export * from "./file-attribute-form";
export * from "./number-attribute-form"; export * from "./number-attribute-form";
export * from "./relation-attribute-form"; export * from "./relation-attribute-form";
export * from "./select-attribute-form";
export * from "./text-attribute-form"; export * from "./text-attribute-form";
export * from "./url-attribute-form"; export * from "./url-attribute-form";

View File

@ -1,105 +0,0 @@
import React from "react";
// 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">
<div className="flex items-center gap-1 flex-grow truncate">
{/* <button type="button">
<GripVertical className="text-custom-text-400" size={14} />
</button> */}
<p className="bg-custom-primary-500/10 text-custom-text-300 text-xs p-1 rounded inline truncate">
🚀 Option 1
</p>
</div>
<div className="flex-shrink-0 flex items-center gap-2">
<button
type="button"
className="hidden group-hover:inline-block text-custom-text-300 text-xs"
>
Set as default
</button>
<button type="button">
<MoreHorizontal className="text-custom-text-400" size={14} />
</button>
</div>
</div>
);
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} />
)}
/>
<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

@ -0,0 +1,3 @@
export * from "./option-form";
export * from "./select-attribute-form";
export * from "./select-option";

View File

@ -0,0 +1,107 @@
import React, { 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 { Popover, Transition } from "@headlessui/react";
// react-color
import { TwitterPicker } from "react-color";
// ui
import { PrimaryButton } from "components/ui";
import { ICustomAttribute } from "types";
type Props = {
objectId: string;
parentId: string;
};
export const OptionForm: React.FC<Props> = observer(({ objectId, parentId }) => {
const [optionName, setOptionName] = useState("");
const [optionColor, setOptionColor] = useState("#000000");
const router = useRouter();
const { workspaceSlug } = router.query;
const { customAttributes: customAttributesStore } = useMobxStore();
const { createAttributeOption, createAttributeOptionLoader } = customAttributesStore;
const handleCreateOption = async () => {
if (!workspaceSlug) return;
if (!optionName || optionName === "") return;
const payload: Partial<ICustomAttribute> = {
color: optionColor,
display_name: optionName,
type: "option",
};
await createAttributeOption(workspaceSlug.toString(), objectId, {
...payload,
parent: parentId,
}).then(() => {
setOptionName("");
setOptionColor("#000000");
});
};
return (
<div className="flex items-center gap-2">
<div className="bg-custom-background-100 rounded border border-custom-border-200 flex items-center gap-2 px-3 py-2 flex-grow">
{/* <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"
value={optionName}
onChange={(e) => setOptionName(e.target.value)}
placeholder="Enter new option"
/>
<Popover className="relative">
{({ 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"
style={{ backgroundColor: optionColor }}
/>
</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">
<TwitterPicker
color={optionColor}
onChange={(value) => {
setOptionColor(value.hex);
close();
}}
/>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
</div>
<div className="flex-shrink-0">
<PrimaryButton
onClick={handleCreateOption}
size="sm"
className="!py-1.5 !px-2"
loading={createAttributeOptionLoader}
>
{createAttributeOptionLoader ? "Adding..." : "Add"}
</PrimaryButton>
</div>
</div>
);
});

View File

@ -0,0 +1,41 @@
import React from "react";
// mobx
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// react-hook-form
import { Controller } from "react-hook-form";
// components
import { FormComponentProps, Input, OptionForm, SelectOption } from "components/custom-attributes";
export const SelectAttributeForm: React.FC<FormComponentProps & { multiple?: boolean }> = observer(
({ control, multiple = false, objectId = "", watch }) => {
const { customAttributes: customAttributesStore } = useMobxStore();
const { entityAttributes } = customAttributesStore;
const options = entityAttributes?.[objectId]?.[watch("id") ?? ""]?.children;
return (
<div className="space-y-3">
<Controller
control={control}
name="display_name"
render={({ field: { onChange, value } }) => (
<Input placeholder="Enter field title" value={value} onChange={onChange} />
)}
/>
<div>
<p className="text-xs">Options</p>
<div className="mt-3 space-y-2 w-3/5">
{options?.map((option) => (
<SelectOption key={option.id} objectId={objectId} option={option} />
))}
</div>
<div className="mt-2 w-3/5">
<OptionForm objectId={objectId} parentId={watch("id") ?? ""} />
</div>
</div>
</div>
);
}
);

View File

@ -0,0 +1,68 @@
import { useRouter } from "next/router";
// mobx
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// ui
import { Tooltip } from "components/ui";
// icons
import { MoreHorizontal } from "lucide-react";
// types
import { ICustomAttribute } from "types";
type Props = {
objectId: string;
option: ICustomAttribute;
};
export const SelectOption: React.FC<Props> = observer(({ objectId, option }) => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { customAttributes: customAttributesStore } = useMobxStore();
const { updateAttributeOption } = customAttributesStore;
const handleSetAsDefault = async () => {
if (!workspaceSlug || !option.parent) return;
await updateAttributeOption(workspaceSlug.toString(), objectId, option.parent, option.id, {
is_default: true,
});
};
return (
<div className="group flex items-center justify-between gap-1 hover:bg-custom-background-80 px-2 py-1 rounded">
<div className="flex items-center gap-1 flex-grow truncate">
{/* <button type="button">
<GripVertical className="text-custom-text-400" size={14} />
</button> */}
<Tooltip tooltipContent={option.display_name}>
<p
className="text-custom-text-300 text-xs p-1 rounded inline truncate"
style={{
backgroundColor: `${option.color}20`,
}}
>
{option.display_name}
</p>
</Tooltip>
</div>
<div className="flex-shrink-0 flex items-center gap-2">
{option.is_default ? (
<span className="text-custom-text-300 text-xs">Default</span>
) : (
<button
type="button"
onClick={handleSetAsDefault}
className="hidden group-hover:inline-block text-custom-text-300 text-xs"
>
Set as default
</button>
)}
<button type="button">
<MoreHorizontal className="text-custom-text-400" size={14} />
</button>
</div>
</div>
);
});

View File

@ -0,0 +1 @@
export * from "./text";

View File

@ -0,0 +1,26 @@
// types
import { ICustomAttribute } from "types";
type Props = {
attributeDetails: ICustomAttribute;
issueId: string;
onChange: (value: string) => void;
projectId: string;
value: string;
};
export const CustomTextAttribute: React.FC<Props> = ({
attributeDetails,
issueId,
onChange,
value,
}) => (
<input
className="border border-custom-border-200 rounded outline-none p-1 text-xs"
defaultValue={attributeDetails.default_value ?? ""}
id={`attribute-${attributeDetails.display_name}-${attributeDetails.id}`}
name={`attribute-${attributeDetails.display_name}-${attributeDetails.id}`}
onChange={(e) => onChange(e.target.value)}
value={value}
/>
);

View File

@ -1,4 +1,5 @@
export * from "./attribute-forms"; export * from "./attribute-forms";
export * from "./attributes";
export * from "./dropdowns"; export * from "./dropdowns";
export * from "./delete-object-modal"; export * from "./delete-object-modal";
export * from "./input"; export * from "./input";

View File

@ -8,20 +8,7 @@ import { useMobxStore } from "lib/mobx/store-provider";
// headless ui // headless ui
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
// components // components
import { import { Input, TypesDropdown, AttributeForm } from "components/custom-attributes";
TextAttributeForm,
Input,
TypesDropdown,
NumberAttributeForm,
CheckboxAttributeForm,
RelationAttributeForm,
DateTimeAttributeForm,
UrlAttributeForm,
EmailAttributeForm,
FileAttributeForm,
SelectAttributeForm,
AttributeForm,
} from "components/custom-attributes";
// ui // ui
import { Loader, PrimaryButton, SecondaryButton } from "components/ui"; import { Loader, PrimaryButton, SecondaryButton } from "components/ui";
// types // types
@ -211,6 +198,7 @@ export const ObjectModal: React.FC<Props> = observer(
handleUpdateAttribute={async (data) => handleUpdateAttribute={async (data) =>
await handleUpdateAttribute(attributeId, data) await handleUpdateAttribute(attributeId, data)
} }
objectId={object.id ?? ""}
type={attribute.type} type={attribute.type}
/> />
); );

View File

@ -0,0 +1,64 @@
import { useEffect } from "react";
import { useRouter } from "next/router";
// mobx
import { useMobxStore } from "lib/mobx/store-provider";
import { observer } from "mobx-react-lite";
import { Loader } from "components/ui";
import { CustomTextAttribute } from "components/custom-attributes";
type Props = {
entityId: string;
issueId: string;
projectId: string;
};
export const CustomAttributesList: React.FC<Props> = observer(
({ entityId, issueId, projectId }) => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { customAttributes: customAttributesStore } = useMobxStore();
const { entityAttributes, fetchEntityDetails, fetchEntityDetailsLoader } =
customAttributesStore;
const attributes = entityAttributes[entityId] ?? {};
useEffect(() => {
if (!entityAttributes[entityId]) {
if (!workspaceSlug) return;
fetchEntityDetails(workspaceSlug.toString(), entityId);
}
}, [entityAttributes, entityId, fetchEntityDetails, workspaceSlug]);
return (
<div>
{fetchEntityDetailsLoader ? (
<Loader className="flex items-center gap-2">
<Loader.Item height="27px" width="90px" />
<Loader.Item height="27px" width="90px" />
<Loader.Item height="27px" width="90px" />
</Loader>
) : (
<div className="flex items-center gap-2">
{Object.entries(attributes).map(([attributeId, attribute]) => (
<div key={attributeId}>
{attribute.type === "text" && (
<CustomTextAttribute
attributeDetails={attribute}
issueId={issueId}
onChange={() => {}}
projectId={projectId}
value={attribute.default_value ?? ""}
/>
)}
</div>
))}
</div>
)}
</div>
);
}
);

View File

@ -10,7 +10,7 @@ import aiService from "services/ai.service";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// components // components
import { GptAssistantModal } from "components/core"; import { GptAssistantModal } from "components/core";
import { ParentIssuesListModal } from "components/issues"; import { CustomAttributesList, ParentIssuesListModal } from "components/issues";
import { import {
IssueAssigneeSelect, IssueAssigneeSelect,
IssueDateSelect, IssueDateSelect,
@ -416,7 +416,7 @@ export const IssueForm: FC<IssueFormProps> = ({
/> />
)} )}
{/* default object properties */} {/* default object properties */}
{watch("entity") === null && ( {watch("entity") === null ? (
<> <>
{(fieldsToShow.includes("all") || fieldsToShow.includes("priority")) && ( {(fieldsToShow.includes("all") || fieldsToShow.includes("priority")) && (
<Controller <Controller
@ -542,6 +542,12 @@ export const IssueForm: FC<IssueFormProps> = ({
</CustomMenu> </CustomMenu>
)} )}
</> </>
) : (
<CustomAttributesList
entityId={watch("entity") ?? ""}
issueId=""
projectId={projectId}
/>
)} )}
</div> </div>
</div> </div>

View File

@ -1,13 +1,15 @@
export * from "./attachment"; export * from "./attachment";
export * from "./comment"; export * from "./comment";
export * from "./gantt-chart";
export * from "./my-issues"; export * from "./my-issues";
export * from "./peek-overview";
export * from "./sidebar-select"; export * from "./sidebar-select";
export * from "./view-select"; export * from "./view-select";
export * from "./activity"; export * from "./activity";
export * from "./custom-attributes-list";
export * from "./delete-issue-modal"; export * from "./delete-issue-modal";
export * from "./description-form"; export * from "./description-form";
export * from "./form"; export * from "./form";
export * from "./gantt-chart";
export * from "./main-content"; export * from "./main-content";
export * from "./modal"; export * from "./modal";
export * from "./parent-issues-list-modal"; export * from "./parent-issues-list-modal";
@ -15,4 +17,3 @@ export * from "./sidebar";
export * from "./sub-issues-list"; export * from "./sub-issues-list";
export * from "./label"; export * from "./label";
export * from "./issue-reaction"; export * from "./issue-reaction";
export * from "./peek-overview";

View File

@ -8,7 +8,7 @@ import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import useProjectDetails from "hooks/use-project-details"; import useProjectDetails from "hooks/use-project-details";
// components // components
import { SettingsHeader } from "components/project"; import { SettingsSidebar } from "components/project";
import { ObjectModal, ObjectsList } from "components/custom-attributes"; import { ObjectModal, ObjectsList } from "components/custom-attributes";
// ui // ui
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
@ -19,7 +19,7 @@ import { truncateText } from "helpers/string.helper";
import type { NextPage } from "next"; import type { NextPage } from "next";
import { ICustomAttribute } from "types"; import { ICustomAttribute } from "types";
const ControlSettings: NextPage = () => { const CustomObjectSettings: NextPage = () => {
const [isCreateObjectModalOpen, setIsCreateObjectModalOpen] = useState(false); const [isCreateObjectModalOpen, setIsCreateObjectModalOpen] = useState(false);
const [objectToEdit, setObjectToEdit] = useState<ICustomAttribute | null>(null); const [objectToEdit, setObjectToEdit] = useState<ICustomAttribute | null>(null);
@ -56,15 +56,18 @@ const ControlSettings: NextPage = () => {
setObjectToEdit(null); setObjectToEdit(null);
}} }}
/> />
<div className="p-8"> <div className="flex flex-row gap-2">
<SettingsHeader /> <div className="w-80 py-8">
<div> <SettingsSidebar />
<div className="flex items-center justify-between gap-4"> </div>
<h2 className="text-xl font-medium">Custom Objects</h2> <section className="pr-9 py-8 w-full">
<div className="flex items-center justify-between gap-2 py-3.5 border-b border-custom-border-200">
<h3 className="text-xl font-medium">Custom Objects</h3>
<PrimaryButton onClick={() => setIsCreateObjectModalOpen(true)}> <PrimaryButton onClick={() => setIsCreateObjectModalOpen(true)}>
Add Object Add Object
</PrimaryButton> </PrimaryButton>
</div> </div>
<div>
<div className="mt-4 border-y border-custom-border-100"> <div className="mt-4 border-y border-custom-border-100">
<ObjectsList <ObjectsList
handleEditObject={handleEditObject} handleEditObject={handleEditObject}
@ -72,9 +75,10 @@ const ControlSettings: NextPage = () => {
/> />
</div> </div>
</div> </div>
</section>
</div> </div>
</ProjectAuthorizationWrapper> </ProjectAuthorizationWrapper>
); );
}; };
export default ControlSettings; export default CustomObjectSettings;

View File

@ -14,6 +14,7 @@ class CustomAttributesStore {
fetchEntitiesLoader = false; fetchEntitiesLoader = false;
fetchEntityDetailsLoader = false; fetchEntityDetailsLoader = false;
createEntityAttributeLoader = false; createEntityAttributeLoader = false;
createAttributeOptionLoader = false;
// errors // errors
attributesFetchError: any | null = null; attributesFetchError: any | null = null;
error: any | null = null; error: any | null = null;
@ -30,6 +31,9 @@ class CustomAttributesStore {
createEntityAttribute: action, createEntityAttribute: action,
updateEntityAttribute: action, updateEntityAttribute: action,
deleteEntityAttribute: action, deleteEntityAttribute: action,
createAttributeOption: action,
updateAttributeOption: action,
deleteAttributeOption: action,
}); });
this.rootStore = _rootStore; this.rootStore = _rootStore;
@ -196,6 +200,117 @@ class CustomAttributesStore {
}); });
} }
}; };
createAttributeOption = async (
workspaceSlug: string,
objectId: string,
data: Partial<ICustomAttribute> & { parent: string }
) => {
try {
this.createAttributeOptionLoader = true;
const response = await customAttributesService.createProperty(workspaceSlug, data);
runInAction(() => {
this.entityAttributes = {
...this.entityAttributes,
[objectId]: {
...this.entityAttributes[objectId],
[data.parent]: {
...this.entityAttributes[objectId][data.parent],
children: [...this.entityAttributes[objectId][data.parent].children, response],
},
},
};
this.createAttributeOptionLoader = false;
});
return response;
} catch (error) {
runInAction(() => {
this.error = error;
this.createAttributeOptionLoader = false;
});
}
};
updateAttributeOption = async (
workspaceSlug: string,
objectId: string,
parentId: string,
propertyId: string,
data: Partial<ICustomAttribute>
) => {
try {
this.createAttributeOptionLoader = true;
const response = await customAttributesService.patchProperty(workspaceSlug, propertyId, data);
const newOptions = this.entityAttributes[objectId][parentId].children.map((option) => ({
...option,
...(option.id === propertyId ? response : {}),
}));
runInAction(() => {
this.entityAttributes = {
...this.entityAttributes,
[objectId]: {
...this.entityAttributes[objectId],
[parentId]: {
...this.entityAttributes[objectId][parentId],
children: newOptions,
},
},
};
this.createAttributeOptionLoader = false;
});
return response;
} catch (error) {
runInAction(() => {
this.error = error;
this.createAttributeOptionLoader = false;
});
}
};
deleteAttributeOption = async (
workspaceSlug: string,
objectId: string,
parentId: string,
propertyId: string
) => {
try {
this.createAttributeOptionLoader = true;
const response = await customAttributesService.deleteProperty(workspaceSlug, propertyId);
const newOptions = this.entityAttributes[objectId][parentId].children.filter(
(option) => option.id !== propertyId
);
runInAction(() => {
this.entityAttributes = {
...this.entityAttributes,
[objectId]: {
...this.entityAttributes[objectId],
[parentId]: {
...this.entityAttributes[objectId][parentId],
children: newOptions,
},
},
};
this.createAttributeOptionLoader = false;
});
return response;
} catch (error) {
runInAction(() => {
this.error = error;
this.createAttributeOptionLoader = false;
});
}
};
} }
export default CustomAttributesStore; export default CustomAttributesStore;

View File

@ -17,7 +17,7 @@ export type TCustomAttributeUnits = "cycle" | "issue" | "module" | "user";
export interface ICustomAttribute { export interface ICustomAttribute {
children: ICustomAttribute[]; children: ICustomAttribute[];
color: string; color: string;
default_value: string | null; default_value: string;
description: string; description: string;
display_name: string; display_name: string;
extra_settings: { [key: string]: any }; extra_settings: { [key: string]: any };
@ -26,7 +26,7 @@ export interface ICustomAttribute {
is_default: boolean; is_default: boolean;
is_multi: boolean; is_multi: boolean;
is_required: boolean; is_required: boolean;
parent: string; parent: string | null;
project: string | null; project: string | null;
sort_order: number; sort_order: number;
type: TCustomAttributeTypes; type: TCustomAttributeTypes;