From be3c18b677e4e604910fb798d8ac019bbdc7ff46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl-Gerhard=20Lindesva=CC=88rd?= Date: Wed, 16 Apr 2025 11:08:58 +0200 Subject: [PATCH] feature(dashboard): filter on profile properties and support drag n drop for events --- apps/dashboard/package.json | 3 + apps/dashboard/src/app/debug/Debug.tsx | 40 -- apps/dashboard/src/app/debug/page.tsx | 5 - .../src/components/report/reportSlice.ts | 12 + .../report/sidebar/ReportEvents.tsx | 506 +++++++++++------- .../report/sidebar/filters/FilterItem.tsx | 8 +- .../sidebar/filters/FiltersCombobox.tsx | 288 ++++++++-- .../report/sidebar/filters/FiltersList.tsx | 2 +- packages/db/src/services/chart.service.ts | 12 +- packages/trpc/src/routers/chart.ts | 26 + pnpm-lock.yaml | 56 ++ 11 files changed, 658 insertions(+), 300 deletions(-) delete mode 100644 apps/dashboard/src/app/debug/Debug.tsx delete mode 100644 apps/dashboard/src/app/debug/page.tsx diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 91c60e36..577258b3 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -13,6 +13,9 @@ "dependencies": { "@ai-sdk/react": "^1.2.5", "@clickhouse/client": "^1.2.0", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@hookform/resolvers": "^3.3.4", "@hyperdx/node-opentelemetry": "^0.8.1", "@openpanel/auth": "workspace:^", diff --git a/apps/dashboard/src/app/debug/Debug.tsx b/apps/dashboard/src/app/debug/Debug.tsx deleted file mode 100644 index 2693177a..00000000 --- a/apps/dashboard/src/app/debug/Debug.tsx +++ /dev/null @@ -1,40 +0,0 @@ -'use client'; - -import { Button } from '@/components/ui/button'; -import { api } from '@/trpc/client'; -import { useState } from 'react'; - -export function Debug() { - const [sameSite, setSameSite] = useState<'lax' | 'strict' | 'none'>('lax'); - const [domain, setDomain] = useState('localhost'); - const cookiePost = api.user.debugPostCookie.useMutation(); - const cookieGet = api.user.debugGetCookie.useQuery({ - domain, - sameSite, - }); - return ( -
- setDomain(e.target.value)} - /> - - - -
- ); -} diff --git a/apps/dashboard/src/app/debug/page.tsx b/apps/dashboard/src/app/debug/page.tsx deleted file mode 100644 index f9365ace..00000000 --- a/apps/dashboard/src/app/debug/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { Debug } from './Debug'; - -export default function Page() { - return ; -} diff --git a/apps/dashboard/src/components/report/reportSlice.ts b/apps/dashboard/src/components/report/reportSlice.ts index a0776968..33b0dc54 100644 --- a/apps/dashboard/src/components/report/reportSlice.ts +++ b/apps/dashboard/src/components/report/reportSlice.ts @@ -278,6 +278,17 @@ export const reportSlice = createSlice({ state.dirty = true; state.funnelWindow = action.payload || undefined; }, + reorderEvents( + state, + action: PayloadAction<{ fromIndex: number; toIndex: number }>, + ) { + state.dirty = true; + const { fromIndex, toIndex } = action.payload; + const [movedEvent] = state.events.splice(fromIndex, 1); + if (movedEvent) { + state.events.splice(toIndex, 0, movedEvent); + } + }, }, }); @@ -307,6 +318,7 @@ export const { changeUnit, changeFunnelGroup, changeFunnelWindow, + reorderEvents, } = reportSlice.actions; export default reportSlice.reducer; diff --git a/apps/dashboard/src/components/report/sidebar/ReportEvents.tsx b/apps/dashboard/src/components/report/sidebar/ReportEvents.tsx index 5614318f..9f70a4bd 100644 --- a/apps/dashboard/src/components/report/sidebar/ReportEvents.tsx +++ b/apps/dashboard/src/components/report/sidebar/ReportEvents.tsx @@ -2,25 +2,178 @@ import { ColorSquare } from '@/components/color-square'; import { Combobox } from '@/components/ui/combobox'; +import { ComboboxAdvanced } from '@/components/ui/combobox-advanced'; import { DropdownMenuComposed } from '@/components/ui/dropdown-menu'; import { Input } from '@/components/ui/input'; import { useAppParams } from '@/hooks/useAppParams'; import { useDebounceFn } from '@/hooks/useDebounceFn'; import { useEventNames } from '@/hooks/useEventNames'; import { useDispatch, useSelector } from '@/redux'; -import { GanttChart, GanttChartIcon, Users } from 'lucide-react'; - +import { + DndContext, + type DragEndEvent, + KeyboardSensor, + PointerSensor, + closestCenter, + useSensor, + useSensors, +} from '@dnd-kit/core'; +import { + SortableContext, + sortableKeyboardCoordinates, + useSortable, + verticalListSortingStrategy, +} from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; import { alphabetIds } from '@openpanel/constants'; import type { IChartEvent } from '@openpanel/validation'; - -import { ComboboxAdvanced } from '@/components/ui/combobox-advanced'; -import { addEvent, changeEvent, removeEvent } from '../reportSlice'; +import { GanttChartIcon, HandIcon, Users } from 'lucide-react'; +import { + addEvent, + changeEvent, + removeEvent, + reorderEvents, +} from '../reportSlice'; import { EventPropertiesCombobox } from './EventPropertiesCombobox'; -import { ReportEventMore } from './ReportEventMore'; import type { ReportEventMoreProps } from './ReportEventMore'; +import { ReportEventMore } from './ReportEventMore'; import { FiltersCombobox } from './filters/FiltersCombobox'; import { FiltersList } from './filters/FiltersList'; +function SortableEvent({ + event, + index, + showSegment, + showAddFilter, + isSelectManyEvents, + ...props +}: { + event: IChartEvent; + index: number; + showSegment: boolean; + showAddFilter: boolean; + isSelectManyEvents: boolean; +} & React.HTMLAttributes) { + const dispatch = useDispatch(); + const { attributes, listeners, setNodeRef, transform, transition } = + useSortable({ id: event.id ?? '' }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + }; + + return ( +
+
+ + {props.children} +
+ + {/* Segment and Filter buttons */} + {(showSegment || showAddFilter) && ( +
+ {showSegment && ( + { + dispatch( + changeEvent({ + ...event, + segment, + }), + ); + }} + items={[ + { + value: 'event', + label: 'All events', + }, + { + value: 'user', + label: 'Unique users', + }, + { + value: 'session', + label: 'Unique sessions', + }, + { + value: 'user_average', + label: 'Average event per user', + }, + { + value: 'one_event_per_user', + label: 'One event per user', + }, + { + value: 'property_sum', + label: 'Sum of property', + }, + { + value: 'property_average', + label: 'Average of property', + }, + ]} + label="Segment" + > + + + )} + {showAddFilter && } + + {showSegment && + (event.segment === 'property_average' || + event.segment === 'property_sum') && ( + + )} +
+ )} + + {/* Filters */} + {!isSelectManyEvents && } +
+ ); +} + export function ReportEvents() { const selectedEvents = useSelector((state) => state.report.events); const chartType = useSelector((state) => state.report.chartType); @@ -40,6 +193,24 @@ export function ReportEvents() { }); const isSelectManyEvents = chartType === 'retention'; + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }), + ); + + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + + if (over && active.id !== over.id) { + const oldIndex = selectedEvents.findIndex((e) => e.id === active.id); + const newIndex = selectedEvents.findIndex((e) => e.id === over.id); + + dispatch(reorderEvents({ fromIndex: oldIndex, toIndex: newIndex })); + } + }; + const handleMore = (event: IChartEvent) => { const callback: ReportEventMoreProps['onClick'] = (action) => { switch (action) { @@ -55,214 +226,135 @@ export function ReportEvents() { return (

Events

-
- {selectedEvents.map((event, index) => { - return ( -
-
- {alphabetIds[index]} - {isSelectManyEvents ? ( - { - dispatch( - changeEvent({ - id: event.id, - segment: 'user', - filters: [ - { - name: 'name', - operator: 'is', - value: value, - }, - ], - name: '*', - }), - ); - }} - items={eventNames.map((item) => ({ - label: item.name, - value: item.name, - }))} - placeholder="Select event" - /> - ) : ( - { - dispatch( - changeEvent({ - ...event, - name: value, - filters: [], - }), - ); - }} - items={eventNames.map((item) => ({ - label: item.name, - value: item.name, - }))} - placeholder="Select event" - /> - )} - {showDisplayNameInput && ( - { - dispatchChangeEvent({ - ...event, - displayName: e.target.value, - }); - }} - /> - )} - -
- - {/* Segment and Filter buttons */} - {(showSegment || showAddFilter) && ( -
- {showSegment && ( - { + + ({ id: e.id ?? '' }))} + strategy={verticalListSortingStrategy} + > +
+ {selectedEvents.map((event, index) => { + return ( + + {isSelectManyEvents ? ( + { dispatch( changeEvent({ - ...event, - segment, + id: event.id, + segment: 'user', + filters: [ + { + name: 'name', + operator: 'is', + value: value, + }, + ], + name: '*', }), ); }} - items={[ - { - value: 'event', - label: 'All events', - }, - { - value: 'user', - label: 'Unique users', - }, - { - value: 'session', - label: 'Unique sessions', - }, - { - value: 'user_average', - label: 'Average event per user', - }, - { - value: 'one_event_per_user', - label: 'One event per user', - }, - { - value: 'property_sum', - label: 'Sum of property', - }, - { - value: 'property_average', - label: 'Average of property', - }, - ]} - label="Segment" - > - - + items={eventNames.map((item) => ({ + label: item.name, + value: item.name, + }))} + placeholder="Select event" + /> + ) : ( + { + dispatch( + changeEvent({ + ...event, + name: value, + filters: [], + }), + ); + }} + items={eventNames.map((item) => ({ + label: item.name, + value: item.name, + }))} + placeholder="Select event" + /> )} - {/* */} - {showAddFilter && } - - {showSegment && - (event.segment === 'property_average' || - event.segment === 'property_sum') && ( - - )} -
- )} - - {/* Filters */} - {!isSelectManyEvents && } -
- ); - })} - - { - if (isSelectManyEvents) { - dispatch( - addEvent({ - segment: 'user', - name: value, - filters: [ - { - name: 'name', - operator: 'is', - value: [value], - }, - ], - }), + {showDisplayNameInput && ( + { + dispatchChangeEvent({ + ...event, + displayName: e.target.value, + }); + }} + /> + )} + + ); - } else { - dispatch( - addEvent({ - name: value, - segment: 'event', - filters: [], - }), - ); - } - }} - items={eventNames.map((item) => ({ - label: item.name, - value: item.name, - }))} - placeholder="Select event" - /> -
+ })} + + { + if (isSelectManyEvents) { + dispatch( + addEvent({ + segment: 'user', + name: value, + filters: [ + { + name: 'name', + operator: 'is', + value: [value], + }, + ], + }), + ); + } else { + dispatch( + addEvent({ + name: value, + segment: 'event', + filters: [], + }), + ); + } + }} + items={eventNames.map((item) => ({ + label: item.name, + value: item.name, + }))} + placeholder="Select event" + /> +
+ +
); } diff --git a/apps/dashboard/src/components/report/sidebar/filters/FilterItem.tsx b/apps/dashboard/src/components/report/sidebar/filters/FilterItem.tsx index a0d5dd19..763ac217 100644 --- a/apps/dashboard/src/components/report/sidebar/filters/FilterItem.tsx +++ b/apps/dashboard/src/components/report/sidebar/filters/FilterItem.tsx @@ -3,12 +3,11 @@ import { RenderDots } from '@/components/ui/RenderDots'; import { Button } from '@/components/ui/button'; import { ComboboxAdvanced } from '@/components/ui/combobox-advanced'; import { DropdownMenuComposed } from '@/components/ui/dropdown-menu'; +import { InputEnter } from '@/components/ui/input-enter'; import { useAppParams } from '@/hooks/useAppParams'; import { useMappings } from '@/hooks/useMappings'; import { usePropertyValues } from '@/hooks/usePropertyValues'; import { useDispatch, useSelector } from '@/redux'; -import { SlidersHorizontal, Trash } from 'lucide-react'; - import { operators } from '@openpanel/constants'; import type { IChartEvent, @@ -17,8 +16,7 @@ import type { IChartEventFilterValue, } from '@openpanel/validation'; import { mapKeys } from '@openpanel/validation'; - -import { InputEnter } from '@/components/ui/input-enter'; +import { SlidersHorizontal, Trash } from 'lucide-react'; import { changeEvent } from '../../reportSlice'; interface FilterProps { @@ -105,7 +103,7 @@ export function FilterItem({ filter, event }: FilterProps) { onRemove={onRemove} onChangeValue={onChangeValue} onChangeOperator={onChangeOperator} - className="px-4 py-2 shadow-[inset_6px_0_0] shadow-def-200 first:border-t" + className="px-4 py-2 shadow-[inset_6px_0_0] shadow-def-300 first:border-t" /> ); } diff --git a/apps/dashboard/src/components/report/sidebar/filters/FiltersCombobox.tsx b/apps/dashboard/src/components/report/sidebar/filters/FiltersCombobox.tsx index 52f27414..cf6372bd 100644 --- a/apps/dashboard/src/components/report/sidebar/filters/FiltersCombobox.tsx +++ b/apps/dashboard/src/components/report/sidebar/filters/FiltersCombobox.tsx @@ -1,22 +1,58 @@ -import { Combobox } from '@/components/ui/combobox'; +import { Button } from '@/components/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { Input } from '@/components/ui/input'; import { useAppParams } from '@/hooks/useAppParams'; import { useEventProperties } from '@/hooks/useEventProperties'; -import { useDispatch, useSelector } from '@/redux'; -import { FilterIcon } from 'lucide-react'; - +import { useDispatch } from '@/redux'; import { shortId } from '@openpanel/common'; import type { IChartEvent } from '@openpanel/validation'; - +import { AnimatePresence, motion } from 'framer-motion'; +import { FilterIcon } from 'lucide-react'; +import { ArrowLeftIcon, DatabaseIcon, UserIcon } from 'lucide-react'; +import VirtualList from 'rc-virtual-list'; +import { useEffect, useState } from 'react'; import { changeEvent } from '../../reportSlice'; interface FiltersComboboxProps { event: IChartEvent; } +function SearchHeader({ + onBack, + onSearch, + value, +}: { + onBack?: () => void; + onSearch: (value: string) => void; + value: string; +}) { + return ( +
+ {!!onBack && ( + + )} + onSearch(e.target.value)} + /> +
+ ); +} + export function FiltersCombobox({ event }: FiltersComboboxProps) { const dispatch = useDispatch(); const { projectId } = useAppParams(); - + const [open, setOpen] = useState(false); const properties = useEventProperties( { event: event.name, @@ -26,39 +62,219 @@ export function FiltersCombobox({ event }: FiltersComboboxProps) { enabled: !!event.name, }, ); + const [state, setState] = useState<'index' | 'event' | 'profile'>('index'); + const [search, setSearch] = useState(''); + const [direction, setDirection] = useState<'forward' | 'backward'>('forward'); + + useEffect(() => { + if (!open) { + setState('index'); + } + }, [open]); + + // Mock data for the lists + const profileActions = properties + .filter((property) => property.startsWith('profile')) + .map((property) => ({ + value: property, + label: property.split('.').pop() ?? property, + description: property.split('.').slice(0, -1).join('.'), + })); + const eventActions = properties + .filter((property) => !property.startsWith('profile')) + .map((property) => ({ + value: property, + label: property.split('.').pop() ?? property, + description: property.split('.').slice(0, -1).join('.'), + })); + + const handleStateChange = (newState: 'index' | 'event' | 'profile') => { + setDirection(newState === 'index' ? 'backward' : 'forward'); + setState(newState); + }; + + const handleSelect = (action: { + value: string; + label: string; + description: string; + }) => { + setOpen(false); + dispatch( + changeEvent({ + ...event, + filters: [ + ...event.filters, + { + id: shortId(), + name: action.value, + operator: 'is', + value: [], + }, + ], + }), + ); + }; + + const renderIndex = () => { + return ( + + {}} value={search} /> + + { + e.preventDefault(); + handleStateChange('event'); + }} + > + Event properties + + + { + e.preventDefault(); + handleStateChange('profile'); + }} + > + Profile properties + + + + ); + }; + + const renderEvent = () => { + const filteredActions = eventActions.filter( + (action) => + action.label.toLowerCase().includes(search.toLowerCase()) || + action.description.toLowerCase().includes(search.toLowerCase()), + ); + + return ( +
+ handleStateChange('index')} + onSearch={setSearch} + value={search} + /> + + + {(action) => ( + handleSelect(action)} + > +
{action.label}
+
+ {action.description} +
+
+ )} +
+
+ ); + }; + + const renderProfile = () => { + const filteredActions = profileActions.filter( + (action) => + action.label.toLowerCase().includes(search.toLowerCase()) || + action.description.toLowerCase().includes(search.toLowerCase()), + ); + + return ( +
+ handleStateChange('index')} + onSearch={setSearch} + value={search} + /> + + + {(action) => ( + handleSelect(action)} + > +
{action.label}
+
+ {action.description} +
+
+ )} +
+
+ ); + }; return ( - ({ - label: item, - value: item, - }))} - onChange={(value) => { - dispatch( - changeEvent({ - ...event, - filters: [ - ...event.filters, - { - id: shortId(), - name: value, - operator: 'is', - value: [], - }, - ], - }), - ); + { + setOpen(open); }} > - - + + + + + + {state === 'index' && ( + + {renderIndex()} + + )} + {state === 'event' && ( + + {renderEvent()} + + )} + {state === 'profile' && ( + + {renderProfile()} + + )} + + + ); } diff --git a/apps/dashboard/src/components/report/sidebar/filters/FiltersList.tsx b/apps/dashboard/src/components/report/sidebar/filters/FiltersList.tsx index 197feb26..2172c86e 100644 --- a/apps/dashboard/src/components/report/sidebar/filters/FiltersList.tsx +++ b/apps/dashboard/src/components/report/sidebar/filters/FiltersList.tsx @@ -9,7 +9,7 @@ interface ReportEventFiltersProps { export function FiltersList({ event }: ReportEventFiltersProps) { return (
-
+
{event.filters.map((filter) => { return ; })} diff --git a/packages/db/src/services/chart.service.ts b/packages/db/src/services/chart.service.ts index e82fb868..0bc47e93 100644 --- a/packages/db/src/services/chart.service.ts +++ b/packages/db/src/services/chart.service.ts @@ -85,13 +85,13 @@ export function getChartSql({ sb.select.label_0 = `'*' as label_0`; } - // const anyFilterOnProfile = event.filters.some((filter) => - // filter.name.startsWith('profile.properties.'), - // ); + const anyFilterOnProfile = event.filters.some((filter) => + filter.name.startsWith('profile.'), + ); - // if (anyFilterOnProfile) { - // sb.joins.profiles = 'JOIN profiles profile ON e.profile_id = profile.id'; - // } + if (anyFilterOnProfile) { + sb.joins.profiles = `LEFT ANY JOIN (SELECT * FROM ${TABLE_NAMES.profiles} FINAL WHERE project_id = ${escape(projectId)}) as profile on profile.id = profile_id`; + } sb.select.count = 'count(*) as count'; switch (interval) { diff --git a/packages/trpc/src/routers/chart.ts b/packages/trpc/src/routers/chart.ts index ccd2761a..bc332f11 100644 --- a/packages/trpc/src/routers/chart.ts +++ b/packages/trpc/src/routers/chart.ts @@ -3,8 +3,11 @@ import { escape } from 'sqlstring'; import { z } from 'zod'; import { + type IServiceProfile, TABLE_NAMES, + ch, chQuery, + clix, conversionService, createSqlBuilder, db, @@ -77,6 +80,24 @@ export const chartRouter = createTRPCRouter({ }), ) .query(async ({ input: { projectId, event } }) => { + const profiles = await clix(ch) + .select>(['properties']) + .from(TABLE_NAMES.profiles) + .where('project_id', '=', projectId) + .where('is_external', '=', true) + .orderBy('created_at', 'DESC') + .limit(100) + .execute(); + + const profileProperties: string[] = []; + for (const p of profiles) { + for (const property of Object.keys(p.properties)) { + if (!profileProperties.includes(`profile.properties.${property}`)) { + profileProperties.push(`profile.properties.${property}`); + } + } + } + const res = await chQuery<{ property_key: string; created_at: string }>( `SELECT distinct property_key, @@ -116,6 +137,11 @@ export const chartRouter = createTRPCRouter({ 'device', 'brand', 'model', + 'profile.id', + 'profile.first_name', + 'profile.last_name', + 'profile.email', + ...profileProperties, ); return pipe( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2ddc03f1..5e773c1b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -208,6 +208,15 @@ importers: '@clickhouse/client': specifier: ^1.2.0 version: 1.2.0 + '@dnd-kit/core': + specifier: ^6.3.1 + version: 6.3.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@dnd-kit/sortable': + specifier: ^10.0.0 + version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0) + '@dnd-kit/utilities': + specifier: ^3.2.2 + version: 3.2.2(react@18.2.0) '@hookform/resolvers': specifier: ^3.3.4 version: 3.3.4(react-hook-form@7.50.1(react@18.2.0)) @@ -2428,6 +2437,28 @@ packages: '@dabh/diagnostics@2.0.3': resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} + '@dnd-kit/accessibility@3.1.1': + resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} + peerDependencies: + react: '>=16.8.0' + + '@dnd-kit/core@6.3.1': + resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@dnd-kit/sortable@10.0.0': + resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==} + peerDependencies: + '@dnd-kit/core': ^6.3.0 + react: '>=16.8.0' + + '@dnd-kit/utilities@3.2.2': + resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} + peerDependencies: + react: '>=16.8.0' + '@emnapi/core@1.3.1': resolution: {integrity: sha512-pVGjBIt1Y6gg3EJN8jTcfpP/+uuRksIo055oE/OBkDNcjZqVbfkWCksG1Jp4yZnj3iKWyWX8fdG/j6UDYPbFog==} @@ -13127,6 +13158,31 @@ snapshots: enabled: 2.0.0 kuler: 2.0.0 + '@dnd-kit/accessibility@3.1.1(react@18.2.0)': + dependencies: + react: 18.2.0 + tslib: 2.7.0 + + '@dnd-kit/core@6.3.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@dnd-kit/accessibility': 3.1.1(react@18.2.0) + '@dnd-kit/utilities': 3.2.2(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + tslib: 2.7.0 + + '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@dnd-kit/utilities': 3.2.2(react@18.2.0) + react: 18.2.0 + tslib: 2.7.0 + + '@dnd-kit/utilities@3.2.2(react@18.2.0)': + dependencies: + react: 18.2.0 + tslib: 2.7.0 + '@emnapi/core@1.3.1': dependencies: '@emnapi/wasi-threads': 1.0.1