From 07ea32e1cde8258d25a8554269308d982cd19bcf Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Thu, 18 Dec 2025 12:56:56 +0100 Subject: [PATCH] feat: add filtration for empty resources / groups --- .../js/__internal/scheduler/m_scheduler.ts | 36 +++++++++++---- .../resource_manager/resource_manager.ts | 46 +++++++++++++++++++ 2 files changed, 72 insertions(+), 10 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index 9b896fb7fa56..54fe1f9eb57e 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -365,7 +365,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { this._postponeDataSourceLoading(); break; - // TODO Vinogradov refactoring: merge it with startDayHour / endDayHour + // TODO Vinogradov refactoring: merge it with startDayHour / endDayHour case 'offset': this.updateAppointmentDataSource(); @@ -850,12 +850,28 @@ class Scheduler extends SchedulerOptionsBaseWidget { this._renderContentImpl(); } + _filterGroupsIfNeeded() { + const resourceManager = this._workSpace.option('getResourceManager')(); + const allAppointments = this._workSpace.option('getFilteredItems')(); + + if (allAppointments.length > 0) { + resourceManager.filterGroupsByAppointments(allAppointments); + this._layoutManager.filterAppointments(); + + if (!isAgendaWorkspaceComponent(this._workSpace)) { + this._workSpace.renderWorkSpace(); + this._dimensionChanged(null, true); + } + } + } + _dataSourceChangedHandler(result?: Appointment[]) { if (this._readyToRenderAppointments) { this._workSpaceRecalculation.done(() => { this._layoutManager.prepareAppointments(result); this._renderAppointments(); this._updateA11yStatus(); + this._filterGroupsIfNeeded(); }); } } @@ -1338,13 +1354,13 @@ class Scheduler extends SchedulerOptionsBaseWidget { const scrolling = this.getViewOption('scrolling'); const isVirtualScrolling = scrolling.mode === 'virtual'; const horizontalVirtualScrollingAllowed = isVirtualScrolling - && ( - !isDefined(scrolling.orientation) - || ['horizontal', 'both'].includes(scrolling.orientation) - ); + && ( + !isDefined(scrolling.orientation) + || ['horizontal', 'both'].includes(scrolling.orientation) + ); const crossScrollingEnabled = this.option('crossScrollingEnabled') - || horizontalVirtualScrollingAllowed - || isTimelineView(currentViewOptions.type); + || horizontalVirtualScrollingAllowed + || isTimelineView(currentViewOptions.type); const result = extend({ resources: this.option('resources'), @@ -1653,8 +1669,8 @@ class Scheduler extends SchedulerOptionsBaseWidget { const duration = appointmentEndDate.getTime() - appointmentStartDate.getTime(); const isKeepAppointmentHours = this._workSpace.keepOriginalHours() - && dateUtilsTs.isValidDate(appointment.startDate) - && dateUtilsTs.isValidDate(cellStartDate); + && dateUtilsTs.isValidDate(appointment.startDate) + && dateUtilsTs.isValidDate(cellStartDate); if (isKeepAppointmentHours) { const startDate = this.timeZoneCalculator.createDate(appointmentStartDate, 'toGrid'); @@ -2032,7 +2048,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { const isVirtualScrolling = mode === 'virtual'; return isVirtualScrolling - && (orientation === 'horizontal' || orientation === 'both'); + && (orientation === 'horizontal' || orientation === 'both'); } addAppointment(rawAppointment) { diff --git a/packages/devextreme/js/__internal/scheduler/utils/resource_manager/resource_manager.ts b/packages/devextreme/js/__internal/scheduler/utils/resource_manager/resource_manager.ts index 98a66f5c01f7..9e81b41dc694 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/resource_manager/resource_manager.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/resource_manager/resource_manager.ts @@ -1,9 +1,11 @@ import type { SafeAppointment } from '@ts/scheduler/types'; +import type { ListEntity } from '../../view_model/types'; import { getResourceIndex } from '../data_accessor/appointment_resource_data_accessor'; import { ResourceLoader } from '../loader/resource_loader'; import type { ResourceConfig, + ResourceId, } from '../loader/types'; import { getAppointmentColor } from './appointment_color_utils'; import type { AppointmentResource } from './appointment_groups_utils'; @@ -54,6 +56,50 @@ export class ResourceManager { return this.groupsLeafs.length; } + public filterGroupsByAppointments(appointments: ListEntity[]): void { + if (!this.groups.length) { + return; + } + + // Find all resource values used in appointments + const usedResourceValues = new Map>(); + + appointments.forEach((appointment) => { + this.groups.forEach((resourceIndex) => { + const resource = this.resourceById[resourceIndex]; + if (resource) { + const values = resource.idsGetter(appointment.itemData); + if (!usedResourceValues.has(resourceIndex)) { + usedResourceValues.set(resourceIndex, new Set()); + } + const resourceValues = usedResourceValues.get(resourceIndex); + if (resourceValues) { + values.forEach((value: ResourceId) => { + resourceValues.add(value); + }); + } + } + }); + }); + + // Filter resource dataSources to include only used values + this.groups.forEach((resourceIndex) => { + const resource = this.resourceById[resourceIndex]; + if (resource?.items) { + const usedValues = usedResourceValues.get(resourceIndex); + if (usedValues && usedValues.size > 0) { + const filteredItems = resource.items.filter((item) => usedValues.has(item.id)); + resource.items = filteredItems; + } + } + }); + + // Rebuild groupsTree and groupsLeafs after filtering resources + const { groupTree, groupLeafs } = groupResources(this.resourceById, this.groups); + this.groupsTree = groupTree; + this.groupsLeafs = groupLeafs; + } + public groupResources(): ResourceLoader[] { return this.groups .map((group) => this.resourceById[group])