feat: init
This commit is contained in:
@@ -0,0 +1,306 @@
|
||||
"use client";
|
||||
|
||||
import type { FC, FormEvent } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { ETabIndices, WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
|
||||
import type { EditorRefApi } from "@plane/editor";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import type { TIssue } from "@plane/types";
|
||||
import { ToggleSwitch } from "@plane/ui";
|
||||
import { renderFormattedPayloadDate, getTabIndex } from "@plane/utils";
|
||||
// helpers
|
||||
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { useProjectInbox } from "@/hooks/store/use-project-inbox";
|
||||
import { useWorkspace } from "@/hooks/store/use-workspace";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import useKeypress from "@/hooks/use-keypress";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web imports
|
||||
import { DeDupeButtonRoot } from "@/plane-web/components/de-dupe/de-dupe-button";
|
||||
import { DuplicateModalRoot } from "@/plane-web/components/de-dupe/duplicate-modal";
|
||||
import { useDebouncedDuplicateIssues } from "@/plane-web/hooks/use-debounced-duplicate-issues";
|
||||
// services
|
||||
import { FileService } from "@/services/file.service";
|
||||
// local imports
|
||||
import { InboxIssueDescription } from "./issue-description";
|
||||
import { InboxIssueProperties } from "./issue-properties";
|
||||
import { InboxIssueTitle } from "./issue-title";
|
||||
|
||||
const fileService = new FileService();
|
||||
|
||||
type TInboxIssueCreateRoot = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
handleModalClose: () => void;
|
||||
isDuplicateModalOpen: boolean;
|
||||
handleDuplicateIssueModal: (value: boolean) => void;
|
||||
};
|
||||
|
||||
export const defaultIssueData: Partial<TIssue> = {
|
||||
id: undefined,
|
||||
name: "",
|
||||
description_html: "",
|
||||
priority: "none",
|
||||
state_id: "",
|
||||
label_ids: [],
|
||||
assignee_ids: [],
|
||||
start_date: renderFormattedPayloadDate(new Date()),
|
||||
target_date: "",
|
||||
};
|
||||
|
||||
export const InboxIssueCreateRoot: FC<TInboxIssueCreateRoot> = observer((props) => {
|
||||
const { workspaceSlug, projectId, handleModalClose, isDuplicateModalOpen, handleDuplicateIssueModal } = props;
|
||||
// states
|
||||
const [uploadedAssetIds, setUploadedAssetIds] = useState<string[]>([]);
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
// refs
|
||||
const descriptionEditorRef = useRef<EditorRefApi>(null);
|
||||
const submitBtnRef = useRef<HTMLButtonElement | null>(null);
|
||||
const formRef = useRef<HTMLFormElement | null>(null);
|
||||
const modalContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
// hooks
|
||||
const { createInboxIssue } = useProjectInbox();
|
||||
const { getWorkspaceBySlug } = useWorkspace();
|
||||
const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id;
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { getProjectById } = useProject();
|
||||
const { t } = useTranslation();
|
||||
// states
|
||||
const [createMore, setCreateMore] = useState<boolean>(false);
|
||||
const [formSubmitting, setFormSubmitting] = useState(false);
|
||||
const [formData, setFormData] = useState<Partial<TIssue>>(defaultIssueData);
|
||||
const handleFormData = useCallback(
|
||||
<T extends keyof Partial<TIssue>>(issueKey: T, issueValue: Partial<TIssue>[T]) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
[issueKey]: issueValue,
|
||||
});
|
||||
},
|
||||
[formData]
|
||||
);
|
||||
|
||||
// derived values
|
||||
const projectDetails = projectId ? getProjectById(projectId) : undefined;
|
||||
|
||||
const { getIndex } = getTabIndex(ETabIndices.INTAKE_ISSUE_FORM, isMobile);
|
||||
|
||||
// debounced duplicate issues swr
|
||||
const { duplicateIssues } = useDebouncedDuplicateIssues(
|
||||
workspaceSlug,
|
||||
projectDetails?.workspace.toString(),
|
||||
projectId,
|
||||
{
|
||||
name: formData?.name,
|
||||
description_html: formData?.description_html,
|
||||
}
|
||||
);
|
||||
|
||||
const handleEscKeyDown = (event: KeyboardEvent) => {
|
||||
if (descriptionEditorRef.current?.isEditorReadyToDiscard()) {
|
||||
handleModalClose();
|
||||
} else {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Editor is still processing changes. Please wait before proceeding.",
|
||||
});
|
||||
event.preventDefault(); // Prevent default action if editor is not ready to discard
|
||||
}
|
||||
};
|
||||
|
||||
useKeypress("Escape", handleEscKeyDown);
|
||||
|
||||
useEffect(() => {
|
||||
const formElement = formRef?.current;
|
||||
const modalElement = modalContainerRef?.current;
|
||||
|
||||
if (!formElement || !modalElement) return;
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
modalElement.style.maxHeight = `${formElement?.offsetHeight}px`;
|
||||
});
|
||||
|
||||
resizeObserver.observe(formElement);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [formRef, modalContainerRef]);
|
||||
|
||||
const handleFormSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!descriptionEditorRef.current?.isEditorReadyToDiscard()) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Editor is still processing changes. Please wait before proceeding.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const payload: Partial<TIssue> = {
|
||||
name: formData.name || "",
|
||||
description_html: formData.description_html || "<p></p>",
|
||||
priority: formData.priority || "none",
|
||||
state_id: formData.state_id || "",
|
||||
label_ids: formData.label_ids || [],
|
||||
assignee_ids: formData.assignee_ids || [],
|
||||
target_date: formData.target_date || null,
|
||||
};
|
||||
setFormSubmitting(true);
|
||||
|
||||
await createInboxIssue(workspaceSlug, projectId, payload)
|
||||
.then(async (res) => {
|
||||
if (uploadedAssetIds.length > 0) {
|
||||
await fileService.updateBulkProjectAssetsUploadStatus(workspaceSlug, projectId, res?.issue.id ?? "", {
|
||||
asset_ids: uploadedAssetIds,
|
||||
});
|
||||
setUploadedAssetIds([]);
|
||||
}
|
||||
if (!createMore) {
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/intake/?currentTab=open&inboxIssueId=${res?.issue?.id}`);
|
||||
handleModalClose();
|
||||
} else {
|
||||
descriptionEditorRef?.current?.clearEditor();
|
||||
setFormData(defaultIssueData);
|
||||
}
|
||||
captureSuccess({
|
||||
eventName: WORK_ITEM_TRACKER_EVENTS.create,
|
||||
payload: {
|
||||
id: res?.issue?.id,
|
||||
},
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: `Success!`,
|
||||
message: "Work item created successfully.",
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
captureError({
|
||||
eventName: WORK_ITEM_TRACKER_EVENTS.create,
|
||||
payload: {
|
||||
id: formData?.id,
|
||||
},
|
||||
error: error as Error,
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: `Error!`,
|
||||
message: "Some error occurred. Please try again.",
|
||||
});
|
||||
});
|
||||
setFormSubmitting(false);
|
||||
};
|
||||
|
||||
const isTitleLengthMoreThan255Character = formData?.name ? formData.name.length > 255 : false;
|
||||
|
||||
const shouldRenderDuplicateModal = isDuplicateModalOpen && duplicateIssues?.length > 0;
|
||||
|
||||
if (!workspaceSlug || !projectId || !workspaceId) return <></>;
|
||||
return (
|
||||
<div className="flex gap-2 bg-transparent w-full">
|
||||
<div className="rounded-lg w-full">
|
||||
<form ref={formRef} onSubmit={handleFormSubmit} className="flex flex-col w-full">
|
||||
<div className="space-y-5 p-5 rounded-t-lg bg-custom-background-100">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h3 className="text-xl font-medium text-custom-text-200">{t("inbox_issue.modal.title")}</h3>
|
||||
{duplicateIssues?.length > 0 && (
|
||||
<DeDupeButtonRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
isDuplicateModalOpen={isDuplicateModalOpen}
|
||||
label={`${duplicateIssues.length} duplicate issue${duplicateIssues.length > 1 ? "s" : ""} found!`}
|
||||
handleOnClick={() => handleDuplicateIssueModal(!isDuplicateModalOpen)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<InboxIssueTitle
|
||||
data={formData}
|
||||
handleData={handleFormData}
|
||||
isTitleLengthMoreThan255Character={isTitleLengthMoreThan255Character}
|
||||
/>
|
||||
<InboxIssueDescription
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
workspaceId={workspaceId}
|
||||
data={formData}
|
||||
handleData={handleFormData}
|
||||
editorRef={descriptionEditorRef}
|
||||
containerClassName="border-[0.5px] border-custom-border-200 py-3 min-h-[150px]"
|
||||
onEnterKeyPress={() => submitBtnRef?.current?.click()}
|
||||
onAssetUpload={(assetId) => setUploadedAssetIds((prev) => [...prev, assetId])}
|
||||
/>
|
||||
<InboxIssueProperties projectId={projectId} data={formData} handleData={handleFormData} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-5 py-4 flex items-center justify-between gap-2 border-t-[0.5px] border-custom-border-200 rounded-b-lg bg-custom-background-100">
|
||||
<div
|
||||
className="inline-flex items-center gap-1.5 cursor-pointer"
|
||||
onClick={() => setCreateMore((prevData) => !prevData)}
|
||||
role="button"
|
||||
tabIndex={getIndex("create_more")}
|
||||
>
|
||||
<ToggleSwitch value={createMore} onChange={() => {}} size="sm" />
|
||||
<span className="text-xs">{t("create_more")}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
variant="neutral-primary"
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (descriptionEditorRef.current?.isEditorReadyToDiscard()) {
|
||||
handleModalClose();
|
||||
} else {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Editor is still processing changes. Please wait before proceeding.",
|
||||
});
|
||||
}
|
||||
}}
|
||||
tabIndex={getIndex("discard_button")}
|
||||
>
|
||||
{t("discard")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
ref={submitBtnRef}
|
||||
size="sm"
|
||||
type="submit"
|
||||
loading={formSubmitting}
|
||||
disabled={isTitleLengthMoreThan255Character}
|
||||
tabIndex={getIndex("submit_button")}
|
||||
>
|
||||
{formSubmitting ? t("creating") : t("create_work_item")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{shouldRenderDuplicateModal && (
|
||||
<div
|
||||
ref={modalContainerRef}
|
||||
className="relative flex flex-col gap-2.5 px-3 py-4 rounded-lg shadow-xl bg-pi-50"
|
||||
style={{ maxHeight: formRef?.current?.offsetHeight ? `${formRef.current.offsetHeight}px` : "436px" }}
|
||||
>
|
||||
<DuplicateModalRoot
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
issues={duplicateIssues}
|
||||
handleDuplicateIssueModal={handleDuplicateIssueModal}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./modal";
|
||||
@@ -0,0 +1,107 @@
|
||||
"use client";
|
||||
|
||||
import type { FC, RefObject } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { ETabIndices } from "@plane/constants";
|
||||
import type { EditorRefApi } from "@plane/editor";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import type { TIssue } from "@plane/types";
|
||||
import { EFileAssetType } from "@plane/types";
|
||||
import { Loader } from "@plane/ui";
|
||||
import { getDescriptionPlaceholderI18n, getTabIndex } from "@plane/utils";
|
||||
// components
|
||||
import { RichTextEditor } from "@/components/editor/rich-text/editor";
|
||||
// hooks
|
||||
import { useEditorAsset } from "@/hooks/store/use-editor-asset";
|
||||
import { useProjectInbox } from "@/hooks/store/use-project-inbox";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// services
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
|
||||
const workspaceService = new WorkspaceService();
|
||||
|
||||
type TInboxIssueDescription = {
|
||||
containerClassName?: string;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
workspaceId: string;
|
||||
data: Partial<TIssue>;
|
||||
handleData: (issueKey: keyof Partial<TIssue>, issueValue: Partial<TIssue>[keyof Partial<TIssue>]) => void;
|
||||
editorRef: RefObject<EditorRefApi>;
|
||||
onEnterKeyPress?: (e?: any) => void;
|
||||
onAssetUpload?: (assetId: string) => void;
|
||||
};
|
||||
|
||||
// TODO: have to implement GPT Assistance
|
||||
export const InboxIssueDescription: FC<TInboxIssueDescription> = observer((props) => {
|
||||
const {
|
||||
containerClassName,
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
workspaceId,
|
||||
data,
|
||||
handleData,
|
||||
editorRef,
|
||||
onEnterKeyPress,
|
||||
onAssetUpload,
|
||||
} = props;
|
||||
// i18n
|
||||
const { t } = useTranslation();
|
||||
// store hooks
|
||||
const { uploadEditorAsset } = useEditorAsset();
|
||||
const { loader } = useProjectInbox();
|
||||
const { isMobile } = usePlatformOS();
|
||||
|
||||
const { getIndex } = getTabIndex(ETabIndices.INTAKE_ISSUE_FORM, isMobile);
|
||||
|
||||
if (loader === "issue-loading")
|
||||
return (
|
||||
<Loader className="min-h-[6rem] rounded-md border border-custom-border-200">
|
||||
<Loader.Item width="100%" height="140px" />
|
||||
</Loader>
|
||||
);
|
||||
|
||||
return (
|
||||
<RichTextEditor
|
||||
editable
|
||||
id="inbox-modal-editor"
|
||||
initialValue={!data?.description_html || data?.description_html === "" ? "<p></p>" : data?.description_html}
|
||||
ref={editorRef}
|
||||
workspaceSlug={workspaceSlug}
|
||||
workspaceId={workspaceId}
|
||||
projectId={projectId}
|
||||
dragDropEnabled={false}
|
||||
onChange={(_description: object, description_html: string) => handleData("description_html", description_html)}
|
||||
placeholder={(isFocused, description) => t(`${getDescriptionPlaceholderI18n(isFocused, description)}`)}
|
||||
searchMentionCallback={async (payload) =>
|
||||
await workspaceService.searchEntity(workspaceSlug?.toString() ?? "", {
|
||||
...payload,
|
||||
project_id: projectId?.toString() ?? "",
|
||||
})
|
||||
}
|
||||
containerClassName={containerClassName}
|
||||
onEnterKeyPress={onEnterKeyPress}
|
||||
tabIndex={getIndex("description_html")}
|
||||
uploadFile={async (blockId, file) => {
|
||||
try {
|
||||
const { asset_id } = await uploadEditorAsset({
|
||||
blockId,
|
||||
data: {
|
||||
entity_identifier: data.id ?? "",
|
||||
entity_type: EFileAssetType.ISSUE_DESCRIPTION,
|
||||
},
|
||||
file,
|
||||
projectId,
|
||||
workspaceSlug,
|
||||
});
|
||||
onAssetUpload?.(asset_id);
|
||||
return asset_id;
|
||||
} catch (error) {
|
||||
console.log("Error in uploading work item asset:", error);
|
||||
throw new Error("Asset upload failed. Please try again later.");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,233 @@
|
||||
import type { FC } from "react";
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { LayoutPanelTop } from "lucide-react";
|
||||
// plane imports
|
||||
import { ETabIndices } from "@plane/constants";
|
||||
import type { ISearchIssueResponse, TIssue } from "@plane/types";
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
import { renderFormattedPayloadDate, getDate, getTabIndex } from "@plane/utils";
|
||||
// components
|
||||
import { CycleDropdown } from "@/components/dropdowns/cycle";
|
||||
import { DateDropdown } from "@/components/dropdowns/date";
|
||||
import { EstimateDropdown } from "@/components/dropdowns/estimate";
|
||||
import { MemberDropdown } from "@/components/dropdowns/member/dropdown";
|
||||
import { ModuleDropdown } from "@/components/dropdowns/module/dropdown";
|
||||
import { PriorityDropdown } from "@/components/dropdowns/priority";
|
||||
import { StateDropdown } from "@/components/dropdowns/state/dropdown";
|
||||
import { ParentIssuesListModal } from "@/components/issues/parent-issues-list-modal";
|
||||
import { IssueLabelSelect } from "@/components/issues/select";
|
||||
// helpers
|
||||
// hooks
|
||||
import { useProjectEstimates } from "@/hooks/store/estimates";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
|
||||
type TInboxIssueProperties = {
|
||||
projectId: string;
|
||||
data: Partial<TIssue>;
|
||||
handleData: (issueKey: keyof Partial<TIssue>, issueValue: Partial<TIssue>[keyof Partial<TIssue>]) => void;
|
||||
isVisible?: boolean;
|
||||
};
|
||||
|
||||
export const InboxIssueProperties: FC<TInboxIssueProperties> = observer((props) => {
|
||||
const { projectId, data, handleData, isVisible = false } = props;
|
||||
// hooks
|
||||
const { areEstimateEnabledByProjectId } = useProjectEstimates();
|
||||
const { isMobile } = usePlatformOS();
|
||||
// states
|
||||
const [parentIssueModalOpen, setParentIssueModalOpen] = useState(false);
|
||||
const [selectedParentIssue, setSelectedParentIssue] = useState<ISearchIssueResponse | undefined>(undefined);
|
||||
|
||||
const { getIndex } = getTabIndex(ETabIndices.INTAKE_ISSUE_FORM, isMobile);
|
||||
|
||||
const startDate = data?.start_date;
|
||||
const targetDate = data?.target_date;
|
||||
|
||||
const minDate = getDate(startDate);
|
||||
minDate?.setDate(minDate.getDate());
|
||||
|
||||
const maxDate = getDate(targetDate);
|
||||
maxDate?.setDate(maxDate.getDate());
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-wrap gap-2 items-center">
|
||||
{/* state */}
|
||||
<div className="h-7">
|
||||
<StateDropdown
|
||||
value={data?.state_id}
|
||||
onChange={(stateId) => handleData("state_id", stateId)}
|
||||
projectId={projectId}
|
||||
buttonVariant="border-with-text"
|
||||
tabIndex={getIndex("state_id")}
|
||||
isForWorkItemCreation={!data?.id}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* priority */}
|
||||
<div className="h-7">
|
||||
<PriorityDropdown
|
||||
value={data?.priority}
|
||||
onChange={(priority) => handleData("priority", priority)}
|
||||
buttonVariant="border-with-text"
|
||||
tabIndex={getIndex("priority")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Assignees */}
|
||||
<div className="h-7">
|
||||
<MemberDropdown
|
||||
projectId={projectId}
|
||||
value={data?.assignee_ids || []}
|
||||
onChange={(assigneeIds) => handleData("assignee_ids", assigneeIds)}
|
||||
buttonVariant={(data?.assignee_ids || [])?.length > 0 ? "transparent-without-text" : "border-with-text"}
|
||||
buttonClassName={(data?.assignee_ids || [])?.length > 0 ? "hover:bg-transparent" : ""}
|
||||
placeholder="Assignees"
|
||||
multiple
|
||||
tabIndex={getIndex("assignee_ids")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* labels */}
|
||||
<div className="h-7">
|
||||
<IssueLabelSelect
|
||||
value={data?.label_ids || []}
|
||||
onChange={(labelIds) => handleData("label_ids", labelIds)}
|
||||
projectId={projectId}
|
||||
tabIndex={getIndex("label_ids")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* start date */}
|
||||
{isVisible && (
|
||||
<div className="h-7">
|
||||
<DateDropdown
|
||||
value={data?.start_date || null}
|
||||
onChange={(date) => handleData("start_date", date ? renderFormattedPayloadDate(date) : "")}
|
||||
buttonVariant="border-with-text"
|
||||
minDate={minDate ?? undefined}
|
||||
placeholder="Start date"
|
||||
tabIndex={getIndex("start_date")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* due date */}
|
||||
<div className="h-7">
|
||||
<DateDropdown
|
||||
value={data?.target_date || null}
|
||||
onChange={(date) => handleData("target_date", date ? renderFormattedPayloadDate(date) : "")}
|
||||
buttonVariant="border-with-text"
|
||||
minDate={minDate ?? undefined}
|
||||
placeholder="Due date"
|
||||
tabIndex={getIndex("target_date")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* cycle */}
|
||||
{isVisible && (
|
||||
<div className="h-7">
|
||||
<CycleDropdown
|
||||
value={data?.cycle_id || ""}
|
||||
onChange={(cycleId) => handleData("cycle_id", cycleId)}
|
||||
projectId={projectId}
|
||||
placeholder="Cycle"
|
||||
buttonVariant="border-with-text"
|
||||
tabIndex={getIndex("cycle_id")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* module */}
|
||||
{isVisible && (
|
||||
<div className="h-7">
|
||||
<ModuleDropdown
|
||||
value={data?.module_ids || []}
|
||||
onChange={(moduleIds) => handleData("module_ids", moduleIds)}
|
||||
projectId={projectId}
|
||||
placeholder="Modules"
|
||||
buttonVariant="border-with-text"
|
||||
multiple
|
||||
showCount
|
||||
tabIndex={getIndex("module_ids")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* estimate */}
|
||||
{isVisible && projectId && areEstimateEnabledByProjectId(projectId) && (
|
||||
<div className="h-7">
|
||||
<EstimateDropdown
|
||||
value={data?.estimate_point || undefined}
|
||||
onChange={(estimatePoint) => handleData("estimate_point", estimatePoint)}
|
||||
projectId={projectId}
|
||||
buttonVariant="border-with-text"
|
||||
placeholder="Estimate"
|
||||
tabIndex={getIndex("estimate_point")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* add parent */}
|
||||
{isVisible && (
|
||||
<div className="h-7">
|
||||
{selectedParentIssue ? (
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<button
|
||||
type="button"
|
||||
className="flex cursor-pointer items-center justify-between gap-1 h-full rounded border-[0.5px] border-custom-border-300 px-2 py-0.5 text-xs hover:bg-custom-background-80"
|
||||
>
|
||||
<LayoutPanelTop className="h-3 w-3 flex-shrink-0" />
|
||||
<span className="whitespace-nowrap">
|
||||
{selectedParentIssue
|
||||
? `${selectedParentIssue.project__identifier}-${selectedParentIssue.sequence_id}`
|
||||
: `Add parent`}
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
placement="bottom-start"
|
||||
className="h-full w-full"
|
||||
customButtonClassName="h-full"
|
||||
tabIndex={getIndex("parent_id")}
|
||||
>
|
||||
<>
|
||||
<CustomMenu.MenuItem className="!p-1" onClick={() => setParentIssueModalOpen(true)}>
|
||||
Change parent work item
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
className="!p-1"
|
||||
onClick={() => {
|
||||
handleData("parent_id", "");
|
||||
setSelectedParentIssue(undefined);
|
||||
}}
|
||||
>
|
||||
Remove parent work item
|
||||
</CustomMenu.MenuItem>
|
||||
</>
|
||||
</CustomMenu>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="flex cursor-pointer items-center justify-between gap-1 h-full rounded border-[0.5px] border-custom-border-300 px-2 py-0.5 text-xs hover:bg-custom-background-80"
|
||||
onClick={() => setParentIssueModalOpen(true)}
|
||||
>
|
||||
<LayoutPanelTop className="h-3 w-3 flex-shrink-0" />
|
||||
<span className="whitespace-nowrap">Add parent</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<ParentIssuesListModal
|
||||
isOpen={parentIssueModalOpen}
|
||||
handleClose={() => setParentIssueModalOpen(false)}
|
||||
onChange={(issue) => {
|
||||
handleData("parent_id", issue?.id);
|
||||
setSelectedParentIssue(issue);
|
||||
}}
|
||||
projectId={projectId}
|
||||
issueId={undefined}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import type { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { ETabIndices } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import type { TIssue } from "@plane/types";
|
||||
import { Input } from "@plane/ui";
|
||||
// helpers
|
||||
import { getTabIndex } from "@plane/utils";
|
||||
// hooks
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
|
||||
type TInboxIssueTitle = {
|
||||
data: Partial<TIssue>;
|
||||
handleData: (issueKey: keyof Partial<TIssue>, issueValue: Partial<TIssue>[keyof Partial<TIssue>]) => void;
|
||||
isTitleLengthMoreThan255Character?: boolean;
|
||||
};
|
||||
|
||||
export const InboxIssueTitle: FC<TInboxIssueTitle> = observer((props) => {
|
||||
const { data, handleData, isTitleLengthMoreThan255Character } = props;
|
||||
// hooks
|
||||
const { isMobile } = usePlatformOS();
|
||||
|
||||
const { getIndex } = getTabIndex(ETabIndices.INTAKE_ISSUE_FORM, isMobile);
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
value={data?.name}
|
||||
onChange={(e) => handleData("name", e.target.value)}
|
||||
placeholder={t("title")}
|
||||
className="w-full text-base"
|
||||
tabIndex={getIndex("name")}
|
||||
required
|
||||
/>
|
||||
{isTitleLengthMoreThan255Character && (
|
||||
<span className="text-xs text-red-500">{t("title_should_be_less_than_255_characters")}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
49
apps/web/core/components/inbox/modals/create-modal/modal.tsx
Normal file
49
apps/web/core/components/inbox/modals/create-modal/modal.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
"use-client";
|
||||
|
||||
import type { FC } from "react";
|
||||
import { useState } from "react";
|
||||
// plane imports
|
||||
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
|
||||
// hooks
|
||||
import useKeypress from "@/hooks/use-keypress";
|
||||
// local imports
|
||||
import { InboxIssueCreateRoot } from "./create-root";
|
||||
|
||||
type TInboxIssueCreateModalRoot = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
modalState: boolean;
|
||||
handleModalClose: () => void;
|
||||
};
|
||||
|
||||
export const InboxIssueCreateModalRoot: FC<TInboxIssueCreateModalRoot> = (props) => {
|
||||
const { workspaceSlug, projectId, modalState, handleModalClose } = props;
|
||||
// states
|
||||
const [isDuplicateModalOpen, setIsDuplicateModalOpen] = useState(false);
|
||||
// handlers
|
||||
const handleDuplicateIssueModal = (value: boolean) => setIsDuplicateModalOpen(value);
|
||||
|
||||
useKeypress("Escape", () => {
|
||||
if (modalState) {
|
||||
handleModalClose();
|
||||
setIsDuplicateModalOpen(false);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<ModalCore
|
||||
isOpen={modalState}
|
||||
position={EModalPosition.TOP}
|
||||
width={isDuplicateModalOpen ? EModalWidth.VIXL : EModalWidth.XXXXL}
|
||||
className="!bg-transparent rounded-lg shadow-none transition-[width] ease-linear"
|
||||
>
|
||||
<InboxIssueCreateRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
handleModalClose={handleModalClose}
|
||||
isDuplicateModalOpen={isDuplicateModalOpen}
|
||||
handleDuplicateIssueModal={handleDuplicateIssueModal}
|
||||
/>
|
||||
</ModalCore>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import React, { useState } from "react";
|
||||
// types
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import type { TIssue } from "@plane/types";
|
||||
// ui
|
||||
import { AlertModalCore } from "@plane/ui";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
|
||||
type Props = {
|
||||
data: Partial<TIssue>;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: () => Promise<void>;
|
||||
};
|
||||
|
||||
export const DeclineIssueModal: React.FC<Props> = (props) => {
|
||||
const { isOpen, onClose, data, onSubmit } = props;
|
||||
// states
|
||||
const [isDeclining, setIsDeclining] = useState(false);
|
||||
// store hooks
|
||||
const { getProjectById } = useProject();
|
||||
const { t } = useTranslation();
|
||||
// derived values
|
||||
const projectDetails = data.project_id ? getProjectById(data?.project_id) : undefined;
|
||||
|
||||
const handleClose = () => {
|
||||
setIsDeclining(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleDecline = async () => {
|
||||
setIsDeclining(true);
|
||||
await onSubmit().finally(() => setIsDeclining(false));
|
||||
};
|
||||
|
||||
return (
|
||||
<AlertModalCore
|
||||
handleClose={handleClose}
|
||||
handleSubmit={handleDecline}
|
||||
isSubmitting={isDeclining}
|
||||
isOpen={isOpen}
|
||||
title={t("inbox_issue.modals.decline.title")}
|
||||
// TODO: Need to translate the confirmation message
|
||||
content={
|
||||
<>
|
||||
Are you sure you want to decline work item{" "}
|
||||
<span className="break-words font-medium text-custom-text-100">
|
||||
{projectDetails?.identifier}-{data?.sequence_id}
|
||||
</span>
|
||||
{""}? This action cannot be undone.
|
||||
</>
|
||||
}
|
||||
primaryButtonText={{
|
||||
loading: t("declining"),
|
||||
default: t("decline"),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
78
apps/web/core/components/inbox/modals/delete-issue-modal.tsx
Normal file
78
apps/web/core/components/inbox/modals/delete-issue-modal.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// types
|
||||
import { PROJECT_ERROR_MESSAGES } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import type { TIssue } from "@plane/types";
|
||||
// ui
|
||||
import { AlertModalCore } from "@plane/ui";
|
||||
// constants
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
|
||||
type Props = {
|
||||
data: Partial<TIssue>;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: () => Promise<void>;
|
||||
};
|
||||
|
||||
export const DeleteInboxIssueModal: React.FC<Props> = observer(({ isOpen, onClose, onSubmit, data }) => {
|
||||
// states
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
// store hooks
|
||||
const { getProjectById } = useProject();
|
||||
const { t } = useTranslation();
|
||||
// derived values
|
||||
const projectDetails = data.project_id ? getProjectById(data?.project_id) : undefined;
|
||||
|
||||
const handleClose = () => {
|
||||
setIsDeleting(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
setIsDeleting(true);
|
||||
await onSubmit()
|
||||
.then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: `${t("success")!}`,
|
||||
message: `${t("inbox_issue.modals.delete.success")!}`,
|
||||
});
|
||||
})
|
||||
.catch((errors) => {
|
||||
const isPermissionError = errors?.error === "Only admin or creator can delete the work item";
|
||||
const currentError = isPermissionError
|
||||
? PROJECT_ERROR_MESSAGES.permissionError
|
||||
: PROJECT_ERROR_MESSAGES.issueDeleteError;
|
||||
setToast({
|
||||
title: t(currentError.i18n_title),
|
||||
type: TOAST_TYPE.ERROR,
|
||||
message: currentError.i18n_message && t(currentError.i18n_message),
|
||||
});
|
||||
})
|
||||
.finally(() => handleClose());
|
||||
};
|
||||
|
||||
return (
|
||||
<AlertModalCore
|
||||
handleClose={handleClose}
|
||||
handleSubmit={handleDelete}
|
||||
isSubmitting={isDeleting}
|
||||
isOpen={isOpen}
|
||||
title={t("inbox_issue.modals.delete.title")}
|
||||
// TODO: Need to translate the confirmation message
|
||||
content={
|
||||
<>
|
||||
Are you sure you want to delete work item{" "}
|
||||
<span className="break-words font-medium text-custom-text-100">
|
||||
{projectDetails?.identifier}-{data?.sequence_id}
|
||||
</span>
|
||||
{""}? The work item will only be deleted from the intake and this action cannot be undone.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
189
apps/web/core/components/inbox/modals/select-duplicate.tsx
Normal file
189
apps/web/core/components/inbox/modals/select-duplicate.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Search } from "lucide-react";
|
||||
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import type { ISearchIssueResponse } from "@plane/types";
|
||||
import { Loader } from "@plane/ui";
|
||||
// components
|
||||
import { SimpleEmptyState } from "@/components/empty-state/simple-empty-state-root";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import useDebounce from "@/hooks/use-debounce";
|
||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||
// services
|
||||
import { ProjectService } from "@/services/project";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
value?: string | null;
|
||||
onClose: () => void;
|
||||
onSubmit: (issueId: string) => void;
|
||||
};
|
||||
|
||||
const projectService = new ProjectService();
|
||||
|
||||
export const SelectDuplicateInboxIssueModal: React.FC<Props> = (props) => {
|
||||
const { isOpen, onClose, onSubmit, value } = props;
|
||||
// router
|
||||
const { workspaceSlug, projectId, issueId } = useParams();
|
||||
// states
|
||||
const [query, setQuery] = useState("");
|
||||
const [issues, setIssues] = useState<ISearchIssueResponse[]>([]);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
// hooks
|
||||
const { getProjectById } = useProject();
|
||||
const { t } = useTranslation();
|
||||
// derived values
|
||||
const debouncedSearchTerm: string = useDebounce(query, 500);
|
||||
const searchResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/search/search" });
|
||||
const issuesResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/search/issues" });
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen || !workspaceSlug || !projectId) return;
|
||||
|
||||
setIsSearching(true);
|
||||
projectService
|
||||
.projectIssuesSearch(workspaceSlug.toString(), projectId.toString(), {
|
||||
search: debouncedSearchTerm,
|
||||
workspace_search: false,
|
||||
})
|
||||
.then((res: ISearchIssueResponse[]) => setIssues(res))
|
||||
.finally(() => setIsSearching(false));
|
||||
}, [debouncedSearchTerm, isOpen, projectId, workspaceSlug]);
|
||||
|
||||
const filteredIssues = issues.filter((issue) => issue.id !== issueId);
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleSubmit = (selectedItem: string) => {
|
||||
if (!selectedItem || selectedItem.length === 0)
|
||||
return setToast({
|
||||
title: "Error",
|
||||
type: TOAST_TYPE.ERROR,
|
||||
});
|
||||
onSubmit(selectedItem);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const issueList =
|
||||
filteredIssues.length > 0 ? (
|
||||
<li className="p-2">
|
||||
{query === "" && (
|
||||
<h2 className="mb-2 mt-4 px-3 text-xs font-semibold text-custom-text-100">Select work item</h2>
|
||||
)}
|
||||
<ul className="text-sm text-custom-text-100">
|
||||
{filteredIssues.map((issue) => {
|
||||
const stateColor = issue.state__color || "";
|
||||
|
||||
return (
|
||||
<Combobox.Option
|
||||
key={issue.id}
|
||||
as="div"
|
||||
value={issue.id}
|
||||
className={({ active, selected }) =>
|
||||
`flex w-full cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-custom-text-200 ${
|
||||
active || selected ? "bg-custom-background-80 text-custom-text-100" : ""
|
||||
} `
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: stateColor,
|
||||
}}
|
||||
/>
|
||||
<span className="flex-shrink-0 text-xs text-custom-text-200">
|
||||
{getProjectById(issue?.project_id)?.identifier}-{issue.sequence_id}
|
||||
</span>
|
||||
<span className="text-custom-text-200">{issue.name}</span>
|
||||
</div>
|
||||
</Combobox.Option>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</li>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center px-3 py-8 text-center">
|
||||
{query === "" ? (
|
||||
<SimpleEmptyState title={t("issue_relation.empty_state.no_issues.title")} assetPath={issuesResolvedPath} />
|
||||
) : (
|
||||
<SimpleEmptyState title={t("issue_relation.empty_state.search.title")} assetPath={searchResolvedPath} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={React.Fragment} afterLeave={() => setQuery("")} appear>
|
||||
<div className="flex flex-wrap items-start">
|
||||
<div className="space-y-1 sm:basis-1/2">
|
||||
<Dialog as="div" className="relative z-30" onClose={handleClose}>
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-30 overflow-y-auto p-4 sm:p-6 md:p-20">
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative mx-auto max-w-2xl transform rounded-lg bg-custom-background-100 shadow-custom-shadow-md transition-all">
|
||||
<Combobox value={value} onChange={handleSubmit}>
|
||||
<div className="relative m-1">
|
||||
<Search
|
||||
className="pointer-events-none absolute left-4 top-3.5 h-5 w-5 text-custom-text-100 text-opacity-40"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-custom-text-100 outline-none focus:ring-0 sm:text-sm"
|
||||
placeholder="Search..."
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Combobox.Options
|
||||
static
|
||||
className="max-h-80 scroll-py-2 divide-y divide-custom-border-200 overflow-y-auto"
|
||||
>
|
||||
{isSearching ? (
|
||||
<Loader className="space-y-3 p-3">
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
</Loader>
|
||||
) : (
|
||||
<>{issueList}</>
|
||||
)}
|
||||
</Combobox.Options>
|
||||
</Combobox>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
||||
85
apps/web/core/components/inbox/modals/snooze-issue-modal.tsx
Normal file
85
apps/web/core/components/inbox/modals/snooze-issue-modal.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
"use client";
|
||||
|
||||
import type { FC } from "react";
|
||||
import { Fragment, useState } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// ui
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { Calendar } from "@plane/propel/calendar";
|
||||
|
||||
export type InboxIssueSnoozeModalProps = {
|
||||
isOpen: boolean;
|
||||
value: Date | undefined;
|
||||
onConfirm: (value: Date) => void;
|
||||
handleClose: () => void;
|
||||
};
|
||||
|
||||
export const InboxIssueSnoozeModal: FC<InboxIssueSnoozeModalProps> = (props) => {
|
||||
const { isOpen, handleClose, value, onConfirm } = props;
|
||||
// states
|
||||
const [date, setDate] = useState(value || new Date());
|
||||
//hooks
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
<div className="fixed inset-0 z-20 flex w-full justify-center overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
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 flex transform rounded-lg bg-custom-background-100 px-5 py-8 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
|
||||
<div className="flex h-full w-full flex-col gap-y-1">
|
||||
<Calendar
|
||||
className="rounded-md border border-custom-border-200 p-3"
|
||||
captionLayout="dropdown"
|
||||
selected={date ? new Date(date) : undefined}
|
||||
defaultMonth={date ? new Date(date) : undefined}
|
||||
onSelect={(date: Date | undefined) => {
|
||||
if (!date) return;
|
||||
setDate(date);
|
||||
}}
|
||||
mode="single"
|
||||
disabled={[
|
||||
{
|
||||
before: new Date(),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
close();
|
||||
onConfirm(date);
|
||||
}}
|
||||
>
|
||||
{t("inbox_issue.actions.snooze")}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user