import { create } from "zustand"; import { immer } from "zustand/middleware/immer"; interface AssetGroupStore { assetGroups: AssetGroup[]; selectedGroups: string[]; // Array of groupUuids groupHierarchy: AssetGroupHierarchy; // Group CRUD operations addGroup: (group: AssetGroup) => { createdGroups: AssetGroup[]; updatedGroups: AssetGroup[] }; removeGroup: (groupUuid: string) => void; clearGroups: () => void; setGroups: (groups: AssetGroup[]) => void; // Group selection setSelectedGroups: (groupUuids: string[]) => void; addSelectedGroup: (groupUuid: string) => void; removeSelectedGroup: (groupUuid: string) => void; toggleSelectedGroup: (groupUuid: string) => void; clearSelectedGroups: () => void; hasSelectedGroup: (groupUuid: string) => boolean; // Group children management addChildToGroup: (groupUuid: string, child: { type: "Asset" | "Group"; childrenUuid: string }) => { updatedGroups: AssetGroup[] }; removeChildFromGroup: (groupUuid: string, childUuid: string) => void; getGroupChildren: (groupUuid: string) => { type: "Asset" | "Group"; childrenUuid: string }[]; // Group properties setGroupName: (groupUuid: string, newName: string) => void; setGroupVisibility: (groupUuid: string, isVisible: boolean) => void; setGroupLock: (groupUuid: string, isLocked: boolean) => void; toggleGroupVisibility: (groupUuid: string) => void; setGroupExpanded: (groupUuid: string, isExpanded: boolean) => void; // Hierarchy operations buildHierarchy: (assets: Assets, groups: AssetGroup[]) => AssetGroupHierarchy; flattenHierarchy: (hierarchy: AssetGroupHierarchy) => { assets: Asset[]; groups: AssetGroup[] }; getGroupHierarchy: (groupUuid: string) => AssetGroupHierarchyNode | null; getFlatGroupAssets: (groupUuid: string, assets: Assets) => Asset[]; getFlatGroupChildren: (groupUuid: string) => string[]; // Returns all child uuids (both assets and groups) // Helper functions getGroupById: (groupUuid: string) => AssetGroup | undefined; getGroupsContainingAsset: (assetUuid: string) => AssetGroup[]; getGroupsContainingGroup: (childGroupUuid: string) => AssetGroup[]; hasGroup: (groupUuid: string) => boolean; isGroup: (item: AssetGroupChild) => item is AssetGroupHierarchyNode; isEmptyGroup: (groupUuid: string) => boolean; } export const createAssetGroupStore = () => { return create()( immer((set, get) => ({ assetGroups: [], selectedGroups: [], groupHierarchy: [], // Group CRUD operations addGroup: (group) => { const result: { createdGroups: AssetGroup[]; updatedGroups: AssetGroup[] } = { createdGroups: [], updatedGroups: [], }; set((state) => { // Check if group already exists if (state.assetGroups.some((g) => g.groupUuid === group.groupUuid)) { return; } // Find all asset children in the new group const assetChildren = group.childrens.filter((child) => child.type === "Asset"); const assetUuids = new Set(assetChildren.map((child) => child.childrenUuid)); // Remove these assets from existing groups and track updated groups const updatedGroups: AssetGroup[] = []; state.assetGroups.forEach((existingGroup) => { const originalLength = existingGroup.childrens.length; existingGroup.childrens = existingGroup.childrens.filter((child) => !(child.type === "Asset" && assetUuids.has(child.childrenUuid))); // If group was modified, add to updated groups if (existingGroup.childrens.length !== originalLength) { updatedGroups.push({ ...existingGroup }); } }); // Add the new group state.assetGroups.push(group); result.createdGroups.push({ ...group }); result.updatedGroups = updatedGroups; }); return result; }, removeGroup: (groupUuid) => { set((state) => { // First remove this group from any parent groups state.assetGroups.forEach((group) => { group.childrens = group.childrens.filter((child) => !(child.type === "Group" && child.childrenUuid === groupUuid)); }); // Then remove the group itself state.assetGroups = state.assetGroups.filter((g) => g.groupUuid !== groupUuid); // Remove from selected groups state.selectedGroups = state.selectedGroups.filter((uuid) => uuid !== groupUuid); }); }, clearGroups: () => { set((state) => { state.assetGroups = []; state.selectedGroups = []; state.groupHierarchy = []; }); }, setGroups: (groups) => { set((state) => { state.assetGroups = groups; }); }, // Group selection setSelectedGroups: (groupUuids) => { set((state) => { state.selectedGroups = groupUuids; }); }, addSelectedGroup: (groupUuid) => { set((state) => { if (!state.selectedGroups.includes(groupUuid)) { state.selectedGroups.push(groupUuid); } }); }, removeSelectedGroup: (groupUuid) => { set((state) => { state.selectedGroups = state.selectedGroups.filter((uuid) => uuid !== groupUuid); }); }, toggleSelectedGroup: (groupUuid) => { set((state) => { const exists = state.selectedGroups.includes(groupUuid); if (exists) { state.selectedGroups = state.selectedGroups.filter((uuid) => uuid !== groupUuid); } else { state.selectedGroups.push(groupUuid); } }); }, clearSelectedGroups: () => { set((state) => { state.selectedGroups = []; }); }, hasSelectedGroup: (groupUuid: string) => { return get().selectedGroups.includes(groupUuid); }, // Group children management addChildToGroup: (groupUuid, child) => { const result: { updatedGroups: AssetGroup[] } = { updatedGroups: [] }; set((state) => { const targetGroup = state.assetGroups.find((g) => g.groupUuid === groupUuid); if (!targetGroup) return; const updatedGroups: AssetGroup[] = []; // 1️⃣ Remove the child from any other groups (to maintain single-parent rule) state.assetGroups.forEach((group) => { if (group.groupUuid === groupUuid) return; // skip target group const originalLength = group.childrens.length; group.childrens = group.childrens.filter((c) => c.childrenUuid !== child.childrenUuid); if (group.childrens.length !== originalLength) { updatedGroups.push({ ...group }); } }); // 2️⃣ Add the child to the target group (if not already present) if (!targetGroup.childrens.some((c) => c.childrenUuid === child.childrenUuid)) { targetGroup.childrens.push(child); updatedGroups.push({ ...targetGroup }); } // 3️⃣ Rebuild hierarchy after modification state.groupHierarchy = get().buildHierarchy([], state.assetGroups); result.updatedGroups = updatedGroups; }); return result; }, removeChildFromGroup: (groupUuid, childUuid) => { set((state) => { const group = state.assetGroups.find((g) => g.groupUuid === groupUuid); if (group) { group.childrens = group.childrens.filter((child) => child.childrenUuid !== childUuid); state.groupHierarchy = get().buildHierarchy([], state.assetGroups); } }); }, getGroupChildren: (groupUuid) => { const group = get().assetGroups.find((g) => g.groupUuid === groupUuid); return group?.childrens || []; }, // Group properties setGroupName: (groupUuid, newName) => { set((state) => { const group = state.assetGroups.find((g) => g.groupUuid === groupUuid); if (group) { group.groupName = newName; } }); }, setGroupVisibility: (groupUuid, isVisible) => { set((state) => { const group = state.assetGroups.find((g) => g.groupUuid === groupUuid); if (group) { group.isVisible = isVisible; } }); }, setGroupLock: (groupUuid, isLocked) => { set((state) => { const group = state.assetGroups.find((g) => g.groupUuid === groupUuid); if (group) { group.isLocked = isLocked; } }); }, toggleGroupVisibility: (groupUuid) => { set((state) => { const group = state.assetGroups.find((g) => g.groupUuid === groupUuid); if (group) { group.isVisible = !group.isVisible; } }); }, setGroupExpanded: (groupUuid, isExpanded) => { set((state) => { const group = state.assetGroups.find((g) => g.groupUuid === groupUuid); if (group) { group.isExpanded = isExpanded; } }); }, // Hierarchy operations buildHierarchy: (assets: Assets, groups: AssetGroup[]): AssetGroupHierarchy => { const assetMap = new Map(assets.map((asset) => [asset.modelUuid, asset])); const groupMap = new Map(groups.map((group) => [group.groupUuid, group])); const buildNode = (group: AssetGroup): AssetGroupHierarchyNode => { const children: AssetGroupChild[] = []; group.childrens.forEach((child) => { if (child.type === "Asset") { const asset = assetMap.get(child.childrenUuid); if (asset) { children.push(asset); // Remove from assetMap so we know it's been processed assetMap.delete(child.childrenUuid); } } else if (child.type === "Group") { const childGroup = groupMap.get(child.childrenUuid); if (childGroup) { children.push(buildNode(childGroup)); } } }); return { groupUuid: group.groupUuid, groupName: group.groupName, isVisible: group.isVisible, isLocked: group.isLocked, isExpanded: group.isExpanded, children, }; }; // Find root groups (groups that are not children of any other group) const childGroupUuids = new Set(); groups.forEach((group) => { group.childrens.forEach((child) => { if (child.type === "Group") { childGroupUuids.add(child.childrenUuid); } }); }); const rootGroups = groups.filter((group) => !childGroupUuids.has(group.groupUuid)); // Build hierarchy starting from root groups const hierarchy: AssetGroupHierarchy = rootGroups.map(buildNode); // Add remaining assets that are not in any group const ungroupedAssets: Asset[] = []; assetMap.forEach((asset) => { ungroupedAssets.push(asset); }); const finalHierarchy = [...hierarchy, ...ungroupedAssets]; set((state) => { state.groupHierarchy = finalHierarchy; }); return finalHierarchy; }, flattenHierarchy: (hierarchy: AssetGroupHierarchy) => { const assets: Asset[] = []; const groups: AssetGroup[] = []; const processedGroups = new Set(); const processNode = (node: AssetGroupChild) => { if ("modelUuid" in node) { // It's an Asset assets.push(node); } else { // It's an AssetGroupHierarchyNode if (!processedGroups.has(node.groupUuid)) { groups.push({ groupUuid: node.groupUuid, groupName: node.groupName, isVisible: node.isVisible, isLocked: node.isLocked, isExpanded: node.isExpanded, childrens: node.children.map((child) => "modelUuid" in child ? { type: "Asset" as const, childrenUuid: child.modelUuid } : { type: "Group" as const, childrenUuid: child.groupUuid } ), }); processedGroups.add(node.groupUuid); } node.children.forEach(processNode); } }; hierarchy.forEach(processNode); return { assets, groups }; }, getGroupHierarchy: (groupUuid) => { const hierarchy = get().groupHierarchy; const findGroup = (nodes: AssetGroupHierarchy): AssetGroupHierarchyNode | null => { for (const node of nodes) { if ("groupUuid" in node && node.groupUuid === groupUuid) { return node; } if ("children" in node) { const found = findGroup(node.children); if (found) return found; } } return null; }; return findGroup(hierarchy); }, getFlatGroupAssets: (groupUuid, assets) => { const groupHierarchy = get().getGroupHierarchy(groupUuid); if (!groupHierarchy) return []; const flatAssets: Asset[] = []; const assetMap = new Map(assets.map((asset) => [asset.modelUuid, asset])); const collectAssets = (nodes: AssetGroupChild[]) => { nodes.forEach((node) => { if ("modelUuid" in node) { const asset = assetMap.get(node.modelUuid); if (asset) flatAssets.push(asset); } else { collectAssets(node.children); } }); }; collectAssets(groupHierarchy.children); return flatAssets; }, getFlatGroupChildren: (groupUuid) => { const group = get().assetGroups.find((g) => g.groupUuid === groupUuid); if (!group) return []; const allChildren: string[] = []; const collectChildren = (children: { type: "Asset" | "Group"; childrenUuid: string }[]) => { children.forEach((child) => { allChildren.push(child.childrenUuid); if (child.type === "Group") { const childGroup = get().assetGroups.find((g) => g.groupUuid === child.childrenUuid); if (childGroup) { collectChildren(childGroup.childrens); } } }); }; collectChildren(group.childrens); return allChildren; }, // Helper functions getGroupById: (groupUuid) => { return get().assetGroups.find((g) => g.groupUuid === groupUuid); }, getGroupsContainingAsset: (assetUuid) => { return get().assetGroups.filter((group) => group.childrens.some((child) => child.type === "Asset" && child.childrenUuid === assetUuid)); }, getGroupsContainingGroup: (childGroupUuid) => { return get().assetGroups.filter((group) => group.childrens.some((child) => child.type === "Group" && child.childrenUuid === childGroupUuid)); }, hasGroup: (groupUuid) => { return get().assetGroups.some((g) => g.groupUuid === groupUuid); }, isGroup: (item): item is AssetGroupHierarchyNode => { return "children" in item; }, isEmptyGroup: (groupUuid) => { const group = get().assetGroups.find((g) => g.groupUuid === groupUuid); return !group || group.childrens.length === 0; }, })) ); }; export type AssetGroupStoreType = ReturnType;