diff --git a/web/components/api-token/delete-token-modal.tsx b/web/components/api-token/delete-token-modal.tsx index 4c511de4a..97fa0b55f 100644 --- a/web/components/api-token/delete-token-modal.tsx +++ b/web/components/api-token/delete-token-modal.tsx @@ -1,15 +1,16 @@ -import { useState, Fragment, FC } from "react"; +import { useState, FC } from "react"; import { useRouter } from "next/router"; import { mutate } from "swr"; -import { Dialog, Transition } from "@headlessui/react"; -import { IApiToken } from "@plane/types"; -// services -import { Button, TOAST_TYPE, setToast } from "@plane/ui"; -import { API_TOKENS_LIST } from "@/constants/fetch-keys"; -import { APITokenService } from "@/services/api_token.service"; -// ui // types +import { IApiToken } from "@plane/types"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; +// components +import { AlertModalCore } from "@/components/core"; // fetch-keys +import { API_TOKENS_LIST } from "@/constants/fetch-keys"; +// services +import { APITokenService } from "@/services/api_token.service"; type Props = { isOpen: boolean; @@ -32,12 +33,12 @@ export const DeleteApiTokenModal: FC = (props) => { setDeleteLoading(false); }; - const handleDeletion = () => { + const handleDeletion = async () => { if (!workspaceSlug) return; setDeleteLoading(true); - apiTokenService + await apiTokenService .deleteApiToken(workspaceSlug.toString(), tokenId) .then(() => { setToast({ @@ -65,58 +66,17 @@ export const DeleteApiTokenModal: FC = (props) => { }; return ( - - - -
- - -
-
- - -
-
-

- Are you sure you want to delete the token? -

-
- -

- Any application using this token will no longer have the access to Plane data. This action cannot - be undone. -

-
-
- - -
-
-
-
-
-
-
-
+ + Any application using this token will no longer have the access to Plane data. This action cannot be undone. + + } + /> ); }; diff --git a/web/components/api-token/modal/create-token-modal.tsx b/web/components/api-token/modal/create-token-modal.tsx index 966da76e8..19a523439 100644 --- a/web/components/api-token/modal/create-token-modal.tsx +++ b/web/components/api-token/modal/create-token-modal.tsx @@ -1,21 +1,20 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; import { mutate } from "swr"; -import { Dialog, Transition } from "@headlessui/react"; +// types import { IApiToken } from "@plane/types"; -// services +// ui import { TOAST_TYPE, setToast } from "@plane/ui"; - +// components import { CreateApiTokenForm, GeneratedTokenDetails } from "@/components/api-token"; +import { EModalPosition, EModalWidth, ModalCore } from "@/components/core"; +// fetch-keys import { API_TOKENS_LIST } from "@/constants/fetch-keys"; +// helpers import { renderFormattedDate } from "@/helpers/date-time.helper"; import { csvDownload } from "@/helpers/download.helper"; +// services import { APITokenService } from "@/services/api_token.service"; -// ui -// components -// helpers -// types -// fetch-keys type Props = { isOpen: boolean; @@ -86,47 +85,17 @@ export const CreateApiTokenModal: React.FC = (props) => { }; return ( - - {}}> - -
- - -
-
- - - {generatedToken ? ( - - ) : ( - setNeverExpires((prevData) => !prevData)} - onSubmit={handleCreateToken} - /> - )} - - -
-
-
-
+ {}} position={EModalPosition.TOP} width={EModalWidth.XXL}> + {generatedToken ? ( + + ) : ( + setNeverExpires((prevData) => !prevData)} + onSubmit={handleCreateToken} + /> + )} + ); }; diff --git a/web/components/api-token/modal/form.tsx b/web/components/api-token/modal/form.tsx index 7ba0ace71..b22f37b6a 100644 --- a/web/components/api-token/modal/form.tsx +++ b/web/components/api-token/modal/form.tsx @@ -2,13 +2,15 @@ import { useState } from "react"; import { add } from "date-fns"; import { Controller, useForm } from "react-hook-form"; import { Calendar } from "lucide-react"; +// types import { IApiToken } from "@plane/types"; // ui import { Button, CustomSelect, Input, TextArea, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; +// components import { DateDropdown } from "@/components/dropdowns"; // helpers +import { cn } from "@/helpers/common.helper"; import { renderFormattedDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; -// types type Props = { handleClose: () => void; @@ -106,13 +108,14 @@ export const CreateApiTokenForm: React.FC = (props) => { const today = new Date(); const tomorrow = add(today, { days: 1 }); + const expiredAt = watch("expired_at"); return (
-
-

Create token

+
+

Create token

-
+
= (props) => { value={value} onChange={onChange} hasError={Boolean(errors.label)} - placeholder="Token title" - className="w-full text-sm font-medium" + placeholder="Title" + className="w-full text-base" /> )} /> @@ -145,8 +148,8 @@ export const CreateApiTokenForm: React.FC = (props) => { value={value} onChange={onChange} hasError={Boolean(errors.description)} - placeholder="Token description" - className="min-h-24 w-full text-sm" + placeholder="Description" + className="w-full text-base resize-none min-h-24" /> )} /> @@ -162,9 +165,12 @@ export const CreateApiTokenForm: React.FC = (props) => { {value === "custom" @@ -188,33 +194,35 @@ export const CreateApiTokenForm: React.FC = (props) => { ); }} /> - {watch("expired_at") === "custom" && ( - setCustomDate(date)} - minDate={tomorrow} - icon={} - buttonVariant="border-with-text" - placeholder="Set date" - disabled={neverExpires} - /> + {expiredAt === "custom" && ( +
+ setCustomDate(date)} + minDate={tomorrow} + icon={} + buttonVariant="border-with-text" + placeholder="Set date" + disabled={neverExpires} + /> +
)}
{!neverExpires && ( - {watch("expired_at") === "custom" + {expiredAt === "custom" ? customDate ? `Expires ${renderFormattedDate(customDate)}` : null - : watch("expired_at") - ? `Expires ${getExpiryDate(watch("expired_at") ?? "")}` + : expiredAt + ? `Expires ${getExpiryDate(expiredAt ?? "")}` : null} )}
-
+
{}} size="sm" /> @@ -223,10 +231,10 @@ export const CreateApiTokenForm: React.FC = (props) => {
diff --git a/web/components/api-token/modal/generated-token-details.tsx b/web/components/api-token/modal/generated-token-details.tsx index 81e5963a3..88d73e1f3 100644 --- a/web/components/api-token/modal/generated-token-details.tsx +++ b/web/components/api-token/modal/generated-token-details.tsx @@ -28,7 +28,7 @@ export const GeneratedTokenDetails: React.FC = (props) => { }; return ( -
+

Key created

diff --git a/web/components/core/modals/alert-modal.tsx b/web/components/core/modals/alert-modal.tsx new file mode 100644 index 000000000..fbc5e2503 --- /dev/null +++ b/web/components/core/modals/alert-modal.tsx @@ -0,0 +1,90 @@ +import { AlertTriangle, LucideIcon } from "lucide-react"; +// ui +import { Button, TButtonVariant } from "@plane/ui"; +// components +import { EModalPosition, EModalWidth, ModalCore } from "@/components/core"; +// helpers +import { cn } from "@/helpers/common.helper"; + +export type TModalVariant = "danger"; + +type Props = { + content: React.ReactNode | string; + handleClose: () => void; + handleSubmit: () => Promise; + hideIcon?: boolean; + isDeleting: boolean; + isOpen: boolean; + position?: EModalPosition; + primaryButtonText?: { + loading: string; + default: string; + }; + secondaryButtonText?: string; + title: string; + variant?: TModalVariant; + width?: EModalWidth; +}; + +const VARIANT_ICONS: Record = { + danger: AlertTriangle, +}; + +const BUTTON_VARIANTS: Record = { + danger: "danger", +}; + +const VARIANT_CLASSES: Record = { + danger: "bg-red-500/20 text-red-500", +}; + +export const AlertModalCore: React.FC = (props) => { + const { + content, + handleClose, + handleSubmit, + hideIcon = false, + isDeleting, + isOpen, + position = EModalPosition.CENTER, + primaryButtonText = { + loading: "Deleting", + default: "Delete", + }, + secondaryButtonText = "Cancel", + title, + variant = "danger", + width = EModalWidth.XL, + } = props; + + const Icon = VARIANT_ICONS[variant]; + + return ( + +

+ {!hideIcon && ( + + + )} +
+

{title}

+

{content}

+
+
+
+ + +
+ + ); +}; diff --git a/web/components/core/modals/index.ts b/web/components/core/modals/index.ts index a95c22114..71068ca17 100644 --- a/web/components/core/modals/index.ts +++ b/web/components/core/modals/index.ts @@ -1,7 +1,9 @@ +export * from "./alert-modal"; export * from "./bulk-delete-issues-modal"; export * from "./existing-issues-list-modal"; export * from "./gpt-assistant-popover"; export * from "./link-modal"; +export * from "./modal-core"; export * from "./user-image-upload-modal"; export * from "./workspace-image-upload-modal"; export * from "./issue-search-modal-empty-state"; diff --git a/web/components/core/modals/modal-core.tsx b/web/components/core/modals/modal-core.tsx new file mode 100644 index 000000000..5d24df01c --- /dev/null +++ b/web/components/core/modals/modal-core.tsx @@ -0,0 +1,68 @@ +import { Fragment } from "react"; +import { Dialog, Transition } from "@headlessui/react"; +// helpers +import { cn } from "@/helpers/common.helper"; + +export enum EModalPosition { + TOP = "flex items-center justify-center text-center mx-4 my-10 md:my-20", + CENTER = "flex items-end sm:items-center justify-center p-4 min-h-full", +} + +export enum EModalWidth { + XL = "sm:max-w-xl", + XXL = "sm:max-w-2xl", + XXXL = "sm:max-w-3xl", + XXXXL = "sm:max-w-4xl", +} + +type Props = { + children: React.ReactNode; + handleClose: () => void; + isOpen: boolean; + position?: EModalPosition; + width?: EModalWidth; +}; +export const ModalCore: React.FC = (props) => { + const { children, handleClose, isOpen, position = EModalPosition.CENTER, width = EModalWidth.XXL } = props; + + return ( + + + +
+ + +
+
+ + + {children} + + +
+
+
+
+ ); +}; diff --git a/web/components/cycles/delete-modal.tsx b/web/components/cycles/delete-modal.tsx index 5b9eb53cd..728041f5d 100644 --- a/web/components/cycles/delete-modal.tsx +++ b/web/components/cycles/delete-modal.tsx @@ -1,16 +1,16 @@ -import { Fragment, useState } from "react"; +import { useState } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { AlertTriangle } from "lucide-react"; -import { Dialog, Transition } from "@headlessui/react"; -import { ICycle } from "@plane/types"; -// hooks -import { Button, TOAST_TYPE, setToast } from "@plane/ui"; -import { CYCLE_DELETED } from "@/constants/event-tracker"; -import { useEventTracker, useCycle } from "@/hooks/store"; -// components // types +import { ICycle } from "@plane/types"; +// ui +import { TOAST_TYPE, setToast } from "@plane/ui"; +// components +import { AlertModalCore } from "@/components/core"; // constants +import { CYCLE_DELETED } from "@/constants/event-tracker"; +// hooks +import { useEventTracker, useCycle } from "@/hooks/store"; interface ICycleDelete { cycle: ICycle; @@ -24,12 +24,12 @@ export const CycleDeleteModal: React.FC = observer((props) => { const { isOpen, handleClose, cycle, workspaceSlug, projectId } = props; // states const [loader, setLoader] = useState(false); - // router - const router = useRouter(); - const { cycleId, peekCycle } = router.query; // store hooks const { captureCycleEvent } = useEventTracker(); const { deleteCycle } = useCycle(); + // router + const router = useRouter(); + const { cycleId, peekCycle } = router.query; const formSubmit = async () => { if (!cycle) return; @@ -70,66 +70,19 @@ export const CycleDeleteModal: React.FC = observer((props) => { }; return ( -
-
- - - -
- - -
-
- - -
-
-
- -
-
Delete cycle
-
- -

- Are you sure you want to delete cycle{' "'} - {cycle?.name} - {'"'}? All of the data related to the cycle will be permanently removed. This action cannot be - undone. -

-
-
- - - -
-
-
-
-
-
-
-
-
-
+ + Are you sure you want to delete cycle{' "'} + {cycle?.name} + {'"'}? All of the data related to the cycle will be permanently removed. This action cannot be undone. + + } + /> ); }); diff --git a/web/components/cycles/form.tsx b/web/components/cycles/form.tsx index b8b34fe3b..936dbb214 100644 --- a/web/components/cycles/form.tsx +++ b/web/components/cycles/form.tsx @@ -1,12 +1,12 @@ import { useEffect } from "react"; - import { Controller, useForm } from "react-hook-form"; +// types import { ICycle } from "@plane/types"; - +// ui import { Button, Input, TextArea } from "@plane/ui"; - +// components import { DateRangeDropdown, ProjectDropdown } from "@/components/dropdowns"; - +// helpers import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; import { shouldRenderProject } from "@/helpers/project.helper"; @@ -53,121 +53,119 @@ export const CycleForm: React.FC = (props) => { return ( handleFormSubmit(formData, dirtyFields))}> -
+
{!status && ( ( - { - onChange(val); - setActiveProject(val); - }} - buttonVariant="background-with-text" - renderCondition={(project) => shouldRenderProject(project)} - tabIndex={7} - /> +
+ { + onChange(val); + setActiveProject(val); + }} + buttonVariant="border-with-text" + renderCondition={(project) => shouldRenderProject(project)} + tabIndex={7} + /> +
)} /> )} -

{status ? "Update" : "New"} Cycle

+

{status ? "Update" : "Create"} Cycle

-
-
- ( - - )} - /> - {errors?.name?.message} -
-
- ( -