From ac09bb1151e04110b868bb937b971bf9fcd37829 Mon Sep 17 00:00:00 2001 From: shu20031026 Date: Tue, 30 Jan 2024 17:17:06 +0900 Subject: [PATCH 1/3] fix(app): group dnd bug fixes and refactorings --- .../dashboard/endpoints/_/body/index.tsx | 154 +++++++++--------- 1 file changed, 75 insertions(+), 79 deletions(-) diff --git a/packages/app/src/pages/dashboard/endpoints/_/body/index.tsx b/packages/app/src/pages/dashboard/endpoints/_/body/index.tsx index 3434ee07a..9ada98725 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, { useEffect, useRef } from 'react'; import Sortable from 'sortablejs'; import Button from '~/components/button'; import EndpointsEmptyIcon from '~/components/endpoinitsEmptyIcon'; @@ -16,35 +16,53 @@ import Menu from '../../../_/menu'; import Add from './add/'; import Item from './item/'; -export type Props = Parameters[0]; -const Body: React.FC = ({ className, style }) => { - const { t } = useTranslation(); - const { listByGroup, listUngrouped, setList } = useEndpoint(); - // Add modal. - const modal = useModal(); +const UN_GROUP_ID = '-'; +const useEndpointDnD = ({ groupId }: { groupId: string }) => { + const { listByGroup, listUngrouped, setList } = useEndpoint(); 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]); + const listRef = React.useRef(null); useEffect(() => { - if (!listUngroupedRef.current) { + if (!listRef.current) { return; } - sortable.current = Sortable.create(listUngroupedRef.current, { + + const onSort = () => { + if (!sortable.current) { + return; + } + const idArray = sortable.current.toArray(); + + if (groupId === UN_GROUP_ID) { + // UnGroup + 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]); + } else { + // Group + const listBeforeSort = listByGroup.find( + (item) => item.group.id === groupId + )?.list; + if (typeof listBeforeSort === 'undefined') { + return; + } + const newListGrouped = idArray.map((id) => { + return listBeforeSort.find((item) => item.id === id)!; + }); + + const otherGroupList = listByGroup + .filter((groupItem) => groupItem.group.id !== groupId) + .flatMap((groupItem) => groupItem.list); + setList([...newListGrouped, ...otherGroupList, ...listUngrouped]); + } + }; + + sortable.current = Sortable.create(listRef.current, { animation: 300, easing: 'cubic-bezier(1, 0, 0, 1)', ghostClass: 'opacity-0', @@ -57,7 +75,16 @@ const Body: React.FC = ({ className, style }) => { sortable.current.destroy(); } }; - }, [onSort]); + }, [listByGroup, listUngrouped, setList]); + return listRef; +}; + +export type Props = Parameters[0]; +const Body: React.FC = ({ className, style }) => { + const { t } = useTranslation(); + const { listByGroup, listUngrouped } = useEndpoint(); + // Add modal. + const modal = useModal(); return ( <> @@ -97,19 +124,7 @@ const Body: React.FC = ({ className, style }) => { ))} )} - {!!listUngrouped.length && ( -
    - {listUngrouped.map((item) => ( -
  • - -
  • - ))} -
- )} + {!!listUngrouped.length && } {!listByGroup.length && !listUngrouped.length && (
= ({ group, list }) => { 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 listRef = useEndpointDnD({ groupId: group.id }); const ToggleIcon = isOpen ? ChevronDownIcon : ChevronRightIcon; @@ -220,7 +196,7 @@ const Group: React.FC = ({ group, list }) => { )} > {list.map((item) => ( -
  • +
  • ))} @@ -228,3 +204,23 @@ const Group: React.FC = ({ group, list }) => {
    ); }; + +type UnGroupProps = { + list: Endpoint[]; +}; +const UnGroup: React.FC = ({ list }) => { + const listRef = useEndpointDnD({ groupId: UN_GROUP_ID }); + + return ( +
      + {list.map((item) => ( +
    • + +
    • + ))} +
    + ); +}; From ea7376cede37dac6fcc270e36e485b1f2bc47194 Mon Sep 17 00:00:00 2001 From: shu20031026 <75605907+shu20031026@users.noreply.github.com> Date: Tue, 30 Jan 2024 17:24:25 +0900 Subject: [PATCH 2/3] Create breezy-socks-fry.md --- .changeset/breezy-socks-fry.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/breezy-socks-fry.md 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 From 54a4f6127176c625fdc9f3b85c935e36f2346e3f Mon Sep 17 00:00:00 2001 From: nonoakij <45055030+nonoakij@users.noreply.github.com> Date: Wed, 7 Feb 2024 14:21:07 +0900 Subject: [PATCH 3/3] feat(app): dnd group item --- packages/app/src/constants/index.ts | 2 + .../dashboard/endpoints/_/body/index.tsx | 181 +++++++++--------- 2 files changed, 91 insertions(+), 92 deletions(-) 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 9ada98725..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, { useEffect, useRef } 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/'; @@ -16,69 +17,6 @@ import Menu from '../../../_/menu'; import Add from './add/'; import Item from './item/'; -const UN_GROUP_ID = '-'; - -const useEndpointDnD = ({ groupId }: { groupId: string }) => { - const { listByGroup, listUngrouped, setList } = useEndpoint(); - const sortable = useRef(null); - const listRef = React.useRef(null); - - useEffect(() => { - if (!listRef.current) { - return; - } - - const onSort = () => { - if (!sortable.current) { - return; - } - const idArray = sortable.current.toArray(); - - if (groupId === UN_GROUP_ID) { - // UnGroup - 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]); - } else { - // Group - const listBeforeSort = listByGroup.find( - (item) => item.group.id === groupId - )?.list; - if (typeof listBeforeSort === 'undefined') { - return; - } - const newListGrouped = idArray.map((id) => { - return listBeforeSort.find((item) => item.id === id)!; - }); - - const otherGroupList = listByGroup - .filter((groupItem) => groupItem.group.id !== groupId) - .flatMap((groupItem) => groupItem.list); - setList([...newListGrouped, ...otherGroupList, ...listUngrouped]); - } - }; - - 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(); - } - }; - }, [listByGroup, listUngrouped, setList]); - return listRef; -}; - export type Props = Parameters[0]; const Body: React.FC = ({ className, style }) => { const { t } = useTranslation(); @@ -119,12 +57,16 @@ const Body: React.FC = ({ className, style }) => { key={item.group.id} className="py-1 border-b border-thm-on-background-faint" > - + + + ))} )} - {!!listUngrouped.length && } + {!!listUngrouped.length && ( + + )} {!listByGroup.length && !listUngrouped.length && (
    = ({ 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 listRef = useEndpointDnD({ groupId: group.id }); - const ToggleIcon = isOpen ? ChevronDownIcon : ChevronRightIcon; return ( @@ -185,36 +124,94 @@ const Group: React.FC = ({ group, list }) => { {/* Body */} -
      - {list.map((item) => ( -
    • - -
    • - ))} -
    + {children} +
    ); }; -type UnGroupProps = { +type EndpointListProps = { + groupId: string; + className?: string; list: Endpoint[]; }; -const UnGroup: React.FC = ({ list }) => { - const listRef = useEndpointDnD({ groupId: UN_GROUP_ID }); + +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) => (