diff --git a/src/components/Table/__tests__/Table.withTableSettings.test.tsx b/src/components/Table/__tests__/Table.withTableSettings.test.tsx index 1e0f453d9e..25a35b2e68 100644 --- a/src/components/Table/__tests__/Table.withTableSettings.test.tsx +++ b/src/components/Table/__tests__/Table.withTableSettings.test.tsx @@ -288,7 +288,7 @@ describe('withTableSettings', () => { await userEvent.click(screen.getByRole('button', {name: 'Table settings'})); await userEvent.click(await screen.findByRole('button', {name: 'description'})); - await userEvent.click(screen.getByRole('button', {name: 'Apply'})); + await userEvent.click(screen.getByRole('button', {name: 'button_apply'})); expect(updateSettings).toHaveBeenCalledWith([ { diff --git a/src/components/Table/hoc/index.ts b/src/components/Table/hoc/index.ts index c280d72514..767a609842 100644 --- a/src/components/Table/hoc/index.ts +++ b/src/components/Table/hoc/index.ts @@ -20,6 +20,4 @@ export {withTableSettings} from './withTableSettings/withTableSettings'; export type { WithTableSettingsProps, TableSettingsData, - TableColumnSetupItem, } from './withTableSettings/withTableSettings'; -export * from './withTableSettings/TableColumnSetup/TableColumnSetup'; diff --git a/src/components/Table/hoc/withTableSettings/TableColumnSetup/TableColumnSetup.scss b/src/components/Table/hoc/withTableSettings/TableColumnSetup/TableColumnSetup.scss index a9a535280c..003c2f5d69 100644 --- a/src/components/Table/hoc/withTableSettings/TableColumnSetup/TableColumnSetup.scss +++ b/src/components/Table/hoc/withTableSettings/TableColumnSetup/TableColumnSetup.scss @@ -1,6 +1,6 @@ @use '../../../../variables'; -$block: '.#{variables.$ns}table-column-setup'; +$block: '.#{variables.$ns}inner-table-column-setup'; #{$block} { &__controls { diff --git a/src/components/Table/hoc/withTableSettings/TableColumnSetup/TableColumnSetup.tsx b/src/components/Table/hoc/withTableSettings/TableColumnSetup/TableColumnSetup.tsx index a4309e2ec2..4965d10b6f 100644 --- a/src/components/Table/hoc/withTableSettings/TableColumnSetup/TableColumnSetup.tsx +++ b/src/components/Table/hoc/withTableSettings/TableColumnSetup/TableColumnSetup.tsx @@ -3,6 +3,7 @@ import React from 'react'; import {Gear, Grip, Lock} from '@gravity-ui/icons'; import {DragDropContext, Draggable, Droppable} from 'react-beautiful-dnd'; import type { + DraggableChildrenFn, DraggableProvided, DraggableStateSnapshot, OnDragEndResponder, @@ -19,21 +20,17 @@ import type { TreeSelectRenderContainer, TreeSelectRenderItem, } from '../../../../TreeSelect/types'; -import type {ListItemViewProps} from '../../../../useList'; +import type {ListItemCommonProps, ListItemViewProps} from '../../../../useList'; import {ListContainerView, ListItemView} from '../../../../useList'; import {block} from '../../../../utils/cn'; -import type {TableColumnSetupItem, TableSetting} from '../withTableSettings'; +import type {TableColumnConfig} from '../../../Table'; +import type {TableSetting} from '../withTableSettings'; import i18n from './i18n'; import './TableColumnSetup.scss'; -function identity(value: T): T { - return value; -} - -const b = block('table-column-setup'); -const tableColumnSetupCn = b(null); +const b = block('inner-table-column-setup'); const controlsCn = b('controls'); const reorderArray = (list: T[], startIndex: number, endIndex: number): T[] => { @@ -44,17 +41,35 @@ const reorderArray = (list: T[], startIndex: number, endIndex return result; }; -const prepareDndItems = (tableColumnItems: TableColumnSetupItem[]) => { - return tableColumnItems.map((tableColumnItem) => { - const hasSelectionIcon = tableColumnItem.isRequired === false; +const prepareStickyState = ( + itemsById: Record, + visibleFlattenIds: string[], +) => { + let lastStickyStartIdx = 0; + for (; lastStickyStartIdx !== visibleFlattenIds.length; lastStickyStartIdx++) { + const visibleFlattenId = visibleFlattenIds[lastStickyStartIdx]; + const item = itemsById[visibleFlattenId]; + + if (item?.sticky !== 'left' && item?.sticky !== 'start') { + break; + } + } - return { - ...tableColumnItem, - startSlot: tableColumnItem.isRequired ? : undefined, - hasSelectionIcon, - selected: hasSelectionIcon ? tableColumnItem.isSelected : undefined, - }; - }); + let firstStickyEndIdx = visibleFlattenIds.length; + for (; firstStickyEndIdx !== 0; firstStickyEndIdx--) { + const visibleFlattenId = visibleFlattenIds[firstStickyEndIdx - 1]; + const item = itemsById[visibleFlattenId]; + + if (item?.sticky !== 'right' && item?.sticky !== 'end') { + break; + } + } + + return { + stickyStartItemIdList: visibleFlattenIds.slice(0, lastStickyStartIdx), + sortableItemIdList: visibleFlattenIds.slice(lastStickyStartIdx, firstStickyEndIdx), + stickyEndItemIdList: visibleFlattenIds.slice(firstStickyEndIdx), + }; }; const prepareValue = (tableColumnItems: TableColumnSetupItem[]) => { @@ -70,10 +85,13 @@ const prepareValue = (tableColumnItems: TableColumnSetupItem[]) => { }; interface RenderContainerProps { - provided: DraggableProvided; - snapshot: DraggableStateSnapshot; + isDragDisabled?: boolean; + provided?: DraggableProvided; + snapshot?: DraggableStateSnapshot; } +const RENDER_DRAG_DISABLED_CONTAINER_PROPS: RenderContainerProps = {isDragDisabled: true}; + interface SwitcherProps { onKeyDown: React.KeyboardEventHandler; onClick: React.MouseEventHandler; @@ -86,50 +104,68 @@ interface UseDndRenderContainerParams { const useDndRenderContainer = ({onDragEnd, renderControls}: UseDndRenderContainerParams) => { const uniqId = useUniqId(); - const dndRenderContainer: TreeSelectRenderContainer = ({ + const dndRenderContainer: TreeSelectRenderContainer = ({ renderItem, visibleFlattenIds, - items: _items, + itemsById, containerRef, id, className, }) => { - const visibleFlattenItemList = visibleFlattenIds.map((visibleFlattenId, idx) => - renderItem(visibleFlattenId, idx), + const renderDndActiveItem: DraggableChildrenFn = (provided, snapshot, rubric) => { + const renderContainerProps: RenderContainerProps = { + provided, + snapshot, + }; + + return renderItem( + visibleFlattenIds[rubric.source.index], + rubric.source.index, + renderContainerProps, + ); + }; + + const {stickyStartItemIdList, sortableItemIdList, stickyEndItemIdList} = prepareStickyState( + itemsById, + visibleFlattenIds, ); + const stickyStartItemList = stickyStartItemIdList.map((visibleFlattenId, idx) => { + return renderItem(visibleFlattenId, idx, RENDER_DRAG_DISABLED_CONTAINER_PROPS); + }); + + const sortableItemList = sortableItemIdList.map((visibleFlattenId, idx) => { + return renderItem(visibleFlattenId, idx + stickyStartItemIdList.length); + }); + + const stickyEndItemList = stickyEndItemIdList.map((visibleFlattenId, idx) => { + return renderItem( + visibleFlattenId, + stickyStartItemList.length + sortableItemList.length + idx, + RENDER_DRAG_DISABLED_CONTAINER_PROPS, + ); + }); + return ( + {stickyStartItemList} - { - const renderContainerProps: RenderContainerProps = { - provided, - snapshot, - }; - - return renderItem( - visibleFlattenIds[rubric.source.index], - rubric.source.index, - renderContainerProps, - ); - }} - > + {(droppableProvided) => { return (
- {visibleFlattenItemList} + {sortableItemList} {droppableProvided.placeholder}
); }}
+ {stickyEndItemList}
{renderControls()}
@@ -140,23 +176,30 @@ const useDndRenderContainer = ({onDragEnd, renderControls}: UseDndRenderContaine }; const useDndRenderItem = (sortable: boolean | undefined) => { - const renderDndItem: TreeSelectRenderItem = ({ - data, + const renderDndItem: TreeSelectRenderItem = ({ + data: item, props, index, renderContainerProps, }) => { - const isDragDisabled = sortable === false; + const isDragDisabled = sortable === false || renderContainerProps?.isDragDisabled === true; + const endSlot = isDragDisabled ? undefined : ; + const hasSelectionIcon = !item.isRequired; + const startSlot = item.isRequired ? : undefined; + const selected = item.isRequired ? false : props.selected; - const endSlot = - data.endSlot ?? (isDragDisabled ? undefined : ); - - const commonProps = { + const commonProps: ListItemViewProps = { ...props, - ...data, + selected, + startSlot, + hasSelectionIcon, endSlot, }; + if (isDragDisabled) { + return ; + } + const renderItem = (provided: DraggableProvided, snapshot: DraggableStateSnapshot) => ( { /> ); - if (renderContainerProps) { + if (renderContainerProps?.provided && renderContainerProps.snapshot) { return renderItem(renderContainerProps.provided, renderContainerProps.snapshot); } return ( {renderItem} @@ -186,11 +229,17 @@ const useDndRenderItem = (sortable: boolean | undefined) => { return renderDndItem; }; -type Item = TableColumnSetupItem & - ListItemViewProps & { - id: string; - isDragDisabled?: boolean; +export type TableColumnSetupItem = TableSetting & { + title: React.ReactNode; + isRequired?: boolean; + sticky?: TableColumnConfig['sticky']; +}; + +const mapItemDataToProps = (item: TableColumnSetupItem): ListItemCommonProps => { + return { + title: item.title, }; +}; export type RenderControls = (params: { DefaultApplyButton: React.ComponentType; @@ -214,6 +263,8 @@ export interface TableColumnSetupProps { * @deprecated */ renderControls?: RenderControls; + + className?: string; } export const TableColumnSetup = (props: TableColumnSetupProps) => { @@ -225,6 +276,7 @@ export const TableColumnSetup = (props: TableColumnSetupProps) => { onUpdate: propsOnUpdate, sortable, renderControls, + className, } = props; const [open, setOpen] = React.useState(false); @@ -295,20 +347,17 @@ export const TableColumnSetup = (props: TableColumnSetupProps) => { }); }; - const [value, dndItems] = React.useMemo( - () => [prepareValue(items), prepareDndItems(items)] as const, - [items], - ); + const value = React.useMemo(() => prepareValue(items), [items]); return ( ( columns: TableColumnConfig[], settings: TableSettingsData, @@ -79,6 +74,7 @@ const getTableColumnSetupItem = ( isSelected: isProtected ? true : isSelected, isRequired: isProtected, title: column ? getColumnStringTitle(column) : id, + sticky: column?.sticky, }; }; diff --git a/src/components/TableColumnSetup/TableColumnSetup.scss b/src/components/TableColumnSetup/TableColumnSetup.scss new file mode 100644 index 0000000000..e1efacdfdf --- /dev/null +++ b/src/components/TableColumnSetup/TableColumnSetup.scss @@ -0,0 +1,10 @@ +@use '../variables'; + +$block: '.#{variables.$ns}table-column-setup'; + +#{$block} { + &__status { + margin-inline-start: 5px; + color: var(--g-color-text-secondary); + } +} diff --git a/src/components/TableColumnSetup/TableColumnSetup.tsx b/src/components/TableColumnSetup/TableColumnSetup.tsx new file mode 100644 index 0000000000..cca38e2fd3 --- /dev/null +++ b/src/components/TableColumnSetup/TableColumnSetup.tsx @@ -0,0 +1,130 @@ +import React from 'react'; + +import {Gear} from '@gravity-ui/icons'; + +import type {PopperPlacement} from '../../hooks/private'; +import {Button} from '../Button'; +import {Icon} from '../Icon'; +import type {TableColumnConfig} from '../Table/Table'; +import type {TableColumnSetupItem as NewTableColumnSetupItem} from '../Table/hoc/withTableSettings/TableColumnSetup/TableColumnSetup'; +import {TableColumnSetup as NewTableColumnSetup} from '../Table/hoc/withTableSettings/TableColumnSetup/TableColumnSetup'; +import type {TableSetting} from '../Table/hoc/withTableSettings/withTableSettings'; +import {block} from '../utils/cn'; + +import i18n from './i18n'; + +import './TableColumnSetup.scss'; + +const b = block('table-column-setup'); + +export interface TableColumnSetupItem { + id: string; + title: React.ReactNode; + selected?: boolean; + required?: boolean; + sticky?: TableColumnConfig['sticky']; +} + +type Item = TableColumnSetupItem; + +interface SwitcherProps { + onKeyDown: React.KeyboardEventHandler; + onClick: React.MouseEventHandler; +} + +export interface TableColumnSetupProps { + // for Button + disabled?: boolean; + /** + * @deprecated Use renderSwitcher instead + */ + switcher?: React.ReactElement | undefined; + renderSwitcher?: (props: SwitcherProps) => React.ReactElement | undefined; + + items: Item[]; + sortable?: boolean; + + onUpdate: (updated: Item[]) => void; + popupWidth?: number | 'fit' | undefined; + popupPlacement?: PopperPlacement; + getItemTitle?: (item: Item) => TableColumnSetupItem['title']; + showStatus?: boolean; + className?: string; +} + +export const TableColumnSetup = (props: TableColumnSetupProps) => { + const { + switcher, + renderSwitcher: renderSwitcherProps, + disabled, + popupWidth, + popupPlacement, + className, + items: propsItems, + sortable = true, + showStatus, + onUpdate: propsOnUpdate, + } = props; + + const renderStatus = () => { + if (!showStatus) { + return null; + } + + const selected = propsItems.reduce((acc, cur) => (cur.selected ? acc + 1 : acc), 0); + const all = propsItems.length; + const status = `${selected}/${all}`; + + return {status}; + }; + + const renderSwitcher = (switcherProps: SwitcherProps) => { + return ( + renderSwitcherProps?.(switcherProps) || + switcher || ( + + ) + ); + }; + + const items = propsItems.map( + ({id, title, required, selected, sticky}) => ({ + id, + title, + isRequired: required, + isSelected: selected, + sticky, + }), + ); + + const onUpdate = (newSettings: TableSetting[]) => { + propsOnUpdate( + newSettings.map(({id, isSelected}) => { + const prevItem = propsItems.find((item) => item.id === id); + + return { + id, + selected: isSelected, + title: prevItem?.title, + required: prevItem?.required, + }; + }), + ); + }; + + return ( + + ); +}; diff --git a/src/components/TableColumnSetup/i18n/en.json b/src/components/TableColumnSetup/i18n/en.json new file mode 100644 index 0000000000..933351927b --- /dev/null +++ b/src/components/TableColumnSetup/i18n/en.json @@ -0,0 +1,3 @@ +{ + "button_switcher": "Columns" +} diff --git a/src/components/TableColumnSetup/i18n/index.ts b/src/components/TableColumnSetup/i18n/index.ts new file mode 100644 index 0000000000..820c28b081 --- /dev/null +++ b/src/components/TableColumnSetup/i18n/index.ts @@ -0,0 +1,8 @@ +import {addComponentKeysets} from '../../utils/addComponentKeysets'; + +import en from './en.json'; +import ru from './ru.json'; + +const COMPONENT = 'TableColumnSetup'; + +export default addComponentKeysets({en, ru}, COMPONENT); diff --git a/src/components/TableColumnSetup/i18n/ru.json b/src/components/TableColumnSetup/i18n/ru.json new file mode 100644 index 0000000000..05ed5176f6 --- /dev/null +++ b/src/components/TableColumnSetup/i18n/ru.json @@ -0,0 +1,3 @@ +{ + "button_switcher": "Колонки" +} diff --git a/src/components/TableColumnSetup/index.ts b/src/components/TableColumnSetup/index.ts new file mode 100644 index 0000000000..482ccb159b --- /dev/null +++ b/src/components/TableColumnSetup/index.ts @@ -0,0 +1 @@ +export * from './TableColumnSetup'; diff --git a/src/components/index.ts b/src/components/index.ts index f1da847a03..8a59a62982 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -42,6 +42,7 @@ export * from './Slider'; export * from './Spin'; export * from './Switch'; export * from './Table'; +export * from './TableColumnSetup'; export * from './Tabs'; export * from './Text'; export * from './Toaster';