Merge branch 'develop' of github.com:makeplane/plane into develop

This commit is contained in:
sriram veeraghanta 2024-04-16 16:41:29 +05:30
commit 247720b0d4
16 changed files with 219 additions and 93 deletions

View File

@ -746,7 +746,7 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
class TransferCycleIssueAPIEndpoint(BaseAPIView): class TransferCycleIssueAPIEndpoint(BaseAPIView):
""" """
This viewset provides `create` actions for transfering the issues into a particular cycle. This viewset provides `create` actions for transferring the issues into a particular cycle.
""" """

View File

@ -19,7 +19,7 @@ interface CustomEditorProps {
uploadFile: UploadImage; uploadFile: UploadImage;
restoreFile: RestoreImage; restoreFile: RestoreImage;
deleteFile: DeleteImage; deleteFile: DeleteImage;
cancelUploadImage?: () => any; cancelUploadImage?: () => void;
initialValue: string; initialValue: string;
editorClassName: string; editorClassName: string;
// undefined when prop is not passed, null if intentionally passed to stop // undefined when prop is not passed, null if intentionally passed to stop
@ -56,7 +56,7 @@ export const useEditor = ({
}: CustomEditorProps) => { }: CustomEditorProps) => {
const editor = useCustomEditor({ const editor = useCustomEditor({
editorProps: { editorProps: {
...CoreEditorProps(uploadFile, editorClassName), ...CoreEditorProps(editorClassName),
...editorProps, ...editorProps,
}, },
extensions: [ extensions: [
@ -69,6 +69,7 @@ export const useEditor = ({
deleteFile, deleteFile,
restoreFile, restoreFile,
cancelUploadImage, cancelUploadImage,
uploadFile,
}, },
placeholder, placeholder,
}), }),
@ -89,19 +90,37 @@ export const useEditor = ({
}, },
}); });
// for syncing swr data on tab refocus etc, can remove it once this is merged const editorRef: MutableRefObject<Editor | null> = useRef(null);
// https://github.com/ueberdosis/tiptap/pull/4453
const [savedSelection, setSavedSelection] = useState<Selection | null>(null);
// Inside your component or hook
const savedSelectionRef = useRef(savedSelection);
// Update the ref whenever savedSelection changes
useEffect(() => {
savedSelectionRef.current = savedSelection;
}, [savedSelection]);
// Effect for syncing SWR data
useEffect(() => { useEffect(() => {
// value is null when intentionally passed where syncing is not yet // value is null when intentionally passed where syncing is not yet
// supported and value is undefined when the data from swr is not populated // supported and value is undefined when the data from swr is not populated
if (value === null || value === undefined) return; if (value === null || value === undefined) return;
if (editor && !editor.isDestroyed) editor?.commands.setContent(value); if (editor && !editor.isDestroyed && !editor.storage.image.uploadInProgress) {
editor.commands.setContent(value);
const currentSavedSelection = savedSelectionRef.current;
if (currentSavedSelection) {
editor.view.focus();
const docLength = editor.state.doc.content.size;
const relativePosition = Math.min(currentSavedSelection.from, docLength - 1);
editor.commands.setTextSelection(relativePosition);
} else {
editor.commands.focus("end");
}
}
}, [editor, value, id]); }, [editor, value, id]);
const editorRef: MutableRefObject<Editor | null> = useRef(null);
const [savedSelection, setSavedSelection] = useState<Selection | null>(null);
useImperativeHandle( useImperativeHandle(
forwardedRef, forwardedRef,
() => ({ () => ({

View File

@ -126,7 +126,7 @@ export const insertImageCommand = (
if (input.files?.length) { if (input.files?.length) {
const file = input.files[0]; const file = input.files[0];
const pos = savedSelection?.anchor ?? editor.view.state.selection.from; const pos = savedSelection?.anchor ?? editor.view.state.selection.from;
startImageUpload(file, editor.view, pos, uploadFile); startImageUpload(editor, file, editor.view, pos, uploadFile);
} }
}; };
input.click(); input.click();

View File

@ -236,7 +236,7 @@ div[data-type="horizontalRule"] {
margin-bottom: 0; margin-bottom: 0;
& > div { & > div {
border-bottom: 1px solid rgb(var(--color-border-200)); border-bottom: 2px solid rgb(var(--color-border-200));
} }
} }

View File

@ -0,0 +1,45 @@
import { Extension } from "@tiptap/core";
import { Plugin, PluginKey } from "prosemirror-state";
import { UploadImage } from "src/types/upload-image";
import { startImageUpload } from "../plugins/upload-image";
export const DropHandlerExtension = (uploadFile: UploadImage) =>
Extension.create({
name: "dropHandler",
priority: 1000,
addProseMirrorPlugins() {
return [
new Plugin({
key: new PluginKey("dropHandler"),
props: {
handlePaste: (view, event) => {
if (event.clipboardData && event.clipboardData.files && event.clipboardData.files[0]) {
event.preventDefault();
const file = event.clipboardData.files[0];
const pos = view.state.selection.from;
startImageUpload(this.editor, file, view, pos, uploadFile);
return true;
}
return false;
},
handleDrop: (view, event, _slice, moved) => {
if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]) {
event.preventDefault();
const file = event.dataTransfer.files[0];
const coordinates = view.posAtCoords({
left: event.clientX,
top: event.clientY,
});
if (coordinates) {
startImageUpload(this.editor, file, view, coordinates.pos - 1, uploadFile);
}
return true;
}
return false;
},
},
}),
];
},
});

View File

@ -28,11 +28,22 @@ export const CustomHorizontalRule = Node.create<HorizontalRuleOptions>({
group: "block", group: "block",
parseHTML() { parseHTML() {
return [{ tag: "hr" }]; return [
{
tag: `div[data-type="${this.name}"]`,
},
{ tag: "hr" },
];
}, },
renderHTML({ HTMLAttributes }) { renderHTML({ HTMLAttributes }) {
return ["hr", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)]; return [
"div",
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
"data-type": this.name,
}),
["div", {}],
];
}, },
addCommands() { addCommands() {

View File

@ -18,7 +18,7 @@ interface ImageNode extends ProseMirrorNode {
const deleteKey = new PluginKey("delete-image"); const deleteKey = new PluginKey("delete-image");
const IMAGE_NODE_TYPE = "image"; const IMAGE_NODE_TYPE = "image";
export const ImageExtension = (deleteImage: DeleteImage, restoreFile: RestoreImage, cancelUploadImage?: () => any) => export const ImageExtension = (deleteImage: DeleteImage, restoreFile: RestoreImage, cancelUploadImage?: () => void) =>
ImageExt.extend({ ImageExt.extend({
addKeyboardShortcuts() { addKeyboardShortcuts() {
return { return {
@ -28,7 +28,7 @@ export const ImageExtension = (deleteImage: DeleteImage, restoreFile: RestoreIma
}, },
addProseMirrorPlugins() { addProseMirrorPlugins() {
return [ return [
UploadImagesPlugin(cancelUploadImage), UploadImagesPlugin(this.editor, cancelUploadImage),
new Plugin({ new Plugin({
key: deleteKey, key: deleteKey,
appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => { appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => {
@ -124,6 +124,7 @@ export const ImageExtension = (deleteImage: DeleteImage, restoreFile: RestoreIma
addStorage() { addStorage() {
return { return {
images: new Map<string, boolean>(), images: new Map<string, boolean>(),
uploadInProgress: false,
}; };
}, },

View File

@ -29,6 +29,8 @@ import { CustomCodeInlineExtension } from "src/ui/extensions/code-inline";
import { CustomTypographyExtension } from "src/ui/extensions/typography"; import { CustomTypographyExtension } from "src/ui/extensions/typography";
import { CustomHorizontalRule } from "src/ui/extensions/horizontal-rule/horizontal-rule"; import { CustomHorizontalRule } from "src/ui/extensions/horizontal-rule/horizontal-rule";
import { CustomCodeMarkPlugin } from "./custom-code-inline/inline-code-plugin"; import { CustomCodeMarkPlugin } from "./custom-code-inline/inline-code-plugin";
import { UploadImage } from "src/types/upload-image";
import { DropHandlerExtension } from "./drop";
type TArguments = { type TArguments = {
mentionConfig: { mentionConfig: {
@ -38,14 +40,15 @@ type TArguments = {
fileConfig: { fileConfig: {
deleteFile: DeleteImage; deleteFile: DeleteImage;
restoreFile: RestoreImage; restoreFile: RestoreImage;
cancelUploadImage?: () => any; cancelUploadImage?: () => void;
uploadFile: UploadImage;
}; };
placeholder?: string | ((isFocused: boolean) => string); placeholder?: string | ((isFocused: boolean) => string);
}; };
export const CoreEditorExtensions = ({ export const CoreEditorExtensions = ({
mentionConfig, mentionConfig,
fileConfig: { deleteFile, restoreFile, cancelUploadImage }, fileConfig: { deleteFile, restoreFile, cancelUploadImage, uploadFile },
placeholder, placeholder,
}: TArguments) => [ }: TArguments) => [
StarterKit.configure({ StarterKit.configure({
@ -73,10 +76,8 @@ export const CoreEditorExtensions = ({
width: 1, width: 1,
}, },
}), }),
// BulletList,
// OrderedList,
// ListItem,
CustomQuoteExtension, CustomQuoteExtension,
DropHandlerExtension(uploadFile),
CustomHorizontalRule.configure({ CustomHorizontalRule.configure({
HTMLAttributes: { HTMLAttributes: {
class: "my-4 border-custom-border-400", class: "my-4 border-custom-border-400",

View File

@ -1,12 +1,22 @@
import { Editor } from "@tiptap/core";
import { EditorState, Plugin, PluginKey } from "@tiptap/pm/state"; import { EditorState, Plugin, PluginKey } from "@tiptap/pm/state";
import { Decoration, DecorationSet, EditorView } from "@tiptap/pm/view"; import { Decoration, DecorationSet, EditorView } from "@tiptap/pm/view";
import { UploadImage } from "src/types/upload-image"; import { UploadImage } from "src/types/upload-image";
const uploadKey = new PluginKey("upload-image"); const uploadKey = new PluginKey("upload-image");
export const UploadImagesPlugin = (cancelUploadImage?: () => any) => export const UploadImagesPlugin = (editor: Editor, cancelUploadImage?: () => void) => {
new Plugin({ let currentView: EditorView | null = null;
return new Plugin({
key: uploadKey, key: uploadKey,
view(editorView) {
currentView = editorView;
return {
destroy() {
currentView = null;
},
};
},
state: { state: {
init() { init() {
return DecorationSet.empty; return DecorationSet.empty;
@ -27,13 +37,17 @@ export const UploadImagesPlugin = (cancelUploadImage?: () => any) =>
// Create cancel button // Create cancel button
const cancelButton = document.createElement("button"); const cancelButton = document.createElement("button");
cancelButton.type = "button";
cancelButton.style.position = "absolute"; cancelButton.style.position = "absolute";
cancelButton.style.right = "3px"; cancelButton.style.right = "3px";
cancelButton.style.top = "3px"; cancelButton.style.top = "3px";
cancelButton.setAttribute("class", "opacity-90 rounded-lg"); cancelButton.setAttribute("class", "opacity-90 rounded-lg");
cancelButton.onclick = () => { cancelButton.onclick = () => {
cancelUploadImage?.(); if (currentView) {
cancelUploadImage?.();
removePlaceholder(editor, currentView, id);
}
}; };
// Create an SVG element from the SVG string // Create an SVG element from the SVG string
@ -59,6 +73,7 @@ export const UploadImagesPlugin = (cancelUploadImage?: () => any) =>
}, },
}, },
}); });
};
function findPlaceholder(state: EditorState, id: {}) { function findPlaceholder(state: EditorState, id: {}) {
const decos = uploadKey.getState(state); const decos = uploadKey.getState(state);
@ -66,26 +81,38 @@ function findPlaceholder(state: EditorState, id: {}) {
return found.length ? found[0].from : null; return found.length ? found[0].from : null;
} }
const removePlaceholder = (view: EditorView, id: {}) => { const removePlaceholder = (editor: Editor, view: EditorView, id: {}) => {
const removePlaceholderTr = view.state.tr.setMeta(uploadKey, { const removePlaceholderTr = view.state.tr.setMeta(uploadKey, {
remove: { id }, remove: { id },
}); });
view.dispatch(removePlaceholderTr); view.dispatch(removePlaceholderTr);
editor.storage.image.uploadInProgress = false;
}; };
export async function startImageUpload(file: File, view: EditorView, pos: number, uploadFile: UploadImage) { export async function startImageUpload(
editor: Editor,
file: File,
view: EditorView,
pos: number,
uploadFile: UploadImage
) {
editor.storage.image.uploadInProgress = true;
if (!file) { if (!file) {
alert("No file selected. Please select a file to upload."); alert("No file selected. Please select a file to upload.");
editor.storage.image.uploadInProgress = false;
return; return;
} }
if (!file.type.includes("image/")) { if (!file.type.includes("image/")) {
alert("Invalid file type. Please select an image file."); alert("Invalid file type. Please select an image file.");
editor.storage.image.uploadInProgress = false;
return; return;
} }
if (file.size > 5 * 1024 * 1024) { if (file.size > 5 * 1024 * 1024) {
alert("File size too large. Please select a file smaller than 5MB."); alert("File size too large. Please select a file smaller than 5MB.");
editor.storage.image.uploadInProgress = false;
return; return;
} }
@ -110,7 +137,7 @@ export async function startImageUpload(file: File, view: EditorView, pos: number
// Handle FileReader errors // Handle FileReader errors
reader.onerror = (error) => { reader.onerror = (error) => {
console.error("FileReader error: ", error); console.error("FileReader error: ", error);
removePlaceholder(view, id); removePlaceholder(editor, view, id);
return; return;
}; };
@ -121,7 +148,10 @@ export async function startImageUpload(file: File, view: EditorView, pos: number
const { schema } = view.state; const { schema } = view.state;
pos = findPlaceholder(view.state, id); pos = findPlaceholder(view.state, id);
if (pos == null) return; if (pos == null) {
editor.storage.image.uploadInProgress = false;
return;
}
const imageSrc = typeof src === "object" ? reader.result : src; const imageSrc = typeof src === "object" ? reader.result : src;
const node = schema.nodes.image.create({ src: imageSrc }); const node = schema.nodes.image.create({ src: imageSrc });
@ -129,9 +159,9 @@ export async function startImageUpload(file: File, view: EditorView, pos: number
view.dispatch(transaction); view.dispatch(transaction);
view.focus(); view.focus();
editor.storage.image.uploadInProgress = false;
} catch (error) { } catch (error) {
console.error("Upload error: ", error); removePlaceholder(editor, view, id);
removePlaceholder(view, id);
} }
} }

View File

@ -1,9 +1,7 @@
import { EditorProps } from "@tiptap/pm/view"; import { EditorProps } from "@tiptap/pm/view";
import { cn, findTableAncestor } from "src/lib/utils"; import { cn } from "src/lib/utils";
import { UploadImage } from "src/types/upload-image";
import { startImageUpload } from "src/ui/plugins/upload-image";
export function CoreEditorProps(uploadFile: UploadImage, editorClassName: string): EditorProps { export function CoreEditorProps(editorClassName: string): EditorProps {
return { return {
attributes: { attributes: {
class: cn( class: cn(
@ -17,45 +15,12 @@ export function CoreEditorProps(uploadFile: UploadImage, editorClassName: string
if (["ArrowUp", "ArrowDown", "Enter"].includes(event.key)) { if (["ArrowUp", "ArrowDown", "Enter"].includes(event.key)) {
const slashCommand = document.querySelector("#slash-command"); const slashCommand = document.querySelector("#slash-command");
if (slashCommand) { if (slashCommand) {
console.log("registered");
return true; return true;
} }
} }
}, },
}, },
handlePaste: (view, event) => {
if (typeof window !== "undefined") {
const selection: any = window?.getSelection();
if (selection.rangeCount !== 0) {
const range = selection.getRangeAt(0);
if (findTableAncestor(range.startContainer)) {
return;
}
}
}
if (event.clipboardData && event.clipboardData.files && event.clipboardData.files[0]) {
event.preventDefault();
const file = event.clipboardData.files[0];
const pos = view.state.selection.from;
startImageUpload(file, view, pos, uploadFile);
return true;
}
return false;
},
handleDrop: (view, event, _slice, moved) => {
if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]) {
event.preventDefault();
const file = event.dataTransfer.files[0];
const coordinates = view.posAtCoords({
left: event.clientX,
top: event.clientY,
});
if (coordinates) {
startImageUpload(file, view, coordinates.pos - 1, uploadFile);
}
return true;
}
return false;
},
transformPastedHTML(html) { transformPastedHTML(html) {
return html.replace(/<img.*?>/g, ""); return html.replace(/<img.*?>/g, "");
}, },

View File

@ -101,21 +101,44 @@ export default function BlockMenu(props: BlockMenuProps) {
label: "Duplicate", label: "Duplicate",
isDisabled: editor.state.selection.content().content.firstChild?.type.name === "image", isDisabled: editor.state.selection.content().content.firstChild?.type.name === "image",
onClick: (e) => { onClick: (e) => {
const { view } = editor;
const { state } = view;
const { selection } = state;
editor
.chain()
.insertContentAt(selection.to, selection.content().content.firstChild!.toJSON(), {
updateSelection: true,
})
.focus(selection.to + 1, { scrollIntoView: false })
.run();
popup.current?.hide();
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
try {
const { state } = editor;
const { selection } = state;
const firstChild = selection.content().content.firstChild;
const docSize = state.doc.content.size;
if (!firstChild) {
throw new Error("No content selected or content is not duplicable.");
}
// Directly use selection.to as the insertion position
const insertPos = selection.to;
// Ensure the insertion position is within the document's bounds
if (insertPos < 0 || insertPos > docSize) {
throw new Error("The insertion position is invalid or outside the document.");
}
const contentToInsert = firstChild.toJSON();
// Insert the content at the calculated position
editor
.chain()
.insertContentAt(insertPos, contentToInsert, {
updateSelection: true,
})
.focus(Math.min(insertPos + 1, docSize), { scrollIntoView: false })
.run();
} catch (error) {
if (error instanceof Error) {
console.error(error.message);
}
}
popup.current?.hide();
}, },
}, },
]; ];

View File

@ -58,10 +58,10 @@ function nodeDOMAtCoords(coords: { x: number; y: number }) {
[ [
"li", "li",
"p:not(:first-child)", "p:not(:first-child)",
"pre", ".code-block",
"blockquote", "blockquote",
"h1, h2, h3", "h1, h2, h3",
".table-wrapper", "table",
"[data-type=horizontalRule]", "[data-type=horizontalRule]",
].join(", ") ].join(", ")
) )
@ -77,10 +77,25 @@ function nodePosAtDOM(node: Element, view: EditorView, options: DragHandleOption
})?.inside; })?.inside;
} }
function nodePosAtDOMForBlockquotes(node: Element, view: EditorView) {
const boundingRect = node.getBoundingClientRect();
return view.posAtCoords({
left: boundingRect.left + 1,
top: boundingRect.top + 1,
})?.inside;
}
function calcNodePos(pos: number, view: EditorView) { function calcNodePos(pos: number, view: EditorView) {
const $pos = view.state.doc.resolve(pos); const maxPos = view.state.doc.content.size;
if ($pos.depth > 1) return $pos.before($pos.depth); const safePos = Math.max(0, Math.min(pos, maxPos));
return pos; const $pos = view.state.doc.resolve(safePos);
if ($pos.depth > 1) {
const newPos = $pos.before($pos.depth);
return Math.max(0, Math.min(newPos, maxPos));
}
return safePos;
} }
function DragHandle(options: DragHandleOptions) { function DragHandle(options: DragHandleOptions) {
@ -156,6 +171,20 @@ function DragHandle(options: DragHandleOptions) {
if (!(node instanceof Element)) return; if (!(node instanceof Element)) return;
if (node.matches("blockquote")) {
let nodePosForBlockquotes = nodePosAtDOMForBlockquotes(node, view);
if (nodePosForBlockquotes === null || nodePosForBlockquotes === undefined) return;
const docSize = view.state.doc.content.size;
nodePosForBlockquotes = Math.max(0, Math.min(nodePosForBlockquotes, docSize));
if (nodePosForBlockquotes >= 0 && nodePosForBlockquotes <= docSize) {
const nodeSelection = NodeSelection.create(view.state.doc, nodePosForBlockquotes);
view.dispatch(view.state.tr.setSelection(nodeSelection));
}
return;
}
let nodePos = nodePosAtDOM(node, view, options); let nodePos = nodePosAtDOM(node, view, options);
if (nodePos === null || nodePos === undefined) return; if (nodePos === null || nodePos === undefined) return;
@ -244,11 +273,13 @@ function DragHandle(options: DragHandleOptions) {
rect.top += (lineHeight - 20) / 2; rect.top += (lineHeight - 20) / 2;
rect.top += paddingTop; rect.top += paddingTop;
// Li markers // Li markers
if (node.matches("ul:not([data-type=taskList]) li, ol li")) { if (node.matches("ul:not([data-type=taskList]) li, ol li")) {
rect.top += 4; rect.top += 4;
rect.left -= 18; rect.left -= 18;
} }
rect.width = options.dragHandleWidth; rect.width = options.dragHandleWidth;
if (!dragHandleElement) return; if (!dragHandleElement) return;

View File

@ -35,7 +35,7 @@ export const OAuthOptions: React.FC<Props> = observer((props) => {
const response = await authService.socialAuth(socialAuthPayload); const response = await authService.socialAuth(socialAuthPayload);
if (response) handleSignInRedirection(); if (response) handleSignInRedirection();
} else throw Error("Cant find credentials"); } else throw Error("Can't find credentials");
} catch (err: any) { } catch (err: any) {
setToastAlert({ setToastAlert({
title: "Error signing in!", title: "Error signing in!",
@ -56,7 +56,7 @@ export const OAuthOptions: React.FC<Props> = observer((props) => {
const response = await authService.socialAuth(socialAuthPayload); const response = await authService.socialAuth(socialAuthPayload);
if (response) handleSignInRedirection(); if (response) handleSignInRedirection();
} else throw Error("Cant find credentials"); } else throw Error("Can't find credentials");
} catch (err: any) { } catch (err: any) {
setToastAlert({ setToastAlert({
title: "Error signing in!", title: "Error signing in!",

View File

@ -36,7 +36,7 @@ export const OAuthOptions: React.FC<Props> = observer((props) => {
const response = await authService.socialAuth(socialAuthPayload); const response = await authService.socialAuth(socialAuthPayload);
if (response) handleSignInRedirection(); if (response) handleSignInRedirection();
} else throw Error("Cant find credentials"); } else throw Error("Can't find credentials");
} catch (err: any) { } catch (err: any) {
setToast({ setToast({
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR,
@ -57,7 +57,7 @@ export const OAuthOptions: React.FC<Props> = observer((props) => {
const response = await authService.socialAuth(socialAuthPayload); const response = await authService.socialAuth(socialAuthPayload);
if (response) handleSignInRedirection(); if (response) handleSignInRedirection();
} else throw Error("Cant find credentials"); } else throw Error("Can't find credentials");
} catch (err: any) { } catch (err: any) {
setToast({ setToast({
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR,

View File

@ -24,7 +24,7 @@ const RenderIfVisible: React.FC<Props> = (props) => {
as = "div", as = "div",
children, children,
classNames = "", classNames = "",
alwaysRender = false, //render the children even if it is not visble in root alwaysRender = false, //render the children even if it is not visible in root
placeholderChildren = null, //placeholder children placeholderChildren = null, //placeholder children
pauseHeightUpdateWhileRendering = false, //while this is true the height of the blocks are maintained pauseHeightUpdateWhileRendering = false, //while this is true the height of the blocks are maintained
changingReference, //This is to force render when this reference is changed changingReference, //This is to force render when this reference is changed
@ -48,7 +48,7 @@ const RenderIfVisible: React.FC<Props> = (props) => {
// } else { // } else {
// setShouldVisible(entries[0].isIntersecting); // setShouldVisible(entries[0].isIntersecting);
// } // }
setShouldVisible(entries[0].isIntersecting); setShouldVisible(entries[entries.length - 1].isIntersecting);
}, },
{ {
root: root?.current, root: root?.current,

View File

@ -166,7 +166,7 @@ export const KanbanGroup = (props: IKanbanGroup) => {
<div <div
id={`${groupId}__${sub_group_id}`} id={`${groupId}__${sub_group_id}`}
className={cn( className={cn(
"relative h-full transition-all", "relative h-full transition-all min-h-[50px]",
{ "bg-custom-background-80": isDraggingOverColumn }, { "bg-custom-background-80": isDraggingOverColumn },
{ "vertical-scrollbar scrollbar-md": !sub_group_by } { "vertical-scrollbar scrollbar-md": !sub_group_by }
)} )}