Add support for ungrouping tiles and refactor tile drag functions
This commit is contained in:
parent
456f91c9ae
commit
57aafce938
@ -1,10 +1,14 @@
|
||||
import React from "react";
|
||||
import { useDroppable } from "@dnd-kit/core";
|
||||
|
||||
function Droppable({ id, children, disabled }) {
|
||||
function Droppable({ id, children, disabled, ...props }) {
|
||||
const { setNodeRef } = useDroppable({ id, disabled });
|
||||
|
||||
return <div ref={setNodeRef}>{children}</div>;
|
||||
return (
|
||||
<div ref={setNodeRef} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Droppable.defaultProps = {
|
||||
|
@ -9,15 +9,11 @@ import { getGroupItems } from "../../helpers/group";
|
||||
|
||||
import { useGroup } from "../../contexts/GroupContext";
|
||||
|
||||
function MapTiles({ maps, onMapEdit, onMapSelect, subgroup, columns }) {
|
||||
function MapTiles({ maps, onMapEdit, onMapSelect, subgroup }) {
|
||||
const {
|
||||
groups,
|
||||
selectedGroupIds,
|
||||
openGroupId,
|
||||
openGroupItems,
|
||||
selectMode,
|
||||
onGroupOpen,
|
||||
onGroupsChange,
|
||||
onGroupSelect,
|
||||
} = useGroup();
|
||||
|
||||
@ -57,17 +53,7 @@ function MapTiles({ maps, onMapEdit, onMapSelect, subgroup, columns }) {
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SortableTiles
|
||||
groups={subgroup ? openGroupItems : groups}
|
||||
selectedGroupIds={selectedGroupIds}
|
||||
onGroupChange={onGroupsChange}
|
||||
renderTile={renderTile}
|
||||
onTileSelect={onGroupSelect}
|
||||
disableGrouping={subgroup}
|
||||
openGroupId={openGroupId}
|
||||
/>
|
||||
);
|
||||
return <SortableTiles renderTile={renderTile} subgroup={subgroup} />;
|
||||
}
|
||||
|
||||
export default MapTiles;
|
||||
|
@ -4,7 +4,16 @@ import { useDroppable } from "@dnd-kit/core";
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { animated, useSpring } from "react-spring";
|
||||
|
||||
function SortableTile({ id, disableGrouping, hidden, children, isDragging }) {
|
||||
import { GROUP_ID_PREFIX } from "../../contexts/TileDragContext";
|
||||
|
||||
function SortableTile({
|
||||
id,
|
||||
disableGrouping,
|
||||
disableSorting,
|
||||
hidden,
|
||||
children,
|
||||
isDragging,
|
||||
}) {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
@ -12,9 +21,10 @@ function SortableTile({ id, disableGrouping, hidden, children, isDragging }) {
|
||||
setDraggableNodeRef,
|
||||
over,
|
||||
active,
|
||||
} = useSortable({ id });
|
||||
} = useSortable({ id, disabled: disableSorting });
|
||||
|
||||
const { setNodeRef: setGroupNodeRef } = useDroppable({
|
||||
id: `__group__${id}`,
|
||||
id: `${GROUP_ID_PREFIX}${id}`,
|
||||
disabled: disableGrouping,
|
||||
});
|
||||
|
||||
@ -44,7 +54,9 @@ function SortableTile({ id, disableGrouping, hidden, children, isDragging }) {
|
||||
borderWidth: "4px",
|
||||
borderRadius: "4px",
|
||||
borderStyle:
|
||||
over?.id === `__group__${id}` && active.id !== id ? "solid" : "none",
|
||||
over?.id === `${GROUP_ID_PREFIX}${id}` && active.id !== id
|
||||
? "solid"
|
||||
: "none",
|
||||
};
|
||||
|
||||
const { opacity } = useSpring({ opacity: hidden ? 0 : 1 });
|
||||
|
@ -1,88 +1,43 @@
|
||||
import React, { useState } from "react";
|
||||
import React from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import {
|
||||
DndContext,
|
||||
DragOverlay,
|
||||
MouseSensor,
|
||||
TouchSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
closestCenter,
|
||||
} from "@dnd-kit/core";
|
||||
import { DragOverlay } from "@dnd-kit/core";
|
||||
import { SortableContext } from "@dnd-kit/sortable";
|
||||
import { animated, useSpring, config } from "react-spring";
|
||||
import { Badge } from "theme-ui";
|
||||
|
||||
import { moveGroupsInto, moveGroups } from "../../helpers/group";
|
||||
import { moveGroupsInto } from "../../helpers/group";
|
||||
import { keyBy } from "../../helpers/shared";
|
||||
import Vector2 from "../../helpers/Vector2";
|
||||
|
||||
import SortableTile from "./SortableTile";
|
||||
|
||||
function SortableTiles({
|
||||
groups,
|
||||
selectedGroupIds,
|
||||
onGroupChange,
|
||||
renderTile,
|
||||
onTileSelect,
|
||||
disableGrouping,
|
||||
import {
|
||||
useTileDrag,
|
||||
BASE_SORTABLE_ID,
|
||||
GROUP_SORTABLE_ID,
|
||||
GROUP_ID_PREFIX,
|
||||
} from "../../contexts/TileDragContext";
|
||||
import { useGroup } from "../../contexts/GroupContext";
|
||||
|
||||
function SortableTiles({ renderTile, subgroup }) {
|
||||
const { dragId, overId } = useTileDrag();
|
||||
const {
|
||||
groups: allGroups,
|
||||
selectedGroupIds: allSelectedIds,
|
||||
openGroupId,
|
||||
}) {
|
||||
const mouseSensor = useSensor(MouseSensor, {
|
||||
activationConstraint: { delay: 250, tolerance: 5 },
|
||||
});
|
||||
const touchSensor = useSensor(TouchSensor, {
|
||||
activationConstraint: { delay: 250, tolerance: 5 },
|
||||
});
|
||||
openGroupItems,
|
||||
} = useGroup();
|
||||
|
||||
const sensors = useSensors(mouseSensor, touchSensor);
|
||||
const sortableId = subgroup ? GROUP_SORTABLE_ID : BASE_SORTABLE_ID;
|
||||
|
||||
const [dragId, setDragId] = useState();
|
||||
const [overId, setOverId] = useState();
|
||||
|
||||
function handleDragStart({ active, over }) {
|
||||
setDragId(active.id);
|
||||
setOverId(over?.id);
|
||||
if (!selectedGroupIds.includes(active.id)) {
|
||||
onTileSelect(active.id);
|
||||
}
|
||||
}
|
||||
|
||||
function handleDragOver({ over }) {
|
||||
setOverId(over?.id);
|
||||
}
|
||||
|
||||
function handleDragEnd({ active, over }) {
|
||||
setDragId();
|
||||
setOverId();
|
||||
if (!active || !over) {
|
||||
return;
|
||||
}
|
||||
|
||||
let selectedIndices = selectedGroupIds.map((groupId) =>
|
||||
groups.findIndex((group) => group.id === groupId)
|
||||
);
|
||||
// Maintain current group sorting
|
||||
selectedIndices = selectedIndices.sort((a, b) => a - b);
|
||||
|
||||
if (over.id.startsWith("__group__")) {
|
||||
const overId = over.id.slice(9);
|
||||
if (overId === active.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const overGroupIndex = groups.findIndex((group) => group.id === overId);
|
||||
onGroupChange(moveGroupsInto(groups, overGroupIndex, selectedIndices));
|
||||
onTileSelect();
|
||||
} else {
|
||||
if (active.id === over.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const overGroupIndex = groups.findIndex((group) => group.id === over.id);
|
||||
onGroupChange(moveGroups(groups, overGroupIndex, selectedIndices));
|
||||
}
|
||||
const groups = subgroup ? openGroupItems : allGroups;
|
||||
// Only populate selected groups if needed
|
||||
let selectedGroupIds = [];
|
||||
if ((subgroup && openGroupId) || (!subgroup && !openGroupId)) {
|
||||
selectedGroupIds = allSelectedIds;
|
||||
}
|
||||
const disableSorting = openGroupId && !subgroup;
|
||||
const disableGrouping = subgroup || disableSorting;
|
||||
|
||||
const dragBounce = useSpring({
|
||||
transform: !!dragId ? "scale(0.9)" : "scale(1)",
|
||||
@ -91,7 +46,7 @@ function SortableTiles({
|
||||
});
|
||||
|
||||
const overGroupId =
|
||||
overId && overId.startsWith("__group__") && overId.slice(9);
|
||||
overId && overId.startsWith(GROUP_ID_PREFIX) && overId.slice(9);
|
||||
|
||||
function renderSortableGroup(group, selectedGroups) {
|
||||
if (overGroupId === group.id && dragId && group.id !== dragId) {
|
||||
@ -179,6 +134,7 @@ function SortableTiles({
|
||||
id={group.id}
|
||||
key={group.id}
|
||||
disableGrouping={disableTileGrouping}
|
||||
disableSorting={disableSorting}
|
||||
hidden={group.id === openGroupId}
|
||||
isDragging={isDragging}
|
||||
>
|
||||
@ -189,18 +145,10 @@ function SortableTiles({
|
||||
}
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragOver={handleDragOver}
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
>
|
||||
<SortableContext items={groups}>
|
||||
<SortableContext items={groups} id={sortableId}>
|
||||
{renderTiles()}
|
||||
{createPortal(dragId && renderDragOverlays(), document.body)}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,14 @@
|
||||
import React, { useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { Box, Close, Grid, useThemeUI } from "theme-ui";
|
||||
import { useSpring, animated, config } from "react-spring";
|
||||
import ReactResizeDetector from "react-resize-detector";
|
||||
import SimpleBar from "simplebar-react";
|
||||
|
||||
import { useGroup } from "../../contexts/GroupContext";
|
||||
import { UNGROUP_ID_PREFIX } from "../../contexts/TileDragContext";
|
||||
|
||||
import Droppable from "../drag/Droppable";
|
||||
|
||||
import useResponsiveLayout from "../../hooks/useResponsiveLayout";
|
||||
|
||||
@ -27,8 +31,55 @@ function TilesOverlay({ children }) {
|
||||
setContinerSize(size);
|
||||
}
|
||||
|
||||
function renderUngroupBoxes() {
|
||||
return createPortal(
|
||||
<div>
|
||||
<Droppable
|
||||
id={`${UNGROUP_ID_PREFIX}-1`}
|
||||
style={{
|
||||
width: "100vw",
|
||||
height: `calc(50vh - ${containerSize / 2}px)`,
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
}}
|
||||
/>
|
||||
<Droppable
|
||||
id={`${UNGROUP_ID_PREFIX}-2`}
|
||||
style={{
|
||||
width: "100vw",
|
||||
height: `calc(50vh - ${containerSize / 2}px)`,
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
}}
|
||||
/>
|
||||
<Droppable
|
||||
id={`${UNGROUP_ID_PREFIX}-3`}
|
||||
style={{
|
||||
width: `calc(50vw - ${containerSize / 2}px)`,
|
||||
height: "100vh",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
}}
|
||||
/>
|
||||
<Droppable
|
||||
id={`${UNGROUP_ID_PREFIX}-4`}
|
||||
style={{
|
||||
width: `calc(50vw - ${containerSize / 2}px)`,
|
||||
height: "100vh",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
right: 0,
|
||||
}}
|
||||
/>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{openGroupId && renderUngroupBoxes()}
|
||||
{openGroupId && (
|
||||
<Box
|
||||
sx={{
|
||||
|
@ -11,13 +11,9 @@ import { useGroup } from "../../contexts/GroupContext";
|
||||
|
||||
function TokenTiles({ tokens, onTokenEdit, subgroup }) {
|
||||
const {
|
||||
groups,
|
||||
selectedGroupIds,
|
||||
openGroupId,
|
||||
openGroupItems,
|
||||
selectMode,
|
||||
onGroupOpen,
|
||||
onGroupsChange,
|
||||
onGroupSelect,
|
||||
} = useGroup();
|
||||
|
||||
@ -62,17 +58,7 @@ function TokenTiles({ tokens, onTokenEdit, subgroup }) {
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SortableTiles
|
||||
groups={subgroup ? openGroupItems : groups}
|
||||
selectedGroupIds={selectedGroupIds}
|
||||
onGroupChange={onGroupsChange}
|
||||
renderTile={renderTile}
|
||||
onTileSelect={onGroupSelect}
|
||||
disableGrouping={subgroup}
|
||||
openGroupId={openGroupId}
|
||||
/>
|
||||
);
|
||||
return <SortableTiles renderTile={renderTile} subgroup={subgroup} />;
|
||||
}
|
||||
|
||||
export default TokenTiles;
|
||||
|
@ -40,10 +40,13 @@ export function GroupProvider({
|
||||
setOpenGroupId();
|
||||
}
|
||||
|
||||
function handleGroupsChange(newGroups) {
|
||||
if (openGroupId) {
|
||||
// If a group is open then update that group with the new items
|
||||
const groupIndex = groups.findIndex((group) => group.id === openGroupId);
|
||||
/**
|
||||
* @param {string|undefined} groupId The group to apply changes to, leave undefined to replace the full group object
|
||||
*/
|
||||
function handleGroupsChange(newGroups, groupId) {
|
||||
if (groupId) {
|
||||
// If a group is specidifed then update that group with the new items
|
||||
const groupIndex = groups.findIndex((group) => group.id === groupId);
|
||||
let updatedGroups = cloneDeep(groups);
|
||||
const group = updatedGroups[groupIndex];
|
||||
updatedGroups[groupIndex] = { ...group, items: newGroups };
|
||||
|
148
src/contexts/TileDragContext.js
Normal file
148
src/contexts/TileDragContext.js
Normal file
@ -0,0 +1,148 @@
|
||||
import React, { useState, useContext } from "react";
|
||||
import {
|
||||
DndContext,
|
||||
MouseSensor,
|
||||
TouchSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
closestCenter,
|
||||
rectIntersection,
|
||||
} from "@dnd-kit/core";
|
||||
|
||||
import { useGroup } from "./GroupContext";
|
||||
|
||||
import { moveGroupsInto, moveGroups, ungroup } from "../helpers/group";
|
||||
|
||||
const TileDragContext = React.createContext();
|
||||
|
||||
export const BASE_SORTABLE_ID = "__base__";
|
||||
export const GROUP_SORTABLE_ID = "__group__";
|
||||
export const GROUP_ID_PREFIX = "__group__";
|
||||
export const UNGROUP_ID_PREFIX = "__ungroup__";
|
||||
|
||||
export function TileDragProvider({ children }) {
|
||||
const {
|
||||
groups: allGroups,
|
||||
openGroupId,
|
||||
openGroupItems,
|
||||
selectedGroupIds,
|
||||
onGroupsChange,
|
||||
onGroupSelect,
|
||||
onGroupClose,
|
||||
} = useGroup();
|
||||
|
||||
const groupOpen = !!openGroupId;
|
||||
|
||||
const groups = groupOpen ? openGroupItems : allGroups;
|
||||
|
||||
const mouseSensor = useSensor(MouseSensor, {
|
||||
activationConstraint: { delay: 250, tolerance: 5 },
|
||||
});
|
||||
const touchSensor = useSensor(TouchSensor, {
|
||||
activationConstraint: { delay: 250, tolerance: 5 },
|
||||
});
|
||||
|
||||
const sensors = useSensors(mouseSensor, touchSensor);
|
||||
|
||||
const [dragId, setDragId] = useState();
|
||||
const [overId, setOverId] = useState();
|
||||
|
||||
function handleDragStart({ active, over }) {
|
||||
setDragId(active.id);
|
||||
setOverId(over?.id);
|
||||
if (!selectedGroupIds.includes(active.id)) {
|
||||
onGroupSelect(active.id);
|
||||
}
|
||||
}
|
||||
|
||||
function handleDragOver({ over }) {
|
||||
setOverId(over?.id);
|
||||
}
|
||||
|
||||
function handleDragEnd({ active, over }) {
|
||||
setDragId();
|
||||
setOverId();
|
||||
if (!active || !over || active.id === over.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
let selectedIndices = selectedGroupIds.map((groupId) =>
|
||||
groups.findIndex((group) => group.id === groupId)
|
||||
);
|
||||
// Maintain current group sorting
|
||||
selectedIndices = selectedIndices.sort((a, b) => a - b);
|
||||
|
||||
if (over.id.startsWith(GROUP_ID_PREFIX)) {
|
||||
// Handle tile group
|
||||
const overId = over.id.slice(9);
|
||||
if (overId === active.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const overGroupIndex = groups.findIndex((group) => group.id === overId);
|
||||
onGroupsChange(
|
||||
moveGroupsInto(groups, overGroupIndex, selectedIndices),
|
||||
openGroupId
|
||||
);
|
||||
onGroupSelect();
|
||||
} else if (over.id.startsWith(UNGROUP_ID_PREFIX)) {
|
||||
// Handle tile ungroup
|
||||
const newGroups = ungroup(allGroups, openGroupId, selectedIndices);
|
||||
// Close group if it was removed
|
||||
if (!newGroups.find((group) => group.id === openGroupId)) {
|
||||
onGroupClose();
|
||||
}
|
||||
onGroupsChange(newGroups);
|
||||
onGroupSelect();
|
||||
} else {
|
||||
// Hanlde tile move
|
||||
const overGroupIndex = groups.findIndex((group) => group.id === over.id);
|
||||
onGroupsChange(
|
||||
moveGroups(groups, overGroupIndex, selectedIndices),
|
||||
openGroupId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function customCollisionDetection(rects, rect) {
|
||||
if (groupOpen) {
|
||||
const ungroupRects = rects.filter(([id]) =>
|
||||
id.startsWith(UNGROUP_ID_PREFIX)
|
||||
);
|
||||
const intersectingGroupRect = rectIntersection(ungroupRects, rect);
|
||||
if (intersectingGroupRect) {
|
||||
return intersectingGroupRect;
|
||||
}
|
||||
}
|
||||
|
||||
const otherRects = rects.filter(([id]) => id !== UNGROUP_ID_PREFIX);
|
||||
|
||||
return closestCenter(otherRects, rect);
|
||||
}
|
||||
|
||||
const value = { dragId, overId };
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragOver={handleDragOver}
|
||||
sensors={sensors}
|
||||
collisionDetection={customCollisionDetection}
|
||||
>
|
||||
<TileDragContext.Provider value={value}>
|
||||
{children}
|
||||
</TileDragContext.Provider>
|
||||
</DndContext>
|
||||
);
|
||||
}
|
||||
|
||||
export function useTileDrag() {
|
||||
const context = useContext(TileDragContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useTileDrag must be used within a TileDragProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export default TileDragContext;
|
@ -156,6 +156,39 @@ export function moveGroups(groups, to, indices) {
|
||||
return newGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move items from a sub group to the start of the base group
|
||||
* @param {Group[]} groups
|
||||
* @param {string} fromId The id of the group to move from
|
||||
* @param {number[]} indices The indices of the items in the group
|
||||
*/
|
||||
export function ungroup(groups, fromId, indices) {
|
||||
const newGroups = cloneDeep(groups);
|
||||
|
||||
let fromIndex = newGroups.findIndex((group) => group.id === fromId);
|
||||
|
||||
let items = [];
|
||||
for (let i of indices) {
|
||||
items.push(newGroups[fromIndex].items[i]);
|
||||
}
|
||||
|
||||
// Remove items from previous group
|
||||
for (let item of items) {
|
||||
const i = newGroups[fromIndex].items.findIndex((el) => el.id === item.id);
|
||||
newGroups[fromIndex].items.splice(i, 1);
|
||||
}
|
||||
|
||||
// If we have no more items in the group delete it
|
||||
if (newGroups[fromIndex].items.length === 0) {
|
||||
newGroups.splice(fromIndex, 1);
|
||||
}
|
||||
|
||||
// Add to base group
|
||||
newGroups.splice(0, 0, ...items);
|
||||
|
||||
return newGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively find a group within a group array
|
||||
* @param {Group[]} groups
|
||||
|
@ -24,6 +24,7 @@ import { useAuth } from "../contexts/AuthContext";
|
||||
import { useKeyboard } from "../contexts/KeyboardContext";
|
||||
import { useAssets } from "../contexts/AssetsContext";
|
||||
import { GroupProvider } from "../contexts/GroupContext";
|
||||
import { TileDragProvider } from "../contexts/TileDragContext";
|
||||
|
||||
import shortcuts from "../shortcuts";
|
||||
|
||||
@ -259,6 +260,7 @@ function SelectMapModal({
|
||||
onGroupsSelect={setSelectedGroupIds}
|
||||
disabled={!isOpen}
|
||||
>
|
||||
<TileDragProvider>
|
||||
<TilesContainer>
|
||||
<MapTiles
|
||||
maps={maps}
|
||||
@ -266,6 +268,8 @@ function SelectMapModal({
|
||||
onMapSelect={handleMapSelect}
|
||||
/>
|
||||
</TilesContainer>
|
||||
</TileDragProvider>
|
||||
<TileDragProvider>
|
||||
<TilesOverlay>
|
||||
<MapTiles
|
||||
maps={maps}
|
||||
@ -274,6 +278,7 @@ function SelectMapModal({
|
||||
subgroup
|
||||
/>
|
||||
</TilesOverlay>
|
||||
</TileDragProvider>
|
||||
</GroupProvider>
|
||||
</Box>
|
||||
<Button
|
||||
|
@ -24,6 +24,7 @@ import { useAuth } from "../contexts/AuthContext";
|
||||
import { useKeyboard } from "../contexts/KeyboardContext";
|
||||
import { useAssets } from "../contexts/AssetsContext";
|
||||
import { GroupProvider } from "../contexts/GroupContext";
|
||||
import { TileDragProvider } from "../contexts/TileDragContext";
|
||||
|
||||
import shortcuts from "../shortcuts";
|
||||
|
||||
@ -200,12 +201,15 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
|
||||
onGroupsSelect={setSelectedGroupIds}
|
||||
disabled={!isOpen}
|
||||
>
|
||||
<TileDragProvider>
|
||||
<TilesContainer>
|
||||
<TokenTiles
|
||||
tokens={tokens}
|
||||
onTokenEdit={() => setIsEditModalOpen(true)}
|
||||
/>
|
||||
</TilesContainer>
|
||||
</TileDragProvider>
|
||||
<TileDragProvider>
|
||||
<TilesOverlay>
|
||||
<TokenTiles
|
||||
tokens={tokens}
|
||||
@ -213,6 +217,7 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
|
||||
subgroup
|
||||
/>
|
||||
</TilesOverlay>
|
||||
</TileDragProvider>
|
||||
</GroupProvider>
|
||||
</Box>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user