mirror of
https://github.com/unraid/api.git
synced 2026-01-23 17:08:36 -06:00
refactor: simplify organizer operations
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
52
web/src/components/Docker/docker-rename-folder.mutation.ts
Normal file
52
web/src/components/Docker/docker-rename-folder.mutation.ts
Normal 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
@@ -1,2 +1,2 @@
|
||||
export * from './fragment-masking';
|
||||
export * from './gql';
|
||||
export * from "./fragment-masking";
|
||||
export * from "./gql";
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user