Skip to content
Closed
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
@@ -0,0 +1,134 @@
import {
describe, expect, it,
} from '@jest/globals';

import { getResourceManagerMock } from '../__mock__/resource_manager.mock';
import SchedulerTimelineMonth from '../workspaces/m_timeline_month';
import type SchedulerWorkSpace from '../workspaces/m_work_space';

type WorkspaceConstructor<T> = new (container: Element, options?: any) => T;

const createWorkspaceWithHierarchicalResources = <T extends SchedulerWorkSpace>(
WorkSpace: WorkspaceConstructor<T>,
currentView: string,
): { workspace: T; resourceManager: any } => {
const container = document.createElement('div');

// Hierarchical resource configuration
const roomTypeData = [
{ id: 1, text: 'Deluxe' },
{ id: 2, text: 'Super Deluxe' },
{ id: 3, text: 'Luxury' },
];

const roomData = [
{ id: 1, text: '101', roomTypeId: 1 },
{ id: 2, text: '102', roomTypeId: 1 },
{ id: 3, text: '103', roomTypeId: 1 },
{ id: 4, text: '104', roomTypeId: 2 },
{ id: 5, text: '105', roomTypeId: 2 },
{ id: 6, text: '106', roomTypeId: 2 },
{ id: 7, text: '107', roomTypeId: 3 },
{ id: 8, text: '108', roomTypeId: 3 },
{ id: 9, text: '109', roomTypeId: 3 },
{ id: 10, text: '110', roomTypeId: 3 },
];

const resourceConfig = [
{
fieldExpr: 'roomTypeId',
dataSource: roomTypeData,
label: 'Room Type',
},
{
fieldExpr: 'roomId',
dataSource: roomData,
parentResource: 'roomTypeId',
parentFieldExpr: 'roomTypeId',
label: 'Room',
},
];

const resourceManager = getResourceManagerMock(resourceConfig);

const workspace = new WorkSpace(container, {
views: [currentView],
currentView,
currentDate: new Date(2020, 5, 26),
firstDayOfWeek: 0,
groups: ['roomTypeId', 'roomId'],
getResourceManager: () => resourceManager,
});

(workspace as any)._isVisible = () => true;

return { workspace, resourceManager };
};

describe('scheduler workspace hierarchical resources', () => {
it('should filter child resources correctly in groupResources', async () => {
const { resourceManager } = createWorkspaceWithHierarchicalResources(
SchedulerTimelineMonth,
'timelineMonth',
);

// Load and set groups
await resourceManager.loadGroupResources(['roomTypeId', 'roomId']);

const { groupTree, groupLeafs } = {
groupTree: resourceManager.groupsTree,
groupLeafs: resourceManager.groupsLeafs,
};

// Verify that groupsTree is properly filtered
expect(groupTree).toBeDefined();
expect(groupTree.length).toBeGreaterThan(0);

// Check that we have the right structure
groupTree.forEach((rootNode) => {
expect(rootNode.children).toBeDefined();
});

// For now, just check that filtering is working at all
expect(groupLeafs.length).toBeGreaterThan(0);
});

it('should filter child resources correctly in getGroupPanelData', async () => {
const { workspace, resourceManager } = createWorkspaceWithHierarchicalResources(
SchedulerTimelineMonth,
'timelineMonth',
);

// Load groups first
await resourceManager.loadGroupResources(['roomTypeId', 'roomId']);

const { viewDataProvider } = workspace as any;
const options = {
getResourceManager: () => resourceManager,
};

const groupPanelData = viewDataProvider.getGroupPanelData(options);

expect(groupPanelData).toBeDefined();
expect(groupPanelData.groupPanelItems).toBeDefined();
expect(groupPanelData.groupPanelItems.length).toBe(2); // roomTypeId and roomId

const roomTypeItems = groupPanelData.groupPanelItems[0];
const roomIdItems = groupPanelData.groupPanelItems[1];

// roomTypeId items should be unique with rowSpan: 3 items (Deluxe, Super Deluxe, Luxury)
expect(roomTypeItems.length).toBe(3);
// roomId items should be unique: 3 + 3 + 4 = 10
expect(roomIdItems.length).toBe(10);

// Check room types
expect(roomTypeItems[0].text).toBe('Deluxe');
expect(roomTypeItems[1].text).toBe('Super Deluxe');
expect(roomTypeItems[2].text).toBe('Luxury');

// Check rowSpan values
expect(roomTypeItems[0].rowSpan).toBe(3); // Deluxe has 3 children
expect(roomTypeItems[1].rowSpan).toBe(3); // Super Deluxe has 3 children
expect(roomTypeItems[2].rowSpan).toBe(4); // Luxury has 4 children
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export interface GroupPanelCellProps extends PropsWithClassName {
data: GroupItem;
index: number;
cellTemplate?: JSXTemplate<ResourceCellTemplateProps>;
rowSpan?: number;
}

export const GroupPanelCellDefaultProps = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,17 @@ export class GroupPanelVerticalCell extends BaseInfernoComponent<GroupPanelCellP
text,
index,
cellTemplate,
rowSpan,
} = this.props;

// Apply rowSpan as flex-grow for vertical layout
const style = rowSpan ? { 'flex-grow': rowSpan } : undefined;

return (
<div className={`dx-scheduler-group-header ${className}`}>
<div
className={`dx-scheduler-group-header ${className}`}
style={style}
>
{
cellTemplate
? <PublicTemplate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export class GroupPanelVerticalRow extends BaseInfernoComponent<GroupPanelRowPro
id,
key,
text,
rowSpan,
}, index) => <GroupPanelVerticalCell
key={key}
text={text}
Expand All @@ -29,6 +30,7 @@ export class GroupPanelVerticalRow extends BaseInfernoComponent<GroupPanelRowPro
index={index}
color={color}
cellTemplate={cellTemplate}
rowSpan={rowSpan}
/>)
}
</div>
Expand Down
1 change: 1 addition & 0 deletions packages/devextreme/js/__internal/scheduler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ export interface GroupRenderItem extends GroupItem {
resourceName: string;
data: GroupItem;
colSpan?: number;
rowSpan?: number;
isFirstGroupCell?: boolean;
isLastGroupCell?: boolean;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ export class ResourceLoader extends Loader<RawResourceData, ResourceData> {

public icon?: string;

public parentResource?: string;

public parentFieldExpr?: string;

constructor(config: ResourceConfig) {
super(config, { pageSize: 0 });
const accessor = getAppointmentResourceAccessor(config);
Expand All @@ -40,6 +44,8 @@ export class ResourceLoader extends Loader<RawResourceData, ResourceData> {
this.resourceIndex = String(getResourceIndex(config));
this.resourceName = config.label;
this.icon = config.icon;
this.parentResource = config.parentResource;
this.parentFieldExpr = config.parentFieldExpr;
this.onInit();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ type ResourcesConfig = Required<Properties>['resources'];

export type ResourceConfig = ResourcesConfig[number] & {
field?: string; // old notation of fieldExpr
parentResource?: string; // parent resource field name
parentFieldExpr?: string; // field in raw data that contains parent value
};

export type RawResourceData = Record<string, string> & {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { isObject } from '@js/core/utils/type';

import type { GroupRenderItem } from '../../types';
import type { ResourceLoader } from '../loader/resource_loader';
import type { ResourceData, ResourceId } from '../loader/types';
import type { GroupLeaf, GroupNode } from './types';

const stringifyId = (id: ResourceId): string => (isObject(id)
? JSON.stringify(id)
: String(id));

export const groupResources = (resourceById: Record<string, ResourceLoader>, groups: string[]): {
groupTree: GroupNode[];
groupLeafs: GroupLeaf[];
Expand All @@ -19,19 +27,52 @@ export const groupResources = (resourceById: Record<string, ResourceLoader>, gro
.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[] = [];

// Check if this resource has a parent resource for filtering
const { parentResource: parentResourceIndex, parentFieldExpr } = resource;

leafs.forEach((leaf) => {
let itemsToUse = resource.items;

// Filter child resource items based on parent resource value
if (parentResourceIndex && resourceById[parentResourceIndex] && parentFieldExpr) {
const parentValue = leaf.grouped[parentResourceIndex];

itemsToUse = resource.items.filter((item) => {
// Find raw data item by id
const rawDataItem = resource.data.find((rawItem) => {
const rawId = resource.dataAccessor.get('id', rawItem);
return rawId === item.id || (rawItem as Record<string, unknown>).id === item.id;
});

if (!rawDataItem) return false;

// Get parent field value from raw data
const rawDataRecord = rawDataItem as Record<string, unknown>;
const itemParentValue = rawDataRecord[parentFieldExpr]
?? resource.dataAccessor.get(parentFieldExpr, rawDataItem);

// Compare with parent value from leaf
return itemParentValue === parentValue;
});
}

const nodes = itemsToUse.map<GroupNode>((item) => ({
resourceText: item.text,
resourceIndex: resource.resourceIndex,
grouped: { [resource.resourceIndex]: item.id },
children: [],
}));

leaf.children = nodes.map((node) => ({
...node,
grouped: { ...node.grouped, ...leaf.grouped },
}));
});

// Prepare next level leafs
const nextLeafs: GroupNode[] = [];
leafs.forEach((leaf) => {
nextLeafs.push(...leaf.children);
});
leafs = nextLeafs;
Expand Down Expand Up @@ -88,3 +129,96 @@ export const getResourcesByGroupIndex = (
items: resource.items.filter((item) => item.id === leafGroups[resourceIndex]),
}) as ResourceLoader);
};

const createGroupRenderItem = (
item: ResourceData,
resourceIndex: string,
resourceName?: string,
rowSpan?: number,
): GroupRenderItem => {
const result: GroupRenderItem = {
id: item.id as number | string,
text: item.text,
color: item.color,
key: `${resourceIndex}_${stringifyId(item.id)}`,
resourceName: resourceName ?? resourceIndex,
data: {
id: item.id as number | string,
text: item.text,
color: item.color,
},
};

if (rowSpan) {
result.rowSpan = rowSpan;
}

return result;
};

export const createGroupPanelDataFromTree = (
groupsTree: GroupNode[],
resourceById: Record<string, ResourceLoader>,
groups: string[],
columnCountPerGroup: number,
groupByDate: boolean,
baseColSpan: number,
): { groupPanelItems: GroupRenderItem[][]; baseColSpan: number } => {
const groupPanelItems: GroupRenderItem[][] = [];

groups.forEach((resourceIndex, groupIndex) => {
const resource = resourceById[resourceIndex];
if (!resource) return;

if (groupIndex === 0) {
// Parent level: create items with rowSpan based on child count
const items: GroupRenderItem[] = [];
const processedItems = new Set<ResourceId>();

groupsTree.forEach((parentNode) => {
const itemId = parentNode.grouped[resourceIndex];
if (itemId !== undefined && !processedItems.has(itemId)) {
processedItems.add(itemId);
const item = resource.items.find((i) => i.id === itemId);
if (item) {
// Calculate rowSpan as sum of children across all matching parent nodes
const rowSpan = groupsTree
.filter((node) => node.grouped[resourceIndex] === itemId)
.reduce((sum, node) => sum + (node.children?.length ?? 1), 0);

items.push(createGroupRenderItem(item, resourceIndex, resource.resourceName, rowSpan));
}
}
});

groupPanelItems.push(items);
} else {
// Child levels: collect all unique items
const uniqueItems = new Map<ResourceId, GroupRenderItem>();

const collectItems = (nodes: GroupNode[]): void => {
nodes.forEach((node) => {
const itemId = node.grouped[resourceIndex];
if (itemId !== undefined && !uniqueItems.has(itemId)) {
const item = resource.items.find((i) => i.id === itemId);
if (item) {
uniqueItems.set(
itemId,
createGroupRenderItem(item, resourceIndex, resource.resourceName),
);
}
}

if (node.children) {
collectItems(node.children);
}
});
};

collectItems(groupsTree);
groupPanelItems.push(Array.from(uniqueItems.values()));
}
});

return { groupPanelItems, baseColSpan };
};
Loading
Loading