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

Create paginator component #3195

Merged
merged 25 commits into from
May 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c703170
Create paginator component
CarolineDenis Mar 15, 2023
44ac47a
Move Paginator component to molecule and delete uncessary libraries
CarolineDenis Mar 16, 2023
928be8b
Simplify usage of usePaginator()
maxpatiiuk Mar 17, 2023
08bb7ba
Sort Record Sets on the back-end
maxpatiiuk Mar 17, 2023
5cfa38e
Fix sort config causing infinite fetch loop
maxpatiiuk Mar 17, 2023
1ec1f25
Fix wrong sizing for the paginator <select>
maxpatiiuk Mar 17, 2023
bf23b48
Remember user's page size preference
maxpatiiuk Mar 17, 2023
88137e7
Implement pagination in Query overlay and refactor it
CarolineDenis Mar 20, 2023
05a0c4a
Delete duplicate file
maxpatiiuk Apr 19, 2023
93babd6
Fix wrong typing for fetchRows
maxpatiiuk Apr 19, 2023
0360179
Merge remote-tracking branch 'origin/xml-editor' into issue-1398
maxpatiiuk Apr 19, 2023
3f73d6c
Lint code with ESLint and Prettier
maxpatiiuk Apr 19, 2023
d0f8a15
Fix dialog size when navigating between pages in RecordSets overlay
CarolineDenis Apr 20, 2023
62147ef
Fix the size of dialog when changing page in Query Overlay
CarolineDenis Apr 20, 2023
0d08ab3
Remove unnecessary import
CarolineDenis Apr 20, 2023
edb9802
Remove full-height from ResourceView Dialog
CarolineDenis Apr 20, 2023
2032722
Simplify code
CarolineDenis Apr 21, 2023
e9aed7f
Fix failing test
maxpatiiuk Apr 21, 2023
17d5982
Provide simple mock for resize observer
maxpatiiuk Apr 21, 2023
0e47c2b
Work arround DevTools UX issue
maxpatiiuk Apr 27, 2023
73cc56a
Always apply padding right to numeric fields
maxpatiiuk Apr 27, 2023
b397f5a
Fix slider positioning in Interactions Dialog
maxpatiiuk Apr 27, 2023
4763ca4
Lint code with ESLint and Prettier
maxpatiiuk Apr 27, 2023
c428f14
Merge branch 'xml-editor' into issue-1398
CarolineDenis May 3, 2023
949a08a
Fix failing tests
CarolineDenis May 3, 2023
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: 11 additions & 1 deletion specifyweb/frontend/js_src/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@
}

[type='number']:not([readonly], .no-arrows) {
@apply relative;
@apply relative pr-4;
}

.button:is([aria-pressed='true'], [aria-selected='true'], [aria-current]):not(.aria-handled) {
Expand Down Expand Up @@ -309,6 +309,16 @@
}
}

.\! {
/**
* In Chrome DevTools, in the "Element Classes" box (where you customize
* element class names), DevTools apply the first matching class name even
* before you started typing. The first matching happens to be !absolute,
* which breaks the UI. This noop is a workaround for that UX issue
*/
--dev-tools-hack: 0;
}

/* Leaflet dark mode */
.leaflet-bar a {
@apply dark:!border-b-neutral-600 dark:!bg-black dark:!text-white
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ export const fetchRows = async <
TABLE_NAME extends keyof Tables,
SCHEMA extends Tables[TABLE_NAME],
FIELDS extends RR<
string | keyof SCHEMA['fields'],
string | Exclude<keyof SCHEMA['fields'], 'fields'>,
RA<'boolean' | 'null' | 'number' | 'string'>
>
>(
Expand All @@ -169,7 +169,7 @@ export const fetchRows = async <
fields,
distinct = false,
...filters
}: CollectionFetchFilters<SCHEMA> & {
}: Omit<CollectionFetchFilters<SCHEMA>, 'fields'> & {
readonly fields: FIELDS;
readonly distinct?: boolean;
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export function ErrorDialog({
>
{commonText.downloadErrorMessage()}
</Button.Blue>
<span className="-ml-2 flex-1" />
<span className="-mt-2 flex-1" />
<Label.Inline>
<Input.Checkbox
checked={clearCacheOnException}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ export const produceStackTrace = (message: unknown): string =>
pageHtml: document.documentElement.outerHTML,
localStorage: { ...localStorage },
// Network log and page load telemetry
eventLog: globalThis.performance.getEntries(),
eventLog:
process.env.NODE_ENV === 'test'
? []
: globalThis.performance.getEntries(),
navigator: {
userAgent: navigator.userAgent,
language: navigator.language,
Expand Down
10 changes: 4 additions & 6 deletions specifyweb/frontend/js_src/lib/components/FormEditor/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';

import { useLiveState } from '../../hooks/useLiveState';
import {f} from '../../utils/functools';
import { f } from '../../utils/functools';
import type { RA } from '../../utils/types';
import { defined } from '../../utils/types';
import type { AppResourceTabProps } from '../AppResources/TabDefinitions';
Expand Down Expand Up @@ -40,7 +40,7 @@ export function FormEditorWrapper(): JSX.Element {
parsed: [initialParsed],
syncer: { deserializer },
onChange: handleChange,
onSetCleanup:handleSetCleanup
onSetCleanup: handleSetCleanup,
} = React.useContext(FormEditorContext)!;

const originalParsed = React.useRef<ViewSets | undefined>(undefined);
Expand All @@ -61,10 +61,8 @@ export function FormEditorWrapper(): JSX.Element {
(parsed, changedViewNames): void => {
setChanged((changed) => new Set([...changed, ...changedViewNames]));
setParsed(parsed);
handleChange(() =>
updateXml(deserializer(parsed))
);
handleSetCleanup(async ()=>
handleChange(() => updateXml(deserializer(parsed)));
handleSetCleanup(async () =>
Promise.all(
Array.from(changed, async (viewName) =>
clearUrlCache(getViewSetApiUrl(viewName))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ function Field({
parser.type === 'number' &&
rightAlignNumberFields &&
globalThis.navigator.userAgent.toLowerCase().includes('webkit')
? `text-right ${isReadOnly ? '' : 'pr-6'}`
? 'text-right'
: ''
}
`}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,8 +276,6 @@ export function ResourceView<SCHEMA extends AnySchema>({
* navigation buttons don't jump around a lot as you navigate between
* records
*/
const isFullHeight =
dialog === 'modal' && typeof headerButtons === 'function' && !isSubForm;

return (
<Dialog
Expand All @@ -300,9 +298,7 @@ export function ResourceView<SCHEMA extends AnySchema>({
)
}
className={{
container: `${dialogClassNames.normalContainer} ${
isFullHeight ? 'h-full' : ''
}`,
container: dialogClassNames.normalContainer,
content: `${className.formStyles} ${dialogClassNames.flexContent}`,
}}
dimensionsKey={viewName ?? resource?.specifyTable.view}
Expand Down
4 changes: 3 additions & 1 deletion specifyweb/frontend/js_src/lib/components/Forms/Save.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ export function SaveButton<SCHEMA extends AnySchema = AnySchema>({
*/
readonly saveRequired?: boolean;
// Returning false would cancel the save proces (allowing to trigger custom behaviour)
readonly onSaving?: (unsetUnloadProtect: () => void) => false | undefined;
readonly onSaving?: (
unsetUnloadProtect: () => void
) => false | undefined | void;
readonly onSaved?: () => void;
readonly onAdd?: (newResource: SpecifyResource<SCHEMA>) => void;
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,95 +246,95 @@ export function InteractionDialog({
}
onClose={handleClose}
>
<div className="flex flex-col gap-8">
<details>
<details>
<summary>
{interactionsText.byEnteringNumbers({
fieldName: searchField.label,
})}
</summary>
<div className="flex flex-col gap-2">
<AutoGrowTextArea
forwardRef={validationRef}
spellCheck={false}
value={catalogNumbers}
onBlur={(): void => {
const parseResults = split(catalogNumbers).map((value) =>
parseValue(parser, inputRef.current ?? undefined, value)
);
const errorMessages = parseResults
.filter(
(result): result is InvalidParseResult =>
!result.isValid
)
.map(({ reason, value }) => `${reason} (${value})`);
if (errorMessages.length > 0) {
setValidation(errorMessages);
return;
}

const parsed = f
.unique(
(parseResults as RA<ValidParseResult>)
.filter(({ parsed }) => parsed !== null)
.map(({ parsed }) =>
(parsed as number | string).toString()
)
.sort(sortFunction(f.id))
)
.join('\n');
setCatalogNumbers(parsed);
}}
onValueChange={setCatalogNumbers}
{...attributes}
/>
<div>
<Button.Blue
disabled={
catalogNumbers.length === 0 ||
inputRef.current?.validity.valid !== true
}
onClick={(): void => handleProceed(undefined)}
>
{commonText.next()}
</Button.Blue>
</div>
{state.type === 'PreparationSelectState' &&
Object.keys(state.problems).length > 0 ? (
<>
{interactionsText.problemsFound()}
{Object.entries(state.problems).map(
([header, problems], index) => (
<React.Fragment key={index}>
<H3>{header}</H3>
{problems.map((problem, index) => (
<p key={index}>{problem}</p>
))}
</React.Fragment>
)
)}
<div>
<Button.Blue
onClick={(): void =>
setState({
...state,
problems: {},
})
}
>
{commonText.ignore()}
</Button.Blue>
</div>
</>
) : undefined}
</div>
</details>
<div className="flex flex-1 flex-col gap-2">
<details className="contents">
<summary>
{interactionsText.byChoosingRecordSet({ count: totalCount })}
</summary>
{children}
</details>
<details>
<summary>
{interactionsText.byEnteringNumbers({
fieldName: searchField.label,
})}
</summary>
<div className="flex flex-col gap-2">
<AutoGrowTextArea
forwardRef={validationRef}
spellCheck={false}
value={catalogNumbers}
onBlur={(): void => {
const parseResults = split(catalogNumbers).map((value) =>
parseValue(parser, inputRef.current ?? undefined, value)
);
const errorMessages = parseResults
.filter(
(result): result is InvalidParseResult =>
!result.isValid
)
.map(({ reason, value }) => `${reason} (${value})`);
if (errorMessages.length > 0) {
setValidation(errorMessages);
return;
}

const parsed = f
.unique(
(parseResults as RA<ValidParseResult>)
.filter(({ parsed }) => parsed !== null)
.map(({ parsed }) =>
(parsed as number | string).toString()
)
.sort(sortFunction(f.id))
)
.join('\n');
setCatalogNumbers(parsed);
}}
onValueChange={setCatalogNumbers}
{...attributes}
/>
<div>
<Button.Blue
disabled={
catalogNumbers.length === 0 ||
inputRef.current?.validity.valid !== true
}
onClick={(): void => handleProceed(undefined)}
>
{commonText.next()}
</Button.Blue>
</div>
{state.type === 'PreparationSelectState' &&
Object.keys(state.problems).length > 0 ? (
<>
{interactionsText.problemsFound()}
{Object.entries(state.problems).map(
([header, problems], index) => (
<React.Fragment key={index}>
<H3>{header}</H3>
{problems.map((problem, index) => (
<p key={index}>{problem}</p>
))}
</React.Fragment>
)
)}
<div>
<Button.Blue
onClick={(): void =>
setState({
...state,
problems: {},
})
}
>
{commonText.ignore()}
</Button.Blue>
</div>
</>
) : undefined}
</div>
</details>
</div>
</Dialog>
)}
Expand Down
44 changes: 44 additions & 0 deletions specifyweb/frontend/js_src/lib/components/Molecules/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,8 @@ export function Dialog({
[positionKey]
);

useFreezeDialogSize(container, dimensionsKey);

const isFullScreen = containerClassName.includes(dialogClassNames.fullScreen);

const draggableContainer: Props['contentElement'] = React.useCallback(
Expand Down Expand Up @@ -569,3 +571,45 @@ function useTitleChangeNotice(dimensionKey: string | undefined): void {
);
}, [dimensionKey]);
}

function useFreezeDialogSize(
containerSizeRef: HTMLDivElement | null,
dimensionKey: string | undefined
): void {
React.useEffect(() => {
if (dimensionKey === undefined) return;
if (containerSizeRef === null) return undefined;
let oldHeight = containerSizeRef.offsetHeight;
let oldWidth = containerSizeRef.offsetWidth;
const resizeObserver = new ResizeObserver(() => {
const newHeight = containerSizeRef.offsetHeight;
const newWidth = containerSizeRef.offsetWidth;

const width = f.parseInt(containerSizeRef.style.width);
const height = f.parseInt(containerSizeRef.style.height);
const hasBeenChanged =
typeof width === 'number' && typeof height === 'number';

if (oldHeight !== undefined && newHeight < oldHeight && !hasBeenChanged) {
containerSizeRef.style.minHeight = `${oldHeight}px`;
} else oldHeight = newHeight;

if (oldWidth !== undefined && newWidth < oldWidth && !hasBeenChanged) {
containerSizeRef.style.minWidth = `${oldWidth}px`;
} else oldWidth = newWidth;

if (hasBeenChanged) {
containerSizeRef.style.minHeight = '';
containerSizeRef.style.minWidth = '';
}
});

resizeObserver.observe(containerSizeRef);

return () => {
resizeObserver.disconnect();
containerSizeRef.style.minHeight = '';
containerSizeRef.style.minWidth = '';
};
}, [containerSizeRef, dimensionKey]);
}
Loading