diff --git a/specifyweb/backend/workbench/upload/upload_table.py b/specifyweb/backend/workbench/upload/upload_table.py index 7dcd72f55d8..32f788c4ccd 100644 --- a/specifyweb/backend/workbench/upload/upload_table.py +++ b/specifyweb/backend/workbench/upload/upload_table.py @@ -395,7 +395,7 @@ def get_django_predicates( model = self.django_model if self.disambiguation is not None: - if model.objects.filter(id=self.disambiguation).exists(): + if model.objects.filter(pk=self.disambiguation).exists(): return DjangoPredicates(filters={"id": self.disambiguation}) # here's why the line below matters: If some records were added during record epansion, we cannot _filter_ or _eclude_ on them. @@ -554,7 +554,7 @@ def _resolve_reference_attributes(self, model, reference_record) -> dict[str, An def _handle_row(self, skip_match: bool, allow_null: bool) -> UploadResult: model = self.django_model if self.disambiguation is not None: - if model.objects.filter(id=self.disambiguation).exists(): + if model.objects.filter(pk=self.disambiguation).exists(): return UploadResult( Matched( id=self.disambiguation, info=ReportInfo(self.name, [], None) diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/CreateAppResource.test.tsx b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/CreateAppResource.test.tsx index cbf9483ddf8..587d4d72e0f 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/CreateAppResource.test.tsx +++ b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/CreateAppResource.test.tsx @@ -58,7 +58,7 @@ describe('CreateAppResource', () => { // This is a lot more cleaner than the inner HTML expect(getByRole('dialog').textContent).toMatchInlineSnapshot( - `"Select Resource TypeTypeDocumentationLabelDocumentationReportDocumentationDefault User PreferencesDocumentationLeaflet LayersDocumentationRSS Export FeedDocumentationExpress Search ConfigDocumentationType SearchesDocumentationWeb LinksDocumentationField FormattersDocumentationRecord FormattersDocumentationData Entry TablesDocumentationInteractions TablesDocumentationOther XML ResourceOther JSON ResourceOther Properties ResourceOther ResourceCancel"` + `"Select Resource TypeTypeDocumentationLabelDocumentation(opens in a new tab)ReportDocumentation(opens in a new tab)Default User PreferencesDocumentation(opens in a new tab)Leaflet LayersDocumentation(opens in a new tab)RSS Export FeedDocumentation(opens in a new tab)Express Search ConfigDocumentation(opens in a new tab)Type SearchesDocumentation(opens in a new tab)Web LinksDocumentation(opens in a new tab)Field FormattersDocumentation(opens in a new tab)Record FormattersDocumentation(opens in a new tab)Data Entry TablesDocumentation(opens in a new tab)Interactions TablesDocumentation(opens in a new tab)Other XML ResourceOther JSON ResourceOther Properties ResourceOther ResourceCancel"` ); }); diff --git a/specifyweb/frontend/js_src/lib/components/Atoms/DataEntry.tsx b/specifyweb/frontend/js_src/lib/components/Atoms/DataEntry.tsx index 1b8db504a00..c75d789dbf3 100644 --- a/specifyweb/frontend/js_src/lib/components/Atoms/DataEntry.tsx +++ b/specifyweb/frontend/js_src/lib/components/Atoms/DataEntry.tsx @@ -4,10 +4,12 @@ import type { LocalizedString } from 'typesafe-i18n'; import { commonText } from '../../localization/common'; import { formsText } from '../../localization/forms'; import type { RA } from '../../utils/types'; +import { localized } from '../../utils/types'; import type { AnySchema } from '../DataModel/helperTypes'; import type { SpecifyResource } from '../DataModel/legacyTypes'; import type { ViewDescription } from '../FormParse'; import type { cellAlign, cellVerticalAlign } from '../FormParse/cells'; +import { userPreferences } from '../Preferences/userPreferences'; import { Button } from './Button'; import { className } from './className'; import type { icons } from './Icons'; @@ -19,23 +21,24 @@ const dataEntryButton = ( className: string, title: LocalizedString, icon: keyof typeof icons -) => - function ( +) => { + const component = ( props: Omit, 'children' | 'type'> & { readonly onClick: | ((event: React.MouseEvent) => void) | undefined; } - ): JSX.Element { - return ( - - ); - }; + ): JSX.Element => ( + + ); + Object.defineProperty(component, 'name', { value: icon }); + return component; +}; export const columnDefinitionsToCss = ( columns: RA, @@ -54,6 +57,12 @@ export const columnDefinitionsToCss = ( * This is called DataEntry instead of Form because "Form" is already taken */ +const DataEntryAdd = dataEntryButton( + className.dataEntryAdd, + commonText.add(), + 'plus' +); + export const DataEntry = { Grid: wrap< 'div', @@ -145,7 +154,29 @@ export const DataEntry = { }) ), SubFormTitle: wrap('DataEntry.SubFormTitle', 'h3', className.formTitle), - Add: dataEntryButton(className.dataEntryAdd, commonText.add(), 'plus'), + Add({ + enableShortcut, + onClick: handleClick, + title = commonText.add(), + ...rest + }: Omit[0], 'onClick'> & { + readonly onClick: (() => void) | undefined; + readonly enableShortcut: boolean; + }): JSX.Element { + const addButtonShortcut = userPreferences.useKeyboardShortcut( + 'form', + 'recordSet', + 'addResource', + enableShortcut && rest.disabled !== true ? handleClick : undefined + ); + return ( + + ); + }, View: dataEntryButton(className.dataEntryView, commonText.view(), 'eye'), Edit: dataEntryButton(className.dataEntryEdit, commonText.edit(), 'pencil'), Clone: dataEntryButton( @@ -170,14 +201,23 @@ export const DataEntry = { readonly className?: string; readonly resource: SpecifyResource | undefined; }): JSX.Element | null { + const ref = React.useRef(null); + const keyboardShortcut = userPreferences.useKeyboardShortcut( + 'form', + 'dialogs', + 'openRelatedRecordInNewTab', + typeof resource === 'object' && !resource.isNew() + ? (): void => ref.current?.click() + : undefined + ); return typeof resource === 'object' && !resource.isNew() ? ( ) : null; }, }; -/* eslint-enable @typescript-eslint/naming-convention */ diff --git a/specifyweb/frontend/js_src/lib/components/Atoms/Icons.tsx b/specifyweb/frontend/js_src/lib/components/Atoms/Icons.tsx index d376691fecf..8c14f5ce555 100644 --- a/specifyweb/frontend/js_src/lib/components/Atoms/Icons.tsx +++ b/specifyweb/frontend/js_src/lib/components/Atoms/Icons.tsx @@ -75,6 +75,7 @@ export const icons = { collection: , cube: , cubeTransparent: , + cursorClick: , database: , document: , documentReport: , @@ -86,13 +87,10 @@ export const icons = { exclamationCircle: , externalLink: , eye: , + filter: , fingerPrint: , - gallery: - -, - globe: - -, + gallery:, + globe: , hashtag: , // This icon is not from Heroicons. It was drawn by @grantfitzsimmons history: , diff --git a/specifyweb/frontend/js_src/lib/components/Atoms/Link.tsx b/specifyweb/frontend/js_src/lib/components/Atoms/Link.tsx index d762b6ab951..ed346930742 100644 --- a/specifyweb/frontend/js_src/lib/components/Atoms/Link.tsx +++ b/specifyweb/frontend/js_src/lib/components/Atoms/Link.tsx @@ -1,6 +1,7 @@ import React from 'react'; import type { LocalizedString } from 'typesafe-i18n'; +import { commonText } from '../../localization/common'; import type { IR, RA, RR } from '../../utils/types'; import { className } from './className'; import type { IconProps } from './Icons'; @@ -46,7 +47,15 @@ export const Link = { children: ( <> {props.children} - {icons.externalLink} + + {commonText.opensInNewTab()} + {icons.externalLink} + ), }; diff --git a/specifyweb/frontend/js_src/lib/components/Atoms/__tests__/DataEntry.test.ts b/specifyweb/frontend/js_src/lib/components/Atoms/__tests__/DataEntry.test.ts index 9ecd6142915..a642b17faf0 100644 --- a/specifyweb/frontend/js_src/lib/components/Atoms/__tests__/DataEntry.test.ts +++ b/specifyweb/frontend/js_src/lib/components/Atoms/__tests__/DataEntry.test.ts @@ -68,7 +68,7 @@ snapshot(DataEntry.Footer, { children: 'Test' }); snapshot(DataEntry.SubForm, { children: 'Test' }); snapshot(DataEntry.SubFormHeader, { children: 'Test' }); snapshot(DataEntry.SubFormTitle, { children: 'Test' }); -snapshot(DataEntry.Add, { onClick: f.never }); +snapshot(DataEntry.Add, { onClick: f.never, enableShortcut: true }); snapshot(DataEntry.View, { onClick: f.never }); snapshot(DataEntry.Edit, { onClick: f.never }); snapshot(DataEntry.Clone, { onClick: f.never }); diff --git a/specifyweb/frontend/js_src/lib/components/Atoms/__tests__/__snapshots__/DataEntry.test.ts.snap b/specifyweb/frontend/js_src/lib/components/Atoms/__tests__/__snapshots__/DataEntry.test.ts.snap index 2724a1b1932..9c7e5e7af30 100644 --- a/specifyweb/frontend/js_src/lib/components/Atoms/__tests__/__snapshots__/DataEntry.test.ts.snap +++ b/specifyweb/frontend/js_src/lib/components/Atoms/__tests__/__snapshots__/DataEntry.test.ts.snap @@ -1,11 +1,11 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing -exports[` renders without errors 1`] = ` +exports[`Add renders without errors 1`] = `