diff --git a/.changeset/breezy-socks-fry.md b/.changeset/breezy-socks-fry.md new file mode 100644 index 000000000..5a8c12299 --- /dev/null +++ b/.changeset/breezy-socks-fry.md @@ -0,0 +1,5 @@ +--- +"@viron/app": patch +--- + +group dnd bug fixes and refactorings diff --git a/packages/app/src/constants/index.ts b/packages/app/src/constants/index.ts index d5676eec1..8a57d70bf 100644 --- a/packages/app/src/constants/index.ts +++ b/packages/app/src/constants/index.ts @@ -285,3 +285,5 @@ export const HTTP_STATUS = { export type HTTPStatus = (typeof HTTP_STATUS)[keyof typeof HTTP_STATUS]; export type HTTPStatusCode = (typeof HTTP_STATUS)[keyof typeof HTTP_STATUS]['code']; + +export const UN_GROUP_ID = '-' as const; diff --git a/packages/app/src/pages/dashboard/endpoints/_/body/index.tsx b/packages/app/src/pages/dashboard/endpoints/_/body/index.tsx index 3434ee07a..43b4aee4e 100644 --- a/packages/app/src/pages/dashboard/endpoints/_/body/index.tsx +++ b/packages/app/src/pages/dashboard/endpoints/_/body/index.tsx @@ -1,5 +1,5 @@ import classnames from 'classnames'; -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import React, { PropsWithChildren, useEffect, useRef } from 'react'; import Sortable from 'sortablejs'; import Button from '~/components/button'; import EndpointsEmptyIcon from '~/components/endpoinitsEmptyIcon'; @@ -7,6 +7,7 @@ import Head from '~/components/head'; import ChevronDownIcon from '~/components/icon/chevronDown/outline'; import ChevronRightIcon from '~/components/icon/chevronRight/outline'; import PlusIcon from '~/components/icon/plus/outline'; +import { UN_GROUP_ID } from '~/constants'; import { useEndpoint, useEndpointGroupToggle } from '~/hooks/endpoint'; import { Trans, useTranslation } from '~/hooks/i18n'; import { Props as LayoutProps } from '~/layouts/'; @@ -19,46 +20,10 @@ import Item from './item/'; export type Props = Parameters[0]; const Body: React.FC = ({ className, style }) => { const { t } = useTranslation(); - const { listByGroup, listUngrouped, setList } = useEndpoint(); + const { listByGroup, listUngrouped } = useEndpoint(); // Add modal. const modal = useModal(); - const sortable = useRef(null); - const listUngroupedRef = React.useRef(null); - - const onSort = useCallback(() => { - if (!sortable.current) { - return; - } - const idArray = sortable.current.toArray(); - const newListUnGrouped = idArray.map((id) => { - // idArray is created from listUngrouped. So, the following line is safe. - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return listUngrouped.find((item) => item.id === id)!; - }); - const listGrouped = listByGroup.flatMap(({ list }) => list); - setList([...listGrouped, ...newListUnGrouped]); - }, [listByGroup, listUngrouped, setList]); - - useEffect(() => { - if (!listUngroupedRef.current) { - return; - } - sortable.current = Sortable.create(listUngroupedRef.current, { - animation: 300, - easing: 'cubic-bezier(1, 0, 0, 1)', - ghostClass: 'opacity-0', - delayOnTouchOnly: true, - delay: 200, - onSort, - }); - return () => { - if (sortable.current) { - sortable.current.destroy(); - } - }; - }, [onSort]); - return ( <>
@@ -92,23 +57,15 @@ const Body: React.FC = ({ className, style }) => { key={item.group.id} className="py-1 border-b border-thm-on-background-faint" > - + + + ))} )} {!!listUngrouped.length && ( -
    - {listUngrouped.map((item) => ( -
  • - -
  • - ))} -
+ )} {!listByGroup.length && !listUngrouped.length && (
@@ -138,53 +95,11 @@ const Body: React.FC = ({ className, style }) => { }; export default Body; -type GroupProps = { +type GroupAccordionProps = PropsWithChildren<{ group: EndpointGroup; - list: Endpoint[]; -}; -const Group: React.FC = ({ group, list }) => { +}>; +const GroupAccordion: React.FC = ({ group, children }) => { const { isOpen, toggle } = useEndpointGroupToggle(group.id); - const { listByGroup, listUngrouped, setList } = useEndpoint(); - - const sortable = React.useRef(null); - const listRef = React.useRef(null); - - const onSort = useCallback(() => { - if (!sortable.current) { - return; - } - const newOrder = sortable.current.toArray(); - const newList = newOrder?.map((id) => { - return list.find((item) => item.id === id)!; - }); - - const otherGroupList = listByGroup - .filter((groupItem) => groupItem.group.id !== group.id) - .flatMap((groupItem) => groupItem.list); - setList([...newList, ...otherGroupList, ...listUngrouped]); - }, [list]); - - useEffect(() => { - if (!listRef.current) { - return; - } - - sortable.current = Sortable.create(listRef.current, { - animation: 300, - easing: 'cubic-bezier(1, 0, 0, 1)', - ghostClass: 'opacity-0', - delayOnTouchOnly: true, - delay: 200, - onSort, - }); - - return () => { - if (sortable.current) { - sortable.current.destroy(); - } - }; - }, []); - const ToggleIcon = isOpen ? ChevronDownIcon : ChevronRightIcon; return ( @@ -209,22 +124,100 @@ const Group: React.FC = ({ group, list }) => { {/* Body */} -
    - {list.map((item) => ( -
  • - -
  • - ))} -
+ {children} +
); }; + +type EndpointListProps = { + groupId: string; + className?: string; + list: Endpoint[]; +}; + +const EndpointList: React.FC = ({ + groupId, + className, + list, +}) => { + const { listByGroup, listUngrouped, setList } = useEndpoint(); + const sortable = useRef(null); + const ref = React.useRef(null); + + useEffect(() => { + if (!ref.current) { + return; + } + + const onSort = () => { + if (!sortable.current) { + return; + } + const idArray = sortable.current.toArray(); + const targetList = + groupId === UN_GROUP_ID + ? listUngrouped + : listByGroup.find((item) => item.group.id === groupId)?.list; + + if (typeof targetList === 'undefined') { + return; + } + + const sortedTargetList = idArray.map( + // idArray is created from listUngrouped. So, the following line is safe. + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + (id) => targetList.find((item) => item.id === id)! + ); + + let newList: Endpoint[]; + if (groupId === UN_GROUP_ID) { + const groupList = listByGroup.flatMap(({ list }) => list); + // add group list + newList = sortedTargetList.concat(groupList); + } else { + const otherGroupList = listByGroup + .filter((groupItem) => groupItem.group.id !== groupId) + .flatMap((groupItem) => groupItem.list); + // add other group list and ungrouped list. + newList = sortedTargetList.concat(otherGroupList).concat(listUngrouped); + } + setList(newList); + }; + + sortable.current = Sortable.create(ref.current, { + animation: 300, + easing: 'cubic-bezier(1, 0, 0, 1)', + ghostClass: 'opacity-0', + delayOnTouchOnly: true, + delay: 200, + onSort, + }); + return () => { + if (sortable.current) { + sortable.current.destroy(); + } + }; + }, [groupId, listByGroup, listUngrouped, setList]); + + return ( +
    + {list.map((item) => ( +
  • + +
  • + ))} +
+ ); +};