forked from github/plane
chore: single select options
This commit is contained in:
parent
02d18e9edd
commit
cf384d3a4d
@ -29,18 +29,53 @@ type Props = {
|
||||
data: Partial<ICustomAttribute>;
|
||||
handleDeleteAttribute: () => void;
|
||||
handleUpdateAttribute: (data: Partial<ICustomAttribute>) => Promise<void>;
|
||||
objectId: string;
|
||||
type: TCustomAttributeTypes;
|
||||
};
|
||||
|
||||
export type FormComponentProps = {
|
||||
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> = ({
|
||||
data,
|
||||
handleDeleteAttribute,
|
||||
handleUpdateAttribute,
|
||||
objectId,
|
||||
type,
|
||||
}) => {
|
||||
const typeMetaData = CUSTOM_ATTRIBUTES_LIST[type];
|
||||
@ -57,25 +92,6 @@ export const AttributeForm: React.FC<Props> = ({
|
||||
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;
|
||||
|
||||
@ -95,7 +111,7 @@ export const AttributeForm: React.FC<Props> = ({
|
||||
<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>
|
||||
<h6 className="text-sm">{data.display_name ?? typeMetaData.label}</h6>
|
||||
</div>
|
||||
<div className={`${open ? "-rotate-180" : ""} transition-all`}>
|
||||
<ChevronDown size={16} strokeWidth={1.5} rotate="180deg" />
|
||||
@ -103,7 +119,9 @@ export const AttributeForm: React.FC<Props> = ({
|
||||
</Disclosure.Button>
|
||||
<Disclosure.Panel>
|
||||
<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="flex-shrink-0 flex items-center gap-2">
|
||||
<Controller
|
||||
@ -123,7 +141,9 @@ export const AttributeForm: React.FC<Props> = ({
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
<PrimaryButton type="submit">{isSubmitting ? "Saving..." : "Save"}</PrimaryButton>
|
||||
<PrimaryButton type="submit" loading={isSubmitting}>
|
||||
{isSubmitting ? "Saving..." : "Save"}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -1,3 +1,4 @@
|
||||
export * from "./select-attribute";
|
||||
export * from "./attribute-form";
|
||||
export * from "./checkbox-attribute-form";
|
||||
export * from "./date-time-attribute-form";
|
||||
@ -5,6 +6,5 @@ export * from "./email-attribute-form";
|
||||
export * from "./file-attribute-form";
|
||||
export * from "./number-attribute-form";
|
||||
export * from "./relation-attribute-form";
|
||||
export * from "./select-attribute-form";
|
||||
export * from "./text-attribute-form";
|
||||
export * from "./url-attribute-form";
|
||||
|
@ -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>
|
||||
);
|
@ -0,0 +1,3 @@
|
||||
export * from "./option-form";
|
||||
export * from "./select-attribute-form";
|
||||
export * from "./select-option";
|
@ -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>
|
||||
);
|
||||
});
|
@ -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>
|
||||
);
|
||||
}
|
||||
);
|
@ -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>
|
||||
);
|
||||
});
|
1
web/components/custom-attributes/attributes/index.ts
Normal file
1
web/components/custom-attributes/attributes/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./text";
|
26
web/components/custom-attributes/attributes/text.tsx
Normal file
26
web/components/custom-attributes/attributes/text.tsx
Normal 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}
|
||||
/>
|
||||
);
|
@ -1,4 +1,5 @@
|
||||
export * from "./attribute-forms";
|
||||
export * from "./attributes";
|
||||
export * from "./dropdowns";
|
||||
export * from "./delete-object-modal";
|
||||
export * from "./input";
|
||||
|
@ -8,20 +8,7 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// headless ui
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// components
|
||||
import {
|
||||
TextAttributeForm,
|
||||
Input,
|
||||
TypesDropdown,
|
||||
NumberAttributeForm,
|
||||
CheckboxAttributeForm,
|
||||
RelationAttributeForm,
|
||||
DateTimeAttributeForm,
|
||||
UrlAttributeForm,
|
||||
EmailAttributeForm,
|
||||
FileAttributeForm,
|
||||
SelectAttributeForm,
|
||||
AttributeForm,
|
||||
} from "components/custom-attributes";
|
||||
import { Input, TypesDropdown, AttributeForm } from "components/custom-attributes";
|
||||
// ui
|
||||
import { Loader, PrimaryButton, SecondaryButton } from "components/ui";
|
||||
// types
|
||||
@ -211,6 +198,7 @@ export const ObjectModal: React.FC<Props> = observer(
|
||||
handleUpdateAttribute={async (data) =>
|
||||
await handleUpdateAttribute(attributeId, data)
|
||||
}
|
||||
objectId={object.id ?? ""}
|
||||
type={attribute.type}
|
||||
/>
|
||||
);
|
||||
|
64
web/components/issues/custom-attributes-list.tsx
Normal file
64
web/components/issues/custom-attributes-list.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
);
|
@ -10,7 +10,7 @@ import aiService from "services/ai.service";
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { GptAssistantModal } from "components/core";
|
||||
import { ParentIssuesListModal } from "components/issues";
|
||||
import { CustomAttributesList, ParentIssuesListModal } from "components/issues";
|
||||
import {
|
||||
IssueAssigneeSelect,
|
||||
IssueDateSelect,
|
||||
@ -416,7 +416,7 @@ export const IssueForm: FC<IssueFormProps> = ({
|
||||
/>
|
||||
)}
|
||||
{/* default object properties */}
|
||||
{watch("entity") === null && (
|
||||
{watch("entity") === null ? (
|
||||
<>
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("priority")) && (
|
||||
<Controller
|
||||
@ -542,6 +542,12 @@ export const IssueForm: FC<IssueFormProps> = ({
|
||||
</CustomMenu>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<CustomAttributesList
|
||||
entityId={watch("entity") ?? ""}
|
||||
issueId=""
|
||||
projectId={projectId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,13 +1,15 @@
|
||||
export * from "./attachment";
|
||||
export * from "./comment";
|
||||
export * from "./gantt-chart";
|
||||
export * from "./my-issues";
|
||||
export * from "./peek-overview";
|
||||
export * from "./sidebar-select";
|
||||
export * from "./view-select";
|
||||
export * from "./activity";
|
||||
export * from "./custom-attributes-list";
|
||||
export * from "./delete-issue-modal";
|
||||
export * from "./description-form";
|
||||
export * from "./form";
|
||||
export * from "./gantt-chart";
|
||||
export * from "./main-content";
|
||||
export * from "./modal";
|
||||
export * from "./parent-issues-list-modal";
|
||||
@ -15,4 +17,3 @@ export * from "./sidebar";
|
||||
export * from "./sub-issues-list";
|
||||
export * from "./label";
|
||||
export * from "./issue-reaction";
|
||||
export * from "./peek-overview";
|
||||
|
@ -8,7 +8,7 @@ import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
|
||||
import useToast from "hooks/use-toast";
|
||||
import useProjectDetails from "hooks/use-project-details";
|
||||
// components
|
||||
import { SettingsHeader } from "components/project";
|
||||
import { SettingsSidebar } from "components/project";
|
||||
import { ObjectModal, ObjectsList } from "components/custom-attributes";
|
||||
// ui
|
||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
@ -19,7 +19,7 @@ import { truncateText } from "helpers/string.helper";
|
||||
import type { NextPage } from "next";
|
||||
import { ICustomAttribute } from "types";
|
||||
|
||||
const ControlSettings: NextPage = () => {
|
||||
const CustomObjectSettings: NextPage = () => {
|
||||
const [isCreateObjectModalOpen, setIsCreateObjectModalOpen] = useState(false);
|
||||
const [objectToEdit, setObjectToEdit] = useState<ICustomAttribute | null>(null);
|
||||
|
||||
@ -56,15 +56,18 @@ const ControlSettings: NextPage = () => {
|
||||
setObjectToEdit(null);
|
||||
}}
|
||||
/>
|
||||
<div className="p-8">
|
||||
<SettingsHeader />
|
||||
<div>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<h2 className="text-xl font-medium">Custom Objects</h2>
|
||||
<div className="flex flex-row gap-2">
|
||||
<div className="w-80 py-8">
|
||||
<SettingsSidebar />
|
||||
</div>
|
||||
<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)}>
|
||||
Add Object
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mt-4 border-y border-custom-border-100">
|
||||
<ObjectsList
|
||||
handleEditObject={handleEditObject}
|
||||
@ -72,9 +75,10 @@ const ControlSettings: NextPage = () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</ProjectAuthorizationWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ControlSettings;
|
||||
export default CustomObjectSettings;
|
||||
|
@ -14,6 +14,7 @@ class CustomAttributesStore {
|
||||
fetchEntitiesLoader = false;
|
||||
fetchEntityDetailsLoader = false;
|
||||
createEntityAttributeLoader = false;
|
||||
createAttributeOptionLoader = false;
|
||||
// errors
|
||||
attributesFetchError: any | null = null;
|
||||
error: any | null = null;
|
||||
@ -30,6 +31,9 @@ class CustomAttributesStore {
|
||||
createEntityAttribute: action,
|
||||
updateEntityAttribute: action,
|
||||
deleteEntityAttribute: action,
|
||||
createAttributeOption: action,
|
||||
updateAttributeOption: action,
|
||||
deleteAttributeOption: action,
|
||||
});
|
||||
|
||||
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;
|
||||
|
4
web/types/custom-attributes.d.ts
vendored
4
web/types/custom-attributes.d.ts
vendored
@ -17,7 +17,7 @@ export type TCustomAttributeUnits = "cycle" | "issue" | "module" | "user";
|
||||
export interface ICustomAttribute {
|
||||
children: ICustomAttribute[];
|
||||
color: string;
|
||||
default_value: string | null;
|
||||
default_value: string;
|
||||
description: string;
|
||||
display_name: string;
|
||||
extra_settings: { [key: string]: any };
|
||||
@ -26,7 +26,7 @@ export interface ICustomAttribute {
|
||||
is_default: boolean;
|
||||
is_multi: boolean;
|
||||
is_required: boolean;
|
||||
parent: string;
|
||||
parent: string | null;
|
||||
project: string | null;
|
||||
sort_order: number;
|
||||
type: TCustomAttributeTypes;
|
||||
|
Loading…
Reference in New Issue
Block a user