Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
5dea652
Add renderer for taxon tree def items
grantfitzsimmons Jul 1, 2025
444b04f
Add threshold rank preference
grantfitzsimmons Jul 1, 2025
3c25958
Update preferences.ts
grantfitzsimmons Jul 1, 2025
c6e17cc
Merge branch 'issue-6841' into issue-6843
grantfitzsimmons Jul 1, 2025
cc2fbe6
Update Renderers.tsx
grantfitzsimmons Jul 1, 2025
4bb52d5
Merge branch 'issue-6843' of https://github.com/specify/specify7 into…
grantfitzsimmons Jul 1, 2025
2ae8497
Update remotePrefs.test.ts.snap
grantfitzsimmons Jul 1, 2025
8806806
Update renderer to be generic across tree tables
grantfitzsimmons Jul 1, 2025
c760d44
Use user preference instead of remotePref
grantfitzsimmons Jul 1, 2025
25217bf
Remove unused imports
grantfitzsimmons Jul 1, 2025
30c35c6
Update Renderers.tsx
grantfitzsimmons Jul 1, 2025
e80d3b7
Lint code with ESLint and Prettier
grantfitzsimmons Jul 1, 2025
68a187d
Correct description
grantfitzsimmons Jul 1, 2025
95c7c50
Merge branch 'issue-6843' of https://github.com/specify/specify7 into…
grantfitzsimmons Jul 1, 2025
d18394c
Lint code with ESLint and Prettier
grantfitzsimmons Jul 1, 2025
539027d
Remove unused function
grantfitzsimmons Jul 1, 2025
430e743
Update specifyweb/frontend/js_src/lib/components/Preferences/Renderer…
grantfitzsimmons Jul 1, 2025
e7c7afe
Remove 'none' option
grantfitzsimmons Jul 1, 2025
fa71563
Update preferences.ts
grantfitzsimmons Jul 1, 2025
bbd714b
Revert "Remove 'none' option"
grantfitzsimmons Jul 1, 2025
f7357dd
Merge branch 'issue-6843' of https://github.com/specify/specify7 into…
grantfitzsimmons Jul 1, 2025
098d361
Update preferences.ts
grantfitzsimmons Jul 1, 2025
74ff3cb
Merge branch 'issue-6841' into issue-6843
grantfitzsimmons Jul 1, 2025
48314b3
Use RA<>
grantfitzsimmons Jul 1, 2025
86a8e33
Order ranks
grantfitzsimmons Jul 1, 2025
4f08ce2
Lint code with ESLint and Prettier
grantfitzsimmons Jul 1, 2025
77bf41a
Merge branch 'main' into issue-6843
grantfitzsimmons Jul 3, 2025
5d91e20
Merge branch 'main' into issue-6843
melton-jason Jul 7, 2025
7a2eff4
Lint code with ESLint and Prettier
melton-jason Jul 7, 2025
d7632f6
Merge branch 'main' into issue-6843
grantfitzsimmons Jul 24, 2025
c81eafc
Lint code with ESLint and Prettier
grantfitzsimmons Jul 24, 2025
6dcc0f9
Restrict tree rank preference options to the active definition
Gitesh307 Nov 16, 2025
a995792
installed ts-node devDependancy
Gitesh307 Nov 16, 2025
b239180
installed required dependancy
Gitesh307 Nov 16, 2025
9e574a6
Filter tree rank preference options
Gitesh307 Nov 16, 2025
f79c6c0
Lint code with ESLint and Prettier
Gitesh307 Nov 16, 2025
e908ed4
Merge branch 'main' into issue-6843
Gitesh307 Nov 18, 2025
f454e18
Lint code with ESLint and Prettier
Gitesh307 Nov 18, 2025
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 @@ -109,7 +109,8 @@ export function RecordSetAttachments<SCHEMA extends AnySchema>({

const isComplete = fetchedCount.current === recordCount;

const [showCreateRecordSetDialog, setShowCreateRecordSetDialog] = React.useState(false);
const [showCreateRecordSetDialog, setShowCreateRecordSetDialog] =
React.useState(false);

return (
<>
Expand All @@ -133,16 +134,17 @@ export function RecordSetAttachments<SCHEMA extends AnySchema>({
<Button.Info
title={attachmentsText.downloadAllDescription()}
onClick={(): void =>
(recordSetId === undefined && !isComplete) ?
setShowCreateRecordSetDialog(true)
:
loading(
downloadAllAttachments(
(recordSetId !== undefined && !isComplete) ? [] : attachmentsRef.current?.attachments ?? [],
name,
recordSetId,
)
)
recordSetId === undefined && !isComplete
? setShowCreateRecordSetDialog(true)
: loading(
downloadAllAttachments(
recordSetId !== undefined && !isComplete
? []
: (attachmentsRef.current?.attachments ?? []),
name,
recordSetId
)
)
}
>
{attachmentsText.downloadAll()}
Expand All @@ -157,15 +159,15 @@ export function RecordSetAttachments<SCHEMA extends AnySchema>({
header={
attachmentsRef.current?.attachments === undefined
? attachmentsText.attachments()
: (isComplete ?
commonText.countLine({
resource: attachmentsText.attachments(),
count: attachmentsRef.current.attachments.length
}) :
commonText.countLineOrMore({
resource: attachmentsText.attachments(),
count: attachmentsRef.current.attachments.length
}))
: isComplete
? commonText.countLine({
resource: attachmentsText.attachments(),
count: attachmentsRef.current.attachments.length,
})
: commonText.countLineOrMore({
resource: attachmentsText.attachments(),
count: attachmentsRef.current.attachments.length,
})
}
onClose={handleHideAttachments}
>
Expand Down Expand Up @@ -215,13 +217,11 @@ function CreateRecordSetDialog({
}): JSX.Element {
return (
<Dialog
buttons={
<Button.DialogClose>{commonText.close()}</Button.DialogClose>
}
buttons={<Button.DialogClose>{commonText.close()}</Button.DialogClose>}
header={attachmentsText.downloadAll()}
onClose={onClose}
>
{attachmentsText.createRecordSetToDownloadAll()}
</Dialog>
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,6 @@ const containsSystemTables = (queryFieldSpec: QueryFieldSpec) => {
return Boolean(baseIsBlocked || pathHasBlockedSystem);
};


const hasHierarchyBaseTable = (queryFieldSpec: QueryFieldSpec) =>
Object.keys(schema.domainLevelIds).includes(
queryFieldSpec.baseTable.name.toLowerCase() as 'collection'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import { softFail } from '../Errors/Crash';
import { isTreeResource } from '../InitialContext/treeRanks';
import type { BusinessRuleDefs } from './businessRuleDefs';
import { businessRuleDefs } from './businessRuleDefs';
import { backboneFieldSeparator, backendFilter, djangoLookupSeparator } from './helpers';
import {
backboneFieldSeparator,
backendFilter,
djangoLookupSeparator,
} from './helpers';
import type {
AnySchema,
AnyTree,
Expand Down Expand Up @@ -316,10 +320,7 @@ export class BusinessRuleManager<SCHEMA extends AnySchema> {
)
);

const stringValuesAreEqual = (
left: string,
right: string
): boolean =>
const stringValuesAreEqual = (left: string, right: string): boolean =>
rule.isDatabaseConstraint
? left.localeCompare(right, undefined, { sensitivity: 'accent' }) === 0
: left === right;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import type { RA } from '../../utils/types';
import { Input, Select, Textarea } from '../Atoms/Form';
import { iconClassName } from '../Atoms/Icons';
import { ReadOnlyContext } from '../Core/Contexts';
import type { AnySchema } from '../DataModel/helperTypes';
import type { AnySchema, AnyTree } from '../DataModel/helperTypes';
import type { SpecifyTable } from '../DataModel/specifyTable';
import { tables } from '../DataModel/tables';
import type { Collection } from '../DataModel/types';
Expand All @@ -33,6 +33,7 @@ import { useMenuItems, useUserTools } from '../Header/menuItemProcessing';
import { AttachmentPicker } from '../Molecules/AttachmentPicker';
import { AutoComplete } from '../Molecules/AutoComplete';
import { ListEdit } from '../Toolbar/ListEdit';
import { getTreeDefinitions, treeRanksPromise } from '../InitialContext/treeRanks';
import type { PreferenceItem, PreferenceRendererProps } from './types';
import { userPreferences } from './userPreferences';

Expand Down Expand Up @@ -378,3 +379,58 @@ export function DefaultPreferenceItemRender({
/>
);
}

type Rank = { readonly rankId: number; readonly name: string };

/*
* This grabs the ranks from the API and displays them in a dropdown
* The ranks are sorted in ascending order by `rankId` so they appear in the correct order for the user
*/
export function ThresholdRank({
value,
onChange,
tableName,
}: PreferenceRendererProps<number> & {
readonly tableName: AnyTree['tableName'];
}): JSX.Element {
const [items, setItems] = React.useState<readonly Rank[]>([]);

React.useEffect(() => {
let isMounted = true;
treeRanksPromise
.then(() => {
const definitions = getTreeDefinitions(tableName);
const activeDefinition = definitions[0];
/**
* Only expose ranks from the treedef tied to the current discipline.
* Otherwise ranks from unrelated trees leak into the dropdown.
*/
const ranks = activeDefinition?.ranks ?? [];
if (!isMounted) return;
setItems(
ranks
.map<Rank>(({ rankId, name }) => ({
rankId,
name,
}))
.sort((rankA, rankB) => rankA.rankId - rankB.rankId)
);
})
.catch((error: unknown) => {
console.error('Error fetching ThresholdRank items:', error);
if (isMounted) setItems([]);
});
return () => {
isMounted = false;
};
}, [tableName]);

return (
<select value={value ?? ''} onChange={e => onChange(Number(e.target.value))}>
<option value="">None</option>
{items.map(({ rankId, name }) => (
<option key={rankId} value={rankId}>{name}</option>
))}
</select>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
defaultFont,
FontFamilyPreferenceItem,
HeaderItemsPreferenceItem,
ThresholdRank,
WelcomePageModePreferenceItem,
} from './Renderers';
import type { GenericPreferences, PreferencesVisibilityContext } from './types';
Expand Down Expand Up @@ -1474,6 +1475,17 @@ export const userPreferenceDefinitions = {
renderer: ColorPickerPreferenceItem,
container: 'label',
}),
rankThreshold: definePref<number>({
title: preferencesText.rankThreshold(),
description: preferencesText.rankThresholdDescription(),
requiresReload: true,
visible: true,
defaultValue: 0,
renderer: (props) => (
<ThresholdRank {...props} tableName="Geography" />
),
container: 'label',
}),
},
},
taxon: {
Expand Down Expand Up @@ -1502,6 +1514,15 @@ export const userPreferenceDefinitions = {
defaultValue: true,
type: 'java.lang.Boolean',
}),
rankThreshold: definePref<number>({
title: preferencesText.rankThreshold(),
description: preferencesText.rankThresholdDescription(),
requiresReload: true,
visible: true,
defaultValue: 0,
renderer: (props) => <ThresholdRank {...props} tableName="Taxon" />,
container: 'label',
}),
},
},
storage: {
Expand All @@ -1523,6 +1544,17 @@ export const userPreferenceDefinitions = {
renderer: ColorPickerPreferenceItem,
container: 'label',
}),
rankThreshold: definePref<number>({
title: preferencesText.rankThreshold(),
description: preferencesText.rankThresholdDescription(),
requiresReload: true,
visible: true,
defaultValue: 0,
renderer: (props) => (
<ThresholdRank {...props} tableName="Storage" />
),
container: 'label',
}),
},
},
geologicTimePeriod: {
Expand All @@ -1544,6 +1576,17 @@ export const userPreferenceDefinitions = {
renderer: ColorPickerPreferenceItem,
container: 'label',
}),
rankThreshold: definePref<number>({
title: preferencesText.rankThreshold(),
description: preferencesText.rankThresholdDescription(),
requiresReload: true,
visible: true,
defaultValue: 0,
renderer: (props) => (
<ThresholdRank {...props} tableName="GeologicTimePeriod" />
),
container: 'label',
}),
},
},
lithoStrat: {
Expand All @@ -1565,6 +1608,17 @@ export const userPreferenceDefinitions = {
renderer: ColorPickerPreferenceItem,
container: 'label',
}),
rankThreshold: definePref<number>({
title: preferencesText.rankThreshold(),
description: preferencesText.rankThresholdDescription(),
requiresReload: true,
visible: true,
defaultValue: 0,
renderer: (props) => (
<ThresholdRank {...props} tableName="LithoStrat" />
),
container: 'label',
}),
},
},
tectonicUnit: {
Expand All @@ -1586,6 +1640,17 @@ export const userPreferenceDefinitions = {
renderer: ColorPickerPreferenceItem,
container: 'label',
}),
rankThreshold: definePref<number>({
title: preferencesText.rankThreshold(),
description: preferencesText.rankThresholdDescription(),
requiresReload: true,
visible: true,
defaultValue: 0,
renderer: (props) => (
<ThresholdRank {...props} tableName="TectonicUnit" />
),
container: 'label',
}),
},
},
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { render, screen, waitFor } from '@testing-library/react';
import React from 'react';

import { preferencesText } from '../../../localization/preferences';
import { overrideAjax } from '../../../tests/ajax';
import { requireContext } from '../../../tests/helpers';
import * as treeRanks from '../../InitialContext/treeRanks';
import { ThresholdRank } from '../Renderers';
import type { PreferenceItem } from '../types';

overrideAjax('/context/schema_localization.json', {});
requireContext();

const mockedGetTreeDefinitions = jest.spyOn(
treeRanks,
'getTreeDefinitions'
);

describe('ThresholdRank', () => {
beforeEach(() => {
mockedGetTreeDefinitions.mockReset();
});

test('only renders ranks from the active tree definition', async () => {
mockedGetTreeDefinitions.mockReturnValue([
{
definition: { id: 1 } as any,
ranks: [
{ rankId: 50, name: 'Active Rank B' },
{ rankId: 10, name: 'Active Rank A' },
] as any,
},
{
definition: { id: 2 } as any,
ranks: [{ rankId: 5, name: 'Inactive Rank' }] as any,
},
]);

const definition: PreferenceItem<number> = {
title: preferencesText.rankThreshold(),
requiresReload: false,
visible: true,
defaultValue: 0,
values: [],
};
render(
<ThresholdRank
category="tree"
definition={definition}
item="rankThreshold"
subcategory="geography"
tableName="Geography"
value={50}
onChange={jest.fn()}
/>
);

await waitFor(() => expect(screen.getAllByRole('option')).toHaveLength(3));

const labels = screen.getAllByRole('option').map((option) => option.textContent);
expect(labels).toEqual(['None', 'Active Rank A', 'Active Rank B']);
expect(screen.queryByText('Inactive Rank')).not.toBeInTheDocument();
});
});
8 changes: 5 additions & 3 deletions specifyweb/frontend/js_src/lib/components/TreeView/Tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import { idFromUrl } from '../DataModel/resource';
import { deserializeResource } from '../DataModel/serializers';
import { softError } from '../Errors/assert';
import { ResourceView } from '../Forms/ResourceView';
import { getPref } from '../InitialContext/remotePrefs';
import { hasTablePermission } from '../Permissions/helpers';
import { useHighContrast } from '../Preferences/Hooks';
import { userPreferences } from '../Preferences/userPreferences';
Expand Down Expand Up @@ -98,9 +97,12 @@ export function Tree<
'synonymColor'
);

const statsThreshold = getPref(
`TreeEditor.Rank.Threshold.${tableName as 'Geography'}`
const [statsThreshold] = userPreferences.use(
'treeEditor',
treeToPref[tableName],
'rankThreshold'
);

const getStats = React.useCallback(
async (nodeId: number | 'null', rankId: number): Promise<Stats> =>
rankId >= statsThreshold
Expand Down
Loading