Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 6 additions & 6 deletions packages/components/package-lock.json

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

4 changes: 2 additions & 2 deletions packages/components/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@labkey/components",
"version": "7.62.3",
"version": "7.62.4-fb-limitMaxCount.9",
"description": "Components, models, actions, and utility functions for LabKey applications and pages",
"sideEffects": false,
"files": [
Expand Down Expand Up @@ -48,7 +48,7 @@
"homepage": "https://github.com/LabKey/labkey-ui-components#readme",
"dependencies": {
"@hello-pangea/dnd": "18.0.1",
"@labkey/api": "1.52.4",
"@labkey/api": "1.52.5-fb-limitMaxCount.1",
"@testing-library/dom": "~10.4.1",
"@testing-library/jest-dom": "~7.0.1",
"@testing-library/react": "~16.3.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,10 @@ export const ExportModal: FC<ExportModalProperties> = memo(props => {
{model.title}
</CheckboxLK>
</td>
<td className="pull-right">{rowCountDisplay}</td>
<td className="pull-right">
{rowCountDisplay}
{model.rowCountCapped ? '+' : ''}
</td>
<td className="view-name">
{!model.viewName || model.viewName.startsWith('~~') ? 'Default' : model.viewName}{' '}
{model.currentView?.session && <span className="text-muted">(edited)</span>}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,45 @@ describe('PageMenu', () => {
expectPageMenuItems(false, false, true, '34', '34 Total Pages');
});

test('capped rowCount keeps "Last Page" enabled and drops the total-pages footer', () => {
render(<PageMenu {...props} rowCountCapped isLastPage />);
expect(screen.getByText('First Page')).toBeInTheDocument();
// Last Page stays enabled while capped (even with isLastPage true) so there's always a way to reach the last row
const last = screen.getByText('Last Page').parentElement;
expect(last).not.toHaveClass('disabled');
// no fake "Page N" jump, and the total-pages footer is hidden because the true total is unknown
expect(screen.queryByText('Page 34')).not.toBeInTheDocument();
expect(screen.queryByText('34 Total Pages')).not.toBeInTheDocument();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the UI, I see a "Count All Rows" option. Should this check for that to exist?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, the test case below checks that.

});

test('capped rowCount shows "Count All Rows" only when a handler is provided', () => {
const { rerender } = render(<PageMenu {...props} rowCountCapped />);
expect(screen.queryByText('Count All Rows')).not.toBeInTheDocument();

const onShowTotalRowCount = jest.fn();
rerender(<PageMenu {...props} rowCountCapped onShowTotalRowCount={onShowTotalRowCount} />);
expect(screen.getByText('Count All Rows')).toBeInTheDocument();

// not shown when the count is exact
rerender(<PageMenu {...props} onShowTotalRowCount={onShowTotalRowCount} />);
expect(screen.queryByText('Count All Rows')).not.toBeInTheDocument();
});

test('Count All Rows shows a spinner and is disabled while counting', () => {
render(<PageMenu {...props} rowCountCapped loadingTotalCount onShowTotalRowCount={jest.fn()} />);
// the spinner replaces the label, and the item is present but disabled
expect(screen.queryByText('Count All Rows')).not.toBeInTheDocument();
expect(screen.getByText('Counting…')).toBeInTheDocument();
expect(screen.getByText('Counting…').closest('li')).toHaveClass('disabled');
});

test('Count All Rows invokes the handler when clicked', async () => {
const onShowTotalRowCount = jest.fn();
render(<PageMenu {...props} rowCountCapped onShowTotalRowCount={onShowTotalRowCount} />);
await userEvent.click(screen.getByText('Count All Rows'));
expect(onShowTotalRowCount).toHaveBeenCalled();
});

test('interactions', async () => {
render(<PageMenu {...props} />);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { createPortal } from 'react-dom';
import { DropdownButton, MenuDivider, MenuHeader, MenuItem } from '../../dropdowns';
import { useOverlayTriggerState } from '../../OverlayTrigger';
import { Tooltip } from '../../Tooltip';
import { LoadingSpinner } from '../base/LoadingSpinner';

interface Props {
currentPage: number;
Expand All @@ -17,9 +18,12 @@ interface Props {
isLastPage: boolean;
loadFirstPage: () => void;
loadLastPage: () => void;
loadingTotalCount?: boolean;
onShowTotalRowCount?: () => void;
pageCount: number;
pageSize: number;
pageSizes: number[];
rowCountCapped?: boolean;
setPageSize: (size: number) => void;
}

Expand All @@ -31,9 +35,12 @@ export const PageMenu: FC<Props> = props => {
isLastPage,
loadFirstPage,
loadLastPage,
loadingTotalCount,
onShowTotalRowCount,
pageCount,
pageSize,
pageSizes,
rowCountCapped,
setPageSize,
} = props;
const totalPagesText = disabled ? '...' : `${pageCount.toLocaleString()} Total Pages`;
Expand Down Expand Up @@ -68,7 +75,12 @@ export const PageMenu: FC<Props> = props => {
<MenuItem disabled={disabled || isLastPage} onClick={loadLastPage}>
Last Page
</MenuItem>
<MenuHeader className="submenu-footer" text={totalPagesText} />
{rowCountCapped && onShowTotalRowCount && (
<MenuItem disabled={disabled || loadingTotalCount} onClick={onShowTotalRowCount}>
{loadingTotalCount ? <LoadingSpinner msg="Loading..." /> : 'Count All Rows'}
</MenuItem>
)}
{!rowCountCapped && <MenuHeader className="submenu-footer" text={totalPagesText} />}
<MenuDivider />
<MenuHeader text="Page Size" />
{pageSizes?.map(size => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/
import React, { FC, memo, useCallback } from 'react';

import { LoadingState } from '../../../public/LoadingState';
import { isLoading, LoadingState } from '../../../public/LoadingState';

import { incrementClientSideMetricCount } from '../../actions';

Expand All @@ -21,6 +21,7 @@ export interface PaginationData {
pageCount: number;
pageSize: number;
rowCount: number;
rowCountCapped?: boolean;
totalCountLoadingState?: LoadingState;
}

Expand All @@ -32,6 +33,7 @@ export interface PaginationProps extends PaginationData {
// pageSizes is expected to be sorted (ascending)
pageSizes?: number[];
setPageSize: (pageSize) => void;
showTotalRowCount?: () => void;
}

const PAGINATION_METRIC_AREA = 'pagination';
Expand All @@ -52,7 +54,9 @@ export const Pagination: FC<PaginationProps> = memo(props => {
pageSize,
pageSizes = DEFAULT_PAGE_SIZES,
rowCount,
rowCountCapped,
setPageSize,
showTotalRowCount,
totalCountLoadingState,
} = props;
const hasPages = rowCount > pageSizes[0];
Expand Down Expand Up @@ -90,13 +94,19 @@ export const Pagination: FC<PaginationProps> = memo(props => {
[setPageSize]
);

const onShowTotalRowCount = useCallback(() => {
incrementClientSideMetricCount(PAGINATION_METRIC_AREA, 'showTotalRowCount');
showTotalRowCount?.();
}, [showTotalRowCount]);

// Use lk-pagination so we don't conflict with bootstrap pagination class.
return (
<div className="lk-pagination">
<PaginationInfo
offset={offset}
pageSize={pageSize}
rowCount={rowCount}
rowCountCapped={rowCountCapped}
totalCountLoadingState={totalCountLoadingState}
/>

Expand All @@ -116,8 +126,11 @@ export const Pagination: FC<PaginationProps> = memo(props => {
isFirstPage={isFirstPage}
isLastPage={isLastPage}
pageCount={pageCount}
rowCountCapped={rowCountCapped}
loadFirstPage={onLoadFirstPage}
loadLastPage={onLoadLastPage}
loadingTotalCount={isLoading(totalCountLoadingState)}
onShowTotalRowCount={showTotalRowCount ? onShowTotalRowCount : undefined}
pageSize={pageSize}
pageSizes={pageSizes}
setPageSize={onSetPageSize}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,18 @@ describe('PaginationInfo', () => {
expect(document.querySelector('.pagination-info')).toBeInTheDocument();
expect(document.querySelector('.pagination-info').textContent).toBe('1 - 3,412 of 22,341');
});

test('capped rowCount shows a "+" suffix', () => {
render(<PaginationInfo {...getDefaultProps()} rowCount={100000} rowCountCapped />);
expect(document.querySelector('.pagination-info').textContent).toBe('1 - 20 of 100,000+');
});

test('capped rowCount shows total even on the boundary page where rowCount === max', () => {
// rowCount (20) === max (20): without the cap the total is hidden, with the cap it shows "20+"
const { container, rerender } = render(<PaginationInfo {...getDefaultProps()} rowCount={20} />);
expect(container.querySelector('.pagination-info').textContent).toBe('1 - 20');

rerender(<PaginationInfo {...getDefaultProps()} rowCount={20} rowCountCapped />);
expect(container.querySelector('.pagination-info').textContent).toBe('1 - 20 of 20+');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,26 @@ export interface PaginationInfoProps {
offset: number;
pageSize: number;
rowCount: number;
rowCountCapped?: boolean;
totalCountLoadingState?: LoadingState;
}
export const PaginationInfo: FC<PaginationInfoProps> = memo(props => {
const { offset, pageSize, rowCount, totalCountLoadingState } = props;
const { offset, pageSize, rowCount, rowCountCapped, totalCountLoadingState } = props;
const loading = isLoading(totalCountLoadingState);
const outOfBounds = rowCount <= offset;
const min = offset !== rowCount ? offset + 1 : offset;
const max = offset + pageSize;
const text = outOfBounds ? '' : `${min.toLocaleString()} - `;
const showRowCount = !loading && !outOfBounds;
const showTotalCount = !loading && rowCount > max;
// When capped, rowCount equals the cap, so show it with a "+" even on the boundary page where rowCount === max.
const showTotalCount = !loading && !outOfBounds && (rowCount > max || rowCountCapped);

return (
<span className="pagination-info" data-min={min} data-max={max} data-total={rowCount}>
{text}
{loading && <LoadingSpinner msg="" />}
{showRowCount && <span>{max > rowCount ? rowCount.toLocaleString() : max.toLocaleString()}</span>}
{showTotalCount && <span>{` of ${rowCount.toLocaleString()}`}</span>}
{showTotalCount && <span>{` of ${rowCount.toLocaleString()}${rowCountCapped ? '+' : ''}`}</span>}
</span>
);
});
Expand Down
3 changes: 3 additions & 0 deletions packages/components/src/internal/query/selectRows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export interface SelectRowsResponse {
metaData: Query.ResponseMetadata | undefined;
queryInfo: QueryInfo;
rowCount: number;
/** True when rowCount was capped at maxCount rather than counted exactly. */
rowCountCapped?: boolean;
rows: Row[];
schemaQuery: SchemaQuery;
}
Expand Down Expand Up @@ -99,6 +101,7 @@ export async function selectRows(options: SelectRowsOptions): Promise<SelectRows
queryInfo,
rows: resolved.rows,
rowCount: resolved.rowCount,
rowCountCapped: response.rowCountCapped,
schemaQuery,
};
}
Expand Down
6 changes: 6 additions & 0 deletions packages/components/src/public/QueryModel/GridPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,11 @@ class ButtonBar<T> extends PureComponent<GridBarProps<T>> {
actions.setMaxRows(model.id, pageSize);
};

showTotalRowCount = (): void => {
const { model, actions } = this.props;
actions.loadTotalCount(model.id, true, true);
};

render(): ReactNode {
const {
searchActionValues,
Expand Down Expand Up @@ -204,6 +209,7 @@ class ButtonBar<T> extends PureComponent<GridBarProps<T>> {
loadPreviousPage={this.loadPreviousPage}
pageSizes={pageSizes}
setPageSize={this.setPageSize}
showTotalRowCount={this.showTotalRowCount}
/>
);

Expand Down
47 changes: 47 additions & 0 deletions packages/components/src/public/QueryModel/QueryModel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { getQueryParams } from '../../internal/util/URL';

import {
createQueryModelId,
DEFAULT_MAX_COUNT,
DEFAULT_MAX_ROWS,
DEFAULT_OFFSET,
flattenValuesFromRow,
Expand Down Expand Up @@ -117,6 +118,52 @@ describe('QueryModel', () => {
expect(model.isLastPage).toEqual(true);
});

test('maxCount', () => {
// defaults to the cap, and rowCountCapped starts false
let model = new QueryModel({ schemaQuery: SCHEMA_QUERY });
expect(model.maxCount).toEqual(DEFAULT_MAX_COUNT);
expect(model.rowCountCapped).toEqual(false);

// explicit config values pass through, including 0 to request an exact count
expect(new QueryModel({ schemaQuery: SCHEMA_QUERY, maxCount: 500 }).maxCount).toEqual(500);
expect(new QueryModel({ schemaQuery: SCHEMA_QUERY, maxCount: 0 }).maxCount).toEqual(0);
});

test('isLastPage with capped rowCount', () => {
const model = new QueryModel({ schemaQuery: SCHEMA_QUERY }).mutate({
maxRows: 20,
offset: 660,
rowCount: 661,
rows: {},
});
// on the true last page normally
expect(model.isLastPage).toEqual(true);
// a capped rowCount is only a floor, so paging forward stays available
expect(model.mutate({ rowCountCapped: true }).isLastPage).toEqual(false);
});

test('selectedState with capped rowCount', () => {
// selectedOnPage (2) === rowCount (2) but not every visible row is selected
const model = new QueryModel({ schemaQuery: SCHEMA_QUERY }).mutate({
rows: { '1': { test: 1 }, '2': { test: 2 }, '3': { test: 3 } },
orderedRows: ['1', '2', '3'],
selections: new Set(['1', '2']),
rowCount: 2,
maxRows: 20,
queryInfoLoadingState: LoadingState.LOADED,
rowsLoadingState: LoadingState.LOADED,
});
// an exact rowCount matching the selection count reads as ALL
expect(model.selectedState).toBe(GRID_CHECKBOX_OPTIONS.ALL);
// a capped rowCount is only a floor, so it can't establish ALL: 2 of 3 visible selected is SOME
expect(model.mutate({ rowCountCapped: true }).selectedState).toBe(GRID_CHECKBOX_OPTIONS.SOME);
});

test('paginationData includes rowCountCapped', () => {
const model = new QueryModel({ schemaQuery: SCHEMA_QUERY }).mutate({ rowCountCapped: true });
expect(model.paginationData.rowCountCapped).toEqual(true);
});

test('Data', () => {
const model = new QueryModel({ schemaQuery: SCHEMA_QUERY }).mutate({
orderedRows: ORDERED_ROWS,
Expand Down
Loading