Skip to content
Draft
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
1 change: 1 addition & 0 deletions packages/devextreme/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export default [
'themebuilder-scss/src/data/metadata/*',
'js/bundles/dx.custom.js',
'testing/jest/utils/transformers/*',
'vite.config.ts',
'**/ts/',
'js/common/core/localization/cldr-data/*',
'js/common/core/localization/default_messages.js',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1392,6 +1392,7 @@ class Scheduler extends SchedulerOptionsBaseWidget {

// TODO: SSR does not work correctly with renovated render
renovateRender: this._isRenovatedRender(isVirtualScrolling),
skippedDays: currentViewOptions.skippedDays || [],
}, currentViewOptions);

result.notifyScheduler = this._notifyScheduler;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { isAppointmentMatchedResources } from './is_appointment_matched_resource

export const filterByAttributes = <T extends MinimalAppointmentEntity & AllDayPanelOccupation>(
entities: T[],
{ resourceManager, showAllDayPanel, supportAllDayPanel }: FilterOptions,
{
resourceManager, showAllDayPanel, supportAllDayPanel, viewDataProvider,
}: FilterOptions,
): T[] => entities.filter((appointment): boolean => {
if (!appointment.visible) {
return false;
Expand All @@ -18,6 +20,13 @@ export const filterByAttributes = <T extends MinimalAppointmentEntity & AllDayPa
return false;
}

if (viewDataProvider && appointment.itemData) {
const { startDate } = appointment.itemData;
if (startDate && viewDataProvider.isSkippedDate(startDate)) {
return false;
}
}

const resources = resourceManager.groupResources();
return isAppointmentMatchedResources(appointment.itemData, resources);
});
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const getFilterOptions = (schedulerStore: Scheduler): FilterOptions => {
dataAccessor: schedulerStore._dataAccessors,
viewOffset,
firstDayOfWeek: schedulerStore.option('firstDayOfWeek'),
viewDataProvider: schedulerStore._workSpace?.viewDataProvider,
allDayIntervals: shiftIntervals(
getVisibleDateTimeIntervals(compareOptions, true),
viewOffset,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Orientation } from '@js/common';

import type { AllDayPanelModeType, SafeAppointment } from '../types';
import type { AllDayPanelModeType, SafeAppointment, ViewDataProviderType } from '../types';
import type { AppointmentDataAccessor } from '../utils/data_accessor/appointment_data_accessor';
import type { ResourceManager } from '../utils/resource_manager/resource_manager';
import type { GroupLeaf } from '../utils/resource_manager/types';
Expand Down Expand Up @@ -50,6 +50,7 @@ export interface FilterOptions {
firstDayOfWeek?: number;
allDayIntervals: DateInterval[];
regularIntervals: DateInterval[];
viewDataProvider?: ViewDataProviderType;
}

export interface SortedIndex {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -894,6 +894,12 @@ class SchedulerWorkSpace extends Widget<WorkspaceOptionsInternal> {
startDate: this.option('startDate'),
firstDayOfWeek: this.option('firstDayOfWeek'),
showCurrentTimeIndicator: this.option('showCurrentTimeIndicator'),
currentView: {
type: this.type,
skippedDays: this.option('skippedDays'),
skippedDates: this.option('skippedDates'),
skipDatePredicate: this.option('skipDatePredicate'),
},

...this.virtualScrollingDispatcher.getRenderState(),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ interface CommonOptions extends CountGenerationConfig {
cellDuration: number;
indicatorTime?: Date;
timeZoneCalculator?: TimeZoneCalculator;
currentView?: {
type: ViewType;
skippedDays?: number[];
skippedDates?: number[];
skipDatePredicate?: (date: Date) => boolean;
};
}

export interface ViewDataProviderOptions extends CommonOptions {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export class ViewDataGenerator {

public hiddenInterval = 0;

protected currentViewOptions: any;

constructor(public readonly viewType: ViewType) {}

public isWorkWeekView(): boolean {
Expand All @@ -43,8 +45,27 @@ export class ViewDataGenerator {
].includes(this.viewType);
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
public isSkippedDate(date: any) {
const actualDate = date.getDay ? date : new Date(date);

if (this.currentViewOptions?.currentView?.skipDatePredicate) {
return this.currentViewOptions.currentView.skipDatePredicate(actualDate);
}

if (this.currentViewOptions?.currentView?.skippedDays) {
const day = actualDate.getDay();
if (this.currentViewOptions.currentView.skippedDays.includes(day)) {
return true;
}
}

if (this.currentViewOptions?.currentView?.skippedDates) {
const dateOfMonth = actualDate.getDate();
if (this.currentViewOptions.currentView.skippedDates.includes(dateOfMonth)) {
return true;
}
}

return false;
}

Expand Down Expand Up @@ -74,6 +95,8 @@ export class ViewDataGenerator {
hoursInterval,
} = options;

this.currentViewOptions = options;

this._setVisibilityDates(options);
this.setHiddenInterval(startDayHour, endDayHour, hoursInterval);

Expand All @@ -82,9 +105,10 @@ export class ViewDataGenerator {
intervalCount,
currentDate,
viewType,
hoursInterval,
startDayHour,
endDayHour,
hoursInterval,
currentView: options.currentView,
});
const rowCountInGroup = this.getRowCount({
intervalCount,
Expand Down Expand Up @@ -504,6 +528,7 @@ export class ViewDataGenerator {
firstDayOfWeek,
intervalCount,
viewOffset,
currentView,
} = options;
const cellCountInDay = this.getCellCountInDay(startDayHour, endDayHour, hoursInterval);

Expand All @@ -525,6 +550,32 @@ export class ViewDataGenerator {
let currentDate = new Date(
startViewDateTime + millisecondsOffset + offsetByCount + viewOffset,
);

if (currentView?.skippedDays && currentView.skippedDays.length > 0) {
// For week/workWeek views, columnIndex directly represents day index
// We need to map visual columnIndex to actual day, skipping hidden days
let actualDaysCount = 0;
const dateToCheck = new Date(startViewDate);

// Skip to the target day, accounting for skipped days
while (actualDaysCount < columnIndex) {
const isSkipped = this.isSkippedDate(dateToCheck);

if (!isSkipped) {
actualDaysCount++;
}
dateToCheck.setDate(dateToCheck.getDate() + 1);
}

// Now dateToCheck might land on a skipped day, so skip forward to next valid day
while (this.isSkippedDate(dateToCheck)) {
dateToCheck.setDate(dateToCheck.getDate() + 1);
}

const timeOfDay = millisecondsOffset % toMs('day');
currentDate = new Date(dateToCheck.getTime() + timeOfDay + viewOffset);
}

const isMidnightDSTViewStart = timezoneUtils.isLocalTimeMidnightDST(startViewDate);
const isMidnightDST = timezoneUtils.isLocalTimeMidnightDST(currentDate);

Expand Down Expand Up @@ -761,14 +812,21 @@ export class ViewDataGenerator {
startDayHour,
endDayHour,
hoursInterval,
currentView,
} = options;

const cellCountInDay = this.getCellCountInDay(startDayHour, endDayHour, hoursInterval);
const columnCountInDay = isHorizontalView(viewType)
? cellCountInDay
: 1;

return this.daysInInterval * intervalCount * columnCountInDay;
let { daysInInterval } = this;

if (currentView?.skippedDays && currentView.skippedDays.length > 0) {
daysInInterval = this.daysInInterval - currentView.skippedDays.length;
}

return daysInInterval * intervalCount * columnCountInDay;
}

public getRowCount(options): number {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,14 @@ export class ViewDataGeneratorMonth extends ViewDataGenerator {
this._maxVisibleDate = new Date(nextMonthDate.setDate(0));
}

getCellCount() {
return DAYS_IN_WEEK;
getCellCount(options?) {
let cellCount = DAYS_IN_WEEK;

if (options?.currentView?.skippedDays && options.currentView.skippedDays.length > 0) {
cellCount = DAYS_IN_WEEK - options.currentView.skippedDays.length;
}

return cellCount;
}

getRowCount(options) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ export class ViewDataGeneratorWorkWeek extends ViewDataGeneratorWeek {
readonly daysInInterval = 5;

isSkippedDate(date) {
if (this.currentViewOptions?.currentView?.skippedDays) {
const day = date.getDay ? date.getDay() : new Date(date).getDay();
if (this.currentViewOptions.currentView.skippedDays.includes(day)) {
return true;
}
}

return isDataOnWeekend(date);
}

Expand Down
3 changes: 3 additions & 0 deletions packages/devextreme/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,8 @@
"vinyl": "2.2.1",
"vinyl-named": "1.1.0",
"webpack": "5.103.0",
"vite": "^7.1.3",
"vite-plugin-inferno": "^0.0.1",
"webpack-stream": "7.0.0",
"yaml": "2.5.0",
"yargs": "17.7.2"
Expand Down Expand Up @@ -244,6 +246,7 @@
"validate-ts": "gulp validate-ts",
"validate-declarations": "dx-tools validate-declarations --sources ./js --exclude \"js/(renovation|__internal|.eslintrc.js)\" --compiler-options \"{ \\\"typeRoots\\\": [] }\"",
"testcafe-in-docker": "docker build -f ./testing/testcafe/docker/Dockerfile -t testcafe-testing . && docker run -it testcafe-testing",
"dev:playground": "vite",
"test-jest": "cross-env NODE_OPTIONS='--expose-gc' jest --no-coverage --runInBand --selectProjects jsdom-tests",
"test-jest:watch": "jest --watch",
"test-jest:node": "jest --no-coverage --runInBand --selectProjects node-tests",
Expand Down
25 changes: 25 additions & 0 deletions packages/devextreme/playground/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DevExtreme HMR Playground</title>
</head>
<body>
<div id="app">
<select id="theme-selector" style="display: block;">
</select>
<div id="container"></div>
</div>
<script type="module" src="./scheduler-example.ts"></script>
<script type="module">
if (import.meta.hot) {
import.meta.hot.accept('./scheduler-example.ts', () => {
console.log('HMR: Scheduler example updated');
location.reload();
});
console.log('HMR enabled for playground');
}
</script>
</body>
</html>
87 changes: 87 additions & 0 deletions packages/devextreme/playground/newThemeSelector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
const themeKey = 'currentThemeId';

const themeLoaders = import.meta.glob('../artifacts/css/dx.*.css', { as: 'url' });

const themeList = Object.keys(themeLoaders).map((path) => {
const match = path.match(/dx\.(.+)\.css$/);
return match ? match[1] : null;
}).filter(Boolean) as string[];

function groupThemes(themes: string[]) {
const groups: Record<string, string[]> = {};
themes.forEach((theme) => {
const [group] = theme.split('.');
const groupName = group.charAt(0).toUpperCase() + group.slice(1);
if (!groups[groupName]) groups[groupName] = [];
groups[groupName].push(theme);
});
return groups;
}

const groupedThemes = groupThemes(themeList);

function initThemes(dropDownList: HTMLSelectElement) {
Object.entries(groupedThemes).forEach(([group, themes]) => {
const parent = document.createElement('optgroup');
parent.label = group;

themes.forEach((theme) => {
const child = document.createElement('option');
child.value = theme;
child.text = theme.replaceAll('.', ' ');
parent.appendChild(child);
});

dropDownList.appendChild(parent);
});
}

function loadThemeCss(themeId: string): Promise<void> {
return new Promise((resolve, reject) => {
const oldLink = document.getElementById('theme-stylesheet');
if (oldLink) oldLink.remove();

const key = Object.keys(themeLoaders).find((p) => p.includes(`dx.${themeId}.css`));
if (!key) {
reject(new Error(`Theme not found: ${themeId}`));
return;
}

themeLoaders[key]().then((cssUrl: string) => {
const link = document.createElement('link');
link.id = 'theme-stylesheet';
link.rel = 'stylesheet';
link.href = cssUrl;

link.onload = () => resolve();
link.onerror = () => reject(new Error(`Failed to load theme: ${themeId}`));

document.head.appendChild(link);
});
});
}

export function setupThemeSelector(selectorId: string): Promise<void> {
return new Promise((resolve) => {
const dropDownList = document.querySelector<HTMLSelectElement>(`#${selectorId}`);
if (!dropDownList) {
resolve();
return;
}

initThemes(dropDownList);

const savedTheme = window.localStorage.getItem(themeKey) || themeList[0];
dropDownList.value = savedTheme;

loadThemeCss(savedTheme).then(() => {
dropDownList.addEventListener('change', () => {
const newTheme = dropDownList.value;
window.localStorage.setItem(themeKey, newTheme);
loadThemeCss(newTheme);
});

resolve();
});
});
}
Loading
Loading