Skip to content
Merged
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
@@ -0,0 +1,91 @@
import { act, render, screen } from '@testing-library/react';
import React from 'react';

import {
minimumHorizontalSplitWidth,
minimumSplitPaneWidth,
splitViewHandleWidth,
useResponsiveSplitView,
} from '../../../hooks/useResponsiveSplitView';

const horizontalAttribute = 'data-horizontal';
const falseValue = 'false';
const splitContainerTestId = 'split-container';

function TestSplit({
preferredHorizontal,
showContainer = true,
}: {
readonly preferredHorizontal: boolean;
readonly showContainer?: boolean;
}): JSX.Element {
const {
canUseHorizontalSplit,
containerRef,
isHorizontal,
maximumPrimaryPaneWidth,
} = useResponsiveSplitView(preferredHorizontal);
return showContainer ? (
<div
data-can-use-horizontal={canUseHorizontalSplit}
data-horizontal={isHorizontal}
data-primary-pane-max-width={maximumPrimaryPaneWidth}
data-testid={splitContainerTestId}
ref={containerRef}
/>
) : (
<span />
);
}

function resizeContainer(width: number): void {
const container = screen.getByTestId(splitContainerTestId);
Object.defineProperty(container, 'clientWidth', {
configurable: true,
value: width,
});
act((): void => {
window.dispatchEvent(new Event('resize'));
});
}

test('uses the container width to switch split orientation while resizing', () => {
render(<TestSplit preferredHorizontal />);
const container = screen.getByTestId(splitContainerTestId);

resizeContainer(minimumHorizontalSplitWidth - 1);
expect(container).toHaveAttribute('data-can-use-horizontal', falseValue);
expect(container).toHaveAttribute(horizontalAttribute, falseValue);

resizeContainer(minimumHorizontalSplitWidth);
expect(container).toHaveAttribute('data-can-use-horizontal', 'true');
expect(container).toHaveAttribute(horizontalAttribute, 'true');
expect(container).toHaveAttribute(
'data-primary-pane-max-width',
String(minimumSplitPaneWidth + splitViewHandleWidth)
);
});

test('keeps the preferred vertical orientation when there is enough width', () => {
const { rerender } = render(<TestSplit preferredHorizontal />);
const container = screen.getByTestId(splitContainerTestId);
resizeContainer(minimumHorizontalSplitWidth);

rerender(<TestSplit preferredHorizontal={false} />);
expect(container).toHaveAttribute(horizontalAttribute, falseValue);
});

test('remeasures a replacement container after the query editor closes', () => {
const { rerender } = render(<TestSplit preferredHorizontal />);
const originalContainer = screen.getByTestId(splitContainerTestId);
resizeContainer(minimumHorizontalSplitWidth - 1);
expect(originalContainer).toHaveAttribute(horizontalAttribute, falseValue);

rerender(<TestSplit preferredHorizontal showContainer={false} />);
rerender(<TestSplit preferredHorizontal />);

const replacementContainer = screen.getByTestId(splitContainerTestId);
expect(replacementContainer).not.toBe(originalContainer);
resizeContainer(minimumHorizontalSplitWidth);
expect(replacementContainer).toHaveAttribute(horizontalAttribute, 'true');
});
55 changes: 26 additions & 29 deletions specifyweb/frontend/js_src/lib/components/DataViews/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,41 @@ import { useParams } from 'react-router-dom';

import { commonText } from '../../localization/common';
import { dataViewsText } from '../../localization/dataViews';
import { useResponsiveSplitView } from '../../hooks/useResponsiveSplitView';
import { H2 } from '../Atoms';
import { Button } from '../Atoms/Button';
import { DataEntry } from '../Atoms/DataEntry';
import { H2 } from '../Atoms';
import type { Tables } from '../DataModel/types';
import { getTable } from '../DataModel/tables';
import type { Tables } from '../DataModel/types';
import { raise } from '../Errors/Crash';
import { RecordSelectorFromIds } from '../FormSliders/RecordSelectorFromIds';
import { Dialog } from '../Molecules/Dialog';
import { TableIcon } from '../Molecules/TableIcon';
import { hasPermission } from '../Permissions/helpers';
import {
PermissionDenied,
ProtectedTable,
} from '../Permissions/PermissionDenied';
import { hasPermission } from '../Permissions/helpers';
import { RecordSelectorFromIds } from '../FormSliders/RecordSelectorFromIds';
import { QueryResultsWrapper } from '../QueryBuilder/ResultsWrapper';
import { userPreferences } from '../Preferences/userPreferences';
import { parseQueryFields, unParseQueryFields } from '../QueryBuilder/helpers';
import { queryIdField } from '../QueryBuilder/Results';
import { QueryResultsWrapper } from '../QueryBuilder/ResultsWrapper';
import {
SplitView,
SplitViewOrientationButton,
SplitViewToggleButton,
useSplitViewOrientation,
} from '../QueryBuilder/SplitView';
import { NotFoundView } from '../Router/NotFoundView';
import { Dialog } from '../Molecules/Dialog';
import { raise } from '../Errors/Crash';
import type { DataViewQueriesFile } from './queries';
import {
getDataViewQueryDefinition,
makeDataViewQuery,
saveUserDataViewQueries,
serializeDataViewQueries,
useDataViewQueries,
} from './queries';
import type { DataViewQueriesFile } from './queries';
import { DataViewQueryEditorContent } from './QueryEditor';
import { TableIcon } from '../Molecules/TableIcon';
import { userPreferences } from '../Preferences/userPreferences';
import { listen } from '../../utils/events';

const SMALL_SCREEN_WIDTH = 768;

export function TableDataView(): JSX.Element {
const { tableName = '' } = useParams();
Expand Down Expand Up @@ -122,19 +120,15 @@ function LoadedDataViewFromTable({
'splitViewOrientation'
);
const [rawIsSplit, setIsSplit] = React.useState(splitViewByDefault);
const [canSplit, setCanSplit] = React.useState(
window.innerWidth >= SMALL_SCREEN_WIDTH
);
React.useEffect(() => {
const handleResize = (): void =>
setCanSplit(window.innerWidth >= SMALL_SCREEN_WIDTH);
handleResize();
return listen(window, 'resize', handleResize);
}, []);
const isSplit = rawIsSplit && canSplit;
const { isHorizontal, toggleOrientation } = useSplitViewOrientation(
splitViewOrientation === 'horizontal'
);
const isSplit = rawIsSplit;
const { isHorizontal: preferredIsHorizontal, toggleOrientation } =
useSplitViewOrientation(splitViewOrientation === 'horizontal');
const {
canUseHorizontalSplit,
containerRef: splitViewRef,
isHorizontal,
maximumPrimaryPaneWidth,
} = useResponsiveSplitView(preferredIsHorizontal);
const [refreshToken, setRefreshToken] = React.useState(0);
const [queryRunCount, setQueryRunCount] = React.useState(1);
const [queryData, setQueryData] = React.useState<string | undefined>();
Expand Down Expand Up @@ -359,23 +353,26 @@ function LoadedDataViewFromTable({
<DataEntry.Edit onClick={handleOpenQueryEditor} />
</div>
<SplitViewToggleButton
disabled={!canSplit}
isSplit={isSplit}
onToggle={(): void => setIsSplit((split) => !split)}
/>
<SplitViewOrientationButton
disabled={!isSplit}
disabled={!isSplit || !canUseHorizontalSplit}
isHorizontal={isHorizontal}
onToggle={toggleOrientation}
/>
<span className="-ml-2 flex-1" />
</header>
<div className="flex h-full max-h-full min-h-0 min-w-0 flex-1 overflow-hidden">
<div
className="flex h-full max-h-full min-h-0 min-w-0 flex-1 overflow-hidden"
ref={splitViewRef}
>
<SplitView
isHorizontal={isHorizontal}
isSplit={isSplit}
primaryPane={results}
primaryPaneKey="query-results"
primaryPaneMaxWidth={`${maximumPrimaryPaneWidth}px`}
secondaryPane={form}
secondaryPaneKey="record-preview"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export function QueryHeader({
onTriedToSave: handleTriedToSave,
onSaved: handleSaved,
isSplit,
canSplit,
canUseHorizontalSplit,
isHorizontal,
onToggleSplit,
onToggleOrientation,
Expand All @@ -67,7 +67,7 @@ export function QueryHeader({
readonly onTriedToSave: () => void;
readonly onSaved: () => void;
readonly isSplit: boolean;
readonly canSplit: boolean;
readonly canUseHorizontalSplit: boolean;
readonly isHorizontal: boolean;
readonly onToggleSplit: () => void;
readonly onToggleOrientation: () => void;
Expand Down Expand Up @@ -135,13 +135,9 @@ export function QueryHeader({
) : undefined}
{hasPermission('/querybuilder/query', 'execute') && (
<>
<SplitViewToggleButton
disabled={!canSplit}
isSplit={isSplit}
onToggle={onToggleSplit}
/>
<SplitViewToggleButton isSplit={isSplit} onToggle={onToggleSplit} />
<SplitViewOrientationButton
disabled={!isSplit}
disabled={!isSplit || !canUseHorizontalSplit}
isHorizontal={isHorizontal}
onToggle={onToggleOrientation}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';

import { commonText } from '../../localization/common';
import { localized, type RA } from '../../utils/types';
import { type RA, localized } from '../../utils/types';
import { BatchEditFromQuery } from '../BatchEdit';
import type { SerializedResource } from '../DataModel/helperTypes';
import type { SpecifyResource } from '../DataModel/legacyTypes';
Expand Down Expand Up @@ -34,6 +34,8 @@ export function QueryBuilderResults({
resultsRef,
isSplit,
isHorizontal,
maximumPrimaryPaneWidth,
splitViewRef,
onReRun: handleReRun,
onResults: handleResults,
onSelected: handleSelected,
Expand Down Expand Up @@ -61,6 +63,8 @@ export function QueryBuilderResults({
>;
readonly isSplit: boolean;
readonly isHorizontal: boolean;
readonly maximumPrimaryPaneWidth: number;
readonly splitViewRef: React.RefCallback<HTMLDivElement>;
readonly onReRun: () => void;
readonly onResults?: (results: RA<QueryResultRow | undefined>) => void;
readonly onSelected: (ids: RA<number>) => void;
Expand Down Expand Up @@ -149,8 +153,10 @@ export function QueryBuilderResults({
resultsRef={resultsRef}
selectedRows={[selectedRows, setSelectedRows]}
isSplit={isSplit}
splitContainerRef={splitViewRef}
splitHorizontal={isHorizontal}
splitPane={recordPreview}
splitPrimaryPaneMaxWidth={`${maximumPrimaryPaneWidth}px`}
table={table}
onReRun={handleReRun}
onResults={handleResults}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ export function QueryResultsWrapper({
onReRun: handleReRun,
refreshToken,
splitPane,
splitContainerRef,
splitHorizontal,
splitPrimaryPaneMaxWidth,
isSplit,
...props
}: ResultsProps & {
Expand All @@ -49,7 +51,9 @@ export function QueryResultsWrapper({
readonly restoreScrollTopRef?: React.MutableRefObject<number | undefined>;
readonly refreshToken?: number;
readonly splitPane?: JSX.Element;
readonly splitContainerRef?: React.RefCallback<HTMLDivElement>;
readonly splitHorizontal?: boolean;
readonly splitPrimaryPaneMaxWidth?: string;
readonly isSplit?: boolean;
readonly onReRun: () => void;
}): JSX.Element | null {
Expand Down Expand Up @@ -79,14 +83,20 @@ export function QueryResultsWrapper({
return splitPane === undefined ? (
queryResults
) : (
<SplitView
isHorizontal={splitHorizontal ?? true}
isSplit={isSplit}
primaryPane={queryResults}
primaryPaneKey="query-results"
secondaryPane={splitPane}
secondaryPaneKey="split-pane"
/>
<div
className="flex h-full max-h-full min-h-0 min-w-0 flex-1 overflow-hidden"
ref={splitContainerRef}
>
<SplitView
isHorizontal={splitHorizontal ?? true}
isSplit={isSplit}
primaryPane={queryResults}
primaryPaneKey="query-results"
primaryPaneMaxWidth={splitPrimaryPaneMaxWidth}
secondaryPane={splitPane}
secondaryPaneKey="split-pane"
/>
</div>
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from 'react';
import Splitter from 'm-react-splitters';
import React from 'react';

import { Button } from '../Atoms/Button';
import { treeText } from '../../localization/tree';
import { useTriggerState } from '../../hooks/useTriggerState';
import { treeText } from '../../localization/tree';
import { Button } from '../Atoms/Button';

export function useSplitViewOrientation(defaultHorizontal = true): {
readonly isHorizontal: boolean;
Expand Down Expand Up @@ -60,17 +60,29 @@ export function SplitView({
primaryPane,
secondaryPane,
primaryPaneKey,
primaryPaneMaxWidth,
secondaryPaneKey,
isHorizontal,
isSplit = true,
}: {
readonly primaryPane: JSX.Element;
readonly secondaryPane: JSX.Element;
readonly primaryPaneKey: string;
readonly primaryPaneMaxWidth?: string;
readonly secondaryPaneKey: string;
readonly isHorizontal: boolean;
readonly isSplit?: boolean;
}): JSX.Element {
const splitterRef = React.useRef<React.ElementRef<typeof Splitter> | null>(
null
);
const previousIsHorizontal = React.useRef(isHorizontal);
React.useLayoutEffect(() => {
if (previousIsHorizontal.current !== isHorizontal)
splitterRef.current?.setState({ primaryPane: undefined });
previousIsHorizontal.current = isHorizontal;
}, [isHorizontal]);

return (
<Splitter
className={`h-full max-h-full min-h-0 min-w-0 w-full flex-1 overflow-hidden ${
Expand All @@ -79,10 +91,11 @@ export function SplitView({
position={isHorizontal ? 'vertical' : 'horizontal'}
primaryPaneHeight={isSplit ? '50%' : '100%'}
primaryPaneMaxHeight={isSplit ? '80%' : '100%'}
primaryPaneMaxWidth={isSplit ? '80%' : '100%'}
primaryPaneMaxWidth={isSplit ? (primaryPaneMaxWidth ?? '80%') : '100%'}
primaryPaneMinHeight={1}
primaryPaneMinWidth={1}
primaryPaneWidth={isSplit ? '50%' : '100%'}
ref={splitterRef}
>
<div
className="flex h-full min-h-0 min-w-0 overflow-auto"
Expand Down
Loading
Loading