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
3 changes: 3 additions & 0 deletions specifyweb/frontend/js_src/lib/components/Core/Contexts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,9 @@ ErrorContext.displayName = 'ErrorContext';
export const ReadOnlyContext = React.createContext<boolean>(false);
ReadOnlyContext.displayName = 'ReadOnlyContext';

export const RecordMergingContext = React.createContext(true);
RecordMergingContext.displayName = 'RecordMergingContext';

/** If true, form is rendered in a search dialog - required fields are not enforced */
export const SearchDialogContext = React.createContext<boolean>(false);
SearchDialogContext.displayName = 'SearchDialogContext';
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { MemoryRouter, Route, Routes } from 'react-router-dom';

import { requireContext } from '../../../tests/helpers';
import { TableDataView } from '..';

requireContext();

jest.mock('../queries', () => ({
...jest.requireActual('../queries'),
useDataViewQueries: () => [{ version: 1, queries: {} }, jest.fn()],
}));
jest.mock('../../Permissions/helpers', () => ({
...jest.requireActual('../../Permissions/helpers'),
hasPermission: () => true,
}));
jest.mock('../../Permissions/PermissionDenied', () => ({
ProtectedTable: ({ children }: { readonly children: React.ReactNode }) =>
children,
}));
jest.mock('../../QueryBuilder/ResultsWrapper', () => {
const actualReact = jest.requireActual<typeof React>('react');
const { RecordMergingContext } = jest.requireActual('../../Core/Contexts');
return {
...jest.requireActual('../../QueryBuilder/ResultsWrapper'),
QueryResultsWrapper: () => (
<div>
{actualReact.useContext(RecordMergingContext)
? 'Merging enabled'
: 'Merging disabled'}
</div>
),
};
});

test('disables merging throughout Data Views', () => {
render(
<MemoryRouter initialEntries={['/specify/dataviews/agent/']}>
<Routes>
<Route
path="/specify/dataviews/:tableName/"
element={<TableDataView />}
/>
</Routes>
</MemoryRouter>
);
expect(screen.getByText('Merging disabled')).toBeInTheDocument();
});
27 changes: 10 additions & 17 deletions specifyweb/frontend/js_src/lib/components/DataViews/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useResponsiveSplitView } from '../../hooks/useResponsiveSplitView';
import { H2 } from '../Atoms';
import { Button } from '../Atoms/Button';
import { DataEntry } from '../Atoms/DataEntry';
import { RecordMergingContext } from '../Core/Contexts';
import { getTable } from '../DataModel/tables';
import type { Tables } from '../DataModel/types';
import { raise } from '../Errors/Crash';
Expand Down Expand Up @@ -45,13 +46,15 @@ export function TableDataView(): JSX.Element {
return table === undefined ? (
<NotFoundView />
) : (
<ProtectedTable tableName={table.name} action="read">
{hasPermission('/querybuilder/query', 'execute') ? (
<DataViewFromTable tableName={table.name} />
) : (
<PermissionDenied resource="/querybuilder/query" action="execute" />
)}
</ProtectedTable>
<RecordMergingContext.Provider value={false}>
<ProtectedTable tableName={table.name} action="read">
{hasPermission('/querybuilder/query', 'execute') ? (
<DataViewFromTable tableName={table.name} />
) : (
<PermissionDenied resource="/querybuilder/query" action="execute" />
)}
</ProtectedTable>
</RecordMergingContext.Provider>
);
}

Expand Down Expand Up @@ -181,15 +184,6 @@ function LoadedDataViewFromTable({
restoreScrollTopRef.current = resultsScrollRef.current.scrollTop;
setRefreshToken((token) => token + 1);
}, []);
const handleMerged = React.useCallback((): void => {
/*
* Merging removes the selected records. Clear the preview before the
* refreshed results arrive so it does not try to load deleted records.
*/
setSelectedIds([]);
setSelectedIndex(0);
handleRefresh();
}, [handleRefresh]);
const handleCloseQueryEditor = (): void => setQueryData(undefined);
const handleOpenQueryEditor = (): void => {
setIsQueryDirty(false);
Expand Down Expand Up @@ -276,7 +270,6 @@ function LoadedDataViewFromTable({
setSelectedIds([]);
setSelectedIndex(0);
}}
onMerged={handleMerged}
onSortChange={(newFields): void => {
setRuntimeFields(unParseQueryFields(table.name, newFields));
setQueryRunCount((count) => count + 1);
Expand Down
4 changes: 3 additions & 1 deletion specifyweb/frontend/js_src/lib/components/FormMeta/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { H3 } from '../Atoms';
import { Button } from '../Atoms/Button';
import { icons } from '../Atoms/Icons';
import { Link } from '../Atoms/Link';
import { RecordMergingContext } from '../Core/Contexts';
import { toTable } from '../DataModel/helpers';
import type { AnySchema } from '../DataModel/helperTypes';
import type { SpecifyResource } from '../DataModel/legacyTypes';
Expand Down Expand Up @@ -131,7 +132,8 @@ function MetaDialog({
readonly onClose: () => void;
}): JSX.Element {
const subView = React.useContext(SubViewContext);
const canMergeTable = canMerge(resource.specifyTable);
const canMergeTable =
React.useContext(RecordMergingContext) && canMerge(resource.specifyTable);
return (
<Dialog
buttons={commonText.close()}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { f } from '../../utils/functools';
import { type GetSet, type RA } from '../../utils/types';
import { Container, H3 } from '../Atoms';
import { Button } from '../Atoms/Button';
import { RecordMergingContext } from '../Core/Contexts';
import type { SpecifyResource } from '../DataModel/legacyTypes';
import { schema } from '../DataModel/schema';
import type { SpecifyTable } from '../DataModel/specifyTable';
Expand Down Expand Up @@ -235,7 +236,8 @@ export function QueryResults(props: QueryResultsProps): JSX.Element {
setTotalCount,
]);

const canMergeTable = canMerge(table);
const canMergeTable =
React.useContext(RecordMergingContext) && canMerge(table);

const visibleColumns = React.useMemo(
() =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import {
act,
fireEvent,
render,
screen,
waitFor,
} from '@testing-library/react';
import React from 'react';

import { requireContext } from '../../../tests/helpers';
import { RecordMergingContext } from '../../Core/Contexts';
import { tables } from '../../DataModel/tables';
import { defaultDataViewQuery } from '../../DataViews/queries';
import { FormMeta } from '../../FormMeta';
import { formsText } from '../../../localization/forms';
import { UnloadProtectsContext } from '../../Router/UnloadProtect';
import { parseQueryFields, queryFieldsToFieldSpecs } from '../helpers';
import { QueryResults } from '../Results';

requireContext();

jest.mock('../../Permissions/helpers', () => ({
...jest.requireActual('../../Permissions/helpers'),
hasPermission: () => true,
hasTablePermission: () => true,
hasToolPermission: () => false,
}));
jest.mock('../../Permissions/PermissionDenied', () => ({
ProtectedTool: () => null,
ProtectedAction: () => null,
}));
jest.mock('../../Merging', () => ({
RecordMergingLink: () => <button>Merge selected records</button>,
}));
jest.mock('../ToForms', () => ({ QueryToForms: () => null }));
jest.mock('../ToMap', () => ({ QueryToMap: () => null }));
jest.mock('../ResultsTable', () => ({ QueryResultsTable: () => null }));
jest.mock('../../FormMeta/MergeRecord', () => ({
MergeRecord: () => <button>Merge this record</button>,
}));
jest.mock('../../FormMeta/AutoNumbering', () => ({
AutoNumbering: () => null,
}));
jest.mock('../../FormMeta/CarryForward', () => ({
...jest.requireActual('../../FormMeta/CarryForward'),
CarryForwardConfig: () => null,
}));
jest.mock('../../FormMeta/Clone', () => ({
CloneConfig: () => null,
AddButtonConfig: () => null,
}));
jest.mock('../../FormMeta/Definition', () => ({ Definition: () => null }));
jest.mock('../../FormMeta/EditHistory', () => ({ EditHistory: () => null }));
jest.mock('../../FormMeta/PickListUsages', () => ({
PickListUsages: () => null,
}));
jest.mock('../../FormMeta/QueryTreeUsages', () => ({
QueryTreeUsages: () => null,
}));
jest.mock('../../FormMeta/ReadOnlyMode', () => ({ ReadOnlyMode: () => null }));
jest.mock('../../FormMeta/ShareRecord', () => ({ ShareRecord: () => null }));
jest.mock('../../FormCommands', () => ({ GenerateLabel: () => null }));
jest.mock('../../FormFields/Checkbox', () => ({ PrintOnSave: () => null }));

test('hides merging in Data Views without changing Query Builder availability', async () => {
const fields = parseQueryFields(defaultDataViewQuery('Agent').fields);
const fieldSpecs = queryFieldsToFieldSpecs('Agent', fields).map(
([, fieldSpec]) => fieldSpec
);
const results = (
<QueryResults
table={tables.Agent}
queryResource={undefined}
fetchSize={40}
fetchResults={async () => [[1], [2]]}
fetchCount={undefined}
totalCount={2}
fieldSpecs={fieldSpecs}
displayedFields={fields}
allFields={fields}
initialData={[[1], [2]]}
selectedRows={[new Set([1, 2]), jest.fn()]}
onReRun={jest.fn()}
createRecordSet={undefined}
extraButtons={undefined}
/>
);
const { rerender } = render(results);
await waitFor(() =>
expect(
screen.getByRole('button', { name: 'Merge selected records' })
).toBeInTheDocument()
);

await act(async () =>
rerender(
<RecordMergingContext.Provider value={false}>
{results}
</RecordMergingContext.Provider>
)
);
expect(
screen.queryByRole('button', { name: 'Merge selected records' })
).not.toBeInTheDocument();
});

test('hides the preview form merge action when merging is disabled', async () => {
const formMeta = (
<FormMeta
resource={new tables.Agent.Resource({ id: 1 })}
viewDescription={undefined}
/>
);
const { rerender } = render(formMeta, {
wrapper: ({ children }) => (
<UnloadProtectsContext.Provider value={[]}>
{children}
</UnloadProtectsContext.Provider>
),
});
await act(async () =>
fireEvent.click(screen.getByRole('button', { name: formsText.formMeta() }))
);
expect(
screen.getByRole('button', { name: 'Merge this record' })
).toBeInTheDocument();
await act(async () =>
rerender(
<RecordMergingContext.Provider value={false}>
{formMeta}
</RecordMergingContext.Provider>
)
);
await act(async () =>
fireEvent.click(screen.getByRole('button', { name: formsText.formMeta() }))
);
expect(
screen.queryByRole('button', { name: 'Merge this record' })
).not.toBeInTheDocument();
});
Loading