forked from github/plane
refactor: display components
This commit is contained in:
parent
14be78564a
commit
7cf263ecd4
@ -1,17 +1,25 @@
|
||||
// ui
|
||||
import { ToggleSwitch } from "components/ui";
|
||||
// types
|
||||
import { Props } from "./types";
|
||||
import { ICustomAttribute } from "types";
|
||||
|
||||
type Props = {
|
||||
attributeDetails: ICustomAttribute;
|
||||
issueId: string;
|
||||
projectId: string;
|
||||
value: boolean;
|
||||
onChange: (val: boolean) => void;
|
||||
};
|
||||
|
||||
export const CustomCheckboxAttribute: React.FC<Props & { value: boolean }> = ({
|
||||
attributeDetails,
|
||||
onChange,
|
||||
value,
|
||||
}) => {
|
||||
const handleUpdateCheckbox = (val: boolean | string) => onChange(val.toString());
|
||||
const handleUpdateCheckbox = (val: boolean) => onChange(val);
|
||||
|
||||
return (
|
||||
<div className="bg-custom-background-80 flex items-center gap-2 rounded px-2.5 py-0.5 text-xs">
|
||||
<div className="text-xs truncate">
|
||||
{attributeDetails.extra_settings.representation === "toggle_switch" ? (
|
||||
<ToggleSwitch value={value ?? false} onChange={handleUpdateCheckbox} />
|
||||
) : (
|
||||
@ -23,7 +31,6 @@ export const CustomCheckboxAttribute: React.FC<Props & { value: boolean }> = ({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<span>{attributeDetails.display_name}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,7 +1,15 @@
|
||||
// react-datepicker
|
||||
import ReactDatePicker from "react-datepicker";
|
||||
// types
|
||||
import { Props } from "./types";
|
||||
import { ICustomAttribute } from "types";
|
||||
|
||||
type Props = {
|
||||
attributeDetails: ICustomAttribute;
|
||||
issueId: string;
|
||||
projectId: string;
|
||||
value: Date | undefined;
|
||||
onChange: (val: Date | null) => void;
|
||||
};
|
||||
|
||||
const DATE_FORMATS: { [key: string]: string } = {
|
||||
"MM-DD-YYYY": "MM-dd-yyyy",
|
||||
@ -14,16 +22,12 @@ const TIME_FORMATS: { [key: string]: string } = {
|
||||
"24": "HH:mm",
|
||||
};
|
||||
|
||||
export const CustomDateTimeAttribute: React.FC<Props & { value: Date | undefined }> = ({
|
||||
attributeDetails,
|
||||
onChange,
|
||||
value,
|
||||
}) => (
|
||||
export const CustomDateTimeAttribute: React.FC<Props> = ({ attributeDetails, onChange, value }) => (
|
||||
<div className="flex-shrink-0">
|
||||
<ReactDatePicker
|
||||
selected={value}
|
||||
onChange={onChange}
|
||||
className="bg-custom-background-80 rounded text-xs px-2.5 py-0.5 outline-none"
|
||||
className="bg-custom-background-80 rounded text-xs px-2.5 py-0.5 outline-none truncate"
|
||||
calendarClassName="!bg-custom-background-80"
|
||||
dateFormat={`${
|
||||
attributeDetails.extra_settings.hide_date
|
||||
@ -36,6 +40,7 @@ export const CustomDateTimeAttribute: React.FC<Props & { value: Date | undefined
|
||||
}`}
|
||||
showTimeInput={!attributeDetails.extra_settings.hide_time}
|
||||
isClearable={!attributeDetails.is_required}
|
||||
placeholderText={`Enter ${attributeDetails.display_name}`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -3,13 +3,17 @@ import { useEffect, useState } from "react";
|
||||
// react-hook-form
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// types
|
||||
import { Props } from "./types";
|
||||
import { ICustomAttribute } from "types";
|
||||
|
||||
export const CustomEmailAttribute: React.FC<Props & { value: string | undefined }> = ({
|
||||
attributeDetails,
|
||||
onChange,
|
||||
value,
|
||||
}) => {
|
||||
type Props = {
|
||||
attributeDetails: ICustomAttribute;
|
||||
issueId: string;
|
||||
projectId: string;
|
||||
value: string | undefined;
|
||||
onChange: (val: string) => void;
|
||||
};
|
||||
|
||||
export const CustomEmailAttribute: React.FC<Props> = ({ attributeDetails, onChange, value }) => {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
const { control, handleSubmit, reset, setFocus } = useForm({ defaultValues: { email: "" } });
|
||||
@ -45,7 +49,10 @@ export const CustomEmailAttribute: React.FC<Props & { value: string | undefined
|
||||
return (
|
||||
<div className="flex-shrink-0">
|
||||
{!isEditing && (
|
||||
<div className="cursor-pointer text-xs truncate" onClick={() => setIsEditing(true)}>
|
||||
<div
|
||||
className="cursor-pointer text-xs truncate bg-custom-background-80 px-2.5 py-0.5 w-min max-w-full whitespace-nowrap"
|
||||
onClick={() => setIsEditing(true)}
|
||||
>
|
||||
{value && value !== "" ? value : "Empty"}
|
||||
</div>
|
||||
)}
|
||||
|
@ -8,17 +8,26 @@ import { useDropzone } from "react-dropzone";
|
||||
import fileService from "services/file.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import useWorkspaceDetails from "hooks/use-workspace-details";
|
||||
// icons
|
||||
import { getFileIcon } from "components/icons";
|
||||
import { X } from "lucide-react";
|
||||
// helpers
|
||||
import { getFileExtension, getFileName } from "helpers/attachment.helper";
|
||||
// types
|
||||
import { Props } from "./types";
|
||||
import useWorkspaceDetails from "hooks/use-workspace-details";
|
||||
import { ICustomAttribute } from "types";
|
||||
|
||||
type Props = {
|
||||
attributeDetails: ICustomAttribute;
|
||||
issueId: string;
|
||||
projectId: string;
|
||||
value: string | undefined;
|
||||
onChange: (val: string | undefined) => void;
|
||||
};
|
||||
|
||||
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5 MB
|
||||
|
||||
export const CustomFileAttribute: React.FC<Props & { value: string | undefined }> = (props) => {
|
||||
export const CustomFileAttribute: React.FC<Props> = (props) => {
|
||||
const { attributeDetails, onChange, value } = props;
|
||||
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
@ -80,6 +89,13 @@ export const CustomFileAttribute: React.FC<Props & { value: string | undefined }
|
||||
]
|
||||
);
|
||||
|
||||
const handleRemoveFile = () => {
|
||||
if (!workspaceDetails || !value || value === "") return;
|
||||
|
||||
onChange(undefined);
|
||||
fileService.deleteFile(workspaceDetails.id, value);
|
||||
};
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive, isDragReject, fileRejections } = useDropzone({
|
||||
onDrop,
|
||||
maxSize: MAX_FILE_SIZE,
|
||||
@ -95,15 +111,24 @@ export const CustomFileAttribute: React.FC<Props & { value: string | undefined }
|
||||
return (
|
||||
<div className="flex-shrink-0 truncate space-y-1">
|
||||
{value && value !== "" && (
|
||||
<a
|
||||
href={value}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1 p-1 rounded border border-custom-border-200 text-xs truncate"
|
||||
>
|
||||
<span className="flex-shrink-0 h-6 w-6">{getFileIcon(getFileExtension(value))}</span>
|
||||
<span className="truncate">{getFileName(value)}</span>
|
||||
</a>
|
||||
<div className="group flex items-center justify-between gap-2 p-1 rounded border border-custom-border-200 text-xs truncate">
|
||||
<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>
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
className="hidden group-hover:grid place-items-center flex-shrink-0"
|
||||
onClick={handleRemoveFile}
|
||||
>
|
||||
<X size={12} strokeWidth={1.5} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
{...getRootProps()}
|
||||
|
@ -5,13 +5,17 @@ import { Controller, useForm } from "react-hook-form";
|
||||
// ui
|
||||
import { ProgressBar } from "components/ui";
|
||||
// types
|
||||
import { Props } from "./types";
|
||||
import { ICustomAttribute } from "types";
|
||||
|
||||
export const CustomNumberAttribute: React.FC<Props & { value: number | undefined }> = ({
|
||||
attributeDetails,
|
||||
onChange,
|
||||
value,
|
||||
}) => {
|
||||
type Props = {
|
||||
attributeDetails: ICustomAttribute;
|
||||
issueId: string;
|
||||
projectId: string;
|
||||
value: number | undefined;
|
||||
onChange: (val: number | undefined) => void;
|
||||
};
|
||||
|
||||
export const CustomNumberAttribute: React.FC<Props> = ({ attributeDetails, onChange, value }) => {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
const { control, handleSubmit, reset, setFocus } = useForm({ defaultValues: { number: "" } });
|
||||
@ -19,7 +23,10 @@ export const CustomNumberAttribute: React.FC<Props & { value: number | undefined
|
||||
const handleFormSubmit = (data: { number: string }) => {
|
||||
setIsEditing(false);
|
||||
|
||||
onChange(data.number);
|
||||
const number = parseInt(data.number, 10);
|
||||
|
||||
if (isNaN(number)) onChange(undefined);
|
||||
else onChange(number);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@ -84,10 +91,10 @@ export const CustomNumberAttribute: React.FC<Props & { value: number | undefined
|
||||
</>
|
||||
) : (
|
||||
<div
|
||||
className="cursor-pointer text-xs truncate bg-custom-background-80 px-2.5 py-0.5 rounded w-min whitespace-nowrap flex-grow"
|
||||
className="text-xs truncate bg-custom-background-80 px-2.5 py-0.5 w-min max-w-full whitespace-nowrap"
|
||||
onClick={() => setIsEditing(true)}
|
||||
>
|
||||
{value ?? `Enter ${attributeDetails.display_name}`}
|
||||
{value ?? "Empty"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -12,11 +12,19 @@ import modulesService from "services/modules.service";
|
||||
// icons
|
||||
import { Search } from "lucide-react";
|
||||
// types
|
||||
import { Props } from "./types";
|
||||
import { ICustomAttribute } from "types";
|
||||
// fetch-keys
|
||||
import { CYCLES_LIST, MODULE_LIST } from "constants/fetch-keys";
|
||||
|
||||
export const CustomRelationAttribute: React.FC<Props & { value: string | undefined }> = ({
|
||||
type Props = {
|
||||
attributeDetails: ICustomAttribute;
|
||||
issueId: string;
|
||||
projectId: string;
|
||||
value: string | undefined;
|
||||
onChange: (val: string | undefined) => void;
|
||||
};
|
||||
|
||||
export const CustomRelationAttribute: React.FC<Props> = ({
|
||||
attributeDetails,
|
||||
onChange,
|
||||
projectId,
|
||||
|
@ -7,15 +7,22 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// icons
|
||||
import { Check, Search } from "lucide-react";
|
||||
// types
|
||||
import { Props } from "./types";
|
||||
import { ICustomAttribute } from "types";
|
||||
|
||||
export const CustomSelectAttribute: React.FC<
|
||||
Props &
|
||||
(
|
||||
| { multiple?: false; value: string | undefined }
|
||||
| { multiple?: true; value: string[] | undefined }
|
||||
)
|
||||
> = (props) => {
|
||||
type Props = {
|
||||
attributeDetails: ICustomAttribute;
|
||||
issueId: string;
|
||||
projectId: string;
|
||||
} & (
|
||||
| {
|
||||
multiple?: false;
|
||||
onChange: (val: string | undefined) => void;
|
||||
value: string | undefined;
|
||||
}
|
||||
| { multiple?: true; onChange: (val: string[] | undefined) => void; value: string[] | undefined }
|
||||
);
|
||||
|
||||
export const CustomSelectAttribute: React.FC<Props> = (props) => {
|
||||
const { attributeDetails, multiple = false, onChange, value } = props;
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@ -62,8 +69,8 @@ export const CustomSelectAttribute: React.FC<
|
||||
});
|
||||
|
||||
const comboboxProps: any = {
|
||||
value,
|
||||
onChange,
|
||||
value,
|
||||
};
|
||||
|
||||
if (multiple) comboboxProps.multiple = true;
|
||||
@ -101,14 +108,16 @@ export const CustomSelectAttribute: React.FC<
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<span
|
||||
className="px-2.5 py-0.5 rounded text-xs"
|
||||
style={{
|
||||
backgroundColor: `${options.find((o) => o.id === value)?.color}40`,
|
||||
}}
|
||||
>
|
||||
{options.find((o) => o.id === value)?.display_name}
|
||||
</span>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span
|
||||
className="px-2.5 py-0.5 rounded text-xs"
|
||||
style={{
|
||||
backgroundColor: `${options.find((o) => o.id === value)?.color}40`,
|
||||
}}
|
||||
>
|
||||
{options.find((o) => o.id === value)?.display_name}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="cursor-pointer text-xs truncate bg-custom-background-80 px-2.5 py-0.5 rounded">
|
||||
|
@ -3,7 +3,15 @@ import { useEffect, useState } from "react";
|
||||
// react-hook-form
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// types
|
||||
import { Props } from "./types";
|
||||
import { ICustomAttribute } from "types";
|
||||
|
||||
type Props = {
|
||||
attributeDetails: ICustomAttribute;
|
||||
issueId: string;
|
||||
projectId: string;
|
||||
value: string | undefined;
|
||||
onChange: (val: string) => void;
|
||||
};
|
||||
|
||||
export const CustomTextAttribute: React.FC<Props & { value: string | undefined }> = ({
|
||||
attributeDetails,
|
||||
@ -45,7 +53,10 @@ export const CustomTextAttribute: React.FC<Props & { value: string | undefined }
|
||||
return (
|
||||
<div className="flex-shrink-0">
|
||||
{!isEditing && (
|
||||
<div className="cursor-pointer text-xs truncate" onClick={() => setIsEditing(true)}>
|
||||
<div
|
||||
className="cursor-pointer text-xs truncate bg-custom-background-80 px-2.5 py-0.5 w-min max-w-full whitespace-nowrap"
|
||||
onClick={() => setIsEditing(true)}
|
||||
>
|
||||
{value && value !== "" ? value : `Enter ${attributeDetails.display_name}`}
|
||||
</div>
|
||||
)}
|
||||
|
@ -1,9 +0,0 @@
|
||||
// types
|
||||
import { ICustomAttribute } from "types";
|
||||
|
||||
export type Props = {
|
||||
attributeDetails: ICustomAttribute;
|
||||
issueId: string;
|
||||
onChange: (value: any) => void;
|
||||
projectId: string;
|
||||
};
|
@ -3,7 +3,15 @@ import { useEffect, useState } from "react";
|
||||
// react-hook-form
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// types
|
||||
import { Props } from "./types";
|
||||
import { ICustomAttribute } from "types";
|
||||
|
||||
type Props = {
|
||||
attributeDetails: ICustomAttribute;
|
||||
issueId: string;
|
||||
projectId: string;
|
||||
value: string | undefined;
|
||||
onChange: (val: string) => void;
|
||||
};
|
||||
|
||||
export const CustomUrlAttribute: React.FC<Props & { value: string | undefined }> = ({
|
||||
attributeDetails,
|
||||
@ -43,8 +51,11 @@ export const CustomUrlAttribute: React.FC<Props & { value: string | undefined }>
|
||||
return (
|
||||
<div className="flex-shrink-0">
|
||||
{!isEditing && (
|
||||
<div className="cursor-pointer text-xs truncate" onClick={() => setIsEditing(true)}>
|
||||
{value && value !== "" ? value : `Enter ${attributeDetails.display_name}`}
|
||||
<div
|
||||
className="cursor-pointer text-xs truncate bg-custom-background-80 px-2.5 py-0.5 w-min max-w-full whitespace-nowrap"
|
||||
onClick={() => setIsEditing(true)}
|
||||
>
|
||||
{value && value !== "" ? value : "Empty"}
|
||||
</div>
|
||||
)}
|
||||
{isEditing && (
|
||||
|
@ -3,11 +3,11 @@ import { Controller } from "react-hook-form";
|
||||
// components
|
||||
import { FormComponentProps, Input } from "components/custom-attributes";
|
||||
// ui
|
||||
import { CustomSelect, ToggleSwitch } from "components/ui";
|
||||
import { CustomSelect, ToggleSwitch, Tooltip } from "components/ui";
|
||||
// constants
|
||||
import { DATE_FORMATS, TIME_FORMATS } from "constants/custom-attributes";
|
||||
|
||||
export const DateTimeAttributeForm: React.FC<FormComponentProps> = ({ control }) => (
|
||||
export const DateTimeAttributeForm: React.FC<FormComponentProps> = ({ control, watch }) => (
|
||||
<div className="space-y-3">
|
||||
<Controller
|
||||
control={control}
|
||||
@ -44,8 +44,20 @@ export const DateTimeAttributeForm: React.FC<FormComponentProps> = ({ control })
|
||||
name="extra_settings.hide_date"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<div className="flex items-center justify-end gap-1 mt-2">
|
||||
<ToggleSwitch value={value ?? false} onChange={onChange} size="sm" />
|
||||
<span className="text-xs">Don{"'"}t show date</span>
|
||||
<Tooltip
|
||||
tooltipContent="Cannot disable both, date and time"
|
||||
disabled={!watch("extra_settings.hide_time")}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<ToggleSwitch
|
||||
value={value ?? false}
|
||||
onChange={onChange}
|
||||
size="sm"
|
||||
disabled={watch("extra_settings.hide_time")}
|
||||
/>
|
||||
<span className="text-xs">Don{"'"}t show date</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
@ -78,8 +90,20 @@ export const DateTimeAttributeForm: React.FC<FormComponentProps> = ({ control })
|
||||
name="extra_settings.hide_time"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<div className="flex items-center justify-end gap-1 mt-2">
|
||||
<ToggleSwitch value={value ?? false} onChange={onChange} size="sm" />
|
||||
<span className="text-xs">Don{"'"}t show time</span>
|
||||
<Tooltip
|
||||
tooltipContent="Cannot disable both, date and time"
|
||||
disabled={!watch("extra_settings.hide_date")}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<ToggleSwitch
|
||||
value={value ?? false}
|
||||
onChange={onChange}
|
||||
size="sm"
|
||||
disabled={watch("extra_settings.hide_date")}
|
||||
/>
|
||||
<span className="text-xs">Don{"'"}t show time</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
@ -63,7 +63,7 @@ export const IssueModalCustomAttributesList: React.FC<Props> = observer(
|
||||
<CustomCheckboxAttribute
|
||||
attributeDetails={attribute}
|
||||
issueId={issueId}
|
||||
onChange={(val: string) => onChange(attribute.id, [val])}
|
||||
onChange={(val) => onChange(attribute.id, [`${val}`])}
|
||||
projectId={projectId}
|
||||
value={values[attribute.id]?.[0] === "true" ? true : false}
|
||||
/>
|
||||
@ -72,7 +72,7 @@ export const IssueModalCustomAttributesList: React.FC<Props> = observer(
|
||||
<CustomDateTimeAttribute
|
||||
attributeDetails={attribute}
|
||||
issueId={issueId}
|
||||
onChange={(val: string) => onChange(attribute.id, [val])}
|
||||
onChange={(val) => onChange(attribute.id, [val?.toISOString() ?? ""])}
|
||||
projectId={projectId}
|
||||
value={
|
||||
values[attribute.id]?.[0] ? new Date(values[attribute.id]?.[0]) : undefined
|
||||
@ -101,7 +101,9 @@ export const IssueModalCustomAttributesList: React.FC<Props> = observer(
|
||||
<CustomSelectAttribute
|
||||
attributeDetails={attribute}
|
||||
issueId={issueId}
|
||||
onChange={(val: string[]) => onChange(attribute.id, val)}
|
||||
onChange={(val) => {
|
||||
if (val) onChange(attribute.id, val);
|
||||
}}
|
||||
projectId={projectId}
|
||||
value={values[attribute.id] ?? []}
|
||||
multiple
|
||||
@ -111,7 +113,9 @@ export const IssueModalCustomAttributesList: React.FC<Props> = observer(
|
||||
<CustomNumberAttribute
|
||||
attributeDetails={attribute}
|
||||
issueId={issueId}
|
||||
onChange={(val: string) => onChange(attribute.id, [val])}
|
||||
onChange={(val) => {
|
||||
if (val) onChange(attribute.id, [val.toString()]);
|
||||
}}
|
||||
projectId={projectId}
|
||||
value={
|
||||
values[attribute.id]?.[0] ? parseInt(values[attribute.id]?.[0]) : undefined
|
||||
@ -122,7 +126,9 @@ export const IssueModalCustomAttributesList: React.FC<Props> = observer(
|
||||
<CustomRelationAttribute
|
||||
attributeDetails={attribute}
|
||||
issueId={issueId}
|
||||
onChange={(val: string) => onChange(attribute.id, [val])}
|
||||
onChange={(val) => {
|
||||
if (val) onChange(attribute.id, [val]);
|
||||
}}
|
||||
projectId={projectId}
|
||||
value={values[attribute.id]?.[0]}
|
||||
/>
|
||||
@ -131,16 +137,19 @@ export const IssueModalCustomAttributesList: React.FC<Props> = observer(
|
||||
<CustomSelectAttribute
|
||||
attributeDetails={attribute}
|
||||
issueId={issueId}
|
||||
onChange={(val: string) => onChange(attribute.id, [val])}
|
||||
onChange={(val) => {
|
||||
if (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: string) => onChange(attribute.id, [val])}
|
||||
onChange={(val) => onChange(attribute.id, [val])}
|
||||
projectId={projectId}
|
||||
value={attribute.default_value}
|
||||
/>
|
||||
@ -149,10 +158,7 @@ export const IssueModalCustomAttributesList: React.FC<Props> = observer(
|
||||
<CustomUrlAttribute
|
||||
attributeDetails={attribute}
|
||||
issueId={issueId}
|
||||
onChange={(val: string) => {
|
||||
console.log(val);
|
||||
onChange(attribute.id, [val]);
|
||||
}}
|
||||
onChange={(val: string) => onChange(attribute.id, [val])}
|
||||
projectId={projectId}
|
||||
value={values[attribute.id]?.[0]}
|
||||
/>
|
||||
|
@ -26,9 +26,10 @@ import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue | undefined;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export const SidebarCustomAttributesList: React.FC<Props> = observer(({ issue }) => {
|
||||
export const SidebarCustomAttributesList: React.FC<Props> = observer(({ issue, projectId }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
@ -37,14 +38,20 @@ export const SidebarCustomAttributesList: React.FC<Props> = observer(({ issue })
|
||||
customAttributeValues: customAttributeValuesStore,
|
||||
} = useMobxStore();
|
||||
const { entityAttributes, fetchEntityDetails } = customAttributesStore;
|
||||
const { issueAttributeValues, fetchIssueAttributeValues } = customAttributeValuesStore;
|
||||
const { issueAttributeValues, fetchIssueAttributeValues, deleteAttributeValue } =
|
||||
customAttributeValuesStore;
|
||||
|
||||
const handleAttributeUpdate = (attributeId: string, value: string[]) => {
|
||||
const handleAttributeUpdate = (attributeId: string, value: string | string[] | undefined) => {
|
||||
if (!issue || !workspaceSlug) return;
|
||||
|
||||
if (!value) {
|
||||
deleteAttributeValue(workspaceSlug.toString(), projectId, issue.id, attributeId);
|
||||
return;
|
||||
}
|
||||
|
||||
const payload: ICustomAttributeValueFormData = {
|
||||
issue_properties: {
|
||||
[attributeId]: value,
|
||||
[attributeId]: Array.isArray(value) ? value : [value],
|
||||
},
|
||||
};
|
||||
|
||||
@ -109,7 +116,7 @@ export const SidebarCustomAttributesList: React.FC<Props> = observer(({ issue })
|
||||
<CustomCheckboxAttribute
|
||||
attributeDetails={attribute}
|
||||
issueId={issue.id}
|
||||
onChange={(val: string) => handleAttributeUpdate(attribute.id, [val])}
|
||||
onChange={(val) => handleAttributeUpdate(attribute.id, [`${val}`])}
|
||||
projectId={issue.project}
|
||||
value={
|
||||
attributeValue ? (attributeValue?.[0]?.value === "true" ? true : false) : false
|
||||
@ -120,7 +127,9 @@ export const SidebarCustomAttributesList: React.FC<Props> = observer(({ issue })
|
||||
<CustomDateTimeAttribute
|
||||
attributeDetails={attribute}
|
||||
issueId={issue.id}
|
||||
onChange={(val: Date) => handleAttributeUpdate(attribute.id, [val.toISOString()])}
|
||||
onChange={(val: Date | null) => {
|
||||
handleAttributeUpdate(attribute.id, val ? [val.toISOString()] : undefined);
|
||||
}}
|
||||
projectId={issue.project}
|
||||
value={attributeValue ? new Date(attributeValue?.[0]?.value ?? "") : undefined}
|
||||
/>
|
||||
@ -129,7 +138,9 @@ export const SidebarCustomAttributesList: React.FC<Props> = observer(({ issue })
|
||||
<CustomEmailAttribute
|
||||
attributeDetails={attribute}
|
||||
issueId={issue.id}
|
||||
onChange={(val: string) => handleAttributeUpdate(attribute.id, [val])}
|
||||
onChange={(val) => {
|
||||
handleAttributeUpdate(attribute.id, val && val !== "" ? [val] : undefined);
|
||||
}}
|
||||
projectId={issue.project}
|
||||
value={attributeValue ? attributeValue?.[0]?.value : undefined}
|
||||
/>
|
||||
@ -138,7 +149,7 @@ export const SidebarCustomAttributesList: React.FC<Props> = observer(({ issue })
|
||||
<CustomFileAttribute
|
||||
attributeDetails={attribute}
|
||||
issueId={issue.id}
|
||||
onChange={(val: string) => handleAttributeUpdate(attribute.id, [val])}
|
||||
onChange={(val) => handleAttributeUpdate(attribute.id, val)}
|
||||
projectId={issue.project}
|
||||
value={attributeValue ? attributeValue?.[0]?.value : undefined}
|
||||
/>
|
||||
@ -147,7 +158,7 @@ export const SidebarCustomAttributesList: React.FC<Props> = observer(({ issue })
|
||||
<CustomSelectAttribute
|
||||
attributeDetails={attribute}
|
||||
issueId={issue.id}
|
||||
onChange={(val: string[]) => handleAttributeUpdate(attribute.id, val)}
|
||||
onChange={(val) => handleAttributeUpdate(attribute.id, val)}
|
||||
projectId={issue.project}
|
||||
value={Array.isArray(attributeValue) ? attributeValue.map((v) => v.value) : []}
|
||||
multiple
|
||||
@ -157,7 +168,9 @@ export const SidebarCustomAttributesList: React.FC<Props> = observer(({ issue })
|
||||
<CustomNumberAttribute
|
||||
attributeDetails={attribute}
|
||||
issueId={issue.id}
|
||||
onChange={(val: string) => handleAttributeUpdate(attribute.id, [val])}
|
||||
onChange={(val) => {
|
||||
handleAttributeUpdate(attribute.id, val ? val.toString() : undefined);
|
||||
}}
|
||||
projectId={issue.project}
|
||||
value={
|
||||
attributeValue ? parseInt(attributeValue?.[0]?.value ?? "0", 10) : undefined
|
||||
@ -168,7 +181,7 @@ export const SidebarCustomAttributesList: React.FC<Props> = observer(({ issue })
|
||||
<CustomRelationAttribute
|
||||
attributeDetails={attribute}
|
||||
issueId={issue.id}
|
||||
onChange={(val: string) => handleAttributeUpdate(attribute.id, [val])}
|
||||
onChange={(val) => handleAttributeUpdate(attribute.id, val)}
|
||||
projectId={issue.project}
|
||||
value={attributeValue ? attributeValue?.[0]?.value : undefined}
|
||||
/>
|
||||
@ -177,16 +190,19 @@ export const SidebarCustomAttributesList: React.FC<Props> = observer(({ issue })
|
||||
<CustomSelectAttribute
|
||||
attributeDetails={attribute}
|
||||
issueId={issue.id}
|
||||
onChange={(val: string) => handleAttributeUpdate(attribute.id, [val])}
|
||||
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: string) => handleAttributeUpdate(attribute.id, [val])}
|
||||
onChange={(val) =>
|
||||
handleAttributeUpdate(attribute.id, val && val !== "" ? [val] : undefined)
|
||||
}
|
||||
projectId={issue.project}
|
||||
value={attributeValue ? attributeValue?.[0].value : undefined}
|
||||
/>
|
||||
@ -195,7 +211,9 @@ export const SidebarCustomAttributesList: React.FC<Props> = observer(({ issue })
|
||||
<CustomUrlAttribute
|
||||
attributeDetails={attribute}
|
||||
issueId={issue.id}
|
||||
onChange={(val: string) => handleAttributeUpdate(attribute.id, [val])}
|
||||
onChange={(val) =>
|
||||
handleAttributeUpdate(attribute.id, val && val !== "" ? [val] : undefined)
|
||||
}
|
||||
projectId={issue.project}
|
||||
value={attributeValue ? attributeValue?.[0]?.value : undefined}
|
||||
/>
|
||||
|
@ -305,7 +305,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
data={issueDetail ?? null}
|
||||
user={user}
|
||||
/>
|
||||
<div className="h-full w-full flex flex-col divide-y-2 divide-custom-border-200 overflow-hidden">
|
||||
<div className="h-full w-full flex flex-col divide-y divide-custom-border-300 overflow-hidden">
|
||||
<div className="flex items-center justify-between px-5 pb-3">
|
||||
<h4 className="text-sm font-medium">
|
||||
{issueDetail?.project_detail?.identifier}-{issueDetail?.sequence_id}
|
||||
@ -349,30 +349,9 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
</div>
|
||||
|
||||
<div className="h-full w-full px-5 overflow-y-auto">
|
||||
<div className={`divide-y-2 divide-custom-border-200 ${uneditable ? "opacity-60" : ""}`}>
|
||||
<div className={`divide-y divide-custom-border-300 ${uneditable ? "opacity-60" : ""}`}>
|
||||
{showFirstSection && (
|
||||
<div className="py-1">
|
||||
{/* {(fieldsToShow.includes("all") || fieldsToShow.includes("entity")) && (
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:basis-1/2">
|
||||
<Squares2X2Icon className="h-4 w-4 flex-shrink-0" />
|
||||
<p>Object</p>
|
||||
</div>
|
||||
<div className="sm:basis-1/2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="entity"
|
||||
render={({ field: { value } }) => (
|
||||
<ObjectsSelect
|
||||
onChange={(val: string | null) => submitChanges({ entity: val })}
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)} */}
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("state")) && (
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:basis-1/2">
|
||||
@ -663,6 +642,14 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{watchIssue("entity") && (
|
||||
<div className="py-1">
|
||||
<SidebarCustomAttributesList
|
||||
issue={issueDetail}
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("label")) &&
|
||||
watchIssue("entity") === null && (
|
||||
@ -705,11 +692,6 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{watchIssue("entity") && (
|
||||
<div className="py-1">
|
||||
<SidebarCustomAttributesList issue={issueDetail} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
@ -91,6 +91,21 @@ class CustomAttributesService extends APIService {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async deletePropertyValue(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
propertyId: string
|
||||
): Promise<any> {
|
||||
return this.delete(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/property-values/${propertyId}/`
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const customAttributesService = new CustomAttributesService();
|
||||
|
@ -20,6 +20,7 @@ class CustomAttributeValuesStore {
|
||||
issueAttributeValues: observable.ref,
|
||||
fetchIssueAttributeValues: action,
|
||||
createAttributeValue: action,
|
||||
deleteAttributeValue: action,
|
||||
});
|
||||
|
||||
this.rootStore = _rootStore;
|
||||
@ -94,7 +95,6 @@ class CustomAttributeValuesStore {
|
||||
...this.issueAttributeValues,
|
||||
[issueId]: response.children,
|
||||
};
|
||||
this.fetchIssueAttributeValuesLoader = false;
|
||||
});
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
@ -102,6 +102,36 @@ class CustomAttributeValuesStore {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
deleteAttributeValue = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
propertyId: string
|
||||
) => {
|
||||
const newChildren = [...(this.issueAttributeValues?.[issueId] ?? [])];
|
||||
newChildren.filter((c) => c.id !== propertyId);
|
||||
|
||||
try {
|
||||
runInAction(() => {
|
||||
this.issueAttributeValues = {
|
||||
...this.issueAttributeValues,
|
||||
[issueId]: newChildren,
|
||||
};
|
||||
});
|
||||
|
||||
await customAttributesService.deletePropertyValue(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
issueId,
|
||||
propertyId
|
||||
);
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
this.error = error;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default CustomAttributeValuesStore;
|
||||
|
Loading…
Reference in New Issue
Block a user