Skip to content

Commit

Permalink
Merge pull request #801 from cam-inc/other-group-dnd
Browse files Browse the repository at this point in the history
fix(app): group dnd bug fixes and refactorings
  • Loading branch information
nonoakij committed Feb 7, 2024
2 parents 6f8657a + 54a4f61 commit 42d43dd
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 110 deletions.
5 changes: 5 additions & 0 deletions .changeset/breezy-socks-fry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@viron/app": patch
---

group dnd bug fixes and refactorings
2 changes: 2 additions & 0 deletions packages/app/src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
213 changes: 103 additions & 110 deletions packages/app/src/pages/dashboard/endpoints/_/body/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
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';
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/';
Expand All @@ -19,46 +20,10 @@ import Item from './item/';
export type Props = Parameters<LayoutProps['renderBody']>[0];
const Body: React.FC<Props> = ({ className, style }) => {
const { t } = useTranslation();
const { listByGroup, listUngrouped, setList } = useEndpoint();
const { listByGroup, listUngrouped } = useEndpoint();
// Add modal.
const modal = useModal();

const sortable = useRef<Sortable | null>(null);
const listUngroupedRef = React.useRef<HTMLUListElement>(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 (
<>
<div className={className} style={style}>
Expand Down Expand Up @@ -92,23 +57,15 @@ const Body: React.FC<Props> = ({ className, style }) => {
key={item.group.id}
className="py-1 border-b border-thm-on-background-faint"
>
<Group group={item.group} list={item.list} />
<GroupAccordion group={item.group}>
<EndpointList list={item.list} groupId={item.group.id} />
</GroupAccordion>
</li>
))}
</ul>
)}
{!!listUngrouped.length && (
<ul
ref={listUngroupedRef}
id="list"
className="grid grid-cols-1 @[740px]:grid-cols-2 @[995px]:grid-cols-3 gap-6 py-2"
>
{listUngrouped.map((item) => (
<li className="cursor-grab" key={item.id} data-id={item.id}>
<Item endpoint={item} />
</li>
))}
</ul>
<EndpointList list={listUngrouped} groupId={UN_GROUP_ID} />
)}
{!listByGroup.length && !listUngrouped.length && (
<div className="flex flex-col justify-center items-center py-30 gap-6">
Expand Down Expand Up @@ -138,53 +95,11 @@ const Body: React.FC<Props> = ({ className, style }) => {
};
export default Body;

type GroupProps = {
type GroupAccordionProps = PropsWithChildren<{
group: EndpointGroup;
list: Endpoint[];
};
const Group: React.FC<GroupProps> = ({ group, list }) => {
}>;
const GroupAccordion: React.FC<GroupAccordionProps> = ({ group, children }) => {
const { isOpen, toggle } = useEndpointGroupToggle(group.id);
const { listByGroup, listUngrouped, setList } = useEndpoint();

const sortable = React.useRef<Sortable | null>(null);
const listRef = React.useRef<HTMLUListElement>(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 (
Expand All @@ -209,22 +124,100 @@ const Group: React.FC<GroupProps> = ({ group, list }) => {
</span>
</button>
{/* Body */}
<ul
ref={listRef}
id={'list'}
className={classnames(
'grid grid-cols-1 @[740px]:grid-cols-2 @[995px]:grid-cols-3 gap-6 mt-2 py-2',
{
hidden: !isOpen,
}
)}
<div
className={classnames('mt-2', {
hidden: !isOpen,
})}
>
{list.map((item) => (
<li key={item.id} data-id={item.id}>
<Item endpoint={item} />
</li>
))}
</ul>
{children}
</div>
</div>
);
};

type EndpointListProps = {
groupId: string;
className?: string;
list: Endpoint[];
};

const EndpointList: React.FC<EndpointListProps> = ({
groupId,
className,
list,
}) => {
const { listByGroup, listUngrouped, setList } = useEndpoint();
const sortable = useRef<Sortable | null>(null);
const ref = React.useRef<HTMLUListElement>(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 (
<ul
ref={ref}
className={classnames(
'grid grid-cols-1 @[740px]:grid-cols-2 @[995px]:grid-cols-3 gap-6 py-2',
className
)}
>
{list.map((item) => (
<li key={item.id} data-id={item.id} className="cursor-grab">
<Item endpoint={item} />
</li>
))}
</ul>
);
};

0 comments on commit 42d43dd

Please sign in to comment.