Kanban DnD improvement

This commit is contained in:
rahulramesha 2024-04-30 20:16:55 +05:30
parent f5b7964c6b
commit 9870be61bb
8 changed files with 96 additions and 11 deletions

View File

@ -76,6 +76,8 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
const sub_group_by = displayFilters?.sub_group_by; const sub_group_by = displayFilters?.sub_group_by;
const group_by = displayFilters?.group_by; const group_by = displayFilters?.group_by;
const orderBy = displayFilters?.order_by;
const userDisplayFilters = displayFilters || null; const userDisplayFilters = displayFilters || null;
const KanBanView = sub_group_by ? KanBanSwimLanes : KanBan; const KanBanView = sub_group_by ? KanBanSwimLanes : KanBan;
@ -158,7 +160,8 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
issues.getIssueIds, issues.getIssueIds,
updateIssue, updateIssue,
group_by, group_by,
sub_group_by sub_group_by,
orderBy !== "sort_order"
).catch((err) => { ).catch((err) => {
setToast({ setToast({
title: "Error", title: "Error",
@ -259,6 +262,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
displayProperties={displayProperties} displayProperties={displayProperties}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
orderBy={orderBy}
updateIssue={updateIssue} updateIssue={updateIssue}
quickActions={renderQuickActions} quickActions={renderQuickActions}
handleKanbanFilters={handleKanbanFilters} handleKanbanFilters={handleKanbanFilters}

View File

@ -11,6 +11,7 @@ import {
TUnGroupedIssues, TUnGroupedIssues,
TIssueKanbanFilters, TIssueKanbanFilters,
TIssueGroupByOptions, TIssueGroupByOptions,
TIssueOrderByOptions,
} from "@plane/types"; } from "@plane/types";
// constants // constants
// hooks // hooks
@ -39,6 +40,7 @@ export interface IGroupByKanBan {
displayProperties: IIssueDisplayProperties | undefined; displayProperties: IIssueDisplayProperties | undefined;
sub_group_by: TIssueGroupByOptions | undefined; sub_group_by: TIssueGroupByOptions | undefined;
group_by: TIssueGroupByOptions | undefined; group_by: TIssueGroupByOptions | undefined;
orderBy: TIssueOrderByOptions | undefined;
sub_group_id: string; sub_group_id: string;
isDragDisabled: boolean; isDragDisabled: boolean;
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined; updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined;
@ -87,6 +89,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
handleOnDrop, handleOnDrop,
showEmptyGroup = true, showEmptyGroup = true,
subGroupIssueHeaderCount, subGroupIssueHeaderCount,
orderBy,
} = props; } = props;
const member = useMember(); const member = useMember();
@ -180,6 +183,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
displayProperties={displayProperties} displayProperties={displayProperties}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
orderBy={orderBy}
sub_group_id={sub_group_id} sub_group_id={sub_group_id}
isDragDisabled={isDragDisabled} isDragDisabled={isDragDisabled}
updateIssue={updateIssue} updateIssue={updateIssue}
@ -206,6 +210,7 @@ export interface IKanBan {
displayProperties: IIssueDisplayProperties | undefined; displayProperties: IIssueDisplayProperties | undefined;
sub_group_by: TIssueGroupByOptions | undefined; sub_group_by: TIssueGroupByOptions | undefined;
group_by: TIssueGroupByOptions | undefined; group_by: TIssueGroupByOptions | undefined;
orderBy: TIssueOrderByOptions | undefined;
sub_group_id?: string; sub_group_id?: string;
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined; updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined;
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode; quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
@ -252,6 +257,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
handleOnDrop, handleOnDrop,
showEmptyGroup, showEmptyGroup,
subGroupIssueHeaderCount, subGroupIssueHeaderCount,
orderBy,
} = props; } = props;
const issueKanBanView = useKanbanView(); const issueKanBanView = useKanbanView();
@ -263,6 +269,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
displayProperties={displayProperties} displayProperties={displayProperties}
group_by={group_by} group_by={group_by}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
orderBy={orderBy}
sub_group_id={sub_group_id} sub_group_id={sub_group_id}
isDragDisabled={!issueKanBanView?.getCanUserDragDrop(group_by, sub_group_by)} isDragDisabled={!issueKanBanView?.getCanUserDragDrop(group_by, sub_group_by)}
updateIssue={updateIssue} updateIssue={updateIssue}

View File

@ -11,13 +11,19 @@ import {
TSubGroupedIssues, TSubGroupedIssues,
TUnGroupedIssues, TUnGroupedIssues,
TIssueGroupByOptions, TIssueGroupByOptions,
TIssueOrderByOptions,
} from "@plane/types"; } from "@plane/types";
// helpers // helpers
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
// hooks // hooks
import { useProjectState } from "@/hooks/store"; import { useProjectState } from "@/hooks/store";
//components //components
import { KanbanDropLocation, getSourceFromDropPayload, getDestinationFromDropPayload } from "./utils"; import {
KanbanDropLocation,
getSourceFromDropPayload,
getDestinationFromDropPayload,
highlightIssueOnDrop,
} from "./utils";
import { KanbanIssueBlocksList, KanBanQuickAddIssueForm } from "."; import { KanbanIssueBlocksList, KanBanQuickAddIssueForm } from ".";
interface IKanbanGroup { interface IKanbanGroup {
@ -45,6 +51,7 @@ interface IKanbanGroup {
groupByVisibilityToggle?: boolean; groupByVisibilityToggle?: boolean;
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>; scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
handleOnDrop: (source: KanbanDropLocation, destination: KanbanDropLocation) => Promise<void>; handleOnDrop: (source: KanbanDropLocation, destination: KanbanDropLocation) => Promise<void>;
orderBy: TIssueOrderByOptions | undefined;
} }
export const KanbanGroup = (props: IKanbanGroup) => { export const KanbanGroup = (props: IKanbanGroup) => {
@ -52,6 +59,7 @@ export const KanbanGroup = (props: IKanbanGroup) => {
groupId, groupId,
sub_group_id, sub_group_id,
group_by, group_by,
orderBy,
sub_group_by, sub_group_by,
issuesMap, issuesMap,
displayProperties, displayProperties,
@ -102,13 +110,15 @@ export const KanbanGroup = (props: IKanbanGroup) => {
if (!source || !destination) return; if (!source || !destination) return;
handleOnDrop(source, destination); handleOnDrop(source, destination);
highlightIssueOnDrop(payload.source.element.id, orderBy !== "sort_order");
}, },
}), }),
autoScrollForElements({ autoScrollForElements({
element, element,
}) })
); );
}, [columnRef?.current, groupId, sub_group_id, setIsDraggingOverColumn]); }, [columnRef?.current, groupId, sub_group_id, setIsDraggingOverColumn, orderBy]);
const prePopulateQuickAddData = ( const prePopulateQuickAddData = (
groupByKey: string | undefined, groupByKey: string | undefined,
@ -162,16 +172,28 @@ export const KanbanGroup = (props: IKanbanGroup) => {
return preloadedData; return preloadedData;
}; };
const shouldOverlay = isDraggingOverColumn && orderBy !== "sort_order";
return ( return (
<div <div
id={`${groupId}__${sub_group_id}`} id={`${groupId}__${sub_group_id}`}
className={cn( className={cn(
"relative h-full transition-all min-h-[50px]", "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 && !shouldOverlay }
)} )}
ref={columnRef} ref={columnRef}
> >
<div
className={cn(
"absolute top-0 left-0 h-full w-full justify-center items-center text-sm text-custom-text-100",
{
"flex bg-custom-primary-10 border-[2px] border-custom-primary-40 rounded z-[2]": shouldOverlay,
},
{ hidden: !shouldOverlay }
)}
>
<span>Drop here to move issue</span>
</div>
<KanbanIssueBlocksList <KanbanIssueBlocksList
sub_group_id={sub_group_id} sub_group_id={sub_group_id}
columnId={groupId} columnId={groupId}

View File

@ -11,6 +11,7 @@ import {
TUnGroupedIssues, TUnGroupedIssues,
TIssueKanbanFilters, TIssueKanbanFilters,
TIssueGroupByOptions, TIssueGroupByOptions,
TIssueOrderByOptions,
} from "@plane/types"; } from "@plane/types";
// components // components
import { useCycle, useLabel, useMember, useModule, useProject, useProjectState } from "@/hooks/store"; import { useCycle, useLabel, useMember, useModule, useProject, useProjectState } from "@/hooks/store";
@ -113,6 +114,7 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
disableIssueCreation?: boolean; disableIssueCreation?: boolean;
storeType: KanbanStoreType; storeType: KanbanStoreType;
enableQuickIssueCreate: boolean; enableQuickIssueCreate: boolean;
orderBy: TIssueOrderByOptions | undefined;
canEditProperties: (projectId: string | undefined) => boolean; canEditProperties: (projectId: string | undefined) => boolean;
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>; addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
quickAddCallback?: ( quickAddCallback?: (
@ -145,6 +147,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
viewId, viewId,
scrollableContainerRef, scrollableContainerRef,
handleOnDrop, handleOnDrop,
orderBy,
} = props; } = props;
const calculateIssueCount = (column_id: string) => { const calculateIssueCount = (column_id: string) => {
@ -180,7 +183,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
if (subGroupByVisibilityToggle.showGroup === false) return <></>; if (subGroupByVisibilityToggle.showGroup === false) return <></>;
return ( return (
<div key={_list.id} className="flex flex-shrink-0 flex-col"> <div key={_list.id} className="flex flex-shrink-0 flex-col">
<div className="sticky top-[50px] z-[1] py-1 flex w-full items-center bg-custom-background-100 border-y-[0.5px] border-custom-border-200"> <div className="sticky top-[50px] z-[3] py-1 flex w-full items-center bg-custom-background-100 border-y-[0.5px] border-custom-border-200">
<div className="sticky left-0 flex-shrink-0"> <div className="sticky left-0 flex-shrink-0">
<HeaderSubGroupByCard <HeaderSubGroupByCard
column_id={_list.id} column_id={_list.id}
@ -215,6 +218,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
viewId={viewId} viewId={viewId}
scrollableContainerRef={scrollableContainerRef} scrollableContainerRef={scrollableContainerRef}
handleOnDrop={handleOnDrop} handleOnDrop={handleOnDrop}
orderBy={orderBy}
subGroupIssueHeaderCount={(groupByListId: string) => subGroupIssueHeaderCount={(groupByListId: string) =>
getSubGroupHeaderIssuesCount(issueIds as TSubGroupedIssues, groupByListId) getSubGroupHeaderIssuesCount(issueIds as TSubGroupedIssues, groupByListId)
} }
@ -253,6 +257,7 @@ export interface IKanBanSwimLanes {
viewId?: string; viewId?: string;
canEditProperties: (projectId: string | undefined) => boolean; canEditProperties: (projectId: string | undefined) => boolean;
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>; scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
orderBy: TIssueOrderByOptions | undefined;
} }
export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => { export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
@ -262,6 +267,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
displayProperties, displayProperties,
sub_group_by, sub_group_by,
group_by, group_by,
orderBy,
updateIssue, updateIssue,
storeType, storeType,
quickActions, quickActions,
@ -312,7 +318,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
return ( return (
<div className="relative"> <div className="relative">
<div className="sticky top-0 z-[2] h-[50px] bg-custom-background-90 px-2"> <div className="sticky top-0 z-[4] h-[50px] bg-custom-background-90 px-2">
<SubGroupSwimlaneHeader <SubGroupSwimlaneHeader
issueIds={issueIds} issueIds={issueIds}
group_by={group_by} group_by={group_by}
@ -333,6 +339,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
displayProperties={displayProperties} displayProperties={displayProperties}
group_by={group_by} group_by={group_by}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
orderBy={orderBy}
updateIssue={updateIssue} updateIssue={updateIssue}
quickActions={quickActions} quickActions={quickActions}
kanbanFilters={kanbanFilters} kanbanFilters={kanbanFilters}

View File

@ -1,4 +1,5 @@
import pull from "lodash/pull"; import pull from "lodash/pull";
import scrollIntoView from "smooth-scroll-into-view-if-needed";
import { IPragmaticDropPayload, TIssue, TIssueGroupByOptions } from "@plane/types"; import { IPragmaticDropPayload, TIssue, TIssueGroupByOptions } from "@plane/types";
import { ISSUE_FILTER_DEFAULT_DATA } from "@/store/issue/helpers/issue-helper.store"; import { ISSUE_FILTER_DEFAULT_DATA } from "@/store/issue/helpers/issue-helper.store";
@ -87,14 +88,17 @@ export const getDestinationFromDropPayload = (payload: IPragmaticDropPayload): K
const handleSortOrder = ( const handleSortOrder = (
destinationIssues: string[], destinationIssues: string[],
destinationIssueId: string | undefined, destinationIssueId: string | undefined,
getIssueById: (issueId: string) => TIssue | undefined getIssueById: (issueId: string) => TIssue | undefined,
shouldAddIssueAtTop = false
) => { ) => {
const sortOrderDefaultValue = 65535; const sortOrderDefaultValue = 65535;
let currentIssueState = {}; let currentIssueState = {};
const destinationIndex = destinationIssueId const destinationIndex = destinationIssueId
? destinationIssues.indexOf(destinationIssueId) ? destinationIssues.indexOf(destinationIssueId)
: destinationIssues.length; : shouldAddIssueAtTop
? 0
: destinationIssues.length;
if (destinationIssues && destinationIssues.length > 0) { if (destinationIssues && destinationIssues.length > 0) {
if (destinationIndex === 0) { if (destinationIndex === 0) {
@ -145,7 +149,8 @@ export const handleDragDrop = async (
getIssueIds: (groupId?: string, subGroupId?: string) => string[] | undefined, getIssueIds: (groupId?: string, subGroupId?: string) => string[] | undefined,
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined, updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined,
groupBy: TIssueGroupByOptions | undefined, groupBy: TIssueGroupByOptions | undefined,
subGroupBy: TIssueGroupByOptions | undefined subGroupBy: TIssueGroupByOptions | undefined,
shouldAddIssueAtTop = false
) => { ) => {
if (!source.id || !groupBy || (subGroupBy && (!source.subGroupId || !destination.subGroupId))) return; if (!source.id || !groupBy || (subGroupBy && (!source.subGroupId || !destination.subGroupId))) return;
@ -165,7 +170,7 @@ export const handleDragDrop = async (
// for both horizontal and vertical dnd // for both horizontal and vertical dnd
updatedIssue = { updatedIssue = {
...updatedIssue, ...updatedIssue,
...handleSortOrder(destinationIssues, destination.id, getIssueById), ...handleSortOrder(destinationIssues, destination.id, getIssueById, shouldAddIssueAtTop),
}; };
if (source.groupId && destination.groupId && source.groupId !== destination.groupId) { if (source.groupId && destination.groupId && source.groupId !== destination.groupId) {
@ -207,3 +212,18 @@ export const handleDragDrop = async (
); );
} }
}; };
export const highlightIssueOnDrop = (elementId: string | undefined, shouldScrollIntoView = true) => {
setTimeout(async () => {
const sourceElementId = elementId ?? "";
const sourceElement = document.getElementById(sourceElementId);
if (shouldScrollIntoView && sourceElement)
await scrollIntoView(sourceElement, { behavior: "smooth", block: "center" });
sourceElement?.classList?.add("highlight");
setTimeout(() => {
const sourceElementId = elementId ?? "";
const sourceElement = document.getElementById(sourceElementId);
sourceElement?.classList?.remove("highlight");
}, 1000);
}, 200);
};

View File

@ -58,6 +58,7 @@
"react-markdown": "^8.0.7", "react-markdown": "^8.0.7",
"react-popper": "^2.3.0", "react-popper": "^2.3.0",
"sharp": "^0.32.1", "sharp": "^0.32.1",
"smooth-scroll-into-view-if-needed": "^2.0.2",
"swr": "^2.1.3", "swr": "^2.1.3",
"tailwind-merge": "^2.0.0", "tailwind-merge": "^2.0.0",
"use-debounce": "^9.0.4", "use-debounce": "^9.0.4",

View File

@ -630,3 +630,8 @@ div.web-view-spinner div.bar12 {
.scrollbar-lg::-webkit-scrollbar-thumb { .scrollbar-lg::-webkit-scrollbar-thumb {
border: 4px solid rgba(0, 0, 0, 0); border: 4px solid rgba(0, 0, 0, 0);
} }
/* highlight class */
.highlight {
border: 1px solid rgb(var(--color-primary-100));
}

View File

@ -2755,7 +2755,7 @@
dependencies: dependencies:
"@types/react" "*" "@types/react" "*"
"@types/react@*", "@types/react@18.2.42", "@types/react@^18.2.42": "@types/react@*", "@types/react@^18.2.42":
version "18.2.42" version "18.2.42"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.42.tgz#6f6b11a904f6d96dda3c2920328a97011a00aba7" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.42.tgz#6f6b11a904f6d96dda3c2920328a97011a00aba7"
integrity sha512-c1zEr96MjakLYus/wPnuWDo1/zErfdU9rNsIGmE+NV71nx88FG9Ttgo5dqorXTu/LImX2f63WBP986gJkMPNbA== integrity sha512-c1zEr96MjakLYus/wPnuWDo1/zErfdU9rNsIGmE+NV71nx88FG9Ttgo5dqorXTu/LImX2f63WBP986gJkMPNbA==
@ -3699,6 +3699,11 @@ commondir@^1.0.1:
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==
compute-scroll-into-view@^3.0.2:
version "3.1.0"
resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-3.1.0.tgz#753f11d972596558d8fe7c6bcbc8497690ab4c87"
integrity sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==
concat-map@0.0.1: concat-map@0.0.1:
version "0.0.1" version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@ -7633,6 +7638,13 @@ schema-utils@^3.1.1:
ajv "^6.12.5" ajv "^6.12.5"
ajv-keywords "^3.5.2" ajv-keywords "^3.5.2"
scroll-into-view-if-needed@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz#fa9524518c799b45a2ef6bbffb92bcad0296d01f"
integrity sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==
dependencies:
compute-scroll-into-view "^3.0.2"
selecto@~1.26.3: selecto@~1.26.3:
version "1.26.3" version "1.26.3"
resolved "https://registry.yarnpkg.com/selecto/-/selecto-1.26.3.tgz#12f259112b943d395731524e3bb0115da7372212" resolved "https://registry.yarnpkg.com/selecto/-/selecto-1.26.3.tgz#12f259112b943d395731524e3bb0115da7372212"
@ -7774,6 +7786,13 @@ slash@^3.0.0:
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
smooth-scroll-into-view-if-needed@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/smooth-scroll-into-view-if-needed/-/smooth-scroll-into-view-if-needed-2.0.2.tgz#5bd4ebef668474d6618ce8704650082e93068371"
integrity sha512-z54WzUSlM+xHHvJu3lMIsh+1d1kA4vaakcAtQvqzeGJ5Ffau7EKjpRrMHh1/OBo5zyU2h30ZYEt77vWmPHqg7Q==
dependencies:
scroll-into-view-if-needed "^3.1.0"
snake-case@^3.0.4: snake-case@^3.0.4:
version "3.0.4" version "3.0.4"
resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c"