mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
[WEB-1401] chore: toast refactor in space app. (#4546)
* [WEB-1401] chore: toast refactor in space app. * fix: build errors in space app.
This commit is contained in:
parent
1912f6948c
commit
b084844565
@ -6,6 +6,7 @@ import { InstanceProvider } from "@/lib/instance-provider";
|
|||||||
import { StoreProvider } from "@/lib/store-provider";
|
import { StoreProvider } from "@/lib/store-provider";
|
||||||
// styles
|
// styles
|
||||||
import "@/styles/globals.css";
|
import "@/styles/globals.css";
|
||||||
|
import { ToastProvider } from "@/lib/toast-provider";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Plane Deploy | Make your Plane boards public with one-click",
|
title: "Plane Deploy | Make your Plane boards public with one-click",
|
||||||
@ -34,7 +35,9 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<StoreProvider>
|
<StoreProvider>
|
||||||
<InstanceProvider>{children}</InstanceProvider>
|
<ToastProvider>
|
||||||
|
<InstanceProvider>{children}</InstanceProvider>
|
||||||
|
</ToastProvider>
|
||||||
</StoreProvider>
|
</StoreProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import React, { useRef } from "react";
|
import React, { useRef } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useForm, Controller } from "react-hook-form";
|
import { useForm, Controller } from "react-hook-form";
|
||||||
// components
|
// editor
|
||||||
import { EditorRefApi } from "@plane/lite-text-editor";
|
import { EditorRefApi } from "@plane/lite-text-editor";
|
||||||
|
// ui
|
||||||
|
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||||
|
// editor components
|
||||||
import { LiteTextEditor } from "@/components/editor/lite-text-editor";
|
import { LiteTextEditor } from "@/components/editor/lite-text-editor";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssueDetails, useProject, useUser } from "@/hooks/store";
|
import { useIssueDetails, useProject, useUser } from "@/hooks/store";
|
||||||
import useToast from "@/hooks/use-toast";
|
|
||||||
// types
|
// types
|
||||||
import { Comment } from "@/types/issue";
|
import { Comment } from "@/types/issue";
|
||||||
|
|
||||||
@ -39,8 +41,6 @@ export const AddComment: React.FC<Props> = observer((props) => {
|
|||||||
formState: { isSubmitting },
|
formState: { isSubmitting },
|
||||||
reset,
|
reset,
|
||||||
} = useForm<Comment>({ defaultValues });
|
} = useForm<Comment>({ defaultValues });
|
||||||
// toast alert
|
|
||||||
const { setToastAlert } = useToast();
|
|
||||||
|
|
||||||
const onSubmit = async (formData: Comment) => {
|
const onSubmit = async (formData: Comment) => {
|
||||||
if (!workspaceSlug || !projectId || !issueId || isSubmitting || !formData.comment_html) return;
|
if (!workspaceSlug || !projectId || !issueId || isSubmitting || !formData.comment_html) return;
|
||||||
@ -51,8 +51,8 @@ export const AddComment: React.FC<Props> = observer((props) => {
|
|||||||
editorRef.current?.clearEditor();
|
editorRef.current?.clearEditor();
|
||||||
})
|
})
|
||||||
.catch(() =>
|
.catch(() =>
|
||||||
setToastAlert({
|
setToast({
|
||||||
type: "error",
|
type: TOAST_TYPE.ERROR,
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: "Comment could not be posted. Please try again.",
|
message: "Comment could not be posted. Please try again.",
|
||||||
})
|
})
|
||||||
|
@ -3,17 +3,15 @@ import { observer } from "mobx-react-lite";
|
|||||||
import { MoveRight } from "lucide-react";
|
import { MoveRight } from "lucide-react";
|
||||||
import { Listbox, Transition } from "@headlessui/react";
|
import { Listbox, Transition } from "@headlessui/react";
|
||||||
// ui
|
// ui
|
||||||
|
import { setToast, TOAST_TYPE } from "@plane/ui";
|
||||||
import { Icon } from "@/components/ui";
|
import { Icon } from "@/components/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { copyTextToClipboard } from "@/helpers/string.helper";
|
import { copyTextToClipboard } from "@/helpers/string.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssueDetails } from "@/hooks/store";
|
import { useIssueDetails } from "@/hooks/store";
|
||||||
import useClipboardWritePermission from "@/hooks/use-clipboard-write-permission";
|
import useClipboardWritePermission from "@/hooks/use-clipboard-write-permission";
|
||||||
import useToast from "@/hooks/use-toast";
|
|
||||||
// store
|
|
||||||
import { IPeekMode } from "@/store/issue-detail.store";
|
|
||||||
// types
|
// types
|
||||||
import { IIssue } from "@/types/issue";
|
import { IIssue, IPeekMode } from "@/types/issue";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
@ -44,14 +42,12 @@ export const PeekOverviewHeader: React.FC<Props> = observer((props) => {
|
|||||||
const { peekMode, setPeekMode } = useIssueDetails();
|
const { peekMode, setPeekMode } = useIssueDetails();
|
||||||
const isClipboardWriteAllowed = useClipboardWritePermission();
|
const isClipboardWriteAllowed = useClipboardWritePermission();
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
|
||||||
|
|
||||||
const handleCopyLink = () => {
|
const handleCopyLink = () => {
|
||||||
const urlToCopy = window.location.href;
|
const urlToCopy = window.location.href;
|
||||||
|
|
||||||
copyTextToClipboard(urlToCopy).then(() => {
|
copyTextToClipboard(urlToCopy).then(() => {
|
||||||
setToastAlert({
|
setToast({
|
||||||
type: "success",
|
type: TOAST_TYPE.INFO,
|
||||||
title: "Link copied!",
|
title: "Link copied!",
|
||||||
message: "Issue link copied to clipboard",
|
message: "Issue link copied to clipboard",
|
||||||
});
|
});
|
||||||
|
@ -1,17 +1,15 @@
|
|||||||
// hooks
|
|
||||||
// ui
|
// ui
|
||||||
import { StateGroupIcon } from "@plane/ui";
|
import { StateGroupIcon, TOAST_TYPE, setToast } from "@plane/ui";
|
||||||
// icons
|
// icons
|
||||||
import { Icon } from "@/components/ui";
|
import { Icon } from "@/components/ui";
|
||||||
// helpers
|
// constants
|
||||||
import { issueGroupFilter, issuePriorityFilter } from "@/constants/issue";
|
import { issueGroupFilter, issuePriorityFilter } from "@/constants/issue";
|
||||||
|
// helpers
|
||||||
import { renderFullDate } from "@/helpers/date-time.helper";
|
import { renderFullDate } from "@/helpers/date-time.helper";
|
||||||
import { copyTextToClipboard, addSpaceIfCamelCase } from "@/helpers/string.helper";
|
import { copyTextToClipboard, addSpaceIfCamelCase } from "@/helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { IPeekMode } from "@/store/issue-detail.store";
|
import { IIssue, IPeekMode } from "@/types/issue";
|
||||||
// constants
|
// components
|
||||||
import useToast from "hooks/use-toast";
|
|
||||||
import { IIssue } from "types/issue";
|
|
||||||
import { dueDateIconDetails } from "../board-views/block-due-date";
|
import { dueDateIconDetails } from "../board-views/block-due-date";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -20,8 +18,6 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const PeekOverviewIssueProperties: React.FC<Props> = ({ issueDetails, mode }) => {
|
export const PeekOverviewIssueProperties: React.FC<Props> = ({ issueDetails, mode }) => {
|
||||||
const { setToastAlert } = useToast();
|
|
||||||
|
|
||||||
const state = issueDetails.state_detail;
|
const state = issueDetails.state_detail;
|
||||||
const stateGroup = issueGroupFilter(state.group);
|
const stateGroup = issueGroupFilter(state.group);
|
||||||
|
|
||||||
@ -33,8 +29,8 @@ export const PeekOverviewIssueProperties: React.FC<Props> = ({ issueDetails, mod
|
|||||||
const urlToCopy = window.location.href;
|
const urlToCopy = window.location.href;
|
||||||
|
|
||||||
copyTextToClipboard(urlToCopy).then(() => {
|
copyTextToClipboard(urlToCopy).then(() => {
|
||||||
setToastAlert({
|
setToast({
|
||||||
type: "success",
|
type: TOAST_TYPE.INFO,
|
||||||
title: "Link copied!",
|
title: "Link copied!",
|
||||||
message: "Issue link copied to clipboard",
|
message: "Issue link copied to clipboard",
|
||||||
});
|
});
|
||||||
|
@ -1,61 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { AlertTriangle, CheckCircle, Info, X, XCircle } from "lucide-react";
|
|
||||||
// hooks
|
|
||||||
import useToast from "hooks/use-toast";
|
|
||||||
// icons
|
|
||||||
|
|
||||||
const ToastAlerts = () => {
|
|
||||||
const { alerts, removeAlert } = useToast();
|
|
||||||
|
|
||||||
if (!alerts) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="pointer-events-none fixed right-5 top-5 z-50 h-full w-80 space-y-5 overflow-hidden">
|
|
||||||
{alerts.map((alert) => (
|
|
||||||
<div className="relative overflow-hidden rounded-md text-white" key={alert.id}>
|
|
||||||
<div className="absolute right-1 top-1">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="pointer-events-auto inline-flex rounded-md p-1.5 focus:outline-none focus:ring-2 focus:ring-offset-2"
|
|
||||||
onClick={() => removeAlert(alert.id)}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Dismiss</span>
|
|
||||||
<X className="h-5 w-5" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={`px-2 py-4 ${
|
|
||||||
alert.type === "success"
|
|
||||||
? "bg-[#06d6a0]"
|
|
||||||
: alert.type === "error"
|
|
||||||
? "bg-[#ef476f]"
|
|
||||||
: alert.type === "warning"
|
|
||||||
? "bg-[#e98601]"
|
|
||||||
: "bg-[#1B9aaa]"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-x-3">
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
{alert.type === "success" ? (
|
|
||||||
<CheckCircle className="h-8 w-8" aria-hidden="true" />
|
|
||||||
) : alert.type === "error" ? (
|
|
||||||
<XCircle className="h-8 w-8" />
|
|
||||||
) : alert.type === "warning" ? (
|
|
||||||
<AlertTriangle className="h-8 w-8" aria-hidden="true" />
|
|
||||||
) : (
|
|
||||||
<Info className="h-8 w-8" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="font-semibold">{alert.title}</p>
|
|
||||||
{alert.message && <p className="mt-1 text-xs">{alert.message}</p>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ToastAlerts;
|
|
@ -1,97 +0,0 @@
|
|||||||
import React, { createContext, useCallback, useReducer } from "react";
|
|
||||||
// uuid
|
|
||||||
import { v4 as uuid } from "uuid";
|
|
||||||
// components
|
|
||||||
import ToastAlert from "@/components/ui/toast-alert";
|
|
||||||
|
|
||||||
export const toastContext = createContext<ContextType>({} as ContextType);
|
|
||||||
|
|
||||||
// types
|
|
||||||
type ToastAlert = {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
message?: string;
|
|
||||||
type: "success" | "error" | "warning" | "info";
|
|
||||||
};
|
|
||||||
|
|
||||||
type ReducerActionType = {
|
|
||||||
type: "SET_TOAST_ALERT" | "REMOVE_TOAST_ALERT";
|
|
||||||
payload: ToastAlert;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ContextType = {
|
|
||||||
alerts?: ToastAlert[];
|
|
||||||
removeAlert: (id: string) => void;
|
|
||||||
setToastAlert: (data: {
|
|
||||||
title: string;
|
|
||||||
type?: "success" | "error" | "warning" | "info" | undefined;
|
|
||||||
message?: string | undefined;
|
|
||||||
}) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
type StateType = {
|
|
||||||
toastAlerts?: ToastAlert[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type ReducerFunctionType = (state: StateType, action: ReducerActionType) => StateType;
|
|
||||||
|
|
||||||
export const initialState: StateType = {
|
|
||||||
toastAlerts: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const reducer: ReducerFunctionType = (state, action) => {
|
|
||||||
const { type, payload } = action;
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case "SET_TOAST_ALERT":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
toastAlerts: [...(state.toastAlerts ?? []), payload],
|
|
||||||
};
|
|
||||||
|
|
||||||
case "REMOVE_TOAST_ALERT":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
toastAlerts: state.toastAlerts?.filter((toastAlert) => toastAlert.id !== payload.id),
|
|
||||||
};
|
|
||||||
|
|
||||||
default: {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ToastContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
||||||
const [state, dispatch] = useReducer(reducer, initialState);
|
|
||||||
|
|
||||||
const removeAlert = useCallback((id: string) => {
|
|
||||||
dispatch({
|
|
||||||
type: "REMOVE_TOAST_ALERT",
|
|
||||||
payload: { id, title: "", message: "", type: "success" },
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const setToastAlert = useCallback(
|
|
||||||
(data: { title: string; type?: "success" | "error" | "warning" | "info"; message?: string }) => {
|
|
||||||
const id = uuid();
|
|
||||||
const { title, type, message } = data;
|
|
||||||
dispatch({
|
|
||||||
type: "SET_TOAST_ALERT",
|
|
||||||
payload: { id, title, message, type: type ?? "success" },
|
|
||||||
});
|
|
||||||
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
removeAlert(id);
|
|
||||||
clearTimeout(timer);
|
|
||||||
}, 3000);
|
|
||||||
},
|
|
||||||
[removeAlert]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<toastContext.Provider value={{ setToastAlert, removeAlert, alerts: state.toastAlerts }}>
|
|
||||||
<ToastAlert />
|
|
||||||
{children}
|
|
||||||
</toastContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
@ -15,3 +15,6 @@ export const GOD_MODE_URL = encodeURI(`${ADMIN_BASE_URL}${ADMIN_BASE_PATH}`);
|
|||||||
export const ASSET_PREFIX = SPACE_BASE_PATH;
|
export const ASSET_PREFIX = SPACE_BASE_PATH;
|
||||||
|
|
||||||
export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
|
export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
|
||||||
|
|
||||||
|
export const resolveGeneralTheme = (resolvedTheme: string | undefined) =>
|
||||||
|
resolvedTheme?.includes("light") ? "light" : resolvedTheme?.includes("dark") ? "dark" : "system";
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
import { useContext } from "react";
|
|
||||||
import { toastContext } from "@/contexts/toast.context";
|
|
||||||
|
|
||||||
const useToast = () => {
|
|
||||||
const toastContextData = useContext(toastContext);
|
|
||||||
return toastContextData;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useToast;
|
|
20
space/lib/toast-provider.tsx
Normal file
20
space/lib/toast-provider.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
import { useTheme } from "next-themes"
|
||||||
|
// ui
|
||||||
|
import { Toast } from "@plane/ui";
|
||||||
|
// helpers
|
||||||
|
import { resolveGeneralTheme } from "@/helpers/common.helper";
|
||||||
|
|
||||||
|
export const ToastProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
// themes
|
||||||
|
const { resolvedTheme } = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Toast theme={resolveGeneralTheme(resolvedTheme)} />
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -5,9 +5,7 @@ import IssueService from "@/services/issue.service";
|
|||||||
// store types
|
// store types
|
||||||
import { RootStore } from "@/store/root.store";
|
import { RootStore } from "@/store/root.store";
|
||||||
// types
|
// types
|
||||||
import { IIssue, IVote } from "@/types/issue";
|
import { IIssue, IPeekMode, IVote } from "@/types/issue";
|
||||||
|
|
||||||
export type IPeekMode = "side" | "modal" | "full";
|
|
||||||
|
|
||||||
export interface IIssueDetailStore {
|
export interface IIssueDetailStore {
|
||||||
loader: boolean;
|
loader: boolean;
|
||||||
|
2
space/types/issue.d.ts
vendored
2
space/types/issue.d.ts
vendored
@ -66,6 +66,8 @@ export interface IIssue {
|
|||||||
votes: IVote[];
|
votes: IVote[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type IPeekMode = "side" | "modal" | "full";
|
||||||
|
|
||||||
export interface IIssueState {
|
export interface IIssueState {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
Loading…
Reference in New Issue
Block a user