forked from github/plane
refactor: issue modal custom attributes
This commit is contained in:
parent
d04eac30b0
commit
4842fc8e58
@ -1,3 +1,3 @@
|
||||
export * from "./issue-modal-attributes-list";
|
||||
export * from "./issue-modal";
|
||||
export * from "./peek-overview-custom-attributes-list";
|
||||
export * from "./sidebar-custom-attributes-list";
|
||||
|
@ -1,197 +0,0 @@
|
||||
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,
|
||||
CustomFileAttribute,
|
||||
CustomRelationAttribute,
|
||||
CustomSelectAttribute,
|
||||
} from "components/custom-attributes";
|
||||
// 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 | string[] | undefined) => void;
|
||||
projectId: string;
|
||||
values: { [key: string]: string[] };
|
||||
};
|
||||
|
||||
const DESCRIPTION_FIELDS: TCustomAttributeTypes[] = ["email", "number", "text", "url"];
|
||||
|
||||
export const IssueModalCustomAttributesList: React.FC<Props> = observer((props) => {
|
||||
const { entityId, issueId, onChange, projectId, values } = props;
|
||||
|
||||
const [hideOptionalFields, setHideOptionalFields] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { customAttributes: customAttributesStore } = useMobxStore();
|
||||
const { entityAttributes, fetchEntityDetails, fetchEntityDetailsLoader } = customAttributesStore;
|
||||
|
||||
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-2 flex-wrap mt-3.5">
|
||||
{Object.entries(nonDescriptionFields).map(([attributeId, attribute]) => (
|
||||
<div key={attributeId}>
|
||||
{attribute.type === "checkbox" && (
|
||||
<CustomCheckboxAttribute
|
||||
attributeDetails={attribute}
|
||||
issueId={issueId}
|
||||
onChange={(val) => onChange(attribute.id, [`${val}`])}
|
||||
projectId={projectId}
|
||||
value={values[attribute.id]?.[0] === "true" ? true : false}
|
||||
/>
|
||||
)}
|
||||
{attribute.type === "datetime" && (
|
||||
<CustomDateTimeAttribute
|
||||
attributeDetails={attribute}
|
||||
className="bg-transparent border border-custom-border-200 py-1"
|
||||
issueId={issueId}
|
||||
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 === "file" && (
|
||||
<CustomFileAttribute
|
||||
attributeDetails={attribute}
|
||||
className="bg-transparent border border-custom-border-200 py-1"
|
||||
issueId={issueId}
|
||||
onChange={(val) => onChange(attribute.id, val)}
|
||||
projectId={projectId}
|
||||
value={values[attribute.id]?.[0]}
|
||||
/>
|
||||
)}
|
||||
{attribute.type === "multi_select" && (
|
||||
<CustomSelectAttribute
|
||||
attributeDetails={attribute}
|
||||
className="bg-transparent border border-custom-border-200 py-1"
|
||||
issueId={issueId}
|
||||
onChange={(val) => onChange(attribute.id, val)}
|
||||
projectId={projectId}
|
||||
value={values[attribute.id] ?? []}
|
||||
multiple
|
||||
/>
|
||||
)}
|
||||
{attribute.type === "relation" && (
|
||||
<CustomRelationAttribute
|
||||
attributeDetails={attribute}
|
||||
className="bg-transparent border border-custom-border-200 py-1"
|
||||
issueId={issueId}
|
||||
onChange={(val) => onChange(attribute.id, val)}
|
||||
projectId={projectId}
|
||||
value={values[attribute.id]?.[0]}
|
||||
/>
|
||||
)}
|
||||
{attribute.type === "select" && (
|
||||
<CustomSelectAttribute
|
||||
attributeDetails={attribute}
|
||||
className="bg-transparent border border-custom-border-200 py-1"
|
||||
issueId={issueId}
|
||||
onChange={(val) => onChange(attribute.id, val)}
|
||||
projectId={projectId}
|
||||
value={values[attribute.id]?.[0]}
|
||||
multiple={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
@ -0,0 +1,80 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// mobx
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { CustomCheckboxAttribute } from "components/custom-attributes";
|
||||
// ui
|
||||
import { Loader, Tooltip } from "components/ui";
|
||||
|
||||
type Props = {
|
||||
entityId: string;
|
||||
issueId: string;
|
||||
onChange: (attributeId: string, val: string | string[] | undefined) => void;
|
||||
projectId: string;
|
||||
values: { [key: string]: string[] };
|
||||
};
|
||||
|
||||
export const CustomAttributesCheckboxes: React.FC<Props> = observer((props) => {
|
||||
const { entityId, issueId, onChange, projectId, values } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { customAttributes: customAttributesStore } = useMobxStore();
|
||||
const { entityAttributes, fetchEntityDetails, fetchEntityDetailsLoader } = customAttributesStore;
|
||||
|
||||
const attributes = entityAttributes[entityId] ?? {};
|
||||
|
||||
// fetch entity details
|
||||
useEffect(() => {
|
||||
if (!entityAttributes[entityId]) {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
fetchEntityDetails(workspaceSlug.toString(), entityId);
|
||||
}
|
||||
}, [entityAttributes, entityId, fetchEntityDetails, workspaceSlug]);
|
||||
|
||||
const checkboxFields = Object.values(attributes).filter((a) => a.type === "checkbox");
|
||||
|
||||
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>
|
||||
) : (
|
||||
<div>
|
||||
<h5 className="text-sm">Checkboxes</h5>
|
||||
<div className="mt-3.5 space-y-4">
|
||||
{Object.entries(checkboxFields).map(([attributeId, attribute]) => (
|
||||
<div key={attributeId} className="flex items-center gap-2">
|
||||
<Tooltip tooltipContent={attribute.display_name} position="top-left">
|
||||
<p className="text-xs text-custom-text-300 w-1/3 truncate">
|
||||
{attribute.display_name}
|
||||
</p>
|
||||
</Tooltip>
|
||||
<div className="w-2/3 flex-shrink-0">
|
||||
{attribute.type === "checkbox" && (
|
||||
<CustomCheckboxAttribute
|
||||
attributeDetails={attribute}
|
||||
issueId={issueId}
|
||||
onChange={(val) => onChange(attribute.id, [`${val}`])}
|
||||
projectId={projectId}
|
||||
value={values[attribute.id]?.[0] === "true" ? true : false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
@ -0,0 +1,113 @@
|
||||
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";
|
||||
// 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 | string[] | undefined) => void;
|
||||
projectId: string;
|
||||
values: { [key: string]: string[] };
|
||||
};
|
||||
|
||||
const DESCRIPTION_FIELDS: TCustomAttributeTypes[] = ["email", "number", "text", "url"];
|
||||
|
||||
export const CustomAttributesDescriptionFields: React.FC<Props> = observer((props) => {
|
||||
const { entityId, issueId, onChange, projectId, values } = props;
|
||||
|
||||
const [hideOptionalFields, setHideOptionalFields] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { customAttributes: customAttributesStore } = useMobxStore();
|
||||
const { entityAttributes, fetchEntityDetails, fetchEntityDetailsLoader } = customAttributesStore;
|
||||
|
||||
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)
|
||||
);
|
||||
|
||||
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}
|
||||
/>
|
||||
Custom Attributes
|
||||
</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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
@ -0,0 +1,3 @@
|
||||
export * from "./checkboxes";
|
||||
export * from "./description-fields";
|
||||
export * from "./select-fields";
|
@ -0,0 +1,124 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// mobx
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import {
|
||||
CustomDateTimeAttribute,
|
||||
CustomFileAttribute,
|
||||
CustomRelationAttribute,
|
||||
CustomSelectAttribute,
|
||||
} from "components/custom-attributes";
|
||||
// ui
|
||||
import { Loader } from "components/ui";
|
||||
// types
|
||||
import { TCustomAttributeTypes } from "types";
|
||||
|
||||
type Props = {
|
||||
entityId: string;
|
||||
issueId: string;
|
||||
onChange: (attributeId: string, val: string | string[] | undefined) => void;
|
||||
projectId: string;
|
||||
values: { [key: string]: string[] };
|
||||
};
|
||||
|
||||
const SELECT_FIELDS: TCustomAttributeTypes[] = ["datetime", "multi_select", "relation", "select"];
|
||||
|
||||
export const CustomAttributesSelectFields: React.FC<Props> = observer((props) => {
|
||||
const { entityId, issueId, onChange, projectId, values } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { customAttributes: customAttributesStore } = useMobxStore();
|
||||
const { entityAttributes, fetchEntityDetails, fetchEntityDetailsLoader } = customAttributesStore;
|
||||
|
||||
const attributes = entityAttributes[entityId] ?? {};
|
||||
|
||||
// fetch entity details
|
||||
useEffect(() => {
|
||||
if (!entityAttributes[entityId]) {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
fetchEntityDetails(workspaceSlug.toString(), entityId);
|
||||
}
|
||||
}, [entityAttributes, entityId, fetchEntityDetails, workspaceSlug]);
|
||||
|
||||
const selectFields = Object.values(attributes).filter((a) => SELECT_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>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{Object.entries(selectFields).map(([attributeId, attribute]) => (
|
||||
<div key={attributeId}>
|
||||
{attribute.type === "datetime" && (
|
||||
<CustomDateTimeAttribute
|
||||
attributeDetails={attribute}
|
||||
className="bg-transparent border border-custom-border-200 py-1 shadow-custom-shadow-2xs"
|
||||
issueId={issueId}
|
||||
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 === "file" && (
|
||||
<CustomFileAttribute
|
||||
attributeDetails={attribute}
|
||||
className="bg-transparent border border-custom-border-200 py-1 shadow-custom-shadow-2xs"
|
||||
issueId={issueId}
|
||||
onChange={(val) => onChange(attribute.id, val)}
|
||||
projectId={projectId}
|
||||
value={values[attribute.id]?.[0]}
|
||||
/>
|
||||
)}
|
||||
{attribute.type === "multi_select" && (
|
||||
<CustomSelectAttribute
|
||||
attributeDetails={attribute}
|
||||
className="bg-transparent border border-custom-border-200 py-1 shadow-custom-shadow-2xs"
|
||||
issueId={issueId}
|
||||
onChange={(val) => onChange(attribute.id, val)}
|
||||
projectId={projectId}
|
||||
value={values[attribute.id] ?? []}
|
||||
multiple
|
||||
/>
|
||||
)}
|
||||
{attribute.type === "relation" && (
|
||||
<CustomRelationAttribute
|
||||
attributeDetails={attribute}
|
||||
className="bg-transparent border border-custom-border-200 py-1 shadow-custom-shadow-2xs"
|
||||
issueId={issueId}
|
||||
onChange={(val) => onChange(attribute.id, val)}
|
||||
projectId={projectId}
|
||||
value={values[attribute.id]?.[0]}
|
||||
/>
|
||||
)}
|
||||
{attribute.type === "select" && (
|
||||
<CustomSelectAttribute
|
||||
attributeDetails={attribute}
|
||||
className="bg-transparent border border-custom-border-200 py-1 shadow-custom-shadow-2xs"
|
||||
issueId={issueId}
|
||||
onChange={(val) => onChange(attribute.id, val)}
|
||||
projectId={projectId}
|
||||
value={values[attribute.id]?.[0]}
|
||||
multiple={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
@ -226,19 +226,27 @@ export const ObjectModal: React.FC<Props> = observer(
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<TypesDropdown onClick={handleCreateEntityAttribute} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-3 px-6 py-5 border-t border-custom-border-200">
|
||||
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
|
||||
{!object.id && (
|
||||
<PrimaryButton onClick={handleCreateObject} loading={isCreatingObject}>
|
||||
{isCreatingObject ? "Creating..." : "Create Object"}
|
||||
</PrimaryButton>
|
||||
<div
|
||||
className={`flex items-center gap-3 px-6 py-5 border-t border-custom-border-200 ${
|
||||
object.id ? "justify-between" : "justify-end"
|
||||
}`}
|
||||
>
|
||||
{object.id && (
|
||||
<div className="flex-shrink-0">
|
||||
<TypesDropdown onClick={handleCreateEntityAttribute} />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-3">
|
||||
<SecondaryButton onClick={handleClose}>Close</SecondaryButton>
|
||||
{!object.id && (
|
||||
<PrimaryButton onClick={handleCreateObject} loading={isCreatingObject}>
|
||||
{isCreatingObject ? "Creating..." : "Create Object"}
|
||||
</PrimaryButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
|
@ -4,7 +4,6 @@ import { useRouter } from "next/router";
|
||||
|
||||
// mobx
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// react-hook-form
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// services
|
||||
@ -25,7 +24,12 @@ import {
|
||||
} from "components/issues/select";
|
||||
import { CreateStateModal } from "components/states";
|
||||
import { CreateLabelModal } from "components/labels";
|
||||
import { IssueModalCustomAttributesList, ObjectsSelect } from "components/custom-attributes";
|
||||
import {
|
||||
CustomAttributesCheckboxes,
|
||||
CustomAttributesDescriptionFields,
|
||||
CustomAttributesSelectFields,
|
||||
ObjectsSelect,
|
||||
} from "components/custom-attributes";
|
||||
// ui
|
||||
import { CustomMenu, Input, PrimaryButton, SecondaryButton, ToggleSwitch } from "components/ui";
|
||||
import { TipTapEditor } from "components/tiptap";
|
||||
@ -253,7 +257,7 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
|
||||
</>
|
||||
)}
|
||||
<form onSubmit={handleSubmit(handleCreateUpdateIssue)}>
|
||||
<div className="space-y-5">
|
||||
<div className="space-y-5 p-5">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-x-2">
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("project")) && (
|
||||
@ -410,153 +414,16 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("state")) && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="state"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<IssueStateSelect
|
||||
setIsOpen={setStateModal}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
projectId={projectId}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{/* default object properties */}
|
||||
{watch("entity") === null && (
|
||||
<>
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("priority")) && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="priority"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<IssuePrioritySelect value={value} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("assignee")) && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="assignees"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<IssueAssigneeSelect
|
||||
projectId={projectId}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("label")) && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="labels"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<IssueLabelSelect
|
||||
setIsOpen={setLabelModal}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
projectId={projectId}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("startDate")) && (
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="start_date"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<IssueDateSelect
|
||||
label="Start date"
|
||||
maxDate={maxDate ?? undefined}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("dueDate")) && (
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="target_date"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<IssueDateSelect
|
||||
label="Due date"
|
||||
minDate={minDate ?? undefined}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) && (
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="estimate_point"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<IssueEstimateSelect value={value} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="parent"
|
||||
render={({ field: { onChange } }) => (
|
||||
<ParentIssuesListModal
|
||||
isOpen={parentIssueListModalOpen}
|
||||
handleClose={() => setParentIssueListModalOpen(false)}
|
||||
onChange={(issue) => {
|
||||
onChange(issue.id);
|
||||
setSelectedParentIssue(issue);
|
||||
}}
|
||||
projectId={projectId}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (
|
||||
<CustomMenu ellipsis>
|
||||
{watch("parent") ? (
|
||||
<>
|
||||
<CustomMenu.MenuItem
|
||||
renderAs="button"
|
||||
onClick={() => setParentIssueListModalOpen(true)}
|
||||
>
|
||||
Change parent issue
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
renderAs="button"
|
||||
onClick={() => setValue("parent", null)}
|
||||
>
|
||||
Remove parent issue
|
||||
</CustomMenu.MenuItem>
|
||||
</>
|
||||
) : (
|
||||
<CustomMenu.MenuItem
|
||||
renderAs="button"
|
||||
onClick={() => setParentIssueListModalOpen(true)}
|
||||
>
|
||||
Select Parent Issue
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
</CustomMenu>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{watch("entity") !== null && (
|
||||
<div>
|
||||
<IssueModalCustomAttributesList
|
||||
<div className="space-y-5">
|
||||
<CustomAttributesDescriptionFields
|
||||
entityId={watch("entity") ?? ""}
|
||||
issueId={watch("id") ?? ""}
|
||||
onChange={handleCustomAttributesChange}
|
||||
projectId={projectId}
|
||||
values={customAttributesList}
|
||||
/>
|
||||
<CustomAttributesCheckboxes
|
||||
entityId={watch("entity") ?? ""}
|
||||
issueId={watch("id") ?? ""}
|
||||
onChange={handleCustomAttributesChange}
|
||||
@ -568,31 +435,185 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="-mx-5 mt-5 flex items-center justify-between gap-2 border-t border-custom-border-200 px-5 pt-5">
|
||||
<div
|
||||
className="flex cursor-pointer items-center gap-1"
|
||||
onClick={() => setCreateMore((prevData) => !prevData)}
|
||||
>
|
||||
<span className="text-xs">Create more</span>
|
||||
<ToggleSwitch value={createMore} onChange={() => {}} size="sm" />
|
||||
<div className="space-y-4 px-5 py-4 border-t border-custom-border-200">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("state")) && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="state"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<IssueStateSelect
|
||||
setIsOpen={setStateModal}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
projectId={projectId}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{/* default object properties */}
|
||||
{watch("entity") === null ? (
|
||||
<>
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("priority")) && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="priority"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<IssuePrioritySelect value={value} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("assignee")) && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="assignees"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<IssueAssigneeSelect
|
||||
projectId={projectId}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("label")) && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="labels"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<IssueLabelSelect
|
||||
setIsOpen={setLabelModal}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
projectId={projectId}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("startDate")) && (
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="start_date"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<IssueDateSelect
|
||||
label="Start date"
|
||||
maxDate={maxDate ?? undefined}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("dueDate")) && (
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="target_date"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<IssueDateSelect
|
||||
label="Due date"
|
||||
minDate={minDate ?? undefined}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) && (
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="estimate_point"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<IssueEstimateSelect value={value} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="parent"
|
||||
render={({ field: { onChange } }) => (
|
||||
<ParentIssuesListModal
|
||||
isOpen={parentIssueListModalOpen}
|
||||
handleClose={() => setParentIssueListModalOpen(false)}
|
||||
onChange={(issue) => {
|
||||
onChange(issue.id);
|
||||
setSelectedParentIssue(issue);
|
||||
}}
|
||||
projectId={projectId}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (
|
||||
<CustomMenu ellipsis>
|
||||
{watch("parent") ? (
|
||||
<>
|
||||
<CustomMenu.MenuItem
|
||||
renderAs="button"
|
||||
onClick={() => setParentIssueListModalOpen(true)}
|
||||
>
|
||||
Change parent issue
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
renderAs="button"
|
||||
onClick={() => setValue("parent", null)}
|
||||
>
|
||||
Remove parent issue
|
||||
</CustomMenu.MenuItem>
|
||||
</>
|
||||
) : (
|
||||
<CustomMenu.MenuItem
|
||||
renderAs="button"
|
||||
onClick={() => setParentIssueListModalOpen(true)}
|
||||
>
|
||||
Select Parent Issue
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
</CustomMenu>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<CustomAttributesSelectFields
|
||||
entityId={watch("entity") ?? ""}
|
||||
issueId={watch("id") ?? ""}
|
||||
onChange={handleCustomAttributesChange}
|
||||
projectId={projectId}
|
||||
values={customAttributesList}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<SecondaryButton
|
||||
onClick={() => {
|
||||
handleDiscardClose();
|
||||
}}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div
|
||||
className="flex cursor-pointer items-center gap-1"
|
||||
onClick={() => setCreateMore((prevData) => !prevData)}
|
||||
>
|
||||
Discard
|
||||
</SecondaryButton>
|
||||
<PrimaryButton type="submit" loading={isSubmitting}>
|
||||
{status
|
||||
? isSubmitting
|
||||
? "Updating Issue..."
|
||||
: "Update Issue"
|
||||
: isSubmitting
|
||||
? "Adding Issue..."
|
||||
: "Add Issue"}
|
||||
</PrimaryButton>
|
||||
<span className="text-xs">Create more</span>
|
||||
<ToggleSwitch value={createMore} onChange={() => {}} size="sm" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<SecondaryButton
|
||||
onClick={() => {
|
||||
handleDiscardClose();
|
||||
}}
|
||||
>
|
||||
Discard
|
||||
</SecondaryButton>
|
||||
<PrimaryButton type="submit" loading={isSubmitting}>
|
||||
{status
|
||||
? isSubmitting
|
||||
? "Updating Issue..."
|
||||
: "Update Issue"
|
||||
: isSubmitting
|
||||
? "Adding Issue..."
|
||||
: "Add Issue"}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -510,7 +510,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer(
|
||||
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-20">
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
@ -521,7 +521,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer(
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform rounded-lg border border-custom-border-200 bg-custom-background-100 p-5 text-left shadow-xl transition-all sm:w-full sm:max-w-2xl">
|
||||
<Dialog.Panel className="relative transform rounded-lg border border-custom-border-200 bg-custom-background-100 text-left shadow-xl transition-all sm:w-full sm:max-w-2xl">
|
||||
<IssueForm
|
||||
handleFormSubmit={handleFormSubmit}
|
||||
initialData={data ?? prePopulateData}
|
||||
|
@ -78,7 +78,7 @@ export const CustomSearchSelect = ({
|
||||
) : (
|
||||
<Combobox.Button
|
||||
type="button"
|
||||
className={`flex items-center justify-between gap-1 w-full rounded shadow-sm border border-custom-border-200 duration-300 focus:outline-none ${
|
||||
className={`flex items-center justify-between gap-1 w-full rounded shadow-custom-shadow-2xs border border-custom-border-200 duration-300 focus:outline-none ${
|
||||
input ? "px-3 py-2 text-sm" : "px-2.5 py-1 text-xs"
|
||||
} ${
|
||||
disabled
|
||||
|
@ -45,7 +45,7 @@ const CustomSelect = ({
|
||||
) : (
|
||||
<Listbox.Button
|
||||
type="button"
|
||||
className={`flex items-center justify-between gap-1 w-full rounded border border-custom-border-200 shadow-sm duration-300 focus:outline-none ${
|
||||
className={`flex items-center justify-between gap-1 w-full rounded border border-custom-border-200 shadow-custom-shadow-2xs duration-300 focus:outline-none ${
|
||||
input ? "px-3 py-2 text-sm" : "px-2.5 py-1 text-xs"
|
||||
} ${
|
||||
disabled
|
||||
|
Loading…
Reference in New Issue
Block a user