feat: init
This commit is contained in:
1
apps/web/core/components/stickies/sticky/index.ts
Normal file
1
apps/web/core/components/stickies/sticky/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
100
apps/web/core/components/stickies/sticky/inputs.tsx
Normal file
100
apps/web/core/components/stickies/sticky/inputs.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
// import dynamic from "next/dynamic";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// plane imports
|
||||
import type { EditorRefApi } from "@plane/editor";
|
||||
import type { TSticky } from "@plane/types";
|
||||
import { cn, isCommentEmpty } from "@plane/utils";
|
||||
import { StickyEditor } from "@/components/editor/sticky-editor";
|
||||
// hooks
|
||||
import { useWorkspace } from "@/hooks/store/use-workspace";
|
||||
|
||||
// const StickyEditor = dynamic(() => import("../../editor/sticky-editor").then((mod) => mod.StickyEditor), {
|
||||
// ssr: false,
|
||||
// });
|
||||
|
||||
type TProps = {
|
||||
stickyData: Partial<TSticky> | undefined;
|
||||
workspaceSlug: string;
|
||||
handleUpdate: (payload: Partial<TSticky>) => void;
|
||||
stickyId: string | undefined;
|
||||
showToolbar?: boolean;
|
||||
handleChange: (data: Partial<TSticky>) => Promise<void>;
|
||||
handleDelete: () => void;
|
||||
};
|
||||
|
||||
export const StickyInput = (props: TProps) => {
|
||||
const { stickyData, workspaceSlug, handleUpdate, stickyId, handleDelete, handleChange, showToolbar } = props;
|
||||
// refs
|
||||
const editorRef = useRef<EditorRefApi>(null);
|
||||
// navigation
|
||||
const pathname = usePathname();
|
||||
// store hooks
|
||||
const { getWorkspaceBySlug } = useWorkspace();
|
||||
// derived values
|
||||
const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id?.toString() ?? "";
|
||||
const isStickiesPage = pathname?.includes("stickies");
|
||||
// form info
|
||||
const { handleSubmit, reset, control } = useForm<TSticky>({
|
||||
defaultValues: {
|
||||
description_html: stickyData?.description_html,
|
||||
},
|
||||
});
|
||||
// handle description update
|
||||
const handleFormSubmit = useCallback(
|
||||
async (formdata: Partial<TSticky>) => {
|
||||
await handleUpdate({
|
||||
description_html: formdata.description_html ?? "<p></p>",
|
||||
});
|
||||
},
|
||||
[handleUpdate]
|
||||
);
|
||||
// reset form values
|
||||
useEffect(() => {
|
||||
if (!stickyId) return;
|
||||
reset({
|
||||
id: stickyId,
|
||||
description_html: stickyData?.description_html?.trim() === "" ? "<p></p>" : stickyData?.description_html,
|
||||
});
|
||||
}, [stickyData, stickyId, reset]);
|
||||
|
||||
return (
|
||||
<div className="flex-1">
|
||||
<Controller
|
||||
name="description_html"
|
||||
control={control}
|
||||
render={({ field: { onChange } }) => (
|
||||
<StickyEditor
|
||||
id={`description-${stickyId}`}
|
||||
initialValue={stickyData?.description_html ?? ""}
|
||||
value={null}
|
||||
workspaceSlug={workspaceSlug}
|
||||
workspaceId={workspaceId}
|
||||
onChange={(_description, description_html) => {
|
||||
onChange(description_html);
|
||||
handleSubmit(handleFormSubmit)();
|
||||
}}
|
||||
placeholder={(_, value) => {
|
||||
const isContentEmpty = isCommentEmpty(value);
|
||||
if (!isContentEmpty) return "";
|
||||
return "Click to type here";
|
||||
}}
|
||||
containerClassName={cn(
|
||||
"w-full min-h-[256px] max-h-[540px] overflow-y-scroll vertical-scrollbar scrollbar-sm p-4 text-base",
|
||||
{
|
||||
"max-h-[588px]": isStickiesPage,
|
||||
}
|
||||
)}
|
||||
uploadFile={async () => ""}
|
||||
showToolbar={showToolbar}
|
||||
parentClassName="border-none p-0"
|
||||
handleDelete={handleDelete}
|
||||
handleColorChange={handleChange}
|
||||
ref={editorRef}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
106
apps/web/core/components/stickies/sticky/root.tsx
Normal file
106
apps/web/core/components/stickies/sticky/root.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { debounce } from "lodash-es";
|
||||
import { observer } from "mobx-react";
|
||||
import { Minimize2 } from "lucide-react";
|
||||
// plane types
|
||||
import type { TSticky } from "@plane/types";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useSticky } from "@/hooks/use-stickies";
|
||||
// components
|
||||
import { STICKY_COLORS_LIST } from "../../editor/sticky-editor/color-palette";
|
||||
import { StickyDeleteModal } from "../delete-modal";
|
||||
import { StickyInput } from "./inputs";
|
||||
import { getRandomStickyColor, useStickyOperations } from "./use-operations";
|
||||
|
||||
type TProps = {
|
||||
onClose?: () => void;
|
||||
workspaceSlug: string;
|
||||
className?: string;
|
||||
stickyId: string | undefined;
|
||||
showToolbar?: boolean;
|
||||
handleLayout?: () => void;
|
||||
};
|
||||
export const StickyNote = observer((props: TProps) => {
|
||||
const { onClose, workspaceSlug, className = "", stickyId, showToolbar, handleLayout } = props;
|
||||
// navigation
|
||||
// const pathName = usePathname();
|
||||
// states
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
// store hooks
|
||||
const { stickies } = useSticky();
|
||||
// sticky operations
|
||||
const { stickyOperations } = useStickyOperations({ workspaceSlug });
|
||||
// derived values
|
||||
const stickyData: Partial<TSticky> = stickyId ? stickies[stickyId] : { background_color: getRandomStickyColor() };
|
||||
// const isStickiesPage = pathName?.includes("stickies");
|
||||
const backgroundColor =
|
||||
STICKY_COLORS_LIST.find((c) => c.key === stickyData?.background_color)?.backgroundColor ||
|
||||
STICKY_COLORS_LIST[0].backgroundColor;
|
||||
|
||||
const handleChange = useCallback(
|
||||
async (payload: Partial<TSticky>) => {
|
||||
if (stickyId) {
|
||||
await stickyOperations.update(stickyId, payload);
|
||||
} else {
|
||||
await stickyOperations.create({
|
||||
...stickyData,
|
||||
...payload,
|
||||
});
|
||||
}
|
||||
},
|
||||
[stickyId, stickyOperations]
|
||||
);
|
||||
|
||||
const debouncedFormSave = useCallback(
|
||||
debounce(async (payload: Partial<TSticky>) => {
|
||||
await handleChange(payload);
|
||||
}, 500),
|
||||
[stickyOperations, stickyData, handleChange]
|
||||
);
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!stickyId) return;
|
||||
onClose?.();
|
||||
stickyOperations.remove(stickyId);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<StickyDeleteModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
handleSubmit={handleDelete}
|
||||
handleClose={() => setIsDeleteModalOpen(false)}
|
||||
/>
|
||||
<div
|
||||
className={cn("w-full h-fit flex flex-col rounded group/sticky overflow-y-scroll", className)}
|
||||
style={{
|
||||
backgroundColor,
|
||||
}}
|
||||
>
|
||||
{/* {isStickiesPage && <StickyItemDragHandle isDragging={false} />}{" "} */}
|
||||
{onClose && (
|
||||
<button type="button" className="flex-shrink-0 flex justify-end p-2.5" onClick={onClose}>
|
||||
<Minimize2 className="size-4" />
|
||||
</button>
|
||||
)}
|
||||
{/* inputs */}
|
||||
<div className="-mt-2">
|
||||
<StickyInput
|
||||
stickyData={stickyData}
|
||||
workspaceSlug={workspaceSlug}
|
||||
handleUpdate={(payload) => {
|
||||
handleLayout?.();
|
||||
debouncedFormSave(payload);
|
||||
}}
|
||||
stickyId={stickyId}
|
||||
handleDelete={() => setIsDeleteModalOpen(true)}
|
||||
handleChange={handleChange}
|
||||
showToolbar={showToolbar}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
"use client";
|
||||
import type { FC } from "react";
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// ui
|
||||
import { DragHandle } from "@plane/ui";
|
||||
// helper
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
type Props = {
|
||||
isDragging: boolean;
|
||||
};
|
||||
|
||||
export const StickyItemDragHandle: FC<Props> = observer((props) => {
|
||||
const { isDragging } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"hidden group-hover/sticky:flex absolute top-3 left-1/2 -translate-x-1/2 items-center justify-center rounded text-custom-sidebar-text-400 cursor-grab mr-2 rotate-90",
|
||||
{
|
||||
"cursor-grabbing": isDragging,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<DragHandle className="bg-transparent" />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
148
apps/web/core/components/stickies/sticky/use-operations.tsx
Normal file
148
apps/web/core/components/stickies/sticky/use-operations.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import { useMemo } from "react";
|
||||
// plane types
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import type { InstructionType, TSticky } from "@plane/types";
|
||||
// plane utils
|
||||
import { isCommentEmpty } from "@plane/utils";
|
||||
// components
|
||||
import { STICKY_COLORS_LIST } from "@/components/editor/sticky-editor/color-palette";
|
||||
// hooks
|
||||
import { useSticky } from "@/hooks/use-stickies";
|
||||
|
||||
export type TOperations = {
|
||||
create: (data?: Partial<TSticky>) => Promise<void>;
|
||||
update: (stickyId: string, data: Partial<TSticky>) => Promise<void>;
|
||||
remove: (stickyId: string) => Promise<void>;
|
||||
updatePosition: (
|
||||
workspaceSlug: string,
|
||||
sourceId: string,
|
||||
droppedId: string,
|
||||
instruction: InstructionType
|
||||
) => Promise<void>;
|
||||
};
|
||||
|
||||
type TProps = {
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const getRandomStickyColor = (): string => {
|
||||
const randomIndex = Math.floor(Math.random() * STICKY_COLORS_LIST.length);
|
||||
return STICKY_COLORS_LIST[randomIndex].key;
|
||||
};
|
||||
|
||||
export const useStickyOperations = (props: TProps) => {
|
||||
const { workspaceSlug } = props;
|
||||
// store hooks
|
||||
const { stickies, getWorkspaceStickyIds, createSticky, updateSticky, deleteSticky, updateStickyPosition } =
|
||||
useSticky();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isValid = (data: Partial<TSticky>) => {
|
||||
if (data.name && data.name.length > 100) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("stickies.toasts.not_updated.title"),
|
||||
message: t("stickies.toasts.errors.wrong_name"),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const stickyOperations: TOperations = useMemo(
|
||||
() => ({
|
||||
create: async (data?: Partial<TSticky>) => {
|
||||
try {
|
||||
const payload: Partial<TSticky> = {
|
||||
background_color: getRandomStickyColor(),
|
||||
...data,
|
||||
};
|
||||
const workspaceStickIds = getWorkspaceStickyIds(workspaceSlug);
|
||||
// check if latest sticky is empty
|
||||
if (workspaceStickIds && workspaceStickIds.length >= 0) {
|
||||
const latestSticky = stickies[workspaceStickIds[0]];
|
||||
if (latestSticky && (!latestSticky.description_html || isCommentEmpty(latestSticky.description_html))) {
|
||||
setToast({
|
||||
message: t("stickies.toasts.errors.already_exists"),
|
||||
type: TOAST_TYPE.WARNING,
|
||||
title: t("stickies.toasts.not_created.title"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!workspaceSlug) throw new Error("Missing required fields");
|
||||
if (!isValid(payload)) return;
|
||||
await createSticky(workspaceSlug, payload);
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: t("stickies.toasts.created.title"),
|
||||
message: t("stickies.toasts.created.message"),
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error in creating sticky:", error);
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("stickies.toasts.not_created.title"),
|
||||
message: error?.data?.error ?? t("stickies.toasts.not_created.message"),
|
||||
});
|
||||
}
|
||||
},
|
||||
update: async (stickyId: string, data: Partial<TSticky>) => {
|
||||
try {
|
||||
if (!workspaceSlug) throw new Error("Missing required fields");
|
||||
if (!isValid(data)) return;
|
||||
await updateSticky(workspaceSlug, stickyId, data);
|
||||
} catch (error) {
|
||||
console.error("Error in updating sticky:", error);
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("stickies.toasts.not_updated.title"),
|
||||
message: t("stickies.toasts.not_updated.message"),
|
||||
});
|
||||
}
|
||||
},
|
||||
remove: async (stickyId: string) => {
|
||||
try {
|
||||
if (!workspaceSlug) throw new Error("Missing required fields");
|
||||
await deleteSticky(workspaceSlug, stickyId);
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: t("stickies.toasts.removed.title"),
|
||||
message: t("stickies.toasts.removed.message"),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error in removing sticky:", error);
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("stickies.toasts.not_removed.title"),
|
||||
message: t("stickies.toasts.not_removed.message"),
|
||||
});
|
||||
}
|
||||
},
|
||||
updatePosition: async (
|
||||
workspaceSlug: string,
|
||||
sourceId: string,
|
||||
droppedId: string,
|
||||
instruction: InstructionType
|
||||
) => {
|
||||
try {
|
||||
if (!workspaceSlug) throw new Error("Missing required fields");
|
||||
await updateStickyPosition(workspaceSlug, sourceId, droppedId, instruction);
|
||||
} catch (error) {
|
||||
console.error("Error in updating sticky position:", error);
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("stickies.toasts.not_updated.title"),
|
||||
message: t("stickies.toasts.not_updated.message"),
|
||||
});
|
||||
}
|
||||
},
|
||||
}),
|
||||
[createSticky, deleteSticky, getWorkspaceStickyIds, stickies, updateSticky, updateStickyPosition, workspaceSlug]
|
||||
);
|
||||
|
||||
return {
|
||||
stickyOperations,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user