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
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ describe('ChronoChart', () => {
});

test('dialog open and close', async () => {
const { getAllByRole, user } = mount(
const { getAllByRole, getByRole, queryAllByRole, user } = mount(
<UnloadProtectsContext.Provider value={[]}>
<ChronoChart />
</UnloadProtectsContext.Provider>
Expand All @@ -21,14 +21,16 @@ describe('ChronoChart', () => {

await user.click(button);

const dialog = getAllByRole('dialog')[0];

expect(dialog).toMatchSnapshot();
expect(
getByRole('heading', { name: 'Chronostratigraphic Chart' })
).toBeInTheDocument();
expect(getByRole('img', { name: 'Chrono Chart' })).toBeInTheDocument();
expect(getAllByRole('dialog')).toHaveLength(1);

const closeButton = getAllByRole('button')[4];

await user.click(closeButton);

expect(() => getAllByRole('dialog')).toThrow();
expect(queryAllByRole('dialog')).toHaveLength(0);
});
});
Original file line number Diff line number Diff line change
@@ -1,158 +1,5 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing

exports[`ChronoChart dialog open and close 1`] = `
<div
aria-describedby="modal-0-content"
aria-labelledby="modal-0-header"
aria-modal="true"
class="ReactModal__Content
flex flex-col gap-2 p-4 outline-none rounded resize max-w-[90%] shadow-lg shadow-gray-500 max-h-screen min-w-[min(40rem,90%)]
overflow-x-hidden text-neutral-900 duration-0
dark:border dark:border-neutral-700 dark:text-neutral-200


bg-gradient-to-bl from-gray-200 via-white
to-white dark:from-neutral-800 dark:via-neutral-900 dark:to-neutral-900

"
role="dialog"
style="transform: translate(0px,0px);"
tabindex="-1"
>
<div
class="
flex items-center gap-2 md:gap-4
-m-4 cursor-move p-4
flex-wrap
"
id="modal-0-handle"
>
<div
class="flex items-center gap-2"
>
<h2
class="font-semibold text-black dark:text-white text-xl"
id="modal-0-header"
>
Chronostratigraphic Chart
</h2>
</div>
</div>
<div
class="
dark:text-neutral-350 -mx-1 flex-1 overflow-y-auto px-1 py-4
text-gray-700 flex flex-col gap-2
"
id="modal-0-content"
>
<div
class="flex flex-col items-center justify-center h-full w-full"
style="--transition-duration: 0;"
>
<div
class="react-transform-wrapper transform-component-module_wrapper__SPB86 "
>
<div
class="react-transform-component transform-component-module_content__FBWxo "
style="transform: translate(0px, 0px) scale(1);"
>
<img
alt="Chrono Chart"
class="max-w-full max-h-[70vh] object-contain"
src="/static/img/chronostratChart2023-09.jpg"
/>
</div>
</div>
<div
class="flex gap-2 mt-4"
>
<button
aria-label="Zoom In"
class="icon link rounded"
title="Zoom In"
type="button"
>
<svg
aria-hidden="true"
class="w-6 h-6 flex-shrink-0"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5 8a1 1 0 011-1h1V6a1 1 0 012 0v1h1a1 1 0 110 2H9v1a1 1 0 11-2 0V9H6a1 1 0 01-1-1z"
/>
<path
clip-rule="evenodd"
d="M2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8zm6-4a4 4 0 100 8 4 4 0 000-8z"
fill-rule="evenodd"
/>
</svg>
</button>
<button
aria-label="Zoom Out"
class="icon link rounded"
title="Zoom Out"
type="button"
>
<svg
aria-hidden="true"
class="w-6 h-6 flex-shrink-0"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
fill-rule="evenodd"
/>
<path
clip-rule="evenodd"
d="M5 8a1 1 0 011-1h4a1 1 0 110 2H6a1 1 0 01-1-1z"
fill-rule="evenodd"
/>
</svg>
</button>
<button
aria-label="Reset"
class="icon link rounded"
title="Reset"
type="button"
>
<svg
aria-hidden="true"
class="w-6 h-6 flex-shrink-0"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z"
fill-rule="evenodd"
/>
</svg>
</button>
</div>
</div>
</div>
<div
class="flex gap-2 justify-end"
>
<button
class="button rounded cursor-pointer active:brightness-80 px-4 py-2
disabled:bg-gray-200 disabled:dark:ring-neutral-500 disabled:ring-gray-400 disabled:text-gray-500
dark:disabled:!bg-neutral-700 gap-2 inline-flex items-center capitalize justify-center shadow-sm button hover:brightness-90 dark:hover:brightness-125 bg-[color:var(--secondary-button-color)] text-gray-800
dark:text-gray-100"
type="button"
>
Close
</button>
</div>
</div>
`;

exports[`ChronoChart simple render 1`] = `
<DocumentFragment>
<button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ export function AutoComplete<T>({
inputProps = {},
value: currentValue,
pendingValueRef,
onScrollEnd,
isLoadingMore = false,
extraItems,
}: {
readonly source:
| RA<AutoCompleteItem<T>>
Expand Down Expand Up @@ -115,6 +118,9 @@ export function AutoComplete<T>({
* typing
*/
readonly pendingValueRef?: React.MutableRefObject<string>;
readonly onScrollEnd?: () => void;
readonly isLoadingMore?: boolean;
readonly extraItems?: RA<AutoCompleteItem<T>>;
}): JSX.Element {
const [results, setResults] = React.useState<
RA<AutoCompleteItem<T>> | undefined
Expand Down Expand Up @@ -237,8 +243,17 @@ export function AutoComplete<T>({
* only one element that starts with the current value (the current element),
* thus the filtered list of items has only one item.
*/
// Append paginated extra items to the base results
const allResults = React.useMemo(
() =>
extraItems !== undefined && extraItems.length > 0
? [...(results ?? []), ...extraItems]
: results,
[results, extraItems]
);

const ignoreFilter = currentValue === pendingValue;
const itemSource = ignoreFilter ? (results ?? []) : filteredItems;
const itemSource = ignoreFilter ? (allResults ?? []) : filteredItems;

const pendingItem = results?.find(
({ label, searchValue }) => (searchValue ?? label) === pendingValue
Expand Down Expand Up @@ -431,6 +446,16 @@ export function AutoComplete<T>({
shadow-gray-400 dark:border dark:border-gray-500 dark:bg-neutral-900
`}
ref={dataListRefCallback}
onScroll={
typeof onScrollEnd === 'function'
? (event: React.UIEvent<HTMLUListElement>): void => {
const target = event.currentTarget;
const nearBottom =
target.scrollHeight - target.scrollTop - target.clientHeight < 40;
if (nearBottom) onScrollEnd();
}
: undefined
}
>
{isLoading && (
<Combobox.Option
Expand Down Expand Up @@ -528,7 +553,12 @@ export function AutoComplete<T>({
)}
</Combobox.Option>
)}
{!listHasItems && (
{isLoadingMore && (
<li className={`${optionClassName(false, false)} cursor-auto text-gray-500`}>
{commonText.loading()}
</li>
)}
{!listHasItems && !isLoadingMore && (
<div className={`${optionClassName(false, false)} cursor-auto`}>
{formsText.nothingFound()}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1189,7 +1189,7 @@ export const userPreferenceDefinitions = {
title: preferencesText.treeSearchAlgorithm(),
requiresReload: false,
visible: true,
defaultValue: 'contains',
defaultValue: 'startsWith',
values: [
{
value: 'startsWith',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* Tests for tree search efficiency improvements (#7752).
*
* Tree tables (Taxon, Geography, Storage, etc.) can have 200K+ rows.
* Using LIKE '%pattern%' causes full table scans; LIKE 'pattern%' can use
* B-tree indexes. The typeahead also doesn't need 1000 results — 50 is
* more than enough for a dropdown.
*/
import { requireContext } from '../../../tests/helpers';
import { serializeResource } from '../../DataModel/serializers';
import { tables } from '../../DataModel/tables';
import { queryFieldFilterSpecs } from '../../QueryBuilder/FieldFilterSpec';
import { localized } from '../../../utils/types';
import { QUERY_COMBO_BOX_PAGE_SIZE } from '../constants';
import { makeComboBoxQuery } from '../helpers';

requireContext();

describe('Tree search efficiency', () => {
test('makeComboBoxQuery for tree tables uses startsWith by default', () => {
const query = makeComboBoxQuery({
fieldName: 'fullName',
value: 'rosa',
isTreeTable: true,
typeSearch: {
table: tables.Taxon,
searchFields: [[tables.Taxon.strictGetLiteralField('fullName')]],
name: localized('Taxon'),
title: localized('Taxon'),
formatter: localized(''),
displayFields: undefined,
format: localized('%s'),
},
specialConditions: [],
});

const serialized = serializeResource(query);
const searchField = serialized.fields.find(
(field: { readonly isDisplay: boolean }) => !field.isDisplay
);

// startsWith operator (id 15), NOT like (id 0)
expect(searchField?.operStart).toBe(queryFieldFilterSpecs.startsWith.id);
// Value should NOT be wrapped in % wildcards
expect(searchField?.startValue).toBe('rosa');
});

test('makeComboBoxQuery for non-tree tables uses startsWith by default', () => {
const query = makeComboBoxQuery({
fieldName: 'lastName',
value: 'smith',
isTreeTable: false,
typeSearch: {
table: tables.Agent,
searchFields: [[tables.Agent.strictGetLiteralField('lastName')]],
name: localized('Agent'),
title: localized('Agent'),
formatter: localized(''),
displayFields: undefined,
format: localized('%s'),
},
specialConditions: [],
});

const serialized = serializeResource(query);
const searchField = serialized.fields.find(
(field: { readonly isDisplay: boolean }) => !field.isDisplay
);

// Non-tree tables also default to startsWith
expect(searchField?.operStart).toBe(queryFieldFilterSpecs.startsWith.id);
expect(searchField?.startValue).toBe('smith');
});

test('search limit is at most 50', () => {
// The exported constant from index.tsx controls runQuery limit.
// If someone bumps it back to 1000, this test catches it.
expect(QUERY_COMBO_BOX_PAGE_SIZE).toBeLessThanOrEqual(50);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Results are fetched in pages of this size and loaded on scroll
export const QUERY_COMBO_BOX_PAGE_SIZE = 50;
Loading
Loading