Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Table): table settings component added #51

Merged
merged 46 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
7cc356c
feat(Table): table settings component added
Sep 19, 2024
08f489f
feat(Table): fix parent column selection
Sep 23, 2024
7032c7e
feat(Table): reuse addComponentKeysets from gravity-ui
Sep 23, 2024
da0a80d
feat(Table): change column classnames
Sep 23, 2024
28ad3d2
feat(Table): set popup placement position
Sep 23, 2024
d6348f9
feat(Table): change i18n deps type
Sep 23, 2024
bd548aa
feat(Table): add useMemo hooks for heavy computing
Sep 23, 2024
6625299
feat(Table): remove group sizes
Sep 23, 2024
50423b1
feat(Table): use Item type
Sep 23, 2024
2929c9a
feat(Table): fix data id
Sep 23, 2024
735514a
feat(Table): remove console.log
Sep 23, 2024
bc41dcf
feat(Table): added enableHiding flag support
Sep 23, 2024
353b924
feat(Table): fix dnd behaviour
Sep 23, 2024
69d2ea9
feat(Table): added options to complex disable sorting or filtration
Sep 24, 2024
732df4a
feat(Table): added useTableSettings hook
Sep 24, 2024
f8aa117
feat(Table): prevent firing event then feature disabled
Sep 24, 2024
3acedf9
feat(Table): added alert on ordering change
Sep 24, 2024
a9ca0c4
feat(Table): hardcode replaced by constants
Sep 24, 2024
c712c5e
feat(Table): set parent group font weight
Sep 24, 2024
f24b296
feat(Table): fixed darag-handle line-height
Sep 24, 2024
dc92186
feat(Table): added pinng for settings column
Sep 24, 2024
abe4e36
feat(Table): removed unnecessary react fragment
Sep 24, 2024
f1cb6e7
feat(Table): used spacing variables
Sep 24, 2024
520afad
feat(Table): removed unnecessary ref
Sep 24, 2024
d9b473a
feat(Table): added onSettingsApply callback for TableSettings component
Sep 24, 2024
27dd8e1
feat(Table): removed settingsColumn constant
Sep 24, 2024
cc71f88
feat(Table): removed all typecasts
Sep 24, 2024
94ca15b
feat(Table): fixed useState initializing
Sep 24, 2024
a1dc0c4
feat(Table): removed all localisation components, they are not needed
Sep 24, 2024
d3224f1
feat(Table): added component export
Sep 24, 2024
03998c0
feat(Table): touch sensor added
Sep 24, 2024
2fd151e
feat(Table): fix pin styles
Sep 24, 2024
fdaf9a5
feat(Table): remove naming duplicates
Sep 26, 2024
22e3387
feat(Table): used certain prop instead of spread
Sep 26, 2024
31a70d8
feat(Table): removed hasChildrenColumns helper func
Sep 26, 2024
42067b2
feat(Table): added library prefix for component localization
Sep 26, 2024
0143771
feat(Table): removed unnecessary unknown
Sep 26, 2024
9512ce8
feat(Table): moved cell backgrounds to story styles
Sep 26, 2024
7aa7453
feat(Table): fixed deps
Sep 26, 2024
e60e268
feat(Table): fixed height jumps on drag start
Sep 26, 2024
12fc6c8
feat(Table): fixed drag context and styles for prevent tattered behav…
Sep 26, 2024
c3adeb9
feat(Table): limit popover max height
Sep 26, 2024
76ed107
feat(Table): fixed toggling indeterminate state
Sep 26, 2024
531945d
feat(Table): lint fix
Sep 26, 2024
7b4e950
feat(Table): misspells
Sep 26, 2024
343b1bc
feat(Table): disabled dnd by x axis
Sep 26, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -349,4 +349,46 @@ const ResizingDemo = () => {
};
```

### Column settings
Raubzeug marked this conversation as resolved.
Show resolved Hide resolved

```tsx
const columns: ColumnDef<Person>[] = [
// ...other columns
{
id: 'settings_column_id',
header: (context) => <TableSettings {...context} columnId="settings_column_id" />,
}, // or u can use function getSettingsColumn
];

const data: Person[] = [
/* ... */
];

const TableSettingsDemo = () => {
const [columnVisibility, onColumnVisibilityChange] = React.useState<VisibilityState>({
// for outside control and initial state
column_id: false, // for hidding by default
});
const [columnOrder, onColumnOrderChange] = React.useState<string[]>([
/* leaf columns ids */
]); // for outside control and initial state

// Alternative variant to get state, callbacks, and set on setting apply callbacks - using useTableSettings hook:
// const {state, callbacks} = useTableSettings({initialVisibility: {}, initialOrder: []})

const table = useTable({
columns,
data,
state: {
columnVisibility,
columnOrder,
},
onColumnVisibilityChange,
onColumnOrderChange,
});

return <Table table={table} />;
};
```

Learn more about the table and the column resizing properties in the react-table [docs](https://tanstack.com/table/v8/docs/api/features/column-sizing)
11 changes: 7 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",
"@gravity-ui/eslint-config": "^3.2.0",
"@gravity-ui/i18n": "^1.6.0",
"@gravity-ui/icons": "^2.11.0",
"@gravity-ui/prettier-config": "^1.1.0",
"@gravity-ui/stylelint-config": "^4.0.1",
Expand Down Expand Up @@ -99,6 +100,7 @@
"peerDependencies": {
"@dnd-kit/core": "^6.0.0",
"@dnd-kit/sortable": "^8.0.0",
"@gravity-ui/i18n": "^1.0.0",
"@gravity-ui/icons": "^2.0.0",
"@gravity-ui/uikit": "^6.0.0",
"react": "^17.0.0 || ^18.0.0",
Expand Down
3 changes: 3 additions & 0 deletions src/components/TableSettings/TableSettings.classname.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import {block} from '../../utils';

export const b = block('table-settings');
21 changes: 21 additions & 0 deletions src/components/TableSettings/TableSettings.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
@use '../variables';

$block: '.#{variables.$ns}table-settings';

#{$block} {
max-height: 90vh;
display: flex;
flex-direction: column;

&__popover-content {
padding: var(--g-spacing-1);
margin-block-end: calc(-1 * calc(var(--g-spacing-2) + var(--g-spacing-half)));
overflow: auto;
}

&__popover-actions {
position: relative;
padding: var(--g-spacing-2);
background-color: var(--g-color-base-background);
}
}
143 changes: 143 additions & 0 deletions src/components/TableSettings/TableSettings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import React from 'react';

import {DndContext, MouseSensor, TouchSensor, useSensor, useSensors} from '@dnd-kit/core';
import {SortableContext, verticalListSortingStrategy} from '@dnd-kit/sortable';
import {Gear} from '@gravity-ui/icons';
import {Button, Divider, Icon, Popup} from '@gravity-ui/uikit';
import type {PopperPlacement} from '@gravity-ui/uikit/build/esm/hooks/private';
import type {Column, Header, Table, VisibilityState} from '@tanstack/react-table';

import {TableSettingsColumn} from '../TableSettingsColumn/TableSettingsColumn';

import {b} from './TableSettings.classname';
import {
getInitialOrderItems,
orderStateToColumnOrder,
useOrderedItems,
} from './TableSettings.utils';
import i18n from './i18n';

import './TableSettings.scss';

export interface TableSettingsOptions {
sortable?: boolean;
filterable?: boolean;
}

export interface TableSettingsProps<TData> extends TableSettingsOptions {
table: Table<TData>;
columnId?: string;
onSettingsApply?: ({
visibilityState,
columnOrder,
}: {
visibilityState: VisibilityState;
columnOrder: string[];
}) => void;
}

const POPUP_PLACEMENT: PopperPlacement = ['bottom-end', 'bottom', 'top-end', 'top', 'auto'];

export const TableSettings = <TData extends unknown>({
table,
sortable = true,
filterable = true,
columnId,
onSettingsApply,
}: TableSettingsProps<TData>) => {
const anchorRef = React.useRef<HTMLButtonElement>(null);
const [open, setOpen] = React.useState<boolean>(false);
const columns = table.getAllColumns();
const filteredColumns = React.useMemo(
() => (columnId ? columns.filter((otherColumn) => otherColumn.id !== columnId) : columns),
[columnId, columns],
);
const headers = table.getFlatHeaders();
const headersById = React.useMemo(() => {
return headers.reduce<Record<string, Header<TData, unknown>>>((acc, header) => {
const result = {...acc};
result[header.column.id] = header;
return acc;
}, {});
}, [headers]);

const [visibilityState, setVisibilityState] = React.useState(
() => table.getState().columnVisibility,
);
const [orderState, setOrderState] = React.useState(() => getInitialOrderItems(filteredColumns));

const applyNewSettings = () => {
const columnOrder = orderStateToColumnOrder(orderState);

if (onSettingsApply) onSettingsApply({visibilityState, columnOrder});

if (filterable) table.setColumnVisibility(visibilityState);
if (sortable) table.setColumnOrder(columnOrder);

setOpen(false);
};

const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));

const {orderedItems, activeDepth, handleDragEnd, handleDragStart, handleDragCancel} =
useOrderedItems(filteredColumns, orderState, setOrderState);

const renderColumns = (renderedColumns: Column<TData>[]) => {
return renderedColumns.map((innerColumn) => {
const children = renderColumns(innerColumn.columns);
const header = headersById[innerColumn.id];

return (
<TableSettingsColumn
key={innerColumn.id}
column={innerColumn}
header={header}
visibilityState={visibilityState}
sortable={sortable}
filterable={filterable}
activeDepth={activeDepth}
onVisibilityToggle={setVisibilityState}
>
{children}
</TableSettingsColumn>
);
});
};

return (
<React.Fragment>
<Popup
open={open}
onClose={() => setOpen(false)}
anchorRef={anchorRef}
placement={POPUP_PLACEMENT}
contentClassName={b()}
>
<div className={b('popover-content')}>
<DndContext
onDragEnd={handleDragEnd}
onDragStart={handleDragStart}
onDragCancel={handleDragCancel}
sensors={sensors}
>
<SortableContext
items={orderedItems.map(({id}) => id)}
strategy={verticalListSortingStrategy}
>
{renderColumns(orderedItems)}
</SortableContext>
</DndContext>
</div>
<Divider />
<div className={b('popover-actions')}>
<Button view="action" size="m" onClick={applyNewSettings} width="max">
{i18n('button_apply')}
</Button>
</div>
</Popup>
<Button view="flat-secondary" size="m" ref={anchorRef} onClick={() => setOpen(!open)}>
<Icon data={Gear} />
</Button>
</React.Fragment>
);
};
113 changes: 113 additions & 0 deletions src/components/TableSettings/TableSettings.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import React from 'react';

import type {DragEndEvent, DragStartEvent, UniqueIdentifier} from '@dnd-kit/core';
import {arrayMove} from '@dnd-kit/sortable';
import type {Column} from '@tanstack/react-table';

const filterColumns = <TData extends unknown>(
column: Column<TData> | undefined,
): column is Column<TData> => Boolean(column);

const orderItem = <TData extends unknown>(
item: Column<TData>,
state: Record<string, string[]>,
): Column<TData> => {
const childrenOrder = state[item.id] ?? [];
const orderedChildren = childrenOrder
.map((orderId) => item.columns?.find(({id}) => id === orderId))
.filter(filterColumns);
return {
...item,
columns: orderedChildren?.map((child) => orderItem(child, state)) ?? [],
};
};

export const findContainer = (itemsMap: Record<string, string[]>, id?: UniqueIdentifier) => {
if (!id) return undefined;
return Object.keys(itemsMap).find((key) => itemsMap[key].includes(id.toString()));
};

export const useOrderedItems = <TData extends unknown>(
items: Column<TData>[],
orderState: Record<string, string[]>,
setOrderState: React.Dispatch<React.SetStateAction<Record<string, string[]>>>,
) => {
const [activeDepth, setActiveDepth] = React.useState<number | undefined>();

const depthMap = React.useMemo(() => {
const stack = [...items];
const result: Record<string, number> = {};

while (stack.length) {
const item = stack.pop();
if (!item) continue;
result[item.id] = item.depth;
if (item.columns) stack.push(...item.columns);
}

return result;
}, [items]);

const orderedItems = React.useMemo(() => {
return (
orderState['root']
.map((orderId) => items.find(({id}) => id === orderId))
.filter(filterColumns)
.map((item) => orderItem(item, orderState)) ?? []
);
}, [orderState, items]);

const handleDragEnd = ({active, over}: DragEndEvent) => {
setActiveDepth(undefined);
const activeContainer = findContainer(orderState, active.id);
const overContainer = findContainer(orderState, over?.id);
if (!activeContainer || !overContainer || activeContainer !== overContainer) return;
const activeIndex = orderState[activeContainer].indexOf(active.id.toString());
const overIndex = over ? orderState[overContainer].indexOf(over?.id.toString()) : -1;
if (activeIndex !== overIndex) {
setOrderState((prevState) => ({
...prevState,
[overContainer]: arrayMove(orderState[overContainer], activeIndex, overIndex),
}));
}
};

const handleDragStart = ({active}: DragStartEvent) => {
setActiveDepth(depthMap[active.id]);
};

const handleDragCancel = () => {
setActiveDepth(undefined);
};

return {orderedItems, activeDepth, handleDragEnd, handleDragStart, handleDragCancel};
};

export const orderStateToColumnOrder = (state: Record<string, string[]>) => {
const result = [];
const stack = [...state['root']];

while (stack.length) {
const item = stack.shift();
if (!item) continue;
const children = state[item];

if (children.length) stack.unshift(...children);
else result.push(item);
}
return result;
};

export const getInitialOrderItems = <TData extends unknown>(treeItems: Column<TData>[]) => {
const stack = [...treeItems];
const result: Record<string, string[]> = {root: treeItems.map(({id}) => id)};

while (stack.length) {
const item = stack.shift();
if (!item) continue;
result[item.id] = item?.columns?.map(({id}) => id) ?? [];
if (item.columns) stack.push(...item.columns);
}

return result;
};
Loading
Loading