chore: separate description fields in the create issue modal

This commit is contained in:
Aaryan Khandelwal 2023-09-19 17:09:50 +05:30
parent 7cf263ecd4
commit 6b2a5a97ac
14 changed files with 414 additions and 278 deletions

View File

@ -33,14 +33,14 @@ export const CustomDateTimeAttribute: React.FC<Props> = ({ attributeDetails, onC
attributeDetails.extra_settings.hide_date
? ""
: DATE_FORMATS[attributeDetails.extra_settings.date_format] ?? "dd-MM-yyyy"
}${
} ${
attributeDetails.extra_settings.hide_time
? ""
: ", " + (TIME_FORMATS[attributeDetails.extra_settings.time_format] ?? "HH:mm")
: TIME_FORMATS[attributeDetails.extra_settings.time_format] ?? "HH:mm"
}`}
showTimeInput={!attributeDetails.extra_settings.hide_time}
isClearable={!attributeDetails.is_required}
placeholderText={`Enter ${attributeDetails.display_name}`}
placeholderText={`Select ${attributeDetails.display_name}`}
/>
</div>
);

View File

@ -25,9 +25,7 @@ export const CustomEmailAttribute: React.FC<Props> = ({ attributeDetails, onChan
};
useEffect(() => {
if (isEditing) {
setFocus("email");
}
if (isEditing) setFocus("email");
}, [isEditing, setFocus]);
useEffect(() => {

View File

@ -111,19 +111,21 @@ export const CustomFileAttribute: React.FC<Props> = (props) => {
return (
<div className="flex-shrink-0 truncate space-y-1">
{value && value !== "" && (
<div className="group flex items-center justify-between gap-2 p-1 rounded border border-custom-border-200 text-xs truncate">
<div className="group flex items-center justify-between gap-2 p-1 rounded border border-custom-border-200 text-xs truncate w-min max-w-full whitespace-nowrap">
<a
href={value}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 flex-grow truncate"
>
<span className="flex-shrink-0 h-6 w-6">{getFileIcon(getFileExtension(value))}</span>
<span className="truncate">{getFileName(value)}</span>
<span className="flex-shrink-0 h-5 w-5">{getFileIcon(getFileExtension(value))}</span>
<span className="truncate">
{value.split("/")[value.split("/").length - 1].split("-")[1]}
</span>
</a>
<button
type="button"
className="hidden group-hover:grid place-items-center flex-shrink-0"
className="opacity-0 group-hover:opacity-100 grid place-items-center flex-shrink-0"
onClick={handleRemoveFile}
>
<X size={12} strokeWidth={1.5} />

View File

@ -86,7 +86,9 @@ export const CustomNumberAttribute: React.FC<Props> = ({ attributeDetails, onCha
/>
</div>
) : (
<span className="font-medium truncate">{value}</span>
<span className="font-medium truncate bg-custom-background-80 px-2.5 py-0.5 rounded w-min max-w-full whitespace-nowrap">
{value}
</span>
)}
</>
) : (

View File

@ -5,21 +5,21 @@ import { Combobox, Transition } from "@headlessui/react";
// hooks
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// icons
import { Check, Search } from "lucide-react";
import { Check, Search, XIcon } from "lucide-react";
// types
import { ICustomAttribute } from "types";
type Props = {
attributeDetails: ICustomAttribute;
issueId: string;
onChange: (val: string | string[] | undefined) => void;
projectId: string;
} & (
| {
multiple?: false;
onChange: (val: string | undefined) => void;
value: string | undefined;
}
| { multiple?: true; onChange: (val: string[] | undefined) => void; value: string[] | undefined }
| { multiple?: true; value: string[] | undefined }
);
export const CustomSelectAttribute: React.FC<Props> = (props) => {
@ -87,18 +87,33 @@ export const CustomSelectAttribute: React.FC<Props> = (props) => {
Array.isArray(value) ? (
value.length > 0 ? (
<div className="flex items-center gap-1 flex-wrap">
{value.map((v) => {
const optionDetails = options.find((o) => o.id === v);
{value.map((val) => {
const optionDetails = options.find((o) => o.id === val);
return (
<span
className="px-2.5 py-0.5 rounded text-xs"
<div
key={val}
className="px-2.5 py-0.5 rounded text-xs flex items-center justify-between gap-1"
style={{
backgroundColor: `${optionDetails?.color}40`,
}}
>
{optionDetails?.display_name}
</span>
{((attributeDetails.is_required && value.length > 1) ||
!attributeDetails.is_required) && (
<button
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onChange(value.filter((v) => v !== val));
}}
>
<XIcon size={10} strokeWidth={1.5} />
</button>
)}
</div>
);
})}
</div>
@ -109,14 +124,27 @@ export const CustomSelectAttribute: React.FC<Props> = (props) => {
)
) : (
<div className="flex items-center gap-2 flex-wrap">
<span
className="px-2.5 py-0.5 rounded text-xs"
<div
className="px-2.5 py-0.5 rounded text-xs flex items-center justify-between gap-1"
style={{
backgroundColor: `${options.find((o) => o.id === value)?.color}40`,
}}
>
{options.find((o) => o.id === value)?.display_name}
</span>
{!attributeDetails.is_required && (
<button
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onChange(undefined);
}}
>
<XIcon size={10} strokeWidth={1.5} />
</button>
)}
</div>
</div>
)
) : (

View File

@ -76,6 +76,8 @@ const RenderForm: React.FC<{ type: TCustomAttributeTypes } & FormComponentProps>
return FormToRender;
};
const OPTIONAL_FIELDS = ["checkbox", "file"];
export const AttributeForm: React.FC<Props> = observer(({ attributeDetails, objectId, type }) => {
const [isRemoving, setIsRemoving] = useState(false);
@ -148,7 +150,7 @@ export const AttributeForm: React.FC<Props> = observer(({ attributeDetails, obje
)}
<div className="mt-8 flex items-center justify-between">
<div className="flex-shrink-0 flex items-center gap-2">
{attributeDetails.type !== "checkbox" && (
{!OPTIONAL_FIELDS.includes(attributeDetails.type ?? "") && (
<>
<Controller
control={control}

View File

@ -1,63 +1,126 @@
import { useEffect } from "react";
import { useEffect, 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 { Disclosure } from "@headlessui/react";
// components
import {
CustomCheckboxAttribute,
CustomDateTimeAttribute,
CustomEmailAttribute,
CustomFileAttribute,
CustomNumberAttribute,
CustomRelationAttribute,
CustomSelectAttribute,
CustomTextAttribute,
CustomUrlAttribute,
} from "components/custom-attributes";
// ui
import { Loader } from "components/ui";
import { Loader, ToggleSwitch } from "components/ui";
// icons
import { ChevronDown } from "lucide-react";
// types
import { TCustomAttributeTypes } from "types";
type Props = {
entityId: string;
issueId: string;
onChange: (attributeId: string, val: string[]) => void;
onChange: (attributeId: string, val: string | string[] | undefined) => void;
projectId: string;
values: { [key: string]: string[] };
};
export const IssueModalCustomAttributesList: React.FC<Props> = observer(
({ entityId, issueId, onChange, projectId, values }) => {
const router = useRouter();
const { workspaceSlug } = router.query;
const DESCRIPTION_FIELDS: TCustomAttributeTypes[] = ["email", "number", "text", "url"];
const { customAttributes: customAttributesStore } = useMobxStore();
const { entityAttributes, fetchEntityDetails, fetchEntityDetailsLoader } =
customAttributesStore;
export const IssueModalCustomAttributesList: React.FC<Props> = observer((props) => {
const { entityId, issueId, onChange, projectId, values } = props;
const attributes = entityAttributes[entityId] ?? {};
const [hideOptionalFields, setHideOptionalFields] = useState(false);
useEffect(() => {
if (!entityAttributes[entityId]) {
if (!workspaceSlug) return;
const router = useRouter();
const { workspaceSlug } = router.query;
fetchEntityDetails(workspaceSlug.toString(), entityId);
}
}, [entityAttributes, entityId, fetchEntityDetails, workspaceSlug]);
const { customAttributes: customAttributesStore } = useMobxStore();
const { entityAttributes, fetchEntityDetails, fetchEntityDetailsLoader } = customAttributesStore;
return (
<>
{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>
) : (
<>
{Object.entries(attributes).map(([attributeId, attribute]) => (
const attributes = entityAttributes[entityId] ?? {};
// fetch entity details
useEffect(() => {
if (!entityAttributes[entityId]) {
if (!workspaceSlug) return;
fetchEntityDetails(workspaceSlug.toString(), entityId);
}
}, [entityAttributes, entityId, fetchEntityDetails, workspaceSlug]);
const descriptionFields = Object.values(attributes).filter((a) =>
DESCRIPTION_FIELDS.includes(a.type)
);
const nonDescriptionFields = Object.values(attributes).filter(
(a) => !DESCRIPTION_FIELDS.includes(a.type)
);
return (
<>
{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>
) : (
<>
<Disclosure defaultOpen>
{({ open }) => (
<>
<div className="flex items-center justify-between gap-2">
<Disclosure.Button className="font-medium flex items-center gap-2">
<ChevronDown
className={`transition-all ${open ? "-rotate-90" : ""}`}
size={14}
strokeWidth={1.5}
/>
Description Fields
</Disclosure.Button>
<div className={`flex items-center gap-1 ${open ? "" : "hidden"}`}>
<span className="text-xs">Hide optional fields</span>
<ToggleSwitch
value={hideOptionalFields}
onChange={() => setHideOptionalFields((prev) => !prev)}
/>
</div>
</div>
<Disclosure.Panel className="space-y-3.5 mt-2">
{Object.entries(descriptionFields).map(([attributeId, attribute]) => (
<div
key={attributeId}
className={hideOptionalFields && attribute.is_required ? "hidden" : ""}
>
<input
type={attribute.type}
className="border border-custom-border-200 rounded w-full px-2 py-1.5 text-xs placeholder:text-custom-text-400 focus:outline-none"
placeholder={attribute.display_name}
min={attribute.extra_settings.divided_by ? 0 : undefined}
max={attribute.extra_settings.divided_by ?? undefined}
value={values[attribute.id]?.[0] ?? attribute.default_value}
onChange={(e) => onChange(attribute.id, e.target.value)}
required={attribute.is_required}
/>
{attribute.type === "number" &&
attribute.extra_settings?.representation !== "numerical" && (
<span className="text-custom-text-400 text-[10px]">
Maximum value: {attribute.extra_settings?.divided_by}
</span>
)}
</div>
))}
</Disclosure.Panel>
</>
)}
</Disclosure>
<div className="flex items-center gap-1 flex-wrap mt-3.5">
{Object.entries(nonDescriptionFields).map(([attributeId, attribute]) => (
<div key={attributeId}>
{attribute.type === "checkbox" && (
<CustomCheckboxAttribute
@ -72,27 +135,20 @@ export const IssueModalCustomAttributesList: React.FC<Props> = observer(
<CustomDateTimeAttribute
attributeDetails={attribute}
issueId={issueId}
onChange={(val) => onChange(attribute.id, [val?.toISOString() ?? ""])}
onChange={(val) =>
onChange(attribute.id, val ? [val.toISOString()] : undefined)
}
projectId={projectId}
value={
values[attribute.id]?.[0] ? new Date(values[attribute.id]?.[0]) : undefined
}
/>
)}
{attribute.type === "email" && (
<CustomEmailAttribute
attributeDetails={attribute}
issueId={issueId}
onChange={(val: string) => onChange(attribute.id, [val])}
projectId={projectId}
value={values[attribute.id]?.[0]}
/>
)}
{attribute.type === "file" && (
<CustomFileAttribute
attributeDetails={attribute}
issueId={issueId}
onChange={(val: string) => onChange(attribute.id, [val])}
onChange={(val) => onChange(attribute.id, val)}
projectId={projectId}
value={undefined}
/>
@ -101,34 +157,17 @@ export const IssueModalCustomAttributesList: React.FC<Props> = observer(
<CustomSelectAttribute
attributeDetails={attribute}
issueId={issueId}
onChange={(val) => {
if (val) onChange(attribute.id, val);
}}
onChange={(val) => onChange(attribute.id, val)}
projectId={projectId}
value={values[attribute.id] ?? []}
multiple
/>
)}
{attribute.type === "number" && (
<CustomNumberAttribute
attributeDetails={attribute}
issueId={issueId}
onChange={(val) => {
if (val) onChange(attribute.id, [val.toString()]);
}}
projectId={projectId}
value={
values[attribute.id]?.[0] ? parseInt(values[attribute.id]?.[0]) : undefined
}
/>
)}
{attribute.type === "relation" && (
<CustomRelationAttribute
attributeDetails={attribute}
issueId={issueId}
onChange={(val) => {
if (val) onChange(attribute.id, [val]);
}}
onChange={(val) => onChange(attribute.id, val)}
projectId={projectId}
value={values[attribute.id]?.[0]}
/>
@ -137,37 +176,17 @@ export const IssueModalCustomAttributesList: React.FC<Props> = observer(
<CustomSelectAttribute
attributeDetails={attribute}
issueId={issueId}
onChange={(val) => {
if (val) onChange(attribute.id, [val]);
}}
onChange={(val) => onChange(attribute.id, val)}
projectId={projectId}
value={attribute.default_value !== "" ? attribute.default_value : undefined}
multiple={false}
/>
)}
{attribute.type === "text" && (
<CustomTextAttribute
attributeDetails={attribute}
issueId={issueId}
onChange={(val) => onChange(attribute.id, [val])}
projectId={projectId}
value={attribute.default_value}
/>
)}
{attribute.type === "url" && (
<CustomUrlAttribute
attributeDetails={attribute}
issueId={issueId}
onChange={(val: string) => onChange(attribute.id, [val])}
projectId={projectId}
value={values[attribute.id]?.[0]}
/>
)}
</div>
))}
</>
)}
</>
);
}
);
</div>
</>
)}
</>
);
});

View File

@ -10,6 +10,7 @@ import {
CustomCheckboxAttribute,
CustomDateTimeAttribute,
CustomEmailAttribute,
CustomFileAttribute,
CustomNumberAttribute,
CustomRelationAttribute,
CustomSelectAttribute,
@ -25,165 +26,208 @@ import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
type Props = {
issue: IIssue | undefined;
projectId: string;
};
export const PeekOverviewCustomAttributesList: React.FC<Props> = observer(({ issue }) => {
const router = useRouter();
const { workspaceSlug } = router.query;
export const PeekOverviewCustomAttributesList: React.FC<Props> = observer(
({ issue, projectId }) => {
const router = useRouter();
const { workspaceSlug } = router.query;
const {
customAttributes: customAttributesStore,
customAttributeValues: customAttributeValuesStore,
} = useMobxStore();
const { entityAttributes, fetchEntityDetails } = customAttributesStore;
const { issueAttributeValues, fetchIssueAttributeValues } = customAttributeValuesStore;
const {
customAttributes: customAttributesStore,
customAttributeValues: customAttributeValuesStore,
} = useMobxStore();
const { entityAttributes, fetchEntityDetails } = customAttributesStore;
const { issueAttributeValues, fetchIssueAttributeValues, deleteAttributeValue } =
customAttributeValuesStore;
const handleAttributeUpdate = (attributeId: string, value: string) => {
if (!issue || !workspaceSlug) return;
const handleAttributeUpdate = (attributeId: string, value: string | string[] | undefined) => {
if (!issue || !workspaceSlug) return;
const payload: ICustomAttributeValueFormData = {
issue_properties: {
[attributeId]: value,
},
if (!value) {
deleteAttributeValue(workspaceSlug.toString(), projectId, issue.id, attributeId);
return;
}
const payload: ICustomAttributeValueFormData = {
issue_properties: {
[attributeId]: Array.isArray(value) ? value : [value],
},
};
customAttributeValuesStore.createAttributeValue(
workspaceSlug.toString(),
issue.project,
issue.id,
payload
);
};
customAttributeValuesStore.createAttributeValue(
workspaceSlug.toString(),
issue.project,
issue.id,
payload
);
};
// fetch the object details if object state has id
useEffect(() => {
if (!issue?.entity) return;
// fetch the object details if object state has id
useEffect(() => {
if (!issue?.entity) return;
if (!entityAttributes[issue.entity]) {
if (!workspaceSlug) return;
if (!entityAttributes[issue.entity]) {
if (!workspaceSlug) return;
fetchEntityDetails(workspaceSlug.toString(), issue.entity);
}
}, [issue?.entity, entityAttributes, fetchEntityDetails, workspaceSlug]);
fetchEntityDetails(workspaceSlug.toString(), issue.entity);
}
}, [issue?.entity, entityAttributes, fetchEntityDetails, workspaceSlug]);
// fetch issue attribute values
useEffect(() => {
if (!issue) return;
// fetch issue attribute values
useEffect(() => {
if (!issue) return;
if (!issueAttributeValues || !issueAttributeValues[issue.id]) {
if (!workspaceSlug) return;
if (!issueAttributeValues || !issueAttributeValues[issue.id]) {
if (!workspaceSlug) return;
fetchIssueAttributeValues(workspaceSlug.toString(), issue.project, issue.id);
}
}, [fetchIssueAttributeValues, issue, issueAttributeValues, workspaceSlug]);
fetchIssueAttributeValues(workspaceSlug.toString(), issue.project, issue.id);
}
}, [fetchIssueAttributeValues, issue, issueAttributeValues, workspaceSlug]);
if (!issue || !issue?.entity) return null;
if (!issue || !issue?.entity) return null;
if (!entityAttributes[issue.entity] || !issueAttributeValues?.[issue.id])
return (
<Loader className="space-y-4">
<Loader.Item height="30px" />
<Loader.Item height="30px" />
<Loader.Item height="30px" />
<Loader.Item height="30px" />
</Loader>
);
if (!entityAttributes[issue.entity] || !issueAttributeValues?.[issue.id])
return (
<Loader className="space-y-4">
<Loader.Item height="30px" />
<Loader.Item height="30px" />
<Loader.Item height="30px" />
<Loader.Item height="30px" />
</Loader>
<>
{Object.values(entityAttributes?.[issue.entity] ?? {}).map((attribute) => {
const typeMetaData = CUSTOM_ATTRIBUTES_LIST[attribute.type];
const attributeValue = issueAttributeValues?.[issue.id].find(
(a) => a.id === attribute.id
)?.prop_value;
return (
<div key={attribute.id} className="flex items-center gap-2 text-sm">
<div className="flex-shrink-0 w-1/4 flex items-center gap-2 font-medium">
<typeMetaData.icon className="flex-shrink-0" size={16} strokeWidth={1.5} />
<p className="flex-grow truncate">{attribute.display_name}</p>
</div>
<div className="w-3/4 max-w-[20rem]">
{attribute.type === "checkbox" && (
<CustomCheckboxAttribute
attributeDetails={attribute}
issueId={issue.id}
onChange={(val) => handleAttributeUpdate(attribute.id, [`${val}`])}
projectId={issue.project}
value={
attributeValue
? attributeValue?.[0]?.value === "true"
? true
: false
: false
}
/>
)}
{attribute.type === "datetime" && (
<CustomDateTimeAttribute
attributeDetails={attribute}
issueId={issue.id}
onChange={(val) => {
handleAttributeUpdate(attribute.id, val ? [val.toISOString()] : undefined);
}}
projectId={issue.project}
value={attributeValue ? new Date(attributeValue?.[0]?.value ?? "") : undefined}
/>
)}
{attribute.type === "email" && (
<CustomEmailAttribute
attributeDetails={attribute}
issueId={issue.id}
onChange={(val) => {
handleAttributeUpdate(attribute.id, val && val !== "" ? [val] : undefined);
}}
projectId={issue.project}
value={attributeValue ? attributeValue?.[0]?.value : undefined}
/>
)}
{attribute.type === "file" && (
<CustomFileAttribute
attributeDetails={attribute}
issueId={issue.id}
onChange={(val) => handleAttributeUpdate(attribute.id, val)}
projectId={issue.project}
value={attributeValue ? attributeValue?.[0]?.value : undefined}
/>
)}
{attribute.type === "multi_select" && (
<CustomSelectAttribute
attributeDetails={attribute}
issueId={issue.id}
onChange={(val) => handleAttributeUpdate(attribute.id, val)}
projectId={issue.project}
value={Array.isArray(attributeValue) ? attributeValue.map((v) => v.value) : []}
multiple
/>
)}
{attribute.type === "number" && (
<CustomNumberAttribute
attributeDetails={attribute}
issueId={issue.id}
onChange={(val) => {
handleAttributeUpdate(attribute.id, val ? [val.toString()] : undefined);
}}
projectId={issue.project}
value={
attributeValue ? parseInt(attributeValue?.[0]?.value ?? "0", 10) : undefined
}
/>
)}
{attribute.type === "relation" && (
<CustomRelationAttribute
attributeDetails={attribute}
issueId={issue.id}
onChange={(val) => handleAttributeUpdate(attribute.id, val)}
projectId={issue.project}
value={attributeValue ? attributeValue?.[0]?.value : undefined}
/>
)}
{attribute.type === "select" && (
<CustomSelectAttribute
attributeDetails={attribute}
issueId={issue.id}
onChange={(val) => handleAttributeUpdate(attribute.id, val)}
projectId={issue.project}
value={attributeValue ? attributeValue?.[0]?.value : undefined}
multiple={false}
/>
)}
{attribute.type === "text" && (
<CustomTextAttribute
attributeDetails={attribute}
issueId={issue.id}
onChange={(val) =>
handleAttributeUpdate(attribute.id, val && val !== "" ? [val] : undefined)
}
projectId={issue.project}
value={attributeValue ? attributeValue?.[0].value : undefined}
/>
)}
{attribute.type === "url" && (
<CustomUrlAttribute
attributeDetails={attribute}
issueId={issue.id}
onChange={(val) =>
handleAttributeUpdate(attribute.id, val && val !== "" ? [val] : undefined)
}
projectId={issue.project}
value={attributeValue ? attributeValue?.[0]?.value : undefined}
/>
)}
</div>
</div>
);
})}
</>
);
return (
<>
{Object.values(entityAttributes?.[issue.entity] ?? {}).map((attribute) => {
const typeMetaData = CUSTOM_ATTRIBUTES_LIST[attribute.type];
const attributeValue = issueAttributeValues?.[issue.id].find(
(a) => a.id === attribute.id
)?.prop_value;
return (
<div key={attribute.id} className="flex items-center gap-2 text-sm">
<div className="flex-shrink-0 w-1/4 flex items-center gap-2 font-medium">
<typeMetaData.icon className="flex-shrink-0" size={16} strokeWidth={1.5} />
<p className="flex-grow truncate">{attribute.display_name}</p>
</div>
<div className="w-3/4">
{attribute.type === "checkbox" && (
<CustomCheckboxAttribute
attributeDetails={attribute}
issueId={issue.id}
onChange={(val: string) => handleAttributeUpdate(attribute.id, val)}
projectId={issue.project}
value={
attributeValue ? (attributeValue?.[0]?.value === "true" ? true : false) : false
}
/>
)}
{attribute.type === "datetime" && (
<CustomDateTimeAttribute
attributeDetails={attribute}
issueId={issue.id}
onChange={(val: string) => handleAttributeUpdate(attribute.id, val)}
projectId={issue.project}
value={attributeValue ? new Date(attributeValue?.[0]?.value ?? "") : undefined}
/>
)}
{attribute.type === "email" && (
<CustomEmailAttribute
attributeDetails={attribute}
issueId={issue.id}
onChange={(val: string) => handleAttributeUpdate(attribute.id, val)}
projectId={issue.project}
value={attributeValue ? attributeValue?.[0]?.value : undefined}
/>
)}
{attribute.type === "number" && (
<CustomNumberAttribute
attributeDetails={attribute}
issueId={issue.id}
onChange={(val: string) => handleAttributeUpdate(attribute.id, val)}
projectId={issue.project}
value={
attributeValue ? parseInt(attributeValue?.[0]?.value ?? "0", 10) : undefined
}
/>
)}
{attribute.type === "relation" && (
<CustomRelationAttribute
attributeDetails={attribute}
issueId={issue.id}
onChange={(val: string) => handleAttributeUpdate(attribute.id, val)}
projectId={issue.project}
value={attributeValue ? attributeValue?.[0]?.value : undefined}
/>
)}
{attribute.type === "select" && (
<CustomSelectAttribute
attributeDetails={attribute}
issueId={issue.id}
onChange={(val: string) => handleAttributeUpdate(attribute.id, val)}
projectId={issue.project}
value={attributeValue ? attributeValue?.[0]?.value : undefined}
/>
)}
{attribute.type === "text" && (
<CustomTextAttribute
attributeDetails={attribute}
issueId={issue.id}
onChange={(val: string) => handleAttributeUpdate(attribute.id, val)}
projectId={issue.project}
value={attributeValue ? attributeValue?.[0].value : undefined}
/>
)}
{attribute.type === "url" && (
<CustomUrlAttribute
attributeDetails={attribute}
issueId={issue.id}
onChange={(val: string) => handleAttributeUpdate(attribute.id, val)}
projectId={issue.project}
value={attributeValue ? attributeValue?.[0]?.value : undefined}
/>
)}
</div>
</div>
);
})}
</>
);
});
}
);

View File

@ -127,7 +127,7 @@ export const SidebarCustomAttributesList: React.FC<Props> = observer(({ issue, p
<CustomDateTimeAttribute
attributeDetails={attribute}
issueId={issue.id}
onChange={(val: Date | null) => {
onChange={(val) => {
handleAttributeUpdate(attribute.id, val ? [val.toISOString()] : undefined);
}}
projectId={issue.project}
@ -169,7 +169,7 @@ export const SidebarCustomAttributesList: React.FC<Props> = observer(({ issue, p
attributeDetails={attribute}
issueId={issue.id}
onChange={(val) => {
handleAttributeUpdate(attribute.id, val ? val.toString() : undefined);
handleAttributeUpdate(attribute.id, val ? [val.toString()] : undefined);
}}
projectId={issue.project}
value={

View File

@ -66,7 +66,7 @@ export interface IssueFormProps {
setIsConfirmDiscardOpen: React.Dispatch<React.SetStateAction<boolean>>;
handleFormDirty: (payload: Partial<IIssue> | null) => void;
customAttributesList: { [key: string]: string[] };
handleCustomAttributesChange: (attributeId: string, val: string[]) => void;
handleCustomAttributesChange: (attributeId: string, val: string | string[] | undefined) => void;
fieldsToShow: TIssueFormAttributes[];
}
@ -429,7 +429,7 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
/>
)}
{/* default object properties */}
{watch("entity") === null ? (
{watch("entity") === null && (
<>
{(fieldsToShow.includes("all") || fieldsToShow.includes("priority")) && (
<Controller
@ -555,7 +555,10 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
</CustomMenu>
)}
</>
) : (
)}
</div>
{watch("entity") !== null && (
<div>
<IssueModalCustomAttributesList
entityId={watch("entity") ?? ""}
issueId={watch("id") ?? ""}
@ -563,8 +566,8 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
projectId={projectId}
values={customAttributesList}
/>
)}
</div>
</div>
)}
</div>
</div>
</div>

View File

@ -128,6 +128,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer(
const onDiscardClose = () => {
handleClose();
setActiveProject(null);
setCustomAttributesList({});
};
const handleFormDirty = (data: any) => {
@ -395,20 +396,35 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer(
const handleFormSubmit = async (formData: Partial<IIssue>) => {
if (!workspaceSlug || !activeProject) return;
const payload: Partial<IIssue> = {
...formData,
assignees_list: formData.assignees ?? [],
labels_list: formData.labels ?? [],
description: formData.description ?? "",
// set the fixed issue properties for the payload
let payload: Partial<IIssue> = {
description_html: formData.description_html ?? "<p></p>",
entity: formData.entity,
name: formData.name,
state: formData.state,
};
// if entity is null, set the default object entity properties for the payload
if (formData.entity === null)
payload = {
...payload,
...formData,
assignees_list: formData.assignees ?? [],
labels_list: formData.labels ?? [],
};
let issueResponse: Partial<IIssue> | undefined = {};
if (!data) issueResponse = await createIssue(payload);
else issueResponse = await updateIssue(payload);
if (issueResponse && issueResponse.id && Object.keys(customAttributesList).length > 0)
// create custom attribute values, if any
if (
payload.entity !== null &&
issueResponse &&
issueResponse.id &&
Object.keys(customAttributesList).length > 0
)
await createAttributeValue(workspaceSlug.toString(), activeProject, issueResponse.id, {
issue_properties: customAttributesList,
});
@ -416,6 +432,27 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer(
if (onSubmit) await onSubmit(payload);
};
const handleCustomAttributesChange = (
attributeId: string,
val: string | string[] | undefined
) => {
if (!val) {
setCustomAttributesList((prev) => {
const newCustomAttributesList = { ...prev };
delete newCustomAttributesList[attributeId];
return newCustomAttributesList;
});
return;
}
setCustomAttributesList((prev) => ({
...prev,
[attributeId]: Array.isArray(val) ? val : [val],
}));
};
if (!projects || projects.length === 0) return null;
return (
@ -471,12 +508,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer(
status={data ? true : false}
user={user}
customAttributesList={customAttributesList}
handleCustomAttributesChange={(attributeId: string, val: string[]) => {
setCustomAttributesList((prev) => ({
...prev,
[attributeId]: val,
}));
}}
handleCustomAttributesChange={handleCustomAttributesChange}
fieldsToShow={fieldsToShow}
handleFormDirty={handleFormDirty}
/>

View File

@ -165,7 +165,9 @@ export const PeekOverviewIssueProperties: React.FC<Props> = ({
</div>
</>
)}
{issue.entity !== null && <PeekOverviewCustomAttributesList issue={issue} />}
{issue.entity !== null && (
<PeekOverviewCustomAttributesList issue={issue} projectId={issue.project} />
)}
{/* <div className="flex items-center gap-2 text-sm">
<div className="flex-shrink-0 w-1/4 flex items-center gap-2 font-medium">
<Icon iconName="change_history" className="!text-base flex-shrink-0" />

View File

@ -148,7 +148,7 @@ const IssueDetailsPage: NextPage = () => {
<div className="w-2/3 h-full overflow-y-auto space-y-5 divide-y divide-custom-border-200 p-5">
<IssueMainContent issueDetails={issueDetails} submitChanges={submitChanges} />
</div>
<div className="w-1/3 h-full space-y-5 border-l border-custom-border-300 py-5 overflow-hidden">
<div className="w-1/3 h-full space-y-5 border-l border-custom-border-200 py-5 overflow-hidden">
<IssueDetailsSidebar
control={control}
issueDetail={issueDetails}

View File

@ -100,6 +100,8 @@ class CustomAttributeValuesStore {
runInAction(() => {
this.error = error;
});
this.fetchIssueAttributeValues(workspaceSlug, projectId, issueId);
}
};
@ -130,6 +132,8 @@ class CustomAttributeValuesStore {
runInAction(() => {
this.error = error;
});
this.fetchIssueAttributeValues(workspaceSlug, projectId, issueId);
}
};
}