From b07f9b895ecd7de2fc08a3a2b7de852eec4b6f9d Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Fri, 20 Mar 2026 11:12:40 -0300 Subject: [PATCH 01/16] Init Vite playground with custom plugin for Inferno + experimental decorators --- .../build/vite-plugin-devextreme.ts | 45 ++++++++++ packages/devextreme/eslint.config.mjs | 1 + packages/devextreme/package.json | 5 ++ packages/devextreme/playground/index.html | 25 ++++++ .../devextreme/playground/newThemeSelector.ts | 87 +++++++++++++++++++ .../playground/scheduler-example.ts | 55 ++++++++++++ packages/devextreme/vite.config.ts | 27 ++++++ 7 files changed, 245 insertions(+) create mode 100644 packages/devextreme/build/vite-plugin-devextreme.ts create mode 100644 packages/devextreme/playground/index.html create mode 100644 packages/devextreme/playground/newThemeSelector.ts create mode 100644 packages/devextreme/playground/scheduler-example.ts create mode 100644 packages/devextreme/vite.config.ts diff --git a/packages/devextreme/build/vite-plugin-devextreme.ts b/packages/devextreme/build/vite-plugin-devextreme.ts new file mode 100644 index 000000000000..46e006eac508 --- /dev/null +++ b/packages/devextreme/build/vite-plugin-devextreme.ts @@ -0,0 +1,45 @@ +import { transformAsync } from '@babel/core'; +import type { Plugin } from 'vite'; + +export default function devextremeInfernoPlugin(): Plugin { + return { + name: 'devextreme-inferno', + enforce: 'pre', + + async transform(code: string, id: string) { + if (!/\.[jt]sx?$/.test(id) || id.includes('node_modules')) { + return null; + } + + const isTSX = id.endsWith('.tsx'); + const isTS = id.endsWith('.ts') || isTSX; + + const plugins: unknown[] = [ + ['@babel/plugin-proposal-decorators', { legacy: true }], + ['@babel/plugin-transform-class-properties', { loose: true }], + 'babel-plugin-inferno', + ]; + + const presets: unknown[] = []; + if (isTS) { + presets.push([ + '@babel/preset-typescript', + { isTSX, allExtensions: true }, + ]); + } + + const result = await transformAsync(code, { + filename: id, + plugins, + presets, + sourceMaps: true, + }); + + if (!result?.code) { + return null; + } + + return { code: result.code, map: result.map }; + }, + }; +} diff --git a/packages/devextreme/eslint.config.mjs b/packages/devextreme/eslint.config.mjs index b971ed40dfaa..e148c46f5d32 100644 --- a/packages/devextreme/eslint.config.mjs +++ b/packages/devextreme/eslint.config.mjs @@ -41,6 +41,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', diff --git a/packages/devextreme/package.json b/packages/devextreme/package.json index 4a3b326af28e..60a0c35bafcf 100644 --- a/packages/devextreme/package.json +++ b/packages/devextreme/package.json @@ -69,9 +69,12 @@ "@babel/core": "7.29.0", "@babel/eslint-parser": "catalog:", "@babel/parser": "7.29.0", + "@babel/plugin-proposal-decorators": "7.23.9", + "@babel/plugin-transform-class-properties": "7.23.3", "@babel/plugin-transform-modules-commonjs": "7.28.6", "@babel/plugin-transform-runtime": "7.29.0", "@babel/preset-env": "7.29.0", + "@babel/preset-typescript": "7.23.3", "@devextreme-generator/angular": "3.0.12", "@devextreme-generator/build-helpers": "3.0.12", "@devextreme-generator/core": "3.0.12", @@ -211,6 +214,7 @@ "uuid": "9.0.1", "vinyl": "2.2.1", "vinyl-named": "1.1.0", + "vite": "^7.1.3", "webpack": "5.105.4", "webpack-stream": "7.0.0", "yaml": "2.5.0", @@ -246,6 +250,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", diff --git a/packages/devextreme/playground/index.html b/packages/devextreme/playground/index.html new file mode 100644 index 000000000000..e1f3659444ed --- /dev/null +++ b/packages/devextreme/playground/index.html @@ -0,0 +1,25 @@ + + + + + + DevExtreme HMR Playground + + +
+ +
+
+ + + + diff --git a/packages/devextreme/playground/newThemeSelector.ts b/packages/devextreme/playground/newThemeSelector.ts new file mode 100644 index 000000000000..8ef0fd8b2bcd --- /dev/null +++ b/packages/devextreme/playground/newThemeSelector.ts @@ -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 = {}; + 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 { + 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 { + return new Promise((resolve) => { + const dropDownList = document.querySelector(`#${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(); + }); + }); +} diff --git a/packages/devextreme/playground/scheduler-example.ts b/packages/devextreme/playground/scheduler-example.ts new file mode 100644 index 000000000000..35d76747c243 --- /dev/null +++ b/packages/devextreme/playground/scheduler-example.ts @@ -0,0 +1,55 @@ +import $ from 'jquery'; +import { setupThemeSelector } from './newthemeSelector'; +import Scheduler from '../js/__internal/scheduler/m_scheduler'; + +const dataSource = [ + { + text: "Meeting with John", + startDate: new Date(2024, 0, 10, 9, 0), + endDate: new Date(2024, 0, 10, 10, 30), + allDay: false + }, + { + text: "Conference Call", + startDate: new Date(2024, 0, 10, 14, 0), + endDate: new Date(2024, 0, 10, 15, 0), + allDay: false + }, + { + text: "Team Building Event", + startDate: new Date(2024, 0, 11, 10, 0), + endDate: new Date(2024, 0, 11, 17, 0), + allDay: false + } +]; + +window.addEventListener('load', () => + setupThemeSelector('theme-selector').then(() => { + + + new (Scheduler as any)($('#container'), { + dataSource, + views: ['day', 'week', 'workWeek', 'month'], + currentView: 'week', + currentDate: new Date(2024, 0, 10), + startDayHour: 8, + endDayHour: 18, + height: 600, + editing: { + allowAdding: true, + allowDeleting: true, + allowUpdating: true, + allowResizing: true, + allowDragging: true + }, + onAppointmentAdded: (e) => { + console.log('Appointment added:', e.appointmentData); + }, + onAppointmentUpdated: (e) => { + console.log('Appointment updated:', e.appointmentData); + }, + onAppointmentDeleted: (e) => { + console.log('Appointment deleted:', e.appointmentData); + } + }); +})); diff --git a/packages/devextreme/vite.config.ts b/packages/devextreme/vite.config.ts new file mode 100644 index 000000000000..723a13265873 --- /dev/null +++ b/packages/devextreme/vite.config.ts @@ -0,0 +1,27 @@ +import path from 'path'; +import { defineConfig } from 'vite'; +import devextremeInferno from './build/vite-plugin-devextreme'; + +export default defineConfig({ + root: './playground', + plugins: [devextremeInferno()], + esbuild: false, + server: { + port: 3000, + fs: { + allow: ['..', '.', '../..'], + }, + hmr: true, + }, + resolve: { + alias: { + 'core': path.resolve(__dirname, './js/core'), + 'common': path.resolve(__dirname, './js/common'), + 'data': path.resolve(__dirname, './js/data'), + 'ui': path.resolve(__dirname, './js/ui'), + '@js': path.resolve(__dirname, './js'), + '@ts': path.resolve(__dirname, './js/__internal'), + '__internal': path.resolve(__dirname, './js/__internal'), + }, + }, +}); From 00b8bca532bc3b10150cfe7a2142537b9160317c Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Sun, 15 Mar 2026 09:52:01 -0300 Subject: [PATCH 02/16] Fix Vite plugin: remove type-only class fields, add jQuery integration import - Added removeUninitializedClassFields babel visitor to strip type-only class property declarations that would otherwise initialize as undefined and overwrite values set during _init() in the legacy OOP system - Added @babel/plugin-transform-class-properties (loose) to convert field initializers to constructor assignments - Added @babel/plugin-transform-typescript as explicit dependency - Added jQuery integration side-effect import in scheduler-example.ts - Removed vite-plugin-inferno dependency, replaced with custom plugin Co-Authored-By: Claude Opus 4.6 (1M context) --- .../build/vite-plugin-devextreme.ts | 47 ++++++++++++++----- packages/devextreme/package.json | 1 + .../playground/scheduler-example.ts | 1 + 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/packages/devextreme/build/vite-plugin-devextreme.ts b/packages/devextreme/build/vite-plugin-devextreme.ts index 46e006eac508..c87e5e58a9fa 100644 --- a/packages/devextreme/build/vite-plugin-devextreme.ts +++ b/packages/devextreme/build/vite-plugin-devextreme.ts @@ -1,7 +1,24 @@ import { transformAsync } from '@babel/core'; -import type { Plugin } from 'vite'; +import type { PluginOption } from 'vite'; -export default function devextremeInfernoPlugin(): Plugin { +function removeUninitializedClassFields(): unknown { + return { + visitor: { + ClassProperty(path: { node: { value: unknown }; remove: () => void }) { + if (path.node.value === null || path.node.value === undefined) { + path.remove(); + } + }, + ClassDeclaration(path: { node: { body: { body: Array<{ type: string; value: unknown }> } } }) { + path.node.body.body = path.node.body.body.filter( + (member) => !(member.type === 'ClassProperty' && (member.value === null || member.value === undefined)), + ); + }, + }, + }; +} + +export default function devextremeInfernoPlugin(): PluginOption { return { name: 'devextreme-inferno', enforce: 'pre', @@ -14,24 +31,30 @@ export default function devextremeInfernoPlugin(): Plugin { const isTSX = id.endsWith('.tsx'); const isTS = id.endsWith('.ts') || isTSX; - const plugins: unknown[] = [ - ['@babel/plugin-proposal-decorators', { legacy: true }], - ['@babel/plugin-transform-class-properties', { loose: true }], - 'babel-plugin-inferno', - ]; + const plugins: unknown[] = []; - const presets: unknown[] = []; if (isTS) { - presets.push([ - '@babel/preset-typescript', - { isTSX, allExtensions: true }, + plugins.push([ + '@babel/plugin-transform-typescript', + { + isTSX, + allExtensions: true, + allowDeclareFields: true, + optimizeConstEnums: true, + }, ]); } + plugins.push( + removeUninitializedClassFields, + ['@babel/plugin-proposal-decorators', { legacy: true }], + ['@babel/plugin-transform-class-properties', { loose: true }], + 'babel-plugin-inferno', + ); + const result = await transformAsync(code, { filename: id, plugins, - presets, sourceMaps: true, }); diff --git a/packages/devextreme/package.json b/packages/devextreme/package.json index 60a0c35bafcf..68959b967897 100644 --- a/packages/devextreme/package.json +++ b/packages/devextreme/package.json @@ -73,6 +73,7 @@ "@babel/plugin-transform-class-properties": "7.23.3", "@babel/plugin-transform-modules-commonjs": "7.28.6", "@babel/plugin-transform-runtime": "7.29.0", + "@babel/plugin-transform-typescript": "7.23.6", "@babel/preset-env": "7.29.0", "@babel/preset-typescript": "7.23.3", "@devextreme-generator/angular": "3.0.12", diff --git a/packages/devextreme/playground/scheduler-example.ts b/packages/devextreme/playground/scheduler-example.ts index 35d76747c243..8f71f4bbad3c 100644 --- a/packages/devextreme/playground/scheduler-example.ts +++ b/packages/devextreme/playground/scheduler-example.ts @@ -1,3 +1,4 @@ +import '../js/__internal/integration/jquery'; import $ from 'jquery'; import { setupThemeSelector } from './newthemeSelector'; import Scheduler from '../js/__internal/scheduler/m_scheduler'; From 71691d88333cc75a9cf6924a36e5f92be145c40f Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Sun, 15 Mar 2026 10:21:49 -0300 Subject: [PATCH 03/16] Fix class field initialization order in Vite plugin Move initialized class field assignments to end of constructor body to match TypeScript's legacy useDefineForClassFields:false behavior. This fixes cases where field initializers depend on constructor parameter properties (e.g. this._workSpace._dateTableScrollable). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../build/vite-plugin-devextreme.ts | 64 +++++++++++++++++-- 1 file changed, 60 insertions(+), 4 deletions(-) diff --git a/packages/devextreme/build/vite-plugin-devextreme.ts b/packages/devextreme/build/vite-plugin-devextreme.ts index c87e5e58a9fa..f2e924936772 100644 --- a/packages/devextreme/build/vite-plugin-devextreme.ts +++ b/packages/devextreme/build/vite-plugin-devextreme.ts @@ -9,10 +9,66 @@ function removeUninitializedClassFields(): unknown { path.remove(); } }, - ClassDeclaration(path: { node: { body: { body: Array<{ type: string; value: unknown }> } } }) { - path.node.body.body = path.node.body.body.filter( - (member) => !(member.type === 'ClassProperty' && (member.value === null || member.value === undefined)), + }, + }; +} + +function moveFieldInitializersToConstructor(): unknown { + return { + visitor: { + Class(path: { node: { body: { body: unknown[] } } }) { + const body = path.node.body.body; + + type ClassMember = { + type: string; + kind?: string; + key?: { name: string }; + value?: unknown; + static?: boolean; + body?: { body: unknown[] }; + }; + + const fieldsToMove: ClassMember[] = []; + const remaining: unknown[] = []; + + for (const member of body as ClassMember[]) { + if ( + member.type === 'ClassProperty' + && member.value != null + && !member.static + ) { + fieldsToMove.push(member); + } else { + remaining.push(member); + } + } + + if (fieldsToMove.length === 0) return; + + const ctor = (remaining as ClassMember[]).find( + (m) => m.type === 'ClassMethod' && m.kind === 'constructor', ); + + if (!ctor) return; + + const assignments = fieldsToMove.map((field) => ({ + type: 'ExpressionStatement', + expression: { + type: 'AssignmentExpression', + operator: '=', + left: { + type: 'MemberExpression', + object: { type: 'ThisExpression' }, + property: { type: 'Identifier', name: field.key!.name }, + computed: false, + }, + right: field.value, + }, + })); + + ctor.body!.body.push(...assignments); + + path.node.body.body = remaining; }, }, }; @@ -47,8 +103,8 @@ export default function devextremeInfernoPlugin(): PluginOption { plugins.push( removeUninitializedClassFields, + moveFieldInitializersToConstructor, ['@babel/plugin-proposal-decorators', { legacy: true }], - ['@babel/plugin-transform-class-properties', { loose: true }], 'babel-plugin-inferno', ); From 0016a2073576e652b252cb10e88514b5231245d6 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Fri, 20 Mar 2026 11:14:27 -0300 Subject: [PATCH 04/16] Update pnpm-lock.yaml with vite and babel plugin deps --- pnpm-lock.yaml | 144 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 113 insertions(+), 31 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 944a73db8472..3a6c2e25f6a1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1370,15 +1370,27 @@ importers: '@babel/parser': specifier: 7.29.0 version: 7.29.0 + '@babel/plugin-proposal-decorators': + specifier: 7.23.9 + version: 7.23.9(@babel/core@7.29.0) + '@babel/plugin-transform-class-properties': + specifier: 7.23.3 + version: 7.23.3(@babel/core@7.29.0) '@babel/plugin-transform-modules-commonjs': specifier: 7.28.6 version: 7.28.6(@babel/core@7.29.0) '@babel/plugin-transform-runtime': specifier: 7.29.0 version: 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-typescript': + specifier: 7.23.6 + version: 7.23.6(@babel/core@7.29.0) '@babel/preset-env': specifier: 7.29.0 version: 7.29.0(@babel/core@7.29.0) + '@babel/preset-typescript': + specifier: 7.23.3 + version: 7.23.3(@babel/core@7.29.0) '@devextreme-generator/angular': specifier: 3.0.12 version: 3.0.12(5o33rugezaoatweidwz7xw6yie) @@ -1796,6 +1808,9 @@ importers: vinyl-named: specifier: 1.1.0 version: 1.1.0 + vite: + specifier: ^7.1.3 + version: 7.3.0(@types/node@20.12.8)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass-embedded@1.93.3)(sass@1.97.1)(terser@5.46.0)(yaml@2.5.0) webpack: specifier: 5.105.4 version: 5.105.4(@swc/core@1.15.3) @@ -3076,6 +3091,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0 + '@babel/plugin-proposal-decorators@7.23.9': + resolution: {integrity: sha512-hJhBCb0+NnTWybvWq2WpbCYDOcflSbx0t+BYP65e5R9GVnukiDTi+on5bFkk4p7QGuv190H6KfNiV9Knf/3cZA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-proposal-decorators@7.25.9': resolution: {integrity: sha512-smkNLL/O1ezy9Nhy4CNosc4Va+1wo5w4gzSZeLe6y6dM4mmHfYOCPolXQPHQxonZCF+ZyebxN9vqOolkYrSn5g==} engines: {node: '>=6.9.0'} @@ -3274,8 +3295,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-class-properties@7.25.9': - resolution: {integrity: sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==} + '@babel/plugin-transform-class-properties@7.23.3': + resolution: {integrity: sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -3634,6 +3655,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-typescript@7.23.6': + resolution: {integrity: sha512-6cBG5mBvUu4VUD04OHKnYzbuHNP8huDsD3EDqqpIpsswTDoqHCjLoHb6+QgsV1WsT2nipRqCPgxD3LXnEO7XfA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-typescript@7.28.5': resolution: {integrity: sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA==} engines: {node: '>=6.9.0'} @@ -3705,6 +3732,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/preset-typescript@7.23.3': + resolution: {integrity: sha512-17oIGVlqz6CchO9RFYn5U6ZpWRZIngayYCtrPRSgANSwC2V1Jb+iP74nVxzzXJte8b8BYxrL1yY96xfhTBrNNQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/preset-typescript@7.28.5': resolution: {integrity: sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==} engines: {node: '>=6.9.0'} @@ -20000,7 +20033,7 @@ snapshots: '@babel/helper-annotate-as-pure@7.27.3': dependencies: - '@babel/types': 7.28.6 + '@babel/types': 7.29.0 '@babel/helper-compilation-targets@7.28.6': dependencies: @@ -20018,7 +20051,7 @@ snapshots: '@babel/helper-optimise-call-expression': 7.27.1 '@babel/helper-replace-supers': 7.28.6(@babel/core@7.26.10) '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/traverse': 7.28.6 + '@babel/traverse': 7.29.0 semver: 6.3.1 transitivePeerDependencies: - supports-color @@ -20031,7 +20064,7 @@ snapshots: '@babel/helper-optimise-call-expression': 7.27.1 '@babel/helper-replace-supers': 7.28.6(@babel/core@7.28.5) '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/traverse': 7.28.6 + '@babel/traverse': 7.29.0 semver: 6.3.1 transitivePeerDependencies: - supports-color @@ -20044,7 +20077,7 @@ snapshots: '@babel/helper-optimise-call-expression': 7.27.1 '@babel/helper-replace-supers': 7.28.6(@babel/core@7.28.6) '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/traverse': 7.28.6 + '@babel/traverse': 7.29.0 semver: 6.3.1 transitivePeerDependencies: - supports-color @@ -20057,7 +20090,7 @@ snapshots: '@babel/helper-optimise-call-expression': 7.27.1 '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/traverse': 7.28.6 + '@babel/traverse': 7.29.0 semver: 6.3.1 transitivePeerDependencies: - supports-color @@ -20171,8 +20204,8 @@ snapshots: '@babel/helper-member-expression-to-functions@7.28.5': dependencies: - '@babel/traverse': 7.28.6 - '@babel/types': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color @@ -20230,7 +20263,7 @@ snapshots: '@babel/helper-optimise-call-expression@7.27.1': dependencies: - '@babel/types': 7.28.6 + '@babel/types': 7.29.0 '@babel/helper-plugin-utils@7.27.1': {} @@ -20277,7 +20310,7 @@ snapshots: '@babel/core': 7.26.10 '@babel/helper-member-expression-to-functions': 7.28.5 '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/traverse': 7.28.6 + '@babel/traverse': 7.29.0 transitivePeerDependencies: - supports-color @@ -20286,7 +20319,7 @@ snapshots: '@babel/core': 7.28.5 '@babel/helper-member-expression-to-functions': 7.28.5 '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/traverse': 7.28.6 + '@babel/traverse': 7.29.0 transitivePeerDependencies: - supports-color @@ -20295,7 +20328,7 @@ snapshots: '@babel/core': 7.28.6 '@babel/helper-member-expression-to-functions': 7.28.5 '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/traverse': 7.28.6 + '@babel/traverse': 7.29.0 transitivePeerDependencies: - supports-color @@ -20304,14 +20337,14 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-member-expression-to-functions': 7.28.5 '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/traverse': 7.28.6 + '@babel/traverse': 7.29.0 transitivePeerDependencies: - supports-color '@babel/helper-skip-transparent-expression-wrappers@7.27.1': dependencies: - '@babel/traverse': 7.28.6 - '@babel/types': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color @@ -20328,8 +20361,8 @@ snapshots: '@babel/helper-wrap-function@7.28.3': dependencies: '@babel/template': 7.28.6 - '@babel/traverse': 7.28.6 - '@babel/types': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color @@ -20501,6 +20534,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-proposal-decorators@7.23.9(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-decorators': 7.25.9(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + '@babel/plugin-proposal-decorators@7.25.9(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -20974,7 +21016,7 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-class-properties@7.25.9(@babel/core@7.29.0)': + '@babel/plugin-transform-class-properties@7.23.3(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) @@ -22208,6 +22250,16 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-transform-typescript@7.23.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-typescript@7.28.5(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -22661,6 +22713,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/preset-typescript@7.23.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-typescript': 7.23.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + '@babel/preset-typescript@7.28.5(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -24934,8 +24997,8 @@ snapshots: '@nx/js@22.4.5(@babel/traverse@7.29.0)(@swc/core@1.15.3)(nx@22.4.5(@swc/core@1.15.3))': dependencies: '@babel/core': 7.29.0 - '@babel/plugin-proposal-decorators': 7.25.9(@babel/core@7.29.0) - '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-proposal-decorators': 7.23.9(@babel/core@7.29.0) + '@babel/plugin-transform-class-properties': 7.23.3(@babel/core@7.29.0) '@babel/plugin-transform-runtime': 7.29.0(@babel/core@7.29.0) '@babel/preset-env': 7.29.0(@babel/core@7.29.0) '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) @@ -26470,7 +26533,7 @@ snapshots: '@types/babel__traverse@7.20.6': dependencies: - '@babel/types': 7.28.6 + '@babel/types': 7.29.0 '@types/babel__traverse@7.28.0': dependencies: @@ -36087,7 +36150,7 @@ snapshots: '@babel/generator': 7.28.6 '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.29.0) - '@babel/types': 7.28.6 + '@babel/types': 7.29.0 '@jest/expect-utils': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 @@ -36112,7 +36175,7 @@ snapshots: '@babel/generator': 7.28.6 '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.29.0) - '@babel/types': 7.28.6 + '@babel/types': 7.29.0 '@jest/expect-utils': 30.2.0 '@jest/get-type': 30.1.0 '@jest/snapshot-utils': 30.2.0 @@ -39372,8 +39435,8 @@ snapshots: react-docgen@7.1.1: dependencies: '@babel/core': 7.29.0 - '@babel/traverse': 7.28.6 - '@babel/types': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.28.0 '@types/doctrine': 0.0.9 @@ -39387,8 +39450,8 @@ snapshots: react-docgen@8.0.2: dependencies: '@babel/core': 7.29.0 - '@babel/traverse': 7.28.6 - '@babel/types': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.28.0 '@types/doctrine': 0.0.9 @@ -41894,12 +41957,12 @@ snapshots: testcafe@3.7.2: dependencies: '@babel/core': 7.29.0 - '@babel/plugin-proposal-decorators': 7.25.9(@babel/core@7.29.0) + '@babel/plugin-proposal-decorators': 7.23.9(@babel/core@7.29.0) '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.29.0) '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0) '@babel/plugin-transform-async-generator-functions': 7.26.8(@babel/core@7.29.0) '@babel/plugin-transform-async-to-generator': 7.25.9(@babel/core@7.29.0) - '@babel/plugin-transform-class-properties': 7.25.9(@babel/core@7.29.0) + '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.29.0) '@babel/plugin-transform-class-static-block': 7.26.0(@babel/core@7.29.0) '@babel/plugin-transform-exponentiation-operator': 7.27.1(@babel/core@7.29.0) '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.29.0) @@ -43393,12 +43456,31 @@ snapshots: terser: 5.46.0 yaml: 2.8.1 + vite@7.3.0(@types/node@20.12.8)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass-embedded@1.93.3)(sass@1.97.1)(terser@5.46.0)(yaml@2.5.0): + dependencies: + esbuild: 0.27.2 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.8 + rollup: 4.59.0 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 20.12.8 + fsevents: 2.3.3 + jiti: 2.6.1 + less: 4.4.2 + lightningcss: 1.30.2 + sass: 1.97.1 + sass-embedded: 1.93.3 + terser: 5.46.0 + yaml: 2.5.0 + vite@7.3.0(@types/node@20.12.8)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass-embedded@1.97.1)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.1): dependencies: esbuild: 0.27.2 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 - postcss: 8.5.6 + postcss: 8.5.8 rollup: 4.59.0 tinyglobby: 0.2.15 optionalDependencies: From 97bef08cee2f17f55300ec2d6d36c4f76b062864 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Fri, 20 Mar 2026 11:44:00 -0300 Subject: [PATCH 05/16] Playground - use jQuery API for Scheduler --- .../playground/scheduler-example.ts | 32 +++---------------- 1 file changed, 5 insertions(+), 27 deletions(-) diff --git a/packages/devextreme/playground/scheduler-example.ts b/packages/devextreme/playground/scheduler-example.ts index 8f71f4bbad3c..16c2a425e48e 100644 --- a/packages/devextreme/playground/scheduler-example.ts +++ b/packages/devextreme/playground/scheduler-example.ts @@ -1,34 +1,28 @@ -import '../js/__internal/integration/jquery'; +import '../js/integration/jquery'; import $ from 'jquery'; import { setupThemeSelector } from './newthemeSelector'; -import Scheduler from '../js/__internal/scheduler/m_scheduler'; const dataSource = [ { text: "Meeting with John", startDate: new Date(2024, 0, 10, 9, 0), endDate: new Date(2024, 0, 10, 10, 30), - allDay: false }, { - text: "Conference Call", + text: "Conference Call", startDate: new Date(2024, 0, 10, 14, 0), endDate: new Date(2024, 0, 10, 15, 0), - allDay: false }, { text: "Team Building Event", startDate: new Date(2024, 0, 11, 10, 0), endDate: new Date(2024, 0, 11, 17, 0), - allDay: false - } + }, ]; -window.addEventListener('load', () => +window.addEventListener('load', () => setupThemeSelector('theme-selector').then(() => { - - - new (Scheduler as any)($('#container'), { + $('#container').dxScheduler({ dataSource, views: ['day', 'week', 'workWeek', 'month'], currentView: 'week', @@ -36,21 +30,5 @@ window.addEventListener('load', () => startDayHour: 8, endDayHour: 18, height: 600, - editing: { - allowAdding: true, - allowDeleting: true, - allowUpdating: true, - allowResizing: true, - allowDragging: true - }, - onAppointmentAdded: (e) => { - console.log('Appointment added:', e.appointmentData); - }, - onAppointmentUpdated: (e) => { - console.log('Appointment updated:', e.appointmentData); - }, - onAppointmentDeleted: (e) => { - console.log('Appointment deleted:', e.appointmentData); - } }); })); From e4c39058cd0dd7cbcedb33c539d14d0805abb972 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Fri, 20 Mar 2026 12:42:29 -0300 Subject: [PATCH 06/16] Playground - add widget catalog with all 83 widgets and Playwright smoke tests --- .../build/vite-plugin-devextreme.ts | 23 + packages/devextreme/package.json | 2 + packages/devextreme/playground/catalog.ts | 37 + packages/devextreme/playground/index.html | 43 +- .../playground/playwright.config.ts | 13 + .../playground/scheduler-example.ts | 1 + .../devextreme/playground/tests/smoke.spec.ts | 15 + packages/devextreme/playground/widget-ids.ts | 21 + .../devextreme/playground/widgets/registry.ts | 779 ++++++++++++++++++ pnpm-lock.yaml | 38 + 10 files changed, 957 insertions(+), 15 deletions(-) create mode 100644 packages/devextreme/playground/catalog.ts create mode 100644 packages/devextreme/playground/playwright.config.ts create mode 100644 packages/devextreme/playground/tests/smoke.spec.ts create mode 100644 packages/devextreme/playground/widget-ids.ts create mode 100644 packages/devextreme/playground/widgets/registry.ts diff --git a/packages/devextreme/build/vite-plugin-devextreme.ts b/packages/devextreme/build/vite-plugin-devextreme.ts index f2e924936772..55abb6942c92 100644 --- a/packages/devextreme/build/vite-plugin-devextreme.ts +++ b/packages/devextreme/build/vite-plugin-devextreme.ts @@ -1,6 +1,28 @@ import { transformAsync } from '@babel/core'; import type { PluginOption } from 'vite'; +function removeCjsExportsAssignments(): unknown { + return { + visitor: { + ExpressionStatement(path: { + node: { expression: { type: string; operator?: string; left?: { type: string; object?: { type: string; name?: string } } } }; + remove: () => void; + }) { + const { expression } = path.node; + if ( + expression.type === 'AssignmentExpression' + && expression.operator === '=' + && expression.left?.type === 'MemberExpression' + && expression.left.object?.type === 'Identifier' + && expression.left.object.name === 'exports' + ) { + path.remove(); + } + }, + }, + }; +} + function removeUninitializedClassFields(): unknown { return { visitor: { @@ -102,6 +124,7 @@ export default function devextremeInfernoPlugin(): PluginOption { } plugins.push( + removeCjsExportsAssignments, removeUninitializedClassFields, moveFieldInitializersToConstructor, ['@babel/plugin-proposal-decorators', { legacy: true }], diff --git a/packages/devextreme/package.json b/packages/devextreme/package.json index 68959b967897..93ab47a2d243 100644 --- a/packages/devextreme/package.json +++ b/packages/devextreme/package.json @@ -215,6 +215,7 @@ "uuid": "9.0.1", "vinyl": "2.2.1", "vinyl-named": "1.1.0", + "@playwright/test": "^1.50.0", "vite": "^7.1.3", "webpack": "5.105.4", "webpack-stream": "7.0.0", @@ -252,6 +253,7 @@ "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:playground": "playwright test --config playground/playwright.config.ts", "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", diff --git a/packages/devextreme/playground/catalog.ts b/packages/devextreme/playground/catalog.ts new file mode 100644 index 000000000000..255a5f203090 --- /dev/null +++ b/packages/devextreme/playground/catalog.ts @@ -0,0 +1,37 @@ +import '../js/integration/jquery'; +import $ from 'jquery'; +import { registry } from './widgets/registry'; + +const $nav = $('#nav'); +const $container = $('#container'); +const $header = $('#header'); + +Object.entries(registry).forEach(([id, { label }]) => { + const $li = $('
  • ').appendTo($nav); + $('').attr('href', `#${id}`).text(label).appendTo($li); +}); + +function loadWidget(id: string): void { + const entry = registry[id]; + if (!entry) return; + + $nav.find('a').removeClass('active'); + $nav.find(`a[href="#${id}"]`).addClass('active'); + $header.text(entry.label); + $container.empty(); + + const $el = $('
    ').appendTo($container); + entry.init($el); +} + +window.addEventListener('hashchange', () => { + loadWidget(location.hash.slice(1)); +}); + +const initial = location.hash.slice(1); +if (initial && registry[initial]) { + loadWidget(initial); +} else { + const first = Object.keys(registry)[0]; + location.hash = first; +} diff --git a/packages/devextreme/playground/index.html b/packages/devextreme/playground/index.html index e1f3659444ed..320dad553383 100644 --- a/packages/devextreme/playground/index.html +++ b/packages/devextreme/playground/index.html @@ -3,23 +3,36 @@ - DevExtreme HMR Playground + DevExtreme Playground + - -
    - + + +
    +
    - - + diff --git a/packages/devextreme/playground/playwright.config.ts b/packages/devextreme/playground/playwright.config.ts new file mode 100644 index 000000000000..847a3d07429f --- /dev/null +++ b/packages/devextreme/playground/playwright.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests', + use: { + baseURL: 'http://localhost:3000', + }, + webServer: { + command: 'pnpm run dev:playground', + port: 3000, + reuseExistingServer: true, + }, +}); diff --git a/packages/devextreme/playground/scheduler-example.ts b/packages/devextreme/playground/scheduler-example.ts index 16c2a425e48e..2f61302a0962 100644 --- a/packages/devextreme/playground/scheduler-example.ts +++ b/packages/devextreme/playground/scheduler-example.ts @@ -1,4 +1,5 @@ import '../js/integration/jquery'; +import '../js/ui/scheduler'; import $ from 'jquery'; import { setupThemeSelector } from './newthemeSelector'; diff --git a/packages/devextreme/playground/tests/smoke.spec.ts b/packages/devextreme/playground/tests/smoke.spec.ts new file mode 100644 index 000000000000..7f15123eadc6 --- /dev/null +++ b/packages/devextreme/playground/tests/smoke.spec.ts @@ -0,0 +1,15 @@ +import { test, expect } from '@playwright/test'; +import { widgetIds } from '../widget-ids'; + +for (const widgetId of widgetIds) { + test(`${widgetId} renders without console errors`, async ({ page }) => { + const errors: string[] = []; + page.on('console', (msg) => { if (msg.type() === 'error') errors.push(msg.text()); }); + page.on('pageerror', (err) => errors.push(err.message)); + + await page.goto(`/#${widgetId}`); + await page.waitForTimeout(800); + + expect(errors, `Console errors for ${widgetId}: ${errors.join('\n')}`).toHaveLength(0); + }); +} diff --git a/packages/devextreme/playground/widget-ids.ts b/packages/devextreme/playground/widget-ids.ts new file mode 100644 index 000000000000..0ff4ae59d83e --- /dev/null +++ b/packages/devextreme/playground/widget-ids.ts @@ -0,0 +1,21 @@ +export const widgetIds = [ + 'accordion', 'actionSheet', 'autocomplete', 'barGauge', 'box', + 'bullet', 'button', 'buttonGroup', 'calendar', 'chat', 'checkBox', + 'chart', 'circularGauge', 'colorBox', 'contextMenu', 'dataGrid', + 'dateBox', 'dateRangeBox', 'diagram', 'draggable', 'drawer', + 'dropDownBox', 'dropDownButton', 'fileManager', 'fileUploader', + 'filterBuilder', 'form', 'funnel', 'gallery', 'gantt', + 'htmlEditor', 'linearGauge', 'list', 'loadIndicator', 'loadPanel', + 'lookup', 'map', 'menu', 'multiView', 'numberBox', 'pagination', + 'pieChart', 'pivotGrid', 'pivotGridFieldChooser', 'polarChart', + 'popover', 'popup', 'progressBar', 'radioGroup', 'rangeSelector', + 'rangeSlider', 'recurrenceEditor', 'resizable', 'responsiveBox', + 'sankey', 'scheduler', 'scrollView', 'selectBox', 'slider', + 'sortable', 'sparkline', 'speedDialAction', 'splitter', 'stepper', + 'switch', 'tabPanel', 'tabs', 'tagBox', 'textArea', 'textBox', + 'tileView', 'toast', 'toolbar', 'tooltip', 'treeList', 'treeMap', + 'treeView', 'validationGroup', 'validationSummary', 'validator', + 'vectorMap', +] as const; + +export type WidgetId = typeof widgetIds[number]; diff --git a/packages/devextreme/playground/widgets/registry.ts b/packages/devextreme/playground/widgets/registry.ts new file mode 100644 index 000000000000..a82028b3cd8f --- /dev/null +++ b/packages/devextreme/playground/widgets/registry.ts @@ -0,0 +1,779 @@ +import type { WidgetId } from '../widget-ids'; +import $ from 'jquery'; + +import '../../js/ui/accordion'; +import '../../js/ui/action_sheet'; +import '../../js/ui/autocomplete'; +import '../../js/ui/box'; +import '../../js/ui/button'; +import '../../js/ui/button_group'; +import '../../js/ui/calendar'; +import '../../js/ui/chat'; +import '../../js/ui/check_box'; +import '../../js/ui/color_box'; +import '../../js/ui/context_menu'; +import '../../js/ui/data_grid'; +import '../../js/ui/date_box'; +import '../../js/ui/date_range_box'; +import '../../js/ui/diagram'; +import '../../js/ui/draggable'; +import '../../js/ui/drawer'; +import '../../js/ui/drop_down_box'; +import '../../js/ui/drop_down_button'; +import '../../js/ui/file_manager'; +import '../../js/ui/file_uploader'; +import '../../js/ui/filter_builder'; +import '../../js/ui/form'; +import '../../js/ui/gallery'; +import '../../js/ui/gantt'; +import '../../js/ui/html_editor'; +import '../../js/ui/list'; +import '../../js/ui/load_indicator'; +import '../../js/ui/load_panel'; +import '../../js/ui/lookup'; +import '../../js/ui/map'; +import '../../js/ui/menu'; +import '../../js/ui/multi_view'; +import '../../js/ui/number_box'; +import '../../js/ui/pagination'; +import '../../js/ui/pivot_grid'; +import '../../js/ui/pivot_grid_field_chooser'; +import '../../js/ui/popover'; +import '../../js/ui/popup'; +import '../../js/ui/progress_bar'; +import '../../js/ui/radio_group'; +import '../../js/ui/range_slider'; +import '../../js/ui/recurrence_editor'; +import '../../js/ui/resizable'; +import '../../js/ui/responsive_box'; +import '../../js/ui/scheduler'; +import '../../js/ui/scroll_view'; +import '../../js/ui/select_box'; +import '../../js/ui/slider'; +import '../../js/ui/sortable'; +import '../../js/ui/speed_dial_action'; +import '../../js/ui/splitter'; +import '../../js/ui/stepper'; +import '../../js/ui/switch'; +import '../../js/ui/tab_panel'; +import '../../js/ui/tabs'; +import '../../js/ui/tag_box'; +import '../../js/ui/text_area'; +import '../../js/ui/text_box'; +import '../../js/ui/tile_view'; +import '../../js/ui/toast'; +import '../../js/ui/toolbar'; +import '../../js/ui/tooltip'; +import '../../js/ui/tree_list'; +import '../../js/ui/tree_view'; +import '../../js/ui/validation_group'; +import '../../js/ui/validation_summary'; +import '../../js/ui/validator'; +import '../../js/viz/bar_gauge'; +import '../../js/viz/bullet'; +import '../../js/viz/chart'; +import '../../js/viz/circular_gauge'; +import '../../js/viz/funnel'; +import '../../js/viz/linear_gauge'; +import '../../js/viz/pie_chart'; +import '../../js/viz/polar_chart'; +import '../../js/viz/range_selector'; +import '../../js/viz/sankey'; +import '../../js/viz/sparkline'; +import '../../js/viz/tree_map'; +import '../../js/viz/vector_map'; + +export type WidgetInit = ($el: JQuery) => void; + +interface RegistryEntry { + label: string; + init: WidgetInit; +} + +const apptData = [ + { text: 'Meeting', startDate: new Date(2024, 0, 10, 9, 0), endDate: new Date(2024, 0, 10, 10, 30) }, + { text: 'Conference Call', startDate: new Date(2024, 0, 10, 14, 0), endDate: new Date(2024, 0, 10, 15, 0) }, +]; + +const gridData = [ + { id: 1, name: 'Alice', city: 'London', age: 32 }, + { id: 2, name: 'Bob', city: 'Paris', age: 25 }, + { id: 3, name: 'Carol', city: 'Berlin', age: 41 }, +]; + +const chartData = [ + { arg: 'Jan', val: 30 }, { arg: 'Feb', val: 50 }, { arg: 'Mar', val: 40 }, + { arg: 'Apr', val: 70 }, { arg: 'May', val: 60 }, { arg: 'Jun', val: 80 }, +]; + +const treeData = [ + { id: 1, parentId: 0, text: 'Documents' }, + { id: 2, parentId: 1, text: 'Reports' }, + { id: 3, parentId: 1, text: 'Notes' }, + { id: 4, parentId: 0, text: 'Images' }, +]; + +const dx = ($el: JQuery) => $el as any; + +export const registry: Record = { + accordion: { + label: 'Accordion', + init: ($el) => dx($el).dxAccordion({ dataSource: ['Item 1', 'Item 2', 'Item 3'], width: 400 }), + }, + actionSheet: { + label: 'ActionSheet', + init: ($el) => { + const $btn = $('
    ').appendTo($el); + dx($btn).dxButton({ + text: 'Show ActionSheet', + onClick: () => dx($('
    ').appendTo($el)).dxActionSheet({ + title: 'Actions', + items: [{ text: 'Action 1' }, { text: 'Action 2' }], + visible: true, + target: $btn, + }), + }); + }, + }, + autocomplete: { + label: 'Autocomplete', + init: ($el) => dx($el).dxAutocomplete({ dataSource: ['Alice', 'Bob', 'Carol', 'Dave'], placeholder: 'Type a name...' }), + }, + barGauge: { + label: 'BarGauge', + init: ($el) => dx($el).dxBarGauge({ values: [0.3, 0.6, 0.9], startValue: 0, endValue: 1, height: 300 }), + }, + box: { + label: 'Box', + init: ($el) => dx($el).dxBox({ + direction: 'row', + height: 100, + items: [ + { html: '
    Box 1
    ', ratio: 1 }, + { html: '
    Box 2
    ', ratio: 1 }, + ], + }), + }, + bullet: { + label: 'Bullet', + init: ($el) => dx($el).dxBullet({ value: 65, startScaleValue: 0, endScaleValue: 100, target: 80, height: 60, width: 400 }), + }, + button: { + label: 'Button', + init: ($el) => dx($el).dxButton({ text: 'Click me', type: 'default', stylingMode: 'contained' }), + }, + buttonGroup: { + label: 'ButtonGroup', + init: ($el) => dx($el).dxButtonGroup({ + items: [{ text: 'Left' }, { text: 'Center' }, { text: 'Right' }], + selectionMode: 'single', + selectedItemKeys: ['Center'], + keyExpr: 'text', + }), + }, + calendar: { + label: 'Calendar', + init: ($el) => dx($el).dxCalendar({ value: new Date(), width: 280 }), + }, + chat: { + label: 'Chat', + init: ($el) => dx($el).dxChat({ height: 400, items: [] }), + }, + checkBox: { + label: 'CheckBox', + init: ($el) => dx($el).dxCheckBox({ text: 'Enable feature', value: true }), + }, + chart: { + label: 'Chart', + init: ($el) => dx($el).dxChart({ + dataSource: chartData, + series: [{ argumentField: 'arg', valueField: 'val', type: 'bar' }], + height: 300, + }), + }, + circularGauge: { + label: 'CircularGauge', + init: ($el) => dx($el).dxCircularGauge({ value: 65, height: 300, rangeContainer: { ranges: [] } }), + }, + colorBox: { + label: 'ColorBox', + init: ($el) => dx($el).dxColorBox({ value: '#0d6efd', width: 200 }), + }, + contextMenu: { + label: 'ContextMenu', + init: ($el) => { + $('
    Right-click here
    ').appendTo($el); + dx($('
    ').appendTo($el)).dxContextMenu({ + target: '#ctx-target', + items: [{ text: 'Copy' }, { text: 'Paste' }, { text: 'Delete' }], + }); + }, + }, + dataGrid: { + label: 'DataGrid', + init: ($el) => dx($el).dxDataGrid({ + dataSource: gridData, + keyExpr: 'id', + columns: ['name', 'city', 'age'], + showBorders: true, + }), + }, + dateBox: { + label: 'DateBox', + init: ($el) => dx($el).dxDateBox({ value: new Date(), type: 'date', width: 220 }), + }, + dateRangeBox: { + label: 'DateRangeBox', + init: ($el) => dx($el).dxDateRangeBox({ + startDate: new Date(2024, 0, 1), + endDate: new Date(2024, 0, 31), + width: 400, + }), + }, + diagram: { + label: 'Diagram', + init: ($el) => dx($el).dxDiagram({ height: 400 }), + }, + draggable: { + label: 'Draggable', + init: ($el) => { + const $box = $('
    Drag me
    ').appendTo($el); + dx($box).dxDraggable({ boundary: $el }); + }, + }, + drawer: { + label: 'Drawer', + init: ($el) => { + $el.css({ height: '300px', position: 'relative' }); + const $content = $('

    Main content area

    '); + dx($el).dxDrawer({ + opened: true, + template: () => $('
    Drawer panel
    '), + contentTemplate: () => $content, + height: 300, + }); + }, + }, + dropDownBox: { + label: 'DropDownBox', + init: ($el) => dx($el).dxDropDownBox({ + dataSource: ['Alice', 'Bob', 'Carol'], + value: 'Alice', + width: 300, + }), + }, + dropDownButton: { + label: 'DropDownButton', + init: ($el) => dx($el).dxDropDownButton({ + text: 'Options', + items: [{ text: 'Option 1' }, { text: 'Option 2' }, { text: 'Option 3' }], + }), + }, + fileManager: { + label: 'FileManager', + init: ($el) => dx($el).dxFileManager({ height: 400 }), + }, + fileUploader: { + label: 'FileUploader', + init: ($el) => dx($el).dxFileUploader({ multiple: false, accept: '*', uploadMode: 'useButton' }), + }, + filterBuilder: { + label: 'FilterBuilder', + init: ($el) => dx($el).dxFilterBuilder({ + fields: [ + { dataField: 'name', caption: 'Name' }, + { dataField: 'age', caption: 'Age', dataType: 'number' }, + ], + }), + }, + form: { + label: 'Form', + init: ($el) => dx($el).dxForm({ + formData: { name: 'Alice', email: 'alice@example.com', age: 32 }, + items: [ + { dataField: 'name', label: { text: 'Name' } }, + { dataField: 'email', label: { text: 'Email' } }, + { dataField: 'age', label: { text: 'Age' }, editorType: 'dxNumberBox' }, + ], + width: 400, + }), + }, + funnel: { + label: 'Funnel', + init: ($el) => dx($el).dxFunnel({ + dataSource: [ + { argument: 'Leads', value: 100 }, + { argument: 'Prospects', value: 60 }, + { argument: 'Customers', value: 30 }, + ], + argumentField: 'argument', + valueField: 'value', + height: 300, + }), + }, + gallery: { + label: 'Gallery', + init: ($el) => dx($el).dxGallery({ + dataSource: ['Item 1', 'Item 2', 'Item 3'], + height: 200, + width: 400, + itemTemplate: (item: string) => $(`
    ${item}
    `), + }), + }, + gantt: { + label: 'Gantt', + init: ($el) => dx($el).dxGantt({ + tasks: { + dataSource: [ + { id: 1, parentId: 0, title: 'Project', start: new Date(2024, 0, 1), end: new Date(2024, 0, 31), progress: 50 }, + { id: 2, parentId: 1, title: 'Task 1', start: new Date(2024, 0, 1), end: new Date(2024, 0, 15), progress: 70 }, + ], + }, + height: 300, + }), + }, + htmlEditor: { + label: 'HtmlEditor', + init: ($el) => dx($el).dxHtmlEditor({ + value: '

    Hello world!

    ', + height: 200, + toolbar: { items: ['bold', 'italic', 'underline'] }, + }), + }, + linearGauge: { + label: 'LinearGauge', + init: ($el) => dx($el).dxLinearGauge({ value: 65, height: 100 }), + }, + list: { + label: 'List', + init: ($el) => dx($el).dxList({ + dataSource: ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry'], + height: 250, + }), + }, + loadIndicator: { + label: 'LoadIndicator', + init: ($el) => { + $el.css({ display: 'flex', alignItems: 'center', gap: '12px' }); + dx($('
    ').appendTo($el)).dxLoadIndicator({ visible: true }); + $('Loading...').appendTo($el); + }, + }, + loadPanel: { + label: 'LoadPanel', + init: ($el) => { + $('
    Content area
    ').appendTo($el); + dx($('
    ').appendTo($el)).dxLoadPanel({ + visible: true, + showIndicator: true, + showPane: true, + shading: false, + position: { of: '#lp-target' }, + container: $el, + }); + }, + }, + lookup: { + label: 'Lookup', + init: ($el) => dx($el).dxLookup({ + dataSource: ['Alice', 'Bob', 'Carol', 'Dave'], + value: 'Alice', + width: 250, + }), + }, + map: { + label: 'Map', + init: ($el) => dx($el).dxMap({ + center: { lat: 40.7128, lng: -74.006 }, + zoom: 10, + height: 300, + width: '100%', + provider: 'google', + }), + }, + menu: { + label: 'Menu', + init: ($el) => dx($el).dxMenu({ + dataSource: [ + { text: 'File', items: [{ text: 'New' }, { text: 'Open' }, { text: 'Save' }] }, + { text: 'Edit', items: [{ text: 'Cut' }, { text: 'Copy' }, { text: 'Paste' }] }, + { text: 'View' }, + ], + }), + }, + multiView: { + label: 'MultiView', + init: ($el) => dx($el).dxMultiView({ + dataSource: [ + { html: '
    View 1 content
    ' }, + { html: '
    View 2 content
    ' }, + { html: '
    View 3 content
    ' }, + ], + height: 150, + }), + }, + numberBox: { + label: 'NumberBox', + init: ($el) => dx($el).dxNumberBox({ value: 42, min: 0, max: 100, showSpinButtons: true, width: 180 }), + }, + pagination: { + label: 'Pagination', + init: ($el) => dx($el).dxPagination({ pageCount: 10, pageIndex: 0, pageSize: 10, itemCount: 100 }), + }, + pieChart: { + label: 'PieChart', + init: ($el) => dx($el).dxPieChart({ + dataSource: [ + { arg: 'Apples', val: 35 }, + { arg: 'Oranges', val: 25 }, + { arg: 'Bananas', val: 20 }, + { arg: 'Grapes', val: 20 }, + ], + series: [{ argumentField: 'arg', valueField: 'val' }], + height: 300, + }), + }, + pivotGrid: { + label: 'PivotGrid', + init: ($el) => dx($el).dxPivotGrid({ + dataSource: { + fields: [ + { dataField: 'region', area: 'row' }, + { dataField: 'product', area: 'column' }, + { dataField: 'amount', area: 'data', summaryType: 'sum' }, + ], + store: [ + { region: 'North', product: 'A', amount: 100 }, + { region: 'South', product: 'B', amount: 200 }, + { region: 'North', product: 'B', amount: 150 }, + ], + }, + height: 300, + }), + }, + pivotGridFieldChooser: { + label: 'PivotGridFieldChooser', + init: ($el) => { + const dataSource = { fields: [{ dataField: 'name' }, { dataField: 'age' }], store: gridData } as any; + dx($el).dxPivotGridFieldChooser({ dataSource, height: 300 }); + }, + }, + polarChart: { + label: 'PolarChart', + init: ($el) => dx($el).dxPolarChart({ + dataSource: chartData, + series: [{ argumentField: 'arg', valueField: 'val', type: 'line' }], + height: 300, + }), + }, + popover: { + label: 'Popover', + init: ($el) => { + const $btn = $('
    ').appendTo($el); + dx($btn).dxButton({ text: 'Show Popover', onClick: () => dx($pop).dxPopover('show') }); + const $pop = $('
    ').appendTo($el); + dx($pop).dxPopover({ target: $btn, content: 'Hello from popover!' }); + }, + }, + popup: { + label: 'Popup', + init: ($el) => { + const $btn = $('
    ').appendTo($el); + const $pop = $('
    ').appendTo($el); + dx($pop).dxPopup({ title: 'Info', contentTemplate: () => '

    Popup content

    ', visible: false, width: 300, height: 200 }); + dx($btn).dxButton({ text: 'Show Popup', onClick: () => dx($pop).dxPopup('show') }); + }, + }, + progressBar: { + label: 'ProgressBar', + init: ($el) => dx($el).dxProgressBar({ value: 65, min: 0, max: 100, width: 300 }), + }, + radioGroup: { + label: 'RadioGroup', + init: ($el) => dx($el).dxRadioGroup({ + dataSource: ['Option A', 'Option B', 'Option C'], + value: 'Option A', + }), + }, + rangeSelector: { + label: 'RangeSelector', + init: ($el) => dx($el).dxRangeSelector({ + scale: { startValue: 0, endValue: 100, tickInterval: 10 }, + sliderMarker: { visible: true }, + value: [20, 70], + height: 120, + }), + }, + rangeSlider: { + label: 'RangeSlider', + init: ($el) => dx($el).dxRangeSlider({ min: 0, max: 100, start: 20, end: 70, width: 300 }), + }, + recurrenceEditor: { + label: 'RecurrenceEditor', + init: ($el) => dx($el).dxRecurrenceEditor({ value: 'FREQ=WEEKLY;BYDAY=MO,WE,FR' }), + }, + resizable: { + label: 'Resizable', + init: ($el) => { + const $box = $('
    Resize me
    ').appendTo($el); + dx($box).dxResizable({ width: 200, height: 100, minWidth: 100, minHeight: 60 }); + }, + }, + responsiveBox: { + label: 'ResponsiveBox', + init: ($el) => dx($el).dxResponsiveBox({ + rows: [{ ratio: 1 }], + cols: [{ ratio: 1 }, { ratio: 1 }], + items: [ + { html: '
    Left
    ', location: { row: 0, col: 0 } }, + { html: '
    Right
    ', location: { row: 0, col: 1 } }, + ], + height: 80, + }), + }, + sankey: { + label: 'Sankey', + init: ($el) => dx($el).dxSankey({ + dataSource: [ + { source: 'A', target: 'X', weight: 10 }, + { source: 'A', target: 'Y', weight: 5 }, + { source: 'B', target: 'X', weight: 7 }, + { source: 'B', target: 'Y', weight: 8 }, + ], + height: 300, + }), + }, + scheduler: { + label: 'Scheduler', + init: ($el) => dx($el).dxScheduler({ + dataSource: apptData, + views: ['week'], + currentView: 'week', + currentDate: new Date(2024, 0, 10), + startDayHour: 8, + endDayHour: 18, + height: 500, + }), + }, + scrollView: { + label: 'ScrollView', + init: ($el) => dx($el).dxScrollView({ + width: 300, + height: 150, + content: () => $('
    Scrollable content
    '.repeat(10) + '
    '), + }), + }, + selectBox: { + label: 'SelectBox', + init: ($el) => dx($el).dxSelectBox({ + dataSource: ['Apple', 'Banana', 'Cherry'], + value: 'Apple', + width: 220, + }), + }, + slider: { + label: 'Slider', + init: ($el) => dx($el).dxSlider({ min: 0, max: 100, value: 40, width: 300 }), + }, + sortable: { + label: 'Sortable', + init: ($el) => { + const $list = $('
    ').appendTo($el); + ['Item 1', 'Item 2', 'Item 3', 'Item 4'].forEach((t) => { + $(`
    ${t}
    `).appendTo($list); + }); + dx($list).dxSortable({ filter: 'div' }); + }, + }, + sparkline: { + label: 'Sparkline', + init: ($el) => dx($el).dxSparkline({ + dataSource: [{ val: 4 }, { val: 8 }, { val: 6 }, { val: 9 }, { val: 5 }, { val: 7 }], + valueField: 'val', + type: 'bar', + width: 200, + height: 40, + }), + }, + speedDialAction: { + label: 'SpeedDialAction', + init: ($el) => { + $el.css({ position: 'relative', height: '200px', background: '#f5f5f5' }); + dx($('
    ').appendTo($el)).dxSpeedDialAction({ label: 'Add', icon: 'plus' }); + }, + }, + splitter: { + label: 'Splitter', + init: ($el) => dx($el).dxSplitter({ + items: [ + { html: '
    Left pane
    ' }, + { html: '
    Right pane
    ' }, + ], + orientation: 'horizontal', + height: 200, + }), + }, + stepper: { + label: 'Stepper', + init: ($el) => dx($el).dxStepper({ + items: [{ title: 'Step 1' }, { title: 'Step 2' }, { title: 'Step 3' }], + selectedIndex: 0, + }), + }, + switch: { + label: 'Switch', + init: ($el) => dx($el).dxSwitch({ value: true, switchedOnText: 'ON', switchedOffText: 'OFF' }), + }, + tabPanel: { + label: 'TabPanel', + init: ($el) => dx($el).dxTabPanel({ + dataSource: [ + { title: 'Tab 1', html: '

    Content of Tab 1

    ' }, + { title: 'Tab 2', html: '

    Content of Tab 2

    ' }, + { title: 'Tab 3', html: '

    Content of Tab 3

    ' }, + ], + height: 150, + }), + }, + tabs: { + label: 'Tabs', + init: ($el) => dx($el).dxTabs({ + dataSource: [{ text: 'Home' }, { text: 'Products' }, { text: 'About' }], + selectedIndex: 0, + }), + }, + tagBox: { + label: 'TagBox', + init: ($el) => dx($el).dxTagBox({ + dataSource: ['Angular', 'React', 'Vue', 'jQuery'], + value: ['React', 'Vue'], + width: 350, + }), + }, + textArea: { + label: 'TextArea', + init: ($el) => dx($el).dxTextArea({ + value: 'Some text here...', + height: 100, + width: 300, + }), + }, + textBox: { + label: 'TextBox', + init: ($el) => dx($el).dxTextBox({ value: 'Hello world', width: 250, showClearButton: true }), + }, + tileView: { + label: 'TileView', + init: ($el) => dx($el).dxTileView({ + dataSource: [ + { text: 'Tile 1', color: '#e3f2fd' }, + { text: 'Tile 2', color: '#fce4ec' }, + { text: 'Tile 3', color: '#e8f5e9' }, + { text: 'Tile 4', color: '#fff3e0' }, + ], + itemTemplate: (item: { text: string; color: string }) => + $(`
    ${item.text}
    `), + baseItemHeight: 80, + baseItemWidth: 120, + height: 200, + }), + }, + toast: { + label: 'Toast', + init: ($el) => { + const $btn = $('
    ').appendTo($el); + const $toast = $('
    ').appendTo($el); + dx($toast).dxToast({ message: 'Hello from Toast!', type: 'success', displayTime: 2000 }); + dx($btn).dxButton({ text: 'Show Toast', onClick: () => dx($toast).dxToast('show') }); + }, + }, + toolbar: { + label: 'Toolbar', + init: ($el) => dx($el).dxToolbar({ + items: [ + { widget: 'dxButton', options: { text: 'New', icon: 'plus' }, location: 'before' }, + { widget: 'dxButton', options: { text: 'Save', icon: 'save' }, location: 'before' }, + { widget: 'dxTextBox', options: { placeholder: 'Search...' }, location: 'after' }, + ], + }), + }, + tooltip: { + label: 'Tooltip', + init: ($el) => { + const $target = $('
    Hover me
    ').appendTo($el); + dx($('
    ').appendTo($el)).dxTooltip({ + target: $target, + content: 'This is a tooltip!', + showEvent: 'mouseenter', + hideEvent: 'mouseleave', + }); + }, + }, + treeList: { + label: 'TreeList', + init: ($el) => dx($el).dxTreeList({ + dataSource: treeData, + keyExpr: 'id', + parentIdExpr: 'parentId', + columns: [{ dataField: 'text', caption: 'Name' }], + showBorders: true, + height: 250, + }), + }, + treeMap: { + label: 'TreeMap', + init: ($el) => dx($el).dxTreeMap({ + dataSource: [ + { name: 'A', value: 40 }, + { name: 'B', value: 25 }, + { name: 'C', value: 20 }, + { name: 'D', value: 15 }, + ], + valueField: 'value', + labelField: 'name', + height: 300, + }), + }, + treeView: { + label: 'TreeView', + init: ($el) => dx($el).dxTreeView({ + dataSource: treeData, + keyExpr: 'id', + parentIdExpr: 'parentId', + displayExpr: 'text', + height: 250, + }), + }, + validationGroup: { + label: 'ValidationGroup', + init: ($el) => { + const $group = $('
    ').appendTo($el); + dx($group).dxValidationGroup({}); + const $tb = $('
    ').appendTo($group); + dx($tb).dxTextBox({ placeholder: 'Required field' }); + dx($('
    ').appendTo($group)).dxValidator({ validationRules: [{ type: 'required', message: 'This field is required' }], adapter: { getValue: () => dx($tb).dxTextBox('option', 'value') } }); + dx($('
    ').appendTo($group)).dxButton({ text: 'Validate', validationGroup: $group, useSubmitBehavior: false, onClick: () => { dx($group).dxValidationGroup('validate'); } }); + }, + }, + validationSummary: { + label: 'ValidationSummary', + init: ($el) => { + dx($el).dxValidationSummary({}); + }, + }, + validator: { + label: 'Validator', + init: ($el) => { + const $tb = $('
    ').appendTo($el); + dx($tb).dxTextBox({ placeholder: 'Enter email' }); + dx($('
    ').appendTo($el)).dxValidator({ + validationRules: [{ type: 'email', message: 'Enter a valid email' }], + adapter: { getValue: () => dx($tb).dxTextBox('option', 'value') }, + }); + }, + }, + vectorMap: { + label: 'VectorMap', + init: ($el) => dx($el).dxVectorMap({ height: 300 }), + }, +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3a6c2e25f6a1..bceb8ba9c022 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1421,6 +1421,9 @@ importers: '@jest/globals': specifier: 29.7.0 version: 29.7.0 + '@playwright/test': + specifier: ^1.50.0 + version: 1.58.2 '@stylistic/eslint-plugin': specifier: 'catalog:' version: 5.10.0(eslint@9.39.4(jiti@2.6.1)) @@ -5899,6 +5902,11 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@playwright/test@1.58.2': + resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==} + engines: {node: '>=18'} + hasBin: true + '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} @@ -11238,6 +11246,11 @@ packages: os: [darwin] deprecated: Upgrade to fsevents v2 to mitigate potential security issues + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -15103,6 +15116,16 @@ packages: resolution: {integrity: sha512-+KD8hJtqQMYoTuL1bbGOqxb4z+nZkTAwVdNtWwe8Tc2xNbEmdJYIYoc6Qt0uF55e6YW6KuTHw1DjQ18gMhzepw==} engines: {node: '>=16.0.0'} + playwright-core@1.58.2: + resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.58.2: + resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==} + engines: {node: '>=18'} + hasBin: true + plimit-lit@1.6.1: resolution: {integrity: sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==} engines: {node: '>=12'} @@ -25829,6 +25852,10 @@ snapshots: '@pkgr/core@0.2.9': {} + '@playwright/test@1.58.2': + dependencies: + playwright: 1.58.2 + '@popperjs/core@2.11.8': {} '@preact/signals-core@1.8.0': {} @@ -33108,6 +33135,9 @@ snapshots: nan: 2.22.0 optional: true + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -38962,6 +38992,14 @@ snapshots: pvutils: 1.1.5 tslib: 2.8.1 + playwright-core@1.58.2: {} + + playwright@1.58.2: + dependencies: + playwright-core: 1.58.2 + optionalDependencies: + fsevents: 2.3.2 + plimit-lit@1.6.1: dependencies: queue-lit: 1.5.2 From a5c3cf629a73ebd66ca5e8798f3de17fac67b7e0 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Fri, 20 Mar 2026 13:40:34 -0300 Subject: [PATCH 07/16] Playground - add team grouping, search filter and recent history to sidebar --- packages/devextreme/playground/catalog.ts | 166 +++++++++++++++++++--- packages/devextreme/playground/index.html | 145 +++++++++++++++++-- 2 files changed, 283 insertions(+), 28 deletions(-) diff --git a/packages/devextreme/playground/catalog.ts b/packages/devextreme/playground/catalog.ts index 255a5f203090..5684599c5c7a 100644 --- a/packages/devextreme/playground/catalog.ts +++ b/packages/devextreme/playground/catalog.ts @@ -1,37 +1,167 @@ import '../js/integration/jquery'; +import { setLicenseCheckSkipCondition } from '../js/__internal/core/license/license_validation'; import $ from 'jquery'; import { registry } from './widgets/registry'; +import type { WidgetInit } from './widgets/registry'; +import { setupThemeSelector } from './newThemeSelector'; +import type { WidgetId } from './widget-ids'; -const $nav = $('#nav'); -const $container = $('#container'); -const $header = $('#header'); +setLicenseCheckSkipCondition(); -Object.entries(registry).forEach(([id, { label }]) => { - const $li = $('
  • ').appendTo($nav); - $('').attr('href', `#${id}`).text(label).appendTo($li); -}); +const RECENTS_KEY = 'dx-playground-recents'; +const MAX_RECENTS = 5; + +const widgetGroups: { label: string; ids: WidgetId[] }[] = [ + { + label: 'Grids', + ids: ['dataGrid', 'treeList', 'filterBuilder', 'sortable', 'draggable'], + }, + { + label: 'Scheduler', + ids: ['scheduler', 'pivotGrid', 'pivotGridFieldChooser', 'pagination', 'gantt', 'recurrenceEditor'], + }, + { + label: 'Editors', + ids: [ + 'autocomplete', 'calendar', 'chat', 'checkBox', 'colorBox', 'dateBox', 'dateRangeBox', + 'dropDownBox', 'dropDownButton', 'fileUploader', 'htmlEditor', 'loadPanel', 'lookup', + 'map', 'numberBox', 'popover', 'popup', 'progressBar', 'radioGroup', 'rangeSlider', + 'selectBox', 'slider', 'switch', 'tagBox', 'textArea', 'textBox', 'toast', 'tooltip', + 'validationGroup', 'validationSummary', 'validator', + ], + }, + { + label: 'Navigation', + ids: [ + 'accordion', 'actionSheet', 'box', 'button', 'buttonGroup', 'contextMenu', 'diagram', + 'drawer', 'fileManager', 'form', 'gallery', 'list', 'loadIndicator', 'menu', 'multiView', + 'resizable', 'responsiveBox', 'scrollView', 'speedDialAction', 'splitter', 'stepper', + 'tabPanel', 'tabs', 'tileView', 'toolbar', 'treeView', + 'barGauge', 'bullet', 'chart', 'circularGauge', 'funnel', 'linearGauge', 'pieChart', + 'polarChart', 'rangeSelector', 'sankey', 'sparkline', 'treeMap', 'vectorMap', + ], + }, +]; + +function getRecents(): WidgetId[] { + try { + return JSON.parse(localStorage.getItem(RECENTS_KEY) ?? '[]') as WidgetId[]; + } catch { + return []; + } +} + +function pushRecent(id: WidgetId): void { + const recents = getRecents().filter((r) => r !== id); + recents.unshift(id); + localStorage.setItem(RECENTS_KEY, JSON.stringify(recents.slice(0, MAX_RECENTS))); + renderRecents(); +} + +function deleteRecent(id: WidgetId): void { + const recents = getRecents().filter((r) => r !== id); + localStorage.setItem(RECENTS_KEY, JSON.stringify(recents)); + renderRecents(); +} + +function renderRecents(): void { + const $section = $('#recents-section'); + const $list = $('#recents-list'); + const recents = getRecents().filter((id) => registry[id]); + + $list.empty(); + + if (recents.length === 0) { + $section.hide(); + return; + } + + $section.show(); + + const currentId = location.hash.slice(1); + + recents.forEach((id) => { + const entry = registry[id]; + const $li = $('
  • ').appendTo($list); + $('').attr('href', `#${id}`).text(entry.label).toggleClass('active', id === currentId).appendTo($li); + $('') + .on('click', (e) => { + e.preventDefault(); + deleteRecent(id); + }) + .appendTo($li); + }); +} + +function buildNav(filter: string): void { + const $nav = $('#groups-nav'); + $nav.empty(); + const lc = filter.toLowerCase(); + const currentId = location.hash.slice(1); + + widgetGroups.forEach((group) => { + const matching = group.ids.filter((id) => { + if (!registry[id]) return false; + if (!lc) return true; + return registry[id].label.toLowerCase().includes(lc) || id.toLowerCase().includes(lc); + }); + + if (matching.length === 0) return; + + const $details = $('
    ').attr('open', '').appendTo($nav); + $('').text(group.label).appendTo($details); + const $ul = $('