improvement: merge quick add logic for all layouts. (#5323)
This commit is contained in:
parent
333a989b1a
commit
49a895f117
37 changed files with 680 additions and 1033 deletions
|
|
@ -2,3 +2,4 @@ export * from "./bulk-operations";
|
|||
export * from "./worklog";
|
||||
export * from "./issue-modal";
|
||||
export * from "./issue-details";
|
||||
export * from "./quick-add";
|
||||
|
|
|
|||
1
web/ce/components/issues/quick-add/index.ts
Normal file
1
web/ce/components/issues/quick-add/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "./root";
|
||||
74
web/ce/components/issues/quick-add/root.tsx
Normal file
74
web/ce/components/issues/quick-add/root.tsx
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import { FC, useEffect, useRef } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { UseFormRegister, UseFormSetFocus } from "react-hook-form";
|
||||
// types
|
||||
import { TIssue } from "@plane/types";
|
||||
// components
|
||||
import {
|
||||
CalendarQuickAddIssueForm,
|
||||
GanttQuickAddIssueForm,
|
||||
KanbanQuickAddIssueForm,
|
||||
ListQuickAddIssueForm,
|
||||
SpreadsheetQuickAddIssueForm,
|
||||
TQuickAddIssueForm,
|
||||
} from "@/components/issues/issue-layouts";
|
||||
// constants
|
||||
import { EIssueLayoutTypes } from "@/constants/issue";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store";
|
||||
import useKeypress from "@/hooks/use-keypress";
|
||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
|
||||
export type TQuickAddIssueFormRoot = {
|
||||
isOpen: boolean;
|
||||
layout: EIssueLayoutTypes;
|
||||
prePopulatedData?: Partial<TIssue>;
|
||||
projectId: string;
|
||||
hasError?: boolean;
|
||||
setFocus: UseFormSetFocus<TIssue>;
|
||||
register: UseFormRegister<TIssue>;
|
||||
onSubmit: () => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export const QuickAddIssueFormRoot: FC<TQuickAddIssueFormRoot> = observer((props) => {
|
||||
const { isOpen, layout, projectId, hasError = false, setFocus, register, onSubmit, onClose } = props;
|
||||
// store hooks
|
||||
const { getProjectById } = useProject();
|
||||
// derived values
|
||||
const projectDetail = getProjectById(projectId);
|
||||
// refs
|
||||
const ref = useRef<HTMLFormElement>(null);
|
||||
// click detection
|
||||
useKeypress("Escape", onClose);
|
||||
useOutsideClickDetector(ref, onClose);
|
||||
// set focus on name input
|
||||
useEffect(() => {
|
||||
setFocus("name");
|
||||
}, [setFocus]);
|
||||
|
||||
if (!projectDetail) return <></>;
|
||||
|
||||
const QUICK_ADD_ISSUE_FORMS: Record<EIssueLayoutTypes, FC<TQuickAddIssueForm>> = {
|
||||
[EIssueLayoutTypes.LIST]: ListQuickAddIssueForm,
|
||||
[EIssueLayoutTypes.KANBAN]: KanbanQuickAddIssueForm,
|
||||
[EIssueLayoutTypes.CALENDAR]: CalendarQuickAddIssueForm,
|
||||
[EIssueLayoutTypes.GANTT]: GanttQuickAddIssueForm,
|
||||
[EIssueLayoutTypes.SPREADSHEET]: SpreadsheetQuickAddIssueForm,
|
||||
};
|
||||
|
||||
const CurrentLayoutQuickAddIssueForm = QUICK_ADD_ISSUE_FORMS[layout] ?? null;
|
||||
|
||||
if (!CurrentLayoutQuickAddIssueForm) return <></>;
|
||||
|
||||
return (
|
||||
<CurrentLayoutQuickAddIssueForm
|
||||
ref={ref}
|
||||
isOpen={isOpen}
|
||||
projectDetail={projectDetail}
|
||||
hasError={hasError}
|
||||
register={register}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
@ -9,4 +9,4 @@ export * from "./issue-block-root";
|
|||
export * from "./issue-block";
|
||||
export * from "./week-days";
|
||||
export * from "./week-header";
|
||||
export * from "./quick-add-issue-form";
|
||||
export * from "./quick-add-issue-actions";
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { observer } from "mobx-react";
|
||||
import { TIssue, TPaginationData } from "@plane/types";
|
||||
// components
|
||||
import { CalendarQuickAddIssueForm, CalendarIssueBlockRoot } from "@/components/issues";
|
||||
import { CalendarQuickAddIssueActions, CalendarIssueBlockRoot } from "@/components/issues";
|
||||
// helpers
|
||||
import { renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||
import { useIssuesStore } from "@/hooks/use-issue-layout-store";
|
||||
|
|
@ -75,9 +75,7 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
|
|||
|
||||
{enableQuickIssueCreate && !disableIssueCreation && !readOnly && (
|
||||
<div className="border-b border-custom-border-200 px-1 py-1 md:border-none md:px-2">
|
||||
<CalendarQuickAddIssueForm
|
||||
formKey="target_date"
|
||||
groupId={formattedDatePayload}
|
||||
<CalendarQuickAddIssueActions
|
||||
prePopulatedData={{
|
||||
target_date: formattedDatePayload,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,133 @@
|
|||
"use client";
|
||||
|
||||
import { FC, useState } from "react";
|
||||
import { differenceInCalendarDays } from "date-fns";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
// types
|
||||
import { ISearchIssueResponse, TIssue } from "@plane/types";
|
||||
// ui
|
||||
import { TOAST_TYPE, setToast, CustomMenu } from "@plane/ui";
|
||||
// components
|
||||
import { ExistingIssuesListModal } from "@/components/core";
|
||||
import { QuickAddIssueRoot } from "@/components/issues";
|
||||
// helpers
|
||||
import { EIssueLayoutTypes } from "@/constants/issue";
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
|
||||
type TCalendarQuickAddIssueActions = {
|
||||
prePopulatedData?: Partial<TIssue>;
|
||||
quickAddCallback?: (projectId: string | null | undefined, data: TIssue) => Promise<TIssue | undefined>;
|
||||
addIssuesToView?: (issueIds: string[]) => Promise<any>;
|
||||
onOpen?: () => void;
|
||||
};
|
||||
|
||||
export const CalendarQuickAddIssueActions: FC<TCalendarQuickAddIssueActions> = observer((props) => {
|
||||
const { prePopulatedData, quickAddCallback, addIssuesToView, onOpen } = props;
|
||||
// router
|
||||
const { workspaceSlug, projectId, moduleId } = useParams();
|
||||
// states
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [isExistingIssueModalOpen, setIsExistingIssueModalOpen] = useState(false);
|
||||
const { updateIssue } = useIssueDetail();
|
||||
// derived values
|
||||
const ExistingIssuesListModalPayload = addIssuesToView
|
||||
? moduleId
|
||||
? { module: moduleId.toString(), target_date: "none" }
|
||||
: { cycle: true, target_date: "none" }
|
||||
: { target_date: "none" };
|
||||
|
||||
const handleAddIssuesToView = async (data: ISearchIssueResponse[]) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
const issueIds = data.map((i) => i.id);
|
||||
|
||||
try {
|
||||
// To handle all updates in parallel
|
||||
await Promise.all(
|
||||
data.map((issue) =>
|
||||
updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, prePopulatedData ?? {})
|
||||
)
|
||||
);
|
||||
await addIssuesToView?.(issueIds);
|
||||
} catch (error) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Something went wrong. Please try again.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleNewIssue = () => {
|
||||
setIsOpen(true);
|
||||
if (onOpen) onOpen();
|
||||
};
|
||||
const handleExistingIssue = () => {
|
||||
setIsExistingIssueModalOpen(true);
|
||||
};
|
||||
|
||||
if (!projectId) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{workspaceSlug && projectId && (
|
||||
<ExistingIssuesListModal
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
isOpen={isExistingIssueModalOpen}
|
||||
handleClose={() => setIsExistingIssueModalOpen(false)}
|
||||
searchParams={ExistingIssuesListModalPayload}
|
||||
handleOnSubmit={handleAddIssuesToView}
|
||||
shouldHideIssue={(issue) => {
|
||||
if (issue.start_date && prePopulatedData?.target_date) {
|
||||
const issueStartDate = new Date(issue.start_date);
|
||||
const targetDate = new Date(prePopulatedData.target_date);
|
||||
const diffInDays = differenceInCalendarDays(targetDate, issueStartDate);
|
||||
if (diffInDays < 0) return true;
|
||||
}
|
||||
return false;
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<QuickAddIssueRoot
|
||||
isQuickAddOpen={isOpen}
|
||||
setIsQuickAddOpen={(isOpen) => setIsOpen(isOpen)}
|
||||
layout={EIssueLayoutTypes.CALENDAR}
|
||||
prePopulatedData={prePopulatedData}
|
||||
quickAddCallback={quickAddCallback}
|
||||
customQuickAddButton={
|
||||
<div
|
||||
className={cn(
|
||||
"md:opacity-0 rounded md:border-[0.5px] border-custom-border-200 md:group-hover:opacity-100",
|
||||
{
|
||||
block: isMenuOpen,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<CustomMenu
|
||||
placement="bottom-start"
|
||||
menuButtonOnClick={() => setIsMenuOpen(true)}
|
||||
onMenuClose={() => setIsMenuOpen(false)}
|
||||
className="w-full"
|
||||
customButtonClassName="w-full"
|
||||
customButton={
|
||||
<div className="flex w-full items-center gap-x-[6px] rounded-md px-2 py-1.5 text-custom-text-350 hover:text-custom-text-300">
|
||||
<PlusIcon className="h-3.5 w-3.5 stroke-2 flex-shrink-0" />
|
||||
<span className="text-sm font-medium flex-shrink-0">New issue</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={handleNewIssue}>New issue</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={handleExistingIssue}>Add existing issue</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,264 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { differenceInCalendarDays } from "date-fns";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
// types
|
||||
import { ISearchIssueResponse, TIssue } from "@plane/types";
|
||||
// ui
|
||||
import { TOAST_TYPE, setPromiseToast, setToast, CustomMenu } from "@plane/ui";
|
||||
// components
|
||||
import { ExistingIssuesListModal } from "@/components/core";
|
||||
import { CreateIssueToastActionItems } from "@/components/issues";
|
||||
// constants
|
||||
import { ISSUE_CREATED } from "@/constants/event-tracker";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { createIssuePayload } from "@/helpers/issue.helper";
|
||||
// hooks
|
||||
import { useEventTracker, useIssueDetail, useProject } from "@/hooks/store";
|
||||
import useKeypress from "@/hooks/use-keypress";
|
||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
|
||||
type Props = {
|
||||
formKey: keyof TIssue;
|
||||
groupId?: string;
|
||||
subGroupId?: string | null;
|
||||
prePopulatedData?: Partial<TIssue>;
|
||||
quickAddCallback?: (projectId: string | null | undefined, data: TIssue) => Promise<TIssue | undefined>;
|
||||
addIssuesToView?: (issueIds: string[]) => Promise<any>;
|
||||
onOpen?: () => void;
|
||||
};
|
||||
|
||||
const defaultValues: Partial<TIssue> = {
|
||||
name: "",
|
||||
};
|
||||
|
||||
const Inputs = (props: any) => {
|
||||
const { register, setFocus, projectDetails } = props;
|
||||
|
||||
useEffect(() => {
|
||||
setFocus("name");
|
||||
}, [setFocus]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h4 className="text-sm md:text-xs leading-5 text-custom-text-400">{projectDetails?.identifier ?? "..."}</h4>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
placeholder="Issue Title"
|
||||
{...register("name", {
|
||||
required: "Issue title is required.",
|
||||
})}
|
||||
className="w-full rounded-md bg-transparent py-1.5 pr-2 text-sm md:text-xs font-medium leading-5 text-custom-text-200 outline-none"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const CalendarQuickAddIssueForm: React.FC<Props> = observer((props) => {
|
||||
const { formKey, prePopulatedData, quickAddCallback, addIssuesToView, onOpen } = props;
|
||||
|
||||
// router
|
||||
const { workspaceSlug, projectId, moduleId } = useParams();
|
||||
const pathname = usePathname();
|
||||
// store hooks
|
||||
const { getProjectById } = useProject();
|
||||
const { captureIssueEvent } = useEventTracker();
|
||||
const { updateIssue } = useIssueDetail();
|
||||
// refs
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
// states
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [isExistingIssueModalOpen, setIsExistingIssueModalOpen] = useState(false);
|
||||
// derived values
|
||||
const projectDetail = projectId ? getProjectById(projectId.toString()) : null;
|
||||
const ExistingIssuesListModalPayload = addIssuesToView
|
||||
? moduleId
|
||||
? { module: moduleId.toString(), target_date: "none" }
|
||||
: { cycle: true, target_date: "none" }
|
||||
: { target_date: "none" };
|
||||
|
||||
const {
|
||||
reset,
|
||||
handleSubmit,
|
||||
register,
|
||||
setFocus,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<TIssue>({ defaultValues });
|
||||
|
||||
const handleClose = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
useKeypress("Escape", handleClose);
|
||||
useOutsideClickDetector(ref, handleClose);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) reset({ ...defaultValues });
|
||||
}, [isOpen, reset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!errors) return;
|
||||
|
||||
Object.keys(errors).forEach((key) => {
|
||||
const error = errors[key as keyof TIssue];
|
||||
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: error?.message?.toString() || "Some error occurred. Please try again.",
|
||||
});
|
||||
});
|
||||
}, [errors]);
|
||||
|
||||
const onSubmitHandler = async (formData: TIssue) => {
|
||||
if (isSubmitting || !workspaceSlug || !projectId) return;
|
||||
|
||||
reset({ ...defaultValues });
|
||||
|
||||
const payload = createIssuePayload(projectId.toString(), {
|
||||
...(prePopulatedData ?? {}),
|
||||
...formData,
|
||||
});
|
||||
|
||||
if (quickAddCallback) {
|
||||
const quickAddPromise = quickAddCallback(projectId.toString(), {
|
||||
...payload,
|
||||
});
|
||||
setPromiseToast<any>(quickAddPromise, {
|
||||
loading: "Adding issue...",
|
||||
success: {
|
||||
title: "Success!",
|
||||
message: () => "Issue created successfully.",
|
||||
actionItems: (data) => (
|
||||
<CreateIssueToastActionItems
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
issueId={data.id}
|
||||
/>
|
||||
),
|
||||
},
|
||||
error: {
|
||||
title: "Error!",
|
||||
message: (err) => err?.message || "Some error occurred. Please try again.",
|
||||
},
|
||||
});
|
||||
|
||||
await quickAddPromise
|
||||
.then((res) => {
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_CREATED,
|
||||
payload: { ...res, state: "SUCCESS", element: "Calendar quick add" },
|
||||
path: pathname,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_CREATED,
|
||||
payload: { ...payload, state: "FAILED", element: "Calendar quick add" },
|
||||
path: pathname,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddIssuesToView = async (data: ISearchIssueResponse[]) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
const issueIds = data.map((i) => i.id);
|
||||
|
||||
try {
|
||||
// To handle all updates in parallel
|
||||
await Promise.all(
|
||||
data.map((issue) =>
|
||||
updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, prePopulatedData ?? {})
|
||||
)
|
||||
);
|
||||
await addIssuesToView?.(issueIds);
|
||||
} catch (error) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Something went wrong. Please try again.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleNewIssue = () => {
|
||||
setIsOpen(true);
|
||||
if (onOpen) onOpen();
|
||||
};
|
||||
const handleExistingIssue = () => {
|
||||
setIsExistingIssueModalOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{workspaceSlug && projectId && (
|
||||
<ExistingIssuesListModal
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
isOpen={isExistingIssueModalOpen}
|
||||
handleClose={() => setIsExistingIssueModalOpen(false)}
|
||||
searchParams={ExistingIssuesListModalPayload}
|
||||
handleOnSubmit={handleAddIssuesToView}
|
||||
shouldHideIssue={(issue) => {
|
||||
if (issue.start_date && prePopulatedData?.target_date) {
|
||||
const issueStartDate = new Date(issue.start_date);
|
||||
const targetDate = new Date(prePopulatedData.target_date);
|
||||
const diffInDays = differenceInCalendarDays(targetDate, issueStartDate);
|
||||
if (diffInDays < 0) return true;
|
||||
}
|
||||
return false;
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isOpen && (
|
||||
<div
|
||||
ref={ref}
|
||||
className={`z-20 w-full transition-all ${
|
||||
isOpen ? "scale-100 opacity-100" : "pointer-events-none scale-95 opacity-0"
|
||||
}`}
|
||||
>
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmitHandler)}
|
||||
className="z-50 flex w-full items-center gap-x-2 rounded md:border-[0.5px] border-custom-border-200 bg-custom-background-100 px-2 md:shadow-custom-shadow-2xs transition-opacity"
|
||||
>
|
||||
<Inputs formKey={formKey} register={register} setFocus={setFocus} projectDetails={projectDetail} />
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isOpen && (
|
||||
<div
|
||||
className={cn("md:opacity-0 rounded md:border-[0.5px] border-custom-border-200 md:group-hover:opacity-100", {
|
||||
block: isMenuOpen,
|
||||
})}
|
||||
>
|
||||
<CustomMenu
|
||||
placement="bottom-start"
|
||||
menuButtonOnClick={() => setIsMenuOpen(true)}
|
||||
onMenuClose={() => setIsMenuOpen(false)}
|
||||
className="w-full"
|
||||
customButtonClassName="w-full"
|
||||
customButton={
|
||||
<div className="flex w-full items-center gap-x-[6px] rounded-md px-2 py-1.5 text-custom-text-350 hover:text-custom-text-300">
|
||||
<PlusIcon className="h-3.5 w-3.5 stroke-2 flex-shrink-0" />
|
||||
<span className="text-sm font-medium flex-shrink-0">New issue</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={handleNewIssue}>New issue</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={handleExistingIssue}>Add existing issue</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
@ -7,10 +7,12 @@ import { TIssue } from "@plane/types";
|
|||
// hooks
|
||||
import { ChartDataType, GanttChartRoot, IBlockUpdateData, IssueGanttSidebar } from "@/components/gantt-chart";
|
||||
import { getMonthChartItemPositionWidthInMonth } from "@/components/gantt-chart/views";
|
||||
import { GanttQuickAddIssueForm, IssueGanttBlock } from "@/components/issues";
|
||||
import { QuickAddIssueRoot, IssueGanttBlock, GanttQuickAddIssueButton } from "@/components/issues";
|
||||
//constants
|
||||
import { EIssueLayoutTypes, EIssuesStoreType } from "@/constants/issue";
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
// helpers
|
||||
import { renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||
import { getIssueBlocksStructure } from "@/helpers/issue.helper";
|
||||
//hooks
|
||||
import { useIssues, useUser } from "@/hooks/store";
|
||||
|
|
@ -47,6 +49,9 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
|
|||
const appliedDisplayFilters = issuesFilter.issueFilters?.displayFilters;
|
||||
// plane web hooks
|
||||
const isBulkOperationsEnabled = useBulkOperationStatus();
|
||||
// derived values
|
||||
const targetDate = new Date();
|
||||
targetDate.setDate(targetDate.getDate() + 1);
|
||||
|
||||
useEffect(() => {
|
||||
fetchIssues("init-loader", { canGroup: false, perPageCount: 100 }, viewId);
|
||||
|
|
@ -89,7 +94,16 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
|
|||
|
||||
const quickAdd =
|
||||
enableIssueCreation && isAllowed && !isCompletedCycle ? (
|
||||
<GanttQuickAddIssueForm quickAddCallback={quickAddIssue} />
|
||||
<QuickAddIssueRoot
|
||||
layout={EIssueLayoutTypes.GANTT}
|
||||
QuickAddButton={GanttQuickAddIssueButton}
|
||||
containerClassName="sticky bottom-0 z-[1]"
|
||||
prePopulatedData={{
|
||||
start_date: renderFormattedPayloadDate(new Date()),
|
||||
target_date: renderFormattedPayloadDate(targetDate),
|
||||
}}
|
||||
quickAddCallback={quickAddIssue}
|
||||
/>
|
||||
) : undefined;
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
export * from "./blocks";
|
||||
export * from "./base-gantt-root";
|
||||
export * from "./quick-add-issue-form";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,179 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState, useRef, FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { IProject, TIssue } from "@plane/types";
|
||||
// hooks
|
||||
import { setPromiseToast } from "@plane/ui";
|
||||
import { CreateIssueToastActionItems } from "@/components/issues";
|
||||
import { ISSUE_CREATED } from "@/constants/event-tracker";
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||
import { createIssuePayload } from "@/helpers/issue.helper";
|
||||
import { useEventTracker, useProject } from "@/hooks/store";
|
||||
import useKeypress from "@/hooks/use-keypress";
|
||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
// helpers
|
||||
// ui
|
||||
// types
|
||||
// constants
|
||||
|
||||
interface IInputProps {
|
||||
formKey: string;
|
||||
register: any;
|
||||
setFocus: any;
|
||||
projectDetail: IProject | null;
|
||||
}
|
||||
const Inputs: FC<IInputProps> = (props) => {
|
||||
const { formKey, register, setFocus, projectDetail } = props;
|
||||
|
||||
useEffect(() => {
|
||||
setFocus(formKey);
|
||||
}, [formKey, setFocus]);
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center gap-3">
|
||||
<div className="text-xs font-medium text-custom-text-400">{projectDetail?.identifier ?? "..."}</div>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
placeholder="Issue Title"
|
||||
{...register(formKey, {
|
||||
required: "Issue title is required.",
|
||||
})}
|
||||
className="w-full rounded-md bg-transparent px-2 py-3 text-sm font-medium leading-5 text-custom-text-200 outline-none"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type IGanttQuickAddIssueForm = {
|
||||
prePopulatedData?: Partial<TIssue>;
|
||||
onSuccess?: (data: TIssue) => Promise<void> | void;
|
||||
quickAddCallback?: (projectId: string | null | undefined, data: TIssue) => Promise<TIssue | undefined>;
|
||||
};
|
||||
|
||||
const defaultValues: Partial<TIssue> = {
|
||||
name: "",
|
||||
};
|
||||
|
||||
export const GanttQuickAddIssueForm: React.FC<IGanttQuickAddIssueForm> = observer((props) => {
|
||||
const { prePopulatedData, quickAddCallback } = props;
|
||||
// router
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
const pathname = usePathname();
|
||||
// hooks
|
||||
const { getProjectById } = useProject();
|
||||
const { captureIssueEvent } = useEventTracker();
|
||||
|
||||
const projectDetail = (projectId && getProjectById(projectId.toString())) || undefined;
|
||||
|
||||
const ref = useRef<HTMLFormElement>(null);
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const handleClose = () => setIsOpen(false);
|
||||
|
||||
useKeypress("Escape", handleClose);
|
||||
useOutsideClickDetector(ref, handleClose);
|
||||
|
||||
// form info
|
||||
const {
|
||||
reset,
|
||||
handleSubmit,
|
||||
setFocus,
|
||||
register,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<TIssue>({ defaultValues });
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) reset({ ...defaultValues });
|
||||
}, [isOpen, reset]);
|
||||
|
||||
const onSubmitHandler = async (formData: TIssue) => {
|
||||
if (isSubmitting || !workspaceSlug || !projectId) return;
|
||||
|
||||
reset({ ...defaultValues });
|
||||
|
||||
const targetDate = new Date();
|
||||
targetDate.setDate(targetDate.getDate() + 1);
|
||||
|
||||
const payload = createIssuePayload(projectId.toString(), {
|
||||
...(prePopulatedData ?? {}),
|
||||
...formData,
|
||||
start_date: renderFormattedPayloadDate(new Date()),
|
||||
target_date: renderFormattedPayloadDate(targetDate),
|
||||
});
|
||||
|
||||
if (quickAddCallback) {
|
||||
const quickAddPromise = quickAddCallback(projectId.toString(), { ...payload });
|
||||
setPromiseToast<any>(quickAddPromise, {
|
||||
loading: "Adding issue...",
|
||||
success: {
|
||||
title: "Success!",
|
||||
message: () => "Issue created successfully.",
|
||||
actionItems: (data) => (
|
||||
<CreateIssueToastActionItems
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
issueId={data.id}
|
||||
/>
|
||||
),
|
||||
},
|
||||
error: {
|
||||
title: "Error!",
|
||||
message: (err) => err?.message || "Some error occurred. Please try again.",
|
||||
},
|
||||
});
|
||||
|
||||
await quickAddPromise
|
||||
.then((res) => {
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_CREATED,
|
||||
payload: { ...res, state: "SUCCESS", element: "Gantt quick add" },
|
||||
path: pathname,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_CREATED,
|
||||
payload: { ...payload, state: "FAILED", element: "Gantt quick add" },
|
||||
path: pathname,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{isOpen ? (
|
||||
<div
|
||||
className={cn("sticky bottom-0 z-[1] bg-custom-background-100", {
|
||||
"border border-red-500/20 bg-red-500/10": errors && errors?.name && errors?.name?.message,
|
||||
})}
|
||||
>
|
||||
<div className="shadow-custom-shadow-sm">
|
||||
<form
|
||||
ref={ref}
|
||||
onSubmit={handleSubmit(onSubmitHandler)}
|
||||
className="flex w-full items-center gap-x-3 border-[0.5px] border-custom-border-100 bg-custom-background-100 px-3"
|
||||
>
|
||||
<Inputs formKey={"name"} register={register} setFocus={setFocus} projectDetail={projectDetail ?? null} />
|
||||
</form>
|
||||
<div className="px-3 py-2 text-xs italic text-custom-text-200">{`Press 'Enter' to add another issue`}</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="sticky bottom-0 z-[1] flex w-full cursor-pointer items-center gap-2 border-t-[1px] border-custom-border-200 bg-custom-background-100 px-3 pt-2 text-custom-text-350 hover:text-custom-text-300"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<PlusIcon className="h-3.5 w-3.5 stroke-2" />
|
||||
<span className="text-sm font-medium">New Issue</span>
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
@ -18,3 +18,7 @@ export * from "./properties";
|
|||
|
||||
// save view
|
||||
export * from "./save-filter-view";
|
||||
|
||||
// quick add
|
||||
export * from "./quick-add";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
export * from "./block";
|
||||
export * from "./roots";
|
||||
export * from "./blocks-list";
|
||||
export * from "./quick-add-issue-form";
|
||||
|
|
|
|||
|
|
@ -16,9 +16,11 @@ import {
|
|||
TIssueOrderByOptions,
|
||||
} from "@plane/types";
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { KanbanQuickAddIssueButton, QuickAddIssueRoot } from "@/components/issues";
|
||||
import { highlightIssueOnDrop } from "@/components/issues/issue-layouts/utils";
|
||||
import { KanbanIssueBlockLoader } from "@/components/ui";
|
||||
// helpers
|
||||
import { EIssueLayoutTypes } from "@/constants/issue";
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useProjectState } from "@/hooks/store";
|
||||
|
|
@ -27,7 +29,7 @@ import { useIssuesStore } from "@/hooks/use-issue-layout-store";
|
|||
import { GroupDragOverlay } from "../group-drag-overlay";
|
||||
import { TRenderQuickActions } from "../list/list-view-types";
|
||||
import { GroupDropLocation, getSourceFromDropPayload, getDestinationFromDropPayload, getIssueBlockId } from "../utils";
|
||||
import { KanbanIssueBlocksList, KanBanQuickAddIssueForm } from ".";
|
||||
import { KanbanIssueBlocksList } from ".";
|
||||
|
||||
interface IKanbanGroup {
|
||||
groupId: string;
|
||||
|
|
@ -273,10 +275,9 @@ export const KanbanGroup = observer((props: IKanbanGroup) => {
|
|||
|
||||
{enableQuickIssueCreate && !disableIssueCreation && (
|
||||
<div className="w-full bg-custom-background-90 py-0.5 sticky bottom-0">
|
||||
<KanBanQuickAddIssueForm
|
||||
formKey="name"
|
||||
groupId={groupId}
|
||||
subGroupId={sub_group_id}
|
||||
<QuickAddIssueRoot
|
||||
layout={EIssueLayoutTypes.KANBAN}
|
||||
QuickAddButton={KanbanQuickAddIssueButton}
|
||||
prePopulatedData={{
|
||||
...(group_by && prePopulateQuickAddData(group_by, sub_group_by, groupId, sub_group_id)),
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -1,162 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { TIssue } from "@plane/types";
|
||||
// hooks
|
||||
import { setPromiseToast } from "@plane/ui";
|
||||
import { CreateIssueToastActionItems } from "@/components/issues";
|
||||
import { ISSUE_CREATED } from "@/constants/event-tracker";
|
||||
import { createIssuePayload } from "@/helpers/issue.helper";
|
||||
import { useEventTracker, useProject } from "@/hooks/store";
|
||||
import useKeypress from "@/hooks/use-keypress";
|
||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
// helpers
|
||||
// ui
|
||||
// types
|
||||
// constants
|
||||
|
||||
const Inputs = (props: any) => {
|
||||
const { register, setFocus, projectDetail } = props;
|
||||
|
||||
useEffect(() => {
|
||||
setFocus("name");
|
||||
}, [setFocus]);
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<h4 className="text-xs font-medium leading-5 text-custom-text-300">{projectDetail?.identifier ?? "..."}</h4>
|
||||
<input
|
||||
autoComplete="off"
|
||||
placeholder="Issue Title"
|
||||
{...register("name", {
|
||||
required: "Issue title is required.",
|
||||
})}
|
||||
className="w-full rounded-md bg-transparent px-2 py-1.5 pl-0 text-sm font-medium leading-5 text-custom-text-200 outline-none"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface IKanBanQuickAddIssueForm {
|
||||
formKey: keyof TIssue;
|
||||
groupId?: string;
|
||||
subGroupId?: string | null;
|
||||
prePopulatedData?: Partial<TIssue>;
|
||||
quickAddCallback?: (projectId: string | null | undefined, data: TIssue) => Promise<TIssue | undefined>;
|
||||
}
|
||||
|
||||
const defaultValues: Partial<TIssue> = {
|
||||
name: "",
|
||||
};
|
||||
|
||||
export const KanBanQuickAddIssueForm: React.FC<IKanBanQuickAddIssueForm> = observer((props) => {
|
||||
const { formKey, prePopulatedData, quickAddCallback } = props;
|
||||
// router
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
const pathname = usePathname();
|
||||
// store hooks
|
||||
const { getProjectById } = useProject();
|
||||
const { captureIssueEvent } = useEventTracker();
|
||||
|
||||
const projectDetail = projectId ? getProjectById(projectId.toString()) : null;
|
||||
|
||||
const ref = useRef<HTMLFormElement>(null);
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const handleClose = () => setIsOpen(false);
|
||||
|
||||
useKeypress("Escape", handleClose);
|
||||
useOutsideClickDetector(ref, handleClose);
|
||||
|
||||
const {
|
||||
reset,
|
||||
handleSubmit,
|
||||
setFocus,
|
||||
register,
|
||||
formState: { isSubmitting },
|
||||
} = useForm<TIssue>({ defaultValues });
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) reset({ ...defaultValues });
|
||||
}, [isOpen, reset]);
|
||||
|
||||
const onSubmitHandler = async (formData: TIssue) => {
|
||||
if (isSubmitting || !workspaceSlug || !projectId) return;
|
||||
|
||||
reset({ ...defaultValues });
|
||||
|
||||
const payload = createIssuePayload(projectId.toString(), {
|
||||
...(prePopulatedData ?? {}),
|
||||
...formData,
|
||||
});
|
||||
|
||||
if (quickAddCallback) {
|
||||
const quickAddPromise = quickAddCallback(projectId.toString(), {
|
||||
...payload,
|
||||
});
|
||||
setPromiseToast<any>(quickAddPromise, {
|
||||
loading: "Adding issue...",
|
||||
success: {
|
||||
title: "Success!",
|
||||
message: () => "Issue created successfully.",
|
||||
actionItems: (data) => (
|
||||
<CreateIssueToastActionItems
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
issueId={data.id}
|
||||
/>
|
||||
),
|
||||
},
|
||||
error: {
|
||||
title: "Error!",
|
||||
message: (err) => err?.message || "Some error occurred. Please try again.",
|
||||
},
|
||||
});
|
||||
|
||||
await quickAddPromise
|
||||
.then((res) => {
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_CREATED,
|
||||
payload: { ...res, state: "SUCCESS", element: "Kanban quick add" },
|
||||
path: pathname,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_CREATED,
|
||||
payload: { ...payload, state: "FAILED", element: "Kanban quick add" },
|
||||
path: pathname,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isOpen ? (
|
||||
<div className="m-1.5 overflow-hidden rounded shadow-custom-shadow-sm">
|
||||
<form
|
||||
ref={ref}
|
||||
onSubmit={handleSubmit(onSubmitHandler)}
|
||||
className="flex w-full items-center gap-x-3 bg-custom-background-100 p-3"
|
||||
>
|
||||
<Inputs formKey={formKey} register={register} setFocus={setFocus} projectDetail={projectDetail} />
|
||||
</form>
|
||||
<div className="px-3 py-2 text-xs italic text-custom-text-200">{`Press 'Enter' to add another issue`}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="flex w-full cursor-pointer items-center gap-2 p-3 py-1.5 text-custom-text-350 hover:text-custom-text-300"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<PlusIcon className="h-3.5 w-3.5 stroke-2" />
|
||||
<span className="text-sm font-medium">New Issue</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
@ -3,4 +3,3 @@ export * from "./block-root";
|
|||
export * from "./block";
|
||||
export * from "./roots";
|
||||
export * from "./blocks-list";
|
||||
export * from "./quick-add-issue-form";
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import { setToast, TOAST_TYPE } from "@plane/ui";
|
|||
// components
|
||||
import { ListLoaderItemRow } from "@/components/ui";
|
||||
// constants
|
||||
import { DRAG_ALLOWED_GROUPS } from "@/constants/issue";
|
||||
import { DRAG_ALLOWED_GROUPS, EIssueLayoutTypes } from "@/constants/issue";
|
||||
// hooks
|
||||
import { useProjectState } from "@/hooks/store";
|
||||
import { useIntersectionObserver } from "@/hooks/use-intersection-observer";
|
||||
|
|
@ -26,6 +26,7 @@ import { useIssuesStore } from "@/hooks/use-issue-layout-store";
|
|||
import { TSelectionHelper } from "@/hooks/use-multiple-select";
|
||||
// components
|
||||
import { GroupDragOverlay } from "../group-drag-overlay";
|
||||
import { ListQuickAddIssueButton, QuickAddIssueRoot } from "../quick-add";
|
||||
import {
|
||||
GroupDropLocation,
|
||||
getDestinationFromDropPayload,
|
||||
|
|
@ -36,7 +37,6 @@ import {
|
|||
import { IssueBlocksList } from "./blocks-list";
|
||||
import { HeaderGroupByCard } from "./headers/group-by-card";
|
||||
import { TRenderQuickActions } from "./list-view-types";
|
||||
import { ListQuickAddIssueForm } from "./quick-add-issue-form";
|
||||
|
||||
interface Props {
|
||||
groupIssueIds: string[] | undefined;
|
||||
|
|
@ -274,8 +274,11 @@ export const ListGroup = observer((props: Props) => {
|
|||
|
||||
{enableIssueQuickAdd && !disableIssueCreation && !isGroupByCreatedBy && !isCompletedCycle && (
|
||||
<div className="sticky bottom-0 z-[1] w-full flex-shrink-0">
|
||||
<ListQuickAddIssueForm
|
||||
<QuickAddIssueRoot
|
||||
layout={EIssueLayoutTypes.LIST}
|
||||
QuickAddButton={ListQuickAddIssueButton}
|
||||
prePopulatedData={prePopulateQuickAddData(group_by, group.id)}
|
||||
containerClassName="border-b border-t border-custom-border-200 bg-custom-background-100 "
|
||||
quickAddCallback={quickAddCallback}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,168 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { FC, useEffect, useState, useRef } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { TIssue, IProject } from "@plane/types";
|
||||
// hooks
|
||||
import { setPromiseToast } from "@plane/ui";
|
||||
import { CreateIssueToastActionItems } from "@/components/issues";
|
||||
import { ISSUE_CREATED } from "@/constants/event-tracker";
|
||||
import { createIssuePayload } from "@/helpers/issue.helper";
|
||||
import { useEventTracker, useProject } from "@/hooks/store";
|
||||
import useKeypress from "@/hooks/use-keypress";
|
||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
// ui
|
||||
// types
|
||||
// helper
|
||||
// constants
|
||||
|
||||
interface IInputProps {
|
||||
formKey: string;
|
||||
register: any;
|
||||
setFocus: any;
|
||||
projectDetail: IProject | null;
|
||||
}
|
||||
const Inputs: FC<IInputProps> = (props) => {
|
||||
const { formKey, register, setFocus, projectDetail } = props;
|
||||
|
||||
useEffect(() => {
|
||||
setFocus(formKey);
|
||||
}, [formKey, setFocus]);
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center gap-3">
|
||||
<div className="text-xs font-medium text-custom-text-400">{projectDetail?.identifier ?? "..."}</div>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
placeholder="Issue Title"
|
||||
{...register(formKey, {
|
||||
required: "Issue title is required.",
|
||||
})}
|
||||
className="w-full rounded-md bg-transparent px-2 py-3 text-sm font-medium leading-5 text-custom-text-200 outline-none"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface IListQuickAddIssueForm {
|
||||
prePopulatedData?: Partial<TIssue>;
|
||||
quickAddCallback?: (projectId: string | null | undefined, data: TIssue) => Promise<TIssue | undefined>;
|
||||
}
|
||||
|
||||
const defaultValues: Partial<TIssue> = {
|
||||
name: "",
|
||||
};
|
||||
|
||||
export const ListQuickAddIssueForm: FC<IListQuickAddIssueForm> = observer((props) => {
|
||||
const { prePopulatedData, quickAddCallback } = props;
|
||||
// router
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
const pathname = usePathname();
|
||||
// hooks
|
||||
const { getProjectById } = useProject();
|
||||
const { captureIssueEvent } = useEventTracker();
|
||||
|
||||
const projectDetail = (projectId && getProjectById(projectId.toString())) || undefined;
|
||||
|
||||
const ref = useRef<HTMLFormElement>(null);
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const handleClose = () => setIsOpen(false);
|
||||
|
||||
useKeypress("Escape", handleClose);
|
||||
useOutsideClickDetector(ref, handleClose);
|
||||
|
||||
const {
|
||||
reset,
|
||||
handleSubmit,
|
||||
setFocus,
|
||||
register,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<TIssue>({ defaultValues });
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) reset({ ...defaultValues });
|
||||
}, [isOpen, reset]);
|
||||
|
||||
const onSubmitHandler = async (formData: TIssue) => {
|
||||
if (isSubmitting || !workspaceSlug || !projectId) return;
|
||||
|
||||
reset({ ...defaultValues });
|
||||
|
||||
const payload = createIssuePayload(projectId.toString(), {
|
||||
...(prePopulatedData ?? {}),
|
||||
...formData,
|
||||
});
|
||||
|
||||
if (quickAddCallback) {
|
||||
const quickAddPromise = quickAddCallback(projectId.toString(), { ...payload });
|
||||
setPromiseToast<any>(quickAddPromise, {
|
||||
loading: "Adding issue...",
|
||||
success: {
|
||||
title: "Success!",
|
||||
message: () => "Issue created successfully.",
|
||||
actionItems: (data) => (
|
||||
<CreateIssueToastActionItems
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
issueId={data.id}
|
||||
/>
|
||||
),
|
||||
},
|
||||
error: {
|
||||
title: "Error!",
|
||||
message: (err) => err?.message || "Some error occurred. Please try again.",
|
||||
},
|
||||
});
|
||||
|
||||
await quickAddPromise
|
||||
.then((res) => {
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_CREATED,
|
||||
payload: { ...res, state: "SUCCESS", element: "List quick add" },
|
||||
path: pathname,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_CREATED,
|
||||
payload: { ...payload, state: "FAILED", element: "List quick add" },
|
||||
path: pathname,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`border-b border-t border-custom-border-200 bg-custom-background-100 ${
|
||||
errors && errors?.name && errors?.name?.message ? `border-red-500 bg-red-500/10` : ``
|
||||
}`}
|
||||
>
|
||||
{isOpen ? (
|
||||
<div className="shadow-custom-shadow-sm">
|
||||
<form
|
||||
ref={ref}
|
||||
onSubmit={handleSubmit(onSubmitHandler)}
|
||||
className="flex w-full items-center gap-x-3 border-[0.5px] border-t-0 border-custom-border-100 bg-custom-background-100 px-3"
|
||||
>
|
||||
<Inputs formKey={"name"} register={register} setFocus={setFocus} projectDetail={projectDetail ?? null} />
|
||||
</form>
|
||||
<div className="px-3 py-2 text-xs italic text-custom-text-200">{`Press 'Enter' to add another issue`}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="flex w-full cursor-pointer items-center gap-2 px-2 py-3 text-custom-text-350 hover:text-custom-text-300"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<PlusIcon className="h-3.5 w-3.5 stroke-2" />
|
||||
<span className="text-sm font-medium">New Issue</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { TQuickAddIssueButton } from "../root";
|
||||
|
||||
export const GanttQuickAddIssueButton: FC<TQuickAddIssueButton> = observer((props) => {
|
||||
const { onClick } = props;
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="sticky bottom-0 z-[1] flex w-full cursor-pointer items-center gap-2 border-t-[1px] border-custom-border-200 bg-custom-background-100 px-3 pt-2 text-custom-text-350 hover:text-custom-text-300"
|
||||
onClick={onClick}
|
||||
>
|
||||
<PlusIcon className="h-3.5 w-3.5 stroke-2" />
|
||||
<span className="text-sm font-medium">New Issue</span>
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export * from "./list";
|
||||
export * from "./kanban";
|
||||
export * from "./gantt";
|
||||
export * from "./spreadsheet";
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { TQuickAddIssueButton } from "../root";
|
||||
|
||||
export const KanbanQuickAddIssueButton: FC<TQuickAddIssueButton> = observer((props) => {
|
||||
const { onClick } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex w-full cursor-pointer items-center gap-2 p-3 py-1.5 text-custom-text-350 hover:text-custom-text-300"
|
||||
onClick={onClick}
|
||||
>
|
||||
<PlusIcon className="h-3.5 w-3.5 stroke-2" />
|
||||
<span className="text-sm font-medium">New Issue</span>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { TQuickAddIssueButton } from "../root";
|
||||
|
||||
export const ListQuickAddIssueButton: FC<TQuickAddIssueButton> = observer((props) => {
|
||||
const { onClick } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex w-full cursor-pointer items-center gap-2 px-2 py-3 text-custom-text-350 hover:text-custom-text-300"
|
||||
onClick={onClick}
|
||||
>
|
||||
<PlusIcon className="h-3.5 w-3.5 stroke-2" />
|
||||
<span className="text-sm font-medium">New Issue</span>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { TQuickAddIssueButton } from "../root";
|
||||
|
||||
export const SpreadsheetAddIssueButton: FC<TQuickAddIssueButton> = observer((props) => {
|
||||
const { onClick } = props;
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-x-[6px] rounded-md px-2 pt-3 text-custom-text-350 hover:text-custom-text-300"
|
||||
onClick={onClick}
|
||||
>
|
||||
<PlusIcon className="h-3.5 w-3.5 stroke-2" />
|
||||
<span className="text-sm font-medium">New Issue</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { TQuickAddIssueForm } from "../root";
|
||||
|
||||
export const CalendarQuickAddIssueForm: FC<TQuickAddIssueForm> = observer((props) => {
|
||||
const { ref, isOpen, projectDetail, register, onSubmit } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`z-20 w-full transition-all ${isOpen ? "scale-100 opacity-100" : "pointer-events-none scale-95 opacity-0"
|
||||
}`}
|
||||
>
|
||||
<form
|
||||
ref={ref}
|
||||
onSubmit={onSubmit}
|
||||
className="z-50 flex w-full items-center gap-x-2 rounded md:border-[0.5px] border-custom-border-200 bg-custom-background-100 px-2 md:shadow-custom-shadow-2xs transition-opacity"
|
||||
>
|
||||
<h4 className="text-sm md:text-xs leading-5 text-custom-text-400">{projectDetail?.identifier ?? "..."}</h4>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
placeholder="Issue Title"
|
||||
{...register("name", {
|
||||
required: "Issue title is required.",
|
||||
})}
|
||||
className="w-full rounded-md bg-transparent py-1.5 pr-2 text-sm md:text-xs font-medium leading-5 text-custom-text-200 outline-none"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { TQuickAddIssueForm } from "../root";
|
||||
|
||||
export const GanttQuickAddIssueForm: FC<TQuickAddIssueForm> = observer((props) => {
|
||||
const { ref, projectDetail, hasError, register, onSubmit } = props;
|
||||
|
||||
return (
|
||||
<div className={cn("shadow-custom-shadow-sm", hasError && "border border-red-500/20 bg-red-500/10")}>
|
||||
<form
|
||||
ref={ref}
|
||||
onSubmit={onSubmit}
|
||||
className="flex w-full items-center gap-x-3 border-[0.5px] border-custom-border-100 bg-custom-background-100 px-3"
|
||||
>
|
||||
<div className="flex w-full items-center gap-3">
|
||||
<div className="text-xs font-medium text-custom-text-400">{projectDetail?.identifier ?? "..."}</div>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
placeholder="Issue Title"
|
||||
{...register("name", {
|
||||
required: "Issue title is required.",
|
||||
})}
|
||||
className="w-full rounded-md bg-transparent px-2 py-3 text-sm font-medium leading-5 text-custom-text-200 outline-none"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<div className="px-3 py-2 text-xs bg-custom-background-100 italic text-custom-text-200">{`Press 'Enter' to add another issue`}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
export * from "./list";
|
||||
export * from "./kanban";
|
||||
export * from "./gantt";
|
||||
export * from "./calendar";
|
||||
export * from "./spreadsheet";
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { TQuickAddIssueForm } from "../root";
|
||||
|
||||
export const KanbanQuickAddIssueForm: FC<TQuickAddIssueForm> = observer((props) => {
|
||||
const { ref, projectDetail, register, onSubmit } = props;
|
||||
|
||||
return (
|
||||
<div className="m-1 overflow-hidden rounded shadow-custom-shadow-sm">
|
||||
<form ref={ref} onSubmit={onSubmit} className="flex w-full items-center gap-x-3 bg-custom-background-100 p-3">
|
||||
<div className="w-full">
|
||||
<h4 className="text-xs font-medium leading-5 text-custom-text-300">{projectDetail?.identifier ?? "..."}</h4>
|
||||
<input
|
||||
autoComplete="off"
|
||||
placeholder="Issue Title"
|
||||
{...register("name", {
|
||||
required: "Issue title is required.",
|
||||
})}
|
||||
className="w-full rounded-md bg-transparent px-2 py-1.5 pl-0 text-sm font-medium leading-5 text-custom-text-200 outline-none"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<div className="px-3 py-2 text-xs italic text-custom-text-200">{`Press 'Enter' to add another issue`}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { TQuickAddIssueForm } from "../root";
|
||||
|
||||
export const ListQuickAddIssueForm: FC<TQuickAddIssueForm> = observer((props) => {
|
||||
const { ref, projectDetail, register, onSubmit } = props;
|
||||
|
||||
return (
|
||||
<div className="shadow-custom-shadow-sm">
|
||||
<form
|
||||
ref={ref}
|
||||
onSubmit={onSubmit}
|
||||
className="flex w-full items-center gap-x-3 border-[0.5px] border-t-0 border-custom-border-100 bg-custom-background-100 px-3"
|
||||
>
|
||||
<div className="flex w-full items-center gap-3">
|
||||
<div className="text-xs font-medium text-custom-text-400">{projectDetail?.identifier ?? "..."}</div>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
placeholder="Issue Title"
|
||||
{...register("name", {
|
||||
required: "Issue title is required.",
|
||||
})}
|
||||
className="w-full rounded-md bg-transparent px-2 py-3 text-sm font-medium leading-5 text-custom-text-200 outline-none"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<div className="px-3 py-2 text-xs italic text-custom-text-200">{`Press 'Enter' to add another issue`}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { TQuickAddIssueForm } from "../root";
|
||||
|
||||
export const SpreadsheetQuickAddIssueForm: FC<TQuickAddIssueForm> = observer((props) => {
|
||||
const { ref, projectDetail, register, onSubmit } = props;
|
||||
|
||||
return (
|
||||
<div className="pb-2">
|
||||
<form
|
||||
ref={ref}
|
||||
onSubmit={onSubmit}
|
||||
className="z-10 flex items-center gap-x-5 border-[0.5px] border-t-0 border-custom-border-100 bg-custom-background-100 px-4 shadow-custom-shadow-sm"
|
||||
>
|
||||
<h4 className="w-20 text-xs leading-5 text-custom-text-400">{projectDetail?.identifier ?? "..."}</h4>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
placeholder="Issue Title"
|
||||
{...register("name", {
|
||||
required: "Issue title is required.",
|
||||
})}
|
||||
className="w-full rounded-md bg-transparent py-3 text-sm leading-5 text-custom-text-200 outline-none"
|
||||
/>
|
||||
</form>
|
||||
<p className="ml-3 mt-3 text-xs italic text-custom-text-200">
|
||||
Press {"'"}Enter{"'"} to add another issue
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export * from "./root";
|
||||
export * from "./form";
|
||||
export * from "./button";
|
||||
185
web/core/components/issues/issue-layouts/quick-add/root.tsx
Normal file
185
web/core/components/issues/issue-layouts/quick-add/root.tsx
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
"use client";
|
||||
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import { useForm, UseFormRegister } from "react-hook-form";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
// types
|
||||
import { IProject, TIssue } from "@plane/types";
|
||||
// ui
|
||||
import { setPromiseToast } from "@plane/ui";
|
||||
// components
|
||||
import { CreateIssueToastActionItems } from "@/components/issues";
|
||||
// constants
|
||||
import { ISSUE_CREATED } from "@/constants/event-tracker";
|
||||
import { EIssueLayoutTypes } from "@/constants/issue";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { createIssuePayload } from "@/helpers/issue.helper";
|
||||
// hooks
|
||||
import { useEventTracker } from "@/hooks/store";
|
||||
// plane web components
|
||||
import { QuickAddIssueFormRoot } from "@/plane-web/components/issues";
|
||||
|
||||
export type TQuickAddIssueForm = {
|
||||
ref: React.RefObject<HTMLFormElement>;
|
||||
isOpen: boolean;
|
||||
projectDetail: IProject;
|
||||
hasError: boolean;
|
||||
register: UseFormRegister<TIssue>;
|
||||
onSubmit: () => void;
|
||||
};
|
||||
|
||||
export type TQuickAddIssueButton = {
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
type TQuickAddIssueRoot = {
|
||||
isQuickAddOpen?: boolean;
|
||||
layout: EIssueLayoutTypes;
|
||||
prePopulatedData?: Partial<TIssue>;
|
||||
QuickAddButton?: FC<TQuickAddIssueButton>;
|
||||
customQuickAddButton?: JSX.Element;
|
||||
containerClassName?: string;
|
||||
setIsQuickAddOpen?: (isOpen: boolean) => void;
|
||||
quickAddCallback?: (projectId: string | null | undefined, data: TIssue) => Promise<TIssue | undefined>;
|
||||
};
|
||||
|
||||
const defaultValues: Partial<TIssue> = {
|
||||
name: "",
|
||||
};
|
||||
|
||||
export const QuickAddIssueRoot: FC<TQuickAddIssueRoot> = observer((props) => {
|
||||
const {
|
||||
isQuickAddOpen,
|
||||
layout,
|
||||
prePopulatedData,
|
||||
QuickAddButton,
|
||||
customQuickAddButton,
|
||||
containerClassName = "",
|
||||
setIsQuickAddOpen,
|
||||
quickAddCallback,
|
||||
} = props;
|
||||
// router
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
const pathname = usePathname();
|
||||
// states
|
||||
const [isOpen, setIsOpen] = useState(isQuickAddOpen ?? false);
|
||||
// store hooks
|
||||
const { captureIssueEvent } = useEventTracker();
|
||||
// form info
|
||||
const {
|
||||
reset,
|
||||
handleSubmit,
|
||||
setFocus,
|
||||
register,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<TIssue>({ defaultValues });
|
||||
|
||||
useEffect(() => {
|
||||
if (isQuickAddOpen !== undefined) {
|
||||
setIsOpen(isQuickAddOpen);
|
||||
}
|
||||
}, [isQuickAddOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) reset({ ...defaultValues });
|
||||
}, [isOpen, reset]);
|
||||
|
||||
const handleIsOpen = (isOpen: boolean) => {
|
||||
if (isQuickAddOpen !== undefined && setIsQuickAddOpen) {
|
||||
setIsQuickAddOpen(isOpen);
|
||||
} else {
|
||||
setIsOpen(isOpen);
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmitHandler = async (formData: TIssue) => {
|
||||
if (isSubmitting || !workspaceSlug || !projectId) return;
|
||||
|
||||
reset({ ...defaultValues });
|
||||
|
||||
const payload = createIssuePayload(projectId.toString(), {
|
||||
...(prePopulatedData ?? {}),
|
||||
...formData,
|
||||
});
|
||||
|
||||
if (quickAddCallback) {
|
||||
const quickAddPromise = quickAddCallback(projectId.toString(), { ...payload });
|
||||
setPromiseToast<any>(quickAddPromise, {
|
||||
loading: "Adding issue...",
|
||||
success: {
|
||||
title: "Success!",
|
||||
message: () => "Issue created successfully.",
|
||||
actionItems: (data) => (
|
||||
<CreateIssueToastActionItems
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
issueId={data.id}
|
||||
/>
|
||||
),
|
||||
},
|
||||
error: {
|
||||
title: "Error!",
|
||||
message: (err) => err?.message || "Some error occurred. Please try again.",
|
||||
},
|
||||
});
|
||||
|
||||
await quickAddPromise
|
||||
.then((res) => {
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_CREATED,
|
||||
payload: { ...res, state: "SUCCESS", element: ` ${layout} quick add` },
|
||||
path: pathname,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_CREATED,
|
||||
payload: { ...payload, state: "FAILED", element: `${layout} quick ad` },
|
||||
path: pathname,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (!projectId) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
containerClassName,
|
||||
errors && errors?.name && errors?.name?.message ? `border-red-500 bg-red-500/10` : ``
|
||||
)}
|
||||
>
|
||||
{isOpen ? (
|
||||
<QuickAddIssueFormRoot
|
||||
isOpen={isOpen}
|
||||
layout={layout}
|
||||
prePopulatedData={prePopulatedData}
|
||||
projectId={projectId?.toString()}
|
||||
hasError={errors && errors?.name && errors?.name?.message ? true : false}
|
||||
setFocus={setFocus}
|
||||
register={register}
|
||||
onSubmit={handleSubmit(onSubmitHandler)}
|
||||
onClose={() => handleIsOpen(false)}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{QuickAddButton && <QuickAddButton onClick={() => handleIsOpen(true)} />}
|
||||
{customQuickAddButton && <>{customQuickAddButton}</>}
|
||||
{!QuickAddButton && !customQuickAddButton && (
|
||||
<div
|
||||
className="flex w-full cursor-pointer items-center gap-2 px-2 py-3 text-custom-text-350 hover:text-custom-text-300"
|
||||
onClick={() => handleIsOpen(true)}
|
||||
>
|
||||
<PlusIcon className="h-3.5 w-3.5 stroke-2" />
|
||||
<span className="text-sm font-medium">New Issue</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
export * from "./columns";
|
||||
export * from "./roots";
|
||||
export * from "./spreadsheet-view";
|
||||
export * from "./quick-add-issue-form";
|
||||
export * from "./spreadsheet-header-column";
|
||||
|
|
|
|||
|
|
@ -1,238 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { TIssue } from "@plane/types";
|
||||
// hooks
|
||||
import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui";
|
||||
import { CreateIssueToastActionItems } from "@/components/issues";
|
||||
import { ISSUE_CREATED } from "@/constants/event-tracker";
|
||||
import { createIssuePayload } from "@/helpers/issue.helper";
|
||||
import { useEventTracker, useProject, useWorkspace } from "@/hooks/store";
|
||||
import useKeypress from "@/hooks/use-keypress";
|
||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
// helpers
|
||||
// ui
|
||||
// types
|
||||
// constants
|
||||
|
||||
type Props = {
|
||||
formKey: keyof TIssue;
|
||||
groupId?: string;
|
||||
subGroupId?: string | null;
|
||||
prePopulatedData?: Partial<TIssue>;
|
||||
quickAddCallback?: (projectId: string | null | undefined, data: TIssue) => Promise<TIssue | undefined>;
|
||||
};
|
||||
|
||||
const defaultValues: Partial<TIssue> = {
|
||||
name: "",
|
||||
};
|
||||
|
||||
const Inputs = (props: any) => {
|
||||
const { register, setFocus, projectDetails } = props;
|
||||
|
||||
useEffect(() => {
|
||||
setFocus("name");
|
||||
}, [setFocus]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h4 className="w-20 text-xs leading-5 text-custom-text-400">{projectDetails?.identifier ?? "..."}</h4>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
placeholder="Issue Title"
|
||||
{...register("name", {
|
||||
required: "Issue title is required.",
|
||||
})}
|
||||
className="w-full rounded-md bg-transparent py-3 text-sm leading-5 text-custom-text-200 outline-none"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const SpreadsheetQuickAddIssueForm: React.FC<Props> = observer((props) => {
|
||||
const { formKey, prePopulatedData, quickAddCallback } = props;
|
||||
// store hooks
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { currentProjectDetails } = useProject();
|
||||
const { captureIssueEvent } = useEventTracker();
|
||||
// router
|
||||
const pathname = usePathname();
|
||||
// form info
|
||||
const {
|
||||
reset,
|
||||
handleSubmit,
|
||||
setFocus,
|
||||
register,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<TIssue>({ defaultValues });
|
||||
|
||||
// ref
|
||||
const ref = useRef<HTMLFormElement>(null);
|
||||
|
||||
// states
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const handleClose = () => setIsOpen(false);
|
||||
|
||||
// hooks
|
||||
useKeypress("Escape", handleClose);
|
||||
useOutsideClickDetector(ref, handleClose);
|
||||
|
||||
useEffect(() => {
|
||||
setFocus("name");
|
||||
}, [setFocus, isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) reset({ ...defaultValues });
|
||||
}, [isOpen, reset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!errors) return;
|
||||
|
||||
Object.keys(errors).forEach((key) => {
|
||||
const error = errors[key as keyof TIssue];
|
||||
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: error?.message?.toString() || "Some error occurred. Please try again.",
|
||||
});
|
||||
});
|
||||
}, [errors]);
|
||||
|
||||
// const onSubmitHandler = async (formData: TIssue) => {
|
||||
// if (isSubmitting || !workspaceSlug || !projectId) return;
|
||||
|
||||
// // resetting the form so that user can add another issue quickly
|
||||
// reset({ ...defaultValues });
|
||||
|
||||
// const payload = createIssuePayload(workspaceDetail!, projectDetails!, {
|
||||
// ...(prePopulatedData ?? {}),
|
||||
// ...formData,
|
||||
// });
|
||||
|
||||
// try {
|
||||
// quickAddStore.createIssue(
|
||||
// workspaceSlug.toString(),
|
||||
// projectId.toString(),
|
||||
// {
|
||||
// group_id: groupId ?? null,
|
||||
// sub_group_id: null,
|
||||
// },
|
||||
// payload
|
||||
// );
|
||||
|
||||
// setToast({
|
||||
// type: TOAST_TYPE.SUCCESS,
|
||||
// title: "Success!",
|
||||
// message: "Issue created successfully.",
|
||||
// });
|
||||
// } catch (err: any) {
|
||||
// Object.keys(err || {}).forEach((key) => {
|
||||
// const error = err?.[key];
|
||||
// const errorTitle = error ? (Array.isArray(error) ? error.join(", ") : error) : null;
|
||||
|
||||
// setToast({
|
||||
// type: TOAST_TYPE.ERROR,
|
||||
// title: "Error!",
|
||||
// message: errorTitle || "Some error occurred. Please try again.",
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
// };
|
||||
|
||||
const onSubmitHandler = async (formData: TIssue) => {
|
||||
if (isSubmitting || !currentWorkspace || !currentProjectDetails) return;
|
||||
|
||||
reset({ ...defaultValues });
|
||||
|
||||
const payload = createIssuePayload(currentProjectDetails.id, {
|
||||
...(prePopulatedData ?? {}),
|
||||
...formData,
|
||||
});
|
||||
|
||||
if (quickAddCallback) {
|
||||
const quickAddPromise = quickAddCallback(currentProjectDetails.id, { ...payload } as TIssue);
|
||||
setPromiseToast<any>(quickAddPromise, {
|
||||
loading: "Adding issue...",
|
||||
success: {
|
||||
title: "Success!",
|
||||
message: () => "Issue created successfully.",
|
||||
actionItems: (data) => (
|
||||
<CreateIssueToastActionItems
|
||||
workspaceSlug={currentWorkspace.slug}
|
||||
projectId={currentProjectDetails.id}
|
||||
issueId={data.id}
|
||||
/>
|
||||
),
|
||||
},
|
||||
error: {
|
||||
title: "Error!",
|
||||
message: (err) => err?.message || "Some error occurred. Please try again.",
|
||||
},
|
||||
});
|
||||
|
||||
await quickAddPromise
|
||||
.then((res) => {
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_CREATED,
|
||||
payload: { ...res, state: "SUCCESS", element: "Spreadsheet quick add" },
|
||||
path: pathname,
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_CREATED,
|
||||
payload: { ...payload, state: "FAILED", element: "Spreadsheet quick add" },
|
||||
path: pathname,
|
||||
});
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isOpen && (
|
||||
<div>
|
||||
<form
|
||||
ref={ref}
|
||||
onSubmit={handleSubmit(onSubmitHandler)}
|
||||
className="z-10 flex items-center gap-x-5 border-[0.5px] border-t-0 border-custom-border-100 bg-custom-background-100 px-4 shadow-custom-shadow-sm"
|
||||
>
|
||||
<Inputs
|
||||
formKey={formKey}
|
||||
register={register}
|
||||
setFocus={setFocus}
|
||||
projectDetails={currentProjectDetails ?? null}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isOpen && (
|
||||
<p className="ml-3 mt-3 text-xs italic text-custom-text-200">
|
||||
Press {"'"}Enter{"'"} to add another issue
|
||||
</p>
|
||||
)}
|
||||
|
||||
{!isOpen && (
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-x-[6px] rounded-md px-2 pt-3 text-custom-text-350 hover:text-custom-text-300"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<PlusIcon className="h-3.5 w-3.5 stroke-2" />
|
||||
<span className="text-sm font-medium">New Issue</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -5,8 +5,9 @@ import { TIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@pl
|
|||
// components
|
||||
import { LogoSpinner } from "@/components/common";
|
||||
import { MultipleSelectGroup } from "@/components/core";
|
||||
import { SpreadsheetQuickAddIssueForm } from "@/components/issues";
|
||||
import { QuickAddIssueRoot, SpreadsheetAddIssueButton } from "@/components/issues";
|
||||
// constants
|
||||
import { EIssueLayoutTypes } from "@/constants/issue";
|
||||
import { SPREADSHEET_PROPERTY_LIST, SPREADSHEET_SELECT_GROUP } from "@/constants/spreadsheet";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store";
|
||||
|
|
@ -109,7 +110,11 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
|
|||
<div className="border-t border-custom-border-100">
|
||||
<div className="z-5 sticky bottom-0 left-0 mb-3">
|
||||
{enableQuickCreateIssue && !disableIssueCreation && (
|
||||
<SpreadsheetQuickAddIssueForm formKey="name" quickAddCallback={quickAddCallback} />
|
||||
<QuickAddIssueRoot
|
||||
layout={EIssueLayoutTypes.SPREADSHEET}
|
||||
QuickAddButton={SpreadsheetAddIssueButton}
|
||||
quickAddCallback={quickAddCallback}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ export const IssueTitleInput: FC<IssueTitleInputProps> = observer((props) => {
|
|||
/255
|
||||
</div>
|
||||
</div>
|
||||
{title?.length === 0 && <span className="text-sm text-red-500">Title is required</span>}
|
||||
{title?.length === 0 && <span className="text-sm font-medium text-red-500">Title is required</span>}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,3 +2,4 @@ export * from "./bulk-operations";
|
|||
export * from "./worklog";
|
||||
export * from "./issue-modal";
|
||||
export * from "./issue-details";
|
||||
export * from "./quick-add";
|
||||
|
|
|
|||
1
web/ee/components/issues/quick-add/index.ts
Normal file
1
web/ee/components/issues/quick-add/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "./root";
|
||||
1
web/ee/components/issues/quick-add/root.tsx
Normal file
1
web/ee/components/issues/quick-add/root.tsx
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "ce/components/issues/quick-add/root";
|
||||
Loading…
Add table
Add a link
Reference in a new issue