diff --git a/.lintstagedrc b/.lintstagedrc deleted file mode 100644 index 5637a80d63..0000000000 --- a/.lintstagedrc +++ /dev/null @@ -1,14 +0,0 @@ - { - "!(src/ui/**/*)*.{js,jsx,ts,tsx}": [ - "eslint --fix --quiet", - "prettier --write" - ], - "src/ui/**/*.{js,jsx,ts,tsx}": [ - "eslint -c src/ui/.ci-eslintrc --fix --quiet", - "prettier --write" - ], - "*.{css,scss}": [ - "stylelint --fix --quiet", - "prettier --write" - ] - } diff --git a/.lintstagedrc.json b/.lintstagedrc.json index 43099d6a20..e34aecc35a 100644 --- a/.lintstagedrc.json +++ b/.lintstagedrc.json @@ -10,5 +10,8 @@ "*.{css,scss}": [ "stylelint --fix --quiet", "prettier --write" + ], + "src/i18n-keysets/**/!({keyset,context}).json": [ + "npm run lint:i18n" ] } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 9cd079ab91..f4dc93d419 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@doc-tools/transform": "^2.18.3", "@gravity-ui/app-layout": "^1.2.0", "@gravity-ui/browserslist-config": "^4.3.0", - "@gravity-ui/chartkit": "^4.15.0", + "@gravity-ui/chartkit": "^4.17.0", "@gravity-ui/dashkit": "^6.7.2", "@gravity-ui/date-utils": "^1.3.1", "@gravity-ui/expresskit": "^1.1.2", @@ -4798,13 +4798,13 @@ "integrity": "sha512-GhZemc/gPq/Kf4OYyXZpeZLgL69pI8oidWhdOKUHOWSYQyKviDDZDxrMzjZzvCWOxrpQVFtmqiCBMW8nqJivEQ==" }, "node_modules/@gravity-ui/chartkit": { - "version": "4.15.0", - "resolved": "https://registry.npmjs.org/@gravity-ui/chartkit/-/chartkit-4.15.0.tgz", - "integrity": "sha512-7ITSxXLKvTedsMD9lecUVhZXy7roSDnaDNNspNhOB847QrJ0Nu13p4nmJVGf6QJBaeQQRjrfUgCCnI1quMYJdw==", + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@gravity-ui/chartkit/-/chartkit-4.17.0.tgz", + "integrity": "sha512-GEO85OOv4r+ItuOBrVin9UhTSdpcffgTfr5MXt34vfA/ZBu81o8O6Cs0i/JmoySHUGYjayphd40iy4zNSJ2Xdw==", "dependencies": { "@bem-react/classname": "^1.6.0", "@gravity-ui/date-utils": "^1.4.1", - "@gravity-ui/yagr": "^4.1.0", + "@gravity-ui/yagr": "^4.2.0", "afterframe": "^1.0.2", "d3": "^7.8.5", "lodash": "^4.17.21", @@ -5571,9 +5571,9 @@ } }, "node_modules/@gravity-ui/yagr": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@gravity-ui/yagr/-/yagr-4.1.0.tgz", - "integrity": "sha512-Je0NPvXiOcQJxef3HqmeHVBwclIq+PbPvwZJYUbZZjyjkDVGQbAxEkgbQfiiabST+GVmsfiZczudxiq/l/Lumw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@gravity-ui/yagr/-/yagr-4.2.0.tgz", + "integrity": "sha512-4Rc0BpVC6/EbS9MRWM5+6Rc/2I5nGqyq1rxWtVLAehxYjwTn3iyC/XERNVtrQr4i+d0UFkA1lhdSD7JmNPwjJg==", "dependencies": { "uplot": "1.6.27" }, diff --git a/package.json b/package.json index b7ed37fb21..380c0f7e3d 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "@doc-tools/transform": "^2.18.3", "@gravity-ui/app-layout": "^1.2.0", "@gravity-ui/browserslist-config": "^4.3.0", - "@gravity-ui/chartkit": "^4.15.0", + "@gravity-ui/chartkit": "^4.17.0", "@gravity-ui/dashkit": "^6.7.2", "@gravity-ui/date-utils": "^1.3.1", "@gravity-ui/expresskit": "^1.1.2", diff --git a/scripts/i18n-keysets-sort.js b/scripts/i18n-keysets-sort.js new file mode 100644 index 0000000000..420766e970 --- /dev/null +++ b/scripts/i18n-keysets-sort.js @@ -0,0 +1,28 @@ +/** + * Weblate create PR with changed and approved keysets with sorted keys + * That's why we need to decrease diff + */ + +const {spawnSync} = require('child_process'); +const {readFileSync, writeFileSync} = require('fs'); + +const keysetFiles = process.argv.slice(2); +keysetFiles.forEach((filePath) => { + const fileContent = readFileSync(filePath).toString(); + try { + const keysets = JSON.parse(fileContent); + const res = {}; + Object.keys(keysets) + .sort() + .forEach((key) => { + res[key] = keysets[key]; + }); + writeFileSync(filePath, JSON.stringify(res, null, 2)); + + spawnSync('git', ['add', filePath]); + process.exitCode = 0; + } catch (e) { + console.error(e); + process.exitCode = 1; + } +}); diff --git a/src/i18n-keysets/collections/ru.json b/src/i18n-keysets/collections/ru.json index 8a7c817be3..a6e58d4359 100644 --- a/src/i18n-keysets/collections/ru.json +++ b/src/i18n-keysets/collections/ru.json @@ -11,10 +11,10 @@ "action_create-workbook": "Создать воркбук", "action_delete": "Удалить", "action_edit": "Редактировать", + "action_move": "Переместить", + "action_reset-all": "Снять все", "action_select": "Выбрать", "action_select-all": "Выбрать все", - "action_reset-all": "Снять все", - "action_move": "Переместить", "label_add-demo-workbook": "Добавить пример воркбука", "label_add-learning-materials-workbook": "Добавить учебные материалы", "label_delete-collection": "Удаление коллекции", diff --git a/src/i18n-keysets/connections.yadocs.view/en.json b/src/i18n-keysets/connections.yadocs.view/en.json new file mode 100644 index 0000000000..4345fe9789 --- /dev/null +++ b/src/i18n-keysets/connections.yadocs.view/en.json @@ -0,0 +1,23 @@ +{ + "button_add": "Add", + "button_apply": "Add", + "button_auth": "Authentication", + "button_cancel": "Cancel", + "button_update": "Update data", + "label_403-not-authotized-description": "Log in to your Yandex account or change the file access type and try adding it again", + "label_access-type": "Access type", + "label_add-document": "Add document", + "label_add-input-help": "It is forbidden to add links to or download files restricted by password. Use private files that are available after authentication.", + "label_add-input-private": "Path to file", + "label_add-input-private-note": "Example: /path/to/document", + "label_add-input-public": "Link to file", + "label_add-input-public-note": "Example: https://disk.yandex.ru/i/id", + "label_auto-update": "Update automatically", + "label_auto-update-help": "Data in the tables will be updated no more than once every 30 minutes", + "label_form-tile": "Yandex Documents", + "label_logout-dialog-description": "Sheets from files with restricted access will no longer be displayed", + "label_logout-dialog-title": "Are you sure you want to revoke your token?", + "label_radio-value-private": "Private", + "label_radio-value-public": "Public", + "label_workspace-placeholder": "To create a connection, add links to files in Yandex Documents. Supported formats: {{formats}}." +} diff --git a/src/i18n-keysets/connections.yadocs.view/ru.json b/src/i18n-keysets/connections.yadocs.view/ru.json new file mode 100644 index 0000000000..b79e83f1e8 --- /dev/null +++ b/src/i18n-keysets/connections.yadocs.view/ru.json @@ -0,0 +1,23 @@ +{ + "button_add": "Добавить", + "button_apply": "Добавить", + "button_auth": "Аутентификация", + "button_cancel": "Отмена", + "button_update": "Обновить данные", + "label_403-not-authotized-description": "Войдите в свой аккаунт на Яндексе или измените тип доступа к файлу и попробуйте добавить его снова", + "label_access-type": "Доступ к файлу", + "label_add-document": "Добавить документ", + "label_add-input-help": "Добавлять ссылки на файлы, доступ к которым ограничен с помощью пароля, а также скачивать такие файлы запрещено. Используйте приватные файлы, доступные после аутентификации.", + "label_add-input-private": "Путь до файла", + "label_add-input-private-note": "Пример: /path/to/document", + "label_add-input-public": "Ссылка на файл", + "label_add-input-public-note": "Пример: https://disk.yandex.ru/i/id", + "label_auto-update": "Обновлять автоматически", + "label_auto-update-help": "Данные в таблицах будут обновляться не чаще, чем один раз за 30 минут.", + "label_form-tile": "Яндекс Документы", + "label_logout-dialog-description": "Содержимое файлов с приватным доступом перестанет отображаться", + "label_logout-dialog-title": "Вы действительно хотите отозвать свой токен?", + "label_radio-value-private": "Приватный", + "label_radio-value-public": "Публичный", + "label_workspace-placeholder": "Чтобы создать подключение, добавьте ссылки на файлы в Яндекс Документах. Поддерживаемые форматы: {{formats}}." +} diff --git a/src/i18n-keysets/sql/en.json b/src/i18n-keysets/sql/en.json index 561b294b8d..7c49cabbcf 100644 --- a/src/i18n-keysets/sql/en.json +++ b/src/i18n-keysets/sql/en.json @@ -30,11 +30,13 @@ "label_duplicate": "Duplicate", "label_empty-chart": "The visualization will appear here as soon as you write the SQL query and press Run in the bottom-left corner", "label_empty-chart_il": "The visualization will appear here as soon as you select a connection, write an SQL query, and click Run in the bottom-left corner.", + "label_failed-connection-text": "Failed to load connection", "label_new-chart-type-text": "Select chart type to start:", - "label_new-connection-text": "Select connection to start:", + "label_new-connection-text": "To start, select a connection", "label_params": "Params", "label_placeholder-default-value": "Default value", "label_placeholder-name": "Name", + "label_ql-auto-execution-chart": "Automatically apply changes", "label_query": "Query", "label_run": "Run", "label_select-connection": "Select", @@ -52,6 +54,5 @@ "label_visualization-pie": "Pie chart", "label_visualization-scatter": "Scatter chart", "label_visualization-treemap": "Tree chart", - "text_default-name": "New QL-сhart", - "label_ql-auto-execution-chart": "Automatically apply changes" + "text_default-name": "New QL-сhart" } diff --git a/src/i18n-keysets/sql/keyset.json b/src/i18n-keysets/sql/keyset.json index 03cc5e4703..d5f2f055eb 100644 --- a/src/i18n-keysets/sql/keyset.json +++ b/src/i18n-keysets/sql/keyset.json @@ -140,6 +140,10 @@ "en": "APPROVED", "ru": "APPROVED" }, + "label_failed-connection-text": { + "en": "APPROVED", + "ru": "APPROVED" + }, "label_params": { "en": "APPROVED", "ru": "APPROVED" diff --git a/src/i18n-keysets/sql/ru.json b/src/i18n-keysets/sql/ru.json index 7e23056aca..c67eba46eb 100644 --- a/src/i18n-keysets/sql/ru.json +++ b/src/i18n-keysets/sql/ru.json @@ -30,11 +30,13 @@ "label_duplicate": "Дублировать", "label_empty-chart": "Визуализация появится, как только вы напишете SQL-запрос и нажмете кнопку «Запустить» в левом нижнем углу", "label_empty-chart_il": "Чтобы построить визуализацию, выберите подключение, напишите SQL-запрос и нажмите кнопку «Запустить» в левом нижнем углу.", + "label_failed-connection-text": "Не удалось загрузить подключение", "label_new-chart-type-text": "Выберите тип чарта, чтобы начать:", - "label_new-connection-text": "Выберите подключение, чтобы начать:", + "label_new-connection-text": "Выберите подключение, чтобы начать", "label_params": "Параметры", "label_placeholder-default-value": "Значение по умолчанию", "label_placeholder-name": "Имя", + "label_ql-auto-execution-chart": "Автоматически применять изменения", "label_query": "Запрос", "label_run": "Запустить", "label_select-connection": "Выбрать", @@ -52,6 +54,5 @@ "label_visualization-pie": "Круговая диаграмма", "label_visualization-scatter": "Точечная диаграмма", "label_visualization-treemap": "Древовидная диаграмма", - "text_default-name": "Новый QL-чарт", - "label_ql-auto-execution-chart": "Автоматически применять изменения" + "text_default-name": "Новый QL-чарт" } diff --git a/src/i18n-keysets/temp2/context.json b/src/i18n-keysets/temp2/context.json deleted file mode 100644 index 0967ef424b..0000000000 --- a/src/i18n-keysets/temp2/context.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/src/i18n-keysets/temp2/en.json b/src/i18n-keysets/temp2/en.json deleted file mode 100644 index ef5da961d6..0000000000 --- a/src/i18n-keysets/temp2/en.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "categories": "Categories 2", - "date": "Date" -} diff --git a/src/i18n-keysets/temp2/keyset.json b/src/i18n-keysets/temp2/keyset.json deleted file mode 100644 index 8bb250d779..0000000000 --- a/src/i18n-keysets/temp2/keyset.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "context": "", - "allowedStatuses": [ - "APPROVED", - "EXPIRED", - "GENERATED", - "REQUIRES_TRANSLATION", - "TRANSLATED" - ], - "status": { - "categories": { - "en": "APPROVED", - "ru": "APPROVED" - }, - "date": { - "en": "APPROVED", - "ru": "APPROVED" - } - } -} diff --git a/src/i18n-keysets/temp2/ru.json b/src/i18n-keysets/temp2/ru.json deleted file mode 100644 index 59cc2475f3..0000000000 --- a/src/i18n-keysets/temp2/ru.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "categories": "Категории 2", - "date": "Дата" -} diff --git a/src/i18n-keysets/temp3/context.json b/src/i18n-keysets/temp3/context.json deleted file mode 100644 index 0967ef424b..0000000000 --- a/src/i18n-keysets/temp3/context.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/src/i18n-keysets/temp3/en.json b/src/i18n-keysets/temp3/en.json deleted file mode 100644 index 059f8841e4..0000000000 --- a/src/i18n-keysets/temp3/en.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "categories": "Categories", - "date": "Date" -} diff --git a/src/i18n-keysets/temp3/keyset.json b/src/i18n-keysets/temp3/keyset.json deleted file mode 100644 index 8bb250d779..0000000000 --- a/src/i18n-keysets/temp3/keyset.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "context": "", - "allowedStatuses": [ - "APPROVED", - "EXPIRED", - "GENERATED", - "REQUIRES_TRANSLATION", - "TRANSLATED" - ], - "status": { - "categories": { - "en": "APPROVED", - "ru": "APPROVED" - }, - "date": { - "en": "APPROVED", - "ru": "APPROVED" - } - } -} diff --git a/src/i18n-keysets/temp3/ru.json b/src/i18n-keysets/temp3/ru.json deleted file mode 100644 index 8eceff53d5..0000000000 --- a/src/i18n-keysets/temp3/ru.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "categories": "Категории", - "date": "Дата" -} diff --git a/src/server/components/features/features-list/DashBoardGlobalParams.ts b/src/server/components/features/features-list/DashBoardGlobalParams.ts deleted file mode 100644 index 39c294b9f6..0000000000 --- a/src/server/components/features/features-list/DashBoardGlobalParams.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {Feature} from '../../../../shared'; -import {createFeatureConfig} from '../utils'; - -export default createFeatureConfig({ - name: Feature.DashBoardGlobalParams, - state: { - development: true, - production: true, - }, -}); diff --git a/src/server/modes/charts/plugins/datalens/preparers/line/d3.ts b/src/server/modes/charts/plugins/datalens/preparers/line/d3.ts index 1e052bd337..22053caa0a 100644 --- a/src/server/modes/charts/plugins/datalens/preparers/line/d3.ts +++ b/src/server/modes/charts/plugins/datalens/preparers/line/d3.ts @@ -75,6 +75,7 @@ export function prepareD3Line(args: PrepareFunctionArgs): ChartKitWidgetData { width: 36, }, }, + dashStyle: graph.dashStyle, }; }); diff --git a/src/server/modes/charts/plugins/datalens/preparers/line/prepare-line-data.ts b/src/server/modes/charts/plugins/datalens/preparers/line/prepare-line-data.ts index be5355d457..a4416e2431 100644 --- a/src/server/modes/charts/plugins/datalens/preparers/line/prepare-line-data.ts +++ b/src/server/modes/charts/plugins/datalens/preparers/line/prepare-line-data.ts @@ -484,7 +484,10 @@ export function prepareLineData(args: PrepareFunctionArgs) { }); } - if (visualizationId === WizardVisualizationId.Line) { + if ( + visualizationId === WizardVisualizationId.Line || + visualizationId === WizardVisualizationId.LineD3 + ) { mapAndShapeGraph({ graphs, shapesConfig, diff --git a/src/server/modes/charts/plugins/datalens/preparers/scatter/d3.ts b/src/server/modes/charts/plugins/datalens/preparers/scatter/d3.ts index c1461c6d7e..4ed9b74639 100644 --- a/src/server/modes/charts/plugins/datalens/preparers/scatter/d3.ts +++ b/src/server/modes/charts/plugins/datalens/preparers/scatter/d3.ts @@ -1,3 +1,4 @@ +import {SymbolType} from '@gravity-ui/chartkit/build/constants'; import type { ChartKitWidgetData, ScatterSeries, @@ -20,7 +21,7 @@ type MapScatterSeriesArgs = { function mapScatterSeries(args: MapScatterSeriesArgs): ScatterSeries { const {x, y, graph} = args; - return { + const series: ScatterSeries = { type: 'scatter', name: graph.name || '', color: typeof graph.color === 'string' ? graph.color : undefined, @@ -66,6 +67,12 @@ function mapScatterSeries(args: MapScatterSeriesArgs): ScatterSeries node.offsetWidth - node.clientWidth; + const setNewLayout = ({ gridLayout, layout, @@ -55,7 +58,8 @@ const setNewLayout = ({ contentHeight: number; }) => { const {rowHeight, margin} = gridLayout; - const newHeight = Math.ceil(contentHeight / (rowHeight + margin[1])) + 1; + // contentHeight = n * rowHeight + (n - 1) * margin[y], where n is number of grid rows + const newHeight = Math.ceil((contentHeight + margin[1]) / (rowHeight + margin[1])); const correspondedLayoutItemIndex = layout.findIndex((layoutItem) => layoutItem.i === widgetId); const correspondedLayoutItem = layout[correspondedLayoutItemIndex]; @@ -78,6 +82,23 @@ const setNewLayout = ({ cb({widgetId, needSetDefault, adjustedWidgetLayout}); }; +const setOverflowYStyle = (node: HTMLElement, value: string) => { + const st = node.getAttribute('style'); + // If there are inline styles, we adding scrollY style in the end with !important + node.setAttribute( + 'style', + `${st || ''}${!st || st?.endsWith(';') ? '' : ';'}overflow-y: ${value} !important;`, + ); + + return () => { + if (st) { + node.setAttribute('style', st); + } else { + node.removeAttribute('style'); + } + }; +}; + export function adjustWidgetLayout({ widgetId, rootNode, @@ -96,12 +117,17 @@ export function adjustWidgetLayout({ return; } - const scrollableNode = node.querySelector(`.${CHARTKIT_SCROLLABLE_NODE_CLASSNAME}`); - + const scrollableNode = node.querySelector( + `.${CHARTKIT_SCROLLABLE_NODE_CLASSNAME}`, + ) as HTMLElement | null; + const mainNode = node.querySelector(`.${CHARTKIT_MAIN_CLASSNAME}`); const errorNode = node.querySelector(`.${CHARTKIT_ERROR_NODE_CLASSNAME}`); + const rootNodeTopPosition = node.getBoundingClientRect().top; + if (errorNode && !scrollableNode) { - const fullContentHeight = errorNode.scrollHeight; + const errorOffsetFromRoot = errorNode.getBoundingClientRect().top - rootNodeTopPosition; + const fullContentHeight = errorNode.scrollHeight + errorOffsetFromRoot; const contentHeight = fullContentHeight > MAX_AUTO_HEIGHT_PX ? MAX_AUTO_HEIGHT_PX : fullContentHeight; @@ -121,25 +147,43 @@ export function adjustWidgetLayout({ return; } - const rootNodeTopPosition = node.getBoundingClientRect().top; const scrollableNodeTopPosition = scrollableNode.getBoundingClientRect().top; - const scrollableNodeTopOffsetFromRoot = scrollableNodeTopPosition - rootNodeTopPosition; const belowLyingNodesHeight = collectBelowLyingNodesHeight(scrollableNode, node, 0); - const {scrollHeight} = scrollableNode; - - // If widget has horizontal scroll, then we must add scrollBar height to fullContentHeight. - // We consider that horizontal scrollBar is equal to vertical scrollBar, - // so we could calculate horizontal scrollBar size. - - let scrollBar = 0; - if (scrollableNode.scrollWidth > scrollableNode.clientWidth) { - scrollBar = (scrollableNode as HTMLElement).offsetWidth - scrollableNode.clientWidth; + // Calculating scrollHeight without scroll and scrollbar width + let scrollBar = getScrollbarWidth(scrollableNode); + let scrollHeight = scrollableNode.scrollHeight; + + if (scrollBar > 0) { + // If scrollBar is presented and there is scalable content + // calculating height without scroll + const reset = setOverflowYStyle(scrollableNode, 'hidden'); + scrollHeight = scrollableNode.scrollHeight; + scrollBar = scrollableNode.clientWidth >= scrollableNode.scrollWidth ? 0 : scrollBar; + reset(); + } else if (scrollableNode.clientWidth < scrollableNode.scrollWidth) { + // if scrollbar hidden but content is bigger that container + // we assuming that horizaontal scroll is equal to vertical + const reset = setOverflowYStyle(scrollableNode, 'scroll'); + scrollBar = getScrollbarWidth(scrollableNode); + reset(); } + // Getting additional bottom paddings and margins around mainNode + // for example tables spacing around scrollableNode + const additionalPaddings = + mainNode && mainNode.parentElement + ? mainNode.parentElement.getBoundingClientRect().bottom - + mainNode.getBoundingClientRect().bottom + : 0; + const fullContentHeight = - scrollHeight + scrollableNodeTopOffsetFromRoot + belowLyingNodesHeight + scrollBar; + scrollableNodeTopOffsetFromRoot + + scrollHeight + + scrollBar + + belowLyingNodesHeight + + additionalPaddings; const contentHeight = fullContentHeight > MAX_AUTO_HEIGHT_PX ? MAX_AUTO_HEIGHT_PX : fullContentHeight; diff --git a/src/ui/components/Widgets/Chart/ChartSelector.tsx b/src/ui/components/Widgets/Chart/ChartSelector.tsx index 26fae8cfea..c7cad99209 100644 --- a/src/ui/components/Widgets/Chart/ChartSelector.tsx +++ b/src/ui/components/Widgets/Chart/ChartSelector.tsx @@ -304,7 +304,11 @@ export const ChartSelector = (props: ChartSelectorWidgetProps) => { ); return ( -
+
{ return changedParams; }, [prevSavedProps?.params, props.params, usedParamsRef, innerParamsRef, isWizardChart]); + const clearedOuterParams = React.useMemo(() => { + let clearedParams; + const propsParams = props.params; + const prevSavedPropsParams = prevSavedProps?.params; + const isEqualParamsWithPrev = prevSavedProps && isEqual(prevSavedPropsParams, propsParams); + + const hasParamsChanged = !prevSavedProps || !isEqualParamsWithPrev; + + const isOuterAndInnerParamsEqual = + hasParamsChanged && innerParamsRef?.current + ? isEqual(innerParamsRef?.current, propsParams) + : false; + + if (!hasChangedActionParams && !isOuterAndInnerParamsEqual) { + clearedParams = prevSavedProps + ? Object.keys(omit(prevSavedProps.params, Object.keys(props?.params || {}))) + : []; + } + + return clearedParams; + }, [ + hasChangedActionParams, + prevSavedProps?.params, + props.params, + usedParamsRef, + innerParamsRef, + isWizardChart, + ]); + const hasChangedInnerParamsFromInside = React.useMemo(() => { return prevInnerParams && !isEqual(innerParamsRef?.current, prevInnerParams); }, [prevInnerParams, innerParamsRef?.current]); @@ -346,6 +375,7 @@ export const ChartWidget = (props: ChartWidgetProps) => { widgetRenderTimeRef, usageType, enableActionParams, + clearedOuterParams, }); const handleFiltersClear = React.useCallback(() => { @@ -436,7 +466,7 @@ export const ChartWidget = (props: ChartWidgetProps) => { return (
diff --git a/src/ui/components/Widgets/Chart/hooks/useAutoHeightResizeObserver.ts b/src/ui/components/Widgets/Chart/hooks/useAutoHeightResizeObserver.ts new file mode 100644 index 0000000000..5d090554b4 --- /dev/null +++ b/src/ui/components/Widgets/Chart/hooks/useAutoHeightResizeObserver.ts @@ -0,0 +1,78 @@ +import React from 'react'; + +/** + * resizeObserver hook takes two conditional options + * isInit - enables resizeObserver only for loaded components + * autoHeight - property if changes to false removes resizeObserver + * onResize is only called when width changes, height is ignored + */ +export const useResizeObserver = (options: { + rootNodeRef: React.RefObject; + onResize: () => void; + enable: boolean; +}) => { + const {rootNodeRef, enable, onResize} = options; + + const isInitResizeCall = React.useRef(true); + const resizeObserver = React.useRef(null); + const previousWidthValue = React.useRef(null); + + const onResizeRef = React.useRef(onResize); + onResizeRef.current = onResize; + + const resizeObserverHandler = React.useCallback(() => { + if (!rootNodeRef.current) { + return; + } + + const {clientWidth} = rootNodeRef.current; + if (isInitResizeCall.current) { + isInitResizeCall.current = false; + previousWidthValue.current = clientWidth; + return; + } + + if (previousWidthValue.current === clientWidth) { + // As we are looking only cases when width change skipping other cases + // to prevent when autoHeight value is set + return; + } + + previousWidthValue.current = clientWidth; + onResizeRef.current(); + }, [onResizeRef, previousWidthValue, rootNodeRef, isInitResizeCall]); + + const disconnectResizeObserver = React.useCallback(() => { + if (resizeObserver.current) { + resizeObserver.current.disconnect(); + resizeObserver.current = null; + isInitResizeCall.current = true; + previousWidthValue.current = null; + } + }, [resizeObserver, isInitResizeCall]); + + React.useLayoutEffect(() => { + if (!rootNodeRef.current) { + return; + } + + if (!enable) { + disconnectResizeObserver(); + return; + } else if (!resizeObserver.current) { + previousWidthValue.current = null; + resizeObserver.current = new ResizeObserver(resizeObserverHandler); + resizeObserver.current.observe(rootNodeRef.current); + } + }, [enable, rootNodeRef, resizeObserver, resizeObserverHandler, disconnectResizeObserver]); + + // Unmount + React.useEffect(() => { + return () => { + if (resizeObserver.current) { + resizeObserver.current.disconnect(); + resizeObserver.current = null; + } + }; + }, [resizeObserver]); +}; diff --git a/src/ui/components/Widgets/Chart/hooks/useLoadingChart.ts b/src/ui/components/Widgets/Chart/hooks/useLoadingChart.ts index 223413947f..08f39cc84f 100644 --- a/src/ui/components/Widgets/Chart/hooks/useLoadingChart.ts +++ b/src/ui/components/Widgets/Chart/hooks/useLoadingChart.ts @@ -211,7 +211,9 @@ export const useLoadingChart = (props: LoadingChartHookProps) => { ? omit(localParams, clearedOuterParams) : localParams; - // when clear params of widget from outer (ex cleared actionParams from other chart cause empty params in curent) + // when clear params of widget from outer + // ex cleared actionParams from other chart cause empty params in current + // or params contains params without local const newParams = hasChangedOuterParams && isEmpty(params) ? { diff --git a/src/ui/components/Widgets/Chart/hooks/useLoadingChartSelector.tsx b/src/ui/components/Widgets/Chart/hooks/useLoadingChartSelector.tsx index 748361ce34..d57bddf03a 100644 --- a/src/ui/components/Widgets/Chart/hooks/useLoadingChartSelector.tsx +++ b/src/ui/components/Widgets/Chart/hooks/useLoadingChartSelector.tsx @@ -4,7 +4,7 @@ import {AxiosResponse} from 'axios'; import debounce from 'lodash/debounce'; import {useSelector} from 'react-redux'; import {useHistory} from 'react-router-dom'; -import {DashTabItemControl, DashTabItemControlSourceType, Feature} from 'shared'; +import {DashTabItemControl, Feature} from 'shared'; import {adjustWidgetLayout as dashkitAdjustWidgetLayout} from 'ui/components/DashKit/utils'; import { @@ -31,6 +31,7 @@ import { ResolveWidgetControlDataRefArgs, } from '../types'; +import {useResizeObserver} from './useAutoHeightResizeObserver'; import {LoadingChartHookProps, useLoadingChart} from './useLoadingChart'; type LoadingChartSelectorHookProps = Pick< @@ -56,7 +57,6 @@ type LoadingChartSelectorHookProps = Pick< chartId: string; }; -const WIDGET_DEBOUNCE_TIMEOUT = 300; const WIDGET_RESIZE_DEBOUNCE_TIMEOUT = 600; export const useLoadingChartSelector = (props: LoadingChartSelectorHookProps) => { const { @@ -119,8 +119,11 @@ export const useLoadingChartSelector = (props: LoadingChartSelectorHookProps) => if (renderedData?.status === 'success') { pushStats(renderedData, 'dash', dataProvider); } + + const newAutoHeight = Boolean(props.data.autoHeight); + adjustLayout(!newAutoHeight); }, - [dataProvider], + [adjustLayout, dataProvider, props.data.autoHeight], ); /** @@ -148,17 +151,11 @@ export const useLoadingChartSelector = (props: LoadingChartSelectorHookProps) => adjustLayout(false); return; } + const newAutoHeight = Boolean(props.data.autoHeight); - // fix same as for table at CHARTS-7640 - if (widgetType === DashTabItemControlSourceType.External) { - setTimeout(() => { - adjustLayout(!newAutoHeight); - }, WIDGET_DEBOUNCE_TIMEOUT); - } else { - adjustLayout(!newAutoHeight); - } + adjustLayout(!newAutoHeight); }, - [adjustLayout, widgetType, props.data.autoHeight], + [adjustLayout, props.data.autoHeight], ); const { @@ -332,6 +329,26 @@ export const useLoadingChartSelector = (props: LoadingChartSelectorHookProps) => ], ); + /** + * Resize observer adjustLayout + * + * For selector if autoHeight prop enabled or when an error occurred + */ + const autoHeightEnabled = isInit && (Boolean(props.data.autoHeight) || Boolean(error)); + + const debounceResizeAdjustLayot = React.useCallback( + debounce(() => { + adjustLayout(false); + }, WIDGET_RESIZE_DEBOUNCE_TIMEOUT), + [adjustLayout], + ); + + useResizeObserver({ + onResize: debounceResizeAdjustLayot, + rootNodeRef, + enable: autoHeightEnabled, + }); + /** * changed widget content size */ diff --git a/src/ui/components/Widgets/Chart/hooks/useLoadingChartWidget.ts b/src/ui/components/Widgets/Chart/hooks/useLoadingChartWidget.ts index c9574543fc..08d5d26179 100644 --- a/src/ui/components/Widgets/Chart/hooks/useLoadingChartWidget.ts +++ b/src/ui/components/Widgets/Chart/hooks/useLoadingChartWidget.ts @@ -7,7 +7,6 @@ import {useSelector} from 'react-redux'; import {useHistory} from 'react-router-dom'; import {DashSettings, FOCUSED_WIDGET_PARAM_NAME, Feature} from 'shared'; import {adjustWidgetLayout as dashkitAdjustWidgetLayout} from 'ui/components/DashKit/utils'; -import {DASH_WIDGET_TYPES} from 'ui/units/dash/modules/constants'; import { ChartKitWrapperLoadStatusUnknown, @@ -38,6 +37,7 @@ import { ResolveWidgetDataRef, } from '../types'; +import {useResizeObserver} from './useAutoHeightResizeObserver'; import {LoadingChartHookProps, useLoadingChart} from './useLoadingChart'; type LoadingChartWidgetHookProps = Pick< @@ -60,6 +60,7 @@ type LoadingChartWidgetHookProps = Pick< | 'widgetDataRef' | 'widgetRenderTimeRef' | 'enableActionParams' + | 'clearedOuterParams' > & ChartWidgetProps & { tabIndex: number; @@ -106,6 +107,7 @@ export const useLoadingChartWidget = (props: LoadingChartWidgetHookProps) => { usageType, enableActionParams, settings, + clearedOuterParams, } = props; const loadOnlyVisibleCharts = (settings as DashSettings).loadOnlyVisibleCharts; @@ -121,7 +123,6 @@ export const useLoadingChartWidget = (props: LoadingChartWidgetHookProps) => { const resolveMetaDataRef = React.useRef(); const resolveWidgetDataRef = React.useRef(); const mutationObserver = React.useRef(null); - const resizeObserver = React.useRef(null); const isNewRelations = useSelector(selectIsNewRelations); @@ -152,16 +153,13 @@ export const useLoadingChartWidget = (props: LoadingChartWidgetHookProps) => { if (renderedData?.status === 'success') { pushStats(renderedData, 'dash', dataProvider); } - const newAutoHeight = isWidgetTypeWithAutoHeight(loadedWidgetType) - ? tabs[tabIndex].autoHeight - : false; - if (loadedWidgetType === DASH_WIDGET_TYPES.TABLE) { - setTimeout(() => { - adjustLayout(!newAutoHeight); - }, WIDGET_DEBOUNCE_TIMEOUT); - } else { - adjustLayout(!newAutoHeight); - } + + const newAutoHeight = + isWidgetTypeWithAutoHeight(loadedWidgetType) || renderedData?.status === 'error' + ? tabs[tabIndex].autoHeight + : false; + + adjustLayout(!newAutoHeight); }, [dataProvider, tabs, tabIndex, adjustLayout, loadedWidgetType], ); @@ -239,6 +237,7 @@ export const useLoadingChartWidget = (props: LoadingChartWidgetHookProps) => { usageType, ignoreUsedParams: true, // tmp fix CHARTS-7290 TODO: CHARTS-6619 return this later with announcement of changes enableActionParams, + clearedOuterParams, }); const { @@ -353,7 +352,6 @@ export const useLoadingChartWidget = (props: LoadingChartWidgetHookProps) => { loadDescription(); return () => { mutationObserver.current?.disconnect(); - resizeObserver.current?.disconnect(); }; }, [loadDescription]); @@ -596,6 +594,41 @@ export const useLoadingChartWidget = (props: LoadingChartWidgetHookProps) => { } }, [loadedData?.isNewWizard, isLoadedWidgetWizard]); + /** + * Resize observer adjustLayout + * If widget is loaded and is valid type or has error -> taking autoHeight prop value + */ + + const debounceResizeAdjustLayot = React.useCallback( + debounce(() => { + updateImmediateLayout({ + type: loadedWidgetType, + autoHeight: tabs[tabIndex].autoHeight, + widgetId, + gridLayout, + rootNode: rootNodeRef, + layout, + cb: props.adjustWidgetLayout, + }); + }, WIDGET_RESIZE_DEBOUNCE_TIMEOUT), + [ + widgetId, + loadedWidgetType, + tabs, + tabIndex, + gridLayout, + rootNodeRef, + layout, + props.adjustWidgetLayout, + ], + ); + + useResizeObserver({ + onResize: debounceResizeAdjustLayot, + rootNodeRef, + enable: isInit && Boolean(tabs[tabIndex].autoHeight), + }); + /** * set force load all charts (no matter if they are in viewport or not) if there is loadOnlyVisibleCharts setting disabled */ diff --git a/src/ui/constants/visualizations/line.ts b/src/ui/constants/visualizations/line.ts index 034c67b3bd..2b391ed11f 100644 --- a/src/ui/constants/visualizations/line.ts +++ b/src/ui/constants/visualizations/line.ts @@ -354,7 +354,6 @@ export const LINE_D3_VISUALIZATION: GraphShared['visualization'] = { id: WizardVisualizationId.LineD3, placeholders: [LineXPlaceholder, {...LineYPlaceholder, required: true}], allowSegments: false, - allowShapes: false, }; export const AREA_VISUALIZATION: GraphShared['visualization'] = { diff --git a/src/ui/constants/visualizations/scatter.ts b/src/ui/constants/visualizations/scatter.ts index 22e974abee..8725c4f5d3 100644 --- a/src/ui/constants/visualizations/scatter.ts +++ b/src/ui/constants/visualizations/scatter.ts @@ -118,6 +118,5 @@ export const SCATTER_VISUALIZATION: GraphShared['visualization'] = { export const SCATTER_D3_VISUALIZATION: GraphShared['visualization'] = { ...SCATTER_VISUALIZATION, id: WizardVisualizationId.ScatterD3, - allowShapes: false, allowComments: false, }; diff --git a/src/ui/libs/DatalensChartkit/ChartKit/components/Widget/components/Table/Table.tsx b/src/ui/libs/DatalensChartkit/ChartKit/components/Widget/components/Table/Table.tsx index e2f4a9867c..4b5141b0b4 100644 --- a/src/ui/libs/DatalensChartkit/ChartKit/components/Widget/components/Table/Table.tsx +++ b/src/ui/libs/DatalensChartkit/ChartKit/components/Widget/components/Table/Table.tsx @@ -115,52 +115,35 @@ function attachTreeHandler(cell: TableCommonCell, tableProps: TableProps) { export class Table extends React.PureComponent { state: TableState = {waitingForFont: true, dataTableRefInitialized: false}; - private timer: ReturnType | null = null; private dataTableRef?: ChartKitDataTable; private id?: string; componentDidMount() { validateConfigAndData({data: this.props.data.data, config: this.props.data.config}); - // @ts-ignore | ts doesn't know about FontFaceSet API - document.fonts.load('700 10pt "YS Text"').then((fonts) => { - if (!fonts.length) { - console.warn( - 'The font used for the table headers has not loaded,' + - ' the table may not be displayed correctly', - ); - } + // after fonts load table is reflowing + // so now we wait all queued fonts to be ready before trigger onLoad + document.fonts.ready.finally(() => { this.setState({waitingForFont: false}, () => { this.onLoad(); this.props.onChartLoad?.({widget: this.dataTableRef}); }); - - if (this.timer) { - clearTimeout(this.timer); - } }); - - this.timer = setTimeout(() => { - this.setState({waitingForFont: false}, this.onLoad); - }, 2000); } componentDidUpdate(prevProps: TableProps) { - this.onLoad(); + // requestAnimationFrame adding this callback after DataTable has calculated it's own sizes + requestAnimationFrame(() => { + this.onLoad(); - this.props.onRender?.({renderTime: Number(Performance.getDuration(this.getId()))}); + this.props.onRender?.({renderTime: Number(Performance.getDuration(this.getId()))}); + }); if (prevProps.data !== this.props.data) { validateConfigAndData({data: this.props.data.data, config: this.props.data.config}); } } - componentWillUnmount() { - if (this.timer) { - clearTimeout(this.timer); - } - } - render() { Performance.mark(this.getId(true)); diff --git a/src/ui/libs/DatalensChartkit/ChartKit/components/Widget/components/Table/__tests__/misc.test.ts b/src/ui/libs/DatalensChartkit/ChartKit/components/Widget/components/Table/__tests__/misc.test.ts index 51906bca59..b35b73b269 100644 --- a/src/ui/libs/DatalensChartkit/ChartKit/components/Widget/components/Table/__tests__/misc.test.ts +++ b/src/ui/libs/DatalensChartkit/ChartKit/components/Widget/components/Table/__tests__/misc.test.ts @@ -1,7 +1,15 @@ import {SortedDataItem} from '@gravity-ui/react-data-table'; +import {StringParams, TableHead} from 'shared'; import {DataTableData} from 'ui/libs/DatalensChartkit/types'; -import {getCellWidth, getTreeSetColumnSortAscending, prepareLinkHref} from '../utils/misc'; +import { + getCellWidth, + getTreeSetColumnSortAscending, + mergeStringParams, + prepareLinkHref, +} from '../utils/misc'; + +const HEAD_1 = {id: 'p1'} as TableHead; describe('chartkit/Table/utils/misc', () => { test.each< @@ -145,4 +153,101 @@ describe('chartkit/Table/utils/misc', () => { expect(data).toEqual(expected); }); }); + + test.each<[Parameters[0], StringParams]>([ + [{current: {p1: ['1']}, row: {p1: ['2']}}, {p1: ['2']}], + [{current: {p1: ['1']}, row: {p1: ['1']}}, {p1: ['']}], + + [{current: {p1: ['1']}, row: {p1: ['1']}, metaKey: true}, {p1: ['']}], + [{current: {p1: ['1']}, row: {p1: ['2']}, metaKey: true}, {p1: ['1', '2']}], + [{current: {p1: ['1', '2']}, row: {p1: ['1']}, metaKey: true}, {p1: ['2']}], + [ + {current: {p1: ['1'], p2: ['1']}, row: {p1: ['1']}, metaKey: true}, + {p1: [''], p2: ['1']}, + ], + [{current: {p1: '1'}, row: {p1: '2'}, metaKey: true}, {p1: ['1', '2']}], + [{current: {p1: '1'}, row: {p1: ['2']}, metaKey: true}, {p1: ['1', '2']}], + [{current: {p1: ['1']}, row: {p1: '2'}, metaKey: true}, {p1: ['1', '2']}], + [{current: {p1: '1'}, row: {p1: '1'}, metaKey: true}, {p1: ['']}], + [{current: {p1: '1'}, row: {p1: ['1']}, metaKey: true}, {p1: ['']}], + [{current: {p1: ['1']}, row: {p1: '1'}, metaKey: true}, {p1: ['']}], + + [ + { + current: {p1: ['1']}, + row: {p1: ['1']}, + metaKey: true, + selectedRows: [{cells: [{value: '1'}]}], + head: [HEAD_1], + }, + {p1: ['']}, + ], + [ + { + current: {p1: ['1']}, + row: {p1: ['1']}, + metaKey: true, + selectedRows: [{cells: [{value: '1'}]}, {cells: [{value: '1'}]}], + head: [HEAD_1], + }, + {p1: ['']}, + ], + [ + { + current: {p1: '1'}, + row: {p1: ['1']}, + metaKey: true, + selectedRows: [{cells: [{value: '1'}]}, {cells: [{value: '1'}]}], + head: [HEAD_1], + }, + {p1: ['']}, + ], + [ + { + current: {p1: '1'}, + row: {p1: '1'}, + metaKey: true, + selectedRows: [{cells: [{value: '1'}]}, {cells: [{value: '1'}]}], + head: [HEAD_1], + }, + {p1: ['']}, + ], + [ + { + current: {p1: ['1']}, + row: {p1: '1'}, + metaKey: true, + selectedRows: [{cells: [{value: '1'}]}, {cells: [{value: '1'}]}], + head: [HEAD_1], + }, + {p1: ['']}, + ], + + [ + { + current: {p1: ['1p']}, + row: {p1: ['1p']}, + metaKey: true, + selectedRows: [{cells: [{value: '1', custom: {actionParams: {p1: ['1p']}}}]}], + head: [HEAD_1], + }, + {p1: ['']}, + ], + [ + { + current: {p1: ['1p']}, + row: {p1: ['1p']}, + metaKey: true, + selectedRows: [ + {cells: [{value: '1', custom: {actionParams: {p1: ['1p']}}}]}, + {cells: [{value: '1', custom: {actionParams: {p1: ['1p']}}}]}, + ], + head: [HEAD_1], + }, + {p1: ['']}, + ], + ])('mergeStringParams (argIndex: %#)', (args, expected) => { + const result = mergeStringParams(args); + expect(result).toEqual(expected); + }); }); diff --git a/src/ui/libs/DatalensChartkit/ChartKit/components/Widget/components/Table/utils/event-handlers.ts b/src/ui/libs/DatalensChartkit/ChartKit/components/Widget/components/Table/utils/event-handlers.ts index 417b24e2d1..47cc2e80e7 100644 --- a/src/ui/libs/DatalensChartkit/ChartKit/components/Widget/components/Table/utils/event-handlers.ts +++ b/src/ui/libs/DatalensChartkit/ChartKit/components/Widget/components/Table/utils/event-handlers.ts @@ -1,5 +1,5 @@ import type {Column} from '@gravity-ui/react-data-table'; -import type {TableHead} from 'shared'; +import type {TableHead, TableRow} from 'shared'; import {CLICK_ACTION_TYPE} from '../../../../../../modules/constants/constants'; import type {DataTableData} from '../../../../../../types'; @@ -21,10 +21,11 @@ export function getCellOnClickHandler(args: { actionParamsData?: ActionParamsData; onChange: TableProps['onChange']; head: TableHead[]; + selectedRows?: TableRow[]; }) { - const {actionParamsData, head, onChange} = args; + const {actionParamsData, head, selectedRows, onChange} = args; - const handleCellClick: Column['onClick'] = ({row}, col) => { + const handleCellClick: Column['onClick'] = ({row}, col, event) => { const onClick = getCellOnClick(row, col.name); const cellActionParams = actionParamsData ? getActionParams({ @@ -32,6 +33,8 @@ export function getCellOnClickHandler(args: { row, column: col, head, + metaKey: event.metaKey, + selectedRows, }) : undefined; diff --git a/src/ui/libs/DatalensChartkit/ChartKit/components/Widget/components/Table/utils/misc.ts b/src/ui/libs/DatalensChartkit/ChartKit/components/Widget/components/Table/utils/misc.ts index 4e92a25c81..18818954e3 100644 --- a/src/ui/libs/DatalensChartkit/ChartKit/components/Widget/components/Table/utils/misc.ts +++ b/src/ui/libs/DatalensChartkit/ChartKit/components/Widget/components/Table/utils/misc.ts @@ -3,9 +3,12 @@ import React from 'react'; import {sanitizeUrl} from '@braintree/sanitize-url'; import {transformParamsToActionParams} from '@gravity-ui/dashkit'; import {Column, Comparator, SortedDataItem} from '@gravity-ui/react-data-table'; +import clone from 'lodash/clone'; import get from 'lodash/get'; +import intersection from 'lodash/intersection'; import isEqual from 'lodash/isEqual'; import isPlainObject from 'lodash/isPlainObject'; +import without from 'lodash/without'; import { BarTableCell, BarViewOptions, @@ -23,12 +26,15 @@ import {formatNumber} from 'shared/modules/format-units/formatUnit'; import {MarkupItem, MarkupItemType} from '../../../../../../../../components/Markup'; import {DataTableData, TableWidget} from '../../../../../../types'; +import {addParams} from '../../../../../helpers/action-params-handlers'; import {hasMatchedActionParams} from '../../../../../helpers/utils'; import type {ActionParamsData} from './types'; const MARKUP_ITEM_TYPES: MarkupItemType[] = ['bold', 'concat', 'italics', 'text', 'url']; +type ValuesMap = Record; + const decodeURISafe = (uri: string) => { return decodeURI(uri.replace(/%(?![0-9a-fA-F][0-9a-fA-F]+)/g, '%25')); }; @@ -338,7 +344,7 @@ export function getActionParamsEventScope( }, undefined); } -function extractCellActionParams(args: {cell: TableCell; head?: TableHead}) { +function extractCellActionParams(args: {cell: TableCell; head?: TableHead}): StringParams { const {cell, head} = args; const cellCustomData = get(cell, 'custom'); @@ -356,6 +362,121 @@ function extractCellActionParams(args: {cell: TableCell; head?: TableHead}) { return {}; } +function setMapValue(map: ValuesMap, value: string, hash: string) { + if (typeof map[value] === 'undefined') { + // eslint-disable-next-line no-param-reassign + map[value] = {value: 0, hashes: []}; + } + + if (map[value].hashes.indexOf(hash) === -1) { + map[value].hashes.push(hash); + } + + // eslint-disable-next-line no-param-reassign + map[value].value = map[value].value + 1; +} + +function getValuesMap(args: {selectedRows: TableRow[]; head?: TableHead[]}) { + const {selectedRows, head} = args; + + return selectedRows.reduce((acc, row) => { + if (!('cells' in row)) { + return acc; + } + + const rowHash = Object.values(row.cells).reduce((acc, cell, i) => { + const cellParams = extractCellActionParams({cell, head: head?.[i]}); + return acc + Object.values(cellParams).join(); + }, ''); + + row.cells.forEach((cell, i) => { + const cellParams = extractCellActionParams({cell, head: head?.[i]}); + Object.values(cellParams).forEach((cellValue) => { + if (Array.isArray(cellValue)) { + cellValue.forEach((value) => { + setMapValue(acc, value, rowHash); + }); + } else { + setMapValue(acc, cellValue, rowHash); + } + }); + }); + + return acc; + }, {}); +} + +function shouldRemoveValue(map: ValuesMap, key: string) { + return ( + // Value is contained only in one row + map[key]?.value === 1 || + // Values are contained in completely identical rows + (map[key]?.value > 1 && map[key].hashes.length === 1) + ); +} + +function mergeStringParamsByRowDeselecting(args: { + current: StringParams; + row: StringParams; + selectedRows: TableRow[]; + head?: TableHead[]; +}) { + const {current, row, selectedRows, head} = args; + const valuesMap = getValuesMap({selectedRows, head}); + const hasSelectedRows = Boolean(selectedRows.length); + + return Object.keys(current).reduce((acc, key) => { + acc[key] = Array.isArray(current[key]) ? current[key] : [current[key] as string]; + const rowKey = Array.isArray(row[key]) ? row[key] : [row[key] as string]; + const intersectedValues = intersection(acc[key], rowKey); + + if (intersectedValues.length) { + const itemsToFilter = hasSelectedRows + ? intersectedValues.filter((value) => { + return ( + typeof valuesMap[String(value)]?.value === 'number' && + shouldRemoveValue(valuesMap, String(value)) + ); + }) + : intersectedValues; + + acc[key] = without(acc[key], ...itemsToFilter); + if (!acc[key].length) { + acc[key] = ['']; + } + } + + return acc; + }, {}); +} + +export function mergeStringParams(args: { + current: StringParams; + row: StringParams; + head?: TableHead[]; + metaKey?: boolean; + selectedRows?: TableRow[]; +}): StringParams { + const {current, row, metaKey, selectedRows = [], head} = args; + const isRowAlreadySelected = hasMatchedActionParams(row, current); + + if (metaKey) { + return isRowAlreadySelected + ? mergeStringParamsByRowDeselecting({current, row, selectedRows, head}) + : addParams(current, row); + } + + const result = clone(row); + + if (isRowAlreadySelected) { + Object.keys(result).forEach((key) => { + result[key] = ['']; + }); + } + + return result; +} + export function getRowActionParams(args: {row?: DataTableData; head?: TableHead[]}): StringParams { const {row, head} = args; @@ -376,23 +497,25 @@ function getActionParamsByRow(args: { actionParams: StringParams; row?: DataTableData; head?: TableHead[]; + metaKey?: boolean; + selectedRows?: TableRow[]; }): StringParams { - const {actionParams, row, head} = args; + const {actionParams, row, head, metaKey, selectedRows} = args; if (!row) { return {}; } const rowActionParams = getRowActionParams({row, head}); - const isRowAlreadySelected = hasMatchedActionParams(rowActionParams, actionParams); - - if (isRowAlreadySelected) { - Object.keys(rowActionParams).forEach((key) => { - rowActionParams[key] = ['']; - }); - } + const resultParams = mergeStringParams({ + current: actionParams, + row: rowActionParams, + head, + metaKey, + selectedRows, + }); - return transformParamsToActionParams(rowActionParams); + return transformParamsToActionParams(resultParams); } export function getActionParams(args: { @@ -400,12 +523,20 @@ export function getActionParams(args: { row?: DataTableData; column?: Column; head?: TableHead[]; + metaKey?: boolean; + selectedRows?: TableRow[]; }): StringParams { - const {actionParamsData, row, head} = args; + const {actionParamsData, row, head, metaKey, selectedRows} = args; switch (actionParamsData.scope) { case 'row': { - return getActionParamsByRow({actionParams: actionParamsData.params, row, head}); + return getActionParamsByRow({ + actionParams: actionParamsData.params, + row, + head, + metaKey, + selectedRows, + }); } // There is no way to reach this code. Just satisfies ts default: { diff --git a/src/ui/libs/DatalensChartkit/ChartKit/components/Widget/components/Table/utils/render.tsx b/src/ui/libs/DatalensChartkit/ChartKit/components/Widget/components/Table/utils/render.tsx index 7b7fa56865..bc9be87db0 100644 --- a/src/ui/libs/DatalensChartkit/ChartKit/components/Widget/components/Table/utils/render.tsx +++ b/src/ui/libs/DatalensChartkit/ChartKit/components/Widget/components/Table/utils/render.tsx @@ -26,6 +26,7 @@ import { import {Markup} from '../../../../../../../../components/Markup'; import {markupToRawString} from '../../../../../../modules/table'; import {ChartKitDataTable, DataTableData} from '../../../../../../types'; +import {hasMatchedActionParams} from '../../../../../helpers/utils'; import {Bar} from '../Bar/Bar'; import {TableProps} from '../types'; @@ -306,6 +307,20 @@ export const getColumnsAndNames = ({ topLevelWidth?: number; actionParamsData?: ActionParamsData; }) => { + const selectedRows = rows.filter((row) => { + if (!('cells' in row)) { + return false; + } + + const preparedRow = row.cells.reduce((acc, cell, i) => { + acc[`cell-${i}`] = cell; + return acc; + }, {}); + const actionParams = getRowActionParams({row: preparedRow, head}); + + return Boolean(hasMatchedActionParams(actionParams, actionParamsData?.params)); + }); + return head.reduce( // eslint-disable-next-line complexity ( @@ -488,6 +503,7 @@ export const getColumnsAndNames = ({ onClick: getCellOnClickHandler({ actionParamsData, head, + selectedRows, onChange, }), sortable: isGroupSortAvailable && isColumnSortable, diff --git a/src/ui/libs/DatalensChartkit/ChartKit/helpers/action-params-handlers.ts b/src/ui/libs/DatalensChartkit/ChartKit/helpers/action-params-handlers.ts index 1c3d447a9a..c1e4a4ac30 100644 --- a/src/ui/libs/DatalensChartkit/ChartKit/helpers/action-params-handlers.ts +++ b/src/ui/libs/DatalensChartkit/ChartKit/helpers/action-params-handlers.ts @@ -76,7 +76,7 @@ function setSeriesSelectState(series: Highcharts.Series, selected: boolean) { } } -function addParams(params: StringParams, addition: StringParams = {}) { +export function addParams(params: StringParams, addition: StringParams = {}) { const result = cloneDeep(params); return Object.entries(addition).reduce((acc, [key, val]) => { @@ -198,18 +198,23 @@ export const handleChartLoadingForActionParams = (args: { export function handleSeriesClickForActionParams(args: { chart: Highcharts.Chart; + point?: Highcharts.Point; clickScope: GraphWidgetEventScope; - event: Highcharts.SeriesClickEventObject; + event: MouseEvent; onChange?: ChartKitAdapterProps['onChange']; actionParams: StringParams; }) { - const {chart, clickScope, event, onChange, actionParams: prevActionParams} = args; + const {chart, clickScope, event, point, onChange, actionParams: prevActionParams} = args; const multiSelect = Boolean(event.metaKey); let newActionParams: StringParams = prevActionParams; switch (clickScope) { case 'point': { - const currentPoint = event.point; + if (!point) { + return; + } + + const currentPoint = point; const currentPointParams = getPointActionParams(currentPoint); const chartPoints = chart.series.reduce( (acc, s) => acc.concat(s.getPointsCollection()), diff --git a/src/ui/libs/DatalensChartkit/ChartKit/helpers/apply-hc-handlers.ts b/src/ui/libs/DatalensChartkit/ChartKit/helpers/apply-hc-handlers.ts index 1f6a7e6c83..f7d075fa1e 100644 --- a/src/ui/libs/DatalensChartkit/ChartKit/helpers/apply-hc-handlers.ts +++ b/src/ui/libs/DatalensChartkit/ChartKit/helpers/apply-hc-handlers.ts @@ -76,20 +76,25 @@ export const applySetActionParamsEvents = (args: { ); wrap( - get(data, pathToSeriesEvents), + get(data, pathToChartEvents), 'click', function ( - this: Highcharts.Series, - proceed: Highcharts.SeriesClickCallbackFunction, - event: Highcharts.SeriesClickEventObject, + this: Highcharts.Chart, + proceed: Highcharts.ChartClickCallbackFunction, + event: Highcharts.PointerEventObject, ) { - handleSeriesClickForActionParams({ - chart: this.chart, - clickScope, - event, - onChange, - actionParams, - }); + const point = this.hoverPoint; + if (point) { + handleSeriesClickForActionParams({ + chart: this, + clickScope, + event, + onChange, + actionParams, + point, + }); + } + proceed?.apply(this, [event]); }, ); diff --git a/src/ui/units/connections/components/custom-forms/Yadocs/components/AdditionalTitleContent.tsx b/src/ui/units/connections/components/custom-forms/Yadocs/components/AdditionalTitleContent.tsx index 6bd9eee47a..7b2141cfaf 100644 --- a/src/ui/units/connections/components/custom-forms/Yadocs/components/AdditionalTitleContent.tsx +++ b/src/ui/units/connections/components/custom-forms/Yadocs/components/AdditionalTitleContent.tsx @@ -3,15 +3,16 @@ import React from 'react'; import {HelpPopover} from '@gravity-ui/components'; import {Button, Checkbox, Icon} from '@gravity-ui/uikit'; import block from 'bem-cn-lite'; +import {I18n} from 'i18n'; import {ConnectorType} from 'shared'; import {registry} from 'ui/registry'; import {ButtonLogout} from '../../components'; -import {i18n8857} from '../constants'; import iconSync from '../../../../../../assets/icons/sync.svg'; const b = block('conn-form-yadocs'); +const i18n = I18n.keyset('connections.yadocs.view'); const ICON_SIZE = 18; type Props = { @@ -44,7 +45,7 @@ export const AdditionalTitleContent = (props: Props) => { ) : ( )} @@ -55,15 +56,15 @@ export const AdditionalTitleContent = (props: Props) => { disabled={disableControls} onUpdate={clickAutoUpdateCheckbox} > - {i18n8857['label_auto-update']} + {i18n('label_auto-update')}
diff --git a/src/ui/units/connections/components/custom-forms/Yadocs/components/DialogAddDocument/DialogAddDocument.tsx b/src/ui/units/connections/components/custom-forms/Yadocs/components/DialogAddDocument/DialogAddDocument.tsx index 04724cba39..133ed2a75b 100644 --- a/src/ui/units/connections/components/custom-forms/Yadocs/components/DialogAddDocument/DialogAddDocument.tsx +++ b/src/ui/units/connections/components/custom-forms/Yadocs/components/DialogAddDocument/DialogAddDocument.tsx @@ -1,16 +1,18 @@ import React from 'react'; +import {HelpPopover} from '@gravity-ui/components'; import {Dialog, RadioButton, TextInput} from '@gravity-ui/uikit'; import type {ButtonProps} from '@gravity-ui/uikit'; import block from 'bem-cn-lite'; +import {I18n} from 'i18n'; import DialogManager from '../../../../../../../components/DialogManager/DialogManager'; import {DataLensApiError} from '../../../../../../../typings'; -import {i18n8857} from '../../constants'; import './DialogAddDocument.scss'; const b = block('conn-form-yadocs'); +const i18n = I18n.keyset('connections.yadocs.view'); const FILE_MODE = { PUBLIC: 'public', PRIVATE: 'private', @@ -42,13 +44,11 @@ const DialogAddYadoc = (props: DialogAddYadocProps) => { const propsButtonApply: Partial = {disabled: !value, loading}; const applyDisabled = !value || loading; const inputLabel = - mode === 'private' - ? i18n8857['label_add-input-private'] - : i18n8857['label_add-input-public']; + mode === 'private' ? i18n('label_add-input-private') : i18n('label_add-input-public'); const inputNote = mode === 'private' - ? i18n8857['label_add-input-private-note'] - : i18n8857['label_add-input-public-note']; + ? i18n('label_add-input-private-note') + : i18n('label_add-input-public-note'); const handleInputUpdate = (nextPath: string) => { setValue(nextPath); @@ -91,27 +91,31 @@ const DialogAddYadoc = (props: DialogAddYadocProps) => { return ( - +
- + - {i18n8857['label_radio-value-public']} + {i18n('label_radio-value-public')} - {i18n8857['label_radio-value-private']} + {i18n('label_radio-value-private')}
- + {inputNote}} @@ -124,8 +128,8 @@ const DialogAddYadoc = (props: DialogAddYadocProps) => {
diff --git a/src/ui/units/connections/components/custom-forms/Yadocs/components/DocsList.tsx b/src/ui/units/connections/components/custom-forms/Yadocs/components/DocsList.tsx index 6773cdffab..585d558e76 100644 --- a/src/ui/units/connections/components/custom-forms/Yadocs/components/DocsList.tsx +++ b/src/ui/units/connections/components/custom-forms/Yadocs/components/DocsList.tsx @@ -3,13 +3,14 @@ import React from 'react'; import {Plus} from '@gravity-ui/icons'; import {Button, Icon, List} from '@gravity-ui/uikit'; import block from 'bem-cn-lite'; +import {I18n} from 'i18n'; import type {YadocItem} from 'ui/units/connections/store'; import {YadocListItemView} from '../components'; -import {i18n8857} from '../constants'; import type {HandleItemClick, YadocListItem} from '../types'; const b = block('conn-form-yadocs'); +const i18n = I18n.keyset('connections.yadocs.view'); const ITEM_HEIGHT = 52; type Props = { @@ -66,7 +67,7 @@ export const DocsList = (props: Props) => {
diff --git a/src/ui/units/connections/components/custom-forms/Yadocs/components/Workspace.tsx b/src/ui/units/connections/components/custom-forms/Yadocs/components/Workspace.tsx index be3364f1b9..9b8248d251 100644 --- a/src/ui/units/connections/components/custom-forms/Yadocs/components/Workspace.tsx +++ b/src/ui/units/connections/components/custom-forms/Yadocs/components/Workspace.tsx @@ -3,6 +3,7 @@ import React from 'react'; import DataTable from '@gravity-ui/react-data-table'; import {Loader} from '@gravity-ui/uikit'; import block from 'bem-cn-lite'; +import {I18n} from 'i18n'; import clone from 'lodash/clone'; import get from 'lodash/get'; import type {FileSourcePreview, FileSourceSchema} from 'shared/schema/types'; @@ -16,9 +17,10 @@ import {getYadocErrorData} from '../../../../utils'; import {ErrorView} from '../../../ErrorView/ErrorView'; import {ColumnFilter, ColumnsHeaderSwitcher} from '../../components'; import {getColumnsWithTypeIcons} from '../../utils/render'; -import {i18n8857} from '../constants'; const b = block('conn-form-yadocs'); +const i18n = I18n.keyset('connections.yadocs.view'); +const ACCEPTED_EXTENTIONS = ['XLSX']; const shouldToShowVeil = (item?: YadocItem) => { return Boolean(item && isYadocSourceItem(item) && item.status === 'in_progress'); @@ -49,7 +51,9 @@ const EmptyWorkspace = () => { return ( ); }; diff --git a/src/ui/units/connections/components/custom-forms/Yadocs/constants.ts b/src/ui/units/connections/components/custom-forms/Yadocs/constants.ts deleted file mode 100644 index 476b8f8b56..0000000000 --- a/src/ui/units/connections/components/custom-forms/Yadocs/constants.ts +++ /dev/null @@ -1,26 +0,0 @@ -// TODO: add texts [CHARTS-8857] -export const i18n8857 = { - button_auth: 'Authentication', - button_add: 'Add', - button_apply: 'Apply', - button_cancel: 'Cancel', - button_update: 'Update data', - 'label_auto-update': 'Update automatically', - 'label_auto-update-help': - 'Data in the tables will be updated no more than once every 30 minutes', - 'label_form-tile': 'Yandex Documents', - 'label_workspace-placeholder': 'To create a connection, add links to Yandex Documents files', - 'label_add-input-public-note': 'For example: https://disk.yandex.ru/i/id', - 'label_add-input-private-note': 'For example: /Disk/path/to/document', - 'label_add-input-public': 'Link on Disk', - 'label_add-input-private': 'Path on Disk', - 'label_radio-value-public': 'Public', - 'label_radio-value-private': 'Private', - 'label_access-type': 'Access type', - 'label_add-document': 'Add document', - 'label_logout-dialog-description': - 'Sheets from files with restricted access will no longer be displayed', - 'label_logout-dialog-title': 'Are you sure you want to revoke your token?', - 'label_403-not-authotized-description': - 'Log in to your Yandex account or change your file access settings and try adding it again', -}; diff --git a/src/ui/units/connections/components/custom-forms/Yadocs/containers/ActionBarContainer.tsx b/src/ui/units/connections/components/custom-forms/Yadocs/containers/ActionBarContainer.tsx index 6d955ac9e6..40fa6b03fe 100644 --- a/src/ui/units/connections/components/custom-forms/Yadocs/containers/ActionBarContainer.tsx +++ b/src/ui/units/connections/components/custom-forms/Yadocs/containers/ActionBarContainer.tsx @@ -1,6 +1,7 @@ import React from 'react'; import block from 'bem-cn-lite'; +import {I18n} from 'i18n'; import {useDispatch, useSelector} from 'react-redux'; import {ConnectorType} from 'shared'; @@ -18,11 +19,11 @@ import { } from '../../../../store'; import {FormTitle} from '../../../FormTitle/FormTitle'; import {AdditionalTitleContent} from '../components'; -import {i18n8857} from '../constants'; import {useYadocsDialogs} from './useYadocsDialogs'; const b = block('conn-form-yadocs'); +const i18n = I18n.keyset('connections.yadocs.view'); export const ActionBarContainer = () => { const dispatch = useDispatch(); @@ -104,7 +105,7 @@ export const ActionBarContainer = () => { diff --git a/src/ui/units/connections/components/custom-forms/Yadocs/containers/useYadocsDialogs.ts b/src/ui/units/connections/components/custom-forms/Yadocs/containers/useYadocsDialogs.ts index e36bc5a3cf..96100859d6 100644 --- a/src/ui/units/connections/components/custom-forms/Yadocs/containers/useYadocsDialogs.ts +++ b/src/ui/units/connections/components/custom-forms/Yadocs/containers/useYadocsDialogs.ts @@ -1,5 +1,6 @@ import React from 'react'; +import {I18n} from 'i18n'; import {get} from 'lodash'; import {batch, useDispatch} from 'react-redux'; @@ -25,7 +26,8 @@ import type { import {DIALOG_CONN_CONFIRM, DIALOG_CONN_S3_SOURCES} from '../../../dialogs'; import {DIALOG_CONN_WITH_INPUT} from '../../components'; import {DIALOG_CONN_ADD_YADOC} from '../components/DialogAddDocument/DialogAddDocument'; -import {i18n8857} from '../constants'; + +const i18n = I18n.keyset('connections.yadocs.view'); type OpenAddDocumentDialogArgs = Omit; @@ -214,16 +216,16 @@ export const useYadocsDialogs = () => { openDialog({ id: DIALOG_CONN_CONFIRM, props: { - description: i18n8857['label_logout-dialog-description'], + description: i18n('label_logout-dialog-description'), dialogProps: { onEnterKeyDown: onApply, }, headerProps: { - caption: i18n8857['label_logout-dialog-title'], + caption: i18n('label_logout-dialog-title'), }, footerProps: { - textButtonApply: i18n8857['button_apply'], - textButtonCancel: i18n8857['button_cancel'], + textButtonApply: i18n('button_apply'), + textButtonCancel: i18n('button_cancel'), }, onApply, onClose: handleCloseDialog, diff --git a/src/ui/units/connections/utils/yadocs.ts b/src/ui/units/connections/utils/yadocs.ts index a0ec82fccb..38dffd432c 100644 --- a/src/ui/units/connections/utils/yadocs.ts +++ b/src/ui/units/connections/utils/yadocs.ts @@ -2,44 +2,44 @@ import {I18n} from 'i18n'; import type {DataLensApiError} from 'ui/typings'; import {parseError} from 'ui/utils/errors/parse'; -import {i18n8857} from '../components/custom-forms/Yadocs/constants'; import {ConverterErrorCode} from '../constants'; import type {YadocItem} from '../store'; -const i18n = I18n.keyset('connections.gsheet.view'); +const i18nGSheet = I18n.keyset('connections.gsheet.view'); +const i18nYadocs = I18n.keyset('connections.yadocs.view'); const getErrorTitle = ({type, code}: {type: YadocItem['type']; code: string}) => { switch (code) { case ConverterErrorCode.FILE_LIMIT_EXCEEDED: { - return i18n('label_file-limit-exceeded'); + return i18nGSheet('label_file-limit-exceeded'); } case ConverterErrorCode.INVALID_LINK: { - return i18n('label_invalid-link'); + return i18nGSheet('label_invalid-link'); } case ConverterErrorCode.NOT_FOUND: { - return i18n('label_file-not-found'); + return i18nGSheet('label_file-not-found'); } case ConverterErrorCode.NO_DATA: { - return i18n('label_empty-file'); + return i18nGSheet('label_empty-file'); } case ConverterErrorCode.PERMISSION_DENIED: { - return i18n('label_403-title'); + return i18nGSheet('label_403-title'); } case ConverterErrorCode.TOO_MANY_COLUMNS: { - return i18n('label_too-many-columns'); + return i18nGSheet('label_too-many-columns'); } case ConverterErrorCode.UNSUPPORTED_DOCUMENT: { - return i18n('label_unsupported-document'); + return i18nGSheet('label_unsupported-document'); } } // Defaults switch (type) { case 'uploadedYadoc': { - return i18n('label_gsheet-uploading-failure'); + return i18nGSheet('label_gsheet-uploading-failure'); } default: { - return i18n('label_source-info-failed'); + return i18nGSheet('label_source-info-failed'); } } }; @@ -47,7 +47,7 @@ const getErrorTitle = ({type, code}: {type: YadocItem['type']; code: string}) => const getErrorDescription = ({code}: {code?: string}) => { switch (code) { case ConverterErrorCode.PERMISSION_DENIED: { - return i18n8857['label_403-not-authotized-description']; + return i18nYadocs('label_403-not-authotized-description'); } default: { return ''; diff --git a/src/ui/units/dash/containers/Dialogs/Settings/Settings.tsx b/src/ui/units/dash/containers/Dialogs/Settings/Settings.tsx index 07310e2674..ee264094e1 100644 --- a/src/ui/units/dash/containers/Dialogs/Settings/Settings.tsx +++ b/src/ui/units/dash/containers/Dialogs/Settings/Settings.tsx @@ -229,12 +229,7 @@ const Settings = () => { loadOnlyVisibleCharts={loadOnlyVisibleCharts} onUpdateLoadOnlyVisibleCharts={handleUpdateLoadOnlyVisibleCharts} /> - {Utils.isEnabledFeature(Feature.DashBoardGlobalParams) && ( - - )} + - {connection ? ( - - - {connection.name} - - } - items={[ - { - action: () => { - this.toggleNavigation(); - }, - text: i18n('sql', 'button_change-connection'), - }, - { - action: () => { - window.open( - `${DL.ENDPOINTS.connections}/${connection?.entryId}`, - ); - }, - text: i18n('sql', 'button_to-connection'), - }, - ]} - /> - ) : ( -
- {i18n('sql', 'label_new-connection-text')} - -
- )} + {this.renderConnectionBlock()} {workbookId ? ( + + {connection.name} + + } + items={[ + { + action: () => { + this.toggleNavigation(); + }, + text: i18n('sql', 'button_change-connection'), + }, + { + action: () => { + window.open(`${DL.ENDPOINTS.connections}/${connection?.entryId}`); + }, + text: i18n('sql', 'button_to-connection'), + }, + ]} + /> + ); + } else if (connectionStatus === ConnectionStatus.Empty) { + return ( +
+ {i18n('sql', 'label_new-connection-text')} + +
+ ); + } else { + // This means that connectionStatus === ConnectionStatus.Failed + return ( +
+ {i18n('sql', 'label_failed-connection-text')} + +
+ ); + } + } + private onClickButtonRun = () => { if (this.props.valid) { this.props.drawPreview(); @@ -261,6 +283,7 @@ const makeMapStateToProps = (state: DatalensGlobalState) => { return { chartType: getChartType(state), connection: getConnection(state), + connectionStatus: getConnectionStatus(state), defaultPath: getDefaultPath(state), params: getParams(state), valid: getValid(state), diff --git a/src/ui/units/ql/containers/PaneVisualization/PaneVisualization.tsx b/src/ui/units/ql/containers/PaneVisualization/PaneVisualization.tsx index e806eb5d85..b4aba41ced 100644 --- a/src/ui/units/ql/containers/PaneVisualization/PaneVisualization.tsx +++ b/src/ui/units/ql/containers/PaneVisualization/PaneVisualization.tsx @@ -38,6 +38,7 @@ class PaneVisualization extends React.PureComponent< if (this.props.isQueryEmpty) { return; } + this.props.drawPreview({ withoutTable: true, }); diff --git a/src/ui/units/ql/store/actions/ql.ts b/src/ui/units/ql/store/actions/ql.ts index 9fd81e519a..8840074ca5 100644 --- a/src/ui/units/ql/store/actions/ql.ts +++ b/src/ui/units/ql/store/actions/ql.ts @@ -56,6 +56,7 @@ import {setExtraSettings as setWizardExtraSettings} from '../../../wizard/action import { AVAILABLE_CHART_TYPES, AppStatus, + ConnectionStatus, QL_MOCKED_DATASET_ID, VisualizationStatus, } from '../../constants'; @@ -91,6 +92,7 @@ export const SET_EXTRA_SETTINGS = Symbol('ql/SET_EXTRA_SETTINGS'); export const SET_QUERY_METADATA = Symbol('ql/SET_QUERY_METADATA'); export const SET_TABLE_PREVIEW_DATA = Symbol('ql/SET_TABLE_PREVIEW_DATA'); export const SET_VISUALIZATION_STATUS = Symbol('ql/SET_VISUALIZATION_STATUS'); +export const SET_CONNECTION_STATUS = Symbol('ql/SET_CONNECTION_STATUS'); export const SET_COLUMNS_ORDER = Symbol('ql/SET_COLUMNS_ORDER'); export const SET_CONNECTION_SOURCES = Symbol('ql/SET_CONNECTION_SOURCES'); export const SET_CONNECTION_SOURCE_SCHEMA = Symbol('ql/SET_CONNECTION_SOURCE_SCHEMA'); @@ -375,6 +377,13 @@ export const setVisualizationStatus = (visualizationStatus: VisualizationStatus) }; }; +export const setConnectionStatus = (connectionStatus: ConnectionStatus) => { + return { + type: SET_CONNECTION_STATUS, + connectionStatus, + }; +}; + export const toggleTablePreview = () => { return { type: TOGGLE_TABLE_PREVIEW, @@ -453,6 +462,16 @@ export const drawPreview = ({withoutTable}: {withoutTable?: boolean} = {}) => { }; }; +export const drawPreviewIfValid = ({withoutTable}: {withoutTable?: boolean} = {}) => { + return (dispatch: AppDispatch, getState: () => DatalensGlobalState) => { + const valid = getValid(getState()); + + if (valid) { + dispatch(drawPreview({withoutTable})); + } + }; +}; + const applyUrlParams = (params: QlConfigParam[]) => { // If there are no parameters, then exit immediately if (params.length === 0) { @@ -523,26 +542,22 @@ type FetchConnectionSourcesArgs = { export const fetchConnectionSources = ({entryId}: FetchConnectionSourcesArgs) => { return async function (dispatch: AppDispatch) { - try { - // Requesting information about connection sources - const {sources: connectionSources, freeform_sources: connectionFreeformSources} = - await getSdk().bi.getConnectionSources({ - connectionId: entryId, - }); - - if (!connectionSources) { - throw new Error(i18n('sql', 'error_failed-to-load-connection')); - } + // Requesting information about connection sources + const {sources: connectionSources, freeform_sources: connectionFreeformSources} = + await getSdk().bi.getConnectionSources({ + connectionId: entryId, + }); - dispatch( - setConnectionSources({ - connectionSources, - connectionFreeformSources, - }), - ); - } catch (error) { - logger.logError('ql: getConnectionSources failed', error); + if (!connectionSources) { + throw new Error(i18n('sql', 'error_failed-to-load-connection')); } + + dispatch( + setConnectionSources({ + connectionSources, + connectionFreeformSources, + }), + ); }; }; @@ -676,30 +691,39 @@ export const initializeApplication = (args: InitializeApplicationArgs) => { }), ); + // By default ChartType === 'sql', since there were only sql charts before + const chartType = entry?.data.shared.chartType || 'sql'; + const { connection: {entryId: connectionEntryId}, } = entry.data.shared; - // We request the connection for which the chart is built - const loadedConnectionEntry = await getSdk().us.getEntry({ - entryId: connectionEntryId, - }); + let connection: QLConnectionEntry | null = null; - if (!loadedConnectionEntry) { - throw new Error(i18n('sql', 'error_failed-to-load-connection')); - } + try { + // We request the connection for which the chart is built + const loadedConnectionEntry = await getSdk().us.getEntry({ + entryId: connectionEntryId, + }); - const connection: QLConnectionEntry = loadedConnectionEntry as QLConnectionEntry; - const keyParts = connection.key.split('/'); - connection.name = keyParts[keyParts.length - 1]; + if (loadedConnectionEntry) { + connection = loadedConnectionEntry as QLConnectionEntry; - dispatch(setConnection(connection)); + const keyParts = connection.key.split('/'); + connection.name = keyParts[keyParts.length - 1]; - // By default ChartType === 'sql', since there were only sql charts before - const chartType = entry?.data.shared.chartType || 'sql'; + dispatch(setConnection(connection)); - if (chartType === QLChartType.Sql) { - dispatch(fetchConnectionSources({entryId: loadedConnectionEntry.entryId})); + dispatch(setConnectionStatus(ConnectionStatus.Ready)); + + if (chartType === QLChartType.Sql) { + dispatch( + fetchConnectionSources({entryId: loadedConnectionEntry.entryId}), + ); + } + } + } catch (e) { + dispatch(setConnectionStatus(ConnectionStatus.Failed)); } const { @@ -830,7 +854,7 @@ export const initializeApplication = (args: InitializeApplicationArgs) => { datalensGlobalState = getState(); dispatch( - drawPreview({ + drawPreviewIfValid({ withoutTable: false, }), ); @@ -1171,16 +1195,6 @@ export const setQlChartActualRevision = (isDraft?: boolean) => { }; }; -const drawPreviewIfValid = () => { - return (dispatch: AppDispatch, getState: () => DatalensGlobalState) => { - const valid = getValid(getState()); - - if (valid) { - dispatch(drawPreview()); - } - }; -}; - export const updateQueryAndRedraw = ({query, index}: {query: QLConfigQuery; index: number}) => { return (dispatch: AppDispatch) => { dispatch(updateQuery({query, index})); diff --git a/src/ui/units/ql/store/reducers/ql.ts b/src/ui/units/ql/store/reducers/ql.ts index a61e968b39..b36b300748 100644 --- a/src/ui/units/ql/store/reducers/ql.ts +++ b/src/ui/units/ql/store/reducers/ql.ts @@ -17,7 +17,13 @@ import { } from 'units/wizard/selectors/visualization'; import {selectExtraSettings as getExtraSettingsWizard} from 'units/wizard/selectors/widget'; -import {AppStatus, DEFAULT_SALT, PANE_VIEWS, VisualizationStatus} from '../../constants'; +import { + AppStatus, + ConnectionStatus, + DEFAULT_SALT, + PANE_VIEWS, + VisualizationStatus, +} from '../../constants'; import {isPromQlQueriesEmpty, isQLQueryEmpty} from '../../utils/query'; import { ADD_PARAM, @@ -33,6 +39,7 @@ import { SET_CONNECTION, SET_CONNECTION_SOURCES, SET_CONNECTION_SOURCE_SCHEMA, + SET_CONNECTION_STATUS, SET_DEFAULT_PATH, SET_ENTRY, SET_ERROR, @@ -60,6 +67,7 @@ import { QLActionSetConnection, QLActionSetConnectionSourceSchema, QLActionSetConnectionSources, + QLActionSetConnectionStatus, QLActionSetDefaultPath, QLActionSetEntry, QLActionSetError, @@ -84,6 +92,7 @@ const initialState: QLState = { appStatus: AppStatus.Loading, defaultPath: DL.USER_LOGIN ? DL.USER_FOLDER : '/', visualizationStatus: VisualizationStatus.Empty, + connectionStatus: ConnectionStatus.Empty, extraSettings: {}, tablePreviewVisible: true, error: null, @@ -126,6 +135,8 @@ export const getChartType = (state: DatalensGlobalState) => state.ql?.chartType; export const getConnection = (state: DatalensGlobalState) => state.ql.connection; +export const getConnectionStatus = (state: DatalensGlobalState) => state.ql.connectionStatus; + export const getConnectionSources = (state: DatalensGlobalState) => state.ql.connectionSources; export const getConnectionSourcesSchemas = (state: DatalensGlobalState) => @@ -857,6 +868,14 @@ export default function ql(state: QLState = initialState, action: QLAction) { }; } + case SET_CONNECTION_STATUS: { + const {connectionStatus} = action as QLActionSetConnectionStatus; + return { + ...state, + connectionStatus, + }; + } + case SET_QUERY_VALUE: { const {newValue} = action as QLActionSetQueryValue; return { diff --git a/src/ui/units/ql/store/typings/ql.ts b/src/ui/units/ql/store/typings/ql.ts index 0fc886f587..a6be1cbe8c 100644 --- a/src/ui/units/ql/store/typings/ql.ts +++ b/src/ui/units/ql/store/typings/ql.ts @@ -10,7 +10,7 @@ import type { import {DatasetAction} from 'units/wizard/actions/dataset'; import {VisualizationAction} from 'units/wizard/actions/visualization'; -import {AppStatus, VisualizationStatus} from '../../constants'; +import {AppStatus, ConnectionStatus, VisualizationStatus} from '../../constants'; // QLEntry - chart created in QL export interface QLEntry extends GetEntryResponse { @@ -34,6 +34,7 @@ export interface QLState { appStatus: AppStatus; defaultPath: string; visualizationStatus: VisualizationStatus; + connectionStatus: ConnectionStatus; extraSettings: CommonSharedExtraSettings; tablePreviewVisible: boolean; error: Error | null; @@ -190,6 +191,11 @@ export interface QLActionSetConnection { connection: QLConnectionEntry; } +export interface QLActionSetConnectionStatus { + type: symbol; + connectionStatus: ConnectionStatus; +} + export interface QLActionSetQueryValue { type: symbol; newValue: string; diff --git a/src/ui/units/wizard/actions/dialog.ts b/src/ui/units/wizard/actions/dialog.ts index 8fab5d91aa..83fcdacf75 100644 --- a/src/ui/units/wizard/actions/dialog.ts +++ b/src/ui/units/wizard/actions/dialog.ts @@ -179,11 +179,19 @@ export function openDialogPointsSize({ visualization, }: OpenDialogPointsSizeArguments) { return function (dispatch: WizardDispatch) { + const visualizationId = visualization.id; + + const pointType = + visualizationId === WizardVisualizationId.Scatter || + visualizationId === WizardVisualizationId.ScatterD3 + ? 'scatter' + : 'geopoint'; + dispatch( openDialog({ id: DIALOG_POINTS_SIZE, props: { - pointType: visualization.id as 'geopoint' | 'scatter', + pointType, geopointsConfig: geopointsConfig, hasMeasure: Boolean(placeholder.items.length), onCancel: () => dispatch(closeDialog()), diff --git a/src/ui/units/wizard/containers/Wizard/SectionVisualization/PlaceholdersContainer/ShapesPlaceholder/ShapesPlaceholder.tsx b/src/ui/units/wizard/containers/Wizard/SectionVisualization/PlaceholdersContainer/ShapesPlaceholder/ShapesPlaceholder.tsx index 783cc00032..1edf157f9b 100644 --- a/src/ui/units/wizard/containers/Wizard/SectionVisualization/PlaceholdersContainer/ShapesPlaceholder/ShapesPlaceholder.tsx +++ b/src/ui/units/wizard/containers/Wizard/SectionVisualization/PlaceholdersContainer/ShapesPlaceholder/ShapesPlaceholder.tsx @@ -107,7 +107,8 @@ class ShapesPlaceholder extends React.Component { private openShapesDialog = (item?: Field | Field[]) => { const {visualization} = this.props; const paletteType = - visualization.id === WizardVisualizationId.Scatter + visualization.id === WizardVisualizationId.Scatter || + visualization.id === WizardVisualizationId.ScatterD3 ? PaletteTypes.Points : PaletteTypes.Lines; this.props.openDialogShapes({ diff --git a/tests/opensource-suites/dash/params/params.test.ts b/tests/opensource-suites/dash/params/params.test.ts index f0fd4cd68a..28009bf8a8 100644 --- a/tests/opensource-suites/dash/params/params.test.ts +++ b/tests/opensource-suites/dash/params/params.test.ts @@ -25,248 +25,137 @@ const DASH_PARAMS: Array<[string, string]> = [ ['param3', 'value1'], ]; -const checkNewParams = async (page: Page) => { - // checking for a new view of parameter setting +const openParams = async (page: Page) => { const newParamsButton = await page.$(slct(ParamsSettingsQA.Open)); - let isNewParams = false; if (newParamsButton !== null) { const newParams = await page.$(slct(ParamsSettingsQA.Settings)); if (!newParams) { await newParamsButton.click(); } - isNewParams = true; } - - return isNewParams; }; -const getParamsFromPage = async ( - page: Page, - {isOldDialogParams}: {isOldDialogParams?: boolean} = {}, -) => { - const isNewParams = await checkNewParams(page); - - // if the type of parameters is new - if (isNewParams) { - // getting the entire list of parameters without errors - const paramRows = await page.$$(slct(ParamsSettingsQA.ParamRow)); - - // for each row, getting the parameters and values in the array of arrays - const paramsContent = await Promise.all( - paramRows.map((paramRow) => - (async () => { - const paramsSeed: string[] = []; - - // getting the name of the parameter - const titleEl = await paramRow.$(`${slct(ParamsSettingsQA.ParamTitle)} input`); - const title = ((await titleEl?.inputValue()) || '').trim(); - - // if the parameter name is empty, then ignoring the line - if (title === '') { - return paramsSeed; - } +const getParamsFromPage = async (page: Page) => { + await openParams(page); - // getting the parameter values - const valuesEl = await paramRow.$$(slct(ParamsSettingsQA.ParamValue)); + // getting the entire list of parameters without errors + const paramRows = await page.$$(slct(ParamsSettingsQA.ParamRow)); - // if there are no parameter values, adding a parameter without a value - if (valuesEl.length < 1) { - paramsSeed.push(`${title}=`); - return paramsSeed; - } + // for each row, getting the parameters and values in the array of arrays + const paramsContent = await Promise.all( + paramRows.map((paramRow) => + (async () => { + const paramsSeed: string[] = []; - for (let i = 0; i < valuesEl.length; i++) { - const valuePromise = valuesEl[i]?.textContent(); + // getting the name of the parameter + const titleEl = await paramRow.$(`${slct(ParamsSettingsQA.ParamTitle)} input`); + const title = ((await titleEl?.inputValue()) || '').trim(); - const value = ((await valuePromise) || '').trim(); - - paramsSeed.push(`${title}=${value}`); - } + // if the parameter name is empty, then ignoring the line + if (title === '') { return paramsSeed; - })(), - ), - ); - - // transforming to single-level array of all parameters from an array of arrays - return paramsContent.flat(); - } - - // if the view of parameters is old, but the values need to be retrieved from the dialog - if (isOldDialogParams) { - // opening the parameters dialog - await page.click(slct(ParamsSettingsQA.AddOld)); + } - // getting the entire list of parameters - const paramRows = await page.$$(slct(ParamsSettingsQA.ParamRow)); + // getting the parameter values + const valuesEl = await paramRow.$$(slct(ParamsSettingsQA.ParamValue)); - // for each row, getting the parameters and values in the array of arrays - const paramsContent = await Promise.all( - paramRows.map((paramRow) => - (async () => { - const paramsSeed: string[] = []; - - // getting the name of the parameter - const titleEl = await paramRow.$(`${slct(ParamsSettingsQA.ParamTitle)} input`); - const title = ((await titleEl?.inputValue()) || '').trim(); - - // if the parameter name is empty, then ignoring the line - if (title === '') { - return paramsSeed; - } - - // getting the parameter values - const valuesEl = await paramRow.$$( - `${slct(ParamsSettingsQA.ParamValue)} input`, - ); - - // if there are no parameter values, adding a parameter without a value - if (valuesEl.length < 1) { - paramsSeed.push(`${title}=`); - return paramsSeed; - } - - for (let i = 0; i < valuesEl.length; i++) { - const valuePromise = valuesEl[i]?.inputValue(); - - const value = ((await valuePromise) || '').trim(); - - paramsSeed.push(`${title}=${value}`); - } + // if there are no parameter values, adding a parameter without a value + if (valuesEl.length < 1) { + paramsSeed.push(`${title}=`); return paramsSeed; - })(), - ), - ); + } - // closing the parameters dialog - await page.click(slct(ParamsSettingsQA.ApplyOld)); - - // transforming to a single-level array of all parameters from an array of arrays - return paramsContent.flat(); - } + for (let i = 0; i < valuesEl.length; i++) { + const valuePromise = valuesEl[i]?.textContent(); - // if the view of parameters is old without a dialog, then getting all the values at once - const paramsEl = await page.$$(slct(ParamsSettingsQA.ParamContentOld)); + const value = ((await valuePromise) || '').trim(); - const paramsContent = await Promise.all(paramsEl.map((el) => el.textContent())); + paramsSeed.push(`${title}=${value}`); + } + return paramsSeed; + })(), + ), + ); - const params = paramsContent.map((param) => (param || '').trim()); - return params; + // transforming to single-level array of all parameters from an array of arrays + return paramsContent.flat(); }; const addParam = async (page: Page, param: {title: string; value: string}) => { - const isNewParams = await checkNewParams(page); - - if (isNewParams) { - // click the add parameters button to ensure that a new line appears - await page.click(slct(ParamsSettingsQA.Add)); + await openParams(page); - // getting the entire list of parameters - const paramRows = await page.$$(slct(ParamsSettingsQA.ParamRow)); + // click the add parameters button to ensure that a new line appears + await page.click(slct(ParamsSettingsQA.Add)); - // getting all the parameter names - const paramRowsTitles = await Promise.all( - paramRows.map((paramRow) => - (async () => { - const titleEl = await paramRow.$(`input`); - const title = ((await titleEl?.inputValue()) || '').trim(); + // getting the entire list of parameters + const paramRows = await page.$$(slct(ParamsSettingsQA.ParamRow)); - return title; - })(), - ), - ); + // getting all the parameter names + const paramRowsTitles = await Promise.all( + paramRows.map((paramRow) => + (async () => { + const titleEl = await paramRow.$(`input`); + const title = ((await titleEl?.inputValue()) || '').trim(); - // finding the index of the first line of the parameter with an empty name - const addIndex = paramRowsTitles.findIndex((title) => title === ''); - - const lastParamRow = paramRows[addIndex]; + return title; + })(), + ), + ); - // filling the name of the parameter - const paramTitleInput = await lastParamRow.$(`${slct(ParamsSettingsQA.ParamTitle)} input`); - paramTitleInput?.fill(param.title); + // finding the index of the first line of the parameter with an empty name + const addIndex = paramRowsTitles.findIndex((title) => title === ''); - if (param.value) { - // click on the button to add a new value for the input field appears - const addParamValueButton = await lastParamRow.$(slct(ParamsSettingsQA.ParamAddValue)); - await addParamValueButton?.click(); + const lastParamRow = paramRows[addIndex]; - // finding the parameter value input field and add the value - const paramValueInput = await lastParamRow.$( - `${slct(ParamsSettingsQA.ParamValue)} input`, - ); - await paramValueInput?.fill(param.value); + // filling the name of the parameter + const paramTitleInput = await lastParamRow.$(`${slct(ParamsSettingsQA.ParamTitle)} input`); + paramTitleInput?.fill(param.title); - // shifting the focus from the input field for the value is preserved - await paramTitleInput?.focus(); - } + if (param.value) { + // click on the button to add a new value for the input field appears + const addParamValueButton = await lastParamRow.$(slct(ParamsSettingsQA.ParamAddValue)); + await addParamValueButton?.click(); - return; - } + // finding the parameter value input field and add the value + const paramValueInput = await lastParamRow.$(`${slct(ParamsSettingsQA.ParamValue)} input`); + await paramValueInput?.fill(param.value); - // if the view of parameters is old, then open the dialog for add a parameter and fill in the values - await page.click(slct(ParamsSettingsQA.AddOld)); - await page.fill(`${slct(ParamsSettingsQA.ParamTitle)} input`, param.title); - if (param.value) { - await page.fill(`${slct(ParamsSettingsQA.ParamValue)} input`, param.value); + // shifting the focus from the input field for the value is preserved + await paramTitleInput?.focus(); } - await page.click(slct(ParamsSettingsQA.ApplyOld)); }; const removeParam = async (page: Page, paramTitle: string) => { - const isNewParams = await checkNewParams(page); - - if (isNewParams) { - await page.click(slct(ParamsSettingsQA.Add)); + await openParams(page); - // getting the entire list of parameters - const paramRows = await page.$$(slct(ParamsSettingsQA.ParamRow)); + await page.click(slct(ParamsSettingsQA.Add)); - // getting all the parameter names - const paramRowsTitles = await Promise.all( - paramRows.map((paramRow) => - (async () => { - const titleEl = await paramRow.$(`input`); - const title = ((await titleEl?.inputValue()) || '').trim(); + // getting the entire list of parameters + const paramRows = await page.$$(slct(ParamsSettingsQA.ParamRow)); - return title; - })(), - ), - ); - - // finding the index of the parameter to be deleted - const removeIndex = paramRowsTitles.findIndex((title) => title === paramTitle); - if (removeIndex < 0) { - throw new Error('index of param for delete not found'); - } - - // emulating hovering over a line for the delete button to appear - await paramRows[removeIndex].hover(); - - const removeButton = await paramRows[removeIndex].$(slct(ParamsSettingsQA.Remove)); - await removeButton?.click(); + // getting all the parameter names + const paramRowsTitles = await Promise.all( + paramRows.map((paramRow) => + (async () => { + const titleEl = await paramRow.$(`input`); + const title = ((await titleEl?.inputValue()) || '').trim(); - return; - } - - // if the view of parameters is old, then first getting all the parameters - let paramsEl = await page.$$(slct(ParamsSettingsQA.ParamContentOld)); - const paramsContent = await Promise.all(paramsEl.map((el) => el.textContent())); - - const paramsTitles = paramsContent.map((param) => (param || '').split('=').shift()?.trim()); + return title; + })(), + ), + ); // finding the index of the parameter to be deleted - const removeIndex = paramsTitles.findIndex((title) => title === paramTitle); + const removeIndex = paramRowsTitles.findIndex((title) => title === paramTitle); if (removeIndex < 0) { throw new Error('index of param for delete not found'); } - // again getting the full components of the parameters, not just the content - paramsEl = await page.$$(slct(ParamsSettingsQA.ParamOld)); - - // emulating hovering over a parameter to make the delete button appear - await paramsEl[removeIndex].hover(); + // emulating hovering over a line for the delete button to appear + await paramRows[removeIndex].hover(); - const removeButton = await paramsEl[removeIndex].$(slct(ParamsSettingsQA.ParamRemoveOld)); + const removeButton = await paramRows[removeIndex].$(slct(ParamsSettingsQA.Remove)); await removeButton?.click(); }; diff --git a/tests/page-objects/dashboard/DashboardPage.ts b/tests/page-objects/dashboard/DashboardPage.ts index e49e8a3b80..593eb81311 100644 --- a/tests/page-objects/dashboard/DashboardPage.ts +++ b/tests/page-objects/dashboard/DashboardPage.ts @@ -30,6 +30,7 @@ import Revisions from '../common/Revisions'; import {SourceTypes} from '../../page-objects/common/DialogControlPO/SourceType'; import { + CreateEntityButton, DashboardDialogSettingsQa, DialogControlQa, DialogDashTitleQA, @@ -96,6 +97,7 @@ class DashboardPage extends BasePage { tabsList: '.gc-adaptive-tabs__tabs-list', tabItem: '.gc-adaptive-tabs__tab', tabItemActive: '.gc-adaptive-tabs__tab_active', + tabItemDisabled: '.gc-adaptive-tabs__tab_disabled', tabContainer: '.gc-adaptive-tabs__tab-container', selectControl: '.yc-select-control', /** @deprecated instead use selectItems */ @@ -106,6 +108,7 @@ class DashboardPage extends BasePage { selectItems: '.g-select-list', selectItemsMobile: '.g-select-list_mobile', selectItemTitle: '.g-select-list__option', + selectItemTitleDisabled: '.g-select-list__option_disabled', radioManualControl: DialogControlQa.radioSourceType, inputNameControl: 'control-name-input', @@ -165,13 +168,13 @@ class DashboardPage extends BasePage { return makrdownNode.innerHTML(); } - async createDashboard(dashName: string) { + async createDashboard({editDash, dashName}: {editDash: () => Promise; dashName: string}) { // click the button to create a new dashboard - await this.page.click(slct('create-entry-button')); + await this.page.click(slct(CreateEntityButton.Button)); + + // callback with start actions with dash in edit mode + await editDash(); - // TODO: CHARTS-8652, refine tests for new behavior - // temp step of changing the settings, because it is impossible to save the untouched dash - await this.enableDashboardTOC(); await this.clickSaveButton(); // waiting for the dialog to open, specify the name, save @@ -920,7 +923,7 @@ class DashboardPage extends BasePage { if (fullTab) { const tab = await fullTab.waitForSelector( - `${DashboardPage.selectors.tabItem} >> text=${tabName}`, + `${DashboardPage.selectors.tabItem}:not(${DashboardPage.selectors.tabItemDisabled}) >> text=${tabName}`, ); await tab.click(); return; @@ -934,7 +937,7 @@ class DashboardPage extends BasePage { if (shortTab) { await shortTab.click(); const tab = await this.page.waitForSelector( - `${DashboardPage.selectors.selectItems} ${DashboardPage.selectors.selectItemTitle} >> text=${tabName}`, + `${DashboardPage.selectors.selectItems} ${DashboardPage.selectors.selectItemTitle}:not(${DashboardPage.selectors.selectItemTitleDisabled}) >> text=${tabName}`, ); await tab.click(); return; diff --git a/tests/suites/dash/base/actionPanel.test.ts b/tests/suites/dash/base/actionPanel.test.ts index d0e0969eae..44f7af8fe6 100644 --- a/tests/suites/dash/base/actionPanel.test.ts +++ b/tests/suites/dash/base/actionPanel.test.ts @@ -1,9 +1,10 @@ import {Page, expect} from '@playwright/test'; import DashboardPage from '../../../page-objects/dashboard/DashboardPage'; -import {getUniqueTimestamp, openTestPage, slct} from '../../../utils'; +import {openTestPage, slct} from '../../../utils'; import {COMMON_SELECTORS} from '../../../utils/constants'; import datalensTest from '../../../utils/playwright/globalTestDefinition'; +import {CreateEntityButton} from '../../../../src/shared/constants/qa/components'; const PARAMS = { DASH_NAME_PREFIX: 'e2e-test-dash', @@ -15,11 +16,9 @@ datalensTest.describe('Dashboards are Basic functionality', () => { datalensTest('Adding a selector, the save button is active', async ({page}: {page: Page}) => { const dashboardPage = new DashboardPage({page}); - const dashName = `${PARAMS.DASH_NAME_PREFIX}-${getUniqueTimestamp()}`; - await openTestPage(page, '/dashboards'); - await dashboardPage.createDashboard(dashName); + await page.click(slct(CreateEntityButton.Button)); const saveButton = dashboardPage.page.locator(slct(COMMON_SELECTORS.ACTION_PANEL_SAVE_BTN)); @@ -31,8 +30,6 @@ datalensTest.describe('Dashboards are Basic functionality', () => { }); await expect(saveButton, 'Save button is not active').not.toBeDisabled(); - - await dashboardPage.deleteDashFromEditMode(); }); datalensTest( @@ -40,11 +37,9 @@ datalensTest.describe('Dashboards are Basic functionality', () => { async ({page}: {page: Page}) => { const dashboardPage = new DashboardPage({page}); - const dashName = `${PARAMS.DASH_NAME_PREFIX}-${getUniqueTimestamp()}`; - await openTestPage(page, '/dashboards'); - await dashboardPage.createDashboard(dashName); + await page.click(slct(CreateEntityButton.Button)); const saveButton = dashboardPage.page.locator( slct(COMMON_SELECTORS.ACTION_PANEL_SAVE_BTN), @@ -60,8 +55,6 @@ datalensTest.describe('Dashboards are Basic functionality', () => { await dashboardPage.deleteSelector(PARAMS.CONTROL_TITLE); await expect(saveButton, 'Save button is active').toBeDisabled(); - - await dashboardPage.deleteDashFromEditMode(); }, ); }); diff --git a/tests/suites/dash/base/relations.test.ts b/tests/suites/dash/base/relations.test.ts index ad0a573ac1..439c2b8eb4 100644 --- a/tests/suites/dash/base/relations.test.ts +++ b/tests/suites/dash/base/relations.test.ts @@ -36,27 +36,27 @@ datalensTest.describe('Dashboards are Basic functionality', () => { await openTestPage(page, '/dashboards'); - await dashboardPage.createDashboard(dashName); - - await dashboardPage.addSelector({ - controlTitle: PARAMS.CONTROL_TITLE, - controlFieldName: PARAMS.CONTROL_FIELD_NAME, - controlItems: PARAMS.CONTROL_ITEMS, - }); - - await dashboardPage.addChart({ - chartName: PARAMS.CHART_NAME, - chartUrl: PARAMS.CHART_URL, + await dashboardPage.createDashboard({ + editDash: async () => { + await dashboardPage.addSelector({ + controlTitle: PARAMS.CONTROL_TITLE, + controlFieldName: PARAMS.CONTROL_FIELD_NAME, + controlItems: PARAMS.CONTROL_ITEMS, + }); + + await dashboardPage.addChart({ + chartName: PARAMS.CHART_NAME, + chartUrl: PARAMS.CHART_URL, + }); + await dashboardPage.setupLinks({ + linkType: ConnectionsDialogQA.TypeSelectOutputOption, + chartField: PARAMS.CHART_FIELD, + selectorName: PARAMS.CONTROL_TITLE, + }); + }, + dashName, }); - await dashboardPage.setupLinks({ - linkType: ConnectionsDialogQA.TypeSelectOutputOption, - chartField: PARAMS.CHART_FIELD, - selectorName: PARAMS.CONTROL_TITLE, - }); - - await dashboardPage.clickSaveButton(); - await waitForCondition(async () => { const elems = await page.$$(SELECTORS.CHART_LEGEND_ITEM); return elems.length > 2; diff --git a/tests/suites/dash/mobile/mobileDash.test.ts b/tests/suites/dash/mobile/mobileDash.test.ts index 2234542440..2205d7133d 100644 --- a/tests/suites/dash/mobile/mobileDash.test.ts +++ b/tests/suites/dash/mobile/mobileDash.test.ts @@ -134,6 +134,10 @@ datalensTest.describe(`Dashboards - mobile version on ${DEVICE_NAME}`, () => { // checking the selector opening with a pop-up window await controlSelect.click(); const controlModal = await page.waitForSelector(SELECTORS.SHEET); + + // waiting for content load + await page.waitForSelector(SELECTORS.SELECTOR_ITEMS); + const controlBox = await controlModal.boundingBox(); // check that the selector has opened from below diff --git a/tests/suites/dash/params/params.test.ts b/tests/suites/dash/params/params.test.ts index 338d9e400d..428bdf6d14 100644 --- a/tests/suites/dash/params/params.test.ts +++ b/tests/suites/dash/params/params.test.ts @@ -25,248 +25,137 @@ const DASH_PARAMS: Array<[string, string]> = [ ['param3', 'value1'], ]; -const checkNewParams = async (page: Page) => { - // checking for a new view of parameter setting +const openParams = async (page: Page) => { const newParamsButton = await page.$(slct(ParamsSettingsQA.Open)); - let isNewParams = false; if (newParamsButton !== null) { const newParams = await page.$(slct(ParamsSettingsQA.Settings)); if (!newParams) { await newParamsButton.click(); } - isNewParams = true; } - - return isNewParams; }; -const getParamsFromPage = async ( - page: Page, - {isOldDialogParams}: {isOldDialogParams?: boolean} = {}, -) => { - const isNewParams = await checkNewParams(page); - - // if the type of parameters is new - if (isNewParams) { - // getting the entire list of parameters without errors - const paramRows = await page.$$(slct(ParamsSettingsQA.ParamRow)); - - // for each row, getting the parameters and values in the array of arrays - const paramsContent = await Promise.all( - paramRows.map((paramRow) => - (async () => { - const paramsSeed: string[] = []; - - // getting the name of the parameter - const titleEl = await paramRow.$(`${slct(ParamsSettingsQA.ParamTitle)} input`); - const title = ((await titleEl?.inputValue()) || '').trim(); - - // if the parameter name is empty, then ignoring the line - if (title === '') { - return paramsSeed; - } - - // getting the parameter values - const valuesEl = await paramRow.$$(slct(ParamsSettingsQA.ParamValue)); +const getParamsFromPage = async (page: Page) => { + await openParams(page); - // if there are no parameter values, adding a parameter without a value - if (valuesEl.length < 1) { - paramsSeed.push(`${title}=`); - return paramsSeed; - } + // getting the entire list of parameters without errors + const paramRows = await page.$$(slct(ParamsSettingsQA.ParamRow)); - for (let i = 0; i < valuesEl.length; i++) { - const valuePromise = valuesEl[i]?.textContent(); + // for each row, getting the parameters and values in the array of arrays + const paramsContent = await Promise.all( + paramRows.map((paramRow) => + (async () => { + const paramsSeed: string[] = []; - const value = ((await valuePromise) || '').trim(); + // getting the name of the parameter + const titleEl = await paramRow.$(`${slct(ParamsSettingsQA.ParamTitle)} input`); + const title = ((await titleEl?.inputValue()) || '').trim(); - paramsSeed.push(`${title}=${value}`); - } + // if the parameter name is empty, then ignoring the line + if (title === '') { return paramsSeed; - })(), - ), - ); - - // transforming to single-level array of all parameters from an array of arrays - return paramsContent.flat(); - } - - // if the view of parameters is old, but the values need to be retrieved from the dialog - if (isOldDialogParams) { - // opening the parameters dialog - await page.click(slct(ParamsSettingsQA.AddOld)); - - // getting the entire list of parameters - const paramRows = await page.$$(slct(ParamsSettingsQA.ParamRow)); + } - // for each row, getting the parameters and values in the array of arrays - const paramsContent = await Promise.all( - paramRows.map((paramRow) => - (async () => { - const paramsSeed: string[] = []; + // getting the parameter values + const valuesEl = await paramRow.$$(slct(ParamsSettingsQA.ParamValue)); - // getting the name of the parameter - const titleEl = await paramRow.$(`${slct(ParamsSettingsQA.ParamTitle)} input`); - const title = ((await titleEl?.inputValue()) || '').trim(); - - // if the parameter name is empty, then ignoring the line - if (title === '') { - return paramsSeed; - } - - // getting the parameter values - const valuesEl = await paramRow.$$( - `${slct(ParamsSettingsQA.ParamValue)} input`, - ); - - // if there are no parameter values, adding a parameter without a value - if (valuesEl.length < 1) { - paramsSeed.push(`${title}=`); - return paramsSeed; - } - - for (let i = 0; i < valuesEl.length; i++) { - const valuePromise = valuesEl[i]?.inputValue(); - - const value = ((await valuePromise) || '').trim(); - - paramsSeed.push(`${title}=${value}`); - } + // if there are no parameter values, adding a parameter without a value + if (valuesEl.length < 1) { + paramsSeed.push(`${title}=`); return paramsSeed; - })(), - ), - ); + } - // closing the parameters dialog - await page.click(slct(ParamsSettingsQA.ApplyOld)); + for (let i = 0; i < valuesEl.length; i++) { + const valuePromise = valuesEl[i]?.textContent(); - // transforming to a single-level array of all parameters from an array of arrays - return paramsContent.flat(); - } + const value = ((await valuePromise) || '').trim(); - // if the view of parameters is old without a dialog, then getting all the values at once - const paramsEl = await page.$$(slct(ParamsSettingsQA.ParamContentOld)); - - const paramsContent = await Promise.all(paramsEl.map((el) => el.textContent())); + paramsSeed.push(`${title}=${value}`); + } + return paramsSeed; + })(), + ), + ); - const params = paramsContent.map((param) => (param || '').trim()); - return params; + // transforming to single-level array of all parameters from an array of arrays + return paramsContent.flat(); }; const addParam = async (page: Page, param: {title: string; value: string}) => { - const isNewParams = await checkNewParams(page); + await openParams(page); - if (isNewParams) { - // click the add parameters button to ensure that a new line appears - await page.click(slct(ParamsSettingsQA.Add)); + // click the add parameters button to ensure that a new line appears + await page.click(slct(ParamsSettingsQA.Add)); - // getting the entire list of parameters - const paramRows = await page.$$(slct(ParamsSettingsQA.ParamRow)); + // getting the entire list of parameters + const paramRows = await page.$$(slct(ParamsSettingsQA.ParamRow)); - // getting all the parameter names - const paramRowsTitles = await Promise.all( - paramRows.map((paramRow) => - (async () => { - const titleEl = await paramRow.$(`input`); - const title = ((await titleEl?.inputValue()) || '').trim(); + // getting all the parameter names + const paramRowsTitles = await Promise.all( + paramRows.map((paramRow) => + (async () => { + const titleEl = await paramRow.$(`input`); + const title = ((await titleEl?.inputValue()) || '').trim(); - return title; - })(), - ), - ); - - // finding the index of the first line of the parameter with an empty name - const addIndex = paramRowsTitles.findIndex((title) => title === ''); - - const lastParamRow = paramRows[addIndex]; + return title; + })(), + ), + ); - // filling the name of the parameter - const paramTitleInput = await lastParamRow.$(`${slct(ParamsSettingsQA.ParamTitle)} input`); - paramTitleInput?.fill(param.title); + // finding the index of the first line of the parameter with an empty name + const addIndex = paramRowsTitles.findIndex((title) => title === ''); - if (param.value) { - // click on the button to add a new value for the input field appears - const addParamValueButton = await lastParamRow.$(slct(ParamsSettingsQA.ParamAddValue)); - await addParamValueButton?.click(); + const lastParamRow = paramRows[addIndex]; - // finding the parameter value input field and add the value - const paramValueInput = await lastParamRow.$( - `${slct(ParamsSettingsQA.ParamValue)} input`, - ); - await paramValueInput?.fill(param.value); + // filling the name of the parameter + const paramTitleInput = await lastParamRow.$(`${slct(ParamsSettingsQA.ParamTitle)} input`); + paramTitleInput?.fill(param.title); - // shifting the focus from the input field for the value is preserved - await paramTitleInput?.focus(); - } + if (param.value) { + // click on the button to add a new value for the input field appears + const addParamValueButton = await lastParamRow.$(slct(ParamsSettingsQA.ParamAddValue)); + await addParamValueButton?.click(); - return; - } + // finding the parameter value input field and add the value + const paramValueInput = await lastParamRow.$(`${slct(ParamsSettingsQA.ParamValue)} input`); + await paramValueInput?.fill(param.value); - // if the view of parameters is old, then open the dialog for add a parameter and fill in the values - await page.click(slct(ParamsSettingsQA.AddOld)); - await page.fill(`${slct(ParamsSettingsQA.ParamTitle)} input`, param.title); - if (param.value) { - await page.fill(`${slct(ParamsSettingsQA.ParamValue)} input`, param.value); + // shifting the focus from the input field for the value is preserved + await paramTitleInput?.focus(); } - await page.click(slct(ParamsSettingsQA.ApplyOld)); }; const removeParam = async (page: Page, paramTitle: string) => { - const isNewParams = await checkNewParams(page); - - if (isNewParams) { - await page.click(slct(ParamsSettingsQA.Add)); + await openParams(page); - // getting the entire list of parameters - const paramRows = await page.$$(slct(ParamsSettingsQA.ParamRow)); + await page.click(slct(ParamsSettingsQA.Add)); - // getting all the parameter names - const paramRowsTitles = await Promise.all( - paramRows.map((paramRow) => - (async () => { - const titleEl = await paramRow.$(`input`); - const title = ((await titleEl?.inputValue()) || '').trim(); + // getting the entire list of parameters + const paramRows = await page.$$(slct(ParamsSettingsQA.ParamRow)); - return title; - })(), - ), - ); - - // finding the index of the parameter to be deleted - const removeIndex = paramRowsTitles.findIndex((title) => title === paramTitle); - if (removeIndex < 0) { - throw new Error('index of param for delete not found'); - } - - // emulating hovering over a line for the delete button to appear - await paramRows[removeIndex].hover(); - - const removeButton = await paramRows[removeIndex].$(slct(ParamsSettingsQA.Remove)); - await removeButton?.click(); + // getting all the parameter names + const paramRowsTitles = await Promise.all( + paramRows.map((paramRow) => + (async () => { + const titleEl = await paramRow.$(`input`); + const title = ((await titleEl?.inputValue()) || '').trim(); - return; - } - - // if the view of parameters is old, then first getting all the parameters - let paramsEl = await page.$$(slct(ParamsSettingsQA.ParamContentOld)); - const paramsContent = await Promise.all(paramsEl.map((el) => el.textContent())); - - const paramsTitles = paramsContent.map((param) => (param || '').split('=').shift()?.trim()); + return title; + })(), + ), + ); // finding the index of the parameter to be deleted - const removeIndex = paramsTitles.findIndex((title) => title === paramTitle); + const removeIndex = paramRowsTitles.findIndex((title) => title === paramTitle); if (removeIndex < 0) { throw new Error('index of param for delete not found'); } - // again getting the full components of the parameters, not just the content - paramsEl = await page.$$(slct(ParamsSettingsQA.ParamOld)); - - // emulating hovering over a parameter to make the delete button appear - await paramsEl[removeIndex].hover(); + // emulating hovering over a line for the delete button to appear + await paramRows[removeIndex].hover(); - const removeButton = await paramsEl[removeIndex].$(slct(ParamsSettingsQA.ParamRemoveOld)); + const removeButton = await paramRows[removeIndex].$(slct(ParamsSettingsQA.Remove)); await removeButton?.click(); }; @@ -506,7 +395,7 @@ datalensTest.describe(`Dashboards - chart/external selector/dashboard parameters // adding a valid parameter await addParam(page, {title: validParam[0], value: validParam[1]}); - let actualParams = await getParamsFromPage(page, {isOldDialogParams: true}); + let actualParams = await getParamsFromPage(page); // checking if there is only one valid parameter on the page expect(actualParams).toEqual([validParam.join('=')]); @@ -534,7 +423,7 @@ datalensTest.describe(`Dashboards - chart/external selector/dashboard parameters // adding a valid parameter await addParam(page, {title: validParam[0], value: validParam[1]}); - actualParams = await getParamsFromPage(page, {isOldDialogParams: true}); + actualParams = await getParamsFromPage(page); // checking if there is only one valid parameter on the page expect(actualParams).toEqual([validParam.join('=')]); diff --git a/tests/suites/dash/revisions/revisionsListWithCreationDashboards.test.ts b/tests/suites/dash/revisions/revisionsListWithCreationDashboards.test.ts index 6424be7048..26f8cfccd6 100644 --- a/tests/suites/dash/revisions/revisionsListWithCreationDashboards.test.ts +++ b/tests/suites/dash/revisions/revisionsListWithCreationDashboards.test.ts @@ -15,7 +15,9 @@ import datalensTest from '../../../utils/playwright/globalTestDefinition'; import {ActionPanelEntryContextMenuQa} from '../../../../src/shared/constants/qa/action-panel'; import {DashRevisions} from '../../../../src/shared'; -const TIMEOUT = 4000; +const PARAMS = { + INITIAL_TITLE: 'New dash', +}; const waitCheckActualizeRevisionList = async ({ page, @@ -54,8 +56,12 @@ datalensTest.describe('Dashboard Versioning', () => { const dashName = `e2e-test-dash-revisions-${getUniqueTimestamp()}`; await openTestPage(page, '/dashboards'); - await dashboardPage.createDashboard(dashName); - await page.waitForTimeout(TIMEOUT); + await dashboardPage.createDashboard({ + editDash: async () => { + await dashboardPage.addTitle(PARAMS.INITIAL_TITLE); + }, + dashName, + }); }); datalensTest.afterEach(async ({page}: {page: Page}) => { const dashboardPage = new DashboardPage({page}); @@ -66,7 +72,6 @@ datalensTest.describe('Dashboard Versioning', () => { 'Creating a dashboard, checking the rendering of the revision list', async ({page}: {page: Page}) => { const dashboardPage = new DashboardPage({page}); - await dashboardPage.exitEditMode(); await dashboardPage.waitForOpeningRevisionsList(); let items = await page.$$(slct(COMMON_SELECTORS.REVISIONS_LIST_ROW)); @@ -104,6 +109,7 @@ datalensTest.describe('Dashboard Versioning', () => { 'Creating a dashboard, editing, checking the updated revision list', async ({page}: {page: Page}) => { const dashboardPage = new DashboardPage({page}); + await dashboardPage.enterEditMode(); await dashboardPage.editDashWithoutSaving(); await dashboardPage.clickSaveButton(); await dashboardPage.waitForOpeningRevisionsList(); @@ -118,6 +124,7 @@ datalensTest.describe('Dashboard Versioning', () => { 'Creating a dashboard, editing, saving as a draft, making the draft version relevant', async ({page}: {page: Page}) => { const dashboardPage = new DashboardPage({page}); + await dashboardPage.enterEditMode(); await dashboardPage.makeDraft(); await dashboardPage.waitForOpeningRevisionsList(); @@ -183,6 +190,7 @@ datalensTest.describe('Dashboard Versioning', () => { 'Creating a dashboard, editing, saving and publishing, checking the updated revision list', async ({page}: {page: Page}) => { const dashboardPage = new DashboardPage({page}); + await dashboardPage.enterEditMode(); await dashboardPage.makeDraft(); await dashboardPage.enterEditMode(); await dashboardPage.editDashWithoutSaving(); diff --git a/tests/suites/dash/revisions/revisionsListWithCreationSaveAsCopy.test.ts b/tests/suites/dash/revisions/revisionsListWithCreationSaveAsCopy.test.ts index a182692039..488335ad95 100644 --- a/tests/suites/dash/revisions/revisionsListWithCreationSaveAsCopy.test.ts +++ b/tests/suites/dash/revisions/revisionsListWithCreationSaveAsCopy.test.ts @@ -7,6 +7,10 @@ import {getUniqueTimestamp, openTestPage, slct, waitForCondition} from '../../.. import {COMMON_SELECTORS} from '../../../utils/constants'; import datalensTest from '../../../utils/playwright/globalTestDefinition'; +const PARAMS = { + INITIAL_TITLE: 'New dash', +}; + const waitCheckActualizeRevisionList = async ({ page, actualItemIndex, @@ -44,8 +48,13 @@ datalensTest.describe('Dashboard Versioning', () => { const dashName = `e2e-test-dash-revisions-${getUniqueTimestamp()}`; await openTestPage(page, '/dashboards'); - await dashboardPage.createDashboard(dashName); - await page.waitForTimeout(RENDER_TIMEOUT); + await dashboardPage.createDashboard({ + editDash: async () => { + await dashboardPage.addTitle(PARAMS.INITIAL_TITLE); + }, + dashName, + }); + await dashboardPage.enterEditMode(); }); datalensTest( 'Creating a dashboard, editing, save as a new dashboard', @@ -100,7 +109,7 @@ datalensTest.describe('Dashboard Versioning', () => { // wait for page load to be able to remove await page.waitForTimeout(RENDER_TIMEOUT); - await dashboardPage.deleteDashFromEditMode(); + await dashboardPage.deleteDashFromViewMode(); }, ); }); diff --git a/tests/suites/dash/selectors/addSelectors.test.ts b/tests/suites/dash/selectors/addSelectors.test.ts index 1cc85715ba..97efb4b2e9 100644 --- a/tests/suites/dash/selectors/addSelectors.test.ts +++ b/tests/suites/dash/selectors/addSelectors.test.ts @@ -34,18 +34,19 @@ datalensTest.describe('Dashboards are Basic functionality', () => { await openTestPage(page, '/dashboards'); - await dashboardPage.createDashboard(dashName); - - // adding a selector with a default value - await dashboardPage.addSelector({ - controlTitle: PARAMS.CONTROL_TITLE, - controlFieldName: PARAMS.CONTROL_FIELD_NAME, - controlItems, - defaultValue: controlDefaultValue, + await dashboardPage.createDashboard({ + editDash: async () => { + // adding a selector with a default value + await dashboardPage.addSelector({ + controlTitle: PARAMS.CONTROL_TITLE, + controlFieldName: PARAMS.CONTROL_FIELD_NAME, + controlItems, + defaultValue: controlDefaultValue, + }); + }, + dashName, }); - await dashboardPage.clickSaveButton(); - // get the control by name const control = await getControlByTitle(page, PARAMS.CONTROL_TITLE); diff --git a/tests/suites/dash/widgets/autoheight.test.ts b/tests/suites/dash/widgets/autoheight.test.ts index 7366463410..d67017ade2 100644 --- a/tests/suites/dash/widgets/autoheight.test.ts +++ b/tests/suites/dash/widgets/autoheight.test.ts @@ -2,7 +2,7 @@ import type {Page} from '@playwright/test'; import {COMMON_CHARTKIT_SELECTORS} from '../../../page-objects/constants/chartkit'; import DashboardPage from '../../../page-objects/dashboard/DashboardPage'; -import {openTestPage, waitForCondition} from '../../../utils'; +import {openTestPage, slct, waitForCondition} from '../../../utils'; import {RobotChartsDashboardUrls} from '../../../utils/constants'; import datalensTest from '../../../utils/playwright/globalTestDefinition'; @@ -10,11 +10,20 @@ const DASH_STATE = { markdownAutoHeightTabActive: 'cd80ae4f86', }; -const TEXTS = { +const TABS = { + CHARTS: 'Charts', + SELECTORS: 'Selectors', +}; + +const WIDGET_TABS = { TAB_CHART: 'columnchart', TAB_MD_AUTO: 'md-autoheight', + TAB_INDICATOR: 'metric-autoheight', + TAB_TABLE_AUTO: 'table-autoheight', }; +const CONTROL_SELECT_ITEM = 'chartkit-control'; + const hasNoScroll = async (page: Page, selector: string) => { return await page.evaluate((selector) => { const body = document.querySelector(selector); @@ -60,26 +69,59 @@ datalensTest.describe('Dashboards - Auto-height of widgets', () => { // waiting for the widget content to load await page.waitForSelector(`.${COMMON_CHARTKIT_SELECTORS.graph}`); - await dashboard.changeWidgetTab(TEXTS.TAB_MD_AUTO); + await dashboard.changeWidgetTab(WIDGET_TABS.TAB_MD_AUTO); // waiting for the widget content to load - const selector = `.${COMMON_CHARTKIT_SELECTORS.scrollableNode}`; - await page.waitForSelector(selector); + const scrollableSelector = `.${COMMON_CHARTKIT_SELECTORS.scrollableNode}`; + await page.waitForSelector(scrollableSelector); // check that there is no scroll await waitForCondition(async () => { - const noScroll = await hasNoScroll(page, selector); + const noScroll = await hasNoScroll(page, scrollableSelector); + return noScroll === true; + }); + + // go to auto-height indicator tab + await dashboard.changeWidgetTab(WIDGET_TABS.TAB_INDICATOR); + + // waiting for the widget content to load + await page.waitForSelector(scrollableSelector); + + // check that there is no scroll + await waitForCondition(async () => { + const noScroll = await hasNoScroll(page, scrollableSelector); + return noScroll === true; + }); + + // go to auto-height table tab + await dashboard.changeWidgetTab(WIDGET_TABS.TAB_TABLE_AUTO); + + // waiting for the widget content to load + await page.waitForSelector(scrollableSelector); + + // check that there is no scroll + await waitForCondition(async () => { + const noScroll = await hasNoScroll(page, scrollableSelector); return noScroll === true; }); // go back to the first tab - await dashboard.changeWidgetTab(TEXTS.TAB_CHART); + await dashboard.changeWidgetTab(WIDGET_TABS.TAB_CHART); // waiting for the widget content to load - await page.waitForSelector(selector); + await page.waitForSelector(`.${COMMON_CHARTKIT_SELECTORS.graph}`); + + // Switching dashboart to selectors tab + await dashboard.changeTab({tabName: TABS.SELECTORS}); + + // waiting for the selector content to load + await page.waitForSelector(slct(CONTROL_SELECT_ITEM)); + + // checking scrollable block + await page.waitForSelector(scrollableSelector); // check that there is no scroll await waitForCondition(async () => { - const noScroll = await hasNoScroll(page, selector); + const noScroll = await hasNoScroll(page, scrollableSelector); return noScroll === true; }); },