refactor: simplify organizer operations

This commit is contained in:
Pujit Mehrotra
2025-10-13 12:48:33 -04:00
parent 11ee93cae0
commit fd802ea2d8
17 changed files with 841 additions and 409 deletions

View File

@@ -178,6 +178,9 @@ const organizerRoot = computed(
() => result.value?.docker?.organizer?.views?.[0]?.root as ResolvedOrganizerFolder | undefined
);
const flatEntries = computed(() => result.value?.docker?.organizer?.views?.[0]?.flatEntries || []);
const rootFolderId = computed(() => result.value?.docker?.organizer?.views?.[0]?.root?.id || 'root');
const containers = computed<DockerContainer[]>(() => result.value?.docker?.containers || []);
function findContainerResourceById(
@@ -279,7 +282,8 @@ const isDetailsDisabled = computed(() => props.disabled || isSwitching.value);
</div>
<DockerContainersTable
:containers="containers"
:organizer-root="organizerRoot"
:flat-entries="flatEntries"
:root-folder-id="rootFolderId"
:loading="loading"
:active-id="activeId"
:selected-ids="selectedIds"

View File

@@ -32,6 +32,8 @@ const containers = computed<DockerContainer[]>(() => []);
const organizerRoot = computed(
() => result.value?.docker?.organizer?.views?.[0]?.root as ResolvedOrganizerFolder | undefined
);
const flatEntries = computed(() => result.value?.docker?.organizer?.views?.[0]?.flatEntries || []);
const rootFolderId = computed(() => result.value?.docker?.organizer?.views?.[0]?.root?.id || 'root');
const handleRefresh = async () => {
await refetch({ skipCache: true });
@@ -52,7 +54,8 @@ const handleRefresh = async () => {
<DockerContainersTable
:containers="containers"
:organizer-root="organizerRoot"
:flat-entries="flatEntries"
:root-folder-id="rootFolderId"
:loading="loading"
@created-folder="handleRefresh"
/>

View File

@@ -5,8 +5,10 @@ import { useMutation } from '@vue/apollo-composable';
import BaseTreeTable from '@/components/Common/BaseTreeTable.vue';
import { GET_DOCKER_CONTAINERS } from '@/components/Docker/docker-containers.query';
import { CREATE_DOCKER_FOLDER } from '@/components/Docker/docker-create-folder.mutation';
import { CREATE_DOCKER_FOLDER_WITH_ITEMS } from '@/components/Docker/docker-create-folder-with-items.mutation';
import { DELETE_DOCKER_ENTRIES } from '@/components/Docker/docker-delete-entries.mutation';
import { MOVE_DOCKER_ENTRIES_TO_FOLDER } from '@/components/Docker/docker-move-entries.mutation';
import { MOVE_DOCKER_ITEMS_TO_POSITION } from '@/components/Docker/docker-move-items-to-position.mutation';
import { PAUSE_DOCKER_CONTAINER } from '@/components/Docker/docker-pause-container.mutation';
import { SET_DOCKER_FOLDER_CHILDREN } from '@/components/Docker/docker-set-folder-children.mutation';
import { START_DOCKER_CONTAINER } from '@/components/Docker/docker-start-container.mutation';
@@ -20,8 +22,7 @@ import { useTreeData } from '@/composables/useTreeData';
import type {
DockerContainer,
ResolvedOrganizerEntry,
ResolvedOrganizerFolder,
FlatOrganizerEntry,
} from '@/composables/gql/graphql';
import type { DropEvent } from '@/composables/useDragDrop';
import type { TreeRow } from '@/composables/useTreeData';
@@ -30,7 +31,8 @@ import type { Component } from 'vue';
interface Props {
containers: DockerContainer[];
organizerRoot?: ResolvedOrganizerFolder;
flatEntries?: FlatOrganizerEntry[];
rootFolderId?: string;
loading?: boolean;
compact?: boolean;
activeId?: string | null;
@@ -42,6 +44,7 @@ const props = withDefaults(defineProps<Props>(), {
compact: false,
activeId: null,
selectedIds: () => [],
rootFolderId: 'root',
});
const UButton = resolveComponent('UButton');
@@ -107,49 +110,21 @@ function toContainerTreeRow(
};
}
function buildDockerTreeRow(entry: ResolvedOrganizerEntry): TreeRow<DockerContainer> | null {
if (entry.__typename === 'OrganizerContainerResource') {
const meta = entry.meta as DockerContainer | null | undefined;
const row = toContainerTreeRow(meta, entry.name || undefined);
row.id = entry.id;
row.containerId = meta?.id;
return row;
}
return {
id: entry.id as string,
type: 'container',
name: (entry as unknown as { name?: string }).name || 'Unknown',
state: '',
ports: '',
autoStart: 'Off',
updates: '—',
};
}
const organizerRootRef = computed(() => {
if (!props.organizerRoot) return undefined;
return {
id: props.organizerRoot.id,
name: props.organizerRoot.name,
children: props.organizerRoot.children,
};
});
const flatEntriesRef = computed(() => props.flatEntries);
const containersRef = computed(() => props.containers);
const { treeData, entryParentById, folderChildrenIds, parentById, getRowById } =
const { treeData, entryParentById, folderChildrenIds, parentById, positionById, getRowById } =
useTreeData<DockerContainer>({
organizerRoot: organizerRootRef,
flatEntries: flatEntriesRef,
flatData: containersRef,
buildTreeRow: (entry) => buildDockerTreeRow(entry as ResolvedOrganizerEntry),
buildFlatRow: toContainerTreeRow,
});
const { visibleFolders, expandedFolders, toggleExpandFolder, setExpandedFolders } = useFolderTree({
organizerRoot: organizerRootRef,
flatEntries: flatEntriesRef,
});
const rootFolderId = computed<string>(() => props.organizerRoot?.id || '');
const rootFolderId = computed<string>(() => props.rootFolderId || 'root');
const busyRowIds = ref<Set<string>>(new Set());
function setRowsBusy(ids: string[], busy: boolean) {
@@ -305,7 +280,9 @@ const columnsMenuItems = computed<DropdownMenuItems>(() => {
});
const { mutate: createFolderMutation, loading: creating } = useMutation(CREATE_DOCKER_FOLDER);
const { mutate: createFolderWithItemsMutation } = useMutation(CREATE_DOCKER_FOLDER_WITH_ITEMS);
const { mutate: moveEntriesMutation, loading: moving } = useMutation(MOVE_DOCKER_ENTRIES_TO_FOLDER);
const { mutate: moveItemsToPositionMutation } = useMutation(MOVE_DOCKER_ITEMS_TO_POSITION);
const { mutate: deleteEntriesMutation, loading: deleting } = useMutation(DELETE_DOCKER_ENTRIES);
const { mutate: setFolderChildrenMutation } = useMutation(SET_DOCKER_FOLDER_CHILDREN);
const { mutate: startContainerMutation } = useMutation(START_DOCKER_CONTAINER);
@@ -363,72 +340,6 @@ const confirmToStart = computed(() => containerActions.confirmToStart.value || [
const confirmToPause = computed(() => containerActions.confirmToPause.value || []);
const confirmToResume = computed(() => containerActions.confirmToResume.value || []);
function getFolderChildrenList(folderId: string): string[] {
return [...(folderChildrenIds.value[folderId] || [])];
}
function computeInsertIndex(
children: string[],
targetId: string,
area: 'before' | 'after' | 'inside'
): number {
const idx = Math.max(0, children.indexOf(targetId));
return area === 'before' ? idx : idx + 1;
}
async function reorderWithinFolder(
folderId: string,
movingIds: string[],
targetId: string,
area: 'before' | 'after' | 'inside'
) {
const current = getFolderChildrenList(folderId);
const removeSet = new Set(movingIds);
const filtered = current.filter((id) => !removeSet.has(id));
const insertIndex = computeInsertIndex(filtered, targetId, area);
const finalIds = [
...filtered.slice(0, insertIndex),
...movingIds.filter((id) => id !== targetId),
...filtered.slice(insertIndex),
];
await setFolderChildrenMutation(
{ folderId, childrenIds: finalIds },
{
refetchQueries: [{ query: GET_DOCKER_CONTAINERS, variables: { skipCache: true } }],
awaitRefetchQueries: true,
}
);
}
async function moveAcrossFoldersWithPosition(
destinationFolderId: string,
movingIds: string[],
targetId: string,
area: 'before' | 'after' | 'inside'
) {
await moveEntriesMutation(
{ destinationFolderId, sourceEntryIds: movingIds },
{
refetchQueries: [{ query: GET_DOCKER_CONTAINERS, variables: { skipCache: true } }],
awaitRefetchQueries: true,
}
);
const current = getFolderChildrenList(destinationFolderId).filter((id) => !movingIds.includes(id));
const insertIndex = computeInsertIndex(current, targetId, area);
const finalIds = [
...current.slice(0, insertIndex),
...movingIds.filter((id) => id !== targetId),
...current.slice(insertIndex),
];
await setFolderChildrenMutation(
{ folderId: destinationFolderId, childrenIds: finalIds },
{
refetchQueries: [{ query: GET_DOCKER_CONTAINERS, variables: { skipCache: true } }],
awaitRefetchQueries: true,
}
);
}
async function moveIntoFolder(destinationFolderId: string, movingIds: string[]) {
await moveEntriesMutation(
{ destinationFolderId, sourceEntryIds: movingIds },
@@ -441,33 +352,18 @@ async function moveIntoFolder(destinationFolderId: string, movingIds: string[])
async function createFolderFromDrop(containerEntryId: string, movingIds: string[]) {
const parentId = entryParentById.value[containerEntryId] || rootFolderId.value;
const parentChildren = getFolderChildrenList(parentId);
const targetIndex = parentChildren.indexOf(containerEntryId);
const targetPosition = positionById.value[containerEntryId] ?? 0;
const name = window.prompt('New folder name?')?.trim();
if (!name) return;
await createFolderMutation(
{ name, parentId, childrenIds: [] },
{
refetchQueries: [{ query: GET_DOCKER_CONTAINERS, variables: { skipCache: true } }],
awaitRefetchQueries: true,
}
);
const toMove = [containerEntryId, ...movingIds.filter((id) => id !== containerEntryId)];
await moveEntriesMutation(
{ destinationFolderId: name, sourceEntryIds: toMove },
{
refetchQueries: [{ query: GET_DOCKER_CONTAINERS, variables: { skipCache: true } }],
awaitRefetchQueries: true,
}
);
const updated = getFolderChildrenList(parentId).filter((id) => !toMove.includes(id));
const final = [
...updated.slice(0, Math.max(0, targetIndex)),
name,
...updated.slice(Math.max(0, targetIndex)),
];
await setFolderChildrenMutation(
{ folderId: parentId, childrenIds: final },
await createFolderWithItemsMutation(
{
name,
parentId,
sourceEntryIds: toMove,
position: targetPosition
},
{
refetchQueries: [{ query: GET_DOCKER_CONTAINERS, variables: { skipCache: true } }],
awaitRefetchQueries: true,
@@ -477,7 +373,7 @@ async function createFolderFromDrop(containerEntryId: string, movingIds: string[
}
async function handleDropOnRow(event: DropEvent<DockerContainer>) {
if (!props.organizerRoot) return;
if (!props.flatEntries) return;
const { target, area, sourceIds: movingIds } = event;
if (!movingIds.length) return;
@@ -491,15 +387,22 @@ async function handleDropOnRow(event: DropEvent<DockerContainer>) {
await createFolderFromDrop(target.id, movingIds);
return;
}
const parentId = entryParentById.value[target.id] || rootFolderId.value;
const sameParent = movingIds.every(
(id) => (entryParentById.value[id] || rootFolderId.value) === parentId
const targetPosition = positionById.value[target.id] ?? 0;
const position = area === 'before' ? targetPosition : targetPosition + 1;
await moveItemsToPositionMutation(
{
sourceEntryIds: movingIds,
destinationFolderId: parentId,
position
},
{
refetchQueries: [{ query: GET_DOCKER_CONTAINERS, variables: { skipCache: true } }],
awaitRefetchQueries: true,
}
);
if (sameParent) {
await reorderWithinFolder(parentId, movingIds, target.id, area);
} else {
await moveAcrossFoldersWithPosition(parentId, movingIds, target.id, area);
}
}
function getSelectedEntryIds(): string[] {
@@ -623,7 +526,7 @@ function getRowActionItems(row: TreeRow<DockerContainer>): DropdownMenuItems {
:active-id="activeId"
:selected-ids="selectedIds"
:busy-row-ids="busyRowIds"
:enable-drag-drop="!!organizerRoot"
:enable-drag-drop="!!flatEntries"
@row:click="
(payload) =>
emit('row:click', {

View File

@@ -15,126 +15,36 @@ export const GET_DOCKER_CONTAINERS = gql`
id
name
type
children {
__typename
... on ResolvedOrganizerFolder {
id
name
type
children {
__typename
... on ResolvedOrganizerFolder {
id
name
type
children {
__typename
... on ResolvedOrganizerFolder {
id
name
type
}
... on OrganizerContainerResource {
id
name
type
meta {
id
names
state
status
image
ports {
privatePort
publicPort
type
}
autoStart
hostConfig {
networkMode
}
created
isUpdateAvailable
isRebuildReady
}
}
}
}
... on OrganizerContainerResource {
id
name
type
meta {
id
names
state
status
image
ports {
privatePort
publicPort
type
}
autoStart
hostConfig {
networkMode
}
created
isUpdateAvailable
isRebuildReady
}
}
}
}
... on OrganizerContainerResource {
id
name
type
meta {
id
names
state
status
image
ports {
privatePort
publicPort
type
}
autoStart
hostConfig {
networkMode
}
created
isUpdateAvailable
isRebuildReady
}
}
}
}
... on OrganizerContainerResource {
}
flatEntries {
id
type
name
parentId
depth
position
path
hasChildren
childrenIds
meta {
id
name
type
meta {
id
names
state
status
image
ports {
privatePort
publicPort
type
}
autoStart
hostConfig {
networkMode
}
created
isUpdateAvailable
isRebuildReady
names
state
status
image
ports {
privatePort
publicPort
type
}
autoStart
hostConfig {
networkMode
}
created
isUpdateAvailable
isRebuildReady
}
}
}

View File

@@ -0,0 +1,62 @@
import { gql } from '@apollo/client';
export const CREATE_DOCKER_FOLDER_WITH_ITEMS = gql`
mutation CreateDockerFolderWithItems(
$name: String!
$parentId: String
$sourceEntryIds: [String!]
$position: Float
) {
createDockerFolderWithItems(
name: $name
parentId: $parentId
sourceEntryIds: $sourceEntryIds
position: $position
) {
version
views {
id
name
root {
__typename
... on ResolvedOrganizerFolder {
id
name
type
}
}
flatEntries {
id
type
name
parentId
depth
position
path
hasChildren
childrenIds
meta {
id
names
state
status
image
ports {
privatePort
publicPort
type
}
autoStart
hostConfig {
networkMode
}
created
isUpdateAvailable
isRebuildReady
}
}
}
}
}
`;

View File

@@ -0,0 +1,60 @@
import { gql } from '@apollo/client';
export const MOVE_DOCKER_ITEMS_TO_POSITION = gql`
mutation MoveDockerItemsToPosition(
$sourceEntryIds: [String!]!
$destinationFolderId: String!
$position: Float!
) {
moveDockerItemsToPosition(
sourceEntryIds: $sourceEntryIds
destinationFolderId: $destinationFolderId
position: $position
) {
version
views {
id
name
root {
__typename
... on ResolvedOrganizerFolder {
id
name
type
}
}
flatEntries {
id
type
name
parentId
depth
position
path
hasChildren
childrenIds
meta {
id
names
state
status
image
ports {
privatePort
publicPort
type
}
autoStart
hostConfig {
networkMode
}
created
isUpdateAvailable
isRebuildReady
}
}
}
}
}
`;

View File

@@ -0,0 +1,52 @@
import { gql } from '@apollo/client';
export const RENAME_DOCKER_FOLDER = gql`
mutation RenameDockerFolder($folderId: String!, $newName: String!) {
renameDockerFolder(folderId: $folderId, newName: $newName) {
version
views {
id
name
root {
__typename
... on ResolvedOrganizerFolder {
id
name
type
}
}
flatEntries {
id
type
name
parentId
depth
position
path
hasChildren
childrenIds
meta {
id
names
state
status
image
ports {
privatePort
publicPort
type
}
autoStart
hostConfig {
networkMode
}
created
isUpdateAvailable
isRebuildReady
}
}
}
}
}
`;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,2 +1,2 @@
export * from './fragment-masking';
export * from './gql';
export * from "./fragment-masking";
export * from "./gql";

View File

@@ -1,14 +1,8 @@
import { computed, ref, unref } from 'vue';
import type { OrganizerEntry } from '@/composables/useTreeData';
import type { FlatOrganizerEntry } from '@/composables/gql/graphql';
import type { MaybeRef } from 'vue';
export interface FolderNode {
id: string;
name: string;
children: FolderNode[];
}
export interface FlatFolderRow {
id: string;
name: string;
@@ -17,61 +11,47 @@ export interface FlatFolderRow {
}
export interface FolderTreeOptions {
organizerRoot?: MaybeRef<{ id: string; name?: string; children?: OrganizerEntry[] } | undefined>;
flatEntries?: MaybeRef<FlatOrganizerEntry[] | undefined>;
}
export function useFolderTree(options: FolderTreeOptions) {
const { organizerRoot } = options;
const { flatEntries } = options;
const expandedFolders = ref<Set<string>>(new Set());
function buildFolderOnlyTree(
entry?: { id: string; name?: string; children?: OrganizerEntry[] } | null
): FolderNode | null {
if (!entry) return null;
const folders: FolderNode[] = [];
for (const child of entry.children || []) {
if ((child as OrganizerEntry).__typename?.includes('Folder')) {
const sub = buildFolderOnlyTree(
child as { id: string; name?: string; children?: OrganizerEntry[] }
);
if (sub) folders.push(sub);
}
}
return { id: entry.id, name: entry.name || 'Unnamed', children: folders };
}
const folderTree = computed<FolderNode | null>(() => {
return buildFolderOnlyTree(unref(organizerRoot));
const allFolders = computed<FlatOrganizerEntry[]>(() => {
const entries = unref(flatEntries);
if (!entries) return [];
return entries.filter((e) => e.type === 'folder');
});
function flattenVisibleFolders(
node: FolderNode | null,
depth = 0,
out: FlatFolderRow[] = []
): FlatFolderRow[] {
if (!node) return out;
const visibleFolders = computed<FlatFolderRow[]>(() => {
const folders = allFolders.value;
const visible: FlatFolderRow[] = [];
const expanded = expandedFolders.value;
out.push({
id: node.id,
name: node.name,
depth,
hasChildren: node.children.length > 0,
});
const visibleIds = new Set<string>();
if (expandedFolders.value.has(node.id)) {
for (const child of node.children) {
flattenVisibleFolders(child, depth + 1, out);
for (const folder of folders) {
if (!folder.parentId) {
visibleIds.add(folder.id);
} else if (visibleIds.has(folder.parentId) && expanded.has(folder.parentId)) {
visibleIds.add(folder.id);
}
}
return out;
}
for (const folder of folders) {
if (visibleIds.has(folder.id)) {
visible.push({
id: folder.id,
name: folder.name,
depth: folder.depth,
hasChildren: folder.hasChildren,
});
}
}
const visibleFolders = computed<FlatFolderRow[]>(() => {
return flattenVisibleFolders(folderTree.value);
return visible;
});
function toggleExpandFolder(id: string) {
@@ -94,7 +74,6 @@ export function useFolderTree(options: FolderTreeOptions) {
}
return {
folderTree,
visibleFolders,
expandedFolders,
toggleExpandFolder,

View File

@@ -1,5 +1,6 @@
import { computed, unref } from 'vue';
import type { FlatOrganizerEntry } from '@/composables/gql/graphql';
import type { MaybeRef } from 'vue';
export interface TreeRow<T = unknown> {
@@ -15,112 +16,88 @@ export interface TreeRow<T = unknown> {
containerId?: string;
}
export interface OrganizerEntry {
__typename?: string;
id: string;
name?: string;
children?: OrganizerEntry[];
meta?: unknown;
type?: string;
}
export interface TreeDataOptions<T> {
organizerRoot?: MaybeRef<{ id: string; children?: OrganizerEntry[] } | undefined>;
flatEntries?: MaybeRef<FlatOrganizerEntry[] | undefined>;
flatData?: MaybeRef<T[]>;
buildTreeRow: (entry: OrganizerEntry) => TreeRow<T> | null;
buildFlatRow?: (item: T) => TreeRow<T>;
}
export function useTreeData<T = unknown>(options: TreeDataOptions<T>) {
const { organizerRoot, flatData, buildTreeRow, buildFlatRow } = options;
function buildTree(entry: OrganizerEntry): TreeRow<T> | null {
if (entry.__typename?.includes('Folder')) {
const children = (entry.children || [])
.map((child) => buildTree(child))
.filter(Boolean) as TreeRow<T>[];
return {
id: entry.id,
type: 'folder',
name: entry.name || 'Unnamed',
children,
};
}
return buildTreeRow(entry);
}
const { flatEntries, flatData, buildFlatRow } = options;
const treeData = computed<TreeRow<T>[]>(() => {
const root = unref(organizerRoot);
const flat = unref(flatData);
const flat = unref(flatEntries);
const fallbackFlat = unref(flatData);
if (root) {
return (root.children || []).map((child) => buildTree(child)).filter(Boolean) as TreeRow<T>[];
if (flat && flat.length > 0) {
const entriesById = new Map(flat.map((e) => [e.id, e]));
const rootEntries: TreeRow<T>[] = [];
function buildTreeFromFlat(entry: FlatOrganizerEntry): TreeRow<T> {
const row: TreeRow<T> = {
id: entry.id,
type: entry.type,
name: entry.name,
meta: entry.meta as T,
children: [],
};
if (entry.hasChildren) {
row.children = entry.childrenIds
.map((childId) => entriesById.get(childId))
.filter(Boolean)
.map((child) => buildTreeFromFlat(child!));
}
return row;
}
for (const entry of flat) {
if (!entry.parentId) {
rootEntries.push(buildTreeFromFlat(entry));
}
}
return rootEntries;
}
if (flat && buildFlatRow) {
return flat.map(buildFlatRow);
if (fallbackFlat && buildFlatRow) {
return fallbackFlat.map(buildFlatRow);
}
return [];
});
const entryParentById = computed<Record<string, string>>(() => {
const map: Record<string, string> = {};
const root = unref(organizerRoot);
function walk(node?: { id: string; children?: OrganizerEntry[] } | null) {
if (!node) return;
for (const child of node.children || []) {
const id = (child as { id?: string }).id;
if (id) map[id] = node.id;
if ((child as OrganizerEntry).__typename?.includes('Folder')) {
walk(child as { id: string; children?: OrganizerEntry[] });
}
}
}
walk(root);
return map;
const entries = unref(flatEntries);
if (!entries) return {};
return Object.fromEntries(
entries.filter((e) => e.parentId).map((e) => [e.id, e.parentId!])
);
});
const folderChildrenIds = computed<Record<string, string[]>>(() => {
const map: Record<string, string[]> = {};
const root = unref(organizerRoot);
function walk(node?: { id: string; children?: OrganizerEntry[] } | null) {
if (!node) return;
map[node.id] = (node.children || []).map((c) => (c as { id: string }).id);
for (const child of node.children || []) {
if ((child as OrganizerEntry).__typename?.includes('Folder')) {
walk(child as { id: string; children?: OrganizerEntry[] });
}
}
}
walk(root);
return map;
const entries = unref(flatEntries);
if (!entries) return {};
return Object.fromEntries(
entries.filter((e) => e.type === 'folder').map((e) => [e.id, e.childrenIds])
);
});
const parentById = computed<Record<string, string>>(() => {
const map: Record<string, string> = {};
const root = unref(organizerRoot);
const entries = unref(flatEntries);
if (!entries) return {};
return Object.fromEntries(
entries
.filter((e) => e.type === 'folder' && e.parentId)
.map((e) => [e.id, e.parentId!])
);
});
function walk(node?: { id: string; children?: OrganizerEntry[] } | null, parentId?: string) {
if (!node) return;
if (parentId) map[node.id] = parentId;
for (const child of node.children || []) {
if ((child as OrganizerEntry).__typename?.includes('Folder')) {
walk(child as { id: string; children?: OrganizerEntry[] }, node.id);
}
}
}
walk(root, undefined);
return map;
const positionById = computed<Record<string, number>>(() => {
const entries = unref(flatEntries);
if (!entries) return {};
return Object.fromEntries(entries.map((e) => [e.id, e.position]));
});
function flattenRows(rows: TreeRow<T>[], filterType?: string): TreeRow<T>[] {
@@ -150,6 +127,7 @@ export function useTreeData<T = unknown>(options: TreeDataOptions<T>) {
entryParentById,
folderChildrenIds,
parentById,
positionById,
flattenRows,
getRowById,
};