forked from github/plane
chore: separate description fields in the create issue modal
This commit is contained in:
parent
7cf263ecd4
commit
6b2a5a97ac
@ -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>
|
||||
);
|
||||
|
@ -25,9 +25,7 @@ export const CustomEmailAttribute: React.FC<Props> = ({ attributeDetails, onChan
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditing) {
|
||||
setFocus("email");
|
||||
}
|
||||
if (isEditing) setFocus("email");
|
||||
}, [isEditing, setFocus]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -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} />
|
||||
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
|
@ -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>
|
||||
)
|
||||
) : (
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
@ -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={
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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" />
|
||||
|
@ -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}
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user