Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,30 +1,99 @@
import {
describe, expect, it,
beforeAll, describe, expect, it,
} from '@jest/globals';
import { getResourceManagerMock } from '@ts/scheduler/__mock__/resource_manager.mock';

import { ResourceLoader } from '../loader/resource_loader';
import type { RawResourceData } from '../loader/types';
import {
getAllGroupValues, getGroupTexts, getLeafGroupValues, getResourcesByGroupIndex, groupResources,
} from './group_utils';
import type { GroupLeaf } from './types';

const groupsLeafs: any = [
{ groupIndex: 0, grouped: { assigneeId: 1, roomId: 3 } },
{ groupIndex: 1, grouped: { assigneeId: 3, roomId: 4 } },
{ groupIndex: 2, grouped: { roomId: 0 } },
{ groupIndex: 3, grouped: { assigneeId: 0, roomId: 0 } },
const assigneeData: RawResourceData[] = [
{ id: 0, text: 'Samantha Bright', color: '#727bd2' },
{ id: 1, text: 'John Heart', color: '#32c9ed' },
];
const resourceById: any = {
assigneeId: {
resourceIndex: 'assigneeId',
items: [{ id: 0, text: 'Samantha Bright' }, { id: 1, text: 'John Heart' }],

const roomData: RawResourceData[] = [
{ id: 0, text: 'Room 1', color: '#aaa' },
{ id: 1, text: 'Room 2', color: '#ccc' },
];

const roomHierarchyData: RawResourceData[] = [
{
id: 'board', text: 'Board rooms', color: '#111', parentId: null,
},
{
id: 'open', text: 'Open spaces', color: '#222', parentId: null,
},
{
id: 11, text: 'Room 11', color: '#333', parentId: 'board',
},
{
id: 12, text: 'Room 12', color: '#444', parentId: 'board',
},
roomId: {
resourceIndex: 'roomId',
items: [{ id: 0, text: 'Room 1' }, { id: 1, text: 'Room 2' }],
{
id: 21, text: 'Room 21', color: '#555', parentId: 'open',
},
];

const createResourceLoader = async (
fieldExpr: string,
dataSource: RawResourceData[],
label: string,
parentIdExpr?: string,
): Promise<ResourceLoader> => {
const loader = new ResourceLoader({
fieldExpr,
dataSource,
label,
parentIdExpr,
});

await loader.load();

return loader;
};

const createHierarchicalRoomResource = (): Promise<ResourceLoader> => createResourceLoader(
'roomId',
roomHierarchyData,
'Room',
'parentId',
);

const createGroupLeaf = (
groupIndex: number,
grouped: GroupLeaf['grouped'],
): GroupLeaf => ({
groupIndex,
grouped,
resourceText: '',
resourceIndex: '',
children: [],
});

const groupsLeafs: GroupLeaf[] = [
createGroupLeaf(0, { assigneeId: 1, roomId: 3 }),
createGroupLeaf(1, { assigneeId: 3, roomId: 4 }),
createGroupLeaf(2, { roomId: 0 }),
createGroupLeaf(3, { assigneeId: 0, roomId: 0 }),
];

describe('groups utils', () => {
// eslint-disable-next-line @typescript-eslint/init-declarations
let resourceById: Record<string, ResourceLoader>;

beforeAll(async () => {
const [assigneeId, roomId] = await Promise.all([
createResourceLoader('assigneeId', assigneeData, 'Assignee'),
createResourceLoader('roomId', roomData, 'Room'),
]);

resourceById = { assigneeId, roomId };
});

describe('groupResources', () => {
it('should return empty tree for empty groups', () => {
expect(groupResources(resourceById, [])).toEqual({
Expand All @@ -40,6 +109,13 @@ describe('groups utils', () => {
});
});

it('should return empty tree when groups only contains keys missing from resourceById', () => {
expect(groupResources(resourceById, ['unknownId'])).toEqual({
groupTree: [],
groupLeafs: [],
});
});

it('should group by one group', () => {
expect(groupResources(resourceById, ['roomId'])).toEqual({
groupTree: [
Expand Down Expand Up @@ -186,6 +262,106 @@ describe('groups utils', () => {
],
});
});

it('should group hierarchical resource by parent-child tree', async () => {
const hierarchicalRoom = await createHierarchicalRoomResource();

expect(groupResources({ roomId: hierarchicalRoom }, ['roomId'])).toEqual({
groupTree: [
{
resourceText: 'Board rooms',
resourceIndex: 'roomId',
grouped: { roomId: 'board' },
children: [
{
resourceText: 'Room 11',
resourceIndex: 'roomId',
grouped: { roomId: 11 },
children: [],
},
{
resourceText: 'Room 12',
resourceIndex: 'roomId',
grouped: { roomId: 12 },
children: [],
},
],
},
{
resourceText: 'Open spaces',
resourceIndex: 'roomId',
grouped: { roomId: 'open' },
children: [
{
resourceText: 'Room 21',
resourceIndex: 'roomId',
grouped: { roomId: 21 },
children: [],
},
],
},
],
groupLeafs: [
{
resourceText: 'Room 11',
resourceIndex: 'roomId',
grouped: { roomId: 11 },
children: [],
groupIndex: 0,
},
{
resourceText: 'Room 12',
resourceIndex: 'roomId',
grouped: { roomId: 12 },
children: [],
groupIndex: 1,
},
{
resourceText: 'Room 21',
resourceIndex: 'roomId',
grouped: { roomId: 21 },
children: [],
groupIndex: 2,
},
],
});
});

it('should use only leaf bands for hierarchical resource instead of cartesian product', async () => {
const hierarchicalRoom = await createHierarchicalRoomResource();

const { groupLeafs } = groupResources({ roomId: hierarchicalRoom }, ['roomId']);

expect(groupLeafs).toHaveLength(3);
expect(groupLeafs.map((leaf) => leaf.grouped.roomId)).toEqual([11, 12, 21]);
});

it('should combine hierarchical and flat resources', async () => {
const hierarchicalRoom = await createHierarchicalRoomResource();

const { groupTree, groupLeafs } = groupResources({
roomId: hierarchicalRoom,
assigneeId: resourceById.assigneeId,
}, ['roomId', 'assigneeId']);

expect(groupLeafs).toHaveLength(6);
expect(groupLeafs[0].grouped).toEqual({ roomId: 11, assigneeId: 0 });
expect(groupLeafs[1].grouped).toEqual({ roomId: 11, assigneeId: 1 });
expect(groupTree[0].resourceText).toBe('Board rooms');
expect(groupTree[0].children[0].children[0].resourceText).toBe('Samantha Bright');
});

it('should return an empty group tree when an earlier resource in groups has an empty dataSource', async () => {
const emptyRoom = await createResourceLoader('roomId', [], 'Room');

expect(groupResources({
roomId: emptyRoom,
assigneeId: resourceById.assigneeId,
}, ['roomId', 'assigneeId'])).toEqual({
groupTree: [],
groupLeafs: [],
});
});
});

describe('getAllGroupValues', () => {
Expand Down Expand Up @@ -246,12 +422,12 @@ describe('groups utils', () => {
it('should return resources of groupIndex', () => {
expect(getResourcesByGroupIndex(groupsLeafs, resourceById, 3)).toEqual([
{
items: [{ id: 0, text: 'Samantha Bright' }],
resourceIndex: 'assigneeId',
...resourceById.assigneeId,
items: [{ id: 0, text: 'Samantha Bright', color: '#727bd2' }],
},
{
items: [{ id: 0, text: 'Room 1' }],
resourceIndex: 'roomId',
...resourceById.roomId,
items: [{ id: 0, text: 'Room 1', color: '#aaa' }],
},
]);
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,41 +1,120 @@
import type { ResourceLoader } from '../loader/resource_loader';
import type { ResourceId } from '../loader/types';
import type { ResourceHierarchyNode } from './hierarchy_tree_utils';
import type { GroupLeaf, GroupNode } from './types';

const isVirtualRoot = (node: GroupNode): boolean => !node.resourceIndex;

const createFlatResourceNodes = (
resource: ResourceLoader,
): GroupNode[] => resource.items.map((item) => ({
resourceText: item.text,
resourceIndex: resource.resourceIndex,
grouped: { [resource.resourceIndex]: item.id },
children: [],
}));

const hierarchyToGroupNodes = (
hierarchyNodes: ResourceHierarchyNode[],
resourceIndex: string,
parentGrouped: Record<string, ResourceId>,
): GroupNode[] => hierarchyNodes.map((node) => {
const grouped = { ...parentGrouped, [resourceIndex]: node.data.id };

return {
resourceText: node.data.text,
resourceIndex,
grouped,
children: hierarchyToGroupNodes(node.children, resourceIndex, grouped),
};
});

const collectGroupLeaves = (nodes: GroupNode[]): GroupNode[] => {
const leaves: GroupNode[] = [];

const walk = (node: GroupNode): void => {
if (node.children.length === 0) {
leaves.push(node);
return;
}

node.children.forEach(walk);
};

nodes.forEach(walk);

return leaves;
};

const mergeGroupedIntoTree = (
node: GroupNode,
parentGrouped: Record<string, ResourceId>,
): GroupNode => ({
...node,
grouped: { ...parentGrouped, ...node.grouped },
children: node.children.map((child) => mergeGroupedIntoTree(child, parentGrouped)),
});

const createResourceNodes = (
resource: ResourceLoader,
): GroupNode[] => {
if (resource.hasHierarchy) {
return hierarchyToGroupNodes(resource.hierarchyTree, resource.resourceIndex, {});
}

return createFlatResourceNodes(resource);
};

const attachResourceNodes = (
leafs: GroupNode[],
nodes: GroupNode[],
): GroupNode[] => {
const nextLeafs: GroupNode[] = [];

leafs.forEach((leaf) => {
leaf.children = nodes.map((node) => mergeGroupedIntoTree(node, leaf.grouped));

leaf.children.forEach((child) => {
nextLeafs.push(...collectGroupLeaves([child]));
});
});

return nextLeafs;
};

export const groupResources = (resourceById: Record<string, ResourceLoader>, groups: string[]): {
groupTree: GroupNode[];
groupLeafs: GroupLeaf[];
} => {
if (!groups.length || Object.keys(resourceById).length === 0) {
const validGroups = groups.filter((group) => resourceById[group]);

if (!validGroups.length) {
return {
groupTree: [],
groupLeafs: [],
};
}

const head: GroupNode[] = [{} as GroupNode];
const head: GroupNode[] = [{
resourceText: '',
resourceIndex: '',
grouped: {},
children: [],
}];
let leafs: GroupNode[] = head;

groups
.filter((group) => resourceById[group])
.forEach((group) => {
const resource = resourceById[group];
const nodes = resource.items.map<GroupNode>((item) => ({
resourceText: item.text,
resourceIndex: resource.resourceIndex,
grouped: { [resource.resourceIndex]: item.id },
children: [],
}));
const nextLeafs: GroupNode[] = [];

leafs.forEach((leaf) => {
leaf.children = nodes.map((node) => ({
...node,
grouped: { ...node.grouped, ...leaf.grouped },
}));
nextLeafs.push(...leaf.children);
});
leafs = nextLeafs;
});
validGroups.forEach((group) => {
const resource = resourceById[group];
const nodes = createResourceNodes(resource);

if (leafs.length > 0 && isVirtualRoot(leafs[0])) {
head[0].children = nodes;
leafs = collectGroupLeaves(nodes);
return;
}

leafs = attachResourceNodes(leafs, nodes);
});

const groupLeafs = leafs.map<GroupLeaf>((leaf, index) => ({
...leaf,
Expand Down
Loading