Implement deeplink, creating pastes, log deduplication
Summary: This diff implements the remaining features in the logs plugin: - deeplinking - merging duplicate rows The logs plugin source code has now been reduced from originally `935` to `285` LoC. All optimisation code has been removed from the plugin: * debouncing data processing * pre-rendering (and storing!) all rows Finally applied some further styling tweaks and applied some renames to DataTable / DataSource + types finetuning. Some more will follow. Fixed a emotion warning in unit tests which was pretty annoying. Reviewed By: passy Differential Revision: D26666190 fbshipit-source-id: e45e289b4422ebeb46cad927cfc0cfcc9566834f
This commit is contained in:
committed by
Facebook GitHub Bot
parent
dec8e88aeb
commit
525e079284
@@ -228,8 +228,6 @@ const persistor = persistStore(store, undefined, () => {
|
||||
setPersistor(persistor);
|
||||
|
||||
const CodeBlock = styled(Input.TextArea)({
|
||||
fontFamily:
|
||||
'SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace;',
|
||||
fontSize: '0.8em',
|
||||
...theme.monospace,
|
||||
color: theme.textColorSecondary,
|
||||
});
|
||||
|
||||
@@ -42,7 +42,7 @@ export function SidebarTitle({
|
||||
const LeftMenuTitle = styled(Layout.Horizontal)({
|
||||
padding: `0px ${theme.inlinePaddingH}px`,
|
||||
lineHeight: `${theme.space.large}px`,
|
||||
fontSize: theme.fontSize.smallBody,
|
||||
fontSize: theme.fontSize.small,
|
||||
textTransform: 'uppercase',
|
||||
'> :first-child': {
|
||||
flex: 1,
|
||||
|
||||
@@ -83,7 +83,7 @@ function ResultTopDialog(props: {status: HealthcheckStatus}) {
|
||||
showIcon
|
||||
message={messages.message}
|
||||
style={{
|
||||
fontSize: theme.fontSize.smallBody,
|
||||
fontSize: theme.fontSize.small,
|
||||
lineHeight: '16px',
|
||||
fontWeight: 'bold',
|
||||
paddingTop: '10px',
|
||||
@@ -179,7 +179,7 @@ function SetupDoctorFooter(props: {
|
||||
checked={props.acknowledgeCheck}
|
||||
onChange={(e) => props.onAcknowledgeCheck(e.target.checked)}
|
||||
style={{display: 'flex', alignItems: 'center'}}>
|
||||
<Text style={{fontSize: theme.fontSize.smallBody}}>
|
||||
<Text style={{fontSize: theme.fontSize.small}}>
|
||||
Do not show warning about these problems at startup
|
||||
</Text>
|
||||
</Checkbox>
|
||||
|
||||
@@ -78,7 +78,7 @@ function WelcomeFooter({
|
||||
return (
|
||||
<FooterContainer>
|
||||
<Checkbox checked={checked} onChange={(e) => onCheck(e.target.checked)}>
|
||||
<Text style={{fontSize: theme.fontSize.smallBody}}>
|
||||
<Text style={{fontSize: theme.fontSize.small}}>
|
||||
Show this when app opens (or use ? icon on left)
|
||||
</Text>
|
||||
</Checkbox>
|
||||
|
||||
@@ -45,7 +45,7 @@ export default function BlocklistSettingButton(props: {
|
||||
key={pluginId}
|
||||
closable
|
||||
onClose={() => props.onRemovePlugin(pluginId)}>
|
||||
<Text style={{fontSize: theme.fontSize.smallBody}} ellipsis>
|
||||
<Text style={{fontSize: theme.fontSize.small}} ellipsis>
|
||||
{pluginId}
|
||||
</Text>
|
||||
</Tag>
|
||||
@@ -54,7 +54,7 @@ export default function BlocklistSettingButton(props: {
|
||||
) : (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: theme.fontSize.smallBody,
|
||||
fontSize: theme.fontSize.small,
|
||||
color: theme.textColorSecondary,
|
||||
}}>
|
||||
No Blocklisted Plugin
|
||||
@@ -70,7 +70,7 @@ export default function BlocklistSettingButton(props: {
|
||||
key={category}
|
||||
closable
|
||||
onClose={() => props.onRemoveCategory(category)}>
|
||||
<Text style={{fontSize: theme.fontSize.smallBody}} ellipsis>
|
||||
<Text style={{fontSize: theme.fontSize.small}} ellipsis>
|
||||
{category}
|
||||
</Text>
|
||||
</Tag>
|
||||
@@ -79,7 +79,7 @@ export default function BlocklistSettingButton(props: {
|
||||
) : (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: theme.fontSize.smallBody,
|
||||
fontSize: theme.fontSize.small,
|
||||
color: theme.textColorSecondary,
|
||||
}}>
|
||||
No Blocklisted Category
|
||||
|
||||
@@ -69,7 +69,7 @@ function DetailCollapse({detail}: {detail: string | React.ReactNode}) {
|
||||
<Paragraph
|
||||
type="secondary"
|
||||
style={{
|
||||
fontSize: theme.fontSize.smallBody,
|
||||
fontSize: theme.fontSize.small,
|
||||
marginBottom: 0,
|
||||
}}
|
||||
ellipsis={{rows: 3}}>
|
||||
@@ -92,7 +92,7 @@ function DetailCollapse({detail}: {detail: string | React.ReactNode}) {
|
||||
<Collapse.Panel
|
||||
key="detail"
|
||||
header={
|
||||
<Text type="secondary" style={{fontSize: theme.fontSize.smallBody}}>
|
||||
<Text type="secondary" style={{fontSize: theme.fontSize.small}}>
|
||||
View detail
|
||||
</Text>
|
||||
}>
|
||||
@@ -149,14 +149,14 @@ function NotificationEntry({notification}: {notification: PluginNotification}) {
|
||||
<Layout.Right center>
|
||||
<Layout.Horizontal gap="tiny" center>
|
||||
{icon}
|
||||
<Text style={{fontSize: theme.fontSize.smallBody}}>{pluginName}</Text>
|
||||
<Text style={{fontSize: theme.fontSize.small}}>{pluginName}</Text>
|
||||
</Layout.Horizontal>
|
||||
{actions}
|
||||
</Layout.Right>
|
||||
<Title level={4} ellipsis={{rows: 2}}>
|
||||
{title}
|
||||
</Title>
|
||||
<Text type="secondary" style={{fontSize: theme.fontSize.smallBody}}>
|
||||
<Text type="secondary" style={{fontSize: theme.fontSize.small}}>
|
||||
{clientName && appName
|
||||
? `${clientName}/${appName}`
|
||||
: clientName ?? appName ?? 'Not Connected'}
|
||||
|
||||
@@ -56,6 +56,7 @@ test('Correct top level API exposed', () => {
|
||||
Array [
|
||||
"Atom",
|
||||
"DataTableColumn",
|
||||
"DataTableManager",
|
||||
"DefaultKeyboardAction",
|
||||
"Device",
|
||||
"DeviceLogEntry",
|
||||
|
||||
@@ -78,6 +78,7 @@ export {Idler} from './utils/Idler';
|
||||
export {createDataSource, DataSource} from './state/datasource/DataSource';
|
||||
|
||||
export {DataTable, DataTableColumn} from './ui/datatable/DataTable';
|
||||
export {DataTableManager} from './ui/datatable/useDataTableManager';
|
||||
|
||||
export {
|
||||
Interactive as _Interactive,
|
||||
|
||||
@@ -108,11 +108,8 @@ export class DataSource<
|
||||
output: Entry<T>[] = [];
|
||||
|
||||
/**
|
||||
* Returns a direct reference to the stored records.
|
||||
* The collection should be treated as readonly and mutable;
|
||||
* the collection might be directly written to by the datasource,
|
||||
* so for an immutable state create a defensive copy:
|
||||
* `datasource.records.slice()`
|
||||
* Returns a defensive copy of the stored records.
|
||||
* This is a O(n) operation! Prefer using .size and .get instead!
|
||||
*/
|
||||
get records(): readonly T[] {
|
||||
return this._records.map(unwrap);
|
||||
@@ -134,6 +131,18 @@ export class DataSource<
|
||||
this.setSortBy(undefined);
|
||||
}
|
||||
|
||||
public get size() {
|
||||
return this._records.length;
|
||||
}
|
||||
|
||||
public getRecord(index: number): T {
|
||||
return this._records[index]?.value;
|
||||
}
|
||||
|
||||
public get outputSize() {
|
||||
return this.output.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a defensive copy of the current output.
|
||||
* Sort, filter, reverse and window are applied, but windowing isn't.
|
||||
|
||||
@@ -203,11 +203,11 @@ const SandySplitContainer = styled.div<{
|
||||
alignItems: props.center ? 'center' : 'stretch',
|
||||
gap: normalizeSpace(props.gap, theme.space.small),
|
||||
overflow: props.center ? undefined : 'hidden', // only use overflow hidden in container mode, to avoid weird resizing issues
|
||||
'> :nth-child(1)': {
|
||||
'>:nth-of-type(1)': {
|
||||
flex: props.grow === 1 ? splitGrowStyle : splitFixedStyle,
|
||||
minWidth: props.grow === 1 ? 0 : undefined,
|
||||
},
|
||||
'> :nth-child(2)': {
|
||||
'>:nth-of-type(2)': {
|
||||
flex: props.grow === 2 ? splitGrowStyle : splitFixedStyle,
|
||||
minWidth: props.grow === 2 ? 0 : undefined,
|
||||
},
|
||||
|
||||
@@ -16,6 +16,7 @@ import React, {
|
||||
RefObject,
|
||||
MutableRefObject,
|
||||
CSSProperties,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
import {TableRow, DEFAULT_ROW_HEIGHT} from './TableRow';
|
||||
import {DataSource} from '../../state/datasource/DataSource';
|
||||
@@ -23,7 +24,7 @@ import {Layout} from '../Layout';
|
||||
import {TableHead} from './TableHead';
|
||||
import {Percentage} from '../../utils/widthUtils';
|
||||
import {DataSourceRenderer, DataSourceVirtualizer} from './DataSourceRenderer';
|
||||
import {useDataTableManager, TableManager} from './useDataTableManager';
|
||||
import {useDataTableManager, DataTableManager} from './useDataTableManager';
|
||||
import {TableSearch} from './TableSearch';
|
||||
import styled from '@emotion/styled';
|
||||
import {theme} from '../theme';
|
||||
@@ -40,7 +41,7 @@ interface DataTableProps<T = any> {
|
||||
onSelect?(record: T | undefined, records: T[]): void;
|
||||
onRowStyle?(record: T): CSSProperties | undefined;
|
||||
// multiselect?: true
|
||||
tableManagerRef?: RefObject<TableManager>;
|
||||
tableManagerRef?: RefObject<DataTableManager<T> | undefined>; // Actually we want a MutableRefObject, but that is not what React.createRef() returns, and we don't want to put the burden on the plugin dev to cast it...
|
||||
_testHeight?: number; // exposed for unit testing only
|
||||
}
|
||||
|
||||
@@ -92,7 +93,9 @@ export function DataTable<T extends object>(
|
||||
props.onSelect,
|
||||
);
|
||||
if (props.tableManagerRef) {
|
||||
(props.tableManagerRef as MutableRefObject<TableManager>).current = tableManager;
|
||||
(props.tableManagerRef as MutableRefObject<
|
||||
DataTableManager<T>
|
||||
>).current = tableManager;
|
||||
}
|
||||
const {
|
||||
visibleColumns,
|
||||
@@ -246,6 +249,17 @@ export function DataTable<T extends object>(
|
||||
return <EmptyTable dataSource={dataSource} />;
|
||||
}, []);
|
||||
|
||||
useEffect(
|
||||
function cleanup() {
|
||||
return () => {
|
||||
if (props.tableManagerRef) {
|
||||
(props.tableManagerRef as MutableRefObject<undefined>).current = undefined;
|
||||
}
|
||||
};
|
||||
},
|
||||
[props.tableManagerRef],
|
||||
);
|
||||
|
||||
return (
|
||||
<Layout.Container grow>
|
||||
<Layout.Top>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
import {CopyOutlined, FilterOutlined} from '@ant-design/icons';
|
||||
import {Checkbox, Menu} from 'antd';
|
||||
import {TableManager} from './useDataTableManager';
|
||||
import {DataTableManager} from './useDataTableManager';
|
||||
import React from 'react';
|
||||
import {normalizeCellValue} from './TableRow';
|
||||
import {tryGetFlipperLibImplementation} from '../../plugin/FlipperLib';
|
||||
@@ -17,7 +17,7 @@ import {DataTableColumn} from './DataTable';
|
||||
|
||||
const {Item, SubMenu} = Menu;
|
||||
|
||||
export function tableContextMenuFactory(tableManager: TableManager) {
|
||||
export function tableContextMenuFactory(tableManager: DataTableManager<any>) {
|
||||
const lib = tryGetFlipperLibImplementation();
|
||||
if (!lib) {
|
||||
return (
|
||||
|
||||
@@ -25,7 +25,6 @@ import {CaretDownFilled, CaretUpFilled} from '@ant-design/icons';
|
||||
import {Layout} from '../Layout';
|
||||
import {Sorting, OnColumnResize, SortDirection} from './useDataTableManager';
|
||||
import {ColumnFilterHandlers, FilterIcon} from './ColumnFilter';
|
||||
import {DEFAULT_ROW_HEIGHT} from './TableRow';
|
||||
|
||||
const {Text} = Typography;
|
||||
|
||||
@@ -41,27 +40,27 @@ function SortIcons({
|
||||
<CaretUpFilled
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSort(direction === 'up' ? undefined : 'up');
|
||||
onSort(direction === 'asc' ? undefined : 'asc');
|
||||
}}
|
||||
className={
|
||||
'ant-table-column-sorter-up ' + (direction === 'up' ? 'active' : '')
|
||||
'ant-table-column-sorter-up ' + (direction === 'asc' ? 'active' : '')
|
||||
}
|
||||
/>
|
||||
<CaretDownFilled
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSort(direction === 'down' ? undefined : 'down');
|
||||
onSort(direction === 'desc' ? undefined : 'desc');
|
||||
}}
|
||||
className={
|
||||
'ant-table-column-sorter-down ' +
|
||||
(direction === 'down' ? 'active' : '')
|
||||
(direction === 'desc' ? 'active' : '')
|
||||
}
|
||||
/>
|
||||
</SortIconsContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const SortIconsContainer = styled.span<{direction?: 'up' | 'down'}>(
|
||||
const SortIconsContainer = styled.span<{direction?: 'asc' | 'desc'}>(
|
||||
({direction}) => ({
|
||||
visibility: direction === undefined ? 'hidden' : undefined,
|
||||
display: 'inline-flex',
|
||||
@@ -127,7 +126,7 @@ function TableHeadColumn({
|
||||
...filterHandlers
|
||||
}: {
|
||||
column: DataTableColumn<any>;
|
||||
sorted: 'up' | 'down' | undefined;
|
||||
sorted: SortDirection;
|
||||
isResizable: boolean;
|
||||
onSort: (id: string, direction: SortDirection) => void;
|
||||
sortOrder: undefined | Sorting;
|
||||
@@ -169,7 +168,7 @@ function TableHeadColumn({
|
||||
e.stopPropagation();
|
||||
onSort(
|
||||
column.key,
|
||||
sorted === 'up' ? undefined : sorted === 'down' ? 'up' : 'down',
|
||||
sorted === 'asc' ? 'desc' : sorted === 'desc' ? undefined : 'asc',
|
||||
);
|
||||
}}
|
||||
role="button"
|
||||
|
||||
@@ -49,7 +49,6 @@ const TableBodyRowContainer = styled.div<TableBodyRowContainerProps>(
|
||||
? `4px solid ${theme.primaryColor}`
|
||||
: `4px solid transparent`,
|
||||
paddingTop: 1,
|
||||
borderBottom: `1px solid ${theme.dividerColor}`,
|
||||
minHeight: DEFAULT_ROW_HEIGHT,
|
||||
lineHeight: `${DEFAULT_ROW_HEIGHT - 2}px`,
|
||||
'& .anticon': {
|
||||
@@ -75,6 +74,7 @@ const TableBodyColumnContainer = styled.div<{
|
||||
flexGrow: props.width === undefined ? 1 : 0,
|
||||
overflow: 'hidden',
|
||||
padding: `0 ${theme.space.small}px`,
|
||||
borderBottom: `1px solid ${theme.dividerColor}`,
|
||||
verticalAlign: 'top',
|
||||
whiteSpace: props.multiline ? 'normal' : 'nowrap',
|
||||
wordWrap: props.multiline ? 'break-word' : 'normal',
|
||||
|
||||
@@ -11,7 +11,7 @@ import React, {createRef} from 'react';
|
||||
import {DataTable, DataTableColumn} from '../DataTable';
|
||||
import {render, act} from '@testing-library/react';
|
||||
import {createDataSource} from '../../../state/datasource/DataSource';
|
||||
import {computeDataTableFilter, TableManager} from '../useDataTableManager';
|
||||
import {computeDataTableFilter, DataTableManager} from '../useDataTableManager';
|
||||
import {Button} from 'antd';
|
||||
|
||||
type Todo = {
|
||||
@@ -41,7 +41,7 @@ const columns: DataTableColumn[] = [
|
||||
|
||||
test('update and append', async () => {
|
||||
const ds = createTestDataSource();
|
||||
const ref = createRef<TableManager>();
|
||||
const ref = createRef<DataTableManager<Todo>>();
|
||||
const rendering = render(
|
||||
<DataTable
|
||||
dataSource={ds}
|
||||
@@ -55,15 +55,15 @@ test('update and append', async () => {
|
||||
expect(elem.length).toBe(1);
|
||||
expect(elem[0].parentElement).toMatchInlineSnapshot(`
|
||||
<div
|
||||
class="css-8pa5c2-TableBodyRowContainer efe0za01"
|
||||
class="css-1b7miqb-TableBodyRowContainer efe0za01"
|
||||
>
|
||||
<div
|
||||
class="css-kkcfb6-TableBodyColumnContainer efe0za00"
|
||||
class="css-bqa56k-TableBodyColumnContainer efe0za00"
|
||||
>
|
||||
test DataTable
|
||||
</div>
|
||||
<div
|
||||
class="css-kkcfb6-TableBodyColumnContainer efe0za00"
|
||||
class="css-bqa56k-TableBodyColumnContainer efe0za00"
|
||||
>
|
||||
true
|
||||
</div>
|
||||
@@ -98,7 +98,7 @@ test('update and append', async () => {
|
||||
|
||||
test('column visibility', async () => {
|
||||
const ds = createTestDataSource();
|
||||
const ref = createRef<TableManager>();
|
||||
const ref = createRef<DataTableManager<Todo>>();
|
||||
const rendering = render(
|
||||
<DataTable
|
||||
dataSource={ds}
|
||||
@@ -112,15 +112,15 @@ test('column visibility', async () => {
|
||||
expect(elem.length).toBe(1);
|
||||
expect(elem[0].parentElement).toMatchInlineSnapshot(`
|
||||
<div
|
||||
class="css-8pa5c2-TableBodyRowContainer efe0za01"
|
||||
class="css-1b7miqb-TableBodyRowContainer efe0za01"
|
||||
>
|
||||
<div
|
||||
class="css-kkcfb6-TableBodyColumnContainer efe0za00"
|
||||
class="css-bqa56k-TableBodyColumnContainer efe0za00"
|
||||
>
|
||||
test DataTable
|
||||
</div>
|
||||
<div
|
||||
class="css-kkcfb6-TableBodyColumnContainer efe0za00"
|
||||
class="css-bqa56k-TableBodyColumnContainer efe0za00"
|
||||
>
|
||||
true
|
||||
</div>
|
||||
@@ -137,10 +137,10 @@ test('column visibility', async () => {
|
||||
expect(elem.length).toBe(1);
|
||||
expect(elem[0].parentElement).toMatchInlineSnapshot(`
|
||||
<div
|
||||
class="css-8pa5c2-TableBodyRowContainer efe0za01"
|
||||
class="css-1b7miqb-TableBodyRowContainer efe0za01"
|
||||
>
|
||||
<div
|
||||
class="css-kkcfb6-TableBodyColumnContainer efe0za00"
|
||||
class="css-bqa56k-TableBodyColumnContainer efe0za00"
|
||||
>
|
||||
test DataTable
|
||||
</div>
|
||||
@@ -174,7 +174,7 @@ test('sorting', async () => {
|
||||
title: 'item b',
|
||||
done: false,
|
||||
});
|
||||
const ref = createRef<TableManager>();
|
||||
const ref = createRef<DataTableManager<Todo>>();
|
||||
const rendering = render(
|
||||
<DataTable
|
||||
dataSource={ds}
|
||||
@@ -195,7 +195,7 @@ test('sorting', async () => {
|
||||
}
|
||||
// sort asc
|
||||
act(() => {
|
||||
ref.current?.sortColumn('title', 'down');
|
||||
ref.current?.sortColumn('title', 'asc');
|
||||
});
|
||||
{
|
||||
const elem = await rendering.findAllByText(/item/);
|
||||
@@ -208,7 +208,7 @@ test('sorting', async () => {
|
||||
}
|
||||
// sort desc
|
||||
act(() => {
|
||||
ref.current?.sortColumn('title', 'up');
|
||||
ref.current?.sortColumn('title', 'desc');
|
||||
});
|
||||
{
|
||||
const elem = await rendering.findAllByText(/item/);
|
||||
@@ -249,7 +249,7 @@ test('search', async () => {
|
||||
title: 'item b',
|
||||
done: false,
|
||||
});
|
||||
const ref = createRef<TableManager>();
|
||||
const ref = createRef<DataTableManager<Todo>>();
|
||||
const rendering = render(
|
||||
<DataTable
|
||||
dataSource={ds}
|
||||
@@ -514,7 +514,7 @@ test('compute filters', () => {
|
||||
test('onSelect callback fires, and in order', () => {
|
||||
const events: any[] = [];
|
||||
const ds = createTestDataSource();
|
||||
const ref = createRef<TableManager>();
|
||||
const ref = createRef<DataTableManager<Todo>>();
|
||||
const rendering = render(
|
||||
<DataTable
|
||||
dataSource={ds}
|
||||
@@ -566,7 +566,7 @@ test('onSelect callback fires, and in order', () => {
|
||||
test('selection always has the latest state', () => {
|
||||
const events: any[] = [];
|
||||
const ds = createTestDataSource();
|
||||
const ref = createRef<TableManager>();
|
||||
const ref = createRef<DataTableManager<Todo>>();
|
||||
const rendering = render(
|
||||
<DataTable
|
||||
dataSource={ds}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
import {DataTableColumn} from 'flipper-plugin/src/ui/datatable/DataTable';
|
||||
import {Percentage} from '../../utils/widthUtils';
|
||||
import produce from 'immer';
|
||||
import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||
import {useCallback, useEffect, useMemo, useState} from 'react';
|
||||
import {DataSource} from '../../state/datasource/DataSource';
|
||||
import {useMemoize} from '../../utils/useMemoize';
|
||||
|
||||
@@ -20,9 +20,45 @@ export type Sorting = {
|
||||
direction: Exclude<SortDirection, undefined>;
|
||||
};
|
||||
|
||||
export type SortDirection = 'up' | 'down' | undefined;
|
||||
export type SortDirection = 'asc' | 'desc' | undefined;
|
||||
|
||||
export type TableManager = ReturnType<typeof useDataTableManager>;
|
||||
export interface DataTableManager<T> {
|
||||
/** The default columns, but normalized */
|
||||
columns: DataTableColumn<T>[];
|
||||
/** The effective columns to be rendererd */
|
||||
visibleColumns: DataTableColumn<T>[];
|
||||
/** The currently applicable sorting, if any */
|
||||
sorting: Sorting | undefined;
|
||||
/** Reset the current table preferences, including column widths an visibility, back to the default */
|
||||
reset(): void;
|
||||
/** Resizes the column with the given key to the given width */
|
||||
resizeColumn(column: string, width: number | Percentage): void;
|
||||
/** Sort by the given column. This toggles statefully between ascending, descending, none (insertion order of the data source) */
|
||||
sortColumn(column: string, direction: SortDirection): void;
|
||||
/** Show / hide the given column */
|
||||
toggleColumnVisibility(column: string): void;
|
||||
/** Active search value */
|
||||
setSearchValue(value: string): void;
|
||||
/** current selection, describes the index index in the datasources's current output (not window) */
|
||||
selection: Selection;
|
||||
selectItem(
|
||||
nextIndex: number | ((currentIndex: number) => number),
|
||||
addToSelection?: boolean,
|
||||
): void;
|
||||
addRangeToSelection(
|
||||
start: number,
|
||||
end: number,
|
||||
allowUnselect?: boolean,
|
||||
): void;
|
||||
clearSelection(): void;
|
||||
getSelectedItem(): T | undefined;
|
||||
getSelectedItems(): readonly T[];
|
||||
/** Changing column filters */
|
||||
addColumnFilter(column: string, value: string, disableOthers?: boolean): void;
|
||||
removeColumnFilter(column: string, index: number): void;
|
||||
toggleColumnFilter(column: string, index: number): void;
|
||||
setColumnFilterFromSelection(column: string): void;
|
||||
}
|
||||
|
||||
type Selection = {items: ReadonlySet<number>; current: number};
|
||||
|
||||
@@ -38,15 +74,13 @@ export function useDataTableManager<T>(
|
||||
dataSource: DataSource<T>,
|
||||
defaultColumns: DataTableColumn<T>[],
|
||||
onSelect?: (item: T | undefined, items: T[]) => void,
|
||||
) {
|
||||
): DataTableManager<T> {
|
||||
const [columns, setEffectiveColumns] = useState(
|
||||
computeInitialColumns(defaultColumns),
|
||||
);
|
||||
// TODO: move selection with shifts with index < selection?
|
||||
// TODO: clear selection if out of range
|
||||
const [selection, setSelection] = useState<Selection>(emptySelection);
|
||||
const selectionRef = useRef(selection);
|
||||
selectionRef.current = selection; // store last seen selection for fetching it later
|
||||
|
||||
const [sorting, setSorting] = useState<Sorting | undefined>(undefined);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
@@ -86,21 +120,22 @@ export function useDataTableManager<T>(
|
||||
[],
|
||||
);
|
||||
|
||||
// N.B: we really want to have stable refs for these functions,
|
||||
// to avoid that all context menus need re-render for every selection change,
|
||||
// hence the selectionRef hack
|
||||
const clearSelection = useCallback(() => {
|
||||
setSelection(emptySelection);
|
||||
}, []);
|
||||
|
||||
const getSelectedItem = useCallback(() => {
|
||||
return selectionRef.current.current < 0
|
||||
return selection.current < 0
|
||||
? undefined
|
||||
: dataSource.getItem(selectionRef.current.current);
|
||||
}, [dataSource]);
|
||||
: dataSource.getItem(selection.current);
|
||||
}, [dataSource, selection]);
|
||||
|
||||
const getSelectedItems = useCallback(() => {
|
||||
return [...selectionRef.current.items]
|
||||
return [...selection.items]
|
||||
.sort()
|
||||
.map((i) => dataSource.getItem(i))
|
||||
.filter(Boolean) as any[];
|
||||
}, [dataSource]);
|
||||
}, [dataSource, selection]);
|
||||
|
||||
useEffect(
|
||||
function fireSelection() {
|
||||
@@ -221,7 +256,7 @@ export function useDataTableManager<T>(
|
||||
dataSource.setSortBy(key as any);
|
||||
}
|
||||
if (!sorting || sorting.direction !== direction) {
|
||||
dataSource.setReversed(direction === 'up');
|
||||
dataSource.setReversed(direction === 'desc');
|
||||
}
|
||||
setSorting({key, direction});
|
||||
}
|
||||
@@ -271,6 +306,7 @@ export function useDataTableManager<T>(
|
||||
selection,
|
||||
selectItem,
|
||||
addRangeToSelection,
|
||||
clearSelection,
|
||||
getSelectedItem,
|
||||
getSelectedItems,
|
||||
/** Changing column filters */
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
*/
|
||||
|
||||
// Exposes all the variables defined in themes/base.less:
|
||||
|
||||
export const theme = {
|
||||
white: 'white', // use as counter color for primary
|
||||
black: 'black',
|
||||
@@ -38,7 +37,12 @@ export const theme = {
|
||||
huge: 24,
|
||||
} as const,
|
||||
fontSize: {
|
||||
smallBody: '12px',
|
||||
default: '14px',
|
||||
small: '12px',
|
||||
} as const,
|
||||
monospace: {
|
||||
fontFamily: 'SF Mono,Monaco,Andale Mono,monospace',
|
||||
fontSize: '12px',
|
||||
} as const,
|
||||
} as const;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user