diff --git a/package-lock.json b/package-lock.json index cb759b5363..32927a4b61 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@gravity-ui/app-layout": "^2.1.0", "@gravity-ui/browserslist-config": "^4.3.0", "@gravity-ui/chartkit": "^5.19.1", - "@gravity-ui/dashkit": "^8.22.1", + "@gravity-ui/dashkit": "^8.23.0", "@gravity-ui/date-utils": "^2.5.6", "@gravity-ui/expresskit": "^2.1.0", "@gravity-ui/gateway": "^3.1.1", @@ -5056,9 +5056,9 @@ } }, "node_modules/@gravity-ui/dashkit": { - "version": "8.22.1", - "resolved": "https://registry.npmjs.org/@gravity-ui/dashkit/-/dashkit-8.22.1.tgz", - "integrity": "sha512-XTwJMxIisbzJd4q5YvuanaSoYsJcMTu5JYwDN5CCdKlinXjX3GVDAYkDee0A0x4xuYFRD3qclSCAJLSHTiACdg==", + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/@gravity-ui/dashkit/-/dashkit-8.23.0.tgz", + "integrity": "sha512-ChnCAv84HDULby/mtcQKhRTbRlsFZq8T5YkDisolMjaVL5tZ5BcbTcaPyO8giSf1k0XqM7e+/sH3YK3zQKM4hA==", "dependencies": { "@bem-react/classname": "^1.6.0", "@gravity-ui/icons": "^2.11.0", diff --git a/package.json b/package.json index 8b1fd4c0f6..43f379accb 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "@gravity-ui/app-layout": "^2.1.0", "@gravity-ui/browserslist-config": "^4.3.0", "@gravity-ui/chartkit": "^5.19.1", - "@gravity-ui/dashkit": "^8.22.1", + "@gravity-ui/dashkit": "^8.23.0", "@gravity-ui/date-utils": "^2.5.6", "@gravity-ui/expresskit": "^2.1.0", "@gravity-ui/gateway": "^3.1.1", diff --git a/src/ui/components/DashKit/helpers.tsx b/src/ui/components/DashKit/helpers.tsx index 4d28b8bf1c..1110eea33f 100644 --- a/src/ui/components/DashKit/helpers.tsx +++ b/src/ui/components/DashKit/helpers.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import type {ConfigItem, DashKitProps, ItemState} from '@gravity-ui/dashkit'; +import type {ConfigItem, DashKitProps, ItemParams, ItemState, MenuItem} from '@gravity-ui/dashkit'; import {DashKit} from '@gravity-ui/dashkit'; import {MenuItems} from '@gravity-ui/dashkit/helpers'; import {Copy, Pencil, TrashBin} from '@gravity-ui/icons'; @@ -22,16 +22,16 @@ const b = block('dashkit-plugin-menu'); type TabData = {id: string; chartId: string; params: StringParams; state: ItemState}; -const removeEmptyParams = (params: StringParams) => { +const removeEmptyParams = (params: ItemParams) => { return Object.entries(params).reduce((result, [key, value]) => { if (value !== null && value !== undefined) { result[key] = value; } return result; - }, {} as StringParams); + }, {} as ItemParams); }; -export function getEditLink(configItem: ConfigItem, params: StringParams, state: ItemState) { +export function getEditLink(configItem: ConfigItem, params: ItemParams, state: ItemState) { const {type, data} = configItem; let entryId: string | undefined; @@ -64,20 +64,20 @@ export function getEditLink(configItem: ConfigItem, params: StringParams, state: return `${endpoint}/${entryId}${queryPrams}`; } -export function getDashKitMenu() { +export function getDashKitMenu(): Array { return [ { id: 'edit', title: i18n('label_edit'), icon: , - handler: (configItem: ConfigItem, params: StringParams, state: ItemState) => { + handler: (configItem, params, state) => { const link = getEditLink(configItem, params, state); if (link) { window.open(link, '_blank'); } }, - visible: (configItem: ConfigItem) => { + visible: (configItem) => { const {type, data} = configItem; return ( diff --git a/src/ui/units/dash/containers/Body/Body.tsx b/src/ui/units/dash/containers/Body/Body.tsx index bc9c9aed0b..c4ece8fcff 100644 --- a/src/ui/units/dash/containers/Body/Body.tsx +++ b/src/ui/units/dash/containers/Body/Body.tsx @@ -80,7 +80,6 @@ import { getPastedWidgetData, getPreparedCopyItemOptions, memoizedGetLocalTabs, - sortByOrderIdOrLayoutComparator, } from '../../modules/helpers'; import type {TabsHashStates} from '../../store/actions/dashTyped'; import { @@ -114,6 +113,7 @@ import { import {getPropertiesWithResizeHandles} from '../../utils/dashkitProps'; import {scrollIntoView} from '../../utils/scrollUtils'; import {DashError} from '../DashError/DashError'; +import {getGroupedItems} from '../Dialogs/Tabs/PopupWidgetsOrder/helpers'; import {FixedHeaderContainer, FixedHeaderControls} from '../FixedHeader/FixedHeader'; import TableOfContent from '../TableOfContent/TableOfContent'; import {Tabs} from '../Tabs/Tabs'; @@ -181,12 +181,6 @@ type GetPreparedCopyItemOptions = ( itemToCopy: PreparedCopyItemOptions, ) => PreparedCopyItemOptions; -const GROUPS_WEIGHT = { - [FIXED_GROUP_HEADER_ID]: 2, - [FIXED_GROUP_CONTAINER_ID]: 1, - [DEFAULT_GROUP]: 0, -} as const; - // Body is used as a core in different environments class Body extends React.PureComponent { static getDerivedStateFromProps(props: BodyProps, state: DashBodyState) { @@ -289,6 +283,10 @@ class Body extends React.PureComponent { byId: {}, columns: 0, }; + _memoizedOrderedConfig?: { + key: DashKitProps['config']; + config: DashKitProps['config']; + }; state: DashBodyState = { fixedHeaderCollapsed: {}, @@ -702,6 +700,11 @@ class Body extends React.PureComponent { if (isEmpty && !hasFixedContainerElements && this.props.mode !== Mode.Edit) { return null; } + + if (params.isMobile) { + return children; + } + const {fixedHeaderCollapsed = false, isEmbeddedMode, isPublicMode} = params.context; return ( @@ -730,6 +733,11 @@ class Body extends React.PureComponent { if (isEmpty && !hasFixedHeaderElements && this.props.mode !== Mode.Edit) { return null; } + + if (params.isMobile) { + return children; + } + const {fixedHeaderCollapsed = false, isEmbeddedMode, isPublicMode} = params.context; return ( @@ -871,41 +879,6 @@ class Body extends React.PureComponent { return this._memoizedMenu; }; - getMobileLayout(): DashKitProps['config'] | null { - const {tabData} = this.props; - const tabDataConfig = tabData as DashKitProps['config'] | null; - - if (!tabDataConfig) { - return tabDataConfig; - } - - const {byId, columns} = this.getMemoLayoutMap(); - const getWeight = (item: DashTabItem): number => { - const parentId = getLayoutParentId(byId[item.id]); - - return (GROUPS_WEIGHT as any)[parentId] || 0; - }; - - return { - ...tabDataConfig, - items: (tabDataConfig.items as DashTab['items']) - .sort((prev, next) => { - const prevWeight = getWeight(prev); - const nextWeight = getWeight(next); - - if (prevWeight === nextWeight) { - return sortByOrderIdOrLayoutComparator(prev, next, byId, columns); - } - - return nextWeight - prevWeight; - }) - .map((item, index) => ({ - ...item, - orderId: item.orderId || index, - })) as ConfigItem[], - }; - } - dataProviderContextGetter = () => { const {tabId, entryId} = this.props; @@ -918,6 +891,36 @@ class Body extends React.PureComponent { [DASH_INFO_HEADER]: new URLSearchParams(dashInfo).toString(), }; }; + private getConfig = () => { + const {tabData} = this.props; + const tabDataConfig = tabData; + + if (!tabDataConfig || !DL.IS_MOBILE) { + return tabDataConfig; + } + + const memoItems = this._memoizedOrderedConfig; + + if (!memoItems || memoItems.key !== tabDataConfig) { + const sortedItems = getGroupedItems(tabDataConfig.items, tabDataConfig.layout).reduce( + (list, group) => { + list.push(...group); + return list; + }, + [], + ); + + this._memoizedOrderedConfig = { + key: tabDataConfig as DashKitProps['config'], + config: { + ...tabDataConfig, + items: sortedItems as ConfigItem[], + }, + }; + } + + return this._memoizedOrderedConfig?.config; + }; private renderDashkit = () => { const {isGlobalDragging} = this.state; @@ -925,7 +928,6 @@ class Body extends React.PureComponent { mode, settings, tabs, - tabData, handlerEditClick, isEditModeLoading, globalParams, @@ -935,9 +937,7 @@ class Body extends React.PureComponent { const context = this.getContext(); - const tabDataConfig = DL.IS_MOBILE - ? this.getMobileLayout() - : (tabData as DashKitProps['config'] | null); + const tabDataConfig = this.getConfig(); const isEmptyTab = !tabDataConfig?.items.length; diff --git a/src/ui/units/dash/containers/Dialogs/Tabs/PopupWidgetsOrder/PopupWidgetsOrder.scss b/src/ui/units/dash/containers/Dialogs/Tabs/PopupWidgetsOrder/PopupWidgetsOrder.scss index 1b990f6eb5..f9b89a2c89 100644 --- a/src/ui/units/dash/containers/Dialogs/Tabs/PopupWidgetsOrder/PopupWidgetsOrder.scss +++ b/src/ui/units/dash/containers/Dialogs/Tabs/PopupWidgetsOrder/PopupWidgetsOrder.scss @@ -18,6 +18,14 @@ overflow: auto; } + &__group-list { + margin-bottom: 32px; + + &:last-child { + margin-bottom: 0; + } + } + &__row { padding: 8px; } diff --git a/src/ui/units/dash/containers/Dialogs/Tabs/PopupWidgetsOrder/PopupWidgetsOrder.tsx b/src/ui/units/dash/containers/Dialogs/Tabs/PopupWidgetsOrder/PopupWidgetsOrder.tsx index 986f31882c..d0f6bb99d1 100644 --- a/src/ui/units/dash/containers/Dialogs/Tabs/PopupWidgetsOrder/PopupWidgetsOrder.tsx +++ b/src/ui/units/dash/containers/Dialogs/Tabs/PopupWidgetsOrder/PopupWidgetsOrder.tsx @@ -4,14 +4,13 @@ import type {PopupProps} from '@gravity-ui/uikit'; import {Button, Dialog, List, Popup} from '@gravity-ui/uikit'; import block from 'bem-cn-lite'; import {I18n} from 'i18n'; -import {cloneDeep} from 'lodash'; import type {DashTabItem, DashTabLayout} from 'shared'; import {DialogTabsQA, EntryDialogQA} from 'shared'; import {registry} from 'ui/registry'; import {getLayoutMap, sortByLayoutComparator} from '../../../../modules/helpers'; -import {getPreparedItems, getUpdatedItems, getWidgetRowText} from './helpers'; +import {getGroupedItems, getUpdatedItems, getWidgetRowText} from './helpers'; import './PopupWidgetsOrder.scss'; @@ -36,6 +35,8 @@ const LIST_PREFIX_TEXTS = { image: i18n('label_image'), }; +const ROW_HEIGHT = 34; + const WidgetRow = (item: DashTabItem) => { const typeLabel = LIST_PREFIX_TEXTS[item.type]; const text = getWidgetRowText(item); @@ -49,31 +50,32 @@ const WidgetRow = (item: DashTabItem) => { export const PopupWidgetsOrder = (props: PopupWidgetsOrderProps) => { const {anchorRef, onClose, onApply, items, layout, tabId} = props; - const [preparedItems, setPreparedItems] = React.useState>([]); - const isApplyDisabled = preparedItems.length <= 1; + const [groupedItems, setGroupedItems] = React.useState>>([]); + const isApplyDisabled = items.length <= 1; React.useEffect(() => { - if (items?.length) { - setPreparedItems(getPreparedItems(cloneDeep(items), layout)); - } - }, [items]); + setGroupedItems(getGroupedItems(items, layout)); + }, [items, layout]); const handleOnSortEnd = React.useCallback( - ({oldIndex, newIndex}: {oldIndex: number; newIndex: number}) => { + (groupIndex, {oldIndex, newIndex}: {oldIndex: number; newIndex: number}) => { if (oldIndex === newIndex) { return; } + const groupItems = groupedItems[groupIndex]; const newItems = getUpdatedItems({ - items: preparedItems, - dragItem: preparedItems[oldIndex], + items: groupItems, + dragItem: groupItems[oldIndex], oldIndex, newIndex, }); + const clone = [...groupedItems]; + clone[groupIndex] = newItems; - setPreparedItems(newItems); + setGroupedItems(clone); }, - [preparedItems], + [groupedItems], ); const handleResetToDefaultClick = React.useCallback(() => { @@ -91,11 +93,27 @@ export const PopupWidgetsOrder = (props: PopupWidgetsOrderProps) => { }; }); - setPreparedItems(newItems); - }, [preparedItems]); + setGroupedItems(getGroupedItems(newItems, layout)); + }, [items, layout]); const handleApplyClick = () => { - onApply({tabId, items: preparedItems}); + const itemsLayoutOrder = layout.reduce>((memo, item, index) => { + memo[item.i] = index; + + return memo; + }, {}); + + const sortedItems = groupedItems + .reduce((memo, group) => { + memo.push(...group); + return memo; + }, []) + .sort((a, b) => itemsLayoutOrder[a.id] - itemsLayoutOrder[b.id]); + + onApply({ + tabId, + items: sortedItems, + }); }; const {getCaptionText} = registry.dash.functions.getAll(); @@ -118,21 +136,33 @@ export const PopupWidgetsOrder = (props: PopupWidgetsOrderProps) => { >
- {preparedItems?.length ? ( - - ) : ( -
{i18n('label_no-items')}
- )} +
+ {groupedItems.map((preparedItems, index) => { + if (preparedItems.length) { + return ( + 1} + items={preparedItems} + className={b('group-list')} + itemClassName={b('row')} + // Fix in @gravity-ui 7 + itemsHeight={preparedItems.length * ROW_HEIGHT} + onSortEnd={handleOnSortEnd.bind(this, index)} + renderItem={WidgetRow} + qa={DialogTabsQA.PopupWidgetOrderList} + /> + ); + } + + return null; + })} + {groupedItems.every((list) => list.length === 0) && ( +
{i18n('label_no-items')}
+ )} +
, layout: Array, layout: Array) => { + const preparedItems = getPreparedItems(items, layout); + + const parentByItem = layout.reduce>((memo, item) => { + const parent = item.parent ?? DEFAULT_GROUP; + + memo[item.i] = parent; + + return memo; + }, {}); + + return [FIXED_GROUP_HEADER_ID, FIXED_GROUP_CONTAINER_ID, DEFAULT_GROUP].map((group) => { + return preparedItems.filter((item) => parentByItem[item.id] === group); + }); +}; + export const getUpdatedItems = ({ items, dragItem, diff --git a/src/ui/units/dash/containers/Dialogs/Tabs/TabItem.tsx b/src/ui/units/dash/containers/Dialogs/Tabs/TabItem.tsx index e6b47b2b58..1378eebddb 100644 --- a/src/ui/units/dash/containers/Dialogs/Tabs/TabItem.tsx +++ b/src/ui/units/dash/containers/Dialogs/Tabs/TabItem.tsx @@ -46,7 +46,6 @@ type OwnProps = { type DispatchProps = ResolveThunks; type Props = OwnProps & DispatchProps; - class TabItem extends React.PureComponent { state = { editMode: false, diff --git a/src/ui/units/dash/containers/Dialogs/Tabs/Tabs.tsx b/src/ui/units/dash/containers/Dialogs/Tabs/Tabs.tsx index 70eb490231..1b04f93850 100644 --- a/src/ui/units/dash/containers/Dialogs/Tabs/Tabs.tsx +++ b/src/ui/units/dash/containers/Dialogs/Tabs/Tabs.tsx @@ -41,6 +41,7 @@ type State = { expandedItemIndex?: number; }; +const ROW_HEIGHT = 40; class Tabs extends React.PureComponent { static getDerivedStateFromProps(nextProps: Props, prevState: State) { if (nextProps.visible === prevState.prevVisible) { @@ -80,6 +81,9 @@ class Tabs extends React.PureComponent { itemClassName={b('sortable-item', { highlight: tabs.length > 1, })} + // Fix in @gravity-ui 7 + itemsHeight={ROW_HEIGHT * tabs.length} + itemHeight={ROW_HEIGHT} activeItemIndex={expandedItemIndex} onSortEnd={({oldIndex, newIndex}) => this.moveItem(oldIndex, newIndex)} renderItem={(tab, isActive) => ( @@ -99,7 +103,6 @@ class Tabs extends React.PureComponent { onChangeItemOrder={this.handleChangeOrderItem} /> )} - itemHeight={40} />
nextOrderId) { - return 1; - } else if (prevOrderId < nextOrderId) { - return -1; - } - return 0; + return prevOrderId - nextOrderId; }; export const getLayoutParentId = (layout: DashTabLayout) => {