diff --git a/specifyweb/backend/context/views.py b/specifyweb/backend/context/views.py index 87861acc126..c3ad4416777 100644 --- a/specifyweb/backend/context/views.py +++ b/specifyweb/backend/context/views.py @@ -329,7 +329,13 @@ def user(request): obj_to_data(c) for c in available_collections ] - data['agent'] = obj_to_data(request.specify_user_agent) if request.specify_user_agent != None else None + current_agent = ( + obj_to_data(request.specify_user_agent) + if request.specify_user_agent is not None + else None + ) + data['agent'] = current_agent + data['current_agent'] = current_agent if settings.RO_MODE or not request.user.is_authenticated: data['usertype'] = "readonly" diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/AppResourceEditButton.test.tsx b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/AppResourceEditButton.test.tsx index 76e1b3e8977..c8aff34b80e 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/AppResourceEditButton.test.tsx +++ b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/AppResourceEditButton.test.tsx @@ -70,18 +70,18 @@ describe('AppResourceEditButton', () => { const dialog = getByRole('dialog'); expect(dialog.innerHTML).toMatchInlineSnapshot(` -"

TestTitle

" -`); + "

TestTitle

" + `); expect(handleDeleted).not.toHaveBeenCalled(); }); diff --git a/specifyweb/frontend/js_src/lib/components/Attachments/__tests__/uploadFile.test.ts b/specifyweb/frontend/js_src/lib/components/Attachments/__tests__/uploadFile.test.ts index 8eddbdf7907..7439350e268 100644 --- a/specifyweb/frontend/js_src/lib/components/Attachments/__tests__/uploadFile.test.ts +++ b/specifyweb/frontend/js_src/lib/components/Attachments/__tests__/uploadFile.test.ts @@ -31,12 +31,15 @@ describe('uploadFile', () => { return { open: jest.fn(), send: jest.fn((..._args: readonly unknown[]) => listeners[nextEvent]?.()), - addEventListener: jest.fn((eventName: EventName, callback: () => void) => { - listeners[eventName] = callback; - }), + addEventListener: jest.fn( + (eventName: EventName, callback: () => void) => { + listeners[eventName] = callback; + } + ), removeEventListener: jest.fn( (eventName: EventName, callback: () => void) => { - if (listeners[eventName] === callback) listeners[eventName] = undefined; + if (listeners[eventName] === callback) + listeners[eventName] = undefined; } ), upload: { diff --git a/specifyweb/frontend/js_src/lib/components/Core/Main.tsx b/specifyweb/frontend/js_src/lib/components/Core/Main.tsx index 9f9383537b4..881fd55ba00 100644 --- a/specifyweb/frontend/js_src/lib/components/Core/Main.tsx +++ b/specifyweb/frontend/js_src/lib/components/Core/Main.tsx @@ -38,7 +38,9 @@ export function Main({ }: { readonly menuItems: RA; }): JSX.Element { - const [hasAgent] = React.useState(userInformation.agent !== null); + const [hasAgent] = React.useState( + userInformation.currentCollectionAgent !== null + ); const mainRef = React.useRef(null); React.useEffect(() => { diff --git a/specifyweb/frontend/js_src/lib/components/Errors/FormatError.tsx b/specifyweb/frontend/js_src/lib/components/Errors/FormatError.tsx index 03fb7df0fb9..a2b0e6623b2 100644 --- a/specifyweb/frontend/js_src/lib/components/Errors/FormatError.tsx +++ b/specifyweb/frontend/js_src/lib/components/Errors/FormatError.tsx @@ -147,7 +147,7 @@ export function handleAjaxError( * If exceptions occur because user has no agent, don't display the error * message, so as not to spawn a new dialog on top of the "No Agent" dialog */ - if (userInformation.agent === null) throw error; + if (userInformation.currentCollectionAgent === null) throw error; if (errorMode !== 'silent') { const isNotFoundError = diff --git a/specifyweb/frontend/js_src/lib/components/InitialContext/userInformation.ts b/specifyweb/frontend/js_src/lib/components/InitialContext/userInformation.ts index e9a4c9c9bbd..2fc9b8527ff 100644 --- a/specifyweb/frontend/js_src/lib/components/InitialContext/userInformation.ts +++ b/specifyweb/frontend/js_src/lib/components/InitialContext/userInformation.ts @@ -18,22 +18,29 @@ export type UserInformation = SerializedRecord & { readonly name: LocalizedString; readonly isauthenticated: boolean; readonly availableCollections: RA>; - readonly agent: SerializedRecord; + readonly currentCollectionAgent: SerializedRecord | null; + readonly agent: SerializedRecord | null; }; const userInfo: Writable = {} as UserInformation; export const fetchContext = load< - Omit & { + Omit & { // eslint-disable-next-line @typescript-eslint/naming-convention readonly available_collections: RA>; + readonly current_agent?: SerializedRecord | null; } >('/context/user.json', 'application/json').then( async ({ available_collections: availableCollections, ...data }) => { - Object.entries(data).forEach(([key, value]) => { + const currentCollectionAgent = data.current_agent ?? data.agent ?? null; + const rest = { ...data }; + delete rest.current_agent; + Object.entries(rest).forEach(([key, value]) => { // @ts-expect-error userInfo[key as keyof UserInformation] = value; }); + userInfo.currentCollectionAgent = currentCollectionAgent; + userInfo.agent = currentCollectionAgent; await import('../DataModel/tables').then( async ({ fetchContext }) => fetchContext ); diff --git a/specifyweb/frontend/js_src/lib/components/Interactions/LoanReturn.tsx b/specifyweb/frontend/js_src/lib/components/Interactions/LoanReturn.tsx index df0ec7b2e49..70701aafa1c 100644 --- a/specifyweb/frontend/js_src/lib/components/Interactions/LoanReturn.tsx +++ b/specifyweb/frontend/js_src/lib/components/Interactions/LoanReturn.tsx @@ -96,7 +96,7 @@ function PreparationReturn({ const loanReturnPreparation = React.useRef( new tables.LoanReturnPreparation.Resource({ returneddate: getDateInputValue(new Date()), - receivedby: userInformation.agent.resource_uri, + receivedby: userInformation.currentCollectionAgent?.resource_uri, }) ); const [state, setState] = React.useState>(() => diff --git a/specifyweb/frontend/js_src/lib/components/Molecules/index.tsx b/specifyweb/frontend/js_src/lib/components/Molecules/index.tsx index 0adade45a00..6420a8f4c10 100644 --- a/specifyweb/frontend/js_src/lib/components/Molecules/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/Molecules/index.tsx @@ -7,9 +7,9 @@ import React from 'react'; +import { useHueDifference } from '../../hooks/useHueDifference'; import { commonText } from '../../localization/common'; import type { RA } from '../../utils/types'; -import { useHueDifference } from '../../hooks/useHueDifference'; export const loadingGif = (
@@ -31,7 +31,7 @@ export const loadingGif = ( * This must be accompanied by a label since loading bar is hidden from screen * readers */ -const LoadingBar = () => { +function LoadingBar() { const hueDifference = useHueDifference(); return ( @@ -45,7 +45,7 @@ const LoadingBar = () => { />
); -}; +} export const loadingBar = ; diff --git a/specifyweb/frontend/js_src/lib/components/Permissions/helpers.ts b/specifyweb/frontend/js_src/lib/components/Permissions/helpers.ts index aa482b8210e..fdca7be0718 100644 --- a/specifyweb/frontend/js_src/lib/components/Permissions/helpers.ts +++ b/specifyweb/frontend/js_src/lib/components/Permissions/helpers.ts @@ -30,12 +30,15 @@ export function hasTablePermission( ): boolean { const isReadOnly = getCache('forms', 'readOnlyMode') ?? false; if (isReadOnly && action !== 'read') return false; + const permissionsForCollection = getTablePermissions()[collectionId]; if ( - getTablePermissions()[collectionId][tableNameToResourceName(tableName)][ - action - ] + permissionsForCollection?.[tableNameToResourceName(tableName)]?.[action] ) return true; + if (permissionsForCollection === undefined) { + f.log(`Permissions for collection ${collectionId} are not loaded`); + return false; + } console.log(`No permission to ${action} ${tableName}`); return false; } @@ -49,7 +52,7 @@ export const hasPermission = < ): boolean => resource === '%' && action === '%' ? userInformation.isadmin - : getOperationPermissions()[collectionId][resource][action] + : getOperationPermissions()[collectionId]?.[resource]?.[action] ? true : (f.log(`No permission to ${action.toString()} ${resource}`) ?? false); @@ -78,7 +81,7 @@ export const hasDerivedPermission = < action: keyof ReturnType[number][RESOURCE], collectionId = schema.domainLevelIds.collection ): boolean => - getDerivedPermissions()[collectionId][resource][action] + getDerivedPermissions()[collectionId]?.[resource]?.[action] ? true : (f.log(`No permission to ${action.toString()} ${resource}`) ?? false); diff --git a/specifyweb/frontend/js_src/lib/components/QueryBuilder/LoanReturn.tsx b/specifyweb/frontend/js_src/lib/components/QueryBuilder/LoanReturn.tsx index add7e02b663..a5af9cc8cbf 100644 --- a/specifyweb/frontend/js_src/lib/components/QueryBuilder/LoanReturn.tsx +++ b/specifyweb/frontend/js_src/lib/components/QueryBuilder/LoanReturn.tsx @@ -135,7 +135,7 @@ export function QueryLoanReturn({ type: 'Dialog', loanReturnPreparation: new tables.LoanReturnPreparation.Resource({ returneddate: getDateInputValue(new Date()), - receivedby: userInformation.agent.resource_uri, + receivedby: userInformation.currentCollectionAgent?.resource_uri, }), queryResource: resourceToJson( typeof getQueryFieldRecords === 'function' diff --git a/specifyweb/frontend/js_src/lib/components/QueryComboBox/__tests__/defaultRecord.test.ts b/specifyweb/frontend/js_src/lib/components/QueryComboBox/__tests__/defaultRecord.test.ts index 0027a1b1748..30d7dca6869 100644 --- a/specifyweb/frontend/js_src/lib/components/QueryComboBox/__tests__/defaultRecord.test.ts +++ b/specifyweb/frontend/js_src/lib/components/QueryComboBox/__tests__/defaultRecord.test.ts @@ -27,7 +27,9 @@ test('Query Combo Box with current agent selected', () => { field: tables.CollectionObject.strictGetRelationship('cataloger'), defaultRecord, }); - expect(resource.get('cataloger')).toBe(userInformation.agent.resource_uri); + expect(resource.get('cataloger')).toBe( + userInformation.currentCollectionAgent?.resource_uri + ); }); test('Query Combo Box with current user selected', () => { const resource = new tables.RecordSet.Resource(); diff --git a/specifyweb/frontend/js_src/lib/components/QueryComboBox/helpers.ts b/specifyweb/frontend/js_src/lib/components/QueryComboBox/helpers.ts index 4d0a3dad9f5..c9a7fdb727f 100644 --- a/specifyweb/frontend/js_src/lib/components/QueryComboBox/helpers.ts +++ b/specifyweb/frontend/js_src/lib/components/QueryComboBox/helpers.ts @@ -2,7 +2,12 @@ import type { RA, WritableArray } from '../../utils/types'; import { toTable, toTreeTable } from '../DataModel/helpers'; import type { AnySchema } from '../DataModel/helperTypes'; import type { SpecifyResource } from '../DataModel/legacyTypes'; -import { idFromUrl, strictIdFromUrl } from '../DataModel/resource'; +import { + fetchResource, + getResourceApiUrl, + idFromUrl, + strictIdFromUrl, +} from '../DataModel/resource'; import type { Relationship } from '../DataModel/specifyField'; import type { SpecifyTable } from '../DataModel/specifyTable'; import { tables } from '../DataModel/tables'; @@ -232,8 +237,65 @@ export function pendingValueToResource( ); } +/** + * QueryComboBox can be rendered for a collection other than the currently + * logged-in one, via forceCollection. New resources are initialized with the + * current domain by default, so override scope to the target collection + */ +export async function scopeNewResourceToCollection( + resource: SpecifyResource, + collectionId: number | undefined +): Promise> { + if (!resource.isNew() || typeof collectionId !== 'number') return resource; + + const scopingRelationship = resource.specifyTable.getScopingRelationship(); + if (scopingRelationship === undefined) return resource; + + const scopeTableName = scopingRelationship.relatedTable.name; + try { + const disciplineUrl = + userInformation.availableCollections.find(({ id }) => id === collectionId) + ?.discipline ?? + (scopeTableName === 'Collection' + ? undefined + : (await fetchResource('Collection', collectionId)).discipline); + const discipline = + scopeTableName === 'Division' || scopeTableName === 'Institution' + ? typeof disciplineUrl === 'string' + ? await fetchResource('Discipline', strictIdFromUrl(disciplineUrl)) + : undefined + : undefined; + const divisionUrl = discipline?.division; + const division = + scopeTableName === 'Institution' + ? typeof divisionUrl === 'string' + ? await fetchResource('Division', strictIdFromUrl(divisionUrl)) + : undefined + : undefined; + + const targetScopeUrl = + scopeTableName === 'Collection' + ? getResourceApiUrl('Collection', collectionId) + : scopeTableName === 'Discipline' + ? disciplineUrl + : scopeTableName === 'Division' + ? divisionUrl + : scopeTableName === 'Institution' + ? division?.institution + : undefined; + + if (typeof targetScopeUrl === 'string') + resource.set(scopingRelationship.name, targetScopeUrl as never); + } catch { + // Fall back to default scoping if the lookup fails + } + + return resource; +} + const DEFAULT_RECORD_PRESETS = { - CURRENT_AGENT: () => userInformation.agent.resource_uri, + CURRENT_AGENT: () => + userInformation.currentCollectionAgent?.resource_uri ?? null, CURRENT_USER: () => userInformation.resource_uri, BLANK: () => null, } as const; @@ -264,7 +326,8 @@ export function useQueryComboBoxDefaults({ const record = toTable(resource, 'CollectionObject'); record?.set( 'cataloger', - record?.get('cataloger') ?? userInformation.agent.resource_uri, + record?.get('cataloger') ?? + userInformation.currentCollectionAgent?.resource_uri, { silent: true, } @@ -279,7 +342,8 @@ export function useQueryComboBoxDefaults({ const record = toTable(resource, 'LoanReturnPreparation'); record?.set( 'receivedBy', - record?.get('receivedBy') ?? userInformation.agent.resource_uri, + record?.get('receivedBy') ?? + userInformation.currentCollectionAgent?.resource_uri, { silent: true, } diff --git a/specifyweb/frontend/js_src/lib/components/QueryComboBox/index.tsx b/specifyweb/frontend/js_src/lib/components/QueryComboBox/index.tsx index 80e5af29805..cb9c588a9c4 100644 --- a/specifyweb/frontend/js_src/lib/components/QueryComboBox/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/QueryComboBox/index.tsx @@ -46,6 +46,7 @@ import { getRelatedCollectionId, makeComboBoxQuery, pendingValueToResource, + scopeNewResourceToCollection, useQueryComboBoxDefaults, } from './helpers'; import type { TypeSearch } from './spec'; @@ -221,6 +222,7 @@ export function QueryComboBox({ typeof collectionRelationships === 'object' && typeof resource === 'object' ? getRelatedCollectionId(collectionRelationships, resource, field.name) : undefined; + const targetCollectionId = forceCollection ?? relatedCollectionId; const loading = React.useContext(LoadingContext); const handleOpenRelated = (isReadOnly: boolean): void => @@ -248,6 +250,15 @@ export function QueryComboBox({ (typeof typeSearch === 'object' ? typeSearch?.table : undefined) ?? field.relatedTable; + const createPendingResource = React.useCallback( + async () => + scopeNewResourceToCollection( + pendingValueToResource(field, typeSearch, pendingValueRef.current), + targetCollectionId + ), + [field, typeSearch, targetCollectionId] + ); + // Used to fetch again tree def if the component type changes const componentType = resource?.specifyTable === tables.Component ? resource?.get('type') : null; @@ -381,7 +392,7 @@ export function QueryComboBox({ const canAdd = !RESTRICT_ADDING.has(field.relatedTable.name) && - hasTablePermission(field.relatedTable.name, 'create'); + hasTablePermission(field.relatedTable.name, 'create', targetCollectionId); const isReadOnly = React.useContext(ReadOnlyContext); @@ -446,14 +457,14 @@ export function QueryComboBox({ ? (): void => state.type === 'AddResourceState' ? setState({ type: 'MainState' }) - : setState({ - type: 'AddResourceState', - resource: pendingValueToResource( - field, - typeSearch, - pendingValueRef.current - ), - }) + : loading( + createPendingResource().then((pendingResource) => + setState({ + type: 'AddResourceState', + resource: pendingResource, + }) + ) + ) : undefined } /> @@ -481,14 +492,14 @@ export function QueryComboBox({ onClick={(): void => state.type === 'AddResourceState' ? setState({ type: 'MainState' }) - : setState({ - type: 'AddResourceState', - resource: pendingValueToResource( - field, - typeSearch, - pendingValueRef.current - ), - }) + : loading( + createPendingResource().then((pendingResource) => + setState({ + type: 'AddResourceState', + resource: pendingResource, + }) + ) + ) } /> ) : undefined} @@ -653,7 +664,7 @@ export function QueryComboBox({ {state.type === 'SearchState' ? ( void; + readonly onSaved?: () => void; readonly response: SetAgentsResponse; }): JSX.Element | null { const [response, setResponse] = React.useState(initialResponse); @@ -58,21 +61,30 @@ export function MissingAgentsDialog({ async () => typeof userAgents === 'object' ? Promise.all( - userAgents.map(async ({ divisionId, ...rest }) => - fetchResource('Division', divisionId).then((division) => ({ - division, - isRequired: - response.MissingAgentForAccessibleCollection?.all_accessible_divisions.includes( - divisionId - ) === true, - ...rest, - })) - ) - ).then((userAgents) => - Array.from(userAgents).sort( - sortFunction(({ division }) => division.name) + Array.from( + new Set(userAgents.flatMap(({ collections }) => collections)), + async (collectionId) => fetchUserPermissions(collectionId) ) ) + .then(async () => + Promise.all( + userAgents.map(async ({ divisionId, ...rest }) => + fetchResource('Division', divisionId).then((division) => ({ + division, + isRequired: + response.MissingAgentForAccessibleCollection?.all_accessible_divisions.includes( + divisionId + ) === true, + ...rest, + })) + ) + ) + ) + .then((userAgents) => + Array.from(userAgents).sort( + sortFunction(({ division }) => division.name) + ) + ) : undefined, [userAgents, response] ), @@ -122,7 +134,7 @@ export function MissingAgentsDialog({ }).then(({ data, status }) => status === Http.BAD_REQUEST ? setResponse(JSON.parse(data)) - : handleClose() + : (handleSaved ?? handleClose)() ) ) } diff --git a/specifyweb/frontend/js_src/lib/components/Security/User.tsx b/specifyweb/frontend/js_src/lib/components/Security/User.tsx index eb8885ec09c..457463e1468 100644 --- a/specifyweb/frontend/js_src/lib/components/Security/User.tsx +++ b/specifyweb/frontend/js_src/lib/components/Security/User.tsx @@ -75,6 +75,7 @@ import { UserIdentityProviders, UserRoles, } from './UserComponents'; +import type { UserAgents } from './UserHooks'; import { useCollectionRoles, useUserAgents, useUserRoles } from './UserHooks'; import { UserInviteLink } from './UserInviteLink'; import { @@ -214,6 +215,7 @@ function UserView({ 'SettingAgents', { readonly response: SetAgentsResponse; + readonly userAgents: UserAgents | undefined; } > | State<'Main'> @@ -235,6 +237,11 @@ function UserView({ '/admin/user/invite_link', 'create' ); + const userAgentsReadState = hasTablePermission('Agent', 'read') + ? hasTablePermission('Discipline', 'read') + ? 'full' + : 'missingDisciplineRead' + : 'missingAgentRead'; const canSeeInstitutionalPolicies = hasDerivedPermission( '/permissions/institutional_policies/user', 'read' @@ -350,6 +357,7 @@ function UserView({ collectionId={collectionId} isSuperAdmin={isSuperAdmin} userAgents={userAgents} + userAgentsReadState={userAgentsReadState} userPolicies={userPolicies} onChange={setUserPolicies} onChangedAgent={handleChangedAgent} @@ -505,6 +513,7 @@ function UserView({ ? setState({ type: 'SettingAgents', response: JSON.parse(data), + userAgents, }) : Array.isArray(institutionPolicies) && changedInstitutionPolicies @@ -536,6 +545,7 @@ function UserView({ setState({ type: 'SettingAgents', response: JSON.parse(data), + userAgents, }); } else return true; return undefined; @@ -655,9 +665,13 @@ function UserView({ setState({ type: 'Main' })} + onSaved={(): void => { + setVersion((version) => version + 1); + setState({ type: 'Main' }); + }} /> diff --git a/specifyweb/frontend/js_src/lib/components/Security/UserCollections.tsx b/specifyweb/frontend/js_src/lib/components/Security/UserCollections.tsx index cda027fe177..0ca599d86c4 100644 --- a/specifyweb/frontend/js_src/lib/components/Security/UserCollections.tsx +++ b/specifyweb/frontend/js_src/lib/components/Security/UserCollections.tsx @@ -15,6 +15,7 @@ import { ping } from '../../utils/ajax/ping'; import type { IR, RA } from '../../utils/types'; import { defined } from '../../utils/types'; import { replaceKey, toggleItem } from '../../utils/utils'; +import { ErrorMessage } from '../Atoms'; import { Button } from '../Atoms/Button'; import { className } from '../Atoms/className'; import { Form, Input, Label, Select } from '../Atoms/Form'; @@ -169,6 +170,7 @@ export function CollectionAccess({ onChangedAgent: handleChangeAgent, collectionId, userAgents, + userAgentsReadState, isSuperAdmin, }: { readonly userPolicies: IR | undefined> | undefined; @@ -178,6 +180,10 @@ export function CollectionAccess({ readonly onChangedAgent: () => void; readonly collectionId: number; readonly userAgents: UserAgents | undefined; + readonly userAgentsReadState: + | 'full' + | 'missingAgentRead' + | 'missingDisciplineRead'; readonly isSuperAdmin: boolean; }): JSX.Element { const hasCollectionAccess = @@ -238,7 +244,16 @@ export function CollectionAccess({ : undefined ); - const isReadOnly = React.useContext(ReadOnlyContext) || !canAssignAgent; + const agentReadWarning = + userAgentsReadState === 'missingAgentRead' + ? userText.cannotReadAgentsForUserAssignment() + : userAgentsReadState === 'missingDisciplineRead' + ? userText.cannotReadDisciplinesForUserAssignment() + : undefined; + const isReadOnly = + React.useContext(ReadOnlyContext) || + !canAssignAgent || + userAgentsReadState !== 'full'; return (
{hasPermission('/permissions/policies/user', 'read', collectionId) && @@ -277,6 +292,9 @@ export function CollectionAccess({ ) : ( )} + {agentReadWarning !== undefined && ( + {agentReadWarning} + )}
); diff --git a/specifyweb/frontend/js_src/lib/components/Security/UserHooks.tsx b/specifyweb/frontend/js_src/lib/components/Security/UserHooks.tsx index a10154f0fee..0ea56148571 100644 --- a/specifyweb/frontend/js_src/lib/components/Security/UserHooks.tsx +++ b/specifyweb/frontend/js_src/lib/components/Security/UserHooks.tsx @@ -15,7 +15,6 @@ import { strictIdFromUrl, } from '../DataModel/resource'; import { schema } from '../DataModel/schema'; -import { serializeResource } from '../DataModel/serializers'; import { tables } from '../DataModel/tables'; import type { Address, Collection, SpecifyUser } from '../DataModel/types'; import { userInformation } from '../InitialContext/userInformation'; @@ -145,8 +144,7 @@ export function useUserAgents( ] as const); return ( typeof userId === 'number' - ? hasTablePermission('Agent', 'read') && - hasTablePermission('Division', 'read') + ? hasTablePermission('Agent', 'read') ? fetchCollection( 'Agent', { @@ -155,7 +153,7 @@ export function useUserAgents( }, backendFilter('division').isIn(divisions.map(([id]) => id)) ).then(({ records }) => records) - : Promise.resolve([serializeResource(userInformation.agent)]) + : Promise.resolve([]) : Promise.resolve([]) ).then((rawAgents) => { const agents = Object.fromEntries( diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/CustomSelectElement.tsx b/specifyweb/frontend/js_src/lib/components/WbPlanView/CustomSelectElement.tsx index bcdd53e4ddf..f3e4a7aa0b1 100644 --- a/specifyweb/frontend/js_src/lib/components/WbPlanView/CustomSelectElement.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/CustomSelectElement.tsx @@ -849,8 +849,8 @@ export function CustomSelectElement({ >